refactor: 移除审批和回写功能模块

移除了整个审批(approvals)和官方回写(writebacks)功能模块,
包括相关模型、路由、服务和配置项。更新了数据库迁移文件,
删除了相关的审批请求表和官方回写运行表。同时从API路由器中
移除了相应的路由,并调整了安全常量和字段验证器以匹配变更。
```
This commit is contained in:
2026-07-08 17:08:59 +08:00
parent 19e59e83cc
commit 0a153b264a
48 changed files with 135 additions and 2225 deletions

View File

@@ -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}'"

View File

@@ -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),
}

View File

@@ -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):

View File

@@ -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,
)