大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Spark实时社交媒体舆情分析与趋势预测(Java版本+可视化大屏+Kafka+SpringBoot+Vue3)
项目介绍
随着微博、抖音、知乎、小红书等社交媒体平台的快速发展,网络舆情呈现出数据规模大、传播速度快、情绪演化复杂等特点。传统离线批处理方式难以满足舆情监测对时效性的要求,亟需构建一套能够支撑实时采集、流式计算、情感分析与趋势预测的综合系统。本文围绕“基于Spark实时社交媒体舆情分析与趋势预测”课题,设计并实现了一套前后端分离的舆情分析平台。系统后端采用Java语言与Spring Boot框架构建RESTful服务,结合JWT完成管理员身份认证与权限控制;数据处理层引入Spark思想的流式窗口统计与Kafka消息缓冲机制,对社交媒体帖文进行情感倾向识别、热度指数计算和按小时窗口聚合;趋势预测模块基于历史热度序列构建多元线性回归模型,输出未来窗口的热度预测值,并采用RMSE、MAE、MAPE等指标评价预测效果;前端采用Vue3、Vue Router、Pinia、Element Plus与ECharts实现管理后台与数据可视化大屏,支持帖文管理、话题管理、实时舆情查看、趋势对比和个人中心维护等功能。数据库选用MySQL,库名为db_social_opinion,核心业务表均以t_前缀命名,覆盖管理员、用户、平台、话题、帖文、实时统计、预测结果与误差指标等实体。测试结果表明,系统能够稳定完成舆情事件模拟、实时统计刷新与趋势预测展示,界面日期时间统一采用“2026-11-02 17:25:17”格式,满足本科毕业设计对完整性、规范性与可演示性的要求。本文工作对高校舆情教学实验、中小规模舆情监测系统原型开发具有一定参考价值。
源码下载
链接: https://pan.baidu.com/s/14h6tDPLjs39Rs6U48sXXlg?pwd=1234
提取码: 1234
系统展示
![]()
![]()
![]()
![]()
核心代码
package com.java1234.controller; import com.java1234.common.R; import com.java1234.service.DashboardService; import com.java1234.vo.*; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * 大屏数据控制器 */ @RestController @RequestMapping("/api/dashboard") @RequiredArgsConstructor public class DashboardController { private final DashboardService dashboardService; @GetMapping("/overview") public R<DashboardOverview> overview() { return R.ok(dashboardService.getOverview()); } @GetMapping("/trend") public R<List<TrendItem>> trend() { return R.ok(dashboardService.getTrend()); } @GetMapping("/top-topics") public R<List<TopTopicItem>> topTopics() { return R.ok(dashboardService.getTopTopics()); } @GetMapping("/sentiment") public R<List<SentimentItem>> sentiment() { return R.ok(dashboardService.getSentiment()); } @GetMapping("/platform") public R<List<PlatformItem>> platform() { return R.ok(dashboardService.getPlatform()); } @GetMapping("/recent") public R<List<RecentPostItem>> recent() { return R.ok(dashboardService.getRecentPosts()); } }<template> <div class="page-container"> <div class="page-card"> <div class="page-title">舆情热度预测分析</div> <div class="error-cards"> <div class="error-card"> <div class="metric-label">RMSE (均方根误差)</div> <div class="metric-value">{{ errorMetric.rmse }}</div> </div> <div class="error-card"> <div class="metric-label">MAE (平均绝对误差)</div> <div class="metric-value">{{ errorMetric.mae }}</div> </div> <div class="error-card"> <div class="metric-label">MAPE (平均绝对百分比误差 %)</div> <div class="metric-value">{{ errorMetric.mape }}%</div> </div> </div> <div ref="compareRef" class="pred-chart pred-chart-compare"></div> <div ref="residualRef" class="pred-chart pred-chart-residual"></div> <el-table :data="tableData" stripe border style="width:100%"> <el-table-column prop="window_time" label="时间窗口" min-width="170"> <template #default="{ row }">{{ formatWindowTime(row.window_time) }}</template> </el-table-column> <el-table-column prop="true_heat" label="真实热度" min-width="120"> <template #default="{ row }"><span style="color:#409eff;font-weight:600">{{ row.true_heat }}</span></template> </el-table-column> <el-table-column prop="pred_heat" label="预测热度" min-width="120"> <template #default="{ row }"><span style="color:#67c23a;font-weight:600">{{ row.pred_heat }}</span></template> </el-table-column> <el-table-column label="误差" min-width="100"> <template #default="{ row }"> <span :style="{ color: Math.abs(row.true_heat - row.pred_heat) > 50 ? '#f56c6c' : '#909399' }"> {{ (row.true_heat - row.pred_heat).toFixed(2) }} </span> </template> </el-table-column> <el-table-column prop="create_time" label="生成时间" min-width="170"> <template #default="{ row }">{{ formatDateTime(row.create_time) }}</template> </el-table-column> </el-table> <el-pagination style="margin-top:16px;justify-content:flex-end" v-model:current-page="page" v-model:page-size="size" :total="total" layout="total, prev, pager, next" @change="loadTable" /> </div> </div> </template> <script setup> /** * 预测分析页面:真实 vs 预测对比图 + 误差分析 */ import { ref, onMounted, onUnmounted } from 'vue' import * as echarts from 'echarts' import request from '@/utils/request' import { formatDateTime, formatWindowTime } from '@/utils/format' const errorMetric = ref({ rmse: 0, mae: 0, mape: 0 }) const tableData = ref([]) const page = ref(1) const size = ref(10) const total = ref(0) const compareRef = ref(null) const residualRef = ref(null) let charts = [] function buildAxisLabel() { return { rotate: 30, interval: 'auto', fontSize: 11, margin: 16, formatter(val) { const text = formatWindowTime(val); return text.length >= 16 ? `${text.slice(0,10)}\n${text.slice(11)}` : text }, } } function initCompareChart(data) { const chart = echarts.init(compareRef.value) const labels = data.map(d => formatWindowTime(d.window_time)) chart.setOption({ title: { text: '真实热度 vs 预测热度 对比', left: 'center', textStyle: { fontSize: 15 } }, tooltip: { trigger: 'axis' }, legend: { data: ['真实热度', '预测热度'], top: 32 }, xAxis: { type: 'category', data: labels, axisLabel: buildAxisLabel() }, yAxis: { type: 'value', name: '热度指数' }, series: [ { name: '真实热度', type: 'line', smooth: true, data: data.map(d => Number(d.true_heat)), itemStyle: { color: '#409eff' }, lineStyle: { width: 3 } }, { name: '预测热度', type: 'line', smooth: true, data: data.map(d => Number(d.pred_heat)), itemStyle: { color: '#67c23a' }, lineStyle: { width: 3, type: 'dashed' } }, ], grid: { left: 20, right: 24, bottom: 28, top: 72, containLabel: true }, }) charts.push(chart) } function initResidualChart(data) { const chart = echarts.init(residualRef.value) const labels = data.map(d => formatWindowTime(d.window_time)) chart.setOption({ title: { text: '预测残差分析 (真实值 - 预测值)', left: 'center', textStyle: { fontSize: 15 } }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: labels, axisLabel: buildAxisLabel() }, yAxis: { type: 'value', name: '残差' }, series: [{ type: 'bar', data: data.map(d => ({ value: d.residual, itemStyle: { color: d.residual >= 0 ? '#409eff' : '#f56c6c' } })), barWidth: 20 }], grid: { left: 20, right: 24, bottom: 28, top: 56, containLabel: true }, }) charts.push(chart) } async function loadData() { const [errorRes, compareRes, residualRes] = await Promise.all([ request.get('/prediction/error'), request.get('/prediction/compare'), request.get('/prediction/residual'), ]) errorMetric.value = errorRes.data charts.forEach(c => c.dispose()) charts = [] initCompareChart(compareRes.data) initResidualChart(residualRes.data) } async function loadTable() { const res = await request.get('/prediction/list', { params: { page: page.value, size: size.value } }) tableData.value = res.data.items total.value = res.data.total } onMounted(() => { loadData(); loadTable() }) onUnmounted(() => charts.forEach(c => c.dispose())) </script> <style scoped> .pred-chart { width: 100%; margin-bottom: 24px; } .pred-chart-compare { height: 480px; } .pred-chart-residual { height: 420px; } </style>