这两年做毕设辅导和接私活,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:存储微信用户信息。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| openid | varchar(64) | 微信用户唯一标识 |
| unionid | varchar(64) | 用户开放平台唯一标识,可空 |
| nickname | varchar(64) | 昵称 |
| avatar | varchar(255) | 头像URL |
| phone | varchar(20) | 手机号,可空 |
| gender | tinyint | 性别 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
景区表t_scenic:存储景点基础信息。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(100) | 景区名称 |
| summary | text | 简介 |
| detail | longtext | 详情介绍 |
| cover_image | varchar(255) | 封面图 |
| images | text | 图集(JSON数组) |
| address | varchar(255) | 地址 |
| longitude | decimal(10,6) | 经度 |
| latitude | decimal(10,6) | 纬度 |
| open_time | varchar(50) | 开放时间 |
| ticket_tip | varchar(255) | 门票说明 |
| status | tinyint | 上架状态:0下架1上架 |
| view_count | int | 浏览量 |
| create_time | datetime | 创建时间 |
订单表t_order:记录用户预订记录。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单编号 |
| user_id | bigint | 用户ID |
| scenic_id | bigint | 景区ID |
| visit_date | date | 游玩日期 |
| ticket_type | varchar(20) | 票型(成人票/儿童票) |
| ticket_num | int | 票数 |
| total_amount | decimal(10,2) | 总金额 |
| contact_name | varchar(50) | 联系人 |
| contact_phone | varchar(20) | 联系电话 |
| status | tinyint | 状态:0待支付1已支付2已取消3已核销 |
| pay_time | datetime | 支付时间 |
| create_time | datetime | 创建时间 |
评论表t_comment:记录用户点评。字段包括 id、user_id、scenic_id、content、rating(评分1-5)、images、status(0待审核1通过2不通过)、create_time。
路由推荐表t_route:定义文化路线,字段包括 id、title、subtitle、cover、description、scenic_ids(关联景区ID,逗号分隔)、status、sort_order。
管理员表t_admin:admin 账号密码(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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>application.yml里注意几个关键配置:数据库账号密码用环境变量或者profile区分,不要写死;Redis要设置密码;Jackson设置时间格式。这里贴一个简单配置:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tour_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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 Result<UserVO> 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_type=authorization_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 LambdaQueryWrapper<User>().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 class="page"> <!-- 数据加载中 --> <view class="loading-mask" wx:if="{{loading}}"> <view class="spinner"></view> <text>正在加载景区信息...</text> </view> <!-- 加载完成 --> <block wx:else> <view class="banner"> <image src="{{detail.coverImage}}" mode="aspectFill"></image> </view> ... </block> </view>有一点切记:加载动画不要搞太复杂。小程序包体积有2MB限制,图片和第三方库占太多空间,加载反而更慢,对用户体验是负担。
4.2 首页、景点详情、预订流程实现
首页是整个小程序的门面。我设计的首页结构是:顶部搜索框、轮播图、功能导航区(门票预约、文化路线、讲解服务)、热门景点推荐列表。这些数据都从后端接口拿,轮播图用swiper组件,景点列表用scroll-view配合触底加载更多。
小程序请求接口用wx.request。因为需要附带Token,我把请求封装成一个独立模块utils/request.js:
const 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 id="map" longitude="{{detail.longitude}}" latitude="{{detail.latitude}}" scale="14" show-location markers="{{markers}}" style="width: 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证书、小程序白名单这些环境问题,多给自己留一点容错时间,很多第一次做小程序的人,最后不是栽在业务逻辑上,而是被这些“看起来很小但特别耗时”的环境问题卡住的。希望这篇复盘能帮你少走一点弯路。