企业内部网络管理系统实战:SpringBoot+Vue3+MyBatis
企业里管网络的同学估计都有同一种体验设备台账在 Excel 里IP 规划记在脑子里工单记录散落在微信对话里领导问起来“机房有多少台交换机”“某个 IP 现在通不通”的时候只能现场翻表、问人、靠记忆。真被问多几次你就会发现中小型企业需要的根本不是什么大而全的网管平台而是一套“够用、能跑、好改”的小系统。这套基于 Java SpringBoot Vue3 MyBatis 的企业内部小型网络管理系统源码刚好踩在大多数企业的节奏上前后端分离、MySQL 做数据存储、功能边界克制、二次开发省事。这篇文章我把整套系统的设计与实现细节拆开来讲后端怎么搭、前端怎么接、联调阶段会踩哪些坑一条条写清楚想自己做一版内部管理后台的同学可以照着搞。1. 企业内部网络管理系统到底要解决什么问题1.1 运维场景里的真实痛点先说个我见过的真实场景。某公司五十多台网络设备三种品牌混用IP 地址段分了五个可是台账信息全在一份共享 Excel 里谁改谁知道改完也不一定通知别人。有一次核心交换机扩容端口结果把财务那边的专网地址段给冲了查了大半天才发现 Excel 里的记录是三个月前的老版本。这种问题不是单靠纪律就能解决的人总会偷懒、会忘但系统不会。一套内部网络管理系统至少要把三件事管起来网络资产台账、IP 地址池使用状态、故障报修与处理记录。资产台账解决“有什么、在哪、配置是啥”的问题IP 地址管理解决“哪些地址被占用、哪些还能分”的问题工单流程解决“谁报修、谁处理、结果怎么样”的问题。1.2 系统的正确技术边界做内部系统最容易犯的毛病是“什么都想做进去”。监控想接、日志想收、告警想上、CMDB 想建结果一个内部小系统被撑成了半吊子网管平台开发周期翻倍维护成本也上去了。我建议第一版就守住三个边界只管基础台账与流程不做实时抓包只保留轻量权限体系不做复杂的多租户只在 Web 端做管理不做 Agent 客户端。这套系统的定位是“管理”不是“监控”它解决的是信息一致性和流程可追溯的问题。实时流量监控、端口状态监测这些功能可以等系统跑顺之后再逐步接或者干脆交给专门的监控工具没有必要一上来就塞进内部管理系统里。2. 技术选型SpringBoot Vue3 MyBatis MySQL 是怎么凑到一起的2.1 后端选 SpringBoot不是为了跟风SpringBoot 到今天已经不算什么新技术了但它在企业内部系统里的地位依然稳。原因很简单生态成熟、上手直接、团队招聘容易。企业内部系统通常没有太极端的并发要求几百人同时登录已经算高峰期了SpringBoot 默认配置完全扛得住。另一个核心点在于自动配置的能力。比如集成 MySQL、MyBatis、RedisSpringBoot 都提供了对应的 Starter配置文件写一下就能跑不用像早期 SSM 框架那样堆一大堆 XML。这对小团队维护特别友好一个人也能把后端撑起来。SpringBoot 的版本选择需要注意。如果你是新建项目建议直接用 2.7.x 版本的居多配套的mybatis-spring-boot-starter也更成熟。如果赶新潮用 Spring Boot 3.x就要求 JDK 17 以上而且包名从javax.*换成了jakarta.*不少老项目迁移时会遇到坑。2.2 前端选 Vue3看重的是组合式 API 和生态Vue3 这几年已经彻底站稳了相比 Vue2最大的提升是组合式 API。一套企业内部后台管理系统页面逻辑往往有大量“表格 表单 弹窗”的重复结构用组合式 API 可以把查询逻辑、表单校验、提交逻辑都抽成独立函数代码复用起来比 Options API 舒服得多。配套的生态也跟得上路由用 Vue Router 4状态管理用 PiniaUI 组件库用 Element Plus这几个组合已经成了国内后端管理系统的事实标准。加上 Vite 作为构建工具热更新速度非常快后端接口还在调前端页面已经能空跑看效果了。如果团队里有同学没接触过 Vue3学起来成本其实也不高核心语法和 Vue2 很接近主要是心智模式要改一下。2.3 为什么坚持用 MyBatis而不是 JPA很多新项目会用 Spring Data JPA但内部管理系统我更推荐 MyBatis。原因不外乎两点SQL 可控、动态 SQL 好用。企业内部系统的查询场景非常多变比如 IP 地址查询可能同时要按网段、按状态、按归属部门过滤还可能要拼接模糊搜索条件。MyBatis 的 XML Mapper 可以随时调整 SQL特殊统计需求也能直接用原生 SQL 写排查问题时直接把 SQL 捞出来执行一遍就知道了。JPA 虽然也能用条件构造器但如果团队对 ORM 的理解不够深遇到复杂查询往往会生成出效率很差的 SQL而且还难调。MyBatis 还有一个对内部项目很有利的特点上手门槛低。Java 开发的同学基本都写过 SQL把 SQL 映射关系理顺就能干活不需要先补一堆 JPA 的底层原理。2.4 MySQL 为什么够用这套系统数据量再膨胀也就是几万条设备记录、几十万条工单记录的量级MySQL 单库完全没问题。MySQL 的好处是部署简单、社区资料多、备份方案成熟企业内部随便找台服务器就能装。要注意的是版本选择。MySQL 5.7 用得最多但官方已经停止维护了新部署建议选 MySQL 8.0。另外字符集一定要设置成utf8mb4否则存 emoji 或者一些生僻字时会报错。后面部署章节我还会说时区和 SSL 的问题这些都是 MySQL 很容易埋坑的地方。3. 系统整体设计从页面到数据库的完整链路3.1 前后端分离下的请求是怎么走的这套系统是标准的前后端分离架构前端 Vue3 运行在浏览器里用户在页面上点击查询Vue 组件调用封装好的 axios 方法向后端发送 HTTP 请求。后端 SpringBoot 通过 Controller 接收请求调用 Service 层处理业务逻辑Service 层再通过 MyBatis 的 Mapper 接口访问 MySQL 数据库。数据一步步返回后前端把 JSON 数据渲染到表格或表单中。开发阶段前端通过 Vite 代理把/api开头的请求转发到后端http://localhost:8080避免跨域问题生产部署阶段可以把 Vue 构建出来的静态文件直接放进 SpringBoot 的static目录也可以单独用 Nginx 托管两者都能做到“同源访问”。大多数企业内部系统都推荐第一种方式简单、少一套维护。3.2 核心功能模块拆解我按实际使用频率给这套系统的功能模块排了个优先级用户与权限管理支持登录、退出、密码修改角色绑定菜单和按钮权限这是所有管理系统的地基。网络设备台账维护交换机、路由器、防火墙、无线 AP 等设备的基础信息包括品牌型号、管理 IP、所在位置、固件版本。IP 地址库管理按网段维护 IP 地址池记录某个地址被哪台设备占用占用状态可以标记为“空闲 / 已占用 / 保留”。工单处理流程员工提交网络报修单管理员派单处理人填写处理结果整个流程在系统里留痕。登录日志与操作日志记录谁在什么时间做了什么事排查问题时非常有用。这套系统不要一开始就把菜单做得太深。默认两侧菜单一级菜单 5 个以内子页面尽量控制在 10 个以内用户不需要培训就能点明白。3.3 数据库表结构设计参考下面是一份简化但够用的表结构设计覆盖了用户、设备、IP 地址和工单四张核心表CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role_id BIGINT, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE net_device ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_name VARCHAR(100) NOT NULL, device_type VARCHAR(50), brand VARCHAR(50), model VARCHAR(100), ip_address VARCHAR(50), location VARCHAR(200), status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE ip_address_pool ( id BIGINT PRIMARY KEY AUTO_INCREMENT, segment VARCHAR(50) NOT NULL, ip_address VARCHAR(50) NOT NULL, status TINYINT DEFAULT 0, device_id BIGINT, remark VARCHAR(200), update_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE net_work_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(30) NOT NULL, title VARCHAR(100), content TEXT, reporter_id BIGINT, handler_id BIGINT, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, handle_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有几个细节可以注意。所有状态字段统一用TINYINT比如设备状态 1 表示在线、0 表示下线、2 表示维修中代码里定义好枚举页面上做转换展示就好。工单号建议用“WO 年月日 当天序号”的格式比如WO20250613001既能识别时间也能在口头沟通时快速定位问题。4. 后端实操从工程搭建到接口落地4.1 环境准备与项目初始化开发环境建议统一成一套JDK 1.8 或 17、Maven 3.8、MySQL 8.0、IDEA。SpringBoot 版本选择 2.7.x对应的 MyBatis starter 用2.3.x系列比较稳。创建工程可以用 Spring Initializr也可以手工搭 Maven 项目。核心依赖就这几个dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里有个小坑要提醒SpringBoot 2.7.x 配套的 MySQL 驱动如果用的是旧的mysql-connector-java连接字符串写法会有差异新驱动推荐使用com.mysql.cj.jdbc.Driver。我一般直接引入mysql-connector-j省得后续版本对不上。4.2 环境配置与多环境切换内部系统的配置相对简单核心是application.ymlserver: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/net_manager?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: yourpassword mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.nms.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case非常关键打开之后数据库的下划线字段就能自动映射到实体类的驼峰属性不用手工写一堆 ResultMap。StdOutImpl是控制台打印 SQL 日志开发阶段必须开后端排查问题全靠它。上线前把log-impl换成Slf4jImpl或者直接去掉。多环境切换建议用application-dev.yml和application-prod.yml通过spring.profiles.active控制。生产环境不要把密码写死在配置文件里用环境变量注入。4.3 实体、Mapper 与 XML 映射实体类按数据库字段设计比如设备表Data public class NetDevice { private Long id; private String deviceName; private String deviceType; private String brand; private String model; private String ipAddress; private String location; private Integer status; private Date createTime; }Mapper 接口Mapper public interface NetDeviceMapper { ListNetDevice selectDeviceList(Param(keyword) String keyword, Param(status) Integer status); NetDevice selectByIp(Param(ip) String ip); int insertDevice(NetDevice device); int updateDevice(NetDevice device); }对应resources/mapper/NetDeviceMapper.xml?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.nms.mapper.NetDeviceMapper select idselectDeviceList resultTypecom.example.nms.entity.NetDevice SELECT id, device_name, device_type, brand, model, ip_address, location, status, create_time FROM net_device where if testkeyword ! null and keyword ! AND (device_name LIKE CONCAT(%, #{keyword}, %) OR ip_address LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC /select select idselectByIp resultTypecom.example.nms.entity.NetDevice SELECT * FROM net_device WHERE ip_address #{ip} /select insert idinsertDevice useGeneratedKeystrue keyPropertyid INSERT INTO net_device(device_name, device_type, brand, model, ip_address, location, status) VALUES(#{deviceName}, #{deviceType}, #{brand}, #{model}, #{ipAddress}, #{location}, #{status}) /insert update idupdateDevice UPDATE net_device SET device_name #{deviceName}, device_type #{deviceType}, brand #{brand}, model #{model}, ip_address #{ipAddress}, location #{location}, status #{status} WHERE id #{id} /update /mapper动态 SQL 是 MyBatis 最实用的部分。where标签会自动去掉多余的AND查询条件有没有值都能正常工作这种写在 XML 里的 SQL 一眼就能看懂调起优来也方便。4.4 登录认证与接口权限内部系统的认证不需要做得太重JWT 方案够用。用户登录成功后后端生成一个包含用户 ID 和角色的 Token 返回给前端前端把 Token 存在本地请求时放到 Header 的Authorization字段里。后端加一个拦截器校验 TokenComponent public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/auth/login)) { return true; } String token request.getHeader(Authorization); if (token null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } return true; } }这里有个经验权限校验不用一开始就上 Spring Security 或者 Sa-Token先把这个轻量拦截器跑起来后面真出现复杂权限需求再平滑迁移。内部系统的用户量少这个方案完全够用而且出了问题排查起来特别直接。当然后续如果想做按钮级权限可以再加一层RequiresPermission(device:add)类似的注解让拦截器根据注解判断用户角色。这是第二步的事第一版先把登录状态守住。4.5 MyBatis 缓存和 TypeHandler 使用建议默认情况下 MyBatis 的一级缓存是开启的作用范围是同一个 SqlSession也就是一次请求内有效。很多人没关注它但它造成的“虚惊一场”我见过不少在一个事务里先查询、再更新、再查询同一条数据结果发现第二次查回来的还是旧值以为是缓存问题其实是机制让一级缓存在一等场景下返回了同一个对象。内部管理系统对缓存的需求不高默认配置就够了。真要提升查询性能优先在数据库层面加索引比如ip_address这种高频查询字段建个普通索引就够了。TypeHandler用得相对少但碰到枚举类型和 JSON 字段时会用到。比如设备类型字段想存成字符串Java 里用枚举接收就需要自定义 TypeHandler。不过对第一版系统我的建议是别为了“优雅”过度设计直接用 String 接收就好简单是最重要的。5. 前端实操Vue3 管理后台从零到能跑5.1 用 Vite 快速初始化 Vue3 项目前端用 Vite 创建项目命令如下npm create vitelatest net-manager-web -- --template vue cd net-manager-web npm install npm install vue-router4 pinia axios element-plus创建完基础项目后需要把 Element Plus 按需引入或全量引入。内部后台系统用户量小我图省事直接全量引入import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router import { createPinia } from pinia const app createApp(App) app.use(ElementPlus) app.use(router) app.use(createPinia()) app.mount(#app)5.2 axios 封装和 Token 处理axios 封装是所有后台页面的基础统一处理请求前缀、Token、错误提示import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default request这里做了个小设计开发环境 Vite 代理/api到localhost:8080生产环境 SpringBoot 也会统一处理/api前缀的请求这样前后端不用频繁改地址。Vite 代理配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }5.3 表格页面和权限控制的落地设备管理页面是典型的后台 CRUD 页面顶部搜索栏 表格 分页 新增/编辑弹窗。用 Vue3 组合式 API 写起来非常清晰script setup import { ref, onMounted } from vue import request from ../api/request const list ref([]) const total ref(0) const queryParams ref({ keyword: , status: null, pageNum: 1, pageSize: 10 }) async function fetchList() { const res await request.get(/device/list, { params: queryParams.value }) list.value res.rows total.value res.total } function handleSearch() { queryParams.value.pageNum 1 fetchList() } onMounted(fetchList) /script页面按钮的权限控制建议用前端指令实现。先在后端登录接口里返回当前用户拥有的权限标识列表前端保存到 Pinia再封装一个v-permission指令。没有权限标识的按钮直接移除元素这种做法的效果比单纯隐藏好因为用户连看到入口的机会都没有。6. 前后端联调与生产部署阶段6.1 开发阶段的跨域问题前后端分离开发时跨域问题几乎一定会遇到。我在本地测试时用 Vite 代理解决而不是在后端加CrossOrigin。原因很简单后端加跨域意味着上线也要带着这些配置生产环境反而容易出问题。如果你确实需要后端允许跨域一定要把允许的来源配到具体域名别用*。6.2 Vue 构建产物整合进 SpringBoot如果不想单独部署前端可以把 Vue 项目构建后的dist目录复制到 SpringBoot 的src/main/resources/static下再配合路由的history模式处理createWebHistory()注意history模式下页面刷新如果走了 SpringBoot 默认 404 路由需要后端配置一个兜底转发把非 API 请求重新指回index.html。很多新同学在这里卡住刷新页面就白屏实际上是没做这个 fallback。我一般用 Nginx 托管前端、后端单独部署的方式因为后续前端更新不用重新打 Java 包。但如果你公司只有一台小服务器前端打进 SpringBoot 是最省事的选择看团队习惯。6.3 部署时的数据库和时区细节生产环境 SQL 执行要注意MySQL 安装时的字符集如果没有明确指定很可能默认是latin1。建议部署的时候执行ALTER DATABASE net_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接 URL 里serverTimezoneAsia/Shanghai必须加上否则 SpringBoot 和 MySQL 的时区不一致日期数据查出来会差 8 小时。useSSLfalse在没有开启 SSL 加密的 MySQL 环境中可以避免启动报错但要确保网络环境可信。allowPublicKeyRetrievaltrue是 MySQL 8 用户常见的坑不配置直接报 “Public Key Retrieval is not allowed”我第一次部署时也踩过。7. 常见问题排查与避坑记录这里把开发这套系统过程中最容易碰到的问题整理成一张速查表现象常见原因处理方式后端启动报时区错误MySQL 时区设置异常连接串加serverTimezoneAsia/Shanghai查询结果字段全为 null没开下划线转驼峰map-underscore-to-camel-case: true前端请求报 404路由 history 刷新未兜底配置 fallback 到index.htmlaxios 请求 401Token 过期或没传拦截器统一处理跳转登录页MyBatis 查询结果多出重复记录多表查询未去重SQL 检查关联条件加DISTINCT登录成功但菜单不显示权限标识没返回完整检查角色菜单关联表数据MySQL 8 连接报公钥错误加密连接未配置白名单连接串添加allowPublicKeyRetrievaltrueSpringBoot 3 项目无法注入 Mapper与 jakarta 包冲突统一用 2.7.x 或调整依赖版本还有一个容易被忽略的坑MyBatis 的 XML 文件如果放在src/main/java目录下Maven 编译时默认不会把它复制到 target 里导致运行报 “Invalid bound statement”。我习惯统一放在src/main/resources/mapper下然后在pom.xml里把 resources 目录显式声明进去省得用 IDE 时一切正常、打包上线就出幺蛾子。另外提醒一句SpringBoot 版本不要追太高。尤其是内部系统稳定压倒一切。新版本的好处往往体现在新特性上但对内部系统来说等生态插件都跟上再升级也不迟。8. 后续还能怎么扩展这套系统跑顺之后扩展空间其实很大。我可以分享几个我实际尝试过的方向第一加一个文件存储服务。设备资料、变更单、网络拓扑图都需要存附件直接在系统里接入 MinIO 这类对象存储组件效果比传数据库好太多。实现方式也不复杂后端封装一个上传接口前端用 El-upload 组件对接。第二把 MySQL 换成主从或者加 Redis 缓存。当系统使用人数超过 500 人、工单数据超过 10 万条的时候列表查询可以接 Redis 缓存热点数据比如部门、IP 地址池这种频繁读不频繁写的内容能明显缓解 MySQL 压力。第三加入简单的告警通知。设备状态异常、工单超时未处理这些事件可以通过企业微信、钉钉或飞书的机器人 webhook 推到群里让运维人员第一时间知道。这个功能实现成本很低但价值感很强。最后说一个我个人的体会。做内部系统技术永远不是最大的门槛真正的门槛是搞清楚“谁在用、用什么痛、怎么才顺手”。这套 SpringBoot Vue3 MyBatis MySQL 的组合足够应对绝大多数企业内部管理场景而且因为前后端分离、职责清晰后续想换成更强大的消息队列、微服务也不是什么难事。但请记住第一版一定要克制先跑起来给同事用再根据反馈迭代这才是企业内部系统最踏实的落地路径。