二期内容提交

This commit is contained in:
2026-08-04 17:18:07 +08:00
parent db72bdf884
commit b7d36d4766
30 changed files with 537 additions and 19 deletions
@@ -4,12 +4,14 @@ import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 订单微服务启动入口,负责装配订单领域组件并扫描 MyBatis-Plus Mapper。
*/
@MapperScan("com.baiyue.order.mapper")
@EnableFeignClients(basePackages = "com.baiyue.order.client")
@EnableScheduling
@SpringBootApplication(scanBasePackages = "com.baiyue")
public class BaiyueOrderApplication {
public static void main(String[] args) {
@@ -0,0 +1,29 @@
package com.baiyue.order.client;
import com.baiyue.common.core.api.ApiResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import java.time.LocalDateTime;
/** 订单审核事件投递到头程入库域的内部客户端。 */
@FeignClient(name = "firstleg-service", contextId = "orderFirstlegInboundClient",
url = "${baiyue.clients.firstleg.url:}", path = "/internal/firstleg/v1/inbounds")
public interface FirstlegInboundClient {
@PostMapping("/order-approved")
ApiResponse<Long> create(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody OrderApprovedInboundCommand command);
record OrderApprovedInboundCommand(
long tenantId, long orderId, String orderNo, Long customerId, String customerName,
Long warehouseId, String warehouseName, Long deliveryWarehouseId, String deliveryWarehouseName,
LocalDateTime expectedArrivalAt, String businessType, String channelCode, String channelName,
Long carrierId, String carrierName, String taxMode, String containerMode,
Boolean customsInspection, String destinationType, Long destinationWarehouseId,
String destinationCode, String destinationName, String publicRemark,
String expressCompanyName, String expressTrackingNo
) {
}
}
@@ -55,6 +55,8 @@ public final class OrderRequests {
@NotBlank @Size(max = 128) String receivingWarehouseName,
@NotBlank @Size(max = 64) String channelCode,
@NotBlank @Size(max = 128) String channelName,
@NotNull Long carrierId,
@NotBlank @Size(max = 128) String carrierName,
@NotBlank @Pattern(regexp = "BUY_DECLARATION|TAX_REFUND") String taxMode,
@Pattern(regexp = "FULL_CONTAINER|LCL") String containerMode,
@NotNull Boolean customsInspection,
@@ -38,6 +38,8 @@ public class OrderEntity extends BaseEntity {
private String receivingTime;
private String channelCode;
private String channelName;
private Long carrierId;
private String carrierName;
private String taxMode;
private String containerMode;
private Boolean customsInspection;
@@ -26,6 +26,8 @@ public class OutboxEventEntity {
private String payloadJson;
private String publishStatus;
private Integer retryCount;
private LocalDateTime nextRetryAt;
private LocalDateTime publishedAt;
private LocalDateTime createdAt;
}
@@ -6,12 +6,14 @@ import com.baiyue.order.entity.*;
import com.baiyue.order.mapper.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
import java.time.LocalDateTime;
/**
* 订单聚合仓储,组合订单主表、货物、参与方、备注、状态历史与 Outbox 的持久化操作,
@@ -49,6 +51,29 @@ public class OrderRepository {
public void insertNote(OrderInternalNoteEntity note) { noteMapper.insert(note); }
public void insertHistory(OrderStatusHistoryEntity history) { historyMapper.insert(history); }
public void insertOutbox(OutboxEventEntity event) { outboxMapper.insert(event); }
public List<OutboxEventEntity> pendingInboundEvents(int limit) {
return outboxMapper.selectList(new LambdaQueryWrapper<OutboxEventEntity>()
.eq(OutboxEventEntity::getEventType, "FIRST_LEG_ORDER_APPROVED")
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
.and(v -> v.isNull(OutboxEventEntity::getNextRetryAt)
.or().le(OutboxEventEntity::getNextRetryAt, LocalDateTime.now()))
.orderByAsc(OutboxEventEntity::getCreatedAt)
.last("LIMIT " + Math.min(Math.max(limit, 1), 100)));
}
public void markOutboxPublished(long id, LocalDateTime publishedAt) {
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
.eq(OutboxEventEntity::getId, id)
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
.set(OutboxEventEntity::getPublishStatus, "PUBLISHED")
.set(OutboxEventEntity::getPublishedAt, publishedAt));
}
public void markOutboxRetry(long id, LocalDateTime nextRetryAt) {
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
.eq(OutboxEventEntity::getId, id)
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
.setSql("retry_count = retry_count + 1")
.set(OutboxEventEntity::getNextRetryAt, nextRetryAt));
}
public void insertIdempotency(OrderIdempotencyRecordEntity record) { idempotencyMapper.insert(record); }
public OrderIdempotencyRecordEntity findIdempotency(
@@ -5,6 +5,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.dto.OrderDetailResponse;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.dto.TenantOrderCountResponse;
import com.baiyue.order.client.FirstlegInboundClient;
import com.baiyue.order.entity.*;
import com.baiyue.order.repository.OrderRepository;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
@@ -304,7 +305,9 @@ public class OrderApplicationService {
order.setReceivingWarehouseAddress(formatAddress(snapshots.receiving()));
order.setReceivingTime(formatReceivingTime(snapshots.receiving()));
order.setChannelCode(request.channelCode());
order.setChannelName(request.channelName()); order.setTaxMode(request.taxMode());
order.setChannelName(request.channelName());
order.setCarrierId(request.carrierId()); order.setCarrierName(request.carrierName());
order.setTaxMode(request.taxMode());
order.setContainerMode(request.containerMode()); order.setCustomsInspection(request.customsInspection());
order.setDestinationType(request.destinationType());
order.setAmazonWarehouseCode("FBA".equals(request.destinationType()) && snapshots.destination() != null
@@ -366,12 +369,26 @@ public class OrderApplicationService {
event.setTenantId(order.getTenantId()); event.setAggregateType("FIRST_LEG_ORDER"); event.setAggregateId(order.getId());
event.setEventType(target == OrderStatus.APPROVED ? "FIRST_LEG_ORDER_APPROVED" : "FIRST_LEG_ORDER_CANCELLED");
try {
event.setPayloadJson(json.writeValueAsString(Map.of("tenantId", order.getTenantId(), "orderId", order.getId(),
event.setPayloadJson(target == OrderStatus.APPROVED
? json.writeValueAsString(inboundCommand(order))
: json.writeValueAsString(Map.of("tenantId", order.getTenantId(), "orderId", order.getId(),
"orderNo", order.getOrderNo(), "status", target.name())));
} catch (JsonProcessingException exception) { throw new BizException("Cannot serialize order event"); }
event.setPublishStatus("PENDING"); event.setRetryCount(0); event.setCreatedAt(LocalDateTime.now());
repository.insertOutbox(event);
}
private FirstlegInboundClient.OrderApprovedInboundCommand inboundCommand(OrderEntity order) {
return new FirstlegInboundClient.OrderApprovedInboundCommand(
order.getTenantId(), order.getId(), order.getOrderNo(), order.getCustomerId(), order.getCustomerName(),
order.getReceivingWarehouseId(), order.getReceivingWarehouseName(), order.getDestinationWarehouseId(),
order.getDestinationWarehouseName(), order.getExpectedShipDate() == null ? null : order.getExpectedShipDate().atStartOfDay(),
order.getDestinationType() + "_" + order.getTransportMode(), order.getChannelCode(), order.getChannelName(),
order.getCarrierId(), order.getCarrierName(),
"BUY_DECLARATION".equals(order.getTaxMode()) ? "TAX_INCLUDED" : "TAX_EXCLUDED",
order.getContainerMode(), order.getCustomsInspection(), order.getDestinationType(),
order.getDestinationWarehouseId(), order.getAmazonWarehouseCode(), order.getDestinationWarehouseName(),
order.getCustomerRemark(), order.getExpressCompany(), order.getExpressTrackingNo());
}
private String nextNo(long orderId) {
return "FL" + orderId;
}
@@ -0,0 +1,47 @@
package com.baiyue.order.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.order.client.FirstlegInboundClient;
import com.baiyue.order.entity.OutboxEventEntity;
import com.baiyue.order.repository.OrderRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/** 可靠投递订单审核事件;接收端以订单唯一键保证重复投递安全。 */
@Component
public class OrderInboundOutboxPublisher {
private final OrderRepository repository;
private final FirstlegInboundClient client;
private final ObjectMapper json;
private final String internalToken;
public OrderInboundOutboxPublisher(OrderRepository repository, FirstlegInboundClient client,
ObjectMapper json,
@Value("${baiyue.security.internal-service-token}") String internalToken) {
this.repository = repository;
this.client = client;
this.json = json;
this.internalToken = internalToken;
}
@Scheduled(fixedDelayString = "${baiyue.order.inbound-outbox.fixed-delay-ms:3000}")
public void publishApprovedOrders() {
for (OutboxEventEntity event : repository.pendingInboundEvents(50)) {
try {
FirstlegInboundClient.OrderApprovedInboundCommand command = json.readValue(
event.getPayloadJson(), FirstlegInboundClient.OrderApprovedInboundCommand.class);
ApiResponse<Long> response = client.create(internalToken, command);
if (response == null || !response.successful()) {
throw new IllegalStateException("头程入库服务返回失败");
}
repository.markOutboxPublished(event.getId(), LocalDateTime.now());
} catch (Exception exception) {
repository.markOutboxRetry(event.getId(), LocalDateTime.now().plusSeconds(10));
}
}
}
}
+2
View File
@@ -26,6 +26,8 @@ CREATE TABLE IF NOT EXISTS ord_order (
receiving_time VARCHAR(128) NULL COMMENT '国内收货仓收货时间快照',
channel_code VARCHAR(64) NULL COMMENT '渠道编码',
channel_name VARCHAR(128) NULL COMMENT '渠道名称快照',
carrier_id BIGINT UNSIGNED NULL COMMENT '船司逻辑ID快照',
carrier_name VARCHAR(128) NULL COMMENT '船司名称快照',
tax_mode VARCHAR(24) NULL COMMENT '买单或退税',
container_mode VARCHAR(24) NULL COMMENT '整柜或散货',
customs_inspection TINYINT(1) NULL COMMENT '是否商检',
@@ -0,0 +1,24 @@
SET NAMES utf8mb4;
USE baiyue_order;
SET @carrier_id_exists := (
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'ord_order' AND column_name = 'carrier_id'
);
SET @carrier_id_sql := IF(@carrier_id_exists = 0,
'ALTER TABLE ord_order ADD COLUMN carrier_id BIGINT UNSIGNED NULL COMMENT ''船司逻辑ID快照'' AFTER channel_name',
'SELECT 1');
PREPARE carrier_id_statement FROM @carrier_id_sql;
EXECUTE carrier_id_statement;
DEALLOCATE PREPARE carrier_id_statement;
SET @carrier_name_exists := (
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'ord_order' AND column_name = 'carrier_name'
);
SET @carrier_name_sql := IF(@carrier_name_exists = 0,
'ALTER TABLE ord_order ADD COLUMN carrier_name VARCHAR(128) NULL COMMENT ''船司名称快照'' AFTER carrier_id',
'SELECT 1');
PREPARE carrier_name_statement FROM @carrier_name_sql;
EXECUTE carrier_name_statement;
DEALLOCATE PREPARE carrier_name_statement;