diff --git a/docs/sql/fix_online_insurance_pay_amount_20260725.sql b/docs/sql/fix_online_insurance_pay_amount_20260725.sql
new file mode 100644
index 000000000..ed455558c
--- /dev/null
+++ b/docs/sql/fix_online_insurance_pay_amount_20260725.sql
@@ -0,0 +1,131 @@
+-- ============================================================================
+-- 矫正在线支付「保险订单」的 pay_amount
+-- 背景:提交 f11b781c2 上线到本次回退期间,在线支付回调把 order_basic_info.pay_amount
+-- 写成了「完整实付(充电+保险)」。规则应为:
+-- order_basic_info.pay_amount = 充电金额(不含保险)
+-- order_pay_record 汇总 = 完整实付(充电 + 保险)
+-- 故需把这批订单的 pay_amount 从「完整金额」矫正为「pay_amount - insurance_amount」。
+-- 适用:MySQL 5.7+(未用 CTE,5.7 可直接跑)
+-- 日期:2026-07-25
+--
+-- 识别口径:在线支付(微信'4'/支付宝'5') + 已支付 + insurance>0 + pay_amount≈明细汇总。
+-- · 正确订单:pay_amount = 明细 - 保险 < 明细
+-- · 错误订单:pay_amount = 明细(含保险) => 命中本脚本
+--
+-- ⚠️ 关于「已完成(order_status='6')」订单:结算时已用错误的 pay_amount 计算过
+-- 退款(residue = pay_amount - 消费),即保险已被多退,本脚本无法追回那笔钱;
+-- 对已完成订单矫正 pay_amount 只是让「记录」变正确,且会与已落库的 refund_amount
+-- 不再自洽。是否处理已完成订单请按业务决定(见第 3 步的两种 WHERE)。
+-- ============================================================================
+
+
+-- ---------------------------------------------------------------------------
+-- 第 1 步:DRY-RUN 预览(务必先只跑这个,核对数量与金额)
+-- ---------------------------------------------------------------------------
+SELECT
+ o.order_code,
+ o.order_status,
+ CASE o.order_status WHEN '6' THEN '已完成' WHEN '4' THEN '异常' ELSE '未完成' END AS status_desc,
+ o.pay_mode,
+ o.pay_amount AS cur_pay_amount, -- 当前(错误,含保险)
+ o.insurance_amount AS insurance_amount,
+ pr.pay_record_sum AS pay_record_sum, -- 明细汇总(完整实付)
+ (o.pay_amount - o.insurance_amount) AS corrected_pay_amount -- 矫正后(充电金额)
+FROM order_basic_info o
+JOIN (
+ SELECT order_code, SUM(pay_amount) AS pay_record_sum
+ FROM order_pay_record
+ WHERE del_flag = '0'
+ GROUP BY order_code
+) pr ON pr.order_code = o.order_code
+WHERE o.pay_mode IN ('4','5') -- 在线支付(微信/支付宝)
+ AND o.pay_status = '1' -- 已支付
+ AND o.insurance_amount > 0 -- 有保险
+ AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005 -- pay_amount == 明细汇总 => 含了保险(错误)
+ORDER BY o.order_status, o.order_code;
+
+
+-- ---------------------------------------------------------------------------
+-- 第 2 步:备份受影响行(建议执行,便于回滚)
+-- ---------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS bak_order_pay_amount_fix_20260725 (
+ order_code VARCHAR(64) PRIMARY KEY,
+ old_pay_amount DECIMAL(10,2),
+ insurance_amount DECIMAL(10,2),
+ new_pay_amount DECIMAL(10,2),
+ order_status VARCHAR(4),
+ backup_time DATETIME
+);
+
+INSERT INTO bak_order_pay_amount_fix_20260725
+ (order_code, old_pay_amount, insurance_amount, new_pay_amount, order_status, backup_time)
+SELECT o.order_code, o.pay_amount, o.insurance_amount,
+ (o.pay_amount - o.insurance_amount), o.order_status, NOW()
+FROM order_basic_info o
+JOIN (
+ SELECT order_code, SUM(pay_amount) AS pay_record_sum
+ FROM order_pay_record WHERE del_flag = '0' GROUP BY order_code
+) pr ON pr.order_code = o.order_code
+WHERE o.pay_mode IN ('4','5')
+ AND o.pay_status = '1'
+ AND o.insurance_amount > 0
+ AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005
+ON DUPLICATE KEY UPDATE backup_time = NOW();
+
+
+-- ---------------------------------------------------------------------------
+-- 第 3 步:正式矫正(确认第 1 步无误后执行)
+-- pay_amount = pay_amount - insurance_amount = 充电金额
+--
+-- ▸ 方案 A(推荐):只矫正「未完成」订单,避免动已结算/已退款的历史账
+-- —— 防止这些单将来结算时多退保险。把下面这条的注释去掉执行:
+-- ---------------------------------------------------------------------------
+-- UPDATE order_basic_info o
+-- JOIN (
+-- SELECT order_code, SUM(pay_amount) AS pay_record_sum
+-- FROM order_pay_record WHERE del_flag = '0' GROUP BY order_code
+-- ) pr ON pr.order_code = o.order_code
+-- SET o.pay_amount = o.pay_amount - o.insurance_amount
+-- WHERE o.pay_mode IN ('4','5')
+-- AND o.pay_status = '1'
+-- AND o.insurance_amount > 0
+-- AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005
+-- AND o.order_status NOT IN ('6','7'); -- 排除已完成/已关闭
+
+-- ▸ 方案 B:矫正「全部」受影响订单(含已完成,仅让记录口径一致;已多退的保险无法追回)
+-- 如需按此执行,去掉下面这条的注释:
+-- UPDATE order_basic_info o
+-- JOIN (
+-- SELECT order_code, SUM(pay_amount) AS pay_record_sum
+-- FROM order_pay_record WHERE del_flag = '0' GROUP BY order_code
+-- ) pr ON pr.order_code = o.order_code
+-- SET o.pay_amount = o.pay_amount - o.insurance_amount
+-- WHERE o.pay_mode IN ('4','5')
+-- AND o.pay_status = '1'
+-- AND o.insurance_amount > 0
+-- AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005;
+
+
+-- ---------------------------------------------------------------------------
+-- 第 4 步:矫正后复核(应为 0 行)
+-- ---------------------------------------------------------------------------
+-- 复核逻辑:矫正后 正确订单应满足 pay_amount + insurance = 明细汇总;
+-- 若仍有 pay_amount == 明细汇总 且 insurance>0,说明还有漏网的。
+SELECT COUNT(*) AS still_wrong
+FROM order_basic_info o
+JOIN (
+ SELECT order_code, SUM(pay_amount) AS pay_record_sum
+ FROM order_pay_record WHERE del_flag = '0' GROUP BY order_code
+) pr ON pr.order_code = o.order_code
+WHERE o.pay_mode IN ('4','5')
+ AND o.pay_status = '1'
+ AND o.insurance_amount > 0
+ AND ABS(o.pay_amount - pr.pay_record_sum) < 0.005;
+
+
+-- ---------------------------------------------------------------------------
+-- 回滚(如需):从备份表还原
+-- ---------------------------------------------------------------------------
+-- UPDATE order_basic_info o
+-- JOIN bak_order_pay_amount_fix_20260725 b ON b.order_code = o.order_code
+-- SET o.pay_amount = b.old_pay_amount;
diff --git a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java
index 43581f9da..f97a12988 100644
--- a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java
+++ b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java
@@ -1302,12 +1302,17 @@ public class OrderService {
// 支付订单成功
OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode);
BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount();
- log.info("adapayCallback-订单:{}, 网关支付金额(amount):{}, 订单保险金额(insuranceAmount):{}, payRecord将记录:{}",
- orderCode, amount, insuranceAmount, amount);
+ // 主表 payAmount 只存充电金额(不含保险):充电金额 = 网关实付(amount) - 保险金额;
+ // order_pay_record 仍记录完整实付(amount),故校验时 明细 == payAmount + 保险。
+ BigDecimal chargeAmount = amount;
+ if (insuranceAmount != null && insuranceAmount.compareTo(BigDecimal.ZERO) > 0) {
+ chargeAmount = amount.subtract(insuranceAmount).setScale(2, RoundingMode.HALF_UP);
+ }
+ log.info("adapayCallback-订单:{}, 网关支付金额(amount):{}, 订单保险金额(insuranceAmount):{}, 充电金额(payAmount将记录):{}, order_pay_record记录:{}",
+ orderCode, amount, insuranceAmount, chargeAmount, amount);
PayOrderSuccessCallbackDTO callbackDTO = PayOrderSuccessCallbackDTO.builder()
.orderCode(orderCode)
- .payAmount(amount) // 完整支付金额(含保险),与 order_pay_record 汇总一致
- .insuranceAmount(insuranceAmount) // 保险金额,回调中用于计算下发桩的充电金额
+ .payAmount(chargeAmount) // 充电金额(不含保险)
.payMode(payModel)
.acquirer(AcquirerEnum.ADAPAY.getValue())
.build();
diff --git a/jsowell-admin/src/test/java/com/jsowell/pile/service/impl/OrderPaymentVoucherInsuranceTest.java b/jsowell-admin/src/test/java/com/jsowell/pile/service/impl/OrderPaymentVoucherInsuranceTest.java
new file mode 100644
index 000000000..267bc7176
--- /dev/null
+++ b/jsowell-admin/src/test/java/com/jsowell/pile/service/impl/OrderPaymentVoucherInsuranceTest.java
@@ -0,0 +1,157 @@
+package com.jsowell.pile.service.impl;
+
+import com.jsowell.common.enums.DelFlagEnum;
+import com.jsowell.common.enums.MemberWalletEnum;
+import com.jsowell.common.enums.ykc.OrderPayModeEnum;
+import com.jsowell.common.enums.ykc.OrderPayRecordEnum;
+import com.jsowell.common.enums.ykc.OrderPayStatusEnum;
+import com.jsowell.pile.domain.MemberWalletLog;
+import com.jsowell.pile.domain.OrderBasicInfo;
+import com.jsowell.pile.domain.OrderPayRecord;
+import com.jsowell.pile.service.MemberWalletLogService;
+import com.jsowell.pile.service.OrderPayRecordService;
+import com.jsowell.pile.vo.web.OrderPaymentCheckResult;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.objenesis.ObjenesisStd;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+/**
+ * 支付凭证校验({@code inspectOrderPayment})保险口径的单元测试(mock 依赖,不连库)。
+ *
+ *
规则:主表 payAmount = 充电金额;order_pay_record 汇总 = 完整实付(充电 + 保险);
+ * {@code payAmount==明细 或 payAmount+保险==明细} 任一成立即视为一致。
+ */
+class OrderPaymentVoucherInsuranceTest {
+
+ private static final String ORDER_CODE = "O-TEST";
+
+ private static void setField(Object target, String name, Object value) {
+ try {
+ Field f = target.getClass().getDeclaredField(name);
+ f.setAccessible(true);
+ f.set(target, value);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static OrderPaymentCheckResult inspect(OrderBasicInfoServiceImpl service, OrderBasicInfo order) {
+ try {
+ Method m = OrderBasicInfoServiceImpl.class.getDeclaredMethod("inspectOrderPayment", OrderBasicInfo.class);
+ m.setAccessible(true);
+ return (OrderPaymentCheckResult) m.invoke(service, order);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static OrderPayRecord payRecord(String payMode, String amount) {
+ return OrderPayRecord.builder()
+ .payMode(payMode)
+ .payAmount(new BigDecimal(amount))
+ .delFlag(DelFlagEnum.NORMAL.getValue())
+ .createTime(new Date())
+ .build();
+ }
+
+ private static MemberWalletLog walletOut(String amount) {
+ return MemberWalletLog.builder()
+ .type(MemberWalletEnum.TYPE_OUT.getValue())
+ .subType(MemberWalletEnum.SUBTYPE_PAYMENT_FOR_ORDER.getValue())
+ .amount(new BigDecimal(amount))
+ .createTime(new Date())
+ .build();
+ }
+
+ private static OrderBasicInfo order(String payMode, String payAmount, String insurance) {
+ OrderBasicInfo o = new OrderBasicInfo();
+ o.setOrderCode(ORDER_CODE);
+ o.setPayMode(payMode);
+ o.setPayStatus(OrderPayStatusEnum.paid.getValue());
+ o.setPayAmount(new BigDecimal(payAmount));
+ o.setPayTime(new Date());
+ o.setInsuranceAmount(new BigDecimal(insurance));
+ return o;
+ }
+
+ private OrderBasicInfoServiceImpl serviceWith(List records, List walletLogs) {
+ OrderPayRecordService payRecordService = Mockito.mock(OrderPayRecordService.class);
+ MemberWalletLogService walletLogService = Mockito.mock(MemberWalletLogService.class);
+ when(payRecordService.getOrderPayRecordList(ORDER_CODE)).thenReturn(records);
+ when(walletLogService.getOrderRecord(ORDER_CODE, MemberWalletEnum.TYPE_OUT.getValue())).thenReturn(walletLogs);
+ // OrderBasicInfoServiceImpl 构造器/字段初始化里有 SpringUtils.getBean,依赖 Spring 容器;
+ // 用 Objenesis 绕过构造器实例化,只注入本用例需要的依赖。
+ OrderBasicInfoServiceImpl service = new ObjenesisStd().newInstance(OrderBasicInfoServiceImpl.class);
+ setField(service, "logger", LoggerFactory.getLogger(OrderPaymentVoucherInsuranceTest.class));
+ setField(service, "orderPayRecordService", payRecordService);
+ setField(service, "memberWalletLogService", walletLogService);
+ return service;
+ }
+
+ @Test
+ void balanceInsuranceOrder_isConsistent() {
+ // 余额:payAmount=50(充电),明细=50.5(充电+保险),保险0.5 → 50+0.5==50.5 ✓
+ OrderBasicInfoServiceImpl service = serviceWith(
+ Collections.singletonList(payRecord(OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue(), "50.5")),
+ Collections.singletonList(walletOut("-50.5")));
+ OrderPaymentCheckResult r = inspect(service,
+ order(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), "50.00", "0.5"));
+ assertTrue(r.isOrderPaymentReady(), r.getReason());
+ }
+
+ @Test
+ void balanceNoInsurance_isConsistent() {
+ // 余额无保险:payAmount=50,明细=50 → 50==50 ✓(回归)
+ OrderBasicInfoServiceImpl service = serviceWith(
+ Collections.singletonList(payRecord(OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue(), "50")),
+ Collections.singletonList(walletOut("-50")));
+ OrderPaymentCheckResult r = inspect(service,
+ order(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), "50", "0"));
+ assertTrue(r.isOrderPaymentReady(), r.getReason());
+ }
+
+ @Test
+ void balanceGenuineMismatch_isFlagged() {
+ // 余额真异常:payAmount=40,明细=50.5,保险0.5 → 40≠50.5 且 40.5≠50.5 → 不通过
+ OrderBasicInfoServiceImpl service = serviceWith(
+ Collections.singletonList(payRecord(OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue(), "50.5")),
+ Collections.singletonList(walletOut("-50.5")));
+ OrderPaymentCheckResult r = inspect(service,
+ order(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), "40", "0.5"));
+ assertFalse(r.isOrderPaymentReady());
+ }
+
+ @Test
+ void onlineInsuranceOrder_isConsistent() {
+ // 在线:payAmount=50(充电),明细=50.5,保险0.5 → 50+0.5==50.5 ✓
+ OrderBasicInfoServiceImpl service = serviceWith(
+ Collections.singletonList(payRecord(OrderPayRecordEnum.WECHATPAY_PAYMENT.getValue(), "50.5")),
+ Collections.emptyList());
+ OrderPaymentCheckResult r = inspect(service,
+ order(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue(), "50.00", "0.5"));
+ assertTrue(r.isOrderPaymentReady(), r.getReason());
+ }
+
+ @Test
+ void onlineLegacyFullPayAmount_isConsistent() {
+ // 兼容 f11b781c2 期间历史单:payAmount=50.5(含保险),明细=50.5 → 50.5==50.5 ✓
+ OrderBasicInfoServiceImpl service = serviceWith(
+ Collections.singletonList(payRecord(OrderPayRecordEnum.WECHATPAY_PAYMENT.getValue(), "50.5")),
+ Collections.emptyList());
+ OrderPaymentCheckResult r = inspect(service,
+ order(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue(), "50.5", "0.5"));
+ assertTrue(r.isOrderPaymentReady(), r.getReason());
+ }
+}
diff --git a/jsowell-admin/src/test/java/com/jsowell/pile/service/programlogic/BalanceInsuranceRefundTest.java b/jsowell-admin/src/test/java/com/jsowell/pile/service/programlogic/BalanceInsuranceRefundTest.java
new file mode 100644
index 000000000..43386580b
--- /dev/null
+++ b/jsowell-admin/src/test/java/com/jsowell/pile/service/programlogic/BalanceInsuranceRefundTest.java
@@ -0,0 +1,96 @@
+package com.jsowell.pile.service.programlogic;
+
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 余额支付「电费 + 保险费」退款计算的单元测试(纯计算,不连库)。
+ *
+ * 前提:order_pay_record 汇总(principalPay + giftPay) = 完整实付(充电 + 保险),
+ * 故 {@code 电费预付款 = 总支付 - 保险}。这是「余额与在线一样从钱包扣保险」之后才成立的口径。
+ *
+ * 覆盖场景:
+ *
+ * - 无保险 + 正常消费(回归,行为不变)
+ * - 有保险 + 正常消费(消费≥1) → 保险不退
+ * - 有保险 + 消费<1 → 保险退
+ * - 保险跨本金/赠金拆分 + 消费<1
+ * - 折扣把实际电费消费压到<1 → 保险退
+ * - 消费恰好=1(边界)→ 保险不退
+ *
+ */
+class BalanceInsuranceRefundTest {
+
+ private static Map refund(String principalPay, String giftPay,
+ String orderAmount, String discount, String insurance) {
+ try {
+ Method method = AbstractProgramLogic.class.getDeclaredMethod("calculateBalanceRefund",
+ BigDecimal.class, BigDecimal.class, BigDecimal.class, BigDecimal.class, BigDecimal.class);
+ method.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ Map result = (Map) method.invoke(null,
+ new BigDecimal(principalPay), new BigDecimal(giftPay), new BigDecimal(orderAmount),
+ new BigDecimal(discount), new BigDecimal(insurance));
+ return result;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void assertAmount(String key, String expected, BigDecimal actual) {
+ assertTrue(actual != null && new BigDecimal(expected).compareTo(actual) == 0,
+ key + " 期望 " + expected + " 实际 " + actual);
+ }
+
+ /** electricityPrincipal/Gift = 电费退款;insurancePrincipal/Gift = 保险退款。 */
+ private static void assertRefund(Map m,
+ String electricityPrincipal, String electricityGift,
+ String insurancePrincipal, String insuranceGift) {
+ assertAmount("returnPrincipal", electricityPrincipal, m.get("returnPrincipal"));
+ assertAmount("returnGift", electricityGift, m.get("returnGift"));
+ assertAmount("returnPrincipalForInsurance", insurancePrincipal, m.get("returnPrincipalForInsurance"));
+ assertAmount("returnGiftForInsurance", insuranceGift, m.get("returnGiftForInsurance"));
+ }
+
+ @Test
+ void noInsurance_normalConsumption_refundUnusedElectricityOnly() {
+ // 本金50全充电,消费30 → 退20;无保险
+ assertRefund(refund("50", "0", "30", "0", "0"), "20", "0", "0", "0");
+ }
+
+ @Test
+ void withInsurance_normalConsumption_insuranceNotRefunded() {
+ // 实付50.5(充电50+保险0.5),消费30(≥1) → 电费退20,保险不退
+ assertRefund(refund("50.5", "0", "30", "0", "0.5"), "20", "0", "0", "0");
+ }
+
+ @Test
+ void withInsurance_lowConsumption_insuranceRefunded() {
+ // 实付50.5,消费0.5(<1) → 电费退49.5,保险退0.5
+ assertRefund(refund("50.5", "0", "0.5", "0", "0.5"), "49.5", "0", "0.5", "0");
+ }
+
+ @Test
+ void insuranceSplitAcrossPrincipalAndGift_lowConsumption() {
+ // 本金0.3+赠金50.2=50.5;保险0.5优先扣本金0.3、赠金补0.2;消费0.2(<1)
+ // 电费:本金退0,赠金退49.8;保险退:本金0.3、赠金0.2
+ assertRefund(refund("0.3", "50.2", "0.2", "0", "0.5"), "0", "49.8", "0.3", "0.2");
+ }
+
+ @Test
+ void discountReducesConsumptionBelowOne_insuranceRefunded() {
+ // 消费10、折扣9.5 → 实际电费消费0.5(<1) → 电费退49.5,保险退0.5
+ assertRefund(refund("50.5", "0", "10", "9.5", "0.5"), "49.5", "0", "0.5", "0");
+ }
+
+ @Test
+ void consumptionExactlyOne_insuranceNotRefunded() {
+ // 实际电费消费=1.0,非严格<1 → 保险不退;电费退49
+ assertRefund(refund("50.5", "0", "1", "0", "0.5"), "49", "0", "0", "0");
+ }
+}
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java
index 4886cdd3e..2424f17e0 100644
--- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/OrderBasicInfoServiceImpl.java
@@ -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)) {
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java
index 7f45776cc..e62c46a75 100644
--- a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/DelayMerchantProgramLogic.java
@@ -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 stringBigDecimalMap = calculateTheAmount(memberVO, chargeAmount);
+ // 计算下发金额:按完整实付(充电+保险)拆分本金/赠金
+ Map 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);
}
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java
index 5d6fd34e9..1c7d959bf 100644
--- a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/NotDelayMerchantProgramLogic.java
@@ -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 stringBigDecimalMap = calculateTheAmount(memberVO, chargeAmount);
+ // 计算下发金额:按完整实付(充电+保险)拆分本金/赠金
+ Map 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);
}