From b745328d49849df9800ec9c9963738237ea7faad Mon Sep 17 00:00:00 2001 From: Pine Date: Thu, 27 Aug 2026 20:19:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(tasks):=20=E8=BF=90=E8=90=A5=E7=AB=AF?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E8=AF=A6=E6=83=85/=E7=AB=9E=E6=A0=87/?= =?UTF-8?q?=E6=8A=A5=E5=90=8D=E7=AB=AF=E7=82=B9=20+=20=E5=9B=AD=E5=8C=BA?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E4=BB=BB=E5=8A=A1=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 运营端 /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=本园/发布即上架/默认公有) --- app/api/routers/rbac_operator.py | 53 +++++++++++++++++++++++++++++++ app/park/routers.py | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/app/api/routers/rbac_operator.py b/app/api/routers/rbac_operator.py index 0abbce1..74d1672 100644 --- a/app/api/routers/rbac_operator.py +++ b/app/api/routers/rbac_operator.py @@ -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, diff --git a/app/park/routers.py b/app/park/routers.py index 06e1bea..c0f170a 100644 --- a/app/park/routers.py +++ b/app/park/routers.py @@ -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,