基于SSM+Flask的游泳会员管理系统设计与实现
先说一个现象很多游泳馆、健身俱乐部到现在还在用Excel登记会员办卡靠手写、扣款靠嘴算、签到靠划本子。会员问一句“卡里还有多少钱、还剩几次”前台得翻半天记录才能答上来。这种管理方式在会员几十个人的时候还能凑合一旦超过两三百人问题就会集中爆发充值记录对不上、卡到期忘了提醒、课程预约全靠群里接龙。我做的这套基于JavaSSMFlask的游泳会员管理系统就是为了解决这类小而实际的场馆管理痛点。核心思路是SSM负责会员、卡片、充值、消费这些业务主干的CRUD和事务控制Flask承担数据统计和可视化接口的辅助服务两者通过HTTP接口协作互不干扰。文章适合正在做课程设计、毕业设计的同学也适合想用低成本方案搞定场馆信息化的经营者参考内容里涉及的表结构、接口设计、调试流程都可以直接照搬。1. 项目整体设计与技术选型思路1.1 为什么是SSMFlask的混合架构先说选型。这套系统的主体是Java生态里的经典组合Spring SpringMVC MyBatis也就是大家常说的SSM。在国内的课程设计、毕业设计和中小企业后台项目中SSM几乎是一套“标准答案”Spring管对象和事务SpringMVC管接口路由MyBatis管SQL映射。学习资料多、踩坑经验多、面试时也容易讲清楚。但一个实际的会员管理系统绝对不只是增删改查。游泳馆经营者特别关心的是这个月新增了多少会员、哪种卡卖得最好、每天哪个时段人最多、营业收入走势怎么样。这些数据分析功能用Java写也行但代码量和图表渲染成本都不低。于是我引入了Python的Flask框架专门做一个独立的统计分析服务。理由很直接Python做数据处理和可视化图表的生态太舒服了pyecharts生成图表只要几行代码Pandas处理统计数据也比手写Java循环高效太多。这样就形成了一个“双引擎”架构SSM负责业务主链路Flask负责数据分析链路。两者访问同一个MySQL数据库业务数据由SSM写入统计分析由Flask只读查询再通过JSON接口把结果交给前端图表展示。数据只有一份不存在同步问题技术上各用各的擅长领域这是我选择这套架构最核心的理由。提示如果你只是做课程设计这套混合架构反而是个加分项。答辩时老师问你“为什么用两个框架”你可以从“技术选型要匹配业务场景”这个角度回答比单纯背SSM概念要有说服力得多。1.2 系统模块划分与业务边界整个系统我按业务拆成了六个主要模块边界从一开始就划清楚了避免后续代码乱成一团模块名称技术归属核心职责会员档案管理SSM会员新增、资料修改、状态管理卡类型与办卡管理SSM计时卡/计次卡配置办卡、续卡、换卡充值消费管理SSM余额充值、消费扣款、流水查询课程与预约管理SSM课程发布、会员预约、签到核销数据统计分析Flask会员增长、营收汇总、卡片销售排行系统登录权限SSM管理员账号登录会话管理模块划分上有一条铁律业务数据只能由SSM写入Flask永远只读。为什么这么定因为Java侧的Spring事务管理很成熟MyBatis配合数据库行锁可以保证充值扣款这种关键操作不出错。而Flask这边如果也开放写入权限就会出现两个服务同时操作同一张表的情况一旦并发扣款或者重复办卡数据就乱了。职责分离之后Flask只管从数据库捞数据、做统计、出图表逻辑简单出错概率也低。1.3 数据流和接口调用关系数据流是这样一个走向管理员在浏览器登录SSM系统操作会员、卡片、充值等功能数据写进MySQLFlask服务独立运行定时或按请求去查询业务表计算出统计数据通过/api/stats/xxx这样的接口输出JSON前端页面调用SSM的接口做日常管理操作调用Flask的接口做图表渲染。这里要注意一个细节SSM和Flask不能同时占同一个端口。我实际开发时Tomcat跑在8080端口Flask跑在5001端口。前端页面访问两个不同端口必然触发跨域问题我在Flask这边统一用flask-cors解决跨域SSM那边的接口因为前端和它在同一个Tomcat下不需要额外处理。这个边界厘清之后前后端调试就顺畅了。2. 数据库设计与核心表结构2.1 会员主表与卡类型设计数据库设计是整个系统的地基这块没想清楚后面写代码会到处打补丁。我实际落地的核心表一共八张member会员表、card_type卡类型表、member_card会员持卡表、recharge_record充值记录表、consume_record消费记录表、course课程表、course_appointment预约表、check_in签到表外加一张sys_user管理员表。会员主表是最先设计的字段覆盖了场馆管理最关心的信息。我给出我实际用的表结构你可以直接拿去改CREATE TABLE member ( id int(11) NOT NULL AUTO_INCREMENT, member_no varchar(20) NOT NULL COMMENT 会员编号规则如 YYMMDD三位序号, name varchar(50) NOT NULL COMMENT 会员姓名, gender tinyint(4) DEFAULT 1 COMMENT 1男 2女, phone varchar(11) DEFAULT NULL, id_card varchar(18) DEFAULT NULL COMMENT 身份证号用于购买保险, height decimal(5,2) DEFAULT NULL COMMENT 身高cm部分学员基础信息, weight decimal(5,2) DEFAULT NULL COMMENT 体重kg, emergency_contact varchar(50) DEFAULT NULL COMMENT 紧急联系人, emergency_phone varchar(11) DEFAULT NULL, status tinyint(4) DEFAULT 1 COMMENT 1正常 0冻结, remark varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_member_no (member_no), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里我特意用member_no作为唯一业务编号而不是直接用自增id对外展示。原因很实际前台打印会员列表时一串“20250312001”这样的编号看起来专业而且Excel导入导出时不容易出错。自增id留给系统内部做外键关联用。卡类型和持卡表是分开设计的这样后续换卡、续卡时不用改动会员主表。卡类型表核心字段有card_name月卡、季卡、年卡、30次卡、50次卡等、card_kind计时还是计次、duration_days有效天数计次卡可空、total_times总次数计时卡可空、price和status。计次卡和计时卡是两种完全不同的计费模式必须用card_kind区分开业务逻辑里再分别处理。2.2 充值消费流水与余额一致性接下来是重头戏余额和流水。我的设计原则是余额字段可以存在但永远以流水表为准。什么意思就是member_card表里有一个balance字段方便前台快速展示但每一笔变动都必须写入recharge_record或consume_record。这两张流水表都包含会员卡id、变动金额、变动后的余额、操作管理员id、变动类型、变动时间、备注。举个例子会员充值500元前台页面看到的是余额增加数据库里实际发生了两件事向recharge_record插入一条记录同时更新member_card表的balance字段。这两个动作必须在一个数据库事务里完成要么都成功要么都失败。这正是选择SSM的价值所在Transactional注解一加Spring事务帮我把这个一致性兜住。消费扣款的SQL也要防止并发问题。同一张卡两个收银员同时刷卡扣款如果用update member_card set balance balance - 100 where id 1数据库的行锁会保证只有一个请求先执行成功另一个排队执行时余额已经不够了修改行数为0业务层就能感知到并报错。这里一定要用条件更新我下面会展示具体写法。2.3 课程、预约与签到建模游泳馆除了卖卡还有教练课业务。课程表course字段包含课程名称、教练姓名、上课日期、开始时间、结束时间、上课地点泳道/泳池区域、预约上限人数、已预约人数、课程状态。预约表course_appointment用来记录哪个会员预约了哪节课同时加一个status字段区分已预约、已签到、已取消。签到的实现逻辑是会员到场后前台在系统里搜到预约记录点击“签到”系统把预约状态改为已签到同时往check_in表写入一条签到记录记录签到时间和操作人。这里有一个实际业务细节要注意计时卡会员和计次卡会员的签到扣减逻辑完全不同。计时卡按有效期和剩余天数判断前端展示的是“卡内还剩多少天”计次卡每次签到要扣一次消费记录相当于把签到动作转化为一次计次扣减。如果不在数据库层面对两类卡做区分签到逻辑很容易写乱。3. 核心功能实现与关键代码拆解3.1 SSM三层架构的会员管理实现SSM的老规矩请求走Controller-Service-Mapper三层。以“新增会员”为例Controller层只负责接收参数和返回结果Service层写业务规则Mapper层跟数据库打交道。Controller大概是这样的写法Controller RequestMapping(/member) public class MemberController { Autowired private MemberService memberService; RequestMapping(value /add, method RequestMethod.POST) ResponseBody public Result add(RequestBody Member member, HttpSession session) { AdminUser admin (AdminUser) session.getAttribute(loginUser); return memberService.addMember(member, admin.getId()); } }Service层生成会员编号同时检查手机号是否重复。这块有一个新手很容易忽略的细节会员编号生成要处理并发。我用的方案是查询当天已有的最大编号加1后拼接日期前缀再放到事务里执行。考虑到单场馆单日办卡量不会太大这个方案足够稳定如果后续要做多场馆高并发可以把编号生成改成数据库序列或Redis自增。Mapper层用MyBatis的注解或XML都行。我个人习惯用XML因为复杂的动态SQL比如多条件组合查询会员列表在XML里维护比在注解里拼字符串清晰得多。一个典型的动态查询会用到where标签和if标签根据姓名、手机号、卡状态等条件动态拼SQL这样前台搜索框就能灵活组合条件。3.2 余额扣减与并发防护的实战写法消费扣款是关系到钱的功能必须严谨。我直接给出MyBatis XML里的核心SQLupdate iddeductBalance UPDATE member_card SET balance balance - #{amount}, update_time NOW() WHERE id #{cardId} AND status 1 AND balance #{amount} /update这个SQL的巧妙之处在于把余额检查放进了WHERE条件里。如果余额不足影响行数是0Service层根据返回的int值是否为1来判断扣款是否成功。相比先查余额再扣款的“两步走”这样一步到位的方式能有效避免并发下两个人同时读到同一个余额、然后都认为可以扣款的问题。扣款成功后紧接着往consume_record插入一条流水记录。两步操作必须在同一个事务方法里Override Transactional(rollbackFor Exception.class) public boolean consume(Integer cardId, BigDecimal amount, Integer adminId, String remark) { int rows cardMapper.deductBalance(cardId, amount); if (rows 0) { throw new BusinessException(余额不足或卡片状态异常); } ConsumeRecord record new ConsumeRecord(); record.setCardId(cardId); record.setAmount(amount); record.setAfterBalance(cardMapper.getBalance(cardId)); record.setAdminId(adminId); record.setRemark(remark); consumeRecordMapper.insert(record); return true; }这里加Transactional(rollbackFor Exception.class)非常关键。Spring默认只在遇到RuntimeException时回滚检查异常不会回滚。如果抛出一个BusinessException我自定义的运行时异常事务才能正常回滚保证不会出现“扣款没成功但流水插进去了”或者反过来“流水没插进去但钱扣了”的脏数据。3.3 Flask辅助服务统计报表与可视化接口Flask这块我定位为“纯辅助服务”只做三件事读数据库、算数据、出接口。我用的技术栈是Flask 2.x PyMySQL pyecharts。PyMySQL负责连MySQLpyecharts负责生成图表的HTML或JSON配置。一个典型的“近7日会员增长趋势”接口代码很简洁from flask import Flask, jsonify from flask_cors import CORS import pymysql app Flask(__name__) CORS(app) def get_db(): return pymysql.connect( host127.0.0.1, userroot, password123456, databaseswim_club, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) app.route(/api/stats/member_growth) def member_growth(): db get_db() cursor db.cursor() sql SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM member WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY day cursor.execute(sql) rows cursor.fetchall() cursor.close() db.close() return jsonify(rows) if __name__ __main__: app.run(host0.0.0.0, port5001)这个接口返回的JSON数组前端拿到后直接传给pyecharts的JS库就能渲染折线图。整段代码没有Java那套繁琐的引入和注入十来行就搞定了。这就是我坚持引入Flask的原因技术服务于业务哪个工具顺手就用哪个没必要为了“统一技术栈”去牺牲效率。注意Flask的数据库连接要记得关闭。我在上面的示意里每次都手动db.close()更规范的做法是写一个上下文管理器或用with语句保证连接释放。连接泄漏是Flask连着MySQL时候的高频问题跑几天接口突然变慢多半就是连接没释放。3.4 登录认证与权限控制的简化处理后台管理系统肯定要有登录功能。我这里没有引入Shiro或Spring Security这类重量级框架而是用手写的拦截器加Session实现的。原因很简单系统只有管理员一种角色用不上复杂的角色权限模型过度设计反而增加代码量。具体做法管理员登录成功后把用户对象放进Session写一个LoginInterceptor拦截器注册到SpringMVC配置里放行/login、/static/**等路径其余/admin/**路径都校验Session是否为空。在校验的同时还可以把当前操作人的id取出来作为后面流水表admin_id字段的写入来源。如果你想要接口级别的防篡改能力可以在这个基础上引入JWT。但我要说句实在话如果只是课设或小型场馆内部系统Session方案完全够用。答辩时如果老师问你“为什么不用Shiro”你可以回答说“系统角色单一Session方案实现代码更少维护成本更低且能满足现有的安全需求。”这种回答反而是加分的因为它体现了你思考过“方案是否匹配业务场景”。4. 完整落地流程从搭建到部署4.1 环境准备与项目骨架整个系统要在本地跑起来环境清单如下版本都是我这套代码亲测兼容的组合组件版本说明JDK1.8SSM运行基础Maven3.6.x依赖管理与打包Tomcat8.5Servlet容器跑SSMMySQL5.7 或 8.0数据存储Python3.8Flask运行环境Flask2.x辅助服务框架项目目录我是按“Java主工程 Python辅助工程”两个子项目分开管理的结构大概是这样swim-club-system/ ├── swim-club-admin/ # SSM主工程 │ ├── pom.xml │ ├── src/main/java/com/club/swim/ │ │ ├── controller/ │ │ ├── service/ │ │ ├── mapper/ │ │ ├── entity/ │ │ └── interceptor/ │ ├── src/main/resources/ │ │ ├── jdbc.properties │ │ ├── spring-mybatis.xml │ │ ├── spring-mvc.xml │ │ └── mapper/*.xml │ └── src/main/webapp/ │ ├── WEB-INF/web.xml │ ├── pages/ # JSP页面或静态页面 │ └── static/ └── swim-stats-service/ # Flask辅助工程 ├── app.py ├── stats_api.py └── requirements.txtJava工程放一个目录Python工程放另一个两者之间不互相依赖只共享同一个MySQL库。这套划分方式是经过几次返工后总结出的最佳结构如果一开始就把Java和Python代码混在一个工程里Maven和pip的依赖管理会互相干扰部署时也说不清楚谁是谁。4.2 开发环境联调要点开发联调是花时间最多的地方。SSM用IDEA启动Flask用PyCharm或直接命令行启动两个服务同时开着。第一次联调一定会碰到跨域问题页面上有一个图表数据从http://localhost:5001/api/xxx来浏览器默认会拦截这个跨域请求。Flask侧用flask-cors解决这是最成熟的做法pip install flask-corsfrom flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})等调试期过了上线时再把origins收紧成具体域名。Java侧因为是同端口部署的不需要处理跨域。另外一个关键点是数据库连接配置。SSM的jdbc.properties里有个经典坑jdbc.urljdbc:mysql://localhost:3306/swim_club?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiserverTimezone这个参数在MySQL 8.x下是必须的否则连接会报时区错误。characterEncodingutf8加上useUnicodetrue是为了防止中文乱码少了这个配置会员姓名写入数据库后很可能变成问号。4.3 调试文档怎么写才有用这类项目交付时通常附带“调试文档”和“讲解材料”。我曾经接手过别人的项目光启动就折腾了一下午就是因为文档只写了“导入项目、启动Tomcat”八个字。所以我整理调试文档时坚持一个原则让一个从没碰过这个项目的人按文档操作能独立跑起来。文档的固定结构是四段。第一段环境准备列清JDK、Maven、Tomcat、MySQL版本缺一不可第二段初始化数据库写清导入SQL文件的完整命令包括CREATE DATABASE和USE的指令第三段启动顺序先启动Flask服务再启动SSM主服务每步注明预期结果比如“控制台输出Running on http://127.0.0.1:5001表示Flask启动成功”第四段是测试用例给出登录账号、测试步骤和预期页面效果。配套的讲解材料更偏向“为什么”。比如面试官或老师问“事务在哪个方法上体现”你要能明确说出consume方法上的Transactional并解释回滚规则问到“SSM和Flask数据怎么同步”你要能说清共用一个MySQL库、Flask只读不写。讲解文档里我会专门把这些高频问题列出来配上标准回答思路省得临场发挥时逻辑混乱。4.4 部署上线的两种方式部署方案分两种。简单粗暴的方式Java工程用Maven的package打成war包放进Tomcat的webapps目录Flask服务用python app.py直接起端口固定5001。适合课程设计演示或小场馆内网使用。正式一点的方式Java工程同样打war包前端静态资源用Nginx托管并配置反向代理把/api/stats/开头的请求转发到Flask服务其余请求转发到Tomcat。这样做的好处是前端浏览器只跟Nginx的一个端口通信没有跨域问题而且Flask服务挂了还能通过Nginx配置快速切换备用节点。对一个小型场馆系统来说Nginx方案已经非常够用没必要上Docker加K8s那套全家桶。5. 常见问题与排查技巧实录5.1 数据库连接与中文乱码这类项目的高频故障我整理了一个速查表基本都是实操中真实踩过的坑症状可能原因解决方案启动报Access denied for user数据库账号密码错误或MySQL8的认证插件不兼容检查jdbc.properties确认账号密码MySQL8使用caching_sha2_password驱动要用mysql-connector-java8.x控制台报Unknown database数据库没创建或名字不对先执行CREATE DATABASE swim_club DEFAULT CHARSET utf8mb4页面中文全部变问号JDBC连接串缺少编码参数在jdbc.url末尾加useUnicodetruecharacterEncodingutf8Tomcat启动端口占用8080端口被其他程序占用修改Tomcat的server.xml里Connector端口或杀掉占用进程MyBatis报Invalid bound statement (not found)Mapper接口与XML的namespace或方法id不匹配检查XML的namespace为接口全限定名每个方法id与接口方法名一致XML路径在Maven里要能被扫描到5.2 SSM整合的经典配置坑SSM整合本身有几个经典配置顺序问题。spring-mybatis.xml里必须配置数据源、SqlSessionFactory和MapperScannerConfigurer三者的顺序不能乱。MapperScannerConfigurer会把指定包下的所有接口自动代理成MyBatis的Mapper配置如下bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.club.swim.mapper/ /bean如果这个配置漏了启动时会报Autowired注入Mapper失败因为Spring容器里压根没有对应的Bean。新手最容易在这个环节卡住排查方法就是看启动日志里有没有类似的Bean创建错误。另一个坑是Spring和SpringMVC的容器边界。我在网上见过不少人把Controller的扫描和Service的扫描混在同一个配置里导致事务失效。正确做法是Spring容器扫描Service和MapperSpringMVC容器只扫描Controller。事务的tx:annotation-driven必须放在Spring的配置文件里放在SpringMVC配置里事务不会生效代码跑起来没有报错但数据库不会真的回滚。5.3 Flask侧的高频问题处理Flask服务这边见得最多的是两个问题。第一个是端口冲突5000端口经常被macOS的AirPlay或其他服务占用我统一改成5001后就没再遇到第二个是PyMySQL连接MySQL8时的认证问题如果用的PyMySQL版本太老会报Authentication plugin caching_sha2_password cannot be loaded。解决办法一是升级PyMySQL到最新版二是在创建MySQL用户时指定mysql_native_password插件。Flask接口报错时还有一个排查技巧通过app.run(debugTrue)启动开发服务接口异常时浏览器会直接显示详细的Python报错堆栈和出错的代码行数比日志文件直观得多。等代码稳定后再把debug关掉防止生产环境暴露错误细节。5.4 那些调试中值得留意的细节调试阶段建议把SQL日志打开MyBatis的日志级别设成DEBUG控制台会打印每条执行的SQL和参数。我有一半以上的BUG都是这么发现的尤其是动态SQL拼接不对时日志里一眼就能看出SQL条件和预期不符。member_no的生成规则建议挪到Service层做单元测试覆盖。我当时写了一个测试用例连续调用1000次生成方法检查是否有重复编号。虽然单场馆场景下并发量不大但编号一旦重复整个会员体系的唯一性就崩了提前测试能省很多事。还有一点容易被忽略数据库初始化脚本要按版本命名比如v1.0_init.sql、v1.1_add_signin.sql。后面每加一个功能就生成一个新的增量脚本不要改旧的初始化脚本。否则别人拿你的项目从头部署时按文档导入旧脚本再跑新代码字段对不上一堆报错根本没法调试。整套系统做完我的体会是技术选型没有绝对的高下只有匹配不匹配。SSMFlask这套组合在纯商业项目里未必是主流但在这个具体场景下它确实让团队把精力花在了“业务逻辑”而不是“框架适配”上。如果你也正在做一个类似的管理系统建议先把业务表关系理清楚再写一行代码把事务边界定义好再考虑接口怎么串把文档写明白再交付给别人跑。最后再分享一个小经验部署时我在同一台服务器上同时琢磨过“要不要把Flask也包装成服务”后来发现生产环境直接用systemd或supervisor守护python app.py就够了简单可靠别把部署搞得太复杂。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →