"""统一编号服务 —— 原子取号,替代各处分散扫描式生成(§3.3 编号约定)。 取号基于 bre_sequence(name/current_seq)表 + PostgreSQL 事务级 advisory lock: pg_advisory_xact_lock 在事务内串行化同名取号,锁随事务提交/回滚自动释放, 与 CRUD 同事务提交 ⇒ 并发请求不重复、失败自动回滚不留空号。 存量数据迁移:首次使用某序列名时由 seed_fn(服务端传入)从既有记录推算 起始号(原扫描逻辑),此后全部走原子递增,不再扫描。 """ from collections.abc import Awaitable, Callable from sqlalchemy import select, text from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1.module_bre.sequence.model import SequenceModel class NumberGenService: """原子序号发生器。实例化于 HTTP/服务层,随当前事务取号。""" def __init__(self, db: AsyncSession) -> None: self.db = db @staticmethod async def _acquire_lock(db: AsyncSession, name: str) -> None: # hashtextextended 对文本做稳定 64 位哈希,同名序列恒映射同一把锁 await db.execute(text("SELECT pg_advisory_xact_lock(hashtextextended(:n, 0))"), {"n": name}) async def next_seq( self, name: str, *, start: int = 1, seed_fn: Callable[[], Awaitable[int | None]] | None = None, ) -> int: """原子取号。返回本序列名下一个应使用的序号。 name: 序列名(如 "combination:2026" / "tree:3"),按编号维度天然分片; start: 新序列的默认起始值; seed_fn: 仅当序列行尚不存在时调用,从存量数据推算起始号(兼容既有数据迁移)。 """ await self._acquire_lock(self.db, name) row = await self.db.scalar(select(SequenceModel).where(SequenceModel.name == name)) if row is None: init = await seed_fn() if seed_fn is not None else start if init is None or init < 1: init = start self.db.add(SequenceModel(name=name, current_seq=init)) await self.db.flush() return init row.current_seq += 1 await self.db.flush() return int(row.current_seq) async def exists(self, name: str) -> bool: """序列行是否已初始化(供服务端判断是否需要 seed_fn 兜底)。""" return (await self.db.scalar(select(SequenceModel.id).where(SequenceModel.name == name))) is not None