跨位宽(-9999→-10000)后纯字典序反转,MAX 恒取旧值导致当日建单 持续撞唯一键;同前缀下长度大即数值大,同级同长时字典序与数值一致。 新增跨位宽/同宽/空表三个集成用例。 Co-Authored-By: Claude Code <noreply@anthropic.com>
50 lines
2.0 KiB
Python
50 lines
2.0 KiB
Python
"""单据编号生成器(业务 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}"
|