动态表当读取历史数据接口
- 我们在上面的渲染动态表单接口 (formDataShow)方法中进行扩充,大致思路是在渲染表单之前会根据 taskId 查出流程 id,并且根据流程实例 id 查出所有表单填写的值,将这些值构建成一个 hashMap 字典,这样就是一个 key-value 的形式,那么接下来如果我们的表单默认值如果不是一个字符串而是一个以 FormProperty 开头的其他表单的名称,我们就从这个字典里面通过 key 将之前存的值读取出来并且渲染出来,这就是我们的大致思路
ActivitiMapper 中编写查询表单历史数据的方法
/**
* 读取表单数据
* @param PROC_INST_ID 流程实例 id
* @return
*/
@Select("select Control_ID_,Control_VALUE_ from formdata where PROC_INST_ID_ = #{PROC_INST_ID}")
public List<Map<String,String>> selectFormData(@Param("PROC_INST_ID")String PROC_INST_ID);
formDataShow 方法中读取表单历史数据,并保存在数据字典中
/*
构建表单控件历史数据字典
key 是控件的 id
value 是控件的值
这里是我们根据 taskId 查询出来的所有流程的表单数据
*/
Map<String,String> controlListMap = new HashMap<String, String>();
// 读取数据库本流程实例的所有表单数据
List<Map<String, String>> tempControlList = activitiMapper.selectFormData(task.getProcessInstanceId());
// 将查询出来的控件 id 和值保存在数据字典中
for(Map<String,String> map : tempControlList){
controlListMap.put(map.get("Control_ID_").toString(), map.get("Control_VALUE_").toUpperCase());
}
之前返回给前端的默认值是写死的现在需要,如果默认值保存的是之前任意环节的控件 id 时,需要读取出之前控件的值
// 如果默认值是之前任意表单控件的 id ,那么我们应该读取之前表单的值
if(split[3].startsWith("FormProperty_")){
// 是表单数据 从之前的保存的所有控件历史数据字典中读取
// 在这里 split[3] 拿到的就是历史表单的 key
if(controlListMap.containsKey(split[3])){
form.put("controlDefValue", controlListMap.get(split[3]));
}else{
// 如果字典中不存在给出错误提示
form.put("controlDefValue", "读取失败,检查"+split[3]+"配置");
}
}else{
// 不是之前表单数据
form.put("controlDefValue", split[3]);
}
画 bpmn js 测试,我们需要在 A 环节填写的数据在 B 环节读取出来
-
张三表单的数据
FormProperty_26555eg-_-string-_-姓名-_-请输入姓名-_-f
FormProperty_0c4etf8-_-string-_-姓别-_-男或女-_-f
- 李四的表单数据
李四自己的表单
FormProperty_0gvpb9n-_-string-_-姓名-_-我是张三-_-f
读取张三表单填写的内容,FormProperty_0c4etf8 这个保存的就是张三表单的key 通过它就可以读取到张三填写的值
FormProperty_11muvfh-_-string-_-姓别-_-FormProperty_0c4etf8-_-f
bpmn js 创建好之后,上传,部署,略过......
查询流程
-
张三填写数据的任务
- 模拟张三提交表单
- 张三表单数据 这个 “女” 就是李四需要读取出来的
FormProperty_26555eg-_-我是张三-_-f!!FormProperty_0c4etf8-_-女--s
-
切换用户查看渲染李四的表单数据,可以看到张三表单中填写的数据被读取出来了
渲染表单 formDataShow 完整代码
/**
* 渲染动态表单
* @return
*/
@GetMapping("/formDataShow")
public AjaxResponse formDataShow(@RequestParam("taskId")String taskId){
try{
// 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
if(GlobalConfig.Test){
// 测试环境使用内存用户登录
securityUtil.logInAs("zhangsan");
}
// 查询任务
Task task = taskRuntime.task(taskId);
/*
构建表单控件历史数据字典
key 是控件的 id
value 是控件的值
这里是我们根据 taskId 查询出来的所有流程的表单数据
*/
Map<String,String> controlListMap = new HashMap<String, String>();
// 读取数据库本流程实例的所有表单数据
List<Map<String, String>> tempControlList = activitiMapper.selectFormData(task.getProcessInstanceId());
// 将查询出来的控件 id 和值保存在数据字典中
for(Map<String,String> map : tempControlList){
controlListMap.put(map.get("Control_ID_").toString(), map.get("Control_VALUE_").toUpperCase());
}
/**
* 关键代码,这里在强调一下,在 activiti6 和 5 的时候实际上是有 form 这个类的
* 但是在 activiti7 中去掉的为了轻量化,但是我们在 7 中还是有方法可以通过流程
* 定义的 id 和任务的 id 拿到一个叫 userTask 的类,在 userTask 类中可以拿到表单属性
*/
UserTask userTask = (UserTask) repositoryService.getBpmnModel(task.getProcessDefinitionId())
/**
* 获取流程元素,这里是要传什么呢?这里实际上是要传任务的key的,在 task 里并没有任务的 key 的
* 在 activiti 6 中是有任务的 key 的,不过我们可以用另外一个方案,我们可以用表单的 key ,这里
* 我们可以表表单的 key 和任务的 key 启成一模一样的名字,这样你拿表单的 key 就相当于拿任务的 key
* 了
*/
.getFlowElement(task.getFormKey());
// 说明该环节是不需要表单的
if(userTask == null){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
"无表单"
);
}
List<FormProperty> formProperties = userTask.getFormProperties();
// 保存分割后的格式返回给前端
List<Map<String,Object>> listMap = new ArrayList<Map<String, Object>>();
for (FormProperty formProperty : formProperties) {
// 分割表单数据
String[] split = formProperty.getId().split("-_-");
Map<String,Object> form = new HashMap<String,Object>();
form.put("id", split[0]);
form.put("controlType", split[1]);
form.put("controlLabel", split[2]);
// 如果默认值是之前任意表单控件的 id ,那么我们应该读取之前表单的值
if(split[3].startsWith("FormProperty_")){
// 是表单数据 从之前的保存的所有控件历史数据字典中读取
// 在这里 split[3] 拿到的就是历史表单的 key
if(controlListMap.containsKey(split[3])){
form.put("controlDefValue", controlListMap.get(split[3]));
}else{
// 如果字典中不存在给出错误提示
form.put("controlDefValue", "读取失败,检查"+split[3]+"配置");
}
}else{
// 不是之前表单数据
form.put("controlDefValue", split[3]);
}
form.put("controlParam", split[4]);
listMap.add(form);
}
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
listMap
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"渲染动态表单失败",
e.toString()
);
}
}
高亮历史流程渲染
效果
先画流程图,上传,部署,启动和之前的一样,这里略过
获取需要高亮的连线 id 编号方法
// 获取一条流程实例历史
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(instanceId)
.singleResult();
// 根据流程定义 key 获取 BMPN
BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
// 获取流程
Process process = bpmnModel.getProcesses().get(0);
// 获取所有流程 FlowElement 的信息,就是所有bpmn的节点
Collection<FlowElement> flowElements = process.getFlowElements();
/**
* 这个 map 中的 key 就是 开始节点和结束节点的编号拼起来的字符串,
* value 就是开始节点和结束节点的连线,到时候我们根据开始节点和结束节点
* 就可以获取到需要高亮的连线
*/
Map<String,String> map = new HashMap<String,String>();
for (FlowElement flowElement : flowElements) {
// 判断是否是线条
if(flowElement instanceof SequenceFlow){
SequenceFlow sequenceFlow = (SequenceFlow)flowElement;
String ref = sequenceFlow.getSourceRef();
String targetRef = sequenceFlow.getTargetRef();
/**
* 保存开始节点和结束节点,与它们之间连线的对应关系
* key: 开始节点 编号 + 结束节点 编号
* value: 连线编号
*/
map.put(ref+targetRef,sequenceFlow.getId());
}
}
/**
* 获取已经完成的全部流程历史节点
*/
List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(instanceId)
.list();
/**
* 将各个历史节的开始节点和结束几点的编号两两对应起来,
* 就可以从上面的 map 中获取到需要高亮的连线
*/
Set<String> keyList = new HashSet<String>();
for (HistoricActivityInstance i : list) {
for (HistoricActivityInstance j : list) {
if(i != j){
keyList.add(i.getActivityId()+j.getActivityId());
}
}
}
// 获取高亮连线 id
Set<String> highLine = new HashSet<String>();
// 根据已经完成的(开始节点编号+结束节点编号)组成的 key 获取需要高亮的连线编号
keyList.forEach(s -> highLine.add(map.get(s)));
最终获取到的连线,因为 任务1 我已经执行过了所以获取到的是两根连线编号
获取需要高亮已经完成的任务节点编号方法
// 获取已经完成的节点
List<HistoricActivityInstance> listFinished = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(instanceId)
.finished()
.list();
// 已经完成的节点高亮
Set<String> highPoint = new HashSet<>();
// 保存已经完成的流程节点编号
listFinished.forEach(s -> highPoint.add(s.getActivityId()));
这时获取到的是 开始节点 和 任务1 节点,因为任务一我已经执行过了
获取需要高亮下一步我要代办执行的任务节点编号方法
// 获取代办节点
List<HistoricActivityInstance> listUnFinished = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(instanceId)
.unfinished()
.list();
// 代办的节点高亮
Set<String> waitingToDo = new HashSet<>();
// 保存需要代办的节点编号
listUnFinished.forEach(s -> waitingToDo.add(s.getActivityId()));
这时获取到的就是 任务2 节点编号,因为 任务1 执行完之后就流转到 任务2 了
获取当前用户已经完成的任务节点编号
// 获取当前用户完成的任务
List<HistoricTaskInstance> taskInstanceList = historyService.createHistoricTaskInstanceQuery()
.taskAssignee(userInfoBean.getUsername())
.processInstanceId(instanceId)
.finished()
.list();
// 当前用户完成的高亮
Set<String> iDo = new HashSet<String>();
// 保存用户完成的节点编号
taskInstanceList.forEach(s -> iDo.add(s.getTaskDefinitionKey()));
当前用户完成了 任务1
最终得到的数据
{
"status": 0,
"msg": "成功",
"obj": {
"waitingToDo": [
"Activity_2"
],
"highPoint": [
"StartEvent_1",
"Activity_1"
],
"iDo": [
"Activity_1"
],
"highLine": [
null,
"Flow_2",
"Flow_1"
]
}
}
完整方法
/**
* 高亮显示路程历史
* @param instanceId 流程实例id
* @param userInfoBean 用户信息
* @return
*/
@GetMapping("/getHighlight")
public AjaxResponse getHighlight(@RequestParam("instanceId")String instanceId,
@AuthenticationPrincipal UserInfoBean userInfoBean){
try {
// 获取一条流程实例历史
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(instanceId)
.singleResult();
// 根据流程定义 key 获取 BMPN
BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
// 获取流程
Process process = bpmnModel.getProcesses().get(0);
// 获取所有流程 FlowElement 的信息,就是所有bpmn的节点
Collection<FlowElement> flowElements = process.getFlowElements();
/**
* 这个 map 中的 key 就是 开始节点和结束节点的编号拼起来的字符串,
* value 就是开始节点和结束节点的连线,到时候我们根据开始节点和结束节点
* 就可以获取到需要高亮的连线
*/
Map<String,String> map = new HashMap<String,String>();
for (FlowElement flowElement : flowElements) {
// 判断是否是线条
if(flowElement instanceof SequenceFlow){
SequenceFlow sequenceFlow = (SequenceFlow)flowElement;
String ref = sequenceFlow.getSourceRef();
String targetRef = sequenceFlow.getTargetRef();
/**
* 保存开始节点和结束节点,与它们之间连线的对应关系
* key: 开始节点 编号 + 结束节点 编号
* value: 连线编号
*/
map.put(ref+targetRef,sequenceFlow.getId());
}
}
/**
* 获取已经完成的全部流程历史节点
*/
List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(instanceId)
.list();
/**
* 将各个历史节的开始节点和结束几点的编号两两对应起来,
* 就可以从上面的 map 中获取到需要高亮的连线
*/
Set<String> keyList = new HashSet<String>();
for (HistoricActivityInstance i : list) {
for (HistoricActivityInstance j : list) {
if(i != j){
keyList.add(i.getActivityId()+j.getActivityId());
}
}
}
// 获取高亮连线 id
Set<String> highLine = new HashSet<String>();
// 根据已经完成的(开始节点编号+结束节点编号)组成的 key 获取需要高亮的连线编号
keyList.forEach(s -> highLine.add(map.get(s)));
// 获取已经完成的节点
List<HistoricActivityInstance> listFinished = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(instanceId)
.finished()
.list();
// 已经完成的节点高亮
Set<String> highPoint = new HashSet<>();
// 保存已经完成的流程节点编号
listFinished.forEach(s -> highPoint.add(s.getActivityId()));
// 获取代办节点
List<HistoricActivityInstance> listUnFinished = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(instanceId)
.unfinished()
.list();
// 代办的节点高亮
Set<String> waitingToDo = new HashSet<>();
// 保存需要代办的节点编号
listUnFinished.forEach(s -> waitingToDo.add(s.getActivityId()));
// 获取当前用户完成的任务
List<HistoricTaskInstance> taskInstanceList = historyService.createHistoricTaskInstanceQuery()
.taskAssignee(userInfoBean.getUsername())
.processInstanceId(instanceId)
.finished()
.list();
// 当前用户完成的高亮
Set<String> iDo = new HashSet<String>();
// 保存用户完成的节点编号
taskInstanceList.forEach(s -> iDo.add(s.getTaskDefinitionKey()));
Map<String,Object> reMap = new HashMap<String, Object>();
// 高亮已经完成的节点
reMap.put("highPoint",highPoint);
// 高亮连线节点编号
reMap.put("highLine", highLine);
// 高亮代办节点编号
reMap.put("waitingToDo", waitingToDo);
// 高亮当前用户完成的节点编号
reMap.put("iDo",iDo);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
reMap
);
} catch (Exception e) {
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"高亮历史任务失败",
e.toString()
);
}
}
bpmn js 扩展下载
在 bpmnjs 目录下 app 目录下的 index.html 中添加导出按钮
<li>
<a id="downloadBPNM" href title="download as SVG image">
导出
</a>
</li>
创建 tools.js
import $ from 'jquery'
const proHost = window.location.protocol + "//" + window.location.host;
const href = window.location.href.split("bpmnjs")[0];
const key = href.split(window.location.host)[1];
const publicurl = proHost + key;
const tools = {
/**
* 下载方法
* @param bpmnModeler
*/
download(bpmnModeler){
var downloadLink = $("#downloadBPNM");
bpmnModeler.saveXML({format:true},function(err,xml){
if(err){
return console.error("could not save bpmn",err);
}
tools.setEncoded(downloadLink,"digaram.bpmn",xml);
});
},
/**
*
* @param link 下载的按钮
* @param name 下载的名字
* @param data 下载的数据
*/
setEncoded(link, name, data) {
var encodedData = encodeURIComponent(data);
if (data) {
link.addClass('active').attr({
'href': 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
'download': name
});
} else {
link.removeClass('active');
}
}
}
export default tools
在 index.html 同级目录的 index.js 中引入创建的 tools.js
在 index.html 同级目录的 index.js 添加导出按钮的点击事件,bpmnModeler 就是 bpmn 的模型
$("#downloadBPNM").on('click',function(){
tools.download(bpmnModeler);
});
测试 正常导出
在线部署 bpmn
index.html 中添加部署按钮
<li>
<a id="saveBPNM" href>
部署
</a>
</li>
index.js 中添加部署的点击事件
// 部署 bpmn
$("#saveBPNM").on('click',function(){
tools.saveBPMN(bpmnModeler);
});
tools.js 中添加在线部署的方法
/**
* 部署方法
* @param bpmnModeler
*/
saveBPMN(bpmnModeler){
var downloadLink = $("#downloadBPNM");
bpmnModeler.saveXML({format:true},function(err,xml){
if(err){
return console.error("could not save bpmn",err);
}
console.info(xml)
// 参数就是 bpmn xml 字符串
var param = {
"xmlBPMN": xml
};
// 调用后台接口上传bpmn
$.ajax({
url: publicurl + "processDefinition/addDeploymentByString",
type: "post",
dataType: "json",
data: param,
success: function(res){
if(res.status == 0){
alert("部署成功");
}else{
alert("部署失败");
}
},
error: function(err){
console.info(err);
}
});
});
}
上传 BPMN 并展示
index.html 添加导入按钮
<!-- 导入 bpmn -->
<li>
<form id="form" name="myForm" onsubmit="return false" method="post" enctype="multipart/form-data" title="上传文件">
<input type="file" name="uploadFile" id="uploadFile" accept=".bpmn" style="display:none"/>
<label class="label" for="uploadFile">导入</label>
</form>
</li>
index.js 添加导入按钮变事件
// 上传 bpmn
$("#uploadFile").on("change",function(i){
tools.uploadBPMN(bpmnModeler);
});
tools.js 中编写 uploadBPMN 上传 bpmn 方法
/**
* 上传 bpmn
*/
uploadBPMN(bpmnModeler){
// 获取文件
var fileUpload = document.myForm.uploadFile.files[0];
// 创建 FormData 对象
var fm = new FormData();
fm.append("processFile",fileUpload)
$.ajax({
url: publicurl + "processDefinition/uploadBPMN",
type: "post",
data: fm,
async: false,
contentType: false,
processData: false,
success: function(res){
if(res.status == 0){
var url = publicurl + "bpmn/" + res.obj;
// 打开上传的 bpmn
tools.openBPMN_URL(bpmnModeler,url);
}else{
alert(res.msg);
}
}
});
},
/**
* 打开上传的 bpmn
* @param url
*/
openBPMN_URL(bpmnModeler,url){
$.ajax(url,{dataType: "text"}).done(
// 返回 xml 文件
async function (xml) {
try {
// 导入 xml
await bpmnModeler.importXML(xml);
}catch (e) {
console.error(e);
}
});
}
编写后台上传接口,uploadBpmnPath 是上传的文件目录在 yml 中配置的换成自己的就可以了
/**
* 添加流程定义通过在线提交 BPMN 的 xml
* @param processFile
* @return
*/
@PostMapping("/uploadBPMN")
public AjaxResponse addDeploymentByString(HttpServletRequest request,
@RequestParam("processFile") MultipartFile processFile
){
try{
if(processFile.isEmpty()){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
GlobalConfig.ResponseCode.ERROR.getDesc(),
"BPMN 不能为空"
);
}
// 获取原始文件名
String originFileName = processFile.getOriginalFilename();
// 获取文件后缀
String suffixName = originFileName.substring(originFileName.lastIndexOf("."));
// 新的文件名
String fileName = UUID.randomUUID() + suffixName;
// 上传文件的路径
File filePath = new File(uploadBpmnPath+fileName);
if(!filePath.getParentFile().exists()){
filePath.getParentFile().mkdirs();
}
// 上传
processFile.transferTo(filePath);
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.SUCCESS.getCode(),
GlobalConfig.ResponseCode.SUCCESS.getDesc(),
fileName
);
}catch (Exception e){
return AjaxResponse.AjaxData(
GlobalConfig.ResponseCode.ERROR.getCode(),
"上传 BPMN 失败",
e.getMessage()
);
}
}
}
uploadBpmnPath 上传的目录,yml 中 通过 @Value("${uploadBpmnPath}") 引用
# bpmn 文件上传路径
upload:
bpmn:
path: G:/gu-pao/activiti7_workflow/src/main/resources/resources/bpmn/
上传的文件目录映射
@Configuration
public class PathMapping implements WebMvcConfigurer {
@Value("${upload.bpmn.path}")
private String uploadBpmnPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 添加默认映射
registry.addResourceHandler("/**").addResourceLocations("classpath:/resources/");
// 添加 bpmn 路径映射
registry.addResourceHandler("/bpmn/**")
.addResourceLocations("file:"+uploadBpmnPath.replace("/","\\"));
}
}
测试导入本地 bpmn,成功上传并回显
源码地址
https://gitee.com/jitashou18089237297/activiti-integrates-bpmnjs.git