mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-08-14 10:07:35 +08:00
统一保险订单资金口径:payAmount=充电金额,明细=完整实付(充电+保险)
规则:order_basic_info.pay_amount 只记充电金额(不含保险);order_pay_record 汇总为用户完整实付(充电+保险);凭证校验以 payAmount+保险==明细 为准。 - 在线支付(OrderService.adapayCallback):payAmount 存 amount-保险(充电金额), 回退f11b781c2的"完整金额"写入;下发桩额度直接用 payAmount,避免结算多退保险。 - 余额支付(Not/DelayMerchantProgramLogic.balancePayOrderV2):与在线一致,从钱包 扣 充电+保险,order_pay_record 记完整实付,payAmount 仍只记充电金额;余额不足 (<充电+保险)直接拒绝。会员交易流水记完整实付。 - 凭证校验(OrderBasicInfoServiceImpl.inspectOrderPayment):余额/非余额支路统一 改为容错对比 payAmount==明细 或 payAmount+保险==明细;补偿回写只写充电金额, 修复余额支路仍按 payAmount==明细 会误判新余额保险单的问题。 - 新增 docs/sql/fix_online_insurance_pay_amount_20260725.sql:矫正f11b781c2期间 在线保险单被写成完整金额的 payAmount(dry-run/备份/回滚齐全)。 - 新增单测:calculateBalanceRefund 6 例 + inspectOrderPayment 5 例(计算层,不连库)。
This commit is contained in:
@@ -1137,29 +1137,36 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
|
||||
.reason("订单支付明细与会员钱包出账金额不一致")
|
||||
.build();
|
||||
}
|
||||
// 余额支付:payAmount 已通过回调累加(可能含保险),payRecordAmount 仅含余额支付记录,
|
||||
// 直接比较两者即可,不应再加保险金额(避免双重计算)
|
||||
// 余额支付主表 payAmount 只记充电金额;order_pay_record/钱包出账为完整实付(充电+保险)。
|
||||
// 故 payAmount==明细 或 payAmount+保险==明细 任一成立即视为一致(无保险时两者等价,兼容历史单)。
|
||||
BigDecimal balanceInsuranceAmount = positiveOrZero(orderBasicInfo.getInsuranceAmount());
|
||||
BigDecimal balanceChargeOnly = payRecordAmount.subtract(balanceInsuranceAmount);
|
||||
if (balanceChargeOnly.compareTo(BigDecimal.ZERO) < 0) {
|
||||
balanceChargeOnly = BigDecimal.ZERO;
|
||||
}
|
||||
if (!baseFieldsReady) {
|
||||
return OrderPaymentCheckResult.builder()
|
||||
.checkCompleted(true)
|
||||
.paymentEvidenceFound(true)
|
||||
.orderPaymentReady(false)
|
||||
.paymentAmount(payRecordAmount)
|
||||
.paymentAmount(balanceChargeOnly) // 修复回写只写充电金额(明细-保险)
|
||||
.paymentTime(paymentTime)
|
||||
.reason("支付流水存在,但订单主表支付字段不完整(payStatus=" + payStatus
|
||||
+ ", payAmount=" + orderBasicInfo.getPayAmount()
|
||||
+ ", payTime=" + orderBasicInfo.getPayTime() + ")")
|
||||
.build();
|
||||
}
|
||||
if (!samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) {
|
||||
boolean balanceAmountConsistent = samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)
|
||||
|| samePaymentAmount(orderBasicInfo.getPayAmount().add(balanceInsuranceAmount), payRecordAmount);
|
||||
if (!balanceAmountConsistent) {
|
||||
return OrderPaymentCheckResult.builder()
|
||||
.checkCompleted(true)
|
||||
.paymentEvidenceFound(true)
|
||||
.orderPaymentReady(false)
|
||||
.paymentAmount(payRecordAmount)
|
||||
.paymentAmount(balanceChargeOnly)
|
||||
.paymentTime(paymentTime)
|
||||
.reason("支付流水存在,但订单主表支付金额(" + orderBasicInfo.getPayAmount()
|
||||
+ ")与支付明细汇总(" + payRecordAmount + ")不一致")
|
||||
+ ")+保险金额(" + balanceInsuranceAmount + ")与支付明细汇总(" + payRecordAmount + ")不一致")
|
||||
.build();
|
||||
}
|
||||
return OrderPaymentCheckResult.builder()
|
||||
@@ -1174,26 +1181,44 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
|
||||
|
||||
// 非余额支付保持兼容:必须有完整主表支付字段;若存在支付明细,则再校验明细金额。
|
||||
if (!baseFieldsReady) {
|
||||
// 补偿修复回写主表 payAmount 时只写充电金额:payRecordAmount 是完整实付(充电+保险),需扣掉保险。
|
||||
BigDecimal chargeOnlyAmount = null;
|
||||
if (payRecordEvidence) {
|
||||
BigDecimal insuranceAmount = positiveOrZero(orderBasicInfo.getInsuranceAmount());
|
||||
chargeOnlyAmount = payRecordAmount.subtract(insuranceAmount);
|
||||
if (chargeOnlyAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
chargeOnlyAmount = BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
return OrderPaymentCheckResult.builder()
|
||||
.checkCompleted(true)
|
||||
.paymentEvidenceFound(payRecordEvidence)
|
||||
.orderPaymentReady(false)
|
||||
.paymentAmount(payRecordEvidence ? payRecordAmount : null)
|
||||
.paymentAmount(chargeOnlyAmount)
|
||||
.paymentTime(paymentTime)
|
||||
.reason("订单主表支付字段不完整(payStatus=" + payStatus
|
||||
+ ", payAmount=" + orderBasicInfo.getPayAmount()
|
||||
+ ", payTime=" + orderBasicInfo.getPayTime() + ")")
|
||||
.build();
|
||||
}
|
||||
// payAmount 已通过回调/支付记录设置(微信/支付宝/白名单等),不应再加保险金额(避免双重计算)
|
||||
if (payRecordEvidence && !samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)) {
|
||||
// 兼容主表 payAmount 的两种历史口径,避免保险订单被误判为异常:
|
||||
// - 旧口径:payAmount = 充电金额(历史/旧在线回调把保险减掉了),此时 payAmount + 保险 = 完整实付;
|
||||
// - 新口径:payAmount = 完整金额(f11b781c2 起含保险),此时 payAmount 直接等于完整实付。
|
||||
// order_pay_record 汇总(payRecordAmount)始终是用户真实支付的完整金额(充电+保险),
|
||||
// 故 payAmount==明细 或 payAmount+保险==明细 任一成立即视为一致;无保险时保险=0,两条等价。
|
||||
// (余额支付支路已在上方单独校验,此处仅覆盖非余额支付。)
|
||||
BigDecimal insuranceAmount = positiveOrZero(orderBasicInfo.getInsuranceAmount());
|
||||
boolean amountConsistent = samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)
|
||||
|| samePaymentAmount(orderBasicInfo.getPayAmount().add(insuranceAmount), payRecordAmount);
|
||||
if (payRecordEvidence && !amountConsistent) {
|
||||
return OrderPaymentCheckResult.builder()
|
||||
.checkCompleted(true)
|
||||
.paymentEvidenceFound(true)
|
||||
.orderPaymentReady(false)
|
||||
.paymentTime(paymentTime)
|
||||
.reason("订单支付明细金额(" + payRecordAmount
|
||||
+ ")与订单主表支付金额(" + orderBasicInfo.getPayAmount() + ")不一致")
|
||||
+ ")与订单主表支付金额(" + orderBasicInfo.getPayAmount()
|
||||
+ ")+保险金额(" + insuranceAmount + ")均不一致")
|
||||
.build();
|
||||
}
|
||||
return OrderPaymentCheckResult.builder()
|
||||
@@ -5406,12 +5431,8 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取启动金额:保险金额不计入下发桩的充电额度
|
||||
BigDecimal payAmountForPile = dto.getPayAmount();
|
||||
if (dto.getInsuranceAmount() != null && dto.getInsuranceAmount().compareTo(BigDecimal.ZERO) > 0) {
|
||||
payAmountForPile = dto.getPayAmount().subtract(dto.getInsuranceAmount());
|
||||
}
|
||||
BigDecimal chargeAmount = computeChargeAmount(orderInfo.getMerchantId(), orderInfo.getStationId(), orderInfo.getMemberId(), payAmountForPile);
|
||||
// 获取启动金额:dto.payAmount 已是充电金额(不含保险),直接用于下发桩
|
||||
BigDecimal chargeAmount = computeChargeAmount(orderInfo.getMerchantId(), orderInfo.getStationId(), orderInfo.getMemberId(), dto.getPayAmount());
|
||||
|
||||
// 发送启动指令
|
||||
if (StringUtils.equals(pileConnectorDetailVO.getChargePortType(), Constants.THREE)) {
|
||||
|
||||
@@ -336,12 +336,20 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic {
|
||||
throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR.getValue(), "启动金额必须大于0");
|
||||
}
|
||||
dto.setPayAmount(chargeAmount);
|
||||
// 保险费:和在线支付一致,余额支付也需从钱包扣保险;无保险则为 0
|
||||
BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount() == null
|
||||
? BigDecimal.ZERO : orderBasicInfo.getInsuranceAmount();
|
||||
if (insuranceAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
insuranceAmount = BigDecimal.ZERO;
|
||||
}
|
||||
// 实际应扣金额 = 充电金额 + 保险费(完整实付,写入 order_pay_record)
|
||||
BigDecimal totalPayAmount = chargeAmount.add(insuranceAmount);
|
||||
// 总余额
|
||||
BigDecimal totalAccountAmount = memberVO.getTotalBalance() == null
|
||||
? BigDecimal.ZERO
|
||||
: memberVO.getTotalBalance();
|
||||
if (totalAccountAmount.compareTo(chargeAmount) < 0) {
|
||||
// 总余额小于充电金额
|
||||
if (totalAccountAmount.compareTo(totalPayAmount) < 0) {
|
||||
// 总余额不足以覆盖 充电金额 + 保险费,直接拒绝
|
||||
throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT);
|
||||
}
|
||||
|
||||
@@ -349,15 +357,16 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic {
|
||||
BigDecimal principalBalancePay = BigDecimal.ZERO;
|
||||
// 赠送金支付金额
|
||||
BigDecimal giftBalancePay = BigDecimal.ZERO;
|
||||
// 计算下发金额
|
||||
Map<String, BigDecimal> stringBigDecimalMap = calculateTheAmount(memberVO, chargeAmount);
|
||||
// 计算下发金额:按完整实付(充电+保险)拆分本金/赠金
|
||||
Map<String, BigDecimal> stringBigDecimalMap = calculateTheAmount(memberVO, totalPayAmount);
|
||||
if (stringBigDecimalMap != null) {
|
||||
principalBalancePay = stringBigDecimalMap.get("principalBalancePay");
|
||||
giftBalancePay = stringBigDecimalMap.get("giftBalancePay");
|
||||
}
|
||||
|
||||
// 更新支付金额 = 本金支付金额 + 赠送金支付金额,确保与 order_pay_record 汇总一致
|
||||
chargeAmount = principalBalancePay.add(giftBalancePay);
|
||||
// 实际扣款合计(= 完整实付,含保险),用于扣款、order_pay_record、会员流水;
|
||||
// 注意:主表 payAmount 仍只记充电金额(chargeAmount),不含保险。
|
||||
BigDecimal actualPaidAmount = principalBalancePay.add(giftBalancePay);
|
||||
|
||||
// 更新会员钱包 全部金额都用于支付订单
|
||||
UpdateMemberBalanceDTO updateMemberBalanceDTO = UpdateMemberBalanceDTO.builder()
|
||||
@@ -421,7 +430,7 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic {
|
||||
.actionType(ActionTypeEnum.FORWARD.getValue())
|
||||
.payMode(PayModeEnum.PAYMENT_OF_BALANCE.getValue())
|
||||
.paymentInstitutions(PaymentInstitutionsEnum.LOCAL_ACCOUNTS.getValue())
|
||||
.amount(dto.getPayAmount()) // 单位元
|
||||
.amount(actualPaidAmount) // 单位元,完整实付(充电+保险)
|
||||
.build();
|
||||
memberTransactionRecordService.insertSelective(record);
|
||||
}
|
||||
|
||||
@@ -248,12 +248,20 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic {
|
||||
throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR.getValue(), "启动金额必须大于0");
|
||||
}
|
||||
dto.setPayAmount(chargeAmount);
|
||||
// 保险费:和在线支付一致,余额支付也需从钱包扣保险;无保险则为 0
|
||||
BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount() == null
|
||||
? BigDecimal.ZERO : orderBasicInfo.getInsuranceAmount();
|
||||
if (insuranceAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
insuranceAmount = BigDecimal.ZERO;
|
||||
}
|
||||
// 实际应扣金额 = 充电金额 + 保险费(完整实付,写入 order_pay_record)
|
||||
BigDecimal totalPayAmount = chargeAmount.add(insuranceAmount);
|
||||
// 总余额
|
||||
BigDecimal totalAccountAmount = memberVO.getTotalBalance() == null
|
||||
? BigDecimal.ZERO
|
||||
: memberVO.getTotalBalance();
|
||||
if (totalAccountAmount.compareTo(chargeAmount) < 0) {
|
||||
// 总余额小于充电金额
|
||||
if (totalAccountAmount.compareTo(totalPayAmount) < 0) {
|
||||
// 总余额不足以覆盖 充电金额 + 保险费,直接拒绝
|
||||
throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT);
|
||||
}
|
||||
|
||||
@@ -261,15 +269,16 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic {
|
||||
BigDecimal principalBalancePay = BigDecimal.ZERO;
|
||||
// 赠送金支付金额
|
||||
BigDecimal giftBalancePay = BigDecimal.ZERO;
|
||||
// 计算下发金额
|
||||
Map<String, BigDecimal> stringBigDecimalMap = calculateTheAmount(memberVO, chargeAmount);
|
||||
// 计算下发金额:按完整实付(充电+保险)拆分本金/赠金
|
||||
Map<String, BigDecimal> stringBigDecimalMap = calculateTheAmount(memberVO, totalPayAmount);
|
||||
if (stringBigDecimalMap != null) {
|
||||
principalBalancePay = stringBigDecimalMap.get("principalBalancePay");
|
||||
giftBalancePay = stringBigDecimalMap.get("giftBalancePay");
|
||||
}
|
||||
|
||||
// 更新支付金额 = 本金支付金额 + 赠送金支付金额
|
||||
chargeAmount = principalBalancePay.add(giftBalancePay);
|
||||
// 实际扣款合计(= 完整实付,含保险),用于扣款、order_pay_record、会员流水;
|
||||
// 注意:主表 payAmount 仍只记充电金额(chargeAmount),不含保险。
|
||||
BigDecimal actualPaidAmount = principalBalancePay.add(giftBalancePay);
|
||||
|
||||
// 更新会员钱包 全部金额都用于支付订单
|
||||
UpdateMemberBalanceDTO updateMemberBalanceDTO = UpdateMemberBalanceDTO.builder()
|
||||
@@ -333,7 +342,7 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic {
|
||||
.actionType(ActionTypeEnum.FORWARD.getValue())
|
||||
.payMode(PayModeEnum.PAYMENT_OF_BALANCE.getValue())
|
||||
.paymentInstitutions(PaymentInstitutionsEnum.LOCAL_ACCOUNTS.getValue())
|
||||
.amount(dto.getPayAmount()) // 单位元
|
||||
.amount(actualPaidAmount) // 单位元,完整实付(充电+保险)
|
||||
.build();
|
||||
memberTransactionRecordService.insertSelective(record);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user