chore(docs): Wave 0 调研产出 + P0/P1/P2 反馈调研
建立项目级标杆文档 docs/_overview/ 作为产品全景索引, 解决"PRD 零碎、文档膨胀、跨子系统调研无入口"的问题。 主要内容: - 00-index 总索引 + 维护协议 + 与 CLAUDE.md 关系 - 01-product-overview 产品全景脑图(6 角色 / 6 子系统 / 数据流 / 7 业务概念 / 8+1 AI 矩阵 / 22 术语) - 02a-miniprogram-page-matrix 小程序 21 页业务指纹 - 02b-adminweb-page-matrix admin-web 19 路由业务指纹 - 03-test-spec 测试规范 (L1-L5 分层 + 走查模板 + 75-95 case 估算) - 04-doc-conflicts 39 条冲突索引(P0×8 / P1×13 / P2×13 + 5 子项) - 04a/b/c-conflicts-*-detail 业务故事卡(7 字段:关联/逻辑/影响/选项/判定) - 05-orphan-pages-cleanup admin-web 6 孤儿页面处置(1 归档 + 4 保留) - WAVES-MASTER-PLAN.md 全 Wave 主计划(0-5,共 22-32 工作日) - WAVE-1-KICKOFF.md Wave 1 实施 kickoff - GLOBAL-DECISION-DASHBOARD.md 全局决策仪表板 反馈调研产物: - 04a-feedback/ P0 两轮反馈(8+8 项决策 + D-1/2/3 + F-1/2 子代理产出) - 04b-feedback/ P1 两轮反馈(13+1+5 项 + E-1/2/3/4 + G-1/2 子代理产出) - 04c-feedback/ P2 反馈(13 项 + 5 子项 + H-1/2/3 子代理产出) - NEO-DECISIONS-LOG 累积决策记录 关键追加发现 8 处 D Bug(原蓝本 0): - P0-3 看板沙箱接入(Wave 1 W1-T1) - P0-5 致命 1 (4 处 fdw_etl 残留, 已修 commit17f045a) - P0-5 致命 2 (JWT aud 缺失, 已修 commit17f045a) - P0-6 clearAllTasks 守卫 (Wave 3) - P0-8 DBViewer 黑名单漏 (已修 commit17f045a) - P1-3 task-detail 跳转传 task_id 而非 customer_id - P2-7 board-finance 隐式 null - 2 个独立 Bug (page_context.created_at + ClueCategory 字典) 参考: docs/_overview/00-index.md
This commit is contained in:
298
docs/_overview/04c-feedback/P2-6-and-P2-9-design.md
Normal file
298
docs/_overview/04c-feedback/P2-6-and-P2-9-design.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# P2-6 答疑 + P2-9 功能设计
|
||||
|
||||
> 日期:2026-05-04 / 触发:Neo 在 04c 反馈
|
||||
> 状态:**仅设计 + 待 Neo 审稿**,实施延后到 Wave 5
|
||||
> 作者:子代理(基于代码现状调研)
|
||||
|
||||
---
|
||||
|
||||
## P2-6 — 答疑:是否改百炼 APP1 的系统 Prompt
|
||||
|
||||
### 一、当前应用 1 prompt 状态
|
||||
|
||||
代码层面调研结论:
|
||||
|
||||
1. `apps/backend/app/ai/prompts/` 下只有 `app2~app8` 的 prompt 模板,**没有 `app1_*.py`**。
|
||||
2. APP1 的系统 prompt **完全在百炼控制台云端配置**,后端只通过 `app_id` 调用(参见 `apps/backend/app/ai/dashscope_client.py:69-178` 的 `DashScopeClient.call_app_stream`)。
|
||||
3. APP1 的输出形态:**纯文本 SSE 流**(`xcx_chat.py` 的 `chat_stream` 端点),前端按 token 渲染,流末尾发 `done` 事件。**没有任何结构化字段输出**。
|
||||
4. `biz.ai_conversations.title` 字段**已存在**(varchar(200),见 `db/zqyy_app/schemas/biz.sql:60`),但当前**只读不写** — `chat_service.py` 的列表查询 `SELECT title` 会读,创建 `_create_session` 与 SSE 落库流程都没有对它赋值。
|
||||
5. `chat_service.generate_title()` 已经有"自定义标题 > 客户姓名 > 首条消息前 20 字 > '新对话'"的回退链(`chat_service.py:494-522`),所以**当前 title 列表展示用的是回退链,不是 AI 摘要**。
|
||||
|
||||
### 二、修改方案
|
||||
|
||||
#### 是否需要改百炼 APP1 prompt:**N(不改)**
|
||||
|
||||
#### 理由
|
||||
|
||||
1. **流式纯文本 + 末尾插结构化字段会破坏 SSE 体验**:APP1 是逐 token 流式输出。在末尾再让模型多吐一段 `<title>关于王昕的消费</title>` 会让用户看到原始 XML/JSON 标记一闪而过,或需要前端再加解析层 — 复杂度不值。
|
||||
2. **百炼控制台的 prompt 是"配置在云端"的资产**:每次调整都要进百炼控制台手工改,**不在 git 仓库的版本管理内**,改动不可追溯,违背 NeoZQYY"一切逻辑改动可追溯"原则。
|
||||
3. **生成 title 是低价值场景**:title 只用于"对话历史列表"的辨识度,不参与业务逻辑。用全功率 APP1 模型 + 工具调用做摘要是杀鸡用牛刀。
|
||||
4. **回退链已经够用**:`generate_title` 已能用客户姓名 + 首条消息前 20 字给出可辨识的标题。优化空间是"AI 让标题更精炼",但代价不应该是改 APP1 主对话 prompt。
|
||||
|
||||
#### 推荐替代方案(三选一)
|
||||
|
||||
| 方案 | 描述 | 成本 | 推荐度 |
|
||||
|------|------|------|--------|
|
||||
| **R1**(推荐) | 首轮对话结束后,后端**异步**用 `qwen-turbo` 直调一次"摘要 prompt"(类似 APP6 备注分析的轻量调用),16 字内,失败回退到现有 `generate_title` | 低(qwen-turbo 价格远低于 APP1) | ★★★★★ |
|
||||
| **R2** | 直接用现有 `chat_service.generate_title` 回退链,**不上 AI 摘要**,把 title 列变成"客户姓名 / 首句截断"二选一 | 零额外成本 | ★★★★(若 Neo 觉得"够用",优先选这条) |
|
||||
| **R3** | 借用 APP6(备注分析)的结构化输出能力做摘要 | 中,APP6 prompt 要扩展支持"摘要"模式 | ★★(改动面比 R1 大,无收益) |
|
||||
|
||||
#### 推荐:**R1 + R2 组合(默认 R2,可观察后升级 R1)**
|
||||
|
||||
- **Wave 5 先落 R2**:首轮 user message 落库时,同步 `UPDATE ai_conversations SET title = LEFT(content, 16)`(纯 SQL,零外部调用)。这一步可以解决"列表里全是空 title"的问题,且与现有 `generate_title` 回退链完全兼容。
|
||||
- **后续观察**:如果产品反馈"前 16 字太机械"(例如用户首句是"你好,帮我看看"),再上 R1。R1 的实现可直接复用 `DashScopeClient.call_app(app_id=qwen-turbo, prompt="给下面对话写 ≤16 字标题: ...")` 模式,加进 background task 即可。
|
||||
- **不动 APP1 的百炼系统 prompt**。
|
||||
|
||||
#### 给 Neo 的最终回答
|
||||
|
||||
> **不改 APP1 prompt。** P2-6 选项 A 描述里那句"由应用 1 首轮自动摘要生成"是策略层措辞,落到工程上更稳妥的实现是后端首轮自己写 title(纯 SQL 截断或异步轻量 AI 调用),与 APP1 主对话流解耦。
|
||||
|
||||
---
|
||||
|
||||
## P2-9 — tenant-admin 无权限页编辑入口 功能设计
|
||||
|
||||
### 背景与目标
|
||||
|
||||
现状:`apps/miniprogram/miniprogram/pages/no-permission/no-permission.wxml:54` 硬编码 `<text class="reason-footer-value">厉超</text>`。
|
||||
Neo 反馈选项 A,但**不直接显示 site_admin 的真实姓名**(隐私 / 角色边界),改为在 tenant-admin 后台单独提供一个"编辑无权限页显示信息"的管理入口,由 site_admin / tenant_admin 自己填"对外昵称、联系方式、说明文字"。
|
||||
|
||||
### 一、数据模型
|
||||
|
||||
#### 表结构
|
||||
|
||||
```sql
|
||||
-- db/zqyy_app/migrations/2026-05-XX__create_site_contact_info.sql
|
||||
CREATE TABLE biz.site_contact_info (
|
||||
site_id bigint PRIMARY KEY REFERENCES biz.sites(id),
|
||||
display_name varchar(50) NOT NULL DEFAULT '门店管理员', -- 对外显示昵称(非真名)
|
||||
phone varchar(20), -- 可选联系电话
|
||||
wechat_id varchar(50), -- 可选微信号
|
||||
notes varchar(200), -- 自定义说明文字(覆盖默认"如有疑问请联系管理员")
|
||||
updated_by bigint REFERENCES auth.tenant_admins(id),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE biz.site_contact_info IS '门店对外联系信息(用于小程序无权限页等公开场景)';
|
||||
COMMENT ON COLUMN biz.site_contact_info.display_name IS '对外显示昵称,默认"门店管理员",不应填真实姓名';
|
||||
COMMENT ON COLUMN biz.site_contact_info.notes IS '自定义说明文字,空则用默认文案';
|
||||
```
|
||||
|
||||
#### 设计要点
|
||||
|
||||
1. **表名定为 `biz.site_contact_info`**:与 `biz.sites` 直接关联,语义上是门店对外联络配置,不属于 auth schema。
|
||||
2. **PK 直接用 `site_id`**:每个门店只有一行,不需要独立自增 id;减少 JOIN。
|
||||
3. **不存 `tenant_admin_id` 关联**:因为门店管理员可能轮换,且这里**显示的是"门店身份",不是"具体某个人"**。
|
||||
4. **`display_name` 默认 "门店管理员"**:迁移时所有现存 site 都 INSERT 一条默认记录,即便 tenant_admin 还没来配置,小程序也能显示通用文案。
|
||||
5. **不加 RLS**:这张表本质是公开信息(小程序未登录页要读),走应用层授权即可;tenant-admin 改自己门店通过 API 层 `WHERE site_id = ANY(:managed_site_ids)` 过滤。
|
||||
|
||||
#### 默认值迁移 SQL
|
||||
|
||||
```sql
|
||||
INSERT INTO biz.site_contact_info (site_id, display_name)
|
||||
SELECT id, '门店管理员'
|
||||
FROM biz.sites
|
||||
WHERE id NOT IN (SELECT site_id FROM biz.site_contact_info);
|
||||
```
|
||||
|
||||
### 二、后端 API 设计
|
||||
|
||||
#### 端点清单
|
||||
|
||||
| 方法 | 路径 | aud | 说明 |
|
||||
|------|------|-----|------|
|
||||
| GET | `/api/tenant/site-contact?siteId={id}` | tenant-admin | 取自己管辖门店的配置;不传 siteId 取首个 |
|
||||
| PATCH | `/api/tenant/site-contact` | tenant-admin | 编辑配置 |
|
||||
| GET | `/api/xcx/site-contact?siteId={id}` | miniapp(允许 limited token) | 小程序无权限页读取 |
|
||||
|
||||
#### 权限矩阵
|
||||
|
||||
| 角色 | GET 自己门店 | GET 任意门店 | PATCH 自己门店 | PATCH 任意门店 |
|
||||
|------|------|------|------|------|
|
||||
| `site_admin` | OK | 拒 | OK | 拒 |
|
||||
| `tenant_admin`(管多个 site) | OK(白名单内) | 拒(白名单外) | OK(白名单内) | 拒(白名单外) |
|
||||
| 小程序 limited token | OK(只能读自己 apply 时填的 site) | — | — | — |
|
||||
| 小程序 approved token | OK(自己绑定的 site) | — | — | — |
|
||||
|
||||
权限校验通过 `auth.tenant_admins.managed_site_ids` 白名单实现。
|
||||
|
||||
#### Pydantic schema
|
||||
|
||||
```python
|
||||
# apps/backend/app/schemas/site_contact.py
|
||||
from pydantic import Field
|
||||
from app.schemas.base import CamelModel
|
||||
|
||||
class SiteContactInfo(CamelModel):
|
||||
site_id: int
|
||||
display_name: str = Field(..., max_length=50)
|
||||
phone: str | None = Field(None, max_length=20)
|
||||
wechat_id: str | None = Field(None, max_length=50)
|
||||
notes: str | None = Field(None, max_length=200)
|
||||
updated_at: str # ISO 8601
|
||||
|
||||
class SiteContactUpdateRequest(CamelModel):
|
||||
site_id: int
|
||||
display_name: str = Field(..., min_length=1, max_length=50)
|
||||
phone: str | None = Field(None, max_length=20, pattern=r"^[\d\-\+\s]*$")
|
||||
wechat_id: str | None = Field(None, max_length=50)
|
||||
notes: str | None = Field(None, max_length=200)
|
||||
```
|
||||
|
||||
#### 校验规则
|
||||
|
||||
- `display_name`:必填,1-50 字符,**禁止包含全字符姓名常见字段**(可加简单黑名单:`厉超` 等明显的真名 — 实施时若 Neo 想软提示,可改为"前端给提示,后端不强制")。
|
||||
- `phone`:选填,允许数字 / `-` / `+` / 空格,长度 ≤ 20。
|
||||
- `notes`:选填,≤ 200 字符。
|
||||
- 写入时 `updated_by` 取 JWT 中 `tenant_admin_id`。
|
||||
|
||||
### 三、tenant-admin 前端页面设计
|
||||
|
||||
#### 路径与菜单位置
|
||||
|
||||
- 路径:`/site-contact`
|
||||
- 菜单:在现有侧边栏 `用户审核 / 用户管理 / Excel 上传 / 维客线索管理 / 店铺管理员` 之后增加 `门店联络信息`,图标用 `IdcardOutlined`。
|
||||
- 权限:tenant_admin 看到所有管辖门店的下拉切换;site_admin 只看自己门店(下拉禁用)。
|
||||
|
||||
#### 页面结构(ASCII 草图)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 门店联络信息 │
|
||||
│ 用于小程序"无权限页/账号被禁页"对外展示。 │
|
||||
│ 请勿填写真实姓名,推荐使用"店长" / "客服" 等通用称呼。 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 门店: [朗朗桌球(总店) ▾] ← tenant_admin 可切换 │
|
||||
│ │
|
||||
│ 对外昵称* [店长_______________] 1-50 字 │
|
||||
│ 联系电话 [13800000000_______] 可选,数字+-空格 │
|
||||
│ 微信号 [_____________________] 可选 │
|
||||
│ 自定义说明 [如有疑问请扫码加微信___________________] │
|
||||
│ 0/200 │
|
||||
│ │
|
||||
│ 最近修改:tenant_admin_neo / 2026-05-04 14:32:11 │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 恢复默认 │ │ 保 存 │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
│ ─── 预览 ────────────────────────────────────────────── │
|
||||
│ 小程序"无权限页"将显示: │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ 请联系管理员 │ │
|
||||
│ │ 店长 │ │
|
||||
│ │ ☎ 138-0000-0000 │ │
|
||||
│ │ 如有疑问请扫码加微信 │ │
|
||||
│ └─────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 实现要点
|
||||
|
||||
- 表单组件:`Form.useForm()` + Ant Design `Input` / `Input.TextArea`。
|
||||
- 实时预览:右下角同步渲染小程序的卡片样式(用 React 组件还原)。
|
||||
- 保存按钮:loading 态 + 成功 toast。
|
||||
- "恢复默认":一键填回 `display_name='门店管理员'`,清空其他字段。
|
||||
|
||||
### 四、小程序 no-permission 改造
|
||||
|
||||
#### 改造点
|
||||
|
||||
1. `no-permission.ts` `_checkStatus` 调用完 `/api/xcx/me` 后,**串行**再请求 `/api/xcx/site-contact?siteId={当前用户 site_id}`。
|
||||
2. 拿到的 `displayName / phone / wechatId / notes` 写入 `data.contactInfo`。
|
||||
3. `no-permission.wxml:51-54` 替换:
|
||||
|
||||
```xml
|
||||
<!-- 改造前 -->
|
||||
<view class="reason-footer">
|
||||
<text class="reason-footer-label">请联系管理员</text>
|
||||
<text class="reason-footer-value">厉超</text>
|
||||
</view>
|
||||
|
||||
<!-- 改造后 -->
|
||||
<view class="reason-footer">
|
||||
<text class="reason-footer-label">请联系管理员</text>
|
||||
<text class="reason-footer-value">{{contactInfo.displayName || '门店管理员'}}</text>
|
||||
<text class="reason-footer-extra" wx:if="{{contactInfo.phone}}">☎ {{contactInfo.phone}}</text>
|
||||
<text class="reason-footer-extra" wx:if="{{contactInfo.wechatId}}">微信:{{contactInfo.wechatId}}</text>
|
||||
<text class="reason-footer-notes" wx:if="{{contactInfo.notes}}">{{contactInfo.notes}}</text>
|
||||
</view>
|
||||
```
|
||||
|
||||
4. 兜底:接口失败 / 未配置时,`contactInfo = { displayName: '门店管理员' }`。
|
||||
5. 同时检查 `disabled` 状态下的 `contact-hint`(`no-permission.wxml:58-61` "如有疑问,请联系管理员"),如有 `notes` 则用 `notes` 覆盖。
|
||||
|
||||
#### 站点 ID 来源
|
||||
|
||||
小程序 limited token 也需能解出 `siteId`。当前 `auth.user_applications` 在用户 apply 时已记 `site_id`,后端 `/api/xcx/site-contact` 读 token 中 `site_id`(若 approved)或 `latestApplication.site_id`(若 rejected/disabled)。设计时**不要让前端传 siteId**(防伪造),由后端从 token / 申请记录推断。
|
||||
|
||||
### 五、默认值 / 迁移策略
|
||||
|
||||
1. **建表 + 默认数据**:迁移脚本同步 INSERT 所有现存 `biz.sites` 行,`display_name='门店管理员'`,其他字段 NULL。
|
||||
2. **朗朗桌球(总店)**:**不在迁移里写"店长"/"厉超",保持默认 '门店管理员'**,让 Neo 上线后手工进 tenant-admin 配置(避免误把真名写进迁移)。
|
||||
3. **新建门店**:在 `biz.sites` 新建门店的服务里(若有)或加 trigger,自动 INSERT 一行默认 site_contact_info。简化方案:每次小程序读取 API 时若行不存在,后端自动 INSERT 默认值再返回。
|
||||
|
||||
### 六、测试规范
|
||||
|
||||
#### 后端 unit(`apps/backend/tests/unit/`)
|
||||
|
||||
- `test_site_contact_schema.py`:Pydantic 校验
|
||||
- `display_name` 空字符串 / 超 50 字 / 含 emoji 的边界
|
||||
- `phone` 含中文 / 字母 → 拒
|
||||
- `notes` 超 200 字 → 拒
|
||||
- `test_site_contact_service.py`:服务层
|
||||
- 行不存在时自动返回默认值
|
||||
- 更新时 `updated_by` / `updated_at` 自动写入
|
||||
|
||||
#### 后端 integration(`apps/backend/tests/integration/`,连测试库)
|
||||
|
||||
- `test_site_contact_api.py`
|
||||
- tenant_admin A 改自己门店 → 200
|
||||
- tenant_admin A 改非管辖门店 → 403
|
||||
- site_admin 改非自己门店 → 403
|
||||
- 小程序 limited token 读自己 apply 的 site → 200
|
||||
- 小程序 token 读他人 site → 403
|
||||
- 跨 tenant_admin 的 site_id 隔离(白名单 ANY 校验)
|
||||
|
||||
#### tenant-admin e2e(Playwright,`apps/tenant-admin/tests/e2e/`)
|
||||
|
||||
- `site-contact.spec.ts`
|
||||
- 登录 tenant_admin,跳到 `/site-contact`,看到门店切换下拉
|
||||
- 改 `display_name` 为 "店长" → 保存 → toast → 重载页面 → 字段保留
|
||||
- 改 phone 为非法格式 → 表单校验失败,无法提交
|
||||
- 切换到非管辖门店 → 接口 403,前端显示"无权限"
|
||||
|
||||
#### 小程序 e2e(微信开发者工具,`apps/miniprogram/tests/e2e/`)
|
||||
|
||||
- `no-permission.spec.ts`(参考 P0-7 已有 e2e 模式)
|
||||
- mock `/api/xcx/me` 返回 `status='disabled'`
|
||||
- mock `/api/xcx/site-contact` 返回 `{displayName:'店长', phone:'13800000000'}`
|
||||
- 断言 wxml 渲染包含 "店长" 和 "13800000000",**不包含 "厉超"**
|
||||
- mock 接口 500 → 兜底显示 "门店管理员"
|
||||
|
||||
### 七、Wave 分配建议
|
||||
|
||||
| 任务 | Wave | 说明 |
|
||||
|------|------|------|
|
||||
| 数据模型 + 迁移 SQL | Wave 5 | 与 P10 tenant-admin 账号体系收尾联动 |
|
||||
| 后端 API(3 个端点 + Pydantic + 权限) | Wave 5 | 依赖数据模型 |
|
||||
| tenant-admin 前端页面 | Wave 5 | 依赖后端 API |
|
||||
| 小程序 no-permission 改造 | Wave 5 | 依赖 `/api/xcx/site-contact` |
|
||||
| 全部测试 | 跟随各自实施 | unit + integration + e2e 并行 |
|
||||
| 文档同步(`docs/database/` + `docs/_overview/02b-adminweb-page-matrix.md` + `02a-miniprogram-page-matrix.md`) | Wave 5 | 表 + API + 页面入口三处同步 |
|
||||
|
||||
预估工作量(单人):后端 0.5 天 / tenant-admin 0.5 天 / 小程序 0.2 天 / 测试 0.5 天 ≈ **1.7 天**。
|
||||
|
||||
### 八、给 Neo 的设计审稿提问
|
||||
|
||||
1. **表名**:推荐 `biz.site_contact_info`。是否同意?或倾向 `biz.site_public_contact` / `biz.site_display_info`?
|
||||
2. **`display_name` 真名黑名单**:是否需要后端硬拦"厉超 / 真实姓名"?推荐前端弹提示但后端不强制(避免遗漏 / 误伤)。
|
||||
3. **小程序读取时机**:`no-permission` `onLoad` 时拉一次就够,还是 `onShow` 每次都拉(允许 tenant_admin 改了之后用户立即看到)?推荐 `onShow`。
|
||||
4. **menu 菜单文字**:推荐 `门店联络信息`,可选 `小程序展示信息` / `对外展示设置`,Neo 选哪个?
|
||||
5. **新建门店时是否自动 INSERT 默认行**:推荐"读取时若不存在则自动 INSERT 默认值"(零侵入)。或"`biz.sites` INSERT 时加 trigger"(复杂)。Neo 选哪种?
|
||||
6. **是否要审计这张表的修改**:每次 PATCH 是否要落 `audit_log` 表?当前 NeoZQYY 没有通用审计表设计,推荐**先不加**,只靠 `updated_by / updated_at` 字段。
|
||||
7. **R2 vs R1(P2-6)的取舍**:Neo 是否同意 Wave 5 先做 R2(纯 SQL 取首句 16 字),后续观察后再考虑 R1(qwen-turbo 异步摘要)?
|
||||
|
||||
---
|
||||
|
||||
> 等 Neo 审稿确认 → 进 Wave 5 实施 → 实施时按本文档分阶段提 PR。
|
||||
Reference in New Issue
Block a user