From 0dd9c8db073960173081f0edc1801c1ff94eb63b Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 20 Oct 2026 16:45:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...t-dispatch-navigation-20261020-targeted.py | 10 + deploy/sql/README.md | 2 + .../sql/lastleg-dispatch-daily-sequence.sql | 7 + deploy/sql/海外区域统一增量-MySQL8.sql | 8 +- lastleg-service/docs/business-index.md | 1 + ...astleg-internal-modularization-baseline.md | 6 +- .../analyze-lastleg-dependencies.test.mjs | 35 +- .../fleet/DispatchDetailReadAdapter.java | 24 ++ .../dispatch/port/DispatchDetailReader.java | 7 + .../repository/DispatchNumberSequence.java | 29 ++ .../service/DispatchAggregateService.java | 6 +- .../service/DispatchNumberGenerator.java | 33 ++ .../gateway/HttpOwnedBusinessFileGateway.java | 7 +- .../mapper/OwnedBusinessQueryMapper.java | 10 +- .../model/OwnedBusinessRows.java | 14 +- .../repository/OwnedBusinessRepository.java | 3 +- .../OwnedBusinessOwnershipService.java | 1 + .../service/OwnedBusinessProjection.java | 8 +- .../service/OwnedBusinessQueryService.java | 6 +- .../service/OwnedBusinessRecordService.java | 14 +- .../fleet/trip/FleetTripCalendarRow.java | 12 + .../fleet/trip/FleetTripCalendarService.java | 9 +- .../lastleg/fleet/trip/FleetTripMapper.java | 19 +- .../trip/PlatformFleetTripController.java | 5 +- .../src/main/resources/db/lastleg.sql | 8 +- .../resources/openapi/fleet-trips-v1.yaml | 13 +- .../fleet/DispatchDetailReadAdapterTest.java | 26 ++ .../DispatchNumberSequenceTest.java | 59 ++++ .../DispatchAggregatePersistenceTest.java | 3 + .../service/DispatchNumberGeneratorTest.java | 31 ++ .../ownedbusiness/OwnedBusinessFixtures.java | 10 +- .../OwnedBusinessOwnershipTest.java | 9 + .../OwnedBusinessPersistenceTest.java | 44 ++- .../ownedbusiness/OwnedBusinessQueryTest.java | 8 +- .../trip/FleetTripCalendarMapperTest.java | 17 +- .../trip/FleetTripCalendarServiceTest.java | 20 +- .../lastleg-slice-dependencies.json | 25 ++ scripts/README.md | 6 + scripts/admin-test-env-modules/README.md | 4 +- ...dispatch-workflow-extension-acceptance.mjs | 322 +++++++++++++++++- ...patch-workflow-extension-contract.test.mjs | 41 ++- .../dispatch-workflow-extensions-test-env.mjs | 42 ++- scripts/synchronous-dependency-baseline.mjs | 3 + .../synchronous-dependency-contract.test.mjs | 15 +- 44 files changed, 889 insertions(+), 93 deletions(-) create mode 100644 .logs/appointment-dispatch-navigation-20261020-targeted.py create mode 100644 deploy/sql/lastleg-dispatch-daily-sequence.sql create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/DispatchDetailReadAdapter.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDetailReader.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchNumberSequence.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchNumberGenerator.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarRow.java create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/DispatchDetailReadAdapterTest.java create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/repository/DispatchNumberSequenceTest.java create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchNumberGeneratorTest.java diff --git a/.logs/appointment-dispatch-navigation-20261020-targeted.py b/.logs/appointment-dispatch-navigation-20261020-targeted.py new file mode 100644 index 000000000..a22250bca --- /dev/null +++ b/.logs/appointment-dispatch-navigation-20261020-targeted.py @@ -0,0 +1,10 @@ +import subprocess, shutil, xml.etree.ElementTree as ET +from pathlib import Path +root=Path.cwd() +report=root/'lastleg-service/target/surefire-reports/TEST-com.baiyue.lastleg.dispatch.service.DispatchAggregatePersistenceTest.xml' +properties=ET.parse(report).getroot().find('properties') +classpath=next(item.attrib['value'] for item in properties if item.attrib['name']=='java.class.path') +sources=[str(root/'lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip'/name) for name in ['FleetTripCalendarServiceTest.java','FleetTripCalendarMapperTest.java']] +subprocess.run(['javac','-encoding','UTF-8','--release','17','-cp',classpath,'-d',str(root/'lastleg-service/target/test-classes'),*sources],check=True) +result=subprocess.run([shutil.which('mvn'),' -T'.strip(),'1','-DforkCount=1','-DreuseForks=true','-pl','lastleg-service','surefire:test','-Dtest=FleetTripCalendarServiceTest,FleetTripCalendarMapperTest'],check=False) +raise SystemExit(result.returncode) diff --git a/deploy/sql/README.md b/deploy/sql/README.md index 90a27fb40..784182525 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -1,5 +1,7 @@ # 单测试环境数据库空库初始化 +排车单号使用`PCyyyyMMdd0001`,日期取`Asia/Shanghai`,全平台跨客户/仓库共用独立日流水。`ll_dispatch_daily_sequence`同步Lastleg源基线与海外空库基线;已有结构的定向新增入口为`lastleg-dispatch-daily-sequence.sql`,只建序列表,不改已有业务号、不回填历史、不重放空库初始化。源码和DDL须配套发布;文件存在不表示已执行或部署。流水在独立短事务分配,回滚或取消不复用,四位范围每日0001至9999,准确单量以主单统计为准。 + 自有业务运营视图的结构在 Lastleg 源基线及海外统一空库基线新增四张附加表:`ll_fleet_owned_business_ownership/record/command/history`,分别保存原排车装车完成时的车辆归属证据、运营信息、命令与不可变历史;不新增排车主单或第二套运输状态。权限为 IAM `lastleg-permissions.sql` 中的自有业务 list/edit/upload,并同步国内统一空库基线。业务唯一规则见工作区 `docs/海外仓业务/自有业务首版开发契约.md`。本次仅准备源码基线,无 SQL 执行、存量回填或部署;旧已装车 TEST 数据不倒推历史归属,匹配版本按新原生样本验收。 客户计费重独立确认新增 `fl_chargeable_weight_confirmation`,源表与国内空库基线同步;存量 TEST 使用 `migrations/cn/baiyue_firstleg/V8__chargeable_weight_confirmation.sql`,不回填费用确认。须随 Firstleg、Order、Lastleg、Admin 和 Client 匹配版本核对;本次未执行 SQL、未部署。 diff --git a/deploy/sql/lastleg-dispatch-daily-sequence.sql b/deploy/sql/lastleg-dispatch-daily-sequence.sql new file mode 100644 index 000000000..ca4740ee2 --- /dev/null +++ b/deploy/sql/lastleg-dispatch-daily-sequence.sql @@ -0,0 +1,7 @@ +-- 排车新编号所需独立结构;仅新建序列表,不改写已有排车业务号或业务数据。 +USE baiyue_lastleg; +CREATE TABLE IF NOT EXISTS ll_dispatch_daily_sequence ( + business_date DATE NOT NULL COMMENT '排车编号业务日期(Asia/Shanghai)', + last_sequence INT NOT NULL COMMENT '当日已分配排车流水号,范围0001至9999', + PRIMARY KEY(business_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='排车单号每日独立原子流水'; diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 6ce789b99..a82791968 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -1865,6 +1865,12 @@ CREATE TABLE ll_order_no_daily_sequence ( PRIMARY KEY (business_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='尾程单号按日原子流水'; +CREATE TABLE ll_dispatch_daily_sequence ( + business_date DATE NOT NULL COMMENT '排车编号业务日期(Asia/Shanghai)', + last_sequence INT NOT NULL COMMENT '当日已分配排车流水号,范围0001至9999', + PRIMARY KEY(business_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='排车单号每日独立原子流水'; + CREATE TABLE ll_order_operation_log ( id bigint NOT NULL COMMENT '操作记录主键', customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户数据隔离键', @@ -4921,7 +4927,7 @@ CREATE TABLE IF NOT EXISTS ll_fleet_owned_business_record ( CONSTRAINT ck_owned_business_record_version CHECK (record_version>=0), CONSTRAINT ck_owned_business_money CHECK ( (currency IS NULL AND total_amount IS NULL) - OR (currency IN ('CNY','USD') AND total_amount IS NOT NULL AND total_amount>=0)), + OR (currency IS NOT NULL AND currency IN ('CNY','USD') AND total_amount IS NOT NULL AND total_amount>=0)), CONSTRAINT ck_owned_business_distance CHECK (actual_distance_miles IS NULL OR actual_distance_miles>=0), CONSTRAINT ck_owned_business_fuel CHECK (fuel_consumption_litres_per_100km IS NULL OR fuel_consumption_litres_per_100km>=0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='原排车自有业务附加运营信息'; diff --git a/lastleg-service/docs/business-index.md b/lastleg-service/docs/business-index.md index 37aa6e714..d1c7c2c63 100644 --- a/lastleg-service/docs/business-index.md +++ b/lastleg-service/docs/business-index.md @@ -85,6 +85,7 @@ - 车次成本:`fleet/trip/FleetTripCostService.java`、`fleet-trip-costs-v1.yaml`;按DEPART实测及外接实收分摊,不产生客户账单/付款。 - 报价/统计:`fleet/quote/`、`fleet/reporting/`,合同`fleet-quotes-v1.yaml`、`fleet-operations-v1.yaml`;实测凭证按车次归属,仅当前/历史版本引用可下载。 +- 自有业务运营:`fleet/ownedbusiness/`、`fleet-owned-business-v1.yaml`,共同规则见`../../../docs/海外仓业务/自有业务首版开发契约.md`;原排车装车后三阶段按装车时自有车辆归属展示,仅补金额、逐目的地到达、里程、油耗与照片。原详情只经`dispatch.port.DispatchDetailReader`读取,原装车事务经`OwnedBusinessOwnershipCapture`捕获归属,不另建履约流程。 - 外接选价合同:`fleet-quote-candidates-v1.yaml`、`fleet-trips-v1.yaml`;来源快照与成交价分开,确认承运复验。 - 在途交接:`fleet/exception/`、`fleet-handovers-v1.yaml`;后台确认交接,旧资源分别结束任务,DEPART不重写。 diff --git a/lastleg-service/docs/lastleg-internal-modularization-baseline.md b/lastleg-service/docs/lastleg-internal-modularization-baseline.md index dc68a0cbe..2ac4107d3 100644 --- a/lastleg-service/docs/lastleg-internal-modularization-baseline.md +++ b/lastleg-service/docs/lastleg-internal-modularization-baseline.md @@ -24,6 +24,10 @@ 既有预约执行与改量只经 `dispatch.port.DispatchAppointmentAmendmentPort` 进入整车事务边界。`FbaAppointmentExecutionAdapter` 对该端口、`DispatchContracts`、`DispatchTypes`,以及 `FbaAppointmentTransportAdapter` 对该端口、`DispatchCommands`、`DispatchContracts`、`DispatchTypes` 的七条精确类引用进入 `reviewedClassDependencies`;不增加 `inbound-pda→dispatch-delivery` 的切片通用许可。适配器不得直连整车 Service、Repository 或 Mapper,其他消费者不继承本项许可,反向合同同时核对这两类越界。此边界只覆盖原预约与现行整车的关联、执行和同事务变更,不授权新增日历预约权威或流程。 +### 自有车辆运营视图读取边界(2026-10-20) + +`fleet.ownedbusiness`只为原排车装车后的自有车辆组合运营资料。五条精确引用限`OwnedBusinessModels→DispatchViews`、`OwnedBusinessProjection→DispatchContracts`、`OwnedBusinessQueryService→DispatchContracts/DispatchTypes/DispatchDetailReader`,均为`dispatch.port`公开事实或只读应用端口;不增加`fleet→dispatch-delivery`通用许可,不共享原排车实体、Mapper或具体Service。原详情适配器在dispatch内部委托唯一提供者。`DispatchAggregateService→OwnedBusinessOwnershipCapture`仅在原装车事务捕获归属,不能反向调用运营查询;捕获失败随原确认回滚。反向合同拒绝具体持久化/Service引用、其他消费者借用端口许可及任何包循环。 + ### 平台自营统计边界 `selfoperated/analytics/` 在依赖分析器中作为独立 `selfoperated-analytics` 切片优先匹配,其 `outbound`、`inventory` 和 `model` 子包不归入客户出库、客户库存或尾程交接切片。该子域通过内部 HTTP 使用 Integration,持久化十张独立 `ll_selfop_*` 表,不引用现有客户履约实体或 Mapper。唯一基础能力引用为 `SelfopEgressController → security.InternalServiceTokenGuard`,复用内部令牌校验;基线仅登记此精确类边,不增加共享技术切片通配许可,包循环仍按同一门禁拒绝。 @@ -32,7 +36,7 @@ 车次关联客户排车单新增 `dispatch-delivery→fleet`:fleet声明 `FleetDispatchGateway`,dispatch内适配器锁定自身排车/出库/派送并同步车和司机快照;子排车修改与发车使用fleet公开的关联门禁。fleet不导入dispatch实体、Mapper或业务服务,因此不新增双向切片依赖。车次与客户单据的归属不变,关联和同步处于同一Lastleg事务;客户单据已经开工时不允许通过改派覆盖执行事实。 -当前分析器把 `wallet.WalletCommandOutboxService` 归入 `shared-technical`,它依赖尾程Outbox实体/Mapper;粗粒度图因而存在 `fleet→shared-technical→tailleg-handoff→dispatch-delivery→fleet` 聚合环。不能把这一聚合环等同于fleet包或Spring组件循环,也不能宣称切片图无环。门禁检查实际包无环、fleet不反向导入dispatch/delivery、dispatch只使用公开端口/门禁;粗粒度切片图保留为耦合报告,不因门禁通过隐去这条聚合环。 +当前分析器把 `wallet.WalletCommandOutboxService` 归入 `shared-technical`,它依赖尾程Outbox实体/Mapper;粗粒度图因而存在 `fleet→shared-technical→tailleg-handoff→dispatch-delivery→fleet` 聚合环。不能把这一聚合环等同于fleet包或Spring组件循环,也不能宣称切片图无环。门禁检查实际包无环,fleet仅允许上节自有运营的五条公开只读引用,dispatch只使用公开端口/门禁;粗粒度切片图保留为耦合报告,不因门禁通过隐去聚合环。 2026-10-02 新增 `fleet` 业务切片,归入当前 Lastleg 部署单元;司机档案与文件适配在 `fleet/driver`,计划与资源占用在 `fleet/trip`,纯资格、分段容量和成本分摊在 `fleet/domain`。fleet依赖技术HTTP配置,以及车辆应用层的 `VehicleRepository` / `VehicleAssignmentGuard` 端口和已有状态服务;新增 `fleet→overseas-master-data` 是同一物理车辆主档和车次占用的明确业务协作,不允许直接依赖车辆持久化适配器。车辆应用层继续只依赖自身端口,注入的占用检查实现归fleet。分析器显式分类并验证这些边界,旧统计及退役安全投影断言不代表当前架构事实。 diff --git a/lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs b/lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs index 44371e5bc..aa50f7c47 100644 --- a/lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs +++ b/lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs @@ -9,6 +9,13 @@ const moduleRoot = path.resolve(import.meta.dirname, '..'); const analyzer = path.join(moduleRoot, 'scripts', 'analyze-lastleg-dependencies.mjs'); const contract = path.join(moduleRoot, 'src', 'test', 'resources', 'architecture', 'lastleg-slice-dependencies.json'); const vehiclePrefix = 'com.baiyue.lastleg.masterdata.vehicle'; +const ownedBusinessReadEdges = [ + ['model.OwnedBusinessModels', 'DispatchViews'], + ['service.OwnedBusinessProjection', 'DispatchContracts'], + ['service.OwnedBusinessQueryService', 'DispatchContracts'], + ['service.OwnedBusinessQueryService', 'DispatchTypes'], + ['service.OwnedBusinessQueryService', 'DispatchDetailReader'], +].map(([source, target]) => `com.baiyue.lastleg.fleet.ownedbusiness.${source}->com.baiyue.lastleg.dispatch.port.${target}`).sort(); test('selfoperated analytics is independent of customer outbound and inventory slices', () => { const report = analyze(); @@ -42,13 +49,13 @@ test('fleet stays a business slice and its policies have no infrastructure depen const policyClasses = classes.filter(entry => entry.packageName === 'com.baiyue.lastleg.fleet.domain'); assert.equal(policyClasses.length, 3); for (const entry of policyClasses) assert.ok(entry.imports.every(imported => imported.startsWith('java.'))); - assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'fleet').map(edge => edge.target).sort(), ['overseas-master-data', 'shared-technical']); + assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'fleet').map(edge => edge.target).sort(), ['dispatch-delivery', 'overseas-master-data', 'shared-technical']); for (const edge of report.classDependencies.filter(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.') && edge.target.startsWith(vehiclePrefix))) { assert.ok(edge.target.startsWith(vehiclePrefix + '.application.'), 'fleet must use vehicle application ports, not persistence adapters'); } assert.equal(report.packageSccs.some(component => component.some(name => name.startsWith('com.baiyue.lastleg.fleet.'))), false); - assert.equal(report.classDependencies.some(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.') - && /com\.baiyue\.lastleg\.(dispatch|delivery)\./.test(edge.target)), false); + assert.deepEqual(report.classDependencies.filter(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.') + && /com\.baiyue\.lastleg\.(dispatch|delivery)\./.test(edge.target)).map(edge => `${edge.source}->${edge.target}`).sort(), ownedBusinessReadEdges); const dispatchEdges = report.classDependencies.filter(edge => edge.source.startsWith('com.baiyue.lastleg.dispatch.') && edge.target.startsWith('com.baiyue.lastleg.fleet.')); assert.ok(dispatchEdges.length > 0); @@ -58,13 +65,33 @@ test('fleet stays a business slice and its policies have no infrastructure depen || edge.source === 'com.baiyue.lastleg.dispatch.fleet.FleetDispatchPlanningMapper' && edge.target === 'com.baiyue.lastleg.fleet.trip.FleetDispatchPlanningGateway' || edge.source === 'com.baiyue.lastleg.dispatch.fleet.PdaFleetHandoverController' - && edge.target === 'com.baiyue.lastleg.fleet.exception.FleetHandoverPdaQuery'), + && edge.target === 'com.baiyue.lastleg.fleet.exception.FleetHandoverPdaQuery' + || edge.source === 'com.baiyue.lastleg.dispatch.service.DispatchAggregateService' + && edge.target === 'com.baiyue.lastleg.fleet.ownedbusiness.port.OwnedBusinessOwnershipCapture'), 'dispatch must use fleet ports and guards, not its persistence mappers'); const incomingEdges = report.classDependencies.filter(edge => edge.source.startsWith('com.baiyue.lastleg.transfer.') && edge.target.startsWith('com.baiyue.lastleg.fleet.')); assert.ok(incomingEdges.length > 0); assert.ok(incomingEdges.every(edge => edge.source === 'com.baiyue.lastleg.transfer.fleet.FleetIncomingAdapter' && /\.(FleetBookingIncomingGateway|FleetDispatchGateway)$/.test(edge.target)), 'incoming adapter uses fleet ports only'); }); +test('owned business reads only reviewed dispatch ports and cannot inherit persistence access', () => { + const report = analyze(); + const baseline = JSON.parse(readFileSync(contract, 'utf8')); + const prefix = 'com.baiyue.lastleg.fleet.ownedbusiness.'; + const reviewed = baseline.reviewedClassDependencies.filter(edge => edge.source.startsWith(prefix) + && edge.target.startsWith('com.baiyue.lastleg.dispatch.')); + assert.deepEqual(reviewed.map(edge => `${edge.source}->${edge.target}`).sort(), ownedBusinessReadEdges); + assert.equal(baseline.allowedSliceEdges.includes('fleet->dispatch-delivery'), false); + for (const target of ['entity.DispatchAggregate', 'mapper.DispatchAggregateMapper', 'service.DispatchAggregateService']) { + const edge = { source: prefix + 'service.OwnedBusinessQueryService', target: 'com.baiyue.lastleg.dispatch.' + target }; + report.classDependencies.push(edge); + assert.ok(contractViolations(report, baseline).some(message => message.includes(`${edge.source}->${edge.target}`))); + } + const unreviewed = { source: 'com.baiyue.lastleg.fleet.trip.FleetTripService', target: 'com.baiyue.lastleg.dispatch.port.DispatchDetailReader' }; + report.classDependencies.push(unreviewed); + assert.ok(contractViolations(report, baseline).some(message => message.includes(`${unreviewed.source}->${unreviewed.target}`))); +}); + function analyze(sourceSet = 'main') { return JSON.parse(execFileSync(process.execPath, [analyzer, '--source-set', sourceSet, '--format', 'json'], { cwd: moduleRoot, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/DispatchDetailReadAdapter.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/DispatchDetailReadAdapter.java new file mode 100644 index 000000000..9e1afdab7 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/DispatchDetailReadAdapter.java @@ -0,0 +1,24 @@ +package com.baiyue.lastleg.dispatch.fleet; + +import com.baiyue.lastleg.dispatch.port.DispatchContracts; +import com.baiyue.lastleg.dispatch.port.DispatchDetailReader; +import com.baiyue.lastleg.dispatch.port.DispatchViews; +import com.baiyue.lastleg.dispatch.service.DispatchAggregateService; +import org.springframework.stereotype.Component; + +/** 只读委托既有排车应用服务,运营域不引用主流程实现或持久化实体。 */ +@Component +public class DispatchDetailReadAdapter implements DispatchDetailReader { + private final DispatchAggregateService dispatch; + + /** 复用唯一原排车详情提供者,不建立第二套查询规则。 */ + public DispatchDetailReadAdapter(DispatchAggregateService dispatch) { + this.dispatch = dispatch; + } + + /** 原单资格与可见性仍由原应用服务核对。 */ + @Override + public DispatchViews.Detail detail(DispatchContracts.Actor actor, String dispatchNo) { + return dispatch.detail(actor, dispatchNo); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDetailReader.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDetailReader.java new file mode 100644 index 000000000..4225b40e2 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDetailReader.java @@ -0,0 +1,7 @@ +package com.baiyue.lastleg.dispatch.port; + +/** 原排车详情的只读应用端口,供运营视图组合真实履约事实。 */ +public interface DispatchDetailReader { + /** 沿原排车读取及身份守卫,不创建单据、不推进状态。 */ + DispatchViews.Detail detail(DispatchContracts.Actor actor, String dispatchNo); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchNumberSequence.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchNumberSequence.java new file mode 100644 index 000000000..b24f3ba13 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchNumberSequence.java @@ -0,0 +1,29 @@ +package com.baiyue.lastleg.dispatch.repository; + +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; + +/** 整车单号独立日流水;独立短事务归还序列锁后才进入车辆、司机及货物锁。 */ +@Repository +public class DispatchNumberSequence { + private final JdbcTemplate jdbc; + + /** 流水与排车主单使用相同服务数据库。 */ + public DispatchNumberSequence(JdbcTemplate jdbc) { this.jdbc = jdbc; } + + /** 唯一日期行原子递增,连接内取回本次值;失败不复用已分配的业务号。 */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public long next(LocalDate date) { + jdbc.update("INSERT INTO ll_dispatch_daily_sequence(business_date,last_sequence) VALUES(?,LAST_INSERT_ID(1)) " + + "ON DUPLICATE KEY UPDATE last_sequence=LAST_INSERT_ID(last_sequence+1)", date); + Long value = jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class); + if (value == null || value < 1) throw new IllegalStateException("排车日流水分配失败"); + if (value > 9999) throw new BizException(ResultCode.CONFLICT, "当日排车单四位流水号已用满"); + return value; + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java index 449149519..7c5c760be 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java @@ -10,6 +10,7 @@ import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate; import com.baiyue.lastleg.dispatch.port.*; import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository; import com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository; +import com.baiyue.lastleg.fleet.ownedbusiness.port.OwnedBusinessOwnershipCapture; import com.baiyue.lastleg.fleet.trip.FleetExecutionModels; import com.baiyue.lastleg.fleet.trip.FleetTripExecutionService; import com.fasterxml.jackson.core.JsonProcessingException; @@ -31,6 +32,7 @@ import static com.baiyue.lastleg.dispatch.port.DispatchContracts.*; /** 真整车创建、加货与查询;客户只属于明细,不属于整车头。 */ @Service public class DispatchAggregateService implements DispatchExecutionPort,DispatchAppointmentAmendmentPort { + @jakarta.annotation.Resource private DispatchNumberGenerator numbers; @jakarta.annotation.Resource private DispatchAppointmentAllocations appointments; @jakarta.annotation.Resource private DispatchPhysicalClaimService physicalClaims; @jakarta.annotation.Resource private DispatchItemSourceService relabelSources; @@ -47,7 +49,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA private final ObjectMapper json; @jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway evidence; @jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.repository.DispatchExecutionRepository executionReads; - @jakarta.annotation.Resource private com.baiyue.lastleg.fleet.ownedbusiness.port.OwnedBusinessOwnershipCapture ownedBusinessOwnership; + @jakarta.annotation.Resource private OwnedBusinessOwnershipCapture ownedBusinessOwnership; /** 领域编排只访问Repository和提供者应用入口。 */ public DispatchAggregateService(DispatchAggregateRepository repository, DispatchSourceRepository sources, @@ -268,7 +270,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA var replay = commands.replay(actor,null,identity); if (replay.isPresent()) return replay.get(); long id=IdWorker.getId(); - String number="DS-"+UUID.randomUUID().toString().replace("-","").toUpperCase(Locale.ROOT); + String number=numbers.next(); var shell=fleet.createDispatchShell(actor.operatorId(),new FleetExecutionModels.DispatchShellRequest(id,number, Long.parseLong(request.warehouseId()),request.idempotencyKey(),identity.commandHash(),request.plannedDepartureAt(), fleetVehicle(request.vehicle()),fleetDriver(request.driver()),request.remark())); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchNumberGenerator.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchNumberGenerator.java new file mode 100644 index 000000000..0acba1e36 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchNumberGenerator.java @@ -0,0 +1,33 @@ +package com.baiyue.lastleg.dispatch.service; + +import com.baiyue.lastleg.dispatch.repository.DispatchNumberSequence; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.time.Clock; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +/** 平台排车单号:PC、北京时间日期与每日独立四位流水。 */ +@Component +public class DispatchNumberGenerator { + private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai"); + private final DispatchNumberSequence sequences; + private final Clock clock; + + /** 正式运行按统一业务时区取当天日期。 */ + @Autowired + public DispatchNumberGenerator(DispatchNumberSequence sequences) { this(sequences, Clock.systemUTC()); } + + /** 固定时钟用于核实跨午夜归零边界。 */ + DispatchNumberGenerator(DispatchNumberSequence sequences, Clock clock) { this.sequences = sequences; this.clock = clock; } + + /** 已有命令重放由上层账本恢复原号,本方法只用于首次建单。 */ + public String next() { + LocalDate date = LocalDate.now(clock.withZone(BUSINESS_ZONE)); + long value = sequences.next(date); + if (value < 1 || value > 9999) throw new IllegalStateException("排车日流水不在四位范围内"); + return "PC" + date.format(DateTimeFormatter.BASIC_ISO_DATE) + String.format(Locale.ROOT, "%04d", value); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/gateway/HttpOwnedBusinessFileGateway.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/gateway/HttpOwnedBusinessFileGateway.java index 2d439889f..a962aa57b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/gateway/HttpOwnedBusinessFileGateway.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/gateway/HttpOwnedBusinessFileGateway.java @@ -11,7 +11,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import java.time.LocalDateTime; import java.util.Set; -import static com.baiyue.lastleg.fleet.ownedbusiness.service.OwnedBusinessSupport.*; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.common.core.api.ResultCode; /** File保存照片,本域另行授权原排车及分类引用。 */ @Component @@ -76,6 +77,10 @@ public class HttpOwnedBusinessFileGateway implements OwnedBusinessFileGateway { byte[] png={(byte)137,80,78,71,13,10,26,10};if(!"image/png".equals(type) || bytes.lengthSELECT * FROM ll_dispatch_item WHERE dispatch_order_id IN #{id} ORDER BY dispatch_order_id,id") - List cargo(@Param("ids") List ids); + @Select("") + List cargo(@Param("ids") List ids); /** 只聚合真实FBA分配,目的仓与数量保持原事实,不按客户重复整车。 */ @Select("""