状态:待用户审查 · 日期:2026-08-23 · 作者:Sisyphus (brainstorming 产出) 关联:
docs/mascot-design.md(IP 官方命名)、docs/function-analysis-by-membership.md、会员体系docs/superpowers/specs/2026-07-09-family-membership-design.md
两条需求线:
| # | 决策点 | 结论 |
|---|---|---|
| 1 | 管家选择模式 | 自助直选:浏览→直接绑定,系统校验等级+容量,可随时更换;无推送通知 |
| 2 | 高级档位判定 | 仅 L2 有效订阅家庭可自选管家;L1 仅 AI 教练 |
| 3 | 人格差异方式 | 性格话术差异化:独立 system prompt,共享知识库与工具链 |
| 4 | 选择粒度 | 用户级偏好(users.mascot 已按 user 隔离) |
| 5 | 管理端范围 | 增量补齐:分配管理视图 + 强制解绑 + 管家停接新单开关;保留现有审核/结算 |
| 6 | 存储方案修订 | 初选"新建 coach_personas 表",发现现有吉祥物管线后改为复用(users.mascot + MascotEnum + system_prompts),不建重复表 |
| 7 | 命名 | 维持现名浠宝/福宝(IP 设计书 v2.2 官方命名),code 保持 xibao/fubao |
| 层 | 资产 | 说明 |
|---|---|---|
| DB | users.mascot VARCHAR(20) |
用户级形象选择,值 xibao/fubao,schema.sql:34 |
| Java 枚举 | MascotEnum(enums/MascotEnum.java) |
XIBAO("xibao","女","温柔可爱的浠宝小女孩") displayName 浠宝;FUBAO("fubao","男","聪明活泼的福宝小男孩") displayName 福宝 |
| 注入 | AIChatController./chat/send(:76-109)、/nutrition/send(:245-272) |
读 user.getMascot() → inputs 放入 mascot_name/mascot_gender/mascot_persona |
| 缺口① | AIChatController./health-coach/send(:420-447) |
漏注入,需补齐 |
| LangGraph 模型 | app/models/common.py:11 mascot_code: Optional[str] |
字段已预留 |
| 缺口② | app/graphs/health_coach_graph.py:89 |
只挂单一 get_prompt("health_coach"),无人格路由 |
| 提示词服务 | app/prompt_service.py |
从 system_prompts 表按 prompt_key 加载(经 Java /api/system-prompts/get,TTL 缓存),DEFAULT_PROMPTS 兜底 |
| 管理端 | cfc-web/src/views/admin/SystemPromptManagement.vue + system_prompts 表(schema.sql:4990) |
prompt 内容后台可配,零发版改话术 |
| 选择入口 | cfc-frontend/pages/user-edit/user-edit.vue |
mascot 切换 UI 已有 |
| 渲染 | cfc-frontend/pages/ai/chat.vue(944行) |
mascotCode/mascotName/mascotIcon 主题化渲染已有 |
| 资产 | 说明 |
|---|---|
butler_profiles(schema.sql:2626) |
tier/status/member_count/max_members/annual_fee_l1,l2/commission_rate_l1,l2/certificates/description |
butler_assignments(schema.sql:2690) |
family_id UNIQUE(uk_family_assignment) ↔ butler_user_id + subscription_id + level + assigned_at;一家一管家 |
| 订阅激活自动分配 | MemberSubscriptionService.activateSubscription:165 → ButlerService.assignMemberToButler(familyId,subscriptionId,level,amount):298:approved+tier=level+member_count<max_members 按 created_at 轮询取首个;可能找不到(无分配行) |
| 家庭端控制器 | controller/butler/ButlerController.java /api/butler/*(apply/profile/update-profile/members/service-records/commissions)——均为管家自身视角 |
| 管理端控制器 | AdminButlerController /api/admin/butler/list\|review\|assign-member\|service-records\|settlements——assign-member 为管理员手动指派 |
| 订阅校验 | MemberSubscriptionService.getActiveSubscription(familyId):54 / hasActiveSubscription:78 |
| Web 管理端 | views/admin/review/ButlerAudit.vue、ReviewCenter.vue、ServiceRoleApplications.vue、api/butler.js、permissions.js |
| 小程序 | pages/butler/apply.vue(管家申请入驻页) |
仅 1 处变更:
-- butler_profiles 增加(DatabaseInitializer.ensureColumn 迁移 + schema.sql 同步)
ALTER TABLE butler_profiles ADD COLUMN accepting TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否接受新家庭绑定 0=停接';
迁移要求:
config/DatabaseInitializer.runMigrations() 内 ensureColumn("butler_profiles", "accepting", ...),幂等schema.sql 的 butler_profiles CREATE TABLE(:2626 起)mvn clean compile 验证其余全部复用:users.mascot 不动;butler_assignments 换绑走 UPDATE;无新表。
FamilyButlerSelectController,放 controller/butler/ 包;Bean 名 familyButlerSelectController 与现有 butlerController 无冲突;业务方法加入现有 ButlerService)familyId 推导方式与 MembershipController 一致:userMapper.selectById(userId).getFamilyId();familyId 为空 → error("未找到家庭信息")。
| 接口 | 校验/逻辑 | 返回 |
|---|---|---|
POST /api/butler/available-list |
查询条件:status='approved' AND tier='L2' AND accepting=1 AND member_count<max_members。浏览不校验 L2(非 L2 可看列表,绑定才校验)。脱敏:昵称/头像/简介/证书数/剩余名额,不含联系方式 | Result<List<Map>> |
POST /api/butler/my-butler |
按 familyId(取自 userId 归属家庭)查 assignments 行 + 关联管家摘要 + 当前订阅等级 | 有绑定返回详情;无绑定 data=null |
POST /api/butler/select {butlerUserId} |
事务内依次:①getActiveSubscription 且 level=='L2',否则 error("该功能面向久久一生会员")②目标管家 approved+tier='L2'+accepting=1③条件增容 UPDATE butler_profiles SET member_count=member_count+1 WHERE id=? AND status='approved' AND accepting=1 AND member_count<max_members,影响行数=0 → error("该管家名额已满") 并终止④assignments 存在行则 UPDATE butler_user_id(旧管家 member_count-1),不存在则 INSERT(family_id 冲突时按唯一键重试 UPDATE 路径)⑤记录 assigned_at 刷新 |
success 后返回新管家摘要 |
并发安全:容量扣减用条件 UPDATE 原子完成;解绑旧管家计数在成功换绑后执行;整体 @Transactional(rollbackFor = Exception.class)。
AIChatController)| 位置 | 变更 |
|---|---|
/health-coach/send |
补 mascot 注入,与 /chat/send 对齐:inputs 增加 mascot_name/mascot_gender/mascot_persona + coach_id = mascotCode 原始值(xibao/fubao/null);缺省时 inputs.put("coach_id","") 由 LangGraph 兜底 |
UserController 无改动(mascot 更新已有 updateProfile 路径);AiGateway.chat 签名不变(inputs 透传)。
实现注意(已核实):AiGateway.chat 将 inputs 中 String/Number/Boolean 键值写入请求体 context 对象(AiGateway.java:162-168),LangGraph 侧从 state["context"] 读取——故 coach_id 放入 inputs 即可透传,无需修改 common.py 传输模型(其 mascot_code 字段与本次路由无关)。PII 脱敏 sanitizeContext 对 'xibao'/'fubao' 短码无影响。
AdminButlerController 增量,沿用 /api/admin/butler/* 前缀与角色校验风格)| 接口 | 逻辑 |
|---|---|
POST /api/admin/butler/assignments |
分页列表:联 users(家长昵称/头像)、butler_assignments、users(管家昵称)、member_subscription.level;筛选:管家ID/级别/关键字 |
POST /api/admin/butler/unassign {assignmentId} |
强制解绑:物理删除 assignments 行(服务留痕在 butler_service_records,无需保留分配行)+ 对应管家 member_count-1,事务 |
POST /api/admin/butler/toggle-accepting {profileId, accepting} |
更新 butler_profiles.accepting;仅影响新选择,不动存量绑定 |
新增接口须同步登记 docs/superpowers/api/API_REFERENCE.md。
coach_id 传输路径:Java inputs → AiGateway body.context(已核实)→ FastAPI → graph state 的 context 字典,无需改 common.py 传输模型;generate_answer 从 state["context"] 取值generate_answer 中提示词路由:
ctx = state.get("context") or {}
coach_id = ctx.get("coach_id")
if coach_id not in ("xibao", "fubao"):
coach_id = None
system_prompt = (
await get_prompt(f"health_coach_{coach_id}") if coach_id else None
) or await get_prompt("health_coach") or DEFAULT_COACH_PROMPT
即按人格 key 优先,逐级回退到全局 health_coach,再回退内置默认——任何一环缺失都不报错。
health_coach_xibao:温柔知心风(倾听共情、温和建议、多用鼓励性语言)health_coach_fubao:活泼行动派(直接给行动清单、打卡式督促、轻快语气)| 文件 | 变更 |
|---|---|
pages/ai/chat.vue |
微调欢迎语:isHealthCoach 分支由固定"你好!我是健康教练"改为拼接当前教练名(如"你好!我是{{mascotName}},你的健康教练");消息头像/导航主题已按 mascot 渲染不动 |
pages/user-edit/user-edit.vue |
已有 mascot 选择项;核对选项描述文案体现"AI 健康教练"语境,不改逻辑 |
新页 pages/butler/select.vue |
①管家卡片列表(头像/昵称/简介/证书数/剩余名额徽标)+ 点击出确认弹窗(uni.showModal)②顶部状态区三态:未绑定(去选择)/已绑定(管家摘要卡+"更换管家"按钮)/L2 未订阅或过期(升级引导条跳会员购买页)③非 L2 用户可浏览列表但点选择时报错 toast(后端兜底)④遵守小程序约束:无 ?.、flexbox、:key="getItemKey(item)" 方法调用、日期 parseDate()、禁止 CSS Grid |
| 会员中心相关页 | 新增「我的管家」入口卡片:同上三态摘要;点击进 select 页 |
pages.json |
注册 select 页面路径与标题 |
API 封装加至小程序 utils/api.js:getAvailableButlers / getMyButler / selectButler。
| 文件 | 变更 |
|---|---|
新视图 views/admin/ButlerAssignments.vue |
el-table 分配关系:家庭ID/家长昵称/管家昵称/订阅级别/绑定时间;筛选栏(管家下拉+关键字);操作列「强制解绑」→ elMessageBox.confirm → unassign 接口 → 刷新 |
views/admin/review/ButlerAudit.vue(管家档案所在列表) |
新列「接单中」el-switch 绑 accepting,change 时调 toggle-accepting;accepting=0 行灰色标识"已停接" |
api/butler.js |
新增 getAssignments / unassignAssignment / toggleAccepting 封装 |
路由表 + permissions.js |
注册 ButlerAssignments 页面与权限码 |
| 场景 | 处理 |
|---|---|
| L2 到期仍有绑定 | 不自动解绑;家庭端入口显示"订阅已到期";管理员可强制解绑;无定时任务 |
| 并发选满 | 条件 UPDATE 原子扣容(§5.1 步骤③),0 行受影响即"名额已满"回滚 |
| 换绑频率 | 不限次数,每次完整校验 |
| coach_id 缺失/非法 | 三级回退:人格 key → 全局 health_coach → DEFAULT_COACH_PROMPT,不报错 |
| LangGraph 不可用 | AiGateway 熔断+Fallback 已覆盖,不动 |
| 管家停接 | accepting=0 仅拦截新绑定(available-list 过滤 + select 校验双重拦截);存量家庭不受影响,其会话照常 |
| 强制解绑后 | 家庭立即可重新自选任意合格管家 |
| 首次选择 vs 换绑 | 订阅激活自动分配可能未产生 assignments 行(当时无合格管家):select 接口 INSERT/UPDATE 双路径兼容(§5.1 步骤④) |
| 旧管家计数一致性 | 换绑/解绑均同步 member_count ±1;存量数据漂移校正不在本期范围(member_count 一律以条件更新维护) |
/api/ai/butler/* 已有)cd cfc-backend && mvn clean compile 通过(EXIT 0)node --check 通过(不做整包 build)/health-coach/send 对话风格随 prompt key 生效;mascot 未设置时回退默认 prompt 正常对话API_REFERENCE.md 已登记三个家庭端接口与三个管理端接口后端迁移+接口 → LangGraph prompt 路由(可与后端并行)→ 小程序页面 → Web 管理端 → API 文档同步 → 端到端验证