本文还有配套的精品资源点击获取简介这个系统能自动从权威公开网页抓取全国及各省疫情数据包括确诊、治愈、死亡等关键指标并实时存入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广东days30这种路径参数查询参数的组合设计如果你是刚学完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行代码却让爬虫在目标页面临时改版时能准确报出“找不到数据表格”而不是静默返回空数组。对比HttpClientJsoup组合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字段稳定显示rangerows控制在200以内。如果真上了ES学生得先学倒排索引原理、分词器配置、Kibana可视化教学重点就偏了。2.3 为什么前端用原生ECharts不选Vue-ECharts或React-ApexChartsVue-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; charsetutf-8Jsoup默认拒绝解析加这行后自动转成Document。followRedirects(true)处理302跳转因为有些数据页会先跳转到HTTPS地址。解析层Parser以NationalDataParser.java为例它解析国家卫健委通报页。关键不是写对选择器而是容错解析策略。比如确诊人数可能在td累计确诊strong12345/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次后解析逻辑仍有效。落库层SaverDataSaver.java负责将解析结果存入MySQL。重点是事务边界控制。我用Transactional标注saveBatch()方法但把insertOrUpdate()拆成两个独立SQL先INSERT ... ON DUPLICATE KEY UPDATE插入新数据再UPDATE province_data SET is_latest0 WHERE province? AND is_latest1标记旧数据过期。这样即使插入失败旧数据标记也不会错乱。表结构里provincedate设为联合主键天然防止重复插入。提示爬虫日志必须包含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?days30返回JSON结构严格遵循{ dates: [2023-01-01,...], confirmed: [123,456,...], cured: [78,90,...], dead: [1,2,...] }。注意dates是字符串数组不是时间戳——因为ECharts的xAxis.typetime需要毫秒数而前端用new Date(dateStr).getTime()转换更可控避免后端时区处理失误。热力地图接口GET /api/v1/province/heatmap?date2023-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?date2023-01-01topN5返回{ 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT省级疫情数据主表;关键设计点有三第一联合唯一索引uk_province_date。这是防重写的铁壁。每次爬虫入库前SQL用INSERT INTO province_data (...) VALUES (...) ON DUPLICATE KEY UPDATE confirmedVALUES(confirmed), curedVALUES(cured), ...确保同一省份同一天数据只存一份。实测在并发爬取如同时跑全国和湖北两个任务时不会产生脏数据。第二is_latest字段与定时标记逻辑。这不是简单的布尔值而是数据版本控制开关。DataSaver.java在保存新数据后立即执行UPDATE province_data SET is_latest0 WHERE province? AND is_latest1再把新数据的is_latest设为1。这样前端查“最新数据”时SQL只需SELECT * FROM province_data WHERE is_latest1不用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.enabledtrue运维同学能随时看SQL执行TOP10。4. 完整实操流程与关键环节实现4.1 本地环境准备三步到位拒绝“环境配置地狱”很多学生卡在第一步——环境装不起来。我按最小白的路径设计全程无需命令行编译IDEA点点点就能跑通。第一步装JDK 11和MySQL 8.0- JDK必须11Spring Boot 2.7.x最低要求别用17或21mvnw脚本里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密码设为123456application.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?useSSLfalseserverTimezoneAsia/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?days30)拉数据跟爬虫执行时间无关。这种松耦合让调试变得极其简单爬虫失败看crawler日志图表不显示查webui控制台Network选项卡看接口返回。4.3 ECharts图表实现从配置到交互的工业级写法以省级热力地图ProvinceHeatmap.vue为例展示如何写出可维护的图表代码template div refchartDom classchart-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?date2023-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”然后点页面刷新按钮。如果断点停在stylewidth: 0px; height: 0px;说明容器宽高为0CSS里加.chart-container { width: 100%; height: 500px; }即可。5.3 数据对不上时间戳与日期的隐秘战争现象前端图表显示“2023-10-05确诊12345”但MySQL里SELECT * FROM province_data WHERE date2023-10-05查到的是12340。根源是时区错位- 后端application.yml里spring.jackson.time-zoneGMT8确保Date对象序列化为东八区时间。- 但MySQL服务器时区可能是SYSTEM跟随系统而Windows系统时区常设为“北京”Linux可能是UTC。查MySQL时区SELECT global.time_zone, session.time_zone;。- 解决方案在MySQL命令行执行SET GLOBAL time_zone 8:00;并修改my.iniWindows或my.cnfLinux的[mysqld]段加default-time-zone08: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()方法返回ListProvinceDataECharts的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 date2022-01-01的执行计划再对比加索引前后的rows值变化。-图表交互挑战要求学生在热力地图上实现“点击省份下方柱状图自动切换为该省各市数据”。这会逼他们理解ECharts的chart.on(click, params {})事件机制和父子组件通信。如果你是开发者想把它变成生产可用的系统重点关注两点-数据质量门禁在DataSaver.java里加规则引擎比如“若某省confirmed比昨日增长超1000%且new_confirmed为0则标记is_verified0通知管理员人工审核”。用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接口设计、图表联动响应等实战技术点。本文还有配套的精品资源点击获取