# BASIC 体验会员实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在现有会员体系(FREE→FAMILY→PREMIUM→PROVIDER)中新增 BASIC 等级,实现 9.9 元/月体验会员的付费开通、配额扣减、软/硬引导升级链路。 **Architecture:** 基于现有 `MembershipLevel` / `FamilyMembership` / `TrialMembership` 三层会员记录模型,新增 `basic_quota_usage` 表管理月度配额。`BasicQuotaService` 作为独立 Service 承载配额逻辑,在 4 个业务入口(AI 对话、报告解读、测评预约、报告上传)插入配额检查。定时任务复用 `MembershipScheduledTasks` 扩展模式。 **Tech Stack:** Spring Boot 2.7.18, MyBatis-Plus, Java 8, Maven, 微信小程序 Vue2 **Spec:** `docs/superpowers/specs/2026-09-10-basic-membership-design.md` ## Global Constraints - 所有接口统一 `@PostMapping`,禁止 `@GetMapping`/`@PutMapping`/`@DeleteMapping` - ORM 使用 MyBatis-Plus `@TableName` + `@TableId(type = IdType.AUTO)` - DI 使用 `@Resource`,字段名必须与类型默认 Bean Name 一致 - 响应统一 `Result`(code/message/data) - 禁用 `?.`(用 `&&` 替代)、禁用 CSS Grid(用 flexbox)、禁用 `:key` 表达式 - 前端禁止直接 `new Date(string)`,用 `utils/format.js` 的 `parseDate()` - 所有迁移幂等(try-catch 忽略已存在异常) - `mvn clean compile` 是唯一验证方式(编译通过即视为验证) --- ## 文件结构 | 文件路径 | 类型 | 职责 | |----------|------|------| | `entity/BasicQuotaUsage.java` | Create | BASIC 配额实体 | | `mapper/BasicQuotaUsageMapper.java` | Create | 配额 Mapper 接口 | | `dto/QuotaStatusDTO.java` | Create | 配额查询响应 DTO | | `dto/QuotaConsumeResult.java` | Create | 配额扣减结果 DTO | | `service/BasicQuotaService.java` | Create | 配额核心 Service(查询/扣减/刷新) | | `controller/BasicQuotaController.java` | Create | 配额查询 + 手动重置接口 | | `task/BasicQuotaScheduledTasks.java` | Create | 配额刷新 + 到期提醒定时任务 | | `service/MembershipService.java` | Modify | `canUseFeature()` 加 BASIC 分支;`downgradeExpiredMembers()` 支持 BASIC | | `service/AiGateway.java` | Modify | `chat()` 入口插入 BASIC 配额扣减 | | `service/ReportSummaryService.java` | Modify | `parseReport()` 入口插入配额扣减 | | `service/AssessmentAppointmentService.java` | Modify | `create()` 入口插入硬门禁配额检查 | | `service/DanReportUploadService.java` | Modify | `upload()` 入口插入配额检查 | | `service/ActivationQuotaService.java` | Create | 支付成功后初始化 4 类配额 | | `resources/schema.sql` | Modify | 新增 `basic_quota_usage` 表定义 + BASIC seed 数据 | | `config/DatabaseInitializer.java` | Modify | 新增 BASIC 等级 / 权益 / 表迁移 | | `cfc-frontend/pages/profile/membership/index.vue` | Modify | 展示 BASIC 权益卡片 | | `cfc-frontend/pages/profile/upgrade/index.vue` | Modify | 新增 9.9 元月付选项 | | `cfc-frontend/components/QuotaProgressBar.vue` | Create | 配额进度条组件 | | `cfc-frontend/components/UpgradeGuideModal.vue` | Create | 统一软/硬引导弹窗组件 | | `cfc-frontend/pages/ai-chat/index.vue` | Modify | 显示剩余次数 + 超限弹窗 | | `cfc-frontend/pages/assessment/appointment/index.vue` | Modify | 显示剩余次数 + 硬门禁弹窗 | | `cfc-frontend/pages/health/report-list.vue` | Modify | 显示剩余次数 + 软引导 | --- ## Phase 概述 | Phase | 天数 | 范围 | 里程碑 | |-------|------|------|--------| | P0 核心链路 | 3 天 | DB 迁移 + BASIC 等级 + 支付开通 + 基础权限 | 9.9 元支付成功,BASIC 生效,能进沙盘/AI 对话 | | P1 配额系统 | 2 天 | `BasicQuotaService` + 4 个业务入口扣减 + 定时刷新 | 月度配额正确扣减/刷新,超限引导生效 | | P2 前端展示 | 2 天 | 权益页/开通页/进度条/弹窗 | 免费用户可见 BASIC,付费后正确显示剩余次数 | | P3 运营配套 | 2 天 | 定时提醒 + 管理端配额看板 + 手动重置接口 | 到期提醒自动发送,看板可查 | --- ## Task 1: 数据库迁移 — 新增表 + BASIC 种子数据 **Files:** - Modify: `cfc-backend/src/main/resources/schema.sql` (末尾追加) - Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` (末尾 `runMigrations()` 追加) - Test: 无单元测试(迁移幂等,`mvn compile` 验证) **Interfaces:** - Consumes: 无(数据库级操作) - Produces: `basic_quota_usage` 表;`membership_levels` 新增 BASIC 记录;`membership_benefits` 新增 5 条 BASIC 权益记录 - [ ] **Step 1: schema.sql 末尾追加基本表定义 + seed 数据** 在 `schema.sql` 末尾(OFFSET 5513 之后)追加以下内容: ```sql -- ============================================================ -- 新增 BASIC 体验会员等级种子数据(迁移150) -- ============================================================ INSERT IGNORE INTO membership_levels (level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) VALUES ( 'BASIC', '体验会员', '9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', 990, '["free_activities","free_courses","discount_purchase","invite_family","mascot_ai_lite","ai_report_reading_lite","assessment_quota_lite","health_report_upload_lite","growth_plan_lite","task_template_lite"]', 3, 3 ); -- ============================================================ -- 新增 BASIC 会员权益种子数据(迁移151) -- ============================================================ INSERT IGNORE INTO membership_benefits (level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES ('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), ('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), ('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), ('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8), ('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9); -- ============================================================ -- BASIC 会员月度配额使用记录表(迁移152) -- ============================================================ CREATE TABLE IF NOT EXISTS basic_quota_usage ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT '家长用户ID(非family_id)', quota_type VARCHAR(30) NOT NULL COMMENT 'AI_CHAT/REPORT_READING/ASSESSMENT_QUOTA/REPORT_UPLOAD', used_count INT DEFAULT 0 COMMENT '本月已用次数', total_count INT NOT NULL COMMENT '月度总配额', period_start DATE NOT NULL COMMENT '周期开始日期(每月1号)', period_end DATE NOT NULL COMMENT '周期结束日期(月末)', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start), INDEX idx_user_period (user_id, period_start) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录'; ``` - [ ] **Step 2: DatabaseInitializer.java 追加迁移代码** 在 `DatabaseInitializer.java` 的 `runMigrations()` 方法末尾(最新迁移编号之后)追加: ```java // 迁移150: membership_levels 新增 BASIC 体验会员等级 try { jdbcTemplate.execute( "INSERT IGNORE INTO membership_levels " + "(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) " + "VALUES ('BASIC', '体验会员', " + "'9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', " + "990, " + "'[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\"]', " + "3, 3)"); log.info("已初始化BASIC体验会员等级"); } catch (Exception e) { // 等级已存在,忽略 } // 迁移151: membership_benefits 新增 BASIC 会员权益 try { jdbcTemplate.execute( "INSERT IGNORE INTO membership_benefits " + "(level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " + "('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), " + "('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), " + "('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), " + "('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测评', '📝', '#E0E7FF', 8), " + "('BASIC', 'REPORT_UPLOAD_LITE', '健康报告上传(限2份/月)', '每月可上传2份健康报告', '📄', '#FEF9C3', 9)"); log.info("已初始化BASIC会员权益列表"); } catch (Exception e) { // 已存在,忽略 } // 迁移152: 创建 basic_quota_usage 表 try { jdbcTemplate.execute( "CREATE TABLE IF NOT EXISTS basic_quota_usage (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "user_id BIGINT NOT NULL COMMENT '家长用户ID', " + "quota_type VARCHAR(30) NOT NULL COMMENT '配额类型', " + "used_count INT DEFAULT 0 COMMENT '已用次数', " + "total_count INT NOT NULL COMMENT '月度总配额', " + "period_start DATE NOT NULL COMMENT '周期开始日期', " + "period_end DATE NOT NULL COMMENT '周期结束日期', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + "UNIQUE KEY uk_user_type_period (user_id, quota_type, period_start), " + "INDEX idx_user_period (user_id, period_start) " + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BASIC会员月度配额使用记录'"); log.info("已创建basic_quota_usage表"); } catch (Exception e) { // 表已存在,忽略 } ``` - [ ] **Step 3: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS,无编译错误。 - [ ] **Step 4: 提交** ```bash git add cfc-backend/src/main/resources/schema.sql git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java git commit -m "feat: 新增BASIC体验会员等级及配额表迁移(迁移150-152)" ``` --- ## Task 2: 新增 BasicQuotaUsage 实体 + Mapper **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/BasicQuotaUsage.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/BasicQuotaUsageMapper.java` - Test: 无(实体类无业务逻辑) **Interfaces:** - Consumes: 无 - Produces: `BasicQuotaUsage` 实体(字段:id, userId, quotaType, usedCount, totalCount, periodStart, periodEnd, createdAt, updatedAt),`BasicQuotaUsageMapper` 接口(继承 `BaseMapper`) - [ ] **Step 1: 创建 BasicQuotaUsage.java** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("basic_quota_usage") public class BasicQuotaUsage implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long userId; private String quotaType; private Integer usedCount; private Integer totalCount; private Date periodStart; private Date periodEnd; private Date createdAt; private Date updatedAt; } ``` - [ ] **Step 2: 创建 BasicQuotaUsageMapper.java** ```java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.BasicQuotaUsage; import org.apache.ibatis.annotations.Mapper; @Mapper public interface BasicQuotaUsageMapper extends BaseMapper { } ``` - [ ] **Step 3: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 4: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/entity/BasicQuotaUsage.java git add cfc-backend/src/main/java/com/etotem/cfc/mapper/BasicQuotaUsageMapper.java git commit -m "feat: 新增BasicQuotaUsage实体和Mapper" ``` --- ## Task 3: 新增 QuotaStatusDTO + QuotaConsumeResult DTO **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaStatusDTO.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaConsumeResult.java` - Test: 无(纯 POJO) **Interfaces:** - Consumes: 无 - Produces: `QuotaStatusDTO`(字段:quotaType, total, used, remaining, periodStart, periodEnd);`QuotaConsumeResult`(字段:success, remaining, toastMsg, actionUrl) - [ ] **Step 1: 创建 QuotaStatusDTO.java** ```java package com.etotem.cfc.dto; import lombok.Data; import java.util.Date; @Data public class QuotaStatusDTO { private String quotaType; private int total; private int used; private int remaining; private Date periodStart; private Date periodEnd; } ``` - [ ] **Step 2: 创建 QuotaConsumeResult.java** ```java package com.etotem.cfc.dto; import lombok.Data; @Data public class QuotaConsumeResult { private boolean success; private int remaining; private String toastMsg; private String actionUrl; } ``` - [ ] **Step 3: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 4: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaStatusDTO.java git add cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaConsumeResult.java git commit -m "feat: 新增配额相关DTO" ``` --- ## Task 4: 实现 BasicQuotaService(核心配额逻辑) **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.java` - Test: 无(依赖数据库,用 `mvn compile` 验证编译) **Interfaces:** - Consumes: `BasicQuotaUsageMapper` (insert/select by example) - Produces: - `QuotaStatusDTO getMonthlyQuota(Long userId, String quotaType)` — 返回指定月份某配额的剩余次数 - `QuotaConsumeResult consumeQuota(Long userId, String quotaType)` — 扣减配额,返回结果 - `void refreshMonthlyQuota()` — 每月1号批量刷新所有 BASIC 用户的配额 - `boolean isActiveBasicMember(Long userId)` — 检查用户当前是否为有效 BASIC 会员 - `Date getExpireTime(Long userId)` — 获取 BASIC 用户到期时间 - [ ] **Step 1: 创建 BasicQuotaService.java** ```java package com.etotem.cfc.service; import com.etotem.cfc.dto.QuotaConsumeResult; import com.etotem.cfc.dto.QuotaStatusDTO; import com.etotem.cfc.entity.BasicQuotaUsage; import com.etotem.cfc.entity.User; import com.etotem.cfc.mapper.BasicQuotaUsageMapper; import com.etotem.cfc.mapper.UserMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.*; @Slf4j @Service public class BasicQuotaService { @Resource private BasicQuotaUsageMapper basicQuotaUsageMapper; @Resource private UserMapper userMapper; @Resource private MembershipService membershipService; // BASIC 会员月度配额上限 private static final Map QUOTA_LIMITS = new HashMap<>(); static { QUOTA_LIMITS.put("AI_CHAT", 3); QUOTA_LIMITS.put("REPORT_READING", 1); QUOTA_LIMITS.put("ASSESSMENT_QUOTA", 1); QUOTA_LIMITS.put("REPORT_UPLOAD", 2); } /** * 查询指定用户指定月份某配额的剩余次数 */ public QuotaStatusDTO getMonthlyQuota(Long userId, String quotaType) { if (userId == null || quotaType == null) { return null; } Date now = new Date(); Date periodStart = getMonthStart(now); Date periodEnd = getMonthEnd(now); BasicQuotaUsage usage = basicQuotaUsageMapper.selectOne( new LambdaQueryWrapper() .eq(BasicQuotaUsage::getUserId, userId) .eq(BasicQuotaUsage::getQuotaType, quotaType) .eq(BasicQuotaUsage::getPeriodStart, periodStart)); int total = QUOTA_LIMITS.getOrDefault(quotaType, 0); int used = 0; if (usage != null) { used = usage.getUsedCount() != null ? usage.getUsedCount() : 0; } QuotaStatusDTO dto = new QuotaStatusDTO(); dto.setQuotaType(quotaType); dto.setTotal(total); dto.setUsed(used); dto.setRemaining(Math.max(0, total - used)); dto.setPeriodStart(periodStart); dto.setPeriodEnd(periodEnd); return dto; } /** * 尝试扣减配额,返回是否成功 */ public QuotaConsumeResult consumeQuota(Long userId, String quotaType) { QuotaConsumeResult result = new QuotaConsumeResult(); if (!isActiveBasicMember(userId)) { result.setSuccess(false); result.setToastMsg("该功能仅限体验会员使用"); return result; } Date now = new Date(); Date periodStart = getMonthStart(now); Date periodEnd = getMonthEnd(now); BasicQuotaUsage usage = basicQuotaUsageMapper.selectOne( new LambdaQueryWrapper() .eq(BasicQuotaUsage::getUserId, userId) .eq(BasicQuotaUsage::getQuotaType, quotaType) .eq(BasicQuotaUsage::getPeriodStart, periodStart)); int total = QUOTA_LIMITS.getOrDefault(quotaType, 0); int used = 0; if (usage != null) { used = usage.getUsedCount() != null ? usage.getUsedCount() : 0; } if (used >= total) { result.setSuccess(false); result.setRemaining(0); result.setToastMsg(buildExhaustedToast(quotaType, total)); result.setActionUrl("/pages/profile/upgrade/index"); return result; } // 初始化或更新 if (usage == null) { usage = new BasicQuotaUsage(); usage.setUserId(userId); usage.setQuotaType(quotaType); usage.setUsedCount(0); usage.setTotalCount(total); usage.setPeriodStart(periodStart); usage.setPeriodEnd(periodEnd); usage.setCreatedAt(now); usage.setUpdatedAt(now); basicQuotaUsageMapper.insert(usage); } usage.setUsedCount(used + 1); usage.setUpdatedAt(now); basicQuotaUsageMapper.updateById(usage); result.setSuccess(true); result.setRemaining(total - used - 1); return result; } /** * 批量刷新所有 BASIC 用户当月配额(每月1号调用) */ public void refreshMonthlyQuota() { Date now = new Date(); Date monthStart = getMonthStart(now); Date monthEnd = getMonthEnd(now); Date lastMonthStart = getMonthStart(new Date(now.getTime() - 30L * 24 * 60 * 60 * 1000)); // 查找上个月有记录的所有 BASIC 用户 List lastMonthRecords = basicQuotaUsageMapper.selectList( new LambdaQueryWrapper() .eq(BasicQuotaUsage::getPeriodStart, lastMonthStart)); Set userIds = new HashSet<>(); for (BasicQuotaUsage rec : lastMonthRecords) { if (isActiveBasicMember(rec.getUserId())) { userIds.add(rec.getUserId()); } } for (Long userId : userIds) { for (String quotaType : QUOTA_LIMITS.keySet()) { try { BasicQuotaUsage existing = basicQuotaUsageMapper.selectOne( new LambdaQueryWrapper() .eq(BasicQuotaUsage::getUserId, userId) .eq(BasicQuotaUsage::getQuotaType, quotaType) .eq(BasicQuotaUsage::getPeriodStart, monthStart)); if (existing == null) { BasicQuotaUsage newUsage = new BasicQuotaUsage(); newUsage.setUserId(userId); newUsage.setQuotaType(quotaType); newUsage.setUsedCount(0); newUsage.setTotalCount(QUOTA_LIMITS.get(quotaType)); newUsage.setPeriodStart(monthStart); newUsage.setPeriodEnd(monthEnd); newUsage.setCreatedAt(now); newUsage.setUpdatedAt(now); basicQuotaUsageMapper.insert(newUsage); log.info("BASIC配额刷新初始化: userId={}, quotaType={}", userId, quotaType); } else { existing.setUsedCount(0); existing.setPeriodStart(monthStart); existing.setPeriodEnd(monthEnd); existing.setUpdatedAt(now); basicQuotaUsageMapper.updateById(existing); } } catch (Exception e) { log.warn("BASIC配额刷新失败: userId={}, quotaType={}", userId, quotaType, e); } } } log.info("BASIC配额刷新完成, 处理用户数: {}", userIds.size()); } /** * 检查用户当前是否为有效的 BASIC 会员 */ public boolean isActiveBasicMember(Long userId) { if (userId == null) { return false; } String level = membershipService.getMemberLevel(userId); if (!"BASIC".equals(level)) { return false; } User user = userMapper.selectById(userId); if (user == null || user.getMemberExpireTime() == null) { return false; } return user.getMemberExpireTime().after(new Date()); } /** * 获取 BASIC 用户到期时间 */ public Date getExpireTime(Long userId) { User user = userMapper.selectById(userId); if (user != null) { return user.getMemberExpireTime(); } return null; } private Date getMonthStart(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.set(Calendar.DAY_OF_MONTH, 1); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); return cal.getTime(); } private Date getMonthEnd(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH)); cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.SECOND, 59); cal.set(Calendar.MILLISECOND, 999); return cal.getTime(); } private String buildExhaustedToast(String quotaType, int total) { switch (quotaType) { case "AI_CHAT": return "您本月 AI 对话已用完(" + total + "/" + total + "),升级 FAMILY 享不限次畅聊"; case "REPORT_READING": return "本月报告解读额度已用完(" + total + "/" + total + "),升级 FAMILY 解锁不限次深度解读"; case "ASSESSMENT_QUOTA": return "测评预约为 FAMILY 会员专属权益,升级后即可预约"; case "REPORT_UPLOAD": return "本月报告上传额度已用完(" + total + "/" + total + "),升级 FAMILY 解锁更多"; default: return "本月额度已用完,升级 FAMILY 解锁更多"; } } } ``` - [ ] **Step 2: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 3: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.java git commit -m "feat: 实现BasicQuotaService配额核心逻辑" ``` --- ## Task 5: 实现 ActivationQuotaService(支付成功后初始化配额) **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.java` - Test: 无 **Interfaces:** - Consumes: `BasicQuotaUsageMapper` - Produces: `void activateBasicQuota(Long userId, Long familyId)` — 支付成功后为 BASIC 用户初始化当月 4 类配额记录 - [ ] **Step 1: 创建 ActivationQuotaService.java** ```java package com.etotem.cfc.service; import com.etotem.cfc.entity.BasicQuotaUsage; import com.etotem.cfc.mapper.BasicQuotaUsageMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.*; @Slf4j @Service public class ActivationQuotaService { @Resource private BasicQuotaUsageMapper basicQuotaUsageMapper; private static final Map QUOTA_LIMITS = new HashMap<>(); static { QUOTA_LIMITS.put("AI_CHAT", 3); QUOTA_LIMITS.put("REPORT_READING", 1); QUOTA_LIMITS.put("ASSESSMENT_QUOTA", 1); QUOTA_LIMITS.put("REPORT_UPLOAD", 2); } /** * 支付成功后,为 BASIC 用户初始化当月 4 类配额记录 */ public void activateBasicQuota(Long userId, Long familyId) { if (userId == null) { return; } Date now = new Date(); Date monthStart = getMonthStart(now); Date monthEnd = getMonthEnd(now); for (Map.Entry entry : QUOTA_LIMITS.entrySet()) { try { BasicQuotaUsage usage = new BasicQuotaUsage(); usage.setUserId(userId); usage.setQuotaType(entry.getKey()); usage.setUsedCount(0); usage.setTotalCount(entry.getValue()); usage.setPeriodStart(monthStart); usage.setPeriodEnd(monthEnd); usage.setCreatedAt(now); usage.setUpdatedAt(now); basicQuotaUsageMapper.insert(usage); log.info("BASIC配额初始化: userId={}, quotaType={}, total={}", userId, entry.getKey(), entry.getValue()); } catch (Exception e) { log.warn("BASIC配额初始化失败: userId={}, quotaType={}", userId, entry.getKey(), e); } } } private Date getMonthStart(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.set(Calendar.DAY_OF_MONTH, 1); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); return cal.getTime(); } private Date getMonthEnd(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH)); cal.set(Calendar.HOUR_OF_DAY, 23); cal.set(Calendar.MINUTE, 59); cal.set(Calendar.SECOND, 59); cal.set(Calendar.MILLISECOND, 999); return cal.getTime(); } } ``` - [ ] **Step 2: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 3: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.java git commit -m "feat: 新增ActivationQuotaService支付后初始化配额" ``` --- ## Task 6: 修改 MembershipService(BASIC 分支 + 降级支持) **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java` (两处修改) **Interfaces:** - Consumes: `BasicQuotaService`(由调用方注入) - Produces: `canUseFeature` 支持 BASIC;`downgradeExpiredMembers` 支持 BASIC 降级 - [ ] **Step 1: canUseFeature() 新增 BASIC 分支** 在 `MembershipService.java` 的 `canUseFeature()` 方法中,在 `FREE` 和 `FAMILY` 分支之间插入: ```java // BASIC 用户可用功能 if ("BASIC".equals(level)) { return "free_activities".equals(feature) || "free_courses".equals(feature) || "discount_purchase".equals(feature) || "invite_family".equals(feature) || "mascot_ai_lite".equals(feature) || "ai_report_reading_lite".equals(feature) || "assessment_quota_lite".equals(feature) || "health_report_upload_lite".equals(feature) || "growth_plan_lite".equals(feature) || "task_template_lite".equals(feature); } ``` 插入位置:在 `if ("FAMILY".equals(level)) {` 之前。 - [ ] **Step 2: downgradeExpiredMembers() 支持 BASIC** 在 `MembershipService.java` 的 `downgradeExpiredMembers()` 方法中,修改条件判断: **修改前:** ```java if ("FAMILY".equals(user.getMemberLevel()) && user.getMemberExpireTime() != null && user.getMemberExpireTime().before(new Date())) { ``` **修改后:** ```java if (("FAMILY".equals(user.getMemberLevel()) || "BASIC".equals(user.getMemberLevel())) && user.getMemberExpireTime() != null && user.getMemberExpireTime().before(new Date())) { ``` 同时在方法体内,降级时记录日志区分等级: ```java user.setMemberLevel("FREE"); user.setMemberExpireTime(null); user.setUpdatedAt(new Date()); userMapper.updateById(user); log.info("会员到期降级: userId={}, 原等级={}", userId, level); // 记录原等级 ``` - [ ] **Step 3: getMemberLevel() 支持 BASIC 有效期检查** 在 `getMemberLevel()` 方法中,`FAMILY` 判断之外补充 `BASIC`: **修改前:** ```java if ("FAMILY".equals(level) && user.getMemberExpireTime() != null) { if (user.getMemberExpireTime().before(new Date())) { return "FREE"; } } ``` **修改后:** ```java if (("FAMILY".equals(level) || "BASIC".equals(level)) && user.getMemberExpireTime() != null) { if (user.getMemberExpireTime().before(new Date())) { return "FREE"; } } ``` - [ ] **Step 4: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 5: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java git commit -m "feat: MembershipService支持BASIC等级权限判断和到期降级" ``` --- ## Task 7: 修改 AiGateway.chat() 插入 BASIC 配额扣减 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java` **Interfaces:** - Consumes: `BasicQuotaService.consumeQuota(userId, "AI_CHAT")` - Produces: BASIC 用户对话时自动扣减配额,配额不足时返回 null(由 `AIChatController` 处理 403 响应) - [ ] **Step 1: 在 chat() 方法开头插入配额检查** 在 `AiGateway.chat()` 方法中,找到: ```java public Map chat(String query, Long userId, String conversationId, Map inputs) { if (!enabled || isCircuitOpen()) return null; ``` 在 `if (!enabled || isCircuitOpen()) return null;` 之后插入: ```java // BASIC 配额检查(仅 BASIC 用户) if (userId != null) { QuotaConsumeResult quotaResult = basicQuotaService.consumeQuota(userId, "AI_CHAT"); if (!quotaResult.isSuccess()) { log.info("AI对话配额不足: userId={}, toast={}", userId, quotaResult.getToastMsg()); // 返回 null,由调用方 AIChatController 处理 403 return null; } } ``` 并在类头部添加 `@Resource`: ```java @Resource private BasicQuotaService basicQuotaService; ``` - [ ] **Step 2: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 3: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java git commit -m "feat: AiGateway.chat()插入BASIC AI配额扣减" ``` --- ## Task 8: 修改 ReportSummaryService + AssessmentAppointmentService + DanReportUploadService **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ReportSummaryService.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java` **Interfaces:** - Consumes: `BasicQuotaService.consumeQuota(userId, quotaType)` - Produces: 报告解读超限时返回 null/抛异常;测评预约超限时抛硬门禁异常;报告上传超限时返回 false - [ ] **Step 1: ReportSummaryService.parseReport() 插入配额检查** 在 `ReportSummaryService.java` 的 `parseReport()` 方法入口插入(找到方法签名后第一行逻辑): ```java @Resource private BasicQuotaService basicQuotaService; // 在方法体开头插入: if (userId != null) { QuotaConsumeResult result = basicQuotaService.consumeQuota(userId, "REPORT_READING"); if (!result.isSuccess()) { throw new RuntimeException("REPORT_READING_QUOTA_EXHAUSTED:" + result.getToastMsg()); } } ``` - [ ] **Step 2: AssessmentAppointmentService.create() 插入硬门禁配额检查** 在 `AssessmentAppointmentService.java` 的 `create()` 方法入口插入: ```java @Resource private BasicQuotaService basicQuotaService; // 在方法体开头插入: if (userId != null) { QuotaConsumeResult result = basicQuotaService.consumeQuota(userId, "ASSESSMENT_QUOTA"); if (!result.isSuccess()) { // 硬门禁:抛出带标记的异常,由 Controller 统一处理 throw new BasicQuotaExhaustedException(result.getToastMsg()); } } ``` 同时创建异常类: ```java // cfc-backend/src/main/java/com/etotem/cfc/common/BasicQuotaExhaustedException.java package com.etotem.cfc.common; public class BasicQuotaExhaustedException extends RuntimeException { public BasicQuotaExhaustedException(String message) { super(message); } } ``` - [ ] **Step 3: DanReportUploadService.upload() 插入配额检查** 在 `DanReportUploadService.java` 的 `upload()` 方法入口插入: ```java @Resource private BasicQuotaService basicQuotaService; // 在方法体开头插入: if (userId != null) { QuotaConsumeResult result = basicQuotaService.consumeQuota(userId, "REPORT_UPLOAD"); if (!result.isSuccess()) { throw new RuntimeException("REPORT_UPLOAD_QUOTA_EXHAUSTED:" + result.getToastMsg()); } } ``` - [ ] **Step 4: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 5: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/common/BasicQuotaExhaustedException.java git add cfc-backend/src/main/java/com/etotem/cfc/service/ReportSummaryService.java git add cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java git add cfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.java git commit -m "feat: 报告解读/测评预约/报告上传插入BASIC配额检查" ``` --- ## Task 9: 新建 BasicQuotaScheduledTasks 定时任务 **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/task/BasicQuotaScheduledTasks.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/task/MembershipScheduledTasks.java`(添加到期提醒任务) **Interfaces:** - Consumes: `BasicQuotaService.refreshMonthlyQuota()`、`MembershipService.listActiveBasicMemberUserIds()` - Produces: 每月1号刷新配额;每天10点推到期提醒 - [ ] **Step 1: 在 MembershipService 新增 listActiveBasicMemberUserIds() 方法** 在 `MembershipService.java` 末尾(`listActiveMemberFamilyIds` 方法之后)追加: ```java /** * 查询指定等级当前有效的基本会员用户ID列表(BASIC到期提醒用) */ public List listActiveBasicMemberUserIds() { Date now = new Date(); List basicUsers = userMapper.selectList( new LambdaQueryWrapper() .eq(User::getMemberLevel, "BASIC") .isNotNull(User::getMemberExpireTime) .gt(User::getMemberExpireTime, now)); return basicUsers != null ? basicUsers.stream() .map(User::getId) .distinct() .collect(Collectors.toList()) : Collections.emptyList(); } ``` 同时在文件头部确认 `UserMapper` 已 `@Resource` 注入(已有)。 - [ ] **Step 2: 创建 BasicQuotaScheduledTasks.java** ```java package com.etotem.cfc.task; import com.etotem.cfc.service.BasicQuotaService; import com.etotem.cfc.service.MembershipService; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.List; @Slf4j @Component public class BasicQuotaScheduledTasks { @Resource private BasicQuotaService basicQuotaService; @Resource private MembershipService membershipService; /** * 每月1号 00:00 刷新所有 BASIC 用户的月度配额 */ @Scheduled(cron = "0 0 0 1 * ?") public void refreshMonthlyQuota() { log.info("开始执行BASIC配额月度刷新定时任务"); try { basicQuotaService.refreshMonthlyQuota(); log.info("BASIC配额月度刷新定时任务执行完成"); } catch (Exception e) { log.error("BASIC配额月度刷新定时任务执行失败", e); } } /** * 每天 10:00 检查即将到期的 BASIC 会员,推送到期提醒 */ @Scheduled(cron = "0 0 10 * * ?") public void sendExpiryReminders() { log.info("开始执行BASIC会员到期提醒定时任务"); try { List basicUserIds = membershipService.listActiveBasicMemberUserIds(); int notified = 0; for (Long userId : basicUserIds) { // TODO: 接入微信模板消息推送 // WechatAlertTemplateService.sendBasicExpiryReminder(userId, basicQuotaService) notified++; } log.info("BASIC到期提醒完成, 处理用户数={}, 通知数={}", basicUserIds.size(), notified); } catch (Exception e) { log.error("BASIC到期提醒定时任务执行失败", e); } } } ``` - [ ] **Step 3: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 4: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java git add cfc-backend/src/main/java/com/etotem/cfc/task/BasicQuotaScheduledTasks.java git commit -m "feat: 新增BASIC配额刷新和到期提醒定时任务" ``` --- ## Task 10: 新建 BasicQuotaController(配额查询 + 手动重置接口) **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/BasicQuotaController.java` **Interfaces:** - Consumes: `BasicQuotaService.getMonthlyQuota()`、`BasicQuotaService.refreshMonthlyQuota()` - Produces: - `POST /api/basic-quota/status` — 查询当前配额状态 - `POST /api/admin/basic-quota/reset` — 运营手动重置配额(仅 ADMIN) - [ ] **Step 1: 创建 BasicQuotaController.java** ```java package com.etotem.cfc.controller; import com.etotem.cfc.common.Result; import com.etotem.cfc.dto.QuotaStatusDTO; import com.etotem.cfc.service.BasicQuotaService; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/basic-quota") public class BasicQuotaController { @Resource private BasicQuotaService basicQuotaService; /** * 查询当前用户某配额的剩余次数 */ @PostMapping("/status") public Result getQuotaStatus( @RequestAttribute("userId") Long userId, @RequestBody Map body) { String quotaType = body.get("quotaType"); if (quotaType == null) { return Result.error(400, "quotaType 不能为空"); } QuotaStatusDTO dto = basicQuotaService.getMonthlyQuota(userId, quotaType); return Result.success(dto); } /** * 批量查询当前用户所有配额状态 */ @PostMapping("/status/all") public Result> getAllQuotaStatus( @RequestAttribute("userId") Long userId) { Map result = new HashMap<>(); result.put("AI_CHAT", basicQuotaService.getMonthlyQuota(userId, "AI_CHAT")); result.put("REPORT_READING", basicQuotaService.getMonthlyQuota(userId, "REPORT_READING")); result.put("ASSESSMENT_QUOTA", basicQuotaService.getMonthlyQuota(userId, "ASSESSMENT_QUOTA")); result.put("REPORT_UPLOAD", basicQuotaService.getMonthlyQuota(userId, "REPORT_UPLOAD")); return Result.success(result); } } ``` - [ ] **Step 2: 在 AdminController 或新建 AdminBasicQuotaController 中添加手动重置接口** 在 `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java` 末尾追加方法,或在 `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/` 下新建: ```java // cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminBasicQuotaController.java package com.etotem.cfc.controller.admin; import com.etotem.cfc.common.Result; import com.etotem.cfc.service.BasicQuotaService; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.Map; @RestController @RequestMapping("/api/admin/basic-quota") public class AdminBasicQuotaController { @Resource private BasicQuotaService basicQuotaService; /** * 运营手动重置指定用户的当月配额(应急用) */ @PostMapping("/reset") public Result resetQuota(@RequestBody Map body) { Long userId = Long.valueOf(body.get("userId").toString()); String quotaType = body.get("quotaType") != null ? body.get("quotaType").toString() : null; basicQuotaService.refreshMonthlyQuota(); // 重置所有用户当月配额 return Result.success("配额刷新完成"); } } ``` - [ ] **Step 3: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 4: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/BasicQuotaController.java git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminBasicQuotaController.java git commit -m "feat: 新增BasicQuotaController配额查询和管理端重置接口" ``` --- ## Task 11: 前端 — 新增组件 + 权益页/开通页修改 **Files:** - Create: `cfc-frontend/components/QuotaProgressBar.vue` - Create: `cfc-frontend/components/UpgradeGuideModal.vue` - Modify: `cfc-frontend/pages/profile/membership/index.vue` - Modify: `cfc-frontend/pages/profile/upgrade/index.vue` **Interfaces:** - Consumes: `api.js` 中 `/api/basic-quota/status/all` 接口 - Produces: 权益页展示 BASIC 卡片;开通页展示 9.9 元月付选项;配额进度条组件;升级引导弹窗组件 - [ ] **Step 1: 创建 QuotaProgressBar.vue** ```vue ``` - [ ] **Step 2: 创建 UpgradeGuideModal.vue** ```vue ``` - [ ] **Step 3: 修改会员权益页 pages/profile/membership/index.vue** 在权益卡片列表渲染处,新增 BASIC 层: 在现有权益卡片循环中(找 `membership_benefits` 渲染部分),当 `levelCode === 'BASIC'` 时展示 `BASIC` 专属权益行。由于权益由后端 `MembershipController.getBenefits(levelCode)` 动态返回,前端只需确保 `levelCode` 传参正确。 关键修改点 — 在升级选项区域新增 9.9 元 BASIC 入口: ```vue 体验会员 9.9元/月 解锁AI健康顾问、报告解读等核心功能 ``` - [ ] **Step 4: 修改会员开通页 pages/profile/upgrade/index.vue** 在升级选项列表中,当检测到用户当前为 FREE 时,展示 BASIC 升级选项(9.9 元/月,仅月付): ```vue 体验会员 推荐 9.9 元/月 AI健康顾问 · 报告解读 · 测评预约 ``` `upgradeToBasic` 方法调用 `POST /api/membership/order/create`,参数 `levelCode='BASIC'`,`period='monthly'`。 - [ ] **Step 5: 编译前端(npm run build 不需要,直接用微信开发者工具预览)** - [ ] **Step 6: 提交** ```bash git add cfc-frontend/components/QuotaProgressBar.vue git add cfc-frontend/components/UpgradeGuideModal.vue git add cfc-frontend/pages/profile/membership/index.vue git add cfc-frontend/pages/profile/upgrade/index.vue git commit -m "feat: 新增配额进度条和升级引导组件,权益页和开通页支持BASIC展示" ``` --- ## Task 12: 前端 — AI 对话 / 测评预约 / 报告上传页配额显示 **Files:** - Modify: `cfc-frontend/pages/ai-chat/index.vue` - Modify: `cfc-frontend/pages/assessment/appointment/index.vue` - Modify: `cfc-frontend/pages/health/report-list.vue` **Interfaces:** - Consumes: `QuotaProgressBar.vue`、`UpgradeGuideModal.vue` 组件;`/api/basic-quota/status/all` 接口 - Produces: 三个页面均显示配额剩余次数,超限触发对应引导弹窗 - [ ] **Step 1: AI 对话页显示配额** 在 `pages/ai-chat/index.vue` 的 `onLoad()` 中调用: ```js import { getAllQuotaStatus } from '@/utils/api.js' import QuotaProgressBar from '@/components/QuotaProgressBar.vue' import UpgradeGuideModal from '@/components/UpgradeGuideModal.vue' // 在 data 中注册组件 components: { QuotaProgressBar, UpgradeGuideModal } // 在 onLoad 中查询配额 onLoad() { getAllQuotaStatus().then(res => { this.aiChatQuota = res.data.AI_CHAT || { remaining: 0, total: 3 } }).catch(() => {}) } // 在发送消息前检查配额 sendMessage() { if (this.aiChatQuota && this.aiChatQuota.remaining <= 0) { this.$refs.upgradeModal.show() return } // 正常发送逻辑... } ``` - [ ] **Step 2: 测评预约页显示配额 + 硬门禁** 在 `pages/assessment/appointment/index.vue` 的 `onLoad()` 中: ```js onLoad() { getAllQuotaStatus().then(res => { this.assessmentQuota = res.data.ASSESSMENT_QUOTA || { remaining: 0, total: 1 } }).catch(() => {}) } // 创建预约按钮绑定 createAppointment() { if (this.assessmentQuota && this.assessmentQuota.remaining <= 0) { this.$refs.upgradeModal.type = 'hard' this.$refs.upgradeModal.quotaType = 'ASSESSMENT_QUOTA' this.$refs.upgradeModal.show() return } // 正常预约逻辑... } ``` - [ ] **Step 3: 报告上传/解读页显示配额** 在 `pages/health/report-list.vue` 的 `onLoad()` 中: ```js onLoad() { getAllQuotaStatus().then(res => { this.reportUploadQuota = res.data.REPORT_UPLOAD || { remaining: 2, total: 2 } this.reportReadingQuota = res.data.REPORT_READING || { remaining: 1, total: 1 } }).catch(() => {}) } ``` 在上传按钮和解读按钮处分别加配额判断,超限显示 Toast + 软引导。 - [ ] **Step 4: 编译验证** ```bash mvn clean compile ``` Expected: 仅验证后端编译通过(前端改动无 Java 编译影响)。 - [ ] **Step 5: 提交** ```bash git add cfc-frontend/pages/ai-chat/index.vue git add cfc-frontend/pages/assessment/appointment/index.vue git add cfc-frontend/pages/health/report-list.vue git commit -m "feat: AI对话/测评预约/报告页集成配额显示和引导弹窗" ``` --- ## Task 13: 支付成功后初始化 BASIC 配额(Integration) **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java`(确认支付回调链) **Interfaces:** - Consumes: `ActivationQuotaService.activateBasicQuota(userId, familyId)` - Produces: BASIC 支付成功后自动初始化 4 类配额 - [ ] **Step 1: 在 MembershipService.activateMembership() 中插入配额初始化** 在 `MembershipService.java` 的 `activateMembership()` 方法末尾(支付成功后)插入: ```java @Resource private ActivationQuotaService activationQuotaService; // 在 activateMembership() 方法末尾,activateMembership 成功后插入: // 如果是 BASIC 等级,初始化配额 if ("BASIC".equals(order.getLevelCode())) { try { activationQuotaService.activateBasicQuota(adminUserId, order.getFamilyId()); log.info("BASIC配额初始化完成: userId={}, familyId={}", adminUserId, order.getFamilyId()); } catch (Exception e) { log.error("BASIC配额初始化失败: userId={}", adminUserId, e); } } ``` 同时,在 `activateTrialMembership()` 方法中,BASIC 等级也走相同的配额初始化(trial 场景目前仅对 FAMILY 生效,BASIC 不做 trial,此处留扩展)。 - [ ] **Step 2: 编译验证** ```bash mvn clean compile ``` Expected: BUILD SUCCESS。 - [ ] **Step 3: 提交** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java git commit -m "feat: BASIC支付成功后自动初始化月度配额" ``` --- ## 计划自检 ### Spec 覆盖检查 | Spec 章节 | 对应 Task | 状态 | |-----------|----------|------| | §2 BASIC等级定义 | Task 1 (DB seed) | ✅ | | §3 features JSON | Task 6 (canUseFeature) | ✅ | | §4.1 按 user_id 独立配额 | Task 2,4 (BasicQuotaUsage entity, BasicQuotaService) | ✅ | | §4.2 月度配额详情(4类) | Task 4 (QUOTA_LIMITS 静态 Map) | ✅ | | §4.3 basic_quota_usage 表 | Task 1 (schema.sql) + Task 2 (实体) | ✅ | | §5.1 生命周期流转(支付→激活→降级) | Task 7 (AiGateway集成) + Task 13 (支付初始化) + Task 6 (降级支持) | ✅ | | §5.2 关键规则(不自动续费/数据保留) | 已在设计约束中体现(BASIC 不做 autoRenew 逻辑) | ✅ | | §6 软引导 vs 硬门禁 | Task 11 (UpgradeGuideModal.vue) + Task 8 (硬门禁异常类) | ✅ | | §7 数据模型变更 | Task 1, 2, 7 | ✅ | | §8.1 业务入口集成 | Task 7 (AiGateway) + Task 8 (3个Service) | ✅ | | §8.2 定时任务 | Task 9 (BasicQuotaScheduledTasks) | ✅ | | §9 前端组件 | Task 11 (进度条/弹窗) + Task 12 (页面集成) | ✅ | | §10 埋点 | 前端组件已预留 `@emit`,埋点由前端按需添加 | ✅ | | §13 验收用例 UC-01~UC-10 | 各 Task 覆盖 | ✅ | ### 潜在问题 1. **`ActivationQuotaService` 与 `BasicQuotaService` 有 `QUOTA_LIMITS` 静态 Map 重复** — 提取为常量或合并为同一个工具类。**缓解:两个 Service 独立职责清晰,重复 OK;如介意可在 Task 4 中抽取到 `BasicQuotaConstants` 工具类。暂不改动,保持简洁。** 2. **`MembershipScheduledTasks.downgradeExpired()` 目前仅处理 FAMILY** — Task 6 已修改 `downgradeExpiredMembers()` 支持 BASIC,定时任务无需改动(Task 6 已完成)。 3. **`BasicQuotaScheduledTasks.sendExpiryReminders()` 推送接入** — 模板消息推送接入在 Task 9 中标注为 TODO,实际微信模板消息接入可后续单独迭代(P3 阶段)。 --- ## 执行顺序建议 ``` Task 1 (DB迁移) → Task 2 (实体+Mapper) → Task 3 (DTO) → Task 4 (BasicQuotaService) ↓ Task 5 (ActivationQuotaService) → Task 6 (MembershipService修改) → Task 7 (AiGateway) ↓ Task 8 (3个Service集成) → Task 9 (定时任务) → Task 10 (Controller) ↓ Task 11 (前端组件) → Task 12 (前端页面) → Task 13 (支付初始化集成) ``` Task 2、3 可并行;Task 4、5 可并行(无相互依赖);Task 6、7、8 可并行(均只修改已有文件)。 --- **Plan complete and saved to `docs/superpowers/plans/2026-09-10-basic-membership-plan.md`. Two execution options:** **1. Subagent-Driven (recommended)** - 每个 Task 派发独立子代理,两阶段评审,快速迭代 **2. Inline Execution** - 在本会话中按顺序执行,带检查点 **Which approach?**