一个开源的汽修rbac后台管理系统项目,基于若依框架,实现了activiti工作流,附源码

news/2024/7/10 2:02:47 标签: 开源, spring boot, 后端, vue, Activiti

文章目录

    • 前言&源码
    • 项目参考图:
  • e店邦O2O平台项目总结
    • 一、springboot
        • 1.1、springboot自动配置原理
        • 1.2、springboot优缺点
        • 1.3、springboot注解
    • 二、rbac
        • 2.1、概括
        • 2.2、三个元素的理解
    • 三、数据字典
        • 3.1、概括与作用
        • 3.2、怎么设计
        • 3.3、若依中使用字典
    • 四、工作流——Activiti7
        • 4.1、概念
        • 4.2、如何使⽤
        • 4.3、优点缺点
        • 4.4、常用操作步骤
          • 【Deployment】 (创建并部署一个新的流程定义)
          • 【ProcessDefinition】 (查询流程定义对象)
          • 【ProcessInstance】 (查询流程实例对象)
          • 【Task】 (查询任务信息)
          • 【HistoricActivityInstance】 (查询历史活动实例信息)
          • 【Execution】(查询执行流数据)
          • 【IdentityLink】(查询身份与流程数据的绑定关系)
        • 4.5、项目怎么用,怎么设计表
          • 4.5.1、流程定义明细模块:
          • 4.5.2、发起/提交审核模块:
          • 4.5.3、套餐审核信息模块:
          • 4.5.4、我的待办、我的已办模块:
        • 4.6、思考总结:
    • 五、若依脚手架
        • 5.1、概念
        • 5.2、如何快速掌握脚手架
        • 5.3、如何通过脚手架快速复制出一个curd操作流程
        • 5.4、其他
    • 六、项目——操作
        • 6.1、开发意识
        • 6.2、修改bug能力
    • 七、项目——技术上
        • 7.1、基础:
        • 7.2、拓展:

前言&源码

为了更加熟悉activiti工作流的使用和实战而改造的项目,欢迎大家参考和提出问题建议一起学习~

源码gitee仓库地址:Yuzaki-NASA / Activiti7_test_car_rbac
master分支是稳定版,dev分支是后来加了个新的并行审核流程和客户管理,个人测了多遍没啥问题,建议拉dev的代码。
sql文件在caro2o-business下的resources/sql里,启动项目前记得先添加一下sql

原模板源码gitee仓库地址:Yuzaki-NASA / activiti7-caro2o-template
这个是参考的模板,功能除去一些被我优化过的地方以外大多一致,还写了很多注释,便于对照理解学习

项目参考图:

  • 养修预约-服务项crud(很多页面都类似,就不一一例举了)

在这里插入图片描述

  • 养修预约-结算单明细

在这里插入图片描述

  • 流程管理-流程定义明细

在这里插入图片描述

  • 套餐审核-我的已办

在这里插入图片描述

  • 套餐审核-我的待办-进度查看

在这里插入图片描述


项目总结:
(源码中的caro2o-ui下的src/assets路径下也有总结的pdf文件)

e店邦O2O平台项目总结

一、springboot

1.1、springboot自动配置原理

用自己的大白话来总结就是:

自动配置简单来说就是自动去把第三方组件的Bean装载到IOC容器中,不需要开发人员再去写Bean相关的配置。在SpringBoot应用里只需要在启动类上加@SpringBootApplication注解就可以实现自动配置。
@SpringBootApplication注解是一个复合注解,真正去实现自动配置的注解是它里面的@EnableAutoConfiguration这样一个注解。自动配置的实现主要依靠三个核心的关键技术:

①、第一个,引入Starter

在这里插入图片描述

启动依赖组件的时候,这个组件里必须要包含一个@Configuration配置类,而在这个配置类里面我们需要通过@Bean这个注解去声明需要装配到IOC容器里面的Bean对象。

②、第二个,这个配置类是放在第三方的jar包里面,然后通过SpringBoot中约定优于配置的这样一个理念,使用Spring里拥有的SpringFactoriesLoader(Spring的一种加载方式,在Spring的底层非常常见)去把这个配置类的全限定名(路径)放在classpath:/META-INF/spring.factories文件里面,这样SpringBoot就可以知道第三方jar包里面这个配置类的位置。

约定优于配置理念:
维基百科解释如下:
约定优于配置(convention over configuration),也称作按约定编程,是一种软件设计范式,旨在减少软件开发人员需做出决定的数量,活得简单的好处,而又不失灵活性。
本质上是说,开发人员仅需要规定应用中不符约定的部分,例如,如果模型中有个名为 Sale 的类,那么数据库中对应的表就会默认命名为 sales。只有偏离这一约定时,例如将该表命名为“products_sold”,才需写有关这个名字的配置。
如果您所用工具的约定与你的期望相符,便可省去配置;反之,你可以配置来达到你所期待的方式。

/META-INF/spring.factories文件以key-value键值对作为内容,其中有一个Key为EnableAutoConfiguration且Value为各个第三方jar包的Configuration全限定名,而@EnableAutoConfiguration注解就是通过这里自动加载到所有符合要求的第三方依赖。例如我们项目中用到的Avitiviti依赖包

在这里插入图片描述

③、第三个,SpringBoot拿到所有第三方jar包里面声明的配置类以后,再通过Spring提供的ImportSelector这样一个接口来实现对这些配置类的动态加载,从而去完成自动配置这样一个动作。

在我看来,Springboot是约定优于配置这一理念下的一个产物,所以在很多地方都能看到这一类的思想,它的出现让开发人员可以更加聚焦(集中注意)在业务代码的编写上,而不需要去关心和业务无关的配置。

拓展:其实自动配置的思想在SpringFramework3.x版本里面的@Enable注解就已经有了实现的一个雏形,@Enable注解是一个模块驱动的意思,也就是说我们只需要增加@Enable注解就能自动打开某个功能,而不需要针对这个功能去做Bean的配置,@Enable注解的底层也是去帮我们自动去完成这样一个模块相关Bean的注入的,然后基于这一理念有了后来的SpringBoot自动配置。

img

1.2、springboot优缺点

优点:

  1. 创建独立Spring应用
  2. 内嵌web服务器(如tomcat等)
  3. 自动starter依赖,简化构建配置
  4. 自动配置Spring以及第三方功能
  5. 提供生产级别的监控、健康检查以及外部优化配置
  6. 无代码生成、无需编写XML

缺点:

  1. 迭代快
  2. 封装太深,内部原理复杂,不容易精通
1.3、springboot注解

在这里插入图片描述

springboot常见注解可以参考这个:https://zhuanlan.zhihu.com/p/593053050?utm_id=0

来说一下caro2o项目中一些比较常用和重要的注解:

  • @RestController:

    • @RestController是@Controller和 @ResponseBody 的结合体,两个标注合并起来的作用。@RestController类中的所有方法只能返回String、Object、Json等实体对象,不能跳转到模版页面。
    • 如果只是使用@RestController注解Controller,则Controller中的方法无法返回jsp页面,或者html,配置的视图解析器 InternalResourceViewResolver不起作用,返回的内容就是Return 里的内容。
    • 如果需要返回到指定页面,则需要用 @Controller配合视图解析器InternalResourceViewResolver才行。如果需要返回JSON,XML或自定义mediaType内容到页面,则需要在对应的方法上加上@ResponseBody注解。
  • @PathVariable:

    • @PathVariable 映射 URL 绑定的占位符,通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx”) 绑定到操作方法的入参中。单个变量或数组都可以。

    •     /**
           * 获取养修信息预约详细信息
           */
          @PreAuthorize("@ss.hasPermi('business:appointment:query')")
          @GetMapping(value = "/{id}")
          public AjaxResult getInfo(@PathVariable("id") Long id)
          {
              return AjaxResult.success(busAppointmentService.selectBusAppointmentById(id));
          }
      
          /**
           * 删除养修信息预约(真删除)
           */
          @PreAuthorize("@ss.hasPermi('business:appointment:remove')")
          @Log(title = "养修信息预约", businessType = BusinessType.DELETE)
      	@DeleteMapping("/{ids}")
          public AjaxResult remove(@PathVariable Long[] ids)
          {
              return toAjax(busAppointmentService.deleteBusAppointmentByIds(ids));
          }
      
          /**
           * 删除养修信息预约(假删除)
           */
          @PreAuthorize("@ss.hasPermi('business:appointment:remove')")
          @Log(title = "删除养修信息预约", businessType = BusinessType.UPDATE)
          @PutMapping("/delete/{ids}")
          public AjaxResult updateDel(@PathVariable Long[] ids)
          {
              busAppointmentService.updateDel(ids);
              return AjaxResult.success();
          }
      
          /**
           * 删除养修信息预约(假删除)
           */
          @PreAuthorize("@ss.hasPermi('business:appointment:generate')")
          @Log(title = "养修信息预约", businessType = BusinessType.INSERT)
          @PostMapping("/generate/{appointmentId}")
          public AjaxResult generate(@PathVariable Long appointmentId)
          {
              return AjaxResult.success(busAppointmentService.generate(appointmentId));
          }
      
  • @RequestBody:Controller中接收的入参是对象的Json格式时贴,下面代码块中的POST和PUT方法都有用到,不多赘述了。

  • @Validated:是Spring Validation框架提供的参数验证功能,贴在controller类里方法的入参前开启参数校验功能,比较常贴在POST和PUT方法上:

    •     /**
           * 新增养修信息预约
           */
          @PreAuthorize("@ss.hasPermi('business:appointment:add')")
          @Log(title = "养修信息预约", businessType = BusinessType.INSERT)
          @PostMapping
          public AjaxResult add(@Validated @RequestBody BusAppointment busAppointment)
          {
              return toAjax(busAppointmentService.insertBusAppointment(busAppointment));
          }
      
          /**
           * 修改养修信息预约
           */
          @PreAuthorize("@ss.hasPermi('business:appointment:edit')")
          @Log(title = "养修信息预约", businessType = BusinessType.UPDATE)
          @PutMapping
          public AjaxResult edit(@Validated @RequestBody BusAppointment busAppointment)
          {
              return toAjax(busAppointmentService.updateBusAppointment(busAppointment));
          }
      
    • 在domain的类里的get方法上贴相关校验注解,如@NotBlank(贴在字符串成员上,表示不能为空或空字符串)、@NotNull(不能为Null)、@Size(限制字符串长度)等等

      •  	@NotBlank(message = "客户姓名不能为空")
            @Size(min = 0, max = 64, message = "客户姓名长度不能超过64个字符")
            public String getCustomerName() 
            {
                return customerName;
            }
            public void setCustomerPhone(String customerPhone) 
            {
                this.customerPhone = customerPhone;
            }
        
        @NotNull(message = "预约时间不能为空")
        public Date getAppointmentTime() 
        {
            return appointmentTime;
        }
        public void setActualArrivalTime(Date actualArrivalTime) 
        {
            this.actualArrivalTime = actualArrivalTime;
        }
        
        
        
  • @JsonFormat:在Jackson中定义的一个注解,是一个时间格式化注解。此注解用于属性上,作用是把DATE类型的数据转化成为我们想要的格式。

    • // 例如
      @JsonFormat(pattern = "yyyy-MM-dd")
      @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
      @JsonFormat(pattern = "yyyy年MM月dd日 HH时mm分ss秒")
      
  • @Param:在Mapper类中使用,这个注解是为SQL语句中参数赋值而服务的。

    • @Param的作用就是给参数命名,比如在mapper里面某方法A(int id),当添加注解后A(@Param(“userId”) int id),也就是说外部想要取出传入的id值,只需要取它的参数名userId就可以了。将参数值传如SQL语句中,通过#{userId}进行取值给SQL的参数赋值。
    • Mapper类中的方法参数不为基本数据类型或者有多个参数时,使用该注解。
  • @Transactional:在service层开启事务,防止执行途中出错而造成的数据混乱。

  • 其余还有一些若依自己封装的注解类似@PreAuthorize(权限)、@Log(打印日志)、@Excel(导出文件相关注解)就不展开说明了,不同项目会封装不同的自定义注解,这些都需要自己去研究其作用与实现。

二、rbac

2.1、概括

RBAC是一种基于角色实现访问控制的权限管理机制,通过定义角色和权限、用户和角色、角色和角色之间的关系,实现多层次、细粒度、可复用的权限管理系统。

基本模型有三个元素:用户、角色和权限。模型设计基于“多对多”原则,即多个用户可以具有相同的角色,一个用户可以具有多个角色。同样,您可以将同一权限分配给多个角色,也可以将同一角色分配给多个权限。

更详细解释见:https://zhuanlan.zhihu.com/p/513142061

2.2、三个元素的理解

用户信息需要确保安全性,不能泄露。

角色关系到用户和权限,需要设计合理。

权限字段应在前端与后端都有校验:前端通过菜单或按钮的显示与否体现对不同角色权限的控制,但前端可能会被用户恶意修改视图去显示出因没有权限而过滤掉的功能菜单或按钮,此时在后端也要增加权限校验,在该用户没拥有该权限时,发起的请求返回403错误,弹框提示该用户缺少对应权限。

三、数据字典

3.1、概括与作用

数据字典是整个平台中数据描述的有效机制。通过界面进行可视化的操作和维护,能快速录入和修改平台上统一的字典数据。有效提高了数据的重复利用率和产品、项目的开发效率。整个数据字典数据为框架平台所共享,用户可以更好地对系统进行自定义管理,以满足自己的个性化需求。

3.2、怎么设计

参考:https://www.python100.com/html/82651.html

3.3、若依中使用字典

1、js中引入方法

import { getDicts } from "@/api/system/dict/data";

2、加载数据字典

export default {
  data() {
    return {
      xxxxxOptions: [],
      .....
...

created() {
  this.getDicts("字典类型").then(response => {
    this.xxxxxOptions = response.data;
  });
},

3、读取数据字典

<uni-data-select
  v-for="dict in xxxxxOptions"
  :key="dict.dictValue"
  :text="dict.dictLabel"
  :value="dict.dictValue"
/>

Activiti7_322">四、工作流——Activiti7

4.1、概念

没有⼯作流引擎之前如果要控制业务流程我们可能通过改变某个字段的状态来实现,这会带来⼀旦我们流程发⽣变化的时候我们就需要去同步修改代码。⽽⽤流程引擎它⾥⾯内置可25张表,我们只要读取它⾥⾯的表就可以了,与表对应的它还提供⼀系列可操作表的接⼝。核⼼⼀个类是ProcessEngine,通过它能获取⼀系列的service接⼝。

4.2、如何使⽤

部署⼯作流引擎,其实就是jar包api

流程定义:.bpmn⽂件,是⼀个xml⽂件定义了流程信息

流程定义部署

启动⼀个流程实例

⽤户查询代办任务,⼀个instance有多个task

⽤户办理任务

流程结束

4.3、优点缺点

优点
1、 最大的优点就是免费开源,这也是很多人选择的原因
2、 小项目中应用简单的串行并行流转基本能满足需求。
缺点
1、节点定义概念不同
2、缺乏可“追溯”性
3、扩展需要与很多的Event来实现
4、二次开发难度大,门槛高

4.4、常用操作步骤
【Deployment】 (创建并部署一个新的流程定义)

获取方式:
repositoryService.createDeployment().deploy();
对应的表:act_re_deployment

用于存储流程部署的相关信息。该表记录了每个流程部署的唯一标识符(ID)、名称(NAME)、类别(CATEGORY)、租户标识符(TENANT_ID)、键(KEY)以及部署时间(DEPLOY_TIME)等信息。

核心字段:
id、name、deployementTime、category、key、tenantid


【ProcessDefinition】 (查询流程定义对象)

获取方式:
repositoryService.createProcessDefinitionQuery()
.deploymentId(“流程部署id”)
.processDefinitionId(“流程定义id”)
.processDefinitionKey(“流程定义的key”)
.processDefinitionName(“流程定义的name”)
.singleResult();
对应的表:act_re_procdef

用于存储流程定义的相关信息。该表记录了每个流程定义的ID、名称、版本号、资源文件和图片文件等信息。

通过查询act_re_procdef表,您可以获得以下信息:

  • 流程定义ID(id):这是每个流程定义的唯一标识符。
  • 流程定义名称(name):这是流程定义的名称。
  • 版本号(version):这是流程定义的版本号。
  • 资源文件(resource_name):这是与流程定义关联的资源文件名称。
  • 图片文件(image_name):这是与流程定义关联的图片文件名称。

act_re_procdef表与act_ge_bytearray表之间存在多对一的关系,即一个流程定义对应多个资源文件和图片文件。在Activiti中,每个流程定义都会在act_re_procdef表中增加一条记录,同时也会在act_ge_bytearray表中存在相应的资源记录。

通过查询act_re_procdef表,您可以了解流程定义的相关信息,包括其名称、版本号以及与之关联的资源文件和图片文件。这对于管理和维护业务流程非常有用。

核心字段:
id、name、key、description、resourceName、deploymentId、tenantId、engineVersion


【ProcessInstance】 (查询流程实例对象)

获取方式:
方式1:runtimeService.startProcessInstanceByKey(processDefinitionKey);
方式2:
runtimeService.createProcessInstanceQuery()
.processInstanceId(“流程实例id”)
.processDefinitionId(“流程定义id”)
.processDefinitionKey(“流程定义的key”)
.deploymentId(“流程部署id”)
.processDefinitionName(“流程定义的name”)
.processInstanceBusinessKey(“流程实例业务key”)
.singleResult();

对应的表:act_hi_procinst

用于存储流程实例的历史信息。该表记录了每个流程实例的ID、名称、业务键、状态以及相关的其他信息。

通过查询act_hi_procinst表,您可以获得以下信息:

  • 流程实例ID(proc_id):这是每个流程实例的唯一标识符。
  • 流程实例名称(proc_name):这是流程实例的名称。
  • 业务键(business_key):这是与流程实例关联的业务键,通常用于标识业务流程的唯一性。
  • 状态(state):这是流程实例的状态,例如已启动、已完成、已暂停等。
  • 其他相关信息:act_hi_procinst表还包含其他与流程实例相关的信息,例如创建时间、更新时间、所属组织等。

通过查询act_hi_procinst表,您可以了解流程实例的历史记录,包括其状态变化、执行路径以及相关的其他信息。这对于分析和优化业务流程非常有用。

核心字段:
name、businessKey、deploymentId、descriptionName、processDefinitionId、processDefinitionKey、processDefinitionName、startTime、startTimeUser、tenantId、activityId、 processInstanceId


【Task】 (查询任务信息)

获取方式:
taskService.createTaskQuery()
.taskId(“taskId”)
.taskAssignee(“节点任务负责人”)
.taskCandidateUser(“taskCandidateUser”)
.taskDefinitionKey(“taskDefinitionKey”)
.processDefinitionKey(“流程定义的key”)
.processInstanceId(“流程实例id”)
.deploymentId(“流程部署id”)
.singleResult();
对应的表:act_ru_task

用于存储正在执行的任务信息。该表记录了每个任务的ID、名称、状态、执行路径等信息。

通过查询act_ru_task表,您可以获得以下信息:

  • 任务ID(task_id):这是每个任务的唯一标识符。
  • 任务名称(name):这是任务的名称。
  • 任务状态(status):这是任务的状态,例如待办、已完成、正在进行中等。
  • 执行路径(execution_id):这是与任务关联的流程实例的执行路径信息。
  • 其他相关信息:act_ru_task表还包含其他与任务相关的信息,例如创建时间、更新时间、任务节点类型等。

act_ru_task表与act_ge_bytearray表之间存在多对一的关系,即一个任务对应多个附件文件。在Activiti中,每个任务都会在act_ru_task表中增加一条记录,同时也会在act_ge_bytearray表中存在相应的附件记录。

通过查询act_ru_task表,您可以了解正在执行的任务的相关信息,包括其ID、名称、状态以及执行路径等。这对于跟踪和管理业务流程中的任务非常有用。

核心字段:
name、description、priority、owner、assignee、delegationState、formKey、parentTaskId、
processInstanceId、executionId、processDefinitionId、processVariables


【HistoricActivityInstance】 (查询历史活动实例信息)

获取方式:
historyService.createHistoricActivityInstanceQuery()
.processDefinitionId(“流程定义id”)
.taskAssignee(“节点任务负责人”)
.processInstanceId(“流程实例id”)
.singleResult();
对应的表:act_hi_actinst

是一个历史节点表,用于存储历史流程实例的信息。该表记录了每个历史流程实例的ID、名称、业务键、状态以及相关的其他信息,包括开始时间、结束时间等。通过查询 act_hi_actinst 表,您可以了解已经执行过的流程实例的历史记录,例如流程的执行路径、各个节点的执行时间等信息。这对于分析和优化业务流程非常有用,可以帮助企业更好地了解业务流程的执行情况,从而进行改进和优化。

核心字段:
id、activityId、activityName、activityType、processDefinitionId、processInstanceId、executionId、taskId、assignee、startTime、endTime、durationInMilli、tenantId


【Execution】(查询执行流数据)

获取方式:
runtimeService.createExecutionQuery()
.processDefinitionKey(“流程定义的key”)
.executionId(“executionId”)
.processDefinitionId(“流程定义id”)
.processInstanceId(“流程实例id”)
.processDefinitionKey(“流程定义的key”)
.singleResult();
对应的表:act_ru_execution

是存储运行时数据的表,主要包含执行过程中的活动、任务、变量等数据。该表记录了每个流程实例的执行路径信息,例如当前执行到哪个流程节点、哪些分支已经被激活等。通过查询 act_ru_execution 表,可以获取流程实例的实时运行状态信息,例如哪个任务正在由哪个用户执行、执行到哪个节点等。这对于跟踪和管理业务流程中的实例非常有用。

核心字段:
id、activityId、processInstanceId、name、description


【IdentityLink】(查询身份与流程数据的绑定关系)

获取方式:
方式1:runtimeService.getIdentityLinksForProcessInstance(processInstanceId)
方式2:repositoryService.getIdentityLinksForProcessDefinition(ProcessDefinitionId)
方式3:taskService.getIdentityLinksForTask(taskId)
对应的表:act_ru_identitylink

存储了用户或用户组与流程数据之间的绑定关系。该表记录了用户或用户组与流程实例、流程任务等数据的关联信息。通过查询 act_ru_identitylink 表,可以获取用户或用户组与流程实例、流程任务等数据的绑定关系,例如哪个用户或用户组执行了哪个流程任务、哪些流程任务被指定给了哪些用户或用户组等。这对于了解业务流程的执行情况、进行权限管理和任务分配等操作非常有用。

核心字段:
type、userId、taskId、processDefinitionId、processInstanceId

4.5、项目怎么用,怎么设计表
4.5.1、流程定义明细模块:
  1. 设置一个流程管理模块,数据库创建一张流程定义明细表bus_bpmn_info和与之对应的查询页面,表中要有processKey和version这两个字段,在该页面增加一个流程文件部署功能,需要选择审核类型、上传bpmn流程文件、添加备注(描述信息,可不填),然后通过repositoryService服务的deploy部署一个新流程,部署后就可以在act_re_procdef表里查到刚才部署的流程定义了。将流程定义的所需信息存放到我们自己建的bpmnInfo流程定义明细表中,在查询页面显示出来我们新建过的流程定义。

        @Override
        public void deploy(DeployVO vo) throws IOException {
            //参数判断--文件大小--文件后缀
            if(vo == null){
                throw new ServiceException("参数异常");
            }
            MultipartFile file = vo.getFile();
            if(file == null || file.getSize() == 0){
                throw new ServiceException("必需选择一个流程文件");
            }
            String ext = FileUploadUtils.getExtension(file);
            if(!"bpmn".equalsIgnoreCase(ext)){
                throw new ServiceException("文件格式必须为 bpmn 格式");
            }
            //流程部署
            Deployment deployment = repositoryService.createDeployment()
                    .addInputStream(vo.getBpmnLabel(), file.getInputStream())
                    .deploy();
            //流程类-解下流程文件, 获取流程文件所有信息封装对象-ProcessDefinition---act_re_procdef
            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                    .deploymentId(deployment.getId()).singleResult();
            //保存流程信息对象: BpmnInfo
            BpmnInfo bpmnInfo = new BpmnInfo();
            bpmnInfo.setInfo(vo.getInfo());
            bpmnInfo.setBpmnLabel(vo.getBpmnLabel());
            bpmnInfo.setBpmnType(vo.getBpmnType());
    
            bpmnInfo.setDeployTime(deployment.getDeploymentTime());
            bpmnInfo.setVersion(processDefinition.getVersion());
            bpmnInfo.setProcessDefinitionKey(processDefinition.getKey());
    
            bpmnInfoMapper.insertBpmnInfo(bpmnInfo);
    
        }
    
  2. 在流程定义明细页面中可以查看流程文件或流程图,具体实现代码:

        @Override
        public InputStream getResource(String type, Long id) {
            BpmnInfo bpmnInfo = bpmnInfoMapper.selectBpmnInfoById(id);
    
            if (bpmnInfo==null||!("xml".equalsIgnoreCase(type)||"png".equalsIgnoreCase(type))) {
                throw new ServiceException("参数异常或文件格式异常");
            }
    
            InputStream inputStream = null;
    
            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                    .processDefinitionKey(bpmnInfo.getProcessDefinitionKey())
                    .processDefinitionVersion(bpmnInfo.getVersion())
                    .singleResult();
            if("xml".equalsIgnoreCase(type)){
                inputStream = repositoryService
                        .getResourceAsStream(processDefinition.getDeploymentId(), bpmnInfo.getBpmnLabel());
            }else if("png".equalsIgnoreCase(type)){
                DefaultProcessDiagramGenerator processDiagramGenerator = new DefaultProcessDiagramGenerator();
                BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
                /**
                 * 第一个参数: 流程定义模型
                 * 第二个参数: 高亮节点集合
                 * 第三个参数: 高亮连线集合
                 */
                inputStream = processDiagramGenerator.generateDiagram(bpmnModel,
                        Collections.emptyList(),
                        Collections.emptyList(),
                        "宋体",
                        "宋体",
                        "宋体");
            }
            return inputStream;
        }
    
  3. 流程定义的撤销:

        /**
         * 批量撤销流程定义明细
         *
         * @param ids 需要删除的流程定义明细主键
         * @return 结果
         */
        @Override
        public int deleteBpmnInfoByIds(Long[] ids) {
            if (ids==null||ids.length<1) {
                throw new ServiceException("参数异常");
            }
    
            for (Long id : ids) {
                BpmnInfo bpmnInfo = bpmnInfoMapper.selectBpmnInfoById(id);
                if (bpmnInfo==null) {
                    throw new ServiceException("参数异常");
                }
    
                ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                        .processDefinitionKey(bpmnInfo.getProcessDefinitionKey())
                        .processDefinitionVersion(bpmnInfo.getVersion())
                        .singleResult();
    
                if (processDefinition==null) {
                    throw new ServiceException("存在撤销项参数异常");
                }
    
                repositoryService.deleteDeployment(processDefinition.getDeploymentId(),true);
    
                bpmnInfoMapper.deleteBpmnInfoById(id);
            }
            return 1;
        }
    
4.5.2、发起/提交审核模块:

​ 发起审核的弹框里需要用户从前端传入所有所需的参数,如审核人等。并且前端和后端都要添加状态判断——该业务处在什么状态下才允许发起审核、该业务的某些条件是否影响审核节点等。

	@Transactional
    @Override
    public void startAudit(AuditVO vo) {

        //参数判断
        ServiceItem serviceItem = serviceItemMapper.selectServiceItemById(vo.getId());
        if(serviceItem == null){
            throw new ServiceException("参数异常");
        }

        if(!ServiceItem.CARPACKAGE_YES.equals(serviceItem.getCarPackage())){
            throw new ServiceException("必须是套餐才允许审核");
        }
        if(!(ServiceItem.AUDITSTATUS_INIT.equals(serviceItem.getAuditStatus())
                || ServiceItem.AUDITSTATUS_REPLY.equals(serviceItem.getAuditStatus()))){
            throw new ServiceException("必须是初始化或者审核拒绝状态才可以进行审核");
        }
        //审核信息保存
        CarPackageAudit audit = new CarPackageAudit();

        audit.setInfo(vo.getInfo());
        audit.setServiceItemId(vo.getId());
        audit.setServiceItemName(serviceItem.getName());
        audit.setServiceItemInfo(serviceItem.getInfo());
        audit.setServiceItemPrice(serviceItem.getDiscountPrice());
        audit.setCreatorId(SecurityUtils.getUserId().toString());
        audit.setStatus(CarPackageAudit.STATUS_IN_ROGRESS);
        audit.setCreateTime(new Date());
        carPackageAuditMapper.insertCarPackageAudit(audit);

        BpmnInfo bpmnInfo = bpmnInfoMapper.selectLastByType(CarPackageAudit.FLOW_AUDIT_TYPE);
        //流程启动---businesskey   map(审核流程涉及参数)

        String businessKey = audit.getId().toString();
        String processDefinitionKey = bpmnInfo.getProcessDefinitionKey();
        Map<String, Object> map = new HashMap<>();

        //设置节点审核人:财务
        if(vo.getFinanceId() != null){
            map.put("financeId", vo.getFinanceId());
        }
        //设置节点审核人:店长
        if(vo.getShopOwnerId() != null){
            map.put("shopOwnerId", vo.getShopOwnerId());
        }
        // 流程图中不支持BigDecimal 校验, 转换long类型
        map.put("disCountPrice", serviceItem.getDiscountPrice().longValue());

        ProcessInstance instance = runtimeService.startProcessInstanceByKey(processDefinitionKey, businessKey, map);

        //audit.setInstanceId(instance.getProcessInstanceId());  //流程实例id
        audit.setInstanceId(instance.getId());  //流程实例id
        carPackageAuditMapper.updateCarPackageAudit(audit);

        //套餐项状态--审核中
        serviceItem.setAuditStatus(ServiceItem.AUDITSTATUS_AUDITING);
        serviceItemMapper.updateServiceItem(serviceItem);

    }
4.5.3、套餐审核信息模块:
  1. 每一个开启审核的业务对应一个执行的流程实例,我们要创建一个业务表bus_car_package_audit,表中要拥有关联服务项表的字段service_item_id(为了页面回显效果也可以将name、info、price字段加上)、关联流程实例的字段instance_id,还可以将关联流程定义的字段process_key也加上,还有一些状态status和创建者id和创建时间create_time等。

    CREATE TABLE `bus_car_package_audit` (
      `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
      `service_item_id` bigint DEFAULT NULL COMMENT '服务单项id',
      `service_item_name` varchar(100) DEFAULT NULL COMMENT '服务项名称',
      `service_item_info` varchar(255) DEFAULT NULL COMMENT '服务单项备注',
      `service_item_price` decimal(10,2) DEFAULT NULL COMMENT '服务单项审核价格',
      `instance_id` varchar(64) DEFAULT NULL COMMENT '流程实例id',
      `creator_id` varchar(20) DEFAULT NULL COMMENT '创建者',
      `info` varchar(255) DEFAULT NULL COMMENT '备注',
      `status` int DEFAULT NULL COMMENT '状态【审核中0/审核拒绝1/审核通过2/审核撤销3】',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb3 COMMENT='套餐审核';
    
  2. 审核历史按钮功能:查看该条审核的审批操作历史。使用对应的流程实例id通过historyService.createHistoricTaskInstanceQuery()可查询到该实例的每一条审批节点记录,

        @Override
        public List<HistoryVO> queryHistory(Long instanceId) {
    
            if(instanceId == null){
                throw new ServiceException("参数异常");
            }
            BpmnInfo bpmnInfo = bpmnInfoMapper.selectLastByType(CarPackageAudit.FLOW_AUDIT_TYPE);
    
            //原生的activit7返回domain对象 不一定满足页面的要求, 所以:一般将元素activiti对象进行二次加工
            List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery()
                    .processInstanceId(instanceId.toString())  //
                    .processDefinitionKey(bpmnInfo.getProcessDefinitionKey())  //套餐审核节点
                    .finished()  //要求节点执行审核操作
                    .list();
            
            //思考: 怎么查询历史??
            List<HistoryVO> vos = new ArrayList<>();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            for (HistoricTaskInstance task : list) {
                HistoryVO vo = new HistoryVO();
                vo.setTaskName(task.getName());
                // 将Date类型转成对应格式的String
                vo.setEndTime(sdf.format(task.getEndTime()));
                vo.setStartTime(sdf.format(task.getStartTime()));
                //间隔时间: 花费时间:  endTime-startTime
                // 格式是 毫秒 ---> xx年 xx天 xxx月xxx日 xx时
                vo.setDurationInMillis(task.getDurationInMillis() / 1000 + "s");
                //审核备注
                //查询节点审核备注信息?
                //由于可能存在并行网关,有多条审核备注,所以要拼接在一起
                List<Comment> comments = taskService.getTaskComments(task.getId());
                if(comments != null || comments.size() > 0){
                    StringBuilder sb = new StringBuilder(80);
                    for (Comment comment : comments) {
                        //节点备注信息
                        sb.append(comment.getFullMessage());
                    }
                    vo.setComment(sb.toString());
                }
                vos.add(vo);
            }
            return vos;
        }
    
  3. 进度查看按钮功能:查看流程进行到哪,在流程图png中将进行到的节点用红框高亮的方式显示出来。

        @Override
        public InputStream getProcessImg(Long id) {
            BpmnInfo bpmnInfo = bpmnInfoMapper.selectLastByType(CarPackageAudit.FLOW_AUDIT_TYPE);
            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                    .processDefinitionKey(bpmnInfo.getProcessDefinitionKey())
                    .processDefinitionVersion(bpmnInfo.getVersion())  //指定版本
                    .singleResult();//???
    
            CarPackageAudit audit = carPackageAuditMapper.selectCarPackageAuditById(id);
            List<String> activeActivityIds = new ArrayList<>();
            if(audit.getStatus().equals(CarPackageAudit.STATUS_IN_ROGRESS)){
                //高亮显示当前流程所在节点-坐标
                activeActivityIds = runtimeService.getActiveActivityIds(audit.getInstanceId());
            }else{
                activeActivityIds = Collections.emptyList();
            }
            //图片
            DefaultProcessDiagramGenerator processDiagramGenerator = new DefaultProcessDiagramGenerator();
            BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
            /**
             * 第一个参数: 流程定义模型
             * 第二个参数: 高亮节点集合---当前流程推进到哪个节点了---传的是节点坐标
             * 第三个参数: 高亮连线集合
             */
            InputStream inputStream = processDiagramGenerator.generateDiagram(bpmnModel,
                    activeActivityIds,
                    Collections.emptyList(),
                    "宋体",
                    "宋体",
                    "宋体");
            return inputStream;
        }
    
  4. 撤销审核按钮功能:顾名思义。先校验该流程状态是否允许被撤销,撤销时需要完成三个步骤——服务套餐状态置为初始化、审核信息记录状态置为审核撤销、将运行流程实例(关联到的几个表)执行撤销方法runtimeService.deleteProcessInstance()

        @Override
        public void auditCancel(Long id) {
            //参数校验
            CarPackageAudit audit = carPackageAuditMapper.selectCarPackageAuditById(id);
            if(audit == null){
                throw new ServiceException("参数异常");
            }
            if(!CarPackageAudit.STATUS_IN_ROGRESS.equals(audit.getStatus())){
                throw new ServiceException("只有在审核中状态才允许撤销操作");
            }
            //服务套餐--状态-初始化
            ServiceItem serviceItem = serviceItemMapper.selectServiceItemById(audit.getServiceItemId());
            serviceItem.setAuditStatus(ServiceItem.AUDITSTATUS_INIT);
            serviceItemMapper.updateServiceItem(serviceItem);
    
            //审核信息记录--状态--撤销
            audit.setStatus(CarPackageAudit.STATUS_CANCEL);
            carPackageAuditMapper.updateCarPackageAudit(audit);
    
            //流程--流程结束--删除
            runtimeService.deleteProcessInstance(audit.getInstanceId(), "审核被撤销了");
        }
    
4.5.4、我的待办、我的已办模块:
  1. 前端代码可以直接拷贝套餐审核信息模块的vue文件,因为查询的都是业务表bus_car_package_audit。不同的是该两个模块只负责流程的推动和审批,故没有撤销按钮功能,而我的待办模块会多一个“审批”功能,即分配给当前用户的审批流程可以通过该操作选择同意或拒绝来推动流程进行。

  2. 查询功能参数需要添加当前用户条件,因为只能查到当前登录用户自己负责的审核流程。若依有自带的SecurityUtils工具类获取当前登录用户的id、name等信息,再通过taskService.createTaskQuery().taskAssignee(SecurityUtils.getUserId().toString()).list();拿到当前尚在推动流程阶段(未结束)的用户自己负责的流程实例,获取到流程实例id,即可在业务表bus_car_package_audit查询到对应的审核业务数据。上述说的是我的待办模块,而在我的已办模块,只需将查询未结束的流程换成查询全部流程(从history表中查)即可,List list = historyService.createHistoricTaskInstanceQuery().taskAssignee(SecurityUtils.getUserId().toString()).list();

  3. 这里提供一个更为直观的多表联查方法:我们一开始就能用SecurityUtils拿到用户id,在对应的表通过ASSIGNEE_字段筛选出当前登录用户所负责的流程实例(待办则查act_ru_task表,已办则查act_hi_taskinst表),再通过拿到的流程实例id的字符串集合去业务表bus_car_package_audit获取到最终自己负责的业务数据。

    // mapper接口方法,注意因为比普通查询多了userId条件,所以需要加@Param注解给多个参数命名,传CarPackageAudit对象是为了页面上的条件查询,即通过审核状态与创建时间筛选数据。最后传的字符串tableName是查询的表名,因为待办与已办的sql只有一个表名之差,所以复用一下,在动态sql里使用${}替换字符串,因为不是通过参数传入的字段,所以不会有动态sql注入的风险。
    List<CarPackageAudit> selectHisByUserId(@Param("userId") Long userId, @Param("carPackageAudit") CarPackageAudit carPackageAudit, @Param("tableName") String tableName);
    
    	<!-- mapper.xml里的sql -->
        <select id="selectHisByUserId" resultMap="CarPackageAuditResult">
            select c.* from bus_car_package_audit c LEFT JOIN ${tableName} a ON a.PROC_INST_ID_ = c.instance_id
            <where>
                a.ASSIGNEE_ = #{userId}
                <if test="carPackageAudit.params.beginCreateTime != null and carPackageAudit.params.beginCreateTime != '' and carPackageAudit.params.endCreateTime != null and carPackageAudit.params.endCreateTime != ''">
                    and c.create_time between #{carPackageAudit.params.beginCreateTime} and #{carPackageAudit.params.endCreateTime}
                </if>
                <if test="carPackageAudit.status != null"> and c.status = #{carPackageAudit.status}</if>
            </where>
        </select>
    
    // service中的方法:
    // 已办
    List<CarPackageAudit> list = carPackageAuditMapper.selectHisByUserId(SecurityUtils.getUserId(),carPackageAudit,"act_ru_task");
    
    // 待办
    List<CarPackageAudit> list = carPackageAuditMapper.selectHisByUserId(SecurityUtils.getUserId(),carPackageAudit,"act_hi_taskinst");
    
    
  4. 我的已办中的审批功能:首先校验状态是否能进行审核。然后taskService.createTaskQuery().processInstanceId(audit.getInstanceId())查询任务,判断是否为null(因为若使用排他网关,可能其他人先一步审核通过了,若为null,则什么也不做直接return),然后根据是否审核通过添加备注信息:taskService.addComment(task.getId(), audit.getInstanceId().toString(), message); 然后新建一个map存放节点条件,key为条件字段的变量名value为布尔值(同意or拒绝),然后任务处理taskService.complete(task.getId(), map);。随后业务线推进,若审核通过,判断是否还有下一个节点:若有则什么也不做(等待流程到下个节点继续推动),若没有则代表当前流程正常结束,即可修改套餐状态和业务信息状态。若审核拒绝,则直接修改套餐状态和业务信息状态。

    @Override
        public void audit(PackageAuditVO vo) {
    
            //审核条件
            //id != null
            //状态 为审核中
            if(vo == null){
                throw new ServiceException("参数异常");
            }
            CarPackageAudit audit = carPackageAuditMapper.selectCarPackageAuditById(vo.getId());
            if(audit == null || !CarPackageAudit.STATUS_IN_ROGRESS.equals(audit.getStatus())){
                throw new ServiceException("参数异常或者状态异常");
            }
            //流程推进: 节点审核
            //查询任务
            Task task = taskService.createTaskQuery()
                    .processInstanceId(audit.getInstanceId())
                    .singleResult();
            if(task == null){
                return;
            }
            //审核备注
            String message = "";
            if(CarPackageAudit.STATUS_PASS.equals(vo.getAuditStatus())){
                //通过
                message = "审批人:" + SecurityUtils.getUsername() + "通过, 审核备注:[" + vo.getAuditInfo() + "]";
            }else{
                //拒绝
                message = "审批人:" + SecurityUtils.getUsername() + "拒绝, 审核备注:[" + vo.getAuditInfo() + "]";
            }
            taskService.addComment(task.getId(), audit.getInstanceId().toString(), message);
    
            Map<String, Object> map = new HashMap<>();
            map.put("shopOwner", CarPackageAudit.STATUS_PASS.equals(vo.getAuditStatus()));
            //处理
            taskService.complete(task.getId(), map);
    
    
            ServiceItem serviceItem = serviceItemMapper.selectServiceItemById(audit.getServiceItemId());
    
            //业务线推进
            if(CarPackageAudit.STATUS_PASS.equals(vo.getAuditStatus())){
                //审核通过
                Task nextTask = taskService.createTaskQuery()
                        .processInstanceId(audit.getInstanceId())
                        .singleResult();
                //判断是否有下一个节点
                if(nextTask == null){
                    //  没有: 当前流程正常结束
                    //       1:服务套餐--审核通过
                    serviceItem.setAuditStatus(ServiceItem.AUDITSTATUS_APPROVED);
                    serviceItemMapper.updateServiceItem(serviceItem);
                    //       2:审核流程信息--审核通过
                    audit.setStatus(CarPackageAudit.STATUS_PASS);
                    carPackageAuditMapper.updateCarPackageAudit(audit);
                }
                //有: 当前流程还在继续 -- 啥都不做
            }else {
                //审核拒绝
                //1:服务套餐--审核拒绝
                serviceItem.setAuditStatus(ServiceItem.AUDITSTATUS_REPLY);
                serviceItemMapper.updateServiceItem(serviceItem);
                //2:审核流程信息--审核拒绝
                audit.setStatus(CarPackageAudit.STATUS_REJECT);
                carPackageAuditMapper.updateCarPackageAudit(audit);
                //3:流程--流程正常结束
            }
        }
    
4.6、思考总结:
  • 熟悉activiti重要的那些表,及对应的service操作。

在这里插入图片描述

  • 设计业务表时想清楚需要哪些字段去关联哪些activiti的表和哪些其他业务,还有需要显示的数据,表设计得好则代码写起来就能简便很多。
  • 使用工作流实现某个功能时,从最后需要拿到的数据往前推,最后的数据有哪些对应的字段可以和哪些表关联,建立每张表的联系,最后联系到我们提供的参数数据,即可搭建好这座参数与返回值连接的桥梁,完成需求。

五、若依脚手架

5.1、概念

脚手架(scaffolding)指的是创建项目时,自动完成的创建初始文件等初始化工作。这些工作往往是每次新建工程都要进行的重复性工作。如创建Maven 项目时使用的原型(archetype)等。脚手架是一种由一些 model–view–controller 框架支持的技术,程序员可以在其中指定应用程序数据库的使用方式。

5.2、如何快速掌握脚手架
  • 看官方文档
  • 使用脚手架创建项目后看代码并实际上手操作
  • 问有经验的老前辈
5.3、如何通过脚手架快速复制出一个curd操作流程

有官方文档就按照官方文档操作一遍,没有就自己捣鼓或问别人。

5.4、其他

工作中很大概率不会使用若依这样的脚手架,则拿到脚手架后的改造就要自己操作了,注意哪些文件夹和类名要改,pom里依赖的坐标名、版本等,最后再全局替换一下需要替换的字段,启动项目看看有没有问题。最好是公司的脚手架已配置好初始化信息。

六、项目——操作

6.1、开发意识
  1. 产品是开发给用户用的,一些功能和需求多站在用户角度考虑,写代码前先整体想好该怎么开发,想得细致入微一些,考虑得周全一些,把要实现的步骤尽可能明细地列举出来,能画出业务流程原型图最好,觉得不合理或有更优方案的地方及时和经理或上级沟通交流,确定好最终方案,再开始开发,事半功倍。
  2. 设计一张表的时候,先把页面列表要什么字段加进去,其次再考虑每个字段可能要关联的其他字段(例如页面只显示用户姓名,但我们要把用户id的字段也加上,因为id才是可以唯一识别的主键),其次再考虑该表会关联到的其他表需要通过什么字段关联起来或者建立起什么关系,最后考虑该表的应用场景应该再加些什么字段去丰富它(经验积累,如创建时间create_time,状态status,软删除is_delete,创建人user_id等等)。考虑好每个字段用什么数据类型,是否要采用字典(常用于可选项较少的下拉框选项)。
  3. 添加时弹窗需要回显什么,如果是要封装的数据则在后端包装一个VO类去传,后端接收前端传来的入参需要包装时也同理。编辑和删除操作时多考虑除了对应的数据改变以外,其他数据和表的状态是否要一起改变,不要漏掉关联的逻辑。
  4. 写业务需求时还是要多多思考多多沟通,尽善尽美。
6.2、修改bug能力

后端

  • 先看抛出的异常类型,锁定bug产生的原因。通过报错信息定位到报错位置,仔细排查解决。
  • 看看是不是哪个注解漏贴了,哪里有可能造成空指针了,包import导入的对不对、是不是你要用的那个依赖的包。
  • mapper.xml里的sql先在sql工具中的查询页面运行一遍,没报错再粘贴过去。
  • debug模式打断点看执行时数据是否正常。

前端:

  • 多用console.log查看数据是否有问题。
  • 注意漏加this的问题
  • 异步数据没获取到的问题
  • 箭头函数造成的作用域问题,使得this指向有误,解决方法:在箭头函数外写let that = this,箭头函数中使用that来指向this。

七、项目——技术上

7.1、基础:
  • 使用脚手架添加菜单和生成代码时,务必注意模块名和路径这类敏感信息不要写错。善用数据字典。

  • 善用各种工具类,例如验证手机号和验证车牌号,能省很多事

    // 验证是否非法手机号
    boolean phoneLegal = RegexUtils.isPhoneLegal(busAppointment.getCustomerPhone());
    Assert.isTrue(phoneLegal, "非法手机号码");
    
    // 验证是否非法车牌号
    VehiclePlateNoUtil.VehiclePlateNoEnum vehiclePlateNo = VehiclePlateNoUtil.getVehiclePlateNo(busAppointment.getLicensePlate());
    Assert.notNull(vehiclePlateNo, "非法车牌号");
    
    // 获取当前登录用户信息
    Long userId = SecurityUtils.getUserId();
    String username = SecurityUtils.getUsername();
    
    // 将Date数据转成想要的格式的String字符串
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    vo.setEndTime(sdf.format(task.getEndTime()));
    vo.setStartTime(sdf.format(task.getStartTime()));
    
  • domain中有用到数据字典字段的类,在类里加上静态常量,避免手写出错或后期要修改时造成的各种麻烦

        public static final Integer FLOW_AUDIT_TYPE = 0;//服务套餐审核类型
        public static final Integer FLOW_PERSONAL_LEAVE = 1;//事假审核类型
        public static final Integer FLOW_SICK_LEAVE = 2;//病假审核类型
    
        public static final Integer STATUS_IN_ROGRESS = 0;//审核中
        public static final Integer STATUS_REJECT = 1;//审核拒绝(拒绝)
        public static final Integer STATUS_PASS = 2;//审核通过(同意)
        public static final Integer STATUS_CANCEL = 3;//审核撤销
    
        public static final Integer IS_DELETE_YES = 1; // 已删除
        public static final Integer IS_DELETE_NO = 0; // 未删除
    
  • 添加目录、二级菜单、菜单下的按钮时,若需要添加权限字段,则记得统一添加(前端v-hasPermi,后端controller的方法上@PreAuthorize(“@ss.hasPermi(‘business:appointment:add’)”),脚手架页面的菜单权限字段上),前端权限控制是否显示,后端权限控制当前用户是否有权执行该请求。

    	// 后端
        @PreAuthorize("@ss.hasPermi('business:appointment:add')")
        @Log(title = "新增养修信息预约", businessType = BusinessType.INSERT)
        @PostMapping
        public AjaxResult add(@Validated @RequestBody BusAppointment busAppointment)
        {
            return toAjax(busAppointmentService.insertBusAppointment(busAppointment));
        }
    
          <!-- 前端 -->
    	  <el-col :span="1.5">
            <el-button
              type="primary"
              plain
              icon="el-icon-plus"
              size="mini"
              @click="handleAdd"
              v-hasPermi="['business:appointment:add']"
              >新增</el-button
            >
          </el-col>
    

    // 页面:

    在这里插入图片描述

  • 写动态sql或条件查询语句时注意代码书写格式,批量操作的数组用where xxx in (xxx),时间范围用between,善用<include refid="xxx"/>

    	<sql id="selectBusStatementVo">
            select id,
                   customer_name,
                   ...
                   is_delete
            from bus_statement
        </sql>
    
        <select id="selectBusStatementList" parameterType="BusStatement" resultMap="BusStatementResult">
            <include refid="selectBusStatementVo"/>
            <where>
                <if test="params.beginActualArrivalTime != null and params.beginActualArrivalTime != '' and params.endActualArrivalTime != null and params.endActualArrivalTime != ''">
                    and actual_arrival_time between #{params.beginActualArrivalTime} and #{params.endActualArrivalTime}
                </if>
                <if test="isDelete != null "> and is_delete = #{isDelete}</if>
            </where>
        </select>
    
        <delete id="deleteBusStatementByIds" parameterType="String">
            delete from bus_statement where id in
            <foreach item="id" collection="array" open="(" separator="," close=")">
                #{id}
            </foreach>
        </delete>
    
  • 软删除时记得修改一些逻辑上相关的sql,因为软删除数据还存在,要加上is_delete = #{isDelete}的条件

  • 使用postman测试接口:

    • 通过验证码请求http://localhost:8080/captchaImage获取到验证码的uuid和code

    • 登录请求http://localhost:8080/login,body通过raw-JSON格式带上uuid、code、username、password。

      {
      	"uuid": "b9896c01fb814f128d4f6bb47d5fb99f",
      	"username": "admin",
      	"password": "admin123",
      	"code": "14"
      }
      
    • 登录成功后,后续在需要测试的接口请求头带上Content-Type和Authorization,Content-Type固定填入application/json,Authorization填入刚才登录接口返回的token

      在这里插入图片描述

    • 可在若依框架里系统管理-参数设置中关闭验证码

      在这里插入图片描述

7.2、拓展:
  • 预约单超时取消:采用若依自带的定时任务功能。因是个人项目不需要考虑表数据量过大,设置的是每小时执行一次定时任务。若定时任务需要遍历的表数据量过大,则应错峰执行定时任务,如每天凌晨执行。

    /**
     * 定时任务调度测试
     *
     * @author ruoyi
     */
    @Component("appointmentTask")
    public class AppointmentTask {
        @Autowired
        private BusAppointmentMapper appointmentMapper;
    
        /**
         * 预约超时取消
         */
        public void AppointmentOvertime() {
            List<Integer> status = new ArrayList<>();
            status.add(BusAppointment.STATUS_APPOINTMENT);
    
            List<BusAppointment> list = appointmentMapper.selectByStatus(status, BusAppointment.IS_DEL);
    
            for (BusAppointment busAppointment : list) {
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(busAppointment.getAppointmentTime());
                calendar.add(Calendar.HOUR_OF_DAY, 6);
                Date overTime = calendar.getTime();
    
                if (overTime.before(new Date())) {
                    appointmentMapper.updateStatus(busAppointment.getId(), BusAppointment.STATUS_OVERTIME);
    //                System.out.println(busAppointment.getCustomerName() + "已超时");
                }
            }
        }
    }
    

    前端定时任务页面:

    在这里插入图片描述

    拓拓展:Calendar类的入门使用

  • 结算单明细页面,数据无论做任何修改后,在执行保存前都不允许操作支付按钮。给支付按钮标签加:disabled=“canPay”,初始值为true,任何修改操作的方法里都将该值置为true,执行保存方法后该值置为false。

  • 客户管理-拜访记录-回访顾客下拉框,首先在create生命周期里查询获取到bus_customer表里的全顾客列表customerList,然后将customerList放入el-select下的el-option作为v-for遍历的数组,key和value为item.id,lable为item.name

    	 <el-form-item label="回访客户" prop="customerId">
            <el-select
              v-model="queryParams.customerId"
              placeholder="请选择"
              clearable
            >
              <el-option
                v-for="item in customerList"
                :key="item.id"
                :label="item.customerName"
                :value="item.id"
              />
            </el-select>
          </el-form-item>
    
     created() {
        this.getUserList();
        this.getCustomerList();
      },
      
      methods: {
        getCustomerList() {
          listCustomer().then((response) => {
            this.customerList = response.rows;
            console.log(this.customerList);
          });
        },
      }
    
  • 增加了并行网关的工作流流程审核:

    • 先在IDEA19制定一张并行网关的bpmn
      在这里插入图片描述

    • 要改动的地方不多,前端加一个发起并行审核的按钮

      •       <el-col :span="1.5">
                <el-button
                  type="success"
                  plain
                  icon="el-icon-edit"
                  size="mini"
                  :disabled="!canAudit"
                  @click="handleParallelAudit"
                  v-hasPermi="['business:serviceItem:edit']"
                  >发起并行审核</el-button
                >
              </el-col>
          	  <!-- 审核窗口复制一个之前的,只是多一栏下拉框供给并行的选择第二个店长,然后绑定另一个值v-model="shopOwnerId2 -->
                <el-form-item
                  label="审核人(店长):"
                  prop="shopOwners"
                  v-if="isParallel"
                >
                  <el-select size="medium" v-model="shopOwnerId2">
                    <el-option
                      v-for="item in auditInfo.shopOwners"
                      :key="item.userId"
                      :label="item.nickName"
                      :value="item.userId"
                    >
                    </el-option>
                  </el-select>
                </el-form-item>
        
      • data() {
            shopOwnerId2: null,
            // 是否是并行审核
            isParallel: false,
        }
         
        methods:{
            /** 发起并行审核弹窗 */
            handleParallelAudit() {
              if (!this.canAudit) {
                return;
              }
              getAuditInfo(this.id).then((res) => {
                this.resetAudit();
                console.log(res);
                this.auditInfo = res.data;
                this.isParallel = true;
                this.auditOpen = true;
              });
            },
                
            /** 确认发起审核 */
            auditSubmit() {
              let param = {
                id: this.id,
                shopOwnerId: this.shopOwnerId,
                shopOwnerId2: this.shopOwnerId2,
                financeId: this.financeId,
                info: this.info,
              };
              // 开始审核
              if (this.isParallel) {
                startParallelAudit(param).then((res) => {
                  this.getList();
                  this.$modal.msgSuccess("发起审核成功!");
                  this.isParallel = false;
                  this.auditOpen = false;
                })
                .catch(() => {});
              } else {
                startAudit(param).then((res) => {
                  this.getList();
                  this.$modal.msgSuccess("发起审核成功!");
                  this.isParallel = false;
                  this.auditOpen = false;
                });
              }
            },
        }
        
    • 后端新增一个接口接收并行工作流的审核提交,新建一个vo类,因为多了个shopOwnerId2参数要接收。服务层也可以复制之前的再修改调整,改一下校验逻辑,注意在并行网关时双店长审核有一个审核拒绝,另一个在activiti里并不会自动删除(结束流程)而是还在ru_task里,需要手动结束其他并行流程,提供一个思路

      • else {
            		// else里写审核拒绝逻辑
        	// 拿到当前审核流程实例下的其他并行流程
                    List<Task> list = taskService.createTaskQuery()
                            .processInstanceId(audit.getInstanceId()).list();
                    if (list != null && list.size() > 0) {
                        for (Task otherTask : list) {
                            taskService.complete(otherTask.getId(), map);
                            taskService.addComment(otherTask.getId(), audit.getInstanceId().toString(), "其余审核人已拒绝");
                        }
                    }
                    serviceItemMapper.updateServiceItemStatus(audit.getServiceItemId(), BusServiceItem.AUDITSTATUS_REPLY);
                    audit.setStatus(CarPackageAudit.STATUS_REJECT);
                    carPackageAuditMapper.updateCarPackageAudit(audit);
                }
        
    • 一些可能不会报错的bug:流程走向有问题,先看看bpmn有没有写对,使用文本编辑看看每个节点的参数和连接下个节点是否正确。然后看代码,很可能是服务层写错了,比如我遇到一个问题是两个店长都审核完了,结果走财务审核时又跳到了一个店长角色审核,一看才发现是添加条件map时财务节点的value复制错了vo.getshowOnerId。


收工,后续有机会再迭代新功能。


http://www.niftyadmin.cn/n/5187740.html

相关文章

蓝桥杯第三周算法竞赛D题E题

发现更多计算机知识&#xff0c;欢迎访问Cr不是铬的个人网站 D迷宫逃脱 拿到题目一眼应该就能看出是可以用动态规划来解决。但是怎么定义dp呢? 这个题增加难度的点就在当所在位置与下一个要去的位置互质的时候&#xff0c;会消耗一把钥匙。当没有钥匙的时候就不能移动了。想…

delphi电子处方流转(药店)

【delphi电子处方流转(药店)】支持 处方下载、处方核验、处方审核、药品销售出库明细上传、药品销售出库明细撤销等功能。

vue中使用iconfont图标

1. 选择一个图标加入购物车 2、点击右上角购物车图标。点击下载代码&#xff0c;并添加至项目。 3、将下载好的代码文件放入项目中的assets目录的styles下 iconfont.css直接放在styles下 在styles目录下新建目录iconfont&#xff0c;将iconfont.ttf、iconfont.woff、iconfont…

java springboot在当前测试类中添加临时属性 不影响application和其他范围

目前 我们的属性基本都写在 application.yml 里面了 但是 如果 我们只是想做一下临时变量的测试 有没有办法实现呢&#xff1f; 显然是有的 这里 我们还是先在application.yml中去写一个 test属性 下面加个prop 然后 我们尝试在测试类中 获取一下这个属性 直接用 Value 读取…

nvm工具的使用

一、出现的问题 今天安装了nvm工具后&#xff0c;意外发现node命令和npm命令失效了&#xff0c;命令行出现了以下现象 当输入node -v&#xff0c;显示&#xff1a;‘node’不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 最后发现&#xff0c;是因为在安装…

系列八、JVM的内存结构【方法区】

一、概述 方法区是一个供各线程共享的运行时内存区域。它存储了每一个类的结构信息&#xff0c;例如运行时常量池&#xff08;Runtime Constant Pool&#xff09;、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范&#xff0c;在不同的虚拟机里面实现是不一样…

VSCode打开Json 文件格式化

在VSCode中打开JSON文件时&#xff0c;你可以使用以下步骤来格式化JSON并显示为多行&#xff1a; 使用快捷键&#xff1a; 在打开的JSON文件中&#xff0c;使用快捷键格式化文档。 Windows/Linux&#xff1a;Shift Alt FmacOS&#xff1a;Shift Option F 右键菜单&#xff…

深度学习OCR中文识别 - opencv python 计算机竞赛

文章目录 0 前言1 课题背景2 实现效果3 文本区域检测网络-CTPN4 文本识别网络-CRNN5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习OCR中文识别系统 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;…