SpringBoot+微信小程序景区门票售卖系统实战:从设计到部署
做毕业设计选题目是个挺让人头秃但又很有意思的过程。我当时挑来挑去最后敲定了“SpringBoot旅游景区门票售卖微信小程序”这个方向原因很直接它既有一个明确的业务场景也把后端开发里最常用的一整套技术栈串明白了。跑通这个项目之后前端、后端、支付、缓存、防并发、数据可视化全都过了一遍答辩的时候心里特别有底。这套组合放到现在依然稳——SpringBoot负责出接口微信小程序负责移动端展示和卖票业务逻辑写在中间层数据落在MySQL热点数据和库存用Redis顶住。它能解决的真实问题是让游客在微信里完成从浏览景区、选择票种、下单支付到园区核销的全流程省掉现场排队买票的麻烦。如果你准备做Java方向的毕业设计或者想快速攒一个能写在简历上的全栈项目这篇里的思路和代码完全可以照着改。1. 项目整体设计与思路拆解1.1 为什么选SpringBoot加微信小程序这个组合我先看了市面上一圈毕业设计的技术路线。传统JavaWeb是JSP加Servlet得自己手写一堆Servlet和过滤器步骤繁琐不说前后端混在一起后期改起来想哭PHP方案开发速度快但等毕业出来找工作时Java岗位的需求量明显更大也有人用Python Flask或者Django做生态也不错但终归不是Java系的。对比下来SpringBoot加微信小程序这条路两头都占SpringBoot让后端工程像开了挂一样轻量内嵌Tomcat、自动配置、起步就是爽微信小程序则是天然贴近真实用户场景不用下载App扫码或搜索就能打开微信支付、登录、模板消息这些能力官方都有现成的封装对学生项目来说接入成本完全能接受。标题里那句“可做计算机毕业设计JAVA、PHP、爬虫、APP、小程序、C#、C、python、数据可视化”说白了就是同一个业务题目在不同语言下的变体。别被这些词绕晕核心永远是业务加工程能力两件事。Java加SpringBoot是目前认可度最高、资料最多、最容易落地的组合就算你是零基础搜问题也搜得到答案这就是我推荐先做这套的原因。1.2 功能模块怎么拆才像个正经项目很多同学上来就写Controller写着写着发现页面和接口对不上返工好几次。正确做法是先画功能模块图把边界划清楚。用户端的核心链路是浏览景区、查看门票、下单支付、查看订单、出示核销码。围绕这条主链路我拆成了首页轮播、景区列表、景区详情、门票详情、订单确认页、订单列表、核销码页这么几个页面。管理端则是景区信息维护、门票库存管理、订单管理、经营数据统计管理员可以登录后台对基础数据进行增删改查。这个拆法背后的逻辑很简单用户端永远是销售入口管理端永远是数据出口。门票从创建到下架、订单从待支付到已核销整个过程在两端都有对应操作。模块图一旦清晰表结构设计、接口命名、前端页面规划也就顺理成章了。我在第一版就吃了没拆模块的亏后来花了一晚上重新整理才把项目掰回正轨。1.3 技术选型与答辩加分项怎么配技术选型我按毕业设计最容易踩坑的场景来配核心原则是资料多、装环境坑少、答辩老师熟悉。后端我用SpringBoot 2.7.xORM选MyBatis-Plus数据库用MySQL 8.0缓存和库存扣减交给Redis。文件存储这块本地磁盘方案在服务器重启后容易丢所以用了MinIO做对象存储这也是当前企业里很常见的做法。管理端有两种路子一种是单独搭一个Vue后台另一种是把管理功能直接做进小程序里用角色字段区分用户和管理员。我图省事选了后者一个AppID里搞定答辩演示时也方便。加分项我加了两个一是管理端用ECharts做销售数据可视化柱状图、折线图一摆页面逼格直接上来二是用Python写了一个爬虫脚本把公开的景区信息定时抓进数据库用来填充演示数据。前提是只抓合法来源的公开数据并且控制频率这个大家在引用时要自己把关。这两个点面试和答辩时都很容易被追问属于投入产出比特别高的部分。2. 核心细节解析与实操要点2.1 数据库设计五张表撑起一个业务闭环项目用了五张核心表分别是用户表、景区表、门票表、订单表和核销记录表。用户表记录openid、昵称、头像、手机号、角色等景区表存景区名称、简介、位置、封面图门票表存所属景区、票种名称、价格、库存、已售数量、上下架状态订单表存订单号、用户ID、票品快照、数量、金额、状态、核销码核销记录表则是游客入园时扫码的记录关联订单和操作员。这里有一个特别容易被忽略的点订单表和门票表之间不要只存一个外键一定要做票品快照。什么意思用户下单那一刻把当时买的票种名称、价格、景区名称直接冗余一份到订单表里。因为门票价格和名称是可以被管理员改的如果订单只关联ID一旦价格变动历史订单的金额就对不上了。线下买过票的都知道小票上印的价格就是以当时支付为准这就是快照的思路。CREATE TABLE tb_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL, ticket_id BIGINT NOT NULL, scenic_id BIGINT NOT NULL, ticket_name VARCHAR(64) NOT NULL COMMENT 票品名称快照, scenic_name VARCHAR(64) NOT NULL COMMENT 景区名称快照, price DECIMAL(10,2) NOT NULL COMMENT 单价快照, quantity INT NOT NULL DEFAULT 1, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已核销 3超时关闭 4已退款, verify_code VARCHAR(8) DEFAULT NULL COMMENT 核销码, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, expire_time DATETIME NOT NULL COMMENT 订单过期时间, KEY idx_user (user_id), KEY idx_order_no (order_no) ) COMMENT 订单表;字段设计上还有一个点订单状态建议用数字枚举而不是字符串。数据库里存0、1、2、3、4这样的数字代码里定义一组常量去映射不要直接在业务代码里散落写“待支付”“已支付”这些中文否则后期改文案时得翻遍全项目。2.2 微信登录从code到用户体系的一次握手微信小程序里没有传统意义上的账号密码用户身份靠openid来识别。流程是这样的前端调用wx.login()拿到一个临时code把这个code传到后端后端拿code去微信服务器的code2Session接口换取openid和session_key。openid是用户在某个小程序下的唯一标识同一个用户在不同小程序里的openid也不同。我在这里犯过一个典型错误一开始偷懒让前端把openid直接传到后端来识别用户结果别人在请求里把openid改成别的就等于是别人的身份了。后来老老实实改成后端拿到code换到openid之后查用户表没有就自动注册然后生成一个自定义登录态token比如UUID存入Redis并设置过期时间再把token返回给前端。前端后续请求都带这个token后端拦截器统一校验。PostMapping(/api/auth/login) public Result login(RequestBody LoginRequest req) { String code req.getCode(); // 调用微信接口换取openid和session_key WxSession session wxService.code2Session(code); if (session null) { return Result.error(登录失败请重试); } User user userMapper.selectByOpenid(session.getOpenid()); if (user null) { user new User(); user.setOpenid(session.getOpenid()); user.setNickname(微信用户); user.setRole(1); // 1普通用户 2管理员 userMapper.insert(user); } // 生成登录态token String token UUID.randomUUID().toString().replace(-, ); redisTemplate.opsForValue().set(token: token, user.getId().toString(), 2, TimeUnit.DAYS); return Result.success(token); }看起来简单但这里有三个坑第一code只能使用一次而且有效期很短前端不能缓存第二生产环境必须配小程序AppSecret这个密钥绝对不要放在前端代码里第三登录态过期后前端要能自动重新登录不要每次都强行跳转到登录页打断用户体验。2.3 门票售卖的核心流程与订单状态机门票售卖整个流程我建议用一个状态机来理解。订单从创建开始有四个主要状态待支付、已支付、已核销、超时关闭。用户选好票提交订单此时状态是待支付同时调用微信支付接口拉起支付支付成功后微信服务器会异步通知后端回调地址后端把订单改成已支付用户到景区后出示核销码工作人员扫码核销订单变成已核销如果用户一直不支付订单到期自动关闭。这个状态机最关键的一点是支付状态以谁为准。实际支付是否成功永远以微信支付服务端的回调为准不能只看前端传回来的结果。我在开发时就遇到过用户明明没支付前端伪造了一个成功回调过来差点把订单状态改掉。所以后端回调接口里必须验签确认是微信服务器发来的消息才处理。超时关单也有两种做法一种是后端定时任务扫描超过15分钟未支付的订单统一关闭另一种是用户在查询订单时发现已过期就顺手关闭。我建议两个都写定时任务保证不漏查询时关闭保证及时。2.4 库存与超卖问题Redis在这里派上用场门票销售最怕超卖。两个人同时买最后一张票如果代码先查库存再判断库存大于0再更新在并发下两边都可能查到库存是1然后都扣减成功票就超卖了。数据库层面加个where stock 0能缓解一部分但在高并发场景下还是不够稳妥。毕业设计阶段不一定要上分布式事务但至少要能说出思路。我的方案是下单前用Redis的Lua脚本原子扣减库存。Redis的单线程模型保证了脚本执行过程中不会被其他请求打断所以扣减操作天然是原子的不会出现两个请求同时扣到同一张票。local stock tonumber(redis.call(get, KEYS[1])) if stock nil then return -1 -- 缓存不存在回源数据库 end if stock 0 then return 0 -- 库存不足 end redis.call(decr, KEYS[1]) return 1用户支付成功后再异步把Redis扣减的库存同步到MySQL里真正扣减支付超时或关单时再把库存回补。这套方案的好处是把库存扣减的粒度控制在了下单那一刻而不是支付成功时才扣因为用户一旦占住了票其他人就买不到了这也符合真实景区卖票的逻辑。3. 实操过程与核心环节实现3.1 环境准备清单开始写代码之前先把环境配齐不然中途缺一个工具很容易卡住。我列一下当时我用的版本组合踩过坑的都在括号里标好了JDK 1.8SpringBoot 2.7.x和JDK8是绝配别图新鲜上JDK17坑多Maven 3.6以上用阿里云镜像下载依赖能快一半MySQL 5.7或者8.05.7兼容性更好8.0功能更强按自己机器选Redis 6.x做缓存和库存扣减要用微信开发者工具最新版就行小程序AppID注册个个人或者企业小程序个人主体也能做开发演示环境配置有一个很常见的坑SpringBoot版本太高导致JDK8启动不了。热词里就有人搜“springboot版本太高”我建议直接锁死2.7.x这个版本线配合JDK8稳定得一批。别小看环境问题当时我在实验室帮同学配环境光版本冲突就折腾了两个小时最后统一用2.7.18才消停。3.2 后端骨架搭建与关键接口实现后端我用Spring Initializr创建工程主要依赖就这么几个spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、spring-boot-starter-data-redis、lombok再加上微信SDK。如果用weixin-java-miniapp这个开源库可以省去很多对接微信的麻烦。application.yml文件里核心是数据源、Redis和MinIO的配置。记得数据库连接串里加时区参数比如serverTimezoneAsia/Shanghai不然时间对不上。让我贴一个配置片段后面照着写就行spring: datasource: url: jdbc:mysql://localhost:3306/ticket_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 server: port: 8080 minio: endpoint: http://127.0.0.1:9000 access-key: minioadmin secret-key: minioadmin bucket: scenic-images关键接口我按业务主链路来排。登录接口上面写过然后是门票列表接口支持分页、按景区筛选、关键词搜索用MyBatis-Plus的Page直接查再就是创建订单接口逻辑是校验门票是否存在且上架、校验库存然后生成订单号、落库之后调用微信支付参数接口返回支付参数给前端。创建订单的代码要注意一个点订单号不要用数据库自增ID要自己生成格式像20250607 时间戳 随机数这样前端和后台都好用。3.3 小程序端页面与交互要点小程序端我规划了四个tabBar首页、订单、商城入口也可以直接叫景区门票、我的。首页放轮播图、景区推荐位列表页负责展示全部景区加搜索详情页展示景区介绍和可选票种订单页是列表和订单状态切换我的页面放个人信息和售后入口。一个很容易被问到的细节是“微信小程序页面列表加载更多”。这个功能我建议用onReachBottom这个页面生命周期方法当滚动到底部时自动触发下一页加载。前端要维护一个page和pageSize每次加载完递增页码接口返回的数据追加进数组而不是整体覆盖。做过前端的人都知道直接把返回数据setData覆盖原数组会导致列表闪一下回到顶部体验很差。onReachBottom() { if (this.data.hasMore !this.data.loading) { this.setData({ loading: true, page: this.data.page 1 }); this.loadList(); } }, loadList() { wx.request({ url: https://api.example.com/api/ticket/list, data: { page: this.data.page, pageSize: 10 }, success: (res) { const list this.data.list.concat(res.data.list); this.setData({ list: list, hasMore: res.data.hasMore, loading: false }); } }); }分页加载这个功能虽然简单但答辩时几乎必问它考察的是对移动端交互细节的把控能力值得提前练熟。3.4 MinIO图片存储与上传链路景区图片上传是个绕不开的需求。一开始我用的是本地磁盘存储图片直接扔到服务器目录下后来发现两个问题一是服务器重启时临时目录可能被清空二是小程序真机访问图片地址必须走HTTPS可信域名本地地址根本加载不出来。后来我换成了MinIO自建对象存储开箱即用还免费。SpringBoot集成MinIO只需要加一个minio依赖然后写一个工具类封装上传、下载、生成临时URL的操作。上传接口的逻辑是小程序先用wx.chooseImage选图然后把图片二进制流传给后端后端再将文件流转存到MinIO最后把文件URL存进数据库响应给前端展示。public String uploadFile(MultipartFile file) throws Exception { String fileName UUID.randomUUID().toString() file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(.)); minioClient.putObject( PutObjectArgs.builder() .bucket(scenic-images) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); return endpoint /scenic-images/ fileName; }这里有一个经验图片上传前在小程序端一定要做压缩。wx.chooseImage里有个sizeType参数设置为[compressed]上传原图的话一张动辄好几MB接口传输慢后端存储也浪费。3.5 联调、部署与答辩演示准备本地联调阶段微信开发者工具有一个很省事的选项勾选设置里的“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”这样工具里就能直接请求http://localhost:8080不用先去买个域名再配HTTPS。但是真机预览就麻烦了手机上的小程序访问后端必须走HTTPS请求域名还得在小程序后台的request合法域名里加白名单。所以生产或者演示环境要么用一台有公网IP的云服务器配上域名和证书要么用一些内网穿透工具临时映射出来。我当时的做法是租一台最便宜的云服务器域名套一个HTTPS证书后端用java -jar直接跑起来Nginx转发一下请求算是最标准的部署方式。答辩之前一定要做一次完整演示演练手机开飞行模式再关掉确认断网重连不影响登录态准备一套干净的演示数据不要把开发过程里的脏数据都留在页面上支付模拟通道提前测一遍确保流程能走通。别小看这些准备工作我见过不少同学因为演示时网络卡顿、数据错乱项目本身做完了却减了印象分。4. 常见问题与排查技巧实录4.1 登录与鉴权问题登录失败是最早会遇到的问题。AppID填错了、AppSecret不对、code已经过期都会导致登录接口报错。排查思路是先看前端wx.login是否返回了code再看后端调用微信接口有没有报错最后看数据库用户表有没有成功写入。这里有一个调试技巧把微信接口返回的原始JSON先打出来看一眼很多问题是参数名拼错导致的。token过期的场景也要提前设计好。我的做法是后端拦截器里校验token发现过期就返回401前端拿到401后清除本地缓存并重新调用wx.login换取新token。另外前后端分离的项目一定别忘了配跨域CORS否则浏览器同源策略直接拦下所有请求自己写一个实现WebMvcConfigurer的配置类允许所有来源和请求头就行。4.2 支付回调与验签问题支付回调阶段踩坑的概率最高。第一个坑是验签失败通常是证书序列号、私钥格式、回调签名参数顺序不一致造成的。微信支付API v3的验签逻辑稍微有点绕但照着官方示例一步一步拆就没问题。第二个坑是回调地址外网连不通本地开发时微信服务器无法访问你的localhost要么部署到服务器要么用内网穿透工具把本地端口暴露出去但穿透工具只是临时方案正式演示还是老老实实上服务器。最容易被忽略的是回调接口的幂等性微信支付可能会重试多次回调同一个订单可能收到多条相同或延迟的消息。处理方式是回调里先查订单状态如果已经是已支付就直接返回成功不再重复修改不然两个回调同时改状态会出现二次扣减库存或者重复发核销码的问题。4.3 并发与数据一致性问题超卖和重复下单是并发问题的两个典型代表。超卖的解决方案前面写了Redis Lua脚本扣减库存重复下单则需要前端和后端双重防护。前端在下单按钮点击后立刻置灰并加loading防止用户手快连续点后端在创建订单时检查同一个用户是不是已经有一笔待支付订单如果有就直接拒绝或者提示去支付旧订单。一个实际项目中的隐蔽坑是支付回调先于用户查询请求到达导致前端页面还显示待支付实际上订单已经支付成功了。我在开发中就遇到过用户支付成功后回到订单页因为没刷新数据页面还停留在待支付状态但下一秒再点支付就报订单不存在。解决办法是订单查询接口以后端状态为准前端不要完全信任本地缓存的状态。4.4 图片显示与前端显示问题图片相关的坑主要集中在真机显示不出来。如果开发工具里图片正常真机白屏先检查图片域名有没有加入小程序后台的downloadFile和request合法域名白名单。另外开发者工具里如果设置了不校验域名可能在模拟器看起来正常但真机就是不行这个要提前心里有数。上传图片时如果接口很慢大概率是原图太大。wx.chooseImage的sizeType改成[compressed]之后图片体积会明显下降。如果是MinIO权限配置不对比如bucket有私有权限那么生成的URL需要签名或者改成公读否则前端永远拿不到图。MinIO的bucket权限我建议直接设成公读因为景区图片本身没有机密性省事。4.5 Controller层接口防护与防爬虫热词里有个搜索是“java controller层 如何防护 防止爬虫”说明这个点大家确实关心。景区门票系统是公开查票、登录下单的模式所以特别容易被人用脚本批量刷接口。我上了这么几个防护成本低但有效IP限流用Redis做一个固定窗口计数器同一IP一分钟请求超过比如100次就拒绝。接口鉴权除了登录接口所有接口都走拦截器校验token。参数签名前端请求携带时间戳加签名比如把参数按字典序拼接后MD5后端校验签名和时效防止参数被篡改。高频接口加验证码比如登录接口和下单接口失败次数多了弹滑块验证码。GetMapping(/api/ticket/list) public Result list(RequestParam Integer page, RequestParam(required false) String keyword, HttpServletRequest request) { String ip request.getRemoteAddr(); // 简单IP限流每分钟最多60次 Long count redisTemplate.opsForValue().increment(limit:ip: ip); redisTemplate.expire(limit:ip: ip, 60, TimeUnit.SECONDS); if (count 60) { return Result.error(请求过于频繁); } // 正常业务逻辑 return Result.success(ticketService.pageQuery(page, keyword)); }注意毕业设计阶段这些防护点到为止就行不用做成一整套微服务网关。但答辩老师如果问“怎么防止别人爬你数据、刷你接口”能流利说出这几种方案明显比只会写CRUD强很多。最后聊聊我做完这个项目的实际感受。最值钱的收获不是把SpringBoot和小程序跑通而是把一条真实的业务链路从下单到支付再到核销完整地走了一遍这个过程里踩过的每一个坑都比顺利运行时学到的东西更多。如果你也准备做类似的题目我建议先把主流程跑通再加可视化、爬虫这些花活答辩前多想想异常场景比如超卖、重复支付、接口被刷这些才是答辩现场真正拉开差距的地方。等主流程稳了可以继续往上加多景区联票、会员积分、游客画像分析往深了做足够当一个中期项目了。