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

这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/2020SpringW/
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/2020SpringW/homework/10456
结对学号 221701102 221701339
这个作业的目标 疫情统计可视化的实现
作业正文 https://www.cnblogs.com/Zhifeng-Shen/p/12465684.html
其他参考文献 ...

一、git仓库链接 代码规范链接

二、成品展示

视频演示
(可能有广告,请耐心等待~)

新型冠状病毒疫情数据服务平台采用地图与数据结合的方式,展示疫情情况。页面上方以数字形式显示疫情数据,页面下方以地图展示疫情的全国分布情况,具体省份以折线图展示疫情的增减趋势。

全国疫情数据:以不同文字颜色显示数据截止日期前的现有确诊人数、现有疑似人数、现有治愈人数、现有死亡人数,并统计与昨日对比的的增减趋势。

用户可以利用右上角的选择日期按钮,指定数据的截止日期,我们提供日期选择框方便用户选择日期。

中国新型冠状病毒疫情图:以不同颜色代表不同的确诊人数区间,颜色随着确诊人数的增加变深,颜色越深代表此地区疫情情况越严重。右侧提供数据视图按钮与保存图片按钮。点击保存图片按钮,将当前查看的地图保存为"中国新型冠状病毒疫情图.png"。

鼠标移动到具体省份上可以高亮显示,点击具体省份,显示省份名称、确诊人数与查看详情按钮。

鼠标移动到左下角的取色器可以高亮显示指定区间的省份,点击可展示或取消展示。

可查看当前地图显示疫情情况对应的数据视图。

具体省份疫情情况:以不同文字颜色显示数据截止日期前的现有确诊人数、现有疑似人数、现有治愈人数、现有死亡人数,并统计与昨日对比的的增减趋势。同样可以选择指定的截止日期。

以折线图形式显示新增确诊趋势、新增疑似趋势、死亡/治愈趋势,鼠标移动到折点上可以显示具体日期与具体疫情情况。

三、结对过程

即刚开始拿到题目后,和队友怎么讨论,解决问题和查找资料的过程,并提供两人结对讨论的截图

由于两个人都是第一次接触到Github团队协作,首先通过讨论和实践,熟悉了多人合作相关功能如dev分支、冲突的处理与合并等。

  • 查找资料与分工的过程,由221701339负责后端与前后端接口部分,221701102负责前端。

  • 后端先完成,提供API给前端调用。

  • 前端基本完成,展示效果

  • 对于界面美化的讨论,并协助修改了BUG|ू・ω・` )

  • 依然是协助修改BUG,最后完美收工

  • 关于为什么没有部署到服务器:

四、设计实现过程

​ 总体思路:后端提供API接口,把经过一定处理的数据返回给前端,前端请求API取得数据并展示。

​ 后端:Spring Boot,前端:Angular

1.后端

​ 为了检验之前学习路线中Spring Boot成果,特地采用了它。选择Spring Boot很主要的原因是,Spring Boot 提供约定优于配置方式,免去大量配置工作,同时构建Web API变得非常容易,通过Java提供注解特性也大大简化了配置工作。还提供内嵌Tomcat容器,方便部署。

a.数据来源(Data )

​ 这里使用之前作业提供日志文件作为数据源,一方面是为了服用之前经过验证的可靠代码,另一方面是如果使用爬虫或者调用第三方API需要一定的学习成本,且可能不稳定。同时感谢徐助教提供的日志数据。

d.数据访问(DAO)

​ 这里我们复用了之前读取处理的日志文件代码。但是,以前的代码提供返回数据并不是我们想要的。因此我们增加获取国家和省份两种数据方式。

b.业务逻辑(Service)

​ 有了前面的铺垫后,我们使用业务逻辑只需把处理好的数据包装成一定格式即可。

c.控制器(Controller)

​ 控制器只负责接受请求,把参数进行简单处理,并传递给Servicec层处理,返回最终结果结果。

d.配置

​ 为了前端访问,我们需要配置跨域。

​ 由于采取读取文件日志形式,因此对于参数相同请求的请求,我们通过缓存已经处理结果来减少不必要的I/O时间,下次再有相同参数的请求同时直接使用缓存的结果,当然这非常适合不会修改的日志文件这种情况。

e.提高灵活性

​ 在Spring Boot中有个application.properties配置文件,可以配置一些参数,也可以自定配置参数。同时也可以通过启动的命令参数、环境变量来覆盖application.properties提供的默认值。

​ 因为是使用读取日志文件来获取数据,因此需要提供的日志文件的目录和文件的编码方式。

​ 因此我们定义了infectstatistic.log.pathinfectstatistic.log.encoding配置项指定日志文件的目录和文件的编码方式。这些配置项可以通过启动项目附带命令参数指定或者直接修改application.properties等来进行灵活配置。同时我们指定了默认值。有关Spring Boot外部配置参见:Spring Boot Externalized Configuration

d.接口

​ 有两个接口,一个是用于返回国家总体疫情情况、历史疫情数据和和其他省份总体疫情数据。

GET http://localhost:8080/statistics/v1/overview
url参数:
	endDate:字符串,值格式为yyyy-mm-dd,必须指定。指定数据直截止日期。
返回示例:
{
    "self": {
        "name": "中国",
        "total": {
            "patient": 178,
            "survivor": 27,
            "suspect": 317,
            "dead": 21
        },
        "history": [
            {
                "key": "2020-01-20",
                "value": {
                    "patient": 31,
                    "survivor": 0,
                    "suspect": 0,
                    "dead": 0
                }
            },
            {
                "key": "2020-01-21",
                "value": {
                    "patient": 0,
                    "survivor": 0,
                    "suspect": 0,
                    "dead": 0
                }
            }
        ]
    },
    "children": [
        {
            "key": "黑龙江",
            "value": {
                "patient": 1,
                "survivor": 0,
                "suspect": 0,
                "dead": 0
            }
        },
        {
            "key": "西藏",
            "value": {
                "patient": 1,
                "survivor": 0,
                "suspect": 0,
                "dead": 0
            }
        }
    ],
    "_links": {
        "self": {
            "href": "http://localhost:8080/statistics/v1/overview?endDate=2020-03-10"
        }
    }
}

​ 另一个接口返回省份总体疫情情况和历史疫情数据。

GET http://localhost:8080/statistics/v1/detail
url参数:
	name:字符串,省份名称,必须指定。
	endDate:字符串,值格式为yyyy-mm-dd,必须指定。指定数据直截止日期。
返回示例:
{
    "name": "福建",
    "total": {
        "patient": 0,
        "survivor": 0,
        "suspect": 0,
        "dead": 0
    },
    "history": [
        {
            "key": "2020-01-20",
            "value": {
                "patient": 0,
                "survivor": 0,
                "suspect": 0,
                "dead": 0
            }
        },
        {
            "key": "2020-01-21",
            "value": {
                "patient": 0,
                "survivor": 0,
                "suspect": 0,
                "dead": 0
            }
        }
    ],
    "_links": {
        "self": {
            "href": "http://localhost:8080/statistics/v1/detail?name=%E7%A6%8F%E5%BB%BA&endDate=2020-03-10"
        }
    }
}

2.前端

​ 这里我们使用Angular作为前端框架,官方推荐是TypeScript作为开发语言,TypeScript提供面向对象语法方式、同时类型安全也得到了增强,可以减少出错可能。Angular还提供依赖注入框架等,来提升开发效率和模块化程度。

​ 前端主要分为两个视图,一个是全国视图,另一个是省份视图。

a.数据展示

​ 数据展示主要分为文本和图标。文字展示显示数据总体情况,二图标展示的是数据的分布以及变化趋势。

​ 这里图表控件来自ECharts,使用的是图表有地图、和折线图。为了让数据方便绑定,还是利用了ngx-echarts拓展来实现。

b.界面布局

​ 为了使得构建响应式网页,我们使用了Bootstrap提供的代码设计页面布局,同时Bootstrap还提供了丰富而美观的控件。为了方便动态内容切换和减少外部JavaScript干扰,我们使用了ng-bootstrap拓展来实现。

c.数据获取

​ 由于后端已经规定好了接口,前端就可以直接调用了。因此我们可以定义一个数据服务来获取后端数据,并把它注入Angular依赖注入容器中。

​ 不过后端返回并不是都可以直接使用,需要转换成一定格式的数据才能直接显示或者提供给图表控件。

五、功能结构图

六、代码说明

1.后端

​ 为了适应新的数据要求,我们定义以下的pojo:

​ InfectionCell类表示基本疫情数据,包括:确诊人数、治愈人数、疑似人数、死亡人数

public class InfectionCell {
    /**
     * 确诊人数
     */
    private int patient;
    /**
     * 治愈人数
     */
    private int survivor;
    /**
     * 疑似人数
     */
    private int suspect;
    /**
     * 死亡人数
     */
    private int dead;
}

DetailInfectionItem类表示省份疫情数据,包括:省份名称、总体疫情数据,省份历史疫情数据。

public class DetailInfectionItem {
    /**
     * 省份名称
     */
    private String name;
    /**
     * 省份基本疫情数据
     */
    private InfectionCell total;
    /**
     * 省份历史疫情数据
     */
    private List<Pair<LocalDate, InfectionCell>> history;
}

OverviewInfectionItem类表示国家疫情数据,包括:国家总体疫情数据、各省份总体数据。

public class OverviewInfectionItem {
    /**
     * 国家基本疫情数据
     */
    private DetailInfectionItem self;
    /**
     * 国家各省份基本疫情数据
     */
    private Collection<Pair<String, InfectionCell>> children;
}

​ 获取数据,包括获取省份数据和全国数据。这里复用之前的作业(点击链接查看)写的代码,不过只是有关于数据读取处理的代码,这里只解释新的代码,旧的代码解释可以在之前的作业找到。因为有新的数据需求,原来处理数据的InfectStatistician 类需要增加两个方法getCountryStatistics()getProvinceStatistics

/**
 * 从处理好的日志数据,统计国家疫情数据
 *
 * @param name 国家名
 * @return 国家疫情数据
 */
public OverviewInfectionItem getCountryStatistics(String name) {
    if (!ready) {
        throw new InfectStatisticException("无法执行操作,请重新取数据");
    }

    OverviewInfectionItem overview = new OverviewInfectionItem();
    Map<String, InfectionCell> map = new HashMap<>(257);
    DetailInfectionItem all = new DetailInfectionItem();
    all.setName(name);
    all.setHistory(new LinkedList<>());
    all.setTotal(new InfectionCell());
    overview.setSelf(all);
    overview.setChildren(new LinkedList<>());

    data.sort((o1, o2) -> o1.getKey().compareTo(o2.getKey()));
    Iterator<Pair<LocalDate, Collection<InfectionItem>>> iterator = data.listIterator();
    Pair<LocalDate, Collection<InfectionItem>> pair = null;
    if (iterator.hasNext()) {
        pair = iterator.next();
    }
    LocalDate current = minDate.plusDays(0);
    while (endDate.isAfter(current) || endDate.isEqual(current)) {
        LocalDate date = current;
        InfectionCell day = new InfectionCell();
        if (pair != null && pair.getKey().equals(date)) {
            for (InfectionItem item : pair.getValue()) {
                updateInfectionCellBy(item, day);
                updateInfectionCellBy(item, all.getTotal());

                InfectionCell province = getOrCreateFrom(map, item.name);
                updateInfectionCellBy(item, province);
            }
            if (iterator.hasNext()) {
                pair = iterator.next();
            } else {
                pair = null;
            }
        }
        all.getHistory().add(new Pair<>(current, day));
        current = current.plusDays(1);
    }

    List<Pair<String, InfectionCell>> children = new LinkedList<>();
    for (String key : map.keySet()) {
        children.add(new Pair<>(key, map.get(key)));
    }
    overview.setChildren(children);
    return overview;
}

getCountryStatistics()方法首先初始化要返回的OverviewInfectionItem,接着按日期升序排序每个日志文件处理的结果列表data

​ 之后遍历结果列表data,然后按日志文件的日期到传入参数endDate开始循环,如果当前日期与结果列表data有日期匹配,遍历结果列表data日期匹配匹配的项目,更新省份、全国的数据。之后生成一个当日全国疫情数据加入到OverviewInfectionItemhistory中,如果之前结果列表data没有日期匹配匹配的项目,则当天数据默默认都为0。

​ 遍历结束后,把日志文件出现的省份所对应的疫情数据InfectionItem添加到OverviewInfectionItemchildren中。

/**
 * 从处理好的日志数据,统计指定省份疫情数据
 *
 * @param province 省份名称
 * @return 省份疫情数据
 */
public DetailInfectionItem getProvinceStatistics(String province) {
    if (!ready) {
        throw new InfectStatisticException("无法执行操作,请重新取数据");
    }

    DetailInfectionItem all = new DetailInfectionItem();
    all.setName(province);
    all.setTotal(new InfectionCell());
    all.setHistory(new LinkedList<>());

    data.sort((o1, o2) -> o1.getKey().compareTo(o2.getKey()));
    Iterator<Pair<LocalDate, Collection<InfectionItem>>> iterator = data.listIterator();
    Pair<LocalDate, Collection<InfectionItem>> pair = null;
    if (iterator.hasNext()) {
        pair = iterator.next();
    }
    LocalDate current = minDate.plusDays(0);
    while (endDate.isAfter(current) || endDate.isEqual(current)) {
        LocalDate date = current;
        InfectionCell day = new InfectionCell();
        if (pair != null && pair.getKey().equals(date)) {
            for (InfectionItem item : pair.getValue()) {
                if (item.name.equals(province)) {
                    updateInfectionCellBy(item, day);
                    updateInfectionCellBy(item, all.getTotal());
                }
            }
            if (iterator.hasNext()) {
                pair = iterator.next();
            } else {
                pair = null;
            }
        }
        all.getHistory().add(new Pair<>(current, day));
        current = current.plusDays(1);
    }
    return all;
}

getProvinceStatistics()方法与getCountryStatistics()流程类似,不过在遍历过程中只添加更新参数province指定省份的总体疫情数据和历史疫情数据。

2.前端

​ 前端使用StatisticsService服务来从后端获取数据,该服务被注入到根模块中。

export class StatisticsService {
  private url: string = "http://localhost:8080/statistics/v1/";

  constructor(private http: HttpClient) {
  }

  getDetailStatistics(name: string, endDate: Date): Observable<DetailItem> {
    return this.http.get<DetailItem>(this.url + "detail"
      , {
        params: {
          name: name,
          endDate: formatDate(endDate, "yyyy-MM-dd", "zh-Hans")
        }
      })
      .pipe(
        catchError(this.handleError<DetailItem>('getDetailStatistics', null))
      );
  }

  getOverviewStatistics(name:string,endDate:Date):Observable<OverviewItem>{
    return this.http.get<OverviewItem>(this.url + "overview",
      {
        params: {
          name: name,
          endDate: formatDate(endDate, "yyyy-MM-dd", "zh-Hans")
        }
      })
      .pipe(
        catchError(this.handleError<OverviewItem>('getOverviewStatistics', null))
      );
  }

  private handleError<T> (operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      console.error(error);
      return of(result as T);
    }
  }
}

​ 为了把后端的数据转化成图表控件可接受的数据格式,定义了一个工具类:

export class StatisticsUtil {
  static getKeyValues<K, V>(pairs: KeyValuePair<K, V>[]) {
    let values: K[] = [];
    if (pairs) {
      for (let pair of pairs)
        values.push(pair.key);
	}
	return values;
  }

    static getPatientValues<K>(pairs:KeyValuePair<K,InfectionCell>[]):number[]{
        let values:number[]=[];
        if(pairs){
            for (let pair of pairs){
                let cell:InfectionCell=pair.value;
                values.push(cell.patient);
            }
        }
        return values;
    }

    static getSurvivorValues<K>(pairs:KeyValuePair<K,InfectionCell>[]):number[]{
        let values:number[]=[];
        if(pairs){
            for (let pair of pairs){
                let cell:InfectionCell=pair.value;
                values.push(cell.survivor);
            }
        }
        return values;
    }

    static getSuspectValues<K>(pairs:KeyValuePair<K,InfectionCell>[]):number[]{
        let values:number[]=[];
        if(pairs){
            for (let pair of pairs){
                let cell:InfectionCell=pair.value;
                values.push(cell.suspect);
            }
        }
        return values;
    }

  static getDeadValues<K>(pairs: KeyValuePair<K, InfectionCell>[]): number[] {
    let values: number[] = [];
    if (pairs) {
      for (let pair of pairs) {
        let cell: InfectionCell = pair.value;
        values.push(cell.dead);
      }
    }
    return values;
  }

}

getKeyValues()方法用于获取由{ key:K; value:V; }对象构成的数组的key属性数组,getPatientValues()方法用户获取由{ key:K; value:V; }对象构成的数组中value对象的patient属性数组,其他getDeadValues()getSurvivorValues()getSuspectValues()getPatientValues()类似,只不过获取value对象的对应属性数组。

七、心路历程与收获

221701339

首先阅读《构建之法》以来,收获最多的是软件开发流程要关注的地方,比如代码规范、单元测试、代码复审。这些是对于初入软件工程容易犯错或忽视的地方,我们一直在试错,需要不断总结自己来改进以后的行为。《构建之法》提供了大量的方法论,当自己做了相关工作之后,回过来再仔细品味,总有一番收获。

对于团队合作或者结对编程,如果大家都是心有灵犀一点通,那么工作起来就能得心应手。然而这只是理想情况,不同人还是存在差异的,比如技术、性格、目标,这些差异化会导致团队或者结对,出现不同步。当然需要有人来及时矫正。

这次结对过程,其实充满了挑战。在VS Code的Git操作上捣鼓了很久,我之前一直使用的是IDEA自带Git插件;对于未知的领域学习需要一定时间,但是面对这种紧急的任务,需要不断讨论和帮助;需要合理的时间安排,因为大家选课不是都一样的,因此需要安排时间进行项目讨论,同时话还面临其他科目在时间上的管理。等等。

我觉得如果时一周时间只完成这个任务,而没有其他事件的干扰,那么收获得可能更多。

221701102

在《构建之法中》读到“结对编程使程序的设计和代码质量都有了进一步的提升”,简直不能再同意。回想起自己完成的那次寒假作业,我需求分析都做了好几天,但这次结对作业,效率很高地就完成了。通过这次结对,我对团队协作有了更深的了解,一个人完成代码开发工作量有一点点大,对需求的理解和实现都会因为主观因素带来偏差,但团队协作不一样,多个人的灵感能够碰撞出不一样的火花,带来质量的提升。

同时我的队友熟悉且擅长前端框架,所以在框架的搭建、BUG修改方面帮到了我很多,肥肠感谢他。

八、评价队友

我的队友是一名效率高、认真负责、条理清晰、编码能力强的优秀软工学子。————221701102

我的队友很好沟通,同时也善于学习,做事也比较认真。————221701339

原文地址:https://www.cnblogs.com/Zhifeng-Shen/p/12465684.html