Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # order/AGENTS.md
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
| 下单仓库主数据守卫 | `service/OrderMasterDataGuard.java`、`client/WarehouseClient.java`;校验公共收货仓、目的仓类型、状态、地址与收货能力,并保存服务端快照 |
|
||||
| 统一订单七状态与操作映射 | `../../docs/统一订单主状态与操作契约.md`;`LOADED` 硬门禁和 `DELIVERED` 可信回传同时核对三期需求确认表与开工门禁 |
|
||||
| 货物内部查询与箱唛绑定来源 | `controller/InternalOrderCargoController.java` 向 Firstleg 提供箱唛生成的 SKU 快照来源;`CargoLabelBinding.skuNo` 对 `ONE_CLICK_DROPSHIP` 为强制字段。Firstleg 消费端为 `../firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java`,变更两端 DTO 必须同步。 |
|
||||
| 智能装柜待审核候选内部查询 | `controller/InternalPendingReviewOrderController.java`、`service/PendingReviewOrderQueryService.java`、`dto/PendingReviewOrderDtos.java`;Firstleg 消费端为 `../firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java`,契约 `src/main/resources/openapi/order-v1.yaml` |
|
||||
| 快照/Outbox | `service/`、`repository/`、`event/` |
|
||||
| DTO/持久化 | `dto/`、`entity/`、`mapper/`、`repository/` |
|
||||
| SQL/OpenAPI | `src/main/resources/db/`、`src/main/resources/openapi/` |
|
||||
@@ -34,6 +35,7 @@
|
||||
- `inbound_code` 是跨客户全局唯一业务码;生成模式由 `InboundCodeModeProvider` 控制,序列与唯一约束不得退化为客户内唯一。修改格式、长度或生成时机时同步检查 Order SQL、Firstleg 入库快照、箱唛箱码、前端展示和全局唯一性契约测试。
|
||||
- 下单和后续关键状态操作不得信任客户端仓库名称、类型或启用状态;公共仓库必须通过 Warehouse 内部接口按 ID 实时校验。私人地址不得伪装为公共目的仓,非私人目的地必须引用类型匹配的公共仓库;Warehouse 不可用时失败关闭,不回退到旧快照或模拟数据。
|
||||
- Warehouse 往来单位、Firstleg 入库协作只通过客户端 DTO/内部令牌;修改契约需同步消费者。
|
||||
- 智能装柜待审核候选只返回 Order 权威 `SUBMITTED` 订单,并按服务身份、客户/仓库/渠道、入仓码、创建日期和分页条件查询;Firstleg 可据此提前占位,但不得把该内部查询或申报数量当作审核、测量或可物理装柜完成的事实。
|
||||
- 中文批量模板按登录客户生成取货/私人地址、国内收货仓、目的仓和渠道下拉;仓库目录必须翻页读取全部候选,不得只取第一页。导入时重新读取并校验动态目录、客户归属和启用状态,不信任模板隐藏页;文件上限 5MB、有效数据最多 50 行,仅创建草稿,`PARTIAL` 允许单行失败、`ATOMIC` 任一行失败不得落单。
|
||||
- 平台只能在 `DRAFT/REJECTED/SUBMITTED/APPROVED` 版本化修改客户原始申报;不得修改客户、国内收货仓、订单号、入仓码、状态或仓库/PDA/设备实测、测量审核和计费事实,不得增删或重排货物行。国内收货仓或中转仓变化必须重新下单。
|
||||
- 只有渠道发生变化才同时调用 Firstleg 履约可逆性与重新计价门禁,任一内部校验不可用或失败均不写入;成功后 Outbox 必须携带完整申报快照和字段变化。Firstleg 消费按 `eventId + payload hash` 幂等、按订单版本单调推进,渠道变化才使未确认价格预估失效。
|
||||
|
||||
@@ -23,6 +23,7 @@ public final class PendingReviewOrderDtos {
|
||||
@Size(max = 40) String inboundCode,
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate createdStart,
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate createdEnd,
|
||||
List<@Min(1) Long> excludedOrderIds,
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size
|
||||
) {
|
||||
|
||||
@@ -217,6 +217,7 @@ public class OrderRepository {
|
||||
Long customerId, String inboundCode,
|
||||
LocalDateTime createdFrom,
|
||||
LocalDateTime createdBefore,
|
||||
List<Long> excludedOrderIds,
|
||||
long page, long size) {
|
||||
LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<OrderEntity>()
|
||||
.eq(OrderEntity::getDeleted, false)
|
||||
@@ -227,6 +228,8 @@ public class OrderRepository {
|
||||
.eq(StringUtils.hasText(inboundCode), OrderEntity::getInboundCode, inboundCode)
|
||||
.ge(createdFrom != null, OrderEntity::getCreatedAt, createdFrom)
|
||||
.lt(createdBefore != null, OrderEntity::getCreatedAt, createdBefore)
|
||||
.notIn(excludedOrderIds != null && !excludedOrderIds.isEmpty(), OrderEntity::getId,
|
||||
excludedOrderIds)
|
||||
.orderByDesc(OrderEntity::getCreatedAt)
|
||||
.orderByDesc(OrderEntity::getId);
|
||||
return PageResult.from(orderMapper.selectPage(new Page<>(page, size), wrapper));
|
||||
|
||||
@@ -34,7 +34,8 @@ public class PendingReviewOrderQueryService {
|
||||
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());
|
||||
normalize(query.inboundCode()), createdFrom, createdBefore, query.excludedOrderIds(),
|
||||
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,
|
||||
|
||||
@@ -14,6 +14,7 @@ paths:
|
||||
仅接受内部服务令牌。只返回 ord_order 中未删除且 status=SUBMITTED 的头程订单,
|
||||
mainStatus 固定为 PENDING_REVIEW。createdStart/createdEnd 按 Asia/Shanghai 自然日闭区间过滤
|
||||
ord_order.created_at,稳定按 created_at DESC、id DESC 分页;不读取或修复 Firstleg 投影。
|
||||
excludedOrderIds 由 Firstleg 根据当前有效装柜计划提供,用于在数据库分页前排除已经占柜的订单。
|
||||
parameters:
|
||||
- name: X-Internal-Service-Token
|
||||
in: header
|
||||
@@ -25,6 +26,7 @@ paths:
|
||||
- {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: excludedOrderIds, in: query, required: false, style: form, explode: true, schema: {type: array, items: {type: string, pattern: '^[1-9][0-9]*$'}}}
|
||||
- {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:
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@ class InternalPendingReviewOrderControllerTest {
|
||||
|
||||
@Test
|
||||
void acceptsOnlyTheConfiguredInternalServiceToken() {
|
||||
var query = new PendingReviewOrderDtos.Query(List.of("SEA-US"), null, null, null, null, null, 1, 20);
|
||||
var query = new PendingReviewOrderDtos.Query(List.of("SEA-US"), null, null, null, null, null,
|
||||
List.of(101L), 1, 20);
|
||||
var page = new PageResult<PendingReviewOrderDtos.Row>(List.of(), 1, 20, 0, 0);
|
||||
when(service.page(query)).thenReturn(page);
|
||||
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ class PendingReviewOrderOpenApiContractTest {
|
||||
.contains("channelCodes")
|
||||
.contains("createdStart")
|
||||
.contains("createdEnd")
|
||||
.contains("excludedOrderIds")
|
||||
.contains("PendingReviewOrderRow:")
|
||||
.contains("mainStatus: {type: string, enum: [PENDING_REVIEW]}")
|
||||
.contains("orderId: {type: string, pattern: '^[1-9][0-9]*$'}")
|
||||
|
||||
+3
-1
@@ -38,7 +38,8 @@ class PendingReviewOrderRepositoryTest {
|
||||
void buildsSubmittedOnlyStablePageWithEverySupportedFilter() throws Exception {
|
||||
when(orderMapper.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(new Page<>());
|
||||
repository.pendingReviewPage(List.of("SEA-US", "AIR-US"), 11L, 22L, "RK-1",
|
||||
LocalDateTime.of(2026, 9, 1, 0, 0), LocalDateTime.of(2026, 9, 4, 0, 0), 3, 40);
|
||||
LocalDateTime.of(2026, 9, 1, 0, 0), LocalDateTime.of(2026, 9, 4, 0, 0),
|
||||
List.of(101L, 102L), 3, 40);
|
||||
|
||||
ArgumentCaptor<Page<OrderEntity>> page = ArgumentCaptor.forClass(Page.class);
|
||||
ArgumentCaptor<Wrapper<OrderEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
|
||||
@@ -53,6 +54,7 @@ class PendingReviewOrderRepositoryTest {
|
||||
.contains(".eq(OrderEntity::getStatus, OrderStatus.SUBMITTED)")
|
||||
.contains(".in(channelCodes != null && !channelCodes.isEmpty(), OrderEntity::getChannelCode, channelCodes)")
|
||||
.contains(".lt(createdBefore != null, OrderEntity::getCreatedAt, createdBefore)")
|
||||
.contains(".notIn(excludedOrderIds != null && !excludedOrderIds.isEmpty(), OrderEntity::getId,")
|
||||
.contains(".orderByDesc(OrderEntity::getCreatedAt)")
|
||||
.contains(".orderByDesc(OrderEntity::getId)");
|
||||
}
|
||||
|
||||
+8
-7
@@ -36,16 +36,16 @@ class PendingReviewOrderQueryServiceTest {
|
||||
|
||||
@Test
|
||||
void convertsShanghaiNaturalDatesToClosedDayRangeAndNormalizesFilters() {
|
||||
when(repository.pendingReviewPage(any(), any(), any(), any(), any(), any(), anyLong(), anyLong()))
|
||||
when(repository.pendingReviewPage(any(), any(), any(), any(), any(), any(), any(), anyLong(), anyLong()))
|
||||
.thenReturn(new PageResult<>(List.of(), 2, 20, 0, 0));
|
||||
|
||||
service.page(new PendingReviewOrderDtos.Query(List.of(" SEA-US ", "SEA-US"), 11L, 22L,
|
||||
" RK-1 ", LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 3), 2, 20));
|
||||
" RK-1 ", LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 3), List.of(101L), 2, 20));
|
||||
|
||||
@SuppressWarnings("unchecked") ArgumentCaptor<List<String>> channels = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).pendingReviewPage(channels.capture(), eq(11L), eq(22L), eq("RK-1"),
|
||||
eq(LocalDateTime.of(2026, 9, 1, 0, 0)),
|
||||
eq(LocalDateTime.of(2026, 9, 4, 0, 0)), eq(2L), eq(20L));
|
||||
eq(LocalDateTime.of(2026, 9, 4, 0, 0)), eq(List.of(101L)), eq(2L), eq(20L));
|
||||
assertThat(channels.getValue()).containsExactly("SEA-US");
|
||||
}
|
||||
|
||||
@@ -58,11 +58,12 @@ class PendingReviewOrderQueryServiceTest {
|
||||
order.setCreatedAt(LocalDateTime.of(2026, 9, 3, 10, 30));
|
||||
OrderCargoEntity first = cargo(101L, 2);
|
||||
OrderCargoEntity second = cargo(101L, 3);
|
||||
when(repository.pendingReviewPage(any(), any(), any(), any(), any(), any(), anyLong(), anyLong()))
|
||||
when(repository.pendingReviewPage(any(), any(), any(), any(), any(), any(), any(), anyLong(), anyLong()))
|
||||
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
|
||||
when(repository.findCargoByOrderIds(null, List.of(101L))).thenReturn(List.of(first, second));
|
||||
|
||||
var result = service.page(new PendingReviewOrderDtos.Query(null, null, null, " ", null, null, 1, 20));
|
||||
var result = service.page(new PendingReviewOrderDtos.Query(null, null, null, " ", null, null,
|
||||
List.of(), 1, 20));
|
||||
|
||||
assertThat(result.records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.orderId()).isEqualTo(101L);
|
||||
@@ -75,9 +76,9 @@ class PendingReviewOrderQueryServiceTest {
|
||||
@Test
|
||||
void rejectsReversedDatesAndBlankChannelValues() {
|
||||
assertThatThrownBy(() -> service.page(new PendingReviewOrderDtos.Query(null, null, null, null,
|
||||
LocalDate.of(2026, 9, 4), LocalDate.of(2026, 9, 3), 1, 20))).isInstanceOf(BizException.class);
|
||||
LocalDate.of(2026, 9, 4), LocalDate.of(2026, 9, 3), List.of(), 1, 20))).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> service.page(new PendingReviewOrderDtos.Query(List.of(" "), null, null,
|
||||
null, null, null, 1, 20))).isInstanceOf(BizException.class);
|
||||
null, null, null, List.of(), 1, 20))).isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
private static OrderCargoEntity cargo(long orderId, int packageCount) {
|
||||
|
||||
Reference in New Issue
Block a user