132 lines
5.2 KiB
Python
132 lines
5.2 KiB
Python
"""客户评级等级配置的请求/响应模型。
|
||
|
||
分值(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="评分")
|