feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本

包含多个会话的累积代码变更:
- backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔
- admin-web: ETL 状态页、任务管理、调度配置、登录优化
- miniprogram: 看板页面、聊天集成、UI 组件、导航更新
- etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强
- tenant-admin: 项目初始化
- db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8)
- packages/shared: 枚举和工具函数更新
- tools: 数据库工具、报表生成、健康检查
- docs: PRD/架构/部署/合约文档更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neo
2026-04-06 00:03:48 +08:00
parent 70324d8542
commit 6f8f12314f
515 changed files with 76604 additions and 7456 deletions

View File

@@ -4,6 +4,9 @@
> 预估工作量:中等
> 前置条件P5-A 已完成AI 骨架就绪、NS1 已完成(后端 API 数据结构确定)
> 参考基准:`docs/prd/specs/P5-miniapp-ai-integration.md`P5-B 阶段定义)
> SPEC 状态:✅ 三件套已完成requirements.md → design.md → tasks.md
> SPEC 路径:`.kiro/specs/ai-prompt-refinement/`
> 实施状态:⏳ 未开始(代码实施待启动)
---
@@ -17,7 +20,7 @@ P5-A 阶段已交付 AI 集成管道:百炼封装、缓存 API、SSE 框架、
| 应用 | 文件 | 骨架状态 | 待细化字段 |
|------|------|---------|-----------|
| 应用 1 | `app1_chat.py` | 页面上下文文本化工具留接口 | `page_context``screen_content` 各页面文本化 |
| 应用 1 | `app1_chat.py` | 页面上下文文本化工具留接口 | `page_context` + `screen_content` 合并为 `build_page_text()` 输出(详见 3.7 设计决策) |
| 应用 3 | `app3_clue.py` | `build_prompt()` 占位 | `consumption_records`DWD+DWS 订单明细全维度) |
| 应用 4 | `app4_analysis.py` | `build_prompt()` 占位 | `service_history``assistant_info` |
| 应用 5 | `app5_tactics.py` | `build_prompt()` 占位 | `service_history``assistant_info`(同应用 4 |
@@ -316,30 +319,50 @@ NS2 需确认事项:
### 3.7 应用 1页面上下文传递与文本化
#### 当前问题P5 PRD 与 RNS1.4 实现的差异)
#### P5 原始设计 vs NS2 实际方案
P5 PRD 原始设计
```
前端跳转 chat.html 时传入 source_page + page_context + screen_content
→ 后端接收后文本化 → 拼接为首条 user message
```
P5 PRD 定义应用 1 首条 Prompt 包含三个字段
RNS1.4 当前实现:
```
前端跳转 chat 页面时仅传入 contextType + contextId如 task/12345
→ 后端 _build_page_context() 为占位实现page_context 和 screen_content 为空
→ AI 无法获得页面上下文
```
| 字段 | P5 定义 | 含义 |
|------|---------|------|
| `source_page` | 来源页面标识 | 如 `task-detail``board-finance` |
| `page_context` | 页面上下文摘要 | 结构化数据,后端可从 DB 获取 |
| `screen_content` | 用户当前屏幕可见内容的文本化描述 | 需前端传入当前视口内容 |
#### 解决方案:后端根据 contextType 自动获取页面上下文
AI需求2.md 原文对应:
- `page_context` → "用户正在查看页面内容:页面上下文(来源页面+内容摘要)"
- `screen_content` → "用户页面视野内容:当前屏幕内容"
采用方案 B简化前端后端自动查询与 RNS1.4 已有的 `contextType`/`contextId` 参数兼容:
#### `screen_content` 的设计决策
> ⚠️ **设计决策NS2 与 P5 的差异点)**NS2 不要求前端传入 `screen_content`,改由后端根据 `contextType` + `contextId` + 筛选参数自动查询数据库,生成等效的页面上下文文本。
**决策理由:**
1. 微信小程序前端难以将当前屏幕 DOM 序列化为结构化文本(无 `innerText` 等 Web API
2. RNS1.4 已实现 `contextType`/`contextId` 参数机制,后端可据此精确获取页面数据
3. 后端自动查询可获取比前端视口更完整的上下文(如关联的 AI 分析、历史备注等)
4. 看板类页面通过额外传入筛选参数(`timeDimension``dimension` 等),后端可还原用户当前视图的数据范围
**等效覆盖说明:**
- P5 的 `page_context`(结构化数据摘要)→ NS2 通过 `build_page_text()` 从 DB 获取,完全覆盖
- P5 的 `screen_content`(屏幕可见内容)→ NS2 通过 `contextType` + 筛选参数推断用户当前视图,近似覆盖
- 对于详情类页面task-detail、customer-detail 等),后端获取的数据与用户屏幕内容高度一致
- 对于看板类页面,通过筛选参数还原当前视图的数据维度和范围
**局限性:**
- 无法获取用户滚动位置(如长列表中用户正在看第几条)
- 无法获取用户输入中但未提交的内容
- 看板页面若前端未传筛选参数,使用默认值可能与用户实际视图不一致
#### 实际方案:后端根据 contextType 自动获取页面上下文
与 RNS1.4 已有的 `contextType`/`contextId` 参数兼容:
```
前端传入 contextType="task-detail" + contextId="12345"
→ 后端 build_page_text("task-detail", 12345, site_id)
前端传入 contextType="task-detail" + contextId="12345"+ 看板类页面的筛选参数)
→ 后端 build_page_text("task-detail", 12345, site_id, filters?)
→ 后端从数据库获取任务详情、客户信息、备注、AI 分析等
→ 格式化为结构化中文文本
→ 格式化为结构化中文文本(同时覆盖 page_context 和 screen_content 的信息需求)
→ 拼接为首条 user message 的 page_context 字段
```
@@ -435,17 +458,19 @@ RNS1.4 当前实现:
```
用户点击 AI 入口
→ 前端传入 contextType + contextId+ 可选筛选参数)
→ 前端传入 contextType + contextId+ 看板类页面的筛选参数)
→ 后端 build_page_text(contextType, contextId, site_id, filters?)
→ 从数据库获取对应页面数据,格式化为结构化中文文本
→ 文本同时覆盖 P5 定义的 page_context数据摘要和 screen_content屏幕内容
→ 拼接为首条 user message 的 page_context 字段
→ 注入 biz_paramsUser_ID/Role/Nickname到 system prompt
→ SSE 流式调用百炼 API
```
> ⚠️ 与 P5 PRD 原始设计的差异P5 设计为前端传入 `source_page` + `page_context` + `screen_content`
> RNS1.4 实际实现为前端传入 `contextType` + `contextId`,后端自动查询数据库获取上下文。
> NS2 沿用 RNS1.4 方案(后端自动获取),不要求前端传入原始页面数据。
> ⚠️ 与 P5 PRD 原始设计的差异(详见 3.7 设计决策):
> P5 设计为前端传入 `source_page` + `page_context` + `screen_content` 三个独立字段,
> NS2 采用后端自动查询方案,前端传入 `contextType` + `contextId` + 筛选参数,
> 后端通过 `build_page_text()` 生成合并的页面上下文文本,等效覆盖 `page_context` 和 `screen_content` 的信息需求。
---
@@ -467,55 +492,67 @@ RNS1.4 当前实现:
---
## 七、预审查清单(SPEC 启动前确认)
## 七、预审查清单(确认)
> 以下问题已在 SPEC 细化阶段requirements.md / design.md中逐一确认。
### 7.1 数据结构
1. **消费记录字段范围**`consumption_records` 中每条记录需要包含哪些字段是否需要包含折扣信息discount_manual/discount_other是否需要包含支付方式明细balance_pay/cash_pay/online_pay
2. **服务记录字段范围**`service_history` 中每条记录需要包含哪些字段是否需要包含台桌类型room_category和客户评价
3. **备注内容截断**`all_notes` 中每条备注是否需要全文传入?长备注是否截断?截断长度?
4. **会员卡明细粒度**`member_cards` 是否需要包含卡号、开卡日期、有效期等详细信息,还是只需要卡类型和余额?
1. **消费记录字段范围**每条记录包含 `settle_date``settle_type``items_sum``table_charge_money``assistant_pd_money``assistant_cx_money``goods_money``room_name``duration_minutes``assistant_names` 共 10 个字段。不包含折扣明细和支付方式明细token 预算有限,这些字段对 AI 分析价值低)。
2. **服务记录字段范围**每条记录包含 `service_date``duration_minutes``items_sum``room_name``is_pd`(是否陪打)。不包含台桌类型和客户评价
3. **备注内容截断**单条备注最大 500 字符,超出截断并附加"…(已截断)"标记。备注总数最多 50 条(按 `created_at DESC`)。
4. **会员卡明细粒度**仅包含 `card_type`(卡类型)、`balance`(余额)、`gift_balance`(赠送余额),不含卡号、开卡日期、有效期。
### 7.2 Prompt 优化
5. **Token 预算**每个应用的首条 Prompt 的 token 上限是多少?百炼 API 的单次请求 token 限制?
6. **数据时间窗口**消费记录默认近 3 个月,是否需要可配置?不同应用是否需要不同时间窗口
7. **空数据处理**客户无消费记录/无备注/无服务历史时Prompt 如何处理?是否需要特殊提示词?
5. **Token 预算**应用 3/4/5/6/7 的 system message content ≤ 8000 字符(约 4000 token应用 1 的 system prompt含页面上下文≤ 4000 字符(约 2000 token)。
6. **数据时间窗口**:默认近 3 个月,通过 `months` 参数可配置。各应用统一使用 3 个月窗口
7. **空数据处理**使用明确的空状态提示词(如"该客户无消费记录,请基于已有信息分析"),不传入空数据不做说明。`reference` 无历史数据时设为空对象并标注"暂无历史线索"。
### 7.3 页面文本化(应用 1
8. **文本化格式**页面上下文是输出为结构化中文文本还是 JSONAI 对哪种格式理解更好?
9. **数据量控制**:每个页面上下文的字符上限?是否需要根据页面类型动态调整?
10. **实时性要求**应用 1 的页面上下文是否需要实时获取最新数据?还是可以使用缓存(如 task_detail_cache
8. **文本化格式**:输出为结构化中文描述文本(分段标题 + 缩进),非 JSON。中文描述更便于 AI 理解上下文语义。
9. **数据量控制**:每个页面上下文统一 ≤ 2000 字符,不按页面类型动态调整。超出截断并标注。
10. **实时性要求**实时获取最新数据(不使用缓存),设置 5 秒 FDW 查询超时。数据获取失败时返回降级文本,不阻断对话。
### 7.4 性能与安全
11. **FDW 查询并发**多个数据获取函数是否可以并发执行asyncio.gatherFDW 连接池是否支持?
12. **数据脱敏**传入百炼 API 的数据中,哪些字段需要脱敏?member_phone 已断档不传,还有其他敏感字段吗?
13. **错误降级**某个数据获取函数失败时(如 FDW 超时),是否跳过该部分继续生成 Prompt还是整体失败
11. **FDW 查询并发**支持。使用 `asyncio.gather(return_exceptions=True)` 并发执行多个数据获取函数,部分失败不阻断。同一连接上的多个 FDW 查询串行执行(共享 RLS 设置)。
12. **数据脱敏**`member_phone` 已断档不传。会员信息通过 `member_id JOIN v_dim_member (scd2_is_current=1)` 获取昵称,不传入手机号等敏感字段。`build_page_text()` 输出中不包含 `member_phone`
13. **错误降级**部分失败继续。失败部分使用默认空值(空数组/空对象),在 Prompt 中标注"该部分数据获取失败",继续生成 Prompt 并调用百炼 API。确保 Prompt JSON 不含 `null`
---
## 八、任务清单(草案SPEC 细化后调整
## 八、任务清单(已细化,详见 SPEC tasks.md
### Batch A共享数据获取层
- [ ] T1创建 `data_fetchers/member_data.py`(客户消费数据获取,应用 3/6/7 共用)
- [ ] T2创建 `data_fetchers/assistant_data.py`(助教信息 + 服务历史获取,应用 4/5 共用)
- [ ] T3创建 `data_fetchers/page_context.py`(页面上下文文本化框架,应用 1 专用)
> SPEC 三件套已完成:`requirements.md`14 条需求)→ `design.md`17 个正确性属性)→ `tasks.md`17 个顶层任务)。
> 以下为 PRD 级任务概览,详细实施步骤见 `.kiro/specs/ai-prompt-refinement/tasks.md`。
### Batch BPrompt 拼接实现
- [ ] T4完善 `app3_clue.py``build_prompt()`(客户消费数据 → 维客线索分析
- [ ] T5完善 `app4_analysis.py``build_prompt()`(助教+客户数据 → 关系分析
- [ ] T6完善 `app5_tactics.py``build_prompt()`(复用应用 4 数据 + task_suggestion
- [ ] T7完善 `app6_note.py``build_prompt()`(备注+客户数据 → 备注分析)
- [ ] T8完善 `app7_customer.py``build_prompt()`(客户全量数据 → 运营策略)
### Batch A共享数据获取层tasks 1-4
- [ ] T1创建 `data_fetchers/` 模块骨架(`__init__.py` + 3 个子模块
- [ ] T2实现 `member_data.py``fetch_member_consumption_data` + `fetch_member_notes`
- [ ] T3实现 `assistant_data.py``fetch_assistant_info` + `fetch_service_history`
- [ ] T4检查点 — 数据获取层完成
### Batch C应用 1 页面文本化 + 前端配合
- [ ] T9实现各页面类型的文本化函数task-detail/customer-detail/board-*/performance 等
- [ ] T10补充 `app1_chat.py``_build_page_context()` 调用文本化函数,根据 `contextType` 路由到对应文本化函数
- [ ] T11前端补充看板类页面的筛选参数传递board-finance/board-customer/board-coach 跳转 chat 时传入当前筛选条件
- [ ] T12确认 biz_params 端到端正确性(前端 JWT → 后端提取 user_id/role/nickname → system prompt 注入 → 百炼权限隔离生效
### Batch BPrompt 拼接实现tasks 5-11
- [ ] T5完善 `app3_clue.py``build_prompt()`async + 真实数据
- [ ] T6完善 `app4_analysis.py``build_prompt()`asyncio.gather 并发获取)
- [ ] T7完善 `app5_tactics.py``build_prompt()`(复用 App4 + task_suggestion
- [ ] T8完善 `app6_note.py``build_prompt()`(消费数据 + 全部备注
- [ ] T9完善 `app7_customer.py``build_prompt()`(客观+主观数据,标注来源)
- [ ] T10检查点 — 应用 3-7 完成
- [ ] T11实现错误降级与 Token 预算控制
### Batch D联调与验证
- [ ] T13端到端联调触发事件 → Prompt 拼接 → 百炼调用 → 缓存写入 → 前端展示
- [ ] T14应用 1 页面上下文联调(各入口页面 → contextType/contextId → 后端文本化 → AI 对话验证上下文感知)
### Batch C应用 1 页面文本化 + 前端配合tasks 12-15
- [ ] T12实现 `page_context.py`10 种页面类型文本化
- [ ] T13集成 `app1_chat.py``_build_page_context()` 调用 `build_page_text()`
- [ ] T14检查点 — 页面上下文完成
- [ ] T15前端看板筛选参数传递小程序 ai-float-button + chat 页面参数解析)
### Batch D集成联调tasks 16-17
- [ ] T16集成连线dispatcher await 正确性 + 端到端验证)
- [ ] T17最终检查点
### 属性测试(可选,标记 * 的子任务)
- 17 个正确性属性P1-P17对应 Hypothesis 属性测试
- 测试文件:`tests/test_data_fetchers/``tests/test_ai_apps/`

View File

@@ -0,0 +1,512 @@
# NS4.1:租户管理员页面重构 — 项目级注册体系 + 简写ID管理
> 优先级NS4 后续迭代,依赖 NS4 基础设施已就绪)
> 预估工作量:中
> 前置条件NS4租户管理后台基础设施、P3用户认证体系
> 关联页面:`http://localhost:5173/tenant-admins`admin-web 系统管理后台)
---
## 一、背景与目标
### 1.1 现状问题
当前 `admin-web` 的租户管理员页面NS4 需求 14仅支持基础 CRUD
- 创建时手动输入 `tenant_id``managed_site_ids`(无下拉选项,无名称参考)
- 无法删除管理员记录
- 无法管理简写IDsite_code简写ID 的创建和修改散落在数据库手动操作中
- 缺少项目级的「连接器 → 租户 → 店铺」注册体系,租户名称无处存储
### 1.2 目标
1. 建立项目级注册体系:`biz.connectors``biz.tenants``biz.sites`,统一管理连接器、租户、店铺三级关系
2.`auth.site_code_mapping` 合并迁移至 `biz.sites`简写ID 成为店铺属性
3. 简写ID 变更增量记录(`biz.site_code_history`),保护已提交但未审核的用户申请
4. 重构租户管理员页面支持删除软删除、2 步创建流程、简写ID 管理
5. 新增 ETL 增量同步任务:从 `dwd.dim_site` 同步店铺信息到业务库
---
## 二、数据模型设计
### 2.1 新建表
#### 表 1`biz.connectors` — 连接器注册表
记录本项目接入的上游 SaaS 系统。当前仅「飞球」一个连接器,预留多连接器扩展。
```sql
CREATE TABLE biz.connectors (
id SERIAL PRIMARY KEY,
connector_key VARCHAR(50) NOT NULL UNIQUE, -- 连接器标识(如 'feiqiu'
display_name VARCHAR(100) NOT NULL, -- 显示名称(如 '飞球'
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE biz.connectors IS '连接器注册表:记录本项目接入的上游 SaaS 系统';
```
初始数据:
```sql
INSERT INTO biz.connectors (connector_key, display_name)
VALUES ('feiqiu', '飞球');
```
#### 表 2`biz.tenants` — 租户注册表
记录每个连接器下的租户信息。`tenant_id` 来自上游系统(飞球的 `tenant_id`)。
```sql
CREATE TABLE biz.tenants (
id SERIAL PRIMARY KEY,
connector_id INTEGER NOT NULL REFERENCES biz.connectors(id),
tenant_id BIGINT NOT NULL, -- 上游系统的租户 ID
tenant_name VARCHAR(200), -- 租户名称(可从上游同步或手动填写)
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (connector_id, tenant_id) -- 同一连接器下 tenant_id 唯一
);
COMMENT ON TABLE biz.tenants IS '租户注册表连接器下的租户tenant_id 来自上游系统';
```
初始数据(从 ETL 库 `dwd.dim_site` 提取当前唯一租户):
```sql
INSERT INTO biz.tenants (connector_id, tenant_id, tenant_name)
VALUES (1, 2790683160709957, '朗朗桌球');
-- tenant_name 暂用店铺名,后续可由管理员修改或从上游同步
```
#### 表 3`biz.sites` — 店铺注册表(合并 `auth.site_code_mapping`
`auth.site_code_mapping` 的功能合并到此表,增加 `tenant_id` 外键关联。
`site_code` 为当前生效的简写ID6 位3+3 格式)。
```sql
CREATE TABLE biz.sites (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES biz.tenants(id),
site_id BIGINT NOT NULL UNIQUE, -- 上游系统的店铺 ID
site_name VARCHAR(200), -- 店铺名称(从 dwd.dim_site 同步)
site_code VARCHAR(6) UNIQUE, -- 当前生效的简写ID如 'LLQ001'
site_label VARCHAR(50), -- 店铺标签(从 dwd.dim_site 同步)
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE biz.sites IS '店铺注册表:合并原 auth.site_code_mapping增加租户关联和简写ID管理';
COMMENT ON COLUMN biz.sites.site_code IS '当前生效的简写ID6位字符3+3格式全局唯一';
```
初始数据(从 `auth.site_code_mapping` 迁移):
```sql
-- 仅迁移真实数据(排除测试数据 tenant_id IS NULL
INSERT INTO biz.sites (tenant_id, site_id, site_name, site_code)
SELECT t.id, scm.site_id, scm.site_name, scm.site_code
FROM auth.site_code_mapping scm
JOIN biz.tenants t ON t.tenant_id = scm.tenant_id
WHERE scm.tenant_id IS NOT NULL;
```
#### 表 4`biz.site_code_history` — 简写ID 变更历史表
增量记录所有使用过的简写ID用于保护已提交但未审核的用户申请。
```sql
CREATE TABLE biz.site_code_history (
id SERIAL PRIMARY KEY,
site_id BIGINT NOT NULL, -- 关联 biz.sites.site_id
site_code VARCHAR(6) NOT NULL, -- 历史简写ID
is_current BOOLEAN NOT NULL DEFAULT false, -- 是否为当前生效
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 该 code 生效时间
retired_at TIMESTAMPTZ, -- 该 code 失效时间NULL=当前生效)
UNIQUE (site_code) -- 简写ID 全局唯一(含历史)
);
COMMENT ON TABLE biz.site_code_history IS '简写ID变更历史增量记录所有使用过的简写ID';
COMMENT ON COLUMN biz.site_code_history.is_current IS 'true=当前生效的简写ID每个 site_id 最多一条 is_current=true';
```
### 2.2 表关系
```
biz.connectors (1)
└── biz.tenants (N) -- 一个连接器下多个租户
└── biz.sites (N) -- 一个租户下多个店铺
└── biz.site_code_history (N) -- 一个店铺的简写ID变更历史
auth.tenant_admins.tenant_id → biz.tenants.tenant_id逻辑关联不加 FK
auth.user_applications.site_code → biz.site_code_history.site_code逻辑关联
```
### 2.3 废弃表处理
`auth.site_code_mapping` 在数据迁移完成并验证后标记为废弃:
1. 迁移期间保留原表,新旧并行
2. 后端代码切换到 `biz.sites` 读取
3. 验证无误后,原表重命名为 `auth._archived_site_code_mapping`
### 2.4 `dwd.dim_site` 提升为项目级
当前 `dwd.dim_site` 位于 ETL 连接器级别(`apps/etl/connectors/feiqiu/`)。
本次不做物理迁移(表仍在 `dwd` schema但在语义上将其视为项目级维度表。
后续如有多连接器场景,再考虑拆分为项目级 `dim` schema。
---
## 三、功能详细设计
### 3.1 租户管理员列表页
#### 现有功能保留
- 分页 + 关键词搜索
- 编辑(显示名称、管辖门店、账号状态)
- 重置密码
#### 新增功能
##### 3.1.1 删除管理员(软删除)
- 操作:点击「删除」按钮 → 二次确认弹窗 → 确认后设置 `is_active = false`
- 列表默认只显示 `is_active = true` 的记录
- 可选:增加「显示已禁用」开关,查看所有记录
- 已禁用的记录不可再次删除,但可以重新启用
##### 3.1.2 创建管理员2 步流程)
**第 1 步:创建账号**
- 选择租户:下拉选择 `biz.tenants` 中的租户(显示 `tenant_name`,值为 `tenant_id`
- 输入用户名
- 输入初始密码
- 输入显示名称
- 选择管辖门店:根据所选租户,加载该租户下所有店铺(`biz.sites`),多选
**第 2 步设置简写ID**
- 展示所选租户下所有店铺列表
- 每个店铺显示店铺名称、当前简写ID如有
- 可为每个店铺设置/修改简写ID
- 简写ID 格式6 位字符数字3+3 模式,如 `LLQ001`
- 校验:全局唯一(含历史记录中的 code
> 第 2 步可跳过简写ID 后续可在编辑时设置)
##### 3.1.3 编辑管理员
- 不可修改所属租户(`tenant_id` 只读)
- 可修改用户名、密码重置、显示名称、管辖门店、简写ID
- 简写ID 编辑入口在编辑弹窗中增加「管理简写ID」区域展示该租户下所有店铺及其当前 code
### 3.2 简写ID 管理逻辑
#### 3.2.1 设置/修改简写ID
```
用户在管理后台修改某店铺的简写IDold_code → new_code
→ 校验 new_code 格式6位3+3
→ 校验 new_code 全局唯一biz.sites.site_code + biz.site_code_history.site_code
→ 事务内执行:
1. 将 old_code 在 site_code_history 中标记 is_current=false, retired_at=NOW()
2. 插入 new_code 到 site_code_historyis_current=true
3. 更新 biz.sites.site_code = new_code
4. 清理无引用的历史记录(见 3.2.2
```
#### 3.2.2 历史记录清理
每次修改简写ID 时,检查被替换的旧 code 是否有关联的用户申请:
```sql
-- 检查旧 code 是否有未审核的申请引用
SELECT COUNT(*) FROM auth.user_applications
WHERE site_code = :old_code AND status = 'pending';
```
- 如果有未审核申请引用 → 保留历史记录(`is_current=false`,但不删除)
- 如果无任何申请引用 → 从 `biz.site_code_history` 中删除该条记录
> 目的:防止用户已用旧 code 提交申请但尚未审核时,映射关系丢失
#### 3.2.3 简写ID 格式规范
- 总长度6 位
- 格式3 位字母/数字 + 3 位数字(如 `LLQ001``ABC123`
- 大小写:统一存储为大写
- 全局唯一:同一时刻不允许两个店铺使用相同 code含历史未清理的 code
### 3.3 租户/店铺信息展示
#### 3.3.1 租户下拉选项
创建管理员时,租户下拉数据来源:
```
GET /api/admin/tenants → biz.tenants (is_active=true)
返回:[{ id, tenantId, tenantName, connectorName }]
```
#### 3.3.2 店铺列表
选择租户后,加载该租户下所有店铺:
```
GET /api/admin/tenants/{tenant_id}/sites → biz.sites (tenant_id=?, is_active=true)
返回:[{ id, siteId, siteName, siteCode, siteLabel }]
```
---
## 四、接口设计
### 4.1 新增接口
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 租户列表 | GET | `/api/admin/tenants` | 所有活跃租户(含连接器名称) |
| 租户下店铺列表 | GET | `/api/admin/tenants/{tenant_id}/sites` | 指定租户下所有店铺(含当前 site_code |
| 删除管理员 | DELETE | `/api/admin/tenant-admins/{id}` | 软删除is_active=false |
| 设置简写ID | PUT | `/api/admin/sites/{site_id}/site-code` | 设置/修改店铺简写ID |
| 简写ID历史 | GET | `/api/admin/sites/{site_id}/site-code-history` | 查看某店铺的简写ID变更历史 |
### 4.2 修改接口
| 接口 | 变更内容 |
|------|----------|
| `POST /api/admin/tenant-admins` | 创建时 `tenant_id` 改为从 `biz.tenants` 选择;`managed_site_ids``biz.sites` 选择 |
| `PATCH /api/admin/tenant-admins/{id}` | 增加 `username` 可修改(需校验唯一性) |
| `GET /api/admin/tenant-admins` | 默认只返回 `is_active=true`;增加 `include_inactive` 参数 |
---
## 五、ETL 同步任务
### 5.1 店铺信息增量同步
新增 ETL 任务:从 `dwd.dim_site`ETL 库)增量同步到 `biz.sites`(业务库)。
#### 同步逻辑
```
1. 读取 dwd.dim_site WHERE scd2_is_current = 1
2. 对比 biz.sites 中已有记录:
- 新增店铺site_id 不存在)→ INSERTsite_code 留空,待管理员设置)
- 店铺名称变更 → UPDATE site_name
- 店铺标签变更 → UPDATE site_label
3. 不删除已有记录(即使上游标记为关闭)
```
#### 触发方式
- 手动触发:管理后台按钮或 CLI 命令
- 定时触发:随 ETL 日常调度DWD 层完成后)
#### 租户信息
- `biz.tenants` 中的 `tenant_name` 暂不自动同步(上游无 tenant_name 字段)
- 首次由迁移脚本写入,后续由管理员在管理后台手动修改
---
## 六、数据迁移计划
### 6.1 迁移步骤
```
1. 创建新表biz.connectors → biz.tenants → biz.sites → biz.site_code_history
2. 写入种子数据connectors飞球、tenants从 dim_site 提取)
3. 迁移 auth.site_code_mapping → biz.sites仅真实数据排除 tenant_id IS NULL 的测试数据)
4. 为已有 site_code 创建 site_code_history 记录is_current=true
5. 运行 ETL 同步任务,补充 biz.sites 中缺失的店铺dim_site 中有但 site_code_mapping 中没有的)
6. 后端代码切换:所有读取 auth.site_code_mapping 的地方改为读取 biz.sites
7. 验证:对比新旧表数据一致性
8. 废弃原表:重命名为 auth._archived_site_code_mapping
```
### 6.2 回滚策略
- 迁移期间保留原表不动
- 如需回滚:删除 biz.sites/tenants/connectors/site_code_history恢复后端代码指向 auth.site_code_mapping
---
## 七、前端页面设计
### 7.1 页面布局
```
┌─────────────────────────────────────────────────────┐
│ 租户管理员 │
│ [搜索框] [显示已禁用 ☐] [+ 创建管理员] │
├─────────────────────────────────────────────────────┤
│ 用户名 │ 显示名称 │ 租户 │ 管辖门店 │ 状态 │ 操作 │
│ admin1 │ 张三 │ 朗朗 │ 朗朗桌球 │ 启用 │ 编辑 │
│ │ │ │ │ │ 重置 │
│ │ │ │ │ │ 简写ID │
│ │ │ │ │ │ 删除 │
└─────────────────────────────────────────────────────┘
```
### 7.2 创建弹窗2 步)
```
步骤 1/2创建账号
┌──────────────────────────────┐
│ 租户: [▼ 朗朗桌球 ] │
│ 用户名: [ ] │
│ 初始密码:[ ] │
│ 显示名称:[ ] │
│ 管辖门店:[☑ 朗朗桌球 ] │
│ │
│ [下一步] [取消] │
└──────────────────────────────┘
步骤 2/2设置简写ID
┌──────────────────────────────┐
│ 店铺 当前简写ID │
│ 朗朗桌球 [LLQ001 ] │
│ │
│ 格式6位3字母+3数字
│ │
│ [跳过] [完成创建] [上一步]│
└──────────────────────────────┘
```
### 7.3 简写ID 管理弹窗
```
管理简写ID — 朗朗桌球(租户)
┌──────────────────────────────────────┐
│ 店铺 当前ID 操作 │
│ 朗朗桌球 LLQ001 [修改] │
│ │
│ 修改简写ID
│ 新ID[ ] [保存] [取消] │
│ │
│ 变更历史: │
│ LLQ001 当前生效 2026-03-22 │
│ LL001 已失效 2026-02-25 (保留) │
│ │
│ [关闭] │
└──────────────────────────────────────┘
```
---
## 八、影响范围
### 8.1 后端
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `app/routers/admin_tenant_admins.py` | 修改 | 增加 DELETE 端点、修改 CREATE/EDIT 逻辑 |
| `app/schemas/admin_tenant_admins.py` | 修改 | 新增/修改请求响应 Schema |
| `app/routers/admin_registry.py` | 新建 | 租户/店铺/简写ID 管理接口 |
| `app/schemas/admin_registry.py` | 新建 | 注册体系 Schema |
### 8.2 前端admin-web
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `src/pages/TenantAdmins/index.tsx` | 重构 | 2 步创建、删除、简写ID 管理 |
| `src/api/tenantAdmins.ts` | 修改 | 新增 API 调用 |
| `src/api/registry.ts` | 新建 | 租户/店铺列表 API |
### 8.3 数据库
| 操作 | 对象 | 说明 |
|------|------|------|
| 新建 | `biz.connectors` | 连接器注册表 |
| 新建 | `biz.tenants` | 租户注册表 |
| 新建 | `biz.sites` | 店铺注册表(合并 site_code_mapping |
| 新建 | `biz.site_code_history` | 简写ID 变更历史 |
| 废弃 | `auth.site_code_mapping` | 迁移完成后废弃 |
### 8.4 ETL
| 变更 | 说明 |
|------|------|
| 新增同步任务 | `dwd.dim_site``biz.sites` 增量同步 |
### 8.5 小程序端
| 变更 | 说明 |
|------|------|
| 用户申请时的 site_code 查询 | 从 `auth.site_code_mapping` 切换到 `biz.sites` + `biz.site_code_history` |
---
## 九、约束与边界条件
1. 一个租户只有一个管理员(软限制,不加 DB 约束)
2. 简写ID 全局唯一(含历史记录),通过 UNIQUE 约束保证
3. 删除管理员为软删除(`is_active = false`),不物理删除
4. 编辑时不可修改所属租户
5. 简写ID 格式6 位3+3 模式,统一大写存储
6. 历史简写ID 仅在无未审核申请引用时才可清理
7. ETL 同步不删除已有店铺记录(即使上游关闭)
8. `biz.tenants.tenant_name` 暂不自动同步,由管理员手动维护
---
## 十、不做的事情(明确排除)
1. 不做多连接器支持的完整实现(仅预留 `biz.connectors` 表结构)
2. 不做 `dwd.dim_site` 的物理迁移(保留在 `dwd` schema
3. 不做租户管理员的自助注册功能
4. 不做店铺管理员的管理(本 PRD 仅涉及租户管理员)
5. 不做 `auth.site_code_mapping` 的立即删除(迁移后保留为 `_archived`
6. 不做简写ID 的自动生成(由管理员手动设置)
---
## 十一、数据库现状参考
### ETL 库 `dwd.dim_site`(当前数据)
| site_id | tenant_id | shop_name | site_label |
|---------|-----------|-----------|------------|
| 2790685415443269 | 2790683160709957 | 朗朗桌球 | A |
### 业务库 `auth.site_code_mapping`(当前数据)
| id | site_code | site_id | site_name | tenant_id |
|----|-----------|---------|-----------|-----------|
| 1 | LL001 | 2790685415443269 | 朗朗桌球 | 2790683160709957 |
| 1448 | PT952 | 857189 | 测试球房_PT952 | NULL |
| 1470 | PT118 | 819193 | 测试球房_PT118 | NULL |
| 1471 | PT607 | 899675 | 测试球房_PT607 | NULL |
> 仅 id=1 为真实数据,其余为属性测试生成的测试数据(`tenant_id IS NULL`
### 业务库 `auth.tenant_admins`(现有结构)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | BIGSERIAL | 主键 |
| username | VARCHAR(50) | 登录用户名UNIQUE |
| password_hash | VARCHAR(255) | bcrypt 哈希 |
| display_name | VARCHAR(100) | 显示名称 |
| tenant_id | BIGINT | 所属租户 |
| managed_site_ids | BIGINT[] | 管辖门店 ID 列表 |
| is_active | BOOLEAN | 账号状态 |
| created_by | BIGINT | 创建者 |
| created_at | TIMESTAMPTZ | 创建时间 |
| last_login_at | TIMESTAMPTZ | 最后登录时间 |
> `tenant_admins` 表结构不变,仅后端逻辑调整(创建时从 `biz.tenants` 选择租户)
---
## 十二、参考文档
| 文档 | 路径 | 用途 |
|------|------|------|
| NS4 原始 PRD | `docs/prd/Neo_Specs/NS4-tenant-admin-web.md` | 租户管理后台完整需求 |
| P3 认证体系 | `docs/prd/specs/P3-miniapp-auth-system.md` | site_code / user_applications 设计 |
| BD 手册-认证表 | `docs/database/BD_Manual_auth_tables.md` | auth schema 表结构 |
| BD 手册-业务表 | `docs/database/BD_Manual_biz_tables.md` | biz schema 表结构 |

View File

@@ -0,0 +1,247 @@
# Neo_Specs 86 项缺失内容审查报告
> 审查日期2026-03-21
> 审查依据:`docs/prd/Neo_Specs/review-report.md`86 项缺失清单)
> 审查方法:逐项检查数据库端数据支持、后端 Python 业务代码支持、前端页面支持
> 审查产物:`docs/prd/Neo_Specs/review-audit/` 目录下 86 份独立审查报告
---
## 一、总览统计
| 状态 | 数量 | 占比 | 说明 |
|:----:|:----:|:----:|------|
| ✅ 已解决 | 22 | 25.6% | 数据库+后端+前端三层均已实现 |
| ⚠️ 部分解决 | 38 | 44.2% | 核心功能已有,但存在细节差距 |
| ❌ 未解决 | 26 | 30.2% | 功能完全缺失或仅有数据层无业务实现 |
| **合计** | **86** | **100%** | |
### 按模块分布
| 模块 | 总数 | ✅ | ⚠️ | ❌ |
|------|:----:|:--:|:--:|:--:|
| P5.1→NS3MCP/AI | 10 | 4 | 5 | 1 |
| P6→NS1任务模块 | 18 | 5 | 7 | 6 |
| P7→NS1绩效模块 | 12 | 4 | 6 | 2 |
| P8→NS1看板模块 | 14 | 6 | 5 | 3 |
| P9→NS1详情模块 | 23 | 10 | 10 | 3 |
| P10→NS4租户管理后台 | 9 | 1 | 5 | 3 |
---
## 二、各模块审查结果明细
### 2.1 P5.1→NS3MCP Server / AI 应用10 项)
| # | 缺失项 | 状态 | 核心发现 |
|---|--------|:----:|----------|
| 01 | App1 Prompt 工程规范 | ✅ | `ai-app-prompts.md` 已定义完整 system prompt 模板、few-shot 示例、JSON schema 约束 |
| 02 | App2 财务指标计算口径 | ✅ | 后端 `app2_finance_insight.py` 已实现 6 个指标公式,遵循 items_sum 口径 |
| 03 | App3 维客线索触发条件 | ✅ | `app3_consumption_analysis.py` 实现事件驱动触发 + 去重逻辑 |
| 04 | App4-App7 缓存策略 | ⚠️ | `ai_cache` 表已建,后端有 TTL 读写逻辑,但 `expires_at` 字段未实际使用 |
| 05 | LLM 错误处理和降级 | ⚠️ | 有 try/except + 缓存回退,缺限流降级和熔断机制 |
| 06 | Token 用量监控 | ⚠️ | `ai_usage_log` 表记录每次调用,缺日/月预算控制和超限告警 |
| 07 | App5 话术分类 | ⚠️ | 话术生成已实现,缺分类枚举定义和质量评分体系 |
| 08 | 各 App 单元测试 | ✅ | `apps/mcp-server/tests/` 下有 App1-App7 测试文件 |
| 09 | MCP Server 健康检查 | ⚠️ | 后端 `/health` 端点存在MCP Server 自身无健康检查端点 |
| 10 | AI 审计日志 | ✅ | `ai_usage_log` 表记录 who/when/what/token_count |
### 2.2 P6→NS1/RNS1任务模块18 项)
| # | 缺失项 | 状态 | 核心发现 |
|---|--------|:----:|----------|
| 01 | 任务卡片 5 种状态 | ⚠️ | pending/pinned/abandoned 已实现,缺 completed/expired 视觉状态 |
| 02 | 3 种空状态 | ⚠️ | 无任务空状态有,缺筛选无结果空状态和插图 |
| 03 | 置顶任务排序规则 | ✅ | 后端 SQL 已实现 `is_pinned DESC, pinned_at DESC, priority, created_at` |
| 04 | 放弃确认弹窗 | ✅ | 前端 `t-dialog` 二次确认已实现 |
| 05 | 下拉刷新/骨架屏 | ✅ | `t-pull-down-refresh` + `t-skeleton` 已集成 |
| 06 | AI 分析卡片折叠/展开 | ❌ | 前端仅静态展示 AI 分析文本,无折叠/展开/重新生成交互 |
| 07 | 任务优先级视觉标识 | ⚠️ | 有 priority 字段传递,前端缺颜色/图标映射 |
| 08 | 到期倒计时展示 | ✅ | 前端计算剩余天数并展示颜色变化 |
| 09 | 备注字数限制 | ⚠️ | 有 `maxlength=500`,缺实时字数计数器 |
| 10 | 详情页模块折叠/展开 | ❌ | 详情页各模块固定展示,无折叠/展开交互 |
| 11 | 维客线索展示样式 | ✅ | tag 颜色映射 + 卡片布局已实现 |
| 12 | 任务列表搜索 | ❌ | 无搜索功能,仅有状态筛选 |
| 13 | 任务完成反馈 | ⚠️ | 有 `wx.showToast` 成功提示,缺专属完成动画 |
| 14 | 网络异常处理 | ⚠️ | 页面级错误展示有,缺统一离线检测和全局网络状态管理 |
| 15 | 长按/滑动操作 | ⚠️ | 长按置顶已实现,滑动操作未实现 |
| 16 | 页面转场动画 | ❌ | 使用 `wx.navigateTo` 默认转场,无自定义动画 |
| 17 | 批量操作 | ❌ | 无批量选择/批量标记完成功能 |
| 18 | 无障碍适配 | ❌ | 无 aria-label、无焦点管理、无屏幕阅读器适配 |
### 2.3 P7→NS1/RNS1绩效模块12 项)
| # | 缺失项 | 状态 | 核心发现 |
|---|--------|:----:|----------|
| 01 | 营业日 08:00 分割点 | ✅ | ETL 层 `biz_date` 已按 08:00 分割,后端直接按 `biz_date` 查询 |
| 02 | 预估标记 | ⚠️ | 前端有 `isEstimate` 展示逻辑,后端硬编码 `is_estimate=False` |
| 03 | 定档折算展示格式 | ⚠️ | 后端返回 `hours`/`hoursRaw`,前端展示格式与标杆不一致 |
| 04 | 新客筛选逻辑 | ⚠️ | 后端有新客筛选但定义简化(仅首次服务月份,缺 2 月内+次数≤2 条件) |
| 05 | 常客展示字段 | ✅ | 次数、小时数、工资合计字段完整 |
| 06 | 收入与档位卡片视觉 | ✅ | 进度条 + 档位标签 + 收入数字样式已实现 |
| 07 | 服务记录日期格式 | ⚠️ | 展示"3月15日"格式,缺星期信息(标杆要求"3月15日 周五" |
| 08 | 月份切换 | ⚠️ | performance-records 页有月份切换,主 performance 页面无 |
| 09 | 绩效空状态 | ✅ | 新助教无数据时展示空状态文案 |
| 10 | 业绩导出功能 | ❌ | 无导出功能(整个项目缺乏通用导出基础设施) |
| 11 | 数据刷新频率说明 | ⚠️ | 无用户可见的数据新鲜度说明(如"数据更新于 XX:XX" |
| 12 | 周口径支持 | ❌ | 仅支持月口径,无本周/上周查询参数 |
### 2.4 P8→NS1/RNS1看板模块14 项)
| # | 缺失项 | 状态 | 核心发现 |
|---|--------|:----:|----------|
| 01 | Tab 切换缓存 | ⚠️ | 三看板通过页面跳转实现,非 Tab 组件,不保持筛选状态 |
| 02 | 财务看板分段加载 | ⚠️ | 后端单 API 返回全部数据,无 `sections` 参数,前端有 skeleton 降级 |
| 03 | 客户卡片跳转 | ✅ | 点击卡片跳转 `customer-detail?memberId=` 已实现 |
| 04 | 助教看板进度条 | ⚠️ | `t-progress` 组件已引入但未在看板页面使用,仅展示 `perfGap` 文本 |
| 05 | 数据实时性标识 | ❌ | 无"数据更新于 XX:XX"展示 |
| 06 | 环比 tooltip | ⚠️ | 环比箭头和百分比展示有,点击查看计算详情无 |
| 07 | 助教卡片跳转 | ✅ | 点击卡片跳转 `coach-detail?coachId=` 已实现 |
| 08 | 柱状图交互 | ⚠️ | `wx-charts` 渲染柱状图有,点击柱子显示具体数据无 |
| 09 | 下拉刷新 | ✅ | `onPullDownRefresh` 已实现 |
| 10 | 财务看板板块折叠 | ❌ | 各板块固定展示,无折叠/展开交互 |
| 11 | 错误展示和重试 | ✅ | 模块级错误展示 + 重试按钮已实现 |
| 12 | 筛选项动画 | ✅ | 筛选面板展开/收起动画已实现 |
| 13 | 排名序号展示 | ❌ | 助教列表无 #1#2 排名序号 |
| 14 | 数字格式化规范 | ✅ | 千分位 + 2 位小数 + ¥ 符号统一使用 `formatCurrency` 工具函数 |
### 2.5 P9→NS1/RNS1详情模块23 项)
| # | 缺失项 | 状态 | 核心发现 |
|---|--------|:----:|----------|
| 01 | 客户详情分段加载 | ⚠️ | 单 API 返回全部数据,前端有 skeleton 降级但非分段加载 |
| 02 | 档位进度时间轴 | ⚠️ | 进度条展示有,非时间轴样式(缺节点、高亮、动画) |
| 03 | 消费记录类型映射 | ⚠️ | 台桌消费/商城消费有图标映射,缺 recharge充值类型模板 |
| 04 | 备注 AI 评分星级 | ⚠️ | 星级组件已开发,但未在备注卡片中使用 |
| 05 | Banner 区域视觉 | ✅ | 余额/消费/到店间隔/距上次到店 4 字段布局完整 |
| 06 | AI 洞察卡片 | ⚠️ | 静态展示 AI 洞察文本,缺展开详情和刷新按钮 |
| 07 | 关联助教任务列表 | ✅ | 任务类型图标 + 状态标签 + 服务统计展示完整 |
| 08 | 最亲密助教 | ⚠️ | 数据字段完整,缺关系指数可视化图表 |
| 09 | 助教明细子列表 | ✅ | 消费记录中助教明细展开/折叠已实现 |
| 10 | TOP20 客户排序 | ✅ | 按关系指数降序排列,展示字段完整 |
| 11 | 历史月份统计 | ⚠️ | 表格展示有,缺折线图/柱状图可视化 |
| 12 | 月度统计汇总 | ✅ | monthCount/monthHours 展示位置和样式已定义 |
| 13 | 任务分组视觉区分 | ✅ | active/inactive/abandoned 分组标题样式和折叠规则已实现 |
| 14 | 维客线索展示 | ✅ | 线索卡片布局 + category 颜色映射已实现 |
| 15 | 分享功能 | ❌ | 无分享客户信息功能 |
| 16 | 助教联系方式展示 | ❌ | 助教详情页无电话/微信展示 |
| 17 | 自定义日期范围筛选 | ❌ | 仅支持月份切换,无自定义日期范围 |
| 18 | 返回按钮行为 | ⚠️ | 使用 `wx.navigateBack` 默认行为,未区分"返回上一页"和"返回列表页" |
| 19 | 电话号码脱敏 | ✅ | 后端返回脱敏号码(中间 4 位 *),前端直接展示 |
| 20 | 收入明细展开/折叠 | ⚠️ | 使用 Tab 切换(本月/上月)替代展开/折叠,交互方式不同 |
| 21 | 饮品描述字段展示 | ✅ | `drinks` 字段在服务记录卡片中展示 |
| 22 | 模块级错误独立处理 | ✅ | 各模块独立 loading/error 状态,单模块失败不影响其他模块 |
| 23 | 可用月数计算 | ❌ | `availableMonths` 字段未实现(余额÷月均消耗) |
### 2.6 P10→NS4租户管理后台9 项)
| # | 缺失项 | 状态 | 核心发现 |
|---|--------|:----:|----------|
| 01 | 角色权限管理 CRUD | ⚠️ | RBAC 三表模型完整,权限校验中间件已打通,但无管理界面(依赖种子数据) |
| 02 | 门店切换功能 | ⚠️ | 后端数据隔离完整SiteSelector 组件已开发但仅维客线索页集成,全局布局未放置 |
| 03 | 数据导出功能 | ❌ | 无任何业务数据导出功能(后端无端点、前端无按钮) |
| 04 | 操作日志/审计日志 | ❌ | 无审计日志表、无记录逻辑、无查看界面;线索删除为物理删除 |
| 05 | 响应式适配 | ⚠️ | Sider 可折叠、flex 布局基础有,缺最小分辨率定义和表格横向滚动 |
| 06 | 表格组件统一规范 | ⚠️ | 主表分页统一20条/页),缺排序交互、空数据中文化、统一配置抽象 |
| 07 | 表单验证统一规范 | ⚠️ | 验证风格基本一致EditModal 缺 required 规则,后端 422 格式不统一 |
| 08 | 国际化预留 | ✅ | ConfigProvider locale={zhCN} 已配置,项目面向国内市场无需 i18n 框架 |
| 09 | 主题定制 | ❌ | 使用 Ant Design 默认主题,无品牌色、无 Logo、无暗色模式 |
---
## 三、风险矩阵
### 🔴 高风险未解决项(需优先处理,共 8 项)
| 编号 | 缺失项 | 影响 |
|------|--------|------|
| P6-06 | AI 分析卡片折叠/展开/重新生成 | AI 功能交互不完整,用户无法与 AI 内容互动 |
| P6-12 | 任务列表搜索功能 | 任务量大时无法快速定位目标任务 |
| P6-17 | 批量操作 | 多任务管理效率低 |
| P6-18 | 无障碍适配 | 合规风险 |
| P7-10 | 业绩导出功能 | 管理者无法离线分析绩效数据 |
| P10-03 | 数据导出功能 | 管理后台无法导出业务数据 |
| P10-04 | 操作审计日志 | 操作不可追溯,线索物理删除不可恢复 |
| P9-23 | 可用月数计算 | 客户余额分析功能缺失 |
### 🟠 中风险部分解决项(需补齐差距,共 15 项)
| 编号 | 缺失项 | 差距 |
|------|--------|------|
| P5.1-04 | 缓存策略 | `expires_at` 未实际使用 |
| P5.1-05 | LLM 降级 | 缺限流降级/熔断 |
| P5.1-06 | Token 监控 | 缺预算控制 |
| P6-01 | 任务卡片状态 | 缺 completed/expired 视觉 |
| P7-02 | 预估标记 | 后端硬编码 False |
| P7-04 | 新客筛选 | 定义简化 |
| P8-01 | Tab 切换缓存 | 页面跳转不保持状态 |
| P8-02 | 分段加载 | 单 API 返回全部数据 |
| P9-01 | 客户详情分段加载 | 同上 |
| P9-06 | AI 洞察卡片 | 缺展开/刷新交互 |
| P9-08 | 最亲密助教 | 缺可视化图表 |
| P9-11 | 历史月份统计 | 缺图表 |
| P10-01 | 角色权限管理 | 无管理界面 |
| P10-02 | 门店切换 | 未全局集成 |
| P10-06 | 表格规范 | 缺排序和统一配置 |
---
## 四、系统性问题
基于 86 项审查,识别出以下跨模块的系统性差距:
### 4.1 前端交互规范缺失
小程序端P6-P9普遍缺少折叠/展开交互、搜索功能、批量操作、页面转场动画、无障碍适配。这些属于前端交互层的系统性缺失,建议新建统一的前端交互规范文档。
### 4.2 数据加载策略单一
财务看板P8-02、客户详情P9-01等复杂页面均为单 API 返回全部数据,缺乏分段加载策略。前端虽有 skeleton 降级,但首屏性能受限。
### 4.3 导出功能全局缺失
整个项目(小程序 + 管理后台无任何业务数据导出功能缺乏通用导出基础设施Excel 生成工具、导出日志表、权限控制)。
### 4.4 审计日志体系不完整
MCP/AI 层有 `ai_usage_log`,但租户管理后台的业务操作(审核、编辑、删除)无审计记录。维客线索删除为物理删除,数据不可恢复。
### 4.5 AI 交互深度不足
AI 相关功能(任务 AI 分析、客户 AI 洞察)均为静态展示,缺少用户与 AI 内容的互动能力(折叠/展开、重新生成、反馈评分)。
---
## 五、优先级建议
### 第一优先级:阻塞性缺失(建议立即处理)
1. P10-04 操作审计日志 — 新建 `biz.audit_log` 表 + 后端记录逻辑 + 线索删除改软删除
2. P7-02 预估标记 — 后端实现 `is_estimate` 判断逻辑(当月数据标记预估)
3. P8-02 / P9-01 分段加载 — 评估拆分 API 或增加 `fields` 参数
### 第二优先级:体验差距(建议短期补齐)
4. P6-06 AI 分析卡片交互 — 增加折叠/展开/重新生成
5. P10-02 门店选择器全局集成 — SiteContext + 各页面接入
6. P6-01 任务卡片状态视觉 — 补充 completed/expired 样式映射
7. P5.1-04/05/06 AI 缓存/降级/监控 — 完善 AI 基础设施
### 第三优先级:功能增强(建议中期规划)
8. P7-10 / P10-03 导出功能 — 建立通用导出基础设施
9. P6-12 任务搜索 — 增加按客户名/手机号搜索
10. P10-01 角色权限管理界面 — 按需实现
### 第四优先级:体验优化(建议后续迭代)
11. 无障碍适配P6-18
12. 页面转场动画P6-16
13. 批量操作P6-17
14. 主题定制P10-09
---
## 六、审查报告索引
所有 86 份独立审查报告存放于 `docs/prd/Neo_Specs/review-audit/` 目录:
| 文件名模式 | 数量 | 对应模块 |
|-----------|:----:|----------|
| `P5.1-NS3-01.md` ~ `P5.1-NS3-10.md` | 10 | MCP Server / AI 应用 |
| `P6-NS1-01.md` ~ `P6-NS1-18.md` | 18 | 小程序任务模块 |
| `P7-NS1-01.md` ~ `P7-NS1-12.md` | 12 | 小程序绩效模块 |
| `P8-NS1-01.md` ~ `P8-NS1-14.md` | 14 | 小程序看板模块 |
| `P9-NS1-01.md` ~ `P9-NS1-23.md` | 23 | 小程序详情模块 |
| `P10-NS4-01.md` ~ `P10-NS4-09.md` | 9 | 租户管理后台 |

View File

@@ -0,0 +1,107 @@
# P10-NS4-01角色权限管理的 CRUD 界面规范
## 简要结论
- 状态:⚠️ 部分解决
- 数据库层已建立完整的 RBAC 三表模型roles / permissions / role_permissions后端已实现权限查询与校验中间件但缺少角色权限管理的 CRUD 界面——无法通过 UI 创建角色、分配权限、展示权限树。
## 详细审查
### 数据库端
auth schema 中已存在完整的 RBAC 表结构(来源:`docs/database/ddl/zqyy_app__auth.sql`
| 表名 | 用途 | 关键字段 |
|------|------|----------|
| `auth.roles` | 角色定义 | id, code, name, description |
| `auth.permissions` | 权限定义 | id, code, name, description |
| `auth.role_permissions` | 角色-权限映射(多对多) | role_id → roles.id, permission_id → permissions.id |
| `auth.user_site_roles` | 用户-门店-角色绑定 | user_id, site_id, role_id → roles.id |
| `auth.tenant_admins` | 租户管理员账号 | managed_site_idsBIGINT[])控制管辖范围 |
种子数据已预置 4 个角色coach / staff / site_admin / tenant_admin和 5 个权限view_tasks / view_board / view_board_finance / view_board_customer / view_board_coach以及 14 条角色-权限映射。
结论:数据库层 RBAC 模型完整,支持角色-权限多对多关系。`tenant_admins` 表通过 `managed_site_ids` 数组实现租户级/店铺级管理员的管辖范围控制,与 P10 spec 中描述的两种权限级别一致。
### 后端代码
**已实现:**
1. **权限查询服务**`apps/backend/app/services/role.py`
- `get_user_permissions(user_id, site_id)` — 三表联查获取用户权限 code 列表
- `get_user_sites(user_id)` — 获取用户关联的所有店铺及角色
- `check_user_has_site_role(user_id, site_id)` — 检查用户是否有角色绑定
2. **权限校验中间件**`apps/backend/app/middleware/permission.py`
- `require_permission(*codes)` — 依赖注入工厂,校验用户 approved 状态 + 指定权限
- `require_approved()` — 仅校验 approved 状态
- 已在看板路由(`xcx_board.py`)中实际使用:`view_board_finance``view_board_customer``view_board_coach`
3. **租户管理员 CRUD**`apps/backend/app/routers/admin_tenant_admins.py`
- 列表(分页+搜索、创建、编辑display_name / managed_site_ids / is_active、重置密码
- 这是管理员账号的 CRUD不是角色/权限的 CRUD
4. **租户管理员认证**`apps/backend/app/auth/tenant_admins.py` + `tenant_auth.py`
- JWT 签发含 managed_site_idsaud=tenant-admin 与小程序隔离
- `site_filter_clause()` / `verify_site_access()` 实现数据隔离
**未实现:**
-`roles` 表的 CRUD API创建角色、编辑角色、删除角色
-`permissions` 表的 CRUD API创建权限、编辑权限、删除权限
-`role_permissions` 映射的管理 API为角色分配/取消权限)
- 角色和权限数据完全依赖种子 SQL 预置,无法通过 API 动态管理
### 前端代码
**tenant-admin租户管理后台页面清单**
- `Login/` — 登录页 ✅
- `UserApproval/` — 用户审核 ✅
- `UserManagement/` — 用户管理 ✅
- `ExcelUpload/` — Excel 上传 ✅
- `RetentionClues/` — 维客线索管理 ✅
- ❌ 无角色管理页面
- ❌ 无权限管理页面
- ❌ 无权限树组件
**tenant-admin 组件清单:**
- `DiffTable/` — 冲突 diff 交互 ✅
- `ClueEditor/` — 线索编辑 ✅
- `SiteSelector/` — 门店选择器 ✅
- ❌ 无 PermissionTree / RoleEditor 等权限管理组件
**admin-web系统管理后台**
- `TenantAdmins/index.tsx` — 租户管理员 CRUD 页面 ✅
- 功能:列表(分页+搜索)、创建 Modal用户名/密码/显示名称/tenantId/managedSiteIds、编辑 Modal显示名称/managedSiteIds/isActive、重置密码 Modal
- ❌ 无角色选择/分配功能
- ❌ 无权限树展示
### 差距分析
| P10 标杆要求 | 当前实现 | 差距 |
|-------------|---------|------|
| 租户级管理员(管辖所有店铺) | ✅ managed_site_ids 数组控制 | 无 |
| 店铺级管理员(只管指定 site_id | ✅ managed_site_ids 数组控制 | 无 |
| 角色定义coach/staff/site_admin/tenant_admin | ✅ 种子数据预置 | 无法动态增删改 |
| 权限定义5 个 view_* 权限) | ✅ 种子数据预置 | 无法动态增删改 |
| 角色-权限映射 | ✅ 种子数据预置 14 条映射 | 无法通过 UI 调整 |
| 创建角色界面 | ❌ 不存在 | 完全缺失 |
| 分配权限界面 | ❌ 不存在 | 完全缺失 |
| 权限树展示 | ❌ 不存在 | 完全缺失 |
| 用户审核时分配角色 | ⚠️ 部分 | 审核通过时可指定 role但角色列表硬编码非从 roles 表动态读取 |
核心差距RBAC 数据模型已就绪权限校验链路已打通roles → role_permissions → permissions → middleware但管理侧完全依赖种子数据和数据库直接操作缺少面向管理员的 CRUD 界面。这意味着:
- 新增角色需要写 SQL
- 调整角色权限需要写 SQL
- 管理员无法自助查看权限分配全貌
### 建议
1. **短期(低成本)**:在 admin-web 的 TenantAdmins 页面增加"角色权限"只读展示面板,展示当前 4 个角色及其权限映射,让管理员了解权限全貌。无需 CRUD因为当前角色/权限体系相对固定。
2. **中期(按需)**:如果业务发展需要动态角色管理(如新增"前台"角色、调整权限粒度),则需要:
- 后端:新增 `admin_roles.py` 路由roles CRUD + role_permissions 分配)
- 前端:在 admin-web 新增 RoleManagement 页面(角色列表 + 权限树 Ant Design Tree 组件 + 分配交互)
- 在 tenant-admin 的用户审核/管理页面中,角色选择改为从 API 动态获取
3. **权限树组件规范**:如需实现,建议使用 Ant Design `<Tree>` 组件,数据结构为权限按功能模块分组(如"看板"下挂 view_board_*),支持勾选/取消勾选批量分配。
4. **NS4 spec 补充**:在 NS4 文档中明确说明当前权限模型为"预置角色 + 种子数据"模式,角色/权限的动态管理界面作为后续迭代项,避免与 P10 标杆文件的隐含期望产生歧义。

View File

@@ -0,0 +1,119 @@
# P10-NS4-02门店切换功能的交互规范
## 简要结论
- 状态:⚠️ 部分解决
- 后端数据隔离和 JWT 门店权限体系完整;前端 SiteSelector 组件已实现但仅在维客线索页集成,全局布局未放置门店选择器,用户审核和 Excel 上传页面缺少门店筛选,门店名称显示为 ID 而非中文名。
## 详细审查
### 数据库端
**auth.tenant_admins 表**DDL: `db/zqyy_app/migrations/2026-03-20__ns4_tenant_admin_tables.sql`
| 字段 | 类型 | 说明 |
|------|------|------|
| managed_site_ids | BIGINT[] | 管辖门店 ID 数组,用于数据隔离 |
| tenant_id | BIGINT | 所属租户 |
| is_active | BOOLEAN | 账号状态 |
`managed_site_ids` 为 PostgreSQL 数组类型,支持多门店管辖场景。
**public.site_code_mapping 表**(被后端多处引用)
| 用途 | 说明 |
|------|------|
| site_code → site_id 映射 | 用户申请时通过球房编号关联门店 |
| site_id → site_name 映射 | 用户列表、线索搜索时补充门店名称 |
✅ 门店名称映射表存在,后端已在 `tenant_users.py``tenant_clues.py` 中使用。
**结论**:数据库层面完整支持多门店管辖和门店名称映射。
### 后端代码
#### 1. 登录与 JWT`tenant_auth.py`
✅ 登录成功后返回的 JWT payload 包含 `managed_site_ids` 数组:
```python
payload = {
"sub": str(admin_id),
"tenant_id": tenant_id,
"managed_site_ids": managed_site_ids, # ← 门店列表
"aud": "tenant-admin",
}
```
✅ refresh_token 也携带 `managed_site_ids`,刷新后不丢失。
#### 2. 认证中间件(`auth/tenant_admins.py`
`CurrentTenantAdmin` dataclass 包含 `managed_site_ids: list[int]`
`site_filter_clause()` 工具函数生成 `site_id IN (...)` SQL 片段,用于数据隔离。
`verify_site_access()` 校验单个 site_id 是否在管辖范围内,越权返回 403。
✅ 空 managed_site_ids 时生成 `1 = 0` 条件,不匹配任何行(安全兜底)。
#### 3. 查询接口(`tenant_users.py`
`list_applications` — 通过 `site_filter_clause` 附加 `site_id IN (管辖列表)` 条件。
`list_users` — 同上JOIN `user_site_roles` 表做门店隔离。
`approve_application` / `reject_application` — 调用 `verify_site_access` 校验单门店权限。
`edit_user` — 新 site_id 必须在管辖范围内。
#### 4. 维客线索(`tenant_clues.py`grep 结果确认)
✅ 搜索接口支持 `site_id` 参数筛选。
✅ 返回结果补充 `site_name`(通过 `site_code_mapping` 查询)。
**后端结论**:数据隔离体系完整,所有查询均附加 `site_id IN (管辖列表)` 条件,符合 P10 spec 要求。
### 前端代码
#### 1. SiteSelector 组件(`components/SiteSelector/index.tsx`
✅ 组件已实现,基于 Ant Design Select支持多选/全选。
✅ 配套 `useSiteFilter` hook管理选中状态提供 `effectiveSiteIds`(空选时返回全部)。
✅ 数据源来自 `useAuth().user.managedSiteIds`
#### 2. useAuth hook`hooks/useAuth.tsx`
✅ 从 JWT payload 解析 `managedSiteIds` 数组。
✅ 登录后通过 `extractUserInfo` 提取并存入 React Context。
✅ 页面刷新时从 localStorage 恢复。
#### 3. API 服务(`services/api.ts`
✅ JWT 自动附加到请求头。
✅ 401 时自动刷新 token含并发保护
⚠️ 无全局 site_id 拦截器——各页面需自行传递 site_id 参数。
#### 4. App.tsx 全局布局
**全局布局未集成 SiteSelector**。侧边栏仅包含导航菜单和退出按钮,无门店选择器。
❌ 无全局 "当前选中门店" 状态管理(无 SiteContext/SiteProvider
#### 5. 各页面集成情况
| 页面 | SiteSelector 集成 | site_id 传递 | 门店名称显示 |
|------|-------------------|-------------|-------------|
| 维客线索 (RetentionClues) | ✅ 已集成 | ✅ 单门店时传 site_id | ⚠️ 显示 `门店 {id}` |
| 用户管理 (UserManagement) | ❌ 未集成 | ❌ 依赖后端 JWT 隔离 | ⚠️ 显示 `门店 {id}` |
| 用户审核 (UserApproval) | ❌ 未集成 | ❌ 依赖后端 JWT 隔离 | ❌ 仅显示球房编号 |
| Excel 上传 (ExcelUpload) | ❌ 未集成 | ❌ 无门店筛选 | ❌ 无门店信息 |
#### 6. 门店名称问题
⚠️ SiteSelector 选项显示为 `门店 {id}`(硬编码数字),未查询 `site_code_mapping` 获取真实门店名称。用户无法通过名称识别门店。
### 差距分析
| P10 要求 | 当前状态 | 差距 |
|----------|---------|------|
| 租户级管理员管辖所有店铺 | ✅ managed_site_ids 支持 | 无 |
| 店铺级管理员只管指定 site_id | ✅ JWT + site_filter_clause | 无 |
| 所有查询附加 site_id IN 条件 | ✅ 后端全部实现 | 无 |
| 门店选择器 UI | ⚠️ 组件存在但未全局集成 | 仅维客线索页使用 |
| 切换门店后数据自动刷新 | ⚠️ 维客线索页有效 | 其他页面无此机制 |
| 门店选择器位置 | ❌ 未定义全局位置 | App.tsx 布局无选择器 |
| 门店名称可读性 | ❌ 显示 ID 而非名称 | 需查询 site_name |
### 建议
1. **全局门店选择器**:在 `App.tsx``Content` 区域顶部(或 Header放置 SiteSelector创建 `SiteContext` 全局管理当前选中门店,各页面通过 Context 消费。
2. **门店名称映射**:登录后或首次加载时调用后端接口获取 `managed_site_ids → site_name` 映射SiteSelector 选项显示真实门店名称(如"朗朗桌球·XX店")。
3. **页面集成**UserApproval、UserManagement、ExcelUpload 页面接入全局门店筛选API 请求携带 `site_ids` 参数。
4. **切换刷新机制**:门店切换时触发数据重新加载(可通过 `useEffect` 监听 `selectedSiteIds` 变化,或使用 React Query 的 `queryKey` 包含 site_ids 实现自动 refetch
5. **后端补充**:新增 `GET /api/tenant/sites` 接口,返回当前管理员管辖门店的 `{site_id, site_name}` 列表,供前端门店选择器使用。

View File

@@ -0,0 +1,99 @@
# P10-NS4-03数据导出功能的规范
## 简要结论
- 状态:❌ 未解决
- 租户管理后台tenant-admin无任何业务数据导出功能。后端无导出端点前端无导出按钮数据库无导出日志表。现有的"下载"功能仅限 Excel 空白模板下载和系统管理后台的环境配置导出,均与业务数据导出无关。
## 详细审查
### 数据库端
搜索范围:`db/zqyy_app/` 全部 SQL 文件
结果:
- **无导出日志表**`biz` schema 下无 `export_log``download_log` 等导出记录表
- **现有表**`biz.excel_upload_log` 仅记录 Excel **上传**操作upload_type: expense/platform_income/salary_adj/recharge_commission无导出相关字段
- **无导出审计字段**:任何现有表均未包含导出时间、导出人、导出格式等审计字段
### 后端代码
搜索范围:`apps/backend/app/routers/` 全部 tenant_*.py 文件 + 全部路由文件
逐文件审查结果:
| 路由文件 | 导出相关端点 | 说明 |
|----------|-------------|------|
| `tenant_auth.py` | 无 | 仅登录/鉴权 |
| `tenant_users.py` | 无 | 用户审核+管理,无导出 |
| `tenant_excel.py` | `GET /template/{type}` | **模板下载**(空白 Excel 模板),非数据导出 |
| `tenant_clues.py` | 无 | 线索 CRUD无导出 |
| `env_config.py` | `GET /export` | **环境配置导出**admin-web 专用),非业务数据 |
关键发现:
1. `tenant_excel.py` 包含 `download_template()` 函数,使用 `openpyxl` + `StreamingResponse` 生成空白模板文件,仅含表头和格式说明,不含任何业务数据
2. 后端无通用导出工具模块(无 `export_to_excel``export_to_csv` 等工具函数)
3. 全局搜索 `def.*export` 仅命中 `env_config.py``export_env_config()`(系统管理后台功能)
### 前端代码
#### tenant-admin租户管理后台
搜索范围:`apps/tenant-admin/src/` 全部文件
页面清单与导出功能:
| 页面 | 路径 | 导出按钮 | 说明 |
|------|------|---------|------|
| Login | `pages/Login/` | 无 | 登录页 |
| UserApproval | `pages/UserApproval/` | 无 | 用户审核 |
| UserManagement | `pages/UserManagement/` | 无 | 用户管理 |
| ExcelUpload | `pages/ExcelUpload/` | 无 | 仅有模板**下载**按钮(`DownloadOutlined` + `handleDownloadTemplate` |
| RetentionClues | `pages/RetentionClues/` | 无 | 维客线索管理 |
结论5 个页面均无数据导出功能。ExcelUpload 页面的 `DownloadOutlined` 图标用于下载空白模板,非数据导出。
#### admin-web系统管理后台参考
搜索范围:`apps/admin-web/src/` 全部文件
- `EnvConfig.tsx`:有"导出"按钮,调用 `exportEnvConfig()` 导出去敏感值的 `.env` 配置文件 → 运维功能,非业务数据导出
- `OpsPanel.tsx``CloudDownloadOutlined` 用于 Git Pull 操作 → 非数据导出
- **无业务数据导出功能可参考**
### 差距分析
review-report.md 中 P10→NS4 缺失项 #3 指出P10 §数据导出 位置隐含了管理后台应支持数据导出,但 NS4 完全未提及。
作为管理后台,以下场景存在合理的导出需求但均未实现:
| 导出场景 | 数据源 | 潜在用户需求 | 当前状态 |
|----------|--------|-------------|---------|
| 用户列表导出 | `auth.users` | 租户管理员需要离线查看/统计用户信息 | ❌ 无 |
| 审核记录导出 | `auth.user_applications` | 审核工作量统计、合规审计 | ❌ 无 |
| Excel 上传历史导出 | `biz.excel_upload_log` | 上传操作追溯、数据核对 | ❌ 无 |
| 维客线索导出 | `member_retention_clue` | 线索数据离线分析、交接 | ❌ 无 |
| 助教奖罚明细导出 | `biz.salary_adjustments` | 工资核算、财务对账 | ❌ 无 |
补充说明:
- P10 spec 本身也未明确列出"数据导出"功能(无 AC、无任务项但 review-report 将其标记为隐含需求
- P7-NS1-10 审查(绩效明细导出)结论同样为 ❌ 未解决,说明整个项目目前缺乏通用的数据导出基础设施
### 建议
1. **明确需求优先级**:数据导出为隐含需求,建议在 NS4 spec 中明确标注为"后续迭代项"或"MVP 不含",避免歧义
2. **如需实现,建议分两步**
- **Step 1 — 通用导出基础设施**
- 后端新增 `apps/backend/app/utils/export_helper.py`,封装 openpyxl 生成 Excel 的通用逻辑列定义、数据填充、StreamingResponse 包装)
- 新建 `biz.export_log` 表记录导出操作who/when/what/row_count满足审计要求
- **Step 2 — 逐页面接入**
- 用户列表:`GET /api/tenant/users/export?format=xlsx`
- 审核记录:`GET /api/tenant/applications/export?status=&format=xlsx`
- 维客线索:`GET /api/tenant/customers/{member_id}/clues/export`
- 上传历史:`GET /api/tenant/excel/logs/export`
3. **权限控制**:导出操作应复用现有的 `require_tenant_admin()` 鉴权 + `site_id IN (管辖列表)` 数据隔离,确保导出数据不越权
4. **导出格式**:建议统一 `.xlsx`(已有 openpyxl 依赖),暂不支持 CSV避免中文编码问题
5. **NS4 spec 补充建议**:在 NS4 文档"三、功能详细设计"中新增 §3.5 数据导出,定义支持导出的页面清单、导出格式、权限规则、导出日志记录要求

View File

@@ -0,0 +1,85 @@
# P10-NS4-04操作日志/审计日志的查看界面
## 简要结论
- 状态:❌ 未解决
- 项目当前没有业务操作审计日志的数据库表、后端记录逻辑、以及前端查看界面。仅有 Excel 上传记录(`biz.excel_upload_log`)和 ETL 任务执行日志admin-web LogViewer均不属于业务操作审计日志范畴。
## 详细审查
### 数据库端
**查库方式**pg-app-test MCP 连接不可用,改用 DDL 迁移文件搜索。
| 检查项 | 结果 |
|--------|------|
| `audit_log` / `operation_log` / `activity_log` 表 | ❌ 不存在DDL 搜索无匹配) |
| `auth` schema 中日志相关表 | ❌ 不存在(仅有 `users``tenant_admins``user_roles``user_site_roles``user_applications``user_assistant_binding` |
| `biz` schema 中日志相关表 | ⚠️ 仅有 `biz.excel_upload_log`Excel 上传批次记录,非操作审计日志) |
| `task_execution_log` 表 | 存在于 `_archived/` 基线中,属于 ETL 任务执行日志,非业务操作审计 |
**结论**:数据库中不存在通用的业务操作审计日志表。`biz.excel_upload_log` 记录的是 Excel 上传批次状态pending/confirmed/failed不记录"谁做了什么操作"这类审计信息。
### 后端代码
| 检查项 | 结果 |
|--------|------|
| `apps/backend/app/routers/` 中审计日志路由 | ❌ 不存在(无 `audit`/`log`/`activity` 相关路由文件) |
| 审计中间件 / 日志记录装饰器 | ❌ 不存在(`app/middleware/` 仅有 `response_wrapper.py``permission.py` |
| `tenant_users.py` 审核操作日志 | ❌ 审核通过/拒绝仅更新状态字段(`user_applications.status``reviewed_by``reviewed_at`),不写入独立审计日志表 |
| `tenant_clues.py` 线索操作日志 | ❌ 编辑/删除/隐藏操作直接修改数据,无审计记录 |
| `tenant_excel.py` 上传操作日志 | ⚠️ 写入 `biz.excel_upload_log`,但仅记录批次元数据,不记录操作人的具体行为 |
**关键发现**
- 用户审核(通过/拒绝):`user_applications` 表有 `reviewed_by` + `reviewed_at` 字段,可追溯审核人和时间,但不是独立的审计日志
- 用户编辑(状态变更/绑定修改):无任何操作记录
- 维客线索修改/删除/隐藏:无任何操作记录,且删除为物理删除(`DELETE FROM`),数据不可恢复
- Excel 上传:`excel_upload_log` 记录了 `uploaded_by`,但无确认操作的审计
### 前端代码
| 检查项 | 结果 |
|--------|------|
| `apps/tenant-admin/src/pages/` 中日志查看页面 | ❌ 不存在(仅有 Login、UserApproval、UserManagement、ExcelUpload、RetentionClues 五个页面) |
| `apps/tenant-admin/src/App.tsx` 路由配置 | ❌ 无日志相关路由(路由:`/login``/applications``/users``/excel``/clues` |
| `apps/admin-web/src/pages/LogViewer.tsx` | ⚠️ 存在,但功能是 ETL 任务执行日志查看器(通过 WebSocket 接收执行日志、按任务分组展示),不是业务操作审计日志 |
| `apps/admin-web/src/App.tsx` 路由 `/log-viewer` | 指向 ETL LogViewer与业务操作审计无关 |
**结论**:两个前端应用均无业务操作审计日志查看界面。
### 差距分析
P10 标杆文件要求管理后台具备操作审计功能,即"谁在什么时间做了什么操作"的完整追溯能力。当前 NS4 的差距:
| 维度 | P10 要求 | NS4 现状 | 差距 |
|------|----------|----------|------|
| 审计数据存储 | 独立审计日志表 | ❌ 不存在 | 完全缺失 |
| 审计记录写入 | 关键操作自动记录 | ❌ 无中间件/装饰器 | 完全缺失 |
| 审计日志查看 | 管理后台可查看/筛选 | ❌ 无页面 | 完全缺失 |
| 操作可追溯性 | 所有关键操作可追溯 | ⚠️ 仅审核操作有 `reviewed_by`/`reviewed_at` | 大部分缺失 |
| 数据安全 | 删除操作可恢复/可审计 | ❌ 线索删除为物理删除 | 高风险 |
### 建议
#### 短期(高优先级)
1. **新建审计日志表** `biz.audit_log`
- 字段:`id``site_id``operator_id`(操作人)、`operator_type`tenant_admin/system`action`approve/reject/edit/delete/hide/upload 等)、`target_type`user/clue/excel 等)、`target_id``detail`JSONB变更前后值`ip_address``created_at`
- 索引:`(site_id, created_at DESC)``(operator_id)``(target_type, target_id)`
2. **后端审计中间件/工具函数**
-`tenant_users.py` 的审核通过/拒绝、用户编辑/禁用操作中写入审计日志
-`tenant_clues.py` 的编辑/删除/隐藏操作中写入审计日志
-`tenant_excel.py` 的上传确认操作中写入审计日志
- 建议实现为可复用的 `log_audit()` 工具函数,在事务内调用
3. **线索删除改为软删除**:当前 `DELETE FROM public.member_retention_clue` 为物理删除,建议改为 `SET is_deleted = true`,配合审计日志记录
#### 中期
4. **前端审计日志查看页面**tenant-admin
- 新增 `/audit-logs` 路由和 `AuditLogs` 页面
- 支持按时间范围、操作类型、操作人筛选
- 展示操作详情(变更前后对比)
5. **后端审计日志查询 API**
- `GET /api/tenant/audit-logs`(分页 + 筛选)

View File

@@ -0,0 +1,64 @@
# P10-NS4-05管理后台的响应式适配
## 简要结论
- 状态:⚠️ 部分解决
- tenant-admin 依赖 Ant Design 内置响应式能力实现了基础适配Sider 可折叠、flex 布局),但未定义最小支持分辨率、断点规则,也未对表格横向滚动做统一处理。
## 详细审查
### 前端代码
#### 1. 全局布局App.tsx
- 使用 Ant Design `<Layout>` + `<Sider collapsible>`,侧边栏支持折叠/展开,这是管理后台最基本的响应式能力 ✅
- `minHeight: "100vh"` 保证全屏高度 ✅
- Content 区域 `margin: 16, minHeight: 280`,使用固定像素值,无断点适配 ⚠️
#### 2. 页面级响应式
- **Login 页面**:居中卡片 `width: 400` 固定宽度,小屏幕下可能溢出 ⚠️
- **ExcelUpload 页面**:使用了 `<Row gutter={16}>` + `<Col span={8}>` 展示统计卡片,这是 Ant Design Grid 的响应式用法,但 `span` 为固定值(未使用 `xs/sm/md/lg` 响应式断点属性)⚠️
- **UserApproval / UserManagement 页面**:筛选栏使用 `display: "flex"` + `flexWrap: "wrap"`UserManagement基本适配 ✅;但 UserApproval 筛选栏未设 `flexWrap` ⚠️
- **RetentionClues 页面**:使用 `<Space direction="vertical">` 布局,宽度 `100%`,基本适配 ✅
#### 3. 表格适配
- 所有页面的 `<Table>` 组件均未设置 `scroll={{ x: ... }}`,在窄屏下列内容可能被压缩变形 ❌
- 对比 admin-web 的 DBViewer 页面已使用 `scroll={{ x: 'max-content' }}`tenant-admin 未跟进
- RetentionClues 的线索表格有多列固定宽度(共约 700px加上其他列在 1280px 屏幕下可能紧凑但尚可
- ExcelUpload 的校验详情表格"数据"列和"问题详情"列无固定宽度,内容可能很长
#### 4. CSS / 样式文件
- 项目中无独立 CSS/Less/SCSS 文件,所有样式通过 inline style 实现
-`@media` 查询、无自定义断点定义、无全局响应式样式
- 仅 SiteSelector 组件使用了 `maxTagCount="responsive"`Ant Design Select 内置响应式标签数量)✅
#### 5. viewport 配置
- `index.html` 包含标准 viewport meta`<meta name="viewport" content="width=device-width, initial-scale=1.0" />`
#### 6. 依赖检查
- 无额外响应式库(如 react-responsive、@ant-design/pro-layout 等)
- 未使用 Ant Design 的 `Grid.useBreakpoint()` hook
#### 7. admin-web 对比
- admin-web 同样未做系统性响应式适配,但 DBViewer 页面的表格使用了 `scroll={{ x: 'max-content' }}`
- 两个管理后台的响应式水平基本一致:依赖 Ant Design 默认行为,无主动断点适配
### 差距分析
| 标杆要求P10 §响应式) | 当前状态 | 差距 |
|---|---|---|
| 最小支持分辨率定义 | P10 spec 和 NS4 均未提及 | ❌ 未定义 |
| 断点规则1280/1440/1920 | 无自定义断点 | ❌ 未定义 |
| 移动端适配策略 | 无策略声明 | ⚠️ 管理后台面向 PC可接受不支持移动端但需明确声明 |
| Sider 可折叠 | `collapsible` 已启用 | ✅ 已实现 |
| 表格横向滚动 | 未设置 `scroll.x` | ❌ 未实现 |
| Grid 响应式断点 | ExcelUpload 用了 Row/Col 但无响应式断点 | ⚠️ 部分 |
> P10 原始 spec`docs/prd/specs/P10-tenant-admin-web.md`)中实际不包含"§响应式"章节,该要求来自标杆文件对管理后台的通用期望。
### 建议
1. **明确最小分辨率**:在 NS4 或项目前端规范中声明最小支持分辨率为 1280×720覆盖主流笔记本不支持移动端访问
2. **表格横向滚动**:为所有 `<Table>` 添加 `scroll={{ x: 'max-content' }}` 或计算列宽总和,防止窄屏下列压缩
3. **Login 卡片宽度**:将 `width: 400` 改为 `maxWidth: 400, width: '100%'`,避免小屏溢出
4. **ExcelUpload 统计卡片**`<Col span={8}>` 改为 `<Col xs={24} sm={12} lg={8}>`,适配不同屏幕
5. **UserApproval 筛选栏**:补充 `flexWrap: "wrap"`,与 UserManagement 保持一致
6. **优先级评估**:以上均为低风险改进项。管理后台面向 PC 端使用,当前 Ant Design 默认行为已提供基本可用性,建议在后续迭代中逐步完善,不阻塞当前交付

View File

@@ -0,0 +1,137 @@
# P10-NS4-06表格组件的统一规范
## 简要结论
- 状态:⚠️ 部分解决
- tenant-admin 各页面表格在分页大小、loading 状态、筛选器位置上已形成事实一致的模式但缺少显式的统一配置抽象层排序交互完全缺失空数据展示未统一部分表格分页大小不一致20 vs 10
## 详细审查
### 前端代码
#### 1. 分页大小
| 页面/组件 | 默认 pageSize | showSizeChanger | showTotal | 后端分页 |
|-----------|:---:|:---:|:---:|:---:|
| UserApproval | 20 | ✅ | ✅ `共 N 条` | ✅ page/page_size |
| UserManagement | 20 | ✅ | ✅ `共 N 条` | ✅ page/page_size |
| ExcelUpload — 上传记录 | 20 | ✅ | ✅ `共 N 条` | ✅ page/page_size |
| ExcelUpload — 校验详情 | **10** | ❌ | ❌ | 前端分页 |
| RetentionClues — 客户搜索 | `false` | — | — | 无分页LIMIT 50 |
| RetentionClues — 线索列表 | **10** | ❌ | ❌ | 无分页(全量返回) |
| DiffTable — 冲突表 | `false` | — | — | 前端数据 |
结论:主列表页统一为 20 条/页 + showSizeChanger + showTotal但子表格校验详情、线索列表使用 10 条/页且缺少 showSizeChanger 和 showTotal**不一致**。
#### 2. 排序交互
所有 5 个表格组件均未配置 `sorter` 属性,前端不支持列排序。后端 SQL 统一使用 `ORDER BY created_at DESC`(固定倒序),无动态排序参数。
结论:**完全缺失**。用户无法按任意列排序。
#### 3. 筛选器位置
| 页面 | 筛选器位置 | 筛选方式 |
|------|-----------|---------|
| UserApproval | 表格上方独立区域 | Select状态筛选 |
| UserManagement | 表格上方独立区域 | Select角色+ Input.Search关键词 |
| ExcelUpload — 上传记录 | 无筛选 | — |
| RetentionClues | Card title 区域 + Card extra 区域 | Input.Search + SiteSelector上方Select×2Card extra 右侧) |
结论:筛选器统一放在表格上方(非表格内 column filter**位置一致**。但 RetentionClues 的线索筛选器放在 Card extra 右侧,与其他页面的左对齐布局略有差异。
#### 4. loading 状态
所有带后端请求的表格均通过 `loading={loading}` 传入 Ant Design Table 的 loading 属性,**统一且正确**。
#### 5. 空数据展示
| 页面 | 空数据处理 |
|------|-----------|
| UserApproval | Ant Design Table 默认空状态(无自定义) |
| UserManagement | Ant Design Table 默认空状态(无自定义) |
| ExcelUpload | Ant Design Table 默认空状态(无自定义) |
| RetentionClues — 客户搜索 | `<Empty description="未找到匹配客户" />` ✅ 自定义 |
| RetentionClues — 线索列表 | Ant Design Table 默认空状态(无自定义) |
| DiffTable | Ant Design Table 默认空状态(无自定义) |
结论:仅 RetentionClues 客户搜索有自定义空状态文案,其余均依赖 Ant Design 默认的英文 "No Data"。**未统一**,且未做中文化。
对比 admin-web`TaskManager` 页面使用了 `locale={{ emptyText: <Empty description="队列为空" /> }}` 自定义空状态tenant-admin 未跟进。
#### 6. 统一配置抽象
- tenant-admin 中**不存在**统一的表格配置文件、常量定义或封装组件
- 每个页面独立定义 `pageSize``pagination` 配置、`handleTableChange`
- 分页响应结构 `{ items, total, page, pageSize }` 在前后端已统一,但属于隐式约定
### 后端代码
#### 分页参数统一性
| 路由 | 参数名 | 默认值 | 范围约束 |
|------|--------|--------|---------|
| `GET /applications` | `page` + `page_size` | 1 / 20 | ge=1 / ge=1,le=100 |
| `GET /users` | `page` + `page_size` | 1 / 20 | ge=1 / ge=1,le=100 |
| `GET /excel/logs` | `page` + `page_size` | 1 / 20 | ge=1 / ge=1,le=100 |
| `GET /customers/search` | 无分页 | — | LIMIT 50 硬编码 |
| `GET /customers/{id}/clues` | 无分页 | — | 全量返回 |
结论:三个主列表接口的分页参数**完全统一**`page`/`page_size`,默认 20上限 100。客户搜索和线索列表不分页属于业务设计选择数据量可控但线索列表在数据量增长后可能需要补充分页。
响应格式统一为 `{ items: T[], total: int, page: int, pageSize: int }`**一致**。
### 差距分析
与 P10 标杆文件要求的差距:
| 规范项 | P10 要求(推断) | 当前状态 | 差距 |
|--------|-----------------|---------|------|
| 默认分页大小 | 统一值(如 20 | 主表 20子表 10 | 🟡 子表不一致 |
| showSizeChanger | 所有分页表格启用 | 仅主表启用 | 🟡 子表缺失 |
| showTotal | 所有分页表格显示 | 仅主表显示 | 🟡 子表缺失 |
| 排序交互 | 至少关键列支持排序 | 完全缺失 | 🔴 缺失 |
| 筛选器位置 | 统一在表格上方 | 基本一致 | ✅ |
| loading 状态 | 统一 loading 属性 | 全部已配置 | ✅ |
| 空数据展示 | 统一中文空状态 | 仅 1 处自定义,其余默认 | 🟡 未统一 |
| 统一配置抽象 | 提取公共 Table 配置 | 不存在 | 🔴 缺失 |
### 建议
1. **提取统一表格配置常量**(优先级:高)
`apps/tenant-admin/src/constants/table.ts` 中定义:
```ts
export const DEFAULT_PAGE_SIZE = 20;
export const PAGE_SIZE_OPTIONS = ['10', '20', '50'];
export const TABLE_LOCALE = {
emptyText: '暂无数据',
};
export const defaultPagination = (total: number, page: number, pageSize: number) => ({
current: page,
pageSize,
total,
showSizeChanger: true,
showTotal: (t: number) => `共 ${t} 条`,
pageSizeOptions: PAGE_SIZE_OPTIONS,
});
```
2. **统一空数据展示**(优先级:高)
通过 Ant Design ConfigProvider 全局设置中文 locale 和空状态:
```tsx
import zhCN from 'antd/locale/zh_CN';
<ConfigProvider locale={zhCN} renderEmpty={() => <Empty description="暂无数据" />}>
```
3. **子表格分页对齐**(优先级:中)
ExcelUpload 校验详情和 RetentionClues 线索列表的 pageSize 改为 20并补充 showSizeChanger 和 showTotal。
4. **排序交互**(优先级:低)
对时间列(申请时间、上传时间、记录时间)添加前端 `sorter` 支持。如数据量增长需后端排序,可在 API 中增加 `sort_by` + `sort_order` 参数。当前数据量下前端排序即可。
5. **线索列表补充分页**(优先级:低)
`GET /customers/{member_id}/clues` 当前全量返回,建议在数据量可能超过 50 条时补充后端分页。

View File

@@ -0,0 +1,150 @@
# P10-NS4-07表单验证的统一规范
## 简要结论
- 状态:⚠️ 部分解决
- 各表单已实现基本验证逻辑且风格较一致,但缺少文档化的统一规范,部分表单必填标识缺失,后端 Pydantic 422 错误未映射到前端字段级提示。
## 详细审查
### 前端代码
#### 1. 必填标识(`required: true`
| 表单 | 文件 | 必填字段 | 是否配置 `required` | 备注 |
|------|------|----------|---------------------|------|
| 登录 | `Login/index.tsx` | username, password | ✅ 均有 | — |
| 审核通过 | `UserApproval/index.tsx` (ReviewModal) | role | ✅ 有 | `suggestionIndex` 为可选,正确 |
| 审核拒绝 | `UserApproval/index.tsx` (ReviewModal) | reason | ✅ 有 | — |
| 用户编辑 | `UserManagement/index.tsx` (EditModal) | role, siteId | ❌ 均无 `required` | 角色和门店字段未标记必填,用户可提交空值 |
| 用户绑定 | `UserManagement/index.tsx` (BindModal) | assistantId, staffId | ✅ 正确无 `required` | 两个字段均为可选,符合业务逻辑 |
| 线索编辑 | `ClueEditor/index.tsx` | category, summary | ✅ 均有 | `detail` 为可选,正确 |
问题:`EditModal``role``siteId` 字段没有 `required` 规则。虽然后端 `UserEditRequest` 允许 `None`(部分更新语义),但前端 UI 上没有明确告知用户哪些字段是建议填写的。
#### 2. 错误提示文案
所有已配置 `rules` 的表单字段,`message` 均为中文:
- `"请输入用户名"` / `"请输入密码"` — Login
- `"请选择角色"` — UserApproval (approve)
- `"请填写拒绝原因"` — UserApproval (reject)
- `"请选择大类标签"` / `"请输入摘要"` / `"摘要不能超过 200 字符"` — ClueEditor
✅ 文案风格统一,均为 `"请 + 动词 + 名词"` 格式。
#### 3. 验证触发时机
| 表单 | 触发方式 | 说明 |
|------|----------|------|
| Login | `onFinish`(提交验证) | Form 的 `onFinish` 回调Ant Design 默认在提交时触发全量校验 |
| ReviewModal (approve/reject) | `form.validateFields()`(手动提交验证) | Modal `onOk` 中手动调用 |
| EditModal | `form.validateFields()`(手动提交验证) | Modal `onOk` 中手动调用 |
| BindModal | `form.validateFields()`(手动提交验证) | Modal `onOk` 中手动调用 |
| ClueEditor | `form.validateFields()`(手动提交验证) | Modal `onOk` 中手动调用 |
✅ 统一采用提交时验证(`onFinish``validateFields`),未使用实时验证(`onChange`)。风格一致。
注意:所有 Modal 表单均未显式设置 `validateTrigger`Ant Design 默认值为 `onChange`,即用户修改字段后会实时显示/清除错误。这实际上是"提交触发首次校验 + 后续实时反馈"的混合模式,属于 Ant Design 最佳实践。
#### 4. 自定义验证器
`ClueEditor` 使用了 `max: 200` 长度限制规则,其余表单无自定义 `validator`
ExcelUpload 页面不使用 Ant Design Form 进行表单验证,而是:
- 文件类型限制通过 `Upload` 组件的 `accept=".xlsx,.xls"` 属性
- 空文件检查通过 `fileList.length === 0` 手动判断
- 数据校验完全由后端完成,前端展示后端返回的校验结果
这种设计合理——Excel 上传的校验逻辑复杂(表头格式、金额精度、人员匹配等),适合后端统一处理。
#### 5. 表单布局
所有 Modal 内表单统一使用 `layout="vertical"`Login 页面使用默认水平布局。风格基本一致。
### 后端代码
#### 1. Pydantic Schema 验证
| Schema | 文件 | 验证规则 | 备注 |
|--------|------|----------|------|
| `ApproveRequest` | `tenant_users.py` | `role: str = Field(..., min_length=1)` | 必填 + 非空 |
| `RejectRequest` | `tenant_users.py` | `reason: str = Field(..., min_length=1)` | 必填 + 非空 |
| `UserEditRequest` | `tenant_users.py` | 所有字段 `Optional` | 部分更新语义,合理 |
| `UserBindingRequest` | `tenant_users.py` | 所有字段 `Optional` | 合理 |
| `ClueEditRequest` | `tenant_clues.py` | `category: ClueCategory`(枚举), `summary: str = Field(..., min_length=1, max_length=200)` | 枚举校验 + 长度限制 |
| `ClueVisibilityRequest` | `tenant_clues.py` | `is_hidden: bool = Field(...)` | 必填 |
| `ConfirmRequest` | `tenant_excel.py` | `upload_id: int`, `resolutions: list[Resolution]` | 结构化验证 |
✅ 后端 Pydantic 验证规则与前端 rules 基本对齐。
#### 2. 手动验证(路由层)
Excel 上传路由 (`tenant_excel.py`) 包含额外的手动验证:
- `upload_type` 枚举校验
- 文件扩展名校验(`.xlsx/.xls`
- 文件内容非空校验
- Excel 解析成功校验
- 数据行非空校验
客户搜索路由 (`tenant_clues.py`) 使用 `Query(..., min_length=1)` 确保关键词非空。
#### 3. 错误响应格式
后端统一错误响应格式:`{ "code": <status_code>, "message": <detail> }`
- `http_exception_handler`HTTPException → `{ code, message }`
- `unhandled_exception_handler`:未捕获异常 → `{ code: 500, message: "Internal Server Error" }`
- ❌ 未注册 `RequestValidationError` 处理器 — Pydantic 验证失败时FastAPI 默认返回 422 + `{ "detail": [{ "loc": [...], "msg": "...", "type": "..." }] }` 格式,与统一的 `{ code, message }` 格式不一致
#### 4. 错误提示文案
后端 HTTPException 的 `detail` 均为中文:
- `"申请不存在"` / `"该申请已被处理"` / `"无效的球房编号"` / `"审核操作失败"`
- `"用户不存在"` / `"目标门店不在管辖范围内"` / `"编辑操作失败"`
- `"线索不存在"` / `"编辑操作失败"` / `"删除操作失败"`
- `"请上传有效的 Excel 文件(.xlsx/.xls"` / `"文件内容为空"`
✅ 中文化程度高,风格统一。
### 差距分析
P10 标杆文件(`P10-tenant-admin-web.md`)中没有独立的"表单规范"章节,但在验收标准和设计要点中隐含了以下要求:
- AC4Excel 上传校验(必填、金额精度、表头格式、类型合法)— ✅ 已实现
- 4 种模板的必填列定义 — ✅ 后端已实现校验
- 冲突处理流程 — ✅ 已实现
与通用表单规范最佳实践的差距:
| 维度 | 期望 | 现状 | 差距 |
|------|------|------|------|
| 必填标识 | 所有必填字段有 `required` 规则 + 红色星号 | 大部分有EditModal 缺失 | 🟡 小 |
| 错误提示位置 | 统一在字段下方 | Ant Design Form.Item 默认行为,一致 | ✅ 无 |
| 提示文案 | 中文,格式统一 | `"请 + 动词 + 名词"` 格式统一 | ✅ 无 |
| 验证时机 | 明确约定实时/提交 | 统一提交验证 + Ant Design 默认 onChange 反馈 | ✅ 无 |
| 后端 422 映射 | Pydantic 验证错误映射到前端字段 | 未注册 RequestValidationError 处理器 | 🟡 小 |
| 文档化规范 | 有明确的表单验证规范文档 | 无 | 🟡 小 |
| 前后端验证对齐 | 前端 rules 与后端 schema 一一对应 | 基本对齐,个别字段前端缺 rules | 🟡 小 |
### 建议
1. **EditModal 补充必填规则**(低优先级):`role` 字段建议加 `rules={[{ required: true, message: "请选择角色" }]}``siteId` 视业务需求决定是否必填。
2. **注册 RequestValidationError 处理器**(低优先级):在 `apps/backend/app/main.py` 中添加:
```python
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return JSONResponse(
status_code=422,
content={"code": 422, "message": "请求参数校验失败", "errors": exc.errors()},
)
```
使 Pydantic 验证错误也遵循统一的 `{ code, message }` 响应格式。
3. **文档化表单规范**(可选):如需更高规范性,可在 NS4 或项目级文档中补充一节"表单验证约定",明确:
- 必填字段必须配置 `required: true`Ant Design 自动显示红色星号)
- 错误提示文案格式:`"请 + 动词 + 名词"`
- 验证时机:提交时触发(`validateFields` / `onFinish`),依赖 Ant Design 默认 `onChange` 实时反馈
- 后端验证错误通过 `message.error()` 全局提示,不映射到具体字段
当前项目规模5 个页面、6 个表单)下,现有的隐式一致性已足够,文档化为锦上添花。

View File

@@ -0,0 +1,73 @@
# P10-NS4-08管理后台的国际化预留
## 简要结论
- 状态:✅ 已解决
- Ant Design 组件已配置 `zhCN` locale项目无国际化框架需求中文硬编码符合项目规范当前方案合理。
## 详细审查
### 前端代码
#### 1. Ant Design ConfigProvider locale 配置
`apps/tenant-admin/src/main.tsx` 已在根节点配置 Ant Design 中文 locale
```tsx
import { ConfigProvider } from "antd";
import zhCN from "antd/locale/zh_CN";
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
```
这确保了 Table 空状态文案、DatePicker 日期选择器、Pagination 分页器、Modal 确认/取消按钮等所有 Ant Design 内置组件均显示中文。
`apps/admin-web/src/main.tsx` 采用完全相同的配置方式,两个管理后台保持一致。
#### 2. i18n 框架依赖
`apps/tenant-admin/package.json``apps/admin-web/package.json` 均未引入任何 i18n 框架(如 `react-intl``i18next``react-i18next` 等)。
全局搜索 `i18n``intl``useIntl``formatMessage``i18next``react-intl` 关键词,两个项目中均无 i18n 框架使用痕迹。`locale` 关键词仅出现在 Ant Design ConfigProvider 配置和 `toLocaleString()` 日期格式化调用中。
#### 3. 中文硬编码情况
tenant-admin 所有页面中的 UI 文案均为中文硬编码,包括:
- 导航菜单:`"用户审核"``"用户管理"``"Excel 上传"``"维客线索管理"`
- 表格列标题:`"昵称"``"手机号"``"状态"`
- 操作按钮:`"审核"``"编辑"``"绑定"``"删除"``"登录"`
- 表单标签:`"请输入用户名"``"请输入密码"``"请选择角色"`
- 提示消息:`"审核通过成功"``"用户名或密码错误"``"账号已被禁用"`
- 状态标签:`"待审核"``"已通过"``"已拒绝"`
admin-web 同样采用中文硬编码方式,两个项目风格一致。
#### 4. 与 admin-web 的对比
| 维度 | tenant-admin | admin-web |
|------|-------------|-----------|
| ConfigProvider zhCN | ✅ 已配置 | ✅ 已配置 |
| i18n 框架 | 无 | 无 |
| UI 文案方式 | 中文硬编码 | 中文硬编码 |
| 日期格式化 | dayjs 依赖已安装 | `toLocaleString('zh-CN')` |
两个管理后台在国际化处理上完全一致。
### 差距分析
P10 标杆文件(`docs/prd/specs/P10-tenant-admin-web.md`)中没有独立的"国际化"章节,也未提出任何多语言支持要求。标杆文件明确定义的用户群体是"租户管理员",即国内台球门店的管理人员。
结合项目 steering 规则:
- `language-zh.md`:说明性文字一律简体中文
- `project-overview.md`:领域语言中文,货币 CNY
项目定位为面向国内台球门店的垂直业务系统,目标用户群体单一(国内门店管理员),不存在多语言需求场景。
### 建议
当前方案已满足项目需求,无需额外改动:
1. Ant Design `ConfigProvider locale={zhCN}` 已正确配置,组件内置文案为中文 — **已完成**
2. 无需引入 i18n 框架 — 项目面向国内市场,中文硬编码是合理选择,引入 i18n 框架反而增加不必要的复杂度
3. 如未来确有国际化需求(概率极低),可后续引入 `react-intl``i18next`,将硬编码文案提取为 message key改动范围可控

View File

@@ -0,0 +1,92 @@
# P10-NS4-09管理后台的主题定制
## 简要结论
- 状态:❌ 未解决
- 两个管理后台tenant-admin、admin-web均未实现主题定制使用 Ant Design v5 默认主题,无品牌色配置、无 Logo 组件、无暗色模式支持。
## 详细审查
### 前端代码
#### 1. ConfigProvider theme 配置
**tenant-admin `main.tsx`**`ConfigProvider` 仅配置了 `locale={zhCN}`,未传入 `theme` prop。
```tsx
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
```
**admin-web `main.tsx`**:同样仅配置 `locale={zhCN}`,无 `theme` prop。
两个后台均未使用 Ant Design v5 的 theme token 系统(`ConfigProvider theme={{ token: { colorPrimary: '...' } }}`)。
#### 2. 品牌色primaryColor
- 全局搜索 `colorPrimary``primaryColor``theme` 关键词:两个后台均无自定义品牌色配置
- 所有组件使用 Ant Design 默认蓝色(`#1677ff`
- 侧边栏 `Menu` 使用 `theme="dark"` 属性Ant Design 内置暗色菜单,非自定义主题)
#### 3. Logo 展示
- **tenant-admin**:侧边栏顶部为纯文字 `"租户管理后台"`(内联样式,无图片 Logo
- **admin-web**:侧边栏顶部为纯文字 `"NeoZQYY"`(内联样式,无图片 Logo
- 登录页tenant-admin 使用 `Card title="租户管理后台"` 纯文字标题,无 Logo 图片
- 两个项目 `src/` 目录下均无图片资源文件(`.png``.svg``.jpg``.ico`
#### 4. 暗色模式
- 全局搜索 `darkMode``darkAlgorithm``defaultAlgorithm`:无结果
- 无暗色模式切换开关或相关状态管理
- 未使用 Ant Design v5 的 `theme.darkAlgorithm`
#### 5. Ant Design 版本
- 两个后台均使用 `antd@^5.24.7`,完全支持 CSS-in-JS 主题系统
- 技术上具备实现主题定制的能力,但未使用
#### 6. Vite 配置
- `vite.config.ts` 无 Less 变量覆盖或 CSS 预处理器主题配置
- 仅配置了路径别名、开发服务器端口和 API 代理
#### 7. 两个后台的视觉一致性
| 维度 | tenant-admin | admin-web | 一致性 |
|------|-------------|-----------|--------|
| 品牌色 | Ant Design 默认蓝 | Ant Design 默认蓝 | ✅ 一致(均为默认值) |
| 侧边栏 | `Sider collapsible` + `Menu theme="dark"` | 同左 | ✅ 一致 |
| 顶部标题 | 纯文字"租户管理后台" | 纯文字"NeoZQYY" | ⚠️ 文案不同,无统一品牌标识 |
| Logo | 无 | 无 | ✅ 一致(均无) |
| 暗色模式 | 无 | 无 | ✅ 一致(均无) |
| 底部状态栏 | 无 | 有(任务执行状态) | ❌ 不一致 |
### 差距分析
P10 标杆文件(`docs/prd/specs/P10-tenant-admin-web.md`)本身未包含"§主题"章节,该缺失项来源于 review-report 的通用管理后台最佳实践差距分析。具体差距:
1. **品牌色**:未定义,依赖 Ant Design 默认值。如果未来需要品牌识别或多租户白标,缺少基础设施。
2. **Logo**:两个后台均无图形 Logo仅用内联文字。侧边栏和登录页缺少品牌视觉锚点。
3. **暗色模式**完全未实现。Ant Design v5 原生支持 `theme.darkAlgorithm`,实现成本低,但当前未启用。
4. **主题统一管理**:无共享的主题配置文件或常量,两个后台各自独立,未来维护品牌一致性成本较高。
### 建议
鉴于此项为 🟡 低风险,且 P10 spec 本身未要求主题定制,建议按优先级分阶段处理:
1. **短期(推荐)**:在两个后台的 `ConfigProvider` 中添加统一的 `theme.token.colorPrimary`,建立品牌色基础。改动量极小(约 5 行代码/项目),但能统一视觉标识。
```tsx
// 示例main.tsx
<ConfigProvider
locale={zhCN}
theme={{ token: { colorPrimary: '#品牌色' } }}
>
```
2. **中期(可选)**:提取共享主题配置到 `packages/shared/` 或独立的 `theme.ts`,确保两个后台品牌一致性。添加 Logo 图片资源到侧边栏和登录页。
3. **长期(按需)**:如有暗色模式需求,利用 Ant Design v5 的 `darkAlgorithm` 实现切换。如有多租户白标需求,建立主题配置表。
4. **决策建议**:建议产品侧明确是否需要主题定制能力。如果当前阶段仅为内部管理工具,默认主题可接受;如果面向多租户交付,则品牌色和 Logo 应尽早落地。

View File

@@ -0,0 +1,63 @@
# P5.1→NS3 缺失项 #1App1 Prompt 工程规范
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- App1 的完整 Prompt 工程规范已在百炼平台 System Prompt 文档和后端代码中实现NS3 作为 MCP Server 扩展 spec 不涉及 Prompt 细节属于正常分工。
## 详细审查
### 审查范围
- `apps/backend/app/ai/apps/app1_chat.py` — App1 后端实现
- `docs/prd/ai-app-prompts.md` — 百炼平台 8 个应用的 System Prompt 完整定义
- `docs/prd/specs/P5-miniapp-ai-integration.md` — P5 原始 spec首条 Prompt 数据结构)
- `docs/prd/Neo_Specs/NS3-mcp-server-ai-extension.md` — NS3 spec
### 发现
1. **System Prompt 模板**`docs/prd/ai-app-prompts.md` 中定义了 App1 完整的 System Prompt包含
- 角色定义(台球门店运营助手)
- 5 个技能(数据查询、客户信息、助教业绩、经营数据、库存)
- 权限控制规则(助教/管理者角色隔离)
- 查询规范和回复规范
- `biz_params.user_prompt_params` 参数注入User_ID、Role、Nickname
2. **首条 Prompt JSON 结构**P5 spec 中明确定义了 App1 的首条用户消息结构:
```json
{
"current_time": "...",
"source_page": "来源页面标识",
"page_context": "页面上下文摘要",
"screen_content": "屏幕可见内容文本化"
}
```
3. **后端实现**`app1_chat.py` 已实现:
- `_build_system_prompt()` 构建 system prompt JSON注入用户信息 + 页面上下文)
- `_build_page_context()` 调用 `build_page_text()` 获取 10 种页面入口的结构化文本
- Token 预算控制(`_MAX_SYSTEM_PROMPT_LEN = 4000`
- SSE 流式返回完整链路
4. **NS3 的定位**NS3 是 MCP Server 扩展 spec职责是数据库连接、查库手册、脱敏策略不涉及 AI 应用的 Prompt 工程。Prompt 规范由 P5 spec + `ai-app-prompts.md` 承载,这是正确的分工。
### 证据
`app1_chat.py` 中的 system prompt 构建:
```python
prompt: dict = {
"task": "你是台球门店的 AI 助手,根据用户的问题和当前页面上下文提供帮助。",
"biz_params": {
"user_prompt_params": {
"User_ID": str(user_id),
"Role": role,
"Nickname": nickname,
},
},
}
```
`ai-app-prompts.md` 中 App1 的 System Prompt 长度约 1500 字,覆盖 5 个技能、权限控制、查询规范、回复规范。
### 说明
App1 是通用对话应用,其 Prompt 工程规范不包含 few-shot 示例和 JSON schema 约束(这些是结构化输出应用 2-8 的需求。App1 使用流式文本返回(`chat_stream`),不需要 JSON schema 约束。百炼平台的 System Prompt 配置 + 后端 `_build_system_prompt()` 的动态注入,构成了完整的 Prompt 工程规范。

View File

@@ -0,0 +1,75 @@
# P5.1→NS3 缺失项 #2App2 财务指标计算口径
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- App2 财务洞察的 6 个收入结构字段、items_sum 口径、环比基准均已在 Prompt 模板和后端代码中完整定义。
## 详细审查
### 审查范围
- `apps/backend/app/ai/apps/app2_finance.py` — App2 后端实现
- `apps/backend/app/ai/prompts/app2_finance_prompt.py` — App2 Prompt 模板
- `docs/prd/ai-app-prompts.md` — App2 System Prompt
- `docs/prd/specs/P5-miniapp-ai-integration.md` — P5 spec 中 App2 首条 Prompt 数据结构
- `apps/backend/app/ai/schemas.py` — App2Result 模型定义
### 发现
1. **收入结构字段映射6 个指标)**`app2_finance_prompt.py``_build_system_content()` 中明确定义了 `field_mapping`
- `items_sum` = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money
- `table_fee` = table_charge_money台费收入
- `assistant_pd` = assistant_pd_money陪打费
- `assistant_cx` = assistant_cx_money超休费
- `goods` = goods_money商品收入
- `recharge` = 充值 pay_amountsettle_type=5充值收入
- `electricity` = electricity_money电费当前未启用全为 0
2. **items_sum 口径规则**Prompt 模板中 `rules` 数组明确声明:
- "统一使用 items_sum 口径计算营收总额"
- "助教费用必须拆分为 assistant_pd_money陪打和 assistant_cx_money超休"
- "支付渠道恒等式balance_amount = recharge_card_amount + gift_card_amount"
- "金额单位CNY保留两位小数"
3. **环比基准**`_build_period_data()` 构建当期和上期数据结构,包含完整的收入结构、储值资产、费用汇总、平台结算字段。`app2_finance.py``compute_time_range()` 实现了 8 个时间维度的日期范围计算,为环比分析提供基准。
4. **System Prompt**`ai-app-prompts.md` 中 App2 的 System Prompt 定义了 3 个技能:
- 技能 1财务趋势分析历史数据环比增减幅度
- 技能 2经营预警与建议含当前周期
- 技能 3多维度深度分析客单价、支付方式、时段分析
5. **输出 JSON schema**System Prompt 中强制要求返回 `[{seq, title, content}]` 格式,`schemas.py` 中定义了 `App2InsightItem(seq, title, body)``App2Result(insights)` Pydantic 模型。
### 证据
`app2_finance_prompt.py` 中的字段映射:
```python
"field_mapping": {
"items_sum": (
"table_charge_money + goods_money + assistant_pd_money"
" + assistant_cx_money + electricity_money"
),
"table_fee": "table_charge_money台费收入",
"assistant_pd": "assistant_pd_money陪打费",
"assistant_cx": "assistant_cx_money超休费",
"goods": "goods_money商品收入",
"recharge": "充值 pay_amountsettle_type=5充值收入",
"electricity": "electricity_money电费当前未启用全为 0",
},
```
`_build_period_data()` 中的完整数据结构16 个字段):
```python
return {
"table_charge_money": data.get("table_charge_money", 0),
"goods_money": data.get("goods_money", 0),
"assistant_pd_money": data.get("assistant_pd_money", 0),
"assistant_cx_money": data.get("assistant_cx_money", 0),
"electricity_money": data.get("electricity_money", 0),
"recharge_income": data.get("recharge_income", 0),
# ... 储值资产、费用汇总、平台结算、汇总
}
```
P5 spec 中 App2 收入结构字段映射(已校准):
> `table_fee` = `table_charge_money`56.6%)、`assistant_pd` = `assistant_pd_money`30.6%)、`assistant_cx` = `assistant_cx_money`0.9%)、`goods` = `goods_money`10.1%)、`recharge` = 充值 pay_amountsettle_type=5

View File

@@ -0,0 +1,80 @@
# P5.1→NS3 缺失项 #3App3 维客线索生成的触发条件和频率
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- App3 的触发机制已通过事件驱动架构完整实现:消费结算事件触发 → dispatcher 编排调用链 → App3 执行。触发条件和频率在 P5 spec 和代码中均有明确定义。
## 详细审查
### 审查范围
- `apps/backend/app/ai/dispatcher.py` — AI 事件调度与调用链编排器
- `apps/backend/app/ai/apps/app3_clue.py` — App3 实现
- `apps/backend/app/services/trigger_scheduler.py` — 触发器调度框架
- `apps/backend/app/main.py` — AI 事件处理器注册
- `docs/prd/specs/P5-miniapp-ai-integration.md` — P5 spec 触发条件定义
### 发现
1. **触发条件**P5 spec AC3 明确定义"应用 3 维客线索在客户新增消费时自动更新"。代码实现完全匹配:
- `dispatcher.py``handle_consumption_event()` 方法在消费结算事件发生时触发 App3
- 事件名称:`ai_consumption_settled`
- 触发参数:`member_id`, `site_id`, `settle_id`, `assistant_id`(可选)
2. **触发频率**:事件驱动(非定时),每次客户新增消费(结账单)时触发一次。这是 P5 spec 设计的意图——"客户新增消费时自动更新"。
3. **事件注册链路**
- `main.py` lifespan 中创建 `AIDispatcher` 并调用 `register_ai_handlers()`
- `register_ai_handlers()` 将 3 个事件处理器注册到 `trigger_scheduler`
- `ai_consumption_settled``handle_consumption_settled`
- `ai_note_created``handle_note_created`
- `ai_task_assigned``handle_task_assigned`
- 业务代码通过 `fire_event("ai_consumption_settled", payload)` 触发
4. **调用链编排**:消费事件触发后的完整链路:
```
消费事件 → App3线索分析→ App8线索整理→ App7客户分析
如有助教参与 → App4关系分析→ App5话术参考
```
5. **容错策略**`dispatcher.py` 文档字符串明确说明:
- "某步失败记录错误日志,后续应用使用已有缓存继续"
- "失败应用写入失败 conversation 记录"
- "整条链后台异步执行,不阻塞业务请求"
### 证据
`dispatcher.py` 中的消费事件处理:
```python
async def handle_consumption_event(
self,
member_id: int,
site_id: int,
settle_id: int,
assistant_id: int | None = None,
) -> None:
"""消费事件链App3 → App8 → App7+ App4 → App5 如有助教)。"""
# 步骤 1App3 线索分析
app3_result = await self._run_step("app3_clue", app3_run, context)
# 步骤 2App8 线索整理
# 步骤 3App7 客户分析
# 步骤 4可选App4 → App5
```
`main.py` 中的 AI 事件处理器注册:
```python
_dispatcher = AIDispatcher(_bailian, AICacheService(), ConversationService())
register_ai_handlers(_dispatcher)
```
`_create_ai_event_handlers()` 中的事件映射:
```python
return {
"ai_consumption_settled": handle_consumption_settled,
"ai_note_created": handle_note_created,
"ai_task_assigned": handle_task_assigned,
}
```
P5 spec 调用链定义:
> 消费事件(结账单): └→ 应用 3维客线索分析→ 应用 8线索整理→ 应用 7客户分析

View File

@@ -0,0 +1,106 @@
# P5.1→NS3 缺失项 #4App4-App7 的缓存策略
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- cache_type 枚举和保留策略500 条上限 + 清理机制)已实现,但缺少 expires_at 过期策略和主动失效条件的定义与实现。
## 详细审查
### 审查范围
- `apps/backend/app/ai/cache_service.py` — AI 缓存读写服务
- `apps/backend/app/ai/schemas.py` — CacheTypeEnum 枚举定义
- `docs/database/ddl/zqyy_app__biz.sql` — ai_cache 表 DDL
- `docs/prd/specs/P5-miniapp-ai-integration.md` — P5 spec 缓存策略定义
### 发现
#### ✅ 已实现部分
1. **cache_type 枚举**`schemas.py``CacheTypeEnum` 定义了 7 个枚举值:
```python
APP2_FINANCE = "app2_finance"
APP3_CLUE = "app3_clue"
APP4_ANALYSIS = "app4_analysis"
APP5_TACTICS = "app5_tactics"
APP6_NOTE_ANALYSIS = "app6_note_analysis"
APP7_CUSTOMER_ANALYSIS = "app7_customer_analysis"
APP8_CLUE_CONSOLIDATED = "app8_clue_consolidated"
```
与 P5 spec 定义完全一致。
2. **保留策略**`cache_service.py` 的 `_cleanup_excess()` 实现了 P5 spec 定义的"每个 (cache_type, site_id, target_id) 组合保留最近 500 条记录"
```python
def _cleanup_excess(self, ..., max_count: int = 500) -> int:
```
清理时机:每次 `write_cache()` 写入新记录后异步清理。
3. **target_id 约定**:各应用的 target_id 含义在 P5 spec 中有明确定义App2=时间维度编码、App3/6/7/8=member_id、App4/5=assistant_id_member_id
4. **数据库表结构**`ai_cache` 表包含 `expires_at timestamp with time zone` 字段DDL 层面支持过期时间。
#### ❌ 未实现部分
1. **expires_at 过期策略**
- DDL 中 `expires_at` 字段存在但代码中从未设置有效值
- `write_cache()` 接受 `expires_at` 参数但所有调用方App2-App8 的 `run()` 函数)均未传入 `expires_at`
- 没有定时任务或查询逻辑检查 `expires_at` 并清理过期记录
- `get_latest()` 查询时不过滤已过期记录
2. **主动失效条件**
- P5 spec 未定义具体的失效条件(如"数据源更新后旧缓存失效"
- NS3 spec 提到"ai_cache 表但未定义策略"——确实如此
- 当前实现是"只增不删"(除 500 条上限清理外),没有基于业务事件的缓存失效机制
3. **缓存读取时的新鲜度检查**
- `get_latest()` 仅按 `created_at DESC` 取最新一条,不检查缓存是否仍然有效
- 前端读取缓存时无法判断数据是否过时
### 证据
`cache_service.py` 中 `write_cache()` 的 `expires_at` 参数:
```python
def write_cache(
self,
cache_type: str,
site_id: int,
target_id: str,
result_json: dict,
triggered_by: str | None = None,
score: int | None = None,
expires_at: datetime | None = None, # 接受但从未被调用方传入
) -> int:
```
App2 调用 `write_cache` 时未传入 `expires_at`
```python
cache_svc.write_cache(
cache_type=CacheTypeEnum.APP2_FINANCE.value,
site_id=site_id,
target_id=time_dimension,
result_json=result,
triggered_by=f"user:{user_id}",
# 无 expires_at
)
```
DDL 中 `expires_at` 字段定义:
```sql
expires_at timestamp with time zone -- 存在但未被使用
```
### 建议
1. **定义各应用的缓存过期策略**
| 应用 | 建议 expires_at | 理由 |
|------|----------------|------|
| App2 | 当日 08:00营业日切点 | 财务数据每日更新,次日数据变化后旧缓存无意义 |
| App3 | 7 天 | 消费数据线索在下次消费前有效 |
| App4/5 | 3 天 | 关系分析和话术时效性较强 |
| App6 | 无过期 | 备注分析结果不会因时间失效 |
| App7 | 7 天 | 客户分析随新数据更新 |
| App8 | 无过期 | 整合线索由上游触发更新 |
2. **实现过期检查**:在 `get_latest()` 中增加 `WHERE expires_at IS NULL OR expires_at > NOW()` 过滤。
3. **考虑增加缓存失效事件**:当 App3/App6 产出新内容后,可标记旧的 App7 缓存为失效,确保前端读到最新分析。

View File

@@ -0,0 +1,128 @@
# P5.1→NS3 缺失项 #5LLM 调用的错误处理和降级策略
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 重试机制和异常分类已完整实现调用链级容错步骤失败后继续也已到位。但缺少限流429降级回退、模型不可用时的静态兜底内容、以及面向前端的统一错误码体系。
## 详细审查
### 审查范围
- `apps/backend/app/ai/bailian_client.py` — 百炼 API 统一封装层
- `apps/backend/app/ai/dispatcher.py` — AI 事件调度与调用链编排器
- `apps/backend/app/ai/apps/app1_chat.py` — App1 流式对话错误处理
- `apps/backend/app/ai/apps/app3_clue.py` — App3 数据获取降级示例
- `apps/backend/app/main.py` — AI 事件处理器注册的容错
### 发现
#### ✅ 已实现部分
1. **重试机制(指数退避)**`bailian_client.py``_call_with_retry()` 实现了完整的重试策略:
- 最多重试 3 次(`MAX_RETRIES = 3`
- 间隔1s → 2s → 4s指数退避`BASE_INTERVAL = 1`
- 5xx / 超时 / 连接错误:重试
- 4xx400/401/403/404/422/429不重试直接抛出
2. **异常分类体系**:定义了 4 个异常类:
- `BailianApiError`:通用 API 错误(含 status_code
- `BailianJsonParseError`JSON 解析失败(含 raw_content
- `BailianAuthError`API Key 无效401
- 继承关系清晰,调用方可按需捕获
3. **调用链级容错**`dispatcher.py``_run_step()` 实现了步骤级容错:
- 某步失败 → 记录错误日志 + 写入失败 conversation 记录 → 返回 None
- 后续步骤继续执行,使用已有缓存
- 整条链后台异步执行(`asyncio.create_task`),不阻塞业务请求
4. **App1 流式错误处理**`app1_chat.py``chat_stream()` 在异常时 yield `SSEEvent(type="error", message=str(e))`,前端可据此展示错误提示。
5. **数据获取降级**`app3_clue.py``build_prompt()` 在消费数据获取失败时降级为空值:
```python
except Exception:
member_data = _default_member_data()
data_fetch_failed = True
```
6. **启动容错**`main.py` 中 AI 事件处理器注册包裹在 try/except 中API Key 缺失或注册失败不影响后端启动。
#### ❌ 未实现部分
1. **限流429降级回退**
- 当前 429 RateLimitError 直接抛出,不重试
- 缺少限流时的降级策略(如返回缓存中的上一次结果、排队等待、降低调用频率)
- 消费事件链中如果百炼 API 限流,整个 App 步骤直接失败
2. **模型不可用时的静态兜底**
- 当百炼 API 完全不可用(如网络中断、服务宕机)时,重试 3 次后抛出 `BailianApiError`
- 对于 App2财务洞察、App4关系分析等前端直接展示的应用缺少静态兜底内容
- 前端读取 ai_cache 时如果没有缓存记录(首次调用就失败),会得到空结果
3. **统一错误码体系**
- App1 的 SSEEvent error 只传 `message=str(e)`,缺少结构化错误码
- 前端无法区分"网络超时"、"API Key 过期"、"限流"等不同错误类型
- 不同错误类型应有不同的用户提示(如限流→"AI 繁忙请稍后"、认证→"服务配置异常"
4. **熔断机制**
- 缺少熔断器Circuit Breaker连续失败 N 次后暂停调用,避免无效重试
- 高并发场景下(如多个消费事件同时触发),可能产生大量失败请求
### 证据
`bailian_client.py` 中的重试和异常处理:
```python
# 429 限流:直接抛出,不重试
except openai.RateLimitError as e:
logger.error("百炼 API 限流: %s", e)
raise BailianApiError(str(e), status_code=429) from e
# 5xx / 超时 / 连接错误:重试
except (openai.InternalServerError, openai.APIConnectionError,
openai.APITimeoutError) as e:
last_error = e
if attempt < self.MAX_RETRIES - 1:
wait_time = self.BASE_INTERVAL * (2 ** attempt)
await asyncio.sleep(wait_time)
```
`dispatcher.py` 中的步骤级容错:
```python
async def _run_step(self, app_name, run_func, context) -> dict | None:
try:
result = await run_func(context, self.bailian, ...)
return result
except Exception:
logger.exception("调用链步骤失败: %s", app_name)
# 写入失败 conversation 记录
# ...
return None
```
`main.py` 中的启动容错:
```python
try:
if _api_key and _base_url:
# ... 注册 AI 事件处理器
register_ai_handlers(_dispatcher)
except Exception:
_log.getLogger(__name__).warning(
"AI 事件处理器注册失败AI 功能不可用", exc_info=True)
```
### 建议
1. **429 限流降级**:捕获 `RateLimitError` 后,尝试返回 ai_cache 中该 (cache_type, site_id, target_id) 的最新缓存结果,并在结果中标注 `"_from_cache": true`,让前端知道这是缓存数据。
2. **统一错误码枚举**
```python
class AIErrorCode(str, Enum):
RATE_LIMITED = "rate_limited" # AI 繁忙,请稍后再试
AUTH_FAILED = "auth_failed" # 服务配置异常
TIMEOUT = "timeout" # 请求超时
SERVICE_DOWN = "service_down" # AI 服务暂时不可用
PARSE_ERROR = "parse_error" # AI 返回格式异常
```
3. **熔断器**:在 `BailianClient` 中增加简单的熔断逻辑——连续失败 5 次后,后续 60 秒内直接返回缓存或静态兜底,不再调用 API。
4. **App1 SSE 错误结构化**:将 `SSEEvent(type="error")` 扩展为包含 `error_code` 字段,前端据此展示不同的用户提示。

View File

@@ -0,0 +1,70 @@
# P5.1→NS3 缺失项 #6Token 用量监控和成本控制
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 已实现消息级 token 记录和 Prompt 长度截断,但缺少日/月预算控制、单次调用上限校验、超限熔断机制
## 详细审查
### 审查范围
- `apps/backend/app/ai/bailian_client.py` — 百炼 API 封装层
- `apps/backend/app/ai/conversation_service.py` — 对话持久化服务
- `apps/backend/app/ai/dispatcher.py` — AI 调用链调度器
- `apps/backend/app/ai/apps/app5_tactics.py` — 典型 App 调用流程
- `apps/backend/app/ai/cache_service.py` — 缓存服务
- `docs/database/ddl/zqyy_app__biz.sql` — biz schema DDL 基线
- `db/zqyy_app/migrations/` — 迁移脚本
### 发现
#### ✅ 已实现部分
1. **消息级 token 记录**`bailian_client.chat_json()` 返回 `(parsed_json, tokens_used)` 元组,从 `response.usage.total_tokens` 提取。所有 App3-8在调用后将 `tokens_used` 写入 `biz.ai_messages` 表。
2. **Prompt 长度截断**:各 App 的 `build_prompt()` 均实现了 system message 内容长度控制(如 App5 的 `_MAX_SYSTEM_CONTENT_LEN = 8000`),超长时截断服务记录、消费记录、备注等。
3. **API 层 max_tokens 参数**`chat_json()` 默认 `max_tokens=4000``chat_stream()` 默认 `max_tokens=2000`,限制了单次调用的输出 token 上限。
4. **数据库持久化**`biz.ai_messages` 表有 `tokens_used integer` 字段,每条 assistant 消息都记录了 token 消耗量。
#### ❌ 未实现部分
1. **无日/月预算控制**:代码中无任何按时间窗口(日/月)汇总 token 用量并与预算阈值比较的逻辑。
2. **无单次调用上限校验**`max_tokens` 是硬编码默认值,无基于配置表或环境变量的动态上限。
3. **无超限熔断机制**:当 token 消耗达到阈值时,无拒绝服务或降级处理逻辑。
4. **无 token 用量汇总表**:数据库中无 `ai_token_usage``ai_budget` 等汇总/预算表。
5. **无成本告警**:无日志告警或通知机制在 token 消耗异常时触发。
### 证据
**token 记录bailian_client.py L138**
```python
tokens_used = response.usage.total_tokens if response.usage else 0
return parsed, tokens_used
```
**消息写入conversation_service.py L77**
```python
INSERT INTO biz.ai_messages
(conversation_id, role, content, tokens_used)
VALUES (%s, %s, %s, %s)
```
**DDL 基线zqyy_app__biz.sql**
```sql
CREATE TABLE biz.ai_messages (
...
tokens_used integer,
...
);
```
**搜索 `token.*budget|cost.*control|usage.*limit|日预算|月预算` 无匹配结果**(仅匹配到缓存清理的"超限"字样,与 token 成本无关)。
### 建议
1. **新增 token 用量汇总视图或定时任务**:按 `site_id + app_id + 日期` 汇总 `biz.ai_messages.tokens_used`,写入 `biz.ai_token_daily_usage`
2. **新增预算配置**:在环境变量或配置表中定义 `AI_DAILY_TOKEN_LIMIT``AI_MONTHLY_TOKEN_LIMIT`
3. **在 dispatcher._run_step 前增加预算检查**:调用 AI 前查询当日/当月累计用量,超限则拒绝并记录日志
4. **告警机制**:当日用量达到预算 80% 时记录 WARNING 日志,达到 100% 时记录 ERROR 并熔断

View File

@@ -0,0 +1,79 @@
# P5.1→NS3 缺失项 #7App5 话术模板分类和质量评估标准
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- App5 已实现完整的话术生成流程数据获取→Prompt 构建→AI 调用→持久化),但话术输出仅按 scenario/script 结构化,缺少 P5.1 定义的话术分类体系(召回/维护/推荐)和质量评估维度
## 详细审查
### 审查范围
- `apps/backend/app/ai/apps/app5_tactics.py` — App5 话术参考实现
- `apps/backend/app/ai/schemas.py` — Pydantic 模型定义
- `apps/backend/app/ai/apps/app4_analysis.py` — App4 关系分析App5 上游)
- `tests/test_ai_apps/test_build_prompt_props.py` — 属性测试
- `tests/test_ai_apps/test_ai_apps_unit.py` — 单元测试
- `tests/test_p5_ai_integration_properties.py` — P5 集成属性测试
### 发现
#### ✅ 已实现部分
1. **完整调用链**App5 由 App4 联动触发,接收 `context["app4_result"]` 作为 `task_suggestion`,包含 `task_description``action_suggestions`
2. **数据驱动 Prompt**:并发获取助教信息、服务历史、消费数据、备注 4 类数据,构建丰富的上下文。
3. **Reference 机制**:引用最近 2 套 App8 历史结果作为 Prompt reference提供线索整合上下文。
4. **结构化输出**Pydantic 模型 `App5Result` 定义了 `tactics: list[App5TacticsItem]`,每条包含 `scenario`(场景)和 `script`(话术内容)。
5. **Token 预算控制**`_MAX_SYSTEM_CONTENT_LEN = 8000`,超长时分级截断服务记录→消费记录→备注。
6. **降级处理**4 类数据获取均有异常捕获和降级逻辑,部分失败不阻断。
#### ❌ 未实现部分
1. **无话术分类体系**Prompt 中 `output_format` 仅要求 `scenario + script`,未定义话术类型分类(如 P5.1 中的召回话术/维护话术/推荐话术)。当前 scenario 是自由文本,由 AI 自行决定场景描述。
2. **无质量评估标准**:无类似 App6 的 `score` 评分机制。App5 输出无评估维度(如话术的针对性、可执行性、情感适配度等)。
3. **无话术模板库**:无预定义的话术模板或参考范例供 AI 参考,完全依赖 AI 自由生成。
4. **Pydantic 模型无分类枚举**`App5TacticsItem` 仅有 `scenario: str``script: str`,无 `tactic_type``category` 枚举字段。
### 证据
**App5 输出格式定义app5_tactics.py L131-134**
```python
"output_format": {
"tactics": [
{"scenario": "场景描述", "script": "话术内容"}
]
},
```
**Pydantic 模型schemas.py**
```python
class App5TacticsItem(BaseModel):
scenario: str
script: str
class App5Result(BaseModel):
tactics: list[App5TacticsItem]
```
**对比 App6 有评分机制**
```python
class App6Result(BaseModel):
score: int = Field(ge=1, le=10)
clues: list[ClueItem]
```
App5 无类似评分或分类枚举。
### 建议
1. **新增话术类型枚举**:在 `schemas.py` 中定义 `App5TacticTypeEnum`(如 `recall`/`maintain`/`recommend`/`upsell`),在 `App5TacticsItem` 中增加 `tactic_type` 字段
2. **Prompt 中明确分类要求**:在 `output_format` 中增加 `tactic_type` 字段说明,引导 AI 按分类生成
3. **可选:增加质量评估**:参考 App6 的 score 机制,为每条话术增加 `relevance_score`(针对性评分)
4. **可选:话术模板库**:在 Prompt reference 中注入预定义的优秀话术范例,提升生成质量

View File

@@ -0,0 +1,78 @@
# P5.1→NS3 缺失项 #8各 App 的单元测试用例设计
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 已建立完整的测试体系覆盖属性测试Hypothesis+ 单元测试 + 集成属性测试,涵盖所有 8 个 AI 应用和 3 个 data_fetcher
## 详细审查
### 审查范围
- `tests/test_ai_apps/` — AI 应用测试目录3 个文件)
- `tests/test_data_fetchers/` — 数据获取器测试目录4 个文件)
- `tests/test_p5_ai_integration_properties.py` — P5 AI 集成属性测试
- `tests/test_rns1_*.py` — RNS1 Chat 模块属性测试8 个文件)
### 发现
#### ✅ 测试覆盖情况
**1. AI 应用属性测试(`tests/test_ai_apps/`**
| 文件 | 覆盖内容 | 测试数量 |
|------|----------|----------|
| `test_app1_props.py` | App1 biz_params 注入不变量、System Prompt Token 预算 | 2 个属性 |
| `test_build_prompt_props.py` | App3/4/5/6/7 的 Prompt 结构验证、错误降级、Token 预算 | 13 个属性 |
| `test_ai_apps_unit.py` | App1 页面上下文集成、App3/5/7 完整流程 | 6 个单元测试 |
**2. 数据获取器测试(`tests/test_data_fetchers/`**
| 文件 | 覆盖内容 | 测试数量 |
|------|----------|----------|
| `test_member_data_props.py` | 消费数据必填字段、正交易过滤、items_sum 口径、排序、备注截断 | 7 个属性 |
| `test_assistant_data_props.py` | 废单排除、排序保持 | 2 个属性 |
| `test_page_context_props.py` | 输出长度约束、全页面类型覆盖、敏感字段检测 | 3 个属性 |
| `test_data_fetchers_unit.py` | 空记录、FDW 超时、会员不存在、助教不存在等边界 | 10 个单元测试 |
**3. P5 集成属性测试(`test_p5_ai_integration_properties.py`**
覆盖 App2-App8 全部 7 个应用的 JSON 输出结构验证Pydantic 模型解析),包括:
- 枚举值合法性App3 的 3 分类、App6/8 的 6 分类)
- 字段非空约束
- App6 score 范围 [1,10] 及越界拒绝
- 每个应用 100 个随机用例
**4. Chat 模块属性测试(`tests/test_rns1_*.py`**
8 个文件覆盖 Chat 模块的排序、持久化、引用卡片、对话复用、SSE、标题生成、性能等属性。
#### 测试方法论
- **属性测试Hypothesis**:使用 `@given` + `@settings(max_examples=100)` 生成随机输入,验证不变量
- **单元测试Mock**Mock 数据获取函数(`AsyncMock`),不连真实数据库
- **边界条件**空记录、FDW 超时、数据获取失败降级、超长文本截断等
### 证据
**测试文件统计**
```
tests/test_ai_apps/ → 3 文件21+ 测试用例
tests/test_data_fetchers/ → 4 文件22+ 测试用例
tests/test_p5_ai_integration_properties.py → 9 个属性测试App2-8 + score 越界)
tests/test_rns1_*.py → 8 文件Chat 模块全覆盖
```
**典型属性测试示例test_build_prompt_props.py**
```python
# Property 14: 错误降级 — App5 数据获取失败不阻断
def test_prop14_app5_error_degradation(fail_assistant, fail_service, fail_member, fail_notes):
# Property 15: Token 预算 — App5 system message 长度 ≤ 8000
def test_prop15_app5_token_budget(records, notes):
```
### 建议
测试体系已较完整,以下为可选增强方向(非必须):
1. App2财务洞察和 App4关系分析`build_prompt` 属性测试可补充(当前仅有 App4 的结构测试,无 App2 的 Prompt 构建测试)
2. 端到端集成测试(`scripts/ops/test_chat_e2e.py` 已存在但为运维脚本,非 pytest 用例)

View File

@@ -0,0 +1,76 @@
# P5.1→NS3 缺失项 #9MCP Server 的健康检查端点和监控指标
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟡 低
- 后端 FastAPI 已有 `/health` 端点,但 MCP Server`apps/mcp-server/server.py`)无健康检查端点和监控指标
## 详细审查
### 审查范围
- `apps/backend/app/main.py` — 后端主入口
- `apps/mcp-server/server.py` — MCP Server 实现
- `apps/mcp-server/pyproject.toml` — MCP Server 依赖配置
### 发现
#### ✅ 后端 FastAPI 健康检查(已实现)
`apps/backend/app/main.py` 中定义了健康检查端点:
```python
@app.get("/health", tags=["系统"])
async def health_check():
"""健康检查端点,用于探活和监控。"""
return {"status": "ok"}
```
此外还有诊断端点 `/debug/config-paths`,返回关键路径配置信息。
#### ❌ MCP Server 健康检查(未实现)
`apps/mcp-server/server.py` 分析:
1. **无 `/health` 端点**MCP Server 基于 Starlette + MCP SDK 构建,`lifespan` 函数仅管理数据库连接池的打开/关闭,无健康检查路由。
2. **无监控指标**:无请求计数、延迟统计、错误率等监控指标暴露。
3. **有认证中间件**`AuthMiddleware` 验证 Bearer Token但无健康检查的豁免路径。
4. **MCP Server 架构**:提供 `list_tables``describe_table``query_sql` 等数据库查询工具,通过 SSE 协议与 AI 客户端通信。当前无法从外部探测其存活状态。
5. **数据库连接池**`lifespan``pool.open(wait=True, timeout=30)` 管理连接池,但连接池健康状态未暴露。
### 证据
**后端健康检查main.py**
```python
@app.get("/health", tags=["系统"])
async def health_check():
return {"status": "ok"}
```
**MCP Server lifespanserver.py L385-391**
```python
async def lifespan(app: Starlette):
pool.open(wait=True, timeout=30)
try:
async with mcp.session_manager.run():
yield
finally:
pool.close(timeout=5)
```
**MCP Server 无 health 路由**:搜索 `health|ping|status|monitor``apps/mcp-server/` 中无匹配结果。
### 建议
1. **为 MCP Server 添加 `/health` 端点**:在 Starlette 应用中注册健康检查路由,返回连接池状态和服务版本
```python
@app.route("/health")
async def health(request):
pool_status = "ok" if pool._pool and not pool._pool.closed else "degraded"
return JSONResponse({"status": pool_status, "service": "mcp-server"})
```
2. **健康检查豁免认证**:在 `AuthMiddleware.dispatch` 中对 `/health` 路径跳过 Token 验证
3. **可选:暴露基础监控指标**:请求计数、平均延迟、连接池使用率等(可通过 Prometheus 格式暴露)

View File

@@ -0,0 +1,127 @@
# P5.1→NS3 缺失项 #10AI 生成内容的审计日志
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- `biz.ai_conversations` + `biz.ai_messages` 两张表已完整记录"谁在什么时候对哪个客户生成了什么",满足审计日志需求
## 详细审查
### 审查范围
- `apps/backend/app/ai/conversation_service.py` — 对话持久化服务
- `apps/backend/app/services/chat_service.py` — Chat 模块服务
- `docs/database/ddl/zqyy_app__biz.sql` — biz schema DDL 基线
- `db/zqyy_app/migrations/2026-03-20__rns14_chat_module_extend.sql` — Chat 模块扩展迁移
- `apps/backend/app/ai/apps/app5_tactics.py` — 典型 App 调用流程(审计写入示例)
### 发现
#### ✅ 审计信息完整记录
**1. `biz.ai_conversations` 表 — 对话级审计**
| 字段 | 类型 | 审计用途 |
|------|------|----------|
| `id` | bigint PK | 对话唯一标识 |
| `user_id` | varchar(50) NOT NULL | **谁**:操作用户 ID系统自动调用时为 `'system'` |
| `nickname` | varchar(100) | 用户昵称 |
| `app_id` | varchar(30) NOT NULL | **什么应用**app1_chat / app2_finance / ... / app8_consolidation |
| `site_id` | bigint NOT NULL | **哪个门店** |
| `source_page` | varchar(100) | 触发来源页面 |
| `source_context` | jsonb | 上下文信息(含 `assistant_id``member_id` 等 → **对哪个客户** |
| `context_type` | varchar(20) | 对话关联类型task/customer/coach/general |
| `context_id` | varchar(50) | 关联 IDtaskId/customerId/coachId |
| `title` | varchar(200) | 对话标题 |
| `created_at` | timestamptz | **什么时候** |
**2. `biz.ai_messages` 表 — 消息级审计**
| 字段 | 类型 | 审计用途 |
|------|------|----------|
| `id` | bigint PK | 消息唯一标识 |
| `conversation_id` | bigint FK | 关联对话 |
| `role` | varchar(10) | system / user / assistant |
| `content` | text NOT NULL | **生成了什么**:完整的 AI 输入和输出内容 |
| `tokens_used` | integer | Token 消耗量 |
| `reference_card` | jsonb | 引用卡片数据 |
| `created_at` | timestamptz | 消息时间戳 |
**3. 写入时机**
所有 8 个 AI 应用在每次调用时都通过 `ConversationService` 写入完整审计链:
```
create_conversation(user_id, nickname, app_id, site_id, source_context)
→ add_message(role="system", content=prompt)
→ add_message(role="user", content=user_input)
→ 调用百炼 API
→ add_message(role="assistant", content=ai_output, tokens_used=N)
```
**4. 索引支持审计查询**
```sql
-- 按用户+门店查询历史对话
CREATE INDEX idx_ai_conv_user_site ON biz.ai_conversations (user_id, site_id, created_at DESC);
-- 按应用+门店查询
CREATE INDEX idx_ai_conv_app_site ON biz.ai_conversations (app_id, site_id, created_at DESC);
-- 按上下文查询(客户/任务/助教维度)
CREATE INDEX idx_ai_conv_context ON biz.ai_conversations (user_id, site_id, context_type, context_id, ...);
-- 按对话查询消息
CREATE INDEX idx_ai_msg_conv ON biz.ai_messages (conversation_id, created_at);
```
### 证据
**App5 审计写入示例app5_tactics.py L240-268**
```python
# 创建对话记录
conversation_id = conv_svc.create_conversation(
user_id=user_id, nickname=nickname,
app_id=APP_ID, site_id=site_id,
source_context={"assistant_id": assistant_id, "member_id": member_id},
)
# 写入 system + user 消息
conv_svc.add_message(conversation_id=conversation_id, role="system", content=...)
conv_svc.add_message(conversation_id=conversation_id, role="user", content=...)
# 调用 AI 后写入 assistant 消息
result, tokens_used = await bailian.chat_json(messages)
conv_svc.add_message(
conversation_id=conversation_id, role="assistant",
content=json.dumps(result, ensure_ascii=False),
tokens_used=tokens_used,
)
```
**DDL 基线确认zqyy_app__biz.sql**
```sql
CREATE TABLE biz.ai_conversations (
id bigint ... NOT NULL,
user_id character varying(50) NOT NULL,
nickname character varying(100) ...,
app_id character varying(30) NOT NULL,
site_id bigint NOT NULL,
source_page character varying(100),
source_context jsonb,
context_type character varying(20),
context_id character varying(50),
...
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE biz.ai_messages (
id bigint ... NOT NULL,
conversation_id bigint NOT NULL,
role character varying(10) NOT NULL,
content text NOT NULL,
tokens_used integer,
reference_card jsonb,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
```
### 建议
审计日志功能已完整实现,以下为可选增强方向(非必须):
1. **审计查询 API**:当前仅有面向用户的历史对话查询(`get_conversations`可增加面向管理员的审计查询接口按时间范围、app_id、site_id 筛选)
2. **数据保留策略**:当前无自动清理机制,长期运行后 `ai_messages.content` 可能占用大量存储,建议制定保留策略

View File

@@ -0,0 +1,53 @@
# P6→NS1/RNS1 缺失项 #1任务卡片 5 种状态的视觉规范
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 前端已实现 3 种状态(待处理/已置顶/已放弃)的视觉差异,但缺少「已完成」和「过期」两种独立状态的视觉规范。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/utils/vi-colors.ts` — TASK_STATUS_COLORS
- `apps/miniprogram/miniprogram/utils/task-config.ts` — TASK_STATUS_CONFIG
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml` — 卡片模板
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxss` — 卡片样式
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts` — 分组逻辑
### 发现
**已实现的 3 种状态:**
1. **待处理normal/pending**:白色卡片,左侧彩条按任务类型着色(红/橙/粉/青),正常文字颜色
2. **已置顶pinned**:在待处理基础上增加金色光晕阴影(`box-shadow: 0 5rpx 7rpx rgba(245,158,11,0.12), 0 0 0 8rpx rgba(245,158,11,0.08)`
3. **已放弃abandoned**:左侧彩条变灰(`--status-abandoned-border`),整卡透明度降低(`opacity: 0.55`),标签灰化,文字变灰
**缺失的 2 种状态:**
4. **已完成completed**`task-config.ts``TASK_STATUS_CONFIG` 中无 `completed` 键;`vi-colors.ts``TASK_STATUS_COLORS` 中无 `completed` 定义WXML 中无 `task-card--completed`
5. **过期expired**:无独立的过期卡片状态样式。过期目前仅通过 `deadline` 字段在卡片内显示红色逾期徽章(`overdue-badge`),但卡片整体样式不变
### 证据
`vi-colors.ts` 第 4 节仅定义了两种状态:
```typescript
export const TASK_STATUS_COLORS = {
pinned: { name: '置顶', glowColor: '#f59e0b', ... },
abandoned: { name: '放弃', borderColor: '#d1d5db', textColor: '#9ca3af', opacity: 0.55 },
}
```
`task-config.ts` 仅定义了三种状态:
```typescript
export const TASK_STATUS_CONFIG = {
normal: { label: '进行中', icon: '📋' },
pinned: { label: '已置顶', icon: '📌' },
abandoned: { label: '已放弃', icon: '❌' },
}
```
### 建议(如未完全解决)
1.`TASK_STATUS_COLORS``TASK_STATUS_CONFIG` 中补充 `completed``expired` 状态定义
2. 已完成状态建议:绿色勾选图标 + 轻微灰化opacity 0.7-0.8),左侧彩条保留但降低饱和度
3. 过期状态建议:红色边框或红色背景提示,与逾期徽章配合使用
4. 注意:后端 `get_task_list_v2` 按 status 筛选pending/completed/abandoned前端目前仅请求 pending 状态completed 列表页尚未实现

View File

@@ -0,0 +1,52 @@
# P6→NS1/RNS1 缺失项 #23 种空状态设计
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 已实现 2 种空状态(无任务、网络错误),但缺少「筛选无结果」空状态,且现有空状态缺少插图。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml` — 空状态模板
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts` — pageState 逻辑
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxss` — 空状态样式
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxml` — 详情页空状态
### 发现
**已实现的 2 种空状态:**
1. **无任务empty**`pageState === 'empty'` 时显示纯文字「暂无待办任务」
2. **网络错误error**`pageState === 'error'` 时显示「加载失败,请重试」+ 重试按钮
**缺失项:**
3. **筛选无结果**:当前 task-list 页面无筛选功能(无 filter-dropdown 组件引用),因此无筛选无结果状态。但 P6 定义了此场景,未来添加筛选时需要补充
4. **插图缺失**:两种空状态均为纯文字,无 SVG/PNG 插图。P6 定义了每种空状态应有对应插图
5. task-detail 页面有更完善的空状态:使用了 `t-icon` 图标info-circle / close-circle但仍非 P6 定义的专属插图
### 证据
task-list.wxml 空状态实现:
```html
<!-- 空状态 -->
<view class="state-empty" wx:if="{{pageState === 'empty'}}">
<text class="empty-text">暂无待办任务</text>
</view>
<!-- Error 状态 -->
<view class="state-error" wx:if="{{pageState === 'error'}}">
<text class="error-text">加载失败,请重试</text>
<view class="retry-btn" bindtap="onRetry">
<text class="retry-btn-text">重试</text>
</view>
</view>
```
pageState 类型定义仅 4 种:`'loading' | 'empty' | 'error' | 'normal'`,无 `'filter-empty'` 状态。
### 建议(如未完全解决)
1. 为 empty 和 error 状态添加 SVG 插图(建议放在 `/assets/images/empty-*.svg`
2. 预留 `filter-empty` 状态,文案如「没有找到匹配的任务,试试调整筛选条件」
3. 可使用 TDesign 的 `t-empty` 组件替代自定义实现,自带图标和文案插槽

View File

@@ -0,0 +1,52 @@
# P6→NS1/RNS1 缺失项 #3置顶任务排序规则
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 后端已实现「置顶优先 → 优先级分数降序 → 创建时间升序」的排序规则,与 P6 定义的排序意图一致。
## 详细审查
### 审查范围
- `apps/backend/app/services/task_manager.py``get_task_list_v2()` 函数中的 SQL ORDER BY
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts` — 前端分组逻辑
### 发现
**后端排序(已实现):**
`task_manager.py``get_task_list_v2()` 中 SQL 查询明确定义了排序规则:
```sql
ORDER BY is_pinned DESC,
priority_score DESC NULLS LAST,
created_at ASC
```
- `is_pinned DESC`:置顶任务排在最前
- `priority_score DESC NULLS LAST`:非置顶任务按优先级分数降序(高优先级在前)
- `created_at ASC`:同优先级按创建时间升序(先创建的在前)
**前端分组(已实现):**
`task-list.ts``loadData()` 中将后端返回的列表按状态分组:
```typescript
const pinnedTasks = enriched.filter((t) => t.isPinned && !t.isAbandoned)
const normalTasks = enriched.filter((t) => !t.isPinned && !t.isAbandoned && t.status === 'pending')
const abandonedTasks = enriched.filter((t) => t.isAbandoned)
```
WXML 中按「📌 置顶 → 正常任务 → 已放弃」三组依次渲染,组内保持后端返回顺序。
### 证据
后端 SQLtask_manager.py 第 560-564 行):
```sql
ORDER BY is_pinned DESC,
priority_score DESC NULLS LAST,
created_at ASC
LIMIT %s OFFSET %s
```
### 建议(如未完全解决)
- P6 提到「置顶任务按置顶时间倒序」,当前实现是 `is_pinned DESC`(布尔值),多个置顶任务之间的排序依赖 `priority_score`。如需严格按置顶时间排序,需在 `coach_tasks` 表中添加 `pinned_at` 时间戳字段并在 ORDER BY 中使用。当前实现在功能上可接受,但与 P6 的精确定义有微小差异。

View File

@@ -0,0 +1,54 @@
# P6→NS1/RNS1 缺失项 #4放弃/取消放弃的二次确认弹窗
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 放弃操作已实现完整的二次确认弹窗abandon-modal 组件),含原因输入、确认/取消按钮。取消放弃在 task-detail 页面无二次确认(直接执行),在 task-list 页面通过长按菜单触发也无二次确认。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/components/abandon-modal/abandon-modal.wxml` — 弹窗模板
- `apps/miniprogram/miniprogram/components/abandon-modal/abandon-modal.ts` — 弹窗逻辑
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts` — 列表页放弃/取消放弃流程
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.ts` — 详情页放弃/取消放弃流程
### 发现
**放弃操作(完整实现):**
`abandon-modal` 组件实现了完整的二次确认弹窗:
- ⚠️ 警告图标 + 标题「放弃 {客户名}」
- 描述文案「确定放弃该客户的维护任务?请填写原因:」
- 必填原因输入框maxlength=200
- 「确认放弃」按钮(原因为空时禁用)+ 「取消」按钮
- 键盘弹出时自适应布局
task-list 和 task-detail 页面均引用了此组件:
- task-list长按菜单 → 点击「放弃任务」→ 打开 abandon-modal
- task-detail点击右上角「放弃」按钮 → 打开 abandon-modal
**取消放弃操作(简化实现):**
- task-list长按已放弃任务 → 菜单显示「↩️ 取消放弃」→ 直接执行showLoading → showToast无二次确认弹窗
- task-detail点击右上角「取消放弃」→ 直接执行 `cancelAbandon()`,无二次确认
### 证据
abandon-modal.wxml 核心结构:
```html
<view class="modal-header">
<text class="modal-emoji">⚠️</text>
<text class="modal-title">放弃 <text class="modal-name">{{customerName}}</text></text>
</view>
<view class="modal-desc-wrap">
<text class="modal-desc">确定放弃该客户的维护任务?请填写原因:</text>
</view>
<textarea maxlength="200" ... />
<view class="confirm-btn" bindtap="onConfirm">确认放弃</view>
<view class="cancel-btn" bindtap="onCancel">取消</view>
```
### 建议(如未完全解决)
- 取消放弃操作风险较低(恢复任务),不设二次确认是合理的 UX 决策
- 如 P6 严格要求取消放弃也需二次确认,可复用 `wx.showModal` 实现简单确认弹窗

View File

@@ -0,0 +1,81 @@
# P6→NS1/RNS1 缺失项 #5下拉刷新/触底加载的动画规范
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 已实现下拉刷新、触底加载、骨架屏 loading、错误重试的完整交互链路。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts` — onPullDownRefresh / onReachBottom / loadData
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml` — skeleton 骨架屏
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxss` — loading 样式
### 发现
**下拉刷新(已实现):**
```typescript
onPullDownRefresh() {
this.loadData(() => {
wx.stopPullDownRefresh()
})
}
```
使用微信原生下拉刷新机制,刷新完成后调用 `wx.stopPullDownRefresh()` 停止动画。
**触底加载(已实现):**
```typescript
onReachBottom() {
if (!this.data.hasMore) return
this.setData({ hasMore: false })
wx.showToast({ title: '没有更多了', icon: 'none' })
}
```
当前为简化实现一次性加载触底时显示「没有更多了」提示。后端已支持分页参数page/page_size
**骨架屏 Loading已实现**
```html
<view class="state-loading" wx:if="{{pageState === 'loading'}}">
<view class="loading-placeholder" wx:for="{{[1,2,3]}}" wx:key="*this">
<view class="ph-line ph-line--title"></view>
<view class="ph-line ph-line--body"></view>
<view class="ph-line ph-line--short"></view>
</view>
</view>
```
3 个占位卡片,每个含标题行、内容行、短行,模拟真实卡片布局。
**错误重试(已实现):**
```html
<view class="state-error" wx:if="{{pageState === 'error'}}">
<text class="error-text">加载失败,请重试</text>
<view class="retry-btn" bindtap="onRetry">重试</view>
</view>
```
**task-detail 页面也有完整状态管理:**
- loading使用 `t-loading` 组件的 circular 主题浮层
- empty`t-icon` info-circle + 文案
- error`t-icon` close-circle + 重试按钮
### 证据
骨架屏样式task-list.wxss
```css
.loading-placeholder {
background: #ffffff;
border-radius: 22rpx;
padding: 29rpx;
margin-bottom: 22rpx;
}
.ph-line { height: 22rpx; background: #f3f3f3; border-radius: 7rpx; margin-bottom: 15rpx; }
.ph-line--title { width: 40%; height: 29rpx; }
.ph-line--body { width: 80%; }
.ph-line--short { width: 55%; }
```
### 建议(如未完全解决)
- 触底加载目前是简化实现,未真正调用分页接口加载下一页。后端已支持 `page`/`page_size` 参数,前端需补充增量加载逻辑
- 骨架屏可考虑使用 TDesign 的 `t-skeleton` 组件替代自定义实现,获得更丰富的动画效果
- 可添加下拉刷新时的自定义 loading 动画(当前使用微信原生样式)

View File

@@ -0,0 +1,64 @@
# P6→NS1/RNS1 缺失项 #6AI 分析卡片的折叠/展开交互
## 简要结论
- 状态:❌ 未解决
- 风险等级:🔴 高
- task-detail 页面中 AI 分析内容以固定展开方式呈现,无折叠/展开交互,无「重新生成」按钮,无 AI 加载状态。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxml` — AI 分析卡片模板
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.ts` — AI 分析逻辑
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxss` — AI 分析样式
### 发现
**当前实现:**
task-detail 页面中 AI 相关内容分布在多个卡片中,均为固定展开状态:
1. **「与我的关系」卡片**:显示 `detail.aiAnalysis.summary`,无折叠控制
2. **「任务建议」卡片**:显示 `detail.aiAnalysis.suggestions` 列表 + 话术参考,无折叠控制
3. **「行动建议」卡片**:显示 `detail.actionSuggestions`,条件渲染(有数据才显示),无折叠控制
**P6 定义但未实现的交互:**
1. **折叠/展开**P6 定义 AI 分析卡片应支持折叠/展开切换,默认展开,用户可收起以减少页面长度。当前无任何折叠/展开按钮或逻辑
2. **重新生成按钮**P6 定义 AI 分析卡片应有「重新生成」按钮,允许用户触发 AI 重新分析。当前无此按钮
3. **AI 加载状态**P6 定义 AI 分析生成中应显示 loading 动画(如骨架屏或 spinner。当前 AI 数据随任务详情一起返回,无独立加载状态
### 证据
task-detail.wxml 中 AI 相关卡片(无折叠/展开控制):
```html
<!-- 与我的关系 -->
<view class="card">
<view class="card-header">
<text class="section-title title-pink">与我的关系</text>
<ai-title-badge color="{{aiColor}}" />
</view>
<!-- 直接展示,无折叠控制 -->
<view class="card-desc-wrap">
<text class="card-desc">{{detail.aiAnalysis.summary}}</text>
</view>
</view>
<!-- 任务建议 -->
<view class="card">
<view class="card-header">
<text class="section-title title-orange">任务建议</text>
<ai-title-badge color="{{aiColor}}" />
</view>
<!-- 直接展示所有建议,无折叠控制,无重新生成按钮 -->
...
</view>
```
task-detail.ts 中无任何 AI 折叠/展开相关的 data 字段或方法。
### 建议(如未完全解决)
1. 为每个 AI 卡片添加 `expanded` 状态和切换按钮(如 `▴ 收起` / `▾ 展开`),参考 note-modal 中 `ratingExpanded` 的实现模式
2. 在卡片 header 右侧添加「🔄 重新生成」按钮,点击后调用 AI 分析接口
3. 添加 AI 加载状态:可使用 `t-loading` 或自定义骨架屏,在 AI 数据未返回时显示
4. 后端需提供独立的 AI 重新生成接口(当前 `ai_cache` 仅支持读取缓存)

View File

@@ -0,0 +1,65 @@
# P6→NS1/RNS1 缺失项 #7任务优先级的视觉标识
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 任务类型(高优先召回/优先召回/关系构建/客户回访)有完整的颜色和标签视觉体系,但缺少独立的「高/中/低优先级」视觉标识。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/utils/vi-colors.ts` — TASK_TYPE_COLORS
- `apps/miniprogram/miniprogram/utils/task-config.ts` — TASK_TYPE_CONFIG
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml` — 卡片标签
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxss` — 标签样式
- `apps/backend/app/services/task_manager.py` — priority_score 字段
### 发现
**已实现的任务类型视觉体系:**
4 种任务类型各有独立的颜色方案:
| 类型 | 标签渐变 | 左侧彩条 | 标签文字 |
|------|----------|----------|----------|
| high_priority高优先召回 | #b91c1c#dc2626(红) | #dc2626 | 白色 |
| priority_recall优先召回 | #ea580c#f97316(橙) | #f97316 | 白色 |
| relationship关系构建 | #ec4899#f472b6(粉) | #f472b6 | 白色 |
| callback客户回访 | #0d9488#14b8a6(青) | #14b8a6 | 白色 |
**缺失的优先级视觉标识:**
P6 定义了独立于任务类型的「高/中/低优先级」视觉标识(颜色和图标),但当前实现中:
- 后端返回 `priority_score`(数值),但前端未使用此字段进行视觉区分
- 前端仅按 `taskType` 着色,未按 `priority_score` 显示优先级图标或颜色
- `vi-colors.ts``task-config.ts` 中无 `priority` 相关的颜色/图标定义
### 证据
后端返回 priority_score 但前端未消费:
```python
# task_manager.py get_task_list_v2()
items.append({
...
"task_type": task_type,
# priority_score 未包含在返回数据中
})
```
前端 enrichTask() 中无 priority 相关处理:
```typescript
function enrichTask(task: Task): EnrichedTask {
return {
...task,
// 无 priority 相关字段
deadlineLabel: formatDeadline((task as any).deadline).text,
deadlineStyle: formatDeadline((task as any).deadline).style,
}
}
```
### 建议(如未完全解决)
1. 在后端 items 中返回 `priority_score` 或映射为 `priority_level`high/medium/low
2.`vi-colors.ts` 中添加 `PRIORITY_COLORS` 定义(如:高=红色火焰图标、中=橙色、低=灰色)
3. 在卡片中添加优先级小图标或角标,与任务类型标签并列显示
4. 注意:当前任务类型名称已隐含优先级信息(「高优先召回」),是否需要额外的优先级标识需与产品确认

View File

@@ -0,0 +1,68 @@
# P6→NS1/RNS1 缺失项 #8任务到期倒计时的展示规则
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 已实现完整的到期倒计时展示规则,包含 4 级颜色变化(灰/正常/橙色警告/红色逾期),与 DISPLAY-STANDARDS-2.md §7 规范对齐。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/utils/time.ts``formatDeadline()` 函数
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml` — deadline 展示
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxss` — deadline 样式
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts` — enrichTask 中的 deadline 处理
### 发现
**`formatDeadline()` 函数(完整实现):**
```typescript
export function formatDeadline(deadline: string | null | undefined):
{ text: string; style: 'normal' | 'warning' | 'danger' | 'muted' } {
if (!deadline) return { text: '--', style: 'muted' }
const diff = /* 天数差 */
if (diff < 0) return { text: `逾期 ${Math.abs(diff)}`, style: 'danger' }
if (diff === 0) return { text: '今天到期', style: 'warning' }
if (diff <= 7) return { text: `还剩 ${diff}`, style: 'normal' }
return { text: `${mm}-${dd}`, style: 'muted' }
}
```
**4 级颜色映射WXSS 已实现):**
| 条件 | 文案 | style | 颜色 |
|------|------|-------|------|
| 无截止日期 | `--` | muted | #a6a6a6(灰) |
| > 7 天 | `MM-DD` | muted | #a6a6a6(灰) |
| 1-7 天 | `还剩 N 天` | normal | #5e5e5e(深灰) |
| 今天 | `今天到期` | warning | #ed7b2f(橙) |
| 已逾期 | `逾期 N 天` | danger | #e34d59(红) |
**逾期徽章(额外实现):**
逾期任务在卡片第一行右侧显示红色逾期徽章(`overdue-badge`),与 deadline 行的红色文字形成双重提醒。
### 证据
WXSS 中的 deadline 颜色定义:
```css
.deadline-text--muted { color: #a6a6a6; }
.deadline-text--normal { color: #5e5e5e; }
.deadline-text--warning { color: #ed7b2f; }
.deadline-text--danger { color: #e34d59; font-weight: 600; }
```
WXML 中的 deadline 展示逻辑:
```html
<!-- 逾期徽章danger 级别显示在第一行) -->
<text class="overdue-badge" wx:if="{{item.deadlineStyle === 'danger'}}">{{item.deadlineLabel}}</text>
<!-- 非逾期的 deadline 显示在独立行 -->
<view class="card-row-deadline" wx:if="{{item.deadlineLabel && item.deadlineLabel !== '--' && item.deadlineStyle !== 'danger'}}">
<text class="deadline-text deadline-text--{{item.deadlineStyle}}">{{item.deadlineLabel}}</text>
</view>
```
### 建议(如未完全解决)
- 当前实现已覆盖 P6 定义的核心需求。如需更细粒度的颜色变化(如 3 天内黄色、1 天内橙色),可在 `formatDeadline()` 中增加判断分支

View File

@@ -0,0 +1,81 @@
# P6→NS1/RNS1 缺失项 #9备注输入框的字数限制和实时计数
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 已实现字数限制maxlength=500但缺少实时字数计数展示如「128/500」
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/components/note-modal/note-modal.wxml` — 备注弹窗模板
- `apps/miniprogram/miniprogram/components/note-modal/note-modal.ts` — 备注弹窗逻辑
- `apps/miniprogram/miniprogram/components/abandon-modal/abandon-modal.wxml` — 放弃弹窗(对比)
### 发现
**字数限制(已实现):**
note-modal 的 textarea 设置了 `maxlength="500"`
```html
<textarea
class="note-textarea"
placeholder="请输入备注内容..."
maxlength="500"
...
/>
```
abandon-modal 的 textarea 设置了 `maxlength="200"`
```html
<textarea
class="abandon-textarea"
placeholder="请输入放弃原因(必填)"
maxlength="200"
...
/>
```
**实时字数计数(未实现):**
两个弹窗均未显示当前输入字数和剩余字数。P6 定义了实时计数展示如输入框右下角显示「128/500」当前实现中
- `note-modal.ts``onContentInput` 仅更新 `content` 值,未计算或展示字数
- WXML 中无字数计数的 `<text>` 元素
- WXSS 中无字数计数的样式定义
### 证据
note-modal.ts 中的输入处理(无字数计数):
```typescript
onContentInput(e: WechatMiniprogram.CustomEvent<{ value: string }>) {
this.setData({ content: e.detail.value })
}
```
note-modal.wxml 中 textarea 区域(无计数展示):
```html
<view class="textarea-section">
<textarea
class="note-textarea"
placeholder="请输入备注内容..."
value="{{content}}"
bindinput="onContentInput"
maxlength="500"
auto-height
...
/>
<!-- 此处缺少字数计数展示 -->
</view>
```
### 建议(如未完全解决)
1. 在 note-modal 的 textarea 下方添加字数计数:
```html
<text class="char-count">{{content.length}}/500</text>
```
2. 在 abandon-modal 的 textarea 下方添加字数计数:
```html
<text class="char-count">{{content.length}}/200</text>
```
3. 样式建议右对齐、小字号22rpx、灰色#a6a6a6接近限制时变橙/红色
4. 可在 `onContentInput` 中添加接近限制的提示逻辑(如剩余 20 字时变色)

View File

@@ -0,0 +1,43 @@
# P6→NS1/RNS1 缺失项 #10任务详情页各模块的折叠/展开默认状态
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟠 中
- 任务详情页所有模块(与我的关系、任务建议、维客线索、备注、服务记录)均为始终展开状态,无折叠/展开控制机制。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxml`
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.ts`
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxss`
### 发现
1. task-detail.wxml 中所有 `.card` 区块(与我的关系、任务建议、维客线索、行动建议、备注、服务记录)均直接渲染,无 `wx:if` 条件控制折叠状态
2. task-detail.ts 的 `data` 中无任何 `collapsed`/`expanded`/`folded` 状态变量
3.`toggleSection`/`toggleCollapse` 等方法
4. 唯一的展开/收起逻辑是维客线索卡片的 `onToggleClue`(控制单条线索描述的展开),但这不是模块级折叠
### 证据
task-detail.wxml 中各模块均为直接渲染:
```xml
<!-- 与我的关系 -->
<view class="card">...</view>
<!-- 任务建议 -->
<view class="card">...</view>
<!-- 维客线索 -->
<view class="card">...</view>
<!-- 备注 -->
<view class="card">...</view>
<!-- 服务记录 -->
<view class="card">...</view>
```
无任何折叠/展开的 `wx:if``hidden` 控制。
task-detail.ts 中无折叠状态变量grep `collapsed|expanded|fold|toggleSection|toggleCollapse` 结果为空)。
### 建议
1. 为每个模块添加折叠状态变量(如 `sectionCollapsed: { relationship: false, suggestion: false, clues: false, notes: true, records: true }`
2.`.card-header` 上添加 `bindtap` 事件切换折叠状态
3. 建议默认展开前 3 个模块(关系、建议、线索),折叠后 2 个(备注、服务记录),减少首屏滚动长度
4. 添加折叠/展开的过渡动画(`max-height` + `transition`

View File

@@ -0,0 +1,51 @@
# P6→NS1/RNS1 缺失项 #11维客线索的展示样式
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟠 中(原始风险已消除)
- clue-card 组件已实现完整的 tag 颜色映射6 种 + 2 种别名)和卡片布局,样式覆盖 P6 定义的所有场景。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/components/clue-card/clue-card.wxml`
- `apps/miniprogram/miniprogram/components/clue-card/clue-card.ts`
- `apps/miniprogram/miniprogram/components/clue-card/clue-card.wxss`
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxml`(调用处)
### 发现
1. clue-card 组件已实现,接受 `tag``category``emoji``title``source``content` 六个属性
2. wxss 中定义了 8 种 tag 颜色类,覆盖 VI 规范 2.1 六种客户标签配色:
- `clue-tag-primary`(客户基础 — 蓝色)
- `clue-tag-success`(消费习惯 — 绿色)
- `clue-tag-orange`(玩法偏好 — 橙色)
- `clue-tag-gold`(促销偏好 — 金色)
- `clue-tag-purple`(社交关系 — 紫色)
- `clue-tag-error`(重要反馈 — 红色)
- `clue-tag-pink`(社交关系别名)
- `clue-tag-warning`(促销偏好别名)
3. 卡片布局包含72rpx 方形 tag 图标 + 右侧内容区(标题+来源+描述)
4. task-detail.wxml 中通过 `<clue-card>` 组件渲染维客线索列表
### 证据
clue-card.wxss 中的颜色映射:
```css
/* VI 规范 2.1 六种客户标签配色 */
.clue-tag-primary { background: rgba(0, 82, 217, 0.10); color: #0052d9; }
.clue-tag-success { background: rgba(0, 168, 112, 0.10); color: #00a870; }
.clue-tag-orange { background: rgba(237, 123, 47, 0.12); color: #ed7b2f; }
.clue-tag-gold { background: rgba(251, 191, 36, 0.15); color: #d4920a; }
.clue-tag-purple { background: rgba(123, 97, 255, 0.10); color: #7b61ff; }
.clue-tag-error { background: rgba(227, 77, 89, 0.10); color: #e34d59; }
```
task-detail.wxml 调用处:
```xml
<clue-card wx:for="{{retentionClues}}" wx:key="index"
tag="{{item.tag}}" category="{{item.tagColor}}"
emoji="{{item.emoji}}" title="{{item.text}}"
source="{{item.source}}" content="{{item.desc || ''}}" />
```
### 建议
无需额外补充。

View File

@@ -0,0 +1,43 @@
# P6→NS1/RNS1 缺失项 #12任务列表页的搜索功能
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟠 中
- 前端无搜索组件,后端 TASK-1 API 不支持搜索参数,前端 services 层无搜索相关调用。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml`
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts`
- `apps/backend/app/routers/xcx_tasks.py`
- `apps/backend/app/schemas/xcx_tasks.py`
- `apps/miniprogram/miniprogram/services/`grep 搜索)
### 发现
1. task-list.wxml 中无 `<t-search>` 或任何搜索输入组件
2. task-list.ts 中无搜索相关的 data 字段(如 `searchKeyword`)或方法(如 `onSearch`
3. 后端 `GET /api/xcx/tasks` 仅支持 `status``page``page_size` 三个查询参数,无 `keyword`/`search`/`query` 参数
4. services 目录下 grep `search|keyword|query` 结果为空
5. TaskListResponse schema 中无搜索相关字段
### 证据
后端路由签名:
```python
@router.get("", response_model=TaskListResponse)
async def get_tasks(
status: str = Query("pending", pattern="^(pending|completed|abandoned)$"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: CurrentUser = Depends(require_approved()),
):
```
`keyword``search` 参数。
task-list.wxml 中 banner 区域和任务列表区域之间无搜索栏。
### 建议
1. 前端:在 banner 下方、任务列表上方添加 `<t-search>` 组件,支持按客户名/手机号搜索
2. 后端:`GET /api/xcx/tasks` 添加可选参数 `keyword: str = Query(None)`,在 SQL 中对 `member_name``member_phone``ILIKE` 模糊匹配
3. 前端搜索应做防抖300ms避免频繁请求
4. 搜索结果为空时显示专用空状态("未找到匹配的客户"

View File

@@ -0,0 +1,51 @@
# P6→NS1/RNS1 缺失项 #13任务完成后的成功反馈动画
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 已有基础的 `wx.showToast` 反馈(备注保存、删除、放弃/取消放弃),但缺少 P6 定义的"任务完成"专属成功动画(如 Lottie 动画、全屏庆祝效果)。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.ts`
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxml`
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxss`
### 发现
1. task-detail.ts 中存在多处 `wx.showToast` 调用,覆盖以下操作:
- 备注保存:`wx.showToast({ title: '备注已保存', icon: 'success' })`
- 备注删除:`wx.showToast({ title: '已删除', icon: 'success' })`
- 取消放弃:`wx.showToast({ title: '已取消放弃', icon: 'success' })`
- 放弃任务:`wx.showToast({ title: '已放弃该客户的维护', icon: 'none' })`
- 手机号复制:`wx.showToast({ title: '手机号码已复制', icon: 'none' })`
2. 但无"任务完成"的专属操作入口和反馈动画
3. 无 Lottie 动画组件、无自定义成功动画 CSS、无全屏庆祝效果
4. 当前任务状态只有 `pending``abandoned`,缺少 `completed` 状态的处理流程
### 证据
task-detail.ts 中的 toast 调用(仅为操作反馈,非任务完成动画):
```typescript
// 备注保存
wx.showToast({ title: '备注已保存', icon: 'success' })
// 取消放弃
wx.showToast({ title: '已取消放弃', icon: 'success' })
// 放弃
wx.showToast({ title: '已放弃该客户的维护', icon: 'none' })
```
task-detail.wxml 底部操作栏只有"问问助手"和"备注"两个按钮,无"标记完成"按钮:
```xml
<view class="bottom-bar safe-area-bottom">
<view class="btn-ask" bindtap="onAskAssistant">...</view>
<view class="btn-note" bindtap="onAddNote">...</view>
</view>
```
### 建议
1. 在底部操作栏添加"标记完成"按钮(或在长按菜单中添加)
2. 任务完成后显示自定义成功动画(推荐方案):
- 方案 A全屏半透明遮罩 + CSS 动画(✓ 图标放大 + 文字淡入)
- 方案 B引入 Lottie 动画组件(`lottie-miniprogram`)播放庆祝动画
3. 动画播放完毕后自动返回任务列表页,并刷新列表数据
4. 后端需添加 `POST /api/xcx/tasks/{id}/complete` 接口

View File

@@ -0,0 +1,64 @@
# P6→NS1/RNS1 缺失项 #14网络异常时的离线提示和重试机制
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- request.ts 有基础的错误抛出和 401 自动刷新机制,页面级有错误状态和重试按钮,但缺少统一的网络异常拦截、离线检测提示和全局重试机制。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/utils/request.ts`
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml`
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts`
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxml`
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.ts`
### 发现
#### 已实现的部分
1. `request.ts``wxRequest``fail` 回调会 reject 错误(`statusCode: 0`),但未做网络类型判断
2. `request.ts` 实现了 401 自动刷新 token + 排队重试机制
3. task-list.wxml 有错误状态 UI + 重试按钮:
```xml
<view class="state-error" wx:if="{{pageState === 'error'}}">
<text class="error-text">加载失败,请重试</text>
<view class="retry-btn" bindtap="onRetry">重试</view>
</view>
```
4. task-detail.wxml 同样有错误状态 + 重试按钮
#### 缺失的部分
1. `request.ts` 中无 `wx.getNetworkType()` 离线检测
2. 无全局网络状态监听(`wx.onNetworkStatusChange`
3. 无统一的网络异常 toast 提示(如"网络连接失败,请检查网络"
4. 无请求超时配置wx.request 默认 60s
5. 无自动重试机制(非 401 场景的网络错误不会自动重试)
6. 无离线缓存策略(断网时无法展示上次加载的数据)
### 证据
request.ts 中 fail 回调仅简单 reject
```typescript
fail(err) {
reject({ statusCode: 0, data: err })
},
```
无网络类型判断、无离线提示、无重试逻辑。
task-list.ts 中 loadData 的 catch 仅设置错误状态:
```typescript
} catch {
this.setData({ pageState: 'error' })
}
```
无区分网络错误和服务端错误。
### 建议
1. `request.ts` 增强:
- 请求前调用 `wx.getNetworkType()` 检测网络状态,无网络时直接提示
- 添加请求超时配置(建议 15s
- 非 401 网络错误自动重试 1 次(指数退避)
- 统一的网络错误 toast`wx.showToast({ title: '网络连接失败', icon: 'none' })`
2. `app.ts` 中注册 `wx.onNetworkStatusChange` 全局监听,断网时显示顶部提示条
3. 页面级错误状态区分"网络错误"和"服务端错误",显示不同的提示文案
4. 可选:添加离线缓存(`wx.setStorageSync` 缓存上次成功的列表数据)

View File

@@ -0,0 +1,57 @@
# P6→NS1/RNS1 缺失项 #15任务卡片的长按/滑动操作
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟡 低
- 长按操作已完整实现(上下文菜单含置顶/备注/AI助手/放弃/取消放弃),但滑动操作(如滑动删除、滑动置顶)未实现。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml`
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts`
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxss`
### 发现
#### 已实现:长按操作
1. 所有任务卡片(置顶/一般/已放弃)均绑定了 `bindlongpress="onTaskLongPress"`
2. 长按后弹出上下文菜单(`.ctx-menu`),菜单项包括:
- 📌 置顶/取消置顶(`onCtxPin`
- 📝 备注(`onCtxNote`
- 🤖 问问AI助手`onCtxAI`
- 🗑️ 放弃任务(`onCtxAbandon`
- ↩️ 取消放弃(已放弃任务专属,`onCtxCancelAbandon`
3. 菜单定位跟随手指触摸位置,有边界检测防止溢出屏幕
#### 未实现:滑动操作
1.`bindtouchmove`/`bindtouchstart`/`bindtouchend` 事件
2. 无滑动删除swipe-to-deleteUI
3. 无滑动置顶交互
4.`<t-swipe-cell>` 组件使用
### 证据
task-list.wxml 中卡片事件绑定(有 longpress无 touchmove
```xml
<view class="task-card ..."
bindtap="onTaskTap" bindlongpress="onTaskLongPress">
```
task-list.ts 中长按处理完整实现:
```typescript
onTaskLongPress(e: WechatMiniprogram.TouchEvent) {
this._longPressed = true
// ... 获取目标任务、计算菜单位置、显示上下文菜单
this.setData({
contextMenuVisible: true,
contextMenuX: x, contextMenuY: y,
contextMenuTarget: target,
})
}
```
### 建议
1. 考虑是否真正需要滑动操作 — 长按菜单已覆盖所有操作场景,滑动操作可能增加交互复杂度
2. 如需实现,推荐使用 TDesign 的 `<t-swipe-cell>` 组件包裹任务卡片
3. 滑动操作建议仅暴露最常用的 1-2 个操作(如置顶、放弃),避免操作过载
4. 优先级较低,可作为后续体验优化迭代

View File

@@ -0,0 +1,58 @@
# P6→NS1/RNS1 缺失项 #16页面切换时的转场动画规范
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 无自定义转场动画配置完全依赖微信小程序默认的页面切换动画。router.ts 仅封装了 wx.navigateTo/switchTab/navigateBack无动画参数。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/utils/router.ts`
- `apps/miniprogram/miniprogram/app.json`window 配置)
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts`(页面跳转调用)
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.ts`(页面跳转调用)
### 发现
1. `router.ts` 仅封装了三个基础路由方法(`navigateTo``switchTab``navigateBack`),无 `routeType`/`animationType`/`animationDuration` 参数
2. `app.json``window` 配置仅设置了导航栏样式,无 `pageOrientation``animationType` 等动画配置
3. 页面跳转直接调用 `wx.navigateTo({ url: ... })`,未使用 `routeType` 参数
4. 无页面进入/退出的自定义 CSS 动画
5.`wx.navigateTo``routeType` 参数(微信基础库 2.29.2+ 支持)
### 证据
router.ts 完整内容(无动画配置):
```typescript
export function navigateTo(url: string): void {
wx.navigateTo({ url })
}
export function switchTab(url: string): void {
wx.switchTab({ url })
}
export function navigateBack(delta: number = 1): void {
wx.navigateBack({ delta })
}
```
app.json window 配置(无动画相关字段):
```json
"window": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "球房运营助手",
"navigationBarBackgroundColor": "#ffffff"
}
```
task-list.ts 中直接调用 wx.navigateTo
```typescript
wx.navigateTo({
url: `${DETAIL_ROUTE}?id=${id}`,
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
})
```
### 建议
1. 微信小程序默认转场动画(从右滑入/滑出)已满足基本体验,此项优先级较低
2. 如需自定义,可在 `router.ts``navigateTo` 中添加 `routeType` 参数(需基础库 2.29.2+
3. 可选方案:页面 `onLoad` 时添加入场 CSS 动画opacity + translateY 渐入),提升视觉流畅感
4. 建议作为 P13前端打磨的后续迭代项

View File

@@ -0,0 +1,58 @@
# P6→NS1/RNS1 缺失项 #17任务列表的批量操作
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 前端无多选模式、无批量操作 UI后端无批量操作接口。所有操作均为单任务粒度。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml`
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts`
- `apps/backend/app/routers/xcx_tasks.py`
### 发现
#### 前端
1. task-list.wxml 中无 checkbox/多选组件
2. task-list.ts 中无 `selectedTasks`/`isMultiSelect`/`batchMode` 等状态变量
3. 无"全选"/"批量标记完成"/"批量放弃"等操作按钮
4. 无编辑模式切换入口(如顶部"编辑"按钮)
#### 后端
1. `xcx_tasks.py` 中所有操作接口均为单任务粒度:
- `POST /{task_id}/pin`
- `POST /{task_id}/unpin`
- `POST /{task_id}/abandon`
- `POST /{task_id}/restore`
2. 无批量操作接口(如 `POST /batch/pin``POST /batch/abandon`
### 证据
后端路由清单(全部为单任务操作):
```python
@router.post("/{task_id}/pin") # 单个置顶
@router.post("/{task_id}/unpin") # 单个取消置顶
@router.post("/{task_id}/abandon") # 单个放弃
@router.post("/{task_id}/restore") # 单个恢复
```
task-list.ts data 中无批量相关字段:
```typescript
data: {
pageState: 'loading',
pinnedTasks: [],
normalTasks: [],
abandonedTasks: [],
taskCount: 0,
// ... 无 selectedTasks、batchMode 等
}
```
### 建议
1. 此功能优先级较低 — 当前任务列表规模(每日 10-30 条)下,单任务操作已满足需求
2. 如需实现,建议分步:
- 第一步:前端添加编辑模式(长按进入多选 → 底部浮出批量操作栏)
- 第二步:后端添加批量接口 `POST /api/xcx/tasks/batch` 接受 `task_ids` 数组 + `action` 枚举
3. 批量操作建议限制:单次最多选择 20 条,防止误操作
4. 建议在用户反馈确认需求后再实现,避免过度设计

View File

@@ -0,0 +1,62 @@
# P6→NS1/RNS1 缺失项 #18无障碍适配
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 项目自有页面task-list、task-detail 及自定义组件)中无任何 `aria-label``aria-role` 等无障碍属性。仅 TDesign 组件库内部自带了部分无障碍支持。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/task-list/task-list.wxml`
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.wxml`
- `apps/miniprogram/miniprogram/components/clue-card/clue-card.wxml`
- `apps/miniprogram/miniprogram/components/` 下所有自定义组件
- `apps/miniprogram/miniprogram/miniprogram_npm/tdesign-miniprogram/`(对比参考)
### 发现
1. 全局 grep `aria-label|aria-role|aria-hidden|role=` 在项目自有 wxml 文件中结果为零
2. 仅 TDesign 组件库(`miniprogram_npm/tdesign-miniprogram/`)内部使用了无障碍属性:
- `swiper-nav.wxml``aria-role="button" aria-label="上一张/下一张"`
- `tabs.wxml``aria-role="tablist"`
- `upload.wxml``aria-role="presentation"` + 动态 `aria-label`
3. task-list.wxml 中的交互元素缺少无障碍标注:
- 任务卡片无 `aria-role="button"``aria-label`
- 重试按钮无 `aria-role="button"`
- 上下文菜单项无 `aria-label`
4. task-detail.wxml 中同样缺失:
- 底部操作栏按钮无 `aria-label`
- 手机号查看/复制按钮无 `aria-label`
- 话术复制按钮无 `aria-label`
### 证据
task-list.wxml 中任务卡片(无无障碍属性):
```xml
<view class="task-card ..."
hover-class="task-card--hover" hover-stay-time="100"
data-id="{{item.id}}" data-tasktype="{{item.taskType}}"
bindtap="onTaskTap" bindlongpress="onTaskLongPress">
```
task-detail.wxml 中底部操作栏(无无障碍属性):
```xml
<view class="btn-ask" bindtap="onAskAssistant" hover-class="btn-ask--hover">
<t-icon name="chat" size="36rpx" color="#ffffff" />
<text class="btn-text">问问助手</text>
</view>
```
对比 TDesign 组件(有无障碍属性):
```xml
<view ... aria-role="button" aria-label="上一张"/>
```
### 建议
1. 为所有可交互元素添加 `aria-role``aria-label`
- 任务卡片:`aria-role="button" aria-label="{{item.customerName}} {{item.taskTypeLabel}}"`
- 操作按钮:`aria-role="button" aria-label="问问助手"` / `aria-label="添加备注"`
- 上下文菜单项:`aria-role="menuitem" aria-label="置顶"`
2. 为非交互的装饰性元素添加 `aria-hidden="true"`(如 banner 背景图、调试面板)
3. 确保焦点顺序合理banner → 任务列表 → 底部操作栏
4. 建议创建一个无障碍适配 checklist在后续页面开发中统一执行
5. 优先级较低,可作为 P13 前端打磨的后续迭代

View File

@@ -0,0 +1,57 @@
# P7→NS1/RNS1 缺失项 #1营业日 08:00 分割点的完整处理规范
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- ETL 层已通过 `biz_date_sql_expr()` 统一实现 08:00 营业日分割DWS 表中的 biz_date/stat_date 字段已按此规则生成;后端查询使用 `create_time` 按自然月过滤(非 biz_date但因 DWS 层已预聚合,实际数据口径一致。
## 详细审查
### 审查范围
- `packages/shared/src/neozqyy_shared/datetime_utils.py``biz_date_sql_expr()` 函数
- `apps/etl/connectors/feiqiu/tasks/dws/assistant_customer_task.py` — DWS 客户统计 ETL
- `apps/etl/connectors/feiqiu/tasks/dws/member_visit_task.py` — 会员到店 ETL
- `apps/etl/connectors/feiqiu/tasks/dws/finance_discount_task.py` — 财务折扣 ETL
- `apps/backend/app/services/fdw_queries.py` — 后端 FDW 查询
- `apps/backend/app/services/performance_service.py` — 绩效服务
### 发现
1. **ETL 层已完整实现 08:00 分割**
- `biz_date_sql_expr(col, day_start_hour=8)` 生成 `DATE(col - INTERVAL '8 hours')` SQL 表达式
- 所有 DWS 任务assistant_customer_task、member_visit_task、finance_discount_task、goods_stock_daily_task、assistant_project_tag_task均调用此函数
- `cutoff` 值从配置 `app.business_day_start_hour` 读取,默认 8
2. **Python 层也有对应函数**
- `business_date(dt, day_start_hour=8)` — 将时间戳归属到营业日
- `business_month(dt, day_start_hour=8)` — 将时间戳归属到营业月
- `business_day_range(biz_date)` — 返回营业日精确时间戳范围 `[当天08:00, 次日08:00)`
3. **后端查询层**
- `get_service_records()` 使用 `create_time >= start_date AND create_time < end_date` 按自然月过滤
- `get_salary_calc()` 使用 `salary_month` 字段DWS 预聚合,已按营业日口径)
- 服务记录明细查询按自然月时间戳过滤,与 P7 定义的"当月1日 08:00 ~ 次月1日 08:00"存在微小差异(差 8 小时),但实际影响极小
### 证据
```python
# packages/shared/src/neozqyy_shared/datetime_utils.py
def biz_date_sql_expr(col: str, day_start_hour: int = 8) -> str:
return f"DATE({col} - INTERVAL '{day_start_hour} hours')"
# assistant_customer_task.py — DWS 层使用
cutoff = self.config.get("app.business_day_start_hour", 8)
biz_expr = biz_date_sql_expr("start_use_time", cutoff)
# → DATE(start_use_time - INTERVAL '8 hours') AS service_date
```
```python
# fdw_queries.py — 后端查询(按自然月,非 biz_date
start_date = f"{year}-{month:02d}-01"
end_date = f"{year}-{month + 1:02d}-01"
# WHERE sl.create_time >= start_date AND sl.create_time < end_date
```
### 建议(微调项)
- 后端 `get_service_records()` 的月份过滤可考虑使用 `business_month_range()` 生成 `[当月1日 08:00, 次月1日 08:00)` 范围,与 ETL 层 biz_date 口径完全对齐
- 当前差异仅影响每月 1 日 00:00-08:00 之间的少量记录归属,风险极低

View File

@@ -0,0 +1,63 @@
# P7→NS1/RNS1 缺失项 #2"预估"标记的判断逻辑
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 前端已实现"当月 = 预估"的判断逻辑并展示预估标签,但后端 `is_estimate` 字段硬编码为 `False`未实现真正的预估判断。当前方案是纯前端判断year/month == 当前年月),未考虑 ETL 数据更新延迟等场景。
## 详细审查
### 审查范围
- `apps/backend/app/services/fdw_queries.py``get_service_records()``is_estimate` 字段
- `apps/backend/app/schemas/xcx_performance.py` — PERF-1/PERF-2 响应 schema
- `apps/miniprogram/miniprogram/pages/performance/performance.wxml` — 预估标签展示
- `apps/miniprogram/miniprogram/pages/performance/performance.ts``isCurrentMonth` 判断
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.wxml` — 预估标签展示
### 发现
1. **后端 `is_estimate` 硬编码为 `False`**
- `fdw_queries.get_service_records()` 第 405 行注释明确写道:`# is_estimate 不存在于视图中,默认 False`
- `PerformanceOverviewResponse` schema 中没有 `is_estimate` 字段
- `RecordsSummary` schema 中也没有 `is_estimate` 字段
2. **前端使用纯客户端判断**
- `performance.ts` 中:`const isCurrentMonth = year === nowYear && month === nowMonth`
- `performance-records.ts` 中:同样的 `isCurrentMonth` 判断
- WXML 中根据 `isCurrentMonth` 展示"预估"标签和"我的预估收入"文案
3. **前端展示已到位**
- `performance.wxml``<text wx:if="{{isCurrentMonth}}" class="estimate-tag">预估</text>`
- 收入标签:`{{isCurrentMonth ? '我的预估收入' : '我的收入'}}`
- 合计标签:`本月合计<text wx:if="{{isCurrentMonth}}"> 预估</text>`
- `performance-records.wxml`:统计概览中 `<text class="stat-hint" wx:if="{{isCurrentMonth}}">预估</text>`
### 证据
```python
# fdw_queries.py — is_estimate 硬编码
records.append({
...
"income": float(row[10]) if row[10] is not None else 0.0,
# is_estimate 不存在于视图中,默认 False
"is_estimate": False,
})
```
```typescript
// performance.ts — 纯前端判断
const isCurrentMonth = year === nowYear && month === nowMonth
```
```xml
<!-- performance.wxml — 预估标签展示 -->
<text wx:if="{{isCurrentMonth}}" class="estimate-tag">预估</text>
<text class="income-label">{{isCurrentMonth ? '我的预估收入' : '我的收入'}}</text>
```
### 建议
1. **明确"预估"的业务定义**P7 AC7 要求"当月数据显示预估标记",当前前端的"当月 = 预估"实现基本满足此需求,但需确认:
- 是否所有当月数据都算预估?还是仅未结算的记录?
- 月末 ETL 完成最终计算后,当月数据是否仍标记为预估?
2. **后端应提供 `is_estimate` 字段**:即使当前逻辑是"当月 = 预估",也应由后端返回此标记,避免前后端判断逻辑不一致
3. **单条记录级别的预估标记**`is_estimate` 字段已在 `xcx_tasks.py``xcx_customers.py` 的 schema 中定义,但在绩效 schema 中缺失

View File

@@ -0,0 +1,73 @@
# P7→NS1/RNS1 缺失项 #3定档折算惩罚的展示格式
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 后端已返回 `service_hours`(折算后)和 `service_hours_raw`(折算前)两个字段,前端 performance-records 页面已实现折前/折后对比展示DISPLAY-STANDARDS.md 已定义课时展示规范。但 P7 AC6 要求的"120分钟定档折算30分钟"格式未被采用,实际使用的是"2.0h(折后 2.5h"格式,且 performance 概览页未展示折算信息。
## 详细审查
### 审查范围
- `apps/backend/app/services/performance_service.py``compute_summary()``group_records_by_date()`
- `apps/backend/app/schemas/xcx_performance.py``RecordsSummary` 中的 `total_hours_raw`
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.wxml` — 折算展示
- `docs/miniprogram-dev/design-system/DISPLAY-STANDARDS.md` — §2 课时展示规范
### 发现
1. **后端数据已完整**
- `fdw_queries.get_service_records()` 返回 `service_hours``income_seconds / 3600.0`)和 `service_hours_raw``real_use_seconds / 3600.0`
- `compute_summary()` 计算 `total_hours``total_hours_raw`
- `RecordsSummary` schema 包含 `total_hours: float``total_hours_raw: float`
2. **performance-records 页面已实现折算展示**
- WXML 中:`<text class="record-hours-deduct" wx:if="{{rec.hoursRaw && rec.hoursRaw !== rec.hours}}">折前 {{fmt.hours(rec.hoursRaw)}}</text>`
- 统计概览中:`<text class="stat-hours-raw" wx:if="{{totalHoursRawLabel}}">折前 {{totalHoursRawLabel}}</text>`
- 格式为"折前 Nh"而非 P7 要求的"120分钟定档折算30分钟"
3. **DISPLAY-STANDARDS.md 已定义规范**
- §2.1 规则总表:`带折算备注 | 实际h折后 原始h | 2.0h(折后 2.5h`
- §2.3 折算标注字段约定:`hours`(折算后)、`hoursRaw`(折算前),仅当两者不同时展示括号备注
4. **performance 概览页未展示折算**
- `group_records_by_date()` 中的 `record_item` 只有 `hours` 字段,没有 `hoursRaw`
- 概览页 WXML 中服务记录只展示 `rec.hours`,无折算信息
5. **格式差异**
- P7 AC6 要求:"120分钟定档折算30分钟"(分钟单位,括号内说明折算量)
- 实际实现:"2.0h(折后 2.5h"(小时单位,括号内展示折前原始值)
- performance-records 实际使用:"折前 2.5h"(无括号,前缀"折前"
### 证据
```python
# performance_service.py — compute_summary 包含 total_hours_raw
def compute_summary(records: list[dict]) -> dict:
total_hours = sum(r.get("service_hours", 0.0) for r in records)
total_hours_raw = sum(r.get("service_hours_raw", 0.0) for r in records)
return {
"total_count": len(records),
"total_hours": round(total_hours, 2),
"total_hours_raw": round(total_hours_raw, 2),
"total_income": round(total_income, 2),
}
```
```xml
<!-- performance-records.wxml — 折算展示 -->
<text class="record-hours">{{fmt.hours(rec.hours)}}</text>
<text class="record-hours-deduct"
wx:if="{{rec.hoursRaw && rec.hoursRaw !== rec.hours}}">
折前 {{fmt.hours(rec.hoursRaw)}}
</text>
```
```markdown
<!-- DISPLAY-STANDARDS.md §2.1 -->
| 带折算备注 | `实际h折后 原始h` | `2.0h(折后 2.5h` |
```
### 建议
1. **统一展示格式**:当前 performance-records 页面使用"折前 Xh"格式,与 DISPLAY-STANDARDS.md 定义的"实际h折后 原始h"格式不一致,需统一
2. **确认是否采用 P7 的分钟格式**P7 要求"120分钟定档折算30分钟",但设计规范和实际实现均使用小时单位,需与产品确认最终格式
3. **performance 概览页补充折算信息**`group_records_by_date()` 应在 `record_item` 中加入 `hours_raw` 字段

View File

@@ -0,0 +1,81 @@
# P7→NS1/RNS1 缺失项 #4"我的新客"筛选逻辑的完整定义
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 后端已实现新客筛选逻辑,但采用的是"本月有服务 + 历史无记录"的简化定义,与 P7 AC3 定义的"首次服务 + 2月内 + 服务次数≤2"条件不完全一致。未使用 `dws_assistant_customer_stats` 表(该表已在 ETL 层建好),而是直接查询 DWD 层视图。
## 详细审查
### 审查范围
- `apps/backend/app/services/performance_service.py``_build_customer_lists()` 函数
- `apps/backend/app/services/fdw_queries.py` — FDW 查询
- `apps/etl/connectors/feiqiu/tasks/dws/assistant_customer_task.py` — DWS 客户统计 ETL
- `docs/database/ddl/etl_feiqiu__dws.sql``dws_assistant_customer_stats` 表结构
- `apps/miniprogram/miniprogram/pages/performance/performance.wxml` — 新客列表展示
### 发现
1. **后端实现的新客定义**
- `_build_customer_lists()` 中新客判断:`if mid not in historical_members`
- 历史查询:`WHERE create_time < 本月1日 AND tenant_member_id = ANY(本月服务过的会员)`
- 即:**本月有服务记录 + 本月之前从未有过服务记录** = 新客
- 未检查"2月内"和"服务次数≤2"条件
2. **P7 AC3 的完整定义**
- 首次服务first_service_date 在本月)
- 2月内首次服务距今不超过 2 个月)
- 服务次数 ≤ 2
3. **DWS 层已有更完整的数据**
- `dws_assistant_customer_stats` 表包含 `first_service_date``last_service_date``total_service_count` 等字段
- ETL 任务 `AssistantCustomerTask` 已按 `biz_date_sql_expr` 计算营业日归属
-`app.v_dws_assistant_customer_stats` RLS 视图可供后端查询
- 但后端 `fdw_queries.py` 中未使用此视图
4. **前端展示已到位**
- 新客列表展示:姓名、头像、最近服务日期、服务次数
- `<text class="customer-detail">最近服务: {{item.lastService}} · {{item.count}}次</text>`
### 证据
```python
# performance_service.py — 新客判断逻辑
# 查询历史记录(本月之前是否有服务记录)
try:
start_date = f"{year}-{month:02d}-01"
with fdw_queries._fdw_context(conn, site_id) as cur:
cur.execute("""
SELECT DISTINCT tenant_member_id
FROM app.v_dwd_assistant_service_log
WHERE site_assistant_id = %s
AND is_delete = 0
AND create_time < %s::timestamptz
AND tenant_member_id = ANY(%s)
""", (assistant_id, start_date, member_ids))
for row in cur.fetchall():
historical_members.add(row[0])
# 新客:历史无记录
if mid not in historical_members:
new_customers.append({...})
```
```sql
-- dws_assistant_customer_stats 表结构(已存在但未被后端使用)
CREATE TABLE dws.dws_assistant_customer_stats (
id bigint NOT NULL,
site_id bigint NOT NULL,
tenant_id bigint NOT NULL,
-- ... 包含 first_service_date, last_service_date, total_service_count 等
-- 唯一约束: (site_id, assistant_id, member_id, stat_date)
);
```
### 建议
1. **对齐 P7 AC3 的完整新客定义**:当前"历史无记录"的判断过于宽松,应补充:
- `first_service_date` 在本月范围内
- `total_service_count <= 2`(或根据业务确认阈值)
- "2月内"条件在当前"当月查询"场景下自然满足,但跨月查看时需考虑
2. **使用 `dws_assistant_customer_stats` 表**:该表已有 `first_service_date``total_service_count` 等预聚合字段,比直接查 DWD 层更高效且口径更准确
3. **确认新客定义的业务边界**:与产品确认"首次服务"是指该助教的首次服务还是全店首次服务

View File

@@ -0,0 +1,74 @@
# P7→NS1/RNS1 缺失项 #5"我的常客"的展示字段
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 后端已在 PERF-1 响应中返回常客列表,包含 P7 AC4 要求的次数、小时数、收入合计三个核心字段。前端已完整展示。Schema 中 `RegularCustomer` 模型字段齐全。
## 详细审查
### 审查范围
- `apps/backend/app/services/performance_service.py``_build_customer_lists()` 常客构建逻辑
- `apps/backend/app/schemas/xcx_performance.py``RegularCustomer` 模型
- `apps/miniprogram/miniprogram/pages/performance/performance.wxml` — 常客列表展示
### 发现
1. **后端常客字段完整**
- `_build_customer_lists()` 中常客判断:`if stats["count"] >= 2`(本月服务次数 ≥ 2
- 返回字段:`name``avatar_char``avatar_color``hours`(总小时数)、`income`(收入合计,格式 `¥N,NNN.NN`)、`count`(服务次数)
- 按收入倒序排列
2. **Schema 定义完整**
- `RegularCustomer(CustomerSummary)` 包含:`hours: float``income: str``count: int`
- `PerformanceOverviewResponse` 包含 `regular_customers: list[RegularCustomer]`
3. **前端展示完整**
- WXML`<text class="customer-detail">{{item.count}}次 · {{fmt.hours(item.hours)}} · {{fmt.safe(item.income)}}</text>`
- 展示格式:`3次 · 4.5h · ¥1,200`
- 支持展开/收起(默认显示 5 条,可展开至 20 条)
4. **P7 AC4 要求对照**
- ✅ 次数 → `count` 字段
- ✅ 小时数 → `hours` 字段
- ✅ 工资合计 → `income` 字段(注:实际为收入合计,非"工资",语义更准确)
### 证据
```python
# performance_service.py — 常客构建
if stats["count"] >= 2:
regular_customers.append({
"name": name,
"avatar_char": char,
"avatar_color": color,
"hours": round(stats["total_hours"], 2),
"income": f"¥{stats['total_income']:,.2f}",
"count": stats["count"],
})
# 按收入倒序
regular_customers.sort(
key=lambda x: float(x.get("income", "¥0").replace("¥", "").replace(",", "")),
reverse=True,
)
```
```python
# xcx_performance.py — Schema
class RegularCustomer(CustomerSummary):
"""常客。"""
hours: float
income: str
count: int
```
```xml
<!-- performance.wxml — 常客展示 -->
<text class="customer-detail">
{{item.count}}次 · {{fmt.hours(item.hours)}} · {{fmt.safe(item.income)}}
</text>
```
### 建议(微调项)
- 常客阈值 `count >= 2` 当前硬编码,可考虑从配置表读取(遵循 feiqiu-data-rules 规则 6
- `income` 字段在后端格式化为字符串(`¥N,NNN.NN`),与 DISPLAY-STANDARDS.md 的金额规范(`¥N,NNN` 无小数)略有差异,建议统一

View File

@@ -0,0 +1,106 @@
# P7→NS1/RNS1 缺失项 #6收入与业绩档位卡片的视觉设计
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 前端已完整实现收入卡片、档位卡片(当前/下一阶段)、升级提示、进度条组件的视觉设计。`perf-progress-bar``metric-card` 组件均已开发完成WXSS 中包含完整的渐变、动画、布局样式。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/performance/performance.wxml` — 收入卡片和档位卡片布局
- `apps/miniprogram/miniprogram/pages/performance/performance.wxss` — 视觉样式
- `apps/miniprogram/miniprogram/components/perf-progress-bar/` — 进度条组件
- `apps/miniprogram/miniprogram/components/metric-card/` — 指标卡片组件
### 发现
1. **收入概览卡片**Banner 区域):
- 双卡片布局:`income-overview` flex 容器,两个 `income-card`
- 左卡片:"我的预估收入"/"我的收入" + 金额44rpx 粗体白色)
- 右卡片:"上月收入" + 金额(绿色高亮 `#a7f3d0`
- 毛玻璃效果:`backdrop-filter: blur(4px)`,半透明白色背景
- SVG 渐变底图:`banner-bg-blue-light-aurora.svg`
2. **档位卡片**(收入情况 Section
- 当前档位:绿色渐变背景(`#f0fdf4 → #dcfce7`),绿色边框,绿色 badge
- 下一阶段:黄色渐变背景(`#fefce8 → #fef9c3`),黄色边框,黄色 badge
- 每个档位卡片展示emoji 图标 + 档位标签 + 基础课费率 + 激励课费率
- 费率展示:`{rate}元/h` 格式,带分隔线
3. **升级提示卡片**
- 蓝色渐变背景(`#eff6ff → #eef2ff`
- 左侧:⏱️ emoji + "距离下一阶段" + "需完成 X 小时"
- 右侧:橙色渐变按钮 "到达即得 X元"
4. **perf-progress-bar 组件**
- 渐变填充条 + 高光动画 + 导火索火星效果
- 支持动态刻度(`ticks` 数组从接口传入,不硬编码)
- 档位高亮:`currentTier` 控制刻度高亮状态
- 动画:`shine`(高光扫过)+ `spark`火花粒子6 个粒子)
5. **metric-card 组件**
- 通用指标卡片:标题 + 数值 + 单位 + 趋势标签
- 趋势支持up绿色箭头/ down红色箭头/ flat横线
- 帮助图标:可选 `helpText`,点击触发 `helpTap` 事件
- 空值处理:`null/undefined → '--'`
6. **前端仍使用 mock 数据**
- `performance.ts``loadData()` 使用 `setTimeout` + 空骨架数据
- 注释标注 `// TODO: 替换为真实 API — 已清空为骨架项`
- 视觉组件和布局已完成,但未接入真实 API
### 证据
```xml
<!-- performance.wxml — 档位卡片 -->
<view class="tier-card tier-current">
<view class="tier-badge badge-current">当前档位</view>
<view class="tier-row">
<view class="tier-icon-label">
<text class="tier-emoji">📊</text>
<text class="tier-label tier-label-green">当前档位</text>
</view>
<view class="tier-rates">
<view class="rate-item">
<text class="rate-value rate-green">{{currentTier.basicRate}}</text>
<text class="rate-unit rate-green-light">元/h</text>
<text class="rate-desc rate-green-light">基础课到手</text>
</view>
<!-- ... 激励课费率 ... -->
</view>
</view>
</view>
```
```css
/* performance.wxss — 档位卡片样式 */
.tier-current {
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
border: 2rpx solid #86efac;
}
.tier-next {
background: linear-gradient(135deg, #fefce8 0%, #fef9c3 100%);
border: 2rpx solid #fde047;
}
.badge-current {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
}
```
```typescript
// perf-progress-bar — 组件属性
properties: {
filledPct: { type: Number, value: 0 }, // 进度百分比
clampedSparkPct: { type: Number, value: 0 }, // 火星位置
currentTier: { type: Number, value: 0 }, // 当前档位
ticks: { type: Array, value: [] }, // 刻度数组(接口传入)
shineRunning: { type: Boolean, value: false },
sparkRunning: { type: Boolean, value: false },
}
```
### 建议(微调项)
- performance 页面尚未使用 `perf-progress-bar` 组件WXML 中未引用),需在联调时集成
- performance 页面尚未使用 `metric-card` 组件,当前收入展示是内联实现
- 前端 mock 数据需替换为真实 API 调用(已有 TODO 标注)

View File

@@ -0,0 +1,58 @@
# P7→NS1/RNS1 缺失项 #7服务记录按天归总的展示格式
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 后端 DateGroup 结构完整,日期标签格式为 `M月D日`(如"3月15日"但缺少星期信息P7 定义的"3月15日 周五"格式);前端直接透传后端 `date` 字段(`YYYY-MM-DD` 格式),未做二次格式化。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_performance.py` — DateGroup / DateGroupRecord 模型
- `apps/backend/app/services/performance_service.py``group_records_by_date()``_format_date_label()`
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.wxml` — 日期标签渲染
- `apps/miniprogram/miniprogram/pages/performance/performance.wxml` — 日期标签渲染
- `docs/miniprogram-dev/design-system/DATETIME-DISPLAY-STANDARD.md` — 日期展示规范
### 发现
1. **后端 DateGroup 结构已定义且完整**`DateGroup` 包含 `date``total_hours``total_income``records` 四个字段,`DateGroupRecord` 包含客户名、时间范围、课时、课程类型、地点、收入等完整字段。
2. **后端 `_format_date_label()` 格式为 `M月D日`**:该函数输出如 `3月15日`,但不包含星期信息。然而实际上 `group_records_by_date()``date` 字段使用的是 `YYYY-MM-DD` 格式的 `date_key`,并未调用 `_format_date_label()`
3. **`_format_date_label()` 未被 `group_records_by_date()` 使用**`group_records_by_date()` 直接将 `date_key``YYYY-MM-DD`)赋值给 `date` 字段,`_format_date_label()` 函数虽然存在但未在 DateGroup 构建中被调用。
4. **前端直接渲染后端返回的 `date` 字段**WXML 中 `{{item.date}}` 直接展示,未做格式化。因此用户看到的是 `2026-03-15` 而非 `3月15日 周五`
5. **设计规范文档 DATETIME-DISPLAY-STANDARD.md 定义的是相对时间规范**(刚刚/N分钟前/N天前/日期),不涉及"M月D日 周X"这种绝对日期+星期的格式。
### 证据
后端 `group_records_by_date()` 中日期赋值performance_service.py:130
```python
result.append({
"date": date_key, # date_key = settle_time.strftime("%Y-%m-%d")
"total_hours": f"{total_hours:g}",
"total_income": f"{total_income:.2f}",
"records": recs,
})
```
后端 `_format_date_label()` 存在但未被 DateGroup 使用performance_service.py:188
```python
def _format_date_label(dt) -> str:
"""格式化日期为 "M月D日" 格式。"""
if hasattr(dt, "strftime"):
return f"{dt.month}{dt.day}"
```
前端直接渲染performance-records.wxml
```xml
<text decode class="dd-date">{{item.date}}&nbsp;&nbsp;</text>
```
### 建议
1. **后端**:在 `group_records_by_date()` 中将 `date` 字段改为调用 `_format_date_label()` 并追加星期信息,格式为 `M月D日 周X`(如"3月15日 周五")。或新增 `date_label` 字段保留格式化后的展示文本,`date` 保留 `YYYY-MM-DD` 用于排序。
2. **前端**:如果后端不改,前端可在 WXS 中增加日期格式化函数,将 `YYYY-MM-DD` 转为 `M月D日 周X`
3. **设计规范**:在 DATETIME-DISPLAY-STANDARD.md 中补充"按天归总场景"的日期标签格式规范(`M月D日 周X`)。

View File

@@ -0,0 +1,73 @@
# P7→NS1/RNS1 缺失项 #8本月/上月切换的交互细节
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- performance-records 页面已实现月份切换(含 loading 状态和数据刷新),但 performance 主页面尚未实现月份切换功能(仅展示当月数据,使用 mock 骨架数据)。两个页面均无切换动画。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/performance/performance.ts` — 绩效主页面逻辑
- `apps/miniprogram/miniprogram/pages/performance/performance.wxml` — 绩效主页面模板
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.ts` — 业绩明细页逻辑
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.wxml` — 业绩明细页模板
- `apps/backend/app/routers/xcx_performance.py` — 后端路由参数
### 发现
1. **performance-records 页面月份切换已实现**
-`switchMonth()` 方法,支持 prev/next 方向切换
-`canGoPrev`/`canGoNext` 状态控制按钮可用性
- 切换后调用 `loadData()` 重新请求数据
-`pageState: 'loading'` 状态展示 loading 浮层
- 不能超过当前月(`canGoNext` 逻辑正确)
2. **performance 主页面未实现月份切换**
- `loadData()` 中硬编码 `year = nowYear, month = nowMonth`,仅展示当月
- WXML 中无月份切换 UI 组件
- 数据仍使用 `setTimeout` + mock 骨架数据,未接入真实 API
-`TODO: 联调时从接口参数或页面参数获取 year/month` 注释
3. **后端 API 已支持月份参数**PERF-1 和 PERF-2 均接受 `year`/`month` 查询参数。
4. **无切换动画**:两个页面的月份切换均为即时替换,无过渡动画效果。
### 证据
performance.ts 中硬编码当月Line 87-89
```typescript
// TODO: 联调时从接口参数或页面参数获取 year/month
const year = nowYear
const month = nowMonth
```
performance-records.ts 中月份切换实现switchMonth 方法):
```typescript
switchMonth(e: WechatMiniprogram.TouchEvent) {
const direction = e.currentTarget.dataset.direction as 'prev' | 'next'
let { currentYear, currentMonth } = this.data
// ... 月份加减逻辑 ...
this.setData({ currentYear, currentMonth, monthLabel, canGoNext, canGoPrev, isCurrentMonth })
this.loadData()
}
```
performance-records.wxml 中月份切换 UI
```xml
<view class="month-switcher">
<view class="month-btn {{canGoPrev ? '' : 'month-btn-disabled'}}" data-direction="prev" bindtap="switchMonth">
<t-icon name="chevron-left" size="32rpx" />
</view>
<text class="month-label">{{monthLabel}}</text>
<view class="month-btn {{canGoNext ? '' : 'month-btn-disabled'}}" data-direction="next" bindtap="switchMonth">
<t-icon name="chevron-right" size="32rpx" />
</view>
</view>
```
### 建议
1. **performance 主页面**:完成 API 联调,移除 mock 数据,增加月份切换 UI 和逻辑(可复用 performance-records 的 `switchMonth` 模式)。
2. **切换动画**P7 AC2 提到的切换动画可作为低优先级优化,当前 loading 浮层已提供基本的状态反馈。
3. **数据刷新策略**:切换月份时已有 loading → 请求 → 渲染的完整流程,满足基本需求。可考虑增加本地缓存避免重复请求已加载过的月份。

View File

@@ -0,0 +1,61 @@
# P7→NS1/RNS1 缺失项 #9绩效页面的空状态
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- performance 主页面和 performance-records 页面均已实现空状态处理,包含空态图标、文案和错误重试。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/performance/performance.ts` — pageState 状态管理
- `apps/miniprogram/miniprogram/pages/performance/performance.wxml` — 空态 UI
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.ts` — pageState 状态管理
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.wxml` — 空态 UI
### 发现
1. **performance 主页面空状态已实现**
- `pageState` 支持 `'loading' | 'empty' | 'error' | 'normal'` 四种状态
- WXML 中有独立的空态区块:图标 `chart-bar` + 文案"暂无业绩数据"
- 有错误态区块:图标 `close-circle` + 文案"加载失败,请点击重试" + 重试按钮
2. **performance-records 页面空状态已实现**
- 同样支持四种 pageState
- 空态区块:图标 `chart-bar` + 文案"暂无数据"
- 错误态区块:图标 `close-circle` + 文案"加载失败,请点击重试" + 重试按钮
-`dateGroups.length === 0` 时自动切换到 `'empty'` 状态
3. **loading 态使用 toast 浮层**:不销毁内容,避免白屏闪烁。
### 证据
performance.wxml 空态区块:
```xml
<!-- 空数据态 -->
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
<t-icon name="chart-bar" size="40px" color="#dcdcdc" />
<text class="empty-text">暂无业绩数据</text>
</view>
```
performance-records.wxml 空态区块:
```xml
<!-- 空态 -->
<view class="page-empty" wx:if="{{pageState === 'empty'}}">
<t-icon name="chart-bar" size="120rpx" color="#dcdcdc" />
<text class="empty-text">暂无数据</text>
</view>
```
performance-records.ts 中空态判断:
```typescript
this.setData({
pageState: dateGroups.length > 0 ? 'normal' : 'empty',
// ...
})
```
### 建议
无需额外补充。空状态处理已覆盖新助教无数据场景。

View File

@@ -0,0 +1,49 @@
# P7→NS1/RNS1 缺失项 #10业绩明细的导出功能
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 后端无绩效数据导出接口,前端无导出按钮。现有的 Excel 相关接口仅用于租户管理后台的数据上传,与绩效导出无关。
## 详细审查
### 审查范围
- `apps/backend/app/routers/xcx_performance.py` — 绩效路由端点清单
- `apps/backend/app/routers/` — 全部路由文件搜索 export/导出/excel 关键词
- `apps/miniprogram/miniprogram/pages/performance/performance.wxml` — 导出按钮
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.wxml` — 导出按钮
### 发现
1. **后端无绩效导出接口**`xcx_performance.py` 仅定义两个端点:
- `GET /api/xcx/performance` — 绩效概览PERF-1
- `GET /api/xcx/performance/records` — 绩效明细PERF-2
-`/export` 或类似导出端点
2. **现有 Excel 接口与绩效无关**`tenant_excel.py` 提供的是租户管理后台的 Excel 上传/校验/写入功能(财务支出、团购收入、助教奖罚、充值业绩归属模板),不涉及绩效数据导出。
3. **前端无导出按钮**performance 和 performance-records 两个页面的 WXML 中均无导出相关的 UI 元素。
4. **P7 中"导出 Excel"为隐含需求**:原始 PRD 中提到但未作为核心功能明确定义NS1/RNS1 未将其纳入实现范围。
### 证据
后端路由完整端点清单xcx_performance.py 文件头注释):
```python
"""
端点清单:
- GET /api/xcx/performance — 绩效概览PERF-1
- GET /api/xcx/performance/records — 绩效明细PERF-2
"""
```
全局搜索 `export|导出|excel` 在后端路由中的结果:仅 `tenant_excel.py`(租户 Excel 上传)和 `env_config.py`(环境配置导出),无绩效相关导出。
### 建议
1. **评估优先级**:导出功能在 P7 中为隐含需求,非核心交互。建议在 MVP 阶段暂不实现,后续根据用户反馈决定是否补充。
2. **如需实现**
- 后端新增 `GET /api/xcx/performance/export?year=&month=` 端点,返回 Excel 文件openpyxl 生成)
- 前端在 performance-records 页面顶部或底部增加"导出本月明细"按钮
- 小程序端可通过 `wx.downloadFile` + `wx.openDocument` 实现文件下载和预览
3. **替代方案**可在管理后台admin-web而非小程序端提供导出功能降低小程序端复杂度。

View File

@@ -0,0 +1,75 @@
# P7→NS1/RNS1 缺失项 #11绩效数据的刷新频率说明
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟡 低
- 后端直接查询 ETL 库 DWS/DWD 视图(通过 FDW无应用层缓存数据新鲜度取决于 ETL 执行频率。ETL 为手动触发CLI无自动定时调度但代码中无数据新鲜度说明文档。
## 详细审查
### 审查范围
- `apps/backend/app/services/performance_service.py` — 数据来源和查询方式
- `apps/backend/app/services/fdw_queries.py` — FDW 查询函数(`get_salary_calc``get_service_records`
- `apps/etl/connectors/feiqiu/orchestration/` — 调度配置
- `apps/etl/connectors/feiqiu/cli/main.py` — CLI 入口
- `apps/etl/connectors/feiqiu/tasks/dws/assistant_salary_task.py` — DWS 薪资任务
### 发现
1. **后端无缓存,直接查 FDW 视图**
- `get_salary_calc()` 查询 `app.v_dws_assistant_salary_calc`DWS 层视图)
- `get_service_records()` 查询 `app.v_dwd_assistant_service_log`DWD 层视图)
- 两者均通过 FDWpostgres_fdw从 ETL 库只读访问
- 无 Redis/内存缓存层,每次请求直接查库
2. **ETL 为手动 CLI 触发,无自动定时调度**
- `cli/main.py` 提供 `--flow``--tasks` 等参数手动执行
- `orchestration/scheduler.py` 已标记为弃用(`ETLScheduler 已弃用`
- 无 cron/定时任务配置文件
- 无 Windows Task Scheduler 或 systemd timer 配置
3. **DWS 薪资表使用 delete-before-insert 策略**`assistant_salary_task.py` 中有 `_delete_by_month()` 方法,符合 DWS 层幂等策略。
4. **无数据新鲜度文档**NS1/RNS1 未定义数据刷新频率,前端也未展示"数据更新时间"提示。
### 证据
fdw_queries.py 中 `get_salary_calc()` 数据来源:
```python
cur.execute("""
SELECT salary_month, assistant_level_name, tier_id, ...
FROM app.v_dws_assistant_salary_calc
WHERE assistant_id = %s AND salary_month = %s::date
""", (assistant_id, calc_month))
```
fdw_queries.py 中 `get_service_records()` 数据来源:
```python
cur.execute("""
SELECT sl.assistant_service_id, dm.nickname AS customer_name, ...
FROM app.v_dwd_assistant_service_log sl
LEFT JOIN app.v_dim_member dm ON ...
WHERE sl.site_assistant_id = %s AND sl.is_delete = 0
AND sl.create_time >= %s::timestamptz
AND sl.create_time < %s::timestamptz
ORDER BY sl.create_time DESC
LIMIT %s OFFSET %s
""", ...)
```
ETL 调度器弃用标记orchestration/scheduler.py
```python
class ETLScheduler:
"""调度器薄包装层(已弃用)。"""
def __init__(self, config, logger):
warnings.warn("ETLScheduler 已弃用,请直接使用 TaskExecutor 和 FlowRunner", ...)
```
### 建议
1. **文档补充**:在 NS1 或运维文档中明确说明数据新鲜度策略:
- DWD 层服务记录ETL 执行后即时更新
- DWS 层薪资计算ETL 执行后 delete-before-insert 重算
- 当前为手动触发,建议说明推荐执行频率(如每日一次)
2. **前端提示**:可在绩效页面底部增加"数据更新于 YYYY-MM-DD HH:mm"提示(需后端返回 ETL 最后执行时间)。
3. **自动调度**:长期建议配置 Windows Task Scheduler 或 cron 定时执行 ETL flow确保数据每日刷新。

View File

@@ -0,0 +1,72 @@
# P7→NS1/RNS1 缺失项 #12业绩明细页的口径选择交互
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 后端 PERF-2 API 仅支持 `year`/`month` 参数(月口径),不支持周口径参数。前端 performance-records 页面仅有月份切换,无周口径切换 UI。
## 详细审查
### 审查范围
- `apps/backend/app/routers/xcx_performance.py` — PERF-2 端点参数定义
- `apps/backend/app/services/performance_service.py``get_records()` 函数签名
- `apps/backend/app/services/fdw_queries.py``get_service_records()` 查询条件
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.ts` — 口径切换逻辑
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.wxml` — 口径切换 UI
### 发现
1. **后端 PERF-2 仅支持月口径**
- 路由参数:`year: int, month: int, page: int, page_size: int`
-`period_type`(月/周)、`week_start`/`week_end` 等周口径参数
- `get_records()` 函数签名:`(user_id, site_id, year, month, page, page_size)`
2. **FDW 查询按月过滤**`get_service_records()` 使用 `create_time >= '{year}-{month:02d}-01'``create_time < '{year}-{month+1:02d}-01'` 作为时间范围,硬编码为自然月。
3. **前端无周口径切换**
- performance-records.ts 中仅有 `switchMonth()` 方法
- WXML 中仅有月份切换器(`month-switcher`),无"本周/上周"切换 UI
- 全局搜索 `week|周|weekly` 在后端绩效文件中无匹配
4. **P7 提到"本周/上周"口径**:原始 PRD 中定义了周维度的业绩查看,但 NS1/RNS1 仅实现了月维度。
### 证据
后端 PERF-2 路由定义xcx_performance.py
```python
@router.get("/records", response_model=PerformanceRecordsResponse)
async def get_performance_records(
year: int = Query(...),
month: int = Query(..., ge=1, le=12),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
user: CurrentUser = Depends(require_approved()),
):
```
FDW 查询时间范围fdw_queries.py:get_service_records
```python
start_date = f"{year}-{month:02d}-01"
if month == 12:
end_date = f"{year + 1}-01-01"
else:
end_date = f"{year}-{month + 1:02d}-01"
```
前端仅有月份切换performance-records.wxml
```xml
<view class="month-switcher">
<view class="month-btn" data-direction="prev" bindtap="switchMonth">...</view>
<text class="month-label">{{monthLabel}}</text>
<view class="month-btn" data-direction="next" bindtap="switchMonth">...</view>
</view>
```
### 建议
1. **评估必要性**:周口径在绩效场景中的实际使用频率较低(助教薪资按月结算),建议与产品确认是否为 MVP 必需。
2. **如需实现**
- 后端PERF-2 增加可选参数 `period_type: str = Query("month", regex="^(month|week)$")`,以及 `week_start: date | None``week_end: date | None`
- FDW 查询:`get_service_records()` 支持自定义时间范围(`start_date`/`end_date`)而非固定月份
- 前端:在月份切换器上方增加 Tab 切换("按月" / "按周"),按周模式下展示"本周/上周"切换器
3. **渐进方案**:可先在前端增加"自定义日期范围"筛选器,后端接受 `start_date`/`end_date` 参数,同时覆盖周口径和任意时间段需求。

View File

@@ -0,0 +1,80 @@
# P8→NS1/RNS1 缺失项 #1三看板 Tab 切换的缓存策略
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🔴 高
- 三看板 Tab 切换已实现,但采用页面跳转而非组件切换,切换回来时不保持筛选状态和滚动位置。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/app.json`tabBar 配置)
- `apps/miniprogram/miniprogram/custom-tab-bar/index.ts`(自定义 TabBar
- `apps/miniprogram/miniprogram/components/board-tab-bar/board-tab-bar.ts`(看板内 Tab 组件)
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts`(财务看板 Tab 切换)
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.ts`(助教看板 Tab 切换)
- `apps/miniprogram/miniprogram/pages/board-customer/board-customer.ts`(客户看板 Tab 切换)
### 发现
1. **Tab 切换方式:页面跳转,非组件内切换**
- `app.json` 中 tabBar 仅注册了 `board-finance` 为 tabBar 页面
- `board-customer``board-coach` 是普通页面(非 tabBar 页面)
-`board-finance` 切换到其他看板使用 `wx.navigateTo()`(页面栈压入)
-`board-coach`/`board-customer` 切换到 `board-finance` 使用 `wx.switchTab()`(清空页面栈)
2. **筛选状态不保持**
- 每个看板页面的筛选状态(`selectedSort``selectedDimension``selectedTime` 等)存储在 Page data 中
- 使用 `wx.navigateTo` 跳转时,离开的页面会被销毁(返回时重新 `onLoad`
- 使用 `wx.switchTab` 回到 `board-finance` 时,该页面会触发 `onShow` 但不会重新 `onLoad`tabBar 页面有缓存)
-`board-coach``board-customer` 作为非 tabBar 页面,每次进入都会重新创建
3. **滚动位置不保持**
- 三个看板页面均无滚动位置保存/恢复逻辑
- 页面重新加载后滚动位置归零
4. **切换动画**
- 未实现 P8 定义的切换动画
- 使用微信默认的页面跳转动画(右滑进入/左滑返回)
### 证据
```typescript
// board-finance.ts — 切换到其他看板
onTabChange(e: WechatMiniprogram.TouchEvent) {
const tab = e.currentTarget.dataset.tab as string
if (tab === 'customer') {
wx.navigateTo({ url: '/pages/board-customer/board-customer' })
} else if (tab === 'coach') {
wx.navigateTo({ url: '/pages/board-coach/board-coach' })
}
}
// board-coach.ts — 切换回财务看板
onTabChange(e: WechatMiniprogram.TouchEvent) {
const tab = e.currentTarget.dataset.tab as string
if (tab === 'finance') {
wx.switchTab({ url: '/pages/board-finance/board-finance' })
} else if (tab === 'customer') {
wx.navigateTo({ url: '/pages/board-customer/board-customer' })
}
}
```
```json
// app.json — 仅 board-finance 是 tabBar 页面
"tabBar": {
"custom": true,
"list": [
{ "pagePath": "pages/task-list/task-list", "text": "任务" },
{ "pagePath": "pages/board-finance/board-finance", "text": "看板" },
{ "pagePath": "pages/my-profile/my-profile", "text": "我的" }
]
}
```
### 建议
1. **方案 A推荐**:将筛选状态持久化到 `getApp().globalData``wx.setStorageSync`,页面 `onLoad` 时恢复
2. **方案 B**:将三个看板合并为一个页面,使用 `wx:if``hidden` 切换内容区域,天然保持状态
3. 滚动位置可通过 `onPageScroll` 记录 + `onLoad``wx.pageScrollTo` 恢复
4. 切换动画可通过 CSS transition 在顶部 Tab 区域实现高亮滑动效果

View File

@@ -0,0 +1,69 @@
# P8→NS1/RNS1 缺失项 #2财务看板分段加载策略
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🔴 高
- 后端 BOARD-3 API 一次返回全部 6 板块数据,无 sections 参数支持分段加载;前端仅赠送卡矩阵做了异步加载,其余板块为 mock 静态数据。
## 详细审查
### 审查范围
- `apps/backend/app/routers/xcx_board.py`BOARD-3 路由定义)
- `apps/backend/app/schemas/xcx_board.py`FinanceBoardResponse schema
- `apps/backend/app/services/board_service.py`get_finance_board 服务)
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts`(前端加载逻辑)
### 发现
1. **后端 API 不支持分段加载**
- BOARD-3 路由参数仅有 `time``area``compare`,无 `sections` 参数
- `FinanceBoardResponse` 一次性返回全部 6 个 Panel`overview``recharge``revenue``cashflow``expense``coach_analysis`
- `get_finance_board` 服务函数内部顺序构建所有板块数据,无条件跳过机制
2. **前端加载策略:大部分为 mock 静态数据**
- `board-finance.ts``overview``revenue``cashflow``expense``coachAnalysis` 的数据全部在 Page data 中以空字符串初始化,未调用 API
-`_loadGiftRows()` 方法通过 `fetchBoardFinance()` 加载了赠送卡矩阵(`recharge.giftRows`)和 AI 洞察数据
- 页面 `onLoad` 时直接设置 `pageState: 'normal'`,无整体数据加载流程
3. **唯一的区域条件过滤**
- `recharge` 板块在 `selectedArea !== 'all'` 时通过 `wx:if` 隐藏(前端条件渲染)
- 后端 schema 中 `recharge: RechargePanel | None` 支持 area≠all 时返回 null
### 证据
```python
# xcx_board.py — BOARD-3 路由,无 sections 参数
@router.get("/finance", response_model=FinanceBoardResponse)
async def get_finance_board(
time: FinanceTimeEnum = Query(default=FinanceTimeEnum.month),
area: AreaFilterEnum = Query(default=AreaFilterEnum.all),
compare: int = Query(default=0, ge=0, le=1),
user: CurrentUser = Depends(require_permission("view_board_finance")),
):
# FinanceBoardResponse — 一次返回全部 6 板块
class FinanceBoardResponse(CamelModel):
overview: OverviewPanel
recharge: RechargePanel | None
revenue: RevenuePanel
cashflow: CashflowPanel
expense: ExpensePanel
coach_analysis: CoachAnalysisPanel
```
```typescript
// board-finance.ts — 仅加载赠送卡数据,其余为 mock
async _loadGiftRows() {
const data = await fetchBoardFinance({
time: this.data.selectedTime,
area: this.data.selectedArea,
compare: this.data.compareEnabled ? 1 : 0,
})
// 仅处理 giftRows 和 aiInsights
}
```
### 建议
1. **后端**:为 BOARD-3 API 增加可选 `sections` 查询参数(如 `sections=overview,recharge`),服务层按需构建板块,未请求的板块返回 null
2. **前端**:实现分段加载策略——首次加载仅请求 `overview`,用户滚动到对应板块时再按需请求其余板块(利用 IntersectionObserver 或 onPageScroll 触发)
3. 当前 mock 数据阶段此问题影响不大,但联调前必须完成分段加载改造,否则 6 板块全量查询会导致首屏加载缓慢

View File

@@ -0,0 +1,50 @@
# P8→NS1/RNS1 缺失项 #3客户看板卡片点击跳转到 customer-detail
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 客户卡片已实现 bindtap 事件,点击后通过 wx.navigateTo 跳转到 customer-detail 页面并传递 id 参数。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/board-customer/board-customer.wxml`(卡片模板)
- `apps/miniprogram/miniprogram/pages/board-customer/board-customer.ts`(点击事件处理)
- `apps/miniprogram/miniprogram/app.json`customer-detail 页面注册)
### 发现
1. **WXML 模板中卡片绑定了 bindtap 事件**
- 每个 `customer-card` 元素绑定了 `bindtap="onCustomerTap"`
- 通过 `data-id="{{item.id}}"` 传递客户 ID
- 同时设置了 `hover-class="customer-card--hover"` 提供点击反馈
2. **TS 中实现了跳转逻辑**
- `onCustomerTap` 方法从事件中提取 `id`,使用 `wx.navigateTo` 跳转到 `customer-detail` 页面
- 跳转 URL 格式:`/pages/customer-detail/customer-detail?id=xxx`
3. **customer-detail 页面已注册**
- `app.json` 的 pages 数组中包含 `pages/customer-detail/customer-detail`
- `pages/customer-detail/` 目录存在
### 证据
```html
<!-- board-customer.wxml -->
<view
class="customer-card"
hover-class="customer-card--hover"
wx:for="{{customers}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="onCustomerTap"
>
```
```typescript
// board-customer.ts
onCustomerTap(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id as string
wx.navigateTo({ url: '/pages/customer-detail/customer-detail?id=' + id })
},
```

View File

@@ -0,0 +1,80 @@
# P8→NS1/RNS1 缺失项 #4助教看板的"距升档"进度条
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🔴 高
- 后端仅返回 `perfGap` 文本字段(如"距升档 13.8h"),无进度百分比数据;`perf-progress-bar` 组件已开发但未在看板页面中使用,看板仅以文字形式展示距升档信息。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_board.py`CoachBoardItem schema
- `apps/backend/app/services/board_service.py`get_coach_board 服务)
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.wxml`(看板模板)
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.ts`(看板逻辑)
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.json`(组件引用)
- `apps/miniprogram/miniprogram/components/perf-progress-bar/perf-progress-bar.ts`(进度条组件)
### 发现
1. **后端 Schema 无进度百分比字段**
- `CoachBoardItem` 中与升档相关的字段:
- `perf_hours: float` — 当前定档业绩小时数
- `perf_gap: str | None` — 文本描述(如"距升档 13.8h"
- `perf_reached: bool` — 是否已达标
- 缺少:`perf_pct`(进度百分比)、`perf_target`(目标小时数)、`perf_tier`(当前档位)等可视化所需字段
2. **perf-progress-bar 组件已开发,功能完整**
- 组件支持:`filledPct`(填充百分比)、`clampedSparkPct`(火星位置)、`currentTier`(当前档位 0~5`ticks`(刻度数组)
- 支持高光动画和火花动画
- 已在 `task-list``coach-detail` 页面中使用
3. **看板页面未引用进度条组件**
- `board-coach.json``usingComponents` 中无 `perf-progress-bar`
- `board-coach.wxml` 中升档信息仅以文字展示:
- 未达标:`<text class="bottom-right bottom-right--warning">{{item.perfGap}}</text>`
- 已达标:`<text class="bottom-right bottom-right--success">✅ 已达标</text>`
### 证据
```python
# CoachBoardItem — 仅有文本字段,无百分比
class CoachBoardItem(CamelModel):
perf_hours: float = 0.0
perf_hours_before: float | None = None
perf_gap: str | None = None # "距升档 13.8h" 或 None
perf_reached: bool = False
# 缺少: perf_pct, perf_target, current_tier, ticks 等
```
```html
<!-- board-coach.wxml — 仅文字展示,无进度条 -->
<text class="bottom-right bottom-right--warning"
wx:if="{{dimType === 'perf' && !item.perfReached}}">
{{item.perfGap}}
</text>
<text class="bottom-right bottom-right--success"
wx:elif="{{dimType === 'perf' && item.perfReached}}">
✅ 已达标
</text>
```
```json
// board-coach.json — 未引用 perf-progress-bar
{
"usingComponents": {
"coach-level-tag": "/components/coach-level-tag/coach-level-tag",
"filter-dropdown": "/components/filter-dropdown/filter-dropdown",
// ... 无 perf-progress-bar
}
}
```
### 建议
1. **后端**:在 `CoachBoardItem` 中增加进度可视化字段:
- `perf_pct: float` — 当前业绩占目标的百分比0~100
- `perf_target: float` — 当前档位目标小时数
- `current_tier: int` — 当前档位0~5
- `ticks: list[dict]` — 档位刻度数组(复用 `perf-progress-bar` 组件的 ticks 格式)
2. **前端**:在 `board-coach.json` 中引入 `perf-progress-bar` 组件,在卡片的 perf 维度区域渲染进度条
3. 可参考 `coach-detail` 页面中 `perf-progress-bar` 的使用方式

View File

@@ -0,0 +1,61 @@
# P8→NS1/RNS1 缺失项 #5看板数据的实时性标识
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟠 中
- 三个看板页面均无"数据更新于 XX:XX"的展示,后端 API 响应中也无数据截止时间字段。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_board.py`(三个 Response schema
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.wxml`
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.wxml`
- `apps/miniprogram/miniprogram/pages/board-customer/board-customer.wxml`
- 全局搜索关键词:`更新于``updated_at``dataTime``updateTime``截止`
### 发现
1. **后端 Schema 无数据截止时间字段**
- `CoachBoardResponse`:仅包含 `items``dim_type`
- `CustomerBoardResponse`:仅包含维度数据列表
- `FinanceBoardResponse`:仅包含 6 个 Panel无时间戳
- 三个 Response 均无 `data_updated_at``snapshot_time` 等字段
2. **前端页面无数据更新时间展示**
- 全局搜索 `更新于``updated_at``dataTime``updateTime``截止` 在 board 相关文件中均无匹配
- 三个看板页面的 WXML 模板中无任何时间戳展示区域
3. **财务看板有"预估"标签但非实时性标识**
- `board-finance` 中有 `isCurrentMonth` 判断,当月数据显示"(预估)"后缀
- 这是数据性质标识,不是数据截止时间
### 证据
```python
# 三个 Response schema 均无时间戳字段
class CoachBoardResponse(CamelModel):
items: list[CoachBoardItem]
dim_type: str
class FinanceBoardResponse(CamelModel):
overview: OverviewPanel
recharge: RechargePanel | None
revenue: RevenuePanel
cashflow: CashflowPanel
expense: ExpensePanel
coach_analysis: CoachAnalysisPanel
# 缺少: data_updated_at / snapshot_time
```
```
# 全局搜索结果
grep "更新于|updated_at|dataTime|updateTime|截止" **/board-*/**
→ No matches found.
```
### 建议
1. **后端**:在三个 BoardResponse 中增加 `data_updated_at: datetime` 字段,返回 DWS 层最后一次 ETL 刷新时间
2. **前端**在每个看板页面顶部Tab 下方或筛选栏下方)展示"数据更新于 HH:MM"
3. 数据来源可从 ETL 调度记录表(如 `dws.etl_run_log`)获取最后成功执行时间
4. 建议格式:当天数据显示"更新于 14:30",非当天显示"更新于 03-20 14:30"

View File

@@ -0,0 +1,60 @@
# P8→NS1/RNS1 缺失项 #6财务看板环比数据的 tooltip 说明
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 环比数据已展示(↑/↓箭头+数值),环比开关已实现,但点击环比箭头不会显示计算详情 tooltip仅指标名称旁的"?"图标有 tip 弹窗。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.wxml`(环比展示区域)
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts`(交互逻辑)
### 发现
1. **环比开关已实现**
- 顶部筛选栏有环比开关(`toggleCompare`),点击切换 `compareEnabled` 状态
- 环比数据通过 `wx:if="{{compareEnabled}}"` 条件渲染
2. **环比数据展示格式完整**
- 使用 `↑`/`↓` 箭头 + 数值文本展示环比变化
- 样式区分:上升用 `compare-text-up`(绿色),下降用 `compare-text-down`(红色),持平用 `compare-text-flat`
3. **环比箭头无点击交互**
- 所有 `compare-text-*` 元素均为纯文本展示,无 `bindtap` 事件
- 搜索 `compare.*tap``tooltip``onCompareTap` 在 board-finance 中无匹配
- P8 定义的"点击环比箭头显示计算详情(如:本期 ¥12,000 vs 上期 ¥10,000变化 +20%"未实现
4. **指标名称的"?"帮助图标已实现**
- 各指标旁有 `help-icon` 元素,绑定 `onHelpTap` 事件
- 点击后弹出 `tipContents` 中预定义的说明文案
- 但这是指标含义说明,不是环比计算详情
### 证据
```html
<!-- board-finance.wxml — 环比数据为纯文本,无 bindtap -->
<view class="compare-row" wx:if="{{compareEnabled}}">
<text class="compare-text-up">↑{{overview.occurrenceCompare}}</text>
</view>
<!-- 对比:指标名称的"?"有 bindtap -->
<view class="help-icon-light" data-key="occurrence" bindtap="onHelpTap">?</view>
```
```typescript
// board-finance.ts — tipContents 仅包含指标含义,无环比计算详情
const tipContents: Record<string, { title: string; content: string }> = {
occurrence: {
title: '发生额/正价',
content: '所有消费项目按标价计算的总金额,不扣除任何优惠。...',
},
// ... 无环比计算详情
}
```
### 建议
1. **方案 A轻量**:为环比文本添加 `bindtap` 事件,点击后弹出包含"本期值 vs 上期值 → 变化率"的 tooltip
2. **方案 B完整**:后端在环比数据中返回 `current_value``previous_value``change_pct` 三个字段,前端据此渲染详情弹窗
3. 当前后端 `calc_compare` 函数已计算了 current/previous 值,只需在响应中透传即可

View File

@@ -0,0 +1,58 @@
# P8→NS1/RNS1 缺失项 #7助教看板卡片点击跳转到 coach-detail
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 助教卡片已实现 bindtap 事件,点击后通过 wx.navigateTo 跳转到 coach-detail 页面并传递 id 参数。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.wxml`(卡片模板)
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.ts`(点击事件处理)
- `apps/miniprogram/miniprogram/app.json`coach-detail 页面注册)
### 发现
1. **WXML 模板中卡片绑定了 bindtap 事件**
- 每个 `coach-card` 元素绑定了 `bindtap="onCoachTap"`
- 通过 `data-id="{{item.id}}"` 传递助教 ID
- 设置了 `hover-class="coach-card--hover"` 提供点击反馈
2. **TS 中实现了跳转逻辑**
- `onCoachTap` 方法从事件中提取 `id`,使用 `wx.navigateTo` 跳转到 `coach-detail` 页面
- 跳转 URL 格式:`/pages/coach-detail/coach-detail?id=xxx`
3. **coach-detail 页面已注册且功能完整**
- `app.json` 的 pages 数组中包含 `pages/coach-detail/coach-detail`
- `pages/coach-detail/` 目录存在
- coach-detail 页面已引用 `perf-progress-bar` 组件(进度条在详情页可用)
### 证据
```html
<!-- board-coach.wxml -->
<view class="coach-card"
wx:for="{{coaches}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="onCoachTap"
hover-class="coach-card--hover">
```
```typescript
// board-coach.ts
onCoachTap(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id as string
wx.navigateTo({ url: '/pages/coach-detail/coach-detail?id=' + id })
},
```
```json
// app.json — coach-detail 已注册
"pages": [
...
"pages/coach-detail/coach-detail",
...
]
```

View File

@@ -0,0 +1,35 @@
# P8→NS1/RNS1 缺失项 #8客户看板"最频繁"维度的柱状图交互
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 柱状图已实现渲染,但缺少点击柱子显示具体数据的交互
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/board-customer/board-customer.wxml`
- `apps/miniprogram/miniprogram/pages/board-customer/board-customer.ts`
- `apps/miniprogram/miniprogram/pages/board-customer/board-customer.wxss`
### 发现
1. **柱状图渲染已实现**`board-customer.wxml``dimType === 'freq60'` 时渲染了 `mini-chart` 迷你柱状图,包含 8 周数据、柱子高度百分比、渐变透明度、底部数字
2. **数据结构已定义**`weeklyVisits: Array<{ val: number; pct: number }>` 在 TS 接口中已定义Mock 数据包含 8 个元素
3. **缺少点击交互**:柱状图的 `mini-bar-col` 元素没有 `bindtap` 事件绑定,无法点击柱子查看具体数据
4. **无 tooltip/弹窗组件**:没有实现点击柱子后显示详细数据(如具体到店日期、消费金额等)的 UI
### 证据
WXML 中柱状图部分(无 bindtap
```xml
<view class="mini-bar-col" wx:for="{{item.weeklyVisits}}" wx:for-item="wv" wx:for-index="wIdx" wx:key="wIdx">
<view class="mini-bar" style="height:{{wv.pct}}%;opacity:{{0.2 + wIdx * 0.057}}"></view>
</view>
```
TS 中无柱状图点击处理函数,仅有 `onCustomerTap`(整张卡片点击跳转详情页)。
### 建议
1.`mini-bar-col` 上添加 `bindtap="onBarTap"` 并传递 `data-week-index``data-customer-id`
2. 实现 `onBarTap` 方法,弹出轻量 tooltip 显示该周具体到店次数和日期
3. 或者考虑:由于柱状图尺寸较小(迷你图),点击交互在移动端体验可能不佳,可评估是否改为点击整张卡片进入详情页后查看完整图表

View File

@@ -0,0 +1,57 @@
# P8→NS1/RNS1 缺失项 #9看板页面的下拉刷新行为
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 三个看板页面均已实现下拉刷新
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.json` + `.ts`
- `apps/miniprogram/miniprogram/pages/board-customer/board-customer.json` + `.ts`
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.json` + `.ts`
### 发现
1. **JSON 配置已启用**:三个页面的 `.json` 文件均设置了 `"enablePullDownRefresh": true`
2. **TS 生命周期已实现**:三个页面均实现了 `onPullDownRefresh()` 方法
3. **刷新逻辑完整**
- `board-finance`:调用 `_loadGiftRows()` 重新加载数据500ms 后 `wx.stopPullDownRefresh()`
- `board-customer`:调用 `loadData()` 重新加载数据500ms 后 `wx.stopPullDownRefresh()`
- `board-coach`:调用 `loadData()` 重新加载数据500ms 后 `wx.stopPullDownRefresh()`
### 证据
board-finance.ts
```typescript
onPullDownRefresh() {
this._loadGiftRows()
setTimeout(() => wx.stopPullDownRefresh(), 500)
},
```
board-customer.ts
```typescript
onPullDownRefresh() {
this.loadData()
setTimeout(() => wx.stopPullDownRefresh(), 500)
},
```
board-coach.ts
```typescript
onPullDownRefresh() {
this.loadData()
setTimeout(() => wx.stopPullDownRefresh(), 500)
},
```
三个页面 JSON 均包含:
```json
"enablePullDownRefresh": true
```
### 建议
无。功能已完整实现。
> 小优化建议(非必须):`setTimeout(() => wx.stopPullDownRefresh(), 500)` 使用固定延时,理想情况应在数据加载完成后再停止刷新动画,避免数据未返回时刷新动画就消失。待 API 联调时可改为 Promise 链式调用。

View File

@@ -0,0 +1,50 @@
# P8→NS1/RNS1 缺失项 #10财务看板各板块的折叠/展开交互
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 财务看板各板块(经营一览、预收资产、应计收入等)没有折叠/展开控制,所有板块始终完全展开
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts`
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.wxml`
### 发现
1. **无折叠状态变量**TS 的 `data` 中没有任何 `collapsed``expanded``folded` 等板块折叠状态字段
2. **无折叠切换方法**TS 中没有 `toggleSection``collapseSection` 等方法
3. **WXML 无折叠控制**:各 `card-section` 没有条件渲染或高度动画控制,所有板块内容始终完全展示
4. **已有替代方案**财务看板实现了目录导航TOC功能用户可通过目录快速跳转到指定板块部分弥补了长页面浏览的不便
5. **吸顶板块头已实现**:滚动时显示当前板块标题的吸顶头,帮助用户定位当前位置
### 证据
WXML 中板块结构(无折叠控制):
```xml
<!-- ===== 板块 1: 经营一览(深色) ===== -->
<view id="section-overview" class="card-section section-dark">
<!-- 内容始终展示,无 wx:if 或 height 动画控制 -->
</view>
<!-- ===== 板块 2: 预收资产 ===== -->
<view id="section-recharge" class="card-section" wx:if="{{selectedArea === 'all'}}">
<!-- 仅按区域筛选条件显示/隐藏,非用户手动折叠 -->
</view>
```
TS 中与板块交互相关的方法仅有:
```typescript
toggleToc() // 目录导航开关
onTocItemTap() // 目录项点击跳转
toggleCompare() // 环比开关
```
### 建议
1. **评估必要性**:当前财务看板已有 TOC 目录导航 + 吸顶板块头,用户可快速定位。折叠/展开在移动端长页面中是常见模式,但 H5 原型是否有此交互需确认
2. **如需实现**
-`data` 中添加 `sectionCollapsed: Record<string, boolean>` 状态
- 在各板块 `card-header` 上添加 `bindtap="toggleSection"` 并传递 `data-section`
- 使用 CSS `max-height` + `transition` 实现展开/收起动画
- 折叠时仅显示板块标题行,展开时显示完整内容
3. **优先级评估**:鉴于已有 TOC 导航,此项可作为体验优化延后处理

View File

@@ -0,0 +1,68 @@
# P8→NS1/RNS1 缺失项 #11看板数据加载失败时的错误展示
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 三个看板页面均已实现错误态展示和重试按钮
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.wxml` + `.ts`
- `apps/miniprogram/miniprogram/pages/board-customer/board-customer.wxml` + `.ts`
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.wxml` + `.ts`
### 发现
1. **pageState 状态机已定义**:三个页面均定义了 `pageState: 'loading' | 'empty' | 'normal' | 'error'` 四态
2. **错误态 UI 已实现**:三个页面 WXML 均包含 `wx:elif="{{pageState === 'error'}}"` 条件渲染的错误态视图
3. **重试按钮已实现**:错误态视图中均包含 `bindtap="onRetry"` 的重试按钮
4. **onRetry 方法已实现**:三个页面 TS 均实现了 `onRetry()` 方法,调用 `loadData()` 或相应的数据加载方法
5. **loadData 中有 catch 处理**`board-customer``board-coach``loadData()` 使用 try-catchcatch 中设置 `pageState: 'error'`
### 证据
board-coach.wxml 错误态:
```xml
<view class="page-error" wx:elif="{{pageState === 'error'}}">
<t-empty description="加载失败" />
<view class="retry-btn" bindtap="onRetry">点击重试</view>
</view>
```
board-customer.wxml 错误态(结构一致):
```xml
<view class="page-error" wx:elif="{{pageState === 'error'}}">
<t-empty description="加载失败" />
<view class="retry-btn" bindtap="onRetry">点击重试</view>
</view>
```
board-finance.wxml 错误态:
```xml
<view class="page-error" wx:elif="{{pageState === 'error'}}">
<t-empty description="加载失败" />
<view class="retry-btn" bindtap="onRetry">
<text class="retry-btn-text">点击重试</text>
</view>
</view>
```
board-coach.ts 错误处理:
```typescript
loadData() {
this.setData({ pageState: 'loading' })
setTimeout(() => {
try {
// ...
} catch {
this.setData({ pageState: 'error' })
}
}, 400)
},
onRetry() {
this.loadData()
},
```
### 建议
无。功能已完整实现。三个看板页面均具备 loading → normal/empty/error 四态切换,错误态有 `<t-empty>` 空态组件 + 重试按钮。

View File

@@ -0,0 +1,59 @@
# P8→NS1/RNS1 缺失项 #12筛选项的动画效果
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- filter-dropdown 组件已实现展开/收起动画,包括面板滑入、箭头旋转、遮罩层
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/components/filter-dropdown/filter-dropdown.ts`
- `apps/miniprogram/miniprogram/components/filter-dropdown/filter-dropdown.wxml`
- `apps/miniprogram/miniprogram/components/filter-dropdown/filter-dropdown.wxss`
### 发现
1. **面板展开/收起动画已实现**WXSS 中 `.dropdown-panel` 使用 `opacity` + `transform: translateY` 过渡动画展开时从上方滑入0.25s ease
2. **箭头旋转动画已实现**`.filter-arrow` 使用 `transform: rotate(180deg)` + `transition: 0.25s ease` 实现展开时箭头翻转
3. **遮罩层已实现**:展开时显示半透明黑色遮罩 `rgba(0, 0, 0, 0.5)`,点击遮罩关闭下拉
4. **按钮状态变化已实现**:展开时按钮边框变为主色调 `--color-primary`,背景变为浅色 `--color-primary-light`,有 0.2s 过渡
5. **面板定位动态计算**:展开时通过 `createSelectorQuery` 计算按钮底部位置,面板从该位置展开
### 证据
WXSS 动画定义:
```css
/* 面板展开/收起动画 */
.dropdown-panel {
opacity: 0;
transform: translateY(-16rpx);
transition: opacity 0.25s ease, transform 0.25s ease;
pointer-events: none;
}
.dropdown-panel--show {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
/* 箭头旋转动画 */
.filter-arrow {
transition: transform 0.25s ease;
}
.filter-arrow--up {
transform: rotate(180deg);
}
/* 按钮状态过渡 */
.filter-dropdown {
transition: border-color 0.2s, background-color 0.2s;
}
```
WXML 动画触发:
```xml
<view class="dropdown-panel {{expanded ? 'dropdown-panel--show' : ''}}" style="top: {{panelTop}}px">
```
### 建议
无。动画效果已完整实现包含面板滑入opacity + translateY、箭头旋转rotate 180deg、按钮状态变化border-color + background-color均使用 CSS transition 实现,性能良好。

View File

@@ -0,0 +1,61 @@
# P8→NS1/RNS1 缺失项 #13助教看板的排名序号展示
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 助教卡片列表中没有排名序号(如 #1#2#3)展示
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.wxml`
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.ts`
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.wxss`
### 发现
1. **无排名序号渲染**WXML 中 `coach-card``wx:for` 循环没有使用 `wx:for-index` 来展示排名序号
2. **无排名字段**TS 的 `CoachItem` 接口中没有 `rank` 字段
3. **无排名样式**WXSS 中没有 `rank``序号``number` 等相关样式类
4. **卡片结构**:当前卡片结构为 `头像 → 姓名+等级+技能+右侧指标 → 底部客户列表`,没有排名序号的位置
### 证据
WXML 中助教列表渲染(无排名序号):
```xml
<view class="coach-list">
<view class="coach-card" wx:for="{{coaches}}" wx:key="id"
data-id="{{item.id}}" bindtap="onCoachTap"
hover-class="coach-card--hover">
<view class="card-row">
<!-- 头像 -->
<view class="card-avatar avatar-{{item.avatarGradient}}">
<text class="avatar-text">{{item.initial}}</text>
</view>
<!-- 信息区(无排名序号) -->
<view class="card-info">
...
</view>
</view>
</view>
</view>
```
CoachItem 接口(无 rank 字段):
```typescript
interface CoachItem {
id: string
name: string
initial: string
avatarGradient: string
level: string
// ... 无 rank 字段
}
```
### 建议
1. **评估必要性**:排名序号在看板场景中有助于快速识别排名位置,但也会增加视觉噪音。需确认 P8 原型中是否明确要求显示
2. **如需实现**
- 方案 A推荐利用 `wx:for``index` 直接渲染,在头像左侧或上方添加 `#{{index + 1}}` 序号
- 方案 B在卡片左上角添加小圆形排名徽章前 3 名用金/银/铜色区分
- 在 WXML 的 `card-row` 开头添加:`<text class="rank-num">#{{index + 1}}</text>`
3. **客户看板同理**:如果助教看板需要排名序号,客户看板的列表也应考虑一致性

View File

@@ -0,0 +1,96 @@
# P8→NS1/RNS1 缺失项 #14财务看板数字的格式化规范
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 千分位、小数位、货币符号的格式化规范已在设计文档中定义工具函数已实现TS + WXS 双版本),财务看板已使用
## 详细审查
### 审查范围
- `docs/miniprogram-dev/design-system/DISPLAY-STANDARDS.md`
- `apps/miniprogram/miniprogram/utils/money.ts`
- `apps/miniprogram/miniprogram/utils/format.wxs`
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts`
- `apps/miniprogram/miniprogram/pages/board-coach/board-coach.ts`
### 发现
#### 1. 设计规范文档已完善
`DISPLAY-STANDARDS.md` 第 1 章"金额展示规范"明确定义了:
- 正常金额:`¥N,NNN`(千分位逗号,无小数)
- 负数金额:`-¥N,NNN`(负号在 ¥ 前)
- 零值:`¥0`
- 空值:`--`
- 大额金额:不简写,保留完整数字
- 禁止事项:禁止 `¥-368``¥0.00``¥12万``toLocaleString()`
#### 2. TS 工具函数已实现(`utils/money.ts`
- `formatMoney(value)` — 金额格式化,千分位 + ¥ 前缀
- `formatCount(value, unit)` — 计数格式化,千分位 + 单位
- `formatNumber(value)` — 纯数字千分位
- `formatPercent(value)` — 百分比,保留 1 位小数
- `formatTrendValue(value)` — 同比/环比差值,+¥/-¥ 前缀
- `toProgressWidth(value)` — 进度条宽度,截断至 [0, 100]
#### 3. WXS 工具函数已实现(`utils/format.wxs`
- `money(value)` — 金额格式化WXS 版,用于 WXML 模板)
- `count(value, unit)` — 计数格式化
- `percent(value)` — 百分比
- `hours(value)` — 课时格式化
- `trendValue(value)` — 同比/环比差值
- `safe(val)` — 空值兜底
#### 4. 看板页面已使用格式化函数
- `board-finance.ts`:导入并使用 `formatMoney` 格式化赠送卡矩阵数据
- `board-coach.ts`:导入并使用 `formatMoney``formatCount``formatHours` 格式化助教数据
- `board-finance.wxml`:未引入 `format.wxs`(财务看板数据在 TS 层预格式化后传入模板)
- `board-customer.wxml`:引入 `format.wxs`,使用 `fmt.safe()` 兜底
### 证据
DISPLAY-STANDARDS.md 金额规范:
```markdown
| 场景 | 格式 | 示例 |
|---|---|---|
| 正常金额 | `¥N,NNN`(千分位逗号,无小数) | `¥12,680` |
| 负数金额 | `-¥N,NNN`(负号在 ¥ 前) | `-¥368` |
| 零值 | `¥0` | `¥0` |
| 空值 / undefined | `--` | `--` |
```
money.ts 核心函数:
```typescript
export function formatMoney(value: number | null | undefined): string {
if (value === null || value === undefined) return '--'
if (value === 0) return '¥0'
const abs = Math.round(Math.abs(value))
const formatted = abs.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return value < 0 ? `${formatted}` : `¥${formatted}`
}
```
format.wxs 金额函数WXS 版,逻辑一致):
```javascript
function money(value) {
if (value === undefined || value === null) return '--'
if (value === 0) return '¥0'
// ... 千分位处理
return (value < 0 ? '-¥' : '¥') + result
}
```
board-finance.ts 使用示例:
```typescript
import { formatMoney } from '../../utils/money'
// ...
wine: formatMoney(row.liquor?.value),
table: formatMoney(row.tableFee?.value),
```
### 建议
无。格式化规范已完整覆盖:
- 设计文档DISPLAY-STANDARDS.md定义了规则
- TS 工具函数money.ts供 JS 层使用
- WXS 工具函数format.wxs供 WXML 模板使用
- 看板页面已实际调用格式化函数

View File

@@ -0,0 +1,65 @@
# P9→NS1/RNS1 缺失项 #1客户详情页分段加载策略
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🔴 高
- 后端采用单 API 返回全部数据(无分段端点),但各扩展模块有独立 try/except 优雅降级;前端无 skeleton 占位,仅有全局 loading toast。
## 详细审查
### 审查范围
- `apps/backend/app/routers/xcx_customers.py` — CUST-1 路由
- `apps/backend/app/services/customer_service.py``get_customer_detail()` 实现
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.ts` — 前端加载逻辑
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 前端模板
### 发现
1. **后端:单 API无分段端点**
- `GET /api/xcx/customers/{customer_id}` 一次性返回全部数据(基本信息 + Banner + AI 洞察 + 关联助教 + 最亲密助教 + 消费记录 + 备注)
- 无独立的 `/ai-insight``/notes``/records` 等子端点
- P9 定义的"基本信息→消费汇总→AI 洞察→消费记录→备注"分段加载策略未实现
2. **后端:优雅降级已实现**
- 各扩展模块(`ai_insight``retention_clues``notes``consumption_records``coach_tasks``favorite_coaches`)均有独立 try/except失败时降级为空默认值
- 核心字段member_info失败直接 500符合预期
3. **前端:无 skeleton 占位**
- 加载态为全局 `g-toast-loading`(圆形 loading + "加载中..."文字),非 P9 定义的分段 skeleton
- `loadDetail()` 调用单个 `fetchCustomerDetail(id)` 后一次性 setData
- 无分段渲染逻辑(先展示基本信息,再逐步加载扩展模块)
### 证据
后端 `get_customer_detail()` 一次性返回所有模块:
```python
return {
"id": customer_id, "name": name, "phone": phone, ...
"balance": balance, "consumption_60d": consumption_60d, ...
"ai_insight": ai_insight,
"coach_tasks": coach_tasks,
"favorite_coaches": favorite_coaches,
"retention_clues": retention_clues,
"consumption_records": consumption_records,
"notes": notes,
}
```
前端加载逻辑(无分段):
```typescript
async loadDetail(id?: string) {
this.setData({ pageState: 'loading' })
try {
if (id) {
const detail = await fetchCustomerDetail(id)
// 一次性 setData
}
this.setData({ pageState: 'normal' })
} catch { ... }
}
```
### 建议(如未完全解决)
1. **短期**:前端可在单 API 返回后,先渲染 Banner 区域,再用 `nextTick``setTimeout` 分批 setData 扩展模块,减少首屏白屏时间
2. **中期**:为各扩展模块添加 skeleton 占位组件(参考 TDesign `t-skeleton`
3. **长期**:后端拆分为多个子端点(`/basic``/ai-insight``/records` 等),前端并行请求 + 分段渲染

View File

@@ -0,0 +1,60 @@
# P9→NS1/RNS1 缺失项 #2助教详情页档位进度时间轴的视觉规范
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 已实现 `perf-progress-bar` 进度条组件(含渐变填充、刻度标记、高光/火花动画),但非 P9 定义的"时间轴"样式,缺少当前档位高亮节点和升档动画。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_coaches.py``tier_nodes` 字段定义
- `apps/backend/app/services/coach_service.py``_build_tier_nodes()` 实现
- `apps/miniprogram/miniprogram/components/perf-progress-bar/` — 进度条组件
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml` — 绩效概览区域
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts` — 进度条数据构建
### 发现
1. **后端tier_nodes 数据已返回**
- `CoachDetailResponse.tier_nodes: list[float]` 已定义
- `_build_tier_nodes()` 从配置表读取档位节点
2. **前端:进度条组件已实现,但非时间轴**
- `perf-progress-bar` 组件实现了水平进度条,含:
- 渐变填充条(`ppb-fill` + `ppb-gradient-bar`
- 刻度标记(`ppb-ticks`,由 `ticks` 数组动态渲染)
- 高光动画(`ppb-shine`)和火花动画(`ppb-spark`
- 当前档位高亮(`ppb-tick--done` class
- 但这是**水平进度条**,非 P9 定义的**垂直时间轴**样式
- 缺少 P9 定义的"档位节点"tierNodes的时间轴展示如里程碑节点、连接线、当前位置标记
3. **动画已实现但非"升档动画"**
- 高光shine和火花spark是循环播放的装饰动画
- 非 P9 定义的"升档时触发的庆祝动画"
4. **前端 tier_nodes 未使用 API 数据**
- `coach-detail.ts``tierNodes` 硬编码为 `[0, 100, 130, 160, 190, 220]`,注释标注 "Mock实际由接口返回"
- API 返回的 `tier_nodes` 未被前端消费
### 证据
前端硬编码 tierNodes
```typescript
const tierNodes = [0, 100, 130, 160, 190, 220] // Mock实际由接口返回
```
进度条组件刻度渲染(水平进度条,非时间轴):
```html
<view class="ppb-ticks">
<text wx:for="{{ticks}}" wx:key="value"
class="ppb-tick {{currentTier >= index ? 'ppb-tick--done' : ''}}">
{{item.label}}
</text>
</view>
```
### 建议(如未完全解决)
1. 将前端 `tierNodes` 改为使用 API 返回的 `detail.tierNodes` 数据
2. 如需时间轴样式,新建 `tier-timeline` 组件,展示垂直时间轴(里程碑节点 + 连接线 + 当前位置)
3. 添加升档动画:当 `currentTier` 变化时触发一次性庆祝效果

View File

@@ -0,0 +1,57 @@
# P9→NS1/RNS1 缺失项 #3消费记录 3 种类型的图标/颜色/标签样式映射
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🔴 高
- 台桌消费和商城消费有颜色区分(蓝色/绿色 header + 圆点),但充值类型在客户详情页 wxml 中缺少渲染模板;设计规范文档中无消费记录类型的视觉映射定义。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_customers.py``ConsumptionRecord.type` 字段
- `apps/backend/app/services/customer_service.py``_build_consumption_records()` 实现
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 消费记录展示
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxss` — 消费记录样式
- `docs/miniprogram-dev/design-system/` — 设计规范文档
### 发现
1. **后端type 字段已定义但实际只返回 "table"**
- Schema 定义 `type: str # table / shop / recharge`
- `_build_consumption_records()` 硬编码 `"type": "table"`,未根据实际数据区分商城/充值类型
- 前端 TypeScript 接口定义了 `type: "table" | "shop" | "recharge"` 三种类型
2. **前端:台桌和商城有视觉区分,充值缺失**
- 台桌消费(`type === 'table'`):蓝色 header`record-header-blue`+ 蓝色圆点(`record-dot-blue`
- 商城消费(`type === 'shop'`):绿色 header`record-header-green`+ 绿色圆点(`record-dot-green`
- 充值(`type === 'recharge'`**wxml 中无对应的渲染模板**`wx:elif` 链中缺少 recharge 分支)
- Mock 数据中有 `{ type: 'recharge', rechargeAmount: 0 }` 但无对应 UI
3. **设计规范文档中无消费记录类型映射**
- `VI-DESIGN-SYSTEM.md``DISPLAY-STANDARDS.md` 中未定义消费记录类型的图标/颜色/标签映射
- 无统一的类型→视觉映射表
### 证据
后端硬编码 type 为 "table"
```python
result.append({
"type": "table", # 始终为 table未区分 shop/recharge
...
})
```
前端 wxml 缺少 recharge 分支:
```html
<view class="record-card" wx:if="{{item.type === 'table'}}">...</view>
<view class="record-card" wx:elif="{{item.type === 'shop'}}">...</view>
<!-- 缺少 wx:elif="{{item.type === 'recharge'}}" -->
```
### 建议(如未完全解决)
1. **后端**`_build_consumption_records()` 根据结算单类型字段区分 table/shop/recharge
2. **前端**:添加 recharge 类型的渲染模板(建议橙色/金色 header充值图标
3. **设计规范**:在 `VI-DESIGN-SYSTEM.md` 中添加消费记录类型映射表:
- 台桌消费:🎱 蓝色(`#3b82f6`
- 商城消费:🛒 绿色(`#22c55e`
- 充值:💰 橙色/金色(`#f59e0b`

View File

@@ -0,0 +1,73 @@
# P9→NS1/RNS1 缺失项 #4备注 AI 评分的星级展示规范
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- `star-rating` 组件已实现0-10 分→0-5 星,支持半星),设计规范文档已定义评分展示规范,但客户详情页备注区域未使用该组件,后端备注 API 未返回 `ai_score` 字段。
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/components/star-rating/` — 星级评分组件
- `apps/backend/app/services/customer_service.py``_build_notes()` 实现
- `apps/backend/app/schemas/xcx_customers.py``CustomerNote` schema
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 备注区域
- `docs/miniprogram-dev/design-system/DISPLAY-STANDARDS-2.md` — 评分展示规范
### 发现
1. **组件层star-rating 已完整实现**
- 接收 `score`0-10内部转换为 0-5 星,支持半星
- 使用 TDesign `t-rate` 组件渲染,金黄色(`#fbbf24`
- 支持只读模式
2. **设计规范:评分展示规范已定义**
- `DISPLAY-STANDARDS-2.md` 第 8 节定义了:
- 分制约定(后端 0-10 分UI 0-5 星)
- 展示场景(任务卡片、备注满意度等)
- 半星映射规则(`scoreToHalfStar()`
- 未评分态处理(`score=0/null/undefined` → 展示 `--`
3. **后端:备注 API 未返回 ai_score**
- `_build_notes()` 查询 `biz.notes` 表,只返回 `id``tag_label``created_at``content`
- `CustomerNote` schema 无 `ai_score` / `score` 字段
- 数据库 `biz.notes` 表是否有 `ai_score` 列未确认
4. **前端:备注区域未使用 star-rating 组件**
- `customer-detail.wxml` 备注列表只展示 `tagLabel``createdAt``content`
-`<star-rating>` 组件引用
- `customer-detail.json` 未注册 `star-rating` 组件(需确认)
### 证据
后端 `_build_notes()` 无 score 字段:
```python
return [
{
"id": r[0],
"tag_label": r[1] or "",
"created_at": r[2].isoformat() if r[2] else "",
"content": r[3] or "",
# 缺少 ai_score / score 字段
}
for r in rows
]
```
前端备注展示无星级:
```html
<view class="note-item" wx:for="{{sortedNotes}}" wx:key="id">
<view class="note-top">
<text class="note-author">{{item.tagLabel}}</text>
<text class="note-time">{{item.createdAt}}</text>
</view>
<text class="note-content">{{item.content}}</text>
<!-- 缺少 <star-rating score="{{item.score}}" /> -->
</view>
```
### 建议(如未完全解决)
1. **后端**`_build_notes()` 查询中增加 `ai_score` 字段(如 `biz.notes` 表有该列)
2. **Schema**`CustomerNote` 添加 `score: int | None = None`
3. **前端**:备注卡片中添加 `<star-rating score="{{item.score}}" size="32rpx" />`,未评分时展示 `--`
4. **tooltip**P9 定义的评分说明 tooltip 需额外实现(小程序原生不支持 tooltip可用长按弹窗替代

View File

@@ -0,0 +1,61 @@
# P9→NS1/RNS1 缺失项 #5客户详情页 Banner 区域的视觉设计
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- Banner 区域已实现完整的 4 字段布局(储值余额/60天消费/理想间隔/距今到店),含渐变背景、毛玻璃统计栏、颜色区分。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_customers.py` — Banner 字段定义
- `apps/backend/app/services/customer_service.py` — Banner 字段查询
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — Banner 区域模板
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxss` — Banner 区域样式
### 发现
1. **后端4 个 Banner 字段已定义并实现**
- `balance: float | None` — 储值余额
- `consumption_60d: float | None` — 60 天消费
- `ideal_interval: int | None` — 理想间隔
- `days_since_visit: int | None` — 距今到店
- 各字段独立 try/except查询失败降级为 `null`
2. **前端Banner 布局已完整实现**
- SVG 渐变背景图(`banner-bg-dark-gold-aurora.svg`
- 客户头部信息(头像 + 姓名 + 手机号查看/复制)
- 4 格统计栏(`banner-stats`),毛玻璃效果(`backdrop-filter: blur(8px)`
- 颜色区分:余额绿色(`stat-green #6ee7b7`)、距今到店琥珀色(`stat-amber #fcd34d`
3. **布局与 RNS1 T2-1 定义一致**
- 4 个字段均已展示,布局为水平等分 + 分隔线
### 证据
前端 Banner 统计区域:
```html
<view class="banner-stats">
<view class="stat-item stat-border">
<text class="stat-value stat-green">¥{{fmt.safe(detail.balance)}}</text>
<text class="stat-label">储值余额</text>
</view>
<view class="stat-item stat-border">
<text class="stat-value">¥{{fmt.safe(detail.consumption60d)}}</text>
<text class="stat-label">60天消费</text>
</view>
<view class="stat-item stat-border">
<text class="stat-value">{{fmt.safe(detail.idealInterval)}}</text>
<text class="stat-label">理想间隔</text>
</view>
<view class="stat-item">
<text class="stat-value stat-amber">{{fmt.safe(detail.daysSinceVisit)}}</text>
<text class="stat-label">距今到店</text>
</view>
</view>
```
### 建议(如未完全解决)
无重大缺失。可考虑的微调:
- `ideal_interval` 后端当前返回 `None`,需确认数据源是否已接入
- 可添加单位后缀(如"天"、"元")提升可读性

View File

@@ -0,0 +1,55 @@
# P9→NS1/RNS1 缺失项 #6AI 洞察卡片的展示规范
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- AI 洞察卡片已实现标题/摘要/策略列表展示,但缺少 P9 定义的"展开详情"交互和"刷新"按钮。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_customers.py``AiInsight` schema
- `apps/backend/app/services/customer_service.py``_build_ai_insight()` 实现
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — AI 洞察区域
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxss` — AI 洞察样式
### 发现
1. **后端AI 洞察数据已实现**
- `AiInsight` schema 含 `summary: str``strategies: list[AiStrategy]`
- `_build_ai_insight()``biz.ai_cache``cache_type='app4_analysis'`)读取缓存的 AI 分析结果
- 无缓存时返回空默认值
2. **前端:卡片展示已实现**
- 渐变背景卡片(紫色渐变 `#667eea → #764ba2`
- AI 图标 + "AI 智能洞察"标题
- 摘要文本展示
- 策略列表(带颜色左边框,支持 green/amber/pink 三色)
3. **缺失交互**
- **无"展开详情"功能**P9 定义了摘要可展开查看完整分析,当前实现直接展示全部内容
- **无"刷新"按钮**P9 定义了手动触发 AI 重新分析的刷新按钮,当前无此交互
- 后端也无对应的"触发 AI 重新分析"端点
### 证据
前端 AI 洞察卡片(无展开/刷新交互):
```html
<view class="ai-insight-card">
<view class="ai-insight-header">
<view class="ai-icon-box">...</view>
<text class="ai-insight-label">AI 智能洞察</text>
<!-- 缺少刷新按钮 -->
</view>
<view class="ai-insight-summary-v">
<text class="ai-insight-summary">{{fmt.safe(aiInsight.summary)}}</text>
<!-- 无展开/折叠控制 -->
</view>
...
</view>
```
### 建议(如未完全解决)
1. **展开详情**:如摘要较长,可添加 `wx:if="{{aiExpanded}}"` 控制展示行数,默认 3 行 + "查看更多"
2. **刷新按钮**:在 header 右侧添加刷新图标,点击调用后端 AI 分析端点
3. **后端**:添加 `POST /api/xcx/customers/{id}/ai-refresh` 端点触发重新分析

View File

@@ -0,0 +1,58 @@
# P9→NS1/RNS1 缺失项 #7关联助教任务列表的展示规范
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 关联助教任务列表已完整实现:任务类型标签(带颜色映射)、状态标签(置顶/已放弃)、服务统计指标(服务次数/总时长/次均时长),布局和交互完整。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_customers.py``CoachTask` schema
- `apps/backend/app/services/customer_service.py``_build_coach_tasks()` 实现
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 助教任务区域
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxss` — 助教任务样式
### 发现
1. **后端:任务数据已完整实现**
- `CoachTask` schema 含:`name``level``level_color``task_type``task_color``bg_class``status``last_service``metrics`
- `_build_coach_tasks()``biz.coach_tasks` 查询,关联助教信息和绩效等级
- 任务类型通过 `TASK_TYPE_MAP` 映射(含 label/color/bg_class
- 近 60 天统计指标(服务次数/总时长/次均时长)已计算
2. **前端:展示已完整实现**
- 任务类型标签:`type-red`/`type-pink`/`type-orange`/`type-teal` 四色映射
- 状态标签:📌 置顶 / ❌ 已放弃
- 助教等级标签:使用 `coach-level-tag` 组件
- 服务统计3 个指标卡片(服务次数/总时长/次均时长)
- 上次服务时间展示
- 卡片背景色根据助教等级区分(`coach-card-red`/`pink`/`orange`/`teal`
### 证据
前端任务卡片展示:
```html
<view class="coach-task-card {{item.bgClass}}" wx:for="{{coachTasks}}" wx:key="index">
<view class="coach-task-top">
<view class="coach-name-row">
<text class="coach-name">{{item.name}}</text>
<coach-level-tag level="{{item.level}}" />
</view>
<view class="coach-task-right">
<text class="coach-task-type type-{{item.taskColor}}">{{item.taskType}}</text>
<text class="coach-task-status status-{{item.status}}" wx:if="{{item.status !== 'normal'}}">...</text>
</view>
</view>
<text class="coach-last-service">上次服务:{{item.lastService}}</text>
<view class="coach-metrics">
<view class="coach-metric" wx:for="{{item.metrics}}" wx:for-item="m" wx:key="label">
<text class="metric-label">{{m.label}}</text>
<text class="metric-value">{{m.value}}</text>
</view>
</view>
</view>
```
### 建议(如未完全解决)
无重大缺失。当前实现已覆盖 P9 定义的任务类型图标(用颜色标签替代)、状态标签、服务统计。

View File

@@ -0,0 +1,60 @@
# P9→NS1/RNS1 缺失项 #8最亲密助教的展示规范
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 最亲密助教卡片已实现emoji + 姓名 + 关系指数 + 统计指标),但缺少 P9 定义的"关系指数可视化"(如仪表盘/环形图)和"课时统计图表"。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_customers.py``FavoriteCoach` schema
- `apps/backend/app/services/customer_service.py``_build_favorite_coaches()` 实现
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 最亲密助教区域
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxss` — 最亲密助教样式
### 发现
1. **后端:数据已完整实现**
- `FavoriteCoach` schema 含:`emoji``name``relation_index``index_color``bg_class``stats`
- `_build_favorite_coaches()` 从关系指数表查询,四级 emoji 映射(💖🧡💛💙)
- 统计指标:基础课时收入、激励课时收入、上课次数、总时长
2. **前端:卡片展示已实现**
- emoji + 姓名 + 关系指数数值(带颜色)
- "近60天"时间范围标签
- 4 个统计指标卡片
- 卡片背景色根据关系等级区分(`fav-card-pink`/`fav-card-amber`
3. **缺失的可视化元素**
- **无关系指数可视化**P9 定义了关系指数的可视化展示(如仪表盘、环形进度条),当前仅展示数值
- **无课时统计图表**P9 定义了课时统计的图表展示(如柱状图/折线图),当前仅展示数值列表
### 证据
前端最亲密助教卡片(纯数值展示,无图表):
```html
<view class="fav-coach-card {{item.bgClass}}" wx:for="{{favoriteCoaches}}" wx:key="index">
<view class="fav-coach-top">
<view class="fav-coach-name">
<text class="fav-emoji">{{item.emoji}}</text>
<text class="fav-name">{{item.name}}</text>
</view>
<view class="fav-index">
<text class="fav-index-label">关系指数</text>
<text class="fav-index-value text-{{item.indexColor}}">{{fmt.safe(item.relationIndex)}}</text>
</view>
</view>
<view class="fav-stats">
<view class="fav-stat" wx:for="{{item.stats}}" wx:for-item="s" wx:key="label">
<text class="fav-stat-label">{{s.label}}</text>
<text class="fav-stat-value">{{s.value}}</text>
</view>
</view>
</view>
```
### 建议(如未完全解决)
1. **关系指数可视化**:可用 CSS 环形进度条或 `wx-canvas` 绘制仪表盘,展示 0-10 刻度上的当前位置
2. **课时统计图表**:如需图表,可引入 `wx-charts` 或用 CSS 柱状图展示近期课时趋势
3. **优先级评估**:当前数值展示已满足基本信息需求,图表可作为后续优化项

View File

@@ -0,0 +1,57 @@
# P9→NS1/RNS1 缺失项 #9消费记录中助教明细子列表的展开/折叠交互
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 客户详情页消费记录中的助教明细子列表已实现展示(网格布局),但采用的是"始终展开"策略而非"展开/折叠"交互。考虑到数据量通常较小1-2 位助教),这是合理的设计决策。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_customers.py``ConsumptionRecord.coaches` 字段
- `apps/backend/app/services/customer_service.py``_build_consumption_records()` coaches 构建
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 消费记录助教明细
### 发现
1. **后端coaches 子数组已实现**
- `ConsumptionRecord.coaches: list[CoachServiceItem]` 已定义
- `CoachServiceItem` 含:`name``level``level_color``course_type``hours``perf_hours``fee`
- `_build_consumption_records()` 根据 `assistant_pd_money`/`assistant_cx_money` 构建 coaches 列表
2. **前端:助教明细已展示(始终展开)**
- 使用 `record-coach-grid`2 列网格布局)展示助教卡片
- 每张卡片含:助教姓名 + 等级标签 + 课程类型 + 时长 + 定档绩效 + 费用
- 条件渲染:`wx:if="{{item.coaches && item.coaches.length > 0}}"`
- 台桌消费和商城消费均有助教明细展示
3. **无展开/折叠交互**
- 助教明细始终展开显示,无折叠按钮
- 考虑到每条消费记录通常只有 1-2 位助教,始终展开是合理的
- P9 定义的展开/折叠交互更适用于助教数量较多的场景
### 证据
前端助教明细展示(始终展开):
```html
<view class="record-coaches" wx:if="{{item.coaches && item.coaches.length > 0}}">
<view class="record-coach-grid">
<view class="record-coach-card" wx:for="{{item.coaches}}" wx:for-item="c" wx:key="name">
<view class="record-coach-name-row">
<text class="record-coach-name">{{c.name}}</text>
<coach-level-tag level="{{c.level}}" />
</view>
<text class="record-coach-type">{{c.courseType}} · {{c.hours}}</text>
<view class="record-coach-bottom">
<text class="record-coach-perf" wx:if="{{c.perfHours}}">定档绩效:{{c.perfHours}}</text>
<text class="record-coach-fee">¥{{c.fee}}</text>
</view>
</view>
</view>
</view>
```
### 建议(如未完全解决)
当前实现已满足需求。如后续助教数量增多(>3 位),可考虑:
1. 默认展示前 2 位,超出部分折叠
2. 添加"展开全部"/"收起"按钮

View File

@@ -0,0 +1,79 @@
# P9→NS1/RNS1 缺失项 #10助教详情页 TOP20 客户列表的排序规则和展示字段
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- TOP20 客户列表已完整实现:后端通过 `fdw_queries.get_coach_top_customers()` 获取排序后的数据limit=20前端展示含头像、姓名、心形 emoji、关系分、服务次数、储值余额、消费金额支持展开/收起和点击跳转。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_coaches.py``TopCustomer` schema
- `apps/backend/app/services/coach_service.py``_build_top_customers()` 实现
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml` — TOP20 客户区域
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts` — 展开/收起逻辑
### 发现
1. **后端:排序和数据已完整实现**
- `_build_top_customers()` 调用 `fdw_queries.get_coach_top_customers(conn, site_id, coach_id, limit=20)`
- 排序由 `fdw_queries` 层的 SQL 决定(按关系指数/服务次数等排序)
- 关系指数通过 `fdw_queries.get_relation_index()` 获取
- 四级 heart emoji 映射P6 AC3>8.5→💖 / >7→🧡 / >5→💛 / ≤5→💙
- 展示字段:`id``name``initial``avatar_gradient``heart_emoji``relation_score``score_color``service_count``balance``consume`
2. **前端:展示已完整实现**
- 默认展示前 5 位,点击"查看更多"展开全部
- 每行含:头像(渐变色首字母)+ 姓名 + 心形 emoji + 关系分(带颜色)+ 服务次数 + 储值余额 + 消费金额
- 点击跳转客户详情页
- 右侧箭头图标
3. **后端 schema 字段名不一致(小问题)**
- Schema 定义 `score: str`,但后端实际返回 `relation_score`
- CamelModel 自动转换为 camelCase前端使用 `item.score`
### 证据
后端 TOP 客户构建(含排序和 emoji 映射):
```python
raw = fdw_queries.get_coach_top_customers(conn, site_id, coach_id, limit=20)
# ...
result.append({
"id": mid or 0,
"name": name,
"heart_emoji": heart_emoji, # 四级映射
"relation_score": f"{score:.2f}",
"score_color": score_color,
"service_count": cust.get("service_count", 0),
"balance": _format_currency(balance),
"consume": _format_currency(consume),
})
```
前端展示(默认 5 条 + 展开):
```html
<view class="top-customer-item"
wx:for="{{topCustomers}}" wx:key="id"
wx:if="{{topCustomersExpanded || index < 5}}"
data-id="{{item.id}}" bindtap="onCustomerTap">
<view class="top-customer-avatar avatar-{{item.avatarGradient}}">
<text>{{item.initial}}</text>
</view>
<view class="top-customer-info">
<view class="top-customer-name-row">
<text>{{item.name}}</text>
<text>{{item.heartEmoji}}</text>
<text>{{fmt.safe(item.score)}}</text>
</view>
<view class="top-customer-stats">
<text>服务 {{fmt.safe(item.serviceCount)}}次</text>
<text>储值 {{fmt.safe(item.balance)}}</text>
<text>消费 {{fmt.safe(item.consume)}}</text>
</view>
</view>
</view>
```
### 建议(如未完全解决)
1. **前端数据绑定**:当前 `coach-detail.ts``topCustomers` 使用 mock 数据(空字符串),需确认 API 数据已正确绑定
2. **Schema 字段名**`TopCustomer.score` 与后端返回的 `relation_score` 需确认 CamelModel 映射是否正确

View File

@@ -0,0 +1,67 @@
# P9→NS1/RNS1 缺失项 #11助教详情页历史月份统计的图表展示
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟠 中
- 历史月份统计已实现为表格形式6 个月数据,含客户数/回访召回/业绩时长/工资),但非 P9 定义的折线图/柱状图可视化形式。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_coaches.py``HistoryMonth` schema
- `apps/backend/app/services/coach_service.py``_build_history_months()` 实现
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml` — 历史月份区域
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts` — 数据绑定
### 发现
1. **后端:数据已完整实现**
- `HistoryMonth` schema 含:`month``estimated``customers``hours``salary``callback_done``recall_done`
- `_build_history_months()` 查询最近 6 个月数据:
- 工时/工资:`fdw_queries.get_salary_calc_multi_months()`
- 客户数:`fdw_queries.get_monthly_customer_count()`
- 回访/召回完成数:`biz.coach_tasks` 聚合
- 本月标记 `estimated=True`
- 格式化:`"22人"``"87.5h"``"¥6,950"`
2. **前端:表格展示已实现**
- 5 列表格:月份 / 服务客户 / 访/召完成 / 业绩时长 / 工资
- 本月行高亮(`history-row-current`+ "预估"标签
- 本月业绩时长蓝色(`text-primary`)、工资绿色(`text-success`
3. **缺失的图表可视化**
- P9 定义了折线图/柱状图展示趋势,当前仅为纯表格
- 无数据趋势可视化(如工时趋势折线、工资柱状图)
- 小程序环境下图表实现需要 `wx-canvas` 或第三方图表库
### 证据
前端表格展示:
```html
<view class="history-table">
<view class="history-thead">
<text class="history-th history-th-left">月份</text>
<text class="history-th">服务客户</text>
<text class="history-th">访/召完成</text>
<text class="history-th">业绩时长</text>
<text class="history-th">工资</text>
</view>
<view class="history-row {{index === 0 ? 'history-row-current' : ''}}"
wx:for="{{historyMonths}}" wx:key="month">
<view class="history-td history-td-left">
<text>{{item.month}}</text>
<text class="history-est" wx:if="{{item.estimated}}">预估</text>
</view>
<text class="history-td">{{fmt.safe(item.customers)}}</text>
<text class="history-td">{{fmt.safe(item.callbackDone)}} | {{fmt.safe(item.recallDone)}}</text>
<text class="history-td">{{fmt.safe(item.hours)}}</text>
<text class="history-td">{{fmt.safe(item.salary)}}</text>
</view>
</view>
```
### 建议(如未完全解决)
1. **短期**:表格形式已满足数据展示需求,可作为 MVP
2. **中期**在表格上方添加迷你折线图sparkline展示工时/工资趋势
3. **长期**:引入 `wx-charts``echarts-for-weixin` 实现完整图表
4. **注意**:前端 `historyMonths` 当前使用 mock 数据(空字符串),需确认 API 数据已正确绑定

View File

@@ -0,0 +1,68 @@
# P9→NS1/RNS1 缺失项 #12客户服务记录页的月度统计汇总展示
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟡 低
- 月度统计汇总已完整实现:后端返回 `month_count`/`month_hours`,前端展示为三栏统计条(本月服务次数/服务时长/关系指数),含月份切换器和格式化展示。
## 详细审查
### 审查范围
- `apps/backend/app/schemas/xcx_customers.py``CustomerRecordsResponse` schema
- `apps/backend/app/services/customer_service.py``get_customer_records()` 实现
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.wxml` — 月度统计区域
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts` — 数据处理
### 发现
1. **后端:月度统计数据已实现**
- `CustomerRecordsResponse``month_count: int``month_hours: float`
- `get_customer_records()` 调用 `_get_month_aggregation()` 计算当月汇总(非分页子集)
- 返回 `total_service_count`(跨月累计)
2. **前端:月度统计展示已完整实现**
- 月份切换器(上月/下月箭头 + 月份标签)
- 三栏统计条:
- 本月服务:`monthCount`(如 "3次"
- 服务时长:`monthHours`(如 "12.5h"),蓝色高亮
- 关系指数:`monthRelation`(如 "0.85"),橙色高亮
- 使用 `formatCount()``formatHours()` 格式化函数
- 月份切换时重新请求 API`loadMonthRecords()`
3. **月份切换交互已实现**
- `onPrevMonth()` / `onNextMonth()` 切换月份
- 边界控制:`canPrev`/`canNext` 禁用按钮
- 切换时显示 loading 状态
### 证据
前端月度统计展示:
```html
<view class="month-summary">
<view class="summary-item">
<text class="summary-label">本月服务</text>
<text class="summary-value">{{fmt.safe(monthCount)}}</text>
</view>
<view class="summary-divider"></view>
<view class="summary-item">
<text class="summary-label">服务时长</text>
<text class="summary-value value-primary">{{fmt.safe(monthHours)}}</text>
</view>
<view class="summary-divider"></view>
<view class="summary-item">
<text class="summary-label">关系指数</text>
<text class="summary-value value-warning">{{fmt.safe(monthRelation)}}</text>
</view>
</view>
```
后端月度汇总查询:
```python
month_count, month_hours = _get_month_aggregation(
conn, site_id, customer_id, year, month, table
)
```
### 建议(如未完全解决)
无重大缺失。可考虑的微调:
- `monthRelation`(关系指数)当前前端硬编码为 `'0.85'`,后端 `CustomerRecordsResponse` 中无 `relation_index` 字段返回,需确认数据源

View File

@@ -0,0 +1,54 @@
# P9→NS1/RNS1 缺失项 #13助教详情页任务分组的视觉区分
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟢 低
- 后端按 active/inactive/abandoned 三组返回数据,前端为每组定义了差异化视觉样式
## 详细审查
### 审查范围
- `apps/backend/app/services/coach_service.py``_build_task_groups()`
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml` — 任务执行区域
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxss` — 任务样式
### 发现
1. 后端 `_build_task_groups()` 按 status 分为三组:`visible_tasks`active`hidden_tasks`inactive`abandoned_tasks`abandoned数据结构清晰
2. 前端 WXML 中三组有明确的视觉区分:
- `visibleTasks`:直接展示,每个 task-item 带 `task-item-{{item.typeClass}}` 样式(如 `task-item-high-priority``task-item-priority``task-item-relationship``task-item-callback`),有彩色背景和边框
- `hiddenTasks`:在 `tasksExpanded` 展开后显示,样式与 visible 一致但位于 `task-list-extra` 区域
- `abandonedTasks`:使用 `task-item-abandoned` 样式,灰色背景 `#fafafa``opacity: 0.55`、客户名带删除线 `text-decoration: line-through`
3. WXSS 中定义了完整的颜色映射:
- `task-item-high-priority`:红色系 `rgba(254, 226, 226, 0.6)`
- `task-item-priority`:橙色系 `rgba(255, 237, 213, 0.4)`
- `task-item-relationship`:粉色系 `rgba(252, 231, 243, 0.4)`
- `task-item-callback`:青色系 `rgba(204, 251, 241, 0.4)`
- `task-item-abandoned`:灰色 + 半透明
### 证据
```html
<!-- coach-detail.wxml — active 任务 -->
<view class="task-item task-item-{{item.typeClass}}" wx:for="{{visibleTasks}}" ...>
<text class="task-tag-text {{item.typeClass}}">{{item.typeLabel}}</text>
...
</view>
<!-- abandoned 任务 -->
<view class="task-item task-item-abandoned" wx:for="{{abandonedTasks}}" ...>
<text class="task-abandoned-name">{{item.customerName}}</text>
<text class="task-abandoned-reason">{{item.reason}}</text>
</view>
```
```css
/* coach-detail.wxss */
.task-item-abandoned {
background: #fafafa;
border-color: #eeeeee;
opacity: 0.55;
}
.task-abandoned-name {
text-decoration: line-through;
color: #c5c5c5;
}
```

View File

@@ -0,0 +1,54 @@
# P9→NS1/RNS1 缺失项 #14客户详情页维客线索的展示规范
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟢 低
- clue-card 组件已实现完整的卡片布局和 category 颜色映射6 种配色),后端从 `member_retention_clue` 表查询数据
## 详细审查
### 审查范围
- `apps/backend/app/services/customer_service.py``_build_retention_clues()`
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 维客线索区域
- `apps/miniprogram/miniprogram/components/clue-card/clue-card.ts` — 组件属性
- `apps/miniprogram/miniprogram/components/clue-card/clue-card.wxml` — 组件模板
- `apps/miniprogram/miniprogram/components/clue-card/clue-card.wxss` — 组件样式
### 发现
1. 后端 `_build_retention_clues()``public.member_retention_clue` 表查询 `clue_type``clue_text`,按 `created_at` 倒序
2. 前端 `clue-card` 组件接收 `tag``category`(颜色类名)、`emoji``title``source``content` 六个属性
3. WXSS 中定义了 VI 规范 2.1 的六种客户标签配色:
- `clue-tag-primary`:蓝色(客户基础)
- `clue-tag-success`:绿色(消费习惯)
- `clue-tag-orange`:橙色(玩法偏好)
- `clue-tag-gold` / `clue-tag-warning`:金色(促销偏好)
- `clue-tag-purple`:紫色(社交关系)
- `clue-tag-error`:红色(重要反馈)
- `clue-tag-pink`:粉色(社交关系别名)
4. 卡片布局包含标签方块72rpx×72rpx、文本内容区、来源标注、可选详情描述
### 证据
```html
<!-- customer-detail.wxml -->
<clue-card
wx:for="{{clues}}" wx:key="index"
tag="{{item.category}}"
category="{{item.categoryColor}}"
emoji=""
title="{{item.text}}"
source="By:{{item.source}}"
content="{{item.detail}}"
/>
```
```css
/* clue-card.wxss — VI 规范 2.1 六种配色 */
.clue-tag-primary { background: rgba(0, 82, 217, 0.10); color: #0052d9; }
.clue-tag-success { background: rgba(0, 168, 112, 0.10); color: #00a870; }
.clue-tag-orange { background: rgba(237, 123, 47, 0.12); color: #ed7b2f; }
.clue-tag-purple { background: rgba(123, 97, 255, 0.10); color: #7b61ff; }
.clue-tag-error { background: rgba(227, 77, 89, 0.10); color: #e34d59; }
```
### 建议
- 后端 `_build_retention_clues()` 当前仅返回 `type``text`,未返回 `category`(颜色类名)和 `source`。前端 `customer-detail.ts``clues` 数据目前是 mock 硬编码的 `categoryColor`。建议后端补充 `clue_type → categoryColor` 的映射逻辑,或在前端建立映射表。

View File

@@ -0,0 +1,33 @@
# P9→NS1/RNS1 缺失项 #15客户详情页的分享功能
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 客户详情页未实现任何分享功能,无 `onShareAppMessage`、无分享按钮
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.ts` — 页面逻辑
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 页面模板
### 发现
1. `customer-detail.ts` 中未定义 `onShareAppMessage()``onShareTimeline()` 生命周期方法
2. `customer-detail.wxml` 中无 `<button open-type="share">` 按钮
3. 底部操作栏仅有"问问助手"和"备注"两个按钮,无分享入口
4. 全文搜索 `share` 关键词在 customer-detail 目录下无匹配
### 证据
```html
<!-- customer-detail.wxml — 底部操作栏,无分享按钮 -->
<view class="bottom-bar safe-area-bottom">
<view class="btn-chat" bindtap="onStartChat">问问助手</view>
<view class="btn-note" bindtap="onAddNote">备注</view>
</view>
```
### 建议
- 如需实现分享功能,需在 `customer-detail.ts` 中添加 `onShareAppMessage()` 方法
- 可在底部操作栏或页面右上角添加分享按钮
- 分享内容应包含客户基本信息脱敏后的姓名、ID接收方打开后跳转到客户详情页
- 注意:分享内容中不应包含敏感信息(完整手机号、余额等)

View File

@@ -0,0 +1,41 @@
# P9→NS1/RNS1 缺失项 #16助教详情页的联系方式展示
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 助教详情页未展示任何联系方式(电话/微信),后端也未返回相关字段
## 详细审查
### 审查范围
- `apps/backend/app/services/coach_service.py``get_coach_detail()` 返回字段
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml` — 页面模板
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts` — 页面逻辑
### 发现
1. 后端 `get_coach_detail()` 返回的字段中无 `phone``mobile``wechat` 等联系方式字段
2. 后端 `fdw_queries.get_assistant_info()` 返回的 `assistant_info` 中仅使用了 `name``avatar``level``skills``work_years``hire_date`
3. 前端 WXML 中 Banner 区域仅展示:头像、姓名、等级标签、技能标签、工龄、客户数
4. 全文搜索 `phone|mobile|wechat|微信|电话|联系` 在 coach-detail 目录下无匹配
### 证据
```python
# coach_service.py — get_coach_detail() 返回值,无联系方式
return {
"id": coach_id,
"name": assistant_info.get("name", ""),
"avatar": assistant_info.get("avatar", ""),
"level": ...,
"skills": ...,
"work_years": ...,
"customer_count": ...,
"hire_date": ...,
# 无 phone / wechat 字段
}
```
### 建议
- 如需展示联系方式,需确认数据来源(飞球 SaaS 是否提供助教手机号/微信号)
- 后端需在 `get_assistant_info()` 查询中增加联系方式字段
- 前端在 Banner 区域或"更多信息"卡片中添加联系方式展示
- 联系方式应做脱敏处理,提供"点击查看"交互

View File

@@ -0,0 +1,46 @@
# P9→NS1/RNS1 缺失项 #17消费记录的时间范围筛选
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- 仅支持按月切换(上/下月箭头),未实现自定义日期范围筛选
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts` — 页面逻辑
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.wxml` — 页面模板
- `apps/backend/app/services/customer_service.py``get_customer_records()` API
### 发现
1. 前端仅实现了 `onPrevMonth()``onNextMonth()` 两个月份切换方法
2. WXML 中月份切换 UI 为左右箭头 + 月份标签(如"2026年3月"),无日期选择器
3. 后端 `get_customer_records()` 接收 `year``month` 参数,按月查询
4. 无自定义日期范围的 API 参数(如 `start_date``end_date`
5. 月份范围有边界控制:`minYearMonth`(数据起始)到 `maxYearMonth`(当前月)
### 证据
```html
<!-- customer-service-records.wxml — 仅月份切换 -->
<view class="month-switcher">
<view class="month-btn" bindtap="onPrevMonth">
<t-icon name="chevron-left" />
</view>
<text class="month-label">{{monthLabel}}</text>
<view class="month-btn" bindtap="onNextMonth">
<t-icon name="chevron-right" />
</view>
</view>
```
```typescript
// customer-service-records.ts — 仅按月请求
async loadMonthRecords(customerId: string, year: number, month: number) { ... }
```
### 建议
- 如需自定义日期范围,需:
1. 前端添加日期范围选择器组件(如 TDesign `t-date-time-picker`
2. 后端 `get_customer_records()` 增加 `start_date` / `end_date` 可选参数
3. 月份切换和自定义范围可共存,自定义范围优先级更高
- 当前按月切换已满足基本需求,自定义范围为增强功能,优先级可后置

View File

@@ -0,0 +1,46 @@
# P9→NS1/RNS1 缺失项 #18详情页的返回按钮行为
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟡 低
- customer-service-records 页面有明确的 `navigateBack` 返回逻辑customer-detail 和 coach-detail 页面依赖小程序默认导航栏返回,未自定义返回行为
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.ts`
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml`
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts`
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml`
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts`
### 发现
1. `customer-service-records.ts` 定义了 `onBack()` 方法,调用 `wx.navigateBack()`
2. `customer-detail.ts``coach-detail.ts` 均未定义自定义返回方法
3. 两个详情页的 WXML 中均无自定义返回按钮,依赖微信小程序默认导航栏的返回按钮
4. 默认导航栏返回行为是 `navigateBack`(返回上一页),而非 `navigateTo`(返回列表页)
5. 如果用户通过分享链接直接进入详情页(页面栈为空),默认返回按钮无法返回列表页
### 证据
```typescript
// customer-service-records.ts — 有明确返回逻辑
onBack() {
wx.navigateBack()
}
// customer-detail.ts — 无返回方法
// coach-detail.ts — 无返回方法
```
### 建议
- 对于通过分享链接直接进入的场景,建议在详情页添加返回兜底逻辑:
```typescript
onBack() {
if (getCurrentPages().length > 1) {
wx.navigateBack()
} else {
wx.redirectTo({ url: '/pages/customer-list/customer-list' })
}
}
```
- 当前依赖默认导航栏返回在正常导航流程中可正常工作,仅在直接进入场景有问题

View File

@@ -0,0 +1,47 @@
# P9→NS1/RNS1 缺失项 #19客户详情页电话号码的脱敏展示
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟢 低
- 后端已实现 `_mask_phone()` 脱敏函数(中间 4 位用 `****`),前端实现了"点击查看/复制"交互
## 详细审查
### 审查范围
- `apps/backend/app/services/customer_service.py``_mask_phone()` 函数、`get_customer_detail()` 返回
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 电话展示区域
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.ts` — 电话交互逻辑
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts` — 同样的脱敏逻辑
### 发现
1. 后端 `_mask_phone()` 实现:`phone[:3] + "****" + phone[-4:]`,如 `139****5678`
2. 后端 `get_customer_detail()` 同时返回 `phone`(脱敏)和 `phone_full`(完整),供前端按需展示
3. customer-detail 前端实现了 `phoneVisible` 状态切换:
- 默认显示脱敏号码 `138****5678`
- 点击"查看"按钮切换为完整号码
- 显示完整号码后按钮变为"复制",调用 `wx.setClipboardData`
4. customer-service-records 页面也实现了相同的脱敏+查看交互:
- 前端自行脱敏:`detail.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')`
- 同样有 `onTogglePhone()``onCopyPhone()` 方法
### 证据
```python
# customer_service.py
def _mask_phone(phone: str | None) -> str:
"""手机号脱敏139****5678 格式。"""
if not phone or len(phone) < 7:
return phone or ""
return f"{phone[:3]}****{phone[-4:]}"
```
```html
<!-- customer-detail.wxml -->
<text class="phone">{{phoneVisible ? detail.phone : '138****5678'}}</text>
<view class="phone-toggle-btn" bindtap="{{phoneVisible ? 'onCopyPhone' : 'onTogglePhone'}}">
<text class="phone-toggle-text">{{phoneVisible ? '复制' : '查看'}}</text>
</view>
```
### 建议
- customer-detail.wxml 中脱敏号码硬编码为 `138****5678`,应改为使用后端返回的 `phone` 字段(已脱敏)
- 建议统一脱敏策略:要么全部由后端脱敏,要么全部由前端脱敏,避免两端重复实现

View File

@@ -0,0 +1,43 @@
# P9→NS1/RNS1 缺失项 #20助教详情页"收入明细"的展开/折叠交互
## 简要结论
- 状态:⚠️ 部分解决
- 风险等级:🟡 低
- 实现了本月/上月 Tab 切换交互,但不是 P9 定义的展开/折叠交互4 项收入明细始终全部展示
## 详细审查
### 审查范围
- `apps/backend/app/services/coach_service.py``_build_income()` 返回结构
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts``switchIncomeTab()`
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml` — 收入明细区域
### 发现
1. 后端 `_build_income()` 返回 `this_month``last_month` 各 4 项:基础课时费、激励课时费、充值提成、酒水提成
2. 前端实现了 Tab 切换交互(`onIncomeTabTap``switchIncomeTab`),在"本月"和"上月"之间切换
3. 切换后重新计算合计金额并更新 `currentIncome``incomeTotal`
4. 收入明细列表始终全部展示 4 项,无展开/折叠逻辑
5. P9 定义的"展开/折叠"交互未实现——当前是 Tab 切换而非折叠面板
### 证据
```html
<!-- coach-detail.wxml — Tab 切换,非展开/折叠 -->
<view class="income-tabs">
<view class="income-tab {{incomeTab === 'this' ? 'active' : ''}}" data-tab="this" bindtap="onIncomeTabTap">
<text>本月</text><text class="income-tab-est" wx:if="{{incomeTab === 'this'}}">预估</text>
</view>
<view class="income-tab {{incomeTab === 'last' ? 'active' : ''}}" data-tab="last" bindtap="onIncomeTabTap">
<text>上月</text>
</view>
</view>
<!-- 收入列表始终全部展示 -->
<view class="income-list">
<view class="income-item" wx:for="{{currentIncome}}" wx:key="label">...</view>
<view class="income-total">...</view>
</view>
```
### 建议
- 当前 Tab 切换交互在功能上已满足"查看本月/上月收入"的需求
- 如需严格对齐 P9 的展开/折叠设计,可在收入明细区域添加折叠逻辑:默认仅显示合计,点击展开显示 4 项明细
- 考虑到仅 4 项数据,全部展示的体验可能优于折叠,建议与产品确认是否需要折叠

View File

@@ -0,0 +1,42 @@
# P9→NS1/RNS1 缺失项 #21客户服务记录中"饮品描述"字段的展示
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟢 低
- `drinks` 字段已在数据模型、API 传递、组件属性、组件模板中完整贯通
## 详细审查
### 审查范围
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts``ServiceRecord` 接口、数据映射
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.wxml` — 组件调用
- `apps/miniprogram/miniprogram/components/service-record-card/service-record-card.ts` — 组件属性
- `apps/miniprogram/miniprogram/components/service-record-card/service-record-card.wxml` — 组件模板
### 发现
1. `customer-service-records.ts``ServiceRecord` 接口定义了 `drinks: string`(注释:商品/饮品描述)
2. 数据映射中从 API 响应提取:`drinks: r.drinks || ''`
3. WXML 中通过 `service-record-card` 组件传递:`drinks="{{item.drinks}}"`
4. `service-record-card` 组件定义了 `drinks` 属性:`{ type: String, value: '' }`
5. 组件模板中在第二行展示:`<text class="svc-drinks">{{drinks || '—'}}</text>`,无数据时显示破折号
### 证据
```typescript
// customer-service-records.ts — ServiceRecord 接口
interface ServiceRecord {
/** 商品/饮品描述 */
drinks: string
// ...
}
// 数据映射
drinks: r.drinks || '',
```
```html
<!-- customer-service-records.wxml — 传递给组件 -->
<service-record-card drinks="{{item.drinks}}" ... />
<!-- service-record-card.wxml — 展示 -->
<text class="svc-drinks">{{drinks || '—'}}</text>
```

View File

@@ -0,0 +1,68 @@
# P9→NS1/RNS1 缺失项 #22详情页各模块的加载失败独立处理
## 简要结论
- 状态:✅ 已解决
- 风险等级:🟢 低
- 后端对每个扩展模块使用独立 try/except 优雅降级;前端 customer-service-records 也实现了模块级独立错误处理
## 详细审查
### 审查范围
- `apps/backend/app/services/customer_service.py``get_customer_detail()` 错误处理
- `apps/backend/app/services/coach_service.py``get_coach_detail()` 错误处理
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts` — 前端错误处理
### 发现
#### 后端 — customer_service.py
1. 核心字段member_info查询失败 → 直接抛 500/404
2. Banner 字段balance、consumption_60d、days_since_visit各自独立 try/except失败降级为 `None`
3. 扩展模块ai_insight、retention_clues、notes、consumption_records、coach_tasks、favorite_coaches各自独立 try/except失败降级为空默认值
4. 注释明确标注:"核心字段查询失败 → 500扩展模块查询失败 → 空默认值(优雅降级)"
#### 后端 — coach_service.py
1. 核心字段assistant_info查询失败 → 直接抛 500/404
2. 扩展模块income、tier_nodes、top_customers、service_records、task_groups、notes、history_months各自独立 try/except
3. 每个模块失败时 logger.warning 记录日志,降级为空默认值
#### 前端 — customer-service-records.ts
1. `loadCustomerInfo()` 失败不阻塞记录展示:`catch` 中仅 `console.error`,注释"客户信息加载失败不阻塞记录展示"
2. `loadMonthRecords()` 失败设置 `pageState: 'error'`,提供重试按钮
### 证据
```python
# customer_service.py — 每个模块独立 try/except
try:
ai_insight = _build_ai_insight(customer_id, conn)
except Exception:
logger.warning("构建 aiInsight 失败,降级为空", exc_info=True)
ai_insight = {"summary": "", "strategies": []}
try:
retention_clues = _build_retention_clues(customer_id, conn)
except Exception:
logger.warning("构建 retentionClues 失败,降级为空列表", exc_info=True)
retention_clues = []
```
```python
# coach_service.py — 同样的模式
try:
income = _build_income(conn, site_id, coach_id, now)
except Exception:
logger.warning("构建 income 失败,降级为空", exc_info=True)
income = {"this_month": [], "last_month": []}
```
```typescript
// customer-service-records.ts — 客户信息加载失败不阻塞
async loadCustomerInfo(id: string) {
try { ... } catch (err) {
console.error('[customer-service-records] loadCustomerInfo failed:', err)
// 客户信息加载失败不阻塞记录展示
}
}
```
### 建议
- 前端 customer-detail.ts 的 `loadDetail()` 目前是单一 try/catch所有数据在一个请求中获取。如果后端某个模块降级为空前端会正常展示空状态但如果整个 API 请求失败,所有模块都不可用。这是可接受的设计,因为核心数据和扩展数据在同一个 API 调用中返回。

View File

@@ -0,0 +1,59 @@
# P9→NS1/RNS1 缺失项 #23客户详情页"可用月数"的计算说明
## 简要结论
- 状态:❌ 未解决
- 风险等级:🟡 低
- `available_months` 字段仅在客户看板 Schema 中定义,客户详情页 API 未返回该字段,计算公式未在代码中实现
## 详细审查
### 审查范围
- `apps/backend/app/services/customer_service.py``get_customer_detail()` 返回字段
- `apps/backend/app/schemas/xcx_board.py` — Schema 定义
- `apps/backend/app/services/board_service.py` — 看板服务
- `apps/backend/app/services/fdw_queries.py` — 数据查询
### 发现
1. `xcx_board.py` 中定义了 `available_months: str # "约0.8个月"``monthly_consume: float`
2. `fdw_queries.py` 中查询了 `monthly_consume` 字段(来自 `v_dws_member_consumption_summary`
3. 但在整个后端代码中,未找到 `available_months` 的计算赋值逻辑——仅有 Schema 定义
4. `customer_service.py``get_customer_detail()` 返回字段中无 `available_months`
5. 客户详情页前端也未展示"可用月数"字段
6. P9 定义的计算公式(余额 ÷ 月均消耗)和边界处理(月均消耗为 0 时)均未实现
### 证据
```python
# xcx_board.py — 仅有 Schema 定义
class CustomerBoardItem(BaseModel):
monthly_consume: float
available_months: str # "约0.8个月"
# customer_service.py — get_customer_detail() 返回值中无 available_months
return {
"id": customer_id,
"name": name,
"phone": phone,
"balance": balance,
"consumption_60d": consumption_60d,
"days_since_visit": days_since_visit,
# 无 available_months
}
```
```
# 全局搜索 available_months 赋值逻辑 → 无结果
# 仅在 Schema 定义中出现
```
### 建议
1.`customer_service.py``board_service.py` 中实现计算逻辑:
```python
def _calc_available_months(balance: float, monthly_consume: float) -> str:
if monthly_consume <= 0:
return "—" # 月均消耗为 0无法计算
months = balance / monthly_consume
return f"约{months:.1f}个月"
```
2. 需要确认 `monthly_consume` 的口径:是 60 天月均还是历史全量月均
3. 边界处理:余额为 0 → "0个月";月均消耗为 0 → 显示"—"或"充足"
4. 将计算结果添加到 `get_customer_detail()` 返回值中,前端在 Banner 统计区域展示

View File

@@ -0,0 +1,288 @@
# Spec 审查报告Neo_Specs 遗漏需求细节
> 审查日期2026-03-20
> 审查方法:以 `docs/prd/specs/` 中 P5.1、P6-P10 为标杆,逐项对照 `docs/prd/Neo_Specs/` 中 NS1/NS3/NS4/RNS1识别 Neo_Specs 中遗漏的需求细节
> 审查范围6 个标杆文件 × 对应 Neo_Specs 文件
> 规则NS1/RNS1 中新增的需求(标杆文件没有的)不算遗漏,仅关注标杆文件中有但 Neo_Specs 中缺失的内容
---
## 一、总览汇总
| 标杆文件 | 对照文件 | 缺失项总数 | 🔴 高 | 🟠 中 | 🟡 低 |
|---------|---------|:----------:|:-----:|:-----:|:-----:|
| P5.1MCP Server AI 扩展) | NS3 | 10 | 2 | 5 | 3 |
| P6小程序前端-任务模块) | NS1 + RNS1 | 18 | 6 | 8 | 4 |
| P7小程序前端-绩效模块) | NS1 + RNS1 | 12 | 3 | 5 | 4 |
| P8小程序前端-看板模块) | NS1 + RNS1 | 14 | 4 | 5 | 5 |
| P9小程序前端-详情模块) | NS1 + RNS1 | 23 | 4 | 10 | 9 |
| P10租户管理后台 | NS4 | 9 | 0 | 4 | 5 |
| **合计** | | **86** | **19** | **37** | **30** |
---
## 二、高风险缺失项汇总(🔴 高,共 19 项)
以下缺失项可能导致功能不完整、联调失败或数据错误,建议优先补充。
### P5.1 → NS32 项)
| # | 缺失内容 | 影响 |
|---|---------|------|
| 1 | App1智能任务生成的完整 Prompt 工程规范P5.1 定义了 system prompt 模板、few-shot 示例、输出 JSON schema 约束NS3 仅提及"调用 LLM 生成任务"无 prompt 细节 | 生成质量不可控,无法验收 |
| 2 | App2财务洞察的指标计算口径P5.1 明确了 6 个财务指标的计算公式(含 items_sum 口径、环比基准NS3 仅列出"生成财务分析报告" | 财务数据可能计算错误 |
### P6 → NS1/RNS16 项)
| # | 缺失内容 | 影响 |
|---|---------|------|
| 1 | 任务卡片的视觉状态机P6 定义了 5 种卡片状态(待处理/已置顶/已完成/已放弃/过期的颜色、图标、交互差异NS1/RNS1 仅按 status 分组返回数据 | 前端无法正确渲染状态差异 |
| 2 | 任务列表的空状态设计P6 定义了 3 种空状态(无任务/筛选无结果/网络错误的文案和插图NS1/RNS1 未提及 | 空状态体验缺失 |
| 3 | 置顶任务的排序规则P6 明确"置顶任务按置顶时间倒序,非置顶按优先级→创建时间排序"NS1 TASK-1 未定义排序规则 | 列表排序可能不符合预期 |
| 4 | 任务操作的确认弹窗规范P6 定义了放弃/取消放弃操作的二次确认弹窗文案和按钮样式NS1/RNS1 未提及前端交互细节 | 误操作风险 |
| 5 | 下拉刷新与触底加载的动画规范P6 定义了 loading 动画、skeleton 屏、错误重试的交互细节NS1/RNS1 仅提及"分页"和"懒加载" | 加载体验不一致 |
| 6 | 任务详情页的 AI 分析展示规范P6 定义了 AI 分析卡片的折叠/展开交互、"重新生成"按钮、加载状态NS1 TASK-2 仅定义了 aiAnalysis 数据字段 | AI 功能交互不完整 |
### P7 → NS1/RNS13 项)
| # | 缺失内容 | 影响 |
|---|---------|------|
| 1 | 营业日分割点08:00的完整处理规范P7 多次强调"营业日以 08:00 为分割点"并给出具体示例("本月"= 当月1日 08:00 ~ 次月1日 08:00NS1/RNS1 的 PERF-1/PERF-2 使用 `biz_date` 查询但未说明 biz_date 是否已按 08:00 分割 | 跨日数据归属可能错误(如凌晨 2 点的服务归属哪天) |
| 2 | "预估"标记的完整规范P7 AC7 和 T7 明确要求"当月数据显示预估标记"NS1 PERF-1 提到 `isEstimate` 字段但未定义判断逻辑(什么条件下标记为预估?当月所有数据都是预估?还是仅未结算的?) | 预估标记逻辑不明确 |
| 3 | 定档折算惩罚的展示格式P7 AC6 明确要求"120分钟定档折算30分钟"格式NS1 PERF-2 提到 `hoursRaw`(折算前)和 `hours`(折算后)但未定义前端展示格式 | 折算信息展示不一致 |
### P8 → NS1/RNS14 项)
| # | 缺失内容 | 影响 |
|---|---------|------|
| 1 | 看板页面的 Tab 切换交互P8 定义了助教/客户/财务三个 Tab 的切换动画、缓存策略切换回来保持筛选状态NS1/RNS1 仅定义了 API 参数 | Tab 切换体验不完整 |
| 2 | 财务看板的数据加载策略P8 定义了"首次加载经营一览,其他板块懒加载"的分段加载策略NS1 BOARD-3 是单个 API 返回全部 6 板块数据 | 首屏加载慢200+ 字段一次返回) |
| 3 | 客户看板卡片的点击跳转P8 定义了"点击客户卡片跳转到 customer-detail 页面"的交互NS1/RNS1 BOARD-2 未定义卡片点击行为和跳转参数 | 看板→详情的导航链路断裂 |
| 4 | 助教看板的"距升档"进度条P8 定义了升档进度的可视化展示(进度条 + 百分比 + 差距小时数NS1 BOARD-1 仅返回 `perfGap` 文本字段 | 进度可视化缺失 |
### P9 → NS1/RNS14 项)
| # | 缺失内容 | 影响 |
|---|---------|------|
| 1 | 客户详情页的数据加载顺序P9 定义了"基本信息→消费汇总→AI 洞察→消费记录→备注"的分段加载策略和 skeleton 占位NS1 CUST-1 是单个 API 返回全部数据 | 首屏加载慢 |
| 2 | 助教详情页的档位进度可视化P9 定义了档位节点tierNodes的时间轴样式、当前档位高亮、升档动画NS1 COACH-1 仅返回 tierNodes 数据 | 档位展示缺乏视觉引导 |
| 3 | 消费记录的类型图标映射P9 定义了台桌消费/商城消费/充值 3 种类型的图标、颜色、标签样式NS1 CUST-1 仅返回 `type` 字段 | 消费类型视觉区分不明确 |
| 4 | 备注的 AI 评分展示P9 定义了备注卡片上 AI 评分的星级展示1-5 星)和评分说明 tooltipNS1 仅返回 `ai_score` 数值 | 评分展示不直观 |
---
## 三、各标杆文件详细对比
### 3.1 P5.1MCP Server AI 扩展vs NS3
P5.1 定义了 MCP Server 的 7 个 AI 应用App1-App7的完整规范NS3 覆盖了整体架构但在以下细节上有遗漏。
| # | 风险 | 缺失内容 | P5.1 位置 | NS3 现状 | 补充建议 |
|---|:----:|---------|----------|---------|---------|
| 1 | 🔴 | App1 Prompt 工程规范system prompt 模板、few-shot 示例、输出 JSON schema | §App1 详细设计 | 仅提及"调用 LLM" | 补充完整 prompt 模板和输出约束 |
| 2 | 🔴 | App2 财务指标计算口径6 个指标公式、items_sum 口径、环比基准) | §App2 详细设计 | 仅列出"生成财务分析" | 补充指标公式,引用 DWD-DOC 口径规范 |
| 3 | 🟠 | App3 维客线索生成的触发条件和频率(每日/每周/事件驱动) | §App3 调度策略 | 未定义触发机制 | 补充调度策略和去重逻辑 |
| 4 | 🟠 | App4-App7 的缓存策略cache_type 枚举、过期时间、失效条件) | §缓存设计 | 提及 ai_cache 表但未定义策略 | 补充各 App 的缓存 TTL 和失效触发条件 |
| 5 | 🟠 | LLM 调用的错误处理和降级策略(超时/限流/模型不可用时的回退) | §异常处理 | 未提及 | 补充降级策略(如返回缓存结果或提示用户) |
| 6 | 🟠 | Token 用量监控和成本控制(单次调用上限、日/月预算、超限处理) | §成本控制 | 未提及 | 补充 token 预算和告警机制 |
| 7 | 🟠 | App5话术生成的话术模板分类和质量评估标准 | §App5 详细设计 | 仅提及"生成话术" | 补充话术分类(召回/维护/推荐)和评估维度 |
| 8 | 🟡 | 各 App 的单元测试用例设计(输入/输出示例、边界条件) | §测试策略 | 未提及 | 补充关键 App 的测试用例 |
| 9 | 🟡 | MCP Server 的健康检查端点和监控指标 | §运维 | 未提及 | 补充 /health 端点和关键指标(延迟/成功率/token 用量) |
| 10 | 🟡 | AI 生成内容的审计日志(谁在什么时候对哪个客户生成了什么) | §审计 | 未提及 | 补充审计日志表或字段 |
---
### 3.2 P6小程序前端-任务模块vs NS1/RNS1
P6 定义了任务列表和任务详情两个页面的完整前端规范NS1/RNS1 主要关注后端 API 设计,以下前端交互细节有遗漏。
| # | 风险 | 缺失内容 | P6 位置 | NS1/RNS1 现状 | 补充建议 |
|---|:----:|---------|--------|--------------|---------|
| 1 | 🔴 | 任务卡片 5 种状态的视觉规范(颜色/图标/交互差异) | §任务卡片设计 | 仅按 status 分组返回 | 在契约或前端 spec 中补充状态→样式映射表 |
| 2 | 🔴 | 3 种空状态设计(无任务/筛选无结果/网络错误)的文案和插图 | §空状态 | 未提及 | 补充空状态 UI 规范 |
| 3 | 🔴 | 置顶任务排序规则(置顶按时间倒序,非置顶按优先级→创建时间) | §排序逻辑 | TASK-1 未定义排序 | 在 API 契约中明确 ORDER BY 规则 |
| 4 | 🔴 | 放弃/取消放弃的二次确认弹窗文案和按钮样式 | §操作确认 | 未提及交互细节 | 补充确认弹窗规范 |
| 5 | 🔴 | 下拉刷新/触底加载的动画规范loading/skeleton/错误重试) | §加载状态 | 仅提及"分页""懒加载" | 补充加载状态 UI 规范 |
| 6 | 🔴 | AI 分析卡片的折叠/展开交互、"重新生成"按钮、加载状态 | §AI 分析展示 | TASK-2 仅定义数据字段 | 补充 AI 卡片交互规范 |
| 7 | 🟠 | 任务优先级的视觉标识(高/中/低优先级的颜色和图标) | §优先级展示 | 未提及优先级视觉 | 补充优先级→样式映射 |
| 8 | 🟠 | 任务到期倒计时的展示规则(距到期 N 天的颜色变化) | §到期提醒 | 未提及 | 补充倒计时展示规则 |
| 9 | 🟠 | 备注输入框的字数限制和实时计数 | §备注交互 | createNote 仅定义参数 | 补充输入限制(如 500 字) |
| 10 | 🟠 | 任务详情页各模块的折叠/展开默认状态 | §详情布局 | 未提及 | 补充各模块默认展开/折叠状态 |
| 11 | 🟠 | 维客线索的展示样式tag 颜色映射、线索卡片布局) | §维客线索 | 定义了 tag 格式但未定义样式 | 补充 tag 颜色和卡片样式 |
| 12 | 🟠 | 任务列表页的搜索功能(按客户名/手机号搜索) | §搜索 | 未提及搜索功能 | 确认是否需要搜索,如需则补充 |
| 13 | 🟠 | 任务完成后的成功反馈动画 | §操作反馈 | 未提及 | 补充成功/失败的 toast 规范 |
| 14 | 🟠 | 网络异常时的离线提示和重试机制 | §异常处理 | 未提及 | 补充网络异常 UI 和重试策略 |
| 15 | 🟡 | 任务卡片的长按/滑动操作(如滑动删除、长按置顶) | §手势交互 | 未提及 | 确认是否需要手势操作 |
| 16 | 🟡 | 页面切换时的转场动画规范 | §转场 | 未提及 | 补充页面转场动画类型 |
| 17 | 🟡 | 任务列表的批量操作(如批量标记完成) | §批量操作 | 未提及 | 确认是否需要批量操作 |
| 18 | 🟡 | 无障碍适配(屏幕阅读器标签、焦点顺序) | §无障碍 | 未提及 | 补充关键元素的 aria 标签 |
---
### 3.3 P7小程序前端-绩效模块vs NS1/RNS1
P7 定义了绩效概览和绩效明细两个页面的完整规范NS1 §3.3 和 RNS1 T1-3/T1-4 覆盖了 API 设计,但以下细节有遗漏。
| # | 风险 | 缺失内容 | P7 位置 | NS1/RNS1 现状 | 补充建议 |
|---|:----:|---------|--------|--------------|---------|
| 1 | 🔴 | 营业日 08:00 分割点的完整处理规范 | AC2、页面清单多处 | PERF-1/PERF-2 用 `biz_date` 查询但未说明 biz_date 是否已按 08:00 分割 | 在 API 契约中明确biz_date 已由 ETL 层按 08:00 分割,后端直接按 biz_date 查询即可;或补充后端时间转换逻辑 |
| 2 | 🔴 | "预估"标记的判断逻辑 | AC7、T7 | PERF-1 有 `isEstimate` 字段但未定义判断条件 | 补充:当月所有数据标记为预估?还是仅未结算的?还是基于 ETL 数据更新时间? |
| 3 | 🔴 | 定档折算惩罚的展示格式规范 | AC6 | PERF-2 返回 `hours`/`hoursRaw` 但未定义展示格式 | 补充:前端展示为"120分钟定档折算30分钟"格式,明确 hours 和 hoursRaw 的关系 |
| 4 | 🟠 | "我的新客"筛选逻辑的完整定义 | AC3 | NS1 §3.3 提及 `dws_assistant_customer_stats` 但未定义"首次服务 + 2月内 + 服务次数≤2"的 SQL 条件 | 补充新客筛选的 WHERE 条件 |
| 5 | 🟠 | "我的常客"的展示字段(次数、小时数、工资合计) | AC4 | NS1 §3.3 提及数据源但未定义响应字段 | 补充常客列表的响应 schema |
| 6 | 🟠 | 收入与业绩档位卡片的视觉设计 | §performance 页面 | RNS1 T1-3 定义了数据字段但未定义卡片布局 | 补充卡片 UI 规范(进度条、档位标签、收入数字样式) |
| 7 | 🟠 | 服务记录按天归总的展示格式 | §performance 页面 | RNS1 PERF-1 定义了 DateGroup 结构但未定义日期标签格式 | 补充:日期标签格式(如"3月15日 周五")、每日汇总行样式 |
| 8 | 🟠 | 本月/上月切换的交互细节 | AC2 | RNS1 T1-6 提到 F8月份切换但未定义切换动画和数据刷新策略 | 补充切换交互规范 |
| 9 | 🟡 | 绩效页面的空状态(新助教无数据时的展示) | 隐含 | 未提及 | 补充空状态文案 |
| 10 | 🟡 | 业绩明细的导出功能(如导出 Excel | 隐含 | 未提及 | 确认是否需要导出功能 |
| 11 | 🟡 | 绩效数据的刷新频率说明(实时/每日/每次 ETL 后) | 隐含 | 未提及 | 补充数据新鲜度说明 |
| 12 | 🟡 | 业绩明细页的口径选择交互(本月/上月/本周/上周等) | §performance-records | RNS1 L3 定义了月份切换但 P7 还提到"本周/上周"口径NS1/RNS1 未覆盖周口径 | 确认是否需要周口径,如需则补充 API 参数 |
---
### 3.4 P8小程序前端-看板模块vs NS1/RNS1
P8 定义了三个看板页面(助教/客户/财务的完整前端规范NS1 八¾节和 RNS1 RNS1.3 覆盖了 API 和筛选矩阵,但以下前端细节有遗漏。
| # | 风险 | 缺失内容 | P8 位置 | NS1/RNS1 现状 | 补充建议 |
|---|:----:|---------|--------|--------------|---------|
| 1 | 🔴 | 三看板 Tab 切换的缓存策略(切换回来保持筛选状态和滚动位置) | §Tab 交互 | 未提及 Tab 缓存 | 补充 Tab 切换时的状态保持策略 |
| 2 | 🔴 | 财务看板分段加载策略(首次加载经营一览,其他板块懒加载) | §加载策略 | BOARD-3 单个 API 返回全部 6 板块 | 考虑拆分 API 或补充前端分段渲染策略 |
| 3 | 🔴 | 客户看板卡片点击跳转到 customer-detail 的交互和参数传递 | §卡片交互 | BOARD-2 未定义点击行为 | 补充卡片点击→跳转的参数customerId |
| 4 | 🔴 | 助教看板"距升档"进度条的可视化规范(进度条+百分比+差距小时数) | §进度展示 | BOARD-1 仅返回 `perfGap` 文本 | 补充进度条 UI 规范,或增加 `perfProgress` 百分比字段 |
| 5 | 🟠 | 看板数据的实时性标识(数据截止时间展示) | §数据时效 | 未提及 | 补充"数据更新于 XX:XX"的展示 |
| 6 | 🟠 | 财务看板环比数据的 tooltip 说明(点击环比箭头显示计算详情) | §环比交互 | 定义了环比格式但未定义交互 | 补充环比 tooltip 规范 |
| 7 | 🟠 | 助教看板卡片点击跳转到 coach-detail 的交互 | §卡片交互 | BOARD-1 未定义点击行为 | 补充卡片点击→跳转的参数coachId |
| 8 | 🟠 | 客户看板"最频繁"维度的柱状图交互(点击柱子显示具体数据) | §柱状图 | 定义了 weeklyVisits 数据但未定义交互 | 补充柱状图点击交互 |
| 9 | 🟠 | 看板页面的下拉刷新行为 | §刷新 | 未提及 | 补充下拉刷新规范 |
| 10 | 🟡 | 财务看板各板块的折叠/展开交互 | §板块交互 | 未提及 | 补充板块折叠/展开默认状态 |
| 11 | 🟡 | 看板数据加载失败时的错误展示 | §异常处理 | 未提及 | 补充错误状态 UI |
| 12 | 🟡 | 筛选项的动画效果(下拉展开/收起) | §筛选交互 | 未提及 | 补充筛选面板动画 |
| 13 | 🟡 | 助教看板的排名序号展示 | §排名 | 未提及 | 补充是否需要显示排名序号(如 #1#2 |
| 14 | 🟡 | 财务看板数字的格式化规范(千分位、小数位、货币符号) | §数字格式 | 未提及 | 补充数字格式化规则(如 ¥12,345.67 |
---
### 3.5 P9小程序前端-详情模块vs NS1/RNS1
P9 定义了客户详情、助教详情、客户服务记录三个页面的完整前端规范NS1 §3.4/§3.5 和 RNS1 RNS1.2 覆盖了 API 设计,但以下细节有遗漏。
| # | 风险 | 缺失内容 | P9 位置 | NS1/RNS1 现状 | 补充建议 |
|---|:----:|---------|--------|--------------|---------|
| 1 | 🔴 | 客户详情页分段加载策略基本信息→消费汇总→AI 洞察→消费记录→备注) | §加载策略 | CUST-1 单个 API 返回全部数据 | 考虑拆分 API 或补充前端分段渲染和 skeleton 占位规范 |
| 2 | 🔴 | 助教详情页档位进度时间轴的视觉规范(节点样式、当前档位高亮、升档动画) | §档位展示 | COACH-1 返回 tierNodes 数据但未定义视觉 | 补充时间轴 UI 规范 |
| 3 | 🔴 | 消费记录 3 种类型的图标/颜色/标签样式映射 | §消费记录 | CUST-1 仅返回 `type` 字段 | 补充 type→icon/color 映射表 |
| 4 | 🔴 | 备注 AI 评分的星级展示规范1-5 星样式、评分说明 tooltip | §备注展示 | 仅返回 `ai_score` 数值 | 补充星级 UI 和 tooltip 文案 |
| 5 | 🟠 | 客户详情页 Banner 区域的视觉设计(余额/消费/到店间隔/距上次到店的布局) | §Banner | RNS1 T2-1 定义了 4 个字段但未定义布局 | 补充 Banner UI 规范 |
| 6 | 🟠 | AI 洞察卡片的展示规范(标题/摘要/展开详情/刷新按钮) | §AI 洞察 | RNS1 T2-1 定义了 aiInsight 字段但未定义交互 | 补充 AI 洞察卡片 UI 和交互 |
| 7 | 🟠 | 关联助教任务列表的展示规范(任务类型图标、状态标签、服务统计) | §关联助教 | RNS1 T2-2 定义了数据字段但未定义展示 | 补充任务列表 UI 规范 |
| 8 | 🟠 | 最亲密助教的展示规范(关系指数可视化、课时统计图表) | §亲密助教 | RNS1 T2-3 定义了数据字段但未定义展示 | 补充亲密助教卡片 UI |
| 9 | 🟠 | 消费记录中助教明细子列表的展开/折叠交互 | §消费记录 | RNS1 定义了 coaches 子数组但未定义交互 | 补充子列表展开/折叠规范 |
| 10 | 🟠 | 助教详情页 TOP20 客户列表的排序规则和展示字段 | §TOP 客户 | RNS1 T2-5 定义了扩展字段但未定义排序 | 补充排序规则(按什么排?关系指数?消费金额?) |
| 11 | 🟠 | 助教详情页历史月份统计的图表展示(折线图/柱状图/表格) | §历史统计 | RNS1 T2-6 定义了数据字段但未定义展示形式 | 补充图表类型和交互 |
| 12 | 🟠 | 客户服务记录页的月度统计汇总展示monthCount/monthHours 的位置和样式) | §月度统计 | RNS1 T2-4 定义了字段但未定义展示 | 补充月度统计 UI 规范 |
| 13 | 🟠 | 助教详情页任务分组active/inactive/abandoned的视觉区分 | §任务分组 | RNS1 T2-5 定义了分组但未定义视觉 | 补充分组标题样式和折叠规则 |
| 14 | 🟠 | 客户详情页维客线索的展示规范线索卡片布局、category 颜色映射) | §维客线索 | RNS1 定义了脱敏规则但未定义展示 | 补充线索卡片 UI |
| 15 | 🟡 | 客户详情页的分享功能(分享客户信息给其他助教) | §分享 | 未提及 | 确认是否需要分享功能 |
| 16 | 🟡 | 助教详情页的联系方式展示(电话/微信) | §联系方式 | 未提及 | 确认是否需要展示联系方式 |
| 17 | 🟡 | 消费记录的时间范围筛选(除月份切换外是否需要自定义日期范围) | §时间筛选 | 仅支持月份切换 | 确认是否需要自定义日期范围 |
| 18 | 🟡 | 详情页的返回按钮行为(返回上一页 vs 返回列表页) | §导航 | 未提及 | 补充返回导航规则 |
| 19 | 🟡 | 客户详情页电话号码的脱敏展示(中间 4 位用 * | §隐私 | 未提及前端脱敏 | 补充:后端返回完整号码还是脱敏号码?前端是否需要"点击查看完整号码" |
| 20 | 🟡 | 助教详情页"收入明细"的展开/折叠交互 | §收入明细 | RNS1 定义了本月/上月各 4 项但未定义交互 | 补充收入明细展示规范 |
| 21 | 🟡 | 客户服务记录中"饮品描述"字段的展示位置和样式 | §服务记录 | NS1 L4 定义了 `drinks` 字段但未定义展示 | 补充饮品信息展示位置 |
| 22 | 🟡 | 详情页各模块的加载失败独立处理(某个模块失败不影响其他模块展示) | §异常处理 | 未提及 | 补充模块级错误处理策略 |
| 23 | 🟡 | 客户详情页"可用月数"的计算说明(余额÷月均消耗) | §余额分析 | RNS1 定义了 `availableMonths` 但未说明计算公式 | 补充计算公式和边界处理(月均消耗为 0 时) |
---
### 3.6 P10租户管理后台vs NS4
P10 定义了管理后台的完整规范React + Vite + Ant DesignNS4 覆盖了主要功能模块,但以下细节有遗漏。
| # | 风险 | 缺失内容 | P10 位置 | NS4 现状 | 补充建议 |
|---|:----:|---------|---------|---------|---------|
| 1 | 🟠 | 角色权限管理的 CRUD 界面规范(创建角色、分配权限、权限树展示) | §权限管理 | 提及权限模型但未定义管理界面 | 补充权限管理页面的 UI 和交互规范 |
| 2 | 🟠 | 门店切换功能的交互规范(多门店用户的门店选择器位置、切换后的数据刷新) | §门店管理 | 提及多门店但未定义切换交互 | 补充门店选择器 UI 和切换流程 |
| 3 | 🟠 | 数据导出功能的规范(哪些页面支持导出、导出格式、导出权限) | §数据导出 | 未提及 | 补充导出功能清单和权限控制 |
| 4 | 🟠 | 操作日志/审计日志的查看界面 | §审计 | 未提及 | 补充日志查看页面规范 |
| 5 | 🟡 | 管理后台的响应式适配(最小支持分辨率、移动端适配策略) | §响应式 | 未提及 | 补充最小分辨率和断点规则 |
| 6 | 🟡 | 表格组件的统一规范(分页大小、排序交互、筛选器位置) | §表格规范 | 未提及 | 补充 Ant Design Table 的统一配置 |
| 7 | 🟡 | 表单验证的统一规范(必填标识、错误提示位置、实时验证 vs 提交验证) | §表单规范 | 未提及 | 补充表单验证规则 |
| 8 | 🟡 | 管理后台的国际化预留(是否需要多语言支持) | §国际化 | 未提及 | 确认是否需要国际化预留 |
| 9 | 🟡 | 管理后台的主题定制品牌色、Logo 位置、暗色模式) | §主题 | 未提及 | 确认是否需要主题定制 |
---
## 四、跨文件系统性问题
以下问题在多个标杆文件中反复出现,反映了 Neo_Specs 整体层面的系统性缺失。
### 4.1 前端交互规范缺失(影响 P6/P7/P8/P9
NS1/RNS1 主要关注后端 API 设计和数据字段定义,对前端交互细节覆盖不足:
- 加载状态skeleton/loading/错误重试)未统一规范
- 空状态设计未覆盖
- 操作确认弹窗未规范
- 页面转场动画未定义
- 下拉刷新行为未统一
建议:新建一份前端交互规范文档(如 `NS-FE-INTERACTION.md`),统一定义上述交互模式,各页面 spec 引用即可。
### 4.2 数据加载策略缺失(影响 P8/P9
多个复杂页面(财务看板、客户详情、助教详情)的 API 设计为单个接口返回全部数据但标杆文件定义了分段加载策略。NS1/RNS1 未考虑:
- 首屏优先加载哪些模块
- 非首屏模块的懒加载触发条件
- 各模块独立的 loading/error 状态
建议:在 API 契约中为复杂页面提供分段加载方案(拆分 API 或定义 `fields` 参数控制返回范围)。
### 4.3 视觉样式映射缺失(影响 P6/P7/P8/P9
多处数据字段(状态、类型、等级、优先级)需要前端映射为视觉样式(颜色、图标、标签),但 NS1/RNS1 仅定义了数据字段,未提供映射表:
- 任务状态→颜色/图标
- 课程类型→标签样式
- 会员等级→标签颜色
- 助教等级→标签样式
- 消费类型→图标
建议:在 API 契约或前端规范中补充统一的枚举→样式映射表。
### 4.4 异常处理规范缺失(影响所有文件)
标杆文件中定义了各种异常场景的处理方式,但 Neo_Specs 普遍缺少:
- 网络异常时的 UI 展示和重试机制
- 单个模块加载失败时的降级策略
- 数据为空时的空状态展示
- 操作失败时的错误提示文案
建议:新建异常处理规范文档,统一定义错误码→用户提示的映射。
### 4.5 营业日时间规范未贯穿(影响 P7/P8
P7 多次强调"营业日以 08:00 为分割点"这个规则影响绩效和看板的所有时间相关查询。NS1/RNS1 使用 `biz_date` 字段但未明确说明 biz_date 的生成规则是否已包含 08:00 分割逻辑。
建议:在 API 契约中明确声明"所有 biz_date 字段已由 ETL 层按 08:00 营业日分割点处理,后端和前端无需额外转换"。
---
## 五、建议优先级
### 立即处理(阻塞开发)
1. 补充营业日 08:00 分割点的处理说明P7→NS1
2. 补充"预估"标记的判断逻辑P7→NS1
3. 补充任务排序规则到 API 契约P6→NS1
4. 评估复杂页面的分段加载策略P8/P9→NS1
### 短期补充(影响联调质量)
5. 新建前端交互规范文档,统一加载/空状态/确认弹窗/错误处理
6. 补充枚举→样式映射表
7. 补充 App1/App2 的 Prompt 工程和指标口径P5.1→NS3
8. 补充管理后台权限管理界面规范P10→NS4
### 后续完善(不阻塞但影响体验)
9. 补充各页面的无障碍适配
10. 补充数据导出功能规范
11. 补充页面转场动画规范

View File

@@ -1,22 +1,51 @@
# 百炼平台 AI 应用提示词Qwen3.5-Plus
> 本文档定义 8 个 AI 应用的系统提示词System Prompt供百炼平台配置使用。
> 模型Qwen3.5-Plus
> 模型Qwen3.5-Plus(应用 1、5/ Qwen3-Max-Preview应用 2、3、4、6、7、8
> 所有应用均启用深度思考模式enable_thinking=true
> 权威来源:`docs/prd/specs/P5-miniapp-ai-integration.md` + `docs/prd/AI需求2.md`
> 首条 Prompt用户消息由后端代码在调用时拼接 JSON 数据,结构定义见 P5 spec「首条 Prompt 数据结构」章节。
> NS2 实现代码:`apps/backend/app/ai/`data_fetchers + apps 子包),属性测试:`tests/test_data_fetchers/` + `tests/test_ai_apps/`
### 应用 ID 与环境变量映射2026-03-05 更新
> 最后同步时间2026-03-21从百炼控制台获取线上配置并对齐
| 应用 | 名称 | 环境变量 Key | 百炼应用 ID |
|------|------|-------------|------------|
| 应用 1 | 通用对话 | `BAILIAN_APP_ID_1_CHAT` | `979dabe6f22a43989632b8c662cac97c` |
| 应用 2 | 财务洞察 | `BAILIAN_APP_ID_2_FINANCE` | `1dcdb5f39c3040b6af8ef79215b9b051` |
| 应用 3 | 客户数据维客线索分析 | `BAILIAN_APP_ID_3_CLUE` | `708bf45439cd48c7ab9a514d03482890` |
| 应用 4 | 关系分析/任务建议 | `BAILIAN_APP_ID_4_ANALYSIS` | `ea7b1c374f574b9a925a2fb5789a9b90` |
| 应用 5 | 话术参考 | `BAILIAN_APP_ID_5_TACTICS` | `46f54e6053df4bb0b83be29366025cf6` |
| 应用 6 | 备注分析 | `BAILIAN_APP_ID_6_NOTE` | `025bb344146b4e4e8be30c444adab3b4` |
| 应用 7 | 客户分析 | `BAILIAN_APP_ID_7_CUSTOMER` | `df35e06991b24d49971c03c6428a9c87` |
| 应用 8 | 维客线索整理 | `BAILIAN_APP_ID_8_CONSOLIDATE` | `407dfb89283b4196934eec5fefe3ebc2` |
### NS2 后端实现要点2026-03-21 更新)
应用 1 支持 10 种页面入口上下文contextType`page_context.py` 文本化后注入首条消息:
| contextType | 入口页面 | 数据来源(代码实际查询) |
|-------------|---------|------------------------|
| `task-detail` | 任务详情 | App: `biz.coach_tasks` + `biz.coach_tasks_member_view` + `biz.coach_tasks_assistant_view` + `biz.notes` + `biz.ai_cache`(app4_analysis) |
| `task-list` | 任务列表 | App: `biz.coach_tasks`(按 status 分组统计;有 contextId 时复用 task-detail |
| `customer-detail` | 客户详情 | FDW: `fdw_etl.v_dim_member`(scd2_is_current=1) + `fdw_etl.v_dwd_settlement_head` + `fdw_etl.v_dws_member_consumption_summary`App: `member_retention_clue` |
| `coach-detail` | 助教详情 | FDW: `fdw_etl.v_dim_assistant`App: `biz.coach_tasks`(按 status 分组统计) |
| `board-finance` | 财务看板 | FDW: `fdw_etl.v_dwd_settlement_head`settle_type IN 1,3近 1 月汇总) |
| `board-customer` | 客户看板 | FDW: `fdw_etl.v_dwd_settlement_head` JOIN `fdw_etl.v_dim_member`Top 10 客户) |
| `board-coach` | 助教看板 | FDW: `fdw_etl.v_dwd_assistant_service_log` JOIN `fdw_etl.v_dim_assistant`Top 10 助教) |
| `performance` | 绩效页 | FDW: `fdw_etl.v_dws_assistant_salary_calc` JOIN `fdw_etl.v_dim_assistant` |
| `customer-service-records` | 服务记录 | FDW: `fdw_etl.v_dwd_assistant_service_log`is_trash=false近 10 条) |
| `my-profile` | 个人中心 | 无查询(静态文本:"当前为个人信息页面,可查询个人绩效和任务情况" |
应用 3-7 的 `biz_params` 注入机制:后端 `run()` 函数接收 `member_id`/`assistant_id`/`site_id` 等参数,通过 data_fetchers 层查询数据库拼接 JSON作为首条用户消息发送。
Token 预算约束:应用 3-7 的数据文本化后限制在 ≤8000 字符以内(单个 fetcher 输出),应用 1 页面上下文 ≤2000 字符(`MAX_PAGE_CONTEXT_LENGTH = 2000`),应用 1 system prompt 总长 ≤4000 字符(`_MAX_SYSTEM_PROMPT_LEN = 4000`)。超限时按优先级截断。
### 应用 ID 与环境变量映射2026-03-22 更新P14 DashScope 迁移)
| 应用 | 名称 | 环境变量 Key | 百炼应用 ID | 选用模型 | temperature |
|------|------|-------------|------------|----------|-------------|
| 应用 1 | 通用对话 | `DASHSCOPE_APP_ID_1_CHAT` | `979dabe6f22a43989632b8c662cac97c` | Qwen3.5-Plus | 0.7 |
| 应用 2 | 财务洞察 | `DASHSCOPE_APP_ID_2_FINANCE` | `1dcdb5f39c3040b6af8ef79215b9b051` | Qwen3-Max-Preview | 0 |
| 应用 3 | 客户数据维客线索分析 | `DASHSCOPE_APP_ID_3_CLUE` | `708bf45439cd48c7ab9a514d03482890` | Qwen3-Max-Preview | 0 |
| 应用 4 | 关系分析/任务建议 | `DASHSCOPE_APP_ID_4_ANALYSIS` | `ea7b1c374f574b9a925a2fb5789a9b90` | Qwen3-Max-Preview | 0 |
| 应用 5 | 话术参考 | `DASHSCOPE_APP_ID_5_TACTICS` | `46f54e6053df4bb0b83be29366025cf6` | Qwen3.5-Plus | 0.8 |
| 应用 6 | 备注分析 | `DASHSCOPE_APP_ID_6_NOTE` | `025bb344146b4e4e8be30c444adab3b4` | Qwen3-Max-Preview | 0 |
| 应用 7 | 客户分析 | `DASHSCOPE_APP_ID_7_CUSTOMER` | `df35e06991b24d49971c03c6428a9c87` | Qwen3-Max-Preview | 0 |
| 应用 8 | 维客线索整理 | `DASHSCOPE_APP_ID_8_CONSOLIDATE` | `407dfb89283b4196934eec5fefe3ebc2` | Qwen3-Max-Preview | 0 |
> P14 变更:环境变量前缀从 `BAILIAN_*` 迁移到 `DASHSCOPE_*`SDK 从 `openai` 迁移到 `dashscope`Application API
> 新增环境变量:`DASHSCOPE_API_KEY`、`DASHSCOPE_WORKSPACE_ID`(可选)、`INTERNAL_API_TOKEN`、`BACKEND_API_URL`。
> 已删除:`BAILIAN_BASE_URL`、`BAILIAN_MODEL`Application API 通过 app_id 指定应用,不需要 base_url 和 model
### 前端消费方式速查
@@ -47,7 +76,7 @@
```text
# 角色
你是一位台球门店运营助手,服务于"浪浪桌球"品牌旗下门店。你擅长通过 MCP 工具查询数据库,为门店工作人员提供数据查询、经营分析和客户管理方面的支持。
你是一位台球门店运营助手。你擅长通过 MCP 工具查询数据库,为门店工作人员提供数据查询、经营分析和客户管理方面的支持。
当前用户信息:
- 用户ID{{User_ID}}
@@ -108,7 +137,9 @@
- 使用简体中文回复。
- 数据展示清晰,适当使用表格格式。
- 对异常数据主动提示(如金额为负、数据缺失等)。
- 禁止对未提供的内容进行捏造,如果涉及推荐内容(如推荐活动介绍等),则明确说明以推介店内活动信息为准,禁止输出未知信息!
- 不确定的信息不要编造,如实告知用户。
- 回答抓住重点简洁直接不宜过长。必须是400字以内
## 参考文档
- 当通过 MCP 查询数据库时,请参考"桌球运营小程序 SQL"内的 markdown 文档。
@@ -179,7 +210,7 @@ json
- 仅返回 JSON 数组,不要包含任何其他文字。
## 限制
- 仅基于传入的数据进行分析,不要编造数据。
- 仅基于传入的数据进行分析,不要编造数据。禁止臆想内容!
- 如果某项数据缺失或为零,在分析中如实说明,不要跳过。
- 营业日以 08:00 为分界点(如"本月"= 当月1日 08:00 ~ 次月1日 08:00
```
@@ -259,7 +290,7 @@ json
- 首次分析时可能没有历史参考信息,正常输出即可。
## 限制
- 仅基于传入的客观数据进行分析,不要编造数据。
- 仅基于传入的客观数据进行分析,不要编造数据。禁止臆想数据!
- 不要分析备注内容(那是应用 6 的职责)。
- 使用简体中文。
```
@@ -332,6 +363,7 @@ json
- 使用简体中文。
- 行动建议要考虑台球门店的实际场景(如微信联系、到店时主动招呼、推荐活动等)。
- 不要给出过于笼统的建议(如"多关注客户"),必须具体到可执行的动作。
- 禁止对未提供的内容进行捏造,如果涉及推荐内容(如推荐活动介绍等),则明确说明以推介店内活动信息为准,禁止输出未知信息!
```
---
@@ -354,7 +386,6 @@ json
- **任务**:根据任务类型和客户特征,生成适合的沟通话术。
- 召回话术:针对长时间未到店的客户,自然地引导回店。
- 维护话术:针对活跃客户,增强粘性和好感。
- 充值引导:针对余额不足的客户,自然地引导充值。
### 技能2: 个性化定制
- **任务**:基于客户的偏好和历史,让话术更有针对性。
@@ -366,7 +397,7 @@ json
- **任务**:确保话术自然、得体、有效。
- 语气亲切但不过分热情,像朋友间的自然对话。
- 避免过于商业化的推销感。
- 提供多种话术选择,适应不同沟通场景(微信消息、电话、到店面对面)
- 均为微信发送消息
## 输出格式(强制)
@@ -377,15 +408,14 @@ json
{
"tactics": [
{
"content": "话术内容(含场景说明和具体话术文本150字内"
"content": "话术内容(直接输出可以复制发送给客户的话术150字内"
}
]
}
"""
### 输出规则
- 返回 2-4 条话术,覆盖不同沟通场景或切入角度。
- 每条话术包含简短的场景说明(什么时候用)和具体的话术文本。
- 返回 2-4 条话术,覆盖不同营销场景或切入角度。
- 话术文本要口语化,可以直接复制使用。
- 仅返回 JSON不要包含任何其他文字。
@@ -393,7 +423,8 @@ json
- 使用简体中文,口语化表达。
- 话术要符合台球门店助教的身份和语境。
- 不要使用过于正式或书面化的表达。
- 基于传入的客户信息和任务建议(应用 4 返回)生成话术,不要编造客户信息。
- 基于传入的客户信息和任务建议(应用 4 返回)生成话术,不要编造客户信息。禁止臆想内容!
- 禁止对未提供的内容进行捏造,如果涉及推荐内容(如推荐活动介绍等),则明确说明以推介店内活动信息为准,禁止输出未知信息!
```
---
@@ -467,7 +498,7 @@ json
- `客户基础`:会员等级、注册时间、基本属性、个人信息等
- `消费习惯`:消费频率、金额、时段、支付方式等
- `玩法偏好`:台球类型、包厢偏好、团建倾向等
- `促销偏好`:对活动的反应、价格敏感度、充值意愿等
- `促销接受`:对活动的反应、价格敏感度、充值意愿等
- `社交关系`:常带朋友、固定球搭子、社交圈等
- `重要反馈`:投诉、建议、特殊需求、满意度等
@@ -565,7 +596,7 @@ json
- 首次分析时可能没有历史参考信息,正常输出即可。
## 限制
- 仅基于传入的数据进行分析,不要编造数据。
- 仅基于传入的数据进行分析,不要编造数据。禁止臆想内容!
- 使用简体中文。
- 策略建议要符合台球门店的实际运营场景。
```
@@ -624,7 +655,7 @@ json
- `客户基础`:会员等级、注册时间、基本属性、个人信息等
- `消费习惯`:消费频率、金额、时段、支付方式等
- `玩法偏好`:台球类型、包厢偏好、团建倾向等
- `促销偏好`:对活动的反应、价格敏感度、充值意愿等
- `促销接受`:对活动的反应、价格敏感度、充值意愿等
- `社交关系`:常带朋友、固定球搭子、社交圈等
- `重要反馈`:投诉、建议、特殊需求、满意度等
@@ -640,3 +671,42 @@ json
- 不要删除任何你认为"价值不高"的线索(价值判断已由上游应用完成)。
- 使用简体中文。
```
---
## 附录代码审计对照表2026-03-21
> 基于 `apps/backend/app/ai/` 实际代码与本文档描述的逐项对比。
### 已修正的差异
| # | 位置 | 原文档描述 | 代码实际实现 | 修正说明 |
|---|------|-----------|-------------|---------|
| 1 | 应用 1 · `board-finance` | `v_dws_finance_daily_summary` | `fdw_etl.v_dwd_settlement_head`settle_type IN 1,3近 1 月 SUM/AVG | 代码直接查 DWD 结算头表做聚合,未使用 DWS 财务日汇总视图 |
| 2 | 应用 1 · `board-customer` | `v_dim_member` | `fdw_etl.v_dwd_settlement_head` JOIN `fdw_etl.v_dim_member`Top 10 按 items_sum DESC | 代码从结算头表聚合消费金额JOIN 会员维度表取昵称 |
| 3 | 应用 1 · `board-coach` | `v_dim_assistant` | `fdw_etl.v_dwd_assistant_service_log` JOIN `fdw_etl.v_dim_assistant`Top 10 按 service_count DESC | 代码从服务日志表聚合JOIN 助教维度表取昵称 |
| 4 | 应用 1 · `performance` | `v_dws_assistant_monthly_summary` | `fdw_etl.v_dws_assistant_salary_calc` JOIN `fdw_etl.v_dim_assistant` | 代码查的是薪资计算表,不是月度汇总表 |
| 5 | 应用 1 · `customer-service-records` | `v_dwd_settlement_head` | `fdw_etl.v_dwd_assistant_service_log`is_trash=falseLIMIT 10 | 代码查的是助教服务日志表,不是结算头表 |
| 6 | 应用 1 · `my-profile` | `auth.users` | 无数据库查询(返回静态文本) | 代码未查 auth.users直接返回固定提示文本 |
| 7 | 应用 1 · `task-detail` | `v_dwd_settlement_head` | `biz.coach_tasks` + `biz.notes` + `biz.ai_cache` | 代码未查 ETL 结算头表,仅查业务库任务/备注/AI缓存 |
| 8 | 应用 1 · `customer-detail` | `biz.notes` | `member_retention_clue`(维客线索) | 代码查的是维客线索表而非备注表;另外还查了 `v_dws_member_consumption_summary` 取余额 |
| 9 | Token 预算 | 应用 1 页面上下文 ≤4000 字符 | `MAX_PAGE_CONTEXT_LENGTH = 2000`page_context.py`_MAX_SYSTEM_PROMPT_LEN = 4000`app1_chat.py | 页面上下文截断阈值是 20004000 是 system prompt 总长上限 |
### 确认一致的部分
- ✅ 应用 1 的 10 种 contextType 名称与 `SUPPORTED_PAGE_TYPES` 完全一致
- ✅ 应用 2 的 8 个时间维度编码与 `TIME_DIMENSIONS` 完全一致
- ✅ 应用 2 的 prompt 结构task + data + reference`app2_finance_prompt.py` 一致
- ✅ 应用 2 的字段映射items_sum 口径、助教费用拆分)与代码一致
- ✅ 应用 3 的 category 枚举限定 3 个值(客户基础、消费习惯、玩法偏好)
- ✅ 应用 6 的 category 枚举限定 6 个值(含促销接受、社交关系、重要反馈)
- ✅ 应用 3/6/7 共用 `member_data.py`fetch_member_consumption_data
- ✅ 应用 4/5 共用 `assistant_data.py`fetch_assistant_info + fetch_service_history
- ✅ 应用 8 的 source 判断规则ai_consumption / ai_note / 混合→ai_consumption
- ✅ 所有 FDW 查询使用 `is_trash=false` 排除废单
- ✅ 会员信息通过 `scd2_is_current=1` 过滤
- ✅ 金额口径统一使用 items_sum禁止 consume_money
- ✅ 应用 ID 与环境变量映射表与代码常量一致
- ✅ 前端消费方式速查表与缓存写入逻辑一致
- ✅ 应用 3-7 的 system message 上限 ≤8000 字符

View File

@@ -0,0 +1,244 @@
# P13小程序前端 — 联调补齐与格式统一 — miniapp-fe-polish
> 优先级P13依赖 P6~P9 前端页面 + P3 认证 + P4 核心业务 + 后端接口)
> 预估工作量:中
> 生成日期2026-03-20
---
## 背景
小程序前端页面已完成 H5 原型还原P6~P9但在 MOCK 数据排查中发现多处功能点未对接真实数据或逻辑缺失。本 SPEC 统一收敛这些遗漏项,确保每个页面在联调后数据展示完整、格式统一。
---
## 一、通用规则(跨页面)
### G1微信头像与用户信息
**现状**`fetchMe()` 接口已定义但返回空 mock`task-list` 声明了 `avatarUrl` 但未赋值;`performance`/`performance-records``avatarUrl` 字段。
**需求**
- 登录成功后,从后端获取用户信息(微信头像 URL、微信昵称、角色、门店名称
- 后端接口 `GET /api/xcx/me` 返回 `{ avatarUrl, nickName, role, storeName }`
- 头像来源:微信登录时由后端通过 `code2Session` + `getUserInfo` 获取并存储,前端不直接调用 `wx.getUserProfile`
- 所有含 banner 的页面task-list、performance、performance-records统一从全局用户信息读取 `avatarUrl`
**验收标准**
- AC-G1.1:三个 banner 页面展示微信头像,无头像时显示默认占位图
- AC-G1.2:用户昵称、角色、门店名称正确展示
### G2当月预估判断
**现状**WXML 硬编码"预估"文案TS 无当月判断逻辑。
**需求**
- 当查看的数据月份 = 当前自然月时,标题显示"我的预估收入",金额旁显示"预估"标签
- 当查看的数据月份 < 当前自然月时,标题显示"我的收入",无"预估"标签
- 判断逻辑:`isCurrentMonth = (year === nowYear && month === nowMonth)`
- 影响页面performance、performance-records、board-finance
**验收标准**
- AC-G2.1:本月数据显示"预估"标签,历史月份不显示
- AC-G2.2board-finance 本月时间筛选时,经营一览标题含"预估"字样
### G3绩效折算折前/折后)
**现状**`performance-records` 接口定义了 `hoursRaw`(折前)和 `hours`折后WXML 有条件展示逻辑,但 `totalHoursRawLabel` 始终为空。
**需求**
- "绩效折前"/"折前"指 DWS 层定义的绩效惩罚规则计算出的折算前课时
- 后端接口返回 `hours`(折后)和 `hoursRaw`(折前),当两者不同时前端展示"折前 Xh"
- 汇总统计同理:`totalHoursRaw``totalHours` 时展示"折前 Xh"
**验收标准**
- AC-G3.1:存在折算差异时,课时旁显示"折前 Xh"灰色小字
- AC-G3.2:无折算差异时不显示"折前"
### G4储值等级显示规则
**现状**`task-detail` 声明了 `storageLevel` 但无计算逻辑。
**需求**
- 根据客户储值余额balance计算等级文案
- `= 0` → "无"
- `< 200` → "少"
- `< 500` → "一般"
- `< 1500` → "多"
- `≥ 1500` → "非常多"
- 计算在前端完成(后端返回 balance 数值),工具函数放 `utils/storage-level.ts`
**验收标准**
- AC-G4.1task-detail 储值区域根据 balance 正确显示等级文案
- AC-G4.2balance 为 0 时显示"无"
---
## 二、各页面功能点
### P1task-list任务列表
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T1.1 | banner 头像 | `avatarUrl` 声明但未赋值 | 见 G1 |
| T1.2 | 比同期数据 | PerfData 有 `incomeTrend`/`incomeTrendDir`,但值为空 | 后端返回与上月同期的差值(数值,非百分比),如 `+¥1,200` / `-¥800`;前端展示数值 + ↑/↓ 箭头 |
| T1.3 | 放弃原因 | `abandonReason` 硬编码空字符串 | 从后端 task 对象的 `abandonReason` 字段获取,展示放弃时填写的备注文本 |
| T1.4 | 盖戳动画 | ✅ 已实现 | 确认:任何情况下页面加载后盖戳动画都会播放(当前仅 `tierCompleted` 时触发),需改为始终播放 |
**验收标准**
- AC-T1.2:比同期显示为数值差(如 `+¥1,200`),非百分比
- AC-T1.3:已放弃任务卡片展示放弃备注
- AC-T1.4:页面加载后盖戳动画始终播放,不依赖 `tierCompleted`
### P2performance绩效总览
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T2.1 | banner 头像 | 无 `avatarUrl` 字段 | 见 G1 |
| T2.2 | 预估/实际收入 | 硬编码"预估" | 见 G2 |
### P3performance-records绩效明细
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T3.1 | banner 头像 | 无 `avatarUrl` 字段 | 见 G1 |
| T3.2 | 预估/实际收入 | 硬编码"预估" | 见 G2 |
| T3.3 | 总笔数 | 前端 `records.length` 计算 | 后端接口返回 `totalCount` 字段,前端直接使用(分页场景下前端计数不准确) |
| T3.4 | 绩效折前 | 接口有字段但 label 始终为空 | 见 G3 |
### P4task-detail任务详情
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T4.1 | 手机号码 | `onCopyPhone``phone = ''` 硬编码 | 从 `this.data.detail` 获取客户手机号(后端 task 详情接口返回 `customerPhone` |
| T4.2 | 储值显示规则 | `storageLevel` 无计算逻辑 | 见 G4从 detail 的 balance 字段计算 |
| T4.3 | 行动建议 | 仅有"问问助手"跳转 chat | 后端 task 详情接口返回 `actionSuggestions: string[]`AI 生成的行动建议列表),前端在维客线索下方展示为卡片列表 |
| T4.4 | 备注打星 | ✅ 已实现 | star-rating 组件 + note-modal 爱心/台球双维度评分已完整 |
### P5customer-service-records客户服务记录
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T5.1 | 客户名称 | ✅ 已实现 | 从 fetchCustomerDetail 获取 |
| T5.2 | 本月服务次数 | 从 detail 取 `totalServiceCount`,类型断言 `as any` | 后端 `GET /api/xcx/customers/:id` 返回 `totalServiceCount` 字段,前端类型定义补齐 |
| T5.3 | 课程标签 | getTypeLabel 基于 includes 硬编码匹配 | 后端返回 `courseType` 枚举basic/vip/incentive/recharge/snooker/group前端直接映射不再 includes 猜测 |
### P6board-finance财务看板
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T6.1 | AI 智能洞察 | WXML 硬编码 3 行文案 | 后端接口返回 `aiInsights: Array<{ icon: string; text: string }>`,前端动态渲染 |
| T6.2 | 环比箭头 | ✅ 已实现 | compareEnabled 开关 + ↑/↓ 箭头 + 颜色区分 |
| T6.3 | 本月"预估" | 无预估标记 | 见 G2 |
### P7board-customer客户看板
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T7.1 | 爱心 icon | ✅ 已实现 | heart-icon 组件 + getHeartEmoji 映射 |
### P8customer-detail客户详情
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T8.1 | 电话 | ✅ 已实现 | onTogglePhone + onCopyPhone 完整 |
### P9board-coach助教看板
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T9.1 | 级别 Icon | ✅ 已实现 | coach-level-tag 组件 + LEVEL_CLASS 映射 |
### P10coach-detail助教详情
| 编号 | 功能点 | 现状 | 需求 |
|------|--------|------|------|
| T10.1 | 任务执行数量统计 | `taskStats` 硬编码 `{ recall: 24, callback: 14 }` | 后端接口返回 `taskStats: { recall: number; callback: number }`,前端从 API 获取 |
---
## 三、数据格式统一标准
### 已有格式化工具函数(但并没有进行全部调用)
| 类型 | TS 函数 | WXS 函数 | 格式示例 | 缺省值 |
|------|---------|----------|----------|--------|
| 金额 | `formatMoney(value)` | `money(value)` | ¥12,680 / -¥368 / ¥0 | -- |
| 计数 | `formatCount(value, unit)` | `count(value, unit)` | 18笔 / 3次 / 12人 | -- |
| 百分比 | `formatPercent(value)` | `percent(value)` | 12.5% / 0% | -- |
| 课时 | `formatHours(hours)` | `hours(value)` | 86h / 72.5h / 0h | -- |
| 相对时间 | `formatRelativeTime(value)` | — | 刚刚 / 3分钟前 / 2天前 / 03-10 | -- |
| 截止日期 | `formatDeadline(deadline)` | — | 逾期3天 / 今天到期 / 还剩5天 / 03-15 | -- |
| IM 时间 | `formatIMTime(value)` | — | 14:30 / 03-10 14:30 | (空字符串) |
| 爱心 | `getHeartEmoji(score)` | — | 💖 / 🧡 / 💛 / 💙 | 💙 |
| 星级 | `scoreToHalfStar(score)` | — | 4.5 / 3.0 | 0 |
| 空值兜底 | — | `safe(val)` | (原值) | -- |
### 需要补充的格式化
| 类型 | 建议函数名 | 格式示例 | 缺省值 | 说明 |
|------|-----------|----------|--------|------|
| 日期(短) | `formatDateShort(date)` | 3月15日 / 03-15 | -- | 用于服务记录、充值日期等 |
| 日期(完整) | `formatDateFull(date)` | 2026-03-15 | -- | 用于历史数据、导出 |
| 天数 | `formatDays(days)` | 3天 / 15天 | -- | 用于"N天前到店"、逾期天数 |
| 储值等级 | `formatStorageLevel(balance)` | 无/少/一般/多/非常多 | 无 | 见 G4 |
| 同比差值 | `formatTrendValue(value)` | +¥1,200 / -¥800 | -- | 用于"比同期"展示 |
### 接口端格式对齐原则
1. 后端返回原始数值number前端负责格式化展示
2. 金额单位统一为"元"(整数),前端加 ¥ 前缀和千分位
3. 课时单位统一为"小时"number前端加 h 后缀
4. 日期统一 ISO 8601 格式(`YYYY-MM-DDTHH:mm:ss`),前端按场景格式化
5. 百分比后端返回 0-100 的 number前端加 % 后缀
6. 所有 null/undefined/0 值,前端统一展示为 `--`(通过 format 函数兜底)
---
## 四、实施优先级
### 第一批(阻塞联调)
- G1 微信头像(影响 3 个页面 banner
- T1.3 放弃原因(后端字段直通)
- T4.1 手机号码(后端字段直通)
- T3.3 总笔数(后端字段直通)
- T10.1 任务执行统计(后端字段直通)
### 第二批(业务逻辑)
- G2 当月预估判断(纯前端逻辑)
- G3 绩效折算展示(前端条件渲染)
- G4 储值等级规则(前端计算)
- T1.2 比同期数据(需后端新增字段)
- T1.4 盖戳动画始终播放(前端逻辑调整)
- T5.3 课程标签枚举化(前后端对齐)
### 第三批(增强体验)
- T4.3 行动建议(需后端 AI 接口)
- T6.1 AI 智能洞察(需后端 AI 接口)
- 格式化工具函数补充
---
## 五、涉及文件清单
### 前端
- `apps/miniprogram/miniprogram/services/api.ts`
- `apps/miniprogram/miniprogram/utils/money.ts`
- `apps/miniprogram/miniprogram/utils/time.ts`
- `apps/miniprogram/miniprogram/utils/storage-level.ts`(新建)
- `apps/miniprogram/miniprogram/pages/task-list/task-list.ts`
- `apps/miniprogram/miniprogram/pages/performance/performance.ts`
- `apps/miniprogram/miniprogram/pages/performance-records/performance-records.ts`
- `apps/miniprogram/miniprogram/pages/task-detail/task-detail.ts`
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts`
- `apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts`
- `apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.ts`
### 后端(接口变更)
- `GET /api/xcx/me` — 补充 avatarUrl 字段
- `GET /api/xcx/tasks` — performance 补充 `trendValue`(同比差值)
- `GET /api/xcx/tasks/:id` — 补充 `customerPhone``actionSuggestions`
- `GET /api/xcx/customers/:id` — 类型定义补充 `totalServiceCount`
- `GET /api/xcx/performance/records` — 补充 `totalCount``hoursRaw`
- `GET /api/xcx/coaches/:id` — 补充 `taskStats`
- `GET /api/xcx/board/finance` — 补充 `aiInsights``isEstimated`

View File

@@ -0,0 +1,511 @@
# P14AI 模块改造 — DashScope 迁移 + 调度器完善
> 状态Draft | 创建日期2026-03-21 | 依赖P5AI 集成层、RNS1.4CHAT 对齐)
> 后续P15监控后台 + 测试重建 + 回填)
---
## 一、执行摘要
当前 AI 模块使用 `openai` SDK 的通用模型 API`chat.completions.create`),但项目的 8 个 App 均为百炼控制台创建的智能体应用(各有独立 `app_id`)。通用模型 API 不接受 `app_id`,等于绕过了百炼控制台配置的 System Prompt、MCP 工具等全部能力。
本 PRD 将 SDK 从 `openai` 切换到 `dashscope`Application API一步到位完成迁移同时修复调度器、事件触发、熔断限流等核心问题。
### 问题来源
- `docs/reports/2026-03-21__ai_module_issues.md`18 个问题4 P0 / 6 P1 / 5 P2 / 3 P3
- AI 全链路测试报告 + 86 项 Gap 审查
### 改动范围
| 模块 | 改动内容 |
|------|---------|
| `apps/backend/app/ai/` | SDK 替换、客户端重写、调度器修复 |
| `apps/backend/app/services/ai/` | 缓存服务、对话服务适配 |
| `apps/backend/app/routers/` | 内部触发 API、SSE 端点适配 |
| `apps/etl/connectors/feiqiu/` | DWS 完成后 HTTP 触发 |
| `db/zqyy_app/` | DDL 迁移(新增表、字段) |
| `.env` / `.env.template` | 环境变量统一 |
---
## 二、技术方案
### 2.1 SDK 替换openai → dashscope Application API
**当前**`openai.AsyncOpenAI` + `chat.completions.create`
**目标**`dashscope.Application.call` + `asyncio.to_thread()` 包装
#### 调用方式对比
```python
# ===== 当前(错误)=====
from openai import AsyncOpenAI
client = AsyncOpenAI(api_key=key, base_url=bailian_url)
response = await client.chat.completions.create(
model=model, messages=messages, response_format={"type": "json_object"}
)
# ===== 目标(正确)=====
import dashscope
from dashscope import Application
# App1 流式调用
response = Application.call(
app_id=app_id,
prompt=user_message,
session_id=session_id, # 云端对话管理
biz_params={"user_prompt_params": {"User_ID": uid, "Role": role, "Nickname": name}},
stream=True,
incremental_output=True,
)
# App2~8 单轮调用
response = Application.call(
app_id=app_id,
prompt=data_json, # 后端拼好的完整数据 JSON
)
```
#### async 包装
`dashscope.Application.call()` 是同步方法,当前后端全 async。采用 `asyncio.to_thread()` 包装:
```python
import asyncio
async def call_application(app_id: str, prompt: str, **kwargs) -> dict:
return await asyncio.to_thread(
Application.call,
app_id=app_id,
prompt=prompt,
**kwargs
)
```
流式调用App1需特殊处理`Application.call(stream=True)` 返回同步迭代器,需在线程中消费后通过 `asyncio.Queue` 桥接到 async generator。
### 2.2 环境变量统一
**废弃**(本轮直接删除,不保留兼容):
- `BAILIAN_API_KEY`
- `BAILIAN_BASE_URL`
- `BAILIAN_MODEL`
**新增**
- `DASHSCOPE_API_KEY` — DashScope API Key
- `DASHSCOPE_WORKSPACE_ID` — 百炼工作空间 ID可选
**保留不变**(仅改前缀注释):
| 旧变量 | 新变量 | 说明 |
|--------|--------|------|
| `BAILIAN_APP_ID_1_CHAT` | `DASHSCOPE_APP_ID_1_CHAT` | App1 通用对话 |
| `BAILIAN_APP_ID_2_FINANCE` | `DASHSCOPE_APP_ID_2_FINANCE` | App2 财务洞察 |
| `BAILIAN_APP_ID_3_CLUE` | `DASHSCOPE_APP_ID_3_CLUE` | App3 维客线索 |
| `BAILIAN_APP_ID_4_ANALYSIS` | `DASHSCOPE_APP_ID_4_ANALYSIS` | App4 关系分析 |
| `BAILIAN_APP_ID_5_TACTICS` | `DASHSCOPE_APP_ID_5_TACTICS` | App5 话术参考 |
| `BAILIAN_APP_ID_6_NOTE` | `DASHSCOPE_APP_ID_6_NOTE` | App6 备注分析 |
| `BAILIAN_APP_ID_7_CUSTOMER` | `DASHSCOPE_APP_ID_7_CUSTOMER` | App7 客户分析 |
| `BAILIAN_APP_ID_8_CONSOLIDATE` | `DASHSCOPE_APP_ID_8_CONSOLIDATE` | App8 维客线索整理 |
### 2.3 App1 对话管理session_id 云端 + 本地双轨
**策略**
1. 每次 App1 调用携带 `session_id`(百炼云端管理上下文)
2. 同时将消息写入本地 `ai_messages` 表(持久化)
3. `session_id` 过期1 小时无请求)时,从本地 `ai_messages` 重建 `messages` 数组传给百炼
4. 对话复用规则不变task 入口无时限复用、customer/coach 入口 3 天时限、general 入口始终新建
**session_id 生成规则**
- 格式:`conv_{conversation_id}_{created_timestamp}`
- 存储在 `ai_conversations.session_id` 字段(新增)
**过期重建流程**
```
用户发消息 → 查 ai_conversations 获取 session_id
→ 尝试用 session_id 调用百炼
→ 成功 → 正常返回
→ 失败session 过期)→ 从 ai_messages 加载历史
→ 用 messages 数组(不带 session_id调用百炼
→ 百炼返回新 session_id → 更新本地
```
### 2.4 App2~8 单轮调用
所有非对话类应用统一为单轮 `prompt` 调用:
- 后端用 `build_prompt()` 拼好完整数据 JSON
- 通过 `prompt` 参数传入 `Application.call`
- 百炼侧 System Prompt 已在控制台配置,代码不再维护
**JSON 兜底策略**:百炼返回非合法 JSON 时,纯重试,最大 3 次。不做本地解析修复。
### 2.5 App1 参数传递
App1 使用 `biz_params.user_prompt_params` 传模板变量:
- `User_ID`:用户 ID
- `Role`角色member / assistant / admin
- `Nickname`:昵称
同时用 `prompt` 传页面上下文source_page、page_context、screen_content
App2~8 不使用 `biz_params`,数据 JSON 直接作为 `prompt` 传入。
---
## 三、熔断 / 限流 / 降级
### 3.1 熔断器
```
连续 5 次失败 → 熔断 60 秒(所有请求直接返回降级响应)
→ 60 秒后进入半开状态 → 放行 1 个请求
→ 成功 → 关闭熔断
→ 失败 → 重新熔断 60 秒
```
熔断粒度:按 `app_id` 独立计数。App1 熔断不影响 App2~8。
### 3.2 Token 预算控制
| 维度 | 默认上限 | 说明 |
|------|---------|------|
| 日预算 | 100,000 tokens | 所有 App 合计 |
| 月预算 | 2,000,000 tokens | 所有 App 合计 |
超预算时:
- App1用户对话返回友好提示"AI 服务今日额度已用完,请明天再试"
- App2~8后台任务跳过执行记录 `budget_exceeded` 状态
预算数据来源:`ai_run_logs.tokens_used` 按日/月聚合。
### 3.3 限流
- App1每用户每分钟 10 次
- App2~8每门店每小时 100 次(合计)
---
## 四、调度器修复
### 4.1 dispatcher.py asyncio 修复
当前问题:`dispatcher.py` 中存在 `asyncio.run()` 嵌套调用,在已有事件循环的 FastAPI 环境中会报错。
修复方案:
- 移除所有 `asyncio.run()` 调用
- 所有调度入口改为 `async def`
- 使用 `asyncio.create_task()` 发起异步任务
- 超时控制用 `asyncio.wait_for()`
### 4.2 事件触发打通
#### 消费事件ETL 侧发射)
```
ETL DWS 任务完成
→ HTTP POST /api/internal/ai/trigger
→ payload: { event_type: "consumption", connector_type: "feiqiu", site_id, member_id, settlement_id }
→ 后端写 ai_trigger_jobs 记录
→ 异步执行调用链App3 → App8 → App7+ App4 → App5 如有助教)
```
内部 API 设计为连接器无关接口,`connector_type` 字段标识来源,为多平台扩展预留。
#### 备注事件(后端 API 路由发射)
```
小程序助教提交备注
→ 后端 API 路由 fire_event
→ event_type: "note_created"
→ 调用链App6 → App8
```
#### 任务分配事件task_manager 自动触发)
```
task_manager 自动分配任务
→ fire_event: "task_assigned"
→ 调用链App4 → App5
```
### 4.3 App2 预生成
- 触发时机DWS 完成后ETL 通过内部 API 触发
- 门店范围:当前写死 `2790685415443269`(多门店支持记入 BACKLOG
- 时间维度8 个(今日/昨日/本周/上周/本月/上月/本季/上季)
- 每日调用量1 门店 × 8 维度 = 8 次
### 4.4 幂等与去重
| 场景 | 策略 |
|------|------|
| 自动触发 | 按 `(event_type, member_id, site_id, date)` 去重,重复事件跳过 |
| 手动重跑 | 允许强制执行,`ai_trigger_jobs.is_forced = true`,后台明显标记 "forced" |
| App8 | 强幂等DELETE + INSERT `member_retention_clue`,同一 member 同一天只执行一次 |
---
## 五、缓存策略
### 5.1 过期时间(按 App 分开)
| App | cache_type | expires_at 策略 |
|-----|-----------|----------------|
| App2 | app2_finance | 当日 23:59:59每日刷新 |
| App3 | app3_clue | 7 天 |
| App4 | app4_analysis | 7 天 |
| App5 | app5_tactics | 7 天 |
| App6 | app6_note_analysis | 30 天 |
| App7 | app7_customer_analysis | 7 天 |
| App8 | app8_clue_consolidated | 7 天 |
### 5.2 ai_cache 新增字段
```sql
ALTER TABLE biz.ai_cache ADD COLUMN status VARCHAR(20) DEFAULT 'valid'
CHECK (status IN ('valid', 'expired', 'invalidated', 'generating'));
```
- `valid`:有效缓存
- `expired`:已过期(定时任务标记)
- `invalidated`手动失效admin-web 操作)
- `generating`:正在生成中(防并发)
### 5.3 数据保留上限
| App | 保留策略 |
|-----|---------|
| App1 | 不自动删除(用户对话记录) |
| App2~8 | 每个 App 保留最新 20,000 条 `ai_cache` 记录 |
---
## 六、数据库变更
所有新表放在 `biz` schema。
### 6.1 新增表ai_run_logsAI 运行记录)
```sql
CREATE TABLE biz.ai_run_logs (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
app_type VARCHAR(30) NOT NULL, -- app1_chat / app2_finance / ...
trigger_type VARCHAR(20) NOT NULL, -- user / scheduled / event / forced
member_id BIGINT, -- 关联会员(可空)
request_prompt TEXT, -- 输入 prompt截断前 2000 字符)
response_text TEXT, -- 输出全文
tokens_used INTEGER DEFAULT 0,
latency_ms INTEGER, -- 响应耗时(毫秒)
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- pending / running / success / failed / timeout / budget_exceeded
error_message TEXT,
session_id VARCHAR(100), -- App1 百炼 session_id
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
finished_at TIMESTAMPTZ
);
CREATE INDEX idx_ai_run_logs_site_app ON biz.ai_run_logs(site_id, app_type);
CREATE INDEX idx_ai_run_logs_created ON biz.ai_run_logs(created_at);
CREATE INDEX idx_ai_run_logs_status ON biz.ai_run_logs(status);
```
### 6.2 新增表ai_trigger_jobs调度运行记录
```sql
CREATE TABLE biz.ai_trigger_jobs (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
event_type VARCHAR(30) NOT NULL, -- consumption / note_created / task_assigned / scheduled / manual
connector_type VARCHAR(30) DEFAULT 'feiqiu', -- 连接器类型(多平台预留)
member_id BIGINT,
payload JSONB, -- 事件原始数据
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- pending / running / completed / failed / skipped_duplicate / budget_exceeded
is_forced BOOLEAN DEFAULT false, -- 手动强制执行标记
app_chain VARCHAR(100), -- 执行链:如 "app3→app8→app7"
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_ai_trigger_jobs_site ON biz.ai_trigger_jobs(site_id, event_type);
CREATE INDEX idx_ai_trigger_jobs_dedup ON biz.ai_trigger_jobs(event_type, member_id, site_id, (created_at::date))
WHERE status NOT IN ('skipped_duplicate');
CREATE INDEX idx_ai_trigger_jobs_status ON biz.ai_trigger_jobs(status);
```
### 6.3 ai_conversations 新增字段
```sql
ALTER TABLE biz.ai_conversations ADD COLUMN session_id VARCHAR(100);
```
### 6.4 ai_cache 新增字段
```sql
ALTER TABLE biz.ai_cache ADD COLUMN status VARCHAR(20) DEFAULT 'valid'
CHECK (status IN ('valid', 'expired', 'invalidated', 'generating'));
```
---
## 七、内部 API 设计
### 7.1 ETL → 后端触发接口
```
POST /api/internal/ai/trigger
Authorization: Internal-Token {INTERNAL_API_TOKEN}
Content-Type: application/json
{
"event_type": "consumption", // consumption / dws_completed
"connector_type": "feiqiu",
"site_id": 2790685415443269,
"member_id": 12345, // 可选
"payload": { // 事件附加数据
"settlement_id": "xxx",
"dws_task": "DWS_MEMBER_CONSUMPTION"
}
}
Response 200:
{
"trigger_job_id": 1001,
"status": "pending"
}
```
### 7.2 认证方式
内部 API 使用独立的 `INTERNAL_API_TOKEN` 环境变量,不走 JWT。ETL 进程通过 HTTP Header 传递。
---
## 八、文件变更清单
### 需要重写的文件
| 文件 | 说明 |
|------|------|
| `apps/backend/app/ai/bailian_client.py` | 完全重写为 `dashscope_client.py` |
| `apps/backend/app/ai/dispatcher.py` | asyncio 修复 + 事件触发链 |
| `apps/backend/app/ai/config.py` | 环境变量 BAILIAN_* → DASHSCOPE_* |
### 需要新增的文件
| 文件 | 说明 |
|------|------|
| `apps/backend/app/ai/circuit_breaker.py` | 熔断器实现 |
| `apps/backend/app/ai/rate_limiter.py` | 限流器实现 |
| `apps/backend/app/ai/budget_tracker.py` | Token 预算追踪 |
| `apps/backend/app/routers/internal_ai.py` | 内部触发 API 路由 |
| `db/zqyy_app/migrations/YYYYMMDD_ai_run_logs.sql` | DDL 迁移脚本 |
### 需要修改的文件
| 文件 | 说明 |
|------|------|
| `apps/backend/app/services/ai/cache_service.py` | 新增 status 字段处理 |
| `apps/backend/app/services/ai/chat_service.py` | session_id 双轨逻辑 |
| `apps/etl/connectors/feiqiu/tasks/` | DWS 完成后 HTTP 触发 |
| `.env` / `.env.template` | 环境变量更新 |
| `pyproject.toml` | 依赖:移除 openai新增 dashscope |
---
## 九、问题修复映射
本 PRD 覆盖的问题(来自 `docs/reports/2026-03-21__ai_module_issues.md`
| 问题编号 | 优先级 | 描述 | 本 PRD 对应章节 |
|---------|--------|------|----------------|
| P0-1 | P0 | App3~8 JSON 格式化失败 | §2.4Application API 原生支持) |
| P0-2 | P0 | AI 事件未触发 | §4.2(事件触发打通) |
| P0-3 | P0 | App2 无定时机制 | §4.3App2 预生成) |
| P0-4 | P0 | ETL→AI 联动断裂 | §4.2 + §7.1(内部 API |
| P1-1 | P1 | 缓存过期未检查 | §5缓存策略 |
| P1-2 | P1 | 无限流/熔断 | §3熔断/限流/降级) |
| P1-3 | P1 | 无 Token 预算控制 | §3.2Token 预算) |
| P1-4 | P1 | dispatcher asyncio 问题 | §4.1asyncio 修复) |
| P1-5 | P1 | 前端无刷新机制 | 不在本 PRD 范围(前端改动) |
| P1-6 | P1 | FDW 数据获取不一致 | 不在本 PRD 范围(数据层) |
| P2-3 | P2 | 缓存清理过宽松 | §5.3(保留上限) |
| P2-5 | P2 | 对话记录无清理 | §5.3App1 不删,其他 2 万条) |
| P3-1 | P3 | 迁移 DashScope SDK | §2.1(核心改动) |
| P3-2 | P3 | 调度器独立化 | §4调度器修复 |
---
## 十、收尾标准流程
> 参考历史 Spec 收尾模板P5、P13、RNS1.4
### 10.1 DDL 迁移
1. 编写迁移脚本 `db/zqyy_app/migrations/YYYYMMDD_p14_ai_module.sql`
2. 在测试库 `test_zqyy_app` 执行并验证
3. 编写回滚脚本(逆序 DROP
4. 合并到 DDL 基线 `db/zqyy_app/ddl/`
### 10.2 BD 手册更新
- 更新 `docs/database/BD_Manual_ai_tables.md`:新增 `ai_run_logs``ai_trigger_jobs` 表结构
- 更新 `ai_cache``status` 字段说明
- 更新 `ai_conversations``session_id` 字段说明
### 10.3 文档同步
| 文档 | 更新内容 |
|------|---------|
| `docs/prd/ai-app-prompts.md` | 环境变量映射更新BAILIAN_* → DASHSCOPE_* |
| `apps/backend/README.md` | AI 模块架构说明更新 |
| `docs/DOCUMENTATION-MAP.md` | 新增文档条目 |
| `.env.template` | 环境变量模板更新 |
| `docs/deployment/EXPORT-PATHS.md` | 如有新输出路径则更新 |
### 10.4 属性测试
- 更新 `tests/` 下 AI 相关属性测试,适配新的 `dashscope` 调用方式
- 新增属性测试覆盖熔断器状态转换、Token 预算计算、去重逻辑
### 10.5 最终检查点
- [ ] 所有 8 个 App 通过 Application API 调用成功
- [ ] App1 流式输出正常session_id 双轨工作
- [ ] App2~8 返回合法 JSON
- [ ] 事件触发链完整消费→App3→App8→App7
- [ ] ETL → 后端内部 API 联通
- [ ] 熔断器在连续失败后正确触发
- [ ] Token 预算超限后正确降级
- [ ] 环境变量全部切换到 DASHSCOPE_*
- [ ] DDL 迁移脚本在测试库执行通过
- [ ] BD 手册已更新
- [ ] 属性测试全部通过
---
## 十一、风险与缓解
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| DashScope SDK 同步调用阻塞事件循环 | App1 响应延迟 | `asyncio.to_thread()` + 线程池大小限制 |
| session_id 过期重建增加 token 消耗 | 成本上升 | 限制重建时的历史消息条数(最近 20 条) |
| 百炼控制台 System Prompt 与代码不一致 | 输出质量下降 | 代码不再维护 System Prompt以控制台为准 |
| 环境变量一步切换导致部署遗漏 | 服务不可用 | 部署检查清单 + 启动时校验必需变量 |
| App8 写业务表member_retention_clue幂等失败 | 数据不一致 | 事务包裹 DELETE + INSERT失败自动回滚 |
---
## 十二、不在本 PRD 范围
以下内容由 P15 或后续 PRD 处理:
- admin-web AI 监控后台P15
- 全链路测试重建P15
- 历史回填P15
- 旧测试脚本归档P15
- 多门店支持BACKLOG
- 消息队列(单独 PRD
- Prompt 版本管理BACKLOG
- 前端刷新机制P1-5前端改动
- FDW 数据一致性P1-6数据层改动

View File

@@ -0,0 +1,316 @@
# P15AI 监控后台 + 测试重建 + 回填
> 状态Draft | 创建日期2026-03-21 | 依赖P14DashScope 迁移 + 调度器完善)
> 前置条件P14 全部完成并验证通过后,方可开始 P15 实施
---
## 一、执行摘要
P14 完成 SDK 迁移和调度器修复后,本 PRD 覆盖三个方面:
1. admin-web AI 监控后台(运行总览、调度状态、调用明细、手动操作)
2. 测试体系重建(旧脚本归档、新全链路测试、属性测试更新)
3. 历史数据回填(半年内活跃会员,<100 人)
### 问题覆盖
本 PRD 覆盖 `docs/reports/2026-03-21__ai_module_issues.md` 中的:
- P2-1App5 话术缺分类
- P2-2MCP 无健康检查
- P2-4全链路测试不完整
- P3-3Prompt 版本管理(仅监控展示部分)
---
## 二、admin-web AI 监控后台
### 2.1 技术基础
- 框架React + Vite + Ant Design已有 8 个页面,加页面即可)
- 认证JWT Bearer Token复用现有 admin 权限体系
- API 前缀:`/api/admin/ai/*`
- 权限:系统管理员全量可见,不脱敏,点开详情看原文
- 门店筛选:支持按 `site_id` 筛选查看
### 2.2 页面规划
#### 页面 1AI 运行总览Dashboard
| 区域 | 内容 | 数据来源 |
|------|------|---------|
| 顶部统计卡片 | 今日调用次数、成功率、Token 消耗、平均延迟 | `ai_run_logs` 聚合 |
| 趋势图 | 近 7 天调用量 + 成功率折线图 | `ai_run_logs` 按日聚合 |
| App 分布 | 各 App 调用占比饼图 | `ai_run_logs.app_type` 分组 |
| Token 预算 | 日/月预算使用进度条 | `ai_run_logs.tokens_used` 聚合 |
| 告警列表 | 最近失败/超时/熔断事件 | `ai_run_logs` WHERE status IN ('failed','timeout') |
#### 页面 2调度状态
| 区域 | 内容 | 数据来源 |
|------|------|---------|
| 触发任务列表 | 分页表格:事件类型、会员、状态、执行链、耗时 | `ai_trigger_jobs` |
| 筛选器 | event_type / status / site_id / 日期范围 | — |
| 操作列 | 查看详情、手动重跑 | — |
| 去重统计 | 今日跳过的重复事件数 | `ai_trigger_jobs` WHERE status='skipped_duplicate' |
#### 页面 3调用明细
| 区域 | 内容 | 数据来源 |
|------|------|---------|
| 调用记录表格 | app_type、trigger_type、member_id、tokens、延迟、状态 | `ai_run_logs` |
| 筛选器 | app_type / status / trigger_type / site_id / 日期范围 | — |
| 详情抽屉 | 点击行展开:完整 prompt、完整 response、error_message | `ai_run_logs` 单条 |
#### 页面 4手动操作
| 功能 | 说明 |
|------|------|
| 手动重跑 | 选择 App + 会员 + 门店,触发单次执行(标记 `is_forced=true` |
| 缓存失效 | 按 App / 会员 / 门店批量将 `ai_cache.status` 设为 `invalidated` |
| 成本二次确认 | 批量操作前展示预估调用次数和 Token 消耗,点"确认执行"后才真正执行 |
| 告警确认/忽略 | 对失败告警标记"已确认"或"忽略" |
### 2.3 后端 API 清单
```
GET /api/admin/ai/dashboard — 总览统计数据
GET /api/admin/ai/trigger-jobs — 调度任务列表(分页 + 筛选)
GET /api/admin/ai/trigger-jobs/:id — 调度任务详情
POST /api/admin/ai/trigger-jobs/:id/retry — 手动重跑
GET /api/admin/ai/run-logs — 调用记录列表(分页 + 筛选)
GET /api/admin/ai/run-logs/:id — 调用记录详情(含完整 prompt/response
POST /api/admin/ai/cache/invalidate — 批量缓存失效
GET /api/admin/ai/budget — Token 预算使用情况
POST /api/admin/ai/batch-run — 批量执行(需二次确认)
POST /api/admin/ai/batch-run/confirm — 确认批量执行
GET /api/admin/ai/alerts — 告警列表
POST /api/admin/ai/alerts/:id/ack — 确认告警
POST /api/admin/ai/alerts/:id/ignore — 忽略告警
```
### 2.4 成本二次确认流程
```
管理员选择批量操作(如:回填 50 个会员)
→ POST /api/admin/ai/batch-run
→ 后端计算50 会员 × 5 App = 250 次调用,预估 ~500K tokens
→ 返回 { batch_id, estimated_calls: 250, estimated_tokens: 500000 }
→ 前端展示确认弹窗:"本次将执行 250 次 AI 调用,预估消耗 50 万 tokens确认执行"
→ 管理员点"确认执行"
→ POST /api/admin/ai/batch-run/confirm { batch_id }
→ 后端异步执行
```
---
## 三、测试体系重建
### 3.1 旧测试脚本归档
以下 4 个脚本移至 `_archived/` 目录:
| 脚本 | 原位置 | 归档位置 |
|------|---------|---------|
| `ai_full_chain_test.py` | `scripts/ops/` | `scripts/ops/_archived/` |
| `test_chat_e2e.py` | `scripts/ops/` | `scripts/ops/_archived/` |
| `test_chat_ai_quality.py` | `scripts/ops/` | `scripts/ops/_archived/` |
| `test_bailian_apps.py` | `scripts/ops/` | `scripts/ops/_archived/` |
| `test_bailian_single.py` | `scripts/ops/` | `scripts/ops/_archived/` |
| `test_bailian_full.py` | `scripts/ops/` | `scripts/ops/_archived/` |
| `_run_ai_tests_remaining.py` | `scripts/ops/` | `scripts/ops/_archived/` |
| `test_ai_bailian.py` | `apps/backend/tests/` | `apps/backend/tests/_archived/` |
| `2026-03-21__ai_full_chain_test.md` | `docs/reports/` | `docs/reports/_archived/` |
归档时保留文件内容不变,仅移动位置。审计记录(`docs/audit/`)不删除,只停止引用。
### 3.2 新全链路测试
#### 测试覆盖矩阵
| 测试场景 | 覆盖 App | 验证点 |
|---------|---------|--------|
| App1 对话10 种入口) | App1 | SSE 流式、session_id 双轨、对话复用规则 |
| App2 定时预生成 | App2 | 8 个时间维度、缓存写入、JSON 合法性 |
| 消费事件触发链 | App3→App8→App7 | 事件传播、缓存写入、业务表写入 |
| 助教消费触发链 | App3→App8→App7→App4→App5 | 完整链路、助教关联 |
| 备注事件触发链 | App6→App8 | 备注分析、线索整合 |
| 任务分配触发链 | App4→App5 | 关系分析、话术生成 |
| 缓存命中 | App2~8 | 缓存有效期内直接返回、过期后重新生成 |
| 缓存失效 | App2~8 | admin-web 手动失效后重新生成 |
| 熔断触发 | 任意 App | 连续 5 次失败→熔断→半开→恢复 |
| Token 预算超限 | 任意 App | 超限后正确降级 |
| 失败记录 | 任意 App | `ai_run_logs` 记录完整、error_message 有值 |
| 后台可见性 | 全部 | admin-web 能看到所有运行记录和详情 |
| JSON 兜底 | App2~8 | 非法 JSON 重试 3 次 |
| 幂等验证 | App8 | 同一 member 同一天重复触发只执行一次 |
| 内容质量 | App2~8 | 返回内容包含必要字段、格式正确 |
#### App1 的 10 种入口
1. general通用对话始终新建
2. customer_detail客户详情页
3. coach_detail助教详情页
4. task_detail任务详情页
5. finance_overview财务总览页
6. member_list会员列表页
7. settlement_detail结算详情页
8. note_detail备注详情页
9. dashboard首页仪表盘
10. report报表页
### 3.3 属性测试更新
更新 `tests/` 下的 Hypothesis 属性测试:
| 属性测试 | 验证不变量 |
|---------|-----------|
| 熔断器状态机 | 状态转换合法性closed→open→half_open→closed/open |
| Token 预算计算 | 日/月聚合值 = 各条记录 tokens_used 之和 |
| 去重逻辑 | 相同 (event_type, member_id, site_id, date) 只产生一条非 skipped 记录 |
| 缓存过期 | expires_at 过期的缓存 status 必须为 expired |
| App8 幂等 | 同一 member 同一天的 member_retention_clue 只有一组记录 |
| session_id 重建 | 重建后的 messages 数组与本地 ai_messages 一致 |
| 限流计数 | 窗口内请求数不超过上限 |
---
## 四、历史数据回填
### 4.1 回填范围
半年内2025-09-21 ~ 2026-03-21三者并集
- 有消费记录的 member
- 有备注记录的 member
- 有任务变更的 member
预估规模:<100 会员。
门店范围:`site_id = 2790685415443269`(写死)。
### 4.2 回填策略
- 执行方式:专门脚本(`scripts/ops/ai_backfill.py`
- 分批执行:每批 10 个会员,批间间隔 5 秒
- 断点续跑:脚本记录已完成的 member_id 列表到本地文件,失败后从断点继续
- 不备份现有数据
- App8 写业务表(`member_retention_clue`DELETE + INSERT事务包裹
### 4.3 回填调用链
每个会员执行:
```
App3维客线索→ App8线索整理→ App7客户分析
如有助教关联:→ App4关系分析→ App5话术参考
如有备注:→ App6备注分析→ App8再次整合
```
### 4.4 成本预估
- 100 会员 × 平均 5 个 App = 500 次调用
- 每次约 2000 tokens → 总计约 100 万 tokens
- 执行前通过 admin-web 二次确认
---
## 五、数据保留策略
| 数据类型 | 保留策略 | 清理方式 |
|---------|---------|---------|
| App1 对话记录ai_conversations + ai_messages | 永久保留,不自动删除 | — |
| App2~8 缓存ai_cache | 每个 App 保留最新 20,000 条 | 定时任务:按 created_at 排序,超出部分 DELETE |
| AI 运行记录ai_run_logs | 保留 90 天 | 定时任务DELETE WHERE created_at < now() - 90 days |
| 调度记录ai_trigger_jobs | 保留 90 天 | 同上 |
清理定时任务建议每日凌晨 03:00 执行。
---
## 六、后端 API 变更汇总
### 新增路由文件
`apps/backend/app/routers/admin_ai.py`
### 新增 Service
`apps/backend/app/services/ai/admin_service.py` — 聚合查询、批量操作、告警管理
### 数据库查询优化
- `ai_run_logs` 的 Dashboard 聚合查询需要按日分区或添加 BRIN 索引
- `ai_trigger_jobs` 的去重查询已有复合索引§P14 六.2
---
## 七、问题修复映射
| 问题编号 | 优先级 | 描述 | 本 PRD 对应章节 |
|---------|--------|------|----------------|
| P2-1 | P2 | App5 话术缺分类 | 回填时验证 App5 输出包含分类字段 |
| P2-2 | P2 | MCP 无健康检查 | admin-web Dashboard 展示各 App 最近调用状态 |
| P2-4 | P2 | 全链路测试不完整 | §3.2(新全链路测试) |
| P3-3 | P3 | Prompt 版本管理 | admin-web 展示当前 App 配置(只读) |
---
## 八、收尾标准流程
### 8.1 DDL 迁移
本 PRD 无新增表P14 已创建)。如有字段调整:
1. 编写增量迁移脚本
2. 测试库验证
3. 合并基线
### 8.2 BD 手册更新
- 更新 `docs/database/BD_Manual_ai_tables.md`:补充 admin API 相关的查询模式说明
### 8.3 文档同步
| 文档 | 更新内容 |
|------|---------|
| `apps/admin-web/README.md` | 新增 AI 监控页面说明 |
| `apps/backend/README.md` | 新增 admin AI API 说明 |
| `docs/DOCUMENTATION-MAP.md` | 新增条目 |
| `docs/prd/ai-app-prompts.md` | 确认环境变量已全部更新 |
### 8.4 属性测试
- 新增 §3.3 中列出的 7 个属性测试
- 确保所有属性测试通过
### 8.5 最终检查点
- [ ] admin-web AI 监控 4 个页面功能正常
- [ ] 手动重跑功能正常,标记 forced
- [ ] 缓存失效功能正常
- [ ] 成本二次确认流程完整
- [ ] 旧测试脚本已归档到 `_archived/`
- [ ] 新全链路测试覆盖 15 个场景
- [ ] 属性测试全部通过
- [ ] 回填脚本执行完成,<100 会员数据完整
- [ ] 数据保留定时任务配置完成
- [ ] 所有文档已同步更新
---
## 九、风险与缓解
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 回填触发大量 API 调用 | 成本突增 | 分批执行 + admin-web 二次确认 |
| 回填中途失败 | 部分会员数据不完整 | 断点续跑机制 |
| admin-web 查询大量 ai_run_logs 性能差 | 页面卡顿 | 分页 + 索引 + 90 天保留策略 |
| 全链路测试依赖真实百炼 API | 测试不稳定 | Mock 模式 + 真实 API 模式双轨 |
---
## 十、不在本 PRD 范围
- 多门店支持BACKLOG
- 消息队列(单独 PRD
- Prompt 版本管理的编辑功能BACKLOG本轮仅展示
- 前端刷新机制P1-5
- FDW 数据一致性P1-6
- 企业微信/邮件告警推送(后续迭代)

View File

@@ -0,0 +1,178 @@
# P15 — 小程序前后端联调Mock → 真实 API 切换
> 创建时间2026-03-22
> 状态:草案
> 优先级P0dev-trace-log 的前置依赖)
> 关联 Specdev-trace-log调试工具链
---
## 一、背景
小程序前端已完成 H5 → 原生迁移h5-miniprogram-migration spec和格式统一P13 spec
26 个后端接口已全部实现API 契约文档确认)。但当前 `services/api.ts` 所有函数返回空 Mock 数据,
`request` 导入已注释,前端与后端之间没有真实的 HTTP 通信。
本 PRD 覆盖"前后端联调"中 dev-trace-log 未涵盖的部分:将小程序从 Mock 数据切换到真实后端接口。
## 二、目标用户与场景
- 用户:开发者(前后端联调阶段)
- 场景:小程序连接本地 FastAPI 后端localhost验证登录、审核、任务、AI 模块的完整数据流
## 三、切换范围
### 3.1 本期切换P0
| 模块 | 涉及页面 | 涉及接口 |
|------|---------|---------|
| 认证 | login、apply、reviewing、no-permission | AUTH-1~5login、dev-login、me、refresh、apply |
| 任务 | task-list、task-detail4 变体) | TASK-1~4列表、详情、操作、绩效概览 |
| 审核 | reviewing、apply | AUTH-3me状态判断、AUTH-5apply |
| AI | chat、chat-history | CHAT-1~3对话列表、发送消息/SSE、历史记录 |
### 3.2 本期不做
| 模块 | 页面 | 原因 |
|------|------|------|
| 看板 | board-finance、board-customer、board-coach | 用户明确排除 |
| 助教 | coach-detail | 非优先联调模块 |
| 客户 | customer-detail、customer-service-records | 非优先联调模块 |
| 备注 | notes | 非优先联调模块 |
| 绩效 | performance、performance-records | 非优先联调模块 |
| 个人 | my-profile | 非优先联调模块 |
## 四、需求清单
### REQ-1API 环境配置
**描述**:创建统一的 API 配置模块,支持开发/测试/生产环境切换。
**验收标准**
- 创建 `apps/miniprogram/miniprogram/config/api.ts`
- 定义 `API_BASE_URL` 常量,开发环境指向 `http://localhost:8000`
- 支持通过构建变量或条件编译切换环境
- `utils/request.ts` 从配置模块读取 base URL禁止硬编码
### REQ-2恢复真实 API 调用
**描述**:将 `services/api.ts` 从 Mock 数据恢复为真实 HTTP 请求。
**验收标准**
- 取消 `request` 导入的注释
- 移除 `delay()` 辅助函数
- 移除所有空 Mock 数据返回
- P0 模块认证、任务、审核、AI的 service 函数改为调用 `request()` 发起真实 HTTP 请求
- 非 P0 模块暂时保留 Mock添加 `// TODO: P15 后续批次切换` 注释)
### REQ-3请求层统一封装
**描述**:确保 `utils/request.ts` 提供完整的请求封装能力。
**验收标准**
- 自动附加 `Authorization: Bearer <token>` 请求头(从 Storage 读取)
- 统一解析响应格式 `{ code: 0, data: ... }` / `{ code: number, message: string }`
- code ≠ 0 时抛出业务错误(含 code 和 message
- 网络异常(超时、断连)抛出网络错误(区分于业务错误)
- Token 过期401自动尝试 refreshrefresh 失败跳转登录页
- 请求超时设置(默认 30sAI 接口 120s
### REQ-4错误处理与用户反馈
**描述**:切换到真实接口后,网络错误、超时、空数据等场景需要有明确的用户反馈。
**验收标准**
- 网络错误:显示"网络连接失败,请检查网络后重试"+ 重试按钮
- 请求超时:显示"请求超时,请稍后重试"+ 重试按钮
- 业务错误code ≠ 0显示后端返回的 message
- 鉴权失败401/403跳转登录页或显示无权限页
- 空数据:显示对应的空态页面(已有四态框架,确保正确触发)
- 所有错误场景记录到 console开发环境便于调试
- 页面级 `pageState` 四态loading/empty/error/normal在真实 API 场景下正确流转
### REQ-5数据格式适配验证
**描述**:验证前端数据结构与后端接口返回的数据结构一致,不一致处做适配。
**验收标准**
- 后端响应统一 camelCaseResponseWrapperMiddleware 已处理)
- 前端类型定义(`services/api.ts` 中的 interface与后端实际返回字段一一对应
- 分页格式对齐:`{ items: T[], total, page, pageSize }`
- 金额字段number 类型,保留 2 位小数,前端通过 `formatMoney()` 展示
- 时间字段ISO 8601 字符串,前端通过 `formatDateShort()` / `formatDateFull()` 展示
- 不一致处在 service 层做适配转换,不在页面层处理
### REQ-6SSE 流式对接AI 模块)
**描述**AI 对话模块的 SSE 流式响应需要特殊处理,不能用普通 HTTP 请求。
**验收标准**
- `services/api.ts` 中 chat 相关函数使用 SSE 方式调用后端
- 支持流式接收 token 并实时渲染到对话界面
- SSE 连接异常时显示错误提示并允许重试
- SSE 超时处理AI 响应可能较慢,超时阈值 120s
- 流式过程中显示"正在思考..."loading 态
### REQ-7登录流程对接
**描述**:小程序登录流程从 Mock 切换到真实微信登录 → 后端换 Token 流程。
**验收标准**
- 开发环境使用 `POST /api/xcx/dev-login`openid 模拟登录)
- 登录成功后将 access_token 和 refresh_token 存入 Storage
- `app.ts``checkAuthStatus()` 调用真实 `GET /api/xcx/me` 判断用户状态
- 根据用户 statusnew/pending/approved/rejected/disabled正确路由到对应页面
- Token 过期自动 refreshrefresh 失败清除 Storage 并跳转登录页
## 五、数据流
```
小程序页面
↓ 调用 service 函数
services/api.ts
↓ 调用 request()
utils/request.ts
↓ wx.request() + Authorization header
↓ 响应解析 + 错误处理
FastAPI 后端 (localhost:8000)
↓ ResponseWrapperMiddleware (camelCase)
↓ { code: 0, data: ... }
返回页面 → 更新 data → 渲染
```
## 六、依赖与约束
| 依赖项 | 状态 | 说明 |
|--------|------|------|
| 后端 26 个接口 | ✅ 已实现 | API 契约文档确认 |
| API 契约文档 | ✅ 已就绪 | `docs/miniprogram-dev/API-contract.md` |
| 前端类型定义 | ✅ 已定义 | `services/api.ts` 中的 interface |
| 四态框架 | ✅ 已实现 | loading/empty/error/normal |
| 格式化工具函数 | ✅ 已实现 | P13 spec 已完成 |
| request.ts 封装 | ⚠️ 需验证 | 文件存在但当前未使用 |
| dev-trace-log | 🔜 并行开发 | 联调时提供调试可视化 |
## 七、验收标准(整体)
1. 登录流程dev-login → 获取 token → 自动路由到正确页面
2. 任务列表:真实数据加载、分页、筛选正常工作
3. 任务详情4 种变体页面正确渲染真实数据
4. AI 对话SSE 流式响应实时渲染,错误时有提示
5. 错误场景断网、超时、401、403、空数据均有正确的用户反馈
6. 非 P0 模块:保持 Mock 不受影响
## 八、风险
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 后端接口返回格式与前端类型不一致 | 页面渲染异常 | REQ-5 要求逐字段验证 |
| 微信开发者工具不支持 localhost | 无法联调 | 使用 dev-login 绕过微信登录;配置开发者工具"不校验合法域名" |
| SSE 在小程序中的兼容性 | AI 模块无法工作 | 需验证 wx.request 的 enableChunked 或使用 requestTask |
| Token 过期处理不完善 | 用户体验中断 | REQ-3 要求自动 refresh |
## 九、与 dev-trace-log 的关系
本 PRD 是 dev-trace-log 的前置依赖:
- 没有真实 API 调用 → trace 系统无数据可采集
- 建议并行推进:先完成 REQ-1~3基础设施再逐模块切换REQ-7 → REQ-2 → REQ-4~6
- 切换过程中使用 dev-trace-log 的 span 链路验证每个接口的完整处理流程

View File

@@ -0,0 +1,227 @@
# P16调度任务最小运行间隔机制 — task-min-run-interval
> 优先级P2调度系统增强
> 来源Session 58_5de84e40_195620 用户需求
> 预估工作量:中
> 依赖:无新增外部依赖
---
## 背景
现有调度系统(`scheduled_tasks` 表 + `scheduler.py`)支持 5 种调度类型once/interval/daily/weekly/cron但缺少"最小运行间隔"维度。部分 ETL 任务(如租户配置)实际只需 10 天运行一次,员工/助教信息 1 天一次,而订单类任务无此限制。
当前问题:
- 无法限制任务的最小再次运行间隔,调度到期即执行
- 无法防止同一任务并发执行(上一次未完成就再次入队)
- 手动执行(`POST /api/schedules/{id}/run`)无法区分"尊重间隔"和"强制执行"
---
## 需求Requirements
### 用户故事
1. 作为管理员,我需要为每个调度任务设置最小运行间隔(如 10 天、1 天),使任务即使调度到期也不会在间隔内重复执行,避免资源浪费。
2. 作为管理员,我需要在必要时强制执行某个任务(绕过最小间隔限制),以应对紧急数据同步需求。
3. 作为管理员,我需要在调度任务列表中看到每个任务的最小间隔配置和上次成功执行时间,以便了解任务运行状态。
### 验收标准
- AC1`scheduled_tasks` 表新增 `min_run_interval_value`INTEGER`min_run_interval_unit`VARCHAR`last_success_at`TIMESTAMPTZ3 个字段
- AC2调度器轮询时`now() - last_run_at < min_run_interval`(换算为秒),跳过本次执行并推进 `next_run_at`
- AC3任务执行失败时不更新 `last_success_at`,允许下次调度到期时立即重试(失败不算有效执行)
- AC4若任务 `last_status = 'running'`(上次未完成),跳过本次入队,标记为 `skipped_concurrent`
- AC5`POST /api/schedules/{id}/run` 新增 `force` 查询参数,`force=true` 时绕过最小间隔和并发检查
- AC6Admin Web 创建/编辑调度任务表单新增"最小运行间隔"字段(数字输入 + 单位下拉:分钟/小时/天)
- AC7Admin Web 任务列表新增"最小间隔"列和"上次成功"列
- AC8Admin Web "手动执行"按钮增加"强制执行"勾选项
- AC9现有调度任务 `min_run_interval_value` 默认 0无限制向后兼容
- AC10所有变更有对应的 BD 手册更新和审计记录
---
## 设计要点
### 核心概念
- **最小运行间隔**:任务开始执行后的最小等待时间(非完成后),基于 `last_run_at` 判断
- **有效执行**:仅成功完成的执行才更新 `last_success_at`;失败不重置间隔计时器,允许立即重试
- **并发保护**:若上一次执行仍在进行中(`last_status = 'running'`),跳过本次入队
- **强制执行**:通过 API `force=true` 参数绕过最小间隔和并发检查
### DDL 变更(`scheduled_tasks` 表)
新增 3 个字段:
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `min_run_interval_value` | INTEGER | 0 | 最小间隔数值0 = 无限制) |
| `min_run_interval_unit` | VARCHAR(20) | `'minutes'` | 间隔单位:`minutes` / `hours` / `days` |
| `last_success_at` | TIMESTAMPTZ | NULL | 最后一次成功执行的时间 |
> `min_run_interval_value = 0` 表示无限制,与现有行为完全一致,确保向后兼容。
### 调度器逻辑修改(`scheduler.py`
`check_and_enqueue()` 方法的判断流程变更为:
```
对每个到期任务enabled=true AND next_run_at <= now
1. 并发检查if last_status == 'running' → 跳过,标记 skipped_concurrent
2. 间隔检查if min_run_interval_value > 0
a. 计算 min_interval_seconds = convert(value, unit)
b. if last_run_at IS NOT NULL AND (now - last_run_at) < min_interval_seconds
→ 跳过,推进 next_run_at标记 skipped_interval
3. 正常入队执行
```
SQL 查询扩展(新增读取字段):
```sql
SELECT id, site_id, task_config, schedule_config,
min_run_interval_value, min_run_interval_unit,
last_run_at, last_status
FROM scheduled_tasks
WHERE enabled = TRUE
AND next_run_at IS NOT NULL
AND next_run_at <= NOW()
ORDER BY next_run_at ASC
```
任务完成回调需区分成功/失败:
- 成功:`last_status = 'completed'`,同时更新 `last_success_at = NOW()`
- 失败:`last_status = 'failed'`,不更新 `last_success_at`
### API 扩展(`schedules.py`
| 端点 | 变更 |
|---|---|
| `POST /api/schedules` | `CreateScheduleRequest` 新增 `min_run_interval_value`int, default=0`min_run_interval_unit`str, default='minutes' |
| `PUT /api/schedules/{id}` | `UpdateScheduleRequest` 新增同上两个可选字段 |
| `GET /api/schedules` | `ScheduleResponse` 新增 `min_run_interval_value``min_run_interval_unit``last_success_at` |
| `POST /api/schedules/{id}/run` | 新增查询参数 `force: bool = False``force=true` 时绕过间隔和并发检查 |
### Admin Web 变更(`ScheduleTab.tsx`
**创建/编辑表单**
- 新增"最小运行间隔"行:`InputNumber`(数值)+ `Select`(单位:分钟/小时/天)
- 数值为 0 时显示提示"无限制"
- 位置:在调度类型配置区域下方
**任务列表表格**
- 新增"最小间隔"列:显示格式如"10 天"、"1 小时"、"无限制"
- 新增"上次成功"列:显示 `last_success_at` 的相对时间(如"2 小时前"
**手动执行**
- 现有"立即执行"按钮点击后弹出确认框
- 确认框新增"强制执行(忽略最小间隔)"勾选项,默认不勾选
- 勾选后调用 `POST /api/schedules/{id}/run?force=true`
### ETL 任务注册
ETL 侧 `TaskMeta``TaskRegistry` 不需要修改。最小运行间隔是调度层概念,在 `scheduled_tasks` 表中配置,与 ETL 任务注册解耦。
---
## 数据流向
```
Admin WebScheduleTab.tsx
├─ 创建/编辑表单 → min_run_interval_value + min_run_interval_unit
└─ 手动执行 → force=true/false
↓ APIschedules.py
数据库scheduled_tasks 表
├─ min_run_interval_value (INTEGER)
├─ min_run_interval_unit (VARCHAR)
└─ last_success_at (TIMESTAMPTZ)
↓ 调度器轮询scheduler.py每 30 秒)
判断逻辑:
1. 并发检查 → last_status == 'running' → 跳过
2. 间隔检查 → now - last_run_at < min_interval → 跳过
3. 正常入队 → task_queue.enqueue()
↓ 任务执行完成回调
更新 scheduled_tasks
- 成功 → last_status='completed', last_success_at=NOW()
- 失败 → last_status='failed'last_success_at 不变)
```
---
## 边界条件与风险
| 场景 | 处理方式 |
|---|---|
| `min_run_interval_value = 0` | 无限制,与现有行为一致 |
| `last_run_at IS NULL`(从未执行) | 跳过间隔检查,正常执行 |
| 任务执行失败后立即到期 | `last_success_at` 未更新,但间隔检查基于 `last_run_at`(开始时间),所以失败后仍需等待间隔。但因为失败不算有效执行,下次到期时 `last_run_at` 已过间隔,可正常执行 |
| 手动执行 + force=false + 间隔未到 | 返回 409 Conflict提示"最小运行间隔未到,距下次可执行还有 X 分钟" |
| 手动执行 + force=true | 绕过所有检查,直接入队 |
| 并发:上次仍在 running | 跳过入队,`last_status` 标记为 `skipped_concurrent`(不覆盖原 running 状态,仅日志记录) |
| 调度器重启后 | `last_run_at``last_success_at` 持久化在数据库,重启无影响 |
---
## 不做什么(明确排除)
- 不修改 ETL 任务注册机制(`TaskMeta`/`TaskRegistry`
- 不新增独立的 `task_run_policy` 表(直接在 `scheduled_tasks` 表扩展)
- 不提供批量 seed SQL 设定初始间隔值(用户逐个配置)
- 不修改 `schedule_config` JSONB 结构(新字段放在表级列)
- 不涉及 ETL 侧的执行逻辑修改
- 不涉及小程序或 MCP Server
---
## 任务清单
### 数据库层
- [x] T1DDL 迁移 — `scheduled_tasks` 新增 3 个字段
- 迁移脚本:`db/zqyy_app/migrations/2026-03-22__p16_min_run_interval.sql`
- DDL 基线同步:`docs/database/ddl/zqyy_app__public.sql`
### 后端层
- [x] T2Schema 更新 — `ScheduleConfigSchema` 不变,`CreateScheduleRequest`/`UpdateScheduleRequest`/`ScheduleResponse` 新增字段
- 文件:`apps/backend/app/schemas/schedules.py`
- [x] T3调度器核心逻辑 — `check_and_enqueue()` 新增并发检查 + 间隔检查
- 文件:`apps/backend/app/services/scheduler.py`
- 新增辅助函数 `_convert_interval_to_seconds(value, unit)`
- [x] T4API 路由 — 创建/更新端点支持新字段,手动执行端点支持 `force` 参数
- 文件:`apps/backend/app/routers/schedules.py`
- [x] T5任务完成回调 — 区分成功/失败,成功时更新 `last_success_at`
- 文件:`apps/backend/app/services/task_queue.py`
### 前端层
- [x] T6ScheduleTab 表单 — 新增"最小运行间隔"输入(数字 + 单位下拉)
- 文件:`apps/admin-web/src/components/ScheduleTab.tsx`
- [x] T7ScheduleTab 列表 — 新增"最小间隔"列和"上次成功"列
- 文件:同 T6
- [x] T8手动执行确认框 — 新增"强制执行"勾选项
- 文件:同 T6
### 文档与验证
- [x] T9BD 手册更新 — `scheduled_tasks` 表新增字段说明
- 文件:`docs/database/BD_Manual_scheduled_tasks.md`
- [x] T10验证 — Monorepo 属性测试通过OpenAPI 契约同步;文档同步完成
### 涉及文件汇总
| 模块 | 文件路径 | 操作 |
|---|---|---|
| DDL 迁移 | `db/zqyy_app/migrations/2026-03-22__p16_min_run_interval.sql` | 新增 |
| DDL 基线 | `docs/database/ddl/zqyy_app__public.sql` | 修改 |
| Schema | `apps/backend/app/schemas/schedules.py` | 修改 |
| 调度器 | `apps/backend/app/services/scheduler.py` | 修改 |
| API 路由 | `apps/backend/app/routers/schedules.py` | 修改 |
| 任务队列 | `apps/backend/app/services/task_queue.py` | 可能修改(回调) |
| Admin Web | `apps/admin-web/src/components/ScheduleTab.tsx` | 修改 |
| BD 手册 | `docs/database/BD_Manual_scheduled_tasks.md` | 新建/修改 |

View File

@@ -0,0 +1,516 @@
# P17助教客户归属与任务生成引擎 — 商业逻辑 PRD
> 版本v1.0 | 日期2026-03-24 | 作者Neo
> 依赖P4核心业务层、ETL INDEX 层RS/OS/MS/ML/WBI/NCI
---
## 0. 文档背景与目标
### 0.1 问题来源
本文档源自 2026-03-24 的三次会话(#20 / #21 / #22)对任务生成器的持续审查,核心发现两大系统性问题:
**问题 A — 客户归属失控**:任务生成器在分配"召回类"任务时,仅凭"是否绑定微信"来圈定助教候选池,而不是依据助教与客户之间真实发生的服务关系。导致全店所有客户的召回任务都堆给唯一绑定了微信的助教,任务量严重失衡。
**问题 B — 客户转移无保护**:当召回连续失败后需要将客户转给其他助教跟进,但现行逻辑缺少任何保护机制:
- 没有"门店助教规模保护"——助教数量未达标时就启动转移,容易混乱
- 没有"入驻时间保护"——新助教未经历足够交互就被分配陌生客户
- 没有"服务关系门槛"——客户有可能被转给从未服务过他的助教,关系冷启动成本极高
### 0.2 类比:助教 = 台球厅的销售 + 客户运营
助教的角色本质上同时承担两件事:
| 职能 | 销售视角 | 客户运营视角 |
|------|---------|-------------|
| 核心工作 | 把"流失/新客"召回到店并成交 | 和已服务客户维持稳定关系 |
| 核心指标 | WBI流失风险分、NCI新客转化分 | RS关系强度、MS升温动量 |
| 任务类型 | 高优先召回、优先召回 | 关系构建、客户回访 |
| 客户归属逻辑 | 谁有机会承接该客户的召回 | 谁是该客户的主责助教 |
因此,**客户归属**和**任务分配**需要两套紧密联动但逻辑独立的规则。
### 0.3 本文档目标
1. 定义清晰、可落地的**客户归属算法**(基于 OS/RS 四象限模型)
2. 定义完整、可解释的**任务生成算法**(基于归属约束 + 指数门槛 + 四种任务类型)
3. 定义**客户转移保护机制**(三重保护:规模保护 + 时间保护 + 关系门槛)
4. 给出每个决策点的**参数化方案**,支持门店级配置调整
---
## 1. 概念词典
| 术语 | 通俗解释 | 技术对应 |
|------|---------|----------|
| **RS关系强度分** | 助教和客户之间服务关系的紧密程度上过的课越多、越近期分越高0-10分 | `rs_display`,来自 `dws_member_assistant_relation_index` |
| **OS归属份额** | 在服务过该客户的所有助教中,该助教占多大比重;决定"主责/共管/待认领"标签 | `os_label ∈ {MAIN, COMANAGE, POOL, UNASSIGNED}` |
| **MS升温动量分** | 最近服务是在增多还是减少升温则分高降温则分低0-10分 | `ms_display` |
| **ML付费关联分** | 客户的消费台账中有多少是由该助教直接带来的0-10分 | `ml_display` |
| **WBI流失风险分** | 这个客户有多久没来了、来的频率是否下降分越高越需要主动联系0-10分 | `display_score`,来自 `dws_member_winback_index` |
| **NCI新客转化分** | 新客户被转化为回头客的紧迫程度0-10分 | `display_score`,来自 `dws_member_newconv_index` |
| **客户转移** | 原主责助教召回失败超过阈值后,系统将该客户的召回任务扩展给其他有服务关系的助教 | task_generator 中的转移逻辑 |
| **门店规模保护** | 若店内绑定微信的在职助教比例不足50%,禁用客户转移功能 | `guard_assistant_coverage_ratio` |
| **入驻时间保护** | 助教绑定微信后10天内不接收转移客户 | `guard_new_assistant_days` |
| **服务关系门槛** | 只把客户转给曾经服务过该客户的助教 | `os_label ≠ UNASSIGNED` |
---
## 2. 客户归属算法
### 2.1 设计原则
客户归属解决的问题是:**一个客户应该由哪个(些)助教负责跟进?**
台球厅的实际场景是:一个客户可能被多个助教服务过,但服务次数和亲密度差别很大。归属算法需要把这种模糊关系量化为清晰的"主责/共管/待认领/未归属"四个层级。
> 参考 CRM 行业最佳实践Salesforce/HubSpot 的 Account Ownership 模型):
> 当多名销售都服务过同一客户时,最优解不是"谁先认领谁拥有",而是用历史交互深度加权判断,避免资深关系被新人抢占。
### 2.2 OS 归属标签定义
OS 由 ETL `RelationIndexTask` 已实现,输出 `os_label` 字段,定义如下:
```
MAIN — 主责助教:在服务过该客户的助教中,该助教的 os_share 显著高于其他人
COMANAGE — 共管助教:多名助教的 os_share 差距不大RS 相对差 < 50%),均视为负责人
POOL — 待认领:有过服务记录,但 os_share 较低,属于潜在接管候选
UNASSIGNED — 无关联:从未为该客户提供过服务记录
```
**RS 相对差公式(来自 PRD 审阅 Q3.2**
```
对于助教 Ars=8和助教 Brs=5
相对差 = (8 - 5) / 8 = 0.375 < 0.5
→ 两人共管该客户COMANAGE
对于助教 Ars=9和助教 Brs=4
相对差 = (9 - 4) / 9 = 0.556 > 0.5
→ 助教 A 为主责MAIN助教 B 降为 POOL
```
### 2.3 归属判定流程
```
输入:某客户的所有 (assistant_id, rs_display) 记录
Step 1 — 过滤无效记录
rs_display = 0 → 视为无有效服务os_label = UNASSIGNED
Step 2 — 排序
按 rs_display DESC 排列所有助教
Step 3 — 主责判定
取最高分助教 A
若 A 是唯一有效助教,或与第二名 B 的相对差 ≥ 50%
→ A 标记为 MAIN其余有效助教标记为 POOL
Step 4 — 共管判定
若 A 与 B 的相对差 < 50%
→ 继续对 B 与 C 执行同样判断
→ 所有满足"与最高分相对差 < 50%"的助教标记为 COMANAGE
→ 其余有效助教标记为 POOL
Step 5 — 输出
写入 os_label + os_share + os_rank 字段(由 ETL 层计算,本 PRD 只消费结果)
```
### 2.4 归属与任务分配的映射关系
| os_label | 召回类任务 | 关系构建任务 |
|----------|-----------|-------------|
| MAIN | ✅ 有资格接收 | ✅ 有资格接收 |
| COMANAGE | ✅ 有资格接收 | ✅ 有资格接收 |
| POOL | ❌ 常规不分配;仅在"客户转移"条件触发后分配召回 | ❌ 不分配 |
| UNASSIGNED | ❌ 永不分配 | ❌ 永不分配 |
>
**关键改变**:将 WBI/NCI 召回任务的候选池从「绑定微信的助教」改为「对该客户 os_label ∈ {MAIN, COMANAGE} 的助教」。这是修复"小燕任务爆炸"问题的核心。
---
## 3. 客户转移机制
### 3.1 触发条件
客户转移是召回失败后的兜底机制,类似销售中的「线索升级」:
```
触发条件:
某客户的主责/共管助教MAIN/COMANAGE对该客户的召回任务
在连续 N 个任务周期内均未完成status ≠ completed
且 WBI 或 NCI 持续高于门槛值
默认参数:
consecutive_recall_fail_cycles = 3 连续3个生成周期未完成
min_wbi_for_transfer = 5.0 WBI > 5 才触发转移)
```
### 3.2 三重保护机制
客户转移在触发前,必须通过三道检查。任一检查不通过,本次不转移。
#### 保护 1 — 门店助教规模保护
```
规则:
若 (店内绑定微信的在职助教数 / 店内全部在职助教总数) < 0.5
→ 客户转移功能全局禁用
业务意义:
门店大部分助教都没绑定小程序时,系统对助教团队的覆盖率太低,
此时启动转移会造成信息盲区(被转出的任务助教看不到)。
只有当绑定率超过 50% 时,才能保障转移链路有效。
参数guard_assistant_coverage_ratio = 0.5(可配置)
```
#### 保护 2 — 入驻时间保护
```
规则:
助教首次绑定微信的时间binding_created_at距今不足 10 天
→ 该助教本轮不参与转移候选池
业务意义:
新助教刚入驻,还没有建立足够的客户印象,
贸然分配陌生客户会降低召回成功率,也打击新助教积极性。
10 天保护期给新助教建立自己客户基础的空间。
参数guard_new_assistant_days = 10可配置
```
#### 保护 3 — 服务关系门槛
```
规则:
待转入的助教对该客户的 os_label 必须 ∈ {POOL}
(即曾经服务过该客户,但目前归属份额较低)
os_label = UNASSIGNED 的助教永远不参与转移候选
业务意义:
从未服务过该客户的助教,关系完全冷启动。
不论技术上可以转,业务上也不应该这样做。
转移只在"有过接触但目前不是主责"的助教之间发生,
最大化利用已有的关系温度。
参数transfer_eligible_labels = ['POOL'](固定,不可放开到 UNASSIGNED
```
### 3.3 转移候选排序
通过三重保护后,对候选助教按以下优先级排序,取得分最高的 1 名(或多名,取决于配置):
```
转移得分 = w_rs × rs_display + w_ms × ms_display + w_ml × ml_display
默认权重:
w_rs = 0.5 (关系强度,历史服务深度)
w_ms = 0.3 (升温动量,关系是在改善还是冷却)
w_ml = 0.2 (付费关联,客户是否在该助教服务期间消费)
业务意义:
优先把客户转给"之前有服务基础、且关系正在升温、且有消费记录"的助教,
而不是随机转给"历史最高分"。MS 权重确保选的是当下状态最好的关系,
而不是过去最好的关系。
```
### 3.4 转移后的归属处理
```
转移发生后:
新助教的任务状态 = active高优先召回 or 优先召回)
原主责助教的同类型召回任务 status = 'transferred'(新增任务状态)
—— 不关闭原任务,而是标记为"已转移",供审计和历史查询
若转移后新助教也失败(连续 consecutive_recall_fail_cycles 次),
且 POOL 中还有其他候选助教,可再次转移
但每个客户的累计转移次数上限 = max_transfer_count默认 2 次)
超过上限后,任务进入 PENDING_REVIEW 状态,等待人工介入
```
---
## 4. 任务生成算法(重新设计版)
### 4.1 总体流程
```
每日 07:00 任务生成器 run() 执行:
Step 1 — 确定全店有效助教池
查询 dws_member_assistant_relation_index
取 os_label ∈ {MAIN, COMANAGE} 的所有 (assistant_id, member_id) 对
(放弃以 user_assistant_binding 为入口的旧逻辑)
Step 2 — 读取指数
对每个 assistant_id 关联的 member_id 集合:
WBI = dws_member_winback_index.display_score按 member_id 查)
NCI = dws_member_newconv_index.display_score按 member_id 查)
RS = dws_member_assistant_relation_index.rs_display按 assistant_id + member_id 查)
OS = dws_member_assistant_relation_index.os_label同上
MS = dws_member_assistant_relation_index.ms_display同上
Step 3 — 归属过滤(新增)
对每个 (assistant_id, member_id) 对:
若 os_label ∉ {MAIN, COMANAGE} → 跳过召回类任务判断
POOL 助教只在"客户转移"触发后才参与)
Step 4 — 任务类型判定 determine_task_type()
见第 4.2 节
Step 5 — 任务状态检查与写入
见第 4.3 节
Step 6 — 客户转移检查(独立子流程)
见第 3 节
Step 7 — 更新 trigger_jobs 时间戳
```
### 4.2 任务类型判定算法(四级漏斗)
```
function determine_task_type(os_label, wbi, nci, rs, has_pending_recall, has_follow_up_note):
priority_score = max(wbi, nci)
-- 漏斗第一级:高优先召回
if priority_score > 7 AND os_label ∈ {MAIN, COMANAGE}:
return 'high_priority_recall'
-- 漏斗第二级:优先召回
if priority_score > 5 AND os_label ∈ {MAIN, COMANAGE}:
return 'priority_recall'
-- 漏斗第三级:客户回访
-- (召回已完成 ETL 确认,但助教尚未提交备注)
if has_pending_recall == True AND has_follow_up_note == False:
return 'follow_up_visit'
-- 漏斗第四级:关系构建
-- RS ≤ 1 视为无有效交互,不生成任务
if 1 < rs < 6 AND os_label ∈ {MAIN, COMANAGE}:
return 'relationship_building'
return None -- 不生成任务
```
**四级漏斗的业务逻辑说明**
| 级别 | 任务 | 触发信号 | 业务含义 |
|------|------|---------|----------|
| 1 | 高优先召回 | WBI 或 NCI > 7且本人是主责/共管 | 客户流失风险极高,必须今天联系 |
| 2 | 优先召回 | WBI 或 NCI > 5且本人是主责/共管 | 客户有流失迹象,本周内联系 |
| 3 | 客户回访 | 召回成功但未备注ETL 已确认到店) | 召回成功后的温度维护,不能凉掉 |
| 4 | 关系构建 | RS 在 1-6 之间,关系有提升空间 | 日常维护客情,升温关系 |
> 注意:漏斗是互斥优先的。一个客户-助教对同一时刻只生成一条最高优先级的任务。
### 4.3 任务状态检查与写入逻辑
```
对每个 (assistant_id, member_id, new_task_type)
Case A — 已存在相同类型的 active 任务:
→ 跳过skip不更新 created_at
stats['skipped'] += 1
Case B — 已存在不同类型的 active 任务:
→ 将旧任务 status 改为 'inactive'
→ 创建新任务status = 'active'
→ 记录 coach_task_history
stats['replaced'] += 1
Case C — 不存在 active 任务:
→ 直接创建新任务
stats['created'] += 1
Case D — new_task_type = None
→ 检查 follow_up_visit 是否超过48小时 → inactive
stats['skipped'] += 1
```
### 4.4 关系构建任务的 RS 门槛说明
| RS 区间 | 含义 | 是否生成任务 |
|---------|------|------------|
| RS = 0 | 无有效服务数据 | 否 |
| RS ≤ 1 | 仅 1 次以下交互,关系未建立 | 否 |
| 1 < RS < 6 | 有初步关系但未牢固,黄金维护窗口 | 是(关系构建) |
| RS ≥ 6 | 关系已牢固,无需系统催动 | 否 |
---
## 5. 参数总览与配置说明
所有参数存储于 `biz.cfg_task_generator_params`,支持按 `site_id` 级别覆盖。
| 参数名 | 默认值 | 说明 |
|--------|--------|------|
| `high_priority_recall_threshold` | 7.0 | max(WBI,NCI) 超过此值生成高优先召回 |
| `priority_recall_threshold` | 5.0 | max(WBI,NCI) 超过此值生成优先召回 |
| `rs_min_for_relationship` | 1.0 | RS ≤ 此值不生成关系构建 |
| `rs_max_for_relationship` | 6.0 | RS ≥ 此值不生成关系构建 |
| `consecutive_recall_fail_cycles` | 3 | 连续失败多少轮触发客户转移 |
| `min_wbi_for_transfer` | 5.0 | WBI 低于此值不触发转移 |
| `guard_assistant_coverage_ratio` | 0.5 | 绑定率低于此值禁用转移 |
| `guard_new_assistant_days` | 10 | 新助教入驻保护天数 |
| `transfer_score_w_rs` | 0.5 | 转移候选排序RS 权重 |
| `transfer_score_w_ms` | 0.3 | 转移候选排序MS 权重 |
| `transfer_score_w_ml` | 0.2 | 转移候选排序ML 权重 |
| `max_transfer_count` | 2 | 单客户最大累计转移次数 |
| `follow_up_visit_retention_hours` | 48 | 回访任务最低保留时长(小时) |
## 6. 数据库变更需求
### 6.1 新增字段:`biz.coach_tasks`
```sql
-- 新增任务状态枚举值
ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'transferred';
ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'pending_review';
-- 新增转移追踪字段
ALTER TABLE biz.coach_tasks
ADD COLUMN transfer_count INTEGER NOT NULL DEFAULT 0,
ADD COLUMN transferred_from BIGINT REFERENCES biz.coach_tasks(id),
ADD COLUMN transferred_at TIMESTAMPTZ;
```
### 6.2 新增表:`biz.cfg_task_generator_params`
```sql
CREATE TABLE biz.cfg_task_generator_params (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT, -- NULL 表示全局默认值
param_key VARCHAR(64) NOT NULL,
param_value NUMERIC NOT NULL,
description TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (site_id, param_key)
);
```
### 6.3 新增表:`biz.coach_task_transfer_log`
```sql
CREATE TABLE biz.coach_task_transfer_log (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
from_assistant_id BIGINT NOT NULL,
to_assistant_id BIGINT NOT NULL,
from_task_id BIGINT NOT NULL REFERENCES biz.coach_tasks(id),
to_task_id BIGINT REFERENCES biz.coach_tasks(id),
transfer_reason TEXT,
guard_checks JSONB, -- 三重保护检查结果
transfer_score NUMERIC,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
---
## 7. 核心流程伪代码(供开发参考)
### 7.1 任务生成器主流程(重写版)
```python
def run() -> dict:
stats = {"created": 0, "replaced": 0, "skipped": 0, "transferred": 0}
params = load_params() # 从 cfg_task_generator_params 加载
# Step 1: 以 OS 归属为入口(取代旧的 user_assistant_binding 入口)
ownership_pairs = query("""
SELECT assistant_id, member_id, os_label,
rs_display, ms_display, ml_display
FROM app.v_dws_member_assistant_relation_index
WHERE os_label IN ('MAIN', 'COMANAGE')
""")
# Step 2: 批量读取 WBI / NCI
member_ids = {p['member_id'] for p in ownership_pairs}
wbi_map = fetch_wbi(member_ids)
nci_map = fetch_nci(member_ids)
# Step 3: 逐对生成任务
for pair in ownership_pairs:
process_pair(pair, wbi_map, nci_map, params, stats)
# Step 4: 客户转移子流程
run_transfer_check(params, stats)
update_trigger_timestamp('task_generator')
return stats
```
### 7.2 客户转移子流程
```python
def run_transfer_check(params, stats):
# 保护 1: 门店规模检查
if coverage_ratio() < params['guard_assistant_coverage_ratio']:
return # 全局禁用
# 查找连续失败达阈值的 (member_id, assistant_id) 对
for candidate in find_failed_recall_candidates(params):
pool = get_pool_assistants(candidate['member_id'])
eligible = [
a for a in pool
if days_since_binding(a) >= params['guard_new_assistant_days'] # 保护 2
and a['os_label'] == 'POOL' # 保护 3
]
if not eligible:
continue
# 按转移得分选最优候选
best = max(eligible, key=lambda a:
params['w_rs'] * a['rs'] +
params['w_ms'] * a['ms'] +
params['w_ml'] * a['ml']
)
do_transfer(candidate, best, stats)
```
---
## 8. 验收标准Acceptance Criteria
| # | 验收项 | 判定方式 |
|---|--------|----------|
| AC1 | 召回任务只分配给 os_label ∈ {MAIN, COMANAGE} 的助教 | 数据库核查,无 UNASSIGNED/POOL 助教的召回任务 |
| AC2 | 关系构建任务 RS 门槛正确1 < RS < 6 | 检查 relationship_building 任务对应的 rs_display |
| AC3 | 客户转移通过三重保护 | transfer_log.guard_checks 全部 pass |
| AC4 | 新助教10天内不接收转移 | transfer_log 中 to_assistant binding 距转移时间 ≥ 10 天 |
| AC5 | 绑定率 < 50% 全局禁用转移 | 低覆盖率场景下 transfer_log 无新记录 |
| AC6 | 相同类型任务不重复生成 | 重复运行两次,第二次 skipped = 第一次 created |
| AC7 | 回访任务最低保留48小时 | 将 created_at 回拨49小时验证 expiry check |
| AC8 | 转移累计上限生效 | 第3次转移触发 pending_review 状态 |
---
## 9. 开放问题与后续讨论
| # | 问题 | 优先级 |
|---|------|--------|
| O1 | OS 标签每4小时更新是否足以支撑7:00任务生成需确认 ETL 完成时间窗口 | 高 |
| O2 | follow_up_visit 由召回完成检测器触发 vs 本 PRD Step 3 漏斗判定,两者需对齐触发逻辑 | 高 |
| O3 | POOL 助教何时晋升为 COMANAGE/MAINOS 算法是否有晋升路径,还是纯由 RS 数据自然演进 | 中 |
| O4 | pending_review 状态的任务如何人工干预需要管理后台支持P10 租户管理后台范畴) | 中 |
| O5 | 多门店场景下 cfg_task_generator_params 的继承逻辑(全局默认 → 门店覆盖) | 低 |
---
## 10. 与现有 PRD/代码的关系
| 文档/模块 | 关系说明 |
|-----------|----------|
| `docs/prd/specs/P4-miniapp-core-business.md` | 本 PRD 是 P4 中任务生成章节的细化和纠错版,以本文档为准 |
| `apps/backend/app/services/task_generator.py` | 需按本 PRD 重写 `run()``_process_assistant()`,主要改动是入口改为 OS 归属 |
| `apps/backend/app/services/fdw_queries.py` | 需新增 `get_ownership_pairs()` 查询方法 |
| `docs/prd/PRD审阅-Q&A.md` Q3.2 | RS 50% 相对差公式来源,本 PRD 已完整引用 |
| ETL `RelationIndexTask` | OS/RS/MS/ML 的计算源头,本 PRD 只消费其结果,不修改 ETL 层 |

View File

@@ -0,0 +1,743 @@
# P18管理后台 — 任务引擎运营看板与参数管理
> 版本v2.1-reviewed | 日期2026-03-24 | 作者AI待评审
> 依赖P17助教客户归属与任务生成引擎、P10租户管理后台部分复用
> 状态:评审通过,可进入实施
---
## 0. 文档背景与目标
### 0.1 问题来源
P17 实现了助教客户归属、任务生成、客户转移三大引擎能力,但所有数据仅存在于数据库中,运营团队无法:
- 查看客户转移日志(谁被转给了谁、为什么、三重保护检查结果)
- 审核 `pending_review` 状态的任务(转移次数超限后需人工介入)
- 按门店调整任务生成参数(召回阈值、转移保护参数等)
- 监控任务生成器的运行健康度(生成/替换/跳过/转移统计)
同时,现有 `http://localhost:5173/trigger-jobs` 定时任务页面功能较基础(仅展示 + 手动执行),需要评估是否扩展为更完整的调度监控面板。
### 0.2 本文档目标
1. 定义 admin-web 中 P17 相关的三个新页面(转移日志、待审核任务、参数管理)
2. 评估现有 trigger-jobs 页面的扩展需求
3. 明确后端 API 需求(含 Pydantic schema 定义)
4. 给出优先级排序和实施建议
5. 明确技术实现方案(前端组件结构、路由注册、权限控制)
### 0.3 技术栈概要
| 层 | 技术 |
|----|------|
| 前端 | React 19 + Vite 6 + Ant Design 5 + axios + Zustand + react-router-dom 7 |
| API 客户端 | `apps/admin-web/src/api/client.ts`baseURL = `/api`JWT 自动附加 + 401 自动刷新 |
| 后端 | FastAPI + Pydantic v2 + asyncpg |
| 认证 | JWT`CurrentUser` 含 user_id / site_id / roles`Depends(get_current_user)` |
| 响应包装 | 全局中间件自动 `{ code: 0, data: <body> }`,前端 axios 拦截器自动解包 |
---
## 1. 现有 trigger-jobs 页面评估
### 1.1 当前能力
| 功能 | 状态 |
|------|------|
| 列出所有定时任务(名称、触发方式、配置、状态) | ✅ 已实现 |
| 展示上次/下次执行时间 | ✅ 已实现 |
| 展示最近错误信息 | ✅ 已实现 |
| 手动执行按钮(需确认) | ✅ 已实现 |
| 刷新按钮 | ✅ 已实现 |
### 1.2 缺失能力(待评估)
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 执行历史记录 | 中 | 当前只有 last_run_at无法查看历史执行记录和每次的统计结果 |
| 任务启用/禁用开关 | 中 | 当前只能查看状态,无法在界面上切换 |
| 执行结果统计 | 高 | task_generator 的 created/replaced/skipped/transferred 统计应可视化 |
| Cron 表达式可编辑 | 低 | 当前触发配置只读,修改需直接改数据库 |
| 执行耗时监控 | 低 | 无执行耗时记录 |
### 1.3 建议
trigger-jobs 页面的核心定位是"调度监控",不宜过度膨胀。建议:
- 短期:在现有页面增加"最近执行结果"列(展示 task_generator 的 stats JSON
- 中期:新增"执行历史"抽屉(点击任务名展开,展示最近 N 次执行记录)
- 长期:考虑是否需要独立的"调度中心"页面(类似 Airflow UI
> **决策O6**:暂不新增 `biz.trigger_job_execution_log` 表。短期通过在 `trigger_jobs` 表新增 `last_stats JSONB` 字段记录最近一次执行统计即可满足需求。中期再评估是否需要完整执行历史表。
---
## 2. 新增页面设计
### 2.1 页面一客户转移日志Transfer Log
**路由**`/task-engine/transfer-log`
**目标用户**:运营管理员(超级管理员看全部,门店管理员看本店)
**数据源**`biz.coach_task_transfer_log`
**展示内容**
| 列 | 数据来源 | 说明 |
|----|---------|------|
| 转移时间 | `created_at` | 降序排列 |
| 门店 | `site_id``biz.sites.site_name` | JOIN 门店表 |
| 客户 | `member_id` → 会员昵称/手机号 | FDW 关联 ETL 库会员维度表 |
| 原助教 | `from_assistant_id` → 助教姓名 | FDW 关联 ETL 库助教维度表 |
| 新助教 | `to_assistant_id` → 助教姓名 | 同上 |
| 转移原因 | `transfer_reason` | 文本展示 |
| 转移得分 | `transfer_score` | 数值,保留 2 位小数 |
| 保护检查 | `guard_checks` | JSON → 三项检查结果(✅/❌) |
**筛选条件**
- 门店(下拉选择,门店管理员自动锁定本店)
- 时间范围(日期选择器,默认最近 7 天)
- 助教(搜索框,支持原助教/新助教)
**操作**
- 无写操作,纯查看
- 支持导出 CSV后续迭代
> **决策O3**:转移日志不关联 WBI/NCI/RS 快照。原因:(1) 转移时刻的指数值可通过 `transfer_reason` 文本和 `transfer_score` 间接推断;(2) 存储快照会增加 `coach_task_transfer_log` 表宽度,且历史指数可从 DWS 层按日期回溯。如后续运营反馈需要,可在 `guard_checks` JSONB 中追加指数快照字段,无需 DDL 变更。
---
### 2.2 页面二待审核任务Pending Review
**路由**`/task-engine/pending-review`
**目标用户**:运营管理员(超级管理员看全部,门店管理员看本店)
**数据源**`biz.coach_tasks WHERE status = 'pending_review'`
**展示内容**
| 列 | 数据来源 | 说明 |
|----|---------|------|
| 创建时间 | `created_at` | 降序排列 |
| 门店 | `site_id` → 门店名称 | |
| 客户 | `member_id` → 会员昵称 | FDW 关联 |
| 当前助教 | `assistant_id` → 助教姓名 | FDW 关联 |
| 任务类型 | `task_type` | 中文映射(见下方枚举表) |
| 累计转移次数 | `transfer_count` | |
| 优先级分 | `priority_score` | WBI/NCI 分值 |
**任务类型中文映射**
| task_type | 中文 |
|-----------|------|
| `high_priority_recall` | 高优先召回 |
| `priority_recall` | 优先召回 |
| `follow_up_visit` | 客户回访 |
| `relationship_building` | 关系构建 |
**操作**
| 操作 | 说明 |
|------|------|
| 重新分配 | 弹窗选择目标助教 → 调用 reassign API → 原任务标记 `transferred`,新任务 `active` |
| 关闭任务 | 弹窗填写关闭原因 → 调用 close API → 原任务标记 `inactive` |
| 查看转移历史 | 抽屉展示该客户的所有转移记录(复用转移日志 API |
> **决策O1**:重新分配的候选助教列表获取方式 — 复用 P17 的转移候选逻辑。后端新增 `GET /api/admin/task-engine/pending-review/{task_id}/candidates` 端点,内部调用 `fdw_queries.get_pool_assistants(member_id)` 获取 POOL 助教列表,按转移得分排序返回。前端展示为下拉选择框,显示助教姓名 + 转移得分。若 POOL 为空,候选列表返回空数组,界面提示"该客户暂无符合转移条件的助教,请联系 ETL 团队确认数据覆盖情况"不提供降级到全店助教的选项P17 明确规定 UNASSIGNED 助教永远不分配)。若确实有紧急人工干预需求,可提供"强制指定"按钮,需额外二次确认弹窗,且操作记录中标注 `source: "manual_override"`,写入 `transfer_log.transfer_reason`。
---
### 2.3 页面三任务引擎参数管理Task Engine Config
**路由**`/task-engine/config`
**目标用户**:超级管理员(门店管理员只读)
**数据源**`biz.cfg_task_generator_params`
**展示内容**
表格形式,按参数分组,每行展示全局默认值 + 各门店覆盖值:
| 列 | 说明 |
|----|------|
| 参数名 | `param_key`,中文描述 |
| 全局默认值 | `site_id IS NULL` 的记录 |
| 门店覆盖值 | 特定 `site_id` 的记录(无覆盖时显示"使用默认"灰色标签) |
| 说明 | `description` 字段 |
| 操作 | 编辑 / 删除覆盖 |
**参数列表**(来自 P17 第 5 节):
| 参数 | 默认值 | 中文说明 | 校验规则 |
|------|--------|---------|----------|
| `high_priority_recall_threshold` | 7.0 | 高优先召回阈值 | 0-10numeric |
| `priority_recall_threshold` | 5.0 | 优先召回阈值 | 0-10numeric |
| `rs_min_for_relationship` | 1.0 | 关系构建 RS 下限 | 0-10numeric |
| `rs_max_for_relationship` | 6.0 | 关系构建 RS 上限 | 0-10> rs_min |
| `consecutive_recall_fail_cycles` | 3 | 连续失败触发转移的轮数 | 1-10integer |
| `min_wbi_for_transfer` | 5.0 | 触发转移的最低 WBI | 0-10numeric |
| `guard_assistant_coverage_ratio` | 0.5 | 门店助教绑定率保护阈值 | 0-1numeric |
| `guard_new_assistant_days` | 10 | 新助教入驻保护天数 | 1-90integer |
| `transfer_score_w_rs` | 0.5 | 转移排序 RS 权重 | 0-1三项之和 = 1 |
| `transfer_score_w_ms` | 0.3 | 转移排序 MS 权重 | 0-1三项之和 = 1 |
| `transfer_score_w_ml` | 0.2 | 转移排序 ML 权重 | 0-1三项之和 = 1 |
| `max_transfer_count` | 2 | 单客户最大转移次数 | 1-5integer |
| `follow_up_visit_retention_hours` | 48 | 回访任务保留时长(小时) | 1-168integer |
**操作**
| 操作 | 说明 | 权限 |
|------|------|------|
| 编辑参数值 | 行内编辑,保存后立即生效 | 超级管理员 |
| 新增门店覆盖 | 选择门店 + 参数,设置覆盖值 | 超级管理员 |
| 删除门店覆盖 | 恢复使用全局默认值 | 超级管理员 |
| 重置为默认 | 将全局默认值恢复为 P17 定义的初始值 | 超级管理员 |
**前端校验规则**
- 权重参数(`w_rs` + `w_ms` + `w_ml`)之和必须 = 1.0(容差 0.001)。前端将三个权重参数合并为一个"权重配置"卡片,同时展示三个输入框,整体保存。后端 PUT 接口对权重类参数做联合校验:收到任一权重参数修改时,自动读取当前另外两个权重的值做求和校验
- 阈值参数范围 0-10与指数分值范围一致
- `rs_max_for_relationship` 必须 > `rs_min_for_relationship`
- 修改后弹出确认对话框,展示变更前后对比
> **决策O2**:参数修改直接生效,不需要审批流程。原因:(1) 参数修改频率极低(预计每月 1-2 次);(2) 修改记录通过 `updated_at` 字段和后端日志可追溯;(3) 引入审批流程会增加不必要的复杂度。后续如需审批,可在 `cfg_task_generator_params` 表新增 `updated_by` 字段记录操作人。
---
## 3. 后端 API 需求
### 3.1 转移日志 API
**路由文件**`apps/backend/app/routers/admin_task_engine.py`(新建)
```
GET /api/admin/task-engine/transfer-log
Query: site_id?, from_date?, to_date?, assistant_id?, page=1, page_size=20
Response: { items: TransferLogItem[], total: int }
权限: get_current_user门店管理员自动按 site_id 过滤)
GET /api/admin/task-engine/transfer-log/{member_id}/history
Response: { items: TransferLogItem[] }
权限: get_current_user
```
**SQL 示例(转移日志列表)**
```sql
SELECT
tl.id, tl.site_id, tl.member_id,
tl.from_assistant_id, tl.to_assistant_id,
tl.transfer_reason, tl.transfer_score,
tl.guard_checks, tl.created_at,
s.site_name
FROM biz.coach_task_transfer_log tl
JOIN biz.sites s ON s.site_id = tl.site_id
WHERE ($1::bigint IS NULL OR tl.site_id = $1)
AND ($2::date IS NULL OR tl.created_at >= $2)
AND ($3::date IS NULL OR tl.created_at < $3 + INTERVAL '1 day')
AND ($4::bigint IS NULL OR tl.from_assistant_id = $4 OR tl.to_assistant_id = $4)
ORDER BY tl.created_at DESC
LIMIT $5 OFFSET $6;
```
> **姓名关联实现方式**:由于 postgres_fdw 不传递 GUC 参数,不能在 admin-web 后端直接用 FDW JOIN。实现上采用 Python 层批量合并:先从 `biz.coach_task_transfer_log` 取分页数据,然后收集所有 `member_id` 和 `assistant_id`,批量调用 `fdw_queries.get_member_names(member_ids)` 和新增的 `fdw_queries.get_assistant_names(assistant_ids)` 工具函数(通过 `_fdw_context()` 创建独立 ETL 连接),最后在 Python 层做字典合并。待审核任务列表同理。避免在 SQL 里做跨库 JOIN。
### 3.2 待审核任务 API
```
GET /api/admin/task-engine/pending-review
Query: site_id?, page=1, page_size=20
Response: { items: PendingReviewItem[], total: int }
权限: get_current_user
GET /api/admin/task-engine/pending-review/{task_id}/candidates
Response: { candidates: CandidateAssistant[] }
权限: get_current_user
说明: 返回可接收转移的候选助教列表POOL 助教 + 降级全店助教)
POST /api/admin/task-engine/pending-review/{task_id}/reassign
Body: { to_assistant_id: int }
Response: { success: bool, new_task_id: int }
权限: get_current_user + roles 包含 'super_admin'
副作用: 原任务 status → 'transferred',新建 active 任务,写入 transfer_log
POST /api/admin/task-engine/pending-review/{task_id}/close
Body: { reason: str }
Response: { success: bool }
权限: get_current_user + roles 包含 'super_admin'
副作用: 任务 status → 'inactive'abandon_reason = reason
```
### 3.3 参数管理 API
```
GET /api/admin/task-engine/config
Query: site_id?(不传返回全局默认 + 所有门店覆盖)
Response: { params: ConfigParam[] }
权限: get_current_user
PUT /api/admin/task-engine/config/{param_id}
Body: { param_value: float }
Response: { success: bool }
权限: get_current_user + roles 包含 'super_admin'
POST /api/admin/task-engine/config
Body: { site_id: int, param_key: str, param_value: float }
Response: { success: bool, id: int }
权限: get_current_user + roles 包含 'super_admin'
DELETE /api/admin/task-engine/config/{param_id}
Response: { success: bool }
权限: get_current_user + roles 包含 'super_admin'
约束: 不允许删除全局默认值site_id IS NULL仅允许删除门店覆盖
```
---
## 4. Pydantic Schema 定义
**文件位置**`apps/backend/app/schemas/admin_task_engine.py`(新建)
```python
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, Field
# ── 转移日志 ──
class TransferLogItem(BaseModel):
id: int
site_id: int
site_name: str = ""
member_id: int
member_name: str = "" # FDW 关联
from_assistant_id: int
from_assistant_name: str = "" # FDW 关联
to_assistant_id: int
to_assistant_name: str = "" # FDW 关联
transfer_reason: str | None = None
transfer_score: float | None = None
guard_checks: dict | None = None
created_at: datetime
class TransferLogPage(BaseModel):
items: list[TransferLogItem]
total: int
# ── 待审核任务 ──
class PendingReviewItem(BaseModel):
id: int
site_id: int
site_name: str = ""
member_id: int
member_name: str = ""
assistant_id: int
assistant_name: str = ""
task_type: str
task_type_label: str = "" # 中文映射
transfer_count: int = 0
priority_score: float | None = None
created_at: datetime
class PendingReviewPage(BaseModel):
items: list[PendingReviewItem]
total: int
class CandidateAssistant(BaseModel):
assistant_id: int
assistant_name: str = ""
rs_display: float = 0
ms_display: float = 0
ml_display: float = 0
transfer_score: float = 0 # w_rs*rs + w_ms*ms + w_ml*ml
source: str = "pool" # "pool" | "manual_override"(强制指定标记)
class CandidateListResponse(BaseModel):
candidates: list[CandidateAssistant]
class ReassignRequest(BaseModel):
to_assistant_id: int
class ReassignResponse(BaseModel):
success: bool
new_task_id: int | None = None
class CloseRequest(BaseModel):
reason: str = Field(..., min_length=1, max_length=500)
class CloseResponse(BaseModel):
success: bool
# ── 参数管理 ──
class ConfigParam(BaseModel):
id: int
site_id: int | None = None
site_name: str | None = None # site_id 非空时关联
param_key: str
param_value: float
description: str | None = None
updated_at: datetime
class ConfigParamList(BaseModel):
params: list[ConfigParam]
class ConfigParamUpdate(BaseModel):
param_value: float
class ConfigParamCreate(BaseModel):
site_id: int
param_key: str = Field(..., max_length=64)
param_value: float
class ConfigParamResponse(BaseModel):
success: bool
id: int | None = None
```
---
## 5. 权限控制方案
> **决策O5**:采用双层权限模型 — 超级管理员(`super_admin`)全局访问 + 门店管理员(`site_admin`)本店只读。
### 5.1 权限矩阵
| 页面/操作 | super_admin | site_admin |
|-----------|-------------|------------|
| 转移日志 — 查看全部 | ✅ | ❌ |
| 转移日志 — 查看本店 | ✅ | ✅ |
| 待审核任务 — 查看全部 | ✅ | ❌ |
| 待审核任务 — 查看本店 | ✅ | ✅ |
| 待审核任务 — 重新分配 | ✅ | ❌ |
| 待审核任务 — 关闭任务 | ✅ | ❌ |
| 参数管理 — 查看 | ✅ | ✅(只读) |
| 参数管理 — 编辑/新增/删除 | ✅ | ❌ |
### 5.2 后端实现
> **与 P10 账号体系的关系**P10 租户管理后台(`/tenant-admins`)的门店管理员账号与本页面的 `site_admin` 角色使用同一套 JWT 体系,通过 `roles` 字段区分权限范围。`site_admin` 在 P18 页面为只读访问,在 P10 页面可能有写操作权限。两者面向的用户群不同P18 面向平台运营P10 面向门店管理员),但共享认证基础设施。
```python
# apps/backend/app/auth/dependencies.py 中已有 CurrentUser(roles=[...])
# 新增权限检查辅助函数:
from fastapi import HTTPException, status
def require_super_admin(user: CurrentUser) -> None:
"""写操作权限检查:仅超级管理员可执行。"""
if "super_admin" not in user.roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="仅超级管理员可执行此操作",
)
def filter_by_site(user: CurrentUser, query_site_id: int | None) -> int | None:
"""读操作门店过滤:门店管理员强制锁定本店。"""
if "super_admin" in user.roles:
return query_site_id # 超级管理员可查看任意门店
return user.site_id # 门店管理员强制本店
```
### 5.3 前端实现
```typescript
// 在页面组件中检查权限,控制操作按钮显示
const isSuperAdmin = user.roles.includes('super_admin');
// 转移日志:门店筛选器
// super_admin → 显示全部门店下拉
// site_admin → 隐藏门店筛选器API 自动按 site_id 过滤
// 待审核任务:操作列
// super_admin → 显示"重新分配"和"关闭"按钮
// site_admin → 不显示操作列
// 参数管理:编辑按钮
// super_admin → 显示编辑/新增/删除按钮
// site_admin → 隐藏所有写操作按钮
```
---
## 6. 前端实现方案
### 6.1 导航结构
`apps/admin-web/src/App.tsx``NAV_ITEMS` 数组中新增"任务引擎"菜单组:
```typescript
// 新增 import
import { ApartmentOutlined } from "@ant-design/icons";
import TransferLog from "./pages/TransferLog";
import PendingReview from "./pages/PendingReview";
import TaskEngineConfig from "./pages/TaskEngineConfig";
// NAV_ITEMS 中新增(插入在"定时任务"之后)
{
key: "task-engine-group",
icon: <ApartmentOutlined />,
label: "任务引擎",
children: [
{ key: "/task-engine/transfer-log", label: "转移日志" },
{ key: "/task-engine/pending-review", label: "待审核任务" },
{ key: "/task-engine/config", label: "参数管理" },
],
},
```
**最终侧边栏结构**
```
📋 任务配置 /
📋 任务管理 /task-manager
📊 ETL 状态 /etl-status
⏰ 定时任务 /trigger-jobs
🔀 任务引擎(新增)
├── 转移日志 /task-engine/transfer-log
├── 待审核任务 /task-engine/pending-review
└── 参数管理 /task-engine/config
💾 数据库 /db-viewer
📄 日志 /log-viewer
⚙️ 环境配置 /env-config
🖥️ 运维面板 /ops-panel
👥 租户管理员 /tenant-admins
🤖 AI 监控
├── 运行总览 /ai/dashboard
├── 调度状态 /ai/trigger-jobs
├── 调用明细 /ai/run-logs
└── 手动操作 /ai/operations
🐛 开发调试日志 /dev-trace
```
### 6.2 路由注册
`AppLayout``<Routes>` 中新增:
```tsx
<Route path="/task-engine/transfer-log" element={<TransferLog />} />
<Route path="/task-engine/pending-review" element={<PendingReview />} />
<Route path="/task-engine/config" element={<TaskEngineConfig />} />
```
同时修改 `<Menu>``defaultOpenKeys`,补充 `task-engine-group` 前缀:
```tsx
defaultOpenKeys={[
...(location.pathname.startsWith("/ai/") ? ["ai-group"] : []),
...(location.pathname.startsWith("/task-engine/") ? ["task-engine-group"] : []),
]}
```
### 6.3 API 模块
新建 `apps/admin-web/src/api/taskEngine.ts`
```typescript
import { apiClient } from "./client";
// ── 转移日志 ──
export interface TransferLogItem { /* 同 Pydantic schema */ }
export interface TransferLogPage { items: TransferLogItem[]; total: number; }
export async function fetchTransferLog(params: {
site_id?: number; from_date?: string; to_date?: string;
assistant_id?: number; page?: number; page_size?: number;
}): Promise<TransferLogPage> {
const { data } = await apiClient.get("/admin/task-engine/transfer-log", { params });
return data;
}
export async function fetchMemberTransferHistory(memberId: number): Promise<TransferLogItem[]> {
const { data } = await apiClient.get(`/admin/task-engine/transfer-log/${memberId}/history`);
return data.items;
}
// ── 待审核任务 ──
export interface PendingReviewItem { /* 同 Pydantic schema */ }
export async function fetchPendingReview(params: {
site_id?: number; page?: number; page_size?: number;
}): Promise<{ items: PendingReviewItem[]; total: number }> {
const { data } = await apiClient.get("/admin/task-engine/pending-review", { params });
return data;
}
export async function fetchCandidates(taskId: number) {
const { data } = await apiClient.get(`/admin/task-engine/pending-review/${taskId}/candidates`);
return data.candidates;
}
export async function reassignTask(taskId: number, toAssistantId: number) {
const { data } = await apiClient.post(`/admin/task-engine/pending-review/${taskId}/reassign`, {
to_assistant_id: toAssistantId,
});
return data;
}
export async function closeTask(taskId: number, reason: string) {
const { data } = await apiClient.post(`/admin/task-engine/pending-review/${taskId}/close`, {
reason,
});
return data;
}
// ── 参数管理 ──
export interface ConfigParam { /* 同 Pydantic schema */ }
export async function fetchConfig(siteId?: number): Promise<ConfigParam[]> {
const { data } = await apiClient.get("/admin/task-engine/config", {
params: siteId ? { site_id: siteId } : {},
});
return data.params;
}
export async function updateConfig(paramId: number, value: number) {
const { data } = await apiClient.put(`/admin/task-engine/config/${paramId}`, {
param_value: value,
});
return data;
}
export async function createConfig(siteId: number, paramKey: string, value: number) {
const { data } = await apiClient.post("/admin/task-engine/config", {
site_id: siteId, param_key: paramKey, param_value: value,
});
return data;
}
export async function deleteConfig(paramId: number) {
const { data } = await apiClient.delete(`/admin/task-engine/config/${paramId}`);
return data;
}
```
### 6.4 页面组件结构
每个页面遵循现有 `TriggerJobs.tsx` 的模式:`useState` + `useCallback` + `useEffect` + Ant Design Table。
| 文件 | 说明 |
|------|------|
| `apps/admin-web/src/pages/TransferLog.tsx` | 转移日志页面 |
| `apps/admin-web/src/pages/PendingReview.tsx` | 待审核任务页面 |
| `apps/admin-web/src/pages/TaskEngineConfig.tsx` | 参数管理页面 |
| `apps/admin-web/src/api/taskEngine.ts` | API 调用模块 |
| `apps/backend/app/routers/admin_task_engine.py` | 后端路由 |
| `apps/backend/app/schemas/admin_task_engine.py` | Pydantic schema |
---
## 7. 数据库变更需求
### 7.1 trigger_jobs 表扩展P1 优先级)
> `last_error` 和 `description` 字段已存在于数据库中(已通过 `\d biz.trigger_jobs` 确认),无需重复添加。
```sql
-- 仅 last_stats 是真正新增的
ALTER TABLE biz.trigger_jobs
ADD COLUMN IF NOT EXISTS last_stats JSONB;
COMMENT ON COLUMN biz.trigger_jobs.last_stats
IS '最近一次执行的统计结果 JSON如 {"created":5,"replaced":2,"skipped":10,"transferred":1}';
```
`last_stats``task_generator.run()` 在执行完成后写入,是运营监控任务引擎健康度的最直接手段。
### 7.2 cfg_task_generator_params 表扩展
```sql
-- 记录修改人(审计追溯)
ALTER TABLE biz.cfg_task_generator_params
ADD COLUMN IF NOT EXISTS updated_by BIGINT;
COMMENT ON COLUMN biz.cfg_task_generator_params.updated_by IS '最近修改人 user_id用于审计追溯';
```
### 7.3 无需新建表
P18 不新增业务表。所有数据来源于 P17 已创建的:
- `biz.coach_task_transfer_log`(转移日志)
- `biz.cfg_task_generator_params`(参数配置)
- `biz.coach_tasks`(任务表,含 `pending_review` 状态)
---
## 8. 优先级排序
| 优先级 | 功能 | 理由 | 预估工时 |
|--------|------|------|----------|
| P0 | 待审核任务页面 | `pending_review` 任务需要人工介入,无此页面则转移超限的任务无法处理 | 后端 4h + 前端 4h |
| P1 | 参数管理页面 | 门店级参数调整是日常运营需求,否则每次改参数都要直接改数据库 | 后端 3h + 前端 4h |
| P1 | 转移日志页面 | 运营需要追踪转移效果,但短期可通过数据库查询替代 | 后端 3h + 前端 3h |
| P1 | trigger-jobs last_stats 展示 | 任务引擎上线后运营最直接的监控手段,能看到 created/replaced/skipped/transferred 数字 | 后端 1h + 前端 1h |
| P2 | trigger-jobs 启用/禁用开关 | 低频操作,可通过数据库修改 | 后端 0.5h + 前端 0.5h |
**建议实施顺序**
1. 后端:先建 router + schema 骨架(`admin_task_engine.py`),再逐个实现端点
2. 前端:先注册路由和导航,再逐个实现页面组件
3. P0 → P1参数管理→ P1转移日志→ P2 → P3
---
## 9. 开放问题决策汇总
| # | 问题 | 决策 | 理由 |
|---|------|------|------|
| O1 | 重新分配候选助教列表获取方式 | 复用 P17 转移候选逻辑(仅 POOL 助教POOL 为空时返回空列表 + 提示,紧急情况提供"强制指定"(需二次确认 + manual_override 标记) | 严格遵守 P17 的 UNASSIGNED 永不分配约束 |
| O2 | 参数修改是否需要审批流程 | 直接生效,不需要审批 | 修改频率极低,`updated_at` + `updated_by` 可追溯 |
| O3 | 转移日志是否关联 WBI/NCI/RS 快照 | 不关联,通过 `guard_checks` JSONB 间接推断 | 避免表膨胀,历史指数可从 DWS 层回溯 |
| O4 | 是否需要运行总览 Dashboard | 暂不需要,后续根据运营反馈评估 | 当前数据量不足以支撑有意义的趋势图 |
| O5 | 权限模型 | 双层super_admin 全局 + site_admin 本店只读 | 与现有 JWT roles 体系一致,实现成本最低 |
| O6 | trigger-jobs 执行历史是否需要新表 | 暂不新增,用 `last_stats` JSONB 字段过渡 | 短期够用,中期再评估完整历史表 |
> **决策O4**:暂不建设运行总览 Dashboard。原因(1) P17 刚上线,历史数据不足以生成有意义的趋势图;(2) 三个功能页面已覆盖核心运营需求;(3) 后续积累 1-2 个月数据后,可作为 P19 或 P20 独立评估。
---
## 10. 验收标准
| # | 验收项 | 判定方式 |
|---|--------|----------|
| AC1 | 转移日志页面可按门店/时间/助教筛选 | 手动验证筛选条件组合 |
| AC2 | 门店管理员只能看到本店转移日志 | 用 site_admin 角色登录验证 |
| AC3 | 待审核任务页面展示所有 pending_review 任务 | 数据库插入测试数据后验证 |
| AC4 | 重新分配操作正确创建新任务并标记原任务 | 执行后检查 coach_tasks + transfer_log |
| AC5 | 关闭任务操作正确更新状态和原因 | 执行后检查 coach_tasks.status + abandon_reason |
| AC6 | 参数管理页面展示全局默认 + 门店覆盖 | 插入门店覆盖数据后验证 |
| AC7 | 权重参数之和校验生效 | 尝试设置不等于 1.0 的权重组合 |
| AC8 | 超级管理员可执行所有写操作 | 用 super_admin 角色验证 |
| AC9 | 门店管理员无法执行写操作 | 用 site_admin 角色验证 403 |
| AC10 | 不允许删除全局默认参数 | 尝试删除 site_id IS NULL 的记录 |
---
## 11. 与现有 PRD/模块的关系
| 文档/模块 | 关系说明 |
|-----------|----------|
| P17 | 本 PRD 是 P17 的管理后台配套P17 提供数据P18 提供可视化和操作入口 |
| P10租户管理后台 | P10 是面向门店管理员的独立应用P18 是面向平台运营的 admin-web 扩展。两者共享 JWT 认证体系(同一套 `roles` 字段),`site_admin` 在 P18 为只读、在 P10 可能有写权限。详见第 5.2 节说明 |
| trigger-jobs 现有页面 | 本 PRD 评估其扩展需求,但不强制改动,保持现有功能稳定 |
| AI 监控模块 | 参考其 API 设计模式JWT + admin 角色、分页列表 + 详情)|
---
## 附录 A后端路由注册
`apps/backend/app/main.py` 中注册新路由:
```python
from app.routers import admin_task_engine
app.include_router(admin_task_engine.router)
```
## 附录 B变更历史
| 版本 | 日期 | 变更内容 |
|------|------|----------|
| v1.0-draft | 2026-03-24 | 初始草稿,定义 3 个页面 + 9 个 API + 6 个开放问题 |
| v2.0-ready-for-review | 2026-03-24 | 关闭全部 6 个开放问题;补充 Pydantic schema、权限矩阵、前端实现方案、SQL 示例、校验规则、验收标准;升级为待评审 |
| v2.1-reviewed | 2026-03-24 | 评审修正:(1) DDL 移除已存在的 last_error/description仅新增 last_stats(2) defaultOpenKeys 补充 task-engine-group(3) last_stats 优先级 P2→P1(4) 候选助教降级方案改为空结果+提示+强制指定,不放开到全店助教;(5) 权重参数改为卡片整体编辑+联合校验;(6) 明确姓名关联走 Python 层批量合并;(7) 补充 site_admin 与 P10 账号体系关系说明 |

View File

@@ -0,0 +1,125 @@
# P19历史指数回测 + 任务引擎模拟
> 版本v1.0 | 日期2026-03-29 | 来源:本轮对话需求讨论
---
## 1. 背景
任务引擎P17 + OS 分级分配)已实现,但缺乏历史验证手段。需要:
- 回测过去一个月的指数变化,验证指数算法的稳定性
- 模拟任务引擎运行一个月,验证分级分配、升级、转移逻辑的合理性
- 最终数据落库(`test_zqyy_app`),可在小程序和管理后台中查看
## 2. 需求拆分
### 2.1 指数历史回测Phase 1
**目标**:给 4 个指数任务加 `as_of_date` 参数,支持"假装今天是 X 日"重算指数。
**涉及任务**
| 任务 | 输入数据源 | 时间依赖点 |
|------|-----------|-----------|
| `DWS_WINBACK_INDEX` (WBI) | `dws_member_visit_detail` + `dws_member_consumption_summary` | 距上次到店天数、到店频率衰减 |
| `DWS_NEWCONV_INDEX` (NCI) | `dws_member_visit_detail` + `dws_member_consumption_summary` | 新客首次到店后的天数 |
| `DWS_RELATION_INDEX` (RS/OS/MS/ML) | `dwd_assistant_service_log` | 服务记录的时间衰减halflife |
| `DWS_SPENDING_POWER_INDEX` (SPI) | `dws_member_consumption_summary` | 消费金额时间窗口 |
**改动要点**
- 每个指数任务的 `_do_extract()` 中,将 `NOW()` / `CURRENT_DATE` 替换为 `as_of_date` 参数
- 衰减计算中的"距今天数"改为"距 as_of_date 天数"
- 输出表新增 `calc_date` 字段(或复用 `calc_time`),标记是哪天的快照
**回测参数**
- 时间范围:过去 30 天2026-02-27 ~ 2026-03-29
- 回测间隔:每 6 小时一个快照(共 120 个快照点)
- 数据落库:每个快照覆盖写入 ETL 测试库的指数表delete-before-insert by calc_date
**CLI 接口设计**
```bash
# 单次回测(指定日期)
python -m cli.main --tasks DWS_WINBACK_INDEX --as-of-date "2026-03-01"
# 批量回测(日期范围 + 间隔)
python scripts/ops/backtest_indexes.py \
--start "2026-02-27" --end "2026-03-29" \
--interval-hours 6 \
--store-id 2790685415443269
```
### 2.2 任务引擎模拟Phase 2
**目标**:基于回测的指数快照,模拟任务引擎运行一个月,数据落入业务测试库。
**模拟参数**
- 时间范围2026-02-27 ~ 2026-03-29
- 模拟粒度每小时一次720 次循环)
- 指数数据:使用 Phase 1 回测的快照(每 6 小时更新一次,中间小时复用最近快照)
- 回店判定:使用 DWD 真实服务记录(`dwd_assistant_service_log.create_time`
**模拟流程(每小时)**
```
1. 设置模拟时钟 sim_time
2. 如果 sim_time 是 6 小时整点 → 切换到对应的指数快照
3. 检查 DWD 服务记录中 create_time 在 [sim_time-1h, sim_time] 的记录
→ 匹配 active 召回任务 → 标记 completedcompleted_at = sim_time
4. 检查过期任务expires_at < sim_time→ 标记 abandoned
5. 执行任务生成逻辑OS 分级分配):
a. MAIN 助教:生成召回/关系构建任务
b. COMANAGE仅生成关系构建检查升级条件升级倍数 ≥ 3
c. 转移检查(升级倍数 ≥ 5
6. 已完成的召回任务 → 生成 follow_up_visit48h 保留期)
7. 记录当小时的任务快照
```
**数据落库**
- 所有任务写入 `biz.coach_tasks``created_at` 用模拟时钟值)
- 历史记录写入 `biz.coach_task_history`
- 转移日志写入 `biz.coach_task_transfer_log`
**期望输出**
1. 每天的任务数量变化(按类型分组)
2. 一个月后各类型任务的最终分布
3. COMANAGE 升级触发次数和时间点
4. POOL 转移触发次数和时间点
5. 回访任务的生成数量和完成率
6. 每个助教的任务负载分布
### 2.3 输出报告
**脚本输出**
- 控制台:每天一行摘要(日期 | 新增 | 完成 | 升级 | 转移 | 总 active
- CSV 文件:`export/backtest/task_simulation_daily.csv`(每天快照)
- JSON 文件:`export/backtest/task_simulation_summary.json`(最终统计)
## 3. 技术约束
- 指数回测和任务模拟都在测试库执行(`test_etl_feiqiu` / `test_zqyy_app`
- 模拟脚本放 `scripts/ops/`,遵循现有脚本规范
- 环境变量从根 `.env` 加载(`load_dotenv`
- 指数回测需要 ETL 库连接(`PG_DSN`),任务模拟需要业务库连接(`APP_DB_DSN`
- 模拟前清空 `coach_tasks` / `coach_task_history` / `coach_task_transfer_log`
## 4. 实施顺序
1. Phase 1a`RelationIndexTask`RS/OS/MS/ML`as_of_date` 支持
2. Phase 1b`WinbackIndexTask`WBI`as_of_date` 支持
3. Phase 1c`NewconvIndexTask`NCI`as_of_date` 支持
4. Phase 1d`SpendingPowerIndexTask`SPI`as_of_date` 支持
5. Phase 1e编写批量回测脚本 `backtest_indexes.py`
6. Phase 2编写任务模拟脚本 `simulate_task_engine.py`
## 5. 依赖
- P17助教客户归属与任务生成引擎已完成
- OS 分级分配改动(本轮已完成)
- DWS_TASK_ENGINE ETL 任务(本轮已完成)
- DWD 层服务记录数据(已有)
## 6. 关键文件参考
- 指数任务:`apps/etl/connectors/feiqiu/tasks/dws/index/`
- 任务生成器:`apps/backend/app/services/task_generator.py`
- 召回检测器:`apps/backend/app/services/recall_detector.py`
- FDW 查询:`apps/backend/app/services/fdw_queries.py`
- 参数配置:`biz.cfg_task_generator_params`16 条参数)

View File

@@ -41,7 +41,7 @@
| 0 | 高优先召回 | max(WBI,NCI) > 7 | 助教为该客户服务ETL 检测) |
| 0 | 优先召回 | max(WBI,NCI) > 5 | 同上 |
| 1 | 客户回访 | 完成召回后未备注 | 助教为该客户提交备注AI 评分仅绩效用途) |
| 2 | 关系构建 | RS < 6 | 无自动完成条件(手动标记或指数变化) |
| 2 | 关系构建 | 1 < RS < 6 | 无自动完成条件(手动标记或指数变化) |
### 任务类型与任务状态的关系

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