一个开源的汽修rbac后台管理系统项目,基于若依框架,实现了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

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。


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/152471.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32CubeIDE报“xxx is not implemented and will always fail”解决方法

本文介绍STM32CubeIDE报“xxx is not implemented and will always fail”解决方法。 最近用STM32CubeIDE开发STM32程序时&#xff0c;编译报警告&#xff1a; warning: _close is not implemented and will always fail warning: _lseek is not implemented and will always…

idea中把spring boot项目打成jar包

打jar包 打开项目&#xff0c;右击项目选中Open Module Settings进入project Structure 选中Artifacts&#xff0c;点击中间的加号&#xff08;Project Settings->Artifacts->JAR->From modules with dependencies &#xff09; 弹出Create JAR from Modules&#…

想转行互联网行业,是选择网络安全还是人工智能?

随着数字时代的到来&#xff0c;网络安全和人工智能成了科技创新产业的重要组成部分。也逐渐成了大多数人心中热门的行业选择。那么该如何抉择呢&#xff1f; 首先我们来了解下人工智能的发展前景&#xff1a; 如今&#xff0c;人工智能技术无论是在核心技术方面&#xff0c…

系列四、本地接口(Native Interface)

一、概述 本地接口的作用是融合不同的编程语言为Java所用&#xff0c;它的初衷是融合C/C程序&#xff0c;Java诞生的时候正是C/C横行的时候&#xff0c;要想立足&#xff0c;必须要调用C/C的程序&#xff0c;于是Java就在内存中开辟了一块区域专门用于处理标记为native的代码&a…

wpf devexpress数据统计

GridControl允许显示总结信息关于单个数据行分组。例如&#xff0c;你可以显示记录数量&#xff0c;最小和最大值。这个统计信息可以叫做数据统计。 创建统计 GridControl 支持总结和分组统计&#xff1a; 总结统计 - 一个总结函数值计算对于所有列和视图显示统计面板和固定统…

2023最受推荐的五款项目管理工具

1、进度猫 进度猫是国内一款轻量级项目管理工具&#xff0c;适用于实时协作的团队。 以甘特图为向导&#xff0c;基于任务清单todolist&#xff0c;支持多用户协作&#xff1b; 甘特图显示具体任务清单、时间和任务的进度&#xff1b; 对未完成任务、已完成任务进行分类管…

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

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

【Spring进阶系列丨第二篇】Spring中的两大核心技术IoC(控制反转)与DI(依赖注入)

前言 我们都知道Spring 框架主要的优势是在 简化开发 和 框架整合 上&#xff0c;至于如何实现就是我们要学习Spring 框架的主要内容&#xff0c;今天我们就来一起学习Spring中的两大核心技术IoC&#xff08;控制反转&#xff09;与DI&#xff08;依赖注入&#xff09;。 文章目…

【BIM入门实战】高程点无法放置的解决方法

文章目录 一、问题提出二、解决办法1. 检查模型图形样式2. 高程点可以放置的图元一、问题提出 在平面图中添加高程点时有时会遇到无法在楼板等平面构件上放置高程点,应如何设置才能使高程点正常放置? 如下图所示,楼板上无法放置高程点: 二、解决办法 1. 检查模型图形样式…

Linux C/C++全栈开发知识图谱(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)

众所周知&#xff0c;在所有的编程语言中&#xff0c;C语言是一门颇具学习难度&#xff0c;需要很长学习周期的编程语言。甚至很多人经常听到一句调侃的话语——“C&#xff0c;从入门到放弃”。 C界的知名书籍特别多&#xff0c;从简单到高端书籍&#xff0c;许多书籍都是C之…

1.mysql安装及基础

目录 概述安装上传jar包解压添加用户组和用户更改权限修改配置文件 my.cnf初始化登录mysql修改密码远程登录生效配置 sql语句分类数据定义语言 结束 概述 mysql安装及基础&#xff0c;后续涉及基础会继续补充。 安装 上传jar包 下载地址 解压 tar -zxvf mysql-5.7.44-li…

代码随想录 Day47 动态规划15 LeetCode T583 两个字符串的删除操作 T72 编辑距离

LeetCode T583 两个字符串的删除操作 题目链接:583. 两个字符串的删除操作 - 力扣&#xff08;LeetCode&#xff09; 题目思路: 本题有两个思路 1.使用两个字符串的长度之和-2*最长公共子串(换汤不换药) 代码随想录Day45 动态规划13 LeetCode T1143最长公共子序列 T1135 不相交…

kubectl 本地远程链接k8s多个集群,远程管控多集群,查看日志 部署服务(windows版)

文章目录 一、前言二、windows上安装kubectl和mobaxterm2.1 准备安装包2.2 安装kubectl2.3 链接k8s集群2.4 查看某一个pod的容器日志2.5 切换context 上下文配置&#xff0c;实现在多个k8s集群间动态切换 一、前言 现如今是一个万物皆上云 的时代&#xff0c;各种云层出不穷&am…

JEECG BOOT 前端记录

目录 查询 1、模糊搜索中文 2、下拉框选择 3、文本框 新增 1、添加文本框 2、图片上传 3、文件上传 4、富文本 5、下拉框数字回显文字 第一种&#xff1a; 第二种&#xff1a; 展示 1、字典翻译注解Dict 1.2、字典表翻译用法 2、点击事件调接口 查询 1、模糊搜索中…

新生儿腿纹不对称:原因、科普和注意事项

引言&#xff1a; 新生儿身上出现腿纹不对称的现象在一些家庭中可能引起担忧&#xff0c;然而&#xff0c;了解这一现象的原因以及如何正确处理是非常重要的。本文将科普新生儿腿纹不对称的原因&#xff0c;提供相关信息&#xff0c;并为父母和监护人提供注意事项&#xff0c;…

腾讯待办是什么?关停之后如何继续提醒待办事项?

由于业务方向调整&#xff0c;腾讯待办将于2023年的12月20日全面停止运营并下架。那么腾讯待办是什么呢&#xff1f;它是一款以微信小程序呈现的待办事项和日程管理工具&#xff0c;旨在帮助用户更好地管理自己的待办事项和日程安排。用户可以在该小程序中创建待办事项、设置提…

单词故事嵌入:通过自然语言处理解开叙事

一、介绍 在自然语言处理和文本分析领域&#xff0c;寻求理解和表示人类叙事丰富而复杂的结构是一个持续的挑战。在研究人员和数据科学家可以使用的众多工具和技术中&#xff0c;“Word Story Embeddings”作为一种创新且有前景的方法脱颖而出。这些嵌入建立在词嵌入的基础上&a…

力扣每日一题-最长奇偶子数组-2023.11.16

力扣每日一题&#xff1a;最长奇偶子数组 题目链接:2760.最长奇偶子数组 题目描述 代码思路 利用单指针进行扫描&#xff0c;符合子数组起点要求时&#xff0c;开始记录子数组长度。题目本身不难理解&#xff0c;就是判断的条件比较多&#xff0c;需要耐心和细心。 代码纯享…

进程终止和进程等待

一 进程终止 (1)exit和return 先前已经了解了进程创建&#xff0c;以及进程大致相关的数据结构&#xff0c;但是有个小知识一直没提及&#xff0c;那就是exit&#xff0c;还有就是return 0。这两个的作用有点相似&#xff0c;都可以终止进程&#xff0c;但又有点不同&#xff…

Hoppscotch:开源 API 开发工具,快捷实用 | 开源日报 No.77

hoppscotch/hoppscotch Stars: 56.1k License: MIT Hoppscotch 是一个开源的 API 开发生态系统&#xff0c;主要功能包括发送请求和获取实时响应。该项目具有以下核心优势&#xff1a; 轻量级&#xff1a;采用简约的 UI 设计。快速&#xff1a;实时发送请求并获得响应。支持多…