• 结队第二次作业——某次疫情统计可视化的实现


    这个作业属于哪个课程 <课程的链接>
    这个作业要求在哪里 <作业要求的链接>
    结对学号 221701107、221701137
    这个作业的目标 基于 Web 实现第一次结对中的原型设计
    作业正文 TODO
    其他参考文献 echartsVue 实战开发疫情地图Echarts 疫情地图项目实战2019-nCoV

    一. Github仓库地址:xjliang/InfectStatisticWeb

    代码规范链接:codestyle.md

    二、作品展示

    • 动手试试看(注:2020-04-13 后无法使用)

      在线演示地址

    • 项目预览

    • 比较各个省份的疫情情况

    • 疫情地图

    • 通过一定规则对选择的省份进行排序比较

    • 可以暂停时间线或者鼠标点击达到快进效果

    • 可以指定时间段内的疫情统计

    • 显示或隐藏某种统计信息(toggle)

    • 查看各省份走势图

    三、结队讨论过程描述

    stage1: planning

    Stage2: coding

    Stage3: merge

    Stage4: deployment

    四、设计实现过程

    首先,我们考虑了一下要涉及的功能,包括

    • 全国疫情分布情况图
    • 省份疫情趋势图
    • 省份每日增长情况
    • 省份间排序比较

    于是先去网上找了一些 echarts 的使用方法,B 站上如何使用 Vue 做地图展示,不过最终还是没有使用前后端分离的方法实现(技术不熟练),直接用一个 spring boot 项目直接把前后端揉在一个项目里。

    开始构建代码时,我们依然分为前后端,不过,前后端间的耦合度太大,整个项目的进行需要不断地等待某一端的接口完毕后才能完整测试,这着实是传统 web 项目的缺陷。

    后端部分是使用 MySQL 作为数据源,MyBatis 作为持久层框架,方便数据库的访问,由于前期没有使用 MyBatis-generator,导致一些自己编写的 SQL 健壮性太差,后期部署时才发现一些数据接口问题,只能说自己对 mybatis 框架的一些语法不熟,这方面还需要加强。

    数据库设计时,为了后续可以方便地访问某一个省份某一天的疫情统计情况,我把省份名和日期作为记录的两个字段,还加上了一些疫情统计字段(累计、新增)情况。

    持久层解决后,我便着手业务层。这个业务层的插入数据还是挺麻烦的,需要将从网上获取的 JSON 数据解析、将待更新日的数据删除,再逐个省份地把数据插入到数据库。业务层的另一个重要接口是获取某个时间段的疫情统计数据,数据库里的数据库记录(record)有 updateDate 字段,用来记录该数据的更新日期,要完成该业务,需要给这个接口传入一个时间段的参数,用于数据库筛选。

    之后就是控制层了,控制层主要设计了给页面展示用的接口(ModelAndView),以及一个数据接口,调用业务层接口,将数据结果返回。

    最后要处理的就是数据更新问题,我们不可能每次都手动去网上爬取数据,再更新到数据库,肯定有更好的实现方式,这里我们采用的是定时任务(schedule)。我们添加了一个定时任务,每 6 个小时自动更新数据(前提是该程序必须一直运行):获取待更新的日期列表,调用业务层的添加数据方法,逐日从网上爬取数据,将数据持久化到数据库。

    后端数据接口搞定后,便将注意力转移到前端。前段的页面确实不好做(审美不行),就去晚上找了一些参考资料,快速构建了前端页面,关键的问题是如何通过约定好的接口获取后端的数据。这里我们使用 JQuery 的 Ajax 接口,通过 POST 请求访问数据接口,得到数据后将数据通过 echarts 渲染,这里都是通过 javascript 处理这些数据处理及渲染问题。

    echarts 最主要的就是 option,把 option 选项设置好,基本上就可以处理好 echats 的显示了。

    功能结构图

    五、代码说明

    • Model:疫情统计对象(与数据库表结构相同)

      public class EpidemicSituation {
          private String id;
      
          /**
           * 省份编码
           */
          private String provinceCode;
      
          /**
           * 省份名称
           */
          private String provinceName;
      
          /**
           * 更新时间
           */
          private Date updateDate;
      
          /**
           * 新增疑似
           */
          private Integer newSuspectNum;
      
          /**
           * 累计疑似
           */
          private Integer totalSuspectNum;
      
          /**
           * 新增确诊
           */
          private Integer newConfirmNum;
      
          /**
           * 累计确诊
           */
          private Integer totalConfirmNum;
      
          /**
           * 新增死亡
           */
          private Integer newDeadNum;
      }
      
    • 业务层接口:插入指定日期的疫情统计数据

      1. 获取 JSON 数据
      2. 删除 当前数据
      3. 插入解析后的 Java Bean
      public int insertAll(String httpArg) {
          System.out.println("now is:" + httpArg);
          
          // 获取 httpArg 对应的 JSON 数据
          String str = HttpUtils.httpToStr(httpArg);
          if (str == null) {
              return -1;
          }
          Gson gson = new Gson();
          Map<String, Object> map = gson.fromJson(str, new TypeToken<HashMap<String, Object>>() {
          }.getType());
          List<Map<String, Object>> features = (List<Map<String, Object>>) map.get("features");
          Date currentDate = MyUtils.strToDate(httpArg, "yyyyMMdd");
      
          // 1. delete current data
          EpidemicSituation d = new EpidemicSituation();
          d.setUpdateDate(currentDate);
      
          EpidemicSituationExample epidemicSituationExample = new EpidemicSituationExample();
          epidemicSituationExample.createCriteria().andUpdateDateEqualTo(currentDate);
          epidemicSituationMapper.deleteByExample(epidemicSituationExample);
      
          // 2. update data
          for (Map<String, Object> f : features) {
              Map<String, Object> p = (Map<String, Object>) f.get("properties");
              EpidemicSituation record = new EpidemicSituation();
              record.setId(httpArg + MyUtils.getUUID32());
              record.setUpdateDate(currentDate);
              record.setProvinceCode("" + MyUtils.doubleToInt((Double) p.get("adcode")));
              record.setProvinceName((String) p.get("name"));
              record.setNewSuspectNum(MyUtils.doubleToInt((Double) p.get("新增疑似")));
              record.setTotalSuspectNum(MyUtils.doubleToInt((Double) p.get("累计疑似")));
              record.setNewConfirmNum(MyUtils.doubleToInt((Double) p.get("新增确诊")));
              record.setTotalConfirmNum(MyUtils.doubleToInt((Double) p.get("累计确诊")));
              record.setNewDeadNum(MyUtils.doubleToInt((Double) p.get("新增死亡")));
              record.setTotalDeadNum(MyUtils.doubleToInt((Double) p.get("累计死亡")));
      
              epidemicSituationMapper.insert(record);
          }
          return features.size();
      }
      
    • 定时任务:每 6 个小时执行一次,调用业务层接口更新数据

      @Scheduled(cron = "0 0 0/6 * * ?")
      private void updateYqInformation() throws FileNotFoundException {
          log.info("开始更新疫情数据");
          String serverPath = ResourceUtils.getURL("classpath:property").getPath();
          String day = MyUtils.getYesterdayByDate();
          String lastDay = PropertyUtils.readByKey(serverPath + "/my.properties", "lastDay");
          List<String> list = MyUtils.getDays(lastDay, day, MyUtils.USER_DATE_FORMAT);
          for (String str : list) {
              int i = epidemicService.insertAll(str);
              if (i != -1) {
                  // 得到当前的确诊人数
                  List<EpidemicSituation> listByDay = epidemicService.selectByTimeRange(new TimeRange(str, str, MyUtils.USER_DATE_FORMAT));
                  BigDecimal totalSuspectNum = new BigDecimal(0);
                  BigDecimal totalConfirmNum = new BigDecimal(0);
                  BigDecimal totalDeadNum = new BigDecimal(0);
                  for (EpidemicSituation d : listByDay) {
                      totalSuspectNum = totalSuspectNum.add(new BigDecimal(d.getTotalSuspectNum()));
                      totalConfirmNum = totalConfirmNum.add(new BigDecimal(d.getTotalConfirmNum()));
                      totalDeadNum = totalDeadNum.add(new BigDecimal(d.getTotalDeadNum()));
                  }
                  log.info(totalSuspectNum + " " + totalConfirmNum + " " + totalDeadNum);
                  Map<String, Object> map = new HashMap<>();
                  map.put("lastDay", str);
                  map.put("totalSuspectNum", totalSuspectNum.toString());
                  map.put("totalConfirmNum", totalConfirmNum.toString());
                  map.put("totalDeadNum", totalDeadNum.toString());
                  PropertyUtils.savePro(serverPath + "/my.properties", map);
              }
          }
          log.info("疫情数据更新完成");
      }
      
    • 后端控制器接口

      selectByTimeRange:获取指定时间范围内是数据

      overview:提供给客户端的接口,访问此接口即可得到 ModelAndView,模型和视图,也就是前端展示的页面

      @PostMapping("/selectByTimeRange")
      public List<EpidemicSituation> selectByTimeRange(TimeRange range) {
          return epidemicService.selectByTimeRange(range);
      }
      
      @GetMapping("/overview")
      public ModelAndView overview(Model model) throws Exception {
          String serverPath = ResourceUtils.getURL("classpath:property").getPath();
          log.info(serverPath);
          Map<String, String> m = PropertyUtils.readToMap(serverPath + "/my.properties");
          model.addAttribute("totalSuspectNum", m.get("totalSuspectNum"));
          model.addAttribute("totalConfirmNum", m.get("totalConfirmNum"));
          model.addAttribute("totalDeadNum", m.get("totalDeadNum"));
          model.addAttribute("lastUpdateDay", m.get("lastDay"));
          model.addAttribute("cityList", cityDataService.listCity());
          return new ModelAndView("infect/index", "reportModel", model);
      }
      
    • 前端使用 Ajax 调用后端数据接口

      arg:时间段,数据格式为

      {
      	startDateStr: "",
      	endDateStr: "",
      	dateFormat: ""
      }
      
      function selectByTimeRange(arg) {
          var url = getPath() + "/selectByTimeRange";
          var list;
          $.ajax({
              url: url,
              data: arg,
              type: "POST",
              async: false,
              dataType: "json",
              success: function (data) {
                  list = data;
              },
              error: function (request) {
                  alert("产生错误!!!请重试!!!");
              }
          });
          return list;
      }
      
    • 前端使用 themeleaf 模板引擎

      <div class="item">
          <h4 th:text="${reportModel.totalConfirmNum}">0</h4>
          <span>
              <i class="icon-dot" style="color: #006cff"></i>
              确诊
          </span>
      </div>
      <div class="item">
          <h4 th:text="${reportModel.totalSuspectNum}">0</h4>
          <span>
              <i class="icon-dot" style="color: #6acca3"></i>
              疑似
          </span>
      </div>
      <div class="item">
          <h4 th:text="${reportModel.totalDeadNum}">0</h4>
          <span>
              <i class="icon-dot" style="color: #6acca3"></i>
              死亡
          </span>
      </div>
      

    六、心路历程与收获

    效能分析和 PSP

    PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
    Planning 计划 40 30
    Estimate 估计这个任务需要多少时间 40 30
    Development 开发 1235 1115
    Analysis 需求分析 (包括学习新技术) 80 40
    Design Spec 生成设计文档 0 0
    Design Review 设计复审 10 10
    Coding Standard 代码规范 (为目前的开发制定合适的规范) 35 45
    Design 具体设计 50 60
    Coding 具体编码 960 830
    Code Review 代码复审 40 50
    Test 测试(自我测试,修改代码,提交修改) 60 80
    Reporting 报告 120 120
    Test Report 测试报告 40 30
    Size Measurement 计算工作量 40 40
    Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 40 50
    合计 1395 1265

    总结

    221701107 梁晓键

    这次原型设计的实现原型打算使用前后端分离架构,但是由于学艺不精,对前端框架了解较少,也就放弃了该想法。很多时候完成的结果反映了我们的技能点有多少。

    后端是实现还是比较得心应手的,使用 Spring Boot 整合 MyBatis,但是由于前期没有挖掘出 MyBatis-generator 的功能,导致手写了许多不够健壮的 SQL 语句,为后续的 bug 奠定了基础。

    这次作业我的另一个收获就是利用 aliyun 部署了我们的 web 应用程序,这是第一次买云服务器,手动部署,过程中难免磕磕碰碰,但由于“谷哥”的帮助,还是很快就完成了预期的效果。这一过程中,我刚开始没有正确规划好安装的过程,也没有使用官方的教程,导致中期出现了难以解决的问题,甚至想要放弃,但是又心有不甘(这可花了我一周买水的钱),不能让它就此空转一个月,后面用了一些更好的教程,就快速完成了部署。我深切的体会到:做任何事一定要规划好,不然后悔莫及(凡事预则立,不预则废)。

    在结对过程中,我和队友虽然没有面对面地交流,但是我们通过 QQ 远程交流很频繁,快速推动了本次作业的进展。本来以为本次作业的难度系数应该有 5 颗星,心有余悸,感谢对队友的大力支持合作,让我快速锁定了推动作业进行的方向,并坚定不移的完成自己的选择。这也让我意思到了团队开发中队友的重要性,好的、积极的队友能促进软件开发的进程,与队友进行良好的沟通同样至关重要。

    221701137 张平

    每一次的软工实践任务,都能学到了很多新技能,依然觉得在软件工程这门学科里面自己还欠缺许多知识。只有不断提高自己的学习能力,才能更好的适应需求。现如今的各类软件数量非常之多,每接触一款新软件,都需要去了解它的使用方法和规则,一步步的摸索,一点点的尝试。不同软件之间的作用区别也是很大,通过选择合适的软件可以让自己的任务得到更加顺利的完成。同时,程序员确实不是一个人独干,团队的重要性也很重要。今后不仅要提升自己的专业技能,也更应该能够适应团队,发挥自己的优势,以及队友的长处,让整个团队更有竞争力。

    队友非常给力,与之合作感觉各项任务都轻松了许多,我想给队友一个满分的评价。感谢对队友的大力支持合作。这也让我意思到了团队开发中队友的重要性,好的、积极的队友能促进软件开发的进程,与队友进行良好的沟通同样至关重要。


    (Done)

    作者:张平

    -------------------------------------------

    个性签名:本来无一物,何处惹尘埃!

    哈哈哈(っ•̀ω•́)っ✎⁾⁾!

  • 相关阅读:
    Python3 input() 函数
    Python3 enumerate() 函数
    Python3 ascii() 函数
    Python3 sorted() 函数
    css sprite
    Java 理论与实践: 并发集合类
    关于 Java Collections API 您不知道的 5 件事,第 1 部分
    Treasure! Free Mobile Design Resources
    Indigo Studio
    ionic:Build mobile apps faster with the web technologies you know and love
  • 原文地址:https://www.cnblogs.com/zp37/p/12495866.html
Copyright © 2020-2023  润新知