日期: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
price_monthly=990),仅月付。当前实现与两份设计规格整体一致,但存在 1 个 P0 级功能缺陷 + 8 处口径/完整性差异。AiGateway.chat() 对所有登录用户无差别扣 AI_CHAT 配额(每月 3 次):
/nutrition/send 配额耗尽时 NPE(resp 未判空)。listActiveBasicMemberUserIds() 仍返回空列表)。| 字段 | 值 | 说明 |
|---|---|---|
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() |
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 恢复规格新增) |
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 |
/plans 返回 BASIC(990/月);前端 pages/membership/pay?levelCode=BASIC&period=monthly;支付回调 → activateMembership() 写 family_memberships BASIC 记录并全家同步等级(level-agnostic,BASIC 可用)。downgradeExpiredMembers() 按 family_memberships.end_date 过期 → 全家降 FREE(不区分等级,BASIC 生效)。UpgradeGuideModal 软引导(PRD 符合);入口门禁 getMyMembership().memberLevel !== 'FREE'。AdminBasicQuotaController 可手动重置配额。| # | 差异项 | 设计要求(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(文档) |
discount_purchase,但无生效折扣配置(member_discount_family 仅 FAMILY 生效),对比表显示 BASIC 商品折扣"—"。声明与效果不符,建议从 BASIC features 移除或补充配置。REPORT_READING 超限后 triggerAiAnalysis 静默 return,用户端无任何提示(报告上传成功但永远不解读)。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。MembershipService.getMembershipCompareData()(1744-1746 行)向用户承诺 FAMILY/PREMIUM"不限次数";consumeQuota 内原有的 isActiveBasicMember 前置校验(避免误伤 FREE/FAMILY 的 AI 对话)"——现状恰好相反(FREE/FAMILY 都被限成 3 次);isActiveBasicMember(userId) 消耗配额。在 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 已有,只差后端传参)。
_lite 语义)还是恢复 2 份/月配额?discount_purchase 是否从 BASIC features 移除。