SpringBoot如何实现一个实时更新的进度条

各位看官可以关注博主个人博客,了解更多信息。
作者:Surpasser
链接地址:https://surpass.org.cn

前言

博主近期接到一个任务,大概内容是:导入excel表格批量修改状态,期间如果发生错误则所有数据不成功,为了防止重复提交,做一个类似进度条的东东。

那么下面我会结合实际业务对这个功能进行分析和记录。

正文

思路

前端使用bootstrap,后端使用SpringBoot分布式到注册中心,原先的想法是导入表格后异步调用修改数据状态的方法,然后每次计算修改的进度然后存放在session中,前台jquery写定时任务访问获取session中的进度,更新进度条进度和百分比。但是这存在session在服务间不共享,跨域问题。那么换一个方式存放,存放在redis中,前台定时任务直接操作获取redis的数据。

实施

进度条

先来看一下bootstrap的进度条

<div class="progress progress-striped active">
    <div class="progress-bar progress-bar-success" role="progressbar"
         aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
         style="width: 40%;">
        40%
    </div>
</div>

进度条更新主要更新style="width: 40%;"的值即可,div里面的40%可以省略,无非时看着明确。

可以考虑将进度条放入弹出层。

定时任务

//点击确认导入执行此方法
function bulkImportChanges() {
    //获取批量操作状态文件
    var files = $("#importChanges").prop("files");
    var changesFile = files[0];
    var formData = new FormData();
    formData.append("importFile",changesFile);
    $.ajax({
        type : 'post',
        url : "/risk/bulk***es",
        data : formData,
        processData : false,      //文件ajax上传要加这两个的,要不然上传不了
        contentType : false,      //
        success : function(obj) {
            //导入成功
            if (obj.rspCode == "00") {
                //定时任务获取redis导入修改进度
                var progress = "";
                var timingTask = setInterval(function(){
                    $.ajax({
                        type: 'post',
                        url: "/risk/t***k",
                        dataType : 'json',
                        success: function(result) {
                            progress = result.value;
                            if (progress != "error"){
                                var date = progress.substring(0,6);
                                //这里更新进度条的进度和数据
                                $(".progress-bar").width(parseFloat(date)+"%");
                                $(".progress-bar").text(parseFloat(date)+"%");
                            }
                        }
                    });
                    //导入修改完成或异常(停止定时任务)
                    if (parseInt(progress)==100 || progress == "error") {
                        //清除定时执行
                        clearInterval(timingTask);
                        $.ajax({
                            type: 'post',
                            url: "/risk/de***ess",
                            dataType : 'json',
                            success: function(result) {
                                $("#bulkImportChangesProcessor").hide();
                                if (parseInt(progress) == 100) {
                                    alert("批量导入修改状态成功");
                                }
                                if (progress == "error") {
                                    alert("批量导入修改状态失败");
                                }
                                //获取最新数据
                                window.location.href="/risk/re***ByParam";
                            }
                        });
                    }
                }, 1000)
            }else {
                $("#bulkImportChangesProcessor").hide();
                alert(obj.rspMsg);
                window.location.href="/risk/re***ByParam";
            }
        }
    });
}

解释:点击确认导入文件后成功后开启定时任务每一秒(一千毫秒)访问一次后台获取redis存放的进度,返回更新进度条,如果更新完成或者更新失败(根据后台返回的数据决定)则停止定时任务显示相应的信息并刷新页面。获取最新数据。

后台控制层

/**
     * 退单管理批量修改状态导入文件
     * @param importFile
     * @return
     */
    @ResponseBody
    @RequestMapping("/bulk***es")
    public Map<String,Object> bulk***es(MultipartFile importFile){
        log.info("退单管理批量修改状态导入文件,传入参数:"+importFile);
        Map<String,Object> map = new HashMap<>();
        List<Bulk***esEntity> fromExcel = null;
        try{
            //使用工具类导入转成list
            String[] header = {"sy***um","t***mt","ha***ult","re***nd","sy***nd","r**k"};
            fromExcel = importExcelUtil.importDataFromExcel(importFile, header, BulkImportChangesEntity.class);
            if (fromExcel.size()==0){
                map.put("rspCode","99");
                map.put("rspMsg","导入数据不能为空");
                return map;
            }
        }catch (Exception e){
            map.put("rspCode","99");
            map.put("rspMsg","导入操作表失败,请注意数据列格式");
            return map;
        }
        try {
            //这里会对list集合中的数据进行处理

            log.info("调用服务开始,参数:"+JSON.toJSONString(fromExcel));
            //String url = p4_zuul_url+"/***/ri***eat/bu***nges";
            String url = p4_zuul_url+"/***-surpass/ri***eat/bu***nges";
            String result = HttpClientUtil.doPost(url,JSON.toJSONString(fromExcel));
            log.info("调用服务结束,返回数据:"+result);
            if (result != null){
                map = JSONObject.parseObject(result, Map.class);
                log.info("批量修改状态导入:"+JSON.toJSONString(map));
            }
        }catch (Exception e){
            map.put("rspCode","99");
            map.put("rspMsg","导入操作表失败");
            log.info("bu***es exception",e);
            return map;
        }
        return map;
    }

    /**
     * 获取退单管理批量修改状态导入文件进度条进度
     * @return
     */
    @ResponseBody
    @RequestMapping("/t***sk")
    public Map<String,Object> t***sk(){
        Map<String,Object> map = new HashMap<>();
        //获取redis值
        String progress = HttpClientUtil.doGet(
                p4_zuul_url + "/" + p4_redis + "/redis***ler/get?key=progressSchedule");
        if (progress != null){
            map = JSONObject.parseObject(progress, Map.class);
            log.info("进度条进度:"+JSON.toJSONString(map));
            map.put("progressSchedule",progress);
        }else {
            HttpClientUtil.doGet(
                    p4_zuul_url + "/" + p4_redis + "/redis***ler/del?key=progressSchedule");
        }
        return map;
    }

    /**
     * 清除redis进度条进度
     * @return
     */
    @ResponseBody
    @RequestMapping("/de***ess")
    public Map<String,Object> de***ess(){
        Map<String,Object> map = new HashMap<>();
        String progress = HttpClientUtil.doGet(
                p4_zuul_url + "/" + p4_redis + "/redis***ler/del?key=progressSchedule");
        if (progress != null){
            map = JSONObject.parseObject(progress, Map.class);
            log.info("返回数据:"+JSON.toJSONString(map));
        }
        return map;
    }

导入时调用第一个bulk***es方法,定时任务调用t***sk方法,导入完成或发生错误调用de***ess方法删除redis数据,避免占用资源。

服务层

@Async//开启异步
    @Transactional(rollbackFor = Exception.class)//事务回滚级别
    @Override
    public void bulkImportChanges(List<BulkImportChangesParam> list) {
        //初始化进度
        Double progressBarSchedule = 0.0;
        redisClient.set("progressSchedule", progressBarSchedule + "");//存redis
        try {
            for (int i = 1; i <= list.size(); i++) {
                RiskRetreatEntity entity = riskRetreatMapper.selectRetreatListBySysRefNum(list.get(i-1).getSysRefNum());
                if (entity == null){
                    //查询结果为空直接进行下次循环不抛出
                    continue;
                }
                //实体封装
                ···
                //更新
                riskRetreatMapper.updateRetreatByImport(entity);
                //计算修改进度并存放redis保存(1.0 / list.size())为一条数据进度
                progressBarSchedule = (1.0 / list.size()) * i*100;
                redisClient.set("progressSchedule", progressBarSchedule+"");
                if (i==list.size()){
                    redisClient.set("progressSchedule", "100");
                }
            }
        }catch (Exception e){
            //当发生错误则清除缓存直接抛出回滚
            redisClient.set("progressSchedule","error");
            log.info("导入更新错误,回滚");
            log.info("bulkImportChanges exception:",e);
            throw e;
        }
    }

每更新一条数据存放进度,当发生错误则进行回滚。如果开启异步则需要在启动类添加注解@EnableAsync

@EnableAsync
···//其他注解
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

结果样式

结尾

这次结合了前端的定时任务,后台事务以及异步,总的来说还是一次🙅‍不错的体验。

优秀的代码只能通过时间的冲刷才会显现的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355