关于后端异步+前端进度条的简单实现

举个例子,现在有一个接口,就拿若依的批量给用户授权角色接口来说。

原来的接口是这样的

bash 复制代码
    /**
     * 批量选择用户授权
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.GRANT)
    @PutMapping("/authUser/selectAll")
    public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)
    {
        List<Long> userIdList = Arrays.stream(userIds).collect(Collectors.toList());
		sysRoleService.insertAuthUsers(roleId, Arrays.asList(userIdList));
        return AjaxResult.success(taskId);
    }

直接在前端获取到用户id然后就直接在sysRoleService.insertAuthUsers方法中进行数据库操作了。

如果前端用户数据量太大的话,这样返回会很慢,导致前端加载时间长,现在简单的改造这个方法。

首先写一个线程池,执行异步的时候调用。

bash 复制代码
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig implements AsyncConfigurer {

    private static ThreadPoolTaskExecutor executor;
    //获取单前机器cpu数量
    private static final int cpu = Runtime.getRuntime().availableProcessors();
    //设置核心线程数
    private static final int corePoolSize = cpu;
    //设置最大线程数
    private static final int maxPoolSize = 1000;
    //设置线程空闲时间(秒)
    private static final int keepAliveTime = 60;
    //设置主线程等待时间
    private static final int awaitTerminationSeconds = 120;
    //缓存队列数
    private static final int queueCapacity = 200;
    //线程池前缀名
    private static final String threadNamePrefix = "ryTaskExecutor-";

    public static ThreadPoolTaskExecutor getExecutor() {
        return executor;
    }

    @Bean("ryTaskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        //等待队列大小
        executor.setQueueCapacity(queueCapacity);
        //空闲时间
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setAwaitTerminationSeconds(awaitTerminationSeconds);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // RejectedExecutionHandler:当pool已经达到max-size的时候,如何处理新任务
        // CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

异步任务进度条

bash 复制代码
public class AsyncTaskProgress implements Serializable {
    private static final long serialVersionUID = 1L;

    private String status; // 任务状态:PENDING-进行中,SUCCESS-成功,FAILURE-失败
    private Integer progress; // 任务进度:0-100
    private String result; // 任务结果(仅在状态为SUCCESS时有用)
    private String error; // 任务失败原因(仅在状态为FAILURE时有用)

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public Integer getProgress() {
        return progress;
    }

    public void setProgress(Integer progress) {
        this.progress = progress;
    }

    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }
}

新建异步任务实现类

bash 复制代码
@Service
public class AsyncTaskService {

    private static final String REDIS_PREFIX = "ASYNC_TASK_"; // Redis中保存异步任务进度对象的前缀
    @Autowired
    public RedisTemplate redisTemplate;
	
	//生成异步任务的id,用于前端查询改任务的进度使用
    public String generateTaskId() {
        return UUID.randomUUID().toString();
    }
}

开启异步任务

bash 复制代码
    public void startAsyncTask(Long roleId, List<Long> userIds, String taskId) {
        CompletableFuture.runAsync(() -> {
            AsyncTaskProgress taskProgress = new AsyncTaskProgress();
            int size = userIds.size();
            int total = 0;
            taskProgress.setStatus("PENDING"); //任务状态:进行中
            taskProgress.setProgress(total);//任务进度: 0
            taskProgress.setResult(null); //任务结果:空
            taskProgress.setError(null); //任务错误:空

            //在此进行异步任务的业务处理
            try {
                for (Long userId : userIds) {
                    Thread.sleep(1000);//停一秒
                    total++;
                    int percent = (int) (total * 100.0 / size);
                    sysRoleService.insertAuthUsers(roleId, Arrays.asList(userId));
                    taskProgress.setProgress(percent);
                    saveAsyncTaskProgress(taskId,taskProgress);
                }
                taskProgress.setProgress(100);
                taskProgress.setResult("Task result"); // 任务结果
                taskProgress.setStatus("SUCCESS"); // 任务状态:成功
                saveAsyncTaskProgress(taskId,taskProgress);
            } catch (Exception e) {
                taskProgress.setError(e.getMessage());
                taskProgress.setStatus("FAILURE");
                saveAsyncTaskProgress(taskId,taskProgress);
            }
        }, AsyncThreadPoolConfig.getExecutor());
    }
     //保存进度条进度
    private void saveAsyncTaskProgress(String taskId, AsyncTaskProgress taskProgress) {
        redisTemplate.opsForValue().set(REDIS_PREFIX + taskId, taskProgress, 30, TimeUnit.MINUTES); // 将任务进度对象保存到Redis中,有效期30分钟



    public AsyncTaskProgress getAsyncTaskProgress(String taskId) {
        AsyncTaskProgress taskProgress = (AsyncTaskProgress) redisTemplate.opsForValue().get(REDIS_PREFIX + taskId); // 获取任务进度对象
        if (taskProgress == null) {
            throw new RuntimeException("无法获取异步任务进度,可能已经过期或不存在!"); // 如果获取不到,抛出异常
        }
        return taskProgress;
    }
    }

角色授权用户接口改成这样

bash 复制代码
    /**
     * 批量选择用户授权
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.GRANT)
    @PutMapping("/authUser/selectAll")
    public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)
    {
        List<Long> userIdList = Arrays.stream(userIds).collect(Collectors.toList());
        roleService.checkRoleDataScope(roleId);
        String taskId = asyncTaskService.generateTaskId();
        asyncTaskService.startAsyncTask(roleId, userIdList,taskId);
        return AjaxResult.success(taskId);
    }

此时还需要写一个根据任务id获取进度条的接口以供前端轮询。

bash 复制代码
    @GetMapping("/progress/{taskId}")
    public AsyncTaskProgress getAsyncTaskProgress(@PathVariable("taskId") String taskId) {
        return asyncTaskService.getAsyncTaskProgress(taskId);
    }

前端改造

在授权页面加上一个进度条,这里直接用element-ui的组件。

bash 复制代码
<el-progress v-if="taskStatus" :percentage="progress"></el-progress>

执行保存操作

bash 复制代码
    /** 选择授权用户操作 */
    handleSelectUser() {
      const roleId = this.queryParams.roleId;
      const userIds = this.userIds.join(",");
      if (userIds == "") {
        this.$modal.msgError("请选择要分配的用户");
        return;
      }
      this.taskStatus = true;
      //这个方法就是调用/authUser/selectAll接口
      authUserSelectAll({ roleId: roleId, userIds: userIds }).then(res => {
        // this.$modal.msgSuccess(res.msg);
        // if (res.code === 200) {
        //   this.visible = false;
        //   this.$emit("ok");
        // }
        this.taskId = res.msg;
        this.sleep(1000);
        const pollTaskInterval = setInterval(async () => {
        //这个方法就是调用/progress/{taskId}接口
          getProgress(this.taskId).then(res => {
            console.log(res)
            const taskProgress = res;
            this.progress = taskProgress.progress;
          if (taskProgress.status === 'SUCCESS') {
            this.visible = false;
            this.taskProgress = false;
            this.$emit("ok");
            this.progress = 0;
            clearInterval(pollTaskInterval) // 停止轮询
          } else if (taskProgress.status === 'FAILURE') {
            this.taskStatus = 'FAILURE' // 将任务状态设置为失败
            this.taskError = taskProgress.error // 将失败原因保存到状态中
            clearInterval(pollTaskInterval) // 停止轮询
          }
          })
        }, 1000) // 每1秒轮询一次任务进度
      });
    },

完整的前端页面代码

bash 复制代码
<template>
  <!-- 授权用户 -->
  <el-dialog title="选择用户" :visible.sync="visible" width="800px" top="5vh" append-to-body>
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
      <el-form-item label="用户名称" prop="userName">
        <el-input
          v-model="queryParams.userName"
          placeholder="请输入用户名称"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="手机号码" prop="phonenumber">
        <el-input
          v-model="queryParams.phonenumber"
          placeholder="请输入手机号码"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-row>
      <el-table @row-click="clickRow" ref="table" :data="userList" @selection-change="handleSelectionChange" height="260px">
        <el-table-column type="selection" width="55"></el-table-column>
        <el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
        <el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
        <el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
        <el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
        <el-table-column label="状态" align="center" prop="status">
          <template slot-scope="scope">
            <dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
          </template>
        </el-table-column>
        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
          <template slot-scope="scope">
            <span>{{ parseTime(scope.row.createTime) }}</span>
          </template>
        </el-table-column>
      </el-table>
      <pagination
        v-show="total>0"
        :total="total"
        :page.sync="queryParams.pageNum"
        :limit.sync="queryParams.pageSize"
        @pagination="getList"
      />
    </el-row>
    <div slot="footer" class="dialog-footer">
      <el-button type="primary" @click="handleSelectUser">确 定</el-button>
      <el-button @click="visible = false">取 消</el-button>
    </div>

    <el-progress v-if="taskStatus" :percentage="progress"></el-progress>
  </el-dialog>
</template>

<script>
import { unallocatedUserList, authUserSelectAll, getProgress } from "@/api/system/role";
export default {
  dicts: ['sys_normal_disable'],
  props: {
    // 角色编号
    roleId: {
      type: [Number, String]
    }
  },
  data() {
    return {
      // 遮罩层
      visible: false,
      // 选中数组值
      userIds: [],
      // 总条数
      total: 0,
      // 未授权用户数据
      userList: [],
      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        roleId: undefined,
        userName: undefined,
        phonenumber: undefined
      },
      taskId:"",
      progress:0,
      taskStatus: false,
    };
  },
  methods: {
    // 显示弹框
    show() {
      this.queryParams.roleId = this.roleId;
      this.getList();
      this.visible = true;
    },
    clickRow(row) {
      this.$refs.table.toggleRowSelection(row);
    },
    // 多选框选中数据
    handleSelectionChange(selection) {
      this.userIds = selection.map(item => item.userId);
    },
    // 查询表数据
    getList() {
      unallocatedUserList(this.queryParams).then(res => {
        this.userList = res.rows;
        this.total = res.total;
      });
    },
    /** 搜索按钮操作 */
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    /** 重置按钮操作 */
    resetQuery() {
      this.resetForm("queryForm");
      this.handleQuery();
    },
    /** 选择授权用户操作 */
    handleSelectUser() {
      const roleId = this.queryParams.roleId;
      const userIds = this.userIds.join(",");
      if (userIds == "") {
        this.$modal.msgError("请选择要分配的用户");
        return;
      }
      this.taskStatus = true;
      authUserSelectAll({ roleId: roleId, userIds: userIds }).then(res => {
        // this.$modal.msgSuccess(res.msg);
        // if (res.code === 200) {
        //   this.visible = false;
        //   this.$emit("ok");
        // }
        this.taskId = res.msg;
        this.sleep(1000);
        const pollTaskInterval = setInterval(async () => {
          getProgress(this.taskId).then(res => {
            console.log(res)
            const taskProgress = res;
            this.progress = taskProgress.progress;
          if (taskProgress.status === 'SUCCESS') {
            this.visible = false;
            this.taskProgress = false;
            this.$emit("ok");
            this.progress = 0;
            clearInterval(pollTaskInterval) // 停止轮询
          } else if (taskProgress.status === 'FAILURE') {
            this.taskStatus = 'FAILURE' // 将任务状态设置为失败
            this.taskError = taskProgress.error // 将失败原因保存到状态中
            clearInterval(pollTaskInterval) // 停止轮询
          }
          })
        }, 1000) // 每1秒轮询一次任务进度
      });
    },
    sleep(numberMillis) {
      var now = new Date();
      var exitTime = now.getTime() + numberMillis;
      while (true) {
          now = new Date();
          if (now.getTime() > exitTime)
            return true;
      }
    }
  }
};
</script>
相关推荐
y25081 分钟前
《Object类》
java·开发语言
曙曙学编程2 分钟前
初级数据结构——树
android·java·数据结构
BestandW1shEs8 分钟前
彻底理解消息队列的作用及如何选择
java·kafka·rabbitmq·rocketmq
爱吃烤鸡翅的酸菜鱼10 分钟前
Java算法OJ(8)随机选择算法
java·数据结构·算法·排序算法
码蜂窝编程官方13 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
gqkmiss13 分钟前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃19 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰23 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye30 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm32 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang