PayrollManagementSystem/backend/app/schemas/product.py
liuwenxin 7eeed85c96 feat(base): 产品管理接口(类别校验/组合唯一/启停)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 10:35:03 +08:00

105 lines
4.0 KiB
Python
Raw 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.

"""产品管理的请求/响应模型。
数量与单价(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="状态筛选")