fix(backend): 权限漏洞修复 + 业务逻辑审查修复

1. 宏观状态越权修复 (products.py + product_service.py):
   - update_overall_status 增加权限校验:仅 SUPER_ADMIN 或当前操作该产品主线任务的人可修改,否则 403

2. 任务撤回越权修复 (task_service.py + tasks.py):
   - recall_task 增加校验:操作人必须等于上游任务负责人(谁发出的谁撤回),否则 403
   - 管理员 (SUPER_ADMIN/SUPERVISOR) 直接放行

3. 驳回上游溯源修复 (task_service.py - reject_task):
   - 将 TaskLog (action_type=create) 提升为优先溯源方式,解决协助分支转交后驳回找不到正确发起人的 Bug
   - 保留 parent_task.assignee_id + task.assignee_id 两级兜底

4. complete_task 父节点继承修复:
   - 修复 task_type == 'MAIN' 永不匹配的 Bug(模型无此值)
   - 改为 not parent_task_id or task_type in (TRANSFER, RECOVERY)
This commit is contained in:
2026-08-11 15:05:30 +08:00
parent d40a8d480e
commit 88dc7381f6
4 changed files with 120 additions and 22 deletions

View File

@ -395,14 +395,50 @@ async def end_task(
# ============================================================
async def recall_task(
db: AsyncSession, task_id: uuid.UUID, operator_id: str | None = None
db: AsyncSession, task_id: uuid.UUID, operator_id: str | None = None,
operator_role: str | None = None,
) -> TaskResponse:
"""撤回 PENDING 转交:标记为 CANCELED以被撤回节点为父生成接力新任务给操作人。"""
"""撤回 PENDING 转交:标记为 CANCELED以被撤回节点为父生成接力新任务给操作人。
权限校验:
- 管理员SUPER_ADMIN / SUPERVISOR直接放行
- 操作人 必须等于 上游任务的负责人(谁发出的谁才能撤回),否则 403
"""
task = await _get_task_or_404(db, task_id)
if task.status != TASK_STATUS_PENDING:
raise HTTPException(status_code=409, detail="只有待接收(PENDING)的任务可以撤回")
# ── 权限校验:谁发出的谁才能撤回 ──
if not (operator_role and operator_role in ADMIN_ROLES):
# 查找上游任务的负责人(发出者)
upstream_assignee: str | None = None
if task.parent_task_id:
parent_result = await db.execute(
select(Task).where(Task.id == task.parent_task_id)
)
parent_task = parent_result.scalar_one_or_none()
if parent_task:
upstream_assignee = parent_task.assignee_id
else:
# 无父任务:从任务日志追溯创建人
log_result = await db.execute(
select(TaskLog).where(
TaskLog.task_id == task_id,
TaskLog.action_type == "create",
).order_by(TaskLog.created_at.asc()).limit(1)
)
create_log = log_result.scalar_one_or_none()
if create_log:
upstream_assignee = create_log.operator_id
# 如果找不到上游负责人,或者操作人不等于上游负责人 → 403
if not upstream_assignee or operator_id != upstream_assignee:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="您不是该任务的发起人,无法撤回",
)
now = get_beijing_time()
# 1. 废掉当前待接收任务
@ -608,8 +644,20 @@ async def reject_task(
# --- 2. 确定返工任务的负责人(追溯上一道工序的转交人) ---
rework_assignee_id: str | None = None
if task.parent_task_id:
# 有父任务:返工给父任务的负责人(即上一环的转交人 A
# 🚀 优先方式:从任务日志追溯创建人(准确记录是谁发起的转交
log_result = await db.execute(
select(TaskLog).where(
TaskLog.task_id == task_id,
TaskLog.action_type == "create",
).order_by(TaskLog.created_at.asc()).limit(1)
)
create_log = log_result.scalar_one_or_none()
if create_log and create_log.operator_id:
rework_assignee_id = create_log.operator_id
# 兜底方式1有父任务 → 返工给父任务的负责人
if not rework_assignee_id and task.parent_task_id:
parent_result = await db.execute(
select(Task).where(Task.id == task.parent_task_id)
)
@ -617,20 +665,8 @@ async def reject_task(
if parent_task:
rework_assignee_id = parent_task.assignee_id
# 兜底方式2用当前任务的负责人
if not rework_assignee_id:
# 无父任务(顶层转交):从任务日志追溯创建人(转交发起者 A
log_result = await db.execute(
select(TaskLog).where(
TaskLog.task_id == task_id,
TaskLog.action_type == "create",
).order_by(TaskLog.created_at.asc()).limit(1)
)
create_log = log_result.scalar_one_or_none()
if create_log and create_log.operator_id:
rework_assignee_id = create_log.operator_id
if not rework_assignee_id:
# 最后兜底:用当前任务的负责人(通常不应该走到这里)
rework_assignee_id = task.assignee_id
# --- 3. 创建返工任务 ---
@ -913,7 +949,12 @@ async def complete_task(
# 🚀 智能父节点继承算法
# 主线任务转交 → 保持平级继承(主分支永远在一维主干上)
# 协助分支转交 → 认当前任务为父(形成向外无限延伸的孙子节点树枝)
new_parent_id = task.parent_task_id if task.task_type == "MAIN" else task.id
# 注TASK_TYPE 实际值为 TRANSFER/RECOVERY/SPAWN不存在 "MAIN"
is_main_line = (
not task.parent_task_id
or task.task_type in ("TRANSFER", "RECOVERY")
)
new_parent_id = task.parent_task_id if is_main_line else task.id
next_task = Task(
product_id=task.product_id,