cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java
@@ -3,30 +3,57 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.dto.DeviceRecordDto;
import com.ruoyi.device.excel.DeviceRecordExportWord;
import com.ruoyi.device.mapper.DeviceMapper;
import com.ruoyi.device.mapper.DeviceRecordMapper;
import com.ruoyi.device.pojo.Device;
import com.ruoyi.device.pojo.DeviceRecord;
import com.ruoyi.device.service.DeviceRecordService;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.system.domain.vo.SysRoleVo;
import com.ruoyi.system.mapper.SysRoleMenuMapper;
import org.springframework.beans.BeanUtils;
import org.apache.poi.xwpf.usermodel.BreakType;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.TableRowAlign;
import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblCellMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblLayoutType;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTrPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblLayoutType;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -40,14 +67,24 @@
 */
@Service
public class DeviceRecordServiceImpl extends ServiceImpl<DeviceRecordMapper, DeviceRecord> implements DeviceRecordService {
    @Resource
    private DeviceMapper deviceMapper;
    private static final String FORM_TITLE = "仪器设备运行记录";
    private static final String RECORD_NUMBER = "LMSY-CX07-JL03";
    private static final String FONT_NAME = "宋体";
    private static final int[] COLUMN_WIDTHS = {1622, 4168, 1288, 1107, 1137, 1076, 940, 1182, 1288, 1350};
    private static final String[] HEADER_FIRST_ROW = {"使用日期", "样品名称", "时间 24 小时制", "", "设备状况", "", "环境条件", "", "使用人", "备注"};
    private static final String[] HEADER_SECOND_ROW = {"", "", "开始", "结束", "使用前", "使用后", "℃", "%RH", "", ""};
    private static final String FOOTNOTE = "注:设备使用前后一栏,正常划“√”;不正常划“×”并写明情况。";
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
    @Resource
    private SysRoleMenuMapper sysRoleMenuMapper;
    @Override
    public IPage<DeviceRecordDto> deviceRecordPage(Integer deviceId, Page page, String sampleCode, String managementNumber) {
    public IPage<DeviceRecordDto> deviceRecordPage(Integer deviceId, Page page, String deviceName,
                                                   String specificationModel, String sampleCode,
                                                   String managementNumber) {
        // todo:仅看自己
        Long userId = SecurityUtils.getUserId();
        // 获取权限判断是否是仅看我
@@ -59,90 +96,390 @@
            testUserId = SecurityUtils.getUserId().intValue();
        }
        return baseMapper.deviceRecordPage(deviceId, page, sampleCode, managementNumber, testUserId);
        return baseMapper.deviceRecordPage(deviceId, page, deviceName, specificationModel,
                sampleCode, managementNumber, testUserId);
    }
    @Override
    public void exportUseRecord(Integer deviceId, String sampleCode, String managementNumber, String exportDate, HttpServletResponse response) {
        // 查询cnas设备使用记录(按当前筛选条件, 条件为空时导出全部)
        List<DeviceRecord> deviceList = baseMapper.selectExportList(deviceId, sampleCode, managementNumber, exportDate);
        // 设备信息(未选中具体设备时导出全部设备的记录, 设备信息留空)
        Device device = deviceId == null ? null : deviceMapper.selectById(deviceId);
        // 查询设备属于哪个实验室
        String laboratoryName = "装备线缆实验室";
        // 要映射到word数据
        List<DeviceRecordExportWord> deviceExportList = new ArrayList<>();
        // deviceExportList 赋值
        for (DeviceRecord deviceRecord : deviceList) {
            // 处理设备 开始使用时间 和 结束时间
            String startTime = "";
            String endTime = "";
            String operationDate = "";
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
            if (deviceRecord.getUseStartDate() != null) {
                startTime = deviceRecord.getUseStartDate().format(formatter);
                endTime = deviceRecord.getUseEndDate().format(formatter);
                String[] startTimeSplit = startTime.split(" ");
                String[] endTimeSplit = endTime.split(" ");
                if (startTimeSplit[0].equals(endTimeSplit[0])) {
                    startTime = startTimeSplit[1];
                    endTime = endTimeSplit[1];
                } else {
                    endTime = endTimeSplit[0] + '\n' + endTimeSplit[1];
                    startTime = startTimeSplit[0] + '\n' + startTimeSplit[1];
                }
                DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy.M.d");
                operationDate = deviceRecord.getUseStartDate().format(formatter2);
            }
            // 处理使用前和使用后
            String useBeforeString = deviceRecord.getUseBefore() == 0? "异常" : "良好";
            String useAfterString = deviceRecord.getUseAfter() == 0 ? "异常" : "良好";
            DeviceRecordExportWord deviceRecordExportWord = new DeviceRecordExportWord();
            // 进行赋值
            BeanUtils.copyProperties(deviceRecord, deviceRecordExportWord);
            deviceRecordExportWord.setUseBeforeString(useBeforeString); // 使用前
            deviceRecordExportWord.setUseAfterString(useAfterString); // 使用后
            deviceRecordExportWord.setOperationDate(operationDate); // 设备操作日期
            deviceRecordExportWord.setUseStartDateString(startTime); // 开始时间
            deviceRecordExportWord.setUseEndDateString(endTime); // 结束时间
            deviceExportList.add(deviceRecordExportWord);
    public void exportUseRecord(Integer deviceId, String deviceName, String specificationModel,
                                String managementNumber, String sampleCode, HttpServletResponse response) {
        List<DeviceRecordDto> records = baseMapper.selectExportList(deviceId, deviceName,
                specificationModel, managementNumber, sampleCode);
        if (records == null || records.isEmpty()) {
            throw new ServiceException("没有可导出的使用记录,无法导出");
        }
        // 获取路径
        InputStream inputStream = this.getClass().getResourceAsStream("/static/word/use-record.docx");
        Configure configure = Configure.builder()
                .bind("useRecord", new HackLoopTableRenderPolicy())
                .build();
        String deviceName = device == null ? "" : device.getDeviceName();
        String deviceManagementNumber = device == null ? "" : device.getManagementNumber();
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("useRecord", deviceExportList);
                    put("deviceName", deviceName);
                    put("managementNumber", deviceManagementNumber);
                    put("laboratory", laboratoryName);
                }});
        // 一台设备一本运行记录,多台设备在同一 Word 内按设备分页
        Map<Integer, List<DeviceRecordDto>> deviceRecords = new LinkedHashMap<>();
        for (DeviceRecordDto record : records) {
            deviceRecords.computeIfAbsent(record.getDeviceId(), key -> new ArrayList<>()).add(record);
        }
        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();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("导出失败");
        try (XWPFDocument document = new XWPFDocument()) {
            setupPage(document);
            boolean first = true;
            for (List<DeviceRecordDto> group : deviceRecords.values()) {
                if (!first) {
                    addPageBreak(document);
                }
                renderForm(document, group.get(0), mergeSameConditionRecords(group));
                first = false;
            }
            response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
            String fileName = URLEncoder.encode(FORM_TITLE, StandardCharsets.UTF_8).replace("+", "%20");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".docx");
            try (OutputStream os = response.getOutputStream()) {
                document.write(os);
                os.flush();
            }
        } catch (IOException e) {
            throw new RuntimeException("导出失败", e);
        }
    }
    /**
     * 同一设备、同一天、起止时间/温湿度/使用人/使用前后状况相同的记录合并一行,样品编号用“、”拼接。
     */
    private List<DeviceRecordDto> mergeSameConditionRecords(List<DeviceRecordDto> records) {
        Map<String, DeviceRecordDto> merged = new LinkedHashMap<>();
        for (DeviceRecordDto record : records) {
            String key = String.join("|",
                    formatDate(record.getUseStartDate()),
                    formatTime(record.getUseStartDate()),
                    formatTime(record.getUseEndDate()),
                    valueOrEmpty(record.getTemperature()),
                    valueOrEmpty(record.getHumidity()),
                    valueOrEmpty(record.getUsePerson()),
                    String.valueOf(record.getUseBefore()),
                    String.valueOf(record.getUseAfter()));
            DeviceRecordDto existing = merged.get(key);
            if (existing == null) {
                merged.put(key, record);
            } else {
                existing.setSampleCode(appendDistinct(existing.getSampleCode(), record.getSampleCode()));
                existing.setRemark(appendDistinct(existing.getRemark(), record.getRemark()));
            }
        }
        return new ArrayList<>(merged.values());
    }
    private String appendDistinct(String existing, String addition) {
        if (addition == null || addition.trim().isEmpty()) {
            return existing;
        }
        if (existing == null || existing.trim().isEmpty()) {
            return addition;
        }
        List<String> parts = java.util.Arrays.asList(existing.split("、"));
        return parts.contains(addition) ? existing : existing + "、" + addition;
    }
    private void addPageBreak(XWPFDocument document) {
        XWPFParagraph paragraph = document.createParagraph();
        paragraph.setSpacingAfter(0);
        paragraph.createRun().addBreak(BreakType.PAGE);
    }
    private void setupPage(XWPFDocument document) {
        CTBody body = document.getDocument().getBody();
        CTSectPr sectPr = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
        CTPageSz pageSz = sectPr.isSetPgSz() ? sectPr.getPgSz() : sectPr.addNewPgSz();
        pageSz.setW(BigInteger.valueOf(16838));
        pageSz.setH(BigInteger.valueOf(11906));
        pageSz.setOrient(STPageOrientation.LANDSCAPE);
        CTPageMar pageMar = sectPr.isSetPgMar() ? sectPr.getPgMar() : sectPr.addNewPgMar();
        pageMar.setTop(BigInteger.valueOf(680));
        pageMar.setBottom(BigInteger.valueOf(680));
        pageMar.setLeft(BigInteger.valueOf(794));
        pageMar.setRight(BigInteger.valueOf(794));
        pageMar.setHeader(BigInteger.valueOf(851));
        pageMar.setFooter(BigInteger.valueOf(992));
        pageMar.setGutter(BigInteger.ZERO);
    }
    private void renderForm(XWPFDocument document, DeviceRecordDto device, List<DeviceRecordDto> records) {
        XWPFParagraph recordNumber = addParagraph(document, "记录编号:" + RECORD_NUMBER, 10.5, false, ParagraphAlignment.LEFT);
        recordNumber.setSpacingAfter(240);
        XWPFParagraph title = addParagraph(document, FORM_TITLE, 22, true, ParagraphAlignment.CENTER);
        title.setSpacingAfter(240);
        renderDeviceInfo(document, device);
        document.createParagraph();
        XWPFTable table = document.createTable(2 + records.size(), COLUMN_WIDTHS.length);
        configureTable(table, COLUMN_WIDTHS);
        for (int i = 0; i < table.getNumberOfRows(); i++) {
            XWPFTableRow row = table.getRow(i);
            row.setHeight(i < 2 ? 560 : 520);
            for (int j = 0; j < COLUMN_WIDTHS.length; j++) {
                setCellWidth(row.getCell(j), COLUMN_WIDTHS[j]);
            }
            setRowProperties(row, i < 2);
        }
        XWPFTableRow firstHeader = table.getRow(0);
        for (int i = 0; i < HEADER_FIRST_ROW.length; i++) {
            setCellText(firstHeader.getCell(i), HEADER_FIRST_ROW[i]);
        }
        XWPFTableRow secondHeader = table.getRow(1);
        for (int i = 0; i < HEADER_SECOND_ROW.length; i++) {
            setCellText(secondHeader.getCell(i), HEADER_SECOND_ROW[i]);
        }
        mergeHorizontal(firstHeader, COLUMN_WIDTHS, 6, 2);
        mergeHorizontal(firstHeader, COLUMN_WIDTHS, 4, 2);
        mergeHorizontal(firstHeader, COLUMN_WIDTHS, 2, 2);
        mergeVertical(firstHeader.getCell(0), secondHeader.getCell(0));
        mergeVertical(firstHeader.getCell(1), secondHeader.getCell(1));
        mergeVertical(firstHeader.getCell(5), secondHeader.getCell(8));
        mergeVertical(firstHeader.getCell(6), secondHeader.getCell(9));
        for (int i = 0; i < records.size(); i++) {
            XWPFTableRow row = table.getRow(2 + i);
            String[] values = buildRowValues(records.get(i));
            for (int j = 0; j < values.length; j++) {
                setCellText(row.getCell(j), values[j]);
            }
        }
        XWPFParagraph footnote = addParagraph(document, FOOTNOTE, 10.5, false, ParagraphAlignment.LEFT);
        footnote.setSpacingBefore(120);
        footnote.setSpacingAfter(0);
    }
    private XWPFParagraph addParagraph(XWPFDocument document, String text, double fontSize, boolean bold, ParagraphAlignment alignment) {
        XWPFParagraph paragraph = document.createParagraph();
        paragraph.setAlignment(alignment);
        addRun(paragraph, text, fontSize, bold);
        return paragraph;
    }
    private void addDeviceField(XWPFParagraph paragraph, String label, String value) {
        addRun(paragraph, label, 10.5, false);
        XWPFRun valueRun = addRun(paragraph, padUnderline(value), 10.5, false);
        valueRun.setUnderline(UnderlinePatterns.SINGLE);
    }
    private String padUnderline(String value) {
        String content = value == null ? "" : value.trim();
        int width = 0;
        for (int i = 0; i < content.length(); i++) {
            width += content.charAt(i) >= 0x2E80 ? 2 : 1;
        }
        StringBuilder text = new StringBuilder(content);
        for (int i = width; i < 20; i++) {
            text.append(' ');
        }
        return text.toString();
    }
    private XWPFRun addRun(XWPFParagraph paragraph, String text, double fontSize, boolean bold) {
        XWPFRun run = paragraph.createRun();
        run.setFontFamily(FONT_NAME);
        run.setFontFamily(FONT_NAME, XWPFRun.FontCharRange.eastAsia);
        run.setFontSize(fontSize);
        run.setBold(bold);
        run.setText(text);
        return run;
    }
    private void renderDeviceInfo(XWPFDocument document, DeviceRecordDto device) {
        String[] labels = {"设备名称:", "设备型号:", "设备编号:"};
        String[] values = {
                device == null ? "" : device.getDeviceName(),
                device == null ? "" : device.getSpecificationModel(),
                device == null ? "" : device.getManagementNumber()
        };
        int[] widths = {5052, 5053, 5053};
        XWPFTable table = document.createTable(1, labels.length);
        CTTbl ctTbl = table.getCTTbl();
        CTTblPr tblPr = ctTbl.getTblPr() != null ? ctTbl.getTblPr() : ctTbl.addNewTblPr();
        CTTblWidth tblWidth = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
        tblWidth.setType(STTblWidth.DXA);
        tblWidth.setW(BigInteger.valueOf(totalWidth(COLUMN_WIDTHS)));
        CTTblLayoutType layoutType = tblPr.isSetTblLayout() ? tblPr.getTblLayout() : tblPr.addNewTblLayout();
        layoutType.setType(STTblLayoutType.FIXED);
        setNoBorders(tblPr);
        CTTblCellMar cellMar = tblPr.isSetTblCellMar() ? tblPr.getTblCellMar() : tblPr.addNewTblCellMar();
        setZeroWidth(cellMar.isSetLeft() ? cellMar.getLeft() : cellMar.addNewLeft());
        setZeroWidth(cellMar.isSetRight() ? cellMar.getRight() : cellMar.addNewRight());
        CTTblGrid grid = ctTbl.getTblGrid() != null ? ctTbl.getTblGrid() : ctTbl.addNewTblGrid();
        while (grid.sizeOfGridColArray() > 0) {
            grid.removeGridCol(0);
        }
        for (int width : widths) {
            grid.addNewGridCol().setW(BigInteger.valueOf(width));
        }
        XWPFTableRow row = table.getRow(0);
        for (int i = 0; i < labels.length; i++) {
            XWPFTableCell cell = row.getCell(i);
            setCellWidth(cell, widths[i]);
            cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
            XWPFParagraph paragraph = cell.getParagraphs().isEmpty() ? cell.addParagraph() : cell.getParagraphs().get(0);
            paragraph.setAlignment(ParagraphAlignment.LEFT);
            paragraph.setSpacingBefore(0);
            paragraph.setSpacingAfter(0);
            addDeviceField(paragraph, labels[i], values[i]);
        }
    }
    private void setNoBorders(CTTblPr tblPr) {
        CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders();
        setNoBorder(borders.isSetTop() ? borders.getTop() : borders.addNewTop());
        setNoBorder(borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft());
        setNoBorder(borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom());
        setNoBorder(borders.isSetRight() ? borders.getRight() : borders.addNewRight());
        setNoBorder(borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH());
        setNoBorder(borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV());
    }
    private void setNoBorder(CTBorder border) {
        border.setVal(STBorder.NIL);
        border.setSz(BigInteger.ZERO);
        border.setSpace(BigInteger.ZERO);
        border.setColor("auto");
    }
    private void setZeroWidth(CTTblWidth width) {
        width.setType(STTblWidth.DXA);
        width.setW(BigInteger.ZERO);
    }
    private void configureTable(XWPFTable table, int[] widths) {
        table.setTableAlignment(TableRowAlign.CENTER);
        CTTbl ctTbl = table.getCTTbl();
        CTTblPr tblPr = ctTbl.getTblPr() != null ? ctTbl.getTblPr() : ctTbl.addNewTblPr();
        CTTblWidth tblWidth = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
        tblWidth.setType(STTblWidth.DXA);
        tblWidth.setW(BigInteger.valueOf(totalWidth(widths)));
        CTTblLayoutType layoutType = tblPr.isSetTblLayout() ? tblPr.getTblLayout() : tblPr.addNewTblLayout();
        layoutType.setType(STTblLayoutType.FIXED);
        CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders();
        setBorder(borders.isSetTop() ? borders.getTop() : borders.addNewTop());
        setBorder(borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft());
        setBorder(borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom());
        setBorder(borders.isSetRight() ? borders.getRight() : borders.addNewRight());
        setBorder(borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH());
        setBorder(borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV());
        CTTblGrid grid = ctTbl.getTblGrid() != null ? ctTbl.getTblGrid() : ctTbl.addNewTblGrid();
        while (grid.sizeOfGridColArray() > 0) {
            grid.removeGridCol(0);
        }
        for (int width : widths) {
            grid.addNewGridCol().setW(BigInteger.valueOf(width));
        }
    }
    private int totalWidth(int[] widths) {
        int total = 0;
        for (int width : widths) {
            total += width;
        }
        return total;
    }
    private void setBorder(CTBorder border) {
        border.setVal(STBorder.SINGLE);
        border.setSz(BigInteger.valueOf(4));
        border.setSpace(BigInteger.ZERO);
        border.setColor("000000");
    }
    private void setCellWidth(XWPFTableCell cell, int width) {
        CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
        CTTblWidth tcW = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
        tcW.setType(STTblWidth.DXA);
        tcW.setW(BigInteger.valueOf(width));
    }
    private void setCellText(XWPFTableCell cell, String text) {
        cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
        XWPFParagraph paragraph = cell.getParagraphs().isEmpty() ? cell.addParagraph() : cell.getParagraphs().get(0);
        paragraph.setAlignment(ParagraphAlignment.CENTER);
        paragraph.setSpacingBefore(0);
        paragraph.setSpacingAfter(0);
        addRun(paragraph, text == null ? "" : text, 10.5, false);
    }
    private void setRowProperties(XWPFTableRow row, boolean repeatHeader) {
        CTRow ctRow = row.getCtRow();
        CTTrPr trPr = ctRow.isSetTrPr() ? ctRow.getTrPr() : ctRow.addNewTrPr();
        trPr.addNewCantSplit();
        if (repeatHeader) {
            trPr.addNewTblHeader();
        }
    }
    private void mergeHorizontal(XWPFTableRow row, int[] widths, int firstIndex, int span) {
        int width = 0;
        for (int i = firstIndex; i < firstIndex + span; i++) {
            width += widths[i];
        }
        XWPFTableCell first = row.getCell(firstIndex);
        setCellWidth(first, width);
        CTTcPr tcPr = first.getCTTc().isSetTcPr() ? first.getCTTc().getTcPr() : first.getCTTc().addNewTcPr();
        (tcPr.isSetGridSpan() ? tcPr.getGridSpan() : tcPr.addNewGridSpan()).setVal(BigInteger.valueOf(span));
        for (int i = firstIndex + span - 1; i > firstIndex; i--) {
            row.removeCell(i);
        }
    }
    private void mergeVertical(XWPFTableCell first, XWPFTableCell second) {
        setVMerge(first, STMerge.RESTART);
        setVMerge(second, STMerge.CONTINUE);
    }
    private void setVMerge(XWPFTableCell cell, STMerge.Enum value) {
        CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
        (tcPr.isSetVMerge() ? tcPr.getVMerge() : tcPr.addNewVMerge()).setVal(value);
    }
    private String[] buildRowValues(DeviceRecordDto record) {
        return new String[]{
                formatDate(record.getUseStartDate()),
                valueOrEmpty(record.getSampleCode()),
                formatTime(record.getUseStartDate()),
                formatTime(record.getUseEndDate()),
                statusText(record.getUseBefore()),
                statusText(record.getUseAfter()),
                valueOrEmpty(record.getTemperature()),
                valueOrEmpty(record.getHumidity()),
                valueOrEmpty(record.getUsePerson()),
                valueOrEmpty(record.getRemark())
        };
    }
    private String formatDate(LocalDateTime value) {
        return value == null ? "" : value.format(DATE_FORMATTER);
    }
    private String formatTime(LocalDateTime value) {
        return value == null ? "" : value.format(TIME_FORMATTER);
    }
    private String statusText(Integer value) {
        if (value == null) {
            return "";
        }
        return value == 0 ? "异常" : "正常";
    }
    private String valueOrEmpty(String value) {
        return value == null ? "" : value;
    }
}