瀏覽代碼

feat(family-cf): 家庭CF值体系完善

后端:
- 修复confirmReceive CF计算:改用totalAmount(原价)而非moneyAmount
- 新增FamilyPlatformBalanceController(balance/logs/exchange-coupon)
- FamilyPlatformPointsService新增exchangeCouponByCf方法

前端:
- 商品详情页展示'购此商品可得X CF值'
- 新增家庭CF值页面(余额+流水+兑换优惠券)
- 财富页增加'家庭CF值'入口
- api.js新增family-platform相关接口
iwt 1 月之前
父節點
當前提交
379a822a1a

+ 121 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyPlatformBalanceController.java

@@ -0,0 +1,121 @@
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FamilyPlatformBalance;
+import com.etotem.cfc.entity.FamilyPlatformBalanceLog;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.FamilyPlatformPointsService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+/**
+ * 家庭平台积分(CF值)接口
+ * 提供给小程序端查询余额、流水,以及用CF值兑换优惠券
+ */
+@RestController
+@RequestMapping("/api/family-platform")
+public class FamilyPlatformBalanceController {
+
+    @Resource
+    private FamilyPlatformPointsService familyPlatformPointsService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /** 获取当前用户归属家庭的CF值余额 */
+    @PostMapping("/balance")
+    public Result<Map<String, Object>> getBalance(@RequestAttribute("userId") Long userId) {
+        if (userId == null) return Result.error("未登录");
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            Map<String, Object> empty = new java.util.HashMap<>(); empty.put("familyId", null); empty.put("available", 0); empty.put("frozen", 0); return Result.success(empty);
+        }
+        return Result.success(familyPlatformPointsService.getBalance(user.getFamilyId()));
+    }
+
+    /** 获取当前用户归属家庭的CF值流水 */
+    @PostMapping("/logs")
+    public Result<Page<FamilyPlatformBalanceLog>> getLogs(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        if (userId == null) return Result.error("未登录");
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.success(new Page<>());
+        }
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        return Result.success(familyPlatformPointsService.getLogs(user.getFamilyId(), page, size));
+    }
+
+    /** 用家庭CF值兑换优惠券 */
+    @PostMapping("/exchange-coupon")
+    public Result<String> exchangeCoupon(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        if (userId == null) return Result.error("未登录");
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("请先加入家庭");
+        }
+        Long couponId = params.get("couponId") != null
+                ? Long.valueOf(params.get("couponId").toString()) : null;
+        if (couponId == null) return Result.error("couponId不能为空");
+        try {
+            familyPlatformPointsService.exchangeCouponByCf(user.getFamilyId(), couponId, userId);
+            // TODO: 实际发券逻辑待实现
+            return Result.success("兑换成功");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /** 管理员:查询指定家庭的CF值余额 */
+    @PostMapping("/admin/balance")
+    public Result<Map<String, Object>> adminBalance(@RequestBody Map<String, Object> params,
+                                                     @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) return Result.error("无权限");
+        Long familyId = params.get("familyId") != null
+                ? Long.valueOf(params.get("familyId").toString()) : null;
+        if (familyId == null) return Result.error("familyId不能为空");
+        return Result.success(familyPlatformPointsService.getBalance(familyId));
+    }
+
+    /** 管理员:查询指定家庭的CF值流水 */
+    @PostMapping("/admin/logs")
+    public Result<Page<FamilyPlatformBalanceLog>> adminLogs(@RequestBody Map<String, Object> params,
+                                                             @RequestAttribute("role") String role) {
+        if (!"admin".equals(role)) return Result.error("无权限");
+        Long familyId = params.get("familyId") != null
+                ? Long.valueOf(params.get("familyId").toString()) : null;
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        if (familyId == null) return Result.error("familyId不能为空");
+        return Result.success(familyPlatformPointsService.getLogs(familyId, page, size));
+    }
+
+    /** 管理员:手动调整家庭CF值 */
+    @PostMapping("/admin/adjust")
+    public Result<String> adminAdjust(@RequestBody Map<String, Object> params,
+                                       @RequestAttribute("role") String role,
+                                       @RequestAttribute("userId") Long adminId) {
+        if (!"admin".equals(role)) return Result.error("无权限");
+        Long familyId = params.get("familyId") != null
+                ? Long.valueOf(params.get("familyId").toString()) : null;
+        Integer amount = params.get("amount") != null
+                ? ((Number) params.get("amount")).intValue() : 0;
+        String remark = (String) params.get("remark");
+        if (familyId == null || amount == 0) return Result.error("参数错误");
+        try {
+            familyPlatformPointsService.adjust(familyId, amount,
+                    remark != null ? remark : "管理员" + adminId + "手动调整");
+            return Result.success("调整成功");
+        } catch (Exception e) {
+            return Result.error(e.getMessage());
+        }
+    }
+}

+ 24 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java

@@ -2,8 +2,10 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.entity.Coupon;
 import com.etotem.cfc.entity.FamilyPlatformBalance;
 import com.etotem.cfc.entity.FamilyPlatformBalanceLog;
+import com.etotem.cfc.mapper.CouponMapper;
 import com.etotem.cfc.mapper.FamilyPlatformBalanceLogMapper;
 import com.etotem.cfc.mapper.FamilyPlatformBalanceMapper;
 import org.springframework.stereotype.Service;
@@ -35,6 +37,9 @@ public class FamilyPlatformPointsService {
     @Resource
     private FamilyPlatformBalanceLogMapper logMapper;
 
+    @Resource
+    private CouponMapper couponMapper;
+
     /** 获取或创建家庭的积分池记录(悲观锁) */
     @Transactional
     public FamilyPlatformBalance getOrCreate(Long familyId) {
@@ -185,6 +190,25 @@ public class FamilyPlatformPointsService {
                 "adjust", null, remark);
     }
 
+    /**
+     * 用家庭CF值兑换优惠券(扣减家庭池,不扣个人积分)
+     */
+    @Transactional
+    public void exchangeCouponByCf(Long familyId, Long couponId, Long refUserId) {
+        Coupon coupon = couponMapper.selectById(couponId);
+        if (coupon == null) {
+            throw new RuntimeException("优惠券不存在");
+        }
+        Integer pointsPrice = coupon.getPointsPrice();
+        if (pointsPrice == null || pointsPrice <= 0) {
+            throw new RuntimeException("该优惠券不支持积分兑换");
+        }
+        // 扣除家庭池CF值
+        spend(familyId, pointsPrice, "coupon_exchange", couponId,
+              "兑换优惠券: " + (coupon.getName() != null ? coupon.getName() : "优惠券"));
+        // TODO: 实际发券逻辑(关联到 refUserId 的优惠券账户)
+    }
+
     /** 查询家庭 CF 值余额 */
     public Map<String, Object> getBalance(Long familyId) {
         FamilyPlatformBalance b = getOrCreate(familyId);

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

@@ -814,13 +814,13 @@ public class ProductOrderService {
         order.setUpdatedAt(new Date());
         orderMapper.updateById(order);
 
-        // 家庭池 CF 值发放:floor(实付金额(元)) × points_multiplier
+        // 家庭池 CF 值发放:floor(商品原价(元)) × points_multiplier
         try {
-            Integer moneyAmount = order.getMoneyAmount();
-            if (moneyAmount != null && moneyAmount > 0 && order.getFamilyId() != null) {
+            Integer totalAmount = order.getTotalAmount();
+            if (totalAmount != null && totalAmount > 0 && order.getFamilyId() != null) {
                 Product p = productMapper.selectById(order.getProductId());
                 if (p != null && p.getPointsMultiplier() != null) {
-                    int earned = (moneyAmount / 100) * p.getPointsMultiplier().intValue();
+                    int earned = (totalAmount / 100) * p.getPointsMultiplier().intValue();
                     if (earned > 0) {
                         familyPlatformPointsService.earn(
                                 order.getFamilyId(), earned,

+ 11 - 0
cfc-frontend/pages.json

@@ -1540,6 +1540,17 @@
           }
         }
       ]
+    },
+    {
+      "root": "pages/wealth-cf",
+      "pages": [
+        {
+          "path": "index",
+          "style": {
+            "navigationBarTitleText": "家庭CF值"
+          }
+        }
+      ]
     }
   ],
   "globalStyle": {

+ 28 - 0
cfc-frontend/pages/discover-detail/product-detail/product-detail.vue

@@ -30,6 +30,10 @@
           <text class="meta-item" v-if="product.domain">{{ domainLabel }}</text>
           <text class="meta-item">库存 {{ displayStock }}</text>
         </view>
+        <view v-if="product.pointsMultiplier && product.pointsMultiplier > 1" class="cf-hint-row">
+          <text class="cf-hint-icon">💎</text>
+          <text class="cf-hint-text">购此商品可得 {{ cfEarnedValue }} CF值</text>
+        </view>
         <view class="vendor-row">
           <text class="vendor-label">供应商:</text>
           <text class="vendor-name">{{ product.vendorName || '平台官方' }}</text>
@@ -143,6 +147,7 @@ export default {
       selectedSpecs: {},
       selectedSku: null,
       productCoupons: [],
+      cfEarnedValue: 0,
       // 会员身份(实时调 getMyMembership,FREE=非会员)
       isMember: false,
       memberLevel: 'FREE'
@@ -340,6 +345,10 @@ export default {
     loadProductCoupons: function(productId) {
       if (!productId) return
       var that = this
+      // 计算可得CF值:floor(原价/100) × pointsMultiplier
+      if (that.product && that.product.price && that.product.pointsMultiplier) {
+        that.cfEarnedValue = Math.floor(that.product.price / 100) * that.product.pointsMultiplier;
+      }
       getProductCoupons({ productId: productId }).then(function(res) {
         if (res.code === 200) {
           that.productCoupons = res.data || []
@@ -788,6 +797,25 @@ export default {
   font-size: 24rpx;
   color: #F97316;
 }
+/* CF值提示 */
+.cf-hint-row {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 24rpx;
+  background: linear-gradient(135deg, #667eea22, #764ba222);
+  border-radius: 16rpx;
+  margin-top: 12rpx;
+}
+.cf-hint-icon {
+  font-size: 28rpx;
+  margin-right: 8rpx;
+}
+.cf-hint-text {
+  font-size: 24rpx;
+  color: #667eea;
+  font-weight: 500;
+}
+
 /* 积分兑换优惠券 */
 .coupon-exchange-section {
   background: #fff;

+ 469 - 0
cfc-frontend/pages/wealth-cf/index.vue

@@ -0,0 +1,469 @@
+<template>
+  <view class="container">
+    <!-- 未登录 -->
+    <template v-if="!isLoggedIn">
+      <view class="empty-state">
+        <text class="empty-icon">💎</text>
+        <text class="empty-title">登录以查看家庭CF值</text>
+        <text class="empty-desc">CF值是家庭共享的平台积分,购买商品、参与活动即可获得</text>
+        <button class="login-btn" @click="goLogin">去登录</button>
+      </view>
+    </template>
+
+    <!-- 已登录 -->
+    <template v-else>
+      <!-- 余额卡片 -->
+      <view class="balance-card">
+        <view class="balance-label">家庭CF值余额</view>
+        <view class="balance-value">{{ balance.available }}</view>
+        <view class="balance-unit">CF值(1元=1积分)</view>
+        <view class="balance-row">
+          <view class="balance-item">
+            <text class="balance-item-label">累计获得</text>
+            <text class="balance-item-value">{{ balance.totalEarned }}</text>
+          </view>
+          <view class="balance-item">
+            <text class="balance-item-label">已消费</text>
+            <text class="balance-item-value">{{ balance.exchanged }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 操作按钮 -->
+      <view class="action-row">
+        <view class="action-btn" @click="refreshData">
+          <text class="action-icon">🔄</text>
+          <text class="action-text">刷新</text>
+        </view>
+        <view class="action-btn" @click="openExchangeModal" v-if="balance.available > 0">
+          <text class="action-icon">🎫</text>
+          <text class="action-text">兑换优惠券</text>
+        </view>
+      </view>
+
+      <!-- 流水列表 -->
+      <view class="section">
+        <view class="section-header">
+          <text class="section-title">流水记录</text>
+          <text class="section-more" @click="loadMoreLogs" v-if="hasMore">加载更多</text>
+        </view>
+        <view class="log-list" v-if="logs.length > 0">
+          <view class="log-item" v-for="log in logs" :key="log.id">
+            <view class="log-left">
+              <text class="log-type">{{ logTypeLabel(log.type) }}</text>
+              <text class="log-remark">{{ log.remark || log.refType }}</text>
+            </view>
+            <view class="log-right">
+              <text class="log-amount" :class="log.amount > 0 ? 'log-plus' : 'log-minus'">
+                {{ log.amount > 0 ? '+' : '' }}{{ log.amount }}
+              </text>
+              <text class="log-time">{{ formatTime(log.createdAt) }}</text>
+            </view>
+          </view>
+        </view>
+        <view class="empty-logs" v-else>
+          <text>暂无流水记录</text>
+        </view>
+      </view>
+    </template>
+
+    <!-- 兑换优惠券弹窗 -->
+    <view class="modal-mask" v-if="showExchangeModal" @click="showExchangeModal = false">
+      <view class="exchange-modal" @click.stop>
+        <view class="modal-header">
+          <text class="modal-title">兑换优惠券</text>
+          <text class="modal-close" @click="showExchangeModal = false">×</text>
+        </view>
+        <view class="modal-body">
+          <view class="coupon-list" v-if="exchangeableCoupons.length > 0">
+            <view class="coupon-item" v-for="coupon in exchangeableCoupons" :key="coupon.id" @click="doExchange(coupon)">
+              <view class="coupon-info">
+                <text class="coupon-name">{{ coupon.name }}</text>
+                <text class="coupon-desc">{{ couponDesc(coupon) }}</text>
+              </view>
+              <view class="coupon-price">{{ coupon.pointsPrice }} CF值</view>
+            </view>
+          </view>
+          <view class="empty-coupons" v-else>
+            <text>暂无可兑换优惠券</text>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getFamilyPlatformBalance, getFamilyPlatformLogs, getExchangeableCoupons, exchangeCouponByCf } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      isLoggedIn: false,
+      balance: { available: 0, totalEarned: 0, exchanged: 0, frozen: 0 },
+      logs: [],
+      page: 1,
+      size: 20,
+      hasMore: false,
+      showExchangeModal: false,
+      exchangeableCoupons: []
+    }
+  },
+  onLoad() {
+    this.checkLogin()
+    this.loadBalance()
+    this.loadLogs()
+  },
+  onShow() {
+    if (this.isLoggedIn) this.loadBalance()
+  },
+  methods: {
+    checkLogin() {
+      const token = uni.getStorageSync('token')
+      this.isLoggedIn = !!token
+    },
+    goLogin() {
+      uni.navigateTo({ url: '/pages/login/login' })
+    },
+    async loadBalance() {
+      try {
+        const res = await getFamilyPlatformBalance()
+        if (res.code === 200 && res.data) {
+          this.balance = res.data
+        }
+      } catch (e) {
+        console.error('加载余额失败', e)
+      }
+    },
+    async loadLogs() {
+      try {
+        const res = await getFamilyPlatformLogs({ page: this.page, size: this.size })
+        if (res.code === 200 && res.data) {
+          if (this.page === 1) {
+            this.logs = res.data.records || []
+          } else {
+            this.logs = this.logs.concat(res.data.records || [])
+          }
+          this.hasMore = this.logs.length < (res.data.total || 0)
+        }
+      } catch (e) {
+        console.error('加载流水失败', e)
+      }
+    },
+    loadMoreLogs() {
+      if (this.hasMore) {
+        this.page++
+        this.loadLogs()
+      }
+    },
+    refreshData() {
+      this.page = 1
+      this.loadBalance()
+      this.loadLogs()
+    },
+    async openExchangeModal() {
+      this.showExchangeModal = true
+      try {
+        const res = await getExchangeableCoupons()
+        if (res.code === 200) {
+          this.exchangeableCoupons = res.data || []
+        }
+      } catch (e) {
+        console.error('加载可兑换优惠券失败', e)
+      }
+    },
+    async doExchange(coupon) {
+      if (coupon.pointsPrice > this.balance.available) {
+        uni.showToast({ title: 'CF值不足', icon: 'none' })
+        return
+      }
+      uni.showLoading({ title: '兑换中...' })
+      try {
+        const res = await exchangeCouponByCf({ couponId: coupon.id })
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '兑换成功', icon: 'success' })
+          this.showExchangeModal = false
+          this.loadBalance()
+        } else {
+          uni.showToast({ title: res.message || '兑换失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        uni.showToast({ title: '兑换失败', icon: 'none' })
+      }
+    },
+    logTypeLabel(type) {
+      const map = {
+        earn: '获得', spend: '消费', freeze: '冻结',
+        unfreeze: '解冻', withdraw: '提现', adjust: '调整', migration: '迁移'
+      }
+      return map[type] || type
+    },
+    couponDesc(coupon) {
+      if (coupon.type === 'DISCOUNT') {
+        return (coupon.discountRate / 100).toFixed(1) + '折'
+      }
+      return '立减' + (coupon.value / 100).toFixed(2) + '元'
+    },
+    formatTime(dateStr) {
+      if (!dateStr) return ''
+      const d = new Date(dateStr)
+      const m = d.getMonth() + 1
+      const day = d.getDate()
+      const h = d.getHours()
+      const min = String(d.getMinutes()).padStart(2, '0')
+      return m + '/' + day + ' ' + h + ':' + min
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f6fa;
+  padding-bottom: 40rpx;
+}
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 200rpx 40rpx;
+}
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 20rpx;
+}
+.empty-title {
+  font-size: 32rpx;
+  color: #333;
+  font-weight: 600;
+  margin-bottom: 12rpx;
+}
+.empty-desc {
+  font-size: 26rpx;
+  color: #999;
+  margin-bottom: 40rpx;
+  text-align: center;
+}
+.login-btn {
+  width: 280rpx;
+  height: 80rpx;
+  line-height: 80rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+  border: none;
+}
+.balance-card {
+  margin: 30rpx;
+  padding: 40rpx 30rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  border-radius: 24rpx;
+  color: #fff;
+}
+.balance-label {
+  font-size: 26rpx;
+  opacity: 0.8;
+  margin-bottom: 10rpx;
+}
+.balance-value {
+  font-size: 72rpx;
+  font-weight: 700;
+  line-height: 1;
+  margin-bottom: 8rpx;
+}
+.balance-unit {
+  font-size: 22rpx;
+  opacity: 0.7;
+  margin-bottom: 30rpx;
+}
+.balance-row {
+  display: flex;
+  justify-content: space-around;
+  border-top: 1rpx solid rgba(255,255,255,0.2);
+  padding-top: 20rpx;
+}
+.balance-item {
+  text-align: center;
+}
+.balance-item-label {
+  font-size: 22rpx;
+  opacity: 0.7;
+  display: block;
+  margin-bottom: 6rpx;
+}
+.balance-item-value {
+  font-size: 28rpx;
+  font-weight: 600;
+}
+.action-row {
+  display: flex;
+  margin: 0 30rpx 20rpx;
+  gap: 20rpx;
+}
+.action-btn {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 24rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.action-icon {
+  font-size: 36rpx;
+  margin-bottom: 8rpx;
+}
+.action-text {
+  font-size: 26rpx;
+  color: #333;
+}
+.section {
+  margin: 20rpx 30rpx;
+}
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+.section-more {
+  font-size: 24rpx;
+  color: #667eea;
+}
+.log-list {
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+}
+.log-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.log-item:last-child {
+  border-bottom: none;
+}
+.log-left {
+  flex: 1;
+  margin-right: 20rpx;
+}
+.log-type {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+  margin-bottom: 4rpx;
+}
+.log-remark {
+  font-size: 22rpx;
+  color: #999;
+}
+.log-right {
+  text-align: right;
+}
+.log-amount {
+  font-size: 28rpx;
+  font-weight: 600;
+  display: block;
+}
+.log-plus { color: #52c41a; }
+.log-minus { color: #ff4d4f; }
+.log-time {
+  font-size: 20rpx;
+  color: #bbb;
+  margin-top: 4rpx;
+  display: block;
+}
+.empty-logs {
+  text-align: center;
+  padding: 60rpx;
+  color: #999;
+  font-size: 26rpx;
+  background: #fff;
+  border-radius: 16rpx;
+}
+.exchange-modal {
+  background: #fff;
+  border-radius: 24rpx 24rpx 0 0;
+  max-height: 80vh;
+  display: flex;
+  flex-direction: column;
+}
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.modal-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+.modal-close {
+  font-size: 40rpx;
+  color: #999;
+  line-height: 1;
+}
+.modal-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 20rpx 30rpx;
+}
+.coupon-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+.coupon-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx;
+  background: #f8f9ff;
+  border-radius: 16rpx;
+  border: 1rpx solid #e8eaff;
+}
+.coupon-info {
+  flex: 1;
+  margin-right: 20rpx;
+}
+.coupon-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+}
+.coupon-desc {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 4rpx;
+  display: block;
+}
+.coupon-price {
+  font-size: 26rpx;
+  color: #667eea;
+  font-weight: 600;
+  white-space: nowrap;
+}
+.empty-coupons {
+  text-align: center;
+  padding: 60rpx;
+  color: #999;
+  font-size: 26rpx;
+}
+</style>

+ 5 - 1
cfc-frontend/pages/wealth/index.vue

@@ -253,7 +253,8 @@ export default {
         { icon: '🛒', label: '购物车', action: 'goToCart' },
         { icon: '📋', label: '我的订单', action: 'goToOrders' },
         { icon: '🔄', label: '售后服务', action: 'goToAfterSales' },
-        { icon: '📊', label: '收益中心', action: 'goToIncome' }
+        { icon: '📊', label: '收益中心', action: 'goToIncome' },
+        { icon: '💎', label: '家庭CF值', action: 'goToFamilyCf' }
       ],
       children: [],
       familyMembersVisible: [],
@@ -460,6 +461,9 @@ export default {
     goToIncome() {
       uni.navigateTo({ url: '/pages/promotion/commission' })
     },
+    goToFamilyCf() {
+      uni.navigateTo({ url: '/pages/wealth-cf/index' })
+    },
     goToAchievements() {
       uni.navigateTo({ url: '/pages/rewards/badge' })
     },

+ 5 - 0
cfc-frontend/utils/api.js

@@ -2634,3 +2634,8 @@ export const getGrowthRecommendations = (category = 'health', limit = 3) =>
 
 // ===== 食材戒备匹配 =====
 export const getFoodCautionList = () => request('/api/food/recommendation/caution', 'POST', {})
+
+// ===== 家庭平台积分(CF值)=====
+export const getFamilyPlatformBalance = () => request('/api/family-platform/balance', 'POST', {})
+export const getFamilyPlatformLogs = (data) => request('/api/family-platform/logs', 'POST', data)
+export const exchangeCouponByCf = (data) => request('/api/family-platform/exchange-coupon', 'POST', data)

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-126baec79bae710b25bfb22eb091c4063603db92
+a3f21cb8401d84ed0a54fdcef5edb9d6ba2a73dc

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1016",
+  "version": "1.0.1017",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1016",
+      "version": "1.0.1017",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",