项目场景:
最近公司需要开发一个新功能,就是需要获取微信的审批信息来生成pdf文件,审批申请单是固定一个类似表格的样式,但是表格的内容会变的,有多有少,并且还有审批节点是动态配置的,比较麻烦。所以当时笔者就想到了用html模板来做,先用html来做一个大概的模板像是,里面的内容用变量来替换。然后生成pdf的插件就用wkhtmltopdf,因为这个之前用过,效果还不错。
开发准备
首先进入wkhtmltopdf官网去下载工具。如图所示,首先开发在自己电脑上就下载Windows版本。
下载之后打开是一个安装包,点击运行一直下一步就行,安装完成之后需要配置本地环境变量。如下图所示:
配置好之后,打开cmd命令窗口,输入wkhtmltopdf html路径 pdf保存路径
html路径:可以是本地的html文件路径,也可以是url链接
pdf路径:这个是保存本地的路径或者服务器的路径
输完命令直接回车,然后会显示进度条,完成之后就可以去查看生成的pdf了。
代码实现(wkhtmltopdf版本):
Controller类:
package com.ccic.webapi.controller;
import com.ccic.webapi.service.AuditService;
import com.ccic.webapi.util.Result;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* TOO
*
* @author hj
* @version 1.0
*/
@RestController
@Slf4j
@RequestMapping("/audit/api")
@Api(tags = "审核")
public class AuditController {
@Autowired
private AuditService auditService;
/**
* 生成pdf
* @param spNo
* @param templateId
* @return
*/
@GetMapping("/auditToPdf")
public String auditToPdf(@RequestParam String spNo){
return auditService.auditToPdf(spNo);
}
}
Service类:
package com.ccic.webapi.service;
/**
* TOO
*
* @author hj
* @version 1.0
*/
public interface AuditService {
String auditToPdf(String spNo);
}
ServiceImpl实现层:
package com.example.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.example.mapper.AddrsbDepartmentMapper;
import com.example.mapper.TAddrsbEmpMapper;
import com.example.service.AuditService;
import com.example.util.WkhtmltopdfUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.*;
/**
* TOO
*
* @author hj
* @version 1.0
*/
@Service
@Slf4j
public class AuditServiceImpl implements AuditService {
//获取审批申请详情
public static String getapprovaldetail = "https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldetail?access_token=ACCESS_TOKEN";
private String[] msgtypeStr = {"Text", "Textarea", "Number", "Money", "Date", "Contact", "Tips"};
@Value("${basePath}")
private String bathBase;
@Value("${wechat.cp.httpProxyHost}")
private String httpProxyHost;
@Value("${wechat.cp.httpProxyPort}")
private String httpProxyPort;
/**
* 生成pdf
* @param spNo
*/
public String auditToPdf(String spNo){
String access_token = "";//调用凭证
String url = getapprovaldetail.replaceAll("ACCESS_TOKEN",access_token);
JSONObject json = new JSONObject();
json.put("sp_no",spNo);
//调用详情接口
String res = HttpRequest.post(url).body(json.toJSONString()).execute().body();
String res = json;
//拿到结果
JSONObject jsonObject = JSON.parseObject(res);
if(jsonObject.containsKey("errcode") && jsonObject.getInteger("errcode") == 0){
try {
JSONObject object = jsonObject.getJSONObject("info");
//以下都是获取审批详情的内容,放到变量里面,倒是做替换使用
String spName = object.getString("sp_name");
String spbh = object.getString("sp_no");
String empCn = object.getJSONObject("applyer").getString("userid");
String comeName = object.getJSONObject("applyer").getString("partyid");
String date = DateUtil.format(DateUtil.date(object.getLong("apply_time")*1000),"yyyy年MM月dd日 HH时mm分ss秒");
//内容,这个内容根据自己的实际模板情况去拼接,因为拿到的内容是集合,需要自己去处理,自立就不放了
String content = this.pjtr(object);
//审批节点,也是一样,同理获取审批内容,自行处理
Map<String, Object> map = this.getAuditNode(object);
//抄送人
String csr = "";
//备注信息
String remarks = "";
//获取模版,这个是获取html的模板内容,方法在下面会贴出
String mbnr = getResource("/template/shmb.html");
String str = mbnr;
//获取章,这个是水印,也可以认为是盖章,把png图片转成base64字符码。
String zhang = getResource("/template/shtg.png.base64");
//html中的模板变量,我都用中文【】标记,以便于替换真正的内容
str = str.replaceAll("【审批模板名字】",isNullToChar(spName));
str = str.replaceAll("【审批编号】",isNullToChar(spbh));
str = str.replaceAll("【applyer.userid】",StrUtil.isBlank(isNullToChar(empCn)) ? "涂烔" : isNullToChar(empCn));
str = str.replaceAll("【applyer.party】",isNullToChar(comeName));
str = str.replaceAll("【apply_time】",isNullToChar(date));
//这里我是要替换一大推内容,不确定性,content里面还包含了html代码片段。
str = str.replaceAll("<content2></content2>",isNullToChar(content));
str = str.replaceAll("【执行部门姓名】",StrUtil.isBlank(isNullToChar(map.get("zxbmxm"))) ? "涂烔" : isNullToChar(map.get("zxbmxm")));
str = str.replaceAll("【执行部门意见】",isNullToChar(map.get("zxbmyj")));
str = str.replaceAll("【归属部门姓名】",StrUtil.isBlank(isNullToChar(map.get("gsbmxm"))) ? "涂烔" : isNullToChar(map.get("gsbmxm")));
str = str.replaceAll("【归属部门意见】",isNullToChar(map.get("gsbmyj")));
str = str.replaceAll("【抄送人】",isNullToChar(csr));
str = str.replaceAll("【备注信息】",isNullToChar(remarks));
//判断是否审核通过,通过才有章
if(object.getInteger("sp_status") == 2){
//获取章
String zhang = getResource("/template/shtg.png.base64");
str = str.replaceAll("【chapterSrc】",zhang);
//这里在html模板上做了一个隐藏的标记,也可以说是样式
str = str.replaceAll("【chapterShow】","inline-block");
}
//先生成html文件,并返回html文件路径
String htmlPath = this.updateFile(str,spName);
//pdf的保存路径
String pdfPath = bathBase + spName + "new" + ".pdf";
//生成pdf操作,具体实现方法后面贴出
WkhtmltopdfUtil.html2pdf(htmlPath,pdfPath);
return pdfPath;
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
public String getAuditStatus(Integer status){
String s = "";
if(1 == status){
s = "审批中";
}else if(2 == status){
s = "已同意";
}else if(3 == status){
s = "已驳回";
}else if(4 == status){
s = "已转审";
}else if(11 == status){
s = "已退回";
}else if(12 == status){
s = "已加签";
}else if (13 == status){
s = "已同意并加签";
}
return s;
}
//去除null值
public static String isNullToChar(Object st){
st = st == null ? "" : st;
return String.valueOf(st);
}
/**
* 拼接审批内容<tr><td> 的
* @param jsonObject
* @return
*/
public String pjtr(JSONObject jsonObject){
List<String> msgTypeList = Arrays.asList(msgtypeStr);
StringBuffer sb = new StringBuffer("");
JSONArray jsonArray = jsonObject.getJSONObject("apply_data").getJSONArray("contents");
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject object = jsonArray.getJSONObject(i);
String control = object.getString("control");
JSONObject title = object.getJSONArray("title").getJSONObject(0);
JSONObject value = object.getJSONObject("value");
if(msgTypeList.contains(control)){
sb.append("<tr height=\"30px\">");
sb.append("<td>"+title.getString("text")+"</td>");
sb.append("<td colspan=\"2\">"+value.getString("text")+"</td>");
sb.append("</tr>");
}
}
return sb.toString();
}
//获取html静态文件内容,这个是在resources下面建了一个文件夹template
public static String getResource(String path){
log.info("path:{}",path);
try {
InputStream is = AuditServiceImpl.class.getResourceAsStream(path);
InputStreamReader reader = new InputStreamReader(is);
BufferedReader br = new BufferedReader(reader);
String s;
StringBuffer sb = new StringBuffer("");
while ((s=br.readLine()) != null){
sb.append(s+"\n");
}
if(StrUtil.isBlank(sb.toString())) throw new RuntimeException("RSA KEY IS NULL");
return sb.toString();
}catch (Exception e){
log.error("获取RSA KEY RESOURCE失败:{}",e.getMessage());
}
return null;
}
//生成html文件并保存在服务器
public String updateFile(String html,String fileName){
String htmlpath = bathBase + "wshtml" + File.separator + fileName + ".html";
File file = new File(htmlpath);
try{
if(!file.getParentFile().exists()){
file.getParentFile().mkdir();
}
if(!file.exists()){
file.createNewFile();
}
FileOutputStream outputStream = new FileOutputStream(file);
byte[] b = html.getBytes();
outputStream.write(b);
outputStream.close();
}catch (Exception e){
log.error("异常:{}",e.getMessage());
}
return htmlpath;
}
}
WkhtmltopdfUtil类:主要是生成pdf的方法类
package com.example.util;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
/**
* html转pdf 注意:运行前需要先安装 wkhtmltopdf 并加入path环境变量
* 下载 https://wkhtmltopdf.org/downloads.html
* 调用及参数设置
* 中文乱码或者空白解决方法:
* 如果wkhtmltopdf中文显示空白或者乱码方框
* 打开windows c:\Windows\fonts\simsun.ttc拷贝到linux服务器/usr/share/fonts/目录下,再次生成pdf中文显示正常
*/
@Slf4j
public class WkhtmltopdfUtil {
private static final String toPdfTool = "wkhtmltopdf";
private static final String pageSize = "--page-size A4"; //纸张类型尺寸
private static final String smart="--disable-smart-shrinking"; //smart
private static final String encode="--encoding UTF-8"; //编码格式
//页边距 左右上下的边距
private static final String margin = "--margin-left 16mm --margin-right 16mm --margin-top 14mm --margin-bottom 16mm";
/**
* html转pdf
* @param htmlpath 本地文件或者网络文件地址
* @param pdfpath 目标文件
* @param
*/
public static String html2pdf(String htmlpath, String pdfpath) {
StringBuilder cmd = new StringBuilder();
cmd.append(smart);
cmd.append(" ");
cmd.append(encode);
cmd.append(" ");
cmd.append(pageSize);
cmd.append(" ");
cmd.append(margin);
html2pdf(htmlpath, pdfpath, cmd.toString());
return pdfpath;
}
//生成方法,它是通过运行cmd命令来实现的
public static String html2pdf(String htmlpath, String pdfpath, String prop) {
log.info("html2pdf2方法中传递的htmlpath为:{},pdfpath为:{}",htmlpath,pdfpath);
File file = new File(pdfpath);
File parent = file.getParentFile();
if(!parent.exists()){
parent.mkdirs();
}
StringBuilder cmd = new StringBuilder();
cmd.append(toPdfTool);
cmd.append(" ");
cmd.append(prop);
cmd.append(" ");
cmd.append(htmlpath);
cmd.append(" ");
cmd.append(pdfpath);
try {
log.info("执行的cmd命令为:{}",cmd.toString());
Process process = Runtime.getRuntime().exec(cmd.toString());
BufferedInputStream bis = new BufferedInputStream(process.getErrorStream());
BufferedReader br = new BufferedReader(new InputStreamReader(bis));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
if (process.exitValue() != 0) {
System.out.println("wkhtmltopdf命令未正常结束" + htmlpath + " " + pdfpath);
}
bis.close();
br.close();
} catch (IOException e) {
log.error("html转换为Pdf/IO流异常,异常信息为:{}", e.getMessage());
} catch (Exception e) {
log.error("html转换为Pdf发生异常,异常信息为:{}", e.getMessage());
}
return pdfpath;
}
}
后面部署发现的问题:wkhtmltopdf
开发完之后肯定要部署一版做测试,但是发现部署成了问题。因为笔者在做开发的时候实在windows环境下做的开发,一切都很顺利,但是要部署的话肯定实在linux上的,单纯的linux也不麻烦,按照官网下载linux版本去安装就行了。关键在于我这边的环境部署是OC容器,然后上面的人说要安装软件很麻烦,又要走流程啥的,就让我整其它办法。实在不行再来装这个。没办法,换呗,从网上找来找去,决定试试IText7。
解决方案:换成IText7
引入依赖
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>2.1.4</version>
</dependency>
换成IText7之后发现,需要字体,并且还对html的样式有严格要求,之前的模板转换不过来,没办法,一步一步调,先把字体搞定,再调样式。
容器环境,我也不知道去哪找字体,就想到把字体放在项目里面,通过静态文件去读取。
说干就干,如下:
private static final String FONT = "./src/main/resources/font/simhei.ttf";
/**
* 设置BaseFont
* @param fontPath 字体路径
* @return
*/
private static ConverterProperties creatBaseFont(String fontPath) {
if(StrUtil.isBlank(fontPath)) {
fontPath = FONT;
}
ConverterProperties properties = new ConverterProperties();
FontProvider fontProvider = new DefaultFontProvider();
FontProgram fontProgram;
try {
//这里在linux死活找不到路径
fontProgram = FontProgramFactory.createFont(fontPath);
fontProvider.addFont(fontProgram);
properties.setFontProvider(fontProvider);
} catch (IOException e) {
log.error("creat base font erro" , e );
}
return properties;
}
一开始不管改成什么路径,打包之后在linux都找不到字体路径,但是在windows下又是可以的,一顿百度,然后找到了大佬的博文,把字体路径以文件流的方式传过去。如下代码:
大佬博文:
package com.ccic.webapi.util;
import cn.hutool.core.util.StrUtil;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider;
import com.itextpdf.io.font.FontProgram;
import com.itextpdf.io.font.FontProgramFactory;
import com.itextpdf.layout.font.FontProvider;
import lombok.extern.slf4j.Slf4j;
import .ClassPathResource;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* TOO
*
* @author hj
* @version 1.0
*/
@Slf4j
public class Pdf7Kit {
private static byte[] toByteArray(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
}
return output.toByteArray();
}
/**
* 设置BaseFont
* @param fontPath 字体路径
* @return
*/
private static ConverterProperties creatBaseFont(String fontPath) {
ClassPathResource resource = null;
if(StrUtil.isBlank(fontPath)) {
resource = new ClassPathResource("font/simhei.ttf");
}
ConverterProperties properties = new ConverterProperties();
FontProvider fontProvider = new DefaultFontProvider();
FontProgram fontProgram;
try {
fontProgram = FontProgramFactory.createFont(Pdf7Kit.toByteArray(resource.getInputStream()));
fontProvider.addFont(fontProgram);
properties.setFontProvider(fontProvider);
} catch (IOException e) {
log.error("creat base font erro" , e );
}
return properties;
}
/**
* 将html文件转换成pdf
* @param htmlPath
* @param pdfPath
* @param fontPath
* @throws IOException
*/
public static void creatPdf(String htmlPath , String pdfPath,String fontPath) throws IOException {
if(StrUtil.isBlank(htmlPath) || StrUtil.isBlank(pdfPath)) {
log.warn("html2pdf fail. htmlPath or pdfPath is null .");
return;
}
// 拼接html路径
String src = htmlPath;
ConverterProperties properties = creatBaseFont(fontPath);
HtmlConverter.convertToPdf(new File(src), new File(pdfPath),properties);
}
}
字体解决之后,html样式就一步一步调整了。最后还是做出来了。
总结
技术无止境,我们要做的就是跟上脚步。