|
|
@@ -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` 字段物理删除
|
|
|
+- 存量历史订单、历史券数据处理
|
|
|
+- 会员等级本身的定价/权益结构调整
|