phase1-user-stories.md 49 KB

Phase 1 用户故事与验收标准

基于 phase1-product-plan.md1.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 发起咨询

验收标准(咨询会话管理):

  1. 后端按照 userId + birthday 唯一确定一条咨询记录(同用户+同生日始终返回同一条记录)
  2. 同一用户对同一生日重复点击"开始咨询",后端直接返回已有的咨询记录 + 完整聊天历史,不创建新记录
  3. 输入的生日是全新生日时,前端弹出确认对话框:"此生日将开启全新的命盘咨询,确认吗?"
  4. 用户确认后,后端计算完整的数字命盘(24个位置),创建新咨询记录,并返回结果
  5. 用户取消确认,停留在首页,不跳转、不创建
  6. 点击"开始咨询"到命盘渲染完成,总耗时不超过 3 秒
  7. 命盘计算全部由后端 CalculatorService 完成,前端只负责展示(calculator.js 保留为降级兜底,不作为主路径)
  8. 命盘页面 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 响应示例)

{
  "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 参数

前端缓存策略:

  1. 用户通过链接打开小程序时:
    • onLaunch(options) 读取 options.query.ref,转为大写后写入缓存 pending_referrer
    • 不覆盖已有 pending_referrer,以用户最后一次点击的链接为准
  2. pending_referrer 永不过期,直到注册成功后才清除
  3. 用户注册(微信授权登录)时,从缓存读取 pending_referrer 传入注册接口

后端注册逻辑(UserService.loginOrRegister()):

  1. 用户不存在(新用户)+ referrerCode 有效 → invitedBy = 上级ID
  2. 用户不存在 + referrerCode 无效 → invitedBy = null(静默降级,不报错)
  3. 用户不存在 + referrerCode 为空 → invitedBy = null(正常注册)
  4. 用户不存在 + referrerCode自己的新openidinvitedBy = null(防止自邀请)
  5. 用户已存在 → 忽略所有 referrerCode,不覆盖已有 invitedBy(关系一次性锁定)
  6. referrerCode 查询不到对应上级 → 静默跳过,不抛异常

数据库更新:

  1. 绑定成功后,如果上级存在,inviter.directCount += 1(实时更新统计字段)

边界与异常:

  1. 用户未登录直接生成命盘(纯前端计算)→ 允许,不强制登录
  2. 登录中途中断(拒绝授权)→ 不清除 pending_referrer,下次触发登录仍携带
  3. 注册接口网络失败 → 保留 pending_referrer,重试时继续携带
  4. 同一设备切换微信账号 → 新的 openid 按新用户处理,正常绑定关系

US-6.3 B端佣金结算(能量师→能量师,固定金额)

作为 平台
我希望 当能量师A的下级B升级为能量师时,自动结算固定金额佣金
以便 能量师获得推广收益

验收标准:

触发条件:

  1. 订单 productType = "practitioner" 且支付成功 → 进入 B端佣金结算流程
  2. 订单 productType = "annual" → 走 US-6.4 C端佣金(互斥分支)

佣金计算:

  1. 一级佣金(L1,直接上级):固定金额,从 sys_config 读取 commission.practitioner.l1(默认 ¥500 = 50000分)
  2. 二级佣金(L2,上上级):固定金额,从 sys_config 读取 commission.practitioner.l2(默认 ¥100 = 10000分)
  3. 佣金单位为(避免浮点精度问题)

收款人资格:

  1. L1:buyer.invitedBy 对应的上级存在 → 创建佣金(不需要校验VIP是否有效)
  2. L2:上级的 invitedBy 对应的上上级存在 → 创建佣金
  3. 收款人不存在时 → 静默跳过,不报错

状态与幂等:

  1. 佣金 status 直接写入 "settled"(即时到账,无 pending 冷静期)
  2. 同一 outTradeNo 重复回调 → 幂等处理,不创建重复佣金
  3. 佣金金额固定的另一层含义:不受种子价/标准价差异影响,¥500/¥100 固定不变

统计更新:

  1. L1 创建时:inviter.directCount += 1inviter.convertedCount += 1
  2. 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端佣金结算流程

佣金计算:

  1. 直接佣金(L1):totalFee × directRate / 10000,从 sys_config 读取 commission.annual.direct_rate(默认 4000 = 40%)
  2. 上级佣金(L2):totalFee × upstreamRate / 10000,从 sys_config 读取 commission.annual.upstream_rate(默认 500 = 5%)
  3. 金额计算使用整数截断(非四舍五入),剩余零头归平台

收款人资格(与 B端不同):

  1. L1:buyer.invitedBy 对应的上级必须已付费referralCode != null),才创建佣金
  2. L2:上级的 invitedBy 对应的上上级必须已付费referralCode != null),才创建佣金
  3. C端佣金不要求收款人是"能量师"(practitioner),只要曾经付费即可

状态与幂等:

  1. 佣金 status 直接写入 "settled"
  2. 幂等处理同 US-6.3

与 B端的差异:

  1. C端佣金不更新 directCount/convertedCount/indirectCount(这些统计仅用于B端"升级能量师")
  2. 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. 付费到期后入口自动隐藏

页面结构(自上而下):

① 推广码卡片:

  1. 显示推广码 我的推广码: ABC123 + 复制按钮
  2. "生成推广海报"按钮 → 调用后端生成含小程序码的海报
  3. 无推广码(极端情况)→ 显示"推广码生成中…"

② 收益统计卡片(三列等宽):

  1. 总收益:累计所有佣金总额(status = settled
  2. 可提现:当前可提现金额(与总收益相同,Phase 1 不做提现扣除)
  3. 今日新增:当日 00:00 至今产生的佣金总额
  4. 金额以元为单位,保留两位小数(后端存储分,前端 /100

③ 团队统计(三列):

  1. 直接下级:directCount
  2. 间接下级:indirectCount
  3. 升级下级:convertedCount
  4. 零数据时显示引导文案:"暂无推广数据,分享推广码给好友开始赚取佣金"

④ 佣金明细列表(最近50条):

  1. 显示:时间(MM-DD HH:mm)、来源(匿名"用户****")、级别(L1/L2标签)、金额(+¥XXX.XX)、状态
  2. 按时间倒序排列
  3. 空数据时显示"暂无佣金记录"引导

⑤ 推广工具区:

  1. "📱 生成推广海报" → 合成含小程序码的分享海报
  2. "🔗 复制推广链接" → 复制带 ref 参数的小程序路径到剪贴板
  3. "📊 提现申请" → 置灰不可点击,显示"即将开放"(Phase 1 不做提现)

页面状态:

  1. Loading 状态:各区块显示骨架屏脉冲动画
  2. 错误状态:顶部显示错误提示条 + "点击重试"按钮
  3. 空数据状态:佣金列表/团队统计显示引导文案
  4. 下拉刷新:重新请求所有数据

后端 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端或能量师) → 不显示订阅卡片,改为显示"✅ 已开通"徽章+生成推广海报入口

交互流程(未登录):

  1. 显示完整订阅卡片:🔮 AI解读会员 ¥131/年,列出权益(无限AI解读、个性化报告、推广赚佣金)
  2. 点击"登录后开通 ¥131" → 先跳登录页,登录后自动跳转支付页

交互流程(已登录未付费):

  1. 显示同上"立即开通 ¥131"按钮
  2. 点击 → 跳转支付页 product=annual,金额 ¥131

交互流程(已付费):

  1. 订阅卡片替换为灰色小徽章"✅ 已开通AI解读 | 有效期至 2027-05-28"
  2. 已付费用户显示"📱 生成推广海报"入口
  3. 能量师(practitioner)显示"✅ 已开通能量师"

支付页适配:

  1. payment/index.vue 支持 URL 参数 product
    • product=annual → 显示 C端年费 ¥131 和对应权益列表
    • product=practitioner → 显示能量师价格(通过 API 获取种子/标准价)
  2. createOrder 接口接收 productType 参数

后端适应:

  1. Order 新增 productType 字段("annual" | "practitioner"
  2. User 新增 vipType 字段("annual" | "practitioner" | null
  3. 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 表,结构如下:

    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 + 内存缓存:

  1. @PostConstruct loadAllToCache():启动时将全量配置读入 ConcurrentHashMap<String, String>
  2. 对外提供 getString(key, default)getInt(key, default) 两个读方法,O(1) 取值
  3. key 不存在时 → 返回代码中传入的 defaultValue永不抛异常
  4. updateConfig(key, value):更新 DB + 同步写入缓存,无需重启
  5. batchUpdate(Map<String, String>):循环调用 updateConfig,整个操作在 @Transactional

管理后台 API:

  1. POST /api/admin/config/list → 返回 List<SysConfig>(含全部字段)
  2. POST /api/admin/config/update → 接收 { "configKey": "value", ... } → 批量更新

小程序端 API:POST /api/pricing/current

// 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             // 名额上限
  }
}
  1. isSeedPrice 的计算逻辑:统计 orders 表中 product_type='practitioner' AND status='paid' 的记录数,小于 seedLimit 则为 true

管理后台页面(src/views/settings/index.vue):

  1. 左侧菜单新增"系统配置"入口,路由 /settings,图标 ⚙️
  2. 页面分为三个卡片区域,每个卡片顶部有"恢复默认值"按钮(仅重置单个卡片内的字段)

定价配置卡片:

  1. 四个输入项:能量师种子价、标准价、种子价名额上限、C端年费
  2. 用户输入的是元(整数),提交时自动 ×100 转为分
  3. 种子价名额上限下方实时显示"当前已使用:X / Y"(调用 countByProductTypeAndStatus
  4. 修改数量时,"已使用"比例随之动态变化

B端佣金配置卡片:

  1. 两个输入项:L1 佣金(¥500)、L2 佣金(¥100)
  2. 输入单位为元(整数),提交时 ×100 转为分
  3. 下方实时显示比例计算:
    • "L1 ¥500 = 种子价 38.1% / 标准价 25.2%"
    • "L2 ¥100 = 种子价 7.6% / 标准价 5.0%"
  4. 比例随定价或佣金值的修改实时联动更新

C端佣金配置卡片:

  1. 两个输入项:直接佣金比例(40%)、上级佣金比例(5%)
  2. 输入单位为"%"(整数,如输入"40"表示40%),提交时转为万分比 ×100
  3. 下方实时显示金额分配预览:
    • "每笔 ¥131:直接 ¥52.40(40.0%)/ 上级 ¥6.55(5.0%)/ 平台 ¥72.05(55.0%)"
  4. 费率修改时金额分配实时联动更新

保存逻辑:

  1. 点击"保存全部配置"按钮 → 触发 POST /api/admin/config/update
  2. 所有字段批量提交,不逐个保存
  3. 保存前做前端校验:金额 > 0、比例 0-100%、名额上限 > 0
  4. 保存成功 → ElMessage.success("配置已保存,已实时生效")
  5. 保存失败 → ElMessage.error("保存失败:" + 错误详情)
  6. 前端无 el-form 的 reset 行为 —— 用户手动刷新页面可恢复到上次保存的值

错误与边界:

  1. 网络失败时显示"保存失败,请重试",页面不跳转,已修改的值保留在输入框中
  2. 同时两个管理员保存 → 后保存的覆盖先保存的,不做冲突检测(Phase 1 简化)
  3. 输入非法值时(如空字符串、负数),el-input-number 的 min 属性直接阻止输入
  4. 所有金额字段不允许出现小数(元为整数输入,单位转换在后端)

8.1.2 后端接口规格

POST /api/admin/config/list

// 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

// Request:
{
  "pricing.practitioner.seed": "131400",
  "commission.practitioner.l1": "60000"
}
// Response: { "code": 0 }

POST /api/admin/config/reset(可选,单条重置)

// 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 核心代码:

@Service
public class SysConfigService {
    private final ConcurrentHashMap<String, String> 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<String, String> configs) {
        configs.forEach(this::updateConfig);
    }
}

8.1.4 管理后台前端规格

文件 操作 说明
src/views/settings/index.vue 新增 系统配置页面(~200行)
src/router/index.ts 修改 新增 /settings 路由
src/layout/index.vue 修改 新增菜单项

侧边栏菜单新增:

<el-menu-item index="/settings">
  <el-icon><Setting /></el-icon>
  <span>系统配置</span>
</el-menu-item>

页面组件结构:

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

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 → 接收筛选参数,返回分页订单列表

    // 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 动态拼接)

前端页面:

  1. 表格列:ID、订单号、用户ID、产品类型("能量师" / "年费")、金额(¥格式)、种子价标记、状态(已支付/待支付)、支付时间
  2. 产品类型使用 el-tag 展示:能量师→蓝色,年费→绿色
  3. 种子价订单显示 🔥 种子 el-tag(橙色)
  4. 状态使用 el-tag:已支付→success,待支付→warning
  5. 表格上方的工具栏包含两个筛选器:
    • 产品类型:el-select(全部 / 能量师 / C端年费)
    • 状态:el-select(全部 / 已支付 / 待支付)
  6. 筛选器 change 时重新请求接口
  7. 底部分页组件:显示总条数,支持切换页码

当前代码待补全:

  • AdminController.listOrders() 目前返回 Result.success(null) → 需替换为真实数据
  • Order 实体和 OrderRepository 已有,但缺 productTypeisSeedPrice 字段 → 需新增
  • 订单表 orders 需增加 product_typeis_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 → 支持分页和按级别筛选

    // 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):

  1. 顶部统计卡片(已有框架)→ 增强:
    • 已结算佣金:¥3,500.00(绿色)
    • 待结算佣金:¥0.00(黄色,Phase 1 恒为 0,保留字段显示)
    • 总佣金:¥3,500.00
    • 新增第四个卡片:总笔数(L1: 12 笔 / L2: 8 笔)
  2. 表格列(增强现有):ID、订单ID、来源用户ID、获得用户ID、级别(L1/L2 el-tag)、金额(¥格式)、备注(remark 字段)、状态、创建时间
  3. 新增级别筛选器:el-select(全部 / L1 / L2),筛选时重新请求接口
  4. 底部分页:当前 :total="commissions.length" 是前端假分页,改为后端真分页
  5. 备注列显示 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,OI,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<ChartRecord> 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=truerequireConfirmation=true 则弹确认框;③确认后再次请求;④成功后跳转 chart/index;⑤"想了解的问题"加 maxlength=100 + 实时字数显示
client/utils/api.js 新增 consultationApistart(data)POST /api/chart/startdetail(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