PayrollManagementSystem/backend/app/services/no_generator.py
liuwenxin 64c57e9141 fix(no_generator): 编号 MAX 检索改长度优先排序,破解位宽进位后字典序失效
跨位宽(-9999→-10000)后纯字典序反转,MAX 恒取旧值导致当日建单
持续撞唯一键;同前缀下长度大即数值大,同级同长时字典序与数值一致。
新增跨位宽/同宽/空表三个集成用例。

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

50 lines
2.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.

"""单据编号生成器(业务 YW / 发货 SH / 回款 HK 共用)。
规则:``{前缀}{YYYYMMDD}-{当日4位序号}``,序号按自然日重置。
并发安全:``SELECT MAX(no) ... FOR UPDATE`` 锁住当日序号区间 —— 后到的
事务会等待先到者提交后再读到新 MAX,序号单调不重;残余竞态(锁等待超时
等极端场景)由服务层捕唯一键冲突(1062)重试一次兜底。
"""
from datetime import datetime
from sqlalchemy import func, select
from sqlalchemy.orm import Session
def today_prefix(prefix: str) -> str:
"""当日编号前缀(如 YW20261013-),LIKE 匹配与拼号共用同一来源防漂移。"""
return f"{prefix}{datetime.now().strftime('%Y%m%d')}-"
def next_no(
session: Session,
*,
model: type,
column_name: str,
prefix: str,
seq_width: int = 4,
) -> str:
"""生成下一个编号。
:param model: 编号所在的 ORM 模型(businesses/shipments/receipts)
:param column_name: 编号列名(business_no/shipment_no/receipt_batch_no)
:param prefix: 单据前缀(NO_PREFIX_* 常量)
:param seq_width: 序号位数(默认 4);超出宽度的序号按实际位数输出,
VARCHAR(32) 余量充足、唯一性不受影响
"""
like_prefix = today_prefix(prefix)
column = getattr(model, column_name)
# 排序按「长度优先、同级字典序」:同前缀下序号位数增多(9999→10000)后
# 纯字典序会反转("-10000" < "-9999",'1'<'9'),seq 将恒重算出同一号码、
# 每笔建单都撞唯一键 —— 长度大即数值大,同级同长时字典序与数值序一致
max_no = session.execute(
select(column)
.where(column.like(f"{like_prefix}%"))
.order_by(func.length(column).desc(), column.desc())
.limit(1)
.with_for_update()
).scalar()
seq = int(max_no.rsplit("-", 1)[1]) + 1 if max_no else 1
return f"{like_prefix}{seq:0{seq_width}d}"