feat/m3-increments #1
@ -8,7 +8,7 @@
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
@ -35,10 +35,13 @@ def next_no(
|
||||
"""
|
||||
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(column.desc())
|
||||
.order_by(func.length(column).desc(), column.desc())
|
||||
.limit(1)
|
||||
.with_for_update()
|
||||
).scalar()
|
||||
|
||||
82
backend/tests/integration/test_no_generator.py
Normal file
82
backend/tests/integration/test_no_generator.py
Normal file
@ -0,0 +1,82 @@
|
||||
"""单据编号生成器的跨位宽正确性(no_generator)。
|
||||
|
||||
守:当日序号超过 4 位宽度(9999→10000)后,VARCHAR 字典序排序会失效
|
||||
("-10000" < "-9999",'1'<'9')——MAX 检索若按纯字典序,seq 会恒重算出
|
||||
同一号码,每笔建单都撞唯一键。修复口径:长度优先、同级按字典序
|
||||
(同前缀下长度大即数值大;同长时字典序与数值序一致)。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.constants import NO_PREFIX_BUSINESS
|
||||
from app.core.db import get_session_factory
|
||||
from app.models import Business, Customer
|
||||
from app.services.no_generator import next_no, today_prefix
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
def _insert_business_nos(nos: list[str]) -> None:
|
||||
"""直插同日前缀的业务编号行(仅编号用于 MAX 检索;其余列取默认/可空)。
|
||||
|
||||
直插绕过服务层(服务层走完整创建链太重),业务行为挂在 t_ 客户名下,
|
||||
由 integration/conftest 兜底清理(YW% 单据 + t\\_% 客户)。
|
||||
"""
|
||||
factory = get_session_factory()
|
||||
with factory() as session:
|
||||
customer = Customer(
|
||||
company_name="t_no_generator_client",
|
||||
owner_name="t_no_generator_owner",
|
||||
customer_type="enterprise",
|
||||
)
|
||||
session.add(customer)
|
||||
session.flush()
|
||||
session.add_all(Business(business_no=no, customer_id=customer.id) for no in nos)
|
||||
session.commit()
|
||||
|
||||
|
||||
class TestMaxCrossWidth:
|
||||
"""MAX 检索在同日前缀下跨位宽取最大序号。"""
|
||||
|
||||
def test_序号破四位宽后仍按数值取最大(self) -> None:
|
||||
"""4 位与 5 位并存:"-9999" 字典序大于 "-10000",纯字典序会取错。"""
|
||||
prefix = today_prefix(NO_PREFIX_BUSINESS)
|
||||
_insert_business_nos([f"{prefix}9999", f"{prefix}10000"])
|
||||
factory = get_session_factory()
|
||||
with factory() as session:
|
||||
no = next_no(
|
||||
session, model=Business, column_name="business_no", prefix=NO_PREFIX_BUSINESS
|
||||
)
|
||||
assert no == f"{prefix}10001"
|
||||
|
||||
def test_同位宽内字典序与数值序一致仍取最大(self) -> None:
|
||||
"""同宽多行(修复的排序不破坏原行为):是长度优先而非只按长度取首行。"""
|
||||
prefix = today_prefix(NO_PREFIX_BUSINESS)
|
||||
_insert_business_nos([f"{prefix}0099", f"{prefix}8499", f"{prefix}8500"])
|
||||
factory = get_session_factory()
|
||||
with factory() as session:
|
||||
no = next_no(
|
||||
session, model=Business, column_name="business_no", prefix=NO_PREFIX_BUSINESS
|
||||
)
|
||||
assert no == f"{prefix}8501"
|
||||
|
||||
def test_无当日编号时从1起编(self) -> None:
|
||||
"""当日无同前缀行(昨日编号不被 like 命中):从 0001 起,不受历史天数影响。
|
||||
|
||||
当日若已有其他用例留下的同前缀编号(用例间无隔离库),跳过——
|
||||
空表场景由每次 `down -v` 重建后的首个用例覆盖。
|
||||
"""
|
||||
prefix = today_prefix(NO_PREFIX_BUSINESS)
|
||||
factory = get_session_factory()
|
||||
with factory() as session:
|
||||
existing = session.execute(
|
||||
select(Business.id).where(Business.business_no.like(f"{prefix}%")).limit(1)
|
||||
).scalar()
|
||||
if existing is not None:
|
||||
pytest.skip(f"当日已有同前缀编号 {existing},空表场景跳过")
|
||||
with factory() as session:
|
||||
no = next_no(
|
||||
session, model=Business, column_name="business_no", prefix=NO_PREFIX_BUSINESS
|
||||
)
|
||||
assert no == f"{prefix}0001"
|
||||
Loading…
Reference in New Issue
Block a user