"""产品管理的请求/响应模型。 数量与单价(stock_quantity/sale_price)入参出参一律用**字符串**承载: 前端 JS 的 number 无法精确表示全部十进制小数,字符串是金额红线的入口闸门, Decimal 转换统一收敛在 service 层。字段命名与 user/role/param 一致采用 蛇形直出(前端 TS 接口同构)。 """ from datetime import datetime from decimal import Decimal, InvalidOperation from pydantic import BaseModel, ConfigDict, Field, field_validator from app.core.response import PageQuery def _parse_non_negative(value: str, *, label: str) -> str: """非负数字字符串校验:必须可被 Decimal 解析、为有限数且 ≥ 0。 ``NaN`` 参与比较会抛 InvalidOperation、``Infinity`` 会在后续运算溢出, 均须在入口用 ``is_finite()`` 拦下(与 schemas/param.py 的 _parse_amount 同一口径)。 """ try: amount = Decimal(value.strip()) except InvalidOperation as exc: raise ValueError(f"{label}必须是有效数字:{value}") from exc if not amount.is_finite(): raise ValueError(f"{label}必须为有限数字:{value}") if amount < 0: raise ValueError(f"{label}不得为负数:{value}") return value class ProductOut(BaseModel): """产品响应(列表与详情共用)。数量/单价按红线以字符串出参。""" id: int product_name: str spec_model: str unit: str | None = None stock_quantity: str | None = None sale_price: str | None = None product_category: str status: int = 1 remark: str | None = None create_time: datetime update_time: datetime class ProductCreateIn(BaseModel): """新增产品。 spec_model 允许缺省/空:service 层归一为空串(组合唯一键 uk_products_name_spec 对 NULL 不去重,归 NULL 会放行重名同规格数据)。 """ model_config = ConfigDict(str_strip_whitespace=True) product_name: str = Field(min_length=1, max_length=128, description="产品名称") spec_model: str | None = Field(default=None, max_length=128, description="规格型号,空归一为空串") unit: str | None = Field(default=None, max_length=16, description="计量单位") stock_quantity: str | None = Field(default=None, description="数量,非负数字字符串") sale_price: str | None = Field(default=None, description="销售单价(元),非负数字字符串") product_category: str = Field(min_length=1, max_length=32, description="产品类别") remark: str | None = Field(default=None, max_length=255) @field_validator("stock_quantity") @classmethod def _check_stock_quantity(cls, value: str | None) -> str | None: """空串与 None 同义(可空列),归一为 None;非空则须为非负数字。""" if not value: return None return _parse_non_negative(value, label="数量") @field_validator("sale_price") @classmethod def _check_sale_price(cls, value: str | None) -> str | None: """空串与 None 同义(可空列),归一为 None;非空则须为非负数字。""" if not value: return None return _parse_non_negative(value, label="销售单价") class ProductUpdateIn(ProductCreateIn): """编辑产品。 前端表单全量提交,故为全量替换语义;**不含 status** —— 启用/停用是 独立操作(走 PUT /{id}/status),避免编辑表单误触停用。 """ class ProductStatusIn(BaseModel): """启用/停用产品。""" status: int = Field(ge=0, le=1, description="1启用 0停用") class ProductQuery(PageQuery): """产品列表查询条件。""" keyword: str | None = Field(default=None, max_length=64, description="名称或规格,模糊匹配") product_category: str | None = Field(default=None, max_length=32, description="按类别筛选") status: int | None = Field(default=None, ge=0, le=1, description="状态筛选")