Files
zkxw/backend/app/crud/biz_no_generator.py
T
2026-10-14 12:32:03 +08:00

148 lines
4.2 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.
"""业务协议号自动生成:XBCC / JZCC + YYYYMMDD + 6 位序号"""
from datetime import datetime
from typing import Literal, Optional
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.crud.protocol_donation_convert import assert_biz_no_available
from app.models.models import BizBizNoSequence
PREFIX_COLLECTION = "XBCC"
PREFIX_DONATION = "JZCC"
SEQ_DIGITS = 6
MAX_SEQ = 10**SEQ_DIGITS - 1
BizNoKind = Literal["collection", "donation"]
def _prefix_for(kind: BizNoKind) -> str:
return PREFIX_COLLECTION if kind == "collection" else PREFIX_DONATION
def _format_biz_no(prefix: str, biz_date: str, seq: int) -> str:
return f"{prefix}{biz_date}{seq:0{SEQ_DIGITS}d}"
def _allocate_sqlite_sequence(
db: Session,
*,
tenant_no: str,
prefix: str,
biz_date: str,
user_id: str,
now: datetime,
) -> int:
"""SQLite 测试环境使用 ORM 分配;生产 MySQL 仍走原子 SQL。"""
row = db.query(BizBizNoSequence).filter(
BizBizNoSequence.tenant_no == tenant_no,
BizBizNoSequence.prefix == prefix,
BizBizNoSequence.biz_date == biz_date,
BizBizNoSequence.del_flag == 0,
).first()
if row:
row.last_seq = int(row.last_seq or 0) + 1
row.update_by = user_id
row.update_time = now
else:
row = BizBizNoSequence(
id=generate_uuid(),
tenant_no=tenant_no,
prefix=prefix,
biz_date=biz_date,
last_seq=1,
order_num=0,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(row)
db.flush()
return int(row.last_seq)
def allocate_next_biz_no(
db: Session,
*,
tenant_no: str,
kind: BizNoKind,
user_id: Optional[str] = None,
biz_date: Optional[datetime] = None,
) -> str:
"""
原子分配下一个业务协议号。
须在 create 事务内调用;使用 MySQL LAST_INSERT_ID 保证并发安全。
"""
prefix = _prefix_for(kind)
now = biz_date or datetime.now()
date_str = now.strftime("%Y%m%d")
uid = user_id or ""
dialect_name = getattr(getattr(db.get_bind(), "dialect", None), "name", "")
if dialect_name == "sqlite":
seq_int = _allocate_sqlite_sequence(
db,
tenant_no=tenant_no,
prefix=prefix,
biz_date=date_str,
user_id=uid,
now=now,
)
if seq_int > MAX_SEQ:
raise ValueError(f"当日{prefix}协议号序号已用尽(最大 {MAX_SEQ})")
biz_no = _format_biz_no(prefix, date_str, seq_int)
assert_biz_no_available(
db,
tenant_no=tenant_no,
biz_no=biz_no,
for_donation=(kind == "donation"),
)
return biz_no
db.execute(
text(
"""
INSERT INTO biz_biz_no_sequence (
id, tenant_no, prefix, biz_date, last_seq,
order_num, remark, create_by, create_dept_id, create_time,
update_by, update_time, del_flag
) VALUES (
:id, :tenant_no, :prefix, :biz_date, LAST_INSERT_ID(1),
0, NULL, :user_id, '', :now,
:user_id, :now, 0
)
ON DUPLICATE KEY UPDATE
last_seq = LAST_INSERT_ID(last_seq + 1),
update_by = :user_id,
update_time = :now
"""
),
{
"id": generate_uuid(),
"tenant_no": tenant_no,
"prefix": prefix,
"biz_date": date_str,
"user_id": uid,
"now": now,
},
)
seq = db.execute(text("SELECT LAST_INSERT_ID()")).scalar()
if seq is None or int(seq) < 1:
raise ValueError("协议号序号分配失败")
seq_int = int(seq)
if seq_int > MAX_SEQ:
raise ValueError(f"当日{prefix}协议号序号已用尽(最大 {MAX_SEQ})")
biz_no = _format_biz_no(prefix, date_str, seq_int)
assert_biz_no_available(
db,
tenant_no=tenant_no,
biz_no=biz_no,
for_donation=(kind == "donation"),
)
return biz_no