2 天以前 4f55d3cb4bc644e4534106336f2047af1a4db5df
feat(config): 添加新环境配置并扩展设备台账功能

- 添加 application-dlsmls-pro.yml 新环境配置文件
- 修改 application-dev.yml 中数据库连接URL
- 在 DeviceLedger 实体类中增加 areaId、isIotDevice 和 externalCode 字段
- 在 DeviceLedgerDto 中添加物联网设备标识和外部编码属性
- 更新 DeviceLedgerMapper.xml 查询逻辑,关联设备区域表并添加过滤条件
- 在 DeviceMaintenance 相关实体类中增加区域ID字段支持
- 修改 DeviceMaintenanceMapper.xml 查询语句,关联设备区域信息
- 扩展 DeviceMaintenanceServiceImpl 中的保存和更新方法
- 在 InspectionTask 实体类中添加巡检状态和区域ID字段
- 增加 InspectionTask 的分页查询和记录查询功能
- 添加 JclyController 控制器类用于HTTP服务接口
已添加10个文件
已修改27个文件
1921 ■■■■■ 文件已修改
src/main/java/com/ruoyi/device/controller/DeviceAreaController.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/dto/DeviceAreaTreeDto.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/dto/DeviceMaintenanceDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/mapper/DeviceAreaMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/DeviceArea.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/DeviceLedger.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/DeviceMaintenance.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/DeviceRepair.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/MaintenanceTask.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/IDeviceAreaService.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceAreaServiceImpl.java 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/vo/DeviceMaintenanceVo.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/vo/DeviceRepairVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/controller/JclyController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/RealTimeEnergyConsumptionServiceImpl.java 441 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/TimingTaskController.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/dto/InspectionTaskDto.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/dto/TimingTaskDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/mapper/InspectionTaskMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/pojo/TimingTask.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/InspectionTaskService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/TimingTaskService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java 185 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduleUtils.java 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dlsmls-pro.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceLedgerMapper.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceMaintenanceMapper.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceRepairMapper.xml 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/inspectiontask/InspectionTaskMapper.xml 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceAreaController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package com.ruoyi.device.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.device.pojo.DeviceArea;
import com.ruoyi.device.service.IDeviceAreaService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "设备区域管理")
@RestController
@RequestMapping("/device/area")
public class DeviceAreaController {
    @Autowired
    private IDeviceAreaService deviceAreaService;
    @GetMapping("/tree")
    @ApiOperation("设备区域树")
    public AjaxResult tree() {
        return AjaxResult.success(deviceAreaService.listTree());
    }
    @GetMapping("/treeWithDevices")
    @ApiOperation("设备区域树-包含设备列表")
    public AjaxResult treeWithDevices() {
        return AjaxResult.success(deviceAreaService.listTreeWithDevices());
    }
    @GetMapping("/page")
    @ApiOperation("设备区域分页")
    public AjaxResult page(Page page, DeviceArea deviceArea) {
        return AjaxResult.success(deviceAreaService.queryPage(page, deviceArea));
    }
    @GetMapping("/{id}")
    @ApiOperation("设备区域详情")
    public AjaxResult detail(@PathVariable Long id) {
        return AjaxResult.success(deviceAreaService.getById(id));
    }
    @PostMapping
    @ApiOperation("新增设备区域")
    @Log(title = "设备区域", businessType = BusinessType.INSERT)
    public AjaxResult add(@RequestBody DeviceArea deviceArea) {
        return deviceAreaService.saveDeviceArea(deviceArea);
    }
    @PutMapping
    @ApiOperation("修改设备区域")
    @Log(title = "设备区域", businessType = BusinessType.UPDATE)
    public AjaxResult update(@RequestBody DeviceArea deviceArea) {
        return deviceAreaService.updateDeviceArea(deviceArea);
    }
    @DeleteMapping
    @ApiOperation("删除设备区域")
    @Log(title = "设备区域", businessType = BusinessType.DELETE)
    public AjaxResult delete(@RequestBody List<Long> ids) {
        return deviceAreaService.removeDeviceAreas(ids);
    }
}
src/main/java/com/ruoyi/device/dto/DeviceAreaTreeDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.ruoyi.device.dto;
import com.ruoyi.device.pojo.DeviceLedger;
import lombok.Data;
import java.util.List;
@Data
public class DeviceAreaTreeDto {
    private Long id;
    private Long parentId;
    private String areaName;
    private String label;
    private Long sort;
    private String remark;
    private List<DeviceLedger> deviceList;
    private List<DeviceAreaTreeDto> children;
}
src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java
@@ -5,6 +5,7 @@
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.dto.DateQueryDto;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -162,4 +163,17 @@
    @Schema(description = "设备类型")
    private String type;
    @ApiModelProperty("是否为物联设备 0-否 1-是")
    private Integer isIotDevice;
    @ApiModelProperty("外部编码")
    private String externalCode;
    @Schema(description = "设备区域ID")
    private Long areaId;
    @Schema(description = "设备区域名称")
    @TableField(exist = false)
    private String areaName;
}
src/main/java/com/ruoyi/device/dto/DeviceMaintenanceDto.java
@@ -13,6 +13,9 @@
@Data
public class DeviceMaintenanceDto extends DeviceMaintenance {
    @Schema(description = "设备区域名称")
    private String areaName;
    @Schema(description = "设备保养id")
    private Long id;
src/main/java/com/ruoyi/device/mapper/DeviceAreaMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.device.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.device.pojo.DeviceArea;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface DeviceAreaMapper extends BaseMapper<DeviceArea> {
    IPage<DeviceArea> queryPage(Page page, @Param("deviceArea") DeviceArea deviceArea);
}
src/main/java/com/ruoyi/device/pojo/DeviceArea.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package com.ruoyi.device.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("device_area")
@ApiModel("设备区域")
public class DeviceArea {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("区域名称")
    private String areaName;
    @ApiModelProperty("父级ID")
    private Long parentId;
    @ApiModelProperty("排序")
    private Long sort;
    @ApiModelProperty("备注")
    private String remark;
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty("更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @ApiModelProperty("创建用户")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty("更新用户")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @ApiModelProperty("租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @TableField(exist = false)
    @ApiModelProperty("父级名称")
    private String parentName;
}
src/main/java/com/ruoyi/device/pojo/DeviceLedger.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -54,6 +55,8 @@
     * ä¾›åº”商名称
     */
    private String supplierName;
    private Long areaId;
    /**
     * å•位
@@ -161,4 +164,10 @@
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @ApiModelProperty("是否为物联设备 0-否 1-是")
    private Integer isIotDevice;
    @ApiModelProperty("外部编码")
    private String externalCode;
}
src/main/java/com/ruoyi/device/pojo/DeviceMaintenance.java
@@ -22,6 +22,9 @@
    @Schema(description = "设备台账id")
    private Long deviceLedgerId;
    @Schema(description = "设备区域ID")
    private Long areaId;
    @Schema(description = "保养任务id")
    private Long maintenanceTaskId;
src/main/java/com/ruoyi/device/pojo/DeviceRepair.java
@@ -22,6 +22,9 @@
    @Schema(description = "设备台账id")
    private Long deviceLedgerId;
    @Schema(description = "设备区域ID")
    private Long areaId;
    private String deviceName;
    private String deviceModel;
src/main/java/com/ruoyi/device/pojo/MaintenanceTask.java
@@ -37,6 +37,9 @@
    @TableId(type = IdType.AUTO)
    private Long id;
    @Schema(description = "设备区域ID")
    private Long areaId;
    @Schema(description = "设备名称")
    @Excel(name = "保养任务名称")
    private String taskName;
@@ -116,4 +119,7 @@
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @TableField(exist = false)
    private String areaName;
}
src/main/java/com/ruoyi/device/service/IDeviceAreaService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.device.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.device.dto.DeviceAreaTreeDto;
import com.ruoyi.device.pojo.DeviceArea;
import com.ruoyi.framework.web.domain.AjaxResult;
import java.util.List;
public interface IDeviceAreaService extends IService<DeviceArea> {
    IPage<DeviceArea> queryPage(Page page, DeviceArea deviceArea);
    List<DeviceAreaTreeDto> listTree();
    List<DeviceAreaTreeDto> listTreeWithDevices();
    AjaxResult saveDeviceArea(DeviceArea deviceArea);
    AjaxResult updateDeviceArea(DeviceArea deviceArea);
    AjaxResult removeDeviceAreas(List<Long> ids);
}
src/main/java/com/ruoyi/device/service/impl/DeviceAreaServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,190 @@
package com.ruoyi.device.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.common.utils.bean.BeanUtils;
import com.ruoyi.device.dto.DeviceAreaTreeDto;
import com.ruoyi.device.mapper.DeviceAreaMapper;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.pojo.DeviceArea;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.service.IDeviceAreaService;
import com.ruoyi.framework.web.domain.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.*;
@Service
public class DeviceAreaServiceImpl extends ServiceImpl<DeviceAreaMapper, DeviceArea> implements IDeviceAreaService {
    @Autowired
    private DeviceAreaMapper deviceAreaMapper;
    @Autowired
    private DeviceLedgerMapper deviceLedgerMapper;
    @Override
    public IPage<DeviceArea> queryPage(Page page, DeviceArea deviceArea) {
        return deviceAreaMapper.queryPage(page, deviceArea);
    }
    @Override
    public List<DeviceAreaTreeDto> listTree() {
        return buildTree(Collections.emptyMap());
    }
    @Override
    public List<DeviceAreaTreeDto> listTreeWithDevices() {
        LambdaQueryWrapper<DeviceLedger> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.isNotNull(DeviceLedger::getAreaId);
        queryWrapper.orderByAsc(DeviceLedger::getId);
        List<DeviceLedger> deviceLedgers = deviceLedgerMapper.selectList(queryWrapper);
        Map<Long, List<DeviceLedger>> deviceMap = new HashMap<>();
        for (DeviceLedger deviceLedger : deviceLedgers) {
            deviceMap.computeIfAbsent(deviceLedger.getAreaId(), key -> new ArrayList<>()).add(deviceLedger);
        }
        return buildTree(deviceMap);
    }
    @Override
    public AjaxResult saveDeviceArea(DeviceArea deviceArea) {
        if (deviceArea == null || !StringUtils.hasText(deviceArea.getAreaName())) {
            return AjaxResult.error("区域名称不能为空");
        }
        normalizeDeviceArea(deviceArea);
        if (existsSameName(deviceArea.getParentId(), deviceArea.getAreaName(), null)) {
            return AjaxResult.error("同级区域名称已存在");
        }
        if (deviceArea.getParentId() != null && deviceAreaMapper.selectById(deviceArea.getParentId()) == null) {
            return AjaxResult.error("父级区域不存在");
        }
        return this.save(deviceArea) ? AjaxResult.success() : AjaxResult.error();
    }
    @Override
    public AjaxResult updateDeviceArea(DeviceArea deviceArea) {
        if (deviceArea == null || deviceArea.getId() == null) {
            return AjaxResult.error("区域ID不能为空");
        }
        if (!StringUtils.hasText(deviceArea.getAreaName())) {
            return AjaxResult.error("区域名称不能为空");
        }
        normalizeDeviceArea(deviceArea);
        DeviceArea current = deviceAreaMapper.selectById(deviceArea.getId());
        if (current == null) {
            return AjaxResult.error("区域不存在");
        }
        if (deviceArea.getParentId() != null && deviceArea.getId().equals(deviceArea.getParentId())) {
            return AjaxResult.error("父级区域不能选择自身");
        }
        if (isDescendant(deviceArea.getId(), deviceArea.getParentId())) {
            return AjaxResult.error("父级区域不能选择子节点");
        }
        if (deviceArea.getParentId() != null && deviceAreaMapper.selectById(deviceArea.getParentId()) == null) {
            return AjaxResult.error("父级区域不存在");
        }
        if (existsSameName(deviceArea.getParentId(), deviceArea.getAreaName(), deviceArea.getId())) {
            return AjaxResult.error("同级区域名称已存在");
        }
        return this.updateById(deviceArea) ? AjaxResult.success() : AjaxResult.error();
    }
    @Override
    public AjaxResult removeDeviceAreas(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return AjaxResult.error("请选择要删除的区域");
        }
        LambdaQueryWrapper<DeviceArea> childWrapper = new LambdaQueryWrapper<>();
        childWrapper.in(DeviceArea::getParentId, ids);
        if (deviceAreaMapper.selectCount(childWrapper) > 0) {
            return AjaxResult.error("存在子区域,不能直接删除");
        }
        return this.removeBatchByIds(ids) ? AjaxResult.success() : AjaxResult.error();
    }
    private List<DeviceAreaTreeDto> buildTree(Map<Long, List<DeviceLedger>> deviceMap) {
        LambdaQueryWrapper<DeviceArea> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.and(wrapper -> wrapper.isNull(DeviceArea::getParentId).or().eq(DeviceArea::getParentId, 0L));
        queryWrapper.orderByAsc(DeviceArea::getSort).orderByAsc(DeviceArea::getId);
        List<DeviceArea> rootList = deviceAreaMapper.selectList(queryWrapper);
        List<DeviceAreaTreeDto> tree = new ArrayList<>();
        for (DeviceArea deviceArea : rootList) {
            DeviceAreaTreeDto node = toTreeDto(deviceArea, deviceMap);
            node.setChildren(buildChildren(deviceArea.getId(), deviceMap));
            tree.add(node);
        }
        return tree;
    }
    private void normalizeDeviceArea(DeviceArea deviceArea) {
        if (deviceArea.getParentId() != null && deviceArea.getParentId() == 0L) {
            deviceArea.setParentId(null);
        }
        if (deviceArea.getSort() == null) {
            deviceArea.setSort(0L);
        }
    }
    private boolean existsSameName(Long parentId, String areaName, Long excludeId) {
        LambdaQueryWrapper<DeviceArea> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DeviceArea::getAreaName, areaName);
        if (parentId == null) {
            queryWrapper.and(wrapper -> wrapper.isNull(DeviceArea::getParentId).or().eq(DeviceArea::getParentId, 0L));
        } else {
            queryWrapper.eq(DeviceArea::getParentId, parentId);
        }
        if (excludeId != null) {
            queryWrapper.ne(DeviceArea::getId, excludeId);
        }
        return deviceAreaMapper.selectCount(queryWrapper) > 0;
    }
    private boolean isDescendant(Long currentId, Long parentId) {
        if (currentId == null || parentId == null) {
            return false;
        }
        Long cursor = parentId;
        while (cursor != null && cursor != 0L) {
            if (currentId.equals(cursor)) {
                return true;
            }
            DeviceArea parent = deviceAreaMapper.selectById(cursor);
            if (parent == null) {
                return false;
            }
            cursor = parent.getParentId();
        }
        return false;
    }
    private List<DeviceAreaTreeDto> buildChildren(Long parentId, Map<Long, List<DeviceLedger>> deviceMap) {
        LambdaQueryWrapper<DeviceArea> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DeviceArea::getParentId, parentId);
        queryWrapper.orderByAsc(DeviceArea::getSort).orderByAsc(DeviceArea::getId);
        List<DeviceArea> children = deviceAreaMapper.selectList(queryWrapper);
        List<DeviceAreaTreeDto> result = new ArrayList<>();
        for (DeviceArea child : children) {
            DeviceAreaTreeDto node = toTreeDto(child, deviceMap);
            node.setChildren(buildChildren(child.getId(), deviceMap));
            result.add(node);
        }
        return result;
    }
    private DeviceAreaTreeDto toTreeDto(DeviceArea deviceArea, Map<Long, List<DeviceLedger>> deviceMap) {
        DeviceAreaTreeDto dto = new DeviceAreaTreeDto();
        BeanUtils.copyProperties(deviceArea, dto);
        dto.setLabel(deviceArea.getAreaName());
        dto.setDeviceList(new ArrayList<>(deviceMap.getOrDefault(deviceArea.getId(), Collections.emptyList())));
        dto.setChildren(new ArrayList<>());
        return dto;
    }
}
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java
@@ -11,7 +11,9 @@
import com.ruoyi.device.dto.DeviceMaintenanceDto;
import com.ruoyi.device.execl.DeviceMaintenanceExeclDto;
import com.ruoyi.device.mapper.DeviceMaintenanceMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.DeviceMaintenance;
import com.ruoyi.device.service.IDeviceLedgerService;
import com.ruoyi.device.service.IDeviceMaintenanceService;
import com.ruoyi.device.vo.DeviceMaintenanceVo;
import com.ruoyi.device.vo.DeviceRepairVo;
@@ -36,6 +38,7 @@
public class DeviceMaintenanceServiceImpl extends ServiceImpl<DeviceMaintenanceMapper, DeviceMaintenance> implements IDeviceMaintenanceService {
    private final DeviceMaintenanceMapper deviceMaintenanceMapper;
    private final IDeviceLedgerService deviceLedgerService;
    private final SparePartsMapper sparePartsMapper;
    private final SparePartsRequisitionRecordService sparePartsRequisitionRecordService;
    private final FileUtil fileUtil;
@@ -49,6 +52,12 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult saveDeviceRepair(DeviceMaintenanceDto deviceMaintenance) {
        DeviceLedger byId = deviceLedgerService.getById(deviceMaintenance.getDeviceLedgerId());
        if (byId != null) {
            deviceMaintenance.setDeviceName(byId.getDeviceName());
            deviceMaintenance.setDeviceModel(byId.getDeviceModel());
            deviceMaintenance.setAreaId(byId.getAreaId());
        }
        boolean save = this.save(deviceMaintenance);
        if (save){
            // å¤„理图片上传
@@ -62,6 +71,15 @@
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult updateDeviceDeviceMaintenance(DeviceMaintenanceDto deviceMaintenance) {
        DeviceMaintenance oldDeviceMaintenance = this.getById(deviceMaintenance.getId());
        Long effectiveDeviceLedgerId = deviceMaintenance.getDeviceLedgerId() != null
                ? deviceMaintenance.getDeviceLedgerId()
                : oldDeviceMaintenance.getDeviceLedgerId();
        DeviceLedger byId = deviceLedgerService.getById(effectiveDeviceLedgerId);
        if (byId != null) {
            deviceMaintenance.setDeviceName(byId.getDeviceName());
            deviceMaintenance.setDeviceModel(byId.getDeviceModel());
            deviceMaintenance.setAreaId(byId.getAreaId());
        }
        // å¤„理备件使用情况
        if (com.github.xiaoymin.knife4j.core.util.CollectionUtils.isNotEmpty(deviceMaintenance.getSparePartsUseList())) {
            List<Long> sparePartIds = new ArrayList<>();
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java
@@ -67,6 +67,7 @@
        DeviceLedger byId = deviceLedgerService.getById(deviceRepairDto.getDeviceLedgerId());
        deviceRepairDto.setDeviceName(byId.getDeviceName());
        deviceRepairDto.setDeviceModel(byId.getDeviceModel());
        deviceRepairDto.setAreaId(byId.getAreaId());
        if (deviceRepairDto.getStatus() == null) {
            deviceRepairDto.setStatus(STATUS_PENDING_REPAIR);
        }
@@ -86,6 +87,15 @@
        if (oldDeviceRepair == null) {
            return AjaxResult.error("报修记录不存在");
        }
        Long effectiveDeviceLedgerId = deviceRepairDto.getDeviceLedgerId() != null
                ? deviceRepairDto.getDeviceLedgerId()
                : oldDeviceRepair.getDeviceLedgerId();
        DeviceLedger byId = deviceLedgerService.getById(effectiveDeviceLedgerId);
        if (byId != null) {
            deviceRepairDto.setDeviceName(byId.getDeviceName());
            deviceRepairDto.setDeviceModel(byId.getDeviceModel());
            deviceRepairDto.setAreaId(byId.getAreaId());
        }
        if (deviceRepairDto.getStatus() != null
                && deviceRepairDto.getStatus() == STATUS_COMPLETED
                && (oldDeviceRepair.getStatus() == null
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java
@@ -4,7 +4,11 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.device.mapper.DeviceAreaMapper;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.mapper.MaintenanceTaskMapper;
import com.ruoyi.device.pojo.DeviceArea;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.MaintenanceTask;
import com.ruoyi.device.service.MaintenanceTaskService;
import com.ruoyi.framework.web.domain.AjaxResult;
@@ -32,10 +36,16 @@
    private final SysUserMapper sysUserMapper;
    private final TimingTaskServiceImpl timingTaskService;
    private final MaintenanceTaskScheduler maintenanceTaskScheduler;
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final DeviceAreaMapper deviceAreaMapper;
    @Override
    public AjaxResult listPage(Page page, MaintenanceTask maintenanceTask) {
        Page<MaintenanceTask> taskPage = maintenanceTaskMapper.selectPage(page, new QueryWrapper<MaintenanceTask>().orderByDesc("create_time"));
        QueryWrapper<MaintenanceTask> queryWrapper = new QueryWrapper<MaintenanceTask>().orderByDesc("create_time");
        if (maintenanceTask.getAreaId() != null) {
            queryWrapper.eq("area_id", maintenanceTask.getAreaId());
        }
        Page<MaintenanceTask> taskPage = maintenanceTaskMapper.selectPage(page, queryWrapper);
        // 2. å¦‚果没有数据,直接返回空分页
        if (taskPage.getRecords().isEmpty()) {
            return AjaxResult.success(taskPage);
@@ -57,10 +67,33 @@
            List<SysUser> users = sysUserMapper.selectUserByIds((new ArrayList<>(userIds)));
            users.forEach(user -> userNickNameMap.put(user.getUserId(), user.getNickName()));
        }
        Map<Long, DeviceLedger> ledgerMap = new HashMap<>();
        Set<Long> areaIds = new HashSet<>();
        taskPage.getRecords().forEach(task -> {
            // è®¾ç½®ç™»è®°äººæ˜µç§°
            if (task.getTaskId() != null) {
                DeviceLedger deviceLedger = deviceLedgerMapper.selectById(task.getTaskId());
                if (deviceLedger != null) {
                    ledgerMap.put(task.getTaskId(), deviceLedger);
                    if (deviceLedger.getAreaId() != null) {
                        areaIds.add(deviceLedger.getAreaId());
                    }
                }
            }
        });
        Map<Long, String> areaNameMap = new HashMap<>();
        if (!areaIds.isEmpty()) {
            List<DeviceArea> areas = deviceAreaMapper.selectBatchIds(new ArrayList<>(areaIds));
            areas.forEach(area -> areaNameMap.put(area.getId(), area.getAreaName()));
        }
        taskPage.getRecords().forEach(task -> {
            // ç’å‰§ç–†é§æ˜î†‡æµœçƒ˜æ¨€ç»‰?
            if (task.getRegistrantId() != null) {
                task.setRegistrant(userNickNameMap.getOrDefault(task.getRegistrantId(), "未知用户"));
                task.setRegistrant(userNickNameMap.getOrDefault(task.getRegistrantId(), "鏈煡鐢ㄦ埛"));
            }
            DeviceLedger deviceLedger = ledgerMap.get(task.getTaskId());
            if (deviceLedger != null) {
                task.setAreaId(deviceLedger.getAreaId());
                task.setAreaName(areaNameMap.getOrDefault(deviceLedger.getAreaId(), ""));
            }
        });
        return AjaxResult.success(taskPage);
@@ -68,6 +101,10 @@
    @Override
    public AjaxResult add(MaintenanceTask maintenanceTask) {
        DeviceLedger deviceLedger = deviceLedgerMapper.selectById(maintenanceTask.getTaskId());
        if (deviceLedger != null) {
            maintenanceTask.setAreaId(deviceLedger.getAreaId());
        }
        maintenanceTask.setActive(true);
        // è®¡ç®—首次执行时间
        TimingTask task = new TimingTask();
@@ -85,6 +122,10 @@
    @Override
    public AjaxResult updateByMaintenanceTaskId(MaintenanceTask maintenanceTask) {
        MaintenanceTask maintenanceTask1 = maintenanceTaskMapper.selectById(maintenanceTask.getId());
        DeviceLedger deviceLedger = deviceLedgerMapper.selectById(maintenanceTask.getTaskId());
        if (deviceLedger != null) {
            maintenanceTask.setAreaId(deviceLedger.getAreaId());
        }
        if (maintenanceTask1 == null) {
            return AjaxResult.warn("没有此数据");
        }
src/main/java/com/ruoyi/device/vo/DeviceMaintenanceVo.java
@@ -10,6 +10,8 @@
@Data
public class DeviceMaintenanceVo extends DeviceMaintenance {
    @Schema(description = "设备区域名称")
    private String areaName;
    @Schema(description = "设备保养id")
    private Long id;
src/main/java/com/ruoyi/device/vo/DeviceRepairVo.java
@@ -10,6 +10,9 @@
@Data
public class DeviceRepairVo extends DeviceRepair {
    @Schema(description = "设备区域名称")
    private String areaName;
    @Schema(description = "报修时间字符串")
    private String repairTimeStr;
src/main/java/com/ruoyi/http/service/controller/JclyController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
package com.ruoyi.http.service.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.service.IDeviceLedgerService;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.http.service.impl.RealTimeEnergyConsumptionServiceImpl;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/iot")
@Api(tags = "数采接口")
public class JclyController extends BaseController {
    @Autowired
    private RealTimeEnergyConsumptionServiceImpl realTimeEnergyConsumptionService;
    @Autowired
    private IDeviceLedgerService deviceLedgerService;
    /**
     * å®žæ—¶èŽ·å–æ¸©æ¹¿åº¦ï¼ŒäºŒæ°§åŒ–ç¢³æ•°æ®
     */
    @GetMapping("/getRealData")
    public AjaxResult getRealData() {
        List<DeviceLedger> iotDevices = deviceLedgerService.list(new LambdaQueryWrapper<DeviceLedger>()
                .eq(DeviceLedger::getIsIotDevice, 1)
                .isNotNull(DeviceLedger::getExternalCode)
                .ne(DeviceLedger::getExternalCode, ""));
        Map<String, String> guidDeviceNameMap = iotDevices.stream()
                .filter(item -> StringUtils.isNotEmpty(item.getExternalCode()))
                .collect(Collectors.toMap(
                        item -> item.getExternalCode().trim(),
                        item -> StringUtils.isNotEmpty(item.getDeviceName()) ? item.getDeviceName().trim() : "",
                        (oldValue, newValue) -> StringUtils.isNotEmpty(oldValue) ? oldValue : newValue,
                        LinkedHashMap::new
                ));
        List<String> guidList = new ArrayList<>(guidDeviceNameMap.keySet());
        List<Map<String, String>> maps = realTimeEnergyConsumptionService
                .getRealData(guidList);
        for (Map<String, String> item : maps) {
            String guid = item.get("guid");
            if (StringUtils.isNotEmpty(guid)) {
                String deviceName = guidDeviceNameMap.get(guid.trim());
                if (StringUtils.isNotEmpty(deviceName)) {
                    item.put("deviceName", deviceName);
                }
            }
        }
        return AjaxResult.success(maps);
    }
    /**
     * èŽ·å–åŽ†å²æ•°æ®
     */
    @GetMapping("/getHistoryData")
    public AjaxResult getHistoryData(@RequestParam(value = "guid") String guid,
                                     @RequestParam(value = "startTime") long startTime,
                                     @RequestParam(value = "endTime") long endTime) {
        List<Map<String, String>> maps = realTimeEnergyConsumptionService.getHistoryData(guid, startTime, endTime);
        return AjaxResult.success(maps);
    }
}
src/main/java/com/ruoyi/http/service/impl/RealTimeEnergyConsumptionServiceImpl.java
@@ -3,15 +3,16 @@
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.http.service.RealTimeEnergyConsumptionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
 * @author :yys
@@ -21,110 +22,438 @@
@Slf4j
public class RealTimeEnergyConsumptionServiceImpl implements RealTimeEnergyConsumptionService {
    private static final long REMOTE_CACHE_TTL_SECONDS_30 = 30L;
    private static final String URL = "https://new.e-elitech.cn/api/data-api";
    private static final String TOKEN_URL = "/elitechAccess/getToken";
    private static final String REAL_TIME_URL = "/elitechAccess/v2/getRealTimeData"; //获取设备实时数据
    private static final String REAL_TIME_URL = "/elitechAccess/v2/getRealTimeData";
    private static final String REAL_HISTORY_URL = URL + "/elitechAccess/v2/getHistoryData";
    private static final String KET_ID = "75804708";
    private static final String KEY_SECRET = "xTUGToozKpYgUPqTsZzB";
    private static final String USER_NAME = "用户30773662";
    private static final String USER_NAME = "\u7528\u623730773662";
    private static final String PASS_WORD = "y17775163675";
    private static final String DEVICE_GUID = "90444196515214284663";
    private static final String REAL_TIME_CACHE_PREFIX = "JCLY:REAL_TIME:";
    private static final String HISTORY_CACHE_PREFIX = "JCLY:HISTORY:";
    /**
     * æ ¹æ®paramCode提取探头参数
     * @param paramList è®¾å¤‡å‚数数组
     * @param targetCode ç›®æ ‡æŽ¢å¤´ç¼–码
     * @return æŽ¢å¤´å‚数对象(包含name/value/unit)
     */
    private static final String TOKEN_CACHE_KEY = "JCLY_TOKEN:";
    private static final String STATUS_KEY = "status";
    private static final String STATUS_MESSAGE_KEY = "statusMessage";
    private static final String STATUS_ONLINE = "在线";
    private static final String STATUS_OFFLINE = "offline";
    private static final String STATUS_ERROR = "error";
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    private static JSONObject getProbeParam(JSONArray paramList, String targetCode) {
        if (paramList == null) {
            return new JSONObject();
        }
        for (int i = 0; i < paramList.size(); i++) {
            JSONObject paramObj = paramList.getJSONObject(i);
            if (targetCode.equals(paramObj.getString("paramCode"))) {
                return paramObj;
            }
        }
        return new JSONObject(); // æœªåŒ¹é…åˆ°è¿”回空对象,避免空指针
        return new JSONObject();
    }
    /**
     * å®žæ—¶èŽ·å–æ¸©æ¹¿åº¦ï¼ŒäºŒæ°§åŒ–ç¢³æ•°æ®
     */
    public static void main(String[] args) {
        String realTimeData = getRealTimeData(getToken());
        Map<String, Object> map = JSON.parseObject(realTimeData, Map.class);
        if(map.get("code").equals(0)){
            // 1. è§£æžå¤–层data为JSON数组(接口返回的设备列表)
            JSONArray deviceList = JSON.parseArray(map.get("data").toString());
            // 2. éåŽ†è®¾å¤‡åˆ—è¡¨ï¼ˆæ­¤å¤„ä»…å–ç¬¬ä¸€ä¸ªè®¾å¤‡ï¼Œè‹¥æœ‰å¤šä¸ªè®¾å¤‡å¯å¾ªçŽ¯å¤„ç†ï¼‰
            if (!deviceList.isEmpty()) {
                JSONObject deviceObj = deviceList.getJSONObject(0);
                // 3. è§£æžè®¾å¤‡å†…的参数数组(所有paramCode对应的参数)
                JSONArray paramList = deviceObj.getJSONArray("data");
    public List<Map<String, String>> getHistoryData(String guid, long startTime, long endTime) {
        List<Map<String, String>> resultList = new ArrayList<>();
        String token = getToken();
        try {
            String historyData = requestHistoryData(token, guid, startTime, endTime);
            JSONObject resultObj = JSON.parseObject(historyData);
            if (resultObj == null) {
                resultList.add(buildStatusItem(guid, STATUS_ERROR, "history response is empty"));
                return resultList;
            }
                // 4. å®šä¹‰ç›®æ ‡æŽ¢å¤´çš„paramCode,按需扩展
                String[] targetCodes = {"0100", "0110", "0120", "0130"};
                for (String code : targetCodes) {
                    // 5. éåŽ†å‚æ•°æ•°ç»„ï¼ŒåŒ¹é…ç›®æ ‡paramCode
                    for (int i = 0; i < paramList.size(); i++) {
                        JSONObject paramObj = paramList.getJSONObject(i);
                        String currentCode = paramObj.getString("paramCode");
                        if (code.equals(currentCode)) {
                            // 6. æå–核心字段(值、单位、探头名称)
                            String paramName = paramObj.getString("paramName"); // æŽ¢å¤´1/探头2...
                            String value = paramObj.getString("value");       // æ•°å€¼ï¼ˆ345.80/24.90...)
                            String unitCode = paramObj.getString("unitCode"); // å•位(Lux/℃/%RH/ppm)
            Integer code = resultObj.getInteger("code");
            if (!Integer.valueOf(0).equals(code)) {
                resultList.add(buildStatusItem(guid, resolveStatusByCodeOrMessage(code, resultObj.getString("msg")), resultObj.getString("msg")));
                return resultList;
            }
                            // 7. ä¸šåŠ¡å¤„ç†ï¼šæ‰“å°/赋值/存储等(按需修改)
                            System.out.println(paramName + ":" + value + " " + unitCode);
                            // åŒ¹é…åˆ°åŽç›´æŽ¥è·³å‡ºå†…层循环,提升效率
                            break;
            JSONArray historyList = resultObj.getJSONArray("data");
            if (historyList == null || historyList.isEmpty()) {
                resultList.add(buildStatusItem(guid, STATUS_OFFLINE, "no history data"));
                return resultList;
            }
            for (int i = 0; i < historyList.size(); i++) {
                JSONObject historyObj = historyList.getJSONObject(i);
                Map<String, String> historyItem = new HashMap<>();
                historyItem.put("guid", firstNonBlank(
                        historyObj.getString("deviceGuid"),
                        historyObj.getString("guid"),
                        guid
                ));
                historyItem.put("subUId", stringValue(historyObj.get("subUid")));
                historyItem.put("monitorTimeStamp", stringValue(historyObj.get("monitorTimeStamp")));
                historyItem.put("monitorTimeStr", historyObj.getString("monitorTimeStr"));
                historyItem.put("position", historyObj.getString("position"));
                historyItem.put("address", historyObj.getString("address"));
                historyItem.put(STATUS_KEY, resolveStatusByAlarmState(historyObj.get("alarmState"), STATUS_ONLINE));
                JSONArray paramList = historyObj.getJSONArray("data");
                if (paramList != null && !paramList.isEmpty()) {
                    for (int j = 0; j < paramList.size(); j++) {
                        JSONObject paramObj = paramList.getJSONObject(j);
                        String paramCode = paramObj.getString("paramCode");
                        String value = paramObj.getString("value");
                        String unitCode = paramObj.getString("unitCode");
                        String fullValue = concatValueWithUnit(value, unitCode);
                        if ("0100".equals(paramCode)) {
                            historyItem.put("light", fullValue);
                        } else if ("0110".equals(paramCode)) {
                            historyItem.put("temperature", fullValue);
                        } else if ("0120".equals(paramCode)) {
                            historyItem.put("humidity", fullValue);
                        } else if ("0130".equals(paramCode)) {
                            historyItem.put("co2", fullValue);
                        } else if ("0042".equals(paramCode)) {
                            historyItem.put("battery", fullValue);
                        } else if (paramCode != null) {
                            historyItem.put(paramCode, fullValue);
                        }
                    }
                }
                resultList.add(historyItem);
            }
            return resultList;
        } catch (Exception ex) {
            log.error("history data parse/request failed, guid={}", guid, ex);
            resultList.add(buildStatusItem(guid, STATUS_ERROR, ex.getMessage()));
            return resultList;
        }
    }
    public List<Map<String, String>> getRealData(List<String> guidList) {
        log.info("start get real data");
        List<Map<String, String>> listMaps = new ArrayList<>();
        if (guidList == null || guidList.isEmpty()) {
            return listMaps;
        }
        String token = getToken();
        try {
            String realTimeData = getRealTimeData(token, guidList);
            JSONObject batchResp = JSON.parseObject(realTimeData);
            Integer code = batchResp == null ? null : batchResp.getInteger("code");
            if (Integer.valueOf(0).equals(code)) {
                parseRealDataResponse(batchResp, guidList, listMaps);
                return listMaps;
            }
            log.warn("batch getRealData failed, fallback one by one. code={}, msg={}", code, batchResp == null ? null : batchResp.getString("msg"));
        } catch (Exception ex) {
            log.error("batch getRealData exception, fallback one by one", ex);
        }
        for (String guid : guidList) {
            listMaps.add(fetchSingleDeviceRealData(token, guid));
        }
        return listMaps;
    }
    public static void main(String[] args) {
        System.out.println();
    }
    public static String getToken(){
    public String getToken() {
        String cachedToken = sanitizeToken(redisTemplate.opsForValue().get(TOKEN_CACHE_KEY));
        if (cachedToken != null) {
            return cachedToken;
        }
        Map<String, String> param = new HashMap<>();
        param.put("keyId", KET_ID);
        param.put("keySecret", KEY_SECRET);
        param.put("userName", USER_NAME);
        param.put("password", PASS_WORD);
        log.info("请求参数:{}", JSON.toJSONString( param));
        log.info("request token payload: {}", JSON.toJSONString(param));
        String result = HttpUtils.sendPostJson(URL + TOKEN_URL, JSON.toJSONString(param));
        log.info("返回结果:{}", result);
        log.info("request token response: {}", result);
        Map<String, Object> map = JSON.parseObject(result, Map.class);
        if (map.get("code").equals(0)) {
        if (Integer.valueOf(0).equals(map.get("code"))) {
            Object token = map.get("data");
            log.info("token:{}", token);
            redisTemplate.opsForValue().set(TOKEN_CACHE_KEY, token.toString(), 60 * 60 * 12);
            return token.toString();
        }
        log.error("get token failed, response={}", result);
        return null;
    }
    private String sanitizeToken(String token) {
        if (token == null) {
            return null;
        }
        String cleanedToken = token.replace("\0", "").trim();
        return cleanedToken.isEmpty() ? null : cleanedToken;
    }
    private String firstNonBlank(String... values) {
        for (String value : values) {
            if (value != null && !value.trim().isEmpty()) {
                return value;
            }
        }
        return null;
    }
    private String stringValue(Object value) {
        return value == null ? null : String.valueOf(value);
    }
    private String refreshToken() {
        redisTemplate.delete(TOKEN_CACHE_KEY);
        return getToken();
    }
    private boolean isUnauthorizedException(Exception ex) {
        if (ex == null || ex.getMessage() == null) {
            return false;
        }
        String msg = ex.getMessage().toLowerCase();
        return msg.contains("401") || msg.contains("unauthorized");
    }
    private boolean isUnauthorizedResponse(String result) {
        if (result == null || result.trim().isEmpty()) {
            return false;
        }
        try {
            JSONObject obj = JSON.parseObject(result);
            if (obj == null) {
                return false;
            }
            Integer code = obj.getInteger("code");
            if (code != null && (code == 401 || code == 403)) {
                return true;
            }
            String msg = obj.getString("msg");
            return msg != null && msg.toLowerCase().contains("unauthorized");
        } catch (Exception ignore) {
            return result.contains("401");
        }
    }
    private String requestWithTokenRetry(String url, String payload, String token, String scene) {
        String usedToken = sanitizeToken(token);
        if (usedToken == null) {
            usedToken = refreshToken();
        }
        try {
            String result = HttpUtils.sendPostJson(url, payload, usedToken);
            if (!isUnauthorizedResponse(result) && StringUtils.isNotEmpty(result)) {
                return result;
            }
            log.warn("{} got unauthorized response, refresh token and retry once", scene);
        } catch (Exception ex) {
            if (!isUnauthorizedException(ex)) {
                throw ex;
            }
            log.warn("{} got 401 exception, refresh token and retry once", scene);
        }
        String newToken = refreshToken();
        if (newToken == null) {
            throw new RuntimeException("refresh token failed");
        }
        return HttpUtils.sendPostJson(url, payload, newToken);
    }
    private String concatValueWithUnit(String value, String unitCode) {
        if (value == null) {
            return null;
        }
        return value + (unitCode == null ? "" : unitCode);
    }
    private Map<String, String> buildStatusItem(String guid, String status, String statusMessage) {
        Map<String, String> result = new HashMap<>();
        result.put("guid", guid);
        result.put(STATUS_KEY, status);
        if (statusMessage != null && !statusMessage.trim().isEmpty()) {
            result.put(STATUS_MESSAGE_KEY, statusMessage);
        }
        return result;
    }
    public static String getRealTimeData(String  token){
    private String resolveStatusByCodeOrMessage(Integer code, String message) {
        if (code != null && (code == 5 || code == 1003 || code == 1004)) {
            return STATUS_OFFLINE;
        }
        if (message == null) {
            return STATUS_ERROR;
        }
        String lowered = message.toLowerCase();
        if (lowered.contains("offline") || message.contains("离线")) {
            return STATUS_OFFLINE;
        }
        return STATUS_ERROR;
    }
    private void parseRealDataResponse(JSONObject responseObj, List<String> requestedGuids, List<Map<String, String>> output) {
        JSONArray deviceList = responseObj.getJSONArray("data");
        Set<String> returnedGuids = new HashSet<>();
        if (deviceList != null) {
            for (int deviceIndex = 0; deviceIndex < deviceList.size(); deviceIndex++) {
                JSONObject deviceObj = deviceList.getJSONObject(deviceIndex);
                Map<String, String> deviceData = parseSingleDevice(deviceObj, null);
                String guid = deviceData.get("guid");
                if (guid != null) {
                    returnedGuids.add(guid);
                }
                output.add(deviceData);
            }
        }
        for (String requestGuid : requestedGuids) {
            if (!returnedGuids.contains(requestGuid)) {
                output.add(buildStatusItem(requestGuid, STATUS_OFFLINE, "device not returned by remote API"));
            }
        }
    }
    private Map<String, String> fetchSingleDeviceRealData(String token, String guid) {
        try {
            String singleResult = getRealTimeData(token, Collections.singletonList(guid));
            JSONObject singleObj = JSON.parseObject(singleResult);
            if (singleObj == null) {
                return buildStatusItem(guid, STATUS_ERROR, "single device response is empty");
            }
            Integer code = singleObj.getInteger("code");
            if (!Integer.valueOf(0).equals(code)) {
                return buildStatusItem(guid, resolveStatusByCodeOrMessage(code, singleObj.getString("msg")), singleObj.getString("msg"));
            }
            JSONArray dataList = singleObj.getJSONArray("data");
            if (dataList == null || dataList.isEmpty()) {
                return buildStatusItem(guid, STATUS_OFFLINE, "single device response has no data");
            }
            return parseSingleDevice(dataList.getJSONObject(0), guid);
        } catch (Exception ex) {
            log.error("single getRealData failed, guid={}", guid, ex);
            return buildStatusItem(guid, STATUS_ERROR, ex.getMessage());
        }
    }
    private Map<String, String> parseSingleDevice(JSONObject deviceObj, String fallbackGuid) {
        Map<String, String> deviceData = new HashMap<>();
        String deviceGuid = firstNonBlank(
                deviceObj.getString("deviceGuid"),
                deviceObj.getString("guid"),
                deviceObj.getString("devGuid"),
                deviceObj.getString("sn"),
                fallbackGuid
        );
        if (deviceGuid != null) {
            deviceData.put("guid", deviceGuid);
        }
        JSONArray paramList = deviceObj.getJSONArray("data");
        if (paramList == null || paramList.isEmpty()) {
            deviceData.put(STATUS_KEY, STATUS_OFFLINE);
            deviceData.put(STATUS_MESSAGE_KEY, "device data is empty");
            return deviceData;
        }
        deviceData.put(STATUS_KEY, resolveStatusByAlarmState(deviceObj.get("alarmState"), STATUS_ONLINE));
        for (int i = 0; i < paramList.size(); i++) {
            JSONObject paramObj = paramList.getJSONObject(i);
            String code = paramObj.getString("paramCode");
            String value = paramObj.getString("value");
            String unitCode = paramObj.getString("unitCode");
            String fullValue = concatValueWithUnit(value, unitCode);
            if ("0100".equals(code)) {
                deviceData.put("light", fullValue);
            } else if ("0110".equals(code)) {
                deviceData.put("temperature", fullValue);
            } else if ("0120".equals(code)) {
                deviceData.put("humidity", fullValue);
            } else if ("0130".equals(code)) {
                deviceData.put("co2", fullValue);
            } else if ("0042".equals(code)) {
                deviceData.put("battery", fullValue);
            }
        }
        return deviceData;
    }
    private String resolveStatusByAlarmState(Object alarmState, String defaultStatus) {
        if (alarmState == null) {
            return defaultStatus;
        }
        if (alarmState instanceof Boolean) {
            return (Boolean) alarmState ? STATUS_OFFLINE : STATUS_ONLINE;
        }
        String normalized = String.valueOf(alarmState).trim().toLowerCase();
        if ("true".equals(normalized) || "1".equals(normalized)) {
            return STATUS_OFFLINE;
        }
        if ("false".equals(normalized) || "0".equals(normalized)) {
            return STATUS_ONLINE;
        }
        return defaultStatus;
    }
    public String getRealTimeData(String token, List<String> guidList) {
        Map<String, Object> param = new HashMap<>();
        param.put("keyId", KET_ID);
        param.put("keySecret", KEY_SECRET);
        param.put("deviceGuids", Collections.singletonList(DEVICE_GUID));
        log.info("请求参数:{}", JSON.toJSONString( param));
        String result = HttpUtils.sendPostJson(URL + REAL_TIME_URL, JSON.toJSONString(param),token);
        log.info("返回结果:{}", result);
        param.put("deviceGuids", guidList);
        log.info("request realtime payload: {}", JSON.toJSONString(param));
        String cacheKey = REAL_TIME_CACHE_PREFIX + JSON.toJSONString(param);
        String cachedResult = sanitizeToken(redisTemplate.opsForValue().get(cacheKey));
        if (cachedResult != null) {
            log.info("hit realtime cache: {}", cacheKey);
            return cachedResult;
        }
        String result = requestWithTokenRetry(URL + REAL_TIME_URL, JSON.toJSONString(param), token, "getRealTimeData");
        log.info("request realtime response: {}", result);
        cacheRemoteResponse(cacheKey, result);
        return result;
    }
    public String requestHistoryData(String token, String guid, long startTime, long endTime) {
        Map<String, Object> param = new HashMap<>();
        param.put("keyId", KET_ID);
        param.put("keySecret", KEY_SECRET);
        param.put("deviceGuid", guid);
        param.put("startTime", startTime);
        param.put("endTime", endTime);
        log.info("request history payload: {}", JSON.toJSONString(param));
        String cacheKey = HISTORY_CACHE_PREFIX + JSON.toJSONString(param);
        String cachedResult = sanitizeToken(redisTemplate.opsForValue().get(cacheKey));
        if (cachedResult != null) {
            log.info("hit history cache: {}", cacheKey);
            return cachedResult;
        }
        String result = requestWithTokenRetry(REAL_HISTORY_URL, JSON.toJSONString(param), token, "getHistoryData");
        log.info("request history response: {}", result);
        cacheRemoteResponse(cacheKey, result);
        return result;
    }
    private void cacheRemoteResponse(String cacheKey, String result) {
        if (result == null || result.trim().isEmpty()) {
            return;
        }
        redisTemplate.opsForValue().set(cacheKey, result, REMOTE_CACHE_TTL_SECONDS_30, TimeUnit.SECONDS);
    }
}
src/main/java/com/ruoyi/inspectiontask/controller/TimingTaskController.java
@@ -7,8 +7,11 @@
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.inspectiontask.dto.InspectionTaskDto;
import com.ruoyi.inspectiontask.dto.TimingTaskDto;
import com.ruoyi.inspectiontask.pojo.InspectionTask;
import com.ruoyi.inspectiontask.pojo.TimingTask;
import com.ruoyi.inspectiontask.service.InspectionTaskService;
import com.ruoyi.inspectiontask.service.TimingTaskService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,6 +31,7 @@
public class TimingTaskController extends BaseController {
    private TimingTaskService timingTaskService;
    private InspectionTaskService inspectionTaskService;
    /**
     * å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨æŸ¥è¯¢
@@ -63,6 +67,26 @@
    }
    /**
     * ä¿®æ”¹å¯ç”¨çŠ¶æ€
     */
    @PostMapping("/changeEnable")
    @Operation(summary = "启用或禁用定时任务")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    public R changeEnable(@RequestBody TimingTask timingTask) throws SchedulerException {
        return R.ok(timingTaskService.changeEnable(timingTask.getId(), timingTask.getIsEnabled()));
    }
    /**
     * å½“日巡检记录
     */
    @GetMapping("/recordList/{timingId}")
    @Operation(summary = "按定时任务查询巡检记录")
    public R<IPage<InspectionTaskDto>> recordList(Page<InspectionTask> page, @PathVariable Long timingId) {
        IPage<InspectionTaskDto> list = inspectionTaskService.selectInspectionTaskRecordList(page, timingId);
        return R.ok(list);
    }
    /**
     * å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨åˆ é™¤
     */
    @DeleteMapping("/delTimingTask")
src/main/java/com/ruoyi/inspectiontask/dto/InspectionTaskDto.java
@@ -1,10 +1,13 @@
package com.ruoyi.inspectiontask.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.inspectiontask.pojo.InspectionTask;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
@Data
@@ -25,4 +28,14 @@
    private List<StorageBlobVO> commonFileListAfterVO; //生产后
    private List<StorageBlobVO> commonFileListBeforeVO; //生产前
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTimeStart;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTimeEnd;
    private String areaName;
}
src/main/java/com/ruoyi/inspectiontask/dto/TimingTaskDto.java
@@ -9,4 +9,6 @@
public class TimingTaskDto extends TimingTask {
    private List<String> inspector;
    private String areaName;
}
src/main/java/com/ruoyi/inspectiontask/mapper/InspectionTaskMapper.java
@@ -1,11 +1,17 @@
package com.ruoyi.inspectiontask.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.inspectiontask.dto.InspectionTaskDto;
import com.ruoyi.inspectiontask.pojo.InspectionTask;
import org.apache.ibatis.annotations.Param;
/**
 * @author :yys
 * @date : 2025/9/19 10:46
 */
public interface InspectionTaskMapper extends BaseMapper<InspectionTask> {
    IPage<InspectionTask> selectInspectionTaskAggregatePage(Page<InspectionTask> page, @Param("dto") InspectionTaskDto dto);
}
src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -36,6 +37,10 @@
    @Schema(description = "设备id")
    private Integer taskId;
    @ApiModelProperty(value = "巡检状态:1=待巡检,2=已巡检")
    @Excel(name = "巡检状态", readConverterExp = "1=待巡检,2=已巡检")
    private Integer inspectionStatus;
    @Schema(description = "巡检人ID")
    private String inspectorId;
@@ -113,4 +118,9 @@
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    private Long areaId;
    @Schema(description = "来源定时任务ID")
    private Long timingId;
}
src/main/java/com/ruoyi/inspectiontask/pojo/TimingTask.java
@@ -117,4 +117,9 @@
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    private Long areaId;
    @Schema(description = "设备ID集合")
    private String taskIdsStr;
}
src/main/java/com/ruoyi/inspectiontask/service/InspectionTaskService.java
@@ -14,6 +14,8 @@
    IPage<InspectionTaskDto> selectInspectionTaskList(Page<InspectionTask> page, InspectionTaskDto inspectionTaskDto);
    IPage<InspectionTaskDto> selectInspectionTaskRecordList(Page<InspectionTask> page, Long timingId);
    int addOrEditInspectionTask(InspectionTaskDto inspectionTaskDto);
    int delByIds(Long[] ids);
src/main/java/com/ruoyi/inspectiontask/service/TimingTaskService.java
@@ -17,6 +17,8 @@
    int addOrEditTimingTask(TimingTaskDto timingTaskDto) throws SchedulerException;
    int changeEnable(Long id, Integer isEnabled) throws SchedulerException;
    int delByIds(Long[] ids);
    void updateTaskExecutionTime(Long taskId);
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
@@ -12,7 +12,9 @@
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.device.mapper.DeviceAreaMapper;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.pojo.DeviceArea;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.DeviceRepair;
@@ -28,6 +30,8 @@
import org.springframework.transaction.annotation.Transactional;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.*;
import java.util.function.Function;
@@ -53,30 +57,21 @@
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final DeviceAreaMapper deviceAreaMapper;
    private static final String INSPECTION_RESULT_ABNORMAL = "0";
    private static final String INSPECTION_RESULT_NORMAL = "1";
    private static final int REPAIR_STATUS_PENDING = 0;
    @Override
    public IPage<InspectionTaskDto> selectInspectionTaskList(Page<InspectionTask> page, InspectionTaskDto inspectionTaskDto) {
        LambdaQueryWrapper<InspectionTask> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByDesc(InspectionTask::getCreateTime);
        if (StringUtils.isNotBlank(inspectionTaskDto.getTaskName())) {
            queryWrapper.like(InspectionTask::getTaskName, inspectionTaskDto.getTaskName());
        }
        if (StringUtils.isNotBlank(inspectionTaskDto.getInspectionProject())) {
            queryWrapper.like(InspectionTask::getInspectionProject, inspectionTaskDto.getInspectionProject());
        }
        queryWrapper.orderByDesc(InspectionTask::getCreateTime);
        IPage<InspectionTask> entityPage = inspectionTaskMapper.selectPage(page, queryWrapper);
        IPage<InspectionTask> entityPage = inspectionTaskMapper.selectInspectionTaskAggregatePage(page, inspectionTaskDto);
        //  æ— æ•°æ®æå‰è¿”回
        if (CollectionUtils.isEmpty(entityPage.getRecords())) {
            return new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
        }
        //登记人ids
        List<Long> registrantIds = entityPage.getRecords().stream().map(InspectionTask::getRegistrantId).collect(Collectors.toList());
        // æ‰¹é‡æŸ¥è¯¢ç™»è®°äºº
        Map<Long, SysUser> sysUserMap;
        if (!registrantIds.isEmpty()) {
            List<SysUser> sysUsers = sysUserMapper.selectUsersByIds(registrantIds);
@@ -84,9 +79,9 @@
        } else {
            sysUserMap = new HashMap<>();
        }
        //获取所有不重复的用户ID
        Set<Long> allUserIds = entityPage.getRecords().stream()
                .map(InspectionTask::getInspectorId) // èŽ·å–"2,3"这样的字符串
                .map(InspectionTask::getInspectorId)
                .filter(StringUtils::isNotBlank)
                .flatMap(idsStr -> Arrays.stream(idsStr.split(",")))
                .map(idStr -> {
@@ -99,7 +94,6 @@
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        // ä½¿ç”¨SQL批量查询用户信息
        Map<Long, String> userIdToNameMap = allUserIds.isEmpty()
                ? Collections.emptyMap()
                : sysUserMapper.selectUsersByIds(new ArrayList<>(allUserIds))
@@ -108,42 +102,138 @@
                        SysUser::getUserId,
                        SysUser::getNickName,
                        (existing, replacement) -> existing));
        List<InspectionTaskDto> dtoList = entityPage.getRecords().stream().map(inspectionTask -> {
            InspectionTaskDto dto = new InspectionTaskDto();
            BeanUtils.copyProperties(inspectionTask, dto);  // å¤åˆ¶ä¸»å¯¹è±¡å±žæ€§
            // è®¾ç½®ç™»è®°äºº
            SysUser sysUser = sysUserMap.get(inspectionTask.getRegistrantId());
            if (sysUser != null) {
                dto.setRegistrant(sysUser.getNickName());
            }
            // å¤„理巡检人名称
            if (StringUtils.isNotBlank(inspectionTask.getInspectorId())) {
                String inspectorNames = Arrays.stream(inspectionTask.getInspectorId().split(","))
                        .map(String::trim)
                        .map(idStr -> {
                            try {
                                Long userId = Long.parseLong(idStr);
                                return userIdToNameMap.getOrDefault(userId, "未知用户(" + idStr + ")");
                            } catch (NumberFormatException e) {
                                return "无效ID(" + idStr + ")";
                            }
                        })
                        .collect(Collectors.joining(","));
                dto.setInspector(inspectorNames);
            }
        Set<Long> areaIds = entityPage.getRecords().stream()
                .map(InspectionTask::getAreaId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> areaNameMap = new HashMap<>();
        if (!areaIds.isEmpty()) {
            List<DeviceArea> areas = deviceAreaMapper.selectBatchIds(new ArrayList<>(areaIds));
            areas.forEach(area -> areaNameMap.put(area.getId(), area.getAreaName()));
        }
            dto.setDateStr(inspectionTask.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
            // åˆå§‹åŒ–三个附件列表
            dto.setCommonFileListVO(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.FILE, RecordTypeEnum.INSPECTION_TASK, inspectionTask.getId()));
            dto.setCommonFileListAfterVO(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.AFTER_FILE, RecordTypeEnum.INSPECTION_TASK, inspectionTask.getId()));
            dto.setCommonFileListBeforeVO(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.BEFORE_FILE, RecordTypeEnum.INSPECTION_TASK, inspectionTask.getId()));
        List<InspectionTaskDto> dtoList = buildInspectionTaskDtoList(
                entityPage.getRecords(),
                sysUserMap,
                userIdToNameMap,
                areaNameMap
        );
        IPage<InspectionTaskDto> resultPage = new Page<>();
        BeanUtils.copyProperties(entityPage, resultPage);
        resultPage.setRecords(dtoList);
        return resultPage;
    }
            return dto;
        }).collect(Collectors.toList());
    private List<InspectionTaskDto> buildInspectionTaskDtoList(List<InspectionTask> records,
                                                               Map<Long, SysUser> sysUserMap,
                                                               Map<Long, String> userIdToNameMap,
                                                               Map<Long, String> areaNameMap) {
        if (CollectionUtils.isEmpty(records)) {
            return Collections.emptyList();
        }
        return records.stream()
                .map(record -> buildInspectionTaskDto(record, sysUserMap, userIdToNameMap, areaNameMap))
                .collect(Collectors.toList());
    }
        // 7. æž„建返回分页对象
    private InspectionTaskDto buildInspectionTaskDto(InspectionTask baseTask,
                                                     Map<Long, SysUser> sysUserMap,
                                                     Map<Long, String> userIdToNameMap,
                                                     Map<Long, String> areaNameMap) {
        InspectionTaskDto dto = new InspectionTaskDto();
        BeanUtils.copyProperties(baseTask, dto);
        SysUser sysUser = sysUserMap.get(baseTask.getRegistrantId());
        if (sysUser != null) {
            dto.setRegistrant(sysUser.getNickName());
        }
        if (StringUtils.isNotBlank(baseTask.getInspectorId())) {
            String inspectorNames = Arrays.stream(baseTask.getInspectorId().split(","))
                    .map(String::trim)
                    .map(idStr -> {
                        try {
                            Long userId = Long.parseLong(idStr);
                            return userIdToNameMap.getOrDefault(userId, "未知用户(" + idStr + ")");
                        } catch (NumberFormatException e) {
                            return "无效ID(" + idStr + ")";
                        }
                    })
                    .collect(Collectors.joining(","));
            dto.setInspector(inspectorNames);
        }
        if (baseTask.getCreateTime() != null) {
            dto.setDateStr(baseTask.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        }
        if (baseTask.getAreaId() != null) {
            dto.setAreaName(areaNameMap.getOrDefault(baseTask.getAreaId(), ""));
        }
        dto.setCommonFileListVO(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.FILE, RecordTypeEnum.INSPECTION_TASK, baseTask.getId()));
        dto.setCommonFileListAfterVO(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.AFTER_FILE, RecordTypeEnum.INSPECTION_TASK, baseTask.getId()));
        dto.setCommonFileListBeforeVO(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.BEFORE_FILE, RecordTypeEnum.INSPECTION_TASK, baseTask.getId()));
        return dto;
    }
    @Override
    public IPage<InspectionTaskDto> selectInspectionTaskRecordList(Page<InspectionTask> page, Long timingId) {
        InspectionTaskDto queryDto = new InspectionTaskDto();
        queryDto.setTimingId(timingId);
        queryDto.setCreateTimeStart(LocalDate.now().atStartOfDay());
        queryDto.setCreateTimeEnd(queryDto.getCreateTimeStart().plusDays(1));
        IPage<InspectionTask> entityPage = inspectionTaskMapper.selectInspectionTaskAggregatePage(page, queryDto);
        if (CollectionUtils.isEmpty(entityPage.getRecords())) {
            return new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
        }
        List<Long> registrantIds = entityPage.getRecords().stream().map(InspectionTask::getRegistrantId).collect(Collectors.toList());
        Map<Long, SysUser> sysUserMap;
        if (!registrantIds.isEmpty()) {
            List<SysUser> sysUsers = sysUserMapper.selectUsersByIds(registrantIds);
            sysUserMap = sysUsers.stream().collect(Collectors.toMap(SysUser::getUserId, Function.identity()));
        } else {
            sysUserMap = new HashMap<>();
        }
        Set<Long> allUserIds = entityPage.getRecords().stream()
                .map(InspectionTask::getInspectorId)
                .filter(StringUtils::isNotBlank)
                .flatMap(idsStr -> Arrays.stream(idsStr.split(",")))
                .map(idStr -> {
                    try {
                        return Long.parseLong(idStr.trim());
                    } catch (NumberFormatException e) {
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> userIdToNameMap = allUserIds.isEmpty()
                ? Collections.emptyMap()
                : sysUserMapper.selectUsersByIds(new ArrayList<>(allUserIds))
                .stream()
                .collect(Collectors.toMap(
                        SysUser::getUserId,
                        SysUser::getNickName,
                        (existing, replacement) -> existing));
        Set<Long> areaIds = entityPage.getRecords().stream()
                .map(InspectionTask::getAreaId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> areaNameMap = new HashMap<>();
        if (!areaIds.isEmpty()) {
            List<DeviceArea> areas = deviceAreaMapper.selectBatchIds(new ArrayList<>(areaIds));
            areas.forEach(area -> areaNameMap.put(area.getId(), area.getAreaName()));
        }
        List<InspectionTaskDto> dtoList = entityPage.getRecords().stream()
                .map(task -> buildInspectionTaskDto(task, sysUserMap, userIdToNameMap, areaNameMap))
                .collect(Collectors.toList());
        IPage<InspectionTaskDto> resultPage = new Page<>();
        BeanUtils.copyProperties(entityPage, resultPage);
        resultPage.setRecords(dtoList);
@@ -166,6 +256,7 @@
        BeanUtils.copyProperties(inspectionTaskDto, inspectionTask);
        inspectionTask.setRegistrantId(SecurityUtils.getLoginUser().getUserId());
        inspectionTask.setRegistrant(SecurityUtils.getLoginUser().getUsername());
        inspectionTask.setInspectionStatus(2);
        fillAcceptanceInfo(inspectionTask, oldInspectionTask);
        int i;
        if (Objects.isNull(inspectionTaskDto.getId())) {
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java
@@ -15,9 +15,12 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@DisallowConcurrentExecution // ç¦æ­¢å¹¶å‘执行同一个Job
@@ -56,9 +59,12 @@
//                throw new JobExecutionException("定时任务已禁用: " + taskId);
//            }
            // 2. åˆ›å»ºå¹¶ä¿å­˜å·¡æ£€ä»»åŠ¡è®°å½• - è¿™å°±æ˜¯æ‚¨æä¾›çš„代码应该放的位置
            InspectionTask inspectionTask = createInspectionTask(timingTask);
            inspectionTaskMapper.insert(inspectionTask);
            // 2. è§£æžè®¾å¤‡ID列表,为每个设备创建巡检任务记录
            List<Integer> deviceIds = resolveDeviceIds(timingTask);
            for (Integer deviceId : deviceIds) {
                InspectionTask inspectionTask = createInspectionTask(timingTask, deviceId);
                inspectionTaskMapper.insert(inspectionTask);
            }
            // 3. æ›´æ–°å®šæ—¶ä»»åŠ¡çš„æ‰§è¡Œæ—¶é—´
            if (!tasks.isEmpty()) {
@@ -97,16 +103,32 @@
        }
    }
    // è¿™å°±æ˜¯æ‚¨æä¾›çš„代码封装成的方法
    private InspectionTask createInspectionTask(TimingTask timingTask) {
    private List<Integer> resolveDeviceIds(TimingTask timingTask) throws JobExecutionException {
        if (StringUtils.isNotBlank(timingTask.getTaskIdsStr())) {
            return Arrays.stream(timingTask.getTaskIdsStr().split(","))
                    .map(String::trim)
                    .filter(StringUtils::isNotBlank)
                    .map(Integer::parseInt)
                    .collect(Collectors.toList());
        }
        if (timingTask.getTaskId() != null) {
            List<Integer> list = new ArrayList<>();
            list.add(timingTask.getTaskId());
            return list;
        }
        throw new JobExecutionException("定时任务 " + timingTask.getId() + " æœªé…ç½®è®¾å¤‡ID");
    }
    private InspectionTask createInspectionTask(TimingTask timingTask, Integer deviceId) {
        InspectionTask inspectionTask = new InspectionTask();
        // å¤åˆ¶åŸºæœ¬å±žæ€§
        inspectionTask.setTaskName(timingTask.getTaskName());
        inspectionTask.setInspectionProject(timingTask.getInspectionProject());
        inspectionTask.setTaskId(timingTask.getTaskId());
        inspectionTask.setTaskId(deviceId);
        inspectionTask.setInspectorId(timingTask.getInspectorIds());
        inspectionTask.setInspectionLocation(timingTask.getInspectionLocation());
        inspectionTask.setAreaId(timingTask.getAreaId());
        inspectionTask.setTimingId(timingTask.getId());
        String remarks = "自动生成自定时任务ID: " + timingTask.getId();
        if (StringUtils.isNotBlank(timingTask.getRemarks())) {
            remarks = remarks + ";" + timingTask.getRemarks();
@@ -116,6 +138,7 @@
        inspectionTask.setFrequencyType(timingTask.getFrequencyType());
        inspectionTask.setFrequencyDetail(timingTask.getFrequencyDetail());
        inspectionTask.setTenantId(timingTask.getTenantId());
        inspectionTask.setInspectionStatus(1);
        return inspectionTask;
    }
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduleUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,171 @@
package com.ruoyi.inspectiontask.service.impl;
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.HashSet;
import java.util.Set;
final class TimingTaskScheduleUtils {
    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
    private TimingTaskScheduleUtils() {
    }
    static LocalDateTime calculateFirstExecutionTime(String frequencyType, String frequencyDetail) {
        return calculateNextExecutionTime(frequencyType, frequencyDetail, LocalDateTime.now().minusSeconds(1));
    }
    static LocalDateTime calculateNextExecutionTime(String frequencyType, String frequencyDetail, LocalDateTime currentTime) {
        if (frequencyType == null || frequencyDetail == null) {
            throw new IllegalArgumentException("任务参数不能为空");
        }
        switch (frequencyType.toUpperCase()) {
            case "DAILY":
                return calculateDailyNextTime(frequencyDetail, currentTime);
            case "WEEKLY":
                return calculateWeeklyNextTime(frequencyDetail, currentTime);
            case "MONTHLY":
                return calculateMonthlyNextTime(frequencyDetail, currentTime);
            case "QUARTERLY":
                return calculateQuarterlyNextTime(frequencyDetail, currentTime);
            default:
                throw new IllegalArgumentException("不支持的频率类型: " + frequencyType);
        }
    }
    private static LocalDateTime calculateDailyNextTime(String timeStr, LocalDateTime current) {
        LocalTime executionTime = parseTime(timeStr);
        LocalDateTime nextTime = LocalDateTime.of(current.toLocalDate(), executionTime);
        return current.isBefore(nextTime) ? nextTime : nextTime.plusDays(1);
    }
    private static LocalDateTime calculateWeeklyNextTime(String detail, LocalDateTime current) {
        String[] parts = validateAndSplit(detail, ",", 2);
        Set<DayOfWeek> targetDays = parseDayOfWeeks(parts[0]);
        LocalTime time = parseTime(parts[1]);
        LocalDateTime nextTime = current;
        for (int i = 0; i < 366; i++) {
            nextTime = nextTime.plusDays(1);
            if (targetDays.contains(nextTime.getDayOfWeek())) {
                return LocalDateTime.of(nextTime.toLocalDate(), time);
            }
        }
        throw new IllegalArgumentException("无法找到下一次执行时间");
    }
    private static LocalDateTime calculateMonthlyNextTime(String detail, LocalDateTime current) {
        String[] parts = validateAndSplit(detail, ",", 2);
        int dayOfMonth = validateDayOfMonth(parts[0]);
        LocalTime time = parseTime(parts[1]);
        for (int i = 0; i < 24; i++) {
            YearMonth targetYearMonth = YearMonth.from(current).plusMonths(i);
            int adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
            LocalDateTime target = LocalDateTime.of(
                    targetYearMonth.getYear(),
                    targetYearMonth.getMonthValue(),
                    adjustedDay,
                    time.getHour(),
                    time.getMinute()
            );
            if (target.isAfter(current)) {
                return target;
            }
        }
        throw new IllegalArgumentException("无法找到下一次执行时间");
    }
    private static LocalDateTime calculateQuarterlyNextTime(String detail, LocalDateTime current) {
        String[] parts = validateAndSplit(detail, ",", 3);
        int startMonth = validateMonth(parts[0]);
        int dayOfMonth = validateDayOfMonth(parts[1]);
        LocalTime time = parseTime(parts[2]);
        YearMonth anchor = YearMonth.of(current.getYear(), startMonth);
        for (int i = 0; i < 12; i++) {
            YearMonth targetYearMonth = anchor.plusMonths(3L * i);
            int adjustedDay = Math.min(dayOfMonth, targetYearMonth.lengthOfMonth());
            LocalDateTime target = LocalDateTime.of(
                    targetYearMonth.getYear(),
                    targetYearMonth.getMonthValue(),
                    adjustedDay,
                    time.getHour(),
                    time.getMinute()
            );
            if (target.isAfter(current)) {
                return target;
            }
        }
        throw new IllegalArgumentException("无法找到下一次执行时间");
    }
    private static LocalTime parseTime(String timeStr) {
        try {
            return LocalTime.parse(timeStr, TIME_FORMATTER);
        } catch (DateTimeParseException e) {
            throw new IllegalArgumentException("时间格式必须为HH:mm", e);
        }
    }
    private static String[] validateAndSplit(String input, String delimiter, int expectedParts) {
        String[] parts = input.split(delimiter);
        if (parts.length != expectedParts) {
            throw new IllegalArgumentException("参数格式错误");
        }
        return parts;
    }
    private static int validateDayOfMonth(String dayStr) {
        int day = Integer.parseInt(dayStr.trim());
        if (day < 1 || day > 31) {
            throw new IllegalArgumentException("日期必须在1-31之间");
        }
        return day;
    }
    private static int validateMonth(String monthStr) {
        int month = Integer.parseInt(monthStr.trim());
        if (month < 1 || month > 12) {
            throw new IllegalArgumentException("月份必须在1-12之间");
        }
        return month;
    }
    private static Set<DayOfWeek> parseDayOfWeeks(String dayOfWeekStr) {
        Set<DayOfWeek> days = new HashSet<>();
        for (String dayStr : dayOfWeekStr.split("\\|")) {
            switch (dayStr.trim().toUpperCase()) {
                case "MON":
                    days.add(DayOfWeek.MONDAY);
                    break;
                case "TUE":
                    days.add(DayOfWeek.TUESDAY);
                    break;
                case "WED":
                    days.add(DayOfWeek.WEDNESDAY);
                    break;
                case "THU":
                    days.add(DayOfWeek.THURSDAY);
                    break;
                case "FRI":
                    days.add(DayOfWeek.FRIDAY);
                    break;
                case "SAT":
                    days.add(DayOfWeek.SATURDAY);
                    break;
                case "SUN":
                    days.add(DayOfWeek.SUNDAY);
                    break;
                default:
                    throw new IllegalArgumentException("无效的星期几: " + dayStr);
            }
        }
        return days;
    }
}
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
@@ -6,6 +6,8 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.device.mapper.DeviceAreaMapper;
import com.ruoyi.device.pojo.DeviceArea;
import com.ruoyi.inspectiontask.dto.TimingTaskDto;
import com.ruoyi.inspectiontask.mapper.TimingTaskMapper;
import com.ruoyi.inspectiontask.pojo.TimingTask;
@@ -35,6 +37,7 @@
    private final TimingTaskMapper timingTaskMapper;
    private final TimingTaskScheduler timingTaskScheduler;
    private final SysUserMapper sysUserMapper;
    private final DeviceAreaMapper deviceAreaMapper;
    private static final int ENABLED = 1;
    private static final int DISABLED = 0;
@@ -52,6 +55,12 @@
        }
        if (timingTask.getIsEnabled() != null) {
            queryWrapper.eq(TimingTask::getIsEnabled, timingTask.getIsEnabled());
        }
        if (timingTask.getAreaId() != null) {
            queryWrapper.eq(TimingTask::getAreaId, timingTask.getAreaId());
        }
        if (StringUtils.isNotBlank(timingTask.getTaskIdsStr())) {
            queryWrapper.like(TimingTask::getTaskIdsStr, timingTask.getTaskIdsStr());
        }
        queryWrapper.orderByDesc(TimingTask::getCreateTime);
        IPage<TimingTask> taskPage = timingTaskMapper.selectPage(page, queryWrapper);
@@ -89,6 +98,17 @@
            users.forEach(user -> userNickNameMap.put(user.getUserId(), user.getNickName()));
        }
        // 4.1 æ‰¹é‡æŸ¥è¯¢åŒºåŸŸåç§°
        Set<Long> areaIds = taskPage.getRecords().stream()
                .map(TimingTask::getAreaId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> areaNameMap = new HashMap<>();
        if (!areaIds.isEmpty()) {
            List<DeviceArea> areas = deviceAreaMapper.selectBatchIds(new ArrayList<>(areaIds));
            areas.forEach(area -> areaNameMap.put(area.getId(), area.getAreaName()));
        }
        // 5. è½¬æ¢ä¸ºDTO
        List<TimingTaskDto> dtoList = taskPage.getRecords().stream().map(task -> {
            TimingTaskDto dto = new TimingTaskDto();
@@ -112,6 +132,11 @@
                dto.setInspector(inspectorNickNames);
            }
            // è®¾ç½®åŒºåŸŸåç§°
            if (task.getAreaId() != null) {
                dto.setAreaName(areaNameMap.getOrDefault(task.getAreaId(), ""));
            }
            return dto;
        }).collect(Collectors.toList());
@@ -122,7 +147,7 @@
    }
    @Override
    @Transactional
    @Transactional(rollbackFor = Exception.class)
    public int addOrEditTimingTask(TimingTaskDto timingTaskDto) throws SchedulerException {
        TimingTask oldTimingTask = null;
        if (Objects.nonNull(timingTaskDto.getId())) {
@@ -179,6 +204,34 @@
            }
            return result;
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int changeEnable(Long id, Integer isEnabled) throws SchedulerException {
        TimingTask oldTimingTask = timingTaskMapper.selectById(id);
        if (oldTimingTask == null) {
            throw new IllegalArgumentException("定时任务不存在");
        }
        TimingTask update = new TimingTask();
        update.setId(id);
        update.setIsEnabled(resolveEnabledValue(isEnabled, oldTimingTask));
        update.setActive(ENABLED == update.getIsEnabled());
        int result = timingTaskMapper.updateById(update);
        if (result <= 0) {
            return result;
        }
        boolean enabled = isEnabled(update.getIsEnabled(), update.isActive());
        if (!enabled) {
            timingTaskScheduler.unscheduleTimingTask(id);
        } else if (oldTimingTask.getIsEnabled() != null && oldTimingTask.getIsEnabled() == DISABLED) {
            timingTaskScheduler.scheduleTimingTask(oldTimingTask);
        } else {
            timingTaskScheduler.resumeTimingTask(id);
        }
        return result;
    }
    public LocalDateTime calculateFirstExecutionTime(TimingTask task) {
@@ -315,7 +368,7 @@
    }
    private LocalDateTime calculateCustomFirstExecution(String frequencyDetail) {
        return null;
        return TimingTaskScheduleUtils.calculateFirstExecutionTime("QUARTERLY", frequencyDetail);
    }
    @Override
@@ -421,7 +474,7 @@
    /**
     * è®¡ç®—每季度任务的下次执行时间
     */
    private LocalDateTime calculateQuarterlyNextTime(String detail, LocalDateTime current) {
    private LocalDateTime calculateQuarterlyNextTimeLegacy(String detail, LocalDateTime current) {
        String[] parts = detail.split(",");
        int quarterMonth = Integer.parseInt(parts[0]); // 1=第1个月,2=第2个月,3=第3个月
        int dayOfMonth = Integer.parseInt(parts[1]);
@@ -457,6 +510,9 @@
    /**
     * è§£æžæ˜ŸæœŸå‡ å­—符串
     */
    private LocalDateTime calculateQuarterlyNextTime(String detail, LocalDateTime current) {
        return TimingTaskScheduleUtils.calculateNextExecutionTime("QUARTERLY", detail, current);
    }
    private Set<DayOfWeek> parseDayOfWeeks(String dayOfWeekStr) {
        Set<DayOfWeek> days = new HashSet<>();
        String[] dayStrs = dayOfWeekStr.split("\\|");
src/main/resources/application-dev.yml
@@ -74,7 +74,7 @@
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://localhost:3306/product-inventory-management-new-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        url: jdbc:mysql://localhost:3306/product-inventory-management-dlsmls-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      # ä»Žåº“数据源
src/main/resources/application-dlsmls-pro.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
# é¡¹ç›®ç›¸å…³é…ç½®
ruoyi:
  # åç§°
  name: RuoYi
  # ç‰ˆæœ¬
  version: 3.8.9
  # ç‰ˆæƒå¹´ä»½
  copyrightYear: 2025
  # æ–‡ä»¶è·¯å¾„ ç¤ºä¾‹ï¼ˆ Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: /javaWork/product-inventory-management/file
  # èŽ·å–ip地址开关
  addressEnabled: false
  # éªŒè¯ç ç±»åž‹ math æ•°å­—计算 char å­—符验证
  captchaType: math
  # ååŒå®¡æ‰¹ç¼–号前缀(配置文件后缀命名)
  approvalNumberPrefix: NEW
  # ä¸ªæŽ¨ Unipush é…ç½®
  getui:
    appId: PfjyAAE0FK64FaO1w2CMb1
    appKey: zTMb831OEL6J4GK1uE3Ob4
    masterSecret: K1GFtsv42v61tXGnF7SGE5
    domain: https://restapi.getui.cn/v2/
    # ç¦»çº¿æŽ¨é€ä½¿ç”¨çš„包名/组件名
    intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
  port: 9003
  servlet:
    # åº”用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # è¿žæŽ¥æ•°æ»¡åŽçš„æŽ’队数,默认为100
    accept-count: 1000
    threads:
      # tomcat最大线程数,默认为200
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
# æ—¥å¿—配置
logging:
  level:
    com.ruoyi: warn
    org.springframework: warn
minio:
  endpoint: http://114.132.189.42/
  port: 7019
  secure: false
  accessKey: admin
  secretKey: 12345678
  preview-expiry: 24 # é¢„览地址默认24小时
  default-bucket: jxc
# ç”¨æˆ·é…ç½®
user:
  password:
    # å¯†ç æœ€å¤§é”™è¯¯æ¬¡æ•°
    maxRetryCount: 5
    # å¯†ç é”å®šæ—¶é—´ï¼ˆé»˜è®¤10分钟)
    lockTime: 10
# Spring配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://172.17.0.1:3306/product-inventory-management-dlsmls-pro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: xd@123456..
      # ä»Žåº“数据源
      slave:
        # ä»Žæ•°æ®æºå¼€å…³/默认关闭
        enabled: false
        url:
        username:
        password:
      # åˆå§‹è¿žæŽ¥æ•°
      initialSize: 5
      # æœ€å°è¿žæŽ¥æ± æ•°é‡
      minIdle: 10
      # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
      maxActive: 20
      # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´
      maxWait: 60000
      # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
      connectTimeout: 30000
      # é…ç½®ç½‘络超时时间
      socketTimeout: 60000
      # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      minEvictableIdleTimeMillis: 300000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # è®¾ç½®ç™½åå•,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-username: ruoyi
        login-password: 123456
      filter:
        stat:
          enabled: true
          # æ…¢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
  # èµ„源信息
  messages:
    # å›½é™…化资源文件路径
    basename: i18n/messages
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
      # å•个文件大小
      max-file-size: 1GB
      # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
      max-request-size: 2GB
  # æœåŠ¡æ¨¡å—
  devtools:
    restart:
      # çƒ­éƒ¨ç½²å¼€å…³
      enabled: false
  # redis é…ç½®
  data:
    mongodb:
      uri: mongodb://114.132.189.42:9028/chat_memory_db_dlsmls-pro
    # redis é…ç½®
    redis:
      # åœ°å€
#      host: 127.0.0.1
      host: 172.17.0.1
      # ç«¯å£ï¼Œé»˜è®¤ä¸º6379
      port: 6379
      # æ•°æ®åº“索引
      database: 4
      # å¯†ç 
      #    password: root2022!
      password:
      # è¿žæŽ¥è¶…æ—¶æ—¶é—´
      timeout: 10s
      lettuce:
        pool:
          # è¿žæŽ¥æ± ä¸­çš„æœ€å°ç©ºé—²è¿žæŽ¥
          min-idle: 0
          # è¿žæŽ¥æ± ä¸­çš„æœ€å¤§ç©ºé—²è¿žæŽ¥
          max-idle: 8
          # è¿žæŽ¥æ± çš„æœ€å¤§æ•°æ®åº“连接数
          max-active: 8
          # #连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
  # Quartz定时任务配置(新增部分)
  quartz:
    job-store-type: jdbc  # ä½¿ç”¨æ•°æ®åº“存储
    jdbc:
      initialize-schema: never  # é¦–次运行时自动创建表结构,成功后改为never
      schema: classpath:org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql  # MySQL表结构脚本
    properties:
      org:
        quartz:
          scheduler:
            instanceName: RuoYiScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate  # MySQL适配
            tablePrefix: qrtz_  # è¡¨åå‰ç¼€ï¼Œä¸Žè„šæœ¬ä¸€è‡´
            isClustered: false  # å•节点模式(集群需改为true)
            clusterCheckinInterval: 10000
            txIsolationLevelSerializable: true
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10  # çº¿ç¨‹æ± å¤§å°
            threadPriority: 5
            makeThreadsDaemons: true
          updateCheck: false  # å…³é—­ç‰ˆæœ¬æ£€æŸ¥
# token配置
token:
  # ä»¤ç‰Œè‡ªå®šä¹‰æ ‡è¯†
  header: Authorization
  # ä»¤ç‰Œå¯†é’¥
  secret: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ
  # ä»¤ç‰Œæœ‰æ•ˆæœŸï¼ˆé»˜è®¤30分钟)
  expireTime: 450
# MyBatis Plus配置
mybatis-plus:
  # æœç´¢æŒ‡å®šåŒ…别名   æ ¹æ®è‡ªå·±çš„项目来
  typeAliasesPackage: com.ruoyi.**.pojo
  # é…ç½®mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # åŠ è½½å…¨å±€çš„é…ç½®æ–‡ä»¶
  configLocation: classpath:mybatis/mybatis-config.xml
  global-config:
    enable-sql-runner: true
    db-config:
      id-type: auto
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true
  params: count=countSql
# Swagger配置
swagger:
  # æ˜¯å¦å¼€å¯swagger
  enabled: true
  # è¯·æ±‚前缀
  pathMapping: /dev-api
# é˜²æ­¢XSS攻击
xss:
  # è¿‡æ»¤å¼€å…³
  enabled: true
  # æŽ’除链接(多个用逗号分隔)
  excludes: /system/notice
  # åŒ¹é…é“¾æŽ¥
  urlPatterns: /system/*,/monitor/*,/tool/*
# ä»£ç ç”Ÿæˆ
gen:
  # ä½œè€…
  author: ruoyi
  # é»˜è®¤ç”ŸæˆåŒ…路径 system éœ€æ”¹æˆè‡ªå·±çš„æ¨¡å—名称 å¦‚ system monitor tool
  packageName: com.ruoyi.project.system
  # è‡ªåŠ¨åŽ»é™¤è¡¨å‰ç¼€ï¼Œé»˜è®¤æ˜¯true
  autoRemovePre: false
  # è¡¨å‰ç¼€ï¼ˆç”Ÿæˆç±»åä¸ä¼šåŒ…含表前缀,多个用逗号分隔)
  tablePrefix: sys_
  # æ˜¯å¦å…è®¸ç”Ÿæˆæ–‡ä»¶è¦†ç›–到本地(自定义路径),默认不允许
  allowOverwrite: false
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: /javaWork/product-inventory-management/file/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: /javaWork/product-inventory-management/file/prod/uploads # æ­£å¼ç›®å½•
  path: /javaWork/product-inventory-management/file # ä¸Šä¼ ç›®å½•
  urlPrefix: /prod-api/common # é“¾æŽ¥å‰ç¼€
  domain: http://1.15.17.182:9074 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
src/main/resources/mapper/device/DeviceLedgerMapper.xml
@@ -31,9 +31,14 @@
        dl.tenant_id,
        dl.is_depr,
        dl.annual_depreciation_amount,
        dl.type
        dl.type,
        dl.area_id,
        dl.is_iot_device,
        dl.external_code,
        da.area_name AS areaName
        FROM device_ledger dl
        left join sys_user su on dl.create_user = su.user_id
        left join device_area da on dl.area_id = da.id
        <where>
            <!-- è®¾å¤‡åç§° -->
            <if test="deviceLedger.deviceName != null and deviceLedger.deviceName != ''">
@@ -76,6 +81,21 @@
            <if test="deviceLedger.tenantId != null">
                AND tenant_id = #{deviceLedger.tenantId}
            </if>
            <!-- è®¾å¤‡åŒºåŸŸ -->
            <if test="deviceLedger.areaId != null">
                AND dl.area_id = #{deviceLedger.areaId}
            </if>
            <!-- æ˜¯å¦ç‰©è”设备 -->
            <if test="deviceLedger.isIotDevice != null">
                AND dl.is_iot_device = #{deviceLedger.isIotDevice}
            </if>
            <!-- å¤–部编码 -->
            <if test="deviceLedger.externalCode != null and deviceLedger.externalCode != ''">
                AND dl.external_code LIKE CONCAT('%', #{deviceLedger.externalCode}, '%')
            </if>
        </where>
        ORDER BY create_time DESC
    </select>
src/main/resources/mapper/device/DeviceMaintenanceMapper.xml
@@ -8,6 +8,7 @@
    <select id="queryPage" resultType="com.ruoyi.device.dto.DeviceMaintenanceDto">
        select dm.id,
        dm.device_ledger_id,
        dm.area_id,
        dm.maintenance_plan_time,
        dm.maintenance_actually_time,
        dm.maintenance_result,
@@ -21,9 +22,11 @@
        dl.device_name,
        dm.machinery_category,
        dl.device_model,
        da.area_name,
        su.nick_name as create_user_name
        from device_maintenance dm
        left join device_ledger dl on dm.device_ledger_id = dl.id
        left join device_area da on dl.area_id = da.id
        left join sys_user su on dm.create_user = su.user_id
        <where>
            <if test="deviceMaintenanceDto.deviceName != null">
@@ -48,12 +51,16 @@
                and dm.maintenance_actually_time >= str_to_date(#{deviceMaintenanceDto.maintenanceActuallyTime}, '%Y-%m-%d')
                and dm.maintenance_actually_time &lt; date_add(str_to_date(#{deviceMaintenanceDto.maintenanceActuallyTime}, '%Y-%m-%d'), interval 1 day)
            </if>
            <if test="deviceMaintenanceDto.areaId != null">
                and dm.area_id = #{deviceMaintenanceDto.areaId}
            </if>
        </where>
        order by dm.create_time desc
    </select>
    <select id="detailById" resultType="com.ruoyi.device.vo.DeviceMaintenanceVo">
        select dm.id,
               dm.device_ledger_id,
               dm.area_id,
               dm.maintenance_plan_time,
               dm.maintenance_actually_time,
               dm.maintenance_result,
@@ -67,9 +74,11 @@
               dm.maintenance_actually_name,
               dl.device_name,
               dl.device_model,
               da.area_name,
               su.user_name as create_user_name
        from device_maintenance dm
                 left join device_ledger dl on dm.device_ledger_id = dl.id
                 left join device_area da on dl.area_id = da.id
                 left join sys_user su on dm.create_user = su.user_id
        where dm.id = #{id}
    </select>
src/main/resources/mapper/device/DeviceRepairMapper.xml
@@ -8,6 +8,7 @@
    <select id="queryPage" resultType="com.ruoyi.device.vo.DeviceRepairVo">
        select dr.id,
               dr.device_ledger_id,
               dr.area_id,
                dr.repair_time,
                dr.repair_name,
                dr.remark,
@@ -22,12 +23,14 @@
                dr.update_time,
                dr.create_user,
                dr.update_user,
               dr.tenant_id,
               dl.device_name,
               dl.device_model,
               dr.machinery_category
                dr.tenant_id,
                dl.device_name,
                dl.device_model,
               dr.machinery_category,
               da.area_name
        from device_repair dr
        left join device_ledger dl on dr.device_ledger_id = dl.id
        left join device_area da on dl.area_id = da.id
        <where>
            <if test="deviceRepairDto.deviceName != null">
                and dl.device_name like concat('%',#{deviceRepairDto.deviceName},'%')
@@ -51,12 +54,16 @@
            <if test="deviceRepairDto.maintenanceTimeStr != null and deviceRepairDto.maintenanceTimeStr != '' ">
                and dr.maintenance_time like concat('%',#{deviceRepairDto.maintenanceTimeStr},'%')
            </if>
            <if test="deviceRepairDto.areaId != null">
                and dr.area_id = #{deviceRepairDto.areaId}
            </if>
        </where>
        order by dr.create_time desc
    </select>
    <select id="detailById" resultType="com.ruoyi.device.vo.DeviceRepairVo">
        select dr.id,
               dr.device_ledger_id,
               dr.area_id,
               dr.repair_time,
               dr.repair_name,
               dr.remark,
@@ -74,9 +81,11 @@
               dr.tenant_id,
               dl.device_name,
               dr.machinery_category,
               dl.device_model
               dl.device_model,
               da.area_name
        from device_repair dr
                 left join device_ledger dl on dr.device_ledger_id = dl.id
                 left join device_area da on dl.area_id = da.id
        where dr.id = #{id}
    </select>
src/main/resources/mapper/inspectiontask/InspectionTaskMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.inspectiontask.mapper.InspectionTaskMapper">
    <select id="selectInspectionTaskAggregatePage" resultType="com.ruoyi.inspectiontask.pojo.InspectionTask">
        SELECT
            MIN(id) AS id,
            GROUP_CONCAT(DISTINCT task_name ORDER BY create_time SEPARATOR ',') AS task_name,
            MAX(inspection_project) AS inspection_project,
            MAX(task_id) AS task_id,
            CASE
                WHEN SUM(CASE WHEN inspection_status = 1 THEN 1 ELSE 0 END) > 0 THEN 1
                WHEN SUM(CASE WHEN inspection_status = 2 THEN 1 ELSE 0 END) > 0 THEN 2
                ELSE MAX(inspection_status)
            END AS inspection_status,
            MAX(inspector_id) AS inspector_id,
            MAX(inspector) AS inspector,
            MAX(remarks) AS remarks,
            MAX(inspection_result) AS inspection_result,
            MAX(abnormal_description) AS abnormal_description,
            MAX(device_repair_id) AS device_repair_id,
            MAX(acceptance_user_id) AS acceptance_user_id,
            MAX(acceptance_name) AS acceptance_name,
            MAX(registrant_id) AS registrant_id,
            MAX(registrant) AS registrant,
            MAX(frequency_type) AS frequency_type,
            MAX(frequency_detail) AS frequency_detail,
            MAX(inspection_location) AS inspection_location,
            MAX(deleted) AS deleted,
            MAX(create_user) AS create_user,
            MAX(create_time) AS create_time,
            MAX(update_user) AS update_user,
            MAX(update_time) AS update_time,
            MAX(tenant_id) AS tenant_id,
            MAX(dept_id) AS dept_id,
            MAX(area_id) AS area_id,
            MAX(timing_id) AS timing_id
        FROM inspection_task
        <where>
            deleted = 0
            <if test="dto != null and dto.taskName != null and dto.taskName != ''">
                AND task_name LIKE CONCAT('%', #{dto.taskName}, '%')
            </if>
            <if test="dto != null and dto.inspectionProject != null and dto.inspectionProject != ''">
                AND inspection_project LIKE CONCAT('%', #{dto.inspectionProject}, '%')
            </if>
            <if test="dto != null and dto.areaId != null">
                AND area_id = #{dto.areaId}
            </if>
            <if test="dto != null and dto.timingId != null">
                AND timing_id = #{dto.timingId}
            </if>
            <if test="dto != null and dto.createTimeStart != null">
                AND create_time <![CDATA[>=]]> #{dto.createTimeStart}
            </if>
            <if test="dto != null and dto.createTimeEnd != null">
                AND create_time <![CDATA[<]]> #{dto.createTimeEnd}
            </if>
        </where>
        GROUP BY IFNULL(timing_id, id)
        ORDER BY create_time DESC
    </select>
</mapper>