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>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-14 18:48:32 +08:00
1 parent 9a70264b51
commit 91fc6f6293
11 files changed
+243 -40

No files matched your search

+14 -23
View File
@@ -7,29 +7,11 @@ Decimal 转换统一收敛在 service 层。字段命名与 user/role/param 一
"""
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
from app.schemas.business import AMOUNT_MAX, PRICE_MAX, _check_amount
class ProductOut(BaseModel):
@@ -70,18 +52,27 @@ class ProductCreateIn(BaseModel):
@field_validator("stock_quantity")
@classmethod
def _check_stock_quantity(cls, value: str | None) -> str | None:
"""空串与 None 同义(可空列),归一为 None;非空则须为非负数字。"""
"""空串与 None 同义(可空列),归一为 None;非空走通用金额校验。
复用 _check_amount:容量/小数位与 DECIMAL(14,2) 列口径一致
(此前私写的 _parse_non_negative 不查小数位与容量,属同语义
字段两处口径漂移)。
"""
if not value:
return None
return _parse_non_negative(value, label="数量")
return _check_amount(value, "数量", AMOUNT_MAX)
@field_validator("sale_price")
@classmethod
def _check_sale_price(cls, value: str | None) -> str | None:
"""空串与 None 同义(可空列),归一为 None;非空则须为非负数字。"""
"""空串与 None 同义(可空列),归一为 None;非空走通用金额校验。
scale=4 与列口径 Numeric(14,4) 一致 —— 带精度的历史单价必须能
原样提交(「编辑任何带精度历史行都失败」的 fba8cfc 教训)。
"""
if not value:
return None
return _parse_non_negative(value, label="销售单价")
return _check_amount(value, "销售单价", PRICE_MAX, scale=4)
class ProductUpdateIn(ProductCreateIn):