This commit is contained in:
2026-08-18 11:41:10 +08:00
parent 3e1010fa26
commit ec4b1e0fe5
28 changed files with 412 additions and 42 deletions
@@ -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;主订单为空;禁止子订单嵌套');
@@ -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 模式仅校验渠道可用性");
}
}
@@ -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)");
}
}
@@ -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();
+30 -3
View File
@@ -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,
@@ -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)