这个作业属于哪个课程 | 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仓库链接 代码规范链接
-
git仓库链接:https://github.com/abse4411/InfectStatisticWeb
-
代码规范链接:https://github.com/abse4411/InfectStatistic-main/blob/master/221701339/codestyle.md
二、成品展示
视频演示
(可能有广告,请耐心等待~)
新型冠状病毒疫情数据服务平台采用地图与数据结合的方式,展示疫情情况。页面上方以数字形式显示疫情数据,页面下方以地图展示疫情的全国分布情况,具体省份以折线图展示疫情的增减趋势。
全国疫情数据:以不同文字颜色显示数据截止日期前的现有确诊人数、现有疑似人数、现有治愈人数、现有死亡人数,并统计与昨日对比的的增减趋势。
用户可以利用右上角的选择日期按钮,指定数据的截止日期,我们提供日期选择框方便用户选择日期。
中国新型冠状病毒疫情图:以不同颜色代表不同的确诊人数区间,颜色随着确诊人数的增加变深,颜色越深代表此地区疫情情况越严重。右侧提供数据视图按钮与保存图片按钮。点击保存图片按钮,将当前查看的地图保存为"中国新型冠状病毒疫情图.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.path
和infectstatistic.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> history;
}
OverviewInfectionItem
类表示国家疫情数据,包括:国家总体疫情数据、各省份总体数据。
public class OverviewInfectionItem {
/**
* 国家基本疫情数据
*/
private DetailInfectionItem self;
/**
* 国家各省份基本疫情数据
*/
private Collection> children;
}
获取数据,包括获取省份数据和全国数据。这里复用之前的作业(点击链接查看)写的代码,不过只是有关于数据读取处理的代码,这里只解释新的代码,旧的代码解释可以在之前的作业找到。因为有新的数据需求,原来处理数据的InfectStatistician
类需要增加两个方法getCountryStatistics()
和getProvinceStatistics
:
/**
* 从处理好的日志数据,统计国家疫情数据
*
* @param name 国家名
* @return 国家疫情数据
*/
public OverviewInfectionItem getCountryStatistics(String name) {
if (!ready) {
throw new InfectStatisticException("无法执行操作,请重新取数据");
}
OverviewInfectionItem overview = new OverviewInfectionItem();
Map 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>> iterator = data.listIterator();
Pair> 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> 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
日期匹配匹配的项目,更新省份、全国的数据。之后生成一个当日全国疫情数据加入到OverviewInfectionItem
的history
中,如果之前结果列表data
没有日期匹配匹配的项目,则当天数据默默认都为0。
遍历结束后,把日志文件出现的省份所对应的疫情数据InfectionItem
添加到OverviewInfectionItem
的children
中。
/**
* 从处理好的日志数据,统计指定省份疫情数据
*
* @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>> iterator = data.listIterator();
Pair> 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 {
return this.http.get(this.url + "detail"
, {
params: {
name: name,
endDate: formatDate(endDate, "yyyy-MM-dd", "zh-Hans")
}
})
.pipe(
catchError(this.handleError('getDetailStatistics', null))
);
}
getOverviewStatistics(name:string,endDate:Date):Observable{
return this.http.get(this.url + "overview",
{
params: {
name: name,
endDate: formatDate(endDate, "yyyy-MM-dd", "zh-Hans")
}
})
.pipe(
catchError(this.handleError('getOverviewStatistics', null))
);
}
private handleError (operation = 'operation', result?: T) {
return (error: any): Observable => {
console.error(error);
return of(result as T);
}
}
}
为了把后端的数据转化成图表控件可接受的数据格式,定义了一个工具类:
export class StatisticsUtil {
static getKeyValues(pairs: KeyValuePair[]) {
let values: K[] = [];
if (pairs) {
for (let pair of pairs)
values.push(pair.key);
}
return values;
}
static getPatientValues(pairs:KeyValuePair[]):number[]{
let values:number[]=[];
if(pairs){
for (let pair of pairs){
let cell:InfectionCell=pair.value;
values.push(cell.patient);
}
}
return values;
}
static getSurvivorValues(pairs:KeyValuePair[]):number[]{
let values:number[]=[];
if(pairs){
for (let pair of pairs){
let cell:InfectionCell=pair.value;
values.push(cell.survivor);
}
}
return values;
}
static getSuspectValues(pairs:KeyValuePair[]):number[]{
let values:number[]=[];
if(pairs){
for (let pair of pairs){
let cell:InfectionCell=pair.value;
values.push(cell.suspect);
}
}
return values;
}
static getDeadValues(pairs: KeyValuePair[]): 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