微信小程序页面迁移校验之前 P5任务处理之前

This commit is contained in:
Neo
2026-03-09 01:19:21 +08:00
parent 263bf96035
commit 6e20987d2f
1112 changed files with 153824 additions and 219694 deletions

View File

@@ -0,0 +1,326 @@
"""
备注服务
负责备注 CRUD、星星评分存储与读取。
备注类型根据关联任务自动确定follow_up_visit 任务 → follow_up否则 normal。
星星评分不参与回访完成判定,不参与 AI 分析,仅存储。
"""
import json
import logging
from fastapi import HTTPException
logger = logging.getLogger(__name__)
def _get_connection():
"""延迟导入 get_connection避免纯函数测试时触发模块级导入失败。"""
from app.database import get_connection
return get_connection()
def _record_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 中记录变更(复用 task_manager 的模式)。"""
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
async def create_note(
site_id: int,
user_id: int,
target_type: str,
target_id: int,
content: str,
task_id: int | None = None,
rating_service_willingness: int | None = None,
rating_revisit_likelihood: int | None = None,
) -> dict:
"""
创建备注。
1. 验证评分范围1-5 或 NULL不合法则 422
2. 确定 note type
- 若 task_id 关联的任务 task_type='follow_up_visit' → type='follow_up'
- 否则 → type='normal'
3. INSERT INTO biz.notes
4. 若 type='follow_up'
- 触发 AI 应用 6 分析P5 实现)
- 若 ai_score >= 6 且关联任务 status='active' → 标记任务 completed
5. 返回创建的备注记录
注意:星星评分不参与回访完成判定,不参与 AI 分析,仅存储。
"""
# 验证评分范围
for label, val in [
("再次服务意愿评分", rating_service_willingness),
("再来店可能性评分", rating_revisit_likelihood),
]:
if val is not None and (val < 1 or val > 5):
raise HTTPException(
status_code=422, detail=f"{label}必须在 1-5 范围内"
)
conn = _get_connection()
try:
# 确定 note type
note_type = "normal"
task_info = None
if task_id is not None:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, task_type, status, site_id
FROM biz.coach_tasks
WHERE id = %s
""",
(task_id,),
)
row = cur.fetchone()
if row is None:
raise HTTPException(status_code=404, detail="关联任务不存在")
task_info = {
"id": row[0],
"task_type": row[1],
"status": row[2],
"site_id": row[3],
}
if task_info["site_id"] != site_id:
raise HTTPException(status_code=403, detail="权限不足")
if task_info["task_type"] == "follow_up_visit":
note_type = "follow_up"
# INSERT 备注
with conn.cursor() as cur:
cur.execute("BEGIN")
cur.execute(
"""
INSERT INTO biz.notes
(site_id, user_id, target_type, target_id, type,
content, rating_service_willingness,
rating_revisit_likelihood, task_id)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id, site_id, user_id, target_type, target_id,
type, content, rating_service_willingness,
rating_revisit_likelihood, task_id,
ai_score, ai_analysis, created_at, updated_at
""",
(
site_id, user_id, target_type, target_id, note_type,
content, rating_service_willingness,
rating_revisit_likelihood, task_id,
),
)
row = cur.fetchone()
note = {
"id": row[0],
"site_id": row[1],
"user_id": row[2],
"target_type": row[3],
"target_id": row[4],
"type": row[5],
"content": row[6],
"rating_service_willingness": row[7],
"rating_revisit_likelihood": row[8],
"task_id": row[9],
"ai_score": row[10],
"ai_analysis": row[11],
"created_at": row[12].isoformat() if row[12] else None,
"updated_at": row[13].isoformat() if row[13] else None,
}
# 若 type='follow_up',触发 AI 分析并可能标记任务完成
if note_type == "follow_up" and task_id is not None:
ai_score = ai_analyze_note(note["id"])
if ai_score is not None:
# 更新备注的 ai_score
cur.execute(
"""
UPDATE biz.notes
SET ai_score = %s, updated_at = NOW()
WHERE id = %s
""",
(ai_score, note["id"]),
)
note["ai_score"] = ai_score
# 若 ai_score >= 6 且关联任务 status='active' → 标记任务 completed
if ai_score >= 6 and 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",
old_status="active",
new_status="completed",
old_task_type=task_info["task_type"],
new_task_type=task_info["task_type"],
detail={
"note_id": note["id"],
"ai_score": ai_score,
},
)
conn.commit()
return note
except HTTPException:
conn.rollback()
raise
except Exception:
conn.rollback()
raise
finally:
conn.close()
async def get_notes(
site_id: int, target_type: str, target_id: int
) -> list[dict]:
"""
查询某目标的备注列表。
按 created_at DESC 排序,包含星星评分和 AI 评分。
"""
conn = _get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, site_id, user_id, target_type, target_id,
type, content, rating_service_willingness,
rating_revisit_likelihood, task_id,
ai_score, ai_analysis, created_at, updated_at
FROM biz.notes
WHERE site_id = %s
AND target_type = %s
AND target_id = %s
ORDER BY created_at DESC
""",
(site_id, target_type, target_id),
)
rows = cur.fetchall()
conn.commit()
return [
{
"id": r[0],
"site_id": r[1],
"user_id": r[2],
"target_type": r[3],
"target_id": r[4],
"type": r[5],
"content": r[6],
"rating_service_willingness": r[7],
"rating_revisit_likelihood": r[8],
"task_id": r[9],
"ai_score": r[10],
"ai_analysis": r[11],
"created_at": r[12].isoformat() if r[12] else None,
"updated_at": r[13].isoformat() if r[13] else None,
}
for r in rows
]
finally:
conn.close()
async def delete_note(note_id: int, user_id: int, site_id: int) -> dict:
"""
删除备注。
验证备注归属user_id + site_id后执行硬删除。
- 不存在 → 404
- 不属于当前用户 → 403
"""
conn = _get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, user_id, site_id
FROM biz.notes
WHERE id = %s
""",
(note_id,),
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="资源不存在")
if row[2] != site_id or row[1] != user_id:
raise HTTPException(status_code=403, detail="权限不足")
with conn.cursor() as cur:
cur.execute("BEGIN")
cur.execute(
"DELETE FROM biz.notes WHERE id = %s",
(note_id,),
)
conn.commit()
return {"id": note_id, "deleted": True}
except HTTPException:
conn.rollback()
raise
except Exception:
conn.rollback()
raise
finally:
conn.close()