Ver código fonte

docs(design): 会员体系重构设计-统一定价+优惠券发放/积分兑换

Xiaogang Liao 1 mês atrás
pai
commit
e47a2d4261

+ 231 - 0
docs/superpowers/specs/2026-08-05-membership-coupon-system-redesign.md

@@ -0,0 +1,231 @@
+# 会员体系重构:统一定价 + 优惠券发放/积分兑换设计
+
+- 日期:2026-08-05
+- 状态:待用户审查
+- 关联文档:`2026-07-09-family-membership-design.md`(原会员体系设计)、`2026-08-03-product-gift-membership-design.md`
+
+## 1. 背景与目标
+
+现有会员体系通过「会员价 / 折扣」体现等级差异:
+
+- 商品表 `products.member_price`(会员价字段)
+- 活动表 `activities.member_price`(会员价字段)
+- 系统配置 `member_discount_family` / `member_discount_premium`(bps 折扣)
+- `MembershipService.applyMemberDiscount()`、`ActivityDTO.resolveMemberPrice()`、`ProductDTO.resolveMemberPrice()`
+
+**本次调整目标:**
+
+1. **商品对所有用户统一定价**,不再按会员等级区分商品价格。
+2. 等级差异改由**优惠券数量**体现:不同级别会员拥有不同数量的优惠券。
+3. 优惠券来源两条路径:
+   - **发放**:加入会员时赠送 + 会员有效期内周期性补发;
+   - **积分兑换**:用户用积分自助兑换,兑换积分额按商品/券配置。
+4. 新增**家庭人口券**:每新增 1 名家庭成员,向该成员账户发放固定数量优惠券(默认 4 张)。
+
+## 2. 现状盘点
+
+| 模块 | 现状 | 本次变更 |
+|------|------|----------|
+| 会员等级 | `membership_levels`(FREE/FAMILY/PREMIUM/PROVIDER)+ `user.member_level` + `family_memberships` | 等级保留,权益从「价格折扣」改为「券数量」 |
+| 商品 | `products.price` + `products.member_price` | **停用 member_price**,统一 price |
+| 活动 | `activities.price` + `activities.member_price` | **保留**(活动继续支持会员价) |
+| 优惠券 | `coupon`(type=FIXED、value、min_spend、applicable_to、有效期、总量)+ `user_coupon`(AVAILABLE/USED);`CouponService`(claim/apply/markUsed/revert/issueToUser) | 扩展券类型、绑定商品、积分价、发放规则 |
+| 积分 | `family_members.total_points` + `system_points` + `points_log`;`PointsExchangeService`(兑换商品、日限次、最低积分) | 复用扣积分与限次校验 |
+| 家庭成员 | `family_members`(family_id/user_id/昵称/积分);`FamilyMemberService.addMember()` | 新增成员触发人口券发放 |
+
+## 3. 关键决策(已与用户确认)
+
+1. **券绑定具体商品**:每张券模板可关联一个商品(`product_id`,NULL=全场通用),兑换积分额在券模板上配置。
+2. **三种券类型**:满减券(FIXED)+ 无门槛券(CASH)+ 折扣券(DISCOUNT)。
+3. **发放时机**:加入送 + 周期性再发(会员有效期内按月/季度自动补发)。
+4. **家庭人口券**:发到新成员自己的账户(每成员默认 4 张)。
+5. **旧会员价**:商品停用、活动保留。
+6. **积分兑换入口**:积分兑换中心 + 商品详情页双入口。
+
+## 4. 数据模型
+
+### 4.1 `coupon` 表扩展(迁移 + schema.sql 同步)
+
+| 新字段 | 类型 | 说明 |
+|--------|------|------|
+| `coupon_type` | VARCHAR(16) | `FIXED` 满减 / `CASH` 无门槛 / `DISCOUNT` 折扣;默认 `FIXED`(兼容旧数据) |
+| `discount_rate` | INT | 折扣券专用,千分比(9000 = 9 折) |
+| `product_id` | BIGINT NULL | 绑定商品 ID;NULL = 全场通用 |
+| `points_price` | INT | 积分兑换价;0 = 不可积分兑换(仅发放可得) |
+| `grant_type` | VARCHAR(16) NULL | 发放规则类型:`JOIN` / `PERIODIC` / `POPULATION`;NULL = 不自动发放 |
+| `grant_level_code` | VARCHAR(32) NULL | JOIN/PERIODIC 对应的会员等级 |
+| `grant_period` | VARCHAR(16) NULL | PERIODIC 周期:`MONTHLY` / `QUARTERLY` |
+| `grant_quantity` | INT | 每次发放数量(POPULATION 默认 4) |
+
+> 说明:发放规则采用**独立列**而非 JSON(管理端表单友好、可索引、结构固定)。原方案 A 的 `grant_rule` JSON 拆为 4 列。
+
+现有 `coupon` 字段复用:`type`(FIXED 语义保留)、`value` 面额、`min_spend` 起用门槛、`applicable_to`(ALL/MEMBERSHIP)、有效期、`total_count`/`used_count`。
+
+### 4.2 `user_coupon` 表
+
+不变。发放走 `issueToUser()`(不校验"每人每券限 1 张");`claim()` 的防重逻辑仅保留给「领取中心」场景。
+
+### 4.3 新建 `coupon_grant_log` 发券流水表
+
+```sql
+CREATE TABLE coupon_grant_log (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '收券用户ID',
+    coupon_id BIGINT NOT NULL COMMENT '券模板ID',
+    grant_type VARCHAR(16) NOT NULL COMMENT 'JOIN/PERIODIC/POPULATION/EXCHANGE',
+    period VARCHAR(16) COMMENT '周期标识 YYYY-MM(PERIODIC 防重用)',
+    quantity INT DEFAULT 1 COMMENT '发放数量',
+    source VARCHAR(64) COMMENT '触发来源描述(订单号/成员ID等)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_grant (user_id, coupon_id, grant_type, period),
+    INDEX idx_coupon (coupon_id),
+    INDEX idx_user (user_id)
+) COMMENT='优惠券发放流水表';
+```
+
+- **PERIODIC 防重**:唯一键 `(user_id, coupon_id, grant_type, period)`,period 存 `YYYY-MM`。
+- **审计**:JOIN/POPULATION/EXCHANGE 也落库,管理端可查询。
+
+## 5. 发券引擎
+
+### 5.1 JOIN:开通/续费发放
+
+位置:`MembershipService.activateMembership()`(付费回调统一入口)+ `grantMembershipByGift()`(赠送)。
+
+逻辑:
+1. 激活会员后,查 `grant_type=JOIN AND grant_level_code=开通等级` 的券模板;
+2. 逐模板调 `couponService.issueToUser()` 发 `grant_quantity` 张;
+3. 写 `coupon_grant_log`(grant_type=JOIN,period=NULL)。
+
+**试用(trial)不发**:`activateTrialMembership()` 不触发发券,防零成本刷券。
+
+### 5.2 PERIODIC:周期性补发
+
+位置:新建 `CouponGrantTask`(Spring 定时任务,每日执行;参考现有 `MembershipScheduledTasks`)。
+
+逻辑:
+1. 查 `grant_type=PERIODIC` 的券模板;
+2. 对每个模板,查当前有效会员(`family_memberships`:payment_status=paid 且 end_date > now,取家庭创建者 user_id,等级匹配 `grant_level_code`);
+3. 计算应发周期:`grant_period=MONTHLY` 则当月首次执行时发放;用 `coupon_grant_log` 唯一键 `(user_id, coupon_id, PERIODIC, YYYY-MM)` 防重;
+4. 发放 + 落流水。
+
+执行规则:每日跑一次,只补当月(或本季度)未发的用户;已发则跳过(防重由唯一键保证,插入冲突即跳过)。
+
+### 5.3 POPULATION:新增家庭成员发放
+
+位置:`FamilyMemberService.addMember()`(成员添加成功后)。
+
+逻辑:
+1. 查 `grant_type=POPULATION` 的券模板(通常 1 个);
+2. 目标账户:
+   - 新成员关联到用户(`family_member.user_id != null`)→ 发到该用户;
+   - 手动添加无账号成员(`user_id == null`)→ 发到**家庭创建者**账户(保证"每成员 4 张"不因缺账号丢失);
+3. 发放 `grant_quantity` 张(默认 4)+ 落流水(grant_type=POPULATION,source=成员ID)。
+
+**移除成员不回收**:已发放券保留至自然过期。
+
+## 6. 积分兑换(双入口)
+
+### 6.1 兑换校验流程(共用)
+
+```
+校验券模板存在 + 启用(status 上架)
+校验 points_price > 0
+日限次(两层叠加):
+  全局:复用 exchange_daily_limit(默认 5 次/日)
+  单模板:coupon_exchange_daily_limit(新配置,默认 1 张/券模板/日)
+最低积分:复用 exchange_points_min(默认 100)
+扣积分:pointsService.deductSystemPoints(familyMemberId, pointsPrice, "积分兑换优惠券: 券名")
+发券:couponService.issueToUser()
+落流水:coupon_grant_log(grant_type=EXCHANGE)
+```
+
+- 目标 familyMemberId 解析复用 `PointsExchangeService.exchangeProduct()` 现有逻辑(childId 优先,否则按 userId 查)。
+- 兑换历史写入 `points_exchange_records`:`product_id = 券模板 id`、`product_name = 券名`、`product_type = coupon`(不新建 `points_exchange_products` 配置项,避免双份配置源;记录仅用于历史展示)。
+
+### 6.2 入口一:积分兑换中心
+
+`points_exchange_products` 表支持 `productType=coupon` 的配置项(管理端新增):配置时关联 `coupon_id`,`points_price` 取券模板值。兑换中心分类展示。
+
+> 实现简化:兑换中心直接展示 `coupon.points_price > 0` 且启用的券列表,不强制复用 `points_exchange_products` 配置(避免双份配置源)。管理端在「优惠券管理」统一维护。
+
+### 6.3 入口二:商品详情页
+
+商品详情接口返回该商品绑定的券列表(`product_id = 商品ID` 且 `points_price > 0`),前端展示「XX 积分兑换」按钮,点击调同一兑换接口。
+
+## 7. 商品统一价(停用商品侧会员价)
+
+| 位置 | 改动 |
+|------|------|
+| `ProductDTO.resolveMemberPrice()` | 直接返回 `price`(不再按等级计算) |
+| `ProductRecommendationService` | `memberPrice` 字段返回 `price` |
+| 小程序商品列表/详情页 | 移除「会员价」展示,仅显示统一价 |
+| 管理端商品表单 | 隐藏 `member_price` 输入框 |
+| `products.member_price` 字段 | 保留不删(数据兼容),不再参与定价 |
+| 活动侧(`ActivityDTO.resolveMemberPrice`、`MembershipController.applyMemberDiscount`、`member_discount_*`) | **完全不动** |
+
+## 8. 前端改动
+
+### 8.1 小程序(cfc-frontend)
+
+| 页面 | 改动 |
+|------|------|
+| 商品详情页 | 新增「积分兑换优惠券」区块:绑定该商品的券 + 兑换按钮 + 积分余额展示 |
+| 积分兑换中心 | 新增「优惠券」分类(points_price>0 的券) |
+| 我的优惠券 | 券卡片展示来源标签(会员赠送 / 积分兑换 / 家庭人口) |
+| 会员中心 | 展示等级权益:加入赠送 X 张 / 每周期发放 X 张 |
+| 家庭成员管理 | 新增成员成功提示「已发放 X 张优惠券」 |
+
+### 8.2 管理端(cfc-web)
+
+| 页面 | 改动 |
+|------|------|
+| 优惠券管理 | 表单新增:coupon_type、discount_rate、product_id(商品选择器)、points_price、grant_type、grant_level_code、grant_period、grant_quantity |
+| 商品管理 | 移除会员价输入 |
+| 新增「发券记录」页面 | 按用户/券/类型查询 `coupon_grant_log` |
+
+## 9. 边界情况(默认决策)
+
+| 场景 | 处理 |
+|------|------|
+| 成员被移除 | 已发券不回收,自然过期 |
+| 会员过期/退款 | JOIN/PERIODIC 已发券不回收;PERIODIC 停止补发 |
+| 手动添加成员(无账号) | 人口券发到家庭创建者账户 |
+| 券过期 | 现有 `listAvailable()` 已过滤;核销 `apply()` 校验有效期 |
+| 折扣券核销 | `CouponService.apply()` 扩展:DISCOUNT 抵扣 = `orderAmount * discount_rate / 10000`,上限 = orderAmount |
+| 无门槛券(CASH) | min_spend 忽略,直接抵扣 value |
+| 商品退款 | 复用 `couponService.revert()` |
+| 试用会员 | 不发 JOIN 券 |
+| 重复续费(同等级) | JOIN 券照发(续费视为一次发放) |
+
+## 10. 数据库迁移
+
+`DatabaseInitializer.runMigrations()` 追加(编号接现有最大编号):
+
+1. `coupon` 表添加 8 列(`coupon_type`/`discount_rate`/`product_id`/`points_price`/`grant_type`/`grant_level_code`/`grant_period`/`grant_quantity`),用 `ensureColumn` 逐列添加;
+2. 创建 `coupon_grant_log` 表(`CREATE TABLE IF NOT EXISTS`);
+3. schema.sql 同步:`coupon` CREATE TABLE 补列 + 追加 `coupon_grant_log` 建表语句。
+
+## 11. 测试策略
+
+- **单元测试**:
+  - `CouponService.apply()`:FIXED/CASH/DISCOUNT 三类型抵扣计算、满减门槛、折扣上限;
+  - 发券引擎:JOIN(含 trial 不发)、PERIODIC(跨月防重)、POPULATION(有/无 user_id 成员的目标账户);
+  - 积分兑换:积分不足、日限次、扣积分+发券+流水原子性。
+- **集成测试**:`FamilyMemberManagementFlowTest` 扩展新增成员发券断言。
+- **回归**:活动侧会员价 `applyMemberDiscount` 不受影响;商品下单全链路(统一价)。
+
+## 12. 实施范围
+
+### In scope
+
+- 后端:coupon 表扩展 + coupon_grant_log + 发券引擎(JOIN/PERIODIC/POPULATION)+ 积分兑换券 + 商品统一价 + 折扣券核销
+- 管理端:优惠券配置表单、发券记录页、商品表单调整
+- 小程序:商品详情兑换入口、兑换中心优惠券分类、我的券来源标签、会员权益展示、成员添加提示
+
+### Out of scope
+
+- 活动侧会员价调整(保留现状)
+- `products.member_price` 字段物理删除
+- 存量历史订单、历史券数据处理
+- 会员等级本身的定价/权益结构调整