```
refactor: 移除审批和回写功能模块 移除了整个审批(approvals)和官方回写(writebacks)功能模块, 包括相关模型、路由、服务和配置项。更新了数据库迁移文件, 删除了相关的审批请求表和官方回写运行表。同时从API路由器中 移除了相应的路由,并调整了安全常量和字段验证器以匹配变更。 ```
This commit is contained in:
@@ -86,11 +86,6 @@ class BusinessResponseKey(StrEnum):
|
||||
DATA = "data"
|
||||
|
||||
|
||||
class BusinessPayloadKey(StrEnum):
|
||||
DATA = "data"
|
||||
APPROVAL_TICKET_ID = "approval_ticket_id"
|
||||
|
||||
|
||||
class BusinessField(StrEnum):
|
||||
ID = "id"
|
||||
STATUS = "status"
|
||||
@@ -98,7 +93,6 @@ class BusinessField(StrEnum):
|
||||
|
||||
class BusinessErrorDetail(StrEnum):
|
||||
RECORD_NOT_FOUND = "Record not found"
|
||||
HIGH_RISK_APPROVAL_REQUIRED = "High-risk domain change requires approval_ticket_id"
|
||||
|
||||
|
||||
UNKNOWN_FIELD_TEMPLATE = "Unknown field '{field}'"
|
||||
|
||||
@@ -4,10 +4,10 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.masking import mask_configured
|
||||
from app.core.security import ApiPrincipal, require_api_key
|
||||
from app.core.security import require_api_key
|
||||
from app.modules.business.constants import BusinessField, BusinessResponseKey
|
||||
from app.modules.business.registry import supported_domain_values
|
||||
from app.modules.business.schemas import DomainListRead, DomainRecordCreate, DomainRecordUpdate
|
||||
from app.modules.business.schemas import DomainListRead
|
||||
from app.modules.business.service import BusinessService
|
||||
|
||||
router = APIRouter(dependencies=[Depends(require_api_key)])
|
||||
@@ -53,49 +53,3 @@ def get_record(
|
||||
}
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/{domain}")
|
||||
def create_record(
|
||||
domain: str,
|
||||
payload: DomainRecordCreate,
|
||||
db: Session = Depends(get_db),
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
try:
|
||||
data = BusinessService(db).create_record(
|
||||
domain,
|
||||
payload.data,
|
||||
principal.actor,
|
||||
payload.approval_ticket_id,
|
||||
)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
return {
|
||||
BusinessResponseKey.DOMAIN: domain,
|
||||
BusinessResponseKey.DATA: mask_configured(data, domain=domain),
|
||||
}
|
||||
|
||||
|
||||
@router.patch("/{domain}/{record_id}")
|
||||
def update_record(
|
||||
domain: str,
|
||||
record_id: int,
|
||||
payload: DomainRecordUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
principal: ApiPrincipal = Depends(require_api_key),
|
||||
) -> dict:
|
||||
try:
|
||||
data = BusinessService(db).update_record(
|
||||
domain,
|
||||
record_id,
|
||||
payload.data,
|
||||
actor=principal.actor,
|
||||
approval_ticket_id=payload.approval_ticket_id,
|
||||
)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
return {
|
||||
BusinessResponseKey.DOMAIN: domain,
|
||||
BusinessResponseKey.DATA: mask_configured(data, domain=domain),
|
||||
}
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.core.constants import ActorValue
|
||||
|
||||
|
||||
class DomainRecordCreate(BaseModel):
|
||||
data: dict[str, Any] = Field(..., description="Domain fields to create.")
|
||||
actor: str = ActorValue.API
|
||||
approval_ticket_id: str | None = Field(
|
||||
default=None,
|
||||
description="Required by policy for business record creates.",
|
||||
)
|
||||
|
||||
|
||||
class DomainRecordUpdate(BaseModel):
|
||||
data: dict[str, Any] = Field(..., description="Domain fields to update.")
|
||||
actor: str = ActorValue.API
|
||||
approval_ticket_id: str | None = Field(
|
||||
default=None,
|
||||
description="Required by policy for business record updates.",
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DomainRecordRead(BaseModel):
|
||||
|
||||
@@ -10,21 +10,14 @@ from sqlalchemy import Select, func, select
|
||||
from sqlalchemy.sql.schema import Column
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.constants import ActorValue
|
||||
from app.core.pagination import bounded_limit, bounded_offset
|
||||
from app.modules.audit.constants import AuditRiskLevel, AuditSource
|
||||
from app.modules.audit.schemas import AuditLogCreate
|
||||
from app.modules.audit.service import AuditService
|
||||
from app.modules.approvals.constants import ApprovalActionValue, approval_action
|
||||
from app.modules.approvals.service import ApprovalService
|
||||
from app.modules.business.registry import get_domain_model, get_writable_fields, is_high_risk_domain
|
||||
from app.modules.business.registry import get_domain_model, get_writable_fields
|
||||
from app.modules.business.constants import (
|
||||
INVALID_FIELD_VALUE_TEMPLATE,
|
||||
READ_ONLY_FIELD_TEMPLATE,
|
||||
UNKNOWN_FIELD_TEMPLATE,
|
||||
BusinessErrorDetail,
|
||||
BusinessField,
|
||||
BusinessPayloadKey,
|
||||
)
|
||||
|
||||
|
||||
@@ -90,11 +83,10 @@ def _model_payload(domain: str, model: Any, data: dict[str, Any]) -> dict[str, A
|
||||
|
||||
|
||||
class BusinessService:
|
||||
"""Manage generic CRUD operations across registered business domains."""
|
||||
"""Read business records across registered domains."""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.audit = AuditService(db)
|
||||
|
||||
def list_records(
|
||||
self,
|
||||
@@ -124,116 +116,3 @@ class BusinessService:
|
||||
detail=BusinessErrorDetail.RECORD_NOT_FOUND,
|
||||
)
|
||||
return serialize_model(record)
|
||||
|
||||
def create_record(
|
||||
self,
|
||||
domain: str,
|
||||
data: dict[str, Any],
|
||||
actor: str = ActorValue.API,
|
||||
approval_ticket_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
model = get_domain_model(domain)
|
||||
high_risk = is_high_risk_domain(domain)
|
||||
payload = _model_payload(domain, model, data)
|
||||
record = model(**payload)
|
||||
self.db.add(record)
|
||||
if high_risk:
|
||||
self.db.flush()
|
||||
self._consume_approval(
|
||||
approval_ticket_id,
|
||||
domain,
|
||||
record.id,
|
||||
approval_action(ApprovalActionValue.CREATE, domain),
|
||||
data,
|
||||
actor,
|
||||
)
|
||||
self.db.commit()
|
||||
self.db.refresh(record)
|
||||
result = serialize_model(record)
|
||||
self.audit.log(
|
||||
AuditLogCreate(
|
||||
actor=actor,
|
||||
source=AuditSource.API,
|
||||
action=approval_action(ApprovalActionValue.CREATE, domain),
|
||||
target_type=domain,
|
||||
target_id=str(record.id),
|
||||
risk_level=AuditRiskLevel.HIGH if high_risk else AuditRiskLevel.LOW,
|
||||
request_payload={
|
||||
BusinessPayloadKey.DATA: data,
|
||||
BusinessPayloadKey.APPROVAL_TICKET_ID: approval_ticket_id,
|
||||
},
|
||||
response_payload=result,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def update_record(
|
||||
self,
|
||||
domain: str,
|
||||
record_id: int,
|
||||
data: dict[str, Any],
|
||||
actor: str = ActorValue.API,
|
||||
approval_ticket_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
model = get_domain_model(domain)
|
||||
high_risk = is_high_risk_domain(domain)
|
||||
record = self.db.get(model, record_id)
|
||||
if record is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=BusinessErrorDetail.RECORD_NOT_FOUND,
|
||||
)
|
||||
payload = _model_payload(domain, model, data)
|
||||
if high_risk:
|
||||
self._consume_approval(
|
||||
approval_ticket_id,
|
||||
domain,
|
||||
record_id,
|
||||
approval_action(ApprovalActionValue.UPDATE, domain),
|
||||
data,
|
||||
actor,
|
||||
)
|
||||
for key, value in payload.items():
|
||||
setattr(record, key, value)
|
||||
self.db.commit()
|
||||
self.db.refresh(record)
|
||||
result = serialize_model(record)
|
||||
self.audit.log(
|
||||
AuditLogCreate(
|
||||
actor=actor,
|
||||
source=AuditSource.API,
|
||||
action=approval_action(ApprovalActionValue.UPDATE, domain),
|
||||
target_type=domain,
|
||||
target_id=str(record.id),
|
||||
risk_level=AuditRiskLevel.HIGH if high_risk else AuditRiskLevel.LOW,
|
||||
request_payload={
|
||||
BusinessPayloadKey.DATA: data,
|
||||
BusinessPayloadKey.APPROVAL_TICKET_ID: approval_ticket_id,
|
||||
},
|
||||
response_payload=result,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def _consume_approval(
|
||||
self,
|
||||
approval_ticket_id: str | None,
|
||||
domain: str,
|
||||
record_id: str | int | None,
|
||||
action: str,
|
||||
payload: dict[str, Any],
|
||||
actor: str,
|
||||
) -> None:
|
||||
if not approval_ticket_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=BusinessErrorDetail.HIGH_RISK_APPROVAL_REQUIRED,
|
||||
)
|
||||
ApprovalService(self.db).consume_for(
|
||||
approval_ticket_id,
|
||||
domain,
|
||||
record_id,
|
||||
action,
|
||||
payload,
|
||||
actor,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user