去年帮学弟做的毕业设计,正好就是这套基于 SpringBoot 加微信小程序的旅行业务管理系统。说实话,一开始我以为这就是个普通的 CRUD 项目,真做起来才发现,旅行业务里产品、订单、支付、点评这些模块串到一起之后,要考虑的东西远比课本上多。这套系统最后完整跑通,小程序端上线体验版,后端部署在云服务器上,答辩一次通过。我把整个过程的设计思路、技术选型、模块拆分、关键实现和踩坑记录都整理在下面,给正在做类似选题、或者想了解旅行社智慧运营平台怎么落地的同学一个参考。
1. 项目整体设计与技术选型思路
1.1 这个系统到底要解决什么问题
传统旅行社的线上化一直有个尴尬:大平台抽成高、客户数据拿不到、营销活动也做不起来,自建App成本又太高,普通小旅行社根本扛不住。微信小程序恰好卡在这个位置上,用户扫码即用、不用下载安装,还能复用微信的登录和支付能力。这套系统做的,就是把旅行社最核心的业务搬到小程序上,让游客能查线路、看详情、下单、支付、写评价,让运营人员在后台管理产品、处理订单、看经营数据。
从功能范围来看,它不是一个全功能ERP,而是一条完整的业务闭环。游客端是轻量化的前台,强调“搜得到、看得懂、下得了单”;运营端则需要一个稳定的后台,至少能维护产品信息、上下架、修改库存、处理订单状态。再往后还能扩展优惠券、限时秒杀、线路收藏这些营销玩法。毕业设计做到这个程度,既能体现工作量,又不至于失控。
1.2 为什么偏偏是 SpringBoot 加微信小程序
选技术栈的时候,我也想过用 Vue 搭一个 PC 管理后台,再用 Uniapp 套小程序壳。但评估下来,SpringBoot + 微信小程序原生开发的组合对这个项目最合适。
SpringBoot 的优势不用多说,内嵌 Tomcat、自动配置、起步依赖齐全,写接口的效率比 SSH 时代高太多了。最重要的是它生态成熟,MyBatis-Plus、Sa-Token、微信支付 SDK 都有现成方案,遇到问题搜得到答案,这对毕设选手极其友好。另一个实际考虑是,绝大多数计算机毕设答辩老师对 SpringBoot 的认可度高,技术亮点容易讲清楚。
微信小程序这边,原生开发虽然有 WXML、WXSS 这类学习成本,但它和微信生态耦合最深,登录、支付、订阅消息的接入最顺畅。用 Uniapp 这类跨端框架写起来是爽,但一旦涉及原生组件、微信专属接口,还是会绕回条件编译那一套。原生开发对于旅行业务这种列表、详情、表单居多的场景,工作量完全可控。
1.3 整体架构和核心业务链路
系统的整体结构是典型的前后端分离:小程序端负责展示和交互,SpringBoot 后端提供 RESTful API,MySQL 存业务数据,MinIO 做图片和文件存储。部署时后端跑在云服务器上,通过 Nginx 把请求转发到 SpringBoot 服务,域名做 HTTPS,小程序端正式版只允许请求白名单里的合法域名。
业务链路上有几个关键流程必须设计清楚。登录链路是小程序端通过 wx.login 拿到临时 code,交给后端去微信接口换 openid,后端再签发自己的登录态令牌。下单链路是用户选好线路、填写出行人信息、生成订单、发起微信支付,支付回调通知后端后修改订单状态。评价链路则是在订单完成后,用户对该条线路打分评论,形成口碑闭环。这三条链路只要跑通,整个系统的主要价值就体现出来了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心模块拆解与数据库设计
2.1 功能模块一张清单
做毕设最忌讳一上来就写代码,先把功能边界划清楚,后面开发会顺畅很多。这个系统按角色可以拆成下面这些模块:
| 模块 | 游客端 | 运营端 |
|---|---|---|
| 用户管理 | 微信授权登录、个人信息查看 | 用户列表查询、账号状态管理 |
| 产品管理 | 线路搜索、分类筛选、详情查看 | 产品增删改查、上下架、库存修改 |
| 订单管理 | 创建订单、微信支付、取消订单 | 订单列表、状态流转处理 |
| 评价管理 | 订单完成后发表评价 | 评价审核、回复、删除 |
| 首页运营 | 轮播图、热门线路推荐 | Banner配置、推荐位维护 |
| 数据统计 | 个人订单统计 | 每日订单量、销售额、热门线路TOP5 |
表格一列出来就清楚,游客端做的是体验,运营端做的是管理。很多同学会把两边功能写成两套系统,其实在单体 SpringBoot 项目里,只需要在接口层做角色权限区分,同一套服务复用即可。
2.2 数据库表设计与建表理由
数据库是业务系统的地基,表设计不合理,后面写Mapper都要哭。我最后落地的表不算多,核心就是用户表、产品表、订单表、评价表、分类表、轮播图表这几张,再加一张支付记录表用来处理回调幂等。
用户表 sys_user 字段不多,但 openid 一定要加唯一索引。微信登录就是靠 openid 识别用户身份的,重复注册或者并发登录时,唯一索引能挡住脏数据。nickname、avatar 从微信资料里拿,注意别直接存微信返回的默认头像,有些默认头像在真机上会频繁变化。
产品表 travel_product 是核心业务表,字段设计上要区分“静态属性”和“动态属性”。静态属性像标题、封面图、目的地、行程天数、详情描述;动态属性是当前库存、已售数量、上下架状态。这种区分对后面做列表筛选和库存扣减都有好处。价格字段必须用 DECIMAL,比如 DECIMAL(10,2),绝对不能用 DOUBLE,否则金额在订单对账时会出现精度问题。images 字段我用的是 JSON 数组字符串,方便存多张轮播图,查询时再解析。
订单表 travel_order 是最关键的表,我在上面吃过亏。订单号 order_no 不能依赖数据库自增主键,必须单独生成,我用的规则是“yyyyMMddHHmmss + 6位随机数”,再加唯一索引。status 字段用 TINYINT 表示状态机,0 待支付、1 已支付、2 已完成、3 已取消、4 退款中、5 已退款,每个状态的变化都伴随一个时间字段记录。支付记录表 trade_pay_record 专门存微信支付回调的原始参数,不管回调发几次,都先查这个表做幂等判断。
2.3 表关系与业务闭环
表之间关系不算复杂:一个用户有多条订单和评价,一个产品被多条订单和评价引用,订单和评价是一对一关系,因为一笔订单只能评价一次,这也是我设计时特意用 order_id 做唯一索引的原因。
状态机的设计是整个订单模块的灵魂。从用户视角看,订单只有待支付、待出行、已完成、已取消;从运营视角看,系统内部还要处理退款审核、库存回补、评价解锁。我把订单表里的 status 定位成“业务可展示状态”,而把退款、核销之类的中途状态放在支付记录表和单独的日志表里,这样小程序前端拿到的状态永远是干净、可直接展示的,避免前后端对状态枚举各解释一套。
3. 微信小程序端从零搭建的实操细节
3.1 项目目录结构和初始化
微信开发者工具创建项目时,我选了 JavaScript 原生模板,没有用 TypeScript,原因是毕设阶段不需要那层类型约束,反而会让学弟调试时多绕弯路。目录结构上,我建议按“页面-公共组件-工具库”三个维度组织:
- pages 目录按业务域拆:home、product、order、user、comment
- components 目录放公共组件,比如产品卡片、空状态、加载动画
- utils 目录放请求封装、日期格式化、鉴权工具
- assets 目录放全局样式和静态图片
app.json 里需要配置 pages、window、tabBar。tabBar 我设置了三个入口:首页、订单、我的,这符合游客类小程序的基本习惯。注意 tabBar 的图标不能用网络图片,必须本地路径,而且尺寸有要求,否则编译直接报错。
3.2 请求封装:让每个页面少写几十行
小程序自带 wx.request,但直接裸用会有一堆重复代码:每次都要拼 baseURL、带 token、处理错误码、弹提示。我封装了一个 utils/request.js,统一收口所有请求。核心逻辑是返回 Promise,请求前自动带上存储里的 token,响应后统一判断 code,401 就跳到登录页,网络错误统一弹 toast。
javascript复制const BASE_URL = 'https://api.journey.com/api';
function request(url, method, data) {
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + url,
method: method || 'GET',
data: data || {},
header: {
'Content-Type': 'application/json',
'Authorization': wx.getStorageSync('token') || ''
},
success(res) {
if (res.statusCode === 200 && res.data.code === 0) {
resolve(res.data.data);
} else if (res.statusCode === 401) {
wx.removeStorageSync('token');
wx.navigateTo({ url: '/pages/login/login' });
reject(res);
} else {
wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' });
reject(res);
}
},
fail(err) {
wx.showToast({ title: '网络异常,请检查连接', icon: 'none' });
reject(err);
}
});
});
}
module.exports = { request };
封装好之后,页面里调接口只要一行:request('/product/list', 'GET', {page: 1}) 。这件事看起来简单,但很多同学不重视,结果每个页面几百行重复代码,后期改域名要改十几个文件,那个滋味谁改谁知道。
3.3 微信登录流程:从 code 到 token
小程序登录的核心不是用户手动输入账号密码,而是利用微信的临时登录凭证 code。流程是:小程序端调用 wx.login 拿到 code,传给后端;后端拿着 code 请求微信的 jscode2session 接口,换取 openid 和 session_key;后端根据 openid 查用户表,查到就更新资料,查不到就自动注册;最后后端生成新的登录令牌返回给小程序,小程序存到本地。
我这里建议在小程序启动时就完成静默登录,不要等用户点按钮。具体做法是在 app.js 的 onLaunch 里先检查是否有有效 token,没有就调 wx.login。用户看的是秒开的首页,背后其实已经完成了身份切换。代码大致是这样:
javascript复制wx.login({
success: (res) => {
const code = res.code;
wx.request({
url: BASE_URL + '/auth/login',
method: 'POST',
data: { code: code },
success: (res) => {
wx.setStorageSync('token', res.data.data.token);
}
});
}
});
后端在登录成功时同时返回用户的基本信息,小程序端可以顺带把头像昵称更新到缓存里。这里注意,wx.getUserProfile 接口现在只能通过用户主动点击按钮触发,不能在小程序启动时直接调用,所以首次登录拿到的昵称可能是“微信用户”,需要引导用户在个人中心手动完善。
3.4 列表页加载更多的正确姿势
旅行社小程序里线路列表是最常见的页面,如果不做分页,一次返回几十条数据,小程序首屏渲染就会卡。我用的方案是后端分页加小程序端 onReachBottom 触底加载。小程序端维护三个状态:page、hasMore、loading,每次触底时先把 loading 置为 true 防止重复请求,请求回来后追加数组。
javascript复制Page({
data: {
products: [],
page: 1,
pageSize: 10,
hasMore: true,
loading: false
},
onReachBottom() {
if (this.data.loading || !this.data.hasMore) return;
const nextPage = this.data.page + 1;
this.setData({ loading: true });
request('/product/list', 'GET', { page: nextPage, pageSize: this.data.pageSize })
.then((res) => {
const list = res.records || [];
this.setData({
products: this.data.products.concat(list),
page: nextPage,
hasMore: res.hasMore,
loading: false
});
});
}
});
这里有一个很多新手会踩的坑:直接用 this.data.products.push() 改数据。在小程序里,修改 data 必须走 setData,直接操作 this.data 不会触发视图更新,页面看起来就没反应。另外,上拉加载的同时最好给列表底部加一个“已经到底了”的提示,不然用户会一直触发加载请求。
3.5 真机预览与上线前配置
真机预览是毕设验收最紧张的环节,最容易出岔子的有三个地方。第一,正式版小程序必须配置请求合法域名,域名必须备案并且支持 HTTPS。开发调试期间可以在开发者工具的详情面板里勾选“不校验合法域名”,但真机预览如果不勾选且域名没备案,请求直接失败。第二,如果用到微信支付,必须绑定特约商户号并完成产品权限申请,个人主体的小程序无法开通支付能力,所以我当时在演示环境里用了一个模拟支付回调接口,用来完整演示订单状态流转。第三,提审前一定要看小程序类目是否匹配,旅游类小程序有些类目需要额外的经营资质,毕设如果是练习项目,提交体验版给答辩老师看就够了,没必要走正式发布审核。
4. SpringBoot 后端核心实现
4.1 后端工程结构与分层规范
后端工程我用的是 Maven 多环境配置,开发环境连接本地 MySQL,生产环境通过环境变量注入数据库地址。包结构按职责分:controller 只做参数接收和结果包装,service 处理业务规则,mapper 只写数据库操作,entity 对应表结构,common 放统一返回结果、异常处理、工具类。
统一返回结果这块我设计了一个 Result 对象,包含 code、msg、data 三个字段,code 为 0 表示成功。配合全局异常处理器,业务里只要抛自定义异常,前端就能拿到统一格式的错误信息。别小看这个设计,如果不统一,有的接口返回 {success: true},有的返回 {code: 200},小程序端封装就没法做。
控制器层还有一个容易忽略的细节:所有接口都要加统一的 /api 前缀,后续如果用 Nginx 做路径转发,一个前缀能省很多配置功夫。同时跨域问题也要在 SpringBoot 处理,我用了一个 CorsFilter Bean,允许所有来源,这在开发环境方便,生产环境再改成白名单。
4.2 集成 MyBatis-Plus 的配置与使用
MyBatis-Plus 是我在这个项目里用得最顺手的组件,它把单表 CRUD 的模板代码几乎消灭了。引入依赖后在启动类上加 @MapperScan 注解,所有 Mapper 接口都不用写 XML 就能获得基础的增删改查方法。分页需要单独配置分页插件,否则 Page 对象不会自动拼接 limit:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
实体类里我用了 @TableName 指定表名,全局配置 map-underscore-to-camel-case 自动把下划线字段映射成驼峰属性。创建时间和更新时间用 @TableField(fill = FieldFill.INSERT) 配合 MetaObjectHandler 自动填充,这样插入和更新时不用手动 set 时间字段,避免遗漏。
4.3 小程序登录接口的实现细节
后端登录接口是整个系统的身份入口,我建议用 RestTemplate 发起 HTTP 请求微信接口,逻辑足够清晰。核心代码是拼参数请求微信的 jscode2session 接口,拿到返回的 JSON,从中取出 openid:
java复制@PostMapping("/auth/login")
public Result login(@RequestBody LoginRequest request) {
String url = "https://api.weixin.qq.com/sns/jscode2session"
+ "?appid=" + appid
+ "&secret=" + secret
+ "&js_code=" + request.getCode()
+ "&grant_type=authorization_code";
String responseText = restTemplate.getForObject(url, String.class);
JSONObject json = JSON.parseObject(responseText);
if (json.getInteger("errcode") != null && json.getInteger("errcode") != 0) {
throw new BusinessException("微信登录失败");
}
String openid = json.getString("openid");
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>().eq(User::getOpenid, openid));
if (user == null) {
user = new User();
user.setOpenid(openid);
user.setNickname("旅行者" + openid.substring(openid.length() - 4));
userMapper.insert(user);
}
String token = JwtUtil.createToken(user.getId(), user.getNickname());
return Result.success(token);
}
这里有几个容易踩的坑。微信返回的 JSON 里如果出现 errcode,说明 code 已经失效或参数错误,一定要处理错误,不要盲目往下走。生产环境的 appid 和 secret 不要硬编码在代码里,放到配置文件中并通过环境变量覆盖。JWT 过期时间我设的是 7 天,小程序端每次启动请求时如果发现 401,就重新走一遍 wx.login。
4.4 订单创建与支付回调处理
订单创建接口要注意事务,一次操作要完成订单插入、库存扣减、购物车清理等多个动作,任何一步失败都要整体回滚。库存扣减我用的是乐观锁,在 travel_product 表加一个 version 字段,扣减时先查当前版本号,再通过 UPDATE ... SET stock = stock - 1, version = version + 1 WHERE id = ? AND version = ? 更新,受影响行数为 0 就说明超卖,直接提示用户库存不足。
微信支付回调是这个项目里对代码功底要求最高的部分。回调通知是一个 HTTPS 请求,微信会带上签名和报文数据,后端要做四件事:验签、解密、幂等判断、更新订单状态。验签可以借助微信支付 SDK 的 WxPayService 工具类。解密后拿到订单号和实付金额,先查本地订单,核对金额一致,再查支付记录表有没有处理过该订单号,处理过就直接返回成功,避免重复更新。最后更新订单状态、生成支付记录。整个逻辑必须用事务包裹,一旦中间出现异常,微信会持续重试回调,这反而是好事,能保证最终一致。
4.5 文件上传与 MinIO 接入
旅行产品的图片和详情介绍里的图片不能存数据库,之前我最开始用的是把图片转 Base64 塞进字段,结果一个详情页几张图就把数据库打爆了。后来换成 MinIO 做对象存储。MinIO 是一个开源的轻量存储服务,兼容 S3 API,部署也很简单:
bash复制docker run -d --name minio \
-p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
-v /data/minio:/data \
minio/minio server /data --console-address ":9001"
SpringBoot 接入 MinIO 只需要在配置类里初始化 MinioClient,再写一个上传方法。上传后拿到的对象 key 返回给前端,前端拼出访问 URL 展示。注意存储桶的权限策略要设成公开读,否则小程序端图片无法加载。我当时是在 MinIO 控制台新建了 travel-images 桶,并配置了匿名只读策略,这样所有上传图片都能通过 http://服务器IP:9000/travel-images/xxx.jpg 直接访问。上线后这个地址要通过 Nginx 反代到 HTTPS 域名下,不然小程序正式版因域名白名单限制还是加载不了图片。
5. 踩坑记录与问题排查技巧
5.1 高频问题速查表
开发周期里我记录了一批高频问题,整理成速查表,很多问题在答辩演示前才暴露,提前对照检查能省不少事。
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 小程序请求一直超时 | 服务器安全组未放行 8080 端口 | 在云控制台放行对应端口,或改用 Nginx 转发 80/443 |
| 登录接口报“invalid code” | code 只能使用一次,重复提交 | 前端保证一次登录只传一次 code,后端做缓存 |
| 图片不显示 | 存储桶权限为私有 | 在 MinIO 控制台设置匿名只读策略 |
| 分页数据一直重复第一页 | 分页插件没配置 | 增加 MybatisPlusInterceptor 分页插件 |
| 支付回调一直报验签失败 | 密钥配置错误或证书过期 | 核对商户号和 API v3 密钥 |
| 真机预览白屏 | 未配置合法域名 | 开发者工具详情里勾选不校验,或在小程序后台加白名单 |
| 数据库写入中文乱码 | 连接串未指定编码 | JDBC URL 增加 characterEncoding=utf8 |
| 修改小程序代码没生效 | 缓存了旧编译包 | 清除缓存并重新编译 |
5.2 一次完整排查:订单列表接口 500
说一个我印象最深的排查过程。小程序端订单列表页面刚接上后端时,一进来就报 500,后端日志显示 SQL 语法错误。排查步骤是先在数据库客户端手写了一遍查询 SQL,发现没问题;再看 MyBatis-Plus 生成的 SQL,发现 ORDER BY 后面多了一个空条件,原因是排序字段传参是 orderBy,结果后端接收参数名写成了 sortField,参数没传进来,拼接了空字符串。改成 @RequestParam(value = "sortField", required = false) 后,问题解决。
这类问题很典型,前后端参数命名不一致在联调时频繁出现。我的习惯是提前定义一份接口文档,哪怕是简单的 Markdown 表格,也要把每个接口的请求参数、响应结构写清楚。前后端各拿一份,联调效率至少提升一倍。答辩的时候老师问接口设计,也能直接拿出文档说事。
5.3 答辩演示前的整体检查清单
毕设不是写完就完,还要能现场演示。根据我陪学弟演练的经验,演示前建议按这个顺序检查一遍:
连接数据库的服务有没有启动,云服务器上的后端进程是否还活着,最简单的方式是直接访问一个接口看返回。小程序端用开发者工具编译前,确认当前用的是测试号还是真实 AppID,测试号不支持支付类接口。本地开发如果连的是远程数据库,记得看云数据库的访问白名单是否包含当前出口 IP。演示前把小程序切到体验版,在真机上完整走一遍登录、浏览线路、下单、模拟支付、评价的流程,不要只停留在开发者工具里。最后一个细节是提前把后端日志级别调成 INFO,演示时一旦报错能在控制台快速定位,不至于全场尴尬。
6. 从毕设到真实运营,还差哪几步
很多同学做完毕设就停了,其实这套系统再往前走一步,就是一个小旅行社真正能用的智慧运营平台。首先需要补的是运营后台的完整权限体系,现在接口层虽然有角色判断,但没有真正的菜单级权限管理,需要引入 Sa-Token 这类框架完善。其次是数据看板,我现在只是做了简单的订单量统计,真实的运营需要按日、周、月维度的销售趋势图表,这个可以用 ECharts 在前端渲染。第三是消息触达,微信小程序的订阅消息功能可以在订单状态变化时通知用户,这个非常实用。
我自己在这一套系统里花得最多的时间不是写代码,而是调整业务细节,比如订单取消后库存回补的时机、支付回调超时对账的处理、图片资源清理策略。这些细节在课本上都不讲,只有真正跑业务才会遇到。如果你正在做类似的平台,我建议先把这些边界情况列出来,宁可功能少一点,也要保证核心链路在各种异常情况下不崩。
我个人实际操作中的体会是,毕业设计最怕的不是技术难,而是需求模糊。一开始就把“用户能做什么、管理员能做什么”写清楚,数据库设计就不会跑偏,代码写起来也就顺了。这套旅行平台从零到落地用了不到六周,实际编码时间大约四周,剩下的时间全在联调和排错。希望这份记录能让你少走点弯路,把精力放到真正能展示技术深度的地方去。
