优化
This commit is contained in:
@@ -3,6 +3,7 @@ package com.baiyue.order.client;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name="warehouse-service", contextId="warehousePartnerClient",
|
||||
url="${baiyue.clients.warehouse.url:}", path="/internal/warehouse-partners")
|
||||
@@ -10,5 +11,6 @@ public interface PartnerClient {
|
||||
@GetMapping("/{tenantId}/{id}/active")
|
||||
ApiResponse<Partner> active(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable("tenantId") long tenantId, @PathVariable("id") long id);
|
||||
record Partner(Long id, Long tenantId, String partnerName, String status) { }
|
||||
record Partner(Long id, Long tenantId, String partnerName, String status,
|
||||
String partnerType, List<String> types) { }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.order.dto.OrderDetailResponse;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.dto.TenantOrderCountResponse;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.entity.OrderStatus;
|
||||
import com.baiyue.order.service.OrderApplicationService;
|
||||
@@ -27,6 +28,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 平台端头程订单管理接口,在明确的目标租户范围内提供查询、审核和履约状态流转能力。
|
||||
@@ -110,6 +112,20 @@ public class PlatformFirstLegOrderController {
|
||||
return ApiResponse.success(service.page(OrderScopeGuard.platform(user, tenantId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/tenant-counts")
|
||||
@PreAuthorize("hasAuthority('platform:order:list')")
|
||||
@Operation(summary = "批量查询客户运单数", description = "按客户租户 ID 返回真实头程运单数量,最多 200 个客户。")
|
||||
public ApiResponse<List<TenantOrderCountResponse>> tenantCounts(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam List<Long> tenantIds) {
|
||||
if (tenantIds.isEmpty() || tenantIds.size() > 200 || tenantIds.stream().anyMatch(id -> id == null || id <= 0)) {
|
||||
throw new com.baiyue.common.core.exception.BizException(
|
||||
com.baiyue.common.core.api.ResultCode.BAD_REQUEST, "1 to 200 valid tenant IDs are required / 需要 1 至 200 个有效租户 ID");
|
||||
}
|
||||
OrderScopeGuard.platform(user, tenantIds.get(0));
|
||||
return ApiResponse.success(service.countByTenantIds(tenantIds));
|
||||
}
|
||||
|
||||
@Operation(summary = "Tenant order detail / 租户订单详情", description = "Returns the complete order aggregate within the explicit target tenant. / 返回明确目标租户内的完整订单聚合。")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Query succeeded / 查询成功"),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.baiyue.order.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(name = "TenantOrderCountResponse", description = "平台客户的头程运单数量")
|
||||
public record TenantOrderCountResponse(Long tenantId, Long orderCount) { }
|
||||
@@ -5,11 +5,13 @@ import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.entity.*;
|
||||
import com.baiyue.order.mapper.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 订单聚合仓储,组合订单主表、货物、参与方、备注、状态历史与 Outbox 的持久化操作,
|
||||
@@ -86,6 +88,18 @@ public class OrderRepository {
|
||||
return PageResult.from(orderMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
|
||||
}
|
||||
|
||||
public Map<Long, Long> countByTenantIds(List<Long> tenantIds) {
|
||||
if (tenantIds.isEmpty()) return Map.of();
|
||||
var rows = orderMapper.selectMaps(new QueryWrapper<OrderEntity>()
|
||||
.select("tenant_id", "COUNT(*) AS order_count")
|
||||
.in("tenant_id", tenantIds)
|
||||
.groupBy("tenant_id"));
|
||||
return rows.stream().collect(java.util.stream.Collectors.toMap(
|
||||
row -> ((Number) row.get("tenant_id")).longValue(),
|
||||
row -> ((Number) row.get("order_count")).longValue()
|
||||
));
|
||||
}
|
||||
|
||||
public List<OrderCargoEntity> findCargo(long tenantId, long orderId) {
|
||||
return cargoMapper.selectList(new LambdaQueryWrapper<OrderCargoEntity>()
|
||||
.eq(OrderCargoEntity::getTenantId, tenantId).eq(OrderCargoEntity::getOrderId, orderId)
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.dto.OrderDetailResponse;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.dto.TenantOrderCountResponse;
|
||||
import com.baiyue.order.entity.*;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
@@ -108,6 +109,13 @@ public class OrderApplicationService {
|
||||
}
|
||||
|
||||
public PageResult<OrderEntity> page(long tenantId, OrderRequests.QueryRequest query) { return repository.page(tenantId, query); }
|
||||
|
||||
public List<TenantOrderCountResponse> countByTenantIds(List<Long> tenantIds) {
|
||||
var counts = repository.countByTenantIds(tenantIds);
|
||||
return tenantIds.stream().distinct()
|
||||
.map(tenantId -> new TenantOrderCountResponse(tenantId, counts.getOrDefault(tenantId, 0L)))
|
||||
.toList();
|
||||
}
|
||||
public OrderDetailResponse detail(long tenantId, long id) {
|
||||
OrderEntity order = require(tenantId, id);
|
||||
return new OrderDetailResponse(order, repository.findCargo(tenantId, id), repository.findParties(tenantId, id),
|
||||
|
||||
@@ -72,6 +72,8 @@ public class OrderMasterDataGuard {
|
||||
throw new BizException(ResultCode.FORBIDDEN,"往来单位不属于当前企业");
|
||||
if (!"ACTIVE".equals(value.status()))
|
||||
throw new BizException(ResultCode.CONFLICT,"往来单位已停用");
|
||||
if (value.types() == null || !value.types().contains("SHIPPER"))
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,"所选往来单位不具备发货单位类型");
|
||||
if (!StringUtils.hasText(value.partnerName())) throw unavailable("往来单位");
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,18 @@ paths:
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 需要 platform:order:create 权限}
|
||||
/api/platform/v1/orders/first-leg/tenant-counts:
|
||||
get:
|
||||
summary: 批量查询客户运单数
|
||||
description: 按客户租户 ID 返回真实头程运单数量,最多 200 个租户。
|
||||
parameters:
|
||||
- name: tenantIds
|
||||
in: query
|
||||
required: true
|
||||
schema: {type: array, maxItems: 200, items: {type: integer, format: int64}}
|
||||
responses:
|
||||
'200': {description: 查询成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 需要 platform:order:list 权限}
|
||||
/api/platform/v1/orders/first-leg/{id}/submit:
|
||||
post:
|
||||
summary: 平台代客提交指定订单
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -26,6 +27,8 @@ class OrderControllerPermissionTest {
|
||||
AuthenticatedUser.class, long.class, OrderRequests.QueryRequest.class);
|
||||
assertAuthority(PlatformFirstLegOrderController.class, "detail", "hasAuthority('platform:order:list')",
|
||||
AuthenticatedUser.class, long.class, long.class);
|
||||
assertAuthority(PlatformFirstLegOrderController.class, "tenantCounts", "hasAuthority('platform:order:list')",
|
||||
AuthenticatedUser.class, List.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -24,7 +24,7 @@ class OrderMasterDataGuardTest {
|
||||
@Test
|
||||
void validMasterDataReturnsTrustedServerSnapshot() {
|
||||
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
|
||||
var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE");
|
||||
var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER"));
|
||||
var address=new WarehouseClient.Address("CN","广东省","深圳市","宝安区","518000","示例路1号",null);
|
||||
var capability=new WarehouseClient.Capability("RECEIVING",true,java.time.LocalTime.of(16,0));
|
||||
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"深圳仓","ACTIVE"),List.of(address),List.of(capability));
|
||||
@@ -57,7 +57,7 @@ class OrderMasterDataGuardTest {
|
||||
@Test
|
||||
void masterDataResponseWithNullElementReturnsControlledBusinessError() {
|
||||
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
|
||||
var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE");
|
||||
var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER"));
|
||||
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"深圳仓","ACTIVE"),
|
||||
Collections.singletonList(null), Collections.singletonList(null));
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(partner));
|
||||
@@ -71,7 +71,7 @@ class OrderMasterDataGuardTest {
|
||||
@Test
|
||||
void masterDataResponseMissingSnapshotNameIsServiceUnavailable() {
|
||||
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,10001L,null,"ACTIVE")));
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,10001L,null,"ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
|
||||
assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -133,13 +133,25 @@ class OrderMasterDataGuardTest {
|
||||
void inactiveMasterDataInSuccessfulResponseMapsToConflict() {
|
||||
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
|
||||
when(partners.active("token",10001,7))
|
||||
.thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,10001L,"客户","INACTIVE")));
|
||||
.thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,10001L,"客户","INACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
|
||||
assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.CONFLICT);
|
||||
}
|
||||
@Test
|
||||
void partnerWithoutShipperTypeIsRejected() {
|
||||
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,10001L,"收货单位","ACTIVE","CONSIGNEE",List.of("CONSIGNEE"))));
|
||||
|
||||
assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
verifyNoInteractions(warehouses);
|
||||
}
|
||||
private FeignException feign(int status) {
|
||||
Request request = Request.create(Request.HttpMethod.GET, "http://warehouse-service/internal/test",
|
||||
Map.of(), null, StandardCharsets.UTF_8, null);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class TenantOrderCountServiceTest {
|
||||
@Test
|
||||
void returnsZeroForTenantsWithoutOrdersAndDeduplicatesRequestedIds() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
when(repository.countByTenantIds(List.of(8L, 9L, 8L))).thenReturn(Map.of(8L, 12L));
|
||||
OrderApplicationService service = new OrderApplicationService();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
|
||||
assertThat(service.countByTenantIds(List.of(8L, 9L, 8L)))
|
||||
.extracting("tenantId", "orderCount")
|
||||
.containsExactly(
|
||||
org.assertj.core.groups.Tuple.tuple(8L, 12L),
|
||||
org.assertj.core.groups.Tuple.tuple(9L, 0L)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user