Files
dpb/backend/app/utils/number_gen.py
T
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

62 lines
2.7 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.
"""统一编号服务 —— 原子取号,替代各处分散扫描式生成(§3.3 编号约定)。
取号基于 bre_sequencename/current_seq)表 + PostgreSQL 事务级 advisory lock
pg_advisory_xact_lock 在事务内串行化同名取号,锁随事务提交/回滚自动释放,
与 CRUD 同事务提交 ⇒ 并发请求不重复、失败自动回滚不留空号。
存量数据迁移:首次使用某序列名时由 seed_fn(服务端传入)从既有记录推算
起始号(原扫描逻辑),此后全部走原子递增,不再扫描。
"""
from typing import Any, 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:
from app.config.setting import settings
if settings.DATABASE_TYPE != "postgres":
return # sqlite 测试库无 advisory lock,退化为普通自增(单进程无并发竞争)
# 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