尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
基于SpringBoot+Vue的交通信息发布平台设计与实现
交通信息发布平台这名字听着唬人但说白了就一件事把分散的交通数据汇聚起来经过审核和加工再按不同渠道发布出去。真正动手做的时候才会发现难点根本不在“发布”这个动作本身而是数据从哪来、时效怎么保证、审核怎么不卡流程、大屏展示怎么让人一眼看懂。我这次用 SpringBoot 做后端服务、Vue 做前端管理端和可视化大屏前后端分离把整套平台落地上线了。这篇文章会把从需求拆解、技术选型、核心模块设计到部署联调踩坑的完整过程都整理出来给正在准备做同类信息发布系统的人当个参考。整篇内容主要围绕这么几个问题展开数据接入层怎么抽象才不用每个数据源都改一遍业务代码审核与发布的状态流怎么设计才不容易出乱子WebSocket 实时推送和 Vue 大屏之间的联动怎么做才不卡最后还有 SpringBoot 版本过高、Vue 构建资源映射这类最常见却最烦人的部署问题我也一起写了。适合刚从单体转向前后端分离的同学也适合正在做交通、政务、园区类信息发布系统的人翻一翻里面的一部分方案直接拿来改改就能用。1. 需求拆解智能交通信息发布平台到底要解决什么问题1.1 三个核心问题决定整个系统的边界我接手这个项目时需求文档里写得很宏伟“智能”“全渠道”“实时”之类的词堆了一堆。但我给团队定的第一条规矩是先把用户真正关心的三件事列清楚。第一信息从哪来。路况拥堵、交通事故、施工占道、公交临时调整、气象预警数据来源五花八门。有的数据能拿到标准接口有的只能靠值班人员手工录入还有的是固定格式的文件每天定时同步。如果每一个来源都单独写一套处理逻辑后期维护就是灾难。第二信息怎么被信任。自动接进来的权威数据可以直接发布但人工录入的内容绝对要过审核。交通信息的敏感性不用多说一条“某路段临时封闭”的消息如果发错了直接影响大量车辆绕行。所以审核环节不是要不要的问题而是怎么设计才能既合规又不拖累效率。第三信息发到哪里。大屏 LED、管理后台列表、公共查询端、移动端每个渠道对格式、时效和权限的要求都不一样。管理端发的东西不能直接推到外部大屏外部查询端也不能看到内部审核备注。把这三个问题捋顺系统边界就自然而然出来了需要一个统一的数据接入层负责不同来源数据的接收、解析和标准化需要一个业务处理层负责清洗、去重、审核、优先级判断和发布状态管理还需要一个展示层把管理端和对外发布端分开处理。1.2 为什么选 SpringBoot Vue 而不是别的组合前后端分离不是潮流是这套业务场景下的实际需要。后端处理数据接入、审核流转、定时任务、WebSocket 推送压力集中在接口稳定性和并发上前端要做复杂的管理界面、可视化大屏、地图交互这两块如果耦合在一个单体项目里开发和上线节奏会被互相拖死。SpringBoot 这边生态成熟是最大优势。交通信息对接要用的 HTTP 客户端、定时任务、权限框架、消息中间件Spring 全家桶都有现成方案不需要自己造轮子。而且团队里招人容易SpringBoot 的普及率太高了后面维护不愁没人接手。Vue 这边我选择它而不是 React核心原因是管理端和大屏这种中后台场景里Vue 的模板语法更直观组件化开发效率高而且和 ECharts、腾讯地图这些可视化库的配合非常顺。另一个私心是 Vue 的中文文档和社区资料质量很高团队新人上手成本低。这个项目涉及大量表单、列表、状态标签Vue 的响应式数据绑定写起来比手动操作 DOM 舒服太多。版本选择上我当时也犹豫过。SpringBoot 2.7 和 3.x 之间我最后选了 2.7因为 3.x 刚出来时很多第三方 starter 还在适配期尤其是我要用的一些地图和消息组件踩坑成本太高。这个决定在后来集成过程中真的帮了大忙具体原因放到第5章细讲。2. 后端核心模块数据接入、审核流转与定时任务怎么落地2.1 数据接入层的抽象设计避免每个数据源都改业务代码数据接入是这类平台最容易写烂的地方。如果直接在每个 Controller 里写死调用某个外部接口前三个月项目能跑半年后接入第六个数据源时就会崩溃。我用的方式是抽象一个数据源适配器接口把“取数据”和“处理数据”彻底分离。public interface TrafficDataSource { // 数据源类型用于标识和配置 DataSourceType type(); // 拉取原始数据不同实现返回不同格式的原始数据 ListRawTrafficData fetch(); // 把原始数据转换为平台统一的消息模型 ListTrafficMessage convert(ListRawTrafficData rawList); }第三方路况接口、人工录入、Excel 定时导入都是这个接口的一个实现。人工录入比较特殊它不是主动拉取而是等待用户在页面上提交所以我单独做了一个ManualDataSourceImplconvert 方法直接返回页面提交的数据。Component public class ManualDataSourceImpl implements TrafficDataSource { Override public DataSourceType type() { return DataSourceType.MANUAL; } Override public ListRawTrafficData fetch() { return Collections.emptyList(); } Override public ListTrafficMessage convert(ListRawTrafficData rawList) { // 人工提交的数据已经在Controller层封装为TrafficMessage return rawList.stream().map(raw - (TrafficMessage) raw).collect(Collectors.toList()); } }这样做最大的好处是业务层完全不用关心数据到底从哪来只需要面向TrafficDataSource接口编程。新增一个数据源时写一个新的实现类注册到 Spring 容器里然后在配置中心加上对应的开关就行已有的消息处理、审核、发布逻辑一行都不用动。还有一点值得提数据源接入时的幂等设计。外部接口偶尔会重复推送或者定时任务重跑如果不做幂等同一条交通消息会被插入两次。我在消息表里加了source_msg_id唯一索引同一个来源的消息 ID 重复时就跳过这个字段在多个数据源场景下基本上是必需品。2.2 审核流程的状态机设计不该穿越的状态一律不让穿越交通信息一旦发错影响面很大所以审核流程必须严格。但严格不等于代码写死多个 if-else那样状态多了以后根本维护不了。我用的是一套简单的状态机。消息的状态流转我定义成下面这条链路草稿(DRAFT) - 待审核(PENDING) - 已发布(PUBLISHED) - 已下线(OFFLINE) | | - 已驳回(REJECTED) - 已撤回(WITHDRAWN)代码里我把状态转换写成了一张表用 Map 或者枚举校验器来管理不允许的状态迁移直接抛异常。public enum MessageStatus { DRAFT(0), PENDING(1), PUBLISHED(2), REJECTED(3), WITHDRAWN(4), OFFLINE(5); private static final MapMessageStatus, SetMessageStatus ALLOWED_TRANSITIONS Map.of( DRAFT, Set.of(PENDING), PENDING, Set.of(PUBLISHED, REJECTED), PUBLISHED, Set.of(OFFLINE, WITHDRAWN), OFFLINE, Set.of(PUBLISHED) ); public boolean canTransitionTo(MessageStatus target) { return ALLOWED_TRANSITIONS.getOrDefault(this, Set.of()).contains(target); } }实际业务中还有个隐藏需求审核通过后消息是立即发布还是定时发布。我在状态流转里加了个publishTime字段如果审核时选了定时发布状态转到 PUBLISHED 但实际推送动作由定时任务触发。这样审核人只需要关心内容对不对不用掐着时间点手动点发布。权限设计上审核接口必须是管理员角色才能调用普通编辑只能提交和撤回自己的草稿。Spring Security 的PreAuthorize注解可以搞定但这个项目里我连 RBAC 都没铺开只在用户表里加了一个role字段用自定义权限注解控制接口访问够用且不复杂。2.3 定时聚合与发布任务的两种实现方式这里要说清楚Spring 的Scheduled和 Quartz 到底怎么选。我项目中两个都用了场景完全不同。数据聚合类任务比如每 5 分钟拉一次路况接口、每 15 分钟同步一次公交调整信息这种任务无状态、单机执行就行直接用Scheduled最简单。Component Slf4j public class TrafficDataSyncTask { private final ListTrafficDataSource dataSources; public TrafficDataSyncTask(ListTrafficDataSource dataSources) { this.dataSources dataSources; } Scheduled(cron 0 */5 * * * ?) public void syncExternalTrafficData() { dataSources.stream() .filter(source - !source.type().equals(DataSourceType.MANUAL)) .forEach(source - { try { ListTrafficMessage messages source.convert(source.fetch()); trafficMessageService.saveAndPublish(messages); } catch (Exception e) { log.error(数据源[{}]同步失败, source.type(), e); } }); } }但定时发布任务不一样它要求可靠到点了必须发布不能因为服务重启就丢任务。Scheduled是内存态的重启后内存里的任务状态全丢了所以定时发布我用了 Quartz 的JobDetail加CronTriggerJob 状态持久化到数据库。这样即使服务宕机重启未执行的发布任务也会继续触发。Quartz 集成时有个容易忽略的点默认的RAMJobStore也是内存态必须配JobStoreTX或JobStoreCMT把 job 数据持久化到数据库才能真正实现“重启不丢任务”。2.4 大文件上传下载的处理交通信息发布时经常要附带图片、短视频作为现场佐证比如事故现场的短视频、施工路段的照片。这些文件动辄几十上百 MB直接一次性上传网络稍微波动就失败。我实现了一套前端分片 后端合并的方案。前端把文件按固定大小比如 5MB切成多片逐片上传后端每收到一片就往磁盘写一片全部传完后调一次合并接口。// 前端分片上传逻辑 const CHUNK_SIZE 5 * 1024 * 1024; function createChunks(file) { const chunks []; let start 0; while (start file.size) { chunks.push(file.slice(start, start CHUNK_SIZE)); start CHUNK_SIZE; } return chunks; } async function uploadWithChunks(file, uploadId) { const chunks createChunks(file); for (let i 0; i chunks.length; i) { const formData new FormData(); formData.append(file, chunks[i]); formData.append(uploadId, uploadId); formData.append(chunkIndex, i); await axios.post(/api/upload/chunk, formData); } await axios.post(/api/upload/merge, { uploadId, fileName: file.name }); }这里要提醒一个自己实现时的细节合并时要用 stream 追加写不要用 FileOutputStream 直接一个字节一个字节写也不要把所有 chunk 一次性读进内存文件大时直接 OOM。3. 数据库设计与实时推送链路的关键细节3.1 核心表结构设计为什么状态字段要单独设计这节先说表结构。平台核心表其实就四张消息表、用户表、设备/渠道表、发布记录表。消息表是整张设计的核心。我列几个关键字段CREATE TABLE traffic_message ( id BIGINT AUTO_INCREMENT PRIMARY KEY, msg_type VARCHAR(32) NOT NULL COMMENT 消息类型ROAD_SITUATION/ACCIDENT/CONSTRUCTION/BUS/WEATHER, title VARCHAR(200) NOT NULL, content TEXT, level TINYINT DEFAULT 3 COMMENT 严重级别1-紧急 2-较高 3-普通, status TINYINT DEFAULT 0 COMMENT 状态0-草稿 1-待审核 2-已发布 3-已驳回 4-已撤回 5-已下线, source VARCHAR(64) COMMENT 数据来源MANUAL/API_XXX/SYNC_FILE, source_msg_id VARCHAR(128) COMMENT 来源消息ID用于幂等去重, publish_time DATETIME COMMENT 计划发布时间, expire_time DATETIME COMMENT 过期时间过期自动下线, create_by BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_by BIGINT, audit_time DATETIME, channel_ids VARCHAR(255) COMMENT 发布渠道ID集合逗号分隔, KEY idx_status_publish (status, publish_time), UNIQUE KEY uk_source_msg (source, source_msg_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT交通信息发布消息表;source_msg_id这个唯一索引我前面提过是幂等去重的关键。idx_status_publish索引则是为了定时任务扫描“已发布且到达发布时间”的消息时不会全表扫描。发布记录表也很重要它记录了每条消息发到了哪个渠道、推送结果成功还是失败、失败原因是什么。这个表的价值在事后追溯一旦某条消息发错了可以快速定位是什么时候、由哪个渠道推送出去的。CREATE TABLE publish_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, message_id BIGINT NOT NULL, channel_id BIGINT NOT NULL, push_status TINYINT DEFAULT 0 COMMENT 0-待推送 1-成功 2-失败, fail_reason VARCHAR(500), retry_count TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_message (message_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 WebSocket 实时推送为什么不用轮询消息发布之后前端大屏和管理端列表都要立刻看到最新状态。用 HTTP 轮询每 5 秒刷一次也不是不行但体验上明显有延迟而且大屏同时开好几块轮询请求量会放大很多倍。WebSocket 是这个场景的自然选择。连接建立后服务端可以主动把新消息、状态变更、设备离线通知推到前端延迟控制在毫秒级。但 WebSocket 有一个常见的坑连接鉴权。WebSocket 的握手阶段是一次普通的 HTTP 请求可以拿到 URL 参数和 header但握手成功后后续消息帧里不会再携带 token。所以鉴权必须在握手拦截器里做鉴权通过后把用户信息放进attributes后续处理器从session.getAttributes()里取。Component public class AuthHandshakeInterceptor implements HandshakeInterceptor { Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) { if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest (ServletServerHttpRequest) request; String token servletRequest.getServletRequest().getParameter(token); Long userId tokenService.parseUserId(token); if (userId ! null) { attributes.put(userId, userId); return true; } } response.setStatusCode(HttpStatus.UNAUTHORIZED); return false; } }消息推送的时机我做了分类。普通消息更新走广播通道就是sendToAll所有前端实例都能收到。但像审核驳回这种操作只需要通知提交者本人这时候用用户级通道只推给指定 userId 对应的 WebSocket 会话。前端在大屏上收到的消息格式我统一成这样的 JSON{ type: MESSAGE_PUBLISHED, data: { messageId: 1001, title: 学府路西段发生交通事故请绕行, level: 2, publishTime: 2025-01-12 09:30:00 } }前端根据type字段分发处理逻辑新消息就插入列表并高亮状态变更就更新对应行的状态标签。WebSocket 还有一个不能忽略的点是心跳保活。Nginx 默认 60 秒会断开空闲连接而 WebSocket 本身没有内置心跳机制。我实现里用了 Spring 的WebSocketHandler配合Scheduler每 30 秒给前端发一次 ping 消息前端收到后回一个 pong双方都通过这个机制判断连接是否还活跃。3.3 推送失败的重试与补偿大屏部署在公共场所网络环境不一定稳定消息推送失败是常态。我的做法是推送动作不直接依赖 WebSocket而是先写publish_record表状态为“待推送”再触发 WebSocket 推送。如果推送失败记录更新为“失败”由定时任务每 30 秒扫描一次失败记录自动重试最多 3 次。这里有个设计上的小心机把“业务成功”和“推送成功”分开。业务上的发布成功是指消息状态变成 PUBLISHED推送成功是指前端确实收到了。两者不能混在一起否则一旦推送端出问题消息状态也会卡住审核流程就被拖死了。4. Vue 前端管理后台与可视化大屏的完整实现4.1 Vue 工程搭建、路由设计与请求封装前端部分我用的是 Vue 3 Vite。选 Vite 的原因很实在启动速度快开发时热更新几乎秒开比起旧版 Webpack 那套慢吞吞的编译体验Vite 对开发效率的提升是质的。项目结构我按功能模块划分没有用默认的 views/components 平铺结构。原因很简单这种中后台项目模块边界非常清晰按模块分包可以让多个开发同时改代码时冲突降到最低。src/ ├── api/ # 接口请求定义 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── layout/ # 整体布局 ├── router/ # 路由配置 ├── stores/ # 状态管理 └── views/ ├── dashboard/ # 大屏展示 ├── message/ # 消息管理 ├── audit/ # 审核管理 ├── device/ # 设备管理 ├── statistics/ # 统计报表 └── system/ # 用户/角色管理路由的配置里有一个大屏项目都遇到过的问题路由模式。管理后台我用的是 history 模式地址栏干净、刷新页面不会丢路由但 history 模式要求服务器把不存在的路径都回退到 index.html否则一刷新就 404。这部分 Nginx 配置我放在第5章讲。请求封装这块属于是前端工程化的基本功但我还是要重点强调一下因为团队的接口规范往往在这里体现。import axios from axios const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { // 后端统一返回 { code, data, message } const res response.data if (res.code 0) { return res.data } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { if (error.response?.status 401) { router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )baseURL用/api而不是写死的 IP 和端口这是为了联调和部署的灵活性。开发时可以靠 Vite 的代理把/api转发到后端生产环境靠 Nginx 转发前端代码完全不用改。4.2 管理后台消息管理、审核与设备管理管理后台的页面功能相对标准但有几个交互细节让我反复改了很久。消息列表页默认只显示“非下线”的消息通过 Tab 切换查看不同状态。列表要支持按类型、级别、来源筛选。这里我踩了一个分页的坑用 Vue 的computed做前端筛选分页数据量小的时候没问题消息量到了几千条以后筛选和分页都开始卡。后来改成数据全部走后端分页前端只负责传参数流畅度提升明显。审核页面审核人打开一条待审核消息能看到消息全文、来源、建议发布的渠道以及发布时间的预填值。审核操作有两个按钮通过和驳回。驳回时必须要写驳回原因这是硬校验防止审核人随手点击让提交人一头雾水。设备管理页这个页面管理的是大屏 LED、信息发布终端等硬件设备。每个设备维护一个在线状态设备通过定时上报心跳来保活。页面上的状态指示灯是一个 Vue 的动态 class根据设备状态显示不同颜色的圆点。设备离线超过 5 分钟会在列表顶部显示告警条这个数据是 WebSocket 推过来的。4.3 实时大屏与地图可视化大屏是整个平台最受瞩目的部分也是技术上最繁琐的部分。我把大屏拆成三个区域顶部标题栏、中间地图区、左右两侧的数据面板。地图这块我用了腾讯地图的 JavaScript API。选它的原因一个是文档中文比较好读另一个是交通类数据在腾讯地图上有不少现成的图层处理方案。地图上需要展示的内容包括正在发生的交通事故点位、施工路段区域、公交临时调整的线路高亮。import { Map } from tarjim/qq-map-sdk const map new Map(map-container, { center: [39.9042, 116.4074], zoom: 11 }) // 新增事故标记点 function addAccidentMarker(message) { const marker new map.Marker({ position: message.location, title: message.title, content: div classaccident-marker${message.title}/div }) }这里要提醒一点地图实例和 Vue 组件的生命周期必须处理好。Vue 组件销毁时地图实例要手动destroy()否则页面切换后地图实例还挂在内存里大屏长时间运行内存会一路涨上去最终浏览器崩溃。图表部分用的是 ECharts。大屏上有车流量趋势折线图、消息类型占比饼图、渠道发布成功率柱状图。ECharts 的更新我尽量用setOption而不是重新实例化图表这样动画过渡自然性能也好得多。WebSocket 和大屏的联动是整个前端的核心逻辑。大屏组件在onMounted中建立 WebSocket 连接收到消息后判断消息类型如果是新的交通消息将点位加进地图并把消息摘要插入侧边滚动列表如果是状态变更更新对应点位的状态颜色。const ws new WebSocket(ws://${location.host}/ws/traffic?token${token}) ws.onmessage (event) { const payload JSON.parse(event.data) if (payload.type MESSAGE_PUBLISHED) { addAccidentMarker(payload.data) messageList.unshift(payload.data) } }大屏上还有一个滚动消息列表的效果我一开始用的是 CSS 动画但循环滚动时总是有闪烁和卡顿。尝试几种方案后最后用了插件让列表平滑循环滚动消息多的时候也没有卡顿感。4.4 Vue 播放实时视频流 m3u8交通大屏除了数据展示还要接入现场路口的监控视频。视频流是 HLS 格式的 m3u8 地址Vue 播放 HLS 需要引入hls.js库。import Hls from hls.js function playM3u8(videoElement, url) { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(videoElement) hls.on(Hls.Events.MANIFEST_PARSED, () { videoElement.play() }) } else if (videoElement.canPlayType(application/vnd.apple.mpegurl)) { // iOS Safari 原生支持 HLS videoElement.src url } }这里有个实际部署的坑如果视频流和前端不在同一个域名下跨域问题经常导致视频无法播放。解决方式是让后端在返回视频列表时直接给一个后端代理地址播放时走代理避免跨域。另外一个容易忽略的点是视频组件销毁时要hls.destroy()否则多路视频反复切换内存占用会快速上升。5. 部署集成中的版本坑与调优经验5.1 Spring Boot 版本过高引起的兼容问题这个是整个项目里花费时间最多的坑写出来希望你能避开。项目后期我有一次尝试把 SpringBoot 从 2.7 升到 3.2结果整个工程直接编译不过。原因很典型SpringBoot 3.x 从 Java EE 规范切到了 Jakarta EE所有javax.*包都要改成jakarta.*。// SpringBoot 2.x import javax.persistence.Entity; import javax.validation.constraints.NotBlank; // SpringBoot 3.x import jakarta.persistence.Entity; import jakarta.validation.constraints.NotBlank;如果项目里只是一两个 import 还好说但我项目里有几十个实体类和一堆参数校验注解全量替换不是改个包名这么简单。更麻烦的是有几个第三方 starter 当时还没适配 3.x比如我用的旧版资源映射配置和一些工具包一升级就冲突。后来我试了一些兼容性改造方案但评估下来还是要动的东西太多决定退回 2.7。这个经历给我最大的教训是对于业务系统稳定优先于追新。SpringBoot 3.x 再香如果团队没有准备好应对组件生态的迁移成本升级就是不划算的。如果你确实要新开项目建议直接查一下自己用的 starter 是否已经支持 SpringBoot 3.x。如果支持用 3.x 没问题如果不确定用 2.7 或 2.6 会更稳妥。项目稳定运行比什么都重要框架版本高不高真没那么大的差距。5.2 SpringBoot 打包进 Docker Desktop 时踩的坑项目部署时我准备做成 Docker 镜像本地用 Docker Desktop 调试。这里有一个很隐蔽的坑SpringBoot 的 Dockerfile 基础镜像版本必须和宿主机架构匹配。我的开发机是 Apple Silicon 芯片直接拉默认的openjdk:8-jdk-alpine镜像启动时直接报错说明架构不兼容。后来改用带arm64标签的基础镜像问题才解决。FROM eclipse-temurin:8-jre WORKDIR /app COPY target/traffic-platform.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]另外提一句SpringBoot 项目打成 jar 包后如果用 Dockerfile 默认的 CMD 命令启动时区会用 UTC定时任务的执行时间会差 8 个小时。这是个非常隐蔽的坑我在测试环境跑定时任务时发现时间总是不对后来在 Dockerfile 里显式加了一行ENV TZAsia/Shanghai加上之后定时任务时间就正常了。这个问题一旦上线才发现排查成本会很高。5.3 Vue 前端安装、构建配置与 history 路由刷新 404前端构建环境这块最常见的坑是版本不匹配。Vite 项目对 Node 版本有要求我用的是 Vite 5要求 Node 18。如果本机 Node 版本太老执行npm install和npm run build会直接报错。安装依赖的时候还有另一个问题npm install很慢或者卡死。这种情况在国内环境下很常见解决方式是切换 npm 镜像源。我现在的做法是直接在项目根目录加.npmrc文件把 registry 固定下来这样团队每个人拉下来的依赖版本一致也能避免镜像不一致导致的锁文件冲突。registryhttps://registry.npmmirror.com/前端构建还有一个必须注意的地方history 路由模式下的 404 问题。我用 Vue Router 的 history 模式直接访问http://服务器IP/message这个地址时Nginx 找不到这个路径会返回 404。解决办法是在 Nginx 里配置 fallbacklocation / { root /opt/traffic-web/dist; index index.html; try_files $uri $uri/ /index.html; }这样所有前端路由路径都会回退到index.html由 Vue Router 自己解析路径刷新就不会 404 了。5.4 SpringBoot 资源映射与前后端联调资源映射的问题出现在上传的视频、图片文件怎么被前端访问上。SpringBoot 默认只处理静态资源目录下的文件上传到本地磁盘的文件路径默认是访问不到的。我的实现是在后端加一个资源映射配置把磁盘上的上传目录映射到/upload/**这个虚拟路径Configuration public class ResourceConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:/opt/traffic-upload/); } }这样前端可以直接通过http://后端地址/upload/xxx.jpg访问上传的文件。但生产环境一般不会直接用后端地址访问资源而是通过 Nginx 配置一个/upload/代理到磁盘目录这样静态资源和接口分离后端只承担业务逻辑性能更好location /upload/ { alias /opt/traffic-upload/; }还有一个我在联调阶段踩到的坑前端开发服务器跑在 5173 端口后端接口在 8080 端口直接请求必然跨域。开发时用 Vite 的代理配置解决// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true }, /ws: { target: ws://localhost:8080, ws: true } } }注意/ws的代理要加ws: true否则 WebSocket 握手会被代理截断连接一直建立不起来。这个配置我找了好久才发现是缺了ws: true这个参数相当隐蔽。5.5 性能优化大屏页面加载慢和列表卡顿大屏页面的性能问题几乎是一定会遇到的。我总结下来主要有三个瓶颈。第一个是地图初始化加载慢。腾讯地图的 JavaScript API 脚本比较大首次加载会明显等待。我做了两件事把地图脚本改成异步加载不用index.html里同步引入大屏页用路由懒加载用户进入大屏页时才加载地图组件。第二个是 ECharts 图表数据渲染卡顿。大屏上有多个图表每个图表的定时刷新如果都直接改整个 optionECharts 会频繁重绘性能很差。我的做法是图表更新时分类型处理折线图只更新series.data饼图只更新series[0].data不要每次都重置完整的 option。这个优化在大屏上效果非常明显CPU 占用率降低了一半以上。第三个是消息列表频繁更新导致 DOM 重排。大屏上滚动消息列表每几秒就插入一条新消息如果不加限制DOM 节点会无限增加。我限制了列表最大长度超过 100 条就自动截断保证 DOM 数量可控。部署上线后我用压测工具对后端的/api/message/list和/api/message/publish两个核心接口做了一轮压测。在 500 并发下接口响应时间从最初的 800ms 优化到 200ms 以内。主要优化点有两个一个是列表查询加上了必要的索引另一个是消息内容字段在列表接口中只返回摘要全文走详情接口减少大数据量传输。这两点对于信息发布类系统的优化非常通用。这套系统从需求梳理到上线前后大概用了三个月。回过头看最有价值的不是某个单独的技术亮点而是所有模块之间的配合方式数据接入层让新数据源变得可插拔审核状态机让业务安全可控WebSocket 加上 Vue 的响应式更新让前端能实时感知后端变化这两者是我觉得最值得复用的设计。如果你也要做类似的信息发布平台我的个人建议是一开始不要急着堆功能先把数据接入的抽象和审核状态流设计做好。这两个地基一旦打稳后面加渠道、加设备、加大屏都是顺理成章的事。反过来如果地基没打好数据源一多代码就乱套审核流程一长就出状态乱跳的问题后期返工成本会非常高。最后再分享一个实际操作中的小技巧大屏页面开发时一定开着浏览器的 Performance 面板专门盯内存曲线。大屏页面通常要长时间挂机运行内存泄漏在这个场景下特别致命。我开发期间就抓出来两个泄漏点一个是地图实例没销毁一个是 ECharts 的监听器没卸载。这类问题在普通页面看不出影响但在大屏场景上跑一天之后浏览器卡死的概率几乎是百分之百。提前处理掉后续运维会轻松很多。
RELATED

相关推荐

PyTorch深度学习入门:从环境搭建到实战MNIST手写识别

PyTorch深度学习入门:从环境搭建到实战MNIST手写识别

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/9 7:40:21
ECC内存纠错原理与Linux实战监控指南

ECC内存纠错原理与Linux实战监控指南

1. ECC不是缩写游戏,而是工程里最沉默的守门人ECC——这三个字母在日常聊天里可能被当成某个新出的网红咖啡品牌,或者某家刚融资的科技公司简称。但只要你碰过服务器、修过内存条、调过FPGA、写过嵌入式固件,甚至只是给NAS装过一次系统&#…

📅 2026/9/9 7:40:21
STM32厨房安全监测系统:ESP01S+OneNET+小程序全链路实战

STM32厨房安全监测系统:ESP01S+OneNET+小程序全链路实战

做物联网毕业设计或竞赛项目,很多人不是卡在代码难写,而是卡在“整条链路太长”。以厨房安全监测系统为例:你需要让 STM32 采集温湿度、烟雾浓度,通过 ESP01S 将数据推到 OneNET 云平台,再在微信小程序里显示并远程控制…

📅 2026/9/9 7:35:20
MORE NEWS

更多资讯

📰

用Vue Skills驯服AI编程助手:一套可落地的项目规范方案

最近做完一轮 Vue 项目迭代,复盘了一下这一个月里用 AI 编程助手写前端代码的过程,发现一个很有意思的结论:不是 AI 不聪明,是它压根不知道我这个项目有什么规矩。AI 写出来的组件语法挑不出大毛病,但代码风格能把我逼…

📰

飞飞江湖v2.0商业版:服务器集群改造与运营实战解析

简介:飞飞江湖 v2.0正式商业版是一套采用BBS模型构建的论坛社区类源码资源,面向Web开发工程师、独立站长及社区运营相关人员,可用于搭建互动交流平台、开展二次开发或进行系统架构研究。该版本以rar压缩包形式发布,平台暂未标注文…

📰

飞飞江湖v2.0商业化复盘:从功能验证到稳定运营的关键实践

简介:《飞飞江湖 v2.0正式商业版》是一款基于BBS模型构建的论坛社区类商业源码,面向Web开发者、社区运营者以及对PHP/数据库架构感兴趣的IT学习者。源码开放程度高,便于二次开发与功能扩展,可帮助使用者深入理解论坛系统的用户认证…

📰

从1级到12级:技术学习升级路线的实战复盘与避坑指南

有些人看到“12级(8.5/10)”这几个字,可能会觉得这是某个游戏里的角色等级。但对我来说,这就是我这大半年学习技术的真实状态。从1级那种连命令行都敲不利索的小白,一路摸爬滚打到现在的12级,进度条卡在85%,就差最后这…

📰

JMeter接口测试核心问题解析:从原理到性能压测实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

2026年日志分析平台选型指南:从需求测算到PoC落地的实战方法论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬