基于若依开发管理项目中引入工作流引擎activiti7,包含前后端(原创)

2023-05-16

原项目中用到了工作流引擎,使用若依框架开发,
原二开使用项目:https://gitee.com/y_project/RuoYi-Vue
基于activiti7地址:https://gitee.com/smell2/ruoyi-vue-activiti
导入模块到原二开项目中

使用步骤

  1. admin导入bpmn文件,或者绘制工作流,并激活。
  2. post为普通员工进入我的审批可以查看所有审批(经销商角色)
  3. 数据会存入历史表和任务表
    在这里插入图片描述
  4. 历史表展现全部任务数据,task会展示对应岗位的相应条数据
  5. 登录商管账号,进入代办任务
  6. 进行审批,通过则状态变为“待财务审核”,进入下一节点;不通过则状态变为“审核失败”,流转结束
  7. 登录财务账号,进入代办任务
  8. 进行审批,通过则状态变为“审核成功”,流转结束;不通过则状态变为“审核失败”,流转结束
  9. 经销商可以查看审批,流转完成

注意事项

在这里插入图片描述
工作流程菜单在开发工具查看(没有的话需要事先插入db和相关代码),在线绘制进行bpmn格式的流程图,部署流程导入已经存在的bpmn文件,根据流程key,在java中进行声明和调用该key即可正式使用该工作流

后端相关

activiti7相关依赖

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter</artifactId>
    <version>7.1.0.M4</version>
</dependency>
<dependency>
    <groupId>org.activiti.dependencies</groupId>
    <artifactId>activiti-dependencies</artifactId>
    <version>7.1.0.M4</version>
    <type>pom</type>
</dependency>

解决依赖冲突
activiti7新版本会和mybatis和spring security起冲突,如果项目使用shiro等安全框架会有更大的适配问题
原项目有spring security的情况下需加上配置文件属性

main:
    allow-bean-definition-overriding: true

activity配置文件属性如下,包括检测流程定义,自动更新和生成db系统表,历史数据级别和使用等

activiti:
  check-process-definitions: false
  database-schema-update: true
  history-level: full
  db-history-used: true

db依赖问题或mybatis版本冲突解决方法如下

<exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </exclusion>
 </exclusions>

数据源配置,给url加上如下后缀属性

%2B8&nullCatalogMeansCurrent=true

完整url格式如下:

url: jdbc:mysql://localhost:99999/test11111111?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true

在使用mysql-connect 8.+以上版本的时候需要添加***nullCatalogMeansCurrent=true***参数,否则在使用mybatis-generator生成表对应的xml等时会扫描整个服务器里面的全部数据库中的表,而不是扫描对应数据库的表。因此mysql会扫描所有的库来找表,如果其他库中有相同名称的表,activiti就以为找到了,本质上这个表在当前数据库中并不存在。

接口

流程部署
在这里插入图片描述

可以将bpmn文件放在resource下的processes目录下,activiti启动的时候会自动加载该目录下的bpmn文件,或者通过调用接口方式部署:

  • 上传文件部署

    @PostMapping("/uploadFileAndDeployment")
    public BaseResponse uploadFileAndDeployment(
    @RequestParam(“processFile”)MultipartFile processFile,
    @RequestParam(value = “processName”,required = false) String processName){

    String originalFilename = processFile.getOriginalFilename();
    String extension = FilenameUtils.getExtension(originalFilename);
    if (processName != null){
        processName = originalFilename;
    }
    try {
        InputStream inputStream = processFile.getInputStream();
        Deployment deployment = null;
        if ("zip".equals(extension)){
            // 压缩包部署方式
            ZipInputStream zipInputStream = new ZipInputStream(inputStream);
            deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).name(processName).deploy();
        }else if ("bpmn".equals(extension)){
            // bpmn文件部署方式
            deployment = repositoryService.createDeployment().addInputStream(originalFilename,inputStream).name(processName).deploy();
        }
        return BaseResponse.success(deployment);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return BaseResponse.success();
    

    }

  • 上传BPMN内容字符串部署

    @PostMapping("/postBPMNAndDeployment")
    public BaseResponse postBPMNAndDeployment(@RequestBody AddXMLRequest addXMLRequest){
    Deployment deploy = repositoryService.createDeployment()
    // .addString 第一次参数的名字如果没有添加.bpmn的话,不会插入到 ACT_RE_DEPLOYMENT 表中
    .addString(addXMLRequest.getProcessName()+".bpmn", addXMLRequest.getBpmnContent())
    .name(addXMLRequest.getProcessName())
    .deploy();
    return BaseResponse.success(deploy);
    }

  • 获取流程资源文件

    @GetMapping("/getProcessDefineXML")
    public void getProcessDefineXML(String deploymentId, String resourceName, HttpServletResponse response){
    try {
    InputStream inputStream = repositoryService.getResourceAsStream(deploymentId,resourceName);
    int count = inputStream.available();
    byte[] bytes = new byte[count];
    response.setContentType(“text/xml”);
    OutputStream outputStream = response.getOutputStream();
    while (inputStream.read(bytes) != -1) {
    outputStream.write(bytes);
    }
    inputStream.close();
    } catch (Exception e) {
    e.toString();
    }
    }

流程实例

  • 启动

    @PostMapping("/startProcess")
    public BaseResponse startProcess(
    String processDefinitionKey,
    String instanceName,
    @AuthenticationPrincipal LocalUserDetail userDetail){

    ProcessInstance processInstance = null;
    try{
        StartProcessPayload startProcessPayload = ProcessPayloadBuilder.start().withProcessDefinitionKey(processDefinitionKey)
            .withBusinessKey("businessKey")
            .withVariable("sponsor",userDetail.getUsername())
            .withName(instanceName).build();
        processInstance = processRuntime.start(startProcessPayload);
    }catch (Exception e){
        System.out.println(e);
        return BaseResponse.error("开启失败:"+e.getLocalizedMessage());
    }
    return BaseResponse.success(processInstance);
    

    }

  • 挂起

    @PostMapping("/suspendInstance/{instanceId}")
    public BaseResponse suspendInstance(@PathVariable String instanceId){
    ProcessInstance processInstance = processRuntime.suspend(ProcessPayloadBuilder.suspend().withProcessInstanceId(instanceId).build());
    return BaseResponse.success(processInstance);
    }

  • 激活

    @PostMapping("/resumeInstance/{instanceId}")
    public BaseResponse resumeInstance(@PathVariable String instanceId){
    ProcessInstance processInstance = processRuntime
    .resume(ProcessPayloadBuilder.resume().withProcessInstanceId(instanceId).build());
    return BaseResponse.success(processInstance);
    }

任务数据

  • 通过taskid完成任务

    @PostMapping("/completeTask/{taskId}")
    public BaseResponse completeTask(@PathVariable String taskId){
    Task task = taskRuntime.task(taskId);
    if (task.getAssignee()==null){
    // 说明任务需要拾取
    taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(taskId).build());
    }
    taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskId).build());
    return BaseResponse.success();
    }

  • 获取自己的任务(与鉴权机制挂钩)

    @GetMapping("/getTasks")
    public BaseResponse getTasks(){
    Page taskPage = taskRuntime.tasks(Pageable.of(0, 100));
    List tasks = taskPage.getContent();
    List taskVOS = new ArrayList<>();
    for (Task task : tasks) {
    TaskVO taskVO = TaskVO.of(task);
    ProcessInstance instance = processRuntime.processInstance(task.getProcessInstanceId());
    taskVO.setInstanceName(instance.getName());
    taskVOS.add(taskVO);
    }
    return BaseResponse.success(taskVOS);
    }

历史数据

  • 查询

    public List getProcessHistoryByBusinessKey(String businessKey) {
    ProcessInstance instance = runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(businessKey).singleResult();
    List historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery().processInstanceId(instance.getId())
    .orderByHistoricActivityInstanceStartTime().asc().list();
    List historicActivityInstanceVOList = new ArrayList<>();
    historicActivityInstanceList.forEach(historicActivityInstance -> historicActivityInstanceVOList.add(VOConverter.getHistoricActivityInstanceVO(historicActivityInstance)));
    return historicActivityInstanceVOList;
    }

  • 详情查询

    HistoricDetailQuery historicDetailQuery = historyService.createHistoricDetailQuery();
    List historicDetails = historicDetailQuery.processInstanceId(instanceId).orderByTime().list();
    for (HistoricDetail hd: historicDetails) {
    System.out.println(“流程实例ID:”+hd.getProcessInstanceId());
    System.out.println(“活动实例ID:”+hd.getActivityInstanceId());
    System.out.println(“执行ID:”+hd.getTaskId());
    System.out.println(“记录时间:”+hd.getTime());
    }

  • 历史流程实例查询

    HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
    List processInstances = historicProcessInstanceQuery.processDefinitionId(processDefinitionId).list();
    for (HistoricProcessInstance hpi : processInstances) {
    System.out.println(“业务ID:”+hpi.getBusinessKey());
    System.out.println(“流程定义ID:”+hpi.getProcessDefinitionId());
    System.out.println(“流程定义Key:”+hpi.getProcessDefinitionKey());
    System.out.println(“流程定义名称:”+hpi.getProcessDefinitionName());
    System.out.println(“流程定义版本:”+hpi.getProcessDefinitionVersion());
    System.out.println(“流程部署ID:”+hpi.getDeploymentId());
    System.out.println(“开始时间:”+hpi.getStartTime());
    System.out.println(“结束时间:”+hpi.getEndTime());
    }

  • 任务历史查询(某一次流程的执行经历的多少任务)

    HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
    List taskInstances = historicTaskInstanceQuery.taskId(taskId).list();
    for (HistoricTaskInstance hti : taskInstances) {
    System.out.println(“开始时间:”+hti.getStartTime());
    System.out.println(“结束时间:”+hti.getEndTime());
    System.out.println(“任务拾取时间:”+hti.getClaimTime());
    System.out.println(“删除原因:”+hti.getDeleteReason());
    }

github例子:https://github.com/Activiti/activiti-examples

鉴权机制

官方security轮子

@Component
public class SecurityUtil {
    // 模拟调用了SpringSecurity 登录鉴权
    private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
    @Autowired
    private UserDetailsService userDetailsService;
    public void logInAs(String username) {
        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (user == null) {
            throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
        }
        logger.info("> Logged in as: " + username);
        SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return user.getAuthorities();
            }
            @Override
            public Object getCredentials() {
                return user.getPassword();
            }
            @Override
            public Object getDetails() {
                return user;
            }
            @Override
            public Object getPrincipal() {
                return user;
            }
            @Override
            public boolean isAuthenticated() {
                return true;
            }
            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            } 
            @Override
            public String getName() {
                return user.getUsername();
            }
        }));
        org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
    }
}

与原项目spring security整合
新api包括taskRuntime和processRuntime都会强制使用security,源码使用了如下注解:
@PreAuthorize(“hasRole(‘ACTIVITI_USER’)”)
直接使用接口会导致无权限不允许访问

activiti7中对原有的一些接口做了二次封装,从而进一步简化了用户的使用流程。
通过查看这个两个API的实现类源码来看,调用的话需要调用的用户含有ACTIVITI_USER角色权限。所以,如果没有使用SpringSecurity的话,这两个API便不能直接调用。

  • 先在用户验证处理中插入GROUP_的岗位post和加入ACTIVITI_USER的role

    public UserDetails createLoginUser(SysUser user)
    {
    Set postCode = sysPostService.selectPostCodeByUserId(user.getUserId());
    postCode = postCode.parallelStream().map( s -> “GROUP_” + s).collect(Collectors.toSet());
    postCode.add(“ROLE_ACTIVITI_USER”);
    List collect = postCode.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList());
    return new LoginUser(user, permissionService.getMenuPermission(user), collect);
    //return new LoginUser(user, permissionService.getMenuPermission(user));
    }

  • 在login controller中,每次登录的时候鉴权

    // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
    authentication = authenticationManager
    .authenticate(new UsernamePasswordAuthenticationToken(username, password));

最后将信息放入token中处理

if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
    tokenService.verifyToken(loginUser);
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
  • SecurityConfig也别忘记配

    httpSecurity
    // CSRF禁用,因为不使用session
    .csrf().disable()
    // 认证失败处理类
    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
    // 基于token,所以不需要session
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
    // 过滤请求
    .authorizeRequests()
    // 对于登录login 验证码captchaImage 允许匿名访问
    .antMatchers("/ssologin/token", “/ssologin”,"/login", “/captchaImage”,"/getssourl","/ssologin").anonymous()
    .antMatchers(
    HttpMethod.GET,
    “/.html",
    "/**/
    .html”,
    “//*.css",
    "/
    /.js"
    ).permitAll()
    .antMatchers("/processDefinition/").permitAll()
    .antMatchers("/activitiHistory/
    ").permitAll()
    .antMatchers("/profile/").anonymous()
    .antMatchers("/common/download
    ").anonymous()
    .antMatchers("/common/download/resource**").anonymous()
    .antMatchers("/swagger-ui.html").anonymous()
    .antMatchers("/swagger-resources/").anonymous()
    .antMatchers("/webjars/
    ").anonymous()
    .antMatchers("/
    /api-docs”).anonymous()
    .antMatchers("/druid/**").anonymous()
    // 除上面外的所有请求全部需要鉴权认证
    .anyRequest().authenticated()
    .and()
    .headers().frameOptions().disable();
    httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
    // 添加JWT filter
    httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

查询当前用户任务

  • taskservice方法

    //1.得到ProcessEngine对象
    ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
    //2.得到TaskService对象
    TaskService taskService = processEngine.getTaskService();
    //3.根据流程定义的key,负责人assignee来实现当前用户的任务列表查询
    Task task = taskService.createTaskQuery()
    .processDefinitionKey(“holiday”)
    .taskAssignee(SecurityUtils.getUsername() //通过鉴权拿到当前角色)
    .singleResult();
    //4.任务列表的展示
    System.out.println(“流程实例ID:”+task.getProcessInstanceId());
    System.out.println(“任务ID:”+task.getId()); //5002
    System.out.println(“任务负责人:”+task.getAssignee());
    System.out.println(“任务名称:”+task.getName());

  • taskruntime方法(新版本api)
    通过taskRuntime.tasks获取任务列表并分页,在实例化一条工作流后,activiti会将数据存到ACT_RU_TASK和ACT_RU_IDENTITYLINK表中,IDENTITYLINK通过task_id作为外键关联TASK表,IDENTITYLINK根据用户身份鉴别相应的角色,通过GROUP_ID筛选出对应数据,TYPE_字段则显示该角色是参与者还是贡献者,db如下:
    在这里插入图片描述
    若衣源码:

    @Override
    public Page selectProcessDefinitionList(PageDomain pageDomain) {
    Page list = new Page();
    org.activiti.api.runtime.shared.query.Page pageTasks = taskRuntime.tasks(Pageable.of((pageDomain.getPageNum() - 1) * pageDomain.getPageSize(), pageDomain.getPageSize()));
    List tasks = pageTasks.getContent();
    int totalItems = pageTasks.getTotalItems();
    list.setTotal(totalItems);
    if (totalItems != 0) {
    Set processInstanceIdIds = tasks.parallelStream().map(t -> t.getProcessInstanceId()).collect(Collectors.toSet());
    List processInstanceList = runtimeService.createProcessInstanceQuery().processInstanceIds(processInstanceIdIds).list();
    List actTaskDTOS = tasks.stream()
    .map(t -> new ActTaskDTO(t, processInstanceList.parallelStream().filter(pi -> t.getProcessInstanceId().equals(pi.getId())).findAny().get()))
    .collect(Collectors.toList());
    list.addAll(actTaskDTOS);
    }
    return list;
    }

实际项目遇到的问题及解决方法

Error updating database. Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘VERSION_’ in ‘field list’
Error updating database. Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘PROJECT_RELEASE_VERSION_’ in ‘field list’
原因:
创建表缺少VERSION_字段
添加两个字段。

新版bug问题解决

alter table ACT_RE_DEPLOYMENT add column PROJECT_RELEASE_VERSION_ varchar(255) DEFAULT NULL;
alter table ACT_RE_DEPLOYMENT add column VERSION_ varchar(255) DEFAULT NULL;

在这里插入图片描述

前端相关

基于BPMN2.0的工作流
demo实例https://demo.bpmn.io/s/start
节点如下,教程https://www.jianshu.com/p/a8a21870986a
在这里插入图片描述

审批流程图:
在这里插入图片描述
bpmnjs引入
导入相关代码,包括我的审批,代办任务,历史流程等的表单设计和模块划分,其他的调整等

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于若依开发管理项目中引入工作流引擎activiti7,包含前后端(原创) 的相关文章

随机推荐

  • 搜索文件内容的几种方式

    搜索文件的几种方式 xff1a 一 提取文件 xff0c 插入数据库text xff0c 使用like 查询 使用poi或PageOffice提取文件内容文字 缺点 xff1a 只适合数据量不大的情况 二 提取文件 xff0c 插入数据库t
  • Centos 7虚拟机ifconfig ens或ip addr时,ens33不显示inet地址

    systemctl stop NetworkManager systemctl disable NetworkManager
  • nodeinternalmodulescjsloader936 throw err; 求解决

    D ethereumDkfuwq gt node app js node internal modules cjs loader 936 throw err Error Cannot find module safe buffer Requ
  • Tomcat 下载安装教程

    文章目录 参考资料1 下载2 安装3 卸载4 启动5 关闭6 配置7 部署8 IDEA使用Tomcat 8 1 集成本地Tomcat8 2 Tomcat Maven插件 参考资料 视频 使用Tomcat的前提是你已经熟练Java xff0c
  • JS说古道今

    JS说古道今 本文概要 讲述js的来源及重要的语法特性 xff0c 包括数据类型 DOM 作用域等 xff08 由于专业性比较强就不写诗扯淡了 61 61 xff0c 我尽量写的有趣点吧 JS JSP xff1f JavaScript xf
  • 删除集合当中的空元素(Collections.singleton(null)与stream())

    Arrays asList创建的数据为定长集合 xff0c 集合长度在操作时是不可以改变的 xff0c 不能对集合进行增删操作 Collections singleton null 相关 span class token comment 反
  • RabbitMQ配置更改TCP默认端口5672

    前言 公司新项目需集成RabbitMQ xff0c 但服务器环境已经安装了ActiveMQ 今天同事不说还不知道 xff0c 导致安装后俩MQ打架 端口冲突 而发生的一系列问题 没办法 xff0c 后来居上的就很被动 xff0c 于是就得改
  • WebSocket服务端消息推送

    前言 xff1a 移动互联网蓬勃发展的今天 xff0c 大部分手机 APP和网站都提供了消息推送功能 xff0c 如新闻客户端的热点新闻推荐 xff0c IM 工具的聊天消息提醒 xff0c 电商产品促销信息 xff0c 企业应用的通知和审
  • tomcat7下载

    百度网盘下载 xff1a 链接 xff1a https pan baidu com s 1qbaxHo0dEdL9JC08yzbL6Q 提取码 xff1a ysz9
  • Springboot查看日志

    Springboot查看日志 以前用springMVC时查看日志直接进入tail f out 但是用了springboot之后 xff0c 一时不知道如何实现 xff0c 记录一下 以下以log4j2为日志框架 先在log4j2 xml中添
  • 爬取CSDN专栏文章到本地,并保存为html、pdf、md格式

    前言 突然想爬取CSDN的专栏文章到本地保存了 xff0c 为了影响小一点 xff0c 特地挑选CSDN的首页进行展示 综合资讯这一测试点是什么找到的呢 xff1f 就是点击下图的热点文章 xff0c 然后跳转到具体文章 xff0c 然后再
  • Spring入门第一讲——Spring框架的快速入门

    Spring的概述 什么是Spring xff1f 我们可以从度娘上看到这样有关Spring的介绍 xff1a 说得更加详细一点 xff0c Spring是一个开源框架 xff0c Spring是于2003年兴起的一个轻量级的Java开发框
  • 手动清理RabbitMq队列中的消息

    一 手动删除队列中指定个数的消息 打开RabbitMq管理页面 xff0c 进入队列 点击 Get messages Requeue 改成No Mesaages 设置一个值 点击Get messages 二 一次清理队列中的所有消息 打开R
  • 关于Spring核心配置文件中的各项主要配置

    1 xff1a Spring的核心配置文件中的各种配置 spring的核心配置文件的名字 叫做 applicationContext xml xff0c 后期也可以通过配置文件修改名称 xff0c 在web xml中进行如下修改 xff1a
  • JVM内存设置

    对于jvm的设置我一直知道的很少 xff0c 有时候遇到outOfMemoryError只会一个简单的设置 Xms256m Xmx512m 有几个问题一直没搞明白 xff1a 1 jvm的内存大小究竟应该设置成多少最合适 xff1f 2 j
  • 闭包,一个浪漫的故事

    闭包 xff0c 一个浪漫的故事 前 今天稍微说一下js中的闭包 xff0c 这是个新手很少遇到的老手不常遇到的问题 xff0c 他的出现主要是为了帮我们规避问题 xff0c 另外提供了一种解决问题的新途径 xff0c 这里先说明一点啊 x
  • vue中集成jsplumb报错`Cannot read property ‘parentNode‘ of null`,且无法渲染连线的问题

    简介 我在项目中使用了typescript 43 vue 43 jsplumb作为流程图框架 问题 我在容器中使用的是v for指令渲染 从配置文件中读取流程图的json并且存入this items变量中通过v for指令 xff0c 调用
  • [webpack-cli] Invalid options object. Dev Server has been initialized using an options object

    问题内容 xff1a webpack cli Invalid options object Dev Server has been initialized using an options object that does not matc
  • 使用idea创建web项目

    前言 xff1a 很高兴能够用自己所学知识为你提供答疑 xff01 xff01 xff01 今天我就来操作下如何使用idea这款软件创建web项目 步骤 xff1a 1 创建项目 首先新建一个项目 然后选择最后一个 xff0c 创建一个空白
  • 基于若依开发管理项目中引入工作流引擎activiti7,包含前后端(原创)

    原项目中用到了工作流引擎 xff0c 使用若依框架开发 xff0c 原二开使用项目 xff1a https gitee com y project RuoYi Vue 基于activiti7地址 xff1a https gitee com