POI对Word操作参考

一,网上的API讲解
其实POI的生成Word文档的规则就是先把获取到的数据转成xml格式的数据,然后通过xpath解析表单式的应用取值,判断等等,然后在把取到的值放到word文档中,最后在输出来。

1.1,参考一
1、poi之word文档结构介绍之正文段落

一个文档包含多个段落,一个段落包含多个Runs,一个Runs包含多个Run,Run是文档的最小单元

获取所有段落:List paragraphs = word.getParagraphs();

获取一个段落中的所有Runs:List xwpfRuns = xwpfParagraph.getRuns();

获取一个Runs中的一个Run:XWPFRun run = xwpfRuns.get(index);

2、poi之word文档结构介绍之正文表格

一个文档包含多个表格,一个表格包含多行,一行包含多列(格),每一格的内容相当于一个完整的文档

获取所有表格:List xwpfTables = doc.getTables();

获取一个表格中的所有行:List xwpfTableRows = xwpfTable.getRows();

获取一行中的所有列:List xwpfTableCells = xwpfTableRow.getTableCells();

获取一格里的内容:List paragraphs = xwpfTableCell.getParagraphs();

之后和正文段落一样

注:

表格的一格相当于一个完整的docx文档,只是没有页眉和页脚。里面可以有表格,使用xwpfTableCell.getTables()获取,and so on
在poi文档中段落和表格是完全分开的,如果在两个段落中有一个表格,在poi中是没办法确定表格在段落中间的。(当然除非你本来知道了,这句是废话)。只有文档的格式固定,才能正确的得到文档的结构
3、poi之word文档结构介绍之页眉:

一个文档可以有多个页眉(不知道怎么会有多个页眉。。。),页眉里面可以包含段落和表格

获取文档的页眉:List headerList = doc.getHeaderList();

获取页眉里的所有段落:List paras = header.getParagraphs();

获取页眉里的所有表格:List tables = header.getTables();

之后就一样了

4、poi之word文档结构介绍之页脚:

页脚和页眉基本类似,可以获取表示页数的角标

1.2,参考二
POI操作Word简介

POI读写Excel功能强大、操作简单。但是POI操作时,一般只用它读取word文档,POI只能能够创建简单的word文档,相对而言POI操作时的功能太少。

(2)POI创建Word文档的简单示例

XWPFDocument doc = new XWPFDocument();// 创建Word文件
XWPFParagraph p = doc.createParagraph();// 新建一个段落
p.setAlignment(ParagraphAlignment.CENTER);// 设置段落的对齐方式
p.setBorderBottom(Borders.DOUBLE);//设置下边框
p.setBorderTop(Borders.DOUBLE);//设置上边框
p.setBorderRight(Borders.DOUBLE);//设置右边框
p.setBorderLeft(Borders.DOUBLE);//设置左边框
XWPFRun r = p.createRun();//创建段落文本
r.setText(“POI创建的Word段落文本”);
r.setBold(true);//设置为粗体
r.setColor(“FF0000”);//设置颜色
p = doc.createParagraph();// 新建一个段落
r = p.createRun();
r.setText(“POI读写Excel功能强大、操作简单。”);
XWPFTable table= doc.createTable(3, 3);//创建一个表格
table.getRow(0).getCell(0).setText(“表格1”);
table.getRow(1).getCell(1).setText(“表格2”);
table.getRow(2).getCell(2).setText(“表格3”);
FileOutputStream out = newFileOutputStream(“d:\POI\sample.doc”);
doc.write(out);
out.close();

(3)POI读取Word文档里的文字

FileInputStream stream = newFileInputStream(“d:\POI\sample.doc”);
XWPFDocument doc = new XWPFDocument(stream);// 创建Word文件
for(XWPFParagraph p : doc.getParagraphs())//遍历段落
{
System.out.print(p.getParagraphText());
}
for(XWPFTable table : doc.getTables())//遍历表格
{
for(XWPFTableRow row : table.getRows())
{
for(XWPFTableCell cell : row.getTableCells())
{
System.out.print(cell.getText());
}
}

1.3,参考三,分段混乱

题:在操作POI替换world时发现getRuns将我们预设的${product}自动切换成了

${product, }]
p r o d u c t 成 了 两 个 部 分 123 生 成 错 误 时 的 图 片 解 决 方 法 一 。 ( 未 尝 试 ) 强 制 把 L i s t 中 的 内 容 合 并 成 一 个 字 符 串 , 替 换 内 容 后 , 把 段 落 中 的 X W P F R u n 全 部 r e m o v e 掉 , 然 后 新 建 一 个 含 有 替 换 后 内 容 的 X P W F R u n , 并 赋 给 当 前 段 落 。 解 决 方 法 二 . 请 用 复 制 粘 贴 把 你 的 {product } 成了两个部分 1 2 3 生成错误时的图片 解决方法一。(未尝试) 强制把List中的内容合并成一个字符串,替换内容后,把段落中的XWPFRun全部remove掉,然后新建一个含有替换后内容的XPWFRun,并赋给当前段落。 解决方法二. 请用复制粘贴把你的 product123()ListXWPFRunremoveXPWFRun.{product}添加进world文档里面即可解决,不要手打 目前发现复制粘贴是没有问题的,感觉像是poi的一个bug不知道立贴为证。

注意:${这里尽量不要存中文,否在还出现上面情况}

二,项目应用

2.1,判断生成word的条件
复制代码
1 private boolean getXpathRes(String json,String xpathRule){
2 boolean isTrue = false;
3 try {
4 JSONObject obj = getGoodJson(json, json.replaceAll("\n", “”).replaceAll("“null”", “”"").replaceAll(":null,", “:”",").replaceAll(" “”, “”"));
5 XMLSerializer serializer = new XMLSerializer();
6 String xml = serializer.write(obj,“UTF-8”);
7 log.info(“测试用的,记得删除”+xml);
8 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
9 dbf.setValidating(false);
10 DocumentBuilder db;
11 db = dbf.newDocumentBuilder();
12 StringReader stringReader = new StringReader(xml);
13 InputSource inputSource = new InputSource(stringReader);
14 Document doc;
15 doc = db.parse(inputSource);
16 XPathFactory factory = XPathFactory.newInstance();
17 XPath xpath = factory.newXPath();
18 isTrue = (Boolean) xpath.evaluate(xpathRule, doc,XPathConstants.BOOLEAN);
19 } catch (Exception e) {
20 log.info(“合同解析生成XML报错:”+e.getMessage());
21 }finally{
22 return isTrue;
23 }
24 // return true;
25 }
复制代码

2.1.1,下面就是根据从数据库中取到值,判断规则,和json数据做对比的,就是json数据中有没有数据库中要的值。判断规则是xpath的规则运算符。

JSONObject obj = getGoodJson(json, json.replaceAll("\n", “”).replaceAll("“null”", “”"").replaceAll(":null,", “:”",").replaceAll(" “”, “”"));
XMLSerializer serializer = new XMLSerializer();
String xml = serializer.write(obj,“UTF-8”);

–把json格式的数据以xml的格式输出

首先得到:得到 DOM 解析器的工厂实例
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

然后从 DOM 工厂获得 DOM 解析器

dbf.setValidating(false);默认是false
DocumentBuilder db;
db = dbf.newDocumentBuilder();

当你有一组应用程序接口(API)只允许用Writer或Reader作为输入,但你又想使用String,这时可以用StringWriter或StringReader。
当读入文件时也一样。可以用StringReader代替Reader来哄骗API,而不必非得从某种形式的文件中读入。StringReader的构造器要求一个String参数。例如:xmlReader.parse(new InputSource(new StringReader(xmlStr)));
StringReader stringReader = new StringReader(xml);

— 把符合xml的String转成document对象被java程序解读
StringReader stringReader = new StringReader(xml);
InputSource inputSource = new InputSource(stringReader);
Document doc;
doc = db.parse(inputSource);

–用xpath解析
–生成xpath对象
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();

在 Java 中计算 XPath 表达式时,第二个参数指定需要的返回类型。有五种可能,都在javax.xml.xpath.XPathConstants 类中命名了常量:

XPathConstants.NODESET
XPathConstants.BOOLEAN
XPathConstants.NUMBER
XPathConstants.STRING
XPathConstants.NODE 获取节点 node.getTextContent() 获得节点的内容
xpathRule:数据库中存储的
//industrySubType!=‘20’ and //industrySubType!=‘21’ and //industrySubType!=‘22’ and //industrySubType!=‘23’ and //industrySubType!=‘26’ and //industrySubType!=‘27’ and //industrySubType!=‘28’ and //industrySubType!=‘29’ and //industrySubType!=‘30’ and //industrySubType!=‘148’ and //industrySubType!=‘31’ and //industrySubType!=‘32’ and //industrySubType!=‘37’ and //industrySubType!=‘38’ and //industrySubType!=‘39’ and //industrySubType!=‘11’ and //industrySubType!=‘12’ and //industrySubType!=‘13’ and //industrySubType!=‘14’ and //industrySubType!=‘15’ and //industrySubType!=‘16’

//标示节点中的所有的xml节点
doc就是经过一系列处理,把json数据转化成document对象,并且能被xpath解读的对象:

XPathConstants.BOOLEAN:是返回值,有这个数据就返回true,没有就是false
isTrue = (Boolean) xpath.evaluate(xpathRule, doc,XPathConstants.BOOLEAN);

这里需要见xpath的解析规则

xml的xPath解析规则

2.2,获取模板之后,开始获取里面的参数,这个参数是在数据库中配置的。
复制代码
1 private List<Map<String,Object>> getTemplateParam(List templatNameList,
2 String workOrderId, String productCode,String contractNum,long merchantCode,boolean isMainFlag) throws Exception
3 {
4 log.info(“获取模板原始参数开始=获得的模板有templatNameList"+templatNameList.toString()+"");
5 List aTemplateParamSqlList = new ArrayList();
6 List<Map<String,Object>> aTemplateParamList = new ArrayList<Map<String,Object>>();
7
8 List orginParamList = new ArrayList();
9
10 Map<String, Object> rsMap = null;
11 Map<String,Object> queryTemplateParamSql = new HashMap<String,Object>();
12 Map<String,Object> queryOriginParam = new HashMap<String,Object>();
13
14 //获取每个模板的参数
15 for(String templateName : templatNameList)
16 {
17 log.info(“获取模板原始参数中=获得的模板有templatNameList"+templatNameList.toString()+"成功获取到模板文档”);
18 queryOriginParam.put(“templateName”, templateName.split(",")[0]);
19 //aTemplateParamSqlList = econtractTemplateParamsqlAdvancedService.queryEcontractTemplateParamsqlAdvancedByMap(queryTemplateParamSql);
20 //获取每个模板需要替换的参数
21 orginParamList = econtractTemplateParamAdvancedService.queryEcontractTemplateParamAdvancedByMap(queryOriginParam);
22
23 Map<String,String> searchMap = new HashMap<String,String>();
24 searchMap.put(“orderId”, templateName.split(",")[1]);
25 WorkorderDetail workOrderDeatil = workorderDetailService.findWorkorderDetailByMap(searchMap);
26 String json = workOrderDeatil.getProductParam();
27 String jsonStr = json.replaceAll("\n", “”).replaceAll("“null”", “”"").replaceAll(":null,", “:”",").replaceAll(" “”, “”");
28 rsMap = new HashMap<String,Object>();
29 Map<String, Object> res = getGoodJsonMap(json, jsonStr);
30 String xpath="";
31 for(EcontractTemplateParamAdvanced paramAdvancde:orginParamList){
32 xpath = paramAdvancde.getMethodParam();
33 if(res.get(paramAdvancde.getFieldName())null){
34 //新添加一个实现方式,是先判断是否满足前提条件,如果满足再查询数据,不满足就直接返回/
35 boolean judgeSuccess = false;//判断前提条件是否成立,false-不成立,true-成立
36 if(!StringUtils.isEmpty(xpath)&&xpath.indexOf("@&@")!=-1 || !StringUtils.isEmpty(xpath)&&xpath.indexOf("@&&@")!=-1){//存在这个符号,表示需要判断前提条件,@&@,前提条件,需要取的值的字段,单位
37 // String[] methodParamArray = xpath.split(",");
38 String[] methodParamArray = null;
39 if(!StringUtils.isEmpty(xpath)&&xpath.indexOf("@&&@")!=-1){//@&&@,前提条件中存在特殊符号
40 methodParamArray = xpath.split(";");
41 }else{
42 methodParamArray = xpath.split(",");
43 }
44 if(getXpathRes(json,methodParamArray[1])){
45 judgeSuccess = true;//需要判断前提条件
46 }else{
47 String unit = methodParamArray[methodParamArray.length-1];
48 if(“null”.equals(unit)){//没有单位,在最后是单位
49 rsMap.put(paramAdvancde.getFieldName(),"/");
50 }else if(“instalmentsUnitToShow”.equals(unit)){//分期付款特殊处理
51 rsMap.put(paramAdvancde.getFieldName(),getInstalmentsUnit(methodParamArray));
52 }else if(“hasNotUnit”.equals(unit)){//针对单笔最高和单笔最低
53 rsMap.put(paramAdvancde.getFieldName(),"");
54 }else{//有单位
55 rsMap.put(paramAdvancde.getFieldName(),"/"+unit);
56 }
57 }
58 }else{//不需要判断前提条件
59 judgeSuccess = true;
60 }
61 // 此xpath表达式是用来做逻辑判断的
62 if(!StringUtils.isEmpty(xpath) && xpath.indexOf("=")!=-1 && judgeSuccess && xpath.indexOf("@&@")
-1 && xpath.indexOf("@&&@")
-1){//有=号并且不存在@&@,因为前提条件中会有=号
63 //快易花商户合同里不再是黑白框 应该是√和X 订单中各期商户补贴=0或空值时,为X,费率为/; 非空时,为√,费率取对应的值
64 if(xpath.indexOf(“xx=xx”)!=-1){//快易花中需要用替换的
65 if(getXpathRes(json,xpath)){
66 rsMap.put(paramAdvancde.getFieldName(),“√”);
67 }else{
68 rsMap.put(paramAdvancde.getFieldName(),“×”);
69 }
70 }else{//其他合同还是打■或□,快易花合同中部分也需要用■或□
71 if(getXpathRes(json,xpath)){
72 rsMap.put(paramAdvancde.getFieldName(),“■”);
73 }else{
74 rsMap.put(paramAdvancde.getFieldName(),“□”);
75 }
76 }
77 }else if(!StringUtils.isEmpty(xpath)&& (xpath.indexOf("=")
-1 || xpath.indexOf(”@&@")!=-1 || xpath.indexOf("@&&@")!=-1)&&judgeSuccess){//没有=号或者有@&@符号的规则也需要走以下逻辑
78 //如果call_method为空,则直接利用xpath进行取值替换
79 String str =getXpathValue(json,paramAdvancde.getMethodParam());
80 if(StringUtils.isEmpty(paramAdvancde.getCallMethod())){
81 rsMap.put(paramAdvancde.getFieldName(),str);
82 }else{
83 // 用来判断机具类型的,固定POS、移动POS、快刷、POS附件
84 // JSONObject obj = (JSONObject) JSONSerializer.toJSON(jsonStr);
85 JSONObject obj = getGoodJson(json, jsonStr);
86 JSONArray productListJsonArray = obj.getJSONArray(“productList”);
87 JSONArray PriSinBillCustomDataStr=null;
88 JSONArray PubSinBankCustomDataStr=null;
89 JSONArray PriBatchBankCustomDataStr=null;
90 JSONArray PubBatchBankCustomDataStr=null;
91 if(“dspayfinancialupdatewordfir.docx”.equals(templateName.split(",")[0])&&productListJsonArray.size()>0){
92 JSONObject job = productListJsonArray.getJSONObject(0); // 遍历 jsonarray 数组,把每一个对象转成 json 对象
93 if(job.containsKey(“PriSinBillCustomDataStr”)&&job.getString(“PriSinBillCustomDataStr”)!=null&&xpath.indexOf(“PriSinBillCustomDataStr”)>-1){
94 String PriBill =job.get(“PriSinBillCustomDataStr”).toString()null?"":String.valueOf(job.get(“PriSinBillCustomDataStr”));
95 PriSinBillCustomDataStr = JSONArray.fromObject(PriBill);
96 }else if(job.containsKey(“PubSinBankCustomDataStr”)&&job.getString(“PubSinBankCustomDataStr”)!=null&&xpath.indexOf(“PubSinBankCustomDataStr”)>-1){
97 String PubBank =job.get(“PubSinBankCustomDataStr”).toString()null?"":String.valueOf(job.get(“PubSinBankCustomDataStr”));
98 PubSinBankCustomDataStr = JSONArray.fromObject(PubBank);
99 }else if(job.containsKey(“PriBatchBankCustomDataStr”)&&job.getString(“PriBatchBankCustomDataStr”)!=null&&xpath.indexOf(“PriBatchBankCustomDataStr”)>-1){
100 String PriBatch =job.get(“PriBatchBankCustomDataStr”).toString()null?"":String.valueOf(job.get(“PriBatchBankCustomDataStr”));
101 PriBatchBankCustomDataStr = JSONArray.fromObject(PriBatch);
102 }else if(job.containsKey(“PubBatchBankCustomDataStr”)&&job.getString(“PubBatchBankCustomDataStr”)!=null&&xpath.indexOf(“PubBatchBankCustomDataStr”)>-1){
103 String PubBatch =job.get(“PubBatchBankCustomDataStr”).toString()null?"":String.valueOf(job.get(“PubBatchBankCustomDataStr”));
104 PubBatchBankCustomDataStr = JSONArray.fromObject(PubBatch);
105 }
106 }
107 JSONArray terminalArray =new JSONArray();
108 JSONObject merchantFinanceObject = new JSONObject();//只有主协议需要这个
109 String checkTemplateNameValue = checkTemplateName(templateName);
110 // 如果产品涉及到终端直接去terminobjects中取值
111 if(“1”.equals(checkTemplateNameValue)){//去terminobjects中取值
112 JSONObject terminalObject = (JSONObject)productListJsonArray.get(0);
113 if((JSONArray)terminalObject.get(“terminalObjects”)!= null){//防止空指针
114 terminalArray =(JSONArray)terminalObject.get(“terminalObjects”);
115 }
116 /}
117 // 非终端相关的直接去productList中取值
118 else{
119 terminalArray=productListJsonArray;
/
120 }else if(“2”.equals(checkTemplateNameValue)){//去merchantFinance取值
121 if(obj.containsKey(“merchantFinance”)){
122 merchantFinanceObject = obj.getJSONObject(“merchantFinance”);//主协议的时候需要用到
123 }
124 }
125 if(productListJsonArray
null){
126 rsMap.put(paramAdvancde.getFieldName(),"");
127 }else{
128 String methodParam =paramAdvancde.getMethodParam();
129 // String[] methodParamArray = methodParam.split(",");
130 String[] methodParamArray = null;
131 if(!StringUtils.isEmpty(xpath)&&xpath.indexOf("@&&@")!=-1){//@&&@,前提条件中存在特殊符号
132 methodParamArray = methodParam.split(";");
133 }else{
134 methodParamArray = methodParam.split(",");//tina623
135 }
136 Object[] args = null;
137 if(“1”.equals(checkTemplateNameValue)){//如果产品涉及到终端直接去terminobjects中取值
138 if(paramAdvancde.getCallMethod().startsWith(“get”)){
139 args = new Object[methodParamArray.length+2];
140 args[0]=productListJsonArray;
141 args[1]=terminalArray;//主协议的时候,merchantFinance取的arraylist封装在这里
142 for(int i=2;i<methodParamArray.length+2;i++){
143 args[i]=methodParamArray[i-2];
144 }
145 }else if(paramAdvancde.getCallMethod().startsWith(“set”)){
146 args = new Object[methodParamArray.length+1];
147 args[0]=templateName.split(",")[1];
148 for(int i=1;i<methodParamArray.length+1;i++){
149 args[i]=methodParamArray[i-1];
150 }
151 }
152 }else if(“2”.equals(checkTemplateNameValue)){//主协议—主协议的时候,merchantFinance取的arraylist封装在这里
153 if(paramAdvancde.getCallMethod().startsWith(“get”)){
154 args = new Object[methodParamArray.length+2];
155 args[0]=productListJsonArray;
156 args[1]=merchantFinanceObject;//主协议的时候,merchantFinance取的arraylist封装在这里
157 for(int i=2;i<methodParamArray.length+2;i++){
158 args[i]=methodParamArray[i-2];
159 }
160 }else if(paramAdvancde.getCallMethod().startsWith(“set”)){
161 args = new Object[methodParamArray.length+1];
162 args[0]=templateName.split(",")[1];
163 for(int i=1;i<methodParamArray.length+1;i++){
164 args[i]=methodParamArray[i-1];
165 }
166 }
167 }else{
168 args = new Object[methodParamArray.length+1];
169 // 约定取值方法以get起始的话
170 if(paramAdvancde.getCallMethod().startsWith(“get”)){
171 args[0]=productListJsonArray;
172 }else if(paramAdvancde.getCallMethod().startsWith(“set”)){
173 args[0]=templateName.split(",")[1];
174 }else if(“dspayfinancialupdatewordfir.docx”.equals(templateName.split(",")[0])&&paramAdvancde.getCallMethod().startsWith(“twoGet”)){
175 if(xpath.indexOf(“PriSinBillCustomDataStr”)>-1){
176 args[0]=PriSinBillCustomDataStr;
177 }else if(xpath.indexOf(“PubSinBankCustomDataStr”)>-1){
178 args[0]=PubSinBankCustomDataStr;
179 }else if(xpath.indexOf(“PriBatchBankCustomDataStr”)>-1){
180 args[0]=PriBatchBankCustomDataStr;
181 }else if(xpath.indexOf(“PubBatchBankCustomDataStr”)>-1){
182 args[0]=PubBatchBankCustomDataStr;
183 }
184 }
185 for(int i=1;i<methodParamArray.length+1;i++){
186 args[i]=methodParamArray[i-1];
187 }
188 }
189 Class[] argsClass = new Class[args.length];
190 for (int i = 0, j = args.length; i < j; i++) {
191 if(args[i]!=null){//避免空指针
192 argsClass[i] = args[i].getClass();
193 }
194 }
195 try{
196 Method method = crmContractUtil.getClass().getMethod(paramAdvancde.getCallMethod(),argsClass);
197 rsMap.put(paramAdvancde.getFieldName(),method.invoke(crmContractUtil, args));
198 }catch (Exception e) {
199 e.printStackTrace();
200 }
201 }
202 }
203 }
204 }
205 else{
206 if(res.get(paramAdvancde.getFieldName())!=null && !StringUtils.isEmpty(res.get(paramAdvancde.getFieldName()).toString())){
207 rsMap.put(paramAdvancde.getFieldName(), res.get(paramAdvancde.getFieldName()));
208 }else{
209 rsMap.put(paramAdvancde.getFieldName(), “/”);//如果获取的值为空,自动赋值/
210 }
211 }
212 }
213 rsMap.put(“templateName”, templateName.split(",")[0]);
214 rsMap = converMap(rsMap,contractNum,merchantCode,isMainFlag,res);//封装一些需要计算的数据
215 aTemplateParamList.add(rsMap);
216 }
217 log.info("获取模板原始参数成功…aTemplateParamList
"+aTemplateParamList.toString()+"
=========");
218 return aTemplateParamList;
219 }
220
复制代码

复制代码
1 @SuppressWarnings(“finally”)
2 private String getXpathValue(String json,String xpathRule){
3 String xpathValue = “/”;
4 try {
5 JSONObject obj = getGoodJson(json, json.replaceAll("\n", “”).replaceAll("“null”", “”"").replaceAll(":null,", “:”",").replaceAll(" “”, “”"));
6 XMLSerializer serializer = new XMLSerializer();
7 String xml = serializer.write(obj,“UTF-8”);
8 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
9 dbf.setValidating(false);
10 DocumentBuilder db;
11 db = dbf.newDocumentBuilder();
12 StringReader stringReader = new StringReader(xml);
13 InputSource inputSource = new InputSource(stringReader);
14 Document doc;
15 doc = db.parse(inputSource);
16 XPathFactory factory = XPathFactory.newInstance();
17 XPath xpath = factory.newXPath();
18 Node node = (Node)xpath.evaluate(xpathRule, doc,XPathConstants.NODE);
19 if(nodenull){
20 return “/”;
21 }
22 else{
23 if(StringUtils.isEmpty(node.getTextContent())){
24 return “/”;
25 }else{
26 xpathValue = node.getTextContent();
27 return node.getTextContent();
28 }
29 }
30 } catch (Exception e) {
31 log.info("合同解析生成XML报错,xpathRule
="+xpathRule);
32 return “/”;
33 }finally{
34 return xpathValue;
35 }
36 // return “/”;
37 }
复制代码

划红线的是和一开始的是不一样的,这里是获取json转化过来的xml的文档的node节点的值的。

isTrue = (Boolean) xpath.evaluate(xpathRule, doc,XPathConstants.BOOLEAN);
是通过xpath判断
xpathRule有没有在经过封装的xml文件的document里面的。
orginParamList = econtractTemplateParamAdvancedService.queryEcontractTemplateParamAdvancedByMap(queryOriginParam);
–根据模板名称获取

res里面装的是json转化成的map数据,根据数据库查出来的key值,去取map中的value值。并全部放在map中来。
根据数据库中参数的设置来看取值的逻辑

取值逻辑1:

callmethod 空 methodparam 空
则什么都不往map中放

取值逻辑2:

callmethod 空 methodparam 有值:比如//merchantName
它的取值逻辑主要也是用到了上面的xpath的取值逻辑。
则去json转化的map中查找,找到数值后则放进map中来

取值逻辑3:

callmethod 空 methodparam 有值:比如//isApiPayToBank=1 or //isApiPayToBill=1 or //isBatchApiPayToBank=1
其实它和2的取值逻辑是一样的,只不过xpath的表单式不一样而已。

取值逻辑4:

callmethod 有值,方法名比如setMerchantProperty methodparam 有值:比如参数address或者IpAddress_sin_99bill/IpAddress_sin_ban/IpAddress_bat_ban 可以放值多个参数
这个一般是在json格式的数据中没有这个值,但是还要获取这个数据,通过反射找到setMerchantProperty 这个方法从数据库中其它表中来获取。
address它是参数,需要根据它往反射类中的反射方法中传递的参数。
而存在map中的key值则是word文档中的命名规则,比如input。value值则是从数据库中获取的。
则会通过反射的方法来获取数值。
可以看出来只要是input开头都是通过这个方法获取的。这个主要是为了给替换做准备的。

反射的逻辑详见
反射的一个案例分析

取值逻辑5:

假如上面的4套规则还不能解决一些问题的话,则通过代码直接来设置map的key和value值,放到map中来,比如说当前的时间等等。

rsMap.put(paramAdvancde.getFieldName(), res.get(paramAdvancde.getFieldName()));
最后通过得到的数据就是

复制代码
1 [{payBankNum=/, cwmerchantContactEmail=222@qq.com, masterDay=11, masterContractNum=K18-2000-801-03, depositAccount=/, masterYear=2018, cwmerchantContactCz=/, masterMonth=2, billAccountShareAmount=/, merchantName=还有几天就要放假了呀, year=2018, telephoneormobilephone=18068334025, legalName=222, input28=名字, ddmerchantContactTelphone=18068334025, ddmerchantContactEmail=222@qq.com, bankName=/, cwmerchantContactName=名字, cwmerchantContactTelphone=18068334025, input3=广西壮族自治区贵港市, input5=3333, ddmerchantContactName=名字, input6=3333, input20=2018年 02月 11日, month=2, ddmerchantContactCz=/, merechantURL=/, input23=2019年 02月 10日, day=11, contract_num=K18-2000-801-03, templateName=dsmainfinancialword.docx, accessType=首选, bankAccount=/}, {perMinimum=, perMinimum_pri_bat_ban=, payToCompayKqAcccountRatio=/, payBankNum=/, input69=3333, perMinimum_pub_bat_99bill=, input19=/, payToCompayAcccount=□, input18=□, autoDiectPayOpenFee=/, perMaximum_pub_bat_99bill=/, masterDay=11, input13=□, masterYear=2018, masterMonth=2, holidayPayOpenFee=/, year=2018, holidayPayTechnologyFee=/, memberCode=10012401604, telephoneormobilephone=18068334025, accumulateQuota=/, groupPayCompayKqAccountRatio=/, perMinimum_pri_sin_ban=, perMaximum=/, apiZdPayOpenFee=/, KqPayXinShi=■, bankName=/, perMinimum_pri_bat_99bill=, cwmerchantContactTelphone=18068334025, perMaximum_pri_bat_99bill=/, ddmerchantContactName=名字, BankName_bat_ban_hidden=/, AccountNum_bat_ban=/, month=2, day=11, idContent=234324fds432@qq.com, contract_num=K18-2000-801-03, apiZdPayTechnologyFee=/, perMaximum_pub_bat_ban=/, cwmerchantContactEmail=222@qq.com, holidayPay=□, perMaximum_pri_sin_99bill=/, perMinimum_pub_sin_99bill=, masterContractNum=K18-2000-801-03, groupPayCompayBankAccountRatio=/, depositAccount=/, groupPayToPersonKqAccountRatio=/, cwmerchantContactCz=/, groupPayToPersonBankAccountRatio=/, merchantName=还有几天就要放假了呀, AccountName_bat_ban=/, isBatchApiPayToBank=□, perMaximum_pri_bat_ban=/, perMinimum_pri_sin_99bill=, perMaximum_pub_sin_ban=/, input28=□, ddmerchantContactEmail=222@qq.com, ddmerchantContactTelphone=18068334025, perMinimum_pub_sin_ban=, payToCompayBankAcccountRatio=/, payPersonBankAccountRatio=/, KqPayXinShiFeeRatio=0, payPersonAccountRatio=/, cwmerchantContactName=名字, input3=□, input7=□, input6=/, input9=□, perMaximum_pub_sin_99bill=/, autoDiectPay=□, input22=□, ddmerchantContactCz=/, input24=□, input26=□, perMinimum_pub_bat_ban=, bankAccount=/, templateName=dspayfinancialaddwordfir.docx, perMaximum_pri_sin_ban=/}, {telephoneormobilephone=18068334025, ddmerchantContactEmail=222@qq.com, ddmerchantContactTelphone=18068334025, bankName=/, payBankNum=/, cwmerchantContactEmail=222@qq.com, cwmerchantContactName=名字, cwmerchantContactTelphone=18068334025, masterDay=11, masterContractNum=K18-2000-801-03, ddmerchantContactName=名字, depositAccount=/, masterYear=2018, cwmerchantContactCz=/, masterMonth=2, month=2, merchantName=还有几天就要放假了呀, year=2018, ddmerchantContactCz=/, day=11, contract_num=K18-2000-801-03, templateName=quickSubjectSeal.docx, bankAccount=/}]
复制代码

至于上面的KqPayXinShi=■
则是按照上面的判断规则直接获取就行了

if(getXpathRes(json,xpath)){
rsMap.put(paramAdvancde.getFieldName(),“■”);
}else{
rsMap.put(paramAdvancde.getFieldName(),“□”);
}
2.3,替换模板参数

复制代码
1 private List<Map<String,Object>> replaceParam(List<Map<String,Object>> templatParamList,String contractNum,long contractType)
2 {
3 List<Map<String,Object>> originParamList = new ArrayList<Map<String,Object>>();
4 if(!templatParamList.isEmpty())
5 {
6 String templateName = “”;
7 Map<String,Object> resultMap = null;
8 Map<String,Object> queryOriginParam = new HashMap<String,Object>();
9 List aTemplateParamList = new ArrayList();
10 for(Map<String,Object> originParam : templatParamList)
11 {
12 templateName = (String) originParam.get(“templateName”);
13 queryOriginParam.put(“templateName”, templateName);
14
15 //获取每个模板需要替换的参数
16 aTemplateParamList = econtractTemplateParamAdvancedService.queryEcontractTemplateParamAdvancedByMap(queryOriginParam);
17 if(!aTemplateParamList.isEmpty())
18 {
19 resultMap = new HashMap<String,Object>();
20 for(EcontractTemplateParamAdvanced replaceParams : aTemplateParamList)
21 {
22 if(originParam.containsKey(replaceParams.getFieldName()))
23 {
24 //TODO 调用参数替换规则脚本 未完待续…
25 resultMap.put(replaceParams.getAliasField(), originParam.get(replaceParams.getFieldName()));
26 }
27 }
28 resultMap.put(“templateName”, templateName);
29 resultMap.put(“contract_num”, contractNum);
30 resultMap.put(“contract_type”, contractType);
31 originParamList.add(resultMap);
32 }
33 }
34 }
35 log.info(“替换模板参数成功…”);
36 return originParamList;
37 }
复制代码

复制代码
1 if(originParam.containsKey(replaceParams.getFieldName()))
2
3 originParam:
4 {payBankNum=/, cwmerchantContactEmail=222@qq.com, masterDay=11, masterContractNum=K18-2000-801-03, depositAccount=/, masterYear=2018, cwmerchantContactCz=/, masterMonth=2, billAccountShareAmount=/, merchantName=还有几天就要放假了呀, year=2018, telephoneormobilephone=18068334025, legalName=222, input28=名字, ddmerchantContactTelphone=18068334025, ddmerchantContactEmail=222@qq.com, bankName=/, cwmerchantContactName=名字, cwmerchantContactTelphone=18068334025, input3=广西壮族自治区贵港市, input5=3333, ddmerchantContactName=名字, input6=3333, input20=2018年 02月 11日, month=2, ddmerchantContactCz=/, merechantURL=/, input23=2019年 02月 10日, day=11, contract_num=K18-2000-801-03, templateName=dsmainfinancialword.docx, accessType=首选, bankAccount=/}
5
6
7 { I N P U T 19 INPUT19 INPUT19=/, I N P U T 1 INPUT1 INPUT1=K18-2000-801-03, I N P U T 3 INPUT3 INPUT3=广西壮族自治区贵港市, I N P U T 7 INPUT7 INPUT7=名字, I N P U T 17 INPUT17 INPUT17=/, I N P U T 23 INPUT23 INPUT23=2019年 02月 10日, I N P U T 5 INPUT5 INPUT5=3333, contract_type=0, I N P U T 15 INPUT15 INPUT15=/, I N P U T 29 INPUT29 INPUT29=222, I N P U T 11 INPUT11 INPUT11=名字, I N P U T 13 INPUT13 INPUT13=/, I N P U T 27 INPUT27 INPUT27=/, I N P U T 9 INPUT9 INPUT9=/, I N P U T 18 INPUT18 INPUT18=/, I N P U T 2 INPUT2 INPUT2=还有几天就要放假了呀, I N P U T 4 INPUT4 INPUT4=18068334025, I N P U T 6 INPUT6 INPUT6=3333, I N P U T 8 INPUT8 INPUT8=18068334025, I N P U T 16 INPUT16 INPUT16=/, I N P U T 14 INPUT14 INPUT14=222@qq.com, I N P U T 20 INPUT20 INPUT20=2018年 02月 11日, I N P U T 10 INPUT10 INPUT10=222@qq.com, I N P U T 28 INPUT28 INPUT28=名字, I N P U T 26 INPUT26 INPUT26=首选, contract_num=K18-2000-801-03, I N P U T 12 INPUT12 INPUT12=18068334025, templateName=dsmainfinancialword.docx}
8
9 替换的原理就是把上面获取到的所有的值,根据数据库中的配置,全部提花成 i n p u t input input开头的,因为word文档的每个参数就是这样设置的。
复制代码

if(originParam.containsKey(replaceParams.getFieldName()))

originParam:
{payBankNum=/, cwmerchantContactEmail=222@qq.com, masterDay=11, masterContractNum=K18-2000-801-03, depositAccount=/, masterYear=2018, cwmerchantContactCz=/, masterMonth=2, billAccountShareAmount=/, merchantName=还有几天就要放假了呀, year=2018, telephoneormobilephone=18068334025, legalName=222, input28=名字, ddmerchantContactTelphone=18068334025, ddmerchantContactEmail=222@qq.com, bankName=/, cwmerchantContactName=名字, cwmerchantContactTelphone=18068334025, input3=广西壮族自治区贵港市, input5=3333, ddmerchantContactName=名字, input6=3333, input20=2018年 02月 11日, month=2, ddmerchantContactCz=/, merechantURL=/, input23=2019年 02月 10日, day=11, contract_num=K18-2000-801-03, templateName=dsmainfinancialword.docx, accessType=首选, bankAccount=/}

{ I N P U T 19 INPUT19 INPUT19=/, I N P U T 1 INPUT1 INPUT1=K18-2000-801-03, I N P U T 3 INPUT3 INPUT3=广西壮族自治区贵港市, I N P U T 7 INPUT7 INPUT7=名字, I N P U T 17 INPUT17 INPUT17=/, I N P U T 23 INPUT23 INPUT23=2019年 02月 10日, I N P U T 5 INPUT5 INPUT5=3333, contract_type=0, I N P U T 15 INPUT15 INPUT15=/, I N P U T 29 INPUT29 INPUT29=222, I N P U T 11 INPUT11 INPUT11=名字, I N P U T 13 INPUT13 INPUT13=/, I N P U T 27 INPUT27 INPUT27=/, I N P U T 9 INPUT9 INPUT9=/, I N P U T 18 INPUT18 INPUT18=/, I N P U T 2 INPUT2 INPUT2=还有几天就要放假了呀, I N P U T 4 INPUT4 INPUT4=18068334025, I N P U T 6 INPUT6 INPUT6=3333, I N P U T 8 INPUT8 INPUT8=18068334025, I N P U T 16 INPUT16 INPUT16=/, I N P U T 14 INPUT14 INPUT14=222@qq.com, I N P U T 20 INPUT20 INPUT20=2018年 02月 11日, I N P U T 10 INPUT10 INPUT10=222@qq.com, I N P U T 28 INPUT28 INPUT28=名字, I N P U T 26 INPUT26 INPUT26=首选, contract_num=K18-2000-801-03, I N P U T 12 INPUT12 INPUT12=18068334025, templateName=dsmainfinancialword.docx}

替换的原理就是把上面获取到的所有的值,根据数据库中的配置,全部提花成 i n p u t input input开头的,因为word文档的每个参数就是这样设置的。

2.4,替换word文档中的各个参数并合并word文档

fileName = dealWordHelperService.replaceWordText(finalOriginParamList,templeteFilePath,outFilePath);
templeteFilePath:模板存放的地方
outFilePath:替换掉参数临时的路径

1 templateName = (String)templateParam.get(“templateName”);
2 log.info(“开始替换模板参数=replaceWordText中templateName="+templateName+"===”);
3 OPCPackage pack = POIXMLDocument.openPackage(templeteFile + File.separator + templateName);
4 document = new XWPFDocument(pack);

OPCPackage pack = POIXMLDocument.openPackage(templeteFile + File.separator + templateName);
document = new XWPFDocument(pack);

获取document对象

2.4.1,替换页脚
templateParam:

复制代码
1 {payBankNum=/, cwmerchantContactEmail=222@qq.com, masterDay=11, masterContractNum=K18-2000-801-03, depositAccount=/, masterYear=2018, cwmerchantContactCz=/, masterMonth=2, billAccountShareAmount=/, merchantName=还有几天就要放假了呀, year=2018, telephoneormobilephone=18068334025, legalName=222, input28=名字, ddmerchantContactTelphone=18068334025, ddmerchantContactEmail=222@qq.com, bankName=/, cwmerchantContactName=名字, cwmerchantContactTelphone=18068334025, input3=广西壮族自治区贵港市, input5=3333, ddmerchantContactName=名字, input6=3333, input20=2018年 02月 11日, month=2, ddmerchantContactCz=/, merechantURL=/, input23=2019年 02月 10日, day=11, contract_num=K18-2000-801-03, templateName=dsmainfinancialword.docx, accessType=首选, bankAccount=/}
复制代码

OperatorWordUtil.replaceInFoot(document,templateParam);

复制代码
1 public static void replaceInFoot(XWPFDocument doc, Map<String, Object> params)
2 {
3 List footerList = doc.getFooterList();
4 if (footerList != null && footerList.size() > 0)
5 {
6 for (XWPFFooter xWPFFooter : footerList)
7 {
8 String text = xWPFFooter.getText();
9 LOGGER.info(“记得删除,遍历的text”+text);
10 System.out.println(“控制台记得删除,遍历的text”+text);
11 List xWPFParagraphs = xWPFFooter.getParagraphs();
12 if (xWPFParagraphs != null && xWPFParagraphs.size() > 0)
13 {
14 for (XWPFParagraph xWPFParagraph : xWPFParagraphs)
15 {
16 replaceInPara(xWPFParagraph, params);
17 }
18 }
19 }
20 }
21 }
复制代码
复制代码
1 public static void replaceInPara(XWPFParagraph paragraph, Map<String, Object> replaceMap)
2 {
3 List runs = paragraph.getRuns();
4 for (int i = 0; i < runs.size(); i++)
5 {
6 if (null == runs.get(i))
7 {
8 continue;
9 }
10 String oneparaString = runs.get(i).getText(runs.get(i).getTextPosition());
11 if (StringUtils.isEmpty(oneparaString))
12 {
13 continue;
14 }
15 for (Map.Entry<String, Object> entry : replaceMap.entrySet())
16 {
17 if(null != entry.getValue())
18 {
19 oneparaString = oneparaString.replace(entry.getKey().trim(), (String.valueOf(entry.getValue())!=null? String.valueOf(entry.getValue()): “”));
20 }
21 else
22 {
23 oneparaString = oneparaString.replace(entry.getKey().trim(), “”);
24 }
25 }
26 if(oneparaString!=null && oneparaString.indexOf(breakFlag)>-1){//需要换行
27 LOGGER.info("----->需要手动换行的"+oneparaString);
28 String[] breaks = oneparaString.split(breakFlag);
29 int index = 0;
30 for(String breakinfo:breaks){
31 runs.get(i).setText(breakinfo, index);
32 runs.get(i).addBreak();//换行
33 index++;
34 }
35 }else{//不用换行,根据模板正常显示,原来的逻辑
36 runs.get(i).setText(oneparaString, 0);
37 }
38 }
39 }
复制代码

2.4.2,替换字段里的参数
复制代码
1 public static void replaceInPara(XWPFDocument doc, Map<String, Object> params)
2 {
3 Iterator iterator = doc.getParagraphsIterator();
4 XWPFParagraph para;
5 while (iterator.hasNext())
6 {
7 para = iterator.next();
8 replaceInPara(para, params);
9 }
10 }
复制代码

replaceInPara还是上面的方法
2.4.3,替换表格里面的变量
复制代码
1 public static void replaceInTable(XWPFDocument doc, Map<String, Object> params)
2 {
3
4 Iterator iterator = doc.getTablesIterator();
5 XWPFTable table;
6 List rows;
7 List cells;
8 List paras;
9 while (iterator.hasNext())
10 {
11 table = iterator.next();
12 rows = table.getRows();
13 for (XWPFTableRow row : rows)
14 {
15 cells = row.getTableCells();
16 for (XWPFTableCell cell : cells)
17 {
18 paras = cell.getParagraphs();
19 for (XWPFParagraph para : paras)
20 {
21 replaceInPara(para, params);
22 }
23 }
24 }
25 }
26 }
复制代码

replaceInPara还是上面的方法

一段XWPFParagraph一段的去读

读到每一段之后他会在吧这一段进行按照word文档当初设置的格式,假如是黏贴复制的话不会换行String oneparaString = runs.get(i).getText(runs.get(i).getTextPosition()),假如是手写的的话就会换行,所以要想把自己的变量单独的取出来必须手动的输入这个变量才会遍历的时候单独的把这个字段取出来。

一个段落就是一个XWPFParagraph,注意这个段落就是我们传统意义上的段落比如

一行就是String oneparaString = runs.get(i).getText(runs.get(i).getTextPosition()),但是行就不是我们传统意义上的行了,请见下图关于oneparaString的解释。它就是一行行的读的。眼睛看上去应该是一行,其实是三行,因为在写好的基础上,我们手动的添加了一个 I N P U T 6 INPUT6 INPUT6,一定注意是手动,而不是黏贴复制的。

假如是表格的话,就会在每个单元格算一个段落,word文档中有时候表格是设置的,我们眼睛有时候看不到的,比如每个单元格就是一个cell,其实就是一个段落。

paras = cell.getParagraphs(); cell

在在单元格的基础上进行一行行的读,读的规则和上面的oneparaString 规则是一样的。

2.5,输出(生成临时的word文档,记得上传服务器之后在删除,否则会积累好多的垃圾数据的)
//生成临时word文件
fileName = newTemWord(outFilePath, templateName, document);

fileNames.append(fileName).append(",");

复制代码
1 private String newTemWord(String outFilePath, String templateName,
2 XWPFDocument document) throws FileNotFoundException, IOException
3 {
4 String outTempFilePath = wordFile + File.separator + outFilePath;
5 FileOutputStream outStream = null;
6 String fileName = System.currentTimeMillis() + templateName;
7 String filePath = outTempFilePath+ File.separator + fileName;
8 outStream = new FileOutputStream(filePath);
9 document.write(outStream);
10 outStream.close();
11 return fileName;
12 }
复制代码

2.6,把多个word文档合并成一个word文档
finalWordName = getFinalWordDoc(fileNames.toString(),outFilePath);
log.info(“合同生成成功1!合同名称:”+finalWordName);

复制代码
1 private String getFinalWordDoc(String fileNames,String outFilePath)
2 {
3 String outTempFilePath = wordFile + File.separator + outFilePath;
4 String finalWordName = OperatorWordUtil.mergeWordDocx(outTempFilePath, fileNames);
5 return finalWordName;
6 }
复制代码
复制代码
1 public static String mergeWordDocx(String outTempFilePath,String fileNames)
2 {
3 String finalWordName = “”;
4 if(fileNames.length() > 0 && fileNames != null)
5 {
6 FileInputStream InStream = null;
7 String filePath = “”;
8 List InputStreams = new ArrayList();
9 String[] fileNameArray = fileNames.toString().split(",");
10 InputStream InputStream = null;
11 OutputStream outputStream = null;
12 try
13 {
14 for(String fileName : fileNameArray)
15 {
16 filePath = outTempFilePath + File.separator + fileName;
17 InStream = new FileInputStream(filePath);
18 InputStreams.add(InStream);
19 }
20
21 //合并word
22 InputStream = mergeDocx(InputStreams,outTempFilePath);
23 finalWordName = System.currentTimeMillis()+".docx";
24 outputStream = new FileOutputStream(outTempFilePath+File.separator+finalWordName);
25 int bytesWritten = 0;
26 int byteCount = 0;
27 byte[] bytes = new byte[100000000];
28 while ((byteCount = InputStream.read(bytes)) != -1)
29 {
30 outputStream.write(bytes, bytesWritten, byteCount);
31 bytesWritten += byteCount;
32 }
33 }
34 catch (Exception e)
35 {
36 e.printStackTrace();
37 }
38 finally {
39 try {
40 if (null != outputStream) {
41 outputStream.close();
42 }
43
44 if (null != InputStream) {
45 InputStream.close();
46 }
47
48 if (null != InStream) {
49 InStream.close();
50 }
51 } catch (Exception e2) {
52 LOGGER.error(“OperatorWordUtil close stream failed!”, e2);
53 }
54 }
55 }
56 return finalWordName;
57 }
复制代码
复制代码
1 public static InputStream mergeDocx(final List streams,String outTempFilePath) throws Docx4JException, IOException
2 {
3 WordprocessingMLPackage target = null;
4 File tmpdir = new File(outTempFilePath);
5 final File generated = File.createTempFile(“generated”, “.docx”,tmpdir);
6 int chunkId = 0;
7 Iterator it = streams.iterator();
8 while (it.hasNext())
9 {
10 InputStream is = it.next();
11 if (is != null)
12 {
13 if (target == null)
14 {
15 // Copy first (master) document
16 OutputStream os = new FileOutputStream(generated);
17 os.write(IOUtils.toByteArray(is));
18 os.close();
19
20 target = WordprocessingMLPackage.load(generated);
21 }
22 else
23 {
24 // Attach the others (Alternative input parts)
25 insertDocx(target.getMainDocumentPart(), IOUtils.toByteArray(is), chunkId++);
26 }
27 }
28 }
29
30 if (target != null)
31 {
32 target.save(generated);
33 return new FileInputStream(generated);
34 }
35 else
36 {
37 return null;
38 }
39 }
复制代码
复制代码
1 private static void insertDocx(MainDocumentPart main, byte[] bytes, int chunkId)
2 {
3 try
4 {
5 AlternativeFormatInputPart afiPart = new AlternativeFormatInputPart(new PartName("/part" + chunkId + “.docx”));
6 afiPart.setContentType(new ContentType(ContentTypes.WORDPROCESSINGML_DOCUMENT));
7 afiPart.setBinaryData(bytes);
8 Relationship altChunkRel = main.addTargetPart(afiPart);
9 CTAltChunk chunk = Context.getWmlObjectFactory().createCTAltChunk();
10 chunk.setId(altChunkRel.getId());
11 main.addObject(chunk);
12 }
13 catch (Exception e)
14 {
15 e.printStackTrace();
16 }
17 }
复制代码
1 public final static String WORDPROCESSINGML_DOCUMENT = “application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml”;

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值