格式修改

This commit is contained in:
2026-08-05 10:35:07 +08:00
parent 81c9c92f5d
commit 496fc58a90
241 changed files with 5600 additions and 2088 deletions
@@ -0,0 +1,15 @@
package com.baiyue.firstleg.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Clock;
@Configuration
public class TimeConfiguration {
@Bean
public Clock systemClock() {
return Clock.systemDefaultZone();
}
}
@@ -1,5 +1,8 @@
package com.baiyue.firstleg.controller;
import jakarta.annotation.Resource;
import jakarta.annotation.PostConstruct;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
@@ -23,13 +26,15 @@ import java.security.MessageDigest;
@RestController
@RequestMapping("/internal/firstleg/v1/inbounds")
public class InternalInboundOrderController {
private final InboundOrderCreationService service;
private final byte[] expectedToken;
@Resource
private InboundOrderCreationService service;
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
private byte[] expectedToken;
public InternalInboundOrderController(InboundOrderCreationService service,
@Value("${baiyue.security.internal-service-token}") String token) {
this.service = service;
this.expectedToken = token.getBytes(StandardCharsets.UTF_8);
@PostConstruct
void initializeToken() {
expectedToken = internalToken.getBytes(StandardCharsets.UTF_8);
}
@PostMapping("/order-approved")
@@ -1,5 +1,7 @@
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;
@@ -22,11 +24,8 @@ import org.springframework.http.ResponseEntity;
@RestController
@RequestMapping("/api/merchant/v1/first-leg/orders")
public class MerchantInboundBillingController {
private final MerchantInboundBillingService service;
public MerchantInboundBillingController(MerchantInboundBillingService service) {
this.service = service;
}
@Resource
private MerchantInboundBillingService service;
@GetMapping("/{orderId}/inbound-billing")
@PreAuthorize("hasAuthority('merchant:order:view')")
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.controller;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.dto.OpenApiMeasurementDtos.EventResponse;
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
@@ -32,11 +34,8 @@ import org.springframework.web.multipart.MultipartFile;
@SecurityRequirement(name = "measurementNonce")
@SecurityRequirement(name = "measurementSignature")
public class OpenApiInboundMeasurementController {
private final InboundMeasurementOpenApiService service;
public OpenApiInboundMeasurementController(InboundMeasurementOpenApiService service) {
this.service = service;
}
@Resource
private InboundMeasurementOpenApiService service;
@Operation(summary = "提交一条第三方箱规测量结果和图片",
description = "multipart 包含 data 原始JSON和 image 图片;签名 body 为 data原文、换行、图片SHA-256,外层仍按 METHOD、PATH、时间戳、Nonce、Idempotency-Key、body SHA-256 六行签名")
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.controller;
import jakarta.annotation.Resource;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
@@ -32,9 +34,8 @@ import java.util.List;
@RestController
@RequestMapping("/api/platform/v1/fba-pricing")
public class PlatformFbaMasterDataController {
private final FbaMasterDataService service;
public PlatformFbaMasterDataController(FbaMasterDataService service) { this.service = service; }
@Resource
private FbaMasterDataService service;
@GetMapping("/customer-rules")
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rule:view')")
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.controller;
import jakarta.annotation.Resource;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
@@ -28,9 +30,8 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/platform/v1/inbounds/{inboundId}/fees")
public class PlatformFbaPricingController {
private final FbaPricingService service;
public PlatformFbaPricingController(FbaPricingService service) { this.service = service; }
@Resource
private FbaPricingService service;
@PostMapping("/calculate")
@PreAuthorize("hasAuthority('firstleg:fba-pricing:calculate')")
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.controller;
import jakarta.annotation.Resource;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
@@ -28,14 +30,8 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/platform/v1/first-leg/inbounds/{inboundId}")
public class PlatformInboundMeasurementController {
private final InboundMeasurementService service;
private final MeasurementSubmissionService submission;
public PlatformInboundMeasurementController(InboundMeasurementService service,
MeasurementSubmissionService submission) {
this.service = service;
this.submission = submission;
}
@Resource private InboundMeasurementService service;
@Resource private MeasurementSubmissionService submission;
@Operation(summary = "查询入库测量详情", description = "返回原单、实测箱规、审核快照和 OCR 重量结果。")
@GetMapping("/measurement")
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.controller;
import jakarta.annotation.Resource;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
@@ -34,14 +36,8 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/platform/v1/inbounds")
public class PlatformInboundMeasurementReviewController {
private final MeasurementReviewService service;
private final AutomaticInboundCompletionService automaticCompletion;
public PlatformInboundMeasurementReviewController(MeasurementReviewService service,
AutomaticInboundCompletionService automaticCompletion) {
this.service = service;
this.automaticCompletion = automaticCompletion;
}
@Resource private MeasurementReviewService service;
@Resource private AutomaticInboundCompletionService automaticCompletion;
@BusinessAudit(event = "INBOUND_MEASURE_APPROVE", resourceType = "INBOUND",
resourceId = "#inboundId", description = "测量审核通过")
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.controller;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
@@ -29,11 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/platform/inbounds")
public class PlatformInboundOrderController {
private final InboundOrderQueryService service;
public PlatformInboundOrderController(InboundOrderQueryService service) {
this.service = service;
}
@Resource private InboundOrderQueryService service;
@Operation(summary = "分页查询入库单",
description = "默认分页查询全部企业入库单;可按目标租户和二期状态页签筛选入库单快照。")
@@ -1,5 +1,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.CarrierFbaRateEntity;
@@ -24,27 +26,20 @@ import java.util.List;
/** FBA 计费仓储;平台基础资料列表可跨企业读取,其他业务操作强制带租户条件。 */
@Repository
public class FbaPricingRepository {
private final CustomerChargeRuleMapper rules;
private final CarrierFbaRateMapper rates;
private final InboundPricingMapper inbounds;
private final InboundFeeMapper fees;
private final InboundFeeItemMapper items;
private final InboundPricingIdempotencyMapper idempotencies;
private final InboundOutboxMapper outbox;
public FbaPricingRepository(CustomerChargeRuleMapper rules, CarrierFbaRateMapper rates,
InboundPricingMapper inbounds, InboundFeeMapper fees,
InboundFeeItemMapper items,
InboundPricingIdempotencyMapper idempotencies,
InboundOutboxMapper outbox) {
this.rules = rules;
this.rates = rates;
this.inbounds = inbounds;
this.fees = fees;
this.items = items;
this.idempotencies = idempotencies;
this.outbox = outbox;
}
@Resource
private CustomerChargeRuleMapper rules;
@Resource
private CarrierFbaRateMapper rates;
@Resource
private InboundPricingMapper inbounds;
@Resource
private InboundFeeMapper fees;
@Resource
private InboundFeeItemMapper items;
@Resource
private InboundPricingIdempotencyMapper idempotencies;
@Resource
private InboundOutboxMapper outbox;
public void insertRule(CustomerChargeRuleEntity rule) { rules.insert(rule); }
public void insertRate(CarrierFbaRateEntity rate) { rates.insert(rate); }
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.repository;
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;
@@ -16,13 +18,8 @@ import org.springframework.util.StringUtils;
*/
@Repository
public class FulfillmentTaskRepository {
private final FulfillmentTaskMapper tasks;
private final AssignmentHistoryMapper histories;
public FulfillmentTaskRepository(FulfillmentTaskMapper tasks, AssignmentHistoryMapper histories) {
this.tasks = tasks;
this.histories = histories;
}
@Resource private FulfillmentTaskMapper tasks;
@Resource private AssignmentHistoryMapper histories;
public FulfillmentTaskEntity findOwned(long tenantId, long taskId) {
return tasks.selectOne(new LambdaQueryWrapper<FulfillmentTaskEntity>()
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.repository;
import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
@@ -12,17 +14,12 @@ import org.springframework.stereotype.Repository;
/** 第三方测量开放接口仓储,仅允许读取主单并写测量、明细和幂等记录。 */
@Repository
public class InboundMeasurementOpenApiRepository {
private final InboundOrderLookupMapper inboundOrders;
private final InboundMeasurementMapper measurements;
private final InboundMeasurementItemMapper items;
public InboundMeasurementOpenApiRepository(InboundOrderLookupMapper inboundOrders,
InboundMeasurementMapper measurements,
InboundMeasurementItemMapper items) {
this.inboundOrders = inboundOrders;
this.measurements = measurements;
this.items = items;
}
@Resource
private InboundOrderLookupMapper inboundOrders;
@Resource
private InboundMeasurementMapper measurements;
@Resource
private InboundMeasurementItemMapper items;
public InboundOrderLookupEntity findInbound(long tenantId, String inboundCode) {
return inboundOrders.selectOne(new LambdaQueryWrapper<InboundOrderLookupEntity>()
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.repository;
import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
@@ -20,26 +22,18 @@ import java.util.List;
/** 入库测量仓储,所有读取都同时带租户和入库单边界。 */
@Repository
public class InboundMeasurementRepository {
private final InboundOrderMapper inboundOrders;
private final InboundMeasurementMapper measurements;
private final InboundMeasurementItemMapper items;
private final InboundWeightResultMapper weights;
private final InboundWeightRevisionMapper revisions;
private final MeasureReviewRecordMapper reviews;
public InboundMeasurementRepository(InboundOrderMapper inboundOrders,
InboundMeasurementMapper measurements,
InboundMeasurementItemMapper items,
InboundWeightResultMapper weights,
InboundWeightRevisionMapper revisions,
MeasureReviewRecordMapper reviews) {
this.inboundOrders = inboundOrders;
this.measurements = measurements;
this.items = items;
this.weights = weights;
this.revisions = revisions;
this.reviews = reviews;
}
@Resource
private InboundOrderMapper inboundOrders;
@Resource
private InboundMeasurementMapper measurements;
@Resource
private InboundMeasurementItemMapper items;
@Resource
private InboundWeightResultMapper weights;
@Resource
private InboundWeightRevisionMapper revisions;
@Resource
private MeasureReviewRecordMapper reviews;
public InboundOrderEntity findInbound(long tenantId, long inboundId) {
return inboundOrders.selectOne(new LambdaQueryWrapper<InboundOrderEntity>()
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.repository;
import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
@@ -29,27 +31,18 @@ import java.util.stream.Collectors;
/** 入库单只读仓储;平台分页可跨企业查询,详情继续强制租户隔离。 */
@Repository
public class InboundOrderRepository {
private final InboundOrderMapper orders;
private final InboundTransportSnapshotMapper transports;
private final InboundPalletAllocationMapper pallets;
private final InboundFileMapper files;
private final InboundInternalNoteMapper notes;
private final InboundStatusHistoryMapper histories;
public InboundOrderRepository(
InboundOrderMapper orders,
InboundTransportSnapshotMapper transports,
InboundPalletAllocationMapper pallets,
InboundFileMapper files,
InboundInternalNoteMapper notes,
InboundStatusHistoryMapper histories) {
this.orders = orders;
this.transports = transports;
this.pallets = pallets;
this.files = files;
this.notes = notes;
this.histories = histories;
}
@Resource
private InboundOrderMapper orders;
@Resource
private InboundTransportSnapshotMapper transports;
@Resource
private InboundPalletAllocationMapper pallets;
@Resource
private InboundFileMapper files;
@Resource
private InboundInternalNoteMapper notes;
@Resource
private InboundStatusHistoryMapper histories;
public PageResult<InboundOrderSummary> page(Long tenantId, InboundOrderQuery query) {
LambdaQueryWrapper<InboundOrderEntity> wrapper = new LambdaQueryWrapper<InboundOrderEntity>()
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.repository;
import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baiyue.firstleg.client.WarehousePalletLocationClient.BatchResult;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
@@ -15,11 +17,7 @@ import java.util.stream.Collectors;
@Repository
public class InboundPalletAllocationRepository {
private final InboundPalletAllocationMapper mapper;
public InboundPalletAllocationRepository(InboundPalletAllocationMapper mapper) {
this.mapper = mapper;
}
@Resource private InboundPalletAllocationMapper mapper;
@Transactional
public void replace(long tenantId, long inboundId, long warehouseId, long warehouseResourceTenantId,
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.repository;
import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
@@ -23,32 +25,22 @@ import org.springframework.stereotype.Repository;
/** 入库审核和完成编排仓储;所有业务查询强制携带租户。 */
@Repository
public class InboundWorkflowRepository {
private final InboundOrderMapper orders;
private final InboundMeasurementMapper measurements;
private final MeasureReviewRecordMapper reviews;
private final InboundStatusHistoryMapper histories;
private final InboundIdempotencyMapper idempotencies;
private final InboundFeeMapper fees;
private final InboundOutboxMapper outboxes;
private final InboundAllocationQueryMapper allocations;
public InboundWorkflowRepository(InboundOrderMapper orders,
InboundMeasurementMapper measurements,
MeasureReviewRecordMapper reviews,
InboundStatusHistoryMapper histories,
InboundIdempotencyMapper idempotencies,
InboundFeeMapper fees,
InboundOutboxMapper outboxes,
InboundAllocationQueryMapper allocations) {
this.orders = orders;
this.measurements = measurements;
this.reviews = reviews;
this.histories = histories;
this.idempotencies = idempotencies;
this.fees = fees;
this.outboxes = outboxes;
this.allocations = allocations;
}
@Resource
private InboundOrderMapper orders;
@Resource
private InboundMeasurementMapper measurements;
@Resource
private MeasureReviewRecordMapper reviews;
@Resource
private InboundStatusHistoryMapper histories;
@Resource
private InboundIdempotencyMapper idempotencies;
@Resource
private InboundFeeMapper fees;
@Resource
private InboundOutboxMapper outboxes;
@Resource
private InboundAllocationQueryMapper allocations;
public InboundOrderEntity findOwned(long tenantId, long inboundId) {
return orders.selectOne(new LambdaQueryWrapper<InboundOrderEntity>()
@@ -1,9 +1,10 @@
package com.baiyue.firstleg.security;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.config.MeasurementOpenApiProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@@ -21,23 +22,12 @@ import java.util.Map;
@Component
public class MeasurementRequestAuthenticator {
private static final String HMAC_ALGORITHM = "HmacSHA256";
private final MeasurementOpenApiProperties properties;
private final NonceReplayGuard nonceReplayGuard;
private final Clock clock;
@Autowired
public MeasurementRequestAuthenticator(MeasurementOpenApiProperties properties,
NonceReplayGuard nonceReplayGuard) {
this(properties, nonceReplayGuard, Clock.systemUTC());
}
MeasurementRequestAuthenticator(MeasurementOpenApiProperties properties,
NonceReplayGuard nonceReplayGuard,
Clock clock) {
this.properties = properties;
this.nonceReplayGuard = nonceReplayGuard;
this.clock = clock;
}
@Resource
private MeasurementOpenApiProperties properties;
@Resource
private NonceReplayGuard nonceReplayGuard;
@Resource
private Clock clock;
public AuthenticatedMeasurementApp authenticate(String method, String path,
MeasurementRequestHeaders headers,
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.security;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@@ -9,11 +11,8 @@ import java.time.Duration;
@Component
public class RedisNonceReplayGuard implements NonceReplayGuard {
private static final String KEY_PREFIX = "baiyue:firstleg:measurement-openapi:nonce:";
private final StringRedisTemplate redisTemplate;
public RedisNonceReplayGuard(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Resource
private StringRedisTemplate redisTemplate;
@Override
public boolean claim(String appKey, String nonce, Duration ttl) {
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
@@ -9,17 +11,12 @@ import org.springframework.stereotype.Service;
@Service
public class AutomaticInboundCompletionService {
private final FbaPricingService pricing;
private final InboundWorkflowRepository workflow;
private final InboundCompletionOrchestrator completion;
public AutomaticInboundCompletionService(FbaPricingService pricing,
InboundWorkflowRepository workflow,
InboundCompletionOrchestrator completion) {
this.pricing = pricing;
this.workflow = workflow;
this.completion = completion;
}
@Resource
private FbaPricingService pricing;
@Resource
private InboundWorkflowRepository workflow;
@Resource
private InboundCompletionOrchestrator completion;
public FeeSnapshotResponse completeApproved(long tenantId, long inboundId, long operatorId,
String operatorName, long measurementId,
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FbaPricingDtos.CarrierRateVersionRequest;
@@ -17,9 +19,8 @@ import java.util.Objects;
/** 客户规则和船司价格版本管理。版本内容创建后不可原地修改。 */
@Service
public class FbaMasterDataService {
private final FbaPricingRepository repository;
public FbaMasterDataService(FbaPricingRepository repository) { this.repository = repository; }
@Resource
private FbaPricingRepository repository;
@Transactional
public CustomerChargeRuleEntity createRuleVersion(long tenantId, long operatorId,
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
@@ -12,7 +14,6 @@ import com.baiyue.firstleg.entity.InboundOutboxEntity;
import com.baiyue.firstleg.entity.InboundPricingEntity;
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@@ -32,18 +33,10 @@ import java.util.List;
@Service
public class FbaPricingService {
private static final int MONEY_SCALE = 2;
private final FbaPricingRepository repository;
private final Clock clock;
@Autowired
public FbaPricingService(FbaPricingRepository repository) {
this(repository, Clock.systemDefaultZone());
}
FbaPricingService(FbaPricingRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}
@Resource
private FbaPricingRepository repository;
@Resource
private Clock clock;
@Transactional
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.entity.InboundFeeEntity;
@@ -21,14 +23,10 @@ import java.time.LocalDateTime;
*/
@Service
public class InboundCompletionOrchestrator {
private final InboundWorkflowRepository repository;
private final InboundIdempotencyService idempotency;
public InboundCompletionOrchestrator(InboundWorkflowRepository repository,
InboundIdempotencyService idempotency) {
this.repository = repository;
this.idempotency = idempotency;
}
@Resource
private InboundWorkflowRepository repository;
@Resource
private InboundIdempotencyService idempotency;
/**
* 任务5对接方法一:锁定发送动作。允许前置状态仅为 FEE_CONFIRMING + CONFIRMED。
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.entity.InboundIdempotencyEntity;
@@ -17,11 +19,8 @@ import java.util.HexFormat;
/** 入库写操作的统一持久化幂等边界。 */
@Service
public class InboundIdempotencyService {
private final InboundWorkflowRepository repository;
public InboundIdempotencyService(InboundWorkflowRepository repository) {
this.repository = repository;
}
@Resource
private InboundWorkflowRepository repository;
public Context begin(long tenantId, long operatorId, long inboundId,
String operation, String idempotencyKey, String canonicalRequest) {
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
@@ -50,36 +52,24 @@ public class InboundMeasurementOpenApiService {
private static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
private static final String CALCULATION_RULE_VERSION = "V1_CBM_CEILING_4_GIRTH_L2WH";
private final InboundMeasurementOpenApiRepository repository;
private final MeasurementRequestAuthenticator authenticator;
private final ThirdPartyCallLogService callLogService;
private final OrderCargoClient orderCargoClient;
private final MeasurementImageClient imageClient;
private final String internalToken;
private final ObjectMapper objectMapper;
private final Validator validator;
private final TransactionTemplate transactionTemplate;
public InboundMeasurementOpenApiService(
InboundMeasurementOpenApiRepository repository,
MeasurementRequestAuthenticator authenticator,
ThirdPartyCallLogService callLogService,
OrderCargoClient orderCargoClient,
MeasurementImageClient imageClient,
@Value("${baiyue.security.internal-service-token}") String internalToken,
ObjectMapper objectMapper,
Validator validator,
TransactionTemplate transactionTemplate) {
this.repository = repository;
this.authenticator = authenticator;
this.callLogService = callLogService;
this.orderCargoClient = orderCargoClient;
this.imageClient = imageClient;
this.internalToken = internalToken;
this.objectMapper = objectMapper;
this.validator = validator;
this.transactionTemplate = transactionTemplate;
}
@Resource
private InboundMeasurementOpenApiRepository repository;
@Resource
private MeasurementRequestAuthenticator authenticator;
@Resource
private ThirdPartyCallLogService callLogService;
@Resource
private OrderCargoClient orderCargoClient;
@Resource
private MeasurementImageClient imageClient;
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
@Resource
private ObjectMapper objectMapper;
@Resource
private Validator validator;
@Resource
private TransactionTemplate transactionTemplate;
public EventResponse receive(MeasurementRequestHeaders headers, String rawData, MultipartFile image) {
requireImage(image);
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
@@ -36,14 +38,10 @@ public class InboundMeasurementService {
Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
private static final BigDecimal LB_TO_KG = new BigDecimal("0.45359237");
private final InboundMeasurementRepository repository;
private final MeasurementBatchService batchService;
public InboundMeasurementService(InboundMeasurementRepository repository,
MeasurementBatchService batchService) {
this.repository = repository;
this.batchService = batchService;
}
@Resource
private InboundMeasurementRepository repository;
@Resource
private MeasurementBatchService batchService;
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) {
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
import com.baiyue.firstleg.dto.InboundOrderReference;
@@ -16,13 +18,10 @@ import java.util.List;
/** 将已审核的订单快照幂等转换为待测量入库单。 */
@Service
public class InboundOrderCreationService {
private final InboundOrderMapper orders;
private final InboundStatusHistoryMapper histories;
public InboundOrderCreationService(InboundOrderMapper orders, InboundStatusHistoryMapper histories) {
this.orders = orders;
this.histories = histories;
}
@Resource
private InboundOrderMapper orders;
@Resource
private InboundStatusHistoryMapper histories;
@Transactional
public long createFromApprovedOrder(OrderApprovedInboundCommand command) {
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
@@ -15,11 +17,8 @@ import org.springframework.util.StringUtils;
/** 入库单列表和详情只读应用服务,不执行任何状态推进。 */
@Service
public class InboundOrderQueryService {
private final InboundOrderRepository repository;
public InboundOrderQueryService(InboundOrderRepository repository) {
this.repository = repository;
}
@Resource
private InboundOrderRepository repository;
public PageResult<InboundOrderSummary> page(Long tenantId, InboundOrderQuery query) {
validateQuery(query);
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.MeasurementBatchCommand;
@@ -31,11 +33,8 @@ public class MeasurementBatchService {
static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
static final String CALCULATION_RULE_VERSION = "V1_CBM_CEILING_4_GIRTH_L2WH";
private final InboundMeasurementRepository repository;
public MeasurementBatchService(InboundMeasurementRepository repository) {
this.repository = repository;
}
@Resource
private InboundMeasurementRepository repository;
@Transactional
public InboundMeasurementEntity saveBatch(MeasurementBatchCommand command) {
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
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;
@@ -18,22 +20,16 @@ import java.time.LocalDateTime;
@Service
public class MeasurementLocationOccupationService {
private static final String SOURCE_SERVICE = "firstleg-service";
private final WarehousePalletLocationClient warehouse;
private final InboundPalletAllocationRepository allocations;
private final InboundWorkflowRepository workflow;
private final InboundProperties properties;
private final String internalToken;
public MeasurementLocationOccupationService(WarehousePalletLocationClient warehouse,
InboundPalletAllocationRepository allocations, InboundWorkflowRepository workflow,
InboundProperties properties,
@Value("${baiyue.security.internal-service-token}") String internalToken) {
this.warehouse = warehouse;
this.allocations = allocations;
this.workflow = workflow;
this.properties = properties;
this.internalToken = internalToken;
}
@Resource
private WarehousePalletLocationClient warehouse;
@Resource
private InboundPalletAllocationRepository allocations;
@Resource
private InboundWorkflowRepository workflow;
@Resource
private InboundProperties properties;
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
public void occupy(long tenantId, long inboundId, long operatorId, long measurementId,
int measurementVersion) {
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
@@ -22,17 +24,12 @@ import java.time.LocalDateTime;
*/
@Service
public class MeasurementReviewService {
private final InboundWorkflowRepository repository;
private final InboundIdempotencyService idempotency;
private final InboundProperties properties;
public MeasurementReviewService(InboundWorkflowRepository repository,
InboundIdempotencyService idempotency,
InboundProperties properties) {
this.repository = repository;
this.idempotency = idempotency;
this.properties = properties;
}
@Resource
private InboundWorkflowRepository repository;
@Resource
private InboundIdempotencyService idempotency;
@Resource
private InboundProperties properties;
@Transactional
public void approve(long tenantId, long operatorId, String operatorName, long inboundId,
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import org.springframework.stereotype.Service;
@@ -7,14 +9,10 @@ import org.springframework.transaction.annotation.Transactional;
@Service
public class MeasurementSubmissionService {
private final InboundMeasurementService measurements;
private final MeasurementLocationOccupationService locations;
public MeasurementSubmissionService(InboundMeasurementService measurements,
MeasurementLocationOccupationService locations) {
this.measurements = measurements;
this.locations = locations;
}
@Resource
private InboundMeasurementService measurements;
@Resource
private MeasurementLocationOccupationService locations;
@Transactional
public InboundMeasurementResponses.MeasurementSubmitted submit(
@@ -1,5 +1,8 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.MerchantInboundBillingResponse;
@@ -15,23 +18,16 @@ import java.util.List;
@Service
public class MerchantInboundBillingService {
private final InboundOrderRepository orders;
private final FbaPricingService pricing;
private final InboundMeasurementService measurements;
private final MeasurementImageClient imageClient;
private final String internalToken;
public MerchantInboundBillingService(InboundOrderRepository orders, FbaPricingService pricing,
InboundMeasurementService measurements,
MeasurementImageClient imageClient,
@org.springframework.beans.factory.annotation.Value(
"${baiyue.security.internal-service-token}") String internalToken) {
this.orders = orders;
this.pricing = pricing;
this.measurements = measurements;
this.imageClient = imageClient;
this.internalToken = internalToken;
}
@Resource
private InboundOrderRepository orders;
@Resource
private FbaPricingService pricing;
@Resource
private InboundMeasurementService measurements;
@Resource
private MeasurementImageClient imageClient;
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
public MerchantInboundBillingResponse find(long tenantId, long orderId) {
InboundOrderEntity inbound = orders.findByOrderId(tenantId, orderId);
@@ -1,5 +1,7 @@
package com.baiyue.firstleg.service;
import jakarta.annotation.Resource;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baiyue.firstleg.config.MeasurementOpenApiProperties;
@@ -17,14 +19,10 @@ import java.time.LocalDateTime;
/** 将每次第三方 API 调用独立留档,业务事务回滚也不会丢失失败记录。 */
@Service
public class ThirdPartyCallLogService {
private final ThirdPartyCallLogMapper mapper;
private final MeasurementOpenApiProperties properties;
public ThirdPartyCallLogService(ThirdPartyCallLogMapper mapper,
MeasurementOpenApiProperties properties) {
this.mapper = mapper;
this.properties = properties;
}
@Resource
private ThirdPartyCallLogMapper mapper;
@Resource
private MeasurementOpenApiProperties properties;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public long start(String method, String path, MeasurementRequestHeaders headers, String rawBody) {
+1 -1
View File
@@ -48,6 +48,6 @@ logging:
rollingpolicy:
file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}"
max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}"
max-history: "${LOGGING_ROLLING_MAX_HISTORY:30}"
max-history: "${LOGGING_ROLLING_MAX_HISTORY:7}"
total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}"
clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}"
@@ -3,7 +3,11 @@ package com.baiyue.firstleg.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.InboundOrderReference;
import com.baiyue.firstleg.service.InboundOrderCreationService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.List;
@@ -13,13 +17,16 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(InternalInboundOrderController.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class InternalInboundOrderReferenceControllerTest {
private final InboundOrderCreationService service = mock(InboundOrderCreationService.class);
private final InternalInboundOrderController controller =
new InternalInboundOrderController(service, "internal-token");
@MockBean
private InboundOrderCreationService service;
@Resource
private InternalInboundOrderController controller;
@Test
void 使用内部令牌按租户批量查询订单入仓编码() {
void shouldHandleCase01() {
var request = new InboundOrderReference.BatchRequest(10001L, List.of(77L, 77L, 78L));
when(service.findReferences(10001L, List.of(77L, 78L))).thenReturn(List.of(
new InboundOrderReference.Item(77L, 88L, "RK-77", "WAIT_MEASURE")));
@@ -32,7 +39,7 @@ class InternalInboundOrderReferenceControllerTest {
}
@Test
void 错误内部令牌不能查询任意租户的入仓编码() {
void shouldHandleCase02() {
var request = new InboundOrderReference.BatchRequest(10001L, List.of(77L));
assertThatThrownBy(() -> controller.references("bad-token", request))
.isInstanceOf(BizException.class);
@@ -3,8 +3,11 @@ package com.baiyue.firstleg.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.service.MerchantInboundBillingService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.Set;
@@ -12,12 +15,15 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
@SpringJUnitConfig(MerchantInboundBillingController.class)
class MerchantInboundBillingControllerContractTest {
private final MerchantInboundBillingController controller =
new MerchantInboundBillingController(mock(MerchantInboundBillingService.class));
@MockBean
private MerchantInboundBillingService service;
@Resource
private MerchantInboundBillingController controller;
@Test
void 客户费用查询复用订单查看权限且不提供确认接口() throws Exception {
void shouldHandleCase01() throws Exception {
var method = MerchantInboundBillingController.class.getDeclaredMethod(
"find", AuthenticatedUser.class, long.class);
assertThat(method.getAnnotation(PreAuthorize.class).value())
@@ -27,13 +33,13 @@ class MerchantInboundBillingControllerContractTest {
}
@Test
void 平台身份不能越权调用客户费用接口() {
void shouldHandleCase02() {
var platform = new AuthenticatedUser(1L, 0L, 0L, 1L,
"admin", "PLATFORM", "WEB", Set.of());
assertThatThrownBy(() -> controller.find(platform, 77L)).isInstanceOf(BizException.class);
}
@Test
void 商户测量图片复用订单查看权限() throws Exception {
void shouldHandleCase03() throws Exception {
var method = MerchantInboundBillingController.class.getDeclaredMethod(
"image", AuthenticatedUser.class, long.class, long.class);
assertThat(method.getAnnotation(PreAuthorize.class).value())
@@ -16,6 +16,9 @@ import org.apache.ibatis.session.Configuration;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import jakarta.annotation.Resource;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.Collection;
import java.util.List;
@@ -25,7 +28,16 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(FbaPricingRepository.class)
class FbaPricingRepositoryRuleListTest {
@MockBean private CustomerChargeRuleMapper rules;
@MockBean private CarrierFbaRateMapper rates;
@MockBean private InboundPricingMapper pricing;
@MockBean private InboundFeeMapper fees;
@MockBean private InboundFeeItemMapper feeItems;
@MockBean private InboundPricingIdempotencyMapper idempotency;
@MockBean private InboundOutboxMapper outbox;
@Resource private FbaPricingRepository repository;
@BeforeAll
static void initializeEntityMetadata() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"),
@@ -57,11 +69,9 @@ class FbaPricingRepositoryRuleListTest {
@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");
@@ -75,10 +85,8 @@ class FbaPricingRepositoryRuleListTest {
assertThat(result).isEqualTo(4);
}
private static Collection<Object> queryValues(Long tenantId, String ruleCode) {
CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class);
private Collection<Object> queryValues(Long tenantId, String ruleCode) {
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
FbaPricingRepository repository = repository(rules);
repository.listRules(tenantId, ruleCode);
@@ -90,10 +98,8 @@ class FbaPricingRepositoryRuleListTest {
return wrapper.getValue().getParamNameValuePairs().values();
}
private static Collection<Object> rateQueryValues(Long tenantId, String rateCode) {
CarrierFbaRateMapper rates = mock(CarrierFbaRateMapper.class);
private Collection<Object> rateQueryValues(Long tenantId, String rateCode) {
when(rates.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
FbaPricingRepository repository = repository(mock(CustomerChargeRuleMapper.class), rates);
repository.listRates(tenantId, rateCode);
@@ -105,14 +111,4 @@ class FbaPricingRepositoryRuleListTest {
return wrapper.getValue().getParamNameValuePairs().values();
}
private static FbaPricingRepository repository(CustomerChargeRuleMapper rules) {
return repository(rules, mock(CarrierFbaRateMapper.class));
}
private static FbaPricingRepository repository(
CustomerChargeRuleMapper rules, CarrierFbaRateMapper rates) {
return new FbaPricingRepository(rules, rates,
mock(InboundPricingMapper.class), mock(InboundFeeMapper.class), mock(InboundFeeItemMapper.class),
mock(InboundPricingIdempotencyMapper.class), mock(InboundOutboxMapper.class));
}
}
@@ -18,6 +18,9 @@ import com.baiyue.firstleg.mapper.InboundTransportSnapshotMapper;
import org.junit.jupiter.api.Test;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.mockito.ArgumentCaptor;
import jakarta.annotation.Resource;
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;
@@ -26,22 +29,22 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(InboundOrderRepository.class)
class InboundOrderRepositoryTest {
@MockBean private InboundOrderMapper orders;
@MockBean private InboundTransportSnapshotMapper transports;
@MockBean private InboundPalletAllocationMapper pallets;
@MockBean private InboundFileMapper files;
@MockBean private InboundInternalNoteMapper notes;
@MockBean private InboundStatusHistoryMapper histories;
@Resource private InboundOrderRepository repository;
@Test
@SuppressWarnings({"rawtypes", "unchecked"})
void shouldApplyTenantCompanyAndWarehouseFilters() {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), InboundOrderEntity.class);
InboundOrderMapper orders = mock(InboundOrderMapper.class);
when(orders.selectPage(any(Page.class), any(Wrapper.class)))
.thenReturn(new Page<InboundOrderEntity>(1, 20));
InboundOrderRepository repository = new InboundOrderRepository(
orders,
mock(InboundTransportSnapshotMapper.class),
mock(InboundPalletAllocationMapper.class),
mock(InboundFileMapper.class),
mock(InboundInternalNoteMapper.class),
mock(InboundStatusHistoryMapper.class));
InboundOrderQuery query = new InboundOrderQuery(
null, null, null, null, null, null, null, null,
null, null, null, null, 20001L, 30001L, null, null, null, 1, 20);
@@ -69,8 +72,6 @@ class InboundOrderRepositoryTest {
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
InboundPalletAllocationEntity.class);
InboundOrderMapper orders = mock(InboundOrderMapper.class);
InboundPalletAllocationMapper pallets = mock(InboundPalletAllocationMapper.class);
Page<InboundOrderEntity> page = new Page<>(1, 20);
InboundOrderEntity first = new InboundOrderEntity();
first.setId(1L);
@@ -84,13 +85,6 @@ class InboundOrderRepositoryTest {
allocation(1L, 1, "A0"),
allocation(1L, 2, "A2"),
allocation(2L, 1, "B1")));
InboundOrderRepository repository = new InboundOrderRepository(
orders,
mock(InboundTransportSnapshotMapper.class),
pallets,
mock(InboundFileMapper.class),
mock(InboundInternalNoteMapper.class),
mock(InboundStatusHistoryMapper.class));
var result = repository.page(10001L, new InboundOrderQuery(
null, null, null, null, null, null, null, null,
@@ -118,9 +112,6 @@ class InboundOrderRepositoryTest {
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);
@@ -132,9 +123,6 @@ class InboundOrderRepositoryTest {
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,
@@ -3,8 +3,14 @@ package com.baiyue.firstleg.security;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.config.MeasurementOpenApiProperties;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.time.Clock;
import java.time.Instant;
@@ -13,19 +19,34 @@ import java.util.Map;
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.when;
@DisplayName("第三方测量请求签名校验")
@SpringJUnitConfig(classes = {MeasurementRequestAuthenticator.class,
MeasurementRequestAuthenticatorTest.TestConfig.class})
class MeasurementRequestAuthenticatorTest {
private static final Instant NOW = Instant.parse("2026-08-01T02:30:00Z");
private static final String SECRET = "test-secret-at-least-32-bytes-long";
@MockBean private NonceReplayGuard guard;
@Resource private MeasurementOpenApiProperties properties;
@Resource private MeasurementRequestAuthenticator authenticator;
@BeforeEach
void setUp() {
properties.setEnabled(true);
MeasurementOpenApiProperties.AppCredential credential = new MeasurementOpenApiProperties.AppCredential();
credential.setSecret(SECRET);
credential.setTenantId(10001L);
credential.setSubjectId(90001L);
credential.setWarehouseBindings(Map.of("CN-YW-01", 30001L));
properties.setApps(Map.of("device-provider", credential));
}
@Test
@DisplayName("合法签名按六行规范串校验并占用Nonce")
void shouldAcceptValidSignatureAndClaimNonce() {
MeasurementOpenApiProperties properties = properties();
NonceReplayGuard guard = (appKey, nonce, ttl) -> true;
MeasurementRequestAuthenticator authenticator = new MeasurementRequestAuthenticator(
properties, guard, Clock.fixed(NOW, ZoneOffset.UTC));
when(guard.claim(any(), any(), any())).thenReturn(true);
String body = "{\"eventNo\":\"M1\"}";
String timestamp = String.valueOf(NOW.toEpochMilli());
String canonical = MeasurementRequestAuthenticator.canonicalString("POST",
@@ -46,12 +67,10 @@ class MeasurementRequestAuthenticatorTest {
@DisplayName("非法签名返回统一未认证业务码且不占用Nonce")
void shouldRejectInvalidSignatureBeforeClaimingNonce() {
int[] claims = {0};
NonceReplayGuard guard = (appKey, nonce, ttl) -> {
when(guard.claim(any(), any(), any())).thenAnswer(invocation -> {
claims[0]++;
return true;
};
MeasurementRequestAuthenticator authenticator = new MeasurementRequestAuthenticator(
properties(), guard, Clock.fixed(NOW, ZoneOffset.UTC));
});
assertThatThrownBy(() -> authenticator.authenticate("POST", "/openapi/v1/inbound-measurements",
new MeasurementRequestHeaders("device-provider", String.valueOf(NOW.toEpochMilli()),
@@ -65,8 +84,7 @@ class MeasurementRequestAuthenticatorTest {
@Test
@DisplayName("相同应用重复Nonce被拒绝")
void shouldRejectReplayedNonce() {
MeasurementRequestAuthenticator authenticator = new MeasurementRequestAuthenticator(
properties(), (appKey, nonce, ttl) -> false, Clock.fixed(NOW, ZoneOffset.UTC));
when(guard.claim(any(), any(), any())).thenReturn(false);
String timestamp = String.valueOf(NOW.toEpochMilli());
String body = "{}";
String canonical = MeasurementRequestAuthenticator.canonicalString("POST",
@@ -81,15 +99,16 @@ class MeasurementRequestAuthenticatorTest {
.isEqualTo(ResultCode.CONFLICT));
}
private MeasurementOpenApiProperties properties() {
MeasurementOpenApiProperties properties = new MeasurementOpenApiProperties();
properties.setEnabled(true);
MeasurementOpenApiProperties.AppCredential credential = new MeasurementOpenApiProperties.AppCredential();
credential.setSecret(SECRET);
credential.setTenantId(10001L);
credential.setSubjectId(90001L);
credential.setWarehouseBindings(Map.of("CN-YW-01", 30001L));
properties.setApps(Map.of("device-provider", credential));
return properties;
@Configuration
static class TestConfig {
@Bean
MeasurementOpenApiProperties properties() {
return new MeasurementOpenApiProperties();
}
@Bean
Clock clock() {
return Clock.fixed(NOW, ZoneOffset.UTC);
}
}
}
@@ -3,7 +3,10 @@ package com.baiyue.firstleg.service;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
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.anyLong;
@@ -13,15 +16,15 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(AutomaticInboundCompletionService.class)
class AutomaticInboundCompletionServiceTest {
private final FbaPricingService pricing = mock(FbaPricingService.class);
private final InboundWorkflowRepository workflow = mock(InboundWorkflowRepository.class);
private final InboundCompletionOrchestrator completion = mock(InboundCompletionOrchestrator.class);
private final AutomaticInboundCompletionService service =
new AutomaticInboundCompletionService(pricing, workflow, completion);
@MockBean private FbaPricingService pricing;
@MockBean private InboundWorkflowRepository workflow;
@MockBean private InboundCompletionOrchestrator completion;
@Resource private AutomaticInboundCompletionService service;
@Test
void 客服审核通过后自动计费发布并完成入库() {
void shouldHandleCase01() {
FeeSnapshotResponse fee = fee(501L);
when(pricing.latest(10001L, 88L)).thenReturn(fee);
when(workflow.findOwned(10001L, 88L)).thenReturn(
@@ -43,7 +46,7 @@ class AutomaticInboundCompletionServiceTest {
}
@Test
void 已完成入库的审核重放只返回最新费用() {
void shouldHandleCase02() {
FeeSnapshotResponse fee = fee(501L);
when(workflow.findOwned(10001L, 88L)).thenReturn(order("INBOUNDED", "SENT", 8));
when(pricing.latest(10001L, 88L)).thenReturn(fee);
@@ -58,7 +61,7 @@ class AutomaticInboundCompletionServiceTest {
}
@Test
void 中断在入库处理中时续跑完成而不重复计费() {
void shouldHandleCase03() {
FeeSnapshotResponse fee = fee(501L);
when(pricing.latest(10001L, 88L)).thenReturn(fee);
when(workflow.findOwned(10001L, 88L)).thenReturn(
@@ -4,8 +4,11 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FbaPricingDtos.CustomerRuleVersionRequest;
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -20,18 +23,22 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("FBA计费基础资料版本")
@SpringJUnitConfig(FbaMasterDataService.class)
class FbaMasterDataServiceTest {
private static final LocalDateTime AUGUST = LocalDateTime.of(2026, 8, 1, 0, 0);
@MockBean
private FbaPricingRepository repository;
@Resource
private FbaMasterDataService service;
@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");
List<CustomerChargeRuleEntity> result = service.listRules(null, "CR-1");
assertThat(result).isSameAs(expected);
verify(repository).listRules(null, "CR-1");
@@ -40,7 +47,6 @@ class FbaMasterDataServiceTest {
@Test
@DisplayName("半开有效期首尾相接不视为重叠")
void acceptsAdjacentHalfOpenPeriods() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
CustomerChargeRuleEntity old = activeRule(AUGUST.minusMonths(1), AUGUST);
when(repository.listRules(1L, null)).thenReturn(List.of(old));
when(repository.listRules(1L, "CR-1")).thenReturn(List.of(old));
@@ -48,8 +54,8 @@ class FbaMasterDataServiceTest {
doAnswer(invocation -> { ((CustomerChargeRuleEntity) invocation.getArgument(0)).setId(10L); return null; })
.when(repository).insertRule(any());
CustomerChargeRuleEntity created = new FbaMasterDataService(repository)
.createRuleVersion(1L, 2L, request(AUGUST, AUGUST.plusMonths(1)));
CustomerChargeRuleEntity created = service.createRuleVersion(
1L, 2L, request(AUGUST, AUGUST.plusMonths(1)));
assertThat(created.getId()).isEqualTo(10L);
assertThat(created.getRuleVersion()).isEqualTo(2);
@@ -58,12 +64,11 @@ class FbaMasterDataServiceTest {
@Test
@DisplayName("任意重叠有效期在保存阶段拒绝避免多规则匹配")
void rejectsOverlappingPeriods() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
when(repository.listRules(1L, null)).thenReturn(List.of(
activeRule(AUGUST, AUGUST.plusMonths(1))));
assertThatThrownBy(() -> new FbaMasterDataService(repository)
.createRuleVersion(1L, 2L, request(AUGUST.plusDays(1), AUGUST.plusMonths(2))))
assertThatThrownBy(() -> service.createRuleVersion(
1L, 2L, request(AUGUST.plusDays(1), AUGUST.plusMonths(2))))
.isInstanceOf(BizException.class)
.hasMessageContaining("有效期重叠");
}
@@ -10,9 +10,14 @@ import com.baiyue.firstleg.entity.InboundFeeItemEntity;
import com.baiyue.firstleg.entity.InboundPricingEntity;
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.time.Clock;
@@ -35,16 +40,19 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("FBA费用状态机")
@SpringJUnitConfig(classes = {FbaPricingService.class, FbaPricingServiceTest.TestConfig.class})
class FbaPricingServiceTest {
private static final long TENANT = 10001L;
private static final long OPERATOR = 20001L;
private static final long INBOUND = 30001L;
private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 3, 10, 0);
@MockBean private FbaPricingRepository repository;
@Resource private FbaPricingService service;
@Test
@DisplayName("包税KG按配置系数取体积重较大值并按规则向上舍入")
void calculatesTaxIncludedKgWithConfiguredFactorAndPrecision() {
FbaPricingRepository repository = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("100.000", "1.0001");
CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING");
CarrierFbaRateEntity rate = rate("KG", "2.345678", "10.00");
@@ -52,7 +60,7 @@ class FbaPricingServiceTest {
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(rule));
when(repository.matchRates(inbound, "KG", NOW)).thenReturn(List.of(rate));
FeeSnapshotResponse response = service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-1");
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-1");
assertThat(response.convertedQuantity()).isEqualByComparingTo("167.017");
assertThat(response.chargeableQuantity()).isEqualByComparingTo("167.017");
@@ -64,7 +72,7 @@ class FbaPricingServiceTest {
@Test
@DisplayName("不包税CBM按配置系数折算且精度边界向上取整")
void calculatesTaxExcludedCbmAtRoundingBoundary() {
FbaPricingRepository repository = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("363.001", "0.5000");
CustomerChargeRuleEntity rule = rule("CBM", "363.000000", 4, "CEILING");
CarrierFbaRateEntity rate = rate("CBM", "100.000000", "0.00");
@@ -72,7 +80,7 @@ class FbaPricingServiceTest {
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(rule));
when(repository.matchRates(inbound, "CBM", NOW)).thenReturn(List.of(rate));
FeeSnapshotResponse response = service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-2");
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-2");
assertThat(response.convertedQuantity()).isEqualByComparingTo("1.0001");
assertThat(response.chargeableQuantity()).isEqualByComparingTo("1.0001");
@@ -82,12 +90,12 @@ class FbaPricingServiceTest {
@Test
@DisplayName("审核未通过的测量快照禁止计费")
void rejectsUnapprovedMeasurementSnapshot() {
FbaPricingRepository repository = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
inbound.setMeasureReviewStatus("REJECTED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
assertThatThrownBy(() -> service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3"))
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3"))
.isInstanceOf(BizException.class)
.hasMessageContaining("审核通过");
verify(repository, never()).matchRules(any(), any());
@@ -96,25 +104,25 @@ class FbaPricingServiceTest {
@Test
@DisplayName("无客户规则和多客户规则都明确拒绝")
void rejectsMissingAndAmbiguousRules() {
FbaPricingRepository missing = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
when(missing.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(missing.matchRules(inbound, NOW)).thenReturn(List.of());
assertThatThrownBy(() -> service(missing).calculate(TENANT, OPERATOR, INBOUND, NOW, "none"))
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.matchRules(inbound, NOW)).thenReturn(List.of());
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "none"))
.isInstanceOf(BizException.class).hasMessageContaining("未匹配到有效客户计费规则");
FbaPricingRepository ambiguous = readyRepository();
when(ambiguous.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(ambiguous.matchRules(inbound, NOW)).thenReturn(List.of(
org.mockito.Mockito.reset(repository);
readyRepository();
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(
rule("KG", "167", 3, "CEILING"), rule("KG", "168", 3, "CEILING")));
assertThatThrownBy(() -> service(ambiguous).calculate(TENANT, OPERATOR, INBOUND, NOW, "many"))
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "many"))
.isInstanceOf(BizException.class).hasMessageContaining("多条有效客户计费规则");
}
@Test
@DisplayName("同一幂等键重复试算直接返回原费用快照")
void returnsOriginalSnapshotForRepeatedCalculation() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED"));
@@ -122,7 +130,7 @@ class FbaPricingServiceTest {
InboundFeeItemEntity item = new InboundFeeItemEntity(); item.setUnitPrice(new BigDecimal("2.00"));
when(repository.findBaseItem(TENANT, 501L)).thenReturn(item);
FeeSnapshotResponse response = service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
assertThat(response.feeId()).isEqualTo(501L);
verify(repository, never()).matchRules(any(), any());
@@ -132,7 +140,6 @@ class FbaPricingServiceTest {
@Test
@DisplayName("重复发送只返回首次结果且不会重复写Outbox")
void sendIsIdempotentAndDoesNotDuplicateOutbox() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingEntity inbound = inbound("10", "1");
InboundFeeEntity confirmed = savedFee("CONFIRMED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
@@ -148,7 +155,6 @@ class FbaPricingServiceTest {
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
.when(repository).insertIdempotency(any());
FbaPricingService service = service(repository);
service.send(TENANT, OPERATOR, INBOUND, "send-once");
service.send(TENANT, OPERATOR, INBOUND, "send-once");
@@ -159,12 +165,11 @@ class FbaPricingServiceTest {
@Test
@DisplayName("同一幂等键用于不同试算时点会冲突")
void rejectsReusedIdempotencyKeyWithDifferentRequest() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
completed.setRequestHash("different");
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
assertThatThrownBy(() -> service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "same"))
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same"))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
@@ -173,7 +178,6 @@ class FbaPricingServiceTest {
@Test
@DisplayName("已发布费用可生成新版本且不回退已入库状态")
void adjustsPublishedFeeWithoutRollingBackInbound() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("INBOUNDED");
inbound.setFeeStatus("SENT");
@@ -195,7 +199,7 @@ class FbaPricingServiceTest {
when(repository.completeIdempotency(any(), eq(502L), eq(NOW))).thenReturn(1);
when(repository.findFee(TENANT, 502L)).thenAnswer(invocation -> inserted.get());
FeeSnapshotResponse response = service(repository).adjustPublished(TENANT, OPERATOR, INBOUND,
FeeSnapshotResponse response = service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
new BigDecimal("100.00"), new BigDecimal("20.00"), "客服修正"), "adjust-1");
@@ -208,8 +212,7 @@ class FbaPricingServiceTest {
verify(repository).insertOutbox(any());
}
private FbaPricingRepository readyRepository() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
private void readyRepository() {
when(repository.findIdempotency(anyLong(), anyLong(), any(), any())).thenReturn(null);
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
.when(repository).insertIdempotency(any());
@@ -218,12 +221,14 @@ class FbaPricingServiceTest {
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(1);
when(repository.markInboundCalculated(eq(TENANT), eq(INBOUND), any(), any())).thenReturn(1);
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
return repository;
}
private FbaPricingService service(FbaPricingRepository repository) {
return new FbaPricingService(repository,
Clock.fixed(Instant.parse("2026-08-03T02:00:00Z"), ZoneId.of("Asia/Shanghai")));
@Configuration
static class TestConfig {
@Bean
Clock clock() {
return Clock.fixed(Instant.parse("2026-08-03T02:00:00Z"), ZoneId.of("Asia/Shanghai"));
}
}
private InboundPricingEntity inbound(String weight, String volume) {
@@ -6,9 +6,12 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FulfillmentTaskQuery;
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
import com.baiyue.firstleg.repository.FulfillmentTaskRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.List;
@@ -19,24 +22,24 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("履约任务服务")
@SpringJUnitConfig(FulfillmentTaskService.class)
class FulfillmentTaskServiceTest {
@MockBean private FulfillmentTaskRepository repository;
@Resource private FulfillmentTaskService service;
@Test
@DisplayName("列表查询保留租户和分页条件")
void shouldPreserveTenantAndPaginationInPageQuery() {
FulfillmentTaskRepository repository = mock(FulfillmentTaskRepository.class);
FulfillmentTaskQuery query = new FulfillmentTaskQuery(null, null, null, 1, 20);
PageResult<FulfillmentTaskEntity> expected = new PageResult<>(List.of(), 1, 20, 0, 0);
when(repository.page(10001L, query)).thenReturn(expected);
assertThat(service(repository).page(10001L, query)).isSameAs(expected);
assertThat(service.page(10001L, query)).isSameAs(expected);
verify(repository).page(10001L, query);
}
@Test
@DisplayName("详情和按订单查询不存在时返回404")
void shouldReturnNotFoundForMissingTaskAndOrder() {
FulfillmentTaskRepository repository = mock(FulfillmentTaskRepository.class);
FulfillmentTaskService service = service(repository);
assertThatThrownBy(() -> service.detail(10001L, 1L))
.isInstanceOf(BizException.class)
@@ -51,20 +54,14 @@ class FulfillmentTaskServiceTest {
@Test
@DisplayName("查询分配历史前先校验任务归属租户")
void shouldVerifyTaskTenantBeforeLoadingAssignmentHistory() {
FulfillmentTaskRepository repository = mock(FulfillmentTaskRepository.class);
FulfillmentTaskEntity task = new FulfillmentTaskEntity();
when(repository.findOwned(10001L, 1L)).thenReturn(task);
PageResult<com.baiyue.firstleg.entity.AssignmentHistoryEntity> expected =
new PageResult<>(List.of(), 1, 20, 0, 0);
when(repository.pageHistory(10001L, 1L, 1, 20)).thenReturn(expected);
assertThat(service(repository).assignmentHistory(10001L, 1L, 1, 20)).isSameAs(expected);
assertThat(service.assignmentHistory(10001L, 1L, 1, 20)).isSameAs(expected);
verify(repository).findOwned(10001L, 1L);
}
private FulfillmentTaskService service(FulfillmentTaskRepository repository) {
FulfillmentTaskService service = new FulfillmentTaskService();
ReflectionTestUtils.setField(service, "repository", repository);
return service;
}
}
@@ -5,8 +5,11 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.entity.InboundFeeEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
@@ -19,7 +22,14 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("发送费用并入库状态编排")
@SpringJUnitConfig(InboundCompletionOrchestrator.class)
class InboundCompletionOrchestratorTest {
@MockBean
private InboundWorkflowRepository repository;
@MockBean
private InboundIdempotencyService idempotency;
@Resource
private InboundCompletionOrchestrator service;
@Test
@DisplayName("审核通过且费用和库位引用已准备时进入INBOUNDING")
void shouldBeginInboundAfterAllGuardsPass() {
@@ -104,8 +114,6 @@ class InboundCompletionOrchestratorTest {
}
private Fixture fixture(String reviewStatus, boolean feeRelease) {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
InboundIdempotencyService idempotency = mock(InboundIdempotencyService.class);
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(new InboundIdempotencyService.Context(1L, false));
InboundOrderEntity order = new InboundOrderEntity();
@@ -117,7 +125,7 @@ class InboundCompletionOrchestratorTest {
when(repository.findOwned(10001L, 10L)).thenReturn(order);
when(repository.findConfirmedFee(10001L, 10L, 30L)).thenReturn(fee);
when(repository.countPreparedAllocations(10001L, 10L)).thenReturn(1L);
return new Fixture(new InboundCompletionOrchestrator(repository, idempotency), repository, order);
return new Fixture(service, repository, order);
}
private record Fixture(InboundCompletionOrchestrator service,
@@ -4,9 +4,12 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.entity.InboundIdempotencyEntity;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DuplicateKeyException;
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.assertj.core.api.Assertions.assertThatThrownBy;
@@ -17,16 +20,19 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("入库写操作幂等边界")
@SpringJUnitConfig(InboundIdempotencyService.class)
class InboundIdempotencyServiceTest {
@MockBean
private InboundWorkflowRepository repository;
@Resource
private InboundIdempotencyService service;
@Test
@DisplayName("同键同请求成功记录可重放")
void shouldReplayCompletedRequest() {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
doThrow(new DuplicateKeyException("duplicate")).when(repository).insertIdempotency(any());
InboundIdempotencyEntity existing = existing("SUCCESS");
when(repository.findIdempotency(10001L, "MEASURE_APPROVE", 9L, "key-1"))
.thenReturn(existing);
InboundIdempotencyService service = new InboundIdempotencyService(repository);
InboundIdempotencyService.Context context = service.begin(
10001L, 9L, 10L, "MEASURE_APPROVE", "key-1", "10|20|3|");
@@ -37,13 +43,11 @@ class InboundIdempotencyServiceTest {
@Test
@DisplayName("同键不同请求返回冲突")
void shouldRejectDifferentRequestForSameKey() {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
doThrow(new DuplicateKeyException("duplicate")).when(repository).insertIdempotency(any());
InboundIdempotencyEntity existing = existing("SUCCESS");
existing.setRequestHash("different-hash");
when(repository.findIdempotency(10001L, "MEASURE_APPROVE", 9L, "key-1"))
.thenReturn(existing);
InboundIdempotencyService service = new InboundIdempotencyService(repository);
assertThatThrownBy(() -> service.begin(
10001L, 9L, 10L, "MEASURE_APPROVE", "key-1", "10|20|4|"))
@@ -55,9 +59,7 @@ class InboundIdempotencyServiceTest {
@Test
@DisplayName("新请求完成时只允许PROCESSING条件更新一次")
void shouldCompleteProcessingRecordOnce() {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
when(repository.completeIdempotency(1L, "{\"inboundId\":\"10\"}")).thenReturn(1);
InboundIdempotencyService service = new InboundIdempotencyService(repository);
service.complete(new InboundIdempotencyService.Context(1L, false), 10L);
@@ -13,6 +13,8 @@ import com.baiyue.firstleg.security.AuthenticatedMeasurementApp;
import com.baiyue.firstleg.security.MeasurementRequestAuthenticator;
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import jakarta.validation.Validation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -20,6 +22,11 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.time.OffsetDateTime;
import java.math.BigDecimal;
@@ -41,6 +48,9 @@ import static org.mockito.Mockito.doThrow;
import org.mockito.ArgumentCaptor;
@DisplayName("第三方测量开放接口服务")
@SpringJUnitConfig(classes = {InboundMeasurementOpenApiService.class,
InboundMeasurementOpenApiServiceTest.TestConfig.class})
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class InboundMeasurementOpenApiServiceTest {
private static final String BODY = """
{"eventNo":"M202608010001","warehouseCode":"CN-YW-01","inboundCode":"RK202608010001",
@@ -50,6 +60,14 @@ class InboundMeasurementOpenApiServiceTest {
"boxSpecification":"50.2x40.1x30cm"}
""";
private static final byte[] IMAGE = "image-content".getBytes(java.nio.charset.StandardCharsets.UTF_8);
@MockBean private InboundMeasurementOpenApiRepository repository;
@MockBean private MeasurementRequestAuthenticator authenticator;
@MockBean private ThirdPartyCallLogService logs;
@MockBean private TransactionTemplate transactions;
@MockBean private OrderCargoClient orderCargo;
@MockBean private MeasurementImageClient images;
@Resource private ObjectMapper objectMapper;
@Resource private InboundMeasurementOpenApiService service;
@Test
@DisplayName("回调只写批次明细和幂等结果,不存在入库主单update入口")
@@ -85,12 +103,11 @@ class InboundMeasurementOpenApiServiceTest {
@DisplayName("重复Idempotency-Key且报文一致时返回首次结果,不重复写测量")
void shouldReturnFirstResultForDuplicateCallback() throws Exception {
Fixture fixture = fixture();
ObjectMapper mapper = objectMapper();
EventResponse first = new EventResponse("M202608010001", "RK202608010001", true,
"RECEIVED", OffsetDateTime.parse("2026-08-01T02:30:00Z"));
ThirdPartyCallLogEntity existing = new ThirdPartyCallLogEntity();
existing.setRequestHash(requestHash());
existing.setResultJson(mapper.writeValueAsString(first));
existing.setResultJson(objectMapper.writeValueAsString(first));
existing.setProcessingStatus("ACCEPTED");
when(fixture.logs.claimIdempotency(1L)).thenReturn(false);
when(fixture.logs.findIdempotencyOwner(10001L, "device-provider", "idem-1")).thenReturn(existing);
@@ -137,12 +154,6 @@ class InboundMeasurementOpenApiServiceTest {
}
private Fixture fixture() {
InboundMeasurementOpenApiRepository repository = mock(InboundMeasurementOpenApiRepository.class);
MeasurementRequestAuthenticator authenticator = mock(MeasurementRequestAuthenticator.class);
ThirdPartyCallLogService logs = mock(ThirdPartyCallLogService.class);
TransactionTemplate transactions = mock(TransactionTemplate.class);
OrderCargoClient orderCargo = mock(OrderCargoClient.class);
MeasurementImageClient images = mock(MeasurementImageClient.class);
when(logs.start(anyString(), anyString(), any(), anyString(), anyString())).thenReturn(1L);
when(logs.claimIdempotency(1L)).thenReturn(true);
when(authenticator.authenticate(anyString(), anyString(), any(), anyString(), anyBoolean()))
@@ -161,14 +172,20 @@ class InboundMeasurementOpenApiServiceTest {
TransactionCallback<EventResponse> callback = invocation.getArgument(0);
return callback.doInTransaction(mock(TransactionStatus.class));
});
InboundMeasurementOpenApiService service = new InboundMeasurementOpenApiService(repository,
authenticator, logs, orderCargo, images, "internal-token", objectMapper(),
Validation.buildDefaultValidatorFactory().getValidator(), transactions);
return new Fixture(repository, logs, orderCargo, images, service);
}
private ObjectMapper objectMapper() {
return new ObjectMapper().findAndRegisterModules();
@Configuration
static class TestConfig {
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper().findAndRegisterModules();
}
@Bean
Validator validator() {
return Validation.buildDefaultValidatorFactory().getValidator();
}
}
private InboundOrderLookupEntity inbound() {
@@ -9,10 +9,13 @@ import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
import com.baiyue.firstleg.entity.InboundWeightRevisionEntity;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 java.math.BigDecimal;
import java.util.List;
@@ -26,15 +29,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("平台入库测量应用服务")
@SpringJUnitConfig(InboundMeasurementService.class)
class InboundMeasurementServiceTest {
private InboundMeasurementRepository repository;
private InboundMeasurementService service;
@BeforeEach
void setUp() {
repository = mock(InboundMeasurementRepository.class);
service = new InboundMeasurementService(repository, mock(MeasurementBatchService.class));
}
@MockBean private InboundMeasurementRepository repository;
@MockBean private MeasurementBatchService batchService;
@Resource private InboundMeasurementService service;
@Test
void correctingWeightKeepsOcrOriginalAndWritesImmutableRevision() {
@@ -9,7 +9,10 @@ import com.baiyue.firstleg.dto.InboundOrderQuery;
import com.baiyue.firstleg.dto.InboundOrderSummary;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundOrderRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.time.LocalDateTime;
import java.util.List;
@@ -20,15 +23,20 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(InboundOrderQueryService.class)
class InboundOrderQueryServiceTest {
@MockBean
private InboundOrderRepository repository;
@Resource
private InboundOrderQueryService service;
@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);
assertThat(service.page(null, query)).isSameAs(expected);
verify(repository).page(null, query);
}
@@ -46,9 +54,8 @@ class InboundOrderQueryServiceTest {
@Test
void shouldRejectConflictingTabAndStatus() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
InboundOrderQuery query = query("WAIT_MEASURE", InboundListTab.WAIT_MEASURE, null, null);
assertThatThrownBy(() -> new InboundOrderQueryService(repository).page(10001L, query))
assertThatThrownBy(() -> service.page(10001L, query))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.BAD_REQUEST);
@@ -56,31 +63,28 @@ class InboundOrderQueryServiceTest {
@Test
void shouldRejectReversedArrivalRange() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
LocalDateTime now = LocalDateTime.now();
assertThatThrownBy(() -> new InboundOrderQueryService(repository)
.page(10001L, query(null, null, now, now.minusDays(1))))
assertThatThrownBy(() -> service.page(
10001L, query(null, null, now, now.minusDays(1))))
.isInstanceOf(BizException.class);
}
@Test
void shouldLoadDetailOnlyAfterTenantOwnedOrderExists() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
InboundOrderEntity order = new InboundOrderEntity();
order.setId(9L);
InboundOrderDetail detail = new InboundOrderDetail(null, null, List.of(), List.of(), List.of(), List.of());
when(repository.findOwned(10001L, 9L)).thenReturn(order);
when(repository.detail(10001L, order)).thenReturn(detail);
assertThat(new InboundOrderQueryService(repository).detail(10001L, 9L)).isSameAs(detail);
assertThat(service.detail(10001L, 9L)).isSameAs(detail);
verify(repository).findOwned(10001L, 9L);
verify(repository).detail(10001L, order);
}
@Test
void shouldReturnBusinessNotFoundForCrossTenantOrMissingOrder() {
InboundOrderRepository repository = mock(InboundOrderRepository.class);
assertThatThrownBy(() -> new InboundOrderQueryService(repository).detail(10002L, 9L))
assertThatThrownBy(() -> service.detail(10002L, 9L))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.NOT_FOUND);
@@ -6,10 +6,13 @@ import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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 java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -24,14 +27,13 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("测量批次共享领域服务")
@SpringJUnitConfig(MeasurementBatchService.class)
class MeasurementBatchServiceTest {
private InboundMeasurementRepository repository;
private MeasurementBatchService service;
@MockBean private InboundMeasurementRepository repository;
@Resource private MeasurementBatchService service;
@BeforeEach
void setUp() {
repository = mock(InboundMeasurementRepository.class);
service = new MeasurementBatchService(repository);
when(repository.findInbound(1L, 10L)).thenReturn(inbound());
org.mockito.Mockito.doAnswer(invocation -> {
InboundMeasurementEntity entity = invocation.getArgument(0);
@@ -6,8 +6,14 @@ import com.baiyue.firstleg.config.InboundProperties;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.util.List;
@@ -19,6 +25,9 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(classes = {MeasurementLocationOccupationService.class,
MeasurementLocationOccupationServiceTest.TestConfig.class})
@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;
@@ -27,12 +36,11 @@ class MeasurementLocationOccupationServiceTest {
private static final long OPERATOR = 9L;
private static final long MEASUREMENT = 30L;
private final WarehousePalletLocationClient warehouse = mock(WarehousePalletLocationClient.class);
private final InboundPalletAllocationRepository allocations = mock(InboundPalletAllocationRepository.class);
private final InboundWorkflowRepository workflow = mock(InboundWorkflowRepository.class);
private final InboundProperties properties = properties();
private final MeasurementLocationOccupationService service =
new MeasurementLocationOccupationService(warehouse, allocations, workflow, properties, "internal-token");
@MockBean private WarehousePalletLocationClient warehouse;
@MockBean private InboundPalletAllocationRepository allocations;
@MockBean private InboundWorkflowRepository workflow;
@Resource private InboundProperties properties;
@Resource private MeasurementLocationOccupationService service;
@Test
void sharedWarehouseResourcesUseResourceTenantWhileSnapshotsRemainInBusinessTenant() {
@@ -119,6 +127,14 @@ class MeasurementLocationOccupationServiceTest {
return value;
}
@Configuration
static class TestConfig {
@Bean
InboundProperties inboundProperties() {
return properties();
}
}
private static InboundOrderEntity inbound() {
InboundOrderEntity value = new InboundOrderEntity();
value.setTenantId(BUSINESS_TENANT);
@@ -8,8 +8,11 @@ import com.baiyue.firstleg.dto.MeasurementReviewRequest;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.util.List;
import java.util.concurrent.Callable;
@@ -31,7 +34,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("入库测量审核服务")
@SpringJUnitConfig(MeasurementReviewService.class)
class MeasurementReviewServiceTest {
@MockBean private InboundWorkflowRepository repository;
@MockBean private InboundIdempotencyService idempotency;
@MockBean private InboundProperties properties;
@Resource private MeasurementReviewService service;
@Test
@DisplayName("审核通过是测量到计费的唯一正常推进关口")
void shouldApproveOnlyPendingSubmittedMeasurement() {
@@ -138,12 +146,9 @@ class MeasurementReviewServiceTest {
}
private Fixture fixture(boolean allowRelease) {
InboundWorkflowRepository repository = mock(InboundWorkflowRepository.class);
InboundIdempotencyService idempotency = mock(InboundIdempotencyService.class);
when(idempotency.begin(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
.thenReturn(new InboundIdempotencyService.Context(1L, false));
InboundProperties properties = new InboundProperties();
properties.setAllowRejectedMeasurement(allowRelease);
when(properties.isAllowRejectedMeasurement()).thenReturn(allowRelease);
InboundOrderEntity order = new InboundOrderEntity();
order.setProcessStatus("MEASURE_PENDING");
order.setMeasureReviewStatus("PENDING");
@@ -151,8 +156,7 @@ class MeasurementReviewServiceTest {
measurement.setStatus("SUBMITTED");
when(repository.findOwned(10001L, 10L)).thenReturn(order);
when(repository.findMeasurement(10001L, 10L, 20L)).thenReturn(measurement);
return new Fixture(new MeasurementReviewService(repository, idempotency, properties),
repository, idempotency, order, measurement);
return new Fixture(service, repository, idempotency, order, measurement);
}
private record Fixture(MeasurementReviewService service, InboundWorkflowRepository repository,
@@ -2,7 +2,10 @@ package com.baiyue.firstleg.service;
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@@ -12,13 +15,14 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(MeasurementSubmissionService.class)
class MeasurementSubmissionServiceTest {
private final InboundMeasurementService measurements = mock(InboundMeasurementService.class);
private final MeasurementLocationOccupationService locations = mock(MeasurementLocationOccupationService.class);
private final MeasurementSubmissionService service = new MeasurementSubmissionService(measurements, locations);
@MockBean private InboundMeasurementService measurements;
@MockBean private MeasurementLocationOccupationService locations;
@Resource private MeasurementSubmissionService service;
@Test
void 测量提交与库位占用处于同一事务边界() throws Exception {
void shouldHandleCase01() throws Exception {
assertThat(MeasurementSubmissionService.class
.getDeclaredMethod("submit", long.class, long.class, long.class,
InboundMeasurementRequests.SubmitRequest.class)
@@ -26,7 +30,7 @@ class MeasurementSubmissionServiceTest {
}
@Test
void 测量完成后立即占用库位再返回待客服审核() {
void shouldHandleCase02() {
var request = new InboundMeasurementRequests.SubmitRequest(30L, 2);
var submitted = new InboundMeasurementResponses.MeasurementSubmitted(
30L, 3, "MEASURE_PENDING", "PENDING", "STANDARD", null, null, null);
@@ -38,7 +42,7 @@ class MeasurementSubmissionServiceTest {
}
@Test
void 库位占用失败向上抛出以触发测量事务回滚() {
void shouldHandleCase03() {
var request = new InboundMeasurementRequests.SubmitRequest(30L, 2);
var submitted = new InboundMeasurementResponses.MeasurementSubmitted(
30L, 3, "MEASURE_PENDING", "PENDING", "STANDARD", null, null, null);
@@ -7,7 +7,11 @@ import com.baiyue.firstleg.dto.InboundMeasurementResponses;
import com.baiyue.firstleg.client.MeasurementImageClient;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundOrderRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.util.List;
@@ -19,16 +23,17 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(MerchantInboundBillingService.class)
@TestPropertySource(properties = "baiyue.internal.token=internal-token")
class MerchantInboundBillingServiceTest {
private final InboundOrderRepository orders = mock(InboundOrderRepository.class);
private final FbaPricingService pricing = mock(FbaPricingService.class);
private final InboundMeasurementService measurements = mock(InboundMeasurementService.class);
private final MeasurementImageClient imageClient = mock(MeasurementImageClient.class);
private final MerchantInboundBillingService service = new MerchantInboundBillingService(
orders, pricing, measurements, imageClient, "internal-token");
@MockBean private InboundOrderRepository orders;
@MockBean private FbaPricingService pricing;
@MockBean private InboundMeasurementService measurements;
@MockBean private MeasurementImageClient imageClient;
@Resource private MerchantInboundBillingService service;
@Test
void 客户只能看到本租户已完成入库且已发布的费用() {
void shouldHandleCase01() {
InboundOrderEntity inbound = inbound("INBOUNDED", "SENT");
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
"客户备注", List.of(), List.of(
@@ -64,7 +69,7 @@ class MerchantInboundBillingServiceTest {
}
@Test
void 客服尚未完成发布时客户不可见中间费用() {
void shouldHandleCase02() {
InboundOrderEntity inbound = inbound("FEE_CONFIRMING", "CONFIRMED");
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
@@ -75,7 +80,7 @@ class MerchantInboundBillingServiceTest {
}
@Test
void 使用租户条件查询防止跨租户读取订单费用() {
void shouldHandleCase03() {
when(orders.findByOrderId(20002L, 77L)).thenReturn(null);
assertThatThrownBy(() -> service.find(20002L, 77L)).isInstanceOf(BizException.class);
@@ -85,7 +90,7 @@ class MerchantInboundBillingServiceTest {
}
@Test
void 只能下载已发布计费明细实际引用的图片() {
void shouldHandleCase04() {
InboundOrderEntity inbound = inbound("INBOUNDED", "SENT");
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
InboundMeasurementResponses.Item item = mock(InboundMeasurementResponses.Item.class);
@@ -94,7 +99,9 @@ class MerchantInboundBillingServiceTest {
when(measurement.weightResults()).thenReturn(List.of());
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
when(measurements.detail(10001L, 88L)).thenReturn(measurement);
when(imageClient.download("internal-token", 10001L, 88L, 41L)).thenReturn(
when(imageClient.download(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.eq(10001L), org.mockito.ArgumentMatchers.eq(88L),
org.mockito.ArgumentMatchers.eq(41L))).thenReturn(
org.springframework.http.ResponseEntity.ok()
.contentType(org.springframework.http.MediaType.IMAGE_PNG)
.body(new byte[]{1, 2, 3}));
@@ -106,7 +113,7 @@ class MerchantInboundBillingServiceTest {
}
@Test
void 拒绝下载未被当前计费明细引用的图片() {
void shouldHandleCase05() {
InboundOrderEntity inbound = inbound("INBOUNDED", "SENT");
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
when(measurement.items()).thenReturn(List.of());