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/api/uniapp/business/BusinessFinancialController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/business/BusinessFinancialController.java
index a005a14a0..bd85052d5 100644
--- a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/business/BusinessFinancialController.java
+++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/business/BusinessFinancialController.java
@@ -11,9 +11,12 @@ import com.google.common.collect.ImmutableMap;
import com.jsowell.pile.dto.IndexQueryDTO;
import com.jsowell.pile.dto.MerchantOrderReportDTO;
import com.jsowell.pile.dto.ParkingCouponRecordQueryDTO;
+import com.jsowell.pile.dto.business.BusinessDailySettlementQueryDTO;
import com.jsowell.pile.dto.business.BusinessEfficiencyQueryDTO;
import com.jsowell.pile.dto.business.BusinessOperationAnalysisQueryDTO;
import com.jsowell.pile.dto.business.BusinessScaleQueryDTO;
+import com.jsowell.pile.dto.business.BusinessWorkbenchSummaryQueryDTO;
+import com.jsowell.pile.vo.uniapp.business.BusinessDailySettlementDetailVO;
import com.jsowell.pile.service.BusinessFinancialService;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyAnalysisVO;
@@ -21,6 +24,7 @@ import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyVO;
import com.jsowell.pile.vo.uniapp.business.BusinessGunEfficiencyAnalysisVO;
import com.jsowell.pile.vo.uniapp.business.BusinessOperationAnalysisVO;
import com.jsowell.pile.vo.uniapp.business.BusinessScaleVO;
+import com.jsowell.pile.vo.uniapp.business.BusinessWorkbenchSummaryVO;
import com.jsowell.pile.vo.web.MerchantOrderReportVO;
import com.jsowell.pile.vo.web.TimeDistributionVO;
import org.apache.commons.lang3.StringUtils;
@@ -47,6 +51,59 @@ public class BusinessFinancialController extends BaseController {
@Autowired
private OrderBasicInfoService orderBasicInfoService;
+ /**
+ * 查询工作台首页汇总
+ *
+ * @param dto 查询参数。前端可选传stationIdList,运营商和日期范围由后端按登录账号解析。
+ * @return 工作台首页汇总
+ */
+ @PostMapping("/workbenchSummary")
+ public RestApiResponse> getWorkbenchSummary(@RequestBody(required = false) BusinessWorkbenchSummaryQueryDTO dto) {
+ logger.info("查询工作台首页汇总 params:{}", JSONObject.toJSONString(dto));
+ RestApiResponse> response;
+ try {
+ BusinessWorkbenchSummaryVO result = businessFinancialService.getWorkbenchSummary(dto);
+ response = new RestApiResponse<>(result);
+ logger.info("查询工作台首页汇总成功 today:{}, stationCount:{}",
+ result.getToday(), result.getStationCount());
+ } catch (BusinessException e) {
+ logger.warn("查询工作台首页汇总业务异常 code:{}, message:{}", e.getCode(), e.getMessage(), e);
+ response = new RestApiResponse<>(e.getCode(), e.getMessage());
+ } catch (Exception e) {
+ logger.error("查询工作台首页汇总系统异常 params:{}", JSONObject.toJSONString(dto), e);
+ response = new RestApiResponse<>(e);
+ }
+ return response;
+ }
+
+ /**
+ * 查询结算日报详情
+ *
+ * @param dto 查询参数。前端传开始日期、结束日期,可选传stationIdList。
+ * @return 结算日报详情
+ */
+ @PostMapping("/dailySettlementDetail")
+ public RestApiResponse> getDailySettlementDetail(@RequestBody BusinessDailySettlementQueryDTO dto) {
+ logger.info("查询结算日报详情 params:{}", JSONObject.toJSONString(dto));
+ RestApiResponse> response;
+ try {
+ if (dto == null) {
+ throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR);
+ }
+ BusinessDailySettlementDetailVO result = businessFinancialService.getDailySettlementDetail(dto);
+ response = new RestApiResponse<>(result);
+ logger.info("查询结算日报详情成功 startTime:{}, endTime:{}",
+ dto.getStartTime(), dto.getEndTime());
+ } catch (BusinessException e) {
+ logger.warn("查询结算日报详情业务异常 code:{}, message:{}", e.getCode(), e.getMessage(), e);
+ response = new RestApiResponse<>(e.getCode(), e.getMessage());
+ } catch (Exception e) {
+ logger.error("查询结算日报详情系统异常 params:{}", JSONObject.toJSONString(dto), e);
+ response = new RestApiResponse<>(e);
+ }
+ return response;
+ }
+
/**
* 我的钱包界面查询接口
*
diff --git a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/customer/PayController.java b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/customer/PayController.java
index 9879e89f3..d374086e6 100644
--- a/jsowell-admin/src/main/java/com/jsowell/api/uniapp/customer/PayController.java
+++ b/jsowell-admin/src/main/java/com/jsowell/api/uniapp/customer/PayController.java
@@ -8,6 +8,7 @@ import com.jsowell.common.constant.Constants;
import com.jsowell.common.core.controller.BaseController;
import com.jsowell.common.core.redis.RedisCache;
import com.jsowell.common.enums.adapay.AdapayPayChannelEnum;
+import com.jsowell.common.enums.ykc.OrderPayModeEnum;
import com.jsowell.common.enums.ykc.ReturnCodeEnum;
import com.jsowell.common.exception.BusinessException;
import com.jsowell.common.response.RestApiResponse;
@@ -125,7 +126,8 @@ public class PayController extends BaseController {
if (StringUtils.isBlank(memberId)) {
throw new BusinessException(ReturnCodeEnum.CODE_TOKEN_ERROR);
}
- if (dto.getPayAmount() == null) {
+ if (dto.getPayAmount() == null
+ && !StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), dto.getPayMode())) {
throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR);
}
dto.setMemberId(memberId);
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 3a889dba1..f97a12988 100644
--- a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java
+++ b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java
@@ -1299,19 +1299,20 @@ public class OrderService {
}
}, executor);
- // 初始化充电金额
- BigDecimal chargeAmount = amount;
+ // 支付订单成功
OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode);
BigDecimal insuranceAmount = orderBasicInfo.getInsuranceAmount();
- if (insuranceAmount.compareTo(BigDecimal.ZERO) > 0) {
- // 如果用户支付了保险金额,则充电金额需将保险金额减去
+ // 主表 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(chargeAmount) // amount
- // .payMode(OrderPayModeEnum.PAYMENT_OF_WECHATPAY.getValue())
+ .payAmount(chargeAmount) // 充电金额(不含保险)
.payMode(payModel)
.acquirer(AcquirerEnum.ADAPAY.getValue())
.build();
diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java
index 33fbaf89a..13d3722da 100644
--- a/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java
+++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/index/indexController.java
@@ -272,6 +272,7 @@ public class indexController extends BaseController {
String totalChargingAmount = situation.getTotalChargingAmount();
String totalChargingDegree = situation.getTotalChargingDegree();
Long totalPileCount = totalPilesFuture.get();
+
// 总订单数从settle_order_report的GROUP BY结果中获取(charge_num汇总)
String totalOrderCountStr = situation.getTotalChargingQuantity();
Long totalOrderCount = (totalOrderCountStr != null && !totalOrderCountStr.isEmpty())
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-admin/src/test/java/com/jsowell/pile/util/ChargeAmountUtilsTest.java b/jsowell-admin/src/test/java/com/jsowell/pile/util/ChargeAmountUtilsTest.java
new file mode 100644
index 000000000..63ea0bd85
--- /dev/null
+++ b/jsowell-admin/src/test/java/com/jsowell/pile/util/ChargeAmountUtilsTest.java
@@ -0,0 +1,33 @@
+package com.jsowell.pile.util;
+
+import com.jsowell.common.constant.Constants;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ChargeAmountUtilsTest {
+
+ @Test
+ void resolveStartupAmount_shouldPreferFrontendAmount() {
+ BigDecimal amount = ChargeAmountUtils.resolveStartupAmount(
+ new BigDecimal("100"), new BigDecimal("300"));
+
+ assertEquals(new BigDecimal("100"), amount);
+ }
+
+ @Test
+ void resolveStartupAmount_shouldUseMemberDefaultWhenFrontendAmountIsNull() {
+ BigDecimal amount = ChargeAmountUtils.resolveStartupAmount(null, new BigDecimal("300"));
+
+ assertEquals(new BigDecimal("300"), amount);
+ }
+
+ @Test
+ void resolveStartupAmount_shouldUseCodeDefaultWhenOtherAmountsAreUnavailable() {
+ BigDecimal amount = ChargeAmountUtils.resolveStartupAmount(null, BigDecimal.ZERO);
+
+ assertEquals(Constants.BALANCE_PAY_MAX_AMOUNT_NEW, amount);
+ }
+}
diff --git a/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java b/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java
index 782d37e69..14d29d455 100644
--- a/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java
+++ b/jsowell-common/src/main/java/com/jsowell/common/constant/CacheConstants.java
@@ -78,6 +78,15 @@ public class CacheConstants {
// 余额支付订单退款锁
public static final String ORDER_BALANCE_PAYMENT_REFUND_LOCK = "order_balance_payment_refund_lock:";
+ // 订单支付凭证已校验标记
+ public static final String ORDER_PAYMENT_READY = "order_payment_ready:";
+
+ // 订单支付异常补偿锁
+ public static final String ORDER_PAYMENT_RECONCILE_LOCK = "order_payment_reconcile_lock:";
+
+ // 订单支付异常停机冷却标记
+ public static final String ORDER_PAYMENT_ANOMALY_STOP = "order_payment_anomaly_stop:";
+
// 平台测试员
public static final String PLATFORM_TESTER = "platform_tester:";
diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java
index c3dcef7ec..8a66600e2 100644
--- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java
+++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/ConfirmStartChargingRequestHandler.java
@@ -6,29 +6,28 @@ import com.jsowell.common.constant.Constants;
import com.jsowell.common.core.domain.ykc.YKCDataProtocol;
import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode;
import com.jsowell.common.enums.ykc.OrderPayModeEnum;
+import com.jsowell.common.enums.ykc.ReturnCodeEnum;
+import com.jsowell.common.exception.BusinessException;
import com.jsowell.common.util.BytesUtil;
import com.jsowell.common.util.StringUtils;
import com.jsowell.common.util.YKCUtils;
import com.jsowell.common.util.id.IdUtils;
-import com.jsowell.common.util.spring.SpringUtils;
import com.jsowell.netty.factory.YKCOperateFactory;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.dto.ConfirmStartChargingData;
import com.jsowell.pile.dto.GenerateOrderDTO;
import com.jsowell.pile.dto.SavePileMsgDTO;
import com.jsowell.pile.service.*;
+import com.jsowell.pile.util.ChargeAmountUtils;
import com.jsowell.pile.vo.base.ConfirmStartChargingMemberVO;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.CompletableFuture;
/**
* 充电桩主动申请启动充电 0x31
@@ -42,18 +41,9 @@ import java.util.concurrent.CompletableFuture;
public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler {
private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.REQUEST_START_CHARGING_CODE.getBytes());
- // 引入线程池
- private ThreadPoolTaskExecutor executor = SpringUtils.getBean("threadPoolTaskExecutor");
-
- @Autowired
- private PileAuthCardService pileAuthCardService;
-
@Autowired
private OrderBasicInfoService orderBasicInfoService;
- @Autowired
- private MemberPlateNumberRelationService memberPlateNumberRelationService;
-
@Autowired
private PileMsgRecordService pileMsgRecordService;
@@ -198,8 +188,14 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler {
.vinCode(vinCode)
.build();
- // 启动充电
- Map resultMap = this.confirmStartCharging(confirmStartChargingData);
+ Map resultMap;
+ try {
+ // 启动充电
+ resultMap = this.confirmStartCharging(confirmStartChargingData);
+ } catch (Exception e) {
+ log.error("充电桩主动申请充电处理失败, param:{}", JSON.toJSONString(confirmStartChargingData), e);
+ resultMap = buildFailureResult("05");
+ }
byte[] defeatReasonByteArr = Constants.zeroByteArray;
/**
@@ -279,6 +275,10 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler {
// 查询充电桩信息
PileBasicInfo pileBasicInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileSn);
+ if (pileBasicInfo == null) {
+ log.info("充电桩主动申请充电未查询到桩信息, pileSn:{}", pileSn);
+ return buildFailureResult("05");
+ }
// 查询是否有权限
ConfirmStartChargingMemberVO memberWalletVO = null;
@@ -295,50 +295,44 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler {
log.info("根据vin码:{},查询会员信息:{}", vinCode, JSON.toJSONString(memberWalletVO));
}
- String accountBalance = Constants.ZERO;
-
// 如果查询后memberWalletVO为null, 则标示无权限
if (memberWalletVO == null) {
- resultMap.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
- resultMap.put("accountBalance", accountBalance);
- resultMap.put("defeatReasonCode", "01");
log.info("无此会员权限");
- return resultMap;
- }
-
- // 如果不是平台测试员, 并且也不是站点白名单, 并且总余额小于等于0, 则提示余额不足
- if (!StringUtils.equals(Constants.ONE, memberWalletVO.getPlatformTesterFlag())
- && !StringUtils.equals(Constants.ONE, memberWalletVO.getStationWhiteListFlag())
- && memberWalletVO.getTotalBalance().compareTo(BigDecimal.ZERO) <= 0) {
- resultMap.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
- resultMap.put("accountBalance", accountBalance);
- resultMap.put("defeatReasonCode", "03");
- log.info("余额不足");
- return resultMap;
+ return buildFailureResult("01");
}
/*
鉴权通过, 生成订单并支付
*/
+ BigDecimal chargeAmount;
String payMode;
if (StringUtils.equals(Constants.ONE, memberWalletVO.getPlatformTesterFlag())
|| StringUtils.equals(Constants.ONE, memberWalletVO.getStationWhiteListFlag())) {
// 如果是平台测试员或者是站点白名单
- accountBalance = Constants.WHITELIST_DEFAULT_AMOUNT.toString();
+ chargeAmount = Constants.WHITELIST_DEFAULT_AMOUNT;
payMode = OrderPayModeEnum.PAYMENT_OF_WHITELIST.getValue();
} else {
- BigDecimal totalBalance = memberWalletVO.getTotalBalance();
- accountBalance = totalBalance.toString();
- if (totalBalance.compareTo(Constants.BALANCE_PAY_MAX_AMOUNT_NEW) > 0) {
- accountBalance = Constants.BALANCE_PAY_MAX_AMOUNT_NEW.toString();
+ chargeAmount = ChargeAmountUtils.resolveStartupAmount(
+ dto.getChargeAmount(), memberWalletVO.getDefaultChargeAmount());
+ BigDecimal totalBalance = memberWalletVO.getTotalBalance() == null
+ ? BigDecimal.ZERO
+ : memberWalletVO.getTotalBalance();
+ // 账户余额小于默认充电金额时,使用全部余额
+ if (totalBalance.compareTo(chargeAmount) < 0) {
+ log.info("余额不足默认金额,使用全部余额, memberId:{}, chargeAmount:{}, totalBalance:{}",
+ memberWalletVO.getMemberId(), chargeAmount, totalBalance);
+ chargeAmount = totalBalance;
+ }
+ if (!ChargeAmountUtils.isPositive(chargeAmount)) {
+ log.info("余额不足, memberId:{}, chargeAmount:{}, totalBalance:{}",
+ memberWalletVO.getMemberId(), chargeAmount, totalBalance);
+ return buildFailureResult("03");
}
payMode = OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue();
}
// 生成交易流水号, 传递给订单使用
String transactionCode = IdUtils.generateTransactionCode(pileSn, connectorCode);
- resultMap.put("transactionCode", transactionCode);
- resultMap.put("accountBalance", accountBalance);
dto.setPileSn(pileSn);
if (StringUtils.isNotBlank(memberWalletVO.getPlateNumber())) {
@@ -346,34 +340,40 @@ public class ConfirmStartChargingRequestHandler extends AbstractYkcHandler {
}
dto.setConnectorCode(connectorCode);
dto.setTransactionCode(transactionCode);
- if (memberWalletVO.getDefaultChargeAmount() != null && memberWalletVO.getDefaultChargeAmount().compareTo(BigDecimal.ZERO) > 0) {
- dto.setChargeAmount(memberWalletVO.getDefaultChargeAmount());
- }else {
- dto.setChargeAmount(new BigDecimal(accountBalance));
- }
+ dto.setChargeAmount(chargeAmount);
dto.setPayMode(payMode);
dto.setStartMode(StringUtils.equals("01", startMode) ? "2" : "5");
dto.setMemberId(memberWalletVO.getMemberId());
dto.setMerchantId(pileBasicInfo.getMerchantId() + "");
- // 校验充电桩相关的信息
try {
- orderBasicInfoService.checkPileInfoForEV(dto);
+ // 必须等订单创建并完成余额支付后,才能向桩端返回鉴权成功。
+ Map map = orderBasicInfoService.generateOrderByCardV2(dto);
+ if (map == null) {
+ throw new BusinessException(ReturnCodeEnum.CODE_GENERATE_ORDER_ERROR);
+ }
+ } catch (BusinessException e) {
+ String defeatReasonCode = StringUtils.equals(
+ ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT.getValue(), e.getCode()) ? "03" : "05";
+ log.error("充电桩主动申请充电生成订单失败, memberId:{}, chargeAmount:{}",
+ memberWalletVO.getMemberId(), chargeAmount, e);
+ return buildFailureResult(defeatReasonCode);
} catch (Exception e) {
- // 校验充电桩报错,返回校验值 05-桩停用
- resultMap.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
- resultMap.put("accountBalance", accountBalance);
- resultMap.put("defeatReasonCode", "05");
+ log.error("充电桩主动申请充电生成订单失败, memberId:{}, chargeAmount:{}",
+ memberWalletVO.getMemberId(), chargeAmount, e);
+ return buildFailureResult("05");
}
- // 异步创建订单
- CompletableFuture.runAsync(() -> {
- try {
- Map map = orderBasicInfoService.generateOrderByCardV2(dto);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }, executor);
+ resultMap.put("transactionCode", transactionCode);
+ resultMap.put("accountBalance", chargeAmount.toString());
+ return resultMap;
+ }
+
+ private Map buildFailureResult(String defeatReasonCode) {
+ Map resultMap = new HashMap<>();
+ resultMap.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
+ resultMap.put("accountBalance", Constants.ZERO);
+ resultMap.put("defeatReasonCode", defeatReasonCode);
return resultMap;
}
diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/PileApplyMergeChargeHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/PileApplyMergeChargeHandler.java
index debe0f658..9e59c191a 100644
--- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/PileApplyMergeChargeHandler.java
+++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/PileApplyMergeChargeHandler.java
@@ -138,6 +138,9 @@ public class PileApplyMergeChargeHandler extends AbstractYkcHandler{
log.info("桩号:{}, 并充订单鉴权成功, 结果map:{}", pileSn, map);
} catch (Exception e) {
log.error("桩号:{}, 并充订单鉴权失败, ", pileSn, e);
+ map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
+ map.put("accountBalance", Constants.ZERO);
+ map.put("defeatReasonCode", "05");
}
String transactionCode = map.get("transactionCode");
// 如果交易流水号不等于Constants.ILLEGAL_TRANSACTION_CODE, 表示鉴权通过
diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/RemoteStartChargingRequestHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/RemoteStartChargingRequestHandler.java
index e2cdd1c57..d982a8866 100644
--- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/RemoteStartChargingRequestHandler.java
+++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/RemoteStartChargingRequestHandler.java
@@ -1,5 +1,7 @@
package com.jsowell.netty.handler.yunkuaichong;
+import com.jsowell.common.protocol.SyncPromise;
+import com.jsowell.common.util.RpcUtil;
import com.jsowell.common.constant.Constants;
import com.jsowell.common.constant.mq.ThirdPartyRabbitConstants;
import com.jsowell.common.core.domain.ykc.YKCDataProtocol;
@@ -20,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
+import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
/**
@@ -101,6 +104,17 @@ public class RemoteStartChargingRequestHandler extends AbstractYkcHandler {
String failedReasonMsg = ChargingFailedReasonEnum.getMsgByCode(Integer.parseInt(failedReason, 16));
log.info("远程启动充电命令回复-交易流水号:{}, 桩编码:{}, 枪号:{}, 启动结果(00-失败, 01-成功):{}, 失败原因:{}", transactionCode, pileSn, connectorCode, startResult, failedReasonMsg);
+
+ // 唤醒 pushStartChargingCommand 中等待应答的线程
+ String responseKey = transactionCode + "_0x33";
+ SyncPromise syncPromise = RpcUtil.getSyncPromiseMap().get(responseKey);
+ if (syncPromise != null) {
+ String resultMsg = StringUtils.equals(startResult, Constants.ONE)
+ ? "成功"
+ : "失败(" + failedReasonMsg + ")";
+ syncPromise.setRpcResult(resultMsg.getBytes(StandardCharsets.UTF_8));
+ syncPromise.wake();
+ }
if (StringUtils.equals(startResult, Constants.DOUBLE_ZERO)) {
// 启动失败 2025年4月2日16点39分修改逻辑:启动失败后不退款, 使用支付完成未启动定时任务退款
// orderBasicInfoService.chargingPileFailedToStart(transactionCode, failedReasonMsg);
diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/UploadRealTimeMonitorHandler.java b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/UploadRealTimeMonitorHandler.java
index f7ace0826..494f080b8 100644
--- a/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/UploadRealTimeMonitorHandler.java
+++ b/jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/UploadRealTimeMonitorHandler.java
@@ -19,6 +19,7 @@ import com.jsowell.netty.factory.YKCOperateFactory;
import com.jsowell.pile.domain.OrderBasicInfo;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileBasicInfoService;
+import com.jsowell.pile.vo.web.OrderPaymentCheckResult;
import com.jsowell.thirdparty.common.CommonService;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
@@ -297,6 +298,22 @@ public class UploadRealTimeMonitorHandler extends AbstractYkcHandler {
// 查询数据库中该订单当前信息
OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByTransactionCode(transactionCode);
if (Objects.nonNull(orderInfo)) {
+ OrderPaymentCheckResult paymentCheck = orderBasicInfoService.checkAndRepairOrderPayment(orderInfo);
+ boolean paymentReady = paymentCheck.isOrderPaymentReady();
+ if (!paymentCheck.isCheckCompleted()) {
+ // 查询依赖暂时异常时只保留实时数据,等待后续补偿重试,不能据此判定订单未支付。
+ saveRedisFlag = true;
+ log.error("实时监控支付凭证校验未完成,保留实时数据且不变更订单状态, orderCode:{}, reason:{}",
+ orderInfo.getOrderCode(), paymentCheck.getReason());
+ } else if (!paymentReady) {
+ // 实时监控只能保存原始数据并隔离订单,不能把待支付订单改成已支付。
+ saveRedisFlag = true;
+ orderBasicInfoService.handleOrderPaymentAnomaly(orderInfo,
+ paymentCheck.getReason(), null, true);
+ log.warn("实时监控发现支付凭证无效, orderCode:{}, reason:{}",
+ orderInfo.getOrderCode(), paymentCheck.getReason());
+ }
+ if (paymentCheck.isCheckCompleted() && paymentReady) {
// 保存此订单的soc
YKCUtils.saveSOC(transactionCode, realTimeMonitorData.getSOC());
@@ -315,11 +332,6 @@ public class UploadRealTimeMonitorHandler extends AbstractYkcHandler {
orderInfo.setOrderStatus(OrderStatusEnum.IN_THE_CHARGING.getValue());
}
- if (StringUtils.equals(orderInfo.getPayStatus(), OrderPayStatusEnum.unpaid.getValue())) {
- // 如果发现该订单的支付状态为 0-待支付,将该订单支付状态改为 1-支付完成
- orderInfo.setPayStatus(OrderPayStatusEnum.paid.getValue());
- }
-
// 如果原来没有开始充电时间就保存当前时间为开始充电时间
if (orderInfo.getChargeStartTime() == null) {
updateFlag = true;
@@ -365,6 +377,7 @@ public class UploadRealTimeMonitorHandler extends AbstractYkcHandler {
log.error("异步绑定车辆停车优惠券error", e);
}
}, executor);
+ }
}
// 充电时保存实时数据到redis
diff --git a/jsowell-netty/src/main/java/com/jsowell/netty/service/rabbitmq/PileRabbitListener.java b/jsowell-netty/src/main/java/com/jsowell/netty/service/rabbitmq/PileRabbitListener.java
index 60f161393..67d289af3 100644
--- a/jsowell-netty/src/main/java/com/jsowell/netty/service/rabbitmq/PileRabbitListener.java
+++ b/jsowell-netty/src/main/java/com/jsowell/netty/service/rabbitmq/PileRabbitListener.java
@@ -30,6 +30,7 @@ import com.jsowell.pile.service.PileBasicInfoService;
import com.jsowell.pile.service.PileMsgRecordService;
import com.jsowell.pile.service.YKCPushCommandService;
import com.jsowell.thirdparty.common.CommonService;
+import com.jsowell.pile.vo.web.OrderPaymentCheckResult;
import com.rabbitmq.client.Channel;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
@@ -358,6 +359,22 @@ public class PileRabbitListener {
// 查询数据库中该订单当前信息
OrderBasicInfo orderInfo = orderBasicInfoService.getOrderInfoByTransactionCode(transactionCode);
if (Objects.nonNull(orderInfo)) {
+ OrderPaymentCheckResult paymentCheck = orderBasicInfoService.checkAndRepairOrderPayment(orderInfo);
+ boolean paymentReady = paymentCheck.isOrderPaymentReady();
+ if (!paymentCheck.isCheckCompleted()) {
+ // 查询依赖暂时异常时只保留实时数据,等待后续补偿重试,不能据此判定订单未支付。
+ saveRedisFlag = true;
+ log.error("实时监控支付凭证校验未完成,保留实时数据且不变更订单状态, orderCode:{}, reason:{}",
+ orderInfo.getOrderCode(), paymentCheck.getReason());
+ } else if (!paymentReady) {
+ // 实时监控不能把待支付订单伪造成已支付,只保留实时数据并执行幂等停机。
+ saveRedisFlag = true;
+ orderBasicInfoService.handleOrderPaymentAnomaly(orderInfo,
+ paymentCheck.getReason(), null, true);
+ log.warn("实时监控发现支付凭证无效, orderCode:{}, reason:{}",
+ orderInfo.getOrderCode(), paymentCheck.getReason());
+ }
+ if (paymentCheck.isCheckCompleted() && paymentReady) {
if (StringUtils.equals(orderInfo.getOrderStatus(), OrderStatusEnum.ORDER_COMPLETE.getValue())
|| StringUtils.equals(orderInfo.getOrderStatus(), OrderStatusEnum.STAY_SETTLEMENT.getValue())) {
// 在订单状态为 订单完成或待结算,不保存
@@ -373,11 +390,6 @@ public class PileRabbitListener {
orderInfo.setOrderStatus(OrderStatusEnum.IN_THE_CHARGING.getValue());
}
- if (StringUtils.equals(orderInfo.getPayStatus(), OrderPayStatusEnum.unpaid.getValue())) {
- // 如果发现该订单的支付状态为 0-待支付,将该订单支付状态改为 1-支付完成
- orderInfo.setPayStatus(OrderPayStatusEnum.paid.getValue());
- }
-
// 如果原来没有开始充电时间就保存当前时间为开始充电时间
if (orderInfo.getChargeStartTime() == null) {
updateFlag = true;
@@ -387,6 +399,7 @@ public class PileRabbitListener {
if (updateFlag) {
orderBasicInfoService.updateOrderBasicInfo(orderInfo);
}
+ }
}
// 充电时保存实时数据到redis
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessDailySettlementQueryDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessDailySettlementQueryDTO.java
new file mode 100644
index 000000000..7ffdbe5b2
--- /dev/null
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessDailySettlementQueryDTO.java
@@ -0,0 +1,37 @@
+package com.jsowell.pile.dto.business;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 结算日报详情查询DTO
+ *
+ * @author zhangziao
+ * @date 2026/6/15
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class BusinessDailySettlementQueryDTO {
+
+ /**
+ * 开始日期,格式:yyyy-MM-dd
+ */
+ private String startTime;
+
+ /**
+ * 结束日期,格式:yyyy-MM-dd
+ */
+ private String endTime;
+
+ /**
+ * 站点id列表。运营商账号传当前账号下可见站点id。
+ */
+ private List stationIdList;
+
+}
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessWorkbenchSummaryQueryDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessWorkbenchSummaryQueryDTO.java
new file mode 100644
index 000000000..dafa49b84
--- /dev/null
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/business/BusinessWorkbenchSummaryQueryDTO.java
@@ -0,0 +1,27 @@
+package com.jsowell.pile.dto.business;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 工作台首页汇总查询DTO
+ *
+ * @author zhangziao
+ * @date 2026/6/15
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class BusinessWorkbenchSummaryQueryDTO {
+
+ /**
+ * 站点id列表。可选,不传时按当前登录账号权限查询。
+ */
+ private List stationIdList;
+
+}
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java
index f51858ed3..d028e4d87 100644
--- a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/OrderBasicInfoMapper.java
@@ -264,6 +264,12 @@ public interface OrderBasicInfoMapper {
@Param("orderStatus") String orderStatus,
@Param("payStatus") String payStatus);
+ /**
+ * 查询需要进行支付凭证补偿的余额支付订单。
+ */
+ List queryOrdersForPaymentReconcile(@Param("startTime") String startTime,
+ @Param("endTime") String endTime);
+
/**
* 个人桩查询充电数据
*
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/BusinessFinancialService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/BusinessFinancialService.java
index 87fb249f6..683d24c6e 100644
--- a/jsowell-pile/src/main/java/com/jsowell/pile/service/BusinessFinancialService.java
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/BusinessFinancialService.java
@@ -3,20 +3,40 @@ package com.jsowell.pile.service;
import com.jsowell.common.core.page.PageResponse;
import com.jsowell.pile.dto.MerchantOrderReportDTO;
import com.jsowell.pile.dto.ParkingCouponRecordQueryDTO;
+import com.jsowell.pile.dto.business.BusinessDailySettlementQueryDTO;
import com.jsowell.pile.dto.business.BusinessOperationAnalysisQueryDTO;
import com.jsowell.pile.dto.business.BusinessEfficiencyQueryDTO;
import com.jsowell.pile.dto.business.BusinessScaleQueryDTO;
+import com.jsowell.pile.dto.business.BusinessWorkbenchSummaryQueryDTO;
+import com.jsowell.pile.vo.uniapp.business.BusinessDailySettlementDetailVO;
import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyAnalysisVO;
import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyVO;
import com.jsowell.pile.vo.uniapp.business.BusinessGunEfficiencyAnalysisVO;
import com.jsowell.pile.vo.uniapp.business.BusinessOperationAnalysisVO;
import com.jsowell.pile.vo.uniapp.business.BusinessScaleVO;
+import com.jsowell.pile.vo.uniapp.business.BusinessWorkbenchSummaryVO;
import com.jsowell.pile.vo.web.MerchantOrderReportVO;
import com.jsowell.pile.vo.web.ParkingCouponRecordVO;
public interface BusinessFinancialService {
MerchantOrderReportVO getMyWallet(MerchantOrderReportDTO dto);
+ /**
+ * 查询工作台首页汇总
+ *
+ * @param dto 查询条件
+ * @return 工作台首页汇总
+ */
+ BusinessWorkbenchSummaryVO getWorkbenchSummary(BusinessWorkbenchSummaryQueryDTO dto);
+
+ /**
+ * 查询结算日报详情
+ *
+ * @param dto 查询条件
+ * @return 结算日报详情
+ */
+ BusinessDailySettlementDetailVO getDailySettlementDetail(BusinessDailySettlementQueryDTO dto);
+
/**
* 分页查询停车优免记录
*
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/OrderBasicInfoService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/OrderBasicInfoService.java
index d9094e890..150bfddb5 100644
--- a/jsowell-pile/src/main/java/com/jsowell/pile/service/OrderBasicInfoService.java
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/OrderBasicInfoService.java
@@ -339,6 +339,33 @@ public interface OrderBasicInfoService{
*/
void closeStartFailedOrder(String startTime, String endTime);
+ /**
+ * 校验订单支付凭证,并在底层支付流水完整时修复订单主表支付字段。
+ *
+ * @param orderBasicInfo 订单信息
+ * @return 支付凭证校验结果
+ */
+ OrderPaymentCheckResult checkAndRepairOrderPayment(OrderBasicInfo orderBasicInfo);
+
+ /**
+ * 处理支付凭证异常订单。该方法只会将订单标记为异常,不会走正常结算或退款流程。
+ *
+ * @param orderBasicInfo 订单信息
+ * @param reason 异常原因
+ * @param data 原始交易记录报文,可为空
+ * @param stopCharging 是否需要向充电桩发送停机指令
+ */
+ void handleOrderPaymentAnomaly(OrderBasicInfo orderBasicInfo, String reason,
+ TransactionRecordsData data, boolean stopCharging);
+
+ /**
+ * 扫描并补偿支付凭证异常的余额支付订单。
+ *
+ * @param startTime 开始时间
+ * @param endTime 结束时间
+ */
+ void reconcileChargingOrderPayments(String startTime, String endTime);
+
List getListByOrderCodes(List orderCodes);
/**
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java
index a896626e2..05f32d311 100644
--- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/BusinessFinancialServiceImpl.java
@@ -12,14 +12,18 @@ import com.jsowell.common.enums.ykc.ReturnCodeEnum;
import com.jsowell.common.exception.BusinessException;
import com.jsowell.pile.dto.MerchantOrderReportDTO;
import com.jsowell.pile.dto.ParkingCouponRecordQueryDTO;
+import com.jsowell.pile.dto.QueryOrderDTO;
+import com.jsowell.pile.dto.business.BusinessDailySettlementQueryDTO;
import com.jsowell.pile.dto.business.BusinessEfficiencyQueryDTO;
import com.jsowell.pile.dto.business.BusinessOperationAnalysisQueryDTO;
import com.jsowell.pile.dto.business.BusinessOperationDateRangeDTO;
import com.jsowell.pile.dto.business.BusinessOperationSummaryDTO;
import com.jsowell.pile.dto.business.BusinessScaleQueryDTO;
+import com.jsowell.pile.dto.business.BusinessWorkbenchSummaryQueryDTO;
import com.jsowell.pile.domain.PileConnectorInfo;
import com.jsowell.pile.domain.PileStationInfo;
import com.jsowell.pile.domain.SettleOrderReport;
+import com.jsowell.pile.mapper.OrderBasicInfoMapper;
import com.jsowell.pile.service.BusinessFinancialService;
import com.jsowell.pile.service.CarCouponRecordService;
import com.jsowell.pile.service.ClearingWithdrawInfoService;
@@ -28,6 +32,7 @@ import com.jsowell.pile.service.PileStationInfoService;
import com.jsowell.pile.service.SettleOrderReportService;
import com.jsowell.pile.util.UserUtils;
import com.jsowell.pile.vo.base.ConnectorInfoVO;
+import com.jsowell.pile.vo.uniapp.business.BusinessDailySettlementDetailVO;
import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyAnalysisVO;
import com.jsowell.pile.vo.uniapp.business.BusinessGunEfficiencyAnalysisVO;
import com.jsowell.pile.vo.uniapp.business.BusinessEfficiencyVO;
@@ -38,7 +43,9 @@ import com.jsowell.pile.vo.uniapp.business.BusinessScaleChartVO;
import com.jsowell.pile.vo.uniapp.business.BusinessScaleMetricVO;
import com.jsowell.pile.vo.uniapp.business.BusinessScaleVO;
import com.jsowell.pile.vo.uniapp.business.BusinessStationRankVO;
+import com.jsowell.pile.vo.uniapp.business.BusinessWorkbenchSummaryVO;
import com.jsowell.pile.vo.web.MerchantOrderReportVO;
+import com.jsowell.pile.vo.web.OrderStatisticsVO;
import com.jsowell.pile.vo.web.ParkingCouponRecordVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -52,6 +59,7 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -85,6 +93,18 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService {
private static final String EFFICIENCY_METRIC_GUN_AVG_ELECTRICITY = "GUN_AVG_ELECTRICITY";
private static final String EFFICIENCY_METRIC_POWER_UTILIZATION = "POWER_UTILIZATION";
+ private static final String SETTLEMENT_ITEM_SERVICE_FEE = "CHARGE_SERVICE_FEE";
+ private static final String SETTLEMENT_ITEM_ELECTRICITY_FEE = "CHARGE_ELECTRICITY_FEE";
+ private static final String SETTLEMENT_ITEM_PLATFORM_SUBSIDY = "PLATFORM_SERVICE_SUBSIDY";
+ private static final String SETTLEMENT_ITEM_MERCHANT_PLATFORM_FEE = "MERCHANT_PLATFORM_SERVICE_FEE";
+ private static final String WORKBENCH_CARD_ORDER_MANAGEMENT = "ORDER_MANAGEMENT";
+ private static final String WORKBENCH_CARD_BUSINESS_ADVISER = "BUSINESS_ADVISER";
+ private static final String WORKBENCH_CARD_MY_WALLET = "MY_WALLET";
+ private static final String WORKBENCH_CARD_DAILY_SETTLEMENT = "DAILY_SETTLEMENT";
+ private static final String WORKBENCH_CARD_PARKING_DISCOUNT = "PARKING_DISCOUNT";
+ private static final int STATION_QUERY_BATCH_SIZE = 500;
+ private static final int DEFAULT_DECIMAL_SCALE = 2;
+
/**
* 财务查询通用线程池(支持多方法并行查询)
* 核心线程数4:覆盖经营规模(2路)、我的钱包(3路)、经营分析(2路)、枪均效率(2路)等场景
@@ -114,6 +134,106 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService {
@Autowired
private PileConnectorInfoService pileConnectorInfoService;
+ @Autowired
+ private OrderBasicInfoMapper orderBasicInfoMapper;
+
+ /**
+ * 查询工作台首页汇总
+ *
+ * @param dto 查询条件
+ * @return 工作台首页汇总
+ */
+ @Override
+ public BusinessWorkbenchSummaryVO getWorkbenchSummary(BusinessWorkbenchSummaryQueryDTO dto) {
+ if (dto == null) {
+ dto = new BusinessWorkbenchSummaryQueryDTO();
+ }
+
+ LocalDate today = LocalDate.now();
+ String todayDate = today.toString();
+ String recentStartTime = today.minusDays(6).toString();
+ String recentEndTime = todayDate;
+
+ FinancialScope scope = resolveFinancialScope(dto.getStationIdList());
+
+ CompletableFuture todayOrderCountFuture = CompletableFuture.supplyAsync(
+ () -> countTodayOrderCount(scope.getStationIdList(), todayDate), FINANCIAL_THREAD_POOL);
+ CompletableFuture> recentReportsFuture = CompletableFuture.supplyAsync(
+ () -> queryRawReports(scope.getStationIdList(), recentStartTime, recentEndTime), FINANCIAL_THREAD_POOL);
+
+ List recentReports = recentReportsFuture.join();
+ ReportSummary recentSummary = buildReportSummary(recentReports);
+ ReportSummary todaySummary = buildReportSummary(recentReports, todayDate);
+ Long todayOrderCount = todayOrderCountFuture.join();
+ Integer pendingParkingAppealCount = countPendingParkingAppeal(scope.getStationIdList());
+
+ List cardList = new ArrayList<>();
+ cardList.add(buildWorkbenchCard(WORKBENCH_CARD_ORDER_MANAGEMENT, "订单管理", "今日订单量",
+ BigDecimal.valueOf(todayOrderCount), "单"));
+ cardList.add(buildWorkbenchCard(WORKBENCH_CARD_BUSINESS_ADVISER, "经营参谋", "近7天净现金流",
+ formatMoney(recentSummary.getOrderTotalAmount()), "元"));
+ cardList.add(buildWorkbenchCard(WORKBENCH_CARD_MY_WALLET, "我的钱包", "今日到账",
+ formatMoney(todaySummary.getArrivalAmount()), "元"));
+ cardList.add(buildWorkbenchCard(WORKBENCH_CARD_DAILY_SETTLEMENT, "结算日报", "近7天净现金流",
+ formatMoney(recentSummary.getSettlementAmount()), "元"));
+ cardList.add(buildWorkbenchCard(WORKBENCH_CARD_PARKING_DISCOUNT, "停车减免", "待处理申诉",
+ BigDecimal.valueOf(pendingParkingAppealCount), "条"));
+
+ return BusinessWorkbenchSummaryVO.builder()
+ .today(todayDate)
+ .recentStartTime(recentStartTime)
+ .recentEndTime(recentEndTime)
+ .merchantIdList(scope.getMerchantIdList())
+ .stationIdList(scope.getStationIdList())
+ .stationCount(scope.getStationIdList() != null ? scope.getStationIdList().size() : null)
+ .todayOrderCount(todayOrderCount)
+ .recentNetCashFlow(formatMoney(recentSummary.getOrderTotalAmount()))
+ .todayArrivalAmount(formatMoney(todaySummary.getArrivalAmount()))
+ .recentSettlementNetCashFlow(formatMoney(recentSummary.getSettlementAmount()))
+ .pendingParkingAppealCount(pendingParkingAppealCount)
+ .cardList(cardList)
+ .build();
+ }
+
+ /**
+ * 查询结算日报详情
+ *
+ * @param dto 查询条件
+ * @return 结算日报详情
+ */
+ @Override
+ public BusinessDailySettlementDetailVO getDailySettlementDetail(BusinessDailySettlementQueryDTO dto) {
+ validateDailySettlementQuery(dto);
+
+ FinancialScope scope = resolveFinancialScope(dto.getStationIdList());
+ List reportList = queryRawReports(scope.getStationIdList(), dto.getStartTime(), dto.getEndTime());
+ ReportSummary summary = buildReportSummary(reportList);
+
+ List incomeItems = new ArrayList<>();
+ incomeItems.add(buildSettlementItem(SETTLEMENT_ITEM_SERVICE_FEE, "充电服务费(元)", summary.getChargeServiceFee()));
+ incomeItems.add(buildSettlementItem(SETTLEMENT_ITEM_ELECTRICITY_FEE, "充电电费(元)", summary.getChargeElectricityFee()));
+
+ // List expenseItems = new ArrayList<>();
+ // expenseItems.add(buildSettlementItem(SETTLEMENT_ITEM_MERCHANT_PLATFORM_FEE, "商户给平台的服务费(元)", merchantPlatformServiceFee));
+
+ return BusinessDailySettlementDetailVO.builder()
+ .startTime(dto.getStartTime())
+ .endTime(dto.getEndTime())
+ .merchantIdList(scope.getMerchantIdList())
+ .stationCount(scope.getStationIdList() != null ? scope.getStationIdList().size() : null)
+ .settlementAmount(formatMoney(summary.getSettlementAmount()))
+ .totalOrderCount(BigDecimalUtils.normalize(summary.getTotalOrderCount()))
+ .orderTotalAmount(formatMoney(summary.getOrderTotalAmount()))
+ .orderElectricity(formatQuantity(summary.getOrderElectricity()))
+ .chargeServiceFee(formatMoney(summary.getChargeServiceFee()))
+ .chargeElectricityFee(formatMoney(summary.getChargeElectricityFee()))
+ .tradeAmount(formatMoney(summary.getTradeAmount()))
+ .tradeFee(formatMoney(summary.getTradeFee()))
+ .incomeItems(incomeItems)
+ // .expenseItems(expenseItems)
+ .build();
+ }
+
/**
* 我的钱包查询
* 优化:订单报表DB、汇付余额HTTP、提现金额DB 三个独立数据源并行查询
@@ -467,6 +587,253 @@ public class BusinessFinancialServiceImpl implements BusinessFinancialService {
return authorizedStationIds;
}
+ /**
+ * 校验结算日报详情查询条件
+ *
+ * @param dto 查询条件
+ */
+ private void validateDailySettlementQuery(BusinessDailySettlementQueryDTO dto) {
+ if (dto == null
+ || StringUtils.isBlank(dto.getStartTime())
+ || StringUtils.isBlank(dto.getEndTime())) {
+ throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR);
+ }
+ try {
+ LocalDate startDate = LocalDate.parse(dto.getStartTime());
+ LocalDate endDate = LocalDate.parse(dto.getEndTime());
+ if (endDate.isBefore(startDate)) {
+ throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR);
+ }
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR);
+ }
+ }
+
+ /**
+ * 解析财务接口通用账号和站点范围
+ */
+ private FinancialScope resolveFinancialScope(List stationIdList) {
+ List merchantIdList = UserUtils.getFirstMerchantIdList();
+ List queryStationIds = stationIdList;
+ if (CollectionUtils.isEmpty(queryStationIds)) {
+ if (CollectionUtils.isEmpty(merchantIdList)) {
+ return new FinancialScope(merchantIdList, Collections.emptyList());
+ }
+ queryStationIds = pileStationInfoService.getStationIdsByMerchantIds(merchantIdList);
+ if (queryStationIds == null) {
+ queryStationIds = Collections.emptyList();
+ }
+ }
+ return new FinancialScope(merchantIdList, resolveStationIds(queryStationIds));
+ }
+
+ /**
+ * 统计今日订单量
+ */
+ private Long countTodayOrderCount(List stationIdList, String today) {
+ if (stationIdList != null && stationIdList.isEmpty()) {
+ return 0L;
+ }
+ String startTime = today + " 00:00:00";
+ String endTime = today + " 23:59:59";
+ if (stationIdList == null || stationIdList.size() <= STATION_QUERY_BATCH_SIZE) {
+ QueryOrderDTO queryOrderDTO = QueryOrderDTO.builder()
+ .stationIdList(stationIdList)
+ .startTime(startTime)
+ .endTime(endTime)
+ .build();
+ OrderStatisticsVO statistics = orderBasicInfoMapper.countBusinessOrderStatistics(queryOrderDTO);
+ return statistics != null && statistics.getOrderCount() != null ? statistics.getOrderCount() : 0L;
+ }
+
+ long orderCount = 0L;
+ for (int i = 0; i < stationIdList.size(); i += STATION_QUERY_BATCH_SIZE) {
+ int endIndex = Math.min(i + STATION_QUERY_BATCH_SIZE, stationIdList.size());
+ QueryOrderDTO queryOrderDTO = QueryOrderDTO.builder()
+ .stationIdList(stationIdList.subList(i, endIndex))
+ .startTime(startTime)
+ .endTime(endTime)
+ .build();
+ OrderStatisticsVO statistics = orderBasicInfoMapper.countBusinessOrderStatistics(queryOrderDTO);
+ if (statistics != null && statistics.getOrderCount() != null) {
+ orderCount += statistics.getOrderCount();
+ }
+ }
+ return orderCount;
+ }
+
+ /**
+ * 统计停车减免待处理申诉数
+ */
+ private Integer countPendingParkingAppeal(List stationIdList) {
+ if (stationIdList != null && stationIdList.isEmpty()) {
+ return 0;
+ }
+ // 当前停车减免模块暂无独立申诉状态数据源,预留工作台展示字段。
+ return 0;
+ }
+
+ /**
+ * 构建报表汇总数据
+ */
+ private ReportSummary buildReportSummary(List reportList) {
+ return buildReportSummary(reportList, null);
+ }
+
+ /**
+ * 构建指定日期的报表汇总数据
+ */
+ private ReportSummary buildReportSummary(List reportList, String tradeDate) {
+ ReportSummary summary = new ReportSummary();
+ if (CollectionUtils.isEmpty(reportList)) {
+ return summary;
+ }
+ for (SettleOrderReport report : reportList) {
+ if (StringUtils.isNotBlank(tradeDate) && !StringUtils.equals(tradeDate, report.getTradeDate())) {
+ continue;
+ }
+ summary.add(report);
+ }
+ return summary;
+ }
+
+ /**
+ * 构建工作台卡片
+ */
+ private BusinessWorkbenchSummaryVO.WorkbenchCard buildWorkbenchCard(String cardCode,
+ String cardName,
+ String metricName,
+ BigDecimal metricValue,
+ String unit) {
+ return BusinessWorkbenchSummaryVO.WorkbenchCard.builder()
+ .cardCode(cardCode)
+ .cardName(cardName)
+ .metricName(metricName)
+ .metricValue(metricValue)
+ .unit(unit)
+ .build();
+ }
+
+ /**
+ * 构建结算项
+ */
+ private BusinessDailySettlementDetailVO.SettlementItem buildSettlementItem(String itemCode,
+ String itemName,
+ BigDecimal amount) {
+ return BusinessDailySettlementDetailVO.SettlementItem.builder()
+ .itemCode(itemCode)
+ .itemName(itemName)
+ .unit("元")
+ .amount(formatMoney(amount))
+ .build();
+ }
+
+ /**
+ * 金额统一保留两位小数
+ */
+ private BigDecimal formatMoney(BigDecimal amount) {
+ return formatDecimal(amount);
+ }
+
+ /**
+ * 数量统一保留两位小数
+ */
+ private BigDecimal formatQuantity(BigDecimal amount) {
+ return formatDecimal(amount);
+ }
+
+ /**
+ * 数值统一保留两位小数
+ */
+ private BigDecimal formatDecimal(BigDecimal amount) {
+ return BigDecimalUtils.nullToZero(amount).setScale(DEFAULT_DECIMAL_SCALE, RoundingMode.HALF_UP);
+ }
+
+ private static class FinancialScope {
+ private final List merchantIdList;
+ private final List stationIdList;
+
+ private FinancialScope(List merchantIdList, List stationIdList) {
+ this.merchantIdList = merchantIdList;
+ this.stationIdList = stationIdList;
+ }
+
+ private List getMerchantIdList() {
+ return merchantIdList;
+ }
+
+ private List getStationIdList() {
+ return stationIdList;
+ }
+ }
+
+ private static class ReportSummary {
+ private BigDecimal orderTotalAmount = BigDecimal.ZERO;
+ private BigDecimal totalOrderCount = BigDecimal.ZERO;
+ private BigDecimal orderElectricity = BigDecimal.ZERO;
+ private BigDecimal chargeServiceFee = BigDecimal.ZERO;
+ private BigDecimal chargeElectricityFee = BigDecimal.ZERO;
+ private BigDecimal settlementAmount = BigDecimal.ZERO;
+ private BigDecimal tradeAmount = BigDecimal.ZERO;
+ private BigDecimal tradeFee = BigDecimal.ZERO;
+ private BigDecimal arrivalAmount = BigDecimal.ZERO;
+
+ private void add(SettleOrderReport report) {
+ BigDecimal reportTotalAmount = BigDecimalUtils.nullToZero(report.getTotalAmount());
+ BigDecimal reportSettleAmount = report.getSettleAmount() != null
+ ? report.getSettleAmount() : reportTotalAmount;
+
+ orderTotalAmount = orderTotalAmount.add(reportTotalAmount);
+ totalOrderCount = totalOrderCount.add(BigDecimalUtils.parseOrZero(report.getChargeNum()));
+ orderElectricity = orderElectricity.add(BigDecimalUtils.nullToZero(report.getUseElectricity()));
+ chargeServiceFee = chargeServiceFee.add(BigDecimalUtils.nullToZero(report.getServiceAmount()));
+ chargeElectricityFee = chargeElectricityFee.add(BigDecimalUtils.nullToZero(report.getElectricityAmount()));
+ settlementAmount = settlementAmount.add(reportSettleAmount);
+ tradeAmount = tradeAmount.add(BigDecimalUtils.nullToZero(report.getTradeAmount()));
+ tradeFee = tradeFee.add(BigDecimalUtils.nullToZero(report.getTradeFee()));
+ arrivalAmount = arrivalAmount.add(report.getTradeAmount() != null
+ ? report.getTradeAmount() : reportSettleAmount);
+ }
+
+ private BigDecimal getOrderTotalAmount() {
+ return orderTotalAmount;
+ }
+
+ private BigDecimal getTotalOrderCount() {
+ return totalOrderCount;
+ }
+
+ private BigDecimal getOrderElectricity() {
+ return orderElectricity;
+ }
+
+ private BigDecimal getChargeServiceFee() {
+ return chargeServiceFee;
+ }
+
+ private BigDecimal getChargeElectricityFee() {
+ return chargeElectricityFee;
+ }
+
+ private BigDecimal getSettlementAmount() {
+ return settlementAmount;
+ }
+
+ private BigDecimal getTradeAmount() {
+ return tradeAmount;
+ }
+
+ private BigDecimal getTradeFee() {
+ return tradeFee;
+ }
+
+ private BigDecimal getArrivalAmount() {
+ return arrivalAmount;
+ }
+ }
+
/**
* 获取当前账号可访问的站点ID列表,平台账号返回null表示不限制
*
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 f0b45db1d..c28a33cc4 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
@@ -54,6 +54,7 @@ import com.jsowell.pile.service.programlogic.ProgramLogicFactory;
import com.jsowell.pile.transaction.dto.ClearingBillTransactionDTO;
import com.jsowell.pile.transaction.dto.OrderTransactionDTO;
import com.jsowell.pile.transaction.service.TransactionService;
+import com.jsowell.pile.util.ChargeAmountUtils;
import com.jsowell.pile.util.MerchantUtils;
import com.jsowell.pile.util.UserUtils;
import com.jsowell.pile.vo.OrderInfoDetailVO;
@@ -761,6 +762,18 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
}
for (OrderBasicInfo orderBasicInfo : orderList) {
+ OrderPaymentCheckResult paymentCheck = checkAndRepairOrderPayment(orderBasicInfo);
+ if (!paymentCheck.isCheckCompleted()) {
+ logger.error("关闭启动失败订单前支付凭证校验未完成,本次跳过, orderCode:{}, reason:{}",
+ orderBasicInfo.getOrderCode(), paymentCheck.getReason());
+ continue;
+ }
+ if (!paymentCheck.isOrderPaymentReady()) {
+ handleOrderPaymentAnomaly(orderBasicInfo, paymentCheck.getReason(), null, false);
+ logger.warn("关闭启动失败订单前支付凭证校验失败,跳过正常结算退款, orderCode:{}, reason:{}",
+ orderBasicInfo.getOrderCode(), paymentCheck.getReason());
+ continue;
+ }
// 判断支付成功时间距当前时间是否大于15分钟
String startType = orderBasicInfo.getStartType();
// 立即启动的订单使用支付时间判断,预约充电的订单使用预约时间判断
@@ -797,6 +810,472 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
}
}
+ /**
+ * 校验订单支付凭证,并在支付流水完整、但订单主表字段尚未落库时进行修复。
+ *
+ * 余额支付不能只相信 order_basic_info.pay_status。必须同时存在订单支付明细
+ * 和会员钱包出账流水,且两者金额与主表支付金额一致,才允许进入充电或结算流程。
+ */
+ @Override
+ public OrderPaymentCheckResult checkAndRepairOrderPayment(OrderBasicInfo orderBasicInfo) {
+ if (orderBasicInfo == null || StringUtils.isBlank(orderBasicInfo.getOrderCode())) {
+ return OrderPaymentCheckResult.builder()
+ .checkCompleted(true)
+ .paymentEvidenceFound(false)
+ .orderPaymentReady(false)
+ .reason("订单不存在或订单号为空")
+ .build();
+ }
+
+ OrderPaymentCheckResult result = inspectOrderPayment(orderBasicInfo);
+ if (!result.isCheckCompleted()) {
+ return result;
+ }
+ if (result.isOrderPaymentReady()) {
+ redisCache.setCacheObject(CacheConstants.ORDER_PAYMENT_READY + orderBasicInfo.getOrderCode(),
+ Boolean.TRUE, 10, TimeUnit.MINUTES);
+ return result;
+ }
+
+ // 只有底层流水已经形成且金额彼此一致时,才允许修复主表字段。
+ if (result.isPaymentEvidenceFound() && result.getPaymentAmount() != null
+ && result.getPaymentAmount().compareTo(BigDecimal.ZERO) > 0) {
+ BigDecimal paymentAmount = result.getPaymentAmount();
+ Date paymentTime = result.getPaymentTime() == null ? DateUtils.getNowDate() : result.getPaymentTime();
+ boolean needUpdate = !StringUtils.equals(OrderPayStatusEnum.paid.getValue(), orderBasicInfo.getPayStatus())
+ || orderBasicInfo.getPayAmount() == null
+ || orderBasicInfo.getPayAmount().compareTo(paymentAmount) != 0
+ || orderBasicInfo.getPayTime() == null;
+ if (needUpdate && orderBasicInfo.getId() != null) {
+ OrderBasicInfo update = new OrderBasicInfo();
+ update.setId(orderBasicInfo.getId());
+ update.setOrderCode(orderBasicInfo.getOrderCode());
+ update.setTransactionCode(orderBasicInfo.getTransactionCode());
+ update.setPayStatus(OrderPayStatusEnum.paid.getValue());
+ update.setPayAmount(paymentAmount);
+ update.setPayTime(paymentTime);
+ updateOrderBasicInfo(update);
+
+ // 同步更新当前调用链中的对象,避免后续结算仍拿到旧的 null 金额。
+ orderBasicInfo.setPayStatus(OrderPayStatusEnum.paid.getValue());
+ orderBasicInfo.setPayAmount(paymentAmount);
+ orderBasicInfo.setPayTime(paymentTime);
+ }
+
+ // 再次基于修复后的主表字段校验,确保不会把部分流水误判为成功。
+ result = inspectOrderPayment(orderBasicInfo);
+ if (result.isOrderPaymentReady()) {
+ redisCache.setCacheObject(CacheConstants.ORDER_PAYMENT_READY + orderBasicInfo.getOrderCode(),
+ Boolean.TRUE, 10, TimeUnit.MINUTES);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 将支付凭证异常订单置为异常,并按幂等规则最多发送一次停机指令。
+ * 此处绝不调用正常结算、退款或分账逻辑。
+ */
+ @Override
+ public void handleOrderPaymentAnomaly(OrderBasicInfo orderBasicInfo, String reason,
+ TransactionRecordsData data, boolean stopCharging) {
+ if (orderBasicInfo == null || StringUtils.isBlank(orderBasicInfo.getOrderCode())) {
+ return;
+ }
+
+ String orderCode = orderBasicInfo.getOrderCode();
+ String lockKey = CacheConstants.ORDER_PAYMENT_RECONCILE_LOCK + "anomaly:" + orderCode;
+ String requestId = IdUtils.fastSimpleUUID();
+ Boolean locked = redisCache.lock(lockKey, requestId, 30);
+ if (!Boolean.TRUE.equals(locked)) {
+ logger.info("支付异常订单正在被其他线程处理, orderCode:{}", orderCode);
+ return;
+ }
+
+ OrderBasicInfo latest = null;
+ boolean shouldSendStop = false;
+ try {
+ // 直接查库,避免实时监控线程拿到旧的缓存订单状态。
+ latest = orderBasicInfoMapper.getOrderInfoByOrderCode(orderCode);
+ if (latest == null || StringUtils.equals(OrderStatusEnum.ORDER_COMPLETE.getValue(), latest.getOrderStatus())) {
+ return;
+ }
+
+ shouldSendStop = stopCharging && StringUtils.isNotBlank(latest.getPileSn())
+ && StringUtils.isNotBlank(latest.getConnectorCode())
+ && StringUtils.isNotBlank(latest.getTransactionCode());
+
+ String anomalyReason = StringUtils.isBlank(reason) ? "订单支付凭证校验失败" : reason;
+ String normalizedReason = limitOrderReason("支付凭证异常:" + anomalyReason);
+ Date endTime = data == null ? null : DateUtils.parseDate(data.getEndTime());
+ String stopReasonCode = data == null ? null : normalizeStopReasonCode(data.getStopReasonCode());
+ boolean needUpdate = !StringUtils.equals(OrderStatusEnum.ABNORMAL.getValue(), latest.getOrderStatus())
+ || !StringUtils.equals(normalizedReason, latest.getReason())
+ || latest.getSettlementTime() == null
+ || latest.getChargeEndTime() == null
+ || (StringUtils.isNotBlank(stopReasonCode)
+ && !StringUtils.equals(stopReasonCode, latest.getStopReasonCode()));
+ if (needUpdate) {
+ OrderBasicInfo update = new OrderBasicInfo();
+ update.setId(latest.getId());
+ update.setOrderCode(latest.getOrderCode());
+ update.setTransactionCode(latest.getTransactionCode());
+ update.setOrderStatus(OrderStatusEnum.ABNORMAL.getValue());
+ update.setReason(normalizedReason);
+ update.setSettlementTime(latest.getSettlementTime() == null ? DateUtils.getNowDate() : latest.getSettlementTime());
+ if (latest.getChargeEndTime() == null) {
+ update.setChargeEndTime(endTime == null ? DateUtils.getNowDate() : endTime);
+ }
+ if (StringUtils.isNotBlank(stopReasonCode)) {
+ update.setStopReasonCode(stopReasonCode);
+ }
+ updateOrderBasicInfo(update);
+ }
+
+ // 保存原始交易报文;服务内部按订单号去重,避免重复结算消息制造重复记录。
+ if (data != null) {
+ savePaymentAnomalyReport(latest, data);
+ }
+
+ } catch (Exception e) {
+ logger.error("处理支付凭证异常订单失败, orderCode:{}", orderCode, e);
+ } finally {
+ redisCache.unLock(lockKey);
+ }
+
+ if (shouldSendStop && latest != null) {
+ String transactionCode = latest.getTransactionCode();
+ String stopKey = CacheConstants.ORDER_PAYMENT_ANOMALY_STOP + transactionCode;
+ Boolean firstStop = redisCache.setnx(stopKey, requestId, 30 * 60);
+ if (Boolean.TRUE.equals(firstStop)) {
+ try {
+ pileRemoteService.remoteStopCharging(latest.getPileSn(), latest.getConnectorCode(), transactionCode);
+ logger.warn("支付凭证异常订单已发送停机指令, orderCode:{}, transactionCode:{}", orderCode, transactionCode);
+ } catch (Exception e) {
+ // 指令未成功下发时释放幂等标记,允许后续实时报文或定时补偿再次尝试停机。
+ try {
+ redisCache.deleteObject(stopKey);
+ } catch (Exception cacheException) {
+ logger.error("释放支付异常停机标记失败, orderCode:{}, transactionCode:{}",
+ orderCode, transactionCode, cacheException);
+ }
+ logger.error("支付凭证异常订单发送停机指令失败, orderCode:{}, transactionCode:{}", orderCode, transactionCode, e);
+ }
+ } else {
+ logger.info("支付凭证异常订单已发送过停机指令, orderCode:{}, transactionCode:{}", orderCode, transactionCode);
+ }
+ }
+ }
+
+ /**
+ * 定时补偿余额支付订单。补偿只修复真实存在的支付凭证;完全没有支付凭证的订单直接置为异常,
+ * 不会伪造支付状态,也不会进入正常结算退款。
+ */
+ @Override
+ public void reconcileChargingOrderPayments(String startTime, String endTime) {
+ List orderList = orderBasicInfoMapper.queryOrdersForPaymentReconcile(startTime, endTime);
+ if (CollectionUtils.isEmpty(orderList)) {
+ logger.info("{}-{}期间没有需要支付凭证补偿的订单", startTime, endTime);
+ return;
+ }
+ logger.info("{}-{}期间,共{}条余额支付订单需要支付凭证补偿", startTime, endTime, orderList.size());
+
+ for (OrderBasicInfo orderBasicInfo : orderList) {
+ if (orderBasicInfo == null || StringUtils.isBlank(orderBasicInfo.getOrderCode())) {
+ continue;
+ }
+ String orderCode = orderBasicInfo.getOrderCode();
+ String lockKey = CacheConstants.ORDER_PAYMENT_RECONCILE_LOCK + orderCode;
+ String requestId = IdUtils.fastSimpleUUID();
+ Boolean locked = redisCache.lock(lockKey, requestId, 30);
+ if (!Boolean.TRUE.equals(locked)) {
+ logger.info("支付凭证补偿订单正在被其他线程处理, orderCode:{}", orderCode);
+ continue;
+ }
+ try {
+ OrderBasicInfo latest = orderBasicInfoMapper.getOrderInfoByOrderCode(orderCode);
+ if (latest == null || StringUtils.equals(OrderStatusEnum.ORDER_COMPLETE.getValue(), latest.getOrderStatus())) {
+ continue;
+ }
+ OrderPaymentCheckResult result = checkAndRepairOrderPayment(latest);
+ if (!result.isCheckCompleted()) {
+ logger.error("支付凭证补偿校验未完成,本次跳过, orderCode:{}, reason:{}",
+ orderCode, result.getReason());
+ continue;
+ }
+ if (!result.isOrderPaymentReady()) {
+ boolean stopCharging = isOrderStillCharging(latest);
+ handleOrderPaymentAnomaly(latest, result.getReason(), null, stopCharging);
+ }
+ } catch (Exception e) {
+ logger.error("支付凭证补偿订单处理失败, orderCode:{}", orderCode, e);
+ } finally {
+ redisCache.unLock(lockKey);
+ }
+ }
+ }
+
+ private boolean isOrderStillCharging(OrderBasicInfo orderBasicInfo) {
+ if (orderBasicInfo == null) {
+ return false;
+ }
+ if (StringUtils.equals(OrderStatusEnum.IN_THE_CHARGING.getValue(), orderBasicInfo.getOrderStatus())) {
+ return true;
+ }
+ if (StringUtils.isBlank(orderBasicInfo.getPileSn()) || StringUtils.isBlank(orderBasicInfo.getConnectorCode())
+ || StringUtils.isBlank(orderBasicInfo.getTransactionCode())) {
+ return false;
+ }
+ String chargingTransaction = redisCache.getCacheObject(
+ CacheConstants.PILE_IS_CHARGING + orderBasicInfo.getPileSn() + orderBasicInfo.getConnectorCode());
+ return StringUtils.equals(orderBasicInfo.getTransactionCode(), chargingTransaction);
+ }
+
+ /**
+ * 读取支付明细、钱包出账流水并完成一致性校验。
+ */
+ private OrderPaymentCheckResult inspectOrderPayment(OrderBasicInfo orderBasicInfo) {
+ String payMode = orderBasicInfo.getPayMode();
+ String payStatus = orderBasicInfo.getPayStatus();
+ if (StringUtils.equals(OrderPayStatusEnum.pay_nothing.getValue(), payStatus)) {
+ return OrderPaymentCheckResult.builder()
+ .checkCompleted(true)
+ .paymentEvidenceFound(true)
+ .orderPaymentReady(true)
+ .paymentAmount(orderBasicInfo.getPayAmount())
+ .paymentTime(orderBasicInfo.getPayTime())
+ .reason("订单无需支付")
+ .build();
+ }
+
+ List payRecords;
+ List walletLogs = Collections.emptyList();
+ try {
+ payRecords = orderPayRecordService.getOrderPayRecordList(orderBasicInfo.getOrderCode());
+ if (payRecords == null) {
+ payRecords = Collections.emptyList();
+ }
+ if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode)) {
+ List logs = memberWalletLogService.getOrderRecord(
+ orderBasicInfo.getOrderCode(), MemberWalletEnum.TYPE_OUT.getValue());
+ walletLogs = logs == null ? Collections.emptyList() : logs;
+ }
+ } catch (Exception e) {
+ logger.error("读取订单支付凭证失败, orderCode:{}", orderBasicInfo.getOrderCode(), e);
+ return OrderPaymentCheckResult.builder()
+ .checkCompleted(false)
+ .paymentEvidenceFound(false)
+ .orderPaymentReady(false)
+ .reason("读取支付凭证失败")
+ .build();
+ }
+
+ BigDecimal payRecordAmount = BigDecimal.ZERO;
+ boolean payRecordEvidence = false;
+ Date paymentTime = null;
+ for (OrderPayRecord record : payRecords) {
+ if (record == null || StringUtils.equals(DelFlagEnum.DELETE.getValue(), record.getDelFlag())) {
+ continue;
+ }
+ boolean balanceRecord = StringUtils.equals(OrderPayRecordEnum.PRINCIPAL_BALANCE_PAYMENT.getValue(), record.getPayMode())
+ || StringUtils.equals(OrderPayRecordEnum.GIFT_BALANCE_PAYMENT.getValue(), record.getPayMode());
+ if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode) && !balanceRecord) {
+ continue;
+ }
+ if (record.getPayAmount() != null && record.getPayAmount().compareTo(BigDecimal.ZERO) > 0) {
+ payRecordEvidence = true;
+ payRecordAmount = payRecordAmount.add(record.getPayAmount());
+ }
+ if (record.getCreateTime() != null && (paymentTime == null || record.getCreateTime().after(paymentTime))) {
+ paymentTime = record.getCreateTime();
+ }
+ }
+
+ BigDecimal walletAmount = BigDecimal.ZERO;
+ boolean walletEvidence = false;
+ if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode)) {
+ for (MemberWalletLog walletLog : walletLogs) {
+ if (walletLog == null
+ || !StringUtils.equals(MemberWalletEnum.TYPE_OUT.getValue(), walletLog.getType())
+ || !StringUtils.equals(MemberWalletEnum.SUBTYPE_PAYMENT_FOR_ORDER.getValue(), walletLog.getSubType())
+ || walletLog.getAmount() == null
+ || walletLog.getAmount().compareTo(BigDecimal.ZERO) == 0) {
+ continue;
+ }
+ walletEvidence = true;
+ walletAmount = walletAmount.add(walletLog.getAmount().abs());
+ if (walletLog.getCreateTime() != null && (paymentTime == null || walletLog.getCreateTime().after(paymentTime))) {
+ paymentTime = walletLog.getCreateTime();
+ }
+ }
+ }
+
+ boolean baseFieldsReady = StringUtils.equals(OrderPayStatusEnum.paid.getValue(), payStatus)
+ && orderBasicInfo.getPayAmount() != null
+ && orderBasicInfo.getPayAmount().compareTo(BigDecimal.ZERO) > 0
+ && orderBasicInfo.getPayTime() != null;
+
+ if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode)) {
+ boolean evidenceFound = payRecordEvidence || walletEvidence;
+ if (!payRecordEvidence || !walletEvidence) {
+ return OrderPaymentCheckResult.builder()
+ .checkCompleted(true)
+ .paymentEvidenceFound(evidenceFound)
+ .orderPaymentReady(false)
+ // 余额支付必须同时具备两类底层凭证;单边流水不能用于自动补齐主表支付状态。
+ .paymentAmount(null)
+ .paymentTime(paymentTime)
+ .reason("余额支付缺少订单支付明细或会员钱包出账流水")
+ .build();
+ }
+ if (!samePaymentAmount(payRecordAmount, walletAmount)) {
+ return OrderPaymentCheckResult.builder()
+ .checkCompleted(true)
+ .paymentEvidenceFound(true)
+ .orderPaymentReady(false)
+ .paymentTime(paymentTime)
+ .reason("订单支付明细与会员钱包出账金额不一致")
+ .build();
+ }
+ // 余额支付主表 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(balanceChargeOnly) // 修复回写只写充电金额(明细-保险)
+ .paymentTime(paymentTime)
+ .reason("支付流水存在,但订单主表支付字段不完整(payStatus=" + payStatus
+ + ", payAmount=" + orderBasicInfo.getPayAmount()
+ + ", payTime=" + orderBasicInfo.getPayTime() + ")")
+ .build();
+ }
+ boolean balanceAmountConsistent = samePaymentAmount(orderBasicInfo.getPayAmount(), payRecordAmount)
+ || samePaymentAmount(orderBasicInfo.getPayAmount().add(balanceInsuranceAmount), payRecordAmount);
+ if (!balanceAmountConsistent) {
+ return OrderPaymentCheckResult.builder()
+ .checkCompleted(true)
+ .paymentEvidenceFound(true)
+ .orderPaymentReady(false)
+ .paymentAmount(balanceChargeOnly)
+ .paymentTime(paymentTime)
+ .reason("支付流水存在,但订单主表支付金额(" + orderBasicInfo.getPayAmount()
+ + ")+保险金额(" + balanceInsuranceAmount + ")与支付明细汇总(" + payRecordAmount + ")不一致")
+ .build();
+ }
+ return OrderPaymentCheckResult.builder()
+ .checkCompleted(true)
+ .paymentEvidenceFound(true)
+ .orderPaymentReady(true)
+ .paymentAmount(payRecordAmount)
+ .paymentTime(paymentTime)
+ .reason("余额支付凭证校验通过")
+ .build();
+ }
+
+ // 非余额支付保持兼容:必须有完整主表支付字段;若存在支付明细,则再校验明细金额。
+ 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(chargeOnlyAmount)
+ .paymentTime(paymentTime)
+ .reason("订单主表支付字段不完整(payStatus=" + payStatus
+ + ", payAmount=" + orderBasicInfo.getPayAmount()
+ + ", payTime=" + orderBasicInfo.getPayTime() + ")")
+ .build();
+ }
+ // 兼容主表 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()
+ + ")+保险金额(" + insuranceAmount + ")均不一致")
+ .build();
+ }
+ return OrderPaymentCheckResult.builder()
+ .checkCompleted(true)
+ .paymentEvidenceFound(true)
+ .orderPaymentReady(true)
+ .paymentAmount(orderBasicInfo.getPayAmount())
+ .paymentTime(orderBasicInfo.getPayTime())
+ .reason("支付凭证校验通过")
+ .build();
+ }
+
+ private boolean samePaymentAmount(BigDecimal first, BigDecimal second) {
+ return first != null && second != null
+ && first.setScale(2, RoundingMode.HALF_UP).compareTo(second.setScale(2, RoundingMode.HALF_UP)) == 0;
+ }
+
+ private String limitOrderReason(String reason) {
+ if (reason == null) {
+ return null;
+ }
+ // order_basic_info.reason 在历史库中通常为 varchar(255),原始报文另存 order_abnormal_record。
+ return reason.length() > 250 ? reason.substring(0, 250) : reason;
+ }
+
+ private String normalizeStopReasonCode(String stopReasonCode) {
+ if (StringUtils.isBlank(stopReasonCode) || StringUtils.startsWithIgnoreCase(stopReasonCode, "0x")) {
+ return stopReasonCode;
+ }
+ return "0x" + stopReasonCode;
+ }
+
+ private void savePaymentAnomalyReport(OrderBasicInfo orderBasicInfo, TransactionRecordsData data) {
+ try {
+ OrderAbnormalRecord query = new OrderAbnormalRecord();
+ query.setOrderCode(orderBasicInfo.getOrderCode());
+ if (CollectionUtils.isNotEmpty(orderAbnormalRecordService.selectOrderAbnormalRecordList(query))) {
+ return;
+ }
+ OrderAbnormalRecord record = new OrderAbnormalRecord();
+ BeanUtils.copyBeanProp(record, data);
+ record.setOrderCode(orderBasicInfo.getOrderCode());
+ if (StringUtils.isBlank(record.getPileSn())) {
+ record.setPileSn(orderBasicInfo.getPileSn());
+ }
+ if (StringUtils.isBlank(record.getConnectorCode())) {
+ record.setConnectorCode(orderBasicInfo.getConnectorCode());
+ }
+ if (record.getCreateTime() == null) {
+ record.setCreateTime(DateUtils.getNowDate());
+ }
+ orderAbnormalRecordService.insertOrderAbnormalRecord(record);
+ } catch (Exception e) {
+ logger.error("保存支付凭证异常原始交易报文失败, orderCode:{}", orderBasicInfo.getOrderCode(), e);
+ }
+ }
+
/**
* 判断是否退款保险金额
* @return
@@ -4115,18 +4594,7 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
} else {
// 通过memberId获取账户余额
MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(dto.getMemberId(), merchantId);
- if (memberVO == null) {
- throw new BusinessException("01", "查询用户账户为空");
- }
- accountBalance = memberVO.getTotalBalance();
- if (accountBalance.compareTo(BigDecimal.ZERO) <= 0) {
- logger.info("充电桩主动申请启动充电生成订单 余额不足, memberId:{}, MemberVO:{}", dto.getMemberId(), JSON.toJSONString(memberVO));
- throw new BusinessException("03", "用户账户余额不足");
- }
- BigDecimal defaultAmount = Constants.BALANCE_PAY_MAX_AMOUNT_NEW;
- if (accountBalance.compareTo(defaultAmount) > 0) {
- accountBalance = defaultAmount;
- }
+ accountBalance = resolveBalanceChargeAmount(dto.getChargeAmount(), memberVO, dto.getMemberId());
payMode = OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue();
}
dto.setChargeAmount(accountBalance);
@@ -4218,6 +4686,15 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
// 运营商id
String merchantId = dto.getMerchantId();
+ if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue(), payMode)) {
+ MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(dto.getMemberId(), merchantId);
+ accountBalance = resolveBalanceChargeAmount(accountBalance, memberVO, dto.getMemberId());
+ } else if (StringUtils.equals(OrderPayModeEnum.PAYMENT_OF_WHITELIST.getValue(), payMode)) {
+ accountBalance = Constants.WHITELIST_DEFAULT_AMOUNT;
+ } else if (!ChargeAmountUtils.isPositive(accountBalance)) {
+ throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR);
+ }
+
dto.setChargeAmount(accountBalance);
dto.setPayMode(payMode);
/*
@@ -4276,6 +4753,35 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
return map;
}
+ /**
+ * 确定余额支付的启动金额,并使用当前运营商钱包余额进行校验。
+ */
+ private BigDecimal resolveBalanceChargeAmount(BigDecimal frontendChargeAmount, MemberVO memberVO, String memberId) {
+ if (memberVO == null) {
+ throw new BusinessException("01", "查询用户账户为空");
+ }
+ BigDecimal chargeAmount = ChargeAmountUtils.resolveStartupAmount(
+ frontendChargeAmount, memberVO.getDefaultChargeAmount());
+ if (!ChargeAmountUtils.isPositive(chargeAmount)) {
+ throw new BusinessException(ReturnCodeEnum.CODE_PARAM_NOT_NULL_ERROR.getValue(), "启动金额必须大于0");
+ }
+ BigDecimal totalBalance = memberVO.getTotalBalance() == null
+ ? BigDecimal.ZERO
+ : memberVO.getTotalBalance();
+ // 账户余额小于默认充电金额时,使用全部余额
+ if (totalBalance.compareTo(chargeAmount) < 0) {
+ logger.info("充电桩主动申请启动充电生成订单 余额不足默认金额,使用全部余额, memberId:{}, chargeAmount:{}, totalBalance:{}",
+ memberId, chargeAmount, totalBalance);
+ chargeAmount = totalBalance;
+ }
+ if (!ChargeAmountUtils.isPositive(chargeAmount)) {
+ logger.info("充电桩主动申请启动充电生成订单 余额不足, memberId:{}, chargeAmount:{}, totalBalance:{}",
+ memberId, chargeAmount, totalBalance);
+ throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT);
+ }
+ return chargeAmount;
+ }
+
/**
* 联联平台 获取累计数据用
*
@@ -4925,7 +5431,7 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
return;
}
- // 获取启动金额
+ // 获取启动金额:dto.payAmount 已是充电金额(不含保险),直接用于下发桩
BigDecimal chargeAmount = computeChargeAmount(orderInfo.getMerchantId(), orderInfo.getStationId(), orderInfo.getMemberId(), dto.getPayAmount());
// 发送启动指令
@@ -6299,49 +6805,42 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
return map;
}
- // 如果总余额小于0, 则标示余额不足
- // if (memberWalletVO.getTotalBalance().compareTo(BigDecimal.ZERO) <= 0) {
- // map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
- // map.put("accountBalance", accountBalance);
- // map.put("defeatReasonCode", "03");
- // logger.info("并充生成订单----余额不足");
- // return map;
- // }
-
- // 如果不是平台测试员, 并且也不是站点白名单, 并且总余额小于等于0, 则提示余额不足
- if (!StringUtils.equals(Constants.ONE, memberWalletVO.getPlatformTesterFlag())
- && !StringUtils.equals(Constants.ONE, memberWalletVO.getStationWhiteListFlag())
- && memberWalletVO.getTotalBalance().compareTo(BigDecimal.ZERO) <= 0) {
- map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
- map.put("accountBalance", accountBalance);
- map.put("defeatReasonCode", "03");
- logger.info("余额不足");
- return map;
- }
-
/*
鉴权通过, 生成订单并支付
*/
+ BigDecimal chargeAmount;
String payMode;
if (StringUtils.equals(Constants.ONE, memberWalletVO.getPlatformTesterFlag())
|| StringUtils.equals(Constants.ONE, memberWalletVO.getStationWhiteListFlag())) {
// 如果是平台测试员或者是站点白名单
- accountBalance = Constants.WHITELIST_DEFAULT_AMOUNT.toString();
+ chargeAmount = Constants.WHITELIST_DEFAULT_AMOUNT;
payMode = OrderPayModeEnum.PAYMENT_OF_WHITELIST.getValue();
} else {
- BigDecimal totalBalance = memberWalletVO.getTotalBalance();
- accountBalance = totalBalance.toString();
- if (totalBalance.compareTo(Constants.BALANCE_PAY_MAX_AMOUNT_NEW) > 0) {
- accountBalance = Constants.BALANCE_PAY_MAX_AMOUNT_NEW.toString();
+ chargeAmount = ChargeAmountUtils.resolveStartupAmount(
+ generateOrderDTO.getChargeAmount(), memberWalletVO.getDefaultChargeAmount());
+ BigDecimal totalBalance = memberWalletVO.getTotalBalance() == null
+ ? BigDecimal.ZERO
+ : memberWalletVO.getTotalBalance();
+ // 账户余额小于默认充电金额时,使用全部余额
+ if (totalBalance.compareTo(chargeAmount) < 0) {
+ logger.info("并充生成订单----余额不足默认金额,使用全部余额, memberId:{}, chargeAmount:{}, totalBalance:{}",
+ memberWalletVO.getMemberId(), chargeAmount, totalBalance);
+ chargeAmount = totalBalance;
+ }
+ if (!ChargeAmountUtils.isPositive(chargeAmount)) {
+ map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
+ map.put("accountBalance", accountBalance);
+ map.put("defeatReasonCode", "03");
+ logger.info("并充生成订单----余额不足, memberId:{}, chargeAmount:{}, totalBalance:{}",
+ memberWalletVO.getMemberId(), chargeAmount, totalBalance);
+ return map;
}
payMode = OrderPayModeEnum.PAYMENT_OF_PRINCIPAL_BALANCE.getValue();
}
+ accountBalance = chargeAmount.toString();
// 生成交易流水号, 传递给订单使用
String transactionCode = IdUtils.generateTransactionCode(pileSn, connectorCode);
- map.put("transactionCode", transactionCode);
- map.put("accountBalance", accountBalance);
- // 异步创建订单
// GenerateOrderDTO dto = new GenerateOrderDTO();
generateOrderDTO.setPileSn(pileSn);
if (StringUtils.isNotBlank(memberWalletVO.getPlateNumber())) {
@@ -6350,22 +6849,35 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
generateOrderDTO.setOrderType(OrderTypeEnum.MERGE_CHARGE_ORDER.getValue()); // 并充订单标识
generateOrderDTO.setConnectorCode(connectorCode);
generateOrderDTO.setTransactionCode(transactionCode);
- generateOrderDTO.setChargeAmount(new BigDecimal(accountBalance));
+ generateOrderDTO.setChargeAmount(chargeAmount);
generateOrderDTO.setPayMode(payMode);
generateOrderDTO.setStartMode(StringUtils.equals("01", startMode) ? "2" : "5");
generateOrderDTO.setMemberId(memberWalletVO.getMemberId());
generateOrderDTO.setMerchantId(pileBasicInfo.getMerchantId() + "");
- // 校验充电桩相关的信息
- checkPileInfoForEV(generateOrderDTO);
-
- CompletableFuture.runAsync(() -> {
- try {
- Map resultMap = generateOrderByCardV2(generateOrderDTO);
- } catch (Exception e) {
- throw new RuntimeException(e);
+ try {
+ Map resultMap = generateOrderByCardV2(generateOrderDTO);
+ if (resultMap == null) {
+ throw new BusinessException(ReturnCodeEnum.CODE_GENERATE_ORDER_ERROR);
}
- }, executor);
+ } catch (BusinessException e) {
+ map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
+ map.put("accountBalance", Constants.ZERO);
+ map.put("defeatReasonCode", StringUtils.equals(
+ ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT.getValue(), e.getCode()) ? "03" : "05");
+ logger.error("并充生成订单失败, memberId:{}, chargeAmount:{}",
+ memberWalletVO.getMemberId(), chargeAmount, e);
+ return map;
+ } catch (Exception e) {
+ map.put("transactionCode", Constants.ILLEGAL_TRANSACTION_CODE);
+ map.put("accountBalance", Constants.ZERO);
+ map.put("defeatReasonCode", "05");
+ logger.error("并充生成订单失败, memberId:{}, chargeAmount:{}",
+ memberWalletVO.getMemberId(), chargeAmount, e);
+ return map;
+ }
+ map.put("transactionCode", transactionCode);
+ map.put("accountBalance", accountBalance);
return map;
}
@@ -6672,7 +7184,6 @@ public class OrderBasicInfoServiceImpl implements OrderBasicInfoService {
}
/**
-<<<<<<< HEAD
* 运营端小程序查询订单
* @param dto 查询条件
* @return 订单查询结果
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/YKCPushCommandServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/YKCPushCommandServiceImpl.java
index d4f993d75..4e511ab2d 100644
--- a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/YKCPushCommandServiceImpl.java
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/YKCPushCommandServiceImpl.java
@@ -406,14 +406,40 @@ public class YKCPushCommandServiceImpl implements YKCPushCommandService {
byte[] accountBalanceByteArr = YKCUtils.getPriceByte(chargeAmount.toString(), 2);
byte[] msgBody = Bytes.concat(orderIdByteArr, pileSnByteArr, connectorCodeByteArr, logicCardNumByteArr, physicsCardNumByteArr, accountBalanceByteArr);
+
+ // 注册同步等待容器,用于接收充电桩 0x33 应答并打印启动结果
+ SyncPromise syncPromise = new SyncPromise();
+ String responseKey = transactionCode + "_0x33";
+ RpcUtil.getSyncPromiseMap().put(responseKey, syncPromise);
+
try {
this.runSend(msgBody, pileSn, YKCFrameTypeCode.REMOTE_CONTROL_START_CHARGING_CODE);
} catch (Exception e) {
+ RpcUtil.getSyncPromiseMap().remove(responseKey);
throw new RuntimeException(e);
}
log.info("【=====平台下发充电指令=====】:订单id:{}, 桩号:{}, 枪口号:{}, 逻辑卡号:{}, 物理卡号:{}, 账户余额:{}",
transactionCode, pileSn, BytesUtil.bcd2Str(connectorCodeByteArr), logicCardNum, physicsCardNum, chargeAmount);
+
+ // 等待充电桩应答(最多 3 秒)
+ byte[] response;
+ try {
+ response = syncPromise.get(3, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ response = null;
+ } finally {
+ RpcUtil.getSyncPromiseMap().remove(responseKey);
+ }
+
+ if (response == null) {
+ log.warn("【启动充电结果】订单号:{}, 桩号:{}, 枪号:{}, 结果:等待充电桩响应超时(3s),桩可能未收到指令或未回复",
+ transactionCode, pileSn, connectorCode);
+ } else {
+ log.info("【启动充电结果】订单号:{}, 桩号:{}, 枪号:{}, 结果:{}",
+ transactionCode, pileSn, connectorCode, new String(response, java.nio.charset.StandardCharsets.UTF_8));
+ }
}
/**
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/AbstractProgramLogic.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/AbstractProgramLogic.java
index 6b5c8181f..5f3208ab3 100644
--- a/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/AbstractProgramLogic.java
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/programlogic/AbstractProgramLogic.java
@@ -149,6 +149,30 @@ public abstract class AbstractProgramLogic implements InitializingBean {
*/
public abstract void settleOrder(TransactionRecordsData data, OrderBasicInfo orderBasicInfo);
+ /**
+ * 结算前统一校验支付凭证。支付凭证不完整时只隔离订单,不进入金额计算、退款或分账流程。
+ */
+ protected boolean ensureOrderPaymentReadyForSettlement(OrderBasicInfo orderBasicInfo,
+ TransactionRecordsData data) {
+ if (orderBasicInfo == null) {
+ return false;
+ }
+ OrderPaymentCheckResult paymentCheck = orderBasicInfoService.checkAndRepairOrderPayment(orderBasicInfo);
+ if (!paymentCheck.isCheckCompleted()) {
+ logger.error("订单支付凭证校验未完成,本次跳过结算且不变更订单状态, orderCode:{}, reason:{}",
+ orderBasicInfo.getOrderCode(), paymentCheck.getReason());
+ return false;
+ }
+ if (!paymentCheck.isOrderPaymentReady()) {
+ orderBasicInfoService.handleOrderPaymentAnomaly(orderBasicInfo,
+ paymentCheck.getReason(), data, false);
+ logger.warn("订单支付凭证校验失败,跳过正常结算, orderCode:{}, reason:{}",
+ orderBasicInfo.getOrderCode(), paymentCheck.getReason());
+ return false;
+ }
+ return true;
+ }
+
/**
* 电单车结算逻辑
* @param message
@@ -214,6 +238,9 @@ public abstract class AbstractProgramLogic implements InitializingBean {
BigDecimal orderAmount = new BigDecimal(data.getConsumptionAmount()).setScale(2, RoundingMode.HALF_UP);
// 付款金额 - 实际消费金额,如果有剩余,需要走退款操作 当使用余额支付时payAmount = principalPay + giftPay
BigDecimal payAmount = orderBasicInfo.getPayAmount();
+ if (payAmount == null || payAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalStateException("订单支付金额为空,禁止进入结算: " + orderCode);
+ }
// logger.info("结算订单:【{}】, 支付金额:{}, 消费金额:{}", orderCode, payAmount, orderAmount);
// 有时候充电桩到达金额停止充电会多出一点金额,比如实际需要充50元的电,充电桩传来的消费金额为50.01元,在后台记录的时候需要舍去
@@ -280,6 +307,9 @@ public abstract class AbstractProgramLogic implements InitializingBean {
public OrderDetail returnUpdateOrderDetail(OrderBasicInfo orderBasicInfo, TransactionRecordsData data) {
logger.info("start更新订单详情:【{}】, orderBasicInfo:{}, TransactionRecordsData:{}", orderBasicInfo.getOrderCode(), JSON.toJSONString(orderBasicInfo), JSON.toJSONString(data));
String orderCode = orderBasicInfo.getOrderCode();
+ if (orderBasicInfo.getPayAmount() == null || orderBasicInfo.getPayAmount().compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalStateException("订单支付金额为空,禁止进入结算: " + orderCode);
+ }
BigDecimal orderAmount = orderBasicInfo.getOrderAmount();
// 更新订单详情 查询订单详情 修改订单数据
OrderDetail orderDetail = orderBasicInfoService.getOrderDetailByOrderCode(orderCode);
@@ -480,6 +510,9 @@ public abstract class AbstractProgramLogic implements InitializingBean {
String orderCode = orderBasicInfo.getOrderCode();
// 订单支付金额
BigDecimal payAmount = orderBasicInfo.getPayAmount();
+ if (payAmount == null || payAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalStateException("订单支付金额为空,禁止进入结算: " + orderCode);
+ }
// data中的消费金额 精确到小数点后四位,包含电费、 服务费
BigDecimal dataOrderAmount = new BigDecimal(data.getConsumptionAmount()).setScale(2, RoundingMode.HALF_UP);
// data中的总用电量
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 e1a5d3e25..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
@@ -32,6 +32,7 @@ import com.jsowell.pile.domain.*;
import com.jsowell.pile.domain.ebike.deviceupload.EBikeMessageCmd03;
import com.jsowell.pile.dto.*;
import com.jsowell.pile.transaction.dto.OrderTransactionDTO;
+import com.jsowell.pile.util.ChargeAmountUtils;
import com.jsowell.pile.vo.uniapp.customer.MemberVO;
import com.jsowell.pile.vo.uniapp.customer.PileConnectorDetailVO;
import com.jsowell.pile.vo.web.BalanceDeductionAmountVO;
@@ -321,16 +322,34 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic {
OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode);
// 订单所属运营商id
String merchantId = orderBasicInfo.getMerchantId();
- // 支付金额
- BigDecimal chargeAmount = dto.getPayAmount();
// 会员id
String memberId = dto.getMemberId();
// 查询该会员的余额 在所属运营商的余额
MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(dto.getMemberId(), merchantId);
+ if (memberVO == null) {
+ throw new BusinessException(ReturnCodeEnum.CODE_GET_MEMBER_ACCOUNT_AMOUNT_ERROR);
+ }
+ // 支付金额:前端金额 > 会员默认金额 > 代码默认金额
+ BigDecimal chargeAmount = ChargeAmountUtils.resolveStartupAmount(
+ dto.getPayAmount(), memberVO.getDefaultChargeAmount());
+ if (!ChargeAmountUtils.isPositive(chargeAmount)) {
+ 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();
- if (totalAccountAmount.compareTo(chargeAmount) < 0) {
- // 总余额小于充电金额
+ BigDecimal totalAccountAmount = memberVO.getTotalBalance() == null
+ ? BigDecimal.ZERO
+ : memberVO.getTotalBalance();
+ if (totalAccountAmount.compareTo(totalPayAmount) < 0) {
+ // 总余额不足以覆盖 充电金额 + 保险费,直接拒绝
throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT);
}
@@ -338,13 +357,17 @@ 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、会员流水;
+ // 注意:主表 payAmount 仍只记充电金额(chargeAmount),不含保险。
+ BigDecimal actualPaidAmount = principalBalancePay.add(giftBalancePay);
+
// 更新会员钱包 全部金额都用于支付订单
UpdateMemberBalanceDTO updateMemberBalanceDTO = UpdateMemberBalanceDTO.builder()
.memberId(memberId)
@@ -407,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);
}
@@ -542,6 +565,9 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic {
logger.info("结算订单V2:{}, 是订单完成状态", orderBasicInfo.getOrderCode());
return;
}
+ if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, data)) {
+ return;
+ }
// 查询订单详情
OrderDetail orderDetail = orderBasicInfoService.getOrderDetailByOrderCode(orderBasicInfo.getOrderCode());
@@ -606,6 +632,9 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic {
logger.info("结算订单:{}, 是订单完成状态", orderBasicInfo.getOrderCode());
return;
}
+ if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, null)) {
+ return;
+ }
// 查询订单信息
OrderDetail orderDetail = orderBasicInfoService.getOrderDetailByOrderCode(orderBasicInfo.getOrderCode());
// 订单消费金额, 除了充满自停是需要计算实际消费, 其他金额都是按次收费
@@ -759,6 +788,9 @@ public class DelayMerchantProgramLogic extends AbstractProgramLogic {
logger.info("结算订单:{}, 是订单完成状态", orderBasicInfo.getOrderCode());
return;
}
+ if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, data)) {
+ return;
+ }
// 获取更新数据后的orderBasicInfo对象
returnUpdateOrderBasicInfo(orderBasicInfo, data);
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 8235ca39d..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
@@ -31,6 +31,7 @@ import com.jsowell.pile.domain.*;
import com.jsowell.pile.domain.ebike.deviceupload.EBikeMessageCmd03;
import com.jsowell.pile.dto.*;
import com.jsowell.pile.transaction.dto.OrderTransactionDTO;
+import com.jsowell.pile.util.ChargeAmountUtils;
import com.jsowell.pile.vo.uniapp.customer.MemberVO;
import com.jsowell.pile.vo.web.BalanceDeductionAmountVO;
import com.jsowell.pile.vo.web.OrderDetailInfoVO;
@@ -233,16 +234,34 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic {
OrderBasicInfo orderBasicInfo = orderBasicInfoService.getOrderInfoByOrderCode(orderCode);
// 订单所属运营商id
String merchantId = orderBasicInfo.getMerchantId();
- // 支付金额
- BigDecimal chargeAmount = dto.getPayAmount();
// 会员id
String memberId = dto.getMemberId();
// 查询该会员的余额 在所属运营商的余额
MemberVO memberVO = memberBasicInfoService.queryMemberInfoByMemberId(memberId, merchantId);
+ if (memberVO == null) {
+ throw new BusinessException(ReturnCodeEnum.CODE_GET_MEMBER_ACCOUNT_AMOUNT_ERROR);
+ }
+ // 支付金额:前端金额 > 会员默认金额 > 代码默认金额
+ BigDecimal chargeAmount = ChargeAmountUtils.resolveStartupAmount(
+ dto.getPayAmount(), memberVO.getDefaultChargeAmount());
+ if (!ChargeAmountUtils.isPositive(chargeAmount)) {
+ 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();
- if (totalAccountAmount.compareTo(chargeAmount) < 0) {
- // 总余额小于充电金额
+ BigDecimal totalAccountAmount = memberVO.getTotalBalance() == null
+ ? BigDecimal.ZERO
+ : memberVO.getTotalBalance();
+ if (totalAccountAmount.compareTo(totalPayAmount) < 0) {
+ // 总余额不足以覆盖 充电金额 + 保险费,直接拒绝
throw new BusinessException(ReturnCodeEnum.CODE_BALANCE_IS_INSUFFICIENT);
}
@@ -250,15 +269,16 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic {
BigDecimal principalBalancePay = BigDecimal.ZERO;
// 赠送金支付金额
BigDecimal giftBalancePay = BigDecimal.ZERO;
- // 计算下发金额
- Map stringBigDecimalMap = calculateTheAmount(memberVO);
+ // 计算下发金额:按完整实付(充电+保险)拆分本金/赠金
+ 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()
@@ -322,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);
}
@@ -427,6 +447,9 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic {
logger.info("结算订单:{}, 是订单完成状态", orderBasicInfo.getOrderCode());
return;
}
+ if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, data)) {
+ return;
+ }
// 获取更新数据后的orderBasicInfo对象
// returnUpdateOrderBasicInfo(orderBasicInfo, data);
@@ -495,6 +518,9 @@ public class NotDelayMerchantProgramLogic extends AbstractProgramLogic {
logger.info("结算订单:{}, 是订单完成状态", orderBasicInfo.getOrderCode());
return;
}
+ if (!ensureOrderPaymentReadyForSettlement(orderBasicInfo, data)) {
+ return;
+ }
// 获取更新数据后的orderBasicInfo对象
returnUpdateOrderBasicInfo(orderBasicInfo, data);
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/util/ChargeAmountUtils.java b/jsowell-pile/src/main/java/com/jsowell/pile/util/ChargeAmountUtils.java
new file mode 100644
index 000000000..31955c1a9
--- /dev/null
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/util/ChargeAmountUtils.java
@@ -0,0 +1,35 @@
+package com.jsowell.pile.util;
+
+import com.jsowell.common.constant.Constants;
+
+import java.math.BigDecimal;
+
+/**
+ * 启动充电金额工具类。
+ */
+public final class ChargeAmountUtils {
+
+ private ChargeAmountUtils() {
+ }
+
+ /**
+ * 按“前端金额 > 会员默认金额 > 代码默认金额”的优先级确定启动金额。
+ *
+ * @param frontendChargeAmount 前端传入的启动金额
+ * @param memberDefaultChargeAmount 会员配置的默认启动金额
+ * @return 最终启动金额
+ */
+ public static BigDecimal resolveStartupAmount(BigDecimal frontendChargeAmount, BigDecimal memberDefaultChargeAmount) {
+ if (frontendChargeAmount != null) {
+ return frontendChargeAmount;
+ }
+ if (isPositive(memberDefaultChargeAmount)) {
+ return memberDefaultChargeAmount;
+ }
+ return Constants.BALANCE_PAY_MAX_AMOUNT_NEW;
+ }
+
+ public static boolean isPositive(BigDecimal amount) {
+ return amount != null && amount.compareTo(BigDecimal.ZERO) > 0;
+ }
+}
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessDailySettlementDetailVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessDailySettlementDetailVO.java
new file mode 100644
index 000000000..749a345fe
--- /dev/null
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessDailySettlementDetailVO.java
@@ -0,0 +1,139 @@
+package com.jsowell.pile.vo.uniapp.business;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 结算日报详情VO
+ *
+ * @author zhangziao
+ * @date 2026/6/15
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class BusinessDailySettlementDetailVO {
+
+ /**
+ * 查询开始日期
+ */
+ private String startTime;
+
+ /**
+ * 查询结束日期
+ */
+ private String endTime;
+
+ /**
+ * 当前登录账号有权限的一级运营商id列表
+ */
+ private List merchantIdList;
+
+ /**
+ * 实际参与统计的站点数量
+ */
+ private Integer stationCount;
+
+ /**
+ * 总结算金额
+ */
+ private BigDecimal settlementAmount;
+
+ /**
+ * 收入总金额
+ */
+ private BigDecimal incomeTotalAmount;
+
+ /**
+ * 支出总金额
+ */
+ private BigDecimal expenseTotalAmount;
+
+ /**
+ * 总订单数
+ */
+ private BigDecimal totalOrderCount;
+
+ /**
+ * 订单总额
+ */
+ private BigDecimal orderTotalAmount;
+
+ /**
+ * 订单电量
+ */
+ private BigDecimal orderElectricity;
+
+ /**
+ * 充电服务费
+ */
+ private BigDecimal chargeServiceFee;
+
+ /**
+ * 充电电费
+ */
+ private BigDecimal chargeElectricityFee;
+
+ /**
+ * 平台给商户的服务费补贴
+ */
+ private BigDecimal platformServiceSubsidy;
+
+ /**
+ * 商户给平台的服务费
+ */
+ private BigDecimal merchantPlatformServiceFee;
+
+ /**
+ * 交易到账金额
+ */
+ private BigDecimal tradeAmount;
+
+ /**
+ * 交易手续费
+ */
+ private BigDecimal tradeFee;
+
+ /**
+ * 收入项列表
+ */
+ private List incomeItems;
+
+ /**
+ * 支出项列表
+ */
+ private List expenseItems;
+
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Builder
+ public static class SettlementItem {
+
+ /**
+ * 项编码
+ */
+ private String itemCode;
+
+ /**
+ * 项名称
+ */
+ private String itemName;
+
+ /**
+ * 单位
+ */
+ private String unit;
+
+ /**
+ * 金额
+ */
+ private BigDecimal amount;
+ }
+}
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessWorkbenchSummaryVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessWorkbenchSummaryVO.java
new file mode 100644
index 000000000..7c6315707
--- /dev/null
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/uniapp/business/BusinessWorkbenchSummaryVO.java
@@ -0,0 +1,114 @@
+package com.jsowell.pile.vo.uniapp.business;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 工作台首页汇总VO
+ *
+ * @author zhangziao
+ * @date 2026/6/15
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class BusinessWorkbenchSummaryVO {
+
+ /**
+ * 今日日期
+ */
+ private String today;
+
+ /**
+ * 近7天开始日期
+ */
+ private String recentStartTime;
+
+ /**
+ * 近7天结束日期
+ */
+ private String recentEndTime;
+
+ /**
+ * 当前登录账号有权限的一级运营商id列表
+ */
+ private List merchantIdList;
+
+ /**
+ * 实际参与统计的站点id列表
+ */
+ private List stationIdList;
+
+ /**
+ * 实际参与统计的站点数量
+ */
+ private Integer stationCount;
+
+ /**
+ * 今日订单量
+ */
+ private Long todayOrderCount;
+
+ /**
+ * 经营参谋近7天净现金流
+ */
+ private BigDecimal recentNetCashFlow;
+
+ /**
+ * 我的钱包今日到账
+ */
+ private BigDecimal todayArrivalAmount;
+
+ /**
+ * 结算日报近7天净现金流
+ */
+ private BigDecimal recentSettlementNetCashFlow;
+
+ /**
+ * 停车减免待处理申诉数
+ */
+ private Integer pendingParkingAppealCount;
+
+ /**
+ * 首页卡片列表
+ */
+ private List cardList;
+
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Builder
+ public static class WorkbenchCard {
+
+ /**
+ * 卡片编码
+ */
+ private String cardCode;
+
+ /**
+ * 卡片名称
+ */
+ private String cardName;
+
+ /**
+ * 指标名称
+ */
+ private String metricName;
+
+ /**
+ * 指标值
+ */
+ private BigDecimal metricValue;
+
+ /**
+ * 单位
+ */
+ private String unit;
+ }
+}
diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderPaymentCheckResult.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderPaymentCheckResult.java
new file mode 100644
index 000000000..1cd0b87f8
--- /dev/null
+++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/OrderPaymentCheckResult.java
@@ -0,0 +1,40 @@
+package com.jsowell.pile.vo.web;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 订单支付凭证校验结果。
+ *
+ * paymentEvidenceFound 表示底层支付流水是否真实存在;
+ * orderPaymentReady 表示订单主表字段是否已经可以安全参与后续业务。
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OrderPaymentCheckResult {
+
+ /** 本次校验是否正常完成;数据库/依赖异常时为 false,不能据此判定订单未支付 */
+ private boolean checkCompleted;
+
+ /** 是否存在真实支付凭证 */
+ private boolean paymentEvidenceFound;
+
+ /** 订单支付字段是否完整且一致 */
+ private boolean orderPaymentReady;
+
+ /** 从支付流水恢复出的支付金额 */
+ private BigDecimal paymentAmount;
+
+ /** 从支付流水恢复出的支付时间 */
+ private Date paymentTime;
+
+ /** 校验失败原因 */
+ private String reason;
+}
diff --git a/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml
index 64aab659b..1c860a140 100644
--- a/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml
+++ b/jsowell-pile/src/main/resources/mapper/pile/MemberBasicInfoMapper.xml
@@ -320,6 +320,7 @@
+
+
+
update order_basic_info
set order_status = #{orderStatus,jdbcType=VARCHAR}
diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java
index da1e6c748..fec2aabf9 100644
--- a/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java
+++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/JsowellTask.java
@@ -134,6 +134,8 @@ public class JsowellTask {
// 查询出最近2天支付成功,并且订单状态为未启动的订单
String startTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.addDays(new Date(), -2));
String endTime = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date());
+ // 先补偿/隔离支付凭证异常订单,避免后续启动失败任务把无支付订单当成正常订单结算退款。
+ orderBasicInfoService.reconcileChargingOrderPayments(startTime, endTime);
orderBasicInfoService.closeStartFailedOrder(startTime, endTime);
}