前言
在完成了【我的待办】/【我的发起】功能之后,我们最后需要完成【我的已办】功能完成完整流程的闭环。
一、【我的已办】必要性
① 用户体验与效率提升
(1)快速定位处理记录
用户可以快速查看自己已处理过的流程实例,避免重复处理或遗漏信息。
(2)高频操作支持
在实际业务中,用户经常需要查阅自己处理过的事项,比如查看表单内容、审批意见、处理结果等。
② 审计与责任追溯
(1)操作责任可追踪
系统可以明确记录谁在什么时间处理了什么任务,有助于责任划分。
(2)审计支持
为内审、风控部门提供必要的审查依据,配合“流程流转记录”做流程回溯。
③ 流程闭环验证
(1)确认任务是否正确执行
可以回顾流程流转是否按预期进行,是否存在遗漏步骤或错误处理。
(2)结果验证
用户可以查看某个流程最终处理结果是否符合其初衷,特别在多级审批流程中非常关键。
④ 问题排查与异常处理
当业务出现异常(比如审批状态错误、数据处理异常等),用户可以通过“我的已办”快速定位历史流程,协助 IT 或管理员分析问题。特别是在系统出现 流程卡死、环节错位、节点跳转错误 等问题时,是关键排查入口。
⑤ 流程优化的基础数据支撑
对于产品或流程管理人员来说,“已办数据”可以作为用户行为的真实反馈数据,辅助分析:哪些流程步骤最耗时?哪些节点容易出错或回退?哪些业务的处理量最大?
⑥ 对接监控平台或 SLA 平台的必要前提
若未来有 SLA 监控需求(例如:某审批必须在2小时内完成),“我的已办”数据就是评估用户响应时间的基础。有助于构建 KPI 统计、流程考核体系。
⑦ 配合“我的待办”形成完整闭环
“我的待办”展示的是当前要做的,“我的已办”记录的是已经做过的,两者共同组成了完整的任务生命周期。没有“我的已办”,用户只能靠记忆或系统通知去追踪流程,使用体验大打折扣。对接主门户、待办中心(如钉钉、飞书、OA)的系统时,“已办”数据也是集成要求之一。
二、后端服务搭建
① 定义请求参数
我们应该从框架中的认证信息中获取当前用户,不要添加在请求中,增强系统的安全性。所以请求只要有几乎的分页信息即可。
package com.ceair.entity.request;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author wangbaohai
* @ClassName PageReq
* @description: 分页请求参数
* @date 2025年02月16日
* @version: 1.0.0
*/
@Data
public class PageReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 分页查询的页码和每页大小。
*
* pageNo: 当前页码,默认为1。
* pageSize: 每页显示的记录数,默认为10。
*/
private Long current = 1L;
private Long size = 10L;
}
② 定义响应参数
查询结束之后,我们需要考虑的是到底要传递哪些信息以供前端展示,目前只定义了发起人/流程定义/节点时间等信息,可以自行扩展。
package com.ceair.entity.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @author wangbaohai
* @ClassName MyCompleteTaskVO
* @description: 我的已办任务VO
* @date 2025年05月10日
* @version: 1.0.0
*/
@Data
public class MyCompleteTaskVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
// 流程定义ID
private String processDefinitionId;
// 流程定义名称
private String processDefinitionName;
// 流程实例ID
private String processInstanceId;
// 发起人名称
private String startUserName;
// 发起时间
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
private LocalDateTime startTime;
// 结束时间
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
private LocalDateTime endTime;
}
package com.ceair.entity.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* @author wangbaohai
* @ClassName MyCompleteTaskListInfoVO
* @description: 我的已办任务清单VO
* @date 2025年05月10日
* @version: 1.0.0
*/
@Data
public class MyCompleteTaskListInfoVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
// 我的已办任务清单
private List<MyCompleteTaskVO> myCompleteTaskList;
// 我的已办任务数量
private Long myCompleteTaskCount;
}
③ 定义服务接口
/**
* 查询我的完成任务列表
* <p>
* 该方法用于获取用户已完成的任务列表,根据分页请求进行数据检索
* 主要解决了用户需要查看自己已完成的任务需求
*
* @param pageReq 分页请求对象,包含分页查询的必要信息,如当前页码、每页大小等
* @return 返回一个对象,其中包含查询到的任务列表信息以及相关的分页细节
*/
MyCompleteTaskListInfoVO queryMyCompleteTaskList(PageReq pageReq);
④ 实现服务接口
核心流程包括参数校验;用户身份获取;历史任务查询;VO 组装;返回结果;统一异常处理
(1)入参校验与默认分页
分页信息非空校验
if (pageReq == null) {
log.error("查询我的已办任务列表失败,原因:分页信息不能为空");
throw new IllegalArgumentException("查询我的已办任务列表失败,原因:分页信息不能为空");
}
默认页码与页大小
long current = (Objects.nonNull(pageReq.getCurrent()) && pageReq.getCurrent() > 0)
? pageReq.getCurrent() : 1L;
long size = (Objects.nonNull(pageReq.getSize()) && pageReq.getSize() > 0)
? pageReq.getSize() : 10L;
(2)获取当前用户
使用工具类 userInfoUtils
从认证信息上下文中获取当前用户,工具类由脚手架提供,可以自行实现或者扩展。
UserInfo userInfo = userInfoUtils.getUserInfoFromAuthentication();
if (userInfo == null) {
log.error("查询我的已办任务列表失败,原因:用户未登录");
throw new BusinessException("查询我的已办任务列表失败,原因:用户未登录");
}
(3)查询已办任务历史
查询历史任务实例,通过 Flowable 的 HistoryService,按执行者(taskAssignee)条件查询所有已完成的任务实例(不分页)。
List<HistoricTaskInstance> historicTaskInstances =
historyService.createHistoricTaskInstanceQuery()
.taskAssignee(userInfo.getId().toString())
.list();
收集流程实例 ID
Set<String> processInstanceIds = historicTaskInstances.stream()
.map(HistoricTaskInstance::getProcessInstanceId)
.collect(Collectors.toSet());
(4)分页查询历史流程实例
构造查询:通过 processInstanceIds
过滤,只查询这些已办理过的流程实例;并按启动时间倒序排序。
分页拉取:使用 listPage(offset, size)
方法获取当前页数据。
统计总数:count()
方法获得所有匹配实例的总数,用于前端展示分页。
HistoricProcessInstanceQuery piQuery =
historyService.createHistoricProcessInstanceQuery()
.processInstanceIds(processInstanceIds)
.orderByProcessInstanceStartTime().desc();
List<HistoricProcessInstance> historicProcessInstances =
piQuery.listPage((int)(current - 1) * (int)size, (int)size);
long count = piQuery.count();
(5)组装视图对象(VO)
遍历流程实例
List<MyCompleteTaskVO> myCompleteTaskList = new ArrayList<>();
historicProcessInstances.stream()
.filter(Objects::nonNull)
.forEach(historicInstance -> {
MyCompleteTaskVO vo = new MyCompleteTaskVO();
// …填充字段…
myCompleteTaskList.add(vo);
});
填充核心字段
ProcessDefinition def =
processEngine.getRepositoryService().createProcessDefinitionQuery()
.processDefinitionId(historicEntity.getProcessDefinitionId())
.singleResult();
vo.setProcessDefinitionId(def.getId());
vo.setProcessDefinitionName(def.getName());
if (StringUtils.isNotBlank(historicInstance.getStartUserId())) {
Result<Oauth2BasicUserVO> userResult =
systemFeignClient.queryUserById(Long.valueOf(historicInstance.getStartUserId()));
vo.setStartUserName(userResult.getData().getName());
vo.setStartTime(DateUtil.toLocalDateTime(historicInstance.getStartTime()));
vo.setEndTime(DateUtil.toLocalDateTime(historicInstance.getEndTime()));
}
vo.setProcessInstanceId(historicEntity.getId());
(6)异常捕获与日志
参数异常:转化为 BusinessException
并标记“参数错误”。
业务异常:直接抛出,保留原异常信息。
其它异常:捕获并统一包装为“未知异常”的 BusinessException
。
} catch (IllegalArgumentException e) {
log.error("查询我的已办任务列表失败,原因:参数错误", e);
throw new BusinessException("查询我的已办任务列表失败,原因:参数错误", e);
} catch (BusinessException e) {
log.error("查询我的已办任务列表失败,原因:业务异常", e);
throw e;
} catch (Exception e) {
log.error("查询我的已办任务列表失败,原因:未知异常", e);
throw new BusinessException("查询我的已办任务列表失败,原因:未知异常", e);
}
(7)流程示意图
⑤ 定义功能接口
/**
* 分页查询我的完成任务列表。
* <p>
* 权限: /api/v1/myTask/queryMyCompleteTaskList
* 参数: pageReq - 分页请求对象,用于指定分页信息(页码、页大小等)
* 返回: Result<MyCompleteTaskListInfoVO> 返回封装后的分页任务列表信息
* <p>
* 异常处理:
* - 业务层异常 返回查询任务列表失败信息
* - 其他未知异常 系统异常提示
*/
@PreAuthorize("hasAnyAuthority('/api/v1/myTask/queryMyCompleteTaskList')")
@Parameter(name = "pageReq", description = "分页请求对象", required = true)
@Operation(summary = "分页查询我的完成任务列表")
@PostMapping("queryMyCompleteTaskList")
public Result<MyCompleteTaskListInfoVO> queryMyCompleteTaskList(@RequestBody PageReq pageReq) {
try {
// 调用业务层方法,查询出来的分页数据
return Result.success(mayTaskService.queryMyCompleteTaskList(pageReq));
} catch (Exception e) {
log.error("查询我的完成任务列表失败,原因:{}", e.getMessage());
return Result.error("查询我的完成任务列表失败,原因:" + e.getMessage());
}
}
三、创建我的已办界面
① 定义数据类型
由于我们采用ts语法规范,对数据都需要定义类型
// 我的已办任务 VO
export interface MyCompleteTaskVO {
processDefinitionId: string // 流程定义ID (Java String)
processDefinitionName: string // 流程定义名称
processInstanceId: string // 流程实例ID
startUserName: string // 发起人名称
startTime: string // 发起时间 (Java LocalDateTime → string)
endTime: string // 结束时间 (Java LocalDateTime → string)
}
// 我已办任务清单 VO
export interface MyCompleteTaskListInfoVO {
myCompleteTaskList: MyCompleteTaskVO[] // 我的已办任务列表 (Java List<MyCompleteTaskVO> → MyCompleteTaskVO[])
myCompleteTaskCount: number // 我的已办任务数量 (Java Long → number)
}
② 封装请求接口
封装我们要请求的后端接口的工具api
/**
* 查询我的已办任务
*/
export function queryMyCompleteTaskList(data: PageReq) {
return request.post<any>({
url: '/pm-process/api/v1/myTask/queryMyCompleteTaskList',
data,
})
}
③ 绘制页面
我们的页面简单一些,只要一个表格展示数据即可,查询动作在界面初始化的时候触发。
<script lang="ts" setup>
import type { MyCompleteTaskListInfoVO, MyCompleteTaskVO } from '@/api/task/taskType'
import { queryMyCompleteTaskList } from '@/api/task/taskApi'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
// 定义当前页码
const currentPage = ref<number>(1)
// 默认页行数
const pageSize = ref<number>(10)
// 数据总数
const total = ref<number>(0)
// 定义响应式数据 myCompleteTaskData 收集数据
const myCompleteTaskData = ref<MyCompleteTaskVO[]>([])
// 表格列定义
const tableColumns = [
{ label: '#', type: 'index', align: 'center', width: '50px' },
{ label: '流程定义ID', prop: 'processDefinitionId', align: 'center' },
{ label: '流程定义名称', prop: 'processDefinitionName', align: 'center' },
{ label: '流程实例ID', prop: 'processInstanceId', align: 'center' },
{ label: '发起人名称', prop: 'startUserName', align: 'center' },
{ label: '发起时间', prop: 'startTime', align: 'center' },
{ label: '结束时间', prop: 'endTime', align: 'center' },
]
onMounted(() => {
// 初始化分页参数并加载第一页任务数据
currentPage.value = 1
pageSize.value = 10
// 调用获取我的任务分页数据的方法
queryMyCompleteTaskListPage()
})
/**
* 异步函数:查询我的已办任务列表
* 该函数通过调用后端接口,分页查询并更新我的已办任务数据
*/
async function queryMyCompleteTaskListPage() {
try {
// 设置分页参数
const pageReq = {
current: currentPage.value,
size: pageSize.value,
}
// 调用查询我的已办任务列表接口
const result: any = await queryMyCompleteTaskList(pageReq)
// 如果接口调用成功且返回的状态码为200,则更新数据
if (result.success && result.code === 200) {
// 收集数据
const data: MyCompleteTaskListInfoVO = result.data
myCompleteTaskData.value = data.myCompleteTaskList
// 收集总数
total.value = data.myCompleteTaskCount
}
else {
// 显示查询失败的错误提示信息
ElMessage({
message: `查询失败: ${result.message || '未知错误'}`,
type: 'error',
})
}
}
catch (error) {
// 捕获异常并提取错误信息
let errorMessage = '未知错误'
if (error instanceof Error) {
errorMessage = error.message
}
// 显示操作失败的错误提示信息
ElMessage({
message: `查询失败: ${errorMessage || '未知错误'}`,
type: 'error',
})
}
}
/**
* 异步处理页面数据函数
* 本函数主要用于处理页面初始化或数据更新时所需的操作
* 目前函数中的具体实现是调用一个名为 queryMyStartTaskListPage 的方法
* 该方法可能负责从服务器获取数据、处理数据或者更新页面显示
* 注意:此函数没有显式地定义参数和返回值,可能依赖于外部状态或全局变量
*/
async function handerPageData() {
// 调用获取我的任务分页数据的方法
queryMyCompleteTaskListPage()
}
</script>
<template>
<el-table style="margin: 10px 0px;" :border="true" :data="myCompleteTaskData">
<!-- ID 区域 -->
<el-table-column type="selection" align="center" width="50px" />
<!-- 表格数据 区域 -->
<el-table-column
v-for="(column, index) in tableColumns"
:key="index"
:type="column.type"
:label="column.label"
:prop="column.prop"
:align="column.align"
:width="column.width"
/>
</el-table>
<!-- 分页器 -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40, 50]"
layout="prev, pager, next, jumper,->, sizes, total"
:total="Math.max(total, 0)"
@current-change="queryMyCompleteTaskListPage"
@size-change="handerPageData"
/>
</template>
<style scoped>
</style>
四、新增菜单以及按钮的权限信息
五、分配权限
我们给当前登录用户admin(角色是超级管理员)分配菜单和按钮权限
六、查看界面
七、后记
至此,本专栏关于SpringBoot3整合Flowable7.1.0的入门实战已经差不多了,等我之后有空了再慢慢补充会签,加签,与签,或签,跳签等进阶的操作吧。
本文的完整代码仓库地址请查看专栏的第一篇文章。
本文的后端分支是 process-13
本文的前端分支是 process-15