task claim 表单承载:0058 迁移修复(TEXT 去 server_default) + register/grab 申请表单落库 + claims 返回 form

This commit is contained in:
Pine
2026-09-05 11:08:55 +08:00
parent 22674fbc63
commit 6d0385ef88
5 changed files with 71 additions and 15 deletions
+30
View File
@@ -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")
+20 -5
View File
@@ -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
+1
View File
@@ -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="")
+10 -3
View File
@@ -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)
+10 -7
View File
@@ -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: