说实话,这类“宠物用品销售小程序”项目,我见过太多人拿它当毕业设计或者练手项目。但真正有价值的地方不在于“能跑起来”,而在于你能否把一条核心链路吃透:用户通过微信小程序浏览宠物商品、加入购物车、下单支付,然后管理员在后端管理商品库存、处理订单状态。你要是能把这条链路的每一步都讲清楚,Spring Boot的基础功底基本就稳了。
这篇博文就围绕“springboot宠物用品销售小程序附源码86805”这个项目来展开,聊一聊项目本身的需求拆解、技术选型、核心业务实现思路,以及源码拿到手之后怎么才能顺利跑起来。很多细节都是我实际调试项目时踩过坑之后总结出来的,直接照着做,能省不少时间。
1. 先从项目需求谈起:谁在卖、谁在买、谁来管
1.1 小程序端的角色划分:不只是给顾客逛商品
很多人看“销售小程序”这几个字,下意识觉得这就是一个购物页面套壳,其实并不准确。小程序端给到普通用户的,表面上是商品浏览和下单入口,背后其实是一条完整的用户操作链路。
用户进入小程序,至少要能做这些事情:
- 浏览首页推荐位、轮播图、公告栏,理解当前店铺主推什么商品;
- 按宠物分类筛选商品,比如猫粮、狗粮、宠物玩具、洗护用品、猫砂、宠物医疗保健用品;
- 搜索具体商品名称,查看商品详情页,包括价格、库存、规格、说明、图片;
- 把商品加入购物车,在购物车中调整数量、删除商品;
- 填写或选择收货地址后提交订单;
- 在个人中心查看自己的订单列表、订单详情、订单状态;
- 对订单进行取消或者确认收货操作。
小程序端的核心价值,是让用户在一个轻量级的载体里完成“从看到买”的全部动作。因此前端的页面设计、请求接口的划分,全都得围绕这个目标走。
1.2 管理端的存在意义:没有后台的商城只是一堆静态页面
宠物用品销售小程序通常还要搭配一个管理后台,入口可能是浏览器端的 Web 页面,也可能在小程序里单独做一个管理角色入口。管理后台对应的身份是店铺运营人员或管理员,主要负责以下工作:
- 商品分类管理:增加、修改、停用分类;
- 商品管理:新增宠物用品、编辑商品信息、上传商品图、设置上下架状态、管理库存;
- 轮播图管理:维护首页的推荐展示位,把活动商品放到首页曝光;
- 订单管理:查看用户提交的订单,进行发货操作,处理取消或退款;
- 会员管理:查看注册用户的基本信息和注册时间;
- 数据概览或者简单的统计报表。
为什么这个项目值得推荐?因为它不是单一维度的 CRUD 练习,而是天然形成了“用户端小程序 + 管理端后台 + Spring Boot 服务端 + MySQL 数据库”的完整闭环。每一个功能都不是孤立的,商品、订单、用户、库存这些数据都能在后台管理模块中找到对应入口。这种业务上的联动性,正是新手最容易缺的那一块拼图。
1.3 从业务角度看,宠物用品商城的“难”点在哪里
宠物用品这个垂直领域,和普通服装、电子数码商城比起来,有自己比较特殊的地方。
第一个是商品规格。宠物粮分幼猫成猫、分不同口味、不同净含量;宠物用品又涉及颜色、尺码。一个商品可能对应多个 SKU,如果数据库建模阶段没考虑清楚,后面做购物车、订单的时候会非常难受。
第二个是商品分类的层级。宠物商城常见的分类方式有两种:一种按宠物种类分,比如犬用品、猫用品;一种按商品用途分,比如食品、玩具、医疗保健。实际项目里通常两种维度会混合出现,需要在分类表里预留父子级结构,或者用独立的分类关联表,否则后面扩展分类会发现结构设计根本不够用。
第三个是订单和库存的强关联。用户下单前商品明明还有库存,下单的一瞬间可能就被别的用户买走了。如果扣库存操作不是在数据库层面做原子性控制,并发稍微大一点就会超卖。虽然这种学习项目流量通常不大,但是建模和接口设计的时候把这一点考虑进去,能体现出你对业务的理解深度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与工程结构:为什么 Spring Boot 能撑起小程序后端
2.1 前后端分离下的小程序与服务端协作机制
小程序端和后端之间的协作模式,是典型的前后端分离架构。小程序运行在微信里面,负责页面渲染和用户交互,并不直接访问数据库。所有数据操作都要通过微信小程序的 wx.request 接口,以 HTTP 请求的方式提交给 Spring Boot 后端,由后端完成业务校验、数据库读写后,再以 JSON 格式把结果返回给小程序。
这种架构带来的好处很明显:后端接口可以复用,同一个 Spring Boot 服务既可以给小程序端提供接口,也可以给管理后台提供接口。只要接口的返回格式统一,前端怎么换都不影响业务核心。
因此,项目里通常会约定一个统一响应体结构,常见的是包含 code、message、data 三个字段。成功时 code 为 0 或 200,失败时返回错误码和提示信息。这样做的好处是前端可以非常方便地在请求层做统一拦截,比如所有接口里只要 code 表示登录态过期,就统一跳转登录页,而不需要每个接口单独处理。
2.2 Spring Boot、MyBatis Plus 与 MySQL 的组合逻辑
Spring Boot 在这个项目里的定位,就是快速搭建一个可运行的 Java Web 服务。它最大的价值在于自动配置机制:只要在 pom.xml 里引入相关依赖,框架就会自动帮我们装配大部分的基础组件,比如内嵌的 Tomcat、数据源、JSON 序列化等。项目代码只需要关注自己的业务逻辑,不需要花大量时间处理繁琐的 XML 配置。
数据层方面,我推荐使用 MyBatis Plus,这类来源码项目里最常见的技术栈也是它。MyBatis Plus 对 MyBatis 做了增强,内置了通用的 insert、selectById、updateById、deleteById 方法和 QueryWrapper 条件构造器。对于宠物商城这种业务,大量操作其实是单表 CRUD,用 MyBatis Plus 可以少写很多 XML 文件,代码会清爽得多。
MySQL 则负责把最终数据落盘。商城类项目的数据表之间关联关系比较清晰,用关系型数据库非常合适。宠物商品的库存字段、订单金额字段都要求可靠性和事务性,这不是 NoSQL 能解决的。
2.3 Spring Boot 版本选择:不要盲目追求新版本
这里要重点提醒一下,拿源码学习、跑通项目的时候,Spring Boot 版本非常重要。很多人在网上找相关源码,下载后启动直接报错,原因往往就是版本不匹配。常见的情况有两种:
一种情况是源码用了 Spring Boot 2.x,但你本地的 JDK 或依赖管理工具默认拉取了不兼容的新版本。Spring Boot 3.x 有一个非常大的变化,是把 javax 命名空间迁移到了 jakarta 命名空间,导致大量第三方工具包必须跟着升级。很多旧版源码没有适配时就无法运行。
另一种情况是连接 MySQL 的驱动包坐标改了名字。旧版用的 mysql-connector-java 在新版中已经更名为 mysql-connector-j。如果 pom 文件中的依赖坐标不对,或者版本号里带了传递依赖冲突,连接数据库时就会报驱动类找不到。
对于跑这种学习项目,我通常的建议是优先使用源码自带的版本说明。如果源码本身没有明确标注,尽量选择 Spring Boot 2.5 到 2.7 之间的版本,搭配 JDK 8 或 JDK 11,整体会非常稳定。不要一上来就追求 JDK 17 加 Spring Boot 3.x,除非你已经能自己处理那些迁移问题。
2.4 后端工程包结构的实用划分
当你拿到一份完整源码,第一件事不是急着启动,而是先看后端工程的包结构是不是清晰。一个典型的宠物用品商城后端工程,差不多长这样:
text复制com.petshop
├── config
│ ├── CorsConfig.java
│ ├── MybatisPlusConfig.java
│ └── WebMvcConfig.java
├── controller
│ ├── admin
│ │ ├── AdminGoodsController.java
│ │ ├── AdminCategoryController.java
│ │ └── AdminOrderController.java
│ ├── ApiXXXController.java
│ └── LoginController.java
├── entity
│ ├── Goods.java
│ ├── Category.java
│ ├── CartItem.java
│ ├── Order.java
│ ├── OrderItem.java
│ └── User.java
├── mapper
│ └── 各种Mapper接口.java
├── service
│ ├── GoodsService.java
│ ├── CartService.java
│ ├── OrderService.java
│ └── impl
│ └── 各种实现类.java
├── utils
├── vo
└── PetShopApplication.java
controller 层只负责收参数、转参数、调用 service,不写具体业务逻辑;service 层处理核心业务,比如下单时扣库存、订单状态变更;mapper 层负责数据库操作;entity 对应数据库表字段;vo 用来向前端返回页面所需的组合数据。分得清楚的项目,后续扩展功能会非常顺手。
3. 商品、购物车、订单三大核心模块的后端落地细节
3.1 宠物商品与分类关系的数据库建模
先看商品分类。宠物用品商城的分类结构,很多情况下一开始只需要一级分类就够用,比如“猫粮”“狗粮”“宠物玩具”。但是只要业务稍微扩展,就需要在分类下面再区分“幼猫粮”“成猫粮”“全价猫粮”。所以分类表建议直接支持父子级结构。
基础分类表大致可以这样设计:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 分类ID |
| parent_id | bigint | 父分类ID,0表示顶级分类 |
| name | varchar | 分类名称 |
| icon | varchar | 分类图标 |
| sort | int | 排序号 |
| status | tinyint | 是否启用 |
商品表则要关联到分类:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 商品ID |
| category_id | bigint | 所属分类ID |
| name | varchar | 商品名称 |
| subtitle | varchar | 副标题 |
| main_image | varchar | 主图 |
| images | text | 商品轮播图,多个用逗号分隔 |
| price | decimal | 销售价格 |
| original_price | decimal | 原价,用于展示折扣 |
| stock | int | 库存 |
| sales | int | 销量 |
| detail | text | 富文本详情 |
| status | tinyint | 上下架状态 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
为什么要单独建一张分类表而不直接把分类名称写进商品表?因为分类一旦改名,关联商品会全部失效,而且你没办法按分类层级灵活筛选。建表阶段把关系理顺,后面管理端做分类级联下拉框就非常顺手。
当然,如果商品存在颜色、口味、规格等不同 SKU 的需求,还需要一张独立的 SKU 表来维护规格和库存。学习版商城为了控制复杂度,经常直接在商品表里放一个库存字段,不单独拆 SKU,这也是可以的,但你在理解时应清楚它的局限性。
3.2 购物车数据的存储方式:后端存储优于本地存储
购物车有两种常见的实现方式,一种是纯前端存储,一种是后端数据库存储。
纯前端存储,就是把购物车数据放在微信小程序的 storage 里,用户选择的商品数量和商品ID都存在本地。这种方式实现简单,但是问题非常多——用户换个手机购物车就没了,同一个账号在不同设备上看到的数据不一致。更关键的是,服务端拿不到用户的购物车数据,也就无法做“购物车商品降价提醒”“结算时自动识别失效商品”这类功能。
所以商城项目里,购物车数据建议直接存到后端,通过一个接口响应用户的操作。购物车表的设计可以参照下面这样:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 购物车项ID |
| user_id | bigint | 归属用户 |
| goods_id | bigint | 商品ID |
| goods_name | varchar | 商品名称快照 |
| goods_image | varchar | 商品图片快照 |
| price | decimal | 加入时的价格,结算时再读取最新价格 |
| count | int | 购买数量 |
| checked | tinyint | 是否勾选 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
为什么购物车表里要存商品名称、商品图片这些“冗余字段”?为了列表加载性能。如果购物车列表每次都去关联商品表重新查名称和图片,商品多的时候就会慢。学习项目里表数据量虽然小,但养成快照这种设计习惯是有价值的。
购物车接口大致分成几个:
text复制GET /api/cart/list 查询当前用户的购物车列表
POST /api/cart/add 添加商品到购物车
PUT /api/cart/update 修改购物车中商品数量或勾选状态
DELETE /api/cart/remove 删除购物车中的商品项
实际开发时,add 接口要注意一个细节,就是同一个用户往购物车添加同一件商品时,不要无限新增记录。通常的做法是先查一下购物车表里有没有该用户对该商品的记录,如果有,就直接把数量累加,否则才新增一条。
3.3 订单状态机的设计与库存扣减策略
订单模块是整个商城业务的“心脏”,订单表的设计决定了整个交易流程是否清晰。订单主表和订单明细表的关系是一对多:一个订单对应一个收货地址、一个总金额、一个整体状态;订单明细则记录每一件商品的名称、单价、数量。
订单主表的字段大致是下面这些:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 订单ID |
| order_sn | varchar | 订单编号 |
| user_id | bigint | 下单用户 |
| receiver_name | varchar | 收货人姓名 |
| receiver_phone | varchar | 收货人电话 |
| receiver_address | varchar | 收货地址 |
| total_amount | decimal | 订单总金额 |
| pay_amount | decimal | 实付金额 |
| pay_type | tinyint | 支付方式 |
| status | tinyint | 订单状态 |
| remark | varchar | 用户备注 |
| pay_time | datetime | 支付时间 |
| delivery_time | datetime | 发货时间 |
| finish_time | datetime | 完成时间 |
| create_time | datetime | 创建时间 |
订单状态我这里给一个典型的状态流转:
text复制0 待付款 -> 1 待发货 -> 2 待收货 -> 3 已完成
\ \
\ \-> 5 已取消(用户申请或超时未支付)
\-> 4 已取消
下单这一步是事务的重点。后端在提交订单时需要做的事情很多:校验商品是否存在且处于上架状态、校验商品库存大于等于购买数量、累加商品销量、扣减商品库存、计算总金额、生成订单主表和订单明细表、清空购物车中对应商品项、创建订单编号。这么多操作只要其中一个失败,前面做的所有数据变更都必须回滚。所以下单方法上必须加 @Transactional 注解。
如果要想进一步避免并发超卖,更稳妥的做法是在扣库存时使用乐观锁或者数据库层面的原子更新。比如执行更新库存的 SQL 时带上库存条件:
java复制boolean success = update("update goods set stock = stock - {count} where id = {goodsId} and stock >= {count}")
如果更新影响行数为 0,说明库存不足或商品已经变化,就不能再继续创建订单。这种写法在并发场景下比“先查询再更新”可靠得多。
3.4 管理端对订单状态的推送处理
管理端处理订单,核心操作是发货。管理员在订单列表中找到状态为“待发货”的订单,点击发货后,后端把订单状态从 1 改为 2,同时记录发货时间。流程上比较简单,但要注意一个问题:用户端订单列表需要实时反映这个状态。
小程序端通常用下拉刷新或者进入页面时重新拉取订单列表来获得最新状态,这个不算复杂。只要接口的返回字段里包含状态码和状态名称的映射关系,前端就可以把数字状态翻译成用户能读懂的文案。比较稳妥的做法是后端直接返回一个 statusText 字段,而不是让前端维护一套状态字典,这样即便后端修改了状态定义,前端也不容易出 bug。
4. 微信小程序端的关键开发细节:登录态、请求封装和签名机制
4.1 微信登录要做的不是“用户名密码登录”,而是 code 换 openid
很多第一次接触小程序商城的人,容易把传统 Web 登录的思路带进来:做一个登录页,输入手机号密码,然后调到主页面。但微信小程序的推荐登录方式不是这个思路。
小程序端通过 wx.login 获取一个临时登录凭证 code,然后把 code 传给后端。后端拿着 code、小程序的 AppID 和 AppSecret,去微信官方的接口换取 openid 和 session_key。openid 是用户在当前小程序下的唯一标识,后端拿它作为用户的唯一身份凭证。
后端拿到 openid 后,先去用户表里查一下这个用户是否存在,如果不存在就自动注册一条新记录,如果存在就直接登录。随后后端生成一个自定义登录态 token 返回给小程序。小程序后续每次请求都在请求头里带上这个 token,后端通过拦截器解析 token 判断当前用户身份。
这部分在源码里通常写成一个 LoginController 加一个拦截器。核心代码类似于:
java复制@PostMapping("/api/login")
public Result login(@RequestBody LoginRequest loginRequest) {
String url = "https://api.weixin.qq.com/sns/jscode2session" +
"?appid=" + appid +
"&secret=" + secret +
"&js_code=" + loginRequest.getCode() +
"&grant_type=authorization_code";
// 通过RestTemplate或HttpClient发起请求
// 解析返回的 openid 和 session_key
// 查询用户表,不存在则注册
// 生成token并返回
}
为什么不能直接用 wx.login 的结果当登录态?因为 code 有效期非常短,而且是一次性的,只能用来换 openid,不能作为后续请求的凭证。自定义 token 的目的,是在服务端维护一个可控的会话状态,可以设置有效期,也可以在后台强制把用户踢下线。
4.2 小程序请求封装:统一入口能解决 80% 的联调问题
我以前看过不少初学者写的小程序代码,每个页面的 onLoad 里都直接调一遍 wx.request,写了很多重复代码,而且 baseUrl 一改就得一个文件一个文件地找,非常痛苦。正确做法是把网络请求封装到一个独立的 request.js 文件里。
小程序端 utils 目录下的 request.js 大致长这样:
javascript复制const BASE_URL = 'http://127.0.0.1:8080/api'
function request(url, method = 'GET', data = {}) {
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + url,
method: method,
data: data,
header: {
'Content-Type': 'application/json',
'Authorization': wx.getStorageSync('token') || ''
},
success: (res) => {
if (res.data.code === 0) {
resolve(res.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 }
封装之后,页面里引用就变得非常简单:
javascript复制const { request } = require('../../utils/request')
request('/goods/list', 'GET', { page: 1 }).then(res => {
// 处理返回的商品列表
})
封装带来的直接好处是,你只需要在 request.js 里维护一个 BASE_URL,不管是在本地开发用局域网地址,还是上线改成 HTTPS 域名,都只改这一个文件。另一个好处是统一处理登录过期的情况,用户 token 失效时自动跳回登录页,不需要在每个页面重复写判断逻辑。
4.3 接口“签名”到底在防什么:防止请求被篡改和重放
热门词里有一个是“微信小程序签名”,很多新手看到之后以为小程序端所有请求都要做复杂的加密签名,其实要看项目在什么场景下。
如果只是做一个内部学习项目,接口直接通过 HTTPS 传输,后端靠 token 做身份验证,其实已经能满足基本需求。但如果项目涉及支付,或者接口要暴露在公网环境中,就需要签名机制来保证请求在传输过程中没有被第三方篡改,同时防止第三方拿到请求之后原样重放。
签名机制的一般玩法是这样的:小程序端在发起请求前,把核心业务参数加上一个双方约定好的密钥经过排序拼接,再用哈希算法生成一个 sign 字段。后端收到请求后按同样的规则计算签名,如果两边算出来的签名一致,就认为请求合法且未被篡改。
具体到宠物商城这个项目,最容易出现的安全问题其实不是这个。真正需要关注的是订单金额这类核心数据不能直接信任前端的传参。比如结算接口,正确的做法是后端根据商品单价和数量重新计算金额,而不是信任前端传过来的 totalAmount。前端传过来的参数只能作为参考,服务端必须持有最终的校验逻辑。
4.4 小程序端常见的页面数据绑定与生命周期
页面这一侧,要注意小程序页面的 onLoad、onShow 和 onPullDownRefresh 三个生命周期方法的区别。商品详情页的数据可以在 onLoad 里加载一次就够了;但个人中心的订单列表、购物车页面,最好在 onShow 里重新拉数据。因为用户可能在页面停留期间做了某种操作,返回这个页面时你需要展示最新的数据。
比如用户从购物车页面点击去结算,生成订单成功又返回到购物车页面,如果购物车页面只在 onLoad 里拉数据,那么已经购买过的商品还留在列表里,体验非常奇怪。
购物车页面尤其需要注意按钮的点击状态处理。用户勾选购物车商品时,前端要实时汇总已勾选商品的总价,发起结算请求的时候再把选中的购物车项 ID 列表传给后端。这种交互看起来简单,但前后端的数据结构需要对齐,否则经常出现前端认为选中了商品,后端却没在下单时包含这些商品的问题。
5. 把源码跑起来的完整过程:从数据库到开发者工具
5.1 拿到源码后先看这三个关键文件,别急着点启动
很多学员下载源码后,第一件事就是打开 IDEA 点运行,结果控制台飘红一片,然后就慌了。其实冷静下来,先从三个文件看起,能少走很多弯路。
第一个要看的是后端 src/main/resources/application.yml 或 application-dev.yml。重点看数据源配置:
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/pet_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
这里的数据库名称、用户名、密码必须改成你自己本地的实际值。MySQL 中要先建好一个同名的空数据库,比如 pet_shop,然后导入源码里附带的 SQL 脚本。
第二个要看的是源码目录里有没有 sql 或 doc 文件夹。一般会有一个像 pet_shop.sql 的文件,里面是建表语句和初始数据。不要手动去创建几十张表,直接找到这个 SQL 文件在 Navicat 或者命令行里执行即可。
第三个要看的是 pom.xml 文件里 Spring Boot 的版本号,因为版本问题直接影响驱动包兼容性。如果版本是 2.x,且 JDK 是 8 或 11,那大概率能顺畅启动。
5.2 数据库初始化时的常见坑:执行 SQL 报错怎么处理
导入 SQL 时最容易遇到的问题是编码问题。如果 SQL 脚本里包含中文数据,连接数据库的 URL 一定要带上 characterEncoding=utf8,导入时也把数据库字符集设置为 utf8mb4。否则商品分类、商品名称这些中文内容会直接变成乱码,小程序端显示的时候全是问号。
如果执行 SQL 报字段长度或外键相关的错误,很可能是 SQL 脚本是用高版本 MySQL 导出的,表定义里用了 utf8mb4_0900_ai_ci 之类的排序规则,而你的 MySQL 是 5.7 或更低版本不支持。最简单的处理方式是把排序规则统一替换成 utf8mb4_general_ci 或者 utf8_general_ci,再重新执行。
还有一个小细节,导入之前确认一下当前连接的数据库是你要导入的目标库,不要一不小心把表建到了系统库里,那样启动时后端连的库里面没有表,接口一访问就会报“Table doesn't exist”。
5.3 后端启动前,IDE 里需要确认的两个配置
在 IDEA 里导入 Spring Boot 工程后,不要急着点运行,先确认两件事。
第一,确认项目用的 JDK 版本和 Maven 仓库路径是否配置正确。如果 IDEA 里默认的 Project SDK 是 17,而源码要求 JDK 8,启动时容易遇到类版本错误。可以在 Project Structure 里把 SDK 切到 1.8,并在 Settings 里把 Maven 的 JDK for importer 也一并改掉。
第二,确认 Maven 依赖已经完整下载。第一次导入项目时,IDEA 会自动下载依赖,如果网络不稳定会有一些包下载失败。遇到启动类找不到符号之类的问题,先尝试在 Maven 面板里执行 clean 和 compile,把报错信息看清楚。大部分情况是因为某个依赖没有下载成功。
启动成功的标志是控制台出现 Spring Boot 的启动日志,并且能看到 Tomcat started on port(s): 8080 之类的信息。看到这行,说明后端服务器已经起来了。
5.4 小程序端导入与实际运行:开发者工具和 HBuilder X
源码里的小程序端一般是两种形态之一:原生微信小程序目录,或者从 HBuilder X 里创建的 uni-app 项目。
如果是原生微信小程序,直接在微信开发者工具里选择“导入项目”,把源码里的 miniprogram 或 pages 所在的目录选进去。导入后需要在小程序项目的 app.js 或 config.js 文件里配置后端接口地址。注意,本地调试的时候,如果你用真机预览,不能写 http://localhost:8080,因为手机访问的 localhost 是手机自己。你需要把后端接口地址改成电脑在局域网中的 IP,同时后端还要开启跨域访问,否则小程序请求会被浏览器同源策略拦住。
如果你是跑在电脑端的微信开发者工具里,地址可以填 http://127.0.0.1:8080。但是这里也有个前提条件,开发者工具默认会校验 HTTPS 和合法域名。本地调试时需要在微信开发者工具的“详情 - 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。
如果源码是用 HBuilder X 创建的 uni-app 项目,你需要先确认里面配置的小程序 AppID 是不是你自己的。很多源码自带的 AppID 是原作者申请的测试号,直接运行到微信开发者工具里时会提示项目不属于当前账号。这时候需要在 manifest.json 的“微信小程序配置”里换成自己的 AppID,或者选择一个测试号。
5.5 真机预览和接口联调时最容易被忽略的跨域处理
小程序真机预览时,如果请求一直失败,前端 console 有报错,大概率跟跨域有关。虽然小程序不像浏览器页面那样受同源策略的严格限制,但后端如果完全没有配置跨域头,一些环境下请求还是会异常。
Spring Boot 开启跨域的方式很简单,写一个配置类:
java复制@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);
}
}
如果是前后端分离的管理页面,这个配置几乎是必须的。很多本地联调不通的问题,就是后端少配了这层跨域支持。加上之后再试,一般就好了。
6. 实际开发和调试过程中总结的排错经验
6.1 后端接口返回 404 或 405 时先查什么
联调时发现接口 404,先别焦虑。第一查大小写,Spring Boot 的 @RequestMapping 路径是区分大小写的,/api/goods/list 和 /api/goods/List 完全是两个地址。第二查项目有没有加 context-path,如果配置文件里写了 server.servlet.context-path: /api,那实际接口地址是 http://localhost:8080/api/goods/list,小程序的 BASE_URL 里就不能再重复拼一个 /api。
接口返回 405,通常是请求方式和后端定义不一致。后端 @PostMapping 的接口你用 GET 请求去调,或者后端需要的请求头 Content-Type 格式跟小程序传的不匹配,就会报这个方法不允许。
日常联调时,我的习惯是先把接口文档或者后端 Controller 里的注解路径抄下来,在浏览器直接访问一个 GET 类型接口确认后端能通,再去小程序端排查。这样可以快速把问题缩小到“后端代码问题”还是“前端请求问题”。
6.2 登录之后访问业务接口提示未授权,怎么定位
这种现象在前后端分离项目中特别常见。用户在登录接口拿到了 token,但访问商品列表、购物车等业务接口时,后端拦截器返回未授权,或者压根没有返回用户信息。
定位的方式分三步。第一步,打开小程序调试面板的 Network 选项卡,查看业务接口的请求头里有没有 Authorization 字段。如果没有,说明前端请求封装里没有把 token 放到 header。第二步,如果请求头有 token,就要看后端拦截器有没有把登录接口排除在外。很多项目拦截器写成了一切请求都要校验,导致登录接口本身也被拦截,用户根本拿不到 token。第三步,如果拦截器已经放行登录接口,那问题多半出在 token 解析上,需要断点查一下从 header 里取出来的 token 值是不是为空,或者 token 里解析出的用户 ID 找不对。
6.3 微信开发者工具的页面白屏或数据加载失败
小程序页面白屏,最常见的原因是 JS 报错。这类问题在调试器 Console 面板会直接打出错误日志,不要只盯着页面看,要看 console。常见错误比如 Cannot read property 'xxx' of undefined,多半是后端返回的字段名和前端页面绑定的字段名不一致。比如后端返回 goodsImage,前端写成了 goodsImg,那数据自然显示不出来。
我建议小程序端列表类数据都做一层空数据兜底,比如:
html复制<view wx:if="{{goodsList.length > 0}}">
<!-- 渲染列表 -->
</view>
<view wx:else>
<view class="empty">暂无商品</view>
</view>
这样就算接口没返回数据,页面也不会白得特别难看,至少能提示用户当前状态。
6.4 商品图片不显示,多半不是代码问题
商品图片上传到服务器后,在小程序端不显示,这种事我遇到太多次了。后端返回的图片地址可能是相对路径,比如 /upload/goods/2024/xxx.jpg,小程序拿这个地址去请求,域名解析不出来就显示空白。
解决思路有两种。第一种,后端返回的图片字段直接拼成完整地址,例如 http://你的IP:8080/upload/goods/xxx.jpg。第二种,小程序端在拿到相对路径后自己拼接图片前缀:
javascript复制function formatImage(url) {
if (!url) return ''
if (url.startsWith('http')) return url
return `${BASE_URL}${url}`
}
这里还牵扯到一个问题:商品图片是存放在本地服务器还是使用对象存储。学习项目一般存在本地服务器就可以。你在管理后台上传图片时,会把文件存到后端某个磁盘目录,同时在数据库里记录相对路径。想要在网页或小程序里正确展示,就必须保证图片访问地址能通过公网或局域网访问到这台服务器。
6.5 源码版本过高,旧工程跑不了时的处理思路
热词里有“springboot版本太高”这个词,确实反映了很多人的痛点。我建议的处理思路不是硬着头皮升级到最新,而是让项目先跑在比较成熟的版本组合上。
如果源码是 Spring Boot 2.x,尽量保持 2.7.x 版本,同时把 MySQL 驱动换成 mysql-connector-j。如果源码本身就是 Spring Boot 3.x,那么 JDK 必须 17 以上,否则别想正常启动。还要检查是否有第三方依赖用了 javax 开头,比如一些旧版的文件上传工具包,这种在 3.x 下会直接编译失败,需要找对应的 jakarta 版本。
判断一个学习项目成败,有时候不是功能多华丽,而是依赖和环境干净。这也是为什么很多带源码的学习项目都乐意用 Spring Boot 2.7.x,同一个源码在绝大多数 Windows 电脑上能稳定跑起来。
7. 源码阅读顺序和后续扩展建议
如果这份源码已经在你机器上成功跑通了,我建议不要急着关掉,按照下面的顺序把核心链路再读一遍:先读数据库表结构,理解表之间的关系;再读登录流程,搞清楚 token 怎么生成和校验;然后读商品列表接口,从 Controller 一层层往下看到 Mapper;最后读下单接口,注意事务是怎么加的,库存是在哪一步扣的。
这个顺序是在还原一条完整的业务链路。读完之后你会对 Spring Boot 中 Controller、Service、Mapper 三层如何协作有特别直观的感受。
实际使用过程中,我觉得这个项目后续扩展空间也挺大。如果是我来扩展,我会优先补这几个方向:
一个是优惠券或积分抵扣。在订单表中增加一个优惠金额字段,下单时先计算满减或优惠券抵扣,会让业务更接近真实商城。
另一个是接入真实支付。学生项目一般不会真的接微信支付,会使用模拟支付来处理订单状态流转。如果之后想上线真实运营,支付回调通知地址、证书签名这些内容还是要花不少精力去学习。需要办理好商户号之后再进行二次开发。
再有就是商品 SKU 的扩展。当你想把同一款猫粮的“幼猫版”和“成猫版”分开管理库存时,现有商品表结构会支撑不住。增加一个商品项规格表,购物车项增加 sku_id 字段,订单明细也把 sku_id 带下来,整个体系才算完整。
至于“源码86805”这个编号,我猜是发布源码时打的内部索引号,不影响你阅读和使用。你只需要关注工程内的代码结构和说明文档即可。按源码走的路径把项目打开,动手去改几个功能,比单纯看十个项目的效果都要好。
