
接手这个项目时我第一反应是“企业内部的小型网络管理系统”看着门槛不高真正落地后才发现它既要照顾网络管理员的日常操作习惯又要让非技术背景的领导看得懂数据报表还得保证办公室里几十号人同时操作时不卡壳。整套系统基于Java SpringBoot Vue3 MyBatis MySQL搭建采用前后端分离架构前端用Vue3负责交互展示后端用SpringBoot提供接口服务数据层由MyBatis操作MySQL。我在这篇文章里会完整复盘这套系统的需求拆解、表结构设计、后端实现、前端联调以及部署上线的全过程把关键代码、参数配置和踩过的坑都摊开来讲。适合正在做类似企业内部管理系统、或者想用SpringBootVue3练手全栈项目的同学参考。1. 做这套系统的原因与技术选型背后的决策1.1 为什么选择前后端分离架构一开始我其实纠结过企业内部的小系统是不是直接用SpringBoot模板引擎渲染页面更省事。但后来想清楚一个核心问题这个系统的使用者不只是网络管理员还有部门主管、运维值班人员甚至领导层要看统计报表。不同角色的操作路径差别很大如果用服务端渲染每次改交互都要动后端代码发布流程也变得重。前后端分离之后前端工程独立开发、独立部署后端只需要把接口定义好两边可以并行推进效率高不少。实测下来还有两个好处。第一前端静态资源可以交给Nginx托管接口请求反向代理到后端服务遇到大并发时甚至可以单独给静态资源做缓存策略。第二后端接口可以同时服务Web端和后续可能增加的移动端哪怕现在只做了网页版将来要做一个小程序或者钉钉集成接口不用重写。当然前后端分离也带来了跨域、联调成本、打包部署复杂度这些新问题。这些我在后文会一一展开尤其是跨域和前端路由刷新404这两个坑几乎每个做分离项目的同学都会遇到。1.2 SpringBoot MyBatis开发效率与SQL可控的平衡点后端框架选型上我在Spring Data JPA和MyBatis之间犹豫过。这个系统涉及大量多表关联查询和动态条件筛选比如设备列表要根据设备类型、状态、所属部门、入网时间组合筛选还要关联IP资源表和告警记录统计。JPA的自动建表和Repository确实开发快但面对这种复杂查询要么写JPQL要么用Specification拼条件可读性和调优空间都不如直接写SQL。MyBatis最大的吸引力在于SQL完全可控。我可以在XML里写精细的关联查询可以用whereif标签动态拼接过滤条件还能针对慢查询直接调整SQL写法。配合MyBatis的驼峰映射和自定义TypeHandler实体类和数据库字段之间的转换也很省心。还有一点团队里如果有刚入门的新人MyBatis的学习曲线比JPA平缓得多。你只要会写SQL基本就能上手写Mapper。不过要注意MyBatis的二级缓存我是一律关闭的。这个系统数据变更频繁设备上下线、IP分配状态实时变化开二级缓存反而容易出现脏读。默认走一级缓存和数据库查询就够了复杂度没必要往上加。1.3 Vue3带来的前端开发体验提升前端用Vue3搭配Vite和Element Plus刚开始迁移时确实有点不适应但用顺手之后回不去Vue2了。组合式API把设备的逻辑按功能聚合而不是按选项分散。比如设备列表页搜索条件、表格数据、分页状态、加载动画这些逻辑可以放在一个setup函数里组织代码跳转起来非常直观维护一个几百行的页面也不觉得乱。Vue3的响应式系统基于Proxy实现相比Vue2的Object.defineProperty新增属性、数组下标操作都不需要额外处理。这个项目里有个网络拓扑页面需要动态往设备节点数组里添加新的交换机或路由器数据在Vue2里就要用this.$set处理Vue3直接list.push()就能触发视图更新少写很多别扭代码。组件库选Element Plus还有一个原因表格、表单、弹窗、树形控件都很齐全做管理后台基本不用自己造轮子。搭配Vite的热更新改完代码浏览器秒级刷新前后端联调时效率提升非常明显。2. 需求拆解与数据库设计表结构是系统的地基2.1 功能模块清单设备、IP资源、告警、工单、权限在写任何代码之前我先和网络管理团队聊了两轮需求最后把系统拆成了五个核心模块。设备管理是绝对的主角负责登记和管理交换机、路由器、防火墙、服务器、无线AP等网络设备记录设备型号、序列号、固件版本、上线时间、物理位置等信息。IP资源管理负责维护企业内网的IP地址池记录哪些IP已分配、分配给哪台设备、是否保留、是否冲突。告警管理接收来自网络监控平台的告警信息包括链路断开、设备离线、CPU或内存使用率过高、端口异常等需要支持按级别和类型筛选。工单管理用来派发运维任务比如某台设备需要更换配置、某个IP段需要重新规划工单要能关联设备和IP跟踪处理状态。用户与权限管理则区分普通用户、网络管理员、系统管理员三种角色控制谁能查看、谁能编辑、谁能删除。这里有个容易忽略的点企业内部系统虽然规模小但权限模型一定要从第一天就做好。不要觉得“反正就几十个人用不做权限算了”。一旦设备数据录入不规范、误删操作发生再回头补权限体系非常痛苦。我的做法是RBAC模型用户绑定角色角色绑定菜单权限和操作权限后端在接口层面做校验前端根据权限动态渲染按钮。2.2 核心表的字段设计与关系数据库我选了MySQL 8.0字符集统一用utf8mb4排序规则用utf8mb4_general_ci。不要用utf8否则存emoji或者某些生僻字会报错网络设备备注信息里经常有人粘贴特殊符号这个坑我替你们踩过了。设备表net_device是核心表字段如下字段名类型说明idbigint主键自增device_namevarchar(64)设备名称如“核心交换机-3F”device_typetinyint设备类型1交换机、2路由器、3防火墙、4服务器、5无线APbrandvarchar(32)品牌如华为、H3C、Ciscomodelvarchar(64)型号snvarchar(64)序列号带唯一索引ip_idbigint关联ip_resource表主键locationvarchar(128)物理位置如“A栋3楼机房”statustinyint状态1在线、2离线、3维护中、4报废firmware_versionvarchar(32)固件版本purchase_datedate购买日期warranty_expiredate保修到期日remarkvarchar(255)备注create_timedatetime创建时间update_timedatetime更新时间IP资源表ip_resource用来管理IP地址核心字段包括IP地址varchar存字符串方便模糊查询、子网掩码、网关、所属网段、分配状态0空闲、1已分配、2保留、3冲突、关联设备ID、备注。这里我特意没有把IP设计成int类型虽然int存储更省空间但网络管理员习惯直接按IP段搜索varchar配合索引做LIKE 192.168.1.%查询更直观。告警记录表net_alarm记录了告警消息包括设备ID、告警类型1链路断开、2设备离线、3CPU过载、4内存过载、5端口异常、告警级别1提示、2重要、3严重、告警内容、发生时间、处理状态0未处理、1已确认、2已修复。工单表net_work_order则记录了工单编号、标题、描述、关联设备ID、优先级、状态0待处理、1处理中、2已完成、3已关闭、创建人、处理人、创建时间、完成时间。用户表sys_user和角色表sys_role、用户角色关联表sys_user_role组成权限模型。用户表不要存明文密码这个我后面细说。2.3 字段设计的几个关键细节表结构看似简单但有几个细节直接决定了后面开发是否顺手。第一个是统一的时间字段类型。我全部用datetimeJava后端用LocalDateTime接收配合MyBatis的类型处理器不需要额外配置就能正确读写。别混用timestamp和datetime否则不同MySQL版本下行为不一致排查起来很头疼。第二个是逻辑外键。我没有在数据库层面建外键约束只保留逻辑关联字段比如设备表存ip_id。小型系统数据量不大外键约束对性能影响可以忽略但它对删除操作限制太死。比如要删一个IP资源如果数据库有外键必须先删关联设备操作顺序很僵化。逻辑外键配合代码层面的校验灵活性和完整性都能保证。第三个是状态字段全部用tinyint不直接存中文字符串。后续扩展状态值时只改枚举和字典表不用动表结构。前端通过字典接口拉取状态说明展示端和存储端彻底解耦。3. 后端核心实现从Mapper到Controller3.1 项目结构与分层规范后端工程我按功能包名划分没有用传统的按层分包这样每个业务模块的Controller、Service、Mapper放在一起找代码非常方便。com.example.netmanage ├── common // 通用类统一返回、异常处理、常量 ├── config // 配置类MyBatis、CORS、拦截器 ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // MyBatis数据访问层 ├── entity // 实体类 ├── dto // 数据传输对象 └── security // JWT与权限相关实体类放在entity包里dto专门放前端传入的参数对象比如分页查询参数DeviceQueryDTO包含当前页、每页条数、设备名称、设备类型、状态、位置等字段。为什么不直接用实体接收查询参数因为实体类对应表结构字段固定且不包含分页相关属性。如果用Map接收代码又失去可读性。独立DTO让参数结构清晰后续加筛选条件也不影响实体类。Controller只负责参数接收和结果返回不写业务逻辑。所有业务判断放在Service层Mapper只做数据读写。这个分层规范坚持下来项目越往后越轻松。我见过很多小项目把业务逻辑堆在Controller里前期看着省事加功能时各种方法互相调用改一个需求牵扯一片。3.2 统一返回体与全局异常处理前后端分离项目里接口返回格式不统一前端解析起来就是灾难。我的方案是所有接口返回统一的结果对象ResultT结构是code、message、data三个字段。code为200表示成功其他code对应具体错误场景比如401未登录、403无权限、500系统异常。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }配合RestControllerAdvice做全局异常处理业务里只要抛出BizException就能自动转换成对应的Result返回不需要每个Controller都写try-catch。未登录时抛出401异常前端axios拦截器接收到后自动跳转登录页。这里要注意全局异常处理不能把日志吞掉我看过有些项目全局捕获异常后只返回“系统错误”日志里啥也不留线上问题根本没法定位。我习惯在异常处理里用log.error把堆栈打印出来同时返回给前端友好提示。3.3 设备列表动态查询MyBatis XML写法示例设备列表是这个系统最核心的查询场景管理员打开设备管理页需要按条件组合筛选。在Mapper XML里我用where配合if动态拼接条件既避免了where 11这种丑陋写法也能复用一个SQL应对多种查询组合。select idselectDevicePage resultTypecom.example.netmanage.entity.Device SELECT d.id, d.device_name, d.device_type, d.brand, d.model, d.sn, d.location, d.status, d.firmware_version, d.create_time, d.update_time, i.ip_address, i.subnet_mask, i.gateway FROM net_device d LEFT JOIN ip_resource i ON d.ip_id i.id where if testquery.deviceName ! null and query.deviceName ! AND d.device_name LIKE CONCAT(%, #{query.deviceName}, %) /if if testquery.deviceType ! null AND d.device_type #{query.deviceType} /if if testquery.status ! null AND d.status #{query.status} /if if testquery.location ! null and query.location ! AND d.location LIKE CONCAT(%, #{query.location}, %) /if if testquery.ipAddress ! null and query.ipAddress ! AND i.ip_address LIKE CONCAT(%, #{query.ipAddress}, %) /if /where ORDER BY d.create_time DESC /select对应Mapper接口public interface DeviceMapper { ListDevice selectDevicePage(Param(query) DeviceQueryDTO query); long countDevicePage(Param(query) DeviceQueryDTO query); }分页我用了PageHelper插件Controller里直接传页码和每页条数PageHelper会自动拦截SQL生成count查询和分页语句。这里有个使用规范PageHelper的startPage方法必须紧跟查询方法中间不能插入其他数据库操作否则分页会错乱。我用过一次在startPage和selectDevicePage之间加了一句日志打印结果日志里的Mapper查询也被分页拦截了查出来的count完全不对调试了半小时才定位到。这个问题非常隐蔽写代码时一定注意。3.4 JWT登录认证与接口权限控制登录认证我用的是JWT实现方式不复杂用户输入账号密码后端校验通过后用HMAC SHA-256算法生成tokentoken里包含用户ID、用户名、角色编码和过期时间返回给前端。前端把token存在本地存储每次请求在请求头加Authorization: Bearer token后端通过拦截器解析token并设置当前用户上下文。Component public class JwtInterceptor 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 || !token.startsWith(Bearer )) { throw new BizException(401, 未登录或登录已过期); } // 解析token校验签名和过期时间 Claims claims JwtUtil.parseToken(token.substring(7)); if (claims null) { throw new BizException(401, Token无效); } UserContext.set(claims); return true; } }密码存储方面绝对不能用MD5现在GPU暴力破解MD5速度极快。我用的BCrypt加盐哈希每次加密结果不同数据库泄露了也不容易反推原文。Spring Security的BCryptPasswordEncoder可以单独引入使用不一定要引入整个Security框架。小型系统用拦截器做认证配合方法级权限校验注解比引入Security全家桶轻量得多学习成本也低。不过拦截器方式有个天然局限它对静态资源也会拦截部署时前端静态资源和后端接口如果同源需要在配置里排除静态资源路径否则会出现前端页面能打开、接口请求401的怪现象。这个我在联调时就遇到过排查到最后发现是拦截器把静态资源请求都拦了。4. 前端Vue3实现与前后端联调4.1 项目初始化和依赖安装前端工程我用Vite创建选择Vue3 JavaScript模板组件库用Element Plus状态管理用Pinia路由用Vue Router 4。创建命令简单npm create vitelatest netmanage-web -- --template vue然后安装依赖npm install element-plus npm install pinia npm install vue-router4 npm install axios npm install sassElement Plus按需引入可以用官方推荐的unplugin-auto-import和unplugin-vue-components插件这样打包体积能小不少。我实测过全量引入和按需引入一个只有十几个页面的管理后台全量引入打包后的JS文件会大两三MB首屏加载明显变慢。如果团队对打包体积不敏感或者项目后续页面会非常多按需引入是值得一开始就配置好的。登录页到首页的主流程实现中我会在路由守卫里做登录态校验没有token就跳转登录页有token但访问了不存在的路由就跳404页。这里有个细节本地存储token不要放在localStorage里而是sessionStorage关闭浏览器后登录态自动失效企业内部管理系统这样更安全避免公用电脑上别人直接打开浏览器就能进入系统。4.2 设备资料卡片的双列表格展示设备管理页我用Element Plus的el-table组件数据来自后端分页接口。搜索区域用el-form的inline模式一行排设备名称、设备类型、状态三个筛选项右侧放查询和重置按钮。表格列定义注意几点操作列固定在最右侧宽度100px按钮根据权限动态渲染编辑和删除按钮用v-if控制状态列用el-tag展示不同状态不同颜色在线是绿色、离线是红色、维护中是橙色IP列因为关联了IP资源表展示ip_address字段。Vue3组合式API写这个页面的核心逻辑const queryParams reactive({ current: 1, size: 10, deviceName: , deviceType: null, status: null }); const tableData ref([]); const total ref(0); const loading ref(false); const fetchList async () { loading.value true; try { const res await getDevicePage(queryParams); tableData.value res.data.records; total.value res.data.total; } finally { loading.value false; } }; const handleSearch () { queryParams.current 1; fetchList(); }; const handleReset () { queryParams.deviceName ; queryParams.deviceType null; queryParams.status null; handleSearch(); };分页组件用el-pagination通过v-model:current-page和v-model:page-size绑定页码和每页条数current-change和size-change都触发fetchList。这里要特别注意修改每页条数时页码要重置为1否则用户在第5页切换成每页50条时后端拿不到对应页的数据表格会空白。4.3 axios封装与跨域问题处理axios封装是前端工程质量的关键。我单独建了request.js初始化axios实例设置基础URL和超时时间请求拦截器自动加上认证token响应拦截器统一处理业务状态码。import axios from axios; import { ElMessage } from element-plus; import router from /router; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const token sessionStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); request.interceptors.response.use( response { const res response.data; if (res.code 200) { return res; } if (res.code 401) { sessionStorage.removeItem(token); router.push(/login); return Promise.reject(new Error(登录已过期)); } ElMessage.error(res.message || 系统错误); return Promise.reject(new Error(res.message || 系统错误)); }, error { ElMessage.error(error.message || 网络异常); return Promise.reject(error); } );开发环境跨域用Vite的代理配置解决在vite.config.js里加server.proxy把/api开头的请求转发到后端服务的http://localhost:8080。这样前端请求地址保持/api开头后端Controller的RequestMapping也只写业务路径不写/api前缀。生产环境则依赖Nginx转发这个文末会说。4.4 网络拓扑页的动态渲染实现这个页面是整个系统前端实现中最有挑战的部分。需求是要展示核心交换机、汇聚交换机、接入交换机、路由器之间的连接关系支持点击节点查看设备详情。我没有用第三方图形库直接用Vue3的动态组件配合绝对定位实现了一个简化版拓扑。每个设备节点是一个相对定位的容器根据后端返回的坐标数据设置left和top值节点之间用SVG线条连线。后端在设计设备表时预留了topo_x和topo_y字段存储坐标初始部署时管理员手动拖拽调整位置保存后回写数据库。template div classtopo-container reftopoContainer svg classtopo-lines line v-forline in topoLines :keyline.id :x1line.x1 :y1line.y1 :x2line.x2 :y2line.y2 stroke#409EFF stroke-width2 / /svg div v-fornode in topoNodes :keynode.id classtopo-node :style{ left: node.x px, top: node.y px } clickshowDetail(node) span{{ node.name }}/span /div /div /template拖拽实现我直接用了HTML5拖放API没引第三方库。每个节点draggabletrue拖拽结束后把新坐标通过接口保存。这里需要注意SVG线条的坐标要根据节点尺寸计算中心点不然线会从节点左上角穿过视觉上非常别扭。计算中心点时把节点宽度和高度各取一半偏移量算准后线条才贴合。4.5 表单校验的常见细节新增和编辑设备用同一个弹窗组件表单校验规则用Element Plus的rules配置。设备名称必填、IP必填、设备类型必选SN可选但如果有值长度限制64位。IP格式校验我写了自定义校验函数用正则匹配IPv4地址格式。序列号如果重复后端会返回“序列号已存在”的业务错误前端在新增提交时提前调用查询接口做一次去重校验减少用户等待后端报错的挫败感。时间选择器我用的el-date-picker绑定值为Date类型提交给后端前要格式化成yyyy-MM-dd HH:mm:ss字符串。不格式化直接提交axios序列化后Date对象会变成ISO字符串和MySQL的datetime格式对不上插入时就会报错。这个我在联调第一天就中招了后来统一在提交前调用moment做格式化问题解决。5. 部署、测试与常见问题排查实录5.1 本地联调环境的搭建本地开发我建议后端和前端分开启动。后端用IDEA直接运行SpringBoot主类端口8080。前端用npm run dev启动Vite开发服务器端口5173通过代理转发接口请求。MySQL用Navicat或者命令行创建数据库执行初始化SQL脚本。MySQL安装有个细节如果你本机之前装过5.7版本再装8.0时要彻底卸载干净包括删除数据目录和服务注册信息否则服务起不来或者端口冲突。我见过很多人在Windows上折腾半天最后发现是旧版MySQL服务没删干净。连接MySQL的驱动和配置要注意时区问题。MySQL 8.0以后驱动类名是com.mysql.cj.jdbc.Driver连接串必须加serverTimezoneAsia/Shanghai否则会报时区错误。我用的配置如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/netmanage?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: yourpassword servlet: multipart: max-file-size: 10MB max-request-size: 10MB数据库连接池我用的是HikariCPSpringBoot 2.x以后默认就是它不需要额外引入。连接池参数里maximum-pool-size我设成20小型系统足够了minimum-idle设成5避免空闲连接全部释放后请求还要重新建连。connection-timeout设成30000毫秒防止数据库短暂不可用时请求全部卡死。5.2 上线部署的流程与注意事项项目上线我用单机部署方案一台Linux服务器安装JDK 17、MySQL 8.0、Nginx。后端打成可执行jar包用systemd配置成服务开机自启。前端打包后在Nginx配置静态资源服务和接口反向代理。Nginx配置核心代码如下server { listen 80; server_name netmanage.example.com; root /opt/netmanage-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html;这一句至关重要。前端用了Vue Router的history模式用户直接访问/device/list刷新页面时Nginx如果找不到对应文件会返回404加上这行配置后所有路由都回退到index.html由前端路由接管。不用hash模式的原因很简单URL里带#号既不美观有些内部系统对#开头的地址还有特殊解析问题。jar包部署时我用nohup java -jar启动过一次后来改用systemd管理好处是进程崩溃后自动重启还能查看日志journalctl -u netmanage -f。生产环境MySQL的账号尽量单独创建只授予业务所需的增删改查权限不要用root连接应用这是最基本的安全底线。5.3 常见问题排查速查表下面是这个项目开发、联调、部署过程中我遇到过的典型问题整理成速查表遇到类似报错的直接对号入座。问题现象可能原因解决方案接口返回数据中时间为nullJackson无法序列化LocalDateTime引入jackson-datatype-jsr310在application.yml配置全局日期格式前端请求接口报CORS错误开发环境代理配置缺失或后端未开启跨域开发用Vite proxy生产用Nginx同源代理后端设置CorsFilter兜底MyBatis查询结果某字段一直是null数据库下划线字段无法映射驼峰属性mybatis.configuration.map-underscore-to-camel-casetrue列表分页总数不对PageHelper多租户插件冲突或startPage被其他SQL拦截确保startPage紧跟查询方法中间不执行任何Mapper方法前端打包后刷新404路由history模式未配置try_filesNginx location / 加try_files $uri $uri/ /index.html媒体文件或时间类型报错数据库与Java类型不匹配统一使用LocalDateTime对应datetimetinyint对应Integer上传图片大小超限报错SpringBoot默认上传限制1MB配置spring.servlet.multipart.max-file-size服务器时间显示差8小时JVM默认时区不对JVM启动参数加-Duser.timezoneAsia/Shanghai还有一个很容易踩的坑是MyBatis的if test判断里如果参数类型是Integer不要写! 否则会触发类型比较警告极端情况下判断结果不对。字符串字段才需要同时判断null和空字符串数值类型只需要判断null。这个错误很隐蔽因为大多数情况下数值参数传了就有值不传就是null但如果有人传了空字符串Integer的! 会直接报类型转换异常很难排查。5.4 关于MyBatis缓存的补充说明我在前文提到关闭了MyBatis二级缓存这里展开说下原因。MyBatis默认只开启一级缓存即SqlSession级别的缓存。在Spring集成环境下每个请求对应一个SqlSession请求结束SqlSession关闭一级缓存自然清空不会造成跨请求脏读。二级缓存是Mapper级别的多个SqlSession共享存在三个问题缓存和数据库的一致性难保证因为系统里设备状态随时变化一个UPDATE操作后缓存可能还是旧值分布式部署时缓存都在本地节点节点之间数据不一致配置参数稍微写错还容易序列化异常。所以我建表时也刻意保持数据变更的实时性要求设备上下线状态、IP分配状态、告警处理状态这些高频变更数据全部走数据库实时查询。只有设备类型字典、角色权限这类几乎不变的数据才在服务启动时加载到内存Map里做缓存。这样既保证了性能又避免了缓存一致性问题。一些实操后的经验总结这个项目做完我个人最大的体会是企业内部“小系统”的难点从来不在技术而在需求的完整度和对细节的坚持。技术栈SpringBootVue3MyBatisMySQL都是成熟方案网上资料一大堆但真正把设备管理、IP资源、告警工单、权限控制这几个模块串起来需要想清楚每一张表的关系、每一个接口的边界、每一个交互细节的异常处理。我踩过的坑里最有代表性的三个一定要再强调一遍第一表设计阶段就要统一时间类型和字段命名规范否则后期改表结构比写代码还痛苦第二前后端分离项目跨域和路由刷新404这两个问题要在架构设计时就规划好别等部署了再补第三MyBatis的动态SQL是核心优势但if标签的判断细节要严谨数值类型和字符串类型的判断逻辑完全不同。最后分享一个小技巧这种管理系统做完后我建议花半天时间把初始化SQL脚本里预置好常用的字典数据比如设备类型枚举、告警级别的中文说明这样前端展示时可以少写很多if-else直接通过字典接口映射。还有个后续扩展方向这个系统可以接入网络设备SNMP协议自动采集设备在线状态和端口流量替换掉目前手动登记状态的方式。虽然改动量不小但价值提升是肉眼可见的。