SSM+Vue旅游景点售票系统毕设全攻略:从数据库设计到并发扣减
又到毕设季“ssmvue旅游景点售票系统”这个题目几乎是计算机类专业里最稳的选择之一。别看名字平淡它把后端 SSM、前端 Vue、数据库设计、论文写作全串在一条线上对要毕业设计的同学来说是投入产出比很高的项目。它能解决的是景区线上购票、订单管理、后台统计这些真实需求演示和答辩也方便。无论你是刚学完 Java 的本科生还是想找个中型项目练手的专科生拿这个题目起步都不亏。这篇文章我就从头到尾拆一下选题思路、技术选型、数据库怎么建、论文和程序怎么对应、哪些地方容易踩坑尽量给你一条能直接照做的路线。我见过很多同学一上来就找代码改个名字就去答辩了结果流程一问就懵。实际上这个系统真正能拿得出手的点不在于页面多炫而在于你有没有把订单、库存、票种、支付这堆逻辑理清楚。所以这篇不会只贴代码更多是讲清楚“为什么这么设计”以及我在实际跑通项目时碰到的问题和解决办法。1. 项目整体设计与技术选型思路1.1 为什么选 SSMVue而不是 Spring Boot 前后端不分离先说结论SSMVue 不是最新的技术组合但它是毕设场景里最不容易翻车的一套。SSM 指的是 Spring SpringMVC MyBatis答辩老师对这个框架非常熟悉论文里的“三层架构”“控制反转”“依赖注入”这些理论点都能找到对应的代码位置写起来也顺手。Vue 负责做前端页面展示效果好路由跳转、组件复用、异步请求这些东西也能单独拿出一章来写论文内容瞬间就充实了。有人会问现在企业都用 Spring Boot 了为什么不用 Spring Boot说实话用 Spring Boot 也行甚至更简单但毕设题目已经写死是“ssmvue”那就没必要改。而且 SSM 的 XML 配置虽然繁琐恰恰能用来讲清楚 Spring 的 Bean 管理和 MyBatis 的映射机制遇到运行问题翻起来更有底。Spring Boot 把一切都自动配置好了反而容易让论文显得“干瘪”。再说前后端分离的问题。传统 JSP 项目需要 Tomcat 同时渲染页面前端代码和后端代码缠在一起改个样式都要重启。SSM 只做后端接口Vue 单独跑在 Node 开发服务器上前端用 Axios 请求后端数据解决了“后端同学改完页面把 Java 文件弄乱”的问题。这个分离结构在答辩时也更容易演示前端一套工程后端一套工程各自独立启动专业感立刻就有了。1.2 系统核心功能模块拆解一个能上台面的旅游景点售票系统至少要分成游客端和管理端两条线。游客端的功能是“看得见、买得到、查得到”注册登录、景点列表、景点详情、票种选择、下单支付、我的订单、取消订单。管理端的功能是“管得住”景点信息维护、票种管理、余票调整、订单查询、退款审核、用户管理、公告发布、数据统计。我建议把“公告 轮播图 宣传视频”也纳入功能里不是为了花哨而是论文里的“系统管理”模块需要这些内容来撑起页面截图。特别是景区宣传视频可以用 Vue 播放 m3u8 流媒体这个小功能在演示时会让人眼前一亮后面我会专门讲怎么实现。模块拆解时要注意职责边界。比如“余票管理”到底是属于景点模块还是票种模块我的做法是票种表里带一个总库存字段另外再单独建一张“每日库存表”来管理某一天某个票种还剩多少张这样日票、周票、节假日票都能灵活处理。订单模块只管记录用户买了哪个票种、买了多少张、金额多少不直接改景点表库存库存扣减放在票种或每日库存表上避免两个模块乱改同一张表。1.3 数据库设计关键点数据库是论文里最容易被评阅老师盯住的部分我强烈建议动手写代码前先把表结构画清楚。核心表大概这样用户表 sys_userid、用户名、密码用 MD5 加密、手机号、角色管理员/普通用户、创建时间。景点表 attractionid、景点名称、简介、封面图、详细内容、所在城市、开放时间。票种表 ticket_typeid、景点 id、票种名称成人票、学生票、价格、总库存。每日库存表 daily_stockid、票种 id、销售日期、剩余数量、版本号。订单表 ordersid、订单号、用户 id、总金额、支付状态、下单时间、支付时间。订单明细表 order_itemid、订单 id、票种 id、票价快照、数量、小计。公告表 noticeid、标题、内容、发布时间。这里有个细节订单明细一定要保存“票价快照”不能下单后再去关联票种表价格。因为管理员可能后来调价但历史订单应该按购买时的价格算。这个细节写进论文里评阅老师会认为你考虑了业务状态的一致性问题。每日库存表加一个 version 字段是给乐观锁用的。虽然毕设系统没有高并发但答辩时老师如果问“几个人同时买最后一张票怎么办”你得能说出方案。用UPDATE daily_stock SET remaining remaining - #{num}, version version 1 WHERE id #{id} AND remaining #{num}这种方式一条 SQL 就能保证不超卖。2. 从论文到程序的落地顺序2.1 论文大纲与程序模块的映射很多同学把论文和程序分开做最后写到“系统实现”才发现功能对不上。我的经验是论文的章节顺序就是程序的开发顺序。提纲大致是第一章绪论写选题背景和国内外现状第二章需求分析画用例图、用例说明第三章概要设计画系统架构图和数据库 E-R 图第四章详细设计写各模块的时序图和类图第五章系统实现贴关键代码和截图第六章测试写功能测试和部分性能测试。实际操作时先做需求分析和数据库设计然后照着 E-R 图建表。表建好后后端的 Mapper 层就有原型了接口定义清楚了前端 Vue 页面就能照着一行行写。等你代码写到一半再回头补论文里的时序图和模块说明基本上每个类都有自己的位置不会出现“代码里有的接口论文里没写”的尴尬。有一点要提醒论文里的类图、时序图不要画得太抽象。比如“用户购票时序图”就是从 Vue 页面发送请求到 Controller 接收Service 处理库存和订单Mapper 操作数据库最后返回结果。这种图画出来老师一眼就知道你真做过不是从网上抄的。2.2 后端 SSM 分层实现要点SSM 的分层是 Controller → Service → Mapper这个结构几乎是固定的。Controller 只管参数接收和结果返回Service 负责业务逻辑和事务Mapper 负责数据库操作。对应的包名我用的是 controller、service、mapper、entity、common简单明了。一个容易踩的坑是把业务逻辑写在 Controller 里。比如下单时先判断库存再生成订单这些逻辑如果堆在 Controller方法会变得很长而且事务不好控制。正确的做法是 Controller 里只写十来行调用 Service 的一个createOrder(OrderParam param)方法所有校验、扣库存、生成订单都放进 Service并加上Transactional。我见过同学把库存扣减直接写在 Mapper 的 update 方法里结果库存扣了订单表没写进去这种问题只能靠 Service 层的事务解决。MyBatis 的 XML 映射文件里尽量避免select *只查需要的字段。尤其关联查询时把列别名写清楚。比如订单列表要显示景点名称我会先在 orders 表冗余一个attraction_name字段而不是每次都去关联景点表。毕设数据量小关联查询无所谓但冗余字段在答辩时可以作为“性能优化”的点拿出来讲。2.3 前端 Vue 页面与路由设计含 Vue 安装与环境配置前端我用 Vue CLI 或者 Vite 搭建工程新手我更推荐 Vue CLI因为图形化界面不容易出错。环境配置的第一步是装 Node.js建议用 16 以上版本版本太低会导致依赖安装时报一串错。然后用npm install -g vue/cli装脚手架vue create travel-ticket-web创建项目选 Router 和 VuexCSS 预处理器看个人习惯我选的是 SCSS。如果你的项目用了 TypeScript 模板启动时可能碰到一个很经典的问题failed to load tsconfig vue/tsconfig/tsconfig.web.json: tsconfig not found。这个错误通常是项目里引用了vue/tsconfig但依赖没有装全。解决办法是执行npm install --save-dev vue/tsconfig装完后重启npm run serve就能过。如果你不想在这个地方耗时间创建项目时直接选 JavaScript 版本毕设完全够用省心很多。vue-router 是前端导航的骨架。游客端我设计了/home、/attraction/:id景点详情、/order/create下单、/order/list我的订单这几个路由。这里有一个很实用的技巧/attraction/:id这种带参数的路由在页面里通过this.$route.params.id取参数然后调用后端详情接口。动态路由则可以用来做权限控制比如管理员路由/admin在登录后动态添加到路由表里普通用户点击后直接 404比单纯靠按钮隐藏更安全。Vue 里的插槽在页面复用上是真好用。比如景点卡片组件AttractionCard.vue默认显示图片和名称但我想在管理端的列表里额外显示“编辑”“删除”按钮就可以在组件内部留一个slot/slot外层传什么按钮它就渲染什么按钮。这样游客端和管理端共用一张卡片不用写两套代码。2.4 前后端联调与接口约定前后端分离往后最烦的就是联调。为了不让自己疯掉后端所有接口统一返回一个Result对象结构是{ code: 200, msg: success, data: {} }。前端封装一个 Axios 实例配置baseURL和拦截器。拦截器里读取 localStorage 中的 token每次请求自动加到Authorization头。后端用一个拦截器校验 token但登录接口、注册接口、景点列表这些公开接口要放行。接口路径我也建议从一开始就规范化/api/user/login、/api/attraction/list、/api/order/create、/api/order/list。这样论文里写接口设计表时可以直接搬过去。联调时的跨域问题开发环境有两个解决方案。一个是在 Vue 的vue.config.js里配置devServer.proxy把/api代理到后端http://localhost:8080这样浏览器觉得请求是同源的不会报跨越。另一个是后端写一个CorsFilter允许指定域名我两个都做保险。这里有个经验如果你用代理后端就不要开跨域如果你用 token 且设置了Session就要注意Cookie跨域会失效所以尽量用 token 不用 Session。3. 核心环节的实现细节3.1 景点与票务库存管理景点管理的核心是“一个景点下有多个票种每个票种还要分日期管理库存”。比如欢乐谷白天的成人票今天剩 500 张明天剩 300 张你不能用一个总库存字段去顶。所以我在实现时用了ticket_type表存票价和总库存用daily_stock表存“某天剩余数量”。后端新增票种时默认生成一个无限库存标记管理端也可以设置一个“可销售日期范围”然后提前把每天的库存初始化成总量。这个初始化逻辑可以写一个简单的定时任务每晚会把第二天的库存补上。但定时任务对很多同学来说有点复杂所以也可以退一步购票查询时如果daily_stock里没有对应日期的记录就自动创建一条并设置剩余量为总库存。管理端修改库存时我会记录修改人并在订单表里留下日志。答辩老师问“这个库存是实时更新的吗”你可以回答前台下单走的是乐观锁扣减管理端调整余票走的是单独的更新接口两者都通过数据库事务保证一致。这里不要吹自己做得多复杂诚实说明是基础方案就行。3.2 订单生成与库存扣减含并发处理订单生成是整个系统最关键的部分我写的时候把步骤拆得很清晰校验用户是否登录参数是否合法票种是否存在。生成唯一订单号格式类似202606011230001234含义是年月日时分秒加四位随机数。调用库存扣减 SQL执行UPDATE daily_stock SET remaining remaining - #{num} WHERE ticket_type_id #{ticketTypeId} AND sales_date #{salesDate} AND remaining #{num}。如果影响行数为 0说明库存不够直接抛异常回滚。保存订单主表和订单明细表两张表在一个事务里。返回订单号给前端前端跳转到支付模拟页面。这里涉及一个最常见的面试问题为什么用数据库扣减而不是在 Java 代码里先查库存再判断因为在代码里先查后判是非原子操作两个人同时查到剩余 1 张都判断能买最后肯定超卖。而数据库的UPDATE ... WHERE remaining #{num}是原子操作数据库自己会锁行影响行数为 0 时就不会成功这就是不超卖的关键。订单状态我用一个status字段表示0 未支付、1 已支付、2 已使用、3 已取消。支付模拟就直接在订单详情页放一个“模拟支付完成”的按钮调用后端/api/order/pay接口把状态改成已支付。你可以在后端写死一个超时时间比如 15 分钟不支付自动取消这个靠定时器扫表就行不扫也不影响毕设演示但论文里可以写“预留定时任务实现自动取消”。3.3 购票流程与二维码/验票场景购票流程演示时我建议把“下单支付后生成二维码”这个环节加进去因为它能讲的东西很多。前端拿到订单号后可以用一个二维码组件把订单号和订单状态编码成二维码字符串。这个字符串不需要额外存储只需要在“检票”时能解析出来就行。后端负责验票的逻辑是这样的手机扫码后前端解析出订单号调用后端/api/order/verify接口后端根据订单号查询订单状态。如果是“1 已支付”就把状态改成“2 已使用”返回验票成功如果已经是“2 已使用”返回“该票已使用”如果是“未支付”或“已取消”返回无效。这个流程状态机很清晰写在论文里和时序图里都好看。这里有一个很容易忽略的点生成二维码时不要把用户 id 或密码等敏感信息编进去只放订单号。因为订单号本身就是唯一标识不需要别的信息。而且在验票逻辑里还要判断这个订单是不是属于当前景点防止把欢乐谷的票拿到长隆去刷。最简单的做法是在二维码编码串里再加一个attractionId字段验票时比对。3.4 景区宣传视频与页面展示Vue 播放 m3u8景区详情页想放一段宣传视频这是很常见的需求。但浏览器原生video标签不支持播放 m3u8 格式你放进去了也播不了。就需要用hls.js这个库来处理。安装命令是npm install hls.js然后在 Vue 组件里写一段逻辑import Hls from hls.js export default { mounted() { const video this.$refs.videoPlayer if (Hls.isSupported()) { const hls new Hls() hls.loadSource(https://example.com/live/attraction_video.m3u8) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () video.play()) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { video.src https://example.com/live/attraction_video.m3u8 } } }这段兼容逻辑记下来很有用.m3u8格式主要是苹果生态原生支持Android 和 Windows 浏览器都要靠 hls.js 转成普通视频流。如果不想依赖后端提供 m3u8 源也可以把一段普通 mp4 视频放在public/videos目录下用video直接播放这样演示更稳。但论文里写清楚 HLS 协议的流转流程能让技术含量提升一截。Vue 组件开发中视频弹窗、轮播图、图片懒加载都可以封装成组件。我实际写的时候遇到一个问题同一个页面多个视频组件互相干扰原因是 video 实例没有在beforeDestroy里销毁。所以一定要在组件销毁时调用hls.destroy()否则切路由后声音还在响很多同学都会在这里卡一下。4. 常见问题与排查技巧实录4.1 vue 环境配置与依赖安装报错我周围同学做 Vue 项目一半的时间都耗在环境配置上。最常见的两个问题Node 版本太高或太低、依赖装不进去。遇到npm install报错先删掉node_modules和package-lock.json再用npm install重新装一遍大多数都能解决。如果还不行就查一下是不是用了npm源的问题换个国内镜像源会快很多。前面提到的vue/tsconfig找不到的问题除了手动安装依赖还有一个更快的方法创建项目时直接选“JavaScript”而不是“TypeScript”。毕设项目不追求类型系统Java 后端已经够多类型了前端就别再折腾 TS 了。另一个高频问题是在 IDEA 里开发 Vue 项目时运行按钮是灰色的。解决办法是安装并启用 Vue.js 插件然后在运行配置里选择npm serve或npm run serveNode interpreter 也要指定到本机安装的 Node 路径。关于“vue 项目源码怎么发给别人”这几乎每次演示或提交都有人问。记住一条不要把node_modules发过去别人拿到也没法直接用。正确做法是只发整个项目源码除了 node_modules 和 dist对方下载后先npm install再npm run serve。如果是要发给老师看代码截图或打包部署可以运行npm run build把dist目录部署到 Nginx 或容器里。4.2 跨域与 Session/Token 处理一旦前后端分离跨域和登录态这两个问题就会一起冒出来。我早期图省事用 Session 做登录态结果部署到不同端口后总是莫名其妙掉登录后来才意识到 Session 的 Cookie 被浏览器限制了。换成 token 之后一切都清爽了用户在登录成功后后端返回一个 token 字符串前端存到localStorage以后每个请求都在Authorization请求头上带上它后端拦截器解析 token获取用户 id。拦截器里要对部分接口做放行处理不然游客连景点列表都看不了。放行名单至少包括登录、注册、景点列表、景点详情、公告列表。需要登录才能访问的接口比如下单、支付、订单列表都要校验 token。校验失败时统一返回 401 状态码前端 Axios 拦截器如果收到 401自动跳回登录页。这里有个坑明文 token 容易被伪造所以后端生成 token 时要加上签名或过期时间。我一般直接用 JWT 结构header.payload.signature带上过期时间。不用写完整的 JWT 工具类网上有很多成熟的库但论文里要说明你的 token 是带密钥的否则老师会问安全性怎么保证。4.3 数据库乱码与日期格式问题SSM 项目连 MySQL 时乱码问题几乎每个人都会遇到。连接 URL 里一定要带上useUnicodetruecharacterEncodingutf8否则哪怕数据库建库时选了 utf8mb4Tomcat 收到的中文也会变成问号。另外 Tomcat 的server.xml里也可以加 URIEncodingUTF-8但如果你用了 POST 请求乱码主要是数据库连接串和页面字符集的问题。日期格式同样容易踩坑。MySQL 的datetime字段直接返回给前端时Vue 显示出来是2026-06-01T12:00:00.00008:00很丑。我处理的方法是在后端实体类日期字段上加上 JSON 格式化注解比如JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)这样接口返回的就是干净的时间字符串。前端如果还需要处理相对时间再单独写一个过滤器就行。还有一个常见小问题在 SpringMVC 接收前端日期参数时必须指定日期格式否则RequestParam解析会报 400。可以在 Controller 里的日期参数前加DateTimeFormat(pattern yyyy-MM-dd)或者在全局配置里注册一个类型转换器。4.4 论文查重与代码对应技巧很多同学害怕论文查重于是把网上的系统描述改了改就交上去结果因为大面积重复被退回来。我的建议是论文不要先写先把程序做完然后对着自己的代码写。写系统实现时不要整段粘贴代码而是用“这里通过一个updateDailyStock方法执行数据库的原生更新语句并判断影响行数是否大于 0”这种描述方式关键代码截图只截核心几行。这样既保证了真实感又不会让查重系统红一大片。论文里的 E-R 图、用例图、时序图要从自己的表结构出发。画图工具很多我常用的就是 ProcessOn画完导出图片插入 Word。千万不要去盗用别人的成品图图里的字段名和你的表对不上老师一眼就能看出来。我写需求分析时会把“游客购票流程”“管理员处理退票流程”写成文字用例保证每一步都和实际代码逻辑一致。论文的测试部分可以准备一张表格列出模块、测试内容、预期结果、实际结果。比如“订单模块多个用户同时购买同一场次最后一张票预期只成功 1 个订单实际结果符合预期”。这类测试结果是你自己跑出来的既有内容又不重复老师看了还会觉得你做事踏实。4.5 快速自检清单答辩前我用下面这个清单自检过一遍基本能避免 80% 的低级错误检查项检查方法常见问题登录注册用管理员和普通用户各登录一次密码加密后登录失败Token 过期景点列表游客端打开首页看图片和名称是否加载图片路径写死本地路径部署后 404详情页视频切换不同景点的详情页验证视频是否正常播放hls.js 实例没有销毁声音残留购票流程选择日期和票种下单后看库存是否减少库存扣了但订单没生成需要检查事务支付模拟点击支付后订单状态变为已支付支付接口没加 token 拦截导致 401订单列表分别查看已支付、未支付、已使用状态日期格式不对状态码映射错误管理员后台新增一个景点前端立刻显示缓存导致不刷新需要清理浏览器缓存退票/取消取消未支付订单库存回补只改了订单状态忘了恢复库存自我检查时最好开两个浏览器一个游客端一个管理端同时操作这样可以看出数据刷新是否及时。演示前一定要把数据库里的测试数据清理干净保留几组“看起来真实”的记录比如已支付订单 3 条、未支付订单 2 条、景点 6 个这样演示时不用现场造数据。最后再分享一点个人体会做这个题目最深的感受是SSMVue 本身并不难真正难的是把业务流程闭环想清楚。我第一次写的时候只顾着把接口堆出来结果“下单后库存没减”“支付后订单状态没变”“取消订单后库存没回补”每个问题都要靠加日志反复调试。后来我把订单状态流转和库存扣减画在草稿纸上按状态机来写才慢慢顺起来。所以如果你也正在做这个项目我建议先别急着写代码花一天时间把“用户从选票到验票”这个过程画成图再对照着设计表和接口后续开发效率会高很多。另外演示视频一定要提前录好现场演示极容易出问题。准备好一个测试账号、一组固定的景点数据、一张已支付状态的订单二维码录一段 5 分钟以内的完整流程视频作为备用。答辩时哪怕现场环境突然报错你把视频一放同时口述代码逻辑老师也不会觉得你项目不行。毕竟毕设考察的本质是你有没有真正理解这个系统而不是你现场能不能流畅演示。