ARTICLE DETAIL

资讯详情

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

基于SSM与微信小程序的智慧乡村旅游服务平台设计与全栈实现

基于SSM与微信小程序的智慧乡村旅游服务平台设计与全栈实现 简介面向毕业设计场景的Java微信小程序智慧乡村旅游服务平台完整项目。基于SSM框架、Vue后台与微信小程序采用MySQL数据库包含管理员、用户、商家三角色权限覆盖旅游景点、路线管理、订单处理、购物车、用户充值等核心功能适合计算机相关专业学生毕业设计参考。整套资料共1183个文件、约55.46MB包含Java后端源码、Vue页面、小程序wxml/wxss/js、SQL脚本、论文文档以及环境工具包和分步安装教程支持Eclipse、MyEclipse、STS、IDEA等常见开发工具目录结构清晰。已有171人学习下载。除完整源码与数据库脚本外还提供论文、环境工具包和同框架项目安装教程并实现管理员、用户、商家三类角色的功能闭环可用于方案借鉴、直接部署或二次开发有助于快速掌握SSM小程序Vue项目的整体开发流程。1. 从毕业设计到能跑的项目智慧乡村旅游服务平台到底在解决什么智慧乡村旅游服务平台是这几年 Java 毕业设计里出现频率最高的方向之一不是因为选题新而是因为它足够“整”前端有微信小程序后端有 SSM 框架和 MySQL业务上又能同时覆盖 C 端游客使用和 B 端管理员维护两条线一个项目做下来Java 基础、框架整合、数据库设计、小程序 API 调用全都能练到。相比单纯的管理系统它的亮点在于业务闭环更完整游客从小程序搜索景点、浏览民宿、在线预约管理员在 Web 后台维护景点信息、查看订单、处理评论两边通过统一的后端接口联动。这套方案之所以被大量复用主要是技术选型足够稳妥SSM 是 Java 生态里最经典的分层组合网上能查到的资料和现成代码极多遇到问题不容易卡住微信小程序则天然贴近“乡村旅游”的使用场景扫码即用、不需要下载 App对游客几乎没有学习成本。这篇博文会从表结构设计、后端接口、小程序端联调、上线排错这条主线完整讲一遍这套系统的实现思路和其中容易踩的坑。适合准备做同类毕设、或者想拿一个完整全栈项目练手的人照着做。2. 数据建模与 SSM 后端把乡村游业务拆成可运行的表结构2.1 从业务流程图到四张核心表的建模思路做智慧乡村旅游平台首先要梳理清楚“谁在用、用到什么数据”。站在游客角度用户打开小程序后要做的事无非是浏览景点列表、查看景点详情、看看附近有哪些民宿或农家乐、提交预约订单、查看自己的预约记录、发表评论。站在管理员角度要做的就是景点信息的增删改查、订单状态的更新、评论的审核与回复。两边一合并核心数据实体就出来了。景区信息、民宿信息、订单、评论、用户这几张表就能撑起整个业务。下面这组建表语句是按照常见做法整理出来的字段做了精简但业务关系是完整的CREATE TABLE scenic_spot ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 景点名称, intro TEXT COMMENT 景点介绍, cover_url VARCHAR(255) COMMENT 封面图地址, location VARCHAR(255) COMMENT 位置描述, ticket_price DECIMAL(10,2) DEFAULT 0 COMMENT 门票价格, open_time VARCHAR(100) COMMENT 开放时间, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE homestay ( id INT PRIMARY KEY AUTO_INCREMENT, spot_id INT COMMENT 关联景点, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) DEFAULT 0, address VARCHAR(255), phone VARCHAR(20), status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, spot_id INT, homestay_id INT, visit_date DATE, person_count INT DEFAULT 1, total_price DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE comment ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, spot_id INT, content TEXT, rating INT DEFAULT 5, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这四张表的关联关系很直接scenic_spot 和 homestay 是一对多order 表同时关联用户与景点/民宿comment 表挂在景点下。需要注意的点是 order 表用了反引号包裹因为 order 是 MySQL 的保留字不处理会在建表时报语法错误这是实际开发里非常容易忽略的细节。另外 order 表里同时留了 spot_id 和 homestay_id 两个外键是因为一个订单可能只订门票也可能连带民宿一起订两个字段都允许为空业务上比拆成两种订单表更好维护。2.2 SSM 项目的包结构与 MyBatis 映射层编写要点SSM 项目的目录结构通常按 controller、service、dao、entity 四层来组织再用 spring-mvc.xml、spring-mybatis.xml 两个配置文件完成整合。entity 对应数据库表dao 是 MyBatis 的接口层service 写业务逻辑controller 暴露 HTTP 接口。这样一个分层的好处是职责清晰小程序端请求进来后由 controller 接收参数service 校验和处理数据dao 通过 MyBatis 与 MySQL 交互数据再逐层返回。MyBatis 的使用上有几个细节值得注意。第一接口方法名要与 XML 中的 statement id 保持一致第二查询列表和查询单条的返回类型不同resultType 和 resultMap 不能混用第三条件查询建议用 Param 注解传参避免多个参数时出现绑定异常。下面是一个景点列表查询的 Dao 层写法public interface ScenicSpotDao { ListScenicSpot selectList(Param(keyword) String keyword, Param(offset) int offset, Param(limit) int limit); ScenicSpot selectById(Param(id) Integer id); int insert(ScenicSpot spot); int update(ScenicSpot spot); int delete(Param(id) Integer id); }对应的 mybatis-mapper.xml 里selectList 方法需要手写动态 SQLselect idselectList resultTypecom.example.entity.ScenicSpot SELECT * FROM scenic_spot where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if AND status 1 /where ORDER BY create_time DESC LIMIT #{offset}, #{limit} /select这里用 标签的好处是当 keyword 为空时不会生成多余的 WHERE 关键字避免了手动拼接 SQL 时常见的语法错误。LIMIT 分页参数由前端传入pageNum 和 pageSize 换算成 offset 和 limit 后直接进 SQL这种方式在数据量大时不如 PageHelper 优雅但逻辑透明面试时反而更容易讲清楚。实际项目中也可以用 PageHelper 分页插件只需要在 Spring 配置里注册一个拦截器代码里直接调用 PageHelper.startPage(pageNum, pageSize) 即可。2.3 核心接口景点列表与预约下单的 Controller 层实现后端接口的设计要站在小程序端的调用角度来考虑。前端需要什么格式的数据后端就返回什么结构。通常会统一用一个 Result 类包裹返回值里面包含 code、message、data 三个字段code 为 200 表示成功其他值表示业务异常这样前端拦截器只需要判断 code 就能统一处理报错逻辑。景点列表接口和预约下单接口是这套系统里最关键的两个接口下面分别给出实现。景点列表接口的 Controller 代码RestController RequestMapping(/api/spot) public class ScenicSpotController { Autowired private ScenicSpotService scenicSpotService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String keyword) { int offset (pageNum - 1) * pageSize; ListScenicSpot list scenicSpotService.getList(keyword, offset, pageSize); return Result.success(list); } GetMapping(/detail/{id}) public Result detail(PathVariable Integer id) { ScenicSpot spot scenicSpotService.getById(id); return spot null ? Result.error(景点不存在) : Result.success(spot); } }预约下单接口要比列表接口复杂一些因为它涉及事务要生成订单号、写订单记录还要校验景点是否上架、库存是否充足。订单号的生成方式有很多种常见做法是用时间戳加随机数或者用 Redis 自增序列这里给出一种不依赖额外组件的方案PostMapping(/order/create) Transactional(rollbackFor Exception.class) public Result createOrder(RequestBody OrderCreateDTO dto, RequestAttribute(userId) Integer userId) { // 校验景点状态 ScenicSpot spot scenicSpotService.getById(dto.getSpotId()); if (spot null || spot.getStatus() ! 1) { return Result.error(景点不存在或已下架); } // 计算价格门票按人数民宿按房间数 BigDecimal totalPrice spot.getTicketPrice() .multiply(new BigDecimal(dto.getPersonCount())); if (dto.getHomestayId() ! null) { Homestay homestay homestayService.getById(dto.getHomestayId()); totalPrice totalPrice.add(homestay.getPrice()); } // 生成订单号yyyyMMddHHmmss 4位随机数 String orderNo LocalDateTime.now().format( DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) String.format(%04d, new Random().nextInt(10000)); Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setSpotId(dto.getSpotId()); order.setHomestayId(dto.getHomestayId()); order.setVisitDate(dto.getVisitDate()); order.setPersonCount(dto.getPersonCount()); order.setTotalPrice(totalPrice); order.setStatus(0); orderService.create(order); return Result.success(orderNo); }代码里用 Transactional 注解保证订单写入失败时能回滚避免出现订单表里多了一条脏数据但前端没收到返回的情况。RequestAttribute(userId) 是从拦截器里解析出的用户标识登录拦截器在请求进入 Controller 前通过 token 查到了用户 ID放到 request 属性里。下单接口需要关注的是金额计算逻辑要放在服务端而不是小程序端因为客户端传过来的价格可以伪造。支付环节在毕设里一般用模拟支付代替也就是生成订单后直接跳转到“已提交”页面等管理员在后台确认即可。3. 微信小程序端的消费闭环wx.login、首页加载与预约提交3.1 小程序的全局配置与 request 请求封装小程序端的目录结构按功能划分pages 下放页面utils 下放公共方法api 目录下集中管理后端接口调用。入口文件 app.js 里需要定义全局的基础配置包括后端服务的 baseURL、用户登录后的 token以及全局的登录状态检查逻辑。小程序请求后端接口时需要处理的问题有两个一是请求头要携带 token用于身份识别二是当 token 过期或未登录时要引导用户重新走登录流程。下面是一段常见的 request 封装代码写在 utils/request.js 里const BASE_URL http://localhost:8080; function request(url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method || GET, data: data || {}, header: { Content-Type: application/json, token: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 200 res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(res.data); } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request, BASE_URL };这段封装的逻辑是所有请求自动携带本地存储的 token响应回来时先判断 HTTP 状态码再判断业务码 code401 表示登录态失效需要清掉本地 token 并跳转到登录页。这样做的好处是所有页面不需要重复写错误处理逻辑直接调用 request 方法就能拿到业务数据。真正的项目中还需要留意 BASE_URL 的取值本地调试时用 localhost 或局域网 IP真机预览时必须改成已备案的 HTTPS 域名否则会被微信拦截这一点在第四章会详细说明。3.2 登录流程与页面加载从 wx.login 到换取用户信息小程序的登录和传统 Web 登录有很大区别核心在于微信提供了 wx.login 这个 API它不需要用户输入用户名密码而是直接获取一个临时凭证 code后端拿着这个 code 去微信的服务端接口换取 openid再像普通登录一样下发自定义 token。在智慧乡村旅游平台这个场景下登录页不应该成为游客使用的阻碍。打开小程序进入首页看景点、逛民宿完全不需要登录只有提交预约订单、发表评论时才强制登录。所以登录应该做成“懒触发”的用户在点击预约按钮时检查本地是否已有 token没有就走微信授权登录流程。登录页的核心代码Page({ data: { userInfo: {} }, onLoad() { if (wx.getStorageSync(token)) { wx.switchTab({ url: /pages/index/index }); } }, handleLogin() { wx.login({ success: (res) { if (res.code) { // 将 code 发送到后端换取 token wx.request({ url: http://localhost:8080/api/auth/login, method: POST, data: { code: res.code, nickname: }, success: (resp) { wx.setStorageSync(token, resp.data.data.token); wx.setStorageSync(userInfo, resp.data.data.userInfo); wx.navigateBack(); } }); } } }); } });后端拿到 code 后通过微信的 jscode2session 接口换取 openid再查自己的用户表如果 openid 已存在就更新登录时间不存在就自动创建新用户这就是“免注册登录”的实现原理。这套流程在毕设答辩时是个亮点能讲清楚 code 换 openid 的整个链路比直接做账号密码登录更能体现对微信生态的理解。需要注意的是code 只能用一次且有效期只有几分钟后端拿到 code 后应立即调用微信接口换取 openid不要做缓存。3.3 首页数据渲染景点列表的加载与下拉刷新首页是游客打开小程序后看到的第一个页面通常包含顶部轮播图、景点分类导航、景点推荐列表三块内容。数据结构上建议后端一次性返回减少请求次数也就是提供一个聚合接口同时返回 banner 数据和景点列表。这里的实现方案是设计一个 /api/home 接口返回格式如下{ banners: [ { id: 1, imageUrl: http://.../banner1.jpg, linkId: 3 }, { id: 2, imageUrl: http://.../banner2.jpg, linkId: 7 } ], recommendSpots: [ { id: 1, name: 青石桥古村, coverUrl: http://..., ticketPrice: 30.00, rating: 4.8 }, { id: 2, name: 龙泉山步道, coverUrl: http://..., ticketPrice: 0.00, rating: 4.5 } ] }小程序端的 index.js 里调用 request 方法获取数据再把结果 setData 到页面上。这里有一个实际开发中非常容易遇到的问题setData 的数据量不能太大如果后端一次性返回几十条含富文本的景点数据小程序的渲染会出现明显卡顿。解决方案是首页只取景点名称、封面图、价格这几个核心字段详情页再单独请求完整数据。下面是小程序首页加载的核心代码const { request } require(../../utils/request.js); Page({ data: { banners: [], spots: [], pageNum: 1, pageSize: 6, loading: false, hasMore: true }, onLoad() { this.loadHomeData(); this.loadSpots(); }, loadHomeData() { const that this; request(/api/home, GET).then(data { that.setData({ banners: data.banners }); }); }, loadSpots() { const that this; if (!this.data.hasMore || this.data.loading) return; this.setData({ loading: true }); const params { pageNum: this.data.pageNum, pageSize: this.data.pageSize }; request(/api/spot/list, GET, params).then(list { const spots that.data.spots.concat(list); that.setData({ spots: spots, pageNum: that.data.pageNum 1, hasMore: list.length that.data.pageSize, loading: false }); }); }, onPullDownRefresh() { this.setData({ spots: [], pageNum: 1, hasMore: true }); this.loadSpots(); wx.stopPullDownRefresh(); }, onReachBottom() { this.loadSpots(); } });代码里实现了两个关键能力触底分页加载和下拉刷新。onReachBottom 是页面滚动到底部的生命周期事件在这里调用 loadSpots 加载下一页onPullDownRefresh 配合页面 JSON 配置里的 “enablePullDownRefresh”: true 一起使用。分页加载时用 loading 标志位防止重复请求hasMore 判断是否还有更多数据。这种写法在实际项目中很通用游客刷到列表底部自动加载更多体验上比一次性加载完更好。3.4 预约表单的校验与订单提交预约流程是用户核心操作。从景点详情页点击“预约”按钮后进入预约确认页页面展示所选景点、入园日期选择器、游客人数选择器如果该景点关联了民宿还会展示民宿列表供用户勾选。用户填写完信息点击“提交订单”前端先做一轮本地校验再调用后端接口创建订单。表单页面需要注意的细节是日期选择器小程序的 picker 组件 mode 设为 date同时要限制可选范围不能早于今天。人数选择器用 stepper 组件实现最小值为 1最大值为 10超出范围时给出提示。提交前需要校验的内容包括是否已选择日期、人数是否为有效数字、是否已勾选要预约的民宿。以下是提交订单的代码submitOrder() { const that this; const { spot, visitDate, personCount, homestayId } this.data; if (!visitDate) { wx.showToast({ title: 请选择游玩日期, icon: none }); return; } if (!personCount || personCount 1) { wx.showToast({ title: 请选择人数, icon: none }); return; } const token wx.getStorageSync(token); if (!token) { wx.navigateTo({ url: /pages/login/login }); return; } request(/api/order/create, POST, { spotId: spot.id, homestayId: homestayId, visitDate: visitDate, personCount: personCount }).then(orderNo { wx.showToast({ title: 预约成功, icon: success }); setTimeout(() { wx.redirectTo({ url: /pages/order/detail?orderNo orderNo }); }, 1500); }); }这段代码里先做了用户是否登录的判断未登录就跳转登录页登录成功后用户还需要重新点击提交因为提交时不会自动重放刚才的操作。预约成功拿到订单号后跳转到订单详情页这个页面展示订单的完整信息包括订单状态、游客信息、费用明细。订单列表页则通过 /api/order/list 接口拉取当前用户的历史订单按状态筛选常见的有全部、待确认、已完成、已取消四个页签。4. 联调排错与上线前配置的检查清单4.1 真机预览必查项域名、HTTPS 与开发者工具设置开发阶段可以用开发者工具的“不校验合法域名”选项跳过微信的域名检查直接请求 http://localhost:8080 或局域网 IP。但一旦要在真机上预览或者想发给别人体验就必须面对微信的强制要求request 请求的域名必须是已备案的 HTTPS 域名且需要在微信公众平台的“开发管理-服务器域名”里配置 request 合法域名。真机调试常见的问题是配置了合法域名但请求依然失败。排查时先看开发者工具右上角的“详情-本地设置”里是否勾选了“不校验合法域名”选项如果是在正式环境这个选项默认不能开。接着检查域名证书小程序不支持自签名证书必须使用正规 CA 机构签发的证书。后端如果部署在服务器上还要检查 Nginx 配置是否正确转发请求确保没有把 HTTPS 请求重定向到 HTTP 端口。开发者工具里 Network 面板可以查看请求的完整信息包括请求头、响应体、耗时。真机调试时建议用微信开发者工具的“真机调试”功能它会生成一个临时预览二维码同时在工具里显示真机请求日志配合 vConsole 插件可以在手机页面上直接查看 console 输出和网络请求。这个流程是联调阶段最常用的排错手段。4.2 request:fail 与 code 401两类高频报错的定位思路小程序开发过程中最常见的报错可以归为两类。第一类是 request:fail表示请求根本没有到达后端。排查方向依次是后端服务是否启动、IP 和端口是否可达、域名是否配置、证书是否有效、请求的 URL 是否与后端实际路径一致。一个很容易忽略的坑是后端接口用了 context-path 前缀比如 spring.mvc.servlet.path/api那么前端 BASE_URL 里就不能再写 /api否则会出现 404。第二类是 code 401也就是登录态失效。从拦截器返回 401 的时机来判断一般是 token 为空、token 过期、token 对应的用户不存在三种情况。后端拦截器里建议对这三种情况分别返回不同的 message这样前端能更精准地提示用户。常见做法是前两种提示“请重新登录”最后一种提示“账号已注销”。定位这类问题时先看开发者工具 Network 面板里请求头是否带上了 token再看后端日志里解析 token 时是否报错逐层缩小范围。4.3 管理员后台与小程序端的权限差异对比毕设里的管理系统通常还有一个 Web 端管理后台使用相同的 SSM 架构只是没有小程序端的模板页面。管理员端和小程序端的权限控制方式不同小程序端通过微信登录后获得 token接口层通过拦截器验证用户身份管理后台则使用传统的用户名密码登录会话通过 Session 维护同时用拦截器验证用户角色是否为管理员。两套权限体系放在同一个 Spring 项目里需要特别注意的是拦截路径的配置。常见做法是定义两个拦截器一个拦截 /api/** 路径验证小程序 token另一个拦截 /admin/** 路径验证管理员 Session两个拦截器互不干扰。实际编码中要避免把两个拦截器配置成相同的路径否则会出现请求被第一个拦截器直接放行、到达第二个拦截器时因为 Session 不存在而误判为未登录的情况。5. 小程序端三个高频操作坑位图片上传、富文本渲染与下拉刷新失效5.1 图片上传的两种方案与压缩处理景点和民宿的图片上传是后台管理的必备功能也是小程序端比较容易出问题的地方。后台管理选择图片后通常有两种处理方式一种是把图片 base64 直接作为字符串提交到后端存入数据库另一种是以 multipart/form-data 的方式上传到服务器数据库只保存图片 URL。前者实现简单但数据量大刷新页面时会明显变慢后者是主流做法也是面试中能讲出亮点的点。PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file, HttpServletRequest request) { if (file.isEmpty()) { return Result.error(文件为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) ext; // 获取项目运行目录下的 upload 文件夹 String realPath System.getProperty(user.dir) /upload/; File dir new File(realPath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(realPath fileName)); String url /upload/ fileName; return Result.success(url); }这段上传代码把文件保存在本地磁盘URL 路径返回给前端用于拼接完整访问地址。部署时需要注意本地磁盘保存的图片不会跟着项目打包走如果更换服务器图片文件需要单独迁移。更好的做法是把图片放到云存储上或者用 Nginx 把 /upload 路径映射到一个独立目录这样前后端分离部署时图片依然能正常访问。微信小程序的 chooseMedia 接口可以指定 sizeType 为 compressed这样用户在选图时就能直接拿到压缩后的图片减少服务端存储压力。5.2 富文本内容在微信小程序里的标签兼容处理景点介绍这类长文本后台通常用富文本编辑器录入生成的内容是一段 HTML。网页端直接渲染没有问题但小程序的 rich-text 组件对 HTML 标签的支持有限部分标签不识别遇到 script 或 iframe 标签会直接报错或被忽略。推荐的方案是后端在返回富文本内容时对 HTML 做一次过滤去掉 script、iframe、style 等标签只保留 p、img、h1-h3、ul、li 等基础标签。过滤操作可以在后端统一处理也可以在接口层单独封装public static String cleanHtml(String html) { if (html null) return ; return html.replaceAll(script[^]*.*?/script, ) .replaceAll(iframe[^]*.*?/iframe, ) .replaceAll(style\\s*\\s*\[^\]*\, ) .replaceAll(on[a-zA-Z]\\s*\\s*\[^\]*\, ); }过滤器只做最简单的正则移除实际项目中可以用 Jsoup 这样的 HTML 解析库来清洗更安全也更规范。另外小程序端渲染富文本时img 标签需要设置图片宽度否则大图会超出屏幕。推荐的做法是在后端过滤时给 img 标签加一个 stylemax-width:100%;height:auto;或者在小程序端对富文本内容做后处理。5.3 下拉刷新失效与 onReachBottom 不触发的排查方法下拉刷新失效是一个在真机上偶尔复现、在开发者工具里却正常的问题。排查时先确认页面 json 配置里 enablePullDownRefresh 是否设为 true如果是在 app.json 的 window 项里全局开启部分页面又单独关闭了也会导致刷新失效。还有一点常被漏掉backgroundTextStyle 如果设成了白色在白色背景下刷新动画几乎看不清。onReachBottom 不触发的常见原因有页面本身高度不足没有撑满整个屏幕导致永远无法滚动到底部列表数据没有撑开 scroll 容器页面使用了 scroll-view 组件并设置了固定高度此时页面级 onReachBottom 无法触发需要在 scroll-view 上绑定 bindscrolltolower 事件。逻辑上是这样页面级滚动和 scroll-view 内部滚动是两套事件体系用 scroll-view 做长列表时页面本身就是固定不变的所以页面级的触底事件自然失效。把列表外层交给页面滚动或者改用 scroll-view 的 lower 事件二选一就能解决。本文还有配套的精品资源点击获取
返回列表