gongchunyi
4 天以前 64de92ae028b582b7fbe8d3c84eb568edf0d400f
feat: 安全设施巡检调整
已修改4个文件
已添加8个文件
907 ■■■■■ 文件已修改
doc/20260730_safe_facility_inspection_task.sql 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeFacilityInspectionTaskController.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeFacilityLedgerController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/mapper/SafeFacilityInspectionTaskMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/pojo/SafeFacilityInspection.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/pojo/SafeFacilityInspectionTask.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/service/SafeFacilityInspectionTaskService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/service/impl/SafeFacilityInspectionServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/service/impl/SafeFacilityInspectionTaskJob.java 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/service/impl/SafeFacilityInspectionTaskScheduler.java 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/service/impl/SafeFacilityInspectionTaskServiceImpl.java 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/safe/SafeFacilityInspectionMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260730_safe_facility_inspection_task.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
CREATE TABLE IF NOT EXISTS `safe_facility_inspection_task` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '任务ID',
  `inspection_name` varchar(100) DEFAULT NULL COMMENT '巡检任务名称',
  `facility_id` int DEFAULT NULL COMMENT '设施ID',
  `facility_code` varchar(100) DEFAULT NULL COMMENT '设施编号',
  `facility_name` varchar(100) DEFAULT NULL COMMENT '设施名称',
  `inspection_project` varchar(1000) DEFAULT NULL COMMENT '巡检项目',
  `inspector_id` int DEFAULT NULL COMMENT '巡检人ID',
  `frequency_type` varchar(20) DEFAULT NULL COMMENT '频率类型 DAILY/WEEKLY/MONTHLY',
  `frequency_detail` varchar(50) DEFAULT NULL COMMENT '频率详情:每日HH:mm;每周MON,HH:mm;每月DD,HH:mm',
  `next_execution_time` datetime DEFAULT NULL COMMENT '下次执行时间',
  `last_execution_time` datetime DEFAULT NULL COMMENT '最后执行时间',
  `is_enabled` tinyint DEFAULT '1' COMMENT '是否启用 0否 1是',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `create_user` int DEFAULT NULL COMMENT '创建人ID',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `update_user` int DEFAULT NULL COMMENT '更新人ID',
  `tenant_id` int DEFAULT NULL COMMENT '租户ID',
  `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_safe_facility_inspection_task_facility` (`facility_id`) USING BTREE,
  KEY `idx_safe_facility_inspection_task_enabled` (`is_enabled`, `next_execution_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='安全生产-安全设施定时巡检任务';
ALTER TABLE `safe_facility_inspection`
  ADD COLUMN `schedule_task_id` bigint DEFAULT NULL COMMENT '定时巡检任务ID' AFTER `id`,
  ADD COLUMN `inspection_project` varchar(1000) DEFAULT NULL COMMENT '巡检项目' AFTER `inspection_type`,
  ADD KEY `idx_safe_facility_inspection_schedule_task` (`schedule_task_id`) USING BTREE;
src/main/java/com/ruoyi/safe/controller/SafeFacilityInspectionTaskController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
package com.ruoyi.safe.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.safe.pojo.SafeFacilityInspectionTask;
import com.ruoyi.safe.service.SafeFacilityInspectionTaskService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.quartz.SchedulerException;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * <p>
 * å®‰å…¨ç”Ÿäº§--安全设施定时巡检任务 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 */
@RestController
@RequestMapping("/safeFacilityInspectionTask")
@AllArgsConstructor
@Tag(name = "安全生产--安全设施定时巡检任务")
public class SafeFacilityInspectionTaskController {
    private SafeFacilityInspectionTaskService safeFacilityInspectionTaskService;
    @GetMapping("/page")
    @Operation(summary = "分页查询")
    public R page(Page<SafeFacilityInspectionTask> page, SafeFacilityInspectionTask task) {
        return R.ok(safeFacilityInspectionTaskService.pageSafeFacilityInspectionTask(page, task));
    }
    @GetMapping("/{id}")
    @Operation(summary = "根据ID查询")
    public R getById(@PathVariable Long id) {
        return R.ok(safeFacilityInspectionTaskService.getById(id));
    }
    @Operation(summary = "新增安全设施定时巡检任务")
    @Log(title = "安全设施定时巡检任务", businessType = BusinessType.INSERT)
    @PostMapping()
    public R add(@RequestBody SafeFacilityInspectionTask task) throws SchedulerException {
        return R.ok(safeFacilityInspectionTaskService.saveOrUpdateTask(task));
    }
    @Operation(summary = "修改安全设施定时巡检任务")
    @Log(title = "安全设施定时巡检任务", businessType = BusinessType.UPDATE)
    @PutMapping()
    public R update(@RequestBody SafeFacilityInspectionTask task) throws SchedulerException {
        return R.ok(safeFacilityInspectionTaskService.saveOrUpdateTask(task));
    }
    @Operation(summary = "删除安全设施定时巡检任务")
    @Log(title = "安全设施定时巡检任务", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    public R delete(@PathVariable List<Long> ids) {
        return R.ok(safeFacilityInspectionTaskService.removeTaskByIds(ids));
    }
}
src/main/java/com/ruoyi/safe/controller/SafeFacilityLedgerController.java
@@ -5,8 +5,10 @@
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.safe.pojo.SafeFacilityInspection;
import com.ruoyi.safe.pojo.SafeFacilityInspectionTask;
import com.ruoyi.safe.pojo.SafeFacilityLedger;
import com.ruoyi.safe.service.SafeFacilityInspectionService;
import com.ruoyi.safe.service.SafeFacilityInspectionTaskService;
import com.ruoyi.safe.service.SafeFacilityLedgerService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -31,6 +33,7 @@
    private SafeFacilityLedgerService safeFacilityLedgerService;
    private SafeFacilityInspectionService safeFacilityInspectionService;
    private SafeFacilityInspectionTaskService safeFacilityInspectionTaskService;
    @GetMapping("/page")
    @Operation(summary = "分页查询")
@@ -78,6 +81,15 @@
    public R delSafeFacilityLedger(@PathVariable List<Integer> ids) {
        // æ ¡éªŒè®¾æ–½æ˜¯å¦æœ‰å…³è”的巡检记录
        for (Integer id : ids) {
            long taskCount = safeFacilityInspectionTaskService.lambdaQuery()
                    .eq(SafeFacilityInspectionTask::getFacilityId, id)
                    .count();
            if (taskCount > 0) {
                SafeFacilityLedger ledger = safeFacilityLedgerService.getById(id);
                String name = ledger != null ? ledger.getFacilityName() : String.valueOf(id);
                return R.fail("设施「" + name + "」存在关联的巡检任务,无法删除");
            }
            long count = safeFacilityInspectionService.lambdaQuery()
                    .eq(com.ruoyi.safe.pojo.SafeFacilityInspection::getFacilityId, id)
                    .count();
src/main/java/com/ruoyi/safe/mapper/SafeFacilityInspectionTaskMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.safe.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.safe.pojo.SafeFacilityInspectionTask;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®‰å…¨ç”Ÿäº§--安全设施定时巡检任务 Mapper æŽ¥å£
 * </p>
 */
@Mapper
public interface SafeFacilityInspectionTaskMapper extends BaseMapper<SafeFacilityInspectionTask> {
}
src/main/java/com/ruoyi/safe/pojo/SafeFacilityInspection.java
@@ -36,6 +36,9 @@
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    @Schema(description = "定时巡检任务ID")
    private Long scheduleTaskId;
    @Schema(description = "巡检编号")
    private String inspectionCode;
@@ -45,6 +48,9 @@
    @Schema(description = "巡检类型(定期/临时)")
    private String inspectionType;
    @Schema(description = "巡检项目")
    private String inspectionProject;
    @Schema(description = "计划巡检时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
src/main/java/com/ruoyi/safe/pojo/SafeFacilityInspectionTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
package com.ruoyi.safe.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®‰å…¨ç”Ÿäº§--安全设施定时巡检任务
 * </p>
 */
@Data
@TableName("safe_facility_inspection_task")
@Schema(name = "SafeFacilityInspectionTask对象", description = "安全生产--安全设施定时巡检任务")
public class SafeFacilityInspectionTask implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "巡检任务名称")
    private String inspectionName;
    @Schema(description = "设施ID")
    private Integer facilityId;
    @Schema(description = "设施编号")
    private String facilityCode;
    @Schema(description = "设施名称")
    private String facilityName;
    @Schema(description = "巡检项目")
    private String inspectionProject;
    @Schema(description = "巡检人ID")
    private Integer inspectorId;
    @Schema(description = "频率类型 DAILY/WEEKLY/MONTHLY")
    private String frequencyType;
    @Schema(description = "频率详情")
    private String frequencyDetail;
    @Schema(description = "下次执行时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime nextExecutionTime;
    @Schema(description = "最后执行时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime lastExecutionTime;
    @Schema(description = "是否启用 0否 1是")
    private Integer isEnabled;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @Schema(description = "创建人ID")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @Schema(description = "更新人ID")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @Schema(description = "租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Integer tenantId;
    @Schema(description = "部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @TableField(exist = false)
    private String inspectorName;
}
src/main/java/com/ruoyi/safe/service/SafeFacilityInspectionTaskService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.safe.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.safe.pojo.SafeFacilityInspectionTask;
import org.quartz.SchedulerException;
import java.time.LocalDateTime;
import java.util.List;
/**
 * <p>
 * å®‰å…¨ç”Ÿäº§--安全设施定时巡检任务 æœåŠ¡ç±»
 * </p>
 */
public interface SafeFacilityInspectionTaskService extends IService<SafeFacilityInspectionTask> {
    IPage<SafeFacilityInspectionTask> pageSafeFacilityInspectionTask(Page<SafeFacilityInspectionTask> page, SafeFacilityInspectionTask task);
    boolean saveOrUpdateTask(SafeFacilityInspectionTask task) throws SchedulerException;
    boolean removeTaskByIds(List<Long> ids);
    LocalDateTime calculateFirstExecutionTime(String frequencyType, String frequencyDetail);
    LocalDateTime calculateNextExecutionTime(String frequencyType, String frequencyDetail, LocalDateTime currentTime);
}
src/main/java/com/ruoyi/safe/service/impl/SafeFacilityInspectionServiceImpl.java
@@ -6,6 +6,7 @@
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.safe.pojo.SafeFacilityInspection;
@@ -54,9 +55,10 @@
    @Override
    public IPage<SafeFacilityInspection> pageSafeFacilityInspection(Page page, SafeFacilityInspection safeFacilityInspection) {
        IPage<SafeFacilityInspection> result = this.lambdaQuery()
                .like(safeFacilityInspection.getInspectionCode() != null, SafeFacilityInspection::getInspectionCode, safeFacilityInspection.getInspectionCode())
                .eq(safeFacilityInspection.getScheduleTaskId() != null, SafeFacilityInspection::getScheduleTaskId, safeFacilityInspection.getScheduleTaskId())
                .like(StringUtils.isNotBlank(safeFacilityInspection.getInspectionCode()), SafeFacilityInspection::getInspectionCode, safeFacilityInspection.getInspectionCode())
                .eq(safeFacilityInspection.getFacilityId() != null, SafeFacilityInspection::getFacilityId, safeFacilityInspection.getFacilityId())
                .eq(safeFacilityInspection.getStatus() != null, SafeFacilityInspection::getStatus, safeFacilityInspection.getStatus())
                .eq(StringUtils.isNotBlank(safeFacilityInspection.getStatus()), SafeFacilityInspection::getStatus, safeFacilityInspection.getStatus())
                .orderByDesc(SafeFacilityInspection::getCreateTime)
                .page(page);
src/main/java/com/ruoyi/safe/service/impl/SafeFacilityInspectionTaskJob.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,169 @@
package com.ruoyi.safe.service.impl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.safe.mapper.SafeFacilityInspectionMapper;
import com.ruoyi.safe.mapper.SafeFacilityInspectionTaskMapper;
import com.ruoyi.safe.pojo.SafeFacilityInspection;
import com.ruoyi.safe.pojo.SafeFacilityInspectionTask;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.Set;
@Component
@DisallowConcurrentExecution
public class SafeFacilityInspectionTaskJob implements Job, Serializable {
    private static final long serialVersionUID = 1L;
    private static final DateTimeFormatter CODE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    @Autowired
    private SafeFacilityInspectionTaskMapper safeFacilityInspectionTaskMapper;
    @Autowired
    private SafeFacilityInspectionMapper safeFacilityInspectionMapper;
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        Long taskId = jobDataMap.getLong("taskId");
        try {
            SafeFacilityInspectionTask task = safeFacilityInspectionTaskMapper.selectById(taskId);
            if (task == null || Integer.valueOf(0).equals(task.getIsEnabled())) {
                return;
            }
            LocalDateTime executionTime = LocalDateTime.now();
            SafeFacilityInspection inspection = createFacilityInspection(task, executionTime);
            safeFacilityInspectionMapper.insert(inspection);
            task.setLastExecutionTime(executionTime);
            task.setNextExecutionTime(calculateNextExecutionTime(
                    task.getFrequencyType(),
                    task.getFrequencyDetail(),
                    executionTime
            ));
            safeFacilityInspectionTaskMapper.updateById(task);
        } catch (Exception e) {
            throw new JobExecutionException(e);
        }
    }
    private SafeFacilityInspection createFacilityInspection(SafeFacilityInspectionTask task, LocalDateTime executionTime) {
        SafeFacilityInspection inspection = new SafeFacilityInspection();
        inspection.setScheduleTaskId(task.getId());
        inspection.setInspectionCode("SSXJ-" + executionTime.format(CODE_FORMATTER) + "-" + task.getId());
        inspection.setFacilityId(task.getFacilityId());
        inspection.setInspectionType("定期巡检");
        inspection.setInspectionProject(task.getInspectionProject());
        inspection.setPlanTime(task.getNextExecutionTime() != null ? task.getNextExecutionTime() : executionTime);
        inspection.setInspectorId(task.getInspectorId());
        inspection.setCheckDesc(buildCheckDesc(task));
        inspection.setStatus("待巡检");
        inspection.setCreateUser(task.getCreateUser());
        inspection.setUpdateUser(task.getCreateUser());
        inspection.setCreateTime(executionTime);
        inspection.setUpdateTime(executionTime);
        inspection.setTenantId(task.getTenantId());
        inspection.setDeptId(task.getDeptId());
        return inspection;
    }
    private String buildCheckDesc(SafeFacilityInspectionTask task) {
        String desc = "自动生成自定时任务ID: " + task.getId();
        if (StringUtils.isNotBlank(task.getRemark())) {
            desc = desc + ";" + task.getRemark();
        }
        return desc;
    }
    private LocalDateTime calculateNextExecutionTime(String frequencyType,
                                                     String frequencyDetail,
                                                     LocalDateTime currentTime) {
        return switch (frequencyType) {
            case "DAILY" -> calculateDailyNextTime(frequencyDetail, currentTime);
            case "WEEKLY" -> calculateWeeklyNextTime(frequencyDetail, currentTime);
            case "MONTHLY" -> calculateMonthlyNextTime(frequencyDetail, currentTime);
            default -> throw new IllegalArgumentException("不支持的频率类型: " + frequencyType);
        };
    }
    private LocalDateTime calculateDailyNextTime(String timeStr, LocalDateTime current) {
        LocalTime executionTime = LocalTime.parse(timeStr);
        LocalDateTime nextTime = LocalDateTime.of(current.toLocalDate(), executionTime);
        return current.isBefore(nextTime) ? nextTime : nextTime.plusDays(1);
    }
    private LocalDateTime calculateWeeklyNextTime(String detail, LocalDateTime current) {
        String[] parts = detail.split(",");
        LocalTime time = LocalTime.parse(parts[1].trim());
        Set<DayOfWeek> targetDays = parseDayOfWeeks(parts[0]);
        LocalDateTime nextTime = current;
        while (true) {
            nextTime = nextTime.plusDays(1);
            if (targetDays.contains(nextTime.getDayOfWeek())) {
                return LocalDateTime.of(nextTime.toLocalDate(), time);
            }
            if (nextTime.isAfter(current.plusYears(1))) {
                throw new RuntimeException("无法找到下次执行时间");
            }
        }
    }
    private LocalDateTime calculateMonthlyNextTime(String detail, LocalDateTime current) {
        String[] parts = detail.split(",");
        int dayOfMonth = Integer.parseInt(parts[0].trim());
        LocalTime time = LocalTime.parse(parts[1].trim());
        YearMonth currentMonth = YearMonth.from(current);
        for (int i = 0; i < 24; i++) {
            YearMonth targetMonth = currentMonth.plusMonths(i);
            if (dayOfMonth > targetMonth.lengthOfMonth()) {
                continue;
            }
            LocalDateTime candidate = LocalDateTime.of(
                    targetMonth.getYear(),
                    targetMonth.getMonthValue(),
                    dayOfMonth,
                    time.getHour(),
                    time.getMinute()
            );
            if (current.isBefore(candidate)) {
                return candidate;
            }
        }
        throw new IllegalArgumentException("无法计算下次执行时间");
    }
    private Set<DayOfWeek> parseDayOfWeeks(String dayOfWeekStr) {
        Set<DayOfWeek> days = new HashSet<>();
        String[] dayStrs = dayOfWeekStr.split("\\|");
        for (String dayStr : dayStrs) {
            switch (dayStr.trim()) {
                case "MON" -> days.add(DayOfWeek.MONDAY);
                case "TUE" -> days.add(DayOfWeek.TUESDAY);
                case "WED" -> days.add(DayOfWeek.WEDNESDAY);
                case "THU" -> days.add(DayOfWeek.THURSDAY);
                case "FRI" -> days.add(DayOfWeek.FRIDAY);
                case "SAT" -> days.add(DayOfWeek.SATURDAY);
                case "SUN" -> days.add(DayOfWeek.SUNDAY);
                default -> throw new IllegalArgumentException("无效的星期几: " + dayStr);
            }
        }
        return days;
    }
}
src/main/java/com/ruoyi/safe/service/impl/SafeFacilityInspectionTaskScheduler.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,170 @@
package com.ruoyi.safe.service.impl;
import com.ruoyi.safe.pojo.SafeFacilityInspectionTask;
import lombok.RequiredArgsConstructor;
import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.springframework.stereotype.Service;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Date;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class SafeFacilityInspectionTaskScheduler {
    private static final String JOB_GROUP = "safeFacilityInspectionTask";
    private static final String TRIGGER_GROUP = "safeFacilityInspectionTaskTrigger";
    private final Scheduler scheduler;
    public void scheduleTask(SafeFacilityInspectionTask task) throws SchedulerException {
        JobDetail jobDetail = buildJobDetail(task);
        if (scheduler.checkExists(jobDetail.getKey())) {
            scheduler.deleteJob(jobDetail.getKey());
        }
        scheduler.scheduleJob(jobDetail, buildJobTrigger(task, jobDetail));
    }
    public void rescheduleTask(SafeFacilityInspectionTask task) throws SchedulerException {
        TriggerKey triggerKey = getTriggerKey(task.getId());
        Trigger oldTrigger = scheduler.getTrigger(triggerKey);
        if (oldTrigger == null) {
            scheduleTask(task);
            return;
        }
        JobDetail jobDetail = scheduler.getJobDetail(oldTrigger.getJobKey());
        if (jobDetail == null) {
            scheduleTask(task);
            return;
        }
        scheduler.rescheduleJob(triggerKey, buildJobTrigger(task, jobDetail));
    }
    public void unscheduleTask(Long taskId) {
        try {
            scheduler.deleteJob(getJobKey(taskId));
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }
    private JobDetail buildJobDetail(SafeFacilityInspectionTask task) {
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("taskId", task.getId());
        jobDataMap.put("taskName", task.getInspectionName());
        jobDataMap.put("frequencyType", task.getFrequencyType());
        return JobBuilder.newJob(SafeFacilityInspectionTaskJob.class)
                .withIdentity(getJobKey(task.getId()))
                .withDescription(task.getInspectionName())
                .usingJobData(jobDataMap)
                .storeDurably(true)
                .requestRecovery(true)
                .build();
    }
    private Trigger buildJobTrigger(SafeFacilityInspectionTask task, JobDetail jobDetail) {
        return TriggerBuilder.newTrigger()
                .withIdentity(getTriggerKey(task.getId()))
                .withDescription(task.getInspectionName() + "_TRIGGER")
                .forJob(jobDetail)
                .withSchedule(CronScheduleBuilder
                        .cronSchedule(convertToCronExpression(task))
                        .withMisfireHandlingInstructionDoNothing())
                .startAt(task.getNextExecutionTime() != null
                        ? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant())
                        : new Date())
                .build();
    }
    private JobKey getJobKey(Long taskId) {
        return new JobKey("safeFacilityInspectionTask_" + taskId, JOB_GROUP);
    }
    private TriggerKey getTriggerKey(Long taskId) {
        return new TriggerKey("safeFacilityInspectionTaskTrigger_" + taskId, TRIGGER_GROUP);
    }
    private String convertToCronExpression(SafeFacilityInspectionTask task) {
        if (task == null || task.getFrequencyType() == null || task.getFrequencyDetail() == null) {
            throw new IllegalArgumentException("任务频率不能为空");
        }
        return switch (task.getFrequencyType().toUpperCase()) {
            case "DAILY" -> convertDailyToCron(task.getFrequencyDetail());
            case "WEEKLY" -> convertWeeklyToCron(task.getFrequencyDetail());
            case "MONTHLY" -> convertMonthlyToCron(task.getFrequencyDetail());
            default -> throw new IllegalArgumentException("不支持的频率类型: " + task.getFrequencyType());
        };
    }
    private String convertDailyToCron(String frequencyDetail) {
        LocalTime time = parseTime(frequencyDetail);
        return String.format("0 %d %d * * ?", time.getMinute(), time.getHour());
    }
    private String convertWeeklyToCron(String frequencyDetail) {
        String[] parts = validateAndSplit(frequencyDetail, 2);
        String daysOfWeek = Arrays.stream(parts[0].split("\\|"))
                .map(this::convertSingleDayName)
                .collect(Collectors.joining(","));
        LocalTime time = parseTime(parts[1]);
        return String.format("0 %d %d ? * %s", time.getMinute(), time.getHour(), daysOfWeek);
    }
    private String convertMonthlyToCron(String frequencyDetail) {
        String[] parts = validateAndSplit(frequencyDetail, 2);
        int day = validateDayOfMonth(parts[0]);
        LocalTime time = parseTime(parts[1]);
        return String.format("0 %d %d %d * ?", time.getMinute(), time.getHour(), day);
    }
    private String[] validateAndSplit(String input, int expectedParts) {
        String[] parts = input.split(",");
        if (parts.length != expectedParts) {
            throw new IllegalArgumentException("任务频率详情格式错误");
        }
        return Arrays.stream(parts).map(String::trim).toArray(String[]::new);
    }
    private LocalTime parseTime(String timeStr) {
        try {
            return LocalTime.parse(timeStr);
        } catch (DateTimeParseException e) {
            throw new IllegalArgumentException("时间格式必须为HH:mm", e);
        }
    }
    private int validateDayOfMonth(String dayStr) {
        int day = Integer.parseInt(dayStr);
        if (day < 1 || day > 31) {
            throw new IllegalArgumentException("日期必须在1-31之间");
        }
        return day;
    }
    private String convertSingleDayName(String dayName) {
        return switch (dayName.toUpperCase()) {
            case "MON" -> "MON";
            case "TUE" -> "TUE";
            case "WED" -> "WED";
            case "THU" -> "THU";
            case "FRI" -> "FRI";
            case "SAT" -> "SAT";
            case "SUN" -> "SUN";
            default -> throw new IllegalArgumentException("无效的星期几: " + dayName);
        };
    }
}
src/main/java/com/ruoyi/safe/service/impl/SafeFacilityInspectionTaskServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,301 @@
package com.ruoyi.safe.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.safe.mapper.SafeFacilityInspectionTaskMapper;
import com.ruoyi.safe.pojo.SafeFacilityInspectionTask;
import com.ruoyi.safe.pojo.SafeFacilityLedger;
import com.ruoyi.safe.service.SafeFacilityInspectionTaskService;
import com.ruoyi.safe.service.SafeFacilityLedgerService;
import lombok.RequiredArgsConstructor;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
 * <p>
 * å®‰å…¨ç”Ÿäº§--安全设施定时巡检任务 æœåŠ¡å®žçŽ°ç±»
 * </p>
 */
@Service
@RequiredArgsConstructor
public class SafeFacilityInspectionTaskServiceImpl extends ServiceImpl<SafeFacilityInspectionTaskMapper, SafeFacilityInspectionTask> implements SafeFacilityInspectionTaskService {
    private static final int ENABLED = 1;
    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
    private static final Map<String, DayOfWeek> WEEK_DAY_MAP = new HashMap<>();
    static {
        WEEK_DAY_MAP.put("MON", DayOfWeek.MONDAY);
        WEEK_DAY_MAP.put("TUE", DayOfWeek.TUESDAY);
        WEEK_DAY_MAP.put("WED", DayOfWeek.WEDNESDAY);
        WEEK_DAY_MAP.put("THU", DayOfWeek.THURSDAY);
        WEEK_DAY_MAP.put("FRI", DayOfWeek.FRIDAY);
        WEEK_DAY_MAP.put("SAT", DayOfWeek.SATURDAY);
        WEEK_DAY_MAP.put("SUN", DayOfWeek.SUNDAY);
    }
    private final SafeFacilityInspectionTaskMapper safeFacilityInspectionTaskMapper;
    private final SafeFacilityInspectionTaskScheduler safeFacilityInspectionTaskScheduler;
    private final SafeFacilityLedgerService safeFacilityLedgerService;
    private final SysUserMapper sysUserMapper;
    @Override
    public IPage<SafeFacilityInspectionTask> pageSafeFacilityInspectionTask(Page<SafeFacilityInspectionTask> page, SafeFacilityInspectionTask task) {
        LambdaQueryWrapper<SafeFacilityInspectionTask> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(StringUtils.isNotBlank(task.getInspectionName()), SafeFacilityInspectionTask::getInspectionName, task.getInspectionName())
                .like(StringUtils.isNotBlank(task.getFacilityName()), SafeFacilityInspectionTask::getFacilityName, task.getFacilityName())
                .like(StringUtils.isNotBlank(task.getFacilityCode()), SafeFacilityInspectionTask::getFacilityCode, task.getFacilityCode())
                .eq(task.getFacilityId() != null, SafeFacilityInspectionTask::getFacilityId, task.getFacilityId())
                .eq(task.getIsEnabled() != null, SafeFacilityInspectionTask::getIsEnabled, task.getIsEnabled())
                .orderByDesc(SafeFacilityInspectionTask::getId);
        IPage<SafeFacilityInspectionTask> result = safeFacilityInspectionTaskMapper.selectPage(page, queryWrapper);
        fillInspectorName(result.getRecords());
        return result;
    }
    @Override
    @Transactional
    public boolean saveOrUpdateTask(SafeFacilityInspectionTask task) throws SchedulerException {
        SafeFacilityInspectionTask oldTask = null;
        if (task.getId() != null) {
            oldTask = safeFacilityInspectionTaskMapper.selectById(task.getId());
            if (oldTask == null) {
                throw new IllegalArgumentException("定时巡检任务不存在");
            }
        }
        normalizeTask(task, oldTask);
        task.setNextExecutionTime(calculateFirstExecutionTime(task.getFrequencyType(), task.getFrequencyDetail()));
        boolean result;
        if (task.getId() == null) {
            result = safeFacilityInspectionTaskMapper.insert(task) > 0;
            if (result && isEnabled(task.getIsEnabled())) {
                safeFacilityInspectionTaskScheduler.scheduleTask(task);
            }
            return result;
        }
        result = safeFacilityInspectionTaskMapper.updateById(task) > 0;
        if (result) {
            boolean oldEnabled = oldTask != null && isEnabled(oldTask.getIsEnabled());
            boolean newEnabled = isEnabled(task.getIsEnabled());
            if (!newEnabled) {
                safeFacilityInspectionTaskScheduler.unscheduleTask(task.getId());
            } else if (oldEnabled) {
                safeFacilityInspectionTaskScheduler.rescheduleTask(task);
            } else {
                safeFacilityInspectionTaskScheduler.scheduleTask(task);
            }
        }
        return result;
    }
    @Override
    @Transactional
    public boolean removeTaskByIds(List<Long> ids) {
        boolean removed = removeBatchByIds(ids);
        if (removed) {
            for (Long id : ids) {
                safeFacilityInspectionTaskScheduler.unscheduleTask(id);
            }
        }
        return removed;
    }
    @Override
    public LocalDateTime calculateFirstExecutionTime(String frequencyType, String frequencyDetail) {
        LocalDateTime now = LocalDateTime.now();
        return calculateNextExecutionTime(frequencyType, frequencyDetail, now);
    }
    @Override
    public LocalDateTime calculateNextExecutionTime(String frequencyType, String frequencyDetail, LocalDateTime currentTime) {
        validateFrequency(frequencyType, frequencyDetail);
        return switch (frequencyType) {
            case "DAILY" -> calculateDailyNextTime(frequencyDetail, currentTime);
            case "WEEKLY" -> calculateWeeklyNextTime(frequencyDetail, currentTime);
            case "MONTHLY" -> calculateMonthlyNextTime(frequencyDetail, currentTime);
            default -> throw new IllegalArgumentException("不支持的频率类型: " + frequencyType);
        };
    }
    private void normalizeTask(SafeFacilityInspectionTask task, SafeFacilityInspectionTask oldTask) {
        if (StringUtils.isBlank(task.getInspectionName())) {
            throw new IllegalArgumentException("巡检任务名称不能为空");
        }
        if (task.getFacilityId() == null) {
            throw new IllegalArgumentException("请选择安全设施");
        }
        if (StringUtils.isBlank(task.getInspectionProject())) {
            throw new IllegalArgumentException("巡检项目不能为空");
        }
        if (task.getInspectorId() == null) {
            throw new IllegalArgumentException("巡检人不能为空");
        }
        SafeFacilityLedger ledger = safeFacilityLedgerService.getById(task.getFacilityId());
        if (ledger == null) {
            throw new IllegalArgumentException("选择的安全设施不存在");
        }
        if ("报废".equals(ledger.getStatus())) {
            throw new IllegalArgumentException("报废设施不能创建巡检任务");
        }
        task.setFacilityCode(ledger.getFacilityCode());
        task.setFacilityName(ledger.getFacilityName());
        task.setIsEnabled(resolveEnabledValue(task.getIsEnabled(), oldTask));
        task.setFrequencyType(task.getFrequencyType() == null ? null : task.getFrequencyType().trim().toUpperCase());
        task.setFrequencyDetail(task.getFrequencyDetail() == null ? null : task.getFrequencyDetail().trim());
        validateFrequency(task.getFrequencyType(), task.getFrequencyDetail());
    }
    private void validateFrequency(String frequencyType, String frequencyDetail) {
        if (StringUtils.isBlank(frequencyType) || StringUtils.isBlank(frequencyDetail)) {
            throw new IllegalArgumentException("任务频率不能为空");
        }
        switch (frequencyType) {
            case "DAILY" -> parseTime(frequencyDetail);
            case "WEEKLY" -> {
                String[] parts = splitFrequencyDetail(frequencyDetail);
                if (!WEEK_DAY_MAP.containsKey(parts[0])) {
                    throw new IllegalArgumentException("无效的星期: " + parts[0]);
                }
                parseTime(parts[1]);
            }
            case "MONTHLY" -> {
                String[] parts = splitFrequencyDetail(frequencyDetail);
                validateDayOfMonth(parts[0]);
                parseTime(parts[1]);
            }
            default -> throw new IllegalArgumentException("不支持的频率类型: " + frequencyType);
        }
    }
    private LocalDateTime calculateDailyNextTime(String timeStr, LocalDateTime currentTime) {
        LocalTime executionTime = parseTime(timeStr);
        LocalDateTime candidate = LocalDateTime.of(currentTime.toLocalDate(), executionTime);
        return currentTime.isBefore(candidate) ? candidate : candidate.plusDays(1);
    }
    private LocalDateTime calculateWeeklyNextTime(String detail, LocalDateTime currentTime) {
        String[] parts = splitFrequencyDetail(detail);
        DayOfWeek targetDay = WEEK_DAY_MAP.get(parts[0]);
        LocalTime targetTime = parseTime(parts[1]);
        int daysUntil = targetDay.getValue() - currentTime.getDayOfWeek().getValue();
        if (daysUntil < 0 || (daysUntil == 0 && !currentTime.toLocalTime().isBefore(targetTime))) {
            daysUntil += 7;
        }
        return LocalDateTime.of(currentTime.toLocalDate().plusDays(daysUntil), targetTime);
    }
    private LocalDateTime calculateMonthlyNextTime(String detail, LocalDateTime currentTime) {
        String[] parts = splitFrequencyDetail(detail);
        int dayOfMonth = validateDayOfMonth(parts[0]);
        LocalTime targetTime = parseTime(parts[1]);
        YearMonth currentMonth = YearMonth.from(currentTime);
        for (int i = 0; i < 24; i++) {
            YearMonth targetMonth = currentMonth.plusMonths(i);
            if (dayOfMonth > targetMonth.lengthOfMonth()) {
                continue;
            }
            LocalDateTime candidate = LocalDateTime.of(
                    targetMonth.getYear(),
                    targetMonth.getMonthValue(),
                    dayOfMonth,
                    targetTime.getHour(),
                    targetTime.getMinute()
            );
            if (currentTime.isBefore(candidate)) {
                return candidate;
            }
        }
        throw new IllegalArgumentException("无法计算下次执行时间");
    }
    private String[] splitFrequencyDetail(String detail) {
        String[] parts = detail.split(",");
        if (parts.length != 2) {
            throw new IllegalArgumentException("频率详情格式错误");
        }
        return Arrays.stream(parts).map(String::trim).toArray(String[]::new);
    }
    private LocalTime parseTime(String timeStr) {
        try {
            return LocalTime.parse(timeStr, TIME_FORMATTER);
        } catch (DateTimeParseException e) {
            throw new IllegalArgumentException("时间格式必须为HH:mm", e);
        }
    }
    private int validateDayOfMonth(String dayStr) {
        try {
            int day = Integer.parseInt(dayStr);
            if (day < 1 || day > 31) {
                throw new IllegalArgumentException("日期必须在1-31之间");
            }
            return day;
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("日期必须在1-31之间", e);
        }
    }
    private Integer resolveEnabledValue(Integer requestEnabled, SafeFacilityInspectionTask oldTask) {
        if (requestEnabled != null) {
            return requestEnabled;
        }
        if (oldTask != null && oldTask.getIsEnabled() != null) {
            return oldTask.getIsEnabled();
        }
        return ENABLED;
    }
    private boolean isEnabled(Integer enabledValue) {
        return Objects.equals(enabledValue, ENABLED);
    }
    private void fillInspectorName(List<SafeFacilityInspectionTask> records) {
        if (records == null || records.isEmpty()) {
            return;
        }
        List<Long> userIds = records.stream()
                .map(SafeFacilityInspectionTask::getInspectorId)
                .filter(Objects::nonNull)
                .map(Integer::longValue)
                .distinct()
                .collect(Collectors.toList());
        if (userIds.isEmpty()) {
            return;
        }
        Map<Long, String> userNameMap = sysUserMapper.selectUserByIds(userIds).stream()
                .collect(Collectors.toMap(SysUser::getUserId, SysUser::getNickName, (v1, v2) -> v1));
        for (SafeFacilityInspectionTask record : records) {
            if (record.getInspectorId() != null) {
                record.setInspectorName(userNameMap.getOrDefault(record.getInspectorId().longValue(), "未知用户"));
            }
        }
    }
}
src/main/resources/mapper/safe/SafeFacilityInspectionMapper.xml
@@ -4,9 +4,11 @@
    <resultMap type="com.ruoyi.safe.pojo.SafeFacilityInspection" id="SafeFacilityInspectionResult">
        <id property="id" column="id"/>
        <result property="scheduleTaskId" column="schedule_task_id"/>
        <result property="inspectionCode" column="inspection_code"/>
        <result property="facilityId" column="facility_id"/>
        <result property="inspectionType" column="inspection_type"/>
        <result property="inspectionProject" column="inspection_project"/>
        <result property="planTime" column="plan_time"/>
        <result property="inspectorId" column="inspector_id"/>
        <result property="actualTime" column="actual_time"/>