feat: 业务层 services 抽取(财务/结算/补贴)
- FinanceService:opc_add_finance 直写 ORM 改为走 Repository.create(修复四层依赖违规) - SettlementService:结算佣金(5%)/电子合同幂等/争议联动/投资撮合评分 - SubsidyService:政务三级审批状态机(区县→市→省→发放) - rbac_ecosystem/rbac_government/rbac_opc 接线,路由瘦身
This commit is contained in:
@@ -91,13 +91,9 @@ async def sign_contract(
|
|||||||
db: Database = Depends(get_db),
|
db: Database = Depends(get_db),
|
||||||
actor: dict = Depends(require_roles("enterprise", "opc_member")),
|
actor: dict = Depends(require_roles("enterprise", "opc_member")),
|
||||||
):
|
):
|
||||||
task = await db.tasks.get(task_id)
|
from ...services.settlement_service import SettlementService
|
||||||
if task is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Task not found")
|
contract = await SettlementService(db).sign_contract(task_id, actor, req.opc_id)
|
||||||
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)
|
|
||||||
await write_audit(db, action="contract.sign", resource="contract", resource_id=contract["id"],
|
await write_audit(db, action="contract.sign", resource="contract", resource_id=contract["id"],
|
||||||
user=actor, request=request)
|
user=actor, request=request)
|
||||||
return contract
|
return contract
|
||||||
@@ -111,18 +107,11 @@ async def release_escrow(
|
|||||||
db: Database = Depends(get_db),
|
db: Database = Depends(get_db),
|
||||||
actor: dict = Depends(require_roles("enterprise")),
|
actor: dict = Depends(require_roles("enterprise")),
|
||||||
):
|
):
|
||||||
task = await db.tasks.get(task_id)
|
from ...services.settlement_service import SettlementService
|
||||||
if task is None or task["status"] != "completed":
|
|
||||||
raise HTTPException(status_code=400, detail="任务未完成,不能结算")
|
released = await SettlementService(db).release_escrow(task_id)
|
||||||
escrow = await db.escrows.list()
|
await write_audit(db, action="escrow.release", resource="escrow", resource_id=released["id"],
|
||||||
esc = next((e for e in escrow if e["task_id"] == task_id), None)
|
detail=f"amount={released['amount']}", user=actor, request=request)
|
||||||
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)
|
|
||||||
return released
|
return released
|
||||||
|
|
||||||
|
|
||||||
@@ -148,11 +137,9 @@ async def create_dispute(
|
|||||||
db: Database = Depends(get_db),
|
db: Database = Depends(get_db),
|
||||||
actor: dict = Depends(require_roles("enterprise", "opc_member")),
|
actor: dict = Depends(require_roles("enterprise", "opc_member")),
|
||||||
):
|
):
|
||||||
task = await db.tasks.get(task_id)
|
from ...services.settlement_service import SettlementService
|
||||||
if task is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Task not found")
|
dispute = await SettlementService(db).create_dispute(task_id, actor, req.reason)
|
||||||
dispute = await db.disputes.create(task_id, task["title"], actor["id"], req.reason)
|
|
||||||
await db.tasks.set_status(task_id, "disputed")
|
|
||||||
await write_audit(db, action="dispute.open", resource="dispute", resource_id=dispute["id"],
|
await write_audit(db, action="dispute.open", resource="dispute", resource_id=dispute["id"],
|
||||||
user=actor, request=request)
|
user=actor, request=request)
|
||||||
return dispute
|
return dispute
|
||||||
@@ -165,9 +152,9 @@ async def resolve_dispute(
|
|||||||
db: Database = Depends(get_db),
|
db: Database = Depends(get_db),
|
||||||
actor: dict = Depends(require_roles("operator")),
|
actor: dict = Depends(require_roles("operator")),
|
||||||
):
|
):
|
||||||
dispute = await db.disputes.set_status(dispute_id, "resolved", resolution="平台调解结案")
|
from ...services.settlement_service import SettlementService
|
||||||
if dispute is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Dispute not found")
|
dispute = await SettlementService(db).resolve_dispute(dispute_id)
|
||||||
await write_audit(db, action="dispute.resolve", resource="dispute", resource_id=dispute_id,
|
await write_audit(db, action="dispute.resolve", resource="dispute", resource_id=dispute_id,
|
||||||
user=actor, request=request)
|
user=actor, request=request)
|
||||||
return dispute
|
return dispute
|
||||||
@@ -179,24 +166,9 @@ async def investor_matches(
|
|||||||
db: Database = Depends(get_db),
|
db: Database = Depends(get_db),
|
||||||
user: dict = Depends(require_roles("investor")),
|
user: dict = Depends(require_roles("investor")),
|
||||||
):
|
):
|
||||||
pref = await db.investor_prefs.get(user["id"]) or {}
|
from ...services.settlement_service import SettlementService
|
||||||
industries = set(pref.get("industries") or [])
|
|
||||||
data = await db.portal_pages.get("investor", "projects") or {"items": []}
|
return await SettlementService(db).investor_matches(user["id"])
|
||||||
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]}
|
|
||||||
|
|
||||||
|
|
||||||
# ── 路演参与(进入直播/出席)────────────────────────────────────────────────
|
# ── 路演参与(进入直播/出席)────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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="范围内补贴申报列表")
|
@router.get("/subsidies", summary="范围内补贴申报列表")
|
||||||
async def gov_subsidies(
|
async def gov_subsidies(
|
||||||
db: Database = Depends(get_db),
|
db: Database = Depends(get_db),
|
||||||
@@ -134,17 +126,11 @@ async def gov_approve_subsidy(
|
|||||||
db: Database = Depends(get_db),
|
db: Database = Depends(get_db),
|
||||||
user: dict = Depends(require_permission("menu:gov_data")),
|
user: dict = Depends(require_permission("menu:gov_data")),
|
||||||
):
|
):
|
||||||
sub = await db.subsidies.get(aid)
|
from ...services.subsidy_service import SubsidyService
|
||||||
if sub is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Subsidy not found")
|
updated = await SubsidyService(db).approve(aid, user)
|
||||||
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)
|
|
||||||
await write_audit(db, action="subsidy.approve", resource="subsidy", resource_id=aid,
|
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
|
return updated
|
||||||
|
|
||||||
|
|
||||||
@@ -155,12 +141,9 @@ async def gov_pay_subsidy(
|
|||||||
db: Database = Depends(get_db),
|
db: Database = Depends(get_db),
|
||||||
user: dict = Depends(require_permission("menu:gov_data")),
|
user: dict = Depends(require_permission("menu:gov_data")),
|
||||||
):
|
):
|
||||||
sub = await db.subsidies.get(aid)
|
from ...services.subsidy_service import SubsidyService
|
||||||
if sub is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Subsidy not found")
|
updated = await SubsidyService(db).pay(aid)
|
||||||
if sub["status"] != "approved":
|
|
||||||
raise HTTPException(status_code=400, detail="仅已终审的补贴可发放")
|
|
||||||
updated = await db.subsidies.set_status(aid, "paid")
|
|
||||||
await write_audit(db, action="subsidy.pay", resource="subsidy", resource_id=aid,
|
await write_audit(db, action="subsidy.pay", resource="subsidy", resource_id=aid,
|
||||||
user=user, request=request)
|
user=user, request=request)
|
||||||
return updated
|
return updated
|
||||||
|
|||||||
@@ -249,17 +249,15 @@ async def opc_add_finance(
|
|||||||
db: Database = Depends(get_db),
|
db: Database = Depends(get_db),
|
||||||
actor: dict = Depends(require_roles("opc_member")),
|
actor: dict = Depends(require_roles("opc_member")),
|
||||||
):
|
):
|
||||||
record = FinanceRecord(
|
from ...services.finance_service import FinanceService
|
||||||
id=new_id("fin"), user_id=actor["id"], category=req.category,
|
|
||||||
amount=req.amount, date=req.date or utcnow_iso()[:10], note=req.note,
|
record = await FinanceService(db).add_record(
|
||||||
created_at=utcnow_iso(),
|
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 write_audit(db, action="finance.add", resource="finance", resource_id=record["id"],
|
||||||
await db.session.commit()
|
|
||||||
await write_audit(db, action="finance.add", resource="finance", resource_id=record.id,
|
|
||||||
user=actor, request=request)
|
user=actor, request=request)
|
||||||
return {"id": record.id, "category": record.category, "amount": record.amount,
|
return record
|
||||||
"date": record.date, "note": record.note}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tax", summary="OPC 税务申报")
|
@router.get("/tax", summary="OPC 税务申报")
|
||||||
|
|||||||
@@ -964,6 +964,16 @@ class FinanceRepository:
|
|||||||
def __init__(self, session: AsyncSession):
|
def __init__(self, session: AsyncSession):
|
||||||
self.session = session
|
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]:
|
async def list_by_user(self, user_id: str, category: str | None = None) -> list[dict]:
|
||||||
stmt = select(FinanceRecord).where(FinanceRecord.user_id == user_id)
|
stmt = select(FinanceRecord).where(FinanceRecord.user_id == user_id)
|
||||||
if category:
|
if category:
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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]}
|
||||||
@@ -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")
|
||||||
Reference in New Issue
Block a user