package com.emergencyrepair.business.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.emergencyrepair.business.entity.CompletionInfo;
import com.emergencyrepair.business.entity.Fault;
import com.emergencyrepair.business.entity.WorkOrder;
import com.emergencyrepair.business.mapper.CompletionInfoMapper;
import com.emergencyrepair.business.mapper.FaultMapper;
import com.emergencyrepair.business.mapper.WorkOrderMapper;
import com.emergencyrepair.business.entity.ProductNode;
import com.emergencyrepair.business.mapper.ProductNodeMapper;
import com.emergencyrepair.common.BusinessException;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ooxml.POIXMLProperties;
import org.apache.poi.wp.usermodel.HeaderFooterType;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFFooter;
import org.apache.poi.xwpf.usermodel.XWPFHeader;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.apache.xmlbeans.XmlObject;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHdrFtr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTrPr;
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.CTTblGrid;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblCellMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STYAlign;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTextDirection;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalJc;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVAnchor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 工程单打印(需求 32)。
*
*
把选中的故障记录按 Word 模板输出成 A4 横向的四联工程单,明细可自动分页。
* 模板里用 {@code {字段名}} 占位,渲染时替换成真实数据。
*
* 模板文件默认放在 {@code config/engineering-order-template.docx},首次启动若不存在会自动生成一份,
* 运维/业务人员可以直接用 Word 打开修改标题、列宽、联头文字,不需要改代码。
*
* 业务配置放在 {@code config/business-config.yml},可按联号维护第 1~4 联名称。
*
* 模板约定(包含明细字段的表格):
*
* - 明细表最后一行是数据行模板,会按选中记录数复制展开,记录行不允许跨页拆分;
* - 联级字段可以用 {@code {xNo}}、{@code {teamMajor}}、{@code {maxFinishDate}}、{@code {unitNo}}、{@code {unitName}}、{@code {unitDisplay}};
* - 整份模板会被复制 4 次输出为四联(每联一页),{@code {unitNo}} 依次为 1/2/3/4。
*
*/
@Service
@RequiredArgsConstructor
public class EngineeringOrderPrintService {
private static final Logger log = LoggerFactory.getLogger(EngineeringOrderPrintService.class);
/** 一个 Word 文件里的联数。 */
private static final int UNIT_COUNT = 4;
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final Pattern PLACEHOLDER = Pattern.compile("\\{([^{}]*)\\}");
private static final String TEMPLATE_VERSION = "engineering-order-template-v4";
private static final Pattern TEMPLATE_PLACEHOLDER =
Pattern.compile("\\{\\{\\s*([^{}]+?)\\s*\\}\\}|\\{\\s*([^{}]+?)\\s*\\}");
/** 只有包含明细字段的表格才按记录数展开最后一行。 */
private static final Set DETAIL_PLACEHOLDERS = new HashSet<>(Arrays.asList(
"faultCode", "engineeringNo", "workOrderNo", "equipmentName", "quantity", "equipmentCount",
"parentSystem", "engineeringMajor", "faultImpact", "faultNature", "majorFaultDesc",
"repairRequirement", "engineeringNameDesc", "isSelfRepair", "isHRelated", "isMonitorItem",
"monitorItem", "monitorValue", "requestedFinishDate", "remark", "contractorName",
"contractorNames", "startDate", "finishDate", "owner", "reportUnit", "reportDate"
));
private static final BigInteger A4_LANDSCAPE_WIDTH = BigInteger.valueOf(16838);
private static final BigInteger A4_LANDSCAPE_HEIGHT = BigInteger.valueOf(11906);
private static final BigInteger PAGE_MARGIN = BigInteger.valueOf(567);
/** 默认模板的联头占位文字。 */
private static final String DEFAULT_UNIT_HEADER =
"X号:{xNo} ZB型号:{zbModel} 专业:{teamMajor} 最大要求完工时间:{maxFinishDate}";
/** 默认模板的列定义:标题 + 字段占位符。 */
private static final String[][] DEFAULT_COLUMNS = {
{"故障编号", "{faultCode}"},
{"设备名称", "{equipmentName}"},
{"工程名称及损坏情况", "{engineeringNameDesc}"},
{"修理技术要求", "{repairRequirement}"},
{"承修单位", "{contractorName}"},
{"数量", "{quantity}"},
{"备注", "{remark}"}
};
/** 默认模板的列宽(twips),合计约等于 A4 横向可用宽度。 */
private static final int UNIT_COLUMN_WIDTH = 400;
private static final int[] TABLE_COLUMN_WIDTHS = {1450, 2750, 3300, 3300, 2000, 800, 1700, UNIT_COLUMN_WIDTH};
private final FaultMapper faultMapper;
private final WorkOrderMapper workOrderMapper;
private final CompletionInfoMapper completionInfoMapper;
private final ProductNodeMapper productNodeMapper;
@Value("${app.engineering-print.template-path:${app.engineering-print.template:config/engineering-order-template.docx}}")
private String templateLocation;
@Value("${app.engineering-print.unit-names.1:上报人}")
private String unitName1;
@Value("${app.engineering-print.unit-names.2:}")
private String unitName2;
@Value("${app.engineering-print.unit-names.3:}")
private String unitName3;
@Value("${app.engineering-print.unit-names.4:}")
private String unitName4;
@PostConstruct
public void ensureTemplate() {
Path path = templatePath();
if (path == null) {
return;
}
try {
Files.createDirectories(path.getParent());
if (Files.exists(path)) {
return;
}
try (XWPFDocument document = buildDefaultTemplate();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
document.write(out);
Files.write(path, out.toByteArray());
}
log.info("工程单打印模板已生成或升级:{}", path.toAbsolutePath());
} catch (Exception exception) {
log.warn("生成或升级工程单打印模板失败:{}", exception.getMessage());
}
}
private Map loadUnitNames() {
Map names = new LinkedHashMap<>();
names.put(1, text(unitName1).trim());
names.put(2, text(unitName2).trim());
names.put(3, text(unitName3).trim());
names.put(4, text(unitName4).trim());
return names;
}
private boolean templateNeedsUpgrade(Path path) {
try (InputStream input = Files.newInputStream(path);
XWPFDocument document = new XWPFDocument(input)) {
POIXMLProperties.CoreProperties properties = document.getProperties().getCoreProperties();
return !TEMPLATE_VERSION.equals(properties.getKeywords())
|| collectText(document).indexOf("{zbModel}") < 0
|| collectText(document).indexOf("{unitDisplay}") < 0;
} catch (Exception exception) {
log.warn("工程单打印模板无法读取,将重新生成:{}", exception.getMessage());
return true;
}
}
/**
* 按选中的故障记录渲染工程单 Word 文档。
*
* @param faultIds 故障主键
*/
public byte[] render(List faultIds) {
return renderInternal(faultIds, false);
}
/**
* 打印工程单2:按联分组输出,第1联全部明细后换页,再输出第2联,以此类推。
*/
public byte[] renderVersion2(List faultIds) {
return renderInternal(faultIds, true);
}
private byte[] renderInternal(List faultIds, boolean legacyGroupedUnits) {
List ids = normalizeIds(faultIds);
if (ids.isEmpty()) {
throw new BusinessException("请至少选择一条记录后再打印");
}
List