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

发布时间:2026/7/15 21:20:17
智慧充电系统定时任务处理异常订单实战方案 目录一、业务背景二、技术选型项目通用微服务架构三、定时任务整体设计3.1 任务拆分分多个定时任务各司其职降低单次执行压力3.2 通用前置约束防止并发脏数据四、核心定时任务实战代码SpringBoot XXL-Job4.1 通用父类封装公共工具方法4.2 任务 1僵死充电订单自动关停任务高频核心任务4.3 任务 2计费数据修复定时任务4.4 任务 3支付对账异常定时任务五、数据库关键 SQL 实战5.1 查询僵死超时充电订单5.2 查询计费异常订单5.3 标记订单异常更新语句六、生产环境避坑实战经验七、业务落地效果一、业务背景智慧充电系统每日产生大量充电订单充电桩离线、通讯断连、BMS 故障、报文丢失会生成僵死卡单、数据缺失、计费错乱、支付不匹配四类异常订单依靠人工排查效率极低。通过分布式定时任务轮询扫描、自动修复、告警归档实现异常订单半自动化处理减少人工对账与用户投诉。二、技术选型项目通用微服务架构定时框架XXL-Job分布式定时任务避免多实例重复执行支持分片、失败重试、日志告警存储MySQL 订单主表、异常订单记录表、充电桩实时状态表、支付流水表中间件Redis分布式锁、缓存充电桩在线状态、防重复处理订单、MQ异常消息推送运维 / 客服核心服务订单服务、充电桩通讯服务、计费服务、支付对账服务三、定时任务整体设计3.1 任务拆分分多个定时任务各司其职降低单次执行压力任务名称执行周期核心职责僵死订单清理任务每 5 分钟扫描长时间未结束、状态卡在「充电中」的订单自动关停计费数据修复任务每 10 分钟补全缺失起止电量、校验电量逻辑重新核算费用支付对账异常任务每 30 分钟对比订单应收与支付渠道流水处理重复扣款 / 未扣款离线桩遗留订单补偿任务每小时针对离线后重新上线充电桩拉取本地缓存充电记录补单异常订单汇总告警任务每日凌晨 2 点统计当日未修复异常订单生成报表推送运营后台3.2 通用前置约束防止并发脏数据分布式锁处理单订单前通过 Redis 加锁keylock:order:${orderId}过期时间 3min避免多服务同时处理同一订单分片查询不一次性全表扫描按订单创建时间分片、站点分片分页查询百万级订单不 OOM状态机校验仅处理固定状态订单充电中、待结算、待对账已归档、已退款订单直接过滤失败重试机制单次处理失败写入异常日志标记重试次数超过 3 次不再自动处理转为人工工单。四、核心定时任务实战代码SpringBoot XXL-Job4.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僵死充电订单自动关停任务高频核心任务业务规则查询条件订单状态 充电中、当前时间 - 订单启动时间 120 分钟阈值可配置校验充电桩实时状态若充电桩在线下发远程停止充电指令充电桩离线直接强制关闭订单关停后读取充电桩历史上报电量自动结算无电量则标记 0 元异常订单处理失败推送告警生成运维工单。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 ReturnTString 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); // 查询状态充电中、启动时间早于超时节点 ListOrderDO 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计费数据修复定时任务业务规则筛选异常订单订单标记异常、结束电量为空、起始电量大于结束电量、充电时长 0 但费用不为 0调取充电桩本地存储历史充电报文、离线缓存数据补全电量重新执行计费规则修正订单电费、服务费、优惠金额修正完成自动更新订单状态为已完成同步推送支付系统发起补扣 / 退款。javaComponent JobHandler(chargeDataRepairJob) public class ChargeDataRepairJob extends AbstractOrderJobHandler { Autowired private PileHistoryDataMapper historyDataMapper; Autowired private ChargeFeeCalcService feeCalcService; Autowired private PayService payService; Override public ReturnTString execute(String param) { XxlJobLogger.log(启动计费异常订单修复任务); // 查询计费异常订单 ListOrderDO 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支付对账异常定时任务核心逻辑匹配订单应收金额与微信 / 支付宝 / 钱包支付流水处理单边账、重复扣款、未支付已充电订单自动发起退款或追缴。javaComponent JobHandler(payReconciliationJob) public class PayReconciliationJob extends AbstractOrderJobHandler { Autowired private PayRecordMapper payRecordMapper; Autowired private PayRefundService refundService; Override public ReturnTString execute(String param) { // 查询待对账异常订单 ListOrderDO needReconOrder orderMapper.selectNeedReconciliationOrder(); for (OrderDO order : needReconOrder) { Long orderId order.getId(); if (!tryLock(orderId)) continue; try { // 查询该订单所有支付流水 ListPayRecordDO 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 查询僵死超时充电订单sqlSELECT 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 查询计费异常订单sqlSELECT * FROM charge_order WHERE order_status 3 -- 3异常订单 AND (end_power IS NULL OR start_power end_power OR charge_duration 0)5.3 标记订单异常更新语句sqlUPDATE charge_order SET order_status 3, exception_desc #{desc}, update_time NOW() WHERE id #{orderId}六、生产环境避坑实战经验禁止全表无分页查询充电订单数据量巨大不分页直接查询会造成数据库 CPU 打满必须分页 时间分片分布式锁必须兜底释放使用 try-finally 释放锁防止任务异常中断导致锁永久占用定时任务执行时间错开多任务不要同一时间启动例如 5 分、10 分、30 分错峰执行减少 DB 并发压力设置处理重试上限自动修复最多重试 3 次持续失败不再占用定时任务资源转为人工工单离线桩特殊兼容充电桩断电丢失本地数据时自动修复逻辑无法补全电量统一推送客服人工核验幂等保障所有修复、退款、停机操作基于订单状态机做前置判断避免重复停机、重复退款日志完整留存XXL-Job 内置日志记录每条异常订单处理过程方便线上问题回溯告警分级批量异常同一站点 10 条以上推送短信给运维单条异常仅后台站内消息避免消息轰炸。七、业务落地效果90% 常规僵死、电量缺失、单边账异常订单实现全自动处理无需运营每日手工排查充电桩离线遗留订单次日自动补全结算财务对账差异降低 85%用户因订单卡死、扣费错误产生的投诉量大幅下降定时任务日志 异常告警形成闭环快速定位故障充电桩、站点网络故障。