2026-10-12 22:35:03 -04:00
|
|
|
|
"""产品管理的请求/响应模型。
|
|
|
|
|
|
|
|
|
|
|
|
数量与单价(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
|
2026-10-14 06:48:32 -04:00
|
|
|
|
from app.schemas.business import AMOUNT_MAX, PRICE_MAX, _check_amount
|
2026-10-12 22:35:03 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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="产品名称")
|
2026-10-12 23:22:34 -04:00
|
|
|
|
spec_model: str | None = Field(
|
|
|
|
|
|
default=None, max_length=128, description="规格型号,空归一为空串"
|
|
|
|
|
|
)
|
2026-10-12 22:35:03 -04:00
|
|
|
|
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:
|
2026-10-14 06:48:32 -04:00
|
|
|
|
"""空串与 None 同义(可空列),归一为 None;非空走通用金额校验。
|
|
|
|
|
|
|
|
|
|
|
|
复用 _check_amount:容量/小数位与 DECIMAL(14,2) 列口径一致
|
|
|
|
|
|
(此前私写的 _parse_non_negative 不查小数位与容量,属同语义
|
|
|
|
|
|
字段两处口径漂移)。
|
|
|
|
|
|
"""
|
2026-10-12 22:35:03 -04:00
|
|
|
|
if not value:
|
|
|
|
|
|
return None
|
2026-10-14 06:48:32 -04:00
|
|
|
|
return _check_amount(value, "数量", AMOUNT_MAX)
|
2026-10-12 22:35:03 -04:00
|
|
|
|
|
|
|
|
|
|
@field_validator("sale_price")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _check_sale_price(cls, value: str | None) -> str | None:
|
2026-10-14 06:48:32 -04:00
|
|
|
|
"""空串与 None 同义(可空列),归一为 None;非空走通用金额校验。
|
|
|
|
|
|
|
|
|
|
|
|
scale=4 与列口径 Numeric(14,4) 一致 —— 带精度的历史单价必须能
|
|
|
|
|
|
原样提交(「编辑任何带精度历史行都失败」的 fba8cfc 教训)。
|
|
|
|
|
|
"""
|
2026-10-12 22:35:03 -04:00
|
|
|
|
if not value:
|
|
|
|
|
|
return None
|
2026-10-14 06:48:32 -04:00
|
|
|
|
return _check_amount(value, "销售单价", PRICE_MAX, scale=4)
|
2026-10-12 22:35:03 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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="状态筛选")
|