简介:这套PHP手机端商城源码为需要快速搭建H5商城、抖音小店后台或学习PHP商城开发的技术人员提供了一站式方案。包内包含完整可运行的商城系统,附带设置教程,覆盖网站配置、短信与支付接口、商品分类与商品管理、工单、订单、分站及提现管理等核心模块,按文档配置即可完成商城基本功能。资源共346个文件,约13.64MB,以153个gif演示图片、59个js交互脚本、52个php业务逻辑文件、25个png及24个css界面样式为主,另有sql数据库脚本、bat辅助工具及字体文件等,结构与常见PHP商城项目一致。目前已有158人学习下载。对想研究商城业务逻辑、熟悉PHP7+MySQL开发或需要部署演示环境的开发者来说,这份源码加教程能节省大量排查时间,直接对照模块配置,理解前后台运作流程。
1. 从 PHP 手机端商城源码到 H5 商城系统:这套东西到底想干成什么事
做独立商城的人,手里几乎都压过一份 PHP 手机端商城源码。所谓 H5 商城系统,本质上是响应式页面加一套移动优先的下单流程,用户点链接直接进浏览器购物,不需要安装 App。而“抖音商城小店”这几个字决定了它不只是一套空壳页面,商品的发布、订单的同步、库存的核对、售后的闭环,都要有模块跟抖音开放平台做双向对接。这套方向最适合两类人:手里有货、想低成本试水的店主,以及接单做商城的外包开发者。很多人拿到源码后的第一反应是改 Logo,结果后台、支付、回调全部没通,白忙一晚上。在动手之前,我先把这条线上要踩的关键节点逐个拆给你看。
2. 先看技术栈再动手:PHP 版本、源码目录与三处必改配置
2.1 为什么中小商城仍然把 PHP 当第一选择
一份标题带“PHP”的商城源码在市面上流转这么多年,不是没有原因的。中小商城的核心诉求是能快速上线、能小成本维护、能随时找个熟悉 PHP 的人改需求。PHP 在这条赛道上几乎是泥腿子里的老兵:运行环境轻,Nginx 加 PHP-FPM 就能跑;开发门槛低,改完文件刷新立刻看到效果;商城类的成熟源码存量特别大,分销、拼团、优惠券这些插件模板拿过来就能用,不需要从零画轮子。
对比下来你会更清楚自己的选择:
| 维度 | PHP 商城 | Java 商城 | Node.js 商城 |
|---|---|---|---|
| 日常运行开销 | 低,内存占用小 | 高,JVM 常驻吃内存 | 中等,进程常驻 |
| 上手成本 | 低,改完刷新可见 | 高,适合中大型团队 | 中,前端团队更顺手 |
| 成熟源码存量 | 很多,插件体系完善 | 偏企业级,定制成本高 | 少,很多要自己写 |
| 适合场景 | 单店、中小连锁、快速试水 | 中台、多系统集成 | 实时互动、高并发网关 |
我不建议一上来就追最新版本,比如非得把环境切到 PHP 8.3。很多商城源码是在 PHP 7.4 到 8.1 时代写的,直接扔到 8.2 以上可能全是“Deprecated”警告,甚至白屏。最稳妥的做法是先看包内有没有写环境要求;没写的话,先装 PHP 7.4 跑通,再慢慢升级。稳定的老版本永远比新奇的新版本更适合当生产环境。
2.2 看懂一份商城源码的目录结构:先分清入口、业务和临时文件
解压之后别急着双击安装,先花十分钟把目录结构过一遍。我见到的商城源码,无论是不是框架写的,大体的骨架都差不多:
project_root/ ├─ public/ │ ├─ index.php # 唯一入口,所有请求都先进这里 │ ├─ admin.php # 后台管理入口,有些包叫 admin/index.php │ └─ static/ # 静态资源:CSS、JS、图片 ├─ app/ │ ├─ controllers/ # 控制器:接收参数、调服务、返回页面或 JSON │ ├─ models/ # 数据模型:和数据库表一一对应 │ ├─ services/ # 业务服务:支付、订单、第三方对接 │ ├─ validate/ # 参数校验规则 ├─ config/ │ ├─ database.php # 数据库连接配置 │ └─ setting.php # 站点名称、域名、上传目录等 ├─ runtime/ │ ├─ cache/ # 模板缓存、数据缓存 │ └─ log/ # 运行日志,排错时最先看这里 ├─ addons/ # 插件目录:分销、秒杀、会员卡都放这 ├─ install/ # 安装向导,部署完成后建议删掉 ├─ shop.sql # 数据库初始化脚本 └─ README.md # 有没有认真写说明,能看出作者水平重点是理解“入口唯一”:当你在 Nginx 里配置站点时,根目录指向public/不是项目根目录,所有请求统一经过index.php。后台入口可能是独立的admin.php,也可能是通过路由参数区分。
2.3 三处必改配置:数据库连接、站点域名和伪静态规则
拿到源码第一次启动,九成报错都出在这三处。
第一处是数据库连接。多数包会提供一个install/向导,你填好 MySQL 地址、用户名、密码、库名就能自动生成配置文件。没有向导的,就手动打开config/database.php,把 host、database、username、password 和表前缀改掉。这里有个非常隐晦的坑:有些包在安装向导里把数据库配置写进文件了,却另外加了一层“配置缓存”,你改了文件不生效,得去后台“清除缓存”或者手动删掉runtime/cache下的编译文件。
第二处是站点域名。config/setting.php里的site_url或domain字段,直接影响支付回调拼接、图片绝对路径生成和分享链接跳转。本地开发时可以写成http://127.0.0.1:8080,上服务器后一定要改成你的 HTTPS 域名,不然支付回调会跑到一个乱七八糟的地址上。
第三处是伪静态规则。大多数商城源码用的是地址重写,把index.php?s=/home/goods/detail&id=1这种东西变成/goods/detail/1.html。拿 ThinkPHP 系的包举例,Nginx 下我一般这样配置:
server { listen 80; server_name shop.example.com; root /var/www/shop/public; index index.php; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; break; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }这段配置的核心是rewrite ^(.*)$ /index.php?s=$1:所有不存在的文件请求都交给入口文件处理,由框架按s参数解析真正的路由。如果你的源码是基于其他框架的,例如 Laravel 风格,通常改用try_files $uri $uri/ /index.php?$query_string;更合理。改完伪静态后务必测一下商品详情页 URL,返回 404 说明规则不匹配,换一种规则再试。
3. 打通抖音商城小店:商品同步、订单回调和签名校验怎么落地
3.1 先划边界:抖音商城小店的三种接入方式
很多人对“抖音商城小店”有误解,以为源码解压完就自动和抖音打通了。实际上,一份 PHP 商城源码通常只提供商品、订单、会员这些基础模块,抖音开放平台的能力是需要自己去对接的。常见做法有三种:
- 手动导表:商品少、单量少时,直接在抖音商家后台人工上架,本地商城也手动同步,两边靠 Excel 平账。适合刚开业测试阶段的门店。
- 半自动对接:把抖音后台导出的商品 CSV 定时上传到本地商城,订单仍然在抖音侧手工发货。适合每天几十单的店。
- 全开放平台 API 对接:通过接口完成商品上下架、库存同步、订单订阅回调、物流回传。适合要做库存实时同步、多平台经营的店。
标题里既然写了“抖音商城小店”,你至少要奔着第三种去。否则这行字只是白挂了一个标签。我的建议是:先花两天把商品和订单这两条主线打通,售后、退款可以放第二批,否则第一周就会被细节拖死。
3.2 签名算法是第一步:先写一个通用签名函数
对接这类开放平台,第一步永远是签名。虽然每个开放平台的参数规范不同,但思路大同小异:把业务参数按 ASCII 升序排列,把空值过滤掉,加上 app_secret 做拼接,再算摘要。我一般会在app/services/里建一个独立的对接服务类,先实现一个签名函数:
// DoudianService.php 对外开放平台对接服务 class DoudianService { private $appKey; private $appSecret; private $gateway; public function __construct($appKey, $appSecret, $gateway) { $this->appKey = $appKey; $this->appSecret = $appSecret; $this->gateway = $gateway; } // 签名规则:参数按 key 升序,空值不参与拼接 private function makeSign(array $params): string { ksort($params); $stringToSign = ''; foreach ($params as $key => $value) { if ($value === '' || $value === null) { continue; } $stringToSign .= $key . $value; } // 常见变体:appSecret 同时包在字符串头尾 return strtoupper(md5($this->appSecret . $stringToSign . $this->appSecret)); } }这个函数里三个要点:ksort必须按字符升序排;过滤空值是为了避免两边拼接结果不一致;MD5 之前要确认平台要求的是secret + 参数串 + secret还是只拼一次。这种签名规则看起来简单,真正的坑全在拼法细节上,例如哪个字段参与签名、时间戳精确到秒还是毫秒。每对接一个开放平台,我做的第一件事都是先写几十行测试用例,把签名对拍通过再继续。
3.3 商品同步主流程:本地商品和抖音商品要做映射表
商品同步的核心不是“把商品推上去”,而是“维护一张映射表”。本地商城有自己的商品 ID,抖音侧有自己的商品 ID,两侧不一定一一对应。多规格商品在本地是一条记录,在抖音侧可能是多个 SKU 分别占用不同 ID。所以设计数据表时至少要留几个字段:本地商品 ID、平台商品 ID、平台 SKU ID、同步状态、最近同步时间。
下面是一个简化的拉取商品库存主流程:
// 拉取抖音侧的增量商品列表并更新本地映射 public function syncProducts(int $page = 1, int $pageSize = 50): void { $params = [ 'app_key' => $this->appKey, 'timestamp' => time(), 'method' => 'product.list', 'page' => $page, 'page_size' => $pageSize, ]; $params['sign'] = $this->makeSign($params); $resp = Http::post($this->gateway, $params); if (isset($resp['err_no']) && $resp['err_no'] !== 0) { Log::error('抖音商品拉取失败', $resp); return; } foreach ($resp['data']['product_list'] as $item) { // upsert:按平台商品ID判断是插入还是更新 $map = ProductMap::where('platform_product_id', $item['product_id'])->first(); if (!$map) { $map = new ProductMap(); $map->platform_product_id = $item['product_id']; } $map->local_goods_id = $item['out_goods_id']; // 本地商品编码 $map->local_sku_id = $item['out_sku_id']; $map->platform_stock = $item['stock_num']; $map->sync_at = time(); $map->save(); } }这个流程说明了一个容易被忽略的点:out_goods_id这种字段是让你自己填的“外部商品编码”,是两边沟通的桥梁。如果平台要求你在上架商品时指定外部编码,那本地商城生成商品时必须同步生成一个唯一的业务编号,而不是直接用自增 ID。否则,本地删了一条商品记录再重建,抖音那边就对不上了。我在对接时习惯用order_no和goods_no这类带业务前缀的编号做外部唯一键,不用数字自增主键。
3.4 订单回调落地:验签、去重、状态映射一个都不能少
订单回调是整个对接里最怕出错的环节,因为它是平台主动给你推消息,不是你主动去拉。这类回调通常允许一定时间内的重试,处理不好就会重复入账、发货错乱。我处理这类回调的实际套路如下:
// 平台回调入口,返回固定格式告诉对方“我收到了” public function orderCallback(): string { // 1. 拿原始报文并解析 $raw = file_get_contents('php://input'); $data = json_decode($raw, true) ?? []; $sign = $data['sign'] ?? ''; unset($data['sign']); // 2. 验签,防伪造回调 if (!$sign || $sign !== $this->makeSign($data)) { return json_encode(['code' => 400, 'msg' => 'sign invalid']); } $orderKey = 'callback:' . $data['order_id']; // 3. 幂等键:同一个订单号只处理一次 if (!Redis::setnx($orderKey, 1)) { return json_encode(['code' => 0, 'msg' => 'duplicate']); } Redis::expire($orderKey, 86400); try { Db::transaction(function () use ($data) { // 4. 按平台订单号更新本地订单状态 $localOrder = Order::where('platform_order_id', $data['order_id'])->first(); if ($localOrder) { $localOrder->status = $this->mapOrderStatus($data['order_status']); $localOrder->save(); } // 5. 原始报文落库,方便事后排查 Log::channel('callback')->info('doudian_order', $data); }); } catch (\Throwable $e) { Redis::del($orderKey); return json_encode(['code' => 500, 'msg' => $e->getMessage()]); } return json_encode(['code' => 0, 'msg' => 'success']); }这里最重要的不是返回格式,而是第 3 步的幂等键。如果你没有 Redis,可以用订单表加唯一索引替代:在platform_order_id上建唯一索引,重复插入直接报错。还有一点容易被忽略,支付回调里往往不只是一个订单,而是“订单 + 支付单 + 子订单”三层结构。如果你拿到的 payload 里有order_id,又有sub_order_id,建议锁主订单号,给每个子订单的状态单独留字段,避免合并支付时只更新了其中一单。
【提示】回调接口必须返回 HTTP 200,不能因为业务异常就报 500。平台超时重试的次数是有限的,你一直报 500,它重试到上限后就不会再推,损失的是订单数据。正确做法是先把报文落日志,再执行业务,业务失败后人工补偿,而不是把整个接口挡在门外。
4. 源码部署与 H5 调试:从本地跑通到服务器上线的完整路径
4.1 本地跑通这套源码的最小步骤
先不谈“附教程”里写了什么,我拿到任何一套 PHP 商城源码,都会用同一套最小步骤本地跑通。Windows 上用 phpstudy 这类集成环境,Linux 或 Mac 上可以自己装 PHP、MySQL。关键是把版本对齐,别用太新的 PHP。
本地最小化跑通流程:
# 1. 安装依赖(如果包内没有 vendor 目录) composer install --no-dev # 2. 复制环境配置文件 cp .env.example .env # 3. 修改 .env 里的数据库连接、APP_URL 为你本机的地址 # 4. 导入数据库(注意使用 utf8mb4 字符集) mysql -uroot -p --default-character-set=utf8mb4 shop < shop.sql # 5. 用 PHP 内置服务器启动开发环境(不是生产方案) php -S 0.0.0.0:8080 -t public-t public表示把站点根目录指向 public 文件夹。这步能跑通,说明 PHP 版本、数据库、目录权限基本没问题。本地调试时要特别注意,PHP 内置服务器是单线程的,只适合做功能联调,不要拿它测并发,也别拿它测支付回调。支付类回调需要公网能访问到的地址,本地调试建议用内网穿透工具,或者干脆把核心逻辑放到单元测试里跑。
4.2 云服务器上线:站点根目录、目录权限和伪静态
本地能跑了,上服务器才是真正开始。国内服务器商家常用的面板带图形界面,但面板只是减少重复操作,底层的坑一个也不会少。
我上线一套 H5 商城的标准操作是这样:
- 将源码上传到
/www/wwwroot/shop; - 新建站点,域名绑定、站点根目录选
/www/wwwroot/shop/public; - 选择 PHP 版本时先选 7.4 或 8.0,别一上来选 8.2;
- 伪静态选择对应的框架模板;
- 配置 SSL 证书,开启强制 HTTPS 跳转;
- 设置目录权限。
目录权限这一步在面板里经常被忽略,手工敲命令时更要小心:
# 将站点目录属主改为 www 用户(按你所用 web 服务器的实际用户调整) chown -R www:www /www/wwwroot/shop # 业务目录不需要写权限,但 runtime、uploads 需要可写 chmod -R 755 /www/wwwroot/shop chmod -R 775 /www/wwwroot/shop/runtime chmod -R 775 /www/wwwroot/shop/public/uploads很多教程会教你chmod -R 777,我强烈不建议这么干。777 相当于把你的整个店铺门钥匙挂在了大街上,一旦被扫描到上传漏洞,整个目录都能被人写一句话木马。正确做法是目录属主给 web 用户,写权限只给 runtime 和 uploads 这类真正要写文件的目录。
4.3 H5 页面在真机和浏览器里的调试技巧
H5 商城最常见的坑不是功能,而是“手机上看不对”。电脑浏览器正常,手机一打开样式全乱了、按钮点了没反应、图片加载不出来。这类问题不能靠“拍屏发微信”来沟通,要系统化排查。
我调试 H5 页面的三板斧:Chrome 开发者工具的手机模拟模式看布局,真机连调试看请求,再用第三方抓包或后台日志确认接口数据。Chrome 的模拟模式侧重 UI,真机调试侧重真实的浏览器内核行为。如果在微信里打开有问题,就调出微信内置浏览器的 Debug 入口,重点看 JS 报错和 cookie 是否带上了。
初装系统后经常出现的一种情况是:同一套源码在安卓手机正常,在苹果手机白屏,大概率是 JS 语法不支持旧版 Safari。检查一下代码里有没有用了可选链?.、空值合并??这类新语法,然后用 Babel 转译一下发行版。另一类隐蔽问题是缓存,手机浏览器会缓存旧的 CSS、JS,导致新功能一直不生效。解决方法是在静态资源 URL 后面加版本号,例如app.js?v=20240101,或者在框架里开启自动版本号。
5. 避坑排查:H5 商城系统上线前后最常见的 6 个翻车现场
5.1 SQL 导不进去:Unknown collation 报错
现象:执行shop.sql时提示Unknown collation: 'utf8mb4_0900_ai_ci',导入中断。原因:这份 SQL 文件是从 MySQL 8.0 导出的,默认排序规则才叫utf8mb4_0900_ai_ci;你本地或服务器装的是 MySQL 5.7,压根不认识这个规则。解决:如果服务器支持,直接换 MySQL 8.0;如果不想动数据库版本,把 SQL 文件里的utf8mb4_0900_ai_ci全部替换成utf8mb4_general_ci。注意要同时检查建库语句和每张表的DEFAULT CHARSET,别只替换一半。
5.2 H5 页面图片裂图,后台却看得到
现象:商品图、轮播图在后台全部正常,小程序或 H5 页面全部裂图,浏览器控制台显示图片请求 404。原因:源码里存的是“绝对路径带固定域名”的图片地址,你换了域名或迁移了环境,图片地址还是指向旧域名。这种问题在从测试环境切生产环境时特别常见。解决:如果不打算保留原域名,在 SQL 里做一次批量替换,把源域名换成当前域名。注意同时替换两种形式:http://old-domain.com和//old-domain.com,后者是协议相对地址,最容易漏。我在命令行里会先SELECT查一遍涉及的记录数,再跑UPDATE,避免一把梭全表替换误伤用户头像和分享图。
5.3 支付回调收不到,订单状态一直“待付款”
现象:用户明明付了款,本地商城订单状态不更新,后台一直看不到回款记录。原因:支付平台的回调地址没有配置成公网可访问的 HTTPS 地址;或者你服务器上的安全组、系统防火墙拦截了来自支付平台的回调请求。另一种常见原因是源码里拼接回调地址时用了site_url,你在后台没把这个配置改成线上域名。解决:先把回调地址在支付后台改成https://你的域名/payment/notify,然后在服务器上手动模拟一次 POST 请求测试这个地址是否可访问;把源码里的支付日志打开,看有没有收到原始报文。如果日志里什么都没收到,优先查防火墙和安全组,不要怀疑支付平台有问题。
5.4 PHP 版本太新导致白屏或 500
现象:安装完成后一访问首页就白屏,Apache 或 Nginx 日志里能看到PHP Fatal error: Uncaught Error: Call to undefined function。原因:源码是在 PHP 7.x 时代写的,用了老函数,例如mysql_*系列、each()、create_function(),这些在 PHP 8.0 以后被移除或标记为弃用。还有一部分是扩展没开:curl、fileinfo、openssl缺任何一个都会触发异常。解决:先用php -v确认版本,再用php -m列出已加载的扩展,检查 curl 和 fileinfo 是否在列。版本对不上的,最简单的方法是切到 7.4。如果坚持用 PHP 8,把error_reporting临时开到E_ALL,让致命错误直接显示到屏幕上,定位到底是哪个文件哪一行出的问题。
5.5 抖音侧商品同步总失败,本地库存始终是 0
现象:商品同步时提示成功,但抖音小店里的库存一直显示 0,或者本地商城显示的库存和抖音侧对不上。原因:库存同步接口被单独调用了,但同步的是本地“总库存”,没有按 SKU 拆分;还有可能是你的 “外部商品编码” 不唯一,部分商品用了同一个编码。解决:检查库存同步请求里传的是不是 platform 侧的 sku_id,而不是商品 ID。同时核查本地goods_no生成规则,把生成编号的逻辑加上前缀和时间戳,确保唯一性。库存这种强一致的数据,我建议同步时写乐观锁校验,比对版本号再更新,防止高并发下把旧库存覆盖成新库存。
5.6 后台清了缓存,前台还是旧内容
现象:商品价格改完,H5 页面刷新还是旧价格;管理员后台点了“清缓存”,用户端仍然看到旧数据。原因:商城系统通常有两层缓存:文件缓存和浏览器缓存。后台清的是服务端文件缓存,用户浏览器里还缓存着旧的 HTML 页面或接口数据。尤其使用 H5,浏览器对 GET 请求的缓存策略比小程序还要激进。解决:不要只清服务端缓存,把 H5 入口页面的响应头设置为Cache-Control: no-cache,商品详情接口设置成带版本号的强缓存策略。这样既不牺牲访问速度,又能保证改价后用户能看到新结果。核心页面建议写成“每次校验缓存版本号,版本号来自后台修改时间”,服务器端再配合 Redis 做热点缓存。
6. 上线后的第一轮加固:缓存、安全与日志排查的落地技巧
商城源码能跑、能下单、能收到抖音回调,只是开始。我见过太多系统上线第一周就被人刷爆流量、注入恶意脚本的例子,所以上线后第一天就该做一轮安全加固,而不是等出了问题再看。
第一件事是删除安装向导。install/目录如果还在线上,等于告诉别人“我可以被重新安装”,一旦被恶意访问,数据库配置可能被覆盖。上线后我做的第一件事永远是:把 install 目录改名并移出网站根目录,或者直接在代码里屏蔽安装路由。
第二件事是给商品详情和首页加缓存。大多数商城源码自带的文件缓存已经够用,但对商品详情这种读多写少的场景,我习惯用 Redis 做一层对象缓存:
// 商品详情读取:先查 Redis,没有再查数据库并写回 public function detail(int $goodsId): array { $cacheKey = 'goods:detail:' . $goodsId; if (Redis::exists($cacheKey)) { return json_decode(Redis::get($cacheKey), true); } $goods = Goods::with('skus')->find($goodsId); Redis::setex($cacheKey, 600, json_encode($goods->toArray())); return $goods->toArray(); }这段逻辑里 600 是过期秒数。价格、库存这类字段如果你通过公开接口被小红书、抖音那边回流,就影响太大了,必须改用“更新时主动删缓存”而不是单纯依赖过期时间。改价之后执行Redis::del($cacheKey),让下一个访问者重新查库,做到最终一致。
第三件事是收紧上传和错误日志的权限。商品图上传必须限制类型白名单,只允许 jpg、png、webp,禁止上传 php、phtml 文件;同时把 PHP 的错误显示关掉,线上环境只记录日志,不把 SQL 报错直接打在 H5 页面上。你不想让用户看见你数据库表名和字段名,更不想让攻击者看见。
我自己的习惯是,商城上线后的头两周每天都翻一遍运行日志,搜索error和notice,不只是看业务报错,还要看有没有异常的接口调用频率。很多安全问题不是一开始就能发现的,而是藏在正常业务日志的夹缝里。开发时偷懒一时爽,上线后全都要还。希望这套 PHP 手机端商城源码方向能帮你把首个版本扎实落地,少熬夜。
本文还有配套的精品资源,点击获取