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:
+47
-3
@@ -11,12 +11,16 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, File, Header, HTTPException, Request, UploadFile
|
||||
from fastapi import APIRouter, Depends, File, Header, HTTPException, Request, UploadFile
|
||||
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..infrastructure.db import get_session
|
||||
from ..infrastructure.repositories import TaskRepository
|
||||
from . import park_config, tenants
|
||||
from .auth import create_device_token, create_token, parse_device_token, parse_token, require_tenant
|
||||
from .config import settings
|
||||
@@ -447,6 +451,41 @@ async def dashboard_overview(authorization: str | None = Header(None), tenant_id
|
||||
return get_engine(tid).snapshot()
|
||||
|
||||
|
||||
@router.get("/api/tasks", summary="大屏任务展示(平台全局已发布任务)")
|
||||
async def park_tasks(
|
||||
authorization: str | None = Header(None),
|
||||
tenant_id: str | None = None,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
"""返回系统任务中心的上墙任务(published/claimed/doing),供大屏轮播卡片。
|
||||
|
||||
大屏卡片含任务 ID、task_code 与二维码(scan_payload)。任务为平台全局实体,
|
||||
不受园区隔离;此处按园区身份校验后返回。
|
||||
"""
|
||||
await _resolve_tenant(authorization, tenant_id)
|
||||
repo = TaskRepository(session)
|
||||
items = await repo.list_published()
|
||||
now_label = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
def payload(t: dict) -> dict:
|
||||
return {
|
||||
"id": t["id"],
|
||||
"task_code": t["task_code"] or f"TK-{now_label}-{t['id'][-5:]}",
|
||||
"scan_payload": t["task_code"] or f"TK-{now_label}-{t['id'][-5:]}",
|
||||
"title": t["title"],
|
||||
"category": t["category"],
|
||||
"summary": (t["description"] or "")[:160],
|
||||
"budget_min": t["budget_min"],
|
||||
"budget_max": t["budget_max"],
|
||||
"status": t["status"],
|
||||
"claimed_by": t["claimed_by"],
|
||||
"claimed_at": t["claimed_at"],
|
||||
"tags": [tag for tag in (t.get("tags") or "").split(",") if tag],
|
||||
}
|
||||
|
||||
return {"items": [payload(t) for t in items]}
|
||||
|
||||
|
||||
@router.get("/api/park/zones")
|
||||
async def park_zones(authorization: str | None = Header(None), tenant_id: str | None = None):
|
||||
tid = await _resolve_tenant(authorization, tenant_id)
|
||||
@@ -500,8 +539,10 @@ def _gen_code() -> str:
|
||||
|
||||
|
||||
def _publish_bind(device_id: str, payload: dict) -> None:
|
||||
"""未绑定大屏仅订阅 bind 频道;绑定成功后经此频道通知并下发 token。"""
|
||||
hub.publish(f"opc/display/bind/{device_id}", payload, qos=1)
|
||||
"""未绑定大屏仅订阅 bind 频道;绑定成功后经此频道通知并下发 token。
|
||||
retain=True:绑定/解绑状态持久,屏幕即使延迟连接,订阅时也会立即收到最新状态(避免
|
||||
绑定成功但屏幕 MQTT 尚未订阅导致丢失 bound 事件、无法自动进入)。"""
|
||||
hub.publish(f"opc/display/bind/{device_id}", payload, qos=1, retain=True)
|
||||
|
||||
|
||||
@router.post("/api/devices/register")
|
||||
@@ -545,6 +586,9 @@ async def bind_screen_by_code(tid: str, body: BindByCodeBody):
|
||||
async def device_unbind(device_id: str, authorization: str | None = Header(None), tenant_id: str | None = None):
|
||||
await _resolve_tenant(authorization, tenant_id)
|
||||
ok = await tenants.unbind_device(device_id)
|
||||
if ok:
|
||||
# 通知大屏退出到待绑定页(大屏订阅 bind/<device> 频道,收到 unbound 即清理本地并回绑定页)
|
||||
_publish_bind(device_id, {"event": "unbound"})
|
||||
return {"ok": ok, "device_id": device_id}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user