每年到了毕业季,总能在技术群里看到有人在问:Spring Boot 后端搭配微信小程序前端的二手书交易平台,到底应该怎么搭?这个题目确实是典型的全栈练习项目,看起来好像哪里都有教程,但真正从零开始做的时候,会遇到一堆网上没讲透的问题。小程序端的登录鉴权怎么做、发布商品图片怎么传、订单状态怎么流转、后端接口怎么设计才够灵活——这些才是项目能不能顺利跑通的关键。
这篇内容不打算做成面面俱到的说明书,而是把我自己从数据库设计到前后端联调、再到部署上线的完整思路和踩坑记录分享出来。无论你是准备拿它当毕业设计,还是想练手做一个完整的全栈项目,这篇文章应该都能帮你省下不少时间。
1. 项目整体设计与功能拆解
1.1 核心需求分析——二手书交易要解决什么问题
做项目之前,先把业务想明白。二手书交易和普通电商不太一样,有几个非常明显的业务特点。
第一,商品是 C2C 的,不是平台自营。卖书的用户就是普通学生或读者,每个人都可以发布自己的闲置书籍。平台本身没有库存概念,只负责撮合和信息展示。所以用户体系里必须区分“买家”和“卖家”身份,但实际上一个人既会买也会卖,大多数系统直接用同一个用户角色处理,不单独做商家入驻审核。
第二,交易金额小,但信息真实性重要。一本书十几块到几十块不等,用户最在意的是书的品相、版本、是否有笔记划线。这意味着商品详情页必须支持多图展示、文字描述,最好还能提供 ISBN 检索或分类筛选,降低用户找书的成本。
第三,买卖双方有沟通需求。二手书不是标品,买家可能想问“这本书是第几版”“划重点多不多”,卖家也可能想讲价。所以一个简单的留言或私信功能很有必要。做毕设或练手项目时,可以用订单留言或者站内消息的方式实现,不需要上 WebSocket 实时聊天,降低复杂度。
基于这些分析,我把系统功能模块拆成了这几块:
- 用户模块:微信授权登录、个人资料维护、我发布的书籍、我买到的书籍
- 商品模块:图书发布、图书列表(分页/搜索/分类筛选)、图书详情、图书下架与删除
- 交易模块:购物车、生成订单、支付状态模拟、订单状态管理(待付款/待发货/已发货/已完成/已取消)
- 互动模块:用户留言或收藏
- 管理端(可选):后台管理页面,用浏览器访问,管理用户、商品审核、订单查看
这样的功能规模对于毕设或简历项目来说刚刚好,既有完整业务闭环,又不会因为范围太大导致烂尾。
1.2 技术选型思路——为什么是 Spring Boot + 微信小程序
这个组合在近几年的项目里几乎成为标配,原因很现实。
后端选 Spring Boot,是因为它极大降低了 Java 项目的搭建成本。Spring Boot 内置了 Tomcat,自动配置机制把繁琐的 XML 配置都干掉了。一个新项目只需要引入依赖、写几个注解,就能跑起一个可用的 Web 服务,这对学生党来说非常友好。再加上 Spring Boot 的生态非常成熟,无论是连 MySQL、操作 Redis,还是集成 MyBatis-Plus,都有现成的 starter,代码量比传统 SSM 少一半以上。
前端选微信小程序,是因为小程序本身就是一个天然的分发渠道,不需要用户安装 App,扫码就能打开。对它做二手书交易有天然的场景优势:校园里的学生用户,微信群一转发,书就能卖出去。另外,小程序提供的wx.login接口能直接获取用户身份凭证,配合后端换取 openid,免去了手机号注册等繁琐流程,用户体验很流畅。
当然,技术选型不是绝对的。同类方案里,前端也可以用 uniapp 替代原生小程序,后端也可以用若依脚手架快速起项目。但如果是学习目的,我更推荐原生小程序 + 手写 Spring Boot,原因很简单:框架替你做了太多事情,你就学不到底层原理了。等把这一套完整跑通,再去看 uniapp 或若依,会非常轻松。
1.3 功能模块全景与用户操作流程
为了让后续章节有图可循,我先用文字描述一下核心用户流程。
买家视角:打开小程序 → 微信授权登录 → 进入首页浏览图书列表 → 点击分类或搜索关键词 → 进入图书详情,查看实拍图、书况描述 → 点击“加入购物车”或“立即购买” → 生成订单 → 模拟支付 → 等待卖家处理 → 确认收货。
卖家视角:打开小程序 → 进入“卖书”页面 → 填写 ISBN、书名、作者、定价、转让价、书况描述、上传实拍图 → 发布成功 → 在“我发布的”中看到书籍状态 → 收到买家订单后发货 → 确认完成。
管理员视角(后台 Web):登录管理端 → 查看用户列表 → 审核和上下架图书 → 查看所有订单,处理纠纷。
核心流程看起来不复杂,但落到代码上,每一步都有细节。接下来从数据库设计开始,逐个环节拆解。
2. 后端核心设计与实现
2.1 数据库表结构设计——先把地基打牢
数据库设计是很多新手最容易忽略的环节。表结构如果一开始没想清楚,后面写代码可以说是处处别扭。我最终设计了 7 张核心表,这里逐一说明字段设计的理由。
用户表(user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| openid | varchar(64) | 微信唯一标识,业务逻辑里用它关联用户 |
| nickname | varchar(64) | 昵称 |
| avatar_url | varchar(255) | 头像地址 |
| phone | varchar(20) | 联系电话,用于交易联系 |
| create_time | datetime | 注册时间 |
| status | tinyint | 状态,1正常 0封禁 |
openid 是微信用户的唯一身份标识,后端通过微信登录接口拿到之后存库。需要注意,openid 对用户不可见,不要让前端把 openid 当 userId 传。系统内部还是用自增主键 id 关联业务数据。
图书表(book)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 发布者用户 ID |
| title | varchar(128) | 书名 |
| author | varchar(64) | 作者 |
| publisher | varchar(128) | 出版社 |
| isbn | varchar(32) | ISBN 编号 |
| original_price | decimal(10,2) | 原价 |
| sell_price | decimal(10,2) | 转让价 |
| condition_desc | varchar(255) | 书况描述,如“九成新,无笔记” |
| cover_url | varchar(255) | 封面图 |
| images | text | 多图 URL,用逗号分隔或 JSON 字符串 |
| category_id | bigint | 分类 ID |
| status | tinyint | 状态:1在售 2已售 3下架 4待审核 |
| view_count | int | 浏览量,可做排序 |
| create_time | datetime | 发布时间 |
| update_time | datetime | 更新时间 |
图书表的 status 字段是整个商品流转的核心。发布时是待审核或直接上架,被下单后如果是“立即购买”可以立刻置为已售,如果只是加入购物车则应保持“在售”,等订单确定后再改状态。这块逻辑一定要理清,否则容易出现一本书同时被两个人下单的情况。
分类表(category)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(32) | 分类名,如专业课、考研、文学、外语 |
| sort | int | 排序值 |
分类表结构简单,但前端首页的分类导航全靠它。建议在一开始就初始化好数据,不要等发布商品时再做。
购物车表(cart)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 用户 ID |
| book_id | bigint | 图书 ID |
| create_time | datetime | 加入时间 |
购物车表不用存商品快照,因为购物车只是中转,结算后真正落库的是订单表。查询购物车时直接关联 book 表获取实时价格和状态即可。
订单表(orders)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(64) | 订单号,业务展示用 |
| buyer_id | bigint | 买家 ID |
| seller_id | bigint | 卖家 ID |
| book_id | bigint | 图书 ID |
| amount | decimal(10,2) | 成交金额 |
| status | tinyint | 状态:0待付款 1已付款 2已发货 3已完成 4已取消 |
| receiver_name | varchar(32) | 收货人 |
| receiver_phone | varchar(20) | 收货电话 |
| receiver_address | varchar(255) | 收货地址 |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 支付时间 |
| deliver_time | datetime | 发货时间 |
| finish_time | datetime | 完成时间 |
订单表是交易系统的核心。特别注意要冗余book_id和seller_id,不要通过 book 表去反查 seller,否则查询效率和代码可读性都会变差。订单号建议用时间戳 + 随机数生成,避免直接用自增 id 暴露订单量。
收藏表(favorite)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 用户 ID |
| book_id | bigint | 图书 ID |
| create_time | datetime | 收藏时间 |
留言/消息表(message)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| from_user_id | bigint | 发送者 |
| to_user_id | bigint | 接收者 |
| book_id | bigint | 关联书籍,可为空 |
| content | varchar(512) | 内容 |
| create_time | datetime | 创建时间 |
消息表在毕设里属于加分项,但一定要控制好量级,不要做成复杂聊天系统。买家下单前对某本书有疑问,直接留言,卖家在“消息中心”看到后回复即可,简单实用。
2.2 Spring Boot 项目搭建与目录结构规划
后端项目我建议直接用 Spring Initializr 生成,选 Java 8 或 Java 11 都行,框架版本不要追新,稳定优先。我自己用的组合是 Spring Boot 2.7.x + MyBatis-Plus 3.5.x + MySQL 8.0,这套组合在网上能找到大量踩坑资料,出了问题也容易排查。
依赖选择很关键:
- spring-boot-starter-web:提供 MVC 和 Tomcat
- mybatis-plus-boot-starter:简化 SQL 操作,分页查询非常方便
- mysql-connector-java:MySQL 驱动
- lombok:减少实体类 getter/setter 代码
- hutool-all:工具包,生成订单号、封装 HTTP 请求等
- jjwt 或 java-jwt:生成 Token 做登录鉴权
- spring-boot-starter-validation:参数校验
目录结构按常见的分层思想来:
com.example.booktrade ├── controller // 接口层,接收参数,返回结果 ├── service // 业务层,处理核心逻辑 │ └── impl ├── mapper // MyBatis-Plus 数据访问层 ├── entity // 数据库实体类 ├── dto // 请求参数对象 ├── vo // 返回视图对象 ├── config // 配置类,如跨域、拦截器 ├── common // 统一返回结果、异常处理、常量 └── utils // 工具类,如 JWT 工具有些同学喜欢把所有代码堆在 controller 里,一个方法写上几十行 SQL 操作,方便是方便,但项目只要稍微扩展一点就会失控。分层可能看起来多写了一些类,但维护的时候价值就出来了。特别是订单状态流转这种逻辑,拆到 service 层写清楚,比堆在 controller 里好调得多。
2.3 微信登录与 Token 鉴权——最容易被忽略的环节
小程序的登录机制和后端 JWT 鉴权是很多新手最容易懵的地方。我来理一遍完整流程。
小程序端调用wx.login(),拿到一个临时凭证 code。这个 code 的有效期只有 5 分钟,而且只能用一次。然后小程序把 code 通过 HTTP 请求发给后端。
后端拿到 code 后,调用微信的接口:
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=CODE&grant_type=authorization_code微信会返回:
{ "openid": "xxxxx", "session_key": "xxxxx" }这个 openid 就是用户的唯一标识。后端先去数据库查这个 openid 是否已存在,不存在就创建一个新用户。然后生成一个 JWT 返回给小程序端。小程序把 JWT 存到本地 storage,之后每个请求都在 header 里带上Authorization: Bearer <token>。
后端通过一个拦截器拦截所有需要登录的接口,从 token 里解析出用户 ID,放到 ThreadLocal 或请求上下文里,业务代码直接取当前用户。
这里有几个细节需要注意:
- appid 和 secret 不要写死在前端代码里,请求时只需传 code,appid 和 secret 只能保存在后端。
- session_key 是微信用于解密手机号等敏感信息的密钥,一般业务用不到,不要返回给前端。
- JWT 的过期时间建议设置 7 天左右,太短会导致用户体验差,太长有安全风险。
- 必须在启动类或配置类里注册拦截器,并排除登录、商品列表等公开接口,否则小程序端一打开首页就会 401。
我用 jjwt 0.9.1 写过一版,核心代码很简单:
public String generateToken(Long userId) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + 7 * 24 * 3600 * 1000); return Jwts.builder() .setSubject(String.valueOf(userId)) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public Long parseToken(String token) { Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); }2.4 核心接口设计与状态流转约定
接口设计不追求 RESTful 的绝对标准,但 URL 语义要清晰,前后端传参要能对上。我整理的接口清单大致如下:
| 模块 | 方法 | URL | 说明 |
|---|---|---|---|
| 用户 | POST | /api/user/login | 微信登录 |
| 用户 | GET | /api/user/info | 获取当前用户信息 |
| 图书 | GET | /api/book/list | 分页搜索图书 |
| 图书 | GET | /api/book/detail/{id} | 图书详情 |
| 图书 | POST | /api/book/add | 发布图书 |
| 图书 | PUT | /api/book/update | 修改图书 |
| 图书 | DELETE | /api/book/{id} | 下架/删除图书 |
| 图书 | GET | /api/book/mylist | 我发布的图书 |
| 购物车 | GET | /api/cart/list | 购物车列表 |
| 购物车 | POST | /api/cart/add | 加入购物车 |
| 购物车 | DELETE | /api/cart/{id} | 删除购物车项 |
| 订单 | POST | /api/order/create | 创建订单 |
| 订单 | POST | /api/order/pay | 模拟支付 |
| 订单 | POST | /api/order/deliver | 卖家发货 |
| 订单 | POST | /api/order/confirm | 买家确认收货 |
| 订单 | GET | /api/order/list | 订单列表(区分买家/卖家) |
| 收藏 | POST | /api/favorite/add | 添加收藏 |
| 收藏 | GET | /api/favorite/list | 收藏列表 |
订单状态流转是整个项目的业务难点。我在 service 层定义了几个状态常量,然后写了一个状态机校验方法,每次更新状态时都检查前置状态是否合法。
public static final int STATUS_WAIT_PAY = 0; public static final int STATUS_PAID = 1; public static final int STATUS_DELIVERED = 2; public static final int STATUS_FINISHED = 3; public static final int STATUS_CANCELLED = 4;更新状态时的校验逻辑:
switch (targetStatus) { case STATUS_PAID: if (currentStatus != STATUS_WAIT_PAY) { throw new BusinessException("订单状态异常,无法支付"); } break; case STATUS_DELIVERED: if (currentStatus != STATUS_PAID) { throw new BusinessException("订单未付款,无法发货"); } break; case STATUS_FINISHED: if (currentStatus != STATUS_DELIVERED) { throw new BusinessException("订单未发货,无法确认收货"); } break; }这种显式的状态判断虽然代码多一些,但逻辑清晰,排错也方便。千万别图省事直接更新状态,等到订单数据出现错乱时,排查成本远超当初省下的那几分钟。
3. 微信小程序前端核心设计
3.1 原生小程序还是 uniapp——我的选择和建议
小程序的实现方式有原生和 uniapp 两种主流方案。原生小程序好处是没框架层转换,性能好、调试方便、微信新功能可以第一时间使用。坏处是只能在微信生态里跑,如果要发布到支付宝等平台,代码要重写。uniapp 的优势是一套代码多端发布,但缺点也很明显,它的小程序性能损耗在复杂页面里能感受到,而且遇到自定义组件的问题时排查困难。
我的建议是:如果你主要面向微信小程序,且这是练手或毕设项目,原生小程序就够了,踩坑少,教程多。如果你希望以后还能做 App 或 H5,选 uniapp 更划算。但不管选哪个,核心业务逻辑在后端,前端切换并不伤筋动骨。
3.2 页面结构设计与底部导航配置
小程序端的页面结构,我是按下述方案划分的:
- pages/index/index:首页,图书列表、搜索、分类入口
- pages/category/category:分类浏览
- pages/publish/publish:发布图书
- pages/cart/cart:购物车
- pages/me/me:个人中心
- pages/book/detail:图书详情
- pages/order/list:订单列表
- pages/order/detail:订单详情
- pages/message/list:消息列表
底部导航栏(tabBar)一般保留 4 个主入口:首页、分类、发布(可以用中间凸起按钮)、购物车、我的。不过 tabBar 最多支持 5 个,发布按钮居中凸起需要写自定义 tabBar,有一定复杂度。如果想省事,就把发布入口放到“我的”页面里。
tabBar 的配置在app.json中:
{ "pages": [ "pages/index/index", "pages/category/category", "pages/cart/cart", "pages/me/me" ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "images/home.png", "selectedIconPath": "images/home-active.png" }, { "pagePath": "pages/category/category", "text": "分类", "iconPath": "images/cate.png", "selectedIconPath": "images/cate-active.png" }, { "pagePath": "pages/cart/cart", "text": "购物车", "iconPath": "images/cart.png", "selectedIconPath": "images/cart-active.png" }, { "pagePath": "pages/me/me", "text": "我的", "iconPath": "images/me.png", "selectedIconPath": "images/me-active.png" } ] } }tabBar 的图标对尺寸要求很严格,建议 81px * 81px,纯 PNG 格式,不要有透明干扰像素。如果图标不合格,可能出现图标显示不出来或位置偏移的问题。
3.3 核心交互细节:列表分页、下拉刷新、图片上传
列表分页
图书列表页的数据量会随着用户发布增多而变大,必须做分页。小程序端实现分页的逻辑是:记录当前页码,触底时页码加一并请求下一页,把新数据追加到原有数组。
Page({ data: { bookList: [], page: 1, pageSize: 10, hasMore: true }, onReachBottom() { if (!this.data.hasMore) return; this.loadBooks(); }, loadBooks() { const { page, pageSize, bookList } = this.data; wx.request({ url: `${apiBaseUrl}/api/book/list`, data: { page, pageSize, keyword: this.data.keyword, categoryId: this.data.categoryId }, success: (res) => { const records = res.data.data.records; this.setData({ bookList: page === 1 ? records : bookList.concat(records), page: page + 1, hasMore: records.length === pageSize }); } }); } });关于分页返回格式,MyBatis-Plus 的分页插件默认返回{ records, total, size, current }结构,前端可以直接用。网上不少模板项目用的是PageHelper,也大同小异,但 MyBatis-Plus 的过程更顺滑。
下拉刷新
下拉刷新需要在页面 JSON 里开启"enablePullDownRefresh": true,然后在onPullDownRefresh回调里重置 page 为 1,重新请求数据,请求完成后调用wx.stopPullDownRefresh()关闭动画。这里经常有人忘了调用wx.stopPullDownRefresh(),导致刷新动画一直转圈,体验很差。
图片上传
发布图书时通常要上传多张实拍图。小程序端调用wx.chooseMedia选择图片,然后通过wx.uploadFile逐个上传到后端,后端接收后存到服务器或云存储,返回图片 URL。
wx.chooseMedia({ count: 6, mediaType: ['image'], sourceType: ['album', 'camera'], success: (res) => { const files = res.tempFiles; files.forEach((file, index) => { this.uploadImage(file.tempFilePath, index); }); } }); uploadImage(filePath, index) { wx.uploadFile({ url: `${apiBaseUrl}/api/upload`, filePath: filePath, name: 'file', success: (res) => { const data = JSON.parse(res.data); this.setData({ [`images[${index}]`]: data.data }); } }); }上传接口后端只需要保存文件。要注意图片大小的限制,微信小程序上传接口默认限制是 10MB 以内,但实际开发中建议在chooseMedia时限制 sizeType 为 compressed,否则手机原图动辄几 MB,上传慢,服务器存储压力也大。
另外,如果有微信小程序审核的计划,注意wx.chooseMedia的摄像头调用,审核时会要求说明调用摄像头的具体场景。如果你的应用只是发布图书,直接限定album,去掉camera,可以省掉很多审核沟通成本。
3.4 登录态管理与请求封装
小程序端每个页面都要请求后端接口,如果把wx.request裸写在各页面里,后期维护会让人抓狂。我的做法是在 utils 里封装一个request.js:
const request = (url, method, data) => { return new Promise((resolve, reject) => { wx.request({ url: `${apiBaseUrl}${url}`, method, data, header: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${wx.getStorageSync('token')}` }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { wx.navigateTo({ url: '/pages/login/login' }); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail: (err) => reject(err) }); }); };然后所有接口调用都走这个封装:
api.getBookList = (params) => request('/api/book/list', 'GET', params); api.createOrder = (data) => request('/api/order/create', 'POST', data);登录时机也是一个值得注意的点。不要在小程序一启动就强制登录,有些用户只是随便逛逛。我的做法是:未登录状态下允许浏览图书列表和详情,点击“发布”“购物车”“购买”时才跳转登录。登录操作微信用wx.login静默完成,用户几乎感知不到登录过程,体验顺畅很多。
4. 部署、联调与避坑实录
4.1 本地联调环境搭建——微信开发者工具的坑
本地联调阶段,最头疼的问题就是小程序端请求后端接口时的域名校验。微信开发者工具默认要求 HTTPS 域名,并且在后台配置合法域名才能请求,这在开发阶段非常不方便。
解决方案是打开开发者工具的“详情 → 本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。配置后就能通过http://localhost:8080直接访问本地后端。
但这里还有一个隐含问题:如果用真机预览,手机访问不到电脑的localhost,需要改成电脑在局域网中的 IP 地址,比如http://192.168.1.100:8080。同时,后端要配置跨域支持,否则浏览器端(如小程序开发者工具)请求会被浏览器拦截。
后端加一个跨域配置类即可:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns在 Spring Boot 2.4 之后替代了allowedOrigins("*"),写法上别搞混。另外,既然小程序端通过 wx.request 请求时本身不受浏览器同源策略限制,跨域配置主要服务的是开发者工具模拟器和如果你以后要加一个 Web 管理后台的需求,所以保留是有价值的。
4.2 我踩过的坑——上不了线的图片存储方案
图片上传后的存储路径是很多大学生项目最容易出的问题。最开始我把图片存到了项目本地目录,也就是src/main/resources/static/upload下面,开发时测试一切正常。但部署到云服务器后,问题接踵而至:一是 Spring Boot 打成 jar 包后资源路径是只读的,图片写不进去;二是即使用了外置目录,重启服务后图片路径可能丢失。
最后的解决方案是:在服务器上单独建一个/data/upload目录,后端配置一个虚拟路径映射,把/api/upload/**映射到该目录。这样图片就存在服务器的磁盘上,重启不丢。
再往后如果做正式项目,建议直接接入云存储(七牛云、阿里云 OSS 等),图片上传后拿到 CDN 加速 URL,既省服务器空间,访问速度也更快。但做项目的话,本地磁盘也够用,先把业务跑通更重要。
4.3 常见问题排查速查表
我把项目开发过程中遇到的高频问题和解决方法整理成一张速查表,希望对你有帮助。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 小程序请求接口报 404 | 后端路径没对上,或没有加/api前缀 | 检查 controller 的 RequestMapping 配置 |
| 请求一直转圈,没有返回 | 没有关闭域名校验,或后端没启动 | 检查开发者工具本地设置和后端日志 |
| 登录接口返回 401 | code 已过期或重复使用 | 确保每次登录重新 wx.login 获取新 code |
| Token 解析失败 | JWT 密钥不一致,或前后端时间不同步 | 检查 JWT 签名密钥配置 |
| 图片上传失败 | 上传目录不存在,或文件大小超限 | 手动创建 /data/upload 目录,或用 compressed 模式 |
| 数据库中文乱码 | 连接串没有指定编码 | 连接串加characterEncoding=utf8 |
| 分页数据重复 | 未处理重复上拉触发 | 用 hasMore 标志和 loading 标志防止并发请求 |
| 下拉刷新一直在转 | 没有调用 stopPullDownRefresh | 在请求成功后调用关闭 |
还有一个非常经典的问题:MySQL 8.0 的驱动类名变了。如果用 MySQL 5.x 的写法com.mysql.jdbc.Driver连 8.0 会报错,需要改成com.mysql.cj.jdbc.Driver,同时连接串里要加时区参数serverTimezone=Asia/Shanghai,不然会报时间相关的异常。
5. 后端关键原理拾遗——面试和答辩的加分点
既然项目挂在 Spring Boot 下,答辩或面试时,面试官大概率会问几个框架底层相关的问题。如果你只是把代码写出来,说不清框架原理,其实挺亏的。这个项目里最有价值的两个原理点是自动装配和依赖注入,我建议你把这个项目当成切入点,把这两个点吃透。
5.1 Spring Boot 自动装配——它到底“自动”了什么
很多教程在讲自动装配时只顾背概念,但落到项目里,你应该能说清楚:MyBatis-Plus 的SqlSessionFactory是谁创建的?数据源配置怎么生效的?
Spring Boot 的核心是@SpringBootApplication注解,它组合了@EnableAutoConfiguration。这个注解会通过AutoConfigurationImportSelector去加载 jar 包中META-INF/spring.factories文件里声明的所有自动配置类。但加载不等于生效,每个自动配置类都配合使用@ConditionalOnClass、@ConditionalOnMissingBean等条件注解。
以DataSourceAutoConfiguration举例:当你引入了spring-boot-starter-jdbc或 MyBatis 相关依赖,classpath 下存在DataSource.class,这个自动配置类才生效,然后根据spring.datasource.url等配置帮你创建数据源 Bean。如果你没有配置,它就用默认的内存数据库 H2,这就能解释为什么只引入依赖不写配置时,项目跑起来数据库连接报错。
另一层的自动配置发生在 MyBatis-Plus 里。你只需要在配置文件里写好数据源信息,再在启动类加@MapperScan注解,MyBatis-Plus 的MybatisPlusAutoConfiguration就会自动创建SqlSessionFactory和MapperScannerConfigurer,把接口代理成 Mapper 对象。
理解了自动装配,面试时你可以主动提一下spring.factories或org.springframework.boot.autoconfigure.AutoConfiguration.imports的加载机制,顺便说你可以在自己的项目中自定义一个自动配置,比如写一个发短信的 starter。只要思路清晰,这部分是很加分的。
5.2 常用注解的底层逻辑——不只是“加上能用”
项目中用了大量注解,比如@RestController、@Autowired、@Transactional。对这些注解的理解至少要达到能解释“为什么加了注解,功能就生效了”。
@RestController结合了@Controller和@ResponseBody,但底层是靠RequestMappingHandlerMapping在启动时扫描方法上的@RequestMapping,把 URL 和 HandlerMethod 建立映射关系的。所以接口路径不能重复,否则启动时报错。
@Autowired是依赖注入的核心。Spring 容器启动时会创建 Bean 实例,然后在处理依赖注入时,先按类型查找,再按名称查找。如果找到多个同类型的 Bean,可以通过@Qualifier指定名称。
@Transactional是事务注解,底层是 Spring AOP 的动态代理。当调用被注解的方法时,代理对象会开启事务,异常时回滚。但这里有一个经典坑:同类内部方法调用this.xxx()时,this不是代理对象,事务会失效。所以在设计 service 时,如果 A 方法调用了 B 方法,且 B 需要事务,最好把 B 拆到另一个 service 类中。
这些原理在答辩现场非常容易引发追问,建议认真准备。光会 CRUD 的毕业生一抓一大把,能把这些原理讲清楚,差距一下就拉开了。
6. 项目上线实操与后续扩展方向
6.1 从本地到云服务器的部署流程
项目做完在本地能跑,和真正部署上线是两码事。如果只是要在答辩时给老师或评委演示,可以在演示电脑上用本地环境跑,问题不大。但如果要拿去实习面试或给自己做一个完整作品,建议实际部署到云服务器,会更有说服力。
我的部署步骤大致是:
服务器环境准备。买一台最基本的云服务器即可,学生机配置就够用,操作系统选 CentOS 7 或 Ubuntu 20.04。在服务器上安装 JDK 8 或 11、MySQL 8.0、Nginx。
后端部署。本地用 Maven 打包:
mvn clean package -DskipTests打包完成后,target目录下会生成booktrade-0.0.1-SNAPSHOT.jar,通过scp命令或宝塔面板上传到服务器,然后执行:
java -jar booktrade-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod建议在application-prod.yml里覆盖数据库连接、上传目录等配置,不要直接把本地配置带到生产环境。
前端发布。微信小程序不是打包成静态文件部署,而是在微信公众平台注册一个小程序账号,通过开发者工具上传代码,然后在后台提交审核。审核通过后,用户就能在微信中搜索到这个小程序。
域名与 HTTPS。发布小程序时,微信强制要求请求接口必须是 HTTPS 域名,所以需要购买一个域名,解析到服务器,再用 Nginx 配置 SSL 证书反向代理到后端的 8080 端口。这一步对很多学生来说比较陌生,但其实不复杂,Nginx 的配置网上有大量现成模板可以改。
6.2 代码之外的建议——数据初始化和演示准备
每次答辩或项目演示前,建议先初始化一批模拟数据:添加几个测试用户、发布十几本不同品相的图书、故意制造几个不同状态的订单。这样演示时可以直接展示列表、详情、交易流程,不用现场现发布、现创作数据,显得你准备充分。
还可以给账号设置一个测试专用场景:比如发布一本售价 0.01 元的书,然后完整走一遍“购买 → 支付 → 发货 → 确认收货”的闭环,演示效果会非常直观。
6.3 这个项目还能怎么扩展
项目做完后不要停在原地,可以尝试从下面几个方向做扩展,每扩展一个方向,项目的含金量就能上一个台阶:
- 接入真实微信支付:目前用“模拟支付”过渡,替换成微信支付 API 后就是一个完整的商业闭环。
- 接入消息推送:买家下单、卖家发货时通过微信订阅消息通知对方,能显著提升体验。
- 引入 Redis:做热门书籍排行榜、搜索历史缓存、验证码存储,面试时也可以聊缓存设计。
- 增加推荐算法:根据用户浏览和收藏记录,用简单的协同过滤或基于标签的推荐算法做“猜你喜欢”。
- 管理端完善:把后台管理页面从简单的 CRUD 升级成数据统计仪表盘,展示每日订单量、交易额、书籍分类占比等。
这些扩展不用全做,挑一两个感兴趣的方向动手即可。关键是让面试官或老师看到一个“有思考、能落地”的人,而不是一个只会跟着教程敲代码的搬运工。
最后一个实操心得
如果你现在正处在“项目还没跑通”的阶段,我的建议很简单:先把后端跑通,用 Postman 调通登录和图书列表接口,再开始写小程序。前后端同时开工只会让你两头都顾不好。我见过太多同学一上来就猛写小程序页面,结果后端接口一改,前端全部返工,心态直接就崩了。后端的核心逻辑稳定了,前端对接只是时间问题,这个顺序千万别搞反。项目做完之后你会明显感觉到,最值钱的其实不是代码本身,而是“把一个完整系统从零折腾上线”的经验,这些过程里的判断和踩坑,才是毕业设计和简历里真正能打动人的东西。