PayrollManagementSystem/backend/app/schemas/product.py
liuwenxin 91fc6f6293 fix(product): 删除引用保护/单价小数位校验,IntegrityError 全局兜底转 400
- delete_product 补 count_refs 引用检查(业务明细+发货明细 RESTRICT
  外键)与并发窗口 IntegrityError→400 兜底(customer 先例同构),
  docstring 与两个 api 层过时注释同步更新
- 产品 schema 复用 schemas/business._check_amount:sale_price 走
  (PRICE_MAX, scale=4)、stock_quantity 走 AMOUNT_MAX——此前私写
  _parse_non_negative 不查小数位与容量,5 位小数被 Numeric(14,4)
  静默截断(舍入即算错钱);新增 6 个秒级 schema 单测锁定口径
- 全局注册 IntegrityError handler:漏补引用保护的删除类操作收敛为
  400 通用文案(e2e 撞见一个补一个的模式无法保证全覆盖),完整
  异常记日志;rating/customer 的「非 1062 按 500」注释同步更正

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-14 18:48:32 +08:00

98 lines
3.7 KiB
Python
Raw Permalink 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 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="状态筛选")