做毕设或者简历项目,很多人都会选“SpringBoot + 微信小程序 + 二手书交易”这个组合。功能链路清晰:登录、发布、浏览、下单、订单管理,前后端都有得写,技术上是标准的全栈闭环。但这套组合恰恰也是踩坑重灾区——SpringBoot版本选错,启动直接报错;小程序里code换token没处理好,登录态各种失效;联调时域名没配,真机直接白屏。这篇文章就把我从零搭建这个项目的完整过程、数据表设计、后端接口规划、小程序端关键逻辑,以及那些让我浪费过一整天的坑全部拆开讲清楚。
适合正在做这个选题的学生、准备面试的全栈开发者,以及想快速搭一套C端小程序交易demo的工程师参考。无论你是第一次接触小程序还是已经写过几个页面,这篇文章的重点都在“让你少走弯路”这件事上。
1. 技术选型的底层逻辑:为什么SpringBoot和小程序能凑到一起
1.1 SpringBoot解决的核心问题
后端框架的选择其实有很多,SpringMVC、SSH、甚至Node.js都能做,但SpringBoot在高校项目和中小型个人项目里几乎是统治级的存在。原因不复杂:它把Spring生态里最繁琐的配置工作压到了最低。
传统的SpringMVC项目,光一个applicationContext.xml、spring-mvc.xml、web.xml的配置组合就能劝退不少新手,而且每个模块引入依赖时还得操心版本冲突。SpringBoot的核心逻辑是“约定大于配置”,它通过起步依赖(Starter)帮你把常见的依赖组合和版本匹配关系锁死,你只需要在pom.xml里加一个spring-boot-starter-web,一个内嵌的Tomcat、一套完整的MVC能力就齐了。
自动装配这套东西做得很聪明,它利用@EnableAutoConfiguration注解去读取jar包里的META-INF/spring.factories或AutoConfiguration.imports文件,把符合条件(比如classpath里有对应类、配置项没被手动覆盖)的配置类自动加载进来。这也是所有SpringBoot面试题几乎必问的点,我做这个项目时专门把源码里的AutoConfigurationImportSelector看了一遍,后面才真正理解为什么改个配置就能生效。理解了自动装配机制,你在排查问题时就不会觉得SpringBoot像个黑盒。
1.2 微信小程序作为前端容器的好处
小程序这个容器,对于二手书交易这种低频、轻量的C端场景非常合适。用户不需要下载App,微信里搜一下或者扫码就能打开,用完即走,完全符合二手交易“想卖书的时候才上来看看”的使用习惯。
开发层面小程序也有天然优势。WXML + WXSS的语法跟HTML + CSS非常接近,原生JavaScript就能写,有前端基础的人上手很快。而且微信官方提供了完善的能力体系:wx.login登录、wx.chooseMedia选图、wx.request网络请求、wx.cloud云开发,这些API让个人开发者不用自己搭运维体系就能把整个业务跑通。
当然小程序也有些“特殊癖好”,比如所有请求域名必须是HTTPS且得在小程序后台配置白名单,比如部分组件在iOS上的渲染表现与Android不一致,这些我后面会单独讲。提前了解这些限制,能帮你省掉不少调试时间。
1.3 二手书交易场景的教学价值
选择二手书这个领域也有讲究。它不像电商那样需要复杂的商品SKU、库存、优惠券体系,但“用户、商品、订单”三大核心模型一个不少,覆盖了一场真实的C端交易所需要的完整链路。
图书商品天然有结构化的字段:书籍名称、作者、出版社、ISBN、版次、成色描述、定价、售价。这些字段在设计表时非常直观,不需要像时装或数码产品那样搞一堆类目规格。订单模型也不复杂:买家下单、确认收货,卖家出售、完成交易,这里可以做一套清晰的状态机。对新手来说,把这三张主表设计好、把状态流转写清楚,整个项目的核心就立起来了。
我的建议是,做这个项目之前,先别急着打开IDE,把下面的数据表设计、接口列表用纸笔梳理好,后面编码会顺畅很多。
2. 数据库设计:四张核心表撑起二手书的交易闭环
2.1 用户表设计:以openid为锚点
用户表是这个系统的基石,它的核心不是自增主键,而是微信小程序端的openid。每个微信用户在每个小程序下的openid都是唯一的,所以这张表的openid字段必须建唯一索引。
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid', `nickname` varchar(50) DEFAULT '' COMMENT '微信昵称', `avatar` varchar(500) DEFAULT '' COMMENT '头像URL', `phone` varchar(20) DEFAULT '' COMMENT '联系方式', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;首次登录时,小程序端通过wx.login拿到临时code,后端拿这个code去微信接口换openid和session_key。会有一个常见误区:把openid当作所有用户相关操作的传参,前端每次请求带上openid。这是不安全的,openid泄露后别人可以伪装成你的身份操作。正确做法是:后端根据openid生成自己的token,每次请求只校验token,从token里解析出userId,再走业务逻辑。后面的登录章节我会把时序展开讲。
nickname和avatar这两个字段,微信已经不再推荐通过wx.getUserProfile直接拿真实资料了,现在更稳的做法是用户手动填写或选择头像。我实现时就是让用户在小程序里自己设置昵称和头像,这样一个纯粹的注册流程就变成了“首次登录自动创建,引导完善资料”的体验方案。
2.2 书籍表设计:商品状态与字段取舍
商品表是书籍信息的大本营,也就是二手书交易里的“门面”。这张表的字段设计直接影响后端的查询逻辑、小程序端详情页的展示效果,以及后续有没有办法做搜索。
CREATE TABLE `book` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '书名', `author` varchar(50) DEFAULT '' COMMENT '作者', `publisher` varchar(100) DEFAULT '' COMMENT '出版社', `isbn` varchar(20) DEFAULT '' COMMENT 'ISBN', `original_price` decimal(10,2) DEFAULT '0.00' COMMENT '书籍原价', `price` decimal(10,2) NOT NULL COMMENT '售价', `degree` varchar(20) DEFAULT '九成新' COMMENT '成色', `description` text COMMENT '描述', `image_url` varchar(500) DEFAULT '' COMMENT '封面图URL', `seller_id` int(11) NOT NULL COMMENT '卖家用户ID', `status` tinyint(4) DEFAULT '0' COMMENT '状态 0在售 1已售 2下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_seller_id` (`seller_id`), KEY `idx_status_create` (`status`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;status字段是整个商品表里最重要的。从0到2的三个状态,对应着商品在不同阶段的生命周期。上架时可以设为0,被下单之后置为1,卖家手动下架设为2。这样设计有个明显的好处:列表查询只需要加一条WHERE status = 0的条件,就能天然过滤掉已经卖掉或下架的书。如果不加这个状态字段,你的页面就得依赖关联订单去反查商品是否还在售,性能差不说,逻辑还会绕来绕去。
create_time配合状态字段建立了一个联合索引,这在列表页按最新发布排序时非常有用。商品列表的核心需求就是“只看最新的在售书”,这条SQL的执行计划可以直接走索引,数据量到几万条时依然能保持毫秒级响应。
2.3 订单表设计:状态机与幂等控制
订单表是整个交易系统的核心,它关联了买家、卖家和书,同时承载了整个交易流程的状态流转。这里的核心逻辑在于,订单状态必须是一个有限的、清晰的状态集合,每一步流转都要有对应的代码逻辑,避免出现订单状态“乱七八糟”的情况。
CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `book_id` int(11) NOT NULL COMMENT '书籍ID', `seller_id` int(11) NOT NULL COMMENT '卖家ID', `buyer_id` int(11) NOT NULL COMMENT '买家ID', `price` decimal(10,2) NOT NULL COMMENT '成交价', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态 0待付款 1待发货 2已完成 3已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `pay_time` datetime DEFAULT NULL COMMENT '付款时间', `complete_time` datetime DEFAULT NULL COMMENT '完成时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_seller_id` (`seller_id`), KEY `idx_buyer_id` (`buyer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里需要重点说下单的幂等性。当用户连续点击两次“立即购买”按钮,如果后端没有做控制,就有可能会生成两笔重复订单。比较简单的处理方式是,在代码里根据book_id去查询是否已经存在“该本书”的有效订单,如果存在则直接返回已经创建的订单信息,而不是再次创建一条。同时MySQL的事务在这里也会发力:查询是否存在订单、创建订单、修改书籍状态三个动作必须放在同一个事务里,任何一个环节失败就整体回滚。
下单价和书价的关系也要引起重视。我采用的是下单那一刻就把price字段快照到订单表里,而不是下单后动态读取book表的price。如果把价格做成动态读取,卖家在买家下单后改价或改库存,就会引起纠纷。订单表存快照这个做法是我个人强烈推荐的通用原则,凡是涉及交易金额的业务,都应该用快照而不是实时引用。
2.4 收藏表与查询索引的取舍
用户收藏这个功能,做起来简单但表设计的细节也不少。收藏表本质上是“用户—书籍”多对多关系的一张关系表。
CREATE TABLE `favorite` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '用户ID', `book_id` int(11) NOT NULL COMMENT '书籍ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_book` (`user_id`, `book_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;uk_user_book这个联合唯一索引设计得很关键:它既能保证同一个人不能对同一本书重复收藏,又恰好成为查询“当前用户收藏了哪些书”的普通索引。用户收藏列表、取消收藏时判断是否存在记录,都可以直接命中索引。
数据库设计这一层,我把它比作整个项目的“地基”。地基打歪了,后面所有功能模块写起来都费劲。比如想加搜索功能,你的book表里有title和author字段,应该直接加title和author的联合索引或全文索引;想加“我的浏览记录”,那可能得多建一张浏览记录表。建议你在写任何一行代码之前先确认表结构,否则后期改表比写代码还要痛苦很多。
3. SpringBoot后端:搭建骨架、核心接口与鉴权思路
3.1 版本选择:为什么不要上来就追最新版
SpringBoot的版本选择是这个项目里第一个大坑。很多新手在IDEA里创建项目时直接选了最新版本,结果遇到一堆离谱的兼容性问题。我做这个项目时,一开始也用了一个SpringBoot 3.x的新版本,结果依赖引入后项目启动直接报了一些奇怪的错误,后来排查才知道是javax.servlet和jakarta.servlet这两个包名体系切换导致的问题。
SpringBoot 3.x把javax命名空间整体迁移到了jakarta,这意味着大量老版本的第三方库(MyBatis-Plus、某些文件处理库等)在没有升级到兼容版本之前,无法直接运行在3.x之上。虽然现在很多主流库都已经适配了jakarta,但如果你正好碰到一个还没适配的库,就得被迫降级或换方案,相当折腾。
我的建议是,做这类毕设或简历项目,稳字当头,选SpringBoot 2.7.x。2.7是SpringBoot 2.x的最终维护版本,生态兼容性最好,绝大部分第三方库都能直接用,网上搜资料时也基本都能匹配。如果只是练手项目,2.7更合适;如果你已经对JVM生态很熟了且打算长期用,也可以直接上手3.x。但请记住,版本选择没有绝对的对错,能让你快速把项目跑起来、专注于业务逻辑的版本,就是好版本。
另外,IDEA创建项目的时候,Spring Initializr默认生成的依赖版本可能会和你本地JDK版本不匹配。JDK版本和SpringBoot版本的兼容关系大致是:SpringBoot 2.x推荐用JDK 8或11,SpringBoot 3.x要求JDK 17以上。提前确定好自己的JDK环境,再决定用哪个SpringBoot版本。
3.2 项目分层与目录结构:controller-service-mapper够不够用
SpringBoot项目的经典分层是Controller、Service、Mapper三层,但实际开发时,我会在Service内部再细分为接口和实现类,同时也根据电商业务特征增加一个叫“DTO(数据传输对象)”的中间层。这样做看起来多了一层复杂度,但好处是,前端传来的参数和后端返回的数据结构,不会因为实体类字段的增减而被迫变动。
下面这套目录结构是我在多个项目中沉淀下来的,你可以直接参考:
com.example.bookstore ├── common │ ├── result.Result.java // 统一返回封装 │ ├── exception.BusinessException.java │ └── interceptor.LoginInterceptor.java ├── config │ └── WebConfig.java // 拦截器、跨域等配置 ├── controller │ ├── AuthController.java // 登录 │ ├── BookController.java // 书籍发布、列表、详情 │ ├── OrderController.java // 订单创建、状态流转 │ └── FavoriteController.java // 收藏 ├── service │ ├── AuthService.java │ ├── BookService.java │ └── impl │ ├── AuthServiceImpl.java │ └── ... ├── mapper │ ├── UserMapper.java │ ├── BookMapper.java │ └── ... ├── entity │ ├── User.java │ ├── Book.java │ └── Order.java └── dto ├── LoginRequest.java ├── PublishBookRequest.java └── ...Controller层永远保持“薄”。它的职责是接收参数、校验基本格式、调用Service、返回统一结果。不要在里面写SQL、不要写复杂的业务判断。Service层承载业务逻辑,比如发布书籍时要校验用户是否登录、书籍参数是否合法、是否有上传图片,这些步骤即使只有几行代码,也应该放在Service里而不是Controller里。
统一返回结构也很重要。我在common.result里封装了一个Result类,包含code、message和data三个字段。成功时code为200,业务异常时code为500或自定义错误码。这样小程序端就可以根据result.code来判断请求是否成功,代码统一性好,写起来也干净。
3.3 登录与Token鉴权:微信登录逻辑拆解
小程序登录是整个项目的第一个核心功能,也是最容易出错的环节。这里我先把完整流程解释清楚:
- 小程序端调用wx.login(),获取一个临时凭证code。
- 小程序端把code通过wx.request发送到后端接口(比如POST /api/user/login)。
- 后端拿着code,请求微信官方接口
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=CODE&grant_type=authorization_code,换取openid和session_key。 - 后端将openid存入user表,如果该openid不存在,则代表新用户注册,自动创建账号。
- 后端生成一个Token(我用的UUID或JWT),将Token和userId的映射关系写入Redis,并设置过期时间(比如2小时)。
- 后端把Token和用户信息返回给小程序端。
- 小程序端将Token存入本地缓存,后续所有请求的请求头中带上
Authorization: Bearer {token}。
这段流程中需要重点看的坑:
- 不要把appid和secret放在小程序前端代码里,secret只应保存在后端。如果用JWT方案,注意JWT的密钥也要放在后端环境变量里,不要硬编码提交到代码仓。
- code是一次性的,只能用一次,而且有效期很短,不要缓存它。
- session_key不能返回给小程序端,它只用于解密敏感数据(比如手机号),一旦泄露可能导致用户数据被解密。
Token中间件(拦截器)的实现也比较简单。后端写一个HandlerInterceptor,在preHandle方法里去解析请求头里的token,从Redis中取出对应的userId,通过ThreadLocal保存。业务控制器里需要当前用户信息时,直接从ThreadLocal读取就行。这里有个容易忽略的点:拦截器要放行登录和公开接口(比如登录接口本身,以及书籍列表详情这些不登录也能看的页面),否则用户第一次打开小程序只能看到一个空白页面。
3.4 核心接口清单与实现要点
把后端的接口做一次全量盘点,对整体开发是很有帮助的。我按照模块列了一份接口清单,每行就是一个需要去实现的接口,你在开发时可以直接对着清单逐个完成:
| 模块 | 接口 | 说明 | 备注 |
|---|---|---|---|
| 用户 | POST /api/user/login | 微信登录,返回token | 无需登录 |
| 用户 | GET /api/user/me | 获取当前用户信息 | 需要登录 |
| 用户 | PUT /api/user/me | 完善/修改昵称头像 | 需要登录 |
| 书籍 | GET /api/book/list | 分页获取在售书籍 | 支持按关键词、按分类筛选 |
| 书籍 | GET /api/book/{id} | 获取书籍详情 | 公开 |
| 书籍 | POST /api/book/publish | 发布书籍 | 需要登录 |
| 书籍 | PUT /api/book/{id}/status | 下架书籍 | 只允许卖家本人操作 |
| 订单 | POST /api/order/create | 创建订单 | 需要登录 |
| 订单 | GET /api/order/list | 查询我的订单列表 | 可区分买家/卖家视角 |
| 订单 | PUT /api/order/{id}/cancel | 取消订单 | 可区分买家/卖家视角 |
| 订单 | PUT /api/order/{id}/complete | 确认完成订单 | 可区分买家/卖家视角 |
| 收藏 | POST /api/favorite/add | 添加收藏 | 需要登录 |
| 收藏 | DELETE /api/favorite/{bookId} | 取消收藏 | 需要登录 |
| 收藏 | GET /api/favorite/list | 收藏列表 | 需要登录 |
分页查询是列表接口里最常见的实现。我用的是MyBatis-Plus的Page对象,直接配合IPage 接口。但要注意一个问题:返回给前端时,不能只给一个List,还得带上总记录数total、当前页码current、每页大小size,否则小程序端没法做分页加载更多。
发布书籍这个接口里,图片处理也是一个高频操作点。小程序端选择图片后,可以通过wx.uploadFile把文件传给后端接口,后端用MultipartFile接收,然后保存到本地磁盘或上传到OSS。
如果只是本地保存,需要注意两点:一是相对路径不要用相对但未定义的,应该用配置的绝对路径;二是在生产环境下,本地磁盘存储的图片无法被公网直接访问,需要配合Nginx做静态资源映射。如果是毕设演示或本地开发,写一个简单的FileService把文件写到配置目录,再返回拼接好的访问URL(例如http://localhost:8080/images/xx.jpg)就够用了。
事务和并发控制这块,建议在创建订单、标记商品已售等写操作中加入@Transactional注解。以创建订单为例,库存扣减这个语义在二手书场景中体现为“把书籍状态从在售改成已售”,如果两个人同时买同一本书,没有事务保护和状态校验,就可能导致同一本书被卖出两次。所以在创建订单的Service方法里,一定要先执行UPDATE book SET status = 1 WHERE id = ? AND status = 0,然后用update的返回行数判断是否抢到书,行数大于0才能继续,否则直接抛异常提示“这本书已经被卖掉了”。
4. 小程序端的关键实现:登录、浏览、下单的完整链路
4.1 小程序目录结构与页面规划
小程序端的目录规划直接决定你后面开发的心情。原生小程序的页面天然是按照“页面目录 + 4个同名文件”的结构组织的,规划不好,代码会很快变得混乱。
我的小程序端目录结构是这样的:
miniprogram/ ├── app.js // 全局逻辑:启动时检查登录态 ├── app.json // 全局配置:页面路由、窗口样式 ├── app.wxss // 全局样式 ├── utils/ │ ├── request.js // 封装wx.request,统一处理token和错误 │ └── auth.js // 登录相关方法 ├── components/ │ ├── book-card/ // 书籍卡片组件(列表复用) │ └── empty-state/ // 空状态组件 ├── pages/ │ ├── index/ // 首页:书籍列表、搜索、下拉刷新 │ ├── detail/ // 书籍详情页 │ ├── publish/ // 发布书籍页 │ ├── order/ // 我的订单页(可以拆成买家/卖家两个tab) │ ├── favorite/ // 我的收藏页 │ └── mine/ // 个人中心:我的信息、我的发布、退出登录 └── images/ // 本地图片资源app.json里page路由的第一个page是首页,这个不能乱写。tabBar可以配置成首页、发布、订单、我的四个入口。首页是书单流,点击卡片进详情,详情页点立即购买进订单确认页。发布页承担商品录入的功能,买家和卖家两个视角的订单列表可以放在同一个页面里用tab切换,这样代码量不增加多少,功能却显得完整很多。
4.2 登录态管理:wx.login、code与token的落地
小程序端的登录管理比后端更繁琐,因为要处理好“用户打开小程序时是否已登录”的判断。我的做法是写一个auth.js模块,统一管理登录逻辑。
首次启动时,app.js的onLaunch里执行这样一个流程:
- 从本地缓存storage中读取token,如果存在就再用wx.checkSession检测一下微信会话是否过期。
- 如果token不存在或会话过期,调用wx.login()获取code,请求后端登录接口,拿到新的token和用户信息。
- 把token和用户信息写入storage。
这里有一个细节需要特别注意:wx.login不要频繁调用,官方要求小程序端尽量只在需要时调用。如果每次进入页面都调wx.login,会白白消耗网络请求,还有可能被微信频率限制。
在封装请求工具utils/request.js时,我会这样做:
- 定义baseUrl,开发时用本机局域网IP,比如http://192.168.1.100:8080,上线时换成正式域名。
- 每次请求都从storage中取token,并添加到请求头。
- 对返回结果统一处理:如果code为200,resolve并返回数据;如果code为401表示token过期,就清除本地token并跳转登录页或重新wx.login。
- 网络错误(比如域名未配置、断网)时,弹toast提示用户。
这个封装看着简单,但能帮你省下巨量的重复代码。不要在每个页面里都直接写wx.request,后期改域名、加token、加错误统一处理时,你会庆幸自己当初做了这层封装。
4.3 首页和列表页:分页、下拉刷新与搜索
首页是用户进入小程序后看到的第一个页面,它的体验直接决定用户愿不愿意继续逛。对我来说,首页不只要做一个简单的书籍列表,还要实现三个核心交互:分页加载、下拉刷新、关键词搜索。
分页加载是最高频的操作。我用的方式是“scroll-view的触底加载”或“页面的onReachBottom事件”,配合页面的data里维护一个pageNum和pageEnd标志。
data: { bookList: [], pageNum: 1, pageSize: 10, hasMore: true, loading: false }, async loadBookList(reset) { if (this.data.loading) return; if (reset) { this.setData({ pageNum: 1, hasMore: true, bookList: [] }); } if (!this.data.hasMore) return; this.setData({ loading: true }); const res = await request({ url: '/api/book/list', data: { pageNum: this.data.pageNum, pageSize: this.data.pageSize, keyword: this.data.keyword } }); const list = res.data.records || []; this.setData({ bookList: this.data.bookList.concat(list), pageNum: this.data.pageNum + 1, hasMore: this.data.bookList.concat(list).length < res.data.total, loading: false }); }, onReachBottom() { this.loadBookList(false); }, onPullDownRefresh() { this.loadBookList(true).then(() => wx.stopPullDownRefresh()); },这里需要注意的坑:concat拼接列表时一定要用新数组而不是直接修改this.data.bookList,否则视图不会更新;hasMore的判断是根据已加载数量与total比较,防止用户一直滑动时发起多余的请求;loading标志位可以防止快速滚动时重复请求导致的列表错乱。
搜索功能我是在列表页顶部放了一个搜索input,输入关键词后触发搜索,请求时把keyword传给后端。如果只是做简单实现,后端用like '%keyword%'模糊匹配书名和作者就能满足需求。如果想要更好的搜索体验,后续可以再引入Elasticsearch或数据库全文索引,二手书这个体量用like完全够用。
4.4 发布与下单:交易闭环的交互细节
发布页面的交互是整个小程序端最复杂的部分,因为涉及到表单校验和文件上传。用户需要填写书名、作者、出版社、ISBN、成色、原价、售价、描述、封面图等字段。
发布流程的代码逻辑可以这样组织:
- 用户点击封面图时,调用wx.chooseMedia选择图片。
- 立即把图片通过wx.uploadFile上传到后端,获取到图片URL后暂存在data里。
- 用户点击提交时,收集所有表单字段,通过wx.request向后端发布接口提交。
- 发布成功后跳转到我的发布列表或首页,并且提示用户。
这里有个体验层面的小技巧:图片上传最好在用户选完图后立即执行,而不是等提交时才上传。因为上传大图耗时较久,如果用户在提交时才上传,会看到很长的loading状态。选完图立刻传,提交的时候就只需要传输文本字段,体感会快很多。
下单页的核心逻辑在于创建订单的二次确认。用户从详情页点“立即购买”,一定要先弹出一个确认框,展示书籍信息、价格、卖家和这份订单的默认条款(比如“虚拟商品不退换”或“线下交易请当面验货”),避免误下单。确认后调用创建订单接口,拿到订单号后跳转到订单详情页,提示“下单成功,请等待卖家联系”。在创建订单接口里,后端会同时把书籍状态改成已售,避免被其他人抢先购买。
订单列表页的开发需要同时考虑“我是买家”和“我是卖家”两个视角。买家看订单,关心订单状态和联系卖家;卖家看订单,关心订单状态和联系买家。建议订单列表页用一个switchTab或分段器切换“买到的”和“卖出的”,两种列表共用页面的布局,只是接口查询的参数不同。
首页、详情页、发布页、订单页跑通之后,这个项目的主链路其实已经通了。剩下的就是打磨细节:比如成色怎么描述、联系方式要不要脱敏、书籍详情页要不要展示卖家的信用分,这些可以根据时间和精力来安排。
5. 联调和上线中的常见坑:版本、域名、渲染兼容
5.1 版本太高引发的连锁反应:一次具体的排查过程
做这个项目我踩的第一个大坑,就是SpringBoot版本问题。当时我用的是SpringBoot 3.0.1,启动时遇到了一个比较莫名其妙的错误,项目一直无法正常启动,控制台报错信息指向了一个缓存配置类,看起来像是某个配置类的条件没有满足。
排查过程是这样的:先去检查pom.xml依赖,发现spring-boot-starter-data-redis是我手动引入的,而SpringBoot 3.x里的Redis默认客户端从Jedis换成了Lettuce,刚好我又引入了某个老版本的连接池库,导致两者版本不匹配。更麻烦的是,有的第三方库在SpringBoot 3.x下没有适配jakarta.servlet,直接编译不过。
这个排查花了我一个晚上。最后方案很简单,把SpringBoot版本降到2.7.9,然后把项目里所有javax相关的依赖都理了一遍,问题就直接消失了。如果你的项目也遇到类似“启动失败但报错信息莫名其妙”的问题,先别急着调试代码,回到pom.xml看看版本组合是否合理,这是成本最低的排查方向。
另外,IDEA里创建SpringBoot项目时,如果JDK版本和SpringBoot版本不匹配,编译阶段就会报错。建议用JDK 8 + SpringBoot 2.7.x这套组合,或者JDK 17 + SpringBoot 3.x,尽量不要用JDK 11去跑SpringBoot 3.x,虽然可能能跑,但会有一些隐性问题。
5.2 域名、HTTPS与小程序请求合法校验
小程序和普通网页不一样,它对网络请求有着极其严格的要求。正式上线的小程序,所有request域名必须是HTTPS,而且要去小程序后台的“开发管理->开发设置->服务器域名”里配置request合法域名,否则真机调试时请求直接报错“不在以下合法域名列表中”。
开发阶段我们通常没有正式域名和HTTPS证书。你可以这样处理:在微信开发者工具里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”这个选项,然后用http://localhost:8080或局域网IP来调接口。这样在本地调试完全没问题,但要注意:提交体验版或正式版时,这个选项不会生效,小程序端会严格校验域名。所以无论如何,你要么买一台云服务器配好域名和证书,要么用内网穿透方案临时映射一个HTTPS地址来测试。
一个小细节:开发时连接手机真机调试,手机必须和电脑在同一局域网下,然后把baseUrl改成电脑在局域网内的IP,比如http://192.168.1.100:8080,注意不是localhost。否则手机访问不到你电脑上的后端服务。
5.3 iOS渲染机制与scroll-view内组件的兼容问题
小程序在iOS和Android上的渲染表现差异是个容易让人怀疑人生的坑。我遇到的一个典型问题,是在iOS上,scroll-view内部如果放了一些弹层类组件或picker类组件,滚动时会出现错位、卡顿或者组件不跟随滚动的情况。
这个问题在原生小程序和用uni-app开发时都可能遇到。苹果的WebKit渲染引擎对滚动容器内position: fixed元素的处理逻辑与Android不同,fixed元素在滚动时会“粘在”视口上,但iOS的橡皮筋回弹效果有时会打破这个预期,导致视觉上错位。
解决方案通常是:
- 弹层类组件放在scroll-view外部,通过v-show/条件渲染控制显示与隐藏。
- 如果必须在scroll-view内部使用picker类组件,可以尝试设置组件的position为absolute而不是fixed。
- 对scroll-view设置enable-flex属性,或者在滚动时手动调整弹层的位置偏移。
二手书发布页的成色、出版社这类下拉选择,我后来直接改成了页面顶部的底部弹层,这样就不再吃scroll-view内部的渲染问题了。如果你用到了日期选择、地址选择这类官方picker组件,建议先在所有机型上真机测一遍,特别是iOS设备。
关于wx.env.user_data_path这个路径常量,它也值得提前了解:它是小程序在用户本地磁盘上专属于自己应用的数据目录,可以用来保存文件、图片或临时下载的附件。如果你未来想在二手书交易平台里支持“卖家附赠电子版资料下载”这类功能,就可以用这个路径来存储下载的PDF或文档,再通过wx.openDocument预览打开。
6. 从毕设到落地:扩展方向与答辩加分点
6.1 功能扩展:支付、IM与推荐
如果你的项目已经能完整跑通交易闭环,但想让它更有竞争力,可以在下面几个方向中挑一两个做深入:
第一是微信支付。微信支付需要商户号,个人开发者无法直接申请,但如果你有实习公司或能借到企业资质,可以考虑接入。接入支付的流程复杂度主要体现在签名、回调、退款这几个环节,但如果能把这个功能完整落地,项目含金量会明显提升。
第二是站内IM即时通讯。二手交易中买卖双方沟通需求非常强烈,如果能在小程序里实现一个简单的聊天室或留言板,就更完整了。可以用小程序原生的WebSocket能力,也可以接入云开发,再不行就做一个简单的“留言列表”功能,也能达到效果。
第三是推荐系统。想来点高级的,可以用简单的协同过滤或基于内容的推荐,把用户浏览、搜索、收藏的数据作为特征,推荐相关书籍。不需要搞得特别复杂,基于标签匹配的“猜你喜欢”就能让项目有话题性。
6.2 答辩和面试时值得深挖的技术点
这一节想给做毕设或准备面试的同学提醒几个容易被问住、但准备好的话特别加分的技术点。
SpringBoot自动装配原理:面试官问“为什么SpringBoot不需要写xml配置”,你要能说出@EnableAutoConfiguration的生效过程,以及AutoConfigurationImportSelector的加载流程。做项目的过程里,最好亲手debug一遍自动配置类的加载逻辑,这样面试时才能讲得又细又扎实。
跨域问题处理:小程序端不是浏览器环境,其实不存在传统意义上的CORS跨域问题,但如果你是web管理后台,就一定会遇到。我这里提前做了CorsConfig的配置,允许所有来源和指定方法,是为了方便后期增加一个基于Vue的管理后台,两个前端端口都能访问后端接口。
并发场景下如何防止超卖:可以把创建订单和书籍状态更新的并发控制策略重点讲一下。我用的是数据库乐观锁和事务组合,它的核心是让多次并发同时更新同一行数据时只有一条SQL生效,这个思路在很多秒杀项目里都是核心问法。
Token与登录态的安全性:token过期时间、refresh_token机制、Redis存储session的好处,这些如果讲清楚,面试官对你的印象会更好。为了体验,我把token过期时间设计成了7天,正常用没问题,但如果不打算做刷新机制,最好在拦截器里对“即将过期”的token做续期处理,否则用户经常需要重新登录。
6.3 上线部署的建议:从本地跑到服务器
项目彻底开发完之后,把它部署到一台云服务器上,整个项目就算真正落地了。部署建议保持简单:服务器装一个MySQL和Redis,后端打jar包,用nohup或systemd进程托管;前端在小程序开发者工具里上传代码,审核通过后发布。
有一点想重点提醒:本地开发和生产环境的配置一定不要写死在代码里。application.yml里用spring.profiles.active来区分dev和prod两套配置,数据库地址、Redis地址、文件存储路径等都通过环境变量注入。小程序端的baseUrl也要做成可配置的,用不同环境的配置对象来区分,避免每次发布都要改一堆代码。
登录和支付回调的地址,在小程序后台和服务器安全组里都要放量。Nginx上如果能顺便配好HTTPS证书和反向代理,把tomcat直接隐藏到Nginx后面,整个架构会更接近生产标准,也更安全。
二手书交易平台这套东西,做起来不难,但做到“能打”需要花心思。技术选型、数据库设计、接口开发、小程序联调,每一步都有太多能优化的细节。希望这篇文章里的经验和坑能帮你把项目做快一点、做好一点。如果你在做的过程中遇到什么卡住的地方,欢迎回来一起聊,很多时候一个卡点解决掉,后面就是一路畅通了。