# Phase 1 用户故事与验收标准 > 基于 `phase1-product-plan.md` 与 `1.0产品对齐方案.md`,每个用户故事包含:标题、描述、验收标准、优先级 --- ## EPIC 1:命盘生成与展示(P0) ### US-1.1 生日输入与咨询发起 **作为** 数字能量师/C端用户 **我希望** 输入姓名和出生年月日,发起一次命盘咨询 **以便** 获取命盘展示和AI解读服务 **验收标准(输入表单):** 1. 输入页面包含:姓名(选填)、出生年份(4位数字)、月份(1-12)、日期(1-31) 2. 月份和日期支持数字键盘输入,也有下拉选择器 3. 验证:年份必须为 1900-当前年份、月份 1-12、日期根据月份合法性验证 4. 输入无效时显示具体错误提示(如"请输入正确的出生日期") 5. "想了解的问题" 输入框限制 100 字以内(输入时实时显示剩余字数) 6. 点击"开始咨询"后,调用后端 `POST /api/consultation/start` 发起咨询 **验收标准(咨询会话管理):** 7. 后端按照 **userId + birthday** 唯一确定一条咨询记录(同用户+同生日始终返回同一条记录) 8. 同一用户对同一生日重复点击"开始咨询",后端直接返回已有的咨询记录 + 完整聊天历史,**不创建新记录** 9. 输入的生日是**全新**生日时,前端弹出确认对话框:"此生日将开启全新的命盘咨询,确认吗?" 10. 用户确认后,后端计算完整的数字命盘(24个位置),创建新咨询记录,并返回结果 11. 用户取消确认,停留在首页,不跳转、不创建 12. 点击"开始咨询"到命盘渲染完成,总耗时不超过 3 秒 13. 命盘计算全部由后端 `CalculatorService` 完成,前端只负责展示(`calculator.js` 保留为降级兜底,不作为主路径) 14. 命盘页面 URL 可分享 ### US-1.2 三角形命盘可视化展示 **作为** 数字能量师 **我希望** 看到一个清晰、美观的数字三角形,每个位置都标有数字 **以便** 直接用于客户讲解 **验收标准:** 1. 页面展示完整的24个数字位置(8个底部输入位 A-H + 7个三角内部位 I-O + 9个外部三组位 P-X) 2. 内部三角形布局严格遵循(新字母命名): ``` O / \ M N / \ / \ I J K L ``` 3. 底部输入框布局:日(2位)| 月(2位)| 年(4位),从左到右排列(已改为"日月年"顺序) 4. 外部三组以T形线段连接三角内部对应位置: - 左侧组(R, P, Q)对应 21-40 岁,由 I, J, M 计算 - 右侧组(U, S, T)对应 61+ 岁,由 K, L, N 计算 - 顶部组(X, V, W)对应 41-60 岁,由 M, N, O 计算 5. 外部三组使用独立的计算值,非内部数字的直接重复 6. 每个数字使用单独的圆形/方形卡片展示,字体清晰易读 7. 五区使用不同背景色区分(左区/中区/右区/父源/母源/主性格) 8. 数字颜色:1-9 每个数字有独立颜色(参考能量数字配色) 9. 卓越数(11/22/33)用特殊标记突出 10. 底部显示姓名和出生日期信息 ### US-1.3 命盘数字位置命名与计算规则(对照表) **作为** 开发团队 **我希望** 有一份完整的位置命名与计算规则对照表作为开发基准 **以便** 前后端计算逻辑一致,避免歧义 #### 1. 完整字母命名表(A-X) ##### 输入层(8位)—— 生日原始数字 | 字母 | 来源 | 示例 1990-01-15 | 说明 | |------|------|----------------|------| | A | 年份千位 | 1 | 年[0] | | B | 年份百位 | 9 | 年[1] | | C | 年份十位 | 9 | 年[2] | | D | 年份个位 | 0 | 年[3] | | E | 月份十位 | 0 | 月[0] | | F | 月份个位 | 1 | 月[1] | | G | 日期十位 | 1 | 日[0] | | H | 日期个位 | 5 | 日[1] | ##### 内部计算层(7位) | 字母 | 公式 | 中文名 | 旧名对照 | 视觉层级 | |------|------|-------|---------|---------| | I | `reduce(E+F)` | 月能量 | 旧 F | 底部左1 | | J | `reduce(G+H)` | 日能量 | 旧 G | 底部左2 | | K | `reduce(A+B)` | 年前半 | 旧 H | 底部右1 | | L | `reduce(C+D)` | 年后半 | 旧 I | 底部右2 | | M | `reduce(I+J)` | 青年综合数 | 不变 | 中层左 | | N | `reduce(K+L)` | 晚年综合数 | 不变 | 中层右 | | O | `reduce(M+N)` | **主性格数** | 不变 | 顶层 | ##### 外部三组(9位) | 字母 | 公式 | 中文名 | 年龄区间 | 所属组 | |------|------|-------|---------|-------| | P | `reduce(I+M)` | 左侧左子(left-L) | 21-40 | 左侧 | | Q | `reduce(J+M)` | 左侧右子(left-R) | 21-40 | 左侧 | | R | `reduce(P+Q)` | **左侧主数(left)** | 21-40 | 左侧 | | S | `reduce(K+N)` | 右侧左子(right-L) | 61+ | 右侧 | | T | `reduce(L+N)` | 右侧右子(right-R) | 61+ | 右侧 | | U | `reduce(S+T)` | **右侧主数(right)** | 61+ | 右侧 | | V | `reduce(M+O)` | 顶部左子(top-L) | 41-60 | 顶部 | | W | `reduce(N+O)` | 顶部右子(top-R) | 41-60 | 顶部 | | X | `reduce(V+W)` | **顶部主数(top)** | 41-60 | 顶部 | #### 2. 视觉布局示意 ``` X ↙ ↘ V W ↙ ↘ M ───────────── N ↙ ↘ ↙ ↘ P Q S T ↙ ↘ ↙ ↘ I ─────────── J K ─────────── L R (21-40) U (61+) 底部输入: 日 G H | 月 E F | 年 A B C D ``` #### 3. 外部三组年龄映射 | 组 | 位置 | 年龄区间 | 关联内部位 | |----|------|---------|----------| | 左侧组 (R, P, Q) | 三角形左外侧 | **21-40岁** | 基于 I, J, M | | 顶部组 (X, V, W) | 三角形顶外侧 | **41-60岁** | 基于 M, N, O | | 右侧组 (U, S, T) | 三角形右外侧 | **61+岁** | 基于 K, L, N | #### 4. 计算精度说明 - `reduce(N)` 定义:各位数字相加归约至个位,但 11/22/33 保留为卓越数 - 所有 `+` 操作符均先执行普通加法,再调用 `reduce()` 归约 - 外部三组**必须使用独立的归约计算**,不得直接引用内部数字值 #### 5. 后端 API 返回结构(`POST /api/consultation/start` 响应示例) ```json { "recordId": 42, "isNew": false, "chartData": { "positions": { "I": 1, "J": 2, "K": 3, "L": 4, "M": 5, "N": 6, "O": 7, "P": 8, "Q": 9, "R": 1, "S": 2, "T": 3, "U": 4, "V": 5, "W": 6, "X": 7 }, "mainCharacter": 7, "isMasterNumber": false }, "messages": [] } ``` --- ### US-1.4 数字点击查看含义 **作为** 数字能量师 **我希望** 点击三角形中的每个数字可以查看该位置的含义和能量解读 **以便** 快速向客户解释命盘 **验收标准:** 1. 点击三角形中的任一数字卡片,底部弹出该位置的解释面板 2. 解释面板显示:位置名称(如"主性格")、数字、性格特质描述 3. 再次点击或点击空白区域关闭面板 4. 5区位置的描述文字可配置(后续通过后台修改) --- ## EPIC 2:分享与导出(P0) ### US-2.1 命盘分享图片 **作为** 数字能量师 **我希望** 将命盘生成为一张精美的图片,分享到微信或保存到相册 **以便** 发给客户或在朋友圈展示 **验收标准:** 1. 分享按钮位于命盘展示页下方 2. 点击"分享"按钮,生成包含命盘图+姓名+生日的分享卡片 3. 分享卡片支持:保存到相册 / 分享给微信好友 / 分享到朋友圈 4. 分享卡片设计精美,带有品牌水印(可选) 5. 免费用户每日限制 1 次分享,已付费用户不限 ### US-2.2 命盘PDF导出 **作为** 数字能量师 **我希望** 将命盘导出为 PDF 文件,可以直接打印或微信发送 **以便** 客户获得正式的纸质/电子版报告 **验收标准:** 1. PDF 按钮位于命盘展示页(仅已付费用户可见) 2. 点击后生成包含命盘三角形+姓名+生日+日期的 PDF 3. PDF 使用 A4 竖版排版,适合打印 4. 生成过程不超过 5 秒 5. 生成后自动进入微信文件预览,支持转发给微信好友 --- ## EPIC 3:AI 解读(P0) ### US-3.1 AI 解读展示 **作为** 数字能量师 **我希望** 在命盘生成后,看到由AI自动生成的完整文字解读 **以便** 直接发给客户或稍作润色后使用,节省自己查资料写解读的时间 **验收标准:** 1. 命盘展示页底部有"查看AI解读"按钮,点击跳转到AI解读页 2. 解读内容由LLM生成,通过后端API调用,首次加载等待时间 < 5秒 3. 加载过程中显示骨架屏或loading动画,避免用户以为卡死 4. 解读内容至少包含以下章节: - **主性格解读**:顶端数字的核心特质、性格描述、代表人物 - **左区(0-20岁)**:早年运势、成长环境 - **中区(20-40岁)**:中年事业、人际关系 - **右区(40-60岁)**:晚年成就、财运趋势 - **父源区**:父亲遗传、先天能量 - **母源区**:母亲遗传、后天影响 5. 每个章节独立卡片展示,可折叠展开 6. 解读内容基于命盘的实际数字,同一数字对不同命盘解读不同(位置差异) 7. 页面底部显示"本解读由AI生成,仅供参考"的免责声明 ### US-3.2 AI 解读的免费/付费控制 **作为** 平台运营者 **我希望** 控制AI解读功能的免费与付费边界 **以便** 激励用户订阅C端年费或能量师年费 **验收标准:** 1. 未付费用户点击"查看AI解读"时,仅显示**主性格概要**(1段文字) 2. 未付费用户每日可查看主性格概要 1 次 3. 五区完整解读仅在已付费后可用(C端¥131或能量师¥1,314+) 4. 解读页底部显示"¥131 开通完整解读"引导卡片(免费用户可见) 5. 已付费用户可无限次查看完整解读 6. 每次解读请求记录到数据库,用于成本控制和限流 ### US-3.4 AI 交互问答(免费/付费控制) **作为** 平台运营者 **我希望** 控制AI问答互动功能的免费次数,且限定在同一个命盘上 **以便** 防止用户通过切换生日绕过每日限制,同时保证体验清晰可预期 **验收标准:** 1. 未付费用户每日可进行 3 轮 AI 问答互动(每日配额全局统一,不按命盘拆分) 2. 此 3 轮互动**绑定到当前咨询的命盘上**,用户不能通过创建多个生日命盘来获得额外免费次数 3. 已付费用户不受次数限制 4. 用户在命盘页顶部看到剩余次数提示:"今日 {{usedChats}}/{{maxChats}} 轮" 5. 当用户在已用满3次的命盘上新建**另一个生日**的咨询时: - 前端弹出确认对话框 - 对话框提示:"此生日将开启全新的命盘咨询,今日剩余互动次数仍为 0 次,确定吗?" - 用户确认后创建新咨询,次数不重置 6. 超出每日次数后,AI 输入框和快捷问题按钮隐藏,显示引导卡片:"今日 AI 解读次数已用完,开通会员享无限次" 7. 每日次数在 `lastQuotaDate` 跨日时自动归零 ### US-3.3 解读内容缓存 **作为** 系统 **我希望** 同一命盘的重复解读请求不重复调用LLM API **以便** 控制成本,避免同一命盘每次查看都消耗API费用 **验收标准:** 1. 首次生成解读后,将解读内容与命盘ID绑定存储到数据库 2. 后续同一命盘再次查看解读时,直接从数据库读取,不调用LLM 3. 用户自行重新生成解读时,覆盖旧的缓存内容 4. 解读缓存永久保留,不自动过期 --- ## EPIC 4:付费与订阅(P0) ### US-4.1 能量师付费订阅 **作为** 数字能量师 **我希望** 看到清晰的定价方案并进行年费支付 **以便** 获得AI完整解读、分销推广等全部功能 **验收标准:** 1. 付费入口:未付费用户在首页/命盘页/解读页/分享时均会看到升级引导 2. 付费页展示: - 功能对比表(普通用户 vs 能量师) - 价格:种子价 ¥1,314 / 标准价 ¥1,986(通过 `POST /api/pricing/current` 获取) - 种子价名额提示:"仅剩 XX 个种子名额" - "立即开通"支付按钮 3. 通过微信支付完成订阅 4. 支付成功后,用户状态即时更新(`vipEndTime += 365天`) 5. 支付成功后,自动生成专属6位推广码(如无已有) 6. 支付成功后,自动跳转到分销面板(或引导页) 7. 支付失败显示友好提示 ### US-4.2 种子价自动判断 **作为** 平台 **我希望** 系统自动判断用户是否享受种子价 **以便** 运营策略自动化,无需人工干预 **验收标准:** 1. 用户在支付页看到的价格由后端接口 `POST /api/pricing/current` 返回,前端不做判断 2. 后端判断逻辑:统计 `orders` 表中 `product_type='practitioner'` 且 `status='paid'` 的记录数 3. 已付费记录数 < `sys_config.pricing.practitioner.seed_limit`(默认 300)时,返回种子价 4. 已付费记录数 ≥ 限额时,返回标准价 5. 订单创建时将种子价状态锁定(不因后续人数变化而改变已创建订单的价格) 6. 种子价名额满后,所有新用户看到标准价 7. 种子价上限可在管理后台动态修改(`pricing.practitioner.seed_limit`) ### US-4.3 订阅状态与续费 **作为** 付费能量师 **我希望** 在个人中心看到我的订阅状态、到期日,并能续费 **以便** 管理我的会员身份 **验收标准:** 1. 个人中心显示:订阅状态(能量师/C端会员/已过期/普通用户) 2. 显示到期日期,到期前 7 天显示续费提醒 3. 到期后自动降级为普通用户,分销面板入口隐藏 4. 续费操作按**标准价**执行(种子价仅限首次,不限能量师或C端) 5. 续费成功后有效期在原到期日基础上延长1年 --- ## EPIC 5:用户与历史(P1) ### US-5.1 微信一键登录 **作为** 能量师 **我希望** 使用微信一键登录 **以便** 不需要注册账号,降低使用门槛 **验收标准:** 1. 首次使用点击"生成命盘"时弹出微信授权 2. 授权后自动完成注册,无需填写额外信息 3. 后续使用自动登录 4. 用户信息存储在数据库中 ### US-5.2 历史咨询记录 **作为** 能量师 **我希望** 查看我过去的所有咨询记录 **以便** 回顾和继续之前的咨询 **验收标准:** 1. 个人中心页包含"我的咨询"列表 2. 列表按最后互动时间倒序排列 3. 每条记录显示:姓名、生日、咨询时间、最后一条消息摘要 4. 点击记录可跳转到对应的命盘展示页,并恢复完整的聊天历史 5. 支持单条删除 6. 同一用户+同一生日永远返回同一条咨询记录(`(userId, birthday)` 联合唯一) 7. 免费用户仅能看到近 7 天记录,已付费用户可见全部 --- ## EPIC 6:分销推广与佣金(P0-P1) ### US-6.1 推广码生成(付费后生成) **作为** 付费用户(C端 / 能量师) **我希望** 在首次付费后自动获得专属推广码 **以便** 分享给微信好友来发展下级并赚取佣金 **验收标准:** 1. 推广码在**注册时不生成**,仅在**首次支付成功后**生成(¥131年费 / ¥1,314+能量师费任一触发) 2. 推广码规则:6位大写字母+数字,字符集去掉易混淆的 `O/0/I/1`(即 `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`) 3. 推广码冲突时自动重新生成,保证全局唯一 4. 推广码永久有效,不随年费到期失效 5. 普通用户(未付费)没有推广码,也没有分销入口 6. 推广码展示在分销面板顶部,支持一键复制和生成推广海报 7. 推广码下游用于:分享带参注册(US-6.2)和佣金结算(US-6.3 / US-6.4) --- ### US-6.2 分享带参注册与上下级绑定 **作为** 平台 **我希望** 当用户通过推广链接/扫码打开小程序时,自动记录上下级关系 **以便** 为佣金分成打好基础 **验收标准:** **分享链路:** 1. 推广链接格式:`/pages/index/index?ref=ABC123`(6位推广码) 2. 分享卡片 / 小程序码同样携带 `ref` 参数 **前端缓存策略:** 3. 用户通过链接打开小程序时: - `onLaunch(options)` 读取 `options.query.ref`,转为大写后写入缓存 `pending_referrer` - 不覆盖已有 `pending_referrer`,以用户**最后一次点击的链接**为准 4. `pending_referrer` **永不过期**,直到注册成功后才清除 5. 用户注册(微信授权登录)时,从缓存读取 `pending_referrer` 传入注册接口 **后端注册逻辑(`UserService.loginOrRegister()`):** 6. 用户不存在(新用户)+ `referrerCode` 有效 → `invitedBy = 上级ID` 7. 用户不存在 + `referrerCode` 无效 → `invitedBy = null`(静默降级,不报错) 8. 用户不存在 + `referrerCode` 为空 → `invitedBy = null`(正常注册) 9. 用户不存在 + `referrerCode` 为**自己的新openid** → `invitedBy = null`(防止自邀请) 10. 用户**已存在** → 忽略所有 `referrerCode`,不覆盖已有 `invitedBy`(关系一次性锁定) 11. `referrerCode` 查询不到对应上级 → 静默跳过,不抛异常 **数据库更新:** 12. 绑定成功后,如果上级存在,`inviter.directCount += 1`(实时更新统计字段) **边界与异常:** 13. 用户未登录直接生成命盘(纯前端计算)→ 允许,不强制登录 14. 登录中途中断(拒绝授权)→ 不清除 `pending_referrer`,下次触发登录仍携带 15. 注册接口网络失败 → 保留 `pending_referrer`,重试时继续携带 16. 同一设备切换微信账号 → 新的 openid 按新用户处理,正常绑定关系 --- ### US-6.3 B端佣金结算(能量师→能量师,固定金额) **作为** 平台 **我希望** 当能量师A的下级B升级为能量师时,自动结算固定金额佣金 **以便** 能量师获得推广收益 **验收标准:** **触发条件:** 1. 订单 `productType = "practitioner"` 且支付成功 → 进入 B端佣金结算流程 2. 订单 `productType = "annual"` → 走 US-6.4 C端佣金(互斥分支) **佣金计算:** 3. 一级佣金(L1,直接上级):**固定金额**,从 `sys_config` 读取 `commission.practitioner.l1`(默认 ¥500 = 50000分) 4. 二级佣金(L2,上上级):**固定金额**,从 `sys_config` 读取 `commission.practitioner.l2`(默认 ¥100 = 10000分) 5. 佣金单位为**分**(避免浮点精度问题) **收款人资格:** 6. L1:`buyer.invitedBy` 对应的上级存在 → 创建佣金(不需要校验VIP是否有效) 7. L2:上级的 `invitedBy` 对应的上上级存在 → 创建佣金 8. 收款人不存在时 → 静默跳过,不报错 **状态与幂等:** 9. 佣金 `status` 直接写入 `"settled"`(即时到账,无 pending 冷静期) 10. 同一 `outTradeNo` 重复回调 → 幂等处理,不创建重复佣金 11. 佣金金额固定的另一层含义:**不受种子价/标准价差异影响**,¥500/¥100 固定不变 **统计更新:** 12. L1 创建时:`inviter.directCount += 1`,`inviter.convertedCount += 1` 13. L2 创建时:`inviter2.indirectCount += 1` **配置项(后台可配):** | 配置键 | 默认值 | 说明 | |--------|--------|------| | `commission.practitioner.l1` | `50000` | B端一级佣金(分) | | `commission.practitioner.l2` | `10000` | B端二级佣金(分) | --- ### US-6.4 C端佣金结算(年费推荐,比例分成) **作为** 平台 **我希望** 当C端用户B通过上级A的链接注册并付费 ¥131 时,自动按比例分成 **以便** 激励所有付费用户推广 **验收标准:** **触发条件:** 1. 订单 `productType = "annual"` 且支付成功 → 进入 C端佣金结算流程 **佣金计算:** 2. 直接佣金(L1):`totalFee × directRate / 10000`,从 `sys_config` 读取 `commission.annual.direct_rate`(默认 `4000` = 40%) 3. 上级佣金(L2):`totalFee × upstreamRate / 10000`,从 `sys_config` 读取 `commission.annual.upstream_rate`(默认 `500` = 5%) 4. 金额计算使用**整数截断**(非四舍五入),剩余零头归平台 **收款人资格(与 B端不同):** 5. L1:`buyer.invitedBy` 对应的上级**必须已付费**(`referralCode != null`),才创建佣金 6. L2:上级的 `invitedBy` 对应的上上级**必须已付费**(`referralCode != null`),才创建佣金 7. C端佣金**不要求**收款人是"能量师"(practitioner),只要曾经付费即可 **状态与幂等:** 8. 佣金 `status` 直接写入 `"settled"` 9. 幂等处理同 US-6.3 **与 B端的差异:** 10. C端佣金不更新 `directCount/convertedCount/indirectCount`(这些统计仅用于B端"升级能量师") 11. C端佣金比例灵活(后台可配 0%~100%),B端固定金额 **每笔 ¥131 分配示例:** | 分配对象 | 比例 | 金额 | |---------|------|------| | 直接上级 | 40% | ¥52.40 | | 上上级 | 5% | ¥6.55 | | 平台 | 55% | ¥72.05 | **配置项(后台可配):** | 配置键 | 默认值 | 说明 | |--------|--------|------| | `commission.annual.direct_rate` | `4000` | C端直接佣金比例(万分比,4000=40%) | | `commission.annual.upstream_rate` | `500` | C端上级佣金比例(万分比,500=5%) | --- ### US-6.5 分销面板 **作为** 能量师 **我希望** 在分销面板中查看我的推广收益和团队数据 **以便** 追踪推广效果,激励持续推广 **验收标准:** **入口权限:** 1. 个人中心菜单项"💰 我的推广"→ 跳转到分销面板 2. **仅付费用户可见**(`vipEndTime > now`),普通用户看不到入口 3. 付费到期后入口自动隐藏 **页面结构(自上而下):** **① 推广码卡片:** 4. 显示推广码 `我的推广码: ABC123` + 复制按钮 5. "生成推广海报"按钮 → 调用后端生成含小程序码的海报 6. 无推广码(极端情况)→ 显示"推广码生成中…" **② 收益统计卡片(三列等宽):** 7. 总收益:累计所有佣金总额(`status = settled`) 8. 可提现:当前可提现金额(与总收益相同,Phase 1 不做提现扣除) 9. 今日新增:当日 00:00 至今产生的佣金总额 10. 金额以元为单位,保留两位小数(后端存储分,前端 `/100`) **③ 团队统计(三列):** 11. 直接下级:`directCount` 12. 间接下级:`indirectCount` 13. 升级下级:`convertedCount` 14. 零数据时显示引导文案:"暂无推广数据,分享推广码给好友开始赚取佣金" **④ 佣金明细列表(最近50条):** 15. 显示:时间(MM-DD HH:mm)、来源(匿名"用户****")、级别(L1/L2标签)、金额(+¥XXX.XX)、状态 16. 按时间倒序排列 17. 空数据时显示"暂无佣金记录"引导 **⑤ 推广工具区:** 18. "📱 生成推广海报" → 合成含小程序码的分享海报 19. "🔗 复制推广链接" → 复制带 ref 参数的小程序路径到剪贴板 20. "📊 提现申请" → **置灰不可点击**,显示"即将开放"(Phase 1 不做提现) **页面状态:** 21. **Loading 状态**:各区块显示骨架屏脉冲动画 22. **错误状态**:顶部显示错误提示条 + "点击重试"按钮 23. **空数据状态**:佣金列表/团队统计显示引导文案 24. **下拉刷新**:重新请求所有数据 **后端 API 需求:** | 接口 | 说明 | |------|------| | `POST /api/profile/info` | 含 `referralCode`(已有) | | `POST /api/commission/list` | 佣金明细列表(已有) | | `POST /api/commission/stats` | **新增**:总收益+可提现+今日新增+团队统计 | --- ### US-6.6 C端年费订阅入口 **作为** 免费用户(C端) **我希望** 在AI解读页底部看到 ¥131 开通完整解读的引导 **以便** 一键订阅获取完整服务 **验收标准:** **入口位置(用户已确认选项1——命盘解读页底部):** 1. `chart/index.vue` 底部(聊天区下方)固定显示订阅卡片(免费用户) 2. 已付费用户(C端或能量师) → 不显示订阅卡片,改为显示"✅ 已开通"徽章+生成推广海报入口 **交互流程(未登录):** 3. 显示完整订阅卡片:🔮 AI解读会员 ¥131/年,列出权益(无限AI解读、个性化报告、推广赚佣金) 4. 点击"登录后开通 ¥131" → 先跳登录页,登录后自动跳转支付页 **交互流程(已登录未付费):** 5. 显示同上"立即开通 ¥131"按钮 6. 点击 → 跳转支付页 `product=annual`,金额 ¥131 **交互流程(已付费):** 7. 订阅卡片替换为灰色小徽章"✅ 已开通AI解读 | 有效期至 2027-05-28" 8. 已付费用户显示"📱 生成推广海报"入口 9. 能量师(practitioner)显示"✅ 已开通能量师" **支付页适配:** 10. `payment/index.vue` 支持 URL 参数 `product`: - `product=annual` → 显示 C端年费 ¥131 和对应权益列表 - `product=practitioner` → 显示能量师价格(通过 API 获取种子/标准价) 11. `createOrder` 接口接收 `productType` 参数 **后端适应:** 12. Order 新增 `productType` 字段(`"annual"` | `"practitioner"`) 13. User 新增 `vipType` 字段(`"annual"` | `"practitioner"` | `null`) 14. `paySuccess()` 按 `productType` 分支佣金逻辑: - `practitioner` → US-6.3 固定金额 - `annual` → US-6.4 比例分成 --- ## EPIC 7:批注与客户管理(P2) ### US-7.1 命盘批注 **作为** 能量师 **我希望** 在命盘图上添加文字批注或标记 **以便** 为每个客户记录个性化的解读要点 **验收标准:** 1. 在命盘展示页添加"编辑批注"按钮(仅已付费用户可见) 2. 点击后进入批注模式,可添加自由文本 3. 批注内容保存后,下次查看该命盘时依然可见 4. 批注支持简单的富文本(分段、加粗等) 5. 导出的 PDF 中可包含批注内容 ### US-7.2 客户标签分组 **作为** 能量师 **我希望** 为命盘记录添加简单的标签或分组 **以便** 对客户进行分类管理 **验收标准:** 1. 创建命盘时可选择已有标签或新建标签 2. 标签以颜色 + 文字形式展示 3. 历史列表支持按标签筛选 4. 标签管理:新增、编辑、删除 --- ## EPIC 8:管理后台(P0-P1) ### US-8.1 系统配置管理 **作为** 运营管理员 **我希望** 在管理后台可视化编辑定价和佣金参数 **以便** 灵活调整运营策略,无需开发介入 --- #### 8.1.1 验收标准 **DDL & Entity:** 1. 新建 `sys_config` 表,结构如下: ```sql CREATE TABLE `sys_config` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `config_key` VARCHAR(64) NOT NULL COMMENT '配置键(点分命名法)', `config_value` VARCHAR(255) DEFAULT NULL COMMENT '配置值(始终使用字符串存储)', `description` VARCHAR(255) DEFAULT NULL COMMENT '中文说明', `value_type` VARCHAR(20) NOT NULL DEFAULT 'string' COMMENT 'int | price | percent | string', `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_config_key` (`config_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` 2. 预置 8 条种子数据(见下)。 **后端:SysConfigService + 内存缓存:** 3. `@PostConstruct loadAllToCache()`:启动时将全量配置读入 `ConcurrentHashMap` 4. 对外提供 `getString(key, default)`、`getInt(key, default)` 两个读方法,O(1) 取值 5. `key` 不存在时 → 返回代码中传入的 `defaultValue`,**永不抛异常** 6. `updateConfig(key, value)`:更新 DB + 同步写入缓存,**无需重启** 7. `batchUpdate(Map)`:循环调用 `updateConfig`,整个操作在 `@Transactional` 中 **管理后台 API:** 8. `POST /api/admin/config/list` → 返回 `List`(含全部字段) 9. `POST /api/admin/config/update` → 接收 `{ "configKey": "value", ... }` → 批量更新 **小程序端 API:`POST /api/pricing/current`:** ```json // Request: 无参数 // Response: { "code": 0, "data": { "seedPrice": 131400, // pricing.practitioner.seed(分) "standardPrice": 198600, // pricing.practitioner.standard(分) "annualFee": 13100, // pricing.annual(分) "isSeedPrice": true, // 当前用户是否能享受种子价 "remainingSeats": 298, // 种子名额余量 "seedLimit": 300 // 名额上限 } } ``` 10. `isSeedPrice` 的计算逻辑:统计 `orders` 表中 `product_type='practitioner'` AND `status='paid'` 的记录数,小于 `seedLimit` 则为 `true` **管理后台页面(`src/views/settings/index.vue`):** 11. 左侧菜单新增"系统配置"入口,路由 `/settings`,图标 ⚙️ 12. 页面分为三个卡片区域,每个卡片顶部有"恢复默认值"按钮(仅重置单个卡片内的字段) **定价配置卡片:** 13. 四个输入项:能量师种子价、标准价、种子价名额上限、C端年费 14. 用户输入的是**元(整数)**,提交时自动 ×100 转为分 15. 种子价名额上限下方实时显示"当前已使用:X / Y"(调用 `countByProductTypeAndStatus`) 16. 修改数量时,"已使用"比例随之动态变化 **B端佣金配置卡片:** 17. 两个输入项:L1 佣金(¥500)、L2 佣金(¥100) 18. 输入单位为元(整数),提交时 ×100 转为分 19. 下方实时显示比例计算: - "L1 ¥500 = 种子价 38.1% / 标准价 25.2%" - "L2 ¥100 = 种子价 7.6% / 标准价 5.0%" 20. 比例随定价或佣金值的修改**实时联动更新** **C端佣金配置卡片:** 21. 两个输入项:直接佣金比例(40%)、上级佣金比例(5%) 22. 输入单位为"%"(整数,如输入"40"表示40%),提交时转为万分比 ×100 23. 下方实时显示金额分配预览: - "每笔 ¥131:直接 ¥52.40(40.0%)/ 上级 ¥6.55(5.0%)/ 平台 ¥72.05(55.0%)" 24. 费率修改时金额分配**实时联动更新** **保存逻辑:** 25. 点击"保存全部配置"按钮 → 触发 `POST /api/admin/config/update` 26. 所有字段批量提交,不逐个保存 27. 保存前做前端校验:金额 > 0、比例 0-100%、名额上限 > 0 28. 保存成功 → ElMessage.success("配置已保存,已实时生效") 29. 保存失败 → ElMessage.error("保存失败:" + 错误详情) 30. 前端无 `el-form` 的 reset 行为 —— 用户手动刷新页面可恢复到上次保存的值 **错误与边界:** 31. 网络失败时显示"保存失败,请重试",页面不跳转,已修改的值保留在输入框中 32. 同时两个管理员保存 → 后保存的覆盖先保存的,不做冲突检测(Phase 1 简化) 33. 输入非法值时(如空字符串、负数),`el-input-number` 的 min 属性直接阻止输入 34. 所有金额字段不允许出现小数(元为整数输入,单位转换在后端) --- #### 8.1.2 后端接口规格 **`POST /api/admin/config/list`** ```json // Response: { "code": 0, "data": [ { "id": 1, "configKey": "pricing.practitioner.seed", "configValue": "131400", "description": "能量师种子价(分)", "valueType": "price", "updatedAt": "2026-05-28T10:00:00" } // ... 共 8 条 ] } ``` **`POST /api/admin/config/update`** ```json // Request: { "pricing.practitioner.seed": "131400", "commission.practitioner.l1": "60000" } // Response: { "code": 0 } ``` **`POST /api/admin/config/reset`**(可选,单条重置) ```json // Request: { "configKey": "commission.practitioner.l1" } // Response: { "code": 0, "data": { "configKey": "commission.practitioner.l1", "configValue": "50000" } } ``` --- #### 8.1.3 后端类设计 | 类名 | 位置 | 说明 | |------|------|------| | `SysConfig` | `entity/` | JPA 实体 | | `SysConfigRepository` | `repository/` | JPA 接口 | | `SysConfigService` | `service/` | 配置 CRUD + 内存缓存 | | `AdminConfigController` | `controller/` | `/api/admin/config/*` 端点 | | `PricingController` | `controller/` | `/api/pricing/current` 小程序端端点 | | `OrderRepository` | `repository/` | 新增 `countByProductTypeAndStatus()` | **`SysConfigService` 核心代码:** ```java @Service public class SysConfigService { private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); private final SysConfigRepository repository; @PostConstruct public void loadAllToCache() { cache.clear(); repository.findAllByOrderByConfigKeyAsc() .forEach(cfg -> cache.put(cfg.getConfigKey(), cfg.getConfigValue())); } public String getString(String key, String defaultValue) { return cache.getOrDefault(key, defaultValue); } public int getInt(String key, int defaultValue) { String val = cache.get(key); if (val == null) return defaultValue; try { return Integer.parseInt(val); } catch (NumberFormatException e) { return defaultValue; } } @Transactional public void updateConfig(String key, String value) { SysConfig cfg = repository.findByConfigKey(key) .orElseGet(() -> new SysConfig(key, value, "auto", "string")); cfg.setConfigValue(value); cfg.setUpdatedAt(LocalDateTime.now()); repository.save(cfg); cache.put(key, value); } public void batchUpdate(Map configs) { configs.forEach(this::updateConfig); } } ``` --- #### 8.1.4 管理后台前端规格 | 文件 | 操作 | 说明 | |------|------|------| | `src/views/settings/index.vue` | **新增** | 系统配置页面(~200行) | | `src/router/index.ts` | 修改 | 新增 `/settings` 路由 | | `src/layout/index.vue` | 修改 | 新增菜单项 | **侧边栏菜单新增:** ```vue 系统配置 ``` **页面组件结构:** ``` settings/index.vue ├── el-card (header-toolbar: "系统配置" + [保存全部配置]) ├── el-card (定价配置) │ ├── el-form-item: 能量师种子价 → el-input-number(min=0, max=999999, step=100) │ ├── el-form-item: 能量师标准价 → el-input-number(...) │ ├── el-form-item: 种子价名额上限 → el-input-number(min=1, max=99999, step=50) │ └── el-form-item: C端年费 → el-input-number(...) ├── el-card (B端佣金配置) │ ├── el-form-item: L1 佣金 → el-input-number │ ├── el-form-item: L2 佣金 → el-input-number │ └── 实时比例显示 └── el-card (C端佣金配置) ├── el-form-item: 直接佣金比例 → el-input-number(min=0, max=10000, step=500) ├── el-form-item: 上级佣金比例 → el-input-number(min=0, max=5000, step=100) └── 实时分配预览 ``` **表单校验规则:** | 字段 | 校验规则 | |------|---------| | 所有金额(元) | > 0,整数,≤ 999999 | | 比例(%) | 0-100,整数 | | 名额上限 | ≥ 1,≤ 99999 | --- #### 8.1.5 种子数据 SQL ```sql INSERT INTO `sys_config` (`config_key`, `config_value`, `description`, `value_type`) VALUES ('pricing.practitioner.seed', '131400', '能量师种子价(分)', 'price'), ('pricing.practitioner.standard', '198600', '能量师标准价(分)', 'price'), ('pricing.practitioner.seed_limit', '300', '种子价名额上限', 'int'), ('pricing.annual', '13100', 'C端年费(分)', 'price'), ('commission.practitioner.l1', '50000', 'B端一级佣金(分)', 'price'), ('commission.practitioner.l2', '10000', 'B端二级佣金(分)', 'price'), ('commission.annual.direct_rate', '4000', 'C端直接佣金比例(万分比)', 'percent'), ('commission.annual.upstream_rate', '500', 'C端上级佣金比例(万分比)', 'percent'); ``` --- #### 8.1.6 测试用例 | # | 场景 | 步骤 | 预期 | |---|------|------|------| | 1 | 初始加载 | 打开系统配置页 | 8个字段显示正确的默认值 | | 2 | 修改定价 | 种子价改为 ¥2,000 → 保存 | 下次 `POST /api/pricing/current` 返回 seedPrice=200000 | | 3 | 修改佣金 | L1 改为 ¥600 → 保存 | 新订单的佣金 amount=60000 | | 4 | 比例联动 | 直接佣金改为 50% | 下方显示"每笔¥131:直接¥65.50 / 上级¥6.55 / 平台¥58.95" | | 5 | 比例联动反向 | 种子价改为 ¥2,000 | B端卡片"L1占种子价%"从38.1%变为25.0% | | 6 | 输入校验 | 在金额框输入 -100 | el-input-number 阻止输入,值保持为 0 | | 7 | 保存失败 | 网络断开后保存 | 显示"保存失败",输入值保留不丢失 | | 8 | 保存后立即生效 | 改种子价为 ¥2,000 → 保存 → 新用户打开支付页 | 显示 ¥2,000 | | 9 | 缓存同步 | API 直接改 DB → 调用 getInt() | 返回值仍为旧值(下次 loadAllToCache 或调用 updateConfig 后更新) | | 10 | 种子名额展示 | 3笔已支付 practitioner 订单 | 显示"已使用:3 / 300" | --- ### US-8.2 订单管理 **作为** 运营管理员 **我希望** 查看所有订单记录,按产品类型和状态筛选 **以便** 核对收入和对账 --- #### 8.2.1 验收标准 **后端 API(补全 `AdminController.listOrders()`):** 1. `POST /api/admin/orders` → 接收筛选参数,返回分页订单列表 ```json // Request: { "productType": "", // 筛选:"" 全部 | "practitioner" | "annual" "status": "", // 筛选:"" 全部 | "paid" | "pending" "page": 1, "pageSize": 20 } // Response: { "code": 0, "data": { "list": [ { "id": 1, "userId": 42, "outTradeNo": "202605280001", "totalFee": 131400, "productType": "practitioner", "isSeedPrice": true, "status": "paid", "payType": "wxpay", "paidAt": "2026-05-28T10:00:00", "createdAt": "2026-05-28T09:59:00" } ], "total": 128, "page": 1, "pageSize": 20 } } ``` 2. 后端在 `OrderRepository` 中新增分页+动态筛选查询方法(使用 `Specification` 或 `@Query` 动态拼接) **前端页面:** 3. 表格列:ID、订单号、用户ID、产品类型("能量师" / "年费")、金额(¥格式)、种子价标记、状态(已支付/待支付)、支付时间 4. 产品类型使用 el-tag 展示:能量师→蓝色,年费→绿色 5. 种子价订单显示 🔥 种子 el-tag(橙色) 6. 状态使用 el-tag:已支付→success,待支付→warning 7. 表格上方的工具栏包含两个筛选器: - 产品类型:el-select(全部 / 能量师 / C端年费) - 状态:el-select(全部 / 已支付 / 待支付) 8. 筛选器 change 时重新请求接口 9. 底部分页组件:显示总条数,支持切换页码 **当前代码待补全:** - `AdminController.listOrders()` 目前返回 `Result.success(null)` → 需替换为真实数据 - `Order` 实体和 `OrderRepository` 已有,但缺 `productType` 和 `isSeedPrice` 字段 → 需新增 - 订单表 `orders` 需增加 `product_type` 和 `is_seed_price` 列 - 分页需引入 `Pageable` + `Specification`(或简单用 `PageRequest`) --- #### 8.2.2 后端类变更 | 类名 | 变更 | 说明 | |------|------|------| | `Order.java` | 新增字段 | `productType` (String)、`isSeedPrice` (Boolean) | | `OrderRepository` | 新增方法 | `countByProductTypeAndStatus(productType, status)` | | `OrderRepository` | 新增方法 | `findAll(Specification, Pageable)` 或 `findByProductTypeAndStatus` | | `AdminController` | 重写 `listOrders()` | 支持筛选+分页 | --- ### US-8.3 佣金查看 **作为** 运营管理员 **我希望** 查看所有佣金记录和统计 **以便** 了解分销推广效果 --- #### 8.3.1 验收标准 **后端 API(增强现有):** 1. `POST /api/admin/commissions` → 支持分页和按级别筛选 ```json // Request: { "level": 0, // 0: 全部 | 1: L1 | 2: L2 "page": 1, "pageSize": 20 } // Response: { "code": 0, "data": { "list": [ { "id": 1, "orderId": 101, "fromUserId": 42, "toUserId": 10, "level": 1, "amount": 50000, "status": "settled", "remark": "直接推荐升级能量师", "createdAt": "2026-05-28T10:00:00" } ], "total": 56, "page": 1, "pageSize": 20, "summary": { "settledTotal": 350000, // 已结算总佣金(分) "pendingTotal": 0, // 待结算总佣金(分,Phase 1 始终为 0) "grandTotal": 350000, // 总佣金(分) "countByLevel": { // 各级别笔数 "1": 12, "2": 8 } } } } ``` 2. 当前 `AdminController.listAllCommissions()` 返回 `commissionRepository.findAll()`(无分页)→ 需重写 **前端页面(增强现有 `commissions/index.vue`):** 3. 顶部统计卡片(已有框架)→ 增强: - 已结算佣金:¥3,500.00(绿色) - 待结算佣金:¥0.00(黄色,Phase 1 恒为 0,保留字段显示) - 总佣金:¥3,500.00 - **新增第四个卡片**:总笔数(L1: 12 笔 / L2: 8 笔) 4. 表格列(增强现有):ID、订单ID、来源用户ID、获得用户ID、级别(L1/L2 el-tag)、金额(¥格式)、备注(remark 字段)、状态、创建时间 5. 新增级别筛选器:el-select(全部 / L1 / L2),筛选时重新请求接口 6. 底部分页:当前 `:total="commissions.length"` 是前端假分页,改为后端真分页 7. 备注列显示 remark 内容(如"直接推荐升级能量师"、"C端年费直接推荐佣金 (40%)") --- #### 8.3.2 前后端差距分析 | 当前状态 | 目标状态 | 工作量 | |---------|---------|--------| | `AdminController.listAllCommissions()` 无分页,无筛选 | 分页 + level 筛选 + summary 统计 | M | | `CommissionRepository` 无分页方法 | 新增 `findAll(Specification, Pageable)` | S | | 前端假分页(全量拉取) | 后端真分页 | M | | 前端无 level 筛选 | 新增 el-select 筛选器 | S | | 前端无备注列 | 新增 remark 列 | S | | 前端无笔数统计卡片 | 新增第四个统计卡片 | S | --- ### US-8.4 提现管理(推迟至 Phase 2) **已决策:推迟,当前不做。** --- ## 开发优先级总表 | 用户故事 | 优先级 | 预估工期 | 依赖 | |----------|--------|---------|------| | US-1.1 生日输入与咨询发起 | P0 | 3天 | US-5.1(需登录)| | US-1.2 三角形可视化 | P0 | 3天 | US-1.1 | | US-1.4 数字点击查看含义 | P2 | 1天 | US-1.2 | | US-2.1 分享图片 | P0 | 2天 | US-1.2 | | US-2.2 PDF导出 | P0 | 2天 | US-1.2 | | US-3.1 AI解读展示 | P0 | 3天 | US-1.1 + LLM API接入 | | US-3.2 AI解读免费/付费控制 | P0 | 1天 | US-3.1 | | US-3.3 解读内容缓存 | P0 | 1天 | US-3.1 | | US-3.4 AI交互问答(免费/付费控制) | P0 | 1天 | US-1.1 | | US-4.1 能量师付费订阅 | P0 | 2天 | US-5.1 | | US-4.2 种子价自动判断 | P0 | 1天 | US-8.1 | | US-4.3 订阅状态与续费 | P0 | 1天 | US-4.1 | | US-5.1 微信登录 | P1 | 2天 | 无 | | US-5.2 历史咨询记录 | P1 | 2天 | US-5.1 | | US-6.1 推广码生成 | P0 | 1天 | US-4.1 | | US-6.2 分享带参注册与上下级绑定 | P0 | 2天 | US-5.1 | | US-6.3 B端佣金结算 | P0 | 2天 | US-4.1 + US-6.2 + US-8.1 | | US-6.4 C端佣金结算 | P0 | 1天 | US-6.6 + US-6.3 + US-8.1 | | US-6.5 分销面板 | P1 | 2天 | US-6.1 + US-6.3 | | US-6.6 C端年费订阅入口 | P0 | 1.5天 | US-3.2 + US-4.1 | | US-7.1 批注功能 | P2 | 2天 | P0完成 | | US-7.2 客户分组 | P2 | 2天 | P0完成 | | US-8.1 系统配置管理 | **P0** | **2天** | **无(独立基础设施)** | | US-8.2 订单管理 | P1 | 1.5天 | US-4.1 + Order 新增字段 | | US-8.3 佣金查看 | P1 | 1天 | US-6.3 | --- ## 咨询+命盘依赖图 ``` US-5.1 微信登录 │ ▼ US-1.1 生日输入与咨询发起(后端查重(userId+birthday)→确认弹窗→后台计算→返回展示) │ ├──► US-1.2 三角形可视化(24位置新命名 I-X) ├──► US-3.4 AI交互问答(每日3轮,绑定当前命盘) │ ▼ US-5.2 历史咨询记录(按 userId+birthday 唯一归组,可恢复聊天历史) ``` ## 分销相关依赖图 ``` US-8.1 系统配置管理(独立基础设施——最先完成) │ ├──► US-4.2 种子价判断 ├──► US-6.3 B端佣金(读取 L1/L2 金额) └──► US-6.4 C端佣金(读取直接/上级比例) US-5.1 微信登录 │ ▼ US-6.2 分享带参注册(需要 openid) │ ▼ 绑定上下级关系 │ US-4.1 能量师付费 + US-6.6 C端年费订阅 │ │ ▼ ▼ US-6.1 推广码生成 US-6.4 C端佣金结算 │ │ ▼ │ US-6.3 B端佣金结算 ◄──────────────┘(共享 paySuccess() 分支) │ ▼ US-6.5 分销面板(前端展示所有数据) ``` ## 排期建议 | 周次 | 交付内容 | |------|---------| | **Week 1** | US-8.1 系统配置 → US-4.2 种子价 → US-4.1 能量师付费 | | **Week 2** | US-6.2 带参注册 → US-6.1 推广码 → US-6.3 B端佣金 | | **Week 3** | US-6.6 C端订阅入口 → US-6.4 C端佣金 → US-6.5 分销面板 | | **Week 4** | 联调测试 + 管理后台补全(US-8.2 / US-8.3)+ BUG修复 | > **Phase 1 分销核心 P0 合计:约 10天(2周)** > 加上命盘、AI解读、支付等已有模块联调,总计约 4周。 --- ## 附录A:【新计算规则】代码文件变更清单 > 以下清单基于新命名体系(A-X)和咨询会话管理需求,列出所有需修改的代码文件及变更内容。 ### A.1 后端 Java | 文件 | 变更类型 | 变更内容 | |------|---------|---------| | `CalculatorService.java` | 重写 | 内部7位从 `F,G,H,I,M,N,O` → `I,J,K,L,M,N,O`;新增外部9位 `P,Q,R,S,T,U,V,W,X` 的独立计算;新增 `calculateFullTriangle()` 返回全部24个位置 | | `ChartService.java` | 改造 | 新增 `startConsultation(userId, birthday, name, questions)` 方法,含:①查 userId+birthday 是否已有记录;②已有则直接返回+聊天历史;③无则服务器端调 `CalculatorService` 计算+创建记录+可选首次AI解读 | | `ChartController.java` | 改造 | 保留 `/api/chart/create`(兼容旧版);新增 `POST /api/chart/start` → 调 `startConsultation()` | | `ChartRecord.java` | 微调 | 新增 `lastInteractionAt` 字段(`LocalDateTime`,用于列表排序);加 `@Table(uniqueConstraints=...)` 定义 `(userId, birthday)` 联合唯一 | | `ChartRecordRepository.java` | 新增 | `Optional findByUserIdAndBirthday(Long userId, String birthday)` | | `UserService.java` | 微调 | `checkDailyQuota()` 和 `consumeQuota()` 已有逻辑不变(3次/天全局计数),但新增注释明确"非VIP每日3次绑定当前命盘,切换生日不重置" | | `ProfileController.java` | 微调 | 咨询列表接口按 `lastInteractionAt` 倒序 | ### A.2 前端 Vue / JS | 文件 | 变更类型 | 变更内容 | |------|---------|---------| | `client/utils/calculator.js` | 废弃/降级 | `calculateTriangle()` 保留为离线兜底,不再作为主路径调用;内部命名改为 `I,J,K,L,M,N,O`;新增 `calculateFullTriangle()` 含外部 P-X;主入口加注释 `@deprecated 请使用后端计算` | | `client/stores/chart.js` | 改造 | `computeChart()` 不再调 `calculateTriangle()`,改为调 `consultationApi.start()`;返回数据中的 `positions` 直接写入 `currentChart`;不再调 `analyzeTriangle()`(由后端返回) | | `client/components/TriangleChart.vue` | 重写 matrix() | `innerBottom` 从 `[d.F,d.G,d.H,d.I]` → `[d.I,d.J,d.K,d.L]`;`outerLeft` 从 `{main:d.M, sub:[d.F,d.G]}` → `{main:d.R, sub:[d.P,d.Q]}`;`outerRight` 从 `{main:d.N, sub:[d.H,d.I]}` → `{main:d.U, sub:[d.S,d.T]}`;`outerTop` 从 `{main:d.O, sub:[d.M,d.N]}` → `{main:d.X, sub:[d.V,d.W]}` | | `client/pages/index/index.vue` | 改造 | `onAnalyze()` 改为:①调 `POST /api/chart/start`(传 birthday + name + questions);②若返回 `isNew=true` 且 `requireConfirmation=true` 则弹确认框;③确认后再次请求;④成功后跳转 `chart/index`;⑤"想了解的问题"加 `maxlength=100` + 实时字数显示 | | `client/utils/api.js` | 新增 | `consultationApi`:`start(data)` → `POST /api/chart/start`;`detail(id)` → `POST /api/chart/detail` | | `client/pages/chart/index.vue` | 微调 | 适配新的 API 返回结构(`chartData.positions` 含全部 I-X);`onMounted` 加载历史消息逻辑不变 | | `client/pages/records/index.vue` | 微调 | 列表按 `lastInteractionAt` 排序;详情页恢复聊天历史逻辑不变 | ### A.3 数据库 | 变更 | 说明 | |------|------| | `chart_records` 表加联合唯一索引 | `UNIQUE INDEX uk_user_birthday (user_id, birthday)`,确保同用户+同生日只有一条咨询记录 | | `chart_records` 表加字段 | `last_interaction_at DATETIME`,默认等于 `created_at`,每次 AI 聊天时更新,用于列表排序 | | `chart_records` 表现有字段 | `chart_data` 列存储的 JSON 结构从 7个旧位置 → 16个新位置(内部 I-O + 外部 P-X) | ### A.4 变更执行顺序 ``` Step 1: US-1.3(本对照表)→ 开发团队通读,确保理解命名对应关系 Step 2: 数据库迁移 → 加索引+字段 Step 3: CalculatorService.java → 重写为新命名+外部计算 Step 4: ChartService.java + ChartController.java → 新增 startConsultation() Step 5: stores/chart.js + api.js → 前端调后端新接口 Step 6: TriangleChart.vue → matrix() 改用新命名+独立外部值 Step 7: pages/index/index.vue → 确认弹窗 + 100字限制 Step 8: 联调 → 前后端打通 end-to-end ```