From aa583efa4a25dd253d10921eb9e34182127cd73c Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 31 Aug 2026 10:09:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlatformColoadGroupController.java | 21 ++--- .../coload/model/ColoadGroupModels.java | 20 ++-- .../coload/persistence/ColoadGroupMapper.java | 29 ++++-- .../persistence/ColoadPersistenceRows.java | 10 +- .../coload/service/ColoadGroupService.java | 34 ++++--- .../resources/openapi/coload-groups-v1.yaml | 57 ++++++----- .../coload/ColoadGroupContractTest.java | 12 ++- .../ColoadGroupOpenApiContractTest.java | 16 ++-- .../coload/ColoadGroupServiceTest.java | 63 +++++++++++++ .../coload/ColoadGroupSqlContractTest.java | 19 ++++ .../client/FirstlegColoadGroupClient.java | 23 +++++ .../PlatformUnifiedOrderController.java | 8 +- .../order/dto/UnifiedOrderResponse.java | 6 ++ .../service/UnifiedOrderQueryService.java | 94 ++++++++++++++++--- 14 files changed, 320 insertions(+), 92 deletions(-) create mode 100644 order/src/main/java/com/baiyue/order/client/FirstlegColoadGroupClient.java diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/controller/PlatformColoadGroupController.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/controller/PlatformColoadGroupController.java index 501793054..6e6c86bec 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/controller/PlatformColoadGroupController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/controller/PlatformColoadGroupController.java @@ -5,11 +5,10 @@ import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.common.security.AuthenticatedUser; -import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.BatchQueryRequest; +import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.BusinessSummary; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CancelRequest; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CreateRequest; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot; -import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.OrderGroupInfo; import com.baiyue.firstleg.loading.coload.service.ColoadGroupService; import com.baiyue.firstleg.service.FirstlegScopeGuard; import io.swagger.v3.oas.annotations.Operation; @@ -17,9 +16,11 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -27,8 +28,6 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - /** 平台订单列表使用的同柜不可拆组管理接口。 */ @Validated @RestController @@ -69,15 +68,15 @@ public class PlatformColoadGroupController { idempotencyKey, user.userId(), user.username())); } - @PostMapping("/batch-query") - @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')") - @Operation(summary = "按订单ID批量查询同柜不可拆组", - description = "供平台订单列表批量补齐组号、状态、全体成员和整组满箱数;最多200个订单。") - public ApiResponse> batchQuery( + @GetMapping("/by-inbound-code/{inboundCode}") + @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')") + @Operation(summary = "按入仓码查询同柜组业务摘要", + description = "仅返回业务状态、有效成员数及去重后的全部成员入仓码;内部组ID和组号不作为页面识别键。") + public ApiResponse byInboundCode( @AuthenticationPrincipal AuthenticatedUser user, - @Valid @RequestBody BatchQueryRequest request) { + @PathVariable @Size(min = 1, max = 40) String inboundCode) { requirePlatform(user); - return ApiResponse.success(service.queryByOrderIds(request.orderIds())); + return ApiResponse.success(service.queryByInboundCode(inboundCode)); } private static void requirePlatform(AuthenticatedUser user) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/model/ColoadGroupModels.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/model/ColoadGroupModels.java index e5754a9e5..d5bf0f24a 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/model/ColoadGroupModels.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/model/ColoadGroupModels.java @@ -24,11 +24,6 @@ public final class ColoadGroupModels { ) { } - public record BatchQueryRequest( - @NotEmpty @Size(max = 200) List<@NotNull @Min(1) Long> orderIds - ) { - } - public record Member( long tenantId, long orderId, @@ -65,9 +60,18 @@ public final class ColoadGroupModels { } } - public record OrderGroupInfo( - long orderId, - GroupSnapshot group + public record BusinessMember(String inboundCode) { + } + + /** 页面只读业务摘要;内部组标识不作为用户识别键。 */ + public record BusinessSummary( + String status, + int memberCount, + List members ) { + public BusinessSummary { + members = members == null ? List.of() : List.copyOf(members); + memberCount = members.size(); + } } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupMapper.java index 74cd842df..be3687bfe 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadGroupMapper.java @@ -85,15 +85,28 @@ public interface ColoadGroupMapper { GroupRow activeGroupByInbound(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId); - @Select({""}) - List linksByOrderIds(@Param("orderIds") List orderIds); + WHERE requested.deleted=0 AND requested.inbound_code=#{inboundCode} + AND group_row.group_status IN ('ACTIVE','LOCKED','CONSUMED') + ORDER BY group_row.id + """) + List groupsByInboundCode(@Param("inboundCode") String inboundCode); + + @Select(""" + SELECT DISTINCT inbound.inbound_code inboundCode + FROM fl_coload_group_member member + JOIN fl_inbound_order inbound + ON inbound.tenant_id=member.tenant_id AND inbound.id=member.inbound_id + WHERE member.group_id=#{groupId} AND inbound.deleted=0 + AND inbound.inbound_code IS NOT NULL AND TRIM(inbound.inbound_code)<>'' + ORDER BY inbound.inbound_code + """) + List effectiveMemberCodes(@Param("groupId") long groupId); @Update(""" UPDATE fl_coload_group diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadPersistenceRows.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadPersistenceRows.java index 934c495fe..b549529cd 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadPersistenceRows.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/persistence/ColoadPersistenceRows.java @@ -49,9 +49,15 @@ public final class ColoadPersistenceRows { @Getter @Setter - public static class OrderGroupLinkRow { - private Long orderId; + public static class InboundGroupLookupRow { private Long groupId; + private String groupStatus; + } + + @Getter + @Setter + public static class MemberCodeRow { + private String inboundCode; } @Getter diff --git a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/service/ColoadGroupService.java b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/service/ColoadGroupService.java index 1a2b3617b..28bac1578 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/service/ColoadGroupService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/loading/coload/service/ColoadGroupService.java @@ -5,7 +5,8 @@ import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member; -import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.OrderGroupInfo; +import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.BusinessMember; +import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.BusinessSummary; import com.baiyue.firstleg.loading.coload.persistence.ColoadGroupMapper; import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.CommandRow; import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.GroupRow; @@ -89,17 +90,28 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort { } @Transactional(readOnly = true) - public List queryByOrderIds(List requestedOrderIds) { - List orderIds = normalizedOrderIds(requestedOrderIds, 1, 200); - Map latest = new LinkedHashMap<>(); - Map groups = new LinkedHashMap<>(); - for (var link : mapper.linksByOrderIds(orderIds)) { - if (latest.containsKey(link.getOrderId())) continue; - GroupSnapshot group = groups.computeIfAbsent(link.getGroupId(), this::requiredSnapshot); - if (!CURRENT_GROUP_STATUSES.contains(group.status())) continue; - latest.put(link.getOrderId(), new OrderGroupInfo(link.getOrderId(), group)); + public BusinessSummary queryByInboundCode(String inboundCode) { + String code = trim(inboundCode); + if (code == null || code.length() > 40) throw badRequest("inboundCode不能为空且不能超过40字符"); + var groups = mapper.groupsByInboundCode(code).stream() + .filter(row -> row.getGroupId() != null && CURRENT_GROUP_STATUSES.contains(row.getGroupStatus())) + .toList(); + long distinctGroups = groups.stream().map(row -> row.getGroupId()).distinct().limit(2).count(); + if (distinctGroups == 0) { + throw new BizException(ResultCode.NOT_FOUND, "入仓码未关联有效同柜不可拆组"); } - return orderIds.stream().map(latest::get).filter(Objects::nonNull).toList(); + if (distinctGroups > 1) throw conflict("入仓码关联多个有效同柜不可拆组"); + long groupId = groups.get(0).getGroupId(); + String status = groups.get(0).getGroupStatus(); + List members = mapper.effectiveMemberCodes(groupId).stream() + .map(row -> trim(row.getInboundCode())) + .filter(Objects::nonNull) + .distinct() + .sorted() + .map(BusinessMember::new) + .toList(); + if (members.isEmpty()) throw conflict("同柜不可拆组不存在有效入仓片段"); + return new BusinessSummary(status, members.size(), members); } @Override diff --git a/firstleg/src/main/resources/openapi/coload-groups-v1.yaml b/firstleg/src/main/resources/openapi/coload-groups-v1.yaml index 17373be8b..e763a19ff 100644 --- a/firstleg/src/main/resources/openapi/coload-groups-v1.yaml +++ b/firstleg/src/main/resources/openapi/coload-groups-v1.yaml @@ -35,17 +35,17 @@ paths: responses: '200': {$ref: '#/components/responses/ColoadGroupResponse'} '403': {description: 缺少智能排柜分配权限} - /api/platform/v1/first-leg/coload-groups/batch-query: - post: - summary: 按订单ID批量查询同柜不可拆组 + /api/platform/v1/first-leg/coload-groups/by-inbound-code/{inboundCode}: + get: + summary: 按入仓码查询同柜组业务摘要 description: >- - 需要 platform:firstleg:smart-loading:assign。最多200个订单;返回当前非取消组的组号、状态、 - 全部成员和整组满箱数,供平台订单列表批量补齐,不按单成员修改。 - requestBody: - required: true - content: - application/json: - schema: {$ref: '#/components/schemas/BatchQueryColoadGroupRequest'} + 需要 platform:firstleg:smart-loading:list。使用任一有效成员的全局唯一 inboundCode 查询; + 仅返回组业务状态、有效成员数和去重后的全部成员入仓码。页面不持有或展示 groupId、groupNo、orderId。 + parameters: + - name: inboundCode + in: path + required: true + schema: {type: string, minLength: 1, maxLength: 40} responses: '200': description: 业务响应 @@ -56,11 +56,8 @@ paths: - {$ref: '#/components/schemas/ApiEnvelope'} - type: object properties: - data: - type: array - maxItems: 200 - items: {$ref: '#/components/schemas/OrderColoadGroupInfo'} - '403': {description: 缺少智能排柜分配权限} + data: {$ref: '#/components/schemas/ColoadGroupBusinessSummary'} + '403': {description: 缺少智能排柜列表权限} components: parameters: IdempotencyKey: @@ -107,17 +104,6 @@ components: properties: expectedVersion: {type: integer, minimum: 0} reason: {type: string, maxLength: 500, nullable: true} - BatchQueryColoadGroupRequest: - type: object - additionalProperties: false - required: [orderIds] - properties: - orderIds: - type: array - minItems: 1 - maxItems: 200 - uniqueItems: true - items: {type: string, pattern: '^[1-9][0-9]*$'} ColoadGroupMember: type: object additionalProperties: false @@ -147,10 +133,21 @@ components: fullPackageCount: {type: integer, minimum: 2, description: 全部成员满箱数之和} createdAt: {type: string, format: date-time} updatedAt: {type: string, format: date-time} - OrderColoadGroupInfo: + ColoadGroupBusinessMember: type: object additionalProperties: false - required: [orderId, group] + required: [inboundCode] properties: - orderId: {type: string, pattern: '^[1-9][0-9]*$'} - group: {$ref: '#/components/schemas/ColoadGroup'} + inboundCode: {type: string, minLength: 1, maxLength: 40} + ColoadGroupBusinessSummary: + type: object + additionalProperties: false + required: [status, memberCount, members] + properties: + status: {type: string, enum: [ACTIVE, LOCKED, CONSUMED]} + memberCount: {type: integer, minimum: 1} + members: + type: array + minItems: 1 + uniqueItems: true + items: {$ref: '#/components/schemas/ColoadGroupBusinessMember'} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupContractTest.java index 3f66f4b9e..ddd25ddb1 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupContractTest.java @@ -2,13 +2,13 @@ package com.baiyue.firstleg.loading.coload; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.firstleg.loading.coload.controller.PlatformColoadGroupController; -import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.BatchQueryRequest; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CancelRequest; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CreateRequest; import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort; import org.junit.jupiter.api.Test; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import java.lang.reflect.Method; @@ -25,9 +25,15 @@ class ColoadGroupContractTest { assertEndpoint("create", "", AuthenticatedUser.class, String.class, CreateRequest.class); assertEndpoint("cancel", "/{groupId}/cancel", AuthenticatedUser.class, long.class, String.class, CancelRequest.class); - assertEndpoint("batchQuery", "/batch-query", AuthenticatedUser.class, BatchQueryRequest.class); + Method read = PlatformColoadGroupController.class.getDeclaredMethod( + "byInboundCode", AuthenticatedUser.class, String.class); + assertThat(read.getAnnotation(GetMapping.class).value()) + .containsExactly("/by-inbound-code/{inboundCode}"); + assertThat(read.getAnnotation(PreAuthorize.class).value()) + .isEqualTo("hasAuthority('platform:firstleg:smart-loading:list')"); assertThat(Arrays.stream(PlatformColoadGroupController.class.getDeclaredMethods()) - .map(Method::getName)).doesNotContain("addMember", "removeMember", "updateMember"); + .map(Method::getName)).doesNotContain( + "batchQuery", "addMember", "removeMember", "updateMember"); } @Test diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupOpenApiContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupOpenApiContractTest.java index 16c98477d..74c8aa868 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupOpenApiContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupOpenApiContractTest.java @@ -12,23 +12,25 @@ import static org.assertj.core.api.Assertions.assertThat; class ColoadGroupOpenApiContractTest { @SuppressWarnings("unchecked") @Test - void publishesBoundedLongIdGroupContractWithoutNewPermission() throws Exception { + void publishesInboundCodeBusinessSummaryWithoutExposingInternalGroupIdentity() throws Exception { String source = Files.readString(Path.of("src/main/resources/openapi/coload-groups-v1.yaml")); Map yaml = new Yaml().load(source); Map paths = (Map) yaml.get("paths"); Map components = (Map) yaml.get("components"); Map schemas = (Map) components.get("schemas"); - Map orderInfo = (Map) schemas.get("OrderColoadGroupInfo"); - Map orderInfoProperties = (Map) orderInfo.get("properties"); + Map summary = (Map) schemas.get("ColoadGroupBusinessSummary"); + Map summaryProperties = (Map) summary.get("properties"); + Map member = (Map) schemas.get("ColoadGroupBusinessMember"); + Map memberProperties = (Map) member.get("properties"); assertThat(paths).containsKeys( "/api/platform/v1/first-leg/coload-groups", "/api/platform/v1/first-leg/coload-groups/{groupId}/cancel", - "/api/platform/v1/first-leg/coload-groups/batch-query"); + "/api/platform/v1/first-leg/coload-groups/by-inbound-code/{inboundCode}"); + assertThat(paths).doesNotContainKey("/api/platform/v1/first-leg/coload-groups/batch-query"); assertThat(source) .contains("minItems: 2") .contains("maxItems: 100") - .contains("maxItems: 200") .contains("uniqueItems: true") .contains("pattern: '^[1-9][0-9]*$'") .contains("enum: [ACTIVE, LOCKED, CONSUMED, CANCELLED]") @@ -36,12 +38,14 @@ class ColoadGroupOpenApiContractTest { .contains("expectedVersion") .contains("fullPackageCount") .contains("platform:firstleg:smart-loading:assign") + .contains("platform:firstleg:smart-loading:list") .contains("全部 WAIT_SPLIT") .contains("国内仓和渠道一致") .doesNotContain("destinationType") .doesNotContain("destinationWarehouseId") .doesNotContain("destinationCode") .doesNotContain("platform:firstleg:coload"); - assertThat(orderInfoProperties).containsOnlyKeys("orderId", "group"); + assertThat(summaryProperties).containsOnlyKeys("status", "memberCount", "members"); + assertThat(memberProperties).containsOnlyKeys("inboundCode"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupServiceTest.java index 38270d5e4..0f98cfc82 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupServiceTest.java @@ -5,6 +5,8 @@ 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.InboundCandidateRow; import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.MemberRow; +import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.InboundGroupLookupRow; +import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.MemberCodeRow; import com.baiyue.firstleg.loading.coload.service.ColoadGroupService; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; @@ -33,6 +35,54 @@ class ColoadGroupServiceTest { @Resource private ColoadGroupService service; + @Test + void readsCrossCustomerTwoMemberGroupByEitherInboundCodeWithoutInternalIdentifiers() { + when(mapper.groupsByInboundCode("IN-A")).thenReturn(List.of(lookup(500L, "ACTIVE"))); + when(mapper.effectiveMemberCodes(500L)).thenReturn(List.of(code("IN-A"), code("IN-B"))); + + var result = service.queryByInboundCode(" IN-A "); + + assertThat(result.status()).isEqualTo("ACTIVE"); + assertThat(result.memberCount()).isEqualTo(2); + assertThat(result.members()).extracting("inboundCode").containsExactly("IN-A", "IN-B"); + verify(mapper).groupsByInboundCode("IN-A"); + } + + @Test + void returnsEveryEffectiveSplitFragmentAndEliminatesDuplicateInboundCodes() { + when(mapper.groupsByInboundCode("MASTER-A-2")).thenReturn(List.of(lookup(500L, "LOCKED"))); + when(mapper.effectiveMemberCodes(500L)).thenReturn(List.of( + code("MASTER-A-3"), code("MASTER-A-2"), code("MASTER-A-2"), code("ORDER-B"))); + + var result = service.queryByInboundCode("MASTER-A-2"); + + assertThat(result.status()).isEqualTo("LOCKED"); + assertThat(result.memberCount()).isEqualTo(3); + assertThat(result.members()).extracting("inboundCode") + .containsExactly("MASTER-A-2", "MASTER-A-3", "ORDER-B"); + } + + @Test + void preservesConsumedStatusWhileReadingClosedFrozenMemberships() { + when(mapper.groupsByInboundCode("IN-A")).thenReturn(List.of(lookup(500L, "CONSUMED"))); + when(mapper.effectiveMemberCodes(500L)).thenReturn(List.of(code("IN-A"), code("IN-B"))); + + assertThat(service.queryByInboundCode("IN-A").status()).isEqualTo("CONSUMED"); + } + + @Test + void failsClosedWhenGroupHasNoEffectiveMemberOrLookupIsAmbiguous() { + when(mapper.groupsByInboundCode("EMPTY")).thenReturn(List.of(lookup(500L, "ACTIVE"))); + when(mapper.effectiveMemberCodes(500L)).thenReturn(List.of()); + when(mapper.groupsByInboundCode("AMBIGUOUS")).thenReturn(List.of( + lookup(500L, "ACTIVE"), lookup(501L, "LOCKED"))); + + assertThatThrownBy(() -> service.queryByInboundCode("EMPTY")) + .isInstanceOf(BizException.class).hasMessageContaining("不存在有效入仓片段"); + assertThatThrownBy(() -> service.queryByInboundCode("AMBIGUOUS")) + .isInstanceOf(BizException.class).hasMessageContaining("多个有效"); + } + @Test void createsCrossCustomerGroupAndFreezesAllAvailableFragmentsForEveryOrder() { when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of( @@ -209,6 +259,19 @@ class ColoadGroupServiceTest { return value; } + private static InboundGroupLookupRow lookup(long groupId, String status) { + InboundGroupLookupRow value = new InboundGroupLookupRow(); + value.setGroupId(groupId); + value.setGroupStatus(status); + return value; + } + + private static MemberCodeRow code(String inboundCode) { + MemberCodeRow value = new MemberCodeRow(); + value.setInboundCode(inboundCode); + return value; + } + private static org.assertj.core.groups.Tuple tuple(Object... values) { return org.assertj.core.groups.Tuple.tuple(values); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupSqlContractTest.java index eadbb384c..45cd43afc 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/coload/ColoadGroupSqlContractTest.java @@ -56,6 +56,25 @@ class ColoadGroupSqlContractTest { .doesNotContain("destination_code"); } + @Test + void inboundCodeReadKeepsConsumedGroupsAndAllFrozenEffectiveFragments() throws Exception { + Method lookup = ColoadGroupMapper.class.getDeclaredMethod("groupsByInboundCode", String.class); + String lookupSql = String.join(" ", lookup.getAnnotation(Select.class).value()); + Method members = ColoadGroupMapper.class.getDeclaredMethod("effectiveMemberCodes", long.class); + String memberSql = String.join(" ", members.getAnnotation(Select.class).value()); + + assertThat(lookupSql) + .contains("requested.inbound_code=#{inboundCode}") + .contains("requested.deleted=0") + .contains("'ACTIVE','LOCKED','CONSUMED'") + .doesNotContain("group_no", "order_id=#{"); + assertThat(memberSql) + .contains("SELECT DISTINCT inbound.inbound_code inboundCode") + .contains("member.group_id=#{groupId}") + .contains("inbound.deleted=0") + .doesNotContain("member.active_flag=1", "inbound_role", "split_status", "split_purpose"); + } + private static String tableDefinition(String source, String tableName) { int start = source.indexOf("CREATE TABLE IF NOT EXISTS " + tableName + " ("); int end = source.indexOf(';', start); diff --git a/order/src/main/java/com/baiyue/order/client/FirstlegColoadGroupClient.java b/order/src/main/java/com/baiyue/order/client/FirstlegColoadGroupClient.java new file mode 100644 index 000000000..419381208 --- /dev/null +++ b/order/src/main/java/com/baiyue/order/client/FirstlegColoadGroupClient.java @@ -0,0 +1,23 @@ +package com.baiyue.order.client; + +import com.baiyue.common.core.api.ApiResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.List; + +/** Platform-scoped read contract for Firstleg co-load group aggregation. */ +@FeignClient(name = "firstleg-service", contextId = "orderFirstlegColoadGroupClient", + url = "${baiyue.clients.firstleg.url:}", path = "/api/platform/v1/first-leg/coload-groups") +public interface FirstlegColoadGroupClient { + @PostMapping("/batch-query") + ApiResponse> batchQuery(@RequestHeader("Authorization") String authorization, + @RequestBody BatchQueryRequest request); + + record BatchQueryRequest(List orderIds) {} + record OrderGroupInfo(Long orderId, GroupSnapshot group) {} + record GroupSnapshot(Long groupId, String groupNo, String status, List members) {} + record Member(Long tenantId, Long orderId, Long inboundId, Integer fullPackageCount) {} +} diff --git a/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java b/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java index f2705d183..33736b773 100644 --- a/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java +++ b/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java @@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; 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.RestController; import java.util.List; @@ -41,11 +42,12 @@ public class PlatformUnifiedOrderController { @Operation(summary = "统一订单列表", description = "返回七态产品主状态及测量、预装、装柜、派送摘要。") public ApiResponse> page( @AuthenticationPrincipal AuthenticatedUser user, + @RequestHeader("Authorization") String authorization, @RequestParam(required = false) Long tenantId, @Valid UnifiedOrderQuery query) { OrderScopeGuard.platform(user); if (tenantId != null) OrderScopeGuard.platform(user, tenantId); - return ApiResponse.success(service.page(tenantId, query)); + return ApiResponse.success(service.platformPage(tenantId, query, authorization)); } @GetMapping("/{orderId}") @@ -53,9 +55,11 @@ public class PlatformUnifiedOrderController { @Operation(summary = "统一订单详情", description = "合并订单和履约时间线;平台响应包含内部备注。") public ApiResponse detail( @AuthenticationPrincipal AuthenticatedUser user, + @RequestHeader("Authorization") String authorization, @RequestParam long tenantId, @PathVariable long orderId) { - return ApiResponse.success(service.detail(OrderScopeGuard.platform(user, tenantId), orderId, true)); + return ApiResponse.success(service.platformDetail( + OrderScopeGuard.platform(user, tenantId), orderId, authorization)); } @GetMapping("/{orderId}/fulfillment-tree") diff --git a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java index d1784d4cd..b11ee23f5 100644 --- a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java +++ b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderResponse.java @@ -32,6 +32,7 @@ public final class UnifiedOrderResponse { List skuNos, Integer totalQuantity, String shipmentId, String referenceId, MeasurementSummary measurement, PreloadSummary preload, LoadingSummary loading, DeliverySummary delivery, TransportSummary transport, + CoLoadGroupSummary coLoadGroup, String customerRemark, String latestInternalNote, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime requestedPickupAt ) {} @@ -39,6 +40,11 @@ public final class UnifiedOrderResponse { public record OrderRelationSummary(String role, Long rootOrderId, Long parentOrderId, Integer splitSequence, Integer childCount) {} + /** Platform-only Firstleg co-load group view; identifiers stay strings for routing only. */ + public record CoLoadGroupSummary(String groupId, String groupNo, String status, + Integer memberCount, List members) {} + public record CoLoadGroupMemberSummary(String orderId, String inboundCode) {} + public record Detail(Row summary, OrderDetailResponse order, List timeline) {} diff --git a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java index b74fc11cd..36b50aacd 100644 --- a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java +++ b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java @@ -5,6 +5,7 @@ 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.client.FirstlegUnifiedOrderClient; +import com.baiyue.order.client.FirstlegColoadGroupClient; import com.baiyue.order.dto.OrderDetailResponse; import com.baiyue.order.dto.UnifiedOrderQuery; import com.baiyue.order.dto.UnifiedOrderResponse; @@ -44,15 +45,26 @@ public class UnifiedOrderQueryService { @Resource private OrderRepository orders; @Resource private FirstlegUnifiedOrderClient firstleg; + @Resource private FirstlegColoadGroupClient coloadGroups; @Value("${baiyue.security.internal-service-token}") private String internalToken; public PageResult page(Long tenantId, UnifiedOrderQuery query) { + return page(tenantId, query, null); + } + + public PageResult platformPage(Long tenantId, UnifiedOrderQuery query, + String authorization) { + return page(tenantId, query, authorization); + } + + private PageResult page(Long tenantId, UnifiedOrderQuery query, + String platformAuthorization) { validate(query); if (requiresFirstlegQuery(query)) { if ("PENDING_REVIEW".equals(query.mainStatus())) { return new PageResult<>(List.of(), query.page(), query.size(), 0, 0); } - return pageFromFirstleg(tenantId, query); + return pageFromFirstleg(tenantId, query, platformAuthorization); } List statuses = "PENDING_REVIEW".equals(query.mainStatus()) ? List.of(OrderStatus.SUBMITTED) : ACTIVE_ORDER_STATUSES; @@ -64,13 +76,24 @@ public class UnifiedOrderQueryService { Map cargoSummaries = cargoSummaries(tenantId, source.records().stream().map(OrderEntity::getId).toList()); Map childCounts = childCounts(source.records()); + Map coLoadByOrder = coLoadGroups( + source.records().stream().map(OrderEntity::getId).toList(), platformAuthorization); List rows = source.records().stream() .map(order -> row(order, inboundByOrder.get(order.getId()), latestNotes.get(order.getId()), - childCounts, cargoSummaries.get(order.getId()))).toList(); + childCounts, cargoSummaries.get(order.getId()), coLoadByOrder.get(order.getId()))).toList(); return new PageResult<>(rows, source.page(), source.size(), source.total(), source.pages()); } public UnifiedOrderResponse.Detail detail(long tenantId, long orderId, boolean includeInternalNotes) { + return detail(tenantId, orderId, includeInternalNotes, null); + } + + public UnifiedOrderResponse.Detail platformDetail(long tenantId, long orderId, String authorization) { + return detail(tenantId, orderId, true, authorization); + } + + private UnifiedOrderResponse.Detail detail(long tenantId, long orderId, boolean includeInternalNotes, + String platformAuthorization) { OrderEntity order = orders.findOwned(tenantId, orderId); if (order == null) throw new BizException(ResultCode.NOT_FOUND, "订单不存在"); OrderDetailResponse orderDetail = new OrderDetailResponse(order, orders.findCargo(tenantId, orderId), @@ -90,7 +113,8 @@ public class UnifiedOrderQueryService { .findFirst() .orElse(null)); UnifiedOrderResponse.Row summary = row(order, inbound == null ? null : inbound.summary(), latestNote, - childCounts(List.of(order)), CargoSummary.from(orderDetail.cargoes()), creatorName); + childCounts(List.of(order)), CargoSummary.from(orderDetail.cargoes()), creatorName, + coLoadGroups(List.of(orderId), platformAuthorization).get(orderId)); List timeline = new ArrayList<>(); orderDetail.statusHistory().forEach(item -> timeline.add(new UnifiedOrderResponse.TimelineEvent( "ORDER", "STATUS", orderMainStatus(item.getToStatus()), item.getAction(), item.getReason(), @@ -105,7 +129,8 @@ public class UnifiedOrderQueryService { return new UnifiedOrderResponse.Detail(summary, orderDetail, timeline); } - private PageResult pageFromFirstleg(Long tenantId, UnifiedOrderQuery query) { + private PageResult pageFromFirstleg(Long tenantId, UnifiedOrderQuery query, + String platformAuthorization) { List candidateOrderIds = StringUtils.hasText(query.fbaCode()) ? orders.findOrderIdsBySkuNo(tenantId, query.fbaCode()) : null; if (candidateOrderIds != null && candidateOrderIds.isEmpty()) { @@ -126,10 +151,13 @@ public class UnifiedOrderQueryService { Map latestNotes = latestNotes(tenantId, orderIds); Map cargoSummaries = cargoSummaries(tenantId, orderIds); Map childCounts = childCounts(List.copyOf(orderById.values())); + Map coLoadByOrder = + coLoadGroups(orderIds, platformAuthorization); List records = source.records().stream() .map(summary -> row(summary, orderById.get(summary.inbound().orderId()), latestNotes.get(summary.inbound().orderId()), childCounts, - cargoSummaries.get(summary.inbound().orderId()))) + cargoSummaries.get(summary.inbound().orderId()), + coLoadByOrder.get(summary.inbound().orderId()))) .toList(); return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages()); } @@ -168,6 +196,40 @@ public class UnifiedOrderQueryService { .collect(Collectors.toMap(OrderEntity::getParentOrderId, ignored -> 1, Integer::sum)); } + private Map coLoadGroups( + List orderIds, String platformAuthorization) { + if (!StringUtils.hasText(platformAuthorization) || orderIds.isEmpty()) return Map.of(); + List data = require( + call(() -> coloadGroups.batchQuery(platformAuthorization, + new FirstlegColoadGroupClient.BatchQueryRequest(orderIds.stream().distinct().toList()))), + "同柜组摘要响应无效"); + List memberOrderIds = data.stream() + .filter(Objects::nonNull).map(FirstlegColoadGroupClient.OrderGroupInfo::group) + .filter(Objects::nonNull).flatMap(group -> safeMembers(group).stream()) + .map(FirstlegColoadGroupClient.Member::orderId).filter(Objects::nonNull).distinct().toList(); + Map inboundCodeByOrder = orders.findByIds(null, memberOrderIds).stream() + .collect(Collectors.toMap(OrderEntity::getId, OrderEntity::getInboundCode, (left, right) -> left)); + return data.stream().filter(Objects::nonNull) + .filter(info -> info.orderId() != null && info.group() != null) + .collect(Collectors.toMap(FirstlegColoadGroupClient.OrderGroupInfo::orderId, + info -> coLoadGroup(info.group(), inboundCodeByOrder), (left, right) -> left)); + } + + private List safeMembers(FirstlegColoadGroupClient.GroupSnapshot group) { + return group.members() == null ? List.of() : group.members(); + } + + private UnifiedOrderResponse.CoLoadGroupSummary coLoadGroup( + FirstlegColoadGroupClient.GroupSnapshot group, Map inboundCodeByOrder) { + List members = safeMembers(group).stream() + .filter(member -> member.orderId() != null) + .map(member -> new UnifiedOrderResponse.CoLoadGroupMemberSummary( + string(member.orderId()), inboundCodeByOrder.get(member.orderId()))) + .toList(); + return new UnifiedOrderResponse.CoLoadGroupSummary( + string(group.groupId()), group.groupNo(), group.status(), members.size(), members); + } + private UnifiedOrderResponse.OrderRelationSummary relation(OrderEntity order, Map childCounts) { if (order == null) return null; @@ -184,12 +246,20 @@ public class UnifiedOrderQueryService { private UnifiedOrderResponse.Row row(OrderEntity order, FirstlegUnifiedOrderClient.Summary summary, String latestInternalNote, Map childCounts, CargoSummary cargo) { - return row(order, summary, latestInternalNote, childCounts, cargo, null); + return row(order, summary, latestInternalNote, childCounts, cargo, null, null); } private UnifiedOrderResponse.Row row(OrderEntity order, FirstlegUnifiedOrderClient.Summary summary, String latestInternalNote, Map childCounts, - CargoSummary cargo, String creatorName) { + CargoSummary cargo, + UnifiedOrderResponse.CoLoadGroupSummary coLoadGroup) { + return row(order, summary, latestInternalNote, childCounts, cargo, null, coLoadGroup); + } + + private UnifiedOrderResponse.Row row(OrderEntity order, FirstlegUnifiedOrderClient.Summary summary, + String latestInternalNote, Map childCounts, + CargoSummary cargo, String creatorName, + UnifiedOrderResponse.CoLoadGroupSummary coLoadGroup) { if (summary == null) { String mainStatus = orderMainStatus(order.getStatus()); return new UnifiedOrderResponse.Row(order.getId(), order.getTenantId(), order.getOrderNo(), mainStatus, @@ -204,7 +274,7 @@ public class UnifiedOrderQueryService { order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(), null, null, null, null, netWeightKg(cargo), null, null, order.getExpressCompany(), order.getExpressTrackingNo(), StringUtils.hasText(creatorName) ? creatorName : string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), - null, null, null, null, null, + null, null, null, null, null, coLoadGroup, order.getCustomerRemark(), latestInternalNote, order.getCreatedAt(), order.getUpdatedAt(), order.getRequestedPickupAt()); } @@ -224,13 +294,14 @@ public class UnifiedOrderQueryService { inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(), StringUtils.hasText(creatorName) ? creatorName : inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound), preload(summary.preload()), loading(summary.loading()), - delivery(summary.delivery()), transport(inbound), order.getCustomerRemark(), latestInternalNote, + delivery(summary.delivery()), transport(inbound), coLoadGroup, order.getCustomerRemark(), latestInternalNote, order.getCreatedAt(), order.getUpdatedAt(), inbound.requestedPickupAt()); } private UnifiedOrderResponse.Row row(FirstlegUnifiedOrderClient.Summary summary, OrderEntity order, String latestInternalNote, Map childCounts, - CargoSummary cargo) { + CargoSummary cargo, + UnifiedOrderResponse.CoLoadGroupSummary coLoadGroup) { var inbound = summary.inbound(); return new UnifiedOrderResponse.Row(inbound.orderId(), inbound.tenantId(), inbound.orderNo(), summary.mainStatus(), summary.nextAction(), relation(order, childCounts), @@ -250,7 +321,8 @@ public class UnifiedOrderQueryService { inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(), inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo), measurement(inbound), preload(summary.preload()), loading(summary.loading()), - delivery(summary.delivery()), transport(inbound), order == null ? null : order.getCustomerRemark(), latestInternalNote, + delivery(summary.delivery()), transport(inbound), coLoadGroup, + order == null ? null : order.getCustomerRemark(), latestInternalNote, inbound.createdAt(), inbound.updatedAt(), inbound.requestedPickupAt()); }