最近项目有一个导出报表文件的需求,我脑中闪过第一念头就是导出pdf(产品经理没有硬性规定导出excel还是pdf文件),于是赶紧上网查看相关的资料,直到踩了无数的坑把功能做出来了才知道其实导出excel的api更方便,网上的相关博客也更多,不过坑也踩完了,这一次就来把代码和收获整理和分享一下。

导出pdf模板的局限性

  在看到需求之前,我先去网上去搜了一波,发现网上很大一部分博客都是将如何导出pdf模板的,就是先制作一张pdf模板,把固定不变的地方先写好,把需要改变的地方留白并设置参数,然后在代码里为参数赋值就行了,这种方式很简单,代码量也很少,但是!!!这种方式只适合导出格式和内容是固定的文件,而我们的需求是导出一张以产品名称为列,产品属性为行的报表,产品数量不定,这很显然就不能用模板的方式,只好老老实实把报表的数据从头到尾一一导出来。

使用iText导出pdf表格

  iText是一种生成PDF报表的Java组件,先把jar包下下来,maven依赖如下:

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.0.6</version>
</dependency>

按照惯例,先来生一个“Hello World”的文件,代码如下

public class TestPdf {
  public static void main(String[] args) throws Exception {
    
    TestPdf pdf = new TestPdf();
    String filename = "D:/Program Files/pdfTest/testTable3.pdf";
    pdf.createPDF(filename);
    System.out.println("打印完成");
    
}
  public void createPDF(String filename) throws IOException {
    Document document = new Document(PageSize.A4);
    try {
        PdfWriter.getInstance(document, new FileOutputStream(filename));
        document.addTitle("example of PDF");
        document.open();
        document.add(new Paragraph("Hello World!"));
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (DocumentException e) {
        e.printStackTrace();
    } finally {
        document.close();
    }
  }
}

这个没什么可说的,照着写就行了,不过我们导出的pdf大多数是以表格的形式,所以我就直接切入主题了,这里要用到一个很关键的类com.itextpdf.text.pdf.PDFPTable,先导出一张两行两列的表格

 public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
    PdfPTable table = new PdfPTable(2);//生成一个两列的表格
    PdfPCell cell;
    int size = 15;
    cell = new PdfPCell(new Phrase("one"));
    cell.setFixedHeight(size);//设置高度
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("two"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("three"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("four"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    return table;
  }
  
  public void createPDF(String filename) throws IOException {
    Document document = new Document(PageSize.A4);
    try {
        PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(filename));
        document.addTitle("example of PDF");
        document.open();
        PdfPTable table = createTable(writer);
        document.add(table);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (DocumentException e) {
        e.printStackTrace();
    } finally {
        document.close();
    }
}

效果如下图

现在把我们的需求变得更苛刻一点,再增加一行,格子数为1,文字水平和垂直居中对齐,占两行高度,先贴代码再做解释

  public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
    PdfPTable table = new PdfPTable(2);//生成一个两列的表格
    PdfPCell cell;
    int size = 15;
    cell = new PdfPCell(new Phrase("one"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("two"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("three"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("four"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("five"));
    cell.setColspan(2);//设置所占列数
    cell.setFixedHeight(size*2);//设置高度
    cell.setHorizontalAlignment(Element.ALIGN_CENTER);//设置水平居中
    cell.setVerticalAlignment(Element.ALIGN_MIDDLE);//设置垂直居中
    table.addCell(cell);
    return table;
  }

这里有一个很坑的地方,就是每一行的长度要和初始化表格的长度相等,即要把整行给占满,否则后面的都不会打印出来,所以要用到setColspan()方法来合并列,接下来还有一个就是合并行的的方法setRowspan()。我们在导出pdf报表的实际操作中经常会有这样的需求

所以合并行的方法显得尤为重要,那就尝试一下用这个方法来合并行,

public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
    PdfPTable table = new PdfPTable(2);//生成一个两列的表格
    PdfPCell cell;
    int size = 20;
    cell = new PdfPCell(new Phrase("one"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("two"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("three"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("four"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("five"));
    cell.setColspan(1);//设置所占列数
    cell.setRowspan(2);//合并行
    cell.setFixedHeight(size*2);//设置高度
    cell.setHorizontalAlignment(Element.ALIGN_CENTER);//设置水平居中
    cell.setVerticalAlignment(Element.ALIGN_MIDDLE);//设置垂直居中
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("six"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("seven"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    return table;
  }

效果如下

事实上很简单,在原来的代码上将“five”这一行长度由2变为1,加上setRowspan(2)方法,再加上两行长度为1的cell就是上面的效果了。看起来也没有多复杂是吧,现在我们在jar包上用了较新的版本itextpdf-5.0.6.jar,然而在这之前还有一种老版本的jar包itext-2.1.7.jar,依赖如下:

<dependency>
       <groupId>com.lowagie</groupId>  
       <artifactId>itext</artifactId>  
       <version>2.1.7</version>  
  </dependency>

用了这个jar包后发现效果是一样的,那我为什么要提这个版本呢,在后面我们会讲打到。事实上,在实现行合并的还有一种方法就是table中再套一个table,代码如下:

  public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
    PdfPTable table = new PdfPTable(2);//生成一个两列的表格
    PdfPCell cell;
    int size = 20;
    cell = new PdfPCell(new Phrase("one"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("two"));
    cell.setFixedHeight(size);
    cell.setColspan(2);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("three"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("four"));
    cell.setFixedHeight(size);
    cell.setColspan(2);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("five"));
    cell.setColspan(1);//设置所占列数
    //cell.setRowspan(2);//合并行
    cell.setFixedHeight(size*2);//设置高度
    cell.setHorizontalAlignment(Element.ALIGN_CENTER);//设置水平居中
    cell.setVerticalAlignment(Element.ALIGN_MIDDLE);//设置垂直居中
    table.addCell(cell);
    PdfPTable table2 = new PdfPTable(1);//新建一个table
    cell = new PdfPCell(new Phrase("six"));
    cell.setFixedHeight(size);
    table2.addCell(cell);
    cell = new PdfPCell(new Phrase("seven"));
    cell.setFixedHeight(size);
    table2.addCell(cell);
    cell = new PdfPCell(table2);//将table放到cell中
    table.addCell(cell);//将cell放到外层的table中去
    return table;
  }

效果和刚才是完全一样的,这种方法的关键就是将新建的table放到cell中,然后把cell放到外层table中去。这种方法明显比刚才的麻烦很多,我之所以拿出来是因为我在用老版本的jar包中的合并行的方法突然不起作用了,然后临时想了这个办法出来,至于为什么方法失效报错我到现在还没弄明白。。。

在表格中加入单选框/复选框

之所以提这个,在我自己项目中就有这样的需求,而且在网上这种资料非常少,我这也是在官网上看到的,遂记录一下,先看一下单选框怎么加,代码如下

 public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
    PdfPTable table = new PdfPTable(2);//生成一个两列的表格
    PdfPCell cell;
    int size = 20;
    cell = new PdfPCell(new Phrase("one"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("two"));
    cell.setFixedHeight(size);
    cell.setColspan(2);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("three"));
    cell.setFixedHeight(size);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("four"));
    cell.setFixedHeight(size);
    cell.setColspan(2);
    table.addCell(cell);
    cell = new PdfPCell(new Phrase("five"));
    cell.setColspan(1);//设置所占列数
    //cell.setRowspan(2);//合并行
    cell.setFixedHeight(size*2);//设置高度
    cell.setHorizontalAlignment(Element.ALIGN_CENTER);//设置水平居中
    cell.setVerticalAlignment(Element.ALIGN_MIDDLE);//设置垂直居中
    table.addCell(cell);
    PdfPTable table2 = new PdfPTable(1);//新建一个table
    cell = new PdfPCell(new Phrase("six"));
    cell.setFixedHeight(size);
    table2.addCell(cell);
    cell = new PdfPCell(new Phrase("seven"));
    cell.setFixedHeight(size);
    table2.addCell(cell);
    cell = new PdfPCell(table2);//将table放到cell中
    table.addCell(cell);//将cell放到外面的table中去
    Phrase phrase1 = new Phrase();
    Chunk chunk1 = new Chunk("         YES");
    Chunk chunk2 = new Chunk("          NO");
    phrase1.add(chunk1);
    phrase1.add(chunk2);
    cell = new PdfPCell(phrase1);
    cell.setColspan(2);
    table.addCell(cell);
    //增加两个单选框
    PdfFormField  radiogroup=PdfFormField.createRadioButton(writer, true);
    radiogroup.setFieldName("salesModel");
    Rectangle rect1 = new Rectangle(110, 722, 120, 712);
    Rectangle rect2 = new Rectangle(165, 722, 175, 712);
    RadioCheckField radio1 = new RadioCheckField(writer, rect1, null, "self-support" ) ;
    RadioCheckField radio2 = new RadioCheckField(writer, rect2, null, "cooprate") ;
    radio2.setChecked(true);
    PdfFormField radiofield1 = radio1.getRadioField();
    PdfFormField radiofield2 = radio2.getRadioField();
    radiogroup.addKid(radiofield1);
    radiogroup.addKid(radiofield2);
    writer.addAnnotation(radiogroup);
    return table;
  }

  效果如图

这个最烦的地方就是自己要去一点点试单选框的参数来调整位置,就是 Rectangle rect1 = new Rectangle()里面的参数,然后想让哪个radio被选中就用setChecked(true)方法就OK了

解决中文字体的问题

在之前的例子中我用的都是英文字,但是中文字体是一个不得不解决的问题,根据我去网上搜集到的资料大抵有以下几个方法:

  1. 使用windows系统自带的字体,这种事最省事,也是最简单的,代码如下

      public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
        PdfPTable table = new PdfPTable(2);//生成一个两列的表格
        PdfPCell cell;
        int size = 20;
        Font font = new Font(BaseFont.createFont("C:/Windows/Fonts/SIMYOU.TTF",BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED));   
        cell = new PdfPCell(new Phrase("显示中文",font));
        cell.setFixedHeight(size);
        cell.setColspan(2);
        table.addCell(cell);
        return table;
      }
  2. 使用itext-asian.jar中的中文字体,代码如下:
Font font = new Font(BaseFont.createFont( "STSongStd-Light" ,"UniGB-UCS2-H",BaseFont.NOT_EMBEDDED));   

在这里可能会报Font 'STSongStd-Light' with 'UniGB-UCS2-H' is not recognized.的错,网上都说是语言包中的包名有问题,但是我把itexitextpdf包的版本从5.0.6改成5.4.3就解决了,我把语言包解压后发现路径是对的,应该是后面的版本已经把语言包中的包名解决了,那为什么5.0.6的版本不行呢,反正我到现在还没找到原因,如果有谁知道的话欢迎留言告知。

  3.使用自己下载的资源字体。这种方法可以说是最麻烦的,但是也是最有效的。麻烦是因为自己要去下载字体文件,但是效果确是最好的,能够应对各种系统,各种状况,下面会提到,先看一下我的文件路径

然后是代码

Font font = new Font(BaseFont.createFont( "/simsun.ttf",BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED));

其实跟第一个是差不多的,只是一个是读系统的文件,一个是读项目自己的文件。 

在springboot中导出pdf文件

前面做了这么多铺垫,终于来到我们最关键的地方了,前面的东西都是放到java项目中去跑的,没有太多实际用处,在web项目中用到才算真正的应用,假设我们有一个产品类,我们的需求就是把产品数据导成pdf文件,代码如下:

产品Product类

public class Product {
  private String productName;
  private String productCode;
  private float price;
  public String getProductName() {
    return productName;
  }
  public void setProductName(String productName) {
    this.productName = productName;
  }
  public String getProductCode() {
    return productCode;
  }
  public void setProductCode(String productCode) {
    this.productCode = productCode;
  }
  public float getPrice() {
    return price;
  }
  public void setPrice(float price) {
    this.price = price;
  }
  
  public Product(String productName, String productCode, float price) {
    super();
    this.productName = productName;
    this.productCode = productCode;
    this.price = price;
  }
  
}

接下来是controller

    //打印pdf
    @RequestMapping("printPdf")
    public ModelAndView printPdf() throws Exception{    
      Product product1 = new Product("产品一","cp01",120);
      Product product2 = new Product("产品一","cp01",120);
      Product product3 = new Product("产品一","cp01",120);
      List<Product>products = new ArrayList<>();
      products.add(product1);
      products.add(product2);
      products.add(product3);
      Map<String, Object> model = new HashMap<>();
      model.put("sheet", products);          
      return new ModelAndView(new ViewPDF(), model); 
    }

然后是ViewPDF类

public class ViewPDF extends AbstractPdfView {

  @Override
  protected void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer,
          HttpServletRequest request, HttpServletResponse response) throws Exception {
      
      String fileName = new Date().getTime()+"_quotation.pdf"; // 设置response方式,使执行此controller时候自动出现下载页面,而非直接使用excel打开
      response.setCharacterEncoding("UTF-8");
      response.setContentType("application/pdf");
      response.setHeader("Content-Disposition","filename=" + new String(fileName.getBytes(), "iso8859-1"));
      List<Product> products = (List<Product>) model.get("sheet");
      PdfUtil pdfUtil = new PdfUtil();
      pdfUtil.createPDF(document, writer, products);
  }
}

在这里有两个值得注意的地方:

  1. 仔细观察这个类,看父类的bulidPdfDocument(),发现其中的document要求的是这个com.lowagie.text.Document版本的,这就非常坑了,意味着之前的itextpdf-5.0.6.jar不能再用了,只能用老版本的(改名之前的)itext-2.1.7.jar了。

  2. 如果不想要这种直接在浏览器中预览的效果,而是直接下载文件的话就把response.setHeader()方法里面的参数改成下面的,相当于加了一个attachment,以附件形式下载

response.setHeader("Content-Disposition","attachment;filename=" + new String(fileName.getBytes(), "iso8859-1"));

最后是PdfUtil类

public class PdfUtil {

  public void createPDF(Document document,PdfWriter writer, List<Product> products) throws IOException {
    //Document document = new Document(PageSize.A4); 
    try {
        document.addTitle("sheet of product");
        document.addAuthor("scurry");
        document.addSubject("product sheet.");
        document.addKeywords("product.");
        document.open();
        PdfPTable table = createTable(writer,products);
        document.add(table);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (DocumentException e) {
        e.printStackTrace();
    } finally {
        document.close();
    }
}
  public static PdfPTable createTable(PdfWriter writer,List<Product> products) throws IOException, DocumentException { 
    PdfPTable table = new PdfPTable(3);//生成一个两列的表格
    PdfPCell cell;
    int size = 20;
    Font font = new Font(BaseFont.createFont("C://Windows//Fonts//simfang.ttf", BaseFont.IDENTITY_H,
      BaseFont.NOT_EMBEDDED));   
    for(int i = 0;i<products.size();i++) {
      cell = new PdfPCell(new Phrase(products.get(i).getProductCode(),font));//产品编号
      cell.setFixedHeight(size);
      table.addCell(cell);
      cell = new PdfPCell(new Phrase(products.get(i).getProductName(),font));//产品名称
      cell.setFixedHeight(size);
      table.addCell(cell);
      cell = new PdfPCell(new Phrase(products.get(i).getPrice()+"",font));//产品价格
      cell.setFixedHeight(size);
      table.addCell(cell);
    }
    return table;
  }
}

这同时也意味着如果项目要打包部署到linux服务器上去的话,前两种的中文解决办法都不好使了,只能下载中文字体资源文件,然而这其中又有一个坑,打包后文件路径读取不到。

所以要以加载资源文件的形式读取文件,代码如下

BaseFont baseFont = BaseFont.createFont(new ClassPathResource("/simsun.ttf").getPath(), BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED);

效果如下图:

在这里既可以下载也可以打印。

  到这里我这踩了一路的坑都已经说完了,如果之后遇到新的问题我也持续更新这篇博客的。总的来说导出pdf真的是一件很繁琐的事情,还是那句话如果能导excel的话就尽量导excel吧。。。

上面所有内容原文地址:史上最全的springboot导出pdf文件 - 江南入直 - 博客园 (cnblogs.com)

注意点:

1、导出时,如果是直接文件的导出,不应该有返回值,否则会报错
org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation

2、直接导出pdf文件,需要在加一段代码

response.setCharacterEncoding("UTF-8");
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment;filename=" + new String("测试.pdf".getBytes(), "iso8859-1"));

3、如果一定要返回值,有一种思路。将pdf生成的文件上传到oss中,再返回一个key,之后再根据key去找pdf

样例版本代码:

package com.scm.platform.workflow.service;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import com.mountslink.components.exception.BusinessException;
import com.mountslink.components.util.BeanUtils;
import com.mountslink.components.util.CollectionUtils;
import com.mountslink.components.util.DateUtils;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.scm.platform.workflow.bpmp.CcUserTask;
import com.scm.platform.workflow.constant.WorkflowProcessStatus;
import com.scm.platform.workflow.dao.*;
import com.scm.platform.workflow.dto.*;
import com.scm.platform.workflow.model.*;
import io.seata.spring.annotation.GlobalTransactional;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.SequenceFlow;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.TaskService;
import org.activiti.engine.task.IdentityLink;
import org.activiti.engine.task.Task;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class WorkflowPDFExportService {

    private static final Logger logger = LoggerFactory.getLogger(WorkflowPDFExportService.class);
    @Autowired
    private TaskService taskService;

    @Autowired
    private WorkflowTaskDao workflowTaskDao;

    @Autowired
    private WorkflowUserDao workflowUserDao;

    @Autowired
    private WorkflowRoleUserDao roleUserDao;

    @Autowired
    private WorkflowGroupDao workflowGroupDao;

    @Autowired
    private WorkflowGroupUserDao workflowGroupUserDao;

    @Autowired
    private JPAQueryFactory jpaQueryFactory;

    @Autowired
    private WorkflowTenantDao workflowTenantDao;

    @Autowired
    private CallbackService callbackService;

    @Autowired
    private WorkflowTaskService workflowTaskService;

    @Autowired
    private WorkflowProcessService workflowProcessService;

    @Autowired
    private WorkflowProcessDao workflowProcessDao;

    @Autowired
    private RepositoryService repositoryService;

    /**
     * 获取工作流详情,流程节点信息查看
     *
     * @param id 工作流审批信息表id
     * @return
     */
    public void getWorkflow(Long id, String businessCode, HttpServletResponse response) {
        // 查询已审批节点的数据
        QWorkflowProcess p = QWorkflowProcess.workflowProcess;
        QWorkflowConfig c = QWorkflowConfig.workflowConfig;
        QWorkflowUser u = QWorkflowUser.workflowUser;

        // 查询实例对象
        WorkflowProcessDto dto = new WorkflowProcessDto();
        if (id != null) {
            dto = jpaQueryFactory
                    .select(Projections.bean(WorkflowProcessDto.class,
                            p.id, p.status, p.processId, p.json, p.config, p.configId, p.callbackUrl, p.businessId,
                            p.userId, p.businessUserId, p.tenantId, p.currentNodeName, p.currentNodeCreateTime,
                            p.createTime, c.code, c.name, u.name.as("userName"), c.detailUrl))
                    .from(p)
                    .leftJoin(c).on(p.configId.eq(c.id))
                    .leftJoin(u).on(p.userId.eq(u.id))
                    .where(p.id.eq(id))
                    .fetchFirst();
        } else {
            dto = jpaQueryFactory
                    .select(Projections.bean(WorkflowProcessDto.class,
                            p.id, p.status, p.processId, p.json, p.config, p.configId, p.callbackUrl, p.businessId,
                            p.userId, p.businessUserId, p.tenantId, p.currentNodeName, p.currentNodeCreateTime,
                            p.createTime, c.code, c.name, u.name.as("userName"), c.detailUrl))
                    .from(p)
                    .leftJoin(c).on(p.configId.eq(c.id))
                    .leftJoin(u).on(p.userId.eq(u.id))
                    .where(p.businessId.eq(businessCode))
                    .orderBy(p.createTime.desc())
                    .fetchFirst();
        }
        if (Objects.isNull(dto)) {
            throw new BusinessException("当前单据未找到审批任务!");
        }
        WorkflowProcess processModel = workflowProcessDao.findById(dto.getId()).orElse(null);
        dto.setConditionJson(processModel.getConditionJson());
        dto.setContactUnitName(processModel.getContactUnitName());

        // 查询该实例对象所有的已审批节点
        List<WorkflowTaskDto> taskDtoList = workflowTaskDao.findAllByProcessId(dto.getId())
                .stream()
                .map(task -> BeanUtils.copyProperties(task, WorkflowTaskDto.class))
                .collect(Collectors.toList());

        dto.setTaskList(taskDtoList);

        // 查询当前待审批节点的数据,拼一个返回实体,根据activiti内部的实例id查询
        String processId = dto.getProcessId();
        List<Task> tasks = taskService.createTaskQuery().processInstanceId(processId).list();

        for (Task task : tasks) {
            WorkflowTaskDto taskDto = new WorkflowTaskDto();
            taskDto.setProcessId(dto.getId());
            taskDto.setTaskId(task.getId());
            taskDto.setTenantId(dto.getTenantId());
            taskDto.setTaskKey(task.getTaskDefinitionKey());
            // 有assignee直接取assignee
            if (com.mountslink.components.util.StringUtils.isNotEmpty(task.getAssignee())) {
                taskDto.setUserId(Long.parseLong(task.getAssignee()));
                // 添加到返回列表中
                taskDtoList.add(taskDto);
            }
            // 没有assignee取candidate
            else {
                List<IdentityLink> identityLinkList = taskService.getIdentityLinksForTask(task.getId());
                for (IdentityLink identityLink : identityLinkList) {
                    // notify 是查询抄送的,不应该放到审批人中   candidate审批人    notify:抄送人
                    String type = identityLink.getType();
                    if ("notify".contains(type)) {
                        continue;
                    }
                    WorkflowTaskDto copyDto = new WorkflowTaskDto();
                    BeanUtils.copyProperties(taskDto, copyDto);
                    String userIdStr = identityLink.getUserId();
                    String groupIdStr = identityLink.getGroupId();
                    if (!com.mountslink.components.util.StringUtils.isEmpty(userIdStr)) {
                        copyDto.setUserId(Long.parseLong(userIdStr));
                    }
                    if (!com.mountslink.components.util.StringUtils.isEmpty(groupIdStr)) {
                        copyDto.setGroupId(Long.parseLong(groupIdStr));
                    }
                    // 添加到返回列表中
                    taskDtoList.add(copyDto);
                }
            }

        }

        // 返回实体会缺少用户id等信息,需要重新去数据库查一遍
        // 获取用户ID和群组ID,获取用户名和群组名,避免循环查询
        List<Long> userIds = taskDtoList.stream().map(WorkflowTaskDto::getUserId).filter(Objects::nonNull).collect(Collectors.toList());
        List<Long> groupIds = taskDtoList.stream().map(WorkflowTaskDto::getGroupId).filter(Objects::nonNull).collect(Collectors.toList());
        List<WorkflowUser> workflowUserList = workflowUserDao.findAllById(userIds);
        List<WorkflowGroup> workflowGroupList = workflowGroupDao.findAllById(groupIds);
        for (WorkflowTaskDto taskDto : taskDtoList) {

            if (taskDto.getUserId() != null) {
                for (WorkflowUser workflowUser : workflowUserList) {
                    if (taskDto.getUserId().equals(workflowUser.getId())) {
                        taskDto.setBusinessUserId(workflowUser.getBusinessId());
                        taskDto.setUserName(workflowUser.getName());
                        break;
                    }
                }
            }

            if (taskDto.getGroupId() != null) {
                for (WorkflowGroup workflowGroup : workflowGroupList) {
                    if (taskDto.getGroupId().equals(workflowGroup.getId())) {
                        taskDto.setBusinessGroupId(workflowGroup.getBusinessId());
                        taskDto.setGroupName(workflowGroup.getName());
                        break;
                    }
                }
            }
        }
        //设置所有的taskList
        getAllTaskList(dto, taskDtoList, tasks);

        // 设置dto配置模板和展示
        handlerConfigData(dto);

        //导出pdf
//        workflowExportPDF(dto, response);

//        return dto;
    }

    public void workflowExportPDF(Long id, String businessCode, HttpServletResponse response) {

        // 查询已审批节点的数据
        QWorkflowProcess p = QWorkflowProcess.workflowProcess;
        QWorkflowConfig c = QWorkflowConfig.workflowConfig;
        QWorkflowUser u = QWorkflowUser.workflowUser;

        // 查询实例对象
        WorkflowProcessDto dto = new WorkflowProcessDto();
        if (id != null) {
            dto = jpaQueryFactory
                    .select(Projections.bean(WorkflowProcessDto.class,
                            p.id, p.status, p.processId, p.json, p.config, p.configId, p.callbackUrl, p.businessId,
                            p.userId, p.businessUserId, p.tenantId, p.currentNodeName, p.currentNodeCreateTime,
                            p.createTime, c.code, c.name, u.name.as("userName"), c.detailUrl))
                    .from(p)
                    .leftJoin(c).on(p.configId.eq(c.id))
                    .leftJoin(u).on(p.userId.eq(u.id))
                    .where(p.id.eq(id))
                    .fetchFirst();
        } else {
            dto = jpaQueryFactory
                    .select(Projections.bean(WorkflowProcessDto.class,
                            p.id, p.status, p.processId, p.json, p.config, p.configId, p.callbackUrl, p.businessId,
                            p.userId, p.businessUserId, p.tenantId, p.currentNodeName, p.currentNodeCreateTime,
                            p.createTime, c.code, c.name, u.name.as("userName"), c.detailUrl))
                    .from(p)
                    .leftJoin(c).on(p.configId.eq(c.id))
                    .leftJoin(u).on(p.userId.eq(u.id))
                    .where(p.businessId.eq(businessCode))
                    .orderBy(p.createTime.desc())
                    .fetchFirst();
        }
        if (Objects.isNull(dto)) {
            throw new BusinessException("当前单据未找到审批任务!");
        }
        WorkflowProcess processModel = workflowProcessDao.findById(dto.getId()).orElse(null);
        dto.setConditionJson(processModel.getConditionJson());
        dto.setContactUnitName(processModel.getContactUnitName());

        // 查询该实例对象所有的已审批节点
        List<WorkflowTaskDto> taskDtoList = workflowTaskDao.findAllByProcessId(dto.getId())
                .stream()
                .map(task -> BeanUtils.copyProperties(task, WorkflowTaskDto.class))
                .collect(Collectors.toList());

        dto.setTaskList(taskDtoList);

        // 查询当前待审批节点的数据,拼一个返回实体,根据activiti内部的实例id查询
        String processId = dto.getProcessId();
        List<Task> tasks = taskService.createTaskQuery().processInstanceId(processId).list();

        for (Task task : tasks) {
            WorkflowTaskDto taskDto = new WorkflowTaskDto();
            taskDto.setProcessId(dto.getId());
            taskDto.setTaskId(task.getId());
            taskDto.setTenantId(dto.getTenantId());
            taskDto.setTaskKey(task.getTaskDefinitionKey());
            // 有assignee直接取assignee
            if (com.mountslink.components.util.StringUtils.isNotEmpty(task.getAssignee())) {
                taskDto.setUserId(Long.parseLong(task.getAssignee()));
                // 添加到返回列表中
                taskDtoList.add(taskDto);
            }
            // 没有assignee取candidate
            else {
                List<IdentityLink> identityLinkList = taskService.getIdentityLinksForTask(task.getId());
                for (IdentityLink identityLink : identityLinkList) {
                    // notify 是查询抄送的,不应该放到审批人中   candidate审批人    notify:抄送人
                    String type = identityLink.getType();
                    if ("notify".contains(type)) {
                        continue;
                    }
                    WorkflowTaskDto copyDto = new WorkflowTaskDto();
                    BeanUtils.copyProperties(taskDto, copyDto);
                    String userIdStr = identityLink.getUserId();
                    String groupIdStr = identityLink.getGroupId();
                    if (!com.mountslink.components.util.StringUtils.isEmpty(userIdStr)) {
                        copyDto.setUserId(Long.parseLong(userIdStr));
                    }
                    if (!com.mountslink.components.util.StringUtils.isEmpty(groupIdStr)) {
                        copyDto.setGroupId(Long.parseLong(groupIdStr));
                    }
                    // 添加到返回列表中
                    taskDtoList.add(copyDto);
                }
            }

        }

        // 返回实体会缺少用户id等信息,需要重新去数据库查一遍
        // 获取用户ID和群组ID,获取用户名和群组名,避免循环查询
        List<Long> userIds = taskDtoList.stream().map(WorkflowTaskDto::getUserId).filter(Objects::nonNull).collect(Collectors.toList());
        List<Long> groupIds = taskDtoList.stream().map(WorkflowTaskDto::getGroupId).filter(Objects::nonNull).collect(Collectors.toList());
        List<WorkflowUser> workflowUserList = workflowUserDao.findAllById(userIds);
        List<WorkflowGroup> workflowGroupList = workflowGroupDao.findAllById(groupIds);
        for (WorkflowTaskDto taskDto : taskDtoList) {

            if (taskDto.getUserId() != null) {
                for (WorkflowUser workflowUser : workflowUserList) {
                    if (taskDto.getUserId().equals(workflowUser.getId())) {
                        taskDto.setBusinessUserId(workflowUser.getBusinessId());
                        taskDto.setUserName(workflowUser.getName());
                        break;
                    }
                }
            }

            if (taskDto.getGroupId() != null) {
                for (WorkflowGroup workflowGroup : workflowGroupList) {
                    if (taskDto.getGroupId().equals(workflowGroup.getId())) {
                        taskDto.setBusinessGroupId(workflowGroup.getBusinessId());
                        taskDto.setGroupName(workflowGroup.getName());
                        break;
                    }
                }
            }
        }
        //设置所有的taskList
        getAllTaskList(dto, taskDtoList, tasks);

        // 设置dto配置模板和展示
        handlerConfigData(dto);

        try {
            WorkflowTenant tenantDaoOne = workflowTenantDao.getOne(dto.getTenantId());
            //创建文件
            Document document = new Document(PageSize.A4);

            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/pdf");
            response.setHeader("Content-Disposition", "attachment;filename=" + new String("测试.pdf".getBytes(), "iso8859-1"));

            ServletOutputStream outputStream = response.getOutputStream();
            //建立一个书写器
            PdfWriter.getInstance(document, outputStream);
            //打开文件
            document.open();

            // 1- 页面的属性
            Rectangle tRectangle = new Rectangle(PageSize.A4); // 页面大小

            tRectangle.setBackgroundColor(BaseColor.ORANGE); // 页面背景色
            tRectangle.setBorder(1220);// 边框
            tRectangle.setBorderColor(BaseColor.BLUE);// 边框颜色
            tRectangle.setBorderWidth(244.2f);// 边框宽度

            //添加内容
            BaseFont cbf2 = BaseFont.createFont(new ClassPathResource("/simsun.ttf").getPath(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
            Font cfont2 = new Font(cbf2, 20);
            Paragraph ph2 = new Paragraph(tenantDaoOne.getName() + dto.getName(), cfont2);
            ph2.setAlignment(Paragraph.ALIGN_CENTER);
            ph2.setSpacingAfter(20);
            document.add(ph2);

            PdfPTable table = new PdfPTable(4);
            table.setWidthPercentage(100); // 宽度100%填充
            table.setSpacingBefore(10f); // 前间距
            table.setSpacingAfter(10f); // 后间距

            List<PdfPRow> listRow = table.getRows();
            //设置列宽
            float[] columnWidths = {2f, 2f, 2f, 2f};
            table.setWidths(columnWidths);

            String[] firstTitleArr = new String[]{"审批编号", "提交时间", "申请人", "申请人部门", "当前审批状态"};
            List<String> firstContentList = new ArrayList<>();
            firstContentList.add(dto.getBusinessId());

            WorkflowTaskDto firstTaskDao = dto.getTaskList().get(0);
            String userName = firstTaskDao.getUserName();
            firstContentList.add(DateUtils.convert(firstTaskDao.getCreateTime(), DateUtils.DATE_TIME_FORMAT));
            firstContentList.add(userName);

            List<WorkflowGroupUser> workflowGroupUserList = workflowGroupUserDao.findByUserId(firstTaskDao.getUserId());
            Long groupId = workflowGroupUserList.get(0).getGroupId();
            WorkflowGroup one = workflowGroupDao.getOne(groupId);
            firstContentList.add(one.getName());
            firstContentList.add(dto.getStatus() + "");

            for (int i = 0; i < firstContentList.size(); i++) {
                if (i == firstContentList.size() - 1) {
                    PdfPCell cell = new PdfPCell(getChineseSize(firstTitleArr[i]));
                    setCellStyleAndColspan(cell, 1, true);
                    table.addCell(cell);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

                    PdfPCell cell2 = new PdfPCell(getChineseSize(firstContentList.get(i)));
                    setCellStyleAndColspan(cell2, 3, false);
                    table.addCell(cell2);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

                    continue;
                }
                if (i % 2 == 0) {
                    PdfPCell cells2[] = new PdfPCell[4];
                    PdfPRow row2 = new PdfPRow(cells2);
                    cells2[0] = new PdfPCell(getChineseSize(firstTitleArr[i]));
                    setStyle(cells2[0], true);
                    cells2[1] = new PdfPCell(getChineseSize(firstContentList.get(i)));
                    setStyle(cells2[1], false);
                    cells2[2] = new PdfPCell(getChineseSize(firstTitleArr[i + 1]));
                    setStyle(cells2[2], true);
                    cells2[3] = new PdfPCell(getChineseSize(firstContentList.get(i + 1)));
                    setStyle(cells2[3], false);
                    listRow.add(row2);
                }
            }

            PdfPCell cell3 = new PdfPCell(getChineseSize("申请内容"));
            setCellStyleAndColspan(cell3, 4, true);
            table.addCell(cell3);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            List<String> tableTitile = new ArrayList<>();
            List<String> tableValue = new ArrayList<>();
            List<TableDto> tableList = dto.getTableList();
            for (TableDto tableDto : tableList) {
                tableTitile.add(tableDto.getTitle() == null ? "" : tableDto.getTitle().toString());
                Object type = tableDto.getType();
                if (ObjectUtil.isNotEmpty(type) && "date".equals(type.toString())) {
                    DateTime dateTime = DateUtil.parse(tableDto.getValue().toString(), DateUtils.DATE_FORMAT);
                    String convert = DateUtils.convert(dateTime, DateUtils.DATE_FORMAT);
                    tableValue.add(convert);
                } else {
                    tableValue.add(tableDto.getValue() == null ? "" : tableDto.getValue().toString());
                }
            }

            for (int i = 0; i < tableTitile.size(); i++) {
                if (i % 2 == 0) {
                    PdfPCell cells2[] = new PdfPCell[4];
                    PdfPRow row2 = new PdfPRow(cells2);
                    cells2[0] = new PdfPCell(getChineseSize(tableTitile.get(i)));
                    setStyle(cells2[0], true);
                    cells2[1] = new PdfPCell(getChineseSize(tableValue.get(i)));
                    setStyle(cells2[1], false);
                    cells2[2] = new PdfPCell(getChineseSize(tableTitile.get(i + 1)));
                    setStyle(cells2[2], true);
                    cells2[3] = new PdfPCell(getChineseSize(tableValue.get(i + 1)));
                    setStyle(cells2[3], false);
                    listRow.add(row2);
                }
            }
            //将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            PdfPCell cell = new PdfPCell(getChineseSize("审批说明"));
            setCellStyleAndColspan(cell, 1, true);
            table.addCell(cell);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            String json = dto.getJson();
            JSONObject parseObj = JSON.parseObject(json);
            PdfPCell cell2;
            if (ObjectUtil.isNotEmpty(parseObj) && ObjectUtil.isNotEmpty(parseObj.get("auditOpinion"))) {
                cell2 = new PdfPCell(getChineseSize(parseObj.get("auditOpinion").toString()));
            } else {
                cell2 = new PdfPCell(getChineseSize("无"));
            }
            setCellStyleAndColspan(cell2, 3, false);
            table.addCell(cell2);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效


            PdfPCell cell4 = new PdfPCell(getChineseSize("审批流程" + dto.getStatus()));
            setCellStyleAndColspan(cell4, 4, true);
            table.addCell(cell4);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            //审批节点 审批人 操作记录
            String[] taskArr = new String[]{"审批节点", "审批人", "操作记录"};
            PdfPCell taskCell1 = new PdfPCell(getChineseSize(taskArr[0]));
            setCellStyleAndColspan(taskCell1, 1, true);
            table.addCell(taskCell1);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            PdfPCell taskCell2 = new PdfPCell(getChineseSize(taskArr[1]));
            setCellStyleAndColspan(taskCell2, 1, true);
            table.addCell(taskCell2);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            PdfPCell taskCell3 = new PdfPCell(getChineseSize(taskArr[2]));
            setCellStyleAndColspan(taskCell3, 2, true);
            table.addCell(taskCell3);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            List<WorkflowTaskDto> allTaskList = dto.getAllTaskList();
            for (WorkflowTaskDto workflowTaskDto : allTaskList) {
                PdfPCell allTaskCell1 = new PdfPCell(getChineseSize(workflowTaskDto.getTaskName()));
                setCellStyleAndColspan(allTaskCell1, 1, true);
                table.addCell(allTaskCell1);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

                PdfPCell allTaskCell2 = new PdfPCell(getChineseSize(workflowTaskDto.getUserName()));
                setCellStyleAndColspan(allTaskCell2, 1, false);
                table.addCell(allTaskCell2);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

                Date createTime = workflowTaskDto.getCreateTime();
                String convertDateStr = "";
                if (ObjectUtil.isNotEmpty(createTime)) {
                    convertDateStr = DateUtils.convert(createTime, DateUtils.DATE_TIME_FORMAT);
                }
                PdfPCell allTaskCell3 = new PdfPCell(getChineseSize(convertDateStr));
                setCellStyleAndColspan(allTaskCell3, 2, false);
                table.addCell(allTaskCell3);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            }


            PdfPCell csCell = new PdfPCell(getChineseSize("抄送人"));
            setCellStyleAndColspan(csCell, 1, true);
            table.addCell(csCell);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            PdfPCell csCell2 = new PdfPCell(getChineseSize("审批说明"));
            setCellStyleAndColspan(csCell2, 3, false);
            table.addCell(csCell2);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效


            PdfPCell remarkCell = new PdfPCell(getChineseSize("备注信息"));
            setCellStyleAndColspan(remarkCell, 1, true);
            table.addCell(remarkCell);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效

            PdfPCell remarkCel2 = new PdfPCell(getChineseSize("备注内容"));
            setCellStyleAndColspan(remarkCel2, 3, false);
            table.addCell(remarkCel2);//将该单元格添加到表格中,注意该语句必须在对该单元格设置完毕之后添加,否则在该语句之后的单元格设置将失效


            document.add(table);

            //关闭文档
            document.close();

        } catch (Exception e) {

        }

    }

    private static void setCellStyleAndColspan(PdfPCell cell, int i, Boolean color) {
        cell.setColspan(i);//设置该单元格跨列 8 列
        if (color) {
            cell.setBackgroundColor(BaseColor.LIGHT_GRAY); //背景设置为灰色
        }
        cell.setHorizontalAlignment(Element.ALIGN_CENTER);//水平居中
    }

    private static void setStyle(PdfPCell pdfPCell, Boolean backgroundColor) {
        if (backgroundColor) {
            pdfPCell.setBackgroundColor(BaseColor.LIGHT_GRAY);
        }
        pdfPCell.setBorderColor(BaseColor.BLACK);//边框颜色
        pdfPCell.setHorizontalAlignment(Element.ALIGN_CENTER);//水平居中
        pdfPCell.setVerticalAlignment(Element.ALIGN_MIDDLE);//垂直居中
    }

    private static Paragraph getChineseSize(String name) throws Exception {
        BaseFont cbf = BaseFont.createFont(new ClassPathResource("/simsun.ttf").getPath(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
        Font cfont = new Font(cbf, 14);
        Paragraph ph = new Paragraph(name, cfont);
        ph.setAlignment(Paragraph.ALIGN_CENTER);
        return ph;
    }

    private void handlerConfigData(WorkflowProcessDto dto) {
        // 业务传过来的json
        String json = dto.getJson();
        JSONObject jsonObj = JSON.parseObject(json);

        // 工作流配置的config展示字段
        String config = dto.getConfig();
        JSONObject jsonObject = JSON.parseObject(config);
        String controlsTitle = (String) jsonObject.get("controlsTitle");
        dto.setControlsTitle(controlsTitle);
        JSONArray objects = (JSONArray) jsonObject.get("controls");
        List<TableDto> tableList = new ArrayList<>();
        for (Object object : objects) {
            Map map = (Map) object;

            TableDto tableDto = new TableDto();
            tableDto.setKey(map.get("fieldValue"));
            tableDto.setType(map.get("fieldType"));
            tableDto.setDictType(map.get("dictType"));
            tableDto.setTitle(map.get("title"));

            Object fieldValue = map.get("fieldValue");
            String key = fieldValue.toString();
            Object value;
            // 工作流审核状态
            if (key.equals("workflowAuditStatus")) {
                tableDto.setValue(dto.getStatus());
            } else {
                tableDto.setValue(jsonObj.get(key));
            }
            tableList.add(tableDto);
        }
        dto.setTableList(tableList);

        //其它信息列表
        JSONArray otherList = (JSONArray) jsonObject.get("otherList");
        if (CollectionUtils.isNotEmpty(otherList)) {
            List<WorkflowProcessOtherDto> processOtherList = new ArrayList<>();
            for (Object object : otherList) {
                Map map = (Map) object;
                WorkflowProcessOtherDto processOther = new WorkflowProcessOtherDto();
                String otherTitleName = (String) map.get("otherTitle");
                processOther.setOtherTitleName(otherTitleName);
                JSONArray otherDetailList = (JSONArray) map.get("otherDetailList");
                if (CollectionUtils.isNotEmpty(otherDetailList)) {
                    List<TableDto> otherTableList = new ArrayList<>();
                    for (Object detailObj : otherDetailList) {
                        Map detailMap = (Map) detailObj;
                        TableDto tableDto = new TableDto();
                        tableDto.setKey(detailMap.get("fieldValue"));
                        tableDto.setTitle(detailMap.get("title"));
                        tableDto.setType(detailMap.get("fieldType"));
                        tableDto.setDictType(detailMap.get("dictType"));
                        Object fieldValue = detailMap.get("fieldValue");
                        String key = fieldValue.toString();
                        tableDto.setValue(jsonObj.get(key));
                        otherTableList.add(tableDto);
                    }
                    processOther.setTableList(otherTableList);
                }
                processOtherList.add(processOther);
            }
            dto.setProcessOtherList(processOtherList);
        }

        //明细信息
        JSONArray detailList = (JSONArray) jsonObject.get("detailList");
        if (CollectionUtils.isNotEmpty(detailList)) {
            List<WorkflowProcessDetailDto> processDetailList = new ArrayList<>();
            for (Object detailObj : detailList) {
                Map detailMap = (Map) detailObj;
                WorkflowProcessDetailDto rocessDetail = new WorkflowProcessDetailDto();
                String detailTitile = (String) detailMap.get("detailTitle");
                rocessDetail.setDetailTitile(detailTitile);
                //获取业务数据里面明细数据
                String detailFieldValue = (String) detailMap.get("detailFieldValue");
                JSONArray ywDetail = (JSONArray) jsonObj.get(detailFieldValue);
                JSONArray controlList = (JSONArray) detailMap.get("controlList");
                if (CollectionUtils.isNotEmpty(controlList)) {
                    List<TableDto> titleList = new ArrayList<>();
                    for (Object conObj : controlList) {
                        Map conMap = (Map) conObj;
                        TableDto tableDto = new TableDto();
                        tableDto.setKey(conMap.get("fieldValue"));
                        tableDto.setTitle(conMap.get("title"));
                        tableDto.setType(conMap.get("fieldType"));
                        tableDto.setDictType(conMap.get("dictType"));
                        titleList.add(tableDto);
                    }
                    rocessDetail.setTitleList(titleList);
                }
                if (CollectionUtils.isNotEmpty(ywDetail)) {
                    //业务数据
                    List<Object> ywTableList = new ArrayList<>();
                    for (Object ywObj : ywDetail) {
                        Map ywMap = (Map) ywObj;
                        ywTableList.add(ywMap);
                    }
                    rocessDetail.setTableList(ywTableList);
                }
                processDetailList.add(rocessDetail);
            }
            dto.setProcessDetailList(processDetailList);
        }
    }

    private void getAllTaskList(WorkflowProcessDto dto, List<WorkflowTaskDto> taskDtoList, List<Task> tasks) {
        //说明没有任务了
        if (CollectionUtils.isEmpty(tasks)) {
            dto.setAllTaskList(taskDtoList);
            return;
        }
        //获取流程定义id。
        String processDefinitionId = tasks.get(0).getProcessDefinitionId();

        List<WorkflowTaskDto> allTaskList = new ArrayList<>();
        allTaskList.addAll(dto.getTaskList());
        // 获取所有节点的信息返回
        BpmnModel model = repositoryService.getBpmnModel(processDefinitionId);

        List<String> taskKeyList = new ArrayList<>();
        for (WorkflowTaskDto workflowTaskDto : taskDtoList) {
            String taskKey = workflowTaskDto.getTaskKey();
            taskKeyList.add(taskKey);
            if (StringUtils.isNotBlank(workflowTaskDto.getTaskId())) {
                workflowTaskDto.setType(WorkflowProcessStatus.Auditing);
            }
        }
        if (model != null) {
            Collection<FlowElement> flowElements = model.getMainProcess().getFlowElements();
            String nextTargetRef = null;
            String nexUserId = null;
            //找到第一个条件不进行下一个条件判断
            Boolean conditionState = false;
            for (FlowElement e : flowElements) {
                if (e instanceof SequenceFlow && StringUtils.isNotEmpty(dto.getConditionJson())) {
                    SequenceFlow seqFlow = (SequenceFlow) e;
                    String sourceRef = seqFlow.getSourceRef();
                    String targetRef = seqFlow.getTargetRef();
                    if (StringUtils.isNotEmpty(seqFlow.getConditionExpression())) {
                        String expName = seqFlow.getName();
                        if (!conditionState) {
                            boolean isPass = assertExpressTrue(dto.getConditionJson(), dto.getJson(), expName);
                            if (isPass) {
                                conditionState = true;
                                if ("node-end".equals(targetRef)) {
                                    break;
                                } else {
                                    nextTargetRef = targetRef;
                                    nexUserId = targetRef;
                                }
                            }
                        }
                    } else if (StringUtils.isNotEmpty(nextTargetRef) && nextTargetRef.equals(sourceRef)) {
                        if ("node-end".equals(targetRef)) {
                            break;
                        } else {
                            nextTargetRef = targetRef;
                            nexUserId = targetRef;
                        }
                    }
                } else if (e instanceof CcUserTask) {
                    CcUserTask userTask = (CcUserTask) e;
                    String taskKey = userTask.getId();
                    if (StringUtils.isNotEmpty(nexUserId) && !taskKey.equals(nexUserId)) {
                        continue;
                    } else if (taskKeyList.contains(taskKey)) {
                        continue;
                    }

                    WorkflowTaskDto workflowTaskDto = new WorkflowTaskDto();
                    List<String> candidateUsers = ((CcUserTask) e).getCandidateUsers();
                    //如果是指定人,candidateUsers 不为空
                    if (CollectionUtils.isNotEmpty(candidateUsers)) {
                        List<Long> collect = candidateUsers.stream().map(item -> Long.parseLong(item)).collect(Collectors.toList());
                        List<WorkflowUser> userList = workflowUserDao.findAllById(collect);
                        StringBuffer names = new StringBuffer();
                        for (WorkflowUser workflowUser : userList) {
                            names.append(workflowUser.getName() + ",");
                        }
                        workflowTaskDto.setUserName(names.toString().substring(0, names.length() - 1));
                        allTaskList.add(workflowTaskDto);
                    }
                    //如果是指定标签,candidateUsers 不为空
                    List<String> candidateGroups = ((CcUserTask) e).getCandidateGroups();
                    if (CollectionUtils.isNotEmpty(candidateGroups)) {
                        List<Long> roleIds = candidateGroups.stream().map(item -> Long.parseLong(item)).collect(Collectors.toList());
                        List<RoleUser> roleUserList = roleUserDao.findByRoleId(roleIds);
                        List<Long> collect = roleUserList.stream().map(RoleUser::getUserId).collect(Collectors.toList());
                        List<WorkflowUser> userList = workflowUserDao.findAllById(collect);
                        StringBuffer names = new StringBuffer();
                        for (WorkflowUser workflowUser : userList) {
                            names.append(workflowUser.getName() + ",");
                        }
                        workflowTaskDto.setUserName(names.toString().substring(0, names.length() - 1));
                        allTaskList.add(workflowTaskDto);
                    }
                }
            }
        }

        dto.setAllTaskList(allTaskList);
    }

    /**
     * 条件判断
     *
     * @param json
     * @param expression
     * @return
     */
    public Boolean assertExpressTrue(String conditionJson, String json, String expression) {
        boolean eval = false;
        if (StringUtils.isNotEmpty(conditionJson) && StringUtils.isNotEmpty(json)) {
            String el = expression;
            ScriptEngineManager manager = new ScriptEngineManager();
            ScriptEngine engine = manager.getEngineByName("js");
            JSONObject conJsonObj = JSON.parseObject(conditionJson);
            JSONObject objJson = JSON.parseObject(json);
            JSONArray conditionList = (JSONArray) conJsonObj.get("conditionList");
            for (Object object : conditionList) {
                Map map = (Map) object;
                String key = map.get("field").toString();
                String value = objJson.get(key).toString();
                engine.put(key, value);
            }
            try {
                eval = (boolean) engine.eval(el);
            } catch (Exception e) {
                throw new BusinessException("条件判断异常", e);
            }
        }
        return eval;
    }

    /**
     * 工作流审批通过
     *
     * @param dto
     * @param workflowUser
     */
    //@Transactional(rollbackFor = Exception.class)
    @GlobalTransactional(rollbackFor = Exception.class)
    public void resolve(WorkflowAuditDto dto, WorkflowUser workflowUser) {
        // claim声明一个节点被某个用户占用了,避免多个人同时审批同一个节点引发的并发问题
        taskService.claim(dto.getTaskId(), workflowUser.getId().toString());

        // 添加审批节点,更新工作流关联的变量,此处存在疑问,global变量为什么要更新
        WorkflowProcess workflowProcess = workflowProcessService.updateVariables(dto);
        addTask(workflowProcess, workflowUser, dto);
        taskService.complete(dto.getTaskId());

        // 获取当前activiti的审批节点
        Task currentTask = getCurrentTask(workflowProcess.getProcessId());

        // 设置当前节点名称、key、接收时间
        if (currentTask != null) {
            String nodeKey = currentTask.getTaskDefinitionKey();
            String nodeName = workflowProcessService.getCurrentNodeName(workflowProcess, nodeKey);
            // 设置工作流审批信息中当前节点信息
            workflowProcessService.updateCurrentNode(workflowProcess, nodeKey, nodeName);
        } else {
            // 设置工作流审批信息中当前节点信息
            workflowProcessService.updateCurrentNode(workflowProcess, null, null);
            // 最后一个节点由监听器来调用回调接口
            return;
        }

        // 回调接口,需要根据参数判断
        if (Boolean.TRUE.equals(workflowProcess.getIsAlwaysCallback())) {
            callbackService.invoke(workflowProcess, dto.getReason(), dto.getStatus());
        }

        // 是否能够自动回调,如果当前节点和上一个节点是同一个人,就自动连续审批
        autoAudit(dto, workflowUser, workflowProcess.getProcessId());
    }

    /**
     * 更新工作流审批信息表中的状态
     * 增加工作流节点,已处理的任务节点
     *
     * @param workflowProcess
     * @param workflowUser
     * @param dto
     */
    @Transactional(rollbackFor = Exception.class)
    public void addTask(WorkflowProcess workflowProcess,
                        WorkflowUser workflowUser,
                        WorkflowAuditDto dto) {
        workflowProcessService.updateStatus(workflowProcess, dto.getStatus());
        workflowTaskService.addTask(workflowProcess, workflowUser, dto);
    }

    /**
     * 连续审批
     *
     * @param dto
     * @param workflowUser
     * @param processId
     */
    private void autoAudit(WorkflowAuditDto dto, WorkflowUser workflowUser, String processId) {
        // 查询当前工作流,当前用户是否还有可审批的节点
        List<Task> taskList = taskService.createTaskQuery()
                .processInstanceId(processId)
                .taskCandidateOrAssigned(workflowUser.getId().toString())
                .list();
        // 查询所属角色
//        List<WorkflowGroup> workflowGroupList = workflowGroupDao.findByUserIdAndParentId(workflowUser.getId());
//        // 如果下一个审批节点是角色标签,并且当前用户属于该角色,则自动审批
//        for (WorkflowGroup workflowGroup : workflowGroupList) {
//            List<Task> list = taskService.createTaskQuery()
//                    .processInstanceId(processId)
//                    .taskCandidateOrAssigned(workflowGroup.getId().toString())
//                    .list();
//            if (CollectionUtils.isNotEmpty(list)) {
//                taskList.addAll(list);
//            }
//        }

        if (taskList.size() > 0) {
            // 取出第一个审批
            Task nextTask = taskList.get(0);
            WorkflowAuditDto nextDto = BeanUtils.copyProperties(dto, WorkflowAuditDto.class);
            nextDto.setTaskId(nextTask.getId());
            nextDto.setTaskKey(nextTask.getTaskDefinitionKey());
            nextDto.setTaskName(nextTask.getName());
            resolve(nextDto, workflowUser);
        }
    }

    /**
     * 获取当前审批节点
     *
     * @param actProcessId
     * @return
     */
    private Task getCurrentTask(String actProcessId) {
        // 调用activiti查询工作流所有节点
        List<Task> taskList = taskService.createTaskQuery()
                .processInstanceId(actProcessId)
                .list();
        if (CollectionUtils.isEmpty(taskList)) {
            return null;
        }
        // taskList中所有的taskId都是一样的,只有审核人不同,取第一个即可。
        return taskList.get(0);
    }

}

效果:



 

 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐