简介基于RFID的简易图书管理系统是一套面向C#/.NET初学者的课程设计与实战源码包展示如何通过无线射频识别技术实现图书馆的自动借书和还书流程。整个项目在Visual Studio 2015环境下开发数据库采用SQL Server代码中划分了图书管理、图书借阅、读者管理三个核心模块适合正在做课程设计或需要快速入门WinForms开发的读者参考。压缩包共77个文件大小约774KB主要包含C#窗体与业务逻辑源码.cs、Visual Studio解决方案.sln、界面资源与图标.resx/.png/.ico、可执行文件.exe以及SQL数据库脚本.sql既可以直接运行体验也能对照源码进行二次开发。已有4101人下载学习整体结构清晰编译运行门槛低。除完整工程外还附带了数据库建库脚本以及窗体设计素材读者可以对照代码理解RFID读卡器数据读取、借还书表单逻辑和数据库表关系对于完成图书管理系统类课设或学习C#SQL Server集成开发都很有帮助。1. 基于RFID的简易图书管理系统不用扫码枪卡片贴上就能借书很多小型图书馆、班级图书角、社区借阅点还在用“手写登记 Excel 表格”管理图书借出一本书要翻台账、查状态归还时又得核对半天。这学期我接手了一个模拟项目X用 RFID 卡片代替条形码借书时把卡往读卡器上一贴系统自动识别图书编号、读取读者信息、完成借阅登记全程不到两秒。这套基于RFID的简易图书管理系统技术栈是 Java Swing MySQL RC522 射频模块代码量不大但链路完整从硬件读卡、串口通信到业务入库全都覆盖特别适合拿来学硬件与业务系统如何对接也适合做物联网课程的课程设计。我把它完整拆了一遍下文按「架构 → 核心代码 → 运行避坑 → 改造方向」的顺序展开想跑起来的照着做就行。2. 架构设计与技术选型Swing MySQL RC522 的分层实现2.1 项目功能边界与整体分层先看这个系统解决什么问题。核心角色有两类图书管理员和读者。管理员负责录入图书、挂接 RFID 标签、处理借还、查看逾期记录读者能查书、借书、还书。功能很收敛不覆盖预约、续借、罚款等扩展场景这就让代码结构可以保持简单清晰适合作为学习样本。从架构上看项目分了四层硬件接入层、数据访问层、业务逻辑层、界面展示层。硬件接入层封装对 RC522 读写器的操作把 RFID 卡的 UID唯一标识读取出来并翻译成图书编号数据访问层是纯 JDBC 操作 MySQL没有引入 MyBatis 或 Hibernate主要原因是不想增加依赖复杂度让初学的人能一览 SQL 和 Java 的交互方式业务逻辑层单独拎出来做借书、还书、状态校验界面层用 Swing 做桌面窗体保持单机应用的部署形态。这个分层的收益在后期调试时特别明显。我在排一个「图书被重复借出」的故障时只需要看业务逻辑层的方法而不用去翻界面的监听器代码。如果当初为了省事把 SQL 写在按钮点击事件里排查成本会高很多。给新手的建议哪怕系统再小也务必保持三层以上分离。2.2 技术选型为什么是 Swing、MySQL 和 RC522Swing 在 2024 年看起来不算时髦但在这种硬件演示类项目里反而是合理选择。它开箱即用JDK 自带不用搭前端工程对教学场景和课程设计来说部署成本远低于 Web 方案。如果你未来想改成 Web 端业务逻辑层和数据访问层是可以直接复用的改的只是界面层这正是分层架构带来的余地。数据库用 MySQL 8.x主要看中它的稳定性和资料丰富程度。实际建表用的是 InnoDB 引擎事务由业务逻辑层控制比如借书操作里「更新图书状态」和「插入借阅记录」绑在同一个事务里避免出现图书状态标记为借出但记录表里没有数据的不一致情况。RFID 部分用的是 RC522 射频模块工作频率 13.56MHz兼容 ISO 14443A 协议的卡最常见的配套卡是 Mifare1 S50 或 S70。RC522 模块本身不带 USB 接口通过排针与单片机连接但在这套系统里没有用单片机而是用了一套 USB 转串口的 RFID 读卡器。换句话说硬件采购时直接选「串口式 RFID 读写器」插上电脑就枚举成一个 COM 口Java 层通过串口库读写数据即可省去了写单片机固件的步骤。当时我差点买成需要单片机驱动的模块那会多出一条「串口协议转发」的工作量这点在采购时要特别留意。2.3 数据库设计三张表支撑起核心业务系统数据库名建议定为rfid_library包含三张表图书表、读者表、借阅记录表。图书表里有一个关键字段rfid_uid存的是 RFID 卡上读出来的唯一标识这个字段就是物理世界和系统世界的连接点。读者表同样有rfid_uid用来识别读者身份。借阅记录表负责关联图书和读者记录借出和归还的时间点。图书表的字段大致是book_id主键自增、rfid_uid VARCHAR(20) UNIQUE、book_name、author、isbn、location、status TINYINT。status 用 0 表示在馆、1 表示借出。为什么不用字符串存状态因为后续统计时写WHERE status 0比字符串比较更快而且改动状态含义不需要改表结构。借阅记录表有一条设计心得不要在记录里更新归还时间而是插入一条新记录来表示归还行为。也就是借出时插一条borrow_type 1的记录归还时再插一条borrow_type 2的记录这样可以完整保留历史轨迹。如果直接在原记录上更新return_time你想查「这本书在这个月被借过几次」就得靠状态倒推很容易漏。这是我个人习惯的做法。下面是建表脚本的核心部分CREATE DATABASE IF NOT EXISTS rfid_library DEFAULT CHARSET utf8mb4; USE rfid_library; CREATE TABLE book ( book_id INT PRIMARY KEY AUTO_INCREMENT, rfid_uid VARCHAR(20) NOT NULL UNIQUE, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), isbn VARCHAR(20), location VARCHAR(50), status TINYINT DEFAULT 0 COMMENT 0-在馆 1-借出 ); CREATE TABLE reader ( reader_id INT PRIMARY KEY AUTO_INCREMENT, rfid_uid VARCHAR(20) NOT NULL UNIQUE, reader_name VARCHAR(50) NOT NULL, phone VARCHAR(20) ); CREATE TABLE borrow_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_type TINYINT COMMENT 1-借出 2-归还, op_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_book (book_id), INDEX idx_reader (reader_id) );这段 SQL 里值得注意的有三点。第一图书和读者的rfid_uid都加了 UNIQUE 约束避免同一张卡被重复录入第二borrow_record没有设置外键这是刻意的因为演示项目经常要清空表重置数据外键反而成为负担数据完整性靠业务层保证第三op_time用DATETIME而不是TIMESTAMP因为TIMESTAMP有 2038 年问题虽然现在远着但养成好习惯不亏。跑建表脚本时如果报Unknown database说明 MySQL 连接串里没写useSSL或创建库权限不足按报错字面查即可。2.4 工程目录结构与启动类关系整个工程是标准 Maven 结构没有用 Spring Boot因为引入 Spring 容器对这个规模的项目来说是过度设计。源码包路径大致是com.demo.rfid下面按层分包ui放窗体类、service放业务逻辑、dao放 JDBC 操作、util放串口工具和常量配置。ui 包里的MainFrame是程序入口一个JFrame里嵌了多个JPanel通过CardLayout做借书、还书、图书管理几个页面的切换。我不建议把入口做成main方法里直接new MainFrame()就算完事更好的做法是在 main 里先初始化串口、探测读卡器是否存在再弹出主界面。这样如果硬件没插好程序会直接给出中文提示而不是打开窗口后点哪都没反应。在实践过程中我见过不少人把rfid_uid直接作为 book 表主键。这么做短期内没问题但一旦后期要支持「同一本书的多个副本各贴一张卡」主键就得重新设计。所以这里坚持用自增主键加逻辑唯一键给后续扩展留出空间。3. 核心模块拆解卡号读取到借阅状态流转的完整链路3.1 串口通信模块把硬件数据接进 Java 程序RFID 读卡器通过串口与电脑通信Java 侧没有内置串口支持需要引入第三方串口库。常见选择是jSerialComm它对 Windows/macOS/Linux 都友好没有 native 代码编译的坑。这里给出串口打开与读取的封装代码import com.fazecast.jSerialComm.SerialPort; import java.io.InputStream; public class RfidReader { private SerialPort port; private InputStream in; public boolean connect(String portName, int baudRate) { port SerialPort.getCommPort(portName); port.setBaudRate(baudRate); port.setNumDataBits(8); port.setNumStopBits(1); port.setParity(SerialPort.NO_PARITY); port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 1000, 0); boolean opened port.openPort(); if (opened) { in port.getInputStream(); } return opened; } public String readCardUid() throws Exception { byte[] buffer new byte[16]; int len in.read(buffer, 0, buffer.length); if (len 0) { return bytesToHex(Arrays.copyOf(buffer, len)); } return null; } private String bytesToHex(byte[] bytes) { StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02X, b)); } return sb.toString(); } }这段代码做的事就是打开串口、设置波特率、半阻塞读数据。这里的关键参数是TIMEOUT_READ_SEMI_BLOCKING它的含义是「有数据就立即返回没数据最多等 1000 毫秒」。如果选TIMEOUT_READ_BLOCKING模式程序会一直卡在 read 调用上后续所有按钮全部假死这是一次踩坑后才改过来的。不同读卡器返回的数据格式不同有的是纯 UID 十六进制有的是带帧头和校验位的协议帧。我手里的设备是「主动上传模式」卡片靠近时自动往串口推一包数据应用层只需要取出前 4 字节转成十六进制就是 UID。如果你的设备是「应答模式」需要先发一条指令再等返回那就在这个方法里补一步out.write(cmd)。这个封装最大的价值是把硬件差异挡在业务代码之外。业务层调readCardUid()时根本不知道背后是串口还是模拟器。在我的调试过程中曾用一个小程序模拟读卡器往串口发数据业务层完全无感这给联调省了巨大时间。3.2 借书逻辑事务边界与状态校验借书是整个系统最核心的链路管理员点击「开始借书」按钮读卡器侦测到读者的 RFID 卡识别出读者然后侦测到图书的 RFID 卡识别出图书最后确认借出修改图书状态并插入一条借阅记录。这里采用「两次读卡」的设计第一次读到的卡识别为读者第二次读到的卡识别为图书。这个交互逻辑清晰但有个天然缺陷如果管理员拿反了顺序把图书卡先贴上去系统会把它当成读者去查读者表大概率查不到。解决方案有两种一种是在界面上明确提示「请先刷读者卡」另一种是改用不同的数据标识。大多数情况下第一种就够了代码也在合理范围内。业务层的借书方法如下public boolean borrowBook(String readerUid, String bookUid) { if (readerUid null || bookUid null) return false; ReaderDao readerDao new ReaderDao(); BookDao bookDao new BookDao(); BorrowRecordDao recordDao new BorrowRecordDao(); Integer readerId readerDao.findIdByUid(readerUid); if (readerId null) { throw new BusinessException(未识别的读者卡请先登记); } Book book bookDao.findByUid(bookUid); if (book null) { throw new BusinessException(未找到对应图书请先录入); } if (book.getStatus() 1) { throw new BusinessException(这本书已经被借出); } // 事务边界 int connectionAutoCommit 0; // 实际项目中用 transactionTemplate 或手动 commit bookDao.updateStatus(book.getBookId(), 1); recordDao.insert(book.getBookId(), readerId, 1); return true; }这段代码的几条规则是在实际联调中总结出来的。第一先查读者再查图书顺序不能反因为图书状态变更依赖读者存在第二状态为 1 时直接抛异常不能覆盖修改否则会出现一本书同时被两个人持有的脏数据第三两个写操作必须在一个事务里updateStatus成功而insert失败时图书状态和实际借阅记录不一致后面查「谁借走了这本书」会出现黑洞。事务在简易系统里最容易翻车因为 JDBC 默认 autocommit 为 true每条 SQL 独立提交。我习惯用Connection手动setAutoCommit(false)控制写一段try { ... } catch { rollback } finally { close }的模板项目虽小但严谨性要有。如果你后续换成 Spring 事务注解这段代码逻辑保持不变只把事务管理方式替换掉即可。3.3 还书逻辑与逾期判断还书流程比借书简单刷一次图书卡即可把状态置为 0同时插入一条「归还」记录。但有一个环节值得展开逾期判断。逾期需要基于「应还日期」计算应还日期在借出记录里并不存在因为borrow_record表设计时只存了op_time。所以还书时怎么知道这本书该何时还在真实系统里有专门的借阅参数表存储借期天数每次借书时动态计算。这个简易系统的解决方案是在常量类里定义一个默认值public class BorrowConfig { public static final int MAX_BORROW_DAYS 30; public static final double OVERDUE_FINE_PER_DAY 0.5; }还书时取出该图书最近一条borrow_type 1的记录用op_time MAX_BORROW_DAYS算应还日期和当前时间做比较。这里有个坑直接用毫秒差除以 86400000 来算天数会因夏令时或闰秒产生偏差更稳妥的是用LocalDate做日期差计算。LocalDate dueDate LocalDate.parse(opTime, formatter).plusDays(BorrowConfig.MAX_BORROW_DAYS); LocalDate now LocalDate.now(); long overdueDays ChronoUnit.DAYS.between(dueDate, now); if (overdueDays 0) { // 弹窗提示逾期罚款金额 overdueDays * OVERDUE_FINE_PER_DAY }ChronoUnit.DAYS.between是标准的日期差算法不会受时间组件干扰。逾期计算放这里有两个好处第一不依赖数据库函数方便在单元测试里造各种日期场景验证第二后续把借期天数改成可配置时只需要往BorrowConfig里加一个从配置文件加载的逻辑调用处不动。3.4 状态机视角看整个借还链路如果把借书的生命周期画成一个状态向量它其实是一个朴素的状态机在馆0→ 借出1→ 在馆0。看似简单但边界情况都藏在「忽略状态检查」或者「跳步操作」里。举个例子我在测试时发现一个翻车场景图书处于借出状态时管理员直接操作数据库把status改成 0然后这本书在没有归还记录的情况下重新出现在「可借列表」中。这就是没有状态机校验带来的脏数据。所以在这个简易项目中借还操作必须在 service 层做状态校验不能信任前端传参更不能允许绕过 Service 直接 UPDATE 数据库。从这个角度看这个系统的业务逻辑虽然只有几个方法但状态约束意识是值得反复强调的。后续想扩展「预约」「续借」「挂失」功能只要把状态枚举扩展、在状态转移表里加一条规则即可而不是在界面上堆按钮。4. 部署运行指南与避坑记录从建库到串口联调的一次性打穿4.1 环境准备与启动步骤运行这个项目需要准备的环境是 JDK 8 及以上、MySQL 5.7 或 8.x、一个 USB 转串口的 RFID 读卡器、若干张 ISO 14443A 协议的空白卡。如果你暂时没有硬件读卡器厂家一般会提供一个模拟发送工具也可以用串口调试助手手动往 COM 口发一段十六进制数据来模拟刷卡业务层完全能跑通。第一步创建数据库并导入建表脚本同时在book表里手工插入几条示例图书数据。第二步打开src/main/resources/db.properties修改数据库连接信息重点检查 URL 里的characterEncodingutf8如果漏掉这个参数中文数据在 Swing 界面里会出现乱码。第三步运行MainFrame的 main 方法程序启动时会尝试连接串口。如果串口不存在界面会显示红字提示而不是崩溃。连接串口前我先用「设备管理器」确认读卡器枚举出的 COM 端口号比如 COM3。然后在代码里把端口名写死。这种做法快速但不优雅更稳妥的做法是启动时列出所有可用串口让用户下拉选择。二次开发时值得优先改进。4.2 避坑记录五条高频踩坑经验第一条串口被占用导致程序无法启动。现象是程序启动就报PortInUseException。原因是某些串口调试工具或读卡器厂家自带的上位机软件没有退出把 COM 口占住了。解决方法是关闭所有串口调试工具再启动程序如果还是不行右键「计算机 → 管理 → 设备管理器」卸载串口设备后重新插拔让系统重新枚举端口。第二条卡片贴近读卡器却没反应。现象是卡片离天线很近但程序没有弹出识别提示。原因可能有两个方向一是卡片放反了或位置偏离天线感应区RFID 的天线感应区通常只有硬币大小不是整块区域都能读二是卡片类型和读卡器协议不匹配RC522 只支持 ISO 14443A 的卡如果是其他协议就得换硬件。解决方法是调整卡片位置并缓慢移动找感应点确认卡片类型是否在支持列表内。这类问题属于「玄学」最多的场景我第一次联调时换了三种卡片摆放姿势才找到规律。第三条Swing 界面中文乱码。现象是窗口标题、按钮文字全部变成问号。原因通常是 MySQL 连接串没有指定编码或创建表时用了latin1。解决办法是在 JDBC URL 末尾加?useUnicodetruecharacterEncodingutf8同时保证 Java 源码文件用 UTF-8 保存。如果还乱码检查 MySQL 服务端默认字符集。这套组合能解决九成乱码问题。第四条借书成功但列表不刷新。现象是借书操作完成后界面的图书列表还显示「可借」重启程序才正确。原因是借书操作没有触发 TableModel 的刷新事件JTable用的还是加载时的旧数据。解决方法是借还操作完成后重新查库并调用tableModel.fireTableDataChanged()或者重新 setModel。这次体会到GUI 和数据状态是两个东西写界面时每完成一个修改动作都要考虑「视图要不要跟着更新」。第五条S50 卡改了密码后认不出。现象是某天拿一批新卡测试发现读卡器能读到 UID 但后续扇区操作全部失败。原因是有人把卡片的第 0 扇区块密码从默认的FFFFFFFFFFFF改成了自定义值导致程序按默认密钥校验数据块失败。解决方法是确认这批卡是不是被之前项目初始化时写入了配置遇到这种情况重新用空白卡测试。血泪经验是系统在只读 UID 时永远别动扇区内容否则会在某个莫名其妙的时间点翻车。以上五条基本覆盖了运行这个项目的绝大多数线上问题。调试顺序建议始终是先确认硬件层能读到卡再确认数据库连接能查到数据最后才去怀疑业务逻辑。4.3 串口调试技巧没有硬件也能把流程跑通很多同学卡在第一步是因为硬件没到位这里分享一个没有读卡器也能完成全流程联调的方式用com.fazecast.jSerialComm库自带的模拟器或者虚拟串口软件比如 com0com 和 VSPD创建一对虚拟串口 COM3 和 COM4然后写一个小程序往 COM4 发送模拟的 UID 帧系统从 COM3 读取。这样可以把「读卡」环节完全替换掉验证借书、还书、逾期计算这些业务逻辑。我给RfidReader加了一个SimulatedReader的实现继承同一个接口在界面里通过配置切换是走真实串口还是模拟器。这个设计让我在演示时不需要绑定硬件只要告诉观众「这里就是读卡器的事件源」大家就能理解清楚。如果你有硬件联调的需求记得把真实读卡器的驱动装好否则getCommPort返回的列表里根本看不到设备。5. 二次开发思路把单机演示改造成可上线的馆藏系统5.1 方向一界面层替换为 Web 端Swing 界面只适合单机演示真要给图书馆多人同时使用就得改成 Web 架构。好消息是业务层和数据访问层可以原样保留只需要在 service 方法上加上Transactional注解再把 dao 的 JDBC 换成 MyBatis 或 Spring Data JPAController 层对外暴露 REST 接口即可。前端用 Vue 或 React 做扫码界面读卡器客户端仍然跑在 Windows 主机上通过 WebSocket 把卡号推送给后端这样保留现有硬件的接入能力。5.2 方向二把借阅参数做成可配置目前BorrowConfig是硬编码的常量每次改借期天数都要重新编译。改造思路是引入一个config.properties或数据库配置表启动时加载管理员在界面上直接修改。这个改动量不大但实用性很高也是「从演示到能用」的第一步。我一般会把所有业务参数都集中到一个类里避免散落在各个 service 方法中。5.3 方向三增加读者自助借还终端把系统部署到一台上触屏一体机上刷读者卡进入刷图书卡完成借阅归还时不需要管理员确认。这个方向需要在服务端增加防重借控制同一读者对同一本书的借出记录必须处于「未归还」状态才能识别为有效借阅否则弹窗提示「请勿重复借阅」。边界情况还包含「还了一本不在该读者名下的书」的场景这会提醒系统输出明确错误而不要静默处理。从我个人的观察来看大多数有 RFID 设备接入的课程设计或者小型系统真正被反复吐槽的都是细节状态不更新、重复借阅、无提示。改造时建议先用状态机梳理一遍现有流程再动手加 Web 模块。5.4 验证改造效果的测试清单改完代码后验证功能是否回归我通常会按顺序走一遍这组测试用例录入一个新读者卡 → 录入一本新书卡 → 借出成功 → 再次借同一本书被拦截 → 归还成功 → 归还一本没借过的书被拦截 → 逾期数据能统计出来 → 串口拔掉时程序给出明确提示。这八个用例能覆盖百分之八十以上的逻辑分支。从那以后我每次改完这种「硬件 数据库 业务」三段式系统都会强制把这张清单从头到尾跑一遍确认没问题再交付省去很多被拉回去返工的时间。项目虽小但这套流程让交付质量稳定了不少希望帮到你。本文还有配套的精品资源点击获取