# -*- coding: utf-8 -*- """ 备注回溯重分类器(Note Reclassifier) 召回完成后,回溯检查是否有普通备注需重分类为回访备注。 查找 service_time 之后的第一条 normal 备注 → 更新为 follow_up → 触发 AI 应用 6 接口(占位)→ 根据 ai_score 生成 follow_up_visit 任务。 由 trigger_jobs 中的 note_reclassify_backfill 配置驱动(event: recall_completed)。 """ import json import logging logger = logging.getLogger(__name__) def _get_connection(): """延迟导入 get_connection,避免模块级导入失败。""" from app.database import get_connection return get_connection() def _insert_history( cur, task_id: int, action: str, old_status: str | None = None, new_status: str | None = None, old_task_type: str | None = None, new_task_type: str | None = None, detail: dict | None = None, ) -> None: """在 coach_task_history 中记录变更。""" cur.execute( """ INSERT INTO biz.coach_task_history (task_id, action, old_status, new_status, old_task_type, new_task_type, detail) VALUES (%s, %s, %s, %s, %s, %s, %s) """, ( task_id, action, old_status, new_status, old_task_type, new_task_type, json.dumps(detail) if detail else None, ), ) def ai_analyze_note(note_id: int) -> int | None: """ AI 应用 6 备注分析接口(占位)。 P5 AI 集成层实现后替换此占位函数。 当前返回 None 表示 AI 未就绪,跳过评分逻辑。 """ return None def run(payload: dict | None = None) -> dict: """ 备注回溯主流程。 payload 包含: {site_id, assistant_id, member_id, service_time} 1. 查找 biz.notes 中该 (site_id, target_type='member', target_id=member_id) 在 service_time 之后提交的第一条 type='normal' 的备注 2. 将该备注 type 从 'normal' 更新为 'follow_up' 3. 触发 AI 应用 6 接口(P5 实现,本 SPEC 仅定义触发接口): - 调用 ai_analyze_note(note_id) → 返回 ai_score 4. 若 ai_score >= 6: - 生成 follow_up_visit 任务,status='completed'(回溯完成) 5. 若 ai_score < 6: - 生成 follow_up_visit 任务,status='active'(需助教重新备注) 返回: {"reclassified_count": int, "tasks_created": int} """ if not payload: return {"reclassified_count": 0, "tasks_created": 0} site_id = payload.get("site_id") assistant_id = payload.get("assistant_id") member_id = payload.get("member_id") service_time = payload.get("service_time") if not all([site_id, assistant_id, member_id, service_time]): logger.warning("备注回溯缺少必要参数: %s", payload) return {"reclassified_count": 0, "tasks_created": 0} reclassified_count = 0 tasks_created = 0 conn = _get_connection() try: # ── 1. 查找 service_time 之后的第一条 normal 备注 ── note_id = None with conn.cursor() as cur: cur.execute( """ SELECT id FROM biz.notes WHERE site_id = %s AND target_type = 'member' AND target_id = %s AND type = 'normal' AND created_at > %s ORDER BY created_at ASC LIMIT 1 """, (site_id, member_id, service_time), ) row = cur.fetchone() if row: note_id = row[0] conn.commit() if note_id is None: logger.info( "未找到符合条件的 normal 备注: site_id=%s, member_id=%s", site_id, member_id, ) return {"reclassified_count": 0, "tasks_created": 0} # ── 2. 将备注 type 从 'normal' 更新为 'follow_up' ── with conn.cursor() as cur: cur.execute("BEGIN") cur.execute( """ UPDATE biz.notes SET type = 'follow_up', updated_at = NOW() WHERE id = %s AND type = 'normal' """, (note_id,), ) conn.commit() reclassified_count = 1 # ── 3. 触发 AI 应用 6 接口(占位,当前返回 None) ── ai_score = ai_analyze_note(note_id) # ── 4/5. 根据 ai_score 生成 follow_up_visit 任务 ── if ai_score is not None: if ai_score >= 6: # 回溯完成:生成 completed 任务 task_status = "completed" else: # 需助教重新备注:生成 active 任务 task_status = "active" with conn.cursor() as cur: cur.execute("BEGIN") cur.execute( """ INSERT INTO biz.coach_tasks (site_id, assistant_id, member_id, task_type, status, completed_at, completed_task_type) VALUES ( %s, %s, %s, 'follow_up_visit', %s, CASE WHEN %s = 'completed' THEN NOW() ELSE NULL END, CASE WHEN %s = 'completed' THEN 'follow_up_visit' ELSE NULL END ) RETURNING id """, ( site_id, assistant_id, member_id, task_status, task_status, task_status, ), ) new_task_row = cur.fetchone() new_task_id = new_task_row[0] # 记录任务创建历史 _insert_history( cur, new_task_id, action="created_by_reclassify", old_status=None, new_status=task_status, old_task_type=None, new_task_type="follow_up_visit", detail={ "note_id": note_id, "ai_score": ai_score, "source": "note_reclassifier", }, ) conn.commit() tasks_created = 1 else: # AI 未就绪,跳过任务创建 logger.info( "AI 接口未就绪,跳过任务创建: note_id=%s", note_id ) except Exception: logger.exception( "备注回溯失败: site_id=%s, member_id=%s", site_id, member_id, ) conn.rollback() finally: conn.close() logger.info( "备注回溯完成: reclassified_count=%d, tasks_created=%d", reclassified_count, tasks_created, ) return { "reclassified_count": reclassified_count, "tasks_created": tasks_created, }