微信小程序旅游服务软件:Java后端与数据库设计实战

📅 发布时间:2026/10/18 20:40:28
微信小程序旅游服务软件:Java后端与数据库设计实战
简介这是一份面向计算机毕业设计或小程序开发学习的微信旅游服务软件完整工程后端基于Java与SSM框架开发前端为微信小程序支持直接导入IDEA和微信开发者工具运行无需额外改配置。项目围绕景区旅游业务展开涵盖旅游攻略、旅游资讯、景点与酒店信息检索、门票预订、酒店预订、论坛中心、推荐路线、我要发帖以及用户管理等模块把游客浏览、搜索、预订、互动与后台管理串成完整闭环适合作为课程设计、毕业设计或SSM小程序全栈实战的参考范本。整个资源包共22个文件包含19张png界面运行截图、2个zip工程压缩包和1个sql数据库初始化脚本整体大小约61.15MB截图便于核对页面效果两个压缩包分别对应后端工程与小程序前端SQL脚本用于初始化MySQL 8.0数据库能够明显缩短环境准备时间。已有2604人学习下载读者可对照源码理解旅游类小程序的业务建模、前后端接口联调与后台管理设计也能在现有基础上扩展推荐算法、支付接口等功能是一份可运行、可改造的落地项目资料。1. 基于微信小程序旅游服务软件省心的是源码和数据库文件都到位做过小程序的人都知道最磨人的不是写页面而是后端接口和数据库对不上。这套「微信小程序旅游服务软件」的定位很明确前端小程序负责展示、搜索、下单后端用 Java 全家桶提供接口数据库文件直接导入就能跑整个旅游业务闭环——景点列表、关键词搜索、景区详情、路线推荐、订单管理——全部串起来。Java 后端在这个场景里是最稳的选择生态成熟、招人好招、部署资料多跟小程序做前后端分离也顺。适合两类人一是拿它做毕业设计或课程项目的学生二是想快速搭一个旅游类小程序原型、验证业务逻辑的开发者。下面我从架构到落地把每一步讲清楚。2. 从页面到后端旅游小程序的整体链路设计与技术选型2.1 小程序端的页面结构与数据流先看前端。旅游服务小程序通常由五个核心页面组成首页、景点列表页、景区详情页、个人中心页、订单列表页。业务闭环是用户进入首页看到推荐景点点进详情页了解信息决定后下单订单状态在个人中心里追踪。页面之间通过wx.navigateTo跳转数据则统一走wx.request从后端拉。每个页面的结构都遵循微信小程序的固定模式.wxml负责结构.wxss负责样式.js负责逻辑和数据处理。以景点列表页为例核心逻辑是onLoad生命周期里根据关键词请求后端接口拿到 JSON 数据后用setData绑定到页面视图层自动渲染。下面这段代码是列表页最常见的请求写法Page({ data: { spotList: [], keyword: , loading: false, page: 1 }, onLoad(options) { // options 里可能携带从首页跳转过来的关键词 this.setData({ keyword: options.keyword || }); this.fetchSpots(); }, fetchSpots() { if (this.data.loading) return; this.setData({ loading: true }); wx.request({ url: https://yourdomain.com/api/spot/search, method: GET, data: { keyword: this.data.keyword, page: this.data.page, size: 10 }, success: (res) { // 约定 code 为 0 表示业务成功后面章节会讲统一返回结构 if (res.data.code 0) { const list res.data.data || []; this.setData({ spotList: this.data.page 1 ? list : this.data.spotList.concat(list) }); } }, fail: (err) { console.error(请求失败, err); wx.showToast({ title: 网络异常, icon: none }); }, complete: () { this.setData({ loading: false }); } }); } });几个参数要说明。keyword是搜索关键词page和size控制分页——旅游类列表通常数据量大一次性拉全量数据会让首屏白屏好几秒分页是必须的。loading标志位防止用户快速滚动时重复触发请求这在 onReachBottom 触底加载时尤其关键。值得注意的一个坑是success回调里的res.data.code这里的code是我们后端自定义的业务状态码跟 HTTP 状态码是两回事。如果后端返回 HTTP 200 但业务失败前端要单独判断code而不是只看statusCode。2.2 Java 后端框架选型Spring Boot 为什么是默认答案后端 Java 生态里现在做小程序接口Spring Boot 几乎就是默认选择不需要犹豫。原因有三第一内置 Tomcat打包成 jar 直接java -jar就能跑部署成本极低第二Spring MVC 的注解开发方式天然适合 RESTful 接口一个RestController就能暴露 HTTP 接口不用像传统 SSM 那样写大量 XML 配置第三生态完整MyBatis、MySQL、Redis、JWT 这些小程序后端常用的组件都有成熟的 starter 集成。相比早期的 Servlet JSP 方案Spring Boot 把“接口开发”这件事简化到了极致。你不需要关心 DispatcherServlet 怎么配置不需要写 web.xml甚至不需要手动管理 Bean 生命周期。对于旅游服务这种以 CRUD 为主的业务系统Spring Boot 的默认配置覆盖了绝大多数场景框架帮你省下的时间可以全部投入到业务逻辑上。社区资料也多真遇到问题搜解决方案的效率远高于冷门框架。2.3 前后端接口约定RESTful 设计与统一返回结构前后端联调最容易出问题的不是接口功能而是返回格式不统一。有的接口返回{status: 1}有的返回{code: success}前端每接一个接口就要写一套解析逻辑改起来想打人。所以做小程序后端第一件事就是定统一返回结构。我通常定义一个泛型类ResultT所有接口强制返回这个类型Data public class ResultT { private int code; // 业务状态码0 表示成功非 0 表示失败 private String message; // 提示信息 private T data; // 真正的业务数据 public static T ResultT success(T data) { ResultT result new Result(); result.code 0; result.message ok; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 1; result.message message; return result; } }接口路径按照资源名来组织。景点资源是/api/spot订单资源是/api/order用户是/api/user配合 HTTP 方法表达操作语义GET /api/spot/list获取景点列表GET /api/spot/detail?id1获取详情POST /api/order/create创建订单。这里有个设计细节有的团队喜欢把所有接口都写成POST省得考虑参数传递但 GET/POST 语义分开在排查问题时更直观——GET 请求可以直接在浏览器里打开调试POST 请求还得借助工具。旅游服务这类读多写少的系统GET 接口占了多数语义分开的收益很明显。3. 后端 Java 核心模块实现景点搜索、微信登录与订单状态控制3.1 景点搜索模块Controller-Service-Mapper 三层的标准写法后端代码的组织方式直接决定了后续可维护性。旅游系统里最简单的景点查询也值得按三层结构来写。Controller 层只负责参数接收和结果返回不写任何业务逻辑Service 层处理业务规则比如关键词为空时返回默认推荐列表Mapper 层跟数据库打交道SQL 写在 XML 里跟 Java 代码分离方便后续调优。先看 ControllerRestController RequestMapping(/api/spot) public class SpotController { Autowired private SpotService spotService; GetMapping(/search) public ResultPageResultSpotVO search(RequestParam(required false) String keyword, RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size) { // 关键词为空时走默认推荐逻辑 if (keyword null || keyword.trim().isEmpty()) { return Result.success(spotService.recommend(page, size)); } return Result.success(spotService.searchByName(keyword, page, size)); } }再看 Service 层。这里分页和关键词过滤放在一起处理避免LIKE拼接的 SQL 注入风险Service public class SpotService { Autowired private SpotMapper spotMapper; public PageResultSpotVO searchByName(String keyword, int page, int size) { // 计算 offsetMySQL 分页使用的是 limit offset, size int offset (page - 1) * size; ListSpotVO records spotMapper.selectByName(keyword.trim(), offset, size); Long total spotMapper.countByName(keyword.trim()); PageResultSpotVO pageResult new PageResult(); pageResult.setRecords(records); pageResult.setTotal(total); pageResult.setPage(page); pageResult.setSize(size); return pageResult; } }最后 Mapper 接口和 XMLpublic interface SpotMapper { ListSpotVO selectByName(Param(keyword) String keyword, Param(offset) int offset, Param(size) int size); Long countByName(Param(keyword) String keyword); }select idselectByName resultTypecom.example.vo.SpotVO SELECT id, name, city, summary, cover_image, price, hot FROM t_spot WHERE name LIKE CONCAT(%, #{keyword}, %) ORDER BY hot DESC LIMIT #{offset}, #{size} /select select idcountByName resultTypelong SELECT COUNT(*) FROM t_spot WHERE name LIKE CONCAT(%, #{keyword}, %) /select参数说明LIMIT #{offset}, #{size}是 MySQL 的分页语法offset从 0 开始所以前端传的page1在 Service 层要转成offset0。分页必须搭配COUNT(*)查询返回总条数否则前端无法计算总页数也就没法知道什么时候触发触底加载。这里有个小细节selectByName和countByName两条 SQL 的WHERE条件必须保持完全一致否则会出现总条数和列表对不上的问题这是分页最容易翻车的点。3.2 微信登录链路code 换 session 到生成 Token 的完整流程旅游系统涉及下单和订单查询必须有用户身份。微信小程序的标准登录方式是wx.login()获取临时code后端拿这个code调用微信接口换取openid再用openid作为用户的唯一标识。这套流程不复杂但有一个容易被忽略的坑code只能用一次五分钟过期所以后端拿到code后要立刻调用微信接口不能落库也不能重放。Controller 层处理登录请求RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public ResultLoginVO login(RequestBody LoginDTO loginDTO) { // loginDTO 里只有两个字段code 和 nickname String code loginDTO.getCode(); if (code null || code.isEmpty()) { return Result.error(code不能为空); } return Result.success(userService.login(code, loginDTO.getNickname())); } }Service 层的核心是调微信接口解析openid。这里用 Spring 的RestTemplate发起 GET 请求Service public class UserService { private static final String WX_URL https://api.weixin.qq.com/sns/jscode2session?appid%ssecret%sjs_code%sgrant_typeauthorization_code; Value(${wx.appid}) private String appid; Value(${wx.secret}) private String secret; Autowired private UserMapper userMapper; public LoginVO login(String code, String nickname) { // 1. 用 code 换 openid 和 session_key String url String.format(WX_URL, appid, secret, code); String responseText new RestTemplate().getForObject(url, String.class); // 2. 解析 JSON 响应 JSONObject json JSON.parseObject(responseText); String openid json.getString(openid); if (openid null) { // 这里一定是 code 无效或过期微信会返回 errcode 和 errmsg throw new BusinessException(微信登录失败: responseText); } // 3. 查数据库不存在则新注册 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(nickname); userMapper.insert(user); } // 4. 生成 Token后续请求带着 Token 识别用户 String token JwtUtil.generateToken(user.getId(), openid); LoginVO vo new LoginVO(); vo.setToken(token); vo.setUserId(user.getId()); vo.setNickname(user.getNickname()); return vo; } public User getUserByToken(String token) { Long userId JwtUtil.parseToken(token); return userId null ? null : userMapper.selectById(userId); } }这个流程里有一个安全细节微信接口返回的openid是用户的唯一身份标识但session_key不要返回给前端更不能存到自己数据库里。session_key是解密用户手机号等敏感信息的密钥暴露给前端等于把钥匙交出去了。JWT 的generateToken我通常把userId和openid放进去过期时间设 7 天旅游类应用用户登录频率不高太短会让用户体验变差。3.3 订单模块状态机设计与事务控制旅游订单跟电商订单不一样它的状态流转相对简单但状态枚举必须设计清楚。常见的是四态待支付0→ 已支付1→ 已完成2以及从待支付可以走到已取消3。这个状态机不允许越级跳转比如已取消的订单不能再变成已支付已完成的订单不能再取消。实现方式就是 Service 层每个方法开头检查当前状态不合法就抛出异常。订单创建涉及多个表的写操作必须加事务。比如购买一条旅游路线要插入订单表、扣减路线库存、记录订单日志任何一个步骤失败都要整体回滚否则就会出现订单存在但库存没扣的脏数据Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private RouteMapper routeMapper; Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderDTO dto) { // 1. 校验用户是否存在从 Token 解析出的 userId // 2. 校验路线是否存在且可预订 Route route routeMapper.selectById(dto.getRouteId()); if (route null || route.getStock() 0) { throw new BusinessException(该路线暂无余位); } // 3. 生成订单号时间戳 用户ID 随机数避免并发冲突 String orderNo T System.currentTimeMillis() dto.getUserId() (int) (Math.random() * 9000 1000); // 4. 插入订单记录初始状态为待支付 0 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(dto.getUserId()); order.setRouteId(dto.getRouteId()); order.setStatus(0); order.setTotalPrice(route.getPrice()); orderMapper.insert(order); // 5. 扣减库存 int updated routeMapper.decreaseStock(dto.getRouteId()); if (updated 0) { throw new BusinessException(库存扣减失败请重试); } // 6. 返回订单信息给前端前端引导用户去支付 OrderVO vo new OrderVO(); vo.setOrderNo(orderNo); vo.setTotalPrice(route.getPrice()); vo.setStatus(0); return vo; } }事务注解Transactional(rollbackFor Exception.class)里的rollbackFor很重要——Spring 默认只在遇到 RuntimeException 时回滚如果业务异常是自定义的BusinessException要确保它继承 RuntimeException否则事务不回滚。库存扣减用UPDATE ... SET stock stock - 1 WHERE id ? AND stock 0通过受影响行数是否为 0 判断是否超卖这是避免并发下库存变负数的最简单有效的办法。4. 数据库文件设计核心表结构、初始化 SQL 与索引优化4.1 旅游业务的核心表设计与字段说明数据库是整个系统最不能糊弄的部分。旅游服务软件的数据库文件我建议至少包含五张核心表用户表、景点表、路线表、订单表、收藏表。表名用t_前缀区分业务表字段用全小写下划线风格MySQL 里大小写敏感配置不一致时这种命名最安全。表名职责关键字段t_user微信用户信息openid唯一索引、nickname、avatar、phone、create_timet_spot景点/景区信息name、city、summary、detail、cover_image、price、hott_route旅游路线title、spot_ids逗号分隔景点ID、days、price、stockt_order订单记录order_no唯一索引、user_id、route_id、status、total_pricet_favorite用户收藏user_id、spot_id、create_time联合唯一索引字段类型有几个细节值得注意。openid用 VARCHAR(64) 就行微信返回的 openid 通常是 28 位但留点余量。price用 DECIMAL(10,2) 而不是 FLOAT——金额用浮点数在累加和比较时会出现 0.1 0.2 ! 0.3 的经典问题。status用 TINYINT 就够配合 Java 侧的状态枚举使用不要直接用中文存状态查起来慢不说改状态名还要刷数据。create_time默认值设CURRENT_TIMESTAMP这样插入语句不写时间字段也能自动填充。4.2 直接能导入的初始化 SQL 脚本项目交付的数据库文件形态是.sql脚本。拿到手在 Navicat 或者命令行里执行一遍库、表、基础数据就全有了。下面是最小可用的建库建表脚本-- 创建数据库utf8mb4 才能完整保存 emoji 表情 CREATE DATABASE IF NOT EXISTS travel_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE travel_db; -- 用户表 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL, nickname VARCHAR(64) DEFAULT , avatar VARCHAR(255) DEFAULT , phone VARCHAR(20) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 景点表 CREATE TABLE t_spot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL, city VARCHAR(64) DEFAULT , summary VARCHAR(255) DEFAULT , detail TEXT, cover_image VARCHAR(255) DEFAULT , price DECIMAL(10,2) DEFAULT 0.00, hot INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_city (city), KEY idx_hot (hot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 路线表spot_ids 用逗号分隔关联景点ID简单场景下避免建关联表 CREATE TABLE t_route ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, spot_ids VARCHAR(255) NOT NULL, days INT DEFAULT 1, price DECIMAL(10,2) DEFAULT 0.00, stock INT DEFAULT 100, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, route_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已完成 3已取消, total_price DECIMAL(10,2) DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;插入一些景点演示数据方便小程序端直接展示INSERT INTO t_spot (name, city, summary, price, hot) VALUES (龙脊梯田, 桂林, 梯田层层叠叠四季风景各不相同, 80.00, 120), (鼓浪屿, 厦门, 海岛风情老建筑与海滩的完美结合, 100.00, 156), (黄山风景区, 黄山, 奇松、怪石、云海、温泉四绝, 190.00, 230), (西湖景区, 杭州, 三面云山一面城四季玩法不同, 0.00, 300);执行 SQL 时要注意如果之前已经导过一遍再导会报Table already exists先DROP TABLE IF EXISTS还是直接忽略取决于你想保留哪份数据。另外TEXT类型的detail字段不适合建索引MySQL 对 TEXT 字段做索引有长度限制常规做法是只给name和city加索引详情内容靠全文搜索引擎比如 Elasticsearch解决小项目拿 LIKE 凑合就行。4.3 索引设计搜索场景最容易踩的慢查询坑给表加索引不是越多越好而是要看查询模式。旅游系统最常见的查询是用户按城市筛选景点和搜索景点名称。对应到 SQL就是WHERE city ?和WHERE name LIKE %keyword%。前者用普通索引就能加速后者却有天然缺陷。LIKE %keyword%这种前后都带百分号的写法MySQL 无法使用 B Tree 索引加速只能全表扫描。数据量在几百条时感觉不出来到了几万条就能看出区别。解决思路有几个一是改成LIKE keyword%右模糊匹配前面不带百分号索引能用但语义受限二是给name字段加前缀索引比如ALTER TABLE t_spot ADD INDEX idx_name (name(10))只对前 10 个字符建索引能在一定程度上加速三是系统做大后引入全文索引或者搜索引擎但这超出了项目源码的范畴。我的建议是课程设计和小型项目阶段保留idx_city和idx_hot两个索引搜索功能用右模糊或者干脆先全表扫等用户量上来再优化。过早引入搜索引擎对初学者反而是负担MySQL 本身能承载的量级在旅游类原型阶段完全够用。5. 微信小程序与 Java 后端联调的避坑指南真机白屏、Token 过期与 8 小时时差5.1 开发工具正常但真机白屏域名和 HTTPS 的配置问题现象在微信开发者工具里一切正常数据加载流畅一用手机预览页面要么白屏要么一直转圈控制台报request:fail。原因开发者工具默认勾选了“不校验合法域名”所有请求直接放行。真机上微信强制要求请求地址必须是 HTTPS 且域名已备案并且要在小程序后台的「开发设置 - 服务器域名」里配置。如果直接拿http://192.168.1.100:8080这种局域网地址去真机调试必挂。解决本地调试阶段不必立刻申请域名开发者工具里勾选不校验合法域名就行。但真要上线或者给演示用两条路一是买域名和云服务器配 Nginx 做 HTTPS 反代后端 Java 服务监听内网端口二是用内网穿透工具把本地 8080 端口映射成一个 HTTPS 域名适合临时演示。注意小程序后台配置的域名不能带端口号https://api.example.com可以https://api.example.com:8443不行。这是个隐蔽的坑很多人配了半天发现是端口惹的祸。5.2 Token 过期失效小程序端 401 自动重登现象用户一段时间没操作再点某个功能时报“登录已过期”必须重新进入小程序才能用。开发时为了省事不少人的做法是让用户退出重进这体验很拉胯。原因Token 过期后后端接口返回业务码非 0比如 401小程序端每个请求都做一次拦截判断才能统一处理重新登录。解决封装wx.request统一处理响应。拦截到 Token 失效时静默调用wx.login()重新走一遍登录流程拿到新 Token 后把原来的请求重放一遍。这样用户无感知不用手动重进function requestWithAuth(options) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ ...options, url: options.url, header: { Content-Type: application/json, Authorization: token ? Bearer token : }, success: (res) { // 判断业务码Token 失效时 res.data.code 定义成 401 if (res.data.code 401) { // 静默重新登录成功后重放当前请求 wx.login({ success: (loginRes) { wx.request({ url: https://yourdomain.com/api/user/login, method: POST, data: { code: loginRes.code }, success: (loginRel) { wx.setStorageSync(token, loginRel.data.data.token); // 重放原请求 wx.request({ ...options, header: { Authorization: Bearer loginRel.data.data.token }, success: (retryRes) resolve(retryRes.data), fail: reject }); } }); } }); return; } resolve(res.data); }, fail: reject }); }); }这段封装代码的逻辑是所有请求统一走requestWithAuth携带 Token 发出去收到 401 业务码时说明 Token 失效先静默重新登录拿新 Token再重放原请求。有一个细节要注意重放逻辑里必须判断code而不是 HTTPstatusCode因为我们的接口即使业务失败也返回 HTTP 200只有 Token 失效这个业务场景返回code401。5.3 数据库时间与本地时间差了 8 小时现象数据库里存的create_time是凌晨 2 点页面上显示的是前一天 18 点。或者反过来页面上传的时间到库里多了 8 小时。原因MySQL 连接串里没有指定时区服务器时间用的是 UTC而本地是中国标准时间 UTC8。Java 后端读取 DATETIME 时按服务器默认时区解释存入时又按另一个时区转换来回一折腾就差了 8 小时。解决JDBC 连接串显式指定时区这是最省事的方案spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiserverTimezoneAsia/Shanghai这个参数必须在连接串里出现。此外MySQL 的DATETIME类型本身不存时区信息全靠连接的时区做解释所以统一时区是唯一的解法。Java 应用服务器的启动参数最好也加上-Duser.timezoneAsia/Shanghai双保险。排查这类问题一条命令就能定位在 MySQL 里执行SELECT NOW();如果返回的是 UTC 时间那就是连接时区配错了。5.4 图片加载失败和冷启动白屏资源路径与缓存策略现象景点列表能正常显示文字但图片全部裂掉或者小程序首次打开时白屏时间特别长。原因图片路径写成了相对路径或者本地路径比如cover_image存的是/upload/xxx.jpg。小程序端上相对路径会拼到小程序的代码包路径下而不是你的服务器路径上图片自然加载不出来。冷启动白屏则是首屏数据加载慢请求串行导致。解决cover_image字段必须存完整的 URL比如https://yourdomain.com/images/spot/xxx.jpg。后端返回数据时如果发现字段是相对路径要么改数据库数据要么在接口层拼接域名前缀。冷启动优化方面首页的景点列表接口可以跟wx.login并行发起不要串行等登录完成才拉数据。另外一个常见技巧是优先加载文字和骨架屏图片用懒加载image组件的lazy-load属性设为true列表滚动到可视区域才去拉图对弱网环境提升非常明显。6. 部署验证的最后一公里把整套系统跑通并确认每个环节都合格拿到项目源码和数据库文件后第一步别急着改代码先把原封不动的系统跑起来确认基线是正常的。我有一次直接改了数据库字段结果自己把自己搞晕分不清是原项目 bug 还是我改出来的问题。正确的顺序是走完下面四步再动任何代码。第一步导入数据库文件。用 Navicat 连接 MySQL执行travel_db.sql确认表数量和数据条数跟 README 描述一致。第二步改后端配置。打开application.yml改数据库账号密码再改wx.appid和wx.secret没有真实小程序账号时可以先在微信公众平台注册一个测试号AppID 和密钥在开发设置里拿。第三步启动 Java 服务。项目根目录执行mvn spring-boot:run或用 IDE 直接跑启动类看到 Tomcat started 日志后浏览器访问http://localhost:8080/api/spot/list能返回 JSON 数组说明后端通了。第四步微信开发者工具导入小程序端代码改utils/config.js里的接口地址为本机http://localhost:8080勾选不校验合法域名真机预览走通登录、搜索、详情、下单全流程。这几步跑通了整个项目的核心链路就验证完了剩下的就是业务层面的调整。我的习惯是把“登录 → 搜索景点 → 查看详情 → 下单 → 查看订单”这五步操作在手机上录个屏跑一遍就知道前端页面和后端接口有没有割裂。这里还想强调一个跟代码无关但很实际的事这种带数据库文件的项目数据库脚本是整个交付物的灵魂。别人拿到手能不能一键跑起来全看建表语句和执行顺序是否干净。把编码统一成utf8mb4、字段注释写全、索引命名规范这些细节看着不起眼但真正维护过的人才知道它们多值钱。希望这篇笔记帮到你少走几步已经踩过的弯路。本文还有配套的精品资源点击获取