ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue电网服务系统实战:从工单管理到可视化部署

SpringBoot+Vue电网服务系统实战:从工单管理到可视化部署 做电力行业信息化这几年接触过不少类似“基于SpringBootVue的电网服务系统”的项目。这名字一听就很典型——既符合学校毕业设计的选题套路也是不少电力三产、地市级供电公司信息化改造的真实需求。凡是做这类项目的人大概率会遇到同一批问题业务边界怎么划、前后端怎么拆、权限怎么做、报表和可视化怎么出、最后又怎么让系统真正被业务人员用起来。这篇文章我不打算给你堆一堆教科书式理论而是按照一个完整项目的推进顺序把我在实际开发中踩过的坑、验证过的方案、以及可以照抄的代码和配置都捋一遍。内容既覆盖SpringBoot后端的接口设计和数据建模也覆盖Vue前端从搭建到部署的全流程最后再补一份高频问题速查表。无论你是要拿它做课程设计还是真的在做一个能上线的电网服务系统这篇文章都能帮你少走一段弯路。1. 整体设计电网服务系统到底在解决什么问题1.1 业务场景与核心功能边界电网服务系统本质上是一套面向电力用户、坐席人员、抢修班组和运维管理者的综合服务平台。它的业务主线不是“管理设备”而是“管理服务”用户申请报修、提交咨询、查询账单坐席受理后派单给抢修人员抢修人员接单、到场、处理、反馈最后用户确认、系统归档。我在设计这类系统时第一件事不是画架构图而是先和业务方梳理清楚三个核心问题谁在用、解决什么痛点、哪些流程必须线上化。以最常见的市级供电服务场景为例核心角色一般有四种用电用户C端需要线上报修、进度查询、账单缴费记录、用电政策浏览。坐席/客服B端-受理侧接收用户请求分类、审核、创建工单回访用户。抢修人员B端-执行侧接收工单、查看任务详情、上报到场与完工信息。系统管理员用户管理、角色权限、数据字典、日志审计、可视化大屏配置。对应的核心功能可以收敛为五大模块工单管理、用户服务、设备台账、数据可视化、系统管理。好的系统不是堆功能而是抓核心链路。对于电网服务系统“工单”就是绝对的核心实体。设计上我建议把所有业务都围绕工单的“生命周期”展开即“用户提出申请 → 坐席审核派单 → 抢修人员处理 → 用户反馈归档”并且每一步都留下可追踪的操作日志和时间戳。这样既方便用户端实时查看进度也让管理层能统计“平均响应时长”“超时工单数”这些运维指标。1.2 为什么是SpringBoot Vue而不是其他组合这个问题我在方案评审时被问过无数遍。先说结论对于电网服务这类典型的“企业级数据管理系统”SpringBoot Vue目前依然是综合成本最低、生态最完善、招聘市场上最容易找到人的组合。SpringBoot的优势在于它把Spring生态中繁琐的配置几乎全部自动化了。你要连数据库加一个spring-boot-starter-data-jpa或MyBatis的starter你要做接口文档集成SpringDoc或Swagger就是加依赖加注解的事你要做定时任务、消息推送、缓存都有成熟的Starter可以用。相比早期SSM框架需要手写大量XML配置SpringBoot让开发人员把精力放在业务逻辑上这对电力行业这种业务规则复杂、报表繁多的系统尤其重要。Vue这边则胜在“渐进式”和“生态”。你初期可以用Vue Router加Axios快速搭建一个多页面应用后期需要复杂状态管理时再引入Pinia或Vuex需要UI组件时直接上Element Plus完全不用重构。而且Vue对国内开发者非常友好中文文档齐全遇到问题几乎都能搜到解决方案。对于电网服务系统中常见的数据表格、复杂表单、实时数据刷新、大屏可视化这些需求Vue组件化开发能显著降低维护成本。这里要强调一点我说的SpringBoot Vue默认指的是“前后端完全分离”的架构——前端静态资源可以独立部署通常是Nginx后端只提供JSON接口。这么做最大的好处有两点一是前端开发可以完全脱离后端环境独立调试二是后端接口可以被PC端管理系统、移动端H5甚至未来的小程序同时复用避免了重复开发。2. 从零搭建后端核心模块与数据建模2.1 项目初始化版本选择、依赖规划与目录结构不知道你有没有踩过“SpringBoot版本太高启动就报错”的坑。在电网这类政企项目中最稳的方案不是追新而是求稳。我的建议是Java 8 Spring Boot 2.7.x。Spring Boot 3.x虽然已经相当成熟但它强制要求JDK 17很多老项目里基于JDK 8的第三方Jar比如某些报表组件、国产数据库驱动会存在兼容性问题没必要给自己找麻烦。下面是核心依赖的规划清单我直接给你一份可用的pom.xml核心片段parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties java.version1.8/java.version mybatis-plus.version3.5.3.1/mybatis-plus.version jjwt.version0.11.5/jjwt.version /properties dependencies !-- Web 基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus 持久层 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Redis 缓存 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- JWT 认证 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version${jjwt.version}/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version${jjwt.version}/version scoperuntime/scope /dependency !-- 接口文档 -- dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-ui/artifactId version1.7.0/version /dependency !-- Lombok 简化代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies后端目录结构我建议遵循“按业务模块分包而不是按技术层分包”的原则。如果你按Controller、Service、Mapper这样分项目大了以后会非常痛苦。比较推荐的做法如下com.power.grid ├── common // 通用类统一返回值、异常处理、常量、工具类 │ ├── result │ ├── exception │ └── utils ├── config // 配置类MyBatis-Plus配置、Redis配置、WebMvc配置 ├── security // 登录鉴权JWT过滤器、登录Service、权限注解 ├── module // 业务模块按领域划分 │ ├── workorder // 工单模块 │ │ ├── controller │ │ ├── service │ │ ├── mapper │ │ ├── entity │ │ └── dto │ ├── user // 用户模块 │ ├── device // 设备台账模块 │ └── dashboard // 可视化数据模块 └── GridApplication.java2.2 数据模型设计工单体系与设备台账数据建模是整个系统中最不能心急的环节。电网服务系统的核心表我一般会设计成这几张用户表sys_user存放用户、坐席、抢修人员、管理员等所有账号通过user_type区分角色通过dept_id关联组织机构。工单表work_order这是绝对核心的流水型业务表。关键字段如下字段名类型说明idbigint主键order_novarchar(32)工单编号按规则生成user_idbigint提交申请的用户IDorder_typetinyint工单类型1-报修2-咨询3-投诉4-建议descriptionvarchar(500)问题描述addressvarchar(200)故障地址longitude / latitudedecimal故障位置经纬度用于GIS调度statustinyint状态1-待受理2-待派单3-处理中4-已完成5-已归档assignee_idbigint当前处理人IDhandler_phonevarchar(20)抢修人员电话create_time / update_timedatetime创建与更新时间deletedtinyint逻辑删除标记设备台账表device_info记录变压器、电表箱、杆塔等设备的基本信息和所属区域。电网服务系统如果要对设备做巡检和故障关联这张表就是基础。核心字段包括设备编码、设备名称、设备类型、运行状态、安装位置、运维班组、投运日期。操作日志表work_order_log追踪工单状态变更历史。每一笔状态流转都插入一条记录包含操作人、操作内容、操作前状态、操作后状态和时间。这个表非常简单但价值极高——没有操作日志的系统出了问题根本没法追溯。在表结构设计上有几个经验值得说一下。第一所有核心业务表都要有逻辑删除标记不要物理删数据电力行业审计需求很普遍。第二状态字段用tinyint不要用varchar存中文对应关系放在枚举或数据字典里否则后期改状态名会非常痛苦。第三工单编号不要用自增ID直接暴露给用户我一般生成规则是“类型前缀 年月日 当日流水号”比如GD202503120001方便客服和用户电话报号查询。2.3 接口层落地MyBatis-Plus分页、Redis缓存与WebSocket推送后端核心代码我挑三个真正能提升效率的点来讲。第一个是MyBatis-Plus的分页插件这是SpringBoot项目里最常见的需求——列表页必分页不分页系统根本扛不住。分页插件配置其实非常轻量Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }使用的时候Controller接收前端传来的current页码和size每页条数Service里用Page对象包一层即可public IPageWorkOrderVO pageWorkOrders(WorkOrderQuery query) { PageWorkOrder page new Page(query.getCurrent(), query.getSize()); LambdaQueryWrapperWorkOrder wrapper new LambdaQueryWrapper(); wrapper.eq(query.getStatus() ! null, WorkOrder::getStatus, query.getStatus()) .like(StringUtils.hasText(query.getKeyword()), WorkOrder::getOrderNo, query.getKeyword()) .orderByDesc(WorkOrder::getCreateTime); IPageWorkOrder entityPage workOrderMapper.selectPage(page, wrapper); // 这里再转换为VO补充用户名、处理人等冗余显示字段 return convertToVO(entityPage); }这个方案看起来简单但实际项目里有两个坑要提醒你一是分页插件不要和多个拦截器混用出顺序问题如果启用了多租户插件或乐观锁插件要注意PaginationInnerInterceptor需要配置在最里层二是多表关联查询不能直接依赖selectPage需要自己写Mapper XML并在IPage对象里注入总数否则会出现“分页查出来的总数不对”的怪问题。第二个是Redis缓存的典型使用位置。电网服务系统中用户数据字典、工单状态枚举、首页统计卡片这些数据访问频率高、实时性要求又不高非常适合缓存。一个典型的用法就是工单首页的即时统计待受理工单数、今日新增工单数、超时未处理工单数。如果每次都去数据库做COUNT(*)数据量大时性能会很难看。我的做法是统计结果先查Redis查不到才去数据库然后设置30秒到1分钟的过期时间并配合定时任务刷新。public DashboardVO getDashboardStat() { String cacheKey dashboard:stat; DashboardVO vo redisTemplate.opsForValue().get(cacheKey); if (vo ! null) { return vo; } // 从数据库聚合统计 vo workOrderMapper.selectDashboardStat(); // 缓存一个较短的时间例如30秒 redisTemplate.opsForValue().set(cacheKey, vo, 30, TimeUnit.SECONDS); return vo; }第三个是WebSocket推送这在电网服务系统里能极大提升用户体验。最典型的场景是用户在前端页面提交了报修工单坐席在后台受理派单后用户端需要实时看到“工单状态已变化”的通知。用轮询当然也可以但效率太低。我习惯用SpringBoot自带的WebSocket实现推送Component ServerEndpoint(/ws/workorder/{userId}) Slf4j public class WorkOrderWebSocket { private static ConcurrentHashMapLong, Session SESSION_MAP new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(userId) Long userId) { SESSION_MAP.put(userId, session); } OnClose public void onClose(Session session, PathParam(userId) Long userId) { SESSION_MAP.remove(userId); } public static void pushMessage(Long userId, String message) { Session session SESSION_MAP.get(userId); if (session ! null session.isOpen()) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error(WebSocket push error, userId{}, userId, e); } } } }工单状态更新时在Service里调用WorkOrderWebSocket.pushMessage(userId, jsonString)前端就能收到实时通知。这里要注意WebSocket的Session并非线程安全多线程并发发送时应加锁或者用队列串行化否则在高频推送时偶现消息丢失。3. 前端工程实战Vue3 Vite Element Plus3.1 前端项目结构与工程化配置前端我建议直接用Vue3 Vite不要再用Vue2 Vue CLI了。原因很简单Vite的启动速度和热更新体验比Webpack时代的Vue CLI好太多而且Vue3的组合式APIComposition API在代码组织和复用性上优势明显插件生态如今也已经完全成熟。项目创建一条命令就能搞定npm create vitelatest grid-web -- --template vue创建完以后按下面这个结构组织代码src ├── api // 按模块封装的接口请求 │ ├── workorder.js │ ├── user.js │ └── dashboard.js ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── stores // Pinia 状态管理 ├── views // 页面组件 │ ├── login │ ├── dashboard │ ├── workorder │ ├── device │ └── system ├── utils // 工具类request封装、格式化 │ ├── request.js │ └── auth.js ├── App.vue └── main.js入口文件main.js里需要一次性安装Element Plus、Pinia和Routerimport { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css import zhCn from element-plus/es/locale/lang/zh-cn import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus, { locale: zhCn }) app.mount(#app)这里有个细节容易被忽略Element Plus默认是英文语言包表格的分页器、日期选择器显示英文对国内项目来说非常奇怪。上面代码中的zhCn导入是必选项。3.2 路由、登录态与Axios请求层前端路由配置的核心有三件事基础页面路由、登录态守卫、动态处理参数。先看路由基础配置import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, name: Login, component: () import(/views/login/index.vue) }, { path: /, component: () import(/layout/index.vue), redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 数据大屏, icon: Odometer } }, { path: workorder, name: WorkOrder, component: () import(/views/workorder/index.vue), meta: { title: 工单管理, icon: Tickets } } ] } ]注意这里使用了路由懒加载组件通过箭头函数导入这样可以减小首屏加载体积对部署在政企内网的系统来说非常友好。路由守卫的逻辑也不复杂没有token就跳登录页有token但访问登录页就跳回首页再加一个简单的角色权限判断。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else if (to.path /login token) { next(/dashboard) } else { next() } })请求层我习惯自己封装一个Axios实例统一处理请求头、错误提示和401跳转import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 15000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data // 后端统一返回结构 { code, message, data } if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response?.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(error.response?.data?.message || 网络异常) } return Promise.reject(error) } ) export default request这套封装看着简单但能把全项目的重复代码砍掉一大半。后端返回统一结构是约定好的{ code: 200, message: success, data: {...} }一旦出问题前端只需在拦截器里处理一遍。3.3 可视化大屏与工单跟踪ECharts、WebSocket和HLS播放电网服务系统几乎必然有可视化大屏的需求——领导视察要看调度中心要盯。核心就是ECharts图表。Vue3配合ECharts最常见也最整洁的做法不是用封装库而是直接在组件里初始化实例。template div refchartRef styleheight: 350px/div /template script setup import * as echarts from echarts import { ref, onMounted, onBeforeUnmount } from vue import { getLoadTrend } from /api/dashboard const chartRef ref(null) let chartInstance null onMounted(async () { chartInstance echarts.init(chartRef.value) const data await getLoadTrend() chartInstance.setOption({ xAxis: { type: category, data: data.map(i i.hour) }, yAxis: { type: value, name: 负荷(kW) }, series: [{ type: line, smooth: true, areaStyle: {}, data: data.map(i i.load) }] }) }) onBeforeUnmount(() { if (chartInstance) { chartInstance.dispose() } }) /script大屏数据不需要秒级刷新我一般设置30秒一个轮询周期或者直接通过WebSocket推送更新。比起轮询WebSocket在“工单实时状态”这个场景下体验好得多。前端接收推送的写法可以直接在Vue组件里创建WebSocket连接const ws new WebSocket(ws://${location.host}/ws/workorder/${userId}) ws.onmessage (event) { const data JSON.parse(event.data) // 收到工单状态变更消息刷新当前工单列表 if (data.type WORKORDER_UPDATE) { loadWorkOrders() ElNotification({ title: 工单更新, message: 工单 ${data.orderNo} 状态已变更为${data.statusName}, type: info }) } }注意ws://在HTTPS环境下会报错必须写成wss://实际项目中我通常把WebSocket的地址从环境变量里读取避免不同环境来回改。再单独说说“Vue播放m3u8”这个问题相关的搜索热度一直很高也是电网视频监控接入场景里的刚需。比如抢修现场的摄像头流、变电站远程巡检画面很多输出的是HLS协议的视频流即m3u8格式。Vue播放m3u8最常见的方案有两种一是用hls.js库二是直接用video.js加videojs-contrib-hls插件。我推荐hls.js体积小且兼容性好核心代码就这几行import Hls from hls.js const video ref(null) const playM3u8 (url) { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(video.value) hls.on(Hls.Events.MANIFEST_PARSED, () { video.value.play() }) } else if (video.value.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 video.value.src url video.value.play() } }这里最常踩的坑是跨域导致m3u8拉流失败。视频流服务器和前端页面不同源时需要让视频服务器返回正确的CORS头或者在Nginx层做代理转发。我的经验是前端页面和视频流统一走同一个域名下的反向代理最省心后面部署章节会详细讲。3.4 报表导出与锐浪报表类控件的后端整合思路电网服务系统的报表需求不会少——工单月度统计表、设备巡检报表、用电量分析表。很多人第一时间想到用前端导出Excel但一旦遇到复杂的样式打印要求比如精确到表格边框、每页固定表头、合并单元格、套打模板纯Excel导出会让人崩溃。这类需求在实际政企项目中经常和“锐浪报表GridReport”这类报表控件配合使用。锐浪报表是一个成熟的报表开发组件它通过模板定义报表格式后端动态传入数据前端控件负责渲染和打印。在SpringBoot后端整合锐浪报表的核心思路是后端只负责准备数据和生成报表文件前端控件负责展示与打印。具体来说后端先把查询结果集按报表模板的字段要求组织成JSON或DataSet格式然后通过接口返回给前端前端拿到数据后加载模板文件绑定数据源并调用打印预览。这么做的好处是模板调整不需要重新发布前后端代码业务人员自己维护模板即可。如果你没有采购商业报表控件也可以用开源的JasperReports或POI方案做替代。这个话题展开讲内容太多这里先记住一条结论任何报表需求先把模板和数据的边界分开把报表设计交给专用的设计器代码里只做数据准备和调用。否则后期报表样式调整一次你就要改一次代码非常被动。4. 安全、性能与发布部署4.1 JWT认证与权限控制电网服务系统涉及的内部业务数据比较多权限设计不能马虎。我的做法依然是经典的JWT 角色权限模型。用户登录成功后后端签发一个JWT Token里面带上用户ID、用户名、角色编码、过期时间前端每次请求在Header里带上Authorization: Bearer token。后端用一个拦截器或过滤器统一解析Tokenpublic class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); request.setAttribute(userId, claims.get(userId, Long.class)); request.setAttribute(userRole, claims.get(role, String.class)); } catch (JwtException e) { // token 非法或过期记录日志不抛出异常由后续接口权限校验拦截 response.setStatus(401); return; } } chain.doFilter(request, response); } }权限控制这边我建议用最直观的“角色-菜单-按钮”三件套不同角色登录后看到不同的菜单按钮级别控制到“新增、编辑、删除、导出”。代码层面可以用Spring AOP配合自定义注解实现比如RequirePermission(workorder:export)方法级拦截。一个很实际的心得是Token有效期别设置太长默认2小时比较合适也别太短否则用户频繁重新登录会被骂。内网系统可以适当放宽到8小时。如果考虑更细的安全体验可以加“Refresh Token Access Token”双Token机制但普通项目一般没必要上复杂度会增加不少。4.2 Redis在项目中到底缓存了什么很多SpringBoot项目引入Redis只是应付面试实际项目里只用来存储登录验证码挺可惜的。在电网服务系统里Redis有三个我非常推荐的使用位置。第一是缓存数据字典。工单类型、设备状态、区域名称这些字段在表里存的是数字编码页面渲染时需要翻译成中文。如果每次都查数据库列表页10条工单就要多执行几十次查询。我会把所有字典项在项目启动时加载进Redis前端需要时通过一个字典接口批量获取。第二是缓存热点业务数据。比如首页统计卡片、用户通知数量、未读消息数。这些数据特点是“访问频率极高、实时性要求低秒级容忍”非常适合缓存并设置短过期时间。第三是实现分布式锁。比如工单自动编号生成时如果系统是集群部署用Redis的SETNX命令加锁可以避免并发生成重复的工单编号。还有一个容易被忽略的细节Redis key设计一定要有规范。我统一使用业务模块:功能:唯一标识的格式如workorder:detail:123456、dashboard:stat。没有规范的key会把后面排查问题的人逼疯到时候满屏都是密密麻麻的key:1你根本不知道它存的是什么。4.3 Nginx部署与前后端联调前后端分离项目的部署我强烈推荐用Nginx托管前端静态资源并转发API请求而不是把前端打包文件塞进SpringBoot的static目录。前者分离彻底、便于独立扩展也符合生产环境的常规做法。流程分三步前端执行npm run build生成dist目录将dist目录上传到服务器Nginx的站点目录配置Nginx将/api前缀的请求转发到后端服务。核心配置如下server { listen 80; server_name your-domain.com; # 前端静态资源 root /opt/grid-web/dist; index index.html; # 解决Vue Router history模式刷新404问题 location / { try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 视频流播放需要关闭代理缓冲 proxy_buffering off; } # 视频流走独立路径避免和前端路由冲突 location /live/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_buffering off; } }这段配置里有两个容易踩的坑。第一Vue Router若使用history模式必须配置try_files否则用户在非首页刷新页面会直接404如果你不想处理这个配置也可以改用hash模式但URL会带#号看起来不够专业。第二视频流代理必须关闭缓存否则m3u8切片文件会被Nginx缓冲延迟导致播放卡顿。后端部署相对简单打包成Jar即可nohup java -jar grid-server.jar \ --spring.profiles.activeprod \ --server.port8080 \ /opt/grid/logs/app.log 21 用了Nginx做反向代理以后前端请求的baseURL可以直接设置成/api同源访问根本没有跨域问题。这也是为什么我前端request.js里默认的baseURL是/api——哪怕本地开发环境也可以借助Vite的server.proxy配置把请求代理到本地后端生产环境则交给Nginx前后端联调到部署全程都不会遇到CORS问题。4.4 关于过度设计工作流引擎、微服务值得上吗电网服务系统做到中期很多人会动一个念头要不要引入工作流引擎要不要拆成微服务我的回答一贯是先看项目规模和团队能力能不上就不上。工作流引擎比如Flowable、Activiti适合流程复杂、表单动态配置、流程经常调整的业务。如果工单审批固定是三级审批没有动态跳转需求那么用代码写状态机比引入工作流引擎更可控。工作流引擎的BPMN图配置、表结构复杂度、调试成本对一个小团队来说是实打实的负担。如果你真的需要有一个折中方案只管理“工单流转”核心流程不走引擎自带的用户体系把流程实例ID和业务表关联起来。微服务更是如此。电力行业的服务系统用户量往往是几千到几万人级别一台8核16G的服务器跑单体应用绰绰有余。微服务拆分带来的是分布式事务、服务注册发现、链路追踪、配置中心等一堆额外问题。用Spring Boot 模块化代码结构一样能保证逻辑清晰。我的建议是保持“模块边界清晰、代码解耦良好”的单体架构等用户量、业务复杂度真正上来以后再按业务边界抽取服务。5. 常见问题与排查技巧实录做这类项目最大的时间杀手不是写业务代码而是排查各种“看似玄学”的问题。下面这份速查表是我这些年从实际项目里一条条攒下来的。5.1 高频问题速查表问题现象根本原因解决办法SpringBoot项目启动失败提示Unable to start web serverSpringBoot版本与JDK版本不兼容或端口被占用优先使用Spring Boot 2.7.x配JDK 1.8检查端口换用server.portIDEA里创建不了SpringBoot项目无法选JDK 1.8新版本Spring Initializr已不再支持JDK 8先在官网生成项目或直接把工程导入后续改pom.xml的父版本即可前端播放m3u8卡顿、黑屏、加载失败跨域或Nginx缓冲了视频流使用hls.jsNginx对视频路径关闭proxy_buffering配置CORS头分页插件查询总数不准确多表Join查询时MyBatis-Plus的count自动生成逻辑不符自定义count查询语句或用SqlParser注解Redis缓存数据后页面显示的是旧数据更新数据库后忘记删除或更新缓存采用“先更新数据库再删除缓存”策略再设置过期时间兜底前端登录后刷新页面路由跳回登录页刷新时Vuex状态丢失Token存在内存里把Token存放在localStorage路由守卫优先读取storage后端返回的时间比实际少8小时JDBC连接未配置时区在JDBC URL中加入serverTimezoneAsia/Shanghai上传图片后页面不显示静态资源路径未映射配置资源映射或把上传目录交给Nginx托管前端请求接口报CORS错误跨域访问开发环境用Vite代理生产环境统一走Nginx同域转发工单状态推进后用户端看不到更新前端没有实时刷新机制用WebSocket推送状态变更消息或用短轮询兜底5.2 三个我每次都会留意的细节第一业务字段的冗余设计要有意识地做。比如工单列表需要显示用户姓名和抢修人员姓名如果每次都要去用户表JOIN列表接口越写越重。我的做法是在工单表里冗余保存user_name和assignee_name字段创建或派单时同步写入。这样列表查询性能高很多代价是写操作时多一次更新但换来的是查询大幅简化值。第二接口设计要面向“操作”而不是面向“课本”。很多项目喜欢把后端接口设计成对数据库表的CRUD接口名是saveOrder、deleteById前端拿到后自己拼装业务逻辑。其实更好的做法是“面向业务动作设计接口”比如acceptWorkOrder受理工单、dispatchWorkOrder派单、completeWorkOrder完工。每个接口内部做完整的事务控制和状态校验前端只负责触发。这样后端代码可读性和安全性都更强还能减少前端逻辑出错的概率。第三日志一定要打全。电网服务系统出了问题是要追责的我在每个工单状态流转的Service方法里都会记录操作日志包含操作人ID、操作时间、变更前状态、变更后状态、请求参数摘要、IP地址。排查问题的时候这些日志就是你还原现场的唯一依据。写在最后做了这么多年的行业系统我最大的体会是技术选型真的不是这类项目最难的环节SpringBoot Vue这套组合本身已经非常成熟了网上的案例也一抓一大把。真正决定项目成败的往往是你对业务的理解深不深、对边界想得清楚不清楚、对细节扣得狠不狠。如果你正在动手做一个电网服务系统我建议你先别急着写代码花两天时间把工单流程的每个状态节点、每种角色的操作权限、每张表的字段含义理清楚。数据模型画好了接口设计就是水到渠成的事。功能上线之后记得把操作日志、异常监控、数据备份这些“看不见的模块”补上那是系统能不能长期稳定运行的关键。最后再分享一个小技巧遇到前端页面或接口数据对不上的问题先打开浏览器的开发者工具看Network面板确认请求URL、请求参数、响应体三个要素80%的问题都能在这一步定位到。不要靠肉眼猜数据不会说谎。
返回列表