简介:这是一套面向Java后端开发者与企业信息化建设人员的固定资产管理系统源码,基于若依(RuoYi)框架与layui前端构建,适合作为企业级后台项目练手、二次开发或毕业设计参考。系统覆盖资产登记、领用借用、归还、维修、调拨转移、报废处理及统计报表等完整业务链路,并内置组织结构管理与角色权限分配,可依据部门与岗位灵活控制操作范围。压缩包共1883个文件,约8.88MB,以317个Java源码、180个HTML页面、90个JavaScript脚本、40个CSS样式及38个XML配置为主,另含SQL脚本、模板文件与图片资源,前后端结构清晰。目前已有559人学习下载。借助若依的权限体系与layui的组件化界面,读者可快速理解资产全生命周期管理的表结构设计与审批流转逻辑,并在此基础上扩展折旧统计、资产分布分析等模块,是掌握企业级管理系统开发思路的实用参考。
1. 固定资产管理源码(java):一套能跑起来的资产全生命周期系统到底长什么样
很多团队第一次做固定资产管理,都是被 Excel 逼出来的。资产台账散在七八个表格里,谁领走了、谁调岗了、谁离职没归还,全靠群里喊话;到了季度盘点,财务、行政、IT 三方对不上账,一台笔记本能在三个部门同时"存在"。这时候搜索"固定资产管理源码(java)"的人,诉求其实很明确:要一套能直接跑、能改、能对接现有组织架构的 Java 系统,而不是又一篇讲资产折旧公式的科普。
这套系统的核心是资产全生命周期:采购入库、领用分配、调拨转移、维修保养、折旧计提、报废处置,每一步都要留痕、可追溯、能出报表。技术栈上,主流做法是 Spring Boot + MyBatis-Plus + MySQL + Redis,前端 Vue 或 Thymeleaf 都行。它适合中小企业行政/IT 部门自建,也适合外包团队拿来做二次开发底座。下面我按"能不能用、怎么搭、坑在哪"的顺序,把这套源码拆开讲清楚。
2. 资产台账的数据模型怎么设计:从一张 asset 表说起
固定资产管理源码(java)能不能用,第一眼看数据模型。模型设计错了,后面折旧、盘点、报表全是补丁摞补丁。我见过太多项目把资产、领用记录、折旧记录全塞一张表,结果一台资产调拨三次就查不清历史。
2.1 核心表结构与字段取舍
一套能落地的资产台账,至少需要这几张表:资产主表、资产分类表、领用/调拨记录表、折旧记录表、盘点任务表、盘点明细表。资产主表不要存"当前使用人"这种会变的状态,而是通过最新一条领用记录推导,否则历史追溯就断了。
-- 资产主表:只存不随流程变化的静态属性 CREATE TABLE asset ( id BIGINT PRIMARY KEY AUTO_INCREMENT, asset_code VARCHAR(64) NOT NULL COMMENT '资产编码,业务唯一键', asset_name VARCHAR(128) NOT NULL COMMENT '资产名称', category_id BIGINT NOT NULL COMMENT '分类ID,关联 asset_category', spec VARCHAR(255) COMMENT '规格型号', purchase_date DATE COMMENT '采购日期', purchase_price DECIMAL(12,2) COMMENT '采购金额', original_value DECIMAL(12,2) COMMENT '资产原值', salvage_value DECIMAL(12,2) DEFAULT 0 COMMENT '预计残值', useful_life INT COMMENT '使用年限(月)', depreciation_method VARCHAR(32) DEFAULT 'STRAIGHT_LINE' COMMENT '折旧方法', status TINYINT DEFAULT 0 COMMENT '0在库 1领用 2维修 3报废', location VARCHAR(128) COMMENT '存放位置', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 COMMENT '逻辑删除', UNIQUE KEY uk_asset_code (asset_code), KEY idx_category (category_id), KEY idx_status (status) ) COMMENT '资产主表';字段取舍上有几个关键点。asset_code必须是业务唯一键,不能只靠自增 id,因为盘点时扫码、贴标签用的都是这个编码。status用状态机管理,不要用多个布尔字段拼,否则"在库且维修中"这种矛盾状态迟早出现。deleted走逻辑删除,资产数据物理删了,历史折旧记录就成了孤儿。
2.2 领用与调拨记录表:历史追溯的关键
领用、调拨、归还本质上是同一类"资产流转事件",我一般用一张asset_flow表统一记录,用flow_type区分,而不是拆成三张结构几乎一样的表。
CREATE TABLE asset_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, asset_id BIGINT NOT NULL COMMENT '资产ID', flow_type TINYINT NOT NULL COMMENT '1领用 2归还 3调拨 4维修 5报废', from_user BIGINT COMMENT '原使用人/部门', to_user BIGINT COMMENT '新使用人/部门', from_dept BIGINT, to_dept BIGINT, operator BIGINT NOT NULL COMMENT '操作人', flow_time DATETIME NOT NULL COMMENT '发生时间', remark VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_asset (asset_id, flow_time), KEY idx_to_user (to_user) ) COMMENT '资产流转记录';查询"某资产当前在谁手上",就是取该资产flow_time最新的一条记录;查询"某人名下所有资产",就是按to_user分组取每人每资产的最新记录。这种设计的好处是任何时点的资产归属都能还原,代价是查询要写子查询或窗口函数,MySQL 8 用ROW_NUMBER()很顺手。
2.3 折旧记录表与计提逻辑
折旧是固定资产管理里最容易翻车的部分。源码里如果只存一个"当前净值"字段,那历史月份的折旧明细就丢了,财务对账时你拿不出依据。
CREATE TABLE asset_depreciation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, asset_id BIGINT NOT NULL, period VARCHAR(7) NOT NULL COMMENT '折旧期间 yyyy-MM', depreciation DECIMAL(12,2) NOT NULL COMMENT '本期折旧额', accumulated DECIMAL(12,2) NOT NULL COMMENT '累计折旧', net_value DECIMAL(12,2) NOT NULL COMMENT '期末净值', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_asset_period (asset_id, period) ) COMMENT '折旧明细';直线法月折旧额 = (原值 - 残值) / 使用月数,每月计提一次,uk_asset_period保证同一资产同一期间不会重复计提。这里有个血泪经验:折旧计提任务必须幂等,定时任务重跑、手动补提都不能产生重复记录,靠的就是这个唯一键加INSERT ... ON DUPLICATE KEY UPDATE。
3. 用 Spring Boot + MyBatis-Plus 把资产 CRUD 和折旧跑通
模型定好了,接下来是让系统真正动起来。这一章给的是能抄的代码骨架,重点在 MyBatis-Plus 的用法和折旧任务的实现,不是把整个 Controller 贴一遍。
3.1 实体类与 MyBatis-Plus 映射
MyBatis-Plus 的实体类映射是这套源码里最省事的部分,但字段类型和数据库要对齐,否则DECIMAL映射成Double会出现精度丢失,金额算着算着就差几分钱。
@Data @TableName("asset") public class Asset { @TableId(type = IdType.AUTO) private Long id; private String assetCode; private String assetName; private Long categoryId; private String spec; @TableField("purchase_date") private LocalDate purchaseDate; // 金额一律用 BigDecimal,禁止 Double private BigDecimal purchasePrice; private BigDecimal originalValue; private BigDecimal salvageValue; private Integer usefulLife; private String depreciationMethod; private Integer status; private String location; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableLogic private Integer deleted; }@TableLogic让 MyBatis-Plus 自动在查询里拼deleted = 0,删除时改成UPDATE ... SET deleted = 1,不用手写。@TableField(fill = ...)配合MetaObjectHandler自动填充时间字段,省掉每个 Service 里手动 set 的重复代码。金额字段用BigDecimal是硬规矩,Double在累加折旧时误差会累积。
3.2 折旧计提的定时任务实现
折旧计提是这套系统里唯一需要"跑批"的地方,也是最容易出问题的地方。下面是一个幂等计提的实现思路。
@Service @RequiredArgsConstructor public class DepreciationService { private final AssetMapper assetMapper; private final AssetDepreciationMapper depreciationMapper; /** * 计提指定期间折旧,幂等:重复执行不会产生重复记录 * @param period 格式 yyyy-MM */ @Transactional(rollbackFor = Exception.class) public void calculate(String period) { // 1. 查出所有未报废、已启用折旧的资产 List<Asset> assets = assetMapper.selectList( new LambdaQueryWrapper<Asset>() .ne(Asset::getStatus, 3) // 排除报废 .isNotNull(Asset::getUsefulLife) ); for (Asset asset : assets) { // 2. 已计提期数 = 该资产已有折旧记录数 Long donePeriods = depreciationMapper.selectCount( new LambdaQueryWrapper<AssetDepreciation>() .eq(AssetDepreciation::getAssetId, asset.getId()) ); if (donePeriods >= asset.getUsefulLife()) { continue; // 已提足,跳过 } // 3. 直线法月折旧额,保留2位,四舍五入 BigDecimal monthly = asset.getOriginalValue() .subtract(asset.getSalvageValue()) .divide(new BigDecimal(asset.getUsefulLife()), 2, RoundingMode.HALF_UP); // 4. 累计折旧与净值 BigDecimal accumulated = depreciationMapper.sumAccumulated(asset.getId()); accumulated = accumulated == null ? monthly : accumulated.add(monthly); BigDecimal netValue = asset.getOriginalValue().subtract(accumulated); // 5. 唯一键冲突则更新,保证幂等 AssetDepreciation record = new AssetDepreciation(); record.setAssetId(asset.getId()); record.setPeriod(period); record.setDepreciation(monthly); record.setAccumulated(accumulated); record.setNetValue(netValue); depreciationMapper.insertOrUpdate(record); } } }逻辑说明:先排除报废资产,再判断是否已提足年限,避免超提。月折旧额用divide(..., 2, RoundingMode.HALF_UP)明确保留两位,不写这个参数 BigDecimal 遇到除不尽会抛异常。insertOrUpdate依赖uk_asset_period唯一键,重复执行时更新而非插入,这就是幂等的落点。
参数说明:period由调度器传入,格式固定yyyy-MM;usefulLife单位是月,如果业务上按年录入,入库时要乘 12。定时任务建议放在每月最后一天凌晨,用@Scheduled(cron = "0 0 2 L * ?"),但要注意 L 在部分 cron 实现里不支持,稳妥做法是每天跑一次、判断是否月末。
3.3 资产编码生成与并发安全
资产编码如果设计成ZC + 年月 + 4位流水,高并发下用SELECT MAX + 1必然重复。常见做法是用数据库自增序列或 Redis 的INCR,我一般用 Redis 按天做 key。
public String generateAssetCode() { String day = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); String key = "asset:code:" + day; Long seq = redisTemplate.opsForValue().increment(key); // 首次设置过期,避免 key 无限堆积 if (seq != null && seq == 1L) { redisTemplate.expire(key, Duration.ofDays(2)); } return "ZC" + day + String.format("%04d", seq); }increment是原子操作,多实例部署也不会重号。过期时间设两天,是因为跨天零点附近可能有补录,留个缓冲。如果 Redis 不可用,降级方案是数据库唯一键兜底,插入冲突时重试,但重试次数要限制,否则高并发下会打满连接池。
4. 盘点、权限与报表:让系统真正被行政和财务用起来
CRUD 跑通只是及格线,固定资产管理源码(java)能不能被真正用起来,取决于盘点好不好操作、权限细不细、报表出不出得来。这三块是行政和财务最在意的。
4.1 盘点任务的状态流转
盘点不是一次性动作,而是一个有状态的任务:创建任务 → 生成盘点明细 → 扫码/录入实盘 → 差异确认 → 任务关闭。盘点明细表要记录"账面状态"和"实盘状态"两个字段,差异才有依据。
CREATE TABLE inventory_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_name VARCHAR(128) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0进行中 1已完成 2已取消', creator BIGINT NOT NULL, start_time DATETIME, end_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE inventory_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, asset_id BIGINT NOT NULL, book_status TINYINT COMMENT '账面状态', real_status TINYINT COMMENT '实盘状态,null表示未盘', real_location VARCHAR(128), checker BIGINT COMMENT '盘点人', check_time DATETIME, diff_remark VARCHAR(500), UNIQUE KEY uk_task_asset (task_id, asset_id) );uk_task_asset保证同一任务里一台资产只有一条明细,扫码重复提交时更新而非新增。real_status为 null 表示还没盘到,任务关闭前要校验是否还有未盘明细,否则盘点就是走过场。
4.2 行级权限:不同部门只能看自己的资产
固定资产管理里权限是刚需:普通员工只能看自己名下资产,部门管理员看本部门,财务和超管看全部。用 Spring Security 或 Sa-Token 做角色控制只是第一层,真正的难点是行级权限——同一个接口,不同人看到的数据范围不同。
常见做法是在 MyBatis-Plus 里用DataPermissionInterceptor或手写@DataScope注解,在 SQL 拼接时自动加上部门条件。核心思路是:从当前登录用户解析出可见部门列表,拼到WHERE dept_id IN (...)里。
// 简化的数据权限拼接,实际用 MyBatis 拦截器实现 public void applyDataScope(LambdaQueryWrapper<Asset> wrapper, LoginUser user) { if (user.isSuperAdmin()) { return; // 超管不加限制 } List<Long> deptIds = user.getVisibleDeptIds(); if (CollectionUtils.isEmpty(deptIds)) { wrapper.eq(Asset::getId, -1L); // 无权限,查不到任何数据 return; } wrapper.in(Asset::getDeptId, deptIds); }注意deptIds为空时不能直接 return,否则等于放开了全部权限,这是行级权限最常见的翻车点。要么拼一个永远不成立的条件,要么直接抛无权限异常。
4.3 资产报表的三种典型输出
财务要的报表和行政要的不是一回事。常见三类:资产明细台账(按分类、部门、状态筛选)、折旧汇总表(按期间汇总折旧额)、盘点差异表(账面与实盘对比)。报表不建议在 Java 里循环拼,数据量大时直接写 SQL 聚合,导出用 EasyExcel 流式写,避免 OOM。
-- 按部门+分类汇总资产原值与净值 SELECT d.dept_name, c.category_name, COUNT(a.id) AS asset_count, SUM(a.original_value) AS total_original, SUM(a.original_value - IFNULL(dep.accumulated, 0)) AS total_net FROM asset a LEFT JOIN sys_dept d ON a.dept_id = d.id LEFT JOIN asset_category c ON a.category_id = c.id LEFT JOIN ( SELECT asset_id, SUM(depreciation) AS accumulated FROM asset_depreciation GROUP BY asset_id ) dep ON dep.asset_id = a.id WHERE a.deleted = 0 GROUP BY d.dept_name, c.category_name;子查询先聚合折旧再关联,比在 SELECT 里写相关子查询快得多。数据量上百万时,asset_depreciation的asset_id索引必须建,否则这个子查询会全表扫。
5. 固定资产管理源码(java)落地避坑:5 个真实踩过的坑
这套系统从能跑到好用,中间隔着一堆坑。下面 5 条是我和同行反复踩过的,每条按现象、原因、解决写。
坑一:折旧金额对不上,差几分钱。现象是财务对账时发现系统净值比手工算的少几毛。原因是实体类金额字段用了Double,累加时浮点误差累积。解决是把所有金额字段改成BigDecimal,数据库用DECIMAL(12,2),除法必须指定精度和舍入模式,别用默认的divide。
坑二:盘点任务关闭后还能改明细。现象是任务已标记完成,但有人还能提交盘点结果,导致差异表前后不一致。原因是状态校验只在前端做,后端接口没拦。解决是在 Service 层对status != 0的任务直接抛异常,前端校验只是体验,后端校验才是底线。
坑三:资产编码重复,贴标签时发现两台同码。现象是并发导入或批量创建时出现重复编码。原因是用了SELECT MAX(code) + 1这种非原子操作。解决是改用 RedisINCR或数据库序列,同时asset_code上加唯一索引兜底,冲突时捕获异常重试。
坑四:逻辑删除后,关联的折旧记录成了孤儿。现象是资产删了,但折旧汇总表里还有它的数据,报表总数对不上。原因是逻辑删除只改了asset.deleted,没处理关联表。解决是查询折旧时统一 joinasset并过滤deleted = 0,或者删除资产时级联标记关联记录。
坑五:定时任务重跑导致折旧翻倍。现象是运维手动补跑一次计提任务,某资产当月折旧变成两倍。原因是计提逻辑没做幂等,直接INSERT。解决是asset_depreciation加(asset_id, period)唯一键,用INSERT ... ON DUPLICATE KEY UPDATE,重跑只更新不新增。
提示:这五条里,金额精度和幂等是最容易在测试环境漏掉、上线后才爆的,建议在开发阶段就写单元测试覆盖。
6. 从能跑到好用:资产二维码与批量导入的两个进阶技巧
系统跑通之后,真正拉开体验差距的是两个细节:资产标签怎么贴、历史数据怎么迁。这两件事做不好,行政用两天就回去用 Excel 了。
先说二维码。资产入库后要生成标签,扫码能直接跳到该资产的详情或盘点页面。常见做法是用 ZXing 生成二维码,内容不是纯 asset_code,而是一个带业务前缀的短链或深链,比如asset://detail/12345,这样扫码 App 能直接路由。生成时注意纠错级别选M或Q,标签磨损后还能扫出来;尺寸别小于 2cm×2cm,太小打印机打不清。批量生成时用BufferedImage循环会吃内存,建议边生成边写 ZIP 流,别一次性全放内存。
// 批量生成资产二维码,边生成边写 ZIP,避免 OOM public void exportQrZip(List<Asset> assets, OutputStream out) throws IOException { try (ZipOutputStream zos = new ZipOutputStream(out)) { for (Asset asset : assets) { String content = "asset://detail/" + asset.getId(); BitMatrix matrix = new MultiFormatWriter().encode( content, BarcodeFormat.QR_CODE, 200, 200, Map.of(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M) ); zos.putNextEntry(new ZipEntry(asset.getAssetCode() + ".png")); MatrixToImageWriter.writeToStream(matrix, "PNG", zos); zos.closeEntry(); } } }参数上,ERROR_CORRECTION选M是清晰度和容错的平衡点,H虽然更抗损但码点更密,小标签反而扫不出。200×200是打印尺寸的像素基准,实际打印按 300dpi 换算。
再说批量导入。历史台账从 Excel 迁进来,最大的坑不是解析,而是脏数据:编码重复、分类不存在、日期格式五花八门。我的习惯是先做"预校验"再入库,把错误行号和数据一起返回给用户,让他改完再传,而不是边导边报错、导一半卡住。用 EasyExcel 的ReadListener逐行读,校验通过才落库,整批用事务包住,任何一行失败就整体回滚,避免半截数据。
最后说个我自己的习惯:这套源码拿到手,我不会急着改业务,而是先跑一遍折旧计提和盘点流程,把这两个最容易出问题的链路摸清楚,再动其他模块。固定资产管理系统的价值不在功能多,而在数据准、历史清、对得上账。希望帮到你。
本文还有配套的精品资源,点击获取