ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Swing+Socket+MySQL网吧会员管理系统实战全解析

Java Swing+Socket+MySQL网吧会员管理系统实战全解析 简介这是一套面向Java初学者与中小型网吧管理场景的会员系统实战源码聚焦会员信息维护、等级管理与消费记录等核心业务兼顾前后端技术整合与工程规范实践。资源共38个文件压缩包大小3.31MB包含18个XML配置文件用于数据结构定义与框架配置、6个JSP页面构成用户注册、登录、主页等前端入口、2个Java类文件实现核心业务逻辑、1个CSS与1个JS文件支撑界面样式与交互另有Markdown文档提供说明、PNG/JPG图片用于界面美化以及Mavenpom.xml、IDEA.idea目录和Git.gitignore配套工程文件体现完整开发环境适配能力。已有335人学习下载读者可直接导入IntelliJ IDEA运行调试掌握基于Java Web的传统三层架构落地细节理解XML驱动的数据交互方式并复用JSPCSSJS组合构建响应式前台界面是入门级Web项目学习与课程设计的实用参考。 最近接了一个网吧会员管理系统的开发需求场景很典型前台收银机上要跑一个本地桌面程序能开卡、充值、上机、下机结算老板还要能看每日营业额和会员消费记录。客户明确说不要网页版就要那种双击就能打开的桌面软件一来稳定二来不依赖浏览器三来局域网内响应快。这个需求正好是Java的强项我用Java Swing做前端界面、Java Socket做客户端与服务端通信、MySQL存数据一套经典C/S架构就定下来了。从源码设计到前端实现前后花了两周业余时间今天把整个项目的核心设计、实现细节和踩过的坑完整聊一聊正在做类似系统或者准备Java课程设计的同学可以参考一下。这个系统表面上看功能不算多但真正落地的时候要解决的细节问题不少计费精度怎么控制、多个收银台同时操作会不会冲突、会员余额并发扣减会不会出错、界面长时间操作卡死怎么办。这些都是在实际使用中一定会遇到的问题也是这篇文章想重点讲清楚的地方。1. 项目概述与核心需求拆解1.1 网吧会员管理系统到底要管什么我一开始也以为网吧会员管理系统就是“存会员、扣钱”两个功能后来跟老板聊完需求才发现真正的业务流程比想象中复杂一些。会员端功能主要是开卡和充值顾客第一次来报手机号开一张会员卡预存一笔钱之后每次充值金额累加到余额里。上机是核心场景顾客选一台空闲电脑输入会员号系统验证余额足够后开始计时下机时根据时长和费率自动扣费。如果会员在网吧买饮料零食还需要从余额里扣商品费用。管理端则需要维护电脑状态、设置费率、查看上机记录和营业额报表。角色划分也相对清晰收银员负责前台操作老板/管理员可以查看统计数据和修改费率。这些功能集合在一起才构成了一个完整的网吧会员管理闭环。千万不要把系统做成“只有会员增删改查”那样离真正可用还差得远。功能清单整理下来大概是这样的会员管理开卡、登录验证、资料修改、余额查询充值管理充值、充值时赠送金额记录上机管理上机、下机、换机、强制下线计费结算按时长计费、余额扣减、上机记录留存商品销售商品列表、销售扣费可选模块统计报表今日营收、会员排行、上机时长统计1.2 技术选型为什么是JavaSwing而不是网页版客户明确表示不想用网页版这个诉求在网吧行业很常见。网页版虽然部署方便但需要启动Web服务器和数据库服务且依赖浏览器环境对网吧收银台这种要长期开机使用的场景来说稳定性反而不如本地桌面程序。C/S架构的桌面程序双击就能跑界面响应快不会因为浏览器兼容问题出幺蛾子。在Java桌面技术里Swing和JavaFX都能做我选了Swing。原因很简单Swing的资料多、网上案例丰富、JDK 8以后一直稳定维护很多高校的Java课程设计都在用Swing遇到问题更容易找到解决方案。JavaFX虽然界面更现代但学习曲线陡一些对一个小型内部管理系统来说Swing的成熟和稳定反而更重要。前端用Swing后端通信层用Java Socket数据库用MySQL 5.7或8.0JDBC驱动用Connector/J。整套技术栈没有任何重量级框架依赖非常适合作为Java学习项目来理解和复现也能快速部署到真实环境里。1.3 整体架构与通信流程这个项目的架构是三层Swing客户端负责界面展示和用户交互Socket服务端负责接收客户端指令、处理业务逻辑、访问数据库MySQL负责数据持久化。客户端和服务端之间通过自定义的文本协议通信每个指令由命令字和参数组成用竖线分隔。一次完整的“上机”操作流程是这样的收银员在客户端输入会员号和电脑编号点击上机按钮客户端把指令封装成ONLINE|1001|3发送给服务端服务端解析指令校验会员是否存在、余额是否足够、电脑是否空闲然后把电脑状态改为“使用中”插入一条上机记录最后把操作结果返回给客户端。整个过程对收银员来说几乎是瞬间完成。这种设计的好处是前端和后端逻辑完全分离以后如果想加一个Web管理端只需要让Web后端复用同一套Service逻辑或者直接再写一个客户端协议实现就可以了扩展性比较舒服。2. 数据库设计与核心业务逻辑2.1 四张核心数据表的字段设计数据库设计是这个系统的地基表结构一旦定得不合理后边写代码会遇到各种别扭。这个系统最核心的表有四张会员表、电脑表、上机记录表和充值记录表。会员表member用来存会员的基本信息和账户余额重点字段包括id、username、password、real_name、phone、balance、create_time、status。这里面username建议用手机号既可以当登录账号也方便老板在微信里查会员balance用 DECIMAL(10,2) 而不是 DOUBLE避免浮点精度问题导致余额对不上status用来标记正常/冻结状态防止有人恶意欠费后注销跑路。电脑表computer字段不多id、name、status、hourly_rate就够用。name对应实际座位号比如A01、B03status有“空闲”和“使用中”两种状态hourly_rate是这台电脑的每小时费率这样可以支持“普通区4元/小时、电竞区6元/小时”这种差异化定价。上机记录表online_record是计费的核心字段包括id、member_id、computer_id、start_time、end_time、duration_minutes、amount。start_time在上机时写入end_time在下机时更新duration_minutes和amount在下机时计算后写入。这张表一旦插入就不能随便改所有数据都保留方便月底对账。充值记录表recharge_record记录每一笔充值字段是id、member_id、amount、recharge_time如果还要做充值赠送活动可以加一个bonus_amount字段。有了这张表老板想看某个会员充过多少钱直接按member_id分组汇总就行。2.2 计费规则这样设计才不容易扯皮计费规则是这个系统最容易出问题的地方网吧行业的计费习惯和普通软件不一样。我设计的规则是按分钟计费每小时费率取自电脑表不足10分钟按10分钟计这样老板不容易亏顾客也不会因为差几秒钟被多扣钱而投诉。具体算法不复杂上机时记录当前时间戳startTime下机时拿到当前时间戳endTime两者相减得到毫秒数除以60000得到分钟数再向上取整到10的倍数最后除以60乘以费率得到费用。这个计算过程我把核心方法抽出来写了个工具方法代码如下public static double calculateFee(long startTime, long endTime, double hourlyRate) { // 计算实际分钟数 long minutes (endTime - startTime) / 60000; if (minutes 0) { minutes 1; // 最少按1分钟计费防止恶意快速上下机 } // 向上取整到10分钟 long billableMinutes (minutes 9) / 10 * 10; // 换算成小时乘以费率 double fee billableMinutes / 60.0 * hourlyRate; // 保留两位小数避免浮点误差 return Math.round(fee * 100) / 100.0; }虽然按10分钟向上取整可能让顾客多出一点钱但实际运营中顾客通常一坐就是几小时这点误差感知不强而系统逻辑会简单很多。如果以后想做“按秒计费”或“阶梯计费”只需要替换这一个方法其他代码不用动所以设计时把这个方法独立出来非常值得。2.3 余额扣减必须用事务否则会出大问题会员充值和上下机扣费都涉及余额变更这里的核心问题是一次操作必须同时完成“更新余额”和“插入流水记录”两步要么都成功要么都失败。如果只扣了钱没插记录对账时账目对不上如果插了记录没扣钱等于送了钱的漏洞。我用JDBC自带的事务机制来保证数据一致性。以充值为例首先关闭连接的自动提交然后执行“更新会员余额”和“插入充值记录”两条SQL都成功就提交任何一步失败就回滚。这里有一个很容易被忽略的细节连接要从同一个Connection获取不能用两次DBUtil.getConnection()拿两个连接否则事务根本不在同一个会话里回滚也回不到另一条SQL上。public boolean recharge(int memberId, double amount) { String updateSql UPDATE member SET balance balance ? WHERE id ?; String insertSql INSERT INTO recharge_record (member_id, amount, recharge_time) VALUES (?, ?, NOW()); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 try (PreparedStatement ps1 conn.prepareStatement(updateSql); PreparedStatement ps2 conn.prepareStatement(insertSql)) { ps1.setDouble(1, amount); ps1.setInt(2, memberId); ps1.executeUpdate(); ps2.setInt(1, memberId); ps2.setDouble(2, amount); ps2.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这个模式在所有涉及余额变更的操作里都要用包括充值、下机扣费、购买商品扣费。写代码的时候不要偷懒每个方法都做好事务边界后面在线上环境调Bug的时候会感谢现在的自己。3. 后端源码设计与核心实现3.1 服务端框架ServerSocket线程池的搭建服务端是整个系统的大脑我用ServerSocket监听本机9999端口每个客户端连接来之后交给一个线程池去处理。为什么不直接new Thread处理每个连接因为网吧收银台一般有2到3台客户端但高峰期如果再加上老板的管理端并发连接可能到5到10个线程池能控制并发线程数避免服务端资源被耗尽。线程池我用了Executors.newFixedThreadPool(10)然后给每个连接单独开一个ClientHandler去处理。ClientHandler的核心逻辑是一个死循环不断读取客户端发送过来的指令字符串解析后调用对应的业务方法把结果写回客户端的PrintWriter。整个框架大概长这样ServerSocket serverSocket new ServerSocket(9999); ExecutorService threadPool Executors.newFixedThreadPool(10); while (true) { Socket socket serverSocket.accept(); threadPool.execute(new ClientHandler(socket)); }ClientHandler里有输入输出流用BufferedReader.readLine()读取一整行指令用PrintWriter.println()返回结果。这里有个细节要注意客户端每次发送指令必须带换行符否则readLine()会一直阻塞等不到内容而返回结果也要用println()而不是print()确保服务端消息以换行结尾客户端才能正确解析。3.2 自定义文本协议与消息分发我最初想过用JSON做通信协议但后来觉得小系统用JSON反而增加依赖和解析复杂度。最后采用自定义的文本协议每条消息由命令字和参数组成用竖线分隔。命令字全部大写参数按照固定顺序排列。协议定义如下LOGIN|username|password会员登录REGISTER|username|password|phone|realName开卡注册ONLINE|memberId|computerId上机OFFLINE|memberId|computerId下机RECHARGE|memberId|amount充值QUERY_BALANCE|memberId查询余额服务端拿到消息后先按竖线拆分第一个元素就是命令字再用switch分发到对应的Service方法。每个Service方法最后返回一个字符串作为结果比如SUCCESS|余额98.50或者ERROR|会员不存在。结果字符串再通过PrintWriter返回给客户端。这个协议虽然简单但有一个好处方便排查问题。我在调试的时候直接在收银台机器上用命令行工具telnet 127.0.0.1 9999连服务器手工输入ONLINE|1001|3就能看到服务端返回什么内容。这种直观的调试体验比看日志还舒服强烈推荐在小项目里也用这种朴素协议。3.3 上机、下机与结算的代码实现上机逻辑的完整过程是这样先查会员是否存在、余额是否大于0再查电脑是否空闲然后更新电脑状态为“使用中”最后插入一条online_record记录start_time取数据库当前时间。这里要注意数据库时间和客户端本地时间的差异最好让数据库来生成时间避免客户端电脑时间不准导致计费偏差。下机逻辑相对复杂一点先找到该会员在该电脑上未结束的上机记录拿到start_time再计算费用然后更新电脑状态为空闲、更新上机记录的end_time、duration_minutes、amount最后从会员余额中扣减金额。这三步操作也要开启事务。public String offline(int memberId, int computerId) { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 查询未结束的上机记录 String querySql SELECT id, start_time FROM online_record WHERE member_id ? AND computer_id ? AND end_time IS NULL; PreparedStatement queryPs conn.prepareStatement(querySql); queryPs.setInt(1, memberId); queryPs.setInt(2, computerId); ResultSet rs queryPs.executeQuery(); if (!rs.next()) { return ERROR|没有找到未结束的上机记录; } long startTime rs.getTimestamp(start_time).getTime(); long endTime System.currentTimeMillis(); double hourlyRate getComputerRate(conn, computerId); double fee calculateFee(startTime, endTime, hourlyRate); // 2. 更新上机记录 String updateRecord UPDATE online_record SET end_time NOW(), duration_minutes ?, amount ? WHERE id ?; PreparedStatement updatePs conn.prepareStatement(updateRecord); updatePs.setInt(1, (int) ((endTime - startTime) / 60000)); updatePs.setDouble(2, fee); updatePs.setInt(3, rs.getInt(id)); updatePs.executeUpdate(); // 3. 扣减会员余额 String updateBalance UPDATE member SET balance balance - ? WHERE id ?; PreparedStatement balancePs conn.prepareStatement(updateBalance); balancePs.setDouble(1, fee); balancePs.setInt(2, memberId); balancePs.executeUpdate(); // 4. 更新电脑状态 String updateComputer UPDATE computer SET status 0 WHERE id ?; PreparedStatement computerPs conn.prepareStatement(updateComputer); computerPs.setInt(1, computerId); computerPs.executeUpdate(); conn.commit(); return SUCCESS|本次消费 fee 元; } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return ERROR|系统异常; } finally { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这个流程看着不复杂但有一个隐患如果客户端调用下机接口时断开连接事务可能已经提交但客户端没收到结果。为了解决这个问题我在客户端加入了一个“重试查询”机制下机操作后主动查询一次会员余额如果发现余额没变说明操作可能失败提示收银员手动确认。实际用下来这个兜底方法很管用。4. 前端实现细节4.1 Swing主界面怎么搭才能不乱Swing做界面最容易出现的问题是布局混乱控件放得乱七八糟。我的做法是先定一个主框架JFrame的默认布局用BorderLayout左侧放一个JTabbedPane做功能导航右侧放内容面板。每个功能模块会员管理、上机管理、充值、统计各自是独立的JPanel通过JTabbedPane切换。会员管理页面我用JTable展示会员列表表格上方放一个JTextField做关键字搜索和“查询”按钮底部放“开卡”“充值”“编辑”操作按钮。上机管理页面核心是一个展示电脑状态的面板空闲电脑显示绿色使用中显示红色点击电脑图标弹出上机/下机操作框。这样收银员扫一眼就知道哪台电脑能上机不用去一排排看了。界面代码量不大要注意的坑集中在JTable的刷新上。数据更新后不能只调用table.repaint()因为TableModel没有更新界面不会变。正确做法是重新获取数据后调用table.setModel(new DefaultTableModel(...))或者直接更新DefaultTableModel的内容然后再table.updateUI()。4.2 网络请求放进后台线程严禁卡UISwing是单线程模型所有界面更新必须在事件分发线程EDT里完成。如果直接在按钮的ActionListener里写Socket通信界面会在等待网络响应期间卡死点击任何按钮都没反应收银员会以为软件崩溃了。标准做法是按钮点击后先禁用按钮然后开一个新线程去发送网络请求拿到结果后再通过SwingUtilities.invokeLater()切回EDT更新界面。看下面这个例子loginBtn.addActionListener(e - { loginBtn.setEnabled(false); // 防止重复点击 String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); new Thread(() - { String resp NetClient.send(LOGIN| username | password); SwingUtilities.invokeLater(() - { if (resp.startsWith(SUCCESS)) { // 登录成功跳转主界面 showMainFrame(); } else { JOptionPane.showMessageDialog(this, resp); loginBtn.setEnabled(true); } }); }).start(); });这里的NetClient.send()是我封装的一个静态方法内部创建Socket连接、发送指令、等待响应、关闭连接。每次请求都新建连接虽然开销稍大但对网吧这种低并发场景完全够用而且实现简单不用维护连接状态。4.3 体验小优化全局字体、快捷键、状态栏Swing默认字体在Windows上显示中文不好看我会在程序入口统一设置全局字体用微软雅黑一下子界面档次就上去了。代码很简单在main方法里加一行UIManager.put(Label.font, new Font(微软雅黑, Font.PLAIN, 14)); UIManager.put(Button.font, new Font(微软雅黑, Font.PLAIN, 14)); UIManager.put(TextField.font, new Font(微软雅黑, Font.PLAIN, 14));快捷键也不能小看。收银台高峰时期多敲一下鼠标都觉得浪费时间。我给主要操作都绑定了快捷键F2上机、F3下机、F4充值、Enter默认触发当前页面的主按钮。绑定方式用JRootPane.setDefaultButton()设置回车按钮用KeyStroke注册F2/F3这些快捷键。状态栏也是一个容易给好评的细节。我在主窗口底部加了一个JLabel显示当前时间、连接状态、今日营业额。用一个javax.swing.Timer每秒更新一次时间数据变化时刷新营业额收银员不用切页面就能知道大致的营业情况。5. 常见问题与排查技巧实录5.1 数据库连不上的排查清单数据库连接不上是这个项目里最常见的报错新手遇到Communications link failure常常一头雾水。我整理了一套排查顺序按这个顺序查基本能定位问题。先看MySQL服务有没有启动Windows下按WinR输入services.msc找到MySQL服务确认状态是“正在运行”。再看连接URL里的IP和端口本机用localhost:3306远程服务器要换成实际IP且不能写成127.0.0.1替代导致连错。然后检查驱动版本MySQL 8.x必须用mysql-connector-java8.x版本的JAR包5.x会报Unable to load authentication plugin caching_sha2_password。最后确认URL是不是带了useUnicodetruecharacterEncodingutf8useSSLfalse少一个参数都可能引发编码或SSL握手问题。如果这些都没问题再检查防火墙。Windows防火墙可能会拦截Java进程的3306端口访问需要在防火墙入站规则里放行Java或者MySQL服务端口否则程序启动时能连上跑一段时间后连接被切掉也是这个原因。5.2 界面卡死和中文乱码的坑界面卡死大概率是因为在EDT里做了耗时操作前面讲了解决方案这里再说一个容易忽略的场景登录时如果网络请求放在后台线程里但线程内部又直接调用了JOptionPane.showMessageDialog()一样会导致弹窗不显示或者卡住。正确做法是线程内只处理网络数据所有界面操作都用SwingUtilities.invokeLater()包一层再执行。中文乱码的坑一般有两个来源。第一个是数据库连接没有加characterEncodingutf8插入的中文在MySQL客户端里显示正常但从Java查询出来变成问号。第二个是创建表的时候没有指定字符集解决办法是在建库建表时统一用DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci。utf8mb4比utf8多支持一些特殊字符比如表情符号建议直接用utf8mb4省得以后出问题。5.3 计费不准和并发扣款问题计费不准的bug多半出在时间处理上。我一开始用SimpleDateFormat解析数据库时间字符串来计算时长结果多台收银台同时操作的时候偶尔会出现时长算错的情况。后来查资料发现SimpleDateFormat不是线程安全的多个线程共用同一个实例会导致时间解析错乱。最终方案是全部改用long时间戳计算数据库读出来的时间用getTimestamp().getTime()转成毫秒彻底避开格式化解析的问题。并发扣款是另一个大坑。设想一个场景会员同时在两台收银台被操作收银台A和B同时发起下机扣费如果没有做并发控制会员余额可能被扣两次。解决办法有两种简单粗暴的做法是在member表查余额时加FOR UPDATE行锁确保同一时间只有一个事务能修改该会员余额稍微优雅一点的做法是更新余额时用UPDATE member SET balance balance - ? WHERE id ? AND balance ?这样的条件更新影响行数为0就表示余额不足或已被扣过然后回滚事务。我推荐第二种写法代码改动小且性能更好。6. 项目心得与后续扩展整个项目做完我最大的感受是小系统也必须有严谨的设计意识。表结构、事务边界、线程模型、协议约定这些在项目早期就要想清楚否则后期改起来牵一发动全身。我最开始觉得网吧会员管理系统很简单直接上来写代码结果写到计费模块时发现表结构缺字段又回去改表、改DAO、改界面浪费了整整一天时间。如果一开始就先把数据流和事务边界画清楚后面会顺利很多。还有一点心得不要嫌弃Swing界面老气。对企业内部工具来说稳定、快速、易用远比炫酷重要。Swing虽然外观不够现代化但它跨平台、无依赖、性能稳定在局域网桌面应用场景里依然是可靠的选择。实际部署到网吧后收银员上手很快唯一提的意见是“按钮能不能再大一点”由此可见实用性才是王道。如果后续想扩展有几个方向可以考虑把客户端移植成JavaFX拿到更现代的界面服务端接口改成RESTful API方便接入小程序或微信公众号做会员自助查询对接支付宝/微信支付让会员可以通过扫码自助充值增加Redis缓存热点数据提升高峰期的并发能力。这个系统的架构比较清晰扩展起来并不困难完全可以作为长期维护的项目继续迭代。本文还有配套的精品资源点击获取
返回列表