优化
This commit is contained in:
@@ -515,7 +515,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
|||||||
id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)',
|
id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)',
|
||||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||||
order_no VARCHAR(32) NOT NULL COMMENT '客户范围内唯一订单号',
|
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_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID',
|
||||||
customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照',
|
customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照',
|
||||||
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态:DRAFT草稿、SUBMITTED已提交、APPROVED审核通过、ACCEPTED已接单、REJECTED审核驳回、PROCESSING履约中、COMPLETED已完成、CANCELLED已取消',
|
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 '乐观锁版本号',
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uk_order_tenant_no (tenant_id, order_no),
|
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_status_time (tenant_id, status, created_at),
|
||||||
KEY idx_order_tenant_customer (tenant_id, customer_id),
|
KEY idx_order_tenant_customer (tenant_id, customer_id),
|
||||||
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_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'
|
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||||
AND column_name = 'inbound_code' AND is_nullable = 'NO'
|
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'
|
'SELECT 1'
|
||||||
);
|
);
|
||||||
PREPARE order_inbound_code_nullable_stmt FROM @order_inbound_code_nullable_sql;
|
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'
|
WHERE table_schema = DATABASE() AND table_name = 'ord_order' AND column_name = 'inbound_code'
|
||||||
);
|
);
|
||||||
SET @inbound_code_sql := IF(@inbound_code_exists = 0,
|
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');
|
'SELECT 1');
|
||||||
PREPARE inbound_code_statement FROM @inbound_code_sql;
|
PREPARE inbound_code_statement FROM @inbound_code_sql;
|
||||||
EXECUTE inbound_code_statement;
|
EXECUTE inbound_code_statement;
|
||||||
@@ -2436,15 +2436,27 @@ DEALLOCATE PREPARE inbound_code_statement;
|
|||||||
SET @inbound_code_index_exists := (
|
SET @inbound_code_index_exists := (
|
||||||
SELECT COUNT(*) FROM information_schema.statistics
|
SELECT COUNT(*) FROM information_schema.statistics
|
||||||
WHERE table_schema = DATABASE() AND table_name = 'ord_order'
|
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,
|
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');
|
'SELECT 1');
|
||||||
PREPARE inbound_code_index_statement FROM @inbound_code_index_sql;
|
PREPARE inbound_code_index_statement FROM @inbound_code_index_sql;
|
||||||
EXECUTE inbound_code_index_statement;
|
EXECUTE inbound_code_index_statement;
|
||||||
DEALLOCATE PREPARE 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
|
-- 来源: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',
|
task_id BIGINT UNSIGNED NULL COMMENT '头程履约任务ID',
|
||||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
||||||
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
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待分柜',
|
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 '订单状态快照',
|
order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照',
|
||||||
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
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 '逻辑删除标记',
|
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
PRIMARY KEY (id),
|
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),
|
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_process_time (tenant_id, process_status, created_at, id),
|
||||||
KEY idx_inbound_measure_time (tenant_id, measure_review_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)
|
CONSTRAINT fk_inbound_task FOREIGN KEY (task_id) REFERENCES fl_fulfillment_task (id)
|
||||||
) ENGINE=InnoDB COMMENT='二期头程入库单聚合根和列表查询快照';
|
) 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
|
UPDATE baiyue_order.ord_order o
|
||||||
JOIN baiyue_firstleg.fl_inbound_order i
|
JOIN baiyue_firstleg.fl_inbound_order i
|
||||||
ON i.tenant_id = o.tenant_id AND i.order_id = o.id AND i.deleted = 0
|
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', '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', 'hold_flag', '当前是否扣货');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'id', '入库单ID(雪花ID)');
|
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', 'inbound_role', '入仓单层级:MASTER主单、CHILD子单');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'inbounded_at', '入库完成时间');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'inbounded_at', '入库完成时间');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'master_order_id', '仅用于约束有效主单订单唯一性');
|
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_company', '履约快递公司名称快照');
|
||||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'express_tracking_no', '履约快递单号');
|
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', '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_no', '客户范围内唯一订单号');
|
||||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'order_role', '订单层级:MASTER主订单、CHILD子订单');
|
CALL queue_column_comment('baiyue_order', 'ord_order', 'order_role', '订单层级:MASTER主订单、CHILD子订单');
|
||||||
CALL queue_column_comment('baiyue_order', 'ord_order', 'parent_order_id', '直属父订单ID;主订单为空;禁止子订单嵌套');
|
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) {
|
@Valid @RequestBody PriceBookChannelValidation.Request request) {
|
||||||
requireToken(token);
|
requireToken(token);
|
||||||
return ApiResponse.success(service.requireAvailableChannel(request.tenantId(), request.originWarehouseId(),
|
return ApiResponse.success(service.requireAvailableChannel(request.tenantId(), request.originWarehouseId(),
|
||||||
request.channelCode(), request.destinationType()));
|
request.channelCode(), request.destinationType(), request.requireInboundCodePrefix()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requireToken(String token) {
|
private void requireToken(String token) {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ public final class PriceBookChannelValidation {
|
|||||||
@Min(1) long tenantId,
|
@Min(1) long tenantId,
|
||||||
@Min(1) long originWarehouseId,
|
@Min(1) long originWarehouseId,
|
||||||
@NotBlank @Size(max = 64) String channelCode,
|
@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) {}
|
public record Response(String channelCode, String inboundCodePrefix) {}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class MerchantPriceBookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Response requireAvailableChannel(long merchantTenantId, long originWarehouseId, String channelCode,
|
public Response requireAvailableChannel(long merchantTenantId, long originWarehouseId, String channelCode,
|
||||||
String destinationType) {
|
String destinationType, boolean requireInboundCodePrefix) {
|
||||||
requireMerchant(merchantTenantId);
|
requireMerchant(merchantTenantId);
|
||||||
if (originWarehouseId <= 0) throw validation("请选择有效起运仓");
|
if (originWarehouseId <= 0) throw validation("请选择有效起运仓");
|
||||||
Long versionId = repository.activeVersionId();
|
Long versionId = repository.activeVersionId();
|
||||||
@@ -57,6 +57,9 @@ public class MerchantPriceBookService {
|
|||||||
if (versionId == null || !repository.channelAvailable(versionId, originWarehouseId, normalized)) {
|
if (versionId == null || !repository.channelAvailable(versionId, originWarehouseId, normalized)) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "所选渠道已下线,请重新选择");
|
throw new BizException(ResultCode.CONFLICT, "所选渠道已下线,请重新选择");
|
||||||
}
|
}
|
||||||
|
if (!requireInboundCodePrefix) {
|
||||||
|
return new Response(normalized, null);
|
||||||
|
}
|
||||||
var prefix = repository.channelPrefix(normalized);
|
var prefix = repository.channelPrefix(normalized);
|
||||||
if (prefix == null || !StringUtils.hasText(prefix.inboundCodePrefix())) {
|
if (prefix == null || !StringUtils.hasText(prefix.inboundCodePrefix())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置进仓编码");
|
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置进仓编码");
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
|||||||
task_id BIGINT UNSIGNED NULL COMMENT '头程履约任务ID',
|
task_id BIGINT UNSIGNED NULL COMMENT '头程履约任务ID',
|
||||||
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
|
||||||
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
|
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待分柜',
|
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 '订单状态快照',
|
order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照',
|
||||||
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
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 '逻辑删除标记',
|
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
PRIMARY KEY (id),
|
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),
|
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_process_time (tenant_id, process_status, created_at, id),
|
||||||
KEY idx_inbound_measure_time (tenant_id, measure_review_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)
|
CONSTRAINT fk_inbound_task FOREIGN KEY (task_id) REFERENCES fl_fulfillment_task (id)
|
||||||
) ENGINE=InnoDB COMMENT='二期头程入库单聚合根和列表查询快照';
|
) 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 (
|
CREATE TABLE IF NOT EXISTS fl_inbound_pickup_snapshot (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '提货信息快照ID',
|
id BIGINT UNSIGNED NOT NULL COMMENT '提货信息快照ID',
|
||||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ paths:
|
|||||||
'200': {description: 幂等补偿完成}
|
'200': {description: 幂等补偿完成}
|
||||||
/internal/firstleg/v1/pricing/channels/validate:
|
/internal/firstleg/v1/pricing/channels/validate:
|
||||||
post:
|
post:
|
||||||
summary: 校验订单渠道并返回进仓编码前缀
|
summary: 校验订单渠道并按需返回进仓编码前缀
|
||||||
description: 仅供订单服务调用;前缀按渠道和目的仓类型选择,同一渠道的包税与不包税订单共用编码前缀。
|
description: 仅供订单服务调用;AUTO 模式要求并返回编码前缀,LEGACY_MANUAL 模式仅校验渠道可用性。
|
||||||
security:
|
security:
|
||||||
- internalServiceToken: []
|
- internalServiceToken: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -61,14 +61,15 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
required: [tenantId, originWarehouseId, channelCode, destinationType]
|
required: [tenantId, originWarehouseId, channelCode, destinationType, requireInboundCodePrefix]
|
||||||
properties:
|
properties:
|
||||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
originWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
originWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
channelCode: {type: string, minLength: 1, maxLength: 64}
|
channelCode: {type: string, minLength: 1, maxLength: 64}
|
||||||
destinationType: {type: string, enum: [FBA, OVERSEAS, PRIVATE]}
|
destinationType: {type: string, enum: [FBA, OVERSEAS, PRIVATE]}
|
||||||
|
requireInboundCodePrefix: {type: boolean}
|
||||||
responses:
|
responses:
|
||||||
'200': {description: 成功时 data 返回 channelCode 和 inboundCodePrefix;未配置对应进仓编码时返回业务冲突}
|
'200': {description: 成功时 data 返回 channelCode;requireInboundCodePrefix=true 时同时返回 inboundCodePrefix}
|
||||||
/api/platform/v1/first-leg/customer-dimension-config:
|
/api/platform/v1/first-leg/customer-dimension-config:
|
||||||
get:
|
get:
|
||||||
summary: 查询客户固定四箱规尺寸配置
|
summary: 查询客户固定四箱规尺寸配置
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class PriceBookContractTest {
|
|||||||
.getContentAsString(StandardCharsets.UTF_8);
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
assertThat(firstlegOpenapi)
|
assertThat(firstlegOpenapi)
|
||||||
.contains("/internal/firstleg/v1/pricing/channels/validate:")
|
.contains("/internal/firstleg/v1/pricing/channels/validate:")
|
||||||
.contains("required: [tenantId, originWarehouseId, channelCode, destinationType]")
|
.contains("required: [tenantId, originWarehouseId, channelCode, destinationType, requireInboundCodePrefix]")
|
||||||
.contains("同一渠道的包税与不包税订单共用编码前缀");
|
.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"))
|
when(repository.channelPrefix("MATSON_CLX_TRUCK"))
|
||||||
.thenReturn(new ChannelPrefixRow("MATSON_CLX_TRUCK", "ZBYHK", "ZBYHWK"));
|
.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");
|
.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");
|
.inboundCodePrefix()).isEqualTo("ZBYHWK");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,10 +66,22 @@ class MerchantPriceBookServiceTest {
|
|||||||
when(repository.channelPrefix("OA_HOUSTON_TRUCK"))
|
when(repository.channelPrefix("OA_HOUSTON_TRUCK"))
|
||||||
.thenReturn(new ChannelPrefixRow("OA_HOUSTON_TRUCK", "HPHK", null));
|
.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");
|
.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
|
@Test
|
||||||
void shouldReturnEmptyChannelsWithoutActiveVersion() {
|
void shouldReturnEmptyChannelsWithoutActiveVersion() {
|
||||||
when(repository.activeVersionId()).thenReturn(null);
|
when(repository.activeVersionId()).thenReturn(null);
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ public interface FirstlegPricingClient {
|
|||||||
@RequestBody ChannelValidationRequest request);
|
@RequestBody ChannelValidationRequest request);
|
||||||
|
|
||||||
record ChannelValidationRequest(long tenantId, long originWarehouseId, String channelCode,
|
record ChannelValidationRequest(long tenantId, long originWarehouseId, String channelCode,
|
||||||
String destinationType) {}
|
String destinationType, boolean requireInboundCodePrefix) {}
|
||||||
record ChannelValidationResponse(String channelCode, String inboundCodePrefix) {}
|
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.BatchOrderReviewResponse;
|
||||||
import com.baiyue.order.dto.OrderDetailResponse;
|
import com.baiyue.order.dto.OrderDetailResponse;
|
||||||
import com.baiyue.order.dto.OrderRequests;
|
import com.baiyue.order.dto.OrderRequests;
|
||||||
|
import com.baiyue.order.dto.InboundCodePolicyResponse;
|
||||||
import com.baiyue.order.dto.TenantOrderCountResponse;
|
import com.baiyue.order.dto.TenantOrderCountResponse;
|
||||||
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
|
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
|
||||||
import com.baiyue.order.entity.OrderEntity;
|
import com.baiyue.order.entity.OrderEntity;
|
||||||
@@ -47,6 +48,12 @@ public class PlatformFirstLegOrderController {
|
|||||||
@Resource
|
@Resource
|
||||||
private OrderApplicationService service;
|
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 = "平台代客创建草稿")
|
@BusinessAudit(event = "ORDER_CREATE", resourceType = "FIRST_LEG_ORDER", description = "平台代客创建草稿")
|
||||||
@Operation(summary = "平台代客创建草稿", description = "在明确指定的目标租户内创建头程订单草稿,必须携带幂等键。")
|
@Operation(summary = "平台代客创建草稿", description = "在明确指定的目标租户内创建头程订单草稿,必须携带幂等键。")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
|
|||||||
@@ -121,10 +121,9 @@ public class OrderRepository {
|
|||||||
.eq(OrderEntity::getId, orderId));
|
.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;
|
if (!StringUtils.hasText(inboundCode)) return false;
|
||||||
return orderMapper.selectCount(new LambdaQueryWrapper<OrderEntity>()
|
return orderMapper.selectCount(new LambdaQueryWrapper<OrderEntity>()
|
||||||
.eq(OrderEntity::getTenantId, tenantId)
|
|
||||||
.eq(OrderEntity::getInboundCode, inboundCode)
|
.eq(OrderEntity::getInboundCode, inboundCode)
|
||||||
.ne(excludedOrderId != null, OrderEntity::getId, excludedOrderId)) > 0;
|
.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.dto.OrderRequests;
|
||||||
import com.baiyue.order.entity.OrderEntity;
|
import com.baiyue.order.entity.OrderEntity;
|
||||||
import com.baiyue.order.repository.OrderRepository;
|
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.annotation.Resource;
|
||||||
|
import jakarta.validation.Validator;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -13,9 +16,19 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
public class MerchantBatchDraftItemService {
|
public class MerchantBatchDraftItemService {
|
||||||
@Resource private OrderApplicationService orders;
|
@Resource private OrderApplicationService orders;
|
||||||
@Resource private OrderRepository repository;
|
@Resource private OrderRepository repository;
|
||||||
|
@Resource private Validator validator;
|
||||||
|
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public OrderEntity create(long tenantId, long userId, OrderRequests.SaveRequest draft, String idempotencyKey) {
|
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);
|
Long id = orders.createDraft(tenantId, userId, draft, idempotencyKey);
|
||||||
return repository.findOwned(tenantId, id);
|
return repository.findOwned(tenantId, id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,11 @@ public class OrderApplicationService {
|
|||||||
order.setStatus(OrderStatus.DRAFT);
|
order.setStatus(OrderStatus.DRAFT);
|
||||||
apply(order, tenantId, request, snapshots);
|
apply(order, tenantId, request, snapshots);
|
||||||
order.setCreatedBy(userId); order.setUpdatedBy(userId);
|
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);
|
saveChildren(tenantId, userId, order.getId(), request);
|
||||||
record(tenantId, order.getId(), null, OrderStatus.DRAFT, "CREATE", null, userId, null);
|
record(tenantId, order.getId(), null, OrderStatus.DRAFT, "CREATE", null, userId, null);
|
||||||
return order.getId();
|
return order.getId();
|
||||||
@@ -118,7 +122,11 @@ public class OrderApplicationService {
|
|||||||
if (before == OrderStatus.SUBMITTED) {
|
if (before == OrderStatus.SUBMITTED) {
|
||||||
order.setStatus(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);
|
repository.replaceDraftChildren(tenantId, id);
|
||||||
saveChildren(tenantId, userId, id, request);
|
saveChildren(tenantId, userId, id, request);
|
||||||
record(tenantId, id, before, order.getStatus(), "UPDATE", null, userId, null);
|
record(tenantId, id, before, order.getStatus(), "UPDATE", null, userId, null);
|
||||||
@@ -473,7 +481,7 @@ public class OrderApplicationService {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (normalized != null && repository.inboundCodeExists(order.getTenantId(), normalized, order.getId())) {
|
if (normalized != null && repository.inboundCodeExists(normalized, order.getId())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "入仓码已存在,请核对旧系统数据");
|
throw new BizException(ResultCode.CONFLICT, "入仓码已存在,请核对旧系统数据");
|
||||||
}
|
}
|
||||||
order.setInboundCode(normalized);
|
order.setInboundCode(normalized);
|
||||||
@@ -489,6 +497,12 @@ public class OrderApplicationService {
|
|||||||
}
|
}
|
||||||
return pricingChannelGuard.requireAvailable(tenantId, receivingWarehouseId, channelCode, destinationType);
|
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) {
|
private void requirePrintableInboundCode(OrderEntity order) {
|
||||||
String inboundCode = order.getInboundCode();
|
String inboundCode = order.getInboundCode();
|
||||||
if (!StringUtils.hasText(inboundCode) || !inboundCode.matches("[A-Za-z0-9._:-]+")) {
|
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,
|
public String requireAvailable(long tenantId, long originWarehouseId, String channelCode,
|
||||||
String destinationType) {
|
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 {
|
try {
|
||||||
ApiResponse<FirstlegPricingClient.ChannelValidationResponse> response = client.validateChannel(internalToken,
|
ApiResponse<FirstlegPricingClient.ChannelValidationResponse> response = client.validateChannel(internalToken,
|
||||||
new FirstlegPricingClient.ChannelValidationRequest(
|
new FirstlegPricingClient.ChannelValidationRequest(
|
||||||
tenantId, originWarehouseId, channelCode, destinationType));
|
tenantId, originWarehouseId, channelCode, destinationType, requireInboundCodePrefix));
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
throw unavailable();
|
throw unavailable();
|
||||||
}
|
}
|
||||||
@@ -29,8 +39,8 @@ public class OrderPricingChannelGuard {
|
|||||||
? code.message() : response.message();
|
? code.message() : response.message();
|
||||||
throw new BizException(code, message);
|
throw new BizException(code, message);
|
||||||
}
|
}
|
||||||
if (response.data() == null || response.data().inboundCodePrefix() == null
|
if (response.data() == null || requireInboundCodePrefix
|
||||||
|| response.data().inboundCodePrefix().isBlank()) {
|
&& (response.data().inboundCodePrefix() == null || response.data().inboundCodePrefix().isBlank())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置入仓码前缀");
|
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置入仓码前缀");
|
||||||
}
|
}
|
||||||
return response.data().inboundCodePrefix();
|
return response.data().inboundCodePrefix();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
|||||||
id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)',
|
id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)',
|
||||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||||
order_no VARCHAR(32) NOT NULL COMMENT '客户范围内唯一订单号',
|
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_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID',
|
||||||
customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照',
|
customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照',
|
||||||
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态:DRAFT草稿、SUBMITTED已提交、APPROVED审核通过、ACCEPTED已接单、REJECTED审核驳回、PROCESSING履约中、COMPLETED已完成、CANCELLED已取消',
|
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 '乐观锁版本号',
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uk_order_tenant_no (tenant_id, order_no),
|
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_status_time (tenant_id, status, created_at),
|
||||||
KEY idx_order_tenant_role_submitted (tenant_id, order_role, submitted_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_customer (tenant_id, customer_id),
|
||||||
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
|
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
|
||||||
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态';
|
) 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.
|
-- Existing development databases may still have the former NOT NULL contract.
|
||||||
SET @order_inbound_code_nullable_sql = IF(
|
SET @order_inbound_code_nullable_sql = IF(
|
||||||
EXISTS(
|
EXISTS(
|
||||||
@@ -68,7 +95,7 @@ SET @order_inbound_code_nullable_sql = IF(
|
|||||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||||
AND column_name = 'inbound_code' AND is_nullable = 'NO'
|
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'
|
'SELECT 1'
|
||||||
);
|
);
|
||||||
PREPARE order_inbound_code_nullable_stmt FROM @order_inbound_code_nullable_sql;
|
PREPARE order_inbound_code_nullable_stmt FROM @order_inbound_code_nullable_sql;
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ info:
|
|||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
paths:
|
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:
|
/api/platform/v1/orders:
|
||||||
get:
|
get:
|
||||||
summary: 平台统一订单列表
|
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')");
|
.isEqualTo("hasAnyAuthority('merchant:order:list', 'merchant:order:create', 'merchant:order:edit')");
|
||||||
assertThat(method.getGenericReturnType().getTypeName())
|
assertThat(method.getGenericReturnType().getTypeName())
|
||||||
.contains(InboundCodePolicyResponse.class.getName());
|
.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
|
@Test
|
||||||
@@ -29,6 +35,7 @@ class InboundCodePolicyContractTest {
|
|||||||
|
|
||||||
assertThat(openapi)
|
assertThat(openapi)
|
||||||
.contains("/api/merchant/v1/orders/first-leg/inbound-code-policy:")
|
.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("enum: [AUTO, LEGACY_MANUAL]")
|
||||||
.contains("manualEntryRequired: {type: boolean}")
|
.contains("manualEntryRequired: {type: boolean}")
|
||||||
.contains("该策略不按客户区分")
|
.contains("该策略不按客户区分")
|
||||||
|
|||||||
@@ -54,6 +54,32 @@ class OrderRequestsValidationTest {
|
|||||||
assertThat(validator.validate(draft)).isEmpty();
|
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) {
|
private OrderRequests.SaveRequest request(Long carrierId, String carrierName, String pricingTaxMode) {
|
||||||
var cargo = new OrderRequests.Cargo(
|
var cargo = new OrderRequests.Cargo(
|
||||||
"货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE,
|
"货物", 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.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.order.client.WarehouseClient;
|
import com.baiyue.order.client.WarehouseClient;
|
||||||
|
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||||
import com.baiyue.order.dto.OrderRequests;
|
import com.baiyue.order.dto.OrderRequests;
|
||||||
import com.baiyue.order.entity.OrderEntity;
|
import com.baiyue.order.entity.OrderEntity;
|
||||||
import com.baiyue.order.entity.OrderCargoEntity;
|
import com.baiyue.order.entity.OrderCargoEntity;
|
||||||
@@ -37,6 +38,7 @@ class OrderApplicationServiceStateTest {
|
|||||||
@MockBean private OrderMasterDataGuard contextMasterData;
|
@MockBean private OrderMasterDataGuard contextMasterData;
|
||||||
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
||||||
@MockBean private ProductImageGuard contextProductImageGuard;
|
@MockBean private ProductImageGuard contextProductImageGuard;
|
||||||
|
@MockBean private InboundCodeModeProvider contextInboundCodeModeProvider;
|
||||||
@Resource private OrderApplicationService springService;
|
@Resource private OrderApplicationService springService;
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@@ -260,7 +262,7 @@ class OrderApplicationServiceStateTest {
|
|||||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||||
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
|
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
|
||||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class), masterData);
|
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");
|
service.createDraft(10001L, 2L, request, "legacy-code-draft");
|
||||||
|
|
||||||
@@ -268,6 +270,38 @@ class OrderApplicationServiceStateTest {
|
|||||||
verify(repository, never()).nextInboundCodeSequence(any());
|
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
|
@Test
|
||||||
void legacyManualModeRejectsSubmissionWithoutAnExternalInboundCode() {
|
void legacyManualModeRejectsSubmissionWithoutAnExternalInboundCode() {
|
||||||
OrderRepository repository = mock(OrderRepository.class);
|
OrderRepository repository = mock(OrderRepository.class);
|
||||||
@@ -277,7 +311,7 @@ class OrderApplicationServiceStateTest {
|
|||||||
when(repository.findParties(20002L, 1L)).thenReturn(List.of());
|
when(repository.findParties(20002L, 1L)).thenReturn(List.of());
|
||||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class),
|
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class),
|
||||||
mock(OrderMasterDataGuard.class));
|
mock(OrderMasterDataGuard.class));
|
||||||
ReflectionTestUtils.setField(service, "inboundCodeMode", "LEGACY_MANUAL");
|
useInboundCodeMode(service, InboundCodeModeProvider.Mode.LEGACY_MANUAL);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.transit(
|
assertThatThrownBy(() -> service.transit(
|
||||||
20002L, 2L, "u", 1L, OrderStatus.SUBMITTED, null))
|
20002L, 2L, "u", 1L, OrderStatus.SUBMITTED, null))
|
||||||
@@ -548,10 +582,18 @@ class OrderApplicationServiceStateTest {
|
|||||||
when(repository.nextInboundCodeSequence(any(LocalDate.class))).thenReturn(1L);
|
when(repository.nextInboundCodeSequence(any(LocalDate.class))).thenReturn(1L);
|
||||||
ReflectionTestUtils.setField(service, "pricingChannelGuard", pricing);
|
ReflectionTestUtils.setField(service, "pricingChannelGuard", pricing);
|
||||||
ReflectionTestUtils.setField(service, "productImageGuard", mock(ProductImageGuard.class));
|
ReflectionTestUtils.setField(service, "productImageGuard", mock(ProductImageGuard.class));
|
||||||
|
useInboundCodeMode(service, InboundCodeModeProvider.Mode.AUTO);
|
||||||
ReflectionTestUtils.setField(service, "json", new ObjectMapper().findAndRegisterModules());
|
ReflectionTestUtils.setField(service, "json", new ObjectMapper().findAndRegisterModules());
|
||||||
return service;
|
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() {
|
private OrderRequests.SaveRequest request() {
|
||||||
var party = new OrderRequests.Party(
|
var party = new OrderRequests.Party(
|
||||||
"公司", "联系人", "13800138000", null, "CN", null, "深圳", null, "地址", null);
|
"公司", "联系人", "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.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.order.dto.OrderRequests;
|
import com.baiyue.order.dto.OrderRequests;
|
||||||
|
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||||
import com.baiyue.order.entity.OrderIdempotencyRecordEntity;
|
import com.baiyue.order.entity.OrderIdempotencyRecordEntity;
|
||||||
import com.baiyue.order.repository.OrderRepository;
|
import com.baiyue.order.repository.OrderRepository;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -41,6 +42,7 @@ class OrderIdempotencyServiceTest {
|
|||||||
@MockBean private OrderMasterDataGuard contextMasterData;
|
@MockBean private OrderMasterDataGuard contextMasterData;
|
||||||
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
||||||
@MockBean private ProductImageGuard contextProductImageGuard;
|
@MockBean private ProductImageGuard contextProductImageGuard;
|
||||||
|
@MockBean private InboundCodeModeProvider contextInboundCodeModeProvider;
|
||||||
@Resource private OrderApplicationService springService;
|
@Resource private OrderApplicationService springService;
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class OrderPricingChannelGuardTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void forwardsCustomerOriginAndChannelToFirstleg() {
|
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(
|
when(client.validateChannel("token", request)).thenReturn(ApiResponse.success(
|
||||||
new FirstlegPricingClient.ChannelValidationResponse("MATSON_TRUCK", "ZBYHK")));
|
new FirstlegPricingClient.ChannelValidationResponse("MATSON_TRUCK", "ZBYHK")));
|
||||||
|
|
||||||
@@ -40,9 +40,21 @@ class OrderPricingChannelGuardTest {
|
|||||||
verify(client).validateChannel("token", request);
|
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
|
@Test
|
||||||
void removedOrForgedChannelKeepsFirstlegBusinessError() {
|
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(
|
when(client.validateChannel("token", request)).thenReturn(
|
||||||
ApiResponse.failure(ResultCode.CONFLICT, "所选渠道已下线,请重新选择", null, null));
|
ApiResponse.failure(ResultCode.CONFLICT, "所选渠道已下线,请重新选择", null, null));
|
||||||
|
|
||||||
@@ -55,7 +67,7 @@ class OrderPricingChannelGuardTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void nullResponseMapsToServiceUnavailable() {
|
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);
|
when(client.validateChannel("token", request)).thenReturn(null);
|
||||||
|
|
||||||
assertUnavailable(() -> guard.requireAvailable(20002L, 8L, "MATSON_TRUCK", "FBA"));
|
assertUnavailable(() -> guard.requireAvailable(20002L, 8L, "MATSON_TRUCK", "FBA"));
|
||||||
@@ -63,7 +75,7 @@ class OrderPricingChannelGuardTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void downstreamHttpFailureMapsToServiceUnavailable() {
|
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));
|
when(client.validateChannel("token", request)).thenThrow(feign(403));
|
||||||
|
|
||||||
assertUnavailable(() -> guard.requireAvailable(20002L, 8L, "MATSON_TRUCK", "FBA"));
|
assertUnavailable(() -> guard.requireAvailable(20002L, 8L, "MATSON_TRUCK", "FBA"));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.order.service;
|
package com.baiyue.order.service;
|
||||||
|
|
||||||
import com.baiyue.order.dto.OrderRequests;
|
import com.baiyue.order.dto.OrderRequests;
|
||||||
|
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||||
import com.baiyue.order.repository.OrderRepository;
|
import com.baiyue.order.repository.OrderRepository;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -20,6 +21,7 @@ class PlatformOrderPageServiceTest {
|
|||||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||||
@MockBean private ProductImageGuard productImageGuard;
|
@MockBean private ProductImageGuard productImageGuard;
|
||||||
|
@MockBean private InboundCodeModeProvider inboundCodeModeProvider;
|
||||||
@Resource private OrderApplicationService service;
|
@Resource private OrderApplicationService service;
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class TestConfig {
|
static class TestConfig {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.order.service;
|
package com.baiyue.order.service;
|
||||||
|
|
||||||
import com.baiyue.order.repository.OrderRepository;
|
import com.baiyue.order.repository.OrderRepository;
|
||||||
|
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
@@ -23,6 +24,7 @@ class TenantOrderCountServiceTest {
|
|||||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||||
@MockBean private ProductImageGuard productImageGuard;
|
@MockBean private ProductImageGuard productImageGuard;
|
||||||
|
@MockBean private InboundCodeModeProvider inboundCodeModeProvider;
|
||||||
@Resource private OrderApplicationService service;
|
@Resource private OrderApplicationService service;
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class TestConfig {
|
static class TestConfig {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.order.service;
|
package com.baiyue.order.service;
|
||||||
|
|
||||||
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
|
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
|
||||||
|
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||||
import com.baiyue.order.repository.OrderRepository;
|
import com.baiyue.order.repository.OrderRepository;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -27,6 +28,7 @@ class TenantOrderStatisticsServiceTest {
|
|||||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||||
@MockBean private ProductImageGuard productImageGuard;
|
@MockBean private ProductImageGuard productImageGuard;
|
||||||
|
@MockBean private InboundCodeModeProvider inboundCodeModeProvider;
|
||||||
@Resource private OrderApplicationService service;
|
@Resource private OrderApplicationService service;
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
|||||||
Reference in New Issue
Block a user