```
feat: 添加数据库迁移脚本并更新Dockerfile配置 - 在Dockerfile中添加alembic配置文件和目录的复制指令 - 更新alembic/env.py注册新的模块模型:events、workflows、writebacks - 生成完整的初始数据库schema迁移脚本,包含以下表: - approval_requests, attendance_records, audit_logs, domain_events - expenses, feishu_event_receipts, fund_accounts, legacy_sync_runs - official_writeback_runs, performance_metrics, policies, procurements - projects, report_push_runs, risk_event_actions, risk_events - standards, suppliers, work_reports, work_tasks, workflow_actions - workflow_instances等21个数据表结构定义 - 在API路由器中添加新模块的路由:events、workflows、writebacks、observability ```
This commit is contained in:
@@ -102,6 +102,7 @@ class BusinessErrorDetail(StrEnum):
|
||||
|
||||
|
||||
UNKNOWN_FIELD_TEMPLATE = "Unknown field '{field}'"
|
||||
READ_ONLY_FIELD_TEMPLATE = "Field '{field}' is read-only"
|
||||
INVALID_FIELD_VALUE_TEMPLATE = "Invalid value for field '{field}'"
|
||||
|
||||
|
||||
|
||||
@@ -20,12 +20,199 @@ DOMAIN_MODELS: dict[BusinessDomain, type[DeclarativeMeta]] = {
|
||||
BusinessDomain.LEGACY_SYNC_RUNS: models.LegacySyncRun,
|
||||
}
|
||||
|
||||
HIGH_RISK_DOMAINS = frozenset(
|
||||
{
|
||||
BusinessDomain.FUND_ACCOUNTS,
|
||||
BusinessDomain.PERFORMANCE_METRICS,
|
||||
}
|
||||
)
|
||||
DOMAIN_WRITABLE_FIELDS: dict[BusinessDomain, frozenset[str]] = {
|
||||
BusinessDomain.PROJECTS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"name",
|
||||
"owner",
|
||||
"status",
|
||||
"priority",
|
||||
"progress_percent",
|
||||
"risk_level",
|
||||
"budget_amount",
|
||||
"actual_amount",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"description",
|
||||
}
|
||||
),
|
||||
BusinessDomain.TASKS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"title",
|
||||
"project_code",
|
||||
"owner",
|
||||
"status",
|
||||
"priority",
|
||||
"due_date",
|
||||
"completed_at",
|
||||
"blocker",
|
||||
"description",
|
||||
}
|
||||
),
|
||||
BusinessDomain.PROCUREMENTS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"name",
|
||||
"applicant",
|
||||
"project_code",
|
||||
"supplier_name",
|
||||
"budget_subject",
|
||||
"expected_amount",
|
||||
"actual_amount",
|
||||
"approval_status",
|
||||
"delivery_status",
|
||||
"payment_status",
|
||||
"comparison_summary",
|
||||
}
|
||||
),
|
||||
BusinessDomain.EXPENSES: frozenset(
|
||||
{
|
||||
"code",
|
||||
"expense_type",
|
||||
"amount",
|
||||
"applicant",
|
||||
"department",
|
||||
"project_code",
|
||||
"budget_subject",
|
||||
"payment_account",
|
||||
"invoice_status",
|
||||
"approval_status",
|
||||
"payment_status",
|
||||
}
|
||||
),
|
||||
BusinessDomain.FUND_ACCOUNTS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"name",
|
||||
"account_type",
|
||||
"current_balance",
|
||||
"expected_receivable",
|
||||
"expected_payable",
|
||||
"safety_line",
|
||||
"risk_level",
|
||||
"note",
|
||||
}
|
||||
),
|
||||
BusinessDomain.POLICIES: frozenset(
|
||||
{
|
||||
"code",
|
||||
"title",
|
||||
"policy_type",
|
||||
"owner_department",
|
||||
"version",
|
||||
"status",
|
||||
"effective_date",
|
||||
"feishu_doc_url",
|
||||
"summary",
|
||||
}
|
||||
),
|
||||
BusinessDomain.STANDARDS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"title",
|
||||
"standard_type",
|
||||
"applies_to",
|
||||
"status",
|
||||
"check_items",
|
||||
"remediation",
|
||||
"policy_code",
|
||||
}
|
||||
),
|
||||
BusinessDomain.PERFORMANCE_METRICS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"name",
|
||||
"applies_to_role",
|
||||
"formula",
|
||||
"weight",
|
||||
"data_source",
|
||||
"auto_score",
|
||||
"confirmed_score",
|
||||
"status",
|
||||
}
|
||||
),
|
||||
BusinessDomain.SUPPLIERS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"name",
|
||||
"category",
|
||||
"contact",
|
||||
"quality_score",
|
||||
"delivery_score",
|
||||
"price_score",
|
||||
"risk_level",
|
||||
"blacklist_status",
|
||||
}
|
||||
),
|
||||
BusinessDomain.ATTENDANCE_RECORDS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"employee_name",
|
||||
"employee_id",
|
||||
"department",
|
||||
"project_code",
|
||||
"work_date",
|
||||
"check_in_at",
|
||||
"check_out_at",
|
||||
"status",
|
||||
"location",
|
||||
"note",
|
||||
}
|
||||
),
|
||||
BusinessDomain.WORK_REPORTS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"report_type",
|
||||
"title",
|
||||
"reporter",
|
||||
"department",
|
||||
"project_code",
|
||||
"period_start",
|
||||
"period_end",
|
||||
"content",
|
||||
"metrics",
|
||||
"risk_summary",
|
||||
"status",
|
||||
}
|
||||
),
|
||||
BusinessDomain.RISK_EVENTS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"title",
|
||||
"risk_type",
|
||||
"risk_level",
|
||||
"status",
|
||||
"source_domain",
|
||||
"source_record_id",
|
||||
"project_code",
|
||||
"owner",
|
||||
"due_date",
|
||||
"assigned_to",
|
||||
"closed_reason",
|
||||
"review_summary",
|
||||
"description",
|
||||
"mitigation",
|
||||
"evidence",
|
||||
}
|
||||
),
|
||||
BusinessDomain.LEGACY_SYNC_RUNS: frozenset(
|
||||
{
|
||||
"code",
|
||||
"domain",
|
||||
"source_table",
|
||||
"status",
|
||||
"created_count",
|
||||
"updated_count",
|
||||
"skipped_count",
|
||||
"error_message",
|
||||
"note",
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
HIGH_RISK_DOMAINS = frozenset(DOMAIN_MODELS)
|
||||
LOW_RISK_DOMAINS = frozenset(set(DOMAIN_MODELS) - HIGH_RISK_DOMAINS)
|
||||
|
||||
|
||||
@@ -47,3 +234,7 @@ def is_high_risk_domain(domain: str | BusinessDomain) -> bool:
|
||||
|
||||
def get_domain_model(domain: str | BusinessDomain) -> type[DeclarativeMeta]:
|
||||
return DOMAIN_MODELS[normalize_domain(domain)]
|
||||
|
||||
|
||||
def get_writable_fields(domain: str | BusinessDomain) -> frozenset[str]:
|
||||
return DOMAIN_WRITABLE_FIELDS[normalize_domain(domain)]
|
||||
|
||||
@@ -10,7 +10,7 @@ class DomainRecordCreate(BaseModel):
|
||||
actor: str = ActorValue.API
|
||||
approval_ticket_id: str | None = Field(
|
||||
default=None,
|
||||
description="Required by policy for high-risk creates such as funds or performance.",
|
||||
description="Required by policy for business record creates.",
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class DomainRecordUpdate(BaseModel):
|
||||
actor: str = ActorValue.API
|
||||
approval_ticket_id: str | None = Field(
|
||||
default=None,
|
||||
description="Required by policy for high-risk updates such as funds or performance.",
|
||||
description="Required by policy for business record updates.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,9 +17,10 @@ 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, is_high_risk_domain
|
||||
from app.modules.business.registry import get_domain_model, get_writable_fields, is_high_risk_domain
|
||||
from app.modules.business.constants import (
|
||||
INVALID_FIELD_VALUE_TEMPLATE,
|
||||
READ_ONLY_FIELD_TEMPLATE,
|
||||
UNKNOWN_FIELD_TEMPLATE,
|
||||
BusinessErrorDetail,
|
||||
BusinessField,
|
||||
@@ -56,7 +57,7 @@ def _coerce_column_value(column: Column, value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
def _model_payload(model: Any, data: dict[str, Any]) -> dict[str, Any]:
|
||||
def _model_payload(domain: str, model: Any, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate keys and coerce values according to model column types."""
|
||||
|
||||
columns = {
|
||||
@@ -64,6 +65,7 @@ def _model_payload(model: Any, data: dict[str, Any]) -> dict[str, Any]:
|
||||
for column in model.__table__.columns
|
||||
if column.name != BusinessField.ID
|
||||
}
|
||||
writable_fields = get_writable_fields(domain)
|
||||
payload: dict[str, Any] = {}
|
||||
for key, value in data.items():
|
||||
column = columns.get(key)
|
||||
@@ -72,6 +74,11 @@ def _model_payload(model: Any, data: dict[str, Any]) -> dict[str, Any]:
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=UNKNOWN_FIELD_TEMPLATE.format(field=key),
|
||||
)
|
||||
if key not in writable_fields:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=READ_ONLY_FIELD_TEMPLATE.format(field=key),
|
||||
)
|
||||
try:
|
||||
payload[key] = _coerce_column_value(column, value)
|
||||
except (ValueError, TypeError, InvalidOperation) as exc:
|
||||
@@ -127,7 +134,7 @@ class BusinessService:
|
||||
) -> dict[str, Any]:
|
||||
model = get_domain_model(domain)
|
||||
high_risk = is_high_risk_domain(domain)
|
||||
payload = _model_payload(model, data)
|
||||
payload = _model_payload(domain, model, data)
|
||||
record = model(**payload)
|
||||
self.db.add(record)
|
||||
if high_risk:
|
||||
@@ -176,7 +183,7 @@ class BusinessService:
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=BusinessErrorDetail.RECORD_NOT_FOUND,
|
||||
)
|
||||
payload = _model_payload(model, data)
|
||||
payload = _model_payload(domain, model, data)
|
||||
if high_risk:
|
||||
self._consume_approval(
|
||||
approval_ticket_id,
|
||||
|
||||
Reference in New Issue
Block a user