1. 为什么需要一个Java版的网吧管理系统从痛点说起做了几年Java后端接过的管理系统项目不算少但网吧管理系统这个方向是我觉得特别适合拿来练手、也特别适合写进简历的一个完整闭环项目。原因很简单它麻雀虽小五脏俱全涉及用户管理、商品销售、计费扣费、会员体系、数据统计等多个模块而且业务逻辑足够明确不像某些管理系统那样概念化严重做到最后连需求方自己都说不清要什么。我最早接到类似需求是一个小老板找上门来说想把自己网吧的纸质台账Excel记账模式换成系统化管理。他的痛点是会员充值时手工登记容易漏单机位计时靠服务员人力盯经常出现顾客说还没到时间、服务员说已经超时的扯皮商品销售和上机记录完全割裂月底对账基本靠脑子回忆。这些场景翻译成技术语言就是需要一套包含会员管理、上机管理、商品管理、计费策略、经营统计的完整系统。这篇文章我打算围绕基于Spring Boot和Java技术栈实现网吧管理系统来展开把我自己在设计和编码过程中的关键决策、踩坑经历、模块拆分思路全部写出来。适合三类人看正在做Java课程设计或毕业设计的学生想了解业务管理系统从零落地完整过程的初级开发者以及准备把类似项目写进简历、想提前把技术难点摸清的人。先说清楚这套系统的核心价值不只是能增删改查而是要把网吧经营里最敏感的计费逻辑和会员余额流转做对。前者关系每天的收入准确性后者关系客户信任和财务合规这两个点做扎实了整个系统才真正可用。2. 技术选型为什么是Spring Boot而不是其他方案2.1 Spring Boot解决的核心问题网吧管理系统本质上是一个典型的信息管理系统数据模型不复杂并发量也算不上高——一个中型网吧同时在线也就两三百台机器。但这类项目对开发效率、部署便捷性、后期维护成本的要求很具体Spring Boot恰好在这几个维度上都表现得很均衡。我见过不少用原生ServletJSP做的老系统代码耦合度极高改一个计费规则要牵扯到五六个文件。Spring Boot最大的优势是约定优于配置它通过自动配置把Spring生态里繁琐的XML配置全部干掉了你只需要在application.yml里写上数据源地址、端口号就能快速跑起一个可运行的Web应用。这么说可能有点抽象我举个例子如果用传统Spring MVC搭项目你得手动配置DispatcherServlet、ViewResolver、DataSource、事务管理器还要处理包扫描、依赖注入的XML声明。Spring Boot把这些全部自动完成你只管写业务代码。对于网吧管理系统这种业务逻辑大于技术复杂度的项目节省下来的时间可以全部投入到计费规则和业务边界的设计上。2.2 核心依赖清单与版本选择我这次用的技术栈是Spring Boot 2.7.x MyBatis Plus MySQL 8.0 JWT Vue 3前端部分后端纯Java。考虑到网吧管理系统涉及大量条件查询和分页列表MyBatis Plus的LambdaQueryWrapper能大幅减少手写SQL的工作量尤其是会员列表、商品库存、上机记录这些高频查询场景用起来非常顺手。依赖清单大致如下读者可以根据自己的环境微调版本dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这里有一个版本坑想提前提醒Spring Boot 2.7.x对应的是javax.validation包如果你升级到Spring Boot 3.x包名全变成了jakarta.validation。如果你课程设计用的教材是老版本的Spring Boot混着看网上的代码时很容易被这个问题卡住。我的建议是尽量统一用2.7.x版本因为网上的资料和参考代码绝大多数基于这个版本体系。2.3 为什么不用微服务架构有人可能会问既然学的是Spring Cloud那一套为什么不把用户模块、计费模块、商品模块拆成独立微服务我的回答是要从业务规模反推技术架构。网吧管理系统是单体应用的最佳教学案例之一它的模块间调用非常紧密——上机操作要同时扣余额、生成计时记录、更新机位状态拆成微服务后光事务一致性就能把你折磨死。微服务解决的是大规模协作和独立伸缩的问题而网吧管理系统没有独立的伸缩需求。强行引入微服务只会增加系统复杂度和部署成本。这个项目用模块化单体的方式来组织代码一样可以做到逻辑清晰按业务域拆包控制器层负责参数接收服务层负责业务规则Mapper层负责数据访问层次分明职责单一。3. 数据库设计计费正确性的地基工程3.1 核心表结构一览网吧管理系统的数据库设计是整个项目的灵魂。我见过很多人的表结构设计得乱七八糟会员余额和上机记录混在一起商品销售记录没有关联会员ID导致充了多少钱、花了多少钱、还剩多少钱根本对不上账。下面这五张核心表是必须的少一张系统就会缺胳膊少腿。member会员表字段名类型说明idbigint主键card_novarchar(32)会员卡号业务编号namevarchar(50)真实姓名phonevarchar(20)手机号balancedecimal(10,2)余额statustinyint状态1正常 0禁用created_atdatetime创建时间会员表的核心是balance字段所有涉及余额变动的操作都必须通过事务控制不允许任何Service方法直接update member set balance balance - X而不考虑并发问题。余额更新的正确姿势是使用乐观锁机制后续我会在计费模块详细讲。computer机位表字段名类型说明idbigint主键computer_novarchar(20)机位编号如A-01area_typevarchar(20)区域类型普通区/电竞区/包间statustinyint状态0空闲 1使用中 2维护中hourly_pricedecimal(10,2)每小时单价机位表的关键是area_type和hourly_price的关联。不同区域的计费单价不同这是业务规则应该固化在数据库里而不是写死在代码中。我设计的时候还加了一个computer_type字段用来标识普通机器和电竞机器方便后续做差异化计费。subscription_session上机记录表核心中的核心字段名类型说明idbigint主键member_idbigint会员ID临时上机为NULLcomputer_idbigint机位IDstart_timedatetime上机开始时间end_timedatetime上机结束时间duration_minutesint时长分钟total_feedecimal(10,2)总费用statustinyint状态0进行中 1已结束这张表记录了每一次上机行为的完整生命周期。开始上机时插入一条start_time和当前时间相等的记录状态为0下机时更新end_time、计算时长和费用、状态置为1。所有计费统计都基于这张表不允许从其他表反推。sale_order商品销售表字段名类型说明idbigint主键member_idbigint会员ID散客为NULLcommodity_idbigint商品IDquantityint数量total_pricedecimal(10,2)总价sale_timedatetime销售时间commodity商品表字段名类型说明idbigint主键namevarchar(100)商品名称pricedecimal(10,2)售价stockint库存statustinyint状态1上架 0下架3.2 计费相关的字段设计为什么必须谨慎计费是网吧管理系统最敏感的业务数据库层面的设计直接决定了计费逻辑能否正确落地。我特别想强调两点第一金额字段一律使用decimal绝对不要用float或double。浮点数在计算机中以二进制存储0.10.2这种看起来很简单的运算都会产生精度误差。如果会员充100元上机每小时5元上机0.7小时看起来费用是3.5元但用float计算可能得到3.4999999。虽然差异极小但积少成多月底对账的时候绝对会让你怀疑人生。第二必须区分费用计算中单价和最终费用两件事。我设计时把机位表的hourly_price定义为基准单价subscription_session表的total_fee定义为最终收取的费用。这两者之间存在折扣、凑整等业务规则比如会员打八折、按半小时取整这些规则都放在Service层处理数据库只负责存储结果。这样做的目的是保证已发生过的事实历史订单数据不会被后续规则修改所影响。第三计算机位增减状态变更的并发问题。先插入subscription_session记录再更新computer表的状态为使用中这两个操作必须放在同一个事务里。如果先改机位状态再插入记录一旦插入失败就会出现机位显示占用但实际没有上机记录的问题直接导致计费丢单。4. 核心模块拆解从登录鉴权到上机计费4.1 JWT登录鉴权模块状态管理如何做到无状态网吧管理系统通常有两类用户管理员和收银员。我在设计登录模块时直接选择了JWTJSON Web Token方案而不是传统的Session方案。原因是网吧管理系统的前端大概率是一个前后端分离的单页应用后端只提供API接口用Session需要在服务端维护会话状态跨域还要处理Cookie问题非常麻烦。JWT的核心理念是把用户身份信息加密签名后发放给客户端后续请求带上Token即可后端无需存储任何会话数据。我在pom.xml中引入jjwt依赖后封装了一个JWT工具类主要包含三个方法生成Token、解析Token、校验Token是否过期。Token中存放的信息包括用户ID、用户名、角色类型有效期设置为8小时超过后前端需要重新登录。Component public class JwtUtil { private static final String SECRET_KEY your-secret-key-at-least-32-chars; private static final long EXPIRE_TIME 8 * 60 * 60 * 1000; public String generateToken(Integer userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); } }有了这个工具类还不够必须配合拦截器或过滤器对请求进行统一鉴权。我使用的是Spring MVC的HandlerInterceptor方案在preHandle方法里从请求头取Token解析失败或超时直接返回401状态码。这里有个容易被忽视的细节放行登录接口和静态资源。如果拦截器把/api/auth/login也拦了用户永远无法登录这种低级错误会让整个系统直接瘫痪。4.2 会员管理模块余额变动的乐观锁设计会员管理看似简单无非是开卡、充值、消费、查询但余额变动的并发安全是很容易粗心出错的地方。一个典型场景顾客在收银台充值时家人同时在另一个窗口用同一张会员卡买饮料如果不做并发控制余额就可能出现更新丢失——后一个操作覆盖了前一个操作的充值结果。解决这个问题有几种方案悲观锁SELECT ... FOR UPDATE、乐观锁version字段、或者Redis分布式锁。考虑到网吧管理系统的并发量并不高我选择了乐观锁实现简单且足够用。具体做法是给member表增加一个version字段更新余额时检查当前版本号是否匹配匹配才执行更新。Update(UPDATE member SET balance balance #{amount}, version version 1 WHERE id #{memberId} AND version #{oldVersion}) int updateBalance(Param(memberId) Integer memberId, Param(amount) BigDecimal amount, Param(oldVersion) Integer oldVersion);充值接口的业务逻辑是这样的查询会员当前版本号计算出新的余额执行带版本号的更新SQL如果返回影响行数为0说明余额已经被其他人更新过此时要抛出异常让用户重试或提示余额正在变动请稍后重试。另外一个需要注意的点是充值金额和赠送金额是否分开存储。很多网吧有充100送20的营销活动我建议在数据库里增加present_balance字段单独存放赠送金额消费时优先扣赠送金额到期后赠送金额清零。这样财务上能清楚区分实收金额和赠送金额对账时一目了然。4.3 上机与下机流程计费规则如何落地为代码上机下机是整个系统里业务逻辑最复杂的模块因为计费规则有很多边界情况不足半小时怎么算跨区域换机怎么算会员折扣和时段折扣能否叠加这些规则如果写不清楚后期维护就是灾难。我的做法是把计费规则抽象成一个独立的ChargeStrategy接口不同的计费策略实现不同场景的需求public interface ChargeStrategy { BigDecimal calculateFee(BigDecimal hourlyPrice, long minutes); } Component(normalChargeStrategy) public class NormalChargeStrategy implements ChargeStrategy { private static final int STEP_MINUTES 30; Override public BigDecimal calculateFee(BigDecimal hourlyPrice, long minutes) { long chargeUnits (minutes STEP_MINUTES - 1) / STEP_MINUTES; return hourlyPrice .multiply(BigDecimal.valueOf(chargeUnits)) .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); } }这个策略实现的是不足半小时按半小时算的规则。比如每小时5元上机42分钟按半小时向上取整就是1.5个小时费用为7.5元。把计费规则封装成独立接口的好处是后续如果网吧老板说我们要改成不足15分钟按15分钟算只需要新增一个策略实现注入对应Bean即可完全不用改动上机下机的核心流程代码。上机接口的核心代码逻辑是校验机位状态为空闲创建上机记录将机位状态改为使用中这三个操作组成一个事务。下机接口的逻辑相反查询进行中的上机记录计算时长和费用更新会员余额如果是会员卡上机将机位状态改为空闲更新上机记录的状态。我还额外处理了一个场景强制下机。如果顾客忘记下机直接走了收银员需要手动终止这台机位的上机状态同时按实际运行时间收费。处理方式是下机接口接受一个可选参数forceOffline传true时即使当前没有会员关联也允许结束会话费用记录到无会员临时订单中。4.4 商品管理模块库存扣减与销售统计网吧卖饮料零食是重要的收入来源这个模块虽然逻辑简单但库存扣减同样存在并发问题。收银台频繁扫码销售两个窗口同时卖同一瓶饮料库存就可能扣成负数。我的方案是使用条件更新UPDATE commodity SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}。如果影响行数为0说明库存不足直接抛异常提示库存不足请补货。这个方案比先查库存再更新的方式更可靠因为查询和更新天然在一个原子操作中完成无需额外加锁。销售数据统计我设计了独立的视图查询按天、按月汇总销售收入、销售数量、热销商品排行。实现方式是MyBatis的Group By查询配合时间范围条件。比如查询某天的所有销售订单按商品ID分组求得数量和金额总和select idsumSaleByCommodity resultTypemap SELECT commodity_id, SUM(quantity) as total_quantity, SUM(total_price) as total_amount FROM sale_order WHERE sale_time BETWEEN #{startTime} AND #{endTime} GROUP BY commodity_id ORDER BY total_amount DESC /select5. 报表统计与数据看板如何把经营数据变成决策依据5.1 实时经营看板的数据来源和缓存策略网吧老板最关心的是今天赚了多少、上座率怎么样、哪些机器闲置多。我在系统里做了一个实时经营看板展示核心指标今日营收、今日上机收入、商品销售收入、当前在线人数、机位使用率。这些指标的计算方式各不相同今日营收直接查subscription_session表中当天结束时间和sale_order表中当天销售时间的数据求和当前在线人数直接查subscription_session表中状态为0的进行中记录数量机位使用率则用在线机位数除以总机位数。有一个性能问题值得注意如果每次页面刷新都实时查数据库在数据量增大后会产生不少无用查询压力。我的处理方式是使用本地缓存LoadingCache给营收和在线人数设置30秒的过期时间数据变化不要求毫秒级实时的场景完全够用。如果你后续想做得更高级可以引入Redis但现阶段本地缓存已经能很好地平衡实时性和数据库压力了。5.2 计费流水与充值记录的对账查询对账是整个系统里财务人员使用最频繁的功能。设计时我把充值流水和消费流水分开存储充值记录表记录每笔充值金额、赠送金额、支付方式消费记录从subscription_session和sale_order中提取。财务人员只需输入日期范围就能看到完整的资金流转明细。这里我踩过一个坑一开始把充值记录和消费记录合并在一张流水表里用type字段区分收入支出后来发现虽然一个表更简洁但充值记录有赠送金额这个专属字段而消费记录需要关联商品名称合并后大量字段空置表结构显得很松散查询性能也不如分开好。经过重构后拆成两张表字段各自完整关联关系也更清晰。对账功能里有个隐藏需求日结报表。网吧通常每天凌晨做一个日结把当天的所有收入和支出汇总成一张报表确认无误后归档。我的实现方式是定时任务在每天凌晨1点自动生成前一天的数据报表存入daily_report表日结状态标记为未确认。财务人员确认后状态改为已确认报表不可再修改。这样保证了一层审计安全也方便后续对账追查。6. 项目落地的调试经历三个印象深刻的Bug6.1 并发上机导致机位状态错乱的完整排查链路做联调的时候遇到过一个问题两台电脑同时请求上机同一台机位一个顾客在收银台刷卡另一个顾客在自助机上操作结果系统里有两条上机记录机位状态却显示空闲。我当时排查的思路是先看日志发现两个请求都通过了机位状态空闲的判断条件然后做了插入上机记录、更新机位状态的操作。问题根源在于先查询再更新的check-then-act竞态条件两个请求同时查询到空闲状态又同时执行更新最后一条更新覆盖了前一条但上机记录没有回滚。解决方案有两种第一种是给computer表的更新操作加条件WHERE status 0影响行数为0则说明机位被占用直接返回该机位已被使用第二种是使用数据库行锁SELECT ... FOR UPDATE锁定机位行串行化处理。我最终选择的是第一种因为实现成本最低而且在这个场景下语义最清晰。6.2 JWT时间戳遇到的毫秒级过期问题有一次测试人员反馈用户登录后很快就让你重新登录我一开始以为是Token过期时间设置太短后来检查发现setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME))这个写法本身没错问题出在System.currentTimeMillis()返回的是毫秒时间戳而JWT规范要求的是秒级时间戳我在写解析工具时用的是秒级比较导致Token一生成就显示已过期。这个Bug排查了很久因为不是必现问题偶尔正常偶尔失效。最终的修复方案是统一时间单位和比较方式生成Token时用毫秒解析时也统一转成毫秒再比较同时加了一个允许5秒的时钟偏移容差。排查链路总结下来就是前后端时间基准不一致、JWT库的时间单位理解错误、调试日志不到位三者叠加才导致问题如此隐蔽。6.3 金额四舍五入导致报表差一分钱有一次对账发现报表总金额差一分钱我和财务人员确认了半小时才发现问题出在舍入模式上。BigDecimal.divide如果不指定RoundingMode默认会抛异常指定了不同的舍入模式结果可能不同。比如BigDecimal.valueOf(10).divide(BigDecimal.valueOf(3), 2)用HALF_UP结果是3.33用HALF_EVEN结果也是3.33但换成3.335这样的边界值不同舍入模式就出现差异了。我查了所有涉及金额的计算代码最后统一封装了一个金额工具类所有计费和折扣计算强制指定RoundingMode.HALF_UP并且除法保留两位小数。这样看似不起眼的细节恰恰是财务系统最容易出问题的隐患。从那以后我给自己定了一条规矩凡是涉及金额和计费的运算必须显式指定舍入模式禁止使用依赖默认行为的写法。7. 这个系统还能怎么扩展进入生产环境的进阶方向网吧管理系统做完基础版本之后如果要真正投放到生产环境有几个方向值得继续深入。第一个是预约选座功能。现在很多网吧支持线上预约顾客可以在小程序或App上选择机位和时间段提前下单。这个功能牵涉到机位的时段占用需要维护一张reservation表在预约时间段内将机位状态置为已预约同时要处理预约超时释放的定时任务。第二个是弹性计费策略。现在的计费策略是固定单价按时长计算生产环境中更常见的是分时段计费早上8点到下午4点是普通价晚上6点到凌晨12点是高峰价周末再叠加活动价。这个需要把计费策略升级为时间维度可配置我建议设计成规则表而不是继续在Java代码中硬编码。第三个是操作日志与审计追踪。所有涉及余额变动、机位变更、商品库存变动的操作都应该记录操作人、操作时间、操作前后数据快照方便日后追责和审计。这个模块在课程设计中可以不考虑但一旦进入生产环境财务审计是必须的。第四个是数据备份与恢复策略。网吧的数据量虽然谈不上大但每天的经营流水和会员余额都很重要一旦误操作或数据库损坏损失不可估量。建议使用MySQL的mysqldump做每日自动备份保留至少7天的备份文件。具体做法可以写在服务器crontab里也可以用Spring Boot的定时任务调用Shell命令执行。我自己做完这套系统后最大的感受是技术本身不复杂但把业务规则梳理清楚、处理好边界情况、想明白每一步操作的失败兜底才是这类系统真正容易翻车的地方。如果你正在做类似的课程设计或者想转行做Java后端建议不要只盯着CRUD试着把计费规则、并发控制和财务对账这三块吃透面试的时候这就是你的加分项。
