文章标题

SpringBoot @Scheduled 5 秒轮询定时任务实战:微信企业付款提现异步对账 + 自助机订单状态自动同步(含 Redis 队列、分表、微信转账回调处理)

文章标签

#SpringBoot #定时任务 Scheduled #微信企业付款对账 #Redis 队列 #分表 #自助售卖机后端 #工控饺子机

文章目录

  1. 业务场景说明
  2. 核心定时任务完整源码
  3. 代码分层逻辑拆解 3.1 Redis 提现队列异步对账(微信企业付款查询、超时自动撤销) 3.2 年度分表订单状态同步(Redis 订单缓存落库)
  4. 代码存在的风险与隐患
  5. 生产环境优化改造方案
  6. 适用项目场景

一、业务场景说明

本代码是无人值守 AI 饺子机自助项目后端定时调度核心类,使用@Scheduled(fixedRate = 5000)每 5 秒执行一次,承载两大核心异步业务:

  1. 微信企业付款提现对账(Redis 缓冲队列) 用户发起提现后存入 Redis withdraw 数组队列,定时轮询逐个调用微信转账查询接口:
    • 转账成功:更新年度分表tb_user_withdraw_yyyy提现记录状态,移除队列
    • 转账失败 / 已撤销:直接移出队列
    • 用户待确认超过 300 秒:自动调用撤销转账接口,冲减平台收益台账
  2. 自助机订单状态同步落库 设备端取餐操作写入 Redis OrderStatus_订单ID 临时缓存,定时读取批量更新年度分表tb_pay_form_yyyy订单核销状态、取餐数量,清理过期 Redis 缓存。
  3. 配套能力:自动创建年度分表、读取系统全局超时配置、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)处理逻辑

  1. 存储结构:Redis String 存储 JSON 数组,保存待对账提现单据
  2. 遍历优化:倒序for (i = size -1; i >=0),避免list.remove(i)导致下标错位、漏处理数据
  3. 四种微信转账状态分支

    表格

    转账状态 业务处理逻辑
    SUCCESS 转账成功 更新年度提现分表状态,移出 Redis 队列
    FAIL 转账失败 直接移除队列,放弃重试
    CANCELLED 已撤销 直接移除队列
    WAIT_USER_CONFIRM 待用户确认 超过 300 秒自动调用撤销转账接口,冲减平台收益台账
  4. 分表兼容:按年份动态拼接tb_user_withdraw_yyyy,不存在则自动建表
  5. 异常隔离:单条提现单据捕获 Exception,单条失败不阻塞整条队列循环

3.2 自助机订单状态同步逻辑

  1. 缓存设计:设备安卓端取餐操作写入临时 Redis OrderStatus_订单ID,定时任务统一落库,减少频繁直接操作数据库
  2. 年度分表:订单主表tb_pay_form_yyyy、订单明细表tb_pay_form_item_yyyy,启动时自动检查表,不存在自动创建
  3. 批量 SQL 优化:循环拼接 UPDATE 语句,单次jdbcTemplate.update()批量执行,减少数据库 IO
  4. 缓存清理:同步完成后立即删除 Redis 订单状态 key,避免重复更新
  5. 动态配置:从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 异常捕获粒度不合理

  1. 微信 API 异常仅打印堆栈,无日志入库、无告警通知,线上故障无法及时发现;
  2. 顶层大 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方法,职责单一,故障互不影响。


六、适用项目场景

  1. 校园 / 社区无人 AI 饺子机、自助售货机、自助取餐柜后端服务
  2. 微信企业付款(商家转账到零钱)批量对账、异步提现系统
  3. 分表架构下定时同步缓存数据落库的后台调度服务
  4. 工控 Android 设备配套后端,设备端状态缓存统一持久化场景
  5. 中小型支付、提现类后台定时对账系统

文末结语

这套定时任务是自助餐饮设备后端真实落地代码,兼顾了 Redis 异步缓冲、微信支付对账、年度分表三大核心需求,但原生代码存在并发、安全、稳定性隐患。上线生产环境建议按照第五部分优化方案改造,增加分布式锁、预编译 SQL、事务、日志告警,保证多实例集群部署下的数据一致性与服务稳定性。

更多推荐