干了几年Java开发,又带过不少新人,说实话类似“小区物业智能卡管理”这种系统,市面上Demo特别多,但大多只是把界面画出来、数据库塞几条假数据,真正把“刷卡—验证—开门—记日志”这条链路跑通、还把卡的生命周期(发卡、挂失、解挂、退卡)处理明白的,少之又少。最近把手上这套基于Java的小区物业智能卡管理系统完整梳理了一遍,从需求分析到数据库设计,从读卡器接入到门禁验证逻辑,所有代码和演示环境都整理出来了。如果你正在准备毕业设计,或者工作中突然接到一个门禁/物业类的小项目,这篇文章直接把设计思路、核心代码和踩坑记录一次讲透,拿来就能抄作业。
1. 项目设计思路与整体架构
1.1 物业卡管理到底在管什么
先说一个很常见的场景:很多老旧小区还在用最原始的方式管理门禁卡——物业办公室一个Excel表格,住户来办卡就手动填一行卡号,挂失了就在微信群里喊一声,保安拿个本子登记进出记录。这套流程的问题,只要你接触过真实物业项目就能数出一堆:
- 卡片信息靠人工登记,卡号写错、漏登是常事,事后根本对不上。
- 卡片挂失、补办流程没有闭环,丢了的卡在系统里依然是“有效卡”,安全隐患很大。
- 门禁权限和物业费缴费状态完全脱节,欠费半年照样刷卡进门,物业费催缴非常被动。
- 进出记录纸质登记,想查某个人某个时间点有没有进出小区,翻本子翻到崩溃。
所以这个系统的核心目标不是“做一张漂亮的界面”,而是把卡片的全生命周期管理和门禁权限控制真正串起来。具体拆解下来,核心功能就这么几块:
- 住户信息管理:登记住户姓名、手机号、房号、车辆信息等。
- 卡片管理:发卡(绑定住户)、挂失、解挂、退卡、延期。
- 缴费管理:物业费、停车费缴费记录,缴费状态直接联动门禁权限。
- 门禁验证:刷卡时实时校验卡片状态、缴费状态,记录进出日志。
- 管理员登录与操作日志:谁在什么时候操作了哪张卡,全流程可追溯。
这套需求其实非常典型,几乎能覆盖市面上大部分物业门禁系统的核心逻辑。把这两块做扎实,比堆砌十个没啥用的功能模块有价值得多。
1.2 技术选型:为什么是Java + MySQL + IC卡
选型的时候,很多人第一反应是“都用Java了,界面是不是该用JavaFX?”或者说“干脆做成Web系统不就行了?”这里我基于实际场景把选型逻辑理一遍:
Java作为开发语言,主要看中的是稳定性和生态。JDBC操作MySQL非常成熟,Swing做桌面端管理工具虽然视觉效果朴素,但胜在轻量、部署简单、资料多。对于一个小型物业办公室内部使用的系统,Swing完全够用,而且相比Web系统,桌面端在操作读卡器这类硬件设备时更直接,不用折腾浏览器调用本地设备的各种权限问题。
MySQL作为数据库,免费、轻量、文档丰富,配合Navicat或者命令行都能快速操作。考虑到物业系统的数据量级别(一个小区几千户),MySQL的性能绰绰有余。
智能卡选择RFID IC卡(M1卡),主要是成本和易用性。M1卡就是市面上最常见的ID卡/IC卡,一张卡几毛钱,读卡器几十块钱,而且大部分USB读卡器都是免驱的,插上就能用。相比之下CPU卡安全性更高但成本和开发门槛也高,对于这个场景属于杀鸡用牛刀。
关于Swing和JavaFX的选择,我多说一句:个人项目的目标是“稳定跑通、逻辑完整”,不是追求界面多炫酷。Swing的成熟度意味着你遇到任何界面问题,搜索引擎上一定有答案。JavaFX虽然界面现代化很多,但它的生命周期、并发模型对新手来说是个不小的学习成本。所以这个项目我最终选的是Swing + MySQL + M1卡读卡器的组合,整体复杂度可控,又能把核心业务逻辑说清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构
2.1 五张核心表的设计细节
数据库设计是整个项目的基石,表结构没设计好,后面写代码会处处别扭。这个系统我最终落地的核心表是五张:住户表、卡片表、缴费记录表、门禁记录表、管理员表。直接看表结构设计:
| 表名 | 字段 | 类型 | 说明 |
|---|---|---|---|
| owner | id | bigint | 主键,自增 |
| owner_name | varchar(50) | 住户姓名 | |
| owner_phone | varchar(20) | 联系电话 | |
| building_no | varchar(20) | 楼栋号 | |
| unit_no | varchar(20) | 单元号 | |
| room_no | varchar(20) | 房号 | |
| create_time | datetime | 登记时间 | |
| card | id | bigint | 主键,自增 |
| card_no | varchar(50) | 卡片物理卡号(UID) | |
| owner_id | bigint | 关联住户ID | |
| card_type | tinyint | 卡片类型:1业主卡,2租客卡,3临时卡 | |
| status | tinyint | 状态:1正常,2挂失,3禁用,4已退卡 | |
| start_date | date | 生效日期 | |
| expire_date | date | 过期日期 | |
| create_time | datetime | 发卡时间 | |
| payment | id | bigint | 主键,自增 |
| owner_id | bigint | 关联住户ID | |
| pay_type | tinyint | 缴费类型:1物业费,2停车费 | |
| pay_amount | decimal(10,2) | 缴费金额 | |
| pay_date | datetime | 缴费时间 | |
| expire_date | date | 费用覆盖截止时间 | |
| access_log | id | bigint | 主键,自增 |
| card_no | varchar(50) | 刷卡卡号 | |
| owner_id | bigint | 关联住户ID(可为空) | |
| access_time | datetime | 刷卡时间 | |
| access_result | tinyint | 结果:1允许通过,0拒绝 | |
| fail_reason | varchar(100) | 拒绝原因 | |
| admin | id | bigint | 主键,自增 |
| username | varchar(50) | 登录用户名 | |
| password | varchar(100) | 密码(MD5加密存储) | |
| real_name | varchar(50) | 真实姓名 | |
| create_time | datetime | 创建时间 |
这套表设计里有几个点是我刻意处理的,直接照着抄不会踩坑:
card表中card_no存的是物理卡号(UID),这是读卡器读到的那串十六进制字符串,比如“4A3B2C1D”。每一张M1卡的UID在全球范围内基本是唯一的,直接用UID作为业务主键关联是最简单的方案。有些方案会在卡内写入自定义数据,但对于这种场景完全没必要,读UID就够了。
status字段用数字不用字符串,比如1正常、2挂失、3禁用。很多新手喜欢直接存“正常”“挂失”这种中文字符串,看着直观,但后续扩展状态、写统计SQL都会很痛苦。数字状态配合代码里的枚举常量,才是正规做法。
payment表单独拆出来,不把缴费截止日期冗余在owner表里。虽然查询的时候多一次关联,但保证了数据的一致性,而且以后要扩展停车费、水费、电费等多种缴费类型,直接加pay_type字段就行,不用改表结构。
2.2 表关系与状态字段的设计技巧
这五张表的关系非常清晰:owner和card是一对多(一个住户可以有多张卡,比如业主本人一张、家属一张),card和access_log是一对多(一张卡每次刷卡都会生成一条日志),owner和payment是一对多(一个住户有多条缴费记录)。用外键关联是基础,但我在实际建表的时候故意不设物理外键约束,只保留逻辑关联。原因很简单:外键约束会在插入和删除时带来额外的性能开销和操作限制,对于这种小型管理系统的开发阶段,频繁改数据时外键约束非常碍事。逻辑关联配合代码层面的校验,完全够用。
卡片状态字段的设计是整个系统的灵魂。我把卡片状态分成四种:正常、挂失、禁用、已退卡。这个状态机看起来简单,但很多项目就是在这个地方栽了跟头。比如挂失和解挂,本质上是把status从2改回1,但代码里必须判断当前状态:一张已退卡的卡片不能直接挂失,一张正常的卡片不能重复挂失。这些逻辑放在Service层统一处理,不要在界面上散着写。
关于缴费状态如何联动门禁权限,我的处理思路是给payment表维护一个费用截止时间,门禁验证的时候做两件事:先查card表校验卡状态,再查payment表校验该住户名下是否有一笔未过期的缴费记录。说白了就是“有效期”思维:卡片本身有有效期,缴费也有有效期,两个都有效才能进门。这个设计可以平滑扩展到多种费用的组合校验,比如物业费没交但停车费交了,那就只拒绝进门但允许进停车场,只需要在验证逻辑里按pay_type分别判断即可。
3. 智能卡读写原理与核心功能实现
3.1 智能卡到底是什么——RFID IC卡的工作方式
先抛开代码,把智能卡本身的原理说清楚,不然你连读卡器返回的数据都看不懂。
我们常用的门禁卡,正式名称叫RFID IC卡,最常见的型号是M1卡(Mifare S50)。它的工作方式可以理解为:读卡器通过射频信号给卡片供电,卡片内部的芯片被激活后,把存储在芯片里的数据通过射频信号回传给读卡器。整个过程不需要接触,所以也叫“非接触式IC卡”。
M1卡的存储结构是分扇区的,一共16个扇区,每个扇区4个块,每个块16个字节。0扇区0块是厂商代码区,存储着卡的全球唯一UID,这块数据出厂时写死,不可修改。其他扇区可以自由读写,比如往里面写入业主姓名、门禁权限等数据。但对于我们这套系统,只要读取0扇区0块的UID就足够了。
读卡器在电脑上通常以两种形态出现:一种是USB接口的,插上之后系统里会多出一个“USB输入设备”,刷卡时读卡器直接把卡号当成键盘输入输出到当前光标所在的输入框里,后面再跟一个回车;另一种是串口接口的,通过COM口和上位机通信,需要程序主动去读串口数据。我在项目里两种都接入了,开发调试用的是USB免驱版,生产演示用的是串口版,后面会详细说两种方式的代码区别。
3.2 读卡器接入的三种方式对比
开发阶段最头疼的往往不是业务逻辑,而是怎么让Java程序读到卡号。我整理了一下目前主流的三种接入方式,直接看对比:
| 接入方式 | 实现难度 | 稳定性 | 适用场景 | 说明 |
|---|---|---|---|---|
| USB免驱模拟键盘 | 很低 | 高 | 快速原型、临时演示 | 读卡器把卡号模拟成键盘输入,焦点在哪个输入框卡号就进哪个框,代码层面不用做任何串口通信 |
| 串口通信(jSerialComm) | 中等 | 高 | 正式桌面应用 | 通过串口协议主动读取数据,可以自定义指令,适合集成到业务系统里 |
| 厂家SDK(DLL动态库) | 较高 | 高 | 特定品牌读卡器 | 需要JNI调用本地库,部署时要带上对应平台的DLL文件,跨平台麻烦 |
USB免驱的方案我强烈建议在开发初期使用,做界面调试时非常舒服——文本框获得焦点后,卡片一贴,卡号自动就出现在框里了,就跟用键盘打字一样。但要做到“刷卡自动弹出验证结果并记录日志”这种主动式交互,还是得用串口方案。
我的建议是:基础功能先用USB模拟键盘的方式跑通,验证整个业务流程没问题之后,再把读卡操作替换成串口通信。这样分层演进,问题定位会清晰很多。
3.3 发卡、挂失、退卡、刷卡的完整逻辑
这一节是整个项目的核心,我把每个业务操作的完整逻辑链路写出来,代码照着这个思路写就不会乱。
发卡流程:
- 管理员在发卡界面点击“读取卡号”,读卡器进入等待状态。
- 住户把卡贴到读卡器上,程序读取UID。
- 程序先去card表查询这个UID是否已存在。如果存在且状态为“已退卡”或“禁用”,可以提示“该卡已被使用,是否重新启用”;如果状态为“正常”或“挂失”,直接提示“该卡已被登记,请勿重复发卡”。
- 校验通过后,选择住户(通过业主姓名或房号搜索),设置卡片类型、生效日期、过期日期。
- 插入card表,生成一条发卡操作日志。
这里有一个关键点:发卡之前必须检查卡号是否已存在。很多项目忽视这一步,结果同一张卡被发给了两户人家,后面门禁验证时查出来owner_id是A,但实际上B手里拿着这张卡,数据就乱了。
挂失与解挂流程:
挂失操作的核心是“只改状态,不删数据”。很多新手觉得挂失就是把card表里那条记录删掉,这是非常错误的想法——删掉记录之后,这张卡和住户的绑定关系彻底丢失,后面如果要解挂或者补办,历史数据完全对不上。
正确做法是把status从“正常”改为“挂失”,门禁验证时若发现卡片状态为挂失,直接拒绝并记录日志。挂失卡片如果后来找到了,做“解挂”操作,恢复status为“正常”,需要校验卡片是否已过期。
退卡流程:
退卡的本质是解除卡片和住户的绑定关系。这里有两种处理方式:一种是物理删除该卡记录,一种是保留记录但把status置为“已退卡”。我推荐后一种,因为从追溯角度来说,保留“这张卡曾经属于某住户、什么时候退的”这条信息,在物业纠纷处理时非常有用。退卡时同样要把access_log里的关联数据保留好,不要级联删除。
门禁刷卡验证流程:
这是整个系统最重要的实时链路,完整的验证逻辑如下:
- 读卡器读到卡号,程序捕获到UID。
- 用UID去card表查询卡片信息。
- 如果卡不存在,记录日志(拒绝原因:非法卡),返回“无效卡”。
- 如果卡存在但状态为“挂失”,记录日志(拒绝原因:卡片已挂失)。
- 如果卡存在但状态为“禁用”,记录日志(拒绝原因:卡片已被禁用)。
- 如果卡状态为“正常”,但当前时间超过了expire_date,记录日志(拒绝原因:卡片已过期)。
- 校验该卡绑定的住户是否有一笔有效的缴费记录(缴费截止日期大于当前时间)。如果没有,记录日志(拒绝原因:费用未缴)。
- 全部通过,记录日志(结果:允许通过),控制门禁继电器开门(如果有硬件联动的话)。
这条链路的顺序很重要,一定要先校验卡状态再校验费用状态。否则一张挂失的卡,系统先去查了费用,再返回“挂失卡”,逻辑上没错,但日志和报警响应就慢了。人脸识别、指纹识别也是类似的实时校验思路,先身份后权限。
4. 关键代码解析与实操演示
4.1 项目包结构与代码分层
代码分层的核心原则是“界面代码不直接操作数据库,业务逻辑独立封装”。我用的包结构是标准的MVC变体,非常简单清晰:
text复制src/
├── com.propertycard
│ ├── model // 实体类:Owner, Card, Payment, AccessLog, Admin
│ ├── dao // 数据访问层:每个实体对应一个DAO
│ ├── service // 业务逻辑层:发卡、挂失、退卡、门禁验证
│ ├── util // 工具类:JDBC连接、读卡器工具、时间处理
│ └── view // Swing界面:登录界面、主界面、各管理面板
├── resources
│ ├── db.properties // 数据库连接配置文件
│ ├── init.sql // 建库建表脚本
│ └── lib // 依赖的JAR包:MySQL驱动、jSerialComm
不要小看这个分层,它最大的好处是换界面不影响业务逻辑。比如你后期想从Swing换成JavaFX,只需替换view包下的代码,service和dao完全不用动。同理,如果想把读卡器从USB模拟键盘换成串口,也只需要改util包下的读卡器工具类,业务逻辑层完全无感。
4.2 JDBC工具类与数据库连接
数据库连接我用的是最传统的JDBC + 配置文件方式,没有引入MyBatis。原因很简单:这个项目的数据操作都比较简单,SQL能写明白,引入MyBatis反而增加配置复杂度,对读者理解项目核心逻辑也没有帮助。直接看代码:
java复制package com.propertycard.util;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class DBUtil {
private static String url;
private static String username;
private static String password;
static {
try (InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("db.properties")) {
Properties props = new Properties();
props.load(in);
Class.forName("com.mysql.cj.jdbc.Driver");
url = props.getProperty("db.url");
username = props.getProperty("db.username");
password = props.getProperty("db.password");
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
public static void close(AutoCloseable... resources) {
for (AutoCloseable r : resources) {
if (r != null) {
try {
r.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
对应的db.properties配置文件:
properties复制db.url=jdbc:mysql://localhost:3306/property_card?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
db.username=root
db.password=123456
配置里的这三项参数都是踩过坑才加上的:useSSL=false是避免MySQL 8.0版本连接时出现SSL告警;serverTimezone=Asia/Shanghai是解决时区差异导致的时间错乱(中国地区一定要设为上海时区);characterEncoding=utf8是保证中文数据正常读写,不加这个字段,写进去的中文大概率变成问号。
4.3 卡片管理模块的核心代码
卡片管理的核心就是发卡和状态变更。我直接展示CardDAO里最关键的几个方法,这些代码是我实际跑通的版本:
java复制package com.propertycard.dao;
import com.propertycard.model.Card;
import com.propertycard.util.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class CardDAO {
// 根据卡号查询卡片
public Card findByCardNo(String cardNo) {
String sql = "SELECT * FROM card WHERE card_no = ?";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, cardNo);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return mapRow(rs);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
// 发卡:插入新卡记录
public boolean insertCard(Card card) {
String sql = "INSERT INTO card (card_no, owner_id, card_type, status, start_date, expire_date, create_time) "
+ "VALUES (?, ?, ?, ?, ?, ?, NOW())";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, card.getCardNo());
ps.setLong(2, card.getOwnerId());
ps.setInt(3, card.getCardType());
ps.setInt(4, card.getStatus());
ps.setDate(5, new java.sql.Date(card.getStartDate().getTime()));
ps.setDate(6, new java.sql.Date(card.getExpireDate().getTime()));
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
// 更新卡片状态:挂失/解挂/禁用/退卡
public boolean updateStatus(String cardNo, int status) {
String sql = "UPDATE card SET status = ? WHERE card_no = ?";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, status);
ps.setString(2, cardNo);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
// 查询某住户名下所有卡片
public List<Card> findByOwnerId(long ownerId) {
List<Card> cards = new ArrayList<>();
String sql = "SELECT * FROM card WHERE owner_id = ?";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, ownerId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
cards.add(mapRow(rs));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return cards;
}
private Card mapRow(ResultSet rs) throws SQLException {
Card card = new Card();
card.setId(rs.getLong("id"));
card.setCardNo(rs.getString("card_no"));
card.setOwnerId(rs.getLong("owner_id"));
card.setCardType(rs.getInt("card_type"));
card.setStatus(rs.getInt("status"));
card.setStartDate(rs.getDate("start_date"));
card.setExpireDate(rs.getDate("expire_date"));
return card;
}
}
这些CRUD代码看着平淡无奇,但有一个细节值得注意:所有数据库操作都在try-with-resources里完成,Connection、PreparedStatement、ResultSet都会自动关闭,不会出现连接泄漏。很多跑着跑着就“Too many connections”的项目,十有八九是连接没关。
4.4 门禁验证逻辑核心代码
门禁验证是整个系统的“心脏”,这段代码我写得非常谨慎。业务规则在前面第3.3节梳理过,现在直接看实现:
java复制package com.propertycard.service;
import com.propertycard.dao.AccessLogDAO;
import com.propertycard.dao.CardDAO;
import com.propertycard.dao.PaymentDAO;
import com.propertycard.dao.OwnerDAO;
import com.propertycard.model.Card;
import com.propertycard.model.Owner;
import com.propertycard.model.Payment;
import java.util.Date;
public class AccessService {
private final CardDAO cardDAO = new CardDAO();
private final OwnerDAO ownerDAO = new OwnerDAO();
private final PaymentDAO paymentDAO = new PaymentDAO();
private final AccessLogDAO accessLogDAO = new AccessLogDAO();
// 门禁验证入口,cardNo为读卡器读取到的UID
public AccessResult verifyAccess(String cardNo) {
// 1. 查卡
Card card = cardDAO.findByCardNo(cardNo);
if (card == null) {
return fail(cardNo, null, "非法卡,未登记");
}
// 2. 校验卡状态
if (card.getStatus() == 2) {
return fail(cardNo, card.getOwnerId(), "卡片已挂失");
}
if (card.getStatus() == 3) {
return fail(cardNo, card.getOwnerId(), "卡片已被禁用");
}
if (card.getStatus() == 4) {
return fail(cardNo, card.getOwnerId(), "卡片已退卡");
}
// 3. 校验卡片有效期
if (card.getExpireDate() != null && card.getExpireDate().before(new Date())) {
return fail(cardNo, card.getOwnerId(), "卡片已过期");
}
// 4. 校验缴费状态:查住户最近一笔有效缴费记录
if (card.getOwnerId() != null) {
Payment latestPayment = paymentDAO.findLatestValidPayment(card.getOwnerId());
if (latestPayment == null || latestPayment.getExpireDate().before(new Date())) {
return fail(cardNo, card.getOwnerId(), "物业费已欠缴");
}
}
// 5. 全部通过,记录日志
return success(cardNo, card.getOwnerId());
}
private AccessResult fail(String cardNo, Long ownerId, String reason) {
accessLogDAO.insertLog(cardNo, ownerId, 0, reason);
return new AccessResult(false, reason);
}
private AccessResult success(String cardNo, Long ownerId) {
accessLogDAO.insertLog(cardNo, ownerId, 1, "验证通过");
return new AccessResult(true, "验证通过");
}
// 结果封装
public static class AccessResult {
private final boolean allowed;
private final String message;
public AccessResult(boolean allowed, String message) {
this.allowed = allowed;
this.message = message;
}
public boolean isAllowed() {
return allowed;
}
public String getMessage() {
return message;
}
}
}
这里我把校验逻辑封装成独立的AccessService,就是希望门禁验证是一个可以被单元测试覆盖的纯业务方法,而不是散落在界面代码里的一堆判断。这样做的好处是,后续如果要做多门禁设备联动、远程开锁、园区一卡通等扩展,直接复用这个Service类就行。
AccessResult这个内部类也很有用,它把验证结果统一封装成“是否允许”和“提示消息”两个字段,界面层拿到的就是一个可以直接展示的对象,不用自己去拼提示语。
4.5 从零跑通这套系统的操作步骤
很多读者拿到源码后最头疼的是不知道从哪下手。我把我在干净环境里从零跑通这套系统的完整步骤列出来,每一步都不要跳:
- 安装JDK:版本建议8或11,不要用太高版本(JDK 17及以上在Swing项目兼容性上偶尔有小问题),配置好JAVA_HOME环境变量。
- 安装MySQL:建议5.7或8.0,安装时记住root密码。
- 初始化数据库:用Navicat或命令行执行
resources/init.sql脚本,会自动创建property_card数据库和五张表。 - 配置数据库连接:打开
resources/db.properties,把db.username和db.password改成你自己MySQL的账号密码。 - 导入项目到IDE:推荐IntelliJ IDEA,用“Open”方式选择项目根目录导入,等待Maven/Gradle自动下载依赖(如果用我提供的lib方式,则手动把lib下的JAR添加到Project Structure的Libraries里)。
- 启动系统:运行
view/LoginFrame.java的main方法,用初始化脚本里预置的管理员账号密码登录(初始账号:admin,密码:admin123)。 - 接入读卡器:插入USB读卡器,如果你是模拟键盘型读卡器,在发卡界面点击“读取卡号”后,光标定位在卡号输入框,把卡片贴上去,卡号就会自动输入;如果用了串口读卡器,需要先运行串口监听线程。
- 完整流程自测:先添加住户,再发卡,然后刷卡验证,最后去access_log表里看记录。
第7步容易卡住,多提醒一句:USB模拟键盘型读卡器本质是一个输入设备,刷卡时焦点必须停在正确的输入框里。如果焦点跑到按钮上去了,卡号就会打在按钮上,或者触发按钮点击事件,现象就是“刷卡没反应”。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
这套项目我在跑通过程中遇到的高频问题,我整理成一张速查表,遇到问题直接对照着排查:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
启动时报ClassNotFoundException: com.mysql.cj.jdbc.Driver |
MySQL驱动JAR没添加到classpath | 把mysql-connector-java的JAR包加入项目依赖;确认版本和MySQL版本匹配 |
连接数据库报Access denied for user 'root'@'localhost' |
数据库账号或密码错误 | 检查db.properties里的账号密码;确认MySQL root账号的host是localhost还是% |
连接数据库报Communications link failure |
MySQL服务没启动,或url里的端口不对 | 确认3306端口没被占用;用netstat -ano看端口监听状态;检查MySQL服务是否启动 |
写入中文变成乱码??? |
连接URL没有设置字符集,或数据库表字符集不是utf8 | URL加characterEncoding=utf8;建表时指定DEFAULT CHARSET=utf8mb4;确认数据库和表编码一致 |
| 编译报错“源发行版 17 需要目标发行版 17” | JDK版本和项目编译级别不一致 | IDE中把Project Structure的SDK和Language Level都设为一致;检查maven的maven.compiler.source/target配置 |
| 刷卡没反应(USB模拟键盘型) | 焦点不在输入框内;读卡器驱动未安装 | 点击输入框再刷卡;检查设备管理器是否识别到USB输入设备 |
| 刷卡只输出字母数字没有空格回车 | 某些读卡器默认不带回车后缀 | 在界面的输入框监听器里对字符串做trim,不要依赖回车触发查询 |
| Swing界面卡死、无响应 | 在事件分发线程中做了数据库查询或串口等待 | 把耗时操作放到SwingWorker或独立线程中执行,回调更新UI |
| 查询很慢或大量连接堆积 | Connection没有关闭/复用 | 检查代码里是否在finally中关闭Connection;考虑引入连接池(如Druid) |
| 同一张卡可以重复发卡 | 发卡前没有检查卡号是否已存在 | 在insertCard前调用findByCardNo做存在性校验 |
5.2 新手最容易踩的坑与避坑心得
最后分享几个我实际踩过、也见很多新人反复踩的坑,这条价值非常大。
第一个坑:把“卡号”和“卡内数据”混为一谈。 很多新手看到“智能卡管理”就直接去研究怎么往卡里写数据,用Java的APDU指令去操作扇区读写,把工程搞得很复杂。其实对于门禁场景,物理卡号(UID)是辨别身份最可靠的凭证,完全不用往卡里写任何业务数据。卡号出厂唯一且不可篡改,简单、安全、够用。把精力放在业务逻辑上,而不是卡片的底层扇区操作上,这才是这个项目的正确打开方式。
第二个坑:Swing界面卡死。 新手最常见的操作是把数据库查询放在按钮的ActionListener里直接执行,小数据量时没问题,但一旦数据量大一点,或者数据库响应慢一点,界面就会“转圈圈”甚至完全无响应。正确做法是用SwingWorker或者new Thread把耗时操作丢到后台线程,查询完成后再通过SwingUtilities.invokeLater回到事件线程更新界面。这一点在接入串口读卡器时尤其重要——串口读取是阻塞的,如果直接放在事件线程里,整个界面会卡死。
第三个坑:为了“演示效果”堆功能。 做这类系统时,一定不要想着把所有物业功能都塞进去——门禁、停车场、电梯控制、快递柜、智能家居、水电抄表……功能越堆越多,代码越来越乱,最后每个模块都是半吊子。老实说,把卡片管理、门禁验证、缴费联动这三条核心链路做完整、做稳定,远远好过十个半成品功能。项目答辩或验收时,面试官/老师看的是“你理解底层逻辑、能说清楚设计取舍”,而不是功能数量。
第四个坑:忽略日志的重要性。 我在门禁验证里专门设计了access_log表,每条刷卡记录无论成功失败都落库。这个设计看起来不起眼,但在物业场景里价值极大——住户投诉“我没带卡怎么被扣了钱”时,翻日志就能说清楚;安保查“某时间某人在不在小区”时,日志就是最直接的证据。做管理类系统,永远不要忽略审计追踪。
整个项目走完,最大的感受是:这类业务系统真正的复杂度不在于某个技术点有多难,而在于如何把散落的业务规则梳理成一条清晰的验证链路,并且用合理的分层结构组织起来。智能卡只是入口,背后的数据一致性、状态管理、异常处理才是值得深挖的地方。如果你也想动手实现一套类似系统,建议先从最核心的“刷卡—验证—记日志”链路做起,跑通之后再逐步补全发卡、挂失、缴费联动等外围功能。最后再分享一个小技巧:界面开发时先用USB模拟键盘读卡器把业务流程跑通,之后再接串口通信做真实硬件联动,这个顺序能帮你把“硬件问题”和“业务问题”彻底分开排查。
