2026-08-05-membership-coupon-system-redesign.md 12 KB

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

  • 日期: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_logPointsExchangeService(兑换商品、日限次、最低积分) 复用扣积分与限次校验
家庭成员 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 发券流水表

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_recordsproduct_id = 券模板 idproduct_name = 券名product_type = coupon(不新建 points_exchange_products 配置项,避免双份配置源;记录仅用于历史展示)。

6.2 入口一:积分兑换中心

points_exchange_products 表支持 productType=coupon 的配置项(管理端新增):配置时关联 coupon_idpoints_price 取券模板值。兑换中心分类展示。

实现简化:兑换中心直接展示 coupon.points_price > 0 且启用的券列表,不强制复用 points_exchange_products 配置(避免双份配置源)。管理端在「优惠券管理」统一维护。

6.3 入口二:商品详情页

商品详情接口返回该商品绑定的券列表(product_id = 商品IDpoints_price > 0),前端展示「XX 积分兑换」按钮,点击调同一兑换接口。

7. 商品统一价(停用商品侧会员价)

位置 改动
ProductDTO.resolveMemberPrice() 直接返回 price(不再按等级计算)
ProductRecommendationService memberPrice 字段返回 price
小程序商品列表/详情页 移除「会员价」展示,仅显示统一价
管理端商品表单 隐藏 member_price 输入框
products.member_price 字段 保留不删(数据兼容),不再参与定价
活动侧(ActivityDTO.resolveMemberPriceMembershipController.applyMemberDiscountmember_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 字段物理删除
  • 存量历史订单、历史券数据处理
  • 会员等级本身的定价/权益结构调整