百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Spring Boot(十三):Spring Boot 集成 Activiti - 快速实现工作流

ccwgpt 2024-11-01 11:32 29 浏览 0 评论

大家好,我是杰哥

上一篇文章,我们介绍了一个常用工作流框架:Activiti,并通过实际例子,了解了它的工作原理与使用步骤。实际上,Spring Boot 也集成了这个框架,从而能够让我们轻松实现项目中的工作流

需要说明的是,Spring Boot 集成的 Activiti 依赖 activiti-spring-boot-starter中 ,同时也集成了 Security 框架,所以说它可谓是自带权限控制功能,这一点从设计上出发,的确是比较人性化的了,毕竟实际需求中的工作流,往往涉及到特定用户的操作,比如有些工作流只能由某些人发起或者查看,而有些流程,只能由某些人进行审批等

当然你也可以考虑将 Security 换成其他权限控制的框架,或者在初学时,直接将 Security 去掉即可(具体方式网上都有的,可以参考)

据我了解,很多 Spring Boot 框架的 WEB 项目中,往往就是直接采用这两个方式一起进行权限管理的工作流控制的,所以,为了完整起见,我们今天就直接来学习 ActivitiSpring Security 结合来实现工作流控制的方法与步骤

本次演示信息如下(均为当前最新版本):

  • spring-boot-starter-parent 依赖版本:2.7.0
  • activiti-spring-boot-starter 依赖版本:7.1.0.M6
  • spring-boot-starter-security 依赖版本:2.7.0

一 security 相关

对于 Spring Boot 集成 Security 的步骤,我们在文章 Spring Boot(十一):Spring Security 实现权限控制 中已经很详细地介绍过,可以跳过去大概阅读一番,以便更好地入门此次的知识

1 引入 spring-boot-starter-security 依赖

虽然 activiti-spring-boot-starter 也包含了 Security 的依赖,但是在 7.1.0.M6 版本里,却不能够直接使用最新版本的权限配置方式(自定义 SecurityFilterChain Bean 来实现权限配置),所以我这里便额外引入了 spring-boot-starter-security 的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2 表结构

遵从 Security 所采取的权限访问控制方案:RABC -基于角色的权限访问控制(Role-Based Access Control),我们建立如下 5 张表

建表语句如下:

/*
 Navicat Premium Data Transfer
 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 50733
 Source Host           : localhost:3306
 Source Schema         : activiti_spring
 Target Server Type    : MySQL
 Target Server Version : 50733
 File Encoding         : 65001
 Date: 18/07/2022 15:55:30
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `url` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  `pid` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of permission
-- ----------------------------
BEGIN;
INSERT INTO `permission` VALUES (1, '/user/common', 'common', NULL, 0);
INSERT INTO `permission` VALUES (2, '/user/admin', 'admin', NULL, 0);
INSERT INTO `permission` VALUES (3, '/process/*', 'activiti', NULL, 0);
COMMIT;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role
-- ----------------------------
BEGIN;
INSERT INTO `role` VALUES (1, 'USER');
INSERT INTO `role` VALUES (2, 'ADMIN');
INSERT INTO `role` VALUES (3, 'ACTIVITI_USER');
COMMIT;
-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(11) NOT NULL,
  `permission_id` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role_permission
-- ----------------------------
BEGIN;
INSERT INTO `role_permission` VALUES (1, 1, 1);
INSERT INTO `role_permission` VALUES (2, 2, 1);
INSERT INTO `role_permission` VALUES (3, 2, 2);
INSERT INTO `role_permission` VALUES (4, 3, 3);
COMMIT;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
BEGIN;
INSERT INTO `user` VALUES (1, 'user', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
INSERT INTO `user` VALUES (2, 'admin', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
INSERT INTO `user` VALUES (3, 'Jack', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
INSERT INTO `user` VALUES (4, 'Marry', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
COMMIT;
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) NOT NULL,
  `role_id` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user_role
-- ----------------------------
BEGIN;
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 2, 3);
INSERT INTO `user_role` VALUES (5, 3, 3);
INSERT INTO `user_role` VALUES (6, 4, 3);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

其实就是直接拿来文章 Spring Boot(十一):Spring Security 实现权限控制 中的表结构,并根据我们本次对于 Activiti 项目的演示需要,在之前的基础上,分别增加了两个用户:JackMarry,以及他们对应的新角色:ACTIVITI_USER 以及新 url/process/* 的访问权限。当然,根据 RABC 的方式,也配置了其对应的用户角色、角色权限的关系

3 配置类

采用 Spring Boot Security 的最新方式实现,即:

  • 如果想要配置过滤器链,可以通过自定义 SecurityFilterChain Bean 来实现
  • 如果想要配置 WebSecurity,可以通过 WebSecurityCustomizer Bean 来实现
@Configuration
@Slf4j
public class SecurityConfig {
    @Resource
    private PermissionMapper permissionMapper;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
                authorizeRequests = http.csrf().disable().authorizeRequests();
//        方式二:配置来源于数据库
//         1.查询到所有的权限
        List<Permission> allPermission = permissionMapper.findAllPermission();
//         2.分别添加权限规则
        allPermission.forEach((p -> {
            authorizeRequests.antMatchers(p.getUrl()).hasAnyAuthority(p.getName()) ;
        }));
        authorizeRequests.antMatchers("/**").fullyAuthenticated()
                .anyRequest().authenticated().and().formLogin();
        return http.build();
    }
    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        return web -> {
            web.ignoring().antMatchers("/css/**");
            web.ignoring().antMatchers("/js/**");
            web.ignoring().antMatchers("/img/**");
            web.ignoring().antMatchers("/plugins/**");
            web.ignoring().antMatchers("/login.html");
        };
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

当前用户所拥有的的权限等信息,则通过实现 UserDetailsService 接口,并重写其方法 loadUserByUsername() 来获取:

@Slf4j
@Service
public class UserServiceImpl implements UserService  {
    @Resource
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.根据用户名称查询到user用户 
        User userDetails = userMapper.findByUsername(username);
        if (userDetails == null) {
            return null;
        }
        // 2.查询该用户对应的权限 
        List<Permission> permissionList = userMapper.findPermissionByUsername(username);
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        permissionList.forEach((a) -> grantedAuthorities.add(new SimpleGrantedAuthority(a.getName())));
        log.info(">>permissionList:{}<<", permissionList);
        // 设置权限 
        userDetails.setAuthorities(grantedAuthorities);
        return userDetails;
    }
}

到此,就完成了 Security 的相关配置,接下来,让我们正式进入 Spring Boot 集成 Activiti 的实战环节

二 进入 Spring Boot 集成 Activiti 的实战

需要预先说明的是,

Activiti 提供了几个 Service 类,用来管理工作流,常用的有以下四项:

  • 1)RepositoryService:提供流程定义和部署等功能。比如说,实现流程的的部署、删除,暂停和激活以及流程的查询等功能
  • 2)RuntimeService:提供了处理流程实例不同步骤的结构和行为。包括启动流程实例、暂停和激活流程实例等功能
  • 3)TaskService:提供有关任务相关功能的服务。包括任务的查询、删除以及完成等功能
  • 4)HistoryService:提供 Activiti 引擎收集的历史记录信息服务。主要用于历史信息的查询功能
  • 还有以下两项:
  • 1)ManagementService:job 任务查询和数据库操作
  • 2)DynamicBpmnService:无需重新部署就能修改流程定义内容

Spring Boot 集成 Activiti 实现工作流功能,也主要是采用这些 Service 所提供的 相应的 API 来实现的

1 配置文件

application.yml 文件的配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/activiti_spring?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  activiti:
#    flase:       默认值。activiti在启动时,会对比数据库表中保存的版本,如果没有表或者版本不匹配,将抛出异常。(生产环境常用)
#    true:        activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创建。(开发时常用)
#    create_drop: 在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)。(单元测试常用)
#    drop-create: 在activiti启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)。
    database-schema-update: true
    #默认不生成历史表,这里开启
    db-history-used: true
    #历史登记
#    none: 不记录历史流程,性能高,流程结束后不可读取
#    activity: 归档流程实例和活动实例,流程变量不同步
#    audit: 默认值,在activiti基础上同步变量值,保存表单属性
#    full: 性能较差,记录所有实例和变量细节变化,最完整的历史记录,如果需要日后跟踪详细可以开启full(一般不建议开启)
    history-level: full
    deployment-mode:  never-fail # 关闭 SpringAutoDeployment

除了数据库的配置信息以外,在我们的 demo 演示环境,Activiti 的配置如下:

  • database-schema-update 配置为 true,即每次项目启动,都会对数据库进行更新操作,如果表不存在,则自动创建
  • db-history-used 配置为 true 由于默认是不生成历史表的,配置为 true,表示需要生成
  • history-level 配置 为 full,表示记录最完整的历史记录
  • deployment-mode 配置为 never-fail ,即关闭掉 SpringAutoDeployment。如果不关闭,每次重新启动项目的时候,总是会在 ACT_RE_DEPLOYMENT 自动创建一个名为 SpringAutoDeployment 工作流记录。但是在开发阶段,需要经常重启项目,久而久之就会导致 ACT_RE_DEPLOYMENT 的记录越来越大了

2 表结构创建

首次启动时,根据以上的配置,Activiti 会进行表结构的自动创建,以下是项目初次启动时的日志

由于上述我们配置了开启历史表的使用开关:db-history-used: true

所以,这里除了创建 Activiti非历史表结构以外,还创建了其历史表结构

项目启动之后,会发现,我们所配置的 activiti_spring 数据库中,便增加了 25ACT 开头的表

备注:这些表的说明,也可以参考上一篇文章:Activiti工作流(一):OA 上的那些请假流程如何快速实现呢?

3 流程部署

总得来说,可以分为两种方式:自动部署手动部署

自动部署,实际上是 Spring Boot 会在启动项目时,会自动部署项目目录下的 processes 文件夹中的所有流程,而没有在这个目录下的所有流程定义文件,则需要手动方式进行部署了

我们先来看一下自动部署的方式

1)自动部署

a) 创建流程定义

备注:具体创建方式本文不再赘述,大家可以参考上一篇文章:Activiti工作流(一):OA 上的那些请假流程如何快速实现呢?

resources 目录下,创建一个 processes 文件夹,在该文件夹下分别创建两个简单的流程:

  • 请假申请(leaveApplication)
  • 出差申请(myEvection)

其中,请假流程定义了两个步骤:

创建申请审批申请,其负责人分别定义为 ${assignee0} 和 ${assignee1}

b)启动项目

项目启动时,完成 process 目录下的流程文件的自动部署

观察日志,可以看到我们预先创建好的两个流程定义,均被自动部署

我们知道,Activiti 的逻辑,实际上是通过对一系列表的操作,来实现工作流的控制。而流程的部署,则是把我们所创建的流程定义文件,真正与数据库的表关联起来

那么,我们来看看看流程定义表:ACT_RE_PROCDEF

发现表中已有相应的两条记录,验证了这两个流程的确完成了自动部署

当然,如果定义好的流程文件没有在 processes 文件夹下,就需要我们手动部署

2)手动部署

可以通过如下方式指定 bpmn 文件的目录:filePath 进行部署

repositoryService.createDeployment()
.addClasspathResource(filePath)
.deploy();

由于是对流程定义的操作,所以我们采用 RepositoryService 这个服务进行部署

4 查询流程定义

查询流程定义,依旧是对流程定义的操作,所以采用 RepositoryService 服务进行查询,具体代码与上篇文章中的查询方式类似

/**
 * 查询流程
 * @param key
 * @return
 */
@GetMapping(value = {"/list/{key}","/list"})
public ResponseResult getProcessList(@PathVariable(name = "key",required = false) String key) {
    ProcessDefinitionQuery definitionQuery = repositoryService.createProcessDefinitionQuery();
    List<ProcessDefinition> definitionList;
    if (key!=null){
        definitionList = definitionQuery
                .processDefinitionKey(key)
                .list();
    }
   definitionList = definitionQuery.list();
    //提取所有的流程名称
    List<String> processList = new ArrayList<>();
    for (ProcessDefinition processDefinition : definitionList) {
        processList.add(processDefinition.getName());
    }
    return ResponseResult.getSuccessResult(processList);
}

采用 Jack 用户登录之后,带上其 Cookie 头参数,进行如下访问

成功访问

5 启动流程

本次实战,我们来启动请假申请(leaveApplication)流程,由于是运行时状态,所以采用 RuntimeServicestartProcessInstanceByKey(key,map) 方法进行启动,具体代码如下:

/**
 * 启动流程定义(由流程定义-》流程实例)
 * @param key
 * @return
 */
@PostMapping("start/{key}")
public ResponseResult startProcess(@PathVariable(name = "key") String key){
    Map<String,Object> map = new HashMap<>();
    map.put("assignee0","Jack");
    map.put("assignee1","Marry");
    //启动 key 标识的流程定义,并指定 流程定义中的两个参数:assignee0和assignee1
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key,map);
    ResponseResult result = ResponseResult.getSuccessResult(processInstance.getProcessDefinitionName());
    log.info("流程实例的内容:{}",processInstance);
    return result;
}

测试启动:

指定参数 keyleaveApplication ,进行启动操作

访问成功后,看看表中的数据有没有变化呢,查看 ACT_RU_TASK 表,该表中新增了一条记录,为创建申请,负责人为 Jack,表示我们的这个流程已经成功启动,目前进行到了创建申请这一任务了,其负责人为 Jack

当前登录的用户就是 Jack,此时我们通过接口 /process/task/list 接口来查看 Jack 的任务列表

发现目前在 Jack 名下的确存在一个待办任务:创建申请

6 Jack -创建申请

对任务的完成,依旧是采用 TaskService 进行,具体代码如下:

/**
 * 完成任务
 * @param key
 * @param assigne
 * @return
 */
@PostMapping("complete")
public ResponseResult doTask(@RequestParam(name = "key") String key,@RequestParam(name = "assignee")String assigne){
    List<Task> tasks = taskService.createTaskQuery().processDefinitionKey(key)
            .taskAssignee(assigne)
            .list();
    if (tasks!=null && tasks.size()>0){
        for (Task task : tasks) {
            log.info("任务名称:{}",task.getName());
            taskService.complete(task.getId());
            log.info("{},任务已完成",task.getName());
        }
    }
    return ResponseResult.getSuccessResult(null);
}

Jack 来完成“创建申请”的待办任务,分别指定 keyleaveApplicationassigneeJack ,进行任务的完成操作:

访问成功之后,我们再来查看 ACT_RU_TASK(当前任务表)

按照预期,此时正要处理的任务为审批申请,并且负责人为 Marry

当前我们采用 Jack 登录的这个 Cookie 来进行查询,发现 task 列表已经变成了空的

7 Marry -审批申请

1)登录

访问 localhost:8080/logout 登出,会跳转到登录页面

此时,采用 Marry 进行登录

2)查看其名下的任务列表

发现的确存在一个任务:审批申请

3)完成任务

指定 keyleaveApplicationassigneeMarry,访问接口 /process/complete

接口返回成功 此时,再次查询我们的 ACT_RU_TASK 表,会发现我们已经没有了正在处理的任务,说明我们已完成了所有的任务

再次查看历史表 ACT_HI_TASKINST,会看到我们所处理过的两个任务

好了,走到这一步,我们便成功完成了 Spring Boot 集成 Activiti 进行流程定义、流程部署、流程启动、任务完成等工作流管理的一系列操作

总结

今天带领大家完成了 Spring Boot 集成 Activiti 的功能,网上目前可能很难找到类似的案例,因为我们这里采用的 Spring BootActiviti 以及 Security 均是当前最新的版本

Spring BootActiviti 集成,使得我们不仅可以继续使用 Activiti 本身所提供的简易的 API, 而且 Spring Boot 还提供了很多自动化的东西,包括启动时自动创建表结构,自动完成 processes 目录下的流程部署操作等等

真心觉得 Spring Boot 的衍生的确是程序员的福音呢,它使得技术越来越简单化,让我们的学习与使用成本大大降低了呢

文章演示代码地址:https://github.com/helemile/Spring-Boot-Notes/tree/master/activiti/activiti


嗯,就这样。每天学习一点,时间会见证你的强大~


欢迎大家关注我们的公众号,一起持续性学习吧~



往期精彩回顾


总结复盘

架构设计读书笔记与感悟总结

带领新人团队的沉淀总结

复盘篇:问题解决经验总结复盘

网络篇

网络篇(四):《图解 TCP/IP》读书笔记

网络篇(一):《趣谈网络协议》读书笔记(一)

事务篇章

事务篇(四):Spring事务并发问题解决

事务篇(三):分享一个隐性事务失效场景

事务篇(一):毕业三年,你真的学会事务了吗?

Docker篇章

Docker篇(六):Docker Compose如何管理多个容器?

Docker篇(二):Docker实战,命令解析

Docker篇(一):为什么要用Docker?

..........

SpringCloud篇章

Spring Cloud(十三):Feign居然这么强大?

Spring Cloud(十):消息中心篇-Kafka经典面试题,你都会吗?

Spring Cloud(九):注册中心选型篇-四种注册中心特点超全总结

Spring Cloud(四):公司内部,关于Eureka和zookeeper的一场辩论赛

..........

Spring Boot篇章

Spring Boot(十二):陌生又熟悉的 OAuth2.0 协议,实际上每个人都在用

Spring Boot(七):你不能不知道的Mybatis缓存机制!

Spring Boot(六):那些好用的数据库连接池们

Spring Boot(四):让人又爱又恨的JPA

SpringBoot(一):特性概览

..........

翻译

[译]用 Mint 这门强大的语言来创建一个 Web 应用

【译】基于 50 万个浏览器指纹的新发现

使用 CSS 提升页面渲染速度

WebTransport 会在不久的将来取代 WebRTC 吗?

.........

职业、生活感悟

你有没有想过,旅行的意义是什么?

程序员的职业规划

灵魂拷问:人生最重要的是什么?

如何高效学习一个新技术?

如何让自己更坦然地度过一天?

..........

相关推荐

如何让老师看完文章后还啧啧称奇?满分作文有框架,这3点是关键

历年来语文考试中,作文的分数都占着相当大的一个比例,同时作文也是最容易拉开差距的一个模块。别人拿满分,而你却只有20分左右,分数的差距就是这样拉开的。作文想拿很高的分数却是不容易,但不是完全不可能的事...

小学作文写作技巧和方法,万能公式框架法。家长收藏

小学语文老师用心整理,将写作框架编成万能公式。作文的写作是语文学习中的重要一环,从小学到初中再到高中,作文一直是语文考试中占分比重最高的部分。小学阶段的语文写作相对来说比较简单,主要是打基础,但是很多...

如何用爆款改写技巧提升文章吸引力结构重塑:打破原文框架悬念前

如何用爆款改写技巧提升文章吸引力?结构重塑:打破原文框架悬念前置法-在开头设置悬念或提出反常识的结论,吸引读者注意力。例如,将“接纳不完美是治愈的开始”改写为“天天逼自己当完美超人?别杠了!生活本就...

守护袁昆:是否有必要按框架去写文章,拍摄剪辑视频?

(文/守护袁昆)如今不管是写文章还是剪辑视频,越来越多的朋友喜欢用框架、用脚本,作为互联网创作者,我们是否有必要按框架去写文章,拍摄剪辑短视频呢?其实在内容创作过程中,是否使用框架始终是一个充满争议的...

揭秘!爆款文章的秘密:让读者无法抗拒的文章框架

说说我自己一开始写文章都会犯一个毛病,文章,通常是想到哪里,写到哪里,“管不住字儿”。这样往往会出现以下问题绊住我们继续写下去1、很容易,写着就跑偏了,最终出来的成品和最初的设想偏离很大2、会写得很慢...

想要写出逻辑清晰的文章,你需要掌握哪些写作结构

想要写出好文章,就必须要了解文章的结构和框架。一篇文章结构清晰,读者就很容易跟上作者的思路,看出文章的重点内容。如果你对新媒体的文章有进行过研究,你就会发现很多公众号的文章结构都是类似的。所以你需要掌...

写作结构拆解:从选题到框架,如何让文章说服力翻倍?

你有没有想过,为什么有些文章能轻松获得很高的阅读量,而你的文章却始终无人问津?其实,写出爆款文章并没有想象中那么难。关键在于选择一个吸引人的主题,并用一个清晰的写作框架,通过2-3个有力的子观点支撑...

申论怎么写?结构怎么弄?这篇文章是最基本的好文章框架

写作总被吐槽逻辑混乱?三步搭建框架法,新手也能写出漂亮文章

一、结构决定论:信息传递的桥梁写作者和读者之间始终存在一道隐形的鸿沟。作者脑海中的想法如同一棵枝繁叶茂的大树,但直接倾倒给读者时,往往只剩下零散的枝叶,信息在传达过程中的丢失,作者输出的和读者读到的不...

如何搭建文章框架:新手写作者很有必要看

#头条深一度-深度阅读计划#见面好呀,我是潼臻~37岁,边上班边带娃的二胎妈妈藏起生活里的琐碎,期望你我都能遇到更好的自己~~~~~~~~~~~~~~~~~~果然多读书是可以真切学习到有用的东西最近把...

模型上下文协议(MCP)的可视化向导

最近,模型上下文协议(MCP)引起了广泛关注。你一定听说过它。今天,让我们来了解一下它是什么。直观地说,MCP就像是AI应用的USB-C接口。正如USB-C提供了一种标准化的方式,用于将...

97个人放一页PPT!用对Smartart架构图直接开挂!

从讯飞出差回来的路上,在高铁上看到一条微博,关于红楼梦人物的思维导图:下面很多家长说,帮助很大,能帮助上学的孩子梳理清人物关系,我看了下,清楚是清楚,但真的不太好看!作为一名PPT博主,我就顺带在高...

技术架构规范与实践(二)架构设计示例

1.逻辑架构1.1领域概念1.2宏观应用架构1.3宏观流程1.4微服务拆分与分层2.技术架构3.开发架构3.1后端技术栈分类名称版本描述后端框架/组件JavaJdk8(openjdk:8u342)后...

倾斜柱模板安装加固

1、适用范围:呈梯形逐层向内侧倾斜的框架柱。2、工艺流程:定位放线-配模-校正梁位置-安装加固。3、工艺方法:(1)定位放线首先现场技术管理人员对每颗不同标高的梁底、梁中边线及200mm控制线进行平面...

地基与基础工程、主体工程节点构造

#去班味吧#桩头凿除环切法工艺说明:1、根据桩头预留长度(深入承台10cm)放样桩顶标高,施工人员根据测量结果在基桩上用红油漆标注环切线;2、在切割线以上部分桩底,人工用钢钎打入桩底约15cm,打入时...

取消回复欢迎 发表评论: