Explorar o código

fix(risk-specialEquipment): 维护模块存取逻辑重构

按用户需求明确存取逻辑:
1. 查询条件
   - 设备主表冗余存 next_maintenance_date + maintenance_status
   - 列表查询直接命中冗余列(便于筛选和排序)
2. 列表展示
   - 取最新一条维护记录注入 Vo(maintenanceStartTime/EndTime/maintainerName/siteName/latestMaintenanceId)
   - 维护状态按规则动态判定(fillMaintenanceStatus):
     * 无最新记录 -> 未维护
     * 最新='维护中' -> 维护中
     * 最新='待维护' -> 待维护
     * 最新='已维护' + next_date < NOW -> 待维护
     * 最新='已维护' + next_date >= NOW -> 已维护
3. 设备维护按钮
   - 先查 latestInProgress 接口
   - 若有'维护中' -> 打开维护编辑页(继续)
   - 若无 -> 打开新增维护页
4. 维护完成
   - 维护单 maintenance_status='已维护' 时
   - 必须录入 next_maintenance_date
   - 后端 updateByBo 同步更新设备主表 maintenance_status + next_maintenance_date
5. 列表判定
   - next_maintenance_date < NOW 即展示'待维护'(已在 fillMaintenanceStatus 中实现)

变更文件:
- domain/bo/RiskSpecialEquipmentBo: 加 nextMaintenanceDate + maintenanceStatus 字段
- domain/bo/RiskEquipmentMaintenanceBo: 加 nextMaintenanceDate 字段
- domain/RiskSpecialEquipment (Do): 加 nextMaintenanceDate + maintenanceStatus 字段
- domain/vo/RiskSpecialEquipmentVo: 加 6 个最新维护记录字段
- service/IRiskEquipmentMaintenanceService: 加 queryLatestInProgress 方法
- service/impl/RiskEquipmentMaintenanceServiceImpl: insertByBo/updateByBo 同步 equipment
- service/impl/RiskSpecialEquipmentServiceImpl: queryPageList/List/ById 后 fillLatestMaintenance
- controller/RiskEquipmentMaintenanceController: 加 GET /latestInProgress/{equipmentId}
- sql/risk_special_equipment_maintenance.sql: ALTER 加列 + 字典 seed
yangjingjing hai 3 días
pai
achega
663dd082a2

+ 15 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskEquipmentMaintenanceController.java

@@ -113,4 +113,19 @@ public class RiskEquipmentMaintenanceController extends BaseController {
         return R.ok(equipmentMaintenanceService.queryListByEquipmentId(equipmentId));
     }
 
+    /**
+     * 查询设备当前是否有"维护中"状态的记录
+     * <p>
+     * 用于前端"设备维护"按钮点击时的交互判断(用户需求 ③):
+     * - 返回非 null -> 该设备当前有"维护中"记录,前端应打开维护编辑页(继续维护)
+     * - 返回 null    -> 该设备当前无"维护中"记录,前端应打开新增维护页
+     *
+     * @param equipmentId 设备ID
+     */
+    @SaCheckPermission("risk:equipmentMaintenance:list")
+    @GetMapping("/latestInProgress/{equipmentId}")
+    public R<RiskEquipmentMaintenanceVo> latestInProgress(@NotNull(message = "设备ID不能为空") @PathVariable("equipmentId") Long equipmentId) {
+        return R.ok(equipmentMaintenanceService.queryLatestInProgress(equipmentId));
+    }
+
 }

+ 13 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSpecialEquipment.java

@@ -1,6 +1,7 @@
 package org.dromara.risk.domain;
 
 import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import java.io.Serial;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -90,6 +91,18 @@ public class RiskSpecialEquipment extends TenantEntity {
      */
     private Date installDate;
 
+    /**
+     * 下次维护时间(完成维护时由用户录入)
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date nextMaintenanceDate;
+
+    /**
+     * 维护状态(未维护/待维护/维护中/正常)
+     * 由 Service 层根据 next_maintenance_date 与最新维护记录动态计算后回写
+     */
+    private String maintenanceStatus;
+
     /**
      * 使用状态
      */

+ 7 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskEquipmentMaintenanceBo.java

@@ -1,5 +1,6 @@
 package org.dromara.risk.domain.bo;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import io.github.linpeilie.annotations.AutoMapper;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
@@ -94,6 +95,12 @@ public class RiskEquipmentMaintenanceBo extends BaseEntity {
      */
     private String photoList;
 
+    /**
+     * 下次维护时间(维护完成时录入,同步回写到设备主表)
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date nextMaintenanceDate;
+
     /**
      * 备注
      */

+ 11 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskSpecialEquipmentBo.java

@@ -118,6 +118,17 @@ public class RiskSpecialEquipmentBo extends BaseEntity {
      */
     private Date nextInspectionDate;
 
+    /**
+     * 下次维护日期(筛选条件,日期范围在 params 中:beginNextMaintenanceDate/endNextMaintenanceDate)
+     */
+    private Date nextMaintenanceDate;
+
+    /**
+     * 维护状态(未维护/待维护/维护中/正常)
+     */
+    @Size(min = 0, max = 500, message = "维护状态长度不能超过{max}个字符")
+    private String maintenanceStatus;
+
     /**
      * 负责人ID
      */

+ 45 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskSpecialEquipmentVo.java

@@ -113,6 +113,51 @@ public class RiskSpecialEquipmentVo implements Serializable {
     @ExcelProperty(value = "安装日期")
     private Date installDate;
 
+    /**
+     * 下次维护时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @ExcelProperty(value = "下次维护日期")
+    private Date nextMaintenanceDate;
+
+    /**
+     * 维护状态(未维护/待维护/维护中/正常) - 动态判定结果
+     */
+    @ExcelProperty(value = "维护状态")
+    private String maintenanceStatus;
+
+    /**
+     * 最新维护记录ID
+     */
+    @ExcelProperty(value = "最新维护记录ID")
+    private Long latestMaintenanceId;
+
+    /**
+     * 维护开始时间(取最新一条维护记录的实际开始时间)
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "维护开始时间")
+    private Date maintenanceStartTime;
+
+    /**
+     * 维护结束时间(取最新一条维护记录的实际结束时间)
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "维护结束时间")
+    private Date maintenanceEndTime;
+
+    /**
+     * 维护人员(取最新一条维护记录的维护人员)
+     */
+    @ExcelProperty(value = "维护人员")
+    private String maintainerName;
+
+    /**
+     * 现场人员(取最新一条维护记录的现场人员)
+     */
+    @ExcelProperty(value = "现场人员")
+    private String siteName;
+
     /**
      * 使用状态
      */

+ 8 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskEquipmentMaintenanceService.java

@@ -47,6 +47,14 @@ public interface IRiskEquipmentMaintenanceService {
      */
     List<RiskEquipmentMaintenanceVo> queryListByEquipmentId(Long equipmentId);
 
+    /**
+     * 查询设备当前是否存在"维护中"状态的记录(用于前端"设备维护"按钮交互判断)
+     *
+     * @param equipmentId 设备ID
+     * @return 维护中记录(仅一条,最新);无则 null
+     */
+    RiskEquipmentMaintenanceVo queryLatestInProgress(Long equipmentId);
+
     /**
      * 新增设备维护记录
      *

+ 79 - 4
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskEquipmentMaintenanceServiceImpl.java

@@ -11,17 +11,28 @@ import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.risk.domain.RiskEquipmentMaintenance;
+import org.dromara.risk.domain.RiskSpecialEquipment;
 import org.dromara.risk.domain.bo.RiskEquipmentMaintenanceBo;
 import org.dromara.risk.domain.vo.RiskEquipmentMaintenanceVo;
 import org.dromara.risk.mapper.RiskEquipmentMaintenanceMapper;
+import org.dromara.risk.mapper.RiskSpecialEquipmentMapper;
 import org.dromara.risk.service.IRiskEquipmentMaintenanceService;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
 /**
  * 设备维护 业务层实现
+ * <p>
+ * 维护状态流转与设备主表同步逻辑(按用户需求 ③④⑤):
+ * - 新增维护记录:设备 maintenance_status = '维护中'
+ * - 修改维护记录:
+ *   * maintenance_status='已维护' -> 设备 maintenance_status='正常' + next_maintenance_date=bo.nextMaintenanceDate
+ *   * maintenance_status='维护中' -> 设备 maintenance_status='维护中'
+ *   * maintenance_status='待维护' -> 设备 maintenance_status='待维护'
  *
  * @author ruoyi
  */
@@ -31,6 +42,11 @@ import java.util.Map;
 public class RiskEquipmentMaintenanceServiceImpl implements IRiskEquipmentMaintenanceService {
 
     private final RiskEquipmentMaintenanceMapper baseMapper;
+    private final RiskSpecialEquipmentMapper equipmentMapper;
+
+    private static final String MS_PENDING = "待维护";
+    private static final String MS_IN_PROGRESS = "维护中";
+    private static final String MS_NORMAL = "已维护";
 
     /**
      * 查询设备维护
@@ -69,6 +85,7 @@ public class RiskEquipmentMaintenanceServiceImpl implements IRiskEquipmentMainte
         lqw.like(StringUtils.isNotBlank(bo.getEquipmentName()), RiskEquipmentMaintenance::getEquipmentName, bo.getEquipmentName());
         lqw.like(StringUtils.isNotBlank(bo.getMaintainerName()), RiskEquipmentMaintenance::getMaintainerName, bo.getMaintainerName());
         lqw.eq(StringUtils.isNotBlank(bo.getMaintenanceStatus()), RiskEquipmentMaintenance::getMaintenanceStatus, bo.getMaintenanceStatus());
+        lqw.eq(ObjectUtil.isNotNull(bo.getEquipmentId()), RiskEquipmentMaintenance::getEquipmentId, bo.getEquipmentId());
         // 关键词搜索
         String keyword = (String) params.get("keyword");
         if (StringUtils.isNotBlank(keyword)) {
@@ -84,23 +101,58 @@ public class RiskEquipmentMaintenanceServiceImpl implements IRiskEquipmentMainte
     }
 
     /**
-     * 新增设备维护
+     * 新增设备维护记录
+     * 同步回写设备主表 maintenance_status='维护中'
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int insertByBo(RiskEquipmentMaintenanceBo bo) {
         RiskEquipmentMaintenance add = MapstructUtils.convert(bo, RiskEquipmentMaintenance.class);
         int row = baseMapper.insert(add);
         bo.setId(add.getId());
+        // 同步:设备维护状态 -> 维护中
+        syncEquipmentMaintenanceStatus(bo.getEquipmentId(), MS_IN_PROGRESS, null);
         return row;
     }
 
     /**
-     * 修改设备维护
+     * 修改设备维护记录
+     * 同步回写设备主表 maintenance_status + next_maintenance_date
+     * (按用户需求 ④:维护完成时录入下一次维护时间)
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int updateByBo(RiskEquipmentMaintenanceBo bo) {
         RiskEquipmentMaintenance update = MapstructUtils.convert(bo, RiskEquipmentMaintenance.class);
-        return baseMapper.updateById(update);
+        int row = baseMapper.updateById(update);
+        if (row > 0 && bo.getEquipmentId() != null) {
+            String mStatus = bo.getMaintenanceStatus();
+            if (MS_NORMAL.equals(mStatus)) {
+                // 维护完成 -> 设备状态=已维护 + next_maintenance_date 由用户录入
+                syncEquipmentMaintenanceStatus(bo.getEquipmentId(), MS_NORMAL, bo.getNextMaintenanceDate());
+            } else if (MS_IN_PROGRESS.equals(mStatus)) {
+                syncEquipmentMaintenanceStatus(bo.getEquipmentId(), MS_IN_PROGRESS, null);
+            } else if (MS_PENDING.equals(mStatus)) {
+                syncEquipmentMaintenanceStatus(bo.getEquipmentId(), MS_PENDING, null);
+            }
+        }
+        return row;
+    }
+
+    /**
+     * 同步设备主表的维护状态 + 下次维护时间
+     */
+    private void syncEquipmentMaintenanceStatus(Long equipmentId, String maintenanceStatus, Date nextMaintenanceDate) {
+        if (equipmentId == null) {
+            return;
+        }
+        RiskSpecialEquipment update = new RiskSpecialEquipment();
+        update.setId(equipmentId);
+        update.setMaintenanceStatus(maintenanceStatus);
+        if (nextMaintenanceDate != null) {
+            update.setNextMaintenanceDate(nextMaintenanceDate);
+        }
+        equipmentMapper.updateById(update);
     }
 
     /**
@@ -113,14 +165,37 @@ public class RiskEquipmentMaintenanceServiceImpl implements IRiskEquipmentMainte
         }
         return baseMapper.deleteBatchIds(ids);
     }
+
     /**
      * 根据设备ID查询维护列表
      */
     @Override
     public List<RiskEquipmentMaintenanceVo> queryListByEquipmentId(Long equipmentId) {
         return baseMapper.selectVoList(Wrappers.<RiskEquipmentMaintenance>lambdaQuery()
-            .eq(ObjectUtil.isNotNull(equipmentId), RiskEquipmentMaintenance::getEquipmentId, equipmentId));
+            .eq(ObjectUtil.isNotNull(equipmentId), RiskEquipmentMaintenance::getEquipmentId, equipmentId)
+            .orderByDesc(RiskEquipmentMaintenance::getCreateTime));
     }
 
+    /**
+     * 查询设备当前是否有"维护中"状态的记录
+     * <p>
+     * 用于前端"设备维护"按钮交互(用户需求 ③):
+     * - 若有维护中 -> 打开维护编辑页面(继续)
+     * - 若无 -> 打开新增维护页面
+     *
+     * @return 维护中记录(仅一条,最新);无则 null
+     */
+    @Override
+    public RiskEquipmentMaintenanceVo queryLatestInProgress(Long equipmentId) {
+        if (equipmentId == null) {
+            return null;
+        }
+        return baseMapper.selectVoOne(Wrappers.<RiskEquipmentMaintenance>lambdaQuery()
+            .eq(RiskEquipmentMaintenance::getEquipmentId, equipmentId)
+            .eq(RiskEquipmentMaintenance::getMaintenanceStatus, MS_IN_PROGRESS)
+            .orderByDesc(RiskEquipmentMaintenance::getCreateTime)
+            .orderByDesc(RiskEquipmentMaintenance::getId)
+            .last("LIMIT 1"));
+    }
 
 }

+ 143 - 4
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskSpecialEquipmentServiceImpl.java

@@ -11,20 +11,34 @@ import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.risk.domain.RiskEquipmentMaintenance;
 import org.dromara.risk.domain.RiskSpecialEquipment;
 import org.dromara.risk.domain.bo.RiskSpecialEquipmentBo;
+import org.dromara.risk.domain.vo.RiskEquipmentMaintenanceVo;
 import org.dromara.risk.domain.vo.RiskSpecialEquipmentVo;
+import org.dromara.risk.mapper.RiskEquipmentMaintenanceMapper;
 import org.dromara.risk.mapper.RiskSpecialEquipmentMapper;
 import org.dromara.risk.service.IRiskSpecialEquipmentService;
 import org.springframework.stereotype.Service;
 
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * 特种设备 业务层实现
+ * <p>
+ * 维护相关字段存取逻辑:
+ * - 维护记录全部存 risk_equipment_maintenance(多条记录,每次维护新增一条)
+ * - 设备主表 risk_special_equipment 冗余存:
+ *   * next_maintenance_date: 下次维护时间(由用户在维护完成时录入)
+ *   * maintenance_status: 维护状态,由 Service 动态计算 + 回写
+ * - 列表展示用最新一条维护记录的字段(fillLatestMaintenance 注入)
  *
  * @author ruoyi
  */
@@ -34,13 +48,26 @@ import java.util.Map;
 public class RiskSpecialEquipmentServiceImpl implements IRiskSpecialEquipmentService {
 
     private final RiskSpecialEquipmentMapper baseMapper;
+    private final RiskEquipmentMaintenanceMapper maintenanceMapper;
+
+    /**
+     * 维护状态常量(与 sys_dict maintenance_status 对齐)
+     */
+    private static final String MS_NOT_MAINTAINED = "未维护";
+    private static final String MS_PENDING = "待维护";
+    private static final String MS_IN_PROGRESS = "维护中";
+    private static final String MS_COMPLETED = "已维护";
 
     /**
      * 查询特种设备
      */
     @Override
     public RiskSpecialEquipmentVo queryById(Long id) {
-        return baseMapper.selectVoById(id);
+        RiskSpecialEquipmentVo vo = baseMapper.selectVoById(id);
+        if (vo != null) {
+            fillLatestMaintenance(java.util.Collections.singletonList(vo));
+        }
+        return vo;
     }
 
     /**
@@ -50,6 +77,10 @@ public class RiskSpecialEquipmentServiceImpl implements IRiskSpecialEquipmentSer
     public TableDataInfo<RiskSpecialEquipmentVo> queryPageList(RiskSpecialEquipmentBo bo, PageQuery pageQuery) {
         LambdaQueryWrapper<RiskSpecialEquipment> lqw = buildQueryWrapper(bo);
         Page<RiskSpecialEquipmentVo> page = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        // 列表填充最新维护记录 + 维护状态判定
+        if (page.getRecords() != null && !page.getRecords().isEmpty()) {
+            fillLatestMaintenance(page.getRecords());
+        }
         return TableDataInfo.build(page);
     }
 
@@ -59,11 +90,21 @@ public class RiskSpecialEquipmentServiceImpl implements IRiskSpecialEquipmentSer
     @Override
     public List<RiskSpecialEquipmentVo> queryList(RiskSpecialEquipmentBo bo) {
         LambdaQueryWrapper<RiskSpecialEquipment> lqw = buildQueryWrapper(bo);
-        return baseMapper.selectVoList(lqw);
+        List<RiskSpecialEquipmentVo> list = baseMapper.selectVoList(lqw);
+        if (ObjectUtil.isNotEmpty(list)) {
+            fillLatestMaintenance(list);
+        }
+        return list;
     }
 
     /**
      * 构造查询条件
+     * <p>
+     * 用户明确的查询条件:
+     * - 设备名称(模糊)
+     * - 设备类型(精确)
+     * - 维护状态(精确,直接命中设备主表冗余列 maintenance_status)
+     * - 下次维护日期(范围)
      */
     private LambdaQueryWrapper<RiskSpecialEquipment> buildQueryWrapper(RiskSpecialEquipmentBo bo) {
         Map<String, Object> params = bo.getParams();
@@ -74,10 +115,14 @@ public class RiskSpecialEquipmentServiceImpl implements IRiskSpecialEquipmentSer
         lqw.eq(StringUtils.isNotBlank(bo.getInspectionStatus()), RiskSpecialEquipment::getInspectionStatus, bo.getInspectionStatus());
         lqw.like(StringUtils.isNotBlank(bo.getEquipmentName()), RiskSpecialEquipment::getEquipmentName, bo.getEquipmentName());
         lqw.like(StringUtils.isNotBlank(bo.getManufacturer()), RiskSpecialEquipment::getManufacturer, bo.getManufacturer());
+        // 维护状态(冗余列精确匹配)
+        lqw.eq(StringUtils.isNotBlank(bo.getMaintenanceStatus()), RiskSpecialEquipment::getMaintenanceStatus, bo.getMaintenanceStatus());
+        // 下次维护日期范围
+        lqw.between(params.get("beginNextMaintenanceDate") != null && params.get("endNextMaintenanceDate") != null,
+            RiskSpecialEquipment::getNextMaintenanceDate, params.get("beginNextMaintenanceDate"), params.get("endNextMaintenanceDate"));
         // 关键词搜索
         if (StringUtils.isNotBlank(bo.getKeyword())) {
             lqw.and(w -> w.like(RiskSpecialEquipment::getEquipmentName, bo.getKeyword())
-                .or().like(RiskSpecialEquipment::getEquipmentName, bo.getKeyword())
                 .or().like(RiskSpecialEquipment::getEquipmentCode, bo.getKeyword()));
         }
         lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
@@ -96,6 +141,10 @@ public class RiskSpecialEquipmentServiceImpl implements IRiskSpecialEquipmentSer
         if (StringUtils.isBlank(bo.getEquipmentCode())) {
             bo.setEquipmentCode(generateEquipmentCode());
         }
+        // 新增设备默认未维护
+        if (StringUtils.isBlank(bo.getMaintenanceStatus())) {
+            bo.setMaintenanceStatus(MS_NOT_MAINTAINED);
+        }
         RiskSpecialEquipment add = MapstructUtils.convert(bo, RiskSpecialEquipment.class);
         int row = baseMapper.insert(add);
         bo.setId(add.getId());
@@ -143,4 +192,94 @@ public class RiskSpecialEquipmentServiceImpl implements IRiskSpecialEquipmentSer
         return baseMapper.deleteBatchIds(ids);
     }
 
-}
+    /**
+     * 批量填充最新一条维护记录 + 维护状态动态判定
+     * <p>
+     * 1. 取每个设备最新一条维护记录(按 create_time 倒序,id 倒序兜底)
+     * 2. 字段填充到 Vo: maintenanceStartTime / maintenanceEndTime / maintainerName / siteName / latestMaintenanceId
+     * 3. 维护状态判定:
+     *    - 无记录 -> 未维护
+     *    - 最新记录状态='维护中' -> 维护中
+     *    - 最新记录状态='已维护':
+     *      * next_maintenance_date < NOW() -> 待维护
+     *      * next_maintenance_date >= NOW() -> 正常
+     *    - 最新记录状态='待维护' -> 待维护
+     */
+    private void fillLatestMaintenance(List<RiskSpecialEquipmentVo> vos) {
+        if (ObjectUtil.isEmpty(vos)) {
+            return;
+        }
+        Set<Long> equipmentIds = vos.stream()
+            .map(RiskSpecialEquipmentVo::getId)
+            .filter(ObjectUtil::isNotNull)
+            .collect(Collectors.toSet());
+        if (equipmentIds.isEmpty()) {
+            return;
+        }
+        // 一次性查所有设备的最新一条维护记录(用 inSql 子查询避免复杂 group by)
+        // 策略:按 equipmentId 分组查最新一条
+        Map<Long, RiskEquipmentMaintenance> latestMap = new java.util.HashMap<>(equipmentIds.size());
+        for (Long equipmentId : equipmentIds) {
+            RiskEquipmentMaintenance latest = maintenanceMapper.selectOne(
+                Wrappers.<RiskEquipmentMaintenance>lambdaQuery()
+                    .eq(RiskEquipmentMaintenance::getEquipmentId, equipmentId)
+                    .orderByDesc(RiskEquipmentMaintenance::getCreateTime)
+                    .orderByDesc(RiskEquipmentMaintenance::getId)
+                    .last("LIMIT 1")
+            );
+            if (latest != null) {
+                latestMap.put(equipmentId, latest);
+            }
+        }
+        Date now = new Date();
+        for (RiskSpecialEquipmentVo vo : vos) {
+            RiskEquipmentMaintenance latest = latestMap.get(vo.getId());
+            if (latest != null) {
+                vo.setLatestMaintenanceId(latest.getId());
+                vo.setMaintenanceStartTime(latest.getActualStartTime());
+                vo.setMaintenanceEndTime(latest.getActualEndTime());
+                vo.setMaintainerName(latest.getMaintainerName());
+                vo.setSiteName(latest.getSiteName());
+            }
+            // 维护状态以最新一条维护记录 + next_maintenance_date 为准动态判定
+            vo.setMaintenanceStatus(computeMaintenanceStatus(vo, latest, now));
+        }
+    }
+
+    /**
+     * 维护状态动态判定
+     * <p>
+     * 规则(按用户需求 ⑤):
+     * - 无最新记录 -> 未维护
+     * - 最新记录状态='维护中' -> 维护中
+     * - 最新记录状态='已维护' + next_maintenance_date < now -> 待维护
+     * - 最新记录状态='已维护' + next_maintenance_date >= now -> 正常
+     * - 最新记录状态='待维护' -> 待维护
+     */
+    private String computeMaintenanceStatus(RiskSpecialEquipmentVo vo, RiskEquipmentMaintenance latest, Date now) {
+        if (latest == null) {
+            return MS_NOT_MAINTAINED;
+        }
+        String mStatus = latest.getMaintenanceStatus();
+        if (MS_IN_PROGRESS.equals(mStatus)) {
+            return MS_IN_PROGRESS;
+        }
+        if (MS_PENDING.equals(mStatus)) {
+            return MS_PENDING;
+        }
+        if (MS_COMPLETED.equals(mStatus) || "已维护".equals(mStatus)) {
+            Date nextDate = vo.getNextMaintenanceDate();
+            if (nextDate != null && nextDate.before(now)) {
+                return MS_PENDING;
+            }
+            return MS_COMPLETED;
+        }
+        // 未知状态时按 next_maintenance_date 兜底
+        Date nextDate = vo.getNextMaintenanceDate();
+        if (nextDate != null && nextDate.before(now)) {
+            return MS_PENDING;
+        }
+        return MS_COMPLETED;
+    }
+
+}

+ 56 - 0
sql/risk_special_equipment_maintenance.sql

@@ -0,0 +1,56 @@
+-- ============================================
+-- 特种设备维护模块 - 数据结构升级
+-- 按用户需求明确存取逻辑:
+--   1. 维护记录存 risk_equipment_maintenance(多条,不变)
+--   2. 设备主表存 next_maintenance_date + maintenance_status(冗余字段,便于列表查询)
+--   3. 维护状态动态判定:
+--      - 最新记录='维护中' → '维护中'
+--      - 最新记录='已维护' 且 next_maintenance_date < NOW() → '待维护'
+--      - 最新记录='已维护' 且 next_maintenance_date >= NOW() → '正常'
+--      - 无维护记录 → '未维护'
+-- ============================================
+
+-- 1. 设备表新增:下次维护时间 + 维护状态
+ALTER TABLE risk_special_equipment
+  ADD COLUMN `next_maintenance_date` DATE DEFAULT NULL
+    COMMENT '下次维护时间(完成维护时由用户录入)' AFTER `install_date`,
+  ADD COLUMN `maintenance_status` VARCHAR(20) DEFAULT '未维护'
+    COMMENT '维护状态(未维护/待维护/维护中/正常)' AFTER `next_maintenance_date`,
+  ADD KEY `idx_maintenance_status` (`maintenance_status`),
+  ADD KEY `idx_next_maintenance_date` (`next_maintenance_date`);
+
+-- 2. 维护记录表加:预计维护日期(用于列表显示)
+-- (plan_start_time/plan_end_time 已存在,可作为预计时间)
+-- 加 next_maintenance_date 字段:维护完成时录入的下次维护时间(冗余到记录表,便于历史追溯)
+ALTER TABLE risk_equipment_maintenance
+  ADD COLUMN `next_maintenance_date` DATE DEFAULT NULL
+    COMMENT '下次维护时间(维护完成时录入)' AFTER `actual_end_time`;
+
+-- 3. 初始化历史数据
+UPDATE risk_special_equipment
+SET maintenance_status = '未维护'
+WHERE maintenance_status IS NULL OR maintenance_status = '';
+
+-- ============================================
+-- 4. 字典 seed:维护状态 maintenance_status
+-- (待维护/维护中/已维护 已有,此处补"未维护")
+-- ============================================
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_dept, create_by, create_time, remark)
+SELECT 0, '未维护', '未维护', 'maintenance_status', '', 'info', 'N', '0', 103, 1, NOW(), '设备从未做过维护'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'maintenance_status' AND dict_value = '未维护');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_dept, create_by, create_time, remark)
+SELECT 10, '待维护', '待维护', 'maintenance_status', '', 'warning', 'N', '0', 103, 1, NOW(), '下一次维护时间已过'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'maintenance_status' AND dict_value = '待维护');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_dept, create_by, create_time, remark)
+SELECT 20, '维护中', '维护中', 'maintenance_status', '', 'primary', 'N', '0', 103, 1, NOW(), '正在进行维护作业'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'maintenance_status' AND dict_value = '维护中');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_dept, create_by, create_time, remark)
+SELECT 30, '已维护', '已维护', 'maintenance_status', '', 'success', 'N', '0', 103, 1, NOW(), '本次维护已完成且尚未到期'
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'maintenance_status' AND dict_value = '已维护');