运维系统工单定时下发与更新方法
一.背景需求
某企业要定时下发工单,工单分为三种:定期周检、每日检修、巡回检查。
定期周检:每周下发一次工单,检查多个设备,均匀分布到一周内检查,如:主电机周一检查,整流柜周二检查… 。
每日检修:每天下发一次工单(除了周末),多个设备都要检查。
巡回检查:每天下发一次工单(除了周末),每天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分钟
}
可以以同样思路避免多个相近时间下发的工单产生冲突,无需人为在后端配置时间防冲突。