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:
+3
-1
@@ -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
@@ -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 1–18 · 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
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
| `scale` | 探索测试定义与计分 | Yes |
|
||||
| `relation` | RelationInsight | Yes **必做** |
|
||||
| `report` | GrowthReport 读取 + **权益裁剪** | Yes |
|
||||
| `ask` | Thread/Message;规则或薄 LLM | Skeleton |
|
||||
| `ask` | Thread/Message;P1 规则引擎 + 额度 | Yes(Demo) |
|
||||
| `companion` | SolarTerm 读、Mood 写 | Shell |
|
||||
| `membership` | 成长会员状态与额度 | Yes |
|
||||
| `order` | Order + pay-mock + DeepAccess 发放 | Yes |
|
||||
|
||||
+36
-14
@@ -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 或书面 smoke(P1 关键流须自动化)
|
||||
- [ ] Health OK
|
||||
- [ ] Review Report 已输出
|
||||
- [ ] Spec §14 Implementation Notes / Gaps 已更新
|
||||
- [ ] Scope label 正确(Demo / Feature Complete / …)
|
||||
|
||||
+12
-4
@@ -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.
|
||||
|
||||
@@ -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
@@ -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** | 单功能满足本文件 §1–7 |
|
||||
| **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?
|
||||
|
||||
@@ -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 1–18 |
|
||||
| [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 式省略分析。
|
||||
|
||||
@@ -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
@@ -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`)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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`)。
|
||||
|
||||
@@ -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 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -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 1–18 · 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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 Notes;Gaps 标 `Gap:`。
|
||||
8. 对照 Spec §12 + `checklists/feature.md` + DoD 验收。
|
||||
|
||||
## Stop
|
||||
|
||||
- Spec §4 / §7 / §9 / §12 为空 → 禁止编码。
|
||||
- 需求不清 → ASK FIRST。
|
||||
@@ -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
@@ -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 5–18) |
|
||||
| [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
@@ -8,7 +8,20 @@
|
||||
| 用户路径 | 功能命名 |
|
||||
| 商业模型(测评→档案→AI→会员→咨询) | 叙事:成长 / 认识自己 / 理解他人 |
|
||||
|
||||
**不是**竞品复制品。UI/PRD/Ask **禁止**出现:测测(自称)、解码、运势、塔罗、吉凶、命理预测;星盘不作核心叙事。
|
||||
**不是**竞品品牌复制品。UI/PRD/Ask **禁止**出现:测测(自称)、占卜/算命恐吓、医疗疗效承诺;禁止以「塔罗」作主路径入口名。
|
||||
|
||||
**能力与系统可按逆向规范完整对标**:见 [../design/reverse-engineering-spec.md](../design/reverse-engineering-spec.md)(Evidence First · STEP 1–18 · 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)。
|
||||
|
||||
@@ -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 1–18)。
|
||||
**前端全量逆向设计套件(详细):** [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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 1–18 · 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 缺失 = 伪完成。
|
||||
@@ -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.6–2.8;P2 三模块不计入 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 Complete(mock) | 真支付另开 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)· 真支付仍为 mock(P1 接受)· 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 | Gap:birth_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 | 无专用 API(Spec 已写) |
|
||||
| 实现 | 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 择一 | |
|
||||
@@ -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 |
|
||||
| 真支付 | 部署阶段 |
|
||||
@@ -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)
|
||||
@@ -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. 用户角色
|
||||
|
||||
对齐当前 Identity(Visitor 设备身份 + 可选升级)。写清**实际能做什么**,勿照搬竞品。
|
||||
|
||||
| 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 解释 · 统计 · 验收。
|
||||
@@ -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) |
|
||||
@@ -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` | AskPage(ChatThread) |
|
||||
|
||||
```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;集成 thread;AskPage.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 1–2
|
||||
|
||||
| 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 5–18)
|
||||
|
||||
> 支撑前端全功能的数据 / 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 ZSET;Banner 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 5–18)
|
||||
|
||||
- [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 1–4 |
|
||||
| [03-tab-ask.md](03-tab-ask.md) | 「问」测测 AI + 真人 1v1 | STEP 1–4 |
|
||||
| [04-tab-online.md](04-tab-online.md) | 在线(达人 Marketplace) | STEP 1–4 |
|
||||
| [05-tab-mine.md](05-tab-mine.md) | 我的 | STEP 1–4 |
|
||||
| [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 5–18 |
|
||||
| **[complete-design/](complete-design/README.md)** | **完整产品设计包(Design Complete)** — 实现与模拟器校验前的唯一前端设计输入 | 定稿升格 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Completeness Checklist(套件级)
|
||||
|
||||
- [x] 五 Tab 页面拆解
|
||||
- [x] 首页宫格与「更多」工具域
|
||||
- [x] AI / 达人双轨
|
||||
- [x] 星盘与合盘工具面
|
||||
- [x] 测评 / 沉浸体验
|
||||
- [x] 横切系统 STEP 5–18
|
||||
- [ ] 【待真机补证】逐按钮热区坐标与最新版 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 底部五 Tab(D 定稿)
|
||||
|
||||
| 序 | 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` | 12–16 |
|
||||
| `--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;设计层不绑定具体库
|
||||
+119
@@ -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` 各页的 **校验 ID(V-\*)** 采集证据。
|
||||
2. 差异写入 Diff 表 → 回写对应设计文档 / OpenAPI。
|
||||
3. 不阻塞当前 Feature Complete:未验证项保持「设计定稿 (D) + 待模拟器」。
|
||||
|
||||
---
|
||||
|
||||
## 2. 环境准备
|
||||
|
||||
| 项 | 建议 |
|
||||
|---|---|
|
||||
| 模拟器 | Android Emulator API 33+,安装官方测测 APK(或应用商店包) |
|
||||
| 代理 | mitmproxy / Charles;模拟器 Wi‑Fi 代理指向宿主机 |
|
||||
| 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
|
||||
../00–08 逆向分析(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提问」为高意图收银,列表浏览需可退回。
|
||||
@@ -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 做深留存 |
|
||||
|
||||
| In(P1 壳) | Out(P1) |
|
||||
|---|---|
|
||||
| 今日节气展示(本地/轻 API) | 完整心情 OS |
|
||||
| 生活建议(非运势) | 疗效承诺 |
|
||||
| 路由与 Tab 可达 | 强制日记社交 |
|
||||
|
||||
| In(P2) | |
|
||||
|---|---|
|
||||
| 心情记录落库、成长记录 | |
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户价值
|
||||
|
||||
1. **为何需要:** 需要轻量每日触达,不只是一次性测试。
|
||||
2. **P1 获得:** 今日节气与建议壳。
|
||||
3. **付费:** N/A(P1)。
|
||||
|
||||
---
|
||||
|
||||
## 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) · 心情已按产品「其他先做」落地 |
|
||||
@@ -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 收口 |
|
||||
@@ -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 |
|
||||
@@ -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` | ImageCardPage(P2 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 JSON(id, 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 |
|
||||
@@ -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` | LifeRhythmPage(P2 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 | 绑定 Profile;birth_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 |
|
||||
@@ -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 unlock;MembershipPage.spec |
|
||||
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · mock 闭环设计闭合 · 实现 PASS · 测试 PASS |
|
||||
@@ -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 创建画像报告
|
||||
↓
|
||||
展示基础结果(summary:overview、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_access;H5 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
|
||||
@@ -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
|
||||
↓
|
||||
创建 Self(display_name + birth_date)
|
||||
↓
|
||||
可选:添加 TA(relation=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 L2;ProfilePage 四态 |
|
||||
| Process Review | 2026-08-02 [P1-PROCESS-REVIEW](P1-PROCESS-REVIEW.md) · 设计闭合 · 实现 PASS · 测试 PARTIAL |
|
||||
@@ -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 |
|
||||
@@ -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 类型 | 第三方导出 PDF(Gap) |
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
@@ -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 |
|
||||
@@ -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 | |
|
||||
| 挂 Profile;GrowthReport `star` / `synastry` | |
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户价值
|
||||
|
||||
1. **为何需要:** 用熟悉的星座语言认识自己的节奏、运势与互动偏好。
|
||||
2. **完成后获得:** 本命轮盘、三大星座、行星/宫位、运势与行运;合盘页看五主盘、次限与三指数。
|
||||
3. **为何付费:** 完整相位表、年运/一生深析、合盘相处长文、推运深文案与跨盘相位详解。
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户角色
|
||||
|
||||
| Actor | 能力 |
|
||||
|---|---|
|
||||
| Visitor | 有档案可生成基础报告(轮盘 + 今日运势 + 合盘指数) |
|
||||
| DeepAccess / VIP | 可见 detail(完整相位、年运/一生深析、合盘深文案) |
|
||||
| 无档案 | Empty → 页内填生日(可选时/地) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 用户流程
|
||||
|
||||
```text
|
||||
/explore 或 /star
|
||||
↓
|
||||
确认 Profile(birth_date;birth_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 | 必须绑定用户可访问的 Profile;birth_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 Ephemeris(CGO,默认 Moshier);热带 + Whole Sign |
|
||||
| 实现 | `POST /reports/star` · `POST /reports/synastry` · nearby/invites · H5 `/star` · `/synastry` · invite |
|
||||
+45
-60
@@ -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):** Decode→Portrait · Match→RelationInsight · Unlock→DeepAccess
|
||||
|
||||
---
|
||||
|
||||
## 禁用 → 使用(速查)
|
||||
|
||||
| 不使用 | 使用 |
|
||||
|---|---|
|
||||
| 解码 | 性格探索 / 个人画像 |
|
||||
| 测算 | 分析 |
|
||||
| 测试结果(主文案) | 探索结果 |
|
||||
| 运势 | 生活建议 / 今日建议 |
|
||||
| 命盘 / 星盘档案(核心) | 个人画像 |
|
||||
| 合盘 | 关系理解 |
|
||||
| 解锁(主按钮) | 查看深度版 / 完整分析 |
|
||||
| 算命 | 自我探索 |
|
||||
| 预测未来 | 了解趋势 / 认识模式 |
|
||||
| 改命 | 成长改善 |
|
||||
| 会员(若需品牌感) | 成长会员(权益页标题优先) |
|
||||
|
||||
星盘:探索内可选维度,禁止作家园主标题/品牌 slogan。
|
||||
**Code aliases(可用):** Decode≈Portrait · Match≈RelationInsight · Zodiac≈StarProfile
|
||||
|
||||
---
|
||||
|
||||
## Ask 人设
|
||||
|
||||
- 是:了解你的智能伙伴
|
||||
- 不是:占卜、预测、算命
|
||||
- **不是:占卜师、算命先生**
|
||||
|
||||
场景:认识自己 · 理解关系 · 职业探索 · 情绪整理 · 生活建议
|
||||
场景:认识自己 · 理解关系 · 职业探索 · 情绪整理 · 生活建议 · 星座聊聊
|
||||
|
||||
---
|
||||
|
||||
## 付费墙话术
|
||||
|
||||
- 免费:基础画像 / 基础探索结果
|
||||
- 付费:完整分析 · 行为模式 · 关系建议 · 成长方向 → **深度版**
|
||||
- 订阅标题:**成长会员**
|
||||
- 禁止:虚假倒计时、恐吓、道德绑架
|
||||
- 免费:基础解码 / 基础星座 / 基础匹配
|
||||
- 付费:完整分析 → **深度版** / 解锁完整分析
|
||||
- 订阅:**成长会员**
|
||||
- 禁止:恐吓、道德绑架、虚假倒计时
|
||||
|
||||
@@ -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 |
|
||||
| L0–L3 | 全绿(收口时) |
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
验收路径:首页画像 → 深度版 → 分享 → 关系 → 会员 → 问答 → 探索(含刷新恢复)→ 我的成长报告
|
||||
@@ -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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 engines(portrait / 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
@@ -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/§12;playbook: 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
|
||||
- Backend(service 竖切)→ Frontend
|
||||
- Follow patterns + playbooks
|
||||
↓
|
||||
4. Verify
|
||||
- commands(test / health / build:h5)
|
||||
- review.md
|
||||
- DoD + checklist
|
||||
5. Verify
|
||||
- Spec §12 Acceptance + definition-of-done.md(非仅 build)
|
||||
- commands(L0/L1/L2 as required)
|
||||
- Review Report + checklist
|
||||
↓
|
||||
5. Document
|
||||
- OpenAPI / erd / feature 标记若行为变化
|
||||
6. Document
|
||||
- OpenAPI / erd / Spec Gaps / p1-status 标签若变化
|
||||
↓
|
||||
Commit(Conventional Commits,one concern)
|
||||
```
|
||||
@@ -45,4 +50,4 @@ Commit(Conventional Commits,one concern)
|
||||
|
||||
## Playbooks
|
||||
|
||||
Prefer: `add-api` · `new-page` · `new-table` · `payment` · `login`
|
||||
Prefer: `feature-spec` · `add-api` · `new-page` · `new-table` · `payment` · `login`
|
||||
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
../../.ai/definition-of-done.md
|
||||
@@ -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
|
||||
|
||||
@@ -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 1–18 · 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 PASS;use labels: Demo | Feature Complete | P1 Complete.
|
||||
- Print **Review Report**(见 `definition-of-done.md` §7)when 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` 做完整逆向;落地用愈心谷命名,不用竞品品牌词与恐吓话术。
|
||||
|
||||
@@ -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 1–18).
|
||||
4. Prefer `patterns/` + `examples/` + `playbooks/` over free-form invention.
|
||||
5. Finish with Review + DoD + matching checklist.
|
||||
|
||||
## Layout
|
||||
|
||||
|
||||
@@ -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 专业授权)。
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) != ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user