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 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 nodes, List 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 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(); } }