微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
326
apps/backend/app/services/note_service.py
Normal file
326
apps/backend/app/services/note_service.py
Normal 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()
|
||||
Reference in New Issue
Block a user