PayrollManagementSystem/backend/app/schemas/rating.py

132 lines
5.2 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):
"""编辑评级等级。
前端表单全量提交,故为全量替换语义;等级表无启停状态列,字段与新增一致。
"""
class CustomerRatingOut(BaseModel):
"""客户评级响应(每客户一行;总分与分值按红线以字符串出参)。
grade_name 是保存时点按等级映射**快照固化**的值(无外键):等级表后续
增删改不影响已保存的评级结论;为 NULL 表示保存时未命中任何等级
(总分低于全部下限或等级表被清空)。
"""
customer_id: int
score_ship_amount: str
score_receipt_rate: str
score_profit_rate: str
score_contract_cooperation: str
score_site_construction: str
total_score: str
grade_name: str | None = None
remark: str | None = None
scorer_name: str
create_time: datetime
update_time: datetime
class CustomerRatingSaveIn(BaseModel):
"""保存客户评级(每客户一行,整体替换语义)。
五项分值为 0-20 的数字字符串(schema 只拦格式;0≤分≤20 与总分等级
判定在 service 层审计上下文内完成 —— 失败的 400 同样留痕)。总分由
服务端按五项重算,**不采信前端回传的总分**。
"""
model_config = ConfigDict(str_strip_whitespace=True)
score_ship_amount: str = Field(description="发货额评分(0-20 数字字符串)")
score_receipt_rate: str = Field(description="回款率评分(0-20 数字字符串)")
score_profit_rate: str = Field(description="毛利率评分(0-20 数字字符串)")
score_contract_cooperation: str = Field(description="合同履约配合能力评分(0-20 数字字符串)")
score_site_construction: str = Field(description="现场规范施工能力评分(0-20 数字字符串)")
remark: str | None = Field(default=None, max_length=255, description="备注")
@field_validator(
"score_ship_amount",
"score_receipt_rate",
"score_profit_rate",
"score_contract_cooperation",
"score_site_construction",
)
@classmethod
def _check_score(cls, value: str) -> str:
"""单项分值须为有限数字(值域 0-20 由 service 层判定)。"""
return _parse_score(value, label="评分")