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:
@@ -10,7 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..dependencies import get_db
|
||||
from ..schemas.opc import BidRequest, ProfileUpdate, FinanceRecordCreate
|
||||
from ..schemas.opc import BidRequest, ProfileUpdate, FinanceRecordCreate, TaskClaimRequest
|
||||
from ...rbac import require_roles, write_audit
|
||||
from ...infrastructure.repositories import Database, new_id, utcnow_iso
|
||||
from ...infrastructure.models import FinanceRecord
|
||||
@@ -132,6 +132,54 @@ async def opc_grab(
|
||||
return updated
|
||||
|
||||
|
||||
@router.post("/tasks/grab-by-code", summary="扫码/按短码接单")
|
||||
async def opc_grab_by_code(
|
||||
req: TaskClaimRequest,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
updated = await TaskService(db).claim(
|
||||
(req.task_code or "").strip(), actor, source="scan",
|
||||
)
|
||||
await write_audit(db, action="task.claim", resource="task",
|
||||
resource_id=updated["id"], detail=actor.get("username"),
|
||||
user=actor, request=request)
|
||||
return updated
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/doing", summary="开始做单")
|
||||
async def opc_task_doing(
|
||||
task_id: str,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
updated = await TaskService(db).start_doing(task_id, actor)
|
||||
await write_audit(db, action="task.doing", resource="task", resource_id=task_id,
|
||||
user=actor, request=request)
|
||||
return updated
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/complete", summary="完成做单")
|
||||
async def opc_task_complete(
|
||||
task_id: str,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
updated = await TaskService(db).complete(task_id, actor)
|
||||
await write_audit(db, action="task.complete", resource="task", resource_id=task_id,
|
||||
user=actor, request=request)
|
||||
return updated
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/bid", summary="OPC 投标")
|
||||
async def opc_bid(
|
||||
task_id: str,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -5,6 +5,11 @@ class BidRequest(BaseModel):
|
||||
plan: str = ""
|
||||
|
||||
|
||||
class TaskClaimRequest(BaseModel):
|
||||
task_code: str = ""
|
||||
task_id: str = ""
|
||||
|
||||
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
nickname: str | None = None
|
||||
|
||||
@@ -5,16 +5,33 @@ class TaskCreateRequest(BaseModel):
|
||||
category: str = ""
|
||||
sub_category: str = ""
|
||||
description: str = ""
|
||||
tags: str = ""
|
||||
mode: str = "grab"
|
||||
budget_min: int = 0
|
||||
budget_max: int = 0
|
||||
deadline: str = ""
|
||||
task_code: str = ""
|
||||
display_priority: int = 0
|
||||
publisher_name: str = ""
|
||||
|
||||
|
||||
class TaskUpdateRequest(BaseModel):
|
||||
title: str | None = None
|
||||
category: str | None = None
|
||||
sub_category: str | None = None
|
||||
description: str | None = None
|
||||
tags: str | None = None
|
||||
mode: str | None = None
|
||||
budget_min: int | None = None
|
||||
budget_max: int | None = None
|
||||
deadline: str | None = None
|
||||
task_code: str | None = None
|
||||
display_priority: int | None = None
|
||||
publisher_name: str | None = None
|
||||
|
||||
|
||||
class TaskStatusRequest(BaseModel):
|
||||
status: str # review/published/completed/cancelled
|
||||
status: str # review/published/completed/cancelled/claimed/doing
|
||||
|
||||
|
||||
|
||||
@@ -81,6 +98,22 @@ class CourseStatusRequest(BaseModel):
|
||||
status: str # draft/published/ended/offline
|
||||
|
||||
|
||||
class ActivityCreateRequest(BaseModel):
|
||||
title: str
|
||||
type: str | None = None # free/salon
|
||||
mode: str | None = None # online/offline
|
||||
status: str | None = None # open/invite/pending/done/ended
|
||||
subtitle: str | None = None
|
||||
desc: str | None = None
|
||||
location: str | None = None
|
||||
host: str | None = None
|
||||
image: str | None = None
|
||||
link: str | None = None
|
||||
start_at: str | None = None
|
||||
duration_min: int | None = None
|
||||
capacity: int | None = None
|
||||
|
||||
|
||||
class ActivityStatusRequest(BaseModel):
|
||||
status: str # open/invite/pending/done/ended
|
||||
|
||||
|
||||
Reference in New Issue
Block a user