基于Java+SSM+Flask的旅社客房收费管理系统详解

发布时间:2026/10/10 21:29:23
基于Java+SSM+Flask的旅社客房收费管理系统详解
市面上这类系统的叫法五花八门旅社客房收费管理系统、旅店管理系统、客栈管理软件、宾馆客房收费系统、酒店房间收费解决方案说到底都是同一件事把房态和账目管明白。最近我完整带了一套基于JavaSSMFlask的旅社客房收费管理系统从数据库设计、双端代码落地到部署演示全都过了一遍。今天这篇就把整个项目的选型逻辑、表结构设计、核心结算逻辑和调试交付过程中踩过的坑梳理出来给正在做同类收费管理系统、或者打算用Java业务后台Python数据服务这种混合架构的朋友做个参考。这套系统的核心功能并不复杂房间类型与房价维护、顾客登记、入住、退房结算、押金管理、按时计费/按天计费/长包房三种模式的费用自动计算、换房、日结报表、操作日志和经营统计驾驶舱。适合看这篇的人也很明确有JavaWeb基础、想看看SSM怎么把真实业务落地的人或者是正在纠结Java和Flask两个后端怎么配合的人。如果你只是想要一个能直接改改用的旅店管理框架里面的表结构和结算算法也可以直接抄作业。1. 需求梳理先把收一天钱这件事拆成流程1.1 前台一天都在干什么旅社前台的工作一天看下来非常固定客人到店先看房态有房才谈价格选中房间后登记身份信息、收押金、生成一张入住单住的过程中可能出现续住、换房、加钟退房时核算费用、多退少补、房态变成脏房下班前做日结把一天的所有账单汇总封存。任何一个收费管理系统本质都是在给这条链路上的每一环做状态记录金额流转。我见过不少项目把界面做得很花结果入住单和账单之间没有关联退房时根本算不清账。所以在动手写代码之前先把业务状态流转想清楚比急着写接口重要得多。你不必一开始就引入什么领域驱动设计但对旅社系统来说状态的穷举必须提前想到位哪些状态并存是允许的哪些状态迁移是禁止的。1.2 计费规则收费系统真正的核心收费系统最怕计费规则写死在代码里。同一个旅社可能同时存在三种计费模式全天房按晚计费中午12点后退房加收半天或一天钟点房包4小时或6小时超时按小时加收长包房按月或按周结算押金和折扣单独约定。我的建议是建一张计费规则表把计费模式、免费时长、超时单价、是否封顶这些参数都放进表里而不是在Java代码里硬编码。比如定义price_mode1表示日租、2表示钟点、3表示长包。退房结算时Service层根据price_mode走不同的计费分支但分支内部只读参数不写死具体数值。这样以后改价格、调整规则只需要改数据库不用重新编译发布。1.3 状态流转里最容易埋雷的地方房间和订单的状态不能散落着记。房间状态建议只设四个空闲、在住、脏房、维修订单状态建议设五个待入住、在住、已退房、已换房、已取消。这里有一个非常经典的坑换房不是把订单里的房间号改一下这么简单而是旧订单关闭、新订单生成两个动作并且这两个动作必须同时成功或同时失败。否则就会出现同一个客人在两间房同时在住、或者旧房间一直显示被占用的错乱。后面我专门用一章讲一次因为换房逻辑导致的计费故障排查过程问题就出在这一环。2. 架构选型为什么SSM要拉着Flask一起干活2.1 两套技术栈的分工逻辑很多人一看到JavaSSMFlask这个组合就开始质疑一个系统为什么要用两个后端框架是不是炫技答案还真不是。SSM负责的是核心业务事务入住、退房、账务、权限这些操作对事务一致性要求极高Spring管理事务和MyBatis的动态SQL在这里非常成熟Flask这层做的是统计报表和数据驾驶舱。我实际用Flask写统计接口把今日营业额、入住率、房型销售排行、近七天流水趋势一次查出来返回JSON给前端图表渲染比在Java里写一堆复杂的统计SQL再手动封装VO要顺手得多。而且Python生态里做数据聚合、Excel导出非常快。这种Java业务后台Python数据服务的划分不是拍脑袋决定的而是很多中小型项目和课程设计里验证过的组合。两者不需要互相调用数据连接靠的是同一个MySQL实例边界非常清晰SSM职责下单、支付结算、房态变更、登录鉴权、操作日志Flask职责统计报表、数据驾驶舱、Excel导出、定时生成昨日经营日报两边不直接互相调用共享同一个数据库。2.2 共享同一个数据库要注意的事SSM用MyBatis连MySQLFlask用SQLAlchemy连同一个实例看起来简单实际有几个细节必须在前期定好核心业务表的读写权限归SSM侧Flask侧只读。如果Flask确实要写数据比如记录导出行为单独建一张导出日志表不要去动业务表表名和字段名在两边统一命名避免SQLAlchemy自动映射和MyBatis XML映射对不上MySQL连接串统一用utf8mb4字符集否则身份证号、中文姓名、备注信息很容易乱码。2.3 部署形态到底长什么样我实际部署用的是三个进程Tomcat跑SSM工程监听8080提供业务接口Gunicorn跑Flask服务监听5000提供报表接口Nginx统一监听80把不同前缀转发到不同后端。如果是本地演示环境不装Nginx也行直接在Java前端页面里用Axios请求Flask接口但需要在Flask侧配置跨域。很多人在这一步翻车浏览器控制台报CORS错误然后怀疑自己代码写错了。其实根本不是代码问题是8080和5000两个端口属于不同源加一行配置就解决。3. 数据库设计几个必须提前定死的细节3.1 核心表怎么拆表不要拆得太碎也不要全塞一张。我这套系统最终稳定的表结构是七张核心表。表名作用关键字段room房间表id、room_no、floor、room_type_id、statusroom_type房型表id、type_name、base_price、day_price、hour_price、depositcustomer顾客/会员表id、name、id_card、phone、member_levelcheckin入住单表id、checkin_no、room_no、price_mode、price、begin_time、expect_end_time、actual_end_time、deposit、statusbill账单流水表id、bill_no、checkin_id、amount、pay_type、pay_time、statussys_user系统用户表id、username、password、roleoperation_log操作日志表id、user_id、action、detail、create_time到这个粒度就够了再往下拆就是过度设计。很多同学喜欢把顾客地址、爱好、备注全塞进去对收费系统来说这些字段并不会参与金额计算反而增加了表单复杂度。3.2 金额和时间两个被低估的大坑金额一律用DECIMAL(10,2)Java侧用BigDecimalfloat和double绝对不能碰。我见过有人用double算房费238.85元的房间打个八折结果算出来191.08000000000004这种精度损失在对账时就是大麻烦。时间上JDK8之后一律用LocalDateTime而不是java.util.Date数据库字段用DATETIME连接MySQL时注意驱动版本和serverTimezone参数少配一个可能启动直接报错。3.3 房间快照为什么必须冗余房价会调整顾客信息也可能变但历史账单永远不变。checkin表里必须冗余room_no、room_type_name、price这些当时的快照字段退房结算时读的是checkin表里的快照价而不是room_type表里的当前价。别小看这个设计运营调价是常态一旦结算用错价格营业额对账就永远对不平而且这种问题往往在月底才发现。3.4 空房判断与并发防超卖查找空房的SQL一般用NOT EXISTS判断当前房间不在在住订单中。但这里有个并发问题两个前台同时给同一间房办理入住双方同时查房态都是空然后同时插入入住单房间就被卖了两次。解决方案不复杂在插入入住单之前对room表记录加行锁用SELECT ... FOR UPDATE或者用状态做乐观锁更新UPDATE room SET status1 WHERE id? AND status0影响行数为0就说明房间已经被占。4. SSM侧的业务落地事务和动态SQL才是重头4.1 退房结算为什么必须加事务退房结算看起来就是算钱、收钱、把房态改掉实际上涉及至少六步读取入住单、计算费用、插入账单流水、更新订单状态、更新房间状态、更新会员积分。任何一步失败钱和房间状态就会不一致所以Service方法必须加Transactional并且在事务内先对入住单做行锁防止同一个订单被两个窗口重复退。Transactional public void checkout(Integer checkinId, Integer operatorId) { // 先锁入住单防止同一订单被并发退房 CheckIn checkIn checkInMapper.selectByIdForUpdate(checkinId); if (checkIn null || !1.equals(checkIn.getStatus())) { throw new BusinessException(入住单不存在或当前状态不可退房); } // 根据计费模式计算费用 BigDecimal total chargeService.calculate(checkIn); // 生成账单 Bill bill new Bill(); bill.setCheckinId(checkinId); bill.setBillNo(B System.currentTimeMillis()); bill.setAmount(total); bill.setPayType(CASH); billMapper.insert(bill); // 更新订单状态和实际退房时间 checkIn.setStatus(2); checkIn.setActualEndTime(LocalDateTime.now()); checkInMapper.updateById(checkIn); // 房间置为脏房 Room room roomMapper.selectById(checkIn.getRoomId()); room.setStatus(2); roomMapper.updateById(room); }4.2 MyBatis动态SQL做房态多条件搜索房态查询是这个系统使用频率最高的功能。前台输入的条件不固定可能按楼层、按房型、按价格区间、按入住日期查甚至不输条件直接看全部。这种场景用MyBatis动态SQL非常合适。select idsearchRooms resultTypeRoomVO SELECT r.*, rt.type_name FROM room r LEFT JOIN room_type rt ON r.room_type_id rt.id WHERE 11 if testroomTypeId ! null AND r.room_type_id #{roomTypeId} /if if testfloor ! null AND r.floor #{floor} /if if testmaxPrice ! null AND rt.base_price lt; #{maxPrice} /if AND NOT EXISTS ( SELECT 1 FROM checkin c WHERE c.room_id r.id AND c.status 1 ) /select注意XML里的小于号必须用转义这个问题经常让人莫名其妙报错。动态SQL的写法核心就是if标签动态拼接条件但永远在WHERE后面加11或者用where标签避免条件为空时SQL语法错误。4.3 登录鉴权与操作日志别省收费系统里钱是核心但操作日志同样不能省。前台人员万一操作失误或者换班交接说不清楚操志就是还原现场的唯一依据。登录用SpringMVC拦截器动态放行登录接口拦截其它业务路径。每次关键写操作通过自定义注解AOP切入把操作人、动作、参数、时间记入operation_log表。很多课程设计级别的项目会把操作日志当成可有可无的功能我强烈建议保留。它在调试阶段特别有用尤其当多个前台同时操作时日志能告诉你数据是哪一秒、被谁改掉的。5. Flask侧做了个经营数据驾驶舱5.1 为什么报表接口用Flask写更快旅社管理者最终关心的是几个数字今天做了多少钱、开了多少间房、哪类房型卖得最好、这个月的入住率是多少。这些统计SQL写起来并不难但数据聚合以后要转成JSON给前端图表Java那边要定义VO、写Mapper、写Service步骤太多。用Flask写就很快一个蓝图、一个原生SQL、一个jsonify接口就出来了。这种设计上的取舍很关键。把统计类接口从SSM工程里拆出去不会影响核心业务反而让核心工程更干净。5.2 一个典型的统计接口长什么样我自己在Flask里用Blueprint加SQLAlchemy的text()执行原生聚合SQL返回JSON给前端ECharts使用。from flask import Blueprint, jsonify from sqlalchemy import text from extensions import db report_bp Blueprint(report, __name__) report_bp.route(/report/today) def today_report(): sql text( SELECT COUNT(*) AS orders, IFNULL(SUM(amount), 0) AS revenue FROM bill WHERE DATE(pay_time) CURDATE() ) row db.session.execute(sql).fetchone() return jsonify({orders: row.orders, revenue: float(row.revenue)})前端页面用Axios请求这个接口拿到数据后填进ECharts的折线图和饼图就行。整条链路非常短改起来也快。注意Flask连接同一个MySQL时连接池别开太大小型项目5到10个连接足够否则数据库连接数容易被占满。5.3 跨域问题怎么一次解决如果Flask跑在5000端口SSM工程的前端页面在8080端口浏览器里直接跨端口请求就会触发CORS。最简单的解法是用Flask-CORS扩展把报表蓝图所在的Flask应用统一允许跨域。from flask_cors import CORS CORS(app, resources{r/report/*: {origins: *}})如果部署时上了Nginx统一入口跨域问题可以绕开因为浏览器看到的是同一个域名和端口。6. 一次换房引发的重复计费完整排查链路6.1 首先定位现象系统上线后财务对账时发现一个客人的账单比预期多了20元。客人入住时选了A房标准价158元/天中途换到B房退房时账单总额算出来368元。前台说住了两天应该是316元多扣了52元。第一反应是看bill表里这个入住关联的账单记录。查询后发现同一个checkin_id下面出现了两笔账单一笔是换房前系统自动生成的一笔是退房时生成的。说明换房不是单纯改房间号旧订单没有被正确关闭。6.2 顺着数据反查代码继续查checkin表发现同一个customer_id下存在两条status1的在住记录。翻代码后问题清楚了换房Service里只执行了插入新入住单和更新新房间状态完全没管旧入住单。换房这个动作在事务边界上被拆成了两步第一步生成新单成功第二步关闭旧单漏掉于是旧房间一直显示在住旧单也会参与日结。SELECT id, room_no, status, begin_time, actual_end_time FROM checkin WHERE customer_id 10086 ORDER BY begin_time DESC;结果两条记录都是status1一条的room_no是A房一条是B房。这里就能确认是换房逻辑的问题而不是计费算法的问题。因为如果只是计费算法错误订单状态不会出现两条在住。6.3 修复与事后验证修复方式是把换房逻辑做成一个完整的事务旧单关闭、新单插入、两个房间的状态同时更新三步要么全成要么全败。Transactional public void changeRoom(Integer oldCheckinId, Integer newRoomId) { CheckIn old checkInMapper.selectByIdForUpdate(oldCheckinId); if (old null || !1.equals(old.getStatus())) { throw new BusinessException(当前入住单不可换房); } // 1. 关闭旧单 old.setStatus(3); old.setActualEndTime(LocalDateTime.now()); checkInMapper.updateById(old); // 2. 更新旧房间为空闲 Room oldRoom roomMapper.selectById(old.getRoomId()); oldRoom.setStatus(0); roomMapper.updateById(oldRoom); // 3. 创建新入住单 CheckIn fresh createNewCheckin(old, newRoomId); checkInMapper.insert(fresh); // 4. 新房间置为在住 Room newRoom roomMapper.selectById(newRoomId); newRoom.setStatus(1); roomMapper.updateById(newRoom); }修完以后重新测试换房流程旧单状态变为已换房新单正常在住退房时只生成一笔账单。这件事给我最大的提醒是换房这类复合操作一定要把状态流转画清楚再动手事务边界和状态流转是配套的。7. 交付物围读源码、LW、调试文档和讲解怎么用7.1 源码目录应该怎么组织标题里提到的源码LW调试文档讲解这类交付物最常见的打开方式是把工程目录按两条线组织SSM后端工程和Flask报表工程分开数据库脚本单独放一个文件夹。project/ ├── ssm-hotel/ # SSM主工程 ├── flask-report/ # Flask报表工程 ├── sql/ │ ├── init.sql # 建库建表脚本 │ └── demo_data.sql # 演示数据 ├── docs/ │ ├── 调试文档.md │ └── LW.md # 设计说明书/论文 └── README.md拿到源码之后不建议直接全量导入IDE先把demo_data.sql导进MySQL确认库能查出来数据再导入工程。如果一上来就改代码出了问题很难分清是环境问题还是代码问题。7.2 调试文档与启动顺序调试文档的价值在于把启动顺序写清楚。这套系统的启动顺序是固定的创建数据库并导入init.sql和demo_data.sql修改SSM工程的jdbc.properties改成自己的MySQL账号密码用Maven拉依赖配置Tomcat启动SSM工程Flask工程创建虚拟环境执行pip install -r requirements.txt启动Flask服务先验证业务页面的登录再验证报表接口本地演示时可以跳过Nginx直连两个端口但跨域配置必须提前测。常见的启动报错和处理方式我也整理成了一张表报错信息原因处理方式Access denied for user数据库账号或密码错误检查jdbc.properties和Flask配置Public Key Retrieval is not allowedJDBC连接参数缺项连接串加allowPublicKeyRetrievaltruePort 8080 already in useTomcat端口被占换端口或结束占用进程CORS error跨域未配置Flask侧启用flask-cors中文乱码字符集不统一连接串、页面编码统一为utf8mb47.3 讲解/演示的节奏怎么安排拿到一套系统去讲或者去答辩顺序很重要。我最推荐的演示节奏是登录系统看权限区分然后创建一个房间给房间设置价格创建顾客办入住故意等系统计时再退房展示账单明细最后打开报表页看统计数据。不要一上来就点各种菜单那样会让听众抓不住重点。讲解时要主动抛出几个设计亮点退房事务怎么保证一致性、房间快照为什么冗余、换房为什么是两个状态变更、Flask报表接口怎么和SSM共用数据库。这四个点覆盖了架构、数据库、并发、多语言协作是最容易讲出深度的部分。LW设计说明书/论文的写作和调试文档不同不需要大段贴代码重点是业务流程图、功能结构图、表结构设计和测试用例。尤其测试用例要写清楚输入、步骤、预期结果、实际结果这是很多人忽略但非常占篇幅的部分。我自己带项目的习惯是先让数据层稳定再动接口层先保证单笔结算对再做统计报表先让核心流程跑通再去美化界面。这套系统从开始设计到最终交付最大的价值不是某一段代码写得有多漂亮而是整个状态流转和金额核算的闭环是严丝合缝的。把事务边界画清楚、把快照字段留好、把两种技术栈的职责分明白同类旅店管理软件、客栈管理工具换个业务口就能平移到别的场景里去用。