当前位置: 首页 > news >正文

SpringBoot整合Flowable/Activiti

SpringBoot版本: 2.0.1.RELEASE

Flowable版本: 6.3.1

Activiti版本: 6.0.0

一.添加pom依赖

    因为之前我整合的时候有报错关于sqlsession的错误,后面查询文章才发现flowable要排除掉mybatis,又没说具体排除哪一个,所以我这干脆全部排除了

<!-- Flowable dependencies --><dependency><groupId>org.flowable</groupId><artifactId>flowable-spring-boot-starter</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-engine</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-spring</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-rest</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-cmmn-spring</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.flowable</groupId><artifactId>flowable-dmn-spring</artifactId><version>6.3.1</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion></exclusions></dependency>

<!--activiti modeler start--><dependency><groupId>org.activiti</groupId><artifactId>activiti-json-converter</artifactId><version>6.0.0</version><exclusions><exclusion><groupId>org.activiti</groupId><artifactId>activiti-bpmn-model</artifactId></exclusion></exclusions></dependency>

二.yml配置flowable/Activiti相关配置项

  至于层级关系,都是和spring同级的,我一般放在mybatis配置项下面,类似下面这样

  flowable:#关闭定时任务JOBasync-executor-activate: false#  将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。database-schema-update: true# activiti 模块activiti:# 解决启动报错:class path resource [processes/] cannot be resolved to URL because it does not existcheck-process-definitions: false# 检测身份信息表是否存在db-identity-used: true# 默认为false# false:项目启动时,如果数据库中没有表将抛出异常(上线后使用)# true:项目启动时,如果数据库中没有表将自动生成(开发和测试时使用)database-schema-update: true

 三.数据库配置项中url添加&nullCatalogMeansCurrent=true配置

        因为我之前用过activiti工作流,如果不加这个配置工作流会默认查询整个数据库连接下所有的库,只要发现有相关的act表就会认为已经自动生成了. 导致你在A库有了工作流的表之后想在B库再去自动生成就会发现生成不了.加上这个配置就可以了,不知道flowable影不影响,我都习惯给加上了

#  nullCatalogMeansCurrent=true
#  因为mysql使用schema标识库名而不是catalog,因此mysql会扫描所有的库来找表,
#  如果其他库中有相同名称的表,activiti就以为找到了,本质上这个表在当前数据库中并不存在。
#  解决办法就是在数据库链接的配置后面加上 nullCatalogMeansCurrent=true,标识之搜索当前链接的库。
url: jdbc:mysql://192.168.*.*:3306/flowable?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true

四.启动项目 自动生成数据表

        这个会根据flowable的版本而来 实际可能不一致 我的flowable版本是6.3.1,生成了66张表

Activiti版本是6.0.0,生成了28张表

一些常用的需要关注的表的含义(注明了注释)

 

五.IDEA中安装Flowable/Activiti插件

        安装插件: Flowable BPMN visualizer / Activiti BPMN visualizer

        然后在resources目录下新建processes目录

        右键新建一个基于BOMN 2.0协议的文件

        绘制流程图 

 

        注意: flowable/Activiti都会将处于processes目录下的符合后缀名要求的文件自动部署

                 不想要这个功能你可以修改目录名或者查询网站搜索下相关的关闭命令

六.工作流提供的几个Service

Flowable和Activiti都给我们提供了几个Service

因为本身Flowable的团队就是从Activiti团队中过来的,所以Service的命名及使用基本都是一致的,会一个就会另一个

    // 运行时执行管理Service@Autowiredprivate RuntimeService runtimeService;// 工作流任务管理Service@Autowiredprivate TaskService taskService;// 管理流程定义Service@Autowiredprivate RepositoryService repositoryService;// 核心类 可以理解为流程引擎对象@Autowiredprivate ProcessEngine processEngine;// 历史管理Service@Autowiredprivate HistoryService historyService;

七.常用方法

因为几乎所有业务都需要用到"PROC_INST_ID_" 也就是 "流程实例Id"

以及当前任务id-TASK_ID

所以我建议把这两个id都放在业务表中和你的业务表一对一关联(一般都是一个业务开一个流程实例)

1.动态部署

(提供一个文件,通过repositoryService创建部署对象去读取xml文件的内容来自动部署)

部署完之后ACT_RE_DEPLOYMENT,ACT_RE_PROCDEF两个表中会有数据

MultipartFile file; // 提供一个二进制文件对象
Deployment deployment = null;try{DeploymentBuilder deploymentBuilder =repositoryService.createDeployment()
.name(Objects.requireNonNull(file.getOriginalFilename()).substring(0, file.getOriginalFilename().indexOf(".bpmn20.xml"))).addInputStream(file.getOriginalFilename(),file.getInputStream());deployment = deploymentBuilder.deploy();}catch (Exception e){e.printStackTrace();throw new CustomException("文件部署失败");}// 部署完后可以获取部署ID
// 根据部署ID查询流程定义
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult();
System.out.println("流程【"+processDefinition.getName()+"】部署成功, 部署ID为: "+processDefinition.getDeploymentId()+"流程定义Key为: "+processDefinition.getKey());

2.流程启动

Map<String, Object> variables = new HashMap<>();variables.put("startUser", SecurityUtils.getLoginUser().getUser().getUserId().toString());variables.put("data", processInit.getData());variables.put("remark", processInit.getRemark());ProcessInstance processInstance = null;try{// 根据部署ID查询流程定义ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(processInit.getDeployId()).singleResult();//启动流程实例,同时还要指定业务标识businessKey  项目Id//第一个参数:是指流程定义key//第二个参数:业务标识businessKeyprocessInstance = runtimeService.startProcessInstanceByKey(processDefinition.getKey(),processInit.getWorkId().toString(),variables);runtimeService.setProcessInstanceName(processInstance.getId(),processDefinition.getName());

        启动后ACT_RU_EXECUTION会看到流程实例当前执行的节点概况,包括历史节点和当前时节点

PROC_INST_ID_是流程实例Id 

 

这两个表也可以看到实例相关节点信息

 

3.执行下一任务节点并添加审核备注

核心方法为: 

taskService.complete(task.getId());

需要注意的是:如果没有执行人,需要先认领任务而后才去审核任务

SysUser user = SecurityUtils.getLoginUser().getUser();String taskId; // 当前任务IdString processInstanceId; // 流程实例IdProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();String businessKey = processInstance.getBusinessKey(); // 流程实例的业务Id-businessKey// 获取当前待办任务Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).taskId(taskId).singleResult();if(task == null){throw new CustomException(CustomExceptionEnums.DATA_NOT_FOUND);}String assignee = task.getAssignee();if(StringUtils.isEmpty(assignee)){// 如果没有执行人 需要先认领任务taskService.claim(task.getId(),user.getUserId().toString());}if(StringUtils.isNotEmpty(processApproved.getRemark())){// 添加审批备注taskService.addComment(taskId, processInstanceId, ActivityCommentType.APPROVED_REMARK.getType(), processApproved.getRemark());}// 完成当前任务taskService.complete(task.getId());

 通过后可通过taskService获取下一个任务节点Id

// 获取下一个任务ID
List<Task> taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).list();

4.获取待办/已办

因为Activiti和Flowable中并没有获取待办任务和已办任务的直接方法可以调用

所以需要我们自己写sql语句来获取

需要用到以下的表

待办任务所需

"ACT_RU_TASK"运行时任务节点表

"ACT_RU_IDENTITYLINK"运行时流程人员表

"ACT_HI_PROCINST"历史流程实例表

已办任务所需

"ACT_HI_TASKINST"历史任务节点表

"ACT_HI_PROCINST"历史流程实例表

然后我自己通过union将结果合并,并通过task_status来区分待办和已办

-- 待办和已办union all查询
select * from
(
select 0 as task_status,
RES.ID_ as task_id,RES.NAME_ as task_name,RES.CREATE_TIME_ as start_time,null as end_time,RES.PROC_INST_ID_ as process_instance_id,RES.ASSIGNEE_ as assignee,P.BUSINESS_KEY_  as business_key ,P.NAME_ as process_instance_name ,I.TYPE_ as type,I.USER_ID_ as user_id, I.GROUP_ID_ as group_id ,RES.TASK_DEF_KEY_ as task_def_key
from ACT_RU_TASK RES 
left join ACT_RU_IDENTITYLINK I on I.TASK_ID_ = RES.ID_ 
left join ACT_HI_PROCINST P on P.PROC_INST_ID_ = RES.PROC_INST_ID_ UNION ALLselect 1 as task_status,
RES.ID_ as task_id,RES.NAME_ as task_name,RES.START_TIME_ as start_time,RES.END_TIME_ as end_time,RES.PROC_INST_ID_ as process_instance_id,RES.ASSIGNEE_ as assignee,P.BUSINESS_KEY_  as business_key ,P.NAME_ as process_instance_name ,null as type,null as user_id, null as group_id,RES.TASK_DEF_KEY_ as task_def_key
from ACT_HI_TASKINST RES 
left join ACT_HI_PROCINST P on P.PROC_INST_ID_ = RES.PROC_INST_ID_  and RES.END_TIME_ is not null
)v

然后可以通过条件查询,比如"执行人","执行部门","任务节点定义key"等来进行查询

(task_status : 待办/已办)

(assignee : 执行人Id)

(task_def_key : 任务节点定义key)

(type : 表示候选用户 

  • candidate: 表示候选用户或用户组。在任务尚未被领取时,这些用户或用户组可以领取任务。
  • participant: 表示任务的参与者。通常情况下,当任务被领取后,参与者就是具体执行任务的用户。
  • owner: 表示任务的所有者。在某些情况下,任务可能会指定一个所有者,通常是在任务创建时指定,这个所有者有特殊权限或责任来处理任务。

)

(group_id  : 部门Id)

(start_time : 任务节点生成时间)

(end_time : 任务节点完成时间)

比如以下的where条件可以用作待办任务的筛选查询

WHERE v.task_status = 0 and v.assignee = '1'  and v.task_def_key=  'sid-5db27d85-b516-4784-8b58-0f5c9e1c3235' or (v.assignee is null and v.type = 'candidate' and (v.user_id = '1' or v.group_id IN ( '土整中心','123' ) )) order by v.start_time desc

比如以下的where条件可以用作已办任务的筛选查询 

WHERE v.task_status = 1 and v.assignee = '1' and v.task_def_key=  'sid-8abc3887-40c5-49c2-b8c0-b4d3fdefb7a0' and v.end_time is not null order by v.end_time desc


5. 通过taskId获取该任务的备注

/*** 通过taskId获取该任务的备注* @param taskId 任务Id* @param commentType 备注类型* @return 备注详情*/@Overridepublic List<Comment> getCommentByTaskId(String taskId,String commentType){if(StringUtils.isNotEmpty(commentType)){// 带有type筛选return taskService.getTaskComments(taskId, commentType);}return taskService.getTaskComments(taskId);}

6.查询截止目前可驳回的节点列表

我的逻辑是查询当前已完成的用户任务,不包括"开始"节点(因为"开始"节点不是用户任务)

然后有一些特殊情况需要处理:

        比如多次驳回,因为每一次驳回实际上都是将当前任务节点完成然后再加一条"目标驳回任务节点"用作下一个节点

        比如多次驳回之后取最新的节点,不可能出现可驳回列表中出现两个key相同的节点,这不符合逻辑

/*** 查询截止目前可驳回的节点列表* @param processInstanceId 流程ID* @return 结果*/@Overridepublic List<ActHistoricActivityInstanceVo> getAllActInstByInstanceIdToSimple(String processInstanceId,String taskDefKey){// 只要用户任务// 目前已完成的用户任务List<HistoricActivityInstance> userTask = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().activityType("userTask").finished().list();// 流程定义时的用户任务ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();String processDefinitionId = processInstance.getProcessDefinitionId();ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(processDefinitionId).singleResult();// 定义时的所有节点Collection<FlowElement> flowElements = repositoryService.getBpmnModel(processDefinitionId).getMainProcess().getFlowElements();List<ActHistoricActivityInstanceVo> userTasks = new ArrayList<>();// 循环for (HistoricActivityInstance historicActivityInstance : userTask) {for (FlowElement flowElement : flowElements) {if (flowElement instanceof UserTask) {UserTask definitionUserTask = (UserTask) flowElement;String activityName = definitionUserTask.getName();String activityId = definitionUserTask.getId();String activityTaskDefKey = definitionUserTask.getId();// 可以查看其他用户任务属性,如Assignee、Candidate Users、Candidate Groups等// 获取任务定义对象信息// 入参taskDefKey对应的对象SysProcessTaskDef processTaskDef = sysProcessTaskDefMapper.selectSysProcessTaskDefById(taskDefKey);// 当前循环体中taskDefKey对应的对象SysProcessTaskDef currentTaskDef = sysProcessTaskDefMapper.selectSysProcessTaskDefById(activityTaskDefKey);if(currentTaskDef.getSort() >= processTaskDef.getSort()){// 不能驳回到后面的任务去(防止二次提交后可驳回的任务列表乱套)continue;}// 如果Id匹配 就加入自定义集合返回if(activityId.equals(historicActivityInstance.getActivityId()) && !activityTaskDefKey.equals(taskDefKey)){ActHistoricActivityInstanceVo activityInstanceVo = new ActHistoricActivityInstanceVo();activityInstanceVo.setActivityId(activityId);activityInstanceVo.setActivityName(activityName);// 对重复的跳过 (针对多次驳回导致存在多个相同节点的情况)
//                        if(userTasks.contains(activityInstanceVo)) continue;ActHistoricActivityInstanceVo actHistoricActivityInstanceVo = userTasks.stream().filter(u -> u.getActivityId().equals(activityInstanceVo.getActivityId())).findAny().orElse(null);if(actHistoricActivityInstanceVo != null && actHistoricActivityInstanceVo.getStartTime().compareTo(historicActivityInstance.getStartTime()) < 0){// 存在并且时间早于这一次就删除 (针对多次驳回导致存在多个相同节点的情况,取最新的节点审核情况)userTasks.remove(actHistoricActivityInstanceVo);}// 转换赋值BeanUtils.copyProperties(historicActivityInstance, activityInstanceVo);// 获取type为isFillSteps的comment(是否为填报步骤:提交填报时的那个步骤)List<Comment> comments = taskService.getTaskComments(activityInstanceVo.getTaskId(), "isFillSteps");if(comments != null && comments.size() > 0){activityInstanceVo.setFillSteps(true);}userTasks.add(activityInstanceVo);}}}}if(userTasks.size() > 0){userTasks = userTasks.stream().sorted(Comparator.comparing(ActHistoricActivityInstanceVo::getStartTime)).collect(Collectors.toList());}return userTasks;}

 

7.驳回到指定节点

提供流程实例Id,当前任务Id,驳回目标任务Id

/*** 驳回到指定节点* @param actRollbackIo 驳回对象*/@Override@Transactional(rollbackFor = Throwable.class,propagation = Propagation.REQUIRES_NEW)public void rejectToNode(ActRollbackIo actRollbackIo){String processInstanceId = actRollbackIo.getProcessInstanceId();String currentTaskId = actRollbackIo.getTaskId(); // 当前任务String targetTaskId = actRollbackIo.getTargetTaskId(); // 驳回目标任务//  获取该流程所有的活动实例List<HistoricActivityInstance> hisActivityList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).list();ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();String processDefinitionId = processInstance.getProcessDefinitionId();//  当前任务Task currentTask = taskService.createTaskQuery().taskId(currentTaskId).singleResult();//  驳回目标任务HistoricTaskInstance targetTask = historyService.createHistoricTaskInstanceQuery().finished().taskId(targetTaskId).singleResult();if(currentTask == null || targetTask == null){throw new CustomException(CustomExceptionEnums.MISSING_DATA_ERROR);}try{//  当前活动HistoricActivityInstance currentActivity = hisActivityList.stream().filter(e -> currentTask.getId().equals(e.getTaskId())).collect(Collectors.toList()).get(0);//  驳回目标活动HistoricActivityInstance targetActivity = hisActivityList.stream().filter(e -> targetTask.getId().equals(e.getTaskId())).collect(Collectors.toList()).get(0);//  获取xml文件对象BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);//  获取驳回目标活动节点FlowNode targetFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(targetActivity.getActivityId());//  获取当前活动节点FlowNode currentFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(currentActivity.getActivityId());//  临时保存当前活动的原始方向List<SequenceFlow> originalSequenceFlowList = new ArrayList<>();originalSequenceFlowList.addAll(currentFlowNode.getOutgoingFlows());//  清理活动方向currentFlowNode.getOutgoingFlows().clear();//  建立新方向SequenceFlow newSequenceFlow = new SequenceFlow();newSequenceFlow.setId("newSequenceFlowId");newSequenceFlow.setSourceFlowElement(currentFlowNode);newSequenceFlow.setTargetFlowElement(targetFlowNode);List<SequenceFlow> newSequenceFlowList = new ArrayList<>();newSequenceFlowList.add(newSequenceFlow);//  当前节点指向新的方向currentFlowNode.setOutgoingFlows(newSequenceFlowList);//  完成当前任务taskService.claim(currentTaskId,SecurityUtils.getLoginUser().getUser().getUserId().toString());taskService.complete(currentTaskId);//  重新查询当前任务Task nextTask = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();String assignee = (StringUtils.isNull(actRollbackIo.getAssignee())) ? targetTask.getAssignee() : actRollbackIo.getAssignee().toString();if (null != nextTask) {System.out.println("最新任务=="+nextTask);taskService.setAssignee(nextTask.getId(), assignee);}else{throw new CustomException(CustomExceptionEnums.OPERATION_FAILED);}//  恢复原始方向currentFlowNode.setOutgoingFlows(originalSequenceFlowList);
}

8.撤回 删除流程

    /*** 撤回 删除流程* @param processInstanceId 流程实例Id* @param fillId 任务填报Id* @param message 撤回备注信息*/@Override@Transactional(rollbackFor = Throwable.class)public int revoke(String processInstanceId,Long fillId,String message){runtimeService.deleteProcessInstance(processInstanceId, message);historyService.deleteHistoricProcessInstance(processInstanceId);return landProjectFillService.cleanProcessInstance(fillId);}

后续有新的方法再来补充

相关文章:

SpringBoot整合Flowable/Activiti

SpringBoot版本: 2.0.1.RELEASE Flowable版本: 6.3.1 Activiti版本: 6.0.0 一.添加pom依赖 因为之前我整合的时候有报错关于sqlsession的错误,后面查询文章才发现flowable要排除掉mybatis,又没说具体排除哪一个,所以我这干脆全部排除了 <!-- Flowable dependencies -->…...

基础总结篇:Activity生命周期

private int param 1; //Activity创建时被调用 Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.i(TAG, “onCreate called.”); setContentView(R.layout.lifecycle); Button btn (Button) findViewById(R.id.…...

【鸿蒙 HarmonyOS】@ohos.promptAction (弹窗)

一、背景 创建并显示文本提示框、对话框和操作菜单。 文档地址&#x1f449;&#xff1a;文档中心 说明 本模块首批接口从API version 9开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 该模块不支持在UIAbility的文件声明处使用&#xff0c;即…...

ElasticSearch的常用数据类型

常见的数据类型 Text类型&#xff08;文本数据类型&#xff09; 用于全文检索的字段&#xff0c;例如电子邮件的正文或产品的描述。这些字段是analyzed&#xff0c;也就是说&#xff0c;它们通过分析器传递&#xff0c;以便 在被索引之前将字符串转换为单个术语的列表。通过分…...

C/C++预处理过程

目录 前言&#xff1a; 1. 预定义符号 2. #define定义常量 3. #define定义宏 4. 带有副作用的宏参数 5. 宏替换的规则 6. 宏和函数的对比 7. #和## 8. 命名约定 9. #undef 10. 命令行定义 11. 条件编译 12. 头文件的包含 13. 其他预处理指令 总结&#x…...

客服电话系统:专业、便捷的服务沟通桥梁

一、引言 1.客服电话系统在现代服务中的重要性 在信息化时代&#xff0c;服务行业的竞争日益激烈&#xff0c;提供高效、便捷的服务成为企业赢得市场、获取用户信任的关键。客服电话系统作为企业与用户之间的重要沟通桥梁&#xff0c;不仅承载着解答疑问、处理问题的职责&…...

IP地址与子网掩码

1 IP地址 1.1 IPv4与IPv6 1.2 IPv4地址详解 IPv4地址分4段&#xff0c;每段8位&#xff0c;共32位二进制数组成。 1.2.1 地址分类 这32位又被分为网络号和主机号两部分&#xff0c;根据网络号占用位数的不同&#xff0c;又可分为以下几类&#xff1a; A类地址&#xff1a;…...

Python爬取公众号封面图(零基础也能看懂)

&#x1f4da;博客主页&#xff1a;knighthood2001 ✨公众号&#xff1a;认知up吧 &#xff08;目前正在带领大家一起提升认知&#xff0c;感兴趣可以来围观一下&#xff09; &#x1f383;知识星球&#xff1a;【认知up吧|成长|副业】介绍 ❤️感谢大家点赞&#x1f44d;&…...

2024.4.6学习笔记

今日学习韩顺平java0200_韩顺平Java_对象机制练习_哔哩哔哩_bilibili 今日学习p315-p328 动态绑定机制 当调用方法对象的时候&#xff0c;该方法会和该对象的内存地址/运行类型绑定 当调用对象属性时&#xff0c;没有动态绑定机制&#xff0c;哪里声明&#xff0c;哪里使用 …...

2024年华为OD机试真题-查找一个有向网络的头节点和尾节点-Java-OD统一考试(C卷)

题目描述: 给定一个有向图,图中可能包含有环,图使用二维矩阵表示,每一行的第一列表示起始节点,第二列表示终止节点,如[0, 1]表示从0到1的路径。每个节点用正整数表示。求这个数据的首节点与尾节点,题目给的用例会是一个首节点,但可能存在多个尾节点。同时,图中可能含有…...

【Django开发】0到1美多商城项目md教程第5篇:短信验证码,1. 避免频繁发送短信验证码逻辑分析【附代码文档】

美多商城完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;欢迎来到美多商城&#xff01;&#xff0c;项目准备。展示用户注册页面&#xff0c;创建用户模块子应用。用户注册业务实现&#xff0c;用户注册前端逻辑。图形验证码&#xff0c;图形验证码接口设…...

云原生:应用敏捷,华为视角下的应用现代化

Gartner 也提出&#xff0c;到 2023 年&#xff0c;新应用新服务的数量将达到 5 亿&#xff0c;也即是说&#xff1a;“每个企业都正在成为软件企业”。据IDC 预测&#xff0c;到 2025 年三分之二的企业将成为多产的“软件企业”&#xff0c;每天都会发布软件版本。越来越多的企…...

【测试篇】接口测试

接口测试&#xff0c;可以用可视化工具 postman。 如何做接口测试&#xff1f;&#xff1f; 我们可以先在浏览器中随机进入一个网页&#xff0c;打开开发者工具&#xff08;F12&#xff09;。 随便找一个接口Copy–>Copy as cURL(bash) 打开postman 复制地址 进行发送。 …...

突破校园网限速:使用 iKuai 多拨分流负载均衡 + Clash 代理(内网带宽限制通用)

文章目录 1. 简介2. iKuai 部署2.1 安装 VMware2.2 安装 iKuai(1) 下载固件(2) 安装 iKuai 虚拟机(3) 配置 iKuai 虚拟机(4) 配置 iKuai(5) 配置多拨分流 2.3 测试速度 3. Clash 部署(1) 配置磁盘分区(2) 安装 Docker(3) 安装 Clash(4) 设置代理 4. 热点&#xff1a;一起瓜分互…...

03-JAVA设计模式-工厂模式详解

工厂模式 工厂设计模式是一种创建型设计模式&#xff0c;它提供了一种封装对象创建过程的机制&#xff0c;将对象的创建与使用分离。 这种设计模式允许我们在不修改客户端代码的情况下引入新的对象类型。 在Java中&#xff0c;工厂设计模式主要有三种形式&#xff1a;简单工厂…...

百度文心大模型推理成本降至1% / 马斯克起诉OpenAI |魔法半周报

我有魔法✨为你劈开信息大海❗ 高效获取AIGC的热门事件&#x1f525;&#xff0c;更新AIGC的最新动态&#xff0c;生成相应的魔法简报&#xff0c;节省阅读时间&#x1f47b; &#x1f525;资讯预览 百度文心大模型推理成本降至1%&#xff0c;与三星、荣耀等企业达成合作 马斯…...

Struts2的入门:新建项目——》导入jar包——》jsp,action,struts.xml,web.xml——》在项目运行

文章目录 配置环境tomcat 新建项目导入jar包新建jsp界面新建action类新建struts.xml,用来配置action文件配置Struts2的核心过滤器&#xff1a;web.xml 启动测试给一个返回界面在struts.xml中配置以实现页面的跳转&#xff1a;result再写个success.jsp最后在项目运行 配置环境 …...

git 标签功能操作以及回退

Git 标签功能允许开发者为特定的提交打上标签&#xff0c;以便后续能够方便地引用这些提交。标签通常用于标记重要的版本或里程碑&#xff0c;例如软件发布的版本号。与分支不同&#xff0c;标签指向的是固定的提交&#xff0c;一旦设置&#xff0c;就不能轻易更改。下面是一些…...

利用python实现文字转语音

代码如下&#xff1a; import pathlib import tkinter as tk import tkinter.ttk as ttk import tkinter.filedialog as filedialog import tkinter.messagebox as msgbox import pyttsx3class Application(tk.Tk):def __init__(self):super().__init__()self.title("文本…...

拾光坞N3 ARM 虚拟主机 i茅台项目

拾光坞N3 在Dcoker部署i茅台案例 OS&#xff1a;Ubuntu 22.04.1 LTS aarch64 cpu&#xff1a;RK3566 ram&#xff1a;2G 部署流程——》mysql——》java8——》redis——》nginx mysql # 依赖 apt update apt install -y net-tools apt install -y libaio* # 下载mysql wg…...

超短脉冲激光自聚焦效应

前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应&#xff0c;这是一种非线性光学现象&#xff0c;主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场&#xff0c;对材料产生非线性响应&#xff0c;可能…...

java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别

UnsatisfiedLinkError 在对接硬件设备中&#xff0c;我们会遇到使用 java 调用 dll文件 的情况&#xff0c;此时大概率出现UnsatisfiedLinkError链接错误&#xff0c;原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用&#xff0c;结果 dll 未实现 JNI 协…...

从零实现STL哈希容器:unordered_map/unordered_set封装详解

本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说&#xff0c;直接开始吧&#xff01; 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...

OPENCV形态学基础之二腐蚀

一.腐蚀的原理 (图1) 数学表达式&#xff1a;dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一&#xff0c;腐蚀跟膨胀属于反向操作&#xff0c;膨胀是把图像图像变大&#xff0c;而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...

中医有效性探讨

文章目录 西医是如何发展到以生物化学为药理基础的现代医学&#xff1f;传统医学奠基期&#xff08;远古 - 17 世纪&#xff09;近代医学转型期&#xff08;17 世纪 - 19 世纪末&#xff09;​现代医学成熟期&#xff08;20世纪至今&#xff09; 中医的源远流长和一脉相承远古至…...

【Go语言基础【12】】指针:声明、取地址、解引用

文章目录 零、概述&#xff1a;指针 vs. 引用&#xff08;类比其他语言&#xff09;一、指针基础概念二、指针声明与初始化三、指针操作符1. &&#xff1a;取地址&#xff08;拿到内存地址&#xff09;2. *&#xff1a;解引用&#xff08;拿到值&#xff09; 四、空指针&am…...

动态 Web 开发技术入门篇

一、HTTP 协议核心 1.1 HTTP 基础 协议全称 &#xff1a;HyperText Transfer Protocol&#xff08;超文本传输协议&#xff09; 默认端口 &#xff1a;HTTP 使用 80 端口&#xff0c;HTTPS 使用 443 端口。 请求方法 &#xff1a; GET &#xff1a;用于获取资源&#xff0c;…...

前端中slice和splic的区别

1. slice slice 用于从数组中提取一部分元素&#xff0c;返回一个新的数组。 特点&#xff1a; 不修改原数组&#xff1a;slice 不会改变原数组&#xff0c;而是返回一个新的数组。提取数组的部分&#xff1a;slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...

QT开发技术【ffmpeg + QAudioOutput】音乐播放器

一、 介绍 使用ffmpeg 4.2.2 在数字化浪潮席卷全球的当下&#xff0c;音视频内容犹如璀璨繁星&#xff0c;点亮了人们的生活与工作。从短视频平台上令人捧腹的搞笑视频&#xff0c;到在线课堂中知识渊博的专家授课&#xff0c;再到影视平台上扣人心弦的高清大片&#xff0c;音…...

游戏开发中常见的战斗数值英文缩写对照表

游戏开发中常见的战斗数值英文缩写对照表 基础属性&#xff08;Basic Attributes&#xff09; 缩写英文全称中文释义常见使用场景HPHit Points / Health Points生命值角色生存状态MPMana Points / Magic Points魔法值技能释放资源SPStamina Points体力值动作消耗资源APAction…...