feat(approval): 审批单内容快照与二次分配方案版本化

补齐 code-review 遗留 2 项:审批单此前只固化 ref_id/refer_no 指针,复核者看到
的是业务当前内容;二次分配方案在编辑替换与驳回清空时一律物理删除,方案事实
无处回溯。

- 审批单加 version_no 与 snapshot_json(信封 schema_version/order_type/
  captured_at/data);二次分配、月结、调整单三类提交均固化送审内容,开关关闭
  的自动通过路径同样写快照;详情按 snapshot/live/missing 三态回显,历史单
  只读回退当前内容并显式告警,不回填历史快照
- 二次分配方案行改 append-only 版本化(version_no/is_active,唯一键扩为
  business+version+personnel):编辑替换与驳回清空只置历史,核算与详情默认
  只取生效行,业务详情新增 allocation_history
- 送审内容未变不重提、方案未变不新增版本:判据统一为送审内容,两个
  version_no 语义解耦(方案版本=内容第几版,审批单版本=第几次送审)
- 审批单列表、工作台待办、审批超时预警三处 defer 快照列,避免整列拉取
- 月结提交取行加 for_update,防并发提交算出重复的审批单版本号
- 修复审批类型筛选上界 le=2(调整单无法筛选)、快照金额把空值写成 0.00
- V11 迁移三件套同步(Alembic / db/migration / init-dev.sql)

测试:单元 +16(方案内容比较 10、审批入参 6)、集成 +12;全量 891 passed。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-20 18:30:51 +08:00
1 parent 2e0c64849b
commit 4c32a556de
28 files changed
+1458 -76

No files matched your search

+15 -2
View File
@@ -1,6 +1,7 @@
"""审批管理的请求/响应模型(2026-10-15 复核审批补充需求)。"""
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
@@ -31,6 +32,7 @@ class ApprovalOrderOut(BaseModel):
order_type: int
ref_id: int
refer_no: str
version_no: int
status: int
submitter_id: int | None = None
submitter_name: str
@@ -38,9 +40,20 @@ class ApprovalOrderOut(BaseModel):
class ApprovalOrderDetailOut(ApprovalOrderOut):
"""审批单详情:附节点时间线。"""
"""审批单详情:附节点时间线与审批内容。
``content_source`` 语义:``snapshot``=提交时快照(可信);
``live``=历史单无快照、回退读当前业务内容(仅供参考);
``missing``=引用对象已不存在且无快照。
"""
steps: list[ApprovalStepOut] = Field(default_factory=list)
snapshot: dict[str, Any] | None = Field(
default=None, description="提交时内容快照(信封:schema_version/order_type/captured_at/data)"
)
content_source: Literal["snapshot", "live", "missing"] = Field(
default="missing", description="审批内容来源:提交时快照/当前内容回退/缺失"
)
class ApprovalQuery(PageQuery):
@@ -48,7 +61,7 @@ class ApprovalQuery(PageQuery):
keyword: str | None = Field(default=None, max_length=32, description="审批单号/引用单号,模糊")
status: int | None = Field(default=None, ge=1, le=5, description="状态筛选")
order_type: int | None = Field(default=None, ge=1, le=2, description="对象类型筛选")
order_type: int | None = Field(default=None, ge=1, le=3, description="对象类型筛选")
class ApprovalActionIn(BaseModel):
+12 -1
View File
@@ -180,6 +180,14 @@ class SecondaryAllocationOut(BaseModel):
ratio: str
class AllocationVersionOut(SecondaryAllocationOut):
"""方案版本行(含历史;业务详情抽屉回溯用)。"""
version_no: int
is_active: int = Field(description="1当前生效 0历史版本")
create_time: datetime
def _ensure_allocations_unique(items: list["SecondaryAllocationIn"]) -> None:
"""同一业务内参与人去重:uk(business_id, personnel_id) 的前置友好提示
(让 400 文案说「重复参与人」而不是裸唯一键冲突)。"""
@@ -191,13 +199,16 @@ def _ensure_allocations_unique(items: list["SecondaryAllocationIn"]) -> None:
class BusinessDetailOut(BusinessOut):
"""业务详情:附产品明细行与二次分配方案。"""
"""业务详情:附产品明细行、当前方案与方案版本历史。"""
items: list[BusinessItemOut] = Field(default_factory=list)
secondary_allocations: list[SecondaryAllocationOut] = Field(default_factory=list)
allocation_hint: str | None = Field(
default=None, description="方案合计与余额提示(余额归属待澄清#2,仅展示)"
)
allocation_history: list[AllocationVersionOut] = Field(
default_factory=list, description="方案全部版本(含历史;被驳回方案亦保留)"
)
class BusinessCreateIn(BaseModel):