feat(order): add container cross-order fulfillment aggregation
This commit is contained in:
@@ -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<OrderSplitDtos.Tree> 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<List<OrderSplitDtos.ContainerSlice>> containerSlices(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long containerBookingId) {
|
||||
return ApiResponse.success(splitService.containerSlices(containerBookingId, OrderScopeGuard.merchant(user)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<OrderSplitDtos.ContainerSlice>> 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')")
|
||||
|
||||
@@ -49,4 +49,11 @@ public final class OrderSplitDtos {
|
||||
public record Tree(Long parentOrderId, String parentOrderNo, Integer parentVersion,
|
||||
List<Child> 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) {}
|
||||
}
|
||||
|
||||
@@ -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<OrderFulfillment
|
||||
|
||||
@Select("SELECT COALESCE(MAX(split_sequence),0) FROM ord_order WHERE tenant_id=#{tenantId} AND (id=#{parentOrderId} OR parent_order_id=#{parentOrderId}) AND deleted=0")
|
||||
Integer maxSplitSequence(@Param("tenantId") long tenantId, @Param("parentOrderId") long parentOrderId);
|
||||
|
||||
@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} ORDER BY s.tenant_id,s.id")
|
||||
List<OrderSplitDtos.ContainerSlice> 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<OrderSplitDtos.ContainerSlice> findByContainerForTenant(@Param("tenantId") long tenantId, @Param("containerBookingId") long containerBookingId);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ public class OrderFulfillmentSplitService {
|
||||
@Resource private OrderMapper orders;
|
||||
@Resource private ObjectMapper json;
|
||||
|
||||
public List<OrderSplitDtos.ContainerSlice> 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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user