# 平台终身会员设计(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) ```sql 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_yearly` ← `member_fee_family_original_yearly`(兜底 `member_fee_family_original`,默认 199900) - PREMIUM:`original_price_yearly` ← `member_fee_premium_original`(默认 1599900);`original_price_monthly` ← `member_fee_premium_original_monthly`(默认 159900) - 现价字段 `price_monthly/quarterly/yearly` 同步用 sys_config 现值补齐(FAMILY 年费 131400、PREMIUM 年费 1316800 等,幂等 UPDATE 仅当表内为 0/NULL 时) ### 2.2 新增 LIFETIME 等级行(迁移 204) ```sql 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: ```java // 每个 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`,前端可识别 ✅ | ### 授予写入路径(管理员审核通过时) ```java // 升级目标用户 = 家庭创建者(与消费归属口径一致) 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) ```java // 查询终身会员达标候选(分页) @PostMapping("/membership/lifetime-candidates") public Result>> lifetimeCandidates(@RequestBody Map params) // 审核授予终身会员 @PostMapping("/membership/lifetime-grant") public Result lifetimeGrant(@RequestBody Map 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 旧键保留**:不删除,避免影响其他潜在读取方(如管理端配置页面展示)