- 信息员预览基数改用本人销售额(原误用折算毛利润额,金额算错且口径自相矛盾) - 产品类别入参与维度值加存在性校验(拼错静默回退默认阈值 30% 会算错轨交工资) - submit 校验顺序前置查库(未注册 param_code 由 500 修正为 400) - 金额拒绝 NaN/Infinity(比较抛 InvalidOperation、计算抛溢出均穿透为 500) - version_no 并发冲突翻译为 400(对齐 user/role service 的 IntegrityError 处理) - 旧值留痕改取条目生效日时点的值(多未来批次下留痕链不再错位) - 脱敏红线:phone/mobile/id_card 键名整值抹除 + 手机号正则兼容分隔符写法 - 前端:撤回按钮与导出文件名改本地日期(UTC/本地混算)、维度行内精确「改」入口、 detailOf 补 catch、抽屉分页 size 生效、blob 错误读出真实 message - e2e 密码移除硬编码 fallback(缺失即报错退出);按钮文案同步「改」 - 全量回归:后端 406 + 集成 104 + 前端 type-check/build + e2e 7 步 全绿 Co-Authored-By: Claude Code <noreply@anthropic.com>
239 lines
7.5 KiB
Python
239 lines
7.5 KiB
Python
"""计提参数管理的请求/响应模型。
|
||
|
||
字段命名与 user/role 一致采用蛇形直出(前端 TS 接口同构)。
|
||
金额类入参用字符串承载(Decimal 解析),杜绝 float 精度损失。
|
||
"""
|
||
|
||
from datetime import date, datetime
|
||
from decimal import Decimal, InvalidOperation
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||
|
||
from app.core.response import PageQuery
|
||
|
||
|
||
def _parse_amount(value: str, *, field_name: str, allow_negative: bool) -> str:
|
||
"""金额字符串校验:必须可被 Decimal 解析;默认拒绝负数(毛利润额除外)。"""
|
||
try:
|
||
amount = Decimal(value.strip())
|
||
except InvalidOperation as exc:
|
||
raise ValueError(f"{field_name}必须是有效数字:{value}") from exc
|
||
if not amount.is_finite():
|
||
# 'NaN' 的比较会抛 InvalidOperation、'Infinity' 会在后续计算抛溢出,均须在入口拦下
|
||
raise ValueError(f"{field_name}必须为有限数字:{value}")
|
||
if not allow_negative and amount < 0:
|
||
raise ValueError(f"{field_name}不得为负数:{value}")
|
||
return value
|
||
|
||
|
||
class ParamDimensionValueOut(BaseModel):
|
||
"""某参数单个维度的当前生效值。"""
|
||
|
||
dimension_value: str
|
||
value_text: str
|
||
effective_date: date | None = None
|
||
|
||
|
||
class ParamCatalogItemOut(BaseModel):
|
||
"""参数目录项:定义元数据 + 各维度截至查询时点的生效值。"""
|
||
|
||
param_code: str
|
||
param_name: str
|
||
param_group: str
|
||
value_type: str
|
||
unit: str | None = None
|
||
dimension_type: str
|
||
options: list[str] = Field(default_factory=list)
|
||
default_value: str | None = None
|
||
value_min: str | None = None
|
||
value_max: str | None = None
|
||
description: str | None = None
|
||
values: list[ParamDimensionValueOut] = Field(default_factory=list)
|
||
|
||
|
||
class ParamHistoryOut(BaseModel):
|
||
"""单参数的版本历史条目(append-only 留痕)。"""
|
||
|
||
version_no: str
|
||
dimension_value: str
|
||
old_value_text: str | None = None
|
||
value_text: str
|
||
effective_date: date
|
||
operator_name: str
|
||
create_time: datetime
|
||
|
||
|
||
class ParamChangeItemIn(BaseModel):
|
||
"""变更条目:参数 + 维度 + 新值。"""
|
||
|
||
model_config = ConfigDict(str_strip_whitespace=True)
|
||
|
||
param_code: str = Field(min_length=1, max_length=64)
|
||
dimension_value: str = Field(default="", max_length=32)
|
||
value: str = Field(min_length=1, max_length=64)
|
||
|
||
|
||
class ParamChangeIn(BaseModel):
|
||
"""提交变更批次。
|
||
|
||
change_type=1 立即生效(生效日期取今日);change_type=2 指定日期生效,
|
||
生效日期必填且不得早于今日(过去没有「预约」的意义,历史回溯走重算授权)。
|
||
"""
|
||
|
||
model_config = ConfigDict(str_strip_whitespace=True)
|
||
|
||
change_type: int = Field(ge=1, le=2, description="生效方式:1立即生效 2指定日期生效")
|
||
effective_date: date | None = Field(default=None, description="指定日期生效时必填")
|
||
remark: str | None = Field(default=None, max_length=255)
|
||
items: list[ParamChangeItemIn] = Field(min_length=1, max_length=50)
|
||
|
||
@model_validator(mode="after")
|
||
def _check_effective_date(self) -> "ParamChangeIn":
|
||
if self.change_type == 2 and self.effective_date is None:
|
||
raise ValueError("指定日期生效时必须提供生效日期")
|
||
return self
|
||
|
||
|
||
class ParamChangeBatchOut(BaseModel):
|
||
"""变更批次(提交/撤回/列表共用)。"""
|
||
|
||
id: int
|
||
version_no: str
|
||
effective_date: date
|
||
change_type: int
|
||
remark: str | None = None
|
||
operator_name: str
|
||
item_count: int
|
||
create_time: datetime
|
||
|
||
|
||
class ParamBatchItemOut(BaseModel):
|
||
"""批次明细条目(旧值 → 新值)。"""
|
||
|
||
param_code: str
|
||
param_name: str
|
||
dimension_value: str
|
||
old_value_text: str | None = None
|
||
value_text: str
|
||
effective_date: date
|
||
|
||
|
||
class ParamBatchOut(BaseModel):
|
||
"""变更批次(列表项)。"""
|
||
|
||
id: int
|
||
version_no: str
|
||
effective_date: date
|
||
change_type: int
|
||
remark: str | None = None
|
||
operator_name: str
|
||
item_count: int
|
||
create_time: datetime
|
||
|
||
|
||
class ParamBatchDetailOut(BaseModel):
|
||
"""批次明细:批次头 + 逐项变更。"""
|
||
|
||
id: int
|
||
version_no: str
|
||
effective_date: date
|
||
change_type: int
|
||
remark: str | None = None
|
||
operator_name: str
|
||
item_count: int
|
||
create_time: datetime
|
||
items: list[ParamBatchItemOut] = Field(default_factory=list)
|
||
|
||
|
||
class ParamDiffOut(BaseModel):
|
||
"""两日期快照对比项(全量参数,changed 标记是否变化)。"""
|
||
|
||
param_code: str
|
||
param_name: str
|
||
param_group: str
|
||
dimension_value: str
|
||
old_value: str | None = None
|
||
new_value: str | None = None
|
||
changed: bool
|
||
|
||
|
||
class ParamPreviewIn(BaseModel):
|
||
"""计算预览入参。金额用字符串承载,Decimal 解析。"""
|
||
|
||
model_config = ConfigDict(str_strip_whitespace=True)
|
||
|
||
business_role: str | None = Field(
|
||
default=None, max_length=32, description="岗位编码,如 salesperson"
|
||
)
|
||
customer_type: str | None = Field(
|
||
default=None, max_length=32, description="客户类型:personal个人户 enterprise企业户"
|
||
)
|
||
product_category: str | None = Field(
|
||
default=None, max_length=32, description="产品类别:municipal_water/rail_transit"
|
||
)
|
||
ship_date: date = Field(description="发货日")
|
||
receipt_date: date = Field(description="回款到账日")
|
||
revenue: str = Field(min_length=1, description="销售额(不含运费)")
|
||
profit: str = Field(min_length=1, description="毛利润额(可为负)")
|
||
received_amount: str = Field(min_length=1, description="本笔回款金额")
|
||
barter: bool = Field(default=False, description="是否以物抵顶")
|
||
probation: bool = Field(default=False, description="是否试用期")
|
||
|
||
@field_validator("revenue", "received_amount")
|
||
@classmethod
|
||
def _check_non_negative(cls, value: str, info) -> str:
|
||
return _parse_amount(value, field_name=info.field_name, allow_negative=False)
|
||
|
||
@field_validator("profit")
|
||
@classmethod
|
||
def _check_profit(cls, value: str, info) -> str:
|
||
return _parse_amount(value, field_name=info.field_name, allow_negative=True)
|
||
|
||
@model_validator(mode="after")
|
||
def _check_dates(self) -> "ParamPreviewIn":
|
||
if self.receipt_date < self.ship_date:
|
||
raise ValueError("回款到账日不得早于发货日")
|
||
return self
|
||
|
||
|
||
class ParamUsedOut(BaseModel):
|
||
"""预览实际用到的参数(对账透明:取值与生效时点)。"""
|
||
|
||
param_code: str
|
||
value_text: str
|
||
effective_date: date | None = None
|
||
|
||
|
||
class ParamPreviewOut(BaseModel):
|
||
"""计算预览出参。分数串(如 97/10000)为精确值,金额为 Decimal 串。"""
|
||
|
||
payment_days: int
|
||
rate: str
|
||
rate_text: str
|
||
profit_rate: str
|
||
converted_profit: str
|
||
performance_amount: str
|
||
barter_applied: bool
|
||
performance_after_barter: str | None = None
|
||
start_payment_ok: bool
|
||
payment_weight_r: int
|
||
payment_weight: str
|
||
payable_amount: str
|
||
probation_note: str | None = None
|
||
params_used: list[ParamUsedOut] = Field(default_factory=list)
|
||
|
||
|
||
class ParamBatchQuery(PageQuery):
|
||
"""变更记录查询条件。"""
|
||
|
||
start_date: date | None = Field(default=None, description="生效日期起(含)")
|
||
end_date: date | None = Field(default=None, description="生效日期止(含)")
|
||
|
||
|
||
class ParamHistoryQuery(PageQuery):
|
||
"""参数历史查询条件。"""
|
||
|
||
dimension_value: str | None = Field(
|
||
default=None, max_length=32, description="按维度值筛选,空为全部"
|
||
)
|