|
|
@@ -19,10 +19,7 @@ import com.sckw.transport.api.model.vo.ParkingWalletFeeFreezeResult;
|
|
|
import com.sckw.transport.model.*;
|
|
|
import com.sckw.transport.model.enuma.WalletTypEnum;
|
|
|
import com.sckw.transport.model.param.*;
|
|
|
-import com.sckw.transport.repository.KwtParkingChangeStrategyRepository;
|
|
|
-import com.sckw.transport.repository.KwtParkingChangeStrategyUnitRepository;
|
|
|
-import com.sckw.transport.repository.KwtParkingWalletFeeBalanceRepository;
|
|
|
-import com.sckw.transport.repository.KwtParkingWalletFeeRepository;
|
|
|
+import com.sckw.transport.repository.*;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.dubbo.config.annotation.DubboReference;
|
|
|
@@ -51,14 +48,19 @@ public class ParkingWalletFeeService {
|
|
|
|
|
|
private final KwtParkingChangeStrategyRepository parkingChangeStrategyRepository;
|
|
|
private final KwtParkingChangeStrategyUnitRepository parkingChangeStrategyUnitRepository;
|
|
|
+ private final KwtParkingStrategySwitchRepository parkingStrategySwitchRepository;
|
|
|
|
|
|
@DubboReference(version = "1.0.0", group = "design", check = false, timeout = 6000)
|
|
|
RemoteSystemService remoteSystemService;
|
|
|
|
|
|
private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO;
|
|
|
private static final int STRATEGY_OPEN = 1;
|
|
|
+ /** 钱包明细交易类型:冻结(下单时服务费从可用余额转入履约中) */
|
|
|
private static final int TRADE_TYPE_FREEZE = 4;
|
|
|
+ /** 钱包明细交易类型:解冻(审核拒绝全额返还,或订单完结释放履约中金额) */
|
|
|
private static final int TRADE_TYPE_UNFREEZE = 5;
|
|
|
+ /** 钱包明细交易类型:消费(订单完结按实际履约量正式扣减服务费) */
|
|
|
+ private static final int TRADE_TYPE_CONSUME = 6;
|
|
|
|
|
|
/**
|
|
|
* 查询汇总服务费
|
|
|
@@ -307,51 +309,65 @@ public class ParkingWalletFeeService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 查询服务费余额、本次预计服务费与最大可购买数量
|
|
|
- * 规则:
|
|
|
- * 1. 服务费余额 = 采购方当前服务费余额
|
|
|
- * 2. 本次预计服务费 = 采购数量 * 采购方当前配置的收费策略单价
|
|
|
- * 3. 最大可购买数量 = 收费策略开启时,服务费余额 / 策略单价;未开启时不返回余额约束
|
|
|
+ * 查询服务费余额、本次预计服务费与最大可购买数量。
|
|
|
+ * <p>
|
|
|
+ * 业务规则:
|
|
|
+ * 1. 服务费余额取当前登录采购企业的可用服务费余额;
|
|
|
+ * 2. 收费策略总开关关闭时,不计算预计服务费,最大可购买数量返回 null,表示不做余额约束;
|
|
|
+ * 3. 收费策略总开关开启时,按企业绑定的当前收费策略计算预计服务费;
|
|
|
+ * 4. 策略 method 为 0.00 时,实际单价使用 kwt_parking_strategy_switch.default_fee;
|
|
|
+ * 5. 最大可购买数量 = 服务费余额 / 实际策略单价,向下取整,避免超出余额。
|
|
|
*
|
|
|
- * @param param 查询参数
|
|
|
+ * @param param 服务费预估查询参数,purchaseQuantity 为本次采购数量
|
|
|
* @return 服务费余额、本次预计服务费与最大可购买数量
|
|
|
*/
|
|
|
public ParkingWalletFeeEstimateResp queryEstimateServiceFee(ParkingWalletFeeEstimateQueryParam param) {
|
|
|
Long entId = LoginUserHolder.getEntId();
|
|
|
- log.info("服务费预估开始,entId:{}, param:{}", entId, param);
|
|
|
+ log.info("服务费预估开始,企业id:{}, 查询参数:{}", entId, param);
|
|
|
+ if (param == null) {
|
|
|
+ log.warn("服务费预估参数为空,企业id:{}", entId);
|
|
|
+ throw new BusinessPlatfromException(ErrorCodeEnum.PARAM_ERROR, "服务费预估参数不能为空");
|
|
|
+ }
|
|
|
if (param.getPurchaseQuantity() == null || param.getPurchaseQuantity().compareTo(ZERO_AMOUNT) < 0) {
|
|
|
- log.warn("服务费预估参数异常,采购数量非法,entId:{}, purchaseQuantity:{}",
|
|
|
+ log.warn("服务费预估参数异常,采购数量不能为空且不能小于0,企业id:{}, 采购数量:{}",
|
|
|
entId, param.getPurchaseQuantity());
|
|
|
throw new BusinessPlatfromException(ErrorCodeEnum.PARAM_ERROR, "采购数量不能为空且不能小于0");
|
|
|
}
|
|
|
|
|
|
- // 查询采购方当前服务费余额
|
|
|
+ // 查询采购企业当前可用服务费余额;supEntId 为空时按采购企业汇总口径取最新钱包记录。
|
|
|
BigDecimal serviceFeeBalance = queryServiceFeeBalance(entId, null);
|
|
|
+ log.info("服务费预估余额查询完成,企业id:{}, 服务费余额:{}", entId, serviceFeeBalance);
|
|
|
+
|
|
|
+ // 收费策略总开关统一以 kwt_parking_strategy_switch.status 为准,策略表 status 不再作为计费开关。
|
|
|
+ boolean chargeStrategySwitchOpen = isChargeStrategySwitchOpen();
|
|
|
|
|
|
- // 查询当前企业生效中的收费策略(仅取状态开启的最新一条)
|
|
|
+ // 查询当前企业绑定的最新收费策略;若 method 为 0.00,resolveStrategyUnitFee 会使用开关表 default_fee。
|
|
|
KwtParkingChargeStrategy currentStrategy = queryCurrentEnableStrategy(entId);
|
|
|
- BigDecimal currentStrategyUnitFee = currentStrategy == null || currentStrategy.getMethod() == null
|
|
|
- ? ZERO_AMOUNT : currentStrategy.getMethod();
|
|
|
- log.info("服务费预估策略信息,entId:{}, strategyId:{}, strategyStatus:{}, unitFee:{}",
|
|
|
+ BigDecimal currentStrategyUnitFee = resolveStrategyUnitFee(currentStrategy);
|
|
|
+ log.info("服务费预估策略匹配完成,企业id:{}, 开关状态:{}, 策略id:{}, 策略原始单价:{}, 实际计费单价:{}",
|
|
|
entId,
|
|
|
+ chargeStrategySwitchOpen ? "开启" : "关闭",
|
|
|
currentStrategy == null ? null : currentStrategy.getId(),
|
|
|
- currentStrategy == null ? null : currentStrategy.getStatus(),
|
|
|
+ currentStrategy == null ? null : currentStrategy.getMethod(),
|
|
|
currentStrategyUnitFee);
|
|
|
|
|
|
- // 本次预计服务费 = 采购数量 * 策略单价,金额统一保留2位
|
|
|
- BigDecimal estimatedServiceFee = param.getPurchaseQuantity().multiply(currentStrategyUnitFee)
|
|
|
- .setScale(2, RoundingMode.HALF_UP);
|
|
|
+ // 本次预计服务费 = 采购数量 * 实际计费单价,金额保留2位小数;开关关闭时固定返回0。
|
|
|
+ BigDecimal estimatedServiceFee = chargeStrategySwitchOpen
|
|
|
+ ? param.getPurchaseQuantity().multiply(currentStrategyUnitFee).setScale(2, RoundingMode.HALF_UP)
|
|
|
+ : ZERO_AMOUNT;
|
|
|
+ log.info("服务费预估金额计算完成,企业id:{}, 采购数量:{}, 实际计费单价:{}, 预计服务费:{}",
|
|
|
+ entId, param.getPurchaseQuantity(), currentStrategyUnitFee, estimatedServiceFee);
|
|
|
|
|
|
- // 收费策略开关开启时,按服务费余额计算最大可购买数量
|
|
|
+ // 开关开启且存在有效策略单价时,按服务费余额计算最大可购买数量;否则返回 null。
|
|
|
BigDecimal maxPurchaseQuantity = calculateMaxPurchaseQuantity(currentStrategy, currentStrategyUnitFee, serviceFeeBalance);
|
|
|
- log.info("服务费预估计算完成,entId:{}, purchaseQuantity:{}, serviceFeeBalance:{}, estimatedServiceFee:{}, maxPurchaseQuantity:{}",
|
|
|
+ log.info("服务费预估计算完成,企业id:{}, 采购数量:{}, 服务费余额:{}, 预计服务费:{}, 最大可购买数量:{}",
|
|
|
entId, param.getPurchaseQuantity(), serviceFeeBalance, estimatedServiceFee, maxPurchaseQuantity);
|
|
|
|
|
|
ParkingWalletFeeEstimateResp resp = new ParkingWalletFeeEstimateResp();
|
|
|
resp.setServiceFeeBalance(serviceFeeBalance);
|
|
|
resp.setEstimatedServiceFee(estimatedServiceFee);
|
|
|
resp.setMaxPurchaseQuantity(maxPurchaseQuantity);
|
|
|
- log.info("服务费预估结束,entId:{}, resp:{}", entId, resp);
|
|
|
+ log.info("服务费预估结束,企业id:{}, 响应结果:{}", entId, resp);
|
|
|
return resp;
|
|
|
}
|
|
|
|
|
|
@@ -382,14 +398,20 @@ public class ParkingWalletFeeService {
|
|
|
result.setApplyChargeStrategy(Global.NO);
|
|
|
result.setFreezeAmount(ZERO_AMOUNT);
|
|
|
|
|
|
+ if (!isChargeStrategySwitchOpen()) {
|
|
|
+ log.info("收费策略开关未开启,跳过服务费冻结,proEntId:{}, orderNo:{}",
|
|
|
+ param.getProEntId(), param.getOrderNo());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
KwtParkingChargeStrategy currentStrategy = queryCurrentEnableStrategy(param.getProEntId());
|
|
|
- if (currentStrategy == null || !Objects.equals(currentStrategy.getStatus(), STRATEGY_OPEN)) {
|
|
|
- log.info("收费策略未开启或未匹配,跳过服务费冻结,proEntId:{}, orderNo:{}",
|
|
|
+ if (currentStrategy == null) {
|
|
|
+ log.info("未匹配到收费策略,跳过服务费冻结,proEntId:{}, orderNo:{}",
|
|
|
param.getProEntId(), param.getOrderNo());
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
- BigDecimal unitFee = currentStrategy.getMethod() == null ? ZERO_AMOUNT : currentStrategy.getMethod();
|
|
|
+ BigDecimal unitFee = resolveStrategyUnitFee(currentStrategy);
|
|
|
if (unitFee.compareTo(ZERO_AMOUNT) <= 0) {
|
|
|
log.info("收费策略单价无效,跳过服务费冻结,strategyId:{}, unitFee:{}",
|
|
|
currentStrategy.getId(), unitFee);
|
|
|
@@ -484,6 +506,135 @@ public class ParkingWalletFeeService {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 贸易订单完结结算服务费
|
|
|
+ * <p>
|
|
|
+ * 适用场景:所有运单完成后,订单已应用收费策略(applyChargeStrategy=1)。
|
|
|
+ * 处理流程与预付钱包完结一致,分两步完成资金流转:
|
|
|
+ * <ol>
|
|
|
+ * <li>解冻:将下单时冻结的履约中金额(trading_amount)释放回可用余额(service_fee_balance),
|
|
|
+ * 并写入 trade_type=5 的解冻明细(备注:订单完结解冻服务费)</li>
|
|
|
+ * <li>扣减:按「运输净重量 × 采购方当前最优收费策略单价」从可用余额中正式扣减服务费,
|
|
|
+ * 并写入 trade_type=6 的消费明细(备注含策略名称、单价描述、净重量)</li>
|
|
|
+ * </ol>
|
|
|
+ * 幂等:以 trade_type=6 的消费记录作为结算完成标识,重复调用直接返回当前余额。
|
|
|
+ * <p>
|
|
|
+ * 钱包状态变化示例(冻结50元,实际扣减40元):
|
|
|
+ * 解冻前 balance=50, trading=50 → 解冻后 balance=100, trading=0 → 扣减后 balance=60, trading=0
|
|
|
+ *
|
|
|
+ * @param param 结算参数,需包含 orderNo、proEntId、actualQuantity(运输净重量)
|
|
|
+ * @return 结算结果,含实际扣减金额(freezeAmount字段)及扣减后服务费余额
|
|
|
+ */
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public ParkingWalletFeeFreezeResult settleChargeStrategy(ParkingWalletFeeFreezeParam param) {
|
|
|
+ log.info("贸易订单完结结算服务费开始,orderNo:{}, proEntId:{}, supEntId:{}, actualQuantity:{}, chargeStrategyId:{}",
|
|
|
+ param.getOrderNo(), param.getProEntId(), param.getSupEntId(),
|
|
|
+ param.getActualQuantity(), param.getChargeStrategyId());
|
|
|
+ validateSettleParam(param);
|
|
|
+
|
|
|
+ ParkingWalletFeeFreezeResult result = new ParkingWalletFeeFreezeResult();
|
|
|
+ result.setApplyChargeStrategy(Global.YES);
|
|
|
+
|
|
|
+ // 幂等校验:已存在消费明细则视为结算完成,避免重复扣减
|
|
|
+ if (existsConsumeRecord(param.getOrderNo(), param.getProEntId(), param.getSupEntId())) {
|
|
|
+ BigDecimal currentBalance = queryServiceFeeBalance(param.getProEntId(), param.getSupEntId());
|
|
|
+ log.info("服务费已结算,跳过重复处理,orderNo:{}, proEntId:{}, currentBalance:{}",
|
|
|
+ param.getOrderNo(), param.getProEntId(), currentBalance);
|
|
|
+ result.setServiceFeeBalance(currentBalance);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询下单时的冻结明细,作为解冻金额依据
|
|
|
+ KwtParkingWalletFeeBalance freezeRecord = queryFreezeBalanceRecord(param);
|
|
|
+ if (freezeRecord == null) {
|
|
|
+ log.warn("未找到服务费冻结记录,无法结算,orderNo:{}, proEntId:{}, supEntId:{}",
|
|
|
+ param.getOrderNo(), param.getProEntId(), param.getSupEntId());
|
|
|
+ throw new BusinessPlatfromException(ErrorCodeEnum.PARAM_ERROR, "服务费冻结记录不存在,无法结算");
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal freezeAmount = freezeRecord.getTradeAmount() == null ? ZERO_AMOUNT : freezeRecord.getTradeAmount();
|
|
|
+ if (freezeAmount.compareTo(ZERO_AMOUNT) <= 0) {
|
|
|
+ log.warn("服务费冻结金额异常,无法结算,orderNo:{}, freezeRecordId:{}, freezeAmount:{}",
|
|
|
+ param.getOrderNo(), freezeRecord.getId(), freezeAmount);
|
|
|
+ throw new BusinessPlatfromException(ErrorCodeEnum.PARAM_ERROR, "服务费冻结金额异常,无法结算");
|
|
|
+ }
|
|
|
+ log.info("查询到服务费冻结记录,orderNo:{}, freezeRecordId:{}, freezeAmount:{}",
|
|
|
+ param.getOrderNo(), freezeRecord.getId(), freezeAmount);
|
|
|
+
|
|
|
+ KwtParkingWalletFee walletFee = queryWalletFee(param.getProEntId(), param.getSupEntId());
|
|
|
+ if (walletFee == null) {
|
|
|
+ log.warn("采购方服务费账户不存在,无法结算,orderNo:{}, proEntId:{}, supEntId:{}",
|
|
|
+ param.getOrderNo(), param.getProEntId(), param.getSupEntId());
|
|
|
+ throw new BusinessPlatfromException(ErrorCodeEnum.PARAM_ERROR, "采购方服务费账户不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ BigDecimal balanceBeforeSettle = walletFee.getServiceFeeBalance() == null ? ZERO_AMOUNT : walletFee.getServiceFeeBalance();
|
|
|
+ BigDecimal tradingBeforeSettle = walletFee.getTradingAmount() == null ? ZERO_AMOUNT : walletFee.getTradingAmount();
|
|
|
+ log.info("结算前钱包状态,orderNo:{}, walletFeeId:{}, balance:{}, tradingAmount:{}",
|
|
|
+ param.getOrderNo(), walletFee.getId(), balanceBeforeSettle, tradingBeforeSettle);
|
|
|
+
|
|
|
+ // 第一步:解冻履约中金额,释放回可用余额(与预付钱包完结解冻逻辑一致)
|
|
|
+ if (tradingBeforeSettle.compareTo(freezeAmount) < 0) {
|
|
|
+ log.warn("服务费履约中金额不足,无法结算,orderNo:{}, tradingAmount:{}, freezeAmount:{}",
|
|
|
+ param.getOrderNo(), tradingBeforeSettle, freezeAmount);
|
|
|
+ throw new BusinessPlatfromException(ErrorCodeEnum.PARAM_ERROR, "服务费履约中金额不足,无法结算");
|
|
|
+ }
|
|
|
+ walletFee.setServiceFeeBalance(balanceBeforeSettle.add(freezeAmount));
|
|
|
+ walletFee.setTradingAmount(tradingBeforeSettle.subtract(freezeAmount));
|
|
|
+ log.info("完结解冻完成(内存态),orderNo:{}, unfreezeAmount:{}, balanceAfterUnfreeze:{}, tradingAfterUnfreeze:{}",
|
|
|
+ param.getOrderNo(), freezeAmount, walletFee.getServiceFeeBalance(), walletFee.getTradingAmount());
|
|
|
+
|
|
|
+ // 第二步:运输净重量 × 采购方当前最优收费策略单价 = 本次实际扣减服务费
|
|
|
+ SettleConsumeInfo consumeInfo = resolveSettleConsumeInfo(param, freezeAmount);
|
|
|
+ BigDecimal consumeAmount = consumeInfo.consumeAmount();
|
|
|
+ if (consumeAmount.compareTo(ZERO_AMOUNT) < 0) {
|
|
|
+ log.warn("服务费扣减金额计算异常,orderNo:{}, consumeAmount:{}", param.getOrderNo(), consumeAmount);
|
|
|
+ throw new BusinessPlatfromException(ErrorCodeEnum.PARAM_ERROR, "服务费扣减金额异常,无法结算");
|
|
|
+ }
|
|
|
+ log.info("服务费扣减金额计算完成,orderNo:{}, transportNetWeight:{}, strategyId:{}, strategyName:{}, strategyDesc:{}, freezeAmount:{}, consumeAmount:{}",
|
|
|
+ param.getOrderNo(), consumeInfo.transportNetWeight(), consumeInfo.strategyId(),
|
|
|
+ consumeInfo.strategyName(), consumeInfo.strategyDesc(), freezeAmount, consumeAmount);
|
|
|
+
|
|
|
+ saveSettleUnfreezeWalletBalanceLog(param, walletFee, freezeAmount, consumeInfo);
|
|
|
+
|
|
|
+ BigDecimal balanceAfterUnfreeze = walletFee.getServiceFeeBalance();
|
|
|
+ if (balanceAfterUnfreeze.compareTo(consumeAmount) < 0) {
|
|
|
+ log.warn("服务费余额不足,无法扣减,orderNo:{}, balanceAfterUnfreeze:{}, consumeAmount:{}",
|
|
|
+ param.getOrderNo(), balanceAfterUnfreeze, consumeAmount);
|
|
|
+ throw new BusinessPlatfromException(ErrorCodeEnum.PARAM_ERROR, "服务费余额不足,无法结算扣减");
|
|
|
+ }
|
|
|
+ walletFee.setServiceFeeBalance(balanceAfterUnfreeze.subtract(consumeAmount));
|
|
|
+ parkingWalletFeeRepository.updateById(walletFee);
|
|
|
+
|
|
|
+ saveConsumeWalletBalanceLog(param, walletFee, consumeInfo);
|
|
|
+ result.setChargeStrategyId(consumeInfo.strategyId());
|
|
|
+ result.setChargeStrategyDesc(consumeInfo.strategyDesc());
|
|
|
+ result.setFreezeAmount(consumeAmount);
|
|
|
+ result.setServiceFeeBalance(walletFee.getServiceFeeBalance());
|
|
|
+ log.info("贸易订单完结结算服务费完成,orderNo:{}, freezeAmount:{}, consumeAmount:{}, strategyDesc:{}, balanceBeforeSettle:{}, balanceAfterSettle:{}, tradingBeforeSettle:{}, tradingAfterSettle:{}",
|
|
|
+ param.getOrderNo(), freezeAmount, consumeAmount, consumeInfo.strategyDesc(),
|
|
|
+ balanceBeforeSettle, walletFee.getServiceFeeBalance(), tradingBeforeSettle, walletFee.getTradingAmount());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 订单完结服务费扣减计算结果
|
|
|
+ *
|
|
|
+ * @param consumeAmount 本次实际扣减服务费
|
|
|
+ * @param transportNetWeight 运输净重量
|
|
|
+ * @param strategyId 采用的收费策略id
|
|
|
+ * @param strategyName 采用的收费策略名称
|
|
|
+ * @param strategyDesc 收费策略单价描述,如10元/吨
|
|
|
+ */
|
|
|
+ private record SettleConsumeInfo(BigDecimal consumeAmount,
|
|
|
+ BigDecimal transportNetWeight,
|
|
|
+ Long strategyId,
|
|
|
+ String strategyName,
|
|
|
+ String strategyDesc) {
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* 查询采购方服务费余额
|
|
|
*/
|
|
|
@@ -500,6 +651,82 @@ public class ParkingWalletFeeService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 校验订单完结结算入参(订单号、采购方企业id必填)
|
|
|
+ */
|
|
|
+ private void validateSettleParam(ParkingWalletFeeFreezeParam param) {
|
|
|
+ validateUnfreezeParam(param);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断订单是否已完成服务费结算(存在 trade_type=6 的消费明细即为已结算)
|
|
|
+ */
|
|
|
+ private boolean existsConsumeRecord(String orderNo, Long proEntId, Long supEntId) {
|
|
|
+ return parkingWalletFeeBalanceRepository.count(
|
|
|
+ Wrappers.<KwtParkingWalletFeeBalance>lambdaQuery()
|
|
|
+ .eq(KwtParkingWalletFeeBalance::getOrderNo, orderNo)
|
|
|
+ .eq(KwtParkingWalletFeeBalance::getProEntId, proEntId)
|
|
|
+ .eq(supEntId != null, KwtParkingWalletFeeBalance::getSupEntId, supEntId)
|
|
|
+ .eq(KwtParkingWalletFeeBalance::getTradeType, TRADE_TYPE_CONSUME)
|
|
|
+ .eq(KwtParkingWalletFeeBalance::getDelFlag, Global.NO)
|
|
|
+ ) > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析订单完结服务费扣减信息
|
|
|
+ * <p>
|
|
|
+ * 扣减金额 = 运输净重量 × 采购方当前最优收费策略单价;
|
|
|
+ * 最优策略:企业已绑定且开启的策略中,单价最低的一条(对采购方最优惠)。
|
|
|
+ * 若无法匹配策略,则回退为下单冻结金额。
|
|
|
+ *
|
|
|
+ * @param param 结算参数(actualQuantity 为运输净重量)
|
|
|
+ * @param freezeAmount 下单冻结金额(兜底扣减值)
|
|
|
+ * @return 扣减计算结果
|
|
|
+ */
|
|
|
+ private SettleConsumeInfo resolveSettleConsumeInfo(ParkingWalletFeeFreezeParam param, BigDecimal freezeAmount) {
|
|
|
+ BigDecimal transportNetWeight = param.getActualQuantity() == null ? ZERO_AMOUNT : param.getActualQuantity();
|
|
|
+ KwtParkingChargeStrategy strategy = queryOptimalEnableStrategy(param.getProEntId());
|
|
|
+ if (strategy == null && param.getChargeStrategyId() != null) {
|
|
|
+ strategy = parkingChangeStrategyRepository.getById(param.getChargeStrategyId());
|
|
|
+ log.info("未匹配到当前最优策略,回退使用订单关联策略,orderNo:{}, chargeStrategyId:{}",
|
|
|
+ param.getOrderNo(), param.getChargeStrategyId());
|
|
|
+ }
|
|
|
+ if (strategy == null) {
|
|
|
+ log.warn("收费策略无效,扣减金额按冻结金额处理,orderNo:{}, freezeAmount:{}", param.getOrderNo(), freezeAmount);
|
|
|
+ return new SettleConsumeInfo(freezeAmount, transportNetWeight, null, null, null);
|
|
|
+ }
|
|
|
+ BigDecimal unitFee = resolveStrategyUnitFee(strategy);
|
|
|
+ if (unitFee.compareTo(ZERO_AMOUNT) <= 0) {
|
|
|
+ log.warn("收费策略单价无效,扣减金额按冻结金额处理,orderNo:{}, freezeAmount:{}", param.getOrderNo(), freezeAmount);
|
|
|
+ return new SettleConsumeInfo(freezeAmount, transportNetWeight, null, null, null);
|
|
|
+ }
|
|
|
+ String strategyDesc = buildStrategyMethodDesc(strategy);
|
|
|
+ BigDecimal consumeAmount = transportNetWeight.multiply(unitFee).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ log.info("按运输净重量×最优策略单价计算扣减,orderNo:{}, transportNetWeight:{}, strategyId:{}, strategyName:{}, unitFee:{}, consumeAmount:{}",
|
|
|
+ param.getOrderNo(), transportNetWeight, strategy.getId(), strategy.getStrategyName(),
|
|
|
+ unitFee, consumeAmount);
|
|
|
+ return new SettleConsumeInfo(consumeAmount, transportNetWeight, strategy.getId(),
|
|
|
+ strategy.getStrategyName(), strategyDesc);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建完结结算钱包明细备注
|
|
|
+ */
|
|
|
+ private String buildSettleWalletRemark(String actionDesc, ParkingWalletFeeFreezeParam param, SettleConsumeInfo consumeInfo) {
|
|
|
+ StringBuilder remark = new StringBuilder();
|
|
|
+ remark.append("订单").append(param.getOrderNo()).append(",").append(actionDesc);
|
|
|
+ if (consumeInfo.strategyName() != null) {
|
|
|
+ remark.append(",策略:").append(consumeInfo.strategyName());
|
|
|
+ }
|
|
|
+ if (consumeInfo.strategyDesc() != null) {
|
|
|
+ remark.append(",").append(consumeInfo.strategyDesc());
|
|
|
+ }
|
|
|
+ if (consumeInfo.transportNetWeight() != null) {
|
|
|
+ remark.append(",净重").append(consumeInfo.transportNetWeight().stripTrailingZeros().toPlainString());
|
|
|
+ }
|
|
|
+ return remark.toString();
|
|
|
+ }
|
|
|
+
|
|
|
private boolean existsUnfreezeRecord(String orderNo, Long proEntId, Long supEntId) {
|
|
|
return parkingWalletFeeBalanceRepository.count(
|
|
|
Wrappers.<KwtParkingWalletFeeBalance>lambdaQuery()
|
|
|
@@ -549,18 +776,81 @@ public class ParkingWalletFeeService {
|
|
|
param.getOrderNo(), param.getProEntId(), param.getSupEntId(), TRADE_TYPE_UNFREEZE,
|
|
|
unfreezeAmount, walletFee.getServiceFeeBalance());
|
|
|
}
|
|
|
+ /**
|
|
|
+ * 写入订单完结解冻明细(trade_type=5)
|
|
|
+ * 仅释放履约中金额至可用余额,不代表最终扣减完成
|
|
|
+ */
|
|
|
+ private void saveSettleUnfreezeWalletBalanceLog(ParkingWalletFeeFreezeParam param,
|
|
|
+ KwtParkingWalletFee walletFee,
|
|
|
+ BigDecimal unfreezeAmount,
|
|
|
+ SettleConsumeInfo consumeInfo) {
|
|
|
+ Long operatorId = resolveOperatorId(param.getOperatorId());
|
|
|
+ Date now = new Date();
|
|
|
+ KwtParkingWalletFeeBalance balanceRecord = new KwtParkingWalletFeeBalance();
|
|
|
+ balanceRecord.setOrderNo(param.getOrderNo());
|
|
|
+ balanceRecord.setProEntId(param.getProEntId());
|
|
|
+ balanceRecord.setSupEntId(param.getSupEntId());
|
|
|
+ balanceRecord.setTradeType(TRADE_TYPE_UNFREEZE);
|
|
|
+ balanceRecord.setTradeAmount(unfreezeAmount);
|
|
|
+ balanceRecord.setServiceFeeBalance(walletFee.getServiceFeeBalance());
|
|
|
+ balanceRecord.setTradingAmount(walletFee.getTradingAmount());
|
|
|
+ balanceRecord.setRemark(buildSettleWalletRemark("完结解冻服务费", param, consumeInfo));
|
|
|
+ balanceRecord.setDelFlag(Global.NO);
|
|
|
+ balanceRecord.setCreateUser(operatorId);
|
|
|
+ balanceRecord.setUpdateUser(operatorId);
|
|
|
+ balanceRecord.setCreateTime(now);
|
|
|
+ balanceRecord.setUpdateTime(now);
|
|
|
+ parkingWalletFeeBalanceRepository.save(balanceRecord);
|
|
|
+ log.info("服务费完结解冻明细已写入kwt_parking_wallet_fee_balance,orderNo:{}, remark:{}, tradeAmount:{}, balanceAfter:{}, tradingAfter:{}",
|
|
|
+ param.getOrderNo(), balanceRecord.getRemark(), unfreezeAmount,
|
|
|
+ walletFee.getServiceFeeBalance(), walletFee.getTradingAmount());
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 写入订单完结扣减明细(trade_type=6)
|
|
|
+ * 标志服务费正式消费完成,同时作为结算幂等依据
|
|
|
+ */
|
|
|
+ private void saveConsumeWalletBalanceLog(ParkingWalletFeeFreezeParam param,
|
|
|
+ KwtParkingWalletFee walletFee,
|
|
|
+ SettleConsumeInfo consumeInfo) {
|
|
|
+ Long operatorId = resolveOperatorId(param.getOperatorId());
|
|
|
+ Date now = new Date();
|
|
|
+ KwtParkingWalletFeeBalance balanceRecord = new KwtParkingWalletFeeBalance();
|
|
|
+ balanceRecord.setOrderNo(param.getOrderNo());
|
|
|
+ balanceRecord.setProEntId(param.getProEntId());
|
|
|
+ balanceRecord.setSupEntId(param.getSupEntId());
|
|
|
+ balanceRecord.setTradeType(TRADE_TYPE_CONSUME);
|
|
|
+ balanceRecord.setTradeAmount(consumeInfo.consumeAmount());
|
|
|
+ balanceRecord.setServiceFeeBalance(walletFee.getServiceFeeBalance());
|
|
|
+ balanceRecord.setTradingAmount(walletFee.getTradingAmount());
|
|
|
+ balanceRecord.setRemark(buildSettleWalletRemark("完结扣减服务费", param, consumeInfo));
|
|
|
+ balanceRecord.setDelFlag(Global.NO);
|
|
|
+ balanceRecord.setCreateUser(operatorId);
|
|
|
+ balanceRecord.setUpdateUser(operatorId);
|
|
|
+ balanceRecord.setCreateTime(now);
|
|
|
+ balanceRecord.setUpdateTime(now);
|
|
|
+ parkingWalletFeeBalanceRepository.save(balanceRecord);
|
|
|
+ log.info("服务费扣减明细已写入kwt_parking_wallet_fee_balance,orderNo:{}, remark:{}, tradeAmount:{}, balanceAfter:{}, tradingAfter:{}",
|
|
|
+ param.getOrderNo(), balanceRecord.getRemark(), consumeInfo.consumeAmount(),
|
|
|
+ walletFee.getServiceFeeBalance(), walletFee.getTradingAmount());
|
|
|
+ }
|
|
|
|
|
|
private String buildStrategyMethodDesc(KwtParkingChargeStrategy strategy) {
|
|
|
- if (strategy == null || strategy.getMethod() == null) {
|
|
|
+ if (strategy == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ BigDecimal unitFee = resolveStrategyUnitFee(strategy);
|
|
|
+ if (unitFee.compareTo(ZERO_AMOUNT) <= 0) {
|
|
|
return null;
|
|
|
}
|
|
|
if (Objects.equals(ParkingChangeStrategyEnum.BY_TON.getCode(), strategy.getType())) {
|
|
|
- return strategy.getMethod().stripTrailingZeros().toPlainString() + "元/吨";
|
|
|
+ return unitFee.stripTrailingZeros().toPlainString() + "元/吨";
|
|
|
}
|
|
|
if (Objects.equals(ParkingChangeStrategyEnum.BY_TIME.getCode(), strategy.getType())) {
|
|
|
- return strategy.getMethod().stripTrailingZeros().toPlainString() + "元/次";
|
|
|
+ return unitFee.stripTrailingZeros().toPlainString() + "元/次";
|
|
|
}
|
|
|
- return strategy.getMethod().stripTrailingZeros().toPlainString() + "元/月";
|
|
|
+ return unitFee.stripTrailingZeros().toPlainString() + "元/月";
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -623,27 +913,100 @@ public class ParkingWalletFeeService {
|
|
|
private BigDecimal calculateMaxPurchaseQuantity(KwtParkingChargeStrategy currentStrategy,
|
|
|
BigDecimal currentStrategyUnitFee,
|
|
|
BigDecimal serviceFeeBalance) {
|
|
|
- if (currentStrategy == null || !Objects.equals(currentStrategy.getStatus(), STRATEGY_OPEN)
|
|
|
+ boolean strategySwitchOpen = isChargeStrategySwitchOpen();
|
|
|
+ if (!strategySwitchOpen || currentStrategy == null
|
|
|
|| currentStrategyUnitFee.compareTo(ZERO_AMOUNT) <= 0) {
|
|
|
log.info("最大可购买数量未计算,原因:{}",
|
|
|
- currentStrategy == null ? "未匹配到收费策略" : "收费策略未开启或单价<=0");
|
|
|
+ !strategySwitchOpen ? "收费策略开关未开启"
|
|
|
+ : currentStrategy == null ? "未匹配到收费策略" : "策略单价<=0");
|
|
|
return null;
|
|
|
}
|
|
|
//向下取整
|
|
|
- BigDecimal maxPurchaseQuantity = serviceFeeBalance.divide(currentStrategyUnitFee, 4, RoundingMode.DOWN);
|
|
|
+ BigDecimal maxPurchaseQuantity = serviceFeeBalance.divide(currentStrategyUnitFee, 0, RoundingMode.DOWN);
|
|
|
log.info("最大可购买数量计算完成,serviceFeeBalance:{}, unitFee:{}, maxPurchaseQuantity:{}",
|
|
|
serviceFeeBalance, currentStrategyUnitFee, maxPurchaseQuantity);
|
|
|
return maxPurchaseQuantity.max(ZERO_AMOUNT);
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断收费策略总开关是否开启(以 kwt_parking_strategy_switch 表为准)
|
|
|
+ */
|
|
|
+ private boolean isChargeStrategySwitchOpen() {
|
|
|
+ KwtParkingStrategySwitch strategySwitch = parkingStrategySwitchRepository.queryParkingStrategySwitch();
|
|
|
+ return strategySwitch != null && Objects.equals(strategySwitch.getStatus(), STRATEGY_OPEN);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * 查询企业当前生效的收费策略
|
|
|
- * 逻辑:先查企业绑定的策略,再查策略详情,最后取“开启状态”且id最大(最新)的一条
|
|
|
+ * 查询收费策略开关配置的默认服务费
|
|
|
+ */
|
|
|
+ private BigDecimal resolveDefaultFee() {
|
|
|
+ KwtParkingStrategySwitch strategySwitch = parkingStrategySwitchRepository.queryParkingStrategySwitch();
|
|
|
+ if (strategySwitch == null || strategySwitch.getDefaultFee() == null) {
|
|
|
+ return ZERO_AMOUNT;
|
|
|
+ }
|
|
|
+ return strategySwitch.getDefaultFee();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析策略实际计费单价:method 为 0.00 时使用开关表 default_fee
|
|
|
+ */
|
|
|
+ private BigDecimal resolveStrategyUnitFee(KwtParkingChargeStrategy strategy) {
|
|
|
+ if (strategy == null) {
|
|
|
+ return ZERO_AMOUNT;
|
|
|
+ }
|
|
|
+ BigDecimal method = strategy.getMethod() == null ? ZERO_AMOUNT : strategy.getMethod();
|
|
|
+ if (method.compareTo(ZERO_AMOUNT) == 0) {
|
|
|
+ return resolveDefaultFee();
|
|
|
+ }
|
|
|
+ return method;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询企业当前绑定的收费策略(id 最大的一条,用于下单冻结/预估)。
|
|
|
+ * 收费策略是否启用统一由 kwt_parking_strategy_switch.status 控制,不再使用策略表 status 判断。
|
|
|
*
|
|
|
* @param proEntId 采购企业id
|
|
|
* @return 生效策略;未匹配到则返回null
|
|
|
*/
|
|
|
private KwtParkingChargeStrategy queryCurrentEnableStrategy(Long proEntId) {
|
|
|
+ KwtParkingChargeStrategy hitStrategy = listEnableStrategiesByProEntId(proEntId).stream()
|
|
|
+ .max(Comparator.comparing(KwtParkingChargeStrategy::getId))
|
|
|
+ .orElse(null);
|
|
|
+ log.info("匹配生效策略结果,proEntId:{}, hitStrategyId:{}",
|
|
|
+ proEntId, hitStrategy == null ? null : hitStrategy.getId());
|
|
|
+ return hitStrategy;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询采购方当前最优收费策略(实际单价最低的一条,用于订单完结扣费)。
|
|
|
+ * 收费策略是否启用统一由 kwt_parking_strategy_switch.status 控制,不再使用策略表 status 判断。
|
|
|
+ *
|
|
|
+ * @param proEntId 采购企业id
|
|
|
+ * @return 最优策略;未匹配到则返回null
|
|
|
+ */
|
|
|
+ private KwtParkingChargeStrategy queryOptimalEnableStrategy(Long proEntId) {
|
|
|
+ KwtParkingChargeStrategy optimalStrategy = listEnableStrategiesByProEntId(proEntId).stream()
|
|
|
+ .filter(item -> resolveStrategyUnitFee(item).compareTo(ZERO_AMOUNT) > 0)
|
|
|
+ .min(Comparator.comparing(this::resolveStrategyUnitFee)
|
|
|
+ .thenComparing(Comparator.comparing(KwtParkingChargeStrategy::getId).reversed()))
|
|
|
+ .orElse(null);
|
|
|
+ log.info("匹配最优收费策略结果,proEntId:{}, optimalStrategyId:{}, unitFee:{}",
|
|
|
+ proEntId,
|
|
|
+ optimalStrategy == null ? null : optimalStrategy.getId(),
|
|
|
+ optimalStrategy == null ? null : resolveStrategyUnitFee(optimalStrategy));
|
|
|
+ return optimalStrategy;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询企业已绑定的收费策略列表。
|
|
|
+ * 收费策略总开关统一以 kwt_parking_strategy_switch.status 为准,不再使用 kwt_parking_charge_strategy.status 过滤。
|
|
|
+ */
|
|
|
+ private List<KwtParkingChargeStrategy> listEnableStrategiesByProEntId(Long proEntId) {
|
|
|
+ if (!isChargeStrategySwitchOpen()) {
|
|
|
+ log.info("收费策略总开关未开启,不查询企业收费策略绑定,proEntId:{}", proEntId);
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
List<KwtParkingChargeStrategyUnit> strategyUnitList = parkingChangeStrategyUnitRepository.list(
|
|
|
Wrappers.<KwtParkingChargeStrategyUnit>lambdaQuery()
|
|
|
.eq(KwtParkingChargeStrategyUnit::getEntId, proEntId)
|
|
|
@@ -652,18 +1015,17 @@ public class ParkingWalletFeeService {
|
|
|
log.info("查询企业策略绑定关系,proEntId:{}, bindCount:{}",
|
|
|
proEntId, strategyUnitList == null ? 0 : strategyUnitList.size());
|
|
|
|
|
|
- if (org.apache.commons.collections4.CollectionUtils.isEmpty(strategyUnitList)) {
|
|
|
- log.info("企业未绑定任何策略,proEntId:{}, strategyUnitList:[]", proEntId);
|
|
|
- return null;
|
|
|
+ if (CollectionUtils.isEmpty(strategyUnitList)) {
|
|
|
+ log.info("企业未绑定任何策略,proEntId:{}", proEntId);
|
|
|
+ return List.of();
|
|
|
}
|
|
|
List<Long> strategyIds = strategyUnitList.stream()
|
|
|
.map(KwtParkingChargeStrategyUnit::getStrategyId)
|
|
|
.filter(Objects::nonNull)
|
|
|
.toList();
|
|
|
- log.info("查询企业策略ID集合,proEntId:{}, strategyIds:{}", proEntId, strategyIds);
|
|
|
- if (org.apache.commons.collections4.CollectionUtils.isEmpty(strategyIds)) {
|
|
|
- log.info("企业绑定策略ID集合为空");
|
|
|
- return null;
|
|
|
+ if (CollectionUtils.isEmpty(strategyIds)) {
|
|
|
+ log.info("企业绑定策略ID集合为空,proEntId:{}", proEntId);
|
|
|
+ return List.of();
|
|
|
}
|
|
|
List<KwtParkingChargeStrategy> strategyList = parkingChangeStrategyRepository.list(
|
|
|
Wrappers.<KwtParkingChargeStrategy>lambdaQuery()
|
|
|
@@ -671,18 +1033,10 @@ public class ParkingWalletFeeService {
|
|
|
.eq(KwtParkingChargeStrategy::getDelFlag, Global.NO)
|
|
|
.orderByDesc(KwtParkingChargeStrategy::getId)
|
|
|
);
|
|
|
- log.info("查询策略详情完成,proEntId:{}, strategyCount:{}",
|
|
|
- proEntId, strategyList == null ? 0 : strategyList.size());
|
|
|
- if (org.apache.commons.collections4.CollectionUtils.isEmpty(strategyList)) {
|
|
|
- return null;
|
|
|
+ if (CollectionUtils.isEmpty(strategyList)) {
|
|
|
+ return List.of();
|
|
|
}
|
|
|
- KwtParkingChargeStrategy hitStrategy = strategyList.stream()
|
|
|
- .filter(item -> Objects.equals(item.getStatus(), STRATEGY_OPEN))
|
|
|
- .max(Comparator.comparing(KwtParkingChargeStrategy::getId))
|
|
|
- .orElse(null);
|
|
|
- log.info("匹配生效策略结果,proEntId:{}, hitStrategyId:{}",
|
|
|
- proEntId, hitStrategy == null ? null : hitStrategy.getId());
|
|
|
- return hitStrategy;
|
|
|
+ return strategyList;
|
|
|
}
|
|
|
|
|
|
/**
|