From 64c57e9141b185e44a4940c5612abc49c1732be6 Mon Sep 17 00:00:00 2001 From: liuwenxin <1009266660@qq.com> Date: Wed, 14 Oct 2026 14:09:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(no=5Fgenerator):=20=E7=BC=96=E5=8F=B7=20MAX?= =?UTF-8?q?=20=E6=A3=80=E7=B4=A2=E6=94=B9=E9=95=BF=E5=BA=A6=E4=BC=98?= =?UTF-8?q?=E5=85=88=E6=8E=92=E5=BA=8F=EF=BC=8C=E7=A0=B4=E8=A7=A3=E4=BD=8D?= =?UTF-8?q?=E5=AE=BD=E8=BF=9B=E4=BD=8D=E5=90=8E=E5=AD=97=E5=85=B8=E5=BA=8F?= =?UTF-8?q?=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 跨位宽(-9999→-10000)后纯字典序反转,MAX 恒取旧值导致当日建单 持续撞唯一键;同前缀下长度大即数值大,同级同长时字典序与数值一致。 新增跨位宽/同宽/空表三个集成用例。 Co-Authored-By: Claude Code --- backend/app/services/no_generator.py | 7 +- .../tests/integration/test_no_generator.py | 82 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 backend/tests/integration/test_no_generator.py diff --git a/backend/app/services/no_generator.py b/backend/app/services/no_generator.py index 447f227..48f0697 100644 --- a/backend/app/services/no_generator.py +++ b/backend/app/services/no_generator.py @@ -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() diff --git a/backend/tests/integration/test_no_generator.py b/backend/tests/integration/test_no_generator.py new file mode 100644 index 0000000..608a086 --- /dev/null +++ b/backend/tests/integration/test_no_generator.py @@ -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"