From bcbdb084c3a3a0a4de8fca1aa16caf0bc4681ddf Mon Sep 17 00:00:00 2001
From: 吴羡 <16976746+Xian-TT3@user.noreply.gitee.com>
Date: 星期二, 27 十月 2026 09:41:51 +0800
Subject: [PATCH] feat(device): 删除核查计划明细时连带清理未审核记录和附件,已审核记录仍禁止删除

---
 cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java |  524 ++++++++++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 438 insertions(+), 86 deletions(-)

diff --git a/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java b/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java
index d49ccbb..d4fc412 100644
--- a/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java
+++ b/cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceRecordServiceImpl.java
@@ -3,30 +3,56 @@
 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.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,8 +66,18 @@
  */
 @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 int[] COMBINED_COLUMN_WIDTHS = {1300, 2000, 1500, 1600, 2000, 900, 900, 800, 800, 650, 750, 950, 1008};
+    private static final String[] COMBINED_HEADER_FIRST_ROW = {"浣跨敤鏃ユ湡", "璁惧鍚嶇О", "璁惧鍨嬪彿", "璁惧缂栧彿", "鏍峰搧缂栧彿", "鏃堕棿 24 灏忔椂鍒�", "", "璁惧鐘跺喌", "", "鐜鏉′欢", "", "浣跨敤浜�", "澶囨敞"};
+    private static final String[] COMBINED_HEADER_SECOND_ROW = {"", "", "", "", "", "寮�濮�", "缁撴潫", "浣跨敤鍓�", "浣跨敤鍚�", "鈩�", "%RH", "", ""};
+    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;
@@ -62,87 +98,403 @@
         return baseMapper.deviceRecordPage(deviceId, page, 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);
+        List<DeviceRecordDto> records = baseMapper.selectExportList(deviceId, sampleCode, managementNumber, exportDate);
+        if (records == null || records.isEmpty()) {
+            throw new ServiceException(exportDate == null || exportDate.isEmpty()
+                    ? "娌℃湁鍙鍑虹殑浣跨敤璁板綍锛屾棤娉曞鍑�"
+                    : exportDate + " 娌℃湁鍙鍑虹殑浣跨敤璁板綍锛屾棤娉曞鍑�");
         }
 
-        // 鑾峰彇璺緞
-        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);
-                }});
+        // 鍗曞彴璁惧娌跨敤鎴浘琛ㄥご; 澶氬彴璁惧鍚堝苟鍒颁竴寮犺〃, 姣忚甯﹁澶囦俊鎭�
+        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);
+            if (deviceRecords.size() == 1) {
+                List<DeviceRecordDto> group = deviceRecords.values().iterator().next();
+                renderForm(document, group.get(0), group);
+            } else {
+                renderCombinedForm(document, records);
+            }
+
+            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 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]);
+            }
+        }
+    }
+
+    private void renderCombinedForm(XWPFDocument document, 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);
+
+        XWPFTable table = document.createTable(2 + records.size(), COMBINED_COLUMN_WIDTHS.length);
+        configureTable(table, COMBINED_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 < COMBINED_COLUMN_WIDTHS.length; j++) {
+                setCellWidth(row.getCell(j), COMBINED_COLUMN_WIDTHS[j]);
+            }
+            setRowProperties(row, i < 2);
+        }
+
+        XWPFTableRow firstHeader = table.getRow(0);
+        for (int i = 0; i < COMBINED_HEADER_FIRST_ROW.length; i++) {
+            setCellText(firstHeader.getCell(i), COMBINED_HEADER_FIRST_ROW[i]);
+        }
+        XWPFTableRow secondHeader = table.getRow(1);
+        for (int i = 0; i < COMBINED_HEADER_SECOND_ROW.length; i++) {
+            setCellText(secondHeader.getCell(i), COMBINED_HEADER_SECOND_ROW[i]);
+        }
+
+        mergeHorizontal(firstHeader, COMBINED_COLUMN_WIDTHS, 9, 2);
+        mergeHorizontal(firstHeader, COMBINED_COLUMN_WIDTHS, 7, 2);
+        mergeHorizontal(firstHeader, COMBINED_COLUMN_WIDTHS, 5, 2);
+        mergeVertical(firstHeader.getCell(0), secondHeader.getCell(0));
+        mergeVertical(firstHeader.getCell(1), secondHeader.getCell(1));
+        mergeVertical(firstHeader.getCell(2), secondHeader.getCell(2));
+        mergeVertical(firstHeader.getCell(3), secondHeader.getCell(3));
+        mergeVertical(firstHeader.getCell(4), secondHeader.getCell(4));
+        mergeVertical(firstHeader.getCell(8), secondHeader.getCell(11));
+        mergeVertical(firstHeader.getCell(9), secondHeader.getCell(12));
+
+        for (int i = 0; i < records.size(); i++) {
+            XWPFTableRow row = table.getRow(2 + i);
+            String[] values = buildCombinedRowValues(records.get(i));
+            for (int j = 0; j < values.length; j++) {
+                setCellText(row.getCell(j), values[j]);
+            }
+        }
+    }
+
+    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[] buildCombinedRowValues(DeviceRecordDto record) {
+        return new String[]{
+                formatDate(record.getUseStartDate()),
+                valueOrEmpty(record.getDeviceName()),
+                valueOrEmpty(record.getSpecificationModel()),
+                valueOrEmpty(record.getManagementNumber()),
+                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;
+    }
 }

--
Gitblit v1.9.3