diff --git a/alembic/versions/0061_task_eligibility.py b/alembic/versions/0061_task_eligibility.py new file mode 100644 index 0000000..bb8bcee --- /dev/null +++ b/alembic/versions/0061_task_eligibility.py @@ -0,0 +1,26 @@ +"""0061: 任务资格要求扩展:最低信用分 / 要求勋章 / 要求认证 + +发布方可在发布任务时指定: +- min_credit_score:接单方最低信用分(0=不限制) +- required_badges:要求持有徽章 code 列表(JSON) +- required_certifications:要求完成认证 code 列表(JSON,如 opc_talent/business_reg/corporate_account 等) +""" +from alembic import op +import sqlalchemy as sa + +revision = "0061_task_eligibility" +down_revision = "0060_task_sign" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("tasks", sa.Column("min_credit_score", sa.Integer(), nullable=True, server_default="0")) + op.add_column("tasks", sa.Column("required_badges", sa.Text(), nullable=True)) + op.add_column("tasks", sa.Column("required_certifications", sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("tasks", "required_certifications") + op.drop_column("tasks", "required_badges") + op.drop_column("tasks", "min_credit_score") diff --git a/app/api/routers/rbac_hall.py b/app/api/routers/rbac_hall.py index b06007a..681c2bd 100644 --- a/app/api/routers/rbac_hall.py +++ b/app/api/routers/rbac_hall.py @@ -250,6 +250,9 @@ async def hall_publish_task( "escrow_ratio": int(body.get("escrowRatio") or body.get("escrow_ratio") or 100), "invoice_req": json.dumps(body.get("invoiceReq") or {}, ensure_ascii=False), "milestone_json": json.dumps(body.get("milestones") or [], ensure_ascii=False), + "min_credit_score": int(body.get("minCreditScore") or body.get("min_credit_score") or 0), + "required_badges": json.dumps(body.get("requiredBadges") or body.get("required_badges") or [], ensure_ascii=False), + "required_certifications": json.dumps(body.get("requiredCertifications") or body.get("required_certifications") or [], ensure_ascii=False), "status": status, "published_at": utcnow_iso() if status == "published" else "", "publisher_id": user.get("id"), diff --git a/app/infrastructure/models.py b/app/infrastructure/models.py index 58ead18..c54906c 100644 --- a/app/infrastructure/models.py +++ b/app/infrastructure/models.py @@ -288,6 +288,9 @@ class Task(Base): win_quota: Mapped[int] = mapped_column(Integer, default=0) # 竞标中标人数(默认1) register_quota: Mapped[int] = mapped_column(Integer, default=0) # 报名模式人数上限(0=不限)(0035) eligibility: Mapped[str] = mapped_column(Text, default="{}") # 接单资格条件(JSON 集) + min_credit_score: Mapped[int] = mapped_column(Integer, default=0) # 最低信用分要求(0=不限制) + required_badges: Mapped[str] = mapped_column(Text, default="[]") # 要求徽章 code 列表(JSON) + required_certifications: Mapped[str] = mapped_column(Text, default="[]") # 要求认证 code 列表(JSON) settle_mode: Mapped[str] = mapped_column(String, default="operator_escrow") # 资金托管方式 # ── 任务系统 v2 扩展字段(0056)── resource_support_json: Mapped[str] = mapped_column(Text, default="{}") # 提供的支持(算力/数据/场地/工具/对接/资金) diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index 347eeed..f0ca0a1 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -1321,6 +1321,9 @@ class TaskRepository: "bid_quota": t.bid_quota, "win_quota": t.win_quota, "register_quota": t.register_quota, "eligibility": t.eligibility, "settle_mode": t.settle_mode, + "min_credit_score": t.min_credit_score, + "required_badges": t.required_badges, + "required_certifications": t.required_certifications, "created_at": t.created_at, "updated_at": t.updated_at, } @@ -1381,6 +1384,9 @@ class TaskRepository: win_quota=fields.get("win_quota", 0), register_quota=fields.get("register_quota", 0), eligibility=fields.get("eligibility", "{}"), + min_credit_score=fields.get("min_credit_score", 0), + required_badges=fields.get("required_badges", "[]"), + required_certifications=fields.get("required_certifications", "[]"), settle_mode=fields.get("settle_mode", "operator_escrow"), resource_support_json=fields.get("resource_support_json", "{}"), raw_materials_json=fields.get("raw_materials_json", "[]"), @@ -1409,7 +1415,8 @@ class TaskRepository: "publisher_role", "visibility", "c_visible", "park_public", "assign_type", "assign_park_id", "assign_opc_id", "park_id", "park_released", "bid_quota", "win_quota", "register_quota", - "eligibility", "settle_mode", + "eligibility", "min_credit_score", "required_badges", "required_certifications", + "settle_mode", "resource_support_json", "raw_materials_json", "deliver_way", "deliver_items", "accept_standard", "price_type", "pay_type", "escrow_ratio", "invoice_req", "milestone_json", @@ -1597,6 +1604,9 @@ class TaskRepository: "bid_quota": t.bid_quota, "win_quota": t.win_quota, "register_quota": t.register_quota, "eligibility": t.eligibility, "settle_mode": t.settle_mode, + "min_credit_score": t.min_credit_score, + "required_badges": t.required_badges, + "required_certifications": t.required_certifications, "resource_support_json": t.resource_support_json, "raw_materials_json": t.raw_materials_json, "deliver_way": t.deliver_way, "deliver_items": t.deliver_items, diff --git a/app/services/task_service.py b/app/services/task_service.py index 3aaf666..0745030 100644 --- a/app/services/task_service.py +++ b/app/services/task_service.py @@ -98,10 +98,28 @@ class TaskService: return {} full = (await self.db.users.get_by_id(uid)) or {} credit = await self.db.credit_scores.get(uid) + # 勋章(active) + badges: list[str] = [] + try: + for ub in await self.db.user_badges.list_active(uid): + if ub.get("badge_code"): + badges.append(ub["badge_code"]) + except Exception: # noqa: BLE001 + badges = [] + # 认证(approved/active) + certs: list[str] = [] + try: + for c in await self.db.certifications.list_by_user(uid): + if c.get("status") in ("approved", "active") and c.get("cert_type"): + certs.append(c["cert_type"]) + except Exception: # noqa: BLE001 + certs = [] return { **full, "credit_level": (credit or {}).get("level", "L1"), "credit_score": (credit or {}).get("total_score", 0), + "badges": badges, + "certifications": certs, } async def can_accept(self, task: dict | None, actor: dict | None) -> dict: @@ -169,6 +187,29 @@ class TaskService: if cond.get("deposit") and int(cond["deposit"] or 0) > 0: reasons.append("需缴纳接单保证金") + # ── 独立资格字段(0061):最低信用分 / 要求勋章 / 要求认证 ── + min_score = int(task.get("min_credit_score") or 0) + if min_score > 0 and int(full.get("credit_score") or 0) < min_score: + reasons.append(f"信用分需 ≥ {min_score}(当前 {full.get('credit_score', 0)})") + try: + req_badges = json.loads(task.get("required_badges") or "[]") + except Exception: # noqa: BLE001 + req_badges = [] + if isinstance(req_badges, list) and req_badges: + have_badges = set(full.get("badges") or []) + missing_b = [b for b in req_badges if b not in have_badges] + if missing_b: + reasons.append(f"需持有徽章:{'、'.join(missing_b)}") + try: + req_certs = json.loads(task.get("required_certifications") or "[]") + except Exception: # noqa: BLE001 + req_certs = [] + if isinstance(req_certs, list) and req_certs: + have_certs = set(full.get("certifications") or []) + missing_c = [c for c in req_certs if c not in have_certs] + if missing_c: + reasons.append(f"需完成认证:{'、'.join(missing_c)}") + # 软性提示(不阻断) if cond.get("price_range") and isinstance(cond["price_range"], dict): pr = cond["price_range"]