12 小时以前 453fece707fc1e611554fbb10125383eba01449a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
package cn.iocoder.yudao.module.mes.service.qc.indicatorresult;
 
import cn.iocoder.yudao.module.mes.controller.admin.qc.indicatorresult.vo.MesQcQcReportRespVO;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcIndicatorItemTypeEnum;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
 
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
 
/**
 * 产品检验单(纸质"××产品质量检验单"版式)Word 构建器
 *
 * 版式:标题(公司 + 产品检验单)→ 表头信息 5 行 → 检验项目/检验结果 两列矩阵 →
 * 判定结论/质检专用章(留空,打印后手填)→ 备注 → 检验/复核/批准签字行。
 * 不含技术指标列;分组项不展示,录入项平铺;平行测量多值顿号合并。
 */
public class MesQcProductReportWordBuilder {
 
    /** 生产单位固定文案(与纸质报告一致,写死) */
    private static final String COMPANY_NAME = "宁夏九泓化工科技有限公司";
 
    /** 备注默认文案(无单据备注时使用,与纸质报告一致) */
    private static final String DEFAULT_REMARK = "检验结果仅对样品本身或本批次负责。";
 
    /** 空值占位符,与纸质报告 "/" 写法一致 */
    private static final String EMPTY = "/";
 
    /** 四列表宽(twips),总宽 9000 ≈ 15.9cm,适配 A4 竖版 2.5cm 页边距 */
    private static final int[] COL_WIDTHS = {1700, 2800, 1700, 2800};
    private static final int TABLE_WIDTH = 9000;
 
    private static final String FONT = "宋体";
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
 
    private MesQcProductReportWordBuilder() {
    }
 
    /**
     * 构建产品检验单 docx 字节
     */
    public static byte[] build(MesQcQcReportRespVO report) throws IOException {
        try (XWPFDocument doc = new XWPFDocument(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            setupPage(doc);
            writeTitle(doc, report);
            writeHeaderTable(doc, report);
            writeItemTable(doc, report);
            writeConclusionTable(doc);
            writeRemarkTable(doc, report);
            writeSignLine(doc);
            doc.write(out);
            return out.toByteArray();
        }
    }
 
    // ========== 页面/标题 ==========
 
    private static void setupPage(XWPFDocument doc) {
        CTBody body = doc.getDocument().getBody();
        CTSectPr sectPr = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
        // A4 竖版
        CTPageSz pageSz = sectPr.addNewPgSz();
        pageSz.setW(BigInteger.valueOf(11906));
        pageSz.setH(BigInteger.valueOf(16838));
        CTPageMar pageMar = sectPr.addNewPgMar();
        BigInteger margin = BigInteger.valueOf(1418); // 2.5cm
        pageMar.setLeft(margin);
        pageMar.setRight(margin);
        pageMar.setTop(margin);
        pageMar.setBottom(margin);
    }
 
    private static void writeTitle(XWPFDocument doc, MesQcQcReportRespVO report) {
        String product = notBlank(report.getItemName()) ? report.getItemName()
                : (notBlank(report.getDocName()) ? report.getDocName() : "产品");
        addParagraph(doc, COMPANY_NAME, 16, true, ParagraphAlignment.CENTER);
        addParagraph(doc, product + "产品质量检验单", 14, true, ParagraphAlignment.CENTER);
    }
 
    // ========== 表头信息 ==========
 
    private static XWPFTable writeHeaderTable(XWPFDocument doc, MesQcQcReportRespVO report) {
        XWPFTable table = createTable(doc);
        addRow(table, label("生产单位"), value(COMPANY_NAME, 3));
        addRow(table, label("生产时间"), value(formatDate(report.getDocCreateTime())),
                label("批    号"), value(blank(report.getBatchNo())));
        addRow(table, label("采样地点"), value(blank(report.getSamplingLocation())),
                label("批    量"), value(formatQuantity(report.getBatchQuantity())));
        addRow(table, label("采 样 者"), value(blank(report.getInspectorName())),
                label("取样日期"), value(formatDate(report.getSamplingTime())));
        addRow(table, label("执行标准"), value(blank(report.getExecuteStandard())),
                label("检验日期"), value(formatDate(report.getInspectDate())));
        return table;
    }
 
    // ========== 检验项目矩阵 ==========
 
    private static XWPFTable writeItemTable(XWPFDocument doc, MesQcQcReportRespVO report) {
        XWPFTable table = createTable(doc);
        addRow(table, labelSpan("检验项目", 2), labelSpan("检验结果", 2));
        List<MesQcQcReportRespVO.ReportItem> records = new ArrayList<>();
        collectRecordItems(report.getItems(), records);
        if (records.isEmpty()) {
            addRow(table, valueSpan(EMPTY, 2), valueSpan(EMPTY, 2));
            return table;
        }
        for (MesQcQcReportRespVO.ReportItem item : records) {
            addRow(table, valueSpan(blank(item.getIndicatorName()), 2), valueSpan(joinMeasures(item), 2));
        }
        return table;
    }
 
    /**
     * 平铺录入项:分组项(itemType=2)不展示行本身,仅展开其子录入项
     */
    private static void collectRecordItems(List<MesQcQcReportRespVO.ReportItem> nodes,
                                           List<MesQcQcReportRespVO.ReportItem> out) {
        if (nodes == null) {
            return;
        }
        for (MesQcQcReportRespVO.ReportItem node : nodes) {
            boolean group = node.getItemType() != null
                    && node.getItemType().equals(MesQcIndicatorItemTypeEnum.GROUP.getType());
            if (group) {
                collectRecordItems(node.getChildren(), out);
            } else {
                out.add(node);
            }
        }
    }
 
    /**
     * 平行测量多值顿号合并;无值时 "/"
     */
    private static String joinMeasures(MesQcQcReportRespVO.ReportItem item) {
        List<MesQcQcReportRespVO.ReportMeasure> measures = item.getMeasures();
        if (measures == null || measures.isEmpty()) {
            return EMPTY;
        }
        return measures.stream()
                .map(MesQcQcReportRespVO.ReportMeasure::getValue)
                .filter(MesQcProductReportWordBuilder::notBlank)
                .reduce((a, b) -> a + "、" + b)
                .orElse(EMPTY);
    }
 
    // ========== 判定结论 / 质检专用章(留空后填) ==========
 
    private static XWPFTable writeConclusionTable(XWPFDocument doc) {
        XWPFTable table = createTable(doc);
        XWPFTableRow row = addRow(table, valueLeftSpan("判定结论:", 2), valueLeftSpan("质检专用章:", 2));
        // 预留手写/盖章高度
        setRowHeight(row, 2550);
        return table;
    }
 
    // ========== 备注 ==========
 
    private static XWPFTable writeRemarkTable(XWPFDocument doc, MesQcQcReportRespVO report) {
        XWPFTable table = createTable(doc);
        return addRowTo(table, label("备    注"),
                valueLeftSpan(notBlank(report.getRemark()) ? report.getRemark() : DEFAULT_REMARK, 3));
    }
 
    // ========== 签字行 ==========
 
    /** 签字行固定留空,与判定结论/质检专用章一致,打印后手写 */
    private static void writeSignLine(XWPFDocument doc) {
        XWPFParagraph paragraph = doc.createParagraph();
        paragraph.setAlignment(ParagraphAlignment.LEFT);
        paragraph.setSpacingBefore(400);
        addRun(paragraph, "检验:         复核:         批准:        ", 12, true);
    }
 
    // ========== 表格基础 ==========
 
    private static XWPFTable createTable(XWPFDocument doc) {
        // POI 默认表自带 1 个空行 + tblPr(含全边框),删掉默认行复用其 tblPr 即可
        XWPFTable table = doc.createTable();
        while (table.getNumberOfRows() > 0) {
            table.removeRow(0);
        }
        CTTbl ctTbl = table.getCTTbl();
        CTTblPr tblPr = ctTbl.getTblPr();
        CTTblWidth tblW = tblPr.getTblW();
        tblW.setType(STTblWidth.DXA);
        tblW.setW(BigInteger.valueOf(TABLE_WIDTH));
        if (!tblPr.isSetTblLayout()) {
            tblPr.addNewTblLayout().setType(STTblLayoutType.FIXED);
        }
        // POI 新建的表不含 tblGrid,直接补上
        CTTblGrid grid = ctTbl.addNewTblGrid();
        for (int width : COL_WIDTHS) {
            grid.addNewGridCol().setW(BigInteger.valueOf(width));
        }
        return table;
    }
 
    /**
     * 追加一行;每个片段按给定 span 生成合并单元格,span 合计须为 4
     */
    private static XWPFTableRow addRow(XWPFTable table, CellSpec... specs) {
        XWPFTableRow row = table.createRow();
        // createRow() 会按首行列数自动补格,清掉后按片段重建
        while (row.getCtRow().sizeOfTcArray() > 0) {
            row.getCtRow().removeTc(0);
        }
        int colIndex = 0;
        for (CellSpec spec : specs) {
            XWPFTableCell cell = row.createCell();
            applyCell(cell, spec, colIndex);
            colIndex += spec.span();
        }
        return row;
    }
 
    private static XWPFTable addRowTo(XWPFTable table, CellSpec... specs) {
        addRow(table, specs);
        return table;
    }
 
    private static void applyCell(XWPFTableCell cell, CellSpec spec, int colIndex) {
        CTTc tc = cell.getCTTc();
        CTTcPr tcPr = tc.isSetTcPr() ? tc.getTcPr() : tc.addNewTcPr();
        int width = 0;
        for (int i = 0; i < spec.span(); i++) {
            width += COL_WIDTHS[colIndex + i];
        }
        CTTblWidth tcW = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
        tcW.setType(STTblWidth.DXA);
        tcW.setW(BigInteger.valueOf(width));
        if (spec.span() > 1) {
            if (!tcPr.isSetGridSpan()) {
                tcPr.addNewGridSpan().setVal(BigInteger.valueOf(spec.span()));
            } else {
                tcPr.getGridSpan().setVal(BigInteger.valueOf(spec.span()));
            }
        }
        cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
        XWPFParagraph paragraph = cell.getParagraphArray(0) != null
                ? cell.getParagraphArray(0) : cell.addParagraph();
        paragraph.setAlignment(spec.align());
        addRun(paragraph, spec.text(), 12, spec.bold());
    }
 
    // ========== 单元格片段描述 ==========
 
    private record CellSpec(String text, int span, boolean bold, ParagraphAlignment align) {
    }
 
    private static CellSpec label(String text) {
        return new CellSpec(text, 1, true, ParagraphAlignment.CENTER);
    }
 
    private static CellSpec labelSpan(String text, int span) {
        return new CellSpec(text, span, true, ParagraphAlignment.CENTER);
    }
 
    private static CellSpec value(String text) {
        return new CellSpec(text, 1, false, ParagraphAlignment.CENTER);
    }
 
    private static CellSpec value(String text, int span) {
        return new CellSpec(text, span, false, ParagraphAlignment.CENTER);
    }
 
    private static CellSpec valueSpan(String text, int span) {
        return new CellSpec(text, span, false, ParagraphAlignment.CENTER);
    }
 
    private static CellSpec valueLeftSpan(String text, int span) {
        return new CellSpec(text, span, false, ParagraphAlignment.LEFT);
    }
 
    // ========== 小工具 ==========
 
    private static void setRowHeight(XWPFTableRow row, int twips) {
        CTRow ctRow = row.getCtRow();
        CTTrPr trPr = ctRow.isSetTrPr() ? ctRow.getTrPr() : ctRow.addNewTrPr();
        CTHeight height = trPr.addNewTrHeight();
        height.setVal(BigInteger.valueOf(twips));
        height.setHRule(STHeightRule.AT_LEAST);
    }
 
    private static void addParagraph(XWPFDocument doc, String text, int size, boolean bold,
                                     ParagraphAlignment align) {
        XWPFParagraph paragraph = doc.createParagraph();
        paragraph.setAlignment(align);
        addRun(paragraph, text, size, bold);
    }
 
    private static void addRun(XWPFParagraph paragraph, String text, int size, boolean bold) {
        XWPFRun run = paragraph.createRun();
        run.setText(text);
        run.setFontSize(size);
        run.setBold(bold);
        run.setFontFamily(FONT);
        run.setFontFamily(FONT, XWPFRun.FontCharRange.eastAsia);
    }
 
    private static boolean notBlank(String s) {
        return s != null && !s.isBlank();
    }
 
    private static String blank(String s) {
        return notBlank(s) ? s : EMPTY;
    }
 
    private static String formatDate(LocalDateTime time) {
        return time == null ? EMPTY : time.format(DATE_FORMATTER);
    }
 
    private static String formatQuantity(BigDecimal quantity) {
        if (quantity == null) {
            return EMPTY;
        }
        return quantity.stripTrailingZeros().toPlainString();
    }
 
}