SpringBoot+Vue影城管理系统实战:从数据库设计到选座并发控制
接触过不少刚学完 Java 基础的同学第一个想动手做的项目往往就是“XX管理系统”。说实话管理系统确实是练手的好选择业务逻辑清晰、 CRUD 为主、前后端都能练到。但真正能把一个管理系统从 0 到 1 完整做出来并且把细节打磨到位并没有想象中那么轻松。这段时间我正好在做一个基于 SpringBoot Vue 的影城管理系统技术栈就是标题里那套Java MySQL MyBatis。这个项目不算大但涵盖了影院日常运营的核心场景影片管理、排片管理、选座购票、订单管理、会员营销等等。整个做下来踩了不少坑也积累了不少经验。今天这篇文章我就把整个项目的设计与实现过程做个完整的复盘从需求拆解到数据库设计从后端接口到前端页面从部署到排错把关键环节都说清楚希望能给正在做类似项目的同学一条清晰的路。1. 项目整体拆解与核心模块划分动手写代码之前我先做了一件事把影城管理系统当成一个真实的业务场景来梳理。影院运营方管理员需要什么功能观众普通用户需要什么功能这两个角色的诉求完全不同必须先理清楚再动手。1.1 用户角色与权限边界这个系统我划分了三种角色各有各的权限范围角色核心诉求主要操作普通观众快速浏览影片、选座购票、查询订单影片浏览、场次查看、在线选座、下单支付、订单查询影院运营管理员日常运营管控、数据维护影片信息管理、影厅管理、排片管理、订单审核、统计报表系统超级管理员系统级配置与人员管理员工账号管理、角色权限分配、系统参数配置其实很多管理系统在权限这块做得很随意甚至直接做成单角色只有管理员。我觉得既然是系统设计尽量把角色边界做清晰哪怕代码量多一点对于理清业务模型和后续扩展都更有价值。比如后面要做“门店管理员”和“总部运营”的区分角色模型清晰了扩展起来就顺手很多。1.2 功能模块清单整个系统我按业务域拆成了 7 个核心模块影片管理影片信息维护片名、简介、海报、时长、类型、上映状态支持上下架操作。影厅管理影厅座位布局的维护包括影厅号、座位行列数、座位类型普通座/情侣座/无障碍座。排片管理把影片、影厅、时间三者绑定生成具体的场次这是后期所有购票业务的基础。在线购票用户基于某场次选座锁定座位并生成订单模拟支付后出票。订单管理订单状态流转待支付、已支付、已出票、已取消、已退票超时取消机制。会员营销会员等级、积分累计、优惠券发放与核销用于提升用户黏性。统计报表票房统计、场次上座率、影片热度排行用图表展示。这个清单看着不复杂但每一条展开都有业务细节。比如“选座”这件事座位状态就分可售、已售、锁定、维护四种并发场景下怎么保证两个用户不会买到同一个座位这就是一个真正值得思考的业务问题。1.3 技术选型与理由技术栈选的是 SpringBoot Vue MySQL MyBatis这套组合是当下 Java 后端开发非常主流的一套方案适合这个项目的几个原因SpringBoot 简化了项目搭建和配置内置 Tomcat一键启动省去大量 XML 配置。MyBatis 轻量灵活SQL 自己掌控特别适合业务场景清晰、查询条件复杂的管理系统。虽然 JPA 开发更快但 SQL 可控性更强排查问题也直观。MySQL 是关系型数据库的首选生态成熟事务支持好影城这种订单金额的场景必须依赖事务保证数据一致性。Vue Element UI我实际用的是 Vue2 Element UI 的组合做后台管理界面非常高效组件丰富表格、表单、弹窗这些高频组件开箱即用。这个选型不求新但求稳。对大多数初学者或者做毕设性质项目的同学来说这套组合的参考资料最多遇到问题也最容易搜到解决方案。2. 数据库设计像搭积木一样把表关系理顺数据库设计是整个项目的基石设计不好后期写 SQL 就是灾难。影城管理系统涉及的实体不少但实体关系其实很清晰核心就一条业务链影片 → 排片场次 → 座位 → 订单。2.1 核心表结构我实际建的表有 12 张这里挑最关键的说。影片表filmid、film_name片名、film_type类型、film_duration时长分钟、 poster_url海报地址、director导演、actors主演、 description简介、publish_date上映日期、status1上映中 0已下架影片表的字段其实可以更丰富比如想看人数、评分、预告片地址但核心字段就是以上这些。海报地址我用的是相对路径文件实际存服务器目录数据库只存路径字符串避免把图片二进制直接塞进数据库这是个大坑数据库膨胀极快读写性能下降。影厅表hallid、hall_name影厅名、row_count行数、column_count列数、 seat_type座位布局类型、status启用/停用影厅表本身字段不多但座位的生成逻辑在代码里。新建一个影厅后我会在服务端根据行列数自动生成对应的座位记录写入座位表。座位表seatid、hall_id所属影厅、row_no行号、col_no列号、 seat_type普通座/情侣座/VIP座、is_active是否可用座位表与影厅是多对一的关系。为了让用户能直观地看到“我选的座位在哪个位置”座位的行号用数字1、2、3...列号用字母A、B、C...比如“5排C座”就很直观。场次表scheduleid、film_id影片id、hall_id影厅id、start_time开始时间、 end_time结束时间、price票价、stock余票数量、status场次状态场次表是整个购票业务的枢纽。注意 end_time 不用用户输入我会根据影片时长 开场时间 默认间隔比如 20 分钟清场时间自动计算。stock 是冗余字段值等于该影厅可用座位数减去已售和锁定座位数查询列表时直接展示不用每次都 count 子查询性能更好。订单表ordersid、order_no订单编号、user_id下单用户、schedule_id场次id、 seat_ids选座id集合JSON格式、total_price订单总金额、 status0待支付 1已支付 2已出票 3已取消 4已退票、create_time、pay_time订单表的 seat_ids 用 JSON 格式存虽然违反了严格的数据库范式但在这个场景下可读性和扩展性都更好查询时直接拿整个字符串解析即可。如果将来要做复杂的选座数据分析再抽独立表也来得及。2.2 为什么订单表不按“一票一订单”设计这是我在设计阶段认真纠结过的问题。一个订单对应多张票比如一次选两个座位是每条票单独生成一个订单还是一个订单里包含多张票我最终选择了“一单多票”的方案理由有二用户视角“我付了一次钱应该是一笔订单”。一个订单对应多条票记录虽然符合“标准设计”但用户查订单时要看到两个订单逻辑上就不够直观。支付视角一次支付动作对应支付平台的一笔流水订单和支付流水做成 1:1 关系财务对账更简单。基于这个决定订单表里用一个 seat_ids 字段而不是独立的订单明细表。简单场景下这样处理完全够用少一层关联查询。2.3 索引设计表结构定好后我给高频查询字段加了索引orders 表user_id status 组合索引用户查订单列表、order_no 唯一索引商户号查询schedule 表film_id start_time 组合索引按影片和时间筛选场次、hall_id 索引查影厅排期film 表status 索引只查上映中的影片。索引不是越多越好每多一个索引写入时就要多维护一棵 B 树。管理系统写入量不算大但也要克制只加真正在查询和排序中用到的字段。2.4 一个值得注意的小细节时间字段类型排片管理中涉及大量时间比较前端传的是 JSON 字符串比如2025-06-20 14:30:00后端接收后转 LocalDateTime 存进 MySQL 的 datetime 类型。这里我踩过一个坑MyBatis 的 TypeHandler 对 LocalDateTime 的处理在不同版本有差异。如果你用的 JDBC 驱动版本比较老存进去的时间会变成 Date 类型取出来再传给前端时格式化就乱了。我的解决方案统一在代码里配置 JSON 序列化的 DateTimeFormatter 为 yyyy-MM-dd HH:mm:ss同时数据库统一用 datetime不是 timestamp前后端时间显示就稳定了。3. 后端实现业务逻辑落地的关键环节后端我按 SpringBoot 的标准分层来做Controller → Service → Mapper。Controller 只做参数接收和结果返回Service 处理业务逻辑Mapper 写 SQL。分层清晰了出问题找起来快很多。3.1 统一返回结果与全局异常处理我封装了一个返回结果类所有接口的返回结构统一为{ code: 200, message: 成功, data: { ... } }code 为 200 表示成功非 200 表示业务失败比如 400 参数错误、401 未登录、403 无权限、500 系统异常。同时配置了全局异常处理器。比如业务中抛出“座位已被锁定”这样的自定义异常统一在这里拦截并转成 JSON 返回前端拿到 code 和 message 直接弹提示不用每个接口写 try-catch。代码整洁度提升非常明显。3.2 核心业务逻辑选座购票的事务与并发处理购票是整个系统最核心、也最容易出 bug 的地方。用户选了两个座位点击支付此时后端做的事情是校验座位状态 → 锁定座位 → 生成订单 → 扣减库存。这里面最大的问题是并发两个用户同时选中了同一个座位怎么办我用的方案是“乐观锁 状态校验”两步走。座位表里加了一个 version 字段乐观锁标记。更新座位状态时在 SQL 里带上版本校验UPDATE seat_lock SET status 2, user_id #{userId}, version version 1 WHERE id #{seatId} AND hall_id #{hallId} AND schedule_id #{scheduleId} AND status 1 AND version #{oldVersion}这条 SQL 如果影响行数为 0说明座位已经被别人抢了或者状态不对直接抛异常提示“座位已被购买请重新选择”。这个方案的优点是实现简单、性能好不需要引入 Redis 分布式锁这种重武器。对于影城管理系统的并发规模通常单场次几百个座位同时抢票的人不会太多完全够用。另一个关键点是整个购票流程要放在一个事务里。我用的方案是在 Service 方法上加Transactional注解任何一步异常订单、座位锁定、库存扣减全部回滚避免出现“订单没生成但座位被锁了”这种数据不一致。3.3 超时未支付自动取消用户选座后如果一直不支付座位就得一直锁着吗那肯定不行。我实现了一个简单的“定时取消”逻辑创建一个定时任务每隔 1 分钟扫描一次订单表把“创建时间超过 15 分钟且状态仍为待支付”的订单揪出来将其状态改为已取消同时把座位状态释放回可售、库存加回。UPDATE orders SET status 3, cancel_time NOW() WHERE user_id ... AND status 0 AND create_time DATE_SUB(NOW(), INTERVAL 15 MINUTE)后续发布一个“回滚座位”的批量任务处理已取消订单的座位释放。定时任务用 Spring 自带的Scheduled就行单机够用。如果用 SpringBoot 的集群部署就要考虑分布式锁避免多个节点重复处理同一批订单不过那是进阶话题了本项目用不上。3.4 订单编号生成规则订单编号我用的规则是时间戳 随机数。具体格式是yyyyMMddHHmmss 4位随机数字再加用户 id 的后四位做收尾。例如202506201530126518。这样生成的订单号能在一定程度避免重复。如果还要更严格可以在数据库里加唯一索引“用户id 时间范围内的订单号唯一”即使并发下偶发重复插入时也会被数据库挡下来。3.5 排片与场次冲突校验排片管理有一个隐藏门槛同一个影厅在同一时间不能安排两场电影。如果只是做一个单表的增删改查业务很快就成型了但排片一旦冲突后面用户在现场就会看到两个场次混在一起。我给排片接口加了一个冲突校验逻辑新增场次时检查同影厅、同时间段是否已存在场次。// 伪代码冲突校验 ListSchedule schedules scheduleMapper.selectByHallIdAndTimeRange( hallId, startTime, endTime); if (schedules.size() 0) { throw new BusinessException(该影厅在所选时间段已有排片); }这里的“时间段”指开始时间和结束时间的交叉区间SQL 逻辑是SELECT COUNT(*) FROM schedule WHERE hall_id #{hallId} AND status ! 3 AND ((#{newStart} BETWEEN start_time AND end_time) OR (#{newEnd} BETWEEN start_time AND end_time) OR (start_time BETWEEN #{newStart} AND #{newEnd}))这个条件看着复杂其实就是判断两个时间区间是否有重叠。实际开发中你只需要把区间重叠判断的四种情况列出来边界情况多测几组就不会漏。3.6 登录认证JWT系统需要区分用户角色我用的认证方式是 JWT。用户登录成功后后端签发一个 token前端存到本地存储里后续请求在请求头带上Authorization: Bearer token。JWT 的核心优点是服务端无状态不需要在 Session 中保存登录信息。缺点是 token 一旦签发在有效期内无法主动失效。管理系统的权限变更场景不多可以接受。我在 JWT 的 payload 里放了 userId、role 字段。后端写了一个拦截器解析 token把用户信息塞到 ThreadLocal 里Service 层拿当前登录用户就比较方便public class UserContext { private static final ThreadLocalLoginUser HOLDER new ThreadLocal(); public static void set(LoginUser user) { HOLDER.set(user); } public static LoginUser get() { return HOLDER.get(); } public static void clear() { HOLDER.remove(); } }拦截器里校验 token校验通过就放行校验失败返回 401。这里有个细节要留意每次请求结束必须调用UserContext.clear()否则线程池复用时上一个用户的信息会被下一个请求读到产生串号问题。这个坑我排查了很久才找到原因。4. 数据库表设计一次性把表结构和关系理清楚这一节我单独拿出来讲因为数据库设计是很多初学者容易忽视但实际极其重要的一环。一个差劲的表结构会导致后面写 SQL 时这里 join 那里嵌套维护成本直接翻倍。4.1 总体关系梳理整个系统的实体关系和业务链可以这样串起来用户普通用户/管理员都在统一的一张用户表中通过角色字段区分。影片表和影厅表是两个基础资料表本身无关联。排片表把影片和影厅关联起来一个影片可以有多场排片一个影厅可以承接多场排片不同时间。场次表与座位表通过 schedule_id 关联某场次的座位状态单独存储。订单表与用户表、场次表关联。这样设计的好处是数据模型分清了“资料数据”影片、影厅和“业务数据”场次、订单两者分开互不干扰。4.2 关键表的详细字段说明我把几张核心表的建表 SQL 贴出来大家可以直接参考这份结构做开发。-- 用户表 CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL COMMENT 登录账号, password varchar(255) NOT NULL COMMENT 加密密码, nickname varchar(64) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, role tinyint NOT NULL DEFAULT 1 COMMENT 1-观众 2-管理员 3-超级管理员, status tinyint NOT NULL DEFAULT 1 COMMENT 1-正常 0-禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 影片表 CREATE TABLE film ( id int NOT NULL AUTO_INCREMENT, film_name varchar(128) NOT NULL, film_type varchar(64) DEFAULT NULL, duration int DEFAULT NULL COMMENT 时长分钟, poster_url varchar(255) DEFAULT NULL, director varchar(64) DEFAULT NULL, actors varchar(255) DEFAULT NULL, description text, publish_date date DEFAULT NULL, status tinyint NOT NULL DEFAULT 1 COMMENT 1-上映中 0-已下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 场次表 CREATE TABLE schedule ( id int NOT NULL AUTO_INCREMENT, film_id int NOT NULL, hall_id int NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, price decimal(8,2) DEFAULT NULL, stock int NOT NULL DEFAULT 0, status tinyint NOT NULL DEFAULT 1 COMMENT 1-正常 0-停售, PRIMARY KEY (id), KEY idx_film_start (film_id, start_time), KEY idx_hall_time (hall_id, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 座位表场次座位 CREATE TABLE seat_occupancy ( id int NOT NULL AUTO_INCREMENT, schedule_id int NOT NULL, row_no varchar(8) NOT NULL, col_no varchar(8) NOT NULL, status tinyint NOT NULL DEFAULT 1 COMMENT 1-可售 2-锁定 3-已售 4-维护, user_id int DEFAULT NULL COMMENT 锁座/购票用户, order_id int DEFAULT NULL COMMENT 关联订单, version int NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_schedule (schedule_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里解释一下为什么座位表里有个version字段这就是乐观锁标记专门用于解决并发抢座问题。每次更新座位状态时SQL 会带上前一次查询到的 version 值如果执行期间有其他事务改过这行数据version 就变了当前更新就会失败影响行数为 0从而实现“谁先改谁成功”。4.3 建表时的几个通用建议字符集统一用 utf8mb4如果用户提交的文本里有 emoji 符号比如影评里有 用 utf8 会把数据截断或报错utf8mb4 是它的完整超集。金额字段用 decimal不要用 double。电影票的金额虽然只有两位小数但涉及到支付和财务必须精确。float/double 在计算机里是浮点表示的计算会产生精度误差这在财务场景是不可接受的。冗余字段要克制。比如场次表的 stock 是典型的冗余字段设计时要明确它的维护方式每次售/退都同步更新想清楚之后才加。5. 前端实现Vue Element UI 构建后台界面前端这块我采用的是经典的后台管理结构左侧菜单栏、顶部导航栏、右侧内容区。没有过度设计用 Vue 生态里最常用的框架组合效率很高。5.1 前端目录结构src/ ├── api/ // 接口封装 │ ├── film.js │ ├── schedule.js │ ├── order.js │ └── user.js ├── assets/ // 静态资源 ├── components/ // 通用组件 ├── router/ // 路由配置 ├── store/ // 状态管理 ├── views/ │ ├── film/ // 影片管理页面 │ ├── hall/ // 影厅管理页面 │ ├── schedule/ // 排片管理页面 │ ├── order/ // 订单管理页面 │ ├── user/ // 会员管理页面 │ └── dashboard/ // 统计报表页面 └── utils/ // 工具类request封装、时间格式化等把每个模块的页面放在对应目录下维护起来非常直观。新增一个页面时不需要全项目乱找代码位置直接在对应目录里加文件就行。5.2 请求封装与拦截器我封装了一个request.js基于 axios 统一处理请求请求拦截从本地存储取出 token添加到请求头响应拦截解析统一返回值。如果 code 不是 200用 Element UI 的 Message 组件弹出错误提示如果 code 是 401跳转登录页统一处理 token 过期。这样前端所有接口调用处不需要重复写错误处理代码干净很多。// request.js 核心逻辑简化版 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message({ message: res.message, type: error }) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { Message({ message: 网络请求异常, type: error }) return Promise.reject(error) } )5.3 核心页面实现要点选座页面选座页面是这个系统中最有交互感的一部分。我用 CSS Grid 实现座位布局普通座位是方块选中时高亮变色已售和锁定状态置灰且不可点击。数据是从后端接口一次拉取该场次的所有座位状态前端根据状态字段判断渲染样式。交互逻辑上为了防止用户反复点击后端导致重复请求我做了前端限制最多选中 5 个座位点击支付时一次性把座位 id 数组传给后端后端统一处理。排片管理页面排片页面用的是 Element UI 的日期时间选择器 影片下拉框 影厅下拉框。提交排片时把影片时长、开始时间传给后端后端计算结束时间。我还在前端做了一个二次确认提示“当前场次结束时间为 22:15确认排片”避免用户手动输入错误。统计报表页面统计报表我一开始用的是后端查询 前端 ECharts 渲染。后来发现如果数据量不大直接把后端聚合好的 JSON 返回前端图表直接绑定能省去前端再处理数据的步骤逻辑简单可靠。比如“近七天票房走势”后端接口返回[ { date: 06-14, boxOffice: 5200.00 }, { date: 06-15, boxOffice: 6300.00 } ]前端 ECharts 直接把数组塞进 series.data一个简单的柱状图就完成了。5.4 前端打包与部署前端开发完成后执行npm run build会生成 dist 目录里面是纯静态文件。我可以把 dist 目录直接扔进后端项目的 static 目录让 SpringBoot 同时提供静态资源服务和接口服务这样部署只需要跑一个 Java 进程非常省事。这个方案的优点是简单无需单独给前端配 Nginx适合个人项目或学习环境。但如果你做的项目之后要上生产、要分离部署、要做负载均衡那还是建议前后端分离部署前端静态文件放在 Nginx 里后端接口单独启动一个服务通过 Nginx 反向代理。6. 完整开发流程复盘从 0 到 1 的操作顺序很多同学拿到项目后第一反应就是“直接开始写代码”然后被各种问题卡住。实际上合理开发顺序能极大提高效率。我把自己的实操顺序整理在这里。6.1 第一步搭建项目骨架和数据模型先创建 SpringBoot 项目引入依赖spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok、jjwt 等配置好application.yml里的数据源。然后按前面的表结构把建表 SQL 在本地 MySQL 执行一次。遇到字段要微调比如加了某个枚举值就在这一步解决。表结构是一切的根基后面代码都是围绕它来的。6.2 第二步编写实体类和基础 CRUD按表结构生成对应的实体类。这一环节我用 Lombok 的Data注解减少 getter/setter 的样板代码。然后先实现最简单的“影片管理”模块跑通一个完整的“后端查数据 → 返回 JSON → 前端表格展示”链路。这是项目管理中的“纵向切片”做法先打通一条线再横向铺开其他模块而不是先把所有后端写完再一次性写前端。6.3 第三步按业务域顺序实现我实现的顺序是用户模块登录、注册、权限校验影厅模块含座位生成影片模块增删改查、上下架排片模块含时间冲突校验选座购票模块核心业务事务并发控制订单模块状态流转、超时取消统计报表SQL 聚合、ECharts 展示。这个顺序遵循一个原则先做基础资料管理再做依赖这些基础资料的业务功能。排片依赖影片和影厅购票依赖排片和座位逻辑依赖关系清楚了依次实现自然顺畅。6.4 第四步联调和数据填充前后端都写得差不多之后开始联调。我发现很多问题是在这一步才暴露的比如时间格式后端返回的时间字符串是 2025-06-20T14:30:00前端显示有 T还得再做格式化跨域问题前端和后端不在同一端口SpringBoot 没配 CORS请求直接被挡。最终我在后端的 WebMvcConfigurer 里统一配置了跨域Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .maxAge(3600); } }如果部署时前后端在同端口前端扔进后端 static 目录这个配置就不需要。但开发环境一个 8080 一个 5173没跨域配置寸步难行。为了看效果我还手动往数据库插了一些模拟数据6 部影片、4 个影厅包含 8x8、6x10 两种规格、以及未来三天的场次。这一步很重要没有数据前端页面和统计图表都是空的联调无从谈起。6.5 第五步测试与优化最后阶段我做了三类测试功能测试每个接口用测试工具跑一遍确认参数边界比如负数、空值、超长字符串不崩溃并发测试用测试脚本模拟 20 个并发用户抢同一批座位确认不会出现超卖兼容性测试前端在不同浏览器Chrome、Edge上打开确认样式不塌。测试过程的产出不止是修 bug还会发现很多设计不合理的地方我调整了几处接口参数格式加了一些必要的参数校验注解NotNull、Min 等更安全也更友好。7. 常见问题与排查技巧实录这部分是整篇里我觉得最有价值的内容。开发过程中遇到的坑很多是教科书上不会写但实际必踩的列出来供大家参考。7.1 问题一前端请求后端 404/无法访问现象前端页面正常启动但每次调用接口都提示“Network Error”或者后端日志里收到 404。原因这类问题 80% 是路径问题。可能后端RequestMapping路径写错了或者前端api文件里 baseURL 没配对。排查思路先用浏览器直接访问后端接口文档地址看响应是否正确再核对后端application.yml里的 context-path如果设置了/api前缀前端 baseURL 要对应加上最后看浏览器控制台的完整报错信息对比实际请求的 URL 和后端路由是否匹配。7.2 问题二LocalDateTime 序列化格式不对现象前端表格里时间显示为2025-06-20T14:30:00带 T 字母丑且不符合业务预期。原因默认的 Jackson 序列化器对 LocalDateTime 的处理方式会输出 ISO 格式不是我们习惯的yyyy-MM-dd HH:mm:ss。解决方案在application.yml中配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8或者用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)加在实体类的 LocalDateTime 字段上。7.3 问题三MyBatis 中大于小于号报错现象SQL 里写了start_time #{param}报 XML 解析错误。原因MyBatis 的 XML Mapper 文件里会被当作 XML 标签开始符解析直接报错。解决方案用转义字符lt;代替gt;代替或者把比较条件写在 CDATA 区![CDATA[ AND create_time DATE_SUB(NOW(), INTERVAL 15 MINUTE) ]]这是 MyBatis 用得越多越会碰到的坑记牢。7.4 问题四超时取消任务重复执行现象订单明明已经取消了日志里发现取消逻辑跑了两遍报了一次“订单状态更新失败”。原因上一次定时任务还没跑完下一次又触发了同一个订单被两个任务实例同时处理。解决方案最简单的方案是在更新 SQL 里加上状态条件只有“状态待支付”的订单才能被修改为已取消这样谁先更新成功另一个自然影响行数为 0不会破坏数据。UPDATE orders SET status 3 WHERE id #{id} AND status 0这种“乐观状态校验”的方式在大多数业务场景下比引入分布式锁更务实。7.5 问题五前端跨域但是后端配置了 CORS 还是报错现象后端已经加了 CORS 配置前端发起请求仍然提示跨域。原因如果通过 Spring Security 拦截了请求跨域配置必须配合 Security 的过滤器链才能生效。本项目没有用 Spring Security所以只需要在 WebMvcConfigurer 配置即可。但如果后续谁把 Spring Security 加进来记住要在 SecurityConfig 中也允许对应跨域配置。7.6 问题六用户表同一密码不同用户查询超时这个其实是我做性能优化时遇到的。用户量少索引不明显但数据量一旦到几十万没加索引的查询就是全表扫描慢是一定的。后来我给 user 表加了唯一索引username查询瞬间从 500ms 降到 1ms。我的建议是开发阶段没数据看不出问题但表结构一建立就必须把索引想清楚否则后面补索引时已经积累了一堆脏数据迁移成本更高。8. 一点个人经验体会这个影城管理系统开发完我最大的体会是一个“完整”的管理系统难点从来不在 CRUD 本身而在于那些业务规则和边界条件。选座的并发控制、排片的时间冲突校验、超时订单的释放这些才是项目真正有价值的地方也是值得在面试里拿出来讲的东西。另外我还想强调一下源码的整洁性。很多初学者把项目做完代码堆在一起命名混乱注释缺失。这个项目我自己回头去看凡是偷懒的地方比如一个 Mapper 里放了几十个 SQL、Controller 里写业务逻辑后期维护时都想骂自己。代码不是写完就结束了它是给人看的。命名清晰、分层合理、逻辑内聚这个习惯越早养成越好。如果你正准备做类似的系统我的建议是先花两天时间把需求和数据表设计想透彻再动手写代码。开发中遇到问题不要怕用日志和断点去定位而不是瞎猜。做完一个项目你会发现在这个过程中踩过的坑比看十本书都有用。