diff --git a/order/src/main/java/com/baiyue/order/controller/MerchantUnifiedOrderController.java b/order/src/main/java/com/baiyue/order/controller/MerchantUnifiedOrderController.java index 7ab64935e..6c30ba57a 100644 --- a/order/src/main/java/com/baiyue/order/controller/MerchantUnifiedOrderController.java +++ b/order/src/main/java/com/baiyue/order/controller/MerchantUnifiedOrderController.java @@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; /** Merchant product-level order query restricted to the authenticated customer. */ @Tag(name = "Merchant unified orders / 客户统一订单") @@ -51,4 +52,12 @@ public class MerchantUnifiedOrderController { @Operation(summary = "我的父子订单与柜次数量摘要", description = "不返回装柜照片、锁码或内部审计字段。") public ApiResponse fulfillmentTree(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId){return ApiResponse.success(splitService.tree(OrderScopeGuard.merchant(user),orderId));} + + @GetMapping("/container-bookings/{containerBookingId}/fulfillment-slices") + @PreAuthorize("hasAuthority('merchant:order:view')") + @Operation(summary = "按柜次查询我的订单分片") + public ApiResponse> containerSlices(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable long containerBookingId) { + return ApiResponse.success(splitService.containerSlices(containerBookingId, OrderScopeGuard.merchant(user))); + } } 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 4f2c28850..f2705d183 100644 --- a/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java +++ b/order/src/main/java/com/baiyue/order/controller/PlatformUnifiedOrderController.java @@ -25,6 +25,7 @@ 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.RestController; +import java.util.List; /** Platform product-level order query. Legacy first-leg paths remain available. */ @Tag(name = "Platform unified orders / 平台统一订单") @@ -65,6 +66,16 @@ public class PlatformUnifiedOrderController { return ApiResponse.success(splitService.tree(OrderScopeGuard.platform(user,tenantId),orderId)); } + @GetMapping("/container-bookings/{containerBookingId}/fulfillment-slices") + @PreAuthorize("hasAuthority('platform:order:list')") + @Operation(summary = "按柜次查询订单分片") + public ApiResponse> containerSlices(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable long containerBookingId, @RequestParam(required = false) Long tenantId) { + OrderScopeGuard.platform(user); + if (tenantId != null) OrderScopeGuard.platform(user, tenantId); + return ApiResponse.success(splitService.containerSlices(containerBookingId, tenantId)); + } + @PostMapping("/{orderId}/internal-notes") @BusinessAudit(event = "ORDER_INTERNAL_NOTE", resourceType = "FIRST_LEG_ORDER", resourceId = "#orderId", description = "Add internal note / 添加内部备注") @PreAuthorize("hasAuthority('platform:order:note')") diff --git a/order/src/main/java/com/baiyue/order/dto/OrderSplitDtos.java b/order/src/main/java/com/baiyue/order/dto/OrderSplitDtos.java index d8c9e6de6..388be1a4e 100644 --- a/order/src/main/java/com/baiyue/order/dto/OrderSplitDtos.java +++ b/order/src/main/java/com/baiyue/order/dto/OrderSplitDtos.java @@ -49,4 +49,11 @@ public final class OrderSplitDtos { public record Tree(Long parentOrderId, String parentOrderNo, Integer parentVersion, List children, Integer allocatedPackageCount, Integer remainingPackageCount) {} + + public record ContainerSlice(Long containerBookingId, Long tenantId, + Long parentOrderId, String parentOrderNo, + Long childOrderId, String childOrderNo, + Long sourceOrderId, String sourceOrderNo, + String splitRole, Integer packageCount, + Long loadingPlanId) {} } diff --git a/order/src/main/java/com/baiyue/order/mapper/OrderFulfillmentSplitMapper.java b/order/src/main/java/com/baiyue/order/mapper/OrderFulfillmentSplitMapper.java index 592fd2d06..6d6620cee 100644 --- a/order/src/main/java/com/baiyue/order/mapper/OrderFulfillmentSplitMapper.java +++ b/order/src/main/java/com/baiyue/order/mapper/OrderFulfillmentSplitMapper.java @@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Select; import com.baiyue.order.entity.OrderEntity; import java.util.List; +import com.baiyue.order.dto.OrderSplitDtos; import org.apache.ibatis.annotations.Update; @Mapper @@ -41,4 +42,10 @@ public interface OrderFulfillmentSplitMapper extends BaseMapper findByContainer(@Param("containerBookingId") long containerBookingId); + + @Select("SELECT s.container_booking_id AS containerBookingId,s.tenant_id AS tenantId,s.parent_order_id AS parentOrderId,p.order_no AS parentOrderNo,s.child_order_id AS childOrderId,c.order_no AS childOrderNo,s.source_order_id AS sourceOrderId,so.order_no AS sourceOrderNo,s.split_role AS splitRole,CASE WHEN s.split_role='LOADED' THEN s.loaded_package_count ELSE s.remaining_package_count END AS packageCount,s.loading_plan_id AS loadingPlanId FROM ord_order_fulfillment_split s JOIN ord_order p ON p.id=s.parent_order_id AND p.tenant_id=s.tenant_id JOIN ord_order c ON c.id=s.child_order_id AND c.tenant_id=s.tenant_id LEFT JOIN ord_order so ON so.id=s.source_order_id AND so.tenant_id=s.tenant_id WHERE s.container_booking_id=#{containerBookingId} AND s.tenant_id=#{tenantId} ORDER BY s.id") + List findByContainerForTenant(@Param("tenantId") long tenantId, @Param("containerBookingId") long containerBookingId); } diff --git a/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java b/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java index 3e5a15cfa..88691a2e5 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java @@ -25,6 +25,12 @@ public class OrderFulfillmentSplitService { @Resource private OrderMapper orders; @Resource private ObjectMapper json; + public List containerSlices(long containerBookingId, Long tenantId) { + if (containerBookingId <= 0) throw new BizException(ResultCode.BAD_REQUEST, "柜次ID必须大于0"); + return tenantId == null ? splits.findByContainer(containerBookingId) + : splits.findByContainerForTenant(tenantId, containerBookingId); + } + @Transactional public OrderSplitDtos.Response split(OrderSplitDtos.CreateRequest req, long operatorId) { validate(req); diff --git a/order/src/main/resources/openapi/order-v1.yaml b/order/src/main/resources/openapi/order-v1.yaml index 7a72d093e..1c27bf5a9 100644 --- a/order/src/main/resources/openapi/order-v1.yaml +++ b/order/src/main/resources/openapi/order-v1.yaml @@ -267,6 +267,26 @@ paths: - { name: orderId, in: path, required: true, schema: { type: integer, format: int64 } } responses: { '200': { description: 成功 }, '403': { description: 无权限 } } + /api/platform/v1/orders/container-bookings/{containerBookingId}/fulfillment-slices: + get: + summary: 平台按柜次查询全部订单分片 + description: tenantId 可选;省略时跨客户汇总。响应不包含照片、锁码或内部审计字段。 + parameters: + - {name: containerBookingId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + - {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + responses: + '200': {description: 成功;无分片时返回空数组} + '403': {description: 需要平台身份和 platform:order:list 权限} + /api/merchant/v1/orders/container-bookings/{containerBookingId}/fulfillment-slices: + get: + summary: 客户按柜次查询本客户订单分片 + description: 客户范围来自登录态,不接受目标客户参数。 + parameters: + - {name: containerBookingId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} + responses: + '200': {description: 成功;无分片时返回空数组} + '403': {description: 需要 merchant:order:view 权限} + components: securitySchemes: bearerAuth: @@ -385,6 +405,20 @@ components: orderId: {type: string, pattern: '^[1-9][0-9]*$'} orderNo: {type: string} pricingTaxMode: {$ref: '#/components/schemas/PricingTaxMode'} + ContainerSlice: + type: object + properties: + containerBookingId: {type: string} + tenantId: {type: string} + parentOrderId: {type: string} + parentOrderNo: {type: string} + childOrderId: {type: string} + childOrderNo: {type: string} + sourceOrderId: {type: string} + sourceOrderNo: {type: string} + splitRole: {type: string, enum: [LOADED, REMAINDER]} + packageCount: {type: integer} + loadingPlanId: {type: string} parameters: InternalToken: name: X-Internal-Service-Token diff --git a/order/src/test/java/com/baiyue/order/service/OrderContainerAggregationTest.java b/order/src/test/java/com/baiyue/order/service/OrderContainerAggregationTest.java new file mode 100644 index 000000000..5f43dcc94 --- /dev/null +++ b/order/src/test/java/com/baiyue/order/service/OrderContainerAggregationTest.java @@ -0,0 +1,37 @@ +package com.baiyue.order.service; + +import com.baiyue.order.dto.OrderSplitDtos; +import com.baiyue.order.mapper.OrderFulfillmentSplitMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class OrderContainerAggregationTest { + @Mock OrderFulfillmentSplitMapper splits; + @InjectMocks OrderFulfillmentSplitService service; + + @Test void platformAggregatesMultipleCustomersInOneContainer() { + var a = slice(1L, 11L, 101L); + var b = slice(2L, 22L, 202L); + when(splits.findByContainer(900L)).thenReturn(List.of(a, b)); + assertThat(service.containerSlices(900L, null)).containsExactly(a, b); + verify(splits, never()).findByContainerForTenant(anyLong(), anyLong()); + } + + @Test void merchantIsAlwaysTenantScopedAndEmptyIsStable() { + when(splits.findByContainerForTenant(7L, 900L)).thenReturn(List.of()); + assertThat(service.containerSlices(900L, 7L)).isEmpty(); + verify(splits, never()).findByContainer(anyLong()); + } + + private OrderSplitDtos.ContainerSlice slice(long tenant, long parent, long child) { + return new OrderSplitDtos.ContainerSlice(900L, tenant, parent, "P"+parent, child, "C"+child, + parent, "P"+parent, "LOADED", 10, 800L); + } +} diff --git a/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitContractTest.java b/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitContractTest.java index b6b3220e9..ad8b064fa 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitContractTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitContractTest.java @@ -38,6 +38,9 @@ class OrderFulfillmentSplitContractTest { .contains("containerBookingId:") .contains("expectedParentVersion:") .contains("FulfillmentSplitResponse:") - .contains("InternalToken:"); + .contains("InternalToken:") + .contains("/api/platform/v1/orders/container-bookings/{containerBookingId}/fulfillment-slices:") + .contains("/api/merchant/v1/orders/container-bookings/{containerBookingId}/fulfillment-slices:") + .contains("ContainerSlice"); } }