运维系统工单定时下发与更新方法

一.背景需求

某企业要定时下发工单,工单分为三种:定期周检、每日检修、巡回检查

定期周检:每周下发一次工单,检查多个设备,均匀分布到一周内检查,如:主电机周一检查,整流柜周二检查… 。

每日检修:每天下发一次工单(除了周末),多个设备都要检查。

巡回检查:每天下发一次工单(除了周末),每天0时下发,插入一条数据,之后2:00,4:00,6:00…每隔两小时插入一条最新数据,一直到22:00插入最后一条数据,当日的巡回检查就算结束。

数据从PLC取值,PLC值已经拿到数据库的plc_data表中

开发框架:Ruoyi

二.定时下发工单功能实现

DynamicScheduleService.java文件:实现定时器配置逻辑

WorkOrderGenerationService.java文件:工单构造逻辑(数据插入,更新,写入excel)

定时逻辑流程图:

1.定时器维护表:work_order_schedule

关键字段:

device_id:构造时间戳时使用

day_of_week:定期周检为设定的星期几,每日检修和巡回检查都设置为1

state:定时器状态,1为开启,0为关闭

reserved1:预留字段,存放详细设置的时间

2.定时触发器设置逻辑

(1)巡回检查

两小时更新一次数据。

if (schedule.getWorkOrderName().equals("主井巡回检查记录本(巡检工)") ||
    schedule.getWorkOrderName().equals("徐庄矿主要固定设备运行记录(主井绞车)(巡检工)")) {
    
    String cronExpression;
    if (schedule.getWorkOrderName().equals("徐庄矿主要固定设备运行记录(主井绞车)(巡检工)")) {
        cronExpression = "0 0 0,2,4,6,8,10,12,14,16,18,20,22 * * MON-FRI";   //使用硬编码Cron表达式实现两小时定时
    } else {
        cronExpression = "0 1 0,2,4,6,8,10,12,14,16,18,20,22 * * MON-FRI";   //同类型工单错峰执行,0点1分下发
    }
    trigger = new CronTrigger(cronExpression);
    ScheduledFuture<?> future = taskScheduler.schedule(task, trigger);
    scheduledTasks.put(schedule.getId(), future);
}

(2)每日检修

可以设置具体检修时刻

else if (StringUtils.isNotBlank(schedule.getExecuteTime()) ||            //判断存储详细时间的预留字段是否为空
         StringUtils.isNotBlank(schedule.getReserved1())) {
    
    String cronExpression = buildCronExpression(schedule);
    trigger = new CronTrigger(cronExpression);
    ScheduledFuture<?> future = taskScheduler.schedule(task, trigger);
    scheduledTasks.put(schedule.getId(), future);
}

(3)定期周检

都在周一下发,根据设备时间戳在APP端按照时间显示要检修的设备,因为都在周一下发,要处理定时逻辑,防止工单编号冲突

else {
    if(schedule.getWorkOrderName().equals("井筒装备安全间隙周检记录(主井)(机工)") || 
       schedule.getWorkOrderName().equals("主井防火门周检记录(机工)") || ... ) {
        
        String cronExpression = buildIntervalCronExpression(schedule);    //为每类工单配置单独的cron表达式,防冲突
        trigger = new CronTrigger(cronExpression);
        ScheduledFuture<?> future = taskScheduler.schedule(task, trigger);
        scheduledTasks.put(schedule.getId(), future);
    }
}

buildIntervalCronExpression:

private String buildIntervalCronExpression(WorkOrderSchedule schedule) {
        long intervalDays = schedule.getIntervalDays();

        // 生成随机秒数(0-59)
        int randomSecond = new Random().nextInt(60);   //生成随机秒数,加一层冲突防护

        // 不再使用固定的分钟偏移映射表
        // 改为基于设备ID或设备名称生成唯一的分钟偏移
        int minuteOffset = calculateMinuteOffset(schedule);  //弃用了

        // 根据间隔天数构建不同的Cron表达式
        String baseCron;

        //时间设置为0:02~0:08下发以下工单,防止与巡回检查(0:00、0:01)两个工单冲突
        switch ((int) intervalDays) {
            case 7:
                switch (schedule.getWorkOrderName()) {
                    case "井筒装备安全间隙周检记录(主井)(机工)":
                        return String.format("%d %d %d * * %s",randomSecond, 2, 0, "MON");
                    case "主井防火门周检记录(机工)":
                        return String.format("%d %d %d * * %s",randomSecond, 3, 0, "MON");
                    case "主井信号系统定期检修记录(电工)":
                        //闭锁试验:周五、设备检查:周一
                        if(schedule.getDeviceName().equals("闭锁试验")){
                            return String.format("%d %d %d * * %s",randomSecond, 4, 0, "MON");
                        }
                    case "主井衬垫槽深检查(机工)":
                        return String.format("%d %d %d * * %s",randomSecond, 5, 0, "MON");
                    case "防洪设施周检记录(机工)":
                        return String.format("%d %d %d * * %s",randomSecond, 6, 0, "MON");
                    case "主提升机电工定期点检记录(主井)(电工)":
                        if(schedule.getDeviceName().equals("主电机")){
                            return String.format("%d %d %d * * %s",randomSecond, 7, 0, "MON");
                        }
                        //高低压开关柜:周五、主电机:周一、整流柜:周二、外围控制器:周三 、快开:周四、控制柜:周五
                        return "";
                    case "主提升机机工定期点检记录(主井)(机工)":
                        //主轴装置、润滑:周一、减速器、制动:周二、尾绳、导向轮:周三 、井筒、箕斗:周四、防撞:周五
                        return String.format("%d %d %d * * %s",randomSecond, 8, 0, "MON");
                    default:
                        baseCron = "0 %d 0 * * MON";
                }
                break;
            case 1:
                baseCron = "0 %d 0 * * *";
                break;
            default:
                System.out.println("警告:Cron表达式不适合" + intervalDays + "天间隔的调度");
                baseCron = "0 %d 0 * * *";
        }
        return String.format("%d %d %d * * %s",randomSecond, 2, 0, "MON");
    }

解析每日检修具体时间,将其转换为cron表达式的方法如下:

    private String buildCronExpression(WorkOrderSchedule schedule) {
        try {
            // 解析执行时间(格式应为HH:mm)
                String executeTimeStr = schedule.getReserved1() != null ?
                        schedule.getReserved1() : "01:00";
                // 解析执行时间(格式应为HH:mm)
                LocalTime executeTime = LocalTime.parse(executeTimeStr);
                int hour = executeTime.getHour();
                int minute = executeTime.getMinute();
                // 构建Cron表达式
                // 格式: 秒 分 时 日 月 周
                // 这里我们设置为每天指定时间执行
                return String.format("0 %d %d * * *", minute, hour);
        } catch (Exception e) {
            System.err.println("解析执行时间失败,使用默认时间(8:00): " + e.getMessage());
            return "0 0 8 * * *"; // 默认早上8点执行
        }
    }

3.工单构造逻辑

(1)devicelist构造

// devicelist JSON格式
// 主提升机机工定期点检记录(主井)(机工)
private static final String FIXED_DEVICELIST_MainHoistOperatorPeriodic =
        "[[],[],[],[],[],[],[],[],[],[]," +
                "[],[],[],[],[],[],[],[],[0],[0]," +
                "[0],[0],[0],[0],[0],[0],[0]]";

注意不要误删”]”和”,”

(2)巡回检查表的构造

  • 如果是0时,创建一个工单,并更新一条数据(0:00)(使用createDailyInspectionWorkOrder方法)
  • 2:00、4:00等时间,调用接口selectTodayWorkOrderId从数据库中查找到当天最新工单(0时下发),进行数据插入。

注意:如果在0时之后,有人手动添加巡回检查工单,selectTodayWorkOrderId方法会找到最新的(手动添加)的工单进行数据更新

(3)取PLC数据

在appendInspectionRecord(更新工单)方法中取PLC数据。

// 使用selectPlcDataById方法查询单个PLC数据
PlcData plcData = plcDataMapper.selectPlcDataByDescription("模拟_主井提升_主画面_电枢电流");
if (plcData != null) {
    plcDataList.add(plcData);
} else {
    System.out.println("未找到设备_模拟_主井提升_主画面_电枢电流的PLC数据");
}

调用PLC数据接口selectPlcDataByDescription获取数据,填入的参数是要查找的数据名称

取值方式分为3类:

  • 能够从PLC直接取值的,输入正确的数据名称
  • 取不到的,plcData.setValue(null); 直接填入默认值”√” ( 在constructTimeSlotData方法中实现)
  • 工人自己填写的,plcData.setValue(“0”); 置空 (在constructTimeSlotData方法中实现)

构造效果:

(4)把PLC数据插入工单

    // 更新徐庄矿主要固定设备运行记录(主井绞车)(巡检工)对应的result字段
    private void update_MainEquip_ResultField(MainFixedEquipment equipment, int hour, String timeSlotData) {
        // 根据小时数更新对应的result字段
        switch (hour) {
            case 0:
                equipment.setResult1(timeSlotData);   //按照顺序依次插入Result
                break;

在子表中体现的位置是:

(5)时间戳构造

    //根据自定义的设备检修时间设置MaintenancePart时间戳
    private String buildMaintenancePart(WorkOrderSchedule schedule) {
        // 创建查询条件
        WorkOrderSchedule queryParam = new WorkOrderSchedule();
        queryParam.setWorkOrderName(schedule.getWorkOrderName());

        // 查询符合条件的设备列表
        List<WorkOrderSchedule> deviceList = workOrderScheduleService.selectWorkOrderScheduleList(queryParam);   

        if (deviceList == null || deviceList.isEmpty()) {
            return ""; // 返回空字符串
        }

        StringBuilder sb = new StringBuilder();

        // 检查是否为需要当前时间的工单类型
        boolean useCurrentTime = isUseCurrentTimeWorkOrder(schedule.getWorkOrderName());

        for (WorkOrderSchedule device : deviceList) {
            if (device.getState() == null || device.getState() == 0L) {
                continue; // 跳过未启用的设备
            }
            if (sb.length() > 0) {
                sb.append(", ");
            }

            long timestamp;
            int dayValue = 0;
                try {
                    String dayStr = device.getDayOfWeek();
                    if (dayStr != null && !dayStr.isEmpty()) {
                        dayValue = Integer.parseInt(dayStr);
                    }
                } catch (NumberFormatException e) {
                    // 如果转换失败,默认使用0
                }
                timestamp = System.currentTimeMillis() + (dayValue - 1) * 86400000L;

            // 构建设备字符串(不带引号)
            sb.append(device.getDeviceName())
                    .append("/")
                    .append(device.getDeviceId())
                    .append("/")
                    .append(timestamp);
        }

        return sb.toString();
    }

算例:

  • 定期周检:

星期一下发以下工单:

id work_order_name device_name device_id start_time end_time interval_days day_of_week state
16 主井信号系统定期检修记录(电工) 闭锁试验 47 2025-08-29 2026-07-28 7 5 1
17 主井信号系统定期检修记录(电工) 设备检查 48 2025-08-29 2026-07-28 7 1 1

buildMaintenancePart方法先根据工单名称主井信号系统定期检修记录(电工)去查表,得到该工单的设备列表,包括[闭锁试验,设备检查],然后为每个设备构造时间戳,关键代码:

timestamp = System.currentTimeMillis() + (dayValue - 1) * 86400000L;

时间戳构造:

闭锁试验 —— 星期一时间 +(5-1)*86400000L = 周五同时间下发 从星期一过完的基础上加4个整天之后,86400000L是一天的秒数。

设备检查 —— 星期一时间 +(1-1)*86400000L = 当时下发 从星期一基础上加0个整天之后,86400000L是一天的秒数。

在APP端,设备检查周一可用,闭锁试验周五可用

  • 每日检修:

在定时配置表work_order_schedule表中,所有每日检修工单数据项的day_of_week都为1。当计算timestamp时,设备就都是当天的时间戳了。

三.相关技术

1.cron表达式

Cron表达式是一种用于配置定时任务执行时间的字符串格式,由6或7个字段组成,各字段以空格分隔:

// 示例表达式
"0 0 8 * * MON-FRI"  // 每周工作日早上8点执行

字段含义(从左到右):

(0-59)分钟(0-59)小时(0-23)(1-31)(1-12或JAN-DEC)星期(0-7或SUN-SAT)(可选,1970-2099)

特殊字符

*:任意值

,:值列表分隔符(如MON,WED,FRI

-:范围(如8-17

/:步长(如0/15表示每15分钟)

?:不指定(用于日和星期互斥)

2.触发器类型对比

CronTrigger vs PeriodicTrigger

特性 CronTrigger PeriodicTrigger
精度 支持秒级精度 支持纳秒级精度
灵活性 复杂时间规则 固定间隔简单
适用场景 日历式调度 固定频率任务
示例 "0 0 12 * * ?"每天中午12点 Duration.ofHours(2)每2小时
资源消耗 中等 较低
异常处理 错过执行会跳过 会尝试补偿执行
动态调整 需重建触发器 可动态修改间隔

由于担心服务器故障或人为关闭可能会导致固定时间间隔失效,因此采用CronTrigger的方式,只要服务器启动,就按照固定时间执行任务。

3.使用哈希方法构造工单下发时间

    /**
     * 计算唯一的分钟偏移,避免同一工单类型的多个设备冲突
     */
    private int calculateMinuteOffset(WorkOrderSchedule schedule) {
            return Math.abs(schedule.getDeviceId().hashCode()) % 60; // 0-59分钟
    }

可以以同样思路避免多个相近时间下发的工单产生冲突,无需人为在后端配置时间防冲突。