资讯动态

Spring Boot + ECharts 新闻可视化平台:从数据库到图表的完整实现

发布时间:2026/9/14 8:02:41 来源:尧图企业网站定制
简介一套基于Spring Boot和ECharts的新闻可视化分析平台源码同时附带数据库脚本与CSV样例数据专为需要完成新闻数据分析类课程设计或毕业设计的开发者准备。整个项目打通了Spring Boot后端服务、Spring Data JPA数据访问、ECharts前端图表展示三个环节可帮助读者快速理解Java Web项目中后端接口与前端可视化组件的协作方式。压缩包约1.6MB共57个文件收录17个Java源码、6个JavaScript脚本、4个XML配置、1个SQL脚本以及HTML页面、CSS样式、字体、图片和说明文档等目录划分清楚适合对照学习或二次改造。目前已有388人学习关注通过实际编译运行可掌握Spring Boot自动配置、Starter依赖、Repository接口以及ECharts折线图/柱状图/饼图的交互配置等核心技能是一份实践性较强的完整项目参考。1. 新闻可视化平台的真问题Spring Boot 负责统计口径ECharts 只认数组“基于 Spring Boot ECharts 的新闻可视化分析平台”听起来是采集新闻、展示图表但真正动手做一遍会发现核心工作量根本不在收集新闻而在把新闻数据整理成前端能直接消费的统计口径。Spring Boot 在这里不是业务系统它只做一件事接收查询条件返回按时间、来源、分类聚合好的 JSONECharts 也不关心数据来自哪里它只认 x 轴数组、series 数组和键值对对象。这三层各管各的只要数据链路是通顺的平台就成立。这套技术栈也是课程设计和 java 毕设里最高频的组合之一数据库管存储、Spring Boot 管接口、ECharts 管展示每一层都能单独验证。下文按「数据模型 → 聚合接口 → 图表渲染 → 造数与验证」的顺序把这套方案完整拆开代码可以直接抄。2. 数据库设计新闻表字段怎么定聚合 SQL 才好写2.1 为什么用宽表而不是教科书式的多表范式新闻业务按教科书建模往往要拆出新闻表、分类表、来源表、作者表再通过外键关联。但可视化分析平台最高频的操作是“按天统计点击量”“按来源算占比”“按分类看分布”这些都要把多张表 join 在一起做 GROUP BYSQL 写得越长索引越难命中排查问题也越麻烦。常见做法是把分类名、来源名直接冗余成字符串字段只保留一张新闻主表和一张可选的分类字典表。这样做的好处有两个一是插入数据时不需要维护外键造数脚本和爬虫写入都简单二是聚合查询全部落在单表上执行计划清晰可控。代价是冗余字段偶尔会出现写不一致但分析型平台的数据是只读的更新场景极少这个代价可以接受。2.2 news_article 建表 SQL 与关键索引说明下面是一张可以直接运行的新闻主表字段覆盖标题、摘要、来源、分类、地域、发布时间和互动指标CREATE TABLE news_article ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL COMMENT 新闻标题, summary VARCHAR(1000) DEFAULT COMMENT 新闻摘要, source_name VARCHAR(64) NOT NULL COMMENT 来源媒体名称, category_name VARCHAR(32) NOT NULL COMMENT 分类科技/财经/体育等, province VARCHAR(32) DEFAULT NULL COMMENT 涉及地域用于地图展示, publish_time DATETIME NOT NULL COMMENT 精确发布时间, publish_date DATE NOT NULL COMMENT 按天聚合冗余字段, clicks INT DEFAULT 0 COMMENT 点击量, likes INT DEFAULT 0 COMMENT 点赞量, KEY idx_date_category (publish_date, category_name), KEY idx_source_date (source_name, publish_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT新闻文章表;关键点全在最后两个索引和 publish_date 字段上。按天聚合时如果使用DATE(publish_time)MySQL 会因为函数包裹列而放弃索引必须全表扫冗余一个 publish_date 后直接GROUP BY publish_dateidx_date_category 能完整命中筛选加排序。需要按来源过滤再按时间排序时走的是 idx_source_date。对应数据字典如下字段名类型说明idBIGINT主键自增titleVARCHAR(255)标题列表页直接展示summaryVARCHAR(1000)摘要词云和详情页复用source_nameVARCHAR(64)来源媒体可加索引做筛选项category_nameVARCHAR(32)新闻分类饼图按它分组provinceVARCHAR(32)仅当需要地图时使用可留空publish_timeDATETIME精确时间详情页展示用publish_dateDATE冗余日期趋势图按它聚合clicks / likesINT互动指标折线图的值来源2.3 数据量上去之后再加一张聚合统计表当单表超过百万行、前端又要求秒开时每次实时 GROUP BY 就不划算了。我会在项目里再加一张预聚合表让定时任务每天凌晨算好前一天的结果CREATE TABLE news_daily_stats ( stat_date DATE NOT NULL, category_name VARCHAR(32) NOT NULL, source_name VARCHAR(64) NOT NULL, total_clicks BIGINT DEFAULT 0, article_count INT DEFAULT 0, PRIMARY KEY (stat_date, category_name, source_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;写入由 Spring Boot 的 Scheduled 定时任务触发核心逻辑是把 news_article 按日期、分类、来源分组汇总后插入这张表INSERT INTO news_daily_stats (stat_date, category_name, source_name, total_clicks, article_count) SELECT publish_date, category_name, source_name, SUM(clicks), COUNT(*) FROM news_article WHERE publish_date #{date} GROUP BY publish_date, category_name, source_name;这个设计把查询从几百万行的实时聚合变成了对一张窄表的等值查询主键恰好覆盖筛选维度。做数据库课程设计时可以把这个结构演化写进设计文档作为“数据预处理层”的亮点。要注意这张表需要定时清理过期数据统计结果超过一年基本没有查看价值。2.4 时区与字符集两个隐藏比较深的坑publish_time 建议去掉ON UPDATE CURRENT_TIMESTAMP新闻的发布时间是业务属性不应该随记录更新而漂移。字符集必须用 utf8mb4新闻摘要里可能出现 emoji 或其他扩展字符utf8mb3 会直接插入失败。还有 springboot 配置里的时区项。MySQL 连接串上不写时区默认跟随服务器时区而 Spring Boot 所在的容器时区可能不一致导致 ECharts 趋势图出现“数据偏移一天”的诡异现象。常见做法是在 jdbc 连接串上显式声明jdbc:mysql://localhost:3306/news_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai加上serverTimezoneAsia/Shanghai后前后端的时间口径才算真正统一。这个问题在数据量小的时候根本暴露不出来等趋势图日期对不上再排查会浪费大量时间。3. Spring Boot 聚合接口把数据库行转成 ECharts 需要的“轴值”3.1 接口返回体与 VO 设计别把 List新闻 直接抛给前端见过不少平台直接把新闻列表整个返回让前端自己去算总数和占比。数据量小看不出问题一旦数据量上来前端要做大量循环累计工作页面卡顿不说统计口径还容易和 SQL 算出来的对不上。正确做法是聚合计算全部下沉到 SQLSpring Boot 只把计算结果包装成固定结构的 JSON 返回。统一响应体我一般写成泛型结构前端通过 code 判断请求是否成功data 直接绑定图表Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } }图表查询的 VO 也只要薄薄几个字段不需要把整条新闻记录暴露出去Data public class TrendVO { private String date; // 对应 ECharts x 轴 private Long value; // 对应 ECharts series 数值 } Data public class NameValueVO { private String name; // 饼图/柱状图的类别名 private Long value; // 对应的数值 }核心原则是“接口结果集的名字必须跟图表字段名一致”。如果后端返回{ date: 2024-06-01, value: 123 }前端用item.date就能拿到轴数据不需要再做映射。命名不统一前端每张图就要多一层转换逻辑后端把 VO 定好整条链路都会干净很多。3.2 按天趋势、来源占比、分类占比三条聚合 SQL 的 XML 实现三个核心查询分别是按日期统计点击总量趋势折线图、按来源统计文章数来源占比饼图、按分类统计点击量或文章数分类柱状图。Mapper XML 写法如下mapper namespacecom.example.news.mapper.NewsMapper select idselectTrend resultTypecom.example.news.vo.TrendVO SELECT publish_date AS date, SUM(clicks) AS value FROM news_article WHERE publish_date BETWEEN #{startDate} AND #{endDate} if testsource ! null and source ! AND source_name #{source} /if GROUP BY publish_date ORDER BY publish_date /select select idcountBySource resultTypecom.example.news.vo.NameValueVO SELECT source_name AS name, COUNT(*) AS value FROM news_article WHERE publish_date BETWEEN #{startDate} AND #{endDate} GROUP BY source_name ORDER BY value DESC /select select idcountByCategory resultTypecom.example.news.vo.NameValueVO SELECT category_name AS name, SUM(clicks) AS value FROM news_article WHERE publish_date BETWEEN #{startDate} AND #{endDate} GROUP BY category_name ORDER BY value DESC /select /mapper三条 SQL 都是先按时间范围过滤再 GROUP BY 维度字段。注意 countBySource 用的是 COUNT(*) 统计文章条数countByCategory 用的是 SUM(clicks) 统计点击总量两种指标的含义不要混。过滤条件里的#{startDate}和#{endDate}直接用字符串MySQL 对2024-01-01这样的字符串比较 DATE 类型时会隐式转换但传入值必须是yyyy-MM-dd格式否则算不准。3.3 DashboardController 代码与查询参数表Controller 只负责接收参数、做基础校验、调用 Mapper、包装 Result。完整逻辑如下RestController RequestMapping(/api/dashboard) public class DashboardController { private final NewsMapper newsMapper; public DashboardController(NewsMapper newsMapper) { this.newsMapper newsMapper; } GetMapping(/trend) public ResultListTrendVO trend(RequestParam String startDate, RequestParam(required false) String endDate, RequestParam(required false) String source) { if (endDate null || endDate.isEmpty()) { endDate LocalDate.now().toString(); } return Result.ok(newsMapper.selectTrend(startDate, endDate, source)); } GetMapping(/source) public ResultListNameValueVO source(RequestParam String startDate, RequestParam(required false) String endDate) { if (endDate null || endDate.isEmpty()) { endDate LocalDate.now().toString(); } return Result.ok(newsMapper.countBySource(startDate, endDate)); } GetMapping(/category) public ResultListNameValueVO category(RequestParam String startDate, RequestParam(required false) String endDate) { if (endDate null || endDate.isEmpty()) { endDate LocalDate.now().toString(); } return Result.ok(newsMapper.countByCategory(startDate, endDate)); } }接口参数规则如下表参数名类型必填说明startDateString是开始日期格式 yyyy-MM-ddendDateString否结束日期默认当天sourceString否来源筛选传空字符串时不生效这里把 endDate 的默认值放在 Controller 里做兜底比放在 SQL 里更清晰。前端传参时把 source 留空字符串而不是 null否则 MyBatis 的if testsource ! null and source ! 会判断为真导致 SQL 拼出AND source_name null查询结果为空。这是 MyBatis 动态 SQL 最容易踩的一个坑。3.4 为什么选 XML Mapper 而不是 MyBatis-Plus 的 QueryWrapperMyBatis-Plus 做单表 CRUD 确实方便增删改查基本不用写 SQL但聚合查询是另一回事。虽然 QueryWrapper 也支持select(publish_date, SUM(clicks))可一旦 SQL 变复杂、需要if条件动态拼装或者调整索引代码里写字符串 SQL 的可读性会迅速下降。XML Mapper 的优势在于 SQL 独立维护修改后不用重新编译 Java 代码DBA 拿到 XML 文件就能直接调优数据库增删改查。在一个基于 Spring Boot 的 java 毕设项目里XML Mapper 的存在也能体现分层意识Controller 负责参数处理Mapper XML 负责数据访问VO 负责接口契约。面试官问起时会更容易解释清楚“聚合逻辑放在数据库还是应用层”的问题。当然简单的字典表查询仍然可以用 MP 的 Service两者并不冲突。4. ECharts 前端装配从 fetch 数据到三类图表渲染4.1 项目结构选择纯 HTML fetch 或 Vue 二选一如果平台只服务展示场景我会倾向于纯静态 HTML 页面通过 fetch 请求 Spring Boot 接口这样后端项目可以独立部署前端也不需要 Node 构建链路。代码维护成本低课程设计和演示都不容易出环境问题。如果后续要做复杂交互或组件复用再迁移到 Vue 也不迟。页面引入方式遵循 springboot 项目里最常规的做法后端把静态页放在src/main/resources/static下启动后直接访问同源路径天然规避跨域问题。ECharts 通过 CDN 引入即可script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script4.2 fetch 后组装 xAxis 与 series 的推荐写法后端返回的 trend 数据是若干{ date, value }对象ECharts 的 x 轴和 series 需要两个独立数组。直接 map 是最稳妥的写法const resp await fetch(/api/dashboard/trend?startDate2024-01-01endDate2024-12-31); const result await resp.json(); if (result.code ! 200) throw new Error(接口异常); const dates result.data.map(item item.date); const values result.data.map(item item.value); const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ xAxis: { type: category, data: dates }, yAxis: { type: value }, series: [{ type: line, data: values, smooth: true }] });这里的关键约束是 dates 数组和 values 数组必须一一对应。ECharts 在做折线图时如果两个数组长度不一致图表会缺点或错位后端 SQL 里显式ORDER BY publish_date才能保证顺序一致。使用 map 而不是 for 循环代码意图也更明确。4.3 折线图、饼图、柱状图的核心 option 配置饼图比折线图简单的多后端返回的就是{ name, value }结构正好可以直接塞给 dataconst resp await fetch(/api/dashboard/source?startDate2024-01-01endDate2024-12-31); const result await resp.json(); const chart echarts.init(document.getElementById(sourceChart)); chart.setOption({ series: [{ type: pie, radius: [40%, 70%], data: result.data.map(item ({ name: item.name, value: item.value })) }] });radius: [40%, 70%]可以生成环形饼图视觉上比实心饼图更轻盈。柱状图的配置和折线图相近只是把type改成bar再加上一个barMaxWidth限制柱宽否则分类名很长时柱子会特别宽影响可读性。4.4 dataZoom、tooltip 与窗口 resize 的必配项趋势图跨三个月时六十多个点堆在同一个画布里标签必然重叠。dateZoom 组件可以让用户自助缩放chart.setOption({ tooltip: { trigger: axis }, dataZoom: [ { type: inside, start: 0, end: 100 }, { type: slider, start: 0, end: 100, height: 20 } ] });type 为 inside 时鼠标滚轮缩放slider 则显示底部的拖动条两者配置成相同的 start 和 end 值联动的缩放范围才一致。tooltip 的 trigger 用 axis 而不是 item划过某一天时能看到完整上下文。页面容器尺寸变化后图表不会自动重绘需要在页面加载时注册 resize 监听window.addEventListener(resize, () { chart.resize(); });每一次 setOption 之前也建议先调用一次chart.clear()否则多次请求后 ECharts 会把旧的 series 和新 series 合并出现两条数据线叠加的怪象。4.5 进阶注册地图 geoJSON 并给地图加背景图如果新闻数据带有省份字段平台可以增加一个“热度地域分布”模块把饼图升级成中国地图。ECharts 5 本身不内置地图数据需要把 geoJSON 文件放在 static 目录下前端动态注册fetch(/geo/china.json) .then(resp resp.json()) .then(chinaJson { echarts.registerMap(china, chinaJson); chart.setOption({ backgroundColor: url(./img/bg.jpg) no-repeat center, // geo 组件或 series 的 map 属性指向 china }); });地图加背景图的做法实际是在 option 的backgroundColor里写 url 地址地图 geo 区域浮在背景图层之上。样式上建议背景图整体偏暗地图区域用高亮色填充对比度更高。这是“echart 地图加背景图”的典型落地方式新闻平台里配合省份维度做热点分析非常直观。5. 造数脚本、链路验证与一个平台化技巧5.1 用 Python 脚本灌入可复现的模拟新闻数据没有真实新闻源时我会先用 Python 生成模拟数据把 90 天的时间跨度、来源分布、分类分布都做出来方便趋势图和占比图有足够的数据量展示import random from datetime import datetime, timedelta from faker import Faker import pymysql fake Faker(zh_CN) conn pymysql.connect( hostlocalhost, userroot, password123456, databasenews_platform, charsetutf8mb4 ) cur conn.cursor() categories [科技, 财经, 体育, 娱乐] sources [新浪新闻, 腾讯新闻, 澎湃新闻, 36氪] for _ in range(2000): pub datetime.now() - timedelta(daysrandom.randint(0, 89), hoursrandom.randint(0, 23)) cur.execute( INSERT INTO news_article(title, summary, source_name, category_name, publish_time, publish_date, clicks, likes) VALUES(%s, %s, %s, %s, %s, %s, %s, %s), (fake.sentence()[:50], fake.text()[:200], random.choice(sources), random.choice(categories), pub, pub.date(), random.randint(0, 5000), random.randint(0, 500)) ) conn.commit() cur.close() conn.close()脚本用 random 控制 publish_date 分布在最近 90 天clicks 在一定范围内浮动这样前端趋势图能看到自然的起伏而非一条直线。category 和 source 也做了随机分配饼图和柱状图的比例会比较均匀。5.2 三步验证 Spring Boot 到 ECharts 的全链路数据灌完后浏览器 Network 面板就能当作调试工具三步验证链路是否通顺第一步访问/api/dashboard/trend确认 JSON 结构里有code200data 数组里存在多个日期的统计值第二步打开页面源码确认初始化 ECharts 时document.getElementById获取的容器不是 null容器有明确高度第三步点击来源筛选下拉框重新请求 trend 接口看 Network 里请求 URL 是否带上了sourcexxx参数以及返回值是否随之变化。5.3 平台化技巧来源下拉框联动 折线框选右击事件给平台加一个来源筛选下拉框是所有图表联动的常见做法。监听 change 事件后重新 fetch trend 和 category 接口再 setOption 更新图表就能完成同一个维度驱动多图刷新。进阶一点还可以在折线图上启用框选工具让用户拉取一个时间区间看局部数据chart.setOption({ brush: { toolbox: [rect] }, toolbox: { feature: { brush: { type: [rect] } } } }); chart.on(brushselected, (params) { const indexList params.batch[0].selected[0].dataIndex; console.log(框选到的数据点:, indexList); });再配合contextmenu事件拦截默认右键菜单就能在框选区弹出“只看此区间”“导出图片”等自定义操作这就是用户常说的 echart 框选右击事件。落地时注意先event.preventDefault()阻止浏览器默认菜单再把操作绑定在当前图表的 container 上。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价