Test #12

Merged
admin merged 27 commits from test into main 2026-09-20 19:48:44 +08:00
16 changed files with 246 additions and 5 deletions
Showing only changes of commit 11e5c6b769 - Show all commits
Vendored
+5
View File
@@ -56,6 +56,8 @@ def commonPathServices = [
]
def databaseMigrationPaths = ['deploy/sql/']
// 暂停数据库结构只读门禁;保留实现,完成 Credentials 配置后改为 true。
def databaseSchemaContractEnabled = false
def downstreamResults = [:]
def downstreamBuildUrls = [:]
@@ -370,6 +372,9 @@ pipeline {
}
stage('数据库结构只读门禁') {
when {
expression { databaseSchemaContractEnabled }
}
agent { label 'java-builder' }
steps {
script {
+1
View File
@@ -163,6 +163,7 @@ services:
FILE_DB_USERNAME: ${FILE_DB_USERNAME}
FILE_DB_PASSWORD: ${FILE_DB_PASSWORD}
FILE_STORAGE_ROOT: /data/files
PDA_UPDATE_PUBLIC_BASE_URL: ${PDA_UPDATE_PUBLIC_BASE_URL:-}
JAVA_TOOL_OPTIONS: "-Xms1g -Xmx2g -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8"
# Compose 会插值整份文件,即使只部署 auth 等其他服务;File 的必填校验由 deploy-services.sh 按服务执行。
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER-}
@@ -4,6 +4,8 @@ import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
@@ -29,6 +31,13 @@ public interface PendingReviewOrderClient {
@RequestParam long page,
@RequestParam long size);
@PostMapping("/batch")
ApiResponse<List<Row>> batch(
@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody BatchRequest request);
record BatchRequest(List<Long> orderIds) {}
record Row(Long orderId, String orderNo, String inboundCode, Long customerId,
String customerName, Long warehouseId, String channelCode, String channelName,
int totalPackageCount, LocalDateTime createdAt, LocalDateTime expectedArrivalAt,
@@ -13,6 +13,11 @@ import static com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRo
/** 同柜不可拆组在 Firstleg 单库内的唯一持久化入口。 */
@Mapper
public interface ColoadGroupMapper {
@Select({"<script>", "SELECT DISTINCT order_id FROM fl_inbound_order WHERE deleted=0 AND order_id IN",
"<foreach collection='orderIds' item='orderId' open='(' separator=',' close=')'>",
"#{orderId}", "</foreach>", "</script>"})
List<Long> existingOrderIds(@Param("orderIds") List<Long> orderIds);
@Select({"<script>", """
SELECT o.tenant_id tenantId,o.order_id orderId,o.order_no orderNo,
o.id inboundId,o.inbound_code inboundCode,o.process_status processStatus,
@@ -13,6 +13,10 @@ import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.Comm
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.GroupRow;
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.InboundCandidateRow;
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
import com.baiyue.firstleg.client.OrderCargoClient;
import com.baiyue.firstleg.client.PendingReviewOrderClient;
import com.baiyue.firstleg.service.InboundOrderCreationService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -41,6 +45,10 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
@Resource
private ColoadGroupMapper mapper;
@Resource private PendingReviewOrderClient pendingReviewOrders;
@Resource private OrderCargoClient orderCargoClient;
@Resource private InboundOrderCreationService inboundOrderCreationService;
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
@Transactional
public GroupSnapshot create(List<Long> requestedOrderIds, String idempotencyKey,
@@ -51,6 +59,7 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
CommandClaim claim = claim(operatorId, "CREATE", key, requestHash);
if (claim.replayGroupId() != null) return requiredSnapshot(claim.replayGroupId());
preparePendingReviewInbounds(orderIds);
List<InboundCandidateRow> candidates = authoritativeCandidates(orderIds, null);
validateSameRoute(candidates);
Long lockedPlanId = validateAllocationState(candidates);
@@ -265,6 +274,37 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
return resolved;
}
private void preparePendingReviewInbounds(List<Long> orderIds) {
Set<Long> existing = Set.copyOf(mapper.existingOrderIds(orderIds));
List<Long> missing = orderIds.stream().filter(orderId -> !existing.contains(orderId)).toList();
if (missing.isEmpty()) return;
try {
var response = pendingReviewOrders.batch(internalToken,
new PendingReviewOrderClient.BatchRequest(missing));
if (response == null || !response.successful() || response.data() == null) throw unavailable();
for (PendingReviewOrderClient.Row row : response.data()) {
if (row == null || row.orderId() == null || !missing.contains(row.orderId())
|| row.customerId() == null || !"PENDING_REVIEW".equals(row.mainStatus())) {
throw conflict("待审核订单权威快照已变化,请刷新后重试");
}
var snapshotResponse = orderCargoClient.inboundSnapshot(
internalToken, row.customerId(), row.orderId());
if (snapshotResponse == null || !snapshotResponse.successful()
|| snapshotResponse.data() == null
|| snapshotResponse.data().tenantId() != row.customerId()
|| snapshotResponse.data().orderId() != row.orderId()
|| !Objects.equals(snapshotResponse.data().inboundCode(), row.inboundCode())) {
throw conflict("待审核订单履约快照已变化,请刷新后重试");
}
inboundOrderCreationService.prepareFromDraft(snapshotResponse.data());
}
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
throw unavailable();
}
}
private String unavailableReason(InboundCandidateRow row, Long allowedGroupId) {
if (row.getTenantId() == null || row.getTenantId() <= 0
|| row.getInboundId() == null || row.getInboundId() <= 0
@@ -470,6 +510,10 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
return new BizException(ResultCode.CONFLICT, message);
}
private static BizException unavailable() {
return new BizException(ResultCode.INTERNAL_ERROR, "待审核订单权威服务暂不可用");
}
private record CommandClaim(long commandId, Long replayGroupId) {
}
}
@@ -9,7 +9,8 @@ paths:
summary: 创建同柜不可拆组
description: >-
需要 platform:firstleg:smart-loading:assign。orderIds 至少2项;服务端解析每个订单处于待审核、
待测量/测量中、待分柜或装柜中的全部有效入仓片段,要求国内仓和渠道一致,并冻结全部片段及其满箱数。
待测量/测量中、待分柜或装柜中的全部有效入仓片段;待审核订单先经 Order 批量权威核验并幂等准备
PREPARING 入库投影。全部成员要求国内仓和渠道一致,并冻结全部片段及其满箱数。
全部成员未进入装柜计划时创建 ACTIVE 组;全部成员已经按满箱数完整进入同一个装柜计划时直接创建并
锁定为 LOCKED 组。仅部分成员进柜、分散在不同装柜计划、箱数不完整、已属于其他活动组或权威仓库/
渠道快照缺失时拒绝。跨成员仓库或渠道不一致时返回入仓码及仓库名/渠道名;平台错误不得展示
@@ -1,6 +1,11 @@
package com.baiyue.firstleg.loading.coload;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.client.OrderCargoClient;
import com.baiyue.firstleg.client.PendingReviewOrderClient;
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
import com.baiyue.firstleg.service.InboundOrderCreationService;
import com.baiyue.firstleg.loading.coload.persistence.ColoadGroupMapper;
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.GroupRow;
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.CommandRow;
@@ -12,6 +17,7 @@ import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.Orde
import com.baiyue.firstleg.loading.coload.service.ColoadGroupService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@@ -35,9 +41,19 @@ import static org.mockito.Mockito.when;
class ColoadGroupServiceTest {
@MockBean
private ColoadGroupMapper mapper;
@MockBean private PendingReviewOrderClient pendingReviewOrders;
@MockBean private OrderCargoClient orderCargoClient;
@MockBean private InboundOrderCreationService inboundOrderCreationService;
@Resource
private ColoadGroupService service;
@BeforeEach
void allowNoPendingReviewProjectionByDefault() {
when(mapper.existingOrderIds(any())).thenAnswer(invocation -> invocation.getArgument(0));
when(pendingReviewOrders.batch(anyString(), any(PendingReviewOrderClient.BatchRequest.class)))
.thenReturn(ApiResponse.success(List.of()));
}
@Test
void readsCrossCustomerTwoMemberGroupByEitherInboundCodeWithoutInternalIdentifiers() {
when(mapper.groupsByInboundCode("IN-A")).thenReturn(List.of(lookup(500L, "ACTIVE")));
@@ -154,6 +170,31 @@ class ColoadGroupServiceTest {
verify(mapper).insertMember(anyLong(), eq(groupId.getValue()), eq(202L), eq(12L), eq(1002L), eq(50));
}
@Test
void provisionsSelectedPendingReviewOrderBeforeResolvingColoadCandidates() {
PendingReviewOrderClient.Row pending = new PendingReviewOrderClient.Row(
11L, "FO-11", "IN-1001", 101L, "客户", 9L, "CH", "美森海卡",
30, LocalDateTime.now(), null, "PENDING_REVIEW");
OrderApprovedInboundCommand snapshot = new OrderApprovedInboundCommand(
101L, 11L, "FO-11", "IN-1001", 101L, "客户", 9L, "义乌仓",
null, null, null, "FIRST_LEG", "CH", "美森海卡", null, null,
null, null, null, false, "FBA", null, null, null, null, null, null, 30);
when(pendingReviewOrders.batch(anyString(), any(PendingReviewOrderClient.BatchRequest.class)))
.thenReturn(ApiResponse.success(List.of(pending)));
when(mapper.existingOrderIds(List.of(11L, 12L))).thenReturn(List.of(12L));
when(orderCargoClient.inboundSnapshot(anyString(), eq(101L), eq(11L)))
.thenReturn(ApiResponse.success(snapshot));
when(inboundOrderCreationService.prepareFromDraft(snapshot)).thenReturn(1001L);
stubSuccessfulCreate("pending-review", List.of(
candidate(101L, 11L, 1001L, 30), candidate(202L, 12L, 1002L, 50)), "ACTIVE", null);
var result = service.create(List.of(11L, 12L), "pending-review", 7L, "operator");
assertThat(result.status()).isEqualTo("ACTIVE");
verify(inboundOrderCreationService).prepareFromDraft(snapshot);
verify(pendingReviewOrders).batch(anyString(), eq(new PendingReviewOrderClient.BatchRequest(List.of(11L))));
}
@org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.ValueSource(strings = {
"PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
@@ -49,6 +49,9 @@ class ColoadGroupSqlContractTest {
.contains("FROM fl_inbound_order o")
.contains("o.order_no orderNo", "o.inbound_code inboundCode")
.contains("o.warehouse_name warehouseName", "o.channel_name channelName")
.contains("activeAllocationPlanCount", "activeAllocationPlanId", "activeAllocatedPackageCount")
.contains("COUNT(DISTINCT item.loading_plan_id)", "MIN(item.loading_plan_id)")
.contains("COALESCE(SUM(item.package_count),0)")
.contains("FROM fl_loading_plan_item item")
.contains("item.deleted=0")
.contains("member.active_flag=1")
@@ -63,6 +66,15 @@ class ColoadGroupSqlContractTest {
.doesNotContain("destination_code");
}
@Test
void mapperDetectsExistingFirstlegProjectionsBeforeCallingThePendingReviewOwner() throws Exception {
Method method = ColoadGroupMapper.class.getDeclaredMethod("existingOrderIds", java.util.List.class);
String sql = String.join(" ", method.getAnnotation(Select.class).value());
assertThat(sql).contains("SELECT DISTINCT order_id FROM fl_inbound_order")
.contains("deleted=0", "order_id IN")
.doesNotContain("FOR UPDATE");
}
@Test
void inboundCodeReadKeepsConsumedGroupsAndAllFrozenEffectiveFragments() throws Exception {
Method lookup = ColoadGroupMapper.class.getDeclaredMethod("groupsByInboundCode", String.class);
+1 -1
View File
@@ -14,7 +14,7 @@
| 平台订单 API | `controller/PlatformFirstLegOrderController.java` |
| 平台批量审核预览 | `controller/PlatformFirstLegOrderController.java` 的 `/batch-review-preview`、`service/BatchOrderReviewPreviewService.java`、`dto/BatchOrderReviewPreviewResponse.java`;按目标客户和请求顺序一次返回订单、中文货物摘要、渠道、目的仓及逐项 `canApprove/blockReason`,实现固定两次批量查询,禁止前端逐票读取详情 |
| 统一订单查询、审核优先状态与稳定渠道编码 | `service/UnifiedOrderQueryService.java`、`dto/UnifiedOrderResponse.java`、`src/main/resources/openapi/order-v1.yaml`;`channelCode` 直接来自 Order/Firstleg 权威快照,只用于业务校验,不得由 `channelName` 推导。Order 为 `SUBMITTED` 时即使已有 Firstleg 入库投影,统一主状态/下一动作仍固定为 `PENDING_REVIEW/APPROVE_ORDER` |
| Firstleg 智能装柜待审核候选 | `controller/InternalPendingReviewOrderController.java`、`service/PendingReviewOrderQueryService.java`、`dto/PendingReviewOrderDtos.java`、`repository/OrderRepository.java`;内部入口 `/internal/order/v1/pending-review-orders` 只接受服务令牌,只分页返回未删除的 `SUBMITTED` 头程订单并固定投影为 `PENDING_REVIEW`,按 `Asia/Shanghai` 自然日过滤、`created_at DESC, id DESC` 稳定排序,不读取或修复 Firstleg 投影 |
| Firstleg 智能装柜待审核候选 | `controller/InternalPendingReviewOrderController.java`、`service/PendingReviewOrderQueryService.java`、`dto/PendingReviewOrderDtos.java`、`repository/OrderRepository.java`;内部入口 `/internal/order/v1/pending-review-orders` 只接受服务令牌,只分页返回未删除的 `SUBMITTED` 头程订单并固定投影为 `PENDING_REVIEW`,按 `Asia/Shanghai` 自然日过滤、`created_at DESC, id DESC` 稳定排序;`POST /batch` 按最多 100 个订单 ID 批量核验待审核状态,供同柜建组幂等准备 Firstleg `PREPARING` 投影 |
| 平台版本化修改客户原始申报 | `controller/PlatformUnifiedOrderController.java` 的 `PUT /api/platform/v1/orders/{orderId}/declaration`、`service/OrderDeclarationUpdateService.java`、`dto/OrderDeclarationDtos.java`;权限 `platform:order:declaration:edit`,货物以订单内 `itemNo` 定位并保持行集合完整,契约 `src/main/resources/openapi/order-v1.yaml` |
| 申报同步、渠道履约/计价门禁 | `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}`;客户入口必须按认证客户隔离 |
@@ -12,6 +12,8 @@ import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -40,6 +42,14 @@ public class InternalPendingReviewOrderController {
return ApiResponse.success(service.page(query));
}
@PostMapping("/batch")
public ApiResponse<java.util.List<PendingReviewOrderDtos.Row>> batch(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody PendingReviewOrderDtos.BatchRequest request) {
requireToken(token);
return ApiResponse.success(service.batch(request.orderIds()));
}
private void requireToken(String token) {
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
if (!MessageDigest.isEqual(expectedToken, supplied)) {
@@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.springframework.format.annotation.DateTimeFormat;
@@ -41,6 +43,11 @@ public final class PendingReviewOrderDtos {
}
}
public record BatchRequest(
@NotEmpty @Size(min = 1, max = 100) List<@NotNull @Min(1) Long> orderIds
) {
}
public record Row(
@Schema(type = "string", pattern = "^[1-9][0-9]*$") Long orderId,
String orderNo,
@@ -5,6 +5,8 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.dto.PendingReviewOrderDtos;
import com.baiyue.order.entity.OrderCargoEntity;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -40,16 +42,33 @@ public class PendingReviewOrderQueryService {
var source = orders.pendingReviewPage(channelCodes, query.warehouseId(), query.customerId(),
normalize(query.inboundCode()), createdFrom, createdBefore, expectedArrivalFrom,
expectedArrivalBefore, query.excludedOrderIds(), query.page(), query.size());
List<Long> orderIds = source.records().stream().map(order -> order.getId()).toList();
return new PageResult<>(rows(source.records()), source.page(), source.size(), source.total(), source.pages());
}
public List<PendingReviewOrderDtos.Row> batch(List<Long> requestedOrderIds) {
if (requestedOrderIds == null || requestedOrderIds.isEmpty() || requestedOrderIds.size() > 100
|| requestedOrderIds.stream().anyMatch(java.util.Objects::isNull)
|| requestedOrderIds.stream().anyMatch(id -> id <= 0)
|| requestedOrderIds.stream().distinct().count() != requestedOrderIds.size()) {
throw new BizException(ResultCode.VALIDATION_FAILED, "待审核订单ID必须为1至100个不同的正整数");
}
Map<Long, OrderEntity> byId = orders.findByIds(null, requestedOrderIds).stream()
.filter(order -> !Boolean.TRUE.equals(order.getDeleted()))
.filter(order -> order.getStatus() == OrderStatus.SUBMITTED)
.collect(Collectors.toMap(OrderEntity::getId, order -> order));
return rows(requestedOrderIds.stream().map(byId::get).filter(java.util.Objects::nonNull).toList());
}
private List<PendingReviewOrderDtos.Row> rows(List<OrderEntity> source) {
List<Long> orderIds = source.stream().map(OrderEntity::getId).toList();
Map<Long, Integer> packageCounts = orders.findCargoByOrderIds(null, orderIds).stream()
.collect(Collectors.groupingBy(OrderCargoEntity::getOrderId,
Collectors.summingInt(cargo -> cargo.getPackageCount() == null ? 0 : cargo.getPackageCount())));
var rows = source.records().stream().map(order -> new PendingReviewOrderDtos.Row(
return source.stream().map(order -> new PendingReviewOrderDtos.Row(
order.getId(), order.getOrderNo(), order.getInboundCode(), order.getCustomerId(),
order.getCustomerName(), order.getReceivingWarehouseId(), order.getChannelCode(),
order.getChannelName(), packageCounts.getOrDefault(order.getId(), 0), order.getCreatedAt(),
order.getExpectedArrivalAt(), "PENDING_REVIEW")).toList();
return new PageResult<>(rows, source.page(), source.size(), source.total(), source.pages());
}
private static List<String> normalizeChannelCodes(List<String> values) {
@@ -137,6 +137,30 @@ paths:
application/json:
schema: {$ref: '#/components/schemas/PendingReviewOrderPageEnvelope'}
'403': {description: 内部服务令牌无效}
/internal/order/v1/pending-review-orders/batch:
post:
security: []
summary: Firstleg 按订单 ID 批量核验 Order 权威待审核订单
description: >-
仅接受内部服务令牌。只返回请求集合中未删除且 status=SUBMITTED 的订单,按请求顺序返回;
供订单列表创建同柜组前幂等准备 Firstleg PREPARING 入库投影,不返回已审核或失效订单。
parameters:
- name: X-Internal-Service-Token
in: header
required: true
schema: {type: string}
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/PendingReviewOrderBatchRequest'}
responses:
'200':
description: Order 权威待审核订单集合
content:
application/json:
schema: {$ref: '#/components/schemas/PendingReviewOrderListEnvelope'}
'403': {description: 内部服务令牌无效}
/internal/exports/v1/DOMESTIC_WAREHOUSE_ORDER_LIST/batches:
post:
security: []
@@ -1293,6 +1317,23 @@ components:
size: {type: integer, format: int64, minimum: 1}
total: {type: integer, format: int64, minimum: 0}
pages: {type: integer, format: int64, minimum: 0}
PendingReviewOrderBatchRequest:
type: object
additionalProperties: false
required: [orderIds]
properties:
orderIds:
type: array
minItems: 1
maxItems: 100
uniqueItems: true
items: {type: string, pattern: '^[1-9][0-9]*$'}
PendingReviewOrderListEnvelope:
type: object
properties:
code: {type: string, example: '200'}
message: {type: string}
data: {type: array, items: {$ref: '#/components/schemas/PendingReviewOrderRow'}}
PendingReviewOrderRow:
type: object
required: [orderId, orderNo, inboundCode, customerId, customerName, warehouseId, channelCode, channelName, totalPackageCount, createdAt, expectedArrivalAt, mainStatus]
@@ -35,4 +35,14 @@ class InternalPendingReviewOrderControllerTest {
assertThat(controller.page("0123456789abcdef0123456789abcdef", query).data()).isSameAs(page);
verify(service).page(query);
}
@Test
void batchUsesTheSameInternalAuthenticationBoundary() {
var request = new PendingReviewOrderDtos.BatchRequest(List.of(101L, 102L));
when(service.batch(request.orderIds())).thenReturn(List.of());
assertThatThrownBy(() -> controller.batch("wrong", request)).isInstanceOf(BizException.class);
assertThat(controller.batch("0123456789abcdef0123456789abcdef", request).data()).isEmpty();
verify(service).batch(request.orderIds());
}
}
@@ -13,12 +13,15 @@ class PendingReviewOrderOpenApiContractTest {
String yaml = Files.readString(Path.of("src/main/resources/openapi/order-v1.yaml"));
assertThat(yaml)
.contains("/internal/order/v1/pending-review-orders:")
.contains("/internal/order/v1/pending-review-orders/batch:")
.contains("channelCodes")
.contains("createdStart")
.contains("createdEnd")
.contains("expectedArrivalStart")
.contains("expectedArrivalEnd")
.contains("excludedOrderIds")
.contains("PendingReviewOrderBatchRequest:")
.contains("PendingReviewOrderListEnvelope:")
.contains("PendingReviewOrderRow:")
.contains("expectedArrivalAt: {type: string, format: date-time, nullable: true}")
.contains("mainStatus: {type: string, enum: [PENDING_REVIEW]}")
@@ -5,6 +5,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.dto.PendingReviewOrderDtos;
import com.baiyue.order.entity.OrderCargoEntity;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -90,6 +91,38 @@ class PendingReviewOrderQueryServiceTest {
null, null, null, null, null, List.of(), 1, 20))).isInstanceOf(BizException.class);
}
@Test
void batchReturnsOnlyRequestedSubmittedOrdersInRequestOrder() {
OrderEntity submitted = order(101L, OrderStatus.SUBMITTED);
OrderEntity approved = order(102L, OrderStatus.APPROVED);
OrderEntity deleted = order(103L, OrderStatus.SUBMITTED); deleted.setDeleted(true);
when(repository.findByIds(null, List.of(103L, 101L, 102L))).thenReturn(List.of(approved, submitted, deleted));
when(repository.findCargoByOrderIds(null, List.of(101L))).thenReturn(List.of(cargo(101L, 4)));
var result = service.batch(List.of(103L, 101L, 102L));
assertThat(result).singleElement().satisfies(row -> {
assertThat(row.orderId()).isEqualTo(101L);
assertThat(row.totalPackageCount()).isEqualTo(4);
assertThat(row.mainStatus()).isEqualTo("PENDING_REVIEW");
});
}
@Test
void batchRejectsDuplicateOrInvalidOrderIds() {
assertThatThrownBy(() -> service.batch(List.of(101L, 101L))).isInstanceOf(BizException.class);
assertThatThrownBy(() -> service.batch(List.of(0L))).isInstanceOf(BizException.class);
}
private static OrderEntity order(long id, OrderStatus status) {
OrderEntity order = new OrderEntity();
order.setId(id); order.setStatus(status); order.setDeleted(false);
order.setOrderNo("FL" + id); order.setInboundCode("RK" + id);
order.setCustomerId(22L); order.setCustomerName("测试客户");
order.setReceivingWarehouseId(33L); order.setChannelCode("SEA-US"); order.setChannelName("美线");
return order;
}
private static OrderCargoEntity cargo(long orderId, int packageCount) {
OrderCargoEntity cargo = new OrderCargoEntity();
cargo.setOrderId(orderId); cargo.setPackageCount(packageCount);