尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于Java的网吧会员管理系统设计与实现

基于Java的网吧会员管理系统设计与实现 简介在企业级应用开发中B/S架构已成为信息管理系统的主流选择而事务一致性与并发控制则是后端开发的核心挑战。以Spring Boot、MyBatis等主流框架为基础通过合理的数据库表设计与原子更新操作可以有效保障会员余额、上机计时等关键数据的准确性。此类技术方案广泛应用于网吧、网咖等场所的会员管理和计费场景解决手工登记效率低、账目易出错等痛点。本文围绕一套完整的网吧会员管理系统从数据库设计、后端接口开发到Layui前端页面实现详细讲解上机下机自动计费、充值流水、营收统计等核心功能并给出可直接落地的源码与部署避坑指南为类似管理系统的开发提供切实参考。 先交代一下背景。去年接了个网吧老板的单子需求就三句话会员办卡充值要在电脑上搞定上机下机能自动计时扣费每天营收多少一眼能看清。听上去简单实际动手做才发现这里面牵扯到数据一致性、并发扣费、跨天计费这些硬骨头。这篇文章就围绕这套基于Java的网吧会员管理系统设计源码和前端实现把我踩过的坑、验证过可行的方案从头到尾讲一遍。如果你正准备做类似的Java课程设计、毕业设计或者接了店里的小系统项目可以直接照着我这套思路往下落。内容涉及数据库表设计、Spring Boot后端接口、Layui前端页面、本地部署四个大块每一块我都会给出能直接用的代码和配置。1. 项目整体定位与技术选型1.1 网吧会员系统到底在解决什么问题很多刚入门的同学一听“网吧会员管理系统”第一反应是“这不是很简单的增删改查吗”。真做起来你会发现增删改查只是外壳核心难点全在业务规则上。过去网吧用手工登记会员开卡就发一张卡充值和消费全记在纸质本子上。老板最头疼三件事账对不上、会员余额记不清、上机时间靠人肉盯。所以这套系统首先要解决的是把“会员信息、账户余额、上机计费、充值流水、营收统计”这五块业务全部数字化而且每一步都要有据可查。基于这个目标系统拆出来大概七个功能点管理员登录、会员开卡/挂失/注销、会员充值、上机登记、下机结算、消费明细查询、今日营收统计。会员又分两种一种是正式会员用卡号关联账户一种是临时卡不用办卡交押金直接开台。临时卡在结算时按实际上机时间扣费剩下的押金退还。这两类用户的计费逻辑一样但会员会关联余额账户临时卡则是在结算时算差额所以数据库层面要有区分字段。1.2 技术选型三套方案里我为什么选这套先说我踩过的一个选择坑。最早我想用纯Java Swing做桌面端毕竟网吧收银台就是固定一台电脑桌面端似乎更直接。但后来想清楚一个问题老板虽然只在店里看数据但会员系统未来极可能要做微信端查询甚至连锁店统一管理桌面端在扩展性上是死路。于是直接转向B/S架构浏览器访问后端用Java前端单独做页面这也是当前主流招聘简历上最常见的组合。具体技术栈我定的是Spring Boot 2.7 MyBatis MySQL 8.0 Layui。为什么选Spring Boot而不选传统SSM因为SSM的XML配置太啰嗦Spring Boot自动配置极大减少了环境搭建成本开发期间不需要关心Tomcat部署一个jar包直接跑起来。MyBatis则有很强的SQL可控性像余额更新、报表聚合这类SQL我能精确控制比Hibernate更适合这种偏业务计算的场景。前端没有用前后端分离的Vue而是用Layui原因很实际项目本身页面复杂度不高Layui自带表格、表单、弹层和日期组件几行JS就能渲染出后台管理界面省去构建Vue工程和跨域联调的环节。如果你们是多人协作、页面交互重那可以换成VueElement UI但当前这个体量Layui性价比最高。这里顺带说一句选择技术栈先看场景复杂度不要为了所谓“主流”强行上重框架。网吧会员系统是典型的内部管理系统用户量就是店内几台收银机并发极低真正要关注的是业务正确性和开发效率。2. 数据库设计几张核心表奠定整个系统2.1 从业务反推表结构数据库设计绝对不能上来就建表而是先走业务流程。我梳理了五个核心实体管理员、会员、电脑、上机记录、充值记录外加一张消费记录表。每一张表的字段我都坚持一个原则能存原始数据就不要存计算结果时间字段必须精确到秒金额字段全部用DECIMAL避免浮点数精度丢失。会员表是核心字段包括id、卡号card_no、姓名name、手机号phone、余额balance、状态status、创建时间create_time。卡号必须唯一这是会员身份标识。状态字段用TINYINT0代表正常1代表挂失2代表注销。这里有个细节注销不是物理删除因为会员的历史消费记录还要留底物理删了流水就成孤儿数据了。余额字段我用DECIMAL(10,2)默认0绝不用float。上机记录表machine_record则关联了member_id和computer_id还记录了开始时间start_time、结束时间end_time、时长duration_minutes和消费金额amount。表里专门加了一个is_temp字段区分临时卡与正式会员这样统计正式会员消费和临时卡押金时就能直接筛选。电脑表computer单独维护机位信息字段包括machine_no、area普通区、电竞区、包间、unit_price区域单价元/小时和status0空闲、1使用中、2维护。区域单价放在电脑表是为了方便老板调价不同区域价格可能不一样同一区域所有机器共用单价这也简化了计费逻辑。下面是核心建表SQL我精简了不相干的字段CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, phone VARCHAR(11), balance DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1挂失 2注销, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE computer ( id INT PRIMARY KEY AUTO_INCREMENT, machine_no VARCHAR(10) NOT NULL UNIQUE, area VARCHAR(20) COMMENT 普通区/电竞区/包间, unit_price DECIMAL(10,2) COMMENT 每小时单价, status TINYINT DEFAULT 0 COMMENT 0空闲 1使用中 2维护 ); CREATE TABLE machine_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT, computer_id INT, machine_no VARCHAR(10), start_time DATETIME, end_time DATETIME, duration_minutes INT, amount DECIMAL(10,2), is_temp TINYINT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1上机中 2已下机, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE recharge_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT, amount DECIMAL(10,2), operator_id BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里我要特别说明为什么把消费流水单独拆一张consume_record表而不是只靠上机记录去统计。因为网吧除了上网费还有卖饮料、零食的消费场景单一的上机记录承载不了商品消费。虽然不是第一版需求但表结构上预留好后续扩展就不需要大改。consume_record表字段设计为member_id、record_id关联上机记录可为空、amount、create_time。2.2 计费与流水表的设计细节计费规则看起来简单就是“每小时单价按分钟计费”但真落到SQL里有两个坑。第一个坑是金额字段类型。如果用了FLOAT下机结算时0.10.2这类计算会出现丑陋的浮点尾巴导致账目对不上。我统一用DECIMAL(10,2)所有金额运算都在Java中以BigDecimal完成特别是充值时页面拿到String类型的金额一定要用new BigDecimal(str)而不是Double.parseDouble否则精度又会丢。实际开发中我吃过这个亏10块钱充进去余额变成9.999999998老板对账时怎么看怎么不对劲。第二个坑是流水表和数据一致性之间的关系。每笔充值、每笔扣费都必须对应一条流水记录不能只改member表的balance字段。举例说明会员充值时我先往recharge_record插入一条数据再更新member表balance这两个操作必须放在同一个数据库事务里否则插入成功但余额没加上账就错了。我当时用的是Spring的Transactional注解默认遇到RuntimeException就回滚但要注意MySQL的引擎必须是InnoDBMyISAM不支持事务建表时如果沿用旧习惯就会静默失效。下机计费还有一个容易遗漏的点电脑状态和时间。下机时不仅要更新上机记录的end_time和amount还必须把computer表的状态改回0空闲这两个动作同样要在事务里完成。如果只改了记录没改电脑状态下一单上机时发现机器还在“使用中”顾客就直接傻眼。我在实际开发过程中就反复遇到这类状态不同步问题解决方案就是把状态变更和记录更新绑定在同一个Service方法里谁也别想漏。3. 后端核心逻辑实现3.1 会员管理模块后端整体按照三层结构走Controller接收参数返回JSONService写业务逻辑Mapper操作数据库。先看会员新增和分页查询这是最常用的两个接口。新增会员的Controller我这样写RestController RequestMapping(/api/member) public class MemberController { Resource private MemberService memberService; PostMapping(/add) public Result add(RequestBody Member member) { // 卡号手动生成前缀M 时间戳后六位避免用户自己乱填 String cardNo M System.currentTimeMillis() % 1000000; member.setCardNo(cardNo); return memberService.add(member); } GetMapping(/page) public Result page(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String keyword) { return memberService.page(pageNum, pageSize, keyword); } }Service层有个细节新增时手机号不是必填的但卡号必须唯一。用户如果不小心重复提交数据库唯一索引会兜底但返回的异常信息用户看不懂所以我在Service里捕获DuplicateKeyException转换成友好提示“卡号重复请重试”。分页查询则直接用MyBatis的PageHelper插件一行代码搞定物理分页不用手写LIMIT。public Result page(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListMember list memberMapper.selectByKeyword(keyword); PageInfoMember info new PageInfo(list); return Result.success(info); }这里我用了PageHelper但生产环境要注意一下依赖版本和Spring Boot版本的兼容性PageHelper 5.x对Spring Boot 2.x支持得不错。如果不用插件手写LIMIT也完全可以关键是SQL里要对keyword做一个LIKE匹配select idselectByKeyword resultTypeMember SELECT * FROM member where if testkeyword ! null and keyword ! card_no LIKE CONCAT(%, #{keyword}, %) OR name LIKE CONCAT(%, #{keyword}, %) OR phone LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY create_time DESC /select3.2 上机、下机与充值扣费逻辑上机动作的逻辑是校验会员状态和余额如果余额小于预设押金可以拒绝上机如果电脑非空闲则报错。校验通过后把电脑状态改为1使用中插入一条machine_record状态为1。下机逻辑则是结算重点先查出上机记录计算当前时间和start_time的分钟差再根据电脑单价算出金额更新记录状态和余额最后把电脑状态改回空闲。下机核心代码我写在这里注释标注了关键点Transactional public Result checkout(Long recordId) { MachineRecord record recordMapper.selectById(recordId); if (record null || record.getStatus() 2) { return Result.error(上机记录不存在或已结算); } Computer computer computerMapper.selectById(record.getComputerId()); if (computer null || computer.getStatus() ! 1) { return Result.error(电脑状态异常); } // 计算分钟差向上取整防止用户上机1秒扣0元 long minutes Duration.between(record.getStartTime(), LocalDateTime.now()).toMinutes(); if (minutes 1) minutes 1; BigDecimal unitPrice computer.getUnitPrice(); BigDecimal amount unitPrice.multiply(BigDecimal.valueOf(minutes)) .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); // 更新上机记录 record.setEndTime(LocalDateTime.now()); record.setDurationMinutes((int) minutes); record.setAmount(amount); record.setStatus(2); recordMapper.updateById(record); // 正式会员从余额扣费临时卡只记录不扣余额 if (record.getIsTemp() 0) { memberMapper.decreaseBalance(record.getMemberId(), amount); } // 电脑释放 computer.setStatus(0); computerMapper.updateById(computer); return Result.success(amount); }这里有一个非常关键的并发扣费点。如果会员同时在两台机器上上机或者收银员手一抖点了两下下机可能产生重复扣费。解决方式我在Mapper里用了一条原子更新语句UPDATE member SET balance balance - #{amount} WHERE id #{memberId} AND balance #{amount}当返回受影响行数为0时说明余额不足或账户不存在上游方法直接抛出业务异常回滚事务这样就能避免并发时余额被扣成负数。这条SQL其实就相当于数据库层面的乐观锁简单又可靠。充值逻辑相对简单但同样要注意“先插流水再更新余额”的顺序并且开启事务。存入充值流水时我会记录操作员ID方便老板事后审计哪个员工什么时候给哪个会员充了多少。这一步后来被老板专门表扬过说终于能查到是不是有人私下乱改余额了。3.3 当日报表统计日报表是老板每天打开看的第一屏。我提供两个统计数据今日营收全部已下机的消费金额之和、今日充值总额、当前在线人数。SQL用聚合函数直接算比在Java内存里汇总效率高。-- 今日营收仅统计已下机的记录 SELECT COALESCE(SUM(amount), 0) FROM machine_record WHERE status 2 AND DATE(end_time) CURDATE(); -- 今日充值总额 SELECT COALESCE(SUM(amount), 0) FROM recharge_record WHERE DATE(create_time) CURDATE(); -- 当前在线人数 SELECT COUNT(*) FROM machine_record WHERE status 1;这里要注意一个坑DATE(end_time) CURDATE()这类写法在数据量大时无法走索引全表扫描会越来越慢。对于网吧管理系统单店一天几百条消费记录这个写法完全没问题但如果未来做连锁就必须改成范围查询end_time 2024-05-20 00:00:00 AND end_time 2024-05-21 00:00:00。我在代码注释里专门提醒了自己也提醒后续看代码的人。4. 前端实现界面和交互怎么做4.1 页面框架搭建前端这块如果一上来就写CSS布局纯属自我消耗。我选了Layui后直接用它的后台布局脚手架。整体页面分三块顶部是系统标题和当前管理员退出按钮左侧是功能菜单右侧是内容iframe区。菜单项就是会员管理、电脑管理、上机管理、充值与报表这样老板和收银员用起来很直观不用教。登录页是最容易被忽视但用户最先接触的页面。我的做法是单独做一张背景干净的卡片式登录框用户名密码输入框加上一个简单的验证码调用后端接口生成防止机器暴力尝试。验证码实现不复杂后端生成4位随机数写入Session前端用Canvas画出来。项目工期不紧的话建议加上这一步效果十分实在。主界面只需要一个index.html和若干子页面。我在index.html里把左侧菜单绑定好点击菜单项时iframe的src切换到对应子页面。这样写法很“古老”但稳定可靠而且Layui的官方文档就是这种风格出问题好排查。4.2 会员列表与上机面板的交互会员列表页面是数据展示的核心我用了Layui的table组件加上分页条。数据源直接对接后端 /api/member/page接口table组件会自己带上页码和每页条数参数。它的好处是渲染表格不用手写一堆tr/tdtable.render一行JS就能搞定而且支持表头排序、行点击事件很适合这种内部管理系统。上机面板是这家网吧最常用的操作界面。我给收银员设计了一个联动交互左侧选电脑区域右侧显示该区域电脑列表绿色表示空闲红色表示使用中。点击绿色电脑弹窗输入会员卡号后端校验通过后直接上机并刷新电脑状态。下机则更直接点击红色电脑弹窗显示“已用时长、当前费用、确认下机”按钮点确认后调后端的checkout接口。这里的实现有个小技巧电脑状态变化后页面不能靠用户手动刷新而是要在每次上机/下机操作成功后重新请求一个查询所有电脑状态的接口然后动态更新CSS样式。我用jQuery的$.getJSON配合Layui的layer.load做加载提示操作反馈非常实时。前端代码示例上机时发送的AJAX请求function doOnMachine(machineId) { var cardNo $(#cardNoInput).val().trim(); if (!cardNo) { layer.msg(请输入会员卡号); return; } var loadIndex layer.load(1); $.post(/api/machine/online, { machineId: machineId, cardNo: cardNo }, function (res) { layer.close(loadIndex); if (res.code 200) { layer.msg(上机成功); refreshMachineList(); // 刷新电脑状态 } else { layer.msg(res.msg); } }, json); }上面这段代码最值得强调的是后端接口返回的数据格式一定要统一。我强制所有接口返回{code: 200, msg: success, data: ...}前端只要判断code是否等于200就统一处理成功和失败。如果每个接口返回格式都不一样前端就要写一堆if判断维护成本会直线上升。4.3 前端细节体验优化用户天天对着这个系统体验细节不能糊弄。我在三个小地方做了优化老板后来都很认可。第一个是金额输入框做了“只能输入数字和小数点后两位”的校验从源头挡住非法字符。第二个是会员卡号输入后自动拉取会员信息显示姓名和余额收银员不用等后台查询结果就能确认是不是本人。第三个是下机确认弹窗中用醒目的红色字体显示本次消费金额避免顾客结账时产生纠纷。关于页面刷新我做了一个隐藏逻辑电脑列表每30秒自动轮询一次状态即使收银员没有操作系统也会自动把突然掉线的电脑状态同步回来。这个设计出自一个实际场景当时有台机器自己重启了但页面还停留在上机状态顾客下了机系统却还扣着费后来加了这个轮询才基本杜绝。5. 源码结构、本地运行与避坑清单5.1 项目目录结构与启动步骤我把源码搞得比较规整目录结构如下方便大家对照netbar-member/ ├── src/main/java/com/example/netbar/ │ ├── controller/ │ │ ├── MemberController.java │ │ ├── MachineController.java │ │ └── RechargeController.java │ ├── service/ │ │ ├── MemberService.java │ │ ├── MachineService.java │ │ ├── RechargeService.java │ │ └── impl/ │ ├── mapper/ │ │ ├── MemberMapper.java │ │ ├── MachineMapper.java │ │ └── RecordMapper.java │ ├── entity/ │ │ ├── Member.java │ │ ├── Computer.java │ │ └── MachineRecord.java │ └── common/ │ └── Result.java ├── src/main/resources/ │ ├── mapper/ │ │ ├── MemberMapper.xml │ │ ├── MachineMapper.xml │ │ └── RecordMapper.xml │ └── application.yml ├── sql/ │ └── init.sql └── pom.xml运行前置环境三件套JDK 8或11、Maven 3.6、MySQL 5.7或8.0。先把sql/init.sql导入数据库再修改application.yml里的数据库账号密码最后在项目根目录执行mvn spring-boot:run。启动后用浏览器访问localhost:8080默认管理员admin/admin123。JDK版本这里提醒一句Java 8和Spring Boot 2.7的搭配最稳妥不要为了追新直接上Java 17因为有些老依赖反射会报错。我之前被Java 17坑过一个CGLIB代理问题排查了一下午后来老老实实切回8。5.2 新手最容易踩的五个坑第一个坑是数据库连接配置。MySQL 8.0版本的驱动类名是com.mysql.cj.jdbc.Driver不是旧的com.mysql.jdbc.Driver。同时URL里要加serverTimezoneAsia/Shanghai和useSSLfalse否则会报时区错误或者SSL握手失败。这些在application.yml里必须提前配好spring: datasource: url: jdbc:mysql://localhost:3306/netbar_db?serverTimezoneAsia/ShanghaiuseSSLfalsecharacterEncodingutf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver第二个坑是中文乱码。如果你遇到的页面显示乱码优先检查三处数据库表是不是utf8mb4、connection的characterEncoding是不是utf8、前端页面的meta charset是不是utf-8。三处都要一致缺一个都会出乱码。数据库建库语句我直接用CREATE DATABASE netbar_db DEFAULT CHARACTER SET utf8mb4;第三个坑是前端请求404。如果页面能打开但点击功能后网络请求404多半是后端接口路径没对上。我建议前端所有请求都以/api开头后端Controller类上全部标注RequestMapping(/api/xxx)这样路径一目了然。排错时打开浏览器F12看Network比瞎猜高效得多。第四个坑是跨天计费。有的机器晚上11点上机凌晨1点下机按时间差计算完全没问题因为LocalDateTime计算就是毫秒差不存在“日期跨天”这个概念。但如果你是按日期字符串截取来算的就一定会算错。我这里用Duration.between()就是为了绕过这个坑。唯一要注意的是如果网吧设置通宵包时优惠需要在计算前加一个判断如果开始时间在22点后则按通宵价计费。第一版我没做这个被老板提了需求后补上的。第五个坑是并发问题。虽然单店收银并发很低但会员余额扣费这种操作绝不能忽略并发一致性。除了前面提到的原子UPDATE语句我还统一收银台只放一台电脑操作从物理上降低并发概率。如果以后做连锁还需要引入Redis锁或者分布式事务不过这就是另一个话题了。做完这套系统我最大的体会是业务系统开发代码本身没有多玄妙真正拉开差距的是对业务细节的把控。会员充值会不会被重复提交下机结算会不会因为异常导致电脑一直占着报表数据是不是和前台收银记录对得上这些才是老板真正在意的问题。你在参考这套源码时不光是跑起来看效果更建议把每个事务方法都读一遍想清楚为什么充值、上机、下机都必须加Transactional理解透了以后做任何管理系统都能触类旁通。最后再分享一个小技巧这套源码里前端的验证码接口、会员开卡时间生成、统计报表的SQL聚合都是能直接复用到其他管理系统的通用模块。你做下一个项目时把会员表换成客户表把上机记录换成订单表基本框架不用动很快就能交付新需求。本文还有配套的精品资源点击获取
返回列表