按 2026-11-02 需求补齐计划交付后端能力: - 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝; 管理员判断改为按数据库角色重新判定,防止权限码持有者绕过 - 系统监控:单进程采集资源、请求与异常指标,分钟级聚合, 阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义 - 试用期计提:按回款日判定并写入快照,年度结清与调整单联动; 人员模型补试用期、离职日期与工资版本历史字段 - 计提模板:公式骨架受控,页面维护名称与参数绑定 - T+ 采集:自动化配置表与进度查询 内置角色禁停用保护对齐模块注释;核算规则版本提为常量并 补充批量核算失败堆栈日志。 影响范围:后端全部模块、数据库迁移 V22-V28 验证:uv run ruff check 通过;922 个单元测试全部通过; 集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
237 lines
7.4 KiB
Python
237 lines
7.4 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":
|
||
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="按维度值筛选,空为全部"
|
||
)
|