简介:一套基于Java技术栈的疫情数据可视化分析系统完整源码,面向需要学习Spring Boot、MyBatis、MySQL与ECharts整合开发的中高级开发者,也适合作为毕业设计或课程项目的参考实现。项目内置爬虫程序自动抓取COVID-19累计确诊、治愈、死亡等数据,并通过ECharts折线图、柱状图、饼图等方式动态展示,帮助理解疫情趋势和统计数据。压缩包共106个文件,以28个Java源码、13个JavaScript、13个XML、10个CSS等为主,涵盖后端业务逻辑、前端图表配置、数据库脚本及项目配置文件,整体约10MB,目录结构清晰,便于按模块检索。已有1275人学习下载。通过源码可掌握从数据采集、存储到可视化展示的完整链路,尤其能深入理解Spring Boot自动配置、MyBatis动态SQL与MySQL表设计,以及ECharts图表交互和数据绑定的具体写法,适合直接复用或二次扩展。
1. Java基于ECharts的数据可视化疫情分析系统源码,先想清楚它到底解决什么
Java基于ECharts的数据可视化疫情分析系统源码,这类项目在高校毕设和 Java 面试题里出现频率很高,但很多人卡在同一个地方:后端返回的数据结构跟图表的 series 对不上。真正的难点不是画图,而是把 CSV 里的疫情原始数据清洗成折线图、柱状图、中国地图各自要的格式。这套系统的价值集中在四件事:数据落地、聚合统计、接口输出、图表联动。适合两类人:刚学完 Spring Boot 和 ECharts 基础、需要一个完整闭环练手的新手,以及准备面试、想拿数据可视化项目当项目亮点的 Java 开发。读完你能自己搭出一个可复现的大屏,并且知道哪几步最容易翻车。
2. 选型与整体架构:Java 后端 + ECharts 前端,数据链路怎么搭
2.1 疫情数据从哪来:静态 CSV 与第三方接口两条路
疫情分析系统第一步不是写接口,而是把数据拿到手。常见做法是两条路:一是直接使用开源数据仓库维护的疫情 CSV 文件,字段通常包含国家、省份、日期、累计确诊、累计治愈、累计死亡;二是调用第三方汇总接口,这种接口早期很多,但后来接口地址频繁变动、字段命名不统一,还容易出现跨域和限流,拿来当课堂作业可以,放到正式项目里非常不可靠。
所以我看过的多数源码包采用的是第一条路:把一份 CSV 放在 src/main/resources/data 目录下,启动时一次加载进内存。CSV 的字段设计一般长这样:
| 字段名 | 类型 | 说明 |
|---|---|---|
| province_name | String | 省份名,建议不带“省/市”后缀 |
| date | String | 日期,格式 yyyy-MM-dd |
| confirmed | int | 累计确诊数 |
| cured | int | 累计治愈数 |
| dead | int | 累计死亡数 |
注意这里的 province_name 不要直接写“北京市”或“广西壮族自治区”,因为 ECharts 中国地图里的标准名称是“北京”“广西”,后面做地图展示时省去一次字符串替换。如果数据源里已经带了后缀,那就需要写归一化映射,这一点在第 3 章会单独讲。
为什么直接用文件而不是数据库?这类系统的单表数据量一般只有几万行,全量加载到内存完全扛得住,没必要为了“像正式项目”而硬上 MySQL。如果毕设题目明确要求用数据库,那常见做法是 Spring Boot + MyBatis + MySQL,SQL 初始化脚本放在项目的 sql 目录下,但要注意:MyBatis 在这里的价值是完成一次性的 insert 导入,而不是承担复杂查询。真正的高频查询是内存聚合,不是 SQL join。
2.2 后端骨架:Spring Boot 分层与统一返回结构
后端骨架不需要复杂,但分层要清楚。Controller 负责接收参数,Service 负责聚合统计,数据访问层退化成 DataRepository,因为数据源是 CSV 而不是数据库表。三层分完之后,前端永远只跟一种结构打交道——统一返回结果。下面这个 Result 类几乎在每个类似的源码包里都能看到:
public class Result<T> { private int code; private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "ok"; r.data = data; return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; r.data = null; return r; } // getter / setter 省略 }对应的实体类 CovidDaily 也直接对应 CSV 每一行:
public class CovidDaily { private LocalDate date; private String province; private int confirmed; private int cured; private int dead; // getter / setter 省略 }Code 字段的约定建议统一:200 表示业务成功,500 表示业务失败。前端拿到响应后先判断 code 再渲染图表,而不是一看到 HTTP 200 就认为是成功。这样做的目的是把异常处理放到同一个拦截器里,避免每个图表页面都写一遍错误分支。参数上,msg 要写人可以看懂的话,比如“start 不能晚于 end”,方便排查问题。
有些源码会把统计逻辑直接写在 Controller 里,图表要换一个聚合维度就得重写接口,这是最典型的反面写法。我在改这种项目时第一步就是把统计逻辑全部下沉到 Service,Controller 里只留参数解析和数据透传。
2.3 ECharts 与后端如何对接:图表配置不该由前端写死
很多人第一次接触这类项目时会有一个误解:后端应该返回整个 ECharts option,前端拿到后直接 setOption。这个做法看似省事,实际把表现层硬编码进了后端。前端改一个图例位置、换一组颜色,都要后端重新发版本。
更常见的做法是后端返回“数据”,前端自己组装 option。后端接口只输出图表真正需要的数组,比如折线图返回时间序列,地图返回姓名和数值对,横向柱状图返回排名列表。前端负责把数组塞进 series.data。
| 接口 | 返回内容 | 消费组件 |
|---|---|---|
| /api/trend | 全国累计确诊/治愈/死亡随时间变化的数据点 | 折线图 |
| /api/map | 各省份现存确诊值列表 | 中国地图 |
| /api/ranking | 省份确诊数 Top N 列表 | 横向柱状图 |
前端用什么框架都行,原生 JS、Vue、React 都能接这套接口。Vue 项目里常见做法是在 mounted 里发请求,拿到数据后调用 chart.setOption。这里唯一要注意的是:图表实例必须等 DOM 挂载完成后再初始化,否则 echarts.init 会拿到 null,页面直接白屏。按这个约定做,后端可以完全不知道前端用了什么框架,这也是企业级数据可视化大屏的基本解耦方式。
3. 把疫情数据清洗成图表能用的结构:聚合统计与日期补全
3.1 确诊、治愈、死亡的时间序列怎么组织
原始 CSV 一般是长表格式:每个省份每天一行。但折线图需要的是宽表格式:横轴是日期,每个指标各成一条线。所以第一步是先做聚合,再把日期排好序。Java 里最合适的容器是 TreeMap,它天然按 key 升序排列,省去手动排序。
public List<Map<String, Object>> buildNationalTrend(List<CovidDaily> all, LocalDate start, LocalDate end) { Map<LocalDate, long[]> dayMap = new TreeMap<>(); for (CovidDaily d : all) { long[] values = dayMap.computeIfAbsent(d.getDate(), k -> new long[3]); values[0] += d.getConfirmed(); values[1] += d.getCured(); values[2] += d.getDead(); } List<Map<String, Object>> result = new ArrayList<>(); for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) { long[] values = dayMap.getOrDefault(date, new long[3]); Map<String, Object> item = new LinkedHashMap<>(); item.put("date", date.toString()); item.put("confirmed", values[0]); item.put("cured", values[1]); item.put("dead", values[2]); result.add(item); } return result; }这段代码的逻辑有两层:第一层用 computeIfAbsent 把同一日期的所有省份数据累加,得到全国总量;第二层从 start 遍历到 end,逐天生成数据点。这里用 LinkedHashMap 是为了保证 JSON 序列化后的字段顺序是 date、confirmed、cured、dead,前端取数时报错率会低很多。
参数说明:start 和 end 分别控制折线图的横轴范围,来自前端请求参数。如果不做这个限制,数据源从 2020 年 1 月到 2023 年的全部日期都会画出来,x 轴会挤成一片。getOrDefault 的作用是防止 CSV 里某一天完全没有任何省份记录时出现空指针,但也要注意:如果是累计值,缺日直接补 0 会让曲线突然掉到原点,这是灾难级的视觉错误。更稳妥的做法是拿前一天的值填充,后面避坑环节会展开。
3.2 省级地图数据:把“省份名”对齐到 ECharts 地图的坑
中国地图是疫情可视化里最吸睛的部分,也是坑最多的部分。坑不在图表配置,在于数据里的省份名和地图 GeoJSON 里的名字对不上。比如数据源写“内蒙古自治区”,ECharts 地图里叫“内蒙古”;数据源写“澳门特别行政区”,地图里叫“澳门”。如果对不上,地图上的该省份会显示为空白,并且控制台没有明显报错,很多人盯着地图看半天不知道问题出在数据上。
常见的做法是在后端做一次省份名归一化,写成静态映射:
private static final Map<String, String> PROVINCE_ALIAS = new HashMap<>(); static { PROVINCE_ALIAS.put("北京市", "北京"); PROVINCE_ALIAS.put("天津市", "天津"); PROVINCE_ALIAS.put("上海市", "上海"); PROVINCE_ALIAS.put("重庆市", "重庆"); PROVINCE_ALIAS.put("内蒙古自治区", "内蒙古"); PROVINCE_ALIAS.put("广西壮族自治区", "广西"); PROVINCE_ALIAS.put("西藏自治区", "西藏"); PROVINCE_ALIAS.put("宁夏回族自治区", "宁夏"); PROVINCE_ALIAS.put("新疆维吾尔自治区", "新疆"); PROVINCE_ALIAS.put("香港特别行政区", "香港"); PROVINCE_ALIAS.put("澳门特别行政区", "澳门"); } public static String normalizeProvince(String raw) { return PROVINCE_ALIAS.getOrDefault(raw, raw); }这个映射表放后端而不是前端,原因是后端统一处理一份,前端所有图表都能受益。如果前端每个图表各写一套映射,后面加一个“湖北”的别名都要改多个文件。
另外要注意 ECharts 中国地图中有几个特殊区域:北京、天津、上海、重庆四个直辖市用的是短名,其余省份一般也是短名,所以“XX 省”这种写法基本都要剥掉。地图注册时 registerMap 的 GeoJSON 里 name 字段决定了匹配规则,数据里多一个空格都匹配不上。
3.3 必调参数:时间粒度、排序阈值与 null 处理
数据清洗阶段有几个参数值得单独调出来讲。第一是时间粒度:按日聚合还是按周聚合。按日聚合在疫情数据上很直观,但波动大;按周聚合更平滑,适合看趋势。按周聚合不能直接用 Calendar 的默认周规则,否则跨年时周数混乱。简单做法是判断日期是否为周一,每周的聚合值落在该周周一的日期上。
第二是排序阈值。排名图通常取 Top 10 或 Top 15,但排序时相同的数值会被折叠。比如两个省份都是 1000 例,排序后先后顺序每次刷新可能不同,图表看起来就会“跳”。解决方式是在数值相同时按省份名的拼音顺序做二次排序,保证稳定。
第三是 null 处理。CSV 文件里经常有空单元格,用 BufferedReader 读到后如果直接 Integer.parseInt,会抛出 NumberFormatException。通常的做法是解析时把空字符串当成 0,但这只适用于每日新增值;对于累计值,出现空缺时应当沿用前一天的累计数,否则数值会从 10000 突然变成 0。更稳妥的写法是解析完成后跑一遍 fillHoles 方法,按日期顺序把缺失的累计值补成前一条记录的值。这个细节很多人不做,结果折线图中间莫名出现一个深坑,看起来像是疫情数据回升,其实是数据缺口造成的假象。
4. 后端 API 设计:给图表提供刚好的 JSON,别让前端二次加工
4.1 /api/trend、/api/map、/api/ranking 三类接口的返回结构
后端接口设计的核心原则是一个图表对应一个接口,接口返回的数据结构要能被 ECharts 直接消费。前端不应该做求和、过滤、排序这类事情,因为 Java 后端做这些操作既快又容易测试。
/api/trend返回折线图数据,结构是数组,每个元素包含日期和三个指标:
{ "code": 200, "msg": "ok", "data": [ { "date": "2020-02-01", "confirmed": 14380, "cured": 328, "dead": 304 }, { "date": "2020-02-02", "confirmed": 17205, "cured": 475, "dead": 361 } ] }/api/map返回中国地图数据,结构是 name/value 数组:
{ "code": 200, "msg": "ok", "data": [ { "name": "湖北", "value": 36267 }, { "name": "广东", "value": 1122 } ] }/api/ranking返回横向柱状图数据,只看 Top N:
{ "code": 200, "msg": "ok", "data": [ { "name": "湖北", "value": 36267 }, { "name": "广东", "value": 1122 } ] }三个接口的共同点:data 永远是一个数组,数组元素是对象,而不是嵌套很深的结构。这样前端写起来就是一行 map,不会出现在页面上堆一大堆 reduce 逻辑的情况。需要注意 map 接口的 data 里必须包含全部省份,即使 value 是 0 也要输出,否则 ECharts 地图上该省份鼠标悬停时没有 tooltip。
4.2 Spring Boot Controller 代码与参数校验
Controller 层只做三件事:解析参数、调用 Service、包装 Result。参数校验必须在进入 Service 之前完成,否则聚合逻辑跑到一半才发现日期范围非法,浪费不必要的计算。
@RestController @RequestMapping("/api") public class CovidController { private final CovidService covidService; public CovidController(CovidService covidService) { this.covidService = covidService; } @GetMapping("/trend") public Result<List<Map<String, Object>>> trend( @RequestParam(defaultValue = "2020-01-01") String start, @RequestParam(defaultValue = "2020-12-31") String end) { LocalDate startDate = LocalDate.parse(start); LocalDate endDate = LocalDate.parse(end); if (startDate.isAfter(endDate)) { return Result.error("start 不能晚于 end"); } return Result.ok(covidService.buildNationalTrend(startDate, endDate)); } @GetMapping("/map") public Result<List<ProvinceValue>> map() { return Result.ok(covidService.buildProvinceMap()); } @GetMapping("/ranking") public Result<List<ProvinceValue>> ranking( @RequestParam(defaultValue = "10") int top) { if (top < 1 || top > 50) { return Result.error("top 必须在 1-50 之间"); } return Result.ok(covidService.buildProvinceRanking(top)); } }LocalDate.parse 默认解析 ISO 格式,即 yyyy-MM-dd,所以前端传“2020-02-01”这种字符串时不需要额外的日期格式化器,这是 Spring Boot 默认就支持的。如果传进来的是时间戳,那就要在全局配置里加 JsonFormat 注解,但疫情大屏项目一般都用字符串日期,简单直接。
top 参数限制在 1 到 50 之间,是为了防止有人传一个 1000 把前端横向柱状图的柱子画成一条线。参数校验失败时仍然返回 HTTP 200,只是 code 变成 500,原因是前端请求拦截器统一处理业务错误比处理 HTTP 状态码更省事。如果项目走严格 RESTful 风格,也可以返回 400,但给图表项目用会增加前端判断分支,收益不大。
4.3 缓存策略:疫情数据全量加载一次,接口只做聚合
疫情数据属于典型的“低频更新、高频读取”,每天的数据量有限,但图表接口可能被反复请求。常见的做法是启动时把 CSV 全部加载到内存,之后接口每次都基于内存数据聚合,而不是重新读文件。这里给出一个用 @PostConstruct 实现的加载器:
@Component public class CovidDataHolder { private final List<CovidDaily> dailyList = new CopyOnWriteArrayList<>(); @PostConstruct public void init() throws IOException { try (InputStream in = getClass().getResourceAsStream("/data/covid_data.csv"); BufferedReader reader = new BufferedReader( new InputStreamReader(in, StandardCharsets.UTF_8))) { String line; boolean first = true; while ((line = reader.readLine()) != null) { if (first) { first = false; continue; } String[] parts = line.split(","); CovidDaily d = new CovidDaily(); d.setDate(LocalDate.parse(parts[0])); d.setProvince(normalizeProvince(parts[1])); d.setConfirmed(Integer.parseInt(parts[2].trim())); d.setCured(Integer.parseInt(parts[3].trim())); d.setDead(Integer.parseInt(parts[4].trim())); dailyList.add(d); } } } public List<CovidDaily> getAll() { return dailyList; } }这段代码里最容易踩的坑有三个。第一是字符集,CSV 必须用 UTF-8 读取,Windows 下另存的 GBK 文件会让中文省份名变成乱码,地图上所有省份都匹配不上。第二是表头,while 循环里 first 变量用来跳过第一行,如果没有表头就直接去掉这个判断。第三是空字符串,Integer.parseInt("") 会直接抛异常,稳妥做法是先 trim 再判断长度,长度为 0 时补 0。
当并发量稍微上来一点,每次请求都全量遍历几万行数据也能撑住,但如果想更进一步,可以用 Caffeine 对聚合结果做缓存,key 是 start_end_top,value 是聚合后的 List。这样同一个时间范围的请求第二次直接命中缓存,响应时间从毫秒级降到微秒级。要注意的是 Caffeine 的 key 必须覆盖所有影响结果的参数,否则会出现区间 A 的结果被返回给区间 B 的请求,属于典型的缓存污染。
5. ECharts 端开发排查:大屏布局与五个常见问题
5.1 折线图、柱状图、中国地图的 option 组织
后端接口准备好后,前端的工作就是把数据映射成 option。折线图最常见的是全国累计趋势,前端代码大致如下:
fetch('/api/trend') .then(res => res.json()) .then(body => { if (body.code !== 200) return; const data = body.data; const option = { tooltip: { trigger: 'axis' }, legend: { data: ['累计确诊', '治愈', '死亡'] }, xAxis: { type: 'category', data: data.map(d => d.date) }, yAxis: { type: 'value', name: '人数' }, series: [ { name: '累计确诊', type: 'line', smooth: true, data: data.map(d => d.confirmed), areaStyle: { opacity: 0.18 } }, { name: '治愈', type: 'line', smooth: true, data: data.map(d => d.cured) }, { name: '死亡', type: 'line', smooth: true, data: data.map(d => d.dead) } ] }; trendChart.setOption(option); });areaStyle 的 opacity 设成 0.18 而不是默认值 1,目的是让面积填充色透出网格线,视觉上更清爽。如果做成渐变色,可以用 echarts.graphic.LinearGradient,这是 ECharts 5 里柱状图设置渐变色的标准方式,适用于柱状图想突出“由暗到亮”的场景。
中国地图的 option 跟折线图完全是两套思路,需要先用 registerMap 注册 GeoJSON,再配置 visualMap 分段着色:
fetch('./china.json') .then(res => res.json()) .then(geoJson => { echarts.registerMap('china', geoJson); }); fetch('/api/map') .then(res => res.json()) .then(body => { const option = { tooltip: { formatter: p => p.name + '<br/>现存确诊:' + p.value }, visualMap: { min: 0, max: 6000, inRange: { color: ['#e0f3f8', '#fee08b', '#d73027'] } }, series: [{ type: 'map', map: 'china', data: body.data }] }; mapChart.setOption(option); });横向柱状图在疫情分析里常用来做省份排名,也叫横向进度条效果,配置核心是 yAxis 用 category 类型、xAxis 用 value 类型。这个图很适合做 Top 10 排名,因为省份名称在左边纵向排列,长度一目了然,不会像普通柱状图那样标签重叠。
5.2 地图注册与异步加载:ECharts 4 与 5 的差异
地图这一块最容易翻车的是版本差异。ECharts 4 时代,可以直接通过引入 china.js 文件来使用中国地图,很多老教程都这么写。但 ECharts 5 把地图 GeoJSON 从核心包里移除了,必须自己准备一份 china.json 并调用 registerMap 注册。
这段注册逻辑还必须保证在 setOption 之前完成,否则 series 里写 map: 'china' 时图表实例找不到地图数据,页面显示空白。常见做法是把 fetch china.json 和 fetch 业务数据放在同一个 async 流程里,用 Promise.all 并行请求,等两者都返回后再初始化图表。串行请求也可以,但会明显拖慢首屏。如果直接把 china.json 放在 static 目录下,最简单的写法就是用 fetch 相对路径加载,避免 webpack 打包时的路径问题。
5.3 五个必踩的图表坑:渐变、刻度、tooltip、空数据与刷新
下面这五条是我在任何 ECharts 项目里都会提前检查一遍的问题。每条按现象、原因、解决描述。
坑一:折线图 x 轴日期挤成一团。现象是横轴刻度标签叠在一起,完全看不清。原因是日期点太多,ECharts 默认不会自动抽样,几十个日期全部渲染当然会重叠。解决方式是给 xAxis.axisLabel 设置 interval: 'auto',或者配合 dataZoom 做窗口缩放。dataZoom 的典型配置是 inside: true 和 start/end 百分比,比如只看最近 30 天就把 start 设为 80,end 设为 100。
坑二:柱状图上柱子的数值标签不显示。现象是柱子高度正常,但柱子上方没有数字。原因不是图表坏了,而是 series.label 没有开启。解决方式是写 label: { show: true, position: 'top' }。这里要区分两个概念:yAxis.axisLabel 控制坐标轴刻度的文字,series.label 控制柱子上方的数值标签。很多人把两者混在一起配置,结果调了半天 yAxis 没反应。
坑三:地图上 value 为 0 的省份没有 tooltip。现象是鼠标划过新疆、西藏这类低风险地区时弹不出提示框。原因是后端接口只返回了有数据的省份,ECharts 找不到这个区域的 data 项,自然不渲染 tooltip。解决方式是后端 map 接口返回全部省级行政区,value 为 0 也占位。
坑四:第二次 setOption 后图表数据不更新。现象是第一次请求画出了曲线,第二次请求换了时间段,图表没反应或还残留旧曲线。原因是 setOption 默认是合并模式,旧 series 数据不会被清除。解决方式是 setOption(option, true),第二个参数 notMerge 设为 true,强制整体替换。
坑五:页面白屏,控制台报 JSON 解析错误。现象是 fetch 能拿到响应但 res.json() 失败。原因是后端返回的不是纯 JSON,可能是错误页 HTML,也可能是接口路径不对返回了 404 页面。解决方式是先把响应状态打出来,再检查 /api 接口的完整路径和端口。另外 CSV 里中文乱码也会引发类似问题,重点排查后端读取文件的字符集。
5.4 时间轴组件:用 timeline 实现疫情的逐月演变
疫情大屏通常不止一张静态图,还希望用时间轴看 1 月到 6 月的动态变化。ECharts 的 timeline 组件非常适合这个需求,它只需要一个 baseOption 加一个 options 数组:
const months = ['2020-01', '2020-02', '2020-03', '2020-04', '2020-05', '2020-06']; const option = { timeline: { data: months, autoPlay: true, playInterval: 1500 }, baseOption: { xAxis: { type: 'category', data: provinceNames }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: [] }] }, options: months.map(month => ({ series: [{ data: monthData[month] }] })) };baseOption 里放所有时间点都不变的配置,比如 x 轴省份列表、y 轴名称、图例。options 数组里每个元素只写当前时间点有差异的部分,比如柱子数据。playInterval 是自动播放间隔,单位毫秒,1500 到 2000 之间比较合适,太快看不清省份名,太慢让人觉得卡顿。
这里有个常见问题:timeline 的 data 数组长度必须和 options 数组长度一致,否则最后一个时间点会没有数据。如果数据只有 5 个月而 timeline 写了 6 个点,第 6 个点会显示空白或者是上一帧的残留。做动态地图时,每个时间点都要生成一份完整的地图数据,不要把两个月的数组混在一起。
6. 让图表动起来的最小验证路径:从解压到出图的四步
6.1 先看目录再跑项目:pom 和 static 是关键
解压源码后不要急着运行,先花两分钟确认三件事:根目录有没有 pom.xml,这决定它是 Maven 项目还是 Eclipse 普通工程;src/main/resources 下有没有 csv、json、sql 这类数据文件;src/main/resources/static 下有没有 index.html 或 dashboard.html。疫情分析前端页面通常放在 static 目录里,由 Spring Boot 直接托管,这样不存在跨域问题。没有 static 目录的话,前端就得单独起一个服务,还要在后端配置 CORS 过滤器。
6.2 最小运行命令与验证点
在项目根目录执行:
mvn spring-boot:run第一次运行会下载依赖,耗时取决于网络。启动日志里看到 Tomcat started on port(s): 8080 之后,打开浏览器访问 http://localhost:8080/index.html。如果页面能看到折线图、柱状图、中国地图同时出现,说明从数据加载到接口输出全链路没问题。如果只有部分图表有数据,优先打开浏览器开发者工具,看 Network 面板里哪个 /api 请求返回了非 200 的 code。
6.3 进阶技巧:预警阈值与模拟刷新
查数据方向想做得更出彩,可以加一个预警阈值:当日新增确诊超过某个值就把柱状图画成红色。后端返回数据时附带新增量字段,前端在 series.data 里用对象形式给每个柱子单独上色:
series: [{ type: 'bar', data: ranking.map(v => ({ name: v.name, value: v.value, itemStyle: { color: v.dailyIncrease > 1000 ? '#d73027' : '#1890ff' } })) }]刷新方面不一定上 WebSocket,轮询就够用。前端 setInterval 每 30 秒重新请求一次接口,后端在内存里拿到最新数据后直接返回,图表整体替换。注意 setOption 时带上 notMerge 参数,否则旧数据会和新数据叠加,曲线越画越花。
我最早做这个项目时把聚合逻辑写在 Controller 里,图表每换一种粒度就要重写接口,后来老老实实拆成 Service。第二个教训是 setOption 不带 notMerge,前端页面看起来像“图表有记忆”,刷新一次就多一条残影,排查了很久才发现是合并模式的问题。这套方案作为毕设和面试项目完全够用,真正投入生产前你还需要补上权限控制、SQL 防注入和监控告警。希望帮到你。
本文还有配套的精品资源,点击获取