智慧充电系统定时任务处理异常订单实战方案

目录

一、业务背景

二、技术选型(项目通用微服务架构)

三、定时任务整体设计

3.1 任务拆分(分多个定时任务,各司其职,降低单次执行压力)

3.2 通用前置约束(防止并发脏数据)

四、核心定时任务实战代码(SpringBoot + XXL-Job)

4.1 通用父类封装(公共工具方法)

4.2 任务 1:僵死充电订单自动关停任务(高频核心任务)

4.3 任务 2:计费数据修复定时任务

4.4 任务 3:支付对账异常定时任务

五、数据库关键 SQL 实战

5.1 查询僵死超时充电订单

5.2 查询计费异常订单

5.3 标记订单异常更新语句

六、生产环境避坑实战经验

七、业务落地效果


一、业务背景

智慧充电系统每日产生大量充电订单,充电桩离线、通讯断连、BMS 故障、报文丢失会生成僵死卡单、数据缺失、计费错乱、支付不匹配四类异常订单,依靠人工排查效率极低。通过分布式定时任务轮询扫描、自动修复、告警归档,实现异常订单半自动化处理,减少人工对账与用户投诉。

二、技术选型(项目通用微服务架构)

  1. 定时框架:XXL-Job(分布式定时任务,避免多实例重复执行,支持分片、失败重试、日志告警)
  2. 存储:MySQL 订单主表、异常订单记录表、充电桩实时状态表、支付流水表
  3. 中间件:Redis(分布式锁、缓存充电桩在线状态、防重复处理订单)、MQ(异常消息推送运维 / 客服)
  4. 核心服务:订单服务、充电桩通讯服务、计费服务、支付对账服务

三、定时任务整体设计

3.1 任务拆分(分多个定时任务,各司其职,降低单次执行压力)

任务名称执行周期核心职责
僵死订单清理任务每 5 分钟扫描长时间未结束、状态卡在「充电中」的订单,自动关停
计费数据修复任务每 10 分钟补全缺失起止电量、校验电量逻辑,重新核算费用
支付对账异常任务每 30 分钟对比订单应收与支付渠道流水,处理重复扣款 / 未扣款
离线桩遗留订单补偿任务每小时针对离线后重新上线充电桩,拉取本地缓存充电记录补单
异常订单汇总告警任务每日凌晨 2 点统计当日未修复异常订单,生成报表推送运营后台

3.2 通用前置约束(防止并发脏数据)

  1. 分布式锁:处理单订单前通过 Redis 加锁,key=lock:order:${orderId},过期时间 3min,避免多服务同时处理同一订单;
  2. 分片查询:不一次性全表扫描,按订单创建时间分片、站点分片分页查询,百万级订单不 OOM;
  3. 状态机校验:仅处理固定状态订单(充电中、待结算、待对账),已归档、已退款订单直接过滤;
  4. 失败重试机制:单次处理失败写入异常日志,标记重试次数,超过 3 次不再自动处理,转为人工工单。

四、核心定时任务实战代码(SpringBoot + XXL-Job)

4.1 通用父类封装(公共工具方法)

java

/** * 异常订单定时任务公共父类 */ @Component public abstract class AbstractOrderJobHandler extends IJobHandler { @Autowired private StringRedisTemplate redisTemplate; @Autowired private OrderMapper orderMapper; @Autowired private MqProducer mqProducer; // 分布式锁前缀 private static final String ORDER_LOCK_KEY = "charge:job:order:lock:"; // 锁过期时间 3分钟 private static final long LOCK_EXPIRE = 180L; /** * 获取分布式锁 */ public boolean tryLock(Long orderId) { String key = ORDER_LOCK_KEY + orderId; Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_EXPIRE, TimeUnit.SECONDS); return Boolean.TRUE.equals(lock); } /** * 释放锁 */ public void unLock(Long orderId) { String key = ORDER_LOCK_KEY + orderId; redisTemplate.delete(key); } /** * 发送异常告警消息 */ public void sendAlarm(OrderDO order, String errorMsg) { AlarmDTO alarm = new AlarmDTO(); alarm.setOrderId(order.getId()); alarm.setPileCode(order.getPileCode()); alarm.setAlarmType("充电订单异常"); alarm.setContent(errorMsg); alarm.setCreateTime(new Date()); mqProducer.sendOrderAlarm(alarm); } /** * 更新订单为异常状态,记录异常信息 */ public void markOrderException(Long orderId, String exceptionDesc) { OrderDO update = new OrderDO(); update.setId(orderId); update.setOrderStatus(OrderStatusEnum.EXCEPTION.getCode()); update.setExceptionDesc(exceptionDesc); update.setUpdateTime(new Date()); orderMapper.updateById(update); } }

4.2 任务 1:僵死充电订单自动关停任务(高频核心任务)

业务规则:

  1. 查询条件:订单状态 = 充电中、当前时间 - 订单启动时间 > 120 分钟(阈值可配置);
  2. 校验充电桩实时状态:若充电桩在线,下发远程停止充电指令;充电桩离线直接强制关闭订单;
  3. 关停后读取充电桩历史上报电量,自动结算,无电量则标记 0 元异常订单;
  4. 处理失败推送告警,生成运维工单。

java

/** * XXL-Job定时任务:僵死充电订单清理,每5分钟执行 */ @Component @JobHandler("deadChargeOrderJob") public class DeadChargeOrderJob extends AbstractOrderJobHandler { @Autowired private PileClient pileClient; @Autowired private ChargeFeeCalcService feeCalcService; // 超时时长 单位分钟,配置文件读取 @Value("${charge.job.dead.order.timeout:120}") private Integer deadTimeoutMin; @Override public ReturnT<String> execute(String param) throws Exception { XxlJobLogger.log("开始执行僵死充电订单定时任务"); long now = System.currentTimeMillis(); long timeoutTime = now - TimeUnit.MINUTES.toMillis(deadTimeoutMin); // 分页分片查询超时未结束充电订单 int pageNum = 1; int pageSize = 200; while (true) { PageHelper.startPage(pageNum, pageSize); // 查询:状态充电中、启动时间早于超时节点 List<OrderDO> deadOrderList = orderMapper.selectDeadChargeOrder(timeoutTime); if (CollectionUtils.isEmpty(deadOrderList)) { break; } for (OrderDO order : deadOrderList) { Long orderId = order.getId(); // 尝试加锁,处理中订单跳过 if (!tryLock(orderId)) { XxlJobLogger.log("订单{}正在被其他任务处理,跳过", orderId); continue; } try { handleDeadSingleOrder(order); } catch (Exception e) { XxlJobLogger.log("处理僵死订单{}异常:{}", orderId, e.getMessage()); sendAlarm(order, "僵死订单自动关停失败:" + e.getMessage()); } finally { unLock(orderId); } } pageNum++; } XxlJobLogger.log("僵死订单任务执行完成"); return ReturnT.SUCCESS; } /** * 处理单条僵死订单 */ private void handleDeadSingleOrder(OrderDO order) { String pileCode = order.getPileCode(); boolean pileOnline = pileClient.checkPileOnline(pileCode); // 充电桩在线,下发停机指令 if (pileOnline) { boolean stopResult = pileClient.remoteStopCharge(pileCode, order.getGunNo()); if (!stopResult) { markOrderException(order.getId(), "远程下发停机指令失败,桩无响应"); return; } // 等待5s,拉取桩最新计量数据 Thread.sleep(5000); } // 获取桩上报的最终电量 ChargeDataDTO lastData = pileClient.getPileLatestChargeData(pileCode, order.getGunNo()); // 计费核算 ChargeResultDTO chargeResult = feeCalcService.calcFee(order, lastData); // 更新订单为已完成/异常 if (chargeResult.getValidData()) { // 正常结算 orderMapper.closeNormalOrder(order.getId(), chargeResult); } else { markOrderException(order.getId(), "僵死订单无有效充电计量数据"); } } }

4.3 任务 2:计费数据修复定时任务

业务规则:

  1. 筛选异常订单:订单标记异常、结束电量为空、起始电量大于结束电量、充电时长 0 但费用不为 0;
  2. 调取充电桩本地存储历史充电报文、离线缓存数据补全电量;
  3. 重新执行计费规则,修正订单电费、服务费、优惠金额;
  4. 修正完成自动更新订单状态为已完成,同步推送支付系统发起补扣 / 退款。

java

@Component @JobHandler("chargeDataRepairJob") public class ChargeDataRepairJob extends AbstractOrderJobHandler { @Autowired private PileHistoryDataMapper historyDataMapper; @Autowired private ChargeFeeCalcService feeCalcService; @Autowired private PayService payService; @Override public ReturnT<String> execute(String param) { XxlJobLogger.log("启动计费异常订单修复任务"); // 查询计费异常订单 List<OrderDO> errorFeeOrders = orderMapper.selectFeeExceptionOrder(); for (OrderDO order : errorFeeOrders) { Long orderId = order.getId(); if (!tryLock(orderId)) continue; try { // 查询充电桩本地历史充电记录补全电量 PileHistoryDataDO history = historyDataMapper.selectByOrderId(orderId); if (history == null) { sendAlarm(order, "无桩历史充电数据,无法自动修复"); continue; } // 重新计费 ChargeResultDTO newResult = feeCalcService.calcFeeByHistory(order, history); // 更新订单计费信息 orderMapper.repairOrderFee(orderId, newResult); // 金额变更执行退款/补费 payService.reconciliationOrderPay(orderId, newResult); XxlJobLogger.log("订单{}计费修复成功", orderId); } catch (Exception e) { sendAlarm(order, "计费修复失败:" + e.getMessage()); } finally { unLock(orderId); } } return ReturnT.SUCCESS; } }

4.4 任务 3:支付对账异常定时任务

核心逻辑:匹配订单应收金额与微信 / 支付宝 / 钱包支付流水,处理单边账、重复扣款、未支付已充电订单,自动发起退款或追缴。

java

@Component @JobHandler("payReconciliationJob") public class PayReconciliationJob extends AbstractOrderJobHandler { @Autowired private PayRecordMapper payRecordMapper; @Autowired private PayRefundService refundService; @Override public ReturnT<String> execute(String param) { // 查询待对账异常订单 List<OrderDO> needReconOrder = orderMapper.selectNeedReconciliationOrder(); for (OrderDO order : needReconOrder) { Long orderId = order.getId(); if (!tryLock(orderId)) continue; try { // 查询该订单所有支付流水 List<PayRecordDO> payList = payRecordMapper.selectByOrderId(orderId); // 对账逻辑 ReconResultDTO recon = payService.reconOrderAmount(order, payList); if (recon.getRefundFlag()) { // 多扣款,自动退款 refundService.autoRefund(orderId, recon.getRefundAmount()); } else if (recon.getNeedChargeFlag()) { // 未扣费,标记待人工追缴 markOrderException(orderId, "支付流水缺失,存在欠费"); } else { // 对账完成,清除异常标记 orderMapper.clearOrderException(orderId); } } catch (Exception e) { sendAlarm(order, "支付对账异常:" + e.getMessage()); } finally { unLock(orderId); } } return ReturnT.SUCCESS; } }

五、数据库关键 SQL 实战

5.1 查询僵死超时充电订单

sql

SELECT id, order_no, pile_code, gun_no, start_time, user_id FROM charge_order WHERE order_status = 1 -- 1=充电中 AND start_time < #{timeoutTime} LIMIT #{offset}, #{pageSize}

5.2 查询计费异常订单

sql

SELECT * FROM charge_order WHERE order_status = 3 -- 3=异常订单 AND (end_power IS NULL OR start_power > end_power OR charge_duration = 0)

5.3 标记订单异常更新语句

sql

UPDATE charge_order SET order_status = 3, exception_desc = #{desc}, update_time = NOW() WHERE id = #{orderId}

六、生产环境避坑实战经验

  1. 禁止全表无分页查询:充电订单数据量巨大,不分页直接查询会造成数据库 CPU 打满,必须分页 + 时间分片;
  2. 分布式锁必须兜底释放:使用 try-finally 释放锁,防止任务异常中断导致锁永久占用;
  3. 定时任务执行时间错开:多任务不要同一时间启动,例如 5 分、10 分、30 分错峰执行,减少 DB 并发压力;
  4. 设置处理重试上限:自动修复最多重试 3 次,持续失败不再占用定时任务资源,转为人工工单;
  5. 离线桩特殊兼容:充电桩断电丢失本地数据时,自动修复逻辑无法补全电量,统一推送客服人工核验;
  6. 幂等保障:所有修复、退款、停机操作基于订单状态机做前置判断,避免重复停机、重复退款;
  7. 日志完整留存:XXL-Job 内置日志记录每条异常订单处理过程,方便线上问题回溯;
  8. 告警分级:批量异常(同一站点 10 条以上)推送短信给运维,单条异常仅后台站内消息,避免消息轰炸。

七、业务落地效果

  1. 90% 常规僵死、电量缺失、单边账异常订单实现全自动处理,无需运营每日手工排查;
  2. 充电桩离线遗留订单次日自动补全结算,财务对账差异降低 85%;
  3. 用户因订单卡死、扣费错误产生的投诉量大幅下降;
  4. 定时任务日志 + 异常告警形成闭环,快速定位故障充电桩、站点网络故障。