又到毕设季,“ssm+vue旅游景点售票系统”这个题目,几乎是计算机类专业里最稳的选择之一。别看名字平淡,它把后端 SSM、前端 Vue、数据库设计、论文写作全串在一条线上,对要毕业设计的同学来说,是投入产出比很高的项目。它能解决的是景区线上购票、订单管理、后台统计这些真实需求,演示和答辩也方便。无论你是刚学完 Java 的本科生,还是想找个中型项目练手的专科生,拿这个题目起步都不亏。这篇文章我就从头到尾拆一下:选题思路、技术选型、数据库怎么建、论文和程序怎么对应、哪些地方容易踩坑,尽量给你一条能直接照做的路线。
我见过很多同学一上来就找代码,改个名字就去答辩了,结果流程一问就懵。实际上,这个系统真正能拿得出手的点不在于页面多炫,而在于你有没有把订单、库存、票种、支付这堆逻辑理清楚。所以这篇不会只贴代码,更多是讲清楚“为什么这么设计”,以及我在实际跑通项目时碰到的问题和解决办法。
1. 项目整体设计与技术选型思路
1.1 为什么选 SSM+Vue,而不是 Spring Boot 前后端不分离
先说结论:SSM+Vue 不是最新的技术组合,但它是毕设场景里最不容易翻车的一套。SSM 指的是 Spring + SpringMVC + MyBatis,答辩老师对这个框架非常熟悉,论文里的“三层架构”“控制反转”“依赖注入”这些理论点都能找到对应的代码位置,写起来也顺手。Vue 负责做前端页面,展示效果好,路由跳转、组件复用、异步请求这些东西也能单独拿出一章来写,论文内容瞬间就充实了。
有人会问,现在企业都用 Spring Boot 了,为什么不用 Spring Boot?说实话,用 Spring Boot 也行,甚至更简单,但毕设题目已经写死是“ssm+vue”,那就没必要改。而且 SSM 的 XML 配置虽然繁琐,恰恰能用来讲清楚 Spring 的 Bean 管理和 MyBatis 的映射机制,遇到运行问题翻起来更有底。Spring Boot 把一切都自动配置好了,反而容易让论文显得“干瘪”。
再说前后端分离的问题。传统 JSP 项目需要 Tomcat 同时渲染页面,前端代码和后端代码缠在一起,改个样式都要重启。SSM 只做后端接口,Vue 单独跑在 Node 开发服务器上,前端用 Axios 请求后端数据,解决了“后端同学改完页面把 Java 文件弄乱”的问题。这个分离结构在答辩时也更容易演示:前端一套工程,后端一套工程,各自独立启动,专业感立刻就有了。
1.2 系统核心功能模块拆解
一个能上台面的旅游景点售票系统,至少要分成游客端和管理端两条线。游客端的功能是“看得见、买得到、查得到”:注册登录、景点列表、景点详情、票种选择、下单支付、我的订单、取消订单。管理端的功能是“管得住”:景点信息维护、票种管理、余票调整、订单查询、退款审核、用户管理、公告发布、数据统计。
我建议把“公告 + 轮播图 + 宣传视频”也纳入功能里,不是为了花哨,而是论文里的“系统管理”模块需要这些内容来撑起页面截图。特别是景区宣传视频,可以用 Vue 播放 m3u8 流媒体,这个小功能在演示时会让人眼前一亮,后面我会专门讲怎么实现。
模块拆解时要注意职责边界。比如“余票管理”到底是属于景点模块还是票种模块?我的做法是:票种表里带一个总库存字段,另外再单独建一张“每日库存表”来管理某一天某个票种还剩多少张,这样日票、周票、节假日票都能灵活处理。订单模块只管记录用户买了哪个票种、买了多少张、金额多少,不直接改景点表库存,库存扣减放在票种或每日库存表上,避免两个模块乱改同一张表。
1.3 数据库设计关键点
数据库是论文里最容易被评阅老师盯住的部分,我强烈建议动手写代码前先把表结构画清楚。核心表大概这样:
- 用户表 sys_user:id、用户名、密码(用 MD5 加密)、手机号、角色(管理员/普通用户)、创建时间。
- 景点表 attraction:id、景点名称、简介、封面图、详细内容、所在城市、开放时间。
- 票种表 ticket_type:id、景点 id、票种名称(成人票、学生票)、价格、总库存。
- 每日库存表 daily_stock:id、票种 id、销售日期、剩余数量、版本号。
- 订单表 orders:id、订单号、用户 id、总金额、支付状态、下单时间、支付时间。
- 订单明细表 order_item:id、订单 id、票种 id、票价快照、数量、小计。
- 公告表 notice:id、标题、内容、发布时间。
这里有个细节:订单明细一定要保存“票价快照”,不能下单后再去关联票种表价格。因为管理员可能后来调价,但历史订单应该按购买时的价格算。这个细节写进论文里,评阅老师会认为你考虑了业务状态的一致性问题。
每日库存表加一个 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 和 Vuex,CSS 预处理器看个人习惯,我选的是 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 serve,Node 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 里一定要带上useUnicode=true&characterEncoding=utf8,否则哪怕数据库建库时选了 utf8mb4,Tomcat 收到的中文也会变成问号。另外 Tomcat 的server.xml里也可以加 URIEncoding="UTF-8",但如果你用了 POST 请求,乱码主要是数据库连接串和页面字符集的问题。
日期格式同样容易踩坑。MySQL 的datetime字段直接返回给前端时,Vue 显示出来是2026-06-01T12:00:00.000+08:00,很丑。我处理的方法是在后端实体类日期字段上加上 JSON 格式化注解,比如@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),这样接口返回的就是干净的时间字符串。前端如果还需要处理相对时间,再单独写一个过滤器就行。
还有一个常见小问题:在 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 个,这样演示时不用现场造数据。
最后再分享一点个人体会
做这个题目最深的感受是:SSM+Vue 本身并不难,真正难的是把业务流程闭环想清楚。我第一次写的时候,只顾着把接口堆出来,结果“下单后库存没减”“支付后订单状态没变”“取消订单后库存没回补”,每个问题都要靠加日志反复调试。后来我把订单状态流转和库存扣减画在草稿纸上,按状态机来写,才慢慢顺起来。所以如果你也正在做这个项目,我建议先别急着写代码,花一天时间把“用户从选票到验票”这个过程画成图,再对照着设计表和接口,后续开发效率会高很多。
另外,演示视频一定要提前录好,现场演示极容易出问题。准备好一个测试账号、一组固定的景点数据、一张已支付状态的订单二维码,录一段 5 分钟以内的完整流程视频,作为备用。答辩时哪怕现场环境突然报错,你把视频一放,同时口述代码逻辑,老师也不会觉得你项目不行。毕竟毕设考察的本质是你有没有真正理解这个系统,而不是你现场能不能流畅演示。