2026-08-09-lifetime-membership-design.md 12 KB

平台终身会员设计(LIFETIME)

日期:2026-08-09 状态:已获用户批准(方案 A + 规则确认)

1. 背景与目标

平台现有会员等级体系(FREE/FAMILY/PREMIUM/PROVIDER)为订阅制,到期需续费。现新增平台终身会员(LIFETIME):用户年度在平台消费满 100 万,或推广团队销售满 200 万,经管理员审核后升级为终身会员,永久享受 FAMILY 会员全部权益,无需再续费。

同时修正现存价格来源矛盾:getPricingPlans() 目前从 sys_config 取价,而 membership_levels 表的价格字段未被真正使用。本次将会员费用(含原价)统一迁移到 membership_levels(membership-center 管理端的数据源)。

已确认需求

  1. 方案 A:LIFETIME 等级 + 实时聚合统计 + 管理员审核升级
  2. 消费口径:全部订单类型(商城/测评/套餐/会员/活动),仅 status='paid' 的已支付订单,滚动 365 天窗口
  3. 销售口径:推广团队销售额(通过 referralCode 关联的下线订单总额),同样滚动 365 天
  4. 达成方式:系统计算达标候选 → 管理员在管理端审核确认 → 升级为 LIFETIME
  5. 权益:终身的 FAMILY 会员(features 配置同 FAMILY,永久有效免续费)
  6. 费用迁移:membership_levels 增加原价字段;getPricingPlans() 改读此表;sys_config 旧键保留但不再引用

范围外(明确不做)

  • 不做终身会员自动判定(必须管理员审核)
  • 不做终身会员的自动续费/扣费(永久有效)
  • 不删除 sys_config 中既有 member_fee_* 键(仅代码停止引用,避免影响其他读取方)
  • 不新增支付方式

2. 数据库变更(membership_levels 表)

2.1 新增原价字段(迁移 203)

ALTER TABLE membership_levels
  ADD COLUMN original_price_monthly INT COMMENT '原价-月费(分)',
  ADD COLUMN original_price_quarterly INT COMMENT '原价-季费(分)',
  ADD COLUMN original_price_yearly INT COMMENT '原价-年费(分)';
  • DatabaseInitializer 迁移 203(幂等,ensureColumn 模式)+ schema.sql 同步
  • 同步数据:将 sys_config 现值幂等写入对应等级行
    • FAMILY:original_price_yearlymember_fee_family_original_yearly(兜底 member_fee_family_original,默认 199900)
    • PREMIUM:original_price_yearlymember_fee_premium_original(默认 1599900);original_price_monthlymember_fee_premium_original_monthly(默认 159900)
    • 现价字段 price_monthly/quarterly/yearly 同步用 sys_config 现值补齐(FAMILY 年费 131400、PREMIUM 年费 1316800 等,幂等 UPDATE 仅当表内为 0/NULL 时)

2.2 新增 LIFETIME 等级行(迁移 204)

INSERT IGNORE INTO membership_levels
  (level_code, level_name, level_desc, price_monthly, price_quarterly, price_yearly,
   original_price_monthly, original_price_quarterly, original_price_yearly,
   features, max_children, max_tasks_per_day, ai_review_enabled, priority_support, teacher_consultation)
VALUES
  ('LIFETIME', '终身会员', '终身家庭会员权益,永久有效无需续费', 0, 0, 0, 0, 0, 0,
   '["free_activities","free_courses","full_price_purchase","referral_commission","discount_purchase","member_activities","create_family","invite_family","purchase_commission"]',
   6, 20, 1, 1, 1);
  • features 与 FAMILY 一致(终身 = FAMILY 权益)
  • 价格全 0(不可购买,仅管理员授予)
  • DatabaseInitializer 迁移 204 + schema.sql 同步

3. 费用来源切换(getPricingPlans() 改造)

MembershipController.getPricingPlans()(L317)改为统一从 membership_levels 表读取价格与显示字段,不再读 sys_config:

// 每个 level 的 plan 字段:
plan.put("levelCode", level.getLevelCode());
plan.put("levelName", level.getLevelName());
plan.put("levelDesc", level.getLevelDesc());
plan.put("recommended", "FAMILY".equals(level.getLevelCode()));
plan.put("yearly", level.getPriceYearly());          // 不再从 sys_config 读
plan.put("monthly", level.getPriceMonthly());
plan.put("quarterly", level.getPriceQuarterly());
plan.put("originalYearly", level.getOriginalPriceYearly());
plan.put("originalMonthly", level.getOriginalPriceMonthly());
plan.put("originalQuarterly", level.getOriginalPriceQuarterly());

关键约束:

  • 过滤 LIFETIME:终身会员不可购买,getPricingPlans() 跳过 levelCode='LIFETIME' 的行(不返回给小程序购买列表)
  • 返回结构不变(levelCode/levelName/levelDesc/recommended/yearly/monthly/quarterly/originalYearly/originalMonthly/originalQuarterly),前端 pay/upgrade 页无感知
  • MembershipLevel entity 增加 originalPriceMonthly/Quarterly/Yearly 三个字段(对应表列)

数据兜底

表内价格字段为 NULL/0 时,plan 返回 null(前端 showOriginalPrice() 已有 original > current*100 才展示划线的逻辑,NULL/0 自然不显示划线)。

4. 终身会员判定逻辑(天然生效,零改动核心)

现有逻辑已满足 LIFETIME 语义,无需修改

组件 现有行为 LIFETIME 效果
MembershipService.getMemberLevel() (L767) "FAMILY"memberExpireTime 非空才检查过期 LIFETIME 跳过过期检查 → 永久有效 ✅
MembershipService.canUseFeature() (L166) FREE/FAMILY/PREMIUM 分支后兜底 return true LIFETIME 落到兜底 → 全功能放行 ✅
MembershipService.hasPermission() (L228) membership_levels 表 features LIFETIME 行 features 同 FAMILY → 权限一致 ✅
getMyMembership (MembershipController L65) 返回顶层 memberLevel 返回 LIFETIME,前端可识别 ✅

授予写入路径(管理员审核通过时)

// 升级目标用户 = 家庭创建者(与消费归属口径一致)
User adminUser = ...;  // family.getCreatorId()
String fromLevel = adminUser.getMemberLevel() != null ? adminUser.getMemberLevel() : "FREE";
adminUser.setMemberLevel("LIFETIME");
adminUser.setMemberExpireTime(null);   // NULL = 永久有效
adminUser.setUpdatedAt(new Date());
userMapper.updateById(adminUser);

// 升级记录 upgradeType='lifetime'
MemberUpgradeRecord record = new MemberUpgradeRecord();
record.setUserId(adminUserId);
record.setFromLevel(fromLevel);
record.setToLevel("LIFETIME");
record.setUpgradeType("lifetime");
record.setRemark("平台终身会员:消费达标/销售达标,管理员审核授予");
memberUpgradeRecordMapper.insert(record);

MemberUpgradeRecord.upgradeType 现有枚举 pay/consumption/admin 增加 lifetime 值(字符串字段,无 DB 约束,直接复用)。

5. 消费/销售统计(实时聚合,滚动 365 天)

5.1 消费统计(每用户)

5 张订单表 UNION 聚合,过滤 status='paid'paid_at >= now-365d,按用户归属求和:

订单表 用户字段 金额字段 状态字段
product_orders buyer_id total_amount status='paid'
assessment_orders user_id amount status='paid'
package_orders user_id price status='paid'
member_subscription_order family_id → 家庭创建者 amount status='paid'
activity_orders user_id total_amount status='paid'

member_subscription_order 无用户字段,通过 family_id 关联 family.creator_id 归属到用户(与 getFamilyMembership 口径一致)。

阈值:消费 ≥ 1,000,000 元(内部 100_000_000 分)。

5.2 销售统计(每用户 = 推广团队销售额)

  • 找出该用户 referralCode 关联的所有下线用户(user.referrer_id = 该用户.id,一级推广关系)
  • 汇总这些下线的全部订单消费总额(复用 5.1 的聚合逻辑,按下线 buyer 归属到该用户)
  • 阈值:销售 ≥ 2,000,000 元(内部 200_000_000 分)

5.3 候选查询实现

管理端接口 POST /api/admin/membership/lifetime-candidates

  • 对全部用户(或指定筛选:时间/关键字),计算每个用户的消费额 + 销售额
  • 返回达标候选列表:userId / 昵称 / 家庭 / 消费额 / 销售额 / 达标类型(consumption|sales|both)
  • 实现方式:Mapper XML 写 SQL(UNION ALL 5 表 + LEFT JOIN family/user + 汇总),或 Service 内分表查询汇总(数据量可控,推荐 SQL 聚合)
  • 性能:滚动 365 天窗口,paid_at 建索引即可;低频管理操作,实时计算可接受

6. 管理端审核(MembershipCenter.vue)

6.1 新增后端接口(AdminController)

// 查询终身会员达标候选(分页)
@PostMapping("/membership/lifetime-candidates")
public Result<Page<Map<String, Object>>> lifetimeCandidates(@RequestBody Map<String, Object> params)

// 审核授予终身会员
@PostMapping("/membership/lifetime-grant")
public Result<Void> lifetimeGrant(@RequestBody Map<String, Object> params)  // { userId, remark }

lifetime-grant 校验:用户存在、未是 LIFETIME;写入 user.memberLevel='LIFETIME' + memberExpireTime=null + 升级记录(见 §4);返回成功后管理端刷新列表。

6.2 前端页面(MembershipCenter.vue)

  • 新增「终身会员管理」卡片:
    • 候选列表:表格(用户/消费额/销售额/达标类型),每行「授予」按钮 + 确认弹窗(提示授予后永久有效)
    • 已授予列表/api/admin/membership/upgrade-records 过滤 toLevel='LIFETIME' 展示(复用现有接口)
  • 概览统计卡新增「终身会员数」(overview 增加 lifetimeCount,按 user.memberLevel='LIFETIME' 计数)

7. 小程序端展示

7.1 upgrade.vue(套餐选择页)

  • loadData() 已用 memberLevel !== 'FREE' 判断非免费 → LIFETIME 自然进入"非免费"分支,显示会员状态卡 ✅
  • 新增 LIFETIME 分支处理
    • 状态卡到期时间:LIFETIME 显示「永久有效」(membership.memberExpireTime 为 null 时)
    • 续费卡片(v-else 分支):LIFETIME 隐藏「续费会员」卡片与「下一步」按钮,改为显示「您已是终身会员」提示文案
  • getLevelName() map 增加 LIFETIME: '终身会员'

7.2 index.vue(会员中心)与 result.vue

  • index.vue:等级名称显示识别 LIFETIME(如 getLevelName 有对应映射则自然生效);若有「立即续费/升级」入口需对 LIFETIME 隐藏
  • result.vue:不涉及(终身会员非支付链路)

7.3 benefits.vue(我的权益)

  • 终身会员权益列表:LIFETIME 行 features 同 FAMILY,权益接口走表配置自然返回,无需改动

8. 验收标准

后端(mvn clean compile 通过)

  • MembershipLevel entity 含 originalPrice* 3 字段;membership_levels 表含 3 原价列 + LIFETIME 行(迁移 203/204,幂等)
  • getPricingPlans()membership_levels 读价(含原价),过滤 LIFETIME,返回结构不变
  • lifetime-candidates 返回滚动 365 天消费额 + 推广团队销售额双指标候选,阈值正确(分单位)
  • lifetime-grant 授予后:user.memberLevel='LIFETIME'、memberExpireTime=null、升级记录 upgradeType='lifetime'
  • getMemberLevel() 对 LIFETIME 返回 'LIFETIME' 且不因过期降级
  • AdminController overview 含 lifetimeCount

前端(node --check 语法校验,不打包)

  • upgrade.vue:LIFETIME 显示「永久有效」、隐藏续费卡片、无「下一步」
  • index.vue:LIFETIME 无续费/升级入口
  • MembershipCenter.vue:终身会员管理卡片(候选 + 授予 + 已授予列表)
  • 无新增可选链 ?.:key 表达式、CSS Grid、直接 new Date(string)(遵守小程序限制)

数据校验(手动核对)

  • 会员价格/原价迁移后,plans 接口返回值与迁移前一致(sys_config → membership_levels 幂等迁移)
  • LIFETIME 用户在小程序端显示为终身会员、到期时间「永久有效」

9. 风险与注意

  • 统计口径边界member_subscription_order 按家庭创建者归属;退款订单(status='refunded')不计入(仅统计 paid)
  • 销售统计仅一级推广referrer_id 直接关联的下线(现有推广体系为两级,但销售口径按用户确认=「推广团队」定义为推荐码直接下线;如后续需含二级,扩展 SQL 即可)
  • plans 接口过滤 LIFETIME:防止终身会员出现在购买列表(价格全 0 会造成误解)
  • sys_config 旧键保留:不删除,避免影响其他潜在读取方(如管理端配置页面展示)