优化
This commit is contained in:
@@ -515,7 +515,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 '提交后生成并固定的入仓码;未提交草稿为空',
|
||||
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 '订单业务状态:DRAFT草稿、SUBMITTED已提交、APPROVED审核通过、ACCEPTED已接单、REJECTED审核驳回、PROCESSING履约中、COMPLETED已完成、CANCELLED已取消',
|
||||
@@ -556,7 +556,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),
|
||||
UNIQUE KEY uk_order_inbound_code (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)
|
||||
@@ -569,7 +569,7 @@ SET @order_inbound_code_nullable_sql = IF(
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND column_name = 'inbound_code' AND is_nullable = 'NO'
|
||||
),
|
||||
'ALTER TABLE baiyue_order.ord_order MODIFY COLUMN inbound_code VARCHAR(40) NULL COMMENT ''提交后生成并固定的入仓码;未提交草稿为空''',
|
||||
'ALTER TABLE baiyue_order.ord_order MODIFY COLUMN inbound_code VARCHAR(40) NULL COMMENT ''全平台唯一入仓码;来源由全局模式决定,提交后固定''',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE order_inbound_code_nullable_stmt FROM @order_inbound_code_nullable_sql;
|
||||
@@ -2427,7 +2427,7 @@ SET @inbound_code_exists := (
|
||||
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',
|
||||
'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;
|
||||
@@ -2436,15 +2436,27 @@ 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'
|
||||
AND index_name = 'uk_order_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)',
|
||||
'ALTER TABLE ord_order ADD UNIQUE KEY uk_order_inbound_code (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;
|
||||
|
||||
SET @legacy_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 @legacy_inbound_code_index_sql := IF(@legacy_inbound_code_index_exists > 0,
|
||||
'ALTER TABLE ord_order DROP INDEX uk_order_tenant_inbound_code',
|
||||
'SELECT 1');
|
||||
PREPARE legacy_inbound_code_index_statement FROM @legacy_inbound_code_index_sql;
|
||||
EXECUTE legacy_inbound_code_index_statement;
|
||||
DEALLOCATE PREPARE legacy_inbound_code_index_statement;
|
||||
|
||||
-- ============================================================================
|
||||
-- 来源:firstleg\src\main\resources\db\phase2-inbound-schema.sql
|
||||
-- ============================================================================
|
||||
@@ -2460,7 +2472,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
task_id BIGINT UNSIGNED NULL COMMENT '头程履约任务ID',
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '客户内唯一入仓码',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '全平台唯一入仓码',
|
||||
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、PREPARING草稿预入库、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、WAIT_SPLIT待分柜',
|
||||
order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照',
|
||||
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
||||
@@ -2513,7 +2525,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_inbound_tenant_code (tenant_id, inbound_code),
|
||||
UNIQUE KEY uk_inbound_code (inbound_code),
|
||||
UNIQUE KEY uk_inbound_tenant_order (tenant_id, order_id),
|
||||
KEY idx_inbound_process_time (tenant_id, process_status, created_at, id),
|
||||
KEY idx_inbound_measure_time (tenant_id, measure_review_status, created_at, id),
|
||||
@@ -2527,6 +2539,30 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
CONSTRAINT fk_inbound_task FOREIGN KEY (task_id) REFERENCES fl_fulfillment_task (id)
|
||||
) ENGINE=InnoDB COMMENT='二期头程入库单聚合根和列表查询快照';
|
||||
|
||||
SET @inbound_global_code_index_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order'
|
||||
AND index_name = 'uk_inbound_code'
|
||||
);
|
||||
SET @inbound_global_code_index_sql := IF(@inbound_global_code_index_exists = 0,
|
||||
'ALTER TABLE fl_inbound_order ADD UNIQUE KEY uk_inbound_code (inbound_code)',
|
||||
'SELECT 1');
|
||||
PREPARE inbound_global_code_index_statement FROM @inbound_global_code_index_sql;
|
||||
EXECUTE inbound_global_code_index_statement;
|
||||
DEALLOCATE PREPARE inbound_global_code_index_statement;
|
||||
|
||||
SET @inbound_legacy_code_index_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order'
|
||||
AND index_name = 'uk_inbound_tenant_code'
|
||||
);
|
||||
SET @inbound_legacy_code_index_sql := IF(@inbound_legacy_code_index_exists > 0,
|
||||
'ALTER TABLE fl_inbound_order DROP INDEX uk_inbound_tenant_code',
|
||||
'SELECT 1');
|
||||
PREPARE inbound_legacy_code_index_statement FROM @inbound_legacy_code_index_sql;
|
||||
EXECUTE inbound_legacy_code_index_statement;
|
||||
DEALLOCATE PREPARE inbound_legacy_code_index_statement;
|
||||
|
||||
UPDATE baiyue_order.ord_order o
|
||||
JOIN baiyue_firstleg.fl_inbound_order i
|
||||
ON i.tenant_id = o.tenant_id AND i.order_id = o.id AND i.deleted = 0
|
||||
@@ -12797,7 +12833,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'fee_review_sta
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'fee_status', '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'hold_flag', '当前是否扣货');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'id', '入库单ID(雪花ID)');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'inbound_code', '客户内唯一入仓码');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'inbound_code', '全平台唯一入仓码');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'inbound_role', '入仓单层级:MASTER主单、CHILD子单');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'inbounded_at', '入库完成时间');
|
||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'master_order_id', '仅用于约束有效主单订单唯一性');
|
||||
@@ -13769,7 +13805,7 @@ CALL queue_column_comment('baiyue_order', 'ord_order', 'expected_ship_date', '
|
||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'express_company', '履约快递公司名称快照');
|
||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'express_tracking_no', '履约快递单号');
|
||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'id', '订单ID(雪花ID)');
|
||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'inbound_code', '客户提交后生成的入仓码,草稿为空');
|
||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'inbound_code', '全平台唯一入仓码;来源由全局模式决定,提交后固定');
|
||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'order_no', '客户范围内唯一订单号');
|
||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'order_role', '订单层级:MASTER主订单、CHILD子订单');
|
||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'parent_order_id', '直属父订单ID;主订单为空;禁止子订单嵌套');
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ public class InternalPriceBookController {
|
||||
@Valid @RequestBody PriceBookChannelValidation.Request request) {
|
||||
requireToken(token);
|
||||
return ApiResponse.success(service.requireAvailableChannel(request.tenantId(), request.originWarehouseId(),
|
||||
request.channelCode(), request.destinationType()));
|
||||
request.channelCode(), request.destinationType(), request.requireInboundCodePrefix()));
|
||||
}
|
||||
|
||||
private void requireToken(String token) {
|
||||
|
||||
@@ -12,7 +12,8 @@ public final class PriceBookChannelValidation {
|
||||
@Min(1) long tenantId,
|
||||
@Min(1) long originWarehouseId,
|
||||
@NotBlank @Size(max = 64) String channelCode,
|
||||
@NotBlank @Pattern(regexp = "FBA|OVERSEAS|PRIVATE") String destinationType
|
||||
@NotBlank @Pattern(regexp = "FBA|OVERSEAS|PRIVATE") String destinationType,
|
||||
boolean requireInboundCodePrefix
|
||||
) {}
|
||||
|
||||
public record Response(String channelCode, String inboundCodePrefix) {}
|
||||
|
||||
@@ -49,7 +49,7 @@ public class MerchantPriceBookService {
|
||||
}
|
||||
|
||||
public Response requireAvailableChannel(long merchantTenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType) {
|
||||
String destinationType, boolean requireInboundCodePrefix) {
|
||||
requireMerchant(merchantTenantId);
|
||||
if (originWarehouseId <= 0) throw validation("请选择有效起运仓");
|
||||
Long versionId = repository.activeVersionId();
|
||||
@@ -57,6 +57,9 @@ public class MerchantPriceBookService {
|
||||
if (versionId == null || !repository.channelAvailable(versionId, originWarehouseId, normalized)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "所选渠道已下线,请重新选择");
|
||||
}
|
||||
if (!requireInboundCodePrefix) {
|
||||
return new Response(normalized, null);
|
||||
}
|
||||
var prefix = repository.channelPrefix(normalized);
|
||||
if (prefix == null || !StringUtils.hasText(prefix.inboundCodePrefix())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置进仓编码");
|
||||
|
||||
@@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
task_id BIGINT UNSIGNED NULL COMMENT '头程履约任务ID',
|
||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '客户内唯一入仓码',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '全平台唯一入仓码',
|
||||
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、WAIT_SPLIT待分柜',
|
||||
order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照',
|
||||
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
||||
@@ -63,7 +63,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_inbound_tenant_code (tenant_id, inbound_code),
|
||||
UNIQUE KEY uk_inbound_code (inbound_code),
|
||||
UNIQUE KEY uk_inbound_tenant_order (tenant_id, order_id),
|
||||
KEY idx_inbound_process_time (tenant_id, process_status, created_at, id),
|
||||
KEY idx_inbound_measure_time (tenant_id, measure_review_status, created_at, id),
|
||||
@@ -77,6 +77,33 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
CONSTRAINT fk_inbound_task FOREIGN KEY (task_id) REFERENCES fl_fulfillment_task (id)
|
||||
) ENGINE=InnoDB COMMENT='二期头程入库单聚合根和列表查询快照';
|
||||
|
||||
-- 既有开发库若存在跨客户重复码则严格失败,禁止自动覆盖或改写入仓码。
|
||||
SET @inbound_global_code_index_sql = IF(
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_order'
|
||||
AND index_name = 'uk_inbound_code'
|
||||
),
|
||||
'ALTER TABLE baiyue_firstleg.fl_inbound_order ADD UNIQUE KEY uk_inbound_code (inbound_code)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE inbound_global_code_index_stmt FROM @inbound_global_code_index_sql;
|
||||
EXECUTE inbound_global_code_index_stmt;
|
||||
DEALLOCATE PREPARE inbound_global_code_index_stmt;
|
||||
|
||||
SET @inbound_tenant_code_index_drop_sql = IF(
|
||||
EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_order'
|
||||
AND index_name = 'uk_inbound_tenant_code'
|
||||
),
|
||||
'ALTER TABLE baiyue_firstleg.fl_inbound_order DROP INDEX uk_inbound_tenant_code',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE inbound_tenant_code_index_drop_stmt FROM @inbound_tenant_code_index_drop_sql;
|
||||
EXECUTE inbound_tenant_code_index_drop_stmt;
|
||||
DEALLOCATE PREPARE inbound_tenant_code_index_drop_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_pickup_snapshot (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '提货信息快照ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
|
||||
@@ -51,8 +51,8 @@ paths:
|
||||
'200': {description: 幂等补偿完成}
|
||||
/internal/firstleg/v1/pricing/channels/validate:
|
||||
post:
|
||||
summary: 校验订单渠道并返回进仓编码前缀
|
||||
description: 仅供订单服务调用;前缀按渠道和目的仓类型选择,同一渠道的包税与不包税订单共用编码前缀。
|
||||
summary: 校验订单渠道并按需返回进仓编码前缀
|
||||
description: 仅供订单服务调用;AUTO 模式要求并返回编码前缀,LEGACY_MANUAL 模式仅校验渠道可用性。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
requestBody:
|
||||
@@ -61,14 +61,15 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [tenantId, originWarehouseId, channelCode, destinationType]
|
||||
required: [tenantId, originWarehouseId, channelCode, destinationType, requireInboundCodePrefix]
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
originWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
channelCode: {type: string, minLength: 1, maxLength: 64}
|
||||
destinationType: {type: string, enum: [FBA, OVERSEAS, PRIVATE]}
|
||||
requireInboundCodePrefix: {type: boolean}
|
||||
responses:
|
||||
'200': {description: 成功时 data 返回 channelCode 和 inboundCodePrefix;未配置对应进仓编码时返回业务冲突}
|
||||
'200': {description: 成功时 data 返回 channelCode;requireInboundCodePrefix=true 时同时返回 inboundCodePrefix}
|
||||
/api/platform/v1/first-leg/customer-dimension-config:
|
||||
get:
|
||||
summary: 查询客户固定四箱规尺寸配置
|
||||
|
||||
@@ -120,7 +120,7 @@ class PriceBookContractTest {
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(firstlegOpenapi)
|
||||
.contains("/internal/firstleg/v1/pricing/channels/validate:")
|
||||
.contains("required: [tenantId, originWarehouseId, channelCode, destinationType]")
|
||||
.contains("同一渠道的包税与不包税订单共用编码前缀");
|
||||
.contains("required: [tenantId, originWarehouseId, channelCode, destinationType, requireInboundCodePrefix]")
|
||||
.contains("LEGACY_MANUAL 模式仅校验渠道可用性");
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InboundCodeGlobalUniquenessContractTest {
|
||||
@Test
|
||||
void inboundSchemaUsesTheSamePlatformWideInboundCodeIndex() throws Exception {
|
||||
String sql = new ClassPathResource("db/phase2-inbound-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("UNIQUE KEY uk_inbound_code (inbound_code)")
|
||||
.doesNotContain("UNIQUE KEY uk_inbound_tenant_code (tenant_id, inbound_code)");
|
||||
}
|
||||
}
|
||||
+15
-3
@@ -51,9 +51,9 @@ class MerchantPriceBookServiceTest {
|
||||
when(repository.channelPrefix("MATSON_CLX_TRUCK"))
|
||||
.thenReturn(new ChannelPrefixRow("MATSON_CLX_TRUCK", "ZBYHK", "ZBYHWK"));
|
||||
|
||||
assertThat(service.requireAvailableChannel(10001L, 10L, "matson_clx_truck", "FBA")
|
||||
assertThat(service.requireAvailableChannel(10001L, 10L, "matson_clx_truck", "FBA", true)
|
||||
.inboundCodePrefix()).isEqualTo("ZBYHK");
|
||||
assertThat(service.requireAvailableChannel(10001L, 10L, "MATSON_CLX_TRUCK", "OVERSEAS")
|
||||
assertThat(service.requireAvailableChannel(10001L, 10L, "MATSON_CLX_TRUCK", "OVERSEAS", true)
|
||||
.inboundCodePrefix()).isEqualTo("ZBYHWK");
|
||||
}
|
||||
|
||||
@@ -66,10 +66,22 @@ class MerchantPriceBookServiceTest {
|
||||
when(repository.channelPrefix("OA_HOUSTON_TRUCK"))
|
||||
.thenReturn(new ChannelPrefixRow("OA_HOUSTON_TRUCK", "HPHK", null));
|
||||
|
||||
assertThat(service.requireAvailableChannel(10001L, 10L, "OA_HOUSTON_TRUCK", "OVERSEAS")
|
||||
assertThat(service.requireAvailableChannel(10001L, 10L, "OA_HOUSTON_TRUCK", "OVERSEAS", true)
|
||||
.inboundCodePrefix()).isEqualTo("HPHK");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateLegacyChannelWithoutRequiringAnInboundCodePrefix() {
|
||||
when(repository.activeVersionId()).thenReturn(7L);
|
||||
when(repository.channelAvailable(7L, 10L, "LEGACY_CHANNEL")).thenReturn(true);
|
||||
var response = service.requireAvailableChannel(
|
||||
10001L, 10L, "LEGACY_CHANNEL", "FBA", false);
|
||||
|
||||
assertThat(response.channelCode()).isEqualTo("LEGACY_CHANNEL");
|
||||
assertThat(response.inboundCodePrefix()).isNull();
|
||||
verify(repository, never()).channelPrefix("LEGACY_CHANNEL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyChannelsWithoutActiveVersion() {
|
||||
when(repository.activeVersionId()).thenReturn(null);
|
||||
|
||||
@@ -14,6 +14,6 @@ public interface FirstlegPricingClient {
|
||||
@RequestBody ChannelValidationRequest request);
|
||||
|
||||
record ChannelValidationRequest(long tenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType) {}
|
||||
String destinationType, boolean requireInboundCodePrefix) {}
|
||||
record ChannelValidationResponse(String channelCode, String inboundCodePrefix) {}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.order.dto.BatchOrderReviewResponse;
|
||||
import com.baiyue.order.dto.OrderDetailResponse;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.dto.InboundCodePolicyResponse;
|
||||
import com.baiyue.order.dto.TenantOrderCountResponse;
|
||||
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
@@ -47,6 +48,12 @@ public class PlatformFirstLegOrderController {
|
||||
@Resource
|
||||
private OrderApplicationService service;
|
||||
|
||||
@GetMapping("/inbound-code-policy")
|
||||
@PreAuthorize("hasAnyAuthority('platform:order:list', 'platform:order:create', 'platform:order:edit', 'platform:order:submit')")
|
||||
public ApiResponse<InboundCodePolicyResponse> inboundCodePolicy() {
|
||||
return ApiResponse.success(service.inboundCodePolicy());
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ORDER_CREATE", resourceType = "FIRST_LEG_ORDER", description = "平台代客创建草稿")
|
||||
@Operation(summary = "平台代客创建草稿", description = "在明确指定的目标租户内创建头程订单草稿,必须携带幂等键。")
|
||||
@ApiResponses({
|
||||
|
||||
@@ -121,10 +121,9 @@ public class OrderRepository {
|
||||
.eq(OrderEntity::getId, orderId));
|
||||
}
|
||||
|
||||
public boolean inboundCodeExists(long tenantId, String inboundCode, Long excludedOrderId) {
|
||||
public boolean inboundCodeExists(String inboundCode, Long excludedOrderId) {
|
||||
if (!StringUtils.hasText(inboundCode)) return false;
|
||||
return orderMapper.selectCount(new LambdaQueryWrapper<OrderEntity>()
|
||||
.eq(OrderEntity::getTenantId, tenantId)
|
||||
.eq(OrderEntity::getInboundCode, inboundCode)
|
||||
.ne(excludedOrderId != null, OrderEntity::getId, excludedOrderId)) > 0;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ package com.baiyue.order.service;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Validator;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -13,9 +16,19 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class MerchantBatchDraftItemService {
|
||||
@Resource private OrderApplicationService orders;
|
||||
@Resource private OrderRepository repository;
|
||||
@Resource private Validator validator;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public OrderEntity create(long tenantId, long userId, OrderRequests.SaveRequest draft, String idempotencyKey) {
|
||||
var violations = validator.validate(draft);
|
||||
if (!violations.isEmpty()) {
|
||||
String message = violations.stream()
|
||||
.sorted(java.util.Comparator.comparing(violation -> violation.getPropertyPath().toString()))
|
||||
.findFirst()
|
||||
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
|
||||
.orElse("草稿参数不合法");
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, message);
|
||||
}
|
||||
Long id = orders.createDraft(tenantId, userId, draft, idempotencyKey);
|
||||
return repository.findOwned(tenantId, id);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,11 @@ public class OrderApplicationService {
|
||||
order.setStatus(OrderStatus.DRAFT);
|
||||
apply(order, tenantId, request, snapshots);
|
||||
order.setCreatedBy(userId); order.setUpdatedBy(userId);
|
||||
repository.insertOrder(order);
|
||||
try {
|
||||
repository.insertOrder(order);
|
||||
} catch (DuplicateKeyException duplicate) {
|
||||
throw duplicateInboundCode(order, duplicate);
|
||||
}
|
||||
saveChildren(tenantId, userId, order.getId(), request);
|
||||
record(tenantId, order.getId(), null, OrderStatus.DRAFT, "CREATE", null, userId, null);
|
||||
return order.getId();
|
||||
@@ -118,7 +122,11 @@ public class OrderApplicationService {
|
||||
if (before == OrderStatus.SUBMITTED) {
|
||||
order.setStatus(OrderStatus.SUBMITTED);
|
||||
}
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
|
||||
try {
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
|
||||
} catch (DuplicateKeyException duplicate) {
|
||||
throw duplicateInboundCode(order, duplicate);
|
||||
}
|
||||
repository.replaceDraftChildren(tenantId, id);
|
||||
saveChildren(tenantId, userId, id, request);
|
||||
record(tenantId, id, before, order.getStatus(), "UPDATE", null, userId, null);
|
||||
@@ -473,7 +481,7 @@ public class OrderApplicationService {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (normalized != null && repository.inboundCodeExists(order.getTenantId(), normalized, order.getId())) {
|
||||
if (normalized != null && repository.inboundCodeExists(normalized, order.getId())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入仓码已存在,请核对旧系统数据");
|
||||
}
|
||||
order.setInboundCode(normalized);
|
||||
@@ -489,6 +497,12 @@ public class OrderApplicationService {
|
||||
}
|
||||
return pricingChannelGuard.requireAvailable(tenantId, receivingWarehouseId, channelCode, destinationType);
|
||||
}
|
||||
private RuntimeException duplicateInboundCode(OrderEntity order, DuplicateKeyException duplicate) {
|
||||
if (StringUtils.hasText(order.getInboundCode())) {
|
||||
return new BizException(ResultCode.CONFLICT, "入仓码已存在,请核对旧系统数据");
|
||||
}
|
||||
return duplicate;
|
||||
}
|
||||
private void requirePrintableInboundCode(OrderEntity order) {
|
||||
String inboundCode = order.getInboundCode();
|
||||
if (!StringUtils.hasText(inboundCode) || !inboundCode.matches("[A-Za-z0-9._:-]+")) {
|
||||
|
||||
@@ -16,10 +16,20 @@ public class OrderPricingChannelGuard {
|
||||
|
||||
public String requireAvailable(long tenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType) {
|
||||
return validate(tenantId, originWarehouseId, channelCode, destinationType, true);
|
||||
}
|
||||
|
||||
public void requireActive(long tenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType) {
|
||||
validate(tenantId, originWarehouseId, channelCode, destinationType, false);
|
||||
}
|
||||
|
||||
private String validate(long tenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType, boolean requireInboundCodePrefix) {
|
||||
try {
|
||||
ApiResponse<FirstlegPricingClient.ChannelValidationResponse> response = client.validateChannel(internalToken,
|
||||
new FirstlegPricingClient.ChannelValidationRequest(
|
||||
tenantId, originWarehouseId, channelCode, destinationType));
|
||||
tenantId, originWarehouseId, channelCode, destinationType, requireInboundCodePrefix));
|
||||
if (response == null) {
|
||||
throw unavailable();
|
||||
}
|
||||
@@ -29,8 +39,8 @@ public class OrderPricingChannelGuard {
|
||||
? code.message() : response.message();
|
||||
throw new BizException(code, message);
|
||||
}
|
||||
if (response.data() == null || response.data().inboundCodePrefix() == null
|
||||
|| response.data().inboundCodePrefix().isBlank()) {
|
||||
if (response.data() == null || requireInboundCodePrefix
|
||||
&& (response.data().inboundCodePrefix() == null || response.data().inboundCodePrefix().isBlank())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置入仓码前缀");
|
||||
}
|
||||
return response.data().inboundCodePrefix();
|
||||
|
||||
@@ -9,7 +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 '提交后生成并固定的入仓码;未提交草稿为空',
|
||||
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 '订单业务状态:DRAFT草稿、SUBMITTED已提交、APPROVED审核通过、ACCEPTED已接单、REJECTED审核驳回、PROCESSING履约中、COMPLETED已完成、CANCELLED已取消',
|
||||
@@ -54,13 +54,40 @@ 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),
|
||||
UNIQUE KEY uk_order_inbound_code (inbound_code),
|
||||
KEY idx_order_tenant_status_time (tenant_id, status, created_at),
|
||||
KEY idx_order_tenant_role_submitted (tenant_id, order_role, submitted_at),
|
||||
KEY idx_order_tenant_customer (tenant_id, customer_id),
|
||||
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
|
||||
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态';
|
||||
|
||||
-- 既有开发库必须先确认不存在跨客户重复码;若存在则在此处失败,不自动改写业务数据。
|
||||
SET @order_global_inbound_index_sql = IF(
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND index_name = 'uk_order_inbound_code'
|
||||
),
|
||||
'ALTER TABLE baiyue_order.ord_order ADD UNIQUE KEY uk_order_inbound_code (inbound_code)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE order_global_inbound_index_stmt FROM @order_global_inbound_index_sql;
|
||||
EXECUTE order_global_inbound_index_stmt;
|
||||
DEALLOCATE PREPARE order_global_inbound_index_stmt;
|
||||
|
||||
SET @order_tenant_inbound_index_drop_sql = IF(
|
||||
EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND index_name = 'uk_order_tenant_inbound_code'
|
||||
),
|
||||
'ALTER TABLE baiyue_order.ord_order DROP INDEX uk_order_tenant_inbound_code',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE order_tenant_inbound_index_drop_stmt FROM @order_tenant_inbound_index_drop_sql;
|
||||
EXECUTE order_tenant_inbound_index_drop_stmt;
|
||||
DEALLOCATE PREPARE order_tenant_inbound_index_drop_stmt;
|
||||
|
||||
-- Existing development databases may still have the former NOT NULL contract.
|
||||
SET @order_inbound_code_nullable_sql = IF(
|
||||
EXISTS(
|
||||
@@ -68,7 +95,7 @@ SET @order_inbound_code_nullable_sql = IF(
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND column_name = 'inbound_code' AND is_nullable = 'NO'
|
||||
),
|
||||
'ALTER TABLE baiyue_order.ord_order MODIFY COLUMN inbound_code VARCHAR(40) NULL COMMENT ''提交后生成并固定的入仓码;未提交草稿为空''',
|
||||
'ALTER TABLE baiyue_order.ord_order MODIFY COLUMN inbound_code VARCHAR(40) NULL COMMENT ''全平台唯一入仓码;来源由全局模式决定,提交后固定''',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE order_inbound_code_nullable_stmt FROM @order_inbound_code_nullable_sql;
|
||||
|
||||
@@ -6,6 +6,21 @@ info:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/api/platform/v1/orders/first-leg/inbound-code-policy:
|
||||
get:
|
||||
summary: 平台端查询全系统入仓码模式
|
||||
description: 与客户下单端返回同一份全局策略,不按客户区分。
|
||||
responses:
|
||||
'200':
|
||||
description: 查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string, example: '200'}
|
||||
data: {$ref: '#/components/schemas/InboundCodePolicy'}
|
||||
'403': {description: 需要平台订单列表、创建、编辑或提交权限之一}
|
||||
/api/platform/v1/orders:
|
||||
get:
|
||||
summary: 平台统一订单列表
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.order.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class InboundCodeModeProviderTest {
|
||||
@Test
|
||||
void parsesBothSupportedModesCaseInsensitively() {
|
||||
assertThat(new InboundCodeModeProvider("auto").mode())
|
||||
.isEqualTo(InboundCodeModeProvider.Mode.AUTO);
|
||||
assertThat(new InboundCodeModeProvider(" legacy_manual ").legacyManual()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingAndUnknownModesInsteadOfFallingBackToAutomaticGeneration() {
|
||||
assertThatThrownBy(() -> new InboundCodeModeProvider(" "))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("baiyue.order.inbound-code-mode 必须显式配置");
|
||||
assertThatThrownBy(() -> new InboundCodeModeProvider("LEGACY-MANUAL"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("baiyue.order.inbound-code-mode 仅支持 AUTO 或 LEGACY_MANUAL");
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,12 @@ class InboundCodePolicyContractTest {
|
||||
.isEqualTo("hasAnyAuthority('merchant:order:list', 'merchant:order:create', 'merchant:order:edit')");
|
||||
assertThat(method.getGenericReturnType().getTypeName())
|
||||
.contains(InboundCodePolicyResponse.class.getName());
|
||||
|
||||
var platformMethod = PlatformFirstLegOrderController.class.getDeclaredMethod("inboundCodePolicy");
|
||||
assertThat(platformMethod.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAnyAuthority('platform:order:list', 'platform:order:create', 'platform:order:edit', 'platform:order:submit')");
|
||||
assertThat(platformMethod.getGenericReturnType().getTypeName())
|
||||
.contains(InboundCodePolicyResponse.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -29,6 +35,7 @@ class InboundCodePolicyContractTest {
|
||||
|
||||
assertThat(openapi)
|
||||
.contains("/api/merchant/v1/orders/first-leg/inbound-code-policy:")
|
||||
.contains("/api/platform/v1/orders/first-leg/inbound-code-policy:")
|
||||
.contains("enum: [AUTO, LEGACY_MANUAL]")
|
||||
.contains("manualEntryRequired: {type: boolean}")
|
||||
.contains("该策略不按客户区分")
|
||||
|
||||
@@ -54,6 +54,32 @@ class OrderRequestsValidationTest {
|
||||
assertThat(validator.validate(draft)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesTheManuallyEnteredInboundCodeForSingleAndBatchCallers() {
|
||||
var source = request(null, null, "TAX_INCLUDED");
|
||||
var invalid = withInboundCode(source, "INVALID#CODE");
|
||||
var tooLong = withInboundCode(source, "A".repeat(41));
|
||||
var valid = withInboundCode(source, "NYEXX2608140001");
|
||||
|
||||
assertThat(validator.validate(invalid))
|
||||
.anyMatch(violation -> violation.getPropertyPath().toString().equals("inboundCode"));
|
||||
assertThat(validator.validate(tooLong))
|
||||
.anyMatch(violation -> violation.getPropertyPath().toString().equals("inboundCode"));
|
||||
assertThat(validator.validate(valid)).isEmpty();
|
||||
}
|
||||
|
||||
private OrderRequests.SaveRequest withInboundCode(OrderRequests.SaveRequest source, String inboundCode) {
|
||||
return new OrderRequests.SaveRequest(
|
||||
source.customerId(), source.customerName(), source.transportMode(), source.tradeTerm(),
|
||||
source.destinationCountryCode(), source.destinationWarehouseId(), source.destinationWarehouseName(),
|
||||
source.expectedShipDate(), source.customerRemark(), source.sender(), source.receiver(), source.cargoes(),
|
||||
source.pickupMethod(), source.requestedPickupAt(), source.receivingWarehouseId(),
|
||||
source.receivingWarehouseName(), source.channelCode(), source.channelName(), source.carrierId(),
|
||||
source.carrierName(), source.taxMode(), source.pricingTaxMode(), source.containerMode(),
|
||||
source.customsInspection(), source.destinationType(), source.amazonWarehouseCode(), source.shipmentId(),
|
||||
source.referenceId(), inboundCode);
|
||||
}
|
||||
|
||||
private OrderRequests.SaveRequest request(Long carrierId, String carrierName, String pricingTaxMode) {
|
||||
var cargo = new OrderRequests.Cargo(
|
||||
"货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE,
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.order.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InboundCodeGlobalUniquenessContractTest {
|
||||
@Test
|
||||
void orderSchemaUsesOnePlatformWideInboundCodeIndex() throws Exception {
|
||||
String sql = new ClassPathResource("db/order.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("UNIQUE KEY uk_order_inbound_code (inbound_code)")
|
||||
.doesNotContain("UNIQUE KEY uk_order_tenant_inbound_code (tenant_id, inbound_code)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Path;
|
||||
import jakarta.validation.Validator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MerchantBatchDraftItemServiceTest {
|
||||
@Test
|
||||
void rejectsAnInvalidBatchRowBeforeCreatingTheDraft() {
|
||||
OrderApplicationService orders = mock(OrderApplicationService.class);
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
Validator validator = mock(Validator.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ConstraintViolation<OrderRequests.SaveRequest> violation = mock(ConstraintViolation.class);
|
||||
Path path = mock(Path.class);
|
||||
OrderRequests.SaveRequest draft = mock(OrderRequests.SaveRequest.class);
|
||||
when(path.toString()).thenReturn("inboundCode");
|
||||
when(violation.getPropertyPath()).thenReturn(path);
|
||||
when(violation.getMessage()).thenReturn("格式不正确");
|
||||
when(validator.validate(draft)).thenReturn(Set.of(violation));
|
||||
MerchantBatchDraftItemService service = new MerchantBatchDraftItemService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "validator", validator);
|
||||
|
||||
assertThatThrownBy(() -> service.create(1L, 2L, draft, "batch-key"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("inboundCode: 格式不正确");
|
||||
|
||||
verify(orders, never()).createDraft(1L, 2L, draft, "batch-key");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.order.service;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.client.WarehouseClient;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.entity.OrderCargoEntity;
|
||||
@@ -37,6 +38,7 @@ class OrderApplicationServiceStateTest {
|
||||
@MockBean private OrderMasterDataGuard contextMasterData;
|
||||
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
||||
@MockBean private ProductImageGuard contextProductImageGuard;
|
||||
@MockBean private InboundCodeModeProvider contextInboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService springService;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@@ -260,7 +262,7 @@ class OrderApplicationServiceStateTest {
|
||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
|
||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class), masterData);
|
||||
ReflectionTestUtils.setField(service, "inboundCodeMode", "LEGACY_MANUAL");
|
||||
useInboundCodeMode(service, InboundCodeModeProvider.Mode.LEGACY_MANUAL);
|
||||
|
||||
service.createDraft(10001L, 2L, request, "legacy-code-draft");
|
||||
|
||||
@@ -268,6 +270,38 @@ class OrderApplicationServiceStateTest {
|
||||
verify(repository, never()).nextInboundCodeSequence(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyManualModeRejectsAnInboundCodeAlreadyUsedByAnotherCustomer() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
OrderRequests.SaveRequest request = withInboundCode(request(), "NYEXX2608140001");
|
||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||
when(repository.inboundCodeExists(eq("NYEXX2608140001"), anyLong())).thenReturn(true);
|
||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class), masterData);
|
||||
useInboundCodeMode(service, InboundCodeModeProvider.Mode.LEGACY_MANUAL);
|
||||
|
||||
assertThatThrownBy(() -> service.createDraft(10001L, 2L, request, "duplicate-global-code"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("入仓码已存在,请核对旧系统数据");
|
||||
|
||||
verify(repository, never()).insertOrder(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void automaticModeRejectsClientSuppliedInboundCodes() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
OrderRequests.SaveRequest request = withInboundCode(request(), "NYEXX2608140001");
|
||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class), masterData);
|
||||
|
||||
assertThatThrownBy(() -> service.createDraft(10001L, 2L, request, "auto-manual-code"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("当前系统由新系统自动生成入仓码,请勿人工填写");
|
||||
|
||||
verify(repository, never()).insertOrder(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyManualModeRejectsSubmissionWithoutAnExternalInboundCode() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
@@ -277,7 +311,7 @@ class OrderApplicationServiceStateTest {
|
||||
when(repository.findParties(20002L, 1L)).thenReturn(List.of());
|
||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class),
|
||||
mock(OrderMasterDataGuard.class));
|
||||
ReflectionTestUtils.setField(service, "inboundCodeMode", "LEGACY_MANUAL");
|
||||
useInboundCodeMode(service, InboundCodeModeProvider.Mode.LEGACY_MANUAL);
|
||||
|
||||
assertThatThrownBy(() -> service.transit(
|
||||
20002L, 2L, "u", 1L, OrderStatus.SUBMITTED, null))
|
||||
@@ -548,10 +582,18 @@ class OrderApplicationServiceStateTest {
|
||||
when(repository.nextInboundCodeSequence(any(LocalDate.class))).thenReturn(1L);
|
||||
ReflectionTestUtils.setField(service, "pricingChannelGuard", pricing);
|
||||
ReflectionTestUtils.setField(service, "productImageGuard", mock(ProductImageGuard.class));
|
||||
useInboundCodeMode(service, InboundCodeModeProvider.Mode.AUTO);
|
||||
ReflectionTestUtils.setField(service, "json", new ObjectMapper().findAndRegisterModules());
|
||||
return service;
|
||||
}
|
||||
|
||||
private void useInboundCodeMode(OrderApplicationService service, InboundCodeModeProvider.Mode mode) {
|
||||
InboundCodeModeProvider provider = mock(InboundCodeModeProvider.class);
|
||||
when(provider.mode()).thenReturn(mode);
|
||||
when(provider.legacyManual()).thenReturn(mode == InboundCodeModeProvider.Mode.LEGACY_MANUAL);
|
||||
ReflectionTestUtils.setField(service, "inboundCodeModeProvider", provider);
|
||||
}
|
||||
|
||||
private OrderRequests.SaveRequest request() {
|
||||
var party = new OrderRequests.Party(
|
||||
"公司", "联系人", "13800138000", null, "CN", null, "深圳", null, "地址", null);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.order.service;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import com.baiyue.order.entity.OrderIdempotencyRecordEntity;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -41,6 +42,7 @@ class OrderIdempotencyServiceTest {
|
||||
@MockBean private OrderMasterDataGuard contextMasterData;
|
||||
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
||||
@MockBean private ProductImageGuard contextProductImageGuard;
|
||||
@MockBean private InboundCodeModeProvider contextInboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService springService;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
@@ -31,7 +31,7 @@ class OrderPricingChannelGuardTest {
|
||||
|
||||
@Test
|
||||
void forwardsCustomerOriginAndChannelToFirstleg() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA");
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA", true);
|
||||
when(client.validateChannel("token", request)).thenReturn(ApiResponse.success(
|
||||
new FirstlegPricingClient.ChannelValidationResponse("MATSON_TRUCK", "ZBYHK")));
|
||||
|
||||
@@ -40,9 +40,21 @@ class OrderPricingChannelGuardTest {
|
||||
verify(client).validateChannel("token", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyManualModeOnlyRequiresTheChannelToRemainActive() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(
|
||||
20002L, 8L, "LEGACY_CHANNEL", "FBA", false);
|
||||
when(client.validateChannel("token", request)).thenReturn(ApiResponse.success(
|
||||
new FirstlegPricingClient.ChannelValidationResponse("LEGACY_CHANNEL", null)));
|
||||
|
||||
guard.requireActive(20002L, 8L, "LEGACY_CHANNEL", "FBA");
|
||||
|
||||
verify(client).validateChannel("token", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removedOrForgedChannelKeepsFirstlegBusinessError() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "FAKE", "FBA");
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "FAKE", "FBA", true);
|
||||
when(client.validateChannel("token", request)).thenReturn(
|
||||
ApiResponse.failure(ResultCode.CONFLICT, "所选渠道已下线,请重新选择", null, null));
|
||||
|
||||
@@ -55,7 +67,7 @@ class OrderPricingChannelGuardTest {
|
||||
|
||||
@Test
|
||||
void nullResponseMapsToServiceUnavailable() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA");
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA", true);
|
||||
when(client.validateChannel("token", request)).thenReturn(null);
|
||||
|
||||
assertUnavailable(() -> guard.requireAvailable(20002L, 8L, "MATSON_TRUCK", "FBA"));
|
||||
@@ -63,7 +75,7 @@ class OrderPricingChannelGuardTest {
|
||||
|
||||
@Test
|
||||
void downstreamHttpFailureMapsToServiceUnavailable() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA");
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA", true);
|
||||
when(client.validateChannel("token", request)).thenThrow(feign(403));
|
||||
|
||||
assertUnavailable(() -> guard.requireAvailable(20002L, 8L, "MATSON_TRUCK", "FBA"));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -20,6 +21,7 @@ class PlatformOrderPageServiceTest {
|
||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||
@MockBean private ProductImageGuard productImageGuard;
|
||||
@MockBean private InboundCodeModeProvider inboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService service;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
@@ -23,6 +24,7 @@ class TenantOrderCountServiceTest {
|
||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||
@MockBean private ProductImageGuard productImageGuard;
|
||||
@MockBean private InboundCodeModeProvider inboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService service;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -27,6 +28,7 @@ class TenantOrderStatisticsServiceTest {
|
||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||
@MockBean private ProductImageGuard productImageGuard;
|
||||
@MockBean private InboundCodeModeProvider inboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService service;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
Reference in New Issue
Block a user