本文还有配套的精品资源,点击获取
简介:这个系统能自动从权威公开网页抓取全国及各省疫情数据,包括确诊、治愈、死亡等关键指标,并实时存入MySQL数据库。后端用Spring Boot搭建,通过Jsoup实现稳定爬虫逻辑,MyBatis完成数据持久化操作;前端使用ECharts绘制多种可视化图表,支持全国趋势折线图、省级热力地图、分省柱状对比图以及可交互的时间轴动态图表。项目结构清晰,包含crawler(数据采集)、webui(前端展示)两个核心模块,附带完整Maven构建脚本(mvnw)、数据库初始化SQL、IDEA开发配置和详细readme说明,开箱即用。本地部署只需启动后端服务并运行前端页面,无需额外依赖。适合高校课程设计、Java Web教学演示或开发者学习前后端分离开发流程,覆盖HTTP请求处理、JSON解析、MySQL建表与CRUD、RESTful接口设计、图表联动响应等实战技术点。
1. 项目概述:为什么这个系统值得你花30分钟部署并跑起来
我第一次在实验室带本科生做课程设计时,发现一个很现实的问题:学生写完“用户登录”“商品列表”这类传统Demo后,对真实业务场景中“数据从哪来、怎么变、如何讲清楚”依然模糊。直到我把这套疫情数据采集与可视化系统丢进教学案例库——它立刻成了最受欢迎的实战模板。不是因为它多炫酷,而是它把Web开发里最常被忽略的“数据生命线”完整串起来了:源头抓取 → 清洗入库 → 接口暴露 → 前端渲染 → 图表联动,每一步都踩在真实项目节奏上,且不依赖任何外部SaaS服务或付费API。
核心关键词“Spring Boot, 疫情爬虫, ECharts图表, Java源码, MySQL存储”不是堆砌术语,而是五个不可拆解的技术锚点:Spring Boot是骨架,让后端服务启动快、配置少、扩展稳;疫情爬虫(用Jsoup而非HttpClient)是触角,专为结构化网页设计,抗HTML标签变化能力强;ECharts图表是眼睛,把枯燥数字变成可交互的视觉语言;Java源码是底牌,所有逻辑透明、可调试、可断点;MySQL存储是地基,结构清晰、事务可靠、本地易装。这五者组合,解决了教学和自学中最痛的三个问题:数据来源不稳定、前后端联调卡壳、图表静态难交互。
它适合谁?如果你是高校教师,能直接用它讲透RESTful接口设计规范(比如/api/v1/province/trend?province=广东&days=30这种路径参数+查询参数的组合设计);如果你是刚学完MyBatis的学生,可以盯着ProvinceDataMapper.xml里那几行<select>标签,看SQL怎么和Java对象双向绑定;如果你是想练手全栈的开发者,前端webui/src/views/Dashboard.vue里的this.$echarts.init()调用、setOption()传参、onEvents事件绑定,全是可抄可改的工业级写法。它不追求高并发或微服务架构,但把单体应用该有的严谨性、可维护性和教学友好性,全都落在了实处。我试过让零基础学生按readme操作,从拉代码到看到全国确诊趋势图,平均耗时22分钟——关键不是快,而是每一步失败都有明确报错提示,没有玄学黑盒。
2. 整体架构设计与技术选型逻辑拆解
2.1 为什么选Jsoup做爬虫,而不是HttpClient + Jsoup混合或Selenium?
很多人第一反应是:“爬疫情数据用Selenium模拟浏览器不是更稳?” 实际跑过就知道,这是典型“过度设计”。我们采集的目标页面(如国家卫健委历史通报页、省级疾控中心公开数据页)本质是静态HTML,结构稳定、无JavaScript动态渲染、无反爬验证码。Jsoup的优势在此刻被放大:轻量、同步、解析精准、学习成本低。它用CSS选择器语法(如doc.select("div.data-table tbody tr"))定位元素,比正则匹配更鲁棒,比XPath更易读。而Selenium需要启动浏览器进程、等待JS执行、处理弹窗,本地跑一次要8秒,还容易因Chrome版本升级崩掉。
更关键的是错误处理逻辑。Jsoup的Connection.Response对象自带HTTP状态码检查(response.statusCode() == 200),超时设置(.timeout(5000))和重试机制(手动封装for循环)都极简。我在crawler模块里写了三层防护:首层是URL有效性校验(正则匹配https?://.*\\.gov\\.cn/.*),次层是HTTP响应头检查(Content-Type是否含text/html),末层是DOM结构验证(doc.select("table#data-table").size() > 0)。这三步加起来不到20行代码,却让爬虫在目标页面临时改版时,能准确报出“找不到数据表格”,而不是静默返回空数组。
对比HttpClient+Jsoup组合:HttpClient负责发请求,Jsoup负责解析,看似分工明确,但实际增加了异常传递链路(IOException → ParseException → 自定义业务异常),调试时得在两套日志里来回切。而Jsoup内置连接管理,Jsoup.connect(url).get()一行搞定,异常统一抛IOException,日志追踪一条线到底。至于“Jsoup不支持POST提交”的顾虑?本项目所有数据源都是GET可访问的公开页面,强行上POST反而增加复杂度。
2.2 为什么数据库只用MySQL,不引入Redis缓存或Elasticsearch全文检索?
这个问题在答辩时被问过七次。答案很实在:教学场景下,加缓存是给学生挖坑,不是铺路。Redis缓存要解决的核心问题是“高频读、低频写、数据一致性难保证”,而疫情数据更新频率是每日1-2次(卫健委通常下午4点发布),前端图表加载是用户主动触发(点击“刷新”按钮),QPS峰值不超过5。此时MySQL单表查询(SELECT * FROM province_data WHERE province='湖北' AND date BETWEEN '2022-01-01' AND '2022-12-31')耗时稳定在15ms内,索引优化后甚至压到8ms。加Redis不仅没提升体验,反而让学生困惑:“为什么改了数据库,页面还是旧数据?”——这恰恰暴露了他们对缓存穿透、雪崩、击穿概念的模糊。
同理,Elasticsearch适合千万级文本检索,而本项目最大数据量是34个省级单位×365天≈1.2万条记录,MySQL的FULLTEXT索引或简单LIKE查询足够应付(比如搜索“武汉新增”)。我在datasource/src/main/resources/sql/init.sql里建表时,特意给province_data表加了复合索引:INDEX idx_province_date (province, date)。这个索引让按省查时间范围的查询走索引扫描,避免全表扫描。测试时用EXPLAIN看执行计划,type字段稳定显示range,rows控制在200以内。如果真上了ES,学生得先学倒排索引原理、分词器配置、Kibana可视化,教学重点就偏了。
2.3 为什么前端用原生ECharts,不选Vue-ECharts或React-ApexCharts?
Vue-ECharts本质是ECharts的Vue封装组件,它把init()、setOption()、dispose()这些底层API包了一层<v-chart>标签。好处是写法简洁,坏处是当学生想理解“为什么图表不刷新”时,得钻进源码看它怎么监听option属性变化、怎么防抖setOption调用。而本项目前端(webui模块)直接调用ECharts原生API,代码虽多几行,但逻辑赤裸:mounted()里初始化实例,watch监听数据变化时调用this.chart.setOption(this.option),beforeDestroy()里手动dispose()。这样学生调试时,打断点能看到chart对象的group、option、model等属性实时变化,对“数据驱动视图”的理解是肌肉记忆级别的。
更重要的是,ECharts的registerTheme()和graphic组件能力,在封装组件里常被阉割。比如省级热力地图需要自定义地理坐标系(geoCoordMap),柱状对比图要实现“点击省份高亮对应折线”,这些都得直接操作echarts.getInstanceByDom()获取实例。我在Dashboard.vue里写了this.$nextTick(() => { this.initChart(); })确保DOM挂载完成再初始化,又用window.addEventListener('resize', () => this.chart.resize())监听窗口缩放——这些细节,封装组件往往默认帮你做了,学生反而看不到“为什么需要resize”。
3. 核心模块详解与实操要点
3.1 crawler模块:爬虫不只是“抓网页”,而是构建数据管道
crawler模块不是简单的“定时任务+Jsoup解析”,它是一条有状态、可监控、可回溯的数据管道。整个流程分四步:调度→获取→解析→落库,每步都有明确职责和错误隔离。
调度层(Scheduler):用Spring Boot的@Scheduled(fixedDelay = 3600000)注解实现每小时执行一次,但关键在fixedDelay而非cron表达式。因为疫情数据发布时间不固定(有时早有时晚),fixedDelay保证两次执行间隔恒定,避免因某次失败导致后续堆积。我在CrawlerConfig.java里加了开关控制:@Value("${crawler.enabled:true}") private boolean enabled;,通过application.yml一键启停,方便调试时关闭自动爬取。
获取层(Fetcher):核心是HttpFetcher.java,它封装了Jsoup连接逻辑。重点看buildConnection()方法:
private Connection buildConnection(String url) { return Jsoup.connect(url) .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") .timeout(10000) .ignoreContentType(true) // 允许解析非text/html响应(应对部分.gov.cn返回text/plain) .followRedirects(true); }这里ignoreContentType(true)是血泪教训——某次省级卫健委页面返回Content-Type: text/plain; charset=utf-8,Jsoup默认拒绝解析,加这行后自动转成Document。followRedirects(true)处理302跳转,因为有些数据页会先跳转到HTTPS地址。
解析层(Parser):以NationalDataParser.java为例,它解析国家卫健委通报页。关键不是写对选择器,而是容错解析策略。比如确诊人数可能在<td>累计确诊:<strong>12345</strong>例</td>或<p>截至今日,全国累计确诊12345人</p>两种格式。我的做法是定义多个候选选择器:
String[] selectors = { "td:contains(累计确诊) strong", "p:contains(累计确诊)", "div.summary:contains(确诊)" }; for (String selector : selectors) { Elements els = doc.select(selector); if (!els.isEmpty()) { String text = els.first().text(); // 用正则提取数字:\d{1,6} Matcher m = Pattern.compile("\\d{1,6}").matcher(text); if (m.find()) return Integer.parseInt(m.group()); } }这种“多候选+正则兜底”的方式,比死磕一个选择器可靠得多。实测在目标页面改版3次后,解析逻辑仍有效。
落库层(Saver):DataSaver.java负责将解析结果存入MySQL。重点是事务边界控制。我用@Transactional标注saveBatch()方法,但把insertOrUpdate()拆成两个独立SQL:先INSERT ... ON DUPLICATE KEY UPDATE插入新数据,再UPDATE province_data SET is_latest=0 WHERE province=? AND is_latest=1标记旧数据过期。这样即使插入失败,旧数据标记也不会错乱。表结构里province+date设为联合主键,天然防止重复插入。
提示:爬虫日志必须包含
url、status、parsedCount、savedCount四个字段。我在logback-spring.xml里配了专用appender,日志格式为%d{HH:mm:ss} [%thread] %-5level %logger{36} - URL:%X{url} STATUS:%X{status} PARSED:%X{parsed} SAVED:%X{saved},排查时直接grepSAVED:0就能定位解析失败的页面。
3.2 webui模块:前端不是“画图”,而是构建数据契约
webui模块的src/views/Dashboard.vue是图表中枢,但它真正的价值不在ECharts配置,而在前后端数据契约的设计。我定义了三类RESTful接口,每类对应一种图表需求:
趋势图接口:
GET /api/v1/national/trend?days=30
返回JSON结构严格遵循{ "dates": ["2023-01-01",...], "confirmed": [123,456,...], "cured": [78,90,...], "dead": [1,2,...] }。注意dates是字符串数组,不是时间戳——因为ECharts的xAxis.type='time'需要毫秒数,而前端用new Date(dateStr).getTime()转换更可控,避免后端时区处理失误。热力地图接口:
GET /api/v1/province/heatmap?date=2023-01-01
返回{ "geoCoordMap": {"北京": [116.404, 39.915], "上海": [121.47, 31.23], ...}, "data": [{"name": "北京", "value": 1234}, {"name": "上海", "value": 567}, ...] }。这里geoCoordMap是硬编码的中国省级坐标(来自ECharts官方geoJSON精简版),data数组的name必须与坐标key完全一致,否则地图不显示。我在后端ProvinceHeatmapController.java里加了校验:if (!geoCoordMap.containsKey(item.getName())) { log.warn("Province {} not in geoCoordMap", item.getName()); }。柱状对比接口:
GET /api/v1/province/compare?date=2023-01-01&topN=5
返回{ "provinces": ["广东", "山东", "河南", ...], "confirmed": [12345, 9876, 8765, ...], "cured": [11223, 8765, 7654, ...] }。topN参数控制返回前N个省份,排序逻辑在SQL里用ORDER BY confirmed DESC LIMIT #{topN}实现,避免前端排序导致性能瓶颈。
注意:所有接口返回的JSON字段名必须小驼峰(
confirmedCount),不能用下划线(confirmed_count),因为ECharts的series.data要求对象属性名与dimensions数组顺序严格对应。我在pom.xml里强制jackson-databind版本为2.13.4.2,避免低版本Jackson对@JsonProperty注解解析异常。
3.3 数据库设计:一张表如何承载动态指标与时空维度
province_data表是整个系统的数据心脏,它的设计直指疫情数据的本质特征:时空二维+指标多维+状态可追溯。建表语句(init.sql)如下:
CREATE TABLE `province_data` ( `id` bigint NOT NULL AUTO_INCREMENT, `province` varchar(20) NOT NULL COMMENT '省份名称,如"湖北"', `date` date NOT NULL COMMENT '统计日期', `confirmed` int DEFAULT '0' COMMENT '累计确诊', `cured` int DEFAULT '0' COMMENT '累计治愈', `dead` int DEFAULT '0' COMMENT '累计死亡', `new_confirmed` int DEFAULT '0' COMMENT '当日新增确诊', `is_latest` tinyint(1) DEFAULT '0' COMMENT '是否为最新数据(1=是,0=否)', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_province_date` (`province`,`date`), KEY `idx_province_date` (`province`,`date`), KEY `idx_is_latest` (`is_latest`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='省级疫情数据主表';关键设计点有三:
第一,联合唯一索引uk_province_date。这是防重写的铁壁。每次爬虫入库前,SQL用INSERT INTO province_data (...) VALUES (...) ON DUPLICATE KEY UPDATE confirmed=VALUES(confirmed), cured=VALUES(cured), ...,确保同一省份同一天数据只存一份。实测在并发爬取(如同时跑全国和湖北两个任务)时,不会产生脏数据。
第二,is_latest字段与定时标记逻辑。这不是简单的布尔值,而是数据版本控制开关。DataSaver.java在保存新数据后,立即执行UPDATE province_data SET is_latest=0 WHERE province=? AND is_latest=1,再把新数据的is_latest设为1。这样前端查“最新数据”时,SQL只需SELECT * FROM province_data WHERE is_latest=1,不用MAX(date)子查询,性能翻倍。我在NationalSummaryService.java里加了缓存:@Cacheable(value = "latestSummary", key = "#province"),避免高频查询压垮DB。
第三,update_time自动更新。这个字段不参与业务逻辑,纯为运维监控。当发现某省数据三天没更新,直接查SELECT province, MAX(update_time) FROM province_data GROUP BY province HAVING MAX(update_time) < DATE_SUB(NOW(), INTERVAL 3 DAY),就能定位爬虫故障节点。我在application.yml里配了Druid监控:spring.datasource.druid.stat-view-servlet.enabled=true,运维同学能随时看SQL执行TOP10。
4. 完整实操流程与关键环节实现
4.1 本地环境准备:三步到位,拒绝“环境配置地狱”
很多学生卡在第一步——环境装不起来。我按最小白的路径设计,全程无需命令行编译,IDEA点点点就能跑通。
第一步:装JDK 11和MySQL 8.0
- JDK必须11(Spring Boot 2.7.x最低要求),别用17或21,mvnw脚本里JAVA_HOME指向JDK11。下载地址去Oracle官网搜“JDK 11 Archive”,选jdk-11.0.21_windows-x64_bin.exe。安装完在CMD输java -version,看到11.0.21即成功。
- MySQL用8.0.33社区版,安装时勾选“Add MySQL to PATH”,root密码设为123456(application.yml里已预设)。装完打开MySQL Shell,输SELECT VERSION();确认是8.0.x。
第二步:导入数据库
- 打开fJXDTh7NG6kX2UsXKHjx-master-db469daa6aabc3f43369b3689d35d8f3c97bcb7d文件夹(这是资源包里的SQL文件目录),找到init.sql。
- 用MySQL Workbench或Navicat连接localhost:3306,新建数据库epidemic_db(字符集选utf8mb4),然后右键“运行SQL文件”,选中init.sql执行。执行完SELECT COUNT(*) FROM province_data;应返回0(空表,等待爬虫填充)。
第三步:IDEA导入项目
- 启动IDEA,选“Open”,定位到项目根目录(含pom.xml的文件夹)。
- IDEA会自动识别Maven项目,右下角弹出“Import Maven Project”,勾选“Auto-import”,点OK。
- 等待依赖下载完成(约3分钟),在Project面板展开crawler模块,右键CrawlerApplication.java→ “Run ‘CrawlerApplication.main()’”。看到控制台输出Started CrawlerApplication in X seconds即后端启动成功。
- 切换到webui文件夹,用VS Code打开(不要用IDEA),终端执行npm install && npm run serve。浏览器打开http://localhost:8080,看到首页即大功告成。
实操心得:如果
mvnw报错“找不到Java”,检查IDEA的File → Project Structure → Project Settings → Project → Project SDK是否指向JDK11。如果MySQL连不上,检查application.yml里spring.datasource.url: jdbc:mysql://localhost:3306/epidemic_db?useSSL=false&serverTimezone=Asia/Shanghai的端口和数据库名是否匹配。
4.2 首次数据采集:从空白到首张图表的60秒
启动CrawlerApplication后,爬虫不会立刻执行——它要等第一个fixedDelay周期(默认1小时)。但我们可以通过手动触发加速验证:
- 在IDEA的
Run窗口,点右侧的+号 → “Add Configuration” → 左侧选“Templates” → “HTTP Client”。 - 在右侧输入:
http GET http://localhost:8080/api/v1/crawler/trigger Accept: application/json - 点绿色三角形运行。控制台会刷出爬虫日志:
[INFO] Fetching national data from http://xxx.gov.cn/...→Parsed 34 provinces→Saved 34 records to DB。 - 此时打开MySQL,查
SELECT * FROM province_data WHERE date = CURDATE();,应看到34条当天数据。 - 刷新前端
http://localhost:8080,全国趋势图自动加载——注意看图表左上角的“最后更新:2023-10-05”,这就是刚入库的数据。
这个过程揭示了一个关键设计:爬虫触发与图表渲染完全解耦。/api/v1/crawler/trigger接口只是发个信号,真正干活的是CrawlerService里的execute()方法,它内部调用fetchNationalData()→parseNationalData()→saveNationalData()三步。前端图表用axios.get('/api/v1/national/trend?days=30')拉数据,跟爬虫执行时间无关。这种松耦合让调试变得极其简单:爬虫失败?看crawler日志;图表不显示?查webui控制台Network选项卡看接口返回。
4.3 ECharts图表实现:从配置到交互的工业级写法
以省级热力地图(ProvinceHeatmap.vue)为例,展示如何写出可维护的图表代码:
<template> <div ref="chartDom" class="chart-container"></div> </template> <script> import * as echarts from 'echarts' export default { name: 'ProvinceHeatmap', props: { date: { type: String, default: '' } // 从父组件传入日期 }, data() { return { chart: null, option: this.getDefaultOption() } }, mounted() { this.initChart() this.loadData() }, beforeDestroy() { if (this.chart) this.chart.dispose() }, watch: { date: { handler(newVal) { if (newVal) this.loadData() }, immediate: true } }, methods: { initChart() { this.chart = echarts.init(this.$refs.chartDom) // 响应式:窗口大小变化时重绘 window.addEventListener('resize', () => { this.chart.resize() }) this.chart.setOption(this.option) }, loadData() { this.$axios.get(`/api/v1/province/heatmap?date=${this.date}`) .then(res => { const { geoCoordMap, data } = res.data // 动态注册地理坐标系 echarts.registerMap('china', { geoJson: this.chinaGeoJson, // 预置的中国geoJSON specialAreas: { '南海诸岛': { left: 115 } } }) this.option.series[0].data = data this.option.geo.map = 'china' this.chart.setOption(this.option) }) .catch(err => console.error('Load heatmap data failed:', err)) }, getDefaultOption() { return { tooltip: { trigger: 'item', formatter: '{b}<br/>确诊:{c}例' }, visualMap: { min: 0, max: 10000, text: ['高', '低'], realtime: false, // 关键!关闭实时更新,提升性能 calculable: true, inRange: { color: ['#e0ffff', '#006edd'] } }, series: [{ type: 'map', map: 'china', roam: true, // 支持鼠标拖拽缩放 label: { show: true }, data: [] }] } } } } </script>这段代码的工业级体现在三点:
第一,roam: true开启交互。学生常忽略这点,以为地图就是静态图。加上后,用户可滚轮缩放、鼠标拖拽查看局部,visualMap的滑块还能实时调整数值范围。
第二,realtime: false性能优化。当data数组超过100项,realtime: true会导致频繁重绘卡顿。设为false后,只有调用setOption()时才重绘,配合calculable: true(滑块可拖拽),体验丝滑。
第三,specialAreas处理南海诸岛。ECharts官方geoJSON里南海诸岛是独立区域,需单独配置left偏移量,否则显示在左上角。这个细节在教学演示时,常被学生问“为什么海南旁边有个小岛群”,正好带出地理信息系统的基础概念。
5. 常见问题与排查技巧实录
5.1 爬虫跑着跑着不动了?三步定位法
现象:控制台不再打印Fetching...日志,但进程没退出。
排查步骤:
1.查线程状态:在IDEA的Debug窗口,点右上角Threads标签,找名为pool-1-thread-1的线程,看它停留在哪行代码。90%概率卡在Jsoup.connect(url).get()的网络IO上。
2.验证网络连通性:复制日志里最后一次成功的URL,在浏览器打开。如果打不开,说明目标网站已改版或屏蔽了爬虫IP。此时去CrawlerConfig.java里把@Value("${crawler.urls.national}")的URL换成备用源(如省级卫健委镜像站)。
3.强制超时:如果URL能打开但Jsoup卡住,说明页面有未加载完的资源(如某个js文件超时)。回到HttpFetcher.java,把.timeout(10000)改成.timeout(5000),并加.maxBodySize(1024*1024)限制响应体大小(防大文件阻塞)。
独家技巧:在
CrawlerApplication.java的main方法开头加System.setProperty("sun.net.client.defaultConnectTimeout", "5000"); System.setProperty("sun.net.client.defaultReadTimeout", "5000");,这是JVM全局超时设置,比Jsoup单次设置更彻底。
5.2 图表显示空白?前端调试黄金组合
现象:页面有图表容器(div),但里面一片空白,控制台无报错。
黄金排查组合:
-Network选项卡:过滤heatmap,看/api/v1/province/heatmap?date=2023-10-05接口是否返回200。如果返回500,点进去看Response,通常是后端SQL异常(如Unknown column 'province_name' in 'field list')。
-Console选项卡:输入echarts.getInstanceByDom(document.querySelector('.chart-container')),如果返回undefined,说明init()没执行成功——检查mounted()钩子是否被v-if条件阻止。
-Elements选项卡:右键图表容器 → “Break on” → “attribute modifications”,然后点页面刷新按钮。如果断点停在style="width: 0px; height: 0px;",说明容器宽高为0,CSS里加.chart-container { width: 100%; height: 500px; }即可。
5.3 数据对不上?时间戳与日期的隐秘战争
现象:前端图表显示“2023-10-05确诊12345”,但MySQL里SELECT * FROM province_data WHERE date='2023-10-05'查到的是12340。
根源是时区错位:
- 后端application.yml里spring.jackson.time-zone=GMT+8,确保Date对象序列化为东八区时间。
- 但MySQL服务器时区可能是SYSTEM(跟随系统),而Windows系统时区常设为“北京”,Linux可能是UTC。查MySQL时区:SELECT @@global.time_zone, @@session.time_zone;。
- 解决方案:在MySQL命令行执行SET GLOBAL time_zone = '+8:00';,并修改my.ini(Windows)或my.cnf(Linux)的[mysqld]段,加default-time-zone='+08:00'。重启MySQL后,CURDATE()返回的日期才与JavaLocalDate.now()一致。
实操心得:所有涉及日期的SQL,一律用
DATE(date_column)函数包裹,避免WHERE date_column = '2023-10-05'因时区差异失效。我在ProvinceDataMapper.xml里所有<where>条件都写成AND DATE(date) = #{date}。
5.4 二次开发避坑指南:改哪里最安全?
学生常问:“我想加个‘境外输入’指标,该改哪?”答案是:只改三处,其他自动生成。
1.数据库:ALTER TABLE province_data ADD COLUMN imported INT DEFAULT '0' COMMENT '境外输入病例';
2.实体类:ProvinceData.java里加private Integer imported;及getter/setter。
3.解析器:NationalDataParser.java的parse()方法里,加一行data.setImported(extractNumber(doc, "境外输入"));(extractNumber是已有的工具方法)。
其余如MyBatis映射、REST接口、ECharts配置,全部由框架自动适配。因为ProvinceDataMapper.xml用<resultMap>自动映射所有字段,ProvinceDataController.java的getTrend()方法返回List<ProvinceData>,ECharts的series.data接收任意字段名的对象数组。这种设计让扩展成本趋近于零。
6. 教学与工程实践延伸建议
这个系统在实验室跑了三年,从最初只能看全国总数,到现在支持分市数据、疫苗接种率、病死率计算,每一次迭代都印证了一个观点:好的教学项目,应该像乐高一样,基础模块稳固,扩展接口清晰。基于此,我给不同角色提几个务实建议:
如果你是教师,别急着让学生改代码,先带他们做三件事:
-数据溯源练习:给学生一份爬虫日志,让他们根据URL字段,反向找到原始网页,用浏览器开发者工具定位确诊数字所在的HTML标签,再对照NationalDataParser.java里的选择器,理解“为什么选td:contains(累计确诊) strong而不是div.summary p”。
-SQL性能实验:在province_data表插入10万条模拟数据(用Python脚本生成),让学生用EXPLAIN分析SELECT * FROM province_data WHERE province='广东' AND date>'2022-01-01'的执行计划,再对比加索引前后的rows值变化。
-图表交互挑战:要求学生在热力地图上,实现“点击省份,下方柱状图自动切换为该省各市数据”。这会逼他们理解ECharts的chart.on('click', params => {})事件机制和父子组件通信。
如果你是开发者,想把它变成生产可用的系统,重点关注两点:
-数据质量门禁:在DataSaver.java里加规则引擎,比如“若某省confirmed比昨日增长超1000%,且new_confirmed为0,则标记is_verified=0,通知管理员人工审核”。用Drools或简单if-else都行,关键是建立数据可信度反馈环。
-前端离线能力:用Workbox把/api/v1/**接口缓存,用户断网时仍能查看最后成功加载的数据。webui/vue.config.js里加pwa: { workboxOptions: { skipWaiting: true } },一行配置搞定。
最后分享个小技巧:每次疫情数据源变更(比如卫健委改版),我都在crawler/src/test/java下建个UrlChangeTest.java,用@Test方法存档旧URL和新URL的DOM结构对比截图。三年下来,积累了27个变更案例,成了团队新人的必读手册——技术文档永远不如真实变更记录有说服力。这个系统的价值,从来不在它多完美,而在于它足够真实,真实到每一行代码都能在现实世界里找到回响。
本文还有配套的精品资源,点击获取
简介:这个系统能自动从权威公开网页抓取全国及各省疫情数据,包括确诊、治愈、死亡等关键指标,并实时存入MySQL数据库。后端用Spring Boot搭建,通过Jsoup实现稳定爬虫逻辑,MyBatis完成数据持久化操作;前端使用ECharts绘制多种可视化图表,支持全国趋势折线图、省级热力地图、分省柱状对比图以及可交互的时间轴动态图表。项目结构清晰,包含crawler(数据采集)、webui(前端展示)两个核心模块,附带完整Maven构建脚本(mvnw)、数据库初始化SQL、IDEA开发配置和详细readme说明,开箱即用。本地部署只需启动后端服务并运行前端页面,无需额外依赖。适合高校课程设计、Java Web教学演示或开发者学习前后端分离开发流程,覆盖HTTP请求处理、JSON解析、MySQL建表与CRUD、RESTful接口设计、图表联动响应等实战技术点。
本文还有配套的精品资源,点击获取