又到了一年一度选毕设题目的季节。身边不少人在纠结选题,其中问得最多的就是“景区门票订购系统”这类Web应用题目。这个题目几乎年年都有学校在用,SSM+Vue的组合也属于最常见的搭配之一,但真正能把这样一个系统做到“能答辩、有亮点、不挂科”的程度,其实没那么简单。
这篇文章我把自己做这类项目的完整思路写出来,包含为什么选这套技术栈、数据库怎么设计、后端核心模块怎么写、Vue前端怎么组织和论文该怎么搭框架,以及我在实际开发里踩过的一些坑。不管你是刚定题的大四学生,还是想拿这个项目去参加比赛的同学,都可以直接照着这份思路去搭自己的项目。
1. 项目定位与整体设计思路
1.1 毕设选题的现状分析
先聊一个问了很多遍的问题:景区门票订购系统这个题目到底值不值得做?
我的答案是值得,但前提是你得想清楚它能给你带来什么。这类题目的核心特点是“业务闭环完整”——从用户注册、浏览景区、查看门票、下单、支付模拟到最后管理员在后台管理一切,整套流程涵盖了Web应用开发的典型环节。相比纯粹的图书管理系统、新闻发布系统,它多了一层“库存 + 订单 + 支付”的复杂业务逻辑,足以撑起一篇合格的毕业论文。
另一个重要的点是可演示性强。答辩的时候,你不只是拿着一堆截图说“我做了这个功能”,而是可以从用户端下单开始演示,切到后台看到订单数据变化,甚至用两个账号同时抢票展示一下并发控制的处理效果。这些都是在答辩中很容易加分的演示场景。
但要注意,正因为它经典,每年做的人特别多。如果只做基础版(增删改查 + 登录注册),没有任何一点自己的设计思路,很容易被评委判定为“工作量不饱和”。所以我的建议是:在基础功能之上,至少加入一个“有点难度”的点,比如库存防超卖、订单超时自动取消、数据可视化统计,这三选一就能让整个项目提升一个档次。
1.2 技术栈选型的底层逻辑
题目给的是“ssm+vue”,所以后端定为Spring + SpringMVC + MyBatis,前端用Vue,这是很明确的技术组合。不过在具体版本和细节上仍然可以做选择。
先说说SSM为什么到现在还是很多学校的毕设标配。原因很现实:大部分学校数据库、Java Web相关课程讲的就是这套。SpringMVC负责请求路由和参数绑定,MyBatis负责SQL与对象的映射,Spring负责把它们串起来。这样的分层结构清晰,论文里也好写“基于SSM框架”这样的章节内容。
Vue这边我建议优先用Vue 3 + Vite + Element Plus,如果对稳定性和熟悉的程度要求更高,也可以在Vue 2环境下把Element UI跑熟。考虑到这是毕设项目而不是生产项目,重要的是你能把Vue的生命周期、组件通信、路由跳转这些概念说清楚,答辩时被问到“MVVM是什么”“组件之间怎么传参”这类问题能接得住。
这里有个很多同学会误解的地方:明明Spring Boot更简单,为什么要用SSM?如果你自己也这么想过,答辩时就会被老师反问。我的理解是,SSM需要手动配置Spring的XML和JavaConfig、配置MyBatis的映射文件、配置web.xml里的DispatcherServlet,这个过程虽然繁琐,但恰好让你更清楚地理解框架的运行机制。而Spring Boot把一切自动配置了,代码更少,但“黑盒”也更多。所以你可以在论文里写“为了深入理解框架内部原理,选择SSM结构进行实现”,这是一种合理且有深度的选型解释。
技术栈的完整清单列一下,方便照着准备环境:
| 角色 | 技术选型 | 版本建议 |
|---|---|---|
| 后端框架 | Spring + SpringMVC + MyBatis | Spring 5.2.x / MyBatis 3.5.x |
| 数据库 | MySQL | 8.0 或 5.7 |
| 连接池 | Druid | 1.1.x |
| 构建工具 | Maven | 3.6+ |
| 服务器 | Tomcat | 9.x |
| 前端框架 | Vue + Vue Router + Vuex/Pinia | Vue 2.7 或 Vue 3.x |
| UI组件库 | Element UI 或 Element Plus | 2.x |
| 请求工具 | Axios | 1.x |
1.3 系统功能模块拆解
把系统按角色拆开,主要分为用户端和管理员端两部分。
用户端的功能包括:注册与登录、景区列表浏览、按关键词或分类搜索、门票详情查看、选择票种与数量、提交订单、模拟支付、个人订单管理(查看订单状态、取消未支付订单)。这些都是用户能直接感知的功能,需要在演示时逐一点到。
管理员端的核心功能包括:景区管理(新增、编辑、上下架)、门票管理(维护票种、价格、库存)、订单管理(查看所有订单、处理超时订单)、数据统计(按景区维度统计门票销售情况)。管理员端的统计功能如果做到位,很容易成为论文里的亮点章节。
整个系统的核心业务逻辑其实就是一个词:库存。门票库存直接影响用户能否下单,同时下单和取消订单又会回写库存。所有复杂的业务设计都围绕库存展开,这也是后面设计和编码时最需要认真对待的部分。
2. 数据库设计与表结构规划
2.1 业务实体梳理
一个小型的景区门票系统,数据库设计不需要做成“高大全”,但关键实体必须清晰。我按自己的项目经验,建议拆成下面几张核心表:
- 用户表(sys_user)
- 景区表(scenic)
- 景区图片表(scenic_image)——可选,用于存储多张轮播图片
- 门票表(ticket)
- 订单表(orders)
- 订单明细表(order_item)
- 支付记录表(payment_record)
为什么要单独拆出订单明细表?因为一个订单可能包含多个票种,比如同时买了成人票和儿童票。如果只在订单表里存一个票种ID和一个数量,那么“一个订单对应多个票种”的场景就做不了。拆出明细表后,订单主表只存订单号、用户ID、总金额、状态等公共信息,明细表负责票种ID、单价、数量、小计。这个设计虽然多一张表,但让数据关系更合理,也更符合订单系统的典型建模方式。
2.2 核心表结构设计
直接贴我认为比较合理的建表SQL,你可以参考调整。
用户表:
CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT '加密后的密码', real_name VARCHAR(50) COMMENT '真实姓名', phone VARCHAR(20), email VARCHAR(100), avatar_url VARCHAR(255), role TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', status TINYINT DEFAULT 1 COMMENT '1-正常 0-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );景区表:
CREATE TABLE scenic ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '景区名称', location VARCHAR(200) COMMENT '景区地址', description TEXT COMMENT '景区简介', open_time VARCHAR(50) COMMENT '开放时间', cover_image VARCHAR(255) COMMENT '封面图', status TINYINT DEFAULT 1 COMMENT '1-营业 0-停业', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );门票表:
CREATE TABLE ticket ( id INT PRIMARY KEY AUTO_INCREMENT, scenic_id INT NOT NULL COMMENT '所属景区', name VARCHAR(100) NOT NULL COMMENT '票种名称', price DECIMAL(10,2) NOT NULL COMMENT '单价', stock INT NOT NULL DEFAULT 0 COMMENT '库存', sale_count INT NOT NULL DEFAULT 0 COMMENT '已售数量', status TINYINT DEFAULT 1 COMMENT '1-可售 0-下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );订单主表:
CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号', user_id INT NOT NULL COMMENT '下单用户', total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额', status TINYINT NOT NULL COMMENT '0-待支付 1-已支付 2-已取消 3-已使用 4-已过期', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME COMMENT '支付时间', expire_time DATETIME COMMENT '支付过期时间' );订单明细表:
CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, ticket_id INT NOT NULL, ticket_name VARCHAR(100), price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL );这张表设计的几个细节值得多说一句。订单表里我存了order_no这个业务订单号,而不是直接用自增主键返给前端。原因一是业务订单号方便在支付回调、定时任务、异常排查时携带追溯,二是避免把数据库主键直接暴露给用户。生成订单号的方式也很简单,用时间戳加随机数再拼两位业务后缀就行。
2.3 表关系的实际处理
实体间的关联关系归纳起来就是:一个景区拥有多条门票,一个用户可以创建多张订单,一张订单拥有多条明细。在ORM层面,MyBatis处理这些关系最顺手的做法是“嵌套结果映射”,也就是通过join查询把主表和从表的数据一次查出,再用collection标签封装成列表对象。
举个例子,查“订单详情”时,通常需要把订单主表、用户信息、订单明细、门票信息一起返回给前端。我当时在Mapper XML里写的查询大概是这样的思路:
<resultMap id="OrderDetailMap" type="com.example.entity.Order"> <id property="id" column="id"/> <result property="orderNo" column="order_no"/> <result property="totalAmount" column="total_amount"/> <collection property="items" ofType="com.example.entity.OrderItem"> <id property="id" column="item_id"/> <result property="ticketName" column="ticket_name"/> <result property="quantity" column="quantity"/> <result property="subtotal" column="subtotal"/> </collection> </resultMap>这种嵌套查询在写论文时也可以当作一个重点来写,说明MyBatis的resultMap如何解决复杂映射。
还有一点最容易犯错——字段命名不一致。Java实体类用驼峰ticketName,数据库列名用下划线ticket_name,如果MyBatis没有开启驼峰映射,查出来就是null。所以我建议在MyBatis全局配置里加上:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>或者在Spring的XML配置文件中给SqlSessionFactoryBean设置配置项。这个点很小,但真的能省掉大量排查时间。
3. 后端核心模块实现细节
3.1 登录鉴权与用户安全
后端分层的经典结构是Controller → Service → Mapper,SSM项目基本都这样写。Controller只负责接收参数和返回结果,尽量不写业务逻辑;Service层是业务规则的核心;Mapper只做数据访问。
登录模块要注意两个问题:密码安全存储和身份认证方案。
密码不要用明文存库,哪怕只是毕设也建议用BCryptPasswordEncoder或MD5加盐。虽然Spring Security的整套东西在SSM里接入稍重,但我只需要引入一个spring-security-crypto依赖,直接使用它的加密工具类即可,代码量很小。这样在论文的“安全性设计”小节里你也有内容可写了。
身份认证我建议用JWT方案。用户登录成功后,后端生成一个包含用户ID和角色的token返回给前端。前端在每次请求的header上带上Authorization: Bearer xxx。后端写一个拦截器,在执行Controller之前取出token,解析并校验,将用户信息存入ThreadLocal或request attribute里供后续使用。
拦截器示例:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } LoginUser user = JwtUtils.parseToken(token.substring(7)); if (user == null) { response.setStatus(401); return false; } request.setAttribute("loginUser", user); return true; } }然后在SpringMVC配置中注册拦截器,并放行登录接口、注册接口、景区查询接口等不需要认证的路径。
3.2 库存扣减与并发防超卖
这是整个项目中最有“含金量”的部分,也是我强烈建议你在论文里详细展开的部分。
先想一个场景:某景区成人票库存只剩10张,此时有20个人同时下单。如果代码逻辑是“先查库存,库存大于0,再执行update扣减”,就会发生超卖。原因在于查询和更新之间不是原子的,两个请求都读到了库存10,两个都认为自己可以下单,最后库存被扣成8,但订单却生成了12个。
解决思路有两种层次。
第一种是数据库层面的乐观锁。在更新库存时,不单纯用stock = stock - 1,而是加上stock > 0条件:
UPDATE ticket SET stock = stock - #{quantity}, sale_count = sale_count + #{quantity} WHERE id = #{ticketId} AND status = 1 AND stock >= #{quantity}这段SQL会返回受影响行数。如果行数为0,说明库存不足或门票已被下架,此时由Service抛出库存不足的异常,事务回滚,订单也不会生成。用数据库的原子更新来保证并发安全,比“Java代码里先查询再判断”要可靠得多。
第二种是在代码层面使用分布式锁或者synchronized。对于单机部署的毕设项目来说,synchronized在单一JVM内是有效的,但如果将来拆成多实例部署,每个实例的锁互不相通,就会出现问题。写论文时,可以把这两种方案都分析一遍,然后得出“本系统采用数据库乐观锁,同时结合事务机制”的结论。这样显得你有对比思考的过程。
我的完整下单逻辑流程是这样:
- 前端提交订单请求,携带票种ID和数量。
- 后端校验用户登录状态、票种状态和数量合法性。
- 事务开启。
- 执行“预扣减”库存的update语句,校验受影响行数。
- 若行数为0,抛异常,回滚事务。
- 创建订单主表记录,状态为“待支付”,设置过期时间通常为15分钟。
- 创建订单明细记录。
- 事务提交,返回订单号给前端。
这里还要注意一点:不建议在创建订单前就提前把库存扣完并且订单尚未创建,因为如果订单创建失败,会导致库存减少而订单不存在。前面把扣库存和创建订单放在同一个事务里,就是为了保证“要么都成功,要么都失败”。
3.3 订单超时未支付的处理方案
用户下单后迟迟不支付,库存就会被占用。如果没有处理机制,恶意用户或健忘用户会把库存“锁死”,后面想买的用户就买不到了。常见的解决方案有两种:
一种是懒处理:在用户查询订单或尝试支付时,先检查订单是否超过过期时间,如果超时则把订单置为“已过期”,并回补库存。这种方式实现简单,但准确性依赖用户主动操作。
另一种是使用定时任务:Spring自带的@Scheduled注解就能满足需求。每隔30秒扫描一次过期订单,将这些订单改为过期状态并回补库存。
我在项目里选的是定时任务方案,核心代码思路是:
@Scheduled(fixedDelay = 30000) @Transactional public void handleExpiredOrders() { List<Order> expiredList = orderMapper.selectExpiredOrders(now); for (Order order : expiredList) { orderMapper.updateStatus(order.getId(), 4); // 过期 for (OrderItem item : orderMapper.selectItemsByOrderId(order.getId())) { ticketMapper.increaseStock(item.getTicketId(), item.getQuantity()); } } }注意这里的increaseStock是增加库存,SQL为UPDATE ticket SET stock = stock + #{quantity} WHERE id = #{ticketId}。它必须在一个事务里执行,防止状态更新和库存回补之间出现不一致。
这个功能虽然代码量不大,但非常“能打”。论文里可以写“系统通过定时任务扫描订单超时状态,针对过期未支付订单进行库存回补,保证了库存数据的一致性”,在答辩时也能和评委聊“为什么选定时任务而不是懒处理”。
3.4 模拟支付与回调处理
毕设项目不可能接入真实支付,所以通常做一个模拟支付页面。用户点击“去支付”后,后端生成一个支付单号并返回给前端,前端展示一个简单的收银台页面,有“模拟支付成功”和“模拟支付失败”两个按钮。
支付成功后的核心是回调处理。真实支付渠道会异步通知后端接口,毕设里简化成直接调用本地的支付回调查接口。我建议你在设计时保留一个PaymentController,里面的notify接口模拟支付回调,接收订单号和一个模拟的支付流水号,然后完成以下操作:
@PostMapping("notify") public Result notify(@RequestBody PayNotifyDto dto) { return orderService.handlePayNotify(dto); }handlePayNotify的业务如下:
- 根据订单号查出订单
- 如果订单已经是“已支付”,直接返回,不重复处理
- 更新订单状态为已支付,记录支付时间和流水号
- 把订单状态同步到订单明细
这里最经典的坑就是重复回调。如果网络抖动,支付渠道可能发多次回调,后端必须做状态幂等判断。论文里如果提到“接口幂等性设计”,评委的印象分至少不会低。
4. 前端Vue界面与交互设计
4.1 整体布局与组件拆分
Vue前端我采用的是“典型的管理后台 + 商城”混合布局。用户端使用顶部导航、轮播图、门票卡片列表、固定底栏;管理员端使用左侧菜单加右侧内容区。
从工程结构上说,一定要把组件拆分得干净。列举一下我当时的目录结构:
src/ ├── api/ # axios请求封装 │ ├── request.js │ ├── ticket.js │ └── order.js ├── assets/ # 静态资源 ├── components/ # 通用组件 │ ├── TicketCard.vue │ └── SearchBar.vue ├── router/ # 路由配置 │ └── index.js ├── store/ # 状态管理(Vuex或Pinia) │ └── user.js ├── views/ # 页面视图 │ ├── home/Home.vue │ ├── scenic/ScenicDetail.vue │ ├── order/OrderConfirm.vue │ ├── order/OrderList.vue │ └── admin/ │ ├── AdminLogin.vue │ ├── ScenicManage.vue │ └── OrderManage.vue └── utils/ # 工具类组件的核心思想是复用。比如门票卡片组件TicketCard.vue,首页、搜索结果页、景区详情页都会用到,接收一个ticket对象作为prop,点击后通过路由跳转到详情页。把这种组件抽出来,不仅可以减少重复代码,也是论文中“前端组件化设计”的有力例证。
4.2 门票列表与筛选交互
门票列表页最容易遇到的问题是“查询条件与后端参数对应不上”。我在请求封装里统一用params传递查询参数:
export function getTicketList(params) { return request({ url: '/api/v1/ticket/list', method: 'get', params }); }页面里调用时,把搜索关键词、景区分类、排序字段这些值包装成一个对象传进去即可。后端Controller用@RequestParam接收对应参数。要注意的是,筛选条件的排序和分页一定要在后端完成,不要让前端把全量数据查出来再自己在JavaScript里排序。
搜索框这里做一个小的体验优化:防抖处理。用户输入关键词后,不必每次都实时请求,可以等用户停止输入300毫秒后再发起搜索。实现方式就是在watch监听回调里用setTimeout加clearTimeout。这个细节代码量不大,但体现了前端的基本素养。
4.3 下单流程与状态管理
下单流程是前端最需要谨慎设计的页面:从门票详情页点击“预订”,跳转到订单确认页,确认页显示门票信息、数量选择、总价计算,然后是提交订单和模拟支付。
关于状态管理,用户登录信息适合放在Vuex/Pinia中。下单需要用到的“当前选中的票种和数量”,我建议用路由跳转时传递参数,或者用一个简单的event bus传递。要避免把大量临时业务数据放进全局状态,否则刷新页面之后状态丢失,调试起来非常痛苦。
模拟支付的页面其实很简单:展示订单号和金额,两个按钮。点击“模拟支付成功”就调用后端的支付接口,拿到结果后跳转到个人订单列表页,前端根据订单状态显示不同颜色的标签。这里稍微注意一下,支付成功后不要立刻把订单状态改掉再展示,等后端接口返回结果后再刷新列表更稳妥。
5. 论文写作的结构与避坑指南
5.1 论文结构与章节安排
一个SSM+Vue毕设项目的论文,常见结构可以这么规划:
| 章节 | 重点内容 | 建议篇幅 |
|---|---|---|
| 摘要 | 研究背景、技术方案、系统功能、测试结果 | 300字左右 |
| 绪论 | 背景、意义、研究现状、研究内容 | 1500-2000字 |
| 相关技术 | SSM框架、Vue、MySQL、JWT等 | 2000字左右 |
| 需求分析 | 用例图、功能性需求、非功能性需求 | 1500字左右 |
| 系统设计 | 架构图、功能模块图、数据库设计 | 2500字左右 |
| 系统实现 | 关键模块的功能说明、核心代码截图 | 3000字左右 |
| 系统测试 | 功能测试用例表、结果分析 | 1500字左右 |
| 总结与展望 | 完成的工作、不足、后续方向 | 500字左右 |
很多同学的论文“头重脚轻”,需求分析写了一大堆,系统实现却把整个Controller代码贴上去。评委一眼就知道没做过。正确做法是:每个功能模块先用文字描述业务逻辑,再用流程图或时序图说明处理顺序,最后才贴一段关键代码,代码量不宜超过一页。
5.2 摘要与绪论的写法
摘要看起来就一段话,但这是论文最容易被快速扫描的部分。我总结了一个好用的套路:研究背景一句话,技术方案一句话,系统功能两句话,测试结论一句话。整段控制在250-350字,比写一大段“无病呻吟”的背景要强很多。
绪论部分的“研究现状”,不要直接堆砌一大堆行业报告。你写的应该和“景区门票系统”相关,比如电子票务的发展、在线预约的普及、库存和防超卖的技术演进。写这类内容时不要抄大段文字,尽量用自己的话概括,否则查重率会很难看。
5.3 答辩准备与常见问题
答辩环节最能拉开差距。你有两个准备重点:一是演示路径要熟练,二是可能被追问的技术点要心里有数。
演示路径我建议按这个顺序走:用户注册登录 → 搜索景区 → 查看门票 → 下单 → 模拟支付 → 查看订单列表 → 后台登录 → 管理景区门票 → 查看订单数据统计。每一步都要准备好“如果评委要求我重复操作”的场景,比如断网、库存为零、订单过期等异常情况。
常见的答辩问题大致包括:
- 为什么用SSM而不是Spring Boot?
- 数据库表之间是什么关系?
- 如果用户同时下单,怎么防止超卖?
- 订单状态有哪些,状态流转是怎么设计的?
- 前端路由的实现原理是什么?
- 你负责的模块有哪些,代码量大概多少?
前几个问题上面已经讲过,关键是要用自己的话讲清楚,而不是背概念。
6. 实际开发中的踩坑记录
6.1 前后端联调最容易出的问题
第一个坑就是跨域。前端跑在http://localhost:5173,后端跑在http://localhost:8080,两者端口不同,浏览器会拦截跨域请求。解决办法是后端配置CORS过滤器,或者在SpringMVC中继承WebMvcConfigurer重写addCorsMappings方法:
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:5173") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .maxAge(3600); }如果配置完发现OPTIONS请求还是返回405或状态码异常,多半是拦截器没有放行预检请求。解决方法是登录拦截器里判断HttpMethod.OPTIONS直接放行。
6.2 时间格式与Json序列化问题
LocalDateTime是Java 8之后推荐的日期类型,但SSM中常用的Fastjson或Jackson老版本对LocalDateTime支持并不完整,很容易序列化成数组或者直接报错。
我的解决套路是给ObjectMapper配置JavaTimeModule模块,并指定统一的时间格式:
ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));前端也要配合做一件事:从后端拿到的字符串是“yyyy-MM-dd HH:mm:ss”格式,Element Plus组件要求传Date类型,可以用dayjs转换再传给组件。这个细节不处理的话,日期组件很可能在编辑时不显示数据。
6.3 订单号唯一性与异常排查
订单号生成时,并发高的情况下可能出现重复。简单方案是yyyyMMddHHmmss + 4位随机数,但极少数情况下仍然可能重复。我建议往订单号里再拼上用户ID的后四位,或者用UUID去掉横线取前8位,这样重复的概率基本可以忽略。
排查问题时最有效的工具是日志。Controller入口打一条请求日志,Service关键节点打一条业务日志,异常抛出处打一条错误日志。SSM项目里配置Logback或Log4j2不会很复杂,但很多人不重视。等到答辩演示时报错但你根本不知道哪里出错,那时候后悔就晚了。
6.4 定时任务的执行时间设置
如果你用了@Scheduled做超时订单扫描,记得注意任务执行的时机。如果系统时间用的是默认时区,而数据库时间存储的是本地时间,比较的时候要统一。不要把now()写在SQL里之后又在Java里比较,时间基准不一致会影响判断。
我把定时任务的执行频率设为30秒,这个频率对付毕设完全够用。数据库索引方面,给订单表的expire_time加一个普通索引,扫描时走索引会快很多。另外启动多个Tomcat实例时每个实例都会跑这个定时任务,毕设阶段单实例部署不用太担心,但论文里如果提到扩展性,可以说可以通过分布式任务框架来解决。
做这类SSM+Vue的毕设项目,最核心的收获不在于把某个框架用得多熟,而在于你能把一套完整的业务逻辑从头到尾理清楚。库存怎么防超卖、订单超时怎么处理、前后端怎么对接,这些才是将来真正工作里也会遇到的东西。
我个人这几个项目做下来的体会是:框架版本和技术栈真的没那么重要,重要的是你有没有把“为什么这么做”想明白。如果你的论文能回答清楚“为什么选SSM”、“为什么订单要拆明细表”、“为什么用乐观锁扣库存”,答辩基本不会差。
最后再分享一个小技巧:项目完成后,把所有核心的SQL、接口路径、组件结构梳理成一份简单的README文档。别小看这个动作,你中期检查、写论文、答辩前复习,都会反复用到它。做一次,能省下后面的大量时间。