优化
This commit is contained in:
@@ -411,6 +411,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '租户内唯一订单号',
|
||||
inbound_code VARCHAR(40) NULL COMMENT '客户提交后生成的入仓码,草稿为空',
|
||||
customer_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID',
|
||||
customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态',
|
||||
@@ -449,6 +450,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_order_tenant_no (tenant_id, order_no),
|
||||
UNIQUE KEY uk_order_tenant_inbound_code (tenant_id, inbound_code),
|
||||
KEY idx_order_tenant_status_time (tenant_id, status, created_at),
|
||||
KEY idx_order_tenant_customer (tenant_id, customer_id),
|
||||
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
|
||||
@@ -2213,6 +2215,30 @@ PREPARE carrier_name_statement FROM @carrier_name_sql;
|
||||
EXECUTE carrier_name_statement;
|
||||
DEALLOCATE PREPARE carrier_name_statement;
|
||||
|
||||
-- 客户提交时由 Order 固定入仓码;审核通过后 Firstleg 沿用该码创建正式入库单。
|
||||
SET @inbound_code_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ord_order' AND column_name = 'inbound_code'
|
||||
);
|
||||
SET @inbound_code_sql := IF(@inbound_code_exists = 0,
|
||||
'ALTER TABLE ord_order ADD COLUMN inbound_code VARCHAR(40) NULL COMMENT ''客户提交后生成的入仓码,草稿为空'' AFTER order_no',
|
||||
'SELECT 1');
|
||||
PREPARE inbound_code_statement FROM @inbound_code_sql;
|
||||
EXECUTE inbound_code_statement;
|
||||
DEALLOCATE PREPARE inbound_code_statement;
|
||||
|
||||
SET @inbound_code_index_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'ord_order'
|
||||
AND index_name = 'uk_order_tenant_inbound_code'
|
||||
);
|
||||
SET @inbound_code_index_sql := IF(@inbound_code_index_exists = 0,
|
||||
'ALTER TABLE ord_order ADD UNIQUE KEY uk_order_tenant_inbound_code (tenant_id, inbound_code)',
|
||||
'SELECT 1');
|
||||
PREPARE inbound_code_index_statement FROM @inbound_code_index_sql;
|
||||
EXECUTE inbound_code_index_statement;
|
||||
DEALLOCATE PREPARE inbound_code_index_statement;
|
||||
|
||||
-- ============================================================================
|
||||
-- 来源:firstleg\src\main\resources\db\phase2-inbound-schema.sql
|
||||
-- ============================================================================
|
||||
@@ -2294,6 +2320,12 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
CONSTRAINT fk_inbound_task FOREIGN KEY (task_id) REFERENCES fl_fulfillment_task (id)
|
||||
) ENGINE=InnoDB COMMENT='二期头程入库单聚合根和列表查询快照';
|
||||
|
||||
UPDATE baiyue_order.ord_order o
|
||||
LEFT JOIN baiyue_firstleg.fl_inbound_order i
|
||||
ON i.tenant_id = o.tenant_id AND i.order_id = o.id AND i.deleted = 0
|
||||
SET o.inbound_code = COALESCE(i.inbound_code, CONCAT('RK-', o.id))
|
||||
WHERE o.deleted = 0 AND o.status <> 'DRAFT' AND o.inbound_code IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_pickup_snapshot (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '提货信息快照ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
@@ -3440,6 +3472,7 @@ CALL unified_assert_table('baiyue_audit', 'sys_audit_log');
|
||||
CALL unified_assert_table('baiyue_event_bridge', 'bridge_inbox');
|
||||
CALL unified_assert_column('baiyue_order', 'ord_order', 'carrier_id');
|
||||
CALL unified_assert_column('baiyue_order', 'ord_order', 'carrier_name');
|
||||
CALL unified_assert_column('baiyue_order', 'ord_order', 'inbound_code');
|
||||
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id');
|
||||
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_measurement_item', 'external_detail_no');
|
||||
CALL unified_assert_column('baiyue_file', 'file_export_task', 'execution_deadline_at');
|
||||
@@ -4733,6 +4766,21 @@ DROP PROCEDURE IF EXISTS unified_assert_final_extensions;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE unified_assert_final_extensions()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND index_name = 'uk_order_tenant_inbound_code') <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '统一迁移失败:订单入仓码唯一索引未正确创建';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM baiyue_order.ord_order
|
||||
WHERE deleted = 0
|
||||
AND ((status = 'DRAFT' AND inbound_code IS NOT NULL)
|
||||
OR (status <> 'DRAFT' AND inbound_code IS NULL))
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '统一迁移失败:订单状态与入仓码数据不一致';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema = 'baiyue_firstleg'
|
||||
AND table_name IN ('fl_preload_plan', 'fl_split_operation',
|
||||
|
||||
+3
-1
@@ -53,7 +53,9 @@ public class InternalInboundOrderController {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
|
||||
}
|
||||
if (command.tenantId() <= 0 || command.orderId() <= 0 || command.warehouseId() == null
|
||||
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()) {
|
||||
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()
|
||||
|| command.inboundCode() == null || command.inboundCode().isBlank()
|
||||
|| command.inboundCode().length() > 40) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "订单入库快照不完整");
|
||||
}
|
||||
return ApiResponse.success(service.createFromApprovedOrder(command));
|
||||
|
||||
@@ -7,6 +7,7 @@ public record OrderApprovedInboundCommand(
|
||||
long tenantId,
|
||||
long orderId,
|
||||
String orderNo,
|
||||
String inboundCode,
|
||||
Long customerId,
|
||||
String customerName,
|
||||
Long warehouseId,
|
||||
|
||||
@@ -38,7 +38,7 @@ public class InboundOrderCreationService {
|
||||
inbound.setTenantId(command.tenantId());
|
||||
inbound.setOrderId(command.orderId());
|
||||
inbound.setOrderNo(command.orderNo());
|
||||
inbound.setInboundCode("RK-" + command.orderId());
|
||||
inbound.setInboundCode(command.inboundCode());
|
||||
inbound.setProcessStatus("WAIT_MEASURE");
|
||||
inbound.setOrderStatusSnapshot("APPROVED");
|
||||
inbound.setMeasureReviewStatus("NOT_SUBMITTED");
|
||||
|
||||
@@ -6,6 +6,19 @@ info:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/internal/firstleg/v1/inbounds/order-approved:
|
||||
post:
|
||||
summary: 订单审核通过后幂等创建入库单
|
||||
description: 仅供订单服务调用;入仓码由订单提交时生成,Firstleg 必须原样沿用。承运商可在后续履约或计费前补充。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/OrderApprovedInboundCommand'}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败;相同客户和订单重放返回已有入库单 ID}
|
||||
/api/platform/v1/first-leg/tasks:
|
||||
get:
|
||||
summary: 分页查询履约任务
|
||||
@@ -224,6 +237,10 @@ components:
|
||||
in: header
|
||||
name: X-Signature
|
||||
description: HMAC-SHA256 小写十六进制签名
|
||||
internalServiceToken:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-Internal-Service-Token
|
||||
parameters:
|
||||
XTimestamp:
|
||||
name: X-Timestamp
|
||||
@@ -259,6 +276,36 @@ components:
|
||||
in: query
|
||||
schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
|
||||
schemas:
|
||||
OrderApprovedInboundCommand:
|
||||
type: object
|
||||
required: [tenantId, orderId, inboundCode, customerName, warehouseId]
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$', description: 当前客户隔离 ID}
|
||||
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
orderNo: {type: string, nullable: true}
|
||||
inboundCode: {type: string, minLength: 1, maxLength: 40, example: RK-2085308606829346818}
|
||||
customerId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
customerName: {type: string, minLength: 1}
|
||||
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
warehouseName: {type: string, nullable: true}
|
||||
deliveryWarehouseId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
deliveryWarehouseName: {type: string, nullable: true}
|
||||
expectedArrivalAt: {type: string, format: date-time, nullable: true}
|
||||
businessType: {type: string, nullable: true}
|
||||
channelCode: {type: string, nullable: true}
|
||||
channelName: {type: string, nullable: true}
|
||||
carrierId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: 可选,可在后续履约或计费前补充}
|
||||
carrierName: {type: string, nullable: true}
|
||||
taxMode: {type: string, nullable: true}
|
||||
containerMode: {type: string, nullable: true}
|
||||
customsInspection: {type: boolean, nullable: true}
|
||||
destinationType: {type: string, nullable: true}
|
||||
destinationWarehouseId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
destinationCode: {type: string, nullable: true}
|
||||
destinationName: {type: string, nullable: true}
|
||||
publicRemark: {type: string, nullable: true}
|
||||
expressCompanyName: {type: string, nullable: true}
|
||||
expressTrackingNo: {type: string, nullable: true}
|
||||
MerchantInboundBillingEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
+25
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.InboundOrderReference;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.service.InboundOrderCreationService;
|
||||
import com.baiyue.firstleg.service.UnifiedInboundQueryService;
|
||||
import com.baiyue.firstleg.dto.UnifiedInboundQuery;
|
||||
@@ -63,4 +64,28 @@ class InternalInboundOrderReferenceControllerTest {
|
||||
.isInstanceOf(BizException.class);
|
||||
verify(unifiedQueryService).page(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedOrderRequiresInboundCode() {
|
||||
assertThatThrownBy(() -> controller.createFromApprovedOrder(
|
||||
"internal-token", approvedCommand(" ", null)))
|
||||
.isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedOrderAllowsMissingCarrier() {
|
||||
OrderApprovedInboundCommand command = approvedCommand("RK-77", null);
|
||||
when(service.createFromApprovedOrder(command)).thenReturn(88L);
|
||||
|
||||
assertThat(controller.createFromApprovedOrder("internal-token", command).data()).isEqualTo(88L);
|
||||
verify(service).createFromApprovedOrder(command);
|
||||
}
|
||||
|
||||
private static OrderApprovedInboundCommand approvedCommand(String inboundCode, Long carrierId) {
|
||||
return new OrderApprovedInboundCommand(
|
||||
10001L, 77L, "FL-77", inboundCode, 7L, "测试客户", 9L, "义乌仓",
|
||||
null, null, null, "FBA", "AIR", "空运", carrierId,
|
||||
carrierId == null ? null : "承运商", null, null, null, null,
|
||||
null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundOrderCreationServiceTest {
|
||||
@Mock
|
||||
private InboundOrderMapper orders;
|
||||
@Mock
|
||||
private InboundStatusHistoryMapper histories;
|
||||
@InjectMocks
|
||||
private InboundOrderCreationService service;
|
||||
|
||||
@Test
|
||||
void shouldUseSubmittedInboundCodeAndKeepReplayIdempotentWithoutCarrier() {
|
||||
OrderApprovedInboundCommand command = approvedCommand("RK-CUSTOM-77");
|
||||
InboundOrderEntity existing = new InboundOrderEntity();
|
||||
existing.setId(900L);
|
||||
existing.setTenantId(10001L);
|
||||
existing.setOrderId(77L);
|
||||
existing.setInboundCode("RK-CUSTOM-77");
|
||||
when(orders.selectOne(any(Wrapper.class))).thenReturn(null, existing);
|
||||
|
||||
long createdId = service.createFromApprovedOrder(command);
|
||||
long replayedId = service.createFromApprovedOrder(command);
|
||||
|
||||
ArgumentCaptor<InboundOrderEntity> inboundCaptor = ArgumentCaptor.forClass(InboundOrderEntity.class);
|
||||
verify(orders, times(1)).insert(inboundCaptor.capture());
|
||||
verify(histories, times(1)).insert(any(InboundStatusHistoryEntity.class));
|
||||
assertThat(inboundCaptor.getValue().getInboundCode()).isEqualTo("RK-CUSTOM-77");
|
||||
assertThat(inboundCaptor.getValue().getCarrierId()).isNull();
|
||||
assertThat(createdId).isEqualTo(inboundCaptor.getValue().getId());
|
||||
assertThat(replayedId).isEqualTo(900L);
|
||||
}
|
||||
|
||||
private static OrderApprovedInboundCommand approvedCommand(String inboundCode) {
|
||||
return new OrderApprovedInboundCommand(
|
||||
10001L, 77L, "FL-77", inboundCode, 7L, "测试客户", 9L, "义乌仓",
|
||||
null, null, null, "FBA", "AIR", "空运", null, null,
|
||||
null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public interface FirstlegInboundClient {
|
||||
}
|
||||
|
||||
record OrderApprovedInboundCommand(
|
||||
long tenantId, long orderId, String orderNo, Long customerId, String customerName,
|
||||
long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName,
|
||||
Long warehouseId, String warehouseName, Long deliveryWarehouseId, String deliveryWarehouseName,
|
||||
LocalDateTime expectedArrivalAt, String businessType, String channelCode, String channelName,
|
||||
Long carrierId, String carrierName, String taxMode, String containerMode,
|
||||
|
||||
+2
-2
@@ -146,7 +146,7 @@ public class PlatformFirstLegOrderController {
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ORDER_APPROVE", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "Approve order / 审核通过")
|
||||
@Operation(summary = "Approve / 审核通过", description = "Approves a submitted order and records its status history. / 审核通过已提交订单,并记录状态历史。")
|
||||
@Operation(summary = "Approve / 审核通过", description = "Approves a submitted order without requiring carrier assignment and records its status history. / 审核通过已提交订单,不要求预先指派承运商,并记录状态历史。")
|
||||
@PostMapping("/{id}/approve")
|
||||
@PreAuthorize("hasAuthority('platform:order:approve')")
|
||||
public ApiResponse<Void> approve(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@@ -156,7 +156,7 @@ public class PlatformFirstLegOrderController {
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ORDER_ASSIGN_CARRIER", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "指派订单承运商")
|
||||
@Operation(summary = "指派承运商", description = "为待审核订单选择启用的公共承运商,名称快照由服务端主数据回填。")
|
||||
@Operation(summary = "指派承运商", description = "可选地为待审核订单选择启用的公共承运商,名称快照由服务端主数据回填;该动作不是审核前置条件。")
|
||||
@PutMapping("/{id}/carrier")
|
||||
@PreAuthorize("hasAuthority('platform:order:approve')")
|
||||
public ApiResponse<Void> assignCarrier(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
@@ -21,6 +21,7 @@ public class OrderEntity extends BaseEntity {
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private String orderNo;
|
||||
private String inboundCode;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private OrderStatus status;
|
||||
|
||||
@@ -161,6 +161,7 @@ public class OrderRepository {
|
||||
.eq(tenantId != null, OrderEntity::getTenantId, tenantId)
|
||||
.in(OrderEntity::getStatus, statuses)
|
||||
.like(StringUtils.hasText(query.orderNo()), OrderEntity::getOrderNo, query.orderNo())
|
||||
.like(StringUtils.hasText(query.inboundCode()), OrderEntity::getInboundCode, query.inboundCode())
|
||||
.eq(query.customerId() != null, OrderEntity::getCustomerId, query.customerId())
|
||||
.eq(query.receivingWarehouseId() != null, OrderEntity::getReceivingWarehouseId,
|
||||
query.receivingWarehouseId())
|
||||
|
||||
@@ -97,6 +97,7 @@ public class OrderApplicationService {
|
||||
throw new BizException("当前订单状态不允许编辑订单资料");
|
||||
}
|
||||
apply(order, request, snapshots); order.setStatus(OrderStatus.DRAFT); order.setUpdatedBy(userId);
|
||||
if (before == OrderStatus.REJECTED) order.setInboundCode(null);
|
||||
if (before == OrderStatus.SUBMITTED) {
|
||||
order.setStatus(OrderStatus.SUBMITTED);
|
||||
}
|
||||
@@ -118,11 +119,7 @@ public class OrderApplicationService {
|
||||
masterDataGuard.validatePersisted(tenantId, order.getCustomerId(), order.getReceivingWarehouseId(),
|
||||
order.getDestinationWarehouseId(), order.getDestinationType());
|
||||
}
|
||||
if (target == OrderStatus.APPROVED) {
|
||||
if (order.getCarrierId() == null || !StringUtils.hasText(order.getCarrierName())) {
|
||||
throw new BizException(com.baiyue.common.core.api.ResultCode.VALIDATION_FAILED,
|
||||
"请先选择有效承运商后再审核通过");
|
||||
}
|
||||
if (target == OrderStatus.APPROVED && order.getCarrierId() != null) {
|
||||
var carrier = masterDataGuard.validateCarrier(order.getCarrierId());
|
||||
order.setCarrierName(carrier.partnerName());
|
||||
}
|
||||
@@ -130,7 +127,11 @@ public class OrderApplicationService {
|
||||
throw new BizException("Reason is required");
|
||||
}
|
||||
order.setStatus(target); order.setUpdatedBy(userId);
|
||||
if (target == OrderStatus.SUBMITTED) order.setSubmittedAt(LocalDateTime.now());
|
||||
if (target == OrderStatus.SUBMITTED) {
|
||||
if (!StringUtils.hasText(order.getInboundCode())) order.setInboundCode("RK-" + order.getId());
|
||||
order.setSubmittedAt(LocalDateTime.now());
|
||||
}
|
||||
if (target == OrderStatus.DRAFT) order.setInboundCode(null);
|
||||
if (target == OrderStatus.COMPLETED) order.setCompletedAt(LocalDateTime.now());
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
|
||||
record(tenantId, id, from, target, target.name(), reason, userId, username);
|
||||
@@ -423,7 +424,8 @@ public class OrderApplicationService {
|
||||
}
|
||||
private FirstlegInboundClient.OrderApprovedInboundCommand inboundCommand(OrderEntity order) {
|
||||
return new FirstlegInboundClient.OrderApprovedInboundCommand(
|
||||
order.getTenantId(), order.getId(), order.getOrderNo(), order.getCustomerId(), order.getCustomerName(),
|
||||
order.getTenantId(), order.getId(), order.getOrderNo(), order.getInboundCode(),
|
||||
order.getCustomerId(), order.getCustomerName(),
|
||||
order.getReceivingWarehouseId(), order.getReceivingWarehouseName(), order.getDestinationWarehouseId(),
|
||||
order.getDestinationWarehouseName(), order.getExpectedShipDate() == null ? null : order.getExpectedShipDate().atStartOfDay(),
|
||||
order.getDestinationType() + "_" + order.getTransportMode(), order.getChannelCode(), order.getChannelName(),
|
||||
|
||||
@@ -51,8 +51,9 @@ public class UnifiedOrderQueryService {
|
||||
List<OrderStatus> statuses = "PENDING_REVIEW".equals(query.mainStatus())
|
||||
? List.of(OrderStatus.SUBMITTED) : ACTIVE_ORDER_STATUSES;
|
||||
PageResult<OrderEntity> source = orders.unifiedPage(tenantId, query, statuses);
|
||||
Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = summaries(
|
||||
tenantId, source.records().stream().map(OrderEntity::getId).toList());
|
||||
Map<Long, FirstlegUnifiedOrderClient.Summary> inboundByOrder = "PENDING_REVIEW".equals(query.mainStatus())
|
||||
? Map.of()
|
||||
: summaries(tenantId, source.records().stream().map(OrderEntity::getId).toList());
|
||||
List<UnifiedOrderResponse.Row> rows = source.records().stream()
|
||||
.map(order -> row(order, inboundByOrder.get(order.getId()))).toList();
|
||||
return new PageResult<>(rows, source.page(), source.size(), source.total(), source.pages());
|
||||
@@ -64,7 +65,8 @@ public class UnifiedOrderQueryService {
|
||||
OrderDetailResponse orderDetail = new OrderDetailResponse(order, orders.findCargo(tenantId, orderId),
|
||||
orders.findParties(tenantId, orderId), includeInternalNotes ? orders.findNotes(tenantId, orderId) : List.of(),
|
||||
orders.findHistory(tenantId, orderId));
|
||||
FirstlegUnifiedOrderClient.Detail inbound = firstlegDetail(tenantId, orderId);
|
||||
FirstlegUnifiedOrderClient.Detail inbound = order.getStatus() == OrderStatus.SUBMITTED
|
||||
? null : firstlegDetail(tenantId, orderId);
|
||||
UnifiedOrderResponse.Row summary = row(order, inbound == null ? null : inbound.summary());
|
||||
List<UnifiedOrderResponse.TimelineEvent> timeline = new ArrayList<>();
|
||||
orderDetail.statusHistory().forEach(item -> timeline.add(new UnifiedOrderResponse.TimelineEvent(
|
||||
@@ -121,7 +123,8 @@ public class UnifiedOrderQueryService {
|
||||
order.getStatus() == OrderStatus.SUBMITTED ? "APPROVE_ORDER" : null,
|
||||
order.getStatus(), order.getCustomerId(), order.getCustomerName(),
|
||||
order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
|
||||
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null, null,
|
||||
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null,
|
||||
order.getInboundCode(),
|
||||
order.getExpectedShipDate(), null, null, null, null, null, null, null, null,
|
||||
order.getCreatedAt(), order.getUpdatedAt());
|
||||
}
|
||||
@@ -213,7 +216,7 @@ public class UnifiedOrderQueryService {
|
||||
|
||||
private boolean requiresFirstlegQuery(UnifiedOrderQuery query) {
|
||||
return (StringUtils.hasText(query.mainStatus()) && !"PENDING_REVIEW".equals(query.mainStatus()))
|
||||
|| StringUtils.hasText(query.inboundCode()) || query.expectedArrivalDate() != null
|
||||
|| query.expectedArrivalDate() != null
|
||||
|| StringUtils.hasText(query.feeReviewStatus()) || StringUtils.hasText(query.containerNo())
|
||||
|| StringUtils.hasText(query.cabinetNo()) || query.sailingDate() != null || query.loaded() != null
|
||||
|| StringUtils.hasText(query.measurementReviewStatus());
|
||||
|
||||
@@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '租户内唯一订单号',
|
||||
inbound_code VARCHAR(40) NULL COMMENT '客户提交后生成的入仓码,草稿为空',
|
||||
customer_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID',
|
||||
customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态',
|
||||
@@ -47,6 +48,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_order_tenant_no (tenant_id, order_no),
|
||||
UNIQUE KEY uk_order_tenant_inbound_code (tenant_id, inbound_code),
|
||||
KEY idx_order_tenant_status_time (tenant_id, status, created_at),
|
||||
KEY idx_order_tenant_customer (tenant_id, customer_id),
|
||||
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
|
||||
|
||||
@@ -11,7 +11,8 @@ paths:
|
||||
summary: 平台统一订单列表
|
||||
description: >-
|
||||
统一返回七态产品主状态、下一步动作及测量/预装/装柜/派送摘要,需要 platform:order:list。
|
||||
有入库单时同时返回 inboundId、inboundVersion、loadingVersion、pickingVersion;inboundVersion
|
||||
草稿不生成 inboundCode,客户提交进入待审核后即返回固定 inboundCode;有入库单时同时返回
|
||||
inboundId、inboundVersion、loadingVersion、pickingVersion;inboundVersion
|
||||
才是 C04 loading/complete 的 expectedVersion,loadingVersion 当前等于 pickingVersion(兼容别名)。
|
||||
tenantId 省略时跨全部客户;mainStatus 省略时按订单创建时间分页,指定 PENDING_REVIEW 时由 Order 查询,
|
||||
指定其他主状态时由 Firstleg 精确分页。旧 /orders/first-leg 接口保持兼容。
|
||||
@@ -45,7 +46,7 @@ paths:
|
||||
/api/platform/v1/orders/{orderId}:
|
||||
get:
|
||||
summary: 平台统一订单详情
|
||||
description: 合并 Order 与 Firstleg 时间线并按发生时间升序返回;平台可见内部备注。
|
||||
description: 合并 Order 与 Firstleg 时间线并按发生时间升序返回;待审核订单直接返回 Order 在提交时生成的 inboundCode;平台可见内部备注。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UnifiedOrderId'
|
||||
- $ref: '#/components/parameters/UnifiedTenantId'
|
||||
@@ -222,7 +223,7 @@ paths:
|
||||
/api/platform/v1/orders/first-leg/{id}/carrier:
|
||||
put:
|
||||
summary: 为待审核订单指派承运商
|
||||
description: 只接收承运商 ID;服务端从公共往来单位主数据校验 CARRIER 类型并写入可信名称快照。审核通过前必须完成指派。
|
||||
description: 可选的独立指派动作;服务端从公共往来单位主数据校验 CARRIER 类型并写入可信名称快照,不作为审核通过的前置条件。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/OrderId'
|
||||
- $ref: '#/components/parameters/TenantId'
|
||||
@@ -263,7 +264,7 @@ components:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
UnifiedInboundCode:
|
||||
{name: inboundCode, in: query, required: false, schema: {type: string, maxLength: 64}}
|
||||
{name: inboundCode, in: query, required: false, description: 客户提交后生成的入仓码,待审核订单同样可查询, schema: {type: string, maxLength: 40}}
|
||||
UnifiedInternalOrderStatus:
|
||||
name: internalOrderStatus
|
||||
in: query
|
||||
|
||||
@@ -54,7 +54,8 @@ class OrderApplicationServiceStateTest {
|
||||
|
||||
assertThat(orderId).isNotNull();
|
||||
verify(eligibility, never()).requireApproved(anyLong());
|
||||
verify(repository).insertOrder(argThat(order -> order.getStatus() == OrderStatus.DRAFT));
|
||||
verify(repository).insertOrder(argThat(order -> order.getStatus() == OrderStatus.DRAFT
|
||||
&& order.getInboundCode() == null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -120,17 +121,20 @@ class OrderApplicationServiceStateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvalRequiresCarrierAssignment() {
|
||||
void approvalDoesNotRequireCarrierAssignmentAndPublishesInboundCode() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderEntity order = persistedOrder(OrderStatus.SUBMITTED);
|
||||
order.setOrderNo("FL1");
|
||||
order.setInboundCode("RK-1");
|
||||
when(repository.findOwned(10001L, 1L)).thenReturn(order);
|
||||
when(repository.saveOrder(10001L, order)).thenReturn(1);
|
||||
|
||||
assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class))
|
||||
.transit(10001L, 2L, "u", 1L, OrderStatus.APPROVED, null))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("请先选择有效承运商");
|
||||
service(repository, mock(OrderEligibilityGuard.class), mock(OrderMasterDataGuard.class))
|
||||
.transit(10001L, 2L, "u", 1L, OrderStatus.APPROVED, null);
|
||||
|
||||
verify(repository, never()).saveOrder(anyLong(), any());
|
||||
assertThat(order.getStatus()).isEqualTo(OrderStatus.APPROVED);
|
||||
verify(repository).insertOutbox(argThat(event -> event.getPayloadJson().contains("\"inboundCode\":\"RK-1\"")
|
||||
&& event.getPayloadJson().contains("\"carrierId\":null")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -153,6 +157,25 @@ class OrderApplicationServiceStateTest {
|
||||
verify(masterData).validateCarrier(9L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void submittingDraftGeneratesAndPersistsInboundCode() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
OrderEntity order = persistedDraft("SELF_EXPRESS", "OVERSEAS");
|
||||
when(repository.findOwned(20002L, 1L)).thenReturn(order);
|
||||
when(repository.findCargo(20002L, 1L)).thenReturn(List.of(new OrderCargoEntity()));
|
||||
when(repository.findParties(20002L, 1L)).thenReturn(List.of());
|
||||
when(repository.saveOrder(20002L, order)).thenReturn(1);
|
||||
|
||||
service(repository, mock(OrderEligibilityGuard.class), masterData)
|
||||
.transit(20002L, 2L, "u", 1L, OrderStatus.SUBMITTED, null);
|
||||
|
||||
assertThat(order.getStatus()).isEqualTo(OrderStatus.SUBMITTED);
|
||||
assertThat(order.getInboundCode()).isEqualTo("RK-1");
|
||||
assertThat(order.getSubmittedAt()).isNotNull();
|
||||
verify(repository).saveOrder(20002L, order);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantDraftEditorStillCannotEditSubmittedOrder() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
|
||||
@@ -38,6 +38,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
void pendingReviewIsPagedFromOrderAndMappedToApproveAction() {
|
||||
UnifiedOrderQuery query = query("PENDING_REVIEW");
|
||||
OrderEntity order = order(OrderStatus.SUBMITTED);
|
||||
order.setInboundCode("RK-77");
|
||||
when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED))))
|
||||
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
|
||||
when(firstleg.summaries(eq("internal-token"), any()))
|
||||
@@ -48,7 +49,58 @@ class UnifiedOrderQueryServiceTest {
|
||||
assertThat(result.records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.mainStatus()).isEqualTo("PENDING_REVIEW");
|
||||
assertThat(row.nextAction()).isEqualTo("APPROVE_ORDER");
|
||||
assertThat(row.inboundCode()).isEqualTo("RK-77");
|
||||
assertThat(row.inboundId()).isNull();
|
||||
});
|
||||
verify(firstleg, never()).summaries(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingReviewInboundCodeFilterStaysInOrderQuery() {
|
||||
UnifiedOrderQuery query = new UnifiedOrderQuery(
|
||||
null, null, "PENDING_REVIEW", null, null, null, "RK-77", null,
|
||||
null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, 1, 20);
|
||||
when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED))))
|
||||
.thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||
|
||||
service.page(10001L, query);
|
||||
|
||||
verify(orders).unifiedPage(10001L, query, List.of(OrderStatus.SUBMITTED));
|
||||
verify(firstleg, never()).page(any(), any());
|
||||
verify(firstleg, never()).summaries(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void allStatusInboundCodeFilterIncludesPendingOrdersFromOrderQuery() {
|
||||
UnifiedOrderQuery query = new UnifiedOrderQuery(
|
||||
null, null, null, null, null, null, "RK-77", null,
|
||||
null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, 1, 20);
|
||||
when(orders.unifiedPage(eq(10001L), eq(query), any()))
|
||||
.thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||
|
||||
service.page(10001L, query);
|
||||
|
||||
verify(orders).unifiedPage(eq(10001L), eq(query), any());
|
||||
verify(firstleg, never()).page(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingReviewDetailUsesOrderInboundCodeWithoutCallingFirstleg() {
|
||||
OrderEntity order = order(OrderStatus.SUBMITTED);
|
||||
order.setInboundCode("RK-77");
|
||||
when(orders.findOwned(10001L, 77L)).thenReturn(order);
|
||||
when(orders.findCargo(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findParties(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findNotes(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findHistory(10001L, 77L)).thenReturn(List.of());
|
||||
|
||||
var result = service.detail(10001L, 77L, true);
|
||||
|
||||
assertThat(result.summary().inboundCode()).isEqualTo("RK-77");
|
||||
assertThat(result.order().order().getInboundCode()).isEqualTo("RK-77");
|
||||
verify(firstleg, never()).detail(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user