前言
“查看我的发起”功能,就是将当前用户作为流程发起人启动的所有流程实例集中展示,帮助用户随时跟踪自己提交的业务请求的状态与历史,提升透明度与可控性。
业务人员通常不知道流程引擎底层如何运转,只关心“我提交的报销/申请到了哪一步”,该功能就能满足业务人员不再需反复向审批人或管理员打听流程状态,系统自助查询即可,未来可在该视图上附加流程重启、批量操作、AI 预测审批结果等高级功能,具备良好的扩展性。
一、后端服务搭建
① 定义请求参数
在实际查询过程中,可以从系统的认证信息中获取当前业务员信息,就只要传递分页的信息即可。
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 lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* @author wangbaohai
* @ClassName MyStartTaskListInfoVO
* @description: 我发起的流程任务清单VO
* @date 2025年05月07日
* @version: 1.0.0
*/
@Data
public class MyStartTaskListInfoVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
// 我的发起流程任务清单
private List<MyStartTaskVO> myStartTaskList;
// 我的发起流程任务总数
private Long myStartTaskCount;
}
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 MyStartTaskVO
* @description: 我发起的流程任务VO
* @date 2025年05月07日
* @version: 1.0.0
*/
@Data
public class MyStartTaskVO 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;
// 审批状态(0:审批中 1:审批通过 2:审批拒绝)
private Integer status;
// 审批备注
private String comment;
}
③ 定义服务接口
/**
* 查询我发起的任务列表信息
* <p>
* 该方法用于获取用户发起的任务列表,根据分页请求进行数据检索
* 主要解决了用户需要查看自己发起的所有任务的需求
*
* @param pageReq 分页请求对象,包含分页查询的必要信息,如当前页码、每页大小等
* @return 返回一个对象,其中包含查询到的任务列表信息以及相关的分页细节
*/
MyStartTaskListInfoVO queryMyStartTaskList(PageReq pageReq);
④ 实现服务接口
(1)初始化与分页参数校验
初始化 VO:用于最终封装返回结果。
空值校验:若 pageReq 为 null,记录日志并抛出 IllegalArgumentException。
默认值设置:确保 current >= 1、size >= 1,否则分别设为 1 和 10。
// 初始化返回 VO
MyStartTaskListInfoVO myStartTaskListInfoVO = new MyStartTaskListInfoVO();
// 分页信息判空
if (pageReq == null) {
log.error("查询我的已启动任务列表失败,原因:分页信息不能为空");
throw new IllegalArgumentException("查询我的已启动任务列表失败,原因:分页信息不能为空");
}
// 获取当前页与页大小,默认 1 页,每页 10 条
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 获取当前认证用户信息,自定义工具是脚手架中提供的获取认证信息中的登陆人信息的,与本文无关代码没贴出来,可以在文末的代码仓库中找到完整代码。
若返回 null,表示未登录,抛出业务异常。
UserInfo userInfo = userInfoUtils.getUserInfoFromAuthentication();
if (userInfo == null) {
log.error("查询我的已启动任务列表失败,原因:用户未登录");
throw new BusinessException("查询我的已启动任务列表失败,原因:用户未登录");
}
(3)查询历史流程实例并分页
构造查询条件:startedBy(...):只查询当前用户启动的实例。
orderByProcessInstanceStartTime().desc():按启动时间倒序。
执行分页listPage(offset, limit):计算偏移量 offset = (current-1)*size。
获取总数query.count():用于分页组件展示或前端分页控件。
HistoricProcessInstanceQuery query = historyService
.createHistoricProcessInstanceQuery()
.startedBy(userInfo.getId().toString())
.orderByProcessInstanceStartTime()
.desc();
// 分页查询
List<HistoricProcessInstance> list = query
.listPage((int) ((current - 1) * size), (int) size);
// 总数统计
long count = query.count();
(4)结果转换:VO 组装
类型转换:将 HistoricProcessInstance 转为其实现类 HistoricProcessInstanceEntityImpl,以便调用 getStartTime()、getEndTime() 等方法。
关联查询:通过 processDefinitionId 查询 ProcessDefinition,获取流程定义名称。
VO 填充:将基础信息(定义 ID/名称、实例 ID、发起人、开始/结束时间)写入 MyStartTaskVO。
List<MyStartTaskVO> myStartTaskList = new ArrayList<>();
for (HistoricProcessInstance inst : list) {
// 实例转换为内部实现以获取更多字段
HistoricProcessInstanceEntityImpl impl =
(HistoricProcessInstanceEntityImpl) inst;
// 查询流程定义,获取名称等信息
ProcessDefinition pd = repositoryService
.createProcessDefinitionQuery()
.processDefinitionId(impl.getProcessDefinitionId())
.singleResult();
// 填充 VO
MyStartTaskVO vo = new MyStartTaskVO();
vo.setProcessDefinitionId(pd.getId());
vo.setProcessDefinitionName(pd.getName());
vo.setProcessInstanceId(impl.getId());
vo.setStartUserName(userInfo.getAccount());
vo.setStartTime(DateUtil.toLocalDateTime(impl.getStartTime()));
vo.setEndTime(DateUtil.toLocalDateTime(impl.getEndTime()));
myStartTaskList.add(vo);
}
(5)封装返回与异常处理
结果返回:将分页列表与总数写入 VO 并返回。
统一异常封装:
对于 IllegalArgumentException,记录日志后转换为 BusinessException,避免上层重复处理。
对于已捕获的 BusinessException,简化日志并重抛。
对其它所有异常,一律捕获、记录日志,并封装为 BusinessException。
// 封装结果
myStartTaskListInfoVO.setMyStartTaskList(myStartTaskList);
myStartTaskListInfoVO.setMyStartTaskCount(count);
return myStartTaskListInfoVO;
} catch (IllegalArgumentException e) {
log.error("查询我的已启动任务列表失败:非法参数异常", e);
throw new BusinessException("查询我的已启动任务列表失败:非法参数异常", e);
} catch (BusinessException e) {
log.error("查询我的已启动任务列表失败:业务异常", e);
throw new BusinessException("查询我的已启动任务列表失败:业务异常", e);
} catch (Exception e) {
log.error("查询我的已启动任务列表失败:未知异常", e);
throw new BusinessException("查询我的已启动任务列表失败:未知异常", e);
}
(6)完整代码
/**
* 查询当前用户启动的任务列表
*
* @param pageReq 分页请求对象,包含当前页码和每页大小
* @return 返回一个包含任务列表和总数的VO对象
* @throws IllegalArgumentException 如果分页信息为空,则抛出此异常
* @throws BusinessException 如果用户未登录或其他业务逻辑异常,则抛出此异常
*/
@Override
public MyStartTaskListInfoVO queryMyStartTaskList(PageReq pageReq) {
try {
// 初始化
MyStartTaskListInfoVO myStartTaskListInfoVO = new MyStartTaskListInfoVO();
// 分页信息判空
if (pageReq == null) {
log.error("查询我的已启动任务列表失败,原因:分页信息不能为空");
throw new IllegalArgumentException("查询我的已启动任务列表失败,原因:分页信息不能为空");
}
// 获取分页信息,如果不存在默认查询第一页,每页10条数据
long current = (Objects.nonNull(pageReq.getCurrent()) && pageReq.getCurrent() > 0) ? pageReq.getCurrent() :
1L;
long size = (Objects.nonNull(pageReq.getSize()) && pageReq.getSize() > 0) ? pageReq.getSize() : 10L;
// 获取当前用户
UserInfo userInfo = userInfoUtils.getUserInfoFromAuthentication();
if (userInfo == null) {
log.error("查询我的已启动任务列表失败,原因:用户未登录");
throw new BusinessException("查询我的已启动任务列表失败,原因:用户未登录");
}
// 根据当前登录用户查询所有发起的流程
HistoricProcessInstanceQuery historicProcessInstanceQuery =
historyService.createHistoricProcessInstanceQuery()
.startedBy(userInfo.getId().toString())
.orderByProcessInstanceStartTime()
.desc();
// 分页查询
List<HistoricProcessInstance> historicProcessInstanceList = historicProcessInstanceQuery
.listPage((int) (current - 1) * (int) size, (int) size);
// 记录数据总数
long count = historicProcessInstanceQuery.count();
// 遍历 historicProcessInstanceList
List<MyStartTaskVO> myStartTaskList = new ArrayList<>();
historicProcessInstanceList.stream().filter(Objects::nonNull).forEach(historicProcessInstance -> {
// 初始化
MyStartTaskVO myStartTaskVO = new MyStartTaskVO();
// 转换实例获取impl
HistoricProcessInstanceEntityImpl historicProcessInstanceEntity =
(HistoricProcessInstanceEntityImpl) historicProcessInstance;
// 根据流程定义ID获取流程定义信息
ProcessDefinition processDefinition =
repositoryService.createProcessDefinitionQuery()
.processDefinitionId(historicProcessInstanceEntity.getProcessDefinitionId())
.singleResult();
// 收集我的发起任务信息
myStartTaskVO.setProcessDefinitionId(processDefinition.getId());
myStartTaskVO.setProcessDefinitionName(processDefinition.getName());
myStartTaskVO.setProcessInstanceId(historicProcessInstanceEntity.getId());
myStartTaskVO.setStartUserName(userInfo.getAccount());
myStartTaskVO.setStartTime(DateUtil.toLocalDateTime(historicProcessInstanceEntity.getStartTime()));
myStartTaskVO.setEndTime(DateUtil.toLocalDateTime(historicProcessInstanceEntity.getEndTime()));
myStartTaskList.add(myStartTaskVO);
});
// 收集结果数据
myStartTaskListInfoVO.setMyStartTaskList(myStartTaskList);
myStartTaskListInfoVO.setMyStartTaskCount(count);
return myStartTaskListInfoVO;
} catch (IllegalArgumentException e) {
// 捕获参数非法异常并封装为业务异常重新抛出
log.error("查询我的已启动任务列表失败:非法参数异常", e);
throw new BusinessException("查询我的已启动任务列表失败:非法参数异常", e);
} catch (BusinessException e) {
// 捕获业务逻辑异常并重新抛出,避免重复日志输出
log.error("查询我的已启动任务列表失败:业务异常", e);
throw new BusinessException("查询我的已启动任务列表失败:业务异常", e);
} catch (Exception e) {
// 捕获未知异常并封装为业务异常抛出
log.error("查询我的已启动任务列表失败:未知异常", e);
throw new BusinessException("查询我的已启动任务列表失败:未知异常", e);
}
}
⑤ 定义功能接口
/**
* 分页查询我的发起任务列表。
* <p>
* 权限: /api/v1/myTask/queryMyStartTaskList
* 参数: pageReq - 分页请求对象,用于指定分页信息(页码、页大小等)
* 返回: Result<MyStartTaskListInfoVO> 返回封装后的分页任务列表信息
* <p>
* 异常处理:
* - 业务层异常 返回查询任务列表失败信息
* - 其他未知异常 系统异常提示
*/
@PreAuthorize("hasAnyAuthority('/api/v1/myTask/queryMyStartTaskList')")
@Parameter(name = "pageReq", description = "分页请求对象", required = true)
@Operation(summary = "分页查询我的发起任务列表")
@PostMapping("/queryMyStartTaskList")
public Result<MyStartTaskListInfoVO> queryMyStartTaskList(@RequestBody PageReq pageReq) {
try {
// 调用业务层方法,查询出来的分页数据
return Result.success(mayTaskService.queryMyStartTaskList(pageReq));
} catch (Exception e) {
log.error("查询我的发起任务列表失败,原因:{}", e.getMessage());
return Result.error("查询我的发起任务列表失败,原因:" + e.getMessage());
}
}
二、新建我的发起界面
① 定义数据类型
由于我们采用ts语法规范,对数据都需要定义类型
// 我发起的流程任务 VO
export interface MyStartTaskVO {
processDefinitionId: string // 流程定义ID (Java String)
processDefinitionName: string // 流程定义名称
processInstanceId: string // 流程实例ID
startUserName: string // 发起人名称
startTime: string // 发起时间 (Java LocalDateTime → string)
endTime: string // 结束时间 (Java LocalDateTime → string)
status: number // 审批状态(0:审批中 1:通过 2:拒绝)(Java Integer → number)
comment: string // 审批备注 (Java String)
}
// 我发起的流程任务清单 VO
export interface MyStartTaskListInfoVO {
myStartTaskList: MyStartTaskVO[] // 我的发起流程任务清单 (Java List<MyStartTaskVO> → MyStartTaskVO[])
myStartTaskCount: number // 我的发起流程任务总数 (Java Long → number)
}
② 封装请求接口
封装我们要请求的后端接口的工具api
/**
* 分页查询我发起的任务
*/
export function queryMyStartTaskList(data: PageReq) {
return request.post<any>({
url: '/pm-process/api/v1/myTask/queryMyStartTaskList',
data,
})
}
③ 绘制页面
我们的页面简单一些,只要一个表格展示数据即可,查询动作在界面初始化的时候触发。
<script lang="ts" setup>
import type { MyStartTaskListInfoVO, MyStartTaskVO } from '@/api/task/taskType'
import { queryMyStartTaskList } 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)
// 定义响应式数据 myStartTaskData 用于存放查询到的数据
const myStartTaskData = ref<MyStartTaskVO[]>([])
// 表格列定义
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' },
{ label: '操作', align: 'center', width: '200px' },
]
onMounted(() => {
// 初始化分页参数并加载第一页任务数据
currentPage.value = 1
pageSize.value = 10
// 调用获取我的任务分页数据的方法
queryMyStartTaskListPage()
})
/**
* 异步函数:查询我的发起流程任务列表
*
* 该函数通过调用后端API来获取当前用户发起的流程任务列表,并进行分页处理
* 它首先设置分页参数,然后调用API,根据返回结果更新前端数据
* 如果调用失败或返回错误,它将显示错误消息
*/
async function queryMyStartTaskListPage() {
try {
// 设置分页参数
const pageReq = {
current: currentPage.value,
size: pageSize.value,
}
// 调用查询我的发起流程任务列表接口
const result: any = await queryMyStartTaskList(pageReq)
// 如果接口调用成功且返回的状态码为200,则更新数据
if (result.success && result.code === 200) {
// 更新数据
const data: MyStartTaskListInfoVO = result.data
myStartTaskData.value = data.myStartTaskList
total.value = data.myStartTaskCount
}
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() {
// 调用获取我的任务分页数据的方法
queryMyStartTaskListPage()
}
</script>
<template>
<el-table style="margin: 10px 0px;" :border="true" :data="myStartTaskData">
<!-- 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="queryMyStartTaskListPage"
@size-change="handerPageData"
/>
</template>
三、增加菜单以及按钮
四、分配权限
给当前admin用户分配新增的菜单和按钮权限,admin是超级管理员
五、查看界面
后记
下一篇文章我们在操作列,增加审批节点-进度的展示。
本文的后端分支是 process-12
本文的前端分支是 process-14