feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
640
apps/backend/app/routers/admin_task_engine.py
Normal file
640
apps/backend/app/routers/admin_task_engine.py
Normal file
@@ -0,0 +1,640 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P18 任务引擎运营看板 API
|
||||
|
||||
提供转移日志查看、待审核任务管理、参数配置等端点。
|
||||
所有端点需要 JWT 认证;写操作仅限 super_admin。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
from app.auth.dependencies import CurrentUser, get_current_user
|
||||
from app.database import get_connection
|
||||
from app.schemas.admin_task_engine import (
|
||||
CandidateAssistant,
|
||||
CandidateListResponse,
|
||||
CloseRequest,
|
||||
CloseResponse,
|
||||
ConfigParam,
|
||||
ConfigParamCreate,
|
||||
ConfigParamList,
|
||||
ConfigParamResponse,
|
||||
ConfigParamUpdate,
|
||||
PendingReviewItem,
|
||||
PendingReviewPage,
|
||||
ReassignRequest,
|
||||
ReassignResponse,
|
||||
TransferLogItem,
|
||||
TransferLogPage,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/admin/task-engine", tags=["任务引擎管理"])
|
||||
|
||||
# ---- 任务类型中文映射 ----
|
||||
TASK_TYPE_LABELS = {
|
||||
"high_priority_recall": "高优先召回",
|
||||
"priority_recall": "优先召回",
|
||||
"follow_up_visit": "客户回访",
|
||||
"relationship_building": "关系构建",
|
||||
}
|
||||
|
||||
|
||||
# ---- 权限辅助函数 ----
|
||||
|
||||
def _require_super_admin(user: CurrentUser) -> None:
|
||||
"""写操作权限校验:仅超级管理员可执行。"""
|
||||
if "super_admin" not in user.roles:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="仅超级管理员可执行此操作",
|
||||
)
|
||||
|
||||
|
||||
def _filter_site_id(user: CurrentUser, query_site_id: int | None) -> int | None:
|
||||
"""读操作门店过滤:门店管理员强制按自身 site_id 过滤。"""
|
||||
if "super_admin" in user.roles:
|
||||
return query_site_id
|
||||
return user.site_id
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 1. 转移日志
|
||||
# =====================================================================
|
||||
|
||||
@router.get("/transfer-log", response_model=TransferLogPage)
|
||||
async def list_transfer_logs(
|
||||
site_id: int | None = Query(None, description="门店 ID"),
|
||||
from_date: date | None = Query(None, description="起始日期"),
|
||||
to_date: date | None = Query(None, description="截止日期"),
|
||||
assistant_id: int | None = Query(None, description="助教 ID"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> TransferLogPage:
|
||||
"""转移日志分页列表。"""
|
||||
effective_site_id = _filter_site_id(user, site_id)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
conditions = []
|
||||
params: list = []
|
||||
|
||||
if effective_site_id is not None:
|
||||
conditions.append("t.site_id = %s")
|
||||
params.append(effective_site_id)
|
||||
if from_date is not None:
|
||||
conditions.append("t.created_at >= %s")
|
||||
params.append(from_date)
|
||||
if to_date is not None:
|
||||
conditions.append("t.created_at < %s::date + interval '1 day'")
|
||||
params.append(to_date)
|
||||
if assistant_id is not None:
|
||||
conditions.append("(t.from_assistant_id = %s OR t.to_assistant_id = %s)")
|
||||
params.extend([assistant_id, assistant_id])
|
||||
|
||||
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
||||
|
||||
# 总数
|
||||
cur.execute(
|
||||
f"SELECT count(*) AS cnt FROM biz.coach_task_transfer_log t WHERE {where_clause}",
|
||||
params,
|
||||
)
|
||||
total = cur.fetchone()["cnt"]
|
||||
|
||||
# 分页数据
|
||||
offset = (page - 1) * page_size
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT t.*, s.site_name
|
||||
FROM biz.coach_task_transfer_log t
|
||||
LEFT JOIN biz.sites s ON s.site_id = t.site_id
|
||||
WHERE {where_clause}
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
params + [page_size, offset],
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
items = [TransferLogItem(**row) for row in rows]
|
||||
return TransferLogPage(items=items, total=total)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.exception("查询转移日志失败")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"查询转移日志失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/transfer-log/{member_id}/history", response_model=list[TransferLogItem])
|
||||
async def get_member_transfer_history(
|
||||
member_id: int,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> list[TransferLogItem]:
|
||||
"""某客户全部转移历史。"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
# 门店管理员只能看自己门店的记录
|
||||
effective_site_id = _filter_site_id(user, None)
|
||||
if effective_site_id is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT t.*, s.site_name
|
||||
FROM biz.coach_task_transfer_log t
|
||||
LEFT JOIN biz.sites s ON s.site_id = t.site_id
|
||||
WHERE t.member_id = %s AND t.site_id = %s
|
||||
ORDER BY t.created_at DESC
|
||||
""",
|
||||
[member_id, effective_site_id],
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT t.*, s.site_name
|
||||
FROM biz.coach_task_transfer_log t
|
||||
LEFT JOIN biz.sites s ON s.site_id = t.site_id
|
||||
WHERE t.member_id = %s
|
||||
ORDER BY t.created_at DESC
|
||||
""",
|
||||
[member_id],
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
return [TransferLogItem(**row) for row in rows]
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.exception("查询客户转移历史失败: member_id=%s", member_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"查询客户转移历史失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 2. 待审核任务
|
||||
# =====================================================================
|
||||
|
||||
@router.get("/pending-review", response_model=PendingReviewPage)
|
||||
async def list_pending_reviews(
|
||||
site_id: int | None = Query(None, description="门店 ID"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> PendingReviewPage:
|
||||
"""待审核任务列表。"""
|
||||
effective_site_id = _filter_site_id(user, site_id)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
conditions = ["ct.status = 'pending_review'"]
|
||||
params: list = []
|
||||
|
||||
if effective_site_id is not None:
|
||||
conditions.append("ct.site_id = %s")
|
||||
params.append(effective_site_id)
|
||||
|
||||
where_clause = " AND ".join(conditions)
|
||||
|
||||
# 总数
|
||||
cur.execute(
|
||||
f"SELECT count(*) AS cnt FROM biz.coach_tasks ct WHERE {where_clause}",
|
||||
params,
|
||||
)
|
||||
total = cur.fetchone()["cnt"]
|
||||
|
||||
# 分页数据
|
||||
offset = (page - 1) * page_size
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT ct.*, s.site_name
|
||||
FROM biz.coach_tasks ct
|
||||
LEFT JOIN biz.sites s ON s.site_id = ct.site_id
|
||||
WHERE {where_clause}
|
||||
ORDER BY ct.created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
params + [page_size, offset],
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
items = []
|
||||
for row in rows:
|
||||
row["task_type_label"] = TASK_TYPE_LABELS.get(row.get("task_type", ""), "")
|
||||
items.append(PendingReviewItem(**row))
|
||||
|
||||
return PendingReviewPage(items=items, total=total)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.exception("查询待审核任务失败")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"查询待审核任务失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/pending-review/{task_id}/reassign", response_model=ReassignResponse)
|
||||
async def reassign_task(
|
||||
task_id: int,
|
||||
body: ReassignRequest,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> ReassignResponse:
|
||||
"""重新分配待审核任务(仅超级管理员)。
|
||||
|
||||
逻辑:原任务 status → 'transferred',新建 active 任务,写 transfer_log。
|
||||
"""
|
||||
_require_super_admin(user)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
# 查询原任务
|
||||
cur.execute(
|
||||
"SELECT * FROM biz.coach_tasks WHERE id = %s FOR UPDATE",
|
||||
[task_id],
|
||||
)
|
||||
task = cur.fetchone()
|
||||
if task is None:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
if task["status"] != "pending_review":
|
||||
raise HTTPException(status_code=400, detail="任务状态不是待审核,无法重新分配")
|
||||
|
||||
# 原任务标记为 transferred
|
||||
cur.execute(
|
||||
"UPDATE biz.coach_tasks SET status = 'transferred', updated_at = now() WHERE id = %s",
|
||||
[task_id],
|
||||
)
|
||||
|
||||
# 新建 active 任务
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.coach_tasks
|
||||
(site_id, member_id, assistant_id, task_type, priority_score, status, created_at, updated_at)
|
||||
VALUES (%s, %s, %s, %s, %s, 'active', now(), now())
|
||||
RETURNING id
|
||||
""",
|
||||
[task["site_id"], task["member_id"], body.to_assistant_id,
|
||||
task["task_type"], task.get("priority_score")],
|
||||
)
|
||||
new_task_id = cur.fetchone()["id"]
|
||||
|
||||
# 写转移日志
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.coach_task_transfer_log
|
||||
(site_id, member_id, from_assistant_id, to_assistant_id,
|
||||
transfer_reason, transfer_score, created_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, now())
|
||||
""",
|
||||
[task["site_id"], task["member_id"], task["assistant_id"],
|
||||
body.to_assistant_id, "manual_reassign", task.get("priority_score")],
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return ReassignResponse(success=True, new_task_id=new_task_id)
|
||||
except HTTPException:
|
||||
conn.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
conn.rollback()
|
||||
logger.exception("重新分配任务失败: task_id=%s", task_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"重新分配任务失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/pending-review/{task_id}/close", response_model=CloseResponse)
|
||||
async def close_task(
|
||||
task_id: int,
|
||||
body: CloseRequest,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> CloseResponse:
|
||||
"""关闭待审核任务(仅超级管理员)。
|
||||
|
||||
逻辑:任务 status → 'inactive',记录 abandon_reason。
|
||||
"""
|
||||
_require_super_admin(user)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"SELECT id, status FROM biz.coach_tasks WHERE id = %s FOR UPDATE",
|
||||
[task_id],
|
||||
)
|
||||
task = cur.fetchone()
|
||||
if task is None:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
if task["status"] != "pending_review":
|
||||
raise HTTPException(status_code=400, detail="任务状态不是待审核,无法关闭")
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE biz.coach_tasks
|
||||
SET status = 'inactive', abandon_reason = %s, updated_at = now()
|
||||
WHERE id = %s
|
||||
""",
|
||||
[body.reason, task_id],
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return CloseResponse(success=True)
|
||||
except HTTPException:
|
||||
conn.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
conn.rollback()
|
||||
logger.exception("关闭任务失败: task_id=%s", task_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"关闭任务失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 3. 参数管理
|
||||
# =====================================================================
|
||||
|
||||
# 权重参数 key 列表(联合校验用)
|
||||
_WEIGHT_KEYS = {"w_rs", "w_ms", "w_ml"}
|
||||
|
||||
|
||||
@router.get("/config", response_model=ConfigParamList)
|
||||
async def list_config_params(
|
||||
site_id: int | None = Query(None, description="门店 ID(不传则返回全部)"),
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> ConfigParamList:
|
||||
"""参数列表。"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
conditions: list[str] = []
|
||||
params: list = []
|
||||
|
||||
if site_id is not None:
|
||||
# 返回指定门店覆盖 + 全局默认
|
||||
conditions.append("(p.site_id = %s OR p.site_id IS NULL)")
|
||||
params.append(site_id)
|
||||
|
||||
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
||||
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT p.*, s.site_name
|
||||
FROM biz.cfg_task_generator_params p
|
||||
LEFT JOIN biz.sites s ON s.site_id = p.site_id
|
||||
WHERE {where_clause}
|
||||
ORDER BY p.site_id NULLS FIRST, p.param_key
|
||||
""",
|
||||
params,
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
return ConfigParamList(params=[ConfigParam(**row) for row in rows])
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.exception("查询参数配置失败")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"查询参数配置失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.put("/config/{param_id}", response_model=ConfigParamResponse)
|
||||
async def update_config_param(
|
||||
param_id: int,
|
||||
body: ConfigParamUpdate,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> ConfigParamResponse:
|
||||
"""更新参数值(仅超级管理员)。
|
||||
|
||||
权重参数(w_rs / w_ms / w_ml)更新后会校验三者之和是否为 1.0。
|
||||
"""
|
||||
_require_super_admin(user)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
# 查询当前参数
|
||||
cur.execute(
|
||||
"SELECT * FROM biz.cfg_task_generator_params WHERE id = %s FOR UPDATE",
|
||||
[param_id],
|
||||
)
|
||||
param = cur.fetchone()
|
||||
if param is None:
|
||||
raise HTTPException(status_code=404, detail="参数不存在")
|
||||
|
||||
# 更新
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE biz.cfg_task_generator_params
|
||||
SET param_value = %s, updated_at = now()
|
||||
WHERE id = %s
|
||||
""",
|
||||
[body.param_value, param_id],
|
||||
)
|
||||
|
||||
# 权重参数联合校验:w_rs + w_ms + w_ml = 1.0
|
||||
if param["param_key"] in _WEIGHT_KEYS:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT param_key, param_value
|
||||
FROM biz.cfg_task_generator_params
|
||||
WHERE site_id IS NOT DISTINCT FROM %s
|
||||
AND param_key = ANY(%s)
|
||||
""",
|
||||
[param["site_id"], list(_WEIGHT_KEYS)],
|
||||
)
|
||||
weight_rows = cur.fetchall()
|
||||
weight_sum = sum(r["param_value"] for r in weight_rows)
|
||||
if abs(weight_sum - 1.0) > 0.001:
|
||||
conn.rollback()
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"权重参数之和必须为 1.0,当前为 {weight_sum:.4f}",
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return ConfigParamResponse(success=True, id=param_id)
|
||||
except HTTPException:
|
||||
conn.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
conn.rollback()
|
||||
logger.exception("更新参数失败: param_id=%s", param_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"更新参数失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/config", response_model=ConfigParamResponse)
|
||||
async def create_config_param(
|
||||
body: ConfigParamCreate,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> ConfigParamResponse:
|
||||
"""新增门店覆盖参数(仅超级管理员)。"""
|
||||
_require_super_admin(user)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
# 检查是否已存在同 site_id + param_key 的记录
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id FROM biz.cfg_task_generator_params
|
||||
WHERE site_id = %s AND param_key = %s
|
||||
""",
|
||||
[body.site_id, body.param_key],
|
||||
)
|
||||
if cur.fetchone() is not None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"门店 {body.site_id} 已存在参数 {body.param_key} 的覆盖配置",
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.cfg_task_generator_params
|
||||
(site_id, param_key, param_value, updated_at)
|
||||
VALUES (%s, %s, %s, now())
|
||||
RETURNING id
|
||||
""",
|
||||
[body.site_id, body.param_key, body.param_value],
|
||||
)
|
||||
new_id = cur.fetchone()["id"]
|
||||
|
||||
conn.commit()
|
||||
return ConfigParamResponse(success=True, id=new_id)
|
||||
except HTTPException:
|
||||
conn.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
conn.rollback()
|
||||
logger.exception("新增参数失败")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"新增参数失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.delete("/clear-all-tasks")
|
||||
async def clear_all_tasks(
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""【测试用】清空所有 coach_tasks 及关联数据(仅超级管理员)。
|
||||
|
||||
用于开发/测试阶段重置任务数据,让 task_generator 重新生成。
|
||||
按外键依赖顺序删除:transfer_log → notes → history → tasks。
|
||||
"""
|
||||
_require_super_admin(user)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# 按外键依赖顺序:先删引用表,再删主表
|
||||
cur.execute("DELETE FROM biz.coach_task_transfer_log")
|
||||
transfer_count = cur.rowcount
|
||||
cur.execute("DELETE FROM biz.notes WHERE task_id IS NOT NULL")
|
||||
notes_count = cur.rowcount
|
||||
cur.execute("DELETE FROM biz.coach_task_history")
|
||||
history_count = cur.rowcount
|
||||
# coach_tasks 有自引用 FK,先清 parent_task_id 和 transferred_from
|
||||
cur.execute("UPDATE biz.coach_tasks SET parent_task_id = NULL, transferred_from = NULL")
|
||||
cur.execute("DELETE FROM biz.coach_tasks")
|
||||
task_count = cur.rowcount
|
||||
conn.commit()
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"已清空 {task_count} 条任务 + {history_count} 条历史 + {transfer_count} 条转移日志 + {notes_count} 条备注",
|
||||
"deleted_tasks": task_count,
|
||||
"deleted_history": history_count,
|
||||
"deleted_transfers": transfer_count,
|
||||
"deleted_notes": notes_count,
|
||||
}
|
||||
except Exception as exc:
|
||||
conn.rollback()
|
||||
logger.exception("清空任务失败")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"清空任务失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.delete("/config/{param_id}", response_model=ConfigParamResponse)
|
||||
async def delete_config_param(
|
||||
param_id: int,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
) -> ConfigParamResponse:
|
||||
"""删除门店覆盖参数(仅超级管理员)。
|
||||
|
||||
不允许删除 site_id IS NULL 的全局默认参数。
|
||||
"""
|
||||
_require_super_admin(user)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"SELECT id, site_id FROM biz.cfg_task_generator_params WHERE id = %s",
|
||||
[param_id],
|
||||
)
|
||||
param = cur.fetchone()
|
||||
if param is None:
|
||||
raise HTTPException(status_code=404, detail="参数不存在")
|
||||
if param["site_id"] is None:
|
||||
raise HTTPException(status_code=400, detail="不允许删除全局默认参数")
|
||||
|
||||
cur.execute(
|
||||
"DELETE FROM biz.cfg_task_generator_params WHERE id = %s",
|
||||
[param_id],
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return ConfigParamResponse(success=True, id=param_id)
|
||||
except HTTPException:
|
||||
conn.rollback()
|
||||
raise
|
||||
except Exception as exc:
|
||||
conn.rollback()
|
||||
logger.exception("删除参数失败: param_id=%s", param_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"删除参数失败: {str(exc)[:200]}",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user