SpringBoot 定时任务统一处理微信提现、订单状态同步(无人饺子机后台实战)
文章标题
SpringBoot @Scheduled 5 秒轮询定时任务实战:微信企业付款提现异步对账 + 自助机订单状态自动同步(含 Redis 队列、分表、微信转账回调处理)
文章标签
#SpringBoot #定时任务 Scheduled #微信企业付款对账 #Redis 队列 #分表 #自助售卖机后端 #工控饺子机
文章目录
- 业务场景说明
- 核心定时任务完整源码
- 代码分层逻辑拆解 3.1 Redis 提现队列异步对账(微信企业付款查询、超时自动撤销) 3.2 年度分表订单状态同步(Redis 订单缓存落库)
- 代码存在的风险与隐患
- 生产环境优化改造方案
- 适用项目场景
一、业务场景说明
本代码是无人值守 AI 饺子机自助项目后端定时调度核心类,使用@Scheduled(fixedRate = 5000)每 5 秒执行一次,承载两大核心异步业务:
- 微信企业付款提现对账(Redis 缓冲队列)用户发起提现后存入 Redis
withdraw数组队列,定时轮询逐个调用微信转账查询接口:- 转账成功:更新年度分表
tb_user_withdraw_yyyy提现记录状态,移除队列 - 转账失败 / 已撤销:直接移出队列
- 用户待确认超过 300 秒:自动调用撤销转账接口,冲减平台收益台账
- 转账成功:更新年度分表
- 自助机订单状态同步落库设备端取餐操作写入 Redis
OrderStatus_订单ID临时缓存,定时读取批量更新年度分表tb_pay_form_yyyy订单核销状态、取餐数量,清理过期 Redis 缓存。 - 配套能力:自动创建年度分表、读取系统全局超时配置、JdbcTemplate 批量更新 SQL。
技术栈:SpringBoot + Spring Scheduled + RedisTemplate + JdbcTemplate + 微信企业付款 API + 年度分表设计。
二、完整核心定时任务源码
package com.jbossjf.bootproject.service; import com.jbossjf.bootproject.common.weixin.WXPayUtility; import com.jbossjf.bootproject.common.CommonHelp; import com.jbossjf.bootproject.common.JsonGenericUtil; import com.jbossjf.bootproject.model.*; import com.jbossjf.bootproject.service.weixin.CancelTransferService; import com.jbossjf.bootproject.service.weixin.GetTransferBillByOutNoService; import com.jbossjf.bootproject.service.weixin.QueryByOutTradeNoService; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; @Component public class ScheduledTasks { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private PayFormNaticeService payFormNaticeService; @Autowired private CancelTransferService cancelTransferService; @Autowired QueryByOutTradeNoService queryByOutTradeNoService; @Autowired GetTransferBillByOutNoService getTransferBillByOutNoService; @Autowired UserWithdrawNaticeService userWithdrawNaticeService; @Autowired private RedisTemplate redisTemplate; @Autowired private DeviceService deviceService; @Autowired private SystemParametersService systemParametersService; /** * 每5秒执行一次定时调度 * 1. 处理Redis提现队列,微信企业付款对账、超时撤销 * 2. 读取系统配置取餐超时时间 * 3. 年度分表自动建表,同步Redis订单取餐状态至数据库 */ @Scheduled(fixedRate = 1000 * 5) public void performTask() { try { // ====================== 第一块:处理Redis提现队列 withdraw ====================== if(redisTemplate.hasKey("withdraw")) { Map<String, String> map = null; String json = redisTemplate.opsForValue().get("withdraw").toString(); if (!json.trim().equals("[]")) { List<WithdrawalNumberModel> withdrawalNumberModelList = JsonGenericUtil.jsonToList(json, WithdrawalNumberModel.class); SimpleDateFormat formatter1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH); // 倒序遍历,避免remove导致数组下标错乱 for (int i = withdrawalNumberModelList.size() - 1; i >= 0; i--) try{ Thread.sleep(10); map = (Map<String, String>) withdrawalNumberModelList.get(i); // 调用微信查询转账单接口 GetTransferBillByOutNoService.GetTransferBillByOutNoRequest request = new GetTransferBillByOutNoService.GetTransferBillByOutNoRequest(); request.outBillNo = map.get("out_trade_no"); GetTransferBillByOutNoService.TransferBillEntity response = getTransferBillByOutNoService.run(request); // 场景1:转账成功 SUCCESS if (response.state.name().equals("SUCCESS")) { SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy"); Date date = new Date(); String tableName = "tb_user_withdraw_" + yearFormat.format(date); // 不存在分表则自动创建 if (userWithdrawNaticeService.tableExists(tableName) == false) { userWithdrawNaticeService.createTable(tableName); } List<UserWithdraw> userWithdrawList = userWithdrawNaticeService.getUserWithdrawByIDTarget(tableName, response.outBillNo); // 更新提现记录状态与转账单号 String sql = "update " + tableName + " as tpl set tpl.transfer_bill_no = '" + response.transferBillNo + "' , tpl.status = '启用' where " + " tpl.id = '" + userWithdrawList.get(0).getId() + "';"; int u = jdbcTemplate.update(sql); if (u > 0) { // 处理完成移出队列,刷新Redis withdrawalNumberModelList.remove(i); String jsonStr = JsonGenericUtil.toJson(withdrawalNumberModelList); redisTemplate.opsForValue().set("withdraw", jsonStr); } } // 场景2:转账失败,直接移除队列 if (response.state.name().equals("FAIL")) { withdrawalNumberModelList.remove(i); String jsonStr = JsonGenericUtil.toJson(withdrawalNumberModelList); redisTemplate.opsForValue().set("withdraw", jsonStr); } // 场景3:转账已撤销,直接移除队列 if (response.state.name().equals("CANCELLED")) { withdrawalNumberModelList.remove(i); String jsonStr = JsonGenericUtil.toJson(withdrawalNumberModelList); redisTemplate.opsForValue().set("withdraw", jsonStr); } // 场景4:待用户确认超过300秒,自动撤销转账 if (response.state.name().equals("WAIT_USER_CONFIRM")) { Date temp_date = formatter1.parse(response.createTime); int def = CommonHelp.getDistanceDateTime(temp_date, new Date()); if (def >= (300)) { // 调用微信撤销转账接口 CancelTransferService.CancelTransferRequest request1 = new CancelTransferService.CancelTransferRequest(); request1.outBillNo = response.outBillNo; CancelTransferService.CancelTransferResponse response1 = cancelTransferService.run(request1); if (response1.state.equals("CANCELING")) { SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy"); Date date = new Date(); String tableName = "tb_user_withdraw_" + yearFormat.format(date); if (userWithdrawNaticeService.tableExists(tableName) == false) { userWithdrawNaticeService.createTable(tableName); } List<UserWithdraw> userWithdrawList = userWithdrawNaticeService.getUserWithdrawByIDTarget(tableName, response.outBillNo); // 撤销后冲减平台收益台账 String sql = " INSERT INTO tb_tota_userl_profit (id,name,user_id,in_amount,out_amount) " + " VALUES ('" + userWithdrawList.get(0).getUser_id() + "', '用户提现', '" + userWithdrawList.get(0).getUser_id() + "',0," + userWithdrawList.get(0).getAmount() + ") " + " ON DUPLICATE KEY UPDATE out_amount = out_amount - VALUES(out_amount); "; int u = jdbcTemplate.update(sql); if (u > 0) { withdrawalNumberModelList.remove(i); String jsonStr = JsonGenericUtil.toJson(withdrawalNumberModelList); redisTemplate.opsForValue().set("withdraw", jsonStr); } } } } }catch (Exception ex){ // 单条转账处理异常不阻断整体队列循环 ex.printStackTrace(); } } } // ====================== 第二块:读取系统配置,获取取餐超时阈值 ====================== List<SystemParameters> systemParametersList = systemParametersService.GetList(); int client_expired_time = 15000; for(int i = 0;i<systemParametersList.size();i++) { if(systemParametersList.get(i).getParam_name().equals("client_expired_time")) { client_expired_time = Integer.parseInt(systemParametersList.get(i).getParam_value()); } } // ====================== 第三块:订单年度分表自动创建 ====================== SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy"); Date date = new Date(); String tableName = "tb_pay_form_" + yearFormat.format(date); if (payFormNaticeService.tableExists(tableName) == false) { payFormNaticeService.createPayFormTable(tableName); } String tableItemName = "tb_pay_form_item_" + yearFormat.format(date); if (payFormNaticeService.tableExists(tableItemName) == false) { payFormNaticeService.createPayFormItemTable(tableItemName); } // ====================== 第四块:同步Redis订单取餐状态到数据库 ====================== List<PayForm> payFormList = payFormNaticeService.getPayFormUncollectedMealStatusDataTarget(tableName,tableItemName,client_expired_time); StringBuilder sqlSb = new StringBuilder(); String status = ""; int pick_up_quantity = 0; for(int i =0;i<payFormList.size();i++) { String redisKey = "OrderStatus" + payFormList.get(i).getId(); if(redisTemplate.hasKey(redisKey)) { try { String raw = redisTemplate.opsForValue().get(redisKey).toString(); JSONObject rootJson = new JSONObject(raw); pick_up_quantity = Integer.parseInt(rootJson.get("pick_up_quantity").toString()); String orderStatus = rootJson.get("status").toString(); String orderId = rootJson.get("id").toString(); if ("1".equals(orderStatus)) { status = "取餐"; sqlSb.append("UPDATE ").append(tableName) .append(" SET write_off_status = '").append(status).append("' ") .append(" WHERE id = '").append(orderId).append("';"); } else if ("0".equals(orderStatus)) { status = "未取餐"; sqlSb.append("UPDATE ").append(tableName) .append(" SET write_off_status = '").append(status).append("',pick_up_food_quantity = pick_up_food_quantity+ ").append(pick_up_quantity) .append(" WHERE id = '").append(orderId).append("';"); } // 更新完成删除Redis临时缓存 redisTemplate.delete(redisKey); }catch (Exception ex) { System.out.println("订单同步异常:" + ex.getMessage()); } } } // 批量执行所有更新SQL String batchSql = sqlSb.toString(); if(!batchSql.equals("")) { jdbcTemplate.update(batchSql); } }catch (Exception ex) { // 顶层捕获所有异常,防止定时任务直接终止 ex.printStackTrace(); } } }三、代码分层逻辑拆解
3.1 Redis 提现队列(withdraw)处理逻辑
- 存储结构:Redis String 存储 JSON 数组,保存待对账提现单据
- 遍历优化:倒序
for (i = size -1; i >=0),避免list.remove(i)导致下标错位、漏处理数据 - 四种微信转账状态分支
表格
转账状态 业务处理逻辑 SUCCESS 转账成功 更新年度提现分表状态,移出 Redis 队列 FAIL 转账失败 直接移除队列,放弃重试 CANCELLED 已撤销 直接移除队列 WAIT_USER_CONFIRM 待用户确认 超过 300 秒自动调用撤销转账接口,冲减平台收益台账 - 分表兼容:按年份动态拼接
tb_user_withdraw_yyyy,不存在则自动建表 - 异常隔离:单条提现单据捕获 Exception,单条失败不阻塞整条队列循环
3.2 自助机订单状态同步逻辑
- 缓存设计:设备安卓端取餐操作写入临时 Redis
OrderStatus_订单ID,定时任务统一落库,减少频繁直接操作数据库 - 年度分表:订单主表
tb_pay_form_yyyy、订单明细表tb_pay_form_item_yyyy,启动时自动检查表,不存在自动创建 - 批量 SQL 优化:循环拼接 UPDATE 语句,单次
jdbcTemplate.update()批量执行,减少数据库 IO - 缓存清理:同步完成后立即删除 Redis 订单状态 key,避免重复更新
- 动态配置:从
system_parameters读取全局client_expired_time取餐超时阈值,无需硬编码
3.3 定时任务基础配置说明
java
运行
@Scheduled(fixedRate = 1000 * 5)- fixedRate:固定 5 秒间隔执行,从上一次任务开始计时,如果任务执行耗时超过 5 秒会出现任务重叠并发执行;
- 区别于
fixedDelay:从上一次任务结束后再等待 5 秒,无并发风险。
四、现有代码存在的风险与隐患
4.1 定时任务并发重叠风险
使用fixedRate=5000,如果微信接口、数据库查询阻塞超过 5 秒,会同时启动多个定时线程:
- 重复操作 Redis 提现队列,同一单据多次调用微信转账查询接口
- 重复更新订单数据库,引发数据脏写、重复扣减收益
4.2 Redis 无分布式锁,集群环境数据错乱
多实例部署项目时,多台服务器同时读取withdraw队列,重复处理同一条提现记录,造成重复更新数据库、重复调用微信撤销接口。
4.3 SQL 拼接字符串,存在 SQL 注入漏洞
直接拼接userWithdrawList.get(0).getId()、订单 ID 等参数到 SQL 中,恶意字符可实现注入攻击。
4.4 Thread.sleep 阻塞定时线程
循环内手动Thread.sleep(10),拉长单次任务执行时长,加剧任务重叠问题,无业务意义。
4.5 异常捕获粒度不合理
- 微信 API 异常仅打印堆栈,无日志入库、无告警通知,线上故障无法及时发现;
- 顶层大 try-catch 吞掉全部异常,无法定位任务完全不执行的问题。
4.6 Redis 序列化 / 并发修改风险
直接读取 JSON 字符串转 List,循环中修改 List 后全量覆盖 Redis value;高并发下会出现数据丢失(多线程同时读取数组,后写入覆盖先写入的处理结果)。
4.7 硬编码魔法值过多
300 秒超时、5 秒定时、表前缀、状态文本启用/取餐/未取餐全部硬编码,后期维护修改成本极高。
4.8 批量 SQL 无事务
批量 UPDATE 订单时,部分 SQL 执行成功、部分失败,会出现订单状态不一致,无事务回滚机制。
五、生产环境优化改造方案
5.1 替换 fixedRate 为 fixedDelay,杜绝任务并发
java
运行
// 任务执行完成后,等待5秒再执行下一次,不会并发重叠 @Scheduled(fixedDelay = 5000)5.2 增加分布式锁(Redis Lock),支持多实例部署
使用 Redisson 锁,定时任务执行前抢占锁,未抢到直接退出,避免多实例重复处理:
java
运行
RLock lock = redissonClient.getLock("scheduled_task_lock"); try { boolean acquire = lock.tryLock(0, 30, TimeUnit.SECONDS); if (!acquire) return; // 原有业务逻辑 } finally { if(lock.isHeldByCurrentThread()) lock.unlock(); }5.3 预编译 SQL,杜绝 SQL 注入
使用JdbcTemplate.update(String sql, Object[] args)传参,禁止字符串拼接 ID、金额、状态等变量。
5.4 移除无用 Thread.sleep,增加日志分级打印
删除循环内Thread.sleep(10),替换 SLF4J 日志,区分 info/warn/error,异常打印完整堆栈并接入告警(邮件 / 钉钉)。
5.5 Redis 队列优化:改用 List LPOP/RPOP 原子出队
当前方案是全量读取数组、内存修改后覆盖写入,并发极易丢数据;优化为 Redis List 队列,每条提现记录单独一条数据,原子弹出,避免覆盖:
java
运行
// 弹出队尾一条单据,原子操作,多实例安全 String itemJson = redisTemplate.opsForList().rightPop("withdraw_queue");5.6 批量更新增加事务控制
批量更新订单、提现记录时,开启事务,任意一条更新失败全部回滚,保证数据一致性。
5.7 抽取常量类统一管理魔法值
新建ScheduleConstant.java,统一管理定时周期、超时秒数、Redis Key 前缀、数据库表前缀、状态码文本。
5.8 拆分超大定时任务
当前一个方法承载提现对账 + 订单同步两大重业务,建议拆分为两个独立@Scheduled方法,职责单一,故障互不影响。
六、适用项目场景
- 校园 / 社区无人 AI 饺子机、自助售货机、自助取餐柜后端服务
- 微信企业付款(商家转账到零钱)批量对账、异步提现系统
- 分表架构下定时同步缓存数据落库的后台调度服务
- 工控 Android 设备配套后端,设备端状态缓存统一持久化场景
- 中小型支付、提现类后台定时对账系统
文末结语
这套定时任务是自助餐饮设备后端真实落地代码,兼顾了 Redis 异步缓冲、微信支付对账、年度分表三大核心需求,但原生代码存在并发、安全、稳定性隐患。上线生产环境建议按照第五部分优化方案改造,增加分布式锁、预编译 SQL、事务、日志告警,保证多实例集群部署下的数据一致性与服务稳定性。
