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

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

原来的接口是这样的

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>
相关推荐
YBN娜5 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=5 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
一只爱打拳的程序猿6 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
杨荧8 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
minDuck10 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
为将者,自当识天晓地。28 分钟前
c++多线程
java·开发语言
小政爱学习!30 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。36 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
daqinzl36 分钟前
java获取机器ip、mac
java·mac·ip
花花鱼42 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui