
简介一套基于若依框架与layui的固定资产管理系统Java源码面向需要快速搭建企业资产管理后台的开发者与若依框架学习者。系统完整覆盖资产登记、领用、借用、归还、维修、调拨、转移、报废、统计分析及组织结构与角色权限等业务环节资产状态流转与审批流程清晰。压缩包共1883个文件以Java、HTML、JS、CSS、XML等类型为主包含后端业务代码、layui前端页面、数据库SQL及环境配置脚本整体约8.88MB。已有552人学习。通过源码可学习若依权限模型的实际应用、layui表格与表单组件的封装方式以及固定资产从入库到报废的全生命周期管理设计资源还给出了组织架构调整与角色分配的完整示例既适合作为若依二次开发的项目蓝本也适合Java中级开发者做综合实战参考。1. 固定资产管理系统不只是给设备建个表那么简单先聊个经历。去年给一家制造业客户做信息化改造他们原有的固定资产管理是一张Excel大表几千条资产记录全靠几个行政同事手工维护。结果呢年中盘点对不上账设备借出去没人登记报废审批走了三个星期财务算折旧全靠拍脑袋。客户跟我说我们就要一个能扫码、能审批、能出报表的Java系统。听上去简单真做起来才发现固定资产管理这个领域业务复杂度远超想象。这篇博文就来聊聊如何从零构建一套基于Java的固定资产管理系统源码覆盖资产全生命周期管理、领用归还、折旧计算、盘点对账、审批流等核心模块。文章适合有Java基础、想做管理系统练手的开发者也适合被企业资产台账折磨得头大的技术人员——你可以直接参考这套思路去做二次开发甚至落地成商业项目。我会按实际开发顺序来拆先理顺业务再谈技术选型与数据库设计然后落到核心模块的代码实现最后把我踩过的坑和优化经验一并交代。2. 资产业务域拆解先把管什么想清楚再动手做管理系统最忌讳上来就建表写接口。固定资产管理的核心不是技术而是业务模型是否贴合真实场景。我习惯先梳理业务域再映射成数据模型。2.1 资产生命周期与状态机一台设备从进公司到报废会经历入库领用退库维修调拨报废处置这些节点。每个节点对应一个状态所有业务操作本质上都是状态流转加上操作记录。我设计的状态枚举如下public enum AssetStatus { IN_STOCK(0, 在库), IN_USE(1, 领用中), REPAIRING(2, 维修中), SCRAPPED(3, 已报废), LOST(4, 已报失); private final int code; private final String desc; // 构造方法、getter略 }为什么要单独建状态枚举而不是用字符串硬拼因为Java是强类型语言枚举能避免魔法值散落各处还能方便做合法性校验——比如报废只能由维修中或领用中触发状态机的合法性检查放在Service层统一处理不让脏状态流进数据库。2.2 角色权限管资产和用资产是两类人这个系统里的用户大致分三类普通员工申领人、使用人、资产管理员日常维护、审批、财务或高层查看报表、折旧数据。我直接用Spring Security JWT做认证配合自定义注解做接口级权限控制。角色不搞多表关联的复杂RBAC用一个简单的用户-角色表就够了没必要为了炫技引入Shiro或Security的完整权限模型——除非你要做多租户SaaS版。2.3 盘点与折旧最容易被人忽略却最关键的模块很多固定资产系统的demo都只做了增删改查但企业真正离不开的是两个功能盘点和折旧。盘点解决账实相符问题折旧解决资产价值核算问题。不懂财务的开发者往往把折旧做成简单的原值/年限实际上固定资产折旧至少有年限平均法、工作量法、双倍余额递减法、年数总和法四种国内企业99%用年限平均法但代码里最好预留策略接口方便扩展。3. 从单体到模块化技术选型和工程结构设计这套系统我用的是目前Java从业者最熟悉的组合Spring Boot 2.7 MyBatis-Plus MySQL 8.0 Redis Vue 3前端。之所以选这套不是因为它最新而是因为它最不折腾。如果你是一个人在做公司内部系统或者毕业设计真没必要上微服务、上Spring Cloud那是给自己找麻烦。3.1 后端模块划分按业务边界我把工程拆成五个模块但不是Maven多模块而是包结构层面的模块化——毕竟这类系统规模还没到拆微服务的程度controllerREST接口层只做参数接收和结果包装service业务逻辑层事务、状态流转、业务校验都在这层mapper数据访问层MyBatis-Plus的BaseMapperentity数据库实体common统一返回体、异常处理、工具类、常量定义工程结构上还有一个容易被新手忽略的点统一返回体和全局异常处理器。我见过不少系统每个接口返回的JSON格式都不一样前端联调时苦不堪言。所以我一开始就定了标准返回结构public class ApiResultT { private int code; private String message; private T data; private long timestamp; public static T ApiResultT success(T data) { ApiResultT result new ApiResult(); result.code 200; result.data data; result.timestamp System.currentTimeMillis(); return result; } public static T ApiResultT error(int code, String message) { // 类似实现 } }配合RestControllerAdvice全局异常处理把业务异常、参数校验异常、未知异常分别映射成不同的code前端拿到非200就能直接弹提示。3.2 为什么用MyBatis-Plus而不是JPA说实话JPA在复杂查询上写起来确实别扭尤其是多表联查、动态条件拼接这种场景。MyBatis-Plus的优势在于单表CRUD几乎不用写SQL复杂查询用LambdaQueryWrapper也能拼出可读性不错的条件。加上它还自带分页插件做资产列表的分页查询非常省事。对于那些必须手写SQL的报表类查询我单独建*Mapper.xml文件写原生SQL。这种简单用MP、复杂写XML的混合方案是我做这类中小型管理系统最舒坦的方式。3.3 关于JWT和Redis的配合登录我采用的是JWT无状态方案但有一个细节容易踩坑用户修改密码或管理员禁用账号后旧token仍然有效。怎么处理我引入了一个Redis黑名单机制登出或者修改密码时把token的jtiJWT唯一ID写入Redis过期时间设为token剩余有效期。每次请求在JWT过滤器里查一下Redis命中黑名单就拒绝访问。这样既保留了JWT的无状态扩展性又解决了token失效的痛点。4. 数据模型设计几张核心表决定系统的上限前面讲完架构现在给出最核心的数据库设计。设计原则只有一条面向业务过程建模而不是面向页面建模。也就是说表结构不是页面长什么样就建什么样的表而是跟着业务发生的动作走。4.1 资产主表与其他关键表资产主表存的是资产的静态信息比如资产编码、名称、分类、规格型号、原值、启用日期等。注意一个细节资产编码要有生成规则比如分类代码日期流水号ZC-IT-20240617-001这让资产编号有序、可读、方便Excel导入导出时对齐。除主表外还有几张关键表表名核心字段用途asset资产编码、分类ID、资产名称、原值、净值、状态、存放地点、采购日期资产静态与动态信息asset_category父分类ID、分类名称、折旧年限、折旧方法分类及默认折旧策略asset_stock_record资产ID、操作类型、操作人ID、操作时间、备注入库/领用/退库/报废等动作流水asset_repair_record资产ID、故障描述、维修费用、维修状态、维修商维修全记录asset_inventory盘点单号、盘点人、盘点时间、盘点结果盘点主单asset_inventory_item盘点单ID、资产ID、账面数/实盘数、差异原因盘点明细asset_approval审批单号、业务类型、申请内容、审批状态、审批人通用审批流表4.2 台账、流水与审批单三套数据如何联动关于数据一致性我最想强调的一点是台账表永远只存当前状态历史动作全部走流水表。比如员工领用一台笔记本电脑这时候要做三件事更新asset表的status字段为IN_USEuse_emp_id写当前使用人在asset_stock_record表插入一条领用记录记录操作人、时间、备注生成一条审批单记录如果配置了需审批的流程。这三步必须在一个事务里完成。很多入门级系统只做了第1步结果就是出了问题完全不知道这资产是怎么变到这种状态的回溯等于零。4.3 分页查询的大坑千万要注意n1问题资产列表页常见需求是展示资产分类名称、当前使用人姓名而不是分类ID和用户ID。如果直接用MyBatis-Plus的list()查出所有资产再在循环里查分类表和用户表那就是经典的N1问题。资产一旦上万条接口直接卡死。我的做法是列表查询不分步查而是用自定义SQL做一个多表关联的分页查询一次性把需要的冗余字段查出来。只对详情页才用主键回表查完整信息。关于这块后面踩坑记录那章我还会再展开说。5. 核心代码实现资产入库、领用归还、折旧计算、盘点处理现在进入代码环节。我挑四个最核心、也最能体现系统能力的模块来讲。每个模块我会先说明业务逻辑再贴关键代码片段。5.1 资产入库唯一性校验与编码生成入库是最基础的操作但代码里有一个非常关键的校验点资产编码必须唯一。如果入库时不做限制后面领用、盘点全都会乱。另外入库时同步要计算资产的初始月折旧额为报表做准备。Service public class AssetServiceImpl extends ServiceImplAssetMapper, Asset implements AssetService { Override Transactional(rollbackFor Exception.class) public Asset saveAsset(AssetAddDTO dto) { // 1. 校验资产编码是否重复 long count this.count(new LambdaQueryWrapperAsset() .eq(Asset::getAssetCode, dto.getAssetCode())); if (count 0) { throw new BizException(资产编码已存在请勿重复录入); } // 2. 自动生成资产编码如果前端没传 String assetCode StrUtil.isBlank(dto.getAssetCode()) ? generateAssetCode(dto.getCategoryId()) : dto.getAssetCode(); // 3. 组装实体计算初始净值与月折旧额 Asset asset new Asset(); BeanUtils.copyProperties(dto, asset); asset.setAssetCode(assetCode); asset.setStatus(AssetStatus.IN_STOCK.getCode()); asset.setOriginalValue(dto.getOriginalValue()); asset.setNetValue(dto.getOriginalValue()); asset.setMonthDepreciation(calculateMonthDepreciation( dto.getCategoryId(), dto.getOriginalValue())); this.save(asset); // 4. 写入入库流水 saveStockRecord(asset.getId(), StockType.IN_STOCK, 资产入库); return asset; } }注意第3步setOriginalValue和setNetValue分别存原值和净值刚入库时二者相等。以后的每个月末通过定时任务或者手动触发折旧计算把净值逐步减少。如果不存冗余的原值字段后面报表里原值合计净值合计都得join分类表再算折旧费时费力。5.2 领用与归还状态变化与人员绑定领用操作需要做两个动作更新资产状态与使用人以及写入流水。这里隐藏着一个并发问题同一台资产如果被两个人同时点击领用怎样保证不超领我用的方案是乐观锁。在asset表加一个version字段更新时带上版本号条件Update(UPDATE asset SET status #{newStatus}, use_emp_id #{empId}, version version 1 WHERE id #{id} AND version #{version} AND status #{expectStatus}) int updateStatusWithVersion(Param(id) Long id, Param(version) Integer version, Param(expectStatus) Integer expectStatus, Param(newStatus) Integer newStatus, Param(empId) Long empId);如果updateStatusWithVersion返回0说明版本号不匹配或者当前状态不是预期状态直接抛出该资产当前状态不允许领用的提示。这样即使用户连点了两次最多只有一次能成功比加分布式锁简单得多。归还的时候反过来操作状态从IN_USE变回IN_STOCKuse_emp_id置空并校验是否超过预计归还日期超期的话在流水备注里标记出来供管理员处理。5.3 折旧计算策略模式与定时任务的配合折旧这块我选了策略模式而不是if-else堆叠多个方法。看代码就明白了public interface DepreciationStrategy { // 计算月度折旧额 BigDecimal calculate(Asset asset, AssetCategory category); // 策略类型 String getType(); } Component public class StraightLineDepreciationStrategy implements DepreciationStrategy { Override public BigDecimal calculate(Asset asset, AssetCategory category) { // 年限平均法(原值 - 残值率) / 折旧年限 / 12 BigDecimal salvageValue asset.getOriginalValue() .multiply(category.getSalvageRate()); return asset.getOriginalValue() .subtract(salvageValue) .divide(BigDecimal.valueOf(category.getDepreciationYears() * 12), 2, RoundingMode.HALF_UP); } // getType() 返回 straight_line }在AssetCategory表里每个分类可以配置自己的折旧方法这样固定资产录入的时候就直接从分类表带出默认策略。定时任务在每月最后一天晚上跑一次批量更新所有在册且未报废资产的净值并把折旧流水写入asset_depreciation_record表。这个表很重要做财务审计的时候每一分钱的折旧来源都要能追溯到。5.4 盘点模块差异生成与复盘处理盘点流程我设计成三个步骤创建盘点单扫码录入实盘数据生成差异清单并处理差异。创建盘点单时系统会把当前所有在册资产快照到asset_inventory_item表每行带上账面状态在库、领用中、维修中。实盘录入时如果扫码发现资产不存在于当前盘点单就自动标记为盘盈如果盘点单里的资产没被扫到就标记为盘亏。盘亏的资产还需要填写差异原因比如丢失、遗漏登记等。这一步最需要处理好的是账面数/实盘数和盘盈盘亏之间的逻辑关系。我直接设计成字段冗余而不是动态计算虽然违反了部分数据库范式但做报表和复盘时极其方便——读一行就能判断不需要多表join和计算。6. 从开发到上线的实战踩坑记录这部分是这套系统从能跑到能用的关键。坦白讲功能全部写完只是第一步真正能稳定跑起来靠的是下面这几个坑我一个个填过来的。6.1 MyBatis-Plus字段映射boolean是is开头时要注意数据库字段叫is_deleted实体里定义成private Boolean deleted;MyBatis-Plus默认开启了下划线转驼峰按理说没问题。但如果你在实体类上加了TableLogic逻辑删除注解查询时它会自动追加is_deleted 0条件。这里不坑坑在你有自己的字段叫is_deleted但没加逻辑删除注解同时你又配置了全局逻辑删除值——MyBatis-Plus会把所有表名带is_deleted的查询都过滤掉查出来的结果少一半排查半天才发现是MP的全局逻辑删除配置搞的鬼。解决办法要么统一所有表都加逻辑删除字段要么全局配置里把逻辑删除字段名改成别的比如deleted_flag。不要混用。6.2 批量导入Excel时的性能与校验问题固定资产系统几乎都要支持Excel导入初始数据。刚开始我图省事直接把Excel解析成List然后循环调用saveAsset一套下来导入5000条数据跑了接近三分钟还偶发内存溢出。后来改成两段式优化第一段校验全部完成后再批量插入第二段用MyBatis-Plus的saveBatch分片提交。校验时也不能边解析边插库应该先在内存里把所有编码收集成一个Set一次查库判断重复避免每条都查一次。做完这两步5000条数据导入从180秒降到了8秒。6.3 报表统计的大坑千万别在for循环里查数据库系统里有一个部门资产统计报表接口需要按部门聚合资产原值、净值、数量。我最早写的版本是查出所有部门然后循环查每个部门的资产汇总——部门50个就有50次SQL每次SQL全表扫描接口响应花了6秒多。优化方案很直接一次性用GROUP BY查出所有汇总数据然后内存里做分组SELECT department_id, COUNT(*) AS total_count, SUM(original_value) AS total_original, SUM(net_value) AS total_net FROM asset WHERE deleted 0 GROUP BY department_id一次查询出全部结果再和部门列表做关联接口响应降到了200毫秒以内。这个优化思路对所有报表类需求通用——尽量把聚合下推到数据库而不是在Java里循环做。6.4 关于金额字段类型BigDecimal是唯一正解固定资产系统里字段类型的选择说实话我见过有人用double存金额的等到月底对账发现差了几分钱查了一晚上都找不到原因。金额、税率、折旧率这类字段Java里必须用BigDecimal数据库里用DECIMAL(18,2)或更精确的DECIMAL(18,4)否则就会出现经典的浮点数精度问题。另外还有一个容易忽略的金额运算时必须显式指定精度和舍入模式否则divide方法可能抛出ArithmeticExceptionBigDecimal netValue originalValue.subtract(depreciation) .setScale(2, RoundingMode.HALF_UP);不要相信默认精度显式传MathContext或RoundingMode是保平安的习惯。7. 项目落地过程中值得沉淀的几个小经验聊点不那么代码、但实际开发中特别有用的东西。第一编码生成规则想清楚再动手。我见过很多系统资产编号直接用数据库自增ID结果导出的Excel里编号是12345这种毫无规律的流水号外人根本看不出资产类别。好的编码规则应该能让人看一眼就知道大致信息比如ZC-IT-202406-0001表示IT类、2024年6月购入的第一台设备。用Java写一个编码生成器配合redis的自增计数或者数据库序号表很容易实现。第二审批流别一上来就接Flowable或Activiti这类工作流引擎。这类引擎功能强但引入成本和维护成本极高一个小资产系统根本用不上会签、并行网关这些复杂特性。我更建议用简单的状态机配合一张审批记录表去实现代码可控、排错容易。等哪天系统真变大到多租户、复杂审批策略再升级到工作流引擎不迟。第三前端扫码方案的选择。固定资产盘点离不开扫码。我试过H5调用摄像头扫码和微信小程序扫码两种方案——如果团队没有小程序开发能力直接用H5扫码其实就够用了调起摄像头识别条形码/二维码识别到资产编号后自动带入asset_inventory_item的实盘录入界面体验完全够用。第四导出功能一定要考虑大数据量。系统做了Excel导出功能后2000条以内的数据用EasyExcel的write方法导出很轻松。但超过1万条时如果是同步导出用户要从浏览器下载页面白等几十秒。我后来的方案是异步导出接口先把导出任务丢进线程池生成完文件后存到本地磁盘或者OSS再通过WebSocket把下载链接推给前端。用户点导出按钮后继续干别的不用傻等。这套Java固定资产管理系统我从零写完到上线稳定跑了大半年核心思路就是业务先行、数据建模要贴近真实操作、代码层面保持简单可控。如果你正在做一个类似的管理系统不管是为了毕业设计还是公司内部使用都可以按着这个思路去搭骨架。遇到具体问题欢迎在评论区留言咱们可以接着聊。本文还有配套的精品资源点击获取