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
@PostMapping,禁止 @GetMapping/@PutMapping/@DeleteMapping@TableName + @TableId(type = IdType.AUTO)@Resource,字段名必须与类型默认 Bean Name 一致Result<T>(code/message/data)?.(用 && 替代)、禁用 CSS Grid(用 flexbox)、禁用 :key 表达式new Date(string),用 utils/format.js 的 parseDate()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 | 天数 | 范围 | 里程碑 |
|---|---|---|---|
| P0 核心链路 | 3 天 | DB 迁移 + BASIC 等级 + 支付开通 + 基础权限 | 9.9 元支付成功,BASIC 生效,能进沙盘/AI 对话 |
| P1 配额系统 | 2 天 | BasicQuotaService + 4 个业务入口扣减 + 定时刷新 |
月度配额正确扣减/刷新,超限引导生效 |
| P2 前端展示 | 2 天 | 权益页/开通页/进度条/弹窗 | 免费用户可见 BASIC,付费后正确显示剩余次数 |
| P3 运营配套 | 2 天 | 定时提醒 + 管理端配额看板 + 手动重置接口 | 到期提醒自动发送,看板可查 |
Files:
cfc-backend/src/main/resources/schema.sql (末尾追加)cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java (末尾 runMigrations() 追加)mvn compile 验证)Interfaces:
Produces: basic_quota_usage 表;membership_levels 新增 BASIC 记录;membership_benefits 新增 5 条 BASIC 权益记录
[ ] Step 1: schema.sql 末尾追加基本表定义 + seed 数据
在 schema.sql 末尾(OFFSET 5513 之后)追加以下内容:
-- ============================================================
-- 新增 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会员月度配额使用记录';
在 DatabaseInitializer.java 的 runMigrations() 方法末尾(最新迁移编号之后)追加:
// 迁移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: 编译验证
mvn clean compile
Expected: BUILD SUCCESS,无编译错误。
[ ] Step 4: 提交
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)"
Files:
cfc-backend/src/main/java/com/etotem/cfc/entity/BasicQuotaUsage.javacfc-backend/src/main/java/com/etotem/cfc/mapper/BasicQuotaUsageMapper.javaInterfaces:
Produces: BasicQuotaUsage 实体(字段:id, userId, quotaType, usedCount, totalCount, periodStart, periodEnd, createdAt, updatedAt),BasicQuotaUsageMapper 接口(继承 BaseMapper<BasicQuotaUsage>)
[ ] Step 1: 创建 BasicQuotaUsage.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
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<BasicQuotaUsage> {
}
[ ] Step 3: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 4: 提交
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"
Files:
cfc-backend/src/main/java/com/etotem/cfc/dto/QuotaStatusDTO.javacfc-backend/src/main/java/com/etotem/cfc/dto/QuotaConsumeResult.javaInterfaces:
Produces: QuotaStatusDTO(字段:quotaType, total, used, remaining, periodStart, periodEnd);QuotaConsumeResult(字段:success, remaining, toastMsg, actionUrl)
[ ] Step 1: 创建 QuotaStatusDTO.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
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: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 4: 提交
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"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.javamvn compile 验证编译)Interfaces:
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
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<String, Integer> 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<BasicQuotaUsage>()
.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<BasicQuotaUsage>()
.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<BasicQuotaUsage> lastMonthRecords = basicQuotaUsageMapper.selectList(
new LambdaQueryWrapper<BasicQuotaUsage>()
.eq(BasicQuotaUsage::getPeriodStart, lastMonthStart));
Set<Long> 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<BasicQuotaUsage>()
.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: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 3: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/service/BasicQuotaService.java
git commit -m "feat: 实现BasicQuotaService配额核心逻辑"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.javaInterfaces:
BasicQuotaUsageMapperProduces: void activateBasicQuota(Long userId, Long familyId) — 支付成功后为 BASIC 用户初始化当月 4 类配额记录
[ ] Step 1: 创建 ActivationQuotaService.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<String, Integer> 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<String, Integer> 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: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 3: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/service/ActivationQuotaService.java
git commit -m "feat: 新增ActivationQuotaService支付后初始化配额"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java (两处修改)Interfaces:
BasicQuotaService(由调用方注入)Produces: canUseFeature 支持 BASIC;downgradeExpiredMembers 支持 BASIC 降级
[ ] Step 1: canUseFeature() 新增 BASIC 分支
在 MembershipService.java 的 canUseFeature() 方法中,在 FREE 和 FAMILY 分支之间插入:
// 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)) { 之前。
在 MembershipService.java 的 downgradeExpiredMembers() 方法中,修改条件判断:
修改前:
if ("FAMILY".equals(user.getMemberLevel())
&& user.getMemberExpireTime() != null
&& user.getMemberExpireTime().before(new Date())) {
修改后:
if (("FAMILY".equals(user.getMemberLevel()) || "BASIC".equals(user.getMemberLevel()))
&& user.getMemberExpireTime() != null
&& user.getMemberExpireTime().before(new Date())) {
同时在方法体内,降级时记录日志区分等级:
user.setMemberLevel("FREE");
user.setMemberExpireTime(null);
user.setUpdatedAt(new Date());
userMapper.updateById(user);
log.info("会员到期降级: userId={}, 原等级={}", userId, level); // 记录原等级
在 getMemberLevel() 方法中,FAMILY 判断之外补充 BASIC:
修改前:
if ("FAMILY".equals(level) && user.getMemberExpireTime() != null) {
if (user.getMemberExpireTime().before(new Date())) {
return "FREE";
}
}
修改后:
if (("FAMILY".equals(level) || "BASIC".equals(level)) && user.getMemberExpireTime() != null) {
if (user.getMemberExpireTime().before(new Date())) {
return "FREE";
}
}
[ ] Step 4: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 5: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
git commit -m "feat: MembershipService支持BASIC等级权限判断和到期降级"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.javaInterfaces:
BasicQuotaService.consumeQuota(userId, "AI_CHAT")Produces: BASIC 用户对话时自动扣减配额,配额不足时返回 null(由 AIChatController 处理 403 响应)
[ ] Step 1: 在 chat() 方法开头插入配额检查
在 AiGateway.chat() 方法中,找到:
public Map<String, Object> chat(String query, Long userId, String conversationId,
Map<String, Object> inputs) {
if (!enabled || isCircuitOpen()) return null;
在 if (!enabled || isCircuitOpen()) return null; 之后插入:
// 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:
@Resource
private BasicQuotaService basicQuotaService;
[ ] Step 2: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 3: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
git commit -m "feat: AiGateway.chat()插入BASIC AI配额扣减"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/ReportSummaryService.javacfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.javacfc-backend/src/main/java/com/etotem/cfc/service/DanReportUploadService.javaInterfaces:
BasicQuotaService.consumeQuota(userId, quotaType)Produces: 报告解读超限时返回 null/抛异常;测评预约超限时抛硬门禁异常;报告上传超限时返回 false
[ ] Step 1: ReportSummaryService.parseReport() 插入配额检查
在 ReportSummaryService.java 的 parseReport() 方法入口插入(找到方法签名后第一行逻辑):
@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());
}
}
在 AssessmentAppointmentService.java 的 create() 方法入口插入:
@Resource
private BasicQuotaService basicQuotaService;
// 在方法体开头插入:
if (userId != null) {
QuotaConsumeResult result = basicQuotaService.consumeQuota(userId, "ASSESSMENT_QUOTA");
if (!result.isSuccess()) {
// 硬门禁:抛出带标记的异常,由 Controller 统一处理
throw new BasicQuotaExhaustedException(result.getToastMsg());
}
}
同时创建异常类:
// 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);
}
}
在 DanReportUploadService.java 的 upload() 方法入口插入:
@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: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 5: 提交
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配额检查"
Files:
cfc-backend/src/main/java/com/etotem/cfc/task/BasicQuotaScheduledTasks.javacfc-backend/src/main/java/com/etotem/cfc/task/MembershipScheduledTasks.java(添加到期提醒任务)Interfaces:
BasicQuotaService.refreshMonthlyQuota()、MembershipService.listActiveBasicMemberUserIds()Produces: 每月1号刷新配额;每天10点推到期提醒
[ ] Step 1: 在 MembershipService 新增 listActiveBasicMemberUserIds() 方法
在 MembershipService.java 末尾(listActiveMemberFamilyIds 方法之后)追加:
/**
* 查询指定等级当前有效的基本会员用户ID列表(BASIC到期提醒用)
*/
public List<Long> listActiveBasicMemberUserIds() {
Date now = new Date();
List<User> basicUsers = userMapper.selectList(
new LambdaQueryWrapper<User>()
.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
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<Long> 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: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 4: 提交
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配额刷新和到期提醒定时任务"
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/BasicQuotaController.javaInterfaces:
BasicQuotaService.getMonthlyQuota()、BasicQuotaService.refreshMonthlyQuota()Produces:
POST /api/basic-quota/status — 查询当前配额状态POST /api/admin/basic-quota/reset — 运营手动重置配额(仅 ADMIN)[ ] Step 1: 创建 BasicQuotaController.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<QuotaStatusDTO> getQuotaStatus(
@RequestAttribute("userId") Long userId,
@RequestBody Map<String, String> 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<Map<String, QuotaStatusDTO>> getAllQuotaStatus(
@RequestAttribute("userId") Long userId) {
Map<String, QuotaStatusDTO> 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/ 下新建:
// 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<String> resetQuota(@RequestBody Map<String, Object> 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: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 4: 提交
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配额查询和管理端重置接口"
Files:
cfc-frontend/components/QuotaProgressBar.vuecfc-frontend/components/UpgradeGuideModal.vuecfc-frontend/pages/profile/membership/index.vuecfc-frontend/pages/profile/upgrade/index.vueInterfaces:
api.js 中 /api/basic-quota/status/all 接口Produces: 权益页展示 BASIC 卡片;开通页展示 9.9 元月付选项;配额进度条组件;升级引导弹窗组件
[ ] Step 1: 创建 QuotaProgressBar.vue
<template>
<view class="quota-progress-bar">
<view class="quota-label">
<text class="quota-name">{{ name }}</text>
<text class="quota-count">{{ remaining }} / {{ total }}</text>
</view>
<view class="quota-track">
<view class="quota-fill" :style="{ width: percent + '%' }"></view>
</view>
</view>
</template>
<script>
export default {
name: 'QuotaProgressBar',
props: {
remaining: { type: Number, default: 0 },
total: { type: Number, default: 0 },
name: { type: String, default: '' }
},
computed: {
percent() {
if (!this.total) return 0
return Math.round((this.remaining / this.total) * 100)
}
}
}
</script>
<style scoped>
.quota-progress-bar {
margin-bottom: 16rpx;
}
.quota-label {
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.quota-track {
height: 12rpx;
background: #f0f0f0;
border-radius: 6rpx;
overflow: hidden;
}
.quota-fill {
height: 100%;
background: #F97316;
border-radius: 6rpx;
transition: width 0.3s;
}
</style>
[ ] Step 2: 创建 UpgradeGuideModal.vue
<template>
<view class="upgrade-guide-mask" v-if="visible" @click="handleMaskClick">
<view class="upgrade-guide-content" :class="{ 'hard-modal': type === 'hard' }" @click.stop>
<view class="upgrade-guide-icon">{{ icon }}</view>
<view class="upgrade-guide-title">{{ title }}</view>
<view class="upgrade-guide-desc">{{ desc }}</view>
<view class="upgrade-guide-actions">
<view v-if="type === 'soft'" class="btn-know" @click="handleKnow">知道了</view>
<view v-if="type === 'soft'" class="btn-learn" @click="handleLearn">了解详情</view>
<view v-if="type === 'hard'" class="btn-upgrade" @click="handleUpgrade">升级 FAMILY</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UpgradeGuideModal',
data() {
return { visible: false }
},
props: {
type: { type: String, default: 'soft' }, // soft | hard
quotaType: { type: String, default: '' },
remaining: { type: Number, default: 0 },
total: { type: Number, default: 0 }
},
computed: {
icon() {
return this.type === 'hard' ? '🔒' : 'ℹ️'
},
title() {
return this.type === 'hard' ? '升级后即可使用' : '本月额度已用完'
},
desc() {
if (this.type === 'hard') return '该功能为 FAMILY 会员专属权益,升级后即可使用'
if (this.quotaType === 'AI_CHAT') return `您本月 AI 对话已用完(${this.total}/${this.total}),升级 FAMILY 享不限次畅聊`
if (this.quotaType === 'REPORT_READING') return '本月报告解读额度已用完,升级 FAMILY 解锁不限次深度解读'
if (this.quotaType === 'REPORT_UPLOAD') return `本月报告上传额度已用完(${this.total}/${this.total}),升级 FAMILY 解锁更多`
return '本月额度已用完,升级 FAMILY 解锁更多权益'
}
},
methods: {
show() {
this.visible = true
},
hide() {
this.visible = false
},
handleKnow() {
this.hide()
this.$emit('know')
},
handleLearn() {
this.hide()
this.$emit('learn')
},
handleUpgrade() {
this.hide()
uni.navigateTo({ url: '/pages/profile/upgrade/index' })
this.$emit('upgrade')
},
handleMaskClick() {
if (this.type === 'soft') {
this.hide()
}
}
}
}
</script>
<style scoped>
.upgrade-guide-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.upgrade-guide-content {
width: 600rpx;
background: #fff;
border-radius: 24rpx;
padding: 48rpx 36rpx 36rpx;
text-align: center;
}
.upgrade-guide-icon {
font-size: 72rpx;
margin-bottom: 24rpx;
}
.upgrade-guide-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.upgrade-guide-desc {
font-size: 26rpx;
color: #666;
line-height: 1.6;
margin-bottom: 36rpx;
}
.upgrade-guide-actions {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.btn-know, .btn-learn, .btn-upgrade {
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
}
.btn-know {
background: #f5f5f5;
color: #666;
}
.btn-learn {
background: transparent;
color: #F97316;
border: 2rpx solid #F97316;
}
.btn-upgrade {
background: #F97316;
color: #fff;
}
</style>
[ ] Step 3: 修改会员权益页 pages/profile/membership/index.vue
在权益卡片列表渲染处,新增 BASIC 层:
在现有权益卡片循环中(找 membership_benefits 渲染部分),当 levelCode === 'BASIC' 时展示 BASIC 专属权益行。由于权益由后端 MembershipController.getBenefits(levelCode) 动态返回,前端只需确保 levelCode 传参正确。
关键修改点 — 在升级选项区域新增 9.9 元 BASIC 入口:
<!-- 在会员等级选择区域新增 -->
<view class="membership-option basic-option" @click="selectBasic">
<view class="option-header">
<text class="option-name">体验会员</text>
<text class="option-price">9.9元<span class="unit">/月</span></text>
</view>
<view class="option-desc">解锁AI健康顾问、报告解读等核心功能</view>
</view>
在升级选项列表中,当检测到用户当前为 FREE 时,展示 BASIC 升级选项(9.9 元/月,仅月付):
<view v-if="currentLevel === 'FREE'" class="upgrade-card basic-card" @click="upgradeToBasic">
<view class="card-title">
<text class="title-text">体验会员</text>
<text class="title-badge">推荐</text>
</view>
<view class="card-price">
<text class="price-num">9.9</text>
<text class="price-unit">元/月</text>
</view>
<view class="card-features">
<text>AI健康顾问 · 报告解读 · 测评预约</text>
</view>
<button class="card-btn" hover-class="card-btn-hover">立即开通</button>
</view>
upgradeToBasic 方法调用 POST /api/membership/order/create,参数 levelCode='BASIC',period='monthly'。
[ ] Step 5: 编译前端(npm run build 不需要,直接用微信开发者工具预览)
[ ] Step 6: 提交
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展示"
Files:
cfc-frontend/pages/ai-chat/index.vuecfc-frontend/pages/assessment/appointment/index.vuecfc-frontend/pages/health/report-list.vueInterfaces:
QuotaProgressBar.vue、UpgradeGuideModal.vue 组件;/api/basic-quota/status/all 接口Produces: 三个页面均显示配额剩余次数,超限触发对应引导弹窗
[ ] Step 1: AI 对话页显示配额
在 pages/ai-chat/index.vue 的 onLoad() 中调用:
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
}
// 正常发送逻辑...
}
在 pages/assessment/appointment/index.vue 的 onLoad() 中:
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
}
// 正常预约逻辑...
}
在 pages/health/report-list.vue 的 onLoad() 中:
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: 编译验证
mvn clean compile
Expected: 仅验证后端编译通过(前端改动无 Java 编译影响)。
[ ] Step 5: 提交
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对话/测评预约/报告页集成配额显示和引导弹窗"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.javacfc-backend/src/main/java/com/etotem/cfc/controller/MembershipController.java(确认支付回调链)Interfaces:
ActivationQuotaService.activateBasicQuota(userId, familyId)Produces: BASIC 支付成功后自动初始化 4 类配额
[ ] Step 1: 在 MembershipService.activateMembership() 中插入配额初始化
在 MembershipService.java 的 activateMembership() 方法末尾(支付成功后)插入:
@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: 编译验证
mvn clean compile
Expected: BUILD SUCCESS。
[ ] Step 3: 提交
git add cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java
git commit -m "feat: BASIC支付成功后自动初始化月度配额"
| 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 覆盖 | ✅ |
ActivationQuotaService 与 BasicQuotaService 有 QUOTA_LIMITS 静态 Map 重复 — 提取为常量或合并为同一个工具类。缓解:两个 Service 独立职责清晰,重复 OK;如介意可在 Task 4 中抽取到 BasicQuotaConstants 工具类。暂不改动,保持简洁。
MembershipScheduledTasks.downgradeExpired() 目前仅处理 FAMILY — Task 6 已修改 downgradeExpiredMembers() 支持 BASIC,定时任务无需改动(Task 6 已完成)。
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?