按 2026-11-02 需求补齐计划交付后端能力: - 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝; 管理员判断改为按数据库角色重新判定,防止权限码持有者绕过 - 系统监控:单进程采集资源、请求与异常指标,分钟级聚合, 阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义 - 试用期计提:按回款日判定并写入快照,年度结清与调整单联动; 人员模型补试用期、离职日期与工资版本历史字段 - 计提模板:公式骨架受控,页面维护名称与参数绑定 - T+ 采集:自动化配置表与进度查询 内置角色禁停用保护对齐模块注释;核算规则版本提为常量并 补充批量核算失败堆栈日志。 影响范围:后端全部模块、数据库迁移 V22-V28 验证:uv run ruff check 通过;922 个单元测试全部通过; 集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
112 lines
4.2 KiB
Python
112 lines
4.2 KiB
Python
"""产品管理的请求/响应模型。
|
||
|
||
数量与单价(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
|
||
source_trade_json: dict | None = None
|
||
quantity_price_manual: int = 0
|
||
|
||
|
||
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="状态筛选")
|
||
|
||
|
||
class ProductExtractIn(BaseModel):
|
||
item_ids: list[int] = Field(min_length=1, max_length=200)
|
||
fingerprint: str | None = Field(default=None, min_length=64, max_length=64)
|
||
|
||
@field_validator("item_ids")
|
||
@classmethod
|
||
def _ids(cls, value):
|
||
if any(v <= 0 for v in value) or len(value) != len(set(value)):
|
||
raise ValueError("明细ID必须为不重复的正整数")
|
||
return value
|