From be0ba622a034572927957d17288bba83841d23ba Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Sun, 15 Nov 2026 19:11:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(pl):=20=E8=AF=81=E4=BB=B6=E9=A2=9D=E5=BA=A6?= =?UTF-8?q?=E5=88=87=E7=82=B9=E6=94=B9=E4=B8=BA=E6=8C=89=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E5=AE=89=E6=8E=92=E6=97=B6=E9=97=B4=E5=B9=B6=E5=86=BB=E7=BB=93?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 切点前已安排的计划归档跳过购买证/运输证校验;创建较早但安排在生效后的计划走客户+品类新规则。 Co-authored-by: Cursor --- backend/app/api/pl/plan.py | 9 +- backend/app/api/pl/transport.py | 5 ++ backend/app/models/pl_models.py | 3 +- backend/app/schemas/pl/plan.py | 3 +- .../pl_cert_quota_rule_config_service.py | 69 +++++++++++++-- backend/app/services/pl_plan_service.py | 2 + backend/app/services/pl_quota_business.py | 13 ++- backend/tests/test_pl_customer_cert_quota.py | 86 ++++++++++++++++++- ...26-11-15-pl-cert-quota-arrange-cutover.sql | 28 ++++++ ...26-11-15-pl-cert-quota-arrange-cutover.sql | 28 ++++++ ...15-pl-cert-quota-arrange-cutover-design.md | 30 +++++++ .../views/system/cert-quota-rule/index.vue | 2 +- 12 files changed, 257 insertions(+), 21 deletions(-) create mode 100644 docs/sql/2026-11-15-pl-cert-quota-arrange-cutover.sql create mode 100644 docs/sql/migrations/2026-11-15-pl-cert-quota-arrange-cutover.sql create mode 100644 docs/superpowers/specs/2026-11-15-pl-cert-quota-arrange-cutover-design.md diff --git a/backend/app/api/pl/plan.py b/backend/app/api/pl/plan.py index 898305028..bcf778dce 100644 --- a/backend/app/api/pl/plan.py +++ b/backend/app/api/pl/plan.py @@ -2206,8 +2206,7 @@ def create_plan( create_by=user.id, create_dept_id=user.dept_id, ) - from app.services.pl_cert_quota_rule_config_service import stamp_plan_rule_version - stamp_plan_rule_version(db, user, plan) + # 证件额度规则版本在首次安排时按生效时间打标,创建不打标。 db.add(plan) # 添加产品明细 @@ -2379,8 +2378,7 @@ def create_plan_full( create_by=user.id, create_dept_id=user.dept_id, ) - from app.services.pl_cert_quota_rule_config_service import stamp_plan_rule_version - stamp_plan_rule_version(db, user, plan) + # 证件额度规则版本在首次安排时按生效时间打标,创建不打标。 db.add(plan) db.flush() @@ -2531,8 +2529,7 @@ def create_from_contract( create_by=user.id, create_dept_id=user.dept_id, ) - from app.services.pl_cert_quota_rule_config_service import stamp_plan_rule_version - stamp_plan_rule_version(db, user, plan) + # 证件额度规则版本在首次安排时按生效时间打标,创建不打标。 db.add(plan) # 查询合同产品明细 diff --git a/backend/app/api/pl/transport.py b/backend/app/api/pl/transport.py index 47e95c9ac..7de807eb1 100644 --- a/backend/app/api/pl/transport.py +++ b/backend/app/api/pl/transport.py @@ -888,7 +888,10 @@ def arrange_transport( ) apply_resource_snapshot_json(plan=plan, transport=transport, snapshot=snapshot) from app.services.pl_quota_planning import pin_measurement_version + from app.services.pl_cert_quota_rule_config_service import stamp_plan_rule_on_arrange pin_measurement_version(db, user, plan) + # 首次安排按生效时间打标并冻结;改派不重打。 + stamp_plan_rule_on_arrange(db, user, plan) plan.transport_status = "arranged" plan.follow_status = "arranged" plan.freight_amount = freight_amount or 0 @@ -1304,11 +1307,13 @@ def process_approval( transport.carrier_name = vehicle_info['carrier_name'] snapshot = build_transport_resource_snapshot(db, tenant_no, transport.vehicle_id, selected_driver_id=transport.driver_id, selected_escort_id=transport.escort_id) + from app.services.pl_cert_quota_rule_config_service import stamp_plan_rule_on_arrange for plan in plans: assert_pl_row_in_scope(db, plan, PlPlan, user, tenant_no) plan.vehicle_id = transport.vehicle_id plan.license_plate = transport.license_plate apply_resource_snapshot_json(plan=plan, transport=transport, snapshot=snapshot) + stamp_plan_rule_on_arrange(db, user, plan) apply_plan_quota(db, user, plan, "reserve", payload) plan.transport_status = "arranged" plan.follow_status = "arranged" diff --git a/backend/app/models/pl_models.py b/backend/app/models/pl_models.py index c7a7924d7..cfb7e7ab6 100644 --- a/backend/app/models/pl_models.py +++ b/backend/app/models/pl_models.py @@ -370,7 +370,8 @@ class PlPlan(Base): quota_cert_waived_by = Column(String(64), nullable=True, comment="证书齐备确认人ID") quota_cert_waived_by_name = Column(String(100), nullable=True, comment="证书齐备确认人姓名") quota_cert_waived_time = Column(DateTime, nullable=True, comment="证书齐备确认时间") - cert_quota_rule_version = Column(String(32), nullable=True, comment="证件额度规则版本 legacy|customer_category;创建时打标") + cert_quota_rule_version = Column(String(32), nullable=True, comment="证件额度规则版本 legacy|customer_category;首次安排时打标并冻结") + cert_quota_arranged_at = Column(DateTime, nullable=True, comment="首次安排时间(证件额度切点用);再次安排不改") quota_cert_pending_fulfill = Column(SmallInteger, nullable=False, default=0, comment="新规则证书齐备待完善 0-否 1-是") erp_entered = Column(SmallInteger, nullable=False, default=0, comment="是否已录入ERP 0-否 1-是") cancel_reason = Column(String(500), nullable=True, comment="作废原因") diff --git a/backend/app/schemas/pl/plan.py b/backend/app/schemas/pl/plan.py index aa069423b..394c83fea 100644 --- a/backend/app/schemas/pl/plan.py +++ b/backend/app/schemas/pl/plan.py @@ -417,7 +417,8 @@ class PlanRead(PlanBase, PlanResourceSnapshotMixin): quota_cert_waived_by: Optional[str] = Field(default=None, alias="quotaCertWaivedBy", description="证书齐备确认人ID") quota_cert_waived_by_name: Optional[str] = Field(default=None, alias="quotaCertWaivedByName", description="证书齐备确认人姓名") quota_cert_waived_time: Optional[datetime] = Field(default=None, alias="quotaCertWaivedTime", description="证书齐备确认时间") - cert_quota_rule_version: Optional[str] = Field(default=None, alias="certQuotaRuleVersion", description="legacy|customer_category") + cert_quota_rule_version: Optional[str] = Field(default=None, alias="certQuotaRuleVersion", description="legacy|customer_category;首次安排打标") + cert_quota_arranged_at: Optional[datetime] = Field(default=None, alias="certQuotaArrangedAt", description="首次安排时间(证件额度切点)") quota_cert_pending_fulfill: int = Field(default=0, alias="quotaCertPendingFulfill", description="新规则齐备待完善 0否 1是") erp_entered: int = Field(default=0, alias="erpEntered", description="是否已录入ERP 0-否 1-是") cancel_reason: Optional[str] = Field(default=None, alias="cancelReason", description="作废原因") diff --git a/backend/app/services/pl_cert_quota_rule_config_service.py b/backend/app/services/pl_cert_quota_rule_config_service.py index b7b936fec..6caf2469c 100644 --- a/backend/app/services/pl_cert_quota_rule_config_service.py +++ b/backend/app/services/pl_cert_quota_rule_config_service.py @@ -1,4 +1,4 @@ -"""客户品类额度规则切点:系统信息配置;未配置则拦住安排。""" +"""客户品类额度规则切点:按首次安排时间打标;切点前已安排归档跳过校证。""" from datetime import datetime from typing import Optional from uuid import uuid4 @@ -58,20 +58,77 @@ def assert_cert_quota_rule_configured(db: Session, tenant_no: str) -> datetime: return effective_at -def resolve_rule_version_for_create(db: Session, tenant_no: str, create_time: Optional[datetime] = None) -> str: - """计划创建时打标;未配置生效时间时仍标 legacy(安排会被拦住)。""" +def resolve_rule_version_for_arrange( + db: Session, tenant_no: str, arrange_time: Optional[datetime] = None +) -> str: + """首次安排时打标:安排时刻 >= 生效时间 → 新规则,否则祖父 legacy。""" effective_at = get_effective_at(db, tenant_no) - when = create_time or datetime.now() + when = arrange_time or datetime.now() if effective_at is not None and when >= effective_at: return VERSION_CUSTOMER_CATEGORY return VERSION_LEGACY -def stamp_plan_rule_version(db: Session, user, plan) -> str: - version = resolve_rule_version_for_create(db, user.tenant_no, getattr(plan, 'create_time', None) or datetime.now()) +# 兼容旧测试/调用名;语义已改为安排时刻。 +resolve_rule_version_for_create = resolve_rule_version_for_arrange + + +def stamp_plan_rule_on_arrange( + db: Session, user, plan, *, arrange_time: Optional[datetime] = None +) -> str: + """首次安排写入版本与安排时刻;再次安排冻结,不改版本。""" + if getattr(plan, 'cert_quota_arranged_at', None) is not None: + return getattr(plan, 'cert_quota_rule_version', None) or VERSION_LEGACY + when = arrange_time or datetime.now() + version = resolve_rule_version_for_arrange(db, user.tenant_no, when) + plan.cert_quota_arranged_at = when plan.cert_quota_rule_version = version return version +def stamp_plan_rule_version(db: Session, user, plan) -> str: + """兼容旧名:等价于首次安排打标(创建路径不应再调用)。""" + return stamp_plan_rule_on_arrange(db, user, plan) + + def plan_uses_customer_category(plan) -> bool: return (getattr(plan, 'cert_quota_rule_version', None) or VERSION_LEGACY) == VERSION_CUSTOMER_CATEGORY + + +def infer_arranged_at(db: Session, plan) -> Optional[datetime]: + """历史缺字段时用关联运输单创建时间推断首次安排。""" + stamped = getattr(plan, 'cert_quota_arranged_at', None) + if stamped is not None: + return stamped + transport_id = getattr(plan, 'transport_id', None) + tenant_no = getattr(plan, 'tenant_no', None) + if not transport_id or not tenant_no or db is None: + return None + from app.models.pl_models import PlTransport + + transport = ( + db.query(PlTransport) + .filter_by(id=transport_id, tenant_no=tenant_no, del_flag=0) + .first() + ) + return getattr(transport, 'create_time', None) if transport else None + + +def plan_skips_archive_cert_quota(plan, db: Session = None, tenant_no: str = None) -> bool: + """首次安排早于生效时间则归档跳过校证。 + + 优先比较安排时刻与生效时间;若无生效配置或无法得到安排时刻,则不因空版本自动跳过 + (避免误伤仍走合同绑证结转的历史路径;迁移/安排打标会补齐 arranged_at)。 + """ + tn = tenant_no or getattr(plan, 'tenant_no', None) + arranged_at = getattr(plan, 'cert_quota_arranged_at', None) + if arranged_at is None and db is not None: + arranged_at = infer_arranged_at(db, plan) + if arranged_at is None: + return False + if db is not None and tn: + effective_at = get_effective_at(db, tn) + if effective_at is not None: + return arranged_at < effective_at + # 已记录首次安排且版本为 legacy(安排时未达生效点)→ 跳过 + return not plan_uses_customer_category(plan) diff --git a/backend/app/services/pl_plan_service.py b/backend/app/services/pl_plan_service.py index 1ee2f4f49..808bd6ece 100644 --- a/backend/app/services/pl_plan_service.py +++ b/backend/app/services/pl_plan_service.py @@ -1166,7 +1166,9 @@ def create_transport_for_plan( escort_id=escort_id, ) from app.services.pl_quota_planning import pin_measurement_version + from app.services.pl_cert_quota_rule_config_service import stamp_plan_rule_on_arrange pin_measurement_version(db, user, plan) + stamp_plan_rule_on_arrange(db, user, plan) plan.transport_status = "arranged" plan.follow_status = "arranged" plan.freight_amount = freight_amount or 0 diff --git a/backend/app/services/pl_quota_business.py b/backend/app/services/pl_quota_business.py index d6818961f..4a6832b29 100644 --- a/backend/app/services/pl_quota_business.py +++ b/backend/app/services/pl_quota_business.py @@ -120,12 +120,21 @@ def apply_plan_quota(db, user, plan, action, payload=None): evidenceUrls=getattr(payload, 'quota_evidence_urls', None), sourceNote=getattr(payload, 'quota_source_note', None)) from app.services.pl_quota_waiver import assert_waived_sources, quota_cert_waived - from app.services.pl_cert_quota_rule_config_service import plan_uses_customer_category + from app.services.pl_cert_quota_rule_config_service import ( + plan_skips_archive_cert_quota, + plan_uses_customer_category, + ) # 新规则:齐备仅放行安排/改派预占;归档与实际调整必须真证结转。 - # 旧规则(legacy):齐备仍跳过安排与归档(祖父兼容)。 + # 切点前首次安排:归档/实调不校证件(无需齐备);齐备仍可跳过安排预占。 skip_actions = ('reserve', 'resize') if not plan_uses_customer_category(plan): skip_actions = ('reserve', 'resize', 'settle', 'actual_adjust') + grandfather_archive_skip = ( + action in ('settle', 'actual_adjust') + and plan_skips_archive_cert_quota(plan, db, getattr(user, 'tenant_no', None)) + ) + if grandfather_archive_skip: + return {'operationId': ledger_id, 'usageIds': [], 'waived': True, 'skippedCertQuota': True} if quota_cert_waived(plan) and action in skip_actions: if action in ('reserve', 'resize'): assert_waived_sources(db, user, plan, lock=True) diff --git a/backend/tests/test_pl_customer_cert_quota.py b/backend/tests/test_pl_customer_cert_quota.py index 4234c8b3b..02e7447ac 100644 --- a/backend/tests/test_pl_customer_cert_quota.py +++ b/backend/tests/test_pl_customer_cert_quota.py @@ -19,16 +19,94 @@ def test_assert_rule_not_configured_blocks_arrange(): assert '系统信息' in str(exc.value.detail) -def test_resolve_rule_version_by_effective_at(): +def test_resolve_rule_version_by_arrange_effective_at(): from app.services import pl_cert_quota_rule_config_service as cfg db = MagicMock() row = SimpleNamespace(effective_at=datetime(2026, 11, 14, 0, 0, 0)) db.query.return_value.filter_by.return_value.first.return_value = row - assert cfg.resolve_rule_version_for_create(db, 't', datetime(2026, 11, 13)) == 'legacy' - assert cfg.resolve_rule_version_for_create(db, 't', datetime(2026, 11, 14)) == 'customer_category' + assert cfg.resolve_rule_version_for_arrange(db, 't', datetime(2026, 11, 13)) == 'legacy' + assert cfg.resolve_rule_version_for_arrange(db, 't', datetime(2026, 11, 14)) == 'customer_category' db.query.return_value.filter_by.return_value.first.return_value = None - assert cfg.resolve_rule_version_for_create(db, 't', datetime(2026, 11, 20)) == 'legacy' + assert cfg.resolve_rule_version_for_arrange(db, 't', datetime(2026, 11, 20)) == 'legacy' + + +def test_stamp_plan_rule_version_freezes_first_arrange(): + from app.services import pl_cert_quota_rule_config_service as cfg + + db = MagicMock() + row = SimpleNamespace(effective_at=datetime(2026, 11, 14, 0, 0, 0)) + db.query.return_value.filter_by.return_value.first.return_value = row + user = SimpleNamespace(tenant_no='t', id='u') + plan = SimpleNamespace(cert_quota_rule_version=None, cert_quota_arranged_at=None) + first = cfg.stamp_plan_rule_on_arrange(db, user, plan, arrange_time=datetime(2026, 11, 13, 10, 0, 0)) + assert first == 'legacy' + assert plan.cert_quota_rule_version == 'legacy' + assert plan.cert_quota_arranged_at == datetime(2026, 11, 13, 10, 0, 0) + second = cfg.stamp_plan_rule_on_arrange(db, user, plan, arrange_time=datetime(2026, 11, 20, 10, 0, 0)) + assert second == 'legacy' + assert plan.cert_quota_rule_version == 'legacy' + assert plan.cert_quota_arranged_at == datetime(2026, 11, 13, 10, 0, 0) + + +def test_stamp_after_cutoff_uses_customer_category_even_if_created_earlier(): + from app.services import pl_cert_quota_rule_config_service as cfg + + db = MagicMock() + row = SimpleNamespace(effective_at=datetime(2026, 11, 14, 0, 0, 0)) + db.query.return_value.filter_by.return_value.first.return_value = row + user = SimpleNamespace(tenant_no='t', id='u') + plan = SimpleNamespace( + create_time=datetime(2026, 11, 1), + cert_quota_rule_version=None, + cert_quota_arranged_at=None, + ) + version = cfg.stamp_plan_rule_on_arrange(db, user, plan, arrange_time=datetime(2026, 11, 15)) + assert version == 'customer_category' + assert plan.cert_quota_rule_version == 'customer_category' + + +def test_plan_skips_archive_when_arranged_before_effective_at(): + from app.services import pl_cert_quota_rule_config_service as cfg + + db = MagicMock() + db.query.return_value.filter_by.return_value.first.return_value = SimpleNamespace( + effective_at=datetime(2026, 11, 14, 0, 0, 0) + ) + before = SimpleNamespace( + tenant_no='t', + cert_quota_arranged_at=datetime(2026, 11, 10), + cert_quota_rule_version='legacy', + transport_id=None, + ) + after = SimpleNamespace( + tenant_no='t', + cert_quota_arranged_at=datetime(2026, 11, 15), + cert_quota_rule_version='customer_category', + transport_id=None, + ) + assert cfg.plan_skips_archive_cert_quota(before, db, 't') is True + assert cfg.plan_skips_archive_cert_quota(after, db, 't') is False + + +def test_legacy_arranged_before_cutoff_skips_archive_cert_without_waive(monkeypatch): + """切点前首次安排的计划:归档不校证件,无需证书齐备。""" + from app.services import pl_quota_business as biz + + plan = SimpleNamespace( + id='p1', + quota_cert_waived=0, + quota_cert_pending_fulfill=0, + cert_quota_rule_version='legacy', + cert_quota_arranged_at=datetime(2026, 11, 10), + cert_configuration={}, + ) + monkeypatch.setattr( + 'app.services.pl_cert_quota_rule_config_service.plan_skips_archive_cert_quota', + lambda p, db=None, tenant_no=None: True, + ) + result = biz.apply_plan_quota(MagicMock(), SimpleNamespace(id='u', tenant_no='t'), plan, 'settle', None) + assert result.get('waived') is True or result.get('skippedCertQuota') is True def test_apply_plan_quota_new_rule_waive_skips_reserve_not_settle(monkeypatch): diff --git a/docs/sql/2026-11-15-pl-cert-quota-arrange-cutover.sql b/docs/sql/2026-11-15-pl-cert-quota-arrange-cutover.sql new file mode 100644 index 000000000..dfcba0aeb --- /dev/null +++ b/docs/sql/2026-11-15-pl-cert-quota-arrange-cutover.sql @@ -0,0 +1,28 @@ +-- 证件额度切点改为首次安排时间:补 arranged_at 列,并按运输单创建时间回填已安排计划。 +-- MySQL 5.7。可重复执行。 +SET NAMES utf8mb4; +SET @schema_name = DATABASE(); + +SET @col = (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA=@schema_name AND TABLE_NAME='pl_plan' AND COLUMN_NAME='cert_quota_arranged_at'); +SET @sql = IF(@col=0, + 'ALTER TABLE pl_plan ADD COLUMN cert_quota_arranged_at datetime DEFAULT NULL COMMENT ''首次安排时间(证件额度切点用);再次安排不改'' AFTER cert_quota_rule_version', + 'SELECT 1'); +PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; + +-- 已安排:回填首次安排时刻,并按「安排时刻 vs 生效时间」校正规则版本(修正历史按创建打标)。 +UPDATE pl_plan p +INNER JOIN pl_transport t + ON t.id = p.transport_id AND t.tenant_no = p.tenant_no AND t.del_flag = 0 +LEFT JOIN pl_cert_quota_rule_config cfg + ON cfg.tenant_no = p.tenant_no AND cfg.del_flag = 0 +SET + p.cert_quota_arranged_at = COALESCE(p.cert_quota_arranged_at, t.create_time), + p.cert_quota_rule_version = CASE + WHEN cfg.effective_at IS NOT NULL + AND COALESCE(p.cert_quota_arranged_at, t.create_time) >= cfg.effective_at + THEN 'customer_category' + ELSE 'legacy' + END +WHERE p.del_flag = 0 + AND p.transport_status = 'arranged'; diff --git a/docs/sql/migrations/2026-11-15-pl-cert-quota-arrange-cutover.sql b/docs/sql/migrations/2026-11-15-pl-cert-quota-arrange-cutover.sql new file mode 100644 index 000000000..dfcba0aeb --- /dev/null +++ b/docs/sql/migrations/2026-11-15-pl-cert-quota-arrange-cutover.sql @@ -0,0 +1,28 @@ +-- 证件额度切点改为首次安排时间:补 arranged_at 列,并按运输单创建时间回填已安排计划。 +-- MySQL 5.7。可重复执行。 +SET NAMES utf8mb4; +SET @schema_name = DATABASE(); + +SET @col = (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA=@schema_name AND TABLE_NAME='pl_plan' AND COLUMN_NAME='cert_quota_arranged_at'); +SET @sql = IF(@col=0, + 'ALTER TABLE pl_plan ADD COLUMN cert_quota_arranged_at datetime DEFAULT NULL COMMENT ''首次安排时间(证件额度切点用);再次安排不改'' AFTER cert_quota_rule_version', + 'SELECT 1'); +PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; + +-- 已安排:回填首次安排时刻,并按「安排时刻 vs 生效时间」校正规则版本(修正历史按创建打标)。 +UPDATE pl_plan p +INNER JOIN pl_transport t + ON t.id = p.transport_id AND t.tenant_no = p.tenant_no AND t.del_flag = 0 +LEFT JOIN pl_cert_quota_rule_config cfg + ON cfg.tenant_no = p.tenant_no AND cfg.del_flag = 0 +SET + p.cert_quota_arranged_at = COALESCE(p.cert_quota_arranged_at, t.create_time), + p.cert_quota_rule_version = CASE + WHEN cfg.effective_at IS NOT NULL + AND COALESCE(p.cert_quota_arranged_at, t.create_time) >= cfg.effective_at + THEN 'customer_category' + ELSE 'legacy' + END +WHERE p.del_flag = 0 + AND p.transport_status = 'arranged'; diff --git a/docs/superpowers/specs/2026-11-15-pl-cert-quota-arrange-cutover-design.md b/docs/superpowers/specs/2026-11-15-pl-cert-quota-arrange-cutover-design.md new file mode 100644 index 000000000..5bb544f49 --- /dev/null +++ b/docs/superpowers/specs/2026-11-15-pl-cert-quota-arrange-cutover-design.md @@ -0,0 +1,30 @@ +# 证件额度切点:按首次安排时间(设计) + +工单:[#40](https://gitea.aichengyan.com/cykj/shanxia2.0/issues/40) + +## 背景 + +原设计按计划 `create_time` 与 `effective_at` 比较打 `cert_quota_rule_version`。业务要求改为按**首次安排运输**时刻切点;切点前已安排的计划归档不再校购买证/运输证。 + +## 定稿规则 + +1. **未配置 `effective_at`**:安排仍 409 拦住(不变)。 +2. **首次安排**时写入: + - `cert_quota_arranged_at = 安排时刻` + - `cert_quota_rule_version = customer_category` 若 `安排时刻 >= effective_at`,否则 `legacy` +3. **再次调整运输**:不重打 `cert_quota_arranged_at` / `cert_quota_rule_version`(冻结首次安排)。 +4. **创建计划**:不再按创建时间打标(可空,待安排时写入)。 +5. **归档**: + - `legacy`(切点前首次安排):不校验、不扣购买证/运输证,额度结转按跳过处理,允许直接归档。 + - `customer_category`:保持现有最新业务逻辑(选证、齐备仅放行安排、归档须真证等)。 +6. **历史已安排且缺 `cert_quota_arranged_at`**:归档/读规则时用关联 `pl_transport.create_time` 推断;若推断时刻 `< effective_at` 视为 `legacy` 祖父。 + +## 范围 + +- backend:切点服务、安排打标、归档跳过、迁移列 +- web-admin:系统信息「运输证件规则」说明文案 + +## 非目标 + +- 不批量改历史 `quota_cert_waived` +- 不改选证算法本身 diff --git a/web-admin/src/views/system/cert-quota-rule/index.vue b/web-admin/src/views/system/cert-quota-rule/index.vue index 3369969c5..e07908f7d 100644 --- a/web-admin/src/views/system/cert-quota-rule/index.vue +++ b/web-admin/src/views/system/cert-quota-rule/index.vue @@ -7,7 +7,7 @@

- 配置「客户+品类」额度生效时间。未配置时无法安排运输。计划创建时按该时间打规则版本;修改后只影响之后新建的计划。 + 配置「客户+品类」额度生效时间。未配置时无法安排运输。以计划首次安排时刻与该时间比较:之前已安排的归档不再校验购买证/运输证;之后安排的走客户+品类最新规则。首次安排后版本冻结,改派不重打;修改生效时间只影响尚未安排的计划。