建立项目级标杆文档 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
17 KiB
P2-6 答疑 + P2-9 功能设计
日期:2026-05-04 / 触发:Neo 在 04c 反馈 状态:仅设计 + 待 Neo 审稿,实施延后到 Wave 5 作者:子代理(基于代码现状调研)
P2-6 — 答疑:是否改百炼 APP1 的系统 Prompt
一、当前应用 1 prompt 状态
代码层面调研结论:
apps/backend/app/ai/prompts/下只有app2~app8的 prompt 模板,没有app1_*.py。- APP1 的系统 prompt 完全在百炼控制台云端配置,后端只通过
app_id调用(参见apps/backend/app/ai/dashscope_client.py:69-178的DashScopeClient.call_app_stream)。 - APP1 的输出形态:纯文本 SSE 流(
xcx_chat.py的chat_stream端点),前端按 token 渲染,流末尾发done事件。没有任何结构化字段输出。 biz.ai_conversations.title字段已存在(varchar(200),见db/zqyy_app/schemas/biz.sql:60),但当前只读不写 —chat_service.py的列表查询SELECT title会读,创建_create_session与 SSE 落库流程都没有对它赋值。chat_service.generate_title()已经有"自定义标题 > 客户姓名 > 首条消息前 20 字 > '新对话'"的回退链(chat_service.py:494-522),所以当前 title 列表展示用的是回退链,不是 AI 摘要。
二、修改方案
是否需要改百炼 APP1 prompt:N(不改)
理由
- 流式纯文本 + 末尾插结构化字段会破坏 SSE 体验:APP1 是逐 token 流式输出。在末尾再让模型多吐一段
<title>关于王昕的消费</title>会让用户看到原始 XML/JSON 标记一闪而过,或需要前端再加解析层 — 复杂度不值。 - 百炼控制台的 prompt 是"配置在云端"的资产:每次调整都要进百炼控制台手工改,不在 git 仓库的版本管理内,改动不可追溯,违背 NeoZQYY"一切逻辑改动可追溯"原则。
- 生成 title 是低价值场景:title 只用于"对话历史列表"的辨识度,不参与业务逻辑。用全功率 APP1 模型 + 工具调用做摘要是杀鸡用牛刀。
- 回退链已经够用:
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 自己填"对外昵称、联系方式、说明文字"。
一、数据模型
表结构
-- 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 '自定义说明文字,空则用默认文案';
设计要点
- 表名定为
biz.site_contact_info:与biz.sites直接关联,语义上是门店对外联络配置,不属于 auth schema。 - PK 直接用
site_id:每个门店只有一行,不需要独立自增 id;减少 JOIN。 - 不存
tenant_admin_id关联:因为门店管理员可能轮换,且这里显示的是"门店身份",不是"具体某个人"。 display_name默认 "门店管理员":迁移时所有现存 site 都 INSERT 一条默认记录,即便 tenant_admin 还没来配置,小程序也能显示通用文案。- 不加 RLS:这张表本质是公开信息(小程序未登录页要读),走应用层授权即可;tenant-admin 改自己门店通过 API 层
WHERE site_id = ANY(:managed_site_ids)过滤。
默认值迁移 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
# 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 DesignInput/Input.TextArea。 - 实时预览:右下角同步渲染小程序的卡片样式(用 React 组件还原)。
- 保存按钮:loading 态 + 成功 toast。
- "恢复默认":一键填回
display_name='门店管理员',清空其他字段。
四、小程序 no-permission 改造
改造点
no-permission.ts_checkStatus调用完/api/xcx/me后,串行再请求/api/xcx/site-contact?siteId={当前用户 site_id}。- 拿到的
displayName / phone / wechatId / notes写入data.contactInfo。 no-permission.wxml:51-54替换:
<!-- 改造前 -->
<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>
- 兜底:接口失败 / 未配置时,
contactInfo = { displayName: '门店管理员' }。 - 同时检查
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 / 申请记录推断。
五、默认值 / 迁移策略
- 建表 + 默认数据:迁移脚本同步 INSERT 所有现存
biz.sites行,display_name='门店管理员',其他字段 NULL。 - 朗朗桌球(总店):不在迁移里写"店长"/"厉超",保持默认 '门店管理员',让 Neo 上线后手工进 tenant-admin 配置(避免误把真名写进迁移)。
- 新建门店:在
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,前端显示"无权限"
- 登录 tenant_admin,跳到
小程序 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 → 兜底显示 "门店管理员"
- mock
七、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 的设计审稿提问
- 表名:推荐
biz.site_contact_info。是否同意?或倾向biz.site_public_contact/biz.site_display_info? display_name真名黑名单:是否需要后端硬拦"厉超 / 真实姓名"?推荐前端弹提示但后端不强制(避免遗漏 / 误伤)。- 小程序读取时机:
no-permissiononLoad时拉一次就够,还是onShow每次都拉(允许 tenant_admin 改了之后用户立即看到)?推荐onShow。 - menu 菜单文字:推荐
门店联络信息,可选小程序展示信息/对外展示设置,Neo 选哪个? - 新建门店时是否自动 INSERT 默认行:推荐"读取时若不存在则自动 INSERT 默认值"(零侵入)。或"
biz.sitesINSERT 时加 trigger"(复杂)。Neo 选哪种? - 是否要审计这张表的修改:每次 PATCH 是否要落
audit_log表?当前 NeoZQYY 没有通用审计表设计,推荐先不加,只靠updated_by / updated_at字段。 - R2 vs R1(P2-6)的取舍:Neo 是否同意 Wave 5 先做 R2(纯 SQL 取首句 16 字),后续观察后再考虑 R1(qwen-turbo 异步摘要)?
等 Neo 审稿确认 → 进 Wave 5 实施 → 实施时按本文档分阶段提 PR。