PayrollManagementSystem/backend/app/schemas/rating.py
liuwenxin 1625907380 feat(business): 客户评级等级映射配置接口(区间重叠判定/名称唯一)
- 判定=按 sort_order 升序首个 min_score≤总分,边界相邻不算重叠
- 新增同样执行重叠判定(判定表语义下插出重叠即脏配置)
- 等级配置无下游引用(评级为纯计算器,对照表 #25),允许清空

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 12:01:47 +08:00

80 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""客户评级等级配置的请求/响应模型。
分值(min_score/max_score)入参出参一律用**字符串**承载:前端 JS 的 number
无法精确表示全部十进制小数,字符串是分值红线的入口闸门,Decimal 转换统一
收敛在 service 层(与 schemas/product.py 同一口径)。
注意:schema 只拦**格式**(可解析、有限数);0≤min≤max≤100 与区间不重叠
是横跨两字段/多行的业务值域规则,在 service 层审计上下文内统一判定 ——
值域校验失败的 400 也要留痕(2026-10-13 定稿口径)。
"""
from datetime import datetime
from decimal import Decimal, InvalidOperation
from pydantic import BaseModel, ConfigDict, Field, field_validator
def _parse_score(value: str, *, label: str) -> str:
"""分值字符串格式校验:必须可被 Decimal 解析且为有限数。
``NaN`` 参与比较抛 InvalidOperation、``Infinity`` 落库即溢出,均须在
入口用 ``is_finite()`` 拦下(与 schemas/product.py 的 _parse_non_negative
同口径);非负与 ≤100 属业务值域,留给 service 层在审计上下文内判定。
"""
try:
score = Decimal(value.strip())
except InvalidOperation as exc:
raise ValueError(f"{label}必须是有效数字:{value}") from exc
if not score.is_finite():
raise ValueError(f"{label}必须为有限数字:{value}")
return value
class RatingGradeOut(BaseModel):
"""评级等级响应(列表直出)。分值按红线以字符串出参。"""
id: int
grade_name: str
min_score: str
max_score: str
sort_order: int
remark: str | None = None
create_time: datetime
update_time: datetime
class RatingGradeCreateIn(BaseModel):
"""新增评级等级。
min_score/max_score 必填:区间是判定表的判定依据,缺省无从判定。
sort_order 缺省 0(列默认值同源),判定按其升序取首个命中等级。
"""
model_config = ConfigDict(str_strip_whitespace=True)
grade_name: str = Field(min_length=1, max_length=32, description="等级名称(如 A/优秀)")
min_score: str = Field(description="总分下限(含),0-100 数字字符串")
max_score: str = Field(description="总分上限(展示用),0-100 数字字符串")
sort_order: int = Field(default=0, ge=0, description="判定顺序,升序")
remark: str | None = Field(default=None, max_length=255, description="说明")
@field_validator("min_score")
@classmethod
def _check_min_score(cls, value: str) -> str:
"""总分下限须为有限数字(值域 0-100 由 service 层判定)。"""
return _parse_score(value, label="总分下限")
@field_validator("max_score")
@classmethod
def _check_max_score(cls, value: str) -> str:
"""总分上限须为有限数字(值域 0-100 由 service 层判定)。"""
return _parse_score(value, label="总分上限")
class RatingGradeUpdateIn(RatingGradeCreateIn):
"""编辑评级等级。
前端表单全量提交,故为全量替换语义;等级表无启停状态列,字段与新增一致。
"""