# 优惠券全链路改为家庭维度 — 实施计划 **设计稿:** `specs/2026-08-31-coupon-family-based-design.md` **状态:** 待执行 **预计工时:** ~2h(后端主导,前端轻量) --- ## Task 1 — 数据库迁移 + schema.sql(迁移 269) **文件:** `DatabaseInitializer.java`, `schema.sql` **验证:** `mvn clean compile` ``` // 迁移269: 创建 family_coupon + family_coupon_grant_log 表 // (优惠券全链路改为家庭维度:2026-08-31) ``` **Schema.sql 末尾追加**(在 `coupon_grant_log` CREATE 语句之后): ```sql CREATE TABLE IF NOT EXISTS family_coupon ( id BIGINT AUTO_INCREMENT PRIMARY KEY, family_id BIGINT NOT NULL COMMENT '所属家庭ID', coupon_id BIGINT NOT NULL COMMENT '券模板ID', status VARCHAR(16) DEFAULT 'AVAILABLE' COMMENT 'AVAILABLE/USED', received_at DATETIME DEFAULT CURRENT_TIMESTAMP, used_at DATETIME, order_id BIGINT COMMENT '核销时写入的订单号', INDEX idx_family_coupon (family_id, coupon_id), INDEX idx_status (status), INDEX idx_order_id (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭优惠券表'; CREATE TABLE IF NOT EXISTS family_coupon_grant_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, family_id BIGINT NOT NULL COMMENT '受赠家庭ID', coupon_id BIGINT NOT NULL COMMENT '券模板ID', grant_type VARCHAR(16) NOT NULL COMMENT 'JOIN/PERIODIC/POPULATION/EXCHANGE/CF_EXCHANGE', period VARCHAR(16) COMMENT '周期标识(YYYY-MM或YYYY-Qn),PERIODIC防重用', quantity INT DEFAULT 1 COMMENT '发放数量', source VARCHAR(64) COMMENT '触发来源(订单号/成员ID等)', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_family_grant (family_id, coupon_id, grant_type, period), INDEX idx_coupon (coupon_id), INDEX idx_family (family_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭优惠券发放流水表'; ``` --- ## Task 2 — Entity + Mapper(4个新文件) | 文件 | 包 | 说明 | |------|----|------| | `entity/FamilyCoupon.java` | `com.etotem.cfc.entity` | `@TableName("family_coupon")` | | `entity/FamilyCouponGrantLog.java` | `com.etotem.cfc.entity` | `@TableName("family_coupon_grant_log")` | | `mapper/FamilyCouponMapper.java` | `com.etotem.cfc.mapper` | 继承 `BaseMapper`,加 `@Mapper` | | `mapper/FamilyCouponGrantLogMapper.java` | `com.etotem.cfc.mapper` | 同上 | --- ## Task 3 — CouponService 重构(核心) **文件:** `service/CouponService.java` **改动要点:** 1. 注入 `FamilyCouponMapper`, `FamilyCouponGrantLogMapper`, `UserMapper` 2. 保留所有公开方法签名不变 3. 内部实现全部替换为读 `family_coupon` 4. 新增私有方法 `getFamilyId(Long userId)` 做 userId→familyId 转换 5. 新增 public 方法(供外部调用方用): - `grantFamily(Long familyId, Long couponId, String grantType, String period, String source)` - `grantFamilyJoinCoupons(Long familyId, String levelCode, String source)` - `grantFamilyPopulationCoupons(Long familyId, Long memberId)` - `listActiveMemberFamilyIds(String levelCode)` **不删的旧方法**(保留供降级兼容,但停止使用): - `issueToUser` — 标记 `@Deprecated`,不动实现 --- ## Task 4 — 6 条发放路径改写 | # | 文件 | 改动 | |---|------|------| | 1 | `controller/admin/AdminCouponController.java` | `/issue` 参数改为 `familyIds`;新增 `/issue-family` | | 2 | `service/MembershipService.java` | `grantJoinCoupons(adminUserId,...)` → `grantFamilyJoinCoupons(order.getFamilyId(),...)` | | 3 | `service/FamilyMemberService.java` | `grantPopulationCoupons(targetUserId,...)` → `grantFamilyPopulationCoupons(familyId, member.getId())` | | 4 | `task/CouponGrantTask.java` | 改为调用 `listActiveMemberFamilyIds` + `grantFamily` | | 5 | `service/PointsExchangeService.java` | 取 `familyMemberMapper.selectOne(userId).getFamilyId()`,调 `grantFamily` | | 6 | `service/FamilyPlatformPointsService.java` | 直接调 `grantFamily(familyId, ...)` | --- ## Task 5 — CouponController(小程序端,内部改实现) **文件:** `controller/CouponController.java` 不改路由、不改响应字段。内部 `userId` 先解析 `userMapper.selectById(userId).getFamilyId()`,再调 CouponService 新方法。 `apply` 方法中增加归属校验。 --- ## Task 6 — 管理端前端(cfc-web) | 文件 | 改动 | |------|------| | `api/coupon.js` | 新增 `issueFamilyCoupon`、`getFamilyCouponGrantLog` | | `views/Families.vue` | 下拉菜单加「发放优惠券」按钮 + 弹窗 | | `views/admin/CouponManagement.vue` | 批量发放弹窗改为家庭ID列表 | | `views/admin/CouponGrantLog.vue` | 搜索框加「家庭ID」;列表显示家庭名 | --- ## Task 7 — API_REFERENCE.md 更新 追加 `/api/admin/coupon/issue-family` 和 `/api/coupon/*` 的家庭维度说明。 --- ## Task 8 — 编译验证 + 提交 ```bash cd cfc-backend && mvn clean compile git add -A && git commit && git push ```