结队第二次作业——某次疫情统计可视化的实现
这个作业属于哪个课程 | <课程的链接> |
---|---|
这个作业要求在哪里 | <作业要求的链接> |
结对学号 | 221701107、221701137 |
这个作业的目标 | 基于 Web 实现第一次结对中的原型设计 |
作业正文 | TODO |
其他参考文献 | echarts、Vue 实战开发疫情地图、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; }
-
业务层接口:插入指定日期的疫情统计数据
- 获取 JSON 数据
- 删除 当前数据
- 插入解析后的 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)