This commit is contained in:
2026-08-03 13:16:19 +08:00
parent 481747d9dc
commit 9eb03d4bce
54 changed files with 1925 additions and 52 deletions
@@ -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)
);
}
}