
简介基于RFID技术的固定资产管理系统Java源码面向有资产管理数字化需求的企业及JavaMySQL开发者。系统覆盖资产入库、领用、借用、维修、报废、归还、报表等全流程支持PC、手机、飞书三端接入并实现RFID批量盘点帮助企业提升固定资产管理效率。资源为zip压缩包共772个文件大小约4.06MB其中317个java文件承载后端业务180个html及配套js/css构成前端界面38个xml与yml为配置另有数据库sql脚本和启动脚本。已有3020人学习下载。开发者可获得可直接运行的完整项目演示环境附测试账号便于体验核心功能代码分层清晰可参考其RFID集成、多端接口及报表模块设计适合用于企业资产系统二次开发或Java实战学习。1. 资产管理系统源码到手后先搞清它到底替你解决了什么做 Java 开发的迟早会接到一个活给公司或客户做资产管理系统。你以为是 CRUD真正做起来发现资产状态流转、盘点差异、折旧计算、权限隔离全是坑。这套 Java 资产管理源码核心就是把「资产从入库到报废」的完整生命周期和 RFID 盘点场景做成了一套可运行的后端工程适合拿来直接改造成自己的业务底座。它解决的痛点很具体资产编号怎么生成、领用归还是走状态机还是硬更新、RFID 盘点数据怎么批量入库不重复。适合两类人一类是刚入职需要快速交付项目的初中级 Java 工程师另一类是在选型阶段想找参考实现的架构师。先明确一点这不是那种只有一个 README 的玩具工程它把资产领域最常见的业务闭环都串起来了。2. 技术选型与目录结构为什么这套 Java 组合能扛住资产数据2.1 Spring Boot MyBatis Plus 的组合逻辑资产管理系统本质上仍然是企业级 CRUD 应用但资产数据有两个特点字段多、状态变化频繁。一个资产从采购入库开始要经历领用、归还、维修、调拨、报废每个环节都要记录操作人和时间。用 Spring Boot 做容器管理、MyBatis Plus 做 ORM是当前 Java 资产管理开源项目里最常见的技术组合不是因为它新而是因为它稳。MyBatis Plus 的价值在于内置了逻辑删除、自动填充、分页插件这些功能正好命中资产管理系统的需求。比如资产删除通常不是物理删除而是逻辑删除MyBatis Plus 的TableLogic注解直接搞定不用自己写UPDATE asset SET deleted 1 WHERE id ?。分页插件对资产列表这种高频查询也很有用资产过万条以后前端表格必须靠分页撑住。2.2 工程目录怎么读从 controller 到 mapper 的调用链拿到源码先别急着跑先把包结构看明白。这套工程用的是标准的分层架构com.company.asset ├── controller // 接收 HTTP 请求做参数校验 ├── service // 业务逻辑层资产状态流转在这里 ├── mapper // MyBatis Plus 的 Mapper 接口 ├── entity // 数据库实体类 ├── dto // 前端传入的参数对象 ├── vo // 返回给前端的视图对象 ├── config // 配置类包括 MybatisPlusConfig、RedisConfig ├── common // 统一返回结果、异常处理、工具类 ├── rfid // RFID 盘点相关逻辑单独成包 └── task // 定时任务比如资产折旧计算调用链是标准的Controller - Service - Mapper但有两个地方值得注意。第一rfid包是独立出来的说明盘点逻辑和普通 CRUD 是解耦的这个设计在二次开发时很省事。第二task包里放着折旧计算定时任务资产管理系统如果不做折旧那和普通库存系统没区别这块逻辑通常藏在 Service 层里它单独拆包说明作者把资产领域的特有逻辑放在了一等位置。2.3 关键配置项数据源、Redis、文件上传路径application.yml里有几组配置需要根据自己的环境改否则启动必挂。数据源配置是第一个要动的资产管理系统一般用 MySQL注意时区参数MySQL 8.0 必须加serverTimezoneAsia/Shanghai否则查询时间字段会差 8 小时。spring: datasource: url: jdbc:mysql://localhost:3306/asset_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0Redis 在这套系统里承担的角色是缓存资产编号递增器和登录态。资产编号不能用数据库自增主键替代因为资产编号有业务含义比如ZC-2024-0001这个格式必须用 Redis 的 INCR 命令配合日期前缀生成。文件上传路径配置也要改资产照片和采购合同附件会传到本地磁盘默认配置是/data/asset/uploadWindows 环境下要改成D:/asset/upload否则上传接口报FileNotFoundException。3. 资产全生命周期核心代码入库、领用、归还、维修、报废3.1 资产表设计与状态机资产管理系统的数据库设计核心是一张asset主表和一张asset_record流水表。主表存资产当前状态流水表存每一次操作记录。这个设计是行业的常见做法也是这套源码里最有复用价值的部分。很多人第一次做资产系统只建一张表字段改了直接 UPDATE结果审计的时候什么都查不到。CREATE TABLE asset ( id bigint(20) NOT NULL AUTO_INCREMENT, asset_no varchar(32) NOT NULL COMMENT 资产编号, name varchar(128) NOT NULL COMMENT 资产名称, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1-在库 2-已领用 3-维修中 4-已报废, user_id bigint(20) DEFAULT NULL COMMENT 当前使用人, purchase_price decimal(10,2) DEFAULT NULL COMMENT 采购原价, purchase_date date DEFAULT NULL COMMENT 采购日期, rfid_code varchar(64) DEFAULT NULL COMMENT RFID标签编号, deleted tinyint(1) NOT NULL DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_asset_no (asset_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;状态字段status是整个系统的核心所有业务操作都要围绕状态机来校验。常见的做法是入库时状态为在库领用时校验必须是「在库」才能变更为「已领用」归还时校验必须是「已领用」才能回到「在库」。维修和报废同理。这套源码在 Service 层写了一个状态流转校验方法每次更新状态前先查当前状态不匹配直接抛业务异常。3.2 资产入库与领用的 service 实现资产入库的业务逻辑里有一个容易忽略的坑入库操作要做幂等处理。前端可能会因为网络超时重复提交表单导致同一批资产入库两次。源码里的处理方式是先验资产编号是否已存在存在则直接返回错误。Override Transactional(rollbackFor Exception.class) public Long addAsset(AssetAddDTO dto) { // 1. 生成资产编号ZC 日期 Redis序号 String assetNo assetNoGenerator.generate(ZC); // 2. 校验资产编号唯一性防止并发重复 long count this.lambdaQuery() .eq(Asset::getAssetNo, assetNo) .count(); if (count 0) { throw new BusinessException(资产编号重复请重试); } // 3. 保存资产主表 Asset asset new Asset(); BeanUtils.copyProperties(dto, asset); asset.setAssetNo(assetNo); asset.setStatus(AssetStatus.IN_STOCK.getCode()); this.save(asset); // 4. 写入资产操作流水 AssetRecord record new AssetRecord(); record.setAssetId(asset.getId()); record.setAssetNo(assetNo); record.setOperateType(OperateType.IN_STOCK.getCode()); record.setOperateUserId(LoginUtil.getUserId()); assetRecordMapper.insert(record); return asset.getId(); }这段代码里关键是Transactional注解和assetNoGenerator配合。事务保证资产主表和流水表要么同时成功要么同时失败不会出现主表有数据但流水丢失的情况。资产编号生成器用的是 Redis INCR但这里有一个并发隐患步骤 2 的重复校验在极端并发下可能同时通过因为count()查询和save()不是原子操作。真正稳妥的做法是依靠数据库的唯一索引兜底所以建表语句里加了UNIQUE KEY uk_asset_no。领用操作的逻辑正好反过来先校验资产状态再更新主表最后插流水。Override Transactional(rollbackFor Exception.class) public void receiveAsset(Long assetId, Long userId) { Asset asset this.getById(assetId); if (asset null || asset.getDeleted() 1) { throw new BusinessException(资产不存在); } // 关键校验只有“在库”状态才能被领用 if (!AssetStatus.IN_STOCK.getCode().equals(asset.getStatus())) { throw new BusinessException(当前资产状态不可领用); } asset.setStatus(AssetStatus.RECEIVED.getCode()); asset.setUserId(userId); this.updateById(asset); // 记录流水 AssetRecord record new AssetRecord(); record.setAssetId(assetId); record.setAssetNo(asset.getAssetNo()); record.setOperateType(OperateType.RECEIVE.getCode()); record.setOperateUserId(userId); assetRecordMapper.insert(record); }这里最值得借鉴的是状态校验前置。很多初写资产系统的人会漏掉这一步直接updateById改状态结果就会出现「一台已经报废的电脑被领用了」这种离谱数据。状态机的本质就一句话不是你想改成什么状态就改而是当前状态允许流转到什么状态才能改。3.3 资产报废与折旧逻辑删除背后那块遮羞布资产报废是生命周期里最容易写错的功能。很多人的第一反应是删掉记录但资产业务不允许物理删除因为财务审计要留痕。源码里的报废操作是改变状态同时把资产从在用列表里隐藏掉。Override Transactional(rollbackFor Exception.class) public void scrapAsset(Long assetId, String reason) { Asset asset this.getById(assetId); if (asset null) { throw new BusinessException(资产不存在); } if (!AssetStatus.RECEIVED.getCode().equals(asset.getStatus()) !AssetStatus.IN_STOCK.getCode().equals(asset.getStatus())) { throw new BusinessException(维修中资产不能报废); } asset.setStatus(AssetStatus.SCRAPPED.getCode()); asset.setScrapReason(reason); asset.setScrapTime(new Date()); this.updateById(asset); }注意报废校验的两个允许状态在库和已领用。维修中的资产不能报废因为可能还有未完成的维修单关联着这种约束就是状态机比自由更新强的地方。折旧这块用的是定时任务每天晚上跑一次按年限平均折旧法计算资产当前净值结果存到asset_depreciation表做资产盘点的时候可以直接取净值。4. RFID 盘点实战从扫码枪到数据库的完整链路4.1 盘点模块的三种接入方式RFID 是这套源码的亮点模块。资产管理系统做到中期客户一定会提一个需求能不能拿扫码枪走一圈就把资产盘了这就是 RFID 盘点的使用场景。源码里抽象了RfidReaderService接口支持三种接入方式我在实际项目里也都遇到过。public interface RfidReaderService { /** * 连接读写器 */ boolean connect(RfidReaderConfig config); /** * 读取标签返回一批 RFID 标签码 */ ListString readTags(); /** * 断开连接 */ void disconnect(); }第一种是有源读写器通过 TCP 直连服务器读写器扫描到的标签实时上报适合仓库门口这种固定点位。第二种是手持 PDA 离线采集管理员拿着设备走一圈采集完成后把标签列表导入系统。第三种是通过串口连接桌面式读写器适合桌面级的批量读取。这套源码默认实现的是第二种因为手持 PDA 是实际项目里最常用的形态不需要布线拿着就走。4.2 手持机批量上报接口的幂等设计盘点数据上报有一个隐藏问题手持机可能在同一批数据里重复上报也可能因为网络原因重传。如果直接循环插库盘点记录表会重复。源码的处理方式是上报接口整体做幂等用批次号batch_no做唯一约束。PostMapping(/rfid/report) public RString report(RequestBody RfidReportDTO dto) { // dto.batchNo: 手持机生成的批次号UUID // dto.tagList: RFID标签码列表 String batchNo dto.getBatchNo(); // 批次号已存在则直接返回成功 Integer count rfidBatchMapper.selectCount( new LambdaQueryWrapperRfidBatch() .eq(RfidBatch::getBatchNo, batchNo)); if (count ! null count 0) { return R.ok(该批次已上报跳过); } rfidService.processBatch(dto); return R.ok(处理完成); }这个设计的巧妙之处在于用批次号做天然幂等键而不是查标签列表是否重复。因为一次盘点可能扫到几千个标签逐个查重效率太低用批次号挡住整批数据效率高很多。RfidBatch表记录批次信息盘点单ID、上报时间、标签数量RfidTagDetail表记录该批次下的每个标签明细。4.3 盘点差异报告盘盈盘亏怎么生成盘点的最终输出是一份差异报告。RFID 扫到的标签要和数据库里的资产做匹配匹配不上的就是盘亏账上有实物没有数据库里有但没扫到的是盘亏账上有实物找不到。源码里在RfidService中实现了一个比对方法。public RfidDiffReport compareDiff(Long inventoryPlanId, ListString scannedTags) { // 1. 查资产表里所有 rfid_code 不为空的在用资产 ListAsset assets assetMapper.selectList( new LambdaQueryWrapperAsset() .isNotNull(Asset::getRfidCode) .ne(Asset::getStatus, AssetStatus.SCRAPPED.getCode())); // 2. 把扫描到的标签转成 Set 方便比对 SetString scannedSet new HashSet(scannedTags); // 3. 遍历资产找出未扫到的 ListString missingTags new ArrayList(); for (Asset asset : assets) { if (!scannedSet.contains(asset.getRfidCode())) { missingTags.add(asset.getRfidCode()); } } // 4. 扫描到的标签里数据库没有的算盘盈 SetString dbTags assets.stream() .map(Asset::getRfidCode) .collect(Collectors.toSet()); ListString extraTags scannedTags.stream() .filter(tag - !dbTags.contains(tag)) .collect(Collectors.toList()); // 5. 组装差异报告 RfidDiffReport report new RfidDiffReport(); report.setMissingTags(missingTags); // 盘亏 report.setExtraTags(extraTags); // 盘盈 return report; }这段比对逻辑有一个注意点过滤条件里加了ne(Asset::getStatus, AssetStatus.SCRAPPED.getCode())报废资产不参与盘点比对。如果不加这个过滤报废但没撕掉 RFID 标签的资产会被算成盘亏每次盘点都报差异这就是常见的数据噪声。实际用的时候还要注意标签码的大小写不同厂商的读写器返回的标签格式可能不同建议统一转大写再比对。5. 部署与避坑清单从本地跑通到上线这五天最容易翻车的地方5.1 本地环境快速启动Maven 与 MySQL 参数先把环境跑起来再研究代码这是最快的学习路径。项目用 Maven 管理依赖JDK 版本要求 1.8 以上。启动前先建数据库源码目录下通常有sql/init.sql如果没找到直接用前面给出的建表语句也能跑。# 1. 初始化数据库 mysql -uroot -p sql/init.sql # 2. 修改 application.yml 里的数据库密码和 Redis 地址 # 3. 启动 RedisWindows 下直接双击 redis-server.exe # 4. 编译并启动 mvn clean package -DskipTests java -jar target/asset-management-system.jar启动成功后访问http://localhost:8080/api/health看健康检查接口。如果端口被占用在application.yml里改server.port。后端默认端口是 8080前端开发环境下 Vite 代理会转发到 8080改成其他端口记得同步改前端代理配置。5.2 避坑清单6 条真实踩坑记录坑 1MySQL 8.0 驱动导致启动报 Public Key Retrieval 错误现象项目启动时数据源初始化失败报错Public Key Retrieval is not allowed。原因MySQL 8.0 默认使用 caching_sha2_password 认证JDBC 连接时默认不允许客户端从服务器获取公钥。解决连接串加参数allowPublicKeyRetrievaltrueuseSSLfalse或者把连接串改成jdbc:mysql://localhost:3306/asset_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。这两个参数是 MySQL 8.0 环境下的标配不加必出错。坑 2Redis 未启动导致资产编号接口报错现象调用新增资产接口报JedisConnectionException: Could not get a resource from the pool。原因资产编号生成强依赖 Redis INCRRedis 没启动或者 IP 配置不对。解决本地开发先启动 Redis检查spring.redis.host和port。另外注意 Redis 密码如果有密码没配连接也会失败。坑 3逻辑删除字段导致自定义 SQL 查询结果错乱现象写了一个自定义 SQL 联查资产的领用记录查出了已删除的数据。原因MyBatis Plus 的TableLogic只在它的内置方法里生效自己手写的 SQL 如果没加deleted 0条件逻辑删除就成了摆设。解决自定义 SQL 里手动拼接AND a.deleted 0。血泪经验凡是用了逻辑删除的表所有自定义 SQL 都要检查一遍有没有过滤 deleted 字段。坑 4状态机校验漏了一个入口导致资产从维修直接变成报废被拦截现象测试人员把维修中的资产点报废接口返回成功但列表里资产消失了。原因报废方法的校验逻辑没有加拦截器或者 AOP 统一处理只校验了「非报废状态」没校验「维修中不可报废」。解决把状态流转校验抽出来做成一个StateMachineValidator在 Service 层所有变更状态的方法入口统一调用。这个做法推荐直接抄进自己的项目。坑 5批量导入 Excel 时内存溢出现象导入一万条资产记录的 Excel后端 OOM。原因EasyExcel 默认一次性把整个 Sheet 读进内存一万条可能不到 OOM 的量级但资产表字段多一条记录十几个字段对象数量上去以后内存吃紧。解决改用 EasyExcel 的监听器模式逐行读取每读 1000 条就手动清一次List。这套源码里已经实现了AssetImportListener注意看它的invoke方法里有没有做内存释放。坑 6RFID 标签码在数据库里带空格导致盘点匹配不上现象手持机扫到的标签和数据库里的标签一致但比对结果全部显示盘亏。原因导入标签时没有 trim数据库里存了空格前端展示看不出来比对时Set.contains()因为空格差异返回 false。解决入库时统一做tag.trim()比对前也做一次 trim 和 toUpperCase。这类问题排查起来非常玄学因为肉眼看不出来得靠打印日志比对长度才能发现。6. 把这套源码用得更顺手Excel 批量导入、缓存刷新与消息通知资产管理系统上线以后最常用的操作反而不是单个录入而是 Excel 批量导入。运维部门手里本来就有一张老系统的资产台账几千条数据需要一次性迁过来。源码里用了 EasyExcel 的监听器模式做导入但有几个细节做得不够我会在二次开发时改掉。第一个是导入模板校验。默认实现只校验了几个必填字段资产分类这类外键关联字段没有提前做合法性校验导致导入结束后日志里一堆外键错误。我的习惯是在AssetImportListener的invoke方法里先把分类名称转换成分类 ID转换失败的记录直接进错误列表而不是整体回滚。Override public void invoke(AssetExcelData data, AnalysisContext context) { // 同一批次共用一个缓存避免每行都查一次数据库 MapString, Long categoryCache new HashMap(); Long categoryId categoryCache.computeIfAbsent( data.getCategoryName(), name - categoryService.getIdByName(name) ); if (categoryId null) { errors.add(第 context.getCurrentRowIndex() 行分类不存在: data.getCategoryName()); return; } Asset asset new Asset(); BeanUtils.copyProperties(data, asset); asset.setCategoryId(categoryId); asset.setStatus(AssetStatus.IN_STOCK.getCode()); assets.add(asset); // 每 1000 条批量插入一次防止 List 无限增长 if (assets.size() 1000) { saveBatch(); } }用computeIfAbsent做缓存是个省事技巧分类名称到 ID 的映射只需要在第一次出现时查库后面全部走内存。导入数据量大时这个方法能把耗时缩短一半以上。第二个要改的是资产编号生成的并发场景。Redis INCR 本身是原子的但如果 Redis 重启递增序列会从 1 开始如果数据库已有ZC-2024-0001新生成的编号就是ZC-2024-0001唯一索引直接炸掉。我遇到过一次修复方式是在生成器里先查数据库最大值再对比 Redis 当前值取较大者作为起始值。从那以后我每次做资产系统都要强制走一遍边界检查编号生成会不会撞唯一索引、状态流转有没有漏拦截、逻辑删除有没有污染自定义 SQL。这套源码本身不算完美但它的价值在于把资产管理系统最核心的坑都踩过一遍并且给出了解决方案你拿到手改一改就能用于生产。希望帮到你。本文还有配套的精品资源点击获取