优化x
This commit is contained in:
+1
-1
@@ -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}`;客户入口必须按认证客户隔离 |
|
||||
|
||||
+10
@@ -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]
|
||||
|
||||
+10
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user