springboot组装数据使用poi-tl填充word模板,并转化为pdf响应给浏览器自动下载导出

Java后端

pom依赖

 <!--poi-tl-->
        <dependency>
            <groupId>com.deepoove</groupId>
            <artifactId>poi-tl</artifactId>
            <version>1.12.1</version>
        </dependency>
        <!--springEL-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>6.0.10</version>
        </dependency>
        <!--aspose 破解 word转pdf-->
        <dependency>
            <groupId>com.aspose</groupId>
            <artifactId>aspose-words</artifactId>
            <version>15.8.0-jdk16</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/aspose-words-15.8.0-jdk16.jar</systemPath>
        </dependency>

1、controller类

@Slf4j
@RestController
@RequestMapping("/searchInfo")
public class SearchInfoController {

    @Resource
    private ISearchInfoService searchInfoService;
   
 /**
     * @param searchId 寻源id
     * 寻源比价记录导出
     */
    @PostMapping("/searchDetailExport")
    public void searchDetailExport(Long searchId){
        searchInfoService.searchDetailExport(searchId);
    }
}

2、service

public interface ISearchInfoService extends IService<SearchInfo> {   
 /**
     * @param searchId 寻源id
     * 寻源比价记录导出
     */
    void searchDetailExport(Long searchId);
}

3、serviceImpl

@Slf4j
public class SearchInfoServiceImpl extends ServiceImpl<SearchInfoMapper, SearchInfo> implements ISearchInfoService {

    @SneakyThrows
    @Override
    public void searchDetailExport(Long searchId) {
        //判断寻源比价状态,仅限已完成的寻源比价可以下载导出
        Integer state = this.getById(searchId).getState();
        if (!Objects.equals(SearchStateEnum.COMPLETE.getCode(), state)) {
            return;
        }
        //寻源比价基本信息
        ResponsesDetailVO responsesDetailVO = this.winResultDetail(searchId).getData();
        //附件
        String files = responsesDetailVO.getFiles();
        List<SearchInfoFileDTO> searchInfoFileList = JSON.parseArray(files).toJavaList(SearchInfoFileDTO.class);
        List<String> fileNameList = searchInfoFileList.stream().map(SearchInfoFileDTO::getName).collect(Collectors.toList());
        //格式化响应类型
        Integer responseTypeInter = responsesDetailVO.getResponseType();
        String responseType = "";
        if (responseTypeInter == 0) {
            responseType = "全部响应";
        } else if (responseTypeInter == 1){
            responseType = "部分响应";
        }
        //格式化报价截止时间
        String deadLineTime = responsesDetailVO.getDeadline().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        SearchInfoExportVO searchInfoExportVO = SearchInfoExportVO.builder()
                .responsesDetailVO(responsesDetailVO)
                .otherRemark(responsesDetailVO.getRemark())
                .attachmentFileNameList(fileNameList)
                .responseType(responseType)
                .deadline(deadLineTime)
                .productInfoVO(responsesDetailVO.getProductInfoVO())
                .build();
        //导出
        HttpServletResponse response = ServletUtils.getResponse();
        PDFUtils.exportSearchToPdf(searchInfoExportVO, response);
    }
}

4、需要导出数据的VO类

@Data
@Builder
public class SearchInfoExportVO {
    /**
     * 寻源比价基本信息
     */
    private ResponsesDetailVO responsesDetailVO;
    /**
     * 采购需求清单
     */
    private List<ProductInfoVO> productInfoVO;
    /**
     * 寻源比价备注
     */
    private String otherRemark;
    /**
     * 附件
     */
    private List<String> attachmentFileNameList;

    /**
     * 附件集合转化为指定格式列表
     */
    private NumberingRenderData attachmentFileNameData;
    /**
     * 报价响应类型
     */
    private String responseType;
    /**
     * 报价截止时间
     */
    private String deadline;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ResponsesDetailVO {


    /**
     * 需求标题
     */
    private String demandTitle;

    /**
     * 报价截止时间
     */
    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")//存到数据库
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") //从数据库读出
    private LocalDateTime deadline;

    /**
     * 采购周期开始时间
     */
    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")//存到数据库
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") //从数据库读出
    private LocalDateTime periodStartTime;

    /**
     * 采购周期结束时间
     */
    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")//存到数据库
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") //从数据库读出
    private LocalDateTime periodEndTime;

    /**
     * 采购周期
     */
    private String procurementCycle;

    /**
     * 采购单位
     */
    private String procurementUnit;

    /**
     * 报价响应类型 0:全部响应 1:部分响应
     */
    private Integer responseType;

    /**
     * 预算总价
     */
    private BigDecimal totalBudgetPrice;

    /**
     * 商品件数
     */
    private BigDecimal productNum;

    /**
     * 公示天数
     */
    private Integer publicityDay;

    /**
     * 采购需求清单
     */
    private List<ProductInfoVO> productInfoVO;

    /**
     * 供应商响应报价-全部响应
     */
    private List<SupResponseAll> supResponseAll;

    /**
     * 供应商响应报价-部分响应
     */
    private List<SupResponsePart> supResponsePart;

    /**
     * 中选结果
     */
    private List<SelectedResult> selectedResults;

    /**
     * 送货方式
     */
    private String deliveryWay;

    /**
     * 送货期限
     */
    private String deliveryDeadline;

    /**
     * 送货地址
     */
    private String deliveryAddress;

    /**
     * 备注
     */
    private String remark;

    /**
     * 附件
     */
    private String files;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ProductInfoVO {

    /**
     * 商品名称
     */
    private String productName;

    /**
     * 品牌
     */
    private String brand;

    /**
     * 型号
     */
    private String model;

    /**
     * 单位
     */
    private String unit;

    /**
     * 数量
     */
    private BigDecimal num;

    /**
     * 预算单价
     */
    private BigDecimal price;

    /**
     * 备注
     */
    private String remark;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SupResponseAll {

    /**
     * 供应商名称
     */
    private String supplierName;

    /**
     * 联系人
     */
    private String linkman;

    /**
     * 联系人手机号码
     */
    private String phone;

    /**
     * 报价时间
     */
    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")//存到数据库
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") //从数据库读出
    private LocalDateTime offerTime;

    /**
     * 中选总价
     */
    private BigDecimal totalPrice;

    /**
     * 中选商品数量
     */
    private BigDecimal productNum;

    /**
     * 排名
     */
    private Integer ranking;

    /**
     * 报价明细
     */
    private List<QuotationDetail> quotationDetail;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class QuotationDetail {

    /**
     * 商品名称
     */
    private String productName;

    /**
     * 品牌
     */
    private String brand;

    /**
     * 型号
     */
    private String model;

    /**
     * 单位
     */
    private String unit;

    /**
     * 数量
     */
    private BigDecimal num;

    /**
     * 单价报价
     */
    private BigDecimal unitPrice;

    /**
     * 商品sku
     */
    private String productSku;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SelectedResult {

    /**
     * 中选供应商
     */
    private String supplierName;

    /**
     * 中选总价
     */
    private BigDecimal totalPrice;

    /**
     * 中选商品数量
     */
    private BigDecimal productNum;

    /**
     * 中选商品
     */
    private List<SelectedProduct> selectedProducts;

}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SelectedProduct {

    /**
     * 商品名称
     */
    private String productName;

    /**
     * 品牌
     */
    private String brand;

    /**
     * 型号
     */
    private String model;

    /**
     * 单位
     */
    private String unit;

    /**
     * 数量
     */
    private BigDecimal num;

    /**
     * 中选单价
     */
    private BigDecimal selectedPrice;

}

5、导出工具类

public class PDFUtils {

    private final static String SEARCH_TEMPLATE_RESOURCE = "D:/projects/dev/cx-mall-cloud/cx-modules/cx-mall/src/main/resources/templates/search.docx";

    /**
     * poi-tl 寻源比价word导出PDF
     * @throws Exception 异常
     */
    public static void exportSearchToPdf(SearchInfoExportVO data, HttpServletResponse response) throws Exception {
        //表格行循环插件
        LoopRowTableRenderPolicy loopRowTableRenderPolicy = new LoopRowTableRenderPolicy();
        NumberingRenderData numberingRenderData = Numberings.ofDecimal(data.getAttachmentFileNameList().toArray(new String[0])).create();
        data.setAttachmentFileNameData(numberingRenderData);
        Configure config = Configure.builder().useSpringEL(false)
                .bind("productInfoVO", loopRowTableRenderPolicy)
                .bind("quotationDetail", loopRowTableRenderPolicy)
                .bind("selectedProducts", loopRowTableRenderPolicy)
                .build();
        try (XWPFTemplate template = XWPFTemplate.compile(SEARCH_TEMPLATE_RESOURCE, config).render(data)) {
            ByteArrayOutputStream fos = new ByteArrayOutputStream();
            template.write(fos);
            Word2PdfAsposeUtil.doc2pdf(fos, response);
        }
    }
}
@Slf4j
public class Word2PdfAsposeUtil {

    private static boolean getLicense() {
        boolean result = false;
        try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("license.xml")) {
            // License的包路径必须为com.aspose.words.License
            License license = new License();
            license.setLicense(in);
            result = true;
        } catch (Exception e) {
            log.info(e.getMessage());
        }
        return result;
    }

    public static void doc2pdf(OutputStream outputStream, HttpServletResponse response) throws IOException {
        if (!getLicense()) { // 验证License 若不验证则转化出的pdf文档会有水印产生
            return;
        }
        OutputStream out = response.getOutputStream();
        FileInputStream fileInputStream = null;
        try {
            long old = System.currentTimeMillis();
            response.setCharacterEncoding("utf-8");
            response.setHeader(
                    "Content-Disposition",
                    "attachment;filename=".concat(URLEncoder.encode("test.pdf", StandardCharsets.UTF_8)));
            response.setContentType("application/pdf");
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream = (ByteArrayOutputStream) outputStream;
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            Document doc = new Document(byteArrayInputStream); // Address是将要被转化的word文档
            doc.save(out, SaveFormat.PDF);// 全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF,
            IOUtils.copy(byteArrayInputStream, out);
            // EPUB, XPS, SWF 相互转换
            long now = System.currentTimeMillis();
            System.out.println("pdf转换成功,共耗时:" + ((now - old) / 1000.0) + "秒"); // 转化用时
   
        } catch (Exception e) {
            log.info(e.getMessage());
        }finally {
            if (out != null && fileInputStream != null) {
                try {
                    out.flush();
                    PoitlIOUtils.closeQuietlyMulti(out, fileInputStream);
                } catch (IOException e) {
                    log.info(e.getMessage());
                }
            }
        }
    }
}

6、word模板文件(wps/office创建都行,创建好之后放在业务代码对应模块的/resources/templates路径下,例如:src/main/resources/templates/search.docx)

模板中页眉设置插入页码,导出可自动分页:

license放到resources下面 license.xml(破解PDF水印等)

<?xml version="1.0" encoding="UTF-8" ?>
<License>
    <Data>
        <Products>
            <Product>Aspose.Total for Java</Product>
            <Product>Aspose.Words for Java</Product>
        </Products>
        <EditionType>Enterprise</EditionType>
        <SubscriptionExpiry>20991231</SubscriptionExpiry>
        <LicenseExpiry>20991231</LicenseExpiry>
        <SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>
    </Data>
    <Signature>sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=</Signature>
</License>

导出效果:

注:注意往word模板中填充数据对象字段层级间的关系

poi-tl官方文档:Poi-tl Documentation

参考博客:使用poi-tl填充word模板,并转化为pdf输出(兼容word输出)_poi-tl转成pdf返回前端预览-CSDN博客poi-tl导出复杂word/pdf文档(去除aspose转pdf水印,多张图片、表格循环行合并列、表格带图片、循环多个模板导出,多个文档合并)_poi-tl导出pdf-CSDN博客

破解PDF资源aspose-words.zip(0积分下载):https://download.csdn.net/download/m0_49605579/59212077