ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue电影院订票系统:从架构拆解到本地跑通全攻略

SpringBoot+Vue电影院订票系统:从架构拆解到本地跑通全攻略 简介基于SpringBoot与Vue的前后端分离电影院订票系统源代码包主要面向Java全栈学习者、毕业设计与课程设计人员可帮助梳理影片管理、在线选座、订单生成、用户登录等核心流程。压缩包有约300个文件大小7.95MB文件类型包括82个Java后端源码、41个Vue前端组件、17个JavaScript交互脚本、SQL数据库脚本及图片资源目录划分明确便于定位业务模块。系统以SpringBoot 2.4.0和MySQL 8.0.19为后端支撑采用Vue构建前端体现出前后端分离、响应式与模块化设计特点。已有77人浏览学习。资源内含完整源代码、数据库初始化脚本及使用说明覆盖Maven导入、数据库配置、npm安装等关键步骤适合在此基础上快速跑通项目并扩展影院排片、会员管理等业务。1. 前后端分离的电影院订票系统为什么值得你花一个晚上跑通SpringBootVue做出来的电影院订票系统几乎是国内Java后端面试题和毕设选题里出现频率最高的组合之一。这套东西的业务不复杂——用户注册登录、查电影、选场次、选座下单、看订单但麻雀虽小五脏俱全JWT鉴权、跨域配置、数据库关联查询、前端路由守卫一个不少。真正动手跑过一遍的人都知道最大的成本不是写业务代码而是把环境和依赖理顺。很多读者第一次拿到这类含源代码和数据库的项目时卡在启动环节的占了一半。我这里不讲那种「先搭框架再写代码」的教程式流程而是按你拿到手一个现成的订票系统源码时最需要的顺序来讲架构怎么拆、数据库怎么导、配置怎么改、业务链路怎么走、遇到报错怎么处理。适合两类人一是准备毕设答辩的学生想把项目的每个模块讲清楚二是想快速复刻一个前后端分离项目做练手的开发者。2. 架构拆解SpringBoot 后端、Vue 前端与数据库三者之间的契约2.1 后端 SpringBoot控制器、服务、Mapper 三层各管什么事大部分电影院订票系统的后端都遵循经典的三层结构。控制器层只负责接收HTTP请求、做参数校验、返回统一响应体服务层承载业务规则比如下单时校验场次余票、计算订单总价Mapper层配合MyBatis或MyBatis-Plus负责和 MySQL 数据库交互。这套分层的好处是前端要接口文档后端要写单元测试两边都可以对着同一套契约开发。以「查询电影列表」为例后端接口的工作流是Vue前端发起GET请求到/api/movie/listSpringBoot的控制器接收后调用服务层的MovieService.listMovies(page, size, keyword)服务层再通过MyBatis-Plus的分页插件去查movie表最后把结果包装成ResultListMovie返回。前端拿到的JSON结构通常是这样{ code: 200, message: ok, data: { records: [ { id: 1, title: 流浪地球3, poster: /upload/poster-1.jpg, duration: 173, releaseDate: 2025-01-22 } ], total: 20, current: 1, size: 8 } }这里的code、message、data是前后端约定的统一响应格式data里的records、total、current、size是分页查询的标准字段。拿到这个结构前端分页组件才能直接绑定current和size翻页时不需要再解析额外的嵌套对象。我一般会在Result类里提供success()和error(String msg)两个静态工厂方法所有控制器统一返回它避免有的接口返回Map、有的返回裸对象前端Axios拦截器写起来会非常难受。2.2 前端 Vue路由、状态管理、组件库怎么分工Vue 侧的核心模块就那么几个vue-router负责页面跳转与路由鉴权一个登录拦截器判断token存不存在状态管理Vuex 或 Pinia存用户信息和购物车临时数据视图层用 Element UI 或 Element Plus 的表格、弹窗、表单组件快速搭页面。订票系统最常见的页面路由有五个首页电影列表、电影详情、选座下单、登录注册、个人中心散落在views目录下的.vue文件里。路由配置里值得留意的是一段前置守卫代码router.beforeEach((to, from, next) { const token localStorage.getItem(token); const whitelist [/login, /register, /home]; if (!token !whitelist.includes(to.path)) { next(/login); } else { next(); } });这段逻辑的含义是白名单/login、/register、/home不需要登录就能访问其余地址比如/order、/profile如果发现本地没有token强制跳回登录页。注意localStorage存储token只是毕设和中小企业项目最常见的做法实际生产环境更推荐用HttpOnlyCookie 来降低 XSS 风险但在前后端分离且端口不同的本地开发环境下Cookie 需要额外的跨域属性配置用localStorage简单直接这也是多数现成源码的选择。2.3 数据库设计电影、场次、订单、用户四张核心表的关联关系电影院订票系统的数据模型不大核心表通常只有四到五张。movie表存电影基本信息session表有的项目叫schedule建议避开这个单词因为它在部分数据库里和定时任务语义冲突存某部电影在某个影厅的开场时间与票价orders表存订单主数据order_ticket或ticket_seat表存具体选中的座位sys_user表存账号密码。最常用的表关系是一部电影有多条场次即movie与session是一对多一个订单属于某个用户且对应一个场次即orders同时关联sys_user和session一个订单里有多个座位即orders与ticket_seat是一对多。画一张简化版的建表 SQL 会更直观CREATE TABLE movie ( id int NOT NULL AUTO_INCREMENT, title varchar(50) NOT NULL, poster varchar(255) DEFAULT NULL, duration int DEFAULT NULL COMMENT 时长单位分钟, release_date date DEFAULT NULL, summary text, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE session ( id int NOT NULL AUTO_INCREMENT, movie_id int NOT NULL, hall varchar(20) DEFAULT NULL COMMENT 影厅名称, start_time datetime NOT NULL, end_time datetime DEFAULT NULL, price decimal(10,2) DEFAULT NULL, PRIMARY KEY (id), KEY idx_movie_id (movie_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL, session_id int NOT NULL, order_no varchar(32) NOT NULL COMMENT 订单号, total_price decimal(10,2) DEFAULT NULL, status tinyint DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表语句里有两个容易被忽略的设计第一session表单独建了idx_movie_id索引因为按电影Id查场次是最高频的查询路径第二orders.order_no建了唯一索引这是为了保证同一个订单号不会被重复插入同时作为支付回调时的查询条件。座位表的设计则要留意一个边界情况同一场次同一座位不能被两个人选中所以order_ticket表的(session_id, seat_row, seat_col)需要联合唯一约束。关于数据库字符集所有表统一使用utf8mb4而不是utf8因为电影简介和影厅名称里偶尔会出现 emoji 字符utf8在 MySQL 中存不了四个字节的 emoji插入数据时会报Incorrect string value错误。这个属于典型的踩坑点很多新手拿到 SQL 脚本导入后才发现订单备注带个表情就崩了。3. 从仓库到浏览器在本地跑通订票系统的完整流程3.1 环境准备JDK、Node、MySQL 的版本匹配拿到项目源码后第一步不是急着改代码而是确认本机环境跟项目依赖的版本在同一代际。常见约定的配置是JDK 8 配 SpringBoot 2.xJDK 17 配 SpringBoot 3.xVue 2 配 Node 14/16Vue 3 配 Node 16/18MySQL 5.7 和 MySQL 8.x 都能跑但 JDBC 驱动的配置有差异。如果你不清楚手里的源码是哪个版本先看pom.xml里spring-boot-starter-parent的版本号再看前端package.json里的vue字段这两个文件决定后面所有命令是否顺畅。版本不匹配最常见的翻车现场是SpringBoot 3.x 用了javax.servlet前缀的老项目依赖启动直接报ClassNotFoundException: javax.servlet.Filter。解决方式要么把项目降回 SpringBoot 2.7要么把依赖全局替换成jakarta.servlet前缀。为了避免这种折腾我的习惯是装项目前先看根目录的README或者使用说明.txt这些文件里如果有环境要求章节通常写的是作者跑通时的准确版本。3.2 创建数据库并导入初始数据SQL 脚本的两个导入姿势数据库环节的常规做法是用 Navicat、DataGrip 或命令行新建一个名为cinema_db的库然后导入项目sql目录下的初始化脚本。脚本一般包含建表语句和INSERT INTO种子数据电影、场次、管理员账号都在里面。命令行导入方式如下mysql -uroot -p123456 --default-character-setutf8mb4 cinema_db.sql说明一下这里的关键参数--default-character-setutf8mb4是为了防止控制台默认字符集把中文乱码写进表里-uroot -p123456是用户名密码实际使用要换成你本地的账号。如果脚本是用 Navicat 导出的文件头部会带DROP TABLE IF EXISTS语句重复导入不会报错但如果脚本没有写DROP第二次导入就会报Table movie already exists这时候要先手动DROP DATABASE cinema_db;再重新CREATE DATABASE cinema_db DEFAULT CHARACTER SET utf8mb4;。导入完成后验证一下数据量最稳妥USE cinema_db; SELECT COUNT(*) FROM movie; SELECT COUNT(*) FROM session; SELECT COUNT(*) FROM sys_user;每条命令的含义就不过多展开了主要确认三张核心表里有种子数据。这里要特别提醒sys_user表里的密码字段如果是明文或 MD5 值能正常登录如果是 BCrypt 密文而项目里登录逻辑用MD5Utils去做校验那肯定登录不上。这种情况不需要改动代码直接去数据库把密码改成项目加密方式对应的值或者找到项目里预设的初始账号密码比如admin / 123456。3.3 配置 application.yml端口、数据库连接、JWT 密钥后端的配置文件在src/main/resources/application.yml旧项目可能是application.properties。需要改的通常只有三处server.port、spring.datasource.url、spring.datasource.password。一个典型的配置长这样server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cinema_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse 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 global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: cinema-secret-key-please-change expire-hours: 24这段配置里最有价值的是两个细节。第一serverTimezoneAsia/Shanghai必须加在 JDBC 链接上MySQL 5.7 不配置可能没问题但 MySQL 8.x 不配置时时间插入和查询会相差 8 个小时表现形式就是你下单后查订单create_time对不上。第二jwt.secret是要替换成自己的长字符串的如果你直接跑通源码后不改它别人把你的项目打包部署后用同一个默认密钥就能伪造 token这是一个安全后门级别的问题。3.4 启动后端与前端两行命令背后的依赖安装与代理转发后端启动比较直接在项目根目录执行下面的命令即可。如果你用 IDEA直接运行src/main/java下的启动类类名通常带Application后缀也一样命令行的好处是不依赖 IDE 环境。mvn clean package -DskipTests java -jar target/cinema-backend-0.0.1-SNAPSHOT.jar-DskipTests不执行测试用例避免因为缺少测试环境而构建失败。如果本机 Maven 没配过国内镜像mvn package可能会卡在依赖下载上这个见后面避坑章节。启动成功后控制台出现Tomcat started on port(s): 8080后端就绪。前端要两步先装依赖再启动开发服务器npm install npm run servenpm install装的是package.json里声明的依赖如果报错优先检查 Node 版本与node-sass的兼容性npm run serve启动的是 Webpack Dev Server 代理服务。前端页面跑在localhost:8081后端跑在localhost:8080为什么前端 8081 能直接请求后端接口而不报跨域答案在vue.config.jsmodule.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: /api } } } } };这里pathRewrite的作用是什么如果你的后端接口路径统一以/api开头那么^/api: /api等于什么都不改前端请求/api/movie/list就原样转发给后端。有些项目后端接口没有/api前缀那这里就会改成target: http://localhost:8080并且pathRewrite: { ^/api: }把/api剥掉再转发。这个配置属于前后端分离项目实战里最常见的分水岭——配置对了联调没有一丝跨域报错配错了前端日志里全是Access-Control-Allow-Origin的红字。4. 核心订票链路走读查电影、选场次、锁座下单4.1 电影列表与搜索接口响应结构怎么定才方便前端调用后端把电影列表接口做成了支持分页和关键字的通用查询如下GetMapping(/movie/list) public ResultIPageMovie listMovie( RequestParam(defaultValue 1) Integer current, RequestParam(defaultValue 8) Integer size, RequestParam(required false) String keyword) { LambdaQueryWrapperMovie wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Movie::getTitle, keyword) .orderByDesc(Movie::getReleaseDate); IPageMovie page movieMapper.selectPage(new Page(current, size), wrapper); return Result.success(page); }参数含义拆开讲current是当前页码从 1 开始默认 1size是每页条数默认 8keyword是可选参数不传时整个方法退化成「按上映日期倒序取第一页」。这段代码中最值得学的是LambdaQueryWrapper的写法——like(条件, 字段, 值)里的第一个布尔参数为false时MyBatis-Plus 会自动忽略这个查询条件所以前端传不传keyword都没关系不需要写if-else分支。这也是数据库增删改查场景里最常用的套路。前端的列表页拿到这个响应后直接把data交给表格或卡片组件渲染。翻页时监听分页组件的current-change事件重新请求接口即可。注意关键字搜索不要每次都触发全量刷新我一般会在输入框上做 300 毫秒的debounce防抖避免每敲一个字就往后端打一次请求这在 MySQL 表数据量变大后能明显降低数据库压力。4.2 选座页与场次余票前端座位图的组件化实现选座是整个电影院订票系统交互最重的模块。前端座位图通常用一个二维数组表示seatMap[row][col]0表示可选1表示已售2表示当前选中。后端提供的接口是「查场次座位状态」GetMapping(/session/{sessionId}/seats) public ResultListString getSeatStatus(PathVariable Long sessionId) { // 返回格式1_3, 2_5表示第1排第3座、第2排第5座已被占 ListString soldSeats ticketSeatMapper.selectSoldSeats(sessionId); return Result.success(soldSeats); }为什么后端只返回已售座位的坐标而不是整个二维数组这算一个很小的设计决策前端座位图的行列数是固定的比如 8 排 12 座后端只返回[1_3, 2_5]这类被占坐标前端初始化时把所有格子设为可选再把返回的坐标标记为已售接口的响应体积是 O(已售数量)而不是 O(总座位数)。如果影厅扩容到 20 排 20 座接口返回不会有任何变化。前端拿到值后用v-for渲染座位格子的核心逻辑如下template div v-for(row, rowIdx) in seatRows :keyrowIdx classseat-row div v-for(col, colIdx) in row :keycolIdx classseat :class{ sold: soldSet.has(rowIdx _ colIdx), selected: selectedSet.has(rowIdx _ colIdx) } clicktoggleSeat(rowIdx, colIdx) /div /div /template script export default { data() { return { seatRows: Array.from({ length: 8 }, () Array(12).fill(0)), soldSet: new Set(), selectedSet: new Set() }; }, methods: { async loadSeats(sessionId) { const { data } await this.$http.get(/session/${sessionId}/seats); data.forEach(pos this.soldSet.add(pos)); }, toggleSeat(row, col) { const key row _ col; if (this.soldSet.has(key)) return; if (this.selectedSet.has(key)) { this.selectedSet.delete(key); } else { if (this.selectedSet.size 6) { this.$message.warning(单笔订单最多选择6个座位); return; } this.selectedSet.add(key); } } } }; /script这里有一个交互边界选座时用Set保存选中状态点击已售座位直接return不做任何操作。单笔订单上限是 6 个座位——这是电影院业务里常见的限制目的是防止黄牛一次锁掉整排座位。前端做限制后后端下单接口也要做同样的校验不能只靠前端兜底因为用 Postman 绕过前端直接下单时后端必须有独立的判断逻辑。4.3 订单提交与支付模拟状态机的流转逻辑下单接口是整条链路的关键它的校验逻辑必须依次判断接口参数场次Id、座位坐标列表、用户Id是否为空 → 用户是否已登录 → 场次是否存在 → 座位是否被占用 → 创建订单记录 → 更新座位状态。后端实现大致如下PostMapping(/order/create) public ResultOrderVO createOrder(RequestBody OrderCreateDTO dto, RequestHeader(Authorization) String token) { Integer userId jwtUtil.getUserIdFromToken(token.replace(Bearer , )); SessionItem session sessionMapper.selectById(dto.getSessionId()); if (session null) { return Result.error(场次不存在或已下架); } ListString seats dto.getSeats(); // 校验座位是否已被售出 ListString sold ticketSeatMapper.selectSoldSeats(dto.getSessionId()); for (String seat : seats) { if (sold.contains(seat)) { return Result.error(座位 seat 已被购买); } } // 生成订单号并计算总价 String orderNo CZ System.currentTimeMillis() RandomUtil.randomNumbers(4); BigDecimal total session.getPrice().multiply(BigDecimal.valueOf(seats.size())); Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setSessionId(session.getId()); order.setTotalPrice(total); order.setStatus(0); orderMapper.insert(order); // 批量写入座位 for (String seat : seats) { String[] rc seat.split(_); TicketSeat ts new TicketSeat(); ts.setOrderId(order.getId()); ts.setSessionId(session.getId()); ts.setSeatRow(Integer.parseInt(rc[0])); ts.setSeatCol(Integer.parseInt(rc[1])); ticketSeatMapper.insert(ts); } return Result.success(new OrderVO(order.getId(), orderNo, total, 0)); }这段代码有几个细节值得反复看。第一JWT的取值从请求头Authorization拿到后必须去掉Bearer前缀再做解析否则parseToken会解析失败第二订单号和数据库插入用的是System.currentTimeMillis()加四位随机数并发量不大时够用但如果要严谨一点应该用雪花算法或数据库序列第三座位写入没有包事务。如果第 10 个座位插入失败前面 9 个座位已经写进去了用户什么都没买成座位却被占了。修复方式是在方法上标注Transactional并让RuntimeException触发回滚——这是任何一个订票系统最不该漏掉的动作。5. 避坑指南前后端分离项目最容易翻车的五个位置5.1 跨域报错明明配置了 proxy刷新页面后还是 401现象前端登录成功跳转首页后接口正常返回数据按 F5 刷新页面再次请求携带Authorization头直接 401 未授权。原因刷新页面后Vue 应用重新初始化但localStorage里的 token 仍然存在此时浏览器携带的是同一个 token。问题通常不是 token 失效而是后端某个接口没有走代理前缀。排查方法很简单——打开浏览器 DevTools 的 Network 面板看请求地址是localhost:8081/api/...还是localhost:8080/api/...。如果是绝对地址http://localhost:8080说明代码里某处用了axios.defaults.baseURL http://localhost:8080绕过了vue.config.js的代理那跨域报错是必然的。解决统一在vue.config.js里做代理转发前端代码里所有请求写成相对路径/api/xxx不在axios配置里写死后端地址。如果你用的 Vite 而非 Vue CLI对应配置放在vite.config.js的server.proxy字段下原理一致。这个坑多数时候不是环境问题而是代码里混用了两种请求地址规范。5.2 MySQL 8 的时区报错启动能过查询时间差 8 小时现象后端启动不报错但前端展示的订单创建时间比实际时间早了 8 小时或者日志里出现The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL 8.x 默认时区是服务器系统时区而 JDBC 连接串里没写serverTimezone驱动无法识别当前时区。早期 MySQL 5.7 对时区不敏感serverTimezone缺失时默默使用服务器本地时区所以很多老项目的配置直接挪到 MySQL 8 上就会踩雷。解决在application.yml里给 JDBC URL 补上serverTimezoneAsia/ShanghaiuseSSLfalse。如果补了还是差 8 小时检查一下是不是在虚拟机上部署的 MySQL宿主机和虚拟机时间不同步执行SELECT NOW();查看数据库当前时间即可确认。这个问题的排除顺序是先查数据库本地时间再查 JDBC 连接参数最后才是代码里的Jackson时间格式配置。5.3 Maven 依赖下载不下来卡在 Last-Moved 或者超时现象执行mvn clean package卡在某个依赖下载进度条长时间不动最后报Could not transfer artifact xxx from/to central。原因Maven 默认中央仓库对国内网络不太友好尤其是一次性拉取 SpringBoot 全家桶时几十 MB 的依赖包可能是从海外源下载的。这不是代码问题是网络环境问题。解决在~/.m2/settings.xml里配置阿里云镜像源mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror改完配置后清掉本地仓库里下载失败的残留文件mvn clean不会自动清需要删除~/.m2/repository里对应的*.lastUpdated文件再重新执行构建。如果用的是 IDEA 自带的 Maven 设置记得确认settings.xml被正确引用可以在mvn -v输出里看到当前生效的 settings 文件路径。5.4 前端 npm install 报 node-sass 编译失败现象npm install执行到一大半报错信息类似gyp ERR! build error或者直接提示node-sass无法下载binding.node。原因node-sass是需要在安装时从 GitHub 下载预编译二进制文件的Node 版本改了之后老的node-sass版本找不到匹配的二进制包就会现场编译而本机如果没有 Python 环境和 C 构建工具链编译必然失败。这是 Vue 2 老项目高频翻车的位置。解决如果项目没有锁定依赖树直接把node-sass替换成sassDart Sass。修改package.json里的node-sass字段为sass然后把样式代码里import改成use或者改回deep选择器写法构建命令照旧。如果项目必须保留node-sass那就降低 Node 版本——用nvm install 16再nvm use 16然后删除node_modules和package-lock.json重新npm install。顺便说一句前端项目源码发给别人的时候node_modules目录普遍是不打包的拿到手执行npm install能否成功完全取决于 Node 版本和原项目依赖的兼容性这属于一种没得商量的玄学。5.5 座位下单成功但订单列表查不到事务与逻辑删除的联合坑现象用户下单成功支付状态也更新了但个人中心「我的订单」列表里查不到这笔订单。原因这个问题的排查路径一般不短。最常见的原因是后端做了逻辑删除设计orders表有一个deleted字段查询订单列表的 SQL 里默认加了del_flag 0条件而下单写入时这个字段没有被赋值默认值是1或者NULL导致订单「存在但被标记为已删除」。解决在插入订单时显式设置deleted 0或者把数据库表字段默认值改成0。同时检查 MyBatis-Plus 的TableLogic注解是否作用在orders实体类的deleted字段上。如果实体类里没有这个注解网关层的逻辑删除配置不会自动生效查询就会变成全量扫描表现就不是「查不到」而是「把已取消的订单也查出来」。这里的排查顺序建议是先在数据库里SELECT * FROM orders WHERE order_no 你的订单号确认落库数据存在再回头查查询 SQL 的条件拼接。6. 答辩与上线前接口测试、功能扩展与打包清单后端服务起来以后先别急着在页面上点来点去用 Postman 或 Apifox 给核心接口做一轮冒烟测试是最省时间的做法。测试顺序按业务链路走注册 → 登录 → 获取 token → 查询电影列表 → 查场次 → 查座位 → 下单 → 查订单。每成功一步把响应里的关键字段记录下来尤其是orderNo和totalPrice后面扩展支付模块时用得上。分页接口的测试要额外覆盖「边界参数」page传负数、size传0、keyword传超长字符串。这三个参数如果后端没有做参数校验轻则返回空列表重则触发 SQL 拼接异常。处理方式是在Controller层加Min(1)和Max(50)校验注解而不是把判断逻辑塞进 Service——参数校验属于接口层职责这个边界保持清晰后面加参数时不容易漏。如果你想在这个订票系统上做二次开发最值得做的方向是「观影人管理」和「支付回调模拟」。观影人管理本质是新增一张contact表字段包括姓名、身份证号、手机号下单时前端多一步选择观影人后端校验身份证号格式。支付回调模拟则是把一个本来只在课外练手的小项目往真实业务里拉近一步的操作——在支付成功接口里加一个mock_paid的状态机流转配合订单状态的定时扫描能让你在答辩时讲清楚「订单超时未支付自动取消」的逻辑这比把功能堆得多要加分。最后说一句打包。后端部署用mvn clean package -DskipTests打出 jar 包前端部署用npm run build生成dist目录后交给 Nginx 托管静态文件并配置/api和/upload的proxy_pass指向后端地址。上生产前把application.yml里的jwt.secret、数据库密码和spring.profiles.active全改成独立环境的值日志级别从debug调回info——这些是我自己在部署这类系统时反复检查的固定动作算是吃了好几次亏换来的习惯。希望帮你少走一段弯路。本文还有配套的精品资源点击获取
返回列表