From 6d0385ef88c56af1a4bd0d34dd2cbf68d422b536 Mon Sep 17 00:00:00 2001 From: Pine Date: Sat, 5 Sep 2026 11:08:55 +0800 Subject: [PATCH] =?UTF-8?q?task=20claim=20=E8=A1=A8=E5=8D=95=E6=89=BF?= =?UTF-8?q?=E8=BD=BD=EF=BC=9A0058=20=E8=BF=81=E7=A7=BB=E4=BF=AE=E5=A4=8D(T?= =?UTF-8?q?EXT=20=E5=8E=BB=20server=5Fdefault)=20+=20register/grab=20?= =?UTF-8?q?=E7=94=B3=E8=AF=B7=E8=A1=A8=E5=8D=95=E8=90=BD=E5=BA=93=20+=20cl?= =?UTF-8?q?aims=20=E8=BF=94=E5=9B=9E=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- alembic/versions/0058_task_claim_form.py | 30 ++++++++++++++++++++++++ app/api/routers/rbac_hall.py | 25 ++++++++++++++++---- app/infrastructure/models.py | 1 + app/infrastructure/repositories.py | 13 +++++++--- app/services/task_service.py | 17 ++++++++------ 5 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 alembic/versions/0058_task_claim_form.py diff --git a/alembic/versions/0058_task_claim_form.py b/alembic/versions/0058_task_claim_form.py new file mode 100644 index 0000000..65d0961 --- /dev/null +++ b/alembic/versions/0058_task_claim_form.py @@ -0,0 +1,30 @@ +"""0058 task claim 表单 + +任务申请(报名/抢单)承载提交表单:intro/skills/quote/contact/remark/attachments。 +""" +import os +import sys +from datetime import datetime + +import sqlalchemy as sa + +# alembic 在 revision 目录执行时,项目根不在 sys.path;显式加入保证可 import models +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) + +from alembic import op # noqa: E402 + +revision = "0058_task_claim_form" +down_revision = "0057" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "task_claims", + sa.Column("form_json", sa.Text(), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("task_claims", "form_json") diff --git a/app/api/routers/rbac_hall.py b/app/api/routers/rbac_hall.py index e34a705..51ce15d 100644 --- a/app/api/routers/rbac_hall.py +++ b/app/api/routers/rbac_hall.py @@ -280,16 +280,27 @@ async def hall_publish_task( return {"ok": True, "id": task["id"], "status": status, "item": task} -@router.post("/tasks/{task_id}/register", summary="报名任务(register 模式;先到先得,达上限关闭)") +@router.post("/tasks/{task_id}/register", summary="报名任务(register 模式;先到先得,达上限关闭;可携带报名表单)") async def hall_register_task( task_id: str, + body: dict, request: Request, db: Database = Depends(get_db), actor: dict = Depends(require_cap("opc_member")), ): from ...services.task_service import TaskService - claim = await TaskService(db).register(task_id, actor) + # 报名表单:自我介绍/技能/期望报价/联系方式/备注/附件(可选) + form = { + "intro": str(body.get("intro") or "").strip(), + "skills": str(body.get("skills") or "").strip(), + "quote": int(body.get("quote") or 0), + "contact": str(body.get("contact") or "").strip(), + "remark": str(body.get("remark") or "").strip(), + "attachments": str(body.get("attachments") or "").strip(), + } + form = {k: v for k, v in form.items() if v not in ("", 0, [])} + claim = await TaskService(db).register(task_id, actor, form) # 实时通知:报名者确认 + 发布者收报名提醒 from ...services.notification_service import notify as _n task = await db.tasks.get(task_id) @@ -1068,13 +1079,17 @@ async def hall_task_cancel(task_id: str, request: Request, # ── 任务统一合并到大厅:OPC 接单侧操作(原 /opc/tasks/*,2026-09 增量合并) ── -@router.post("/tasks/{task_id}/grab", summary="抢单(grab 模式,先到先得)") -async def hall_task_grab(task_id: str, request: Request, +@router.post("/tasks/{task_id}/grab", summary="抢单(grab 模式,先到先得;可携带接单备注)") +async def hall_task_grab(task_id: str, body: dict, request: Request, db: Database = Depends(get_db), user: dict = Depends(require_cap("opc_member"))): from ...services.task_service import TaskService - updated = await TaskService(db).grab(task_id, user) + form = {} + remark = str(body.get("remark") or "").strip() + if remark: + form["remark"] = remark + updated = await TaskService(db).grab(task_id, user, form or None) # 信用事件:接单成功 +1 if updated and updated.get("status") in ("doing", "claimed", "accepted"): from ...services.credit_engine import on_credit_event diff --git a/app/infrastructure/models.py b/app/infrastructure/models.py index 509d218..9be6369 100644 --- a/app/infrastructure/models.py +++ b/app/infrastructure/models.py @@ -326,6 +326,7 @@ class TaskClaim(Base): claim_source: Mapped[str] = mapped_column(String, default="scan") # scan/grab/bid/assign/recommend claimed_at: Mapped[str] = mapped_column(String, default="") status: Mapped[str] = mapped_column(String, default="claimed") # claimed/doing/completed/withdrawn/assigned/recommended + form_json: Mapped[str] = mapped_column(Text, default="{}") # 申请表单(报名/抢单备注:intro/skills/quote/contact/remark/attachments) created_at: Mapped[str] = mapped_column(String, default="") updated_at: Mapped[str] = mapped_column(String, default="") diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index 3fd22c7..f60dc80 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -1584,20 +1584,27 @@ class TaskClaimRepository: self.session = session def _to_dict(self, c: TaskClaim) -> dict: + try: + form = json.loads(c.form_json or "{}") + except Exception: + form = {} return { "id": c.id, "task_id": c.task_id, "claimer_user_id": c.claimer_user_id, "claimer_name": c.claimer_name, "claim_source": c.claim_source, "claimed_at": c.claimed_at, - "status": c.status, "created_at": c.created_at, "updated_at": c.updated_at, + "status": c.status, "form": form, + "created_at": c.created_at, "updated_at": c.updated_at, } async def create(self, task_id: str, user_id: str, name: str, - source: str = "scan", status: str = "claimed") -> dict: + source: str = "scan", status: str = "claimed", + form: dict | None = None) -> dict: now = utcnow_iso() c = TaskClaim(id=new_id("claim"), task_id=task_id, claimer_user_id=user_id, claimer_name=name, claim_source=source, claimed_at=now, - status=status, created_at=now, updated_at=now) + status=status, created_at=now, updated_at=now, + form_json=json.dumps(form or {}, ensure_ascii=False)) self.session.add(c) await self.session.commit() return self._to_dict(c) diff --git a/app/services/task_service.py b/app/services/task_service.py index 63bb95d..7703744 100644 --- a/app/services/task_service.py +++ b/app/services/task_service.py @@ -184,19 +184,21 @@ class TaskService: # ── 统一匹配落单(claim 双写)──────────────────────────────── async def _match(self, task: dict, actor: dict, taker_id: str, taker_name: str, - source: str, claim_status: str = "claimed") -> dict: + source: str, claim_status: str = "claimed", + form: dict | None = None) -> dict: """匹配成功统一动作:任务 → claimed + TaskClaim 流水 + IM 同步 + 信用事件。""" updated = await self.db.tasks.claim(task["id"], taker_id) await self.db.task_claims.create( task["id"], taker_id, taker_name, source=source, status=claim_status, + form=form or {}, ) _sync_task_group(task["id"]) await _credit_event(self.db, taker_id, "task_match", "任务匹配成功", ref_type="task", ref_id=task["id"]) return updated - async def grab(self, task_id: str, actor: dict) -> dict: - return await self.claim_by_id(task_id, actor, source="grab") + async def grab(self, task_id: str, actor: dict, form: dict | None = None) -> dict: + return await self.claim_by_id(task_id, actor, source="grab", form=form) async def claim(self, task_code: str, actor: dict, source: str = "scan") -> dict: task = await self.db.tasks.get_by_code(task_code) @@ -204,7 +206,8 @@ class TaskService: raise HTTPException(status_code=404, detail="任务不存在") return await self.claim_by_id(task["id"], actor, source=source) - async def claim_by_id(self, task_id: str, actor: dict, source: str) -> dict: + async def claim_by_id(self, task_id: str, actor: dict, source: str, + form: dict | None = None) -> dict: task = await self.db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="任务不存在") @@ -225,7 +228,7 @@ class TaskService: raise HTTPException(status_code=403, detail="不满足接单条件:" + ";".join(acc["reasons"])) return await self._match(task, actor, actor["id"], actor.get("nickname") or actor.get("username", ""), - source) + source, form=form) async def assign(self, task_id: str, taker_user_id: str, actor: dict) -> dict: task = await self.db.tasks.get(task_id) @@ -274,7 +277,7 @@ class TaskService: return await self._match(task, actor, taker_user_id, taker_name, source="recommend", claim_status="assigned") - async def register(self, task_id: str, actor: dict) -> dict: + async def register(self, task_id: str, actor: dict, form: dict | None = None) -> dict: task = await self.db.tasks.get(task_id) if task is None or task["status"] != "published" or task["mode"] != "register": raise HTTPException(status_code=400, detail="任务不可报名") @@ -292,7 +295,7 @@ class TaskService: raise HTTPException(status_code=403, detail="不满足报名条件:" + ";".join(acc["reasons"])) return await self.db.task_claims.create( task_id, actor["id"], actor.get("nickname") or actor.get("username", ""), - source="register", status="registered", + source="register", status="registered", form=form or {}, ) async def register_select(self, task_id: str, claim_id: str, actor: dict) -> dict: