一件代发拣货
This commit is contained in:
+16
@@ -4,6 +4,7 @@ import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.GroupResponse;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.DefaultCalculationModeUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.UpdateRequest;
|
||||
import com.baiyue.firstleg.service.CustomerDimensionConfigService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
@@ -47,4 +48,19 @@ public class PlatformCustomerDimensionConfigController {
|
||||
long scope = FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.update(scope, user.userId(), request));
|
||||
}
|
||||
|
||||
@PutMapping("/default-calculation-mode")
|
||||
@PreAuthorize("hasAuthority('firstleg:dimension-config:edit')")
|
||||
@BusinessAudit(event = "CUSTOMER_DIMENSION_DEFAULT_MODE_UPDATE",
|
||||
resourceType = "DIMENSION_CONFIG", resourceId = "#tenantId",
|
||||
description = "平台设置客户默认箱规计算方式")
|
||||
@Operation(summary = "设置客户默认箱规计算方式")
|
||||
public ApiResponse<GroupResponse> updateDefaultCalculationMode(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@Valid @RequestBody DefaultCalculationModeUpdateRequest request) {
|
||||
long scope = FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.updateDefaultCalculationMode(
|
||||
scope, user.userId(), request.defaultCalculationMode()));
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@@ -41,8 +42,9 @@ public class PlatformFbaMasterDataController {
|
||||
public ApiResponse<List<CustomerChargeRuleEntity>> rules(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) Long tenantId,
|
||||
@RequestParam(required = false) Long customerId,
|
||||
@RequestParam(required = false) String ruleCode) {
|
||||
return ApiResponse.success(service.listRules(optionalPlatformTenant(user, tenantId), ruleCode));
|
||||
return ApiResponse.success(service.listRules(optionalPlatformTenant(user, tenantId), customerId, ruleCode));
|
||||
}
|
||||
|
||||
@GetMapping("/customer-rules/{id}")
|
||||
@@ -66,13 +68,24 @@ public class PlatformFbaMasterDataController {
|
||||
@DeleteMapping("/customer-rules/{id}")
|
||||
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rule:edit')")
|
||||
@BusinessAudit(event = "FBA_RULE_RETIRE", resourceType = "CUSTOMER_CHARGE_RULE", resourceId = "#id",
|
||||
description = "停用客户FBA计费规则版本")
|
||||
description = "删除客户系数配置")
|
||||
public ApiResponse<Void> retireRule(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId, @PathVariable long id) {
|
||||
service.retireRule(FirstlegScopeGuard.platform(user, tenantId), id, user.userId());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PutMapping("/customer-rules/{id}/default")
|
||||
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rule:edit')")
|
||||
@BusinessAudit(event = "FBA_RULE_DEFAULT_SET", resourceType = "CUSTOMER_CHARGE_RULE", resourceId = "#id",
|
||||
description = "设置客户默认FBA计费规则")
|
||||
public ApiResponse<Void> setDefaultRule(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId, @PathVariable long id) {
|
||||
long scope = FirstlegScopeGuard.platform(user, tenantId);
|
||||
service.setDefaultRule(scope, id, user.userId());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/carrier-rates")
|
||||
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rate:view')")
|
||||
@Operation(summary = "查询船司 FBA 价格版本", description = "查询平台公共价格;所有客户共享")
|
||||
|
||||
+7
-3
@@ -43,11 +43,14 @@ public class PlatformFbaPricingController {
|
||||
@Valid @RequestBody(required = false) CalculateRequest request) {
|
||||
long scope = FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.calculate(scope, user.userId(), inboundId,
|
||||
request == null ? null : request.pricingAt(), idempotencyKey));
|
||||
request == null ? null : request.pricingAt(),
|
||||
request == null ? null : request.selectedWeightKg(),
|
||||
request == null ? null : request.selectedVolumeCbm(), idempotencyKey));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm")
|
||||
@PreAuthorize("hasAuthority('firstleg:fba-pricing:confirm')")
|
||||
@PreAuthorize("hasAnyAuthority('firstleg:inbound:confirm-send-fee',"
|
||||
+ " 'firstleg:fba-pricing:confirm')")
|
||||
@BusinessAudit(event = "FBA_FEE_CONFIRM", resourceType = "INBOUND", resourceId = "#inboundId",
|
||||
description = "确认FBA费用快照")
|
||||
public ApiResponse<FeeSnapshotResponse> confirm(
|
||||
@@ -58,7 +61,8 @@ public class PlatformFbaPricingController {
|
||||
}
|
||||
|
||||
@PostMapping("/send")
|
||||
@PreAuthorize("hasAuthority('firstleg:inbound:send-fee')")
|
||||
@PreAuthorize("hasAnyAuthority('firstleg:inbound:confirm-send-fee',"
|
||||
+ " 'firstleg:inbound:send-fee')")
|
||||
@BusinessAudit(event = "FBA_FEE_SEND", resourceType = "INBOUND", resourceId = "#inboundId",
|
||||
description = "发送FBA费用快照并进入待托盘状态")
|
||||
public ApiResponse<FeeSnapshotResponse> send(
|
||||
|
||||
+2
-1
@@ -80,7 +80,8 @@ public class PlatformInboundMeasurementController {
|
||||
resourceId = "#inboundId", description = "保存入库测量草稿")
|
||||
@Operation(summary = "保存测量草稿", description = "保存人工测量批次和服务端计算明细,不推进入库状态。")
|
||||
@PutMapping("/measurement/draft")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:measure:edit')")
|
||||
@PreAuthorize("hasAnyAuthority('platform:firstleg:inbound:warehouse-suggestion:edit',"
|
||||
+ " 'platform:firstleg:inbound:measure:edit')")
|
||||
public ApiResponse<InboundMeasurementResponses.DraftSaved> saveDraft(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
|
||||
@@ -8,6 +8,7 @@ import jakarta.validation.constraints.Digits;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -21,6 +22,10 @@ public final class CustomerDimensionConfigDtos {
|
||||
@NotNull @Size(min = 4, max = 4) List<@Valid RuleUpdateRequest> rules
|
||||
) { }
|
||||
|
||||
public record DefaultCalculationModeUpdateRequest(
|
||||
@NotBlank @Pattern(regexp = "ADD|MULTIPLY") String defaultCalculationMode
|
||||
) { }
|
||||
|
||||
public record RuleUpdateRequest(
|
||||
@NotNull CustomerDimensionBoxType boxType,
|
||||
@NotNull @DecimalMin("0.001") @Digits(integer = 6, fraction = 3) BigDecimal innerLengthCm,
|
||||
@@ -32,7 +37,7 @@ public final class CustomerDimensionConfigDtos {
|
||||
@NotNull @Min(0) Integer version
|
||||
) { }
|
||||
|
||||
public record GroupResponse(Long tenantId, List<RuleResponse> rules) { }
|
||||
public record GroupResponse(Long tenantId, String defaultCalculationMode, List<RuleResponse> rules) { }
|
||||
|
||||
public record RuleResponse(
|
||||
Long id,
|
||||
|
||||
@@ -29,8 +29,21 @@ public final class FbaPricingDtos {
|
||||
@NotBlank @Pattern(regexp = "[A-Z]{3}") String currency,
|
||||
@NotNull LocalDateTime effectiveFrom,
|
||||
LocalDateTime effectiveTo,
|
||||
@Size(max = 500) String remark
|
||||
) {}
|
||||
@Size(max = 500) String remark,
|
||||
@DecimalMin(value = "0", inclusive = false) BigDecimal lengthFactor,
|
||||
@DecimalMin(value = "0", inclusive = false) BigDecimal widthFactor,
|
||||
@DecimalMin(value = "0", inclusive = false) BigDecimal heightFactor,
|
||||
Boolean defaultRule
|
||||
) {
|
||||
public CustomerRuleVersionRequest(String ruleCode, Long customerId, String customerName,
|
||||
String businessType, String chargeMode, String taxMode,
|
||||
BigDecimal conversionFactor, Integer roundingScale,
|
||||
String roundingMode, String currency, LocalDateTime effectiveFrom,
|
||||
LocalDateTime effectiveTo, String remark) {
|
||||
this(ruleCode, customerId, customerName, businessType, chargeMode, taxMode, conversionFactor,
|
||||
roundingScale, roundingMode, currency, effectiveFrom, effectiveTo, remark, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record CarrierRateVersionRequest(
|
||||
@NotBlank @Size(max = 50) String rateCode,
|
||||
@@ -50,7 +63,17 @@ public final class FbaPricingDtos {
|
||||
@Size(max = 500) String remark
|
||||
) {}
|
||||
|
||||
public record CalculateRequest(LocalDateTime pricingAt) {}
|
||||
public record CalculateRequest(
|
||||
LocalDateTime pricingAt,
|
||||
@DecimalMin(value = "0", inclusive = false) BigDecimal selectedWeightKg,
|
||||
@DecimalMin(value = "0", inclusive = false) BigDecimal selectedVolumeCbm,
|
||||
@Size(max = 32) String weightSource,
|
||||
@Size(max = 32) String volumeSource
|
||||
) {
|
||||
public CalculateRequest(LocalDateTime pricingAt) {
|
||||
this(pricingAt, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record AdjustFeeRequest(
|
||||
@NotNull @Positive Integer expectedFeeVersion,
|
||||
|
||||
@@ -55,8 +55,29 @@ public final class InboundMeasurementRequests {
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerLengthCm,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerWidthCm,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerHeightCm,
|
||||
@Positive Long chargeRuleId,
|
||||
@jakarta.validation.constraints.Pattern(regexp = "ADD|MULTIPLY") String coefficientMode,
|
||||
@Size(max = 500) String remark
|
||||
) {
|
||||
public ItemRequest(Long inboundDetailId, String itemNo, String boxSpecification, Integer packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
Long intelligentImageFileId, String overLimitWarning,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
Long chargeRuleId, String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
chargeRuleId, null, remark);
|
||||
}
|
||||
|
||||
public ItemRequest(Long inboundDetailId, String itemNo, String boxSpecification, Integer packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
Long intelligentImageFileId, String overLimitWarning,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, null, remark);
|
||||
}
|
||||
}
|
||||
|
||||
public record SubmitRequest(
|
||||
|
||||
@@ -90,6 +90,8 @@ public final class InboundMeasurementResponses {
|
||||
BigDecimal outerLengthCm,
|
||||
BigDecimal outerWidthCm,
|
||||
BigDecimal outerHeightCm,
|
||||
Long chargeRuleId,
|
||||
String coefficientMode,
|
||||
String remark,
|
||||
String boxCode,
|
||||
String scannedShipmentId,
|
||||
|
||||
@@ -40,7 +40,28 @@ public record MeasurementBatchCommand(
|
||||
BigDecimal outerLengthCm,
|
||||
BigDecimal outerWidthCm,
|
||||
BigDecimal outerHeightCm,
|
||||
Long chargeRuleId,
|
||||
String coefficientMode,
|
||||
String remark
|
||||
) {
|
||||
public Item(Long inboundDetailId, String itemNo, String boxSpecification, int packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
Long intelligentImageFileId, String overLimitWarning,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
Long chargeRuleId, String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
chargeRuleId, null, remark);
|
||||
}
|
||||
|
||||
public Item(Long inboundDetailId, String itemNo, String boxSpecification, int packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
Long intelligentImageFileId, String overLimitWarning,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, null, remark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ public class CustomerChargeRuleEntity extends BaseEntity {
|
||||
private String chargeMode;
|
||||
private String taxMode;
|
||||
private BigDecimal conversionFactor;
|
||||
/** 体积计算各方向系数;历史数据为空时按 1 处理。 */
|
||||
private BigDecimal lengthFactor;
|
||||
private BigDecimal widthFactor;
|
||||
private BigDecimal heightFactor;
|
||||
private Boolean defaultRule;
|
||||
private Integer roundingScale;
|
||||
private String roundingMode;
|
||||
private String currency;
|
||||
|
||||
@@ -23,6 +23,7 @@ public class CustomerDimensionConfigEntity extends BaseEntity {
|
||||
private BigDecimal innerWidthCm;
|
||||
private BigDecimal innerHeightCm;
|
||||
private BigDecimal edgeAdditionCm;
|
||||
private String defaultCalculationMode;
|
||||
private String remark;
|
||||
private String initializationKey;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ public class InboundMeasurementItemEntity {
|
||||
private BigDecimal volumeCbm;
|
||||
private BigDecimal totalVolumeCbm;
|
||||
private BigDecimal volumeWeightKg;
|
||||
private Long chargeRuleId;
|
||||
private String coefficientMode;
|
||||
private String calculationRuleVersion;
|
||||
private Long sourceImageFileId;
|
||||
private String machineNo;
|
||||
|
||||
+14
-3
@@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -17,12 +18,12 @@ public interface CustomerDimensionConfigMapper extends BaseMapper<CustomerDimens
|
||||
<script>
|
||||
INSERT IGNORE INTO fl_customer_dimension_config
|
||||
(id, tenant_id, box_type, inner_length_cm, inner_width_cm, inner_height_cm,
|
||||
edge_addition_cm, remark, initialization_key, created_by, created_at,
|
||||
edge_addition_cm, default_calculation_mode, remark, initialization_key, created_by, created_at,
|
||||
updated_by, updated_at, deleted, version)
|
||||
VALUES
|
||||
<foreach collection="rows" item="row" separator=",">
|
||||
(#{row.id}, #{row.tenantId}, #{row.boxType}, #{row.innerLengthCm}, #{row.innerWidthCm},
|
||||
#{row.innerHeightCm}, #{row.edgeAdditionCm}, #{row.remark}, #{row.initializationKey},
|
||||
#{row.innerHeightCm}, #{row.edgeAdditionCm}, #{row.defaultCalculationMode}, #{row.remark}, #{row.initializationKey},
|
||||
#{operatorId}, CURRENT_TIMESTAMP(3), #{operatorId}, CURRENT_TIMESTAMP(3), 0, 0)
|
||||
</foreach>
|
||||
</script>
|
||||
@@ -32,7 +33,7 @@ public interface CustomerDimensionConfigMapper extends BaseMapper<CustomerDimens
|
||||
|
||||
@Select("""
|
||||
SELECT id, tenant_id, box_type, inner_length_cm, inner_width_cm, inner_height_cm,
|
||||
edge_addition_cm, remark, initialization_key, created_by, created_at,
|
||||
edge_addition_cm, default_calculation_mode, remark, initialization_key, created_by, created_at,
|
||||
updated_by, updated_at, deleted, version
|
||||
FROM fl_customer_dimension_config
|
||||
WHERE tenant_id = #{tenantId}
|
||||
@@ -43,6 +44,16 @@ public interface CustomerDimensionConfigMapper extends BaseMapper<CustomerDimens
|
||||
List<CustomerDimensionConfigEntity> selectDefaultsForCompensation(
|
||||
@Param("tenantId") long tenantId, @Param("initializationKey") String initializationKey);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_customer_dimension_config
|
||||
SET default_calculation_mode = #{mode}, updated_by = #{operatorId},
|
||||
updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE tenant_id = #{tenantId} AND deleted = 0
|
||||
""")
|
||||
int updateDefaultCalculationMode(@Param("tenantId") long tenantId,
|
||||
@Param("mode") String mode,
|
||||
@Param("operatorId") long operatorId);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM fl_customer_dimension_config
|
||||
WHERE tenant_id = #{tenantId}
|
||||
|
||||
+4
@@ -26,6 +26,10 @@ public class CustomerDimensionConfigRepository {
|
||||
return mapper.updateById(value);
|
||||
}
|
||||
|
||||
public int updateDefaultCalculationMode(long tenantId, String mode, long operatorId) {
|
||||
return mapper.updateDefaultCalculationMode(tenantId, mode, operatorId);
|
||||
}
|
||||
|
||||
public List<CustomerDimensionConfigEntity> findDefaultsForCompensation(long tenantId,
|
||||
String initializationKey) {
|
||||
return mapper.selectDefaultsForCompensation(tenantId, initializationKey);
|
||||
|
||||
@@ -42,6 +42,24 @@ public class FbaPricingRepository {
|
||||
private InboundOutboxMapper outbox;
|
||||
|
||||
public void insertRule(CustomerChargeRuleEntity rule) { rules.insert(rule); }
|
||||
|
||||
public int clearDefaultRule(long tenantId, long customerId, long operatorId) {
|
||||
return rules.update(null, new LambdaUpdateWrapper<CustomerChargeRuleEntity>()
|
||||
.eq(CustomerChargeRuleEntity::getTenantId, tenantId)
|
||||
.eq(CustomerChargeRuleEntity::getCustomerId, customerId)
|
||||
.eq(CustomerChargeRuleEntity::getDefaultRule, true)
|
||||
.set(CustomerChargeRuleEntity::getDefaultRule, false)
|
||||
.set(CustomerChargeRuleEntity::getUpdatedBy, operatorId));
|
||||
}
|
||||
|
||||
public int setDefaultRule(long tenantId, long id, long operatorId) {
|
||||
return rules.update(null, new LambdaUpdateWrapper<CustomerChargeRuleEntity>()
|
||||
.eq(CustomerChargeRuleEntity::getTenantId, tenantId)
|
||||
.eq(CustomerChargeRuleEntity::getId, id)
|
||||
.eq(CustomerChargeRuleEntity::getStatus, "ACTIVE")
|
||||
.set(CustomerChargeRuleEntity::getDefaultRule, true)
|
||||
.set(CustomerChargeRuleEntity::getUpdatedBy, operatorId));
|
||||
}
|
||||
public void insertRate(CarrierFbaRateEntity rate) { rates.insert(rate); }
|
||||
|
||||
public CustomerChargeRuleEntity findRule(long tenantId, long id) {
|
||||
@@ -50,6 +68,15 @@ public class FbaPricingRepository {
|
||||
.eq(CustomerChargeRuleEntity::getId, id));
|
||||
}
|
||||
|
||||
public CustomerChargeRuleEntity findDefaultRule(long tenantId, long customerId) {
|
||||
return rules.selectOne(new LambdaQueryWrapper<CustomerChargeRuleEntity>()
|
||||
.eq(CustomerChargeRuleEntity::getTenantId, tenantId)
|
||||
.eq(CustomerChargeRuleEntity::getCustomerId, customerId)
|
||||
.eq(CustomerChargeRuleEntity::getStatus, "ACTIVE")
|
||||
.eq(CustomerChargeRuleEntity::getDefaultRule, true)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
public CarrierFbaRateEntity findRate(long id) {
|
||||
return rates.selectOne(new LambdaQueryWrapper<CarrierFbaRateEntity>()
|
||||
.eq(CarrierFbaRateEntity::getTenantId, CarrierFbaRateEntity.PLATFORM_SCOPE)
|
||||
@@ -57,10 +84,16 @@ public class FbaPricingRepository {
|
||||
}
|
||||
|
||||
public List<CustomerChargeRuleEntity> listRules(Long tenantId, String ruleCode) {
|
||||
return listRules(tenantId, null, ruleCode);
|
||||
}
|
||||
|
||||
public List<CustomerChargeRuleEntity> listRules(Long tenantId, Long customerId, String ruleCode) {
|
||||
return rules.selectList(new LambdaQueryWrapper<CustomerChargeRuleEntity>()
|
||||
.eq(tenantId != null, CustomerChargeRuleEntity::getTenantId, tenantId)
|
||||
.eq(customerId != null, CustomerChargeRuleEntity::getCustomerId, customerId)
|
||||
.eq(ruleCode != null && !ruleCode.isBlank(), CustomerChargeRuleEntity::getRuleCode, ruleCode)
|
||||
.orderByAsc(CustomerChargeRuleEntity::getRuleCode)
|
||||
.orderByDesc(CustomerChargeRuleEntity::getDefaultRule)
|
||||
.orderByDesc(CustomerChargeRuleEntity::getRuleVersion));
|
||||
}
|
||||
|
||||
@@ -101,6 +134,7 @@ public class FbaPricingRepository {
|
||||
.eq(CustomerChargeRuleEntity::getId, id)
|
||||
.eq(CustomerChargeRuleEntity::getStatus, "ACTIVE")
|
||||
.set(CustomerChargeRuleEntity::getStatus, "INACTIVE")
|
||||
.set(CustomerChargeRuleEntity::getDefaultRule, false)
|
||||
.set(CustomerChargeRuleEntity::getUpdatedBy, operatorId));
|
||||
}
|
||||
|
||||
|
||||
+29
-1
@@ -26,6 +26,7 @@ import java.util.Map;
|
||||
@Service
|
||||
public class CustomerDimensionConfigService {
|
||||
public static final BigDecimal DEFAULT_EDGE_ADDITION_CM = new BigDecimal("2.000");
|
||||
public static final String DEFAULT_CALCULATION_MODE = "ADD";
|
||||
private static final BigDecimal TWO = new BigDecimal("2");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
|
||||
@@ -65,6 +66,17 @@ public class CustomerDimensionConfigService {
|
||||
return response(tenantId, existing);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GroupResponse updateDefaultCalculationMode(long tenantId, long operatorId, String mode) {
|
||||
String normalizedMode = normalizeCalculationMode(mode);
|
||||
requireCompleteRules(repository.findAll(tenantId));
|
||||
if (repository.updateDefaultCalculationMode(tenantId, normalizedMode, operatorId)
|
||||
!= CustomerDimensionBoxType.values().length) {
|
||||
throw staleConfiguration();
|
||||
}
|
||||
return response(tenantId, requireCompleteRules(repository.findAll(tenantId)));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InitializeResponse initializeDefault(long tenantId, long operatorId, String initializationKey) {
|
||||
requireTenant(tenantId);
|
||||
@@ -159,6 +171,7 @@ public class CustomerDimensionConfigService {
|
||||
value.setInnerWidthCm(type.defaultWidthCm());
|
||||
value.setInnerHeightCm(type.defaultHeightCm());
|
||||
value.setEdgeAdditionCm(DEFAULT_EDGE_ADDITION_CM);
|
||||
value.setDefaultCalculationMode(DEFAULT_CALCULATION_MODE);
|
||||
value.setRemark(type.defaultRemark());
|
||||
value.setInitializationKey(initializationKey);
|
||||
rows.add(value);
|
||||
@@ -181,6 +194,7 @@ public class CustomerDimensionConfigService {
|
||||
|| value.getVersion() == null || value.getVersion() != 0
|
||||
|| value.getInnerLengthCm() == null || value.getInnerWidthCm() == null
|
||||
|| value.getInnerHeightCm() == null || value.getEdgeAdditionCm() == null
|
||||
|| !DEFAULT_CALCULATION_MODE.equals(normalizeCalculationMode(value.getDefaultCalculationMode()))
|
||||
|| value.getInnerLengthCm().compareTo(type.defaultLengthCm()) != 0
|
||||
|| value.getInnerWidthCm().compareTo(type.defaultWidthCm()) != 0
|
||||
|| value.getInnerHeightCm().compareTo(type.defaultHeightCm()) != 0
|
||||
@@ -200,7 +214,12 @@ public class CustomerDimensionConfigService {
|
||||
}
|
||||
|
||||
private static GroupResponse response(long tenantId, List<CustomerDimensionConfigEntity> values) {
|
||||
return new GroupResponse(tenantId, values.stream().map(CustomerDimensionConfigService::ruleResponse).toList());
|
||||
String mode = normalizeCalculationMode(values.get(0).getDefaultCalculationMode());
|
||||
if (values.stream().anyMatch(value -> !mode.equals(normalizeCalculationMode(value.getDefaultCalculationMode())))) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户默认箱规计算方式配置不一致");
|
||||
}
|
||||
return new GroupResponse(tenantId, mode,
|
||||
values.stream().map(CustomerDimensionConfigService::ruleResponse).toList());
|
||||
}
|
||||
|
||||
private static RuleResponse ruleResponse(CustomerDimensionConfigEntity value) {
|
||||
@@ -232,6 +251,15 @@ public class CustomerDimensionConfigService {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static String normalizeCalculationMode(String value) {
|
||||
if (value == null || value.isBlank()) return DEFAULT_CALCULATION_MODE;
|
||||
String normalized = value.trim().toUpperCase();
|
||||
if (!"ADD".equals(normalized) && !"MULTIPLY".equals(normalized)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "默认箱规计算方式无效");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static void requireTenant(long tenantId) {
|
||||
if (tenantId <= 0) throw new BizException(ResultCode.BAD_REQUEST, "客户ID无效");
|
||||
}
|
||||
|
||||
+3
-2
@@ -69,7 +69,7 @@ public class CustomerDimensionWeightingService {
|
||||
BigDecimal heightCm) {
|
||||
BigDecimal rawVolume = lengthCm.multiply(widthCm).multiply(heightCm)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
return new WeightedDimensions(null, null, lengthCm, widthCm, heightCm,
|
||||
return new WeightedDimensions(null, null, null, lengthCm, widthCm, heightCm,
|
||||
rawVolume, rawVolume.setScale(3, RoundingMode.HALF_UP),
|
||||
"ACTUAL_DIMENSION_V1");
|
||||
}
|
||||
@@ -78,6 +78,7 @@ public class CustomerDimensionWeightingService {
|
||||
public record WeightedDimensions(
|
||||
CustomerDimensionBoxType boxType,
|
||||
Integer ruleVersion,
|
||||
BigDecimal edgeAdditionCm,
|
||||
BigDecimal outerLengthCm,
|
||||
BigDecimal outerWidthCm,
|
||||
BigDecimal outerHeightCm,
|
||||
@@ -119,7 +120,7 @@ public class CustomerDimensionWeightingService {
|
||||
BigDecimal rawVolume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = rawVolume.setScale(3, RoundingMode.HALF_UP);
|
||||
return new WeightedDimensions(boxType, version, outerLength, outerWidth, outerHeight,
|
||||
return new WeightedDimensions(boxType, version, edgeAdditionCm, outerLength, outerWidth, outerHeight,
|
||||
rawVolume, unitVolume, "CUSTOMER_DIMENSION_" + boxType.name() + "_V" + version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +26,6 @@ public class FbaMasterDataService {
|
||||
public CustomerChargeRuleEntity createRuleVersion(long tenantId, long operatorId,
|
||||
CustomerRuleVersionRequest request) {
|
||||
validatePeriod(request.effectiveFrom(), request.effectiveTo());
|
||||
boolean overlaps = repository.listRules(tenantId, null).stream()
|
||||
.filter(v -> "ACTIVE".equals(v.getStatus()))
|
||||
.filter(v -> Objects.equals(v.getCustomerId(), request.customerId()))
|
||||
.filter(v -> Objects.equals(v.getBusinessType(), request.businessType()))
|
||||
.filter(v -> Objects.equals(v.getTaxMode(), request.taxMode()))
|
||||
.anyMatch(v -> overlaps(v.getEffectiveFrom(), v.getEffectiveTo(),
|
||||
request.effectiveFrom(), request.effectiveTo()));
|
||||
if (overlaps) throw conflict("客户计费规则有效期重叠,无法保证唯一匹配");
|
||||
|
||||
CustomerChargeRuleEntity value = new CustomerChargeRuleEntity();
|
||||
value.setTenantId(tenantId);
|
||||
value.setRuleCode(request.ruleCode());
|
||||
@@ -45,6 +36,10 @@ public class FbaMasterDataService {
|
||||
value.setChargeMode(request.chargeMode());
|
||||
value.setTaxMode(request.taxMode());
|
||||
value.setConversionFactor(request.conversionFactor());
|
||||
value.setLengthFactor(defaultFactor(request.lengthFactor()));
|
||||
value.setWidthFactor(defaultFactor(request.widthFactor()));
|
||||
value.setHeightFactor(defaultFactor(request.heightFactor()));
|
||||
value.setDefaultRule(false);
|
||||
value.setRoundingScale(request.roundingScale());
|
||||
value.setRoundingMode(request.roundingMode());
|
||||
value.setCurrency(request.currency());
|
||||
@@ -58,6 +53,15 @@ public class FbaMasterDataService {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setDefaultRule(long tenantId, long id, long operatorId) {
|
||||
CustomerChargeRuleEntity value = rule(tenantId, id);
|
||||
repository.clearDefaultRule(tenantId, value.getCustomerId(), operatorId);
|
||||
if (repository.setDefaultRule(tenantId, id, operatorId) != 1) {
|
||||
throw conflict("规则已停用或不存在");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CarrierFbaRateEntity createRateVersion(long operatorId, CarrierRateVersionRequest request) {
|
||||
validatePeriod(request.effectiveFrom(), request.effectiveTo());
|
||||
@@ -102,6 +106,10 @@ public class FbaMasterDataService {
|
||||
return repository.listRules(tenantId, code);
|
||||
}
|
||||
|
||||
public List<CustomerChargeRuleEntity> listRules(Long tenantId, Long customerId, String code) {
|
||||
return repository.listRules(tenantId, customerId, code);
|
||||
}
|
||||
|
||||
public List<CarrierFbaRateEntity> listRates(String code) {
|
||||
return repository.listRates(code);
|
||||
}
|
||||
@@ -142,4 +150,8 @@ public class FbaMasterDataService {
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
|
||||
private static java.math.BigDecimal defaultFactor(java.math.BigDecimal value) {
|
||||
return value == null ? java.math.BigDecimal.ONE : value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,17 @@ public class FbaPricingService {
|
||||
@Transactional
|
||||
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
||||
LocalDateTime requestedAt, String idempotencyKey) {
|
||||
String hash = hash("CALCULATE", inboundId, requestedAt == null ? "CURRENT" : requestedAt.toString());
|
||||
return calculate(tenantId, operatorId, inboundId, requestedAt, null, null, idempotencyKey);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
||||
LocalDateTime requestedAt, BigDecimal selectedWeightKg,
|
||||
BigDecimal selectedVolumeCbm, String idempotencyKey) {
|
||||
String discriminator = (requestedAt == null ? "CURRENT" : requestedAt.toString()) + ":"
|
||||
+ (selectedWeightKg == null ? "ACTUAL_WEIGHT" : selectedWeightKg) + ":"
|
||||
+ (selectedVolumeCbm == null ? "ACTUAL_VOLUME" : selectedVolumeCbm);
|
||||
String hash = hash("CALCULATE", inboundId, discriminator);
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(
|
||||
tenantId, operatorId, inboundId, "FEE_CALCULATE", idempotencyKey, hash);
|
||||
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
|
||||
@@ -58,10 +68,11 @@ public class FbaPricingService {
|
||||
ActivePriceBookEstimator.Estimate estimate = priceBookEstimator.estimate(new EstimateRequest(
|
||||
inbound.getWarehouseId(), inbound.getChannelCode(), inbound.getDestinationCode(),
|
||||
destinationType(inbound), inbound.getDestinationWarehouseId(), inbound.getDestinationName(), null,
|
||||
pricingTaxMode(inbound), inbound.getActualNetWeightKg(), inbound.getActualVolumeCbm()));
|
||||
pricingTaxMode(inbound), selectedWeightKg == null ? inbound.getActualNetWeightKg() : selectedWeightKg,
|
||||
selectedVolumeCbm == null ? inbound.getActualVolumeCbm() : selectedVolumeCbm));
|
||||
RateRow rate = estimate.rate();
|
||||
BigDecimal weight = inbound.getActualNetWeightKg().setScale(3, RoundingMode.HALF_UP);
|
||||
BigDecimal volume = inbound.getActualVolumeCbm().setScale(4, RoundingMode.HALF_UP);
|
||||
BigDecimal weight = (selectedWeightKg == null ? inbound.getActualNetWeightKg() : selectedWeightKg).setScale(3, RoundingMode.HALF_UP);
|
||||
BigDecimal volume = (selectedVolumeCbm == null ? inbound.getActualVolumeCbm() : selectedVolumeCbm).setScale(4, RoundingMode.HALF_UP);
|
||||
BigDecimal chargeable = estimate.chargeableQuantity();
|
||||
BigDecimal converted = chargeable;
|
||||
BigDecimal subtotal = estimate.amount().setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
|
||||
@@ -54,6 +54,8 @@ public class InboundMeasurementService {
|
||||
@Resource
|
||||
private MeasurementBatchService batchService;
|
||||
@Resource
|
||||
private FbaMasterDataService fbaMasterDataService;
|
||||
@Resource
|
||||
private OrderCargoClient orderCargoClient;
|
||||
@Value("${baiyue.security.internal-service-token:}")
|
||||
private String internalToken;
|
||||
@@ -91,6 +93,13 @@ public class InboundMeasurementService {
|
||||
InboundMeasurementRequests.SaveDraftRequest request) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
validateInboundDetails(tenantId, inbound, request.items());
|
||||
request.items().stream().map(InboundMeasurementRequests.ItemRequest::chargeRuleId)
|
||||
.filter(java.util.Objects::nonNull).distinct().forEach(ruleId -> {
|
||||
var rule = fbaMasterDataService.rule(tenantId, ruleId);
|
||||
if (!"ACTIVE".equals(rule.getStatus()) || !java.util.Objects.equals(rule.getCustomerId(), inbound.getCustomerId())) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "测量明细绑定的计费规则不属于当前客户或已停用");
|
||||
}
|
||||
});
|
||||
InboundMeasurementEntity existing = request.measurementId() == null ? null
|
||||
: repository.findMeasurement(tenantId, inboundId, request.measurementId());
|
||||
OrderCargoClient.MeasurementDeclarationSnapshot declaration = loadDeclaration(inbound);
|
||||
@@ -112,7 +121,7 @@ public class InboundMeasurementService {
|
||||
item.inboundDetailId(), item.itemNo(), item.boxSpecification(), item.packageCount(), item.lengthCm(),
|
||||
item.widthCm(), item.heightCm(), item.intelligentImageFileId(),
|
||||
item.overLimitWarning(), item.outerLengthCm(), item.outerWidthCm(), item.outerHeightCm(),
|
||||
item.remark())).toList());
|
||||
item.chargeRuleId(), item.coefficientMode(), item.remark())).toList());
|
||||
InboundMeasurementEntity saved = batchService.saveBatch(command);
|
||||
return new InboundMeasurementResponses.DraftSaved(saved.getId(), saved.getVersion(), saved.getStatus());
|
||||
}
|
||||
@@ -531,7 +540,8 @@ public class InboundMeasurementService {
|
||||
item.getUnitVolumeCbm(),
|
||||
item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(),
|
||||
item.getOverLimitWarning(), item.getOuterLengthCm(), item.getOuterWidthCm(),
|
||||
item.getOuterHeightCm(), item.getRemark(), item.getBoxCode(), item.getScannedShipmentId(),
|
||||
item.getOuterHeightCm(), item.getChargeRuleId(), item.getCoefficientMode(),
|
||||
item.getRemark(), item.getBoxCode(), item.getScannedShipmentId(),
|
||||
item.getPackageCount(),
|
||||
item.getMachineNo(), item.getDeviceMeasuredAt(), item.getUpdatedAt());
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.MeasurementBatchCommand;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -14,9 +16,13 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 后台录入和第三方回传共用的测量批次持久化服务。
|
||||
@@ -37,10 +43,14 @@ public class MeasurementBatchService {
|
||||
static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
|
||||
private static final BigDecimal MAX_RAW_VOLUME_CBM = new BigDecimal("999999999999.999999");
|
||||
private static final BigDecimal MAX_TOTAL_VOLUME_CBM = new BigDecimal("99999999999999.9999");
|
||||
private static final Pattern SPECIFICATION_COEFFICIENT_PATTERN = Pattern.compile(
|
||||
"(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?);系数:(\\d+(?:\\.\\d+)?)");
|
||||
@Resource
|
||||
private InboundMeasurementRepository repository;
|
||||
@Resource
|
||||
private CustomerDimensionWeightingService dimensionWeighting;
|
||||
@Resource
|
||||
private FbaPricingRepository pricingRepository;
|
||||
|
||||
@Transactional
|
||||
public InboundMeasurementEntity saveBatch(MeasurementBatchCommand command) {
|
||||
@@ -50,7 +60,7 @@ public class MeasurementBatchService {
|
||||
}
|
||||
validateSource(command);
|
||||
List<InboundMeasurementItemEntity> calculatedItems = calculateItems(
|
||||
command, dimensionWeighting.load(command.tenantId()));
|
||||
command, dimensionWeighting.load(command.tenantId()), inbound.getCustomerId());
|
||||
|
||||
InboundMeasurementEntity measurement;
|
||||
if (command.measurementId() == null) {
|
||||
@@ -130,7 +140,8 @@ public class MeasurementBatchService {
|
||||
}
|
||||
|
||||
private List<InboundMeasurementItemEntity> calculateItems(
|
||||
MeasurementBatchCommand command, CustomerDimensionWeightingService.RuleSet dimensionRules) {
|
||||
MeasurementBatchCommand command, CustomerDimensionWeightingService.RuleSet dimensionRules,
|
||||
Long customerId) {
|
||||
Set<String> itemNumbers = new HashSet<>();
|
||||
return command.items().stream().map(source -> {
|
||||
if (!itemNumbers.add(source.itemNo())) {
|
||||
@@ -138,8 +149,49 @@ public class MeasurementBatchService {
|
||||
}
|
||||
CustomerDimensionWeightingService.WeightedDimensions weighted = dimensionRules.resolve(
|
||||
source.lengthCm(), source.widthCm(), source.heightCm());
|
||||
BigDecimal unitVolumeRaw = weighted.rawVolumeCbm();
|
||||
BigDecimal unitVolume = weighted.unitVolumeCbm();
|
||||
String requestedMode = normalizeMode(source.coefficientMode());
|
||||
CustomerChargeRuleEntity coefficientRule = "MULTIPLY".equals(requestedMode)
|
||||
? requireCoefficientRule(command.tenantId(), customerId, source.chargeRuleId())
|
||||
: requestedMode == null ? findDefaultCoefficientRule(command.tenantId(), customerId) : null;
|
||||
String coefficientMode = coefficientRule != null ? "MULTIPLY" : requestedMode;
|
||||
boolean useDimensionRule = "ADD".equals(coefficientMode);
|
||||
// The box-dimension strategy bills against the matched carton tier.
|
||||
// This is important when a measured carton is smaller than a standard
|
||||
// carton: use the configured outer dimensions, rather than adding the
|
||||
// edge allowance to each measured side.
|
||||
BigDecimal addition = weighted.edgeAdditionCm();
|
||||
boolean hasMatchedAddition = useDimensionRule && weighted.boxType() != null;
|
||||
BigDecimal outerLength = hasMatchedAddition ? weighted.outerLengthCm() : source.lengthCm();
|
||||
BigDecimal outerWidth = hasMatchedAddition ? weighted.outerWidthCm() : source.widthCm();
|
||||
BigDecimal outerHeight = hasMatchedAddition ? weighted.outerHeightCm() : source.heightCm();
|
||||
boolean hasOuterOverride = source.outerLengthCm() != null
|
||||
|| source.outerWidthCm() != null
|
||||
|| source.outerHeightCm() != null;
|
||||
if (hasOuterOverride && (source.outerLengthCm() == null
|
||||
|| source.outerWidthCm() == null || source.outerHeightCm() == null
|
||||
|| source.outerLengthCm().signum() <= 0 || source.outerWidthCm().signum() <= 0
|
||||
|| source.outerHeightCm().signum() <= 0)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"仓库修改外箱长、宽、高必须同时填写且大于0,明细 " + source.itemNo());
|
||||
}
|
||||
if (hasOuterOverride) {
|
||||
// Warehouse-confirmed outer dimensions are authoritative. Derived
|
||||
// values below (girth, volume and volumetric weight) use them directly.
|
||||
outerLength = source.outerLengthCm();
|
||||
outerWidth = source.outerWidthCm();
|
||||
outerHeight = source.outerHeightCm();
|
||||
} else if (coefficientRule != null) {
|
||||
BigDecimal specificationCoefficient = resolveSpecificationCoefficient(
|
||||
coefficientRule, source.lengthCm(), source.widthCm(), source.heightCm());
|
||||
outerLength = applyCoefficient(outerLength, specificationCoefficient);
|
||||
outerWidth = applyCoefficient(outerWidth, specificationCoefficient);
|
||||
outerHeight = applyCoefficient(outerHeight, specificationCoefficient);
|
||||
}
|
||||
BigDecimal unitVolumeRaw = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = hasMatchedAddition && !hasOuterOverride
|
||||
? unitVolumeRaw.setScale(3, RoundingMode.HALF_UP)
|
||||
: unitVolumeRaw.setScale(4, RoundingMode.CEILING);
|
||||
BigDecimal packageCount = BigDecimal.valueOf(source.packageCount());
|
||||
BigDecimal totalVolumeRaw = unitVolumeRaw.multiply(packageCount).setScale(6, RoundingMode.HALF_UP);
|
||||
BigDecimal totalVolume;
|
||||
@@ -161,25 +213,115 @@ public class MeasurementBatchService {
|
||||
item.setLengthCm(source.lengthCm());
|
||||
item.setWidthCm(source.widthCm());
|
||||
item.setHeightCm(source.heightCm());
|
||||
item.setGirthCm(source.lengthCm().add(source.widthCm().add(source.heightCm())
|
||||
.multiply(BigDecimal.valueOf(2))).setScale(3, RoundingMode.CEILING));
|
||||
BigDecimal girth = outerLength.add(outerWidth.add(outerHeight).multiply(BigDecimal.valueOf(2)));
|
||||
item.setGirthCm(girth.setScale(3, RoundingMode.CEILING));
|
||||
item.setUnitVolumeCbm(unitVolume);
|
||||
item.setVolumeRawCbm(totalVolumeRaw);
|
||||
item.setVolumeCbm(totalVolume);
|
||||
item.setTotalVolumeCbm(totalVolume);
|
||||
item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
|
||||
item.setCalculationRuleVersion(weighted.calculationRuleVersion());
|
||||
item.setChargeRuleId(coefficientRule == null ? null : coefficientRule.getId());
|
||||
item.setCoefficientMode(coefficientMode);
|
||||
item.setCalculationRuleVersion(hasOuterOverride ? "WAREHOUSE_OUTER_OVERRIDE_V1"
|
||||
: coefficientRule != null
|
||||
? "CUSTOMER_COEFFICIENT_V" + coefficientRule.getRuleVersion() + "_MULTIPLY"
|
||||
: useDimensionRule ? weighted.calculationRuleVersion() : "ACTUAL_DIMENSION_V1");
|
||||
item.setSourceImageFileId(source.intelligentImageFileId());
|
||||
item.setOverLimitWarning(source.overLimitWarning());
|
||||
item.setOuterLengthCm(weighted.outerLengthCm());
|
||||
item.setOuterWidthCm(weighted.outerWidthCm());
|
||||
item.setOuterHeightCm(weighted.outerHeightCm());
|
||||
item.setOuterLengthCm(outerLength.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterWidthCm(outerWidth.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterHeightCm(outerHeight.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setRemark(source.remark());
|
||||
item.setCreatedAt(LocalDateTime.now());
|
||||
return item;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private String normalizeMode(String mode) {
|
||||
if (mode == null || mode.isBlank()) return null;
|
||||
String normalized = mode.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
if (!Set.of("ADD", "MULTIPLY").contains(normalized)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "系数模式仅支持 ADD 或 MULTIPLY");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private CustomerChargeRuleEntity findDefaultCoefficientRule(long tenantId, Long customerId) {
|
||||
if (customerId == null || customerId <= 0) return null;
|
||||
return pricingRepository.findDefaultRule(tenantId, customerId);
|
||||
}
|
||||
|
||||
private CustomerChargeRuleEntity requireCoefficientRule(long tenantId, Long customerId, Long ruleId) {
|
||||
if (customerId == null || customerId <= 0) {
|
||||
if (ruleId == null) return null;
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "入库单客户无效,不能应用计费系数");
|
||||
}
|
||||
CustomerChargeRuleEntity rule = ruleId == null
|
||||
? pricingRepository.findDefaultRule(tenantId, customerId)
|
||||
: pricingRepository.findRule(tenantId, ruleId);
|
||||
if (rule == null || !"ACTIVE".equals(rule.getStatus())
|
||||
|| !Boolean.TRUE.equals(rule.getDefaultRule()) || !customerId.equals(rule.getCustomerId())) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "计费规则不属于当前客户、未启用或未设置默认规则");
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
private BigDecimal applyCoefficient(BigDecimal dimension, BigDecimal coefficient) {
|
||||
if (coefficient == null) coefficient = BigDecimal.ONE;
|
||||
BigDecimal result = dimension.multiply(coefficient);
|
||||
if (result.compareTo(BigDecimal.ZERO) <= 0 || result.compareTo(new BigDecimal("999999999.999")) > 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "应用计费系数后的箱规尺寸超出允许范围");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private BigDecimal resolveSpecificationCoefficient(CustomerChargeRuleEntity rule,
|
||||
BigDecimal length, BigDecimal width,
|
||||
BigDecimal height) {
|
||||
List<SpecificationCoefficient> tiers = parseSpecificationCoefficients(rule.getRemark());
|
||||
if (tiers.isEmpty()) {
|
||||
return rule.getLengthFactor() == null ? BigDecimal.ONE : rule.getLengthFactor();
|
||||
}
|
||||
List<BigDecimal> measured = sortedDimensions(length, width, height);
|
||||
return tiers.stream()
|
||||
.filter(tier -> tier.contains(measured))
|
||||
.findFirst()
|
||||
.orElse(tiers.get(tiers.size() - 1))
|
||||
.coefficient();
|
||||
}
|
||||
|
||||
private List<SpecificationCoefficient> parseSpecificationCoefficients(String remark) {
|
||||
if (remark == null || remark.isBlank()) return List.of();
|
||||
Matcher matcher = SPECIFICATION_COEFFICIENT_PATTERN.matcher(remark);
|
||||
List<SpecificationCoefficient> tiers = new ArrayList<>();
|
||||
while (matcher.find()) {
|
||||
List<BigDecimal> dimensions = sortedDimensions(new BigDecimal(matcher.group(2)),
|
||||
new BigDecimal(matcher.group(3)), new BigDecimal(matcher.group(4)));
|
||||
BigDecimal coefficient = new BigDecimal(matcher.group(5));
|
||||
if (coefficient.signum() > 0) tiers.add(new SpecificationCoefficient(dimensions, coefficient));
|
||||
}
|
||||
tiers.sort(Comparator.comparing(SpecificationCoefficient::volume));
|
||||
return tiers;
|
||||
}
|
||||
|
||||
private List<BigDecimal> sortedDimensions(BigDecimal length, BigDecimal width, BigDecimal height) {
|
||||
List<BigDecimal> dimensions = new ArrayList<>(List.of(length, width, height));
|
||||
dimensions.sort(Comparator.reverseOrder());
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
private record SpecificationCoefficient(List<BigDecimal> dimensions, BigDecimal coefficient) {
|
||||
private BigDecimal volume() {
|
||||
return dimensions.get(0).multiply(dimensions.get(1)).multiply(dimensions.get(2));
|
||||
}
|
||||
|
||||
private boolean contains(List<BigDecimal> measured) {
|
||||
return measured.get(0).compareTo(dimensions.get(0)) <= 0
|
||||
&& measured.get(1).compareTo(dimensions.get(1)) <= 0
|
||||
&& measured.get(2).compareTo(dimensions.get(2)) <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException measurementRangeExceeded(MeasurementBatchCommand.Item source) {
|
||||
return new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"箱规尺寸或箱数超出系统允许范围,明细 " + source.itemNo() + ":"
|
||||
|
||||
Reference in New Issue
Block a user