Flowable7.x学习笔记(二十三 暂完结)查看我的已办

前言

        在完成了【我的待办】/【我的发起】功能之后,我们最后需要完成【我的已办】功能完成完整流程的闭环。

一、【我的已办】必要性

① 用户体验与效率提升

(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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值