云
3 小时以前 401bdd0cfb4d7ce7344e34b3897ff14e303188fb
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
package cn.iocoder.yudao.module.crm.service.contract;
 
import cn.hutool.core.convert.Convert;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
import org.springframework.core.io.ClassPathResource;
import org.w3c.dom.*;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.*;
 
/** 保留用户合同模板的 OOXML 包,仅填写业务槽位并扩展产品明细行。 */
public final class CrmContractDocumentRenderer {
    private static final String W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
    private CrmContractDocumentRenderer() { }
 
    public static byte[] render(CrmContractRespVO contract) throws IOException {
        if (contract == null || contract.getProducts() == null || contract.getProducts().isEmpty()) {
            throw new IllegalArgumentException("合同不存在或没有产品明细,无法生成合同文件");
        }
        try (var input = new ClassPathResource("templates/contract/sale-contract.docx").getInputStream();
             var zip = new ZipInputStream(input); var output = new ByteArrayOutputStream();
             var result = new ZipOutputStream(output)) {
            ZipEntry entry;
            while ((entry = zip.getNextEntry()) != null) {
                byte[] content = zip.readAllBytes();
                if ("word/document.xml".equals(entry.getName())) content = fillDocument(content, contract);
                result.putNextEntry(new ZipEntry(entry.getName()));
                result.write(content);
                result.closeEntry();
            }
            result.finish();
            return output.toByteArray();
        }
    }
 
    private static byte[] fillDocument(byte[] xml, CrmContractRespVO contract) throws IOException {
        try {
            var factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
            var document = factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml));
            Node rowTemplate = null;
            var rows = document.getElementsByTagNameNS(W, "tr");
            for (int i = 0; i < rows.getLength(); i++) {
                if (rows.item(i).getTextContent().contains("${itemName}")) { rowTemplate = rows.item(i); break; }
            }
            if (rowTemplate == null) throw new IOException("合同模板缺少产品明细槽位");
            int index = 0;
            BigDecimal productTotal = BigDecimal.ZERO;
            for (var product : contract.getProducts()) {
                Node row = rowTemplate.cloneNode(true);
                BigDecimal price = Objects.requireNonNull(product.getContractPrice(), "合同单价不能为空");
                BigDecimal count = Objects.requireNonNull(product.getCount(), "合同数量不能为空");
                BigDecimal amount = product.getTotalPrice() != null ? product.getTotalPrice()
                        : price.multiply(count).setScale(2, RoundingMode.HALF_UP);
                productTotal = productTotal.add(amount);
                String name = text(product.getItemName());
                if (name.isBlank()) name = text(product.getItemCode());
                String specification = text(product.getItemSpecification());
                if (!specification.isBlank()) name += "(" + specification + ")";
                replace(row, Map.of("index", String.valueOf(++index), "itemName", name,
                        "unitName", text(product.getItemUnitName()), "count", count.stripTrailingZeros().toPlainString(),
                        "price", money(price), "amount", money(amount)));
                rowTemplate.getParentNode().insertBefore(row, rowTemplate);
            }
            rowTemplate.getParentNode().removeChild(rowTemplate);
            BigDecimal total = Objects.requireNonNull(contract.getTotalPrice(), "合同总金额不能为空");
            BigDecimal discount = productTotal.subtract(total);
            replace(document, Map.of("customerName", text(contract.getCustomerName()),
                    "contractNo", text(contract.getNo()),
                    "contractDate", contract.getOrderDate() == null ? "____________" : contract.getOrderDate().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")),
                    "totalPrice", money(total), "totalUpper", Convert.digitToChinese(total),
                    "discountNote", discount.signum() == 0 ? "" : "(明细合计 " + money(productTotal) + " 元,整单优惠 " + money(discount) + " 元)"));
            if (document.getDocumentElement().getTextContent().contains("${")) throw new IOException("合同模板存在未填写槽位");
            var transformerFactory = TransformerFactory.newInstance();
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
            var output = new ByteArrayOutputStream();
            transformerFactory.newTransformer().transform(new DOMSource(document), new StreamResult(output));
            return output.toByteArray();
        } catch (IOException exception) { throw exception;
        } catch (Exception exception) { throw new IOException("生成合同 Word 文件失败", exception); }
    }
 
    private static void replace(Node node, Map<String, String> values) {
        if (node.getNodeType() == Node.ELEMENT_NODE && "t".equals(node.getLocalName()) && W.equals(node.getNamespaceURI())) {
            String value = node.getTextContent();
            for (var entry : values.entrySet()) value = value.replace("${" + entry.getKey() + "}", entry.getValue());
            node.setTextContent(value);
            return;
        }
        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) replace(child, values);
    }
 
    private static String money(BigDecimal value) { return value.setScale(2, RoundingMode.HALF_UP).toPlainString(); }
    private static String text(String value) { return value == null ? "" : value; }
}