优化
This commit is contained in:
+21
@@ -11,6 +11,8 @@ import com.baiyue.firstleg.dto.InboundOrderReference;
|
||||
import com.baiyue.firstleg.service.InboundOrderCreationService;
|
||||
import com.baiyue.firstleg.service.InboundCartonLabelService;
|
||||
import com.baiyue.firstleg.service.UnifiedInboundQueryService;
|
||||
import com.baiyue.firstleg.service.OrderDeclarationSyncService;
|
||||
import com.baiyue.firstleg.dto.OrderDeclarationSync;
|
||||
import com.baiyue.firstleg.dto.UnifiedInboundDetail;
|
||||
import com.baiyue.firstleg.dto.UnifiedInboundQuery;
|
||||
import com.baiyue.firstleg.dto.UnifiedInboundRequest;
|
||||
@@ -39,6 +41,8 @@ public class InternalInboundOrderController {
|
||||
private InboundCartonLabelService cartonLabels;
|
||||
@Resource
|
||||
private UnifiedInboundQueryService unifiedQueryService;
|
||||
@Resource
|
||||
private OrderDeclarationSyncService declarationSync;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
private byte[] expectedToken;
|
||||
@@ -106,6 +110,23 @@ public class InternalInboundOrderController {
|
||||
request.tenantId(), request.orderIds().stream().distinct().toList()));
|
||||
}
|
||||
|
||||
@PostMapping("/order-channel-changes/validate")
|
||||
public ApiResponse<OrderDeclarationSync.ChannelChangeValidationResponse> validateOrderChannelChange(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestBody OrderDeclarationSync.ChannelChangeValidationRequest request) {
|
||||
requireToken(token);
|
||||
return ApiResponse.success(declarationSync.validateChannelChange(request));
|
||||
}
|
||||
|
||||
@PostMapping("/order-declaration-updated")
|
||||
public ApiResponse<Void> updateOrderDeclaration(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestBody OrderDeclarationSync.UpdatedEvent event) {
|
||||
requireToken(token);
|
||||
declarationSync.consume(event);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/unified-orders/query")
|
||||
public ApiResponse<PageResult<UnifiedInboundSummary>> unifiedPage(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** Order -> Firstleg 客户申报版本事件及审核后渠道履约门禁契约。 */
|
||||
public final class OrderDeclarationSync {
|
||||
private OrderDeclarationSync() {}
|
||||
|
||||
public record ChannelChangeValidationRequest(long tenantId, long orderId, long orderVersion,
|
||||
String channelCode) {}
|
||||
public record ChannelChangeValidationResponse(long orderId, long orderVersion,
|
||||
String channelCode, String mainStatus) {}
|
||||
public record Cargo(Integer itemNo, String cargoName, String cargoNameEn, String skuNo, String model,
|
||||
Integer packageCount, Integer totalQuantity, BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm, BigDecimal grossWeightKg, BigDecimal volumeCbm,
|
||||
BigDecimal netWeightKg, String hsCode, String customsHsCode, Integer unitsPerPackage,
|
||||
BigDecimal unitDeclaredValue, BigDecimal declaredValue, String currency,
|
||||
String material, String usage, String brand, String cargoRemark) {}
|
||||
public record FieldChange(Integer itemNo, String field, Object beforeValue, Object afterValue) {}
|
||||
public record UpdatedEvent(int schemaVersion, String eventId, long tenantId, long orderId,
|
||||
String orderNo, String inboundCode, int version, long operatorId,
|
||||
LocalDateTime occurredAt, String customerRemark,
|
||||
LocalDateTime expectedArrivalAt, String channelCode, String channelName,
|
||||
boolean channelChanged, List<Cargo> cargoes, List<FieldChange> changes) {}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Mapper
|
||||
public interface OrderDeclarationSyncMapper {
|
||||
@Select("""
|
||||
SELECT inbound.id inboundId,inbound.order_id orderId,inbound.order_no orderNo,
|
||||
inbound.inbound_code inboundCode,inbound.process_status processStatus,
|
||||
inbound.channel_code channelCode,
|
||||
(SELECT COUNT(*) FROM fl_coload_group_member member
|
||||
JOIN fl_coload_group group_row ON group_row.id=member.group_id
|
||||
WHERE member.tenant_id=inbound.tenant_id AND member.inbound_id=inbound.id
|
||||
AND member.active_flag=1 AND group_row.group_status IN ('ACTIVE','LOCKED')
|
||||
AND UPPER(group_row.channel_code)<>UPPER(#{newChannelCode})) incompatibleGroupCount,
|
||||
(SELECT COUNT(*) FROM fl_inbound_split_batch batch
|
||||
WHERE batch.tenant_id=inbound.tenant_id AND batch.source_inbound_id=inbound.id
|
||||
AND batch.status='APPLIED') splitCount,
|
||||
(SELECT COUNT(*) FROM fl_loading_plan_item item
|
||||
WHERE item.account_id=inbound.tenant_id AND item.inbound_id=inbound.id AND item.deleted=0) preloadCount,
|
||||
(SELECT COUNT(*) FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_plan_item plan_item ON plan_item.id=pick_item.loading_plan_item_id
|
||||
WHERE plan_item.account_id=inbound.tenant_id AND plan_item.inbound_id=inbound.id) pdaCount
|
||||
FROM fl_inbound_order inbound
|
||||
WHERE inbound.tenant_id=#{tenantId} AND inbound.order_id=#{orderId} AND inbound.deleted=0
|
||||
ORDER BY inbound.created_at DESC,inbound.id DESC LIMIT 1
|
||||
""")
|
||||
GateRow gate(@Param("tenantId") long tenantId, @Param("orderId") long orderId,
|
||||
@Param("newChannelCode") String newChannelCode);
|
||||
|
||||
@Select("""
|
||||
SELECT inbound.id inboundId,inbound.order_id orderId,inbound.order_no orderNo,
|
||||
inbound.inbound_code inboundCode,inbound.process_status processStatus,inbound.channel_code channelCode
|
||||
FROM fl_inbound_order inbound
|
||||
WHERE inbound.tenant_id=#{tenantId} AND inbound.order_id=#{orderId} AND inbound.deleted=0
|
||||
ORDER BY inbound.created_at DESC,inbound.id DESC LIMIT 1 FOR UPDATE
|
||||
""")
|
||||
GateRow lockInbound(@Param("tenantId") long tenantId, @Param("orderId") long orderId);
|
||||
|
||||
@Select("SELECT tenant_id tenantId,order_id orderId,event_id eventId,order_version orderVersion,request_hash requestHash "
|
||||
+ "FROM fl_order_declaration_snapshot WHERE tenant_id=#{tenantId} AND order_id=#{orderId} FOR UPDATE")
|
||||
SnapshotRow lockSnapshot(@Param("tenantId") long tenantId, @Param("orderId") long orderId);
|
||||
|
||||
@Select("SELECT tenant_id tenantId,order_id orderId,event_id eventId,order_version orderVersion,request_hash requestHash "
|
||||
+ "FROM fl_order_declaration_snapshot WHERE tenant_id=#{tenantId} AND event_id=#{eventId}")
|
||||
SnapshotRow byEvent(@Param("tenantId") long tenantId, @Param("eventId") String eventId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_order_declaration_snapshot
|
||||
(tenant_id,order_id,event_id,order_version,request_hash,customer_remark,expected_arrival_at,
|
||||
channel_code,channel_name,cargoes_json,changes_json,occurred_at,created_at,updated_at)
|
||||
VALUES
|
||||
(#{tenantId},#{orderId},#{eventId},#{orderVersion},#{requestHash},#{customerRemark},#{expectedArrivalAt},
|
||||
#{channelCode},#{channelName},#{cargoesJson},#{changesJson},#{occurredAt},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertSnapshot(@Param("tenantId") long tenantId, @Param("orderId") long orderId,
|
||||
@Param("eventId") String eventId, @Param("orderVersion") int orderVersion,
|
||||
@Param("requestHash") String requestHash, @Param("customerRemark") String customerRemark,
|
||||
@Param("expectedArrivalAt") LocalDateTime expectedArrivalAt,
|
||||
@Param("channelCode") String channelCode, @Param("channelName") String channelName,
|
||||
@Param("cargoesJson") String cargoesJson, @Param("changesJson") String changesJson,
|
||||
@Param("occurredAt") LocalDateTime occurredAt);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_order_declaration_snapshot
|
||||
SET event_id=#{eventId},order_version=#{nextVersion},request_hash=#{requestHash},
|
||||
customer_remark=#{customerRemark},expected_arrival_at=#{expectedArrivalAt},
|
||||
channel_code=#{channelCode},channel_name=#{channelName},cargoes_json=#{cargoesJson},
|
||||
changes_json=#{changesJson},occurred_at=#{occurredAt},updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE tenant_id=#{tenantId} AND order_id=#{orderId} AND order_version=#{currentVersion}
|
||||
""")
|
||||
int advanceSnapshot(@Param("tenantId") long tenantId, @Param("orderId") long orderId,
|
||||
@Param("currentVersion") int currentVersion, @Param("nextVersion") int nextVersion,
|
||||
@Param("eventId") String eventId, @Param("requestHash") String requestHash,
|
||||
@Param("customerRemark") String customerRemark,
|
||||
@Param("expectedArrivalAt") LocalDateTime expectedArrivalAt,
|
||||
@Param("channelCode") String channelCode, @Param("channelName") String channelName,
|
||||
@Param("cargoesJson") String cargoesJson, @Param("changesJson") String changesJson,
|
||||
@Param("occurredAt") LocalDateTime occurredAt);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET public_remark=#{customerRemark},expected_arrival_at=#{expectedArrivalAt},
|
||||
channel_code=#{channelCode},channel_name=#{channelName},updated_by=#{operatorId},
|
||||
updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0
|
||||
AND order_id=#{orderId} AND order_no=#{orderNo} AND inbound_code=#{inboundCode}
|
||||
""")
|
||||
int updateAllowedSnapshot(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("orderId") long orderId, @Param("orderNo") String orderNo,
|
||||
@Param("inboundCode") String inboundCode,
|
||||
@Param("customerRemark") String customerRemark,
|
||||
@Param("expectedArrivalAt") LocalDateTime expectedArrivalAt,
|
||||
@Param("channelCode") String channelCode, @Param("channelName") String channelName,
|
||||
@Param("operatorId") long operatorId);
|
||||
|
||||
record GateRow(Long inboundId, Long orderId, String orderNo, String inboundCode, String processStatus,
|
||||
String channelCode, Integer incompatibleGroupCount, Integer splitCount,
|
||||
Integer preloadCount, Integer pdaCount) {}
|
||||
record SnapshotRow(Long tenantId, Long orderId, String eventId, Integer orderVersion, String requestHash) {}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.OrderDeclarationSync.ChannelChangeValidationRequest;
|
||||
import com.baiyue.firstleg.dto.OrderDeclarationSync.ChannelChangeValidationResponse;
|
||||
import com.baiyue.firstleg.dto.OrderDeclarationSync.UpdatedEvent;
|
||||
import com.baiyue.firstleg.mapper.OrderDeclarationSyncMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class OrderDeclarationSyncService {
|
||||
@Resource private OrderDeclarationSyncMapper mapper;
|
||||
@Resource private ObjectMapper json;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ChannelChangeValidationResponse validateChannelChange(ChannelChangeValidationRequest request) {
|
||||
requireValidationRequest(request);
|
||||
String channelCode = normalizeChannel(request.channelCode());
|
||||
var row = mapper.gate(request.tenantId(), request.orderId(), channelCode);
|
||||
if (row == null) throw new BizException(ResultCode.NOT_FOUND, "订单对应入库单不存在");
|
||||
if (isLoadingOrLater(row.processStatus())) {
|
||||
throw conflict("订单已进入装柜或后续履约阶段,渠道不可变更");
|
||||
}
|
||||
if (positive(row.pdaCount())) throw conflict("订单已生成PDA装柜任务,渠道不可变更");
|
||||
if (positive(row.preloadCount())) throw conflict("订单已进入预装计划,渠道不可变更");
|
||||
if (positive(row.splitCount())) throw conflict("订单已产生分柜事实,渠道不可变更");
|
||||
if (positive(row.incompatibleGroupCount())) {
|
||||
throw conflict("订单所属同柜组与新渠道不兼容,渠道不可变更");
|
||||
}
|
||||
return new ChannelChangeValidationResponse(request.orderId(), request.orderVersion(), channelCode,
|
||||
mainStatus(row.processStatus()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void consume(UpdatedEvent event) {
|
||||
requireEvent(event);
|
||||
String hash = hash(event);
|
||||
var inbound = mapper.lockInbound(event.tenantId(), event.orderId());
|
||||
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "订单对应入库单不存在");
|
||||
if (!Objects.equals(inbound.orderNo(), event.orderNo())
|
||||
|| !Objects.equals(inbound.inboundCode(), event.inboundCode())) {
|
||||
throw conflict("订单号或入仓码与 Firstleg 入库快照不一致");
|
||||
}
|
||||
var eventReplay = mapper.byEvent(event.tenantId(), event.eventId());
|
||||
if (eventReplay != null) {
|
||||
if (eventReplay.orderId() != event.orderId() || !hash.equals(eventReplay.requestHash())) {
|
||||
throw conflict("eventId已用于不同申报事件");
|
||||
}
|
||||
return;
|
||||
}
|
||||
var current = mapper.lockSnapshot(event.tenantId(), event.orderId());
|
||||
if (current != null && event.version() < current.orderVersion()) return;
|
||||
if (current != null && event.version() == current.orderVersion()) {
|
||||
throw conflict("同一订单版本已存在不同申报事件");
|
||||
}
|
||||
String cargoesJson = write(event.cargoes());
|
||||
String changesJson = write(event.changes());
|
||||
int saved = current == null
|
||||
? mapper.insertSnapshot(event.tenantId(), event.orderId(), event.eventId(), event.version(), hash,
|
||||
event.customerRemark(), event.expectedArrivalAt(), normalizeChannel(event.channelCode()),
|
||||
event.channelName(), cargoesJson, changesJson, event.occurredAt())
|
||||
: mapper.advanceSnapshot(event.tenantId(), event.orderId(), current.orderVersion(), event.version(),
|
||||
event.eventId(), hash, event.customerRemark(), event.expectedArrivalAt(),
|
||||
normalizeChannel(event.channelCode()), event.channelName(), cargoesJson, changesJson, event.occurredAt());
|
||||
if (saved != 1) throw conflict("申报事件版本已发生并发变化");
|
||||
if (mapper.updateAllowedSnapshot(event.tenantId(), inbound.inboundId(), event.orderId(), event.orderNo(),
|
||||
event.inboundCode(), event.customerRemark(), event.expectedArrivalAt(),
|
||||
normalizeChannel(event.channelCode()), event.channelName(), event.operatorId()) != 1) {
|
||||
throw conflict("Firstleg 入库申报快照已发生变化");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireValidationRequest(ChannelChangeValidationRequest request) {
|
||||
if (request == null || request.tenantId() <= 0 || request.orderId() <= 0 || request.orderVersion() < 0
|
||||
|| !StringUtils.hasText(request.channelCode()) || request.channelCode().length() > 64) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "渠道变更履约校验参数不完整");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireEvent(UpdatedEvent event) {
|
||||
if (event == null || event.schemaVersion() != 1 || !StringUtils.hasText(event.eventId())
|
||||
|| event.eventId().length() > 128 || event.tenantId() <= 0 || event.orderId() <= 0
|
||||
|| event.version() < 0 || event.operatorId() <= 0 || event.occurredAt() == null
|
||||
|| !StringUtils.hasText(event.orderNo()) || !StringUtils.hasText(event.inboundCode())
|
||||
|| event.cargoes() == null || event.cargoes().isEmpty()
|
||||
|| !StringUtils.hasText(event.channelCode()) || !StringUtils.hasText(event.channelName())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "订单申报更新事件不完整");
|
||||
}
|
||||
}
|
||||
|
||||
private String hash(UpdatedEvent event) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(json.writeValueAsBytes(event)));
|
||||
} catch (NoSuchAlgorithmException | JsonProcessingException exception) {
|
||||
throw new IllegalStateException("无法计算订单申报事件摘要", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private String write(Object value) {
|
||||
try {
|
||||
return json.writeValueAsString(value);
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new IllegalStateException("无法持久化订单申报快照", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeChannel(String value) {
|
||||
return value.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static boolean positive(Integer value) { return value != null && value > 0; }
|
||||
private static boolean isLoadingOrLater(String value) {
|
||||
return "LOADING".equals(value) || "LOADED".equals(value) || "DELIVERED".equals(value);
|
||||
}
|
||||
private static String mainStatus(String value) {
|
||||
if ("WAIT_SPLIT".equals(value) || "LOADING".equals(value)
|
||||
|| "LOADED".equals(value) || "DELIVERED".equals(value)) return value;
|
||||
return "WAIT_MEASURE";
|
||||
}
|
||||
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
|
||||
}
|
||||
Reference in New Issue
Block a user