feat: 业务层 services + 领域层 domain
- 领域层 domain/rules.py:RBAC 判定规则纯函数(role/permission/scope),接口层守卫改调领域规则 - 业务层 services/task_service.py:统一任务状态机(grab/bid/deliver/win/review),修复 deliver 无状态预检缺陷 - rbac_opc 抢单/投标/交付改走 TaskService
This commit is contained in:
@@ -168,10 +168,9 @@ async def opc_grab(
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
task = await db.tasks.get(task_id)
|
||||
if task is None or task["status"] != "published" or task["mode"] != "grab":
|
||||
raise HTTPException(status_code=400, detail="任务不可抢单")
|
||||
updated = await db.tasks.set_status(task_id, "in_progress")
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
updated = await TaskService(db).grab(task_id, actor)
|
||||
await write_audit(db, action="task.grab", resource="task", resource_id=task_id,
|
||||
detail=actor.get("username"), user=actor, request=request)
|
||||
return updated
|
||||
@@ -185,11 +184,9 @@ async def opc_bid(
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
task = await db.tasks.get(task_id)
|
||||
if task is None or task["status"] != "published" or task["mode"] != "bid":
|
||||
raise HTTPException(status_code=400, detail="任务不可投标")
|
||||
bid = await db.bids.create(task_id, actor["id"], actor.get("nickname") or actor["username"],
|
||||
req.quote, req.plan)
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
bid = await TaskService(db).bid(task_id, actor, req.quote, req.plan)
|
||||
await write_audit(db, action="task.bid", resource="bid", resource_id=bid["id"],
|
||||
user=actor, request=request)
|
||||
return bid
|
||||
@@ -202,9 +199,9 @@ async def opc_deliver(
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("opc_member")),
|
||||
):
|
||||
updated = await db.tasks.set_status(task_id, "delivered")
|
||||
if updated is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
from ...services.task_service import TaskService
|
||||
|
||||
updated = await TaskService(db).deliver(task_id, actor)
|
||||
await write_audit(db, action="task.deliver", resource="task", resource_id=task_id,
|
||||
user=actor, request=request)
|
||||
return updated
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""领域层:核心业务模型与规则(纯函数/领域服务),不依赖接口/业务/基础设施。"""
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""领域层 · RBAC 判定规则(纯函数,不依赖 HTTP/数据库)。
|
||||
|
||||
四层架构:领域层定义核心业务模型与规则;接口层守卫(rbac.py 的 Depends
|
||||
工厂)只负责调用这些规则并映射为 HTTP 异常,不承载判定语义。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# 数据范围层级(越靠前权限越大)
|
||||
SCOPE_ORDER = ("province", "city", "district")
|
||||
|
||||
|
||||
def role_allowed(user: dict, *roles: str) -> bool:
|
||||
"""当前用户业务角色是否 ∈ 允许角色集合。"""
|
||||
return user.get("role") in set(roles)
|
||||
|
||||
|
||||
def sub_role_allowed(user: dict, *sub_roles: str) -> bool:
|
||||
"""当前用户子角色是否 ∈ 允许子角色集合(如 op_super_admin)。"""
|
||||
return user.get("sub_role") in set(sub_roles)
|
||||
|
||||
|
||||
def permission_granted(user: dict, perm: str) -> bool:
|
||||
"""当前用户是否拥有权限码 perm。"""
|
||||
return perm in user.get("permissions", [])
|
||||
|
||||
|
||||
def is_government(user: dict) -> bool:
|
||||
return user.get("role") == "government"
|
||||
|
||||
|
||||
def scope_level_ok(user: dict, min_level: str | None) -> bool:
|
||||
"""政务数据范围层级校验:不低于 min_level。"""
|
||||
if not user.get("region_id"):
|
||||
return False
|
||||
if not min_level or not user.get("scope_level"):
|
||||
return True
|
||||
cur = user["scope_level"]
|
||||
return cur in SCOPE_ORDER and SCOPE_ORDER.index(cur) <= SCOPE_ORDER.index(min_level)
|
||||
|
||||
|
||||
def region_in_scope(user: dict, region_id: str | None) -> bool:
|
||||
"""目标区域是否落在当前用户数据范围内。"""
|
||||
return bool(region_id) and region_id in user.get("scope_region_ids", [])
|
||||
+18
-29
@@ -1,8 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""RBAC 依赖工厂:角色 / 权限 / 数据范围守卫 + 审计写入。
|
||||
"""接口层 RBAC 守卫工厂:角色 / 权限 / 数据范围守卫 + 审计写入。
|
||||
|
||||
依赖以 ``Depends(get_current_user)`` 为前置,返回已装配 role/权限/
|
||||
scope_region_ids 的当前用户 dict;不满足即抛 403。
|
||||
判定语义在领域层 ``domain/rules.py``;本文件只把规则映射为 HTTP 异常。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -11,73 +10,63 @@ from collections.abc import Callable
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
|
||||
from .api.dependencies import get_current_user
|
||||
from .domain.rules import (
|
||||
is_government,
|
||||
permission_granted,
|
||||
region_in_scope,
|
||||
role_allowed,
|
||||
scope_level_ok,
|
||||
sub_role_allowed,
|
||||
)
|
||||
from .infrastructure.repositories import Database
|
||||
|
||||
# 数据范围层级(越靠前权限越大)
|
||||
_SCOPE_ORDER = ("province", "city", "district")
|
||||
|
||||
|
||||
def require_roles(*roles: str) -> Callable:
|
||||
"""要求当前用户业务角色 ∈ roles;否则 403。"""
|
||||
allowed = set(roles)
|
||||
|
||||
def dep(user: dict = Depends(get_current_user)) -> dict:
|
||||
if user.get("role") not in allowed:
|
||||
if not role_allowed(user, *roles):
|
||||
raise HTTPException(status_code=403, detail="Forbidden: insufficient role")
|
||||
return user
|
||||
|
||||
return dep
|
||||
|
||||
|
||||
def require_sub_roles(*sub_roles: str) -> Callable:
|
||||
"""要求当前用户子角色 ∈ sub_roles(如 op_super_admin);否则 403。"""
|
||||
allowed = set(sub_roles)
|
||||
|
||||
def dep(user: dict = Depends(get_current_user)) -> dict:
|
||||
if user.get("sub_role") not in allowed:
|
||||
if not sub_role_allowed(user, *sub_roles):
|
||||
raise HTTPException(status_code=403, detail="Forbidden: insufficient sub-role")
|
||||
return user
|
||||
|
||||
return dep
|
||||
|
||||
|
||||
def require_permission(perm: str) -> Callable:
|
||||
"""要求当前用户拥有权限码 ``perm``;否则 403。"""
|
||||
def dep(user: dict = Depends(get_current_user)) -> dict:
|
||||
if perm not in user.get("permissions", []):
|
||||
if not permission_granted(user, perm):
|
||||
raise HTTPException(status_code=403, detail=f"Forbidden: missing permission {perm}")
|
||||
return user
|
||||
|
||||
return dep
|
||||
|
||||
|
||||
def require_scope(min_level: str | None = None) -> Callable:
|
||||
"""政务数据范围守卫:要求用户为 government 且有区域,且不低于 min_level。"""
|
||||
"""政务数据范围守卫:要求 government 且区域层级不低于 min_level。"""
|
||||
def dep(user: dict = Depends(get_current_user)) -> dict:
|
||||
if user.get("role") != "government":
|
||||
if not is_government(user):
|
||||
raise HTTPException(status_code=403, detail="Forbidden: not government")
|
||||
if not user.get("region_id"):
|
||||
raise HTTPException(status_code=403, detail="Forbidden: no region scope")
|
||||
if min_level and user.get("scope_level"):
|
||||
if _SCOPE_ORDER.index(user["scope_level"]) > _SCOPE_ORDER.index(min_level):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Forbidden: scope below {min_level}",
|
||||
)
|
||||
if not scope_level_ok(user, min_level):
|
||||
raise HTTPException(status_code=403, detail=f"Forbidden: scope below {min_level}")
|
||||
return user
|
||||
|
||||
return dep
|
||||
|
||||
|
||||
def scope_covers(region_id: str | None) -> Callable:
|
||||
"""要求 ``region_id`` 落在当前用户数据范围内;否则 403。"""
|
||||
def dep(user: dict = Depends(get_current_user)) -> dict:
|
||||
if not region_id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden: target has no region")
|
||||
if region_id not in user.get("scope_region_ids", []):
|
||||
if not region_in_scope(user, region_id):
|
||||
raise HTTPException(status_code=403, detail="Forbidden: out of data scope")
|
||||
return user
|
||||
|
||||
return dep
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""业务层:业务逻辑编排与用例。只依赖领域层与基础设施层,不依赖接口层。"""
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""业务层 · 任务状态机(抢单 / 投标 / 交付 / 中标 / 验收)。
|
||||
|
||||
业务层只处理业务逻辑;不感知 HTTP 之外的框架细节。依赖基础设施层
|
||||
Repository(经 Database 门面),不反向依赖接口层。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from ..infrastructure.repositories import Database
|
||||
|
||||
|
||||
class TaskService:
|
||||
"""统一任务状态流转:grab/bid/deliver/win/review。"""
|
||||
|
||||
def __init__(self, db: Database):
|
||||
self.db = db
|
||||
|
||||
async def grab(self, task_id: str, actor: dict) -> dict:
|
||||
"""抢单:仅 published + grab 模式可抢,抢后置 in_progress。"""
|
||||
task = await self.db.tasks.get(task_id)
|
||||
if task is None or task["status"] != "published" or task["mode"] != "grab":
|
||||
raise HTTPException(status_code=400, detail="任务不可抢单")
|
||||
return await self.db.tasks.set_status(task_id, "in_progress")
|
||||
|
||||
async def bid(self, task_id: str, actor: dict, quote: int, plan: str) -> dict:
|
||||
"""投标:仅 published + bid 模式可投。"""
|
||||
task = await self.db.tasks.get(task_id)
|
||||
if task is None or task["status"] != "published" or task["mode"] != "bid":
|
||||
raise HTTPException(status_code=400, detail="任务不可投标")
|
||||
return await self.db.bids.create(
|
||||
task_id, actor["id"], actor.get("nickname") or actor["username"],
|
||||
quote, plan,
|
||||
)
|
||||
|
||||
async def deliver(self, task_id: str, actor: dict) -> dict:
|
||||
"""交付:任务存在且处于进行中(in_progress)才可交付 → delivered。
|
||||
|
||||
补齐原实现「无任何状态预检」的缺陷,防随意交付。
|
||||
"""
|
||||
task = await self.db.tasks.get(task_id)
|
||||
if task is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
if task["status"] != "in_progress":
|
||||
raise HTTPException(status_code=400, detail="任务未在进行中,无法交付")
|
||||
return await self.db.tasks.set_status(task_id, "delivered")
|
||||
|
||||
async def enterprise_submit(self, task_id: str) -> dict:
|
||||
"""企业提交审核:pending 任务 → published。"""
|
||||
return await self.db.tasks.set_status(task_id, "published")
|
||||
|
||||
async def win_bid(self, task_id: str, bid_id: str) -> dict:
|
||||
"""企业评标中标:bid → win,task → in_progress(双状态联动)。"""
|
||||
bid = await self.db.bids.get(bid_id)
|
||||
if bid is None or bid["task_id"] != task_id:
|
||||
raise HTTPException(status_code=404, detail="竞标不存在")
|
||||
await self.db.bids.set_status(bid_id, "win")
|
||||
return await self.db.tasks.set_status(task_id, "in_progress")
|
||||
|
||||
async def review(self, task_id: str, accept: bool) -> dict:
|
||||
"""企业验收:accept → completed;reject → in_progress。"""
|
||||
target = "completed" if accept else "in_progress"
|
||||
return await self.db.tasks.set_status(task_id, target)
|
||||
Reference in New Issue
Block a user