简介:本资源为基于SpringBoot的固定资产管理系统毕业设计全套资料,面向计算机相关专业需要完成课程设计或毕业论文的本科生,以及希望学习企业级Java Web开发实践的开发者。系统围绕资产分类、资产审批、个人资产与用户管理等核心业务展开,涵盖从需求分析、数据库设计到功能模块实现与系统测试的完整开发流程。压缩包共245个文件,约36.6MB,包含44个Java源文件与44个class编译文件、30个HTML页面、20个JS脚本及12个CSS样式,另有SQL建库脚本、XML配置、图片素材与一份docx论文文档,结构清晰便于按模块查阅。目前已有517人学习下载。读者可获得论文与项目源码配套的完整方案,借助源码理解SpringBoot分层架构、权限控制与审批流程的实现思路,并参考论文中的需求分析、数据库表设计与测试用例,快速完成自己的毕设或二次开发。
1. 固定资产管理系统为什么总在盘点时翻车:从一张Excel台账说起
每到季度末,行政把一份 Excel 台账发到群里,各部门填完回传,财务再手工汇总。这套流程在资产少于两百件时还能凑合,一旦超过五百件,问题就集中爆发:同一台笔记本在三个表里出现、报废设备还在计提折旧、盘点差异查不出是谁改的。固定资产管理系统要解决的核心就是这件事——把资产从采购入库、领用、调拨、维修到报废的全生命周期放进一个可追溯的数据模型里,让账、物、人三者对得上。
用 SpringBoot 做这类系统是当前最常见的落地路径:它把权限、事务、ORM、接口文档这些重复劳动收敛成约定,开发者只需要专注资产台账、折旧规则、盘点流程这些业务本身。这篇笔记面向两类人:一是要交付一套能跑起来的固定资产管理系统的开发者,二是需要把设计过程写成论文的在校同学。我会按「数据模型怎么定 → 后端怎么搭 → 前端怎么接 → 坑在哪 → 怎么验证」的顺序,把可复现的步骤和参数讲清楚,代码和表结构都能直接抄。
2. 资产台账的数据模型:先定表结构再写代码
固定资产管理系统的成败,八成取决于数据模型。很多人一上来就写 Controller,结果做到折旧计算时发现表里没有「使用部门变更历史」,只能回头改表,连带改接口、改前端,返工成本极高。所以第一步是把实体和关系想清楚。
2.1 五张核心表撑起全生命周期
资产域的最小可用模型是五张表:资产主表、分类表、部门表、用户表、变更记录表。资产主表存当前状态,变更记录表存历史轨迹,两者配合才能回答「这台设备什么时候从研发部转到市场部」这类问题。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| asset | id, asset_code, name, category_id, dept_id, user_id, status, purchase_date, price, salvage_rate, useful_life | 资产主表,status 用枚举:0 闲置 1 在用 2 维修 3 报废 |
| asset_category | id, name, parent_id, depreciation_years | 分类表,支持两级分类,折旧年限挂在分类上 |
| sys_dept | id, name, parent_id | 部门表,树形结构 |
| sys_user | id, username, password, dept_id, role | 用户表,role 区分管理员和普通员工 |
| asset_record | id, asset_id, type, from_dept, to_dept, from_user, to_user, operator, create_time | 变更记录,type 区分领用/调拨/维修/报废 |
asset_code 建议用「分类前缀 + 年月 + 四位流水」生成,比如IT2025060001,比纯自增 ID 更容易人工核对。salvage_rate 是残值率,一般设 5%,useful_life 从分类表继承但允许单件覆盖,因为同是电脑,服务器和笔记本的折旧年限不一样。
2.2 折旧计算:直线法的最小实现
折旧是固定资产系统里唯一带数学的部分,也是最容易算错的部分。国内企业普遍用直线法(年限平均法),公式是:月折旧额 =(原值 − 残值)/(使用年限 × 12)。残值 = 原值 × 残值率。
// DepreciationService.java public BigDecimal monthlyDepreciation(BigDecimal price, BigDecimal salvageRate, int usefulLife) { // 残值 = 原值 * 残值率,保留两位 BigDecimal salvage = price.multiply(salvageRate).setScale(2, RoundingMode.HALF_UP); // 应折旧总额 BigDecimal depreciable = price.subtract(salvage); // 月折旧额 = 应折旧总额 / (年限 * 12),保留两位 return depreciable.divide(BigDecimal.valueOf(usefulLife * 12L), 2, RoundingMode.HALF_UP); }这段代码有三个参数要盯住:salvageRate 传 0.05 而不是 5,usefulLife 传整数年。RoundingMode 必须显式指定,否则 BigDecimal 除法遇到除不尽会直接抛 ArithmeticException,这是新手最常见的翻车点。另外折旧要按月计提,建议用定时任务在每月最后一天跑,把结果写进一张 depreciation_detail 表,而不是每次查询时实时算——实时算在资产上千后会有明显性能问题,而且历史月份的折旧额会随参数改动而变化,审计上说不通。
2.3 状态流转用状态机约束,别用 if-else 堆
资产状态不是随便改的:闲置才能领用,在用才能调拨或报修,维修完回到在用,只有管理员能报废。如果把这些规则写成散落各处的 if-else,三个月后没人敢改。常见做法是用一个状态流转表或枚举里的 allowedTransitions 来约束。
public enum AssetStatus { IDLE(0), IN_USE(1), REPAIRING(2), SCRAPPED(3); private final int code; AssetStatus(int code) { this.code = code; } public int getCode() { return code; } // 定义每个状态允许的下一步 public static boolean canTransfer(int from, int to) { return switch (from) { case 0 -> to == 1; // 闲置 -> 在用 case 1 -> to == 2 || to == 0; // 在用 -> 维修 / 退回闲置 case 2 -> to == 1 || to == 3; // 维修 -> 在用 / 报废 default -> false; // 报废是终态 }; } }把流转规则集中在一处,Service 层只调用canTransfer判断,前端也能复用同一套规则做按钮置灰。这样加一个新状态(比如「借出」)时只改一个地方,不会漏。
3. SpringBoot 后端搭建:从建工程到接口跑通
数据模型定了,接下来把后端骨架搭起来。这一章给的是能直接复现的步骤,包括依赖、配置、分层和两个核心接口。
3.1 用 Spring Initializr 建工程与依赖选择
建工程最省事的方式是 Spring Initializr,选 Maven、Java 17、Spring Boot 3.x。依赖勾选:Spring Web、MyBatis-Plus(或 Spring Data JPA)、MySQL Driver、Lombok、Validation。如果团队习惯 JPA,把 MyBatis-Plus 换成 Spring Data JPA 即可,资产这种 CRUD 密集的场景两者都够用,MyBatis-Plus 在复杂查询和分页上更顺手。
<!-- pom.xml 关键依赖 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>版本号不要盲目追新。Spring Boot 3.x 要求 Java 17 起步,如果学校机房或公司服务器还锁在 JDK 8,就老老实实用 Spring Boot 2.7.x,否则启动直接报 UnsupportedClassVersionError。这是「springboot版本太高」这个热搜词背后最真实的痛点。
3.2 application.yml 里必须改的四个配置
配置文件里默认值大多能用,但有四处不改一定出问题。
spring: datasource: url: jdbc:mysql://localhost:3306/asset_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 server: port: 8080serverTimezone 必须显式指定,否则插入时间会差 8 小时,盘点记录的时间戳全乱。map-underscore-to-camel-case 打开后,数据库的 asset_code 能自动映射到 Java 的 assetCode,省掉大量 @Results 注解。逻辑删除字段 deleted 配上后,删除资产只是标记,历史记录还能查到,这对审计很重要。
3.3 分层结构与资产新增接口
标准分层是 controller / service / mapper / entity / dto。Controller 只做参数校验和响应包装,业务逻辑放 Service,数据库操作放 Mapper。下面是一个资产新增接口的完整链路。
// AssetController.java @RestController @RequestMapping("/api/asset") @RequiredArgsConstructor public class AssetController { private final AssetService assetService; @PostMapping public Result<Long> create(@RequestBody @Valid AssetCreateDTO dto) { return Result.ok(assetService.create(dto)); } } // AssetServiceImpl.java @Service @RequiredArgsConstructor public class AssetServiceImpl implements AssetService { private final AssetMapper assetMapper; @Override @Transactional(rollbackFor = Exception.class) public Long create(AssetCreateDTO dto) { // 资产编码唯一性校验 if (assetMapper.exists(new LambdaQueryWrapper<Asset>() .eq(Asset::getAssetCode, dto.getAssetCode()))) { throw new BizException("资产编码已存在"); } Asset asset = new Asset(); BeanUtils.copyProperties(dto, asset); asset.setStatus(AssetStatus.IDLE.getCode()); assetMapper.insert(asset); return asset.getId(); } }@Transactional 的 rollbackFor 要写 Exception.class,默认只回滚 RuntimeException,业务里抛受检异常时事务不生效是高频坑。资产编码唯一性校验放在 Service 而不是数据库唯一索引,是为了给出友好提示;但数据库上仍要加唯一索引兜底,防止并发下两个请求同时通过校验。
3.4 盘点接口与分页查询
盘点本质是「按条件筛出资产 → 逐条核对状态 → 生成差异记录」。分页用 MyBatis-Plus 的 Page 对象,前端传 pageNum 和 pageSize。
@Override public Page<AssetVO> page(AssetQuery query) { Page<Asset> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Asset> wrapper = new LambdaQueryWrapper<Asset>() .eq(query.getDeptId() != null, Asset::getDeptId, query.getDeptId()) .eq(query.getStatus() != null, Asset::getStatus, query.getStatus()) .like(StringUtils.hasText(query.getKeyword()), Asset::getName, query.getKeyword()) .orderByDesc(Asset::getCreateTime); return assetMapper.selectPage(page, wrapper).convert(this::toVO); }条件构造里的第一个布尔参数是「是否拼接该条件」,传 null 时自动跳过,这样一套 wrapper 能覆盖所有筛选组合,不用写一堆 if。orderByDesc 按创建时间倒序,保证新资产排前面。分页查询记得给 dept_id 和 status 建索引,资产过万后不带索引的筛选会明显变慢。
4. 前端对接与论文里的系统设计章节怎么写
后端接口通了,前端和论文是两条并行的收尾线。前端负责把数据变成可操作的界面,论文负责把设计决策讲成有逻辑的文档。
4.1 Vue 前端对接的三个关键点
前端用 Vue3 + Element Plus 是当前主流组合。对接时三个点最容易出问题:请求封装、跨域、打包部署。
// request.js 统一封装 axios import axios from 'axios' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE, // 开发环境指向 http://localhost:8080 timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = `Bearer ${token}` return config }) service.interceptors.response.use( res => res.data, err => { ElMessage.error(err.response?.data?.msg || '请求失败'); return Promise.reject(err) } ) export default servicebaseURL 用环境变量区分开发和生产,开发时在 vite.config.js 里配 proxy 把 /api 转发到 8080,避免跨域。生产环境打包后,把 dist 目录放进 SpringBoot 的 src/main/resources/static 下,或者用 Nginx 单独托管,两种方式都常见。放进 static 的好处是前后端一个包部署,缺点是每次改前端都要重新打后端包。
4.2 论文系统设计章节的骨架
论文里的系统设计章节不是把代码贴一遍,而是讲清楚「为什么这么设计」。建议按这个骨架组织:需求分析(功能性需求 + 非功能性需求)→ 总体架构(分层图 + 技术选型理由)→ 数据库设计(E-R 图 + 表结构说明)→ 核心模块详细设计(资产全生命周期、折旧计算、盘点流程)→ 界面设计(关键页面截图 + 交互说明)。
技术选型理由要写出对比,比如「选用 SpringBoot 而非 SSM,是因为自动配置减少了 XML 配置量,内嵌 Tomcat 简化了部署」。数据库设计部分把第 2 章的表结构整理成规范的三线表,字段类型、约束、说明写全。核心模块用流程图或时序图描述调用链,比贴代码更符合论文的表达习惯。折旧计算这类有算法的部分,把公式和边界条件写清楚,是论文的加分项。
4.3 源码组织与交付
交付源码时目录结构要清晰,README 里写清环境要求、启动步骤、默认账号。常见结构是后端一个模块,前端一个目录,数据库脚本单独放 sql 文件夹。数据库脚本要包含建表语句和几条初始化数据,别人拿到后能一键导入跑起来。如果要把项目发给别人,打包时排除 target、node_modules、.idea 这些目录,只留源码和脚本,压缩包体积能小一个数量级。
5. 固定资产管理系统落地避坑:五条血泪记录
这一章是我做这类系统时踩过的坑,每条按「现象 → 原因 → 解决」写,都是能直接对号入座的。
5.1 盘点时资产状态和记录对不上
现象:盘点后发现某台设备显示「在用」,但变更记录里最后一次是「报废」。原因:报废操作只改了 asset 表的 status,忘了往 asset_record 插记录,或者插记录和改状态不在同一个事务里,中途失败只成功了一半。解决:把状态变更和记录插入放进同一个 @Transactional 方法,并且状态变更统一走一个 changeStatus 方法,禁止在别处直接 update status 字段。
5.2 折旧金额每次查询都在变
现象:同一个资产,今天查月折旧额是 158.33,改了个无关参数后变成 158.34。原因:折旧是实时计算的,且中间步骤的舍入方式不统一,有的地方保留两位有的地方保留四位。解决:折旧在资产入库时就算好并落库,后续只读不重算;如果必须实时算,把舍入规则固定在一处,所有中间结果统一保留四位,最终结果保留两位。
5.3 并发领用导致同一资产被两个人占用
现象:两个管理员同时给同一台闲置设备办领用,结果两条领用记录都成功,资产归属混乱。原因:先查状态再更新,两步之间没有锁。解决:用乐观锁,asset 表加 version 字段,更新时带where id = ? and version = ?,影响行数为 0 就说明被别人改过,抛异常让用户重试。或者直接在更新语句里带状态条件:update asset set status=1, user_id=? where id=? and status=0,靠数据库的行锁保证原子性。
5.4 时间字段差 8 小时
现象:新增资产后,列表里显示的创建时间比实际早 8 小时。原因:数据库连接串没配 serverTimezone,或者 Jackson 序列化没配时区,两者只要有一个不对就会偏。解决:连接串加serverTimezone=Asia/Shanghai,application.yml 里 jackson 配time-zone: GMT+8,两处都配齐。如果用的是 LocalDateTime,还要确认 MySQL 字段类型是 datetime 而不是 timestamp。
5.5 打包后前端页面 404
现象:本地开发一切正常,打成 jar 包部署后访问首页 404,接口却能通。原因:前端打包产物没放进 static 目录,或者放了但 SpringBoot 的静态资源映射被自定义配置覆盖了。解决:确认 dist 里的文件在src/main/resources/static下,且没有自定义 WebMvcConfigurer 把默认映射顶掉。如果用了前端路由的 history 模式,还要加一个 fallback 配置,把非 api 开头的请求都转发到 index.html。
6. 用接口测试和论文查重前的自检清单收尾
系统跑起来不等于交付完成,最后一步是验证。我一般会写一组接口测试覆盖核心链路,再对着论文做一遍自检。
接口测试用 SpringBoot 自带的 MockMvc 或直接上 Postman 都行,重点覆盖四条链路:新增资产 → 领用 → 调拨 → 报废,每一步都断言状态和记录表的变化。下面是一个 MockMvc 的测试片段。
@SpringBootTest @AutoConfigureMockMvc class AssetFlowTest { @Autowired MockMvc mockMvc; @Test void fullLifecycle() throws Exception { // 1. 新增资产,期望返回资产ID String body = "{\"assetCode\":\"IT2025060001\",\"name\":\"测试笔记本\",\"price\":8000,\"categoryId\":1,\"deptId\":1}"; mockMvc.perform(post("/api/asset").contentType(MediaType.APPLICATION_JSON).content(body)) .andExpect(jsonPath("$.code").value(200)); // 2. 领用,期望状态变为在用 mockMvc.perform(post("/api/asset/1/use").param("userId", "2")) .andExpect(jsonPath("$.data.status").value(1)); // 3. 报废,期望状态变为报废且记录表新增一条 mockMvc.perform(post("/api/asset/1/scrap")) .andExpect(jsonPath("$.data.status").value(3)); } }测试跑通后,论文查重前的自检我习惯过一遍这几项:表结构说明和实际建表语句是否一致、折旧公式在论文和代码里是否一致、截图里的数据和测试数据是否对得上、参考文献格式是否统一。论文里最容易出问题的是「设计」和「实现」两章内容重复,设计章讲架构和模型,实现章讲关键代码和运行效果,分工要清楚。
最后说个习惯:每次改完折旧或状态流转这类核心逻辑,我都会把测试用例重跑一遍,再手动走一遍完整流程。固定资产系统的 bug 往往不在功能本身,而在数据的一致性上,而一致性问题是不会在单次点击里暴露的。希望帮到你。
本文还有配套的精品资源,点击获取