基于SpringBoot+Vue的企业资产管理系统全栈开发实战解析

基于SpringBoot+Vue的企业资产管理系统全栈开发实战解析 1. 项目概述与整体设计1.1 为什么需要一套独立的企业资产管理系统搞过企业内部系统开发的人应该都有体会资产盘点这件事看起来简单实际上特别磨人。公司一两百台设备分散在各个部门谁领用了、什么时候归还的、设备是不是已经报废了全靠一张Excel表来回传数据稍微多起来就彻底失控。我之前就经历过一次年中盘点行政同事抱着打印出来的几十页表格挨个部门核对设备编号光是核对显示器序列号就花了整整两天最后还发现有三台笔记本的领用记录根本对不上。从那会儿起我就打定主意必须做一套资产管理系统把设备从采购入库、领用登记、归还入库到报废注销的全流程管起来。这个项目采用前后端分离架构后端用SpringBoot MyBatis MySQL提供RESTful API前端用Vue配合Element UI搭建管理界面。它的核心价值在于把资产的“一生”数字化每一台设备从进入公司到退出公司经历了哪些状态变化、经手人是谁、现在存放在哪里全部通过系统记录可追溯。对于有类似资产管理需求的中小企业、创业团队或者正在学习SpringBoot和Vue全家桶的开发者来说这个项目是非常完整的全栈实战案例覆盖了权限控制、数据CRUD、文件上传、Excel导出等企业级开发中最高频的功能点。1.2 系统功能模块与角色权限设计在动手写代码之前我花了比较大的精力理顺需求和角色权限模型。资产管理系统的用户不是只有一个管理员而是分成三类角色角色核心诉求主要权限范围系统管理员管理一切包括用户、角色、资产分类、系统参数全部功能资产管理员负责资产日常操作入库、领用登记、维修记录资产模块全部功能无用户管理权限普通员工查看名下资产、发起领用申请、查看申请进度仅个人相关数据的查看和申请这样的角色拆分其实是从真实业务反推出来的。如果只做一个“超级管理员”包打天下系统做出来只能演示真正落地用起来第一天就会被行政和IT部门同时吐槽。我见过不少初学者自己做管理系统时只建了一张user表所有登录用户权限一样这样的设计在企业内部是站不住脚的——各部门之间数据隔离是刚需比如财务部门的设备明细、资产价值数据本就不该对所有员工开放。基于这个考量我用RBAC基于角色的访问控制模型来做权限体系共三张核心表用户表、角色表、用户-角色关联表。菜单权限和接口权限则在后端通过拦截器统一校验。1.3 技术选型与架构思路选定技术栈时我没有刻意追新而是选择了目前企业里覆盖面最广、招聘需求里出现频率最高的那一套SpringBoot MyBatis MySQL Vue 2 Element UI。这套组合在企业级管理系统里非常主流网上资料多、轮子全、踩坑经验也丰富遇到问题基本都能搜到解决方案。SpringBoot负责提供接口服务它内置Tomcat打成一个jar包就能跑部署成本低。MyBatis负责数据库交互直接用XML文件写SQL资产管理系统里有大量多表关联查询比如查询资产详情时需要同时带出分类名称、所属部门、当前使用人姓名这种场景用MyBatis的join查询非常清楚。MySQL负责数据持久化Vue负责前端页面渲染。至于前后端分离架构的选择主要是为了之后的维护和扩展。前端的构建产物是一堆静态文件部署到Nginx后端只是单纯的API服务。将来如果要做移动端直接复用同一套后端接口就行App端和Web端共用一套业务逻辑省去重复开发。2. 数据库设计与核心表结构2.1 数据库E-R关系与建库策略数据库是管理系统的地基我反复调整了好几版才最终定稿。资产管理系统的核心是资产信息表但光有这张表远远不够还需要分类表、部门表、用户表、领用记录表、维修记录表、报废记录表等配套支撑。我在设计时的几个原则第一避免全表数据冗余。比如资产表里不直接存部门名称而是存部门ID部门名称通过关联查询得到。虽然查询时多了一次join但部门更名时只需要改部门表一处资产表完全不用动。第二状态字段用tinyint而非字符串。资产的存放状态、领用状态都用数字表示0代表闲置、1代表使用中、2代表维修中、3代表已报废。一方面节省存储空间另一方面在代码里用常亮定义状态值避免中英文混乱导致数据不一致。第三所有核心表都带创建时间和更新时间字段。这个习惯帮我解决了很多次数据排查的麻烦尤其是排查“是谁什么时候改了这条数据”的纠纷时时间戳就是铁证。数据库使用UTF-8字符集排序规则使用utf8mb4_general_ci。需要注意如果资产信息里可能录入生僻汉字或表情符号就得用utf8mb4否则会报Incorrect string value的编码错误。这个坑我踩过资产编号里录入了一个特殊符号直接导致写入失败。2.2 核心数据表字段设计详解资产分类表asset_category字段名类型说明idbigint主键自增namevarchar(64)分类名称如笔记本、显示器、打印机parent_idbigint父分类ID树形结构一级分类为0remarkvarchar(255)备注分类表我做成树形结构支持二级分类。比如一级分类“办公设备”下面对应“台式机”“笔记本”等二级分类。这样资产报表统计时既能看大类汇总又能看细分项。部门表sys_dept字段包括id、dept_name、parent_id同样做成树形。根部门是公司总部的概念下面挂载各个业务部门。部门的树形结构在用户归属和资产归属查询上都会用得到。用户表sys_user字段名类型说明idbigint主键usernamevarchar(64)登录用户名唯一索引passwordvarchar(128)密码加密存储nicknamevarchar(64)姓名/昵称dept_idbigint所属部门IDstatustinyint1正常 0禁用用户表和角色表通过中间表sys_user_role关联。密码加密使用BCryptPasswordEncoder这是Spring Security自带的加密器每次加密结果不同但校验可通过比MD5加盐更安全。资产信息表asset_info这是全系统最重要的一张表字段名类型说明idbigint主键asset_novarchar(64)资产编号公司内部唯一标识asset_namevarchar(128)资产名称category_idbigint分类IDdept_idbigint当前存放部门IDuser_idbigint当前使用人ID闲置为NULLstatustinyint状态0闲置 1使用中 2维修中 3已报废pricedecimal(10,2)资产原值purchase_datedate采购日期warranty_enddate保修截止日期remarkvarchar(255)备注资产编号我采用“Z-2024-0001”这种格式Z表示资产Zenpossession固定资产2024代表年份后面四位是序列号。生成逻辑在后端代码里使用日期加自增序列拼接防止并发重复。2.3 领用与归还记录的流水设计资产领用不是一个简单地把userId改掉的过程必须留痕。我单独建了一张asset_use_record表记录每一次资产状态变化的始末。资产使用记录表asset_use_record字段名类型说明idbigint主键asset_idbigint资产IDuser_idbigint领用人IDdept_idbigint领用部门IDuse_typetinyint1领用 2归还 3转移use_timedatetime操作时间remarkvarchar(255)备注这种流水表的设计思路是“只增不改”。每次资产发生转移就新增一条记录查询名下资产时按user_id和最新状态过滤查询资产全生命周期时则按asset_id排序展示所有记录。企业做固定资产审计时会特别看重这份履历它清清楚楚地记录了资产任何一个时间点由谁负责。维修记录表和报废记录表的结构类似都是围绕资产ID记录每次维修的原因、费用、维修结果以及报废的审批流程和时间点。这些记录后期接给财务系统做固定资产折旧计算也很有价值。3. 后端核心实现与关键代码解析3.1 SpringBoot项目搭建与MyBatis集成搭建SpringBoot项目我习惯直接从Spring Initializr生成基础骨架。关键依赖添加了spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok、spring-boot-starter-validation再加一个jjwt用于生成和解析JWT令牌。有个小坑要提醒MyBatis官方提供的starter版本要和SpringBoot版本匹配。如果你用的是SpringBoot 3.x需要引入mybatis-spring-boot-starter 3.0以上的版本否则启动时会报ClassNotFoundException。我在最初用SpringBoot 3.2搭配MyBatis 2.3.x就遇到过这个坑最后把MyBatis升到3.0.3才解决。数据库连接信息放在application.yml中server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/asset_management?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.asset.entity configuration: map-underscore-to-camel-case: true cache-enabled: falsemap-underscore-to-camel-case这个配置一定要打开不然数据库字段user_id映射不到实体类的userId上所有查询结果都会变成null排查起来相当困扰。cache-enabled设为false是因为资产数据实时性要求高加上二级缓存后容易出现脏读而且我这套系统并发量不大缓存收益远小于数据一致性风险。3.2 JWT登录认证与token拦截器前端调用接口时需要携带登录凭证。我用JWT做无状态认证流程是用户输入用户名密码后端校验通过后生成一个JWT字符串返回给前端前端每次请求在请求头中携带Authorization: Bearer {token}后端拦截器解析token得到用户ID和角色信息放行或拒绝请求JWT工具类核心代码如下public class JwtUtil { private static final String SECRET_KEY your-secret-key-please-change-in-production; private static final long EXPIRE_TIME 7 * 24 * 60 * 60 * 1000L; public static String createToken(Long userId, String username, String role) { return Jwts.builder() .claim(userId, userId) .claim(username, username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }拦截器里做统一校验。这里有个细节放行登录接口和静态资源拦截其他所有接口。如果角色是管理员但访问的是用户管理接口后端还要做细粒度权限校验。我在代码中简单实现了一个RequirePermission注解标注在Controller方法上配合拦截器里的权限判断逻辑可以做到“方法级权限控制”。3.3 资产入库与领用接口的完整实现资产管理系统的核心业务都围绕资产的状态变更展开我挑两个典型接口说明。资产入库接口前端传参资产名称、分类ID、采购日期、价格、数量。后端逻辑是public ApiResult addAsset(AssetDTO dto) { for (int i 0; i dto.getCount(); i) { AssetInfo asset new AssetInfo(); BeanUtils.copyProperties(dto, asset); asset.setAssetNo(generateAssetNo(dto.getCategoryId())); asset.setStatus(0); asset.setCreateTime(new Date()); assetMapper.insert(asset); } return ApiResult.success(); }count大于1时循环插入批量入库场景很常见比如一次性采购20台相同型号的显示器不需要前端提交20次一次提交数量即可。generateAssetNo方法根据当前年份和数据库当天已生成的编号数生成连续编号避免重复。事务控制至关重要。入库是多条数据的写入如果第10台插入失败前9台已经写入了数据就不完整。所以方法上必须加Transactional(rollbackFor Exception.class)保证全部成功或全部回滚。资产领用接口领用操作比较麻烦要同时更新两张表Transactional(rollbackFor Exception.class) public ApiResult useAsset(Long assetId, Long userId, Long deptId, String remark) { AssetInfo asset assetMapper.selectById(assetId); if (asset null) { return ApiResult.error(资产不存在); } if (asset.getStatus() ! 0) { return ApiResult.error(资产当前状态不可领用); } asset.setUserId(userId); asset.setDeptId(deptId); asset.setStatus(1); assetMapper.updateById(asset); UseRecord record new UseRecord(); record.setAssetId(assetId); record.setUserId(userId); record.setDeptId(deptId); record.setUseType(1); record.setUseTime(new Date()); record.setRemark(remark); useRecordMapper.insert(record); return ApiResult.success(); }这里用了一个系统设计中的“乐观锁”思维先查资产当前状态只有闲置状态的资产才能被领用避免资产被重复分配给两个人。如果没有这一步两个员工同时点“领用”同一台闲置设备后提交的人就会覆盖前一个人的记录。虽然这套系统目前是单机部署但代码层面的防御性编程习惯还是要养成。3.4 MyBatis映射文件与动态SQL实战MyBatis最爽的就是写动态SQL。资产列表查询需要支持多条件筛选按资产编号模糊查询、按分类ID查询、按部门ID查询、按状态查询还有分页。这些条件拼在一起用XML的where标签写起来非常清爽select idselectAssetList resultTypecom.example.asset.entity.AssetInfo SELECT a.*, c.name AS categoryName, d.dept_name AS deptName, u.nickname AS userName FROM asset_info a LEFT JOIN asset_category c ON a.category_id c.id LEFT JOIN sys_dept d ON a.dept_id d.id LEFT JOIN sys_user u ON a.user_id u.id where if testassetNo ! null and assetNo ! AND a.asset_no LIKE CONCAT(%, #{assetNo}, %) /if if testcategoryId ! null AND a.category_id #{categoryId} /if if testdeptId ! null AND a.dept_id #{deptId} /if if teststatus ! null AND a.status #{status} /if /where ORDER BY a.create_time DESC /selectwhere标签会自动处理第一个条件前面的AND这个特性特别实用至少帮我少写十几个if拼接分支。三张表用的是LEFT JOIN保证资产信息不会因为分类删除或用户删除而查不到。使用MyBatis还有一个容易踩坑的地方如果参数是单个数字类型直接比较会报错。比如方法入参是Integer status在if标签里写status ! null可能没问题但如果你在SQL里写AND a.status #{status}且参数确实传入了一个Integer一切正常但如果传的是String类型的0MySQL和MyBatis的隐式转换会出奇怪的问题。最稳妥的方案是在DTO里把状态字段定义为Integer前端传参时通过JSON反序列化成数字。4. Vue前端搭建与页面功能实现4.1 Vue项目创建与基础环境配置前端使用Vue 2 Vue Router Vuex Axios Element UI这套组合。创建项目我习惯用Vue CLIvue create asset-web创建过程中选择Manually select features勾选Router、Vuex、CSS Pre-processors。Element UI通过npm安装npm install element-ui npm install axios有个比较关键的环境问题Node.js版本和Vue CLI版本要匹配。Node 18以上的版本配合Vue CLI 5.0比较稳定如果你用的是老版本的Vue CLI 4.x配Node 18构建时大概率会报OpenSSL的错误。解决方案是升级Vue CLI或者在package.json的scripts里加上一句serve: set NODE_OPTIONS--openssl-legacy-provider vue-cli-service serveWindows下用setLinux/Mac下用export这个坑在做Vue项目时非常常见多半原因都是Node升级后和旧版webpack产生兼容性问题。4.2 前端路由设计与权限控制路由设计上核心页面包括登录页、首页dashboard、资产管理列表、资产入库页、领用/归还页、系统管理用户管理、部门管理、分类管理。路由分为公开路由和需要登录的路由。我在vue-router中配置了全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); } else if (!token) { next(/login); } else { next(); } });这里只是做了最简单的登录状态判断。更精细的菜单权限控制我是从后端接口返回该用户的菜单列表前端遍历生成路由。这种方式的好处是权限模型高度集中在前端路由角色变更后刷新菜单即可不需要改前端代码。4.3 Axios请求封装与token拦截处理axios封装可以说是前后端分离项目中最容易出问题的一环。很多新手直接每个页面里调axios遇到token过期、接口报错就在每个页面各写一套处理逻辑代码重复度极高。我统一封装了一个request.jsimport axios from axios; import { Message } from element-ui; import router from ../router; const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }); // 请求拦截器自动带上token service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }, error { return Promise.reject(error); }); // 响应拦截器统一处理错误 service.interceptors.response.use(response { const res response.data; if (res.code 200) { return res; } else { Message.error(res.msg || 请求失败); return Promise.reject(new Error(res.msg)); } }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/login); Message.error(登录已过期请重新登录); } else { Message.error(网络异常请稍后重试); } return Promise.reject(error); }); export default service;这段代码解决了几类问题一是所有请求自动携带token业务代码不用每个接口手动设置请求头二是响应状态码和业务状态码分离处理后端统一返回{code: 200, data: xxx, msg: success}格式前端一旦收到非200状态码就直接弹提示页面代码减负不少三是401统一拦截跳转登录页用户登录过期后的体验是顺畅的不会停留在页面上反复报错。4.4 资产管理页面的核心交互实现资产管理列表页是使用频率最高的页面我设计成顶部筛选条件区域 表格列表区域 分页。筛选条件包括资产编号、资产名称、分类下拉、状态下拉、部门树选择。表格列展示资产编号、名称、分类、当前归属、状态、价格、采购日期。右侧操作按钮根据状态显示不同操作闲置状态显示“领用”使用中显示“归还”维修中显示“完成维修”。这里前端状态展示我用了tag标签el-tag :typestatusType(row.status){{ statusText(row.status) }}/el-tagstatusText(status) { const map { 0: 闲置, 1: 使用中, 2: 维修中, 3: 已报废 }; return map[status] || 未知; }element-ui的el-dialog弹窗用于领用表单表单里有使用人选择器、部门选择器、备注输入框。提交按钮触发调用API成功后刷新列表。整个过程交互流程顺畅不需要跳转新页面用户体验比传统JSP时代好太多了。Vue组件化的好处在处理这类场景时体现得特别明显资产表单、领用表单、维修表单都可以抽成独立组件页面之间复用算是我最满意的前端架构决策。5. 前后端联调与生产部署全过程5.1 跨域问题解析与解决方案前后端分离架构下跨域是绕不开的话题。开发环境前端运行在localhost:8081后端运行在localhost:8080两个端口不同浏览器会判定跨域。解决跨域我采用后端CORS方案在SpringBoot里写一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns用通配符在开发阶段很方便但生产环境最好指定具体的域名否则安全性堪忧。allowCredentials(true)表示允许携带cookie但需要注意一旦开启allowCredentialsallowedOrigins就不能用简单的*必须用allowedOriginPatterns。开发阶段还可以用Vue CLI的proxy代理来规避跨域。在vue.config.js里配置devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } }这样前端请求/api/login时会被代理转发到后端8080端口的/login接口浏览器的角度看是同源的就不存在跨域。生产环境一般由Nginx统一处理后面部署环节再说。5.2 后端打包与Linux环境部署后端部署我没有用Docker而是用了最传统的java -jar方式因为大多数中小公司还没有完整的容器化平台传统方式兼容性最好。首先在pom.xml中指定打包方式build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build执行mvn clean package在target目录下生成asset-system-0.0.1-SNAPSHOT.jar。然后上传到Linux服务器执行nohup java -jar asset-system.jar --spring.profiles.activeprod asset.log 21 这里用了nohup让程序在后台运行日志重定向到asset.log。--spring.profiles.activeprod指定生产环境配置生产和开发环境的数据库连接、端口都分开配置避免改代码。Linux服务器上安装MySQL 8.0我走过一些弯路。最省心的方式是使用官方yum源安装wget https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm rpm -ivh mysql80-community-release-el7-3.noarch.rpm yum install mysql-community-server安装后启动服务设置root密码。注意MySQL 8.0的认证插件默认是caching_sha2_password而一些老版本的JDBC驱动不支持这个认证方式。连接不上时检查一下驱动版本mysql-connector-java 8.0.x完全兼容。数据库初始化时用source命令导入assets.sql建表脚本。5.3 Nginx部署前端静态资源与反向代理前端构建执行npm run build生成dist文件夹。将dist上传到服务器任意位置比如/opt/asset-web。然后配置Nginx。我先安装了nginx再在/etc/nginx/conf.d/下创建asset.confserver { listen 80; server_name your-domain.com; location / { root /opt/asset-web; index index.html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files这行非常关键。Vue使用路由时如果用户直接刷新 /assets/1 这个URLNginx会尝试查找对应的静态文件找不到就回退到index.html再由Vue Router接管路由并渲染对应页面。没有这个配置刷新页面就是404。/api开头的请求反向代理到后端8080端口前端baseURL在生产环境配置为/api彻底解决了跨域问题。前端构建时baseURL建议通过环境变量区分开发环境用/api并走devServer代理生产环境也用/api走Nginx代理两套环境代码不用做任何修改。5.4 服务器防火墙与进程管理注意事项部署上线后我遇到过两个比较容易忽略的问题。第一个是Linux防火墙没放行80端口和8080端口从外网访问不到服务。检查方式systemctl status firewalld firewall-cmd --list-ports firewall-cmd --add-port80/tcp --permanent firewall-cmd --add-port8080/tcp --permanent firewall-cmd --reload第二个是8080端口被占用。如果之前启动过旧版本的后端忘了停掉进程新的jar包因为端口冲突启动失败。习惯用下面的命令查找和清理netstat -tlnp | grep 8080 kill -9 PID生产环境其实更推荐用systemd管理后端进程。我在/lib/systemd/system/asset.service下创建了service文件配置了启动命令、日志路径、自动重启策略这样进程意外退出后会自动拉起比裸的nohup可靠很多。6. 常见问题与排查技巧实录6.1 数据库安装与配置高频问题我整理了几个自己遇到过的典型问题做成表格方便对照排查问题现象排查方向解决方案MySQL安装后无法启动检查错误日志/var/log/mysql/error.log多为my.cnf配置问题或数据目录权限不对chown -R mysql:mysql /var/lib/mysqlroot用户远程连接不上确认root的host允许范围和认证插件create user asset% identified by password单独创建业务账号数据乱码检查库表字符集和JDBC连接串统一使用utf8mb4连接URL加characterEncodingutf8mb4排序异常检查排序字段的类型和collation字符串排序用utf8mb4_general_ci数字排序注意CAST类型转换其中最坑的是MySQL 8.0的默认认证插件问题如果JDBC驱动版本太老会直接报Public Key Retrieval is not allowed解决方法是驱动升级到8.0.x或者在连接串加allowPublicKeyRetrievaltrue。6.2 SpringBoot与MyBatis调试技巧总结开发阶段我推荐在application.yml中开启SQL日志打印logging: level: com.example.asset.mapper: debug这样控制台会打印出每条SQL语句及参数排查SQL语法错误和参数类型不匹配的问题非常高效。等上线后再把日志级别调整回info避免生产环境刷屏。MyBatis还有一个容易被新手忽略的点XML文件中的SQL不能以分号结尾。我在一次多表关联查询时习惯性地在SQL末尾加了分号结果MyBatis报语法错误排查了半小时才发现是分号的问题。注意MyBatis的SQL解析有自己的语法规则和直接在Navicat里执行SQL是两回事。另外在MyBatis里写集合参数时一定要写item别名foreach循环foreach collectionids itemid open( separator, close) #{id} /foreach如果不写itemMyBatis会报There is no getter for property named item的错误非常容易踩。6.3 Vue前端构建与运行常见问题汇总Vue项目的问题主要集中在这几个方向npm install卡死或报错。国内网络环境下npm源不稳定配置为淘宝镜像可以解决npm config set registry https://registry.npmmirror.comVue打包后布局异常。这种情况通常是publicPath配置问题。默认publicPath是/如果你的前端不是部署在域名根路径下而是部署在子路径资源就会全部404或样式错乱。解决方式是在vue.config.js里设置module.exports { publicPath: process.env.NODE_ENV production ? / : / };如果你的部署路径是子目录改成对应的子目录路径即可。路由模式的选择。我使用的是HTML5 History模式URL美观但没有hash符号。部署到Nginx必须配置try_files否则刷新页面404。如果嫌麻烦也可以退回hash模式但URL会多一个#号见仁见智。Element UI按需引入与全量引入的选择。小项目用全量引入最省事打包体积大了也就多几百KB。大项目建议用babel-plugin-component按需引入体积优化明显。我这个项目用了全量引入主要考虑到开发效率和演示需求。6.4 Token失效与权限拦截的边界场景JWT的过期时间我设置的是7天时间到了token自动失效。前端在发起请求时如果收到401响应拦截器会清除本地token并跳转到登录页。这个逻辑对于绝大多数场景是够用的。但有一个边界情况需要处理如果用户在操作过程中token刚好失效他正在填写的表单数据会随着跳转登录页而丢失。体验不算好但优先级不高我暂时没做token无感刷新。如果你对体验要求高可以在后端增加refresh_token机制用短期的access_token 长期的refresh_token组合实现静默续期。另外JWT最关键的安全隐患是密钥泄露。我的SECRET_KEY长度较短只适合学习和内部系统使用生产环境建议用RSA公私钥对来签名或者至少用一个复杂的长随机字符串并配置在环境变量中不要硬编码在代码仓库里。7. 系统优化方向与二次开发建议7.1 从“能用”到“好用”的升级路径当前版本的系统已经能实现资产全生命周期管理但距离商用产品还有差距。我个人认为下一步可以按这几个维度继续扩展资产台账Excel导入导出是刚需。行政人员手里有大量历史数据不具备手工逐条录入的条件。可以用EasyExcel封装一个导入接口前端上传Excel文件后端解析并校验后批量写入。导出方面资产列表一键导出Excel报表是领导最爱提的需求这个做起来成本不高但收益显著。资产二维码标签也是一个很实用的迭代方向。每台资产打印一个专属二维码粘贴在设备上员工扫码就能看到资产信息并申请领用。这个功能在固定资产盘点的场景里能大大提升效率。后端只需增加一个根据资产编号查询详情的通用接口前端做扫码跳转页面即可实现。通知提醒机制是让系统真正“活”起来的关键。保修期快到的资产需要提前提醒管理员领用审批通过后需要告知申请人。短信通知成本偏高但邮件通知几乎零成本SpringBoot集成JavaMailSender就能实现。7.2 基于这套架构扩展新业务的思路很多学完后想拿这个项目做毕业设计或者面试作品的朋友最大的困惑是不知道往哪个方向扩展。其实这套前后端分离架构的通用性极强换一套业务表就是另一个管理系统。比如改成会议室预约系统核心表换成会议室表预约记录表业务逻辑变成时间冲突校验比资产管理系统的状态机还简单。改成图书管理系统核心表换成图书表借阅记录表和资产领用逻辑基本相同。改成任务派发系统核心表换成任务表执行记录表角色模型调整为管理员-执行人两级即可。架构不变业务表变化这就是这套项目的价值所在。面试时如果能说清楚资产领用和归还的状态流转逻辑、说清楚为什么用流水表记录而不直接覆盖字段、说清楚JWT认证的前后端配合方式面试官基本能判断出你是真的动手做过的人。7.3 代码层面的质量改进空间如果想把代码质量再提升一个档次有几个具体的优化方向。第一个是统一异常处理。现在很多Controller里只有简单的try-catch建议使用RestControllerAdvice做一个全局异常处理器业务异常通过自定义ApiException抛出由全局处理器统一转换为标准响应结构。这样Controller代码非常干净业务逻辑和异常处理逻辑分离。第二个是增加参数校验。现在资产入库的接口中字段范围校验主要靠前端后端几乎裸奔。引入spring-boot-starter-validation在DTO上用NotNull、NotBlank、DecimalMin等注解标记约束Controller参数加Validated非法请求在后端被直接拦截数据安全性显著提升。第三个是SQL性能优化意识。目前系统在几千条数据量级上运行毫无压力但如果资产数据量增长到十万条资产列表页的分页查询性能需要关注。资产表的asset_no字段可以建唯一索引category_id和dept_id可以建普通索引。MySQL的索引不是越多越好但查询频率高的字段做索引收益很明显。8. 实操经验总结与踩坑心得做这个项目前前后后花了大约一个月的业余时间从需求分析、数据库建表、后端接口开发、前端页面编写到最终部署上线走完了一整个完整项目流程。积累了不少心得体会挑最核心的几条分享给正在做类似项目的朋友。数据库设计永远要往前想一步。每次改动表结构都意味着后端实体类、Mapper XML、前端表单全部跟着变。我在第一版设计时没有建部门表直接在用户表里放了dept_name字段后来部门改名时所有关联数据都要更新痛苦不堪。宁可前期多花半天设计表结构也别后期花一周改表数据。写接口时先定返回格式再写业务代码。我自定义了ApiResult这个统一的返回体包含code、msg、data三个字段。前端的axios拦截器就是基于这个约定做的统一响应处理。如果每个接口返回格式都不一样前端根本没法做统一拦截代码会散落各处判断response的结构维护成本直线上升。学好MyBatis的动态SQL多表关联查询会轻松很多。资产列表这个页面如果不用MyBatis而是用JPA动态条件组合查询没有XML写起来灵活如果用JDBC手写SQL字符串拼接条件本身就是灾难。MyBatis处于两者之间动态SQL的if、where、foreach标签实际上解决了90%的企业级查询需求。部署过程中印象最深的一个坑是后端打包成功后本地明明测试一切正常上传到Linux服务器就是连不上MySQL。折腾了一晚上发现是安全组策略的问题——云服务器的安全组规则没有放行3306端口。这类环境层面的问题光看应用日志是不行的要从网络连通性开始排查。最后说一句实在话这类管理系统的技术门槛并不高真正的区别在于有没有深入理解业务细节表单要设计得合理状态流转要严谨权限控制要清晰。只有把这些问题在自己的项目中想透了、做透了再去看市面上任何一套业务系统你都能快速理解它的架构和逻辑。这也是我建议所有Java开发学习者花点时间独立完成一个全栈管理项目的最底层原因。