1. 项目背景与核心需求
这个Java会员卡充值系统是我去年为本地连锁超市开发的实际项目。当时超市老板王姐找到我,说他们还在用纸质卡片记录会员积分,经常出现丢失、篡改和计算错误的问题。最夸张的一次,因为店员手误多记了一个零,超市直接损失了八千多块的礼品兑换。
这个系统需要解决几个核心痛点:
- 会员信息的安全存储(避免纸质卡的丢失和涂改)
- 实时准确的余额计算(防止人工计算错误)
- 交易记录的完整追溯(方便对账和异常排查)
- 简单易用的操作界面(降低店员培训成本)
我选择Java作为开发语言有几个考虑:首先超市的收银电脑都是Windows系统,Java的跨平台特性很方便;其次Java强大的类型检查和异常处理机制,能有效避免业务逻辑错误;最重要的是Java丰富的生态库,比如后面会用到的Swing做界面、JDBC连接数据库等。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 基础技术栈
这个项目采用了经典的MVC三层架构:
- Model层:使用MySQL存储会员信息、交易记录
- View层:Java Swing实现图形界面
- Controller层:处理业务逻辑和数据流转
数据库表设计是关键,我最终确定了三张核心表:
sql复制CREATE TABLE member (
id INT PRIMARY KEY AUTO_INCREMENT,
card_number VARCHAR(20) UNIQUE,
name VARCHAR(50),
phone VARCHAR(15),
balance DECIMAL(10,2) DEFAULT 0.00,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE transaction (
id INT PRIMARY KEY AUTO_INCREMENT,
member_id INT,
amount DECIMAL(10,2),
type ENUM('充值','消费'),
operator VARCHAR(50),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (member_id) REFERENCES member(id)
);
CREATE TABLE admin (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE,
password VARCHAR(100),
role ENUM('管理员','操作员')
);
2.2 开发环境配置
很多Java初学者容易在环境配置上踩坑,这里分享我的配置方案:
- JDK选择:用OpenJDK 11(LTS版本),避免使用最新版可能存在的兼容性问题
- IDE:IntelliJ IDEA社区版(完全免费且足够用)
- 数据库:MySQL 8.0(注意要配置好时区:
serverTimezone=Asia/Shanghai) - 依赖管理:Maven引入必要的库:
xml复制<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
</dependencies>
重要提示:记得在MySQL中创建用户并授权,不要直接用root账户连接应用,这是很多课程教学里常忽略的安全隐患。
3. 核心功能实现详解
3.1 会员卡充值逻辑
充值功能看似简单,但实际开发中有很多细节要考虑。这是我的核心代码实现:
java复制public boolean recharge(String cardNumber, BigDecimal amount, String operator) {
Connection conn = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false); // 开启事务
// 1. 检查会员卡是否存在
Member member = memberDAO.findByCardNumber(conn, cardNumber);
if (member == null) {
throw new BusinessException("会员卡不存在");
}
// 2. 更新余额(使用BigDecimal避免浮点精度问题)
BigDecimal newBalance = member.getBalance().add(amount);
if (newBalance.compareTo(MAX_BALANCE) > 0) {
throw new BusinessException("余额不能超过" + MAX_BALANCE);
}
// 3. 记录交易
Transaction transaction = new Transaction();
transaction.setMemberId(member.getId());
transaction.setAmount(amount);
transaction.setType("充值");
transaction.setOperator(operator);
// 4. 执行更新
memberDAO.updateBalance(conn, member.getId(), newBalance);
transactionDAO.save(conn, transaction);
conn.commit();
return true;
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
throw new RuntimeException("充值失败", e);
} finally {
DBUtil.close(conn);
}
}
几个关键设计点:
- 使用事务保证数据一致性(余额更新和交易记录要么都成功,要么都失败)
- BigDecimal处理金额计算(绝对不要用double/float)
- 设置余额上限防止恶意充值
- 完整的异常处理机制
3.2 消费扣款逻辑
消费逻辑比充值更复杂,需要处理并发问题。我最终采用的方案是:
java复制public boolean consume(String cardNumber, BigDecimal amount, String operator) {
Connection conn = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false);
// 使用SELECT...FOR UPDATE加行锁
Member member = memberDAO.findByCardNumberForUpdate(conn, cardNumber);
if (member == null) {
throw new BusinessException("会员卡不存在");
}
// 检查余额是否充足
if (member.getBalance().compareTo(amount) < 0) {
throw new BusinessException("余额不足");
}
BigDecimal newBalance = member.getBalance().subtract(amount);
// 记录消费
Transaction transaction = new Transaction();
transaction.setMemberId(member.getId());
transaction.setAmount(amount);
transaction.setType("消费");
transaction.setOperator(operator);
memberDAO.updateBalance(conn, member.getId(), newBalance);
transactionDAO.save(conn, transaction);
conn.commit();
return true;
} catch (SQLException e) {
// 同上...省略异常处理
}
}
这里的关键是SELECT...FOR UPDATE语句,它可以锁定查询的行记录,防止其他会话同时修改造成余额不一致。这是实际项目中必须考虑的并发安全问题。
4. 图形界面开发实战
4.1 使用Swing构建操作界面
虽然现在流行Web应用,但考虑到超市收银台的电脑配置和网络稳定性,我选择了Java Swing开发本地客户端。这是主界面的核心代码:
java复制public class MainFrame extends JFrame {
private JTextField cardNumberField;
private JTextField amountField;
private JTextArea logArea;
public MainFrame() {
setTitle("超市会员卡管理系统");
setSize(800, 600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 顶部输入区
JPanel inputPanel = new JPanel(new GridLayout(0, 2, 10, 10));
inputPanel.add(new JLabel("会员卡号:"));
cardNumberField = new JTextField();
inputPanel.add(cardNumberField);
inputPanel.add(new JLabel("金额:"));
amountField = new JTextField();
inputPanel.add(amountField);
// 操作按钮
JButton rechargeBtn = new JButton("充值");
rechargeBtn.addActionListener(this::handleRecharge);
JButton consumeBtn = new JButton("消费");
consumeBtn.addActionListener(this::handleConsume);
JButton queryBtn = new JButton("查询");
queryBtn.addActionListener(this::handleQuery);
JPanel btnPanel = new JPanel();
btnPanel.add(rechargeBtn);
btnPanel.add(consumeBtn);
btnPanel.add(queryBtn);
// 日志区
logArea = new JTextArea();
logArea.setEditable(false);
// 布局组装
setLayout(new BorderLayout(10, 10));
add(inputPanel, BorderLayout.NORTH);
add(btnPanel, BorderLayout.CENTER);
add(new JScrollPane(logArea), BorderLayout.SOUTH);
}
private void handleRecharge(ActionEvent e) {
try {
String cardNumber = cardNumberField.getText();
BigDecimal amount = new BigDecimal(amountField.getText());
boolean success = memberService.recharge(cardNumber, amount, "操作员1");
if (success) {
log("充值成功: " + cardNumber + " 金额: " + amount);
}
} catch (Exception ex) {
log("充值失败: " + ex.getMessage());
}
}
// 其他事件处理方法...
}
4.2 界面优化技巧
经过实际使用反馈,我对界面做了这些优化:
- 增加输入验证:限制金额输入必须为数字
java复制amountField.addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
char c = e.getKeyChar();
if (!(Character.isDigit(c) || c == KeyEvent.VK_BACK_SPACE || c == KeyEvent.VK_DELETE || c == '.')) {
e.consume();
}
}
});
- 添加操作快捷键:回车键触发充值
java复制amountField.addActionListener(this::handleRecharge);
- 界面主题美化:使用FlatLaf库让Swing更现代
java复制FlatIntelliJLaf.setup();
UIManager.put("Button.arc", 10); // 圆角按钮
5. 项目部署与运维
5.1 数据库连接池配置
直接每次操作都创建新连接性能很差,我引入了HikariCP连接池:
java复制public class DBUtil {
private static final HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/member_system");
config.setUsername("member_user");
config.setPassword("secure_password");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
dataSource = new HikariDataSource(config);
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
5.2 日志记录方案
好的日志系统对排查问题至关重要,我采用SLF4J+Logback组合:
xml复制<!-- pom.xml -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
java复制private static final Logger logger = LoggerFactory.getLogger(MemberService.class);
public boolean recharge(...) {
try {
logger.info("开始充值操作,卡号:{},金额:{}", cardNumber, amount);
// ...业务逻辑
logger.info("充值成功,卡号:{}", cardNumber);
} catch (Exception e) {
logger.error("充值异常,卡号:" + cardNumber, e);
throw e;
}
}
日志配置文件logback.xml示例:
xml复制<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/member-system.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/member-system.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</configuration>
5.3 打包与部署
使用Maven Assembly插件打包成可执行JAR:
xml复制<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.supermarket.MainApp</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
打包命令:
bash复制mvn clean package assembly:single
最终生成的target/member-system-1.0-jar-with-dependencies.jar可以直接用java -jar命令运行。
6. 项目优化与扩展
6.1 性能优化实践
在实际运行中发现两个性能瓶颈:
- 会员查询响应慢(当会员数超过1万时)
- 交易记录表过大导致统计报表慢
优化方案:
sql复制-- 为会员卡号添加索引
CREATE INDEX idx_member_card ON member(card_number);
-- 交易表按月分表
CREATE TABLE transaction_202301 (
id INT PRIMARY KEY AUTO_INCREMENT,
member_id INT,
amount DECIMAL(10,2),
type ENUM('充值','消费'),
operator VARCHAR(50),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_member_time (member_id, create_time)
) ENGINE=InnoDB;
6.2 安全加固措施
经过安全审计后实施的改进:
- 密码加密存储:使用BCrypt算法
java复制public class PasswordUtil {
private static final int BCRYPT_STRENGTH = 12;
public static String hash(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt(BCRYPT_STRENGTH));
}
public static boolean verify(String password, String hash) {
return BCrypt.checkpw(password, hash);
}
}
- SQL注入防护:全部使用PreparedStatement
- 敏感操作日志记录:记录管理员的所有关键操作
6.3 扩展功能实现
根据客户需求陆续添加的功能:
- 会员积分系统(1元=1积分,可兑换商品)
java复制public class PointService {
private static final BigDecimal POINT_RATE = new BigDecimal("1.0");
public int calculatePoints(BigDecimal amount) {
return amount.multiply(POINT_RATE).intValue();
}
}
- 生日特权(生日当月消费双倍积分)
- 微信小程序查询接口(使用Spring Boot开发)
- 数据统计分析看板(使用JFreeChart生成图表)
7. 常见问题解决方案
7.1 数据库连接问题
错误现象:间歇性出现"Too many connections"错误
排查过程:
- 检查MySQL最大连接数:
show variables like 'max_connections'; - 监控应用连接泄漏:
show processlist; - 发现部分代码没有正确关闭Connection
解决方案:
- 使用try-with-resources确保连接关闭
java复制try (Connection conn = DBUtil.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
// 业务代码
}
- 配置连接池最大等待时间
java复制config.setConnectionTimeout(30000); // 30秒超时
7.2 并发修改冲突
错误现象:高并发时出现余额不一致
解决方案:
- 数据库层面:使用SELECT FOR UPDATE悲观锁
- 应用层面:增加重试机制
java复制public boolean retryConsume(String cardNumber, BigDecimal amount, int maxRetry) {
int retryCount = 0;
while (retryCount < maxRetry) {
try {
return consume(cardNumber, amount);
} catch (OptimisticLockingFailureException e) {
retryCount++;
Thread.sleep(100 * retryCount); // 指数退避
}
}
return false;
}
7.3 内存泄漏问题
错误现象:长时间运行后JVM内存占用持续增长
排查工具:
- 使用VisualVM监控内存使用
- 生成堆转储文件分析
发现问题:Swing的事件监听器没有正确移除
修复方案:
java复制// 在窗口关闭时移除监听器
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
removeAllListeners();
dispose();
}
});
8. 项目总结与心得体会
这个项目虽然技术难度不算高,但让我对Java开发有了更深的实战理解。几个关键收获:
-
事务处理的重要性:初期没有用事务,出现过充值成功但记录丢失的情况。后来所有写操作都加了事务,稳定性大幅提升。
-
并发问题的隐蔽性:在开发环境测试一切正常,上线后高并发时才发现余额不一致问题。这让我养成了在代码审查时特别注意并发安全的习惯。
-
用户体验的细节:比如金额输入框自动过滤非数字字符、回车键快捷操作等小优化,实际使用中大大提升了收银效率。
-
日志系统的价值:完善的日志记录多次帮助我们快速定位线上问题,特别是加上操作人信息后,责任追溯变得非常清晰。
对于想学习Java实战的同学,我的建议是:
- 从这种小型但完整的项目入手,不要一开始就搞复杂架构
- 重视异常处理和事务管理这些基础但关键的部分
- 养成写单元测试的习惯(本文限于篇幅没展示测试代码)
- 多思考用户真实使用场景,而不只是实现功能
这个系统目前稳定运行了一年多,日均处理2000+笔交易。最近超市准备开分店,我正在设计分布式版本,可能会引入Spring Cloud等技术栈。不过基础版的代码我仍然保留着,因为它很好地体现了Java核心技术的实际应用。
