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

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

原来的接口是这样的

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>
相关推荐
狗头大军之江苏分军17 小时前
iPhone 17 vs iPhone 17 Pro:到底差在哪?买前别被忽悠了
前端
小林coding17 小时前
再也不怕面试了!程序员 AI 面试练习神器终于上线了
前端·后端·面试
刘婉晴17 小时前
【Java】NIO 简单介绍
java·nio
文心快码BaiduComate17 小时前
WAVE SUMMIT深度学习开发者大会2025举行 文心大模型X1.1发布
前端·后端·程序员
babytiger17 小时前
python 通过selenium调用chrome浏览器
前端·chrome
passer98117 小时前
基于webpack的场景解决
前端·webpack
渣哥17 小时前
聊聊我和 ArrayList、LinkedList、Vector 的“一地鸡毛”
java
浮游本尊17 小时前
Java学习第20天 - 性能优化与监控
java
奶昔不会射手17 小时前
css3之grid布局
前端·css·css3