feat(tasks): 运营端任务详情/竞标/报名端点 + 园区发布任务端点

- 运营端 /admin/tasks/{id} 详情、/{id}/bids、/{id}/claims、/{id}/bids/{bid_id}/win 开标定标(余标置lost+进 in_progress)
- 园区端 POST /park/api/park/tasks 发布任务(publisher_role=park/park_id=本园/发布即上架/默认公有)
This commit is contained in:
Pine
2026-08-27 20:19:40 +08:00
parent 084f039d10
commit b745328d49
2 changed files with 107 additions and 0 deletions
+53
View File
@@ -75,6 +75,59 @@ async def operator_escrow_release(
return released
@router.get("/tasks/{task_id}", summary="任务详情")
async def get_task_detail(
task_id: str,
db: Database = Depends(get_db),
_u: dict = Depends(require_roles("operator")),
):
task = await db.tasks.get(task_id)
if task is None:
raise HTTPException(status_code=404, detail="任务不存在")
return task
@router.get("/tasks/{task_id}/bids", summary="任务竞标(报名/投标情况)")
async def get_task_bids(
task_id: str,
db: Database = Depends(get_db),
_u: dict = Depends(require_roles("operator")),
):
return {"items": await db.bids.list_for_task(task_id)}
@router.get("/tasks/{task_id}/claims", summary="任务报名/接单情况(抢单/指派/推荐)")
async def get_task_claims(
task_id: str,
db: Database = Depends(get_db),
_u: dict = Depends(require_roles("operator")),
):
return {"items": await db.task_claims.list_by_task(task_id)}
@router.post("/tasks/{task_id}/bids/{bid_id}/win", summary="开标/定标(选定中标 OPC")
async def operator_win_bid(
task_id: str,
bid_id: str,
request: Request,
db: Database = Depends(get_db),
_u: dict = Depends(require_roles("operator")),
):
from ...services.task_service import TaskService
bid = await db.bids.set_status(bid_id, "win")
if bid is None:
raise HTTPException(status_code=404, detail="竞标不存在")
# 其余标置为 lost
for b in await db.bids.list_for_task(task_id):
if b["id"] != bid_id and b.get("status") == "submitted":
await db.bids.set_status(b["id"], "lost")
await db.tasks.set_status(task_id, "in_progress")
await write_audit(db, action="bid.win", resource="bid", resource_id=bid_id,
detail=f"task={task_id} winner={bid.get('opc_name')}", user=_u, request=request)
return bid
@router.get("/tasks", summary="任务列表")
async def list_tasks(
status: str | None = None,
+54
View File
@@ -526,6 +526,54 @@ class ParkStatusBody(BaseModel):
status: str # published/cancelled
class ParkTaskCreateBody(BaseModel):
title: str
category: str = ""
sub_category: str = ""
description: str = ""
tags: str = ""
mode: str = "grab"
budget_min: int = 0
budget_max: int = 0
deadline: str = ""
delivery_days: int = 0
headcount: int = 0
exclusive: bool = False
visibility: str = "public"
c_visible: bool = True
park_public: bool = True # 园区发布默认公有(各园大屏可见/本园可接)
bid_quota: int = 0
win_quota: int = 0
eligibility: str = "{}"
settle_mode: str = "operator_escrow"
@router.post("/api/park/tasks", summary="园区发布任务")
async def park_task_create(
body: ParkTaskCreateBody,
authorization: str | None = Header(None),
tenant_id: str | None = None,
session: AsyncSession = Depends(get_session),
):
"""园区发布任务:publisher_role=park、park_id=本园,发布即上架(published)。"""
tid = await _resolve_tenant(authorization, tenant_id)
repo = TaskRepository(session)
t = await tenants.get_tenant(tid)
task = await repo.create({
"title": body.title, "category": body.category, "sub_category": body.sub_category,
"description": body.description, "tags": body.tags, "mode": body.mode,
"budget_min": body.budget_min, "budget_max": body.budget_max, "deadline": body.deadline,
"delivery_days": body.delivery_days, "headcount": body.headcount, "exclusive": body.exclusive,
"publisher_role": "park", "park_id": tid,
"publisher_name": (t or {}).get("name", "") or "园区",
"visibility": body.visibility, "c_visible": body.c_visible, "park_public": body.park_public,
"bid_quota": body.bid_quota, "win_quota": body.win_quota,
"eligibility": body.eligibility, "settle_mode": body.settle_mode,
"status": "published", "display_priority": 40,
})
return task
@router.get("/api/park/tasks", summary="园区端任务(本园发布 + 指派本园)")
async def park_task_list(
authorization: str | None = Header(None),
@@ -1149,6 +1197,8 @@ async def carrier_company_add_member(body: CompanyMemberBody, user: dict = Depen
r = await tenants.add_company_member(body.company_id, body.user_id)
if r is None:
raise HTTPException(status_code=404, detail="企业或用户不存在")
if not r.get("ok"):
raise HTTPException(status_code=400, detail=r.get("reason", "绑定失败"))
return r
@@ -1250,8 +1300,12 @@ async def park_user_create(body: ParkUserCreateBody, db=Depends(_carrier_db), us
uname = body.username.strip()
if not uname:
raise HTTPException(status_code=400, detail="账号必填")
if not (body.nickname or "").strip():
raise HTTPException(status_code=400, detail="姓名必填")
if await db.users.get_by_username(uname):
raise HTTPException(status_code=400, detail="账号已存在")
if body.phone and await db.users.find_by_phone(body.phone):
raise HTTPException(status_code=400, detail="手机号已被使用")
if not body.password:
raise HTTPException(status_code=400, detail="请设置初始密码")
u = await db.users.create(uname, body.password, nickname=body.nickname, gender=body.gender,