优化
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
|
||||
拼车余位发布查 `fleet/trip/FleetCapacityOfferService.java`;整车建单与准备查 `dispatch/controller/DispatchAggregateController.java` 和 `dispatch/service/DispatchAggregateService.java`,本土业务接线查 `fleet/business/`;既有外接登记衔接合同为 `fleet-preparation-v1.yaml`,排车流程按 `../../docs/海外仓业务/排车列表统一作业改造契约.md`。
|
||||
|
||||
原排车装车后运营视图分别查 `fleet/ownedbusiness/`、`fleet/outsourcedbusiness/` 与各自 `fleet-*-business-v1.yaml`;自有/外配按同事务捕获的车辆归属,不按货源区分。两入口可信作用域及权限独立,外配仅整车承运金额、逐目的仓到达和五装车照片;复用四张附加表,不修改原履约/库存/财务状态。共同业务规则见 `../../docs/海外仓业务/自有业务首版开发契约.md`、`../../docs/海外仓业务/外配业务首版开发契约.md`,实际发布及HTTP只查部署主契约。
|
||||
原排车装车后运营视图分别查 `fleet/ownedbusiness/`、`fleet/outsourcedbusiness/` 与各自 `fleet-*-business-v1.yaml`;自有业务按无实际拼车货物、外配任务按已实际接入需求池货物分类;车辆归属仍读取装车快照,完整判定唯一见外配业务契约。两入口可信作用域及权限独立,外配仅整车承运金额、逐目的仓到达和五装车照片;复用四张附加表,不修改原履约/库存/财务状态。共同业务规则见 `../../docs/海外仓业务/自有业务首版开发契约.md`、`../../docs/海外仓业务/外配业务首版开发契约.md`,实际发布及HTTP只查部署主契约。
|
||||
|
||||
事件重试:`controller/EventOperationsController.java`,内部 `/internal/lastleg/event-operations/{entryId}` 查询、`/retry` 写入;仅卸柜、派送、暂扣、资金及费用事件,以 `EVENT_OPERATIONS_TOKEN` 保护。原事件不可改,DEAD→PENDING 与 `event_delivery_retry_audit` 同事务;脚本 `scripts/event-outbox-operations.mjs`。
|
||||
|
||||
|
||||
@@ -85,8 +85,8 @@
|
||||
|
||||
- 车次成本:`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/outsourcedbusiness/`、`fleet-outsourced-business-v1.yaml`,共同规则见`../../../docs/海外仓业务/外配业务首版开发契约.md`;原排车装车后三阶段按冻结DISPATCHED归属展示,包括正式MASTER/EXTERNAL两种资源模式。只登记整车承运金额、逐目的仓到达和五装车照片,无里程/油耗或成本拆分。内部可信作用域复用四表,入口权限与自有严格分开,不引入第二履约模型。
|
||||
- 自有业务运营:`fleet/ownedbusiness/`、`fleet-owned-business-v1.yaml`,共同规则见`../../../docs/海外仓业务/自有业务首版开发契约.md`;原排车装车后三阶段按无实际拼车货物展示,车辆归属独立,仅补金额、逐目的地到达、里程、油耗与照片。原详情只经`dispatch.port.DispatchDetailReader`读取,原装车事务经`OwnedBusinessOwnershipCapture`捕获归属,不另建履约流程。
|
||||
- 外配业务运营:`fleet/outsourcedbusiness/`、`fleet-outsourced-business-v1.yaml`,共同规则见`../../../docs/海外仓业务/外配业务首版开发契约.md`;原排车装车后三阶段按实际接入需求池拼车货物展示,车辆归属独立,包含MASTER/EXTERNAL两种资源模式。只登记整车承运金额、逐目的仓到达和五装车照片,无里程/油耗或成本拆分。内部可信作用域复用四表,入口权限与自有严格分开,不引入第二履约模型。
|
||||
- 外接选价合同:`fleet-quote-candidates-v1.yaml`、`fleet-trips-v1.yaml`;来源快照与成交价分开,确认承运复验。
|
||||
- 在途交接:`fleet/exception/`、`fleet-handovers-v1.yaml`;后台确认交接,旧资源分别结束任务,DEPART不重写。
|
||||
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ public class OutsourcedBusinessService {
|
||||
}
|
||||
/** 临时车辆合法保留空vehicleId;承运商缺失保留null供页面显示破折号。 */
|
||||
private Row row(OwnedBusinessModels.Row r,String carrierName) {
|
||||
if(!"DISPATCHED".equals(r.vehicle().ownership()) || r.photoSummary().fuelCount()!=0)throw conflict("外配运营快照与冻结归属不一致");
|
||||
if(!java.util.Set.of("OWNED","DISPATCHED").contains(r.vehicle().ownership()) || r.photoSummary().fuelCount()!=0)throw conflict("外配运营快照的车辆归属或照片不合法");
|
||||
return new Row(r.dispatchNo(),r.stage(),r.plannedDepartureAt(),r.loadingCompletedAt(),r.departedAt(),r.deliveryCompletedAt(),r.origin(),
|
||||
new Vehicle(r.vehicle().vehicleId(),r.vehicle().plate(),r.vehicle().ownership(),carrierName==null || carrierName.isBlank()?null:carrierName),r.driver(),r.destinations(),
|
||||
r.actualPalletCount(),operations(r.operations()),r.unitPrice(),r.recordVersion(),r.dispatchRecordVersion(),new PhotoSummary(r.photoSummary().loadingCount()),r.actions(),r.sourceIssues());
|
||||
|
||||
+21
-3
@@ -7,8 +7,24 @@ import java.util.List;
|
||||
/** 在原排车事实之上做自有运营分页,不复制或推进履约模型。 */
|
||||
@Mapper
|
||||
public interface OwnedBusinessQueryMapper {
|
||||
/** 拼车登记须已实收并绑定本车,且原实收来源仍在本车计划托盘组成内;多票只判存在。 */
|
||||
String POOLED_CARGO="""
|
||||
EXISTS (SELECT 1 FROM ll_fleet_external_booking b
|
||||
JOIN ll_warehouse_transfer_order incoming ON incoming.customer_id=b.customer_id AND incoming.transfer_no=b.incoming_no
|
||||
WHERE b.trip_id=d.trip_id AND b.dispatch_no=d.dispatch_no AND b.offer_id IS NOT NULL
|
||||
AND b.status='BOUND' AND b.receipt_json IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM ll_dispatch_item cargo
|
||||
JOIN ll_inbound_plan_pallet_allocation allocation ON allocation.pallet_id=cargo.plan_pallet_id
|
||||
AND allocation.plan_id=cargo.plan_id AND allocation.customer_id=cargo.customer_id
|
||||
JOIN ll_inbound_plan_line line ON line.id=allocation.line_id AND line.plan_id=cargo.plan_id
|
||||
WHERE cargo.dispatch_order_id=d.id AND cargo.customer_id=b.customer_id
|
||||
AND line.customer_id=b.customer_id AND line.inbound_code=incoming.inbound_code
|
||||
AND cargo.warehouse_pallet_id IS NOT NULL AND cargo.package_count>0))
|
||||
""";
|
||||
String SOURCE="""
|
||||
SELECT d.id,d.dispatch_no,d.status,d.stage,d.record_version,d.overseas_warehouse_id,
|
||||
SELECT
|
||||
"""+POOLED_CARGO+" AS pooled_cargo,"+"""
|
||||
d.id,d.dispatch_no,d.status,d.stage,d.record_version,d.overseas_warehouse_id,
|
||||
d.overseas_warehouse_code,d.overseas_warehouse_name,d.planned_departure_at,d.loading_completed_at,
|
||||
d.departed_at,d.delivery_completed_at,d.loading_completion_hash,d.loading_confirmation_json,d.resources_json,
|
||||
o.vehicle_id AS owned_vehicle_id,o.resource_mode AS owned_resource_mode,
|
||||
@@ -20,10 +36,12 @@ public interface OwnedBusinessQueryMapper {
|
||||
String FILTER="""
|
||||
d.status='ACTIVE' AND d.stage IN ('AFTER_LOADING','DELIVERING','DELIVERED')
|
||||
AND d.loading_completed_at IS NOT NULL AND d.loading_confirmation_json IS NOT NULL
|
||||
AND o.vehicle_ownership=#{q.scope.ownership}
|
||||
AND o.vehicle_ownership IN ('OWNED','DISPATCHED')
|
||||
AND CAST(o.loading_completion_hash AS BINARY(128))=CAST(d.loading_completion_hash AS BINARY(128))
|
||||
AND o.loading_completed_at=d.loading_completed_at
|
||||
AND (o.resource_mode='MASTER' OR (#{q.scope.ownership}='DISPATCHED' AND o.resource_mode='EXTERNAL'))
|
||||
AND (o.resource_mode='MASTER' OR (o.vehicle_ownership='DISPATCHED' AND o.resource_mode='EXTERNAL'))
|
||||
AND <if test='!q.scope.pooledCargo'>NOT</if>
|
||||
"""+POOLED_CARGO+"""
|
||||
<if test='q.from != null'> AND d.planned_departure_at >= #{q.from}</if>
|
||||
<if test='q.to != null'> AND d.planned_departure_at < #{q.to}</if>
|
||||
<if test='q.stage != null'> AND d.stage=#{q.stage}</if>
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ public final class OwnedBusinessRows {
|
||||
private LocalDateTime ownershipCapturedAt,loadingCompletedAt;
|
||||
}
|
||||
@Data public static class Source {
|
||||
private Boolean pooledCargo;
|
||||
private Long id,recordVersion;
|
||||
private String dispatchNo,status,stage,overseasWarehouseId,overseasWarehouseCode,overseasWarehouseName;
|
||||
private String loadingCompletionHash,loadingConfirmationJson,resourcesJson;
|
||||
|
||||
+9
-8
@@ -6,13 +6,14 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
|
||||
/** 仅服务端固定入口选择的作用域;不得成为浏览器请求字段或权限别名。 */
|
||||
public enum OwnedBusinessScope {
|
||||
OWNED("OWNED","platform:fleet:owned-business:"),
|
||||
OUTSOURCED("DISPATCHED","platform:fleet:outsourced-business:");
|
||||
private final String ownership,prefix;
|
||||
/** 车辆归属与入口权限一一固定,不能由请求指定。 */
|
||||
OwnedBusinessScope(String ownership,String prefix){this.ownership=ownership;this.prefix=prefix;}
|
||||
/** SQL仅绑定此可信枚举的归属值,不拼接客户端片段。 */
|
||||
public String getOwnership(){return ownership;}
|
||||
OWNED(false,"platform:fleet:owned-business:"),
|
||||
OUTSOURCED(true,"platform:fleet:outsourced-business:");
|
||||
private final boolean pooledCargo;
|
||||
private final String prefix;
|
||||
/** 业务入口按是否实际接入拼车货物分类,与车辆归属独立。 */
|
||||
OwnedBusinessScope(boolean pooledCargo,String prefix){this.pooledCargo=pooledCargo;this.prefix=prefix;}
|
||||
/** SQL绑定服务端固定分类,不接受浏览器指定作用域。 */
|
||||
public boolean isPooledCargo(){return pooledCargo;}
|
||||
/** 查看权限包含本作用域详情、历史和照片读取。 */
|
||||
public String listPermission(){return prefix+"list";}
|
||||
/** 运营写权限独立,外配权限不映射成自有权限。 */
|
||||
@@ -22,5 +23,5 @@ public enum OwnedBusinessScope {
|
||||
/** 外配只支持装车五框,服务调用同样不能绕过。 */
|
||||
public void requireSlot(PhotoSlot slot){if(slot==null || this==OUTSOURCED && !"LOADING".equals(slot.category()))throw new BizException(ResultCode.BAD_REQUEST,"当前业务不支持该照片框");}
|
||||
/** 不存在只显示当前业务归属,不泄露内部身份。 */
|
||||
public BizException missing(){return new BizException(ResultCode.NOT_FOUND,"排车单不存在或不属于当前"+(this==OWNED?"自有":"外配")+"业务");}
|
||||
public BizException missing(){return new BizException(ResultCode.NOT_FOUND,"排车单不存在或不属于当前"+(this==OWNED?"自有业务":"外配任务"));}
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ public class OwnedBusinessProjection {
|
||||
return new Row(source.getDispatchNo(),Stage.valueOf(source.getStage()),codec.iso(source.getPlannedDepartureAt()),
|
||||
codec.iso(source.getLoadingCompletedAt()),codec.iso(source.getDepartedAt()),codec.iso(source.getDeliveryCompletedAt()),
|
||||
new Origin(source.getOverseasWarehouseId(),source.getOverseasWarehouseCode(),source.getOverseasWarehouseName()),
|
||||
new Vehicle(resources.vehicle().vehicleId()==null?null:String.valueOf(resources.vehicle().vehicleId()),resources.vehicle().plate(),scope.getOwnership()),
|
||||
new Vehicle(resources.vehicle().vehicleId()==null?null:String.valueOf(resources.vehicle().vehicleId()),resources.vehicle().plate(),source.getOwnedVehicleOwnership()),
|
||||
new Driver(driver==null?null:driver.name(),driver==null?null:driver.phone()),List.copyOf(destinations.values()),pallets,ops,price,
|
||||
record==null?"0":String.valueOf(record.getRecordVersion()),String.valueOf(source.getRecordVersion()),
|
||||
new PhotoSummary((int)photos.stream().filter(p->"LOADING".equals(p.category())).count(),(int)photos.stream().filter(p->"FUEL".equals(p.category())).count()),
|
||||
|
||||
+7
-5
@@ -17,7 +17,7 @@ import java.util.stream.Collectors;
|
||||
import static com.baiyue.lastleg.fleet.ownedbusiness.service.OwnedBusinessSupport.*;
|
||||
import static com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.*;
|
||||
|
||||
/** 自有车辆装车后视图;无归属快照的旧单不在GET时回填。 */
|
||||
/** 装车后按实际拼车货物分组;车辆归属仅保留原装车证据。 */
|
||||
@Service
|
||||
public class OwnedBusinessQueryService {
|
||||
private final OwnedBusinessRepository repository;
|
||||
@@ -61,7 +61,7 @@ public class OwnedBusinessQueryService {
|
||||
/** 对原排车详情做组合,不创建第二份主单或履约状态。 */
|
||||
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
||||
public Detail detail(Actor actor,String number){codec.require(actor,LIST);return detailForWrite(actor,requireOwned(number));}
|
||||
/** 外配入口只能读取其冻结归属,不能借自有权限取得原详情。 */
|
||||
/** 外配入口只读实际拼车任务,不能借自有权限取得原详情。 */
|
||||
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
||||
public Detail detail(Actor actor,String number,OwnedBusinessScope scope){codec.require(actor,scope.listPermission());return detailForWrite(actor,requireScoped(number,scope),scope);}
|
||||
/** 写入已经有独立权限;回执仍由相同真实来源组成。 */
|
||||
@@ -82,7 +82,7 @@ public class OwnedBusinessQueryService {
|
||||
if(number==null || number.isBlank() || number.length()>128)throw bad("请指定排车单号");
|
||||
var source=repository.source(number);if(source==null)throw missing();eligible(source,OwnedBusinessScope.OWNED);return source;
|
||||
}
|
||||
/** 共享附加表仍按原装车归属隔离,不接受浏览器业务域参数。 */
|
||||
/** 共享附加表按实际拼车分类隔离,不接受浏览器业务域参数。 */
|
||||
public Source requireScoped(String number,OwnedBusinessScope scope) {
|
||||
if(scope==OwnedBusinessScope.OWNED)return requireOwned(number);
|
||||
if(number==null || number.isBlank() || number.length()>128)throw bad("请指定排车单号");
|
||||
@@ -91,13 +91,15 @@ public class OwnedBusinessQueryService {
|
||||
/** 阶段、真实装车hash及车辆快照须与捕获证据一致。 */
|
||||
private void eligible(Source source,OwnedBusinessScope scope) {
|
||||
if(source.getOwnedVehicleOwnership()==null)throw conflict("本单缺少装车时车辆归属证据,不能归入"+(scope==OwnedBusinessScope.OWNED?"自有":"外配")+"业务");
|
||||
if(!scope.getOwnership().equals(source.getOwnedVehicleOwnership()))throw scope.missing();
|
||||
if(!Set.of("OWNED","DISPATCHED").contains(source.getOwnedVehicleOwnership()))throw conflict("装车车辆归属不合法");
|
||||
if(source.getPooledCargo()==null)throw conflict("本单缺少实际拼车分类依据");
|
||||
if(scope.isPooledCargo()!=source.getPooledCargo())throw scope.missing();
|
||||
if(!"ACTIVE".equals(source.getStatus()) || source.getStage()==null || !Set.of("AFTER_LOADING","DELIVERING","DELIVERED").contains(source.getStage())
|
||||
|| source.getLoadingCompletedAt()==null || source.getLoadingConfirmationJson()==null)throw conflict("本单尚无合法装车完成事实");
|
||||
var resource=codec.read(source.getResourcesJson(),DispatchContracts.Resources.class).vehicle();
|
||||
boolean master=resource!=null && resource.mode()!=null && "MASTER".equals(resource.mode().name()) && "MASTER".equals(source.getOwnedResourceMode())
|
||||
&& resource.vehicleId()!=null && Objects.equals(source.getOwnedVehicleId(),resource.vehicleId());
|
||||
boolean external=scope==OwnedBusinessScope.OUTSOURCED && resource!=null && resource.mode()!=null && "EXTERNAL".equals(resource.mode().name())
|
||||
boolean external="DISPATCHED".equals(source.getOwnedVehicleOwnership()) && resource!=null && resource.mode()!=null && "EXTERNAL".equals(resource.mode().name())
|
||||
&& "EXTERNAL".equals(source.getOwnedResourceMode()) && resource.vehicleId()==null && source.getOwnedVehicleId()==null;
|
||||
if((!master && !external) || !Objects.equals(source.getOwnedLoadingCompletionHash(),source.getLoadingCompletionHash())
|
||||
|| !Objects.equals(source.getOwnedLoadingCompletedAt(),source.getLoadingCompletedAt()))throw conflict("车辆归属与原装车事实不一致");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
openapi: "3.0.3"
|
||||
info:
|
||||
title: "原排车外配车辆运营视图"
|
||||
title: "原排车拼车任务运营视图"
|
||||
version: "1.0.0"
|
||||
description: "仅ACTIVE且装车后/派送中/已完成。按原装车归属DISPATCHED及MASTER/EXTERNAL资源快照读取;无快照不回填。复用四张附加表,不推进排车、不触发库存/财务。承运金额CNY/USD显式登记;单价除以实际物理托数并HALF_UP两位。无里程、油耗及油耗照片。响应遵循NON_NULL:可空属性可省略,省略与null均表示空值,0仍是明确登记值;请求必填项仍以required及DTO校验为准。"
|
||||
description: "仅ACTIVE且装车后/派送中/已完成。按需求池已实收绑定及本车真实货物组成判定实际拼车。车辆归属允许OWNED/DISPATCHED,MASTER/EXTERNAL只表示资源身份;无快照不回填。复用四张附加表,不推进排车、不触发库存/财务。承运金额CNY/USD显式登记;单价除以实际物理托数并HALF_UP两位。无里程、油耗及油耗照片。响应遵循NON_NULL:可空属性可省略,省略与null均表示空值,0仍是明确登记值;请求必填项仍以required及DTO校验为准。"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
@@ -182,7 +182,7 @@ components:
|
||||
properties:
|
||||
vehicleId: { type: string, nullable: true, description: "原临时EXTERNAL车辆合法为空,档案MASTER核对原捕获ID" }
|
||||
plate: { type: string, nullable: true }
|
||||
ownership: { type: string, enum: [DISPATCHED] }
|
||||
ownership: { type: string, enum: [OWNED, DISPATCHED] }
|
||||
carrierName: { type: string, nullable: true, description: "只取原车辆快照,缺失显示—;不查当前档案补值" }
|
||||
PhotoSlot:
|
||||
type: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
openapi: "3.0.3"
|
||||
info:
|
||||
title: "原排车自有车辆运营视图"
|
||||
title: "原排车未拼车业务运营视图"
|
||||
version: "1.0.0"
|
||||
description: "装车完成时冻结车辆OWNED/DISPATCHED;所有主单事实来自原dispatch。新增数据仅附加运营记录、命令、历史及归属证据。油耗L/100km手工登记,金额CNY/USD不默认、不换汇;零与未登记不同。响应遵循NON_NULL:可空属性可省略,省略与null均表示空值,0仍是明确登记值;请求必填项仍以required及DTO校验为准。"
|
||||
description: "未实际接入拼车货物的装车后排车;按需求池已实收绑定及本车真实货物组成判定。车辆OWNED/DISPATCHED只表示装车时归属,不决定业务分类;所有主单事实来自原dispatch。新增数据仅附加运营记录、命令、历史及归属证据。油耗L/100km手工登记,金额CNY/USD不默认、不换汇;零与未登记不同。响应遵循NON_NULL:可空属性可省略,省略与null均表示空值,0仍是明确登记值;请求必填项仍以required及DTO校验为准。"
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
@@ -10,7 +10,7 @@ paths:
|
||||
"/api/platform/v1/last-leg/fleet/owned-business":
|
||||
get:
|
||||
operationId: "listOwnedBusiness"
|
||||
summary: "自有车辆装车后视图及同筛选四项统计"
|
||||
summary: "未接入拼车货物的装车后视图及同筛选四项统计"
|
||||
description: "权限:platform:fleet:owned-business:list。仅可信平台主体;只读原排车事实,不改变其状态/版本,不发布费用。"
|
||||
x-permission-code: "platform:fleet:owned-business:list"
|
||||
responses:
|
||||
@@ -540,18 +540,19 @@ components:
|
||||
type: "object"
|
||||
x-java-record: "OwnedBusinessModels.Vehicle"
|
||||
required:
|
||||
- "vehicleId"
|
||||
- "plate"
|
||||
- "ownership"
|
||||
properties:
|
||||
vehicleId:
|
||||
type: "string"
|
||||
nullable: true
|
||||
plate:
|
||||
type: "string"
|
||||
nullable: true
|
||||
ownership:
|
||||
type: "string"
|
||||
enum:
|
||||
- "OWNED"
|
||||
- "DISPATCHED"
|
||||
Driver:
|
||||
type: "object"
|
||||
x-java-record: "OwnedBusinessModels.Driver"
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ final class OwnedBusinessFixtures {
|
||||
/** 装车完成但未发车,证明不依赖DEPART历史。 */
|
||||
static Source source() {
|
||||
var row=new Source();row.setId(1L);row.setDispatchNo("DS-OWN-ONE");row.setStatus("ACTIVE");row.setStage("AFTER_LOADING");
|
||||
row.setPooledCargo(false);
|
||||
row.setRecordVersion(8L);row.setOverseasWarehouseId("9");row.setOverseasWarehouseCode("WH-9");row.setOverseasWarehouseName("测试仓");
|
||||
row.setPlannedDepartureAt(LocalDateTime.parse("2026-10-20T08:00:00"));row.setLoadingCompletedAt(LocalDateTime.parse("2026-10-20T07:30:00"));
|
||||
row.setLoadingCompletionHash("actual-load");row.setOwnedLoadingCompletionHash("actual-load");row.setOwnedLoadingCompletedAt(row.getLoadingCompletedAt());
|
||||
|
||||
+5
-1
@@ -50,8 +50,12 @@ class OwnedBusinessPersistenceTest {
|
||||
customer_name VARCHAR(128),source_key VARCHAR(128),order_no VARCHAR(128),inbound_code VARCHAR(128),
|
||||
pallet_no VARCHAR(128),warehouse_pallet_id BIGINT,destination_type VARCHAR(32),
|
||||
destination_warehouse_code VARCHAR(128),destination_warehouse_name VARCHAR(128),fba_warehouse_code VARCHAR(128),
|
||||
delivery_address VARCHAR(256),postal_code VARCHAR(64),package_count INT)
|
||||
delivery_address VARCHAR(256),postal_code VARCHAR(64),package_count INT,plan_id BIGINT,plan_pallet_id BIGINT)
|
||||
""");
|
||||
jdbc.execute("CREATE TABLE ll_fleet_external_booking(id BIGINT PRIMARY KEY,trip_id BIGINT,dispatch_no VARCHAR(128),customer_id VARCHAR(128),offer_id BIGINT,status VARCHAR(32),incoming_no VARCHAR(128),receipt_json LONGTEXT)");
|
||||
jdbc.execute("CREATE TABLE ll_warehouse_transfer_order(customer_id VARCHAR(128),transfer_no VARCHAR(128),inbound_code VARCHAR(128))");
|
||||
jdbc.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT PRIMARY KEY,plan_id BIGINT,customer_id VARCHAR(128),inbound_code VARCHAR(128))");
|
||||
jdbc.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT,plan_id BIGINT,customer_id VARCHAR(128))");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_appointment_allocation(dispatch_id BIGINT,dispatch_item_id BIGINT,customer_id VARCHAR(64),batch_key VARCHAR(128),destination_key VARCHAR(128),package_count INT)");
|
||||
jdbc.execute("CREATE TABLE ll_inbound_fba_appointment(id BIGINT,appointment_no VARCHAR(64),confirmation_no VARCHAR(64),appointment_start_utc DATETIME(3))");
|
||||
jdbc.execute("""
|
||||
|
||||
+4
-3
@@ -41,14 +41,15 @@ class OwnedBusinessQueryTest {
|
||||
assertThrows(BizException.class,()->service.detail(OwnedBusinessFixtures.actor(),"DS-OWN-ONE"));
|
||||
verifyNoInteractions(dispatch);verify(repo,never()).capture(any());assertEquals(8L,source.getRecordVersion());
|
||||
}
|
||||
/** 装车前、非自有归属、失配的原装车hash均不能作为本页可写来源。 */
|
||||
/** 装车前、已实际拼车、失配的装车hash均不可写;未拼车的外配车辆仍属自有业务。 */
|
||||
@Test void stageAndFrozenOwnershipFactsAreBothRequired() {
|
||||
var repo=mock(OwnedBusinessRepository.class);var codec=OwnedBusinessFixtures.codec();
|
||||
var service=new OwnedBusinessQueryService(repo,new OwnedBusinessProjection(codec),codec,mock(DispatchDetailReader.class));
|
||||
var source=OwnedBusinessFixtures.source();when(repo.source("DS-OWN-ONE")).thenReturn(source);
|
||||
source.setStage("LOADING");assertThrows(BizException.class,()->service.requireOwned("DS-OWN-ONE"));
|
||||
source.setStage("AFTER_LOADING");source.setOwnedVehicleOwnership("DISPATCHED");assertThrows(BizException.class,()->service.requireOwned("DS-OWN-ONE"));
|
||||
source.setOwnedVehicleOwnership("OWNED");source.setOwnedLoadingCompletionHash("other");assertThrows(BizException.class,()->service.requireOwned("DS-OWN-ONE"));
|
||||
source.setStage("AFTER_LOADING");source.setOwnedVehicleOwnership("DISPATCHED");assertSame(source,service.requireOwned("DS-OWN-ONE"));
|
||||
source.setPooledCargo(true);assertThrows(BizException.class,()->service.requireOwned("DS-OWN-ONE"));
|
||||
source.setPooledCargo(false);source.setOwnedLoadingCompletionHash("other");assertThrows(BizException.class,()->service.requireOwned("DS-OWN-ONE"));
|
||||
}
|
||||
/** 列表权限不自动授予编辑或上传,不暴露主流程写动作。 */
|
||||
@Test void readOnlyActorHasOnlyReadGates() {
|
||||
|
||||
+58
-2
@@ -75,7 +75,7 @@ class OwnedBusinessScopePersistenceTest {
|
||||
""",id,"DS-OUT-"+id,stage,load,snapshot,"TEST-OUT-"+id);
|
||||
baseline.jdbc.update("""
|
||||
INSERT INTO ll_dispatch_item SELECT id+?, ?,customer_id,customer_name,source_key,order_no,inbound_code,?, ?,destination_type,
|
||||
destination_warehouse_code,destination_warehouse_name,fba_warehouse_code,delivery_address,postal_code,package_count FROM ll_dispatch_item WHERE dispatch_order_id=1
|
||||
destination_warehouse_code,destination_warehouse_name,fba_warehouse_code,delivery_address,postal_code,package_count,NULL,NULL FROM ll_dispatch_item WHERE dispatch_order_id=1
|
||||
""",id*100,id,"PAL-"+(31+id),31+id);
|
||||
var ownership=new OwnedBusinessRows.Ownership();ownership.setDispatchId(id);ownership.setVehicleId(vehicle.vehicleId());
|
||||
ownership.setResourceMode(mode);ownership.setVehicleOwnership("DISPATCHED");ownership.setVehicleRecordVersion(vehicle.vehicleId()==null?null:3L);
|
||||
@@ -83,6 +83,12 @@ class OwnedBusinessScopePersistenceTest {
|
||||
baseline.jdbc.update("INSERT INTO ll_dispatch_history VALUES(?,?,8,'LOADING_COMPLETE','TEST-picker',NULL,'{}',?)",100+id,id,original.getLoadingCompletedAt());
|
||||
if(!"AFTER_LOADING".equals(stage))baseline.jdbc.update("UPDATE ll_dispatch_order SET departed_at=? WHERE id=?",original.getLoadingCompletedAt().plusHours(1),id);
|
||||
if("DELIVERED".equals(stage))baseline.jdbc.update("UPDATE ll_dispatch_order SET delivery_completed_at=? WHERE id=?",original.getLoadingCompletedAt().plusHours(2),id);
|
||||
// 每张外配测试任务真实关联一票拼车来源,车辆归属不再充当分类依据。
|
||||
baseline.jdbc.update("UPDATE ll_dispatch_item SET plan_id=?,plan_pallet_id=? WHERE dispatch_order_id=? AND customer_id='101'",id*1000,id*1000+1,id);
|
||||
baseline.jdbc.update("INSERT INTO ll_inbound_plan_line VALUES(?,?,'101','IN-A')",id*1000+2,id*1000);
|
||||
baseline.jdbc.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(?,?,?,'101')",id*1000+1,id*1000+2,id*1000);
|
||||
baseline.jdbc.update("INSERT INTO ll_warehouse_transfer_order VALUES('101',?,'IN-A')","INCOMING-"+id);
|
||||
baseline.jdbc.update("INSERT INTO ll_fleet_external_booking VALUES(?,2,?,'101',9,'BOUND',?,'{}')",id,"DS-OUT-"+id,"INCOMING-"+id);
|
||||
}
|
||||
/** 外配真实三权限,与自有集合完全不相交。 */
|
||||
private OwnedBusinessModels.Actor actor(){return new OwnedBusinessModels.Actor("outsourced-operator","TEST外配操作员",Set.of(OutsourcedBusinessModels.LIST,OutsourcedBusinessModels.EDIT,OutsourcedBusinessModels.UPLOAD));}
|
||||
@@ -101,6 +107,56 @@ class OwnedBusinessScopePersistenceTest {
|
||||
assertEquals(0,outsourced.list(actor(),new OwnedBusinessModels.Query(1,20,null,"2026-10-20T08:00:00Z",null,null,"9")).total());
|
||||
for(var stage:OwnedBusinessModels.Stage.values())assertEquals(1,outsourced.list(actor(),new OwnedBusinessModels.Query(1,20,"2026-10-20T08:00:00Z","2026-10-21T08:00:00Z",null,stage,"9")).total());
|
||||
}
|
||||
/** 四种车辆/拼车组合只按货物分类;临时外配未拼车也可从自有入口登记。 */
|
||||
@Test void classificationIsIndependentOfVehicleOwnershipAndResourceMode() {
|
||||
baseline.jdbc.update("UPDATE ll_fleet_owned_business_ownership SET vehicle_ownership='OWNED' WHERE dispatch_id=2");
|
||||
baseline.jdbc.update("UPDATE ll_fleet_external_booking SET status='RECEIVED',dispatch_no=NULL WHERE id=3");
|
||||
baseline.jdbc.update("UPDATE ll_fleet_owned_business_ownership SET vehicle_ownership='DISPATCHED' WHERE dispatch_id=1");
|
||||
var query=new OwnedBusinessModels.Query(1,20,null,null,null,null,null);
|
||||
assertEquals(2,outsourced.list(actor(),query).total());assertEquals(2,queries.list(OwnedBusinessFixtures.actor(),query).total());
|
||||
assertEquals("OWNED",outsourced.detail(actor(),"DS-OUT-2").row().vehicle().ownership());
|
||||
var external=queries.detail(OwnedBusinessFixtures.actor(),"DS-OUT-3");
|
||||
assertNull(external.row().vehicle().vehicleId());assertEquals("DISPATCHED",external.row().vehicle().ownership());
|
||||
var saved=records.save(OwnedBusinessFixtures.actor(),"DS-OUT-3",new OwnedBusinessModels.SaveOperations("0","non-pool-external-save",
|
||||
new OwnedBusinessModels.Operations(new OwnedBusinessModels.Money("USD","0"),null,null,List.of()),null));
|
||||
assertEquals("1",saved.row().recordVersion());
|
||||
var pooled=outsourced.save(actor(),"DS-OUT-2",request("0","pool-owned-save",new OwnedBusinessModels.Money("USD","25"),List.of()));
|
||||
assertEquals("OWNED",pooled.row().vehicle().ownership());assertEquals("25.00",pooled.row().unitPrice());
|
||||
assertThrows(BizException.class,()->queries.detail(OwnedBusinessFixtures.actor(),"DS-OUT-2"));
|
||||
assertThrows(BizException.class,()->outsourced.detail(actor(),"DS-OUT-3"));
|
||||
}
|
||||
/** 发布、询价、占位、实收未排入、取消和非池来源均不计拼车,详情与完整统计一致。 */
|
||||
@Test void onlyReceivedPoolCargoBoundToThisDispatchCounts() {
|
||||
var query=new OwnedBusinessModels.Query(1,1,null,null,null,null,null);
|
||||
for(String status:List.of("DRAFT","CONFIRMED","RECEIVED","CANCELLED")) {
|
||||
baseline.jdbc.update("UPDATE ll_fleet_external_booking SET status=? WHERE id=2",status);
|
||||
assertEquals(2,outsourced.list(actor(),query).total());assertEquals(2,queries.list(OwnedBusinessFixtures.actor(),query).total());
|
||||
assertNotNull(queries.detail(OwnedBusinessFixtures.actor(),"DS-OUT-2"));
|
||||
assertThrows(BizException.class,()->outsourced.detail(actor(),"DS-OUT-2"));
|
||||
}
|
||||
baseline.jdbc.update("UPDATE ll_fleet_external_booking SET status='BOUND',offer_id=NULL WHERE id=2");
|
||||
assertNotNull(queries.detail(OwnedBusinessFixtures.actor(),"DS-OUT-2"));
|
||||
baseline.jdbc.update("UPDATE ll_fleet_external_booking SET offer_id=9,receipt_json=NULL WHERE id=2");
|
||||
assertNotNull(queries.detail(OwnedBusinessFixtures.actor(),"DS-OUT-2"));
|
||||
baseline.jdbc.update("UPDATE ll_fleet_external_booking SET receipt_json='{}',trip_id=999 WHERE id=2");
|
||||
assertNotNull(queries.detail(OwnedBusinessFixtures.actor(),"DS-OUT-2"));
|
||||
baseline.jdbc.update("UPDATE ll_fleet_external_booking SET trip_id=2,dispatch_no='OTHER' WHERE id=2");
|
||||
assertNotNull(queries.detail(OwnedBusinessFixtures.actor(),"DS-OUT-2"));
|
||||
baseline.jdbc.update("UPDATE ll_fleet_external_booking SET dispatch_no='DS-OUT-2',customer_id='102' WHERE id=2");
|
||||
assertNotNull(queries.detail(OwnedBusinessFixtures.actor(),"DS-OUT-2"));
|
||||
}
|
||||
/** 用真实计划组成核对来源,展示入仓码不能代替组成;多条有效拼车登记不重复整车。 */
|
||||
@Test void actualCargoSourceIsRequiredAndMultipleBookingsNeverMultiplyStatistics() {
|
||||
baseline.jdbc.update("UPDATE ll_dispatch_item SET inbound_code='DISPLAY-ONLY' WHERE dispatch_order_id=2");
|
||||
assertNotNull(outsourced.detail(actor(),"DS-OUT-2"));
|
||||
baseline.jdbc.update("INSERT INTO ll_fleet_external_booking SELECT 22,trip_id,dispatch_no,customer_id,offer_id,status,incoming_no,receipt_json FROM ll_fleet_external_booking WHERE id=2");
|
||||
var query=new OwnedBusinessModels.Query(1,1,null,null,null,null,null);
|
||||
assertEquals(3,outsourced.list(actor(),query).total());assertEquals(1,outsourced.list(actor(),query).records().size());
|
||||
baseline.jdbc.update("DELETE FROM ll_inbound_plan_pallet_allocation WHERE plan_id=2000");
|
||||
assertNotNull(queries.detail(OwnedBusinessFixtures.actor(),"DS-OUT-2"));
|
||||
assertEquals(2,outsourced.list(actor(),query).total());
|
||||
assertThrows(BizException.class,()->outsourced.detail(actor(),"DS-OUT-2"));
|
||||
}
|
||||
/** 外配档案和临时车辆分别保持真实ID/空ID及原承运商,GET不建记录。 */
|
||||
@Test void masterAndExternalReadOriginalSnapshotWithoutBackfill() {
|
||||
var master=outsourced.detail(actor(),"DS-OUT-2");var external=outsourced.detail(actor(),"DS-OUT-3");
|
||||
@@ -210,7 +266,7 @@ class OwnedBusinessScopePersistenceTest {
|
||||
@Test void roundingUsesActualDistinctPalletsAndZeroDenominatorStaysNull() {
|
||||
String load=baseline.repository.source("DS-OUT-2").getLoadingConfirmationJson().replace("\"pallets\":[", "\"pallets\":[{\"warehousePalletId\":99,\"palletSpaces\":5,\"weightKg\":10,\"volumeCbm\":1,\"deliveryIndex\":1},");
|
||||
baseline.jdbc.update("UPDATE ll_dispatch_order SET loading_confirmation_json=? WHERE id=2",load);
|
||||
baseline.jdbc.update("INSERT INTO ll_dispatch_item SELECT id+1000,dispatch_order_id,customer_id,customer_name,CONCAT(source_key,'-2'),order_no,inbound_code,'PAL-99',99,destination_type,destination_warehouse_code,destination_warehouse_name,fba_warehouse_code,delivery_address,postal_code,package_count FROM ll_dispatch_item WHERE id=211");
|
||||
baseline.jdbc.update("INSERT INTO ll_dispatch_item SELECT id+1000,dispatch_order_id,customer_id,customer_name,CONCAT(source_key,'-2'),order_no,inbound_code,'PAL-99',99,destination_type,destination_warehouse_code,destination_warehouse_name,fba_warehouse_code,delivery_address,postal_code,package_count,plan_id,plan_pallet_id FROM ll_dispatch_item WHERE id=211");
|
||||
var saved=outsourced.save(actor(),"DS-OUT-2",request("0","out-half-up-rounding",new OwnedBusinessModels.Money("USD","10.01"),List.of()));
|
||||
assertEquals(2,saved.row().actualPalletCount());assertEquals(3,saved.cargo().size());assertEquals("5.01",saved.row().unitPrice());
|
||||
var confirmation=baseline.codec.read(load,DispatchContracts.ConfirmedLoad.class);
|
||||
|
||||
Reference in New Issue
Block a user