feat(task): 完善任务系统 —— 分类字典/字段/四种接单方式/独占限额/交付时限
- Task 加 published_at/delivery_days/headcount/exclusive/publisher_id/category_id;新增 TaskCategory 分类字典表(35 类)。
- mode 归一 grab/bid/assign/recommend(旧 designated→assign、dispatch→recommend);TaskService 加 assign/recommend/select_recommend,claim 支持独占/限额、published+claimed 均可抢(多人)。
- TaskClaim 支持 source=assign/recommend、status=assigned/recommended/withdrawn + count_active_by_task;TaskCategoryRepository;Database 装配 task_categories。
- 端口:operator GET /admin/task-categories + POST tasks/{id}/assign|recommend|select;opc 同;park /api/tasks 返回 mode/deadline/delivery_days/headcount/exclusive/publisher_name。
- 迁移 0009(tasks 加列+task_categories)已应用+stamp;seed 35 分类字典 + task 新字段(含独占/推荐演示) + 补种已初始化库。
- 测试 test_task_system.py(独占/限额/指派/推荐/list_published/publish/category)+补齐 test_task_claim 语义, 15 passed。
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, TaskClaimRequest
|
||||
from ..schemas.opc import BidRequest, ProfileUpdate, FinanceRecordCreate, TaskClaimRequest, TaskAssignRequest, TaskRecommendRequest, TaskSelectRecommendRequest
|
||||
from ...rbac import require_roles, write_audit
|
||||
from ...infrastructure.repositories import Database, new_id, utcnow_iso
|
||||
from ...infrastructure.models import FinanceRecord
|
||||
@@ -180,6 +180,54 @@ async def opc_task_complete(
|
||||
return updated
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/assign", summary="指派任务给某人")
|
||||
async def opc_task_assign(
|
||||
task_id: str,
|
||||
req: TaskAssignRequest,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
updated = await TaskService(db).assign(task_id, req.taker_user_id, actor)
|
||||
await write_audit(db, action="task.assign", resource="task", resource_id=task_id,
|
||||
detail=req.taker_user_id, user=actor, request=request)
|
||||
return updated
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/recommend", summary="推荐候选人")
|
||||
async def opc_task_recommend(
|
||||
task_id: str,
|
||||
req: TaskRecommendRequest,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
records = await TaskService(db).recommend(task_id, req.candidates, actor)
|
||||
await write_audit(db, action="task.recommend", resource="task", resource_id=task_id,
|
||||
detail=",".join(req.candidates), user=actor, request=request)
|
||||
return {"items": records}
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/select", summary="选定推荐人")
|
||||
async def opc_task_select_recommend(
|
||||
task_id: str,
|
||||
req: TaskSelectRecommendRequest,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
updated = await TaskService(db).select_recommend(task_id, req.taker_user_id, actor)
|
||||
await write_audit(db, action="task.select", resource="task", resource_id=task_id,
|
||||
detail=req.taker_user_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, TaskUpdateRequest, TaskStatusRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, ActivityCreateRequest, ActivityStatusRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest
|
||||
from ..schemas.operator import TaskCreateRequest, TaskUpdateRequest, TaskStatusRequest, TaskAssignRequest, TaskRecommendRequest, TaskSelectRecommendRequest, 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
|
||||
@@ -37,6 +37,7 @@ async def create_task(
|
||||
actor: dict = Depends(require_permission("action:task.manage")),
|
||||
):
|
||||
fields = req.model_dump(exclude_none=True)
|
||||
fields["mode"] = _normalize_mode(fields.get("mode", "grab"))
|
||||
if not fields.get("task_code"):
|
||||
fields["task_code"] = _gen_task_code()
|
||||
task = await db.tasks.create(fields)
|
||||
@@ -45,6 +46,13 @@ async def create_task(
|
||||
return task
|
||||
|
||||
|
||||
def _normalize_mode(mode: str) -> str:
|
||||
"""接单方式归一:grab/bid/assign/recommend;旧值 designated→assign、dispatch→recommend。"""
|
||||
mapping = {"designated": "assign", "dispatch": "recommend"}
|
||||
m = mapping.get(mode, mode)
|
||||
return m if m in ("grab", "bid", "assign", "recommend") else "grab"
|
||||
|
||||
|
||||
@router.patch("/tasks/{task_id}", summary="更新任务(增改)")
|
||||
async def update_task(
|
||||
task_id: str,
|
||||
@@ -61,6 +69,62 @@ async def update_task(
|
||||
return task
|
||||
|
||||
|
||||
@router.get("/task-categories", summary="任务分类字典")
|
||||
async def list_task_categories(
|
||||
db: Database = Depends(get_db),
|
||||
_u: dict = Depends(require_roles("operator")),
|
||||
):
|
||||
return await db.task_categories.list()
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/assign", summary="指派任务给某人")
|
||||
async def assign_task(
|
||||
task_id: str,
|
||||
req: TaskAssignRequest,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_permission("action:task.manage")),
|
||||
):
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
updated = await TaskService(db).assign(task_id, req.taker_user_id, actor)
|
||||
await write_audit(db, action="task.assign", resource="task", resource_id=task_id,
|
||||
detail=req.taker_user_id, user=actor, request=request)
|
||||
return updated
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/recommend", summary="推荐候选人")
|
||||
async def recommend_task(
|
||||
task_id: str,
|
||||
req: TaskRecommendRequest,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_permission("action:task.manage")),
|
||||
):
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
records = await TaskService(db).recommend(task_id, req.candidates, actor)
|
||||
await write_audit(db, action="task.recommend", resource="task", resource_id=task_id,
|
||||
detail=",".join(req.candidates), user=actor, request=request)
|
||||
return {"items": records}
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/select", summary="选定推荐人")
|
||||
async def select_recommend_task(
|
||||
task_id: str,
|
||||
req: TaskSelectRecommendRequest,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_permission("action:task.manage")),
|
||||
):
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
updated = await TaskService(db).select_recommend(task_id, req.taker_user_id, actor)
|
||||
await write_audit(db, action="task.select", resource="task", resource_id=task_id,
|
||||
detail=req.taker_user_id, user=actor, request=request)
|
||||
return updated
|
||||
|
||||
|
||||
def _gen_task_code() -> str:
|
||||
"""生成便于扫码展示的短码:TK-YYYYMMDD-XXXX(基于时间戳短采样)。"""
|
||||
import time as _t
|
||||
@@ -81,7 +145,8 @@ async def set_task_status(
|
||||
):
|
||||
if await db.tasks.get(task_id) is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
task = await db.tasks.set_status(task_id, req.status)
|
||||
# 发布时记录发布时间
|
||||
task = (await db.tasks.publish(task_id)) if req.status == "published" else await db.tasks.set_status(task_id, req.status)
|
||||
await write_audit(db, action="task.status", resource="task", resource_id=task_id,
|
||||
detail=req.status, user=actor, request=request)
|
||||
return task
|
||||
|
||||
@@ -10,6 +10,18 @@ class TaskClaimRequest(BaseModel):
|
||||
task_id: str = ""
|
||||
|
||||
|
||||
class TaskAssignRequest(BaseModel):
|
||||
taker_user_id: str
|
||||
|
||||
|
||||
class TaskRecommendRequest(BaseModel):
|
||||
candidates: list[str] = []
|
||||
|
||||
|
||||
class TaskSelectRecommendRequest(BaseModel):
|
||||
taker_user_id: str = ""
|
||||
|
||||
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
nickname: str | None = None
|
||||
|
||||
@@ -3,21 +3,27 @@ from pydantic import BaseModel
|
||||
class TaskCreateRequest(BaseModel):
|
||||
title: str
|
||||
category: str = ""
|
||||
category_id: str | None = None
|
||||
sub_category: str = ""
|
||||
description: str = ""
|
||||
tags: str = ""
|
||||
mode: str = "grab"
|
||||
mode: str = "grab" # grab/bid/assign/recommend
|
||||
budget_min: int = 0
|
||||
budget_max: int = 0
|
||||
deadline: str = ""
|
||||
delivery_days: int = 0
|
||||
headcount: int = 0
|
||||
exclusive: bool = False
|
||||
task_code: str = ""
|
||||
display_priority: int = 0
|
||||
publisher_id: str | None = None
|
||||
publisher_name: str = ""
|
||||
|
||||
|
||||
class TaskUpdateRequest(BaseModel):
|
||||
title: str | None = None
|
||||
category: str | None = None
|
||||
category_id: str | None = None
|
||||
sub_category: str | None = None
|
||||
description: str | None = None
|
||||
tags: str | None = None
|
||||
@@ -25,8 +31,12 @@ class TaskUpdateRequest(BaseModel):
|
||||
budget_min: int | None = None
|
||||
budget_max: int | None = None
|
||||
deadline: str | None = None
|
||||
delivery_days: int | None = None
|
||||
headcount: int | None = None
|
||||
exclusive: bool | None = None
|
||||
task_code: str | None = None
|
||||
display_priority: int | None = None
|
||||
publisher_id: str | None = None
|
||||
publisher_name: str | None = None
|
||||
|
||||
|
||||
@@ -34,6 +44,26 @@ class TaskStatusRequest(BaseModel):
|
||||
status: str # review/published/completed/cancelled/claimed/doing
|
||||
|
||||
|
||||
class TaskAssignRequest(BaseModel):
|
||||
taker_user_id: str
|
||||
|
||||
|
||||
class TaskRecommendRequest(BaseModel):
|
||||
candidates: list[str] = []
|
||||
|
||||
|
||||
class TaskSelectRecommendRequest(BaseModel):
|
||||
taker_user_id: str
|
||||
|
||||
|
||||
class TaskCategoryItem(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
sort: int = 0
|
||||
modes: str = ""
|
||||
status: str = "active"
|
||||
|
||||
|
||||
|
||||
class ProviderCreateRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
Reference in New Issue
Block a user