优化
This commit is contained in:
+10
-11
@@ -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<List<OrderGroupInfo>> batchQuery(
|
||||
@GetMapping("/by-inbound-code/{inboundCode}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
@Operation(summary = "按入仓码查询同柜组业务摘要",
|
||||
description = "仅返回业务状态、有效成员数及去重后的全部成员入仓码;内部组ID和组号不作为页面识别键。")
|
||||
public ApiResponse<BusinessSummary> 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) {
|
||||
|
||||
+12
-8
@@ -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<BusinessMember> members
|
||||
) {
|
||||
public BusinessSummary {
|
||||
members = members == null ? List.of() : List.copyOf(members);
|
||||
memberCount = members.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-8
@@ -85,15 +85,28 @@ public interface ColoadGroupMapper {
|
||||
GroupRow activeGroupByInbound(@Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId);
|
||||
|
||||
@Select({"<script>", """
|
||||
SELECT member.order_id orderId,member.group_id groupId
|
||||
FROM fl_coload_group_member member
|
||||
@Select("""
|
||||
SELECT DISTINCT group_row.id groupId,group_row.group_status groupStatus
|
||||
FROM fl_inbound_order requested
|
||||
JOIN fl_coload_group_member member
|
||||
ON member.tenant_id=requested.tenant_id AND member.inbound_id=requested.id
|
||||
JOIN fl_coload_group group_row ON group_row.id=member.group_id
|
||||
WHERE member.order_id IN
|
||||
""", "<foreach collection='orderIds' item='orderId' open='(' separator=',' close=')'>",
|
||||
"#{orderId}", "</foreach>",
|
||||
"AND group_row.group_status<>'CANCELLED' ORDER BY member.order_id,group_row.created_at DESC", "</script>"})
|
||||
List<OrderGroupLinkRow> linksByOrderIds(@Param("orderIds") List<Long> 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<InboundGroupLookupRow> 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<MemberCodeRow> effectiveMemberCodes(@Param("groupId") long groupId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_coload_group
|
||||
|
||||
+8
-2
@@ -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
|
||||
|
||||
+23
-11
@@ -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<OrderGroupInfo> queryByOrderIds(List<Long> requestedOrderIds) {
|
||||
List<Long> orderIds = normalizedOrderIds(requestedOrderIds, 1, 200);
|
||||
Map<Long, OrderGroupInfo> latest = new LinkedHashMap<>();
|
||||
Map<Long, GroupSnapshot> 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<BusinessMember> 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
|
||||
|
||||
@@ -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'}
|
||||
|
||||
+9
-3
@@ -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
|
||||
|
||||
+10
-6
@@ -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<String, Object> yaml = new Yaml().load(source);
|
||||
Map<String, Object> paths = (Map<String, Object>) yaml.get("paths");
|
||||
Map<String, Object> components = (Map<String, Object>) yaml.get("components");
|
||||
Map<String, Object> schemas = (Map<String, Object>) components.get("schemas");
|
||||
Map<String, Object> orderInfo = (Map<String, Object>) schemas.get("OrderColoadGroupInfo");
|
||||
Map<String, Object> orderInfoProperties = (Map<String, Object>) orderInfo.get("properties");
|
||||
Map<String, Object> summary = (Map<String, Object>) schemas.get("ColoadGroupBusinessSummary");
|
||||
Map<String, Object> summaryProperties = (Map<String, Object>) summary.get("properties");
|
||||
Map<String, Object> member = (Map<String, Object>) schemas.get("ColoadGroupBusinessMember");
|
||||
Map<String, Object> memberProperties = (Map<String, Object>) 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+19
@@ -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);
|
||||
|
||||
@@ -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<List<OrderGroupInfo>> batchQuery(@RequestHeader("Authorization") String authorization,
|
||||
@RequestBody BatchQueryRequest request);
|
||||
|
||||
record BatchQueryRequest(List<Long> orderIds) {}
|
||||
record OrderGroupInfo(Long orderId, GroupSnapshot group) {}
|
||||
record GroupSnapshot(Long groupId, String groupNo, String status, List<Member> members) {}
|
||||
record Member(Long tenantId, Long orderId, Long inboundId, Integer fullPackageCount) {}
|
||||
}
|
||||
@@ -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<PageResult<UnifiedOrderResponse.Row>> 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<UnifiedOrderResponse.Detail> 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")
|
||||
|
||||
@@ -32,6 +32,7 @@ public final class UnifiedOrderResponse {
|
||||
List<String> 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<CoLoadGroupMemberSummary> members) {}
|
||||
public record CoLoadGroupMemberSummary(String orderId, String inboundCode) {}
|
||||
|
||||
public record Detail(Row summary, OrderDetailResponse order,
|
||||
List<TimelineEvent> timeline) {}
|
||||
|
||||
|
||||
@@ -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<UnifiedOrderResponse.Row> page(Long tenantId, UnifiedOrderQuery query) {
|
||||
return page(tenantId, query, null);
|
||||
}
|
||||
|
||||
public PageResult<UnifiedOrderResponse.Row> platformPage(Long tenantId, UnifiedOrderQuery query,
|
||||
String authorization) {
|
||||
return page(tenantId, query, authorization);
|
||||
}
|
||||
|
||||
private PageResult<UnifiedOrderResponse.Row> 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<OrderStatus> statuses = "PENDING_REVIEW".equals(query.mainStatus())
|
||||
? List.of(OrderStatus.SUBMITTED) : ACTIVE_ORDER_STATUSES;
|
||||
@@ -64,13 +76,24 @@ public class UnifiedOrderQueryService {
|
||||
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId,
|
||||
source.records().stream().map(OrderEntity::getId).toList());
|
||||
Map<Long, Integer> childCounts = childCounts(source.records());
|
||||
Map<Long, UnifiedOrderResponse.CoLoadGroupSummary> coLoadByOrder = coLoadGroups(
|
||||
source.records().stream().map(OrderEntity::getId).toList(), platformAuthorization);
|
||||
List<UnifiedOrderResponse.Row> 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<UnifiedOrderResponse.TimelineEvent> 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<UnifiedOrderResponse.Row> pageFromFirstleg(Long tenantId, UnifiedOrderQuery query) {
|
||||
private PageResult<UnifiedOrderResponse.Row> pageFromFirstleg(Long tenantId, UnifiedOrderQuery query,
|
||||
String platformAuthorization) {
|
||||
List<Long> candidateOrderIds = StringUtils.hasText(query.fbaCode())
|
||||
? orders.findOrderIdsBySkuNo(tenantId, query.fbaCode()) : null;
|
||||
if (candidateOrderIds != null && candidateOrderIds.isEmpty()) {
|
||||
@@ -126,10 +151,13 @@ public class UnifiedOrderQueryService {
|
||||
Map<Long, String> latestNotes = latestNotes(tenantId, orderIds);
|
||||
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId, orderIds);
|
||||
Map<Long, Integer> childCounts = childCounts(List.copyOf(orderById.values()));
|
||||
Map<Long, UnifiedOrderResponse.CoLoadGroupSummary> coLoadByOrder =
|
||||
coLoadGroups(orderIds, platformAuthorization);
|
||||
List<UnifiedOrderResponse.Row> 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<Long, UnifiedOrderResponse.CoLoadGroupSummary> coLoadGroups(
|
||||
List<Long> orderIds, String platformAuthorization) {
|
||||
if (!StringUtils.hasText(platformAuthorization) || orderIds.isEmpty()) return Map.of();
|
||||
List<FirstlegColoadGroupClient.OrderGroupInfo> data = require(
|
||||
call(() -> coloadGroups.batchQuery(platformAuthorization,
|
||||
new FirstlegColoadGroupClient.BatchQueryRequest(orderIds.stream().distinct().toList()))),
|
||||
"同柜组摘要响应无效");
|
||||
List<Long> 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<Long, String> 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<FirstlegColoadGroupClient.Member> safeMembers(FirstlegColoadGroupClient.GroupSnapshot group) {
|
||||
return group.members() == null ? List.of() : group.members();
|
||||
}
|
||||
|
||||
private UnifiedOrderResponse.CoLoadGroupSummary coLoadGroup(
|
||||
FirstlegColoadGroupClient.GroupSnapshot group, Map<Long, String> inboundCodeByOrder) {
|
||||
List<UnifiedOrderResponse.CoLoadGroupMemberSummary> 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<Long, Integer> 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<Long, Integer> 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<Long, Integer> 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<Long, Integer> 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<Long, Integer> 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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user