This commit is contained in:
Neo
2026-03-15 10:15:02 +08:00
parent 2dd217522c
commit 72bb11b34f
916 changed files with 65306 additions and 16102803 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -12,14 +12,14 @@
1. 作为助教,我每天打开小程序能看到系统为我分配的任务列表,按优先级排序。
2. 作为助教,我可以置顶/放弃任务,放弃时必须填写原因。
3. 作为助教,我完成召回任务后(客户到店被服务),系统自动标记任务完成。
4. 作为助教,我给客户添加备注后,系统自动通过 AI 分析备注内容(应用 6提取维客线索并评分。回访备注评分 ≥6 分算回访完成
4. 作为助教,我给客户添加备注后,系统自动通过 AI 分析备注内容(应用 6提取维客线索并评分。助教为客户提交备注即算回访完成AI 评分仅用于后续绩效评估,不参与完成判定)
5. 作为助教,我在添加备注时可以对客户进行星星评分(再次服务意愿、再来店可能性,各 1-5 星),回访任务默认展开评分区域,其他任务类型通过"展开评价"按钮手动打开。
6. 作为系统,回访任务至少保留 48 小时,到期后自动失效。
7. 作为系统,当 ETL 数据延迟导致召回完成晚于备注提交时,需要回溯重分类备注。
### 验收标准
- AC1任务生成器每日 4:00 后运行,正确按 max(WBI,NCI) 分配 4 种任务类型
- AC1任务生成器每日 7:00 后运行,正确按 max(WBI,NCI) 分配 4 种任务类型
- AC2同客户-助教-类型的任务跳过;不同类型则关闭旧任务+新建
- AC3回访任务 48 小时滞留机制正常(生成时间算起)
- AC4任务有效/无效状态 + 有效期字段正确流转
@@ -40,19 +40,48 @@
|--------|------|---------|---------|
| 0 | 高优先召回 | max(WBI,NCI) > 7 | 助教为该客户服务ETL 检测) |
| 0 | 优先召回 | max(WBI,NCI) > 5 | 同上 |
| 1 | 客户回访 | 完成召回后未备注 | 提交备注且应用 6 备注分析评分 ≥ 6 |
| 1 | 客户回访 | 完成召回后未备注 | 助教为该客户提交备注AI 评分仅绩效用途) |
| 2 | 关系构建 | RS < 6 | 无自动完成条件(手动标记或指数变化) |
### 任务类型与任务状态的关系
**核心原则**任务类型task_type和任务状态status是两套独立的维度交叉描述一个任务互不干扰。
- **任务类型**task_type描述任务的业务性质由系统根据客户指数自动分配不因用户操作而改变
- `high_priority_recall`(高优先召回)
- `priority_recall`(优先召回)
- `follow_up_visit`(客户回访)
- `relationship_building`(关系构建)
- **任务状态**status描述任务的生命周期阶段可由用户操作或系统自动流转
- `active`(有效):任务当前有效,显示在列表中
- `inactive`(无效):任务已过期或被系统标记为无效,不显示在列表中
- `completed`(已完成):任务已完成
- `abandoned`(已放弃):用户主动放弃任务,记录放弃原因
- **置顶状态**is_pinned独立于任务状态用户可对任何有效任务置顶已放弃任务不可置顶
- **前端展示规则**
- 置顶区域:显示 `is_pinned=true``status=active` 的任务
- 一般任务区域:显示 `is_pinned=false``status=active` 的任务
- 已放弃区域:显示 `status=abandoned` 的任务(任务类型保留,但灰化显示)
- 排序规则:`is_pinned DESC → priority_score DESC NULLS LAST → created_at ASC`,已放弃任务排在所有有效任务之后
- **长按菜单规则**
- 置顶/一般任务:显示"置顶/取消置顶"、"备注"、"问问AI"、"放弃任务"
- 已放弃任务:显示"取消放弃"(直接执行,无需二次确认,将任务恢复至 `status=active``is_pinned=false`
### 任务状态机
```
[生成] → 有效(无有效期)
├── 类型变更 → 旧任务无效(无有效期) + 新任务有效
├── 指数不再满足 → 有效(填充有效期=生成时间+48h)
│ └── 轮询检查 → 超过有效期 → 无效
├── 新回访任务顶替 → 旧任务无效 + 新任务有效
├── 助教放弃 → 无效(记录放弃原因)
── 完成 → 已完成(记录完成时间和完成时状态)
[生成] → 有效(status=active, 无有效期)
├── 类型变更 → 旧任务无效(status=inactive, 无有效期) + 新任务有效
├── 指数不再满足 → 有效(status=active, 填充有效期=生成时间+48h)
│ └── 轮询检查 → 超过有效期 → 无效(status=inactive)
├── 新回访任务顶替 → 旧任务无效(status=inactive) + 新任务有效
├── 助教放弃 → 无效(status=abandoned, 记录放弃原因)
── 助教取消放弃 → 有效(status=active, is_pinned=false, 清除放弃原因)
└── 完成 → 已完成(status=completed, 记录完成时间和完成时状态)
```
### coach_tasks 表核心字段
@@ -80,15 +109,20 @@ biz.notes
- content (TEXT)
- rating_service_willingness (SMALLINT 1-5, 可空,再次服务此客户意愿)
- rating_revisit_likelihood (SMALLINT 1-5, 可空,再来店可能性)
- task_id (关联任务 ID, 可空)
- task_id (关联任务 ID, 可空,备注可独立于任务创建)
- created_at, updated_at
```
> 备注与任务的关系:
> - 备注不要求通过 `task_id` 显式关联回访任务才能触发完成
> - 助教为某客户提交备注时,系统检查该助教×客户是否有有效的回访任务,有则标记完成
> - `task_id` 为可选字段,备注可独立于任务创建(如直接在客户详情页写备注)
> 星星评分说明:
> - 回访任务follow_up_visit备注弹窗默认展开评分区域
> - 其他任务类型:评分区域默认隐藏,通过"展开评价"按钮手动打开
> - 数据迟到场景:助教在召回任务中完成服务后顺手写备注,此时 ETL 数据未到,任务仍为召回类型,评分区域隐藏但可手动展开;当 ETL 数据到达、召回完成检测触发后,回溯机制将该备注重分类为回访备注,星星评分数据一并保留
> - 星星评分不参与回访完成判定(完成判定仅看应用 6 评分 ≥6),不参与应用 6 分析,仅作辅助数据存储,后期功能扩展使用
> - 星星评分不参与回访完成判定(回访完成判定为:助教为该客户提交备注即完成),不参与应用 6 分析,仅作辅助数据存储,后期功能扩展使用
### 触发器机制
@@ -103,7 +137,7 @@ biz.trigger_jobs
```
预置触发器:
1. `task_generator` — cron: 每日 04:00
1. `task_generator` — cron: 每日 07:00
2. `task_expiry_check` — interval: 每小时
3. `recall_completion_check` — event: ETL 数据更新后
4. `note_reclassify_backfill` — event: 召回完成时

View File

@@ -0,0 +1,145 @@
# P5.2P4 前置依赖修复 — p4-prerequisite-fixes
> 优先级P5.2P6 前置,必须在 P6 开发前完成)
> 预估工作量6 个定点修复,无新表/新接口)
> 依赖P4 已完成
> 来源P4 Spec vs 实现差异分析(`docs/reports/P4-spec-vs-implementation-gap-analysis.md`
---
## 背景
P4 核心业务层已实现并通过属性测试,但对比最新 Spec 发现 6 处实现偏差。
这些偏差会影响 P6前端任务模块的正常开发必须前置修复。
参考文档:
- 差异分析:`docs/reports/P4-spec-vs-implementation-gap-analysis.md`
- 生命周期全景:`docs/reports/P4-task-lifecycle-panorama.md`
---
## 需求Requirements
### 验收标准
- AC1任务列表 API 返回 active + abandoned 两种状态abandoned 排在最后且包含 abandon_reason
- AC2召回完成检测器仅完成 high_priority_recall / priority_recall 两种任务类型
- AC3备注回溯重分类器创建回访任务时已有 active 回访 → 顶替(旧→无效),已完成 → 跳过
- AC4回访任务完成条件为「助教为该客户提交备注」不依赖 AI 评分
- AC5trigger_scheduler 的 last_run_at 更新具备事务安全性
- AC6任务生成器 cron 时间为 07:00
---
## 任务清单
### T1任务列表返回已放弃任务GAP-3
文件:`apps/backend/app/services/task_manager.py``get_task_list()`
修改内容:
1. SQL WHERE 条件:`status = 'active'``status IN ('active', 'abandoned')`
2. SQL ORDER BY 增加:`CASE WHEN status = 'abandoned' THEN 1 ELSE 0 END` 排在 `is_pinned DESC` 之前
3. SELECT 增加 `abandon_reason` 字段
4. 返回结构增加 `abandon_reason` 字段
验证:
- 有 abandoned 任务时,列表末尾出现灰化任务且包含 abandon_reason
- 有 active + pinned 任务时,排序为:置顶 → 一般 → 已放弃
---
### T2召回完成检测器过滤任务类型GAP-6
文件:`apps/backend/app/services/recall_detector.py``_process_service_record()`
修改内容:
1. SQL 增加 `AND task_type IN ('high_priority_recall', 'priority_recall')`
验证:
- follow_up_visit 和 relationship_building 类型的 active 任务不会被召回检测器完成
- high_priority_recall 和 priority_recall 正常完成
---
### T3备注回溯重分类器冲突处理GAP-7
文件:`apps/backend/app/services/note_reclassifier.py``run()`
修改内容:
1. 创建回访任务前,先查询是否已有同 (site_id, assistant_id, member_id) 的 follow_up_visit 任务
2. 已有 completed 的回访任务 → 跳过创建(回访完成 +1 的语义已满足)
3. 已有 active 的回访任务 → 旧任务标记 inactive创建新任务
4. 不存在 → 正常创建
验证:
- 重复触发不会产生唯一约束冲突
- 已完成的回访任务不会被覆盖
---
### T4回访完成条件改为「有备注即完成」新-1
文件:
- `apps/backend/app/services/note_service.py``create_note()`
- `apps/backend/app/services/note_reclassifier.py``run()`
修改内容:
1. `note_service.create_note()` 增加逻辑:创建备注后,通过 user_assistant_binding 查 assistant_id
再查该 (site_id, assistant_id, member_id) 是否有 active 的 follow_up_visit 任务,有则标记 completed
2. `note_reclassifier.py`:去掉 AI 评分判定逻辑,有备注 → 回访任务直接标记 completed
无备注 → 创建 active 回访任务(等待备注)
验证:
- 助教提交备注后,对应客户的 active 回访任务自动完成
- 不依赖 AI 评分返回值
---
### T5trigger_scheduler last_run_at 事务安全GAP-9
文件:`apps/backend/app/services/trigger_scheduler.py`
修改内容:
1. `fire_event()``check_scheduled_jobs()` 中,将 last_run_at 更新纳入 handler 的事务范围,
或改为执行前更新at-most-once 语义,配合 handler 幂等性)
验证:
- handler 成功但 commit 失败的场景不会导致重复处理(或重复处理是幂等的)
---
### T6任务生成器 cron 改为 07:00新-2
文件:
- `db/zqyy_app/migrations/2026-03-15__p52_update_cron_0700.sql`(新建迁移脚本)
- `apps/backend/app/services/trigger_scheduler.py``_calculate_next_run()` 默认值
修改内容:
1. 新建迁移脚本:`UPDATE biz.trigger_jobs SET trigger_config = '{"cron_expression": "0 7 * * *"}' WHERE job_name = 'task_generator'`
2. `_calculate_next_run()` 中 cron 默认值从 `"0 4 * * *"` 改为 `"0 7 * * *"`
验证:
- trigger_jobs 表中 task_generator 的 cron_expression 为 `0 7 * * *`
- 下次运行时间计算正确
---
## 执行顺序
```
批次 1无依赖可并行T1、T2、T6
批次 2依赖 T2 确认T3、T4
批次 3依赖 T3/T4 验证T5
```
---
## 不在本 SPEC 范围
| 项目 | 归属 | 说明 |
|------|------|------|
| GAP-5 备注分页 | P6-T6 | 已写入 P6 Spec |
| GAP-8 问问 AI | P5 | P5 AI 集成层已覆盖 |
| GAP-2 cancel_abandon 重置 is_pinned | 已修复 | 2026-03-14 |
| GAP-4 completed_task_type 快照 | 无差异 | 实现正确 |

View File

@@ -27,7 +27,90 @@
- AC7跳档激励"到达XXX即得YYY"计算正确
- AC8任务卡片展示应用 4 的一句话总结ai_cache 缓存读取)
- AC9备注卡片以打星方式展示应用 6 评分1-10 分,映射公式:`星数 = score ÷ 2`5 颗星,支持半星;如 score=7 → 3.5 星)
- AC10维客线索提供者按 source 字段显示"By:系统"或"By:备注",不显示具体人名
- AC10维客线索提供者按规则显示(见下方"维客线索提供者显示规则"章节)
---
## 维客线索提供者显示规则
### 后端过滤策略(安全性优先)
⚠️ **重要**:维客线索的过滤必须在后端完成,客户端不进行任何过滤逻辑,以防止数据泄露风险。
**后端返回规则**
当客户端请求任务详情时,后端应返回已过滤的维客线索列表,仅包含以下类型:
| 线索类型 | 返回条件 | 说明 |
|---------|--------|------|
| 系统生成 | 总是返回 | `source = 'ai_consumption'`(应用 3 消费分析) |
| 我的记录 | 总是返回 | `source = 'manual'``recorded_by_assistant_id = 当前登录用户ID` |
| 他人记录 | 返回脱敏版本 | `source = 'manual'``recorded_by_assistant_id ≠ 当前登录用户ID`,返回时需脱敏 |
| 备注分析 | 返回脱敏版本 | `source = 'ai_note'`,返回时需脱敏 |
**脱敏规则**
对于"他人记录"和"备注分析"类型的线索,后端返回时应:
- 移除或不返回 `recorded_by_assistant_id` 字段
- 移除或不返回 `recorded_by_name` 字段
- 保留 `source` 字段用于前端判断显示"By:某人"
### 前端显示规则
前端接收后端返回的维客线索后,直接根据 `source` 字段显示,无需额外过滤:
| source 值 | 显示文案 |
|----------|--------|
| `ai_consumption` | By:系统 |
| `manual` 且有 `recordedByAssistantId` | By:我 |
| `manual``ai_note` 且无 `recordedByAssistantId` | By:某人 |
**注意**
- "某人"是字符串字面量,不是真实人名
- 不显示具体的助教姓名或ID
- 与客户详情页不同(客户详情页显示具体人名)
### 多人情况处理
当同一客户有多条维客线索时,前端按以下规则展示:
1. **按时间排序**:按 `created_at` 升序排列(最早的在前)
2. **取最先反馈**
- 如果有"By:系统"的线索,优先显示最早的"By:系统"
- 如果没有"By:系统",则显示最早的"By:我"或"By:某人"
3. **单条展示**:维客线索卡片中仅显示一条(最优先的),其他线索可通过"查看全部"或列表展开查看
### 前端实现示例
```typescript
// 前端:直接根据后端返回的数据显示,无需过滤
function getClueProvider(clue: RetentionClue): string {
if (clue.source === 'ai_consumption') {
return 'By:系统'
}
// 后端已确保:如果是 manual 且有 recordedByAssistantId则是当前用户
if (clue.source === 'manual' && clue.recordedByAssistantId) {
return 'By:我'
}
// 其他情况manual 无 ID、ai_note都显示"By:某人"
return 'By:某人'
}
// 前端:从多条线索中选择最优先的
function selectPrimaryClue(clues: RetentionClue[]): RetentionClue {
// 1. 按 createdAt 升序排序
const sorted = clues.sort((a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)
// 2. 优先返回 source='ai_consumption' 的最早线索
const systemClue = sorted.find(c => c.source === 'ai_consumption')
if (systemClue) return systemClue
// 3. 否则返回最早的线索
return sorted[0]
}
```
---
@@ -43,14 +126,14 @@
- 客户信息卡片
- 近期服务记录(时间+时长)
- AI 区域:
- 维客线索(应用 8 整合线索 + 人工,读取 `member_retention_clue`Emoji 作为二级标签、提供者逗号分隔展示)
- 维客线索(应用 8 整合线索 + 人工,读取 `member_retention_clue`Emoji 作为二级标签、提供者按规则展示)
- 客户分析(应用 7 缓存:运营策略数组 + 总结)
- 关系分析 + 任务建议 + 一句话总结(应用 4 缓存,读取应用 8 最新线索)
- 话术参考(应用 5 缓存)
- 备注分析评分(应用 6 缓存1-10 分)
- 备注入口(提交后触发回访完成判定);备注弹窗含星星评分(再次服务意愿+再来店可能性,各 1-5 星):回访任务默认展开评分区域,其他任务类型默认隐藏通过"展开评价"按钮手动打开
- 备注卡片展示应用 6 的 `score`1-10 分),以打星方式呈现(`星数 = score ÷ 2`5 颗星,支持半星;从 `ai_cache` cache_type=app6_note_analysis 读取)
- 维客线索(应用 8提供者显示规则`source=ai_consumption` 显示"By:系统"`source=ai_note` 显示"By:备注",不显示具体人名
- 维客线索提供者显示规则(见上方"维客线索提供者显示规则"章节)
- "问问助手"按钮 → chat.html
### notes备注管理
@@ -81,5 +164,5 @@
- T4-a细化 P5 应用 4关系分析Prompt JSON 结构,实现 `service_history``assistant_info` 等字段的拼接函数(对应 P5-T7-完整)
- T4-b细化 P5 应用 5话术参考Prompt JSON 结构,实现拼接函数(对应 P5-T8-完整)
- [ ] T5实现备注提交功能集成回访完成判定 + 星星评分:回访任务默认展开,其他任务类型通过"展开评价"按钮手动打开)
- [ ] T6实现 notes 页面(列表 + 删除)
- [ ] T6实现 notes 页面(列表 + 删除 + 分页懒加载
- [ ] T7实现通用组件爱心 icon、喜好标签、跟/弃 icon、预估标记

View File

@@ -66,16 +66,18 @@
### 财务看板数据字段映射已校准数据源DWD-DOC 03-财务全景)
收入结构settle_type=1台桌结账
收入结构settle_type IN (1, 3),台桌结账 + 商城订单
| 收入来源 | DWS 字段 | 占比 | 说明 |
|----------|----------|-----:|------|
| 台费 | `table_charge_money` | 56.6% | 按秒计时,台区单价固定 |
| 助教陪打 | `assistant_pd_money` | 30.6% | 按秒计时,单价按等级 |
| 商品 | `goods_money` | 10.1% | 酒水食品 |
| 助教超休 | `assistant_cx_money` | 0.9% | 按课时190 元/课时 |
| 助教超休 | `assistant_cx_money` | 0.9%(仅 type=1 | 按课时190 元/课时settle_type=3 含 477 笔纯超休订单cx=245,480占全口径超休 85%+ |
| 灯控电费 | `electricity_money` | 0% | 门店未启用,全为 0 |
> ⚠️ settle_type=3商城订单中包含大量纯超休/激励课订单,财务看板取数必须 `settle_type IN (1, 3)`,否则严重低估超休收入。
> ⚠️ 消费金额口径:使用 `items_sum`= tc + goods + pd + cx + electricity不得使用 `consume_money`(三种历史口径混合)。
充值收入settle_type=5`dws_finance_recharge_summary` 读取充值退款settle_type=7需扣除。

View File

@@ -38,7 +38,7 @@
- 商城订单:助教列表(花名+级别+课程类型+服务时长+定档绩效)、支付金额、食品酒水总金额
- 充值:充值金额、支付方式
- 备注列表
- AI 维客线索(应用 8 整合线索 + 人工,读取 `member_retention_clue`Emoji 作为二级标签、提供者逗号分隔展示
- AI 维客线索(应用 8 整合线索 + 人工,读取 `member_retention_clue`Emoji 作为二级标签、提供者显示规则见下方"维客线索提供者显示规则"章节
- AI 客户分析(应用 7 缓存:运营策略数组 + 总结,结账单出现后自动生成;从 `ai_cache` cache_type=app7_customer_analysis 读取)
- "问问助手"入口 → chat.html
@@ -88,7 +88,7 @@
| settle_type | 含义 | 数量占比 | 消费记录样式 |
|:-----------:|------|---------|-------------|
| 1 | 台桌结账 | 78.6% | 下沉到 `dwd_table_fee_log` 台费明细 |
| 3 | 商城订单 | 21.4% | 助教列表 + 支付金额 + 食品酒水 |
| 3 | 商城订单 | 21.4% | 助教列表 + 支付金额 + 食品酒水(含 477 笔纯超休/激励课订单) |
| 5 | 正常充值 | — | 从 `dwd_recharge_order` 单独查询 |
| 7 | 充值退款 | 极少10 笔) | 不在消费记录中展示 |
| 6 | 结算退款 | 极少1 笔) | 不在消费记录中展示 |
@@ -126,6 +126,79 @@ items_sum = table_charge_money + goods_money + assistant_pd_money
---
## 维客线索提供者显示规则
### 后端过滤策略(安全性优先)
⚠️ **重要**:维客线索的过滤必须在后端完成,客户端不进行任何过滤逻辑,以防止数据泄露风险。
**客户详情页后端返回规则**
后端应返回所有相关的维客线索(包括系统生成、当前用户、他人记录、备注分析),但需要根据用户权限进行脱敏:
| 线索类型 | 返回条件 | 说明 |
|---------|--------|------|
| 系统生成 | 总是返回 | `source = 'ai_consumption'`,完整返回 |
| 我的记录 | 总是返回 | `source = 'manual'``recorded_by_assistant_id = 当前用户ID`,完整返回 |
| 他人记录 | 返回完整版本 | `source = 'manual'``recorded_by_assistant_id ≠ 当前用户ID`,返回 `recorded_by_name` |
| 备注分析 | 返回完整版本 | `source = 'ai_note'`,返回原备注的 `recorded_by_name` |
### 前端显示规则
**客户详情页**:前端直接显示后端返回的所有维客线索,显示具体人名:
| source 值 | 显示文案 |
|----------|--------|
| `ai_consumption` | By:系统 |
| `manual` 且有 `recordedByAssistantId` | By:我 |
| `manual``ai_note` 且有 `recordedByName` | By:{recordedByName} |
多条线索时,提供者用逗号分隔展示(如"By:系统, 王芳, 李明"
**任务详情页**:前端根据后端返回的脱敏数据显示(详见 P6 规范)
### 多人情况处理
当同一客户有多条维客线索时,前端按以下规则展示:
1. **按时间排序**:按 `created_at` 升序排列(最早的在前)
2. **取最先反馈**
- 如果有"By:系统"的线索,优先显示最早的"By:系统"
- 如果没有"By:系统",则显示最早的"By:我"或其他人名
3. **单条展示**:维客线索卡片中仅显示一条(最优先的),其他线索可通过"查看全部"或列表展开查看
### 前端实现示例
```typescript
// 客户详情页:直接显示后端返回的数据
function getClueProvider(clue: RetentionClue): string {
if (clue.source === 'ai_consumption') {
return 'By:系统'
}
if (clue.source === 'manual' && clue.recordedByAssistantId) {
return 'By:我'
}
if (clue.recordedByName) {
return `By:${clue.recordedByName}`
}
return 'By:某人' // 备用
}
// 从多条线索中选择最优先的
function selectPrimaryClue(clues: RetentionClue[]): RetentionClue {
const sorted = clues.sort((a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)
const systemClue = sorted.find(c => c.source === 'ai_consumption')
if (systemClue) return systemClue
return sorted[0]
}
```
---
## 小程序前端开发强制规范
> 以下规范适用于本 SPEC 中所有小程序页面实现,具有强制约束力。

File diff suppressed because it is too large Load Diff