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