FEATURED · 精选文章

Spring Boot旅游小程序后端开发:登录、景点与订单全流程解析

发布时间 / 2026/9/16 6:23:18
来源 / 创域科博编辑部
栏目 / 资讯中心
Spring Boot旅游小程序后端开发:登录、景点与订单全流程解析 简介这是一套基于Spring Boot与微信小程序的旅游类毕业设计/大作业源码包主要面向Java后端开发初学者、小程序学习者和正在准备相关课题的毕业生帮助解决从需求分析到功能落地中的典型难点。压缩包共有6个文件其中包含Java源码、Markdown说明文档以及多张运行界面截图整体体积仅897KB体量虽小但模块划分清楚。源码中实现了登录注册、景区展示、门票预订、酒店预订、订单结算、攻略介绍、旅游社交、目的地信息检索和关键字检索等模块技术栈涉及Spring Boot、MyBatis、MySQL、Redis与微信小程序基本覆盖旅游类互联网应用的常见业务链。目前已有100人学习借助源码、说明文档与运行截图可快速理解后端接口设计、数据库表关系以及小程序端页面调用逻辑也可对照截图检查项目运行效果。整套内容适合作为毕业设计、大作业的参考案例并可在现有功能之上继续扩展。1. 基于springboot的旅游小程序先把后端边界想清楚基于springboot的旅游小程序核心工作不在小程序那一端而在 springboot 后端能不能稳定输出一套接口微信登录、景点列表、详情、下单再往后是支付回调。搜索这个词的人大多数是拿它做毕设原型、景区轻应用或二手项目改造需求收敛成一句话登录能过、列表能刷、订单能存。这四个点通了项目就跑起来了。真正让这类项目后期难改的不是功能多少而是版本和分层。springboot 版本太高会踩 javax 迁移、mybatis-plus 不兼容、微信 sdk 引用失败的连环坑分层太薄则是不敢加新需求比如讲解员预约的根源。这里按我接手这类项目时的标准做法展开从 idea 创建 springboot 项目开始定依赖、配置、目录再写微信登录、景点查询、下单三条链路最后落到小程序端联调和部署排错。想把这个标题变成可运行代码的人照着往下走即可拿到现成 zip 的也能快速看懂目录、知道参数在哪改、上线前该检查什么。2. 技术选型与工程骨架springboot版本、依赖与目录规划这类项目最常见的坑不在业务代码而在脚手架阶段。先把 springboot 版本定住再谈依赖、配置和目录后面所有接口都长在同一套骨架上改起来才不会到处打架。2.1 springboot版本别追新2.7.x 在小程序后端仍是主流选择检索springboot版本太高出现频率已经说明问题springboot 3.x 强制 JDK 17把 javax.servlet 换成 jakarta.servletmybatis-plus 要升到 3.5.3.2swagger 得换 springdoc网上大量现成示例和毕设代码直接跑不起来。基于springboot的旅游小程序属于典型的中小型 API 服务没有虚拟线程、graalvm 这种新特性的强需求稳定优先。springboot 版本最低 JDK主要坑点适合场景2.5.148安全补丁少mybatis-plus 只能用 3.4.x老项目维护2.7.188无明显坑生态最成熟小程序后端、毕设、外包项目3.2.x17javax 迁移、国产组件兼容差新团队无历史包袱我的选择是 2.7.18这是 2.x 的最后一个维护版本依赖 mybatis-plus 3.5.5、jjwt 0.11.5 都验证过能一起跑。如果接手别人的 zip 发现 pom 里是 3.x先别急着降级只有 springfox swagger、老版 mybatis-generator 这类强依赖 javax 的组件才需要回退到 2.x否则动版本就是连环改代码。2.2 从 idea 创建 springboot 项目到最小可运行骨架idea 创建 springboot 项目最省事的路径是 New Project → Spring Initializr左侧选 Java 8、Spring Boot 2.7.18依赖勾 spring web、mysql driver、lombok、validation。mybatis-plus 和 jwt 这类第三方依赖不在初始列表里直接在 pom 里补parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency /dependencies这几项依赖按顺序说。spring-boot-starter-web 提供 springmvc 和内置 tomcat小程序端所有 http 接口都走它mybatis-plus-boot-starter 用 3.5.5对 springboot 2.7 的自动配置兼容最好不用额外写 mybatis 配置类jjwt-api 选 0.11.5网上抄的 0.9.x 代码风格旧builder 写法和 jjwt 自带工具类都不同混着用容易编译不过data-redis 用于缓存景点详情和登录态mysql-connector-java 在 2.7 里默认是 8.0.33注意 url 里不带时区会直接报错。接着是 springboot 配置 application.yml这是整个项目最容易被反复改的文件server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: travel-blog-demo-2024-change-me-please-32byte expire: 604800参数说明datasource.url 里 useSSLfalse 本地开发可以省掉证书校验生产走内网连接 MySQL 保持 false 问题也不大serverTimezoneAsia/Shanghai 是 MySQL 8 的硬性要求缺了直接抛时区异常。map-underscore-to-camel-case 打开后数据库字段 order_no 自动映射到 Order 实体的 orderNo否则每次查询结果对应字段全是 null。logic-delete-field 配 deleted 后mybatis-plus 会把删除自动变成 update deleted1查询也要用框架自带方法手写 sql 会把已删除数据查出来。jwt.secret 是这个文件里最容易踩的坑jjwt 0.11.5 要求密钥不少于 32 字节写个 short-secret 运行时会抛 WeakKeyException。2.3 目录分层与统一响应体目录按 controller / service / mapper / entity / common / config 六层切。小程序后端没有复杂领域逻辑切太细反而找文件费劲common 放统一响应体、业务异常和全局异常处理config 放 jwt 拦截器和 redis 序列化配置com.example.travel ├── TravelApplication.java ├── common │ ├── Result.java │ ├── BusinessException.java │ └── GlobalExceptionHandler.java ├── config │ ├── JwtInterceptor.java │ └── WebMvcConfig.java ├── controller │ ├── AuthController.java │ ├── SpotController.java │ └── OrderController.java ├── service │ └── impl ├── mapper └── entity统一响应体 Result 长这样Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.msg ok; r.data data; return r; } public static T ResultT fail(int code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } }code 约定 200 成功、401 未登录、500 系统错业务错误用单独编码比如 1001 表示库存不足。GlobalExceptionHandler 用 RestControllerAdvice 统一捕获 BusinessException 和参数校验异常转成 Result 返回controller 里就不用到处 try-catch。小程序端的 request 封装只需认 code 是否等于 200整条联调链路的面会小很多。3. 核心业务接口微信登录、景点查询与订单下单骨架定完开始写真正的业务。三条链路按依赖关系排先有登录态再有景点数据最后才有订单。每条链路都给出可抄的代码和参数边界。3.1 微信登录小程序用 code 换 token 的正确姿势微信小程序的登录链路在旅游类小程序里几乎一样wx.login() 拿一个临时 code 给后端后端拿 code 到微信服务器换 openid 和 session_key再用自己签发的 token 维持登录态。最忌讳的做法是把 appSecret 写进小程序前端打包后能被扒出来secret 只能放在 springboot 配置里。Service Slf4j public class WxAuthService { private final RestTemplate restTemplate new RestTemplate(); Value(${wx.appid}) private String appid; Value(${wx.secret}) private String secret; public String code2Session(String code) { String url https://api.weixin.qq.com/sns/jscode2session?appid appid secret secret js_code code grant_typeauthorization_code; String resp restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(resp); if (json.getInteger(errcode) ! null json.getInteger(errcode) ! 0) { throw new BusinessException(wx login fail: json.getString(errmsg)); } return json.getString(openid); } }code2Session 接口的四个参数里grant_type 固定 authorization_codejs_code 就是 wx.login 返回的一次性 code。code 有效期五分钟且只能用一次后端日志出现 invalid code 时优先怀疑小程序端 onShow 里重复调了 wx.login。返回的 openid 是用户在该小程序下的唯一标识user 表拿它做唯一键session_key 不要返回给前端它只用于后续解密手机号这类敏感操作。拿到 openid 之后的登录逻辑public LoginResult login(String code) { String openid wxAuthService.code2Session(code); AppUser user userMapper.selectOne( new LambdaQueryWrapperAppUser().eq(AppUser::getOpenid, openid)); if (user null) { user new AppUser(); user.setOpenid(openid); user.setNickname(游客 openid.substring(0, 6)); user.setCreateTime(LocalDateTime.now()); userMapper.insert(user); } String token jwtUtil.createToken(user.getId()); return new LoginResult(token, user); }先按 openid 查再决定 insert是幂等登录的关键否则同一用户每次进来都会多一条记录。jwtUtil.createToken 把 userId 放进 subject过期时间从 yml 读毕设场景 7 天够用上生产建议 2 小时并配 redis 续期。后续需要登录的接口在拦截器里解析 token 拿到 userId放进 ThreadLocal 供 service 使用比每个 controller 参数传 userId 干净也比在接口里重复查表省一次 IO。提示配置里一定要有 wx.appid 和 wx.secret 两个占位项不配或配错时报错是 401 而不是 500容易误判成 token 问题。3.2 景点分页与详情查询参数与缓存策略景点列表是旅游小程序流量最大的接口参数一般就三个pageNo、pageSize、keyword。列表接口永远不要返回景区介绍这种超长文本detail 接口单独给列表只回 id、名称、封面、星级、最低价首屏耗时能明显降下来。GetMapping(/page) public ResultIPageSpot page(RequestParam(defaultValue 1) long pageNo, RequestParam(defaultValue 10) long pageSize, RequestParam(required false) String keyword, RequestParam(defaultValue 0) Integer star) { LambdaQueryWrapperSpot wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Spot::getName, keyword) .eq(star ! null star 0, Spot::getStar, star) .orderByDesc(Spot::getHot); return Result.ok(spotService.page(new Page(pageNo, pageSize), wrapper)); }参数说明like 第一个参数是 boolean条件不成立时自动跳过不用手写 if 拼 sqleq 同理star 传 0 时不参与过滤。orderByDesc(getHot) 让热门景点排前面小程序端首次进入看到的列表质量直接决定这个功能去留。Page 对象直接返回mybatis-plus 自动带出 total 和 pages前端分页组件只看这两个值。keyword 为空时 like 条件失效查询退化为普通分页全程预编译不存在 sql 注入问题。详情接口加一层 redis 缓存按 id 缓存 30 分钟public Spot detailWithCache(Long id) { String key travel:spot:detail: id; Object cache redisTemplate.opsForValue().get(key); if (cache ! null) { return (Spot) cache; } Spot spot this.getById(id); if (spot ! null) { redisTemplate.opsForValue().set(key, spot, 30, TimeUnit.MINUTES); } return spot; }这里有一个参数细节set 的第三个参数 30 和第四个参数 TimeUnit.MINUTES 必须一起出现漏掉一个就是写入了永不过期的 key。key 用冒号分段如 travel:spot:detail:12方便在 redis-cli 里用 keys travel:spot:* 扫前缀排查。缓存穿透的简单处理是对不存在的 id 也缓存一个空对象并设 60 秒过期否则有人拿不存在的 id 连续请求redis 会被垃圾 key 塞满。3.3 下单与订单状态机下单接口是整套系统里唯一值得画状态机的地方旅游类订单常见五个状态表结构里直接放 status 整型字段比字符串稳定也省空间状态值含义触发动作前端展示0待支付用户提交订单去支付1已支付微信支付回调成功待使用2已取消用户取消或超时未支付已取消3已使用景区扫码核销已完成4已退款退款单处理完成已退款创建订单的 service 代码Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, Long spotId, Integer ticketTypeId, Integer count) { SpotTicket ticket ticketService.lockAndGet(ticketTypeId); if (ticket.getStock() count) { throw new BusinessException(1001, 库存不足); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setSpotId(spotId); order.setAmount(ticket.getPrice() * count); order.setStatus(0); order.setTicketSnapshot(ticket.getName() x count); orderService.save(order); ticketService.deductStock(ticketTypeId, count); return order; }几个容易忽略的参数和边界。ticketTypeId 对应库存必须在事务里用 select for update 锁住再判断否则高并发下会超卖mybatis-plus 的 selectById 拿到的是快照兜底逻辑要手写 update stock stock - #{count} where stock #{count}影响行数为 0 就回滚。amount 以服务器计算为准小程序端传的金额只能当展示参考绝不能直接入库。ticketSnapshot 把票名称和数量冗余进订单之后门票改价改描述不影响历史订单。generateOrderNo 用 yyyyMMddHHmmss 4 位随机数单机下够用分布式要加机器位。Transactional 保证保存订单和扣库存同生共死任一异常全部回滚。防重复提交是下单接口的隐性需求用户双击按钮会同时发两个请求进入 createOrder 前用 redis setnx 加锁key 是 userId spotId ticketTypeId成功才继续finally 里删锁并设置 2 秒过期比前端 disable 按钮可靠得多。4. 小程序端联调请求封装、登录时序与动态标题后端接口写完联调阶段的痛点在另一端每个页面裸写 wx.request401 要处理、loading 要手动关、公共 header 要重复写。这一章把小程序端三个最常用的改造点讲清楚。4.1 wx.request 封装与 token 处理标准做法是抽一个 request.js 统一收口所有页面只调封装后的方法// utils/request.js const BASE_URL https://yourdomain.com/api; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Authorization: Bearer (wx.getStorageSync(token) || ) }, success(res) { if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { wx.removeStorageSync(token); reject(res.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail(err) { reject(err); } }); }); } module.exports { request };参数说明header 里的 Authorization 是 Bearer 加空格加 token和后端拦截器的解析规则严格对应空格漏了会一直 401。code 200 时只把 data 字段 resolve 出去页面代码不用再 res.data.data 套三层。业务错误用 wx.showToast 统一提示每个页面省掉重复逻辑。BASE_URL 开发环境可指 http://localhost:8080真机调试必须换 https 域名否则 wx.request 直接报 url not in domain list这个报错在 4.2 里和登录问题并列第一高频。4.2 小程序端登录时序与 code2session 联调登录时序在小程序端就一个原则app.js 的 onLaunch 里发一次 wx.login拿到 token 存 storage后续请求靠 header 里的 token 维持。不要在 onLoad 里重复登录code 是一次性的第二次用必失败。// app.js App({ onLaunch() { this.login(); }, login() { wx.login({ success: (res) { wx.request({ url: https://yourdomain.com/api/auth/login, method: POST, data: { code: res.code }, success: (r) { if (r.data.code 200) { wx.setStorageSync(token, r.data.data.token); } } }); } }); } });wx.login 的成功回调里 res.code 就是后端的 js_code。这里不弹授权框wx.getUserProfile 是另一回事只在展示头像昵称时才调用且必须在用户点击事件里触发onLoad 里直接调会静默失败。联调时两个高频现象后端日志报 code 已被使用检查页面 onShow 是否又触发了一次 login报 appid 与 secret 不匹配检查小程序后台 appid 和配置是否一致测试号与生产号经常混用。联调阶段还有个建议后端 AuthController 的 /auth/login 接口不要挂拦截器否则前端还没拿到 token 就被 401 挡掉。拦截器只注册到 /api/order/** 这类需要登录的路径这就是拦截器要显式配置 excludePathPatterns 的原因。ant 匹配规则里 /api/** 和 /api/* 的区别也在这里体现前者匹配多层路径后者只匹配一层配错就会把详情接口漏放行或误拦截。4.3 动态设置标题与首页加载页小程序动态设置标题的接口是 wx.setNavigationBarTitle典型场景是详情页标题跟随景区名变化// pages/spot/detail.js onLoad(options) { const { id } options; request(/spot/ id).then((spot) { wx.setNavigationBarTitle({ title: spot.name }); this.setData({ spot: spot, loaded: true }); }); }onLoad 时页面还没数据拿到接口返回值后再调 setNavigationBarTitle不会闪跳不要用这个接口实现页面上方的自定义导航那部分要改的是 navigationStyle 配置。另一个关联点是刚进入页面的加载体验——不要用 wx.showLoading 扛所有接口景点首屏拆成列表先渲染骨架、图片懒加载wxml 里用 loaded 字段切换骨架屏和真实内容的 wx:if接口 500ms 内能回来就感知不到白屏。给一个小技巧页面跳转传参只用字符串object 参数会被序列化成 [object Object]。列表跳详情前用 JSON.stringify 把查询条件压成字符串传过去进入页面再 JSON.parse这个坑在景点列表到详情的跳转里基本必踩一次。5. 上线前检查小程序备案、HTTPS 与 springboot 配置排错项目能跑和能上线之间隔着一层配置检查。这一章把发布前最容易被卡的三件事集中收一遍。5.1 小程序备案与服务器域名配置小程序备案备注信息按实际经营范围填旅游类的写旅游信息展示、在线预订服务即可不要写商城这类会触发额外资质审查的词。备案完成后在微信公众平台的开发管理里配置 request 合法域名域名必须完成 ICP 备案且有有效 https 证书开发时用的 localhost 在这里不生效真机预览会直接拦截。5.2 nginx 转发与 400/401 定位后端跑在 8080线上常规做法是 nginx 挂在 443 上做反向代理小程序只认 https 域名server { listen 443 ssl; server_name yourdomain.com; ssl_certificate /etc/nginx/ssl/yourdomain.pem; ssl_certificate_key /etc/nginx/ssl/yourdomain.key; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 30s; } }配置说明location /api/ 与 proxy_pass 不带路径时是原样透传后端接口路由保持 /api 前缀小程序端 BASE_URL 不用拆。上线后遇到 401优先看 token 是否过期jwt.expire 单位是秒604800 是 7 天遇到 400多为 json 字段名不匹配或日期格式不对先看小程序端传参和后端实体字段是否一致再看 jackson 的 date-format 是否覆盖了 LocalDateTime——2.7 里 LocalDateTime 默认不按 yml 的 date-format 输出需要额外配 Jackson 的 JavaTimeModule。5.3 springboot 安全项与健康检查如果 pom 里引入了 actuatorspringboot 默认会把 /actuator/heapdump 暴露出来任何人访问就能下载堆内存里面可能有数据库密码和微信 secret。这类敏感信息泄露在 springboot 项目里是排查重点务必将 web 端点收窄management: endpoints: web: exposure: include: health,info这样设置后只有 /actuator/health 和 /actuator/info 可访问。把 health 端点挂进 nginx 的上游健康检查后端服务假死时负载均衡能自动踢掉实例比上线后靠用户截图报 bug 可靠这也是接手任何 springboot 项目时第一个该看的配置项。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻