手摸手系列之 Java 通过 PDF 模板生成 PDF 功能
集团 SaaS 平台目前需要实现导出 PDF 格式的电子委托协议功能。业务方已经提供了一个现成的 PDF 文件作为参考。针对这一需求,我们有两个可行的方案:
- 完全代码生成:根据 PDF 文件的外观,完全通过代码动态生成 PDF 文件。
- 模板填充:将现有的 PDF 文件作为模板,仅需在代码中填充真实数据即可生成最终的 PDF 文件。
从实现效率和开发速度的角度来看,方案二(模板填充)无疑是更优的选择。它不仅能够大幅减少开发工作量,还能确保生成的 PDF 文件与业务方提供的模板完全一致,避免样式偏差。接下来,我们将重点探讨如何通过模板填充的方式实现这一功能。
一、PDF 模板制作
首先通过 PDF 编辑器制作 PDF 模板,这里我选用 Adobe Acrobat Pro 编辑表单来实现,这里我主要用到了表单的文本域和复选框。
工具我放云盘,需要的自取:https://caiyun.139.com/m/i?105CqcMLSgEyR 提取码:6ais
文本域的 name 对应 Java 中 model 类的属性。
二、前端编码
// html
<a-button v-has="'dec:down'" type="primary" icon="printer" :loading="printBatchLoading" @click="handlePrintBatch">批量打印</a-button>
// JavaScript
/**
* 批量打印
*/
handlePrintBatch(){
if (this.selectedRowKeys.length == 0) {
this.$message.error('请选择至少一票数据!')
return
}
let params = {}
params.ids = this.selectedRowKeys.join(',')
this.printBatchLoading = true
let fileName = ''
if (this.selectedRowKeys.length > 1) {
fileName = '电子委托协议批量导出.zip'
} else {
fileName = '电子委托协议导出' + (this.selectionRows[0].consignNo ? this.selectionRows[0].consignNo :
this.selectionRows[0].id) + '.pdf'
}
downloadFile(this.url.exportElecProtocolBatch, fileName,
params).then((res) => {
if (res.success) {
} else {
this.$message.warn(`导出失败!${res.message}`)
}
}).finally(() => {
this.printBatchLoading = false
})
}
/**
* 下载文件
* @param url 文件路径
* @param fileName 文件名
* @param parameter
* @returns {*}
*/
export function downloadFile(url, fileName, parameter) {
return downFile(url, parameter).then((data) => {
if (!data || data.size === 0) {
Vue.prototype['$message'].warning('文件下载失败')
return
}
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(new Blob([data]), fileName)
} else {
let url = window.URL.createObjectURL(new Blob([data]))
let link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link) //下载完成移除元素
window.URL.revokeObjectURL(url) //释放掉blob对象
}
})
}
三、后端编码
/**
* 批量打印电子委托协议
*
* @param ids
* @return org.jeecg.common.api.vo.Result<?>
* @author ZHANGCHAO
* @date 2025/1/16 08:54
*/
@Override
public void exportElecProtocolBatch(String ids, HttpServletResponse response) {
try {
// 获取协议列表
List<ElecProtocol> elecProtocolList = fetchProtocolsByIds(ids);
if (isEmpty(elecProtocolList)) {
throw new RuntimeException("未获取到电子委托协议数据");
}
if (elecProtocolList.size() == 1) {
// 单个文件导出
ElecProtocol protocol = elecProtocolList.get(0);
Map<String, Object> data = prepareDataMap(protocol);
byte[] pdfBytes = generatePdf(data);
// 设置响应头
String pdfFileName = URLEncoder.encode(
"电子委托协议_" + (isNotBlank(protocol.getConsignNo()) ? protocol.getConsignNo() : protocol.getId()) + ".pdf",
"UTF-8");
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=" + pdfFileName);
try (ServletOutputStream outputStream = response.getOutputStream()) {
outputStream.write(pdfBytes);
outputStream.flush();
}
} else {
// 多个文件压缩成 ZIP 导出
response.setContentType("application/zip");
String zipFileName = URLEncoder.encode("电子委托协议导出.zip", "UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=" + zipFileName);
try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) {
for (ElecProtocol protocol : elecProtocolList) {
Map<String, Object> data = prepareDataMap(protocol);
byte[] pdfBytes = generatePdf(data);
String pdfFileName = "电子委托协议_" +
(isNotBlank(protocol.getConsignNo()) ? protocol.getConsignNo() : protocol.getId()) + ".pdf";
ZipEntry zipEntry = new ZipEntry(pdfFileName);
zipOut.putNextEntry(zipEntry);
zipOut.write(pdfBytes);
zipOut.closeEntry();
}
}
}
} catch (Exception e) {
log.error("导出电子委托协议失败: {}", e.getMessage(), e);
throw new RuntimeException("导出失败,请稍后再试");
}
}
/**
* 获取数据
*
* @param ids
* @return java.util.List<org.jeecg.modules.business.entity.ElecProtocol>
* @author ZHANGCHAO
* @date 2025/1/16 16:24
*/
private List<ElecProtocol> fetchProtocolsByIds(String ids) {
return baseMapper.selectBatchIds(Arrays.asList(ids.split(",")));
}
/**
* 处理数据
*
* @param elecProtocol
* @return java.util.Map<java.lang.String, java.lang.Object>
* @author ZHANGCHAO
* @date 2025/1/16 16:23
*/
private Map<String, Object> prepareDataMap(ElecProtocol elecProtocol) {
Map<String, Object> map = new HashMap<>();
map.put("consignorName", elecProtocol.getConsignorName());
map.put("trusteeName", elecProtocol.getTrusteeName());
map.put("gName", elecProtocol.getGName());
if (isNotBlank(elecProtocol.getEntryId())) {
map.put("entryId", "No." + elecProtocol.getEntryId());
}
map.put("codeTs", elecProtocol.getCodeTs());
map.put("receiveDate", elecProtocol.getReceiveDate());
map.put("ieDate", elecProtocol.getIeDate());
map.put("billCode", elecProtocol.getBillCode());
if (isNotBlank(elecProtocol.getTradeMode())) {
List<DictModelVO> jgfs = decListMapper.getDictItemByCode("JGFS");
List<DictModelVO> dictModelVO1=jgfs.stream().filter(i->i.getValue()
.equals(elecProtocol.getTradeMode()))
.collect(Collectors.toList());
map.put("tradeMode", isNotEmpty(dictModelVO1) ? dictModelVO1.get(0).getText() : "");
}
map.put("qtyOrWeight", elecProtocol.getQtyOrWeight());
map.put("packingCondition", elecProtocol.getPackingCondition());
map.put("paperinfo", elecProtocol.getPaperinfo());
if (isNotBlank(elecProtocol.getOriCountry())) {
List<DictModel> dictModels2 = sysBaseApi.getDictItems("erp_countries,name,code");
Map<String, String> dictMap2 = new HashMap<>();
if (isNotEmpty(dictModels2)) {
dictModels2.forEach(dictModel -> {
dictMap2.put(dictModel.getValue(), dictModel.getText());
});
}
if(dictMap2.containsKey(elecProtocol.getOriCountry())) {
map.put("oriCountry", dictMap2.get(elecProtocol.getOriCountry()));
}
}
if (isNotBlank(elecProtocol.getDeclarePrice())) {
if (isNotBlank(elecProtocol.getCurr())) {
// 币制
List<DictModel> dictModels3 = sysBaseApi.getDictItems("erp_currencies,name,code,1=1");
Map<String, String> dictMap3 = new HashMap<>();
if (isNotEmpty(dictModels3)) {
dictModels3.forEach(dictModel -> {
dictMap3.put(dictModel.getValue(), dictModel.getText());
});
}
if(dictMap3.containsKey(elecProtocol.getCurr())) {
map.put("declarePrice", dictMap3.get(elecProtocol.getCurr()) + ": " + elecProtocol.getDeclarePrice() + "元");
} else {
map.put("declarePrice", elecProtocol.getDeclarePrice());
}
} else {
map.put("declarePrice", elecProtocol.getDeclarePrice());
}
}
map.put("otherNote", elecProtocol.getOtherNote());
map.put("promiseNote", elecProtocol.getPromiseNote());
String dateStr = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN);
map.put("dateStr", dateStr);
map.put("printTime", dateStr + " " + DateUtil.format(new Date(), DatePattern.NORM_TIME_PATTERN));
// 处理选框逻辑
String paperinfo = elecProtocol.getPaperinfo();
if (isNotBlank(paperinfo) && paperinfo.length() == 6) {
for (int i = 0; i < paperinfo.length(); i++) {
if (paperinfo.charAt(i) == '1') {
map.put("gou" + (i + 1), "On");
}
}
}
return map;
}
/**
* 生成PDF
*
* @param data
* @return byte[]
* @author ZHANGCHAO
* @date 2025/1/16 16:24
*/
private byte[] generatePdf(Map<String, Object> data) throws Exception {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
PdfReader reader = new PdfReader(this.getClass().getResourceAsStream("/templates/pdf/电子委托协议模板.pdf"));
PdfStamper stamper = new PdfStamper(reader, bos);
AcroFields form = stamper.getAcroFields();
BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
ArrayList<BaseFont> fontList = new ArrayList<>();
fontList.add(bf);
form.setSubstitutionFonts(fontList);
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (entry.getKey().contains("gou")) {
form.setField(entry.getKey(), isNotEmpty(entry.getValue()) ? entry.getValue().toString() : "", true);
} else {
form.setField(entry.getKey(), isNotEmpty(entry.getValue()) ? entry.getValue().toString() : "");
}
}
stamper.setFormFlattening(true);
stamper.close();
return bos.toByteArray();
}
}
同时支持导出单个和批量,单个是直接生成PDF文件,批量是打成压缩包。
四、效果展示
页面数据:
生成的 PDF:
总结
总得来说,Java通过itext PDF模板生成PDF文件功能很简单,主要是数据的填充而已。还可以继续丰富下,比如多行文本、自动换行功能等。