diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseExportServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseExportServiceImpl.java index f81d3a2f..305a57f9 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseExportServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/unionReimburse/service/impl/UnionReimburseExportServiceImpl.java @@ -19,6 +19,20 @@ import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseSer import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy; +import com.deepoove.poi.data.PictureType; +import com.deepoove.poi.data.Pictures; +import com.deepoove.poi.template.ElementTemplate; +import com.google.zxing.*; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.QRCodeReader; +import org.apache.poi.xwpf.usermodel.*; +import org.apache.poi.util.Units; +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import lombok.extern.slf4j.Slf4j; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; @@ -45,13 +59,13 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ /** 当前“我的报销”导出仅允许单据经办人,状态白名单与页面按钮保持一致。 */ @Override public byte[] exportPdf(String id) { - return renderDocument(loadExportForm(id, false, false), true); + return renderDocument(loadExportForm(id, false, false), true, false); } /** 汇总打印支持所有状态,并严格按汇总权限和所属工会范围读取,复用我的报销 PDF 模板。 */ @Override public byte[] exportCollectPdf(String id) { - return renderDocument(loadExportForm(id, true, true), true); + return renderDocument(loadExportForm(id, true, true), true, true); } /** @@ -89,8 +103,8 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ return form; } - /** 两份模板仅保留布局差异,Word/PDF 共用字段组装和循环行配置。 */ - private byte[] renderDocument(UnionReimburse form, boolean asPdf) { + /** 两份模板共用字段和循环行配置;collectPrint 仅为汇总打印补充编号二维码。 */ + private byte[] renderDocument(UnionReimburse form, boolean asPdf, boolean collectPrint) { String templateCode = "UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject()) ? CONDOLENCE_TEMPLATE : ACTIVITY_TEMPLATE; Path docx = null; @@ -107,7 +121,11 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ try (InputStream input = sysOfficeTemplateUtil.getTemplate(templateCode); XWPFTemplate template = XWPFTemplate.compile(input, config); OutputStream output = Files.newOutputStream(docx)) { - template.render(buildExportData(form)).write(output); + Map data = buildExportData(form); + if (collectPrint) { + prepareCollectQrCode(template, data, form.getDocumentNo()); + } + template.render(data).write(output); } if (!asPdf) { return Files.readAllBytes(docx); @@ -139,6 +157,74 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ } } + /** + * 汇总打印沿用老系统的单据编号二维码,仅修改本次生成的文档,不覆盖模板。 + * @param template 当前已编译模板,优先使用 qrcode 图片标记,其次替换标题区的固定二维码 + * @param data 本次渲染数据,qrcode 字段为 80×80 的 PNG 图片对象 + * @param documentNo 原始单据编号,缺失时拒绝打印,避免用业务 ID 冒充编号 + */ + private void prepareCollectQrCode(XWPFTemplate template, Map data, + String documentNo) throws Exception { + if (StrUtil.isBlank(documentNo)) { + throw new BaseException("报销单编号为空,无法生成打印二维码,请先核对单据编号"); + } + var matrix = new QRCodeWriter().encode(documentNo, BarcodeFormat.QR_CODE, 80, 80); + var image = MatrixToImageWriter.toBufferedImage(matrix); + data.put("qrcode", Pictures.ofBufferedImage(image, PictureType.PNG).size(80, 80).create()); + boolean hasTag = template.getElementTemplates().stream() + .anyMatch(item -> item instanceof ElementTemplate + && "qrcode".equals(((ElementTemplate) item).getTagName())); + if (hasTag) { + return; + } + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ImageIO.write(image, "png", bytes); + XWPFDocument document = template.getXWPFDocument(); + // 部分现用模板内嵌了固定二维码。仅识别第一张业务表格之前的二维码,保留徽标等普通图片。 + boolean replaced = false; + for (IBodyElement element : document.getBodyElements()) { + if (element instanceof XWPFTable) break; + if (!(element instanceof XWPFParagraph)) continue; + for (XWPFRun run : ((XWPFParagraph) element).getRuns()) { + for (XWPFPicture picture : run.getEmbeddedPictures()) { + if (picture.getPictureData() == null) continue; + var existing = ImageIO.read(new ByteArrayInputStream(picture.getPictureData().getData())); + if (existing == null) continue; + try { + new QRCodeReader().decode(new BinaryBitmap(new HybridBinarizer( + new BufferedImageLuminanceSource(existing)))); + } catch (NotFoundException | ChecksumException | FormatException e) { + continue; + } + String relationship = document.addPictureData(bytes.toByteArray(), Document.PICTURE_TYPE_PNG); + picture.getCTPicture().getBlipFill().getBlip().setEmbed(relationship); + replaced = true; + } + } + } + if (replaced) return; + // 无二维码标记或固定二维码的模板,在正文开头添加右对齐图片,不依赖模板重新上传。 + XWPFParagraph paragraph; + if (document.getBodyElements().isEmpty()) { + paragraph = document.createParagraph(); + } else { + IBodyElement first = document.getBodyElements().get(0); + var cursor = first instanceof XWPFParagraph + ? ((XWPFParagraph) first).getCTP().newCursor() + : ((XWPFTable) first).getCTTbl().newCursor(); + try { + paragraph = document.insertNewParagraph(cursor); + } finally { + cursor.dispose(); + } + } + paragraph.setAlignment(ParagraphAlignment.RIGHT); + paragraph.setSpacingBefore(0); + paragraph.setSpacingAfter(0); + paragraph.createRun().addPicture(new ByteArrayInputStream(bytes.toByteArray()), + Document.PICTURE_TYPE_PNG, "qrcode.png", Units.pixelToEMU(80), Units.pixelToEMU(80)); + } + /** 平铺字段供两类模板复用,支付总额与发票总额分别计算,不使用第一条支付代替全部明细。 */ @Override public Map buildExportData(UnionReimburse form) {