优化
This commit is contained in:
@@ -18,6 +18,6 @@ public class InboundProperties {
|
||||
private boolean allowRejectedMeasurement = false;
|
||||
private BigDecimal defaultPalletCapacityCbm = new BigDecimal("1.200000");
|
||||
private Duration reservationTtl = Duration.ofMinutes(30);
|
||||
/** 共享收货仓资源所属租户;0 表示与入库业务租户一致。 */
|
||||
/** 历史配置兼容字段;平台公共仓资源当前固定使用技术作用域 0。 */
|
||||
private long warehouseResourceTenantId = 0L;
|
||||
}
|
||||
|
||||
+3
-10
@@ -3,10 +3,9 @@ package com.baiyue.firstleg.controller;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
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.MerchantInboundBillingResponse;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.MerchantInboundBillingService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -33,10 +32,7 @@ public class MerchantInboundBillingController {
|
||||
description = "客户只读查看客服审核通过后发布的费用,不需要确认费用。")
|
||||
public ApiResponse<MerchantInboundBillingResponse> find(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
|
||||
if (user == null || !"MERCHANT".equals(user.appCode()) || user.tenantId() == null) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许客户端访问");
|
||||
}
|
||||
return ApiResponse.success(service.find(user.tenantId(), orderId));
|
||||
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/inbound-billing/images/{fileId}")
|
||||
@@ -46,10 +42,7 @@ public class MerchantInboundBillingController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long orderId,
|
||||
@PathVariable long fileId) {
|
||||
if (user == null || !"MERCHANT".equals(user.appCode()) || user.tenantId() == null) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许客户端访问");
|
||||
}
|
||||
var image = service.downloadImage(user.tenantId(), orderId, fileId);
|
||||
var image = service.downloadImage(FirstlegScopeGuard.merchant(user), orderId, fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.contentType(MediaType.parseMediaType(image.contentType()))
|
||||
.contentLength(image.content().length).body(image.content());
|
||||
|
||||
+1
-5
@@ -4,8 +4,6 @@ import jakarta.annotation.Resource;
|
||||
|
||||
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;
|
||||
@@ -114,9 +112,7 @@ public class PlatformFbaMasterDataController {
|
||||
}
|
||||
|
||||
private static Long optionalPlatformTenant(AuthenticatedUser user, Long tenantId) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return tenantId == null ? null : FirstlegScopeGuard.platform(user, tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-23
@@ -3,14 +3,13 @@ package com.baiyue.firstleg.controller;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
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.AssignRequest;
|
||||
import com.baiyue.firstleg.dto.FulfillmentTaskQuery;
|
||||
import com.baiyue.firstleg.entity.AssignmentHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
|
||||
import com.baiyue.firstleg.service.FulfillmentTaskService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
@@ -65,8 +64,7 @@ public class PlatformFulfillmentAssignmentController {
|
||||
@Parameter(description = "First-leg fulfillment task ID / 头程履约任务 ID", example = "190000000000000101", required = true)
|
||||
@PathVariable long taskId,
|
||||
@Valid @RequestBody AssignRequest request) {
|
||||
requirePlatform(user);
|
||||
requireTenant(tenantId);
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
service.assign(
|
||||
tenantId,
|
||||
user.userId(),
|
||||
@@ -89,8 +87,7 @@ public class PlatformFulfillmentAssignmentController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@Valid FulfillmentTaskQuery query) {
|
||||
requirePlatform(user);
|
||||
requireTenant(tenantId);
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.page(tenantId, query));
|
||||
}
|
||||
|
||||
@@ -105,8 +102,7 @@ public class PlatformFulfillmentAssignmentController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long taskId) {
|
||||
requirePlatform(user);
|
||||
requireTenant(tenantId);
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.detail(tenantId, taskId));
|
||||
}
|
||||
|
||||
@@ -121,8 +117,7 @@ public class PlatformFulfillmentAssignmentController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long orderId) {
|
||||
requirePlatform(user);
|
||||
requireTenant(tenantId);
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.byOrder(tenantId, orderId));
|
||||
}
|
||||
|
||||
@@ -139,20 +134,8 @@ public class PlatformFulfillmentAssignmentController {
|
||||
@PathVariable long taskId,
|
||||
@RequestParam(defaultValue = "1") @Min(1) long page,
|
||||
@RequestParam(defaultValue = "20") @Min(1) @Max(200) long size) {
|
||||
requirePlatform(user);
|
||||
requireTenant(tenantId);
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.assignmentHistory(tenantId, taskId, page, size));
|
||||
}
|
||||
|
||||
private static void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireTenant(long tenantId) {
|
||||
if (tenantId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -9,6 +9,7 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
|
||||
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.InboundMeasurementService;
|
||||
import com.baiyue.firstleg.service.MeasurementSubmissionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -120,10 +121,8 @@ public class PlatformInboundMeasurementController {
|
||||
}
|
||||
|
||||
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
if (tenantId <= 0 || user.userId() == null || user.userId() <= 0) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
if (user.userId() == null || user.userId() <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户和操作人");
|
||||
}
|
||||
}
|
||||
|
||||
+2
-8
@@ -5,14 +5,13 @@ import jakarta.annotation.Resource;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
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.ExceptionReleaseRequest;
|
||||
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
|
||||
import com.baiyue.firstleg.entity.MeasureReviewRecordEntity;
|
||||
import com.baiyue.firstleg.service.MeasurementReviewService;
|
||||
import com.baiyue.firstleg.service.AutomaticInboundCompletionService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -108,11 +107,6 @@ public class PlatformInboundMeasurementReviewController {
|
||||
}
|
||||
|
||||
private static void validateScope(AuthenticatedUser user, long tenantId) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
if (tenantId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
|
||||
}
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-7
@@ -10,6 +10,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.InboundOrderDetail;
|
||||
import com.baiyue.firstleg.dto.InboundOrderQuery;
|
||||
import com.baiyue.firstleg.dto.InboundOrderSummary;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.InboundOrderQueryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -61,15 +62,10 @@ public class PlatformInboundOrderController {
|
||||
}
|
||||
|
||||
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
|
||||
requirePlatform(user);
|
||||
if (tenantId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
|
||||
}
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
}
|
||||
|
||||
private static void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
FirstlegScopeGuard.platform(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ public interface InboundIdempotencyMapper extends BaseMapper<InboundIdempotencyE
|
||||
@Update("""
|
||||
UPDATE fl_inbound_idempotency
|
||||
SET status = 'SUCCESS', result_json = #{resultJson}, completed_at = CURRENT_TIMESTAMP(3)
|
||||
WHERE id = #{id} AND status = 'PROCESSING'
|
||||
WHERE tenant_id = #{tenantId} AND id = #{id} AND status = 'PROCESSING'
|
||||
""")
|
||||
int complete(@Param("id") long id, @Param("resultJson") String resultJson);
|
||||
int complete(@Param("tenantId") long tenantId, @Param("id") long id,
|
||||
@Param("resultJson") String resultJson);
|
||||
}
|
||||
|
||||
@@ -250,6 +250,7 @@ public class FbaPricingRepository {
|
||||
|
||||
public int completeIdempotency(InboundPricingIdempotencyEntity value, long resourceId, LocalDateTime now) {
|
||||
return idempotencies.update(null, new LambdaUpdateWrapper<InboundPricingIdempotencyEntity>()
|
||||
.eq(InboundPricingIdempotencyEntity::getTenantId, value.getTenantId())
|
||||
.eq(InboundPricingIdempotencyEntity::getId, value.getId())
|
||||
.eq(InboundPricingIdempotencyEntity::getStatus, "PROCESSING")
|
||||
.set(InboundPricingIdempotencyEntity::getStatus, "COMPLETED")
|
||||
|
||||
@@ -60,11 +60,15 @@ public class FeeItemRepository {
|
||||
}
|
||||
|
||||
public int updateItem(FeeItemEntity item) {
|
||||
return feeItems.updateById(item);
|
||||
return feeItems.update(item, new LambdaUpdateWrapper<FeeItemEntity>()
|
||||
.eq(FeeItemEntity::getTenantId, item.getTenantId())
|
||||
.eq(FeeItemEntity::getId, item.getId()));
|
||||
}
|
||||
|
||||
public int deleteItem(long id) {
|
||||
return feeItems.deleteById(id);
|
||||
public int deleteItem(long tenantId, long id) {
|
||||
return feeItems.delete(new LambdaQueryWrapper<FeeItemEntity>()
|
||||
.eq(FeeItemEntity::getTenantId, tenantId)
|
||||
.eq(FeeItemEntity::getId, id));
|
||||
}
|
||||
|
||||
public long countBindings(long tenantId, long feeItemId) {
|
||||
@@ -99,11 +103,15 @@ public class FeeItemRepository {
|
||||
}
|
||||
|
||||
public int updateDetail(InboundFeeDetailEntity detail) {
|
||||
return feeDetails.updateById(detail);
|
||||
return feeDetails.update(detail, new LambdaUpdateWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, detail.getTenantId())
|
||||
.eq(InboundFeeDetailEntity::getId, detail.getId()));
|
||||
}
|
||||
|
||||
public int deleteDetail(long detailId) {
|
||||
return feeDetails.deleteById(detailId);
|
||||
public int deleteDetail(long tenantId, long detailId) {
|
||||
return feeDetails.delete(new LambdaQueryWrapper<InboundFeeDetailEntity>()
|
||||
.eq(InboundFeeDetailEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeDetailEntity::getId, detailId));
|
||||
}
|
||||
|
||||
public int review(long tenantId, long inboundId, long detailId, String targetStatus,
|
||||
|
||||
+4
-1
@@ -5,6 +5,7 @@ import jakarta.annotation.Resource;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.firstleg.dto.FulfillmentTaskQuery;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.entity.AssignmentHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
|
||||
@@ -53,7 +54,9 @@ public class FulfillmentTaskRepository {
|
||||
}
|
||||
|
||||
public boolean update(FulfillmentTaskEntity task) {
|
||||
return tasks.updateById(task) == 1;
|
||||
return tasks.update(task, new LambdaUpdateWrapper<FulfillmentTaskEntity>()
|
||||
.eq(FulfillmentTaskEntity::getTenantId, task.getTenantId())
|
||||
.eq(FulfillmentTaskEntity::getId, task.getId())) == 1;
|
||||
}
|
||||
|
||||
public void insertHistory(AssignmentHistoryEntity history) {
|
||||
|
||||
+10
-3
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.repository;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
@@ -42,7 +43,9 @@ public class InboundMeasurementRepository {
|
||||
}
|
||||
|
||||
public boolean updateInbound(InboundOrderEntity inbound) {
|
||||
return inboundOrders.updateById(inbound) == 1;
|
||||
return inboundOrders.update(inbound, new LambdaUpdateWrapper<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, inbound.getTenantId())
|
||||
.eq(InboundOrderEntity::getId, inbound.getId())) == 1;
|
||||
}
|
||||
|
||||
public InboundMeasurementEntity findMeasurement(long tenantId, long inboundId, long measurementId) {
|
||||
@@ -72,7 +75,9 @@ public class InboundMeasurementRepository {
|
||||
}
|
||||
|
||||
public boolean updateMeasurement(InboundMeasurementEntity measurement) {
|
||||
return measurements.updateById(measurement) == 1;
|
||||
return measurements.update(measurement, new LambdaUpdateWrapper<InboundMeasurementEntity>()
|
||||
.eq(InboundMeasurementEntity::getTenantId, measurement.getTenantId())
|
||||
.eq(InboundMeasurementEntity::getId, measurement.getId())) == 1;
|
||||
}
|
||||
|
||||
public void replaceItems(long tenantId, long measurementId,
|
||||
@@ -109,7 +114,9 @@ public class InboundMeasurementRepository {
|
||||
}
|
||||
|
||||
public boolean updateWeight(InboundWeightResultEntity weight) {
|
||||
return weights.updateById(weight) == 1;
|
||||
return weights.update(weight, new LambdaUpdateWrapper<InboundWeightResultEntity>()
|
||||
.eq(InboundWeightResultEntity::getTenantId, weight.getTenantId())
|
||||
.eq(InboundWeightResultEntity::getId, weight.getId())) == 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -108,8 +108,8 @@ public class InboundWorkflowRepository {
|
||||
.eq(InboundIdempotencyEntity::getIdempotencyKey, key));
|
||||
}
|
||||
|
||||
public int completeIdempotency(long id, String resultJson) {
|
||||
return idempotencies.complete(id, resultJson);
|
||||
public int completeIdempotency(long tenantId, long id, String resultJson) {
|
||||
return idempotencies.complete(tenantId, id, resultJson);
|
||||
}
|
||||
|
||||
public InboundFeeEntity findConfirmedFee(long tenantId, long inboundId, long feeId) {
|
||||
|
||||
@@ -67,7 +67,7 @@ public class FeeItemService {
|
||||
if (repository.countBindings(tenantId, id) > 0) {
|
||||
throw conflict("费用项目已被入库费用明细使用,只能停用,不能删除");
|
||||
}
|
||||
if (repository.deleteItem(id) != 1) throw conflict("费用项目删除冲突");
|
||||
if (repository.deleteItem(tenantId, id) != 1) throw conflict("费用项目删除冲突");
|
||||
}
|
||||
|
||||
private FeeItemEntity required(long tenantId, long id) {
|
||||
|
||||
@@ -4,17 +4,30 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
|
||||
/** 头程平台接口租户边界校验。 */
|
||||
/** 头程平台与客户接口的主体边界校验。 */
|
||||
public final class FirstlegScopeGuard {
|
||||
private FirstlegScopeGuard() {}
|
||||
|
||||
public static long platform(AuthenticatedUser user, long targetTenantId) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
platform(user);
|
||||
if (targetTenantId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
|
||||
}
|
||||
return targetTenantId;
|
||||
}
|
||||
|
||||
public static void platform(AuthenticatedUser user) {
|
||||
if (user == null || !"PLATFORM".equals(user.appCode())
|
||||
|| user.tenantId() == null || user.tenantId() != 0L) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||
}
|
||||
}
|
||||
|
||||
public static long merchant(AuthenticatedUser user) {
|
||||
if (user == null || !"MERCHANT".equals(user.appCode())
|
||||
|| user.tenantId() == null || user.tenantId() <= 0L) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许客户端访问");
|
||||
}
|
||||
return user.tenantId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public class InboundFeeDetailService {
|
||||
@Transactional
|
||||
public void delete(long tenantId, long inboundId, long detailId) {
|
||||
requiredEditable(tenantId, inboundId, detailId);
|
||||
if (repository.deleteDetail(detailId) != 1) throw conflict("费用明细删除冲突");
|
||||
if (repository.deleteDetail(tenantId, detailId) != 1) throw conflict("费用明细删除冲突");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
@@ -44,7 +44,7 @@ public class InboundIdempotencyService {
|
||||
record.setUpdatedAt(record.getCreatedAt());
|
||||
try {
|
||||
repository.insertIdempotency(record);
|
||||
return new Context(record.getId(), false);
|
||||
return new Context(tenantId, record.getId(), false);
|
||||
} catch (DuplicateKeyException duplicate) {
|
||||
InboundIdempotencyEntity existing = repository.findIdempotency(
|
||||
tenantId, operation, operatorId, key);
|
||||
@@ -53,7 +53,7 @@ public class InboundIdempotencyService {
|
||||
"Idempotency-Key 已用于不同请求");
|
||||
}
|
||||
if ("SUCCESS".equals(existing.getStatus())) {
|
||||
return new Context(existing.getId(), true);
|
||||
return new Context(tenantId, existing.getId(), true);
|
||||
}
|
||||
throw new BizException(ResultCode.CONFLICT, "相同请求正在处理中,请稍后重试");
|
||||
}
|
||||
@@ -61,7 +61,8 @@ public class InboundIdempotencyService {
|
||||
|
||||
public void complete(Context context, long inboundId) {
|
||||
if (!context.replayed()
|
||||
&& repository.completeIdempotency(context.id(), "{\"inboundId\":\"" + inboundId + "\"}") != 1) {
|
||||
&& repository.completeIdempotency(context.tenantId(), context.id(),
|
||||
"{\"inboundId\":\"" + inboundId + "\"}") != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "幂等记录已被并发修改");
|
||||
}
|
||||
}
|
||||
@@ -75,6 +76,6 @@ public class InboundIdempotencyService {
|
||||
}
|
||||
}
|
||||
|
||||
public record Context(long id, boolean replayed) {
|
||||
public record Context(long tenantId, long id, boolean replayed) {
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,6 +20,7 @@ import java.time.LocalDateTime;
|
||||
@Service
|
||||
public class MeasurementLocationOccupationService {
|
||||
private static final String SOURCE_SERVICE = "firstleg-service";
|
||||
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
|
||||
@Resource
|
||||
private WarehousePalletLocationClient warehouse;
|
||||
@Resource
|
||||
@@ -35,8 +36,7 @@ public class MeasurementLocationOccupationService {
|
||||
int measurementVersion) {
|
||||
InboundOrderEntity order = requireOrder(tenantId, inboundId);
|
||||
long warehouseId = requireWarehouse(order);
|
||||
long resourceTenantId = properties.getWarehouseResourceTenantId() > 0
|
||||
? properties.getWarehouseResourceTenantId() : tenantId;
|
||||
long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID;
|
||||
BigDecimal volume = requireVolume(order);
|
||||
String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion;
|
||||
WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve(
|
||||
|
||||
@@ -4,6 +4,7 @@ import jakarta.annotation.Resource;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baiyue.firstleg.config.MeasurementOpenApiProperties;
|
||||
import com.baiyue.firstleg.entity.ThirdPartyCallLogEntity;
|
||||
import com.baiyue.firstleg.mapper.ThirdPartyCallLogMapper;
|
||||
@@ -55,6 +56,10 @@ public class ThirdPartyCallLogService {
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void complete(long id, String status, String code, String message, String resultJson) {
|
||||
Long tenantId = tenantId(id);
|
||||
if (tenantId == null) {
|
||||
return;
|
||||
}
|
||||
ThirdPartyCallLogEntity update = new ThirdPartyCallLogEntity();
|
||||
update.setId(id);
|
||||
update.setProcessingStatus(status);
|
||||
@@ -62,18 +67,25 @@ public class ThirdPartyCallLogService {
|
||||
update.setResultMessage(limit(message, 1000));
|
||||
update.setResultJson(resultJson);
|
||||
update.setProcessedAt(LocalDateTime.now());
|
||||
mapper.updateById(update);
|
||||
mapper.update(update, new LambdaUpdateWrapper<ThirdPartyCallLogEntity>()
|
||||
.eq(ThirdPartyCallLogEntity::getTenantId, tenantId)
|
||||
.eq(ThirdPartyCallLogEntity::getId, id));
|
||||
}
|
||||
|
||||
/** 签名通过后原子认领 Idempotency-Key;非法签名不会污染幂等空间。 */
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public boolean claimIdempotency(long logId) {
|
||||
Long tenantId = tenantId(logId);
|
||||
if (tenantId == null) {
|
||||
return false;
|
||||
}
|
||||
ThirdPartyCallLogEntity update = new ThirdPartyCallLogEntity();
|
||||
update.setId(logId);
|
||||
update.setIdempotencyAnchor(1);
|
||||
try {
|
||||
mapper.updateById(update);
|
||||
return true;
|
||||
return mapper.update(update, new LambdaUpdateWrapper<ThirdPartyCallLogEntity>()
|
||||
.eq(ThirdPartyCallLogEntity::getTenantId, tenantId)
|
||||
.eq(ThirdPartyCallLogEntity::getId, logId)) == 1;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
return false;
|
||||
}
|
||||
@@ -87,6 +99,11 @@ public class ThirdPartyCallLogService {
|
||||
.eq(ThirdPartyCallLogEntity::getIdempotencyAnchor, 1));
|
||||
}
|
||||
|
||||
private Long tenantId(long id) {
|
||||
ThirdPartyCallLogEntity log = mapper.selectById(id);
|
||||
return log == null ? null : log.getTenantId();
|
||||
}
|
||||
|
||||
private String limit(String value, int max) {
|
||||
if (value == null || value.length() <= max) {
|
||||
return value;
|
||||
|
||||
@@ -41,17 +41,18 @@ CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_from', "DATETIME(3)
|
||||
CALL phase2_fba_add_column('fl_inbound_fee', 'rate_effective_to', "DATETIME(3) NULL COMMENT '价格生效结束快照' AFTER rate_effective_from");
|
||||
CALL phase2_fba_add_column('fl_inbound_idempotency', 'result_resource_id', "BIGINT UNSIGNED NULL COMMENT '成功结果资源ID' AFTER result_json");
|
||||
CALL phase2_fba_add_column('fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '仓储资源所属租户ID' AFTER warehouse_id");
|
||||
"BIGINT UNSIGNED NULL COMMENT '平台公共仓储资源固定使用 0;tenant_id 仍是客户隔离键' AFTER warehouse_id");
|
||||
|
||||
-- 旧分配记录创建于共享仓储资源租户拆分前,按原业务租户回填后再收口非空约束。
|
||||
-- 仓储资源是平台公共主数据;存量分配快照统一迁移到平台作用域 0,再收口非空约束。
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET warehouse_resource_tenant_id = tenant_id
|
||||
WHERE warehouse_resource_tenant_id IS NULL OR warehouse_resource_tenant_id = 0;
|
||||
SET @resource_tenant_nullable = (SELECT IS_NULLABLE FROM information_schema.COLUMNS
|
||||
SET warehouse_resource_tenant_id = 0
|
||||
WHERE warehouse_resource_tenant_id IS NULL OR warehouse_resource_tenant_id <> 0;
|
||||
SET @resource_tenant_needs_modify = (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fl_inbound_pallet_allocation'
|
||||
AND COLUMN_NAME = 'warehouse_resource_tenant_id');
|
||||
SET @sql = IF(@resource_tenant_nullable = 'YES',
|
||||
"ALTER TABLE fl_inbound_pallet_allocation MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL COMMENT '仓储资源所属租户ID'",
|
||||
AND COLUMN_NAME = 'warehouse_resource_tenant_id'
|
||||
AND (IS_NULLABLE = 'YES' OR COLUMN_COMMENT <> '平台公共仓储资源固定使用 0;tenant_id 仍是客户隔离键'));
|
||||
SET @sql = IF(@resource_tenant_needs_modify > 0,
|
||||
"ALTER TABLE fl_inbound_pallet_allocation MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共仓储资源固定使用 0;tenant_id 仍是客户隔离键'",
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_fee_item (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '费用项目ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '账号ID,沿用tenant_id字段名',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户',
|
||||
category VARCHAR(64) NOT NULL COMMENT '费用分类',
|
||||
trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际',
|
||||
fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称',
|
||||
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS fl_fee_item (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库费用明细ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '账号ID,沿用tenant_id字段名',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '绑定费用项目ID',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
||||
@@ -70,3 +70,23 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail (
|
||||
CONSTRAINT fk_inbound_fee_detail_order FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id),
|
||||
CONSTRAINT fk_inbound_fee_detail_item FOREIGN KEY (fee_item_id) REFERENCES fl_fee_item (id)
|
||||
) ENGINE=InnoDB COMMENT='入库单应收应付费用明细';
|
||||
|
||||
-- 既有表同步字段注释:tenant_id 当前承载客户 ID,不是上级租户 ID。
|
||||
SET @fee_tenant_comment = '当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户';
|
||||
SET @ddl = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_fee_item'
|
||||
AND column_name = 'tenant_id' AND column_comment = @fee_tenant_comment) = 1,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE fl_fee_item MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT ''当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户'''
|
||||
);
|
||||
PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
|
||||
SET @ddl = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_fee_detail'
|
||||
AND column_name = 'tenant_id' AND column_comment = @fee_tenant_comment) = 1,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE fl_inbound_fee_detail MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL COMMENT ''当前客户ID(客户隔离键);物理字段名沿用 tenant_id,当前不表示上级租户'''
|
||||
);
|
||||
PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
|
||||
@@ -423,7 +423,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation (
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||
allocation_no INT UNSIGNED NOT NULL COMMENT '入库单内分配序号',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '仓库服务仓库ID',
|
||||
warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL COMMENT '仓储资源所属租户ID',
|
||||
warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共仓储资源固定使用 0;tenant_id 仍是客户隔离键',
|
||||
pallet_id BIGINT UNSIGNED NOT NULL COMMENT '仓库服务托盘ID',
|
||||
pallet_code VARCHAR(50) NOT NULL COMMENT '托盘编码快照',
|
||||
pallet_capacity_cbm DECIMAL(18,4) NOT NULL COMMENT '托盘可用容积快照',
|
||||
|
||||
@@ -4,7 +4,7 @@ USE baiyue_firstleg;
|
||||
-- 既有二期数据库增量:先确保 phase2-inbound-schema.sql 中的测量和幂等表已存在。
|
||||
CREATE TABLE IF NOT EXISTS fl_third_party_call_log (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '第三方调用日志ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '识别出的租户ID,未知应用为0',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '第三方未知/未解析主体哨兵值;不是平台客户记录或平台作用域,不得用于业务数据授权',
|
||||
app_key VARCHAR(128) NULL COMMENT '第三方应用标识',
|
||||
signature_subject VARCHAR(128) NULL COMMENT '签名主体快照',
|
||||
request_method VARCHAR(8) NOT NULL COMMENT 'HTTP方法',
|
||||
@@ -28,3 +28,14 @@ CREATE TABLE IF NOT EXISTS fl_third_party_call_log (
|
||||
KEY idx_third_call_nonce (tenant_id, app_key, nonce),
|
||||
UNIQUE KEY uk_third_call_idempotency_owner (tenant_id, app_key, idempotency_key, idempotency_anchor)
|
||||
) ENGINE=InnoDB COMMENT='第三方测量开放接口原始请求、签名主体和处理结果留档';
|
||||
|
||||
-- 既有表也要同步未知主体 0 的语义,避免历史注释继续误导为真实租户。
|
||||
SET @third_party_tenant_comment = '第三方未知/未解析主体哨兵值;不是平台客户记录或平台作用域,不得用于业务数据授权';
|
||||
SET @ddl = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_third_party_call_log'
|
||||
AND column_name = 'tenant_id' AND column_comment = @third_party_tenant_comment) = 1,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE fl_third_party_call_log MODIFY COLUMN tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''第三方未知/未解析主体哨兵值;不是平台客户记录或平台作用域,不得用于业务数据授权'''
|
||||
);
|
||||
PREPARE statement FROM @ddl; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
|
||||
@@ -91,8 +91,8 @@ components:
|
||||
securitySchemes:
|
||||
bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT}
|
||||
parameters:
|
||||
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]*$'}}
|
||||
OptionalTenantId: {name: tenantId, in: query, required: false, description: 可选目标客户 ID;参数名为历史兼容,不传时查询全部企业, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
TenantId: {name: tenantId, in: query, required: true, description: 目标客户 ID;参数名为历史兼容, 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}}
|
||||
|
||||
@@ -133,7 +133,7 @@ paths:
|
||||
- {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]}}
|
||||
responses:
|
||||
'200':
|
||||
description: 成功或业务失败,按响应体 code 判断;每条记录携带后续租户隔离操作所需的 tenantId
|
||||
description: 成功或业务失败,按响应体 code 判断;每条记录携带后续客户隔离操作所需的 tenantId,该字段当前表示客户 ID
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/InboundOrderPageEnvelope'}
|
||||
@@ -184,7 +184,7 @@ paths:
|
||||
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 非本租户或无订单查看权限}
|
||||
'403': {description: 非当前客户或无订单查看权限}
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/exception-release:
|
||||
post:
|
||||
summary: 测量异常放行
|
||||
@@ -276,7 +276,7 @@ components:
|
||||
required: [id, tenantId]
|
||||
properties:
|
||||
id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$', description: 记录所属企业租户 ID,用于详情和处理接口的租户隔离}
|
||||
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}
|
||||
|
||||
+13
@@ -14,6 +14,8 @@ import java.util.Set;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@SpringJUnitConfig(MerchantInboundBillingController.class)
|
||||
class MerchantInboundBillingControllerContractTest {
|
||||
@@ -38,6 +40,17 @@ class MerchantInboundBillingControllerContractTest {
|
||||
"admin", "PLATFORM", "WEB", Set.of());
|
||||
assertThatThrownBy(() -> controller.find(platform, 77L)).isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantTechnicalTenantCannotReadCustomerBilling() {
|
||||
var invalidMerchant = new AuthenticatedUser(1L, 0L, 0L, 1L,
|
||||
"merchant", "MERCHANT", "WEB", Set.of());
|
||||
|
||||
assertThatThrownBy(() -> controller.find(invalidMerchant, 77L))
|
||||
.isInstanceOf(BizException.class);
|
||||
verify(service, never()).find(0L, 77L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleCase03() throws Exception {
|
||||
var method = MerchantInboundBillingController.class.getDeclaredMethod(
|
||||
|
||||
+29
@@ -1,9 +1,13 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||
import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
|
||||
import com.baiyue.firstleg.mapper.CarrierFbaRateMapper;
|
||||
import com.baiyue.firstleg.mapper.CustomerChargeRuleMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundFeeItemMapper;
|
||||
@@ -85,6 +89,26 @@ class FbaPricingRepositoryRuleListTest {
|
||||
assertThat(result).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void idempotencyCompletionKeepsTenantAndRecordFilters() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||
InboundPricingIdempotencyEntity.class);
|
||||
InboundPricingIdempotencyEntity value = new InboundPricingIdempotencyEntity();
|
||||
value.setTenantId(10001L);
|
||||
value.setId(88L);
|
||||
|
||||
repository.completeIdempotency(value, 501L, java.time.LocalDateTime.now());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.Wrapper<InboundPricingIdempotencyEntity>> wrapper =
|
||||
ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
|
||||
verify(idempotency).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
|
||||
assertThat(wrapper.getValue().getCustomSqlSegment())
|
||||
.contains("tenant_id")
|
||||
.containsPattern("(?i)(^|\\W)id\\s*=");
|
||||
assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L);
|
||||
}
|
||||
|
||||
private Collection<Object> queryValues(Long tenantId, String ruleCode) {
|
||||
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
|
||||
@@ -111,4 +135,9 @@ class FbaPricingRepositoryRuleListTest {
|
||||
return wrapper.getValue().getParamNameValuePairs().values();
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static java.util.Map<String, Object> params(Wrapper<?> wrapper) {
|
||||
return ((AbstractWrapper) wrapper).getParamNameValuePairs();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ class InboundWorkflowSqlContractTest {
|
||||
|
||||
assertThat(migration)
|
||||
.contains("phase2_fba_add_column('fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id'")
|
||||
.contains("SET warehouse_resource_tenant_id = tenant_id")
|
||||
.contains("SET warehouse_resource_tenant_id = 0")
|
||||
.contains("MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL");
|
||||
}
|
||||
}
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baiyue.firstleg.entity.FeeItemEntity;
|
||||
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
|
||||
import com.baiyue.firstleg.mapper.AssignmentHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.FeeItemMapper;
|
||||
import com.baiyue.firstleg.mapper.FulfillmentTaskMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundFeeDetailMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundMeasurementItemMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundMeasurementMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundWeightResultMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundWeightRevisionMapper;
|
||||
import com.baiyue.firstleg.mapper.MeasureReviewRecordMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig({FulfillmentTaskRepository.class, InboundMeasurementRepository.class,
|
||||
FeeItemRepository.class})
|
||||
class TenantScopedMutationRepositoryTest {
|
||||
private static final long TENANT = 10001L;
|
||||
private static final long ID = 88L;
|
||||
|
||||
@MockBean private FulfillmentTaskMapper tasks;
|
||||
@MockBean private AssignmentHistoryMapper histories;
|
||||
@MockBean private InboundOrderMapper inboundOrders;
|
||||
@MockBean private InboundMeasurementMapper measurements;
|
||||
@MockBean private InboundMeasurementItemMapper measurementItems;
|
||||
@MockBean private InboundWeightResultMapper weights;
|
||||
@MockBean private InboundWeightRevisionMapper revisions;
|
||||
@MockBean private MeasureReviewRecordMapper reviews;
|
||||
@MockBean private FeeItemMapper feeItems;
|
||||
@MockBean private InboundFeeDetailMapper feeDetails;
|
||||
@Resource private FulfillmentTaskRepository fulfillmentRepository;
|
||||
@Resource private InboundMeasurementRepository measurementRepository;
|
||||
@Resource private FeeItemRepository feeRepository;
|
||||
|
||||
@BeforeAll
|
||||
static void initializeMetadata() {
|
||||
initialize(FulfillmentTaskEntity.class);
|
||||
initialize(InboundWeightResultEntity.class);
|
||||
initialize(InboundFeeDetailEntity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void fulfillmentUpdateMatchesTenantAndId() {
|
||||
FulfillmentTaskEntity task = new FulfillmentTaskEntity();
|
||||
task.setTenantId(TENANT);
|
||||
task.setId(ID);
|
||||
when(tasks.update(any(), any(Wrapper.class))).thenReturn(1);
|
||||
|
||||
assertThat(fulfillmentRepository.update(task)).isTrue();
|
||||
|
||||
ArgumentCaptor<Wrapper<FulfillmentTaskEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(tasks).update(eq(task), wrapper.capture());
|
||||
assertTenantAndId(wrapper.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void measurementUpdateMatchesTenantAndId() {
|
||||
InboundWeightResultEntity weight = new InboundWeightResultEntity();
|
||||
weight.setTenantId(TENANT);
|
||||
weight.setId(ID);
|
||||
when(weights.update(any(), any(Wrapper.class))).thenReturn(1);
|
||||
|
||||
assertThat(measurementRepository.updateWeight(weight)).isTrue();
|
||||
|
||||
ArgumentCaptor<Wrapper<InboundWeightResultEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(weights).update(eq(weight), wrapper.capture());
|
||||
assertTenantAndId(wrapper.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void feeDetailDeleteMatchesTenantAndId() {
|
||||
when(feeDetails.delete(any(Wrapper.class))).thenReturn(1);
|
||||
|
||||
assertThat(feeRepository.deleteDetail(TENANT, ID)).isEqualTo(1);
|
||||
|
||||
ArgumentCaptor<Wrapper<InboundFeeDetailEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(feeDetails).delete(wrapper.capture());
|
||||
assertTenantAndId(wrapper.getValue());
|
||||
}
|
||||
|
||||
private static void initialize(Class<?> entityType) {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), entityType);
|
||||
}
|
||||
|
||||
private static void assertTenantAndId(Wrapper<?> wrapper) {
|
||||
assertThat(wrapper.getCustomSqlSegment())
|
||||
.contains("tenant_id")
|
||||
.containsPattern("(?i)(^|\\W)id\\s*=");
|
||||
assertThat(((AbstractWrapper<?, ?, ?>) wrapper).getParamNameValuePairs().values())
|
||||
.contains(TENANT, ID);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,18 @@ class FbaPricingServiceTest {
|
||||
@MockBean private FbaPricingRepository repository;
|
||||
@Resource private FbaPricingService service;
|
||||
|
||||
@Test
|
||||
@DisplayName("客户B不能读取客户A的费用快照")
|
||||
void rejectsCrossCustomerFeeLookup() {
|
||||
assertThatThrownBy(() -> service.latest(10002L, INBOUND))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.NOT_FOUND);
|
||||
|
||||
verify(repository).findLatestFee(10002L, INBOUND);
|
||||
verify(repository, never()).findLatestFee(TENANT, INBOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("包税KG按配置系数取体积重较大值并按规则向上舍入")
|
||||
void calculatesTaxIncludedKgWithConfiguredFactorAndPrecision() {
|
||||
|
||||
@@ -22,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
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.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -35,6 +36,17 @@ class FeeManagementServiceTest {
|
||||
@Resource
|
||||
private InboundFeeDetailService details;
|
||||
|
||||
@Test
|
||||
@DisplayName("客户B不能删除客户A的费用明细")
|
||||
void rejectsCrossCustomerFeeDetailDelete() {
|
||||
assertThatThrownBy(() -> details.delete(10002L, 21L, 31L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("费用明细不存在");
|
||||
|
||||
verify(repository).findDetail(10002L, 21L, 31L);
|
||||
verify(repository, never()).deleteDetail(10002L, 31L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("费用项目列表返回分页结果")
|
||||
void pagesItems() {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class FirstlegScopeGuardTest {
|
||||
@Test
|
||||
void platformRequiresZeroTenantAndPositiveTargetCustomer() {
|
||||
AuthenticatedUser platform = user(0L, "PLATFORM");
|
||||
|
||||
assertThat(FirstlegScopeGuard.platform(platform, 10001L)).isEqualTo(10001L);
|
||||
assertForbidden(() -> FirstlegScopeGuard.platform(user(10001L, "PLATFORM"), 10002L));
|
||||
assertForbidden(() -> FirstlegScopeGuard.platform(user(0L, "MERCHANT"), 10002L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantRequiresPositiveAuthenticatedCustomer() {
|
||||
assertThat(FirstlegScopeGuard.merchant(user(10001L, "MERCHANT"))).isEqualTo(10001L);
|
||||
assertForbidden(() -> FirstlegScopeGuard.merchant(user(0L, "MERCHANT")));
|
||||
assertForbidden(() -> FirstlegScopeGuard.merchant(user(10001L, "PLATFORM")));
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(long tenantId, String appCode) {
|
||||
return new AuthenticatedUser(9L, tenantId, tenantId, 1L,
|
||||
"operator", appCode, "WEB", Set.of());
|
||||
}
|
||||
|
||||
private static void assertForbidden(org.assertj.core.api.ThrowableAssert.ThrowingCallable call) {
|
||||
assertThatThrownBy(call)
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -115,7 +115,7 @@ class InboundCompletionOrchestratorTest {
|
||||
|
||||
private Fixture fixture(String reviewStatus, boolean feeRelease) {
|
||||
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(new InboundIdempotencyService.Context(1L, false));
|
||||
.thenReturn(new InboundIdempotencyService.Context(10001L, 1L, false));
|
||||
InboundOrderEntity order = new InboundOrderEntity();
|
||||
order.setProcessStatus("FEE_CONFIRMING");
|
||||
order.setFeeStatus("CONFIRMED");
|
||||
|
||||
+9
-4
@@ -37,7 +37,11 @@ class InboundIdempotencyServiceTest {
|
||||
InboundIdempotencyService.Context context = service.begin(
|
||||
10001L, 9L, 10L, "MEASURE_APPROVE", "key-1", "10|20|3|");
|
||||
|
||||
assertThat(context.replayed()).isTrue();
|
||||
assertThat(context)
|
||||
.extracting(InboundIdempotencyService.Context::tenantId,
|
||||
InboundIdempotencyService.Context::id,
|
||||
InboundIdempotencyService.Context::replayed)
|
||||
.containsExactly(10001L, 1L, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -59,11 +63,12 @@ class InboundIdempotencyServiceTest {
|
||||
@Test
|
||||
@DisplayName("新请求完成时只允许PROCESSING条件更新一次")
|
||||
void shouldCompleteProcessingRecordOnce() {
|
||||
when(repository.completeIdempotency(1L, "{\"inboundId\":\"10\"}")).thenReturn(1);
|
||||
when(repository.completeIdempotency(10001L, 1L, "{\"inboundId\":\"10\"}"))
|
||||
.thenReturn(1);
|
||||
|
||||
service.complete(new InboundIdempotencyService.Context(1L, false), 10L);
|
||||
service.complete(new InboundIdempotencyService.Context(10001L, 1L, false), 10L);
|
||||
|
||||
verify(repository).completeIdempotency(1L, "{\"inboundId\":\"10\"}");
|
||||
verify(repository).completeIdempotency(10001L, 1L, "{\"inboundId\":\"10\"}");
|
||||
}
|
||||
|
||||
private InboundIdempotencyEntity existing(String status) {
|
||||
|
||||
+17
@@ -69,6 +69,23 @@ class InboundMeasurementOpenApiServiceTest {
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private InboundMeasurementOpenApiService service;
|
||||
|
||||
@Test
|
||||
@DisplayName("第三方客户A不能访问只存在于客户B的入库单")
|
||||
void shouldRejectInboundOwnedByAnotherCustomer() {
|
||||
Fixture fixture = fixture();
|
||||
when(fixture.repository.findInbound(10002L, "RK202608010001")).thenReturn(inbound());
|
||||
|
||||
assertThatThrownBy(() -> fixture.service.receive(headers("nonce-cross"), BODY, image()))
|
||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class)
|
||||
.hasMessageContaining("入仓码不存在或不属于绑定仓库");
|
||||
|
||||
verify(fixture.repository).findInbound(10001L, "RK202608010001");
|
||||
verify(fixture.repository, never()).findInbound(10002L, "RK202608010001");
|
||||
verify(fixture.images, never()).upload(anyString(),
|
||||
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
|
||||
org.mockito.ArgumentMatchers.anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("回调只写批次明细和幂等结果,不存在入库主单update入口")
|
||||
void shouldPersistMeasurementsWithoutUpdatingInboundOrder() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -35,6 +36,17 @@ class InboundMeasurementServiceTest {
|
||||
@MockBean private MeasurementBatchService batchService;
|
||||
@Resource private InboundMeasurementService service;
|
||||
|
||||
@Test
|
||||
void customerCannotReadAnotherCustomersMeasurement() {
|
||||
assertThatThrownBy(() -> service.detail(2L, 10L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("入库单不存在");
|
||||
|
||||
verify(repository).findInbound(2L, 10L);
|
||||
verify(repository, never()).findInbound(1L, 10L);
|
||||
verify(repository, never()).listWeights(anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctingWeightKeepsOcrOriginalAndWritesImmutableRevision() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
|
||||
+20
-12
@@ -30,7 +30,7 @@ import static org.mockito.Mockito.when;
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
|
||||
class MeasurementLocationOccupationServiceTest {
|
||||
private static final long BUSINESS_TENANT = 10001L;
|
||||
private static final long RESOURCE_TENANT = 90001L;
|
||||
private static final long PLATFORM_RESOURCE_TENANT = 0L;
|
||||
private static final long WAREHOUSE = 200L;
|
||||
private static final long INBOUND = 88L;
|
||||
private static final long OPERATOR = 9L;
|
||||
@@ -43,8 +43,8 @@ class MeasurementLocationOccupationServiceTest {
|
||||
@Resource private MeasurementLocationOccupationService service;
|
||||
|
||||
@Test
|
||||
void sharedWarehouseResourcesUseResourceTenantWhileSnapshotsRemainInBusinessTenant() {
|
||||
properties.setWarehouseResourceTenantId(RESOURCE_TENANT);
|
||||
void platformWarehouseResourcesUseZeroWhileSnapshotsRemainInBusinessTenant() {
|
||||
properties.setWarehouseResourceTenantId(90001L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
@@ -56,27 +56,27 @@ class MeasurementLocationOccupationServiceTest {
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ReserveRequest> reserve =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReserveRequest.class);
|
||||
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
|
||||
assertThat(reserve.getValue().tenantId()).isEqualTo(RESOURCE_TENANT);
|
||||
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND);
|
||||
assertThat(reserve.getValue().idempotencyKey()).isEqualTo("measure-88-30-3");
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
|
||||
verify(warehouse).confirm(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
|
||||
assertThat(confirm.getValue().tenantId()).isEqualTo(RESOURCE_TENANT);
|
||||
assertThat(confirm.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
assertThat(confirm.getValue().businessId()).isEqualTo(INBOUND);
|
||||
|
||||
verify(allocations, times(2)).replace(
|
||||
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(INBOUND),
|
||||
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
|
||||
org.mockito.ArgumentMatchers.eq(RESOURCE_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")),
|
||||
org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroResourceTenantFallsBackToBusinessTenant() {
|
||||
void defaultResourceTenantRemainsPlatformZero() {
|
||||
properties.setWarehouseResourceTenantId(0L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
@@ -89,17 +89,25 @@ class MeasurementLocationOccupationServiceTest {
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ReserveRequest> reserve =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReserveRequest.class);
|
||||
verify(warehouse).reserve(org.mockito.ArgumentMatchers.eq("internal-token"), reserve.capture());
|
||||
assertThat(reserve.getValue().tenantId()).isEqualTo(BUSINESS_TENANT);
|
||||
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
|
||||
verify(warehouse).confirm(org.mockito.ArgumentMatchers.eq("internal-token"), confirm.capture());
|
||||
assertThat(confirm.getValue().tenantId()).isEqualTo(BUSINESS_TENANT);
|
||||
assertThat(confirm.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
|
||||
verify(allocations, times(2)).replace(
|
||||
org.mockito.ArgumentMatchers.eq(BUSINESS_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(INBOUND),
|
||||
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
|
||||
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")),
|
||||
org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmFailureReleasesReservationInResourceTenantAndKeepsOriginalFailure() {
|
||||
properties.setWarehouseResourceTenantId(RESOURCE_TENANT);
|
||||
properties.setWarehouseResourceTenantId(90001L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
@@ -115,9 +123,9 @@ class MeasurementLocationOccupationServiceTest {
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ReleaseRequest> release =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ReleaseRequest.class);
|
||||
verify(warehouse).release(org.mockito.ArgumentMatchers.eq("internal-token"), release.capture());
|
||||
assertThat(release.getValue().tenantId()).isEqualTo(RESOURCE_TENANT);
|
||||
assertThat(release.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
assertThat(release.getValue().businessId()).isEqualTo(INBOUND);
|
||||
verify(allocations).replace(BUSINESS_TENANT, INBOUND, WAREHOUSE, RESOURCE_TENANT,
|
||||
verify(allocations).replace(BUSINESS_TENANT, INBOUND, WAREHOUSE, PLATFORM_RESOURCE_TENANT,
|
||||
new BigDecimal("1.200000"), batch("RESERVED"));
|
||||
}
|
||||
|
||||
|
||||
+19
-2
@@ -40,6 +40,23 @@ class MeasurementReviewServiceTest {
|
||||
@MockBean private InboundIdempotencyService idempotency;
|
||||
@MockBean private InboundProperties properties;
|
||||
@Resource private MeasurementReviewService service;
|
||||
|
||||
@Test
|
||||
@DisplayName("客户B不能审核客户A的测量")
|
||||
void shouldRejectCrossCustomerReview() {
|
||||
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(new InboundIdempotencyService.Context(10002L, 1L, false));
|
||||
|
||||
assertThatThrownBy(() -> service.approve(10002L, 9L, "审核员", 10L, "cross-customer",
|
||||
new MeasurementReviewRequest(20L, 3, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.NOT_FOUND);
|
||||
|
||||
verify(repository).findOwned(10002L, 10L);
|
||||
verify(repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核通过是测量到计费的唯一正常推进关口")
|
||||
void shouldApproveOnlyPendingSubmittedMeasurement() {
|
||||
@@ -73,7 +90,7 @@ class MeasurementReviewServiceTest {
|
||||
void shouldReplaySameReviewRequest() {
|
||||
Fixture fixture = fixture(false);
|
||||
when(fixture.idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(new InboundIdempotencyService.Context(1L, true));
|
||||
.thenReturn(new InboundIdempotencyService.Context(10001L, 1L, true));
|
||||
|
||||
fixture.service.approve(10001L, 9L, "审核员", 10L, "approve-1",
|
||||
new MeasurementReviewRequest(20L, 3, null));
|
||||
@@ -147,7 +164,7 @@ class MeasurementReviewServiceTest {
|
||||
|
||||
private Fixture fixture(boolean allowRelease) {
|
||||
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(new InboundIdempotencyService.Context(1L, false));
|
||||
.thenReturn(new InboundIdempotencyService.Context(10001L, 1L, false));
|
||||
when(properties.isAllowRejectedMeasurement()).thenReturn(allowRelease);
|
||||
InboundOrderEntity order = new InboundOrderEntity();
|
||||
order.setProcessStatus("MEASURE_PENDING");
|
||||
|
||||
Reference in New Issue
Block a user