Files
docs/海外仓业务/自有业务后端接口设计.md
T
2026-10-20 23:35:46 +08:00

223 lines
27 KiB
Markdown
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.
# 自有业务后端接口设计
2026-10-20 总控已冻结v1,进入独占包实现;尚未执行Maven或环境验收。业务唯一依据为[首版开发契约](自有业务首版开发契约.md),已纳入其第11/12条:计划发车日期筛选、四项统计同一完整筛选,以及独立居中的运营登记弹窗。所有编号以 `dispatchNo` 为用户入口。
用户最新定位已收敛:本页是**原生排车列表装车完成后的自有车辆运营视图**。不创建第二个任务/车次主单,不保存第二份排车货物、预约、单据或审核/装车/发车/签收状态机;外配后续也消费同一排车事实。
### 原排车查询能否直接复用
- `DispatchViews.Query` 当前没有车辆ownership、计划发车日期范围和目的仓关键词过滤;`ListRow` 没有完整目的地/预约和附加运营信息。因此不能靠浏览器取一页后筛OWNED、求统计或逐行GET补齐,列表需要本包新增**只读数据库投影**。
- 原主单详情在服务端通过公开窄端口读取 `dispatch.port.DispatchDetailReader.detail(Actor, dispatchNo)` 的 `DispatchViews.Detail`,再组合本页附加信息;已核实其真实来源,不重写主单查询协议或状态推算。新详情响应建议保留一个明确的 `dispatch: DispatchViews.Detail` 字段;独立三页签由现有事实和附加信息映射展示,不改变排车四页签。
- 新API是有自身list/edit/upload权限的投影视图与附加信息入口,**不是第二套排车API**;原排车查询/Controller/DTO/Mapper全部保持只读,不要求额外公开主流程写动作。
## 1. 事实来源与装车归属捕获
| 事实 | 当前权威代码/数据 | 本页处理 |
| --- | --- | --- |
| 主单、三个阶段、计划/实际发车、装车时间 | `dispatch/entity/DispatchAggregate.java`;`ll_dispatch_order` | 只读 `ACTIVE` 且 `AFTER_LOADING/DELIVERING/DELIVERED`,必须有真实装车确认;不以 `DEPART` 历史作为入选前置 |
| 实际托数 | `DispatchContracts.ConfirmedLoad.pallets`;`loading_confirmation_json` | 按 `warehousePalletId` 去重数,不取逻辑明细数,不合计 `palletSpaces`;缺少可信确认时不伪造托数 |
| 车辆、司机、容量 | `resources_json` 的 `DispatchContracts.Resources` | 读取本趟快照;`VehicleSnapshot` 没有 ownership 字段,`MASTER` 也不等于自有 |
| 当前车辆归属 | `masterdata/vehicle/.../VehicleRepository.VehicleSnapshot.ownership`;`ll_vehicle.ownership` | 有真实 `OWNED/DISPATCHED`,但车辆可在未占用时改归属;当前表不能独自证明历史归属 |
| 历史车辆归属 | 原快照未保存ownership | 已冻结在装车完成同事务捕获真实OWNED/DISPATCHED;根接唯一hook,旧已装车TEST无快照不回填、不按当前主档降级 |
| 多目的仓、货物、各客户 | `ll_dispatch_item`、真实来源及其 destinationKey | 服务端批量汇总;同车一行,各客户来源保留 |
| 预约号、时间和分配数量 | `ll_dispatch_appointment_allocation` + `ll_inbound_fba_appointment`;查询规则见 `DispatchReadMapper.batches` | 按 destinationKey/实际分配关联返回数组,不取首个预约,也不按客户重复整车 |
| 到达、里程、燃油、运营金额 | 当前主单无此页独立权威登记 | 新增附加记录;不推导到达、不改签收、不写Finance、不改排车版本 |
`fleet/reporting/TripOperationsRepository` 仅查实际发车且按货源分类,并使用旧 JdbcTemplate;本模块不复用它的业务过滤或持久化实现。新模块严格 `Controller → Service → Repository → @Mapper`。
## 2. API、权限与入口
统一前缀 `/api/platform/v1/last-leg/fleet/owned-business`。全部要求可信平台身份 `appCode=PLATFORM, tenantId=0`,服务端重复核对真实归属、阶段和仓权限,不接受浏览器声明 ownership/客户归属/托数。
| 方法与相对路径 | 权限 | 作用 |
| --- | --- | --- |
| `GET /` | `platform:fleet:owned-business:list` | 分页与完整筛选四项统计同一只读快照返回;浏览器不逐行GET |
| `GET /{dispatchNo}` | 同上 | 独立三页签所需任务车辆、路线货物与凭证、附加信息及版本 |
| `GET /{dispatchNo}/history?page=1&size=20` | 同上 | 真实操作记录分页;无模拟初始操作 |
| `PUT /{dispatchNo}/operations` | `platform:fleet:owned-business:edit` | 运营弹窗保存;整车金额、逐目的地到达、里程与燃油同一附加版本 |
| `POST /{dispatchNo}/photos/{slot}/uploads` | `platform:fleet:owned-business:upload` | multipart `file` + `idempotencyKey`,只准备一个真实文件,不假称已经保存照片 |
| `PUT /{dispatchNo}/photos/{slot}` | 同上 | 将已上传的 fileId 绑定/替换到该分类框;独立于运营编辑权限 |
| `GET /{dispatchNo}/photos/{slot}/files/{fileId}/content?revisionVersion=` | list | 当前或明确历史版本中已绑定文件的授权代理读取,不返回File内部地址 |
| `GET /{dispatchNo}/commands/{idempotencyKey}` | list | 按当前操作者恢复本页写命令;不允许读取别人的命令载荷 |
没有创建主单、改变运输状态、删除运输或费用发布接口;不公开内部 dispatchId/tripId 作为业务编号。
## 3. 冻结v1 DTO
以下为已冻结v1结构,源码与OpenAPI按本节同步。所有 Long ID/版本通过现有Jackson策略无损输出;Money/数量测量用规范十进制字符串传输并在服务端BigDecimal校验,金额/单价不交前端重新计算。
```ts
type Stage = 'AFTER_LOADING' | 'DELIVERING' | 'DELIVERED'
type PhotoSlot = 'LOADING_SCENE' | 'LOADING_LIST' | 'PRE_DEPARTURE_CHECK'
| 'DRIVER_LICENSE' | 'RECEIPT' | 'FUEL_START' | 'FUEL_END'
type Money = { currency: string; amount: string }
type Arrival = { destinationKey: string; actualArrivedAt: string | null }
type Photo = { slot: PhotoSlot; category: 'LOADING'|'FUEL'; fileId: string;
fileName: string; contentType: 'image/jpeg'|'image/png'; sizeBytes: string;
uploadedAt: string; savedAt: string | null }
type Operations = { money: Money | null; actualDistanceMiles: string | null;
fuelConsumptionLitresPer100Km: string | null; arrivals: Arrival[] }
type Gate = { allowed: boolean; blockReason: string | null }
type Destination = { destinationKey: string; warehouseCode: string | null;
warehouseName: string | null; address: string | null;
appointments: { appointmentNo: string; confirmationNo: string | null;
scheduledAt: string | null; allocatedPackages: string }[];
actualArrivedAt: string | null }
type Row = { dispatchNo: string; stage: Stage; plannedDepartureAt: string;
loadingCompletedAt: string; departedAt: string | null; deliveryCompletedAt: string | null;
origin: { warehouseId: string; warehouseCode: string; warehouseName: string };
vehicle: { vehicleId: string; plate: string; ownership: 'OWNED' };
driver: { name: string | null; phone: string | null };
destinations: Destination[]; actualPalletCount: number | null;
operations: Operations; unitPrice: string | null; recordVersion: string;
dispatchRecordVersion: string;
photoSummary: { loadingCount: number; fuelCount: number };
actions: { edit: Gate; upload: Gate }; sourceIssues: string[] }
type Query = { page?: number; size?: number; plannedDepartureFrom?: string;
plannedDepartureTo?: string; keyword?: string; stage?: Stage; warehouseId?: string }
type ListResult = { records: Row[]; page: number; size: number; total: number;
summary: { dispatchCount: number; afterLoadingCount: number;
deliveringCount: number; deliveredCount: number } }
type Detail = { row: Row; dispatch: DispatchViews.Detail;
cargo: { sourceKey: string; customerId: string; customerName: string | null;
orderNo: string | null; inboundCode: string | null; palletNo: string;
destinationKey: string; packages: string }[]; photos: Photo[] }
type SaveOperations = { recordVersion: string; idempotencyKey: string;
operations: Operations; reason: string | null }
type BindPhoto = { recordVersion: string; idempotencyKey: string; fileId: string }
type Command = { dispatchNo: string; action: 'SAVE_OPERATIONS'|'UPLOAD_PHOTO'|'BIND_PHOTO';
idempotencyKey: string; state: 'SUCCEEDED'|'PENDING'|'UNKNOWN_RESULT'|'REJECTED';
recordVersion: string | null; replayed: boolean; result: Detail | Photo | null;
errorCode: string | null; retryAllowed: boolean }
type History = { eventSource: 'DISPATCH'|'OPERATIONS'; recordVersion: string | null; dispatchRecordVersion: string | null; action: string; operatorName: string | null;
occurredAt: string; reason: string | null; operations: Operations; photos: Photo[] }
```
- 日期筛选明确对应 `planned_departure_at`;接口接收带时区的ISO时间边界,`[from,to)`,前端按最终选定显示时区将日期范围转换,不能靠数据库会话时区猜测。
- page默认1、size默认20且1–200,keyword最长128;匹配dispatchNo/车牌/实际目的仓,不匹配内部ID。
- 四项统计与列表使用完全相同过滤条件,按dispatch去重,统计不带LIMIT;stage筛选也同时作用于统计。标题为“排车数”,没有“固定今天”分支。
- `recordVersion` 是本页附加版本,未登记时只读返回`0`,不因GET建记录;第一笔成功绑定/登记返回`1`。不占用、不回写主单recordVersion。
- `dispatchRecordVersion`仅用于原排车事实的只读定位;运营保存和照片绑定提交本页`recordVersion`,不可混用这两个版本。
- 0金额表示明确登记零;`money=null`表示未登记。Money存在时currency和amount必须成对完整,金额`DECIMAL(18,2)`非负;单价`amount/actualPalletCount`按HALF_UP两位,只读返回;未知/零托数返回null。
- 币种仅CNY/USD,不预选、不换汇;同一dispatch只有一份money,允许同一版本化编辑更正或清空,并留不可变历史。money=null与明确0严格区分。
- 多目的地实到以原车实际destinationKey集合为边界,不额外设置站数上限,禁止重复/不属于该车的key;不要求首站到达等于签收。输入时间必须带时区;不以estimatedArrivalAt或signedAt回填。
- actualDistanceMiles为mile;fuelConsumptionLitresPer100Km为手工登记的L/100km,均可空或明确0,非负、DECIMAL(12,3)。不派生总燃油量,不写默认18.6。
- 7个PhotoSlot来自原型最终函数的5个装车框和2个油耗框;`DRIVER_LICENSE`/`RECEIPT`只表示本页图片位置,不审批司机资格,也不触发派送签收。每框一个当前文件,替换保存不可变历史,不删除File对象。无照片清空入口,避免扩大当前原型操作。
## 4. 保存、命令恢复与错误
1. 编辑/绑定先核对平台、权限、真实自有资格和三个阶段,再读取/创建附加行。并发首次创建依dispatch唯一约束串行;同一附加行内CAS版本、命令成功结果及历史同事务提交。
2. 命令唯一键为dispatchId + idempotencyKey,另存operatorId并强制匹配;摘要包含action、原recordVersion和完整规范化载荷。同键同操作者同摘要返回原结果,不同操作者403、同键异载荷409。重放先于新写版本判断,不修改原版本重发。
3. 新增独立上传命令意图,文件调用前持久化`PENDING`且不持业务行锁跨HTTP;成功后记录原fileId和元数据,不推进附加版本。上传响应未知转`UNKNOWN_RESULT`且`retryAllowed=false`,禁止自动换键/重复上传;GET命令返回原已知状态。现有File没有此业务上传请求键的查找接口,因此不承诺把未知上传自动恢复为成功。
4. 绑定时复核File的tenantId=0、两个受控businessType之一、businessId=本单内部dispatchId、fileId、JPG/PNG及大小;绑定成功才标记照片已保存。上传失败不创建新的业务主单;已知成功上传的fileId可保留原请求另行明确恢复绑定,未知文件不伪造已上传。
5. 不持派送、库存、Finance或车辆占用写锁;只写本页附加行/命令/历史。源读取与CAS的并发保护需要第7节捕获的稳定装车事实;没有新排车状态机。
6. 入口沿现有HTTP200+业务code约定;缺版本/无效金额/非法目的地为参数错误,身份与权限拒绝为403/A0403,缺主单为404/NOT_FOUND,附加版本变化/源事实变化/同键异载荷为409/A0409。普通参数/权限/缺失/版本错误沿项目A0400/A0403/A0404/A0409;上传命令另存OWNED_UPLOAD_UNKNOWN_RESULT诊断码,正文不向用户展示内部ID。
## 5. 新增结构(根维护实际DDL)
全部在Lastleg schema,MySQL8,内部BIGINT主键;表与列按根要求完整中文COMMENT。新包不写初始化或增量SQL文件。
| 表 | 精确主要字段与约束 |
| --- | --- |
| `ll_fleet_owned_business_record` | `id BIGINT PK`;`dispatch_id BIGINT NOT NULL UNIQUE`;`record_version BIGINT NOT NULL`(持久行首次成功为1);`currency VARCHAR(3) NULL`;`total_amount DECIMAL(18,2) NULL`;`actual_distance_miles DECIMAL(12,3) NULL`;`fuel_consumption_litres_per_100km DECIMAL(12,3) NULL`;`arrivals_json JSON NOT NULL`;`photos_json JSON NOT NULL`;`created_by/updated_by VARCHAR(64)`、`created_at/updated_at DATETIME(3)`。Money成对、非负与JSON形状由业务层及可实现CHECK约束共同保证 |
| `ll_fleet_owned_business_command` | `id BIGINT PK`;`dispatch_id BIGINT`、`idempotency_key VARCHAR(128)`联合唯一;另存`operator_id VARCHAR(64) NOT NULL`;`action VARCHAR(32)`;`expected_version BIGINT NULL`(上传无需编辑版本);`request_hash CHAR(64)`;`request_json JSON`;`status VARCHAR(24)`;`result_json JSON NULL`;`error_code VARCHAR(64) NULL`;`created_at/updated_at DATETIME(3)`。保留原请求和原结果,不只保存hash |
| `ll_fleet_owned_business_history` | `id BIGINT PK`;`dispatch_id BIGINT`、`record_version BIGINT`唯一;`action VARCHAR(32)`、`operator_id VARCHAR(64)`、`reason VARCHAR(1000) NULL`、`snapshot_json JSON`、`occurred_at DATETIME(3)`;索引`(dispatch_id,record_version)` |
| `ll_fleet_owned_business_ownership` | `dispatch_id BIGINT PK`;`vehicle_id BIGINT NULL`;`resource_mode VARCHAR(16) NOT NULL`;`vehicle_ownership VARCHAR(16) NOT NULL`;`vehicle_record_version BIGINT NULL`;`ownership_captured_at DATETIME(3) NOT NULL`;`loading_completion_hash VARCHAR(128) NOT NULL`;`loading_completed_at DATETIME(3) NOT NULL`。MASTER读真实VehicleRepository;原dispatch EXTERNAL资源原样保存,并记DISPATCHED及null车辆版本;仅保存归属证据,不复制主单/货物/状态 |
金额、arrival、photo可以用有界JSON存附加行,暂不新增逐目的地/逐图片业务表;历史提供文件旧版本授权依据。并发初建使用唯一键+事务锁,不用未保存的随机业务ID假造记录;主单表不更新。
## 6. 查询与分层实现
新增包内 `controller/OwnedBusinessController`、`model/OwnedBusinessModels`、`service/OwnedBusinessQueryService`、`OwnedBusinessRecordService`、`OwnedBusinessPhotoService`、`repository/OwnedBusinessRepository`、`mapper/OwnedBusinessQueryMapper`、`OwnedBusinessRecordMapper`、`OwnedBusinessCommandMapper`、`OwnedBusinessHistoryMapper` 及`gateway/OwnedBusinessFileGateway`。
所有Mapper显式`@Mapper`,由既有Lastleg根扫描注册。GET为repeatable-read快照;count/四项统计共用同一SQL过滤片段,再做主单分页,页内目的地、预约及货物使用dispatchId集合批量查询,Mapper局部只读复用真实表,不循环调用排车GET或浏览器逐行补数据。细节API直接给三个页签所需数据,不把旧车次八页签整体搬来。
## 7. 根负责的精确外部触点
1. **已冻结唯一捕获hook**:根在DispatchAggregateService.loadingComplete最终确认时点同事务调用本包port.OwnedBusinessOwnershipCapture.capture(long dispatchId, Long vehicleId, String resourceMode, String loadingCompletionHash, Instant loadingCompletedAt)。实现经Repository→Mapper持久化,MASTER只普通读VehicleRepository真实归属/版本,不反向加车辆写锁;EXTERNAL记DISPATCHED。相同原事实捕获重放只返回,异事实拒绝;原命令已成功的回放不补写。
2. **旧已装车单**:无归属快照不回填、不在GET降级使用当前主档;本页不纳入,原排车读取完全不变。新原生样本用装车同事务快照验收。
3. **文件分类(根已明确冻结)**:`OVERSEAS_DISPATCH_LOADING_PHOTO`、`OVERSEAS_DISPATCH_FUEL_PHOTO`,平台tenantId固定0、businessId为原dispatch内部ID,JPG/PNG、每文件10MiB及内容签名校验。根唯一修改File分类、直接测试和契约;本包新增代理只按此消费,并承担原dispatch可见性、类别及当前/历史引用授权。不复用客户POD、司机、运维或车次运营类别授权,也不把dispatchId冒充tripId。内部HTTP沿`/internal/file/v1/business-files/0/{type}/{dispatchId}`,不将该地址或内部业务ID交浏览器。根共享File接线不阻断本包结构实现。
4. **IAM/导航/Gateway/SQL**:根集成3权限及角色授权、Admin路由菜单、Lastleg及统一基线、Gateway合同。路径已落在现有Lastleg域,但仍由根验证实际路由注册,不推定可达。
5. **照片保存语义已冻结**:逐槽上传后绑定立即保存;绑定失败保留原fileId和原绑定键恢复,不重复上传;绑定成功才显示已保存,关闭弹框不写数据。
## 8. 冻结与验证安排
v1所有业务项已冻结。仅根接共享hook、File、权限及4表基线;独占包继续实现,不等待其他无关任务。
本包实现及有意义的定向测试预计2–3小时;根统一排队的编译/Maven、共享接线和环境验收另计。每阶段交付记录在本设计末尾,实际通过仅按根执行证据报告。
## 实现检查点(根统一验证前)
- port.OwnedBusinessOwnershipCapture及MANDATORY捕获提供者已落源码;根只需现有唯一loadingComplete hook。原dispatch实际模式MASTER/EXTERNAL原样持久化,不引入另一套资源模式枚举。
- 原排车和附加流水由UNION ALL在同一查询排序分页,History区分eventSource和两个可空版本;原排车事件operations=null、photos=[],不复制原流水。
- 四表DML已按根当前lastleg.sql末部字段实现:更新显式写NULL;命令二元唯一且摘要含操作者/action/原版本/请求。源码与直接回归仍在收口,尚未运行任何检查。
### 根集成所需精确门禁触点
- `scripts/synchronous-dependency-baseline.mjs`:新增 `lastleg-service/.../fleet/ownedbusiness/gateway/HttpOwnedBusinessFileGateway.java` → file-service,REQUEST、READ_WRITE、MIXED。元数据和内容只读;上传无File远端幂等键,本域写前持久化意图、未知停写,不能自动重试。
- `lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json`:原baseline不允许宽泛fleet→dispatch-delivery,需要逐类只读边评审。本包实际复用为 Models→DispatchViews;Rows/QueryMapper/Repository仅使用本包Source与CargoRow只读投影,不依赖dispatch实体;Projection→公开DispatchContracts;QueryService→公开DispatchDetailReader/DispatchContracts/DispatchTypes。根hook的DispatchAggregateService→OwnedBusinessOwnershipCapture只依赖窄端口,不回调本页查询。后端不得借此访问或修改派送/库存/Finance持久化。
- OpenAPI新增文档数、Gateway自动发现Controller及Mapper注册合同均由根统一执行,不在本对话重复门禁;实际通过尚待根。
- 新附加历史从可信会话保存operatorName到snapshot;原排车历史只有operator_id、缺少姓名权威时operatorName=null,不把内部ID伪装成姓名。
- 根已冻结读取边界:本包不继承DispatchAggregate,不返回DispatchItem,不引用DispatchAggregateService。Source/CargoRow仅描述原SQL所需字段;详情经新公开DispatchDetailReader提供,根唯一维护适配及精确port依赖基线。
### 源码候选完成,等待根统一验证
- 新包生产16文件、独占测试包(夹具与7个测试类)、独立OpenAPI已完成首轮实现,未改主单/派送/财务/库存状态机;原4表DDL及hook根独占。
- 直接回归包含H2读取根4表基线、真实MyBatis NULL清空/CAS、并发0版本单胜出、原键重放、非法目的地不落记录、照片替换与历史引用、原主历史合并分页、同筛选四统计、归属捕获随父事务回滚、异账号恢复403及未知上传不重发。只是测试源码交付,尚未执行。
- 根可统一运行test-compile和7个OwnedBusiness*Test,再按影响运行Mapper、Gateway、同步依赖、端口边及OpenAPI门禁;本对话没有启动Maven、任何检查或环境操作。
### 交根统一验证的冻结输入
- 25路径:生产16、测试8、OpenAPI1;Ordinal路径排序,各行`path|sha256`,UTF-8/LF且末尾一个LF。
- 聚合SHA256:`b8d7c7d9108fe0475410f950579879ca5b4e3819d05f08a3b6e90cdc99ac456f`。
- 当前35个自有业务测试声明已由根实际执行,结果见下方当前验证记录。
- 根已修正两份DDL的money CHECK;新增真实基线回归覆盖币种NULL/金额非空拒绝、两者NULL合法、明确CNY零金额合法及再次清空。
- 根已登记5条公开port读取边、同步File客户端依赖,并报告静态无包环;本对话未运行门禁。后续仅按根确切失败定点解冻。
```text
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/controller/OwnedBusinessController.java|913b4df8cf80f21bc42ea59459ef2a04136ab0d563a0145c4c94e3f233d7fe9a
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/gateway/HttpOwnedBusinessFileGateway.java|4e2bbbd5292df8d15cb689eabf57d270f11c177c4503dc44e73387398cf1d3fd
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/gateway/OwnedBusinessFileGateway.java|a6a3baedfa380dde6f0c3e9a39d6a7f5bf2fdf9e2a4e9b47f8d989ee4f546cfc
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/mapper/OwnedBusinessMapper.java|6317c3428f10b2ccb6f3b71135e117e01e3da76ad742e4e1cb37f74caf808c60
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/mapper/OwnedBusinessQueryMapper.java|d36deb1f2840c103e96de1aab867b03aa80bed9e1f755778afa91db564a037d8
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/model/OwnedBusinessModels.java|c2c7a953c9c9cacb57b059daf6134828d160e0351be97d27e231b7c821a32c1e
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/model/OwnedBusinessRows.java|5d31468f7c5f3f177fe7aef4063bc8f5e71b3f37f56e0b49beeb531ffb16277a
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/port/OwnedBusinessOwnershipCapture.java|f354de8142cbab49a7d42ab6837c2ba836b4940560dbabb100b0642cadd05b08
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/repository/OwnedBusinessRepository.java|27c7cf185e65907923bbbf18a05f10bdfa5e091da608e44a798600a061917ad5
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessCommandService.java|ac507f63245436cb31a2b99001d7c5c7ff9ae2938e7417d795ae17c18cf462e8
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessOwnershipService.java|e1a6d10ab69d44915bbff958a28df970cbdd26b6bb19c83118877c29d09e8868
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessPhotoService.java|e951d0314f66e4b9f5ad17fa70e7c413cc5c6a74eee6ece560223fc1b5787658
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessProjection.java|0a3a2ddb6368add9af5696f119fdbe31a1e807edcfc34a4fb0487b60d11d3cbf
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessQueryService.java|39512a8321dc15e88080570a138ea8c8b56f8ff834c5bc3ada3db4c173b8a586
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessRecordService.java|09a6af10f9bd4ecba2eb50ecd7b338c9b01498bf7ac397a785e5f13ca63669b6
lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessSupport.java|e6f71eca2ecb7dd1a1803a5600aa954a7517091c2f1e7c7678b743ee591a20b7
lastleg-service/src/main/resources/openapi/fleet-owned-business-v1.yaml|34ced2f1cde024701bc9900fd0248de875d2500c47b817aab42d7edc91183364
lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessCommandTest.java|562ef97a01e96ad8e7e951c9ac01f175caa4ee2e1b2b4dca766b9b24902ff524
lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessFileGatewayTest.java|df97ee090d18b8f1663715275ee18135a0bc81061edab3dd3042c35ae91a2148
lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessFixtures.java|dca8eb00914c23e9eec9b776a1800f063e2189e7d3a4b16190d04f35fba8d341
lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessOwnershipTest.java|6f22c03e61a80b8f238253645510b3f941de6ec11f18e2c5df49c8a78ed14eb2
lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessPersistenceTest.java|97836b6665b90186e74a17ec26ffbd2d71238ec7a7fc64d3a091e8bfe45ada08
lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessPhotoTest.java|b4e5f907ce6c56ac7a382788685b24f5989dd018120d0d77739f0af33bb543a2
lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessProjectionTest.java|412711599c45a9d547397087bf9090f48ff3efa44226af48fe41452f9b732358
lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessQueryTest.java|12ab10a71a61ca24556238ea2ae9ce7f6321a242b426c7171a67204db03ce3e6
```
#### 当前验证记录
- Projection仅将DispatchContracts星号import展开为ConfirmedLoad/Resources两条显式import;业务方法未变。逆向还原import后精确得到原45f60872… SHA,根Maven覆盖当前显式import字节。
- 根报告session24317 BUILD SUCCESS:65项、0失败、0错误、0跳过;其中自有业务35、原DispatchAggregatePersistence24、只读适配1、Mapper2、中文schema3。本对话未执行Maven。
- 本轮只读YAML/JSON Pointer核对:48个唯一可达引用,读取2份直接相关OpenAPI,未解析引用0个。DispatchDetail对应DispatchViews.Detail的13字段、required集合及四块结构一致。这不代替根的完整公开API/权限门禁,也不代表部署或HTTP通过。
#### 照片历史中文文案定点修正(待根复验)
- 仅OwnedBusinessRecordService和OwnedBusinessPersistenceTest解冻:7个照片槽位保存原因改为准确中文,现有真实Mapper绑定/替换历史断言均核对中文。未改状态、版本、接口或其他源码。
- 上方65项通过仅覆盖此前候选;本次新两文件输入未由本对话执行检查,待根直接受影响回归,不能扩展旧结果。
#### multipart文档定点修正及计划发车非空核实
- 仅独立OpenAPI解冻:上传idempotencyKey从query移到multipart requestBody,与file共同required,长度8–128保持;Controller、Java DTO、前端及业务逻辑均未改。
- plannedDepartureAt保留非空string:DispatchAggregateService.loadingComplete在写装车完成及捕获归属前调用verifyReviewResources;该方法明确拒绝row.plannedDepartureAt=null(原方法约602行)。原编辑入口requireEditing仅允许BEFORE_LOADING且零扫码,装车后不能经此清空计划发车。自有视图只纳入有新装车归属快照的三个阶段,因此正常受控写路径已保证非空;不新增业务必填门禁,不要求前端类型改为nullable。缺失/损坏存量不作为兼容依据,原无快照TEST仍不回填。
- 本次只读核对并修改文档;公开OpenAPI/权限门禁由根安排,未运行检查或环境HTTP。