二期内容提交
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -180,7 +180,7 @@ class OrderApplicationServiceStateTest {
|
||||
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", "渠道",
|
||||
party, party, List.of(cargo), "PICKUP", 8L, "收货仓", "CH", "渠道", 9L, "船司",
|
||||
"BUY_DECLARATION", "LCL", false, "OVERSEAS", null, null, null);
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ class OrderApplicationServiceStateTest {
|
||||
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", "渠道",
|
||||
sender, receiver, List.of(cargo), pickupMethod, 8L, "收货仓", "CH", "渠道", 9L, "船司",
|
||||
"BUY_DECLARATION", "LCL", false, destinationType, null, null, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ class OrderMasterDataGuardTest {
|
||||
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);
|
||||
return new OrderRequests.SaveRequest(partner,"不可信客户名","SEA",null,"CN",destinationWarehouseId,"私人地址",null,null,party,party,List.of(cargo),"PICKUP",warehouse,"不可信收货仓","CH","渠道","BUY_DECLARATION","LCL",false,destinationType,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","LCL",false,destinationType,null,null,null);
|
||||
}
|
||||
private WarehouseClient.Detail warehouse(long id,String type,boolean receiving) {
|
||||
var value=new WarehouseClient.Warehouse(id,PUBLIC_TENANT_ID,"WH-"+id,"仓库"+id,type,"CN","ACTIVE");
|
||||
|
||||
Reference in New Issue
Block a user