微信小程序云商城源码解析:PHP后端接口与微信支付集成

发布时间:2026/9/15 4:43:01
微信小程序云商城源码解析:PHP后端接口与微信支付集成 简介一份微信小程序云商城前后端完整案例源码配套PHP后端接口适合想从零掌握小程序电商开发、或希望了解前后端协作的开发者。资源共3532个文件约20.34MB核心由1394个JS、683个HTML、381个PHP、321个CSS及321张PNG等组成涵盖小程序页面逻辑、后端API、后台管理模板与静态资源其中LESS、JSON、WXML/WXSS等文件则对应样式预编译、配置与小程序结构目录划分清晰便于按模块阅读。已有238人学习浏览可用作课程设计或毕业设计参考。通过阅读可系统学习商品分类、购物车、订单流程、用户登录、支付回调等实现方式同时理解PHP与MySQL的接口设计、RESTful API约定及微信支付安全处理附带的AdminLTE/bootstrap后台界面也为快速搭建管理端提供了可复用的模板。1. 云商城小程序源码包真正值得读的是 PHP 后端那几条接口拿到这套“微信小程序 PHP 后端”的云商城源码包先别急着只盯着小程序页面。解压后能看到 bootstrap.css、bootstrap.min.css、AdminLTE.css、AdminLTE.min.css 这一组后台资源说明它并不是一个单纯输出 JSON 的 API 项目而是连 MySQL 后端的 PHP 管理端也一并打了包。前端小程序负责浏览、加购、支付动作PHP 后端提供接口并承担订单、商品的持久化两者合在一起才算一个完整的前后端分离项目实战。对刚接触商城类小程序的开发者这套包能同时看到 WXML 页面组织和 PHP 控制器的对应关系对准备把老 PHP 项目改造成小程序数据源的人来说更值得关注的是接口路径、用户识别方式和回调验签这三个细节。下面按拆包顺序来看先处理小程序请求层再映射 PHP 路由然后落地微信支付最后用回调脚本把整套链路验证到闭环。2. 小程序网络层改造从 WXML 到 wx.request 数据流2.1 先看资源包结构AdminLTE 与 bootstrap 暗示了后端形态解压 zip 之后第一件事不是去找 app.json而是先看静态资源里有什么。看到 AdminLTE.css、AdminLTE.min.css 和 bootstrap.css、bootstrap.min.css 这一组文件基本可以确定后端是带管理界面的 PHP 站点而不是纯 API。AdminLTE 是后台管理模板bootstrap 是它的基础样式这意味着资源包里既有小程序端页面又带一套商品管理后台页面。这个结构对应到业务上非常常见小程序用户端与 PHP 管理端共用同一个数据库接口单独暴露给小程序调用。既然拿到的是一整套案例源码就不要把小程序端和管理后台割裂开理解。小程序端执行浏览、下单、支付等动作PHP 后端一方面提供这些动作依赖的 JSON 接口另一方面通过 AdminLTE 页面完成商品上架、订单发货。下面用一张表把典型模块与 PHP 接口的对应关系列出来功能模块小程序端动作PHP 端接口AdminLTE 管理端商品列表、详情goods/list、goods/detail添加、上下架商品购物车加入、修改数量cart/add、cart/update不涉及订单提交、支付、取消order/create、order/pay、order/cancel订单列表、发货用户登录、收货地址user/login、user/address用户查询这组接口路径是这类 PHP 商城案例最常见的设计不一定与本包内完全一致但管理端和小程序端共用控制器层、RESTful 风格命名的思路是通用的。拿到包后先找到index.php里路由定义就能找到等价的映射关系。2.2 统一 request 封装把 token 注入和返回码归一小程序页面一旦变多最糟糕的写法是每个页面自己直接调 wx.request让成功和失败回调散落在几十个文件里。我接手这类案例时会先抽出utils/request.js统一管三件事基础域名、token 注入、返回码判断。这样后面接支付、加统一鉴权只需要改一个文件。// utils/request.js —— 云商城小程序统一请求层 const BASE_URL https://your-domain.com/index.php function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}?r${path}, method: method.toUpperCase(), data, header: { content-type: application/json, X-Auth-Token: wx.getStorageSync(token) || }, success(res) { // PHP 接口通常返回 { code: 0, msg: , data: [] } if (res.data res.data.code 0) { resolve(res.data.data) } else { wx.showToast({ title: (res.data res.data.msg) || 请求失败, icon: none }) reject(res.data) } }, fail(err) { reject(err) } }) }) } module.exports { request }这段代码里最值得注意的细节是 URL 拼法http://域名/index.php?rgoods/list。不少老 PHP 项目没有配置 nginx rewrite直接用 pathinfo 形式访问容易 404?r前置控制器风格在老商城源码里非常常见。X-Auth-Token是登录后由 PHP 下发的业务 token小程序端存入 Storage每次请求带上它服务端从$_SERVER[HTTP_X_AUTH_TOKEN]就能读取当前用户。method.toUpperCase()是防止外层传入小写导致部分请求异常。2.3 商品列表的分页请求避免滚动重复加载商城首页逃不开商品列表而且一旦涉及上拉加载分页逻辑就必须一次做对。onReachBottom触发频率很高如果不加 loading 保护用户快速滚动时同一页会被请求好几次。// pages/goods/list.js const { request } require(../../utils/request) Page({ data: { list: [], page: 1, size: 10, hasMore: true, loading: false }, onLoad() { this.fetchGoods() }, fetchGoods() { if (this.data.loading || !this.data.hasMore) return this.setData({ loading: true }) request(goods/list, GET, { page: this.data.page, size: this.data.size }) .then((res) { this.setData({ list: this.data.list.concat(res.list), hasMore: res.list.length this.data.size, page: this.data.page 1, loading: false }) }) .catch(() this.setData({ loading: false })) }, onReachBottom() { this.fetchGoods() } })这里用concat而不是整段替换是因为上拉加载是在原有列表基础上追加。hasMore的判断依据是本次返回条数是否等于请求的 size相等就认为可能还有下一页。loading标志位是为了防止并发重复请求。新手容易漏掉的一点是前端传的 size 后端必须做上限限制。如果不限制用户把 size 改成 99999一次请求就拉走全表数据对 PHP 接口和 MySQL 的压力都很大。下一章服务端实现里我会把这个边界补上。3. PHP 后端路由控制器、数据表映射与管理后台联动3.1 单入口路由把 API 与 AdminLTE 页面串起来打开案例的index.php第一件要确认的是路由分发方式。典型实现是一个单入口文件同时决定请求是 API 还是后台页面用?r参数做控制器与动作的映射。// index.php —— 简化后的单入口路由 $r $_GET[r] ?? site/index; [$ctrlName, $action] array_pad(explode(/, $r), 2, index); $controllerMap [ goods GoodsController::class, order OrderController::class, user UserController::class, site SiteController::class, ]; if (!array_key_exists($ctrlName, $controllerMap)) { http_response_code(404); echo json_encode([code 404, msg invalid route]); return; } $controller new $controllerMap[$ctrlName](); $response $controller-{$action . Action}(); if (is_array($response)) { header(Content-Type: application/json); echo json_encode($response); } else { echo $response; // AdminLTE 页面模板输出 }这里把动作名拼上Action后缀是因为 PHP 控制器容易碰到list、print这类保留字或内置函数名加后缀可以避免命名冲突。返回值是数组就按 JSON 输出是字符串就当作 HTML 模板输出。这样index.php?rsite/index加载后台首页模板index.php?rgoods/list输出商品 JSON两件事在一个入口里分流完成。接口约定的返回结构统一为{ code, msg, data }业务正常时 code 为 0异常时用非 0 值小程序端 request 封装里做统一拦截。这个约定虽然简单但能避免小程序每个页面自己写一套错误处理逻辑。3.2 商品表设计与查询参数防滥用商城系统的表结构绕不开商品、订单、用户、购物车四张核心表。商品表最少量应覆盖标题、封面、价格、库存、销量与上下架状态下面这张建表语句可以直接用于本案例的本地复现CREATE TABLE shop_goods ( id int(11) unsigned NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL DEFAULT COMMENT 商品标题, cover varchar(500) NOT NULL DEFAULT COMMENT 主图地址, price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 售价, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, sales int(11) NOT NULL DEFAULT 0 COMMENT 销量, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT云商城商品表;对应商品列表接口服务端要对 page 和 size 做边界限制并用 PDO 预处理方式查询public function listAction() { $page max(1, (int) ($_GET[page] ?? 1)); $size min(50, max(1, (int) ($_GET[size] ?? 10))); $stmt $this-db-prepare( SELECT id, title, cover, price, sales FROM shop_goods WHERE status 1 ORDER BY id DESC LIMIT :offset, :size ); $stmt-bindValue(:offset, ($page - 1) * $size, PDO::PARAM_INT); $stmt-bindValue(:size, $size, PDO::PARAM_INT); $stmt-execute(); return [ code 0, data [ list $stmt-fetchAll(PDO::FETCH_ASSOC), page $page, size $size ] ]; }参数用bindValue绑定时必须带PDO::PARAM_INT避免整型参数被当作字符串处理。min(50, ...)这一层就是把前面小程序端 size 不可信的问题兜住所以后端永远不要信任前端传上来的边界值。涉及金额和库存扣减的接口后面还要加事务商品列表这种只读查询不需要。3.3 AdminLTE 管理后台复用同一套控制器后台管理页面和小程序接口共用数据库但后台需要看到库存、上下架、订单状态这类内部字段所以不建议直接复用小程序那些对外接口。更常见的做法是在同一个控制器里拆两个动作数据需求小程序端走管理后台走差异点上架商品列表goods/listgoods/adminListadminList 额外返回 stock、status订单列表order/myorder/adminList按状态与时间筛选发货操作不开放order/ship仅后台可调用需要登录态校验AdminLTE 页面里渲染表格时可以直接在模板中调用控制器方法取数据也可以在前端页面用 ajax 请求?rgoods/adminList。我偏向后一种因为后台页面的筛选、排序在浏览器端做更容易PHP 端只要把数据组合好输出 JSON 即可。这样小程序和后台共用一套参数绑定、一套表结构不用为后台再写一遍重复查询。4. 微信支付集成code2Session、统一下单与订单幂等4.1 用 code 换 openid 的那一步叫 code2Session微信小程序登录是云商城的起点而“小程序用 code 换 token”热搜里讲的正是 code2Session。wx.login 返回的 code 是一次性的临时凭证不能直接当业务登录态使用要交给 PHP 后端去微信服务器换取 openid 和 session_key再由后端自己生成一个业务 token 回传给小程序。小程序端登录代码wx.login({ success(loginRes) { if (!loginRes.code) return wx.request({ url: https://your-domain.com/index.php?ruser/login, method: POST, data: { code: loginRes.code }, success(res) { const token res.data res.data.data res.data.data.token if (token) wx.setStorageSync(token, token) } }) } })PHP 后端对应的处理public function loginAction() { $body json_decode(file_get_contents(php://input), true); $code $body[code] ?? ; $api https://api.weixin.qq.com/sns/jscode2session . ?appid . $this-cfg[appid] . secret . $this-cfg[secret] . js_code . urlencode($code) . grant_typeauthorization_code; $resp file_get_contents($api); $wx json_decode($resp, true); if (!isset($wx[openid])) { return [code 401, msg login failed]; } $userId $this-findOrCreateUser($wx[openid]); $token bin2hex(random_bytes(16)); $this-saveToken($token, $userId); return [code 0, msg ok, data [token $token, user_id $userId]]; }file_get_contents是 PHP 里最直接的调用方式但线上建议换成 curl 并设置超时避免微信接口无响应时 PHP 进程卡死。openid 是用户在小程序内的唯一标识但不要直接拿它做数据库主键仍建议用自增 user_id 关联openid 单独加唯一索引。code 只能使用一次且有效期很短前端不应保存它换完 token 后这个 code 就没有任何用途了。4.2 统一下单金额单位与签名算法微信支付 JSAPI 下单时金额单位是“分”。数据库里 decimal(10,2) 存的是元传参时要做intval($order[pay_amount] * 100)。统一下单参数组织与签名生成是最容易出错的部分private function buildUnifiedOrderParams($order, $openid) { $params [ appid $this-cfg[appid], mch_id $this-cfg[mch_id], nonce_str uniqid(, true), out_trade_no $order[order_no], total_fee intval($order[pay_amount] * 100), body 云商城商品, spbill_create_ip $_SERVER[REMOTE_ADDR], notify_url $this-cfg[notify_url], trade_type JSAPI, openid $openid, ]; $params[sign] $this-buildSign($params, $this-cfg[api_key]); return $params; } private function buildSign($params, $key) { ksort($params); $stringA ; foreach ($params as $k $v) { if ($v || $v null) { continue; } $stringA . $k . . $v . ; } return strtoupper(md5($stringA . key . $key)); }签名的规则是参数按字典序排序拼接成kvkv形式最后拼接key做 MD5 再转大写。空值参数不参与签名sign 字段本身不参与签名。total_fee 单位换算写错是最常见的线上事故数据库 0.01 元如果不乘 100传给微信就变成 1 分钱反过来如果浮点数精度出问题也会导致回调时金额校验失败。4.3 支付回调验签、金额校验、幂等支付回调是整条链路里最容易翻车的环节。微信服务器发起回调后先验签再校验订单号和金额最后更新订单状态并且必须处理重复通知。通知可能因网络原因被微信重发多次这是正常行为而不是异常。public function notifyAction() { $xml file_get_contents(php://input); $data xmlToArray($xml); if (!$this-verifySign($data, $this-cfg[api_key])) { $this-replyXml(FAIL, sign error); return; } $orderNo $data[out_trade_no]; $order $this-findOrder($orderNo); // 关键幂等处理已支付订单直接返回成功 if ($order[status] 1) { $this-replyXml(SUCCESS, OK); return; } if ($data[result_code] SUCCESS (int) $data[total_fee] (int) ($order[pay_amount] * 100)) { $this-db-beginTransaction(); try { $this-updateOrderStatus($orderNo, 1, $data[transaction_id]); $this-db-commit(); $this-replyXml(SUCCESS, OK); } catch (Exception $e) { $this-db-rollBack(); $this-replyXml(FAIL, db error); } return; } $this-replyXml(FAIL, amount mismatch); }回调返回的 XML 必须是固定格式微信收到 FAIL 会按退避策略重试不需要人为阻止。幂等判断放在最前面即使同一笔订单收到多次回调状态也不会被改乱。事务里只更新订单表和记录 transaction_id库存扣减应该在订单创建时预扣或者在回调成功后再扣后者实现更简单但要注意重复回调不能重复扣库存。5. 把支付回调连到本地用回调模拟脚本验证订单状态机5.1 开发者工具本地联调开关小程序开发者工具联调本地 PHP 服务时打开右上角“详情 - 本地设置 - 不校验合法域名”就能直接请求http://127.0.0.1/index.php。这个方法只在本地联调阶段使用真机预览或上线前必须把域名配成 HTTPS 合法域名。联调时先用开发者工具的 Network 面板确认请求头和响应体确认X-Auth-Token和{code,msg,data}结构都正确再去处理支付回调否则问题会被层层掩盖。5.2 回调模拟脚本支付回调在开发环境很难触发真实支付所以我会写一个 bash 脚本模拟微信服务器向本地接口推送 XML。验签部分是本地测试的关键这里用 PHP 命令行生成签名再拼进 XML 用 curl 提交#!/usr/bin/env bash OUT_NO20240101001 SIGN$(php -r $cfg [appid wx123, mch_id 100001, api_key test_api_key]; $arr [ appid $cfg[appid], mch_id $cfg[mch_id], nonce_str abc123, out_trade_no 20240101001, result_code SUCCESS, return_code SUCCESS, total_fee 100, transaction_id 42000000000020240001 ]; ksort($arr); $str ; foreach ($arr as $k $v) { $str . $k . . $v . ; } echo strtoupper(md5($str . key . $cfg[api_key])); ) curl -s -X POST http://127.0.0.1/index.php?rorder/notify \ -H Content-Type: text/xml \ --data-binary xml appid![CDATA[wx123]]/appid mch_id![CDATA[100001]]/mch_id nonce_str![CDATA[abc123]]/nonce_str out_trade_no![CDATA[${OUT_NO}]]/out_trade_no result_code![CDATA[SUCCESS]]/result_code return_code![CDATA[SUCCESS]]/return_code total_fee![CDATA[100]]/total_fee transaction_id![CDATA[42000000000020240001]]/transaction_id sign![CDATA[${SIGN}]]/sign /xml这个脚本里的 total_fee100 对应订单 1.00 元。签名内容必须与 PHP 端 verifySign 的排序规则完全一致否则会触发验签失败。脚本执行后如果流程正常数据库里订单状态应从 0 变为 1并且连续执行多次不会产生重复变更因为幂等判断在最前面拦截了。5.3 核对状态机的三条检验点脚本跑通后不要只看页面上是否跳转成功直接进 MySQL 核对字段变化订单状态数据库 status预期现象待支付0库存未减transaction_id 为空已支付1库存已减transaction_id 有值已发货2状态由 AdminLTE 后台操作变更验证库存是否被重复扣减可以执行SELECT stock FROM shop_goods WHERE id 目标商品id在回调前后各跑一次对比。线上环境如果出现回调问题优先在 notifyAction 入口记录完整 XML 和验签结果不要为了赶时间把验签临时去掉支付类接口不验签等于把钱包打开让别人刷。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询