
交付一个SpringBoot微信小程序的文旅项目很多开发者的第一反应是代码写完、能跑起来、打包发给对方就完事了。但这个思路在真实交付场景里往往要翻车。我经手过好几个类似的文化旅游类小程序项目最深的体会是源码、部署文档、代码讲解这三样东西缺一件都会让交付变成一场灾难。这篇文章就用一个实际项目的完整交付过程来拆解从架构设计、源码组织、部署落地到代码讲解把每一步的来龙去脉和踩坑经验都摊开讲清楚。1. 交付一个文旅小程序项目真正要交付的是什么1.1 甲方要的不是代码是“能跑起来的系统”先说一个最常见的认知错位。开发方觉得代码写完就算完工但接收方——不管是客户、学校导师还是合作的技术同事——他们要的是一个“系统”。这个词的完整含义包括能安装部署的运行环境、能初始化好的数据库、能上传生效的小程序前端以及遇到问题时有据可查的排错路径。我们这次的SpringBoot文化旅游小程序项目项目本身不算特别复杂后端是标准的SpringBoot单体应用小程序端是原生微信小程序业务范围覆盖景区信息展示、旅游路线推荐、文创商品展示、用户登录与收藏等。但正是这种“看起来不难”的项目交付时最容易栽跟头。因为简单项目往往没有专职运维接收方可能只是一个会基本操作、但对Java后端和微信小程序都不太熟的人。所以我把交付物拆成四块缺一不可源码工程后端SpringBoot工程 小程序前端工程必须保证明明细、能一键导入部署文档从零开始的环境搭建、配置修改、启动步骤、上线部署按顺序写完代码讲解文档核心模块的类结构、表结构、接口清单、关键业务流程说明数据初始化脚本建库建表SQL、基础数据景区、路线、分类等的INSERT语句。这四块里源码是“硬件”部署文档是“使用说明书”代码讲解是“维修手册”数据脚本是“燃料”。只给硬件不给说明书接收方第一步就可能卡在环境配置上只给说明书没有维修手册项目一旦要加功能或排查bug对方就彻底抓瞎。1.2 交付标准对方关掉你的电脑之后还能独立完成我给自己定了一个很实用的交付标准假设现在把源码和文档发给一个从没接触过这个项目的人他能不能不看我的任何口头指导仅靠文档完成部署并跑起来这个标准听起来简单做起来很难。因为开发者在写文档时很容易犯“跳步”的毛病——自己太熟悉的东西默认对方也知道。比如application.yml里的数据库密码要改文档里可能只写一句“改成自己的数据库密码”但对方可能连MySQL的root密码都还没设置。比如小程序前端的app.js里要改request的baseURL文档里如果不说清楚“本地联调用局域网IP真机预览必须用公网域名或HTTPS域名”对方大概率会在真机预览时遇到request:fail然后来问你。我这次的项目在交付前专门找了个完全不了解项目背景的同事让他照着部署文档从头走一遍。结果他给我列了七个卡住的点全部都是文档信息不完整导致的。改完文档之后又让他走一遍这次才算通。这个方法强烈建议每个做交付的人都试一次相当于给文档做了一次“用户测试”比自己检查十遍都管用。2. 项目架构与业务模块文旅小程序的骨架怎么搭2.1 后端SpringBoot的结构设计与分层逻辑这个项目的后端我采用了标准的SpringBoot分层架构没有引入微服务、没有用太复杂的技术栈。原因是文旅类小程序的核心业务是“信息展示轻交互”不是高并发高可用场景把技术复杂度压到最低后续维护成本才最低。工程结构大概是下面这样springboot-cul-travel/ ├── src/main/java/com/example/culTravel/ │ ├── controller/ # 控制层接收小程序端请求 │ │ ├── AttractionController.java │ │ ├── RouteController.java │ │ ├── ProductController.java │ │ └── UserController.java │ ├── service/ # 业务逻辑层 │ │ ├── AttractionService.java │ │ ├── RouteService.java │ │ └── UserService.java │ ├── mapper/ # MyBatis-Plus的Mapper接口 │ ├── entity/ # 数据库实体类 │ ├── config/ # 配置类比如跨域、拦截器、微信登录配置 │ ├── common/ # 统一返回结果、异常处理、工具类 │ └── CulTravelApplication.java └── src/main/resources/ ├── application.yml # 核心配置 ├── mapper/ # MyBatis-Plus的XML文件如果用了XML的话 └── sql/ # 数据库初始化脚本为什么用MyBatis-Plus而不是原生MyBatis或者JPA我的考虑很实际MyBatis-Plus提供了一套通用CRUD方法单表操作不用写SQL一个Mapper接口继承BaseMapperT就能直接selectById、selectList、insert、deleteById。文旅项目的业务大部分都是单表查询和简单联查MyBatis-Plus能把开发量压缩一半以上。而且它的代码生成器可以一键生成entity、mapper、service、controller对于交付类项目来说生成后稍作修改就能用非常划算。实体类设计上我建了以下几张核心表表名用途关键字段t_attraction景区/景点信息id, name, description, cover_image, images, address, latitude, longitude, open_time, ticket_price, levelt_route旅游路线id, name, description, cover_image, days, attractions_ids, tagst_product文创商品id, name, price, cover_image, description, stockt_user用户信息id, openid, nickname, avatar, phone, create_timet_favorite收藏记录id, user_id, target_type, target_id, create_timet_banner首页轮播图id, image, link_type, link_id, sort这里有个设计细节值得说t_favorite表用了target_type字段来区分收藏的是景区还是路线而不是分别建两张收藏表。好处是接口统一一个“收藏列表”接口就能同时返回景区和路线的收藏数据坏处是在查询时需要根据target_type去不同的表里取详情会产生多次查询。实际业务中收藏量不会很大用循环查询完全够用而且代码可读性反而更高。2.2 小程序端的页面结构与冷启动路径小程序端我用的原生微信小程序没有上uni-app或者Taro这类跨端框架。原因同样是从交付角度考虑原生小程序在微信开发者工具里打开就能编译运行不依赖node_modules安装、不需要额外构建步骤对接收方来说上手门槛最低。页面结构按业务划分miniprogram/ ├── pages/ │ ├── index/ # 首页轮播图 热门景区 推荐路线 │ ├── attraction/ # 景区列表 / 景区详情 │ ├── route/ # 路线列表 / 路线详情 │ ├── product/ # 文创商品列表 / 商品详情 │ ├── favorite/ # 我的收藏 │ ├── user/ # 个人中心 │ └── login/ # 登录页 ├── utils/ │ ├── request.js # 封装wx.request统一处理baseURL和错误提示 │ └── auth.js # 登录态管理、token存储与校验 ├── components/ # 公共组件比如景区卡片、空状态 ├── app.js # 小程序入口全局数据与登录逻辑 ├── app.json # 页面路由与全局配置 └── project.config.json # 项目配置编译ID等小程序端最容易踩坑的地方是登录流程。微信小程序的登录不是我们传统意义上的“用户名密码登录”而是通过wx.login()获取一个临时code后端再拿这个code去微信服务器换openid和session_key。整个链路是小程序调用wx.login()拿到临时code小程序把code发给后端后端调用微信接口https://api.weixin.qq.com/sns/jscode2session用code appid secret换取openid后端用openid去数据库查用户是否存在不存在就自动注册后端生成自定义登录态token返回给小程序小程序把token存入storage后续所有请求在header里带上token后端通过拦截器校验。这个流程里最容易被忽略的问题是小程序的code只能使用一次而且有效期只有五分钟。如果后端处理慢了或者重复请求了第二次拿同一个code去换就会报40029错误。所以我在部署文档里专门标注了这一点并建议接收方调试时看到errcode:40029就去查是不是重复用了code而不是去查代码逻辑。有了登录体系之后收藏、个人中心这类需要识别用户身份的功能就好做了。我的做法是后端写一个LoginUser注解配合HandlerInterceptor拦截器从request的header里取出token解析出用户ID后放入ThreadLocalController里通过LoginUser直接把当前用户对象拿来做参数注入。这样业务代码里不需要到处写“解析token-查用户”的重复代码讲代码时也特别直观。3. 源码组织与关键实现代码既要跑得动也要讲得清3.1 统一返回体、异常处理与日志三件套必须落地交付项目有一个很现实的问题接收方如果要在这个基础上二次开发他第一步要读懂的就是你的代码风格和约定。我这次在项目里做了三件套既提升了可维护性也是代码讲解时的重点章节。统一返回体。所有接口都返回ResultT类型结构是code、message、data三个字段。成功时code200失败时code业务错误码。小程序端utils/request.js里统一判断code不等于200就直接弹出message提示。这样做的好处是接口的协议非常干净文档里写“所有接口返回格式均为Result结构”接收方看一个接口就能看懂所有接口。全局异常处理。用RestControllerAdviceExceptionHandler做全局兜底业务代码里只需要throw new BusinessException(景区不存在)异常处理器会自动组装成Result返回避免堆栈信息直接暴露给前端。我在common/exception包下放了一个BusinessException它的构造方法接收message由GlobalExceptionHandler统一捕获。日志。这个容易被忽略但非常重要。我引入了Slf4j在每个Service的实现类里都加了关键日志入参、出参、耗时。比如景区详情接口会打印“查询景区详情id1耗时12ms”这样接收方在排查问题时能看到日志输出位置而不是对着黑屏干瞪眼。配置上用了logback-spring.xml按天滚动保留7天生产环境日志打到/data/logs/cul-travel/目录下。这套三件套在我看来不是“高级工程实践”而是面向交付的基础配置。因为代码讲解时这三块是最容易讲出“为什么”的地方——为什么返回体要有code因为小程序端要统一判断为什么要有全局异常因为不能让用户看到500白屏为什么要有日志因为出bug了你才知道去哪看。这种“代码设计和实际用途”的对应关系正是接收方最需要的理解路径。3.2 核心业务接口与代码讲解时的切入点代码讲解文档我按业务模块来组织每个模块的讲解链路是业务需求 - 数据库设计 - 接口定义 - 代码实现 - 关键逻辑说明。以景区模块为例它的核心是列表和详情两个接口。景区列表接口路径是GET /api/attraction/list支持三个可选参数——keyword按名称模糊搜索、level按级别筛选、page和size分页。实现上用了MyBatis-Plus的LambdaQueryWrapper做条件拼接public PageResultAttraction getAttractionList(String keyword, String level, int page, int size) { LambdaQueryWrapperAttraction wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Attraction::getName, keyword); } if (StringUtils.hasText(level)) { wrapper.eq(Attraction::getLevel, level); } wrapper.orderByDesc(Attraction::getCreateTime); PageAttraction attractionPage attractionMapper.selectPage( new Page(page, size), wrapper); return PageResult.of(attractionPage); }这段代码的讲法不能只停留在“实现了什么”而要讲清楚一个Trick用LambdaQueryWrapper而不是直接写SQL字符串的原因是它在编译期就能校验字段名是否正确。字段改名后如果XML里写了select * from t_attraction where name #{name}改表字段就要回头找SQL但Lambda写法会直接编译报错从源头避免了这个坑。景区详情接口路径是GET /api/attraction/detail?id{id}。这里要做的是一件事把景区表中存储的图片组字段解析为数组返回给前端。我在t_attraction表里用了images字段存JSON数组字符串格式是[url1,url2,url3]。后端的处理是查出实体后在Service层做一次转换把这个字符串转成ListString放入返回对象。讲解时我特意强调了这个设计的理由文旅项目里一个景区往往有多张图片如果单独建一张t_attraction_image表查询一个景区详情就要先查主表再查子表字段一多还要做映射繁琐。用JSON字段存储详情查询只需要一次主表查询代码最简洁。**JSON字段的劣势是没法按图片维度做数据库层面的筛选但我们的业务场景是“一个景区整体展示”没有这种需求所以它是合理取舍。**这种“取舍”思路在代码讲解里是最有价值的内容比背API有营养得多。小程序端的代码讲解重点是两个地方utils/request.js的请求封装和app.js里的登录跳转逻辑。request.js里我封装了一个统一的入口const request (url, method, data) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ url: getApp().globalData.baseUrl url, method: method, data: data, header: { Content-Type: application/json, token: token || }, success(res) { if (res.data.code 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络请求失败, icon: none }); reject(err); } }); }); };小程序端最容易出问题的location就是这里的baseUrl。本地开发时工具里可以勾选“不校验合法域名”baseUrl可以用http://localhost:8080或者局域网IP但真机预览时手机和电脑必须在同一网段而且域名必须是HTTPS或者已备案域名才能正常请求。我在文档里单独列了一个“联调环境速查表”把三种情况列得清清楚楚开发者工具内调试、真机预览、线上小程序审核时分别应该怎么配baseUrl和域名白名单。3.3 数据表设计与初始化数据的重要性数据初始化脚本这快我认为要多说几句。很多开发者交付的项目里只有建库建表SQL没有基础数据接收方跑起来之后首页是空白的、景区列表是空的第一反应就是“系统坏了”。所以我在sql目录下放了完整的初始化脚本文件名直观命名01_create_database.sql建数据库设置UTF8MB4字符集02_create_tables.sql建表语句03_insert_base_data.sql插入基础数据包括3张轮播图、8个景区、4条路线、6个文创商品。基础数据怎么来文旅项目的特点是可以找到大量公开的景区信息。我模拟了华东地区某旅游城市的真实数据景区名称、简介、开放时间、门票价格都是按真实信息整理的图片用了可访问的占位图。我特别把门票价格、开放时间这类字段做了格式化处理因为接收方演示时如果看到价格是80.0这种浮点数观感会很差。初始化数据里还埋了一个很关键的“演示友好”设计在t_route表的attractions_ids字段里我让它和t_attraction表的id精确对应。这样路线详情页展示“途经景点”列表时只需要用逗号分隔的id字符串去查景区表就能关联出一串完整的景点信息。演示时路径清晰代码讲解时也好讲一举两得。4. 部署文档与系统落地从空服务器到跑起来4.1 部署架构与服务器规划部署文档是整个交付最容易水、也最容易被忽略的部分。我在这个项目里采用了最简单但最常见的方案单机部署所有服务都在一台CentOS 7.9服务器上。架构如下Java后端SpringBoot打包成Jar通过java -jar启动用nohup后台运行数据库MySQL 8.0存储业务数据缓存/会话本项目中暂时没有强依赖Redis的场景所以没有引入降低部署复杂度前端静态资源小程序端不需要服务器托管直接通过微信开发者工具上传到微信平台反向代理如果小程序端需要HTTPS域名用Nginx转发到后端的8080端口。为什么不做Docker?虽然热门搜索里也有宝塔Docker部署但我的判断是接收方可能连Docker是什么都不清楚而SpringBoot自带的Jar部署已经足够简单。mvn package打一个包java -jar一行命令就运行。对交付场景来说技术栈应该向“最容易被接收方接受”的方向倾斜而不是向“技术最酷”的方向倾斜。如果接收方明确要求容器化我再额外提供一份Dockerfile和docker-compose.yml作为进阶文档。服务器规格建议是2核4G30G SSD。项目并发量不大这个配置绰绰有余成本也低。我在文档里写清楚了服务器购买建议、安全组配置、端口开放等细节因为很多接收方是第一次买服务器不知道安全组没放行8080端口会导致外部访问不了。4.2 分步骤部署实录每一步都有验证点部署文档我的核心原则是**“每个大步骤后都要有一个验证点”**。即在每一步做完之后给接收方一个“怎么知道这一步做对了”的方法。下面是把部署过程拆解后的步骤和验证点安装JDK 17执行java -version能看到版本信息。安装MySQL 8.0执行systemctl status mysqld能看到running状态。初始化数据库执行mysql -uroot -p 01_create_database.sql后执行mysql -uroot -p -e show databases;能看到cul_travel库。上传并打包后端把源码上传到服务器/data/project/在项目根目录执行mvn clean package -DskipTests在target/目录下能看到cul-travel.jar。修改配置编辑application.yml把数据库地址、账号、密码改成服务器实际值。启动后端执行nohup java -jar cul-travel.jar /data/logs/cul-travel/run.log 21 然后执行tail -f /data/logs/cul-travel/run.log看到“Started CulTravelApplication”日志。接口冒烟测试在服务器本机执行curl -X GET http://localhost:8080/api/attraction/list返回JSON数据数组。小程序导入与联调用微信开发者工具导入miniprogram目录修改app.js里的baseUrl为服务器公网IP编译运行。这里有几个细节值得强调。数据库密码如果是接收方自己设置的密码强度不够会触发MySQL的校验插件报错如果使用了特殊字符比如、#application.yml里要记得做转义处理。另外MySQL 8.0的默认认证插件是caching_sha2_password如果JDBC连接报Authentication plugin caching_sha2_password cannot be loaded需要改成mysql_native_password这个坑大约每三个部署者就会踩一个。打包时还有个小技巧在pom.xml里配置了spring-boot-maven-plugin并且打包时用-DskipTests跳过测试这样打出来的Jar是Fat Jar包含所有依赖直接就能跑不需要额外装Tomcat。我在讲解文档里特别标注了这段配置因为如果接收方对SpringBoot不熟很可能会问“你的项目怎么没有web.xml没有外置Tomcat怎么运行”4.3 Nginx与HTTPS:上线前最后一公里小程序真机预览有个硬性要求wx.request的合法域名必须是HTTPS且域名需要在小程序后台的“开发设置-服务器域名”中配置。这就意味着项目要正式上线必须有一张SSL证书和一个已备案域名。在这个过程中我踩过一个大坑写在这里供大家参考小程序后台配置的request合法域名必须去掉前面的https://前缀只填域名比如api.example.com。第一次配的时候我把https://api.example.com整个填进去了结果真机请求一直报url not in domain list排查了半小时才发现是格式问题。Nginx配置我提供了一个可直接使用的server块核心内容如下server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/api.example.com.pem; ssl_certificate_key /etc/nginx/ssl/api.example.com.key; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有一个很容易被忽略的关联配置当后端通过Nginx代理时小程序请求的Referer和实际IP都可能发生变化。如果项目里有用到IP归属地、或基于来源的限流要特别留意X-Forwarded-For头有没有正确传递。我们这个项目暂时没用到这些但我在文档里单独列了一节“上线后需要检查的5个配置项”帮助接收方逐项确认HTTPS证书是否生效、域名是否备案、小程序后台域名白名单是否配置、后端Jar包是否以生产环境配置启动、数据库备份是否已配置定时任务。5. 代码讲解的正确打开方式让接收方从“能跑”到“能改”5.1 讲解文档怎么组织才不流于形式很多交付项目里的“代码讲解”就是一份流水账把每个类名、每个方法名罗列一遍看完等于没看。我的做法是把它写成一份“业务驱动”的文档特别强调接收方最关心的两件事“改哪里能改效果”和“出了问题去哪里查”整份讲解文档分四大部分技术栈与框架约定讲清楚项目用了哪些依赖、每个依赖解决什么问题比如Spring Boot Web、MyBatis-Plus、Lombok、Validation、Fastjson2。请求处理链路以一个具体请求为例从“小程序点击景点列表”开始一步步追踪到后端Controller、Service、Mapper、MySQL画出一条完整的调用链路帮接收方建立整体感知。各业务模块讲解按“景区、路线、文创、用户、收藏、首页轮播”六个模块每个模块给出对应的表结构、Controller接口列表、Service核心逻辑、关键代码片段。常见修改场景速查直接列出“新增一个景区类别怎么做”“首页轮播图怎么换”“文创商品价格怎么改”“如何给景区表加字段”这类具体任务的修改步骤。第四部分其实最受欢迎。因为接收方拿到项目之后95%的行为不是“从零开发新功能”而是在现有功能上做数据修改和样式调整。他们最需要的不是什么设计模式而是“我要把首页的第三张轮播图换掉改哪里”。把这个需求直接映射成操作步骤比让他们读一遍所有源码再自己悟高效得多。5.2 代码讲解时的在线演示脚本如果交付时有现场讲解环节或者录视频讲解我建议提前准备一个40分钟左右的演示脚本按下面的顺序来第一个10分钟讲项目全貌打开后端工程用IDE的Structure面板或目录树快速扫一遍包结构讲清楚controller/service/mapper/entity/config/common/exception每个包的作用。再切到小程序端过一遍页面目录让接收方看到前端和后端的对应关系。这个环节的关键是“不深入代码只建立地图”。第二个10分钟讲数据处理打开数据库客户端展示8张表。挑t_attraction表来拆解字段设计说明哪些是核心字段、哪些是冗余字段、为什么这样冗余。再执行几条SQL演示“改一条轮播图数据后小程序首页实时变化”的效果。这是最有冲击力的环节能让接收方直观感受到“后端的每个改动会直接影响前端展现”。第三个10分钟讲一个完整业务流程从wx.login登录开始到用户点收藏、查看收藏列表再到退出登录把一整条业务链路串起来讲。这是讲解最难的10分钟要求你对代码位置非常熟而且要随时准备回答“如果xx出错了怎么办”。建议对着真实的网络请求走不要只在IDE里脑补。最后一个10分钟讲部署和上线重点演示修改application.yml、打包、启动、看日志、验证接口这一条命令链路。对于接收方来说能看到“一个Jar包在两分钟内起服务”比任何解释都有说服力。5.3 交付后的常见问题与远程支持工具箱最后说说交付之后的事。再完整的交付也一定会遇到问题区别在于接收方遇到问题时有多少自主排查能力。我相信一个健康的交付关系是对方先查文档查不到再问。所以我在交付清单里给接收方配了一套“问问题之前先做的三件事”第一看后端运行日志tail -f /data/logs/cul-travel/run.log把最近的报错信息截图第二看接口返回用curl或Postman直接请求出问题的接口看返回的code和message第三看小程序控制台在微信开发者工具的Console面板里看请求报错信息确认是不是baseURL或域名白名单问题。只要接收方走完这三步80%的技术问题都能在对话里快速定位。剩下的20%你只需要提供精准的修复建议不用再远程连上去瞎猜。我还会给接收方一份“运行环境故障速查表”把常见的错误码、错误信息、可能原因和处理办法列成一张表格放在部署文档的最后几页。比如报错信息可能原因处理办法java.sql.SQLException: Access denied for user数据库账号密码错误检查application.yml中的账号密码errcode 40029小程序code被重复使用确认登录接口只调用一次url not in domain list域名未在小程序后台配置检查request合法域名配置格式Whitelabel Error Page接口路径不存在检查Controller类中的RequestMapping路径Failed to configure a DataSource数据库地址连不通检查服务器网络和MySQL监听端口这份速查表是在交付那次“文档测试”中逐步完善的后来接手的同事反馈说很多问题他照着表自查两分钟就解决了从没卡过超过十分钟。这对做交付的人来说才是真正省心省力的结果。整个项目走下来我个人最深的体会是交付不是“代码写完就结束”而是“让对方能独立使用和修改才结束”。SpringBoot提供了稳定的后端基座微信小程序提供了便捷的展示端但把这些串起来的是结构清晰的源码、按步骤可操作的部署文档以及能回答“为什么这样设计”的代码讲解。如果你正在准备类似的文旅小程序交付项目不妨先花两个小时把这三样东西补齐后面能帮你减少几十个小时的返工时间。