简介:这是一套基于PHP开发的淘宝天猫代付系统源码,面向需要实现代付功能的开发者与中小型电商团队,解决用户授权、订单获取、代付请求与支付回调等业务场景。系统采用RESTful API与OAuth 2.0授权机制,可选Laravel、ThinkPHP等框架,配合MySQL存储数据,并涵盖数据加密、签名验证、权限控制及防SQL注入与XSS等安全设计。资源包共2000个文件,以1407个js、210个html、131个md、117个css、111个json为主,另含少量sql、sh、xml及docx、pptx文档,压缩包约75.19MB,前端资源与说明文档齐备。目前已有254人学习。读者可从中获取完整的代付业务实现思路、接口设计参考、数据库脚本与前端页面结构,适合用于二次开发、课程设计或技术方案验证。
1. 从一单代付说起:php淘宝天猫代付系统到底在解决什么问题
做电商代购、代拍或者代付业务的朋友,大概率都遇到过这样的场景:客户在微信里发来一个淘宝或天猫的商品链接,说“帮我拍下,钱我转你”,然后你手动打开链接、复制商品信息、算好价格、发收款码、等对方转账、再去下单。一单两单还行,一天几十单上百单,光是复制粘贴就能把人逼疯。php淘宝天猫代付系统要解决的,就是把这套“人肉中转”流程自动化——客户在前端提交商品链接和代付金额,系统自动解析商品标题、图片、价格,生成代付订单,对接支付,回调后通知你发货或下单。
这套系统的核心价值在于三点:一是把商品信息解析自动化,省掉手工录入;二是把资金流和订单流绑定,避免“付了没记”或“记了没付”的糊涂账;三是给客户一个自助入口,降低沟通成本。适合谁用?做代购代拍的小团队、有私域流量的淘客、以及需要给客户提供代付通道的站长。技术栈上,PHP 依然是这类中小型系统的主力,部署简单、生态成熟,宝塔面板一装就能跑。但要注意,这类系统涉及淘宝商品数据的获取,淘宝的接口和页面结构一直在变,所以“能跑通”和“长期稳定跑”是两回事,后面会重点讲怎么把解析层做得抗造。
2. 代付系统的订单模型与数据表设计:先把账算清楚再写代码
2.1 代付订单和普通订单的本质区别
普通电商订单是“买家付钱给平台,平台发货”,资金流是单向的。代付系统不一样,它的资金流是两段式的:客户先把钱给你(代付方),你再拿这笔钱去淘宝下单,或者你垫付后客户再结算。这就意味着订单表里必须同时记录“客户应付金额”和“实际下单金额”,而且这两个金额可能因为运费、优惠券、改价而不一致。我见过不少翻车案例,就是只记了一个金额,最后对账时发现差了十几块,查半天查不出来。
所以订单模型至少要拆成三层:代付主订单(记录客户提交的链接、客户应付总额、状态)、商品明细(一个链接可能对应多个 SKU,记录每个 SKU 的标题、图片、单价、数量)、资金流水(记录客户实际支付、退款、补差价)。主订单和明细是一对多,主订单和流水也是一对多。这样设计的好处是,当客户说“我明明付了 200”,你可以直接拉出流水给他看,而不是靠记忆扯皮。
2.2 建表 SQL 与字段说明
下面是我一般会用的核心表结构,基于 MySQL 8.0,字符集用 utf8mb4,因为淘宝商品标题里经常有 emoji 和生僻字。
-- 代付主订单表 CREATE TABLE `dp_order` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '系统内部订单号,规则:dp+日期+随机', `out_trade_no` varchar(64) DEFAULT NULL COMMENT '对接支付时传给支付平台的单号', `customer_id` int unsigned NOT NULL COMMENT '客户ID', `item_url` varchar(512) NOT NULL COMMENT '客户提交的淘宝/天猫商品链接', `item_title` varchar(255) DEFAULT NULL COMMENT '解析出的商品标题', `item_image` varchar(512) DEFAULT NULL COMMENT '解析出的主图URL', `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '客户应付总额', `paid_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '客户已付金额', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已下单 3已完成 4已取消 5已退款', `remark` varchar(255) DEFAULT NULL COMMENT '客户备注', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_customer` (`customer_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代付主订单'; -- 商品明细表 CREATE TABLE `dp_order_item` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `order_id` bigint unsigned NOT NULL, `sku_id` varchar(64) DEFAULT NULL COMMENT '淘宝SKU ID', `sku_name` varchar(255) DEFAULT NULL COMMENT 'SKU名称,如颜色尺码', `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '单价', `quantity` int NOT NULL DEFAULT '1', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代付商品明细'; -- 资金流水表 CREATE TABLE `dp_payment_log` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `order_id` bigint unsigned NOT NULL, `trade_no` varchar(64) DEFAULT NULL COMMENT '支付平台流水号', `amount` decimal(10,2) NOT NULL COMMENT '变动金额,正为收款,负为退款', `type` tinyint NOT NULL COMMENT '1支付 2退款 3补差价', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0处理中 1成功 2失败', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代付资金流水';字段设计上有几个点值得展开。order_no用业务前缀加日期加随机数,不要用自增 ID 直接暴露给客户,否则竞争对手能通过订单号推算出你的单量。total_amount和paid_amount分开存,是为了支持部分支付和补差价场景。status用 tinyint 而不是 enum,因为 enum 改起来麻烦,tinyint 在代码里用常量映射更灵活。dp_payment_log里的amount允许负数,退款就是负的,这样对账时直接 sum 就能得到净收入。
2.3 订单状态流转的代码实现
状态流转是代付系统最容易出 bug 的地方。客户支付成功回调了,但订单状态没改,或者改错了,都会导致重复发货或客户投诉。我一般会把状态流转封装成一个方法,所有状态变更必须走这个方法,禁止在业务代码里直接 update status。
<?php class DpOrderService { // 定义允许的状态流转 private const TRANSITIONS = [ 0 => [1, 4], // 待支付 -> 已支付 或 已取消 1 => [2, 5], // 已支付 -> 已下单 或 已退款 2 => [3], // 已下单 -> 已完成 3 => [], // 已完成 终态 4 => [], // 已取消 终态 5 => [], // 已退款 终态 ]; public function changeStatus(int $orderId, int $newStatus): bool { $order = DpOrderModel::find($orderId); if (!$order) { throw new Exception("订单不存在: {$orderId}"); } $current = $order['status']; if (!in_array($newStatus, self::TRANSITIONS[$current] ?? [])) { // 记录日志,方便排查是谁在乱改状态 Log::error("非法状态流转", ['order_id' => $orderId, 'from' => $current, 'to' => $newStatus]); return false; } // 用乐观锁防止并发重复回调 $affected = DpOrderModel::where('id', $orderId) ->where('status', $current) ->update(['status' => $newStatus]); return $affected > 0; } }这段代码的关键在于TRANSITIONS常量定义了合法的流转路径,任何不在路径里的变更都会被拒绝并记日志。where('status', $current)是乐观锁,防止支付回调重复触发时把状态改乱。参数说明:$orderId是主订单 ID,$newStatus是目标状态值,返回 true 表示变更成功,false 表示被拒绝或并发冲突。实际使用时,支付回调里先查流水是否已处理,再调这个方法,双保险。
3. 淘宝天猫商品信息解析:从链接到结构化数据的落地路径
3.1 解析方案选型:接口、页面、还是第三方
商品解析是代付系统的入口,也是最容易失效的环节。常见做法有三种:一是调用淘宝开放平台的接口,但需要资质和审核,个人开发者基本拿不到;二是直接抓取商品详情页 HTML,然后正则或 DOM 解析,成本低但对抗强;三是用第三方数据服务,按次收费,省心但长期成本高。我一般会建议中小团队用第二种,配合缓存和降级策略,因为代付场景对实时性要求没那么高,商品标题和价格缓存几分钟完全可接受。
选第二种方案,核心要解决两个问题:怎么拿到页面、怎么从页面里提取数据。拿页面这一步,直接用file_get_contents大概率会被跳转到登录页或者返回空,因为淘宝会检查 User-Agent、Referer 和 Cookie。所以必须模拟一个正常的浏览器请求头。提取数据这一步,淘宝的详情页数据是嵌在 JavaScript 变量里的,比如g_config或g_page_config,直接正则匹配 JSON 比解析 DOM 更稳,因为 DOM 结构改版频繁,而数据字段名相对稳定。
3.2 用 PHP cURL 抓取并解析商品 JSON
下面是一个可复现的最小实现,抓取商品标题、主图、价格区间。注意,这里只做技术演示,实际使用要遵守目标网站的服务条款和 robots 协议,控制请求频率。
<?php function fetchTbItem(string $url): array { // 从链接中提取商品ID,支持 item.taobao.com 和 detail.tmall.com if (!preg_match('/[?&]id=(\d+)/', $url, $m)) { throw new Exception("无法从链接中提取商品ID: {$url}"); } $itemId = $m[1]; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => "https://detail.tmall.com/item.htm?id={$itemId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 10, CURLOPT_ENCODING => 'gzip, deflate', CURLOPT_HTTPHEADER => [ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language: zh-CN,zh;q=0.9', 'Referer: https://www.taobao.com/', ], ]); $html = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200 || empty($html)) { throw new Exception("抓取失败,HTTP状态: {$httpCode}"); } // 淘宝详情页数据通常在 g_config 或 g_page_config 变量中 // 这里匹配 g_config = {...}; 的结构 if (!preg_match('/g_config\s*=\s*(\{.+?\});/s', $html, $match)) { // 降级:尝试从页面 title 标签提取标题 if (preg_match('/<title>(.*?)<\/title>/s', $html, $t)) { return ['title' => trim($t[1]), 'price' => '0.00', 'image' => '']; } throw new Exception("页面结构变化,未匹配到数据变量"); } $config = json_decode($match[1], true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception("JSON解析失败: " . json_last_error_msg()); } // 不同类目字段位置可能不同,做兼容取值 $title = $config['item']['title'] ?? $config['itemTitle'] ?? ''; $image = $config['item']['images'][0] ?? $config['item']['picUrl'] ?? ''; $price = $config['item']['price'] ?? $config['price'] ?? '0.00'; return [ 'item_id' => $itemId, 'title' => $title, 'image' => $image, 'price' => $price, ]; }逻辑说明:先正则提取商品 ID,这是所有后续操作的基础。cURL 设置里CURLOPT_ENCODING让服务器返回压缩内容,减少传输量;请求头模拟 Chrome 浏览器,Referer设为淘宝首页,降低被拦截概率。拿到 HTML 后,优先匹配g_config变量,因为它是结构化的 JSON,比正则抠 DOM 稳定。如果匹配失败,降级到<title>标签,至少保证标题能拿到,价格和图片留空,前端提示用户手动填写。参数方面,CURLOPT_TIMEOUT设 10 秒,太短容易超时,太长会拖慢前端响应;实际生产环境建议加一层 Redis 缓存,同一个商品 ID 5 分钟内不重复抓取。
3.3 解析失败时的降级与人工兜底
再稳的解析也有失效的一天。淘宝改版、风控升级、网络抖动,都会导致解析失败。这时候系统不能直接报错让客户干瞪眼,要有降级路径。我的做法是:解析失败时,订单照常创建,但商品标题留空,状态标记为“待补充”,同时给管理员发一条通知。客户在前端看到的是“商品信息解析中,请确认金额后支付”,支付流程不受影响。管理员在后台可以手动补录标题和图片,或者直接复制客户发的链接去下单。
这个降级策略的核心思想是:解析是锦上添花,不是阻塞流程的必要条件。代付系统的本质是资金和订单的匹配,商品信息只是辅助。把解析失败和支付流程解耦,能大幅降低客诉率。另外,建议把每次解析的原始 HTML 存一份到本地或对象存储,保留 7 天,方便出问题时回溯是页面变了还是代码 bug。
4. 支付对接与回调处理:把“钱到了”这件事做扎实
4.1 支付渠道选型与签名机制
代付系统对接的支付渠道,常见的有微信支付、支付宝当面付、以及一些聚合支付。选型时重点看三点:是否支持个人或小微商户、回调是否稳定、手续费多少。不管选哪个,核心逻辑是一样的:发起支付时生成签名,回调时验证签名,验证通过后更新订单状态。签名的作用是防止别人伪造回调通知,把你的订单改成已支付。
以常见的 MD5 签名为例,流程是:把请求参数按 key 字典序排序,拼接成key=value&key=value的字符串,末尾加上商户密钥,做 MD5,得到签名。回调时用同样的方法算一遍,和回调参数里的 sign 对比,一致才处理。这里有个血泪经验:签名用的参数必须是原始值,不能做 URL 解码后再拼,否则中文或特殊字符会导致签名不一致。我见过有人因为这个问题排查了一整天。
4.2 回调处理的幂等与事务
回调处理最大的坑是重复通知。支付平台在没收到你的成功响应时,会按策略重试,可能同一笔订单回调多次。如果你的代码没有幂等处理,就会重复加钱、重复改状态。幂等的实现方式很简单:在处理回调前,先查dp_payment_log里有没有这个trade_no且状态为成功的记录,有就直接返回成功,不再处理。
<?php function handleNotify(array $params): string { // 1. 验证签名 if (!verifySign($params)) { Log::error("回调签名验证失败", $params); return 'fail'; } $orderNo = $params['out_trade_no']; $tradeNo = $params['trade_no']; $amount = $params['total_amount']; // 2. 幂等检查:同一笔支付流水只处理一次 $exists = DpPaymentLogModel::where('trade_no', $tradeNo) ->where('status', 1) ->find(); if ($exists) { return 'success'; // 已处理过,直接告诉支付平台成功 } Db::startTrans(); try { $order = DpOrderModel::where('order_no', $orderNo)->lock(true)->find(); if (!$order) { throw new Exception("订单不存在: {$orderNo}"); } if ($order['status'] != 0) { throw new Exception("订单状态异常: {$order['status']}"); } // 3. 写入流水 DpPaymentLogModel::create([ 'order_id' => $order['id'], 'trade_no' => $tradeNo, 'amount' => $amount, 'type' => 1, 'status' => 1, ]); // 4. 更新订单 $service = new DpOrderService(); $service->changeStatus($order['id'], 1); DpOrderModel::where('id', $order['id'])->update(['paid_amount' => $amount]); Db::commit(); return 'success'; } catch (Exception $e) { Db::rollback(); Log::error("回调处理失败", ['msg' => $e->getMessage(), 'params' => $params]); return 'fail'; } }逻辑说明:先验签,不通过直接拒绝。然后查流水做幂等,已处理过就返回 success,避免重复。接着开事务,lock(true)是悲观锁,防止同一订单并发回调。写流水和改状态在同一个事务里,要么都成功,要么都回滚。返回给支付平台的字符串必须是它约定的格式,通常是success或fail,返回 fail 会触发重试,所以只有真正处理失败才返回 fail。参数说明:$params是支付平台 POST 过来的原始数据,out_trade_no是你传给支付平台的单号,trade_no是支付平台的流水号,两者要分清。
4.3 对账与补单机制
回调不是 100% 可靠的,网络问题、服务器重启、代码 bug 都可能导致回调丢失。所以必须有一个对账机制,定时去支付平台查订单状态,把漏掉的补上。常见做法是每 5 分钟跑一次定时任务,查最近 1 小时内状态为“待支付”但支付平台显示已支付的订单,手动触发一次回调处理逻辑。这个补单逻辑要复用上面的handleNotify,只是参数从支付平台查询接口获取,而不是从 POST 获取。对账跑起来后,基本可以做到资金零丢失。
5. 避坑与排查:代付系统上线后最容易翻车的 5 个点
5.1 商品解析突然全部返回空
现象:前一天还好好的,第二天所有商品解析都返回空标题或空价格。原因:淘宝调整了页面结构,g_config变量名变了,或者数据被移到了别的变量里。解决:不要只依赖一个变量名,代码里做多级降级匹配,同时把原始 HTML 存下来,对比改版前后的差异,快速定位新变量名。另外,解析失败要告警,不要等客户投诉才发现。
5.2 支付回调重复触发导致重复加钱
现象:客户付了一笔,但订单的paid_amount变成了两倍。原因:回调处理没有幂等,支付平台重试时重复执行了加钱逻辑。解决:按trade_no做唯一索引,插入流水时如果冲突就说明已处理过,直接返回成功。同时用事务和悲观锁保证并发安全。
5.3 订单状态卡在“已支付”无法流转
现象:客户付了钱,后台也看到已支付,但点“下单”没反应。原因:状态流转方法里TRANSITIONS配置漏了某条路径,或者并发更新时乐观锁返回 0 行,代码没处理这个返回值。解决:检查状态机配置,确保每条合法路径都在;更新时判断affected rows,为 0 时记录日志并提示用户重试。
5.4 中文商品标题入库变成乱码
现象:解析出来的标题在数据库里显示为问号或乱码。原因:数据库连接字符集不是 utf8mb4,或者 cURL 抓回来的 HTML 编码是 GBK 没转码。解决:建库建表统一用 utf8mb4,PHP 连接时设置charset=utf8mb4;抓取后检测 HTML 的 meta charset,如果是 GBK 用mb_convert_encoding转成 UTF-8 再解析。
5.5 定时对账任务把已完成的订单又改回已支付
现象:对账任务跑完后,一些已完成的老订单状态被改乱了。原因:对账查询条件写得太宽,把历史订单也捞出来了,或者补单逻辑没有判断当前状态。解决:对账只查最近 1 小时内的订单,补单前先检查订单当前状态,只有“待支付”才处理,其他状态直接跳过并记日志。
6. 把代付系统做轻:用缓存和队列扛住并发的小技巧
代付系统的并发压力通常集中在两个点:商品解析和支付回调。解析是 IO 密集型,回调是写密集型。我的习惯是,解析层加 Redis 缓存,同一个商品 ID 5 分钟内只抓一次,缓存 key 用tb_item:{item_id},过期时间 300 秒。这样即使前端被刷,也不会把请求全打到淘宝。回调层加一个简单的队列,把回调数据先扔进 Redis List,然后由消费者进程异步处理,避免回调接口响应超时导致支付平台重试。消费者用LPOP取任务,处理失败就重新入队,重试 3 次后落库告警。
<?php // 解析缓存示例 function getItemWithCache(string $url): array { $itemId = extractItemId($url); $cacheKey = "tb_item:{$itemId}"; $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $cached = $redis->get($cacheKey); if ($cached) { return json_decode($cached, true); } $data = fetchTbItem($url); // 只缓存成功解析且标题非空的结果 if (!empty($data['title'])) { $redis->setex($cacheKey, 300, json_encode($data, JSON_UNESCAPED_UNICODE)); } return $data; }这段代码的关键是setex的 300 秒过期,以及只缓存成功结果。如果解析失败也缓存,会导致 5 分钟内一直返回空,客户体验很差。队列那边,消费者进程建议用 Supervisor 守护,挂了自动拉起。验证方法很简单:用ab或wrk压一下回调接口,看响应时间是否稳定在 200ms 以内,如果超过,说明同步处理太重,该上队列了。
最后说个我自己的教训:代付系统刚上线时,我为了图快,把解析和下单放在同一个接口里同步执行,结果淘宝一慢,整个下单接口就超时,客户以为没提交成功,重复提交,产生了大量垃圾订单。后来把解析拆成异步,下单接口只做入库和返回订单号,前端轮询解析结果,问题才解决。做这类系统,宁可多一步异步,也不要让用户等。希望帮到你。
本文还有配套的精品资源,点击获取