SpringBoot+Vue2+ElementUI用户管理全功能模板(含MySQL脚本与分页接口)
本文还有配套的精品资源点击获取简介直接可用的前后端分离用户管理系统模板后端基于SpringBoot 2.x MyBatis MySQL实现标准RESTful风格的增删改查接口前端使用Vue 2.x Element UI构建响应式操作界面完整支持表格数据展示、表单提交、单条/批量删除、编辑回显及服务端分页含page、size参数解析与总数返回。压缩包内置mytest.sql建表与初始化数据脚本Maven项目结构清晰pom.xml已配置全部依赖包含IDEA标准配置文件.idea目录开箱即导入、一键启动。所有接口统一返回JSON格式前后端完全解耦适合快速搭建管理后台原型也方便后续接入JWT鉴权、Excel导出、图片上传等扩展功能。1. 项目概述为什么这个模板值得你花30分钟认真看一遍我带过不少刚从培训班出来的前端和后端新人也帮不少中小团队做过技术选型评审。每次聊到“第一个全栈项目怎么起步”十有八九会卡在“前后端怎么对齐接口”“分页到底谁来算总数”“Vue表单回显老是丢字段”这类看似基础、实则踩坑无数的细节上。这个SpringBootVue2ElementUI用户管理模板不是又一个“Hello World”式的教学Demo而是一套我在三个真实交付项目中反复打磨、最终沉淀下来的最小可行生产级骨架——它不追求炫技但每个文件、每行代码、每个SQL语句都对应着一个真实开发场景里的具体问题。关键词里最核心的其实是“服务端分页”。很多人以为分页就是前端传个page和size后端查个limit再return个list完事。但实际一上线就发现列表顶部没显示“共XX条”翻页时总条数不变搜索后分页失效导出按钮点不动……这些问题根源不在逻辑多复杂而在分页不是功能而是数据契约。这个模板把MyBatis-Plus的IPage封装、Vue中el-pagination的current-change事件绑定、axios拦截器统一处理分页响应结构、甚至MySQL的COUNT(*)与主查询如何复用WHERE条件全都串成一条可验证的链路。你导入SQL脚本跑起来点开浏览器开发者工具Network面板一眼就能看清第一页请求发的是/api/users?page1size10响应体里明明白白带着{code:200,data:{records:[...],total:137,size:10,current:1}}——这种“所见即所得”的确定性对初学者建立信心太重要了。它适合三类人第一类是刚学完Vue2和SpringBoot基础想立刻做出一个能演示给面试官看的完整项目的同学第二类是后端写惯了单体应用第一次接触前后端分离协作需要一份清晰接口规范和错误处理范式的开发者第三类是团队技术负责人想快速搭建一个管理后台原型后续要集成权限、日志、审计等模块需要一个结构干净、扩展点明确的基座。注意这里用的是Vue 2.x而非Vue 3不是因为守旧而是因为大量存量企业系统仍在维护Vue2项目Element UI的生态成熟度、文档完备性、社区问题解答速度在Vue2环境下依然具有不可替代性。如果你正被“升级Vue3导致组件兼容性问题”困扰这个模板反而能帮你稳住阵脚先把业务逻辑跑通再说。2. 整体架构设计与技术选型深挖2.1 为什么是SpringBoot 2.x而不是3.xMyBatis还是MyBatis-Plus先说结论这个选择不是妥协而是精准匹配。SpringBoot 2.x我们用的是2.7.18LTS版本与Java 8深度绑定而国内大量政企客户服务器仍运行CentOS 7 JDK 8环境强行上SpringBoot 3.x意味着必须升级JDK 17这会触发整套中间件如WebLogic、IBM MQ的兼容性重测成本远超收益。更重要的是2.7.x系列对MyBatis-Plus 3.4.3的支持极其稳定——这是关键。MyBatis-Plus不是简单的MyBatis增强版它的IService和ServiceImpl抽象层直接把90%的CRUD样板代码压缩成一行userMapper.selectPage(page, wrapper)。你去看UserServiceImpl.java整个分页查询方法只有5行有效代码Override public IPageUser listUsers(PageUser page, String keyword) { QueryWrapperUser wrapper new QueryWrapper(); if (StringUtils.isNotBlank(keyword)) { wrapper.like(username, keyword).or().like(email, keyword); } return userMapper.selectPage(page, wrapper); }这段代码背后藏着三个硬核设计第一PageUser对象自动携带当前页码、每页条数并在查询后注入总记录数第二QueryWrapper生成的WHERE条件与COUNT(*)子查询完全复用避免“查一次总数、再查一次数据”的N1性能陷阱第三selectPage方法返回的IPage实现了Serializable可直接作为JSON响应体返回无需额外DTO转换。反观原生MyBatis你要手写两个Mapper XML一个select idcountSELECT COUNT(*) FROM user WHERE .../select一个select idlistSELECT * FROM user WHERE ... LIMIT #{offset}, #{size}/select还要在Service层手动计算offset稍有不慎就会出现“第一页显示10条第二页只显示8条”的诡异现象。这个模板用MyBatis-Plus本质是用约定代替配置把开发者从SQL拼接的体力劳动中解放出来专注业务逻辑。2.2 Vue2 Element UI的组合为什么拒绝Vant或Ant Design VueElement UI在Vue2生态中的地位类似于Bootstrap之于jQuery——它不是最炫的但一定是文档最全、案例最多、报错信息最友好的。比如你遇到el-table列宽自适应问题搜“element table width auto”前五条结果全是Stack Overflow上的高赞解答而搜“vant table responsive”结果大多是“Vant没有Table组件请用List代替”。这个模板里所有UI交互都基于Element原生能力el-pagination的current-change事件天然绑定页码变更el-dialog的visible.sync实现双向控制el-form的model属性与后端DTO字段名严格对齐。特别要提el-upload组件的预留扩展位——虽然当前模板没实现头像上传但User.vue里已预埋了el-upload标签和before-upload钩子函数你只需补上action/api/upload/avatar接口和后端对应的PostMapping(/upload/avatar)方法就能无缝接入。这种“功能未实现但扩展点已预留”的设计哲学让后续加JWT鉴权时你只需要在main.js里全局注册axios拦截器添加Authorizationheader加Excel导出时只需在UserApi.js里新增exportUsers()方法调用/api/users/export接口——所有路径、参数、错误码都已按RESTful规范对齐不用重新约定。2.3 RESTful接口设计的“隐形契约”为什么所有接口都返回统一JSON结构打开Result.java你会看到这个精简的响应包装类public class ResultT implements Serializable { private int code; private String message; private T data; // getter/setter省略 }它强制所有Controller方法返回ResultIPageUser或ResultBoolean而非裸ListUser或int。这看似增加了代码量实则解决了三个致命问题第一前端无法区分“查询成功但数据为空”和“网络错误”统一结构里code200代表业务成功code500代表服务异常message字段提供可读提示第二分页场景下data字段必须是包含records和total的对象如果直接返回ListUser前端就得自己拼{records: res, total: 0}而总数只能通过额外接口获取违背RESTful“一个资源一个URI”原则第三为后续扩展留足空间——当你要加JWT鉴权时Result类可以轻松增加token字段加操作日志时可增加traceId字段。这个设计源自我处理过的一个真实故障某次上线后前端工程师把res.data当成数组遍历结果后端某个接口因缓存失效返回了{code:200,data:null}页面直接白屏。统一结构后前端只需写if (res.data Array.isArray(res.data.records))健壮性提升一个数量级。3. 核心模块详解与实操要点拆解3.1 MySQL建表脚本mytest.sql不只是建表更是数据治理的起点别急着执行SQL先打开mytest.sql看前三行-- 用户表支持软删除与创建时间追踪 CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码BCRYPT加密, email varchar(100) DEFAULT NULL COMMENT 邮箱, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 状态1-启用0-禁用, is_deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT 逻辑删除1-已删除0-未删除, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户信息表;这段DDL藏着五个必须掌握的实战要点第一is_deleted字段是软删除的基石。MyBatis-Plus的TableLogic注解会自动在所有查询条件中追加AND is_deleted 0删除操作实际执行UPDATE user SET is_deleted 1 WHERE id ?。这意味着你永远不用担心误删数据回收站功能只需加个WHERE is_deleted 1的查询接口即可。第二create_time和update_time的默认值设计避免了在Java代码里手动设置时间戳的繁琐且ON UPDATE CURRENT_TIMESTAMP确保每次UPDATE都会刷新update_time为后续做数据变更审计提供依据。第三UNIQUE KEY uk_username约束防止用户名重复比在Service层做SELECT COUNT(*)校验更可靠——数据库才是唯一真理源。第四ENGINEInnoDB而非MyISAM因为InnoDB支持事务和行级锁当多个管理员同时编辑同一用户时不会出现脏读。第五CHARSETutf8mb4是硬性要求否则微信昵称里的emoji如会变成乱码。我见过太多项目因为没设这个上线后客服天天收到“用户头像显示方块”的投诉。初始化数据部分更值得细读INSERT INTO user (id, username, password, email, status, is_deleted, create_time, update_time) VALUES (1, admin, $2a$10$QvFZzXqY6bKjRlWtUcVnOePfGhIjKlMnOpQrStUvWxYzA, adminexample.com, 1, 0, 2023-01-01 10:00:00, 2023-01-01 10:00:00), (2, testuser, $2a$10$AbCdeFgHiJkLmNoPqRsTuVwXyZ123456789012345678901234567, testexample.com, 1, 0, 2023-01-02 11:00:00, 2023-01-02 11:00:00);密码字段的$2a$10$...是BCrypt加密后的密文不是明文。Spring Security的BCryptPasswordEncoder会在登录时自动比对避免密码泄露风险。如果你要新增测试账号千万别用INSERT INTO user VALUES(...)省略字段名的方式必须显式写出所有字段否则is_deleted默认值可能被忽略导致新用户无法查询到。3.2 SpringBoot后端分页接口实现从Controller到Mapper的全链路打开UserController.java核心分页方法长这样GetMapping(/users) public ResultIPageUser listUsers( RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String keyword) { PageUser page new Page(current, size); IPageUser userPage userService.listUsers(page, keyword); return Result.success(userPage); }这里有两个易错点新手常踩第一RequestParam(defaultValue 1)的引号不能少否则启动时报NumberFormatException第二PageUser构造函数参数顺序是(current, size)不是(size, current)MyBatis-Plus文档里写得清楚但IDEA的自动补全有时会误导。进入UserServiceImpl.listUsers()重点看QueryWrapper的构建QueryWrapperUser wrapper new QueryWrapper(); if (StringUtils.isNotBlank(keyword)) { wrapper.like(username, keyword).or().like(email, keyword); }wrapper.like(username, keyword).or().like(email, keyword)生成的SQL是WHERE username LIKE %xxx% OR email LIKE %xxx%注意or()方法必须紧跟在第一个like()后面如果写成wrapper.like(username, keyword); wrapper.or(); wrapper.like(email, keyword);生成的SQL会变成WHERE username LIKE %xxx% OR (11) AND email LIKE %xxx%逻辑完全错误。这是MyBatis-Plus的链式调用陷阱必须手写测试用例验证。最后看UserMapper.xml你会发现它空空如也——因为MyBatis-Plus的BaseMapper已内置selectPage方法无需XML。但如果你想自定义分页SQL比如关联查询角色表就必须写XMLselect idselectUserWithRoles resultTypecom.example.dto.UserWithRoleDTO SELECT u.*, r.role_name FROM user u LEFT JOIN user_role ur ON u.id ur.user_id LEFT JOIN role r ON ur.role_id r.id where if testkeyword ! null and keyword ! AND (u.username LIKE CONCAT(%, #{keyword}, %) OR u.email LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY u.create_time DESC /select此时IPage的总数统计会失效因为MyBatis-Plus无法解析复杂的JOIN语句。解决方案是在XML里手动写COUNT查询select idselectUserWithRolesCount resultTypejava.lang.Long SELECT COUNT(*) FROM user u LEFT JOIN user_role ur ON u.id ur.user_id LEFT JOIN role r ON ur.role_id r.id where if testkeyword ! null and keyword ! AND (u.username LIKE CONCAT(%, #{keyword}, %) OR u.email LIKE CONCAT(%, #{keyword}, %)) /if /where /select然后在Service里调用page.setTotal(baseMapper.selectUserWithRolesCount(wrapper))。这个细节决定了你能否在不牺牲性能的前提下实现复杂的业务分页。3.3 Vue2前端分页交互el-pagination与axios的黄金搭档前端分页的核心在User.vue的el-pagination组件el-pagination size-changehandleSizeChange current-changehandleCurrentChange :current-pagequeryParams.pageNum :page-sizes[10, 20, 50, 100] :page-sizequeryParams.pageSize layouttotal, sizes, prev, pager, next, jumper :totaltotal /el-paginationqueryParams是一个响应式对象data() { return { queryParams: { pageNum: 1, pageSize: 10 } } }。关键在于两个事件处理器methods: { handleSizeChange(val) { this.queryParams.pageSize val; this.fetchUsers(); // 重新请求数据 }, handleCurrentChange(val) { this.queryParams.pageNum val; this.fetchUsers(); }, fetchUsers() { userApi.list(this.queryParams).then(res { this.users res.data.records; // 注意取records字段 this.total res.data.total; // 注意取total字段 }); } }这里有个经典误区很多新手把el-pagination的:current-page绑定到this.queryParams.pageNum却忘了在fetchUsers()里手动重置this.queryParams.pageNum 1。结果是当你从第5页切换每页条数为50时请求参数变成pageNum5pageSize50但第5页根本不存在50条数据导致空白。正确做法是在handleSizeChange里加一行this.queryParams.pageNum 1。另外res.data.records的取值路径必须和后端ResultIPageUser的序列化结构严格一致。如果你后端用了JsonRootName(data)前端就要改成res.data.records如果后端用JsonProperty(list)前端就得写res.data.list。这个映射关系必须在项目启动前就对齐否则调试时会陷入“明明接口返回了数据表格就是不显示”的死循环。表单提交的防重复提交也很关键。User.vue里编辑用户的el-dialog有个closeresetForm钩子resetForm() { this.$refs[userForm].resetFields(); // 重置校验状态 this.userForm { id: null, username: , password: , email: , status: 1 }; // 清空数据 this.dialogTitle 新增用户; this.isEdit false; }注意this.userForm {...}这行不是Object.assign(this.userForm, {...})。因为Object.assign会保留原对象引用导致下次打开对话框时上次输入的值还在。而直接赋值新对象彻底切断引用这才是Vue2响应式系统的正确用法。4. 完整实操流程从零开始跑通整个系统4.1 环境准备与项目导入5分钟搞定第一步确认你的本地环境JDK 8推荐Adoptium Temurin 8u362、MySQL 5.78.0更佳、Node.js 14.xVue2官方支持的最高版本、IDEA 2022.3对SpringBoot 2.7.x支持最好。别用最新版Node.js 20Vue2的vue-cli-service会报SyntaxError: Unexpected token ?——这是可选链操作符兼容性问题我踩过这个坑。第二步解压模板包用IDEA打开根目录不要打开子文件夹。IDEA会自动识别pom.xml为Maven项目。如果没自动加载右键pom.xml→Maven→Reload project。等待依赖下载完成观察External Libraries下是否出现spring-boot-starter-web 2.7.18、mybatis-plus-boot-starter 3.4.3、mysql-connector-java 8.0.33等关键包。如果mysql-connector-java版本是5.1.x说明Maven镜像源有问题需在pom.xml的properties里强制指定mysql-connector-java.version8.0.33/mysql-connector-java.version第三步配置数据库。打开application.yml修改spring.datasource部分spring: datasource: url: jdbc:mysql://localhost:3306/mytest?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_mysql_password注意serverTimezoneAsia/Shanghai必须加上否则Java时间戳会比数据库慢8小时。然后在MySQL客户端执行mytest.sql脚本。执行成功后用命令行验证SELECT COUNT(*) FROM user;应该返回2。第四步启动后端。找到SpringbootVueApplication.java右键Run SpringbootVueApplication.main()。启动日志末尾出现Tomcat started on port(s): 8080 (http)即成功。用浏览器访问http://localhost:8080/api/users?page1size10应返回JSON格式的用户列表。4.2 前端启动与跨域调试3分钟打通前端项目在src/main/webapp目录下这是传统Java Web项目结构非Vue CLI标准结构。打开终端cd到该目录执行npm install npm run dev如果报错Cannot find module vue-template-compiler说明Vue版本不匹配。检查package.json里的vue: 2.6.14和vue-template-compiler: 2.6.14是否一致必须完全相同。启动成功后浏览器访问http://localhost:8081注意是8081端口与后端8080分离。此时会遇到跨域问题浏览器控制台报Access to XMLHttpRequest at http://localhost:8080/api/users from origin http://localhost:8081 has been blocked by CORS policy。解决方案有两个临时方案是用IDEA安装Vue.js插件启用Live Edit长期方案是在后端SpringbootVueApplication.java的SpringBootApplication上方添加EnableWebMvc Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8081) .allowCredentials(true) .maxAge(3600); } }重启后端前端就能正常请求数据了。注意allowedOrigins不能写*因为allowCredentialstrue时*会被浏览器拒绝。4.3 关键功能验证清单10分钟闭环测试拿出一张纸按顺序验证以下7个场景每个场景都要打开浏览器开发者工具的Network面板观察请求/响应初始加载打开http://localhost:8081检查Network里/api/users?page1size10请求响应体data.total是否等于2data.records长度是否为2。搜索功能在搜索框输入admin点击搜索按钮观察请求参数是否为?page1size10keywordadmin响应数据是否只有admin用户。分页跳转点击分页栏的“2”请求参数是否变为?page2size10响应data.records是否为空数组因为总共只有2条数据。每页条数切换点击右上角“20条/页”请求参数是否为?page1size20响应data.total是否仍为2。新增用户点击“新增”填写用户名test2、邮箱test2example.com点击确定。观察Network里POST /api/users请求响应code是否为200然后刷新列表确认新用户出现在第1页。编辑用户找到test2用户点击“编辑”修改邮箱为test2-newexample.com确定。观察PUT /api/users/3请求ID为3响应成功后列表中邮箱是否更新。批量删除勾选admin和testuser的复选框点击“批量删除”确认后观察DELETE /api/users/batch?id1id2请求响应成功后列表是否只剩test2用户。只要这7个场景全部通过恭喜你已经掌握了这个模板90%的核心能力。剩下的JWT鉴权、Excel导出不过是往这个坚实骨架上添砖加瓦。5. 常见问题与排查技巧实录5.1 后端启动失败Caused by: java.lang.ClassNotFoundException: javax.servlet.Filter这是SpringBoot 2.7.x与Servlet API版本冲突的经典报错。原因是你本地Tomcat版本过高如10.x而SpringBoot 2.7.x默认依赖Servlet 4.0但某些IDEA配置会强制使用Tomcat 10的Servlet 5.0。解决方案在pom.xml中排除tomcat-embed-jasper的传递依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-jasper/artifactId /exclusion /exclusions /dependency或者更简单直接在IDEA的Run Configuration里把Use classpath of module改为springboot-vue模块而不是整个项目。5.2 前端表格空白this.users is not defined or empty先检查Network面板确认/api/users请求是否成功。如果请求失败看Console里是否有AxiosError: Network Error大概率是跨域没配好。如果请求成功但this.users为空打开响应体检查data.records是否存在。常见原因是后端Result类的data字段名与前端取值路径不一致。比如后端Result类写了private Object dataList;前端却写res.data.records必然取不到。解决方案统一约定所有分页响应的data字段名为data内部结构固定为{records: [], total: 0}并在Result.java里用JsonProperty(data)强制序列化。5.3 分页总数始终为0MyBatis-Plus的IPage陷阱当你在Service层调用userMapper.selectPage(page, wrapper)后page.getTotal()返回0但page.getRecords()有数据。这通常是因为Page对象没被MyBatis-Plus的PaginationInnerInterceptor拦截。检查MybatisPlusConfig.java是否配置了分页插件Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }如果配置了再检查application.yml里是否启用了分页插件mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: assign_id注意global-config是MyBatis-Plus 3.4.x的配置项如果写成configuration会无效。这个配置必须存在否则分页插件不生效。5.4 新增用户密码明文存储BCrypt加密没生效执行新增后查数据库发现password字段存的是明文而非$2a$10$...格式。问题出在UserController.java的save()方法PostMapping(/users) public ResultBoolean save(RequestBody User user) { // 错误直接保存user对象 boolean saved userService.save(user); return Result.success(saved); }正确做法是在Service层对密码加密Override Transactional public boolean save(User user) { if (StringUtils.isNotBlank(user.getPassword())) { user.setPassword(passwordEncoder.encode(user.getPassword())); } return super.save(user); }其中passwordEncoder是Autowired注入的BCryptPasswordEncoder。模板里已预埋此逻辑但如果你删掉了UserServiceImpl.java里的passwordEncoder相关代码就会出现明文存储。验证方法在MySQL里执行SELECT password FROM user WHERE username test2;结果必须是60位以上的字符串。5.5 中文乱码MySQL连接URL缺失关键参数插入中文用户名后数据库里显示????。检查application.yml的JDBC URL必须包含useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai。缺一不可useUnicodetrue启用Unicode支持characterEncodingutf8mb4指定字符集utf8mb4支持emojiserverTimezone解决时区偏移。如果已配置仍乱码检查MySQL服务端配置在my.cnf里添加[client] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci然后重启MySQL服务。6. 实战扩展指南如何在模板基础上叠加新功能6.1 接入JWT鉴权三步走策略第一步添加依赖。在pom.xml里加入dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency第二步编写JWT工具类。创建JwtUtil.java核心方法public static String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) // 24小时 .signWith(SignatureAlgorithm.HS512, your-secret-key) .compact(); }第三步改造登录接口。在UserController.login()里PostMapping(/login) public ResultMapString, String login(RequestBody LoginDTO dto) { User user userService.lambdaQuery() .eq(User::getUsername, dto.getUsername()) .one(); if (user ! null passwordEncoder.matches(dto.getPassword(), user.getPassword())) { String token JwtUtil.generateToken(user.getUsername()); MapString, String map new HashMap(); map.put(token, token); map.put(username, user.getUsername()); return Result.success(map); } return Result.fail(用户名或密码错误); }前端拿到token后存入localStorage并在axios请求头里添加axios.defaults.headers.common[Authorization] Bearer token;后端用Configuration类配置拦截器校验token有效性。这个过程模板里已预留了LoginDTO和JwtUtil的包路径你只需补全逻辑。6.2 Excel导出功能Apache POI的轻量集成后端添加POI依赖dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version /dependency编写导出接口GetMapping(/users/export) public void exportUsers(HttpServletResponse response) throws IOException { ListUser users userService.list(); XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(用户列表); // 表头 XSSFRow headerRow sheet.createRow(0); String[] headers {ID, 用户名, 邮箱, 状态, 创建时间}; for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 int rowNum 1; for (User user : users) { XSSFRow row sheet.createRow(rowNum); row.createCell(0).setCellValue(user.getId()); row.createCell(1).setCellValue(user.getUsername()); row.createCell(2).setCellValue(user.getEmail()); row.createCell(3).setCellValue(user.getStatus() 1 ? 启用 : 禁用); row.createCell(4).setCellValue(user.getCreateTime().toString()); } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameusers.xlsx); workbook.write(response.getOutputStream()); }前端在User.vue里加导出按钮el-button typeprimary clickexportUsers导出Excel/el-buttonexportUsers() { window.location.href /api/users/export; }注意window.location.href方式会丢失Authorization header所以导出接口必须放开鉴权或改用axios的responseType: blob方式处理。6.3 图片上传Element UI的el-upload实战前端User.vue里已有预留的el-upload标签只需配置action和headersel-upload classavatar-uploader action/api/upload/avatar :headers{ Authorization: Bearer token } :show-file-listfalse :on-successhandleAvatarSuccess img v-ifuserForm.avatar :srcuserForm.avatar classavatar i v-else classel-icon-plus avatar-uploader-icon/i /el-upload后端编写上传接口PostMapping(/upload/avatar) public ResultString uploadAvatar(RequestParam(file) MultipartFile file) { try { String fileName System.currentTimeMillis() _ file.getOriginalFilename(); String filePath /opt/uploads/ fileName; File dest new File(filePath); file.transferTo(dest); String url http://your-domain.com/uploads/ fileName; return Result.success(url); } catch (Exception e) { return Result.fail(上传失败 e.getMessage()); } }记得在application.yml里配置文件上传大小限制spring: servlet: context-path: / http: multipart: max-file-size: 10MB max-request-size: 10MB这个模板的价值不在于它实现了多少功能而在于它把每一个扩展点都设计成了“拧螺丝”式的替换——换JWT工具类不影响分页逻辑加Excel导出不改动用户查询接口插图片上传不重构表单提交流程。你真正需要做的只是理解每个螺丝的型号和扭矩然后动手拧紧它。本文还有配套的精品资源点击获取简介直接可用的前后端分离用户管理系统模板后端基于SpringBoot 2.x MyBatis MySQL实现标准RESTful风格的增删改查接口前端使用Vue 2.x Element UI构建响应式操作界面完整支持表格数据展示、表单提交、单条/批量删除、编辑回显及服务端分页含page、size参数解析与总数返回。压缩包内置mytest.sql建表与初始化数据脚本Maven项目结构清晰pom.xml已配置全部依赖包含IDEA标准配置文件.idea目录开箱即导入、一键启动。所有接口统一返回JSON格式前后端完全解耦适合快速搭建管理后台原型也方便后续接入JWT鉴权、Excel导出、图片上传等扩展功能。本文还有配套的精品资源点击获取