Просмотр исходного кода

feat(check-standard): 新增保养标准批量导入及模板下载功能

- 新增 /import/batch 接口,支持 Excel 批量导入保养标准(仅新增)
- 新增 /import/template 接口,提供含示例数据的导入模板下载
- 新增 ImportCheckStandardVO 定义导入模板列及校验规则
- 实现逐行校验逻辑,任一行有误则整批拒绝并返回全部错误提示
- 校验通过后逐行复用 saveModelByDTO,与界面新增行为一致
- 新增文件内防重及库内重复检测(设备+保养项类别唯一)
- 修复单条删除保养标准时级联删除不完整导致残留孤儿任务的问题
- 修复 cascadingDeleteByKey 中删除标准参数误用项目关联查询条件的 bug
- cascadingDeleteByKey 补充 @Transactional 注解并增加备件明细删除
dazhaxie 2 дней назад
Родитель
Сommit
4df31134fe

+ 84 - 0
platform-dao/src/main/java/com/platform/dao/vo/export/check/ImportCheckStandardVO.java

@@ -0,0 +1,84 @@
+package com.platform.dao.vo.export.check;
+
+import com.platform.office.annotation.Excel;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * @Description 保养标准批量导入 VO(导入专用,模板列与解析逻辑保持一致)
+ * 一行 = 一条保养标准,遵守“一设备 + 一保养项类别”的 1:1:1 绑定规则
+ * @Author qoder
+ * @Version Copyright (c) 2020,北京乾元坤和科技有限公司 All rights reserved.
+ */
+@Data
+@Accessors(chain = true)
+public class ImportCheckStandardVO implements Serializable {
+
+    /**
+     * 设备编号(新号 no 或旧号 zbh 均可,后端双查)
+     */
+    @Excel(name = "设备编号(必填,填新号或旧号)", orderNum = "1")
+    private String sbNo;
+
+    /**
+     * 保养项类别名称(按名称解析 t_sb_type.id,加载该类别下全部保养项)
+     */
+    @Excel(name = "保养项类别(必填,填类别名称)", orderNum = "2")
+    private String categoryName;
+
+    /**
+     * 维护等级:一级/二级/三级
+     */
+    @Excel(name = "维护等级(必填,一级/二级/三级)", orderNum = "3")
+    private String level;
+
+    /**
+     * 执行人方式:使用人/维修人/指定人/班组
+     */
+    @Excel(name = "执行人方式(必填,使用人/维修人/班组)", orderNum = "4")
+    private String checkUserType;
+
+    /**
+     * 执行班组名称(执行人方式为“班组”时必填)
+     */
+    @Excel(name = "执行班组(选填,执行人方式=班组时必填)", orderNum = "5")
+    private String deptName;
+
+    /**
+     * 计划周期(数字)
+     */
+    @Excel(name = "计划周期(必填,正整数)", orderNum = "6")
+    private String period;
+
+    /**
+     * 周期类型:天/周/月/季度/年/公里/台时
+     */
+    @Excel(name = "周期类型(必填,天/周/月/季度/年)", orderNum = "7")
+    private String periodType;
+
+    /**
+     * 上次实际执行日期(yyyy-MM-dd,可空,用于计算下次预计执行日期)
+     */
+    @Excel(name = "上次执行日期(选填,yyyy-MM-dd)", orderNum = "8")
+    private String lastDate;
+
+    /**
+     * 标准工时(可空)
+     */
+    @Excel(name = "标准工时(选填,数字)", orderNum = "9")
+    private String standardHours;
+
+    /**
+     * 检查要求(可空)
+     */
+    @Excel(name = "检查要求(选填)", orderNum = "10")
+    private String requirement;
+
+    /**
+     * 备注(可空)
+     */
+    @Excel(name = "备注(选填)", orderNum = "11")
+    private String remark;
+}

+ 37 - 0
platform-rest/src/main/java/com/platform/rest/controller/check/CheckStandardController.java

@@ -11,6 +11,7 @@ import com.platform.dao.vo.query.check.CheckStandardVO;
 import com.platform.service.check.CheckStandardService;
 import com.platform.service.check.CheckStandardService;
 import com.platform.dao.util.ExcelUtil;
 import com.platform.dao.util.ExcelUtil;
 import com.platform.dao.vo.export.check.ExportCheckStandardVO;
 import com.platform.dao.vo.export.check.ExportCheckStandardVO;
+import com.platform.dao.vo.export.check.ImportCheckStandardVO;
 import com.platform.common.util.BeanConverterUtil;
 import com.platform.common.util.BeanConverterUtil;
 import com.platform.common.validation.group.AddGroup;
 import com.platform.common.validation.group.AddGroup;
 import com.platform.common.validation.group.UpdateGroup;
 import com.platform.common.validation.group.UpdateGroup;
@@ -311,6 +312,42 @@ public class CheckStandardController {
     }
     }
 
 
     /**
     /**
+     * 批量导入保养标准(仅新增,整批校验,任一行有误则全部回滚)
+     *
+     * @param file Excel 文件
+     * @return R
+     */
+    @SysLog("批量导入保养标准")
+    @PostMapping(value = "/import/batch", headers = "content-type=multipart/form-data")
+    @PreAuthorize("@pms.hasPermission('check-spot-standards-add') or @pms.hasPermission('check-polling-standards-add')")
+    public R importBatch(@RequestParam("file") MultipartFile file) {
+        return new R<>(checkStandardService.importBatch(file));
+    }
+
+    /**
+     * 保养标准批量导入模板下载(含一行示例数据)
+     */
+    @SysLog("保养标准导入模板下载")
+    @GetMapping("/import/template")
+    public void importTemplate(HttpServletResponse response) {
+        ImportCheckStandardVO sample = new ImportCheckStandardVO();
+        sample.setSbNo("SB00001");
+        sample.setCategoryName("机械手臂保养");
+        sample.setLevel("一级");
+        sample.setCheckUserType("维修人");
+        sample.setDeptName("");
+        sample.setPeriod("1");
+        sample.setPeriodType("月");
+        sample.setLastDate("2026-01-01");
+        sample.setStandardHours("2");
+        sample.setRequirement("按执行标准检查");
+        sample.setRemark("示例行,导入前请删除");
+        List<ImportCheckStandardVO> list = new ArrayList<>();
+        list.add(sample);
+        ExcelUtil.exportResponseDict(response, ImportCheckStandardVO.class, list, "保养标准导入模板");
+    }
+
+    /**
      * 根据设备ID查询该设备下的所有保养标准(用于批量新增界面回显已有数据)
      * 根据设备ID查询该设备下的所有保养标准(用于批量新增界面回显已有数据)
      *
      *
      * @param sbId 设备ID
      * @param sbId 设备ID

+ 10 - 0
platform-service/src/main/java/com/platform/service/check/CheckStandardService.java

@@ -82,6 +82,16 @@ public interface CheckStandardService extends IBaseService<CheckStandard, CheckS
 
 
     String copyFormSb(CheckStandardDTO model);
     String copyFormSb(CheckStandardDTO model);
 
 
+    /**
+     * 批量导入保养标准(仅新增)。
+     * 先对全表逐行校验,任一行有误则整批拒绝并返回全部错误提示;
+     * 全部通过后逐行复用 {@link #saveModelByDTO} 生成标准(编码、保养项关联、首期任务与界面新增一致)。
+     *
+     * @param file Excel 文件
+     * @return 导入结果提示
+     */
+    String importBatch(MultipartFile file);
+
     String importListByAdd(MultipartFile file);
     String importListByAdd(MultipartFile file);
 
 
     String importListByUpdate(MultipartFile file);
     String importListByUpdate(MultipartFile file);

+ 315 - 7
platform-service/src/main/java/com/platform/service/check/impl/CheckStandardServiceImpl.java

@@ -20,18 +20,21 @@ import com.platform.dao.dto.part.PartInfoDTO;
 import com.platform.dao.entity.check.*;
 import com.platform.dao.entity.check.*;
 import com.platform.dao.entity.part.PartInfo;
 import com.platform.dao.entity.part.PartInfo;
 import com.platform.dao.entity.sb.SbInfo;
 import com.platform.dao.entity.sb.SbInfo;
+import com.platform.dao.entity.upms.SysDept;
 import com.platform.dao.entity.upms.SysFile;
 import com.platform.dao.entity.upms.SysFile;
 import com.platform.dao.entity.upms.SysUser;
 import com.platform.dao.entity.upms.SysUser;
 import com.platform.dao.enums.*;
 import com.platform.dao.enums.*;
 import com.platform.dao.mapper.check.*;
 import com.platform.dao.mapper.check.*;
 import com.platform.dao.mapper.sb.SbInfoMapper;
 import com.platform.dao.mapper.sb.SbInfoMapper;
 import com.platform.dao.mapper.sb.SbTypeMapper;
 import com.platform.dao.mapper.sb.SbTypeMapper;
+import com.platform.dao.mapper.upms.SysDeptMapper;
 import com.platform.dao.mapper.upms.SysFileMapper;
 import com.platform.dao.mapper.upms.SysFileMapper;
 import com.platform.dao.mapper.upms.SysUserMapper;
 import com.platform.dao.mapper.upms.SysUserMapper;
 import com.platform.dao.util.CustomExcelImportUtil;
 import com.platform.dao.util.CustomExcelImportUtil;
 import com.platform.dao.util.ExcelUtil;
 import com.platform.dao.util.ExcelUtil;
 import com.platform.dao.util.MessageTemplateUtil;
 import com.platform.dao.util.MessageTemplateUtil;
 import com.platform.dao.vo.export.check.ExportCheckStandardVO2;
 import com.platform.dao.vo.export.check.ExportCheckStandardVO2;
+import com.platform.dao.vo.export.check.ImportCheckStandardVO;
 import com.platform.dao.vo.query.check.CheckStandardSpareVO;
 import com.platform.dao.vo.query.check.CheckStandardSpareVO;
 import com.platform.dao.vo.query.check.CheckStandardVO;
 import com.platform.dao.vo.query.check.CheckStandardVO;
 import com.platform.service.base.impl.BaseServiceImpl;
 import com.platform.service.base.impl.BaseServiceImpl;
@@ -89,6 +92,8 @@ public class CheckStandardServiceImpl extends BaseServiceImpl<CheckStandardMappe
 
 
     private final SbTypeMapper sbTypeMapper;
     private final SbTypeMapper sbTypeMapper;
 
 
+    private final SysDeptMapper sysDeptMapper;
+
     @Override
     @Override
     public CheckStandard getModelkById(String id) {
     public CheckStandard getModelkById(String id) {
         // 使用新的方法查询,包含保养要求
         // 使用新的方法查询,包含保养要求
@@ -181,11 +186,9 @@ public class CheckStandardServiceImpl extends BaseServiceImpl<CheckStandardMappe
     @Override
     @Override
     @Transactional(rollbackFor = Exception.class)
     @Transactional(rollbackFor = Exception.class)
     public void deleteByPrimaryKey(String id) {
     public void deleteByPrimaryKey(String id) {
-        // 1. 删除保养要求关联关系
-        checkStandardMapper.deleteRequirementsByStandardId(id);
-
-        // 2. 删除点检标准
-        checkStandardMapper.deleteByPrimaryKey(id);
+        // 与批量删除保持一致的完整级联:标准+保养项关联+项目关联+任务+备件+参数,
+        // 避免单条删除后残留孤儿任务(打开任务会报“保养标准已删除”)
+        cascadingDeleteByKey(id);
     }
     }
 
 
     @Override
     @Override
@@ -205,8 +208,11 @@ public class CheckStandardServiceImpl extends BaseServiceImpl<CheckStandardMappe
 
 
 
 
     @Override
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public boolean cascadingDeleteByKey(String id) {
     public boolean cascadingDeleteByKey(String id) {
         int result = mapper.deleteByPrimaryKey(id);
         int result = mapper.deleteByPrimaryKey(id);
+        // 删除保养项关联关系(t_check_standard_requirement),避免孤儿关联
+        checkStandardMapper.deleteRequirementsByStandardId(id);
         Weekend<CheckProjectStandardRelation> weekend = new Weekend<>(CheckProjectStandardRelation.class);
         Weekend<CheckProjectStandardRelation> weekend = new Weekend<>(CheckProjectStandardRelation.class);
         weekend.weekendCriteria().andEqualTo(CheckProjectStandardRelation::getStandardId, id);
         weekend.weekendCriteria().andEqualTo(CheckProjectStandardRelation::getStandardId, id);
         relationMapper.deleteByExample(weekend);
         relationMapper.deleteByExample(weekend);
@@ -216,10 +222,15 @@ public class CheckStandardServiceImpl extends BaseServiceImpl<CheckStandardMappe
         checkJobWeekend.weekendCriteria().andEqualTo(CheckJob::getStandardId, id);
         checkJobWeekend.weekendCriteria().andEqualTo(CheckJob::getStandardId, id);
         checkJobMapper.deleteByExample(checkJobWeekend);
         checkJobMapper.deleteByExample(checkJobWeekend);
 
 
-        // 删除标准参数
+        // 删除标准关联的备件明细
+        Weekend<CheckStandardSpare> spareWeekend = new Weekend<>(CheckStandardSpare.class);
+        spareWeekend.weekendCriteria().andEqualTo(CheckStandardSpare::getCheckId, id);
+        spareMapper.deleteByExample(spareWeekend);
+
+        // 删除标准参数(修复:原误传了项目关联的 weekend,应使用参数自身的查询条件)
         Weekend<CheckStandardParam> checkStandardParamWeekend = new Weekend<>(CheckStandardParam.class);
         Weekend<CheckStandardParam> checkStandardParamWeekend = new Weekend<>(CheckStandardParam.class);
         checkStandardParamWeekend.weekendCriteria().andEqualTo(CheckStandardParam::getStandardId, id);
         checkStandardParamWeekend.weekendCriteria().andEqualTo(CheckStandardParam::getStandardId, id);
-        paramMapper.deleteByExample(weekend);
+        paramMapper.deleteByExample(checkStandardParamWeekend);
         return true;
         return true;
     }
     }
 
 
@@ -1319,6 +1330,303 @@ public class CheckStandardServiceImpl extends BaseServiceImpl<CheckStandardMappe
         return count;
         return count;
     }
     }
 
 
+    /**
+     * 批量导入保养标准(仅新增)。
+     * 逐行校验并组装 DTO,任一行有误则整批拒绝(先校验后落库,保证事务干净);
+     * 全部通过后逐行复用 {@link #saveModelByDTO},确保编码生成、保养项关联、首期任务生成与界面新增完全一致。
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String importBatch(MultipartFile file) {
+        List<ImportCheckStandardVO> rows;
+        try {
+            // 模板由 exportResponseDict 生成:第 1 行为标题、第 2 行为表头,故 titleRows=1
+            rows = ExcelUtil.importExcel(file.getInputStream(), ImportCheckStandardVO.class, 1);
+        } catch (Exception e) {
+            log.error("保养标准导入文件解析失败", e);
+            throw new BusinessException("导入失败:文件解析异常,请使用系统提供的模板");
+        }
+        if (CollectionUtil.isEmpty(rows)) {
+            throw new BusinessException("导入失败:Excel 文件内容为空");
+        }
+        // 防大文件长事务:逐行复用界面新增逻辑(含首期任务生成),限制单次行数
+        if (rows.size() > 1000) {
+            throw new BusinessException("导入失败:单次最多导入 1000 行,请拆分文件后分批导入");
+        }
+
+        List<String> errors = new ArrayList<>();
+        // 校验通过的待保存 DTO,与原始行号一一对应,便于错误定位
+        List<CheckStandardDTO> readyList = new ArrayList<>();
+        // 文件内防重:同一“设备+保养项类别”组合只允许出现一次
+        Map<String, Integer> seenKeys = new HashMap<>();
+        int rowNo = 0;
+        for (ImportCheckStandardVO vo : rows) {
+            rowNo++;
+            // 跳过完全空白的行
+            if (vo == null || isAllBlank(vo)) {
+                continue;
+            }
+            List<String> rowErrors = new ArrayList<>();
+            CheckStandardDTO dto = buildDtoFromRow(vo, rowNo, rowErrors, seenKeys);
+            if (!rowErrors.isEmpty()) {
+                errors.addAll(rowErrors);
+            } else {
+                readyList.add(dto);
+            }
+        }
+
+        if (readyList.isEmpty()) {
+            throw new BusinessException("导入失败:未解析到有效数据行,请检查模板内容");
+        }
+        // 整批拒绝:存在任何错误行则全部不导入
+        if (!errors.isEmpty()) {
+            String msg = String.join(";", errors);
+            if (errors.size() > 20) {
+                msg = errors.subList(0, 20).stream().collect(Collectors.joining(";"))
+                        + ";(其余 " + (errors.size() - 20) + " 条错误略)";
+            }
+            throw new BusinessException("导入失败,共 " + errors.size() + " 行有误,已全部回滚:" + msg);
+        }
+
+        int count = 0;
+        // 记录本次导入生成的编码区间,便于误导入后按编码定位回滚
+        String firstNo = null;
+        String lastNo = null;
+        for (CheckStandardDTO item : readyList) {
+            CheckStandard saved = this.saveModelByDTO(item);
+            if (saved != null && StringUtils.isNotBlank(saved.getNo())) {
+                if (firstNo == null) {
+                    firstNo = saved.getNo();
+                }
+                lastNo = saved.getNo();
+            }
+            count++;
+        }
+        log.info("保养标准批量导入成功,共 {} 条,编码区间 {}~{}", count, firstNo, lastNo);
+        String rangeTip = StringUtils.isNotBlank(firstNo) ? "(编码 " + firstNo + " ~ " + lastNo + ")" : "";
+        return "成功导入 " + count + " 条保养标准" + rangeTip + ",如需撤销可按编码在列表定位后批量删除";
+    }
+
+    /**
+     * 将一行导入数据解析、校验为可保存的 CheckStandardDTO。
+     * 校验失败时把错误写入 rowErrors 并返回 null,不落库。
+     */
+    private CheckStandardDTO buildDtoFromRow(ImportCheckStandardVO vo, int rowNo, List<String> rowErrors, Map<String, Integer> seenKeys) {
+        String prefix = "第" + rowNo + "行:";
+        CheckStandardDTO dto = new CheckStandardDTO();
+
+        // 1. 设备:按新号或旧号双查
+        String sbNo = trim(vo.getSbNo());
+        if (StringUtils.isBlank(sbNo)) {
+            rowErrors.add(prefix + "设备编号不能为空");
+        } else {
+            SbInfo sbInfo = findSbInfoByNo(sbNo);
+            if (sbInfo == null) {
+                rowErrors.add(prefix + "未找到设备编号【" + sbNo + "】");
+            } else {
+                dto.setSbId(sbInfo.getId());
+                dto.setSbNo(sbInfo.getNo());
+                dto.setSbName(sbInfo.getName());
+            }
+        }
+
+        // 2. 保养项类别:按名称解析 t_sb_type.id,并校验该类别下存在保养项
+        String categoryName = trim(vo.getCategoryName());
+        String resolvedTypeId = null;
+        List<String> categoryReqIds = null;
+        if (StringUtils.isBlank(categoryName)) {
+            rowErrors.add(prefix + "保养项类别不能为空");
+        } else {
+            String typeId = sbTypeMapper.selectIdByName(categoryName);
+            if (StringUtils.isBlank(typeId)) {
+                rowErrors.add(prefix + "未找到保养项类别【" + categoryName + "】");
+            } else {
+                Weekend<CheckRequirement> weekendR = Weekend.of(CheckRequirement.class);
+                weekendR.weekendCriteria().andEqualTo(CheckRequirement::getSbType, typeId);
+                List<CheckRequirement> reqs = checkRequirementMapper.selectByExample(weekendR);
+                if (CollectionUtil.isEmpty(reqs)) {
+                    rowErrors.add(prefix + "保养项类别【" + categoryName + "】下没有保养项");
+                } else {
+                    resolvedTypeId = typeId;
+                    categoryReqIds = reqs.stream().map(CheckRequirement::getId).collect(Collectors.toList());
+                    // saveModelByDTO 会按 requirement.sbType 加载该类别下全部保养项并建立关联
+                    CheckRequirement marker = new CheckRequirement();
+                    marker.setSbType(typeId);
+                    dto.setRequirementList(new ArrayList<>(Collections.singletonList(marker)));
+                }
+            }
+        }
+
+        // 3. 维护等级
+        Integer level = parseLevel(trim(vo.getLevel()));
+        if (level == null) {
+            rowErrors.add(prefix + "维护等级填写错误(应为:一级/二级/三级)");
+        } else {
+            dto.setLevel(level);
+        }
+
+        // 4. 执行人方式
+        Integer checkUserType = parseCheckUserType(trim(vo.getCheckUserType()));
+        if (checkUserType == null) {
+            rowErrors.add(prefix + "执行人方式填写错误(应为:使用人/维修人/指定人/班组)");
+        } else {
+            dto.setCheckUserType(checkUserType);
+            // 班组方式必须解析到执行班组
+            if (CheckUserTypeEnum.TEAM.getValue().equals(checkUserType)) {
+                String deptName = trim(vo.getDeptName());
+                if (StringUtils.isBlank(deptName)) {
+                    rowErrors.add(prefix + "执行人方式为班组时,执行班组不能为空");
+                } else {
+                    Weekend<SysDept> deptWeekend = new Weekend<>(SysDept.class);
+                    deptWeekend.weekendCriteria().andEqualTo(SysDept::getName, deptName);
+                    List<SysDept> depts = sysDeptMapper.selectByExample(deptWeekend);
+                    if (CollectionUtil.isEmpty(depts)) {
+                        rowErrors.add(prefix + "未找到执行班组【" + deptName + "】");
+                    } else {
+                        dto.setCheckDeptId(depts.get(0).getDeptId());
+                    }
+                }
+            }
+        }
+
+        // 5. 计划周期 + 周期类型
+        String periodStr = trim(vo.getPeriod());
+        if (StringUtils.isBlank(periodStr)) {
+            rowErrors.add(prefix + "计划周期不能为空");
+        } else {
+            try {
+                dto.setPeriod(Integer.valueOf(periodStr));
+            } catch (NumberFormatException e) {
+                rowErrors.add(prefix + "计划周期必须为数字");
+            }
+        }
+        Integer periodType = parsePeriodType(trim(vo.getPeriodType()));
+        if (periodType == null) {
+            rowErrors.add(prefix + "周期类型填写错误(应为:天/周/月/季度/年/公里/台时)");
+        } else {
+            dto.setPeriodType(periodType);
+        }
+
+        // 6. 上次执行日期(可空)
+        String lastDateStr = trim(vo.getLastDate());
+        if (StringUtils.isNotBlank(lastDateStr)) {
+            try {
+                dto.setLastDate(LocalDate.parse(lastDateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd")));
+            } catch (Exception e) {
+                rowErrors.add(prefix + "上次执行日期格式错误(应为 yyyy-MM-dd)");
+            }
+        }
+
+        // 7. 可选字段
+        dto.setStandardHours(trim(vo.getStandardHours()));
+        dto.setRequirement(trim(vo.getRequirement()));
+        dto.setRemark(trim(vo.getRemark()));
+
+        // 8. 防重复导入:文件内“设备+类别”组合唯一,且库内该设备尚未绑定同类别标准
+        if (StringUtils.isNotBlank(dto.getSbId()) && StringUtils.isNotBlank(resolvedTypeId)) {
+            String key = dto.getSbId() + "|" + resolvedTypeId;
+            Integer firstRow = seenKeys.get(key);
+            if (firstRow != null) {
+                rowErrors.add(prefix + "与第" + firstRow + "行重复(同一设备+同一保养项类别只允许一条标准)");
+            } else {
+                seenKeys.put(key, rowNo);
+            }
+            if (CollectionUtil.isNotEmpty(categoryReqIds)) {
+                Weekend<CheckStandard> existWeekend = new Weekend<>(CheckStandard.class);
+                existWeekend.weekendCriteria().andEqualTo(CheckStandard::getSbId, dto.getSbId());
+                List<CheckStandard> existStandards = mapper.selectByExample(existWeekend);
+                if (CollectionUtil.isNotEmpty(existStandards)) {
+                    List<String> existIds = existStandards.stream().map(CheckStandard::getId).collect(Collectors.toList());
+                    Weekend<CheckStandardRequirement> relWeekend = new Weekend<>(CheckStandardRequirement.class);
+                    relWeekend.weekendCriteria().andIn(CheckStandardRequirement::getStandardId, existIds)
+                            .andIn(CheckStandardRequirement::getRequirementId, categoryReqIds);
+                    if (checkStandardRequirementMapper.selectCountByExample(relWeekend) > 0) {
+                        rowErrors.add(prefix + "该设备已存在类别【" + categoryName + "】的保养标准,不允许重复导入");
+                    }
+                }
+            }
+        }
+
+        // 强制走新增分支并对齐界面新增默认值
+        dto.setId(null);
+        dto.setType(CheckStandardTypeEnum.POLLING.getValue());
+        dto.setEnable(1);
+        dto.setName(StringUtils.isNotBlank(dto.getSbName())
+                ? dto.getSbName() + "-" + trim(vo.getCategoryName()) : trim(vo.getCategoryName()));
+        if (dto.getDetailList() == null) {
+            dto.setDetailList(new ArrayList<>());
+        }
+        return rowErrors.isEmpty() ? dto : null;
+    }
+
+    /**
+     * 按新号(no)或旧号(zbh)查询设备
+     */
+    private SbInfo findSbInfoByNo(String no) {
+        Weekend<SbInfo> byNo = new Weekend<>(SbInfo.class);
+        byNo.weekendCriteria().andEqualTo(SbInfo::getNo, no);
+        List<SbInfo> list = sbInfoMapper.selectByExample(byNo);
+        if (!CollectionUtil.isEmpty(list)) {
+            return list.get(0);
+        }
+        Weekend<SbInfo> byZbh = new Weekend<>(SbInfo.class);
+        byZbh.weekendCriteria().andEqualTo(SbInfo::getZbh, no);
+        list = sbInfoMapper.selectByExample(byZbh);
+        return CollectionUtil.isEmpty(list) ? null : list.get(0);
+    }
+
+    private Integer parseLevel(String text) {
+        if ("一级".equals(text)) {
+            return 1;
+        }
+        if ("二级".equals(text)) {
+            return 2;
+        }
+        if ("三级".equals(text)) {
+            return 3;
+        }
+        return null;
+    }
+
+    private Integer parseCheckUserType(String text) {
+        if ("使用人".equals(text)) {
+            return CheckUserTypeEnum.USE_USER.getValue();
+        }
+        if ("维修人".equals(text)) {
+            return CheckUserTypeEnum.REPAIR_USER.getValue();
+        }
+        if ("指定人".equals(text) || "指定".equals(text)) {
+            return CheckUserTypeEnum.ZHIDING.getValue();
+        }
+        if ("班组".equals(text)) {
+            return CheckUserTypeEnum.TEAM.getValue();
+        }
+        return null;
+    }
+
+    private Integer parsePeriodType(String text) {
+        for (CheckPlanPeriodTypeEnum item : CheckPlanPeriodTypeEnum.values()) {
+            if (item.getDescription().equals(text)) {
+                return item.getValue();
+            }
+        }
+        return null;
+    }
+
+    private String trim(String s) {
+        return s == null ? null : s.trim();
+    }
+
+    private boolean isAllBlank(ImportCheckStandardVO vo) {
+        return StringUtils.isBlank(vo.getSbNo()) && StringUtils.isBlank(vo.getCategoryName())
+                && StringUtils.isBlank(vo.getLevel()) && StringUtils.isBlank(vo.getCheckUserType())
+                && StringUtils.isBlank(vo.getDeptName()) && StringUtils.isBlank(vo.getPeriod())
+                && StringUtils.isBlank(vo.getPeriodType()) && StringUtils.isBlank(vo.getLastDate())
+                && StringUtils.isBlank(vo.getStandardHours()) && StringUtils.isBlank(vo.getRequirement())
+                && StringUtils.isBlank(vo.getRemark());
+    }
+
+
     @Override
     @Override
     public List<CheckStandardVO> selectListBySbId(String sbId) {
     public List<CheckStandardVO> selectListBySbId(String sbId) {
         if (StringUtils.isBlank(sbId)) {
         if (StringUtils.isBlank(sbId)) {