简介:固定资产管理系统Java项目源码以完整源代码形式,提供了一套基于Java技术开发的资产管理系统,覆盖固定资产的增、删、改、查、采购入库、分配折旧、盘点报废等生命周期管理功能,并支持通过ODBC配置灵活切换SQL Server、MySQL、Oracle等数据库,具备良好的跨平台与扩展性,适合Java Web开发者学习数据访问层与业务逻辑设计,也可作为企业资产管理系统的定制模板。压缩包共606个文件、约8.73MB,其中包含131个Java源文件、177个编译后的class文件、59个JSP页面,以及42个jar依赖库和多个tld、dtd、xml配置文件,源码与页面分层清晰,便于按模块阅读和二次开发。包内附带的“必看.pptx”介绍了系统架构、功能模块、数据库表结构及ODBC连接配置步骤,可帮助快速理解运行机制并上手实践。已有1686人学习下载,是从代码层面掌握固定资产全流程管理、提升Java Web综合开发能力的实用资源。
1. 固定资产管理系统:一个 Java 项目源码包,解决的不只是台账问题
如果你在找资产管理系统,大概率是两种处境之一:公司 Excel 台账越记越乱,资产标签贴了又丢,年终盘点全靠人肉对账;或者你是个开发者,想找一个完整的 Java 项目来改造,却总在「代码太老跑不起来」和「功能太简单没参考价值」之间来回横跳。这个固定资产管理系统源码包,属于后者里难得让我愿意多看几眼的:它不只是一个能增删改查的 CRUD 架子,而是把资产从入库、领用、归还、调拨到盘点、报废的完整生命周期都串了起来,并且带了权限控制和报表导出。适合两类人:一类是有 Java Web 基础、想拿真实业务练手的学生或初级开发,另一类是真想给内部做个资产管理后台、但不想从零开始的中小企业技术负责人。
我拆过不少这类项目,很多号称「管理系统」的源码其实只是把几个页面拼在一起,点开一看业务逻辑薄得像层纸。这份源码的厚度不太一样,资产编号生成规则、领用审批流程、盘点差异处理这些细节都有落地实现。接下来我会从架构、核心代码、踩坑记录到二次开发,把这份源码从头到尾捋一遍,你可以边看边对照自己手头的需求,看看哪些模块能直接拿来用。
2. 技术选型与项目结构:一个典型 Java Web 工程的骨架长什么样
2.1 后端技术栈:为什么选 SSM 而不是 Spring Boot
这个项目的后端是基于 SSM 框架搭建的,也就是 Spring + Spring MVC + MyBatis 三件套的组合,构建工具用的是 Maven,数据库是 MySQL,前端是 JSP + JSTL 配合 AdminLTE 这套开源后台模板。可能你会问,现在新项目不都用 Spring Boot 了吗,为什么还要看 SSM?我的判断是,这正是这份源码的价值所在——它不像 Spring Boot 那样把大量配置自动隐藏了,而是把数据源、事务、拦截器、MyBatis 映射这些底层细节都显式地暴露在 XML 和配置类里。你拿到手之后,能清楚地看到每一个 Bean 是怎么装配起来的,MyBatis 的 Mapper 是怎么和 DAO 层关联的,这对于理解 Java Web 项目的运行机制比直接用 Boot 更有效。
另外从改造成本来看,SSM 项目迁移到 Spring Boot 其实不复杂,核心业务代码基本不用动,只要把 Spring 和 MyBatis 的配置换成 Boot 的自动配置,再调整一下依赖就行。所以不要因为技术栈不是最新的就把它划走,这套源码的架构思路是通用的,你把它吃透了,换个框架只是顺手的事。
2.2 目录结构:从 controller 到 mapper 的完整分层
解压源码包之后,你会看到典型的 Maven 工程结构。我建议你先不要急着去看代码,先把目录捋一遍,搞清楚每个包是干什么的,这样后面读代码的时候心里有数。
src/main/java ├── com.company.asset │ ├── controller // 控制层:接收请求、参数校验、返回视图 │ │ ├── AssetController.java │ │ ├── CategoryController.java │ │ ├── DeptController.java │ │ ├── UserController.java │ │ ├── BorrowController.java │ │ ├── ScrapController.java │ │ ├── CheckController.java │ │ └── ReportController.java │ ├── service // 业务层:事务边界、业务规则 │ │ ├── AssetService.java │ │ ├── AssetServiceImpl.java │ │ ├── UserService.java │ │ └── ... │ ├── dao // 数据访问层:MyBatis mapper 接口 │ │ ├── AssetDao.java │ │ ├── CategoryDao.java │ │ ├── DepartmentDao.java │ │ ├── UserDao.java │ │ └── ... │ ├── entity // 实体类:对应数据库表 │ │ ├── Asset.java │ │ ├── Category.java │ │ ├── Department.java │ │ ├── User.java │ │ ├── Borrow.java │ │ └── ... │ ├── vo // 视图对象:承载组合查询结果 │ │ ├── AssetQueryVO.java │ │ └── ReportVO.java │ ├── interceptor // 拦截器:登录校验、权限控制 │ │ └── AuthInterceptor.java │ ├── util // 工具类:编号生成、Excel 导出 │ │ ├── AssetCodeGenerator.java │ │ └── ExcelUtil.java │ └── config // 配置类 │ └── SpringMVCConfig.java ├── resources │ ├── spring-context.xml │ ├── spring-mvc.xml │ ├── mybatis-config.xml │ ├── jdbc.properties │ └── mapper │ ├── AssetMapper.xml │ ├── CategoryMapper.xml │ └── ... └── webapp ├── WEB-INF │ ├── views │ │ ├── asset │ │ ├── borrow │ │ ├── check │ │ ├── report │ │ └── system │ └── web.xml └── static ├── adminlte ├── css ├── js └── images这个分层是教科书式的经典结构:Controller 只负责接收参数和返回视图,业务逻辑全部收敛在 Service 层,DAO 层只做数据读写。我当时看代码的时候特别注意了一下事务注解,发现@Transactional是放在 ServiceImpl 类上的,这符合 Spring 官方推荐的做法——事务边界应该定义在业务逻辑层,而不是控制层。多层结构带来的一个好处是,如果你想把 JSP 换成 Vue + 接口模式,只需要改 Controller 层的返回逻辑,把 ModelAndView 改成@ResponseBody返回 JSON,Service 和 DAO 层可以完全复用。
数据库脚本在sql目录下,一共 8 张核心表:t_asset(资产主表)、t_category(分类表)、t_department(部门表)、t_user(用户表)、t_borrow(领用/归还记录表)、t_borrow_item(领用明细表)、t_check(盘点记录表)、t_scrap(报废记录表)。表之间的关联关系是:资产属于某个分类和某个部门,领用记录关联资产和用户,盘点记录关联资产和盘点结果。字段设计的细节我在后面核心模块里展开讲。
3. 核心模块拆解:资产从入库到报废的完整生命周期
3.1 资产台账模块:编号自动生成与组合查询
资产台账是系统的数据底座,所有其他业务都围绕着资产主表展开。先看资产实体类和它的 Mapper 实现,重点理解两个地方:资产编号怎么生成、组合查询怎么拼 SQL。
// Asset.java 实体类关键字段 public class Asset { private Integer id; // 主键 private String assetCode; // 资产编号,如 ZC-2024-0001 private String assetName; // 资产名称 private Integer categoryId; // 分类ID private Integer deptId; // 使用部门ID private String status; // 状态:在库/领用/维修/报废 private Date purchaseDate; // 购置日期 private BigDecimal price; // 原值 private Integer userId; // 当前保管人 private String remark; // 备注 // getter/setter 省略 }<!-- AssetMapper.xml 组合查询部分 --> <select id="queryAssetList" resultType="com.company.asset.vo.AssetQueryVO"> SELECT a.*, c.name AS categoryName, d.name AS deptName, u.real_name AS userName FROM t_asset a LEFT JOIN t_category c ON a.category_id = c.id LEFT JOIN t_department d ON a.dept_id = d.id LEFT JOIN t_user u ON a.user_id = u.id <where> <if test="assetName != null and assetName != ''"> AND a.asset_name LIKE CONCAT('%', #{assetName}, '%') </if> <if test="categoryId != null"> AND a.category_id = #{categoryId} </if> <if test="deptId != null"> AND a.dept_id = #{deptId} </if> <if test="status != null and status != ''"> AND a.status = #{status} </if> </where> ORDER BY a.id DESC </select>这个组合查询是资产管理里最常见的需求。四个条件全部用<if>动态拼接,判断条件是否为空再决定要不要拼进 SQL,这样前端传什么条件就过滤什么条件,不传就是全量查询。需要注意order by放在<where>外面,排序不会因为动态条件受影响。这里的三表联查返回的是AssetQueryVO,就是为了在前端列表页直接展示分类名、部门名和保管人姓名,避免在 Java 代码里做二次查询。
资产编号生成是另一个值得看的点。
// AssetCodeGenerator.java public class AssetCodeGenerator { // 根据当前日期+当天流水号生成编号,例如 ZC-20241015-0001 public static String generate(String prefix, int seq) { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); return String.format("%s-%s-%04d", prefix, sdf.format(new Date()), seq); } }这段代码逻辑很直观:传入前缀(资产用 ZC,领用单用 LY),拼上日期和四位数流水号。实际生产环境里可以在 Service 层先查询当天已有的最大编号,再 +1 传给generate方法,保证同一天内编号递增、跨天自动归零。如果系统要支持多公司或多仓库,可以在前缀里再加一段机构编码,改造成本很低。
3.2 领用与归还模块:状态机的流转设计
资产领用是整个系统里业务逻辑最完整的流程。公司内部的资产借用不是简单地改一下asset.status就完事了,需要记录领用人、领用时间、预计归还时间,归还的时候还要校验资产状态和保管人是否匹配。这个模块我拆开看,发现它内部是一个典型的状态机驱动。
// BorrowServiceImpl.java 归还逻辑 @Override @Transactional public void returnAsset(Integer borrowItemId) { // 1. 查询领用明细记录 BorrowItem item = borrowItemDao.findById(borrowItemId); if (item == null) { throw new BusinessException("领用记录不存在"); } // 2. 判断记录状态:必须处于"领用中"才能归还 if (!"2".equals(item.getStatus())) { throw new BusinessException("当前记录状态不可归还"); } // 3. 更新明细状态为已归还 item.setStatus("3"); // 3=已归还 item.setReturnTime(new Date()); borrowItemDao.update(item); // 4. 更新资产主表状态为在库 Asset asset = assetDao.findById(item.getAssetId()); asset.setStatus("1"); // 1=在库 asset.setUserId(null); assetDao.update(asset); }这个方法的精妙之处在于状态校验前置。它不是上来就改数据,而是先做两件关键的事:查记录是否存在、查记录状态是否允许归还。这个「状态不匹配就不执行」的思路是这套代码的典型风格,你要是在做二次开发,最好也保持这个习惯,不要在 Service 里跳过状态判断直接改库,不然盘点数据会乱到怀疑人生。
领用的时候逻辑类似,区别在于领用是「创建一条领用单 + 更新资产状态」两步操作,得益于@Transactional注解,这两步要么都成功要么都失败,不会出现领用单建了但资产状态没改的情况。状态值设计上用了数字字符串:1 在库、2 领用中、3 维修、4 报废,这是老项目的常见做法,优点是好比较,缺点是语义不够清晰。你要是想改造,可以把数字映射成枚举类,但要注意前端 JSP 里可能写死了状态值,改的时候前后端要一起动。
3.3 盘点模块:差异记录怎么处理
固定资产年度盘点是最容易出幺蛾子的环节,账实不符是家常便饭。盘点模块的处理逻辑是:管理员发起盘点任务,生成一张盘点单,然后逐条勾选资产填写实盘结果。如果实盘数量与账面数量不一致,系统自动生成差异记录,并把差异原因记录下来。
// CheckServiceImpl.java 盘点差异处理 public CheckResult generateDiffReport(Integer checkId) { // 1. 查出该盘点单下所有明细与资产账面状态 List<CheckItem> items = checkItemDao.findByCheckId(checkId); List<DiffItem> diffList = new ArrayList<>(); for (CheckItem item : items) { Asset asset = assetDao.findById(item.getAssetId()); // 2. 对比账面状态与实盘状态 boolean statusDiff = !asset.getStatus().equals(item.getStatus()); boolean deptDiff = !asset.getDeptId().equals(item.getDeptId()); if (statusDiff || deptDiff) { DiffItem diff = new DiffItem(); diff.setAssetId(item.getAssetId()); diff.setCheckId(checkId); diff.setBookStatus(asset.getStatus()); diff.setRealStatus(item.getStatus()); diff.setBookDept(asset.getDeptId()); diff.setRealDept(item.getDeptId()); diff.setHandleStatus("0"); // 0=待处理 1=已处理 diffDao.insert(diff); } } // 3. 汇总差异结果 return buildCheckResult(checkId, diffList); }这段代码的逻辑是把账面数据和盘点数据逐一对比,发现差异就生成一条待处理记录,等于把「发现什么问题」和「怎么处理问题」拆成了两个阶段。这样设计的好处是盘点操作员只负责录入实盘情况,后续的差异原因分析和责任认定由另一拨人去处理,职责边界清晰。我在实际项目里看到很多同类系统把这两步揉在一起,导致一个普通操作员就能直接改资产状态,容易留下数据安全隐患。
3.4 报表导出:ExcelUtil 里藏着什么工具
系统提供了分类统计、部门资产明细、领用归还记录三类报表,导出功能用的是一种轻量级方案:用 Java 的 POI 库直接操作 XSSFWorkbook 生成 Excel 文件,通过HttpServletResponse以流的形式写回浏览器。
// ExcelUtil.java 导出核心方法 public static void export(List<String> headers, List<List<String>> data, HttpServletResponse response) { XSSFWorkbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("Sheet1"); // 1. 写表头 Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.size(); i++) { headerRow.createCell(i).setCellValue(headers.get(i)); } // 2. 写数据行 for (int i = 0; i < data.size(); i++) { Row row = sheet.createRow(i + 1); List<String> rowData = data.get(i); for (int j = 0; j < rowData.size(); j++) { row.createCell(j).setCellValue(rowData.get(j)); } } // 3. 设置响应头并输出 String fileName = URLEncoder.encode("资产报表.xlsx", "UTF-8"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=" + fileName); workbook.write(response.getOutputStream()); workbook.close(); }这个工具类把导出的核心逻辑封装成了一个静态方法,Controller 调用的时候只需要准备表头数组和二维数据列表,不用关心 Excel 格式细节。需要注意两点:文件名用URLEncoder.encode处理,避免中文文件名在部分浏览器里乱码;导出 Excel 对历史数据量大的表比较吃内存,这是轻量方案的已知局限。如果资产量超过 10 万条,导出时建议加个异步任务 + 下载链接,这是后话了。
4. 避坑指南:跑这个项目最容易翻车的三个地方
4.1 环境排查:JDK 版本和 Tomcat 版本不匹配导致启动失败
现象:在本地启动 Tomcat,控制台直接报UnsupportedClassVersionError或者启动后页面 404,没有报错信息但登录页就是出不来。
原因:源码是在 JDK 8 环境下写的。如果你的本机装的是 JDK 11 或更高版本,编译阶段可能没问题(因为 Maven 的编译插件有版本兼容),但 Tomcat 版本不够新的话,对旧 class 文件的加载处理会出现版本不兼容。另外如果源码包里的web.xml声明的 servlet 版本是 3.1 而你的 Tomcat 是 8.0 以下,也会出现类似的启动异常。
解决:确认 JDK 是 8 并设置好JAVA_HOME环境变量,Tomcat 用 8.5.x 版本,这两个版本组合是这套代码最成熟的运行环境。启动之前检查pom.xml里maven-compiler-plugin的source和target是否都是 1.8,如果不是,改成 1.8 让 Maven 重新编译。如果你是 Mac 环境,注意别同时存在多个 JDK 版本导致命令行用的是 11 而 IDE 用的是 8,这种环境错乱问题不好排查。
4.2 数据库初始化的坑:SQL 脚本导入报错或中文乱码
现象:导入init.sql脚本时提示字段长度超限,或者导入成功但页面显示的中文都是问号?。
原因:典型的字符集问题。数据库连接串里没设置characterEncoding=UTF-8,或者 MySQL 数据库本身的字符集是 latin1,导致中文数据存不进去。字段长度超限通常是 SQL 脚本里的 VARCHAR 长度在导入时因为字符集换算被判定超限。
解决:建库的时候指定字符集,执行 SQL 前先跑一句CREATE DATABASEassetDEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;,这样后面导入表结构就没问题。还要检查jdbc.properties里的连接串,确认一定要带上?useUnicode=true&characterEncoding=UTF-8,注意 JMeter 或命令行直接导入脚本时也要用--default-character-set=utf8参数。
4.3 登录拦截器的白名单遗漏,导致没有验证码模块
现象:输入验证码时发现没有验证码图片,或所有页面请求都被拦截器拦住了,登录后跳转正常但访问静态资源 404。
原因:这套系统的登录模块用的是用户名 + 密码的简单校验,没有接验证码接口。拦截器只放行了/login和/logout两个路径,静态资源路径(比如 css、js、图片放在/static/下)没加白名单,导致页面加载时样式全丢。
解决:这个东西不能只靠代码,要把拦截器的白名单加完整。在AuthInterceptor的preHandle方法里,把/static/**加入放行列表,确保 AdminLTE 模板的样式文件能正常加载。如果你确实要加验证码功能,可以用一个简单的做法:在登录 Controller 里返回一张图片流,同时把验证码存到 Session,然后在拦截器里比对。我一般会用 Servlet 原生 API 生成这种简单的图形验证码,省得再引入一套复杂框架。
5. 把项目跑起来:从拿到源码到看到登录页的完整操作过程
5.1 准备工作:需要用到的软件清单
按照我实际验证过的环境来配,稳的。操作系统 Windows 10/11 或 macOS 都可以,需要安装的软件如下:
- JDK 1.8(确保
java -version输出版本里包含 1.8) - Maven 3.6 或 3.8(用于依赖下载和打包)
- Tomcat 8.5(把 WTP/插件里的 Tomcat 关掉,避免冲突)
- MySQL 5.7 或 8.0(用本机安装即可)
- IDEA 或 Eclipse(推荐 IDEA,社区版或旗舰版都行)
5.2 导入数据库和初始化数据
先把工程解压到某个目录,别放在有中文和空格的路径下,防止各种莫名的构建报错。然后找到源码包里的sql文件夹,打开 MySQL 命令行执行:
mysql -u root -p < init.sql默认数据库名是asset_db,里面预制了几条基础数据:系统管理员账号 admin,普通操作员账号 user01,各有一个默认密码。这部分数据在t_user表的初始化 INSERT 里,MD5 加密方式存储。需要确认密码算法是什么,你可以去UserServiceImpl里看密码校验那段代码,通常会用DigestUtils.md5Hex来做。如果连接串配置的数据库名和你实际建的库名不一致,在页面登录时就会持续报数据库连接超时。
5.3 修改数据库配置与启动 Tomcat
打开jdbc.properties文件,按你的本地环境修改下列参数:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/asset_db?useUnicode=true&characterEncoding=UTF-8 jdbc.username=root jdbc.password=你的数据库密码然后回到 IDEA,打开 Maven 面板执行clean package,把工程打成 WAR 包。确定没有编译错误之后,把target/asset-management.war部署到 Tomcat 的 webapps 目录,或者如果你用 IDEA 启动 Tomcat,配置好 Application context 为/asset。启动完成后浏览器访问http://localhost:8080/asset/login,看到登录页就说明整套环境通了。
输入管理员账号进去之后,建议按这个顺序做一遍基础操作,验证系统是否正常:先在「分类管理」里加一个分类,再去「资产管理」里新增一条资产,看看资产编号是否正确生成;然后到「领用管理」里发起一条领用申请,审批通过后去台账里确认状态已变为「领用中」;最后做一次盘点,把账面状态改一下,看差异记录是否生成。这套流程走下来,你对代码里状态机的理解会比光看代码强得多。
6. 二次开发思路:把 Excel 台账迁移成一套可用的内部系统
6.1 加一个「资产图片」字段的完整流程
很多公司有拍照留存资产实物的需求,比如贵重设备在入库时拍一张照片存到系统里,这个需求在这个系统里改造起来可以直接照着四步走。
第一步,在t_asset表加字段ALTER TABLE t_asset ADD COLUMN asset_img VARCHAR(255) DEFAULT NULL COMMENT '资产图片URL';。第二步,在Asset.java加private String assetImg;属性。第三步,在AssetMapper.xml的resultMap和insert/update语句里加入字段映射,同时新增List<AssetImg>这种嵌套查询也行,但单个 URL 字段直接加主表更好。第四步,在新增资产的 JSP 页面加<input type="file">,Controller 里做文件上传,把文件保存到指定目录并往库里回填一个相对路径 URL。
文件上传这里要注意一个点:Tomcat 默认上传大小限制是 2MB,如果你需要传大图,在spring-mvc.xml里配一个MultipartResolver并把maxUploadSize调大到 10MB 或 20MB。我一般会把上传目录配置到项目外部(比如D:/asset-upload/),这样重新部署不会丢图片,也可以接一个 OSS 云存储来替代本地目录。
6.2 给报表加一个「资产月折旧」统计维度
如果你的使用场景涉及到财务对账,「资产折旧」几乎是必改的功能。固定资产的原值、残值率、折旧年限是一个经典的财务计算三元组,代码实现不复杂,难点在于把折旧计算做成可配置的。思路是:给t_asset加depreciation_year(折旧年限)和residual_rate(残值率,默认 5%),然后在报表模块新增一个按月份求折旧额的方法。
// 新增的计算逻辑 public BigDecimal calculateMonthlyDepreciation(Asset asset) { // 残值 = 原值 * 残值率 BigDecimal residualValue = asset.getPrice().multiply(asset.getResidualRate()); // 月折旧额 = (原值 - 残值) / (折旧年限 * 12) BigDecimal depreciableBase = asset.getPrice().subtract(residualValue); int totalMonths = asset.getDepreciationYear() * 12; return depreciableBase.divide(BigDecimal.valueOf(totalMonths), 2, RoundingMode.HALF_UP); }这个方法的两个隐藏细节值得注意:第一,BigDecimal的除法一定要指定小数位和舍入方式,直接除整数会抛ArithmeticException,这是财务计算里极容易出现的问题;第二,折旧计算的取整方式采用HALF_UP四舍五入,但严格来说财务上更常用「按月分摊、按年调整」的规则,如果你负责的项目后续要对接财务系统,这块策略最好和财务确认过再实现。报表 SQL 里可以用SUM按部门或分类对折旧额做聚合,然后复用ExcelUtil导出,这样一次改造就能同时满足页面查看和线下归档两个场景。
6.3 技术债务整理:JSP 升级成前后端分离
如果你不只是想内部用,而是打算长期在这个系统上继续加功能,我的建议是把前端迁到 Vue + Element 这类成熟的后台组件库,后端在现有 Controller 基础上加一套返回 JSON 的 RESTful 接口。改造顺序不要乱,我的习惯是:先把核心接口抽出来(登录获取 Token、资产列表、新增资产、领用接口),每个接口写死返回统一的Result<T>结构体,前端全部走 HTTP 调用;等接口稳定性有保证了,再逐步把 JSP 页面替换掉。不要想着一次性把所有页面全切过来,资产查询这种高频页面先动,盘点这种低频页面可以留着慢慢改。
老项目最大的风险其实不在技术本身,而在你没有完整跑通业务就动手重构,结果改着改着发现某个隐藏的状态逻辑没考虑到。我说的经验是,不管多急着改造,第一步永远是先把这个系统完完整整用一遍,把代码和操作路径对起来,你在心里画一张流程图,资产状态是怎么从 1 变成 2、再从 2 变成 3 的,每一步在代码里的入口和出口在哪里,都标注清楚再动刀。从那以后我每次接手这类管理系统的源码,都强制自己先跑一遍完整业务流再做任何改造。希望这份拆解笔记能帮你把这套源码跑明白,也让你在下一步写自己的代码时,少踩几个我踩过的坑。
本文还有配套的精品资源,点击获取