ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+微信小程序文旅小程序全栈开发实战复盘

SpringBoot+微信小程序文旅小程序全栈开发实战复盘 这两年做毕设辅导和接私活SpringBoot 微信小程序这套组合我接触的次数非常多。前阵子刚帮一个学弟把“文化旅游小程序”从零搭完从选题、数据库设计到前后端联调、过审踩了不少坑也攒下一些能直接用的经验。正好借这篇稿子把整个项目的设计思路和实现细节完整复盘一遍内容尽量偏实战代码和表结构都会给出来希望给正在做同类型毕设或者想快速上手小程序开发的朋友一点参考。先说下这个项目是干什么的。面向的用户主要是游客和景区运营方游客端通过微信小程序浏览景点介绍、查看旅游攻略、预订门票或讲解服务、发表评价运营方通过后台管理景点内容、处理订单、统计游客数据。技术栈就是题目里写的SpringBoot加微信小程序后端用Java生态前端用小程序原生框架开发数据存储采用MySQL缓存层用Redis整体是一个典型的前后端分离项目。说白了这套系统做完以后就是一个小型但五脏俱全的文旅数字化平台既有C端使用体验也包含了后台管理特别适合作为毕设项目来展示完整度。1. 项目定位与整体技术选型1.1 这个项目解决什么问题文旅类小程序现在的需求其实非常旺盛。很多景区、博物馆、文化街区都在做数字化升级但是大平台定制开发动辄十几万小景区根本承担不起。而这种基于SpringBoot 微信小程序的自研系统刚好能满足轻量化、可定制、成本低的需求。对游客来说它解决了三个核心痛点一是信息分散出发前要翻好几个APP查攻略而小程序可以把景点介绍、开放时间、门票价格、用户评价集中在一个地方二是购票不方便很多现场排队买票真的很浪费时间三是缺乏本地化推荐游客到了景区周边根本不知道哪里值得去。对管理者来说这套系统把线下人工登记、手工统计的活搬到了线上用户行为数据也能沉淀下来做分析。所以我做这个项目时第一件事不是写代码而是把用户场景画清楚。谁在用、在什么场景下用、核心诉求是什么想明白了再动手后面做什么功能、表怎么设计就顺了。1.2 为什么是SpringBoot 微信小程序技术选型这部分很多同学会纠结。有人问我前端为什么不用Vue或React后端为什么不用Python的Django或Flask。说实话我的答案很直接在这个场景下SpringBoot 微信小程序就是最稳的组合。从后端看SpringBoot最大的优势是省事。它把Spring生态里繁琐的XML配置全干掉了依赖管理、自动装配、内嵌Tomcat一套组合拳下来项目启动效率极高。对于文旅这种CRUD密集型的业务系统SpringBoot的生态非常成熟MyBatis-Plus操作数据库Spring Security或拦截器做鉴权Redis做缓存都是有大量现成踩坑案例的技术栈出了问题很容易搜到解决方案。从小程序看微信小程序是当前国内文旅场景里最触手可及的用户入口。游客不需要下载APP搜一下或者扫个码就能打开用完即走完全符合旅游这种低频但刚需的使用特点。再加上微信生态里的支付、地图、订阅消息这些能力小程序天生就适合做LBS加预订这类业务。而且用原生小程序开发不需要引入额外的跨端框架调试起来也直接毕设答辩的时候现场演示不容易出岔子。1.3 整体技术架构与部署形态项目的整体架构是标准的前后端分离我直接放出来表现层微信小程序原生框架负责页面渲染、用户交互、请求后端接口。接入层Nginx作为反向代理负责静态资源访问、接口转发。业务层SpringBoot应用按业务模块拆分为景点、线路、订单、用户、评论、后台管理等功能。数据层MySQL存储业务数据Redis处理热点缓存、Token会话、验证码等。第三方能力微信官方API登录凭证校验、订阅消息、地图SDK等。小程序的请求入口统一走https://api.xxx.com/api/**后端用RESTful风格暴露接口返回JSON格式数据。内部再划分一层业务Service和Mapper控制层不直接操作数据库避免代码全堆在一个类里。部署时最省成本的方案是一台云服务器放SpringBoot jar包和Nginx域名做好备案和HTTPS证书。小程序正式上线要求HTTPS而且必须是备案域名这块千万别拖到最后再搞审核的时候会卡住。项目目录结构 tour-server/ ├── src/main/java/com/example/tour/ │ ├── config/ // 配置类拦截器、Redis、跨域 │ ├── controller/ // 接口层 │ ├── service/ // 业务逻辑层 │ ├── mapper/ // 数据持久层MyBatis-Plus │ ├── entity/ // 数据库实体 │ ├── dto/ // 请求响应参数封装 │ ├── common/ // 统一返回结果、异常处理、工具类 │ └── TourApplication.java └── src/main/resources/ ├── application.yml └── mapper/ // XML文件复杂SQL2. 核心功能模块与数据库设计2.1 功能模块拆分C端和后台功能设计上我把系统分成两个相对独立的部分小程序端和后台管理端。小程序端面向游客后台管理端面向运营人员。这里强调一点毕设答辩时老师最喜欢问“你的系统有哪些角色、这些角色分别能做什么”所以角色划分一定要清晰。小程序端模块用户认证微信授权登录、个人信息维护。首页展示轮播图、热门景点推荐、精选路线。景区模块景区列表、景区详情、地图位置、评论列表。门票与导览预订选择日期、填写游客信息、生成订单。路线推荐文化主题路线展示比如“古城文化一日游”。个人中心我的订单、我的收藏、浏览历史、意见反馈。后台管理端模块登录鉴权管理员账号密码登录。景点管理景点CRUD、上下架、排序、封面图管理。轮播图管理首页运营位图片配置。订单管理查看、查询、核销操作。评论管理审核不通过、删除。数据统计按日统计访问量、订单量用简单图表展示。模块不在多而在闭环。游客能逛、能下单、能评价管理员能管景点、能处理订单、能看数据业务就转起来了这比堆十几个功能但彼此没有关联要强得多。2.2 数据库表结构设计数据库是整个系统的基础。我表设计的原则是“按业务域拆分不过度抽象、不过度冗余”。以下是这个项目里比较关键的表用户表t_user存储微信用户信息。字段类型说明idbigint主键openidvarchar(64)微信用户唯一标识unionidvarchar(64)用户开放平台唯一标识可空nicknamevarchar(64)昵称avatarvarchar(255)头像URLphonevarchar(20)手机号可空gendertinyint性别create_timedatetime创建时间update_timedatetime更新时间景区表t_scenic存储景点基础信息。字段类型说明idbigint主键namevarchar(100)景区名称summarytext简介detaillongtext详情介绍cover_imagevarchar(255)封面图imagestext图集JSON数组addressvarchar(255)地址longitudedecimal(10,6)经度latitudedecimal(10,6)纬度open_timevarchar(50)开放时间ticket_tipvarchar(255)门票说明statustinyint上架状态0下架1上架view_countint浏览量create_timedatetime创建时间订单表t_order记录用户预订记录。字段类型说明idbigint主键order_novarchar(32)订单编号user_idbigint用户IDscenic_idbigint景区IDvisit_datedate游玩日期ticket_typevarchar(20)票型成人票/儿童票ticket_numint票数total_amountdecimal(10,2)总金额contact_namevarchar(50)联系人contact_phonevarchar(20)联系电话statustinyint状态0待支付1已支付2已取消3已核销pay_timedatetime支付时间create_timedatetime创建时间评论表t_comment记录用户点评。字段包括 id、user_id、scenic_id、content、rating评分1-5、images、status0待审核1通过2不通过、create_time。路由推荐表t_route定义文化路线字段包括 id、title、subtitle、cover、description、scenic_ids关联景区ID逗号分隔、status、sort_order。管理员表t_adminadmin 账号密码BCrypt加密、角色、状态、最后登录时间。说实话这几张表已经能覆盖一个文旅系统的核心业务了。我特别想提醒一句不要一上来就搞几十张表毕设项目重要的是逻辑完整不是表多。表设计得越复杂联表查询越痛苦答辩时也越难解释清楚。2.3 关键接口设计与数据流转接口设计遵循RESTful风格核心接口大致如下POST /api/user/login微信登录传入code返回用户信息和Token。GET /api/scenic/list分页获取景区列表。GET /api/scenic/{id}景区详情含评论和推荐线路。POST /api/order/create创建订单。POST /api/order/pay模拟支付正式项目会接入微信支付。GET /api/order/list查询用户订单。GET /api/scenic/search景点关键词搜索。POST /api/comment/add发表评论。以“查看景区详情”为例数据流转是这样的用户在小程序点击景点卡片前端调用GET /api/scenic/{id}后端先从Redis查缓存有就直接返回没有就到MySQL查景区表、查评论表把数据封装成详情DTO返回同时将这个景区的viewCount加1。前端拿到数据后渲染在页面上。GET /api/scenic/1001 返回示例 { id: 1001, name: 古城文化景区, summary: 保存完好的明清古建筑群……, detail: ……, coverImage: https://xxx/abc.jpg, images: [https://xxx/1.jpg, https://xxx/2.jpg], address: 某某市某某区, longitude: 120.123456, latitude: 30.123456, openTime: 08:00-17:30, comments: [ { userName: 张三, content: 很适合带家人来逛, rating: 5, createTime: 2025-05-20 10:00:00 } ] }3. 后端核心环节的实现3.1 SpringBoot工程初始化与包结构后端我使用的是SpringBoot 2.7.x版本Java 8。为什么不建议直接上最新的SpringBoot 3.x因为很多毕业设计用的教学资料、博客还是2.x的体系遇到问题搜索成本低同时2.7.x对MyBatis-Plus、Redis等组件的兼容性好不用折腾一堆新配置。如果你对SpringBoot 3已经很熟用3.x也没问题但小白我建议保守。项目的依赖配置核心部分如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependencyapplication.yml里注意几个关键配置数据库账号密码用环境变量或者profile区分不要写死Redis要设置密码Jackson设置时间格式。这里贴一个简单配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tour_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ${DB_PASSWORD:123456} redis: host: localhost port: 6379 password: ${REDIS_PASSWORD:123456} database: 0 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0包结构按功能分包controller、service、mapper、entity、dto、common、config。这个结构看上去简单但足够清晰。我做项目的时候习惯把VO、DTO和Entity分开不搞大杂烩否则后期改需求就非常痛苦。3.2 登录鉴权微信登录与Token体系微信登录流程是这个小程序项目最核心的一个环节几乎每个面试官或答辩老师都会问。流程不复杂小程序端调用wx.login拿到临时凭证code把code传给后端后端拿code appId appSecret 请求微信接口换取openid和session_key服务端用openid查用户表如果不存在就自动注册然后签发一个Token返回给前端。代码大致如下PostMapping(/login) public ResultUserVO login(RequestBody LoginDTO dto) { // 1. code换openid String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code dto.getCode() grant_typeauthorization_code; String result restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(result); String openid json.getString(openid); if (StringUtils.isBlank(openid)) { return Result.error(登录失败); } // 2. 查库/注册 User user userMapper.selectOne( new LambdaQueryWrapperUser().eq(User::getOpenid, openid)); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(游客 StringUtils.substring(openid, openid.length() - 6)); user.setCreateTime(new Date()); userMapper.insert(user); } // 3. 生成token String token JWT.create() .withClaim(userId, user.getId()) .withExpiresAt(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .sign(Algorithm.HMAC256(jwtSecret)); UserVO vo new UserVO(); vo.setId(user.getId()); vo.setNickname(user.getNickname()); vo.setAvatar(user.getAvatar()); vo.setToken(token); return Result.success(vo); }Token我这里选的是JWT无状态方便扩展。签发后前端把Token放在请求头Authorization: Bearer xxx里后端写一个拦截器统一校验。拦截器里排除登录、景点列表这些公开接口其余接口走Token解析解析失败直接返回401。这里有个坑微信的session_key在后端不要持久化也不要打印日志一旦泄露别人可以伪造用户身份。有些项目把session_key存进数据库这是极不推荐的安全风险很大。有需要就用用完就丢。3.3 核心业务接口的实现以“创建订单”为例说说核心业务的实现细节。用户在小程序选景区、选日期、选票数提交后后端做这几件事参数校验景区是否存在、游玩日期不能是过去日期、票数要在合理范围。幂等处理前端生成唯一请求编号requestId后端用Redis的SETNX做幂等避免用户重复点按钮导致重复下单。计算金额从数据库拿景区价格不是用前端传的价格乘以票数。价格永远以服务端为准这个原则要刻在脑子里。生成订单号用“日期 随机数”生成或者直接用雪花算法。保存订单状态为待支付返回订单号给前端。创建订单的代码我这里使用MyBatis-Plus的Service接口来写简洁很多Transactional(rollbackFor Exception.class) public Order createOrder(CreateOrderDTO dto, Long userId) { Scenic scenic scenicMapper.selectById(dto.getScenicId()); if (scenic null || scenic.getStatus() 0) { throw new BizException(景点不存在或未上架); } if (dto.getVisitDate().isBefore(LocalDate.now())) { throw new BizException(游玩日期不能早于今天); } BigDecimal amount scenic.getTicketPrice().multiply( BigDecimal.valueOf(dto.getTicketNum())); Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setScenicId(dto.getScenicId()); order.setVisitDate(dto.getVisitDate()); order.setTicketType(dto.getTicketType()); order.setTicketNum(dto.getTicketNum()); order.setTotalAmount(amount); order.setStatus(0); orderMapper.insert(order); // 记录Redis幂等标记防止重复提交 return order; }3.4 缓存与并发保护文旅系统的流量集中在节假日比如景区详情页可能同一时间被大量访问。如果每次都查MySQL数据库压力非常大所以详情接口要加Redis缓存缓存key用scenic:detail:{id}value存JSON字符串过期时间设置30分钟。更新景点信息时删除对应缓存或者采用“先更新数据库再删除缓存”的策略。为什么不是“先删缓存再更新数据库”因为并发场景下先删缓存很可能导致其他请求把旧数据又写回缓存数据库更新完反而缓存是脏的。这个知识点面试也常问顺便记一下。另外还要做接口防刷。比如评论接口、发送验证码接口同一个用户1秒内不能调太多次。用Redis计数每个用户每秒钟限制N次超过就拦截。没必要用太复杂的限流框架一个拦截器加Redis计数器就够用了。public boolean isFrequent(Long userId, String action, int maxTimes, int seconds) { String key rate: action : userId : LocalTime.now().getMinute(); Long count redisTemplate.opsForValue().increment(key); if (count 1) { redisTemplate.expire(key, seconds, TimeUnit.SECONDS); } return count maxTimes; }4. 小程序前端实现要点4.1 app.json与启动加载页的调整小程序端的项目结构我是用微信开发者工具默认模板创建的。有同学用uni-app或者Taro来开发也没问题但原生小程序学习成本更低调试更直接所以我这里就以原生为例。刚创建项目的时候小程序默认首页是pages/index/index加载页默认是微信自带的“正在加载”菊花。我们做文旅小程序启动体验很重要所以需要自定义加载页。具体做法是在app.json中配置window节点的navigationBarBackgroundColor、navigationBarTitleText然后在每个页面的.wxml文件最外层放一个loading容器数据请求完成之前显示自定义的loading动画请求回来之后隐藏。小程序的加载页还有一种做法是在app.json里加usingComponents用自定义组件实现全局loading再在页面onLoad的时候控制状态。不过我觉得最省事的方案还是每个页面上方加一个条件渲染的loading区块view classpage !-- 数据加载中 -- view classloading-mask wx:if{{loading}} view classspinner/view text正在加载景区信息.../text /view !-- 加载完成 -- block wx:else view classbanner image src{{detail.coverImage}} modeaspectFill/image /view ... /block /view有一点切记加载动画不要搞太复杂。小程序包体积有2MB限制图片和第三方库占太多空间加载反而更慢对用户体验是负担。4.2 首页、景点详情、预订流程实现首页是整个小程序的门面。我设计的首页结构是顶部搜索框、轮播图、功能导航区门票预约、文化路线、讲解服务、热门景点推荐列表。这些数据都从后端接口拿轮播图用swiper组件景点列表用scroll-view配合触底加载更多。小程序请求接口用wx.request。因为需要附带Token我把请求封装成一个独立模块utils/request.jsconst BASE_URL https://api.xxx.com/api; function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method || GET, data: data || {}, header: { Content-Type: application/json, Authorization: Bearer wx.getStorageSync(token) }, success: (res) { if (res.statusCode 200) { resolve(res.data); } else if (res.statusCode 401) { // token过期逻辑 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); } else { reject(res.data); } }, fail: (err) { reject(err); } }); }); } module.exports { request };景点详情页是用户浏览的核心页面一般包括顶部图集轮播、名称和地址、开放时间、简介、评论列表、底部“立即预订”按钮。详情页的数据在onLoad里根据上一页传过来的id请求。预订流程点击“立即预订”进入订单确认页展示景区、日期、票型、人数用户选择后自动计算总价点击提交订单调用后端创建订单接口弹出提示“预订成功”。这个流程核心是“金额回显”提交时后端重新计算金额前端显示仅供参考。4.3 地图定位与附近推荐文旅项目离不开“位置”这个概念。在小程序里实现地图直接用map组件将景区的经纬度作为markers传入。这样游客在详情页就能一键看到景区位置点击marker还能跳转导航。我是这样实现的map idmap longitude{{detail.longitude}} latitude{{detail.latitude}} scale14 show-location markers{{markers}} stylewidth: 100%; height: 300rpx; /map其中markers动态构造const marker { id: scenic.id, longitude: scenic.longitude, latitude: scenic.latitude, title: scenic.name, iconPath: /images/marker.png, width: 32, height: 32 }同时首页还可以基于wx.getLocation获取用户当前位置然后请求后端接口“根据经纬度获取附近景点”。后端用MySQL的ST_Distance_Sphere函数算距离或者直接用Haversine公式在Service里计算返回按距离排序的景点列表。这功能不算复杂但很加项目亮点。4.4 请求封装与错误处理前端的坑很多不在页面逻辑而在错误处理。小程序如果请求失败要给用户明确反馈比如“网络异常请检查网络设置”。加载数据的时候要有loading状态不能白屏。提交订单成功要弹窗提示失败要告诉用户失败原因。我封装请求的时候会在拦截器里统一处理错误码。后端返回结构统一是{ code: 200, message: success, data: { ... } }前端在request封装里检查code不是200就统一拦截并弹出 Toast这样页面逻辑里就不用写一堆重复的错误提示代码了。还有一个小细节用户点击提交订单后按钮要进入loading状态并禁用防止多次点击重复下单。这种细节处理得当体验提升很明显。5. 常见问题与排错实录5.1 加载页没改掉或者启动黑屏这个问题遇到的实在太多了。改了app.json之后开发者工具里还是不生效。有两个原因很常见一是编译缓存没清点一下工具栏的“编译”旁边下拉箭头选“清除缓存并重新编译”二是改了自定义组件后小程序的app.json里window配置有误比如navigationBarTextStyle只支持black和white写个red它不报错但是样式不生效。还有一种情况启动黑屏是因为首页的onLoad里请求接口卡住没返回而页面又依赖数据渲染。解决办法是给请求加超时限制默认超时时间设置为5秒超时后给出“加载失败点击重试”的兜底。5.2 登录状态失效与Token过期Token有效期我设置的是7天理论上用户7天内不用重复登录。但实际使用中用户可能会删除小程序、清理缓存导致token丢失。这时候前端调用需要登录的接口后端返回401前端需要重新走登录流程。我建议的处理方案是请求封装里做401统一处理弹出提示“登录已过期请重新登录”然后跳转到登录页。不要在每一个页面的回调里重复写这层逻辑。另外要提醒测试的时候wx.login拿到的code是一次性的而且5分钟内有效后端频繁拿到同一个code换openid会报invalid code错误这在调试时遇到过好几次不用慌重新编译小程序就会生成新code。5.3 地图组件在开发者工具正常、真机不显示这是小程序开发的经典问题。开发者工具里地图渲染正常手机上一片空白。原因大概率是“没有配置账号或权限”。map组件依赖腾讯位置服务API需要在开发者工具账号设置里配置对应AppID的合法域名或者移入“不校验合法域名”的开关只是开发者工具临时有效真机预览还是会拦截。我自己的解决方案是在小程序后台配置map相关域名白名单并保证项目使用的是正式AppID而不是测试号。还有一个常见问题是手机上系统定位未打开wx.getLocation会直接走fail回调代码里要给用户一个开启定位的引导提示。5.4 开发调试中的信息与接口安全这一点必须多写几句。在做这个项目的过程中很多人的代码仓库里会把数据库密码、微信AppSecret等关键信息直接写在application.yml里推到公开仓库这是非常严重的信息泄露隐患。微信AppSecret一旦泄露别人可以冒充你的小程序调用接口获取用户信息后果不可控。我建议起码做到这几条数据库密码、AppSecret、JWT密钥通过环境变量注入配置文件里只留占位符。生产环境务必为每个表设置好索引尤其是t_order.user_id、t_comment.scenic_id这类高频查询字段不加索引在数据量上来后SQL慢得让人抓狂。接口层面做好参数校验后端既不要信任前端传入的用户ID、价格字段也要防止SQL注入和XSS注入。MyBatis-Plus的参数绑定已经规避了大部分SQL注入但仍然要避免直接字符串拼接SQL。小程序端不要存储明文密码、Token之外不要保存其他敏感数据本地缓存只放Token和用户ID。5.5 性能优化与体验提升最后聊聊性能优化。文旅小程序最常见的场景是景区详情页有大量图片如果不做处理加载慢得让人崩溃。我的做法图片上传到OSS或者COS这类对象存储并使用缩略图样式参数。比如图片链接后面加?imageView2/2/w/750管理员上传的原始图几兆前端加载直接压缩到几百K体感快非常多。列表接口使用分页每页10条或15条不要一次返回全量数据。小程序的scroll-view配合onReachBottom实现“上拉加载更多”后端用MyBatis-Plus的Page分页。热点数据加Redis缓存详情页几乎只有景点更新管理员才会改30分钟缓存足以应对日常流量。首页Banner图和景区列表页可以在onShow的时候先读本地缓存再请求接口更新这样用户二次打开时会感觉非常流畅。性能优化没有一招制敌的秘诀核心思路就是能缓存就缓存能压缩就压缩能分页就分页。这个项目从设计到上线前前后后我大概用了三周时间白天写后端晚上调小程序端中间有大把时间花在联调和改Bug上。要说最深的体会就是做这种全栈项目千万别一上来就闷头写代码先把角色流程、表结构、接口文档这三样定下来后面基本是体力活。还有一点遇到跨域、HTTPS证书、小程序白名单这些环境问题多给自己留一点容错时间很多第一次做小程序的人最后不是栽在业务逻辑上而是被这些“看起来很小但特别耗时”的环境问题卡住的。希望这篇复盘能帮你少走一点弯路。
返回列表