优化
This commit is contained in:
@@ -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` |
|
||||
|
||||
+36
@@ -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: []
|
||||
|
||||
Reference in New Issue
Block a user