This commit is contained in:
2026-08-07 10:45:09 +08:00
parent 897f91af20
commit 830575cc92
17 changed files with 295 additions and 28 deletions
@@ -53,7 +53,9 @@ public class InternalInboundOrderController {
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
}
if (command.tenantId() <= 0 || command.orderId() <= 0 || command.warehouseId() == null
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()) {
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()
|| command.inboundCode() == null || command.inboundCode().isBlank()
|| command.inboundCode().length() > 40) {
throw new BizException(ResultCode.BAD_REQUEST, "订单入库快照不完整");
}
return ApiResponse.success(service.createFromApprovedOrder(command));
@@ -7,6 +7,7 @@ public record OrderApprovedInboundCommand(
long tenantId,
long orderId,
String orderNo,
String inboundCode,
Long customerId,
String customerName,
Long warehouseId,
@@ -38,7 +38,7 @@ public class InboundOrderCreationService {
inbound.setTenantId(command.tenantId());
inbound.setOrderId(command.orderId());
inbound.setOrderNo(command.orderNo());
inbound.setInboundCode("RK-" + command.orderId());
inbound.setInboundCode(command.inboundCode());
inbound.setProcessStatus("WAIT_MEASURE");
inbound.setOrderStatusSnapshot("APPROVED");
inbound.setMeasureReviewStatus("NOT_SUBMITTED");
@@ -6,6 +6,19 @@ info:
security:
- bearerAuth: []
paths:
/internal/firstleg/v1/inbounds/order-approved:
post:
summary: 订单审核通过后幂等创建入库单
description: 仅供订单服务调用;入仓码由订单提交时生成,Firstleg 必须原样沿用。承运商可在后续履约或计费前补充。
security:
- internalServiceToken: []
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/OrderApprovedInboundCommand'}
responses:
'200': {description: 成功或业务失败;相同客户和订单重放返回已有入库单 ID}
/api/platform/v1/first-leg/tasks:
get:
summary: 分页查询履约任务
@@ -224,6 +237,10 @@ components:
in: header
name: X-Signature
description: HMAC-SHA256 小写十六进制签名
internalServiceToken:
type: apiKey
in: header
name: X-Internal-Service-Token
parameters:
XTimestamp:
name: X-Timestamp
@@ -259,6 +276,36 @@ components:
in: query
schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
schemas:
OrderApprovedInboundCommand:
type: object
required: [tenantId, orderId, inboundCode, customerName, warehouseId]
properties:
tenantId: {type: string, pattern: '^[1-9][0-9]*$', description: 当前客户隔离 ID}
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
orderNo: {type: string, nullable: true}
inboundCode: {type: string, minLength: 1, maxLength: 40, example: RK-2085308606829346818}
customerId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
customerName: {type: string, minLength: 1}
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
warehouseName: {type: string, nullable: true}
deliveryWarehouseId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
deliveryWarehouseName: {type: string, nullable: true}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
businessType: {type: string, nullable: true}
channelCode: {type: string, nullable: true}
channelName: {type: string, nullable: true}
carrierId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: 可选,可在后续履约或计费前补充}
carrierName: {type: string, nullable: true}
taxMode: {type: string, nullable: true}
containerMode: {type: string, nullable: true}
customsInspection: {type: boolean, nullable: true}
destinationType: {type: string, nullable: true}
destinationWarehouseId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
destinationCode: {type: string, nullable: true}
destinationName: {type: string, nullable: true}
publicRemark: {type: string, nullable: true}
expressCompanyName: {type: string, nullable: true}
expressTrackingNo: {type: string, nullable: true}
MerchantInboundBillingEnvelope:
type: object
properties:
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.InboundOrderReference;
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
import com.baiyue.firstleg.service.InboundOrderCreationService;
import com.baiyue.firstleg.service.UnifiedInboundQueryService;
import com.baiyue.firstleg.dto.UnifiedInboundQuery;
@@ -63,4 +64,28 @@ class InternalInboundOrderReferenceControllerTest {
.isInstanceOf(BizException.class);
verify(unifiedQueryService).page(request);
}
@Test
void approvedOrderRequiresInboundCode() {
assertThatThrownBy(() -> controller.createFromApprovedOrder(
"internal-token", approvedCommand(" ", null)))
.isInstanceOf(BizException.class);
}
@Test
void approvedOrderAllowsMissingCarrier() {
OrderApprovedInboundCommand command = approvedCommand("RK-77", null);
when(service.createFromApprovedOrder(command)).thenReturn(88L);
assertThat(controller.createFromApprovedOrder("internal-token", command).data()).isEqualTo(88L);
verify(service).createFromApprovedOrder(command);
}
private static OrderApprovedInboundCommand approvedCommand(String inboundCode, Long carrierId) {
return new OrderApprovedInboundCommand(
10001L, 77L, "FL-77", inboundCode, 7L, "测试客户", 9L, "义乌仓",
null, null, null, "FBA", "AIR", "空运", carrierId,
carrierId == null ? null : "承运商", null, null, null, null,
null, null, null, null, null, null);
}
}
@@ -0,0 +1,59 @@
package com.baiyue.firstleg.service;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class InboundOrderCreationServiceTest {
@Mock
private InboundOrderMapper orders;
@Mock
private InboundStatusHistoryMapper histories;
@InjectMocks
private InboundOrderCreationService service;
@Test
void shouldUseSubmittedInboundCodeAndKeepReplayIdempotentWithoutCarrier() {
OrderApprovedInboundCommand command = approvedCommand("RK-CUSTOM-77");
InboundOrderEntity existing = new InboundOrderEntity();
existing.setId(900L);
existing.setTenantId(10001L);
existing.setOrderId(77L);
existing.setInboundCode("RK-CUSTOM-77");
when(orders.selectOne(any(Wrapper.class))).thenReturn(null, existing);
long createdId = service.createFromApprovedOrder(command);
long replayedId = service.createFromApprovedOrder(command);
ArgumentCaptor<InboundOrderEntity> inboundCaptor = ArgumentCaptor.forClass(InboundOrderEntity.class);
verify(orders, times(1)).insert(inboundCaptor.capture());
verify(histories, times(1)).insert(any(InboundStatusHistoryEntity.class));
assertThat(inboundCaptor.getValue().getInboundCode()).isEqualTo("RK-CUSTOM-77");
assertThat(inboundCaptor.getValue().getCarrierId()).isNull();
assertThat(createdId).isEqualTo(inboundCaptor.getValue().getId());
assertThat(replayedId).isEqualTo(900L);
}
private static OrderApprovedInboundCommand approvedCommand(String inboundCode) {
return new OrderApprovedInboundCommand(
10001L, 77L, "FL-77", inboundCode, 7L, "测试客户", 9L, "义乌仓",
null, null, null, "FBA", "AIR", "空运", null, null,
null, null, null, null, null, null, null, null, null, null);
}
}