From 413bd2939005a7e4dae1365bddd56d8b52ff8c90 Mon Sep 17 00:00:00 2001 From: PineHomePC Date: Mon, 24 Aug 2026 01:54:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=9A=E5=8A=A1=E5=B1=82=20services?= =?UTF-8?q?=20=E6=8A=BD=E5=8F=96=EF=BC=88=E8=B4=A2=E5=8A=A1/=E7=BB=93?= =?UTF-8?q?=E7=AE=97/=E8=A1=A5=E8=B4=B4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FinanceService:opc_add_finance 直写 ORM 改为走 Repository.create(修复四层依赖违规) - SettlementService:结算佣金(5%)/电子合同幂等/争议联动/投资撮合评分 - SubsidyService:政务三级审批状态机(区县→市→省→发放) - rbac_ecosystem/rbac_government/rbac_opc 接线,路由瘦身 --- app/api/routers/rbac_ecosystem.py | 62 +++++++----------------- app/api/routers/rbac_government.py | 31 +++--------- app/api/routers/rbac_opc.py | 16 +++---- app/infrastructure/repositories.py | 10 ++++ app/services/finance_service.py | 25 ++++++++++ app/services/settlement_service.py | 75 ++++++++++++++++++++++++++++++ app/services/subsidy_service.py | 42 +++++++++++++++++ 7 files changed, 183 insertions(+), 78 deletions(-) create mode 100644 app/services/finance_service.py create mode 100644 app/services/settlement_service.py create mode 100644 app/services/subsidy_service.py diff --git a/app/api/routers/rbac_ecosystem.py b/app/api/routers/rbac_ecosystem.py index 8f74bce..808d6c4 100644 --- a/app/api/routers/rbac_ecosystem.py +++ b/app/api/routers/rbac_ecosystem.py @@ -91,13 +91,9 @@ async def sign_contract( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise", "opc_member")), ): - task = await db.tasks.get(task_id) - if task is None: - raise HTTPException(status_code=404, detail="Task not found") - if await db.contracts.get_for_task(task_id) is not None: - return await db.contracts.get_for_task(task_id) - opc_id = req.opc_id or (actor["id"] if actor.get("role") == "opc_member" else "") - contract = await db.contracts.create(task_id, task["title"], actor["id"], opc_id) + from ...services.settlement_service import SettlementService + + contract = await SettlementService(db).sign_contract(task_id, actor, req.opc_id) await write_audit(db, action="contract.sign", resource="contract", resource_id=contract["id"], user=actor, request=request) return contract @@ -111,18 +107,11 @@ async def release_escrow( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise")), ): - task = await db.tasks.get(task_id) - if task is None or task["status"] != "completed": - raise HTTPException(status_code=400, detail="任务未完成,不能结算") - escrow = await db.escrows.list() - esc = next((e for e in escrow if e["task_id"] == task_id), None) - if esc is None: - amount = max(task.get("budget_min", 0), task.get("budget_max", 0)) - commission = int(amount * 0.05) - esc = await db.escrows.create(task_id, task["title"], amount, commission) - released = await db.escrows.set_status(esc["id"], "released") - await write_audit(db, action="escrow.release", resource="escrow", resource_id=esc["id"], - detail=f"amount={esc['amount']}", user=actor, request=request) + from ...services.settlement_service import SettlementService + + released = await SettlementService(db).release_escrow(task_id) + await write_audit(db, action="escrow.release", resource="escrow", resource_id=released["id"], + detail=f"amount={released['amount']}", user=actor, request=request) return released @@ -148,11 +137,9 @@ async def create_dispute( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise", "opc_member")), ): - task = await db.tasks.get(task_id) - if task is None: - raise HTTPException(status_code=404, detail="Task not found") - dispute = await db.disputes.create(task_id, task["title"], actor["id"], req.reason) - await db.tasks.set_status(task_id, "disputed") + from ...services.settlement_service import SettlementService + + dispute = await SettlementService(db).create_dispute(task_id, actor, req.reason) await write_audit(db, action="dispute.open", resource="dispute", resource_id=dispute["id"], user=actor, request=request) return dispute @@ -165,9 +152,9 @@ async def resolve_dispute( db: Database = Depends(get_db), actor: dict = Depends(require_roles("operator")), ): - dispute = await db.disputes.set_status(dispute_id, "resolved", resolution="平台调解结案") - if dispute is None: - raise HTTPException(status_code=404, detail="Dispute not found") + from ...services.settlement_service import SettlementService + + dispute = await SettlementService(db).resolve_dispute(dispute_id) await write_audit(db, action="dispute.resolve", resource="dispute", resource_id=dispute_id, user=actor, request=request) return dispute @@ -179,24 +166,9 @@ async def investor_matches( db: Database = Depends(get_db), user: dict = Depends(require_roles("investor")), ): - pref = await db.investor_prefs.get(user["id"]) or {} - industries = set(pref.get("industries") or []) - data = await db.portal_pages.get("investor", "projects") or {"items": []} - items = data.get("items", []) - scored = [] - for it in items: - text = f"{it.get('title', '')} {it.get('meta', '')}" - score = 0 - for ind in industries: - if ind and ind in text: - score += 10 - stage = pref.get("stage") - if stage and stage in text: - score += 5 - scored.append((score, it)) - scored.sort(key=lambda x: x[0], reverse=True) - return {"items": [{"title": it["title"], "meta": it["meta"], "tag": it.get("tag"), - "match": min(100, s + 50)} for s, it in scored]} + from ...services.settlement_service import SettlementService + + return await SettlementService(db).investor_matches(user["id"]) # ── 路演参与(进入直播/出席)──────────────────────────────────────────────── diff --git a/app/api/routers/rbac_government.py b/app/api/routers/rbac_government.py index f8d201b..d790aef 100644 --- a/app/api/routers/rbac_government.py +++ b/app/api/routers/rbac_government.py @@ -111,14 +111,6 @@ async def gov_data( # ── 补贴三级审批(区县→市→省→发放)───────────────────────────────────────── -# 各政务级别可推进的状态映射 -_SUBSIDY_NEXT: dict[str, dict[str, str]] = { - "gov_district": {"applying": "district_review"}, - "gov_city": {"district_review": "city_review"}, - "gov_province": {"city_review": "province_review", "province_review": "approved"}, -} - - @router.get("/subsidies", summary="范围内补贴申报列表") async def gov_subsidies( db: Database = Depends(get_db), @@ -134,17 +126,11 @@ async def gov_approve_subsidy( db: Database = Depends(get_db), user: dict = Depends(require_permission("menu:gov_data")), ): - sub = await db.subsidies.get(aid) - if sub is None: - raise HTTPException(status_code=404, detail="Subsidy not found") - if sub.get("region_id") and sub["region_id"] not in user.get("scope_region_ids", []): - raise HTTPException(status_code=403, detail="Forbidden: out of data scope") - nxt = (_SUBSIDY_NEXT.get(user.get("sub_role") or "") or {}).get(sub["status"]) - if not nxt: - raise HTTPException(status_code=400, detail="当前级别不能审批该状态") - updated = await db.subsidies.set_status(aid, nxt) + from ...services.subsidy_service import SubsidyService + + updated = await SubsidyService(db).approve(aid, user) await write_audit(db, action="subsidy.approve", resource="subsidy", resource_id=aid, - detail=nxt, user=user, request=request) + detail=updated.get("status"), user=user, request=request) return updated @@ -155,12 +141,9 @@ async def gov_pay_subsidy( db: Database = Depends(get_db), user: dict = Depends(require_permission("menu:gov_data")), ): - sub = await db.subsidies.get(aid) - if sub is None: - raise HTTPException(status_code=404, detail="Subsidy not found") - if sub["status"] != "approved": - raise HTTPException(status_code=400, detail="仅已终审的补贴可发放") - updated = await db.subsidies.set_status(aid, "paid") + from ...services.subsidy_service import SubsidyService + + updated = await SubsidyService(db).pay(aid) await write_audit(db, action="subsidy.pay", resource="subsidy", resource_id=aid, user=user, request=request) return updated diff --git a/app/api/routers/rbac_opc.py b/app/api/routers/rbac_opc.py index 0dd4be1..1e76f57 100644 --- a/app/api/routers/rbac_opc.py +++ b/app/api/routers/rbac_opc.py @@ -249,17 +249,15 @@ async def opc_add_finance( db: Database = Depends(get_db), actor: dict = Depends(require_roles("opc_member")), ): - record = FinanceRecord( - id=new_id("fin"), user_id=actor["id"], category=req.category, - amount=req.amount, date=req.date or utcnow_iso()[:10], note=req.note, - created_at=utcnow_iso(), + from ...services.finance_service import FinanceService + + record = await FinanceService(db).add_record( + user_id=actor["id"], category=req.category, amount=req.amount, + date=req.date or utcnow_iso()[:10], note=req.note, ) - await db.session.add(record) - await db.session.commit() - await write_audit(db, action="finance.add", resource="finance", resource_id=record.id, + await write_audit(db, action="finance.add", resource="finance", resource_id=record["id"], user=actor, request=request) - return {"id": record.id, "category": record.category, "amount": record.amount, - "date": record.date, "note": record.note} + return record @router.get("/tax", summary="OPC 税务申报") diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index 7e60f10..6c006df 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -964,6 +964,16 @@ class FinanceRepository: def __init__(self, session: AsyncSession): self.session = session + async def create(self, user_id: str, category: str, amount: int, date: str, note: str = "") -> dict: + """新增一条财务流水。""" + rec = FinanceRecord( + id=new_id("fin"), user_id=user_id, category=category, amount=amount, + date=date, note=note, created_at=utcnow_iso(), + ) + self.session.add(rec) + await self.session.commit() + return self._to_dict(rec) + async def list_by_user(self, user_id: str, category: str | None = None) -> list[dict]: stmt = select(FinanceRecord).where(FinanceRecord.user_id == user_id) if category: diff --git a/app/services/finance_service.py b/app/services/finance_service.py new file mode 100644 index 0000000..872243c --- /dev/null +++ b/app/services/finance_service.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +"""业务层 · 财务服务(OPC 收支流水)。 + +业务层只处理业务逻辑,依赖基础设施层 Repository;不反向依赖接口层。 +""" +from __future__ import annotations + +from ..infrastructure.repositories import Database + + +class FinanceService: + """OPC 财务流水管理。""" + + def __init__(self, db: Database): + self.db = db + + async def add_record(self, user_id: str, category: str, amount: int, date: str, note: str = "") -> dict: + """记一笔收支(收入为正、支出为负由调用方给定金额符号)。""" + return await self.db.finance.create( + user_id=user_id, category=category, amount=amount, + date=date, note=note, + ) + + async def list_by_user(self, user_id: str, category: str | None = None) -> list[dict]: + return await self.db.finance.list_by_user(user_id, category) diff --git a/app/services/settlement_service.py b/app/services/settlement_service.py new file mode 100644 index 0000000..32d00f6 --- /dev/null +++ b/app/services/settlement_service.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +"""业务层 · 结算/合同/争议/撮合服务。 + +资金相关规则(佣金、结算、争议联动)在业务层集中,接口层瘦身。 +""" +from __future__ import annotations + +from fastapi import HTTPException + +from ..infrastructure.repositories import Database + +# 平台佣金比例 +COMMISSION_RATE = 0.05 + + +class SettlementService: + """结算托管、电子合同、争议、投资撮合。""" + + def __init__(self, db: Database): + self.db = db + + async def release_escrow(self, task_id: str) -> dict: + """验收通过后结算:任务须 completed;无托管则兜底创建(佣金 5%)。""" + task = await self.db.tasks.get(task_id) + if task is None or task["status"] != "completed": + raise HTTPException(status_code=400, detail="任务未完成,不能结算") + escrows = await self.db.escrows.list() + esc = next((e for e in escrows if e["task_id"] == task_id), None) + if esc is None: + amount = max(task.get("budget_min", 0), task.get("budget_max", 0)) + commission = int(amount * COMMISSION_RATE) + esc = await self.db.escrows.create(task_id, task["title"], amount, commission) + return await self.db.escrows.set_status(esc["id"], "released") + + async def sign_contract(self, task_id: str, actor: dict, opc_id: str | None = None) -> dict: + """签订电子合同(幂等:已签返回现有合同)。""" + task = await self.db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="Task not found") + existing = await self.db.contracts.get_for_task(task_id) + if existing is not None: + return existing + resolved_opc = opc_id or (actor["id"] if actor.get("role") == "opc_member" else "") + return await self.db.contracts.create(task_id, task["title"], actor["id"], resolved_opc) + + async def create_dispute(self, task_id: str, actor: dict, reason: str) -> dict: + """发起争议并联动任务置为 disputed。""" + task = await self.db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="Task not found") + dispute = await self.db.disputes.create(task_id, task["title"], actor["id"], reason) + await self.db.tasks.set_status(task_id, "disputed") + return dispute + + async def resolve_dispute(self, dispute_id: str) -> dict: + dispute = await self.db.disputes.set_status(dispute_id, "resolved", resolution="平台调解结案") + if dispute is None: + raise HTTPException(status_code=404, detail="Dispute not found") + return dispute + + async def investor_matches(self, user_id: str) -> dict: + """投资撮合:按投资人偏好(行业/阶段关键词)打分排序。""" + pref = await self.db.investor_prefs.get(user_id) or {} + industries = set(pref.get("industries") or []) + data = await self.db.portal_pages.get("investor", "projects") or {"items": []} + scored = [] + for it in data.get("items", []): + text = f"{it.get('title', '')} {it.get('meta', '')}" + score = sum(10 for ind in industries if ind and ind in text) + if pref.get("stage") and pref["stage"] in text: + score += 5 + scored.append((score, it)) + scored.sort(key=lambda x: x[0], reverse=True) + return {"items": [{"title": it["title"], "meta": it["meta"], "tag": it.get("tag"), + "match": min(100, s + 50)} for s, it in scored]} diff --git a/app/services/subsidy_service.py b/app/services/subsidy_service.py new file mode 100644 index 0000000..cc2dc0f --- /dev/null +++ b/app/services/subsidy_service.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +"""业务层 · 补贴服务(政务三级审批状态机)。""" +from __future__ import annotations + +from fastapi import HTTPException + +from ..infrastructure.repositories import Database + +# 各政务级别可推进的状态映射(领域规则) +SUBSIDY_NEXT: dict[str, dict[str, str]] = { + "gov_district": {"applying": "district_review"}, + "gov_city": {"district_review": "city_review"}, + "gov_province": {"city_review": "province_review", "province_review": "approved"}, +} + + +class SubsidyService: + """补贴申报审批(区县→市→省→发放)。""" + + def __init__(self, db: Database): + self.db = db + + async def approve(self, aid: str, user: dict) -> dict: + """按级别推进补贴审批;越级/越权 403/400。""" + sub = await self.db.subsidies.get(aid) + if sub is None: + raise HTTPException(status_code=404, detail="Subsidy not found") + if sub.get("region_id") and sub["region_id"] not in user.get("scope_region_ids", []): + raise HTTPException(status_code=403, detail="Forbidden: out of data scope") + nxt = (SUBSIDY_NEXT.get(user.get("sub_role") or "") or {}).get(sub["status"]) + if not nxt: + raise HTTPException(status_code=400, detail="当前级别不能审批该状态") + return await self.db.subsidies.set_status(aid, nxt) + + async def pay(self, aid: str) -> dict: + """发放补贴:仅已终审(approved)可发放。""" + sub = await self.db.subsidies.get(aid) + if sub is None: + raise HTTPException(status_code=404, detail="Subsidy not found") + if sub["status"] != "approved": + raise HTTPException(status_code=400, detail="仅已终审的补贴可发放") + return await self.db.subsidies.set_status(aid, "paid")