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

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

ccwgpt 2024-11-01 11:32 26 浏览 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 吗?

.........

职业、生活感悟

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

程序员的职业规划

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

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

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

..........

相关推荐

用Deepseek扩写土木工程毕业论文实操指南

用Deepseek扩写毕业论文实操指南一、前期准备整理现有论文初稿/提纲列清楚论文核心框架(背景、现状、意义、方法、数据、结论等)梳理好关键文献,明确核心技术路线二、Deepseek扩写核心思路...

985学霸亲授,DeepSeek也能绘6大科研图表,5分钟就出图

在实验数据处理中,高效可视化是每个科研人的必修课。传统绘图软件操作复杂、耗时费力,而智能工具DeepSeek的出现彻底改变了这一现状。本文将详解如何用DeepSeek一键生成六大科研常用图表,从思维导...

AI写论文刷屏?大学生正在丢掉的思考力

一、宿舍深夜:当论文变成"Ctrl+C+V"凌晨两点的大学宿舍,小王对着电脑屏幕叹气。本该三天前开始写的近代史论文,此刻还一片空白。他熟练打开某AI写作网站,输入"论五四运动的...

Grok在辅助论文写作上能不能既“聪明”又“可怕”?!

AcademicIdeas-学境思源AI初稿写作随着人工智能技术的飞速发展,论文写作这一学术任务正迎来新的助力。2025年2月18日,美国xAI公司推出了备受瞩目的Grok3模型,其创始人埃隆·...

大四论文沟通场景!音频转文字难题听脑AI来化解

大四学生都知道,写论文时和导师沟通修改意见,简直是“过关斩将”。电话、语音沟通完,想把导师说的修改方向、重点要求记下来,麻烦事儿可不少。手写记不全,用普通录音转文字工具,转完还得自己慢慢找重点,稍不注...

论文写作 | 技术路线图怎么画?(提供经典优秀模板参考)

技术路线图是一种图表或文字说明,用于描述研究目标、方法和实施计划。它展示了研究的整体框架和步骤,有助于读者理解研究的逻辑和进展。在课题及论文中,技术路线图是常见的一部分,甚至是一个类似心脏一样的中枢器...

25年信息系统项目管理师考试第2批论文题目写作建议思路框架

25年信息系统项目管理师考试第2批论文题目写作建议思路框架--马军老师

微信购物应尽快纳入法律框架(微信购物管辖)

符向军近日,甘肃省工商行政管理局发布《2016年上半年信息分析报告》。报告显示,微信网购纠纷迅猛增长,网络购物投诉呈上升趋势。投诉的主要问题有出售的商品质量不过关、消费者通过微信付款后对方不发货、购买...

泛珠三角区域网络媒体与腾讯微信签署《战略合作框架协议》

新海南客户端、南海网7月14日消息(记者任桐)7月14日上午,参加第四届泛珠三角区域合作网络媒体论坛的区域网络媒体负责人及嘉宾一行到腾讯微信总部座谈交流,并签署《战略合作框架协议》(以下简称《框架协...

离线使用、植入微信-看乐心Mambo手环如何打破框架

从2014年开始智能手环就成功进入人们的生活,至今已经演变出数据监测、信息推送、心率监测等诸多五花八门的功能,人们选择智能手环并不指望其能够改变身体健康情况,更多的是通过数据来正视自身运动情况和身体健...

微信私域电商运营策略与框架(微信私域怎么做)

...

华专网络:如何零基础制作一个网站出来?

#如何零基础制作一个网站出来?#你是不是觉得网站建设很复杂,觉得自己是小白,需求不明确、流程搞不懂、怕被外包公司坑……这些问题我都懂!今天华专网络就用大白话给你捋清楚建站的全流程,让你轻松get网站制...

WAIC2024丨明日上午9点,不见不散!共同探讨智能社会与全球治理框架

大咖云集,硕果闪耀WAIC2024世界人工智能大会智能社会论坛将于7月5日9:00-12:00与你相约直播间WAIC2024上海杨浦同济大学哔哩哔哩多平台同步直播探讨智能社会与全球治理框架WAIC...

约基奇:森林狼换来戈贝尔时大家都在嘲笑 他们的阵容框架很不错

直播吧5月4日讯西部季后赛半决赛,掘金将迎战森林狼,约基奇赛前接受采访。约基奇说道:“当蒂姆-康纳利(森林狼总经理、前掘金总经理&曾选中约基奇)做了那笔交易(换来戈贝尔)时,每个人都在嘲笑他...

视频号带货为什么一个流量都没有?顶级分析框架送给你

视频号带货为什么一个流量都没有?遇到问题,一定是步步来分析内容,视频号带货一个流量都没有,用另外一个意思来讲,就可以说是零播放。为什么视频号带货一个流量都没有?跟你说再多,都不如来个分析框架。1、是否...

取消回复欢迎 发表评论: