
简介面向毕业设计及课程设计场景这套基于微信小程序的智慧乡村旅游服务平台源码完整实现管理员、用户、商家三类角色权限闭环用户端可完成旅游景点与景点路线浏览、购物车、充值、收藏及已发货订单管理后台端覆盖旅游景点、景点类型、路线、订单、系统管理等模块商家侧承担景点与订单的运营维护。压缩包共1178个文件约17.01MB包括vue、js、wxml、wxss等小程序与后台前端代码Java后端工程sql数据库脚本以及png、svg等界面素材并附有install/run/build批处理脚本目录结构清晰便于按端定位功能。已有86人浏览学习。下载后可直接对照搭建环境梳理前后端交互、数据库表设计与三类角色的权限划分尤其适合需要快速完成小程序JavaMySQL整合开发与论文配套演示的本科毕业生、高职学生使用。1. 智慧乡村旅游服务平台这套毕设先说清楚它解决了什么问题微信小程序加 Java 加 MySQL这三个词凑在一起基本就是每年毕业设计里最常见的“三件套”。这套智慧乡村旅游服务平台的源码正好把三样东西串成了一个完整闭环游客在小程序里浏览景点、看民宿、下单预订后端用 Java 写接口管数据MySQL 把用户、景点、订单全部落库。它解决的不是某个炫技问题而是一套能演示、能答辩、能往下扩展的小程序全栈项目从微信登录到订单状态流转都有现成实现。适合三类人正在选毕设题目的学生、需要课程设计源码的初学者以及想把前后端联调完整走一遍的开发者。这篇笔记我按拆项目的思路来写从数据库表结构讲到小程序页面渲染中间穿插实际踩过的坑。2. 模块拆解与数据库设计从业务边界到 MySQL 表结构落地2.1 功能模块的三类角色边界拿到源码先别急着跑先看业务边界。这套平台按角色分三块游客、商家、管理员。游客端的入口是小程序功能围绕“浏览—决策—下单—售后”展开景点列表、景点详情、民宿列表、在线预订、订单查询、收藏、评论。商家和管理员的操作集中在后台负责景点上下架、民宿状态维护、订单审核与数据统计。这三类角色如果权限混在一起后期扩展必翻车。所以拿到源码第一步是看包名和 controller 分层常见结构是user相关接口和admin相关接口分开登录拦截器也分两套。毕设答辩时老师喜欢问“权限怎么控制的”能答出这一层就说明你真的看过代码而不只是把项目跑起来。选型上为什么是 Spring Boot MySQL 微信小程序而不是别的对毕设来说Spring Boot 社区资料多报错基本能搜到答案MySQL 是关系型数据库里上手成本最低的可视化工具用 Navicat 或者 DBeaver 都能操作微信小程序是现成的 C 端载体不用自己做 App 签名、不用走应用商店审核。这套组合的性价比确实是最高的源码里也基本不会出现冷门框架。2.2 核心表结构字段取舍与关联关系源码里的表名不一定完全一样但核心的几张表八九不离十user、scenic、hotel、travel_order、comment、favorite。我整理了一下典型字段你拿到源码后可以对照着看表名核心字段设计说明userid, openid, nickname, avatar, phoneopenid 是微信用户的唯一标识必须加唯一索引scenicid, name, image, intro, price, address, statusstatus 控制上下架1 上架 0 下架hotelid, name, image, price, address, status民宿/农家乐字段逻辑和景点表保持一致travel_orderid, order_no, user_id, goods_type, goods_id, amount, stategoods_type 区分景点票还是民宿state 管理状态流转commentid, user_id, goods_type, goods_id, content, score评论挂在具体商品上统一用 goods_type goods_idfavoriteid, user_id, goods_type, goods_id收藏表同样用通用关联方式几个容易踩的字段设计问题在源码里很常见。第一是 order_no 必须唯一不能直接用自增 id 当订单号后面订单状态机流转、售后查询都要靠这个业务编号第二是 state 字段不要用散乱的字符串最好约定数字状态码并在代码里写常量类比如 0 待支付、1 已支付、2 已完成、3 已取消第三是 image 字段只存文件名或者相对路径绝对路径由后端拼接否则换域名就要改数据库这种低级错误在答辩时被问到会很难看。2.3 建库脚本与初始化数据源码包里一般自带 sql 文件可以直接导入。我拿到手第一件事就是看建表语句确认字段类型、索引和字符集。以下是这个项目里常见的建库脚本结构-- 建库utf8mb4 是必须的 CREATE DATABASE IF NOT EXISTS travel_db DEFAULT CHARACTER SET utf8mb4; USE travel_db; -- 用户表 CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(50) DEFAULT COMMENT 昵称, avatar VARCHAR(200) DEFAULT COMMENT 头像地址, phone VARCHAR(20) DEFAULT COMMENT 手机号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 景点表 CREATE TABLE scenic ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 景点名称, image VARCHAR(200) DEFAULT COMMENT 图片文件名, intro TEXT COMMENT 景点介绍, price DECIMAL(10,2) DEFAULT 0.00 COMMENT 门票价格, address VARCHAR(200) DEFAULT COMMENT 地址, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点表;注意两个细节。utf8mb4是硬性要求微信昵称里经常出现 emoji 表情老项目用utf8存 emoji 直接报错这是最典型的字符集翻车现场。uk_openid唯一索引保证同一个微信号只插入一条记录这是登录接口幂等落库的关键没有这个索引高并发下重复注册会很麻烦。字段加 COMMENT 的习惯也要养成答辩时老师打开数据库看表结构COMMENT 就是最好的解说词。初始化数据不用多但必须覆盖所有场景。scenic 表至少插 3 到 5 条其中一条 status0 代表已下架这样才能演示后台上下架功能travel_order 表插几条不同 state 的记录前端页面上的“待付款 / 已支付 / 已完成”标签才有对应数据可显示。导入 SQL 的时候用 Navicat 执行会经常遇到字符集问题注意把连接编码也调成 utf8mb4排序规则选 utf8mb4_general_ci。如果导入时报错优先检查是不是连接串里没指定 characterEncoding。3. Spring Boot 后端实现登录、列表、订单三条链路的代码走读3.1 工程结构与数据源配置后端工程的结构大同小异controller 接收小程序请求service 写业务逻辑mapper 做数据访问。包名通常是 com.travel 之类的结构。先打开 pom.xml 看依赖重点确认有没有 spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok 这几个核心组件。有些源码用的 MyBatis-Plus那就会多一个 mybatis-plus-boot-starter用法上略有差别但核心逻辑一样。数据源配置在 application.yml 里这是所有联调问题的源头贴一份常见配置看一下server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/travel_db?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.travel.entity # 自定义配置 wx: appid: 你的appid secret: 你的secret这里有个血泪经验JDBC URL 里的参数一个都不能省。useSSLfalse避免 MySQL 8 的 SSL 握手报错allowPublicKeyRetrievaltrue解决 Public Key Retrieval is not allowedserverTimezoneAsia/Shanghai防止时间字段出现 8 小时偏差characterEncodingutf8mb4保证 emoji 不会变成问号。这四个参数我几乎每次部署新项目都会用到建议直接复制。wx.appid 和 wx.secret 是微信小程序平台的凭证在 mp.weixin.qq.com 后台的开发管理里能找到测试阶段建议用小程序的测试号或者自己的账号。3.2 微信登录code2Session 与用户落库小程序端 wx.login 会拿到一个临时 code后端拿这个 code 去微信接口换 openid。这是整套系统的登录入口核心代码如下PostMapping(/wx/login) public Result login(RequestBody LoginRequest req) { // 1. 用 code 换 session_key 和 openid String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code req.getCode() grant_typeauthorization_code; String res httpUtil.get(url); JSONObject obj JSON.parseObject(res); String openid obj.getString(openid); // 2. 查用户不存在则插入 User user userMapper.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(微信用户 openid.substring(20)); userMapper.insert(user); } // 3. 生成 token 返回给前端 String token JwtUtil.createToken(user.getId()); return Result.ok(token); }这段代码有三个关键点。一是 code 是一次性的前端必须在 wx.login 回调里立刻传给后端不能存起来复用否则第二次请求会报 invalid code。二是 openid 唯一索引兜底了并发场景下的重复插入问题如果两个请求同时到达一个 insert 会失败但 select 能查到所以更稳的写法是先查后插再 catch 唯一键冲突。三是 token 用的是 JWT把 user_id 放进去后续接口在拦截器里解析小程序端每次请求在 header 里带 Authorization 字段。微信接口地址是固定的https://api.weixin.qq.com/sns/jscode2session如果 appid 和 secret 不匹配返回的 errcode 是 40013调试时先看这个错误码别一上来就怀疑自己的代码。3.3 景点列表分页、过滤与图片路径拼接小程序首页是分页列表接口设计上常见的参数是 page 和 pageSize默认 page1、pageSize10。返回结构除了列表还要带 total前端才能算总页数。核心实现GetMapping(/scenic/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword) { PageParams params new PageParams(); params.setOffset((page - 1) * pageSize); params.setPageSize(pageSize); params.setKeyword(keyword); ListScenic list scenicMapper.pageList(params); list.forEach(s - s.setImage(/upload/ s.getImage())); Long total scenicMapper.count(params); return Result.ok(new PageResult(list, total)); }两个设计细节值得注意。接口只返回 status1 的景点下架数据在 SQL 里直接过滤掉where status 1这样前端不用做额外的判断。图片路径由后端拼成/upload/xxx.jpg再返回前端拿到的就是可以直接访问的完整路径避免在小程序里出现../../upload/xxx.jpg这种相对路径问题。keyword 参数做名称模糊搜索对应 SQL 是where name like concat(%, #{keyword}, %)。分页用的是手算 offset没有引入 PageHelper原因是少一个依赖就少一个坑。如果源码里已经集成了 PageHelper那直接PageHelper.startPage(page, pageSize)更省事两种方式都能跑通。3.4 订单提交幂等与状态流转订单是答辩时最容易被追问的模块。下单接口的核心是防重复提交和状态流转常见实现如下PostMapping(/order/create) public Result createOrder(RequestBody OrderReq req) { // 1. 生成唯一订单号时间戳 用户id毕设场景够用 String orderNo TR System.currentTimeMillis() req.getUserId(); TravelOrder order new TravelOrder(); order.setOrderNo(orderNo); order.setUserId(req.getUserId()); order.setGoodsType(req.getGoodsType()); // 1景点 2民宿 order.setGoodsId(req.getGoodsId()); order.setAmount(req.getAmount()); order.setState(0); // 0待支付 1已支付 2已完成 3已取消 // 2. 先查重再插入 TravelOrder exist orderMapper.findByNo(orderNo); if (exist ! null) { return Result.error(订单已存在); } orderMapper.insert(order); return Result.ok(orderNo); }订单号用时间戳拼 userId 是最简单的方案量大的场景要上雪花算法但毕设演示足够了。状态机流转上0 到 1 到 2 是正向流程0 到 3 是取消后端在更新状态时要校验当前状态能否跳转比如已取消的订单不能改成已支付。如果源码里这部分写到位老师问订单状态怎么管理时可以直接把状态机讲清楚顺便还能展示一下代码里的常量类。4. 微信小程序端对接从登录到页面渲染的完整流程4.1 登录wx.login 把 code 交给后端小程序端登录是整套系统的入口代码写法比较固定wx.login({ success: (res) { wx.request({ url: http://localhost:8080/api/wx/login, method: POST, data: { code: res.code }, success: (resp) { wx.setStorageSync(token, resp.data.data); } }); } });两个点需要注意。url 在开发阶段用 localhost 或者本机 IP真机预览时必须改成电脑的局域网 IP比如http://192.168.1.100:8080/api/wx/login否则手机访问不到电脑上的后端服务。token 拿到后存到 Storage后续所有业务请求都要在 header 里带上这个 token后端拦截器统一解析。4.2 请求封装与 Token 管理小程序没有 axios 那种现成的库但可以自己封装一个 request 方法把公共逻辑全部收拢自动带 token、统一处理 401、统一弹错误提示。源码里一般以 utils/request.js 的形式存在function request(url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method, data: data, header: { Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.data.code 401) { wx.redirectTo({ url: /pages/login/login }); return; } resolve(res.data); }, fail: (err) reject(err) }); }); }BASE_URL 建议单独放在 config.js 里本地联调用 localhost真机测试改成局域网 IP线上环境换成 HTTPS 域名只需改一个文件。后端接口返回结构统一成{ code, data, msg }前端判断 code 而不是 HTTP 状态码因为很多后端框架对业务错误也返回 HTTP 200只看状态码会把业务错误当成成功处理这是前后端联调时最常见的认知差。4.3 景点列表分页渲染与触底加载首页列表用页面自身的 onReachBottom 做触底加载实现起来很直接onLoad() { this.loadList(); }, onReachBottom() { if (this.data.page this.data.totalPage) { this.setData({ page: this.data.page 1 }); this.loadList(); } }, loadList() { request(/scenic/list, GET, { page: this.data.page, pageSize: 10 }) .then(res { this.setData({ list: this.data.list.concat(res.data.list), totalPage: Math.ceil(res.data.total / 10) }); }); }新手最容易写错的地方是触底加载必须用 concat 追加数据而不是直接赋值否则每次翻页都覆盖掉上一页的内容列表永远只有一页。totalPage 的计算是Math.ceil(total / pageSize)不是Math.floor否则最后一页剩一两条数据时会被直接丢掉。这两个细节如果在答辩时主动讲出来老师会认为你真的跑通了分页逻辑。4.4 订单提交与模拟支付预订页面把景点 id、价格、数量传到订单确认页提交时调用/order/create。支付在毕设里一般是模拟实现的常见做法是做一个“确认支付”按钮点击后调用支付接口把订单状态从 0 改成 1不真正接入微信支付——因为个人主体注册的小程序没有微信支付权限这也是答辩时会被问到的问题提前想好措辞就行。订单状态回显用 wx.switchTab 跳转到订单列表页onShow 里重新拉取订单列表保证每次切 tab 都能看到最新状态。这里有个小习惯订单列表页不要用 onLoad 拉数据用 onShow否则从支付页返回时列表不会刷新。5. 联调与部署避坑五条真实踩坑记录5.1 小程序请求连不上本地后端现象开发者工具里 wx.request 报url not in domain list或者直接显示 request:fail。原因开发者工具默认校验合法域名localhost 不在白名单里另一种可能是后端服务没启动或者端口和 BASE_URL 不一致。解决开发阶段在开发者工具右上角“详情—本地设置”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。真机调试时这个方法无效要么用备案域名配 HTTPS要么在演示环境用局域网 IP。我一般会在本地联调阶段直接把“不校验域名”勾上等要部署了再改回来这个开关就是给开发调试用的。5.2 JDBC 连接报错Public Key Retrieval 与时区现象后端启动时报Public Key Retrieval is not allowed或者查询时间字段比实际少了 8 小时。原因MySQL 8.x 默认的 caching_sha2_password 认证方式需要额外的公钥获取流程驱动默认不允许时区问题则是连接串没指定 serverTimezone。解决JDBC URL 后面追加?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai三个参数缺一不可。这条可以说是 MySQL 8 联调的第一坑搜一下全是血泪帖但基本都是同一个解法。5.3 微信 code 只能用一次现象登录接口偶发报错invalid code或者联调时重放请求失败。原因wx.login 返回的临时 code 五分钟有效且只能使用一次。前端代码在 onShow 和 onLoad 里重复触发登录或者后端不小心把同一个 code 请求了两次都会导致这个问题。解决前端在 wx.login 的成功回调里立刻调用后端登录接口不要在页面多个生命周期里重复触发。联调时想重放请求就重新执行一次 wx.login 拿新 code。这个坑排查起来有点玄学因为不是每次必现但只要代码里存在重复调用迟早会触发。5.4 真机预览图片裂图现象开发者工具里图片显示正常手机预览时图片全部裂图。原因开发者工具的 localhost 指向电脑本机手机的 localhost 指向手机自己当然访问不到。另外微信小程序正式环境会拦截 HTTP 明文请求虽然开发调试模式放行了但真机预览时用 IP 访问 HTTP 接口属于灰色地带不稳定。解决真机测试把 BASE_URL 和图片域名都换成电脑的局域网 IP保证手机和电脑在同一个 WiFi 下同时确认电脑防火墙放行了 8080 端口。做线上演示时图片域名必须走 HTTPS这是小程序的硬性要求。5.5 端口占用与 BASE_URL 不一致现象后端能启动但小程序请求全部超时或者部分接口通部分接口不通。原因8080 端口被其他进程占用Spring Boot 自动切到了随机端口但小程序里的 BASE_URL 还写着 8080。还有一种是 context-path 和 URL 对不上比如后端配置了/api前缀小程序请求却没带。解决启动后先看控制台日志里Tomcat started on port(s)这一行确认实际端口再和小程序 config.js 里的 BASE_URL 比对。context-path 的问题更隐蔽如果配了/api小程序请求路径就要写/api/scenic/list而不是/scenic/list。6. 演示与答辩验收一个能让评委跟住的演示顺序源码跑通只是第一步答辩演示的节奏同样重要。我建议按“数据库—后端—小程序—后台”这个顺序来展示和系统真实的数据流动方向一致。先打开 Navicat 展示核心表结构和预置数据让评委知道数据的来龙去脉再启动后端控制台日志能证明接口在正常对外服务接着打开小程序开发者工具先从景点列表浏览开始再走一遍“登录—浏览详情—下单—模拟支付—查看订单”的完整链路最后切到后台管理页展示一次上下架操作然后回到小程序刷新列表让评委看到状态变化实时生效。演示中最怕的是接口突然挂掉。我一般会在演示前强制走一遍完整的自检流程启动后端访问一个接口确认返回 JSON用开发者工具清缓存重新登录确认 code 正常换取 token检查数据库连接执行一次查询最后确认小程序里的 BASE_URL 是当前这台电脑的 IP。这套流程大概两分钟能规避掉九成以上的现场翻车。如果演示时某个接口真的超时了先看后端控制台有没有报错堆栈再看 MySQL 是否还活着。这三个组件里 MySQL 最先挂的可能性最大所以演示前重启一下 MySQL 服务也算是个保险动作。从那以后我每次演示都会强制走一遍“数据库通—后端通—小程序通”的三连通检查再开始录屏或现场操作这个习惯帮我避免了好几次尴尬场面。希望帮到你。本文还有配套的精品资源点击获取