Explorar o código

feat(check): 新增批量下载保养记录PDF功能

- 实现批量下载PDF接口,支持按时间段、设备、部门等条件筛选
- 添加下载条数统计接口,供前端预览下载数量
- 优化保养记录PDF模板,增加执行人和负责人签名区域
- 支持部门筛选功能,包含部门及其所有下级部门的任务
- 实现PDF文件流式打包ZIP返回,避免内存溢出
- 修复定时任务逻辑,调整班组提醒查询条件
- 优化Excel导入功能,增强数据验证和错误提示
- 新增设备健康状态字段,完善设备信息管理
- 添加报修单班长通知功能,支持邮件和站内信推送
dazhaxie hai 2 semanas
pai
achega
9b95d8c5c4
Modificáronse 25 ficheiros con 685 adicións e 162 borrados
  1. 5 0
      platform-dao/src/main/java/com/platform/dao/dto/check/CheckJobDTO.java
  2. 5 0
      platform-dao/src/main/java/com/platform/dao/dto/check/CheckStandardDTO.java
  3. 5 0
      platform-dao/src/main/java/com/platform/dao/dto/repair/RepairApplicationFormDTO.java
  4. 4 0
      platform-dao/src/main/java/com/platform/dao/dto/sb/SbInfoDTO.java
  5. 5 0
      platform-dao/src/main/java/com/platform/dao/entity/repair/RepairApplicationForm.java
  6. 4 0
      platform-dao/src/main/java/com/platform/dao/entity/sb/SbInfo.java
  7. 8 0
      platform-dao/src/main/java/com/platform/dao/mapper/check/CheckJobMapper.java
  8. 64 8
      platform-dao/src/main/java/com/platform/dao/util/CustomExcelImportUtil.java
  9. 5 0
      platform-dao/src/main/java/com/platform/dao/vo/export/sb/ExportSbInfoVO.java
  10. 4 0
      platform-dao/src/main/java/com/platform/dao/vo/query/check/CheckStandardVO.java
  11. 5 0
      platform-dao/src/main/java/com/platform/dao/vo/repair/RepairApplicationFormVO.java
  12. 8 0
      platform-dao/src/main/java/com/platform/dao/vo/sb/SbInfoVO.java
  13. 30 1
      platform-dao/src/main/resources/mapper/check/CheckJobMapper.xml
  14. 7 1
      platform-dao/src/main/resources/mapper/check/CheckStandardMapper.xml
  15. 2 2
      platform-dao/src/main/resources/mapper/upms/SysUserMapper.xml
  16. 36 0
      platform-rest/src/main/java/com/platform/rest/controller/check/CheckJobController.java
  17. 10 0
      platform-rest/src/main/java/com/platform/rest/controller/upms/SysUserController.java
  18. BIN=BIN
      platform-rest/src/main/resources/templates/设备导入模板.xlsx
  19. 18 0
      platform-service/src/main/java/com/platform/service/check/CheckJobService.java
  20. 225 64
      platform-service/src/main/java/com/platform/service/check/impl/CheckJobServiceImpl.java
  21. 64 0
      platform-service/src/main/java/com/platform/service/repair/impl/RepairApplicationFormServiceImpl.java
  22. 97 59
      platform-service/src/main/java/com/platform/service/sb/impl/SbInfoServiceImpl.java
  23. 8 0
      platform-service/src/main/java/com/platform/service/upms/SysUserService.java
  24. 61 0
      platform-service/src/main/java/com/platform/service/upms/impl/SysUserServiceImpl.java
  25. 5 27
      platform-service/src/main/resources/templates/maintenance_record.html

+ 5 - 0
platform-dao/src/main/java/com/platform/dao/dto/check/CheckJobDTO.java

@@ -126,6 +126,11 @@ public class CheckJobDTO extends BaseDTO implements Serializable {
      */
     private List<String> checkDeptIds;
     /**
+     * 批量下载PDF按部门筛选:命中该部门及其所有下级部门的班组任务(check_dept_id),
+     * 或执行人所属部门在该范围内的非班组任务(check_user_id);为空表示不限部门
+     */
+    private String batchDeptId;
+    /**
      * 设备等级
      */
     private Integer sbLevel;

+ 5 - 0
platform-dao/src/main/java/com/platform/dao/dto/check/CheckStandardDTO.java

@@ -114,6 +114,11 @@ public class CheckStandardDTO extends BaseDTO implements Serializable {
      */
     private String checkDeptId;
     /**
+     * 执行人/班组名称(列表模糊查询:匹配执行人姓名或班组名称)
+     */
+    @Transient
+    private String checkUserName;
+    /**
      * 上次生成时间
      */
     @Transient

+ 5 - 0
platform-dao/src/main/java/com/platform/dao/dto/repair/RepairApplicationFormDTO.java

@@ -353,4 +353,9 @@ public class RepairApplicationFormDTO extends BaseDTO implements Serializable {
      * 报修部门ID(筛选条件/保存报修部门)
      */
     private String deptId;
+
+    /**
+     * 通知班长用户id,多个用逗号分隔(非必填,提交报修时给选中班长发邮件+站内通知)
+     */
+    private String notifyTeamLeaders;
 }

+ 4 - 0
platform-dao/src/main/java/com/platform/dao/dto/sb/SbInfoDTO.java

@@ -198,6 +198,10 @@ public class SbInfoDTO extends BaseDTO implements Serializable {
      * 所属车间
      */
     private String positionId;
+    /**
+     * 设备健康状态(用户自定义设备状态):0离线停用 1危急故障 2差 3一般 4良好 5优秀
+     */
+    private Integer healthStatus;
 
     /**
      * 规格型号

+ 5 - 0
platform-dao/src/main/java/com/platform/dao/entity/repair/RepairApplicationForm.java

@@ -288,4 +288,9 @@ public class RepairApplicationForm implements Serializable {
      */
     private String deptId;
 
+    /**
+     * 通知班长用户id,多个用逗号分隔
+     */
+    private String notifyTeamLeaders;
+
 }

+ 4 - 0
platform-dao/src/main/java/com/platform/dao/entity/sb/SbInfo.java

@@ -201,6 +201,10 @@ public class SbInfo implements Serializable {
      */
     private Integer isFinancing;
     /**
+     * 设备健康状态(用户自定义设备状态):0离线停用 1危急故障 2差 3一般 4良好 5优秀
+     */
+    private Integer healthStatus;
+    /**
      * 计量单位
      */
     private String unit;

+ 8 - 0
platform-dao/src/main/java/com/platform/dao/mapper/check/CheckJobMapper.java

@@ -51,6 +51,14 @@ public interface CheckJobMapper extends MyMapper<CheckJob> {
 
     int getNotFinishNumByDTO(CheckJobDTO record);
 
+    /**
+     * 按查询条件统计保养任务数量(复用 List_Condition,与 selectList 口径一致),
+     * 用于批量下载PDF前的条数预览。
+     * @param dto 查询条件
+     * @return 命中条数
+     */
+    long countByCondition(CheckJobDTO dto);
+
     void updateBatch(List<CheckJob> updateList);
 
     void updateBatchByDelStandard(CheckJob checkJob);

+ 64 - 8
platform-dao/src/main/java/com/platform/dao/util/CustomExcelImportUtil.java

@@ -980,7 +980,11 @@ public class CustomExcelImportUtil {
                 if (StringUtils.isBlank(value)) {
                     record.setInitialValue(0.0);
                 } else {
-                    record.setInitialValue(new Double(value));
+                    try {
+                        record.setInitialValue(new Double(value));
+                    } catch (NumberFormatException nfe) {
+                        throw new BusinessException("【设备原值】的值【" + value + "】不是合法数字,请填写纯数字或留空");
+                    }
                 }
                 record.setTypeName(getCellValue(row.getCell(7)).trim());
                 record.setUseType(getUseType(getCellValue(row.getCell(8)).trim()));
@@ -995,7 +999,12 @@ public class CustomExcelImportUtil {
                 if (StringUtils.isBlank(buyDateStr)) {
                     record.setBuyDate(DateUtils.strToLocalDate("2014-01-01", DateUtils.PATTERN_YMD));
                 } else {
-                    LocalDate buyDate = parseImportDate(buyDateStr);
+                    LocalDate buyDate;
+                    try {
+                        buyDate = parseImportDate(buyDateStr);
+                    } catch (Exception de) {
+                        throw new BusinessException("【购置日期】的值【" + buyDateStr + "】格式不正确,应形如 2020-01-01");
+                    }
                     record.setBuyDate(
                             buyDate != null ? buyDate : DateUtils.strToLocalDate("2014-01-01", DateUtils.PATTERN_YMD));
                 }
@@ -1003,7 +1012,12 @@ public class CustomExcelImportUtil {
                 if (StringUtils.isBlank(startDateStr)) {
                     record.setStartDate(DateUtils.strToLocalDate("2019-01-01", DateUtils.PATTERN_YMD));
                 } else {
-                    LocalDate startDate = parseImportDate(startDateStr);
+                    LocalDate startDate;
+                    try {
+                        startDate = parseImportDate(startDateStr);
+                    } catch (Exception de) {
+                        throw new BusinessException("【投用日期】的值【" + startDateStr + "】格式不正确,应形如 2020-01-01");
+                    }
                     record.setStartDate(startDate != null ? startDate
                             : DateUtils.strToLocalDate("2019-01-01", DateUtils.PATTERN_YMD));
                 }
@@ -1016,10 +1030,24 @@ public class CustomExcelImportUtil {
                 if (StringUtils.isNotBlank(posName)) {
                     record.setPositionName(posName.trim());
                 }
+                // 设备健康状态(0-5):第24列,可空;填了必须是0-5的整数
+                String healthStr = getCellValue(row.getCell(23));
+                if (StringUtils.isNotBlank(healthStr)) {
+                    try {
+                        int healthVal = Integer.parseInt(healthStr.trim());
+                        if (healthVal < 0 || healthVal > 5) {
+                            throw new BusinessException("【设备健康状态】的值【" + healthStr + "】不在0-5范围内");
+                        }
+                        record.setHealthStatus(healthVal);
+                    } catch (NumberFormatException nfe) {
+                        throw new BusinessException("【设备健康状态】的值【" + healthStr + "】不是0-5的整数");
+                    }
+                }
 
             } catch (Exception e) {
                 log.error("导入第" + i + "行异常", e);
-                error.append(index).append(",");
+                error.append("第").append(i + 1).append("行[设备编号:").append(index).append("] ")
+                        .append(e.getMessage()).append(";");
             }
             result.add(record);
         }
@@ -1720,7 +1748,11 @@ public class CustomExcelImportUtil {
                 if (StringUtils.isBlank(value)) {
                     record.setInitialValue(0.0);
                 } else {
-                    record.setInitialValue(new Double(value));
+                    try {
+                        record.setInitialValue(new Double(value));
+                    } catch (NumberFormatException nfe) {
+                        throw new BusinessException("【设备原值】的值【" + value + "】不是合法数字,请填写纯数字或留空");
+                    }
                 }
                 record.setTypeName(getCellValue(row.getCell(7)).trim());
                 record.setUseType(getUseType(getCellValue(row.getCell(8)).trim()));
@@ -1735,7 +1767,12 @@ public class CustomExcelImportUtil {
                 if (StringUtils.isBlank(buyDateStr)) {
                     record.setBuyDate(DateUtils.strToLocalDate("2014-01-01", DateUtils.PATTERN_YMD));
                 } else {
-                    LocalDate buyDate = parseImportDate(buyDateStr);
+                    LocalDate buyDate;
+                    try {
+                        buyDate = parseImportDate(buyDateStr);
+                    } catch (Exception de) {
+                        throw new BusinessException("【购置日期】的值【" + buyDateStr + "】格式不正确,应形如 2020-01-01");
+                    }
                     record.setBuyDate(
                             buyDate != null ? buyDate : DateUtils.strToLocalDate("2014-01-01", DateUtils.PATTERN_YMD));
                 }
@@ -1743,7 +1780,12 @@ public class CustomExcelImportUtil {
                 if (StringUtils.isBlank(startDateStr)) {
                     record.setStartDate(DateUtils.strToLocalDate("2019-01-01", DateUtils.PATTERN_YMD));
                 } else {
-                    LocalDate startDate = parseImportDate(startDateStr);
+                    LocalDate startDate;
+                    try {
+                        startDate = parseImportDate(startDateStr);
+                    } catch (Exception de) {
+                        throw new BusinessException("【投用日期】的值【" + startDateStr + "】格式不正确,应形如 2020-01-01");
+                    }
                     record.setStartDate(startDate != null ? startDate
                             : DateUtils.strToLocalDate("2019-01-01", DateUtils.PATTERN_YMD));
                 }
@@ -1756,9 +1798,23 @@ public class CustomExcelImportUtil {
                 if (StringUtils.isNotBlank(posName)) {
                     record.setPositionName(posName.trim());
                 }
+                // 设备健康状态(0-5):第24列,可空;填了必须是0-5的整数
+                String healthStr = getCellValue(row.getCell(23));
+                if (StringUtils.isNotBlank(healthStr)) {
+                    try {
+                        int healthVal = Integer.parseInt(healthStr.trim());
+                        if (healthVal < 0 || healthVal > 5) {
+                            throw new BusinessException("【设备健康状态】的值【" + healthStr + "】不在0-5范围内");
+                        }
+                        record.setHealthStatus(healthVal);
+                    } catch (NumberFormatException nfe) {
+                        throw new BusinessException("【设备健康状态】的值【" + healthStr + "】不是0-5的整数");
+                    }
+                }
             } catch (Exception e) {
                 log.error("导入第" + i + "行异常", e);
-                error.append(index).append(",");
+                error.append("第").append(i + 1).append("行[设备编号:").append(index).append("] ")
+                        .append(e.getMessage()).append(";");
             }
             result.add(record);
         }

+ 5 - 0
platform-dao/src/main/java/com/platform/dao/vo/export/sb/ExportSbInfoVO.java

@@ -166,5 +166,10 @@ public class ExportSbInfoVO implements Serializable {
      */
     @Excel(name = "自定义参数", orderNum = "29")
     private String paramList;
+    /**
+     * 设备健康状态(0-5)
+     */
+    @Excel(name = "设备健康状态", orderNum = "30", dicCode = "SB_HEALTH_STATUS")
+    private Integer healthStatus;
 
 }

+ 4 - 0
platform-dao/src/main/java/com/platform/dao/vo/query/check/CheckStandardVO.java

@@ -106,6 +106,10 @@ public class CheckStandardVO extends BaseVO implements Serializable{
      */
     private String checkDeptName;
     /**
+     * 执行人姓名(执行人方式为具体人时)
+     */
+    private String checkUserName;
+    /**
      * 检查类型: 1-点检 2-巡检
      */
     private Integer type;

+ 5 - 0
platform-dao/src/main/java/com/platform/dao/vo/repair/RepairApplicationFormVO.java

@@ -381,4 +381,9 @@ public class RepairApplicationFormVO extends BaseVO implements Serializable {
      * 报修部门名称
      */
     private String deptName;
+
+    /**
+     * 通知班长用户id,多个用逗号分隔
+     */
+    private String notifyTeamLeaders;
 }

+ 8 - 0
platform-dao/src/main/java/com/platform/dao/vo/sb/SbInfoVO.java

@@ -59,6 +59,10 @@ public class SbInfoVO extends BaseVO implements Serializable {
      */
     private Integer isFinancing;
     /**
+     * 设备健康状态(用户自定义设备状态):0离线停用 1危急故障 2差 3一般 4良好 5优秀
+     */
+    private Integer healthStatus;
+    /**
     * 卡片编号
      */
     private String cardNo;
@@ -412,6 +416,10 @@ public class SbInfoVO extends BaseVO implements Serializable {
     private Integer repaireTimes;//维修次数
 
     private LocalDate lastBaoyangTime;//最近保养时间
+    /**
+     * 上次保养完成时间(该设备最近一次已完成保养任务的实际结束时间)
+     */
+    private LocalDateTime lastMaintainFinishTime;
     private double lastBaoyangMiles;//最近保养里程
     private double lastBaoyangHours;//最近保养台时
     private Integer baoyangTimes;//保养次数

+ 30 - 1
platform-dao/src/main/resources/mapper/check/CheckJobMapper.xml

@@ -258,6 +258,19 @@
         <if test="checkDeptId != null and checkDeptId != ''">
             and checkjob.check_dept_id = #{checkDeptId}
         </if>
+        <if test="batchDeptId != null and batchDeptId != ''">
+            and (
+                EXISTS (
+                    SELECT 1 FROM t_sys_dept_relation dr_bd
+                    WHERE dr_bd.ancestor = #{batchDeptId} AND dr_bd.descendant = checkjob.check_dept_id
+                )
+                OR EXISTS (
+                    SELECT 1 FROM t_sys_user_dept sud_bd
+                    INNER JOIN t_sys_dept_relation dr_bd2 ON dr_bd2.descendant = sud_bd.dept_id
+                    WHERE dr_bd2.ancestor = #{batchDeptId} AND sud_bd.user_id = checkjob.check_user_id
+                )
+            )
+        </if>
         <if test="type != null and type != ''">
             and checkjob.type = #{type}
         </if>
@@ -461,6 +474,19 @@
         <if test="checkDeptId != null and checkDeptId != ''">
             and checkjob_inner.check_dept_id = #{checkDeptId}
         </if>
+        <if test="batchDeptId != null and batchDeptId != ''">
+            and (
+                EXISTS (
+                    SELECT 1 FROM t_sys_dept_relation dr_bd
+                    WHERE dr_bd.ancestor = #{batchDeptId} AND dr_bd.descendant = checkjob_inner.check_dept_id
+                )
+                OR EXISTS (
+                    SELECT 1 FROM t_sys_user_dept sud_bd
+                    INNER JOIN t_sys_dept_relation dr_bd2 ON dr_bd2.descendant = sud_bd.dept_id
+                    WHERE dr_bd2.ancestor = #{batchDeptId} AND sud_bd.user_id = checkjob_inner.check_user_id
+                )
+            )
+        </if>
         <if test="type != null and type != ''">
             and checkjob_inner.type = #{type}
         </if>
@@ -1099,7 +1125,9 @@
         </foreach>
     </select>
 
-    <!-- 查询执行方式为班组且未完成的保养任务,用于每日定时提醒班长 -->
+    <!-- 查询执行方式为班组、未完成(status != 3)且开始日期不晚于今天(start_time <= CURDATE())的保养任务,用于每日早上9点定时提醒班长:
+         既包含已逾期任务,也包含今天到期但发邮件时仍未完成的任务;未来任务(start_time > 今天)不提醒。
+         注意:start_time 为日期级(LocalDate),用 <= CURDATE() 可覆盖“今天”一整天,勿改成 < CURDATE() 否则会漏掉今天到期的任务 -->
     <select id="selectUnfinishedTeamJobs" resultType="com.platform.dao.vo.query.check.CheckJobVO">
         select
         checkjob.id as id,
@@ -1121,6 +1149,7 @@
         and standard.check_user_type = 4
         and checkjob.check_dept_id is not null
         and checkjob.status != 3
+        and checkjob.start_time &lt;= CURDATE()
         order by checkjob.check_dept_id asc, sbinfo.no asc
     </select>
 

+ 7 - 1
platform-dao/src/main/resources/mapper/check/CheckStandardMapper.xml

@@ -235,11 +235,13 @@ checkstandard.last_date,
         sbInfo.level as sbLevel,
         sbInfo.repair_user as repairUser,
         partInfo.name as partName,
-        dept.name as checkDeptName
+        dept.name as checkDeptName,
+        checkUser.real_name as checkUserName
         from t_check_standard as checkstandard
         LEFT JOIN t_sb_info sbInfo on checkstandard.sb_id = sbInfo.id
         LEFT JOIN t_part_info partInfo on checkstandard.part = partInfo.id
         LEFT JOIN t_sys_dept dept on checkstandard.check_dept_id = dept.dept_id
+        LEFT JOIN t_sys_user checkUser on checkstandard.check_user_id = checkUser.user_id
         <where>
             <include refid="List_Condition"/>
             <if test="sbName != null and sbName != ''">
@@ -248,6 +250,10 @@ checkstandard.last_date,
             <if test="sbNo != null and sbNo != ''">
                 and sbInfo.no like concat('%',#{sbNo},'%')
             </if>
+            <if test="checkUserName != null and checkUserName != ''">
+                and (checkUser.real_name like concat('%',#{checkUserName},'%')
+                or dept.name like concat('%',#{checkUserName},'%'))
+            </if>
             <if test="typeId != null and typeId != ''">
                 and sbInfo.type_id  = #{typeId}
             </if>

+ 2 - 2
platform-dao/src/main/resources/mapper/upms/SysUserMapper.xml

@@ -29,8 +29,8 @@
         <if test="teamId !=null and teamId !=''">
             and exists (select 1 from t_sys_user_team t where t.user_id = u.user_id and t.team_id = #{teamId})
         </if>
-        <if test="identityType !=null">
-            and u.identity_type = #{identityType}
+        <if test="roleId !=null and roleId !=''">
+            and exists (select 1 from t_sys_user_role ur where ur.user_id = u.user_id and ur.role_id = #{roleId})
         </if>
         order by u.update_time desc
     </select>

+ 36 - 0
platform-rest/src/main/java/com/platform/rest/controller/check/CheckJobController.java

@@ -347,6 +347,42 @@ public class CheckJobController {
     }
 
     /**
+     * 批量下载保养记录 PDF(打包 zip)
+     * 支持按完成时间段(actualEndTimeStart/actualEndTimeEnd)、设备(sbId)、
+     * 设备类别(typeId)、部门/班组(checkDeptId)等条件筛选,默认只下载已完成任务。
+     *
+     * @param response     HttpServletResponse
+     * @param checkJobDTO  查询条件
+     */
+    @GetMapping("/batch-download-pdf")
+    @SysLog("批量下载保养记录PDF")
+    public void batchDownloadPdf(HttpServletResponse response, CheckJobDTO checkJobDTO) {
+        try {
+            checkJobService.batchDownloadPdf(checkJobDTO, response);
+        } catch (IOException e) {
+            log.error("批量下载保养记录PDF失败", e);
+            try {
+                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                response.getWriter().write("下载失败");
+            } catch (IOException ex) {
+                log.error("写入错误响应失败", ex);
+            }
+        }
+    }
+
+    /**
+     * 统计批量下载范围内的已完成保养记录数量(与 /batch-download-pdf 口径一致),
+     * 供前端弹窗展示“将下载 N 条”预览。
+     *
+     * @param checkJobDTO 查询条件(时间段 searchStartTime/End、status、batchDeptId、type 等)
+     * @return R<Long> 命中条数
+     */
+    @GetMapping("/count-download-pdf")
+    public R<Long> countDownloadPdf(CheckJobDTO checkJobDTO) {
+        return new R<>(checkJobService.countBatchDownloadPdf(checkJobDTO));
+    }
+
+    /**
      * 年度保养计划表(页面查看)
      * 按设备名称/类别聚合,当年每月做过保养则打勾√,按天的保养不计入
      *

+ 10 - 0
platform-rest/src/main/java/com/platform/rest/controller/upms/SysUserController.java

@@ -258,6 +258,16 @@ public class SysUserController {
     }
 
     /**
+     * 报修通知班长候选人:全部班长身份用户 + 当前用户所在班组下的班长(默认勾选)
+     *
+     * @return R
+     */
+    @GetMapping("/team-leaders")
+    public R queryTeamLeaders() {
+        return new R<>(sysUserService.listTeamLeaders());
+    }
+
+    /**
      * 项目角色的人
      *
      * @param sysUserDTO :

BIN=BIN
platform-rest/src/main/resources/templates/设备导入模板.xlsx


+ 18 - 0
platform-service/src/main/java/com/platform/service/check/CheckJobService.java

@@ -198,6 +198,24 @@ public interface CheckJobService extends IBaseService<CheckJob, CheckJobDTO> {
     void downloadPdf(String id, HttpServletResponse response) throws IOException;
 
     /**
+     * 批量下载保养记录 PDF:按时间段/设备/部门等条件查询已完成任务,
+     * 将磁盘上已生成的 PDF 流式打包为 zip 返回。
+     *
+     * @param dto      查询条件(actualEndTimeStart/End、sbId、typeId、checkDeptId 等)
+     * @param response HttpServletResponse
+     */
+    void batchDownloadPdf(CheckJobDTO dto, HttpServletResponse response) throws IOException;
+
+    /**
+     * 统计批量下载范围内的已完成保养任务数量(与 batchDownloadPdf 口径一致),
+     * 用于前端下载前的条数预览。
+     *
+     * @param dto 查询条件(时间段 searchStartTime/End、status、batchDeptId、type 等)
+     * @return 命中条数
+     */
+    long countBatchDownloadPdf(CheckJobDTO dto);
+
+    /**
      * 年度保养计划表:按设备名称/类别聚合,当年每月做过保养则打勾,排除按天的保养。
      *
      * @param dto 需包含 searchYear(年份,为空默认当年)、可选 type(检查类型)

+ 225 - 64
platform-service/src/main/java/com/platform/service/check/impl/CheckJobServiceImpl.java

@@ -72,7 +72,10 @@ import tk.mybatis.mapper.weekend.WeekendCriteria;
 import javax.servlet.http.HttpServletResponse;
 import java.io.*;
 import java.math.BigDecimal;
+import java.net.URLEncoder;
 import java.nio.file.FileSystems;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 import java.time.DayOfWeek;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -1827,6 +1830,40 @@ public class CheckJobServiceImpl extends BaseServiceImpl<CheckJobMapper, CheckJo
             wbUser = checkJob.getActualUser();
         }
 
+        // 执行人:系统填写保养任务完成人(实际执行人 -> 点检人 -> 更新人)
+        String executor = checkJob.getActualUser();
+        if (StringUtils.isBlank(executor)) {
+            executor = detail.getCheckUserName();
+        }
+        if (StringUtils.isBlank(executor)) {
+            executor = checkJob.getUpdateUserName();
+        }
+
+        // 负责人:设备使用人(保管人/车间使用人员)
+        String responsiblePerson = sbInfoVO.getSaveUserName();
+
+        // 周期:直接填写保养标准的周期(计划周期 + 周期类型)
+        String periodUnit = "";
+        Integer periodTypeVal = detail.getPeriodType();
+        if (periodTypeVal != null) {
+            if (periodTypeVal.equals(CheckPlanPeriodTypeEnum.DAY.getValue())) {
+                periodUnit = "天";
+            } else if (periodTypeVal.equals(CheckPlanPeriodTypeEnum.WEEK.getValue())) {
+                periodUnit = "周";
+            } else if (periodTypeVal.equals(CheckPlanPeriodTypeEnum.MONTH.getValue())) {
+                periodUnit = "月";
+            } else if (periodTypeVal.equals(CheckPlanPeriodTypeEnum.SEASON.getValue())) {
+                periodUnit = "季度";
+            } else if (periodTypeVal.equals(CheckPlanPeriodTypeEnum.YEAR.getValue())) {
+                periodUnit = "年";
+            } else if (periodTypeVal.equals(CheckPlanPeriodTypeEnum.MILES.getValue())) {
+                periodUnit = "公里";
+            } else {
+                periodUnit = "台时";
+            }
+        }
+        String cycleText = detail.getPeriod() == null ? periodUnit : detail.getPeriod() + periodUnit;
+
         // 根据标准 id 查询保养项集合
         List<CheckRequirement> dbRequirementList = requirementMapper.selectByStandardId(checkJob.getStandardId());
 
@@ -1879,12 +1916,6 @@ public class CheckJobServiceImpl extends BaseServiceImpl<CheckJobMapper, CheckJo
         for (int i = 0; i < dbRequirementList.size(); i++) {
             CheckRequirement requirement = dbRequirementList.get(i);
             Map<String, Object> checkItem = new HashMap<>();
-            // 根据 period 字段值设置对应的周期勾选标记
-            String duration = requirement.getPeriod();
-            checkItem.put("period1", "月度".equals(duration) ? duration : ""); // 月度
-            checkItem.put("period2", "季度".equals(duration) ? duration : ""); // 季度
-            checkItem.put("period3", "半年".equals(duration) ? duration : ""); // 半年
-            checkItem.put("period4", "年度".equals(duration) ? duration : ""); // 年度
             checkItem.put("manageItem", requirement.getManageItem()); // 管理项目(数据库)
             checkItem.put("checkItem", requirement.getCheckItem()); // 检查项目(数据库)
             checkItem.put("method", requirement.getMethod()); // 方法、工具(数据库)
@@ -1963,6 +1994,11 @@ public class CheckJobServiceImpl extends BaseServiceImpl<CheckJobMapper, CheckJo
             ctx.setVariable("period", savePeriod); // 保存期
             ctx.setVariable("workHours", wbHours);
             ctx.setVariable("maintainers", wbUser);
+            // 右上角签名:执行人(保养任务完成人)、负责人(设备使用人)
+            ctx.setVariable("executor", executor);
+            ctx.setVariable("responsiblePerson", responsiblePerson);
+            // 周期:直接填写保养标准的周期
+            ctx.setVariable("cycleText", cycleText);
             // 动态检查项
             ctx.setVariable("checkItems", checkItems);
             // 图片 URL 列表
@@ -2589,14 +2625,10 @@ public class CheckJobServiceImpl extends BaseServiceImpl<CheckJobMapper, CheckJo
 
     @Override
     public void generateJobNew() {
-        // 检索所有保养标准,按预计执行时间来划分
-        // 预计执行时间还没到的,忽略
-        // 预计执行时间就是今天的,执行
-        // 预计执行时间超时了,执行
+        // 检索所有保养标准:对一条任务都没有的标准保底补建一条任务,
+        // 任务日期落在下次执行日期(nextDate);默认视图不显示未来任务,
+        // 但用户按未来时间段做条件查询时仍能看到,避免零任务标准查不到任何记录
         LocalDateTime currentDate = LocalDateTime.now();
-        LocalDate now = LocalDate.now();
-        // "yyyy-MM-dd"
-        String todayStr = DateUtils.dateToString(currentDate, DateUtils.PATTERN_YMD);
         List<CheckJob> jobList = new ArrayList<CheckJob>();
         List<CheckStandard> standardList = standardMapper.selectAll();
         // 针对没有设置指定人方式的,要更新
@@ -2634,62 +2666,62 @@ public class CheckJobServiceImpl extends BaseServiceImpl<CheckJobMapper, CheckJo
                     }
                     standard.setNextDate(nextDate);
                 }
-                String nextDateStr = DateUtils.dateToString(standard.getNextDate(), DateUtils.PATTERN_YMD);
-                if (nextDateStr.equals(todayStr) || standard.getNextDate().isBefore(now)) { // 预计执行时间就是今天的,执行
-                                                                                            // 预计执行时间超时了,执行
 
-                    checkJob.setSbId(standard.getSbId());
-                    // 有可能设备已经删除了,继续执行其他的
-                    if (StringUtils.isBlank(standard.getSbId())) {
-                        continue;
-                    }
-                    SbInfo sbInfo = sbInfoService.getModelById(standard.getSbId());
-                    if (sbInfo == null) {
-                        continue;
-                    }
-                    if (!sbInfo.getStatus().equals(SbInfoStatusEnum.IN_USE.getValue())) {
-                        continue;
-                    }
-                    if (standard.getCheckUserType() == null) {
-                        if (standard.getPeriodType() < CheckPlanPeriodTypeEnum.WEEK.getValue()) {
-                            checkJob.setCheckUserId(sbInfo.getSaveUser());
-                            standard.setCheckUserType(CheckUserTypeEnum.USE_USER.getValue());
-                        } else {
-                            checkJob.setCheckUserId(sbInfo.getRepairUser());
-                            standard.setCheckUserType(CheckUserTypeEnum.REPAIR_USER.getValue());
-                        }
+                checkJob.setSbId(standard.getSbId());
+                // 有可能设备已经删除了,继续执行其他的
+                if (StringUtils.isBlank(standard.getSbId())) {
+                    continue;
+                }
+                SbInfo sbInfo = sbInfoService.getModelById(standard.getSbId());
+                if (sbInfo == null) {
+                    continue;
+                }
+                if (!sbInfo.getStatus().equals(SbInfoStatusEnum.IN_USE.getValue())) {
+                    continue;
+                }
+                if (standard.getCheckUserType() == null) {
+                    if (standard.getPeriodType() < CheckPlanPeriodTypeEnum.WEEK.getValue()) {
+                        checkJob.setCheckUserId(sbInfo.getSaveUser());
+                        standard.setCheckUserType(CheckUserTypeEnum.USE_USER.getValue());
                     } else {
-                        if (standard.getCheckUserType().equals(CheckUserTypeEnum.USE_USER.getValue())) {
-                            checkJob.setCheckUserId(sbInfo.getSaveUser());
-                        } else if (standard.getCheckUserType().equals(CheckUserTypeEnum.REPAIR_USER.getValue())) {
-                            checkJob.setCheckUserId(sbInfo.getRepairUser());
-                        } else if (standard.getCheckUserType().equals(CheckUserTypeEnum.ZHIDING.getValue())) {
-                            checkJob.setCheckUserId(sbInfo.getRepairUser());
-                        } else if (standard.getCheckUserType().equals(CheckUserTypeEnum.TEAM.getValue())) {
-                            // 班组模式:设置班组ID,不指定具体执行人
-                            checkJob.setCheckDeptId(standard.getCheckDeptId());
-                            checkJob.setCheckUserId(null);
-                        }
+                        checkJob.setCheckUserId(sbInfo.getRepairUser());
+                        standard.setCheckUserType(CheckUserTypeEnum.REPAIR_USER.getValue());
                     }
-                    checkJob.setStandardHours(standard.getStandardHours());
-                    checkJob.setStatus(CheckJobStatusEnum.NOT_EXECUTE.getValue());
-                    // 计算计划开始时间和结束时间
-                    calcTimeNew(checkJob, currentDate, standard);
-                    checkJob.setCreatedTime(LocalDateTime.now());
-                    checkJob.setId(IdGeneratorUtils.getObjectId());
-                    checkJob.setPartId(standard.getPart());
-
-                    checkJob.setType(standard.getType());
-                    checkJob.setReceiveOvertime(false);
-                    checkJob.setRequirement(standard.getRequirement());
-                    checkJob.setRemark(standard.getRemark());
-                    checkJob.setName(standard.getName());
-                    if (standard.getLastDate() == null) {
-                        standard.setLastDate(currentDate.toLocalDate());
+                } else {
+                    if (standard.getCheckUserType().equals(CheckUserTypeEnum.USE_USER.getValue())) {
+                        checkJob.setCheckUserId(sbInfo.getSaveUser());
+                    } else if (standard.getCheckUserType().equals(CheckUserTypeEnum.REPAIR_USER.getValue())) {
+                        checkJob.setCheckUserId(sbInfo.getRepairUser());
+                    } else if (standard.getCheckUserType().equals(CheckUserTypeEnum.ZHIDING.getValue())) {
+                        checkJob.setCheckUserId(sbInfo.getRepairUser());
+                    } else if (standard.getCheckUserType().equals(CheckUserTypeEnum.TEAM.getValue())) {
+                        // 班组模式:设置班组ID,不指定具体执行人
+                        checkJob.setCheckDeptId(standard.getCheckDeptId());
+                        checkJob.setCheckUserId(null);
                     }
-                    updateStandardList.add(standard);
-                    jobList.add(checkJob);
                 }
+                checkJob.setStandardHours(standard.getStandardHours());
+                checkJob.setStatus(CheckJobStatusEnum.NOT_EXECUTE.getValue());
+                // 计算计划开始时间和结束时间
+                LocalDate origLastDate = standard.getLastDate();
+                LocalDate origNextDate = standard.getNextDate();
+                calcTimeNew(checkJob, currentDate, standard);
+                standard.setLastDate(origLastDate);
+                standard.setNextDate(origNextDate);
+                checkJob.setCreatedTime(LocalDateTime.now());
+                checkJob.setId(IdGeneratorUtils.getObjectId());
+                checkJob.setPartId(standard.getPart());
+
+                checkJob.setType(standard.getType());
+                checkJob.setReceiveOvertime(false);
+                checkJob.setRequirement(standard.getRequirement());
+                checkJob.setRemark(standard.getRemark());
+                checkJob.setName(standard.getName());
+                if (standard.getLastDate() == null) {
+                    standard.setLastDate(currentDate.toLocalDate());
+                }
+                updateStandardList.add(standard);
+                jobList.add(checkJob);
             }
         }
         if (!CollectionUtils.isEmpty(jobList)) {
@@ -3031,6 +3063,135 @@ public class CheckJobServiceImpl extends BaseServiceImpl<CheckJobMapper, CheckJo
         }
     }
 
+    /**
+     * 批量下载保养记录 PDF:按时间段/设备/部门等条件查询已完成任务,
+     * 将磁盘上已生成的 PDF 流式打包为 zip 返回。
+     * PDF 仅在任务完成时生成,找不到的文件跳过并记录日志。
+     *
+     * @param dto      查询条件(actualEndTimeStart/End、sbId、typeId、checkDeptId 等)
+     * @param response HttpServletResponse
+     */
+    @Override
+    public void batchDownloadPdf(CheckJobDTO dto, HttpServletResponse response) throws IOException {
+        if (dto == null) {
+            dto = new CheckJobDTO();
+        }
+        // 保养记录 PDF 仅在任务完成时生成,未指定状态时默认只取已完成任务
+        if (dto.getStatus() == null && CollectionUtils.isEmpty(dto.getStatusList())) {
+            dto.setStatusList(Collections.singletonList(CheckJobStatusEnum.FINISHED.getValue()));
+        }
+
+        List<CheckJobVO> jobList = getVOListByDTO(dto);
+
+        String separator = FileSystems.getDefault().getSeparator();
+        String pdfDir = UPLOAD_ROOT_FOLDER + separator + "files";
+        DateTimeFormatter nameFmt = DateTimeFormatter.ofPattern("yyyyMMdd");
+        DateTimeFormatter zipFmt = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+        // 收集磁盘上真实存在的 PDF 文件及其在压缩包内的业务化名称
+        List<File> pdfFiles = new ArrayList<>();
+        List<String> entryNames = new ArrayList<>();
+        Set<String> usedNames = new HashSet<>();
+        int missingCount = 0;
+        if (!CollectionUtils.isEmpty(jobList)) {
+            for (CheckJobVO job : jobList) {
+                String id = job.getId();
+                if (StringUtils.isBlank(id)) {
+                    continue;
+                }
+                String pdfFileName = id.replaceAll("[\\\\/:*?\"<>|]", "_") + ".pdf";
+                File pdfFile = new File(pdfDir, pdfFileName);
+                if (!pdfFile.exists() || !pdfFile.isFile()) {
+                    missingCount++;
+                    log.warn("批量下载保养记录PDF:任务[{}]对应的PDF文件不存在,已跳过。路径={}", id, pdfFile.getAbsolutePath());
+                    continue;
+                }
+                // 压缩包内文件名:设备名称_设备编号_完成日期,便于用户辨认
+                String datePart = job.getActualEndTime() == null ? "" : job.getActualEndTime().format(nameFmt);
+                String baseName = (StringUtils.isBlank(job.getSbName()) ? "保养记录" : job.getSbName())
+                        + (StringUtils.isBlank(job.getSbNo()) ? "" : "_" + job.getSbNo())
+                        + (StringUtils.isBlank(datePart) ? "" : "_" + datePart);
+                baseName = baseName.replaceAll("[\\\\/:*?\"<>|]", "_");
+                String entryName = baseName + ".pdf";
+                // 处理重名:追加序号
+                if (!usedNames.add(entryName)) {
+                    int seq = 2;
+                    String candidate;
+                    do {
+                        candidate = baseName + "(" + seq++ + ").pdf";
+                    } while (!usedNames.add(candidate));
+                    entryName = candidate;
+                }
+                pdfFiles.add(pdfFile);
+                entryNames.add(entryName);
+            }
+        }
+
+        if (pdfFiles.isEmpty()) {
+            log.warn("批量下载保养记录PDF:没有可下载的文件,查询到任务{}条,缺失PDF{}条",
+                    jobList == null ? 0 : jobList.size(), missingCount);
+            response.reset();
+            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+            response.setContentType("text/plain; charset=UTF-8");
+            response.getWriter().write("没有可下载的保养记录PDF文件");
+            return;
+        }
+
+        // 设置响应头,边压缩边写响应流,避免整个压缩包驻留内存
+        response.reset();
+        response.setContentType("application/zip");
+        response.setCharacterEncoding("UTF-8");
+        String zipName = "保养记录_" + LocalDateTime.now().format(zipFmt) + ".zip";
+        // 与本项目导出一致的响应头格式,前端 downLoadExportExcel 按 attachment;filename= 解析文件名
+        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(zipName, "UTF-8"));
+
+        try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
+            byte[] buffer = new byte[8192];
+            for (int i = 0; i < pdfFiles.size(); i++) {
+                File pdfFile = pdfFiles.get(i);
+                zos.putNextEntry(new ZipEntry(entryNames.get(i)));
+                try (FileInputStream fis = new FileInputStream(pdfFile);
+                        BufferedInputStream bis = new BufferedInputStream(fis)) {
+                    int len;
+                    while ((len = bis.read(buffer)) != -1) {
+                        zos.write(buffer, 0, len);
+                    }
+                }
+                zos.closeEntry();
+            }
+            zos.flush();
+        }
+        log.info("批量下载保养记录PDF完成:共打包{}个文件,跳过缺失{}个", pdfFiles.size(), missingCount);
+    }
+
+    /**
+     * 应用“仅本人/本部门”数据权限:当 filter=SELF 时,限定执行人为当前用户,
+     * 并按班组任务可见性规则填充可见部门ID列表。批量下载与列表查询共用,保证口径一致。
+     */
+    private void applySelfDataScope(CheckJobDTO model) {
+        if (model.getFilter() != null && DataFilterTypeEnum.SELF.getValue() == model.getFilter().intValue()) {
+            UserInfo userInfo = SecurityUtils.getUserInfo();
+            model.setCheckUserId(userInfo.getUserId());
+            // 班组任务可见性:包含用户所属部门及所有上级部门ID
+            fillCheckDeptIds(model, userInfo);
+        }
+    }
+
+    @Override
+    public long countBatchDownloadPdf(CheckJobDTO dto) {
+        if (dto == null) {
+            dto = new CheckJobDTO();
+        }
+        // 与 batchDownloadPdf 保持一致:未指定状态时默认只统计已完成任务
+        if (dto.getStatus() == null && CollectionUtils.isEmpty(dto.getStatusList())) {
+            dto.setStatusList(Collections.singletonList(CheckJobStatusEnum.FINISHED.getValue()));
+        }
+        // 应用与列表查询一致的数据权限,保证预览条数与实际可下载范围一致
+        applySelfDataScope(dto);
+        long count = mapper.countByCondition(dto);
+        return count;
+    }
+
     @Override
     public List<CheckJobAnnualPlanVO> getAnnualPlan(CheckJobDTO dto) {
         if (dto == null) {

+ 64 - 0
platform-service/src/main/java/com/platform/service/repair/impl/RepairApplicationFormServiceImpl.java

@@ -511,6 +511,13 @@ public class RepairApplicationFormServiceImpl
                 MessageTemplateUtil.getFreemarkerHtmlContent(repairUrl, model, sb),
                 model.getId(), userIds, mails));
 
+        // 通知班长:给选中的班长发邮件+站内通知,任何异常不影响报修单提交
+        try {
+            sendTeamLeaderNotice(model, sb, repairUrl);
+        } catch (Exception e) {
+            log.error("报修通知班长发送失败, 报修id: " + model.getId(), e);
+        }
+
         // 发送微信通知给所有维修人角色
         if (CollectionUtil.isNotEmpty(openIds) && BeanUtils.isProd()) {
             String mobileDomain = ConfigCache.getLabelByValueAllowNull(SysConfigEnum.SYSTEM_DOMAIN_MOBILE.name());
@@ -603,6 +610,63 @@ public class RepairApplicationFormServiceImpl
         return form;
     }
 
+    /**
+     * 报修提交时通知选中的班长:站内信必发,邮箱未配置的班长跳过邮件,不影响报修单提交
+     *
+     * @param model     报修单DTO
+     * @param sb        报修设备
+     * @param repairUrl 报修单详情地址
+     */
+    private void sendTeamLeaderNotice(RepairApplicationFormDTO model, SbInfo sb, String repairUrl) {
+        if (StringUtils.isBlank(model.getNotifyTeamLeaders())) {
+            return;
+        }
+        List<String> leaderIds = Arrays.stream(model.getNotifyTeamLeaders().split(","))
+                .map(String::trim).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
+        if (CollectionUtil.isEmpty(leaderIds)) {
+            return;
+        }
+        List<String> leaderUserIds = new ArrayList<>();
+        List<String> leaderMails = new ArrayList<>();
+        for (String leaderId : leaderIds) {
+            SysUser leader = userMapper.selectByPrimaryKey(leaderId);
+            if (leader == null) {
+                log.warn("报修通知班长:用户 {} 不存在,跳过", leaderId);
+                continue;
+            }
+            leaderUserIds.add(leader.getUserId());
+            if (StringUtils.isNotBlank(leader.getEmail())) {
+                leaderMails.add(leader.getEmail().trim());
+            } else {
+                log.warn("班长 {} 未配置邮箱,报修通知跳过邮件发送", leader.getRealName());
+            }
+        }
+        if (CollectionUtil.isEmpty(leaderUserIds)) {
+            return;
+        }
+        String sbName = StringUtils.isNotBlank(model.getSbName()) ? model.getSbName() : sb.getName();
+        StringBuilder content = new StringBuilder();
+        content.append("<div>")
+                .append(StringUtils.isBlank(model.getActualUser()) ? "有人" : model.getActualUser())
+                .append("提交了报修单,请知晓:</div>");
+        content.append("<div>报修单号:").append(model.getNo()).append("</div>");
+        content.append("<div>提报人:")
+                .append(StringUtils.isBlank(model.getActualUser()) ? "未知" : model.getActualUser()).append("</div>");
+        content.append("<div>设备名称:").append(StringUtils.isBlank(sbName) ? "未知" : sbName).append("</div>");
+        if (StringUtils.isNotBlank(model.getSbCph())) {
+            content.append("<div>使用位置:").append(model.getSbCph()).append("</div>");
+        }
+        content.append("<div>报修内容:").append(StringUtils.isBlank(model.getContent()) ? "无" : model.getContent())
+                .append("</div>");
+        if (StringUtils.isNotBlank(repairUrl)) {
+            content.append("<div><a href=\"").append(repairUrl)
+                    .append("\" target=\"_blank\">点击查看详情</a></div>");
+        }
+        SpringContextHolder.publishEvent(new WorkplaceBacklogEvent(WorkplaceBacklogTypeEnum.REPAIR.getValue(),
+                WorkplaceBacklogDetailTypeEnum.REPAIR_APPLICATION.getValue(),
+                model.getId(), content.toString(), model.getId(), leaderUserIds, leaderMails));
+    }
+
     @Override
     public void modModelByDTO(RepairApplicationFormDTO model) {
         if (StringUtils.isBlank(model.getSbId())) {

+ 97 - 59
platform-service/src/main/java/com/platform/service/sb/impl/SbInfoServiceImpl.java

@@ -71,6 +71,7 @@ import com.platform.service.util.SysFileUtils;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.exceptions.TooManyResultsException;
+import org.apache.ibatis.session.RowBounds;
 import org.springframework.core.env.Environment;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Isolation;
@@ -1313,6 +1314,18 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                 });
             }
         }
+        // 上次保养完成时间:取该设备最近一次已完成保养任务的实际结束时间
+        Weekend<CheckJob> jobWeekend = new Weekend<>(CheckJob.class);
+        jobWeekend.selectProperties("actualEndTime");
+        jobWeekend.weekendCriteria()
+                .andEqualTo(CheckJob::getSbId, vo.getId())
+                .andEqualTo(CheckJob::getType, CheckStandardTypeEnum.POLLING.getValue())
+                .andEqualTo(CheckJob::getStatus, CheckJobStatusEnum.FINISHED.getValue());
+        jobWeekend.setOrderByClause("actual_end_time DESC");
+        List<CheckJob> finishedJobs = checkJobMapper.selectByExampleAndRowBounds(jobWeekend, new RowBounds(0, 1));
+        if (!CollectionUtils.isEmpty(finishedJobs)) {
+            vo.setLastMaintainFinishTime(finishedJobs.get(0).getActualEndTime());
+        }
         // vo.setCurrentAuditUserName(activitiBusinessService.getCurrentAuditUserName(vo.getProcessInstanceId()));
         return vo;
     }
@@ -1632,8 +1645,6 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
     @Override
     @Transactional(rollbackFor = Exception.class)
     public String importListByAdd(MultipartFile file) {
-        int addNum = 0;
-        List<String> modelError = new ArrayList<String>();
         try {
             List<SbInfoVO> items = CustomExcelImportUtil.importSbInfoList(file.getInputStream());
             if (!CollectionUtils.isEmpty(items)) {
@@ -1643,8 +1654,11 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                 // 用于缓存本次导入新增的生产商,避免同名重复新增
                 Map<String, FirmProducer> newProducerMap = new HashMap<>();
                 List<SbPosition> positionList = sbPositionMapper.selectAll();
+                List<SbType> typeList = sbTypeMapper.selectAll();
                 LocalDateTime now = LocalDateTime.now();
+                List<String> errors = new ArrayList<>();
                 for (SbInfoVO item : items) {
+                    StringBuilder rowErr = new StringBuilder();
                     item.setUseArea(useArea);
                     item.setUseCompany(useCompany);
                     item.setUseProject(useProject);
@@ -1652,34 +1666,31 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                     item.setUseGroup(useGroup);
                     item.setId(IdGeneratorUtils.getObjectId());
 
-                    // 处理所属车间:将车间名称转换为车间ID(匹配不到则留空,不报错)
+                    // 处理所属车间:填了但设备位置(t_sb_position)中匹配不到则记录错误;未填写则跳过
                     if (StringUtils.isNotBlank(item.getPositionName())) {
-                        log.info("导入第{}行,读取到车间名称:{}", item.getNo(), item.getPositionName());
                         boolean findPosition = false;
                         for (SbPosition position : positionList) {
                             if (position.getName().equalsIgnoreCase(item.getPositionName().trim())) {
                                 item.setPositionId(position.getId());
-                                log.info("导入第{}行,匹配车间成功,车间ID:{}", item.getNo(), position.getId());
                                 findPosition = true;
                                 break;
                             }
                         }
                         if (!findPosition) {
-                            log.warn("导入第{}行,系统找不到车间名称【{}】,车间ID留空", item.getNo(), item.getPositionName());
                             item.setPositionId(null);
+                            rowErr.append("所属车间【").append(item.getPositionName()).append("】在设备位置中未找到;");
                         }
-                    } else {
-                        log.warn("导入第{}行,车间名称为空,跳过车间匹配", item.getNo());
                     }
                     // 是否固定资产:默认为否
                     if (item.getIsFinancing() == null) {
                         item.setIsFinancing(SbInfoFinancingEnum.NOT.getValue());
                     }
-                    boolean findSaveUser = false;
+                    // 使用人(保管人):填了但系统中找不到则记录错误
                     if (StringUtils.isNotBlank(item.getSaveUserName())
                             && !"null".equalsIgnoreCase(item.getSaveUserName())) {
+                        boolean findSaveUser = false;
                         for (SysUser user : users) {
-                            if (user.getRealName().equals(item.getSaveUserName())) {
+                            if (user.getRealName().equalsIgnoreCase(item.getSaveUserName())) {
                                 item.setSaveUser(user.getUserId());
                                 item.setUseUser(user.getUserId());
                                 findSaveUser = true;
@@ -1687,51 +1698,56 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                             }
                         }
                         if (!findSaveUser) {
-                            log.warn("新增导入,系统找不到使用人员【{}】,保管人ID留空,后续可手动关联", item.getSaveUserName());
                             item.setSaveUser(null);
                             item.setUseUser(null);
+                            rowErr.append("使用人(保管人)【").append(item.getSaveUserName()).append("】在系统中未找到;");
                         }
                     }
+                    // 维修人:填了但系统中找不到则记录错误
                     if (StringUtils.isNotBlank(item.getRepairUserName())
                             && !"null".equalsIgnoreCase(item.getRepairUserName())) {
                         boolean findRepairUser = false;
                         for (SysUser user : users) {
-                            if (user.getRealName().equals(item.getRepairUserName())) {
+                            if (user.getRealName().equalsIgnoreCase(item.getRepairUserName())) {
                                 item.setRepairUser(user.getUserId());
                                 findRepairUser = true;
                                 break;
                             }
                         }
                         if (!findRepairUser) {
-                            throw new BusinessException("找不到维修人员,请先添加, 名称" + item.getRepairUserName());
+                            rowErr.append("维修人【").append(item.getRepairUserName()).append("】在系统中未找到;");
                         }
                     }
+                    // 第二维修人:填了但系统中找不到则记录错误
                     if (StringUtils.isNotBlank(item.getRepairUserNameSecond())
                             && !"null".equalsIgnoreCase(item.getRepairUserNameSecond())) {
                         boolean findRepairUser = false;
                         for (SysUser user : users) {
-                            if (user.getRealName().equals(item.getRepairUserNameSecond())) {
+                            if (user.getRealName().equalsIgnoreCase(item.getRepairUserNameSecond())) {
                                 item.setRepairUserSecond(user.getUserId());
                                 findRepairUser = true;
                                 break;
                             }
                         }
                         if (!findRepairUser) {
-                            throw new BusinessException("找不到第二维修人员,请先添加, 名称" + item.getRepairUserNameSecond());
+                            rowErr.append("第二维修人【").append(item.getRepairUserNameSecond()).append("】在系统中未找到;");
                         }
                     }
-
-                    List<SbType> typeList = sbTypeMapper.selectAll();
-                    boolean find = false;
-                    for (SbType type : typeList) {
-                        if (type.getName().equals(item.getTypeName())) {
-                            item.setTypeId(type.getId());
-                            find = true;
-                            break;
+                    // 设备类型:必填,且必须能在系统中匹配到
+                    if (StringUtils.isBlank(item.getTypeName())) {
+                        rowErr.append("设备类型未填写;");
+                    } else {
+                        boolean findType = false;
+                        for (SbType type : typeList) {
+                            if (type.getName().equalsIgnoreCase(item.getTypeName())) {
+                                item.setTypeId(type.getId());
+                                findType = true;
+                                break;
+                            }
+                        }
+                        if (!findType) {
+                            rowErr.append("设备类型【").append(item.getTypeName()).append("】在系统中未找到;");
                         }
-                    }
-                    if (!find) {
-                        throw new BusinessException("找不到设备类型,请先添加:" + item.getTypeName());
                     }
 
                     boolean findProdcuer = false;
@@ -1764,8 +1780,15 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                             item.setProducerId(newProducer.getId());
                         }
                     }
+                    if (rowErr.length() > 0) {
+                        errors.add("设备【" + item.getNo() + "】:" + rowErr);
+                    }
                     addItems.add(item);
                 }
+                if (!errors.isEmpty()) {
+                    throw new BusinessException("导入失败,共发现 " + errors.size()
+                            + " 台设备的数据有问题,请修正后重新导入:\n" + String.join("\n", errors));
+                }
                 if (!CollectionUtils.isEmpty(addItems)) {
                     List<SbInfo> itemsAdd = BeanConverterUtil.copyListProperties(addItems, SbInfo.class);
                     mapper.insertListforComplex(itemsAdd);
@@ -1787,9 +1810,7 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
     @Transactional(rollbackFor = Exception.class)
     public String importListByUpdateALl(MultipartFile file) {
 
-        int addNum = 0;
-        List<String> modelError = new ArrayList<String>();
-        Set<Integer> newIndexSet = new HashSet<>();
+        int newCount = 0;
         try {
             List<SbInfoVO> items = CustomExcelImportUtil.importListByUpdateALl(file.getInputStream());
             if (!CollectionUtils.isEmpty(items)) {
@@ -1798,47 +1819,47 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                 // 用于缓存本次导入新增的生产商,避免同名重复新增
                 Map<String, FirmProducer> newProducerMap = new HashMap<>();
                 List<SbPosition> positionList = sbPositionMapper.selectAll();
+                List<SbType> typeList = sbTypeMapper.selectAll();
                 LocalDateTime now = LocalDateTime.now();
+                List<String> errors = new ArrayList<>();
+                List<SbInfo> toSave = new ArrayList<>();
+                List<Boolean> isNewFlags = new ArrayList<>();
                 for (SbInfoVO item : items) {
+                    StringBuilder rowErr = new StringBuilder();
                     // 通过设备编号no查找已有记录,获取ID用于更新(与新增导入保持一致,不依赖Excel中的ID列)
                     SbInfoVO existingInfo = mapper.getByNo(item.getNo());
-                    if (existingInfo == null) {
-                        log.info("更新导入第{}行,设备编号【{}】在系统中不存在,将新增", addNum + 1, item.getNo());
+                    boolean isNew = (existingInfo == null);
+                    if (isNew) {
                         item.setId(IdGeneratorUtils.getObjectId());
                         item.setCreatedTime(now);
                         item.setCreatedUserId(SecurityUtils.getUserInfo().getUserId());
-                        newIndexSet.add(addNum);
                     } else {
                         item.setId(existingInfo.getId());
                     }
 
-                    // 处理所属车间:将车间名称转换为车间ID(匹配不到则留空,不报错)
+                    // 处理所属车间:填了但设备位置(t_sb_position)中匹配不到则记录错误;未填写则跳过
                     if (StringUtils.isNotBlank(item.getPositionName())) {
-                        log.info("更新导入第{}行,读取到车间名称:{}", item.getNo(), item.getPositionName());
                         boolean findPosition = false;
                         for (SbPosition position : positionList) {
                             if (position.getName().equalsIgnoreCase(item.getPositionName().trim())) {
                                 item.setPositionId(position.getId());
-                                log.info("更新导入第{}行,匹配车间成功,车间ID:{}", item.getNo(), position.getId());
                                 findPosition = true;
                                 break;
                             }
                         }
                         if (!findPosition) {
-                            log.warn("更新导入第{}行,系统找不到车间名称【{}】,车间ID留空", item.getNo(), item.getPositionName());
                             item.setPositionId(null);
+                            rowErr.append("所属车间【").append(item.getPositionName()).append("】在设备位置中未找到;");
                         }
-                    } else {
-                        log.warn("更新导入第{}行,车间名称为空,跳过车间匹配", item.getNo());
                     }
                     // 是否固定资产:默认为否
                     if (item.getIsFinancing() == null) {
                         item.setIsFinancing(SbInfoFinancingEnum.NOT.getValue());
                     }
-
-                    boolean findSaveUser = false;
+                    // 使用人(保管人):填了但系统中找不到则记录错误
                     if (StringUtils.isNotBlank(item.getSaveUserName())
                             && !"null".equalsIgnoreCase(item.getSaveUserName())) {
+                        boolean findSaveUser = false;
                         for (SysUser user : users) {
                             if (user.getRealName().equalsIgnoreCase(item.getSaveUserName())) {
                                 item.setSaveUser(user.getUserId());
@@ -1848,11 +1869,12 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                             }
                         }
                         if (!findSaveUser) {
-                            log.warn("更新导入,系统找不到使用人员【{}】,保管人ID留空,后续可手动关联", item.getSaveUserName());
                             item.setSaveUser(null);
                             item.setUseUser(null);
+                            rowErr.append("使用人(保管人)【").append(item.getSaveUserName()).append("】在系统中未找到;");
                         }
                     }
+                    // 维修人:填了但系统中找不到则记录错误
                     if (StringUtils.isNotBlank(item.getRepairUserName())
                             && !"null".equalsIgnoreCase(item.getRepairUserName())) {
                         boolean findRepairUser = false;
@@ -1864,9 +1886,10 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                             }
                         }
                         if (!findRepairUser) {
-                            throw new BusinessException("找不到维修人员,请先添加, 名称" + item.getRepairUserName());
+                            rowErr.append("维修人【").append(item.getRepairUserName()).append("】在系统中未找到;");
                         }
                     }
+                    // 第二维修人:填了但系统中找不到则记录错误
                     if (StringUtils.isNotBlank(item.getRepairUserNameSecond())
                             && !"null".equalsIgnoreCase(item.getRepairUserNameSecond())) {
                         boolean findRepairUser = false;
@@ -1878,21 +1901,24 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                             }
                         }
                         if (!findRepairUser) {
-                            throw new BusinessException("找不到第二维修人员,请先添加, 名称" + item.getRepairUserNameSecond());
+                            rowErr.append("第二维修人【").append(item.getRepairUserNameSecond()).append("】在系统中未找到;");
                         }
                     }
-
-                    List<SbType> typeList = sbTypeMapper.selectAll();
-                    boolean find = false;
-                    for (SbType type : typeList) {
-                        if (type.getName().equalsIgnoreCase(item.getTypeName())) {
-                            item.setTypeId(type.getId());
-                            find = true;
-                            break;
+                    // 设备类型:必填,且必须能在系统中匹配到
+                    if (StringUtils.isBlank(item.getTypeName())) {
+                        rowErr.append("设备类型未填写;");
+                    } else {
+                        boolean findType = false;
+                        for (SbType type : typeList) {
+                            if (type.getName().equalsIgnoreCase(item.getTypeName())) {
+                                item.setTypeId(type.getId());
+                                findType = true;
+                                break;
+                            }
+                        }
+                        if (!findType) {
+                            rowErr.append("设备类型【").append(item.getTypeName()).append("】在系统中未找到;");
                         }
-                    }
-                    if (!find) {
-                        throw new BusinessException("找不到设备类型,请先添加:" + item.getTypeName());
                     }
 
                     boolean findProdcuer = false;
@@ -1925,17 +1951,29 @@ public class SbInfoServiceImpl extends BaseServiceImpl<SbInfoMapper, SbInfo, SbI
                             item.setProducerId(newProducer.getId());
                         }
                     }
-                    if (newIndexSet.contains(addNum)) {
-                        SbInfo newItem = BeanConverterUtil.copyObjectProperties(item, SbInfo.class);
+                    if (rowErr.length() > 0) {
+                        errors.add("设备【" + item.getNo() + "】:" + rowErr);
+                    }
+                    toSave.add(BeanConverterUtil.copyObjectProperties(item, SbInfo.class));
+                    isNewFlags.add(isNew);
+                }
+                // 整批拦下:只要有任何一处问题,全部不入库,汇总报错(事务回滚)
+                if (!errors.isEmpty()) {
+                    throw new BusinessException("导入失败,共发现 " + errors.size()
+                            + " 台设备的数据有问题,请修正后重新导入:\n" + String.join("\n", errors));
+                }
+                // 全部校验通过后再统一写库
+                for (int idx = 0; idx < toSave.size(); idx++) {
+                    if (isNewFlags.get(idx)) {
                         List<SbInfo> singleList = new ArrayList<>();
-                        singleList.add(newItem);
+                        singleList.add(toSave.get(idx));
                         mapper.insertListforComplex(singleList);
+                        newCount++;
                     } else {
-                        mapper.updateByPrimaryKeySelective(BeanConverterUtil.copyObjectProperties(item, SbInfo.class));
+                        mapper.updateByPrimaryKeySelective(toSave.get(idx));
                     }
                 }
             }
-            int newCount = newIndexSet.size();
             int updateCount = items.size() - newCount;
             return "总计修改导入:" + items.size() + "条,更新" + updateCount + "条,新增" + newCount + "条";
         } catch (Exception e) {

+ 8 - 0
platform-service/src/main/java/com/platform/service/upms/SysUserService.java

@@ -12,6 +12,7 @@ import com.platform.dao.vo.im.upms.ImportSysUserVO;
 import com.platform.service.base.IBaseService;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * @Description 用户 service
@@ -178,6 +179,13 @@ public interface SysUserService extends IBaseService<SysUser, SysUserDTO> {
     List<SysUserVO> getProjectRoleUser(SysUserDTO sysUserDTO);
 
     /**
+     * 报修通知班长候选人:全部班长身份用户 + 当前用户所在部门(班组)下的班长
+     *
+     * @return leaders:全部班长列表;defaultIds:当前用户部门下班长的用户id(前端默认勾选)
+     */
+    Map<String, Object> listTeamLeaders();
+
+    /**
      * 导入
      *
      * @param users  :

+ 61 - 0
platform-service/src/main/java/com/platform/service/upms/impl/SysUserServiceImpl.java

@@ -19,6 +19,8 @@ import com.platform.dao.bean.MyPage;
 import com.platform.dao.dto.upms.*;
 import com.platform.dao.entity.upms.*;
 import com.platform.dao.enums.DeptNatureEnum;
+import com.platform.dao.enums.SysDeptManagerType;
+import com.platform.dao.enums.SysRoleCodeEnum;
 import com.platform.dao.enums.SysUserIdentityType;
 import com.platform.dao.mapper.upms.SysDeptManagerMapper;
 import com.platform.dao.mapper.upms.SysDeptMapper;
@@ -571,6 +573,12 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserMapper, SysUser,
 
     @Override
     public List<SysUserVO> selectDeptManager(String deptId, Integer type) {
+        // 派工「维修负责人」下拉:不再依赖 t_sys_dept_manager 手工登记,
+        // 改为直接返回系统中所有拥有【维修人员 (Maintenance)】角色的用户,不限部门
+        if (SysDeptManagerType.REPAIR.getValue().equals(type)) {
+            return listUsersByRoleCode(SysRoleCodeEnum.Maintenance.name());
+        }
+        // 其他类型保留原有按部门链 + t_sys_dept_manager 登记的查询逻辑
         List<SysDeptVO> sysDeptVos = sysDeptRelationMapper.allParentList(new SysDeptDTO().setDescendant(deptId));
         List<String> deptIds = sysDeptVos.stream().map(SysDeptVO::getAncestor).collect(Collectors.toList());
         deptIds.add(deptId);
@@ -579,6 +587,10 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserMapper, SysUser,
 
     @Override
     public List<SysUserVO> selectUserDeptManager(Integer type) {
+        // 派工「维修负责人」下拉:改为按角色查询,忽略当前用户所在部门
+        if (SysDeptManagerType.REPAIR.getValue().equals(type)) {
+            return listUsersByRoleCode(SysRoleCodeEnum.Maintenance.name());
+        }
         UserInfo userInfo = SecurityUtils.getUserInfo();
         List<SysUserDept> sysUserDepts = sysUserDeptService.selectByUserId(userInfo.getUserId());
         if (CollectionUtil.isEmpty(sysUserDepts)) {
@@ -588,6 +600,17 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserMapper, SysUser,
     }
 
     /**
+     * 按角色代码查询用户列表(不限部门)
+     * 复用 SysUserMapper.selectDeptRoleUser,不传 deptIds/deptId 即为全量
+     */
+    private List<SysUserVO> listUsersByRoleCode(String roleCode) {
+        SysUserDTO query = new SysUserDTO();
+        query.setRoleCode(roleCode);
+        List<SysUserVO> users = mapper.selectDeptRoleUser(query);
+        return users == null ? ListUtils.newArrayList(0) : users;
+    }
+
+    /**
      * 当前部门下级角色的人
      *
      * @return
@@ -610,6 +633,44 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserMapper, SysUser,
         return mapper.selectDeptRoleUser(query);
     }
 
+    /**
+     * 报修通知班长候选人:全部班长身份用户 + 当前用户所在部门(班组)下的班长
+     */
+    @Override
+    public Map<String, Object> listTeamLeaders() {
+        // 全部具有班长身份的人
+        SysUserDTO allQuery = new SysUserDTO();
+        allQuery.setRoleCode(SysRoleCodeEnum.BAN_ZHANG.name());
+        List<SysUserVO> leaders = mapper.selectDeptRoleUser(allQuery);
+        if (leaders == null) {
+            leaders = ListUtils.newArrayList(0);
+        }
+        // 当前用户所在部门下的班长,前端默认勾选,找不到则为空
+        List<String> defaultIds = new ArrayList<>();
+        UserInfo userInfo = SecurityUtils.getUserInfo();
+        if (userInfo != null) {
+            List<SysUserDept> userDepts = sysUserDeptService.selectByUserId(userInfo.getUserId());
+            if (CollectionUtil.isNotEmpty(userDepts)) {
+                List<String> deptIds = userDepts.stream().map(SysUserDept::getDeptId)
+                        .filter(deptId -> StringUtils.isNotBlank(deptId)).distinct().collect(Collectors.toList());
+                if (CollectionUtil.isNotEmpty(deptIds)) {
+                    SysUserDTO defaultQuery = new SysUserDTO();
+                    defaultQuery.setRoleCode(SysRoleCodeEnum.BAN_ZHANG.name());
+                    defaultQuery.setDeptIds(deptIds);
+                    List<SysUserVO> defaultLeaders = mapper.selectDeptRoleUser(defaultQuery);
+                    if (CollectionUtil.isNotEmpty(defaultLeaders)) {
+                        defaultIds = defaultLeaders.stream().map(SysUserVO::getUserId).distinct()
+                                .collect(Collectors.toList());
+                    }
+                }
+            }
+        }
+        Map<String, Object> result = new HashMap<>(4);
+        result.put("leaders", leaders);
+        result.put("defaultIds", defaultIds);
+        return result;
+    }
+
     @Override
     public List<SysUserVO> getProjectRoleUser(SysUserDTO sysUserDTO) {
         String deptId = sysUserDTO.getDeptId();

+ 5 - 27
platform-service/src/main/resources/templates/maintenance_record.html

@@ -239,13 +239,13 @@
         </tr>
         <tr>
           <td>签名</td>
-          <td></td>
-          <td></td>
+          <td>[[${executor}]]</td>
+          <td>[[${responsiblePerson}]]</td>
         </tr>
         <tr>
           <td>日期</td>
-          <td></td>
-          <td></td>
+          <td>[[${recordDate}]]</td>
+          <td>[[${recordDate}]]</td>
         </tr>
       </table>
     </div>
@@ -261,13 +261,7 @@
       <div style="float:left;width: 20%;">日期:[[${recordDate}]]</div>
       <div style="float:left;width: 20%;">维保工时:[[${workHours}]]</div>
       <div class="cycle-section">
-        周期:
-        <label><input style="height: 10px;width: 10px; margin:0" type="checkbox" name="cycles" value="month"> 月度</label>
-        <label><input style="height: 10px;width: 10px; margin:0" type="checkbox" name="cycles" value="quarter">
-          季度</label>
-        <label><input style="height: 10px;width: 10px; margin:0" type="checkbox" name="cycles" value="halfYear">
-          半年</label>
-        <label><input style="height: 10px;width: 10px; margin:0" type="checkbox" name="cycles" value="year"> 年度</label>
+        周期:[[${cycleText}]]
       </div>
     </div>
 
@@ -276,10 +270,6 @@
         <thead>
           <tr>
             <th>编号</th>
-            <th>月度</th>
-            <th>季度</th>
-            <th>半年</th>
-            <th>年度</th>
             <th>管理项目</th>
             <th>检查项目</th>
             <th>方法、工具</th>
@@ -292,18 +282,6 @@
         <tbody>
           <tr th:each="item, iterStat : ${checkItems}">
             <td th:text="${iterStat.index + 1}">1</td>
-            <td>
-              <span th:if="${item.period1=='月度'}">√</span>
-            </td>
-            <td>
-              <span th:if="${item.period2=='季度'}">√</span>
-            </td>
-            <td>
-              <span th:if="${item.period3=='半年'}">√</span>
-            </td>
-            <td>
-              <span th:if="${item.period4=='年度'}">√</span>
-            </td>
             <td th:text="${item.manageItem}">${item.manageItem}</td>
             <td th:text="${item.checkItem}">${item.checkItem}</td>
             <td th:text="${item.method}">${item.method}</td>