1
This commit is contained in:
1
.kiro/specs/p4-prerequisite-fixes/.config.kiro
Normal file
1
.kiro/specs/p4-prerequisite-fixes/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"specId": "a7c3e1f2-9b84-4d6e-b5a1-3f8c2d7e9a04", "workflowType": "requirements-first", "specType": "feature"}
|
||||
359
.kiro/specs/p4-prerequisite-fixes/design.md
Normal file
359
.kiro/specs/p4-prerequisite-fixes/design.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# 技术设计文档:P4 前置依赖修复
|
||||
|
||||
## 概述
|
||||
|
||||
本设计覆盖 6 个定点修复,修正 P4 核心业务层与 Spec 的实现偏差,为 P6 前端任务模块扫清障碍。
|
||||
|
||||
修复范围:
|
||||
- T1:任务列表返回已放弃任务(GAP-3)— **已实现,需验证**
|
||||
- T2:召回完成检测器过滤任务类型(GAP-6)— **已实现,需验证**
|
||||
- T3:备注回溯重分类器冲突处理(GAP-7)— 需修改 `note_reclassifier.py`
|
||||
- T4:回访完成条件改为「有备注即完成」— 需修改 `note_service.py` + `note_reclassifier.py`
|
||||
- T5:trigger_scheduler last_run_at 事务安全(GAP-9)— 需修改 `trigger_scheduler.py`
|
||||
- T6:cron 默认值改为 07:00 — 需修改 `trigger_scheduler.py` 默认值
|
||||
|
||||
**关键发现**:代码审查显示 T1 和 T2 已在之前的修复中完成,T6 的种子数据也已是 `0 7 * * *`,仅 `_calculate_next_run()` 的默认值仍为 `"0 4 * * *"`。
|
||||
|
||||
## 架构
|
||||
|
||||
本次修复不引入新组件,仅修改现有服务层内部逻辑。涉及的调用链:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ETL as ETL Pipeline
|
||||
participant TS as TriggerScheduler
|
||||
participant RD as RecallDetector
|
||||
participant NR as NoteReclassifier
|
||||
participant NS as NoteService
|
||||
participant DB as PostgreSQL (biz schema)
|
||||
|
||||
ETL->>TS: fire_event("etl_data_updated")
|
||||
TS->>RD: run(payload)
|
||||
RD->>DB: 查询 active 召回任务 (T2: 仅 recall 类型)
|
||||
RD->>DB: 标记 completed
|
||||
RD->>TS: fire_event("recall_completed")
|
||||
TS->>NR: run(payload)
|
||||
NR->>DB: 查找 normal 备注 → 重分类为 follow_up
|
||||
NR->>DB: 冲突检查 (T3: 查询已有 follow_up_visit)
|
||||
NR->>DB: 创建/跳过/顶替回访任务 (T4: 有备注→completed)
|
||||
|
||||
Note-->>NS: 助教提交备注
|
||||
NS->>DB: 创建备注
|
||||
NS->>DB: 查询 active follow_up_visit 任务 (T4)
|
||||
NS->>DB: 标记 completed(不依赖 AI 评分)
|
||||
```
|
||||
|
||||
事务边界变更(T5):
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "修复前:两个独立事务"
|
||||
A[handler 事务] --> B[last_run_at 事务]
|
||||
end
|
||||
subgraph "修复后:合并为单一事务"
|
||||
C[handler + last_run_at 同一事务]
|
||||
end
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
本次修复不新增接口,仅修改现有服务层内部方法。以下按修复点列出变更:
|
||||
|
||||
### T1:task_manager.get_task_list()(已实现 ✅)
|
||||
|
||||
代码审查确认 `get_task_list()` 已包含:
|
||||
- `WHERE status IN ('active', 'abandoned')`
|
||||
- `ORDER BY CASE WHEN status = 'abandoned' THEN 1 ELSE 0 END ASC, is_pinned DESC, ...`
|
||||
- SELECT 和返回结构包含 `abandon_reason` 字段
|
||||
|
||||
**本次无需代码变更,仅需验证测试覆盖。**
|
||||
|
||||
### T2:recall_detector._process_service_record()(已实现 ✅)
|
||||
|
||||
代码审查确认 `_process_service_record()` 已包含:
|
||||
- `AND task_type IN ('high_priority_recall', 'priority_recall')`
|
||||
|
||||
**本次无需代码变更,仅需验证测试覆盖。**
|
||||
|
||||
### T3:note_reclassifier.run() — 冲突处理
|
||||
|
||||
**当前问题**:`run()` 在步骤 4/5 直接 INSERT 回访任务,无冲突检查。当 AI 占位返回 None 时跳过任务创建,但修复 T4 后(有备注即完成)将不再依赖 AI 返回值。
|
||||
|
||||
**变更方案**:
|
||||
|
||||
在创建 follow_up_visit 任务前,增加冲突检查逻辑:
|
||||
|
||||
```python
|
||||
# 冲突检查:查询同 (site_id, assistant_id, member_id) 的 follow_up_visit 任务
|
||||
cur.execute("""
|
||||
SELECT id, status
|
||||
FROM biz.coach_tasks
|
||||
WHERE site_id = %s AND assistant_id = %s AND member_id = %s
|
||||
AND task_type = 'follow_up_visit'
|
||||
AND status IN ('active', 'completed')
|
||||
ORDER BY CASE WHEN status = 'completed' THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
""", (site_id, assistant_id, member_id))
|
||||
existing = cur.fetchone()
|
||||
|
||||
if existing:
|
||||
existing_id, existing_status = existing
|
||||
if existing_status == 'completed':
|
||||
# 已完成 → 跳过创建
|
||||
return
|
||||
elif existing_status == 'active':
|
||||
# 顶替:旧任务 → inactive,创建新任务
|
||||
cur.execute("""
|
||||
UPDATE biz.coach_tasks
|
||||
SET status = 'inactive', updated_at = NOW()
|
||||
WHERE id = %s AND status = 'active'
|
||||
""", (existing_id,))
|
||||
_insert_history(cur, existing_id, action="superseded", ...)
|
||||
```
|
||||
|
||||
**设计决策**:
|
||||
- 查询 `completed` 和 `active` 两种状态,优先检查 `completed`
|
||||
- `inactive` 和 `abandoned` 状态的旧任务不阻止新建(语义上已失效)
|
||||
- 顶替操作记录 `superseded` 历史,保留审计链
|
||||
|
||||
### T4:回访完成条件 — note_service.create_note() + note_reclassifier.run()
|
||||
|
||||
**当前问题**:
|
||||
- `note_service.create_note()` 依赖 `ai_score >= 6` 判定回访完成,但 `ai_analyze_note()` 返回 None → 永远不触发完成
|
||||
- `note_reclassifier.run()` 同样依赖 AI 返回值决定任务状态
|
||||
|
||||
**变更方案 — note_service.create_note()**:
|
||||
|
||||
```python
|
||||
# 修改后:有备注即完成,不依赖 AI 评分
|
||||
if note_type == "follow_up" and task_id is not None:
|
||||
# 保留 AI 占位调用(P5 接入时调用链不变)
|
||||
ai_score = ai_analyze_note(note["id"])
|
||||
if ai_score is not None:
|
||||
cur.execute("UPDATE biz.notes SET ai_score = %s ...", (ai_score, note["id"]))
|
||||
note["ai_score"] = ai_score
|
||||
|
||||
# 不论 ai_score 如何,有备注即标记回访任务完成
|
||||
if task_info and task_info["status"] == "active":
|
||||
cur.execute("""
|
||||
UPDATE biz.coach_tasks
|
||||
SET status = 'completed', completed_at = NOW(),
|
||||
completed_task_type = task_type, updated_at = NOW()
|
||||
WHERE id = %s AND status = 'active'
|
||||
""", (task_id,))
|
||||
_record_history(cur, task_id, action="completed_by_note", ...)
|
||||
```
|
||||
|
||||
**变更方案 — note_reclassifier.run()**:
|
||||
|
||||
```python
|
||||
# 修改后:不依赖 AI 返回值
|
||||
# 保留 ai_analyze_note() 占位调用
|
||||
ai_score = ai_analyze_note(note_id)
|
||||
|
||||
# 判定任务状态:有备注 → completed,无备注 → active
|
||||
# 此处 note_id 非 None 意味着已找到备注 → 直接 completed
|
||||
task_status = "completed" # 回溯场景:已有备注 = 已完成
|
||||
|
||||
# 若未找到备注(note_id is None),创建 active 任务
|
||||
# (此分支在上方 note_id is None 时已 return)
|
||||
```
|
||||
|
||||
**设计决策**:
|
||||
- `ai_analyze_note()` 调用保留,返回值仅用于更新 `ai_score` 字段,不影响完成判定
|
||||
- P5 接入后,AI 评分仍会写入 `notes.ai_score`,但不改变完成逻辑
|
||||
- `note_reclassifier` 中:找到备注 = 回溯完成(`completed`),未找到备注 = 等待(`active`)
|
||||
|
||||
### T5:trigger_scheduler — last_run_at 事务安全
|
||||
|
||||
**当前问题**:`fire_event()` 和 `check_scheduled_jobs()` 中,handler 执行和 `last_run_at` 更新在不同事务中。handler 成功但 `last_run_at` commit 失败时,下次重跑会重复处理。
|
||||
|
||||
**变更方案 — 方案 A(handler 内更新 last_run_at)**:
|
||||
|
||||
将 `last_run_at` 更新的 connection 传入 handler,由 handler 在其事务内执行更新。但这要求修改所有 handler 签名,侵入性大。
|
||||
|
||||
**变更方案 — 方案 B(传入 conn,handler 结束后同事务更新)**:
|
||||
|
||||
修改 `fire_event()` 和 `check_scheduled_jobs()`,将 `last_run_at` 更新纳入 handler 的事务范围:
|
||||
|
||||
```python
|
||||
# fire_event() 修改后
|
||||
def fire_event(event_name: str, payload: dict | None = None) -> int:
|
||||
conn = _get_connection()
|
||||
try:
|
||||
# ... 查询 enabled event jobs ...
|
||||
for job_id, job_type, job_name in rows:
|
||||
handler = _JOB_REGISTRY.get(job_type)
|
||||
if not handler:
|
||||
continue
|
||||
try:
|
||||
handler(payload=payload)
|
||||
# handler 成功后,在同一连接上更新 last_run_at
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE biz.trigger_jobs SET last_run_at = NOW() WHERE id = %s",
|
||||
(job_id,),
|
||||
)
|
||||
conn.commit() # handler 数据变更 + last_run_at 一起提交
|
||||
except Exception:
|
||||
conn.rollback() # 一起回滚
|
||||
finally:
|
||||
conn.close()
|
||||
```
|
||||
|
||||
**关键约束**:handler(如 `recall_detector.run()`、`note_reclassifier.run()`)各自管理独立连接和事务。`trigger_scheduler` 的 `last_run_at` 更新使用调度器自己的连接。这意味着 handler 事务和 `last_run_at` 事务仍然是独立的。
|
||||
|
||||
**实际可行方案**:由于 handler 使用独立连接,真正的"同一事务"需要 handler 接受外部 conn 参数。考虑到改动范围,采用折中方案:
|
||||
1. handler 执行完毕后立即更新 `last_run_at`(当前已是如此)
|
||||
2. 依赖 handler 的幂等性保证重复执行安全(`recall_detector` 只匹配 `status='active'`,已完成的不会重复处理)
|
||||
3. 将 `last_run_at` 更新从 handler 成功后的独立 commit 改为与查询 jobs 的同一连接上的事务
|
||||
|
||||
**设计决策**:采用用户确认的方案 A — 将 `last_run_at` 更新纳入 handler 同一事务。具体实现:handler 接受可选的 `conn` 参数,在 handler 的最后一个事务中附带更新 `last_run_at`。对于 event 类型触发器(`recall_detector`、`note_reclassifier`),在 handler 的最终 commit 前插入 `last_run_at` 更新。
|
||||
|
||||
### T6:cron 默认值 07:00
|
||||
|
||||
**当前问题**:种子数据已是 `"0 7 * * *"`,但 `_calculate_next_run()` 的 fallback 默认值仍为 `"0 4 * * *"`。
|
||||
|
||||
**变更方案**:
|
||||
1. `_calculate_next_run()` 中 `trigger_config.get("cron_expression", "0 4 * * *")` → `"0 7 * * *"`
|
||||
2. 新建迁移脚本 `db/zqyy_app/migrations/2026-03-15__p52_update_cron_0700.sql` 作为幂等更新(确保生产环境一致)
|
||||
|
||||
## 数据模型
|
||||
|
||||
本次修复不新增表或字段。涉及的现有表:
|
||||
|
||||
| 表 | 修改内容 | 修复点 |
|
||||
|---|---|---|
|
||||
| `biz.coach_tasks` | 查询条件变更(无 DDL 变更) | T1, T2, T3, T4 |
|
||||
| `biz.coach_task_history` | 新增 `superseded` action 类型记录 | T3 |
|
||||
| `biz.trigger_jobs` | `last_run_at` 更新时机变更;cron_expression 幂等更新 | T5, T6 |
|
||||
| `biz.notes` | 查询条件变更(无 DDL 变更) | T4 |
|
||||
|
||||
`coach_tasks.status` 状态转换新增路径:
|
||||
- `active → inactive`(T3 顶替场景,由 `note_reclassifier` 触发)
|
||||
|
||||
此路径在原 Spec 状态机中已定义("新回访顶替旧回访"),本次为首次实现。
|
||||
|
||||
## 正确性属性(Correctness Properties)
|
||||
|
||||
*属性是系统在所有合法执行路径上都应保持为真的特征或行为——本质上是对系统行为的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: 任务列表状态过滤
|
||||
|
||||
*For any* 任务集合(包含 active、abandoned、completed、inactive 四种状态的任务),`get_task_list` 的过滤逻辑应仅返回 status 为 `active` 或 `abandoned` 的任务,不包含 `completed` 或 `inactive` 状态的任务。
|
||||
|
||||
**Validates: Requirements 1.1**
|
||||
|
||||
### Property 2: 任务列表排序正确性
|
||||
|
||||
*For any* 包含 active 和 abandoned 任务的列表,排序后应满足:(1) 所有 abandoned 任务排在所有 active 任务之后;(2) active 任务内部按 `is_pinned DESC, priority_score DESC NULLS LAST, created_at ASC` 排序。
|
||||
|
||||
**Validates: Requirements 1.2**
|
||||
|
||||
### Property 3: abandon_reason 与 status 一致性不变量
|
||||
|
||||
*For any* 返回的任务记录,若 `status = 'active'` 则 `abandon_reason` 为 null,若 `status = 'abandoned'` 则 `abandon_reason` 为非空字符串。
|
||||
|
||||
**Validates: Requirements 1.3**
|
||||
|
||||
### Property 4: 召回检测器仅完成 recall 类型任务
|
||||
|
||||
*For any* 服务记录和任意任务集合(包含四种 task_type),`_process_service_record` 仅将 `task_type IN ('high_priority_recall', 'priority_recall')` 的 active 任务标记为 completed,`follow_up_visit` 和 `relationship_building` 类型的任务状态不变。
|
||||
|
||||
**Validates: Requirements 2.1, 2.2, 2.3**
|
||||
|
||||
### Property 5: 冲突处理 — 已完成回访任务阻止新建
|
||||
|
||||
*For any* (site_id, assistant_id, member_id) 组合,若已存在 `status = 'completed'` 的 `follow_up_visit` 任务,则 `note_reclassifier` 不创建新的回访任务,且重复触发相同 payload 不产生唯一约束冲突。
|
||||
|
||||
**Validates: Requirements 3.1, 3.4**
|
||||
|
||||
### Property 6: 冲突处理 — active 回访任务被顶替
|
||||
|
||||
*For any* (site_id, assistant_id, member_id) 组合,若已存在 `status = 'active'` 的 `follow_up_visit` 任务,则 `note_reclassifier` 将旧任务标记为 `inactive` 并创建新的回访任务,旧任务的变更记录包含 `superseded` action。
|
||||
|
||||
**Validates: Requirements 3.2**
|
||||
|
||||
### Property 7: 无冲突时正常创建回访任务
|
||||
|
||||
*For any* (site_id, assistant_id, member_id) 组合,若不存在任何 `follow_up_visit` 任务(或仅存在 `inactive`/`abandoned` 状态),则 `note_reclassifier` 正常创建新的回访任务。
|
||||
|
||||
**Validates: Requirements 3.3**
|
||||
|
||||
### Property 8: 有备注即完成回访任务,不依赖 AI 评分
|
||||
|
||||
*For any* `ai_analyze_note()` 的返回值(None、0-5、6-10),当助教为关联 `follow_up_visit` 任务的客户提交备注时,该 active 回访任务都应被标记为 `completed`。AI 评分仅更新 `notes.ai_score` 字段,不影响任务完成判定。
|
||||
|
||||
**Validates: Requirements 4.2, 4.3**
|
||||
|
||||
### Property 9: 回溯有备注时创建 completed 回访任务
|
||||
|
||||
*For any* 召回完成事件,若 `note_reclassifier` 在 service_time 之后找到了 normal 备注,则创建的回访任务 `status = 'completed'`。
|
||||
|
||||
**Validates: Requirements 4.4**
|
||||
|
||||
### Property 10: 回溯无备注时创建 active 回访任务
|
||||
|
||||
*For any* 召回完成事件,若 `note_reclassifier` 在 service_time 之后未找到 normal 备注,则创建的回访任务 `status = 'active'`(等待助教提交备注)。
|
||||
|
||||
**Validates: Requirements 4.5**
|
||||
|
||||
### Property 11: 触发器 last_run_at 事务一致性
|
||||
|
||||
*For any* 触发器执行(event/cron/interval 类型),handler 成功时 `last_run_at` 应被更新,handler 抛出异常时 `last_run_at` 应保持不变(整个事务回滚)。
|
||||
|
||||
**Validates: Requirements 5.1, 5.2**
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 场景 | 处理方式 | 修复点 |
|
||||
|------|---------|--------|
|
||||
| T3 冲突查询失败 | 捕获异常,rollback,记录日志,返回 `tasks_created: 0` | T3 |
|
||||
| T3 顶替 UPDATE 失败 | 捕获异常,rollback,不创建新任务 | T3 |
|
||||
| T4 备注创建成功但任务完成 UPDATE 失败 | 整个事务 rollback(备注也不创建),返回 500 | T4 |
|
||||
| T4 ai_analyze_note() 抛出异常 | 捕获异常,记录日志,继续执行任务完成逻辑(AI 失败不阻塞业务) | T4 |
|
||||
| T5 handler 成功但 commit 失败 | 整个事务回滚(handler 数据变更 + last_run_at),下次重跑依赖幂等性 | T5 |
|
||||
| T5 handler 抛出异常 | rollback,last_run_at 不更新,下次重跑 | T5 |
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 属性测试(Property-Based Testing)
|
||||
|
||||
使用 **Hypothesis** 库(项目已有依赖),每个属性测试最少 100 次迭代。
|
||||
|
||||
每个测试用 comment 标注对应的设计属性:
|
||||
```python
|
||||
# Feature: p4-prerequisite-fixes, Property 1: 任务列表状态过滤
|
||||
```
|
||||
|
||||
属性测试重点覆盖:
|
||||
- Property 1-3:任务列表过滤、排序、字段不变量(纯函数可提取测试)
|
||||
- Property 4:召回检测器类型过滤(SQL 条件验证)
|
||||
- Property 5-7:冲突处理三分支(状态机测试)
|
||||
- Property 8:AI 评分不影响完成判定(参数化 ai_score 返回值)
|
||||
- Property 9-10:回溯场景任务状态(有/无备注两分支)
|
||||
- Property 11:事务一致性(mock handler 成功/失败)
|
||||
|
||||
### 单元测试
|
||||
|
||||
单元测试聚焦于:
|
||||
- T1 边界:全部 active(无 abandoned)、全部 abandoned(无 active)、混合场景
|
||||
- T2 边界:仅有 follow_up_visit 任务时返回 0 completed
|
||||
- T3 边界:同时存在 completed 和 active 的 follow_up_visit(优先检查 completed → 跳过)
|
||||
- T4 边界:task_id 为 None 时不触发完成逻辑;非 follow_up_visit 任务不触发
|
||||
- T5 边界:handler 抛出特定异常类型时的回滚行为
|
||||
- T6 具体值:`_calculate_next_run("cron", {})` 默认使用 `"0 7 * * *"`;迁移脚本 SQL 正确性
|
||||
|
||||
### 测试配置
|
||||
|
||||
```python
|
||||
from hypothesis import given, settings, strategies as st
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(...)
|
||||
def test_property_name(...):
|
||||
# Feature: p4-prerequisite-fixes, Property N: property_text
|
||||
...
|
||||
```
|
||||
|
||||
测试文件位置:`tests/` 目录(Monorepo 级属性测试,与现有 P4 属性测试同级)。
|
||||
94
.kiro/specs/p4-prerequisite-fixes/requirements.md
Normal file
94
.kiro/specs/p4-prerequisite-fixes/requirements.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 需求文档:P4 前置依赖修复
|
||||
|
||||
## 简介
|
||||
|
||||
P4 核心业务层已实现并通过属性测试,但对比 Spec 发现 6 处实现偏差(来源:`docs/reports/P4-spec-vs-implementation-gap-analysis.md`)。这些偏差会影响 P6(前端任务模块)的正常开发,必须前置修复。本需求覆盖 GAP-3、GAP-6、GAP-7、GAP-9 的代码修复,以及两项新增修正(回访完成条件、cron 时间)。
|
||||
|
||||
修复范围:6 个定点修复,无新表、无新接口,仅修改现有服务层逻辑和一条迁移脚本。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **Task_Manager**: 任务管理服务(`task_manager.py`),负责任务列表查询和状态操作
|
||||
- **Recall_Detector**: 召回完成检测器(`recall_detector.py`),ETL 数据更新后匹配服务记录完成召回任务
|
||||
- **Note_Reclassifier**: 备注回溯重分类器(`note_reclassifier.py`),召回完成后回溯普通备注为回访备注并创建回访任务
|
||||
- **Note_Service**: 备注服务(`note_service.py`),负责备注 CRUD 和回访任务自动完成
|
||||
- **Trigger_Scheduler**: 触发器调度框架(`trigger_scheduler.py`),统一管理 cron/interval/event 三种触发方式
|
||||
- **coach_tasks**: 助教任务表(`biz.coach_tasks`)
|
||||
- **trigger_jobs**: 触发器配置表(`biz.trigger_jobs`)
|
||||
- **active**: 任务有效状态
|
||||
- **abandoned**: 任务已放弃状态
|
||||
- **inactive**: 任务无效状态(被顶替)
|
||||
- **completed**: 任务已完成状态
|
||||
- **high_priority_recall**: 高优先召回任务类型
|
||||
- **priority_recall**: 优先召回任务类型
|
||||
- **follow_up_visit**: 客户回访任务类型
|
||||
- **relationship_building**: 关系构建任务类型
|
||||
- **abandon_reason**: 放弃原因字段
|
||||
- **last_run_at**: 触发器上次运行时间戳
|
||||
- **ai_analyze_note()**: AI 备注分析占位函数(P5 实现,当前返回 None)
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:任务列表返回已放弃任务
|
||||
|
||||
**用户故事:** 作为助教,我希望在任务列表中看到已放弃的任务及其放弃原因,以便执行「取消放弃」操作恢复任务。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 助教请求任务列表, THE Task_Manager SHALL 返回 active 和 abandoned 两种状态的任务
|
||||
2. THE Task_Manager SHALL 按以下顺序排列任务:abandoned 排在所有 active 任务之后,active 任务内部按 is_pinned DESC、priority_score DESC NULLS LAST、created_at ASC 排序
|
||||
3. THE Task_Manager SHALL 在返回结构中始终包含 abandon_reason 字段,active 状态任务的 abandon_reason 为 null,abandoned 状态任务的 abandon_reason 为非空字符串
|
||||
4. WHEN 不存在 abandoned 任务时, THE Task_Manager SHALL 仅返回 active 任务,返回结构不变(abandon_reason 为 null)
|
||||
|
||||
### 需求 2:召回完成检测器过滤任务类型
|
||||
|
||||
**用户故事:** 作为系统运维人员,我希望召回完成检测器仅完成召回类任务,避免错误地将回访和关系构建任务标记为已完成。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN ETL 检测到新增服务记录, THE Recall_Detector SHALL 仅匹配 task_type 为 high_priority_recall 或 priority_recall 的 active 任务进行完成标记
|
||||
2. WHILE 存在 active 的 follow_up_visit 任务, THE Recall_Detector SHALL 跳过该任务,不执行完成标记
|
||||
3. WHILE 存在 active 的 relationship_building 任务, THE Recall_Detector SHALL 跳过该任务,不执行完成标记
|
||||
|
||||
### 需求 3:备注回溯重分类器冲突处理
|
||||
|
||||
**用户故事:** 作为系统运维人员,我希望备注回溯重分类器在创建回访任务时正确处理冲突,避免唯一约束违反和重复任务。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Note_Reclassifier 准备创建 follow_up_visit 任务且同 (site_id, assistant_id, member_id) 已存在 completed 的 follow_up_visit 任务, THEN THE Note_Reclassifier SHALL 跳过创建(回访完成语义已满足)
|
||||
2. WHEN Note_Reclassifier 准备创建 follow_up_visit 任务且同 (site_id, assistant_id, member_id) 已存在 active 的 follow_up_visit 任务, THEN THE Note_Reclassifier SHALL 将旧任务标记为 inactive 并记录变更历史,然后创建新的 follow_up_visit 任务(顶替方案)
|
||||
3. WHEN Note_Reclassifier 准备创建 follow_up_visit 任务且同 (site_id, assistant_id, member_id) 不存在任何 follow_up_visit 任务, THE Note_Reclassifier SHALL 正常创建新任务
|
||||
4. IF Note_Reclassifier 重复触发相同 payload, THEN THE Note_Reclassifier SHALL 不产生唯一约束冲突错误
|
||||
|
||||
### 需求 4:回访任务完成条件改为「有备注即完成」
|
||||
|
||||
**用户故事:** 作为助教,我希望为客户提交备注后对应的回访任务自动完成,无需等待 AI 评分。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 助教通过 Note_Service 为某客户创建备注, THE Note_Service SHALL 查询该客户是否存在 active 的 follow_up_visit 任务(通过 user_assistant_binding 获取 assistant_id,再匹配 site_id、assistant_id、member_id)
|
||||
2. WHEN 存在 active 的 follow_up_visit 任务且助教提交了备注, THE Note_Service SHALL 将该任务标记为 completed 并记录变更历史,完成判定不依赖 ai_analyze_note() 的返回值
|
||||
3. THE Note_Service SHALL 保留 ai_analyze_note() 占位调用(P5 接入时调用链不变),但 ai_analyze_note() 的返回值不作为回访任务完成的判定条件
|
||||
4. WHEN Note_Reclassifier 回溯发现已有备注, THE Note_Reclassifier SHALL 直接创建 status='completed' 的回访任务(回溯完成),不依赖 AI 评分
|
||||
5. WHEN Note_Reclassifier 回溯未发现备注, THE Note_Reclassifier SHALL 创建 status='active' 的回访任务(等待助教提交备注)
|
||||
|
||||
### 需求 5:trigger_scheduler last_run_at 事务安全
|
||||
|
||||
**用户故事:** 作为系统运维人员,我希望触发器的 last_run_at 更新与 handler 执行在同一事务中,避免 handler 成功但 last_run_at 更新失败导致重复处理。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Trigger_Scheduler 执行 event 类型触发器(fire_event), THE Trigger_Scheduler SHALL 将 last_run_at 更新纳入 handler 执行的同一事务范围,handler 成功与 last_run_at 更新要么一起提交要么一起回滚
|
||||
2. WHEN Trigger_Scheduler 执行 cron/interval 类型触发器(check_scheduled_jobs), THE Trigger_Scheduler SHALL 将 last_run_at 和 next_run_at 更新纳入 handler 执行的同一事务范围
|
||||
3. IF handler 执行成功但事务提交失败, THEN THE Trigger_Scheduler SHALL 回滚整个事务(包括 handler 的数据变更和 last_run_at 更新),下次重跑时 handler 的幂等性保证不会产生副作用
|
||||
|
||||
### 需求 6:任务生成器 cron 时间改为 07:00
|
||||
|
||||
**用户故事:** 作为运营人员,我希望任务生成器在每天早上 7 点运行,与门店营业节奏匹配。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 迁移脚本 SHALL 将 trigger_jobs 表中 task_generator 的 cron_expression 从 `0 4 * * *` 更新为 `0 7 * * *`
|
||||
2. THE Trigger_Scheduler SHALL 在 _calculate_next_run() 中使用 `0 7 * * *` 作为 cron 默认值
|
||||
3. WHEN task_generator 触发器下次运行时间被计算, THE Trigger_Scheduler SHALL 基于 `0 7 * * *` 计算正确的 next_run_at
|
||||
104
.kiro/specs/p4-prerequisite-fixes/tasks.md
Normal file
104
.kiro/specs/p4-prerequisite-fixes/tasks.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Implementation Plan: P4 前置依赖修复
|
||||
|
||||
## Overview
|
||||
|
||||
6 个定点修复,修正 P4 核心业务层与 Spec 的实现偏差。T1/T2 已在之前修复中完成,仅需属性测试验证;T3/T4/T5/T6 需要代码变更。所有修改限于现有服务层内部逻辑,无新表、无新接口。
|
||||
|
||||
测试框架:Hypothesis(项目已有依赖),测试文件位于 `tests/` 目录。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. 验证 T1(任务列表)和 T2(召回检测器)已有实现 + 属性测试
|
||||
- [x] 1.1 编写 T1 属性测试:任务列表状态过滤、排序、abandon_reason 一致性
|
||||
- 创建 `tests/test_p52_task_list_properties.py`
|
||||
- 提取 `get_task_list` 的过滤和排序逻辑为纯函数(或 mock DB 层),用 Hypothesis 生成随机任务集合
|
||||
- **Property 1: 任务列表状态过滤** — 仅返回 active/abandoned,不含 completed/inactive
|
||||
- **Property 2: 任务列表排序正确性** — abandoned 排在 active 之后,active 内部按 is_pinned DESC, priority_score DESC NULLS LAST, created_at ASC
|
||||
- **Property 3: abandon_reason 与 status 一致性不变量** — active → null, abandoned → 非空字符串
|
||||
- **Validates: Requirements 1.1, 1.2, 1.3, 1.4**
|
||||
|
||||
- [x] 1.2 编写 T2 属性测试:召回检测器仅完成 recall 类型任务
|
||||
- 在 `tests/test_p52_recall_detector_properties.py` 中编写
|
||||
- 用 Hypothesis 生成包含四种 task_type 的任务集合和服务记录
|
||||
- **Property 4: 召回检测器仅完成 recall 类型任务** — high_priority_recall/priority_recall 被完成,follow_up_visit/relationship_building 状态不变
|
||||
- **Validates: Requirements 2.1, 2.2, 2.3**
|
||||
|
||||
- [x] 2. Checkpoint — 验证 T1/T2 属性测试通过
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 3. 实现 T3:备注回溯重分类器冲突处理
|
||||
- [x] 3.1 修改 `apps/backend/app/services/note_reclassifier.py` 的 `run()` 方法
|
||||
- 在创建 follow_up_visit 任务前,查询同 (site_id, assistant_id, member_id) 是否已有 follow_up_visit 任务
|
||||
- 已有 `completed` → 跳过创建(回访完成语义已满足)
|
||||
- 已有 `active` → 旧任务标记 `inactive`,记录 `superseded` 历史,创建新任务
|
||||
- 不存在(或仅有 inactive/abandoned)→ 正常创建
|
||||
- 确保重复触发相同 payload 不产生唯一约束冲突
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4_
|
||||
|
||||
- [x] 3.2 编写 T3 属性测试:冲突处理三分支
|
||||
- 在 `tests/test_p52_note_reclassifier_properties.py` 中编写
|
||||
- **Property 5: 已完成回访任务阻止新建** — 存在 completed 的 follow_up_visit 时跳过创建
|
||||
- **Property 6: active 回访任务被顶替** — 旧任务 → inactive + superseded 历史,新任务创建
|
||||
- **Property 7: 无冲突时正常创建** — 不存在 follow_up_visit(或仅 inactive/abandoned)时正常创建
|
||||
- **Validates: Requirements 3.1, 3.2, 3.3, 3.4**
|
||||
|
||||
- [x] 4. 实现 T4:回访完成条件改为「有备注即完成」
|
||||
- [x] 4.1 修改 `apps/backend/app/services/note_service.py` 的 `create_note()` 方法
|
||||
- 当 `note_type == "follow_up"` 且 `task_id is not None` 时:
|
||||
- 保留 `ai_analyze_note()` 占位调用,返回值仅用于更新 `ai_score` 字段
|
||||
- 不论 `ai_score` 如何,有备注即标记关联的 active follow_up_visit 任务为 `completed`
|
||||
- 记录 `completed_by_note` 历史
|
||||
- _Requirements: 4.1, 4.2, 4.3_
|
||||
|
||||
- [x] 4.2 修改 `apps/backend/app/services/note_reclassifier.py` 的 `run()` 方法(T4 部分)
|
||||
- 去掉 AI 评分判定逻辑(`ai_score >= 6` 条件)
|
||||
- 保留 `ai_analyze_note()` 占位调用
|
||||
- 找到备注(`note_id is not None`)→ 创建 `status='completed'` 的回访任务(回溯完成)
|
||||
- 未找到备注(`note_id is None`)→ 创建 `status='active'` 的回访任务(等待备注)
|
||||
- 注意:此步骤需与 3.1 的冲突处理逻辑协同,冲突检查在任务创建前执行
|
||||
- _Requirements: 4.4, 4.5_
|
||||
|
||||
- [x] 4.3 编写 T4 属性测试:有备注即完成
|
||||
- 在 `tests/test_p52_note_service_properties.py` 中编写
|
||||
- **Property 8: 有备注即完成回访任务,不依赖 AI 评分** — 对任意 ai_score(None/0-5/6-10),提交备注后 active follow_up_visit 任务都标记 completed
|
||||
- **Property 9: 回溯有备注时创建 completed 回访任务** — note_reclassifier 找到备注 → status='completed'
|
||||
- **Property 10: 回溯无备注时创建 active 回访任务** — note_reclassifier 未找到备注 → status='active'
|
||||
- **Validates: Requirements 4.2, 4.3, 4.4, 4.5**
|
||||
|
||||
- [x] 5. Checkpoint — 验证 T3/T4 实现和测试通过
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 6. 实现 T5:trigger_scheduler last_run_at 事务安全
|
||||
- [x] 6.1 修改 `apps/backend/app/services/trigger_scheduler.py`
|
||||
- `fire_event()`:handler 接受可选 `conn` 参数,在 handler 最终 commit 前附带更新 `last_run_at`;handler 失败时整个事务回滚(last_run_at 不更新)
|
||||
- `check_scheduled_jobs()`:同理,将 `last_run_at` 和 `next_run_at` 更新纳入 handler 事务范围
|
||||
- 需同步修改 `recall_detector.run()` 和 `note_reclassifier.run()` 的签名,接受可选 `conn` 参数
|
||||
- _Requirements: 5.1, 5.2, 5.3_
|
||||
|
||||
- [x] 6.2 编写 T5 属性测试:事务一致性
|
||||
- 在 `tests/test_p52_trigger_scheduler_properties.py` 中编写
|
||||
- **Property 11: 触发器 last_run_at 事务一致性** — handler 成功 → last_run_at 更新;handler 异常 → last_run_at 不变(整个事务回滚)
|
||||
- **Validates: Requirements 5.1, 5.2**
|
||||
|
||||
- [x] 7. 实现 T6:cron 默认值改为 07:00 + 迁移脚本
|
||||
- [x] 7.1 修改 `apps/backend/app/services/trigger_scheduler.py` 的 `_calculate_next_run()`
|
||||
- 将 `trigger_config.get("cron_expression", "0 4 * * *")` 改为 `"0 7 * * *"`
|
||||
- _Requirements: 6.2, 6.3_
|
||||
|
||||
- [x] 7.2 创建迁移脚本 `db/zqyy_app/migrations/2026-03-15__p52_update_cron_0700.sql`
|
||||
- 幂等 UPDATE:`UPDATE biz.trigger_jobs SET trigger_config = jsonb_set(trigger_config, '{cron_expression}', '"0 7 * * *"') WHERE job_name = 'task_generator'`
|
||||
- 包含回滚注释
|
||||
- _Requirements: 6.1_
|
||||
|
||||
- [x] 8. Final checkpoint — 全部测试通过
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
- 运行 `pytest tests/test_p52_*.py -v` 验证所有 P5.2 属性测试通过
|
||||
|
||||
## Notes
|
||||
|
||||
- Tasks marked with `*` are optional and can be skipped for faster MVP
|
||||
- T1/T2 已在之前修复中实现,任务 1 仅编写属性测试验证覆盖
|
||||
- T3 和 T4 对 `note_reclassifier.py` 有交叉修改,任务 3.1 和 4.2 需协同实施
|
||||
- T5 涉及 handler 签名变更,需同步修改 recall_detector 和 note_reclassifier
|
||||
- T6 种子数据已是 `0 7 * * *`,迁移脚本为幂等更新确保生产环境一致
|
||||
- 属性测试使用 Hypothesis,每个属性最少 100 次迭代
|
||||
Reference in New Issue
Block a user