看到“SpringBoot+微信小程序旅行业务管理系统”这类题目,很多人的第一反应是:又是一套CURD。但真正动手做才发现,旅行行业特有的业务规则——线路排期、余位控制、订单状态流转、支付回调、导游派单——随便拎一个出来都能让所谓的“毕业设计”翻车。这篇文章不打算讲教科书式的架构,就围绕一个真实做过、也帮不少学弟梳理过的旅行社智慧运营平台项目,说说怎么把题目落地成能跑、能演示、能答辩的系统。无论你是准备拿它当毕设,还是想借小程序风口做个旅行社的运营工具,这篇内容都能给你一条可以直接走的路线。
1. 先把题目拆明白:旅行业务管理系统到底要管理什么
1.1 不是简单的商品下单,是“资源+排期+订单”三重联动
旅游产品和普通电商商品最大的区别在于时效性和库存动态性。一件T恤库存是静态数字,卖完就下架;一条旅游线路却有出发日期、成团人数、余位数量、价格随季节浮动这些变量。比如“云南七日游”在寒暑假和淡季价格完全不同,同一个日期可能因为成团人数不足取消,也可能因为报名爆满提前截止。所以做这个系统,核心不是把线路列表和下单接口做出来,而是把“线路—排期—班期—余位—订单”这条链路理顺。
我在项目里把排期单独设计成了一张核心表,叫 tour_schedule,每条线路下挂多个出发日期,每个出发日期维护独立的余位和价格。这样做的好处是:小程序端展示“可报名班期”时,不用每次实时计算,直接查排期表的余位字段;后端在下单时再对余位做一次减扣校验,双重保障不超卖。
1.2 用户角色梳理:C端游客、后台运营、系统管理三拨人
很多毕设会把权限做成一个超级管管理员,然后所有后台功能堆在一起。这个项目的角色如果只分“用户”和“管理员”,演示时确实简单,但答辩老师一问“旅行社内部怎么分工”就容易露馅。我建议至少拆成三类:
- 游客(小程序端):浏览线路、查看排期、下单、支付、查看订单、填写出行人信息、提交评价。
- 运营人员(后台):维护线路和排期、处理订单审核、安排导游和车辆、处理退改申请。
- 系统管理员(后台):管理运营人员账号、查看营收报表、配置基础数据(如目的地分类、公告)。
这个划分不是拍脑袋。旅行社的实际运营流程里,线路编辑和订单审核通常是不同岗位,如果把权限混在一起,后期扩展分销、财务模块时会非常痛苦。毕设阶段哪怕不做细粒度权限,也建议用 Spring Security 或拦截器把这三类角色从登录态上分开,后续答辩也有更多可讲的东西。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:为什么用 SpringBoot + 微信小程序,有哪些替代方案
2.1 后端选型:SpringBoot 是稳妥解,但不是唯一解
SpringBoot 在这个题目里几乎是标配,理由很实际:生态成熟、招人眼熟、资料多。毕设期间遇到问题搜一下基本都有现成答案,这对时间紧的学生太重要了。搭配 MyBatis Plus 可以少写很多单表 CRUD 的 SQL,分页查询、逻辑删除、自动填充这些功能开箱即用,能省下至少两天的重复劳动。
如果是我在真实项目里选型,可能会考虑若依这类快速开发框架,直接生成前后端代码。但毕设场景不建议这么做——代码生成器出来的东西很难讲清楚“你的设计在哪里”,答辩时容易翻车。所以哪怕慢一点,也建议自己搭一套基础结构,至少 Controller、Service、Mapper 的分层要能讲明白。
2.2 小程序端:原生还是 uni-app?
小程序端我推荐直接用微信小程序原生开发。原因很直接:毕设只需要跑通微信开发者工具,原生框架调试最直观,没有额外的编译链问题。如果你对 uni-app 很熟,用它也行,打包成微信小程序效果一样,但要注意 uni-app 的组件和原生小程序在某些 API 行为上有差异,比如 wx.login 的调用时机、页面栈的管理,调试起来多一层麻烦。
原生小程序的结构相对固定:app.json 配全局页面和导航栏,utils/request.js 封装请求,页面由 wxml、wxss、js、json 四件套组成。对没接触过的同学来说,这种“一个页面一个文件夹”的组织方式其实很友好,比 React/Vue 那种组件树更容易上手。
2.3 整体架构:前后端分离,统一返回体和登录态
整个系统的调用关系不复杂:小程序通过 wx.request 发起 HTTPS 请求,请求到 SpringBoot 的 Controller,Controller 调 Service,Service 调 Mapper 操作 MySQL,数据以 JSON 格式返回。这里有两个点会在联调时反复踩坑:
第一是统一返回体。我一般定义一个 Result<T>,包含 code、message、data 三个字段。前端请求时先判断 code 是不是 200,而不是直接拿 data。否则后端一抛异常,前端拿到的结构不一致,调试起来很痛苦。
第二是登录态。小程序的 wx.login 拿到的是临时 code,需要后端通过 code 换 openid。换到 openid 后查数据库判断用户是否已存在,不存在就自动注册,然后生成自定义 token 返回给前端,后续请求都在 header 里带 token。不要直接拿 openid 当身份凭证,token 可以设置过期时间,openid 不行。
3. 系统模块设计:先画业务流程图,再写代码
3.1 核心功能清单和模块划分
以我做过的这个模拟项目为例,功能模块可以分成六大块:
| 模块 | 功能点 | 对应角色 |
|---|---|---|
| 线路管理 | 线路发布、编辑、上下架,多图上传 | 运营人员 |
| 排期管理 | 按线路维护出发日期、余位、价格 | 运营人员 |
| 订单管理 | 订单列表、详情、审核、取消、退款 | 运营人员 |
| 订单流程 | 创建订单、支付(模拟)、出行人信息填写 | 游客 |
| 评价管理 | 订单完成后评价、后台查看回复 | 游客 / 运营 |
| 统计报表 | 营收统计、线路销量排行 | 管理员 |
这些模块不是平均用力的。毕设的评分重点一般放在订单流转和数据关联上:用户下单后订单状态怎么变、余位怎么扣减、退款时怎么恢复余位,这些逻辑完整,分数基本不会低。反而是在后台界面上花太多时间做特效,性价比很低。
3.2 订单状态机:旅行订单比电商订单多一个“出团”
电商订单状态一般是待支付、已支付、已发货、已完成、已退款。旅行业务的订单状态要更细,我整理了一套比较常用的流转:
- 待支付:下单后未完成支付,默认超时关闭(可以做成定时任务,也可以偷懒在查询时判断)。
- 已支付待确认:支付成功,等待运营人员确认订单(旅行社线下需要确认资源是否到位)。
- 已确认:运营确认资源,订单进入生效状态。
- 待出团(可选):在出发前的一个状态,便于旅行社提前联系游客。
- 已出团:游客按排期出行,订单开始计算完成进度。
- 已完成:出行结束,游客可以评价。
- 已取消:未支付取消或用户主动取消。
- 已退款:支付后退款完成,余位恢复。
这套状态在数据库里用一个 status 字段存储,代码里不建议用整型魔法值,可以用枚举类维护。写死数字最大的问题是:三个月后你自己都分不清 2 是“已支付”还是“已确认”。用枚举后,前端展示文案可以直接映射,后端判断状态流转也更清晰。
3.3 业务流程串起来:下单、扣余位、支付回调、出团
整个核心流程我建议按下面这样串:
- 游客在小程序端选择线路和班期,系统展示剩余余位。
- 用户填写出行人姓名、身份证号、手机号,点击提交订单。
- 后端创建订单,状态置为“待支付”,同时不立即扣减余位,而是标记“锁定余位”数量。
- 用户调用模拟支付接口,支付成功后后端回调更新订单状态,同时真正扣减余位。
- 如果用户超时未支付,定时任务将订单置为“已取消”,并释放锁定余位。
为什么不在下单时直接扣余位?因为用户可能下单后不支付,直接把余位扣光,会造成“占着茅坑不拉屎”的问题。但也不建议完全不锁。如果完全不处理并发,两个人同时下单最后一个余位,就会出现超卖。我采用的方案是订单表加一个 lock_expire_time 字段,下单时把余位从“可售余位”扣到“锁定余位”,支付成功后把“锁定余位”改为“已售余位”,超时未支付则反向恢复。这样既能防超卖,又不会让未支付订单长期占坑。
4. 数据库设计与核心表结构
4.1 数据表整体设计和关系
数据库我先列一下这次项目实际用到的主要表,一共 9 张,覆盖业务主体和关联关系:
user:小程序用户,字段包含 openid、昵称、头像、手机号、注册时间。admin_user:后台账号,包含用户名、密码(BCrypt 加密)、角色类型、状态。route:线路表,包含线路名称、简介、详情富文本、封面图、价格标签、状态。tour_schedule:排期表,包含所属线路 id、出发日期、可售余位、锁定余位、成人价、儿童价。order:订单主表,包含订单编号、用户 id、排期 id、总金额、状态、下单时间。order_traveler:出行人表,订单和出行人是 1 对 N,记录姓名、证件号、手机号。payment_record:支付记录表,保存支付流水号、支付方式、支付金额、回调结果。comment:评价表,关联订单 id,包含评分、内容、回复内容。banner:首页轮播图,运营后台可配置。
4.2 核心字段设计要点
订单表的关键字段示例:
sql复制CREATE TABLE `orders` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` bigint(20) NOT NULL COMMENT '下单用户',
`schedule_id` bigint(20) NOT NULL COMMENT '排期ID',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`status` tinyint(4) NOT NULL COMMENT '订单状态: 0待支付 1已支付 2已确认 ...',
`lock_expire_time` datetime DEFAULT NULL COMMENT '余位锁定过期时间',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`create_time` datetime NOT NULL COMMENT '下单时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里 order_no 必须做唯一索引。建议生成规则是 yyyyMMddHHmmss + 用户ID后四位 + 随机四位,这样既方便按时间检索,也基本不会重复。
排期表的字段需要注意余位和锁定的区分:
sql复制`total_quota` int(11) NOT NULL COMMENT '总名额',
`sold_quota` int(11) NOT NULL DEFAULT '0' COMMENT '已售名额',
`lock_quota` int(11) NOT NULL DEFAULT '0' COMMENT '锁定但未支付名额',
实际可售余位计算是 total_quota - sold_quota - lock_quota。写 SQL 时不要直接存一个 remain 字段,因为每次下单、取消、退款都在变,直接用计算字段更不容易出现数据不一致。
4.3 为什么订单和排期要分开表
我见过一些设计图把“线路”和“排期”合并成一张表,一个字段存出发日期,一个字段存价格。这种设计演示单个线路时没问题,但运营人员如果想调整“10月1日出发的价格,但不影响11月1日的价格”,就只能把整条记录复制一份。这样做一段时间后,线路数据会膨胀得非常混乱,后台列表里满屏都是同一个线路名称的重复记录。
排期独立成表,另一层好处是能方便地做“班期状态”维护:比如运营人员可以单独把某个排期设置成“已满”或“临时加车”,不影响线路本身的上下架。这也是旅行业务和普通电商在数据模型上最本质的差异。
5. 后端核心接口和小程序联调实现
5.1 用户登录与 token 生成
小程序端登录的大致流程是这样:
javascript复制// 小程序端 app.js 或登录页
wx.login({
success: (res) => {
if (res.code) {
wx.request({
url: 'https://api.example.com/api/auth/login',
method: 'POST',
data: { code: res.code },
success: (resp) => {
const { token } = resp.data.data;
wx.setStorageSync('token', token);
}
});
}
}
});
后端 Controller 拿到 code 后,调用微信接口换取 openid:
java复制// 这里简化了 HttpClient 调用过程,实际用 Hutool 或 RestTemplate 均可
String url = "https://api.weixin.qq.com/sns/jscode2session"
+ "?appid=" + appId
+ "&secret=" + appSecret
+ "&js_code=" + code
+ "&grant_type=authorization_code";
String result = HttpUtil.get(url);
JSONObject json = JSONUtil.parseObj(result);
String openid = json.getStr("openid");
拿到 openid 后查 user 表。如果不存在则新建用户。然后生成一个 UUID 作为 token,存到 Redis 并设置过期时间,同时把 userId 一并存进去。后续每次请求通过拦截器解析 token,从 Redis 里取用户信息。如果项目没有引入 Redis,也可以把 token 存到数据库表里,只是过期清理要自己写,体验稍差一些。
5.2 线路列表和排期查询的接口设计
小程序首页需要展示“热门线路”,我建议接口返回的数据只保留卡片展示字段,不要一次把整篇富文本详情返回。否则首页加载会非常慢,而且小程序的包体积和渲染性能都经受不住大 JSON。
一个比较合理的列表接口返回结构:
json复制{
"code": 200,
"message": "success",
"data": {
"records": [
{
"routeId": 1001,
"name": "云南经典七日游",
"coverUrl": "https://cdn.example.com/image/xxx.jpg",
"tags": ["纯玩", "含机票"],
"minPrice": 3280
}
],
"total": 56,
"page": 1,
"size": 10
}
}
minPrice 字段不需要单独维护,可以在查询时用 SQL 关联排期表,求同一线路下最早可用班期的最低价格。小程序端直接展示即可。后端写这条 SQL 时要注意,GROUP BY 后只能查 MIN(adult_price),不能顺手把 schedule_id 也 select 出来,否则 MySQL 5.7 的 ONLY_FULL_GROUP_BY 模式会直接报错。
5.3 下单接口的并发安全和事务控制
下单是这个系统里最需要“讲道理”的接口,也是答辩老师最容易追问的地方。核心代码思路如下:
java复制@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long userId, Long scheduleId, List<Traveler> travelers) {
// 1. 校验排期是否存在且未关闭
// 2. 计算实际可售余位,用悲观锁或乐观锁控制并发
Schedule schedule = scheduleMapper.selectByIdForUpdate(scheduleId);
int available = schedule.getTotalQuota() - schedule.getSoldQuota() - schedule.getLockQuota();
if (available < travelers.size()) {
throw new BizException("余位不足");
}
// 3. 锁定余位
schedule.setLockQuota(schedule.getLockQuota() + travelers.size());
scheduleMapper.updateById(schedule);
// 4. 生成订单号,保存订单和出行人信息
// 5. 返回订单
}
selectByIdForUpdate 是行锁,可以保证两个用户同时下单时串行执行。如果不想用悲观锁,可以在更新语句里加 where lock_quota + ? <= total_quota - sold_quota,用一个 update 的返回值是否为 0 来判断是否更新成功。两种方案各有取舍,答辩时讲清楚一种就够。
事务加在 createOrder 方法上,保证“扣减余位”和“创建订单”要么同时成功,要么同时回滚。如果忘了加事务,就会出现订单生成了但余位没锁住的脏数据,演示时一旦出现,基本很难跟导师解释。
5.4 模拟支付与回调处理
毕设通常不接入真实微信支付(个人小程序和企业资质问题太麻烦),我一般建议做一个模拟支付页面:点击“确认支付”,前端调用 /api/order/pay 接口,后端直接把订单状态从“待支付”改为“已支付”,同时把锁定额转成已售额,记录一条支付流水。这样整个业务闭环是完整的,也不影响演示效果。
真实项目中支付回调必须是微信服务器调用后端接口,后端验签后才会更新订单状态。既然毕设不涉及,这部分建议在论文里写清楚“设计了回调接口”,但代码里用模拟逻辑占位,做好接口预留。
5.5 小程序端页面结构和请求封装
小程序端的核心页面建议控制在 8 个以内:首页、线路列表、线路详情、登录页、下单页、订单列表、订单详情、个人中心。页面多不代表系统强,反而容易让小程序体积膨胀编译慢。
请求封装是大家最容易忽视的问题。我习惯在 utils/request.js 里做统一拦截:
javascript复制const BASE_URL = 'https://api.example.com/api';
function request(path, method, data) {
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + path,
method: method,
data: data,
header: {
'Content-Type': 'application/json',
'token': wx.getStorageSync('token') || ''
},
success: (res) => {
if (res.data.code === 200) {
resolve(res.data.data);
} else if (res.data.code === 401) {
wx.navigateTo({ url: '/pages/login/login' });
} else {
wx.showToast({ title: res.data.message, icon: 'none' });
reject(res.data);
}
},
fail: (err) => reject(err)
});
});
}
module.exports = { request, get: (p) => request(p, 'GET'), post: (p, d) => request(p, 'POST', d) };
统一处理 401 过期跳转登录这步很关键。很多人联调时遇到“登录后请求仍然失败”,往往是 token 过期、没有统一跳转,用户根本不知道发生了什么。
6. 部署上线与演示准备
6.1 后端从本机到服务器的打包部署
毕设答辩用的系统,后端建议直接用 Jar 包部署到一台云服务器上,不要只在 IDEA 里点运行。万一答辩现场网络波动或电脑出问题,线上已经跑着的系统能兜底。
打包时几个注意点:
- 数据库连接配置放到
application-prod.yml,用环境变量或外部配置覆盖,不要把密码硬编码进去。 - MyBatis Plus 的逻辑删除和自动填充,在打包后依然有效。
- 如果用了
selectByIdForUpdate,注意 MySQL 的事务隔离级别,默认的REPEATABLE READ下没问题。
部署命令很简单:
bash复制nohup java -jar travel-platform.jar --spring.profiles.active=prod > app.log 2>&1 &
如果遇到“端口被占用”、“数据库连不上”、“时区报错”这老三样,优先看日志,其次是确认安全组是否放行了 8080 端口。
6.2 小程序端发布与真机调试
小程序开发工具里勾选“不校验合法域名”在本地很爽,但真机预览时必须把请求地址改成 HTTPS 域名,并且在小程序后台配置 request 合法域名。不要等到答辩前一天才去配置域名,ICP 备案和 SSL 证书可能耽误好几天。如果没有域名证书,可以考虑用云开发环境自带的 HTTPS 域名,或者直接真机调试模式下临时开启“不校验域名”来演示,但这不是长久之计。
小程序端我建议至少准备两台手机测试——一台安卓一台 iPhone,因为 iPhone 对 HTTPS、缓存、时间格式的兼容性和安卓不完全一致。我一个同学的项目就是后端返回 LocalDateTime 时间格式,安卓正常,iPhone 直接解析不出来,最后统一改成 yyyy-MM-dd HH:mm:ss 字符串才解决。
7. 踩坑实录与排查建议
7.1 高频异常一:支付成功后余位没扣减
这个问题几乎每个人都会遇到。排查思路三步走:先看订单状态是否更新,再看出行人数是否传到后端,最后看排期表的锁定余位和已售余位变化。很多情况下不是代码逻辑错,而是事务没有被触发——Service 方法被同类调用,@Transactional 失效,这是新手最容易忽略的细节。解决方法是用 @Autowired 注入自身代理,或者在写代码时把需要事务的公开方法独立出来。
7.2 高频异常二:小程序端报 request:fail 或 ERR_CONNECT 错误
原因基本都是域名问题和网络环境问题。如果本地开发工具里报这个错,先看开发工具设置里的“不校验合法域名”有没有勾上。真机预览时,看后端服务有没有对外开放,服务器安全组是否放行端口。个人建议是把后端服务的 server.servlet.context-path 统一加上 /api,这样小程序端的 BASE_URL 和本地联调时保持一致,少很多改路径的麻烦。
7.3 高频异常三:时间字段返回格式是一串数字
SpringBoot 默认用 Jackson 序列化 LocalDateTime 时,如果没配置格式,前端拿到的可能是时间戳数组,如 [2025, 5, 1, 10, 30, 0]。这个问题我在做接口联调时踩了很多次。解决方式是在配置文件里加上:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
同时实体类里的 LocalDateTime 字段加 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。两边双重保障,基本不会翻车。
7.4 建议至少准备一份演示脚本
毕设演示其实是一场 5-10 分钟的“业务故事讲述”,建议提前准备好测试账号和演示数据,线路至少放 6 条,排期覆盖“已满、有余位、即将出发”三种状态,订单至少准备一条“已完成待评价”记录。演示顺序从用户端浏览线路开始,下单支付,然后切到后台确认订单,再回小程序端看订单状态变化,最后提交一条评价。整条链路走完,系统的完整度自然就体现出来了。
我在实际带项目时还发现一个规律:答辩时老师问得最多的不是某个接口怎么写,而是“如果两个用户同时买最后一个名额怎么办”“订单取消之后余位怎么处理”“排期已满是否还能下单”。把这三道题答好,项目深度基本就被认定了。这也是我把余位锁定和状态机设计放在前面讲的原因——不是炫技,而是这些才是一个业务系统真正见功夫的地方。
这个项目后续如果想继续扩展,可以往分销返佣、电子合同、短信通知、线路收藏、会员积分这些方向加。底层的账号体系、订单体系、排期体系已经成型,扩展模块基本就是顺着现有结构再开表加接口的事。听完这些,你应该明白这类题目真正的价值不在“小程序能不能跑”,而在于你理解了多少业务约束,以及能不能把约束转化成合理的代码结构。
