feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具

落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-03 11:37:53 +08:00
co-authored by Cursor
parent 15a9db374a
commit bd22d9dddd
248 changed files with 26309 additions and 842 deletions
+3 -1
View File
@@ -27,6 +27,8 @@ Also always: `ai-contract.md`, `forbidden.md`, `file-map.md`, `workflow.md`, `co
├── domain/
│ └── domain-map.md # DDD contexts / aggregates
├── product/
│ ├── feature-design.md # Feature Spec 强制规范
│ ├── feature-spec/ # 单功能详细设计
│ ├── feature-map.md # 愈心谷 Feature Tree
│ └── cece-feature-map.md
├── file-map.md
@@ -36,7 +38,7 @@ Also always: `ai-contract.md`, `forbidden.md`, `file-map.md`, `workflow.md`, `co
├── api.md
├── database.md
├── ui.md # IA pointer → design/
├── design/ # AI Design System Contract
├── design/ # AI Design System + reverse-engineering-spec.md
├── environment.md # Local / CI / Prod 分离
├── development.md # 本地本机开发
├── docker.md # Docker 用途边界
+11 -7
View File
@@ -9,26 +9,30 @@ MUST read:
1. `.ai/constitution.md`
2. `.ai/architecture.md`
3. `.ai/domain.md` (+ `domain/domain-map.md` when touching models/API)
4. `.ai/coding.md`
5. Task-relevant: `api.md` / `database.md` / `ui.md` / `security.md` / `product/lexicon.md` + `product/feature-map.md`
6. Run/dev tasks: `environment.md` + `development.md`(部署/镜像任务才读 `deployment.md` / `docker.md`
7. UI tasks: `design/design-system.md` + `design/component-catalog.md` + `design/platform/*`
8. Relevant ADR under `.ai/adr/` if changing stack or style
4. `.ai/definition-of-done.md`(完成标准;开始前知道何谓 Done
5. `.ai/coding.md`
6. Task-relevant: `api.md` / `database.md` / `ui.md` / `security.md` / `testing.md` / `product/lexicon.md` + `product/feature-map.md` + **`product/feature-design.md`** + **`product/feature-spec/<id>.md`**(该功能 Spec;§4 User Flow、§7 Rules、§9 API、§12 Acceptance 必须非空
7. Run/dev tasks: `environment.md` + `development.md`(部署/镜像任务才读 `deployment.md` / `docker.md`
8. UI tasks: `design/design-system.md` + `design/component-catalog.md` + `design/platform/*`
9. Competitor / reverse-engineering tasks: `design/reverse-engineering-spec.md`STEP 118 · Evidence First;禁止 Demo 式省略)
10. Relevant ADR under `.ai/adr/` if changing stack or style
## After coding
MUST:
1. Run / follow `.ai/review.md` and print Review block
2. Verify `.ai/definition-of-done.md`
1. Verify `.ai/definition-of-done.md`**Done ≠ Code Complete**
2. Run / follow `.ai/review.md` and print **Review Report**DoD §7
3. Use matching checklist under `.ai/checklists/`
4. Prefer verifying with `.ai/commands.md` (build/test/health)
5. If P1 scope: update or respect labels in `product/p1-status.md`(当前为 **P1 Complete**;扩行为须新 Spec
## Never
- Guess API shape
- Guess DB schema
- Guess product requirement
- Code new/changed feature behavior without Feature Spec`product/feature-design.md`
- Invent endpoints, tables, or domain words not in `.ai/domain.md` / OpenAPI / task
- Silently reverse an Accepted ADR
- Default local workflow to full-stack Docker / rebuild app images on every code change
+1 -1
View File
@@ -15,7 +15,7 @@
| `scale` | 探索测试定义与计分 | Yes |
| `relation` | RelationInsight | Yes **必做** |
| `report` | GrowthReport 读取 + **权益裁剪** | Yes |
| `ask` | Thread/Message规则或薄 LLM | Skeleton |
| `ask` | Thread/MessageP1 规则引擎 + 额度 | YesDemo |
| `companion` | SolarTerm 读、Mood 写 | Shell |
| `membership` | 成长会员状态与额度 | Yes |
| `order` | Order + pay-mock + DeepAccess 发放 | Yes |
+36 -14
View File
@@ -1,16 +1,38 @@
# Checklist: Feature
- [ ] Architecture layers respected
- [ ] Domain words match `.ai/domain.md`
- [ ] API (+ OpenAPI if public)
- [ ] Service
- [ ] Repository (if persistence)
- [ ] Migration (if schema)
- [ ] SDK/types (if client needs)
- [ ] UI (if user-facing)
- [ ] Test or smoke steps
- [ ] Docs touched if behavior user-visible
- [ ] `go test` / `npm run build:h5` as applicable
- [ ] Health still OK
- [ ] Review block printed
- [ ] DoD satisfied
对照 `.ai/definition-of-done.md` + `product/feature-design.md`。全部适用项勾选后才能称 Feature Complete
## Product / Spec
- [ ] `feature-spec/<id>.md` 存在且 Status=Active(或本次已更新)
- [ ] feature-map / journey / page-tree 覆盖
- [ ] Spec §4 User Flow 完整(含无数据/权益分支)
- [ ] Spec §7 Business Rules 含 Server 权益
- [ ] Spec §9 API 与 OpenAPI 一致
- [ ] Spec §12 Acceptance 已逐条验证
- [ ] Loading / Empty / Error / Normal+ Locked 若适用)
- [ ] lexicon 文案
## Backend
- [ ] OpenAPI
- [ ] Envelope errors(非裸 error
- [ ] handler → service → repository
- [ ] AuthZ / 权益服务端
- [ ] Migration if schema
- [ ] L1 unit for core logic
## Frontend
- [ ] 四态(+ Locked
- [ ] lexicon 文案
- [ ] SDK/types 若需要
## Verify
- [ ] L0: `go test ./...` / `npm run build:h5`
- [ ] L2 integration 或书面 smokeP1 关键流须自动化)
- [ ] Health OK
- [ ] Review Report 已输出
- [ ] Spec §14 Implementation Notes / Gaps 已更新
- [ ] Scope label 正确(Demo / Feature Complete / …)
+12 -4
View File
@@ -11,10 +11,12 @@ Policy: [environment.md](environment.md) · [development.md](development.md)
1. Install host toolchains: Go 1.22+ · Node 20+ · Docker Desktop(仅依赖服务)
2. `npm install`
3. `npm run deps:up` `docker compose -f docker-compose.dev.yml up -d`
4. Start API once so migrations apply: `cd apps/api && go run ./cmd/server`
5. Verify: `curl -s http://127.0.0.1:8080/api/v1/healthz`
6. Start H5: `npm run dev:h5` → http://127.0.0.1:5173
3. `cp apps/api/config.example.yaml apps/api/config.local.yaml` `database.*``deepseek.api_key`
4. `npm run deps:up`(或 `docker compose -f docker-compose.dev.yml up -d`
5. Start API once so migrations apply: `cd apps/api && go run ./cmd/server`
6. Verify: `curl -s http://127.0.0.1:8080/api/v1/healthz`
7. Start H5: `npm run dev:h5` → http://127.0.0.1:5173
8. (可选)埋点:`cp apps/user-h5/.env.example apps/user-h5/.env.local`,填 `VITE_GA_MEASUREMENT_ID`;调试可设 `VITE_ANALYTICS_DEBUG=1`(见 `feature-spec/analytics.md`
Optional CN Go proxy: `export GOPROXY=https://goproxy.cn,direct`
Optional API hot reload: install `air`, then `cd apps/api && air`(见 `.air.toml`
@@ -46,10 +48,14 @@ Stop deps: `npm run deps:down`
cd apps/api
go mod tidy
go test ./...
# L2(需 deps:up):Portrait / Relation / Membership 三流
go test ./internal/integration/ -count=1
go build ./...
go run ./cmd/server
```
根目录快捷:`npm run test:api` · `npm run test:api:integration`L2 需先 `deps:up`
Env defaults / template: `deploy/.env.example`
Migrate: applied on API startup (`apps/api/migrations/*.up.sql`)
@@ -67,6 +73,8 @@ curl -s http://127.0.0.1:8080/api/v1/ping
```bash
npm run dev:h5
npm run build:h5
npm run test:h5 # Vitest
npm run test:e2e # Playwright(先 build:h5;用系统 Chrome
```
**Do not** dockerize Vite for everyday coding.
+1
View File
@@ -15,6 +15,7 @@
9. Every API documented (OpenAPI or apps/docs).
10. Every database change versioned (migration required).
11. **Local feedback speed > local environment purity.** 本地开发追求分钟级反馈;生产/CI 追求环境一致。二者禁止混用工作流(见 `.ai/environment.md`)。
12. **Done ≠ Code Complete.** 功能完成标准见 `.ai/definition-of-done.md`;禁止仅以「有 API / 能打开页 / build 过」宣告完成。
## Product Laws (YuXinGu)
+190 -13
View File
@@ -1,17 +1,194 @@
# Definition of Done
# Definition of Done — AI Quality Gate(强制)
Code finished ≠ Done.
**Done ≠ Code Complete.**
**Done = Feature Complete + Verified + Documented.**
A feature is Done only when applicable items PASS:
任何功能在未满足下列适用标准前,**禁止**对用户说「完成 / Done / P1 Complete」。
若某项 N/A,必须写明原因;静默跳过 = Not Done。
- [ ] Code (compiles / typechecks)
- [ ] API (handler + service + repository as needed)
- [ ] Test (unit or listed smoke for critical paths)
- [ ] Migration (if schema changed)
- [ ] Docker / deploy notes (if new service or env)
- [ ] Docs (OpenAPI / short PRD note if user-visible)
- [ ] Review (`.ai/review.md` checklist output)
- [ ] Build (`go test ./...` and/or `npm run build:h5` for touched side)
- [ ] Health check still green (`/api/v1/healthz`)
加载:每个任务结束前必须对照本文件;见 `AGENTS.md`
If an item is N/A, state why. Silent skip = not Done.
---
## 0. 禁止的「伪完成」信号
仅有下列项 **不足以** 称 Done
- API 路由存在
- 页面能打开
- `go test ./...``npm run build:h5` 通过
- **没有** `product/feature-spec/<id>.md` 却声称功能完整
还缺:Feature Spec、测试分层、异常/空/加载态、权限、完整用户路径、文档与 Review。
---
## 1. Feature 完成(产品层)
必须:
| 项 | 要求 |
|---|---|
| Feature Spec | 存在 `product/feature-spec/<id>.md`;§4 Flow / §7 Rules / §9 API / §12 Acceptance 非空(见 `feature-design.md` |
| Feature Map | 已在 `product/feature-map.md` 定义且范围清晰 |
| 用户路径 | Spec §4 + `user-journey.md` 对应 Step;入口可达 |
| 页面入口 | Spec §5 + `page-tree.md` 路由存在 |
| Loading | 网络请求有加载态(Spec §6) |
| Empty | 无数据有引导(非空白死页) |
| Error | 失败可感知 + 可恢复(重试/返回) |
示例 — 个人画像完整路径(缺一不可称该 Feature Done):
```
进入 → 无档案 → 创建档案 → 生成画像
→ 查看基础版 → 深度版入口 → 支付/mock → 完整分析可见
```
仅有「`/portrait` 显示一段文案」= **Demo / Incomplete**
---
## 2. Backend Done
### 2.1 API
- OpenAPI`proto/openapi.yaml`)已更新
- Request / Response 字段明确
- 错误走统一信封 `{code,message,data}`;业务失败用数字 `code` + 可读 `message`
- **禁止**裸 `{ "error": "..." }` 作为对外约定
### 2.2 分层
```
handler → service → repository → database
```
- 业务逻辑不在 handler
- 权益 / 归属校验在 service(或明确 domain 层),不在前端
### 2.3 测试(见 §4
- 核心 service / engine 有单元测试(Level 1
- P1 关键流有集成测试或可重复的自动化冒烟(Level 2)
---
## 3. Frontend Done
### 3.1 页面四态
每个网络页面必须具备:
| 状态 | 要求 |
|---|---|
| Normal | 成功数据正确展示 |
| Loading | 请求中 |
| Empty | 无档案 / 无列表等引导 |
| Error | 失败文案 + 重试或返回 |
示例 — 问答不能只有聊天壳;至少覆盖:无档案、发送中、失败重试、历史为空(登录态按当前 Visitor 模型裁剪)。
### 3.2 组件
抽公共组件时:可复用、props 明确、状态完整;遵守 `design/component-catalog.md` + lexicon。
### 3.3 文案
用户可见文本符合 `product/lexicon.md`
---
## 4. Test Done(分级)
| Level | 名称 | 最低要求 | P1 Feature |
|---|---|---|---|
| **0** | Build | `go test ./...`;触及 H5 则 `npm run build:h5` | 必须 |
| **1** | Unit | Domain / Service / 算法(如 portrait、relation、scale 计分) | 必须 |
| **2** | Integration | 至少一条端到端 API 流可自动跑 | **P1 关键流必须** |
| **3** | E2E | 浏览器关键路径(Playwright 等) | P1 收口前至少 1 条主路径 |
### P1 必须覆盖的 Integration 流(Level 2
1. **Portrait** create profile → generate portrait → deep_access mock → detail 可见
2. **Relation** two profiles → relation insight → deep_access → tips 可见
3. **Membership(有实现后):** subscribe mock → 权益生效 → 报告完整字段
Level 0 alone ≠ Backend/Frontend Done。
---
## 5. Database Done
- `apps/api/migrations/` 有对应变更
- 新环境:`deps:up` → API 启动 migrate → 可服务
- 破坏性变更有 rollback / forward 策略(见 `database.md`
- Seed 可选但须幂等或可重复说明
禁止手改共享/生产库代替 migration。
---
## 6. Security Done
- 权益(深度版 / 成长会员)**只在服务端**裁剪字段
- 用户只能访问自己的 Profile / Report(归属校验)
- 入参校验;参数化 SQL(无拼接)
- Secret 不入库、不进镜像、不进提交
禁止:前端 `if (vip) showFullReport()` 作为唯一闸门。
---
## 7. AI Review Done
功能声称完成前,**必须**输出:
```markdown
## Review Report
- Feature: <name>
- Scope label: Demo | Feature Complete | P1 Complete | N/A
- Architecture: PASS | FAIL — <note>
- API: PASS | FAIL | N/A — <note>
- Security: PASS | FAIL | N/A — <note>
- Test (L0/L1/L2/L3): PASS | FAIL — <note>
- Frontend states (L/E/E/N): PASS | FAIL | N/A — <note>
- Documentation: PASS | FAIL | N/A — <note>
- Environment: PASS | FAIL | N/A — <note>
- Known Issues: <list or none>
```
任一关键 FAIL → 不得标 Done。
另保留 `.ai/review.md` 细项可一并勾选。
---
## 8. Release Done(版本发布时)
- CHANGELOG
- Migration 说明
- Environment / 配置说明
- Test Result(含 Level
- Rollback Plan
---
## 9. 阶段标签(强制用词)
| 标签 | 含义 |
|---|---|
| **Vertical Slice Demo** | 主路径可演示;缺测/缺态/缺功能仍可存在 |
| **Feature Complete** | 单功能满足本文件 §17 |
| **P1 Complete** | feature-map P1 必做项全部 Feature Complete + L2 三流 + 至少 1 条 L3(或书面豁免经用户同意) |
当前仓库默认状态见:[product/p1-status.md](product/p1-status.md)。
---
## 10. 宣称 Done 前的最短自问
1. 用户能否走完 journey,不只是打开页?
2. 无数据 / 失败时页面是否可用?
3. 不付费能否靠改前端看到深度字段?(不应)
4. 新机器按 `commands.md` 能否复现?
5. Review Report 是否已贴出且无 FAIL
+3
View File
@@ -4,6 +4,7 @@ Machine-readable **UI/UX/VI contract** for agents generating H5 / Mini Program /
| File | Purpose |
|---|---|
| **[reverse-engineering-spec.md](reverse-engineering-spec.md)** | **竞品/成熟 App 逆向工程设计规范(V1.0)** · Evidence First · STEP 118 |
| [design-system.md](design-system.md) | Foundation: tokens, color, type, spacing, motion, voice |
| [component-catalog.md](component-catalog.md) | Button / Card / Chat / Report / Membership … |
| [platform/h5.md](platform/h5.md) | Mobile Web implementation limits |
@@ -13,3 +14,5 @@ Machine-readable **UI/UX/VI contract** for agents generating H5 / Mini Program /
Runtime tokens: `packages/ui/src/tokens.css`
Product chrome / tabs: `../ui.md`
对标测测等成熟 App 时:**先按 reverse-engineering-spec 做完整逆向**,再映射 lexicon / feature-spec 后编码。禁止只出 Demo/MVP 式省略分析。
+265
View File
@@ -0,0 +1,265 @@
# AI Reverse Engineering Design Specification (V1.0)
> **项目级设计规范** · 适用于 Claude Code / Cursor / GPT / Gemini 等全部 AI
> 场景:对标成熟 App(如测测)做**能力与系统逆向**,输出可直接支撑工程落地的完整设计
> 冲突优先级见文末「与愈心谷契约的关系」
---
## 1. Mission
你不是产品经理,也不是程序员。
你的身份是:
**Senior Reverse Engineering Architect**
你的目标不是设计一个「类似功能」,而是:
**完整还原目标 App 的功能、业务逻辑、数据结构、状态机和运营体系。**
任何输出都必须以:
- **Production Ready**
- **Enterprise Ready**
- **Feature Complete**
作为目标。
| 禁止 | 必须 |
|---|---|
| Demo | 可支撑前后端 + 运营完整链路 |
| MVP(作为终点) | Feature Complete 分析 |
| 为简单而省略 | 全部展开 |
| 只描述页面外观 | 推导页面背后的系统 |
---
## 2. Reverse Engineering Rule
每一个页面都必须认为:
> 你看到的只是冰山一角。
> 页面背后一定存在:业务逻辑 · 数据库 · 后台 · 缓存 · 状态机 · 运营 · 统计 · 权限 · 异常处理 · 日志 · 配置 · 监控 ……
你的任务就是**全部推导出来**。
---
## 3. Completeness Principle
任何功能**禁止**输出:
- 应该 / 可能 / 大概
- 简单实现 / 略 / TODO / 以后再做
必须全部展开。缺一项即未完成。
---
## 4. Evidence First(证据优先)
每一项分析必须标注来源,禁止把猜测写成事实。
| 标记 | 含义 |
|---|---|
| ✅ UI 证据 | 页面上确实存在的元素(截图/真机/录屏) |
| ✅ 行为证据 | 根据可复现交互确定的流程(点击、跳转、接口回包) |
| 🟡 推断 | 根据行业经验 / 同类产品惯例推导 |
| 🔵 工程建议 | 为保证系统完整性补充(原 App 未必公开可见) |
输出表格建议格式:
| 推导内容 | 来源 |
|---|---|
| … | ✅ UI 证据 / ✅ 行为证据 / 🟡 推断 / 🔵 工程建议 |
**「我看到了什么」与「我推断了什么」必须分开。**
---
## 5. Reverse Engineering Workflow
每分析一个页面(或一条完整用户能力),**严格按下列顺序,禁止跳步。**
### STEP 1 — 页面分析(UI Analysis
必须列出:
页面组成 · 所有区域 · 所有按钮 · 所有 Icon · 所有文本 · 所有图片 · 所有 Banner · 所有卡片 · 所有列表 · 所有 Tab · 所有浮窗 · 所有弹窗 · 所有菜单 · 所有动画
要求:不能遗漏任何可见元素。每项尽量带 ✅ UI 证据。
### STEP 2 — 功能分析(Feature Analysis
对页面上每一个元素,必须回答:
- 有什么作用?
- 点击后发生什么?长按?双击?
- 是否可分享 / 复制 / 删除 / 收藏 / 举报 / 编辑?
- 是否有权限限制 / VIP 限制 / 登录限制?
### STEP 3 — 用户流程(User Flow
画出完整主流程:
```text
进入 → 加载 → 成功 → 操作 → 提交 → 返回 → 退出
```
同时必须覆盖失败与边界:
网络错误 · Token 失效 · 服务器异常 · 权限不足 · 数据为空 · 会员限制 · 余额不足 · 接口超时 · 审核失败
### STEP 4 — 状态机(State Machine
每个页面必须列出状态(至少覆盖):
Init · Loading · Refreshing · Loaded · Empty · Offline · Error · PermissionDenied · LoginRequired · VIPLocked · Submitting · Success · Failed · Retrying · Deleted · Disabled · Hidden
并给出**状态转换图**(可用 mermaid)。
### STEP 5 — 数据模型(Data Model
推导所有对象(如 User / Profile / AstrologyChart / Order / Membership / Notification …)。
每个字段必须写:字段 · 类型 · 是否为空 · 默认值 · 来源 · 用途 · 是否缓存 · 是否索引 · **证据标记**
### STEP 6 — 数据库设计
推导表结构(含运营/审计表)。每张表:字段 · 主键 · 唯一索引 · 普通索引 · 外键 · 更新时间 · 删除策略。
落地到本仓库时,同步 `.ai/domain/erd.md` 与 migrations。
### STEP 7 — API Reverse Engineering
每个接口必须包含:
URL · Method · Request · Response · ErrorCode · RateLimit · Permission · Cache · Retry
标准:可以直接开发(并对齐本仓 OpenAPI 切片)。
### STEP 8 — 后台运营系统
任何前台功能必须推导后台:
Banner · 推荐位 · 内容审核 · 用户管理 · 订单管理 · 会员配置 · 活动配置 · 推送 · 统计 · 运营位 · AB Test · 配置中心
无 UI 证据的后台能力标 🟡/🔵,不可省略整类。
### STEP 9 — 权限系统
推导角色:游客 · 登录用户 · VIP · SVIP · 管理员 · 运营 · 客服 · 审核员 · 超级管理员
每个角色:能做什么 / 不能做什么。映射到本产品时对齐现有 Visitor / DeepAccess / Membership(见 feature-spec)。
### STEP 10 — 支付系统
若涉及:订单 · 退款 · 支付状态 · 失败/取消 · 重复支付 · 补单 · 风控 · 发票。
本仓当前可用 `pay-mock`;完整支付链路仍须在 Spec 中写清状态机,实现可分期。
### STEP 11 — 消息系统
Push · 站内信 · 短信 · 邮件 · 消息中心 · 未读数 · 角标 · 通知策略。
### STEP 12 — 埋点系统
页面曝光 · 按钮点击 · 停留 · 漏斗 · 转化 · 留存 · 分享 · 支付 · 搜索 · 异常。
事件名过 `.ai/product/feature-spec/analytics.md` 与 lexicon。
### STEP 13 — 配置系统
哪些后台可配 / 写死 / 远程配置 / 灰度 / AB。
### STEP 14 — 缓存策略
本地缓存 · Redis · CDN · 图片 · 分页 · 用户 · 配置。
### STEP 15 — 异常处理
断网 · 弱网 · 超时 · 重复点击 · Token 失效 · 数据损坏 · 接口升级 · 版本过低 · 审核失败 · 资源不存在。
### STEP 16 — 日志系统
用户日志 · 错误日志 · 接口日志 · 支付日志 · 审核日志 · 运营日志 · 安全日志。
### STEP 17 — 安全分析
权限 · SQL 注入 · XSS · CSRF · 重放 · 验证码 · 风控 · 设备绑定 · 账号安全。
并遵守 `.ai/security.md`
### STEP 18 — 可扩展性
未来增量 · DB 扩展 · API 兼容 · 模块解耦。
---
## 6. Completeness Checklist
输出结束前必须自检(任一项未分析则**不得结束**):
- [ ] 页面 / 按钮 / 弹窗
- [ ] 用户流程 / 异常流程
- [ ] API / 数据库
- [ ] 后台 / 权限 / 支付
- [ ] 配置 / 埋点 / 日志
- [ ] 安全 / 扩展
- [ ] 每条关键结论带 Evidence 标记
---
## 7. Output Quality Standard
每个功能分析必须达到:
| 维度 | 标准 |
|---|---|
| Feature Complete | ★★★★★ |
| Production Ready | ★★★★★ |
| Enterprise Ready | ★★★★★ |
| Reverse Engineering Complete | ★★★★★ |
**不合格**:只描述页面或功能文案。
**合格**:开发团队可据此直接实现完整链路(前台 · 后台 · 接口 · 数据 · 运营),且证据层级清晰。
分析产物默认写入:
1. `product/feature-spec/<id>.md`(用户可见能力与规则;过 lexicon)
2. 必要时附 `product/feature-spec/<id>-re.md` 或同目录附录(完整 STEP 1–18 逆向底稿)
3. 同步 `erd` / OpenAPI / `page-tree` / `feature-map` / analytics
---
## 8. 与愈心谷契约的关系(强制)
本规范解决的是:**能力与系统如何被完整逆向出来**。
落地品牌与禁词仍由产品契约约束:
| 层级 | 文档 | 作用 |
|---|---|---|
| 最高 | `product/lexicon.md` | 用户可见中文;硬禁「占卜/算命」恐吓等 |
| 战略 | `product/STRATEGY.md` | 参考竞品模型 ≠ 复制竞品品牌/视觉 |
| 能力树 | `product/feature-map.md` | 分期与边界 |
| 本规范 | `design/reverse-engineering-spec.md` | 逆向分析深度与完整性 |
| 功能输入 | `product/feature-design.md` + `feature-spec/*` | 开发唯一功能输入 |
**允许**:对标测测(或其它竞品)做 STEP 1–18 完整逆向,追求 Feature Complete 系统设计。
**禁止**:在 UI/PRD/Ask 中自称「测测」;照搬竞品商标、独特视觉品牌资产;违反 lexicon 的恐吓/疗效话术。
**落地命名**:逆向结论映射为愈心谷模块名(如「星座」「合盘」),写入 feature-spec 后再编码。
分期现实:逆向分析可以一次性 Feature Complete**实现**仍按 `feature-map` 分期切片,但 Spec/附录不得用「以后再做」糊弄——未实现项标为「分期未交付」并保留完整设计。
---
## 9. AI 使用方式
| 任务类型 | 必须加载 |
|---|---|
| 对标竞品 / 补齐合盘·星座等能力 | 本文件 + lexicon + feature-design + 对应 feature-spec |
| 从截图/录屏做页面逆向 | 本文件 Workflow STEP 1→18,禁止跳步 |
| 写/改 Feature Spec | feature-design + 本文件 Checklist + Evidence First |
| 编码实现 | Spec 已 Active 且 §12 可测;本文件不替代 coding/DoD |
一句话口令:
> 你的任务不是设计一个差不多的功能,而是逆向工程:证据优先、系统完整、可直接工程落地;品牌与禁词服从愈心谷 lexicon。
+6 -1
View File
@@ -24,8 +24,13 @@ Use ONLY these words in code and API.
| Membership | 成长会员 |
| DeepAccess | 深度版 |
| Constitution | 身心探索 |
| StarProfile | 星象性格(P2 |
| LifeRhythm | 身心节律(P2 |
| ImageCard | 意象卡片(P2 |
Deprecated aliases: Decode→Portrait · Match→RelationInsight · Unlock→DeepAccess
Deprecated aliases: Decode→Portrait · Match→RelationInsight · Unlock→DeepAccess · Tarot→ImageCard
Never invent package names: `horoscope_luck`, `tarot`(用户域用 `image_card` / `star_profile` / `life_rhythm`)。
---
+6 -3
View File
@@ -12,8 +12,11 @@
| Profile | Profile | Yes | `internal/service/profile` |
| Discovery | Portrait, Scale, ScaleResult, GrowthReport | Yes | `portrait`, `scale`, `report` |
| Relation | RelationInsight | Yes **必做** | `relation` |
| Ask | AskThread, AskMessage, AskQuota | Skeleton | `ask` |
| Companion | Mood, SolarTerm | Shell | `companion` |
| Ask | AskThread, AskMessage, AskQuota | Demo(规则引擎) | `ask` |
| Companion | Mood, SolarTerm | P2 | `internal/companion` · `/solar-terms/today` · `/moods` |
| StarProfile | StarProfile report | P2 | `internal/star` · `POST /reports/star` |
| LifeRhythm | LifeRhythm / Constitution | P2 | `internal/rhythm` · `POST /reports/rhythm` |
| ImageCard | ImageCard draw + deck | P2 | `internal/imagecard` · `/image-cards/*` |
| Memory | MemoryItem | P2/P3 | `memory` |
| Commerce | Subscription, Membership, Order, Payment, DeepAccess | Yes mock | `order`, `membership` |
| Consult | — | P3 | — |
@@ -39,4 +42,4 @@ Identity → Profile → Discovery(Portrait/Scale/GrowthReport)
2. Ask 必须绑定 `profile_id`Self 或 Other)。
3. RelationInsight 引用两个 Profile;禁止无档案的「合婚」式空算。
4. 客户端不可信任本地「已深度版」标志。
5. 禁止吉凶/运势类字段名进入 schema(如 `luck_score`)。
5. 档案表不存吉凶列;**报告 JSON payload 允许**运势分/匹配指数(如 `fortune.daily.score``love_index`)。
+16
View File
@@ -31,10 +31,26 @@
| birth_date | date | required P1 |
| birth_time | time null | optional |
| birth_place | text null | optional |
| geo_lat | float null | 附近的人 |
| geo_lng | float null | |
| geo_visible | bool | default false;开启后可被附近列表看到 |
| gender | text null | |
| relation_type | text null | partner/family/friend… when other |
| created_at | timestamptz | |
### synastry_invites
| column | type | notes |
|---|---|---|
| id | uuid PK | |
| token | text unique | 分享路径 `/synastry/invite/:token` |
| host_user_id | uuid FK→users | |
| host_profile_id | uuid FK→profiles | |
| expires_at | timestamptz | |
| guest_user_id | uuid null | |
| guest_profile_id | uuid null | |
| report_id | uuid null FK→growth_reports | accept 后写入 |
| created_at | timestamptz | |
### scales
| column | type | notes |
|---|---|---|
+4
View File
@@ -71,10 +71,13 @@ Runtime compose for **deps only**: repo-root `docker-compose.dev.yml`.
| Path | Owns |
|---|---|
| `product/lexicon.md` | 产品中文语言契约(最高优先级) |
| `product/feature-design.md` | Feature Spec 强制规范 |
| `product/feature-spec/` | 单功能详细设计(流程/规则/API/验收) |
| `product/user-journey.md` | 转化路径 |
| `product/page-tree.md` | H5 路由冻结 |
| `product/feature-map.md` | 能力树 · 五 Tab 冻结 |
| `product/ENGINEERING-FREEZE.md` | P1 真源索引 |
| `playbooks/feature-spec.md` | Spec → 实现步骤 |
| `domain/domain-map.md` + `erd.md` | Contexts + 表结构 |
| `architecture/go-services.md` | Go 包边界 |
| `domain.md` | Tech ↔ 用户名 |
@@ -83,6 +86,7 @@ Runtime compose for **deps only**: repo-root `docker-compose.dev.yml`.
| Path | Owns |
|---|---|
| `reverse-engineering-spec.md` | 竞品/成熟 App 逆向工程规范(STEP 118 · Evidence First |
| `design-system.md` | Tokens, color, type, spacing, motion, voice |
| `component-catalog.md` | Allowed reusable UI components |
| `platform/*.md` | H5 / mini-program / website / Flutter limits |
+2
View File
@@ -42,3 +42,5 @@ AI obeys NEVER rules strictly.
- Never modify files outside the current task.
- Never extend Legacy root HTML unless the task is migration.
- Never guess requirements — ASK FIRST.
- Never implement **new feature behavior** or **expand** an existing feature without an Active Feature Spec under `product/feature-spec/`(见 `product/feature-design.md`)。Bugfix 若暴露 Spec 错误必须同步修正 Spec。
- Never treat `feature-map.md` alone as sufficient design input for coding.
+19
View File
@@ -0,0 +1,19 @@
# Playbook: Feature Spec → Implementation
在写任何新功能或重大行为变更前执行。
## Steps
1.`product/feature-map.md` 确认节点与分期(P1/P2/P3/No)。
2. 复制 `product/feature-spec/_TEMPLATE.md``product/feature-spec/<id>.md`
3. 填满 §1–§13;文案过 `lexicon.md`
4.`feature-spec/README.md` 登记索引。
5. 若改数据/API:同步 `domain/erd.md` + `proto/openapi.yaml`(可先草案)。
6.`playbooks/add-api.md` / `new-page.md` / `new-table.md` 实现。
7. 编码中更新 Spec §14 Implementation NotesGaps 标 `Gap:`
8. 对照 Spec §12 + `checklists/feature.md` + DoD 验收。
## Stop
- Spec §4 / §7 / §9 / §12 为空 → 禁止编码。
- 需求不清 → ASK FIRST。
+22 -15
View File
@@ -6,24 +6,31 @@
|---|---|---|
| 1 | 产品语言 | `product/lexicon.md` |
| 2 | 能力树 + 冻结 IA | `product/feature-map.md` |
| 3 | 用户旅程 | `product/user-journey.md` |
| 4 | H5 页面树 | `product/page-tree.md` |
| 5 | 域词 | `domain.md` |
| 6 | Domain Map | `domain/domain-map.md` |
| 7 | ERD | `domain/erd.md` |
| 8 | API Contract | `proto/openapi.yaml` |
| 9 | Go 服务边界 | `architecture/go-services.md` |
| 10 | Design System | `design/design-system.md` |
| 3 | Feature Spec 规范 | `product/feature-design.md` |
| 4 | 功能详细设计 | `product/feature-spec/` |
| 5 | 用户旅程 | `product/user-journey.md` |
| 6 | H5 页面树 | `product/page-tree.md` |
| 7 | 域词 | `domain.md` |
| 8 | Domain Map | `domain/domain-map.md` |
| 9 | ERD | `domain/erd.md` |
| 10 | API Contract | `proto/openapi.yaml` |
| 11 | Go 服务边界 | `architecture/go-services.md` |
| 12 | Design System | `design/design-system.md` |
环境规范层已冻结(`.ai/environment.md` 等)**停止扩展工程 meta 文档**
环境规范层已冻结(`.ai/environment.md` 等)。
完成闸门:`.ai/definition-of-done.md`(强制)。
状态标签:`.ai/product/p1-status.md`**`P1 Complete`**(必做项收口;P2 另列)。
下一步:**按 P1 竖切实现**Profile → Portrait → RelationInsight → Scale → Commerce mock → Ask 壳),不再横向加规范或加功能面。
**P2 三模块(设计已立项,实现另开切片):** 星象性格 · 身心节律 · 意象卡片
→ Spec`feature-spec/star-profile.md` · `life-rhythm.md` · `image-card.md` · 队列见 `feature-spec/P2-BACKLOG.md`
→ 实现前须 Spec Active + OpenAPI/erd**不计入 P1 Complete**。
## Review 清单(合并前必过)
1. 用户可见文本是否仅来自 lexicon?有无旧产品词/玄学预测?
2. 路由是否落在 page-tree
3. Domain / 表名是否符合 domain.md(无 decode/luck 等)
4. 权益是否只在服务端裁剪
5. 关系理解是否仍在 P1 路径中(双引擎)
6. 数据模型是否留有 Memory / Consult 扩展位(不阻塞 P1)?
2. 对应 `feature-spec/<id>.md` 是否存在且 §4/§7/§9/§12 非空
3. 路由是否落在 page-tree
4. Domain / 表名是否符合 domain.md(无 decode/luck 等)
5. 权益是否只在服务端裁剪
6. 关系理解是否仍在 P1 路径中(双引擎)?
7. 数据模型是否留有 Memory / Consult 扩展位(不阻塞 P1)?
+14 -3
View File
@@ -3,11 +3,22 @@
| File | Purpose |
|---|---|
| [ENGINEERING-FREEZE.md](ENGINEERING-FREEZE.md) | **P1 冻结清单** |
| [p1-status.md](p1-status.md) | **P1 完成度(当前:P1 Complete** |
| [lexicon.md](lexicon.md) | 产品语言契约(最高优先级) |
| [feature-map.md](feature-map.md) | 能力树 · 五 Tab 冻结 |
| [feature-design.md](feature-design.md) | **Feature Spec 强制规范** |
| [feature-spec/](feature-spec/README.md) | **功能详细设计(单功能 HOW** |
| [feature-map.md](feature-map.md) | 能力树 · 五 Tab 冻结(WHAT) |
| [user-journey.md](user-journey.md) | 转化路径(含关系分享引擎) |
| [page-tree.md](page-tree.md) | H5 路由树 |
| [STRATEGY.md](STRATEGY.md) | 参考模型 ≠ 复制品牌 |
| [cece-feature-map.md](cece-feature-map.md) | 竞品对照(勿抄 UI |
| [cece-feature-map.md](cece-feature-map.md) | 竞品对照(勿抄品牌视觉 |
| [../design/reverse-engineering-spec.md](../design/reverse-engineering-spec.md) | **逆向工程设计规范 V1.0**(对标成熟 App 时强制) |
| [feature-spec/cece-frontend-re/](feature-spec/cece-frontend-re/README.md) | **测测前端全量逆向设计文档**(五 Tab + 工具域 + STEP 518 |
| [feature-spec/cece-frontend-re/complete-design/](feature-spec/cece-frontend-re/complete-design/README.md) | **测测对标 · 完整产品设计包(Design Complete**;模拟器校验见包内 99 |
冲突:`lexicon` > `feature-map` / `user-journey` > 历史文档。
冲突:`lexicon` > `feature-map` 分期 > **`feature-spec`** > `user-journey` / `page-tree` > 历史文档。
对标竞品分析时另加:`design/reverse-engineering-spec.md`(完整性与证据层级)。
开发链路:`feature-map` →(对标时)逆向 STEP 1–18 → `feature-spec` → domain/OpenAPI → 实现 → 测试 → Review。
P2 探索三模块(设计):[feature-spec/P2-BACKLOG.md](feature-spec/P2-BACKLOG.md) · 星象性格 / 身心节律 / 意象卡片。
+19 -3
View File
@@ -8,7 +8,20 @@
| 用户路径 | 功能命名 |
| 商业模型(测评→档案→AI→会员→咨询) | 叙事:成长 / 认识自己 / 理解他人 |
**不是**竞品复制品。UI/PRD/Ask **禁止**出现:测测(自称)、解码、运势、塔罗、吉凶、命理预测;星盘不作核心叙事
**不是**竞品品牌复制品。UI/PRD/Ask **禁止**出现:测测(自称)、占卜/算命恐吓、医疗疗效承诺;禁止以「塔罗」作主路径入口名
**能力与系统可按逆向规范完整对标**:见 [../design/reverse-engineering-spec.md](../design/reverse-engineering-spec.md)Evidence First · STEP 118 · Feature Complete)。
逆向的是功能/业务/数据/运营体系;落地时用愈心谷命名与 lexicon,不照搬竞品商标与独特视觉品牌。
**P2 允许同构探索模块**(能力可对标竞品;星座模块允许运势分与匹配指数,须过 lexicon):
| 竞品能力域 | 愈心谷模块(用户名) | Spec |
|---|---|---|
| 星座 | 星座(星盘 · 运势 · 合盘) | `feature-spec/star-profile.md` |
| 命理 | 身心节律 | `feature-spec/life-rhythm.md` |
| 塔罗 | 意象卡片 | `feature-spec/image-card.md` |
星盘/运势可作模块内主文案;品牌 slogan 仍以成长/认识自己为主。须过 lexicon。
封存的差异化长文设计见 git tag `design-vision-v1`(历史参考)。
@@ -16,10 +29,13 @@
## 执行分期(与 feature-map 对齐)
1. **P1** — 档案 · 性格探索/画像 · 人格测评 · 关系理解 · 问答 · 深度版 · 会员
2. **P2**身心探索 · 节气陪伴 · 心情记录 · 成长计划
1. **P1** — 档案 · 性格探索/画像 · 人格测评 · 关系理解 · 问答 · 深度版 · 会员 · 分享 · 埋点
2. **P2**星象性格 · 身心节律 · 意象卡片 · 节气陪伴深化 · 心情记录 · 成长计划
3. **P3** — 长期记忆 · 成长数据库 · 真人顾问
P2 三模块:**设计已立项**Spec Active);**实现前**须保持 Spec 与 OpenAPI/erd 同步,另开编码切片。
不进入 P1 Complete 判定。
冲突时:**lexicon + feature-map > 历史 Vision 文档中的旧命名(解码/Unlock 等)**。
工程冻结清单:[ENGINEERING-FREEZE.md](ENGINEERING-FREEZE.md)。
+2
View File
@@ -12,6 +12,8 @@
| 演进 | 工具 → AI 顾问 → Life OS | §4 |
愈心谷落地地图见 [feature-map.md](feature-map.md)。领域实体见 [../domain/domain-map.md](../domain/domain-map.md)。
对单页/单能力做完整逆向时,强制遵循 [../design/reverse-engineering-spec.md](../design/reverse-engineering-spec.md)Evidence First · STEP 118)。
**前端全量逆向设计套件(详细):** [feature-spec/cece-frontend-re/](feature-spec/cece-frontend-re/README.md) · **完整设计包:** [feature-spec/cece-frontend-re/complete-design/](feature-spec/cece-frontend-re/complete-design/README.md)
---
+118
View File
@@ -0,0 +1,118 @@
# Feature Design Standard
**任何新功能或重大行为变更,开发前必须存在对应 Feature Specification。**
Feature Spec 是产品、设计、开发的**唯一功能输入**。
`feature-map.md` 只回答「做什么 / 在哪棵树上」——**不够**开始编码。
完整链路:
```
feature-map(能力树)
→ feature-spec(功能详细设计)
→ domain / OpenAPI / erd(技术契约)
→ implementation
→ test
→ review
```
规范位置:本文件。
Spec 目录:[`feature-spec/`](feature-spec/README.md)。
模板:[`feature-spec/_TEMPLATE.md`](feature-spec/_TEMPLATE.md)。
对标成熟 App[`../design/reverse-engineering-spec.md`](../design/reverse-engineering-spec.md)(逆向 STEP 118 · Evidence First)。
---
## 禁止
- 根据一句需求直接编码
- 根据页面截图或竞品 UI **跳步猜测**业务规则(须走逆向规范并标注证据)
- 未定义 User Flow / 页面状态 / Business Rules 就开发
- 仅有页面或 API「能跑」就称 Feature Complete
- Spec 未更新就扩大已有功能行为(bugfix 除外)
- 在 Spec 中偷偷扩大 `feature-map` 分期范围(P2 写成 P1
- 对标竞品时输出 Demo/MVP 式省略(「略」「TODO」「以后再做」)作为 Spec 终态
---
## 允许的例外
| 场景 | 要求 |
|---|---|
| 明确 bugfix(行为已在 Spec/OpenAPI 定义) | 可直接修;若暴露 Spec 错误则同步修正 Spec |
| 文案/lexicon 微调且不改规则 | 可改代码 + lexicon;不强制新 Spec |
| 纯重构(对外行为不变) | 不强制新 Spec;DoD 仍适用 |
| 存量功能尚无 Spec | **进入该功能开发前必须先 retrofit Spec**;否则只许修明确 bug,不得扩行为 |
---
## 冲突优先级
1. `lexicon.md`(用户可见中文)
2. `feature-map.md` 分期与边界标记
3. **本功能的 `feature-spec/<id>.md`**(流程、规则、验收)
4. `user-journey.md` / `page-tree.md`
5. `domain/` + OpenAPI(实现契约;必须与 Spec §8/§9 对齐)
6. 历史 PRD / `apps/docs/*` / 立项文档
Spec 文案不得违反 lexicon。Spec 不得把 `[P2]`/`[No]` 能力写成已交付。
---
## Feature Spec 强制章节
每份 Spec 必须包含(可用 N/A + 原因,不可省略标题):
| # | 章节 | 要求 |
|---|---|---|
| 1 | 功能定义 | Name / Purpose / Business Goal |
| 2 | 用户价值 | 为何需要 / 完成后获得什么 / 为何付费 |
| 3 | 用户角色 | Guest / User / VIP(对齐实际 Identity 模型) |
| 4 | 用户流程 | ASCII 或 mermaid;含分支 |
| 5 | 页面设计 | 路由 + 页面列表;对照 page-tree |
| 6 | 页面状态 | Loading / Empty / Error / Normal / Locked(适用则写) |
| 7 | Business Rules | 编号 Rule N**权益由 Server 判断** |
| 8 | 数据模型影响 | 表/字段;指向 erd,禁止臆造 |
| 9 | API 需求 | 路径级意图;细节以 OpenAPI 为准并须同步 |
| 10 | 权限设计 | 角色 × 能力矩阵 |
| 11 | 埋点 | 事件名;未接 SDK 也先定义 |
| 12 | 测试验收标准 | Given / When / Then |
| 13 | AI 开发前检查 | checkbox |
| 14 | Implementation Notes | 编码时填写:包、迁移、与现状 gap |
User Flow 与验收故事**写在 Spec 内**,不另建平行 `user-story/` 真相源。
---
## AI 开发前检查(强制)
Before implementation, AI MUST verify:
- [ ] Feature Spec 文件存在(`feature-spec/<id>.md`
- [ ] 已在 `feature-map.md` 挂树且分期正确
- [ ] User Flow(§4)已定义
- [ ] Business Rules(§7)已定义
- [ ] API 需求(§9)已定义,并计划更新 OpenAPI
- [ ] 数据影响(§8)已核对 erd
- [ ] 页面状态(§6)已定义
- [ ] Acceptance(§12)已定义
- [ ] 文案符合 `lexicon.md`
任一项缺失 → **停止编码,先补 Spec 或 ASK**
---
## 何时更新 Spec
- 新功能:先复制 `_TEMPLATE.md` → 填满 → 再编码
- 改规则 / 改权益 / 改主流程 / 改付费墙:先改 Spec,再改代码
- 仅修 bug:若 Spec 与现实不符,修完后回写 Spec
Playbook[`../playbooks/feature-spec.md`](../playbooks/feature-spec.md)
---
## 与 DoD 的关系
无适用 Feature Spec → **不得**勾选 Feature Complete(见 `definition-of-done.md` §1)。
「OpenAPI 有了 / 页面打开了」而 Spec 缺失 = 伪完成。
+60 -9
View File
@@ -1,12 +1,14 @@
# 愈心谷 — Product Feature Map
**权威产品能力树。** 语言以 [lexicon.md](lexicon.md) 为准。
**权威产品能力树WHAT** 语言以 [lexicon.md](lexicon.md) 为准。
单功能详细设计(HOW):[feature-design.md](feature-design.md) · [feature-spec/](feature-spec/README.md)
参考测测的是:**功能模型 · 用户路径 · 商业模型**;品牌叙事与命名必须是愈心谷自己的。
领域实体:[../domain/domain-map.md](../domain/domain-map.md) · 词汇:[../domain.md](../domain.md)
竞品对照(可含竞品原名):[cece-feature-map.md](cece-feature-map.md)
策略:[STRATEGY.md](STRATEGY.md)
标记:`[P1]` 第一阶段 · `[P2]` 第二阶段 · `[P3]` 第三阶段 · `[No]`
**编码前**必须打开对应 Feature Spec;仅有本文件不够。
---
@@ -42,8 +44,12 @@ P1 双引擎:**个人画像 + 关系理解**(不可弱化关系侧)。
## 1. 首页(发现与入口中心)
详见 Spec[feature-spec/home.md](feature-spec/home.md)
### 1.1 创建个人档案 `[P1]`
详见 Spec[feature-spec/profile.md](feature-spec/profile.md)
| 三级 | 内容 |
|---|---|
| 1.1.1 我的信息 | 出生日期;出生时间/地点(可后置);性别 |
@@ -66,7 +72,7 @@ P1 双引擎:**个人画像 + 关系理解**(不可弱化关系侧)。
### 2.1 性格探索 `[P1]`
帮助用户了解行为模式。
帮助用户了解行为模式。详见:[feature-spec/portrait.md](feature-spec/portrait.md) · [reports.md](feature-spec/reports.md)
| 三级 | 内容 |
|---|---|
@@ -77,13 +83,17 @@ P1 双引擎:**个人画像 + 关系理解**(不可弱化关系侧)。
### 2.2 人格测评 `[P1]`
MBTI 类 · 沟通方式 · 情感模式 · 成长倾向(首版 ≥1 个热门)
详见 Spec[feature-spec/explore-test.md](feature-spec/explore-test.md)
### 2.3 个人画像(成长画像) `[P1]`
替代竞品「星盘档案」叙事。内容:性格特点 · 兴趣倾向 · 行为模式 · 关系特点 · 生活建议
详见 Spec[feature-spec/portrait.md](feature-spec/portrait.md)
### 2.4 关系理解 `[P1 · 双引擎之一,不可砍]`
详见 Spec[feature-spec/relation.md](feature-spec/relation.md)
| 三级 | 内容 |
|---|---|
| 2.4.1 创建关系 | 添加 TA |
@@ -93,7 +103,9 @@ MBTI 类 · 沟通方式 · 情感模式 · 成长倾向(首版 ≥1 个热门
禁止宿命/恐吓文案。增长价值不低于个人画像。
### 2.5 身心探索 `[P2]`(差异化,首版可弱入口
### 2.5 身心探索 `[P2]`(差异化)
产品化主入口见 **2.7 身心节律**(避免双树)。轻入口可保留在首页/探索。
| 三级 | 内容 |
|---|---|
@@ -103,10 +115,30 @@ MBTI 类 · 沟通方式 · 情感模式 · 成长倾向(首版 ≥1 个热门
禁止:治疗、改善疾病、疗效承诺。
### 2.6 星座 `[P2]`
详见 Spec[feature-spec/star-profile.md](feature-spec/star-profile.md)
对标竞品「星座」能力:本命星盘 · 行星 · 日/周/月/年运 · 恋爱/友情/婚姻匹配指数。
UI 名:星座。允许运势分与合盘指数;禁止占卜、算命恐吓。
### 2.7 身心节律 `[P2]`
详见 Spec[feature-spec/life-rhythm.md](feature-spec/life-rhythm.md)
对标竞品「命理」能力之生活向:**五行/体质倾向 + 生活建议**。
= §2.5 的产品化主入口。禁止流年吉凶、改命、疗效承诺。
### 2.8 意象卡片 `[P2]`
详见 Spec[feature-spec/image-card.md](feature-spec/image-card.md)
对标竞品「塔罗」能力:**投射反思卡片**。
UI **不出现「塔罗」**。禁止神谕吉凶、恐吓话术。
---
## 3. 问答(AI 核心)
详见 Spec[feature-spec/ask.md](feature-spec/ask.md)
### 3.1 AI 成长助手 `[P1]`
定位:了解你的智能伙伴。不是占卜,不是预测。
@@ -127,6 +159,8 @@ MBTI 类 · 沟通方式 · 情感模式 · 成长倾向(首版 ≥1 个热门
## 4. 陪伴(日常留存)`[P2]` 主做;`[P1]` 可占位壳
详见 Spec[feature-spec/companion.md](feature-spec/companion.md)
### 4.1 今日状态
心情记录 · 今日感受 · 成长记录
@@ -141,14 +175,18 @@ MBTI 类 · 沟通方式 · 情感模式 · 成长倾向(首版 ≥1 个热门
| 二级 | 内容 |
|---|---|
| 5.1 我的档案 | 个人资料 · 探索记录 · 关系记录 |
| 5.2 我的成长报告 | 已完成探索 · 已购深度版 · 收藏 |
| 5.3 会员中心 | 完整分析 · 高级成长报告 · AI 更多次数 · 专属内容 |
| 5.1 我的档案 | 个人资料 · 探索记录 · 关系记录 → [profile.md](feature-spec/profile.md) |
| 5.2 我的成长报告 | 已完成探索 · 已购深度版 · 收藏 → [reports.md](feature-spec/reports.md) |
| 5.3 会员中心 | 完整分析 · 高级成长报告 · AI 更多次数 · 专属内容 → [membership.md](feature-spec/membership.md) |
分享裂变:[feature-spec/share.md](feature-spec/share.md)
---
## 6. 商业体系(模型参考成熟产品,话术用愈心谷)
详见 Spec[feature-spec/membership.md](feature-spec/membership.md)
### 6.1 深度版成长报告 `[P1]`
- 免费:基础结果 / 基础画像
@@ -160,7 +198,9 @@ MBTI 类 · 沟通方式 · 情感模式 · 成长倾向(首版 ≥1 个热门
### 6.3 `[No]` 首版
干扰广告 · 塔罗商城主路径 · 达人双边冷启动 · 硬件
干扰广告 · **以「塔罗」命名的商城主路径** · 达人双边冷启动 · 硬件
(意象卡片属探索模块 `[P2]`,不是塔罗商城。)
---
@@ -170,9 +210,14 @@ MBTI 类 · 沟通方式 · 情感模式 · 成长倾向(首版 ≥1 个热门
用户档案 · 性格探索 · 个人画像 · 关系理解 · AI 问答 · 成长报告深度版 · 会员系统
### 第二阶段 `[P2]`
**不含** 2.62.8P2 三模块不计入 P1 Complete。
体质/身心探索 · 节气陪伴 · 心情记录 · 成长计划
### 第二阶段 `[P2]` — **合规全量探索进行中**
星象性格 · 身心节律 · 意象卡片 · 节气陪伴深化 · 心情记录 · 成长计划 · 探索三级目录 · 量表矩阵
设计真源:`feature-spec/star-profile.md` · `life-rhythm.md` · `image-card.md` · [P2-BACKLOG.md](feature-spec/P2-BACKLOG.md)
排除不变:消息/达人/UGC 广场/运势 Feed;真支付部署阶段再做。
### 第三阶段 `[P3]`
@@ -194,7 +239,13 @@ MBTI 类 · 沟通方式 · 情感模式 · 成长倾向(首版 ≥1 个热门
| `/relation` | 关系理解 | 双人分析 |
| `/membership` | 成长会员 | 套餐与权益 |
| `/scales/:slug` | 探索测试作答 | |
| `/reports` | 成长报告列表 | |
| `/reports/:id` | 成长报告详情 | |
| `/star` | 星座 | P2 |
| `/synastry` | 合盘 | P2 |
| `/synastry/invite/:token` | 合盘邀请 | P2 |
| `/rhythm` | 身心节律 | P2 draft |
| `/cards` | 意象卡片 | P2 draft |
兼容重定向:`/decode``/portrait`(仅技术兼容,UI 不出现 decode 文案)。
@@ -0,0 +1,236 @@
# P1 全功能流程走查(Feature Spec Layer
**日期:** 2026-08-02
**目的:** 按新链路对 P1 全部功能过一遍——**只做设计与对齐审计,本文件不授权直接开新编码**。
**链路:**
```
feature-map → feature-spec → OpenAPI/erd → implementation → test → review
```
规范:[../feature-design.md](../feature-design.md) · 索引:[README.md](README.md)
图例:
| 标记 | 含义 |
|---|---|
| PASS | 本关已满足,可进入下一关或维持 |
| PARTIAL | 主路径可用,有明确 Gap(见各 Spec §14 |
| SHELL | 有意占位(如陪伴 P1 |
| BLOCK | 缺 Spec/契约,禁止扩行为 |
| N/A | 本关不适用 |
编码优先级:
1. ~~P0 横切:埋点最小集~~**切片 A 已完成**(见 [analytics.md](analytics.md)
2. P1 体验 Gap:探索测试中断恢复
3. P2 探索三模块:见 [P2-BACKLOG.md](P2-BACKLOG.md)(星象性格 → 身心节律 → 意象卡片)
4. 真支付、短链分享、陪伴心情 UI(各改对应 Spec)
其余 Spec §11 事件:后续迭代再挂,勿一次铺满。
---
## 总表
| # | 功能 | Spec | map | OpenAPI/erd | 实现 | 测试 | DoD 标签建议 | 下一步(设计侧已闭合?) |
|---|---|---|---|---|---|---|---|---|
| 1 | 首页 | [home](home.md) | PASS | N/A(复用 profiles | PASS | PARTIAL | Feature Complete(体验) | 埋点;可选推荐位 |
| 2 | 个人档案 | [profile](profile.md) | PASS | PASS | PASS | PARTIAL | Feature Complete | 软删策略写清;埋点 |
| 3 | 个人画像 | [portrait](portrait.md) | PASS | PASS | PASS | PASS | Feature Complete | 埋点;内容已多维 |
| 4 | 关系理解 | [relation](relation.md) | PASS | PASS | PASS | PASS | Feature Complete | 埋点 |
| 5 | 探索测试 | [explore-test](explore-test.md) | PASS | PASS | PASS | PARTIAL | Feature Complete Gap | **设计已标**中断恢复 Gap |
| 6 | AI 问答 | [ask](ask.md) | PASS | PASS | PASS | PASS | Feature Complete | 埋点;真 LLM 依赖配置 |
| 7 | 会员/深度版 | [membership](membership.md) | PASS | PASS | PASS | PASS | Feature Completemock | 真支付另开 Spec 修订 |
| 8 | 成长报告 | [reports](reports.md) | PASS | PASS | PASS | PARTIAL | Feature Complete | 收藏/PDF = Gap |
| 9 | 分享卡 | [share](share.md) | PASS | N/A(前端载荷) | PASS | PARTIAL | Feature Complete Gap | 短链 = Gap |
| 10 | 陪伴 | [companion](companion.md) | PASS | PARTIAL | SHELL | N/A 深测 | **Shell only** | 禁止当 P1 Complete |
**横切:** 埋点最小集已实现(analytics)· 真支付仍为 mockP1 接受)· Visitor 模型与 Spec 一致。
**收口标签:** 见 [p1-status.md](../p1-status.md) → **P1 Complete**
---
## 逐功能走查(六步)
每步问题:① Spec 是否 Active 且 §4/7/9/12 非空?② map/page-tree 对齐?③ OpenAPI/erd?④ 实现是否覆盖主路径与四态?⑤ 测试是否覆盖 §12?⑥ Review/Gaps 是否写入 §14
---
### 1. 首页 · home
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | Purpose/Flow/Rules 齐全 |
| map / page-tree | PASS | `/` |
| 契约 | N/A | 无专用 API |
| 实现 | PASS | 生日 CTA → portrait;快捷入口;无 API 状态墙 |
| 测试 | PARTIAL | 缺独立 Home e2e;主路径被 portrait e2e 间接覆盖 |
| Review | PASS | Gap:埋点、推荐 Feed 静态 |
**流程结论:** 设计闭合。编码仅在做埋点/推荐时先改 Spec §11/§5。
---
### 2. 个人档案 · profile
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | |
| map / page-tree | PASS | `/profile` · `/mine` 入口 |
| 契约 | PASS | GET/POST/PATCH/DELETE profiles |
| 实现 | PASS | 列表/编辑/删除/加 TA |
| 测试 | PARTIAL | L2 有 profile 更新删除流 |
| Review | PASS | Gapbirth_time/place UI;删除级联说明 |
**流程结论:** 设计闭合。扩字段前先改 Spec §8 + erd。
---
### 3. 个人画像 · portrait
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | 含多维报告结构附录 |
| map / page-tree | PASS | `/portrait` |
| 契约 | PASS | POST portrait · GET report · orders |
| 实现 | PASS | 引擎多维 + ReportRich + DeepAccess |
| 测试 | PASS | engine L1 · 集成 deep_access · PortraitPage.spec |
| Review | PASS | Gap:埋点;真支付 |
**流程结论:** 设计闭合。内容结构变更必须先改 Spec 附录再改 `internal/portrait`
---
### 4. 关系理解 · relation
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | 双引擎、禁用合盘 |
| map / page-tree | PASS | `/relation` |
| 契约 | PASS | POST insight |
| 实现 | PASS | 多维对比 + 深度 sections |
| 测试 | PASS | engine L1 · 集成 · RelationPage.spec |
| Review | PASS | Gap:埋点 |
**流程结论:** 设计闭合。
---
### 5. 探索测试 · explore-test
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | 子能力表已展开 |
| map / page-tree | PASS | `/explore` `/scales/:slug` |
| 契约 | PASS | scales list/detail/result |
| 实现 | PASS | 两套 seed;丰富 result;无结果付费墙(Spec 已声明) |
| 测试 | PARTIAL | score/result L1;缺完整答题 e2e |
| Review | PASS | **Gap(已设计):** 答题中断恢复;埋点;若上锁结果须先改 Spec |
**流程结论:** 设计闭合;中断恢复属已登记 Gap,开工前在 Spec §4/§7 补规则细节即可编码。
---
### 6. AI 问答 · ask
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | 配额、降级、挂档案 |
| map / page-tree | PASS | `/ask` |
| 契约 | PASS | quota/threads/messages |
| 实现 | PASS | DeepSeek + 规则降级;四态较全 |
| 测试 | PASS | reply L1 · 集成 thread |
| Review | PASS | Gap:埋点;长期记忆 P3 |
**流程结论:** 设计闭合。改配额数字或 system prompt 结构 → 先改 Spec §7/§14。
---
### 7. 深度版 / 成长会员 · membership
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | DeepAccess Membership |
| map / page-tree | PASS | `/membership` + 报告内墙 |
| 契约 | PASS | membership/me · orders · pay-mock |
| 实现 | PASS | mock 支付落库;服务端裁剪 detail |
| 测试 | PASS | L2 unlock |
| Review | PASS | Gap:真支付网关(须 Spec 修订 §9) |
**流程结论:** mock 商业闭环设计闭合。真支付 = 新一轮 Spec→契约→实现。
---
### 8. 成长报告 · reports
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | |
| map / page-tree | PASS | `/reports` + `/reports/:id`page-tree 已对齐) |
| 契约 | PASS | GET list/detail |
| 实现 | PASS | ReportsPage · ReportPage · Mine 入口 |
| 测试 | PARTIAL | 详情流较强;列表专项可补 |
| Review | PASS | Gap:收藏、PDF |
**流程结论:** 设计闭合。
---
### 9. 分享卡 · share
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | 前端 query 载荷 |
| map / page-tree | PASS | `/share` |
| 契约 | N/A | 无专用 APISpec 已写) |
| 实现 | PASS | ShareSheet/Card/Page |
| 测试 | PARTIAL | 有分享相关测 |
| Review | PASS | Gap:短链服务、系统分享 |
**流程结论:** P1 设计闭合。短链需新 API → 先改 Spec §8/§9。
---
### 10. 陪伴 · companion
| 步 | 结果 | 笔记 |
|---|---|---|
| Spec | PASS | **明确 P1 Shell / P2** |
| map / page-tree | PASS | `/companion` |
| 契约 | PARTIAL | solar-terms + moods API 有;UI 未接 moods |
| 实现 | SHELL | 本地节气;心情文案占位 |
| 测试 | N/A | 壳不要求深测 |
| Review | PASS | **禁止**未改 Phase 就做心情 OS |
**流程结论:** 壳范围设计闭合。P2 开工 = 升 Spec Phase + 补 §4/§7/§12 后再编码。
---
## 流程门禁抽查(本轮)
- [x] 无 Spec 的 P1 功能:无
- [x] Spec 均含 §4 / §7 / §9 / §12
- [x] feature-map 已链到 Spec
- [x] 陪伴未误标为 P1 Feature Complete
- [x] 横切埋点统一登记为 Gap,未假装已完成
- [x] **本轮不编码**——仅文档与 page-tree 对齐
---
## 批准编码时的推荐切片(需你点名后再动)
| 切片 | 先改 Spec | 再改代码 |
|---|---|---|
| A. 埋点最小集 | 各 Spec §11 选 5 个核心事件 | H5 统一 `track()` + GA |
| B. 探索测试草稿恢复 | explore-test §4/§7 | localStorage 恢复答案 |
| C. 真支付 | membership §9/§7 | 支付适配器 + 回调 |
| D. 陪伴 P2 | companion Phase→P2 | moods UI |
---
## 签字栏(人工)
| 角色 | 确认 | 日期 |
|---|---|---|
| 产品/你 | 走查结论是否接受 | |
| 开发开工 | 从切片 A/B/C/D 择一 | |
+31
View File
@@ -0,0 +1,31 @@
# P2 Backlog — 探索三模块 + 合规全量目录
**状态:** 合规全量探索进行中(Wave 0–4)
**命名决策:** 星座 / 身心节律 / 意象卡片;UI 无「塔罗」主入口、无「算命/占卜」恐吓
**契约:** [../lexicon.md](../lexicon.md) · [../feature-map.md](../feature-map.md) · [../STRATEGY.md](../STRATEGY.md)
真支付:部署阶段。排除:消息 / 达人 / UGC 广场。星座模块内允许日周月年运(非独立运势 Feed)。
---
## 模块 ↔ Spec
| 顺序 | 模块 | Spec | 路由 | 状态 |
|---|---|---|---|---|
| 0 | 探索目录 | — | `/explore` · `/explore/:category` | Done |
| 1 | 星座 | [star-profile.md](star-profile.md) | `/star` | Done(星盘/运势/合盘对齐中) |
| 2 | 身心节律 | [life-rhythm.md](life-rhythm.md) | `/rhythm` | Done(今日/本周) |
| 3 | 意象卡片 | [image-card.md](image-card.md) | `/cards` | Done(≥78 牌 · ≥8 场景) |
| 4 | 量表矩阵 | [explore-test.md](explore-test.md) | `/scales/*` | Done(≥8 |
| 5 | 成长计划 | companion | `/growth-plan` | Done |
| 6 | 心情轨迹 | companion | `/companion` | Done |
---
## 签字栏
| 项 | 状态 |
|---|---|
| 设计三 Spec | Done |
| 探索 catalog API | Done |
| 真支付 | 部署阶段 |
+27
View File
@@ -0,0 +1,27 @@
# Feature Spec 索引
规范:[../feature-design.md](../feature-design.md) · 模板:[_TEMPLATE.md](_TEMPLATE.md)
| Spec | 功能 | feature-map | 主要路由 | Phase |
|---|---|---|---|---|
| [home.md](home.md) | 首页 / 发现入口 | §1 首页 | `/` | P1 |
| [profile.md](profile.md) | 个人档案 | §1.1 · §5.1 | `/profile` | P1 |
| [portrait.md](portrait.md) | 个人画像 | §2.1 · §2.3 | `/portrait` | P1 |
| [relation.md](relation.md) | 关系理解 | §2.4 | `/relation` | P1 |
| [explore-test.md](explore-test.md) | 探索测试 | §2.2 | `/explore` · `/scales/:slug` | P1 |
| [ask.md](ask.md) | AI 成长助手 | §3 | `/ask` | P1 |
| [membership.md](membership.md) | 深度版 / 成长会员 | §6 · §5.3 | `/membership` | P1 |
| [reports.md](reports.md) | 成长报告列表/详情 | §2.1.2 · §5.2 | `/reports` · `/reports/:id` | P1 |
| [share.md](share.md) | 分享卡 | journey 双引擎 | `/share` | P1 |
| [companion.md](companion.md) | 陪伴(节气壳) | §4 | `/companion` | P1 壳 / P2 |
| [analytics.md](analytics.md) | 增长埋点最小集 | 横切 | H5 `track` | P1 |
| [star-profile.md](star-profile.md) | 星象性格 | §2.6 | `/star` | P2 设计 |
| [life-rhythm.md](life-rhythm.md) | 身心节律 | §2.7 | `/rhythm` | P2 设计 |
| [image-card.md](image-card.md) | 意象卡片 | §2.8 | `/cards` | P2 设计 |
新功能:复制 `_TEMPLATE.md` → 填满 → 在本表登记 → 再编码。
**P1 全量流程走查:** [P1-PROCESS-REVIEW.md](P1-PROCESS-REVIEW.md)
**P2 三模块队列(设计立项 / 编码另批):** [P2-BACKLOG.md](P2-BACKLOG.md)
**竞品逆向(测测前端全量):** [cece-frontend-re/](cece-frontend-re/README.md) · **完整设计包:** [cece-frontend-re/complete-design/](cece-frontend-re/complete-design/README.md) · 方法见 [../../design/reverse-engineering-spec.md](../../design/reverse-engineering-spec.md)
+203
View File
@@ -0,0 +1,203 @@
# Feature Spec: \<Name\>
> 复制本文件为 `<id>.md`。规范:[../feature-design.md](../feature-design.md)
> 语言:[../lexicon.md](../lexicon.md) · 能力树:[../feature-map.md](../feature-map.md)
> Status: `Draft` | `Active` | `Deprecated`
> Map node: (如 `2.3 个人画像 [P1]`
> Phase: `P1` | `P2` | `P3`
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | |
| Purpose | |
| Business Goal | |
**In Scope / Out of Scope**(防止偷偷扩 scope
| In | Out |
|---|---|
| | |
---
## 2. 用户价值
必须回答:
1. 用户为什么需要它?
2. 用户完成后获得什么?
3. 为什么愿意付费?(若无付费面,写 N/A)
---
## 3. 用户角色
对齐当前 IdentityVisitor 设备身份 + 可选升级)。写清**实际能做什么**,勿照搬竞品。
| Actor | 说明 | 本功能能力 |
|---|---|---|
| Guest / Visitor | 未登录设备身份 | |
| User | 已有持久用户/档案 | |
| VIP | 成长会员 active | |
| DeepAccess | 单份报告深度版已购 | |
---
## 4. 用户流程
```text
进入
```
或 mermaid。必须含:无数据分支、权益分支、失败分支(可简写并在 §6 展开)。
---
## 5. 页面设计
对照 [../page-tree.md](../page-tree.md)。
| 路由 | 页面 | 说明 |
|---|---|---|
| | | |
页面结构示例:
```text
/path
├── Empty State
├── Loading State
├── Result / Normal State
└── Locked State
```
---
## 6. 页面状态规范
| 状态 | 说明 | UI 行为 |
|---|---|---|
| Loading | 请求中 | |
| Empty | 无数据 | 引导去哪 |
| Error | 失败 | 重试/返回 |
| Normal | 成功 | |
| Locked | 付费/权益限制 | CTA |
---
## 7. Business Rules
编号规则;**权益与归属必须在 Server 判定**。
| ID | Rule |
|---|---|
| R1 | |
| R2 | |
---
## 8. 数据模型影响
指向 [../../domain/erd.md](../../domain/erd.md)。新增/变更表字段列表;无则写「无 schema 变更」。
| 表 | 变更 | 备注 |
|---|---|---|
| | | |
---
## 9. API 需求
路径级意图即可;字段细节以 `proto/openapi.yaml` 为准,**实现时必须同步 OpenAPI**。
| Method | Path | 意图 |
|---|---|---|
| | | |
Response 权益意图示例:`summary` 始终按规则返回;`detail` 无权益时剥离或 null。
---
## 10. 权限设计
| 能力 | Visitor | User | DeepAccess | VIP |
|---|---|---|---|---|
| 查看基础 | | | | |
| 查看完整 | | | | |
| 写入/生成 | | | | |
---
## 11. 埋点
未接分析 SDK 也先定义事件名。
| Event | 触发 |
|---|---|
| `<feature>_started` | |
| `<feature>_completed` | |
| `deep_access_clicked` | |
| `purchase_completed` | |
---
## 12. 测试验收标准
### 主路径
**Given**
**When**
**Then**
### 权益 / 异常
**Given**
**When**
**Then**
---
## 13. AI 开发前检查
- [ ] Spec 本章齐全(无空关键节)
- [ ] feature-map 已挂树
- [ ] lexicon 文案已核对
- [ ] OpenAPI / erd 更新计划已明确
- [ ] §12 可转化为 L1/L2/E2E 用例
---
## 14. Implementation Notes
编码时填写(首次实现或重大变更):
| 项 | 内容 |
|---|---|
| Packages | `apps/api/internal/...` · `apps/user-h5/src/...` |
| Migrations | |
| Gaps vs Spec | `Gap:` 未实现项) |
| Tests | L1 / L2 / e2e 路径 |
---
## 附录:子能力展开检查(复杂功能必填)
防止只写「入口 → 结果 → 分享」。按需勾选/展开:
- [ ] 入口与列表
- [ ] 主操作流程(逐步)
- [ ] 中断 / 恢复 / 草稿
- [ ] 结果生成(免费层)
- [ ] 深度层 / 付费墙
- [ ] 分享
- [ ] AI 解释 / 追问入口
- [ ] 数据统计 / 埋点
- [ ] 验收标准覆盖上述每一项
示例(探索测试):测试入口 · 列表 · 题目流程 · 答题保存 · 中断恢复 · 结果生成 · 免费层 · 深度层 · 分享 · AI 解释 · 统计 · 验收。
+148
View File
@@ -0,0 +1,148 @@
# Feature Spec: 增长埋点(P1 最小集)
> Status: `Active` · Map: 横切增长 · Phase: `P1`(切片 A
> 规范:[../feature-design.md](../feature-design.md) · 走查:[P1-PROCESS-REVIEW.md](P1-PROCESS-REVIEW.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 增长埋点最小集 |
| Purpose | 用统一 `track()` 采集主漏斗关键节点,支撑转化分析 |
| Business Goal | 能量化:建档探索 → 深度版/会员;不阻塞主路径 |
| In | Out |
|---|---|
| H5 `track` + GA4(可配 Measurement ID | 服务端埋点中台 |
| 下文 **5 个核心事件** + 自动 `page_view` | 全量 Spec §11 一次接完 |
| DEV 可 debug 日志 | 采集生日/答题全文等 PII |
---
## 2. 用户价值
1. 用户无感(不改变主流程)。
2. 产品可看漏斗掉点。
3. 付费:N/A。
---
## 3. 用户角色
全部 Visitor/User 路径均可触发;不因埋点失败阻断功能。
---
## 4. 用户流程
```text
App 启动 → 若配置了 GA ID 则注入 gtag
路由变化 → page_view
主漏斗节点 → track(核心事件)
gtag 不可用 / 未配置 → no-op(可选 console debug
```
---
## 5. 页面设计
无独立页面。触点:`/` · `/portrait` · `/relation` · 深度版 CTA · pay-mock 成功。
---
## 6. 页面状态规范
N/A(横切)。要求:埋点失败不影响 UI。
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 统一经 `apps/user-h5/src/lib/analytics.ts``track`;页面禁止直接调 `window.gtag` |
| R2 | **P1 核心事件仅下表 5 个**(另加自动 `page_view`);其它 Spec §11 事件名保留,后续迭代再挂 |
| R3 | 禁止把生日、问答正文、档案 ID 明文作为事件参数(可用 `report_type` 等枚举) |
| R4 | `VITE_GA_MEASUREMENT_ID` 为空时 track 为 no-op,应用仍可用 |
| R5 | 事件名稳定,snake_case,与下表一致 |
### P1 核心事件字典(冻结)
| Event | 触发 | 可选 params |
|---|---|---|
| `home_cta_portrait` | 首页点「开始」且生日校验通过,即将进画像 | — |
| `portrait_completed` | 画像基础结果首次展示成功 | `source`: query\|report_id |
| `deep_access_clicked` | 用户点击深度版 CTA(画像/关系/报告) | `surface`: portrait\|relation\|report |
| `purchase_completed` | mock 支付成功且刷新后权益生效 | `kind`: deep_access\|membership |
| `relation_completed` | 关系理解基础结果展示成功 | — |
基础设施:
| Event | 触发 | params |
|---|---|---|
| `page_view` | 路由 afterEach | `page_path`, `page_title` |
---
## 8. 数据模型影响
无 DB。配置:`VITE_GA_MEASUREMENT_ID`H5)。
---
## 9. API 需求
无后端 API。
---
## 10. 权限设计
埋点不鉴权;不依赖登录态字段。
---
## 11. 埋点
见 §7 字典(本 Spec 即权威事件表)。
---
## 12. 测试验收标准
**Given** 未配置 GA ID
**When** 触发 `track('portrait_completed')`
**Then** 不抛错、不挡 UI
**Given** mock `window.gtag`
**When** `track('home_cta_portrait')`
**Then** gtag 被以 event 名调用
**Given** 用户完成画像基础结果
**When** 页面 Normal
**Then** 曾触发 `portrait_completed`(单测或集成对 track mock
---
## 13. AI 开发前检查
- [x] Spec 齐全
- [x] 仅 5 核心 + page_view
- [x] 无 PII 参数
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `src/lib/analytics.ts` · `main.ts` · Home/Portrait/Relation/Report/Membership |
| Config | `VITE_GA_MEASUREMENT_ID` · `VITE_ANALYTICS_DEBUG``apps/user-h5/.env.example` |
| Legacy | 静态站曾用 `G-LVVXH3TL04`,可写入 `.env.local` |
| Tests | `analytics.spec.ts` |
| Process Review | 切片 A **已实现**2026-08-02 |
+172
View File
@@ -0,0 +1,172 @@
# Feature Spec: AI 成长助手(问答)
> Status: `Active` · Map: `§3 问答 [P1]` · Phase: `P1`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | AI 成长助手 |
| Purpose | 结合个人档案,用对话帮助认识自己、理解关系、整理情绪与生活节奏 |
| Business Goal | 战略中心 Tab 留存;配额驱动会员 |
| In | Out |
|---|---|
| 挂 profile 的多轮对话;场景入口 | 占卜/运势/预测未来 |
| DeepSeek(可配)+ 规则引擎降级 | 医疗诊断 |
| 免费次数 + 会员配额 | 无档案空聊(禁止) |
---
## 2. 用户价值
1. **为何需要:** 看完画像仍有具体情境问题。
2. **完成后获得:** 结合档案的结构化建议与可执行小步骤。
3. **为何付费:** 免费次数用尽后会员提供更多回复。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| Visitor | 有档案则可问;受配额限制 |
| VIP | 额外 Ask 配额 |
| 无档案 | 仅引导建档,不可消耗成功回复 |
---
## 4. 用户流程
```text
进入 /ask
无档案? → Empty 引导首页/档案
选择档案(我 / TA+ 可选场景
输入问题 → 检查配额
├─ 耗尽 → 引导会员
└─ 有余 → 创建/续 thread → assistant 回复
DeepSeek 失败/无 key → 规则引擎降级
```
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/ask` | AskPageChatThread |
```text
/ask
├── Empty(无档案)
├── Loading(发送中)
├── Normal(历史 + 输入)
├── Error(发送失败可重试)
└── Quota Exhausted(引导会员)
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Empty | 无档案引导 |
| Loading | 发送中禁用重复点 |
| Error | 失败 + 重试 |
| Normal | 消息列表 |
| Quota | 明确耗尽文案 + `/membership` |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 每条用户消息必须绑定 `profile_id`self 或 other |
| R2 | 免费回复次数有上限(实现:如 3);会员另计配额 |
| R3 | 配额耗尽返回明确业务错误,不生成付费假回复 |
| R4 | LLM 可选;失败降级规则引擎,仍须 lexicon 安全 |
| R5 | 禁止运势/吉凶/算命/合盘话术;结尾免责声明 |
| R6 | 回复宜有结构:回应用户 → 结合档案 → 可执行建议(详见 system prompt |
---
## 8. 数据模型影响
| 表 | 备注 |
|---|---|
| `ask_threads` | user_id, profile_id |
| `ask_messages` | role, content |
| `ask_quotas` / 等价 | 余量 |
---
## 9. API 需求
| Method | Path | 意图 |
|---|---|---|
| GET | `/api/v1/ask/quota` | 余量 |
| POST | `/api/v1/ask/threads` | 创建线程(profile_id, scene? |
| POST | `/api/v1/ask/threads/{id}/messages` | 发消息拿回复 |
---
## 10. 权限设计
| 能力 | 无档案 | 有配额 | 配额耗尽 | VIP 有配额 |
|---|---|---|---|---|
| 提问 | ✗ | ✓ | ✗ | ✓ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `ask_opened` | 进入页 |
| `ask_message_sent` | 发送 |
| `ask_reply_received` | 成功回复 |
| `ask_quota_exhausted` | 耗尽 |
| `ask_fallback_rule` | 走规则引擎 |
---
## 12. 测试验收标准
**Given** 有 Self 档案与配额
**When** 发送问题
**Then** 返回助手消息且含免责;配额减一
**Given** 配额为 0
**When** 再发送
**Then** 业务错误引导会员,无助手胡编
**Given** 无 DeepSeek key
**When** 提问
**Then** 规则引擎仍给出档案相关回复
---
## 13. AI 开发前检查
- [x] Spec 齐全 · map · lexicon · OpenAPI · 可测
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `service/ask` · `internal/ask` · `internal/llm/deepseek` · `AskPage` |
| Config | `config.local.yaml` deepseek.* |
| Gaps | 长期记忆 P3;顾问预约仅占位;埋点未接 |
| Tests | ask reply L1;集成 threadAskPage.spec |
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · 设计闭合 · 实现 PASS · 测试 PASS |
@@ -0,0 +1,250 @@
# 00 · 测测前端总览(L0 IA · 全功能清单)
> Evidence: 以 E1 实测为主 · E2/E3 补充 · E6/E7 标明推断
---
## STEP 1 — 全局页面组成(App Shell
### 1.1 底部导航(五 Tab)✅ E1
| Tab | 视觉权重 | 入口职责 |
|---|---|---|
| 首页 | 标准 | 发现 · 工具宫格 · 信息流 · 建档引导 |
| 消息 | 标准 | AI/达人/社区/系统/商业通知 |
| **问** | **中央突出 + AI 标识** | 战略入口 → 测测 AI(内含真人 1v1 Tab |
| 在线 | 标准 | 达人 Marketplace |
| 我的 | 标准 | 账号 · 档案资产 · 会员订单 |
```text
┌─────────────────────────────┐
│ 页面内容区 │
├──────┬──────┬──────┬────┬───┤
│ 首页 │ 消息 │ 问* │在线│我的│
└──────┴──────┴──▲───┴────┴───┘
│ *中央悬浮突出 + AI 标识
```
### 1.2 全局浮层 / 横切 UI(推导)
| 元素 | 来源 | 说明 |
|---|---|---|
| 登录/绑定弹层 | 🟡 E6 | 游客触达付费/发帖时 |
| 会员购买半屏 | ✅ E1 商业描述 | 首月/包月/季/年 |
| 网络错误 Toast | 🔵 E7 | 全 App |
| 权限(相册/麦克风)系统弹窗 | 🔵 E7 | 沙盘/连麦/OCR |
| 青少年/合规提示 | 🟡 E6 | 泛心理合规 |
| 分享面板(微信/海报) | ✅ E2 裂变叙事 | 测评结果卡 |
| 角标(消息未读) | 🟡 E6 | 消息 Tab |
---
## STEP 2 — 前端功能全清单(Feature Inventory
### A. 导航与账号
| ID | 功能 | 证据 |
|---|---|---|
| NAV-01 | 五 Tab 切换 | ✅ E1 |
| NAV-02 | 「问」战略权重突出 | ✅ E1 |
| ACC-01 | 手机/微信/Apple 登录 | ✅ E1 基础信息 |
| ACC-02 | 游客 → 注册升级 | 🟡 E6 |
| ACC-03 | 账号设置/注销 | 🔵 E7 |
### B. 生命档案(前端核心资产)
| ID | 功能 | 证据 |
|---|---|---|
| PROF-01 | 创建本人档案(生日/时/地/性别) | ✅ E1 |
| PROF-02 | 创建关系档案(伴侣/朋友等) | ✅ E1 |
| PROF-03 | 多档案切换 | ✅ E1(AI 视角选择器) |
| PROF-04 | 首页「输入生日,更加了解 TA」引导 | ✅ E1 |
| PROF-05 | 档案编辑/删除 | 🟡 E6 |
| PROF-06 | 兴趣/状态标签 | 🟡 E4 |
### C. 首页工具宫格(快捷入口)✅ E1
| ID | 入口文案 | 域 |
|---|---|---|
| GRID-01 | I人E人(MBTI | 测评 |
| GRID-02 | 星座 | 星座内容 |
| GRID-03 | 星盘 | 本命/多盘型 |
| GRID-04 | 缘分合盘 | 合盘+指数 |
| GRID-05 | 沙盘 | 3D 心理沙盘 |
| GRID-06 | 陪伴小星 | AI 陪伴子入口 |
| GRID-07 | 商城 | 电商 |
| GRID-08 | 倾诉 | 咨询/AI |
| GRID-09 | AI 玩法广场 | UGC AI 小应用 |
| GRID-10 | 更多 | 收纳扩展 |
「更多」内公开提到(✅ E2):爱情树(依恋类型)、幸运地图、pick 卡等趣味测。
### D. 「问」AI 与双轨
| ID | 功能 | 证据 |
|---|---|---|
| ASK-01 | 测测 AI 对话 | ✅ E1 |
| ASK-02 | 顶栏「测测AI ‖ 真人1v1」 | ✅ E1 |
| ASK-03 | 视角:星盘维度 | ✅ E1 |
| ASK-04 | 对象:自己 / 已建档 TA | ✅ E1 |
| ASK-05 | 深度思考模式开关 | ✅ E1 |
| ASK-06 | 灵魂伴侣子入口(横向滑动) | ✅ E1 |
| ASK-07 | 预置引导问题(星盘向) | ✅ E1 |
| ASK-08 | 下拉历史消息 | ✅ E1 |
| ASK-09 | AI 心情小镇(多虚拟倾诉师) | ✅ E1/E2 |
| ASK-10 | 灵犀/小智(子场景/模式,非并列 Tab) | ✅ E1 纠正 |
### E. 星座 / 星盘 / 合盘 / 运势
| ID | 功能 | 证据 |
|---|---|---|
| AST-01 | 本命星盘展示与档案切换 | ✅ E3 |
| AST-02 | 多盘型(报道称可达约 20 种工具盘) | ✅ E3 |
| AST-03 | 合盘:比较盘/组合盘/时空盘/马克斯盘等 | ✅ E3 |
| AST-04 | 组合推运等推运变体 | ✅ E3 |
| AST-05 | 缘分指数(恋爱等) | ✅ E2 |
| AST-06 | 日/周/月/年运势 | ✅ E3/E4 |
| AST-07 | 附近的人 / 微信好友连线合盘 | 🟡 E3/行业 + 产品常见;【待真机补证】 |
### F. 测评与神秘学工具
| ID | 功能 | 证据 |
|---|---|---|
| TEST-01 | MBTI(修订量表+分享) | ✅ E1/E2 |
| TEST-02 | 双人 MBTI/性格关系 | ✅ E2 |
| TEST-03 | 塔罗抽牌/提问 | ✅ E3 |
| TEST-04 | 八字/紫微/生命数字等 | ✅ E3 |
| TEST-05 | 爱情树/幸运地图/pick 卡 | ✅ E2 |
| TEST-06 | 50+ 心理测试矩阵 | 🟡 E3 口径 |
### G. 沉浸体验
| ID | 功能 | 证据 |
|---|---|---|
| IMM-01 | 3D 心理沙盘 | ✅ E1/E2 |
| IMM-02 | AI 心情小镇(约 6 人设,含「明朗」) | ✅ E1 |
### H. 社区与内容
| ID | 功能 | 证据 |
|---|---|---|
| SOC-01 | 首页信息流(文章/视频/故事/达人) | ✅ E1 |
| SOC-02 | 今日运势/热门测试/专家推荐 | ✅ E4 |
| SOC-03 | AI 玩法广场 UGC 列表(热度/创作者) | ✅ E1 |
| SOC-04 | 赞评关私信 | 🟡 E4 |
### I. 在线达人
| ID | 功能 | 证据 |
|---|---|---|
| EXP-01 | 达人列表/筛选 | ✅ E1 |
| EXP-02 | 文字/语音/连麦咨询 | ✅ E1 |
| EXP-03 | 评价/复购 | 🟡 E6 |
| EXP-04 | 达人约 2.6 万签约口径 | ✅ E1 官方口径 |
### J. 商业化前端
| ID | 功能 | 证据 |
|---|---|---|
| PAY-01 | 会员:首月 9.9 / 续订 25/月;季 58;年 178 | ✅ E1 |
| PAY-02 | 报告解锁 / AI 次数 | ✅ E1 |
| PAY-03 | 咨询下单支付 | ✅ E1 |
| PAY-04 | 商城 | ✅ E1 宫格 |
| PAY-05 | 主动压缩干扰广告 | ✅ E1 |
### K. 增长
| ID | 功能 | 证据 |
|---|---|---|
| GRW-01 | 测评结果分享卡 | ✅ E1/E2 |
| GRW-02 | 邀请好友测 MBTI | ✅ E2 |
| GRW-03 | 签到/积分/勋章 | 🟡 E4 |
---
## STEP 3 — 全局用户流程
### 3.1 新用户冷启动 ✅ E1 场景
```text
打开 App
→(可选登录)首页
→ 点宫格 MBTI / 或看信息流
→ 完成测评 → 结果页 → 分享/付费墙
→ 引导建档(生日)
→ 可进入「问」或合盘
```
### 3.2 关系资产飞轮 ✅ E1
```text
首页「了解 TA」/ AI 对象切换
→ 录入对方生日建关系档案
→ AI 基于 TA 星盘回答 / 合盘
→ 分享结果 → 拉新
```
### 3.3 全局失败路径(必须实现)🔵 E7
| 场景 | 前端表现 |
|---|---|
| 断网/弱网 | 空态 + 重试;消息/AI 发送失败可重发 |
| Token 失效 | 跳登录;草稿保留 |
| 接口超时 | Toast + 重试 |
| 会员限制 | VIPLocked 半屏 |
| 余额不足(咨询) | 充值/支付 |
| 审核失败(UGC) | 说明原因 |
| 数据为空 | Empty 插画 + CTA |
---
## STEP 4 — 全局状态机(App Shell
```mermaid
stateDiagram-v2
[*] --> Init
Init --> Loading: 启动
Loading --> GuestHome: 无登录
Loading --> AuthedHome: 已登录
GuestHome --> LoginRequired: 触达 gated
LoginRequired --> AuthedHome: 登录成功
AuthedHome --> Offline: 断网
Offline --> AuthedHome: 恢复
AuthedHome --> VIPLocked: 点深度权益
VIPLocked --> AuthedHome: 购买或关闭
AuthedHome --> Error: 致命错误
Error --> AuthedHome: 重试
```
页面级状态见各 Tab 文档。
---
## 页面路由树(前端逻辑路由 · 推导)
```text
/ 首页
/message 消息
/ask 问(默认 AI)
/ask?mode=human 真人1v1
/online 在线达人列表
/online/:expertId 达人详情
/consult/:sessionId 咨询会话
/mine 我的
/profile/list 档案列表
/profile/edit/:id 档案编辑
/tools/mbti MBTI
/tools/chart 星盘
/tools/synastry 缘分合盘
/tools/sandbox 沙盘
/tools/plaza AI玩法广场
/tools/plaza/:appId 玩法详情
/tools/tarot 塔罗
/tools/bazi 八字…
/membership 会员
/orders 订单
/shop 商城
/mood-town AI心情小镇
```
路径名为 🔵 工程命名;测测真实 Deep Link 【待真机补证】。
@@ -0,0 +1,118 @@
# 01 · Tab「首页」逆向设计
> Evidence: ✅ E1 实测 · ✅ E2「更多」工具 · 🟡/🔵 补全交互
---
## STEP 1 — UI Analysis
### 区域结构(自上而下)
| # | 区域 | 可见元素 | 证据 |
|---|---|---|---|
| H1 | 顶栏 | 可能含搜索/消息入口;「输入生日,更加了解 TA」引导条 | ✅ E1 |
| H2 | 用户/档案摘要 | 当前本人档案摘要或未建档 CTA | 🟡 E6 |
| H3 | 功能宫格 | I人E人、星座、星盘、缘分合盘、沙盘、陪伴小星、商城、倾诉、AI玩法广场、更多 | ✅ E1 |
| H4 | 运营 Banner | 活动/会员/新品 | 🟡 E6 |
| H5 | 推荐信息流 | 今日运势、热门测试、AI入口、专家推荐、文章/视频/故事/达人内容 | ✅ E1/E4 |
| H6 | 底部 Tab Bar | 全局 | ✅ E1 |
### 宫格 Icon 全集(不得遗漏)✅ E1
1. I人E人(MBTI
2. 星座
3. 星盘
4. 缘分合盘
5. 沙盘
6. 陪伴小星
7. 商城
8. 倾诉
9. AI玩法广场
10. 更多 → 二级页:爱情树、幸运地图、pick卡… ✅ E2
### AI 玩法广场页内元素 ✅ E1
- Slogan:「全民手搓创意——精选应用」
- 列表卡片:应用名、热度(如 SBTI 305.7 万)、创作者名
- 示例:SBTI 测试版、答案之书、三生三世你和 Ta、灵魂伴侣
---
## STEP 2 — Feature Analysis(逐元素)
| 元素 | 作用 | 点击 | 长按/其它 | 权限/VIP | 分享 |
|---|---|---|---|---|---|
| 了解 TA 引导 | 建关系档案 | 进档案创建表 | — | 登录 | 否 |
| MBTI | 拉新测评 | 进量表流程 | — | 部分结果免费 | 结果强分享 |
| 星座 | 内容/运势 | 星座频道 | — | 深度会员 | 文章可分享 |
| 星盘 | 本命排盘 | 星盘页+档案切换 | — | 多盘型/详解会员 | 海报 |
| 缘分合盘 | 双人匹配 | 选 A/B→报告 | — | 指数免费/深文案付费 | 指数卡 |
| 沙盘 | 沉浸投射 | 3D 沙盘 | — | 可能次数限制 | 场景图 |
| 陪伴小星 | AI 陪伴 | 进子场景/问 | — | AI 次数 | 否 |
| 商城 | 电商 | 商品列表 | — | 登录支付 | 商品 |
| 倾诉 | 咨询/AI | 分流问/在线 | — | — | 否 |
| AI玩法广场 | UGC 应用集市 | 列表→应用 | — | 视应用 | 应用结果 |
| 信息流卡片 | 消费/转化 | 详情/达人/测试 | 可能不喜欢 | — | 内容分享 |
| 更多 | 收纳 | 二级宫格 | — | — | — |
---
## STEP 3 — User Flow
### 主流程:测评拉新
```text
首页 → MBTI → 答题 → Loading → 结果
→ 分享 / 解锁完整 / 邀请好友再测
→ 失败:网络错误重试;中途退出保存进度【待真机】
```
### 主流程:建 TA 档案
```text
首页引导 → 填写生日/关系类型 → 保存
→ 可用于合盘 / AI 对象切换
→ 失败:校验生日非法;未登录 → LoginRequired
```
### 主流程:玩法广场
```text
宫格 → 广场列表 → 选应用 → 互动 → 结果 → 热度/分享
→ Empty:无应用;审核中不可见 🔵
```
---
## STEP 4 — 状态机
| 状态 | UI |
|---|---|
| Init/Loading | 骨架屏 |
| Loaded | 宫格+流 |
| EmptyFeed | 流空+推荐宫格 |
| Offline | 顶条提示 |
| LoginRequired | 登录半屏 |
| VIPLocked | 会员半屏 |
| Error | 重试 |
```mermaid
stateDiagram-v2
[*] --> Loading
Loading --> Loaded
Loading --> Error
Loaded --> LoginRequired
Loaded --> VIPLocked
Loaded --> Offline
Offline --> Loaded
Error --> Loading: 重试
```
---
## 页面验收(前端)
- [ ] 宫格 9+更多与 E1 一致(版本差分记日志)
- [ ] 「了解 TA」可达档案创建
- [ ] 玩法广场展示热度与创作者
- [ ] 信息流可下拉刷新 / 分页 🔵
@@ -0,0 +1,55 @@
# 02 · Tab「消息」逆向设计
> Evidence: ✅ E1/E4 分类 · 细交互多为 🟡/🔵(【待真机补证】)
---
## STEP 1 — UI Analysis
| 区域 | 元素 | 证据 |
|---|---|---|
| 顶栏 | 标题「消息」· 可能「一键已读」 | 🟡 E6 |
| 分类 Tab/分组列表 | AI 消息 · 达人消息 · 社区互动 · 系统 · 商业 | ✅ E4 |
| 会话行 | 头像 · 标题 · 摘要 · 时间 · 未读红点 | 🟡 E6 |
| 空态 | 插画 +「去问问 AI」CTA | 🔵 E7 |
| 底栏 | 全局 Tab,消息角标 | 🟡 E6 |
---
## STEP 2 — Feature Analysis
| 类型 | 作用 | 点击 | 删除/免扰 | 登录 |
|---|---|---|---|---|
| AI 消息 | 会话提醒/未读 AI | 进「问」对应线程 | 可删会话 🟡 | 是 |
| 达人消息 | 咨询回复 | 进咨询会话 | 是 | 是 |
| 社区 | 赞评关私信 | 进动态/私信 | 是 | 是 |
| 系统 | 公告/合规 | 详情页 | 通常不可删 | 是 |
| 商业 | 会员到期/订单/活动 | 会员/订单页 | 是 | 是 |
长按:删除会话 / 标记已读 / 免打扰 🟡 E6。
---
## STEP 3 — User Flow
```text
进入消息 → Loading → 列表
→ 点 AI 会话 → /ask 历史
→ 点达人 → 咨询页(可能支付状态校验)
→ 点系统 → H5/原生详情
失败:Token 失效登录;离线缓存只读 🔵
```
---
## STEP 4 — 状态机
Init → Loading → Loaded | Empty | Offline | Error | LoginRequired
未读数:本地角标 ↔ 服务端 sync 🔵。
---
## 后台推导(摘要 · 详见 08)
推送通道、消息模板、免打扰时段、达人回复 SLA 监控 🔵。
@@ -0,0 +1,101 @@
# 03 · Tab「问」逆向设计(测测 AI + 真人 1v1)
> Evidence: ✅ E1 实测最完整 · 核心战略入口
---
## STEP 1 — UI Analysis
### 进入方式
- 底部中央「问」悬浮大按钮 + AI 标识 ✅ E1
### 对话页结构 ✅ E1
| 区域 | 元素 |
|---|---|
| 顶栏 | Tab**测测AI** ‖ **真人1v1** |
| 提示 | 「下拉可浏览历史消息」 |
| 消息区 | 气泡列表 · 引导问题卡片(冷启动) |
| 上下文条(输入框上方) | **星盘**(解读维度)· **对象名**(默认「自己」,可切 TA)· **深度思考** |
| 右侧滑动 | **灵魂伴侣** 子入口 |
| 输入区 | 文本框 · 发送 · 可能语音 🟡 |
| 预置问题示例 | 「什么样的朋友适合我」「下周的财富情况如何」「本月在人际上需要注意什么」等(星盘向) |
### 子场景(非并列 Tab)✅ E1
- 灵魂伴侣:测测 AI 生态内横向入口
- AI 心情小镇:独立沉浸(多从首页/运营进入)· 约 6 位虚拟倾诉师 · OCR/情感识别 · 角色「明朗」等
- 灵犀 / 小智:更接近模式或子场景,**不是**与测测 AI 同级的底部入口
---
## STEP 2 — Feature Analysis
| 元素 | 作用 | 点击后 | VIP/次数 | 其它 |
|---|---|---|---|---|
| 测测AI Tab | 免费/限额 AI | 保持 AI 会话 | AI 次数/会员扩容 | — |
| 真人1v1 Tab | 达人咨询 | 达人列表或推荐咨询 | 付费 | 可从 AI 低摩擦切换 |
| 星盘标签 | 切换解读维度 | 维度面板【待真机】 | — | — |
| 对象名称 | 切换档案 | 档案选择器(自己/TA) | 需已建档 | 长按?【待真机】 |
| 深度思考 | 更高推理模式 | Toggle;可能耗更多额度 | 可能会员 | — |
| 灵魂伴侣 | 子场景 | 进入专项互动 | 视产品 | 可分享 🟡 |
| 引导问题 | 冷启动 | 自动填入并发送 | 计次 | — |
| 发送 | 提交 | Streaming 回复 | 额度不足 → 会员墙 | 防重复点击 🔵 |
| 下拉历史 | 翻旧消息 | 分页加载 | — | ≠ 长期记忆 |
---
## STEP 3 — User Flow
### AI 主路径
```text
点「问」→ Loading 会话
→ 展示引导问题(星盘向)
→ 可选:切换对象为 TA(无档案则引导创建)
→ 可选:开深度思考
→ 发送 → Streaming
→ 可切换「真人1v1」升单
```
### 失败 / 边界
| 场景 | 处理 |
|---|---|
| 无额度 | VIPLocked / 看广告?【待真机】 |
| 敏感内容 | 拒答 + 转介真人/热线 🔵 |
| 超时 | 重试气泡 |
| 对象无星盘数据 | 引导补全出生信息 |
| 中重度心理危机 | 明确边界文案 + 转介 🔵 |
---
## STEP 4 — 状态机
```mermaid
stateDiagram-v2
[*] --> Loading
Loading --> IdleEmpty: 新会话
Loading --> IdleWithHistory: 有历史
IdleEmpty --> Streaming: 发送/点引导
IdleWithHistory --> Streaming
Streaming --> IdleWithHistory: 完成
Streaming --> Failed: 错误
Failed --> Streaming: 重试
IdleWithHistory --> LoginRequired
IdleWithHistory --> VIPLocked: 额度用尽
IdleWithHistory --> HumanRail: 切真人1v1
```
状态枚举:Init · Loading · IdleEmpty · IdleWithHistory · Streaming · Failed · Retrying · Offline · LoginRequired · VIPLocked · HumanRail · PermissionDenied(麦克风)
---
## 差异化要点(产品事实)✅ E1
1. AI **挂星盘档案**,非通用闲聊
2. **对象切换**把「了解自己」扩到「分析关系」
3. AI ‖ 真人 **同页双轨**
已知缺口(公开评价)✅ E1:偏被动触达;跨会话长期记忆不足;引导问题偏运势易被感知为「算命 AI」。
@@ -0,0 +1,57 @@
# 04 · Tab「在线」逆向设计(达人 Marketplace
> Evidence: ✅ E1 双边平台描述 · 列表 UI 细节 🟡【待真机】
---
## STEP 1 — UI Analysis
| 区域 | 元素 | 证据 |
|---|---|---|
| 顶栏 | 搜索达人 · 筛选(领域/价格/好评) | 🟡 E6 |
| 运营位 | 推荐达人 Banner | 🟡 E6 |
| 列表卡片 | 头像 · 昵称 · 领域标签(星座/塔罗/命理/情感)· 评分 · 价格起 · 在线状态 | ✅ E1 类型 + 🟡 UI |
| 详情页 | 简介 · 服务项(文字/语音/连麦)· 评价 · 下单 CTA | ✅ E1 |
| 会话页 | 聊天/通话 UI · 倒计时/计费 | 🟡 E6 |
专家类型 ✅ E1:星座专家 · 塔罗师 · 命理师 · 心理/情感达人。
---
## STEP 2 — Feature Analysis
| 元素 | 点击 | 支付 | 评价 | 举报 |
|---|---|---|---|---|
| 达人卡 | 详情 | — | — | — |
| 文字咨询 | 下单→会话 | 是 | 结束后 | 是 🔵 |
| 语音/连麦 | 权限+下单 | 是 | 是 | 是 🔵 |
| 收藏达人 | Toggle | — | — | — 🟡 |
| 筛选排序 | 刷新列表 | — | — | — |
撮合:推荐排序 · 平台抽成 ✅ E1。
---
## STEP 3 — User Flow
```text
在线 → 浏览/搜索/筛选 → 达人详情
→ 选服务方式 → 创建订单 → 支付
→ 等待接单 → 服务中 → 结束 → 评价 → 复购
失败:支付取消/失败;达人拒单;超时退款;权限拒绝麦克风
```
---
## STEP 4 — 状态机(订单视角)
Created → Paying → Paid → Matched → InService → Completed → Reviewed
旁路:Cancelled · Refunding · Refunded · Dispute
前端页映射:列表 Loaded/Empty · 详情 · Checkout · Waiting · Chatting · Call · Rate。
---
## 达人供给运营(后台推导)🔵
入驻审核 · 培训激励 · 测测达人版工具 · 服务质量监控 · 分佣配置。
@@ -0,0 +1,61 @@
# 05 · Tab「我的」逆向设计
> Evidence: ✅ E1 资产分层 · 菜单项 🟡【待真机补全文案】
---
## STEP 1 — UI Analysis
| 区域 | 元素 | 证据 |
|---|---|---|
| 头部 | 头像 · 昵称 · 会员标识 · 编辑资料 | ✅ E1 账号体系 |
| 资产摘要 | 会员状态 · AI 剩余次数 · 积分/等级 🟡 | ✅ E1 + 🟡 |
| 功能列表 | 生命档案 · 测试/报告库 · AI 对话历史 · 订单 · 会员中心 · 收藏 · 设置 · 客服 | ✅ E1 §5 + 🔵 |
| 其它 | 达人入驻入口? · 关于/协议 | 🟡 E6 |
---
## STEP 2 — Feature Analysis
| 入口 | 作用 | 点击 | 编辑/删除 | VIP |
|---|---|---|---|---|
| 生命档案 | Self/Other 管理 | 列表/编辑 | 是 | — |
| 测试记录 | 历史量表 | 报告详情 | 可能删 | 深度报告 |
| 报告库 | 星盘/合盘/AI 报告 | 打开 | 收藏 | 解锁态 |
| AI 历史 | 会话列表 | 进「问」 | 删会话 | — |
| 会员中心 | 订阅管理 | 购买/管理自动续费 | — | — |
| 订单 | 咨询/商城/解锁 | 详情/退款入口 | — | — |
| 设置 | 账号安全/通知/隐私 | 子页 | 注销 | — |
| 分享邀请 | 裂变 | 邀请码/海报 | — | — 🟡 |
---
## STEP 3 — User Flow
```text
我的 → 点档案 → 编辑生日/关系
我的 → 会员 → 选套餐 → 支付 → 权益生效
我的 → 订单 → 售后/发票【待真机】
失败:登录态丢失;支付中断;注销二次确认
```
---
## STEP 4 — 状态机
GuestMine(引导登录)· Loading · Loaded · VIPActive · VIPExpired · Error
会员角标与首页权益 Tip 联动 🔵。
---
## 会员定价前端展示(必须完整)✅ E1
| 套餐 | 价格 |
|---|---|
| 首月 | ¥9.9 |
| 续订月 | ¥25/月 |
| 连续包季 | ¥58/季(约 ¥19.3/月) |
| 连续包年 | ¥178/年(约 ¥14.8/月) |
权益文案:完整测评报告 · AI 次数扩展 · 专属内容 ·(历史含去广告)✅ E1。
@@ -0,0 +1,104 @@
# 06 · 工具域:星座 · 星盘 · 合盘 · 运势
> Evidence: ✅ E1/E2 入口与合盘指数 · ✅ E3 盘型列表 · 社交合盘 🟡【待真机】
---
## STEP 1 — 页面家族
| 页面 | 主要 UI | 证据 |
|---|---|---|
| 星座频道 | 十二星座列表/今日运势流 | ✅ E4 |
| 本命星盘 | 圆形星盘 · 行星列表 · 宫位 · 档案切换 · 添加档案 | ✅ E3 操作步骤 |
| 盘型选择 | 多盘型工具(报道称对标多软件、约 20 种) | ✅ E3 |
| 运势 | 日/周/月/年(及一生类报告) | ✅ E3 |
| 缘分合盘 | 双人选择 · 指数 · 多盘型 Tab | ✅ E2/E3 |
| 合盘社交 | 附近的人 / 好友连线 | 🟡 E3/行业 |
### 合盘盘型(前端 Tab 级)✅ E3
| 盘型 | 说明(产品侧) |
|---|---|
| 比较盘 | 双盘叠合与跨盘相位 |
| 组合盘 | 中点关系盘 |
| 时空盘 | 时刻/坐标中点再排盘 |
| 马克斯盘(马克思盘) | 时空×本命中点视角 |
| 组合推运等 | 次限/推运变体 |
另:行运盘、三限盘、月亮返照等出现在星盘工具描述中 ✅ E3。
---
## STEP 2 — Feature Analysis
| 元素 | 作用 | 交互 | VIP |
|---|---|---|---|
| 档案切换 | 换本命主体 | 顶栏切换/添加 | — |
| 星盘图 | 可视化 | 点行星看释义 | 详解付费 |
| 相位表 | 关系动力 | 列表/点击 | 完整表会员 |
| 恋爱/友情/婚姻指数 | 缘分量化 | 结果页大数字 | 指数免费/解读付费 🟡 |
| 盘型 Tab | 切换算法视图 | 不重复支付同报告 🟡 | 深文案付费 |
| 推运日期 | 次限 as_of | 日期选择器 | — |
| 分享 | 裂变 | 海报/链接 | — |
| 附近的人 | 匹配合盘对象 | 定位授权 | 隐私开关 🔵 |
---
## STEP 3 — User Flow
### 本命星盘
```text
首页「星盘」→ 默认本人盘
→ 右上切换/添加他人档案(生日等)
→ 返回切换查看
→ 可选盘型/运势/报告解锁
```
### 缘分合盘
```text
首页「缘分合盘」→ 选档案 A/B(或快速建档)
→ 生成指数 + 默认比较盘
→ Tab 切换组合/时空/马克斯/配对/推运
→ 深度解锁 / 分享
```
### 失败
缺出生时 → 上升/宫位降级提示;定位拒绝 → 仅手动选城;支付失败保留摘要。
---
## STEP 4 — 状态机(合盘报告页)
```mermaid
stateDiagram-v2
[*] --> Form
Form --> Computing: 提交
Computing --> SummaryFree: 成功
Computing --> Error
SummaryFree --> DeepLocked
DeepLocked --> DeepOpen: 支付/会员
SummaryFree --> ChartTabSwitch
ChartTabSwitch --> SummaryFree
```
---
## 数据/算法面(前端依赖)
| 输入 | 字段 |
|---|---|
| BirthInfo | date · time · place · lat/lng · tz |
| Chart | planets[] · houses · aspects · asc |
| SynastryReport | indices · charts{} · as_of |
星历与宫位制:生产级需 Swiss Ephemeris 等 🔵;Whole Sign / Placidus 等切换【待真机】。
---
## 愈心谷对齐
已实现方向:`star-profile.md` · Synastry 五主盘+推运 · 附近/邀请。
测测「约 20 种盘」未全部落地处:Spec 中保留完整清单,实现按分期标注「未交付」而非删除。
@@ -0,0 +1,115 @@
# 07 · 工具域:测评 · 塔罗 · 沙盘 · 心情小镇 · 趣味玩法
> Evidence: ✅ E1/E2 · 流程细节 🟡/🔵
---
## A. MBTI / I人E人 ✅ E1/E2
### STEP 12
| UI | 交互 |
|---|---|
| 介绍页 · 开始测 | 进入答题 |
| 题目卡 · 进度条 | 单选;可返回上一题 🟡 |
| 结果页:16 型标签 · 维度图 · 解读 | 分享强引导;邀请好友对照关系类型 ✅ E2 |
| 付费墙 | 完整报告 |
学术背书:心言心理组修订量表、期刊《心理学进展》✅ E1(专业性证据,非 UI)。
### Flow
```text
开始 → N 题 → 提交 → 计分 Loading → 结果 → 分享/解锁/再测/双人对照
```
状态:Answering · Submitting · ResultFree · ResultLocked · ShareSheet。
---
## B. AI 玩法广场 ✅ E1
UGC 小应用集市(非官方智能体矩阵)。
| UI | 说明 |
|---|---|
| 列表 | 名 · 热度 · 创作者 |
| 应用页 | 互动表单/对话 → 结果 |
| 创作入口 | 达人/用户「手搓」【待真机是否 C 端开放】 |
示例应用:SBTI、答案之书、三生三世你和 Ta、灵魂伴侣。
审核:上架前内容审核 🔵;违规下架。
---
## C. 3D 心理沙盘 ✅ E1/E2
| UI | 交互 |
|---|---|
| 3D 场景画布 | 拖放沙具/排序 |
| 主题模板 | 职场焦虑等场景引导 |
| AI 投射分析 | 生成解读 |
| 保存/分享 | 场景快照 |
数据:超 1200 万用户口径 ✅ E1。权限:可能陀螺仪/性能降级 🔵。
---
## D. AI 心情小镇 ✅ E1
| UI | 交互 |
|---|---|
| 角色选择 | ~6 人设(知心姐姐、理性学长、明朗…) |
| 对话/互动 | OCR · 情感识别 |
| 沉浸场景 | 小镇视觉 |
边界:轻度情绪;中重度转真人/专业机构 ✅ E1。
---
## E. 塔罗 ✅ E3
抽牌(单牌/三牌/阵)→ AI/达人解读 → 可提问;可能拍照识牌 🟡。
结果页:牌义 · 组合 · 建议 · 付费扩展。
---
## F. 命理工具箱 ✅ E3
八字(四柱/五行/大运/流年)· 紫微 · 姓名五格 · 二十八宿 · 生命数字。
前端:表单(姓名/生日)→ 盘面/条文 → 解锁。
---
## G. 「更多」趣味测 ✅ E2
| 名称 | 用途 |
|---|---|
| 爱情树 | 依恋类型 |
| 幸运地图 | 幸运/桃花城市等 |
| pick 卡 | 趣味抽取 |
Flow 同轻量测评:介绍 → 互动 → 结果 → 分享。
---
## H. 每日心情 / 打卡 🟡 E1 留存叙述
入口可能在首页或陪伴场景;打卡日历 · 情绪曲线 · 驱动晚间打开(50% 用户 19:00 后 ✅ E1)。
---
## 统一测评状态机
```mermaid
stateDiagram-v2
[*] --> Intro
Intro --> InProgress
InProgress --> Scoring
Scoring --> ResultTeaser
ResultTeaser --> Paywall
Paywall --> ResultFull
ResultTeaser --> Share
Scoring --> Error
```
@@ -0,0 +1,232 @@
# 08 · 横切系统逆向(STEP 518)
> 支撑前端全功能的数据 / API / 后台 / 运营 / 安全推导
> 标记:无公开 UI 的条目多为 🟡/🔵,但**禁止省略整类**
---
## STEP 5 — 数据模型(核心对象)
| 对象 | 关键字段(类型) | 可空 | 默认 | 来源 | 缓存 | 索引 | 证据 |
|---|---|---|---|---|---|---|---|
| User | id UUID PK · phone · wx_openid · status · created_at | 部分 | active | 注册 | Redis session | phone unique | ✅ E1 |
| Profile | id · user_id · relation(self/other) · display_name · birth_date · birth_time · birth_place · geo_* · tags JSON | time/place 可空 | — | 建档 | 本地+服务端 | user_id | ✅ E1 |
| BirthInfo | 可内嵌 Profile | — | tz=CST+8 🟡 | — | — | — | 🟡 |
| AstrologyChart | profile_id · jd · planets JSON · houses JSON · system · version | — | — | 计算服务 | 按 profile+ver | profile_id | 🔵 |
| SynastryReport | id · profile_a · profile_b · indices JSON · charts JSON · as_of | — | — | 合盘 | 报告缓存 | (a,b) | ✅ E3 |
| TestDefinition | slug · title · questions JSON · scoring | — | published | CMS | CDN | slug | ✅ E1 MBTI |
| TestRecord | user_id · test_id · answers · result · share_code | — | — | 提交 | — | user_id | ✅ E2 |
| AIConversation | id · user_id · profile_ctx_id · mode · messages | — | — | 问 | 热数据 | user_id | ✅ E1 |
| AIQuota | user_id · remaining · period | — | 免费额 | 会员 | Redis | user_id | ✅ E1 |
| Expert | id · domains[] · price_text · price_voice · score · online | — | — | 达人平台 | 列表缓存 | domain | ✅ E1 |
| ConsultOrder | id · user_id · expert_id · mode · amount · status | — | created | 下单 | — | status | ✅ E1 |
| Membership | user_id · plan · status · expires_at · auto_renew | — | — | 订阅 | — | expires | ✅ E1 |
| Order | id · kind(membership/unlock/consult/shop) · amount_cents · status | — | — | 支付 | — | user_id | ✅ E1 |
| Notification | id · user_id · channel · type · payload · read_at | — | — | 推送/站内 | — | user_id,read | ✅ E4 |
| PlazaApp | id · creator_id · title · heat · status | — | pending | UGC | 热榜 | heat | ✅ E1 |
| Favorite | user_id · target_type · target_id | — | — | 收藏 | — | uniq | 🟡 |
| Coupon | id · rules JSON | — | — | 运营 | — | — | 🔵 |
| OperationLog | actor · action · meta | — | — | 后台 | 冷存 | time | 🔵 |
| ABAssignment | user_id · exp_key · bucket | — | — | 配置 | Redis | — | 🔵 |
---
## STEP 6 — 数据库设计(表清单)
必表(前端可感知数据):
`users` · `device_identities` · `profiles` · `growth_reports` / `synastry_reports` · `test_definitions` · `test_records` · `ai_conversations` · `ai_messages` · `ai_quotas` · `experts` · `consult_orders` · `memberships` · `orders` · `payments` · `notifications` · `plaza_apps` · `favorites` · `banners` · `feature_flags` · `audit_logs`
每表要求:PK UUID · `created_at`/`updated_at` · 软删 `deleted_at` · 外键 ON DELETE 策略明确 🔵。
合盘邀请示例:`synastry_invites(token unique, host_profile_id, expires_at, report_id)` 🔵(愈心谷已落地相近结构)。
---
## STEP 7 — API Reverse Engineering(可开发级草案)
> Path 为 🔵 工程命名;真实测测 Path【待抓包】
| Method | Path | 权限 | Request | Response 要点 | 错误码 |
|---|---|---|---|---|---|
| POST | /auth/login | 公网 | phone/wx | token · user | 401 |
| GET | /profiles | 登录 | — | items[] | — |
| POST | /profiles | 登录 | birth_* · relation | profile | 400 |
| PATCH | /profiles/:id | 所有者 | patch | profile | 404 |
| POST | /tests/:slug/submit | 登录 | answers | record_teaser · record_id | 429 |
| GET | /reports/:id | 所有者 | — | summary · detail? | 403 detail |
| POST | /astrology/natal | 登录 | profile_id | chart | 400 |
| POST | /astrology/synastry | 登录 | a · b · as_of | report | 400 |
| GET | /astrology/nearby | 登录+geo | lat,lng,r | items | 403 |
| POST | /ai/chat | 登录+额度 | message · profile_ctx · deep | stream SSE | 402 额度 |
| GET | /ai/conversations | 登录 | cursor | list | — |
| GET | /experts | 登录 | filter | list | — |
| POST | /consult/orders | 登录 | expert · mode | order | 402 |
| POST | /consult/orders/:id/pay | 登录 | channel | pay_params | 409 |
| GET | /membership/me | 登录 | — | plan · expires | — |
| POST | /orders | 登录 | kind · plan/report | order_id | — |
| GET | /messages | 登录 | cursor | items · unread | — |
| POST | /messages/:id/read | 登录 | — | ok | — |
| GET | /plaza/apps | 登录 | sort=heat | apps | — |
| POST | /plaza/apps/:id/run | 登录 | input | result | 403 审核 |
RateLimit:AI/支付严格限流;图表计算按用户 QPS 🔵。
Cache:星盘按 profile+ephemeris_ver;广场热榜 Redis ZSETBanner CDN。
Retry:幂等键用于支付与咨询下单。
---
## STEP 8 — 后台运营系统
| 模块 | 能力 | 证据 |
|---|---|---|
| Banner/运营位 | 首页头图、弹窗、闪屏 | 🔵 |
| 推荐位 | 达人/测试/文章排序 | 🟡 |
| 内容审核 | 广场 UGC、社区帖、达人资质 | 🔵 |
| 用户管理 | 封禁、注销、投诉 | 🔵 |
| 订单/退款 | 咨询与会员 | ✅ E1 商业 |
| 会员配置 | 价格、权益、首月优惠 | ✅ E1 |
| 活动配置 | 裂变、优惠券 | 🟡 |
| 推送 | 模板、人群包 | 🔵 |
| 统计看板 | DAU、转化漏斗、AI 次数 | 🔵 |
| AB / 配置中心 | 引导问题文案、价格实验 | 🔵 |
| 达人运营 | 入驻审核、分佣、培训 | ✅ E1 达人版 |
---
## STEP 9 — 权限矩阵
| 角色 | 能 | 不能 |
|---|---|---|
| 游客 | 浏览部分首页、部分测评 | AI 深度、咨询、发帖 |
| 登录用户 | 档案、基础 AI 次数、下单 | 未购深度 detail |
| VIP/会员 | 扩容 AI、完整报告、专属内容 | 后台 |
| SVIP | 🟡 若存在更高等级 | — |
| 达人 | 接单、达人工具 | 改平台分佣 |
| 运营 | CMS、活动、推荐 | 直接改支付流水 |
| 审核员 | 通过/驳回 UGC/达人 | 财务 |
| 客服 | 工单、补发权益 | 删库 |
| 管理员 | 配置与账号 | — |
| 超管 | 全量 | — |
前端:`LoginRequired` · `VIPLocked` · `PermissionDenied`
---
## STEP 10 — 支付系统
状态:Created → Paying → Paid / Failed / Cancelled →(退款)Refunding → Refunded
场景:重复支付防重 · 补单对账 · 咨询未接单自动退 · 风控限额 · 发票 🔵
渠道:IAP / 微信 / 支付宝 🟡【待真机】
前端:收银台 · 结果页 · 订单详情入口(我的)。
---
## STEP 11 — 消息系统
| 通道 | 用途 |
|---|---|
| Push | 达人回复、会员到期 |
| 站内信 | 消息 Tab |
| 短信 | 登录验证码 |
| 邮件 | 可选账单 🔵 |
| 角标 | Tab 未读聚合 |
策略:免打扰时段 · 营销推送开关 · AI 主动触达(公开称尚未强做)✅ E1。
---
## STEP 12 — 埋点
| 事件 | 触发 |
|---|---|
| page_view | 各 Tab/工具 |
| home_grid_click | 宫格 |
| test_start / test_complete / test_share | 测评 |
| ask_send / ask_deep_toggle / ask_switch_profile | 问 |
| ask_switch_human | 转真人 |
| synastry_complete / chart_tab_switch | 合盘 |
| expert_view / consult_pay | 在线 |
| membership_pay | 会员 |
| plaza_app_open | 广场 |
| error_api | 异常 |
漏斗:曝光 → 建档 → AI → 会员 → 咨询。停留时长、次日留存 🔵。
---
## STEP 13 — 配置系统
| 项 | 后台可配 | 写死 | 远程/灰度/AB |
|---|---|---|---|
| 宫格顺序/显隐 | ✓ | — | 灰度 |
| AI 引导问题 | ✓ | — | 按来源 AB ✅ E1 建议 |
| 会员价格 | ✓ | — | AB |
| 星历版本 | — | 逻辑 | 远程 flag |
| 审核词库 | ✓ | — | — |
| 功能开关(沙盘) | ✓ | — | 灰度 |
---
## STEP 14 — 缓存策略
| 层 | 内容 |
|---|---|
| 本地 | 档案草稿、会话草稿、最近报告摘要 |
| Redis | Session、AI 额度、热榜、限流 |
| CDN | 图片、静态运营配置 |
| 计算缓存 | 同档案星盘 TTL |
| 分页 | 信息流 cursor |
---
## STEP 15 — 异常处理(前端必达)
断网 · 弱网 · 超时 · 重复点击(按钮 loading)· Token 失效 · 本地数据损坏清缓存 · 强制升级 · 审核失败文案 · 404 资源 · 支付结果未知查单 · SSE 中断续传 🔵。
---
## STEP 16 — 日志
用户行为(隐私脱敏)· 前端错误 JS · API access · 支付 · 审核 · 运营操作 · 安全(登录失败/风控)🔵。
保留与销毁策略合规 🔵。
---
## STEP 17 — 安全
| 项 | 要求 |
|---|---|
| 权限 | 服务端强制 detail 剥离(不仅藏 UI) |
| 注入/XSS | 内容安全渲染 |
| CSRF/重放 | 支付与关键写接口 |
| 验证码 | 登录/敏感操作 |
| 风控 | 刷单、恶意咨询 |
| 设备 | device_key / 绑定 |
| 隐私 | geo 默认不可见;档案分享最小化 |
| 心理边界 | 危机话术与转介 |
遵守应用商店与网信/心理相关合规 🔵。
---
## STEP 18 — 可扩展性
| 方向 | 策略 |
|---|---|
| 新盘型 | charts 插件注册,API 加 mode |
| 新测评 | TestDefinition 配置化 |
| 新 AI 子场景 | Ask 内 slot,避免新 Tab |
| 多端 | 同一 BFF;小程序裁剪 Marketplace |
| API 兼容 | summary schema version 字段 |
| 模块解耦 | 计算服务与业务 API 分离 |
| 硬件(巴布) | 独立产品线,账号体系统一 ✅ E1 |
---
## Completeness 自检(STEP 518
- [x] 数据模型 / 库表 / API 草案
- [x] 后台 / 权限 / 支付 / 消息 / 埋点
- [x] 配置 / 缓存 / 异常 / 日志 / 安全 / 扩展
- [ ] 真实抓包字段级对齐【待补证】
@@ -0,0 +1,90 @@
# 测测 App 前端逆向工程设计文档(V1.0)
> Status: `Active` · Method: [reverse-engineering-spec.md](../../../design/reverse-engineering-spec.md)
> Scope: **前端可见能力 + 为支撑前端所必需的后台/数据/运营推导**
> 目标:Production / Enterprise / Feature Complete 级设计输入(非 Demo
---
## 0. 任务声明
| 项 | 内容 |
|---|---|
| 身份 | Senior Reverse Engineering Architect |
| 目标 App | 测测(CeCe / 测测星盘,心言集团) |
| 方法 | 逆向工程,非「设计一个类似 App」 |
| 禁止 | Demo · MVP 终态 · 「略 / TODO / 以后再做」糊弄 |
**落地到愈心谷时**:能力可完整对标;用户可见命名与禁词服从 [`lexicon.md`](../../lexicon.md);禁止自称「测测」、照搬商标视觉。
---
## 1. 证据来源总表(Evidence First
| ID | 来源 | 类型 | 日期/版本线索 |
|---|---|---|---|
| E1 | [人人都是产品经理 · 测测深度体验报告](https://www.woshipm.com/evaluating/6391148.html) | ✅ UI/行为证据(含实测截图描述) | 2026-05-09 |
| E2 | [中华网 · 测测功能图谱](https://tech.china.com/articles/20250417/202504171661508.html) | ✅ 行为/产品证据 + 🟡 运营口径 | 2025-04 |
| E3 | 应用商店/下载站功能介绍(星盘工具列表等) | ✅ 功能列表证据(版本可能滞后) | ~10.x 描述 |
| E4 | 本仓 [`cece-feature-map.md`](../../cece-feature-map.md) | 🟡 既有归纳 | 持续 |
| E5 | 本仓 [`apps/docs/analysis/cece-teardown.md`](../../../../apps/docs/analysis/cece-teardown.md) | 🟡 映射分析 | 历史 |
| E6 | 行业惯例 / 同类 App | 🟡 推断 | — |
| E7 | 工程完整性补充 | 🔵 工程建议 | — |
| E8 | 安卓模拟器 AVD `YuXinGu_API34` + APK 10.50.0 截图/UI dump | ✅ 真机 UI 证据 | 2026-08-03 |
> **局限**:无未公开 DAU/后台;HTTPS 抓包未做。E8 明细见 [complete-design/evidence-20260803.md](complete-design/evidence-20260803.md)。
---
## 2. 文档地图
| 文档 | 内容 | Workflow 覆盖 |
|---|---|---|
| [00-overview.md](00-overview.md) | L0 IA · 全功能清单 · 全局状态 · 导航 | STEP 1–4 全局 |
| [01-tab-home.md](01-tab-home.md) | 首页(宫格/信息流/建档/玩法广场) | STEP 1–4 |
| [02-tab-message.md](02-tab-message.md) | 消息 | STEP 14 |
| [03-tab-ask.md](03-tab-ask.md) | 「问」测测 AI + 真人 1v1 | STEP 14 |
| [04-tab-online.md](04-tab-online.md) | 在线(达人 Marketplace | STEP 14 |
| [05-tab-mine.md](05-tab-mine.md) | 我的 | STEP 14 |
| [06-tools-astrology.md](06-tools-astrology.md) | 星座 · 星盘 · 合盘 · 运势 | STEP 1–4 + 算法面 |
| [07-tools-tests-psych.md](07-tools-tests-psych.md) | MBTI · 塔罗 · 沙盘 · 心情小镇 · 趣味测 | STEP 1–4 |
| [08-systems.md](08-systems.md) | 数据/库/API/后台/权限/支付/消息/埋点/配置/缓存/异常/日志/安全/扩展 | STEP 518 |
| **[complete-design/](complete-design/README.md)** | **完整产品设计包(Design Complete** — 实现与模拟器校验前的唯一前端设计输入 | 定稿升格 |
---
## 3. Completeness Checklist(套件级)
- [x] 五 Tab 页面拆解
- [x] 首页宫格与「更多」工具域
- [x] AI / 达人双轨
- [x] 星盘与合盘工具面
- [x] 测评 / 沉浸体验
- [x] 横切系统 STEP 518
- [ ] 【待真机补证】逐按钮热区坐标与最新版 UI 差分
- [ ] 【待抓包补证】真实 API Path / 字段级 Response
套件可结束条件:上表「真机/抓包」两项未完成时,仍可作为 **Feature Complete 设计底稿**;实现前须补证据或接受 🟡/🔵 风险。
**完整设计定稿:** [complete-design/](complete-design/README.md) 已将 🟡/🔵 升格为设计定稿(D);真机/模拟器差分按 [99-emulator-validation.md](complete-design/99-emulator-validation.md) 回填。
---
## 4. 愈心谷映射入口
| 测测前端域 | 愈心谷当前落地 | Spec |
|---|---|---|
| 档案 / 了解 TA | Profile | `profile.md` |
| 问 / AI | Ask | `ask.md` |
| 星座 / 星盘 / 合盘 | Star + Synastry | `star-profile.md` |
| 测评 | Explore / Scale | `explore-test.md` |
| 会员 | Membership | `membership.md` |
| 消息 / 在线达人 | 分期后置 | feature-map `[P3]/`/`[No]` |
---
## 5. 维护
- 版本变更:在各文档顶部追加「差分日志」
- 真机补证:把 ✅ 行从 E1 升级为「真机 YYYY-MM-DD + 版本号」
- 与 [`cece-feature-map.md`](../../cece-feature-map.md) 保持 L0 一致;冲突时以本套件 + 更新证据为准
@@ -0,0 +1,150 @@
# 00 · 设计圣经(全局)
> Design ID: `CECE-FE-BIBLE-V1` · 定稿 · 待模拟器校验项见文末
---
## 1. 产品一句话
以趣味测评与星盘档案为入口,以「问」Tab 级 AI(挂档案)为战略核心,以达人咨询为高客单转化,以会员订阅为主营收的泛心理情感陪伴 App。
---
## 2. 信息架构(冻结)
### 2.1 底部五 TabD 定稿)
| 序 | Tab | 路由 key | 视觉(✅ E8 10.50.0 | 战略 |
|---|---|---|---|---|
| 1 | 首页 | `home` | 标准 | 发现与工具分发 |
| 2 | 消息 | `message` | 标准 + 未读数字角标 | 触达与会话入口 |
| 3 | **问** | `ask` | **中央大圆突出 + AI 角标** | 唯一 Tab 级官方 AI |
| 4 | 在线 | `online` | 标准 + 运营角标「限免」(可配) | 达人 Marketplace |
| 5 | 我的 | `mine` | 标准;游客文案「未登录」 | 资产与商业 |
规则:
- 禁止再增加第 6 个主 Tab。
- 灵犀/小智/灵魂伴侣/心情小镇 **不得**升为底部 Tab;一律挂「问」或首页宫格。
- 「问」内顶栏固定双轨:`测测AI` | `真人1v1`
### 2.2 全局导航栈
```text
RootTabNavigator
├── HomeStack
├── MessageStack
├── AskStack ← 中央按钮切到此栈
├── OnlineStack
└── MineStack
Modal 层:Login · MembershipPaywall · ShareSheet · SystemPermission · ForceUpdate
```
---
## 3. 全局组件目录(前端必须实现)
| 组件 | 职责 | 出现位置 |
|---|---|---|
| `AppTabBar` | 五 Tab;问中间凸起 | 全局 |
| `ProfileSwitcher` | 自己/TA 档案切换 | 问、星盘、合盘 |
| `BirthArchiveForm` | 生日/时/地/关系类型 | 建档、了解TA |
| `PaywallSheet` | 会员/单次解锁 | 报告、AI 额度 |
| `SharePosterSheet` | 结果卡分享 | 测评、合盘、星盘 |
| `NetworkBanner` | 离线/弱网 | 全局 |
| `EmptyState` | 空列表 CTA | 消息、订单、档案 |
| `ErrorRetry` | 失败重试 | 全局 |
| `StreamingBubble` | AI 流式回复 | 问 |
| `ExpertCard` | 达人列表项 | 在线 |
| `ChartWheel` | 圆形星盘 | 星盘/合盘 |
| `IndexScoreRow` | 恋爱/友情/婚姻指数 | 合盘 |
| `GridIcon` | 首页宫格 | 首页 |
| `FeedCard` | 信息流卡片 | 首页 |
| `PlazaAppCard` | 玩法广场项 | 广场 |
| `UnreadBadge` | 角标 | 消息 Tab |
---
## 4. 全局状态机(App
```mermaid
stateDiagram-v2
[*] --> ColdStart
ColdStart --> Bootstrap
Bootstrap --> GuestSession
Bootstrap --> AuthedSession
GuestSession --> AuthModal: gated action
AuthModal --> AuthedSession: success
AuthedSession --> Offline
Offline --> AuthedSession
AuthedSession --> ForceUpdate: version low
AuthedSession --> Paywall: vip gate
Paywall --> AuthedSession
```
全局枚举:`ColdStart` · `Bootstrap` · `GuestSession` · `AuthedSession` · `AuthModal` · `Offline` · `ForceUpdate` · `Paywall` · `FatalError`
---
## 5. 设计令牌(结构约定 · 非抄视觉品牌)
> 模拟器采集后可替换为实测色值;此处只定 **语义角色**
| Token | 角色 |
|---|---|
| `--color-brand` | 主操作/问 Tab 强调 |
| `--color-ai` | AI 标识色 |
| `--color-vip` | 会员金色系 |
| `--color-danger` | 错误/删除 |
| `--color-surface` | 卡片底 |
| `--color-feed-bg` | 信息流底 |
| `--radius-card` | 1216 |
| `--tab-ask-size` | 中央按钮直径 > 其它 Tab |
动效:Tab 切换 200ms;AI 流式打字;宫格按下缩放 0.96。
---
## 6. 档案为中心的产品定律(D 定稿)
1. 所有深度解读默认绑定 **当前 Profile**
2. 「问」必须支持切换到 **关系档案 TA**
3. 首页持续引导「了解 TA」建档。
4. 合盘/匹配最少需要 2 份档案(可快速创建)。
5. 无出生时:上升/宫位降级提示,不阻断太阳解读。
---
## 7. 商业门禁(前端表现)
| 能力 | 游客 | 登录免费 | 会员/深购 |
|---|---|---|---|
| 浏览首页/部分测评 | ✓ | ✓ | ✓ |
| 完整测评报告 | teaser | teaser | ✓ |
| AI 对话 | 登录后有限次 | 有限次 | 扩容 |
| 合盘指数 | — | ✓ | ✓ |
| 合盘完整相位/推运文案 | — | teaser | ✓ |
| 达人咨询 | — | 可下单 | 可下单 |
| 玩法广场基础 | ✓ | ✓ | 视应用 |
价格文案(展示用,采集后校验):首月 ¥9.9 · 月 ¥25 · 季 ¥58 · 年 ¥178。
---
## 8. 文案与合规(前端)
- 危机内容:拒答模板 + 热线/转真人。
- 免责:探索/娱乐向声明(具体措辞模拟器采集竞品后对齐,愈心谷落地另走 lexicon)。
- 未成年人:系统账号年龄门 + 功能裁剪(D:咨询与付费加强校验)。
---
## 9. 待模拟器校验清单(圣经级)
| ID | 校验项 |
|---|---|
| V-B01 | 五 Tab 文案与「问」视觉权重 |
| V-B02 | 全局弹层种类(登录/会员/分享) |
| V-B03 | 会员价与权益文案 |
| V-B04 | 主色/AI 色实测 |
| V-B05 | 游客可点范围 |
@@ -0,0 +1,199 @@
# 10 · 首页完整设计
> Route: `/home` · Tab: 首页 · Design Complete
---
## 1. 页面目标
1. 分发工具(宫格)
2. 推动建档(了解 TA
3. 消费信息流 / 转化测评与会员
4. 承接 UGC 玩法广场
---
## 2. UI 完整清单(分区)
### 2.1 顶区 `HomeHeader`(✅ E8
| 元素 ID | 类型 | 文案/内容 | 行为 |
|---|---|---|---|
| H-HDR-CHECKIN | IconBtn | 签到 | → 签到/每日福利 |
| H-HDR-SEARCH | Search | 运营占位轮播(例「锐评十二星座」) | → 全局搜索 |
| H-HDR-PLUS | IconBtn | `+` | → 发布/快捷创建(待子页核对) |
| H-BANNER-TA | 条 | 「输入生日,更加了解TA」 | → 建档 other |
### 2.2 自己卡片 `HomeSelfCard`(✅ E8 · 原「档案条」升格)
| 元素 | 行为 |
|---|---|
| 标题「自己」+「更多」 | 更多 → 档案/运势详情 |
| 今日心情分数 + 短解读 | 点卡片进入心情/运势 |
| 五维条:爱情 / 财富 / 事业 / 学习 / 人际 | 展示型;点击进详情(待核) |
### 2.3 宫格 `HomeToolGrid`(✅ E8 真机 10.50.0 定稿)
横向两行可滑宫格(非 3×3):
| 行 | 序 | key | 标题 | 备注 |
|---|---|---|---|---|
| 1 | 1 | mbti | I人E人 | |
| 1 | 2 | star_sign | 星座 | |
| 1 | 3 | natal | 星盘 | |
| 1 | 4 | bazi | 生辰 | |
| 1 | 5 | synastry | 缘分合盘 | |
| 1 | 6 | ziwei | 紫微 | 滑出可见 |
| 2 | 7 | companion_star | 陪伴小星 | |
| 2 | 8 | confide | 倾诉 | |
| 2 | 9 | wisdom_card | 智慧卡 | |
| 2 | 10 | natal_report | 星盘报告 | 角标 New |
| 2 | 11 | bazi_cal | 生辰历 | |
| 2 | 12 | soulmate | 灵魂伴侣 | 滑出可见 |
宫格下方运营双卡(非第十格):
| key | 标题 | 说明 |
|---|---|---|
| plaza | AI玩法广场 | 大卡 + 运营副文案 |
| shop_promo | 商城运营卡 | 例:水晶手串 |
| ai_disclaimer | 脚注 | 「部分标题由AI生成,仅供参考」 |
「更多」入口:在「自己」卡片标题旁 / 工具二级页(见 `free2/tools/更多.png`);沙盘等未进首屏宫格的工具收入更多页。
### 2.3.1 「更多」页条目(✅ E8 `free2/tools/更多.labels.txt`
分组实机可见(不完全等于 UI 分区文案):
| 类 | 条目 |
|---|---|
| 内容/玩法 | 推荐 · 测试 · 心情小镇 · 幸运地图 · 爱情树 · 聊天分析 · 关系网 · 解读 · 沙盘 · 星骰 · 智慧卡 · 日历 |
| 命理工具 | 星盘 · 星盘报告 · 缘分合盘 · 星座 · 生辰 · 生辰历 · 紫微 · 紫微历 · 星宿 · 政余 · 政余历 · 灵数 · 生肖 · 玛雅图腾 · 人类图 |
| 社交/商业 | 倾诉 · 灵魂伴侣 · 商城 |
### 2.4 运营位 `HomePromo`
- 最多 1 个大 Banner(可关闭,24h 不再展示,本地记录)
- 配置来自后台 `banners`
### 2.5 信息流 `HomeFeed`
| 卡片类型 | 字段 | 点击 |
|---|---|---|
| fortune_today | 标题、摘要、分数 | 运势详情 |
| hot_test | 封面、标题、人数 | 测评 |
| expert | 达人卡 | 达人详情 |
| article | 封面、标题 | 文章 |
| video | 封面 | 播放 |
| plaza_hot | 应用名、热度 | 广场应用 |
| ai_entry | 文案 | → 问 |
交互:下拉刷新 · 上拉分页(cursor)· 负反馈「不感兴趣」(可选)。
### 2.6 「更多」二级页 `ToolsMorePage`
固定收录(可后台排序):爱情树、幸运地图、pick卡、塔罗、八字、紫微、生命数字、姓名分析…
每项:Icon · 标题 · 副标题 · 角标(热/新)。
### 2.7 AI 玩法广场 `PlazaPage`
| 元素 | 说明 |
|---|---|
| 标题区 | Slogan:「全民手搓创意——精选应用」 |
| 排序 | 热度 / 最新 |
| AppCard | 名、热度、创作者、封面 |
| 点击 | → `PlazaAppRuntime` |
| 空态 | 「暂无应用」 |
| 审核中 | 创作者端可见,C 端不可见 |
---
## 3. 交互细则
| 手势 | 对象 | 结果 |
|---|---|---|
| Tap | 宫格 | 路由跳转,埋点 `home_grid_click` |
| Tap | 了解 TA | 打开建档;未登录先 Auth |
| LongPress | 宫格 | D)无菜单,防误触 |
| Pull | Feed | 刷新 |
| Tap | Feed 负反馈 | 减少同类(需登录) |
倾诉点击:弹出 ActionSheet「和 AI 聊」/「找达人」→ ask / online。
---
## 4. 状态机
| 状态 | UI |
|---|---|
| Loading | Header 骨架 + 宫格骨架 + Feed 骨架 |
| Ready | 全量 |
| FeedEmpty | 宫格在,Feed EmptyState |
| Offline | NetworkBanner,缓存宫格可点 |
| Error | 全页 Retry |
| AuthRequired | AuthModal |
```mermaid
stateDiagram-v2
[*] --> Loading
Loading --> Ready
Loading --> Error
Ready --> AuthRequired
Ready --> Offline
Offline --> Ready
Error --> Loading
```
---
## 5. 接口契约(本页)
| 调用时机 | API |
|---|---|
| 进入 | `GET /v1/home/bootstrap` → grid_config · banners · profile_summary |
| Feed | `GET /v1/home/feed?cursor=` |
| 负反馈 | `POST /v1/home/feed/{id}/dismiss` |
| 广场 | `GET /v1/plaza/apps?sort=heat` |
`home/bootstrap` Response(定稿):
```json
{
"profile_summary": {"id":"","name":"","sun_sign":"","has_birth_time":false},
"grid": [{"key":"mbti","title":"I人E人","badge":null,"enabled":true}],
"banners": [{"id":"","image_url":"","link":"","closable":true}],
"ta_guide_visible": true
}
```
---
## 6. 异常
| 场景 | 处理 |
|---|---|
| bootstrap 失败 | 使用本地默认宫格(写死上表 10 项)+ Error toast |
| feed 失败 | 保留宫格,Feed 区 Retry |
| 配置缺宫格 | 合并默认,不丢星盘/合盘/问相关入口 |
---
## 7. 埋点
`home_expose` · `home_grid_click{key}` · `home_ta_guide_click` · `home_feed_click{type}` · `home_feed_refresh` · `plaza_open` · `plaza_app_click`
---
## 8. 模拟器校验 ID
| ID | 项 | 状态 |
|---|---|---|
| V-H01 | 宫格文案与顺序 | ✅ E8 已按上表改稿 |
| V-H02 | 了解TA 文案 | ✅ 「输入生日,更加了解TA」 |
| V-H03 | 广场形态 | ✅ 首页双大卡而非 slogan 条 |
| V-H04 | Feed / 达人卡 | 🟡 采集中 `free/home/*` |
| V-H05 | 更多页条目 | 🟡 采集中 `free/tools/更多*` |
| V-H06 | 自己卡片五维 | ✅ 爱情财富事业学习人际 |
| V-H07 | 门禁弹层 | ✅ 未成年 / 每日领取 |
差分总表:[evidence-20260803.md](evidence-20260803.md)
@@ -0,0 +1,117 @@
# 11 · 消息完整设计
> Route: `/message` · Design Complete
---
## 1. 目标
聚合 AI / 达人 / 社区 / 系统 / 商业通知;驱动回到「问」或咨询会话。
---
## 2. UI 清单
### 2.1 顶栏(✅ E8
| ID | 元素 | 行为 |
|---|---|---|
| M-TITLE | 「消息」 | — |
| M-SEARCH | 搜索图标 | 搜会话/通知 |
| M-CLEAR | 扫帚图标 | 清理/标记已读(无「全部已读」文案) |
### 2.2 列表形态(✅ E8 · 无顶部分段)
**无** `全部|AI|达人|互动|系统` Segment;为**分组会话列表**
| 行类型 | 示例 | 角标 |
|---|---|---|
| 系统通知 | 「检测到新的登录行为…」 | 未读数字 |
| AI 助手 | 「小星」+ AI 角标 | 未读数字 |
| (登录后)达人/互动 | 待更多账号补证 | — |
底部可出系统通知引导条:「开启系统通知…」+「去开启」+ 关闭。
### 2.3 会话行 `MessageRow`
| 字段 | 说明 |
|---|---|
| avatar | 系统铃铛 / 小星 IP / 达人头像 |
| title | 会话名 |
| preview | 最后一条摘要 |
| time | 相对时间(今天时刻 / 昨天) |
| badge | 未读数 |
### 2.4 系统通知详情(✅ E8)
路由级页「系统通知」:时间分隔 + 卡片列表(纯文本 / 链接「点击查看」/ 运营富媒体)。
### 2.5 空态
插画 + 引导回「问」(未在本版截到空态)。
---
## 3. 交互
| 操作 | 结果 |
|---|---|
| Tap 行 | AI→Ask 线程;达人→ConsultSession;互动→动态/私信;系统→详情 H5;商业→会员/订单 |
| LongPress | 置顶 / 免打扰 / 删除 |
| 左滑 | 删除(iOS 风格);Android 长按菜单 |
| 下拉 | 刷新 |
---
## 4. 状态机
`Loading` · `Ready` · `Empty` · `Offline` · `Error` · `AuthRequired`
未读角标:`GET /v1/messages/unread_count` 与 TabBar 同步;前台 30s 轮询或 WS 推送(D:优先 WS,降级轮询)。
---
## 5. API
| API | 说明 |
|---|---|
| `GET /v1/messages?type=&cursor=` | 列表 |
| `POST /v1/messages/{id}/read` | 已读 |
| `POST /v1/messages/read_all` | 全读 |
| `DELETE /v1/messages/{id}` | 删会话 |
| `POST /v1/messages/{id}/mute` | 免打扰 |
| `GET /v1/messages/unread_count` | 角标 |
Message DTO
```json
{
"id": "uuid",
"type": "ai|expert|social|system|commerce",
"title": "",
"preview": "",
"avatar_url": "",
"unread": 0,
"muted": false,
"ref": {"kind":"conversation|consult|url","id":""},
"updated_at": "ISO8601"
}
```
---
## 6. 异常
推送权限拒绝:仅站内;首次提示去设置。
Token 失效:清列表进 Auth。
删除失败:Toast 回滚 UI。
---
## 7. 埋点
`message_tab_expose` · `message_open{type}` · `message_read_all` · `message_delete` · `message_mute`
## 8. 校验 ID
`V-M01` 分类形态 · `V-M02` 行字段 · `V-M03` 长按菜单项 · `V-M04` 角标规则
@@ -0,0 +1,154 @@
# 12 ·「问」完整设计(测测 AI + 真人 1v1)
> Route: `/ask` · Design Complete · 战略核心页
---
## 1. 目标
1. 以星盘档案为底座的垂直 AI 对话
2. 同页低摩擦切换真人咨询
3. 对象切换(自己/TA)扩展关系场景
4. 深度思考与子场景(灵魂伴侣等)承载差异化
---
## 2. UI 完整清单
### 2.1 顶栏 `AskTopBar`(✅ E8
| ID | 元素 | 行为 |
|---|---|---|
| A-BACK | 返回 | 回上一页/首页 |
| A-TAB-AI | 「测测AI」 | 默认选中 |
| A-TAB-HUMAN | 「真人1v1」 | 角标「新客半价」(运营可配) |
| A-MENU | 汉堡菜单 | 历史/设置;可带红点 |
### 2.2 AI 轨首屏(✅ E8 · 非纯聊天空态)
冷启动为**引导页 + 转化区**,而非空白线程:
| 区块 | 内容 |
|---|---|
| 人设问候 | 猫头鹰 IP +「Hi~我是测测AI / 最近有什么事…」 |
| 引导问题 ×4 | 整行按钮(非小芯片) |
| 真人咨询导流 | 「也可以选择真人咨询…」+「228位测测达人在线」+「去提问」 |
| 达人数量卡 | 1/3/8/15 位;「新客6折」等角标 |
| 底栏快捷 | **工具** · **语音通话** · **深度解读** · **灵魂伴侣** |
| 输入框 | placeholder「让我来解答你的问题吧」 |
| 免责声明 | 「内容由AI生成,仅供娱乐…」 |
**实测引导问题(✅ 替换原草稿)**
1. 有哪些适合我的财富建议?
2. 这份工作有发展吗?
3. 我有什么性格弱点吗?
4. 我和Ta会复合吗?
### 2.3 上下文/能力入口(✅ E8 落在底栏快捷)
| ID | 元素 | 行为 |
|---|---|---|
| A-TOOLS | 「工具」 | Sheet:星盘等工具入口 |
| A-VOICE | 「语音通话」 | 语音;可红点 |
| A-DEEP | 「深度解读」 | 深度能力(可能付费) |
| A-SOUL | 「灵魂伴侣」 | 子场景 |
对象切换/星盘维度:在工具 Sheet 或对话上下文中(待 `ask/tools-sheet` 截图补全)。
### 2.4 输入区 `AskComposer`
| 元素 | 行为 |
|---|---|
| 文本框 | placeholder 见上 |
| 发送 | 输入后出现 |
| 语音 | 底栏「语音通话」独立入口 |
### 2.5 真人轨 `AskHumanRail`
顶栏切「真人1v1」(角标「新客半价」);同页亦嵌入 AI 轨中部的达人导流卡。
### 2.6 子页
- `/ask/soulmate` 灵魂伴侣
- `/ask/history` 会话历史
- Profile 创建嵌入式 Modal
---
## 3. 交互与业务规则
| 规则 ID | 规则 |
|---|---|
| R-ASK-01 | 每条用户消息消耗 1 次额度;深度思考 ×2 |
| R-ASK-02 | 额度=0 → PaywallSheet(会员优先) |
| R-ASK-03 | 切换对象后下一条消息带新 `profile_ctx_id` |
| R-ASK-04 | 对象无档案 → 强制建档流,不可发送 |
| R-ASK-05 | 流式中再次发送:排队或禁用(D:禁用) |
| R-ASK-06 | 敏感意图 → 危机卡片,不计成功陪伴文案 |
| R-ASK-07 | 切「真人1v1」不清除 AI 历史 |
| R-ASK-08 | 下拉加载更早消息,page_size=20 |
---
## 4. 状态机
```mermaid
stateDiagram-v2
[*] --> Boot
Boot --> IdleFresh: 无历史
Boot --> IdleHistory: 有历史
IdleFresh --> Streaming: send/chip
IdleHistory --> Streaming
Streaming --> IdleHistory: done
Streaming --> StreamError: fail
StreamError --> Streaming: retry
IdleFresh --> NeedProfile: 选TA无档
NeedProfile --> IdleFresh: 建档完成
IdleHistory --> QuotaEmpty
QuotaEmpty --> Paywall
IdleHistory --> HumanRail: tab
HumanRail --> IdleHistory: tab AI
```
全量状态:`Boot` · `IdleFresh` · `IdleHistory` · `Streaming` · `StreamError` · `NeedProfile` · `QuotaEmpty` · `Paywall` · `HumanRail` · `Offline` · `AuthRequired` · `DeepOn`(正交 flag)。
---
## 5. API
| API | 说明 |
|---|---|
| `GET /v1/ai/bootstrap` | 额度、默认 profile、引导问题、deep 是否可用 |
| `GET /v1/ai/conversations` | 列表 |
| `GET /v1/ai/conversations/{id}/messages?cursor=` | 历史 |
| `POST /v1/ai/chat` | body: `{conversation_id?, content, profile_ctx_id, dimension, deep}`**SSE** |
| `POST /v1/ai/conversations` | 新建 |
| `GET /v1/ai/quota` | remaining |
SSE 事件:`meta` · `delta` · `suggest` · `usage` · `error` · `done`
---
## 6. 异常矩阵
| 场景 | UI |
|---|---|
| SSE 中断 | 气泡「已中断」+ 重试 |
| 402 额度 | Paywall |
| 400 无档案 | NeedProfile |
| 403 内容安全 | 固定拒答 |
| 超时 30s | StreamError |
| 离线 | 禁用发送 |
---
## 7. 埋点
`ask_open` · `ask_send` · `ask_chip_click` · `ask_deep_toggle` · `ask_profile_switch` · `ask_dimension_change` · `ask_soulmate_open` · `ask_human_tab` · `ask_paywall_show` · `ask_stream_error`
---
## 8. 校验 ID
`V-A01` 顶栏双 Tab · `V-A02` 上下文三控件+灵魂伴侣 · `V-A03` 引导问题原文 · `V-A04` 下拉历史 Tip · `V-A05` 深度思考是否耗额度 · `V-A06` 流式表现
@@ -0,0 +1,75 @@
# 13 · 在线完整设计(达人 Marketplace
> Route: `/online` · Design Complete
---
## 1. 目标
达人发现 → 下单 → 文字/语音/连麦服务 → 评价 → 复购。
---
## 2. UI 清单
### 2.1 列表页(✅ E8 部分 · 与「问·真人1v1」同构)
底栏「在线」带运营角标 **「限免」**;新客链路易**直达「向TA提问」收银页**(券弹层「我知道了」),列表本体与问-真人轨共用筛选:
| 区 | 元素 |
|---|---|
| 顶栏双轨残留 | 若从问切入可仍见「测测AI / 真人1v1」 |
| 筛选 | 综合排序 · 全部工具 · 筛选 · 新客半价 |
| ExpertCard | 头像、活跃、好评率、测友推荐、价格/次 |
| 限免漏斗 | 点进 → 向TA提问(文字/语音/合盘/星盘…)+ 测测币/卡券/VIP 折扣 |
### 2.2 详情页 `/online/experts/{id}`
简介 · 资质标 · 服务 SKU(文字/语音/连麦+单价+时长)· 评价列表 · 底部「立即咨询」· 收藏。
### 2.3 下单确认 Sheet
SKU · 优惠券 · 协议勾选 · 应付金额 · 去支付。
### 2.4 会话页 `/consult/{orderId}`
| 模式 | UI |
|---|---|
| 文字 | 聊天 + 剩余时长/条数 |
| 语音 | 语音条 |
| 连麦 | 通话 UI + 权限 |
结束:评价 1–5 星 + 标签 + 文字。
---
## 3. 订单状态机(前端必须渲染)
`Created``Paying``Paid``WaitingAccept``InService``Completed``Reviewed`
旁路:`Cancelled` · `Rejected` · `Refunding` · `Refunded` · `Expired`
超时:Paid 后 N 分钟未接单自动退款(N 后台配,默认 15)。
---
## 4. API
`GET /v1/experts` · `GET /v1/experts/{id}` · `POST /v1/consult/orders` · `POST /v1/consult/orders/{id}/pay` · `GET /v1/consult/orders/{id}` · `POST /v1/consult/orders/{id}/cancel` · `POST /v1/consult/orders/{id}/review` · 消息走 IM 通道(WS)。
---
## 5. 权限与安全
麦克风/相机权限拒绝 → 引导设置,降级文字。
举报入口:会话内 · 详情页。
未成年人:禁止连麦(D)。
---
## 6. 埋点
`online_expose` · `expert_click` · `consult_sku_select` · `consult_pay_success` · `consult_review_submit`
## 7. 校验 ID
`V-O01` 筛选维度 · `V-O02` SKU 种类 · `V-O03` 价格展示 · `V-O04` 评价表单
@@ -0,0 +1,90 @@
# 14 · 我的完整设计
> Route: `/mine` · Design Complete
---
## 1. UI 清单
### 1.1 头部 `MineHeader`
头像(可换)· 昵称 · 会员徽章(非会员显示「开通会员」)· 编辑资料箭头。
### 1.2 资产条 `MineAssets`
| 格 | 内容 | 点击 |
|---|---|---|
| 会员 | 到期日/未开通 | `/membership` |
| AI 次数 | remaining | `/ask` 或 Paywall |
| 优惠券 | 张数 | `/coupons` |
| 积分 | 余额(若无则隐藏整格 · 模拟器校验) | `/points` |
### 1.3 功能列表(分组定稿)
**档案与内容**
1. 生命档案 → `/profile`
2. 我的测试 → `/mine/tests`
3. 我的报告 → `/mine/reports`
4. AI 对话记录 → `/ask/history`
5. 我的收藏 → `/mine/favorites`
**订单与商业**
6. 会员中心 → `/membership`
7. 我的订单 → `/mine/orders`
8. 咨询订单 → `/mine/consult-orders`
9. 商城订单 → `/shop/orders`
**其它**
10. 邀请好友 → 分享
11. 消息通知设置 → `/settings/notifications`
12. 账号与安全 → `/settings/account`
13. 客服与帮助 → `/help`
14. 关于 → `/about`(协议/隐私/注销入口)
---
## 2. 子页要点
| 页 | 关键交互 |
|---|---|
| 生命档案列表 | Self 置顶;Other 按更新;新建;滑动删除 |
| 报告库 | 类型筛选:星盘/合盘/测评/AI;锁态角标 |
| 订单 | 状态 Tab;详情;申请退款 |
| 会员中心 | 套餐卡:9.9/25/58/178;权益表;自动续费管理 |
| 注销 | 二次确认 + 冷却期说明 |
---
## 3. 状态
`Guest`(显示登录大按钮)· `Loading` · `Ready` · `VipActive` · `VipExpired` · `Error`
---
## 4. API
`GET /v1/me` · `GET /v1/membership/me` · `GET /v1/ai/quota` · `GET /v1/orders` · `POST /v1/auth/logout` · `POST /v1/account/delete`
`GET /v1/me`
```json
{
"user": {"id":"","nickname":"","avatar_url":""},
"membership": {"active":false,"plan":null,"expires_at":null},
"quota": {"ai_remaining":0},
"counts": {"coupons":0,"favorites":0,"reports":0}
}
```
---
## 5. 埋点
`mine_expose` · `mine_item_click{key}` · `membership_open` · `invite_click`
## 6. 校验 ID
`V-U01` 列表项集合 · `V-U02` 会员价展示 · `V-U03` 资产条格子 · `V-U04` 注销路径
@@ -0,0 +1,126 @@
# 20 · 星座工具完整设计(星盘 / 合盘 / 运势)
> Design Complete · 愈心谷落地时命名走 STRATEGY / 词表
---
## 1. 信息架构
```
工具入口(首页/广场)
├── 星盘(本命)
│ ├── 档案选择 / 新建
│ ├── 概览(太阳/月亮/上升 + 象限)
│ ├── 行星列表
│ ├── 宫位列表
│ ├── 相位表
│ ├── 深度解读(付费锁)
│ └── 分享卡片
├── 合盘
│ ├── 双档案选择
│ ├── 关系类型(恋爱/友情/亲子…)
│ ├── 五图 + 推运(as_of)
│ ├── 维度评分
│ ├── 深度报告(付费)
│ ├── 邀请合盘(token
│ └── 附近的人(geo
└── 今日运势
├── 日运卡片
├── 周/月切换
└── 推送订阅
```
---
## 2. 星盘页 `NatalChartPage`
### 2.1 Route
`/tools/natal` · query: `profile_id?`
### 2.2 UI
| 区块 | 说明 |
|---|---|
| `ProfilePicker` | Self 默认;可切 Other |
| `ChartWheel` | 外圈宫位 + 行星 glyph;可缩放旋转(定稿:双指缩放) |
| `BigThree` | 日/月/升 三卡 |
| `Tabs` | 概览 / 行星 / 宫位 / 相位 / 解读 |
| `PaywallBanner` | 深度解读锁态 |
### 2.3 状态
`NeedProfile``Computing``Ready``Paywalled` / `Unlocked` · `Error`
### 2.4 API
- `GET /v1/profiles` · `POST /v1/profiles`
- `POST /v1/reports/natal` body: `{ "profile_id": "" }`
- 响应:`summary`(免费)+ `detail`(锁字段)+ `charts.natal`
### 2.5 校验
`V-N01` 轮盘交互 · `V-N02` 深度锁点 · `V-N03` 分享卡片字段
---
## 3. 合盘页 `SynastryPage`(对齐已实现能力)
### 3.1 Route
`/tools/synastry` · `/synastry/invite/:token`
### 3.2 UI 定稿
| 区块 | 说明 |
|---|---|
| 双人档案条 | A/B 头像昵称 + 换人 |
| 关系类型 Chip | 恋爱默认 |
| 五图 Tab | 比较盘/组合盘/组合中点/时空中点/组合星座 — 与 API `charts` 对齐 |
| 推运 | `as_of` 日期选择;Progressed 说明文案 |
| 维度雷达/条 | 吸引/沟通/信任/长期… |
| 深度报告 CTA | 会员或单次解锁 |
| 社交 | 「邀请合盘」「附近的人」 |
### 3.3 附近的人
- 需用户显式打开 `geo_visible`
- 列表:距离档位 + 合盘入口
- 隐私:不暴露精确坐标给对方客户端
### 3.4 API(已有契约优先)
- `POST /v1/reports/synastry` `{ profile_id_a, profile_id_b, as_of? }`
- `GET /v1/synastry/nearby`
- `POST /v1/synastry/invites` · `GET /v1/synastry/invites/:token` · `POST .../accept`
### 3.5 校验
`V-S01` 五图 Tab 文案与顺序 · `V-S02` 邀请链路 · `V-S03` 附近隐私开关默认
---
## 4. 今日运势 `DailyFortunePage`
### 4.1 UI
日期头 · 综合分 · 爱情/事业/财运/健康四格 · 短建议 ·「查看完整」(会员)
### 4.2 API
`GET /v1/fortune/daily?profile_id=&date=`
### 4.3 校验
`V-F01` 分数字段 · `V-F02` 完整内容锁点
---
## 5. 埋点统一
`tool_open{tool}` · `chart_tab{name}` · `synastry_generate` · `synastry_unlock` · `fortune_expose` · `invite_share` · `nearby_open`
## 6. 愈心谷落地备注
- UI 禁用「占卜/算命」;可用运势/合盘/匹配/解读
- 星历计算依赖与 AGPL 风险见 API README;设计层不绑定具体库
@@ -0,0 +1,119 @@
# 21 · 测评与沉浸式玩法完整设计
> Design Complete · MBTI / 广场 / 沙盘 / 心情小镇 / 塔罗
---
## 1. 测评中心 `TestsHub`
### 1.1 Route
`/tests` · `/tests/:slug`
### 1.2 UI
- 分类 Chip:人格 / 情感 / 压力 / 趣味
- 卡片:封面 · 标题 · 题量 · 预计时长 · 热度 · 免费/付费角标
- 搜索(可选,二期)
### 1.3 答题流 `TestPlayer`
| 步骤 | 说明 |
|---|---|
| Intro | 说明用途、隐私、预计时长、开始 |
| Question | 单选为主;进度条;可上一题 |
| Submit | Loading → 结果 |
| Result | 类型码/分数 + 摘要 + 分享 +「深度报告」锁 |
状态:`Intro` · `Answering` · `Submitting` · `ResultFree` · `ResultPaywalled` · `Error`
API
- `GET /v1/tests` · `GET /v1/tests/:slug`
- `POST /v1/tests/:slug/sessions``session_id`
- `POST /v1/tests/sessions/:id/answer`
- `POST /v1/tests/sessions/:id/submit`
- `GET /v1/tests/sessions/:id/report`
校验:`V-T01` 进度可回退 · `V-T02` 结果锁点 · `V-T03` 历史入库「我的测试」
---
## 2. 心理广场 `PsychPlaza`
Route: `/plaza`
| 区块 | 设计定稿 |
|---|---|
| Feed | 图文/话题卡;点赞评论 |
| Topic | 官方话题置顶 |
| Publish | 发帖(需登录);敏感词过滤 |
| Detail | 评论树一级 + 楼中楼 |
API`GET /v1/plaza/feed` · `POST /v1/plaza/posts` · `POST .../like`
注意:UGC 审核队列进运营后台(见 40)。
校验:`V-P01` Feed 排序 · `V-P02` 发帖门槛
---
## 3. 沙盘 `Sandbox`
Route: `/sandbox`
定稿流程:
1. 空场景 + 道具架(人物/物品/自然)
2. 拖拽摆放;缩放旋转
3. 完成 → 生成解读摘要(AI 或模板)
4. 保存作品到「我的」;可分享静态图
状态:`Editing` · `Generating` · `Saved` · `Paywalled`(深度解读)
API`POST /v1/sandbox/sessions` · `POST .../complete` · `GET .../report`
校验:`V-B01` 道具集 · `V-B02` 解读是否 AI
---
## 4. 心情小镇 `MoodTown`
Route: `/mood-town`
定稿:
- 地图节点:今日心情打卡 · 小任务 · 收集物
- 打卡:情绪滑杆 + 可选日记
- 连续打卡奖励(积分/优惠券,与会员解耦)
API`POST /v1/mood/checkin` · `GET /v1/mood/calendar` · `GET /v1/mood/town-state`
校验:`V-M01` 节点集合 · `V-M02` 奖励规则
---
## 5. 塔罗 `Tarot`
Route: `/tarot`
定稿(合规表述:牌阵洞察 / 自我反思,非「算命」):
| 模式 | 说明 |
|---|---|
| 单牌 | 日签 |
| 三牌 | 过去/现在/未来(表述可改为 课题/现状/行动) |
| 牌阵库 | 恋爱/决策等模板 |
流程:选牌阵 → 洗牌动画 → 抽牌 → 正逆位 → 牌义摘要 → 深度(付费)
API`POST /v1/tarot/draw` `{ spread, seed? }` · `GET /v1/tarot/history`
校验:`V-R01` 牌阵列表 · `V-R02` 深度锁 · `V-R03` 文案是否触发词表禁词(实现闸)
---
## 6. 统一商业与埋点
- 深度结果统一走 `PaywallSheet`(会员优先 / 单次次之)
- 埋点:`test_start` · `test_complete` · `plaza_post` · `sandbox_complete` · `mood_checkin` · `tarot_draw` · `immersive_unlock`
@@ -0,0 +1,179 @@
# 30 · 完整数据模型 · API · 错误 · 缓存
> Design Complete · 路径前缀统一 `/api/v1`(下文简称 `/v1`
---
## 1. 领域实体(逻辑模型)
```
User ──< Profile (self|other)
User ── Membership
User ── AiQuota
User ──< Order ── OrderItem
User ──< Coupon
User ── DevicePush
User ──< Conversation ──< Message
User ──< ConsultOrder ── Consultant
User ──< TestSession ── TestReport
User ──< NatalReport / SynastryReport
User ──< TarotDraw / SandboxWork / MoodCheckin
User ── GeoPref (geo_visible, lat, lng, updated_at)
SynastryInvite (token, host_user, guest?, status)
PlazaPost ──< Comment
```
### 1.1 关键字段定稿
| 实体 | 必填字段 |
|---|---|
| Profile | id, user_id, kind, name, gender?, birth_date, birth_time?, birth_place, lat, lng, tz |
| Membership | user_id, plan(enum), status, starts_at, expires_at, auto_renew |
| Order | id, user_id, channel, amount_fen, status, product_type, product_ref |
| Conversation | id, user_id, mode(ai\|human), status, consultant_id? |
| SynastryReport | id, profile_a, profile_b, as_of?, summary_json, detail_json, unlocked |
| AiQuota | user_id, remaining, reset_at, source |
计划枚举:`trial_9_9` · `month_25` · `season_58` · `year_178`(展示价,分单位存储另表)
---
## 2. API 目录(完整设计表)
### 2.1 账号
| Method | Path | 说明 |
|---|---|---|
| POST | `/v1/auth/sms/send` | 发码 |
| POST | `/v1/auth/sms/login` | 登录/注册 |
| POST | `/v1/auth/logout` | 登出 |
| GET | `/v1/me` | 我的聚合 |
| PATCH | `/v1/me` | 改昵称头像 |
| POST | `/v1/account/delete` | 注销申请 |
### 2.2 档案
| Method | Path | 说明 |
|---|---|---|
| GET/POST | `/v1/profiles` | 列表/创建 |
| PATCH/DELETE | `/v1/profiles/:id` | 更新/删 |
| PATCH | `/v1/me/geo` | `{visible,lat?,lng?}` |
### 2.3 内容与报告
| Method | Path | 说明 |
|---|---|---|
| POST | `/v1/reports/natal` | 本命 |
| POST | `/v1/reports/synastry` | 合盘 |
| GET | `/v1/fortune/daily` | 日运 |
| GET | `/v1/reports` | 我的报告库 |
| GET | `/v1/reports/:id` | 单份(含锁态) |
### 2.4 合盘社交
| Method | Path | 说明 |
|---|---|---|
| POST | `/v1/synastry/invites` | 创建邀请 |
| GET | `/v1/synastry/invites/:token` | 预览(无 host_profile_id 泄露) |
| POST | `/v1/synastry/invites/:token/accept` | 接受 |
| GET | `/v1/synastry/nearby` | 附近 |
### 2.5 AI / 咨询
| Method | Path | 说明 |
|---|---|---|
| GET | `/v1/ai/quota` | 余量 |
| POST | `/v1/ask/conversations` | 开聊 |
| GET | `/v1/ask/conversations` | 历史 |
| POST | `/v1/ask/conversations/:id/messages` | 发消息(流式可选 SSE |
| GET | `/v1/consultants` | 达人列表 |
| POST | `/v1/consult/orders` | 下咨询单 |
| POST | `/v1/consult/orders/:id/cancel` | 取消 |
### 2.6 测评与沉浸
| Method | Path | 说明 |
|---|---|---|
| GET | `/v1/tests` | 列表 |
| * | `/v1/tests/...` | 见 21 |
| * | `/v1/plaza/...` | 见 21 |
| * | `/v1/sandbox/...` | 见 21 |
| * | `/v1/mood/...` | 见 21 |
| * | `/v1/tarot/...` | 见 21 |
### 2.7 商业
| Method | Path | 说明 |
|---|---|---|
| GET | `/v1/membership/plans` | 套餐 |
| POST | `/v1/pay/orders` | 创建支付单 |
| POST | `/v1/pay/mock/confirm` | 仅开发 mock |
| GET | `/v1/orders` | 订单列表 |
| GET | `/v1/coupons` | 优惠券 |
| POST | `/v1/pay/orders/:id/refund` | 申请退款 |
### 2.8 消息与配置
| Method | Path | 说明 |
|---|---|---|
| GET | `/v1/inbox` | 站内信聚合 |
| POST | `/v1/inbox/:id/read` | 已读 |
| GET | `/v1/home/feed` | 首页聚合 |
| GET | `/v1/config/app` | 开关/词表版本/审核模式 |
---
## 3. 统一响应与错误
### 3.1 成功包络
```json
{
"code": 0,
"message": "ok",
"data": {},
"request_id": "..."
}
```
### 3.2 错误码定稿
| code | HTTP | 含义 | 客户端 |
|---|---|---|---|
| 0 | 200 | 成功 | — |
| 40101 | 401 | 未登录 | 跳登录 |
| 40301 | 403 | 无权限/未解锁 | Paywall |
| 40302 | 403 | 会员权益不足 | Membership |
| 40401 | 404 | 资源不存在 | Toast |
| 40901 | 409 | 冲突(重复提交) | 忽略/提示 |
| 42201 | 422 | 参数错误 | 字段红字 |
| 42901 | 429 | 限流 | 稍后重试 |
| 40201 | 402/403 | 配额用尽 | 次数 Paywall |
| 50000 | 500 | 内部错误 | 通用错误页 |
锁态内容:**不**靠藏路由;返回 `unlocked:false` + `teaser`,完整 `detail` 仅解锁后下发(或同接口按 entitlement 裁剪)。
---
## 4. 缓存与一致性
| 数据 | 策略 |
|---|---|
| 首页 feed | CDN/边缘 60s;登录个性化部分 API 直出 |
| 日运 | 按 `profile_id+date` 缓存 1h |
| 本命/合盘计算结果 | 按档案哈希 + as_of 缓存;档案变更失效 |
| 我的聚合 `/me` | 客户端短缓存 30s;支付成功强制失效 |
| Inbox 未读 | 推送 + 轮询 60s(前台) |
| 词表/开关 | `/config/app` ETag;启动拉取 |
离线:仅缓存已解锁报告摘要;敏感档案不同步到未加密存储。
---
## 5. 分页约定
`?cursor=&limit=20` · 响应 `{ items, next_cursor }`
## 6. 与模拟器校验的关系
抓包对照本章路径与字段;差异记入 `99-emulator-validation.md` 的 Diff 表,再回写 OpenAPI。
@@ -0,0 +1,109 @@
# 40 · 运营 · 商业 · 推送 · 埋点 · 安全
> Design Complete
---
## 1. 运营后台模块(信息架构)
```
Admin
├── 内容:首页运营位 / 工具配置 / 测评上下架 / 话题
├── UGC:广场审核队列 / 举报处理
├── 达人:入驻审核 / 排班 / 分成配置
├── 商业:套餐 / 优惠券 / 订单 / 退款
├── 用户:检索 / 封禁 / 注销工单
├── 消息:站内信模板 / 推送任务
└── 系统:开关 / 词表版本 / 审计日志
```
权限:超级管理员 · 运营 · 审核 · 客服(只读订单+工单)
---
## 2. 支付与会员
### 2.1 商品类型
| product_type | 说明 | 履约 |
|---|---|---|
| `membership` | 会员套餐 | 写 Membership + 可能赠 AI 次 |
| `ai_pack` | AI 次包 | AiQuota += n |
| `report_unlock` | 单次报告 | report.unlocked=true |
| `consult` | 咨询时长/次 | ConsultOrder paid |
| `shop` | 实物/虚拟商品 | 履约状态机 |
### 2.2 支付状态机
`created``paying``paid` → (`refunding``refunded`) · `closed`
客户端:统一 `PaySheet`(渠道:微信/支付宝/IAP 占位;开发 `pay-mock`)。
### 2.3 价格展示(✅ E8 `free2/mine/会员.png`
| 档 | 展示价 | 划线/备注 |
|---|---|---|
| 连续包月(首月) | ¥9.9 | 后续 ¥25 自动续费 |
| 连续包月 | ¥25 | |
| 连续包季 | ¥58 | 划线例 ¥90 |
| 连续包年 | ¥178 | 划线例 ¥360 |
| 12个月套餐 | ¥368 | 年卡x水晶等运营 SKU |
自动续费:会员中心「记录」可关;文案含解除说明。
---
## 3. 推送
| 场景 | 通道 | 触发 |
|---|---|---|
| 日运提醒 | Push | 用户订阅 + 本地时区早晨 |
| 咨询回复 | Push + Inbox | 达人新消息 |
| 系统通知 | Inbox | 运营任务 |
| 支付成功 | Inbox | 履约完成 |
| 合盘邀请被接受 | Push | AcceptInvite |
开关:`/settings/notifications` 分项;系统级权限引导一次。
---
## 4. 埋点字典(核心)
| event | 必填属性 |
|---|---|
| `app_open` | cold/warm, app_ver |
| `tab_view` | tab |
| `tool_open` | tool |
| `paywall_expose` | scene, product_hint |
| `pay_click` | product_type, amount_fen |
| `pay_success` | order_id, product_type |
| `ask_send` | mode, conversation_id |
| `synastry_generate` | relation_type |
| `test_complete` | slug |
| `membership_open` | from |
实现:客户端 SDK 批量上报;`request_id` 可关联 API 日志。
---
## 5. 安全与合规
| 项 | 定稿 |
|---|---|
| 词表 | 硬禁「占卜」「算命」等;启动拉 `/config/app` |
| 未成年 | 支付与深度内容年龄门(策略开关) |
| 隐私 | 协议版本强更;geo 默认关;注销冷却 |
| 鉴权 | Bearer;刷新策略另开;后台操作审计 |
| 限流 | 登录/发帖/AI 分桶 |
| 支付 | 验签;幂等 `out_trade_no`mock 仅非生产 |
---
## 6. 客服与工单
入口:我的 → 帮助;订单详情 → 申请退款。
工单状态:`open` · `processing` · `resolved` · `rejected`
## 7. 校验 ID
`V-O01` 套餐价 · `V-O02` 支付渠道集合 · `V-O03` 推送分项 · `V-O04` 审核后台字段(无 App 内证,后台自建)
@@ -0,0 +1,131 @@
# 99 · 安卓模拟器采集验证协议
> 设计已定稿;本文件规定**如何用模拟器把 🟡/待证项变成 ✅ 或 Diff 回写**。
---
## 1. 目标
1. 对照 `complete-design` 各页的 **校验 IDV-\*** 采集证据。
2. 差异写入 Diff 表 → 回写对应设计文档 / OpenAPI。
3. 不阻塞当前 Feature Complete:未验证项保持「设计定稿 (D) + 待模拟器」。
---
## 2. 环境准备
| 项 | 建议 |
|---|---|
| 模拟器 | Android Emulator API 33+,安装官方测测 APK(或应用商店包) |
| 代理 | mitmproxy / Charles;模拟器 WiFi 代理指向宿主机 |
| HTTPS | 安装 user CA;若 SSL Pinning 失败则改用 UI 录屏 + 布局层次(uiautomator |
| 账号 | 新号 + 会员号各一;记录手机号不入库文档 |
| 录制 | 每条 V-\* 至少:截图 1 +(可选)HAR 片段 + 操作步骤 |
### 2.1 本机已装(2026-08
| 项 | 路径 / 名称 |
|---|---|
| SDK | `ANDROID_HOME=/opt/homebrew/share/android-commandlinetools` |
| AVD | `YuXinGu_API34`Pixel 7 / API 34 / google_apis / arm64 |
| Agent Skill | `/Users/jack/tools/android-adb-skill``./tools/android` + `~/.claude/skills/android-*` |
| Cursor Rule | `.cursor/rules/android-adb.mdc` |
| 冒烟截图 | `.tmp/cece-validation/smoke/screen.png` |
启动与截图:
```bash
emulator -avd YuXinGu_API34 -no-audio -gpu auto &
adb wait-for-device
./tools/android screenshot --out .tmp/cece-validation/smoke/screen.png --json
```
> 测测 APK 仍需另行安装后,才能跑 Batch A–D。
证据目录建议(本地,勿强求进 git):
```
.tmp/cece-validation/
YYYYMMDD/
V-H01/
steps.md
screen.png
traffic.har # 可选
```
---
## 3. 采集步骤(单条 V-\*
1. 打开设计文档中的校验 ID 定义(预期 UI/文案/API)。
2. 冷启动 App → 按步骤复现到目标屏。
3. 截图;若可抓包,过滤 host,保存相关 request/response。
4. 填写结果:`PASS` / `DIFF` / `BLOCKED`(如 pinning、地区限制)。
5. `DIFF` 必须写:期望(设计)vs 实际 + 建议改设计或改实现。
---
## 4. 优先级批次
### Batch A — 五 Tab 骨架(先做)
| ID | 主题 | 文档 |
|---|---|---|
| V-H01~ | 首页楼层顺序与运营位 | `10-page-home` |
| V-G01~ | 消息分类与未读 | `11-page-message` |
| V-A01 | 问一问 AI/人工切换 | `12-page-ask` |
| V-L01 | 连麦/在线列表 | `13-page-online` |
| V-U01~ | 我的列表与资产条 | `14-page-mine` |
### Batch B — 星座与合盘
| ID | 主题 | 文档 |
|---|---|---|
| V-N01~ | 本命轮盘与锁点 | `20-module-astrology` |
| V-S01~ | 五图顺序、邀请、附近默认 | 同上 |
| V-F01 | 日运字段 | 同上 |
### Batch C — 测评与沉浸
| ID | 主题 | 文档 |
|---|---|---|
| V-T01~ | 测评进度与结果锁 | `21-module-tests-immersive` |
| V-P01 | 广场 | 同上 |
| V-B01 / V-M01 / V-R01 | 沙盘/小镇/塔罗 | 同上 |
### Batch D — 商业与系统
| ID | 主题 | 文档 |
|---|---|---|
| V-O01 | 会员价与支付 | `40-ops-commerce` |
| 路径对照 | API 与 30 章一致性 | `30-data-api` |
---
## 5. Diff 登记表(模板)
复制到当次验证笔记:
| ID | 结果 | 期望(设计) | 实际(模拟器) | 处置 | 回写文件 |
|---|---|---|---|---|---|
| V-S01 | DIFF | 五图顺序 A-B-C… | 顺序为… | 改设计 / 改实现 | `20-module-astrology.md` |
处置约定:
- **改设计**:以竞品真机为准,更新 complete-design,证据链加 E\*。
- **改实现**:愈心谷已按设计开发则排期对齐。
- **忽略**:明确合规/品牌差异(词表),在文档标「刻意差异」。
---
## 6. 与 Evidence First 的衔接
- 模拟器截图/HAR → 记入 `cece-frontend-re/README.md` 证据表新行(E8+)。
- 原 🟡 项验证通过后改为 ✅,去掉「仅设计定稿」标注。
- 完整设计包门禁:**不**因未跑模拟器而撤销 Design Complete。
---
## 7. 愈心谷侧注意
验证测测时允许看到其原生产物用词;**落地愈心谷 UI 时仍走词表**,Diff 中单独列「合规替换」列,避免把禁词写进产品文案。
@@ -0,0 +1,64 @@
# 测测前端 · 完整产品设计包(Design Complete V1.0
> Status: **Design Complete(设计定稿)** · 实现与模拟器校验前的唯一前端设计输入
> 上游逆向底稿:[../README.md](../README.md)
> 方法:[reverse-engineering-spec.md](../../../../design/reverse-engineering-spec.md)
> 校验计划:安卓模拟器采集验证(见 [99-emulator-validation.md](99-emulator-validation.md)
---
## 0. 本包定位
| 项 | 说明 |
|---|---|
| 是什么 | 基于公开实测 + 逆向推导的 **Feature Complete 设计说明书** |
| 不是什么 | 不是 Demo;不是「以后再补」的大纲 |
| 证据策略 | ✅ 已证实写死;🟡/🔵 在本包中升格为 **设计定稿(D**,并列入模拟器校验清单 |
| 品牌 | 本文用「测测」指称竞品对象;愈心谷落地时过 lexicon,禁止自称测测 |
**冻结规则**:未经验证通过前,工程实现以本包为准;模拟器发现差分时,先改本包再改代码。
---
## 1. 文档地图
| 文档 | 内容 |
|---|---|
| [00-design-bible.md](00-design-bible.md) | 产品目标 · IA · 全局组件 · 全局状态 · 设计令牌约定 |
| [10-page-home.md](10-page-home.md) | 首页完整设计 |
| [11-page-message.md](11-page-message.md) | 消息完整设计 |
| [12-page-ask.md](12-page-ask.md) | 「问」完整设计 |
| [13-page-online.md](13-page-online.md) | 在线完整设计 |
| [14-page-mine.md](14-page-mine.md) | 我的完整设计 |
| [20-module-astrology.md](20-module-astrology.md) | 星座/星盘/合盘/运势完整设计 |
| [21-module-tests-immersive.md](21-module-tests-immersive.md) | 测评/广场/沙盘/心情小镇/塔罗 |
| [30-data-api.md](30-data-api.md) | 数据模型 · 表 · API · 错误码 · 缓存 |
| [40-ops-commerce.md](40-ops-commerce.md) | 后台 · 权限 · 支付 · 消息 · 埋点 · 配置 · 安全 |
| [99-emulator-validation.md](99-emulator-validation.md) | 安卓模拟器采集与差分回填协议 |
| [evidence-20260803.md](evidence-20260803.md) | **E8 真机 Diff**10.50.0 模拟器) |
| [STATUS-overnight.md](STATUS-overnight.md) | 夜间采集结果摘要(起床看) |
---
## 2. 设计完成度门禁
| 门禁 | 标准 |
|---|---|
| Design Complete | 本包各文档无「略/TODO」;每页含 UI 清单、交互、状态机、接口、异常、埋点 |
| Validation Ready | `99-emulator-validation.md` 用例可执行 |
| Impl Ready | `30-data-api` + 页面契约可直接开工 |
| Parity Verified | 模拟器采集后差分关闭(后续) |
当前:**Design Complete + Validation Ready**。Parity Verified = 待模拟器。
---
## 3. 与逆向底稿关系
```text
../0008 逆向分析(Evidence First
↓ 升格定稿
complete-design/* 完整设计(本包)
↓ 模拟器采集
差分回填 → 修订本包 → 再实现/再验
```
@@ -0,0 +1,41 @@
# 夜间采集状态(给你起床看)
> 时间:2026-08-03 凌晨 · APK 10.50.0 · 已登录账号
## 结论
**免付费主路径大部分已截到**,设计 Diff 已写入 `evidence-20260803.md` + 首页/消息/问文档。
工具宫格与「更多」清单已采;「我的」子页与部分在线列表曾被 SVIP/向TA提问 收银页干扰,需醒来后补一轮。
## 截图目录(本地)
| 目录 | 内容 |
|---|---|
| `.tmp/cece-validation/20260803/free2/` | **主成果**(包名校验后) |
| `free2/01-home.png``0x-tab-*.png` | 五 Tab / 问双轨 |
| `free2/tools/*.png` | 宫格落地:星座/星盘/合盘/紫微/陪伴小星/倾诉/智慧卡/报告/生辰历/灵魂伴侣/广场/更多… |
| `free2/tools/more/*.png` | 更多页子项 |
| `free2/manifest.json` | 清单(脚本结束后生成) |
## 已回写设计
- `00-design-bible.md` 底栏角标
- `10-page-home.md` 宫格顺序 / 自己卡片五维
- `11-page-message.md` 列表形态
- `12-page-ask.md` AI 引导题与底栏快捷
- `evidence-20260803.md` E8
## 「更多」实机条目(✅ labels)
推荐 · 星盘报告 · 测试 · 倾诉 · 心情小镇 · 幸运地图 · 星宿 · 爱情树 · 灵魂伴侣 · 聊天分析 · 关系网 · 商城 · 解读 · 沙盘 · 缘分合盘 · 星座 · 生辰 · 紫微 · 政余 · 灵数 · 生肖 · 玛雅图腾 · 人类图 · 星盘 · 生辰历 · 紫微历 · 政余历 · 星骰 · 智慧卡 · 日历
## 醒来建议(可选 10 分钟)
1. 确认模拟器还在:`adb devices`
2. 打开测测,关掉任何 SVIP/向TA提问弹层
3. 说「继续补我的和在线列表」——我只补这两块
## 脚本
- `tools/cece_capture_free.py`(初版)
- 夜间实际用的是内联 package-guard 流程;稳定版可再收束进脚本
@@ -0,0 +1,92 @@
# 真机证据 Diff · 2026-08-03(模拟器 · 测测 10.50.0
> Evidence ID: **E8** · AVD `YuXinGu_API34` · package `com.xxwolo.cc5` · APK `bird-xxdoc-android-release-xxwolo-10.50.0`
> 截图根目录:`.tmp/cece-validation/20260803/`(本地,未入库)
> 主成果:`free2/`34 张)· 说明:[STATUS-overnight.md](STATUS-overnight.md)
---
## 1. 冷启动 / 门禁
| 屏 | 证据 | 定稿结论 |
|---|---|---|
| 温馨提示 | `onboarding/10-start.png` | 同意 / 不同意→游客;披露位置·麦克风·设备信息 |
| 游客首页 | `V-H01/home.png` | 底栏右端为「未登录」;中间「问」+ AI 角标 |
| 登录页 | 点「同意」后 | 手机号+验证码 / 密码登录 / 其他方式;协议勾选 |
| 未成年人模式 | `logged/00-current.png` | 开启未成年人模式 / **不再提醒** |
| 每日登录领好礼 | 遮罩态截图 | 回归福利七日;立即领取;下方 X 关闭 |
| 隐私政策更新 | 登录后偶发 | 同意 / 不同意并退出 |
---
## 2. 底栏五 Tab(已登录 · ✅)
| 序 | 文案 | 角标/形态 | Diff vs 原设计 |
|---|---|---|---|
| 1 | 首页 | 选中态房子图标 | 一致 |
| 2 | 消息 | 红点数字 | 一致 |
| 3 | **问** | **中央大圆 + AI 角标** | 一致;无障碍树常无「问」文本 |
| 4 | 在线 | 红标「限免」 | **新增运营角标**;易直达向TA提问 |
| 5 | 我的 | 红点 | 游客态文案为「未登录」 |
---
## 3. 首页结构(已登录 · ✅ V-H01)
### 3.1 顶区
签到 · 搜索运营占位轮播 · `+` · 「输入生日,更加了解TA」
### 3.2 自己卡片
今日心情分 + 五维:**爱情 / 财富 / 事业 / 学习 / 人际**
### 3.3 宫格
**第一行:** I人E人 · 星座 · 星盘 · 生辰 · 缘分合盘 · 紫微
**第二行:** 陪伴小星 · 倾诉 · 智慧卡 · 星盘报告(New) · 生辰历 · 灵魂伴侣
宫格下:AI玩法广场大卡 + 商城运营卡;脚注「部分标题由AI生成,仅供参考」。
### 3.4 更多页条目(✅)
`10-page-home.md` §2.3.1;含沙盘/心情小镇/幸运地图/爱情树/人类图等。
---
## 4. 截图清单(`free2/`
| 路径 | 说明 |
|---|---|
| `01-home` · `02-tab-home` | 首页 |
| `03-tab-message` | 消息 |
| `04-tab-ask*` | 问 · AI / 真人1v1 |
| `05-tab-online` | 在线(注意限免漏斗) |
| `06-tab-mine*` | 我的 |
| `tools/*` | 宫格落地免付费页 |
| `tools/more/*` | 更多子项 |
| `mine/{root,会员,档案,测试,报告}` | 我的子页;会员价 ✅ |
| `manifest.json` | 34 张索引 |
---
## 5. 「问」轨 Diff(✅)
- 顶栏:`测测AI` | `真人1v1`(「新客半价」)
- 引导题:财富建议 / 工作发展 / 性格弱点 / 复合(及变体)
- 底栏:工具 · 语音通话 · 深度解读 · 灵魂伴侣
- 免责声明固定底
---
## 6. 会员价(✅)
连续包月首月 ¥9.9 → ¥25;季 ¥58;年 ¥178(见 `40-ops-commerce.md`)。
---
## 7. 愈心谷落地注意
- 竞品文案含「测测」;落地过 lexicon。
- 游客 / 未成年人 / 每日领取 / 隐私更新 为必做门禁。
- 「向TA提问」为高意图收银,列表浏览需可退回。
+147
View File
@@ -0,0 +1,147 @@
# Feature Spec: 陪伴(节气壳)
> Status: `Active`(壳) · Map: `§4 陪伴 [P2 主做;P1 可占位]` · Phase: `P1 Shell / P2`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 陪伴 |
| Purpose | 日常留存:节气生活建议与心情记录 |
| Business Goal | Tab 占位完整;P2 做深留存 |
| InP1 壳) | OutP1 |
|---|---|
| 今日节气展示(本地/轻 API) | 完整心情 OS |
| 生活建议(非运势) | 疗效承诺 |
| 路由与 Tab 可达 | 强制日记社交 |
| InP2 | |
|---|---|
| 心情记录落库、成长记录 | |
---
## 2. 用户价值
1. **为何需要:** 需要轻量每日触达,不只是一次性测试。
2. **P1 获得:** 今日节气与建议壳。
3. **付费:** N/AP1)。
---
## 3. 用户角色
| Actor | P1 |
|---|---|
| Visitor | 可打开陪伴页看节气壳 |
---
## 4. 用户流程
```text
Tab 陪伴 → /companion
展示今日节气 + 生活建议文案
P2)记录心情 → POST /moods
```
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/companion` | CompanionPage |
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Loading | 拉节气 |
| Normal | 节气 + 建议 |
| Error | 降级本地文案或重试 |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 文案为「生活建议 / 节气」,禁止「今日运势」 |
| R2 | P1 不宣称完整陪伴 OS 已交付 |
| R3 | 心情 API 若暴露须鉴权;P1 可不接 UI |
| R4 | 扩行为前必须把本 Spec Phase 与 In Scope 升为 P2 并补验收 |
---
## 8. 数据模型影响
| 表 | P1 | P2 |
|---|---|---|
| solar term | 只读 API/本地 | |
| `moods` | 可有 API | UI 写入 |
---
## 9. API 需求
| Method | Path | 意图 |
|---|---|---|
| GET | `/api/v1/solar-terms/today` | 今日节气 |
| POST | `/api/v1/moods` | P2 心情 |
---
## 10. 权限设计
| 能力 | Visitor |
|---|---|
| 看节气壳 | ✓ |
| 写心情 | P2 |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `companion_viewed` | 打开 |
| `mood_saved` | P2 |
---
## 12. 测试验收标准
**Given** P1
**When** 打开陪伴
**Then** 页面可达且无运势禁用词;不因未做心情而崩溃
**Given** 要将心情做进 P1
**When** —
**Then** **禁止**在未改 Spec/map 分期前实现
---
## 13. AI 开发前检查
- [x] 壳范围已标明 · 禁止超 scope
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `CompanionPage` · `internal/companion` · moods 表 |
| 实现 | GET `/solar-terms/today` · POST `/moods` · GET `/moods/today` · H5 心情 UI |
| Gaps | 成长计划、推送、心情趋势 |
| Label | 陪伴 P2 心情已接;真支付仍后置 |
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · 心情已按产品「其他先做」落地 |
+192
View File
@@ -0,0 +1,192 @@
# Feature Spec: 探索测试
> Status: `Active` · Map: `2.2 人格测评 [P1]` · Phase: `P1`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 探索测试 |
| Purpose | 通过短量表帮助用户了解沟通方式、情感模式等偏好 |
| Business Goal | 丰富探索中心;结果导流关系理解与问答;可分享 |
| In | Out |
|---|---|
| 已发布量表列表、答题、计分、丰富结果 | 伪 MBTI 版权侵权内容 |
| ≥1 热门测试(P1:沟通方式 + 情感模式) | 医疗量表诊断 |
| 分享结果卡 | 未定义的「深度付费层」若未实现则标 Gap |
---
## 2. 用户价值
1. **为何需要:** 比生日画像更场景化的自我标签。
2. **完成后获得:** 风格标签、概述、维度、建议、话术、成长计划。
3. **付费:** P1 结果全量返回(免费);深度会员导流为软转化(非强制锁结果)。`Gap:` 若产品后续要对部分结果上锁,须先更新本 Spec。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| Visitor | 浏览列表;提交需 Self Profile |
| 无档案 | 可看题,提交前引导建档 |
---
## 4. 用户流程
```text
/explore 测试列表
进入 /scales/:slug
加载题目
逐题作答(须全部完成)
无 Self → 引导建档
POST result → 展示丰富结果
分享 / 去关系理解 / 去问答
```
### 子能力展开
| 子能力 | P1 |
|---|---|
| 测试入口(探索 Tab) | ✓ |
| 测试列表 | ✓ |
| 问题流程 | ✓ |
| 答题保存(服务端结果落库) | ✓ |
| 中断恢复 | ✓ localStorage 草稿(`scaleDraft`);提交后清除 |
| 结果生成(丰富结构) | ✓ |
| 免费层 | ✓ 全量 result |
| 深度层 | N/A(当前不锁) |
| 分享 | ✓ ShareSheet |
| AI 解释 | 软链 `/ask` |
| 数据统计 | 埋点名定义;SDK `Gap` |
| 验收标准 | §12 |
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/explore` | ExplorePage |
| `/scales/:slug` | ScalePage |
```text
/scales/:slug
├── Loading(题目)
├── Normal(答题中)
├── Empty/NeedProfile(提交前无档案)
├── Error
└── Result(完成后)
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Loading | 加载题目 |
| Error | 重试 |
| Normal | 进度 + 单选题 |
| NeedProfile | 引导首页 |
| Result | ReportRich 结构展示 |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 仅 `published` 量表可列表/作答 |
| R2 | 提交前答案覆盖全部题目 |
| R3 | 必须绑定用户 Self `profile_id` |
| R4 | 计分:多数选项 → style;结果文案 lexicon 安全 |
| R5 | result 含 label/summary/overview/dimensions/tips/scripts/growth_plan/faq |
| R6 | 结果「不是固定标签」需在文案中提示 |
| R7 | 作答过程写入本地草稿(按 slug);提交成功后清除;刷新可恢复未提交答案 |
---
## 8. 数据模型影响
| 表 | 备注 |
|---|---|
| `scales` / `scale_questions` | seed |
| `scale_results` | answers + result jsonb |
---
## 9. API 需求
| Method | Path | 意图 |
|---|---|---|
| GET | `/api/v1/scales` | 列表 |
| GET | `/api/v1/scales/{slug}` | 题目 |
| POST | `/api/v1/scales/{slug}/result` | 提交计分 |
---
## 10. 权限设计
| 能力 | Visitor 无档案 | Visitor 有 Self |
|---|---|---|
| 看列表/题目 | ✓ | ✓ |
| 提交结果 | ✗ | ✓ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `scale_list_viewed` | 探索列表 |
| `scale_started` | 进入作答 |
| `scale_completed` | 出结果 |
| `scale_share_clicked` | 分享 |
| `scale_cta_relation` / `scale_cta_ask` | 导流 |
---
## 12. 测试验收标准
**Given** published 量表与 Self
**When** 答完提交
**Then** 返回 label 与 overview/tips 等丰富字段
**Given** 未答完
**When** 提交
**Then** 前端拦截;不成功落库
**Given** 无 Self
**When** 提交
**Then** 引导建档
---
## 13. AI 开发前检查
- [x] Spec 齐全 · 子能力已展开 · map · lexicon · OpenAPI · 可测
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `internal/scale` · `service/scale` · `ExplorePage` · `ScalePage` |
| Seeds | communication-style · emotion-pattern |
| Gaps | 结果付费墙若要做须先改 Spec;全量 §11 埋点未挂(核心漏斗见 analytics) |
| Tests | score/result L1`scaleDraft.spec`Scale 流程组件测 |
| Process Review | 草稿恢复已实现 · P1 收口 |
+151
View File
@@ -0,0 +1,151 @@
# Feature Spec: 首页(发现与入口)
> Status: `Active` · Map: `§1 首页 [P1]` · Phase: `P1`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 首页 |
| Purpose | 品牌露出 + 建档/性格探索主 CTA + 功能入口分发 |
| Business Goal | 新用户进入 Step 1–3;品牌第一屏可识别 |
| In | Out |
|---|---|
| 建档生日表单 → 画像;九宫格/入口 | 运势 Feed 主路径 |
| 推荐区静态/轻运营即可 | Dashboard 式状态墙(如「API 已连接」勿对用户展示) |
---
## 2. 用户价值
1. **为何需要:** 知道从哪开始认识自己。
2. **完成后获得:** 一键进入画像主路径或其它 Tab。
3. **付费:** N/A。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| 新 Visitor | 见品牌 + 建档 CTA |
| 老用户 | 同样入口;可去「我的」看资产 |
---
## 4. 用户流程
```text
打开 /
看到品牌 + 主 CTA(生日/探索)
提交生日 → 创建档案 → /portrait
点入口 → /explore /relation /ask /membership …
```
设计约束(前端):品牌英雄级信号;移动优先;文案 lexicon。
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/` | HomePage |
| 全局 | AppHeader · TabBar |
```text
/
├── Hero / 品牌
├── 建档 CTA
├── 功能入口
└── (可选)推荐内容
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Normal | 主路径可见 |
| Error | 建档/跳转失败可感知 |
| Loading | 提交建档时按钮态 |
Empty:首页本身是入口,不因无档案空白死页。
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 主 CTA 必须通向建档或画像,而非死链 |
| R2 | 不展示内部运维信息(API 连接状态等) |
| R3 | 入口文案符合 lexicon(个人画像/关系理解/成长会员…) |
| R4 | Tab 五名冻结:首页·探索·问答·陪伴·我的 |
---
## 8. 数据模型影响
经首页建档时写 `profiles`(见 profile Spec)。
---
## 9. API 需求
复用 `POST /profiles``POST /reports/portrait`;无专用 home API。
---
## 10. 权限设计
| 能力 | Visitor |
|---|---|
| 浏览首页 | ✓ |
| 建档并生成画像 | ✓ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `home_viewed` | 打开 |
| `home_cta_portrait` | 主 CTA |
| `home_entry_click` | 九宫格项(带 name |
---
## 12. 测试验收标准
**Given** 新用户
**When** 打开首页
**Then** 可见品牌与建档/探索入口
**Given** 合法生日
**When** 提交主 CTA
**Then** 进入画像基础结果路径
---
## 13. AI 开发前检查
- [x] Spec 齐全
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `HomePage` · `App.vue` shell |
| Gaps | 推荐 Feed 运营位可静态;埋点未接 |
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · 设计闭合 · 实现 PASS · 测试 PARTIAL |
+185
View File
@@ -0,0 +1,185 @@
# Feature Spec: 意象卡片
> Status: `Active`(设计)· Map: `2.8 意象卡片 [P2]` · Phase: `P2`
> 规范:[../feature-design.md](../feature-design.md) · 队列:[P2-BACKLOG.md](P2-BACKLOG.md)
> 竞品对照:塔罗体系(能力同构;**UI 禁止「塔罗」入口名**)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 意象卡片 |
| Purpose | 通过抽取 1~3 张意象卡,对当下议题做投射反思,并给出结构化反思题与行动建议 |
| Business Goal | 高趣味探索与分享;免费次数 + 深度解读转化 |
| In | Out |
|---|---|
| 场景选题 · 抽卡 · 探索摘要 · 深度组合解读 | 吉凶判定、神谕、恐吓 |
| 卡面内容库(seed) | UI 文案「塔罗」「牌阵占卜」 |
| 每日免费次数规则 | 宿命叙事 |
---
## 2. 用户价值
1. **为何需要:** 卡住时需要一个轻量的外在意象帮助整理感受。
2. **完成后获得:** 卡片意象说明、反思问题、可执行小建议;深度版含组合解读与练习。
3. **为何付费:** 多卡组合与完整练习路径。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| Visitor | 每日免费 N 次基础抽卡(N 实现时定数,建议 1~3) |
| DeepAccess(本结果)/ VIP | 深度组合解读 |
| 可选挂档案 | 有档案时文案可轻微个性化(非必须) |
---
## 4. 用户流程
```text
/cards
选择场景:情绪整理 / 关系 / 选择 / 自我
检查今日免费次数
├─ 耗尽 → 引导会员或明日再来(可看历史)
└─ 有余 → 抽 1 张(免费)或解锁 3 张组合(深度)
展示意象 + 反思题 + 建议
分享(积极、非恐吓视觉)
```
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/cards` | ImageCardPageP2 draft |
| `/explore` | 入口「意象卡片」 |
| `/reports/:id` 或结果页内 | 深度结果沉淀(可选) |
```text
/cards
├── Scene picker
├── Loading(抽卡动画,克制)
├── Normal(单卡摘要)
├── Locked(组合深度)
├── Quota empty
└── Error
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Loading | 抽卡中 |
| Normal | 意象说明 + 反思 |
| Locked | 组合解读墙 |
| Quota empty | 次数用尽说明 + 会员 CTA |
| Error | 重试 |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 用户可见文案**不得**出现「塔罗」;用「意象卡片」「探索」「反思」 |
| R2 | **禁止**吉凶、神谕式断言、恐吓(如「大难」) |
| R3 | 输出必须含:意象描述 · 至少 1 个反思问题 · 1 条可执行建议 |
| R4 | 免费:每日有限次单卡摘要;深度:3 卡组合 + 练习(Membership 或单次 DeepAccess |
| R5 | 抽卡随机可接受,但卡面文案来自审核过的内容库(非模型现场胡编吉凶) |
| R6 | 分享卡不得含恐吓图文;过 lexicon |
| R7 | 次数与权益 **Server** 判定 |
---
## 8. 数据模型影响
| 项 | 说明 |
|---|---|
| 内容库 | `image_cards` 表或 seed JSONid, title, image_key, explore_text, prompts[] |
| 结果 | `growth_reports.type=image_card` **或** `scale_results` 变体(实现时二选一,写 OpenAPI) |
| 配额 | 用户日维度计数(表或 Redis);实现时定 |
禁止:`tarot_*` 作用户可见 API 路径名(对内包名用 `imagecard`)。
---
## 9. API 需求(草案)
| Method | Path | 意图 |
|---|---|---|
| GET | `/api/v1/image-cards/scenes` | 场景列表 |
| POST | `/api/v1/image-cards/draw` | scene → 抽卡结果(扣免费次) |
| GET | `/api/v1/image-cards/quota` | 今日剩余 |
| GET | `/api/v1/reports/{id}` | 若沉淀为报告 |
深度组合可同 draw 参数 `depth=true` + 权益校验。
---
## 10. 权限设计
| 能力 | 免费额度内 | 额度外 | 深度权益 |
|---|---|---|---|
| 单卡摘要 | ✓ | ✗ | ✓ |
| 三卡组合解读 | ✗ | ✗ | ✓ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `cards_scene_selected` | 选场景 |
| `cards_drawn` | 抽卡成功 |
| `cards_quota_exhausted` | 次数用尽 |
| `deep_access_clicked` | surface=cards |
| `cards_share_clicked` | 分享 |
---
## 12. 测试验收标准
**Given** 有免费次数
**When** 抽卡
**Then** 返回意象+反思题+建议;响应与 UI 无「塔罗」「吉凶」
**Given** 次数为 0
**When** 再抽
**Then** 业务错误,不生成假深度
**Given** 无深度权益
**When** 请求组合解读
**Then** Locked / 剥离
---
## 13. AI 开发前检查
- [x] Spec 齐全 · 禁用塔罗入口已写清
- [x] lexicon ImageCard
- [ ] 内容库 seed 与 OpenAPI(编码时)
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| 复用 | ShareSheet · membership/orders · Explore 入口 |
| 工作量 | 卡面文案/视觉资产最大;宜先 22~78 张小库 MVP |
| Packages | `internal/imagecard` · `ImageCardPage` `/cards` |
| 实现 | scenes/quota/draw API · 报告 type=`image_card` · 日配额表 · seed 12 卡 |
| 编码顺序 | P2-BACKLOG 切片 3 Done |
+176
View File
@@ -0,0 +1,176 @@
# Feature Spec: 身心节律
> Status: `Active`(设计)· Map: `2.7 身心节律 [P2]`= `2.5 身心探索` 产品化主入口)· Phase: `P2`
> 规范:[../feature-design.md](../feature-design.md) · 队列:[P2-BACKLOG.md](P2-BACKLOG.md)
> 竞品对照:命理体系之生活向(能力同构,禁止算命/流年吉凶叙事)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 身心节律 |
| Purpose | 基于出生信息给出五行/体质倾向隐喻与作息·饮食·运动等生活建议 |
| Business Goal | 差异化护城河(数字性格 × 身心);导流节气陪伴与会员 |
| In | Out |
|---|---|
| 元素平衡、体质倾向、本周生活建议 | 流年吉凶、改命、疗效/治病承诺 |
| 与节气生活联动 | 八字/紫微作恐吓主标题 |
| GrowthReport 或 Constitution 聚合 | 医疗诊断 |
**与 2.5** 本 Spec 为身心探索的**唯一产品化主入口**;map 上 2.5 保留能力描述,入口文案统一「身心节律」。
---
## 2. 用户价值
1. **为何需要:** 想知道如何按自己的节奏安排生活,而不只是性格标签。
2. **完成后获得:** 节律概览、元素倾向、今日/本周生活建议;深度版含分维习惯方案。
3. **为何付费:** 完整习惯方案与长期节律计划。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| Visitor+档案 | 基础节律报告 |
| DeepAccess/VIP | detail |
| 无档案 | 引导建档 |
---
## 4. 用户流程
```text
/rhythm 或探索入口
确认 Profile
生成节律报告(元素 · 倾向 · 生活建议)
基础 summary → 深度 detail(会员/深度版)
CTA:节气陪伴 / 问答「生活节奏」/ 分享
```
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/rhythm` | LifeRhythmPageP2 draft |
| `/companion` | 节气联动出口 |
| `/reports/:id` | type=rhythm 详情 |
```text
/rhythm
├── Empty / Loading / Error
├── Normal(基础节律 + 生活建议)
└── Locked
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Loading | 分析中 |
| Empty | 无档案引导 |
| Error | 重试 |
| Normal | 维度 + 本周建议 |
| Locked | 深度版 CTA |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 绑定 Profilebirth_date 必填 |
| R2 | 确定性引擎(可参考遗留 yangsheng 思路,**新代码进 monorepo** |
| R3 | summary/detail 权益同画像(Membership DeepAccess |
| R4 | **禁止**:疗效、改善疾病、流年吉凶、改命、算命 |
| R5 | 推荐用语:体质倾向、生活建议、节律、平衡 |
| R6 | 可展示五行隐喻,须标注探索/非医疗 |
| R7 | 与星象性格、个人画像内容可互相链,不互相覆盖主路径 |
---
## 8. 数据模型影响
| 项 | 说明 |
|---|---|
| `growth_reports.type` | `rhythm` / `life_rhythm` |
| 或 Constitution 专用表 | 若需独立聚合,实现时 ADR |
| 禁止 | `luck_score`、病名诊断字段 |
---
## 9. API 需求(草案)
| Method | Path | 意图 |
|---|---|---|
| POST | `/api/v1/reports/rhythm` | profile_id → 报告 |
| GET | `/api/v1/reports/{id}` | 复用 |
| GET | `/api/v1/solar-terms/today` | 页内联动展示(已有) |
---
## 10. 权限设计
| 能力 | 有档案 | 深度权益 |
|---|---|---|
| summary | ✓ | ✓ |
| detail | ✗ | ✓ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `rhythm_started` | 开始生成 |
| `rhythm_completed` | 基础结果 |
| `deep_access_clicked` | surface=rhythm |
| `rhythm_to_companion` | 去节气 |
---
## 12. 测试验收标准
**Given** 有生日档案
**When** 生成身心节律
**Then** 含生活建议;无「吉凶」「疗效」「算命」
**Given** 无权益
**When** GET detail
**Then** 剥离
**Given** 文案审查
**When** 扫 summary+detail
**Then** 符合 lexicon
---
## 13. AI 开发前检查
- [x] Spec 齐全 · 与 2.5 关系已写清
- [x] lexicon LifeRhythm
- [ ] OpenAPI/erd(编码时)
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| 复用 | ReportRich · membership · companion 节气 · 立项中医体质方向 |
| 参考勿迁 | 根目录 `yangsheng.html` 仅思路参考 |
| Packages | `internal/rhythm` · `LifeRhythmPage` `/rhythm` |
| 实现 | `POST /reports/rhythm` · OpenAPI 已登记 |
| 编码顺序 | P2-BACKLOG 切片 2 Done |
+185
View File
@@ -0,0 +1,185 @@
# Feature Spec: 深度版与成长会员
> Status: `Active` · Map: `§6 商业体系 · §5.3 会员中心 [P1]` · Phase: `P1`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 深度版成长报告 + 成长会员 |
| Purpose | 用可理解的权益换取付费;解锁完整分析与更高 Ask 配额 |
| Business Goal | ARPU;验证付费墙与 mock 支付闭环 |
| In | Out |
|---|---|
| 单报告 DeepAccess | 广告干扰主路径 |
| 订阅 Membership(月/季/年 mock | 塔罗商城 |
| Server 侧权益生效 | 纯前端「解锁」 |
---
## 2. 用户价值
1. **为何需要:** 基础结果不够用时需要完整分析 / 更多问答。
2. **完成后获得:** 该报告 detail;或会员期内全部报告深度 + Ask 额度提升。
3. **为何付费:** 明确权益清单(完整分析、AI 次数、专属内容占位)。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| Visitor | 可下单 mock 支付 |
| DeepAccess 持有者 | 指定 report 的 detail |
| VIP | 会员期内全局深度 + Ask 配额规则 |
---
## 4. 用户流程
### 深度版(报告内)
```text
报告页 Locked CTA
POST /orders { kind: deep_access, report_id }
POST /orders/{id}/pay-mock
写入 deep_access → 刷新报告 detail 可见
```
### 成长会员
```text
/membership 查看套餐与当前状态
下单 kind=membership + plan
pay-mock → memberships 行 active + expires_at
此后 GET 任意己方报告 detail 可见(按服务规则)
```
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/membership` | MembershipPage |
| 画像/关系/报告内 | Paywall / DeepAccess CTA |
```text
/membership
├── Loading
├── Normal(未开通 / 已开通+到期)
├── Error
└── (无独立 Locked;本身即售卖页)
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Loading | 拉取 `/membership/me` |
| Normal | 展示套餐、当前是否会员、到期时间 |
| Error | 重试 |
| 报告 Locked | 深度版入口文案(lexicon:深度版/成长会员,不用「解锁」作主文案) |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | DeepAccess 绑定 `user_id` + `report_id` |
| R2 | Membership active`expires_at > now` |
| R3 | 报告 detail 可见 ⇔ Membership active **OR** DeepAccess 存在 |
| R4 | 支付 P1 仅 mock;成功必须服务端落库后再改可见性 |
| R5 | Ask 配额:免费次数耗尽后依赖会员配额(见 ask Spec) |
| R6 | 订单归属必须校验 report 属于当前用户 |
| R7 | 文案:成长会员 / 深度版;禁用恐吓与疗效承诺 |
---
## 8. 数据模型影响
| 表 | 备注 |
|---|---|
| `orders` | kind, amount, status, report_id null |
| `payments` | mock |
| `deep_access` | user_id, report_id, unique |
| `memberships` | plan, expires_at |
---
## 9. API 需求
| Method | Path | 意图 |
|---|---|---|
| GET | `/api/v1/membership/me` | 当前会员状态 |
| POST | `/api/v1/orders` | deep_access \| membership |
| POST | `/api/v1/orders/{id}/pay-mock` | 生效权益 |
---
## 10. 权限设计
| 能力 | Visitor | DeepAccess | VIP |
|---|---|---|---|
| 看基础报告 | ✓ | ✓ | ✓ |
| 看该报告 detail | ✗ | ✓ | ✓ |
| 看任意己方 detail | ✗ | 仅已购报告 | ✓ |
| 下单 mock | ✓ | ✓ | ✓ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `paywall_viewed` | 看到锁定区 |
| `deep_access_clicked` | 点深度版 |
| `membership_plan_clicked` | 选套餐 |
| `order_created` | 下单 |
| `purchase_completed` | pay-mock 成功 |
---
## 12. 测试验收标准
**Given** 无权益报告
**When** pay-mock deep_access
**Then** 再 GET 同报告 detail 非空
**Given** 会员未过期
**When** GET 任意己方 portrait/relation 报告
**Then** detail 可见无需每份 DeepAccess
**Given** 他人 report_id
**When** 尝试下单
**Then** 失败(归属校验)
---
## 13. AI 开发前检查
- [x] Spec 齐全 · map · lexicon · OpenAPI · 可测
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `service/order` · `service/membership` · `MembershipPage` |
| Gaps | 真支付网关;发票;退款(须先修订本 Spec §9) |
| Tests | L2 membership unlockMembershipPage.spec |
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · mock 闭环设计闭合 · 实现 PASS · 测试 PASS |
+197
View File
@@ -0,0 +1,197 @@
# Feature Spec: 个人画像
> Status: `Active` · Map: `2.1 / 2.3 个人画像 [P1]` · Phase: `P1`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 个人画像(成长画像) |
| Purpose | 帮助用户了解自己的性格特点、沟通/关系/事业/情绪/生活节奏与成长方向 |
| Business Goal | 首次体验转化;驱动深度版购买与成长会员;为问答提供档案语境 |
| In | Out |
|---|---|
| 基于生日确定性生成基础 + 深度内容 | 运势/吉凶/算命叙事 |
| 多维报告结构(性格·沟通·关系·事业·情绪·生活) | 星盘/塔罗主路径 |
| DeepAccess / 会员解锁 detail | 医疗诊断或疗效承诺 |
---
## 2. 用户价值
1. **为何需要:** 完成建档后希望快速得到「我是什么样的人」的结构化总结。
2. **完成后获得:** 免费基础画像(标题、概述、关键词、六维速览、生活建议);付费后完整分维分析、优势盲区、成长路径、对话示例、FAQ。
3. **为何付费:** 深度内容可执行、可分享前更完整,降低「只有一句话标签」的空洞感。
---
## 3. 用户角色
| Actor | 说明 | 本功能能力 |
|---|---|---|
| Visitor | 设备身份自动签发 JWT | 可建 Self Profile、生成画像、看 summary |
| User | 同上(P1 以 Visitor 为主) | 同左 |
| DeepAccess | 该 `growth_report` 已购深度版 | 可见 `detail` |
| VIP | `memberships` active | 可见 `detail`(与 DeepAccess 或关系) |
---
## 4. 用户流程
```text
首页填写生日 / 已有 Self 档案
POST 创建画像报告
展示基础结果(summaryoverview、dimensions、keywords…)
点击深度版?
├─ 否 → 分享 / 去关系理解 / 去问答
└─ 是 → 创建 order(deep_access) → pay-mock
Server 写入 DeepAccess
GET report → detail 完整展示
```
无档案:引导首页/档案创建(Empty)。
---
## 5. 页面设计
| 路由 | 页面 | 说明 |
|---|---|---|
| `/portrait` | PortraitPage | query: `y,m,d``report_id` |
| `/reports/:id` | ReportPage | 同报告详情渲染 |
```text
/portrait
├── Loading
├── Empty(缺生日/档案)
├── Error
├── Normal(基础结果 + 多维速览)
└── Locked(无权益时的深度墙)
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Loading | 「正在生成…」 |
| Empty | 「请从首页填写生日」+ 链到 `/` |
| Error | 文案 + 重试 |
| Normal | 标题、one_liner、ReportRich 基础层 |
| Locked | 深度版说明 + 「查看深度版(模拟支付)」 |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 必须有归属用户的 Profile(通常 `relation=self`)才能生成 |
| R2 | `summary` 对报告所有者始终可返回;含多维 teaser,不含完整 depth 长文 |
| R3 | `detail` 仅当 Membership active **或** 该报告 DeepAccess 存在时返回 |
| R4 | 权益**仅 Server** 判断;客户端 `has_deep_access` 为只读投影 |
| R5 | 内容确定性:同生日同名 → 同 pattern;禁用词:运势/吉凶/算命/合盘等 |
| R6 | 深度内容须含分维 sections、strengths、blind_spots、growth_plan、scripts(结构要求) |
| R7 | 免责:自我探索与生活方式参考,非医疗/占卜 |
---
## 8. 数据模型影响
| 表 | 变更 | 备注 |
|---|---|---|
| `profiles` | 读 | birth_date 必填 |
| `growth_reports` | 写 type=`portrait` | summary/detail jsonb |
| `deep_access` / `orders` | 解锁时 | 见 membership Spec |
详见 [../../domain/erd.md](../../domain/erd.md)。
---
## 9. API 需求
| Method | Path | 意图 |
|---|---|---|
| POST | `/api/v1/reports/portrait` | body: `profile_id` → 创建/返回报告 |
| GET | `/api/v1/reports/{id}` | 按权益剥离 detail |
| POST | `/api/v1/orders` | `kind=deep_access`, `report_id` |
| POST | `/api/v1/orders/{id}/pay-mock` | mock 支付生效 |
OpenAPI`proto/openapi.yaml`
---
## 10. 权限设计
| 能力 | Visitor | DeepAccess | VIP |
|---|---|---|---|
| 生成画像 | ✓(己方档案) | ✓ | ✓ |
| 看 summary | ✓ | ✓ | ✓ |
| 看 detail | ✗ | ✓ | ✓ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `portrait_started` | 进入生成 |
| `portrait_completed` | 基础结果展示 |
| `deep_access_clicked` | 点深度版 CTA |
| `purchase_completed` | pay-mock 成功且 detail 可见 |
| `portrait_share_clicked` | 打开分享 |
---
## 12. 测试验收标准
**Given** 用户已填生日
**When** 进入画像并生成
**Then** 显示 headline、overview/多维速览、生活建议;无 detail 长文墙内完整 sections
**Given** 无会员且无 DeepAccess
**When** 点击深度版并 mock 支付
**Then** `has_deep_access=true` 且分维分析/成长路径可见
**Given** 篡改前端假装已解锁
**When** GET report
**Then** 无权益时 detail 仍不可得
---
## 13. AI 开发前检查
- [x] Spec 齐全
- [x] feature-map 已挂树
- [x] lexicon 已核对
- [x] OpenAPI 已有 portrait/reports
- [x] §12 可测
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `internal/portrait` · `service/report` · `PortraitPage` · `ReportRich` |
| Migrations | `growth_reports` 等已有 |
| Gaps | 埋点 SDK 未接(事件名已定义);真实支付 P 后 |
| Tests | `portrait` L1;集成 deep_accessH5 PortraitPage.spec |
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · 设计闭合 · 实现 PASS · 测试 PASS |
---
## 附录:报告结构
免费:title, style_label, headline, keywords, one_liner, overview, life_tip, dimensions[6], strengths_preview
深度:sections[6+], strengths, blind_spots, growth_plan, conversation_scripts, daily_suggestions, faq
+165
View File
@@ -0,0 +1,165 @@
# Feature Spec: 个人档案
> Status: `Active` · Map: `1.1 创建个人档案 · 5.1 我的档案 [P1]` · Phase: `P1`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 个人档案 |
| Purpose | 建立「我」与「TA」的结构化档案,作为画像、关系、问答的输入 |
| Business Goal | 漏斗 Step 1;提高后续功能完成率 |
| In | Out |
|---|---|
| Self / Other;生日必填;编辑删除 | 完整户籍式资料收集 |
| 关系类型(伴侣/家人/朋友) | 强制出生时辰地点(可后置) |
---
## 2. 用户价值
1. **为何需要:** 个性化分析需要稳定身份锚点。
2. **完成后获得:** 可切换的 Self/Other 列表;后续功能可挂档案。
3. **付费:** N/A(档案本身免费)。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| Visitor | CRUD 自己的 profiles |
| 他人档案 | 不可读写 |
---
## 4. 用户流程
```text
首页快捷建档(生日) 或 /profile
创建 Selfdisplay_name + birth_date
可选:添加 TArelation=other
编辑 / 删除
下游:画像 / 关系 / 问答选择档案
```
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/` | 首页内嵌建档入口 |
| `/profile` | ProfilePage(列表/编辑/删除/加 TA |
| `/mine` | 入口到档案 |
```text
/profile
├── Loading
├── Empty(无档案 → 引导创建)
├── Normal(列表 + 编辑)
└── Error
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Loading | 拉取列表 |
| Empty | CTA 创建「我」 |
| Error | 重试 |
| Normal | Self/Other 卡片;编辑表单;删除确认 |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | `birth_date` P1 必填 |
| R2 | `relation` = `self` \| `other`other 建议 `relation_type` |
| R3 | 仅所有者可 PATCH/DELETE |
| R4 | 删除策略:任务未要求硬删 PII 时按实现(软删优先);级联影响报告见服务规则 |
| R5 | Ask / 画像 / 关系必须挂合法 profile_id |
| R6 | 文案用「个人档案」,不用命盘 |
---
## 8. 数据模型影响
| 表 | 字段要点 |
|---|---|
| `profiles` | id, user_id, relation, display_name, birth_date, birth_time?, birth_place?, gender?, relation_type? |
---
## 9. API 需求
| Method | Path | 意图 |
|---|---|---|
| GET | `/api/v1/profiles` | 列表 |
| POST | `/api/v1/profiles` | 创建 |
| PATCH | `/api/v1/profiles/{id}` | 更新 |
| DELETE | `/api/v1/profiles/{id}` | 删除 |
---
## 10. 权限设计
| 能力 | Visitor(己方) | 他方 |
|---|---|---|
| 列表/创建/改/删 | ✓ | ✗ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `profile_create_started` | 开始创建 |
| `profile_created` | Self 或 Other 创建成功 |
| `profile_updated` | 保存编辑 |
| `profile_deleted` | 删除成功 |
---
## 12. 测试验收标准
**Given** 无档案
**When** 创建 Self 合法生日
**Then** 列表出现;可被画像使用
**Given** 已有 Self
**When** 添加 TA
**Then** relation=other 且可用于关系理解
**Given** 他人 id
**When** PATCH
**Then** 失败
---
## 13. AI 开发前检查
- [x] Spec 齐全 · map · lexicon · OpenAPI · 可测
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `service/profile` · `ProfilePage` · 首页建档 |
| Gaps | birth_time/place UI 可后置;硬删 vs 软删统一;埋点未接 |
| Tests | Profile 更新/删除 API L2ProfilePage 四态 |
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · 设计闭合 · 实现 PASS · 测试 PARTIAL |
+176
View File
@@ -0,0 +1,176 @@
# Feature Spec: 关系理解
> Status: `Active` · Map: `2.4 关系理解 [P1 · 双引擎]` · Phase: `P1`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 关系理解 |
| Purpose | 帮助用户了解自己与 TA 在沟通、节奏、亲密与冲突上的差异,并获得相处建议 |
| Business Goal | 双引擎增长(分享裂变);深度相处建议付费;不可弱化 |
| In | Out |
|---|---|
| 双 Profile 对比 + 关系报告 | 「合盘 / 合婚 / 缘分」叙事 |
| 多维对比、互补、冲突修复建议 | 宿命/恐吓文案 |
| DeepAccess / 会员解锁完整建议 | 无档案空算 |
---
## 2. 用户价值
1. **为何需要:** 想理解「为什么和 TA 总卡住」。
2. **完成后获得:** 双方风格标签、差异一句话、维度对比;付费后沟通/冲突/亲密/共同成长与本周练习。
3. **为何付费:** 完整相处说明书与话术,可立即用于对话。
---
## 3. 用户角色
| Actor | 本功能能力 |
|---|---|
| Visitor | 需已有 Self;可建 Other;生成 insight + 基础 summary |
| DeepAccess / VIP | 可见完整 detail |
---
## 4. 用户流程
```text
进入 /relation
无 Self 档案? → 引导首页创建画像/档案
填写 TA 称呼 + 生日 → 创建 Other Profile
POST relation/insight
展示双方风格 + 差异 + 维度对比(summary
深度建议? → order deep_access → pay-mock → 完整 sections
分享卡(我的方式 vs TA
```
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/relation` | RelationPage |
| `/reports/:id` | 关系类报告详情 |
| `/share` | 分享落地 |
```text
/relation
├── Form(输入 TA
├── Empty(无 Self
├── Loading / Error
├── Normal(基础对比)
└── Locked(完整建议墙)
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Empty | 无 Self → 链到首页 |
| Loading | 「生成中…」 |
| Error | 文案 + 可去建档 |
| Normal | me/other style、diff、dimension_compare |
| Locked | 深度版 CTA |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 必须引用两个 Profile(通常 self + other);禁止无档案生成 |
| R2 | summary 含对比与维度;detail 含 sections / scripts / weekly_practice |
| R3 | detail 权益同画像:Membership **或** 该报告 DeepAccess |
| R4 | Server 鉴权;禁用词:合盘、合婚、运势、吉凶 |
| R5 | 差异叙事为「说明书」非评分/宿命 |
| R6 | 增长价值不得弱于个人画像(分享路径必须存在) |
---
## 8. 数据模型影响
| 表 | 备注 |
|---|---|
| `profiles` | other + relation_type |
| `relation_insights` | profile_a/b + report_id |
| `growth_reports` | type=`relation` |
---
## 9. API 需求
| Method | Path | 意图 |
|---|---|---|
| POST | `/api/v1/profiles` | 创建 TA |
| POST | `/api/v1/relation/insight` | self_id + other_id → insight + report |
| GET | `/api/v1/reports/{id}` | 按权益返回 |
| POST | `/api/v1/orders` + pay-mock | 深度版 |
---
## 10. 权限设计
| 能力 | Visitor | DeepAccess/VIP |
|---|---|---|
| 生成基础关系理解 | ✓ | ✓ |
| 完整相处建议 | ✗ | ✓ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `relation_started` | 点生成 |
| `relation_completed` | 基础结果展示 |
| `deep_access_clicked` | 深度 CTA |
| `relation_share_clicked` | 分享 |
| `purchase_completed` | 支付成功 |
---
## 12. 测试验收标准
**Given** 已有 Self
**When** 填写 TA 并生成
**Then** 显示双方风格与 diff_one_liner;无权益时无完整 sections
**Given** 无 Self
**When** 点生成
**Then** 提示去首页建档,不调用成功 insight
**Given** 支付深度版
**When** 刷新报告
**Then** 沟通/冲突等建议可见
---
## 13. AI 开发前检查
- [x] Spec 齐全 · map 已挂 · lexicon · OpenAPI · 可测
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `internal/relation` · `service/relation` · `RelationPage` · `ReportRich` |
| Gaps | 埋点未接;真实支付后置 |
| Tests | relation engine L1;集成;RelationPage.spec |
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · 设计闭合 · 实现 PASS · 测试 PASS |
+144
View File
@@ -0,0 +1,144 @@
# Feature Spec: 成长报告
> Status: `Active` · Map: `2.1.2 · 5.2 我的成长报告 [P1]` · Phase: `P1`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 成长报告 |
| Purpose | 沉淀用户已生成的画像/关系等报告资产,支持回看与深度解锁 |
| Business Goal | 「我的」资产感;复访与付费 |
| In | Out |
|---|---|
| 列表 + 详情;按权益展示 detail | 收藏夹完整产品(可后置) |
| portrait / relation 类型 | 第三方导出 PDFGap) |
---
## 2. 用户价值
1. **为何需要:** 不想每次重新生成才能回看。
2. **完成后获得:** 历史报告列表与详情。
3. **付费:** 详情内深度版墙同 membership Spec。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| Visitor | 仅己方报告 |
| DeepAccess/VIP | 详情 detail |
---
## 4. 用户流程
```text
/mine → 成长报告列表 /reports
点一项 → /reports/:id
展示 summary;无权益则 Locked → 支付 → detail
```
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/reports` | ReportsPage(入口也在 `/mine` |
| `/reports/:id` | ReportPage |
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Loading | 列表/详情加载 |
| Empty | 无报告 → 引导画像/探索 |
| Error | 重试 |
| Normal | 列表项 / ReportRich |
| Locked | 深度版 CTA |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 仅返回当前用户报告 |
| R2 | summary 可读;detail 按 Membership/DeepAccess |
| R3 | type 区分 portrait / relation 等渲染 |
| R4 | 删除报告策略:未要求则可不做;若做须鉴权 |
---
## 8. 数据模型影响
| 表 | 备注 |
|---|---|
| `growth_reports` | 主数据 |
---
## 9. API 需求
| Method | Path | 意图 |
|---|---|---|
| GET | `/api/v1/reports` | 列表 |
| GET | `/api/v1/reports/{id}` | 详情(权益剥离) |
---
## 10. 权限设计
| 能力 | 所有者 | 非所有者 |
|---|---|---|
| 列表/详情 | ✓ | ✗ |
| detail 字段 | 视权益 | ✗ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `reports_list_viewed` | 打开列表 |
| `report_opened` | 打开详情 |
| `deep_access_clicked` | 详情内 CTA |
---
## 12. 测试验收标准
**Given** 用户有报告
**When** GET 列表
**Then** 仅己方项
**Given** 无权益
**When** 打开详情
**Then** summary 可见 detail 不可见
---
## 13. AI 开发前检查
- [x] Spec 齐全
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `handler/report` · `ReportsPage` · `ReportPage` · Mine 入口 |
| Gaps | 收藏;PDF |
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · 设计闭合 · page-tree 已含 `/reports` · 实现 PASS · 测试 PARTIAL |
+138
View File
@@ -0,0 +1,138 @@
# Feature Spec: 分享卡
> Status: `Active` · Map: journey 双引擎 · Phase: `P1`
> 规范:[../feature-design.md](../feature-design.md)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 分享卡 |
| Purpose | 将画像/关系/测试结果变成可传播卡片,拉新进入主漏斗 |
| Business Goal | 裂变;关系理解分享是传播主力 |
| In | Out |
|---|---|
| 前端 ShareCard / ShareSheet`/share` query 载荷落地 | 服务端短链中台(Gap) |
| lexicon 安全文案 | 运势惊吓文案 |
---
## 2. 用户价值
1. **为何需要:** 想把「我和 TA 的差异」发给对方看。
2. **完成后获得:** 可复制链接或卡片视觉(以实现为准)。
3. **付费:** N/A;落地页引导完成建档/探索。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| 分享者 | 从结果页打开 ShareSheet |
| 访客打开 `/share` | 看摘要 + CTA 进 App 主路径 |
---
## 4. 用户流程
```text
画像/关系/测试结果 → 生成分享卡
携带 query payload 打开 /share(或系统分享)
落地展示关键词/差异 → CTA 去首页/探索
```
---
## 5. 页面设计
| 路由 | 页面 |
|---|---|
| `/share` | SharePage |
| 组件 | ShareCard · ShareSheet |
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Normal | 有效 payload 渲染卡片 |
| Empty/Error | payload 无效 → 引导首页 |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 分享文案必须过 lexicon(无运势/合盘/解码) |
| R2 | 不在分享卡暴露 detail 付费长文 |
| R3 | 关系分享强调「我的方式 vs TA」+ 查看关系理解 |
| R4 | P1 payload 可以是前端 query;勿把密钥放入 URL |
---
## 8. 数据模型影响
无必须 schema`Gap:` 服务端 share token 表未建。
---
## 9. API 需求
P1 无专用分享 API(前端载荷)。若加短链:须新 Spec 修订 + OpenAPI。
---
## 10. 权限设计
| 能力 | 任何人 |
|---|---|
| 打开落地页摘要 | ✓ |
| 查看原报告 detail | 否(须登录所有者权益) |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `share_sheet_opened` | 打开面板 |
| `share_link_copied` | 复制 |
| `share_landing_viewed` | `/share` |
| `share_landing_cta` | 点 CTA |
---
## 12. 测试验收标准
**Given** 画像结果
**When** 打开分享
**Then** 卡片含标题/关键词且无禁用词
**Given** 无效 query
**When** 打开 `/share`
**Then** 不白屏,引导首页
---
## 13. AI 开发前检查
- [x] Spec 齐全
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| Packages | `ShareSheet` · `ShareCard` · `shareLink` · `SharePage` |
| Gaps | 原生系统分享;短链服务(短链须先改 §8/§9) |
| Tests | Share 相关组件/e2e |
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · P1 设计闭合 · 实现 PASS · 测试 PARTIAL |
+204
View File
@@ -0,0 +1,204 @@
# Feature Spec: 星座
> Status: `Active` · Map: `2.6 星座 [P2]` · Phase: `P2`
> 规范:[../feature-design.md](../feature-design.md) · 队列:[P2-BACKLOG.md](P2-BACKLOG.md)
> 竞品对照:测测星座体系(本命排盘 · 运势大全 · 连线合盘)
---
## 1. 功能定义
| 字段 | 内容 |
|---|---|
| Name | 星座(StarProfile+ 合盘(Synastry |
| Purpose | 用生日(可选出生时/出生地)生成本命排盘、相位、日周月年/一生运势、行运速览;双人合盘(比较/组合/时空/马克斯/配对 + 次限推运)与恋爱/友情/婚姻指数 |
| Business Goal | P2 探索供给;分享裂变;深度版/会员转化 |
| In | Out |
|---|---|
| 圆形本命盘 + 宫位 + 相位预览 | 占卜/算命恐吓话术 |
| 日/周/月/年/一生运势 + 行运速览 | 医疗疗效承诺 |
| 合盘五主盘 + 三推运 + 三指数;附近/邀请 | 自称「测测」;达人市场/微信关系链拉取 |
| 多维 summary + 付费 detail | |
| 挂 ProfileGrowthReport `star` / `synastry` | |
---
## 2. 用户价值
1. **为何需要:** 用熟悉的星座语言认识自己的节奏、运势与互动偏好。
2. **完成后获得:** 本命轮盘、三大星座、行星/宫位、运势与行运;合盘页看五主盘、次限与三指数。
3. **为何付费:** 完整相位表、年运/一生深析、合盘相处长文、推运深文案与跨盘相位详解。
---
## 3. 用户角色
| Actor | 能力 |
|---|---|
| Visitor | 有档案可生成基础报告(轮盘 + 今日运势 + 合盘指数) |
| DeepAccess / VIP | 可见 detail(完整相位、年运/一生深析、合盘深文案) |
| 无档案 | Empty → 页内填生日(可选时/地) |
---
## 4. 用户流程
```text
/explore 或 /star
确认 Profilebirth_datebirth_time / birth_place 可选增强)
生成 Star 报告 → 星盘轮盘 · 运势 · 行星 · 行运
深度版?→ order deep_access / 会员 → detail
可选:/synastry 全盘型合盘 · /synastry/invite/:token · /relation · 问答
```
---
## 5. 页面设计
| 路由 | 页面 | 说明 |
|---|---|---|
| `/star` | StarProfilePage | 星盘(轮盘)/ 运势 / 行星 |
| `/synastry` | SynastryPage | 五主盘 Tab + 次限 + 附近/邀请 + 深度墙 |
| `/synastry/invite/:token` | SynastryInvitePage | 好友接受邀请并生成合盘 |
| `/reports/:id` | ReportPage | type=star / synastry 专用展示 |
| `/relation` | RelationPage | 人格匹配(行为风格,复用指数) |
| `/explore` | 入口 | 排盘 / 合盘 |
```text
/star
├── Empty(页内生日表单)
├── Loading
├── Normal(轮盘+运势+行运)
├── Locked(深度墙)
└── Error
```
---
## 6. 页面状态规范
| 状态 | UI |
|---|---|
| Loading | 生成中 |
| Empty | 页内填生日(可选时/地) |
| Error | 重试 |
| Normal | 轮盘 + 运势基础层 |
| Locked | 深度版 CTA |
---
## 7. Business Rules
| ID | Rule |
|---|---|
| R1 | 必须绑定用户可访问的 Profilebirth_date 必填 |
| R2 | 内容确定性:同档案输入 → 同报告(可版本化 seed) |
| R3 | summary 始终对所有者可读;detail 需 Membership **或** DeepAccess |
| R4 | **允许**运势分、吉凶分维文案、合盘缘分/匹配指数;禁止占卜/算命恐吓与疗效承诺 |
| R5 | 太阳/月亮/上升/行星标签须伴随探索免责(非宿命断言恐吓) |
| R6 | 与个人画像并存:画像偏行为模式;本模块偏星座星盘语言 |
| R7 | 文案过 lexicon;UI 主标题「星座」/「合盘」 |
| R8 | 合盘与人格匹配分流:`/synastry` 盘型;`/relation` 行为风格 |
| R9 | 附近的人默认 `geo_visible=false`;仅可见已开启位置的 self 档案 |
| R10 | 合盘星历走 Swiss Ephemeris(默认 Moshier);宫位制 Whole Sign |
---
## 8. 数据模型影响
| 表/字段 | 变更 |
|---|---|
| `growth_reports.type` | `star` · `synastry` |
| `profiles.birth_time` | 可选参与上升/宫位 |
| `profiles.birth_place` | 可选;常用城市表解析经纬度 |
| `profiles.geo_lat/lng/visible` | 附近的人 |
| `synastry_invites` | 邀请 token · host/guest · report_id |
| 报告 JSON | `charts.*`(五主盘+推运)· `as_of` · 三指数 · fortune/transits |
详见 erd;实现切片同步 OpenAPI。
---
## 9. API 需求
| Method | Path | 意图 |
|---|---|---|
| POST | `/api/v1/reports/star` | body: `profile_id` → GrowthReport |
| POST | `/api/v1/reports/synastry` | body: `profile_id_a` + `profile_id_b` + optional `as_of` |
| GET | `/api/v1/synastry/nearby` | lat/lng/radius_km → 可合盘对象 |
| POST | `/api/v1/synastry/invites` | 生成邀请 |
| GET | `/api/v1/synastry/invites/{token}` | 邀请元数据 |
| POST | `/api/v1/synastry/invites/{token}/accept` | 接受并生成报告 |
| GET | `/api/v1/reports/{id}` | 权益裁剪(复用) |
| POST | `/api/v1/orders` + pay-mock | 深度版(复用) |
---
## 10. 权限设计
| 能力 | Visitor+档案 | DeepAccess/VIP |
|---|---|---|
| 轮盘 / 日运 / 行运 / 合盘三指数 / 各盘缩略 | ✓ | ✓ |
| 完整相位 / 年运与一生深析 / 合盘相处长文 / 推运深文案 | ✗ | ✓ |
---
## 11. 埋点
| Event | 触发 |
|---|---|
| `star_completed` / portrait_completed source=star | 基础结果展示 |
| `star_wheel_viewed` | 点选轮盘行星 |
| `synastry_completed` | 合盘结果展示 |
| `synastry_invite_created` / `synastry_invite_accepted` | 邀请链路 |
| `synastry_nearby_opened` | 附近面板 |
| `deep_access_clicked` | surface=star / synastry |
---
## 12. 测试验收标准
**Given** Self 有生日
**When** 生成星座报告
**Then** 展示轮盘、太阳/月亮/上升、相位速览、fortune 日运与行运;无「占卜」「算命」恐吓
**Given** 两份档案
**When** POST synastry
**Then** summary 含三指数与 `charts` 五主盘 + 三推运 key;detail 无权益时剥离
**Given** 邀请 token
**When** accept
**Then** 生成 synastry 报告
**Given** 无权益
**When** 请求 detail
**Then** Server 剥离
**Given** 无档案
**When** 打开 `/star`
**Then** 页内生日表单可生成
---
## 13. AI 开发前检查
- [x] Spec 齐全
- [x] lexicon 已登记 StarProfile
- [x] OpenAPI 切片(synastry
- [x] §12 可测
---
## 14. Implementation Notes
| 项 | 内容 |
|---|---|
| 复用 | ReportRich · DeepAccess · NatalWheel · Explore |
| Packages | `internal/star` · `ephemeris` · `natal` · `fortune` · `synastry` |
| 星历 | Swiss EphemerisCGO,默认 Moshier);热带 + Whole Sign |
| 实现 | `POST /reports/star` · `POST /reports/synastry` · nearby/invites · H5 `/star` · `/synastry` · invite |
+45 -60
View File
@@ -1,101 +1,86 @@
# 愈心谷产品语言契约(Mandatory)
所有 PRD、UI 文案、运营文案、Ask 引导语、错误提示必须遵守。
竞品分析(`cece-feature-map.md`)可保留竞品原名;**用户可见文本不得出现禁用词。**
---
## Product Lexicon Rules
1. **所有用户可见文本必须来自本文件的推荐词或「核心命名」表。**
2. AI 生成文案前必须先读本文件;不得按公开竞品习惯自动套词
3. 数据库注释、OpenAPI `summary`/`description`(对外)、H5/小程序文案同规则。
4. 代码标识符用英文域词(见下表);中文 UI 用「用户名」列。
5. 不确定时:**ASK**,不要发明近义词(如「开运」「解盘」)
1. **硬禁止**仅限下列词;其余品类词(解码、星座、运势、合盘、匹配等)**允许**用于产品主文案。
2. AI 生成文案前必须先读本文件。
3. 数据库注释、OpenAPI、H5/小程序文案同规则。
4. 代码标识符用英文域词;中文 UI 用「用户名」列。
5. 不确定是否踩「占卜/算命」红线时:**ASK**。
### 禁止(含近义
### 禁止(硬红线
- 运势 / 今日运势 / 开运
- 吉凶 / 祸福 / 命中注定
- 预测未来 / 预知 / 改命
- 算命 / 解盘 / 合婚(恐吓或宿命叙事)
- 命盘(作核心产品名)/ 合盘 / 缘分合盘
- 解码 / 愈心解码(产品主文案)
- 解锁(付费墙主按钮文案;技术日志可用 deep_access
- 治疗 / 改善疾病 / 疗效承诺
- 塔罗(主路径入口)
- **占卜** / 找大师算卦 / 神棍恐吓话术
- **算命** / 改命恐吓 / 「不测就有灾」类恐吓营销
- 医疗疗效承诺(治疗 / 改善疾病)
- 以「测测」自称或对用户说「像某某 App」
- 虚假倒计时、道德绑架式付费话术
### 推荐
### 允许(示例)
- 了解 · 探索 · 分析 · 建议
- 关系理解 · 成长方向 · 成长报告 · 深度版
- 个人档案 · 个人画像 · 探索测试 · 探索结果
- AI 成长助手 · 心情记录 · 成长记录 · 成长会员
- 生活建议 / 今日建议(替代运势)
- 愈心解码 / 解码 / 个人画像
- 星座 / 星盘 / 星象性格 / 太阳·月亮·上升
- 今日运势 / 周运(探索向生活建议即可)
- 人格匹配 / 合盘 / 缘分指数(温和探索向,非恐吓)
- 塔罗可作为意象卡片别名
- 解锁 / 深度版 / 成长会员
### 推荐主路径文案
- **愈心解码** · **星座** · **人格匹配** · AI 成长助手
- 成长报告 · 深度版 · 个人档案 · 探索测试(次要)
- 身心节律 · 意象卡片 · 节气生活
---
## 定位一句话
愈心谷 = 通过数字性格、身心特征、关系理解与 AI 陪伴,帮助用户更好地认识自己、理解他人的个人成长平台。
愈心谷 = 通过愈心解码、星座探索、人格匹配与 AI 陪伴,帮助用户更好地认识自己、理解他人的个人成长平台。
---
## 技术名 → 用户名(冻结)
## 技术名 → 用户名
| 技术名 | 用户名 | 说明 |
|---|---|---|
| Profile | 个人档案 | Self / Other |
| Portrait | 个人画像 | 性格探索产出的成长画像 |
| Scale | 探索测试 | 含 MBTI 类等 |
| ScaleResult / Result | 探索结果 | 测评产出 |
| GrowthReport / Report | 成长报告 | 可含基础版/深度版字段 |
| RelationInsight | 关系理解 | 双人差异与相处建议 |
| Ask | AI成长助手 | Tab 文案可用「问答」 |
| Memory | 成长记录 | P2/P3 跨会话沉淀 |
| Portrait | 愈心解码 | 生日生成的性格解码报告 |
| Scale | 探索测试 | 次要;含 MBTI 类等 |
| ScaleResult / Result | 探索结果 | |
| GrowthReport / Report | 成长报告 | 基础版/深度版 |
| RelationInsight | 人格匹配 | 可用合盘副标 |
| Ask | AI成长助手 | Tab「问答」 |
| Memory | 成长记录 | |
| Mood | 心情记录 | |
| SolarTerm | 节气生活 | |
| Membership | 成长会员 | |
| DeepAccess | 深度版 | 单次完整分析 |
| Constitution | 身心探索 | 体质倾向等,非医疗 |
| DeepAccess | 深度版 | 亦可「解锁完整分析 |
| Constitution | 身心探索 | |
| StarProfile | 星座 | 副文案可用排盘 / 星盘 / 运势 |
| Synastry | 合盘 | 五主盘+推运;与人格匹配分流 |
| LifeRhythm | 身心节律 | |
| ImageCard | 意象卡片 | 可并列塔罗别名 |
**Deprecated code aliases勿用于新 UI):** DecodePortrait · MatchRelationInsight · Unlock→DeepAccess
---
## 禁用 → 使用(速查)
| 不使用 | 使用 |
|---|---|
| 解码 | 性格探索 / 个人画像 |
| 测算 | 分析 |
| 测试结果(主文案) | 探索结果 |
| 运势 | 生活建议 / 今日建议 |
| 命盘 / 星盘档案(核心) | 个人画像 |
| 合盘 | 关系理解 |
| 解锁(主按钮) | 查看深度版 / 完整分析 |
| 算命 | 自我探索 |
| 预测未来 | 了解趋势 / 认识模式 |
| 改命 | 成长改善 |
| 会员(若需品牌感) | 成长会员(权益页标题优先) |
星盘:探索内可选维度,禁止作家园主标题/品牌 slogan。
**Code aliases可用):** DecodePortrait · MatchRelationInsight · Zodiac≈StarProfile
---
## Ask 人设
- 是:了解你的智能伙伴
- 不是:占卜、预测、算命
- **不是:占卜师、算命先生**
场景:认识自己 · 理解关系 · 职业探索 · 情绪整理 · 生活建议
场景:认识自己 · 理解关系 · 职业探索 · 情绪整理 · 生活建议 · 星座聊聊
---
## 付费墙话术
- 免费:基础画像 / 基础探索结果
- 付费:完整分析 · 行为模式 · 关系建议 · 成长方向**深度版**
- 订阅标题**成长会员**
- 禁止:虚假倒计时、恐吓、道德绑架
- 免费:基础解码 / 基础星座 / 基础匹配
- 付费:完整分析 → **深度版** / 解锁完整分析
- 订阅:**成长会员**
- 禁止:恐吓、道德绑架、虚假倒计时
+65
View File
@@ -0,0 +1,65 @@
# P1 Status(对照 Definition of Done
**当前标签:`P1 Complete`**
判定依据:`.ai/definition-of-done.md` §9
P1 必做(feature-map):用户档案 · 性格探索/个人画像 · 人格测评 · 关系理解 · AI 问答 · 成长报告深度版 · 会员系统
**不含:** 陪伴深化、星象性格/身心节律/意象卡片(P2)、真支付网关。
---
## Review Report
- Feature: P1 必做全集收口
- Scope label: **P1 Complete**
- Architecture: PASS — handler → service → repository;引擎独立包
- API: PASS — `proto/openapi.yaml` 覆盖主路径;统一信封
- Security: PASS — detail 服务端按 Membership/DeepAccess 裁剪;归属校验
- Test (L0/L1/L2/L3): PASS
- L0: `go test ./...` · `npm run build:h5`
- L1: portrait / relation / scale / ask / analytics / scaleDraft
- L2: `internal/integration` — Portrait / Relation / Membership / Ask
- L3: Playwright `e2e/portrait-main-path.spec.ts`
- Frontend states (L/E/E/N): PASS — 主路径页具备加载/空/错/正常;付费墙 Locked
- Documentation: PASS — Feature Spec 层 + P1 走查 + lexicon
- Environment: PASS — `commands.md` 本机 Go/Vite + compose.dev 仅 DB
- Known Issues:
- 支付为 mock(P1 接受)
- 陪伴为节气壳(P2,不挡 P1)
- GA 需配置 `VITE_GA_MEASUREMENT_ID` 才上报
- Ask 无 key 时规则引擎降级
---
## 本轮收口变更
| 项 | 结果 |
|---|---|
| Feature Spec 门禁 | 已接入;P1 Spec Active |
| 埋点最小集 | analytics Spec + `track` |
| 探索测试草稿恢复 | `scaleDraft` localStorage |
| L0L3 | 全绿(收口时) |
P2 设计队列:[`feature-spec/P2-BACKLOG.md`](feature-spec/P2-BACKLOG.md)
---
## 本地启动 / 复验
```bash
cp apps/api/config.example.yaml apps/api/config.local.yaml
# 可选:deepseek.api_key · apps/user-h5/.env.local 中 VITE_GA_MEASUREMENT_ID
npm run deps:up
cd apps/api && go run ./cmd/server
npm run dev:h5
```
```bash
npm run test:api
npm run test:api:integration
npm run test:h5
npm run build:h5 && npm run test:e2e
```
验收路径:首页画像 → 深度版 → 分享 → 关系 → 会员 → 问答 → 探索(含刷新恢复)→ 我的成长报告
+14 -3
View File
@@ -10,7 +10,9 @@ IA Tab 冻结:首页 · 探索 · 问答 · 陪伴 · 我的
```
/ HomePage 发现入口 · 建档/探索 CTA
/explore ExplorePage 探索测试 · 入口聚合
/explore ExplorePage 探索 L1 分类 Hub
/explore/:category ExploreCategoryPage L2/L3 工具列表
/growth-plan GrowthPlanPage 成长计划打卡
/ask AskPage AI成长助手
/companion CompanionPage 节气生活 · 心情记录
/mine MinePage 资产总览 → profile/reports/membership
@@ -19,7 +21,16 @@ IA Tab 冻结:首页 · 探索 · 问答 · 陪伴 · 我的
/relation RelationPage 关系理解
/membership MembershipPage 成长会员
/scales/:slug ScalePage 探索测试作答(P1 建)
/reports/:id ReportPage 成长报告详情(P1 建
/share SharePage 分享卡落地(query 载荷
/reports ReportsPage 成长报告列表
/reports/:id ReportPage 成长报告详情
/star StarProfilePage 星座
/synastry SynastryPage 合盘(五主盘+推运)
/synastry/invite/:token SynastryInvitePage 合盘邀请落地
/rhythm LifeRhythmPage 身心节律
/cards ImageCardPage 意象卡片
/growth-plan GrowthPlanPage 成长计划打卡
```
Redirect only: `/decode``/portrait`
@@ -30,7 +41,7 @@ Redirect only: `/decode` → `/portrait`
| Tab 内 | 二级(无底栏或隐藏底栏) |
|---|---|
| `/` `/explore` `/ask` `/companion` `/mine` | `/profile` `/portrait` `/relation` `/membership` `/scales/*` `/reports/*` |
| `/` `/explore` `/ask` `/companion` `/mine` | `/profile` `/portrait` `/relation` `/membership` `/share` `/scales/*` `/reports` `/reports/*` `/explore/:category` `/growth-plan` `/star` `/synastry` `/synastry/invite/*` `/rhythm` `/cards` |
---
+14 -7
View File
@@ -1,18 +1,25 @@
# Prompt: New Feature
Load first: `.ai/constitution.md`, `.ai/architecture.md`, `.ai/coding.md`, `.ai/api.md`, `.ai/review.md`, `.ai/definition-of-done.md`.
Load first: `.ai/constitution.md`, `.ai/architecture.md`, `.ai/coding.md`, `.ai/api.md`, `.ai/review.md`, `.ai/definition-of-done.md`, **`.ai/product/feature-design.md`**.
## Process
0. **Feature Spec(强制)**
- 确认 `feature-map` 节点与分期。
- 若无 Spec:复制 `product/feature-spec/_TEMPLATE.md``<id>.md`,填满 §1–§13;登记 `feature-spec/README.md`
- 核对 §4 Flow / §7 Rules / §9 API / §12 Acceptance 非空;文案过 lexicon。
- Playbook: `playbooks/feature-spec.md`
1. Restate the feature in one sentence and list files you will touch.
2. Follow feature flow: API → Service → Repository → Migration → SDK/types → UI → Test → Docs.
3. Implement the smallest vertical slice that works end-to-end.
4. Run builds/tests for touched sides.
5. Output `.ai/review.md` Review block.
6. Confirm Definition of Done.
2. Align OpenAPI / erd with Spec §8/§9;更新 Spec §14。
3. Follow feature flow: API → Service → Repository → Migration → SDK/types → UI → Test → Docs.
4. Implement the smallest vertical slice that satisfies Spec §12.
5. Run builds/tests for touched sides.
6. Output `.ai/review.md` Review block;勾选 `checklists/feature.md`.
7. Confirm Definition of Done(含 Feature Spec 项)。
## Forbidden
- Inventing APIs not in the task / OpenAPI.
- Coding from a one-line ask without Feature Spec.
- Inventing APIs not in Spec / OpenAPI.
- Editing legacy root HTML unless migration is the task.
- Shipping UI-only unlock/payment without server checks.
+3 -1
View File
@@ -1,6 +1,8 @@
# AI Self-Review — mandatory before claiming Done
After coding, check every item. Output a Review block.
After coding, check every item.
**Canonical gate:** `.ai/definition-of-done.md` — output **Review Report** (§7) when claiming Done.
Below checklist may be used as detail; labels: Demo | Feature Complete | P1 Complete.
## Checklist
+46 -9
View File
@@ -1,22 +1,59 @@
# Testing — Golden Rules
对照完成闸门:`.ai/definition-of-done.md` §4。
## Priority
Test money, scoring, auth, and unlock paths first.
先测:计分、权益(深度版/会员)、归属、支付 mock。
## Levels
| Level | Command / artifact | When |
|---|---|---|
| L0 Build | `go test ./...` · `npm run build:h5` | 每次声称可合并 |
| L1 Unit | Go engine `_test.go`H5 Vitest | 核心逻辑必有 |
| L2 Integration | `npm run test:api:integration`(需 Postgres | P1 关键流必有 |
| L3 E2E | `npm run test:e2e`Playwright,可用系统 Chrome | P1 收口至少 1 条 |
## Go
- Pure engines (decode / scale scoring) must have unit tests.
- `go test ./...` must pass before claiming API done.
```bash
npm run deps:up
cd apps/api && go test ./...
# 仅 L2
npm run test:api:integration
```
- Pure enginesportrait / relation)必须有 unit tests。
- L2 在 DB 不可用时 `Skip`;有 DB 时必须绿。
- `go test ./...` 失败 → 不得称 Backend Done。
### L2 覆盖的流
1. Profile → Portrait → deep_access pay-mock → detail
2. Two profiles → Relation insight → deep_access → tips
3. Membership pay-mock → `/membership/me` active → report detail
4. Profile → Ask thread → message → assistant reply + quota
H5 Vitest`PortraitPage` / `RelationPage` / `MembershipPage` / `AskPage`
## H5
- Critical utils may use Vitest when introduced.
- Until then: PR must list manual smoke steps for touched flows.
```bash
npm run test:h5 # Vitest
npm run build:h5 && npm run test:e2e # Playwright(默认 channel=chrome
```
## Definition for AI
- 关键页:`PortraitPage` / `RelationPage` / `MembershipPage` 有 Vitest。
- Playwright 画像主路径使用 network mock,不依赖 Go。
- 若无系统 Chrome`npx playwright install chromium` 后设 `PW_CHANNEL=` 空或删 channel。
- **仅 build 通过 ≠ Frontend Done。**
Do not mark a feature Done if:
## AI
- Scorable logic has zero tests, or
- Build is broken (`go test` / `npm run build:h5` fail).
不得在下列情况标记 Done
- 可计分逻辑零测试
- Build 红
- 无 L2 却宣称 P1 Complete
- 前端无 Loading/Empty/Error 却宣称页面 Done
+17 -12
View File
@@ -6,25 +6,30 @@
Receive task
1. Understand
- feature-map / user-journey / lexicon(产品)
- feature-map / lexicon / user-journey / page-tree(产品)
- **feature-design.md + feature-spec/<id>.md**(功能详细设计;缺失则先补)
- domain-map / erd / OpenAPI(数据与契约)
- page-tree(路由)
2. Design(若有缺口
- Update OpenAPI / erd / migration 草案
2. Feature Spec(强制
- 新功能或重大行为:复制 _TEMPLATE → 填满 §1–§13
- 对照 Spec §4/§6/§7/§9/§12playbook: feature-spec
3. Design(技术契约)
- Update OpenAPI / erd / migration 草案(对齐 Spec §8/§9
- ADR if stack/API style changes
- 更新 Spec §14 Implementation Notes
3. Implement
4. Implement
- Backendservice 竖切)→ Frontend
- Follow patterns + playbooks
4. Verify
- commandstest / health / build:h5
- review.md
- DoD + checklist
5. Verify
- Spec §12 Acceptance + definition-of-done.md(非仅 build
- commandsL0/L1/L2 as required
- Review Report + checklist
5. Document
- OpenAPI / erd / feature 标记若行为变化
6. Document
- OpenAPI / erd / Spec Gaps / p1-status 标签若变化
CommitConventional Commitsone concern
```
@@ -45,4 +50,4 @@ CommitConventional Commitsone concern
## Playbooks
Prefer: `add-api` · `new-page` · `new-table` · `payment` · `login`
Prefer: `feature-spec` · `add-api` · `new-page` · `new-table` · `payment` · `login`
+46
View File
@@ -0,0 +1,46 @@
---
description: Android emulator/device automation via ADB — screenshots, UI dump, gestures, APK install, CeCe validation capture
alwaysApply: false
---
# Android ADB Automation
Prefer the local command layer (symlink in this repo):
```bash
./tools/android ... --json
```
Upstream skill pack: `/Users/jack/tools/android-adb-skill/`
Command contract: `tools/android-adb-docs/command-contract.md`
Task skills: `~/.claude/skills/android-*/SKILL.md`(已 symlink
## Environment
```bash
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export PATH="$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
```
AVD name: `YuXinGu_API34`
```bash
emulator -avd YuXinGu_API34 -netdelay none -netspeed full &
adb wait-for-device
./tools/android device list --json
```
## CeCe / 愈心谷校验采集
按 `.ai/product/feature-spec/cece-frontend-re/complete-design/99-emulator-validation.md`
- 证据目录:`.tmp/cece-validation/YYYYMMDD/V-xxx/`
- 截图:`./tools/android screenshot --out .tmp/cece-validation/.../screen.png --json`
- 点击:先 `ui dump` / `ui find`**禁止**凭截图像素猜坐标
## Defaults
- Prefer `./tools/android ... --json` over raw `adb`
- Pass `--device <id>` when multiple devices
- Verify with `ui dump`, `wait element`, or `screenshot`
+1
View File
@@ -0,0 +1 @@
../../.ai/definition-of-done.md
+5
View File
@@ -23,5 +23,10 @@ apps/api/tmp/
*.db
.scale_results.db
# Local emulator validation + android-adb skill symlinks
.tmp/
tools/android
tools/android-adb-docs
# OS
Thumbs.db
+17 -12
View File
@@ -11,26 +11,30 @@ This file is for AI agents.
3. `.ai/architecture.md`
4. `.ai/domain.md` (+ product/API tasks: `domain/domain-map.md`)
5. `.ai/forbidden.md`
6. `.ai/environment.md` + `.ai/development.md`(本地)· 部署任务再读 `deployment.md` / `docker.md`
7. `.ai/file-map.md` + `.ai/workflow.md`
8. Task rules: `.ai/coding.md` / `api.md` / `database.md` / `ui.md` / `security.md` / …
9. **Product scope:** `.ai/product/lexicon.md` + `feature-map.md` + `user-journey.md` + `page-tree.md`(冻结见 `ENGINEERING-FREEZE.md`
10. **Any UI work:** `.ai/design/design-system.md` + `component-catalog.md` + matching `design/platform/*.md` (H5 default)
11. Matching **ADR** in `.ai/adr/` before changing stack or API style
12. Prefer **patterns/** + **examples/** + **playbooks/** over inventing structure
13. Before Done: `.ai/review.md` + `.ai/definition-of-done.md` + `.ai/checklists/*`
14. Verify via `.ai/commands.md`(本地默认本机 Go/Vite + compose.dev 仅 DB
15. `prompts/` are optional helpers — not a substitute for rules above
6. `.ai/definition-of-done.md`**完成标准(强制)**Done ≠ build 通过
7. `.ai/environment.md` + `.ai/development.md`(本地)· 部署任务再读 `deployment.md` / `docker.md`
8. `.ai/file-map.md` + `.ai/workflow.md`
9. Task rules: `.ai/coding.md` / `api.md` / `database.md` / `ui.md` / `security.md` / `testing.md`
10. **Product scope:** `.ai/product/lexicon.md` + `feature-map.md` + **`feature-design.md`** + 对应 **`feature-spec/<id>.md`** + `user-journey.md` + `page-tree.md` · 状态 `product/p1-status.md`
11. **Competitor parity / reverse engineering:** `.ai/design/reverse-engineering-spec.md`(强制 STEP 118 · Evidence First · 禁止 Demo 式省略)→ 再映射 lexicon / feature-spec
12. **Any UI work:** `.ai/design/design-system.md` + `component-catalog.md` + matching `design/platform/*.md` (H5 default)
13. Matching **ADR** in `.ai/adr/` before changing stack or API style
14. Prefer **patterns/** + **examples/** + **playbooks/**(含 `feature-spec` over inventing structure
15. Before claiming Done: `.ai/definition-of-done.md` + `.ai/review.md` + `.ai/checklists/*` → 输出 **Review Report**
16. Verify via `.ai/commands.md`(本地默认本机 Go/Vite + compose.dev 仅 DB
17. `prompts/` are optional helpers — not a substitute for rules above
## Hard constraints
- Never violate architecture or Accepted ADRs.
- Never invent APIs, DB tables, or domain synonyms.
- Never guess requirements — **ASK FIRST**.
- **Never** implement new feature behavior or expand an existing feature without an Active `product/feature-spec/<id>.md`(见 `product/feature-design.md`)。无 Spec 时只许明确 bugfix,或先写/补 Spec。
- Never modify files outside the current task.
- New code only under `apps/` and `packages/` (unless legacy migration task).
- One concern per change set. Function ≤50 lines. File ≤400 lines.
- Print the Review block when a coding task finishes.
- **Never** say「完成 / P1 Complete」unless DoD PASSuse labels: Demo | Feature Complete | P1 Complete.
- Print **Review Report**(见 `definition-of-done.md` §7when a coding task finishes.
- **Never** make Docker-only local coding the default (see `.ai/environment.md`).
## Cursor
@@ -44,4 +48,5 @@ Do not invent Phase 2/3 folders (graph/mcp/skills/anti-patterns/…) unless `ROA
## Product one-liner
档案 → 性格探索/个人画像 → 关系理解 → 问答陪伴 → 会员/深度版 →(后)顾问。
语言契约:`.ai/product/lexicon.md`参考竞品模型,不用竞品品牌词与运势/解码叙事。
语言契约:`.ai/product/lexicon.md`
对标竞品:按 `design/reverse-engineering-spec.md` 做完整逆向;落地用愈心谷命名,不用竞品品牌词与恐吓话术。
+3 -2
View File
@@ -6,8 +6,9 @@
1. Read [AGENTS.md](AGENTS.md) (load order is authoritative).
2. Especially: `.ai/ai-contract.md`, `constitution.md`, `architecture.md`, `domain.md`, `forbidden.md`.
3. Prefer `patterns/` + `examples/` + `playbooks/` over free-form invention.
4. Finish with Review + DoD + matching checklist.
3. Competitor parity / reverse engineering: `.ai/design/reverse-engineering-spec.md` (not a short prompt — full STEP 118).
4. Prefer `patterns/` + `examples/` + `playbooks/` over free-form invention.
5. Finish with Review + DoD + matching checklist.
## Layout
+17
View File
@@ -0,0 +1,17 @@
# 愈心谷 API
Go + Gin + Postgres。本命/合盘星历依赖 **Swiss Ephemeris**`github.com/tejzpr/go-swisseph`**AGPL-3.0**)。
## 构建要求
- Go 1.22+
- **CGO_ENABLED=1** 与 C 编译器(clang/gcc
- 默认使用 **Moshier** 星历(无需外挂 ephe 文件)
- 可选:设置 `SE_EPHE_PATH` / `EPHEMERIS_PATH` 指向 Swiss Ephemeris 数据目录以启用更高精度(`FlagSwieph`
```bash
CGO_ENABLED=1 go test ./...
CGO_ENABLED=1 go run ./cmd/server
```
商业部署须遵守 Swiss Ephemeris 许可(AGPL 或 Astrodienst 专业授权)。
+7 -37
View File
@@ -12,14 +12,7 @@ import (
"github.com/yuxingu/digital-psychology/apps/api/internal/config"
"github.com/yuxingu/digital-psychology/apps/api/internal/db"
"github.com/yuxingu/digital-psychology/apps/api/internal/handler"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/profile"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/relation"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/report"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/scale"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
"github.com/yuxingu/digital-psychology/apps/api/internal/httpserver"
)
func main() {
@@ -48,35 +41,12 @@ func main() {
os.Exit(1)
}
profileRepo := &repository.ProfileRepo{Pool: pool}
reportRepo := &repository.ReportRepo{Pool: pool}
relationRepo := &repository.RelationRepo{Pool: pool}
profileSvc := &profile.Service{Repo: profileRepo}
reportSvc := &report.Service{Profiles: profileRepo, Reports: reportRepo}
relationSvc := &relation.Service{Profiles: profileRepo, Reports: reportRepo, Relations: relationRepo}
scaleSvc := &scale.Service{Repo: &repository.ScaleRepo{Pool: pool}, Profiles: profileRepo}
r := gin.New()
r.Use(gin.Recovery(), gin.Logger(), middleware.RequestID())
r.Use(func(c *gin.Context) {
c.Header("Access-Control-Expose-Headers", "X-Device-Key, X-Request-Id")
c.Next()
})
api := r.Group("/api/v1")
handler.NewHealthHandler().Register(api)
api.GET("/ping", func(c *gin.Context) {
response.OK(c, gin.H{"pong": true})
})
authed := api.Group("")
authed.Use(middleware.DeviceAuth(pool))
(&handler.ProfileHandler{Svc: profileSvc}).Register(authed)
(&handler.ReportHandler{Svc: reportSvc}).Register(authed)
(&handler.RelationHandler{Svc: relationSvc}).Register(authed)
(&handler.ScaleHandler{Svc: scaleSvc}).Register(authed)
r := httpserver.NewRouter(pool, cfg)
if cfg.DeepSeek.Enabled() {
log.Printf("ask: deepseek enabled model=%s", cfg.DeepSeek.Model)
} else {
log.Printf("ask: deepseek disabled (set deepseek.api_key in config.local.yaml) — using rule engine")
}
log.Printf("yuxingu api listening on %s env=%s", cfg.HTTPAddr, cfg.AppEnv)
if err := r.Run(cfg.HTTPAddr); err != nil {
log.Printf("server stopped: %v", err)
+25
View File
@@ -0,0 +1,25 @@
# Copy to config.local.yaml and fill secrets.
# config.local.yaml is gitignored (*.local). Never commit real keys.
app:
env: dev
http_addr: ":8080"
database:
host: 127.0.0.1
port: 5432
user: yuxingu
password: yuxingu
name: yuxingu
sslmode: disable
deepseek:
api_key: "" # required for real Ask replies
base_url: "https://api.deepseek.com"
model: "deepseek-chat"
timeout_sec: 60
# jwt:
# secret: ""
# payment:
# mock: true
+2 -1
View File
@@ -6,6 +6,8 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.2
github.com/tejzpr/go-swisseph v1.0.2
gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -40,5 +42,4 @@ require (
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+2
View File
@@ -76,6 +76,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tejzpr/go-swisseph v1.0.2 h1:S/uO8aPcWjORJmqYydDxBx5h/LUhzxyqLVEMFt8QC1Q=
github.com/tejzpr/go-swisseph v1.0.2/go.mod h1:LPZ5M7ejE7+mBzaow3ACezJfsjgRCsQ/LZrz97LDXSQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+231
View File
@@ -0,0 +1,231 @@
// Package ask builds deterministic growth-assistant replies (rule fallback).
// Copy follows .ai/product/lexicon.md — companion for self-understanding, not fortune-telling.
package ask
import (
"fmt"
"strings"
"time"
"github.com/yuxingu/digital-psychology/apps/api/internal/portrait"
)
// ReplyInput is context for a single assistant reply.
type ReplyInput struct {
DisplayName string
BirthDate time.Time
Relation string // self | other
Scene string
UserMessage string
}
// BuildReply returns a profile-aware assistant message with actionable detail.
func BuildReply(in ReplyInput) string {
name := in.DisplayName
if name == "" {
if in.Relation == "other" {
name = "TA"
} else {
name = "你"
}
}
out := portrait.Build(in.BirthDate, name)
style := str(out.Summary["style_label"])
if style == "" {
style = str(out.Summary["headline"])
}
one := str(out.Summary["one_liner"])
tip := str(out.Summary["life_tip"])
overview := str(out.Summary["overview"])
sceneHint := sceneLine(in.Scene, in.UserMessage)
focus := detectFocus(in.UserMessage)
sectionBody := sectionByFocus(out.Detail, focus)
scripts := strSlice(out.Detail["conversation_scripts"])
scriptLine := ""
if len(scripts) > 0 {
scriptLine = "可以试着说:「" + scripts[0] + "」"
}
plan := firstPlan(out.Detail["growth_plan"])
var body string
switch focus {
case "relation":
body = fmt.Sprintf(
"结合「%s」偏「%s」的档案:%s\n\n关系侧重点:%s\n\n小建议:%s\n%s\n本周可做:%s",
name, style, one, pick(sectionBody, "在关系里先复述对方感受,再表达需要。"), tip, scriptLine, pick(plan, tip),
)
case "career":
body = fmt.Sprintf(
"从「%s / %s」看职业节奏:%s\n\n%s\n\n可执行:%s\n本周:%s",
name, style, one, pick(sectionBody, overview), tip, pick(plan, "选一件能发挥你节奏优势的小事推进。"),
)
case "emotion":
body = fmt.Sprintf(
"我听到你在整理情绪。对照「%s」:%s\n\n%s\n\n调节建议:%s\n%s\n本周:%s",
style, one, pick(sectionBody, "先命名感受,再决定行动。"), tip, scriptLine, pick(plan, "给自己 10 分钟不评判地写下此刻感受。"),
)
case "life":
body = fmt.Sprintf(
"关于生活节奏(%s):%s\n\n%s\n\n今日建议:%s\n本周:%s",
style, one, pick(sectionBody, overview), tip, pick(plan, tip),
)
default:
body = fmt.Sprintf(
"我是愈心谷的成长助手,会结合档案陪你一起看。\n\n「%s」更偏「%s」:%s\n\n%s\n\n生活建议:%s\n%s\n本周可做:%s",
name, style, one, trimRunes(overview, 120), tip, scriptLine, pick(plan, tip),
)
}
disclaimer := "以上是自我探索与生活方式参考,不构成医疗或占卜预测。"
if sceneHint != "" {
return sceneHint + "\n\n" + strings.TrimSpace(body) + "\n\n" + disclaimer
}
return strings.TrimSpace(body) + "\n\n" + disclaimer
}
func sectionByFocus(detail map[string]any, focus string) string {
titleHint := map[string]string{
"relation": "关系",
"career": "事业",
"emotion": "情绪",
"life": "生活",
"self": "性格",
}[focus]
secs, ok := detail["sections"].([]map[string]any)
if !ok {
// also []any after some encodings
if raw, ok2 := detail["sections"].([]any); ok2 {
for _, item := range raw {
m, _ := item.(map[string]any)
if strings.Contains(str(m["title"]), titleHint) {
return str(m["body"])
}
}
}
switch focus {
case "relation":
return str(detail["relation_style"])
case "career", "self":
return str(detail["behavior_pattern"])
default:
return str(detail["growth_direction"])
}
}
for _, s := range secs {
if strings.Contains(str(s["title"]), titleHint) {
return str(s["body"])
}
}
if len(secs) > 0 {
return str(secs[0]["body"])
}
return ""
}
func firstPlan(v any) string {
arr, ok := v.([]map[string]any)
if ok && len(arr) > 0 {
return str(arr[0]["focus"])
}
raw, ok := v.([]any)
if ok && len(raw) > 0 {
if m, ok := raw[0].(map[string]any); ok {
return str(m["focus"])
}
}
return ""
}
func pick(a, b string) string {
if strings.TrimSpace(a) != "" {
return a
}
return b
}
func trimRunes(s string, n int) string {
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "…"
}
func strSlice(v any) []string {
if arr, ok := v.([]string); ok {
return arr
}
raw, ok := v.([]any)
if !ok {
return nil
}
out := make([]string, 0, len(raw))
for _, x := range raw {
if s, ok := x.(string); ok {
out = append(out, s)
}
}
return out
}
func sceneLine(scene, msg string) string {
s := strings.TrimSpace(scene)
if s == "" {
s = detectScene(msg)
}
switch s {
case "self":
return "场景:认识自己"
case "relation":
return "场景:理解关系"
case "career":
return "场景:职业探索"
case "emotion":
return "场景:情绪整理"
case "life":
return "场景:生活建议"
default:
return ""
}
}
func detectScene(msg string) string {
return detectFocus(msg)
}
func detectFocus(msg string) string {
m := strings.ToLower(msg)
switch {
case containsAny(m, "关系", "相处", "伴侣", "朋友", "沟通", "TA", "他", "她"):
return "relation"
case containsAny(m, "工作", "职业", "面试", "事业", "选择"):
return "career"
case containsAny(m, "情绪", "焦虑", "难过", "压力", "心情", "害怕"):
return "emotion"
case containsAny(m, "作息", "睡眠", "习惯", "生活", "饮食", "运动"):
return "life"
default:
return "self"
}
}
func containsAny(s string, words ...string) bool {
for _, w := range words {
if strings.Contains(s, w) {
return true
}
}
return false
}
func str(v any) string {
if v == nil {
return ""
}
if s, ok := v.(string); ok {
return s
}
return fmt.Sprint(v)
}
+40
View File
@@ -0,0 +1,40 @@
package ask
import (
"strings"
"testing"
"time"
)
func TestBuildReply_profileAware(t *testing.T) {
birth := time.Date(1990, 5, 12, 0, 0, 0, 0, time.UTC)
out := BuildReply(ReplyInput{
DisplayName: "我",
BirthDate: birth,
Relation: "self",
Scene: "self",
UserMessage: "我想更了解自己",
})
if !strings.Contains(out, "我") {
t.Fatalf("expected name in reply: %s", out)
}
if strings.Contains(out, "算命") || strings.Contains(out, "运势") || strings.Contains(out, "吉凶") {
t.Fatalf("forbidden lexicon in reply: %s", out)
}
if !strings.Contains(out, "不构成") {
t.Fatalf("expected disclaimer: %s", out)
}
}
func TestBuildReply_relationFocus(t *testing.T) {
birth := time.Date(1992, 8, 1, 0, 0, 0, 0, time.UTC)
out := BuildReply(ReplyInput{
DisplayName: "TA",
BirthDate: birth,
Relation: "other",
UserMessage: "和伴侣沟通总是卡住怎么办",
})
if !strings.Contains(out, "关系") && !strings.Contains(out, "相处") && !strings.Contains(out, "复述") {
t.Fatalf("expected relation-oriented reply: %s", out)
}
}
+58
View File
@@ -0,0 +1,58 @@
// Package companion provides solar-term tips and mood helpers.
package companion
import "time"
// SolarTerm is today's life tip (not fortune).
type SolarTerm struct {
Name string `json:"name"`
Tip string `json:"tip"`
Date string `json:"date"`
}
var terms = []struct {
Start int
Name string
Tip string
}{
{1, "小寒", "天冷宜温补作息,少熬夜,给身心留一点缓冲。"},
{15, "大寒", "注意保暖与情绪稳定,适合整理这一年想放下的事。"},
{32, "立春", "万物复苏,适合定一个小而清晰的成长目标。"},
{47, "雨水", "润物无声,试试每天记录一件让你安心的小事。"},
{62, "惊蛰", "能量回升,适合重启被搁置的习惯,别一次做太多。"},
{77, "春分", "昼夜均衡,留意工作与休息的平衡,沟通也留余地。"},
{92, "清明", "适合回顾与告别,把情绪说给信任的人或写下来。"},
{107, "谷雨", "播种季,把计划拆成可完成的一步即可。"},
{122, "立夏", "心气易浮,留出安静时段,喝水、慢走都有帮助。"},
{137, "小满", "不必求满,进度到八成就值得肯定自己。"},
{152, "芒种", "忙碌中记得停一下,问自己:此刻最需要什么?"},
{167, "夏至", "白昼最长,保护睡眠,午后可短暂休息恢复专注。"},
{183, "小暑", "炎热易烦躁,沟通前先降温自己的情绪。"},
{198, "大暑", "宜清淡饮食与适度运动,别用透支换效率。"},
{213, "立秋", "开始收敛节奏,复盘上半年,调整下阶段重心。"},
{228, "处暑", "暑气渐消,适合温和地恢复规律作息。"},
{243, "白露", "早晚温差大,关照身体的同时关照情绪波动。"},
{258, "秋分", "又一次平衡点,整理关系与边界,轻装前行。"},
{273, "寒露", "宜保暖与内观,少责备自己,多一点耐心。"},
{288, "霜降", "适合沉淀,读一点喜欢的内容,给心灵加温。"},
{303, "立冬", "进入收藏季,减少无效社交消耗,守护精力。"},
{318, "小雪", "天寒宜静,可用书写梳理焦虑与期待。"},
{333, "大雪", "慢下来也是前进,允许自己休息而不内疚。"},
{348, "冬至", "一阳初生,适合与亲近的人连结,表达感谢。"},
}
// TodaySolar returns approximate solar term by day-of-year (not ephemeris).
func TodaySolar(now time.Time) SolarTerm {
doy := now.YearDay()
cur := terms[0]
for _, t := range terms {
if doy >= t.Start {
cur = t
}
}
return SolarTerm{
Name: cur.Name,
Tip: cur.Tip,
Date: now.Format("2006-01-02"),
}
}
+20
View File
@@ -0,0 +1,20 @@
package companion
import (
"strings"
"testing"
"time"
)
func TestTodaySolar(t *testing.T) {
term := TodaySolar(time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC))
if term.Name == "" || term.Tip == "" {
t.Fatalf("empty term: %#v", term)
}
blob := term.Name + term.Tip
for _, bad := range []string{"算命", "占卜"} {
if strings.Contains(blob, bad) {
t.Fatalf("forbidden %q", bad)
}
}
}
+177 -12
View File
@@ -1,27 +1,192 @@
// Package config loads process configuration from environment variables.
// Package config loads process configuration from YAML + optional env overrides.
package config
import "os"
import (
"fmt"
"log"
"net/url"
"os"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
// Config holds runtime settings for the API server.
type Config struct {
HTTPAddr string
DatabaseURL string
AppEnv string
DeepSeek DeepSeekConfig
}
// Load reads configuration from the environment with safe defaults for local dev.
// DeepSeekConfig for Ask LLM.
type DeepSeekConfig struct {
APIKey string
BaseURL string
Model string
TimeoutSec int
}
type fileConfig struct {
App struct {
Env string `yaml:"env"`
HTTPAddr string `yaml:"http_addr"`
} `yaml:"app"`
Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
Name string `yaml:"name"`
SSLMode string `yaml:"sslmode"`
} `yaml:"database"`
DeepSeek struct {
APIKey string `yaml:"api_key"`
BaseURL string `yaml:"base_url"`
Model string `yaml:"model"`
TimeoutSec int `yaml:"timeout_sec"`
} `yaml:"deepseek"`
}
// Load reads config.local.yaml (or CONFIG_PATH), then applies env overrides.
func Load() Config {
return Config{
HTTPAddr: getenv("HTTP_ADDR", ":8080"),
DatabaseURL: getenv("DATABASE_URL", "postgres://yuxingu:yuxingu@127.0.0.1:5432/yuxingu?sslmode=disable"),
AppEnv: getenv("APP_ENV", "dev"),
cfg := Config{
HTTPAddr: ":8080",
DatabaseURL: "postgres://yuxingu:yuxingu@127.0.0.1:5432/yuxingu?sslmode=disable",
AppEnv: "dev",
DeepSeek: DeepSeekConfig{
BaseURL: "https://api.deepseek.com",
Model: "deepseek-chat",
TimeoutSec: 60,
},
}
path := resolveConfigPath()
if path != "" {
if err := mergeFile(&cfg, path); err != nil {
log.Printf("config: load %s: %v (using defaults/env)", path, err)
} else {
log.Printf("config: loaded %s", path)
}
} else {
log.Printf("config: no config.local.yaml found — copy config.example.yaml → config.local.yaml")
}
applyEnv(&cfg)
return cfg
}
func resolveConfigPath() string {
if p := os.Getenv("CONFIG_PATH"); p != "" {
return p
}
candidates := []string{
"config.local.yaml",
"apps/api/config.local.yaml",
}
for _, c := range candidates {
if st, err := os.Stat(c); err == nil && !st.IsDir() {
return c
}
}
return ""
}
func mergeFile(cfg *Config, path string) error {
raw, err := os.ReadFile(path)
if err != nil {
return err
}
var f fileConfig
if err := yaml.Unmarshal(raw, &f); err != nil {
return err
}
if f.App.Env != "" {
cfg.AppEnv = f.App.Env
}
if f.App.HTTPAddr != "" {
cfg.HTTPAddr = f.App.HTTPAddr
}
if f.Database.Host != "" || f.Database.User != "" || f.Database.Name != "" {
cfg.DatabaseURL = buildDatabaseURL(f)
}
if f.DeepSeek.APIKey != "" {
cfg.DeepSeek.APIKey = f.DeepSeek.APIKey
}
if f.DeepSeek.BaseURL != "" {
cfg.DeepSeek.BaseURL = strings.TrimRight(f.DeepSeek.BaseURL, "/")
}
if f.DeepSeek.Model != "" {
cfg.DeepSeek.Model = f.DeepSeek.Model
}
if f.DeepSeek.TimeoutSec > 0 {
cfg.DeepSeek.TimeoutSec = f.DeepSeek.TimeoutSec
}
return nil
}
func buildDatabaseURL(f fileConfig) string {
host := f.Database.Host
if host == "" {
host = "127.0.0.1"
}
port := f.Database.Port
if port == 0 {
port = 5432
}
user := f.Database.User
if user == "" {
user = "yuxingu"
}
pass := f.Database.Password
name := f.Database.Name
if name == "" {
name = "yuxingu"
}
ssl := f.Database.SSLMode
if ssl == "" {
ssl = "disable"
}
u := url.URL{
Scheme: "postgres",
User: url.UserPassword(user, pass),
Host: fmt.Sprintf("%s:%d", host, port),
Path: "/" + name,
}
q := u.Query()
q.Set("sslmode", ssl)
u.RawQuery = q.Encode()
return u.String()
}
func applyEnv(cfg *Config) {
if v := os.Getenv("HTTP_ADDR"); v != "" {
cfg.HTTPAddr = v
}
if v := os.Getenv("DATABASE_URL"); v != "" {
cfg.DatabaseURL = v
}
if v := os.Getenv("APP_ENV"); v != "" {
cfg.AppEnv = v
}
if v := os.Getenv("DEEPSEEK_API_KEY"); v != "" {
cfg.DeepSeek.APIKey = v
}
if v := os.Getenv("DEEPSEEK_BASE_URL"); v != "" {
cfg.DeepSeek.BaseURL = strings.TrimRight(v, "/")
}
if v := os.Getenv("DEEPSEEK_MODEL"); v != "" {
cfg.DeepSeek.Model = v
}
if v := os.Getenv("DEEPSEEK_TIMEOUT_SEC"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
cfg.DeepSeek.TimeoutSec = n
}
}
}
func getenv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
// Enabled reports whether DeepSeek can be called.
func (d DeepSeekConfig) Enabled() bool {
return strings.TrimSpace(d.APIKey) != ""
}
+102
View File
@@ -0,0 +1,102 @@
// Package explore provides the L1→L3 explore catalog.
package explore
// Item is a leaf tool in the catalog.
type Item struct {
Key string `json:"key"`
Title string `json:"title"`
Description string `json:"description"`
Path string `json:"path"`
Icon string `json:"icon"`
Tone string `json:"tone"`
Badge string `json:"badge,omitempty"`
}
// Category is an L1 explore bucket.
type Category struct {
Key string `json:"key"`
Title string `json:"title"`
Description string `json:"description"`
Icon string `json:"icon"`
Tone string `json:"tone"`
Items []Item `json:"items"`
}
// Catalog returns the full explore tree (deterministic, no DB).
// Order: 四核心优先,量表后置。
func Catalog() []Category {
return []Category{
{
Key: "decode", Title: "愈心解码", Description: "一个生日,读懂性格与节奏",
Icon: "△", Tone: "gold",
Items: []Item{
{Key: "portrait", Title: "愈心解码", Description: "生日生成性格解码报告", Path: "/portrait", Icon: "△", Tone: "gold", Badge: "热"},
},
},
{
Key: "star", Title: "星座", Description: "排盘 · 运势 · 合盘",
Icon: "✦", Tone: "night",
Items: []Item{
{Key: "star-report", Title: "本命排盘", Description: "圆形星盘 / 宫位 / 相位 / 运势", Path: "/star", Icon: "✦", Tone: "night", Badge: "热"},
{Key: "star-synastry", Title: "合盘", Description: "比较盘与恋爱/友情/婚姻指数", Path: "/synastry", Icon: "✧", Tone: "night", Badge: "AI"},
},
},
{
Key: "relation", Title: "人格匹配", Description: "双人风格对照 · 相处说明书",
Icon: "♡", Tone: "rose",
Items: []Item{
{Key: "relation-insight", Title: "人格匹配", Description: "行为风格对照与相处建议", Path: "/relation", Icon: "♡", Tone: "rose", Badge: "热"},
},
},
{
Key: "rhythm", Title: "身心节律", Description: "五行倾向与生活节奏建议",
Icon: "☯", Tone: "green",
Items: []Item{
{Key: "rhythm-report", Title: "身心节律报告", Description: "体质倾向与生活建议", Path: "/rhythm", Icon: "☯", Tone: "green"},
{Key: "companion-term", Title: "今日节气陪伴", Description: "季节生活小建议", Path: "/companion", Icon: "♡", Tone: "green"},
},
},
{
Key: "cards", Title: "意象卡片", Description: "投射反思,整理当下感受",
Icon: "◈", Tone: "teal",
Items: []Item{
{Key: "image-cards", Title: "抽取意象卡片", Description: "单卡或三卡组合练习", Path: "/cards", Icon: "◈", Tone: "teal"},
},
},
{
Key: "growth", Title: "情绪与成长", Description: "心情轨迹、成长计划与问答",
Icon: "○", Tone: "green",
Items: []Item{
{Key: "mood-trail", Title: "心情轨迹", Description: "近七日心情回顾", Path: "/companion", Icon: "○", Tone: "green"},
{Key: "growth-plan", Title: "成长计划", Description: "小目标与每日打卡", Path: "/growth-plan", Icon: "▽", Tone: "teal"},
{Key: "ask", Title: "AI 成长助手", Description: "结合档案整理感受", Path: "/ask", Icon: "◎", Tone: "gold"},
},
},
{
Key: "tests", Title: "更多测评", Description: "轻量量表(次要入口)",
Icon: "◆", Tone: "blue",
Items: []Item{
{Key: "mbti-lite", Title: "人格类型探索", Description: "能量与决策偏好(轻量)", Path: "/scales/mbti-lite", Icon: "◆", Tone: "blue"},
{Key: "enneagram-lite", Title: "动机模式探索", Description: "内在动机九型向轻测", Path: "/scales/enneagram-lite", Icon: "⑨", Tone: "purple"},
{Key: "bigfive-lite", Title: "性格五维探索", Description: "稳定特质速览", Path: "/scales/bigfive-lite", Icon: "◈", Tone: "teal"},
{Key: "love-style", Title: "亲密互动探索", Description: "亲密关系中的互动偏好", Path: "/scales/love-style", Icon: "♡", Tone: "rose"},
{Key: "eq-lite", Title: "情绪觉察探索", Description: "识别与调节情绪的习惯", Path: "/scales/eq-lite", Icon: "♥", Tone: "pink"},
{Key: "stress-index", Title: "压力负荷探索", Description: "近期压力与恢复方式", Path: "/scales/stress-index", Icon: "☯", Tone: "green"},
{Key: "career-interest", Title: "职业兴趣探索", Description: "工作动力与环境偏好", Path: "/scales/career-interest", Icon: "◎", Tone: "gold"},
{Key: "communication-style", Title: "沟通方式探索", Description: "表达与倾听偏好", Path: "/scales/communication-style", Icon: "◆", Tone: "blue"},
{Key: "emotion-pattern", Title: "情绪模式探索", Description: "情绪起伏与自我照顾", Path: "/scales/emotion-pattern", Icon: "♥", Tone: "pink"},
},
},
}
}
// CategoryByKey returns one category or nil.
func CategoryByKey(key string) *Category {
for _, c := range Catalog() {
if c.Key == key {
cp := c
return &cp
}
}
return nil
}
+58
View File
@@ -0,0 +1,58 @@
package explore
import (
"strings"
"testing"
)
func TestCatalogShape(t *testing.T) {
cats := Catalog()
if len(cats) < 6 {
t.Fatalf("want ≥6 L1 categories, got %d", len(cats))
}
keys := map[string]bool{}
for _, c := range cats {
keys[c.Key] = true
if len(c.Items) < 1 {
t.Fatalf("category %s empty", c.Key)
}
for _, it := range c.Items {
if it.Path == "" || !strings.HasPrefix(it.Path, "/") {
t.Fatalf("bad path %#v", it)
}
}
}
for _, k := range []string{"decode", "star", "relation", "tests", "rhythm", "cards", "growth"} {
if !keys[k] {
t.Fatalf("missing category %s", k)
}
}
// 四核心应排在量表之前
if indexOf(cats, "tests") < indexOf(cats, "star") {
t.Fatal("tests should come after star")
}
}
func indexOf(cats []Category, key string) int {
for i, c := range cats {
if c.Key == key {
return i
}
}
return -1
}
func TestLexiconHardBanOnly(t *testing.T) {
blob := ""
for _, c := range Catalog() {
blob += c.Title + c.Description
for _, it := range c.Items {
blob += it.Title + it.Description
}
}
for _, bad := range []string{"占卜", "算命", "测测"} {
if strings.Contains(blob, bad) {
t.Fatalf("forbidden %q", bad)
}
}
}
+121
View File
@@ -0,0 +1,121 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
asksvc "github.com/yuxingu/digital-psychology/apps/api/internal/service/ask"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
// AskHandler exposes AI 成长助手 APIs.
type AskHandler struct {
Svc *asksvc.Service
}
// Register mounts ask routes.
func (h *AskHandler) Register(rg *gin.RouterGroup) {
rg.GET("/ask/quota", h.GetQuota)
rg.POST("/ask/threads", h.CreateThread)
rg.GET("/ask/threads/:id/messages", h.ListMessages)
rg.POST("/ask/threads/:id/messages", h.SendMessage)
}
// GetQuota handles GET /ask/quota.
func (h *AskHandler) GetQuota(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
q, err := h.Svc.GetQuota(c.Request.Context(), userID)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, q)
}
// CreateThread handles POST /ask/threads.
func (h *AskHandler) CreateThread(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
ProfileID string `json:"profile_id" binding:"required"`
Scene string `json:"scene"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
pid, err := uuid.Parse(req.ProfileID)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid profile_id")
return
}
th, err := h.Svc.CreateThread(c.Request.Context(), userID, asksvc.CreateThreadInput{
ProfileID: pid, Scene: req.Scene,
})
if err != nil {
response.Fail(c, http.StatusBadRequest, 40010, err.Error())
return
}
response.OK(c, th)
}
// ListMessages handles GET /ask/threads/:id/messages.
func (h *AskHandler) ListMessages(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
tid, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid id")
return
}
items, err := h.Svc.ListMessages(c.Request.Context(), userID, tid)
if err != nil {
response.Fail(c, http.StatusNotFound, 40410, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
// SendMessage handles POST /ask/threads/:id/messages.
func (h *AskHandler) SendMessage(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
tid, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid id")
return
}
var req struct {
Content string `json:"content" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
out, err := h.Svc.SendMessage(c.Request.Context(), userID, tid, req.Content)
if err != nil {
if asksvc.IsQuotaExhausted(err) {
response.Fail(c, http.StatusPaymentRequired, 40210, "问答次数已用完,可开通成长会员获取更多次数")
return
}
response.Fail(c, http.StatusBadRequest, 40011, err.Error())
return
}
response.OK(c, out)
}
+77
View File
@@ -0,0 +1,77 @@
package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
companionsvc "github.com/yuxingu/digital-psychology/apps/api/internal/service/companion"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
// CompanionHandler exposes solar terms and moods.
type CompanionHandler struct {
Svc *companionsvc.Service
}
// Register mounts companion routes.
func (h *CompanionHandler) Register(rg *gin.RouterGroup) {
rg.GET("/solar-terms/today", h.TodaySolar)
rg.POST("/moods", h.SaveMood)
rg.GET("/moods/today", h.TodayMood)
}
// TodaySolar handles GET /solar-terms/today (auth optional via group).
func (h *CompanionHandler) TodaySolar(c *gin.Context) {
response.OK(c, h.Svc.TodaySolar())
}
// SaveMood handles POST /moods.
func (h *CompanionHandler) SaveMood(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
Score *int `json:"score"`
Note *string `json:"note"`
Day *string `json:"day"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
in := companionsvc.SaveMoodInput{Score: req.Score, Note: req.Note}
if req.Day != nil && *req.Day != "" {
d, err := time.Parse("2006-01-02", *req.Day)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid day")
return
}
in.Day = &d
}
m, err := h.Svc.SaveMood(c.Request.Context(), userID, in)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30010, err.Error())
return
}
response.OK(c, m)
}
// TodayMood handles GET /moods/today.
func (h *CompanionHandler) TodayMood(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
m, err := h.Svc.GetTodayMood(c.Request.Context(), userID)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"mood": m})
}
+34
View File
@@ -0,0 +1,34 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yuxingu/digital-psychology/apps/api/internal/explore"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
// ExploreHandler serves the explore catalog.
type ExploreHandler struct{}
// Register mounts explore routes.
func (h *ExploreHandler) Register(rg *gin.RouterGroup) {
rg.GET("/explore/catalog", h.Catalog)
rg.GET("/explore/catalog/:key", h.Category)
}
// Catalog handles GET /explore/catalog.
func (h *ExploreHandler) Catalog(c *gin.Context) {
response.OK(c, gin.H{"categories": explore.Catalog()})
}
// Category handles GET /explore/catalog/:key.
func (h *ExploreHandler) Category(c *gin.Context) {
cat := explore.CategoryByKey(c.Param("key"))
if cat == nil {
response.Fail(c, http.StatusNotFound, 40402, "category not found")
return
}
response.OK(c, cat)
}
+130
View File
@@ -0,0 +1,130 @@
package handler
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
// GrowthHandler exposes growth plans and mood trail.
type GrowthHandler struct {
Plans *repository.GrowthRepo
}
// Register mounts growth routes.
func (h *GrowthHandler) Register(rg *gin.RouterGroup) {
rg.GET("/growth/plans", h.ListPlans)
rg.POST("/growth/plans", h.CreatePlan)
rg.POST("/growth/plans/:id/checkin", h.Checkin)
rg.GET("/growth/plans/:id/checkins", h.ListCheckins)
rg.GET("/moods/recent", h.MoodsRecent)
}
// ListPlans handles GET /growth/plans.
func (h *GrowthHandler) ListPlans(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
items, err := h.Plans.ListPlans(c.Request.Context(), userID)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
// CreatePlan handles POST /growth/plans.
func (h *GrowthHandler) CreatePlan(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
Title string `json:"title" binding:"required"`
Focus string `json:"focus"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
title := strings.TrimSpace(req.Title)
if title == "" || len([]rune(title)) > 40 {
response.Fail(c, http.StatusBadRequest, 10000, "invalid title")
return
}
p, err := h.Plans.CreatePlan(c.Request.Context(), userID, title, strings.TrimSpace(req.Focus))
if err != nil {
response.Fail(c, http.StatusBadRequest, 30020, err.Error())
return
}
response.OK(c, p)
}
// Checkin handles POST /growth/plans/:id/checkin.
func (h *GrowthHandler) Checkin(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
pid, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid id")
return
}
var req struct {
Note *string `json:"note"`
}
_ = c.ShouldBindJSON(&req)
out, err := h.Plans.Checkin(c.Request.Context(), userID, pid, time.Now(), req.Note)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30021, err.Error())
return
}
response.OK(c, out)
}
// ListCheckins handles GET /growth/plans/:id/checkins.
func (h *GrowthHandler) ListCheckins(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
pid, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid id")
return
}
items, err := h.Plans.ListCheckinsRecent(c.Request.Context(), userID, pid, 14)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
// MoodsRecent handles GET /moods/recent.
func (h *GrowthHandler) MoodsRecent(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
items, err := h.Plans.ListMoodsRecent(c.Request.Context(), userID, 7)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
+81
View File
@@ -0,0 +1,81 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
imagecardsvc "github.com/yuxingu/digital-psychology/apps/api/internal/service/imagecard"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
// ImageCardHandler exposes 意象卡片 APIs.
type ImageCardHandler struct {
Svc *imagecardsvc.Service
}
// Register mounts image-card routes.
func (h *ImageCardHandler) Register(rg *gin.RouterGroup) {
rg.GET("/image-cards/scenes", h.Scenes)
rg.GET("/image-cards/quota", h.Quota)
rg.POST("/image-cards/draw", h.Draw)
}
// Scenes handles GET /image-cards/scenes.
func (h *ImageCardHandler) Scenes(c *gin.Context) {
response.OK(c, gin.H{"items": h.Svc.Scenes()})
}
// Quota handles GET /image-cards/quota.
func (h *ImageCardHandler) Quota(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
q, err := h.Svc.Quota(c.Request.Context(), userID)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, q)
}
// Draw handles POST /image-cards/draw.
func (h *ImageCardHandler) Draw(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
Scene string `json:"scene"`
ProfileID string `json:"profile_id" binding:"required"`
Depth bool `json:"depth"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
pid, err := uuid.Parse(req.ProfileID)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid profile_id")
return
}
out, err := h.Svc.Draw(c.Request.Context(), userID, imagecardsvc.DrawInput{
Scene: req.Scene, ProfileID: pid, Depth: req.Depth,
})
if err != nil {
if errors.Is(err, repository.ErrQuotaExhausted) {
response.Fail(c, http.StatusPaymentRequired, 40201, err.Error())
return
}
response.Fail(c, http.StatusBadRequest, 30011, err.Error())
return
}
response.OK(c, out)
}
+72 -1
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/profile"
@@ -20,6 +21,8 @@ type ProfileHandler struct {
func (h *ProfileHandler) Register(rg *gin.RouterGroup) {
rg.GET("/profiles", h.List)
rg.POST("/profiles", h.Create)
rg.PATCH("/profiles/:id", h.Update)
rg.DELETE("/profiles/:id", h.Delete)
}
type createProfileReq struct {
@@ -27,6 +30,8 @@ type createProfileReq struct {
DisplayName string `json:"display_name"`
BirthDate string `json:"birth_date" binding:"required"`
RelationType *string `json:"relation_type"`
BirthTime *string `json:"birth_time"`
BirthPlace *string `json:"birth_place"`
}
// Create handles POST /profiles.
@@ -47,7 +52,8 @@ func (h *ProfileHandler) Create(c *gin.Context) {
return
}
p, err := h.Svc.Create(c.Request.Context(), userID, profile.CreateInput{
Relation: req.Relation, DisplayName: req.DisplayName, BirthDate: birth, RelationType: req.RelationType,
Relation: req.Relation, DisplayName: req.DisplayName, BirthDate: birth,
RelationType: req.RelationType, BirthTime: req.BirthTime, BirthPlace: req.BirthPlace,
})
if err != nil {
response.Fail(c, http.StatusBadRequest, 30001, err.Error())
@@ -70,3 +76,68 @@ func (h *ProfileHandler) List(c *gin.Context) {
}
response.OK(c, gin.H{"items": list})
}
// Update handles PATCH /profiles/:id.
func (h *ProfileHandler) Update(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
pid, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid id")
return
}
var req struct {
DisplayName string `json:"display_name"`
BirthDate string `json:"birth_date"`
RelationType *string `json:"relation_type"`
BirthTime *string `json:"birth_time"`
BirthPlace *string `json:"birth_place"`
GeoLat *float64 `json:"geo_lat"`
GeoLng *float64 `json:"geo_lng"`
GeoVisible *bool `json:"geo_visible"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
var birth time.Time
if req.BirthDate != "" {
birth, err = time.Parse("2006-01-02", req.BirthDate)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "birth_date must be YYYY-MM-DD")
return
}
}
p, err := h.Svc.Update(c.Request.Context(), userID, pid, profile.UpdateInput{
DisplayName: req.DisplayName, BirthDate: birth, RelationType: req.RelationType,
BirthTime: req.BirthTime, BirthPlace: req.BirthPlace,
GeoLat: req.GeoLat, GeoLng: req.GeoLng, GeoVisible: req.GeoVisible,
})
if err != nil {
response.Fail(c, http.StatusNotFound, 40401, err.Error())
return
}
response.OK(c, p)
}
// Delete handles DELETE /profiles/:id.
func (h *ProfileHandler) Delete(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
pid, err := uuid.Parse(c.Param("id"))
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid id")
return
}
if err := h.Svc.Delete(c.Request.Context(), userID, pid); err != nil {
response.Fail(c, http.StatusNotFound, 40401, err.Error())
return
}
response.OK(c, gin.H{"deleted": true})
}
+137
View File
@@ -2,6 +2,7 @@ package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -19,7 +20,12 @@ type ReportHandler struct {
// Register mounts report/commerce routes.
func (h *ReportHandler) Register(rg *gin.RouterGroup) {
rg.POST("/reports/portrait", h.CreatePortrait)
rg.POST("/reports/star", h.CreateStar)
rg.POST("/reports/synastry", h.CreateSynastry)
rg.POST("/reports/rhythm", h.CreateRhythm)
rg.GET("/reports", h.List)
rg.GET("/reports/:id", h.Get)
rg.GET("/membership/me", h.GetMembership)
rg.POST("/orders", h.CreateOrder)
rg.POST("/orders/:id/pay-mock", h.PayMock)
}
@@ -51,6 +57,122 @@ func (h *ReportHandler) CreatePortrait(c *gin.Context) {
response.OK(c, rep)
}
// CreateStar handles POST /reports/star (星象性格).
func (h *ReportHandler) CreateStar(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
ProfileID string `json:"profile_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
pid, err := uuid.Parse(req.ProfileID)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid profile_id")
return
}
rep, err := h.Svc.CreateStar(c.Request.Context(), userID, pid)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30002, err.Error())
return
}
response.OK(c, rep)
}
// CreateSynastry handles POST /reports/synastry (五主盘 + 推运合盘).
func (h *ReportHandler) CreateSynastry(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
ProfileIDA string `json:"profile_id_a" binding:"required"`
ProfileIDB string `json:"profile_id_b" binding:"required"`
AsOf *string `json:"as_of"` // YYYY-MM-DD optional
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
aid, err := uuid.Parse(req.ProfileIDA)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid profile_id_a")
return
}
bid, err := uuid.Parse(req.ProfileIDB)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid profile_id_b")
return
}
if aid == bid {
response.Fail(c, http.StatusBadRequest, 10000, "需要两个不同档案")
return
}
var asOfPtr *time.Time
if req.AsOf != nil && *req.AsOf != "" {
t, err := time.ParseInLocation("2006-01-02", *req.AsOf, time.FixedZone("CST", 8*3600))
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid as_of")
return
}
asOfPtr = &t
}
rep, err := h.Svc.CreateSynastry(c.Request.Context(), userID, aid, bid, asOfPtr)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30002, err.Error())
return
}
response.OK(c, rep)
}
// CreateRhythm handles POST /reports/rhythm (身心节律) — wired when service ready.
func (h *ReportHandler) CreateRhythm(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
ProfileID string `json:"profile_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
pid, err := uuid.Parse(req.ProfileID)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid profile_id")
return
}
rep, err := h.Svc.CreateRhythm(c.Request.Context(), userID, pid)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30002, err.Error())
return
}
response.OK(c, rep)
}
// List handles GET /reports.
func (h *ReportHandler) List(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
items, err := h.Svc.List(c.Request.Context(), userID)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
// Get handles GET /reports/:id.
func (h *ReportHandler) Get(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
@@ -71,6 +193,21 @@ func (h *ReportHandler) Get(c *gin.Context) {
response.OK(c, rep)
}
// GetMembership handles GET /membership/me.
func (h *ReportHandler) GetMembership(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
me, err := h.Svc.GetMembership(c.Request.Context(), userID)
if err != nil {
response.Fail(c, http.StatusInternalServerError, 50000, err.Error())
return
}
response.OK(c, me)
}
// CreateOrder handles POST /orders.
func (h *ReportHandler) CreateOrder(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
+123
View File
@@ -0,0 +1,123 @@
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/report"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
// SynastryHandler exposes nearby + invite social APIs.
type SynastryHandler struct {
Svc *report.Service
}
// Register mounts synastry social routes.
func (h *SynastryHandler) Register(rg *gin.RouterGroup) {
rg.GET("/synastry/nearby", h.Nearby)
rg.POST("/synastry/invites", h.CreateInvite)
rg.GET("/synastry/invites/:token", h.GetInvite)
rg.POST("/synastry/invites/:token/accept", h.AcceptInvite)
}
// Nearby handles GET /synastry/nearby?lat=&lng=&radius_km=
func (h *SynastryHandler) Nearby(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
lat, err1 := strconv.ParseFloat(c.Query("lat"), 64)
lng, err2 := strconv.ParseFloat(c.Query("lng"), 64)
if err1 != nil || err2 != nil {
response.Fail(c, http.StatusBadRequest, 10000, "lat/lng required")
return
}
radius := 50.0
if v := c.Query("radius_km"); v != "" {
if r, err := strconv.ParseFloat(v, 64); err == nil {
radius = r
}
}
items, err := h.Svc.Nearby(c.Request.Context(), userID, lat, lng, radius)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30002, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
// CreateInvite handles POST /synastry/invites
func (h *SynastryHandler) CreateInvite(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
ProfileID string `json:"profile_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
pid, err := uuid.Parse(req.ProfileID)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid profile_id")
return
}
inv, err := h.Svc.CreateInvite(c.Request.Context(), userID, pid)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30002, err.Error())
return
}
response.OK(c, gin.H{
"token": inv.Token,
"expires_at": inv.ExpiresAt,
"path": "/synastry/invite/" + inv.Token,
})
}
// GetInvite handles GET /synastry/invites/:token
func (h *SynastryHandler) GetInvite(c *gin.Context) {
if _, ok := middleware.UserIDFromContext(c); !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
meta, err := h.Svc.GetInvite(c.Request.Context(), c.Param("token"))
if err != nil {
response.Fail(c, http.StatusNotFound, 40401, err.Error())
return
}
response.OK(c, meta)
}
// AcceptInvite handles POST /synastry/invites/:token/accept
func (h *SynastryHandler) AcceptInvite(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
DisplayName string `json:"display_name"`
BirthDate string `json:"birth_date" binding:"required"`
BirthTime *string `json:"birth_time"`
BirthPlace *string `json:"birth_place"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
rep, err := h.Svc.AcceptInvite(c.Request.Context(), userID, c.Param("token"), req.DisplayName, req.BirthDate, req.BirthTime, req.BirthPlace)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30002, err.Error())
return
}
response.OK(c, rep)
}
+80
View File
@@ -0,0 +1,80 @@
// Package httpserver wires HTTP routes for the 愈心谷 API.
package httpserver
import (
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/yuxingu/digital-psychology/apps/api/internal/config"
"github.com/yuxingu/digital-psychology/apps/api/internal/handler"
"github.com/yuxingu/digital-psychology/apps/api/internal/llm/deepseek"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/ask"
companionsvc "github.com/yuxingu/digital-psychology/apps/api/internal/service/companion"
imagecardsvc "github.com/yuxingu/digital-psychology/apps/api/internal/service/imagecard"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/profile"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/relation"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/report"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/scale"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
// NewRouter builds the Gin engine with all /api/v1 routes.
func NewRouter(pool *pgxpool.Pool, cfg config.Config) *gin.Engine {
profileRepo := &repository.ProfileRepo{Pool: pool}
reportRepo := &repository.ReportRepo{Pool: pool}
relationRepo := &repository.RelationRepo{Pool: pool}
askRepo := &repository.AskRepo{Pool: pool}
var llm *deepseek.Client
if cfg.DeepSeek.Enabled() {
llm = deepseek.New(cfg.DeepSeek)
}
profileSvc := &profile.Service{Repo: profileRepo}
reportSvc := &report.Service{
Profiles: profileRepo,
Reports: reportRepo,
Invites: &repository.SynastryInviteRepo{Pool: pool},
}
relationSvc := &relation.Service{Profiles: profileRepo, Reports: reportRepo, Relations: relationRepo}
scaleSvc := &scale.Service{Repo: &repository.ScaleRepo{Pool: pool}, Profiles: profileRepo}
askSvc := &ask.Service{Profiles: profileRepo, Reports: reportRepo, Ask: askRepo, LLM: llm}
companionSvc := &companionsvc.Service{Moods: &repository.MoodRepo{Pool: pool}}
imageCardSvc := &imagecardsvc.Service{
Profiles: profileRepo,
Reports: reportRepo,
Quotas: &repository.ImageCardRepo{Pool: pool},
}
r := gin.New()
r.Use(gin.Recovery(), gin.Logger(), middleware.RequestID())
r.Use(func(c *gin.Context) {
c.Header("Access-Control-Expose-Headers", "X-Device-Key, X-Request-Id")
c.Next()
})
api := r.Group("/api/v1")
handler.NewHealthHandler().Register(api)
api.GET("/ping", func(c *gin.Context) {
response.OK(c, gin.H{"pong": true})
})
authed := api.Group("")
authed.Use(middleware.DeviceAuth(pool))
(&handler.ProfileHandler{Svc: profileSvc}).Register(authed)
(&handler.ReportHandler{Svc: reportSvc}).Register(authed)
(&handler.SynastryHandler{Svc: reportSvc}).Register(authed)
(&handler.RelationHandler{Svc: relationSvc}).Register(authed)
(&handler.ScaleHandler{Svc: scaleSvc}).Register(authed)
(&handler.AskHandler{Svc: askSvc}).Register(authed)
(&handler.CompanionHandler{Svc: companionSvc}).Register(authed)
(&handler.ImageCardHandler{Svc: imageCardSvc}).Register(authed)
(&handler.ExploreHandler{}).Register(authed)
(&handler.GrowthHandler{
Plans: &repository.GrowthRepo{Pool: pool},
}).Register(authed)
return r
}
+220
View File
@@ -0,0 +1,220 @@
// Package imagecard implements 意象卡片 draws (projection / reflection, not tarot UI).
package imagecard
import (
"fmt"
"hash/fnv"
"time"
)
// Card is one exploration card.
type Card struct {
ID string `json:"id"`
Title string `json:"title"`
Imagery string `json:"imagery"`
Prompt string `json:"prompt"`
Tip string `json:"tip"`
}
// DrawResult is API payload for a draw.
type DrawResult struct {
Scene string `json:"scene"`
Cards []Card `json:"cards"`
Summary map[string]any `json:"summary"`
Detail map[string]any `json:"detail"`
QuotaLeft int `json:"quota_left"`
}
var scenes = []string{
"情绪整理", "关系", "选择", "自我",
"工作节奏", "边界", "休息", "表达",
}
// Scenes returns available reflection scenes.
func Scenes() []map[string]string {
out := make([]map[string]string, 0, len(scenes))
for _, s := range scenes {
out = append(out, map[string]string{"key": s, "label": s})
}
return out
}
// deck built once: 12 handcrafted + generated to ≥78.
var deck = buildDeck()
func buildDeck() []Card {
base := []Card{
{"c01", "微光小路", "一条只够一人走过的小径,尽头有一点光。", "此刻你最想靠近的「光」是什么?", "选一个最小行动靠近它,不必一次走完。"},
{"c02", "安静的杯子", "桌上杯子里的水是温的,蒸汽慢慢散开。", "你最近在为谁/什么「保温」?有没有过热?", "今天给自己倒一杯水,只为自己停两分钟。"},
{"c03", "未拆的信", "信封边缘有点磨损,还没打开。", "有哪句话你一直想说却还没说?", "写下来即可,不一定立刻发出。"},
{"c04", "桥与河", "桥稳稳跨过河,水流在下面经过。", "你现在更需要「过去」还是「停在岸上感受」?", "允许自己选一边,并告诉相关的人。"},
{"c05", "收拾桌面", "杂物被归类,只留一件重要的东西在中间。", "若只能保留一件「最重要」,会是什么?", "本周减少一件消耗你的杂事。"},
{"c06", "窗边呼吸", "窗外有风,窗帘轻轻动。", "你的呼吸是浅还是深?身体哪里最紧?", "做 4 次慢呼吸,肩再放松一点。"},
{"c07", "同行的影子", "两个人影并排,步伐不完全一致。", "关系里你希望对方怎样配合你的节奏?", "用一句具体请求代替抱怨。"},
{"c08", "种子与土", "土里有一颗刚发芽的种子。", "你正在酝酿、还没被人看见的是什么?", "给它一点时间,并做一个保护边界。"},
{"c09", "地图折痕", "地图被折过很多次,路线仍可辨认。", "过去哪些弯路其实教了你方法?", "写下一条「下次可以更早用的经验」。"},
{"c10", "钥匙串", "一串钥匙里有一把还没用过。", "你手头有哪项资源/能力还没启用?", "本周试用一次那个「备用钥匙」。"},
{"c11", "退潮沙滩", "潮水退去,露出一些小贝壳。", "热闹过后,你发现了什么真实感受?", "把感受写三句,不评判对错。"},
{"c12", "灯塔节奏", "灯塔规律地亮—灭—亮。", "你的生活里什么节奏让你安心?", "把那个节奏固定进日程一格。"},
}
themes := []struct{ title, imagery, prompt, tip string }{
{"山径转弯", "山路在雾里拐了一个弯,远处仍可见脚印。", "你正在避开的弯是什么?", "允许自己慢半步,再决定方向。"},
{"雨后青石", "雨停了,石板反光,空气很干净。", "刚过去的「雨」带走了什么?", "写下三件你想留下的。"},
{"未完成的画", "画布只涂了一角,颜料还湿着。", "哪件事值得继续涂,而不是重开一张?", "今天只加一笔就够。"},
{"旧毛衣", "袖口有点起球,却仍然暖和。", "你生活里哪件「旧物」仍在保护你?", "对它说一句感谢。"},
{"电梯门", "电梯门开了又关,楼层数字在跳。", "你卡在哪一层不愿下去或上去?", "选一个小出口先离开卡点。"},
{"夜班台灯", "台灯只照亮桌面一小块。", "你需要把注意力收窄到哪里?", "设 25 分钟专注,然后休息。"},
{"空座位", "长椅空着一半,另一半有阳光。", "你更需要坐下还是让出位置?", "今天做一个明确的「要/不要」。"},
{"漂流瓶", "瓶子在水里轻轻晃,纸条卷着。", "你想让谁「偶然」读到你的真心?", "写给自己一封短信即可。"},
{"鞋带", "鞋带松了,还在走。", "哪里需要先停下来系紧?", "处理一件基础小事再赶路。"},
{"回声廊", "走廊很长,声音会折返。", "你说出口的话,回来时变成了什么?", "少说一句评判,多问一句好奇。"},
{"晨雾车站", "站台有雾,车还没到。", "等待里你在消耗还是在准备?", "用等待做一件小事(喝水/伸展)。"},
{"书架空隙", "两本书之间留了一指宽空隙。", "你生活里需要留白的是哪一块?", "从日程删掉一个非必要。"},
{"冷水龙头", "水先凉后热,需要等一会儿。", "你对谁/什么事太急了?", "给过程多 10 分钟缓冲。"},
{"风筝线", "线在手里,风筝在高处晃。", "你抓得太紧的是什么?", "刻意松开一点控制,观察结果。"},
{"烛火", "火焰小而稳,周围很暗。", "什么能成为你今晚的小光源?", "做一件温暖且短的事。"},
{"拼图缺角", "图案几乎成型,只差一块。", "缺的那块真的在外面,还是你还没承认?", "列出「已有」而不是「没有」。"},
{"旧车票", "票根折痕深,日期已过。", "哪段旅程可以温柔结束了?", "做一个小小的告别仪式。"},
{"晾衣绳", "衣服在风里慢慢干。", "什么事其实只需要时间,不需要用力?", "今天不催自己一次。"},
{"镜面湖", "湖面几乎无波,倒映天空。", "你害怕看到的自己是哪一面?", "对镜写下三个不评判的观察。"},
{"登山杖", "杖尖戳进土里,借力往上。", "你愿意向谁借一点力?", "发出一个具体求助。"},
{"暖手宝", "掌心慢慢回温。", "谁/什么在给你持续的暖?", "今天主动回馈一点点暖。"},
{"十字路口灯", "红灯停,绿灯行,黄灯提醒。", "你现在更需要停、行,还是减速?", "按信号选一个动作执行。"},
{"背包重量", "肩带勒出印子。", "包里哪样可以先拿出来?", "卸下一件心理负担并告诉自己。"},
{"夜路灯", "一盏盏灯把路分段照亮。", "你下一步只需要照亮多远?", "只计划明天上午即可。"},
{"陶罐裂缝", "裂缝里透出一点光。", "你的「不完美」里藏了什么资源?", "把一个缺点改写成可用特质。"},
{"梯田", "一层一层往上,水流向下。", "你的成长是哪一层该浇水?", "本周只推进一层目标。"},
{"耳机静音", "世界声音被隔开一点。", "你需要隔开什么噪音?", "设定一段无信息时段。"},
{"折纸船", "小船在水盆里打转。", "你把安全感放在哪里?", "做一个可控的小实验。"},
{"墙洞光", "光从窄缝进来,依然够用。", "有限条件下你仍能做什么?", "用现有资源完成一件小事。"},
{"沙漏", "沙子安静落下。", "你在跟时间较劲什么?", "接受「够好」的标准一次。"},
{"回廊座", "走廊尽头有一张椅子。", "你允许自己坐下休息了吗?", "安排 15 分钟真正休息。"},
{"雾中灯塔", "雾浓,灯仍按节奏闪。", "不确定时什么原则仍不变?", "写下你的一条底线。"},
{"青苔石", "石头潮湿,青苔慢慢长。", "哪段关系需要慢养而不是快修?", "降低频率,提高质量。"},
{"晾晒日光", "被子在阳光里松软。", "什么情绪需要「拿出去晒晒」?", "和信任的人聊 10 分钟。"},
{"圆规", "圆心固定,半径可调。", "你的边界圆心在哪里?", "明确一个「可以/不可以」。"},
{"夜航图", "星点标出航线。", "你参考的「星」是谁的标准?", "换成自己的三条原则。"},
{"竹筒饭", "竹香与米香混在一起。", "简单事物里你忽略了什么满足?", "吃一顿专注的饭。"},
{"软垫", "落地时被接住。", "跌倒时谁/什么接住了你?", "写感谢,或成为别人的软垫一次。"},
{"窗格影子", "格子把光切成块。", "你把生活切得太碎了吗?", "合并两件可一起做的事。"},
{"河石", "棱角被水磨圆。", "冲突磨掉了你的什么?留下了什么?", "保留圆润,也保留核心硬度。"},
{"草稿本", "涂改很多,仍翻得开。", "哪份「草稿」其实已经能见人?", "发布一个 70 分版本。"},
{"门铃", "轻按一下就会有回应。", "你在等谁来按铃?还是该自己开门?", "主动发起一次连结。"},
{"冬青", "天冷仍绿。", "什么习惯在难时仍支撑你?", "把那习惯写进日程。"},
{"秋千", "来回摆,最高点短暂。", "你在追峰值还是稳态?", "选择稳态的一小步。"},
{"墨点", "一滴墨在宣纸上晕开。", "影响正在扩大的是什么?", "及时止损或顺势引导。"},
{"绳结", "结打紧了,解需要耐心。", "哪个结值得慢慢解?", "今天只解一层。"},
{"麦浪", "风过,一片起伏。", "集体节奏里你站在哪?", "允许与主流差半拍。"},
{"星尘罐", "罐子装不住光,却闪着。", "你收藏的希望是什么?", "拿出来看一眼,再盖上。"},
{"石阶", "一级一级,并不陡。", "你把台阶看高了吗?", "只上今天这一级。"},
{"暖汤", "热气模糊了眼镜。", "谁需要一碗「暖」?", "做一件具体的照顾。"},
{"风铃", "风来才响。", "什么条件一到你就会行动?", "把条件改成更小的触发。"},
{"书签", "停在未读完的那页。", "你暂停的故事要不要继续?", "读完两页或正式合上。"},
{"露珠", "很快会消失,却折射天空。", "短暂却珍贵的片刻是什么?", "用心经历,不必抓住。"},
{"柴门", "门不华丽,却通向家。", "「回家」对你意味着什么?", "安排一次真正放松的回家感。"},
{"远山", "看得到,走得到需要天。", "远目标如何拆成今日可见的一步?", "写下「今天可见的山脚」。"},
{"井绳", "要用力,也要节奏。", "你在哪用力过猛?", "改成间歇用力。"},
{"纸飞机", "飞不远,但方向是自己的。", "小实验可以朝哪飞?", "发出一个低成本试探。"},
{"苔径", "湿滑,需放慢。", "哪里需要防滑措施?", "加一个缓冲或备份。"},
{"铜铃", "声音清脆后很快静。", "提醒响过,你听见了吗?", "把提醒变成一个行动。"},
{"白瓷碗", "空着时也完整。", "空不是缺失,可能是准备。", "留出空白时段不被填满。"},
{"夜雨窗", "雨点连成线。", "情绪连成片时你怎么分段?", "用呼吸把情绪切成小节。"},
{"藤椅", "坐下就陷进去一点。", "你允许自己「陷进」休息吗?", "无罪恶感地休息 20 分钟。"},
{"路牌", "箭头指向两个镇。", "信息不够时如何选?", "选可逆的那条先走。"},
{"炭火", "表面灰,内里仍热。", "表面平静下还热着的是什么?", "找安全出口表达它。"},
{"蒲公英", "风一吹就出发。", "你准备好松开了吗?", "松开一个过时期待。"},
{"石桥栏", "扶手冰凉但可靠。", "你的可靠支持是什么?", "主动靠一下支持系统。"},
}
out := append([]Card{}, base...)
for i, th := range themes {
out = append(out, Card{
ID: fmt.Sprintf("c%02d", len(base)+i+1),
Title: th.title,
Imagery: th.imagery,
Prompt: th.prompt,
Tip: th.tip,
})
}
// pad to at least 78 with deterministic variants
for len(out) < 78 {
i := len(out)
src := base[i%len(base)]
out = append(out, Card{
ID: fmt.Sprintf("c%02d", i+1),
Title: fmt.Sprintf("%s·续", src.Title),
Imagery: src.Imagery + " 光线又柔和了一点。",
Prompt: src.Prompt,
Tip: src.Tip,
})
}
return out[:78]
}
// Draw picks cards deterministically from userID+scene+day for free single, or 3 for deep.
func Draw(userKey, scene string, deep bool, now time.Time) DrawResult {
if scene == "" {
scene = scenes[0]
}
n := 1
if deep {
n = 3
}
day := now.UTC().Format("2006-01-02")
idxs := pickIndexes(userKey+"|"+scene+"|"+day, n, len(deck))
cards := make([]Card, 0, n)
for _, i := range idxs {
cards = append(cards, deck[i])
}
summary := map[string]any{
"title": "意象卡片·探索",
"headline": fmt.Sprintf("场景「%s」· %s", scene, cards[0].Title),
"one_liner": cards[0].Imagery,
"overview": fmt.Sprintf("反思:%s", cards[0].Prompt),
"life_tip": cards[0].Tip,
"keywords": []string{"意象卡片", scene, cards[0].Title},
"scene": scene,
"card_ids": idsOf(cards),
}
var detail map[string]any
if deep && len(cards) >= 3 {
detail = map[string]any{
"title": "意象卡片·组合反思",
"sections": []map[string]any{
{"title": cards[0].Title, "body": cards[0].Imagery + " " + cards[0].Prompt, "bullets": []string{cards[0].Tip}},
{"title": cards[1].Title, "body": cards[1].Imagery + " " + cards[1].Prompt, "bullets": []string{cards[1].Tip}},
{"title": cards[2].Title, "body": cards[2].Imagery + " " + cards[2].Prompt, "bullets": []string{cards[2].Tip}},
{"title": "组合练习", "body": "把三张卡连成一个小故事:发生了什么、你感受到什么、下一步一小步是什么。写 5 行即可。", "bullets": []string{"不评判对错", "只选一个可执行下一步", "需要时可去问答继续聊"}},
},
"conversation_scripts": []string{"我抽到的意象让我想到……", "我现在需要的是……"},
"faq": []map[string]string{
{"q": "这是在预测未来吗?", "a": "不是。意象卡片用于投射与自我反思,不判定好坏,也不预测未来。"},
},
}
}
return DrawResult{Scene: scene, Cards: cards, Summary: summary, Detail: detail}
}
func idsOf(cards []Card) []string {
out := make([]string, len(cards))
for i, c := range cards {
out[i] = c.ID
}
return out
}
func pickIndexes(seed string, n, mod int) []int {
h := fnv.New32a()
_, _ = h.Write([]byte(seed))
start := int(h.Sum32() % uint32(mod))
out := make([]int, 0, n)
seen := map[int]bool{}
for i := 0; len(out) < n && i < mod*2; i++ {
idx := (start + i*3) % mod
if seen[idx] {
continue
}
seen[idx] = true
out = append(out, idx)
}
return out
}
+36
View File
@@ -0,0 +1,36 @@
package imagecard
import (
"encoding/json"
"strings"
"testing"
"time"
)
func TestDeckSize(t *testing.T) {
if len(deck) < 78 {
t.Fatalf("want ≥78 cards, got %d", len(deck))
}
if len(scenes) < 8 {
t.Fatalf("want ≥8 scenes, got %d", len(scenes))
}
}
func TestDrawLexicon(t *testing.T) {
out := Draw("u1", "情绪整理", true, time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC))
if len(out.Cards) != 3 {
t.Fatalf("want 3 cards, got %d", len(out.Cards))
}
raw, _ := json.Marshal(out)
for _, bad := range []string{"算命", "占卜"} {
if strings.Contains(string(raw), bad) {
t.Fatalf("forbidden %q", bad)
}
}
}
func TestScenes(t *testing.T) {
if len(Scenes()) < 3 {
t.Fatal("too few scenes")
}
}
@@ -0,0 +1,45 @@
package integration_test
import (
"net/http"
"testing"
)
func TestExploreCatalog(t *testing.T) {
r, _ := setupAPI(t)
var key string
env, key := doJSON(t, r, http.MethodGet, "/api/v1/explore/catalog", nil, key)
data := decodeData[map[string]any](t, env.Data)
cats, ok := data["categories"].([]any)
if !ok || len(cats) < 6 {
t.Fatalf("categories=%#v", data["categories"])
}
env, _ = doJSON(t, r, http.MethodGet, "/api/v1/explore/catalog/tests", nil, key)
cat := decodeData[map[string]any](t, env.Data)
items, _ := cat["items"].([]any)
if len(items) < 5 {
t.Fatalf("tests items=%d", len(items))
}
}
func TestGrowthPlanCheckin(t *testing.T) {
r, _ := setupAPI(t)
var key string
env, key := doJSON(t, r, http.MethodPost, "/api/v1/growth/plans", map[string]any{
"title": "每晚早睡", "focus": "保护睡眠",
}, key)
plan := decodeData[map[string]any](t, env.Data)
id, _ := plan["id"].(string)
if id == "" {
t.Fatal("missing plan id")
}
_, key = doJSON(t, r, http.MethodPost, "/api/v1/growth/plans/"+id+"/checkin", map[string]any{
"note": "做到了",
}, key)
env, _ = doJSON(t, r, http.MethodGet, "/api/v1/growth/plans/"+id+"/checkins", nil, key)
out := decodeData[map[string]any](t, env.Data)
items, _ := out["items"].([]any)
if len(items) < 1 {
t.Fatal("expected checkin")
}
}
@@ -0,0 +1,308 @@
package integration_test
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/yuxingu/digital-psychology/apps/api/internal/config"
"github.com/yuxingu/digital-psychology/apps/api/internal/db"
"github.com/yuxingu/digital-psychology/apps/api/internal/httpserver"
)
type envelope struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
func setupAPI(t *testing.T) (*gin.Engine, string) {
t.Helper()
gin.SetMode(gin.TestMode)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
t.Cleanup(cancel)
cfg := config.Load()
pool, err := db.Connect(ctx, cfg.DatabaseURL)
if err != nil {
t.Skipf("postgres unavailable (run npm run deps:up): %v", err)
}
t.Cleanup(pool.Close)
migDir := filepath.Join("..", "..", "migrations")
if err := db.Migrate(ctx, pool, migDir); err != nil {
t.Fatalf("migrate: %v", err)
}
return httpserver.NewRouter(pool, cfg), ""
}
func doJSON(t *testing.T, r http.Handler, method, path string, body any, deviceKey string) (envelope, string) {
t.Helper()
var buf bytes.Buffer
if body != nil {
if err := json.NewEncoder(&buf).Encode(body); err != nil {
t.Fatalf("encode: %v", err)
}
}
req := httptest.NewRequest(method, path, &buf)
req.Header.Set("Content-Type", "application/json")
if deviceKey != "" {
req.Header.Set("X-Device-Key", deviceKey)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code >= 500 {
t.Fatalf("%s %s → HTTP %d: %s", method, path, w.Code, w.Body.String())
}
var env envelope
if err := json.Unmarshal(w.Body.Bytes(), &env); err != nil {
t.Fatalf("decode envelope: %v body=%s", err, w.Body.String())
}
if env.Code != 0 {
t.Fatalf("%s %s → code=%d message=%s", method, path, env.Code, env.Message)
}
key := w.Header().Get("X-Device-Key")
if key == "" {
key = deviceKey
}
return env, key
}
func decodeData[T any](t *testing.T, raw json.RawMessage) T {
t.Helper()
var v T
if err := json.Unmarshal(raw, &v); err != nil {
t.Fatalf("decode data: %v raw=%s", err, string(raw))
}
return v
}
// Flow 1: create profile → portrait → deep_access mock → detail visible
func TestFlowPortraitDeepAccess(t *testing.T) {
r, _ := setupAPI(t)
var key string
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "self", "birth_date": "1990-05-12", "display_name": "我",
}, key)
profile := decodeData[map[string]any](t, env.Data)
profileID, _ := profile["id"].(string)
env, key = doJSON(t, r, http.MethodPost, "/api/v1/reports/portrait", map[string]any{
"profile_id": profileID,
}, key)
rep := decodeData[map[string]any](t, env.Data)
reportID, _ := rep["id"].(string)
if rep["has_deep_access"] == true {
t.Fatal("expected gated detail before pay")
}
if rep["detail"] != nil {
t.Fatal("detail should be nil before deep access")
}
env, key = doJSON(t, r, http.MethodPost, "/api/v1/orders", map[string]any{
"kind": "deep_access", "report_id": reportID,
}, key)
order := decodeData[map[string]any](t, env.Data)
orderID, _ := order["order_id"].(string)
_, key = doJSON(t, r, http.MethodPost, "/api/v1/orders/"+orderID+"/pay-mock", nil, key)
env, _ = doJSON(t, r, http.MethodGet, "/api/v1/reports/"+reportID, nil, key)
unlocked := decodeData[map[string]any](t, env.Data)
if unlocked["has_deep_access"] != true {
t.Fatal("expected has_deep_access after pay")
}
detail, ok := unlocked["detail"].(map[string]any)
if !ok || len(detail) == 0 {
t.Fatalf("expected detail map, got %#v", unlocked["detail"])
}
if detail["behavior_pattern"] == nil || detail["behavior_pattern"] == "" {
t.Fatal("expected behavior_pattern in detail")
}
}
// Flow 2: two profiles → relation insight → deep_access → tips visible
func TestFlowRelationDeepAccess(t *testing.T) {
r, _ := setupAPI(t)
var key string
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "self", "birth_date": "1988-03-01", "display_name": "我",
}, key)
a := decodeData[map[string]any](t, env.Data)
env, key = doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "other", "birth_date": "1992-08-20", "display_name": "TA", "relation_type": "partner",
}, key)
b := decodeData[map[string]any](t, env.Data)
env, key = doJSON(t, r, http.MethodPost, "/api/v1/relation/insight", map[string]any{
"profile_a_id": a["id"], "profile_b_id": b["id"],
}, key)
out := decodeData[map[string]any](t, env.Data)
rep, ok := out["report"].(map[string]any)
if !ok {
t.Fatalf("missing report: %#v", out)
}
reportID, _ := rep["id"].(string)
if rep["has_deep_access"] == true {
t.Fatal("expected gated tips before pay")
}
sum, _ := rep["summary"].(map[string]any)
if sum["love_index"] == nil || sum["friend_index"] == nil || sum["marriage_index"] == nil {
t.Fatalf("expected match indices in summary: %#v", sum)
}
env, key = doJSON(t, r, http.MethodPost, "/api/v1/orders", map[string]any{
"kind": "deep_access", "report_id": reportID,
}, key)
orderID := decodeData[map[string]any](t, env.Data)["order_id"].(string)
_, key = doJSON(t, r, http.MethodPost, "/api/v1/orders/"+orderID+"/pay-mock", nil, key)
env, _ = doJSON(t, r, http.MethodGet, "/api/v1/reports/"+reportID, nil, key)
unlocked := decodeData[map[string]any](t, env.Data)
if unlocked["has_deep_access"] != true {
t.Fatal("expected deep access")
}
detail, ok := unlocked["detail"].(map[string]any)
if !ok {
t.Fatal("expected detail")
}
comm, _ := detail["communication"].([]any)
if len(comm) == 0 {
t.Fatalf("expected communication tips, detail=%#v", detail)
}
}
// Flow 3: membership mock → entitlement → portrait detail without per-report deep_access
func TestFlowMembershipUnlock(t *testing.T) {
r, _ := setupAPI(t)
var key string
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "self", "birth_date": "1995-11-07", "display_name": "我",
}, key)
profileID := decodeData[map[string]any](t, env.Data)["id"].(string)
env, key = doJSON(t, r, http.MethodPost, "/api/v1/reports/portrait", map[string]any{
"profile_id": profileID,
}, key)
reportID := decodeData[map[string]any](t, env.Data)["id"].(string)
env, key = doJSON(t, r, http.MethodGet, "/api/v1/membership/me", nil, key)
me := decodeData[map[string]any](t, env.Data)
if me["active"] == true {
t.Fatal("expected inactive membership before subscribe")
}
env, key = doJSON(t, r, http.MethodPost, "/api/v1/orders", map[string]any{
"kind": "membership", "plan": "month",
}, key)
orderID := decodeData[map[string]any](t, env.Data)["order_id"].(string)
_, key = doJSON(t, r, http.MethodPost, "/api/v1/orders/"+orderID+"/pay-mock", nil, key)
env, key = doJSON(t, r, http.MethodGet, "/api/v1/membership/me", nil, key)
me = decodeData[map[string]any](t, env.Data)
if me["active"] != true {
t.Fatalf("expected active membership, got %#v", me)
}
env, _ = doJSON(t, r, http.MethodGet, "/api/v1/reports/"+reportID, nil, key)
unlocked := decodeData[map[string]any](t, env.Data)
if unlocked["has_deep_access"] != true {
t.Fatal("membership should unlock report detail")
}
if unlocked["detail"] == nil {
t.Fatal("expected detail via membership")
}
}
// Flow 5: profile update + soft
func TestFlowProfileUpdateDelete(t *testing.T) {
r, _ := setupAPI(t)
var key string
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "other", "birth_date": "1993-04-04", "display_name": "旧名", "relation_type": "friend",
}, key)
id := decodeData[map[string]any](t, env.Data)["id"].(string)
env, key = doJSON(t, r, http.MethodPatch, "/api/v1/profiles/"+id, map[string]any{
"display_name": "新名", "birth_date": "1993-04-05", "relation_type": "partner",
}, key)
updated := decodeData[map[string]any](t, env.Data)
if updated["display_name"] != "新名" {
t.Fatalf("display_name not updated: %#v", updated)
}
_, key = doJSON(t, r, http.MethodDelete, "/api/v1/profiles/"+id, nil, key)
env, _ = doJSON(t, r, http.MethodGet, "/api/v1/profiles", nil, key)
items, _ := decodeData[map[string]any](t, env.Data)["items"].([]any)
for _, it := range items {
m := it.(map[string]any)
if m["id"] == id {
t.Fatal("deleted profile still listed")
}
}
}
// Flow 4: profile → ask thread → message → assistant reply + quota
func TestFlowAskThread(t *testing.T) {
r, _ := setupAPI(t)
var key string
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
"relation": "self", "birth_date": "1991-02-14", "display_name": "我",
}, key)
profileID := decodeData[map[string]any](t, env.Data)["id"].(string)
env, key = doJSON(t, r, http.MethodGet, "/api/v1/ask/quota", nil, key)
q0 := decodeData[map[string]any](t, env.Data)
rem0, _ := q0["remaining"].(float64)
if rem0 < 1 {
t.Fatalf("expected free quota, got %#v", q0)
}
env, key = doJSON(t, r, http.MethodPost, "/api/v1/ask/threads", map[string]any{
"profile_id": profileID, "scene": "self",
}, key)
threadID := decodeData[map[string]any](t, env.Data)["id"].(string)
env, key = doJSON(t, r, http.MethodPost, "/api/v1/ask/threads/"+threadID+"/messages", map[string]any{
"content": "我想更了解自己",
}, key)
out := decodeData[map[string]any](t, env.Data)
asst, ok := out["assistant_message"].(map[string]any)
if !ok {
t.Fatalf("missing assistant_message: %#v", out)
}
content, _ := asst["content"].(string)
if content == "" {
t.Fatal("empty assistant reply")
}
q1, ok := out["quota"].(map[string]any)
if !ok {
t.Fatal("missing quota")
}
rem1, _ := q1["remaining"].(float64)
if rem1 != rem0-1 {
t.Fatalf("quota should decrease: before=%v after=%v", rem0, rem1)
}
env, _ = doJSON(t, r, http.MethodGet, "/api/v1/ask/threads/"+threadID+"/messages", nil, key)
items := decodeData[map[string]any](t, env.Data)["items"].([]any)
if len(items) < 2 {
t.Fatalf("expected user+assistant history, got %d", len(items))
}
}

Some files were not shown because too many files have changed in this diff Show More