Browse Source

fix(cfc-backend): 补齐个人信息保存缺失的 idCard/mascot 映射

UpdateUserDTO 添加 mascot 字段,UserController.updateUser 补充 idCard 和 mascot 从 Map 到 DTO 的映射
User 2 months ago
parent
commit
152d89cede

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/UserController.java

@@ -91,6 +91,8 @@ public class UserController {
         if (params.containsKey("highestEducation")) dto.setHighestEducation((String) params.get("highestEducation"));
         if (params.containsKey("maritalStatus")) dto.setMaritalStatus((String) params.get("maritalStatus"));
         if (params.containsKey("familyRole")) dto.setFamilyRole((String) params.get("familyRole"));
+        if (params.containsKey("idCard")) dto.setIdCard((String) params.get("idCard"));
+        if (params.containsKey("mascot")) dto.setMascot((String) params.get("mascot"));
 
         boolean success = userService.updateUserInfo(userId, dto);
         if (success) {

+ 14 - 8
cfc-backend/src/main/java/com/etotem/cfc/controller/cart/CartController.java

@@ -4,6 +4,7 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.CartItemDTO;
 import com.etotem.cfc.service.CartService;
 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -20,8 +21,8 @@ public class CartController {
     private CartService cartService;
 
     @PostMapping("/add")
-    public Result<CartItemDTO> add(@RequestBody Map<String, Object> params) {
-        Long userId = Long.valueOf(params.get("userId").toString());
+    public Result<CartItemDTO> add(@RequestBody Map<String, Object> params,
+                                   @RequestAttribute("userId") Long userId) {
         Long productId = Long.valueOf(params.get("productId").toString());
         Integer quantity = params.containsKey("quantity") ? Integer.valueOf(params.get("quantity").toString()) : 1;
 
@@ -33,15 +34,14 @@ public class CartController {
     }
 
     @PostMapping("/list")
-    public Result<List<CartItemDTO>> list(@RequestBody Map<String, Object> params) {
-        Long userId = Long.valueOf(params.get("userId").toString());
+    public Result<List<CartItemDTO>> list(@RequestAttribute("userId") Long userId) {
         List<CartItemDTO> list = cartService.getCartList(userId);
         return Result.success(list);
     }
 
     @PostMapping("/update")
-    public Result<Void> update(@RequestBody Map<String, Object> params) {
-        Long userId = Long.valueOf(params.get("userId").toString());
+    public Result<Void> update(@RequestBody Map<String, Object> params,
+                               @RequestAttribute("userId") Long userId) {
         Long productId = Long.valueOf(params.get("productId").toString());
         Integer quantity = Integer.valueOf(params.get("quantity").toString());
 
@@ -50,11 +50,17 @@ public class CartController {
     }
 
     @PostMapping("/remove")
-    public Result<Void> remove(@RequestBody Map<String, Object> params) {
-        Long userId = Long.valueOf(params.get("userId").toString());
+    public Result<Void> remove(@RequestBody Map<String, Object> params,
+                               @RequestAttribute("userId") Long userId) {
         Long productId = Long.valueOf(params.get("productId").toString());
 
         cartService.removeItem(userId, productId);
         return Result.success();
     }
+
+    @PostMapping("/count")
+    public Result<Integer> count(@RequestAttribute("userId") Long userId) {
+        int count = cartService.getCartCount(userId);
+        return Result.success(count);
+    }
 }

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java

@@ -102,6 +102,12 @@ public class ProductOrderController {
         return orderService.applyRefund(orderNo, userId, reason);
     }
 
+    @PostMapping("/handlePaymentSuccess")
+    public Result<String> handlePaymentSuccess(@RequestBody Map<String, Object> params) {
+        String orderNo = (String) params.get("orderNo");
+        return orderService.handlePaymentSuccess(orderNo, null);
+    }
+
     @PostMapping("/notify")
     public Map<String, Object> notify(HttpServletRequest request) {
         try {

+ 1 - 3
cfc-backend/src/main/java/com/etotem/cfc/dto/CartItemDTO.java

@@ -2,8 +2,6 @@ package com.etotem.cfc.dto;
 
 import lombok.Data;
 
-import java.math.BigDecimal;
-
 @Data
 public class CartItemDTO {
     private Long id;
@@ -11,6 +9,6 @@ public class CartItemDTO {
     private Long productId;
     private Integer quantity;
     private String productName;
-    private BigDecimal productPrice;
+    private Integer unitPrice;
     private String coverImage;
 }

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/UpdateUserDTO.java

@@ -27,6 +27,10 @@ public class UpdateUserDTO {
     // 家长身份
     private String familyRole;
 
+<<<<<<< Updated upstream
     // AI助手形象: xibao/fubao
+=======
+    // AI助手形象: xibao(浠宝)/fubao(福宝)
+>>>>>>> Stashed changes
     private String mascot;
 }

+ 10 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/CartService.java

@@ -11,7 +11,6 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
-import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.List;
@@ -102,6 +101,14 @@ public class CartService {
         );
     }
 
+    public int getCartCount(Long userId) {
+        Long count = cartMapper.selectCount(
+            new LambdaQueryWrapper<Cart>()
+                .eq(Cart::getUserId, userId)
+        );
+        return count != null ? count.intValue() : 0;
+    }
+
     private CartItemDTO getCartItemDTO(Long userId, Long productId) {
         Cart cart = cartMapper.selectOne(
             new LambdaQueryWrapper<Cart>()
@@ -122,9 +129,8 @@ public class CartService {
         if (product != null) {
             dto.setProductName(product.getName());
             dto.setCoverImage(product.getCoverImage());
-            // Convert price from 分(integer) to 元(BigDecimal)
-            dto.setProductPrice(BigDecimal.valueOf(product.getPrice() != null ? product.getPrice() : 0)
-                .divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP));
+            // Price stored in 分(integer), send directly
+            dto.setUnitPrice(product.getPrice() != null ? product.getPrice() : 0);
         }
         return dto;
     }

+ 34 - 0
cfc-backend/src/main/resources/schema.sql

@@ -1090,6 +1090,8 @@ CREATE TABLE IF NOT EXISTS product_orders (
     points_used INT DEFAULT 0 COMMENT '使用的积分数量',
     points_cost INT DEFAULT 0 COMMENT '积分抵扣金额(分)',
     money_amount INT DEFAULT 0 COMMENT '现金支付金额(分)',
+    consignee_id BIGINT COMMENT '收货人ID',
+    purchase_info TEXT COMMENT '购买信息JSON: {id_card, hand_signature, ...}',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     INDEX idx_user_id (user_id),
@@ -2263,3 +2265,35 @@ CREATE TABLE IF NOT EXISTS supply_settlement_detail (
     KEY idx_settlement_id (settlement_id),
     KEY idx_order_id (order_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='结算明细';
+
+-- =============================================
+-- 收货人与购买信息采集
+-- =============================================
+
+CREATE TABLE IF NOT EXISTS consignees (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    user_id BIGINT NOT NULL COMMENT '所属用户ID',
+    name VARCHAR(100) NOT NULL COMMENT '收货人姓名',
+    phone VARCHAR(20) COMMENT '手机号',
+    id_card VARCHAR(20) COMMENT '身份证号',
+    hand_signature TEXT COMMENT '手签名(base64图片或URL)',
+    ethnicity VARCHAR(20) COMMENT '民族',
+    blood_type VARCHAR(20) COMMENT '血型',
+    address VARCHAR(500) COMMENT '收货地址',
+    is_default TINYINT(1) DEFAULT 0 COMMENT '是否默认收货人',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_user_id (user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收货人信息(含购买所需特殊信息)';
+
+CREATE TABLE IF NOT EXISTS product_purchase_fields (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    product_id BIGINT NOT NULL COMMENT '商品ID',
+    field_key VARCHAR(50) NOT NULL COMMENT '字段标识: name/phone/id_card/hand_signature/ethnicity/blood_type',
+    field_name VARCHAR(100) NOT NULL COMMENT '显示名: 收货人姓名/手机号/身份证号/手签名/民族/血型',
+    is_required TINYINT(1) DEFAULT 1 COMMENT '是否必填 1=是 0=否',
+    sort_order INT DEFAULT 0 COMMENT '排序',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    INDEX idx_product_id (product_id),
+    UNIQUE KEY uk_product_field (product_id, field_key)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品购买信息字段配置';

+ 332 - 170
cfc-frontend/pages/article-center/index.vue

@@ -1,36 +1,38 @@
 <template>
   <view class="ac-container">
     <!-- 自定义导航栏 -->
-    <view class="kc-nav">
-      <view class="kc-nav-left" @click="onBack">
-        <text class="kc-back-icon">←</text>
+    <view class="ac-nav">
+      <view class="ac-nav-left" @click="onBack">
+        <text class="ac-nav-back">&#x2190;</text>
       </view>
-      <view class="kc-nav-search" @click="focusSearch">
-        <text class="kc-search-icon">🔍</text>
-        <input
-          v-if="searchFocused || searchKeyword"
-          class="kc-search-input"
-          v-model="searchKeyword"
-          placeholder="搜索文章..."
-          confirm-type="search"
-          @confirm="onSearch"
-          @blur="onSearchBlur"
-        />
-        <text v-else class="kc-search-placeholder">搜索文章...</text>
-      </view>
-      <view class="kc-nav-right" @click="goEdit">
-        <text class="kc-add-btn">+</text>
+      <text class="ac-nav-title">知识中心</text>
+      <view class="ac-nav-right">
+        <text class="ac-nav-search-icon" @click="focusSearch">&#x1F50D;</text>
+        <text class="ac-nav-add" @click="goEdit">+</text>
       </view>
     </view>
 
-    <!-- Tab 导航 -->
+    <!-- 搜索栏(聚焦时展开) -->
+    <view v-if="searchFocused || searchKeyword" class="ac-search-bar">
+      <input
+        class="ac-search-input"
+        v-model="searchKeyword"
+        placeholder="搜索文章..."
+        confirm-type="search"
+        @confirm="onSearch"
+        @blur="onSearchBlur"
+      />
+      <text class="ac-search-cancel" @click="searchKeyword = ''; searchFocused = false; onSearch()">取消</text>
+    </view>
+
+    <!-- Tab 导航(药丸式) -->
     <view class="ac-tabs">
-      <scroll-view scroll-x enable-flex show-scrollbar="false" class="tab-scroll">
+      <scroll-view scroll-x show-scrollbar="false" class="ac-tab-scroll">
         <view
           v-for="tab in tabList"
           :key="tab.code"
-          :class="['tab-item', currentTab === tab.code ? 'active' : '']"
-          :style="currentTab === tab.code ? 'color:' + tab.color + ';border-bottom-color:' + tab.color : ''"
+          :class="['ac-tab', currentTab === tab.code ? 'active' : '']"
+          :style="currentTab === tab.code ? 'background:' + tab.color + ';color:#fff;border-color:' + tab.color : ''"
           @click="onTabChange(tab.code)"
         >
           {{ tab.name }}
@@ -38,79 +40,81 @@
       </scroll-view>
     </view>
 
-    <!-- 文章列表 -->
-    <scroll-view scroll-y class="ac-list" @scrolltolower="onLoadMore">
+    <!-- 文章列表区域 -->
+    <scroll-view scroll-y class="ac-scroll" @scrolltolower="onLoadMore">
       <!-- 骨架屏 -->
-      <view v-if="loading && articles.length === 0" class="ac-skeleton">
-        <view v-for="n in 3" :key="n" class="skeleton-card">
-          <view class="skeleton-cover"></view>
-          <view class="skeleton-body">
-            <view class="skeleton-line skeleton-title"></view>
-            <view class="skeleton-line skeleton-summary"></view>
-            <view class="skeleton-line skeleton-summary short"></view>
-            <view class="skeleton-line skeleton-meta"></view>
+      <view v-if="loading && articles.length === 0" class="ac-skeleton-wrap">
+        <view v-for="n in 3" :key="n" class="sk-card">
+          <view class="sk-cover"></view>
+          <view class="sk-body">
+            <view class="sk-row sk-tag"></view>
+            <view class="sk-row sk-title"></view>
+            <view class="sk-row sk-desc"></view>
+            <view class="sk-row sk-footer"></view>
           </view>
         </view>
       </view>
 
       <!-- 空状态 -->
       <view v-else-if="articles.length === 0" class="ac-empty">
-        <image class="empty-img" src="/static/empty-article.png" mode="aspectFit"></image>
-        <text class="empty-text">暂无文章</text>
+        <image class="ac-empty-img" src="/static/empty-article.png" mode="aspectFit"></image>
+        <text class="ac-empty-text">暂无文章</text>
       </view>
 
       <!-- 文章列表 -->
-      <view v-else class="article-list">
+      <view v-else class="ac-feed">
         <view
           v-for="item in articles"
           :key="item.id"
-          class="article-card"
+          class="ac-card"
+          hover-class="ac-card-pressed"
           @click="goDetail(item.id)"
         >
-          <image
-            class="card-cover"
-            :src="item.coverImage || '/static/default-article.png'"
-            mode="aspectFill"
-          />
-          <view class="card-body">
-            <view class="card-tags">
-              <text class="card-category">{{ item.categoryName || '' }}</text>
-              <text class="card-readtime">{{ item.readTime || 3 }}分钟</text>
-            </view>
-            <text class="card-title">{{ item.title }}</text>
-            <text class="card-summary">{{ item.summary || '' }}</text>
-            <view class="card-footer">
-              <text class="card-author">{{ item.author || '浠艾福' }}</text>
-              <text class="card-date">{{ formatDate(item.publishedAt) }}</text>
-              <text class="card-fav">收藏 {{ item.favCount || 0 }}</text>
-            </view>
-            <!-- 五维彩条 -->
-            <view v-if="item.relatedDimensions" class="card-dimensions">
+          <view class="ac-card-media">
+            <image
+              class="ac-card-cover"
+              :src="item.coverImage || '/static/default-article.png'"
+              mode="aspectFill"
+            />
+            <view class="ac-card-dimension-tags" v-if="item.relatedDimensions">
               <view
                 v-for="dim in parseDimensions(item.relatedDimensions)"
                 :key="dim.code"
-                class="dim-dot"
+                class="ac-card-dim-tag"
                 :style="'background:' + dim.color"
-              ></view>
+              >{{ dim.name }}</view>
+            </view>
+          </view>
+          <view class="ac-card-body">
+            <view class="ac-card-meta-top">
+              <text class="ac-card-category" v-if="item.categoryName">{{ item.categoryName }}</text>
+              <text class="ac-card-readtime">&#x1F4D6; {{ item.readTime || 3 }}分钟</text>
+            </view>
+            <text class="ac-card-title">{{ item.title }}</text>
+            <text class="ac-card-summary" v-if="item.summary">{{ item.summary }}</text>
+            <view class="ac-card-footer">
+              <text class="ac-card-author">{{ item.author || '浠艾福' }}</text>
+              <text class="ac-card-sep">|</text>
+              <text class="ac-card-date">{{ formatDate(item.publishedAt) }}</text>
+              <text class="ac-card-fav">&#x2B50; {{ item.favCount || 0 }}</text>
             </view>
           </view>
         </view>
       </view>
 
-      <!-- 加载更多 -->
-      <view v-if="loadingMore" class="loading-more">
-        <text class="loading-text">加载中...</text>
+      <!-- 加载更多 / 没有更多 -->
+      <view v-if="loadingMore" class="ac-loading-more">
+        <text class="ac-loading-text">加载中...</text>
       </view>
-      <view v-if="noMore && articles.length > 0" class="no-more">
-        <text class="no-more-text">— 没有更多了 —</text>
+      <view v-if="noMore && articles.length > 0" class="ac-no-more">
+        <text class="ac-no-more-text">— 没有更多了 —</text>
       </view>
-      <!-- 底部占位 -->
-      <view class="bottom-spacer"></view>
+      <view class="ac-bottom-gap"></view>
     </scroll-view>
 
     <!-- 浮动发布按钮 -->
     <view class="ac-fab" @click="goEdit">
-      <text class="fab-icon">+</text>
+      <text class="ac-fab-icon">+</text>
     </view>
   </view>
 </template>
@@ -254,167 +258,325 @@ export default {
 </script>
 
 <style scoped>
+/* ====================================
+   知识中心 — 重设计
+   白色导航 + 药丸Tab + 精装卡片
+   ==================================== */
+
 .ac-container {
   min-height: 100vh;
   background: #f5f7fa;
   position: relative;
 }
 
-/* 自定义导航栏 */
-.kc-nav {
+/* ---- 导航栏(白色简约) ---- */
+.ac-nav {
   display: flex;
   align-items: center;
-  padding: 80rpx 20rpx 16rpx;
-  background: linear-gradient(135deg, #F97316, #EA580C);
+  justify-content: space-between;
+  padding: 84rpx 28rpx 20rpx;
+  background: #fff;
   position: sticky;
   top: 0;
   z-index: 100;
 }
-.kc-nav-left {
-  width: 50rpx;
-  height: 50rpx;
+.ac-nav-left {
+  width: 60rpx;
+  height: 48rpx;
   display: flex;
   align-items: center;
-  justify-content: center;
+  justify-content: flex-start;
   flex-shrink: 0;
 }
-.kc-back-icon {
+.ac-nav-back {
   font-size: 36rpx;
-  color: #fff;
+  color: #333;
   font-weight: bold;
 }
-.kc-nav-search {
+.ac-nav-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #1a1a1a;
   flex: 1;
+  text-align: center;
+}
+.ac-nav-right {
   display: flex;
   align-items: center;
-  background: rgba(255,255,255,0.25);
-  border-radius: 32rpx;
-  padding: 10rpx 20rpx;
-  margin: 0 16rpx;
-}
-.kc-search-icon {
-  font-size: 24rpx;
-  margin-right: 10rpx;
+  gap: 20rpx;
   flex-shrink: 0;
 }
-.kc-search-placeholder {
-  font-size: 24rpx;
-  color: rgba(255,255,255,0.7);
-}
-.kc-search-input {
-  flex: 1;
-  font-size: 24rpx;
-  color: #fff;
-  background: transparent;
+.ac-nav-search-icon {
+  font-size: 32rpx;
+  color: #666;
 }
-.kc-search-input::placeholder {
-  color: rgba(255,255,255,0.7);
+.ac-nav-add {
+  font-size: 40rpx;
+  color: #F97316;
+  font-weight: 300;
+  line-height: 1;
 }
-.kc-nav-right {
-  width: 50rpx;
-  height: 50rpx;
+
+/* ---- 搜索展开条 ---- */
+.ac-search-bar {
   display: flex;
   align-items: center;
-  justify-content: center;
-  flex-shrink: 0;
+  padding: 12rpx 28rpx 16rpx;
+  background: #fff;
+  border-bottom: 1rpx solid #f0f0f0;
 }
-.kc-add-btn {
-  font-size: 48rpx;
-  color: #fff;
-  font-weight: 300;
-  line-height: 1;
+.ac-search-input {
+  flex: 1;
+  height: 60rpx;
+  background: #f5f5f5;
+  border-radius: 30rpx;
+  padding: 0 28rpx;
+  font-size: 26rpx;
+  color: #333;
+}
+.ac-search-cancel {
+  font-size: 26rpx;
+  color: #999;
+  margin-left: 16rpx;
+  flex-shrink: 0;
 }
 
+/* ---- Tab 药丸导航 ---- */
 .ac-tabs {
+  padding: 16rpx 28rpx;
   background: #fff;
-  padding: 16rpx 0 12rpx;
-  border-bottom: 1rpx solid #eee;
+  border-bottom: 1rpx solid #f0f0f0;
   position: sticky;
-  top: 0;
   z-index: 10;
 }
-.tab-scroll {
+.ac-tab-scroll {
   white-space: nowrap;
-  padding: 0 20rpx;
+  display: flex;
 }
-.tab-item {
-  display: inline-block;
-  padding: 8rpx 24rpx;
-  font-size: 26rpx;
+.ac-tab {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  height: 52rpx;
+  padding: 0 28rpx;
+  margin-right: 16rpx;
+  font-size: 24rpx;
   color: #666;
-  margin-right: 12rpx;
-  border-bottom: 3rpx solid transparent;
+  background: #f0f0f0;
+  border-radius: 26rpx;
+  border: 2rpx solid #f0f0f0;
   flex-shrink: 0;
+  transition: all 0.2s;
 }
-.tab-item.active {
-  font-weight: bold;
+.ac-tab.active {
+  font-weight: 600;
 }
-.ac-list {
-  height: calc(100vh - 230rpx);
+
+/* ---- 滚动列表区域 ---- */
+.ac-scroll {
+  height: calc(100vh - 200rpx);
 }
-/* 骨架屏 */
-.ac-skeleton { padding: 20rpx 24rpx; }
-.skeleton-card {
+
+/* ---- 骨架屏 ---- */
+.ac-skeleton-wrap { padding: 24rpx 28rpx; }
+.sk-card {
   background: #fff;
-  border-radius: 16rpx;
+  border-radius: 24rpx;
   overflow: hidden;
-  margin-bottom: 20rpx;
-}
-.skeleton-cover { height: 280rpx; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; }
-.skeleton-body { padding: 20rpx; }
-.skeleton-line { height: 24rpx; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; border-radius: 4rpx; margin-bottom: 12rpx; }
-.skeleton-title { width: 60%; }
-.skeleton-summary { width: 90%; }
-.skeleton-summary.short { width: 50%; }
-.skeleton-meta { width: 40%; height: 18rpx; }
-@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
-/* 空状态 */
-.ac-empty { display: flex; flex-direction: column; align-items: center; padding-top: 200rpx; }
-.empty-img { width: 200rpx; height: 200rpx; margin-bottom: 24rpx; }
-.empty-text { font-size: 28rpx; color: #999; }
-/* 文章卡片 */
-.article-list { padding: 20rpx 24rpx; }
-.article-card {
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+.sk-cover {
+  height: 340rpx;
+  background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
+  background-size: 200% 100%;
+  animation: sk-shimmer 1.5s infinite;
+}
+.sk-body { padding: 24rpx; }
+.sk-row {
+  height: 22rpx;
+  background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
+  background-size: 200% 100%;
+  border-radius: 6rpx;
+  margin-bottom: 14rpx;
+  animation: sk-shimmer 1.5s infinite;
+}
+.sk-tag { width: 20%; }
+.sk-title { width: 65%; }
+.sk-desc { width: 90%; }
+.sk-footer { width: 40%; height: 18rpx; }
+@keyframes sk-shimmer {
+  0% { background-position: -200% 0; }
+  100% { background-position: 200% 0; }
+}
+
+/* ---- 空状态 ---- */
+.ac-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 240rpx;
+}
+.ac-empty-img {
+  width: 200rpx;
+  height: 200rpx;
+  margin-bottom: 28rpx;
+}
+.ac-empty-text {
+  font-size: 28rpx;
+  color: #bbb;
+}
+
+/* ---- 文章卡片(新版) ---- */
+.ac-feed {
+  padding: 24rpx 28rpx;
+}
+.ac-card {
   background: #fff;
-  border-radius: 16rpx;
+  border-radius: 24rpx;
+  overflow: hidden;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  transition: transform 0.15s;
+}
+.ac-card-pressed {
+  transform: scale(0.97);
+  opacity: 0.9;
+}
+/* 封面 + 维度浮标 */
+.ac-card-media {
+  position: relative;
+  width: 100%;
+  overflow: hidden;
+}
+.ac-card-cover {
+  display: block;
+  width: 100%;
+  height: 340rpx;
+  background: #f0f0f0;
+}
+.ac-card-dimension-tags {
+  position: absolute;
+  bottom: 12rpx;
+  left: 16rpx;
+  display: flex;
+  gap: 8rpx;
+}
+.ac-card-dim-tag {
+  padding: 2rpx 14rpx;
+  border-radius: 12rpx;
+  font-size: 20rpx;
+  color: #fff;
+  font-weight: 500;
+  line-height: 1.4;
+}
+/* 正文区 */
+.ac-card-body {
+  padding: 24rpx 24rpx 28rpx;
+}
+.ac-card-meta-top {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+.ac-card-category {
+  font-size: 20rpx;
+  color: #F97316;
+  background: rgba(249,115,22,0.08);
+  padding: 2rpx 14rpx;
+  border-radius: 8rpx;
+  margin-right: 16rpx;
+}
+.ac-card-readtime {
+  font-size: 20rpx;
+  color: #aaa;
+}
+.ac-card-title {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1a1a1a;
+  line-height: 1.45;
+  margin-bottom: 10rpx;
   overflow: hidden;
-  margin-bottom: 20rpx;
-  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
-}
-.card-cover { width: 100%; height: 280rpx; background: #f0f0f0; }
-.card-body { padding: 20rpx; }
-.card-tags { display: flex; align-items: center; margin-bottom: 10rpx; }
-.card-category { font-size: 20rpx; color: #5B9BD5; background: rgba(91,155,213,0.1); padding: 2rpx 12rpx; border-radius: 8rpx; margin-right: 12rpx; }
-.card-readtime { font-size: 20rpx; color: #999; }
-.card-title { display: block; font-size: 28rpx; font-weight: bold; color: #333; margin-bottom: 8rpx; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
-.card-summary { display: block; font-size: 24rpx; color: #666; line-height: 1.5; margin-bottom: 12rpx; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
-.card-footer { display: flex; align-items: center; font-size: 20rpx; color: #999; }
-.card-author { margin-right: 16rpx; color: #5B9BD5; }
-.card-date { margin-right: 16rpx; }
-.card-fav { margin-left: auto; }
-/* 五维彩条 */
-.card-dimensions { display: flex; margin-top: 10rpx; gap: 8rpx; }
-.dim-dot { width: 20rpx; height: 20rpx; border-radius: 50%; }
-/* 加载更多 */
-.loading-more, .no-more { text-align: center; padding: 30rpx; }
-.loading-text { font-size: 24rpx; color: #999; }
-.no-more-text { font-size: 24rpx; color: #ccc; }
-.bottom-spacer { height: 120rpx; }
-/* 浮动按钮 */
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+.ac-card-summary {
+  display: block;
+  font-size: 24rpx;
+  color: #999;
+  line-height: 1.5;
+  margin-bottom: 16rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+.ac-card-footer {
+  display: flex;
+  align-items: center;
+  font-size: 20rpx;
+  color: #bbb;
+}
+.ac-card-author {
+  color: #888;
+}
+.ac-card-sep {
+  margin: 0 10rpx;
+  color: #ddd;
+}
+.ac-card-date {
+  flex: 1;
+}
+.ac-card-fav {
+  color: #ccc;
+}
+
+/* ---- 加载更多 / 没有更多 ---- */
+.ac-loading-more,
+.ac-no-more {
+  text-align: center;
+  padding: 30rpx;
+}
+.ac-loading-text {
+  font-size: 24rpx;
+  color: #bbb;
+}
+.ac-no-more-text {
+  font-size: 24rpx;
+  color: #ddd;
+}
+.ac-bottom-gap {
+  height: 140rpx;
+}
+
+/* ---- 浮动发布按钮 ---- */
 .ac-fab {
   position: fixed;
-  right: 40rpx;
+  right: 36rpx;
   bottom: 100rpx;
-  width: 100rpx;
-  height: 100rpx;
+  width: 96rpx;
+  height: 96rpx;
   background: linear-gradient(135deg, #F97316, #EA580C);
   color: #fff;
   border-radius: 50%;
   display: flex;
   align-items: center;
   justify-content: center;
-  box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.4);
+  box-shadow: 0 6rpx 20rpx rgba(249,115,22,0.35);
   z-index: 100;
 }
-.fab-icon { font-size: 48rpx; font-weight: bold; line-height: 1; }
+.ac-fab:active {
+  transform: scale(0.92);
+  opacity: 0.85;
+}
+.ac-fab-icon {
+  font-size: 48rpx;
+  font-weight: 300;
+  line-height: 1;
+}
 </style>

+ 81 - 45
cfc-frontend/pages/discover/product-detail/product-detail.vue

@@ -44,6 +44,11 @@
           <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
         </view>
         <view class="action-btns">
+          <view class="cart-icon-wrap" @click="onAddToCart">
+            <text class="cart-icon">🛒</text>
+            <text v-if="cartCount > 0" class="cart-badge">{{ cartCount > 99 ? '99+' : cartCount }}</text>
+          </view>
+          <button class="btn-cart" @click="onAddToCart">加入购物车</button>
           <button class="btn-buy" @click="onBuy">立即购买</button>
         </view>
       </view>
@@ -55,13 +60,15 @@
 </template>
 
 <script>
-import { productDetail, productOrderCreate, productOrderPay } from '@/utils/api.js'
+import config from '@/config.js'
+import { productDetail } from '@/utils/api.js'
 
 export default {
   data() {
     return {
       product: {},
-      loading: false
+      loading: false,
+      cartCount: 0
     }
   },
   computed: {
@@ -91,6 +98,9 @@ export default {
       this.loadDetail(options.id)
     }
   },
+  onShow() {
+    this.loadCartCount()
+  },
   methods: {
     loadDetail(id) {
       this.loading = true
@@ -103,54 +113,58 @@ export default {
         this.loading = false
       })
     },
-    onBuy() {
-      const userId = uni.getStorageSync('userId')
-      if (!userId) {
-        uni.navigateTo({ url: '/pages/login/login' })
-        return
-      }
-      uni.showLoading({ title: '创建订单...' })
-      productOrderCreate({
-        productId: this.product.id,
-        quantity: 1,
-        paymentMethod: 'wechat'
-      }).then(res => {
-        uni.hideLoading()
-        if (res.code === 200 && res.data) {
-          const orderNo = res.data.orderNo
-          uni.showModal({
-            title: '订单已创建',
-            content: '订单号:' + orderNo + ',确定支付吗?',
-            success: (confirm) => {
-              if (confirm.confirm) {
-                this.doPay(orderNo)
-              }
-            }
-          })
-        } else {
-          uni.showToast({ title: res.message || '创建订单失败', icon: 'none' })
+    loadCartCount() {
+      var that = this
+      uni.request({
+        url: config.api('/api/cart/count'),
+        method: 'POST',
+        data: {},
+        header: {
+          'Content-Type': 'application/json',
+          'Authorization': 'Bearer ' + uni.getStorageSync('token')
+        },
+        success: function(res) {
+          if (res.data && res.data.code === 200) {
+            that.cartCount = res.data.data || 0
+          }
         }
-      }).catch(() => {
-        uni.hideLoading()
-        uni.showToast({ title: '创建订单失败', icon: 'none' })
       })
     },
-    doPay(orderNo) {
-      uni.showLoading({ title: '支付中...' })
-      productOrderPay({ orderNo }).then(res => {
-        uni.hideLoading()
-        if (res.code === 200) {
-          uni.showToast({ title: '支付成功', icon: 'success' })
-          setTimeout(() => {
-            uni.navigateTo({ url: '/pages/shop/order-list/order-list' })
-          }, 1500)
-        } else {
-          uni.showToast({ title: res.message || '支付失败', icon: 'none' })
+    onAddToCart() {
+      var that = this
+      uni.request({
+        url: config.api('/api/cart/add'),
+        method: 'POST',
+        data: { productId: this.product.id, quantity: 1 },
+        header: {
+          'Content-Type': 'application/json',
+          'Authorization': 'Bearer ' + uni.getStorageSync('token')
+        },
+        success: function(res) {
+          if (res.data && res.data.code === 200) {
+            uni.showToast({ title: '已加入购物车', icon: 'success' })
+            that.loadCartCount()
+          } else {
+            uni.showToast({ title: res.data.message || '加入失败', icon: 'none' })
+          }
+        },
+        fail: function() {
+          uni.showToast({ title: '网络请求失败', icon: 'none' })
         }
-      }).catch(() => {
-        uni.hideLoading()
-        uni.showToast({ title: '支付失败', icon: 'none' })
       })
+    },
+    onBuy() {
+      if (!this.product.id) return
+      var url = '/pages/shop/checkout/checkout?productId=' + this.product.id
+        + '&productName=' + encodeURIComponent(this.product.name || '')
+        + '&price=' + (this.product.price || 0)
+        + '&coverImage=' + encodeURIComponent(this.product.coverImage || '')
+        + '&quantity=1'
+      uni.navigateTo({ url: url })
+    },
+    formatPriceWithSymbol(price) {
+      if (price === null || price === undefined) return '0.00'
+      return (Number(price) / 100).toFixed(2)
     }
   }
 }
@@ -324,4 +338,26 @@ export default {
 .btn-cart::after {
   border: none;
 }
+.cart-icon-wrap {
+  position: relative;
+  margin-right: 16rpx;
+  padding: 8rpx;
+}
+.cart-icon {
+  font-size: 44rpx;
+}
+.cart-badge {
+  position: absolute;
+  top: -4rpx;
+  right: -8rpx;
+  min-width: 32rpx;
+  height: 32rpx;
+  line-height: 32rpx;
+  text-align: center;
+  background: #F97316;
+  color: #fff;
+  font-size: 20rpx;
+  border-radius: 16rpx;
+  padding: 0 6rpx;
+}
 </style>

+ 7 - 2
cfc-frontend/pages/login/login.vue

@@ -232,8 +232,13 @@ export default {
       if (this.redirectUrl) {
         const redirect = this.redirectUrl
         this.redirectUrl = ''
-        // 使用 redirectTo 支持非 tabBar 页面,避免 switchTab 无法跳转到 discover 等非 tabBar 页
-        uni.redirectTo({ url: redirect })
+        // 注意: redirectTo 不支持跳转 tabBar 页面,必须用 switchTab
+        var tabBarPages = ['/pages/index/index', '/pages/body/index', '/pages/wisdom/index', '/pages/mind/index', '/pages/profile/profile']
+        if (tabBarPages.indexOf(redirect) !== -1) {
+          uni.switchTab({ url: redirect })
+        } else {
+          uni.redirectTo({ url: redirect })
+        }
       } else {
         uni.switchTab({ url: '/pages/index/index' })
       }

+ 1 - 1
cfc-frontend/pages/profile/components/ProfileHeader.vue

@@ -14,7 +14,7 @@
     </view>
 
     <view class="user-card">
-      <view class="avatar">👤</view>
+      <view class="avatar" @click="$emit('avatar-click')">👤</view>
       <view class="user-info">
         <view class="nickname">{{ nickname || '未设置昵称' }}</view>
         <view class="role">{{ role === 'parent' ? '家长模式' : (role === 'child' ? '孩子模式' : '成长规划师模式') }}</view>

+ 1 - 0
cfc-frontend/pages/shop/cart/cart.vue

@@ -140,6 +140,7 @@ export default {
             var list = res.data.data || []
             for (var i = 0; i < list.length; i++) {
               list[i].checked = true
+              list[i].subtotal = list[i].unitPrice * list[i].quantity
             }
             that.items = list
           }

+ 10 - 0
cfc-frontend/pages/shop/payment/payment.vue

@@ -167,6 +167,16 @@ export default {
                 signType: params.signType || 'RSA',
                 paySign: params.paySign,
                 success: function() {
+                  // Notify backend to update order status
+                  uni.request({
+                    url: config.api('/api/product/order/handlePaymentSuccess'),
+                    method: 'POST',
+                    data: { orderNo: that.orderNo },
+                    header: {
+                      'Content-Type': 'application/json',
+                      'Authorization': 'Bearer ' + uni.getStorageSync('token')
+                    }
+                  })
                   uni.showToast({ title: '支付成功', icon: 'success' })
                   setTimeout(function() {
                     uni.redirectTo({

+ 2 - 2
cfc-frontend/pages/user-edit/user-edit.vue

@@ -404,8 +404,8 @@ export default {
 					this.form.realName = userData.realName || ''
 					this.form.gender = userData.gender || ''
 					this.form.idCard = userData.idCard || ''
-					// 生日只取日期部分,去掉时间
-					this.form.birthday = userData.birthday ? userData.birthday.split(' ')[0] : ''
+				// 生日只取日期部分,去掉时间和 ISO 格式的 T 分隔符
+				this.form.birthday = userData.birthday ? String(userData.birthday).split(/[T ]/)[0] : ''
 					this.form.avatar = userData.avatar || ''
 					this.form.role = userData.role || ''
 					this.form.phone = userData.phone || ''