This commit is contained in:
2026-08-07 18:44:51 +08:00
parent 1a76602872
commit 37fec1e012
24 changed files with 601 additions and 24 deletions
@@ -0,0 +1,19 @@
package com.baiyue.order.client;
import com.baiyue.common.core.api.ApiResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
@FeignClient(name = "file-service", contextId = "orderProductImageClient",
url = "${baiyue.clients.file.url:}", path = "/internal/file/v1/product-images")
public interface ProductImageClient {
@GetMapping("/{tenantId}/{fileId}")
ApiResponse<FileMetadata> validate(
@RequestHeader("X-Internal-Service-Token") String token,
@PathVariable long tenantId,
@PathVariable long fileId);
record FileMetadata(Long fileId, String fileName, String contentType, Long sizeBytes) {}
}
@@ -135,7 +135,13 @@ public final class OrderRequests {
@Size(max = 128) String material,
@Size(max = 128) String usage,
@Size(max = 64) String model,
@Size(max = 64) String brand
@Size(max = 64) String brand,
@DecimalMin("0.01") BigDecimal productLengthCm,
@DecimalMin("0.01") BigDecimal productWidthCm,
@DecimalMin("0.01") BigDecimal productHeightCm,
@Size(max = 64) String shipmentId,
@Size(max = 64) String referenceId,
@Schema(description = "Product image file ID / 产品图片文件ID") Long productImageFileId
) {
}
@@ -12,7 +12,7 @@ import com.baiyue.common.core.model.BaseEntity; import com.baomidou.mybatisplus.
* 申报价值及危险品标识等货物信息。</p>
*/
@TableName("ord_order_cargo") public class OrderCargoEntity extends BaseEntity {
@TableId(type=IdType.ASSIGN_ID) private Long id; private Long tenantId; private Long orderId; private Integer itemNo; private String cargoName; private String hsCode; private String packageType; private Integer packageCount; private BigDecimal grossWeightKg; private BigDecimal volumeCbm; private BigDecimal lengthCm; private BigDecimal widthCm; private BigDecimal heightCm; private BigDecimal declaredValue; private String currency; private Boolean dangerousGoods; private String cargoRemark; private String skuNo; private String upcNo; private String cargoNameEn; private String customsHsCode; private Integer unitsPerPackage; private Integer totalQuantity; private BigDecimal netWeightKg; private BigDecimal unitDeclaredValue; private String material; private String productUsage; private String model; private String brand;
@TableId(type=IdType.ASSIGN_ID) private Long id; private Long tenantId; private Long orderId; private Integer itemNo; private String cargoName; private String hsCode; private String packageType; private Integer packageCount; private BigDecimal grossWeightKg; private BigDecimal volumeCbm; private BigDecimal lengthCm; private BigDecimal widthCm; private BigDecimal heightCm; private BigDecimal declaredValue; private String currency; private Boolean dangerousGoods; private String cargoRemark; private String skuNo; private String upcNo; private String cargoNameEn; private String customsHsCode; private Integer unitsPerPackage; private Integer totalQuantity; private BigDecimal netWeightKg; private BigDecimal unitDeclaredValue; private String material; private String productUsage; private String model; private String brand; private BigDecimal productLengthCm; private BigDecimal productWidthCm; private BigDecimal productHeightCm; private String shipmentId; private String referenceId; private Long productImageFileId;
}
@@ -33,6 +33,7 @@ public class OrderApplicationService {
@Resource private OrderEligibilityGuard eligibilityGuard;
@Resource private OrderMasterDataGuard masterDataGuard;
@Resource private OrderPricingChannelGuard pricingChannelGuard;
@Resource private ProductImageGuard productImageGuard;
/** 创建草稿时使用数据库唯一键保证请求幂等。 */
@Transactional
@@ -49,6 +50,7 @@ public class OrderApplicationService {
private Long createDraftInternal(long tenantId, long userId, OrderRequests.SaveRequest request) {
validatePrototypeFields(request);
productImageGuard.requireOwned(tenantId, request.cargoes());
var snapshots = masterDataGuard.validate(tenantId, request);
pricingChannelGuard.requireAvailable(tenantId, request.receivingWarehouseId(), request.channelCode());
OrderEntity order = new OrderEntity();
@@ -80,6 +82,7 @@ public class OrderApplicationService {
boolean allowSubmitted) {
OrderEntity order = require(tenantId, id);
validatePrototypeFields(request);
productImageGuard.requireOwned(tenantId, request.cargoes());
var snapshots = masterDataGuard.validate(tenantId, request);
OrderStatus before = order.getStatus();
if (before != OrderStatus.DRAFT && before != OrderStatus.REJECTED
@@ -189,8 +192,9 @@ public class OrderApplicationService {
throw new BizException("FBA orders require Amazon warehouse code, Shipment ID and Reference ID");
}
for (var cargo : request.cargoes()) {
if (!StringUtils.hasText(cargo.skuNo()) || !StringUtils.hasText(cargo.upcNo())) {
throw new BizException("FBA cargo requires SKU NO. and UPC NO.");
if (!StringUtils.hasText(cargo.skuNo()) || !StringUtils.hasText(cargo.upcNo())
|| !StringUtils.hasText(cargo.shipmentId()) || !StringUtils.hasText(cargo.referenceId())) {
throw new BizException("FBA cargo requires SKU NO., UPC NO., Shipment ID and Reference ID.");
}
}
}
@@ -384,6 +388,10 @@ public class OrderApplicationService {
item.setTotalQuantity(cargo.totalQuantity()); item.setNetWeightKg(cargo.netWeightKg());
item.setUnitDeclaredValue(cargo.unitDeclaredValue()); item.setMaterial(cargo.material());
item.setProductUsage(cargo.usage()); item.setModel(cargo.model()); item.setBrand(cargo.brand());
item.setProductLengthCm(cargo.productLengthCm()); item.setProductWidthCm(cargo.productWidthCm());
item.setProductHeightCm(cargo.productHeightCm()); item.setShipmentId(cargo.shipmentId());
item.setReferenceId(cargo.referenceId());
item.setProductImageFileId(cargo.productImageFileId());
item.setCreatedBy(userId); item.setUpdatedBy(userId); repository.insertCargo(item);
}
if (request.sender() != null) saveParty(tenantId, userId, orderId, "SENDER", request.sender());
@@ -0,0 +1,58 @@
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 feign.FeignException;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Service
public class ProductImageGuard {
@Resource private ProductImageClient client;
@Value("${baiyue.security.internal-service-token}") private String internalToken;
public void requireOwned(long tenantId, List<OrderRequests.Cargo> cargoes) {
Set<Long> validated = new HashSet<>();
for (OrderRequests.Cargo cargo : cargoes) {
Long fileId = cargo.productImageFileId();
if (fileId == null || !validated.add(fileId)) continue;
if (fileId <= 0) throw new BizException(ResultCode.VALIDATION_FAILED, "产品图片文件ID无效");
validate(tenantId, fileId);
}
}
private void validate(long tenantId, long fileId) {
try {
ApiResponse<ProductImageClient.FileMetadata> response = client.validate(internalToken, tenantId, fileId);
if (response == null) throw unavailable();
if (!response.successful()) {
ResultCode code = ResultCode.of(response.code());
String message = response.message() == null || response.message().isBlank()
? code.message() : response.message();
throw new BizException(code, message);
}
if (response.data() == null || response.data().fileId() == null
|| response.data().fileId() != fileId) {
throw unavailable();
}
} catch (BizException exception) {
throw exception;
} catch (FeignException exception) {
throw unavailable();
} catch (RuntimeException exception) {
throw unavailable();
}
}
private static BizException unavailable() {
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "产品图片校验服务暂时不可用");
}
}
+2
View File
@@ -33,6 +33,8 @@ baiyue:
url: ${IAM_SERVICE_URL:}
warehouse:
url: ${WAREHOUSE_SERVICE_URL:}
file:
url: ${FILE_SERVICE_URL:}
logging:
file:
+6
View File
@@ -111,6 +111,12 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo (
product_usage VARCHAR(128) NULL COMMENT '用途',
model VARCHAR(64) NULL COMMENT '型号',
brand VARCHAR(64) NULL COMMENT '品牌',
product_length_cm DECIMAL(12,2) NULL COMMENT '产品长度(厘米)',
product_width_cm DECIMAL(12,2) NULL COMMENT '产品宽度(厘米)',
product_height_cm DECIMAL(12,2) NULL COMMENT '产品高度(厘米)',
shipment_id VARCHAR(64) NULL COMMENT '货物级 Amazon Shipment ID',
reference_id VARCHAR(64) NULL COMMENT '货物级 Amazon Reference ID',
product_image_file_id BIGINT UNSIGNED NULL COMMENT '文件中心产品图片逻辑ID',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
@@ -54,6 +54,12 @@ CALL phase1_add_column('ord_order_cargo', 'material', "VARCHAR(128) NULL COMMENT
CALL phase1_add_column('ord_order_cargo', 'product_usage', "VARCHAR(128) NULL COMMENT 'Usage'");
CALL phase1_add_column('ord_order_cargo', 'model', "VARCHAR(64) NULL COMMENT 'Model'");
CALL phase1_add_column('ord_order_cargo', 'brand', "VARCHAR(64) NULL COMMENT 'Brand'");
CALL phase1_add_column('ord_order_cargo', 'product_length_cm', "DECIMAL(12,2) NULL COMMENT 'Product length in centimeters'");
CALL phase1_add_column('ord_order_cargo', 'product_width_cm', "DECIMAL(12,2) NULL COMMENT 'Product width in centimeters'");
CALL phase1_add_column('ord_order_cargo', 'product_height_cm', "DECIMAL(12,2) NULL COMMENT 'Product height in centimeters'");
CALL phase1_add_column('ord_order_cargo', 'shipment_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Shipment ID'");
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'");
DROP PROCEDURE phase1_add_column;
@@ -39,7 +39,8 @@ class OrderRequestsValidationTest {
var cargo = 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, null, null, null, null,
null, null, null, null, null, null);
var request = new OrderRequests.SaveRequest(
null, "当前企业", "SEA", "FOB", "US", 193000000000200003L, "洛杉矶仓",
null, null, null, null, List.of(cargo), "SELF_EXPRESS", 193000000000200001L,
@@ -398,6 +398,7 @@ class OrderApplicationServiceStateTest {
ReflectionTestUtils.setField(service, "eligibilityGuard", eligibility);
ReflectionTestUtils.setField(service, "masterDataGuard", masterData);
ReflectionTestUtils.setField(service, "pricingChannelGuard", mock(OrderPricingChannelGuard.class));
ReflectionTestUtils.setField(service, "productImageGuard", mock(ProductImageGuard.class));
ReflectionTestUtils.setField(service, "json", new ObjectMapper());
return service;
}
@@ -408,7 +409,8 @@ class OrderApplicationServiceStateTest {
var cargo = 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, null, null, null, null,
null, null, null, null, null, null);
return new OrderRequests.SaveRequest(
7L, "客户", "SEA", null, "US", null, "目的仓", null, null,
party, party, List.of(cargo), "PICKUP", 8L, "收货仓", "CH", "渠道", 9L, "船司",
@@ -421,7 +423,8 @@ class OrderApplicationServiceStateTest {
var cargo = 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, null, null, null, null,
null, null, null, null, null, null);
return new OrderRequests.SaveRequest(
customerId, "当前企业", "SEA", null, "US", null, "目的仓", null, null,
sender, receiver, List.of(cargo), pickupMethod, 8L, "收货仓", "CH", "渠道", 9L, "船司",
@@ -253,7 +253,8 @@ class OrderMasterDataGuardTest {
private OrderRequests.SaveRequest request(Long partner,Long warehouse,String destinationType,Long destinationWarehouseId) {
var party=new OrderRequests.Party("公司","联系人","13800138000",null,"CN",null,"深圳",null,"地址",null);
var cargo=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,false,null,null,null,null,null,null,null,null,null,null,null,null,null,
null,null,null,null,null,null);
return new OrderRequests.SaveRequest(partner,"不可信客户名","SEA",null,"CN",destinationWarehouseId,"私人地址",null,null,party,party,List.of(cargo),"PICKUP",warehouse,"不可信收货仓","CH","渠道",9L,"船司","BUY_DECLARATION",null,"LCL",false,destinationType,null,null,null);
}
private WarehouseClient.Detail warehouse(long id,String type,boolean receiving) {