"""产品管理的请求/响应模型。 数量与单价(stock_quantity/sale_price)入参出参一律用**字符串**承载: 前端 JS 的 number 无法精确表示全部十进制小数,字符串是金额红线的入口闸门, Decimal 转换统一收敛在 service 层。字段命名与 user/role/param 一致采用 蛇形直出(前端 TS 接口同构)。 """ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, field_validator from app.core.response import PageQuery from app.schemas.business import AMOUNT_MAX, PRICE_MAX, _check_amount 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;非空走通用金额校验。 复用 _check_amount:容量/小数位与 DECIMAL(14,2) 列口径一致 (此前私写的 _parse_non_negative 不查小数位与容量,属同语义 字段两处口径漂移)。 """ if not value: return None return _check_amount(value, "数量", AMOUNT_MAX) @field_validator("sale_price") @classmethod def _check_sale_price(cls, value: str | None) -> str | None: """空串与 None 同义(可空列),归一为 None;非空走通用金额校验。 scale=4 与列口径 Numeric(14,4) 一致 —— 带精度的历史单价必须能 原样提交(「编辑任何带精度历史行都失败」的 fba8cfc 教训)。 """ if not value: return None return _check_amount(value, "销售单价", PRICE_MAX, scale=4) 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="状态筛选")