cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java
@@ -9,8 +9,6 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.constant.MenuJumpPathConstants;
import com.ruoyi.common.core.domain.entity.InformationNotification;
import com.ruoyi.common.core.domain.entity.User;
@@ -39,10 +37,19 @@
import com.ruoyi.device.service.DeviceExaminePlanDetailsService;
import com.ruoyi.device.service.DeviceExaminePlanService;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.UserUtils;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.system.service.InformationNotificationService;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -54,7 +61,6 @@
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDate;
@@ -65,6 +71,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -292,57 +299,196 @@
    }
    /**
     * 导出设备核查计划
     * 导出设备核查计划(Excel):有筛选条件时导出筛选结果,没有筛选时导出该年份全部
     * @param deviceExaminePlanId 设备核查计划id
     * @param deviceName 设备名称筛选
     * @param deviceNumber 设备编号筛选
     * @param response 响应体
     */
    @Override
    public void exportDeviceExaminePlanDetail(Integer deviceExaminePlanId, HttpServletResponse response) {
        // 查询设备核查计划
        DeviceExaminePlanDto deviceExaminePlanDto = baseMapper.selectExamineExaminePlanDto(deviceExaminePlanId);
    public void exportDeviceExaminePlanDetail(Integer deviceExaminePlanId, String deviceName, String deviceNumber, HttpServletResponse response) {
        DeviceExaminePlan deviceExaminePlan = baseMapper.selectById(deviceExaminePlanId);
        if (deviceExaminePlan == null) {
            throw new ErrorException("设备核查计划不存在");
        }
        DeviceExaminePlanDetails query = new DeviceExaminePlanDetails();
        query.setPlanId(deviceExaminePlanId);
        query.setDeviceName(deviceName);
        query.setDeviceNumber(deviceNumber);
        List<DeviceExaminePlanDetailsDto> deviceExaminePlanDetailsDtoList =
                deviceExaminePlanDetailsMapper.listDeviceExaminePlanDetail(QueryWrappers.queryWrappers(query));
        // 查询设备核查计划详情
        List<DeviceExaminePlanDetails> deviceExaminePlanDetailsList = deviceExaminePlanDetailsMapper.selectList(Wrappers.<DeviceExaminePlanDetails>lambdaQuery().eq(DeviceExaminePlanDetails::getPlanId, deviceExaminePlanId)
                .orderByAsc(DeviceExaminePlanDetails::getCheckTime));
        // 设置序号
        ArrayList<DeviceExaminePlanDetailsDto> deviceExaminePlanDetailsDtoList = new ArrayList<>();
        deviceExaminePlanDetailsList.forEach(deviceExamineRecordContrastDetail -> {
            DeviceExaminePlanDetailsDto deviceExaminePlanDetailsDto = new DeviceExaminePlanDetailsDto();
            BeanUtils.copyProperties(deviceExamineRecordContrastDetail, deviceExaminePlanDetailsDto);
            deviceExaminePlanDetailsDto.setIndex(deviceExaminePlanDetailsList.indexOf(deviceExamineRecordContrastDetail) + 1);
            deviceExaminePlanDetailsDtoList.add(deviceExaminePlanDetailsDto);
        });
        String planName = StringUtils.isNotBlank(deviceExaminePlan.getPlanName()) ? deviceExaminePlan.getPlanName() : "设备核查计划";
        String[] columns = {"序号", "设备编号", "设备名称", "设备型号", "核查时间", "核查指标",
                "核查方法", "结果如何判定", "核查负责人", "审核状态", "备注"};
        List<String[]> dataRows = new ArrayList<>();
        int index = 1;
        for (DeviceExaminePlanDetailsDto detail : deviceExaminePlanDetailsDtoList) {
            String checkTime = StringUtils.isNotBlank(detail.getCompleteDate()) ? detail.getCompleteDate() : detail.getCheckTime();
            dataRows.add(new String[]{
                    String.valueOf(index++),
                    blankToEmpty(detail.getDeviceNumber()),
                    blankToEmpty(detail.getDeviceName()),
                    blankToEmpty(detail.getDeviceModel()),
                    blankToEmpty(checkTime),
                    blankToEmpty(detail.getCheckIndex()),
                    blankToEmpty(detail.getCheckMethod()),
                    blankToEmpty(detail.getHowResults()),
                    blankToEmpty(detail.getCheckChargerUser()),
                    recordStatusText(detail.getRecordStatus()),
                    blankToEmpty(detail.getRemark())
            });
        }
        // 获取路径
        InputStream inputStream = this.getClass().getResourceAsStream("/static/word/examine-plan-detail.docx");
        Configure configure = Configure.builder()
                .bind("deviceExaminePlanDetailsDtoList", new HackLoopTableRenderPolicy())
                .build();
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("deviceExaminePlan", deviceExaminePlanDto);
                    put("deviceExaminePlanDetailsDtoList", deviceExaminePlanDetailsDtoList);
                    //获取核查人的签名地址
                    put("writeUserUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceExaminePlanDto.getWriteUserId()));
                    //获取审查人的签名地址
                    put("reviewUserUrl", UserUtils.getFinalUserSignatureOrTransparent(deviceExaminePlanDto.getRatifyUserId()));
                }});
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("UTF-8");
        try {
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode(
                    "设备核查计划", "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            OutputStream os = response.getOutputStream();
            template.write(os);
            os.flush();
            os.close();
            inputStream.close();
                    "attachment;filename=" + URLEncoder.encode(planName, "UTF-8") + ".xlsx");
            writePlanExcel(response, planName, columns, dataRows);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("导出失败");
        }
    }
    /**
     * 写计划表:标题行 + 列名 + 数据,整体只加最外边框,全部居中,无背景色
     */
    private void writePlanExcel(HttpServletResponse response, String planName, String[] columns,
                                List<String[]> dataRows) throws IOException {
        Workbook workbook = new XSSFWorkbook();
        Map<String, CellStyle> styleCache = new HashMap<>();
        Font titleFont = buildExcelFont(workbook, true, 16);
        Font headerFont = buildExcelFont(workbook, true, 12);
        Font contentFont = buildExcelFont(workbook, false, 12);
        Sheet sheet = workbook.createSheet("设备核查计划");
        int lastColumn = columns.length - 1;
        Row titleRow = sheet.createRow(0);
        titleRow.setHeightInPoints(30);
        for (int c = 0; c <= lastColumn; c++) {
            Cell cell = titleRow.createCell(c);
            if (c == 0) {
                cell.setCellValue(planName);
            }
            cell.setCellStyle(exportStyle(workbook, styleCache, titleFont, c == 0, c == lastColumn, true, false));
        }
        sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, lastColumn));
        Row headerRow = sheet.createRow(1);
        headerRow.setHeightInPoints(22);
        for (int c = 0; c <= lastColumn; c++) {
            Cell cell = headerRow.createCell(c);
            cell.setCellValue(columns[c]);
            cell.setCellStyle(exportStyle(workbook, styleCache, headerFont,
                    c == 0, c == lastColumn, false, dataRows.isEmpty()));
        }
        for (int r = 0; r < dataRows.size(); r++) {
            Row row = sheet.createRow(r + 2);
            boolean lastDataRow = r == dataRows.size() - 1;
            for (int c = 0; c <= lastColumn; c++) {
                Cell cell = row.createCell(c);
                cell.setCellValue(dataRows.get(r)[c]);
                cell.setCellStyle(exportStyle(workbook, styleCache, contentFont,
                        c == 0, c == lastColumn, false, lastDataRow));
            }
        }
        for (int c = 0; c <= lastColumn; c++) {
            int width = textWidth(columns[c]);
            for (String[] row : dataRows) {
                width = Math.max(width, textWidth(row[c]));
            }
            sheet.setColumnWidth(c, Math.min(255, width + 4) * 256);
        }
        OutputStream os = response.getOutputStream();
        workbook.write(os);
        os.flush();
        workbook.close();
    }
    private Font buildExcelFont(Workbook workbook, boolean bold, int size) {
        Font font = workbook.createFont();
        font.setFontName("宋体");
        font.setBold(bold);
        font.setFontHeightInPoints((short) size);
        return font;
    }
    /**
     * 单元格样式:居中、无背景色;内部细网格线,整体最外圈粗边框
     */
    private CellStyle exportStyle(Workbook workbook, Map<String, CellStyle> styleCache, Font font,
                                  boolean outerLeft, boolean outerRight, boolean outerTop, boolean outerBottom) {
        String key = font.getBold() + "-" + font.getFontHeightInPoints() + "-" + (outerLeft ? 1 : 0)
                + (outerRight ? 1 : 0) + (outerTop ? 1 : 0) + (outerBottom ? 1 : 0);
        CellStyle cached = styleCache.get(key);
        if (cached != null) {
            return cached;
        }
        CellStyle style = workbook.createCellStyle();
        style.setFont(font);
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        if (outerLeft) {
            style.setBorderLeft(BorderStyle.MEDIUM);
        }
        if (outerRight) {
            style.setBorderRight(BorderStyle.MEDIUM);
        }
        if (outerTop) {
            style.setBorderTop(BorderStyle.MEDIUM);
        }
        if (outerBottom) {
            style.setBorderBottom(BorderStyle.MEDIUM);
        }
        styleCache.put(key, style);
        return style;
    }
    /**
     * 估算显示宽度(中文按两个字符宽)
     */
    private int textWidth(String text) {
        if (text == null) {
            return 0;
        }
        int width = 0;
        for (int i = 0; i < text.length(); i++) {
            width += text.charAt(i) > 0xFF ? 2 : 1;
        }
        return width;
    }
    /**
     * 记录状态文案:0未开始、1待批准、2通过、3不通过
     */
    private String recordStatusText(Integer recordStatus) {
        if (recordStatus == null) {
            return "未开始";
        }
        switch (recordStatus) {
            case 1:
                return "待批准";
            case 2:
                return "通过";
            case 3:
                return "不通过";
            default:
                return "未开始";
        }
    }
    private String blankToEmpty(String value) {
        return value == null ? "" : value;
    }
    @Override
@@ -409,6 +555,22 @@
            }
        }
        return planDto;
    }
    /**
     * 核查计划年份列表:删除没有任何明细的空年份计划后,返回有明细的年份(倒序)
     */
    @Override
    public List<String> getExaminePlanYears() {
        List<DeviceExaminePlan> plans = baseMapper.selectList(null);
        for (DeviceExaminePlan plan : plans) {
            Long detailCount = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
                    .eq(DeviceExaminePlanDetails::getPlanId, plan.getPlanId()));
            if (detailCount == null || detailCount == 0) {
                removeById(plan.getPlanId());
            }
        }
        return baseMapper.selectPlanYears();
    }
    /**
@@ -499,17 +661,32 @@
    }
    /**
     * 计划中尚无该设备本轮明细时自动补一条;核查时间取应核查月份
     * 计划中尚无该设备本轮明细时自动补一条;核查时间取档案下次核查日期(精确到日)
     */
    private void fillExamineDetailIfAbsent(DeviceExaminePlan plan, Device device) {
        LocalDate due = calcExamineDueDate(device);
        if (due == null) {
            return;
        }
        if (plan.getPlanYear() != null && String.valueOf(due.getYear()).compareTo(plan.getPlanYear()) > 0) {
        if (StringUtils.isBlank(plan.getPlanYear())) {
            return;
        }
        String checkTime = due.format(DateTimeFormatter.ofPattern("yyyy-MM"));
        int planYearValue;
        try {
            planYearValue = Integer.parseInt(plan.getPlanYear().trim());
        } catch (NumberFormatException e) {
            return;
        }
        int dueYear = due.getYear();
        // 未来年份的计划只登记应核查日期就在该年份的设备,避免把往年的旧日期补进未来的计划
        if (planYearValue > LocalDate.now().getYear()) {
            if (dueYear != planYearValue) {
                return;
            }
        } else if (dueYear > planYearValue) {
            return;
        }
        String checkTime = due.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        Long exist = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
                .eq(DeviceExaminePlanDetails::getPlanId, plan.getPlanId())
                .eq(DeviceExaminePlanDetails::getDeviceId, device.getId())
@@ -682,12 +859,13 @@
                .eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId));
        List<DeviceExamineRecordContrast> contrasts = deviceExamineRecordContrastMapper.selectList(Wrappers.<DeviceExamineRecordContrast>lambdaQuery()
                .eq(DeviceExamineRecordContrast::getPlanDetailsId, planDetailsId));
        boolean hasReviewedRecord = records.stream().anyMatch(item -> item.getReviewStatus() != null
                && (item.getReviewStatus() == 0 || item.getReviewStatus() == 1));
        boolean hasReviewedContrast = contrasts.stream().anyMatch(item -> item.getReviewStatus() != null
                && (item.getReviewStatus() == 0 || item.getReviewStatus() == 1));
        if (hasReviewedRecord || hasReviewedContrast) {
            throw new ErrorException("该计划已存在审核完成的核查记录,为保留历史记录不允许删除");
        // 审核不通过的记录允许连同明细一起删除;审核通过的记录为历史留档,不允许删除
        boolean hasPassedRecord = records.stream().anyMatch(item -> item.getReviewStatus() != null
                && item.getReviewStatus() == 1);
        boolean hasPassedContrast = contrasts.stream().anyMatch(item -> item.getReviewStatus() != null
                && item.getReviewStatus() == 1);
        if (hasPassedRecord || hasPassedContrast) {
            throw new ErrorException("该计划已存在审核通过的核查记录,为保留历史记录不允许删除");
        }
        if (!records.isEmpty()) {
            List<Integer> recordIds = records.stream().map(DeviceExamineRecord::getRecordId).collect(Collectors.toList());