为了更加熟悉activiti工作流的使用和实战而改造的项目,欢迎大家参考和提出问题建议一起学习~
源码gitee仓库地址:Yuzaki-NASA / Activiti7_test_car_rbac
master分支是稳定版,dev分支是后来加了个新的并行审核流程和客户管理,个人测了多遍没啥问题,建议拉dev的代码。
sql文件在caro2o-business下的resources/sql里,启动项目前记得先添加一下sql
原模板源码gitee仓库地址:Yuzaki-NASA / activiti7-caro2o-template
这个是参考的模板,功能除去一些被我优化过的地方以外大多一致,还写了很多注释,便于对照理解学习
项目总结:
(源码中的caro2o-ui下的src/assets路径下也有总结的pdf文件)
用自己的大白话来总结就是:
自动配置简单来说就是自动去把第三方组件的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自动配置。
优点:
缺点:
springboot常见注解可以参考这个:https://zhuanlan.zhihu.com/p/593053050?utm_id=0
来说一下caro2o项目中一些比较常用和重要的注解:
@RestController:
@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语句中参数赋值而服务的。
@Transactional:在service层开启事务,防止执行途中出错而造成的数据混乱。
其余还有一些若依自己封装的注解类似@PreAuthorize(权限)、@Log(打印日志)、@Excel(导出文件相关注解)就不展开说明了,不同项目会封装不同的自定义注解,这些都需要自己去研究其作用与实现。
RBAC是一种基于角色实现访问控制的权限管理机制,通过定义角色和权限、用户和角色、角色和角色之间的关系,实现多层次、细粒度、可复用的权限管理系统。
基本模型有三个元素:用户、角色和权限。模型设计基于“多对多”原则,即多个用户可以具有相同的角色,一个用户可以具有多个角色。同样,您可以将同一权限分配给多个角色,也可以将同一角色分配给多个权限。
更详细解释见:https://zhuanlan.zhihu.com/p/513142061
用户信息需要确保安全性,不能泄露。
角色关系到用户和权限,需要设计合理。
权限字段应在前端与后端都有校验:前端通过菜单或按钮的显示与否体现对不同角色权限的控制,但前端可能会被用户恶意修改视图去显示出因没有权限而过滤掉的功能菜单或按钮,此时在后端也要增加权限校验,在该用户没拥有该权限时,发起的请求返回403错误,弹框提示该用户缺少对应权限。
数据字典是整个平台中数据描述的有效机制。通过界面进行可视化的操作和维护,能快速录入和修改平台上统一的字典数据。有效提高了数据的重复利用率和产品、项目的开发效率。整个数据字典数据为框架平台所共享,用户可以更好地对系统进行自定义管理,以满足自己的个性化需求。
参考:https://www.python100.com/html/82651.html
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"
/>
没有⼯作流引擎之前如果要控制业务流程我们可能通过改变某个字段的状态来实现,这会带来⼀旦我们流程发⽣变化的时候我们就需要去同步修改代码。⽽⽤流程引擎它⾥⾯内置可25张表,我们只要读取它⾥⾯的表就可以了,与表对应的它还提供⼀系列可操作表的接⼝。核⼼⼀个类是ProcessEngine,通过它能获取⼀系列的service接⼝。
部署⼯作流引擎,其实就是jar包api
流程定义:.bpmn⽂件,是⼀个xml⽂件定义了流程信息
流程定义部署
启动⼀个流程实例
⽤户查询代办任务,⼀个instance有多个task
⽤户办理任务
流程结束
优点
1、 最大的优点就是免费开源,这也是很多人选择的原因
2、 小项目中应用简单的串行并行流转基本能满足需求。
缺点
1、节点定义概念不同
2、缺乏可“追溯”性
3、扩展需要与很多的Event来实现
4、二次开发难度大,门槛高
获取方式:
repositoryService.createDeployment().deploy();
对应的表:act_re_deployment
用于存储流程部署的相关信息。该表记录了每个流程部署的唯一标识符(ID)、名称(NAME)、类别(CATEGORY)、租户标识符(TENANT_ID)、键(KEY)以及部署时间(DEPLOY_TIME)等信息。
核心字段:
id、name、deployementTime、category、key、tenantid
获取方式:
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
获取方式:
方式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
获取方式:
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
获取方式:
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
获取方式:
runtimeService.createExecutionQuery()
.processDefinitionKey(“流程定义的key”)
.executionId(“executionId”)
.processDefinitionId(“流程定义id”)
.processInstanceId(“流程实例id”)
.processDefinitionKey(“流程定义的key”)
.singleResult();
对应的表:act_ru_execution
是存储运行时数据的表,主要包含执行过程中的活动、任务、变量等数据。该表记录了每个流程实例的执行路径信息,例如当前执行到哪个流程节点、哪些分支已经被激活等。通过查询 act_ru_execution 表,可以获取流程实例的实时运行状态信息,例如哪个任务正在由哪个用户执行、执行到哪个节点等。这对于跟踪和管理业务流程中的实例非常有用。
核心字段:
id、activityId、processInstanceId、name、description
获取方式:
方式1:runtimeService.getIdentityLinksForProcessInstance(processInstanceId)
方式2:repositoryService.getIdentityLinksForProcessDefinition(ProcessDefinitionId)
方式3:taskService.getIdentityLinksForTask(taskId)
对应的表:act_ru_identitylink
存储了用户或用户组与流程数据之间的绑定关系。该表记录了用户或用户组与流程实例、流程任务等数据的关联信息。通过查询 act_ru_identitylink 表,可以获取用户或用户组与流程实例、流程任务等数据的绑定关系,例如哪个用户或用户组执行了哪个流程任务、哪些流程任务被指定给了哪些用户或用户组等。这对于了解业务流程的执行情况、进行权限管理和任务分配等操作非常有用。
核心字段:
type、userId、taskId、processDefinitionId、processInstanceId
设置一个流程管理模块,数据库创建一张流程定义明细表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);
}
在流程定义明细页面中可以查看流程文件或流程图,具体实现代码:
@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;
}
流程定义的撤销:
/**
* 批量撤销流程定义明细
*
* @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;
}
发起审核的弹框里需要用户从前端传入所有所需的参数,如审核人等。并且前端和后端都要添加状态判断——该业务处在什么状态下才允许发起审核、该业务的某些条件是否影响审核节点等。
@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);
}
每一个开启审核的业务对应一个执行的流程实例,我们要创建一个业务表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='套餐审核';
审核历史按钮功能:查看该条审核的审批操作历史。使用对应的流程实例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;
}
进度查看按钮功能:查看流程进行到哪,在流程图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;
}
撤销审核按钮功能:顾名思义。先校验该流程状态是否允许被撤销,撤销时需要完成三个步骤——服务套餐状态置为初始化、审核信息记录状态置为审核撤销、将运行流程实例(关联到的几个表)执行撤销方法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(), "审核被撤销了");
}
前端代码可以直接拷贝套餐审核信息模块的vue文件,因为查询的都是业务表bus_car_package_audit。不同的是该两个模块只负责流程的推动和审批,故没有撤销按钮功能,而我的待办模块会多一个“审批”功能,即分配给当前用户的审批流程可以通过该操作选择同意或拒绝来推动流程进行。
查询功能参数需要添加当前用户条件,因为只能查到当前登录用户自己负责的审核流程。若依有自带的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();
这里提供一个更为直观的多表联查方法:我们一开始就能用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);
<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");
我的已办中的审批功能:首先校验状态是否能进行审核。然后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:流程--流程正常结束
}
}
脚手架(scaffolding)指的是创建项目时,自动完成的创建初始文件等初始化工作。这些工作往往是每次新建工程都要进行的重复性工作。如创建Maven 项目时使用的原型(archetype)等。脚手架是一种由一些 model–view–controller 框架支持的技术,程序员可以在其中指定应用程序数据库的使用方式。
有官方文档就按照官方文档操作一遍,没有就自己捣鼓或问别人。
工作中很大概率不会使用若依这样的脚手架,则拿到脚手架后的改造就要自己操作了,注意哪些文件夹和类名要改,pom里依赖的坐标名、版本等,最后再全局替换一下需要替换的字段,启动项目看看有没有问题。最好是公司的脚手架已配置好初始化信息。
后端:
前端:
使用脚手架添加菜单和生成代码时,务必注意模块名和路径这类敏感信息不要写错。善用数据字典。
善用各种工具类,例如验证手机号和验证车牌号,能省很多事
// 验证是否非法手机号
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,善用
<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
可在若依框架里系统管理-参数设置中关闭验证码
预约单超时取消:采用若依自带的定时任务功能。因是个人项目不需要考虑表数据量过大,设置的是每小时执行一次定时任务。若定时任务需要遍历的表数据量过大,则应错峰执行定时任务,如每天凌晨执行。
/**
* 定时任务调度测试
*
* @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);
});
},
}
增加了并行网关的工作流流程审核:
要改动的地方不多,前端加一个发起并行审核的按钮
<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>
<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。
收工,后续有机会再迭代新功能。