feat(task): 任务中心扫码接单(多端) —— 服务端任务状态机/端口/迁移

- Task 加 task_code/tags/display_priority/claimed_by/claimed_at/doing_at;新增 TaskClaim 流水表。
- TaskService 加 claimed/doing/completed 状态机(claim/start_doing/complete),grab 并入 claim。
- TaskRepository 加 list_published/get_by_code/update/claim/set_doing + TaskClaimRepository;挂 Database。
- 端口:
  · opc  /tasks/grab-by-code、/tasks/{id}/doing、/tasks/{id}/complete
  · operator POST /tasks(auto task_code) + PATCH /tasks/:id
  · park  /park/api/tasks(大屏展示,含 scan_payload 二维码载荷)
  · training /api/tasks/claim-by-code、/api/tasks/my(小程序 C 端账号→OPC 身份 find-or-create 领单)
- 迁移 0008(tasks 加列 + task_claims)已应用+stamp;seed 补 task_code/tags/grab demo。
- tests/test_task_claim.py(自包含内存库, 状态机+流水+list_published), 直接 async 校验通过。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-25 18:40:35 +08:00
parent b8131aec35
commit b9b5e2e2ff
14 changed files with 818 additions and 28 deletions
+66 -2
View File
@@ -11,7 +11,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response
from pydantic import BaseModel
from ..dependencies import get_db
from ..schemas.operator import TaskCreateRequest, TaskStatusRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, ActivityStatusRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest
from ..schemas.operator import TaskCreateRequest, TaskUpdateRequest, TaskStatusRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, ActivityCreateRequest, ActivityStatusRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest
from ...rbac import require_permission, require_roles, write_audit
from ...infrastructure.repositories import Database
from ...services import compute_client
@@ -36,12 +36,41 @@ async def create_task(
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:task.manage")),
):
task = await db.tasks.create(req.model_dump(exclude_none=True))
fields = req.model_dump(exclude_none=True)
if not fields.get("task_code"):
fields["task_code"] = _gen_task_code()
task = await db.tasks.create(fields)
await write_audit(db, action="task.create", resource="task", resource_id=task["id"],
detail=task["title"], user=actor, request=request)
return task
@router.patch("/tasks/{task_id}", summary="更新任务(增改)")
async def update_task(
task_id: str,
req: TaskUpdateRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:task.manage")),
):
if await db.tasks.get(task_id) is None:
raise HTTPException(status_code=404, detail="Task not found")
task = await db.tasks.update(task_id, req.model_dump(exclude_none=True))
await write_audit(db, action="task.update", resource="task", resource_id=task_id,
detail=(req.title or ""), user=actor, request=request)
return task
def _gen_task_code() -> str:
"""生成便于扫码展示的短码:TK-YYYYMMDD-XXXX(基于时间戳短采样)。"""
import time as _t
from datetime import datetime as _dt
stamp = _dt.now().strftime("%Y%m%d")
rand = f"{int(_t.time()) % 1000000:06d}"
return f"TK-{stamp}-{rand}"
@router.post("/tasks/{task_id}/status", summary="更新任务状态")
async def set_task_status(
task_id: str,
@@ -326,6 +355,19 @@ async def list_activities(
return training.list_activities(status=status)
@router.post("/activities", summary="创建活动")
async def create_activity(
req: ActivityCreateRequest,
request: Request,
db: Database = Depends(get_db),
actor: dict = Depends(require_permission("action:activity.manage")),
):
activity = training.create_activity(req.model_dump(exclude_none=True))
await write_audit(db, action="activity.create", resource="activity", resource_id=activity["id"],
detail=activity["title"], user=actor, request=request)
return activity
@router.post("/activities/{event_id}/status", summary="更新活动状态")
async def set_activity_status(
event_id: str,
@@ -400,3 +442,25 @@ async def set_test_status(
if item is None:
raise HTTPException(status_code=404, detail="Test not found")
return item
# ── 培训业务 · 调研 / 政策 / 流程日志(落库结果只读)─────────────────────────
@router.get("/surveys", summary="调研结果列表")
async def list_surveys(
_u: dict = Depends(require_roles("operator")),
):
return training.list_surveys()
@router.get("/policies", summary="政策测评结果列表")
async def list_policies(
_u: dict = Depends(require_roles("operator")),
):
return training.list_policies()
@router.get("/plans", summary="流程启动日志列表")
async def list_plans(
_u: dict = Depends(require_roles("operator")),
):
return training.list_plans()