
自己平时接过的管理系统项目也不少但像这种“SpringBoot2 Vue3 MyBatis-Plus MySQL8.0”四件套组合的体育馆管理系统还真是值得单独写一篇聊聊。前阵子刚把一个完整的体育馆管理系统源码从零搭起来从场地预约到会员充值再到器材租借前前后后踩了不少坑今天把这个项目的设计思路、核心模块拆解、实操步骤和典型问题一次性整理出来给后面要做类似管理系统、毕业设计或者接外包的同学一些参考。这套系统说白了就是一个典型的前后端分离管理平台后端用SpringBoot2提供REST接口前端用Vue3开发页面MyBatis-Plus负责数据库操作MySQL8.0做持久化存储。整个系统面向体育馆的运营场景核心功能涵盖场地管理、会员管理、预约订场、收费管理、器材租借、统计报表等。如果你是计算机相关专业的学生要拿它做毕业设计或者正在接一个体育馆信息化的私活那这篇文章里的内容基本可以帮你省掉一半的调研时间。提示我尽量把每个模块的“为什么这么做”也讲清楚。不光是给代码给你抄更重要的是让你改得动、跑得起、能答辩能交付。1. 项目概述与整体设计思路1.1 核心需求解析体育馆管理系统的核心业务其实不复杂但要梳理清楚因为它决定了数据库怎么建、接口怎么分、前端页面怎么组织。从我实际跟客户和导师沟通的情况来看主要的需求集中在这样几个方向场地资源管理体育馆里有篮球场、羽毛球场、乒乓球台、健身房等多个类型的场地每个场地有不同的时段价格、可预约状态、维护状态。会员管理体育馆通常采用会员制运营会员卡有不同等级普通会员、银卡、金卡对应不同的折扣会员要能充值、能查看余额和消费记录。预约订场用户选择场地、选择时间段、提交预约系统生成订单并锁定场地防止同一时段重复预订。收费与结算支持按次收费、按时段收费、会员卡余额扣费还要处理押金和退费。统计分析场馆运营方需要看到每天的营收、场地利用率、热门时段以便调整运营策略。搞清楚这些需求以后系统的技术边界就清晰了。它不是一个大而全的ERP而是一个业务聚焦、以“场地预约”为核心闭环的行业管理系统。所以项目分层上我选择了最经典的三层架构 前后端分离而不是引入微服务那套复杂度因为业务规模和团队规模都不需要。1.2 整体架构选型这个项目采用了前后端分离架构后端的职责是纯API提供方前端的职责是纯UI交互方。整个系统分为三个端管理员端后台管理、会员端小程序/H5操作、数据库层。为什么我要坚持前后端分离而不是用传统的服务端渲染有几个很现实的原因开发并行前端和后端可以同时开工不需要等对方。部署隔离API服务和静态页面可以分别部署后端扛请求前端走Nginx故障面缩小。扩展灵活以后如果要上小程序不需要改后端代码直接复用现有API。后端我采用SpringBoot2作为基础框架整合MyBatis-Plus做数据持久层。之所以不用SpringBoot3一方面是因为很多毕业设计和实际生产环境还在用SpringBoot2资料多、踩坑有人顶上另一方面是MyBatis-Plus和SpringBoot2的整合方案非常成熟兼容性上有保障。数据库用MySQL8.0主要看重它默认的utf8mb4字符集、更好的JSON支持和窗口函数后面做统计报表会很顺手。1.3 为什么选这个技术组合单看这几个技术选型可能有人觉得“这不就是烂大街的Java Web组合吗”确实这套组合在管理类项目中非常普遍但普遍恰恰说明它是经过大量项目验证的“黄金搭配”。拿MyBatis-Plus来说它在MyBatis基础上做了大量增强。最直观的体验就是单表CRUD几乎不用写SQL继承一个BaseMapperT就能获得常用的增删改查方法配合TableField、TableLogic这些注解节省的代码量非常可观。体育场馆管理系统里场地表、会员表、订单表这些实体的基本操作全部走内置方法只有涉及多表统计时才需要手写SQL开发效率提升得很明显。Vue3同样如此。组合式API带来的代码组织方式比Vue2的选项式API更适合中大型项目。同样一个“预约订场”页面用Vue3的setup语法糖可以把场地列表加载、时段选择状态、订单提交逻辑拆成几个可独立的函数模块代码的可读性和复用性会好很多。而且Vite作为构建工具冷启动速度比Webpack快一个量级开发体验非常舒服。2. 数据库设计与核心数据模型2.1 数据库表结构规划数据库设计是这类管理系统项目的命门表结构不合理后面写接口和页面的时候会到处受苦。我从事先规划到反复调整最终体育馆管理系统的核心表主要分成了几组用户与会员体系sys_user管理员账号表存后台登录用户的用户名、密码、角色、状态。member会员表存会员姓名、手机号、卡号、等级、余额、积分、注册时间。member_recharge_record充值记录表每一笔充值和赠送金额都留痕。场地与预约体系venue场地表包含场地名称、所属场馆区域、场地类型、容纳人数、每小时价格、状态可订/维护中/禁用。venue_reservation预约订单表包含会员ID、场地ID、预约日期、开始时段、结束时段、订单金额、折扣金额、实付金额、预约状态待支付/已确认/已取消/已完成。venue_maintenance场地维护表记录场地的定期检修、临时封场信息。器材与商品体系equipment器材表包含器材名称、类别、数量、单位时间租金、是否可外借。equipment_rental器材租借记录表关联会员和器材记录租借起止时间、租金、押金、归还状态。订单与财务体系order_info总订单表统一管理场地预约、以会员储值卡消费、器材租赁这些涉及资金流的记录。payment_record支付流水表记录每笔订单的支付方式、支付金额、交易时间。revenue_statistics每日营收统计表用定时任务生成每天各场地的营收汇总减轻报表查询压力。这套表结构是典型的“基础档案 业务单据 财务流水”三件套模型。基础档案场地、会员、器材保持相对静态业务单据预约订单、租借记录动态产生财务流水归集资金。打比赛、做毕设的时候给评委讲清楚这个递进关系会很加分。2.2 关键表设计详解场地预约去重问题整个系统里最有挑战性的数据表就是预约订单表难点在于“同一场地同一时段不能重复预约”。我给的方案是用场地ID 预约日期 开始时段 结束时段构成的联合唯一索引来从数据库层面保证数据不会被重复写入。看一段建表SQL的关键部分CREATE TABLE venue_reservation ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 预约ID, member_id bigint(20) NOT NULL COMMENT 会员ID, venue_id bigint(20) NOT NULL COMMENT 场地ID, reservation_date date NOT NULL COMMENT 预约日期, start_time varchar(10) NOT NULL COMMENT 开始时段如18:00, end_time varchar(10) NOT NULL COMMENT 结束时段如20:00, order_amount decimal(10,2) NOT NULL COMMENT 订单金额, discount_amount decimal(10,2) DEFAULT 0.00 COMMENT 折扣金额, pay_amount decimal(10,2) NOT NULL COMMENT 实付金额, reservation_status tinyint(1) DEFAULT 0 COMMENT 状态0待支付1已确认2已取消3已完成, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_venue_time (venue_id, reservation_date, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT场地预约订单表;那段联合唯一索引的uk_venue_time就是防重单的核心。这里有个容易踩的坑时间段字段到底是用datetime还是varchar我最终选择了varchar存储“18:00”这种格式。原因有两个一是体育馆的时段是按整点切分的粒度固定用字符串就能满足等值匹配二是后续前端展示和校验时字符串直接回显不需要做格式化。如果你想更规范一些也可以用TIME类型但要记得取出来的时候带上处理逻辑。还应该做一个系统层面的兜底校验在服务端提交预约的接口里先查询一下该时段是否已经被占用再执行插入。接口级校验和数据库唯一索引双保险基本杜绝了并发场景下的重复预约问题。2.3 会员储值和财务流水设计会员储值是体育馆系统里资金流最密集的地方。体育馆常见的营销玩法有“充500送50”“充1000送150”这类规则意味着余额和赠送金额要分开记。我在会员表上设计了两个字段balance_amount可用余额和bonus_amount赠送余额。充值时根据充值档位自动计算赠送金额写入充值记录表再更新会员表。每次消费时优先扣除赠送余额再扣可用余额这个策略比较常见也方便财务对账。对应地payment_record表我记录了订单来源场地预约、器材租赁、支付方式余额、微信、支付宝、实收金额、关联订单ID这样每天的对账就是一个简单的分组查询。后期如果要生成营收日报直接从这个表聚合不需要反查业务订单。SELECT DATE(create_time) AS stat_date, SUM(pay_amount) AS total_income, COUNT(DISTINCT order_id) AS order_count FROM payment_record GROUP BY DATE(create_time) ORDER BY stat_date DESC;这段SQL就是每日营收统计的核心逻辑配合SpringBoot里的Scheduled定时任务每天凌晨把前一天的汇总写入revenue_statistics表报表页直接读统计表毫秒级响应。3. 后端核心模块实现与关键技术点3.1 SpringBoot2项目结构与通用配置后端工程我按标准的Maven结构组织分包遵循“controller - service - mapper - entity - common”的套路。common包下面统一放结果封装、异常处理、工具类、全局常量。分包清晰的好处是后期迭代加功能的时候你知道代码该往哪里放。pom.xml里最关键的依赖就是mybatis-plus-boot-starter和mysql-connector-java。这里提醒一句MySQL8.0对应的JDBC驱动包坐标中com.mysql.cj.jdbc.Driver已经是默认驱动类而且8.0版本的驱动对时区和SSL的检查比旧版本严格配置连接串不处理好就会报错。我在application.yml里的数据源配置如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sports_center?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里重点说两个参数。第一个是serverTimezoneAsia/Shanghai不设置的话驱动会把本地时区当成UTC导致时间字段插入和查询差8个小时。第二个是allowPublicKeyRetrievaltrueMySQL8.0的caching_sha2_password认证插件在非SSL连接下首次连接需要获取服务端公钥不加这个参数就会报错。MyBatis-Plus的配置里map-underscore-to-camel-case: true很关键它能自动把数据库的member_name字段映射成实体的memberName属性省去大量TableField注解。逻辑删除的配置也很有用所有带deleted字段的表删除操作自动变成更新保证数据可追溯。3.2 登录鉴权与权限控制后台管理系统的登录鉴权我采用JWTJSON Web Token 拦截器的方式实现。用户登录成功后后端签发一个带有用户ID和角色信息的Token前端每次请求在Authorization头里带上这个Token拦截器解析验证通过就放行。生成Token的核心代码长这样String token Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact();这里有个值得强调的细节SECRET_KEY不能硬编码在代码里我是放在配置文件中环境切换的时候不会因为密钥不一致导致线上全部Token失效。拦截器实现HandlerInterceptor接口在preHandle方法中从请求头取Token解析失败直接返回401解析成功就把用户信息放进ThreadLocal里的UserContext这样后面Service层随时能拿到当前操作人比一遍遍传参省事得多。角色权限方面管理员和普通操作员的功能范围不同我在接口上使用自定义注解RequirePermission(venue:add)做细粒度控制。拦截器拿到角色后再比对注解要求的权限码权限匹配才放行。这套RBAC模型虽然简易但应付一个体育馆后台完全够用。3.3 场地预约与防重并发处理场地预约是一个典型的“验证可订状态 - 计算价格 - 生成订单”事务链。核心实现逻辑在一个Transactional方法中Transactional(rollbackFor Exception.class) public ReservationResult createReservation(ReservationRequest request) { // 1. 校验场地是否存在且处于可预约状态 Venue venue venueMapper.selectById(request.getVenueId()); if (venue null || venue.getStatus() ! 1) { return ReservationResult.fail(场地不存在或暂不可预约); } // 2. 校验该时段是否已被占用 Long count venueReservationMapper.selectCount( new LambdaQueryWrapperVenueReservation() .eq(VenueReservation::getVenueId, request.getVenueId()) .eq(VenueReservation::getReservationDate, request.getReservationDate()) .eq(VenueReservation::getStartTime, request.getStartTime()) .in(VenueReservation::getReservationStatus, 0, 1) ); if (count 0) { return ReservationResult.fail(该时间段已被预约请选择其他时段); } // 3. 计算价格 Member member memberMapper.selectById(request.getMemberId()); BigDecimal discountRate member.getLevel().equals(金卡) ? new BigDecimal(0.85) : new BigDecimal(1.00); BigDecimal payAmount venue.getHourlyPrice() .multiply(new BigDecimal(request.getDurationHours())) .multiply(discountRate) .setScale(2, RoundingMode.HALF_UP); // 4. 插入订单并锁定场地 VenueReservation reservation new VenueReservation(); // ... 设置订单属性 venueReservationMapper.insert(reservation); // 5. 扣减会员余额 memberMapper.deductBalance(request.getMemberId(), payAmount); return ReservationResult.success(预约成功, reservation); }这里需要特别说明事务为什么能防并发。Transactional保证了整个预约操作要么全部成功要么全部回滚。配合数据库端的唯一索引即便两个请求同时通过了步骤2的存在性校验插入的时候也会有一个被索引挡住抛出DuplicateKeyException。不过要注意这一步被拦截后事务会进入回滚状态如果你在同一个事务里捕了异常又不想回滚需要处理事务的rollback标记。前端配合时的体验优化也值得一提场地的时间段按钮一旦被选中立即把该时段标记为“锁定中”同时在提交前发起一个快速校验接口。虽然最终以服务端为准但前置校验能显著减少用户提交失败后的挫败感。3.4 MyBatis-Plus高级查询与统计报表统计报表模块是体育馆管理系统里技术含量最高但不是最显眼的部分。刚开始我以为只要把SQL写出来就完了但实际对接前端图表的时候才发现返回值结构要设计得非常贴合ECharts的输入格式。一个典型的“近7日营收趋势”接口我返回的结构是{ dateList: [2025-03-01, 2025-03-02, 2025-03-03], incomeList: [3200.5, 4150.0, 3800.0], orderCountList: [45, 52, 47] }这样的结构直接用ECharts的data属性绑定就行前端零加工。MyBatis-Plus里的聚合查询还是需要手写SQL来完成的用Select注解直接嵌在Mapper接口里Select(SELECT DATE(create_time) AS date, SUM(pay_amount) AS income, COUNT(*) AS orderCount FROM payment_record WHERE create_time #{startDate} AND create_time #{endDate} GROUP BY DATE(create_time) ORDER BY date) ListIncomeTrendVO selectIncomeTrend(Param(startDate) String startDate, Param(endDate) String endDate);MyBatis-Plus的Select注解里用#{}占位符做预编译能防止SQL注入。这里要注意的是日期字符串拼接如果你把#{startDate}换成${startDate}基本等于把数据库裸奔给用户看。这种低级错误在答辩和代码审查时一旦被发现印象分会掉得厉害。4. 前端Vue3实现要点与实践4.1 前端工程搭建与目录规范前端我采用的是Vite Vue3 TypeScript的组合。Vite的冷启动快TypeScript的静态类型检查能让大型项目管理更安全。有人说“管理系统用JS就够了”但当你改一个带十几个字段的预约表单时TS能帮你省掉大量因为字段拼错导致的低级bug。工程的目录结构大概是这样的src/ ├── api/ # 接口请求统一封装 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── layouts/ # 布局组件 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── views/ # 页面组件 │ ├── dashboard/ # 数据统计仪表盘 │ ├── venue/ # 场地管理 │ ├── member/ # 会员管理 │ └── reservation/ # 预约管理 ├── utils/ # 工具函数 └── types/ # TS类型定义每一层的职责非常单一views里只放页面组装逻辑api里只放网络请求stores里只放全局状态。这种目录约定其实就是在团队协作里降低沟通成本。Vue3的核心变化是组合式API。我用了script setup语法糖页面逻辑可以按功能拆分到独立的composable函数中。比如预约页面我拆成了useVenueList加载场地列表、useTimeSlots管理时段选择、useReservationSubmit提交预约每个函数各管一块逻辑互不干扰代码清晰度比之前用Vue2 Options API好太多。4.2 登录页面与Token持久化前端登录页的逻辑可以说是个小型的综合性工程。用户输入账号密码调login接口后端返回Token前端把Token存到localStorage同时放进Pinia的userStore里再根据路由守卫的规则跳转到首页。用Pinia管理登录状态核心代码如下export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: null as UserInfo | null }), actions: { async login(loginForm: LoginForm) { const res await loginApi(loginForm) this.token res.data.token this.userInfo res.data.userInfo localStorage.setItem(token, this.token) return res }, logout() { this.token this.userInfo null localStorage.removeItem(token) router.push(/login) } } })路由守卫是登录校验的核心。我在router.beforeEach里判断目标路由是否需要鉴权需要的话就看Pinia里有没有Token没有就强制跳登录页。这里有个细节localStorage和Pinia里的Token要保持同步防止刷新页面后Pinia状态丢失但路由还放行的情况。所以我初始化Pinia store时就从localStorage里恢复状态保持单数据源各端一致。Axios请求封装我也得说一说。所有请求走同一个实例统一设置baseURL、超时时间、请求拦截器附加Token和响应拦截器统一处理错误码。后端返回的code字段约定为200表示成功401表示登录过期403表示没有权限。响应拦截器里碰到401就自动清除本地登录状态并跳转登录页这个体验会让使用方感觉系统“很懂规矩”。4.3 场地预约核心页面实现预约订场页面是整套前端里交互最复杂的。它需要展示场地列表、日期选择、时间段网格、价格计算、订单确认这几个环节。时间段网格是一个7xN的表格一周七天、每天从早到晚分时段我使用el-table加自定义列模板来渲染。每个格子根据状态显示不同的样式可预约正常底色点击选中已预约灰色禁用当前选中高亮蓝色核心渲染代码片段el-table-column v-forday in weekDays :keyday :labelday template #default{ row } div v-forslot in timeSlots :keyslot classtime-slot :class{ selected: isSelected(day, row, slot), disabled: isBooked(day, row, slot) } clickhandleSlotClick(day, row, slot) {{ slot }} /div /template /el-table-column这里有个实战心得不要把判断是不是可预约的逻辑放在模板里写一大堆三元表达式而是抽一个isSlotAvailable(day, row, slot)的计算函数因为页面代码会越来越复杂模板里堆逻辑后面改起来非常痛苦。提交预约时除了表单里回显场地、日期、时段、单价、折扣、应付金额还有一个余额校验的前置判断如果会员余额小于应付金额直接禁用提交按钮并提示“余额不足请先充值”。这个小体验设计能挡住很大一部分无效请求。4.4 数据可视化与看板页面看板页面是体育馆管理员每天打开的第一个页面核心数据包括今日营收、今日预约单数、场地利用率、热门时段排行。我使用ECharts做图表封装了一个BaseChart组件通过props传入option配置组件内部负责初始化实例、监听窗口变化、销毁实例。封装ECharts组件其实就三步init初始化、setOption更新配置、dispose销毁实例。但实际工作中比较坑的是图表容器在Tab切换或弹窗打开时宽度计算为0造成图表显示空白。我遇到后加了一个nextTick里调用chart.resize()的处理同时设置ResizeObserver监听容器尺寸变化。场地利用率的热力图是ECharts的heatmap类型x轴是星期y轴是时段颜色越深代表预定量越高。这个图做完以后体育馆的真实运营规律一目了然——比如周五晚上18:00-20:00是篮球场高峰期周六上午是羽毛球场的黄金档。这种可视化不仅提升系统的专业感还可以作为答辩时的亮点材料。5. 环境准备与项目部署实操5.1 MySQL8.0安装与初始化MySQL8.0现在已经是主流版本安装方式有很多种Windows下用安装包、Linux下用yum或者tar包。这里说两个最常见的坑。第一个是Windows安装版的字符集设置。安装过程中不要一路Next一定要在配置界面把utf8mb4作为默认字符集选上。如果装完才发现不是可以在my.ini里补配置[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci [client] default-character-setutf8mb4改完以后记得重启MySQL服务才能生效。校验方法很简单执行SHOW VARIABLES LIKE character_set_server;看到值变成utf8mb4就说明改对了。第二个坑是Linux环境下的初始化命令。8.0版本的初始化方式和5.7不同要用mysqld --initialize-insecure生成空密码的root账号之后再用ALTER USER rootlocalhost IDENTIFIED BY 你的密码;来设置密码。不要用旧的mysql_install_db那个已经废弃了。系统初始化还需要创建数据库和导入项目SQL脚本mysql -uroot -p CREATE DATABASE sports_center DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE sports_center; SOURCE /path/to/sports_center.sql;5.2 后端打包与启动后端工程用Maven打包非常简单mvn clean package -DskipTests打包完成后在target目录下生成sports-center-0.0.1-SNAPSHOT.jar部署时直接java -jar sports-center-0.0.1-SNAPSHOT.jar --spring.profiles.activeprodapplication-prod.yml里改数据库地址、密码和日志级别。有一点要注意生产环境配置里记得关闭Swagger等接口文档以免把字段结构暴露出去。打包前检查一下pom.xml里有没有引入spring-boot-maven-plugin没加的话打出来的包不能直接用java -jar运行因为缺了MANIFEST里的Main-Class信息。想后台守护运行的话用Systemd或者直接nohup java -jar xxx.jar app.log 21 。我自己的服务器习惯用Systemd管理这样开启自启和日志管理都方便。5.3 前端构建与Nginx部署前端开发阶段是npm run dev部署阶段用npm run build构建完成后在dist目录下生成纯静态文件。接下来用Nginx托管Nginx配置的核心点有两个一是把/路径指向dist目录二是把/api开头的请求反向代理到后端服务端口。server { listen 80; server_name your-domain.com; root /opt/sports-center/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }try_files那行是前端路由在history模式下的核心配置不加它刷新一个子路由页面就会404。部署完成后可以用浏览器的无痕窗口完整跑一遍核心流程确认接口、静态资源、路由都没有问题。6. 常见问题排查与实战避坑6.1 数据库连接相关报错Public Key Retrieval is not allowed这个错是我见过最多的。原因我之前提过MySQL8.0默认使用caching_sha2_password插件解决办法就是在JDBC连接串后面加allowPublicKeyRetrievaltrue。但需要说明如果你用的是老版本驱动5.x是看不到这个报错的因为它根本不认识这个认证插件。The server time zone value йʱ is unrecognized这个乱码报错是时区配置问题。JDBC连接串里加上serverTimezoneAsia/Shanghai基本能解决。另外可以顺手把操作系统和JVM的时区也设成Asia/Shanghai彻底消除时区隐患。Unknown database sports_center这个就是建库语句没执行执行一下CREATE DATABASE就好。6.2 MyBatis-Plus使用中的三个高频问题分页失效MyBatis-Plus的分页查询必须显式配置分页插件否则你传了Page对象也只会查出所有数据然后内存分页。SpringBoot2项目中加一个配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }逻辑删除不生效实体类字段上加TableLogic同时配置里指定逻辑未删除值和已删除值。如果删不掉数据大概率是注解没加或者字段名跟配置对不上。自动填充失效如果你表中定义了create_time和update_time想自动维护实体类字段上要加TableField(fill FieldFill.INSERT)之类的注解同时定义MetaObjectHandler的实现类去填充字段值。不配置的话插入时时间字段就是NULL。6.3 前端常见问题处理跨域问题开发阶段前端在http://localhost:5173后端在http://localhost:8081跨域是必然的。最快的解决方案是在Vite配置里加代理// vite.config.ts server: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } }上线以后Nginx的反向代理解决的是同域问题所以前端的baseURL在开发环境和服务端不一致时要把API路径抽成环境变量构建时按环境自动切换。登录后刷新页面状态丢失前面提过Pinia的状态在浏览器刷新后会被清空。解决方案就是持久化要么用pinia-plugin-persistedstate要么自己在初始化时从localStorage恢复。这几乎是每个管理系统项目都躲不开的细节。6.4 部署后接口响应慢的排查思路如果部署到服务器之后用户反馈接口很慢我的排查顺序是这样的先看是不是数据库瓶颈SHOW PROCESSLIST看看有没有慢SQL在跑。然后看是不是缺索引用EXPLAIN分析核心查询语句走没走索引一眼就知道。再看接口有没有N1查询问题MyBatis-Plus里循环查单表会触发N次SQL这种情况用selectBatchIds批量查询代替。最后看是不是静态资源占用带宽图片、JS、CSS有没有开Gzip压缩这个在Nginx里加gzip on;就能优化。还有一个细节容易被忽略服务器内存不够导致频繁GC。部署Java应用前用top命令看一下可用内存给JVM合理设置-Xms和-Xmx然后配合jstat或者arthas做进一步分析。7. 项目扩展与文档整理经验我还想单独聊聊“【含文档】”这三个字。给这个项目配文档的时候我做的是一份前后端分离时期最标准的项目文档包含这几部分需求分析文档用例图 功能清单 业务流程描述。数据库设计文档每张表的字段说明、索引设计、ER图。接口文档所有接口的请求方法、参数、返回示例。部署手册从零开始的环境搭建、启动步骤、常见问题。文档的价值不是写完就完事而是让新人接手项目时能快速跑起来。很多项目功能做得不错项目跑不起来文档是主要障碍。写文档时我坚持的原则是每一步命令行都给出可以直接复制执行的完整命令而不是只写一个方向。环境变量怎么设、依赖怎么装、配置怎么改全部具体到字符级别。对于想拿这个项目做毕业设计或者接私活外包的同学文档这一块认真做会给整个交付质量加分。尤其是接口文档我推荐用Apifox或者Swagger生成既能在线调试又能导出离线文档部门协作或答辩演示都很方便。另外这个项目后续还可以继续扩展的方向有很多。比如对接实际微信支付、支付宝支付接口把在线支付闭环做起来或者加入大屏数据展示功能在体育馆大厅放一个运营驾驶舱大屏再或者接入手环、门禁等物联网设备实现场地自动开门。这些说起来有点远但架构层面已经给这些扩展预留了空间。我在实际做类似项目时的体会是这类管理系统真正的技术难点其实不在某个框架怎么用而在于怎么把业务需求翻译成合理的表结构和稳定的接口设计。技术问题上网一搜基本都有答案但业务建模的经验只能靠一个一个项目慢慢积累。如果你马上要开始做体育馆或者类似的场馆管理系统建议在动手写代码之前先把场馆的预约规则、会员等级折扣、退费流程这些业务细节和甲方对齐业务理不顺后面改起来非常痛苦。最后再分享一个实用技巧拿到一个新的管理系统需求时先画出核心业务流程图搞清楚“谁在什么条件下做什么事产生什么结果”然后对着流程图建表、写接口、画页面。按照这个顺序走项目的返工率会低很多。