Merge branch 'test' into dev

This commit is contained in:
2026-09-03 21:30:08 +08:00
25 changed files with 949 additions and 58 deletions
@@ -0,0 +1,49 @@
package com.baiyue.order.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.dto.PendingReviewOrderDtos;
import com.baiyue.order.service.PendingReviewOrderQueryService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
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.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/** Firstleg 智能装柜读取尚未审核订单的内部入口。 */
@RestController
@RequestMapping("/internal/order/v1/pending-review-orders")
public class InternalPendingReviewOrderController {
@Resource private PendingReviewOrderQueryService service;
@Value("${baiyue.security.internal-service-token}") private String configuredToken;
private byte[] expectedToken;
@PostConstruct
void initializeToken() {
expectedToken = configuredToken.getBytes(StandardCharsets.UTF_8);
}
@GetMapping
public ApiResponse<PageResult<PendingReviewOrderDtos.Row>> page(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @ModelAttribute PendingReviewOrderDtos.Query query) {
requireToken(token);
return ApiResponse.success(service.page(query));
}
private void requireToken(String token) {
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
if (!MessageDigest.isEqual(expectedToken, supplied)) {
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
}
}
}
@@ -0,0 +1,49 @@
package com.baiyue.order.dto;
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.Size;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/** Order 向履约服务提供的待审核订单权威只读契约。 */
public final class PendingReviewOrderDtos {
private PendingReviewOrderDtos() {
}
public record Query(
List<@Size(max = 64) String> channelCodes,
@Min(1) Long warehouseId,
@Min(1) Long customerId,
@Size(max = 40) String inboundCode,
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate createdStart,
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate createdEnd,
@Min(1) long page,
@Min(1) @Max(200) long size
) {
@AssertTrue(message = "创建日期起始日不能晚于结束日")
public boolean isCreatedRangeValid() {
return createdStart == null || createdEnd == null || !createdStart.isAfter(createdEnd);
}
}
public record Row(
@Schema(type = "string", pattern = "^[1-9][0-9]*$") Long orderId,
String orderNo,
String inboundCode,
@Schema(type = "string", pattern = "^[1-9][0-9]*$") Long customerId,
String customerName,
@Schema(type = "string", pattern = "^[1-9][0-9]*$") Long warehouseId,
String channelCode,
String channelName,
int totalPackageCount,
LocalDateTime createdAt,
String mainStatus
) {
}
}
@@ -212,6 +212,26 @@ public class OrderRepository {
return PageResult.from(orderMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
}
/** Order 权威待审核页;只允许查询未删除的 SUBMITTED 头程订单。 */
public PageResult<OrderEntity> pendingReviewPage(List<String> channelCodes, Long warehouseId,
Long customerId, String inboundCode,
LocalDateTime createdFrom,
LocalDateTime createdBefore,
long page, long size) {
LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<OrderEntity>()
.eq(OrderEntity::getDeleted, false)
.eq(OrderEntity::getStatus, OrderStatus.SUBMITTED)
.in(channelCodes != null && !channelCodes.isEmpty(), OrderEntity::getChannelCode, channelCodes)
.eq(warehouseId != null, OrderEntity::getReceivingWarehouseId, warehouseId)
.eq(customerId != null, OrderEntity::getCustomerId, customerId)
.eq(StringUtils.hasText(inboundCode), OrderEntity::getInboundCode, inboundCode)
.ge(createdFrom != null, OrderEntity::getCreatedAt, createdFrom)
.lt(createdBefore != null, OrderEntity::getCreatedAt, createdBefore)
.orderByDesc(OrderEntity::getCreatedAt)
.orderByDesc(OrderEntity::getId);
return PageResult.from(orderMapper.selectPage(new Page<>(page, size), wrapper));
}
public PageResult<OrderEntity> merchantInboundPage(long tenantId, MerchantInboundOrderQuery query) {
com.baiyue.order.entity.OrderStatus effectiveStatus = query.operationStatus() == null
? query.status() : query.operationStatus();
@@ -0,0 +1,65 @@
package com.baiyue.order.service;
import com.baiyue.common.core.api.PageResult;
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.repository.OrderRepository;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/** 为 Firstleg 智能装柜提供 Order 权威待审核订单页。 */
@Service
public class PendingReviewOrderQueryService {
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
@Resource private OrderRepository orders;
public PageResult<PendingReviewOrderDtos.Row> page(PendingReviewOrderDtos.Query query) {
if (query == null || query.page() < 1 || query.size() < 1 || query.size() > 200
|| !query.isCreatedRangeValid()) {
throw new BizException(ResultCode.VALIDATION_FAILED, "待审核订单查询参数不正确");
}
List<String> channelCodes = normalizeChannelCodes(query.channelCodes());
LocalDateTime createdFrom = query.createdStart() == null ? null
: query.createdStart().atStartOfDay(BUSINESS_ZONE).toLocalDateTime();
LocalDateTime createdBefore = query.createdEnd() == null ? null
: query.createdEnd().plusDays(1).atStartOfDay(BUSINESS_ZONE).toLocalDateTime();
var source = orders.pendingReviewPage(channelCodes, query.warehouseId(), query.customerId(),
normalize(query.inboundCode()), createdFrom, createdBefore, query.page(), query.size());
List<Long> orderIds = source.records().stream().map(order -> order.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(
order.getId(), order.getOrderNo(), order.getInboundCode(), order.getCustomerId(),
order.getCustomerName(), order.getReceivingWarehouseId(), order.getChannelCode(),
order.getChannelName(), packageCounts.getOrDefault(order.getId(), 0), order.getCreatedAt(),
"PENDING_REVIEW")).toList();
return new PageResult<>(rows, source.page(), source.size(), source.total(), source.pages());
}
private static List<String> normalizeChannelCodes(List<String> values) {
if (values == null || values.isEmpty()) return List.of();
LinkedHashSet<String> normalized = new LinkedHashSet<>();
for (String value : values) {
if (!StringUtils.hasText(value) || value.trim().length() > 64) {
throw new BizException(ResultCode.VALIDATION_FAILED, "渠道编码不能为空且不能超过64个字符");
}
normalized.add(value.trim());
}
return List.copyOf(normalized);
}
private static String normalize(String value) {
return StringUtils.hasText(value) ? value.trim() : null;
}
}
@@ -6,6 +6,34 @@ info:
security:
- bearerAuth: []
paths:
/internal/order/v1/pending-review-orders:
get:
security: []
summary: Firstleg 分页读取 Order 权威待审核订单
description: >-
仅接受内部服务令牌。只返回 ord_order 中未删除且 status=SUBMITTED 的头程订单,
mainStatus 固定为 PENDING_REVIEW。createdStart/createdEnd 按 Asia/Shanghai 自然日闭区间过滤
ord_order.created_at,稳定按 created_at DESC、id DESC 分页;不读取或修复 Firstleg 投影。
parameters:
- name: X-Internal-Service-Token
in: header
required: true
schema: {type: string}
- {name: channelCodes, in: query, required: false, style: form, explode: true, schema: {type: array, items: {type: string, maxLength: 64}}}
- {name: warehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: customerId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: inboundCode, in: query, required: false, schema: {type: string, maxLength: 40}}
- {name: createdStart, in: query, required: false, schema: {type: string, format: date}}
- {name: createdEnd, in: query, required: false, schema: {type: string, format: date}}
- {name: page, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
- {name: size, in: query, required: true, schema: {type: integer, format: int64, minimum: 1, maximum: 200}}
responses:
'200':
description: Order 权威待审核订单页
content:
application/json:
schema: {$ref: '#/components/schemas/PendingReviewOrderPageEnvelope'}
'403': {description: 内部服务令牌无效}
/internal/exports/v1/DOMESTIC_WAREHOUSE_ORDER_LIST/batches:
post:
security: []
@@ -1001,6 +1029,35 @@ components:
description: 前端外层行标识;同一批次内必须唯一,用于幂等重试和结果关联
draft:
$ref: '#/components/schemas/FirstLegOrderDraft'
PendingReviewOrderPageEnvelope:
type: object
properties:
code: {type: string, example: '200'}
message: {type: string}
data:
type: object
required: [records, page, size, total, pages]
properties:
records: {type: array, items: {$ref: '#/components/schemas/PendingReviewOrderRow'}}
page: {type: integer, format: int64, minimum: 1}
size: {type: integer, format: int64, minimum: 1}
total: {type: integer, format: int64, minimum: 0}
pages: {type: integer, format: int64, minimum: 0}
PendingReviewOrderRow:
type: object
required: [orderId, orderNo, inboundCode, customerId, customerName, warehouseId, channelCode, channelName, totalPackageCount, createdAt, mainStatus]
properties:
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
orderNo: {type: string}
inboundCode: {type: string}
customerId: {type: string, pattern: '^[1-9][0-9]*$'}
customerName: {type: string}
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
channelCode: {type: string}
channelName: {type: string}
totalPackageCount: {type: integer, minimum: 0}
createdAt: {type: string, format: date-time}
mainStatus: {type: string, enum: [PENDING_REVIEW]}
FirstLegBatchDraftResponse:
type: object
required: [total, succeeded, failed, items]