This commit is contained in:
2026-09-15 11:54:14 +08:00
parent 3fb9909dd3
commit c1f01e1e87
56 changed files with 973 additions and 231 deletions
+1
View File
@@ -19,6 +19,7 @@
| 申报同步、渠道履约/计价门禁 | `service/OrderFulfillmentChannelChangeGuard.java`、`OrderPricingChannelGuard.java`、`OrderInboundOutboxPublisher.java`,内部客户端 `FirstlegInboundClient.java`、`FirstlegPricingClient.java`;审核后通过版本化 `ORDER_DECLARATION_UPDATED_V1` 同步 Firstleg |
| 入仓码业务关系摘要 | `service/InboundCodeAssociationService.java`、`dto/InboundCodeAssociationSummary.java`、平台/客户 `FirstLegOrderController` 的 `/inbound-code-associations/{inboundCode}`;客户入口必须按认证客户隔离 |
| 订单编排/状态/幂等 | `service/OrderApplicationService.java`、`entity/OrderStatus.java` |
| 海外派送完成内部回写 | `controller/InternalOrderFulfillmentCompletionController.java`、`service/OrderFulfillmentCompletionService.java`;只接受 Firstleg 内部服务身份和已校验的来源订单/入仓码快照,幂等将对应订单推进为 `COMPLETED` 并写状态历史 |
| 商户订单列表、批量草稿与中文 XLSX 导入 | `service/MerchantInboundOrderListService.java`、`MerchantBatchDraftService.java`、`MerchantBatchDraftItemService.java`、`MerchantBatchImportService.java`;动态目录客户端 `client/MerchantWarehouseCatalogClient.java`、`MerchantPricingCatalogClient.java`,客户地址由 `CustomerAddressService.java` 提供;接口见 `controller/MerchantFirstLegOrderController.java` 的 `/batch-imports/template`、`/batch-imports` |
| 入库号生成 | `config/InboundCodeModeProvider.java`、`mapper/InboundCodeSequenceMapper.java`、`repository/OrderRepository.java`、`service/OrderApplicationService.java` |
| 下单计价渠道校验 | `service/OrderPricingChannelGuard.java`、`client/FirstlegPricingClient.java` |
@@ -0,0 +1,36 @@
package com.baiyue.order.controller;
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.service.OrderFulfillmentCompletionService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@RestController
@RequiredArgsConstructor
public class InternalOrderFulfillmentCompletionController {
private final OrderFulfillmentCompletionService service;
@Value("${baiyue.security.internal-service-token}") private String internalToken;
private byte[] expectedToken;
@PostConstruct void initializeToken() { expectedToken = internalToken.getBytes(StandardCharsets.UTF_8); }
@PostMapping("/internal/order/v1/fulfillment-completions")
public ApiResponse<OrderFulfillmentCompletionService.Result> complete(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@RequestBody OrderFulfillmentCompletionService.Request request) {
if (token == null || !MessageDigest.isEqual(expectedToken, token.getBytes(StandardCharsets.UTF_8))) {
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
}
return ApiResponse.success(service.complete(request));
}
}
@@ -12,6 +12,9 @@ import java.util.Map;
*/
@Mapper
public interface OrderMapper extends BaseMapper<OrderEntity>{
@Select("SELECT * FROM ord_order WHERE tenant_id=#{tenantId} AND id=#{orderId} AND deleted=0 FOR UPDATE")
OrderEntity lockOwned(@Param("tenantId") long tenantId, @Param("orderId") long orderId);
@Select("""
SELECT COUNT(*) AS total_submitted_orders,
COALESCE(SUM(CASE WHEN submitted_at >= #{monthStart}
@@ -0,0 +1,76 @@
package com.baiyue.order.service;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.entity.OrderStatusHistoryEntity;
import com.baiyue.order.mapper.OrderMapper;
import com.baiyue.order.mapper.OrderStatusHistoryMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Objects;
import java.util.Set;
/** 接受国内 Firstleg 验证后的海外派送完成事实,推进对应订单。 */
@Service
public class OrderFulfillmentCompletionService {
private static final Set<OrderStatus> COMPLETABLE = Set.of(
OrderStatus.APPROVED, OrderStatus.ACCEPTED, OrderStatus.PROCESSING);
@Resource private OrderMapper orders;
@Resource private OrderStatusHistoryMapper histories;
@Transactional
public Result complete(Request request) {
long tenantId = positive(request.tenantId(), "客户 ID 无效");
long orderId = positive(request.orderId(), "订单 ID 无效");
OrderEntity order = orders.lockOwned(tenantId, orderId);
if (order == null) throw new BizException(ResultCode.NOT_FOUND, "来源订单不存在");
if (!Objects.equals(order.getOrderNo(), request.orderNo())
|| !Objects.equals(order.getInboundCode(), request.inboundCode())) {
throw new BizException(ResultCode.CONFLICT, "派送完成事件与来源订单标识不一致");
}
if (order.getStatus() == OrderStatus.COMPLETED) {
return new Result(orderId, order.getStatus().name(), true);
}
if (!COMPLETABLE.contains(order.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "来源订单当前状态不允许完成: " + order.getStatus());
}
OrderStatus from = order.getStatus();
LocalDateTime completedAt = LocalDateTime.ofInstant(request.completedAt(), ZoneOffset.UTC);
int version = order.getVersion() == null ? 0 : order.getVersion();
int updated = orders.update(null, new LambdaUpdateWrapper<OrderEntity>()
.eq(OrderEntity::getTenantId, tenantId).eq(OrderEntity::getId, orderId)
.eq(OrderEntity::getVersion, version).eq(OrderEntity::getDeleted, false)
.set(OrderEntity::getStatus, OrderStatus.COMPLETED)
.set(OrderEntity::getCompletedAt, completedAt)
.set(OrderEntity::getUpdatedBy, 0L).set(OrderEntity::getUpdatedAt, completedAt)
.setSql("version=version+1"));
if (updated != 1) throw new BizException(ResultCode.CONFLICT, "来源订单版本已变化,请重试");
OrderStatusHistoryEntity history = new OrderStatusHistoryEntity();
history.setId(IdWorker.getId()); history.setTenantId(tenantId); history.setOrderId(orderId);
history.setFromStatus(from); history.setToStatus(OrderStatus.COMPLETED);
history.setAction("OVERSEAS_DELIVERY_COMPLETED");
history.setReason("海外派送单 " + request.deliveryNo() + " 已签收");
history.setOperatorId(0L); history.setOperatorName("OVERSEAS_EVENT_BRIDGE");
history.setOccurredAt(completedAt); histories.insert(history);
return new Result(orderId, OrderStatus.COMPLETED.name(), false);
}
private static long positive(String value, String message) {
try { long parsed = Long.parseLong(value); if (parsed > 0) return parsed; }
catch (RuntimeException ignored) { }
throw new BizException(ResultCode.BAD_REQUEST, message);
}
public record Request(String tenantId, String orderId, String orderNo, String inboundCode,
String deliveryNo, java.time.Instant completedAt, String eventId,
String payloadHash) { }
public record Result(Long orderId, String status, boolean replayed) { }
}
@@ -6,6 +6,37 @@ info:
security:
- bearerAuth: []
paths:
/internal/order/v1/fulfillment-completions:
post:
security: []
summary: Firstleg 回传海外派送完成并结束对应订单
description: >-
仅接受内部服务令牌。请求必须携带海外事件已验证的来源订单 ID、订单号、入仓码、
FBA 派送单号和完成时间;只推进该派送单关联的订单,COMPLETED 重放返回成功。
parameters:
- name: X-Internal-Service-Token
in: header
required: true
schema: {type: string}
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [tenantId, orderId, orderNo, inboundCode, deliveryNo, completedAt, eventId, payloadHash]
properties:
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
orderNo: {type: string}
inboundCode: {type: string}
deliveryNo: {type: string}
completedAt: {type: string, format: date-time}
eventId: {type: string, format: uuid}
payloadHash: {type: string, pattern: '^[0-9a-fA-F]{64}$'}
responses:
'200': {description: 对应订单已完成或幂等重放成功}
'403': {description: 内部服务令牌无效}
/internal/order/v1/cargoes/{tenantId}/{orderId}:
get:
security: []