Merge branch 'test' into test-02huogui

# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
#	firstleg/src/main/java/com/baiyue/firstleg/BaiyueFirstlegApplication.java
#	firstleg/src/main/resources/openapi/firstleg-v1.yaml
This commit is contained in:
2026-08-07 11:11:43 +08:00
55 changed files with 3173 additions and 94 deletions
@@ -53,7 +53,9 @@ public class InternalInboundOrderController {
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
}
if (command.tenantId() <= 0 || command.orderId() <= 0 || command.warehouseId() == null
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()) {
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()
|| command.inboundCode() == null || command.inboundCode().isBlank()
|| command.inboundCode().length() > 40) {
throw new BizException(ResultCode.BAD_REQUEST, "订单入库快照不完整");
}
return ApiResponse.success(service.createFromApprovedOrder(command));
@@ -75,19 +75,20 @@ public class PlatformFbaMasterDataController {
@GetMapping("/carrier-rates")
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rate:view')")
@Operation(summary = "查询船司 FBA 价格版本", description = "默认查询全部企业;tenantId 可用于筛选目标企业")
@Operation(summary = "查询船司 FBA 价格版本", description = "查询平台公共价格;所有客户共享")
public ApiResponse<List<CarrierFbaRateEntity>> rates(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) Long tenantId,
@RequestParam(required = false) String rateCode) {
return ApiResponse.success(service.listRates(optionalPlatformTenant(user, tenantId), rateCode));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.listRates(rateCode));
}
@GetMapping("/carrier-rates/{id}")
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rate:view')")
public ApiResponse<CarrierFbaRateEntity> rate(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long id) {
return ApiResponse.success(service.rate(FirstlegScopeGuard.platform(user, tenantId), id));
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long id) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.rate(id));
}
@PostMapping("/carrier-rates/versions")
@@ -95,10 +96,10 @@ public class PlatformFbaMasterDataController {
@BusinessAudit(event = "FBA_RATE_VERSION_CREATE", resourceType = "CARRIER_FBA_RATE",
description = "创建船司FBA价格版本")
public ApiResponse<CarrierFbaRateEntity> createRate(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody CarrierRateVersionRequest request) {
long scope = FirstlegScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.createRateVersion(scope, user.userId(), request));
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.createRateVersion(user.userId(), request));
}
@DeleteMapping("/carrier-rates/{id}")
@@ -106,8 +107,9 @@ public class PlatformFbaMasterDataController {
@BusinessAudit(event = "FBA_RATE_RETIRE", resourceType = "CARRIER_FBA_RATE", resourceId = "#id",
description = "停用船司FBA价格版本")
public ApiResponse<Void> retireRate(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId, @PathVariable long id) {
service.retireRate(FirstlegScopeGuard.platform(user, tenantId), id, user.userId());
@PathVariable long id) {
FirstlegScopeGuard.platform(user);
service.retireRate(id, user.userId());
return ApiResponse.success(null);
}
@@ -7,6 +7,7 @@ public record OrderApprovedInboundCommand(
long tenantId,
long orderId,
String orderNo,
String inboundCode,
Long customerId,
String customerName,
Long warehouseId,
@@ -15,6 +15,9 @@ import java.time.LocalDateTime;
@Setter
@TableName("fl_carrier_fba_rate")
public class CarrierFbaRateEntity extends BaseEntity {
/** 船司价格是平台公共主数据,不绑定任何客户。 */
public static final long PLATFORM_SCOPE = 0L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
@@ -50,9 +50,9 @@ public class FbaPricingRepository {
.eq(CustomerChargeRuleEntity::getId, id));
}
public CarrierFbaRateEntity findRate(long tenantId, long id) {
public CarrierFbaRateEntity findRate(long id) {
return rates.selectOne(new LambdaQueryWrapper<CarrierFbaRateEntity>()
.eq(CarrierFbaRateEntity::getTenantId, tenantId)
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(CarrierFbaRateEntity::getId, id));
}
@@ -64,21 +64,34 @@ public class FbaPricingRepository {
.orderByDesc(CustomerChargeRuleEntity::getRuleVersion));
}
public List<CarrierFbaRateEntity> listRates(Long tenantId, String rateCode) {
public List<CarrierFbaRateEntity> listRates(String rateCode) {
return rates.selectList(new LambdaQueryWrapper<CarrierFbaRateEntity>()
.eq(tenantId != null, CarrierFbaRateEntity::getTenantId, tenantId)
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(rateCode != null && !rateCode.isBlank(), CarrierFbaRateEntity::getRateCode, rateCode)
.orderByAsc(CarrierFbaRateEntity::getRateCode)
.orderByDesc(CarrierFbaRateEntity::getRateVersion));
}
/**
* 在创建平台公共价格版本时锁定当前公共价格集合,串行化版本号和有效期检查。
* 调用方必须在事务中使用该方法。
*/
public List<CarrierFbaRateEntity> listRatesForUpdate(String rateCode) {
return rates.selectList(new LambdaQueryWrapper<CarrierFbaRateEntity>()
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(rateCode != null && !rateCode.isBlank(), CarrierFbaRateEntity::getRateCode, rateCode)
.orderByAsc(CarrierFbaRateEntity::getRateCode)
.orderByDesc(CarrierFbaRateEntity::getRateVersion)
.last("FOR UPDATE"));
}
public int nextRuleVersion(long tenantId, String ruleCode) {
List<CustomerChargeRuleEntity> values = listRules(tenantId, ruleCode);
return values.isEmpty() ? 1 : values.get(0).getRuleVersion() + 1;
}
public int nextRateVersion(long tenantId, String rateCode) {
List<CarrierFbaRateEntity> values = listRates(tenantId, rateCode);
public int nextRateVersion(String rateCode) {
List<CarrierFbaRateEntity> values = listRatesForUpdate(rateCode);
return values.isEmpty() ? 1 : values.get(0).getRateVersion() + 1;
}
@@ -91,9 +104,9 @@ public class FbaPricingRepository {
.set(CustomerChargeRuleEntity::getUpdatedBy, operatorId));
}
public int retireRate(long tenantId, long id, long operatorId) {
public int retireRate(long id, long operatorId) {
return rates.update(null, new LambdaUpdateWrapper<CarrierFbaRateEntity>()
.eq(CarrierFbaRateEntity::getTenantId, tenantId)
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(CarrierFbaRateEntity::getId, id)
.eq(CarrierFbaRateEntity::getStatus, "ACTIVE")
.set(CarrierFbaRateEntity::getStatus, "INACTIVE")
@@ -121,7 +134,7 @@ public class FbaPricingRepository {
public List<CarrierFbaRateEntity> matchRates(InboundPricingEntity inbound, String chargeUnit,
LocalDateTime at) {
return rates.selectList(new LambdaQueryWrapper<CarrierFbaRateEntity>()
.eq(CarrierFbaRateEntity::getTenantId, inbound.getTenantId())
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
.eq(CarrierFbaRateEntity::getOriginWarehouseId, inbound.getWarehouseId())
.eq(CarrierFbaRateEntity::getBusinessType, inbound.getBusinessType())
.eq(CarrierFbaRateEntity::getChannelCode, inbound.getChannelCode())
@@ -188,15 +201,17 @@ public class FbaPricingRepository {
.set(InboundFeeEntity::getUpdatedAt, LocalDateTime.now()));
}
public int markInboundCalculated(long tenantId, long inboundId, String chargeMode,
public int markInboundCalculated(long tenantId, long inboundId, String processStatus, String chargeMode,
java.math.BigDecimal chargeable) {
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId)
.eq(InboundPricingEntity::getId, inboundId)
.in(InboundPricingEntity::getProcessStatus, List.of("WAIT_PRICING", "FEE_CONFIRMING"))
.eq(InboundPricingEntity::getProcessStatus, processStatus)
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED"))
.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING")
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
if (!"WAIT_SPLIT".equals(processStatus)) {
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
}
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
else update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeable);
return inbounds.update(null, update);
@@ -214,7 +229,7 @@ public class FbaPricingRepository {
public int markInboundConfirmed(long tenantId, long inboundId) {
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
.eq(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING")
.in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT"))
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
}
@@ -59,10 +59,9 @@ public class FbaMasterDataService {
}
@Transactional
public CarrierFbaRateEntity createRateVersion(long tenantId, long operatorId,
CarrierRateVersionRequest request) {
public CarrierFbaRateEntity createRateVersion(long operatorId, CarrierRateVersionRequest request) {
validatePeriod(request.effectiveFrom(), request.effectiveTo());
boolean overlaps = repository.listRates(tenantId, null).stream()
boolean overlaps = repository.listRatesForUpdate(null).stream()
.filter(v -> "ACTIVE".equals(v.getStatus()))
.filter(v -> Objects.equals(v.getOriginWarehouseId(), request.originWarehouseId()))
.filter(v -> Objects.equals(v.getBusinessType(), request.businessType()))
@@ -75,9 +74,9 @@ public class FbaMasterDataService {
if (overlaps) throw conflict("船司 FBA 价格有效期重叠,无法保证唯一匹配");
CarrierFbaRateEntity value = new CarrierFbaRateEntity();
value.setTenantId(tenantId);
value.setTenantId(CarrierFbaRateEntity.PLATFORM_SCOPE);
value.setRateCode(request.rateCode());
value.setRateVersion(repository.nextRateVersion(tenantId, request.rateCode()));
value.setRateVersion(repository.nextRateVersion(request.rateCode()));
value.setOriginWarehouseId(request.originWarehouseId());
value.setOriginWarehouseName(request.originWarehouseName());
value.setBusinessType(request.businessType());
@@ -103,8 +102,8 @@ public class FbaMasterDataService {
return repository.listRules(tenantId, code);
}
public List<CarrierFbaRateEntity> listRates(Long tenantId, String code) {
return repository.listRates(tenantId, code);
public List<CarrierFbaRateEntity> listRates(String code) {
return repository.listRates(code);
}
public CustomerChargeRuleEntity rule(long tenantId, long id) {
@@ -113,8 +112,8 @@ public class FbaMasterDataService {
return value;
}
public CarrierFbaRateEntity rate(long tenantId, long id) {
CarrierFbaRateEntity value = repository.findRate(tenantId, id);
public CarrierFbaRateEntity rate(long id) {
CarrierFbaRateEntity value = repository.findRate(id);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "船司 FBA 价格版本不存在");
return value;
}
@@ -125,8 +124,8 @@ public class FbaMasterDataService {
}
@Transactional
public void retireRate(long tenantId, long id, long operatorId) {
if (repository.retireRate(tenantId, id, operatorId) != 1) throw conflict("价格已停用或不存在");
public void retireRate(long id, long operatorId) {
if (repository.retireRate(id, operatorId) != 1) throw conflict("价格已停用或不存在");
}
private static void validatePeriod(LocalDateTime from, LocalDateTime to) {
@@ -47,7 +47,7 @@ public class FbaPricingService {
String hash = hash("CALCULATE", inboundId, requestedAt == null ? "CURRENT" : requestedAt.toString());
InboundPricingIdempotencyEntity token = beginIdempotency(
tenantId, operatorId, inboundId, "FEE_CALCULATE", idempotencyKey, hash);
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), "FEE_CONFIRMING");
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
List<CustomerChargeRuleEntity> matchedRules = repository.matchRules(inbound, pricingAt);
@@ -88,11 +88,14 @@ public class FbaPricingService {
repository.insertFee(fee);
repository.insertItem(baseItem(tenantId, fee, rate, chargeable, subtotal, now));
if (additional.signum() > 0) repository.insertItem(additionalItem(tenantId, fee, additional, now));
if (repository.markInboundCalculated(tenantId, inboundId, rule.getChargeMode(), chargeable) != 1) {
if (repository.markInboundCalculated(tenantId, inboundId, inbound.getProcessStatus(),
rule.getChargeMode(), chargeable) != 1) {
throw conflict("入库单状态已变化,费用试算未保存");
}
complete(token, fee.getId(), now);
return response(inbound, fee, rate.getUnitPrice(), "FEE_CONFIRMING");
String nextStatus = "WAIT_SPLIT".equals(inbound.getProcessStatus())
? "WAIT_SPLIT" : "FEE_CONFIRMING";
return response(inbound, fee, rate.getUnitPrice(), nextStatus);
}
@Transactional
@@ -103,14 +106,14 @@ public class FbaPricingService {
String hash = hash("CONFIRM", inboundId, current.getId().toString());
InboundPricingIdempotencyEntity token = beginIdempotency(
tenantId, operatorId, inboundId, "FEE_CONFIRM", idempotencyKey, hash);
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), "FEE_CONFIRMING");
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
LocalDateTime now = LocalDateTime.now(clock);
if (repository.confirmFee(tenantId, current.getId(), operatorId, now) != 1
|| repository.markInboundConfirmed(tenantId, inboundId) != 1) {
throw conflict("费用或入库单状态已变化,不能重复确认");
}
complete(token, current.getId(), now);
return response(tenantId, current.getId(), "FEE_CONFIRMING");
return response(tenantId, current.getId(), null);
}
@Transactional
@@ -232,7 +235,7 @@ public class FbaPricingService {
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING").contains(inbound.getProcessStatus())
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(inbound.getProcessStatus())
|| !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
throw conflict("当前入库单状态不允许费用试算");
}
@@ -38,7 +38,7 @@ public class InboundOrderCreationService {
inbound.setTenantId(command.tenantId());
inbound.setOrderId(command.orderId());
inbound.setOrderNo(command.orderNo());
inbound.setInboundCode("RK-" + command.orderId());
inbound.setInboundCode(command.inboundCode());
inbound.setProcessStatus("WAIT_MEASURE");
inbound.setOrderStatusSnapshot("APPROVED");
inbound.setMeasureReviewStatus("NOT_SUBMITTED");