修改提交

This commit is contained in:
2026-08-22 12:35:32 +08:00
parent 996102e77d
commit e398f1f7e0
31 changed files with 540 additions and 27 deletions
Binary file not shown.
+39
View File
@@ -0,0 +1,39 @@
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
public final class DbQuery {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
throw new IllegalArgumentException("usage: DbQuery <database> <sql>");
}
String url = "jdbc:mysql://127.0.0.1:3306/" + args[0]
+ "?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"
+ "&useSSL=false&allowPublicKeyRetrieval=true";
try (Connection connection = DriverManager.getConnection(url, "root", "123456");
Statement statement = connection.createStatement()) {
boolean hasResult = statement.execute(args[1]);
if (!hasResult) {
System.out.println("updateCount=" + statement.getUpdateCount());
return;
}
try (ResultSet result = statement.getResultSet()) {
ResultSetMetaData meta = result.getMetaData();
for (int column = 1; column <= meta.getColumnCount(); column++) {
if (column > 1) System.out.print("\t");
System.out.print(meta.getColumnLabel(column));
}
System.out.println();
while (result.next()) {
for (int column = 1; column <= meta.getColumnCount(); column++) {
if (column > 1) System.out.print("\t");
System.out.print(String.valueOf(result.getObject(column)));
}
System.out.println();
}
}
}
}
}
@@ -16245,6 +16245,14 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
'SELECT 1'));
PREPARE lastleg_inventory_dimension_stmt FROM @ddl; EXECUTE lastleg_inventory_dimension_stmt; DEALLOCATE PREPARE lastleg_inventory_dimension_stmt;
ALTER TABLE ll_inventory_balance COMMENT='客户入仓单与海外目的仓维度实时库存余额';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ll_local_looseboard_order' AND column_name='lastleg_order_id'), 'SELECT 1',
'ALTER TABLE ll_local_looseboard_order ADD COLUMN lastleg_order_id BIGINT NULL COMMENT ''关联尾程主订单ID'' AFTER customer_id'));
PREPARE lastleg_looseboard_tail_stmt FROM @ddl; EXECUTE lastleg_looseboard_tail_stmt; DEALLOCATE PREPARE lastleg_looseboard_tail_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
AND table_name='ll_local_looseboard_order' AND index_name='uk_ll_local_looseboard_lastleg'), 'SELECT 1',
'ALTER TABLE ll_local_looseboard_order ADD UNIQUE KEY uk_ll_local_looseboard_lastleg(lastleg_order_id)'));
PREPARE lastleg_looseboard_tail_stmt FROM @ddl; EXECUTE lastleg_looseboard_tail_stmt; DEALLOCATE PREPARE lastleg_looseboard_tail_stmt;
CALL baiyue_firstleg.assert_unified_final_schema();
DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema;
+10 -1
View File
@@ -152,7 +152,7 @@ CREATE TABLE IF NOT EXISTS ll_us_truck_order_status_history (
) ENGINE=InnoDB COMMENT='美国卡车订单状态历史';
CREATE TABLE IF NOT EXISTS ll_local_looseboard_order (
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', order_no VARCHAR(64) NOT NULL COMMENT '本土散板订单号',
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', lastleg_order_id BIGINT NULL COMMENT '关联尾程主订单ID', order_no VARCHAR(64) NOT NULL COMMENT '本土散板订单号',
transport_mode VARCHAR(32) NOT NULL COMMENT '运输方式:拼车或优先拼载', pallet_required VARCHAR(8) NOT NULL COMMENT '是否需要打盘:YES或NO',
transport_type VARCHAR(32) NOT NULL COMMENT '运输类型', origin VARCHAR(255) NOT NULL COMMENT '起运地', origin_address VARCHAR(500) NULL COMMENT '起运详细地址',
destination VARCHAR(255) NOT NULL COMMENT '目的地', destination_address VARCHAR(500) NULL COMMENT '目的详细地址、码头或仓号',
@@ -166,8 +166,17 @@ CREATE TABLE IF NOT EXISTS ll_local_looseboard_order (
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', PRIMARY KEY(id),
UNIQUE KEY uk_ll_local_looseboard_no(customer_id,order_no), UNIQUE KEY uk_ll_local_looseboard_key(customer_id,idempotency_key),
UNIQUE KEY uk_ll_local_looseboard_lastleg(lastleg_order_id),
KEY idx_ll_local_looseboard_list(customer_id,status,created_at), KEY idx_ll_local_looseboard_date(customer_id,requested_date,created_at)
) ENGINE=InnoDB COMMENT='本土散板订单';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ll_local_looseboard_order' AND column_name='lastleg_order_id'), 'SELECT 1',
'ALTER TABLE ll_local_looseboard_order ADD COLUMN lastleg_order_id BIGINT NULL COMMENT ''关联尾程主订单ID'' AFTER customer_id'));
PREPARE phase4_looseboard_tail_stmt FROM @ddl; EXECUTE phase4_looseboard_tail_stmt; DEALLOCATE PREPARE phase4_looseboard_tail_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
AND table_name='ll_local_looseboard_order' AND index_name='uk_ll_local_looseboard_lastleg'), 'SELECT 1',
'ALTER TABLE ll_local_looseboard_order ADD UNIQUE KEY uk_ll_local_looseboard_lastleg(lastleg_order_id)'));
PREPARE phase4_looseboard_tail_stmt FROM @ddl; EXECUTE phase4_looseboard_tail_stmt; DEALLOCATE PREPARE phase4_looseboard_tail_stmt;
CREATE TABLE IF NOT EXISTS ll_local_looseboard_history (
id BIGINT NOT NULL COMMENT '主键ID', order_id BIGINT NOT NULL COMMENT '本土散板订单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
@@ -0,0 +1,117 @@
package com.baiyue.firstleg.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(classes = TaillegRealProducerAcceptanceTest.AcceptanceApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.sql.init.mode=never", "spring.cloud.nacos.discovery.enabled=false",
"spring.cloud.nacos.discovery.register-enabled=false", "spring.cloud.nacos.config.enabled=false",
"spring.cloud.nacos.config.import-check.enabled=false", "spring.task.scheduling.enabled=false"
})
@EnabledIfSystemProperty(named = "tailleg.real.producer", matches = "true")
class TaillegRealProducerAcceptanceTest {
static final long CUSTOMER = 990022L;
static final long BOOKING = 990022021L;
static final long PLAN = 990022022L;
static final long FIRST_INBOUND = 990022121L;
static final long DROPSHIP_INBOUND = 990022122L;
static final String MARKER = "REAL-TAIL-20260822-R3";
@Autowired JdbcTemplate jdbc;
@Autowired LoadingCompletedOutboxService outbox;
@SpringBootConfiguration
@EnableAutoConfiguration(excludeName = "com.baiyue.common.security.BaiyueSecurityAutoConfiguration")
@Import(LoadingCompletedOutboxService.class)
@MapperScan("com.baiyue.firstleg.mapper")
static class AcceptanceApplication { }
@Test
void completedLoadingPlanPublishesFirstLegAndDropshipOrders() {
String eventKey = "FIRSTLEG_LOADING_COMPLETED:" + PLAN + ":" + CUSTOMER + ":2";
Integer existing = jdbc.queryForObject("SELECT COUNT(*) FROM fl_inbound_outbox WHERE event_key=?",
Integer.class, eventKey);
if (existing == 0) {
seedCompletedLoadingPlan();
outbox.publish(PLAN);
}
assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM fl_inbound_outbox WHERE event_key=?",
Integer.class, eventKey));
assertEquals(2, jdbc.queryForObject("SELECT JSON_LENGTH(JSON_EXTRACT(payload_json,'$.payload.orders')) " +
"FROM fl_inbound_outbox WHERE event_key=?", Integer.class,
eventKey));
}
private void seedCompletedLoadingPlan() {
jdbc.update("INSERT IGNORE INTO fl_container_booking(id,booking_no,container_no,cabinet_no,warehouse_id," +
"warehouse_name,container_type,created_by,updated_by) VALUES(?,?,?,?,?,?,?,1,1)",
BOOKING, MARKER + "-BOOK", MARKER + "-CNTR", MARKER + "-CAB", 92L, "真实验收海外仓", "40HQ");
jdbc.update("INSERT IGNORE INTO fl_loading_plan(id,plan_no,container_booking_id,plan_status,created_by," +
"updated_by,version) VALUES(?,?,?,'LOADING',1,1,0)", PLAN, MARKER + "-PLAN", BOOKING);
seedInbound(FIRST_INBOUND, 990022221L, MARKER + "-FIRST-IN", "FBA_SEA", "LOADED", "ONT8");
seedInbound(DROPSHIP_INBOUND, 990022222L, MARKER + "-DROP-IN", "ONE_CLICK_DROPSHIP", "LOADED", "LAX9");
seedItem(990022321L, FIRST_INBOUND, 990022421L, 990022221L, MARKER + "-FIRST-IN", "SKU-REAL-A", 3, "ONT8");
seedItem(990022322L, DROPSHIP_INBOUND, 990022422L, 990022222L, MARKER + "-DROP-IN", "SKU-REAL-D", 2, "LAX9");
for (int i = 1; i <= 3; i++) seedLabel(990022540L + i, FIRST_INBOUND, 990022221L, 990022421L,
MARKER + "-FIRST-IN", "FIRST-BOX-" + i, i, 3);
for (int i = 1; i <= 2; i++) seedLabel(990022550L + i, DROPSHIP_INBOUND, 990022222L, 990022422L,
MARKER + "-DROP-IN", "DROP-BOX-" + i, i, 2);
jdbc.update("INSERT IGNORE INTO fl_inbound_pallet_allocation(id,tenant_id,inbound_id,allocation_no," +
"warehouse_id,warehouse_resource_tenant_id,pallet_id,pallet_code,pallet_capacity_cbm," +
"allocated_volume_cbm,allocated_package_count,picked_package_count,location_id,location_code," +
"reservation_no,allocation_status) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED')",
990022621L, CUSTOMER, DROPSHIP_INBOUND, 1, 92L, 0L, 990022721L, MARKER + "-PALLET-D",
1.2, 0.4, 2, 2, 990022821L, "A-01", MARKER + "-RES");
jdbc.update("INSERT INTO fl_loading_pick_task(id,loading_plan_id,container_booking_id,cabinet_no," +
"container_no,task_status,started_by,started_by_name) VALUES(?,?,?,?,?,'LOADING',1,'真实验收')",
990022901L, PLAN, BOOKING, MARKER + "-CAB", MARKER + "-CNTR");
seedPickItem(990022911L, 990022321L, FIRST_INBOUND, 990022421L, 3);
seedPickItem(990022912L, 990022322L, DROPSHIP_INBOUND, 990022422L, 2);
jdbc.update("UPDATE fl_loading_plan SET plan_status='COMPLETED',confirmed_at=NOW(3),version=version+1 WHERE id=?", PLAN);
}
private void seedPickItem(long id, long planItemId, long inboundId, long detailId, int packages) {
jdbc.update("INSERT INTO fl_loading_pick_item(id,pick_task_id,loading_plan_item_id,inbound_id," +
"inbound_detail_id,planned_package_count,picked_package_count,item_status) VALUES(?,?,?,?,?,?,?,'PICKED')",
id, 990022901L, planItemId, inboundId, detailId, packages, packages);
}
private void seedInbound(long id, long orderId, String code, String type, String status, String destination) {
jdbc.update("INSERT IGNORE INTO fl_inbound_order(id,tenant_id,order_id,order_no,inbound_code,process_status," +
"order_status_snapshot,customer_name,warehouse_id,warehouse_name,business_type,destination_warehouse_id," +
"destination_code,destination_name,package_count,actual_volume_cbm,created_by,updated_by) " +
"VALUES(?,?,?,?,?,?, 'IN_TRANSIT',?,92,'真实验收海外仓',?,9001,?,?,?,0.4000,1,1)",
id, CUSTOMER, orderId, MARKER + "-ORDER-" + id, code, status, "真实验收客户", type,
destination, "FBA " + destination, type.equals("ONE_CLICK_DROPSHIP") ? 2 : 3);
}
private void seedItem(long id, long inboundId, long detailId, long orderId, String inboundCode,
String sku, int packages, String destination) {
jdbc.update("INSERT IGNORE INTO fl_loading_plan_item(id,loading_plan_id,account_id,customer_name,inbound_id," +
"inbound_detail_id,order_id,order_no,inbound_code,destination_warehouse_id,destination_warehouse_code," +
"destination_warehouse_name,sku_code,product_name_zh,product_name_en,package_count,product_quantity," +
"pallet_count,gross_weight_kg,net_weight_kg,dimensional_weight_kg,volume_cbm,shipment_no,shipping_mark," +
"created_by,updated_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,1)",
id, PLAN, CUSTOMER, "真实验收客户", inboundId, detailId, orderId, MARKER + "-ORDER-" + inboundId,
inboundCode, 9001L, destination, "FBA " + destination, sku, "真实验收商品", "Real product",
packages, packages * 10, 1, 30.0, 27.0, 35.0, 0.4, MARKER + "-SHIP", MARKER + "-MARK");
}
private void seedLabel(long id, long inboundId, long orderId, long detailId, String inboundCode,
String suffix, int index, int total) {
jdbc.update("INSERT IGNORE INTO fl_inbound_carton_label(id,tenant_id,inbound_id,order_id,inbound_detail_id," +
"cargo_item_no,cargo_name,detail_package_count,inbound_code,box_code,total_carton_count,carton_index," +
"box_specification) VALUES(?,?,?,?,?,1,'真实验收商品',?,?,?,?,?, '验收箱规')",
id, CUSTOMER, inboundId, orderId, detailId, total, inboundCode, MARKER + "-" + suffix, total, index);
}
}
@@ -10,6 +10,7 @@ public record LastlegOrderSummary(
Long id,
String orderNo,
String orderType,
String orderTypeName,
String sourceInboundId,
String inboundCode,
String sourceOrderNo,
@@ -0,0 +1,43 @@
package com.baiyue.lastleg.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Getter
@Setter
@TableName("ll_inbound_order")
public class LastlegInboundOrder {
@TableId private Long id;
private String tenantId;
private String customerId;
private String sourceRegion;
private String sourceInboundId;
private String rootInboundId;
private String sourceOrderId;
private String sourceOrderNo;
private String rootOrderId;
private String inboundCode;
private String destinationWarehouseId;
private String destinationWarehouseCode;
private String destinationWarehouseName;
private Integer packageCount;
private Integer productQuantity;
private Integer palletCount;
private BigDecimal grossWeightKg;
private BigDecimal netWeightKg;
private BigDecimal volumeCbm;
private String sourceEventId;
private String sourceLoadingPlanId;
private Long sourceLoadingPlanVersion;
private Long handoverVersion;
private String payloadHash;
private String status;
private String exceptionFlag;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -1,3 +1,12 @@
package com.baiyue.lastleg.entity;
import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal;
@Data @TableName("ll_order_detail") public class LastlegOrderDetail { @TableId(type=IdType.AUTO) private Long id; private Long orderId; private String sourceInboundDetailId; private String skuCode; private String productNameZh; private String productNameEn; private Integer packageCount; private Integer productQuantity; private Integer palletCount; private BigDecimal grossWeightKg; private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm; private String destinationWarehouseId; private String destinationWarehouseCode; private String destinationWarehouseName; private String fbaTrackingNo; private String shipmentNo; private String shippingMark; }
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
@Data @TableName("ll_order_detail") public class LastlegOrderDetail {
@TableId(type=IdType.AUTO) private Long id; private String customerId; private Long orderId;
private String sourceInboundDetailId; private String skuCode; private String productNameZh; private String productNameEn;
private Integer packageCount; private Integer productQuantity; private Integer palletCount; private BigDecimal grossWeightKg;
private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm;
private String destinationWarehouseId; private String destinationWarehouseCode; private String destinationWarehouseName;
private String fbaTrackingNo; private String shipmentNo; private String shippingMark;
}
@@ -1,3 +1,6 @@
package com.baiyue.lastleg.entity;
import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime;
@Data @TableName("ll_loading_order_status_history") public class LastlegOrderStatusHistory { @TableId(type=IdType.AUTO) private Long id; private Long orderId; private String fromStatus; private String toStatus; private String eventId; private String remark; private LocalDateTime createdAt; }
@Data @TableName("ll_loading_order_status_history") public class LastlegOrderStatusHistory {
@TableId(type=IdType.AUTO) private Long id; private String customerId; private Long orderId;
private String fromStatus; private String toStatus; private String eventId; private String remark; private LocalDateTime createdAt;
}
@@ -12,6 +12,7 @@ import java.math.BigDecimal;
public class InboundPlanLine {
@TableId(type = IdType.INPUT)
private Long id;
private String customerId;
private Long planId;
private Long orderId;
private String inboundCode;
@@ -13,6 +13,7 @@ import java.time.LocalDateTime;
public class InboundPlanPallet {
@TableId(type = IdType.INPUT)
private Long id;
private String customerId;
private Long planId;
private String palletNo;
private String destinationWarehouseId;
@@ -12,6 +12,7 @@ import java.math.BigDecimal;
public class InboundPlanPalletAllocation {
@TableId(type = IdType.INPUT)
private Long id;
private String customerId;
private Long planId;
private Long lineId;
private Long palletId;
@@ -43,8 +43,8 @@ public interface InboundPlanOrderMapper {
@Param("planNo") String planNo, @Param("palletCount") int palletCount,
@Param("updatedAt") LocalDateTime updatedAt);
@Insert("INSERT INTO ll_loading_order_status_history(order_id,from_status,to_status,remark,created_at) " +
"VALUES(#{orderId},#{fromStatus},'INBOUND_PLANNED',#{remark},#{createdAt})")
int insertHistory(@Param("orderId") Long orderId, @Param("fromStatus") String fromStatus,
@Insert("INSERT INTO ll_loading_order_status_history(customer_id,order_id,from_status,to_status,remark,created_at) " +
"VALUES(#{customerId},#{orderId},#{fromStatus},'INBOUND_PLANNED',#{remark},#{createdAt})")
int insertHistory(@Param("customerId") String customerId, @Param("orderId") Long orderId, @Param("fromStatus") String fromStatus,
@Param("remark") String remark, @Param("createdAt") LocalDateTime createdAt);
}
@@ -108,7 +108,7 @@ public class InboundPlanService {
if (orderMapper.markPlanned(customerId, order.orderId(), plan.getPlanNo(), count, now) != 1) {
throw new IllegalStateException("Failed to update order " + order.orderId());
}
orderMapper.insertHistory(order.orderId(), order.currentStatus(),
orderMapper.insertHistory(customerId, order.orderId(), order.currentStatus(),
"Inbound order converted to inbound plan " + plan.getPlanNo(), now);
}
return detail(customerId, plan);
@@ -158,6 +158,7 @@ public class InboundPlanService {
for (SourceOrderSnapshot order : orders) {
InboundPlanLine line = new InboundPlanLine();
line.setId(IdWorker.getId());
line.setCustomerId(order.customerId());
line.setPlanId(plan.getId());
line.setOrderId(order.orderId());
line.setInboundCode(order.inboundCode());
@@ -182,6 +183,7 @@ public class InboundPlanService {
SourceOrderSnapshot sample = warehouseSamples.get(draft.warehouseKey());
InboundPlanPallet pallet = new InboundPlanPallet();
pallet.setId(IdWorker.getId());
pallet.setCustomerId(plan.getCustomerId());
pallet.setPlanId(plan.getId());
pallet.setPalletNo(plan.getPlanNo() + "-P" + String.format("%03d", result.size() + 1));
pallet.setDestinationWarehouseId(sample.destinationWarehouseId());
@@ -205,6 +207,7 @@ public class InboundPlanService {
for (PalletAllocator.AllocationDraft draft : allocation.allocations()) {
InboundPlanPalletAllocation item = new InboundPlanPalletAllocation();
item.setId(IdWorker.getId());
item.setCustomerId(plan.getCustomerId());
item.setPlanId(plan.getId());
item.setLineId(lineByOrder.get(draft.orderId()).getId());
item.setPalletId(pallets.get(draft.pallet()).getId());
@@ -14,6 +14,7 @@ import java.time.LocalDateTime;
public class LocalLooseboardOrder {
@TableId(type = IdType.INPUT) private Long id;
private String customerId;
private Long lastlegOrderId;
private String orderNo;
private String transportMode;
private String palletRequired;
@@ -4,12 +4,17 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardResponses;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardHistory;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardOrder;
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardHistoryMapper;
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardOrderMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
@@ -46,6 +51,9 @@ public class LocalLooseboardService {
@Resource private LocalLooseboardOrderMapper orders;
@Resource private LocalLooseboardHistoryMapper histories;
@Resource private LastlegOrderMapper lastlegOrders;
@Resource private LastlegOrderStatusHistoryMapper lastlegHistories;
@Resource private LastlegOrderNoGenerator lastlegOrderNoGenerator;
@Resource private ObjectMapper objectMapper;
@Transactional
@@ -71,6 +79,8 @@ public class LocalLooseboardService {
order.setVersion(0L);
order.setCreatedAt(now);
order.setUpdatedAt(now);
LastlegOrder lastlegOrder = createLastlegOrder(order, requestHash, now);
order.setLastlegOrderId(lastlegOrder.getId());
try {
orders.insert(order);
} catch (DuplicateKeyException exception) {
@@ -79,6 +89,8 @@ public class LocalLooseboardService {
throw exception;
}
history(order, "CREATE", null, PENDING_ACCEPTANCE, operatorId, null, request.remark(), now);
lastlegHistory(customerId, lastlegOrder.getId(), null, lastlegOrder.getCurrentStatus(),
"Local looseboard order submitted", now);
return detail(customerId, order.getOrderNo());
}
@@ -147,6 +159,7 @@ public class LocalLooseboardService {
order.setStatus(next);
touch(order, now);
orders.updateById(order);
syncLastlegOrder(order, next, now);
history(order, "STATUS", from, next, operatorId, null, request.remark(), now);
return response(order);
}
@@ -220,6 +233,58 @@ public class LocalLooseboardService {
histories.insert(history);
}
private LastlegOrder createLastlegOrder(LocalLooseboardOrder source, String requestHash, LocalDateTime now) {
LastlegOrder order = new LastlegOrder();
order.setId(IdWorker.getId());
order.setOrderNo(lastlegOrderNoGenerator.next());
order.setCustomerId(source.getCustomerId());
order.setOrderType("LOCAL_LOOSEBOARD");
order.setInboundCode(source.getOrderNo());
order.setSourceRegion("OVERSEAS_LOCAL");
order.setSourceInboundId("LOCAL_LOOSEBOARD:" + source.getId());
order.setSourceOrderId(String.valueOf(source.getId()));
order.setSourceOrderNo(source.getOrderNo());
order.setPalletCount(0);
order.setPackageCount(Objects.requireNonNullElse(source.getPackageCount(), 0));
order.setPickedPackageCount(0);
order.setShippedPackageCount(0);
order.setVolumeCbm(source.getTotalVolumeCbm());
order.setCurrentStatus("PENDING_ASSIGNMENT");
order.setSubOrderStatus("LOCAL_LOOSEBOARD:" + PENDING_ACCEPTANCE);
order.setCustomerRemark(source.getRemark());
order.setSourceEventId("LOCAL_LOOSEBOARD:" + source.getId());
order.setSourceLoadingPlanId("LOCAL_LOOSEBOARD");
order.setSourcePayloadHash("sha256:" + requestHash);
order.setCreatedAt(now);
order.setUpdatedAt(now);
lastlegOrders.insert(order);
return order;
}
private void syncLastlegOrder(LocalLooseboardOrder source, String status, LocalDateTime now) {
if (source.getLastlegOrderId() == null) return;
LastlegOrder order = lastlegOrders.selectById(source.getLastlegOrderId());
if (order == null || !source.getCustomerId().equals(order.getCustomerId())) {
throw conflict("Linked tail-leg order is missing");
}
order.setSubOrderStatus("LOCAL_LOOSEBOARD:" + status);
order.setUpdatedAt(now);
lastlegOrders.updateById(order);
}
private void lastlegHistory(String customerId, Long orderId, String from, String to, String remark,
LocalDateTime now) {
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory();
history.setCustomerId(customerId);
history.setOrderId(orderId);
history.setFromStatus(from);
history.setToStatus(to);
history.setEventId("LOCAL_LOOSEBOARD");
history.setRemark(remark);
history.setCreatedAt(now);
lastlegHistories.insert(history);
}
private void validateQuery(LocalLooseboardRequests.Query query) {
if (StringUtils.hasText(query.status()) && !STATUSES.contains(norm(query.status()))) {
throw badRequest("Unsupported looseboard order status");
@@ -0,0 +1,9 @@
package com.baiyue.lastleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.entity.LastlegInboundOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LastlegInboundOrderMapper extends BaseMapper<LastlegInboundOrder> {
}
@@ -24,4 +24,13 @@ public enum LastlegOrderType {
public String label() {
return label;
}
public static String labelOf(String code) {
if (code == null || code.isBlank()) return code;
try {
return valueOf(code.trim()).label;
} catch (IllegalArgumentException exception) {
return code;
}
}
}
@@ -118,7 +118,8 @@ public class RelabelApplicationService {
for (RelabelRequests.Item source : request.items()) insertItem(customerId, applicationId, source, now);
insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
insertOrderHistory(order.getId(), null, order.getCurrentStatus(), "Relabel application submitted", now);
insertOrderHistory(customerId, order.getId(), null, order.getCurrentStatus(),
"Relabel application submitted", now);
return detail(customerId, relabelNo);
}
@@ -194,7 +195,7 @@ public class RelabelApplicationService {
orders.updateById(order);
insertHistory(customerId, application.getId(), before, next, operatorId, request.remark(), now);
if (!Objects.equals(orderBefore, orderStatus)) {
insertOrderHistory(order.getId(), orderBefore, orderStatus, request.remark(), now);
insertOrderHistory(customerId, order.getId(), orderBefore, orderStatus, request.remark(), now);
}
return detail(customerId, relabelNo);
}
@@ -248,8 +249,9 @@ public class RelabelApplicationService {
value.setApplicationId(id); value.setFromStatus(from); value.setToStatus(to); value.setOperatorId(trim(operator));
value.setRemark(trim(remark)); value.setCreatedAt(now); histories.insert(value);
}
private void insertOrderHistory(Long id, String from, String to, String remark, LocalDateTime now) {
LastlegOrderStatusHistory value = new LastlegOrderStatusHistory(); value.setOrderId(id); value.setFromStatus(from);
private void insertOrderHistory(String customerId, Long id, String from, String to, String remark,
LocalDateTime now) {
LastlegOrderStatusHistory value = new LastlegOrderStatusHistory(); value.setCustomerId(customerId); value.setOrderId(id); value.setFromStatus(from);
value.setToStatus(to); value.setEventId("RELABEL_APPLICATION"); value.setRemark(trim(remark)); value.setCreatedAt(now);
orderHistories.insert(value);
}
@@ -12,6 +12,7 @@ import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.model.LastlegOrderType;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@@ -106,7 +107,8 @@ public class LastlegOrderService {
lines.add("ID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)");
for (LastlegOrder order : orders) {
lines.add(String.join(",", List.of(
csv(order.getId()), csv(order.getOrderNo()), csv(order.getOrderType()), csv(order.getInboundCode()),
csv(order.getId()), csv(order.getOrderNo()), csv(LastlegOrderType.labelOf(order.getOrderType())),
csv(order.getInboundCode()),
csv(order.getSourceOrderNo()), csv(order.getCustomerName()), csv(order.getOverseasWarehouseName()),
csv(order.getTransferWarehouseName()), csv(order.getTransferWarehouseCode()), csv(order.getFbaWarehouseCode()),
csv(order.getContainerNo()), csv(order.getContainerStatus()), csv(order.getAbnormal()), csv(order.getDetained()),
@@ -161,7 +163,8 @@ public class LastlegOrderService {
}
private LastlegOrderSummary summary(LastlegOrder o) {
return new LastlegOrderSummary(o.getId(), o.getOrderNo(), o.getOrderType(), o.getSourceInboundId(),
return new LastlegOrderSummary(o.getId(), o.getOrderNo(), o.getOrderType(),
LastlegOrderType.labelOf(o.getOrderType()), o.getSourceInboundId(),
o.getInboundCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
o.getTransferWarehouseName(), o.getFbaWarehouseCode(), o.getContainerNo(), o.getContainerStatus(),
@@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.JsonNode;
public class LoadingCompletedConsumer {
@Resource private LastlegEventInboxMapper inboxMapper;
@Resource private LastlegOrderMapper orderMapper;
@Resource private LastlegInboundOrderMapper inboundOrderMapper;
@Resource private LastlegOrderDetailMapper detailMapper;
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
@Resource private InboundPlanCartonMapper cartonMapper;
@@ -173,9 +174,11 @@ public class LoadingCompletedConsumer {
if (raced != null && Objects.equals(raced.getSourcePayloadHash(), orderHash)) return;
throw exception;
}
createInboundOrderSnapshot(event, payload, source, order, orderHash);
for(var item:source.items()) {
LastlegOrderDetail detail=new LastlegOrderDetail();
detail.setCustomerId(customerId);
detail.setOrderId(order.getId()); detail.setSourceInboundDetailId(item.sourceInboundDetailId()); detail.setSkuCode(item.skuCode());
detail.setProductNameZh(item.productNameZh()); detail.setProductNameEn(item.productNameEn()); detail.setPackageCount(item.packageCount());
detail.setProductQuantity(item.productQuantity()); detail.setPalletCount(item.palletCount()); detail.setGrossWeightKg(item.grossWeightKg());
@@ -185,7 +188,7 @@ public class LoadingCompletedConsumer {
detail.setShipmentNo(item.shipmentNo()); detail.setShippingMark(item.shippingMark()); detailMapper.insert(detail);
createCartonLabels(customerId, order.getId(), item);
}
LastlegOrderStatusHistory history=new LastlegOrderStatusHistory();
LastlegOrderStatusHistory history=new LastlegOrderStatusHistory(); history.setCustomerId(customerId);
history.setOrderId(order.getId()); history.setToStatus("PENDING_ASSIGNMENT"); history.setEventId(event.eventId());
history.setRemark("头程装柜完成自动生成"); history.setCreatedAt(LocalDateTime.now()); historyMapper.insert(history);
if ("FIRST_LEG".equals(orderType)) {
@@ -201,6 +204,28 @@ public class LoadingCompletedConsumer {
}
}
private void createInboundOrderSnapshot(EventEnvelope event, FirstLegLoadingPlanCompletedV1 payload,
FirstLegLoadingPlanCompletedV1.Order source, LastlegOrder order,
String orderHash) {
LastlegInboundOrder snapshot = new LastlegInboundOrder();
snapshot.setId(order.getId()); snapshot.setTenantId("BAIYUE");
snapshot.setCustomerId(order.getCustomerId()); snapshot.setSourceRegion(event.sourceRegion().name());
snapshot.setSourceInboundId(source.sourceInboundId()); snapshot.setRootInboundId(source.rootInboundId());
snapshot.setSourceOrderId(source.sourceOrderId()); snapshot.setSourceOrderNo(source.sourceOrderNo());
snapshot.setRootOrderId(source.rootOrderId()); snapshot.setInboundCode(source.inboundCode());
snapshot.setDestinationWarehouseId(source.destinationWarehouseId());
snapshot.setDestinationWarehouseCode(source.destinationWarehouseCode());
snapshot.setDestinationWarehouseName(source.destinationWarehouseName());
snapshot.setPackageCount(source.packageCount()); snapshot.setProductQuantity(source.productQuantity());
snapshot.setPalletCount(source.palletCount()); snapshot.setGrossWeightKg(source.grossWeightKg());
snapshot.setNetWeightKg(source.netWeightKg()); snapshot.setVolumeCbm(source.volumeCbm());
snapshot.setSourceEventId(event.eventId()); snapshot.setSourceLoadingPlanId(payload.loadingPlanId());
snapshot.setSourceLoadingPlanVersion(event.aggregateVersion()); snapshot.setHandoverVersion(event.handoverVersion());
snapshot.setPayloadHash(orderHash); snapshot.setStatus("READY"); snapshot.setExceptionFlag("NONE");
snapshot.setCreatedAt(order.getCreatedAt()); snapshot.setUpdatedAt(order.getUpdatedAt());
inboundOrderMapper.insert(snapshot);
}
private void registerInTransit(EventEnvelope event, FirstLegLoadingPlanCompletedV1 payload,
FirstLegLoadingPlanCompletedV1.Order source, LastlegOrder order,
String inboundOrderNo) {
@@ -152,9 +152,9 @@ public class WarehouseTransferService {
for (int index = 0; index < request.items().size(); index++) {
WarehouseTransferRequests.Item source = request.items().get(index);
insertTransferItem(customerId, transferId, source, now);
insertOrderDetail(order.getId(), transferNo, index, source, request, now);
insertOrderDetail(customerId, order.getId(), transferNo, index, source, request, now);
}
insertOrderHistory(order.getId(), null, PENDING_REVIEW, transferNo,
insertOrderHistory(customerId, order.getId(), null, PENDING_REVIEW, transferNo,
"Warehouse transfer submitted for review", now);
if (request.needRelabel()) createRelabel(customerId, operatorId, transfer, order, request, now);
return detail(customerId, transferNo);
@@ -258,7 +258,7 @@ public class WarehouseTransferService {
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + next);
order.setUpdatedAt(now); orders.updateById(order);
if (!java.util.Objects.equals(before, orderStatus)) {
insertOrderHistory(order.getId(), before, orderStatus, transferNo,
insertOrderHistory(customerId, order.getId(), before, orderStatus, transferNo,
"Warehouse transfer reviewed: " + next, now);
}
return detail(customerId, transferNo);
@@ -337,7 +337,7 @@ public class WarehouseTransferService {
String before = order.getCurrentStatus(); order.setCurrentStatus("WAIT_OUTBOUND");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:ALLOCATED"); order.setUpdatedAt(now);
orders.updateById(order);
insertOrderHistory(order.getId(), before, order.getCurrentStatus(), transferNo,
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
"Warehouse transfer inventory allocated", now);
return detail(customerId, transferNo);
}
@@ -388,7 +388,7 @@ public class WarehouseTransferService {
String before = order.getCurrentStatus(); order.setCurrentStatus("IN_TRANSIT");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:OUTBOUNDED"); order.setUpdatedAt(now);
orders.updateById(order);
insertOrderHistory(order.getId(), before, order.getCurrentStatus(), transferNo,
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
"Warehouse transfer left source warehouse", now);
return detail(customerId, transferNo);
}
@@ -455,7 +455,7 @@ public class WarehouseTransferService {
String before = order.getCurrentStatus(); order.setCurrentStatus(complete ? "COMPLETED" : "IN_TRANSIT");
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + (complete ? RECEIVED : PARTIALLY_RECEIVED));
order.setUpdatedAt(now); orders.updateById(order);
insertOrderHistory(order.getId(), before, order.getCurrentStatus(), transferNo,
insertOrderHistory(customerId, order.getId(), before, order.getCurrentStatus(), transferNo,
"Warehouse transfer inventory received at destination", now);
return detail(customerId, transferNo);
}
@@ -490,9 +490,10 @@ public class WarehouseTransferService {
transferItems.insert(item);
}
private void insertOrderDetail(long orderId, String transferNo, int index, WarehouseTransferRequests.Item source,
private void insertOrderDetail(String customerId, long orderId, String transferNo, int index, WarehouseTransferRequests.Item source,
WarehouseTransferRequests.Create request, LocalDateTime now) {
LastlegOrderDetail detail = new LastlegOrderDetail();
detail.setCustomerId(customerId);
detail.setOrderId(orderId); detail.setSourceInboundDetailId(transferNo + ":" + index);
detail.setSkuCode(source.skuCode().trim()); detail.setProductNameZh(source.productNameZh().trim());
detail.setPackageCount(source.packageCount()); detail.setDestinationWarehouseCode(request.fbaWarehouseCode().trim());
@@ -537,9 +538,10 @@ public class WarehouseTransferService {
order.setUpdatedAt(now); orders.updateById(order);
}
private void insertOrderHistory(long orderId, String fromStatus, String toStatus, String transferNo,
String remark, LocalDateTime now) {
private void insertOrderHistory(String customerId, long orderId, String fromStatus, String toStatus,
String transferNo, String remark, LocalDateTime now) {
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory();
history.setCustomerId(customerId);
history.setOrderId(orderId); history.setFromStatus(fromStatus); history.setToStatus(toStatus);
history.setEventId("WAREHOUSE_TRANSFER:" + transferNo); history.setRemark(remark);
history.setCreatedAt(now); orderHistories.insert(history);
@@ -1304,6 +1304,7 @@ CREATE TABLE IF NOT EXISTS ll_us_truck_order_status_history (
CREATE TABLE IF NOT EXISTS ll_local_looseboard_order (
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
lastleg_order_id BIGINT NULL COMMENT '关联尾程主订单ID',
order_no VARCHAR(64) NOT NULL COMMENT '本土散板订单号', transport_mode VARCHAR(32) NOT NULL COMMENT '运输方式:拼车或优先拼载',
pallet_required VARCHAR(8) NOT NULL COMMENT '是否需要打盘:YES或NO', transport_type VARCHAR(32) NOT NULL COMMENT '运输类型',
origin VARCHAR(255) NOT NULL COMMENT '起运地', origin_address VARCHAR(500) NULL COMMENT '起运详细地址',
@@ -1319,6 +1320,7 @@ CREATE TABLE IF NOT EXISTS ll_local_looseboard_order (
version BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_local_looseboard_no(customer_id,order_no),
UNIQUE KEY uk_ll_local_looseboard_lastleg(lastleg_order_id),
UNIQUE KEY uk_ll_local_looseboard_key(customer_id,idempotency_key),
KEY idx_ll_local_looseboard_list(customer_id,status,created_at),
KEY idx_ll_local_looseboard_date(customer_id,requested_date,created_at)
@@ -89,7 +89,7 @@ class InboundPlanServiceTest {
assertEquals(1, allocations.getAllValues().stream()
.filter(item -> "PENDING_COUNT".equals(item.getStatus())).count());
verify(orderMapper).markPlanned(any(), org.mockito.ArgumentMatchers.eq(2L), any(), org.mockito.ArgumentMatchers.anyInt(), any());
verify(orderMapper).insertHistory(org.mockito.ArgumentMatchers.eq(2L), any(), any(), any());
verify(orderMapper).insertHistory(any(), org.mockito.ArgumentMatchers.eq(2L), any(), any(), any());
verify(orderMapper).lockOrder("100", 2L);
assertEquals(2L, result.sourceOrderId());
assertEquals("CONT-1", result.containerNo());
@@ -17,5 +17,7 @@ class LocalLooseboardSchemaContractTest {
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_local_looseboard_history"));
assertTrue(sql.contains("operation_type VARCHAR(32) NOT NULL"));
assertTrue(sql.contains("uk_ll_local_looseboard_no(customer_id,order_no)"));
assertTrue(sql.contains("lastleg_order_id BIGINT NULL"));
assertTrue(sql.contains("uk_ll_local_looseboard_lastleg(lastleg_order_id)"));
}
}
@@ -5,6 +5,10 @@ import com.baiyue.lastleg.looseboard.entity.LocalLooseboardHistory;
import com.baiyue.lastleg.looseboard.entity.LocalLooseboardOrder;
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardHistoryMapper;
import com.baiyue.lastleg.looseboard.mapper.LocalLooseboardOrderMapper;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -31,6 +35,9 @@ import static org.mockito.Mockito.when;
class LocalLooseboardServiceTest {
@Mock LocalLooseboardOrderMapper orders;
@Mock LocalLooseboardHistoryMapper histories;
@Mock LastlegOrderMapper lastlegOrders;
@Mock LastlegOrderStatusHistoryMapper lastlegHistories;
@Mock LastlegOrderNoGenerator lastlegOrderNoGenerator;
private LocalLooseboardService service;
@BeforeEach
@@ -38,7 +45,11 @@ class LocalLooseboardServiceTest {
service = new LocalLooseboardService();
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "lastlegOrders", lastlegOrders);
ReflectionTestUtils.setField(service, "lastlegHistories", lastlegHistories);
ReflectionTestUtils.setField(service, "lastlegOrderNoGenerator", lastlegOrderNoGenerator);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
org.mockito.Mockito.lenient().when(lastlegOrderNoGenerator.next()).thenReturn("LMO-LOOSE-1");
}
@Test
@@ -56,6 +67,9 @@ class LocalLooseboardServiceTest {
assertEquals("100", detail.summary().customerId());
assertEquals(LocalLooseboardService.PENDING_ACCEPTANCE, detail.summary().status());
assertEquals("SHARED_TRUCK", detail.summary().transportMode());
verify(lastlegOrders).insert(org.mockito.ArgumentMatchers.<LastlegOrder>argThat(value ->
"LOCAL_LOOSEBOARD".equals(value.getOrderType())
&& "LOCAL_LOOSEBOARD:PENDING_ACCEPTANCE".equals(value.getSubOrderStatus())));
verify(histories).insert(org.mockito.ArgumentMatchers.<LocalLooseboardHistory>argThat(value ->
"CREATE".equals(value.getOperationType())));
}
@@ -15,5 +15,7 @@ class LastlegOrderTypeTest {
Arrays.stream(LastlegOrderType.values()).map(Enum::name).toList());
assertEquals("换标下单", LastlegOrderType.RELABEL_APPLICATION.label());
assertEquals("本土散板", LastlegOrderType.LOCAL_LOOSEBOARD.label());
assertEquals("一件代发", LastlegOrderType.labelOf("ONE_CLICK_DROPSHIP"));
assertEquals("LEGACY_TYPE", LastlegOrderType.labelOf("LEGACY_TYPE"));
}
}
@@ -53,6 +53,8 @@ class LastlegOrderServiceTest {
assertEquals(11L, response.order().id());
assertEquals("100", response.order().customerId());
assertEquals("ONE_CLICK_DROPSHIP", response.order().orderType());
assertEquals("一件代发", response.order().orderTypeName());
assertTrue(java.util.Arrays.stream(response.getClass().getRecordComponents())
.noneMatch(component -> "dropshipPallets".equals(component.getName())));
assertTrue(java.util.Arrays.stream(com.baiyue.lastleg.dto.LastlegOrderDetailResponse.History.class.getRecordComponents())
@@ -2,8 +2,8 @@ package com.baiyue.lastleg.service;
import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com.baiyue.lastleg.mapper.*; import com.baiyue.lastleg.dropship.dto.DropshipRequests; import com.baiyue.lastleg.dropship.service.DropshipOrderService; import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; import com.baiyue.lastleg.inboundplan.service.InboundPlanService; import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests; import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; import com.fasterxml.jackson.databind.*; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; import java.time.Instant; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*;
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
class LoadingCompletedConsumerTest {
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; @Mock InventoryCommandService inventory; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(inboundPlans.convert(anyString(),anyLong())).thenReturn(new InboundPlanResponse(1L,"LIP-1","100",1L,"MSCU1","PENDING_ASSIGNMENT",1,1,BigDecimal.ONE,List.of(),List.of(),List.of())); }
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; @Mock InventoryCommandService inventory; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(inboundPlans.convert(anyString(),anyLong())).thenReturn(new InboundPlanResponse(1L,"LIP-1","100",1L,"MSCU1","PENDING_ASSIGNMENT",1,1,BigDecimal.ONE,List.of(),List.of(),List.of())); }
@Test void createsOneOrderAndOneInboundPlanPerInbound(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(inboundPlans,times(2)).convert(eq("100"),anyLong()); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),any(InventoryCommandRequests.InTransit.class)); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU1".equals(o.getContainerNo()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); }
@Test void repeatedEventReturnsExistingEffect(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); when(inbox.selectById(event.eventId())).thenReturn(row); when(orders.countContainer("100","MSCU1")).thenReturn(2L); assertEquals(2,service.consume(event)); verify(orders).countContainer("100","MSCU1"); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); }
@Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); }
@@ -0,0 +1,132 @@
package com.baiyue.lastleg.service;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
import com.baiyue.lastleg.looseboard.dto.LocalLooseboardRequests;
import com.baiyue.lastleg.looseboard.service.LocalLooseboardService;
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
import com.baiyue.lastleg.transfer.service.WarehouseTransferService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SpringBootTest(classes = TaillegRealConsumerAcceptanceTest.AcceptanceApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE, properties = {
"spring.sql.init.mode=never", "spring.cloud.nacos.discovery.enabled=false",
"spring.cloud.nacos.discovery.register-enabled=false", "spring.cloud.nacos.config.enabled=false",
"spring.cloud.nacos.config.import-check.enabled=false", "spring.task.scheduling.enabled=false"
})
@EnabledIfSystemProperty(named = "tailleg.real.consumer", matches = "true")
class TaillegRealConsumerAcceptanceTest {
static final String CUSTOMER = "990022";
static final String MARKER = "REAL-TAIL-20260822-R3";
static final String EVENT_KEY = "FIRSTLEG_LOADING_COMPLETED:990022022:990022:2";
static final LocalDateTime RELABEL_DEADLINE = LocalDateTime.of(2026, 8, 29, 11, 1, 21, 628_000_000);
static final LocalDateTime TRANSFER_ETA = LocalDateTime.of(2026, 9, 1, 11, 1, 21, 674_000_000);
static final LocalDateTime RETURN_RELABEL_ETA = LocalDateTime.of(2026, 9, 1, 11, 1, 21, 756_000_000);
static final LocalDate REQUESTED_DATE = LocalDate.of(2026, 8, 25);
@Autowired JdbcTemplate jdbc;
@Autowired ObjectMapper json;
@Autowired LoadingCompletedConsumer consumer;
@Autowired RelabelApplicationService relabel;
@Autowired WarehouseTransferService transfer;
@Autowired LocalLooseboardService looseboard;
@SpringBootConfiguration
@EnableAutoConfiguration(excludeName = "com.baiyue.common.security.BaiyueSecurityAutoConfiguration")
@Import({LoadingCompletedConsumer.class, LastlegOrderNoGenerator.class, DropshipOrderService.class,
InboundPlanService.class, InventoryCommandService.class, RelabelApplicationService.class,
WarehouseTransferService.class, LocalLooseboardService.class})
@MapperScan({"com.baiyue.lastleg.mapper", "com.baiyue.lastleg.dropship.mapper",
"com.baiyue.lastleg.inboundplan.mapper", "com.baiyue.lastleg.inventory.mapper",
"com.baiyue.lastleg.inventory.command.mapper", "com.baiyue.lastleg.relabel.mapper",
"com.baiyue.lastleg.transfer.mapper", "com.baiyue.lastleg.looseboard.mapper"})
static class AcceptanceApplication {
@Bean WarehouseResourceGateway warehouseResourceGateway() {
return new WarehouseResourceGateway() {
public WarehousePalletReference ensureDropshipPallet(String customerId, long warehouseId, String businessId) {
return new WarehousePalletReference(1L, "UNUSED", 1L);
}
public long reserve(String customerId, long warehouseId, long palletId, long locationId,
String businessType, String businessId, String reservationKey) { return 1L; }
public void scrap(String customerId, long resourceLockId) { }
public void scrapPallet(String customerId, long palletId) { }
};
}
}
@Test
void consumesRealOutboxAndCreatesEveryTaillegOrderType() throws Exception {
String envelopeJson = jdbc.queryForObject(
"SELECT payload_json FROM baiyue_firstleg.fl_inbound_outbox WHERE event_key=?", String.class, EVENT_KEY);
EventEnvelope envelope = json.readValue(envelopeJson, EventEnvelope.class);
assertEquals(2, consumer.consume(envelope));
Long sourceOrderId = jdbc.queryForObject("SELECT id FROM ll_order WHERE customer_id=? AND " +
"source_loading_plan_id='990022022' AND order_type='FIRST_LEG'", Long.class, CUSTOMER);
String inboundCode = jdbc.queryForObject("SELECT inbound_code FROM ll_order WHERE id=?", String.class, sourceOrderId);
assertNotNull(jdbc.queryForObject("SELECT id FROM ll_inbound_order WHERE customer_id=? AND inbound_code=?",
Long.class, CUSTOMER, inboundCode));
if (!exists("ll_relabel_application", MARKER + "-RELABEL-KEY")) {
relabel.create(CUSTOMER, "real-acceptance", new RelabelRequests.Create(
MARKER + "-RELABEL", "92", RELABEL_DEADLINE, "验收联系人", "13800000000",
MARKER, MARKER + "-RELABEL-KEY", List.of(new RelabelRequests.Item("SKU-REAL-A", null, null,
null, "真实验收商品", 3, 3, MARKER + "-SHIP", MARKER + "-TRACK", "SKU-REAL-A-NEW",
"换标验收", null, null, null))));
}
createTransfer(sourceOrderId, inboundCode, false, "TRANSFER");
createTransfer(sourceOrderId, inboundCode, true, "RETURN-RELABEL");
if (!exists("ll_local_looseboard_order", MARKER + "-LOOSEBOARD-KEY")) {
looseboard.create(CUSTOMER, "real-acceptance", new LocalLooseboardRequests.Create(
"SHARED_TRUCK", "YES", "WAREHOUSE_TO_WAREHOUSE", "洛杉矶海外仓", "Los Angeles",
"ONT8", "FBA ONT8", "40HQ", 3, new BigDecimal("30.00"), new BigDecimal("0.40"),
new BigDecimal("120.00"), "USD", "验收联系人", "13800000000", REQUESTED_DATE,
MARKER, MARKER + "-LOOSEBOARD-KEY"));
}
assertEquals(6, jdbc.queryForObject("SELECT COUNT(DISTINCT order_type) FROM ll_order WHERE customer_id=? " +
"AND (source_loading_plan_id='990022022' OR source_event_id LIKE ? OR customer_remark=?)",
Integer.class, CUSTOMER, "%" + MARKER + "%", MARKER));
}
private void createTransfer(Long sourceOrderId, String inboundCode, boolean needRelabel, String suffix) {
String idempotencyKey = MARKER + "-" + suffix + "-KEY";
if (exists("ll_warehouse_transfer_order", idempotencyKey)) return;
transfer.create(CUSTOMER, "real-acceptance", new WarehouseTransferRequests.Create(
inboundCode, List.of(sourceOrderId), "92", needRelabel ? "LAX9" : "ONT8", "真实验收目的地址",
3, new BigDecimal("0.40"), "TRUCK", needRelabel ? RETURN_RELABEL_ETA : TRANSFER_ETA,
MARKER + "-SHIP-" + suffix,
MARKER + "-TRACK", MARKER + "-BOXES", MARKER, needRelabel, idempotencyKey,
List.of(new WarehouseTransferRequests.Item("SKU-REAL-A", "真实验收商品", 3, MARKER + "-MARK"))));
}
private boolean exists(String table, String idempotencyKey) {
return jdbc.queryForObject("SELECT COUNT(*) FROM " + table + " WHERE customer_id=? AND idempotency_key=?",
Integer.class, CUSTOMER, idempotencyKey) == 1;
}
}
+8
View File
@@ -3677,6 +3677,14 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
'SELECT 1'));
PREPARE phase4_inventory_dimension_030 FROM @ddl; EXECUTE phase4_inventory_dimension_030; DEALLOCATE PREPARE phase4_inventory_dimension_030;
ALTER TABLE ll_inventory_balance COMMENT='客户入仓单与海外目的仓维度实时库存余额';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ll_local_looseboard_order' AND column_name='lastleg_order_id'), 'SELECT 1',
'ALTER TABLE ll_local_looseboard_order ADD COLUMN lastleg_order_id BIGINT NULL COMMENT ''关联尾程主订单ID'' AFTER customer_id'));
PREPARE phase4_looseboard_tail_030 FROM @ddl; EXECUTE phase4_looseboard_tail_030; DEALLOCATE PREPARE phase4_looseboard_tail_030;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
AND table_name='ll_local_looseboard_order' AND index_name='uk_ll_local_looseboard_lastleg'), 'SELECT 1',
'ALTER TABLE ll_local_looseboard_order ADD UNIQUE KEY uk_ll_local_looseboard_lastleg(lastleg_order_id)'));
PREPARE phase4_looseboard_tail_030 FROM @ddl; EXECUTE phase4_looseboard_tail_030; DEALLOCATE PREPARE phase4_looseboard_tail_030;
-- ============================================================================
-- 2026-08-20 / 029