简介:微信小程序商城完整源码是一套可直接运行的电商小程序项目,面向需要搭建微信商城或学习小程序开发的读者,覆盖商品展示、购物车、订单管理、支付等核心流程,适合从入门到进阶的小程序开发者参考使用。资源共60个文件,压缩包仅412KB,以WXML页面结构、WXSS样式、JS业务逻辑、JSON配置和PNG图片素材为主,并附带一份docx源码说明文档;目录包含pages、components、utils、models等模块,页面结构清晰,方便按模块查阅。目前已有7720人学习下载。通过该源码既能快速搭建可用的商城基础框架,也能深入理解全局配置、页面生命周期、网络请求与支付对接等实现细节,帮助开发者在实际项目中少走弯路。 我做小程序开发这些年,商城类项目是接得最多的一种需求。而“微信小程序商城完整源码”这类搜索词的热度一直居高不下,说明大家都在找一个能直接跑起来、结构清晰的参照系,而不是那些下载下来就报错、缺配置、前后端对不上的半成品。
这篇文章就围绕我从零搭建的一套小程序商城项目来拆解。整套源码包含用户登录、商品浏览、购物车、下单、微信支付v3、订单管理和售后流程,后端用的是Spring Boot,小程序端是原生框架,数据库走MySQL。目的是给正在做毕设、面试项目或刚入职需要接手商城开发的读者,提供一个可以直接借鉴的完整方案。
1. 整体架构与模块划分
1.1 为什么选前后端分离
小程序商城和传统网页商城最大的区别在于运行环境。小程序有自己的生命周期、网络请求白名单和登录态体系,天然适合前后端分离的结构。前端只负责页面渲染和交互,所有业务逻辑、权限判断都收敛到后端接口,这样小程序包体积小、加载速度快,也方便后续如果要出App端、H5端,接口可以直接复用。
我在这套源码里把后端拆成了五个核心模块:认证模块(登录态、Token签发)、商品模块(SPU/SKU管理、分类、搜索)、交易模块(购物车、订单、支付、退款)、营销模块(优惠券、限时折扣)、用户模块(收货地址、个人资料、积分)。模块之间通过统一的API层通信,互相不直接调用对方的Mapper,这样后期要扩展分销、秒杀之类的功能时不会牵一发动全身。
1.2 数据库表设计的几个关键点
商城最核心的几张表是:商品表(spu)、规格表(sku)、购物车表(cart)、订单表(orders)、订单明细表(order_item)、支付流水表(pay_log)。我的设计原则有两个:第一是SKU独立成表,不把规格塞进商品表的JSON字段里,因为后面要做库存扣减和价格计算,JSON字段查起来非常痛苦;第二是金额字段统一用分存储,用int类型而不是decimal的元,这样可以避免浮点运算的精度问题,也符合微信支付对接时的单位要求。
订单状态我用的是状态机,字段名叫order_status,取值范围是:待支付(0)、已支付待发货(1)、已发货待收货(2)、已完成(3)、已关闭(4)、退款中(5)、已退款(6)。每次状态流转都必须通过订单服务层的方法来操作,不允许在Controller里直接改状态,这个约束能避免很多因为状态跳变导致的资损问题。
1.3 源码目录结构与关键文件说明
拿到源码之后可以先按这个结构快速找到对应功能的实现位置:
后端结构:
src/main/java/com/mall/ ├── controller/ // 接口层,只做参数接收和数据返回 ├── service/ // 业务逻辑层,核心代码都在这 ├── mapper/ // MyBatis数据访问层 ├── config/ // 微信配置、拦截器、线程池配置 ├── common/ // 统一返回体、异常处理、工具类 └── model/ // 实体类和DTO/VO小程序端结构:
pages/ ├── index/ // 首页 ├── category/ // 分类页 ├── cart/ // 购物车 ├── goods/ // 商品详情 ├── order/ // 订单确认和列表 ├── pay/ // 支付结果 ├── user/ // 个人中心 └── address/ // 收货地址管理小程序的网络请求我统一封装在utils/request.js里,所有请求都会自动带上Token,并且做了HTTP状态码和业务码的双重判断。这套封装逻辑在一开始就写好的话,后面调试支付、订单这些核心流程会省非常多的时间。
2. 商品浏览与购物车的核心逻辑
2.1 商品列表的加载方式
首页和分类页的商品列表,我一开始是用传统的分页方式做的,page和size两个参数传给后端。后来发现小程序的用户习惯是不断往下滑,所以改成了游标分页,用lastId来定位位置,这样即使用户在短时间内快速翻页,也不会出现重复数据的问题。
商品详情页比较麻烦的地方在于SKU的选择。用户需要先选规格(比如颜色、尺码),然后前端展示对应的价格和库存。我这里的实现是:后端一次性返回商品下所有SKU的完整数据,前端在内存里做规格匹配,不需要每次切换规格都请求一次接口。这个方案在处理几十个SKU的商品时性能足够,小型商城不需要上SKU树结构那些复杂算法。
2.2 购物车的计算链路
购物车有个所有人都会踩的坑:前端自己计算合计金额。小程序端改一下商品数量,前端就本地算一个总价,传到后端去下单,这种情况下如果后端不重新校验,用户完全可以通过篡改请求参数来减价。我的处理方式是:购物车接口返回的每一项里都带上最新单价,前端展示用,下单时后端必须根据购物车ID重新从数据库查询价格和库存,以服务端计算结果为准。
还有一条经验是购物车要区分“选中”和“全选”的状态存储。我一开始把选中状态只存在前端storage里,后来发现用户换设备后购物车还在,选中项却丢了。调整方案是把选中状态同步到后端,购物车表里加一个checked字段,这样多端同步就不会有问题。
2.3 订单确认页的价格组成
订单确认页的价格组成看起来简单,实际上容易乱:商品金额、运费、优惠券抵扣。运费的计算规则我是单独抽了一个FreightCalculator类来处理,规则是:满99元包邮,不满则收8元,特殊商品(比如大件家电)在商品表里单独用freight_type字段标识为不参与包邮。
优惠券的设计稍微复杂一点。优惠券需要区分满减券、折扣券和免邮券,每一种的使用条件判断逻辑都不一样,而且一张券只能用一次,必须保证并发下单时不会被重复使用。实现时我在数据库里给优惠券表的used_status字段加了乐观锁,更新时带上当前状态值,更新影响行数为0就说明已经被用了。
3. 微信支付v3对接全流程记录
3.1 商户平台配置准备
微信支付v3和v2最直观的区别就是:v3的API是RESTful风格,数据格式全部用JSON,而且API密钥体系升级成了证书 + 私钥双重验证。对接之前需要准备的配置有:商户号、商户API证书(p12或pem格式)、商户私钥、微信支付平台证书,以及APIv3密钥。其中APIv3密钥是在商户平台手动设置的32位密钥,用来解密回调数据。
这一步最容易出错的地方是证书序列号。v3签名时Header里需要带Wechatpay-Serial,很多新手会把API证书序列号和这个混在一起。记住一个规律:请求时带的是商户API证书的序列号,验签时用的平台证书只是用来解密和验签,不需要在请求头里带它。
相关配置我放在后端的application.yml里,只引用路径不写死绝对路径,方便不同环境切换:
wechat: pay: merchant-id: "你的商户号" api-v3-key: "你的APIv3密钥" private-key-path: "/cert/apiclient_key.pem" merchant-serial-number: "商户证书序列号" notify-url: "https://你的域名/api/pay/callback"3.2 服务端下单与小程序拉起收银台
支付的下单流程实际上有两步:第一步是后端调用微信支付的“小程序下单”接口,拿到prepay_id;第二步是后端用自己的私钥生成二次签名参数,返回给小程序的wx.requestPayment拉起收银台。很多人在第一步成功后就直接把prepay_id丢给前端,这是不对的,小程序端必须要第五个参数paySign,而这个签名必须由后端生成,不能由前端自己算。
后端下单核心代码的大致思路是:
// 构建请求参数,金额单位必须为分 Map<String, Object> body = new HashMap<>(); body.put("appid", appid); body.put("mchid", merchantId); body.put("description", "商城商品购买"); body.put("out_trade_no", orderNo); body.put("notify_url", notifyUrl); Map<String, Integer> amount = new HashMap<>(); amount.put("total", totalFee); // 单位是分,不是元 amount.put("currency", "CNY"); body.put("amount", amount); body.put("payer", Collections.singletonMap("openid", openid)); // 统一下单 String response = wxPayClient.post("/v3/pay/transactions/jsapi", body);拿到响应里的prepay_id之后,后端需要生成小程序端拉起支付所需的签名参数:appId、timeStamp、nonceStr、package(值为prepay_id=xxx),然后用商户私钥做SHA256签名,一并返回给前端。
小程序端拉起支付的代码就是标准的:
wx.requestPayment({ timeStamp: res.data.timeStamp, nonceStr: res.data.nonceStr, package: res.data.package, signType: 'RSA', paySign: res.data.paySign, success: () => { /* 支付成功跳转 */ }, fail: () => { /* 用户取消或支付失败 */ } });3.3 支付回调:这一步千万不能掉以轻心
支付回调是后端最核心的接口。这里有一个很关键的原则:回调接口不能把微信发过来的数据直接当信任数据用。必须做两件事:第一,用微信支付平台证书验证签名,确认这条通知确实是微信官方发来的;第二,使用APIv3密钥解密resource节点中的数据,获取真实的订单号和支付金额,再用这个解出来的金额和本地订单表的金额做一致性对比,金额对不上必须直接报错。
回调的处理逻辑我放在一个独立的服务里面,避免和Controller写在一起。处理完后必须给微信返回一个应答,格式固定为{"code":"SUCCESS","message":"成功"},如果业务处理失败,要返回非SUCCESS的状态,微信才会按照间隔策略继续重试。这里有一个很隐蔽的问题:如果回调处理耗时太长,超过5秒微信就会判定超时并重试,所以回调里不能做太多耗时操作,比如发短信通知用户之类的,应该丢到消息队列里异步处理。
3.4 支付返错排查
我在对接过程中遇到最多的错误码是SIGN_ERROR,基本上都是私钥加载不正确或者签名算法用错了。还有INVALID_REQUEST,这种一般是必填参数缺失,要重点检查openid是不是当前小程序下的,金额单位是不是传成了元。另外注意,v3接口的错误信息是藏在响应体里的,很多人在拦截器里直接把整个异常包装成了业务异常,根本看不到原始报错,调试阶段一定要把微信返回的完整错误信息打出来。
4. 接口安全与性能优化细节
4.1 Token机制的落地方式
小程序的登录态有一个特点,就是wx.login获取的code只能用一次,用于跟微信服务器换openid和session_key。我这套源码里每次登录都会用code交换openid,后端生成一个随机token并缓存到Redis里,有效期是2小时,token过期后前端会拦截401响应,自动用wx.login重新走一遍静默登录流程。
用户手机号解密这里也要提一句。小程序端获取手机号时的加密数据需要后端来解密,解密需要用会话密钥session_key,而且这个session_key不会长期有效,所以不要想存下来反复用。我的做法是不持久化session_key,每次需要解密时临时重新获取,确保拿到的session_key和当前code是对应的。
4.2 参数校验与敏感操作幂等
商城的接口有很多是敏感操作,比如下单、提交订单,这类接口必须做幂等处理。我采用了TokenIdempotent注解的方式,前端在提交订单时先在OrderTokenController里申请一个临时幂等令牌,后端把令牌存入Redis,过期时间5分钟,提交订单时校验并删除令牌。如果用户连点两次提交,第二次就会被拦截,从源头上解决了重复下单问题。
另外后端参数校验不要自己在Controller里if-else写一堆,用Spring的Validation注解配合全局异常处理器,能把大量重复代码收敛掉。所有金额类的参数,前端传上来之后后端都要重新判断一遍,不能信任前端的计算结果。
4.3 缓存与数据库优化
商品详情页是全站流量最高的接口,每次都查数据库肯定扛不住。我把首页Banner、分类导航、热门商品列表都加了一层Redis缓存,过期时间设置成10分钟,后端更新商品时通过@CacheEvict主动清缓存。这样做的效果非常明显,商品详情接口的响应时间从平均300ms降到了40ms左右。
数据库层面主要做了两件事:第一是给订单表的out_trade_no加了唯一索引,防止重复支付回调导致订单状态被覆盖;第二是给订单明细表冗余了商品名和商品快照图,这样订单历史记录就不会因为商品下架或改名而显示异常。
5. 上线阶段碰到的实际问题与排查记录
5.1 支付正常但订单状态不更新
上线第一天就遇到了一个典型的回调问题:用户能正常付款,微信支付后台也能看到交易记录,但小程序里的订单状态一直没有变成已支付。排查流程是先查看后端日志,发现回调接口根本没有收到微信的请求。继续排查之后发现,问题出在服务器安全组没有放行HTTPS端口,微信服务器连不上回调地址,自然就一直重试。
配置回调地址还有一个容易踩的坑:回调地址必须是外网能直接访问的HTTPS地址,不能用IP地址,而且域名需要ICP备案。如果只是本地调试,可以用内网穿透工具把自己的本机服务映射出去,但上线之前一定要换成正式域名,否则微信支付会在几次重试后直接关掉这笔订单的后续通知。
5.2 小程序端swiper与video组件的全屏冲突
商品详情的轮播图区域我用了swiper,后来运营说要在详情页里再嵌一个商品视频,结果出现了iOS端点击视频全屏后,关闭全屏时页面错位的情况。这个问题的根源是swiper组件和video组件在全屏切换时的层级计算有冲突,属于原生组件在WebView渲染时的历史遗留问题。
当时我试过两种方案。第一种是把video改成在弹层里播放,全屏时用cover-view覆盖,效果可以但交互上多了一步。第二种方案更省事:不在swiper里嵌套video,把视频移到详情页独立模块,同时给video设置了enable-progress-gesture和show-center-play-btn,再配合page-orientation设置,全屏退出后的错位问题就消失了。如果你必须把视频放进swiper,至少要在bindfullscreenchange里手动处理一下当前页面的滚动高度。
5.3 软键盘遮挡底部查询按钮
商品搜索页里,因为商标的筛选区在页面底部,iOS端唤起软键盘时经常会出现键盘区域刚好压住查询按钮的问题。小程序里给输入框都加了adjust-position="true",但文本输入框聚焦时页面只是整体往上推,底部区域还是会被遮住。
一个比较稳的处理是在输入框聚焦和失焦事件里,手动调整底部按钮的位置。具体做法是用wx.onKeyboardHeightChange监听键盘高度,然后把按钮的bottom值动态设置成键盘高度加10像素。Android端键盘行为有时候不触发这个事件,所以还要在失焦事件里把bottom重置回初始值。这个补丁很小,但直接影响用户能不能顺利点到按钮,属于上线前必须处理的那类问题。
5.4 微信支付v3的常见报错速查表
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 下单报SIGN_ERROR | 商户私钥加载错误或签名串格式不对 | 检查私钥文件格式,确认签名串拼接顺序 |
| 回调验签失败 | 平台证书版本过期或证书序列号取错 | 下载最新平台证书并更新证书序列号 |
| 拉起收银台报invalid signature | 前端paySign生成算法错误 | 确认后端签名字段拼接方式与官方规则一致 |
| 用户支付成功但订单未更新 | 回调地址不可达或业务逻辑异常 | 查后端日志确认是否收到回调,检查接口幂等 |
| 金额单位错误导致报错 | 把元当分传给了微信 | 所有金额统一转换为分再传输 |
| 退款时报PARAM_ERROR | 证书加密数据解密失败 | 确认使用APIv3密钥解商户证书私钥加密数据 |
写在最后
踩坑踩得多了之后,我自己最大的体会是:商城项目的核心不在于页面多好看、功能多丰富,而是在订单和支付这条主链路上不能有任何逻辑漏洞。支付回调、幂等控制、金额校验这些模块,无论项目多小都要认真对待,因为一旦出了资金安全问题,代价不是改几行代码能弥补的。最后再分享一个小技巧:做支付联调的时候,微信支付后台有沙箱环境可以模拟支付成功、支付失败、退款成功等各种情况,不要直接拿生产环境反复测试,先把沙箱链路跑通再切正式参数,会省掉非常多的麻烦。
本文还有配套的精品资源,点击获取