413bd29390
- FinanceService:opc_add_finance 直写 ORM 改为走 Repository.create(修复四层依赖违规) - SettlementService:结算佣金(5%)/电子合同幂等/争议联动/投资撮合评分 - SubsidyService:政务三级审批状态机(区县→市→省→发放) - rbac_ecosystem/rbac_government/rbac_opc 接线,路由瘦身
76 lines
3.5 KiB
Python
76 lines
3.5 KiB
Python
# -*- 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]}
|