最近我把一套宠物用品销售小程序的源码完整过了一遍,后端用的是Spring Boot,前端是微信原生小程序,整体跑下来最大感受是:它不只是一个简单的增删改查demo,而是把微信登录、商品SKU、购物车、订单状态流转、支付回调、后台管理这些商城必备链路都串起来了。这篇就把整套项目的选型逻辑、核心代码思路、跑通部署时的坑,结合源码工程里实际用到的技术点完整梳理一遍。无论是准备做校园项目、积累全栈经验,还是真打算做宠物类目小程序的运营者,这套东西的参考价值都比想象中大。
1. 项目整体设计与技术选型思路
1.1 为什么是Spring Boot + 微信小程序这套组合
宠物用品销售这个场景有个特点:用户决策链路短、复购率高、需要快速触达。选微信小程序当客户端,核心原因是用户不用装App,扫一扫或者从聊天记录里点一下就能进店。搭配“附近的小程序”这类微信生态流量入口,对中小型宠物用品商家来说获客成本会低很多。小程序端我建议用原生开发,不要一上来就套Uniapp或者Taro,原因后面实操部分会说。
服务端用Spring Boot在这个项目里几乎是“标准答案”。按源码里的依赖来看,Spring Boot负责提供RESTful接口,内置Tomcat省去单独配服务器的步骤;持久层用的MyBatis-Plus,单表CRUD基本不用手写SQL,可以把主要精力放在订单、库存这些核心业务上。这种组合的另一个好处是资料极其丰富——你遇到的90%问题,搜一下“Spring Boot + 微信小程序”基本都有现成方案,对新手特别友好。
1.2 核心业务模块与数据流
整套系统拆开看,可以分成三个端:用户端小程序、商家管理后台、Spring Boot服务端。源码工程里一般对应三个子项目或者同项目多个module。
用户端核心流程是:用户打开小程序 -> 微信授权登录 -> 浏览首页轮播和分类 -> 进入商品详情 -> 加入购物车 -> 提交订单 -> 微信支付 -> 查看订单状态。管理后台负责商品上架、分类管理、订单发货、轮播图配置这些运营操作。
这里最需要注意的是数据流的方向:小程序不直接连数据库,所有操作都走后端接口。举个例子,用户在小程序里点击“加入购物车”,前端只是把 goodsId、count、token 通过POST请求发到 /api/cart/add,后端校验登录态后才会真正操作购物车表。这种前后端分离的设计让业务逻辑集中在服务端,小程序端只做展示和交互,后续如果要增加App或者H5商城,后端接口可以直接复用。
1.3 源码工程目录结构说明
拿到源码后先别急着点运行,把目录结构摸清楚再动手。常见结构如下:
text复制pet-shop-server
├── src/main/java/com/petshop
│ ├── controller # 接口层,接收前端请求
│ ├── service # 业务逻辑层
│ ├── mapper # MyBatis-Plus数据访问层
│ ├── entity # 数据库实体类
│ ├── common # 公共返回体、异常处理、工具类
│ ├── config # 配置类(拦截器、静态资源映射)
│ └── PetShopApplication.java
├── src/main/resources
│ ├── application.yml # 数据源、Redis、微信配置
│ └── mapper # XML文件(复杂SQL场景才需要)
└── sql
└── pet_shop.sql # 数据库初始化脚本
小程序端目录:
text复制pet-shop-miniapp
├── pages
│ ├── index # 首页
│ ├── category # 分类
│ ├── goods # 商品详情
│ ├── cart # 购物车
│ ├── order # 订单确认/列表
│ ├── user # 个人中心
│ └── login # 登录页
├── utils
│ └── request.js # 封装wx.request,统一处理token
├── app.js # 全局逻辑,启动时检查登录态
└── app.json # 页面路由和tabBar配置
这种目录划分是国内大部分中小型项目的标准形态,看懂了它,以后看其他开源项目也能很快上手。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析:几个必须搞懂的关键实现
2.1 微信登录与会话管理
登录是整套系统最容易被忽略但坑最多的环节。很多新手会直接调 wx.getUserProfile 拿用户头像昵称,然后拿这些信息当登录凭证——这是错的。微信官方推荐的方式是 wx.login 获取临时code,再把code发给后端,由后端调微信的 code2Session 接口换取 openid 和 session_key。openid是用户在你这套小程序体系里的唯一身份标识,后端只要存这个openid就能识别用户。
源码里登录接口的处理逻辑大致是:
java复制@PostMapping("/api/user/login")
public Result login(@RequestBody LoginRequest request) {
String url = "https://api.weixin.qq.com/sns/jscode2session"
+ "?appid=" + appId
+ "&secret=" + appSecret
+ "&js_code=" + request.getCode()
+ "&grant_type=authorization_code";
// 用RestTemplate或HttpClient调用微信接口
String openid = wechatClient.getOpenId(url);
User user = userService.findOrCreate(openid);
// 生成自定义token返回给前端,后续请求都带这个token
String token = jwtUtil.generateToken(user.getId());
return Result.success(token);
}
这里有两个关键点:第一,code是一次性的,有效期五分钟,不能重复使用;第二,后端拿到openid后不要直接返回给前端,而是生成一个自定义token(源码里一般用JWT,也有用Redis存session的方式),后续请求通过拦截器校验token。这样做的好处是前端永远接触不到openid,避免接口被恶意调用时泄露用户身份。
2.2 商品、购物车与库存
商城类项目最核心的数据表就三张:商品表、购物车表、库存表(或者商品表里直接带库存字段)。这套源码里商品支持多规格,所以实际会有 goods 和 goods_sku 两张表,SKU表里存 price、stock、spec 这些字段。
购物车设计上有一个值得注意的点:是把购物车数据存在小程序本地(wx.setStorageSync),还是存在后端数据库?源码选择的是后端存储。理由是:宠物用品通常不是冲动消费,用户可能今天加购明天才下单,存在后端才能保证换手机、清缓存后购物车还在。另外后端存储还能在后台上看到用户加购数据,为后续做“加购未下单”的定向营销留了空间。
库存扣减是这里最容易踩坑的地方。最基础的写法是“先查库存够不够,够就update”,但在高并发下会超卖。真实项目里应该用带条件的UPDATE语句:
sql复制UPDATE goods_sku
SET stock = stock - #{count}
WHERE id = #{skuId} AND stock >= #{count}
这种写法数据库层面就保证了原子性,如果受影响行数为0说明库存不足,直接返回“库存不足”提示即可。这套源码虽然单机部署时并发不高,但代码里已经用了这种方式,这一点值得学习。
2.3 订单状态机与支付回调
订单状态是整个商城最复杂的业务链路之一。源码里的订单状态设计为:待付款(0)、待发货(1)、待收货(2)、已完成(3)、已取消(4)、退款中(5)。这些状态在数据库里用tinyint存数字,前端通过枚举映射成中文文案。
状态流转是单向的,不能乱跳:待付款只能变成待发货(付款成功)或已取消(超时未付);待发货只能变成待收货(商家发货);待收货只能变成已完成(用户确认)或退款中。源码里不是到处写if-else判断,而是用一个状态机工具类集中管理,后续加“售后完成”之类的状态也方便扩展。
支付回调是另一个重点。微信支付成功后,微信服务器会异步调用你配置的回调地址,把支付结果POST过来。这个接口有几个必须注意的点:
- 回调地址必须是HTTPS且公网可访问,本地调试可以先用内网穿透工具
- 必须校验微信签名,防止伪造回调
- 收到回调后要幂等处理——同一笔订单可能收到多次回调,不能用一次回调就硬插入“已支付”记录,要先查订单状态,已是待发货就直接返回成功
源码里回调接口大致长这样:
java复制@PostMapping("/api/pay/notify")
public String payNotify(@RequestBody String xmlData) {
// 1. 解析微信返回的XML,校验签名
// 2. 取出out_trade_no(订单号)和transaction_id(微信流水号)
// 3. 更新订单状态为待发货,记录流水号
// 4. 返回"<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>"
}
如果不是做真实支付,只是本地测试,建议在支付接口里预留一个mock开关:开关打开时直接模拟支付成功,返回成功结果给前端,这样就能不依赖商户号把整个下单流程跑通。
3. 实操过程与核心环节实现
3.1 环境准备与初始化
要跑起这套源码,按下面的环境清单准备就行:
| 依赖项 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8或11 | Spring Boot 2.x用这两个版本最稳 |
| Maven | 3.6+ | 管理后端依赖 |
| MySQL | 5.7或8.0 | 导入 pet_shop.sql 初始化脚本 |
| Redis | 5.x/6.x | 如果源码用Redis做会话或缓存,需要启动 |
| 微信开发者工具 | 最新稳定版 | 运行小程序端 |
| 内网穿透工具 | 花生壳/cpolar | 本地调试支付回调时需要 |
后端启动前,重点检查 application.yml 里这几个配置项:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/pet_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
redis:
host: localhost
port: 6379
wechat:
appid: wx你的AppId
secret: 你的AppSecret
其中 wechat.appid 和 wechat.secret 需要去微信公众平台申请小程序账号后,在“开发管理 -> 开发设置”里找。项目里如果直接用源码自带的测试值,登录接口会报 invalid appid 错误,这个是正常现象,换成自己的就行。
小程序端需要在 app.js 里修改后端接口地址:
javascript复制globalData: {
baseUrl: 'http://localhost:8080' // 改成你的后端地址
}
微信开发者工具里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”,才能在本地用http调试。这一步很多人会漏掉,结果页面一直请求失败,其实不是代码问题,是工具配置问题。
3.2 数据库设计要点
源码自带的SQL脚本是整个项目的地基,我建议不要直接一键导入就完事,而是花十分钟把表结构过一遍。核心表大概有这些:
| 表名 | 说明 | 关键字段 |
|---|---|---|
user |
用户表 | id, openid, nickname, avatar |
goods |
商品表 | id, name, category_id, main_image, detail |
goods_sku |
商品规格表 | id, goods_id, spec, price, stock |
cart |
购物车表 | id, user_id, goods_id, sku_id, count, selected |
order |
订单表 | id, order_no, user_id, total_price, status, address_id |
order_item |
订单明细表 | id, order_id, goods_id, sku_id, price, count |
address |
收货地址表 | id, user_id, name, phone, province, city, detail |
banner |
轮播图表 | id, image_url, link_url, sort |
category |
商品分类表 | id, name, sort |
订单和订单明细为什么要拆两张表?因为一个订单可能包含多个商品,如果只放在一张表里,要么出现大量冗余字段,要么没法表达“一单多品”的关系。拆开之后,订单表存的是订单级别的信息(总价、状态、收货人),订单明细表存每个商品的信息(名称快照、单价、数量)。注意“名称快照”这个词——商品名称和价格在订单明细里必须冗余一份,不能通过商品ID去关联查,否则商品改名或者调价后,历史订单显示的数据就变了。
金额字段建议用 decimal(10,2),不要用float或double,否则计算总价时可能出现0.1+0.2=0.30000000000000004的精度问题。
3.3 后端核心接口实现
源码里接口数量不少,但真正核心的就三类:登录接口、加购接口、下单接口。
下单接口是整个系统里逻辑最重的一个,它需要在一个事务里完成:
- 校验用户登录态
- 校验收货地址
- 从购物车取出勾选的商品,计算总价
- 扣减对应SKU的库存
- 生成订单主记录和订单明细记录
- 清空已下单的购物车项
核心代码思路:
java复制@Transactional(rollbackFor = Exception.class)
public Order createOrder(Long userId, Long addressId) {
// 1. 校验地址
Address addr = addressMapper.selectById(addressId);
if (addr == null) throw new ApiException("收货地址不存在");
// 2. 查出购物车中selected=1的条目
List<Cart> cartList = cartMapper.selectBySelected(userId);
// 3. 计算总价并扣库存
BigDecimal total = BigDecimal.ZERO;
List<OrderItem> itemList = new ArrayList<>();
for (Cart cart : cartList) {
GoodsSku sku = goodsSkuMapper.selectById(cart.getSkuId());
int rows = goodsSkuMapper.deductStock(sku.getId(), cart.getCount());
if (rows == 0) {
throw new ApiException("商品[" + sku.getSpec() + "]库存不足");
}
total = total.add(sku.getPrice().multiply(BigDecimal.valueOf(cart.getCount())));
OrderItem item = new OrderItem();
item.setSkuId(sku.getId());
item.setGoodsName(sku.getGoodsName()); // 快照
item.setPrice(sku.getPrice()); // 快照
item.setCount(cart.getCount());
itemList.add(item);
}
// 4. 生成订单
Order order = new Order();
order.setOrderNo(generateOrderNo()); // 格式如:202501011200001234
order.setUserId(userId);
order.setTotalPrice(total);
order.setStatus(0); // 待付款
orderMapper.insert(order);
// 5. 插入订单明细
for (OrderItem item : itemList) {
item.setOrderId(order.getId());
orderItemMapper.insert(item);
}
// 6. 清空已下单购物车
cartMapper.deleteSelected(userId);
return order;
}
这里的 deductStock 就是前面提到的带条件UPDATE,这是防止超卖的关键。@Transactional 注解保证整个方法里任何一个环节出错,库存扣减和订单生成一起回滚,不会出现“扣了库存没生成订单”的脏数据。
3.4 小程序端请求封装与页面联调
小程序端最重要的文件是 utils/request.js,所有接口请求都应该走这个封装,不要在业务页面里直接调 wx.request。封装的核心逻辑是:
javascript复制const request = (url, method = 'GET', data = {}) => {
return new Promise((resolve, reject) => {
wx.request({
url: getApp().globalData.baseUrl + url,
method: method,
data: data,
header: {
'Content-Type': 'application/json',
'token': wx.getStorageSync('token') // 登录后存下的token
},
success: (res) => {
// 如果返回码是401,说明token过期,跳转登录页
if (res.data.code === 401) {
wx.removeStorageSync('token');
wx.navigateTo({ url: '/pages/login/login' });
reject(res.data);
return;
}
// 其他业务错误统一提示
if (res.data.code !== 200) {
wx.showToast({ title: res.data.message, icon: 'none' });
reject(res.data);
return;
}
resolve(res.data);
},
fail: (err) => reject(err)
});
});
};
module.exports = { request };
小程序原生框架的页面生命周期非常简单直接。以首页为例,onLoad 时调用商品列表接口,拿到数据后通过 setData 更新视图:
javascript复制onLoad() {
request('/api/goods/list', 'GET').then(res => {
this.setData({ goodsList: res.data.records });
});
}
调用接口给用户反馈,下拉刷新用 onPullDownRefresh 处理,触底加载更多用 onReachBottom。这套原生开发模式对熟悉Vue或者React的人来说上手成本很低,而且不需要额外编译,开发者工具里保存代码就能看到效果,调试效率很高。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
我把实际跑这个项目时踩过和见过的坑整理成一张表,按“现象 -> 原因 -> 解决”的顺序排查很有效:
| 现象 | 原因 | 解决方法 |
|---|---|---|
登录接口报 invalid appid |
application.yml里还是源码自带的测试appid |
换成自己在微信公众平台申请的小程序AppID和Secret |
| 小程序请求后端一直失败 | 开发者工具默认校验合法域名 | 本地调试勾选“不校验合法域名”;上线前配合法域名并启用HTTPS |
| 支付回调收不到 | 回调地址是内网地址,微信服务器无法访问 | 使用内网穿透工具暴露公网地址,或直接部署到服务器 |
| 下单提示库存不足 | SKU的 stock 字段很小且被前一个订单扣完了 |
在数据库测试环境把库存调大,或者添加测试商品时多填库存 |
| 图片加载404 | 后端返回的是本地相对路径,小程序访问不到 | 配置静态资源映射,或者图片上传到OSS/CDN返回完整URL |
| token过期后所有请求都失败 | 前端没有统一处理401 | 参照上面的 request.js 封装,拦截401跳转登录页 |
| 支付成功但订单状态没变 | 回调验签失败或回调地址没配 | 先看后端日志,确认回调是否到达;再检查签名校验逻辑 |
4.2 三个必须提前处理的“隐形坑”
第一个是订单号的生成。支付回调时需要根据订单号找到对应订单,如果订单号直接用数据库自增ID,很容易被遍历猜测。源码里一般会用时间戳+随机数生成一个比较长的订单号,比如 202501011200001234,这个长度不会超出BigInteger范围,但用字符串存更安全。
第二个是微信支付回调的幂等性。这个问题在本地测试时基本碰不到,一旦真实上线就会遇到:用户支付成功后,微信服务器为了提高送达率会重试多次回调。如果每次回调都执行“把订单改成已支付”,虽然看起来结果一样,但如果回调里还包含“给用户加积分”“推送发货通知”之类的动作,就会重复执行。正确做法是先判断订单状态,已经是待发货就说明处理过了,直接返回成功给微信。
第三个是数据库时间时区问题。开发机的MySQL时区一般和服务器不一致,如果URL里不指定 serverTimezone=Asia/Shanghai,订单创建时间、支付时间会出现差8小时的情况。这个我在第一张表里已经写了,但还是要强调:不是加上就行,要确保数据库本身的时区也是正确的,否则可能白加。
另外一个容易被忽略的点:小程序端的请求 header 里,token 字段名前后端必须完全一致。源码里如果后端拦截器用 request.getHeader("token") 获取,你就不能在请求里写 Authorization。这种问题报错信息往往很迷惑——后端日志显示“未登录”,前端一脸懵,排查半天发现是字段名大小写或者名称不统一。
5. 上线部署与二次开发建议
5.1 部署到服务器的关键步骤
本地跑通只是第一步,真正做小程序商城还是得部署到服务器。流程并不复杂,但有几个坑提前知道能省不少时间。
后端打包部署:
bash复制mvn clean package -Dmaven.test.skip=true
java -jar target/pet-shop.jar --spring.profiles.active=prod
打包时如果遇到 spring-boot-maven-plugin 版本和JDK不匹配的问题,把pom里插件版本降到和Spring Boot版本一致就行。生产环境建议用 nohup 后台运行,并配合systemd管理服务,避免进程意外退出没人管。
小程序前端上线前必须做三件事:第一,app.js 里把baseUrl从 http://localhost:8080 改成正式服务器域名;第二,登录微信公众平台,在“开发管理 -> 开发设置 -> 服务器域名”里配置request合法域名,必须是HTTPS;第三,确认服务器上已经部署了SSL证书,没有证书的话微信请求会被拦下来。
HTTPS这块多说一句:很多学生项目第一次上线都卡在这里。不要自己去生成自签名证书,建议直接用云厂商提供的免费证书,比如阿里云、腾讯云都有一年期免费SSL证书。配置到Nginx后,后端接口地址就变成 https://api.yourdomain.com,小程序端把这个地址配置进去就能正常请求了。
5.2 可以扩展的方向
如果需要把这套系统做成真正能运营的项目,下面几个方向可以结合源码逐步做:
- 优惠券和满减活动:在订单确认页加优惠券选择,后端在计算总价时做抵扣。数据库加
coupon和user_coupon两张表即可 - 宠物档案管理:宠物用品复购率高,可以增加宠物档案功能,记录宠物的品种、年龄、体重,做个性化商品推荐
- 会员积分体系:下单送积分、签到领积分,积分可抵现。这类功能对用户粘性提升很直接
- 后台数据统计:管理端增加销售报表,按天/周/月查看GMV、订单量、热门商品top10
功能扩展时要守住一条原则:先动数据库,再写接口,最后改页面。不要先想着改前端样式,把核心业务链路搞清楚了,扩展功能只是往既有框架里填充。
我实际整理这套源码时,最大的体会是:商城类项目看着模块多,其实核心骨架就那么几条线——登录、选品、下单、支付、履约。把这五条线在数据库和接口层面理清楚,剩下的都是顺着需求往上加功能。如果你拿到的也是类似的宠物用品商城源码,建议先别急着改页面,先把订单状态和库存扣减这两处吃透,因为这两个地方最容易藏坑,也最能体现一个开发者的基本功。
最后分享一个调试小技巧:把微信开发者工具的“不校验合法域名”选项打开,本地联调会省很多事,但项目真正提审之前一定要关掉,并且把request域名都配到小程序后台,否则会直接审核不通过。
