2026-10-23-basic-membership-gap-analysis.md 9.4 KB

9.9 元体验会员(BASIC)权限:当前实现 vs 设计差异分析

日期:2026-10-23 设计依据:docs/superpowers/specs/2026-09-10-basic-membership-design.md(PRD v1.0)、docs/superpowers/specs/2026-10-11-restore-basic-and-diet-share-design.md(恢复规格) 实现依据:schema.sql(5851-5867)、DatabaseInitializer(迁移 351/352/360/381)、MembershipService、BasicQuotaService、ActivationQuotaService、AiGateway、AssessmentAppointmentService、HealthReportController、DietRecordController、BasicQuotaScheduledTasks、前端 cfc-frontend


一、结论(TL;DR)

  1. 9.9 会员 = BASIC「体验会员」,月付 9.9 元(price_monthly=990),仅月付。当前实现与两份设计规格整体一致,但存在 1 个 P0 级功能缺陷 + 8 处口径/完整性差异。
  2. P0:AI 对话配额没有按会员等级分流。AiGateway.chat() 对所有登录用户无差别扣 AI_CHAT 配额(每月 3 次):
    • FREE 用户实际白得 3 次/月(设计为 0 次;仅前端有入口门禁,后端无门禁);
    • FAMILY/PREMIUM 付费用户第 4 次起被拦截(设计为不限次,权益对比表也承诺"不限次数")——直接损害 131.4 元/月付费用户体验;
    • 附带 /nutrition/send 配额耗尽时 NPE(resp 未判空)。
  3. P1 三处:配额耗尽提示不回传用户(软引导文案只进了日志);健康报告上传 2 份/月配额被迁移 352 静默删除(实际变不限次,两份规格均未记录);BASIC 到期提醒定时任务遍历空列表(listActiveBasicMemberUserIds() 仍返回空列表)。
  4. 其余为 P2 完整性问题(开通不初始化配额、FOOD_RECOGNIZE 状态查询未暴露、权益条目缺失 2 项、旧文档口径冲突)。

二、9.9 会员当前实现的权限清单(事实源)

2.1 等级定义

字段 值 说明
level_code BASIC schema.sql:5854 / 迁移 360
level_name 体验会员 前端会员页/权益页/开通页均按此展示
price_monthly 990(分) 9.9 元;季付/年付为 NULL(仅月付,符合 PRD)
max_children 3 FREE=0 / FAMILY=10
max_tasks_per_day 3
level_order 2 FREE=1 < BASIC=2 < FAMILY=3 < PREMIUM=4 < LIFETIME=5;PROVIDER=1(非会员,迁移 351/381)
到期判断 level=="BASIC" 且 memberExpireTime > now BasicQuotaService.isActiveBasicMember()

2.2 功能开关(features,schema 与 canUseFeature 硬编码一致,共 11 项)

feature 权益
free_activities 免费活动
free_courses 免费课程
discount_purchase 优惠价购买(⚠ 无生效折扣配置,见观察 O1)
invite_family 邀请家人
mascot_ai_lite AI 健康顾问 3 次/月
ai_report_reading_lite 报告 AI 解读 1 次/月
assessment_quota_lite 测评预约 1 次/月(硬门禁)
health_report_upload_lite 健康报告上传(配额已被移除 → 实际不限次)
growth_plan_lite 基础成长计划
task_template_lite 更多任务模板
referral_commission 推荐佣金(2026-10-11 恢复规格新增)

2.3 月度配额(BasicQuotaService.QUOTA_LIMITS,每月 1 号 00:00 定时刷新)

配额类型 上限 超限行为(现状) 集成点
AI_CHAT 3 次 软引导(当前只记日志,见 D1/D2) AiGateway.chat():184
REPORT_READING 1 次 静默 return(报告上传成功但永不解读,见 O2) HealthReportController.triggerAiAnalysis():2755
ASSESSMENT_QUOTA 1 次 硬门禁,抛 BasicQuotaExhaustedException AssessmentAppointmentService.createAppointment():34
FOOD_RECOGNIZE 3 次 FREE 拒绝 / BASIC 扣配额 / FAMILY+ 不限,403+toast DietRecordController.recognizeFood():26

2.4 周边链路现状

  • 购买:/plans 返回 BASIC(990/月);前端 pages/membership/pay?levelCode=BASIC&period=monthly;支付回调 → activateMembership() 写 family_memberships BASIC 记录并全家同步等级(level-agnostic,BASIC 可用)。
  • 到期:每天 4:00 downgradeExpiredMembers() 按 family_memberships.end_date 过期 → 全家降 FREE(不区分等级,BASIC 生效)。
  • 前端:AI 对话页展示配额剩余 + UpgradeGuideModal 软引导(PRD 符合);入口门禁 getMyMembership().memberLevel !== 'FREE'。
  • 管理端:AdminBasicQuotaController 可手动重置配额。

三、设计 vs 实现差异对照表

# 差异项 设计要求(PRD/恢复规格) 当前实现 严重度
D1 AI 对话配额等级分流 仅 BASIC 3 次/月;FREE 无 AI;FAMILY+ 不限 所有用户无差别扣配额:FREE 实得 3 次/月;FAMILY+ 第 4 次起被拦;/nutrition/send NPE P0
D2 配额耗尽软引导 Toast/弹窗"本月额度已用完(3/3),升级 FAMILY 享不限次" 后端仅 log.info;/chat/send 返回空 answer;文案不回传用户 P1
D3 健康报告上传配额 PRD:2 份/月 + REPORT_UPLOAD_LITE 权益(共 5 条 benefits) 迁移 352 删除该权益与配额记录;features 仍含 health_report_upload_lite → 实际不限次;两份规格均未记录此变更 P1
D4 BASIC 到期提醒 恢复规格:listActiveBasicMemberUserIds() 恢复真实查询;PRD:到期前 1-5 天推送 仍返回 Collections.emptyList()(注释还写"已废弃")→ 定时任务永远空转;微信模板消息推送仍是 TODO P1
D5 开通时初始化配额 恢复规格:activateMembership 开通 BASIC 时调 activateBasicQuota 无任何调用方(死代码);靠 consumeQuota 懒插入兜底,功能可用但与规格不符 P2
D6 ActivationQuotaService 配额表 应含 FOOD_RECOGNIZE 3 次/月 只有 3 类(缺 FOOD_RECOGNIZE),与 BasicQuotaService 不一致 P2
D7 食材识别进度可见 PRD §6:显示"本月还可识别 X 次" /basic-quota/status/all 只返回 3 类,不含 FOOD_RECOGNIZE;前端无展示 P2
D8 BASIC 权益条目 benefits 应覆盖全部 Lite 权益 仅 4 条(无 FOOD_RECOGNIZE_LITE、无 REPORT_UPLOAD_LITE),权益页看不到"食材识别 3 次/月"说明 P2
D9 旧文档口径冲突 — docs/function-analysis-by-membership.md 仍写 BASIC=家庭会员(不限量为主)、等级映射为 PROFESSIONAL/ENTERPRISE,与实现(体验会员限次、FREE/BASIC/FAMILY/PREMIUM/LIFETIME/PROVIDER)冲突 P2(文档)

观察项(非差异,需产品确认)

  • O1:BASIC features 含 discount_purchase,但无生效折扣配置(member_discount_family 仅 FAMILY 生效),对比表显示 BASIC 商品折扣"—"。声明与效果不符,建议从 BASIC features 移除或补充配置。
  • O2:REPORT_READING 超限后 triggerAiAnalysis 静默 return,用户端无任何提示(报告上传成功但永远不解读)。

四、P0 详解(D1:AI 对话配额无等级分流)

4.1 代码路径

AIChatController./chat/send(无会员门禁)
  → AIService.sendMessage → AiGateway.chat():184
      if (userId != null && userId != 0L) {
          basicQuotaService.consumeQuota(userId, "AI_CHAT");   // 无等级判断
          if (!result.isSuccess()) return null;
      }

consumeQuota() 内部无等级分支:total 恒为 3,任何用户(FREE/BASIC/FAMILY/PREMIUM)第 4 次起 used >= total → success=false → chat() 返回 null:

  • /chat/send:answer="",用户看到空白回复;
  • /nutrition/send(405-407 行):resp.getOrDefault(...) 未判空 → NPE → 500。

4.2 佐证"这是缺陷而非有意设计"

  1. MembershipService.getMembershipCompareData()(1744-1746 行)向用户承诺 FAMILY/PREMIUM"不限次数";
  2. 恢复规格风险节明确写道:"不恢复 consumeQuota 内原有的 isActiveBasicMember 前置校验(避免误伤 FREE/FAMILY 的 AI 对话)"——现状恰好相反(FREE/FAMILY 都被限成 3 次);
  3. PRD §8.1.4:仅对 isActiveBasicMember(userId) 消耗配额。

4.3 修复建议

在 AiGateway.chat() 内按 level_order 分流(与 consumeFoodRecognizeQuota 同构):

if (userId != null && userId != 0L) {
    int userOrder = membershipService.getLevelOrder(membershipService.getMemberLevel(userId));
    int basicOrder = membershipService.getLevelOrder("BASIC");
    if (userOrder < basicOrder) {            // FREE:后端补门禁拒绝
        ...
    } else if (userOrder == basicOrder) {    // BASIC:走配额
        QuotaConsumeResult r = basicQuotaService.consumeQuota(userId, "AI_CHAT");
        if (!r.isSuccess()) { log...; return null; }
    }
    // FAMILY+ 直接放行,不扣配额
}

配套:/nutrition/send 补 resp 判空;配额耗尽文案经 Result 回传前端(软引导 UpgradeGuideModal 已有,只差后端传参)。


五、待确认

  1. D1 是否按 §4.3 方案修(P0,建议最优先);
  2. D3 报告上传:产品意图是"引流不限次"(回写文档、去掉 features 的 _lite 语义)还是恢复 2 份/月配额?
  3. D4 到期提醒推送是否排期(需接微信模板消息);
  4. O1 discount_purchase 是否从 BASIC features 移除。