优化
This commit is contained in:
@@ -227,8 +227,10 @@ public class OrderApplicationService {
|
||||
throw new BizException("FBA orders require Amazon warehouse code, Shipment ID and Reference ID");
|
||||
}
|
||||
for (var cargo : cargoes) {
|
||||
if (!StringUtils.hasText(cargo.getSkuNo()) || !StringUtils.hasText(cargo.getUpcNo())) {
|
||||
throw new BizException("FBA cargo requires SKU NO. and UPC NO.");
|
||||
if (!StringUtils.hasText(cargo.getSkuNo()) || !StringUtils.hasText(cargo.getUpcNo())
|
||||
|| !StringUtils.hasText(cargo.getShipmentId())
|
||||
|| !StringUtils.hasText(cargo.getReferenceId())) {
|
||||
throw new BizException("FBA cargo requires SKU NO., UPC NO., Shipment ID and Reference ID.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,15 @@ CALL phase1_add_column('ord_order_cargo', 'shipment_id', "VARCHAR(64) NULL COMME
|
||||
CALL phase1_add_column('ord_order_cargo', 'reference_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Reference ID'");
|
||||
CALL phase1_add_column('ord_order_cargo', 'product_image_file_id', "BIGINT UNSIGNED NULL COMMENT 'Product image logical file ID'");
|
||||
|
||||
-- 货物级字段上线前,FBA 编号只保存在主单;回填后提交校验可统一按每条货物执行。
|
||||
UPDATE ord_order_cargo cargo
|
||||
JOIN ord_order orders ON orders.id = cargo.order_id AND orders.tenant_id = cargo.tenant_id
|
||||
SET cargo.shipment_id = COALESCE(NULLIF(cargo.shipment_id, ''), orders.shipment_id),
|
||||
cargo.reference_id = COALESCE(NULLIF(cargo.reference_id, ''), orders.reference_id)
|
||||
WHERE orders.destination_type = 'FBA'
|
||||
AND ((cargo.shipment_id IS NULL OR cargo.shipment_id = '')
|
||||
OR (cargo.reference_id IS NULL OR cargo.reference_id = ''));
|
||||
|
||||
DROP PROCEDURE phase1_add_column;
|
||||
|
||||
-- 已有订单库的幂等记录迁移,和订单写入处于同一个本地事务。
|
||||
|
||||
@@ -2,7 +2,7 @@ openapi: 3.0.3
|
||||
info:
|
||||
title: 柏悦头程订单 API
|
||||
description: 业务成功和失败均使用 HTTP 200;仅响应体 code 为字符串 "200" 时表示成功,其他 code 表示失败。
|
||||
version: 1.4.0
|
||||
version: 1.5.0
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
@@ -241,12 +241,53 @@ components:
|
||||
description: 报价包税维度;独立于订单 taxMode 的买单/退税语义
|
||||
FirstLegOrderDraft:
|
||||
type: object
|
||||
required: [pricingTaxMode]
|
||||
required: [pricingTaxMode, cargoes]
|
||||
properties:
|
||||
pricingTaxMode: {$ref: '#/components/schemas/PricingTaxMode'}
|
||||
cargoes:
|
||||
type: array
|
||||
minItems: 1
|
||||
items: {$ref: '#/components/schemas/FirstLegOrderCargo'}
|
||||
description: >-
|
||||
订单保存请求中的完整字段以服务端 FirstLegOrderSaveRequest 为准;pricingTaxMode
|
||||
是新建和编辑订单的必填报价维度;历史存量订单的响应字段仍允许为空。
|
||||
FirstLegOrderCargo:
|
||||
type: object
|
||||
required: [cargoName, packageType, packageCount, grossWeightKg, volumeCbm]
|
||||
properties:
|
||||
skuNo: {type: string, maxLength: 64}
|
||||
cargoName: {type: string, maxLength: 128}
|
||||
cargoNameEn: {type: string, maxLength: 128}
|
||||
productImageFileId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
hsCode: {type: string, maxLength: 20, description: 报关 HS CODE}
|
||||
customsHsCode: {type: string, maxLength: 20, description: 清关 HS CODE}
|
||||
lengthCm: {type: number, minimum: 0, description: 箱规长(厘米)}
|
||||
widthCm: {type: number, minimum: 0, description: 箱规宽(厘米)}
|
||||
heightCm: {type: number, minimum: 0, description: 箱规高(厘米)}
|
||||
unitsPerPackage: {type: integer, minimum: 1, description: 每箱数量}
|
||||
packageCount: {type: integer, minimum: 1, description: 箱数}
|
||||
grossWeightKg: {type: number, exclusiveMinimum: true, minimum: 0, description: 总毛重}
|
||||
volumeCbm: {type: number, exclusiveMinimum: true, minimum: 0, description: 总体积}
|
||||
totalQuantity: {type: integer, minimum: 1, description: 总数量}
|
||||
netWeightKg: {type: number, exclusiveMinimum: true, minimum: 0, description: 总净重}
|
||||
unitDeclaredValue: {type: number, minimum: 0, description: 清关申报单价(USD)}
|
||||
declaredValue: {type: number, minimum: 0, description: 清关申报金额(USD)}
|
||||
material: {type: string, maxLength: 128}
|
||||
usage: {type: string, maxLength: 128}
|
||||
model: {type: string, maxLength: 64}
|
||||
brand: {type: string, maxLength: 64}
|
||||
productLengthCm: {type: number, minimum: 0}
|
||||
productWidthCm: {type: number, minimum: 0}
|
||||
productHeightCm: {type: number, minimum: 0}
|
||||
shipmentId: {type: string, maxLength: 64}
|
||||
referenceId: {type: string, maxLength: 64}
|
||||
upcNo: {type: string, maxLength: 64}
|
||||
cargoRemark: {type: string, maxLength: 500}
|
||||
packageType: {type: string, maxLength: 32}
|
||||
dangerousGoods: {type: boolean}
|
||||
description: >-
|
||||
原型货物明细字段。FBA 货物的 skuNo、upcNo、shipmentId、referenceId 由服务端按每条货物校验;
|
||||
productImageFileId 必须属于当前登录客户。
|
||||
FirstLegOrderSummary:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -35,6 +35,7 @@ class OrderApplicationServiceStateTest {
|
||||
@MockBean private OrderEligibilityGuard contextEligibility;
|
||||
@MockBean private OrderMasterDataGuard contextMasterData;
|
||||
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
||||
@MockBean private ProductImageGuard contextProductImageGuard;
|
||||
@Resource private OrderApplicationService springService;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
@@ -40,6 +40,7 @@ class OrderIdempotencyServiceTest {
|
||||
@MockBean private OrderEligibilityGuard contextEligibility;
|
||||
@MockBean private OrderMasterDataGuard contextMasterData;
|
||||
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
||||
@MockBean private ProductImageGuard contextProductImageGuard;
|
||||
@Resource private OrderApplicationService springService;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
@@ -19,6 +19,7 @@ class PlatformOrderPageServiceTest {
|
||||
@MockBean private OrderEligibilityGuard eligibilityGuard;
|
||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||
@MockBean private ProductImageGuard productImageGuard;
|
||||
@Resource private OrderApplicationService service;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.client.ProductImageClient;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(ProductImageGuard.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=token")
|
||||
class ProductImageGuardTest {
|
||||
@MockBean private ProductImageClient client;
|
||||
@Resource private ProductImageGuard guard;
|
||||
|
||||
@Test
|
||||
void validatesEachDistinctImageAgainstTheCurrentCustomer() {
|
||||
var metadata = new ProductImageClient.FileMetadata(31L, "product.png", "image/png", 100L);
|
||||
when(client.validate("token", 10001L, 31L)).thenReturn(ApiResponse.success(metadata));
|
||||
|
||||
guard.requireOwned(10001L, List.of(cargo(31L), cargo(31L)));
|
||||
|
||||
verify(client, times(1)).validate("token", 10001L, 31L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesCrossCustomerOrMissingImageRejection() {
|
||||
when(client.validate("token", 10001L, 31L)).thenReturn(
|
||||
ApiResponse.failure(ResultCode.NOT_FOUND, "产品图片不存在", null, null));
|
||||
|
||||
assertThatThrownBy(() -> guard.requireOwned(10001L, List.of(cargo(31L))))
|
||||
.isInstanceOf(BizException.class).hasMessage("产品图片不存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapsUnavailableOrMalformedResponsesToServiceUnavailable() {
|
||||
when(client.validate("token", 10001L, 31L)).thenReturn(null);
|
||||
assertUnavailable(() -> guard.requireOwned(10001L, List.of(cargo(31L))));
|
||||
|
||||
when(client.validate("token", 10001L, 31L)).thenReturn(
|
||||
ApiResponse.success(new ProductImageClient.FileMetadata(32L, "wrong.png", "image/png", 10L)));
|
||||
assertUnavailable(() -> guard.requireOwned(10001L, List.of(cargo(31L))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidLogicalIdBeforeCallingFileService() {
|
||||
assertThatThrownBy(() -> guard.requireOwned(10001L, List.of(cargo(0L))))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("文件ID无效");
|
||||
}
|
||||
|
||||
private void assertUnavailable(org.assertj.core.api.ThrowableAssert.ThrowingCallable invocation) {
|
||||
assertThatThrownBy(invocation).isInstanceOf(BizException.class)
|
||||
.hasMessage("产品图片校验服务暂时不可用")
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
private static OrderRequests.Cargo cargo(Long fileId) {
|
||||
return new OrderRequests.Cargo("货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE,
|
||||
null, null, null, null, null, false, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, fileId);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class TenantOrderCountServiceTest {
|
||||
@MockBean private OrderEligibilityGuard eligibilityGuard;
|
||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||
@MockBean private ProductImageGuard productImageGuard;
|
||||
@Resource private OrderApplicationService service;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
|
||||
Reference in New Issue
Block a user