毕业后接了不少类似的Java Web项目,像“SpringBoot+Vue 企业资产管理系统管理平台源码”这种标题,一搜一大把,但很多下载下来不是缺文档就是结构乱,真正能跑起来、能讲清楚、能写到论文里的没几个。我自己带过几届学生的毕设,也帮人改过不少课设代码,今天干脆把这类系统的完整实现思路和核心代码从头到尾捋一遍,就当是给准备拿这套东西做毕设、课设,或者想搞懂前后端分离项目怎么落地的人一份参考地图。
这套企业资产管理系统的定位很明确:围绕公司固定资产的“建档-领用-归还-维修-报废”全生命周期做线上化管理,搭配部门、员工、角色权限,替换掉以前那种“一张Excel传三代”的管法。技术栈就是标题里那套经典组合:SpringBoot提供后端接口,Vue做前端页面,MySQL存数据,Java做主力语言。很适合拿来当毕设/课设,因为每个模块的边界清楚、业务场景贴近真实企业、代码量适中,既有料又不至于失控。
我会把整个项目拆成五块来聊:设计思路、数据库建模、后端实现、前端页面、高频坑位排查。每块都会给到可以直接抄的代码和配置,重点是讲清楚每个关键步骤为什么要这么做。
1. 项目整体设计与思路拆解
1.1 为什么选SpringBoot+Vue这套组合
先说选型。这套系统对应的典型开发场景是“一名后端Java开发者要独立完成一个前后端分离的Web项目”,所以选SpringBoot+Vue是当下最合理的选择,没有之一。
SpringBoot解决的是后端工程化成本的问题。它自带内嵌Tomcat,打成一个jar包就能跑,不用再去配置外置Servlet容器;starter机制把MyBatis、MySQL、Redis这些常用组件的配置收敛到几行配置里;配合Spring Security或者JWT拦截器,权限控制也能做得清晰。对做毕设的人来说,最直观的好处是:你不用花时间处理复杂的环境问题,能把精力放在业务代码上。
Vue这边,核心价值是组件化和响应式。以Element UI为主的组件库把表格、表单、弹窗、分页这些后台管理系统的“标配零件”全部封装好了,前端开发量能砍掉一半以上。而且Vue的响应式机制让页面状态管理非常直观——数据变了页面自动更新,和传统JSP+JQuery那种手动操作DOM的方式相比,代码干净得不是一星半点。
MySQL就更不用说了,免费、稳定、资料海量,几乎所有教程和博客都基于它,踩坑的时候一搜就有答案。这套组合也是国内中小型企业内部系统最流行的技术栈之一,做完这个项目,你以后进公司写业务系统会非常适应。
1.2 资产管理系统核心功能模块怎么划分
很多初学者拿到需求就急着建表,这是大忌。企业资产管理系统的需求核心是“管好资产的状态和流向”,所以模块划分必须围绕资产的生命周期展开。
基础数据层:部门管理、员工管理、资产分类。部门和组织架构是资产归属的“容器”,没有这个上层建筑,后面的资产统计就是一笔糊涂账。
资产业务层:资产台账、领用归还、维修管理、报废管理。其中资产台账是绝对核心,其他所有业务都围绕“资产状态变化”在转。台账里每一条资产记录,应该能从界面看到它当前在谁手上、状态如何、经历过哪些流转。
系统管理:用户管理、角色管理、菜单管理、操作日志。这部分对应RBAC权限模型,核心是让不同角色看到不同菜单、拥有不同按钮权限。比如普通员工只能申请领用,资产管理员能审批和建档,系统管理员管账号。
如果有余力,可以再加资产盘点、报表统计。用EasyExcel做批量导入导出,用ECharts画资产分类占比、部门资产统计图,这两个扩展点几乎每个答辩老师都会喜欢,后面细说。
1.3 版本搭配建议:别一上来就追最新
技术选型里最容易被忽视的就是版本。我见过太多人栽在版本坑里,比如下载了SpringBoot 3.x才发现JDK 8不兼容,或者用了Vue 3 + Element Plus后发现网上的旧教程全对不上。
我的建议是,做毕设/课设场景,安稳压倒一切。下面这套版本组合我实测过很多次,兼容性最好,资料也最全:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 1.8 | 稳定到不能再稳定,企业存量项目大量使用 |
| SpringBoot | 2.7.18 | 2.x最后一个版本,自带大量bug修复 |
| MyBatis-Plus | 3.5.3 | 单表CRUD零SQL,条件构造器非常好用 |
| MySQL | 5.7 或 8.0 | 8.0注意驱动类名和时区配置差异 |
| Vue | 2.6/2.7 + Element UI 2.15 | 组件成熟,教程最多 |
| Node.js | 14.x-16.x | 太新的Node执行旧项目可能报OpenSSL错误 |
很多热词里说“springboot版本太高”,核心问题就出在Boot 3.x+JDK 17+Jakarta命名空间迁移上。网上老教程里大量的javax.*包导入,在Boot 3.x下全部编译失败,你还需要同时处理Spring Security 6的配置变化,成本很高。毕设项目没必要给自己增加这种额外负担。
2. 数据库设计与核心业务梳理
2.1 资产台账表怎么设计:一切业务的起点
数据库是整个系统“地基”,地基歪了后面写多少代码都难受。我建议先画一张核心业务流转图——资产从入职建档开始,到部门领用、归还、报修、报废,每一步都涉及哪个表、哪些状态字段,画完再建表。
先给资产台账表asset的建表SQL:
CREATE TABLE `asset` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `asset_code` varchar(64) NOT NULL COMMENT '资产编号', `asset_name` varchar(128) NOT NULL COMMENT '资产名称', `category_id` bigint(20) DEFAULT NULL COMMENT '资产分类ID', `specification` varchar(255) DEFAULT NULL COMMENT '规格型号', `department_id` bigint(20) DEFAULT NULL COMMENT '所在部门ID', `user_id` bigint(20) DEFAULT NULL COMMENT '使用人ID', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1在库 2已领用 3维修中 4已报废', `purchase_date` date DEFAULT NULL COMMENT '购置日期', `price` decimal(10,2) DEFAULT NULL COMMENT '资产原值', `location` varchar(255) DEFAULT NULL COMMENT '存放位置', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_asset_code` (`asset_code`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='资产台账表';几个关键设计决策:
资产编号asset_code必须唯一。企业里每台设备都有唯一标识,比如固定资产标签号。我建议编码规则统一为“类别前缀+日期+流水”,例如PC-20250101-001。在代码里用数据库唯一索引兜底,防止并发重复。
金额字段用decimal(10,2),绝对不要用float或double。浮点数的二进制表示导致精度丢失,到时候报表金额对不上,你查两天都查不出来。
逻辑删除字段deleted必加。资产管理系统的“删除”业务上应该是作废而非物理删除,因为资产台账属于审计留痕数据。MyBatis-Plus 3.x对逻辑删除有内置支持,配置一下全局就生效。
状态字段status不要直接用字符串描述,用int枚举值。这样扩展性好、存储小、索引快。具体值建议写死在代码常量类里,不要散落在业务逻辑中。
2.2 领用、归还、维修、报废:四张业务记录表
资产台账管理的是“当前状态”,但“状态怎么变成这样”的完整轨迹,要靠业务记录表留存。我设计了四张核心业务表,它们有一个共同思路:只做增,不修改业务痕迹。
领用归还表asset_borrow_record:
CREATE TABLE `asset_borrow_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `asset_id` bigint(20) NOT NULL COMMENT '资产ID', `asset_code` varchar(64) DEFAULT NULL COMMENT '资产编号冗余', `user_id` bigint(20) NOT NULL COMMENT '领用人ID', `department_id` bigint(20) DEFAULT NULL COMMENT '领用人部门', `borrow_time` datetime NOT NULL COMMENT '领用时间', `return_time` datetime DEFAULT NULL COMMENT '归还时间', `status` tinyint(4) NOT NULL COMMENT '状态:0借出中 1已归还', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_asset_id` (`asset_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资产领用归还记录';维修表、报废表结构类似,核心公共字段是:关联资产ID、申请人ID、申请时间、审批状态、处理结果、备注。 审批状态用统一枚举:0待审批、1通过、2驳回、3已完成,避免每张表一套状态码。
这里有一个高频易错点:业务表要不要冗余asset_code、department_name这些字段?我的经验是,冗余少量常用名称字段,查询时能少关联好几张表,对分页列表性能提升非常明显。虽然违背严格的三范式,但在报表查询场景下这是常规做法。
2.3 RBAC权限模型:五张表搞定菜单和按钮
企业系统不像个人博客,权限管不好是要出事的。RBAC(基于角色的访问控制)是当前主流的权限模型,核心就一句话:给用户分配角色,给角色配置菜单和权限,用户最终拥有的权限 = 他的所有角色的权限并集。
具体落地五张表:sys_user用户表、sys_role角色表、sys_menu菜单表、sys_user_role用户角色关联表、sys_role_menu角色菜单关联表。菜单表里用menu_type字段区分目录、菜单和按钮:
| menu_type | 说明 | 例子 |
|---|---|---|
| 1 | 目录 | “资产管理”下一级导航 |
| 2 | 菜单 | “资产台账”页面 |
| 3 | 按钮 | “新增资产”、“导出资产”按钮 |
按钮权限是很多课设忽略的点,但却是答辩亮点。后端接口用自定义@PreAuthorize("hasAuthority('asset:add')")注解校验,前端按钮用v-permission自定义指令控制显隐。记住一条铁律:前端隐藏只是用户体验,后端校验才是安全防线。我见过不少项目前端把按钮一藏就以为安全了,实际上接口裸奔,直接调Postman就能绕过,这是不合格的。
3. 后端核心功能实现要点
3.1 项目初始化、分层与统一返回结构
实操层面,后端我用IDEA新建SpringBoot工程,依赖加入这几组坐标:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>工程分层保持经典四层结构:controller接收参数、service写业务逻辑、mapper访问数据库、entity映射表。另外加config放配置类、common放统一返回结果和异常处理、utils放JWT等工具类。这里别看简单,分层清晰的项目答辩时讲解成本能低一半。
接口返回值必须统一结构,否则前后端联调会出现“这个接口返回{code:200, data:{...}},那个接口返回{success:true, rows:[]}”的混乱局面。自定义一个Result类:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }再用一个@RestControllerAdvice全局异常处理器把业务异常和系统异常统一转换,这样Controller层能瘦身,不用每个方法都包try-catch。我是直接自定义了一个BusinessException,业务里哪不对就throw new BusinessException("资产编号已存在"),全局处理器统一捕获并转成Result.error(...)返回,非常干净。
3.2 JWT登录认证与拦截器:会话管理怎么做
传统单体Web应用常用Session记录登录状态,但前后端分离架构下,我更推荐JWT(JSON Web Token)。JWT的无状态特性让后端不用存会话信息,前端把Token放在请求头里带着走,跨域和水平扩展都方便。
登录接口的核心逻辑:
@Override public LoginResult login(LoginDTO dto) { // 1. 根据用户名查询用户 SysUser user = userMapper.selectOne( new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, dto.getUsername()) ); // 2. 校验用户是否存在、密码是否匹配(BCrypt加密) if (user == null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { throw new BusinessException("用户名或密码错误"); } // 3. 查询用户角色和权限标识,放进token List<String> roles = roleMapper.selectRoleCodesByUserId(user.getId()); List<String> perms = menuMapper.selectPermsByUserId(user.getId()); String token = JwtUtil.generateToken(user.getId(), user.getUsername(), roles, perms); // 4. 返回token和用户基本信息 return new LoginResult(token, user); }这里要点:密码一律用BCrypt加密存储,千万别明文存库。BCrypt是自带盐值的哈希算法,同样的密码每次加密结果都不同,安全性比MD5高一个量级。很多教程还在教MD5+盐,过时了。
拦截器侧,我写一个JwtInterceptor,实现HandlerInterceptor接口,在preHandle方法里从请求头Authorization中取Token,解析失败就抛出401异常:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && JwtUtil.validateToken(token)) { Long userId = JwtUtil.getUserId(token); request.setAttribute("userId", userId); return true; } throw new BusinessException("登录状态已失效,请重新登录"); }然后注册拦截器,注意放行登录接口、静态资源和Swagger文档路径,其余全拦:
registry.addInterceptor(jwtInterceptor) .addPathPatterns("/**") .excludePathPatterns("/api/auth/login", "/error");3.3 资产分页查询与状态流转:最核心的业务代码
资产台账列表是所有页面里最常用的,后端代码直接用MyBatis-Plus的Page和LambdaQueryWrapper动态拼条件,省去手写XML的繁琐。按资产名称模糊查询、状态筛选、分类筛选:
public PageResult<AssetVO> page(AssetQuery query) { Page<Asset> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Asset> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(query.getAssetName()), Asset::getAssetName, query.getAssetName()) .eq(query.getStatus() != null, Asset::getStatus, query.getStatus()) .eq(query.getCategoryId() != null, Asset::getCategoryId, query.getCategoryId()) .orderByDesc(Asset::getCreateTime); Page<Asset> result = assetMapper.selectPage(page, wrapper); // 转换成VO,补充部门名称、分类名称等冗余字段 return convertToPageResult(result); }另一个核心场景是领用审批通过后,要同时更新资产状态、插入领用记录、更新使用人,这一串操作必须放在同一事务里:
@Transactional(rollbackFor = Exception.class) public void approveBorrow(Long recordId) { // 1. 查询领用单 BorrowRecord record = borrowRecordMapper.selectById(recordId); if (record == null || record.getStatus() != 0) { throw new BusinessException("领用单状态异常"); } // 2. 校验资产当前可领用 Asset asset = assetMapper.selectById(record.getAssetId()); if (asset == null || asset.getStatus() != 1) { throw new BusinessException("资产不可领用"); } // 3. 更新资产:状态改为已领用,绑定使用人 asset.setStatus(2); asset.setUserId(record.getUserId()); assetMapper.updateById(asset); // 4. 更新领用单:状态为已领用 record.setStatus(1); borrowRecordMapper.updateById(record); // 5. 写入操作日志 logService.record("资产领用", asset.getAssetCode() + " 领用给 " + record.getUserId()); }加@Transactional是最容易被忽略的细节。理论上讲,更新资产状态和插入记录表必须原子执行,只要其中一个失败了整个领用流程就不能算完成,否则会出现“领用单显示成功,资产状态还在库”的数据不一致问题。我给所有涉及多表写入的Service方法都加了事务,覆盖范围宁多勿少。
4. 前端Vue页面与交互实现
4.1 工程搭建与目录规划:API层统一封装
前端我用Vue CLI创建工程,组件库选Element UI。第一步不是写页面,而是先搭好目录骨架和请求封装,否则后续每个页面都会重复写一堆axios代码。
目录结构如下:
src/ api/ # 按模块拆分的接口定义 assets/ # 静态资源 components/ # 公共组件 router/ # 路由配置 store/ # Vuex状态管理 utils/request.js # axios统一封装 views/ # 页面 asset/ # 资产管理 system/ # 系统管理 login.vue # 登录页 layout.vue # 主布局request.js是整个前端请求的中枢。我在这里做了四件事:统一设置baseURL、请求头自动携带Token、响应结果统一解包、401时自动跳回登录页:
import axios from 'axios'; import { Message } from 'element-ui'; import router from '@/router'; 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 => { const res = response.data; if (res.code !== 200) { Message.error(res.message); return Promise.reject(new Error(res.message)); } return res; }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } Message.error('网络请求异常'); return Promise.reject(error); } ); export default request;API层单独拆到src/api/asset.js,每个接口函数只做参数转发:
import request from '@/utils/request'; export function getAssetPage(data) { return request({ url: '/asset/page', method: 'post', data }); } export function addAsset(data) { return request({ url: '/asset/add', method: 'post', data }); } export function updateAsset(data) { return request({ url: '/asset/update', method: 'post', data }); } export function deleteAsset(id) { return request({ url: '/asset/delete/' + id, method: 'post' }); }4.2 路由守卫与登录状态管理
登录状态我用Vuex + localStorage组合:Vuex负责运行时的快速读取,localStorage负责刷新后状态不丢失。store里存token和userInfo,登录成功后commit这两个字段。
路由守卫是防止未登录用户直接输URL跳进系统内部页面的唯一防线:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path === '/login') { next(); return; } if (!token) { next('/login'); return; } next(); });这里我额外做了动态路由。菜单表里配置了每个菜单对应的前端路由组件路径,用户登录后后端返回他能访问的菜单列表,前端router.addRoutes动态添加。这样不同角色登录后,地址栏直接输/asset/manage,如果没有权限,守卫层就会拦截。
4.3 资产台账页面:表格+查询+弹窗的完整写法
资产管理页是典型的主从结构页面:上半部分是搜索栏,中间是表格,底部是分页器。我直接给出最核心的模板部分,这套结构可以复制到其他任何列表页面使用:
<template> <div class="asset-page"> <!-- 搜索栏 --> <el-form :model="queryParams" inline> <el-form-item label="资产名称"> <el-input v-model="queryParams.assetName" placeholder="请输入资产名称" clearable /> </el-form-item> <el-form-item label="资产状态"> <el-select v-model="queryParams.status" placeholder="全部" clearable> <el-option label="在库" :value="1" /> <el-option label="已领用" :value="2" /> <el-option label="维修中" :value="3" /> <el-option label="已报废" :value="4" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery">查询</el-button> <el-button @click="handleReset">重置</el-button> <el-button type="success" icon="el-icon-plus" @click="handleAdd">新增资产</el-button> <el-button type="warning" icon="el-icon-download" @click="handleExport">导出</el-button> </el-form-item> </el-form> <!-- 数据表格 --> <el-table v-loading="loading" :data="assetList" border stripe> <el-table-column prop="assetCode" label="资产编号" min-width="140" /> <el-table-column prop="assetName" label="资产名称" min-width="140" /> <el-table-column prop="categoryName" label="分类" min-width="100" /> <el-table-column prop="departmentName" label="使用部门" min-width="100" /> <el-table-column prop="userName" label="使用人" min-width="90" /> <el-table-column label="状态" min-width="90"> <template slot-scope="scope"> <el-tag :type="statusTagType(scope.row.status)">{{ statusText(scope.row.status) }}</el-tag> </template> </el-table-column> <el-table-column prop="price" label="资产原值" min-width="100" /> <el-table-column prop="purchaseDate" label="购置日期" min-width="110" /> <el-table-column label="操作" min-width="220" fixed="right"> <template slot-scope="scope"> <el-button size="mini" type="primary" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="mini" type="danger" @click="handleDelete(scope.row)">作废</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="queryParams.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total"> </el-pagination> </div> </template>对应的脚本逻辑核心是查列表和提交表单。查询时直接调getAssetPage,把queryParams整个传过去,后端自动拼接条件。新增和编辑共用一个弹窗el-dialog,里面套el-form,编辑时通过this.$nextTick给表单回填数据。这里有一个经验:编辑回填一定要在弹窗打开、表单渲染完成之后再赋值,否则会出现“第一次打开有数据,第二次打开数据残留”的问题。
表单验证用Element UI内置规则,我在el-form-item上加了prop,再在rules里定义校验:
rules: { assetName: [{ required: true, message: '请输入资产名称', trigger: 'blur' }], price: [ { required: true, message: '请输入资产原值', trigger: 'blur' }, { pattern: /^(([1-9]\d*)|0)(\.\d{1,2})?$/, message: '金额格式不正确', trigger: 'blur' } ] }这里说句题外话,前端校验在用户体验层面必须有,但它最大的作用是“提前拦截低级错误”,减轻后端压力。真正服务端的数据校验在后端也必须做一遍,不要相信前端传来的任何值。
5. 常见问题与排查技巧实录
5.1 前后端联调跨域:60%的项目卡在这一步
前后端分离项目第一个拦路虎就是跨域。浏览器同源策略会拦截前端localhost:8080向后端localhost:8081发的请求,表现形式是浏览器控制台报CORS error,或者请求状态变成net::ERR_FAILED。
我推荐在SpringBoot侧直接统一配置CORS,开发时最省事。写一个配置类:
@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在SpringBoot 2.4+版本里才支持,allowedOrigins("*")和allowCredentials(true)同时使用在新版本里会报错。这个问题热搜里的“springboot版本太高”经常触发,因为旧博客都写的是allowedOrigins。
前端这边,Vue CLI开发环境还可以用vue.config.js里的devServer.proxy做代理转发,把/api前缀的请求转发到后端,这样走的是同源路径,也能避开跨域。两种方案我建议都配上,生产环境用Nginx反向代理,开发环境用代理或CORS都可以。
5.2 时间格式化和JSON序列化:LocalDateTime返回一长串数字
用LocalDateTime作为实体字段类型时,没配置JSON序列化的话,接口返回的是类似"2025-01-01T10:00:00"或者一组数组数字,前端直接显示会非常难看。我一般直接在实体字段上加@JsonFormat注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime;也可以做全局配置,在application.yml里设置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这里我踩过一个大坑:一旦字段上加了@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),前端传回来的日期字符串也能自动反序列化为LocalDateTime,但如果前端传的是"2025-01-01"这种纯日期,就必须要额外处理。所以资产表的purchaseDate我直接用LocalDate而不是LocalDateTime,配合Element UI的el-date-picker的value-format="yyyy-MM-dd",整个链路就不会出错。
5.3 Vue打包后刷新404和静态资源路径异常
开发环境跑得欢,npm run build完部署到Nginx,刷新某个二级路由页面直接404,静态资源CSS/JS也加载不出来。这个问题几乎每一位前端新手都会遇到。
404的根因是Vue Router的history模式:路由是前端模拟的,服务器上没有对应的真实文件,刷新时Nginx去找不存在的路径就返回404。解决思路是在Nginx配置里做try_files兜底,把所有路径请求回退到index.html:
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }静态资源路径异常则是因为打包后默认引用绝对路径/js/chunk.js,部署到子目录或直接双击打开就找不到。我一般把vue.config.js里publicPath改成'./':
module.exports = { publicPath: './', outputDir: 'dist', assetsDir: 'static' };改成相对路径后,dist目录可以放在任意子路径下,也方便你直接拖到本地预览。这个配置和热搜里“vue 打包后布局异常”经常是同一个问题——CSS的url引用路径不对导致样式表加载失败,整个页面变裸奔。
5.4 分页总数不准与资源残留:两个隐蔽小问题
分页总数不准最常见的原因是:使用MyBatis-Plus分页插件时,多个查询条件用同一个参数对象导致count语句统计错误。排查思路是先打开SQL日志,看控制台打印的COUNT语句和SELECT语句是否带上了相同的条件,一般都能看出问题。
另外一个隐蔽的坑是“删除资产时没有级联清理业务表”。直接delete from asset where id = 1很简单,但领用记录、维修记录里的asset_id还指向这条数据,后续统计就会查出“死引用”。我建议删除资产一律走逻辑删除(deleted=1),同时保留业务记录。如果一定要物理删除,那必须把关联表的记录一并处理,比如把领用记录的asset_id置为NULL,并在操作日志里留痕。
最后说一个关于学习和扩展的小建议。如果你打算拿这套系统做毕设答辩,我强烈建议增加两个功能点:一个是用EasyExcel实现资产数据的批量导入导出,替代传统的POI手写,代码量少而且更专业;另一个是用ECharts画资产分类占比图和部门资产统计图,视觉冲击力强,答辩时老师一眼就能看到工作量。这两块在整个系统里属于“锦上添花”的部分,但往往决定了你的项目是“能跑”还是“出彩”。我在实际做项目的时候,每次重写这套系统都会把报表模块往前挪一挪,因为数据可视化是真的能让使用者直观感受到系统价值的。
还有一个小技巧,资产编号生成不要用数据库自增ID拼字符串,那样并发下会出现重复,而且格式很丑。正确的做法是写一个AssetCodeGenerator,每天凌晨重置序列号,用DateTimeFormatter拼出PC-20250101-001这样的编号,再结合数据库唯一索引兜底,既好看又不怕并发。喜欢折腾的还可以引入Redis做自增序列,但就毕设体量来说,不用额外引入中间件增加部署负担。
这套SpringBoot+Vue的资产管理系统,从头到尾做一遍,你对前后端分离、权限模型、状态机流转、接口设计、持久层框架的掌握会有一个质的提升。尤其建议自己动手把每个模块敲一遍,别只盯着我帖出来的代码看,亲手写出一个能跑的完整系统,比看十篇博客都管用。