优化
This commit is contained in:
+13
-2
@@ -2,6 +2,8 @@ package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
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.dto.FbaPricingDtos.CarrierRateVersionRequest;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.CustomerRuleVersionRequest;
|
||||
@@ -36,10 +38,12 @@ public class PlatformFbaMasterDataController {
|
||||
|
||||
@GetMapping("/customer-rules")
|
||||
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rule:view')")
|
||||
@Operation(summary = "查询客户计费规则版本", description = "默认查询全部企业;tenantId 可用于筛选目标企业")
|
||||
public ApiResponse<List<CustomerChargeRuleEntity>> rules(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) Long tenantId,
|
||||
@RequestParam(required = false) String ruleCode) {
|
||||
return ApiResponse.success(service.listRules(FirstlegScopeGuard.platform(user, tenantId), ruleCode));
|
||||
return ApiResponse.success(service.listRules(optionalPlatformTenant(user, tenantId), ruleCode));
|
||||
}
|
||||
|
||||
@GetMapping("/customer-rules/{id}")
|
||||
@@ -105,4 +109,11 @@ public class PlatformFbaMasterDataController {
|
||||
service.retireRate(FirstlegScopeGuard.platform(user, tenantId), id, user.userId());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
private static Long optionalPlatformTenant(AuthenticatedUser user, Long tenantId) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
return tenantId == null ? null : FirstlegScopeGuard.platform(user, tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-6
@@ -10,10 +10,13 @@ import com.baiyue.firstleg.dto.InboundOrderQuery;
|
||||
import com.baiyue.firstleg.dto.InboundOrderSummary;
|
||||
import com.baiyue.firstleg.service.InboundOrderQueryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
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.RequestMapping;
|
||||
@@ -22,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 平台端入库单列表和详情只读接口。 */
|
||||
@Tag(name = "平台入库单", description = "二期入库单列表、页签、筛选和只读聚合详情")
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/inbounds")
|
||||
public class PlatformInboundOrderController {
|
||||
@@ -31,14 +35,16 @@ public class PlatformInboundOrderController {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询入库单", description = "按目标租户和二期状态页签分页查询入库单快照。")
|
||||
@Operation(summary = "分页查询入库单",
|
||||
description = "默认分页查询全部企业入库单;可按目标租户和二期状态页签筛选入库单快照。")
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:list')")
|
||||
public ApiResponse<PageResult<InboundOrderSummary>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@Parameter(description = "可选目标企业租户ID;不传时查询全部企业")
|
||||
@RequestParam(required = false) @Min(1) Long tenantId,
|
||||
@Valid InboundOrderQuery query) {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.page(tenantId, query));
|
||||
}
|
||||
|
||||
@@ -57,11 +63,15 @@ public class PlatformInboundOrderController {
|
||||
}
|
||||
|
||||
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
requirePlatform(user);
|
||||
if (tenantId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.time.LocalDateTime;
|
||||
|
||||
/** 入库列表行,所有名称和运输信息均为业务快照。 */
|
||||
public record InboundOrderSummary(
|
||||
Long id, Long orderId, String orderNo, String inboundCode,
|
||||
Long id, Long tenantId, Long orderId, String orderNo, String inboundCode,
|
||||
String processStatus, String orderStatusSnapshot, String measureReviewStatus,
|
||||
String feeStatus, String feeReviewStatus, String notificationStatus,
|
||||
Integer version,
|
||||
@@ -33,7 +33,7 @@ public record InboundOrderSummary(
|
||||
public static InboundOrderSummary from(
|
||||
InboundOrderEntity order, InboundTransportSnapshotEntity transport, String locationCodes) {
|
||||
return new InboundOrderSummary(
|
||||
order.getId(), order.getOrderId(), order.getOrderNo(), order.getInboundCode(),
|
||||
order.getId(), order.getTenantId(), order.getOrderId(), order.getOrderNo(), order.getInboundCode(),
|
||||
order.getProcessStatus(), order.getOrderStatusSnapshot(), order.getMeasureReviewStatus(),
|
||||
order.getFeeStatus(), order.getFeeReviewStatus(), order.getNotificationStatus(),
|
||||
order.getVersion(),
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.springframework.stereotype.Repository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** FBA 计费仓储,所有业务查询都强制带租户条件。 */
|
||||
/** FBA 计费仓储;平台客户规则列表可跨企业读取,其他业务操作强制带租户条件。 */
|
||||
@Repository
|
||||
public class FbaPricingRepository {
|
||||
private final CustomerChargeRuleMapper rules;
|
||||
@@ -61,9 +61,9 @@ public class FbaPricingRepository {
|
||||
.eq(CarrierFbaRateEntity::getId, id));
|
||||
}
|
||||
|
||||
public List<CustomerChargeRuleEntity> listRules(long tenantId, String ruleCode) {
|
||||
public List<CustomerChargeRuleEntity> listRules(Long tenantId, String ruleCode) {
|
||||
return rules.selectList(new LambdaQueryWrapper<CustomerChargeRuleEntity>()
|
||||
.eq(CustomerChargeRuleEntity::getTenantId, tenantId)
|
||||
.eq(tenantId != null, CustomerChargeRuleEntity::getTenantId, tenantId)
|
||||
.eq(ruleCode != null && !ruleCode.isBlank(), CustomerChargeRuleEntity::getRuleCode, ruleCode)
|
||||
.orderByAsc(CustomerChargeRuleEntity::getRuleCode)
|
||||
.orderByDesc(CustomerChargeRuleEntity::getRuleVersion));
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 入库单只读仓储,所有查询均以租户作为首要隔离条件。 */
|
||||
/** 入库单只读仓储;平台分页可跨企业查询,详情继续强制租户隔离。 */
|
||||
@Repository
|
||||
public class InboundOrderRepository {
|
||||
private final InboundOrderMapper orders;
|
||||
@@ -51,9 +51,9 @@ public class InboundOrderRepository {
|
||||
this.histories = histories;
|
||||
}
|
||||
|
||||
public PageResult<InboundOrderSummary> page(long tenantId, InboundOrderQuery query) {
|
||||
public PageResult<InboundOrderSummary> page(Long tenantId, InboundOrderQuery query) {
|
||||
LambdaQueryWrapper<InboundOrderEntity> wrapper = new LambdaQueryWrapper<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, tenantId)
|
||||
.eq(tenantId != null, InboundOrderEntity::getTenantId, tenantId)
|
||||
.likeRight(StringUtils.hasText(query.inboundCode()), InboundOrderEntity::getInboundCode,
|
||||
query.inboundCode())
|
||||
.likeRight(StringUtils.hasText(query.orderNo()), InboundOrderEntity::getOrderNo, query.orderNo())
|
||||
@@ -85,9 +85,10 @@ public class InboundOrderRepository {
|
||||
.orderByDesc(InboundOrderEntity::getCreatedAt)
|
||||
.orderByDesc(InboundOrderEntity::getId);
|
||||
Page<InboundOrderEntity> result = orders.selectPage(new Page<>(query.page(), query.size()), wrapper);
|
||||
List<Long> ids = result.getRecords().stream().map(InboundOrderEntity::getId).toList();
|
||||
Map<Long, InboundTransportSnapshotEntity> transportByInbound = transportMap(tenantId, ids);
|
||||
Map<Long, String> locationCodesByInbound = locationCodesMap(tenantId, ids);
|
||||
Map<Long, Long> tenantByInbound = result.getRecords().stream()
|
||||
.collect(Collectors.toMap(InboundOrderEntity::getId, InboundOrderEntity::getTenantId));
|
||||
Map<Long, InboundTransportSnapshotEntity> transportByInbound = transportMap(tenantId, tenantByInbound);
|
||||
Map<Long, String> locationCodesByInbound = locationCodesMap(tenantId, tenantByInbound);
|
||||
List<InboundOrderSummary> records = result.getRecords().stream()
|
||||
.map(order -> InboundOrderSummary.from(order, transportByInbound.get(order.getId()),
|
||||
locationCodesByInbound.get(order.getId())))
|
||||
@@ -156,27 +157,31 @@ public class InboundOrderRepository {
|
||||
palletSummaries, fileSummaries, noteSummaries, historySummaries);
|
||||
}
|
||||
|
||||
private Map<Long, InboundTransportSnapshotEntity> transportMap(long tenantId, List<Long> inboundIds) {
|
||||
if (inboundIds.isEmpty()) {
|
||||
private Map<Long, InboundTransportSnapshotEntity> transportMap(
|
||||
Long tenantId, Map<Long, Long> tenantByInbound) {
|
||||
if (tenantByInbound.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return transports.selectList(new LambdaQueryWrapper<InboundTransportSnapshotEntity>()
|
||||
.eq(InboundTransportSnapshotEntity::getTenantId, tenantId)
|
||||
.in(InboundTransportSnapshotEntity::getInboundId, inboundIds))
|
||||
.stream().collect(Collectors.toMap(InboundTransportSnapshotEntity::getInboundId,
|
||||
.eq(tenantId != null, InboundTransportSnapshotEntity::getTenantId, tenantId)
|
||||
.in(InboundTransportSnapshotEntity::getInboundId, tenantByInbound.keySet()))
|
||||
.stream()
|
||||
.filter(item -> owns(tenantByInbound, item.getInboundId(), item.getTenantId()))
|
||||
.collect(Collectors.toMap(InboundTransportSnapshotEntity::getInboundId,
|
||||
Function.identity(), (left, right) -> left));
|
||||
}
|
||||
|
||||
private Map<Long, String> locationCodesMap(long tenantId, List<Long> inboundIds) {
|
||||
if (inboundIds.isEmpty()) {
|
||||
private Map<Long, String> locationCodesMap(Long tenantId, Map<Long, Long> tenantByInbound) {
|
||||
if (tenantByInbound.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return pallets.selectList(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.in(InboundPalletAllocationEntity::getInboundId, inboundIds)
|
||||
.eq(tenantId != null, InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.in(InboundPalletAllocationEntity::getInboundId, tenantByInbound.keySet())
|
||||
.orderByAsc(InboundPalletAllocationEntity::getInboundId)
|
||||
.orderByAsc(InboundPalletAllocationEntity::getAllocationNo))
|
||||
.stream()
|
||||
.filter(item -> owns(tenantByInbound, item.getInboundId(), item.getTenantId()))
|
||||
.filter(item -> StringUtils.hasText(item.getLocationCode()))
|
||||
.collect(Collectors.groupingBy(InboundPalletAllocationEntity::getInboundId,
|
||||
java.util.LinkedHashMap::new,
|
||||
@@ -184,6 +189,10 @@ public class InboundOrderRepository {
|
||||
Collectors.joining("、"))));
|
||||
}
|
||||
|
||||
private static boolean owns(Map<Long, Long> tenantByInbound, Long inboundId, Long tenantId) {
|
||||
return inboundId != null && tenantId != null && tenantId.equals(tenantByInbound.get(inboundId));
|
||||
}
|
||||
|
||||
private static <T> LambdaQueryWrapper<T> owned(
|
||||
com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, ?> tenantColumn,
|
||||
com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, ?> inboundColumn,
|
||||
|
||||
@@ -98,7 +98,7 @@ public class FbaMasterDataService {
|
||||
return value;
|
||||
}
|
||||
|
||||
public List<CustomerChargeRuleEntity> listRules(long tenantId, String code) {
|
||||
public List<CustomerChargeRuleEntity> listRules(Long tenantId, String code) {
|
||||
return repository.listRules(tenantId, code);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public class InboundOrderQueryService {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public PageResult<InboundOrderSummary> page(long tenantId, InboundOrderQuery query) {
|
||||
public PageResult<InboundOrderSummary> page(Long tenantId, InboundOrderQuery query) {
|
||||
validateQuery(query);
|
||||
return repository.page(tenantId, query);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ paths:
|
||||
get:
|
||||
summary: 查询客户计费规则版本
|
||||
description: 需要 firstleg:fba-pricing:rule:view 权限
|
||||
parameters: [{$ref: '#/components/parameters/TenantId'}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/OptionalTenantId'}
|
||||
- {name: ruleCode, in: query, schema: {type: string, maxLength: 50}}
|
||||
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
||||
/api/platform/v1/fba-pricing/customer-rules/versions:
|
||||
post:
|
||||
@@ -87,9 +89,10 @@ components:
|
||||
securitySchemes:
|
||||
bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT}
|
||||
parameters:
|
||||
TenantId: {name: tenantId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
Id: {name: id, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
InboundId: {name: inboundId, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
OptionalTenantId: {name: tenantId, in: query, required: false, description: 不传时查询全部企业, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
TenantId: {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
Id: {name: id, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
InboundId: {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
||||
schemas:
|
||||
AdjustFeeRequest:
|
||||
@@ -107,7 +110,7 @@ components:
|
||||
required: [ruleCode, customerId, customerName, businessType, chargeMode, taxMode, conversionFactor, roundingScale, roundingMode, currency, effectiveFrom]
|
||||
properties:
|
||||
ruleCode: {type: string, maxLength: 50}
|
||||
customerId: {type: integer, format: int64}
|
||||
customerId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
customerName: {type: string, maxLength: 128}
|
||||
businessType: {type: string, maxLength: 32}
|
||||
chargeMode: {type: string, enum: [KG, CBM]}
|
||||
@@ -123,11 +126,11 @@ components:
|
||||
required: [rateCode, originWarehouseId, originWarehouseName, businessType, channelCode, carrierId, carrierName, destinationCode, chargeUnit, unitPrice, currency, additionalFee, effectiveFrom]
|
||||
properties:
|
||||
rateCode: {type: string, maxLength: 50}
|
||||
originWarehouseId: {type: integer, format: int64}
|
||||
originWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
originWarehouseName: {type: string}
|
||||
businessType: {type: string}
|
||||
channelCode: {type: string}
|
||||
carrierId: {type: integer, format: int64}
|
||||
carrierId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
carrierName: {type: string}
|
||||
destinationCode: {type: string}
|
||||
chargeUnit: {type: string, enum: [KG, CBM]}
|
||||
|
||||
@@ -127,12 +127,16 @@ paths:
|
||||
/api/platform/inbounds:
|
||||
get:
|
||||
summary: 分页查询入库单
|
||||
description: 需要 platform:firstleg:inbound:list 权限;仅使用 V2 四个页签
|
||||
description: 需要 platform:firstleg:inbound:list 权限;默认查询全部企业,可选 tenantId 筛选;仅使用 V2 四个页签
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]}}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'200':
|
||||
description: 成功或业务失败,按响应体 code 判断;每条记录携带后续租户隔离操作所需的 tenantId
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/InboundOrderPageEnvelope'}
|
||||
'403': {description: 无查询权限}
|
||||
/api/platform/inbounds/{inboundId}:
|
||||
get:
|
||||
@@ -251,6 +255,34 @@ components:
|
||||
in: query
|
||||
schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
|
||||
schemas:
|
||||
InboundOrderPageEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string, example: '200'}
|
||||
message: {type: string}
|
||||
data: {$ref: '#/components/schemas/InboundOrderPage'}
|
||||
InboundOrderPage:
|
||||
type: object
|
||||
properties:
|
||||
records:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/InboundOrderSummary'}
|
||||
page: {type: integer, minimum: 1}
|
||||
size: {type: integer, minimum: 1}
|
||||
total: {type: integer, format: int64, minimum: 0}
|
||||
pages: {type: integer, format: int64, minimum: 0}
|
||||
InboundOrderSummary:
|
||||
type: object
|
||||
required: [id, tenantId]
|
||||
properties:
|
||||
id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$', description: 记录所属企业租户 ID,用于详情和处理接口的租户隔离}
|
||||
orderId: {type: string, nullable: true}
|
||||
orderNo: {type: string, nullable: true}
|
||||
inboundCode: {type: string, nullable: true}
|
||||
processStatus: {type: string}
|
||||
customerId: {type: string, nullable: true}
|
||||
companyId: {type: string, nullable: true}
|
||||
InboundMeasurementCallbackRequest:
|
||||
type: object
|
||||
required: [eventNo, externalDetailNo, warehouseCode, inboundCode, inboundDetailId, deviceCode, measuredAt, boxSpecification, packageCount, lengthCm, widthCm, heightCm]
|
||||
|
||||
@@ -3,13 +3,30 @@ package com.baiyue.firstleg.controller;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("FBA计费OpenAPI契约")
|
||||
class FbaPricingContractTest {
|
||||
@Test
|
||||
@DisplayName("只有客户规则列表允许省略企业参数")
|
||||
void onlyCustomerRuleListAllowsOptionalTenant() {
|
||||
Method rules = method("rules");
|
||||
assertThat(rules.getParameterTypes()[1]).isEqualTo(Long.class);
|
||||
assertThat(rules.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||
|
||||
for (String scopedMethod : new String[] {"rule", "createRule", "retireRule", "rates",
|
||||
"rate", "createRate", "retireRate"}) {
|
||||
Method method = method(scopedMethod);
|
||||
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
|
||||
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("契约声明自动发布、已发布费用调整和幂等头")
|
||||
void documentsPricingLifecycle() throws Exception {
|
||||
@@ -28,5 +45,26 @@ class FbaPricingContractTest {
|
||||
.doesNotContain("应收")
|
||||
.doesNotContain("应付")
|
||||
.doesNotContain("待客户确认");
|
||||
String customerRules = openapi.substring(
|
||||
openapi.indexOf("/api/platform/v1/fba-pricing/customer-rules:"),
|
||||
openapi.indexOf("/api/platform/v1/fba-pricing/customer-rules/versions:"));
|
||||
assertThat(customerRules)
|
||||
.contains("OptionalTenantId")
|
||||
.contains("name: ruleCode")
|
||||
.doesNotContain("#/components/parameters/Page")
|
||||
.doesNotContain("#/components/parameters/Size")
|
||||
.doesNotContain("name: customerName")
|
||||
.doesNotContain("name: chargeMode")
|
||||
.doesNotContain("name: status");
|
||||
assertThat(openapi)
|
||||
.contains("OptionalTenantId: {name: tenantId, in: query, required: false")
|
||||
.contains("schema: {type: string, pattern: '^[1-9][0-9]*$'}")
|
||||
.doesNotContain("format: int64");
|
||||
}
|
||||
|
||||
private static Method method(String name) {
|
||||
return java.util.Arrays.stream(PlatformFbaMasterDataController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(name))
|
||||
.findFirst().orElseThrow();
|
||||
}
|
||||
}
|
||||
|
||||
+21
-1
@@ -5,6 +5,7 @@ import com.baiyue.firstleg.dto.InboundOrderQuery;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -15,21 +16,40 @@ class InboundOrderControllerContractTest {
|
||||
@Test
|
||||
void shouldDeclareReadOnlyPermissions() throws Exception {
|
||||
assertAuthority("page", "hasAuthority('platform:firstleg:inbound:list')",
|
||||
AuthenticatedUser.class, long.class, InboundOrderQuery.class);
|
||||
AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
|
||||
assertAuthority("detail", "hasAuthority('platform:firstleg:inbound:view')",
|
||||
AuthenticatedUser.class, long.class, long.class);
|
||||
|
||||
Method page = PlatformInboundOrderController.class.getDeclaredMethod(
|
||||
"page", AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
|
||||
assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||
Method detail = PlatformInboundOrderController.class.getDeclaredMethod(
|
||||
"detail", AuthenticatedUser.class, long.class, long.class);
|
||||
assertThat(detail.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDocumentOnlyV2TabsAndStringLongIds() throws Exception {
|
||||
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String inboundList = openapi.substring(
|
||||
openapi.indexOf("/api/platform/inbounds:"),
|
||||
openapi.indexOf("/api/platform/inbounds/{inboundId}:"));
|
||||
String taskList = openapi.substring(
|
||||
openapi.indexOf("/api/platform/v1/first-leg/tasks:"),
|
||||
openapi.indexOf("/api/platform/v1/first-leg/tasks/{taskId}:"));
|
||||
assertThat(openapi)
|
||||
.contains("/api/platform/inbounds:")
|
||||
.contains("/api/platform/inbounds/{inboundId}:")
|
||||
.contains("required: false")
|
||||
.contains("InboundOrderSummary:")
|
||||
.contains("required: [id, tenantId]")
|
||||
.contains("tenantId: {type: string, pattern: '^[1-9][0-9]*$', description:")
|
||||
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]")
|
||||
.contains("platform:firstleg:inbound:list")
|
||||
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
|
||||
assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope");
|
||||
assertThat(taskList).doesNotContain("InboundOrderPageEnvelope");
|
||||
}
|
||||
|
||||
private static void assertAuthority(String methodName, String value, Class<?>... types)
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||
import com.baiyue.firstleg.mapper.CarrierFbaRateMapper;
|
||||
import com.baiyue.firstleg.mapper.CustomerChargeRuleMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundFeeItemMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundFeeMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundPricingIdempotencyMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundPricingMapper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class FbaPricingRepositoryRuleListTest {
|
||||
@BeforeAll
|
||||
static void initializeEntityMetadata() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"),
|
||||
CustomerChargeRuleEntity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRuleListOmitsTenantConditionWhenTenantIsAbsent() {
|
||||
assertThat(queryValues(null, null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRuleListKeepsTenantAndRuleCodeFiltersWhenProvided() {
|
||||
assertThat(queryValues(10001L, "CR-1")).containsExactlyInAnyOrder(10001L, "CR-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void internalVersionCalculationKeepsTenantAndRuleCodeFilters() {
|
||||
CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class);
|
||||
CustomerChargeRuleEntity current = new CustomerChargeRuleEntity();
|
||||
current.setRuleVersion(3);
|
||||
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(current));
|
||||
FbaPricingRepository repository = repository(rules);
|
||||
|
||||
int result = repository.nextRuleVersion(10001L, "CR-1");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaQueryWrapper<CustomerChargeRuleEntity>> wrapper =
|
||||
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(rules).selectList(wrapper.capture());
|
||||
wrapper.getValue().getSqlSegment();
|
||||
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
||||
.containsExactlyInAnyOrder(10001L, "CR-1");
|
||||
assertThat(result).isEqualTo(4);
|
||||
}
|
||||
|
||||
private static Collection<Object> queryValues(Long tenantId, String ruleCode) {
|
||||
CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class);
|
||||
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
FbaPricingRepository repository = repository(rules);
|
||||
|
||||
repository.listRules(tenantId, ruleCode);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaQueryWrapper<CustomerChargeRuleEntity>> wrapper =
|
||||
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(rules).selectList(wrapper.capture());
|
||||
wrapper.getValue().getSqlSegment();
|
||||
return wrapper.getValue().getParamNameValuePairs().values();
|
||||
}
|
||||
|
||||
private static FbaPricingRepository repository(CustomerChargeRuleMapper rules) {
|
||||
return new FbaPricingRepository(rules, mock(CarrierFbaRateMapper.class),
|
||||
mock(InboundPricingMapper.class), mock(InboundFeeMapper.class), mock(InboundFeeItemMapper.class),
|
||||
mock(InboundPricingIdempotencyMapper.class), mock(InboundOutboxMapper.class));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.dto.InboundOrderQuery;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||
import com.baiyue.firstleg.entity.InboundTransportSnapshotEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundFileMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundInternalNoteMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
@@ -73,8 +74,10 @@ class InboundOrderRepositoryTest {
|
||||
Page<InboundOrderEntity> page = new Page<>(1, 20);
|
||||
InboundOrderEntity first = new InboundOrderEntity();
|
||||
first.setId(1L);
|
||||
first.setTenantId(10001L);
|
||||
InboundOrderEntity second = new InboundOrderEntity();
|
||||
second.setId(2L);
|
||||
second.setTenantId(10001L);
|
||||
page.setRecords(java.util.List.of(first, second));
|
||||
when(orders.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
|
||||
when(pallets.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
|
||||
@@ -104,12 +107,85 @@ class InboundOrderRepositoryTest {
|
||||
.contains("ORDER BY inbound_id ASC,allocation_no ASC");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void shouldQueryAndAggregateCrossTenantPageByInboundOwnership() {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), InboundOrderEntity.class);
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||
InboundTransportSnapshotEntity.class);
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||
InboundPalletAllocationEntity.class);
|
||||
InboundOrderMapper orders = mock(InboundOrderMapper.class);
|
||||
InboundTransportSnapshotMapper transports = mock(InboundTransportSnapshotMapper.class);
|
||||
InboundPalletAllocationMapper pallets = mock(InboundPalletAllocationMapper.class);
|
||||
Page<InboundOrderEntity> page = new Page<>(1, 20);
|
||||
page.setRecords(java.util.List.of(order(1L, 10001L), order(2L, 10002L)));
|
||||
when(orders.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
|
||||
when(transports.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
|
||||
transport(1L, 10001L, "T1"),
|
||||
transport(2L, 10002L, "T2"),
|
||||
transport(1L, 10002L, "WRONG")));
|
||||
when(pallets.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
|
||||
allocation(1L, 10001L, 1, "A1"),
|
||||
allocation(2L, 10002L, 1, "B1"),
|
||||
allocation(1L, 10002L, 2, "WRONG")));
|
||||
InboundOrderRepository repository = new InboundOrderRepository(
|
||||
orders, transports, pallets, mock(InboundFileMapper.class),
|
||||
mock(InboundInternalNoteMapper.class), mock(InboundStatusHistoryMapper.class));
|
||||
|
||||
var result = repository.page(null, new InboundOrderQuery(
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, 1, 20));
|
||||
|
||||
assertThat(result.records()).extracting(record -> record.tenantId())
|
||||
.containsExactly(10001L, 10002L);
|
||||
assertThat(result.records()).extracting(record -> record.loadingStatus())
|
||||
.containsExactly("T1", "T2");
|
||||
assertThat(result.records()).extracting(record -> record.locationCodes())
|
||||
.containsExactly("A1", "B1");
|
||||
ArgumentCaptor<Wrapper<InboundOrderEntity>> orderCaptor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(orders).selectPage(any(Page.class), orderCaptor.capture());
|
||||
assertThat(orderCaptor.getValue().getCustomSqlSegment()).doesNotContain("tenant_id");
|
||||
|
||||
ArgumentCaptor<Wrapper<InboundTransportSnapshotEntity>> transportCaptor =
|
||||
ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(transports).selectList(transportCaptor.capture());
|
||||
assertThat(transportCaptor.getValue().getCustomSqlSegment())
|
||||
.doesNotContain("tenant_id")
|
||||
.contains("inbound_id IN");
|
||||
}
|
||||
|
||||
private static InboundPalletAllocationEntity allocation(
|
||||
long inboundId, int allocationNo, String locationCode) {
|
||||
return allocation(inboundId, 10001L, allocationNo, locationCode);
|
||||
}
|
||||
|
||||
private static InboundPalletAllocationEntity allocation(
|
||||
long inboundId, long tenantId, int allocationNo, String locationCode) {
|
||||
InboundPalletAllocationEntity entity = new InboundPalletAllocationEntity();
|
||||
entity.setInboundId(inboundId);
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setAllocationNo(allocationNo);
|
||||
entity.setLocationCode(locationCode);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static InboundOrderEntity order(long id, long tenantId) {
|
||||
InboundOrderEntity entity = new InboundOrderEntity();
|
||||
entity.setId(id);
|
||||
entity.setTenantId(tenantId);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static InboundTransportSnapshotEntity transport(
|
||||
long inboundId, long tenantId, String loadingStatus) {
|
||||
InboundTransportSnapshotEntity entity = new InboundTransportSnapshotEntity();
|
||||
entity.setInboundId(inboundId);
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setLoadingStatus(loadingStatus);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,27 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("FBA计费基础资料版本")
|
||||
class FbaMasterDataServiceTest {
|
||||
private static final LocalDateTime AUGUST = LocalDateTime.of(2026, 8, 1, 0, 0);
|
||||
|
||||
@Test
|
||||
@DisplayName("平台列表不传企业时委托仓储查询全部企业规则")
|
||||
void listsRulesAcrossTenantsWithoutTenantFilter() {
|
||||
FbaPricingRepository repository = mock(FbaPricingRepository.class);
|
||||
List<CustomerChargeRuleEntity> expected = List.of(activeRule(AUGUST, null));
|
||||
when(repository.listRules(null, "CR-1")).thenReturn(expected);
|
||||
|
||||
List<CustomerChargeRuleEntity> result =
|
||||
new FbaMasterDataService(repository).listRules(null, "CR-1");
|
||||
|
||||
assertThat(result).isSameAs(expected);
|
||||
verify(repository).listRules(null, "CR-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("半开有效期首尾相接不视为重叠")
|
||||
void acceptsAdjacentHalfOpenPeriods() {
|
||||
|
||||
@@ -21,6 +21,17 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class InboundOrderQueryServiceTest {
|
||||
@Test
|
||||
void shouldQueryAllPlatformTenantsWhenTenantFilterIsMissing() {
|
||||
InboundOrderRepository repository = mock(InboundOrderRepository.class);
|
||||
InboundOrderQuery query = query(null, null, null, null);
|
||||
PageResult<InboundOrderSummary> expected = new PageResult<>(List.of(), 1, 20, 0, 0);
|
||||
when(repository.page(null, query)).thenReturn(expected);
|
||||
|
||||
assertThat(new InboundOrderQueryService(repository).page(null, query)).isSameAs(expected);
|
||||
verify(repository).page(null, query);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepV2TabMappingWithoutCustomerConfirmation() {
|
||||
assertThat(InboundListTab.WAIT_MEASURE.processStatuses())
|
||||
|
||||
Reference in New Issue
Block a user