news 2026/10/20 11:52:46

SpringBoot台球厅管理系统实战:从计费算法到部署上线全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot台球厅管理系统实战:从计费算法到部署上线全解析

台球厅的管理,听起来不是什么高深的技术场景,可一旦真去盘它的业务,你很快就会发现:桌台费按时间走、酒水商品按数量走、会员卡还有余额和积分,三套账搅在一起,光靠收银员在本子上记,绝对对不上。我第一次给朋友的台球厅做管理系统时,对方提的需求就一句话——“别让客人觉得我们多收钱,也让我自己知道一天到底赚了多少”。就是这句话,让我老老实实从开台、计费、结算、报表这些业务倒推回来,用SpringBoot做了一套完整的台球厅管理系统。这套系统的设计和实现过程,对正在选毕设题目的同学、想给自家小店做轻量后台的店主,都挺有参考价值。下面我把从需求拆解到部署上线的全流程,连同踩过的坑一起整理出来。

1. 先从业务拆需求:台球厅的钱到底怎么算

1.1 台球厅的天花板与钱袋子

台球厅不是单纯的“按小时卖桌子”。大多数球房的真实营收结构是这样的:桌台费是主收入,通常分普通台、斯诺克台、VIP包间,价格不一样;高峰时段和闲时价格也不一样;VIP包间还可能设最低消费。第二块收入是商品销售,也就是吧台卖饮料、零食、香烟,这部分虽然单价低,但毛利高,客人打球打到一半基本都会消费。第三块是会员储值,客人往会员卡里充钱,球房拿到预收款,同时靠充值赠送锁定回头客。

这三块业务相互交叉,手工管理最难的地方在于:

  • 开台靠手写时间,结账时到底打了多久全凭记忆,客人说“我才打了一个小时”,服务员说“你打了两个半小时”,必然扯皮。
  • 会员余额变动没有流水,客人充值、消费、退款全靠收银员口述,账目说不清。
  • 吧台商品库存不盘点,货丢了都不知道,月底对账一脸懵。
  • 老板想统计“今天到底赚了多少”“周末哪几张桌利用率高”,翻本子翻到崩溃。

所以这套系统的核心目标就是:把开台、计时、计费、结算、商品、会员、报表全部串成一条完整链路,每笔钱都有据可查。

1.2 功能模块怎么划分

按照上面的业务拆解,我把系统分成四个核心模块加一个基础模块:

模块核心功能解决什么问题
桌台管理桌型设置、状态流转(空闲/使用中/维护)、开台/换台/关台台费计时计费有据可依
会员管理开卡、充值、余额流水、等级折扣储值资金安全,账目透明
商品管理商品分类、库存、进销存流水吧台货物可盘点,毛利可统计
订单结算台费+商品费合并结算、折扣、实付一次结清不遗漏,避免跑单
报表统计日营收、时段营收、桌台利用率、会员消费排行老板决策有数据支撑

每个模块再往下拆就是接口清单。比如桌台模块至少要有:查询桌台列表、按状态筛选、开台、关台、换台、调整桌型价格。会员模块至少要有:开卡、充值、查询余额、修改资料、积分变动。这里不要一开始就想着把界面画得多花哨,先把接口和表设计想清楚,后面写代码就是体力活。

1.3 为什么选SpringBoot作为后端框架

做这个类型的业务系统,选型不需要追求新潮,稳和资料齐才是第一位的。SpringBoot能成为这类项目的主流选择,核心原因有三个:

第一,自动配置把过去Spring MVC项目里大量XML配置去掉了,一个带Web功能的SpringBoot工程,默认自带Tomcat和SpringMVC,新手也能很快跑起来。第二,生态成熟,MyBatis-Plus、Sa-Token、Knife4j这些常用组件对SpringBoot的支持都很好,几行配置就能接入。第三,社区资料极其丰富,遇到问题搜一下就是解决方案,做毕设和中小型商用项目都比较省心。

我见过不少同学在选型时纠结要不要用Spring Cloud或者Dubbo,说实话,台球厅这种单体业务,用Spring Cloud纯属给自己上难度。单机事务、单库数据,单体应用就是最优解。

2. SpringBoot项目搭建:版本选型与工程结构

2.1 版本别追新:SpringBoot 3.x 与 2.7.x 怎么选

“SpringBoot版本太高”这句话,真的不是段子。我见过太多人新建项目时直接选了最新版,然后发现网上搜到的教程全都对不上,最后卡在启动阶段。

现阶段SpringBoot有两个大版本在广泛使用:2.7.x和3.x。它们之间最大的差异是JDK基线。SpringBoot 3.x强制要求JDK 17以上,而且把javax.servlet这一整套命名空间迁移到了jakarta.servlet,这意味着很多老教程、老依赖、老代码在3.x下直接编译不过。你做这类型项目,如果选修课或者毕设答辩环境是老JDK,或者你复用别人的教学视频代码,3.x会带来一堆不必要的兼容问题。

我推荐的组合是:JDK 1.8 + SpringBoot 2.7.18 + MyBatis-Plus 3.5.x + MySQL 8.0。

对比项SpringBoot 2.7.xSpringBoot 3.x
JDK要求8/11/17均可强制17+
包命名空间javax.*jakarta.*
社区教程数量极多相对少
兼容老项目好差
长期维护2.7.18为最后版本,仍可用持续更新

2.7.x是SpringBoot 2.x的最后一个版本线,包含了过去几年的所有修复和优化,对这套系统来说功能完全够用,而且大部分MyBatis-Plus教程都基于这个版本写的,照着做不容易踩坑。

2.2 项目结构与依赖清单

工程结构要遵循“按包分包”的惯例,别把Controller、Service、Mapper全塞在一个类里。我常用的分层是这样的:

src/main/java/com/example/billiards/ ├── config/ # 配置类:跨域、拦截器、MyBatis-Plus分页插件 ├── controller/ # 接口层:只做参数接收和结果返回 ├── service/ # 业务层:核心逻辑都在这里 │ └── impl/ ├── mapper/ # 数据访问层:与数据库打交道 ├── entity/ # 数据库实体类 ├── dto/ # 请求参数对象 ├── vo/ # 返回视图对象 ├── common/ # 统一返回体、常量、公共工具类 └── exception/ # 自定义异常与全局异常处理

这样的分层有个好处:Controller绝对不会写业务逻辑,Service也不会直接拼SQL,各层职责清晰。答辩时老师问“为什么这么分层”,你可以回答:接口层只做协议转换,业务层做事务和规则处理,数据访问层专注SQL,这样任何一层改动都不影响其他层。

pom.xml里核心依赖有这么几个:spring-boot-starter-web提供Web能力,spring-boot-starter-validation提供参数校验,mybatis-plus-boot-starter操作数据库,mysql-connector-java负责驱动,lombok减少样板代码,knife4j生成接口文档。用Maven创建工程之后,注意配置阿里云镜像仓库,不然下载依赖会慢到你怀疑人生。

2.3 统一返回、全局异常和参数校验

很多新手项目的问题是:每个接口返回的数据结构都不一样,有的返回Map,有的返回JSONObject,有的直接返回实体。前端拿数据得猜字段,联调时必须靠文档。这种项目做完能跑,但维护起来很痛苦。

我写这类系统时,第一步就是定义统一返回体:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

有了这个统一返回体,所有Controller的返回值都变成Result ,前端只需要处理三个字段,不管是列表、分页、详情还是操作结果,全部一个格式。

全局异常处理也必须在项目初期就配上,不然数据库报错、参数校验失败时,SpringBoot默认返回的异常页面极其难懂,前端根本没法处理。用@RestControllerAdvice配合@ExceptionHandler,可以把业务异常、参数校验异常、兜底Exception分别拦截,统一转换成Result格式返回。这套东西一次配好,后面写接口能省掉大量异常判断代码。

3. 核心功能怎么落地:计费算法、库存和结算

3.1 数据库表怎么设计:八张表搞定核心业务

表设计是这类业务系统的灵魂。表建错了,后面接口写得再漂亮也白搭。我按照业务链路设计了八张核心表:

  • 桌台表(t_billiard_table):桌号、桌型、基础时价、高峰时价、状态、所在区域。
  • 桌型表(t_table_type):不同类型桌子的名称和默认价格,便于统一调价。
  • 会员表(t_member):姓名、手机号、余额、积分、等级、开卡时间。
  • 余额流水表(t_member_balance_log):每一次充值、消费、退款都记一笔。
  • 商品表(t_goods):名称、售价、库存、单位、是否上架。
  • 订单表(t_order):桌台、会员、开台时间、关台时间、台费、商品费、实付金额、状态。
  • 订单明细表(t_order_item):一条订单下的多个费用项,区分台费还是商品。
  • 结算表(t_settle_record):记录每次结算的支付方式、优惠金额、操作员。

注意订单和订单明细为什么要分成两张表。因为一笔订单可能同时包含台费、两瓶饮料、一包薯片,如果都塞进订单表,字段会非常冗余,想做统计也麻烦。拆开后,订单表管主账,明细表管每一笔流水,后续做报表只需要对明细做聚合。

建表时还有两个通用约定:主键一律用BIGINT自增,金额字段一律用DECIMAL(10,2),时间字段用DATETIME。千万别用FLOAT或DOUBLE存金额,浮点误差在结算时一定会暴雷。

3.2 桌台计时计费:动态费率与最低消费的一次说清

桌台计费是台球厅管理系统里最容易被写错的地方。先说规则,再写代码。

实际业务里,不同桌型价格不同,不同时段价格也不同。我按“普通时段+高峰时段”的方式设计费率:比如普通台普通时段28元/小时,高峰时段38元/小时;VIP包间普通时段68元/小时,高峰时段88元/小时,低消180元。关台结账时,系统要根据开台时间和当前时间,计算出跨越的每个时段分别占了多少小时,再累加费用。

public BigDecimal calcTableFee(LocalDateTime openTime, LocalDateTime closeTime, BigDecimal normalPrice, BigDecimal peakPrice) { if (closeTime.isBefore(openTime)) { throw new BizException("关台时间不能早于开台时间"); } BigDecimal total = BigDecimal.ZERO; // 按小时计费,不足1小时按1小时;先定位到开台所在整点 LocalDateTime cursor = openTime; while (cursor.isBefore(closeTime)) { LocalDateTime nextHour = cursor.plusHours(1); if (nextHour.isAfter(closeTime)) { nextHour = closeTime; } int hour = cursor.getHour(); boolean peak = hour >= 19 || hour < 1; // 晚上19点到次日1点算高峰 total = total.add(peak ? peakPrice : normalPrice); cursor = nextHour; } return total; }

这段代码有几个细节值得说明。第一,所有金额都用BigDecimal相加,不能用double,否则会有0.1+0.2不等于0.3的问题。第二,用LocalDateTime遍历小时,而不是简单地算时长再乘单价,因为跨时段场景下必须逐小时判断价格。第三,不足一小时按一小时算,这是球房最常见的计费规则,如果改成按分钟计费,只需要把步长从一小时改成30分钟或1分钟。

最低消费的处理很简单:如果按小时计算出来的台费小于最低消费,结算时直接取最低消费金额。但要注意,最低消费是台费和商品费合计还是单台费,不同球房定义不同,这个规则一定要做成可配置项,别写死在代码里。

3.3 商品扣库存与订单结算:事务和乐观锁是底线

台球厅卖商品和普通电商不一样,它是先消费、后结算。客人打球过程中从吧台拿饮料,服务员先在系统里记到当前桌台订单下,关台时一起结账。这样就必须保证一个原子性:记录商品时扣减库存,如果后面客人不要了,或者结算取消了,库存要能加回来。

扣库存最怕的是超卖。两台收银机同时卖同一箱饮料,如果代码只做“查询库存 → 判断是否大于0 → 扣减”,就会在并发情况下把库存扣成负数。解决办法是用乐观锁,在商品表上加一个version字段,更新时带上版本条件:

@Update("UPDATE t_goods SET stock = stock - #{count}, version = version + 1 " + "WHERE id = #{goodsId} AND stock >= #{count}") int deductStock(@Param("goodsId") Long goodsId, @Param("count") Integer count);

这个SQL本身就包含了“库存必须够扣”的条件,数据库行锁保证同一时间只有一个事务能扣成功,返回值是0就说明库存不足或者并发冲突,业务层据此提示“库存不足”。这套方案比先查询再更新少了两次数据库交互,而且天然防超卖,适合台球厅这种小并发场景。

订单结算的方法上要加@Transactional(rollbackFor = Exception.class),让它成为完整事务。关台结算时,一次要做的事包括:计算台费、追加商品费、计算折扣、更新订单状态、扣除会员余额、写余额流水、更新桌台状态为空闲。只要其中一个环节失败,整个事务回滚,订单和库存不会出现一半成功一半失败的情况。

3.4 会员余额流水:账要对得上

会员模块最容易让新手翻车的就是余额更新。很多人的第一版代码长这样:查询会员余额,算出新余额,执行UPDATE。这在并发充值、并发消费时会出现余额覆盖,也就是经典的丢失更新问题。

我建议把余额变更做成流水表驱动的模式。每次充值、消费、退款,先插入一条余额流水,记录变更前余额、变更金额、变更后余额,然后更新会员主表的余额。这里有个细节:余额流水表的“变更后余额”要靠数据库计算,人工在代码里算容易出错。

更稳妥的做法是在更新会员余额时使用原子更新,然后回查最新余额写入流水:

// 扣减余额:先执行原子更新 int rows = memberMapper.deductBalance(memberId, amount); if (rows == 0) { throw new BizException("余额不足"); } // 回查最新余额 Member member = memberMapper.selectById(memberId); // 写入流水 balanceLogMapper.insert(new BalanceLog(memberId, -amount, member.getBalance(), "消费"));

原子更新SQL类似“UPDATE t_member SET balance = balance - #{amount} WHERE id = #{memberId} AND balance >= #{amount}”,更新行数为0就代表余额不足。这样并发扣款时数据库把好了最后一道关,流水记录的余额也是从库里回查出来的真实值,账永远和主表一致。

3.5 日结报表:直接在SQL里聚合,别在代码里循环

报表是老板看得见的价值,也是答辩演示时的亮点。日结报表里最核心的数据就是当天营收,包括台费总收入、商品总收入、会员充值总额、新增会员数、开台次数。

这些数据如果靠Java代码一个个查询再手动相加,性能差且容易漏。直接在SQL里聚合才是正确姿势:

SELECT COUNT(DISTINCT o.id) AS order_count, SUM(o.table_amount) AS table_amount_sum, SUM(o.goods_amount) AS goods_amount_sum, SUM(o.real_amount) AS real_amount_sum FROM t_order o WHERE o.settle_time >= #{startTime} AND o.settle_time < #{endTime}

这里有个容易出错的地方:统计条件一定要用“大于等于开始时间且小于结束时间”,不要写成“between开始时间和结束时间”,否则第二天的零点零分整的数据会被前一天重复统计。我在这上面吃过亏,报表数字对不上,排查了一下午才发现是边界条件写错了。

4. 数据访问层与并发安全:让数据操作稳一点

4.1 ORM怎么选:MyBatis-Plus还是Spring Data JPA

“SpringBoot数据访问”是高频搜索话题,说明很多人都卡在这一层。台球厅管理系统的数据访问层有两种主流方案:Spring Data JPA和MyBatis-Plus。

如果你之前学过Spring Data JPA,用JPA也可以,它胜在完全面向对象,大部分单表操作不需要写SQL。但JPA有个坑:复杂SQL、多表联查、动态条件时,要么写JPQL,要么用Specification,学习曲线比较陡。对一个中小型管理系统来说,两张表的联查、报表聚合查询、动态筛选都很常见,JPA用起来反而没那么顺手。

我更推荐MyBatis-Plus。它保留了MyBatis写SQL的能力,同时内置了单表CRUD,简单操作连SQL都不用写:

// 查询所有空闲桌台 LambdaQueryWrapper<BilliardTable> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(BilliardTable::getStatus, "FREE"); List<BilliardTable> tables = billiardTableMapper.selectList(wrapper);

复杂报表SQL就自己写@Select注解或XML,两种方式自由组合。用MyBatis-Plus把这套系统做完,你还能顺带掌握企业里最主流的Java数据访问方式,答辩和面试都有话题聊。

4.2 分页、主键、驼峰映射:MyBatis-Plus配置细节

MyBatis-Plus接入之后第一件事是配置分页插件。不配置的话,selectPage方法看着能用,实际分页不生效,它是全表查询后在内存里切的页码,数据一多就慢。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

实体类上的主键策略建议用ASSIGN_ID,也就是雪花算法生成主键。不要用数据库自增AUTO_INCREMENT,因为后面做数据迁移、分表或者跨库合并时,雪花ID比自增ID灵活得多。

另一个容易被忽略的约定是驼峰映射。数据库字段一般是下划线,比如table_type_id,Java实体类是tableTypeId。MyBatis-Plus默认开启驼峰映射,这两个就能自动对应上。但你要是自己写XML的resultMap,就必须手动映射所有字段,少写一个字段就查不出值,而且不报错。

4.3 并发场景:开台冲突、库存超卖、重复结算

小体量系统也有并发问题,台球厅前台两三个人同时操作,就可能出现下面三种情况。

开台冲突:两个服务员同时给同一张桌开台。如果代码顺序是“先查状态为空闲→再UPDATE成使用中”,两个请求都能查到空闲,重复开台就发生了。解决办法是把查询和更新合并成一条原子SQL:

int rows = billiardTableMapper.updateStatusByIdAndOldStatus(tableId, "BUSY", "FREE"); if (rows == 0) { throw new BizException("该桌台已被占用,请重新选台"); }

这条SQL执行时数据库会锁住这一行,第二个请求要么等,要么更新0行直接失败。库存超卖的处理方式前面已经说了,用乐观锁。重复结算的防护则要依赖幂等设计:关台结算接口如果被前端误触两次,可能就会给会员扣两次钱。最稳妥的防重方式是在订单表上增加一个结算状态字段,结算时用“UPDATE t_order SET settle_status = 1 WHERE id = ? AND settle_status = 0”做状态流转,更新行数为0说明这张订单已经结算过了,直接提示不能重复操作。

5. 打包部署与上线:从本地跑通到云端

5.1 mvn package打包与内置Tomcat

项目开发完成后,SpringBoot默认是内置Tomcat的,这意味着你不需要在服务器上单独装Tomcat,打包就是一条命令:

mvn clean package -Dmaven.test.skip=true

执行完之后,target目录下会生成一个jar包。这个jar包可以直接丢到服务器上,用java -jar运行。SpringBoot的内置Tomcat会自动读取配置里的端口号,启动后就是完整的服务。这里有一个细节:部署前要在application.yml里把数据库连接改到生产库,同时确认端口开放和服务器防火墙规则。

打包时别嫌麻烦跳过测试不看,至少要保证maven能编译通过。我见过太多本地IDE里跑得好好的项目,一打包就报错,原因就是IDE自动编译的class和maven编译路径不完全一致。养成用命令行mvn package的习惯,能提前暴露不少问题。

5.2 一定用内置Tomcat吗?外置部署怎么做

有同学问“SpringBoot可以不内置Tomcat吗”,答案是完全可以。如果你公司要求统一用外置Tomcat管理部署目录,或者你需要在一个Tomcat下多应用共存,SpringBoot天生支持打war包。

操作分三步:首先,pom.xml里把打包方式从jar改成war;其次,在启动类上继承SpringBootServletInitializer并重写configure方法;最后,把内置Tomcat依赖改成provided,让打出来的war包不包含Tomcat。部署时把war包丢到Tomcat的webapps目录下,启动Tomcat即可。

@SpringBootApplication public class BilliardsApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(BilliardsApplication.class); } }

不过我的实际观点是:对中小型系统,内置Tomcat的jar方式简单得多。外置Tomcat的收益是运维统一,代价是你必须自己管理Tomcat版本、JVM参数、部署目录权限。能少一层依赖,就少一个故障点。

5.3 宝塔面板Docker部署实操

服务器上如果装了宝塔面板,部署SpringBoot项目最省心的方式是用Docker。宝塔自带的Docker管理器能很方便地完成镜像构建和容器管理。

先写一个Dockerfile:

FROM openjdk:8-jdk-alpine WORKDIR /app COPY target/billiards.jar /app/billiards.jar ENV TZ=Asia/Shanghai EXPOSE 8080 ENTRYPOINT ["java", "-jar", "-Xms256m", "-Xmx512m", "/app/billiards.jar"]

再写一份docker-compose.yml,把SpringBoot服务和MySQL一起编排起来:

version: '3' services: mysql: image: mysql:8.0 container_name: billiards-mysql environment: - MYSQL_ROOT_PASSWORD=your_password - MYSQL_DATABASE=billiards ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql app: build: . container_name: billiards-app ports: - "8080:8080" depends_on: - mysql

Docker部署最关键的点是时区。容器默认时区是UTC,如果你不设置TZ=Asia/Shanghai,开台时间和关台时间会差8小时,计费时长直接错乱。我第一次部署上线时忘了配时区,客人打了一小时,系统算了9小时的账,差点出事。这个坑请务必记住。

5.4 上线第一天要盯的几个数据指标

系统上线不是“能跑”就结束,第一天要多看几个数据点。第一个是订单实付金额与会员余额流水的总和是否对得上,这决定了账务模块有没有BUG。第二个是桌台状态是否会出现“占桌却不结账”的情况,如果发生,通常是异常关台没有正确处理。第三个是接口响应时间,尤其是桌台列表和报表接口,如果不做索引,数据量上来后会很卡。

我给这套系统所有查询频繁的字段都加了索引,包括订单表的table_id、member_id、settle_time,会员表的phone,桌台表的状态。最开始的版本没加索引,两千条订单就把报表接口拖到三秒,加上联合索引后降到几十毫秒。数据库索引这事,看起来不起眼,实际上对系统体验影响极大。

6. 高频报错与避坑速查

6.1 开发期五个常见问题

我把自己开发过程中以及帮朋友排查时遇到的高频问题整理成了表格,照着检查能省很多时间。

现象常见原因解决办法
项目启动失败,提示端口被占用上一次运行的进程没杀掉换端口,或者netstat -ano找到进程杀掉
LocalDateTime返回给前端变成时间戳或数组没配置JSON序列化格式在字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
数据库中文乱码连接串没指定字符集JDBC URL加characterEncoding=utf8
接口报404,但Controller明明写了没加@RestController或@RequestMapping检查类级Mapping和包扫描范围
依赖下载慢或失败Maven用的默认中央仓库settings.xml配置阿里云镜像

这里特别说一下LocalDateTime序列化。SpringBoot默认用Jackson序列化时间,不配置的话返回给前端是一串数组,比如“2025,1,1,12,30,0”,前端根本没法直接用。最省事的方式是在全局配置文件里加一个Jackson的date-format配置,或者在实体时间字段上加@JsonFormat注解。我习惯在实体类字段上直接加注解,明确、直观,不容易被全局配置影响。

6.2 上线后容易忽略的三个业务坑

有些坑本地测试测不出来,一到真实运营场景就暴露。

第一个是计费时段跨天问题。我设计的计费规则里,高峰时段是19点到次日凌晨1点。如果客人20点开台,凌晨2点关台,计费循环遍历到第二天凌晨1点整点时,hour=1,此时高峰期判断逻辑是hour >= 19 || hour < 1,凌晨1点整时不满足高峰条件,会被算成普通时段。这个边界值调了很久才发现。建议所有时段规则都用“左闭右开”的区间设计,比如高峰时段定义为[19:00, 01:00),处理跨天时把每个时段拆成两个区间。

第二个是关台时间与结算时间不一致。系统里开台时间用数据库的服务器时间,如果多台服务器或容器时间不同步,就会出现计时偏差。解决方法是统一使用数据库时间now(),或者所有实例都强制同步NTP时间,并在代码里统一用系统提供的时钟服务。

第三个是商品退货库存回补。客人结账后说饮料不要了要退,如果你只做了退款进入余额,没有把库存加回来,月底盘点一定对不上。设计退款功能时就要把库存回补和余额退回放在同一个事务里,另一个容易漏的地方是优惠券或折扣金额的会计处理,这部分虽然复杂,但这套系统里简单做掉就可。

结束时再聊几句个人感受

做这类业务管理系统的第一课,往往不是框架语法,而是先把账算明白。这套SpringBoot台球厅管理系统写下来,我真正花时间的地方全在计费规则、余额流水和报表对账上,代码反而是规则确定之后顺手的翻译工作。如果你照着做一套,我建议先把你所在球房的计费规则、会员折扣、商品盘点周期一条条列在纸上,再动数据库表设计。表设计对了,后面写接口基本就是体力活。后面的扩展方向也很明确:小程序扫码开台、屏幕实时显示用时、会员生日自动发券、大屏经营看板,这些都是在现有订单和账目数据上长出来的功能。前提还是那条:把当前这套系统的账算清楚。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 17:43:20

多模型部署实战:从显存计算到路由调度与稳定性优化

1. 为什么一个团队会同时部署多个大模型&#xff1a;不是铺张&#xff0c;是场景逼的我接手某个模型平台那会儿&#xff0c;线上跑了三个大模型。当时不止一个同学来问&#xff1a;"就一个客服系统&#xff0c;为什么非要部署这么多大模型&#xff1f;"这个问题问得挺…

作者头像 李华
网站建设 2026/10/20 11:52:45

Hadoop MapReduce气象数据分析:从数据预处理到作业调优实战

简介&#xff1a;一套完整覆盖Hadoop气象数据处理全流程的工程代码&#xff0c;面向大数据课程设计、毕业设计以及需要快速上手MapReduce开发的初学者。项目基于HDFS分布式文件系统读取原始气象数据&#xff0c;通过MapReduce完成数据清洗、气温统计、极值分析等计算任务&#…

作者头像 李华
网站建设 2026/10/15 2:31:09

HTTP报错排查实战:连接管理、状态码、HTTPS与认证全解析

我最近被一批HTTP报错轮流轰炸&#xff0c;先是Docker拉镜像时冒出一句error response from daemon: get "https://registry-1.docker.io/v2/": net/http还没缓过来&#xff0c;又看到Steam商店返回Access Denied、Kali源更新提示“暂时不能解析”、ROS的apt源报“没…

作者头像 李华
网站建设 2026/10/15 14:01:04

OpenClaw高阶玩法:从工具到自动化收入流水线

1. 从“会用”到“玩透”&#xff1a;OpenClaw高阶玩法的分水岭在哪里大多数人第一次接触 OpenClaw&#xff0c;基本都停留在“对话、写文案、查资料”这个层面。这没什么问题&#xff0c;但如果你的目标是从效率工具进阶到自动化收入&#xff0c;那就得换一套思路。我见过太多…

作者头像 李华
网站建设 2026/10/19 23:09:47

Shell函数从入门到实战:以nginx管理为例构建高效函数库

说句实在话&#xff0c;Shell学到函数这一章&#xff0c;很多人会觉得"不就是把一段命令包起来嘛&#xff0c;至于专门写一篇吗"。但如果你写过超过200行的部署脚本、维护过几十个站点的nginx配置、或者被半夜三点的告警电话叫起来去重启服务&#xff0c;就会明白&am…

作者头像 李华