封印在家, 写个新型肺炎疫情小工具

2,746 阅读5分钟

这个春节, 应该会让我们永世难忘! 每天早上醒来第一件事就是看新型肺炎相关的消息, 白天就是宅在家做饭/吃饭/看电视. 两天前艰难地回到上海了, 春节假期延长了, 却无心干正事, 每天就在那刷疫情相关的消息. 在刷掘金沸点时, 看到有的网友用天行数据开放的 API 做了个疫情小工具. 我想闲着也无聊, 我也来写一个吧!

概述

体验网址: kaiyuanshuwu.cn/ncov.html, 截图如下:

主要功能:

  • 全国, 以及分省份/城市的确诊病例/疑似病例/死亡人数/治愈人数
  • 支持查看省市疫情地图
  • 疫情相关消息/谣言滚动播报
  • 新型肺炎确诊患者同行程查询

主要技术栈:

  • 数据
    • 开放 API: 天行数据
    • retrofit2, 将 API 转化为 "微服务"
    • redis, 缓存 10min, 减少对 API 的请求 (天行也是大约 10min 更新一次数据)
  • 后端
    • Java
    • Spring Boot
  • 前端
    • Vue
    • Element UI
    • EChart

数据对接

登录天行数据, 申请相关 API 接口. 为了更优化地对接 API 接口, 我们用 retrofit2, 将其转换为 "微服务" 形式. 引入如下两个依赖

<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>retrofit</artifactId>
  <version>2.6.2</version>
</dependency>
<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>converter-jackson</artifactId>
  <version>2.0.2</version>
</dependency>

引入 converter-jackson 的目的是 json 转 java 对象. 定义 TianService 接口, 将 API 映射为对应的接口方法, 然后增加配置文件 TianConfig, 生成 tianService Bean.

@Configuration
public class TianConfig {
    @Autowired
    private GlobalSetting globalSetting;

    @Autowired
    private Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder;

    @Bean
    public TianService tianService() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(globalSetting.getTianHost())
                .client(new OkHttpClient.Builder()
                        .connectTimeout(3000, TimeUnit.MILLISECONDS)
                        .writeTimeout(1000, TimeUnit.MILLISECONDS)
                        .readTimeout(1000, TimeUnit.MILLISECONDS).build())
                .addConverterFactory(JacksonConverterFactory.create(
                        jackson2ObjectMapperBuilder.build()))
                .build();
        return retrofit.create(TianService.class);
    }
}

字段重命名

在对接 API 时, 经常会需要对 JSON 串的字段进行重命名, 比如下划线改驼峰. 建议使用 @JsonProperty 注解. 不过, 如果这个 Java 对象经过 Spring MVC 再次转为 JSON 串时, 又恢复原样了, 因为 Spring MVC 也是用的 jackson.

优雅的解决方案是用 fastjson 替换 Spring MVC 的默认配置.

@Configuration
public class FastJsonHttpMessageConverterConfig {
    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverters() {
        SerializeConfig serializeConfig = SerializeConfig.globalInstance;

        // 初始化一个转换器配置
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializeConfig(serializeConfig);
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat,
                SerializerFeature.WriteNonStringKeyAsString,
                SerializerFeature.DisableCircularReferenceDetect);

        // 初始化转换器
        FastJsonHttpMessageConverter fastConvert = new FastJsonHttpMessageConverter();
        fastConvert.setFastJsonConfig(fastJsonConfig);
        fastConvert.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));

        // 将配置设置给转换器并添加到HttpMessageConverter转换器列表中
        return new HttpMessageConverters(fastConvert);
    }
}

数据缓存

接下来, 在我们 Controller 层调用 TianService 服务即可, 为了减少对 API 的请求次数, 增加一个 Redis 缓存, 设置 Key 的缓存时间为 10min.

由于我的云服务器之前没有安装 Redis, 这里先装一下

yum install -y redis
service redis start
chkconfig redis on

接下来就可以正常使用了. Java 访问 Redis, 我们用 spring-boot-starter-data-redis, 先引入依赖

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

版本号 spring-boot 管理好了. 注意这里需要引入 jedis, 或 common-pool2, 用于生成连接池相关 Bean. 由于 spring-data-redis 自带的 redisTemplate Bean 是 RedisTemplate<Object, Object>, 使用不方便, 因此我们重新定义如下.

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

现在就可以很方便地缓存/读取数据啦!

redisTemplate.opsForValue().set(KEY_NCOV, response, 10, TimeUnit.MINUTES)
redisTemplate.opsForValue().get(KEY_NCOV)

视图层

采用前后端完全分离的方式, 先增加一个 /ncov.html 页面, 它本质是一个 FreeMarker 页面, 引入了 Vue 打包后的 js 和 css. 其他数据采用 api 的方式给到前端. 前端用 axios 将数据引入 Vuex Store 中.

新增 src/ncov.js 文件, 并添加到 webpack 构建配置的入口列表中. 入口文件的主要内容是

new Vue({
  el: '#app',
  store,
  render: h => h(NCov)
});

其中 NCov 就是主要的视图页面.

疫情地图

丁香医生等平台提供的疫情地图, 用的是分段函数划分的严重等级, 这样模糊了一些区别. 这里用 EChart 的地图工具, 然后用“确诊病例数除以100”给地图上色.

我在 Vue 中使用 EChart, 一般用自己简单封装的 EChart 组件. 注意: 为了使用地图工具, 还需要引入对应的 js.

import EChart from 'hui-vue/src/components/EChart';
import 'echarts/map/js/china';

进一步思考

宅在家的这段时间, 阅读了很多内容, 其中, 感触最深的是一位CEO给员工的防疫指南:在不确定的世界强悍地活带来的思考. 自我隔离这段时间是很好的自学, 提升自我, 思考未来的时期.

我们正在经历一次可能漫长而危险的不确定性时期, 人民和国家损失惨重, 以人为本, 不计成本地建设 "小汤山" 医院, 壮士断腕, 做出封城封村, 延迟开工开学的决定. 每个人都关注疫情发展, 自我隔离, 坚信疫情拐点很快就会到来!!!

更多相关资料