简介:这是一套面向PHP开发者、站长与电商创业者的淘宝客商城三合一源码,覆盖淘宝、京东、拼多多多平台导购,整合公众号微信端、H5端与封装APP,并内置三级代理裂变体系,适合需要快速搭建返利/淘客类商城并进行二次开发的用户。压缩包共173个文件,整体仅1.94MB,其中83个PHP文件对应后端业务逻辑,51个PNG图片构成常用素材与界面元素,12个JS与6个CSS负责前端交互和样式,8个HTML页面为主要页面模板,同时附带APK安装包、ini与mobileconfig等配置,便于多端部署。整套源码覆盖从公众号到H5再到APP的完整链路,并含有安装说明与常用图片素材,可以直接部署调试,也能据此理解三级代理体系中的用户归属、佣金分配等核心模块。目前已有1453人学习下载,对于想研究商品接口对接、微信端授权登录或代理分佣逻辑的PHP开发者,是一份完整度较高的实战参考。若搭配LNMP或宝塔环境使用,可快速完成上线前的初始化配置。
1. 三合一淘客系统到底拆了什么
我拿到这套三合一淘客源码第一件事不是解压,而是先看它是否把淘宝客、京东联盟、拼多多多多进宝三个平台的商品和推广链路统一到了同一套 PHP 逻辑里。市面上挂“全渠道淘客”牌子的源码,不少只是单平台 API 对接,后台还留着没写完的占位页面;这套源码则把公众号微信端、H5端和封装 APP 三套入口都补齐,并且带三级代理体系。对技术型运营者来说,最有价值的不是某个模板页有多漂亮,而是统一商品表、联盟 API 适配层、代理关系链结算这三块骨架。适合谁?一类是准备自建淘客商城、想在公众号里沉淀私域流量的操盘手;另一类是想找 PHP 源码做 CPS 系统参考的开发。下面内容按我拆包时的核心逻辑展开,和实际部署时会遇到的环境问题一起讲。
2. PHP商城核心模块:三平台商品与API统一层
2.1 统一商品表:一张表承接淘宝、京东、拼多多
淘宝客的 item_id、京东联盟的 skuId、多多进宝的 goods_id,字段语义完全不同。如果用三套表存,搜索、排序、推荐模块处处要按平台写分支,所以我会先确认源码有没有一张带 platform 字段的统一商品表。没有就自己在迁移脚本里加。
CREATE TABLE `u_goods` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `platform` enum('taobao','jd','pdd') NOT NULL DEFAULT 'taobao' COMMENT '来源平台', `goods_id` varchar(64) NOT NULL COMMENT '平台原始商品ID', `title` varchar(255) NOT NULL COMMENT '商品标题', `cover` varchar(255) DEFAULT NULL COMMENT '主图地址', `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '券后价', `original_price` decimal(10,2) DEFAULT '0.00' COMMENT '原价', `coupon_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠券金额', `commission_rate` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '佣金比率%', `commission` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '预估佣金', `shop_name` varchar(128) DEFAULT NULL COMMENT '店铺名', `promotion_url` text COMMENT '推广长链接或券ID', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1上架 0下架', `create_at` int(11) DEFAULT NULL, `update_at` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_platform_goods` (`platform`,`goods_id`), KEY `idx_price` (`price`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='淘客统一商品表';uk_platform_goods联合唯一索引是关键,淘宝和京东的商品 ID 可能撞号,必须和 platform 拼起来才是唯一值。promotion_url用来缓存联盟返回的推广链接,避免列表页每次请求都去调远端 API。commission_rate存的是接口原始比率,commission是后台算好的预估佣金,列表直接读取这个字段就能做排序。
2.2 API调用层:把三个平台封装成统一类
三平台签名机制差异很大:淘宝 top 协议用 sign 参数,京东要字典排序后 sha256,拼多多走 md5。业务代码里如果到处curl,平台配置一变就要改很多文件。我会在控制器之上放一个客户端类,把转链、详情、搜索收敛成统一入口。
class UnionApiClient { private $config; public function __construct(array $config) { $this->config = $config; } public function getPromotionUrl(string $platform, string $goodsId): string { $method = 'convert_' . strtolower($platform); if (!method_exists($this, $method)) { throw new \InvalidArgumentException('unsupported platform'); } return $this->$method($goodsId); } private function convert_taobao(string $goodsId): string { $params = [ 'method' => 'taobao.tbk.item.convert', 'item_id' => $goodsId, 'adzone_id'=> $this->config['taobao_adzone_id'], 'platform' => '2', ]; return $this->requestTop($params); // 按top协议拼签名后curl } private function convert_jd(string $skuId): string { $req = [ 'skuId' => $skuId, 'positionId' => $this->config['jd_position_id'], ]; return $this->requestJd($req); // sha256签名后POST } private function convert_pdd(string $goodsId): string { $req = [ 'goods_id_list' => [$goodsId], 'pid' => $this->config['pdd_pid'], ]; return $this->requestPdd($req); // md5签名 } }method_exists这段做平台分发,以后加抖音、快手就新增convert_douyin方法,控制器完全不用改。实际使用中,淘宝的adzone_id是必填的;京东的positionId需要在京东联盟后台创建推广位后填进配置;拼多多的goods_id_list必须是数组,且一次最多传20个ID。三个平台的 curl 超时必须显式设成 3 到 5 秒,否则联盟接口卡住会拖垮整个 PHP-FPM 进程。
2.3 微信登录与渠道参数保留
公众号端登录本质是用 code 换 openid,但邀请人渠道参数必须在授权跳转中一直保留,否则代理关系会丢失。下面是一段简化的登录处理。
public function wxLogin(string $code, string $channel = ''): array { $url = sprintf( 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code', $this->config['appid'], $this->config['appsecret'], $code ); $resp = json_decode(file_get_contents($url), true); if (!isset($resp['openid'])) { throw new \RuntimeException('wechat oauth failed'); } $user = $this->userModel->findByOpenid($resp['openid']); if (!$user) { $user = $this->userModel->create([ 'openid' => $resp['openid'], 'channel_code' => $channel, ]); } return $user; }channel一般是分享链接里的?channel=INVITE88,在生成微信授权链接时要把这个值塞进state,回调时从state解出来再传入wxLogin。最容易犯的错是授权回调地址写死,channel在跳转时被丢弃,新用户全部变成顶部代理。所以我会在授权入口把state设置为base64_encode(json_encode(['target' => 来源页, 'channel' => 渠道码])),回调后再拆包,既保留来源页又保留关系链。
3. 公众号、H5、APP三端融合与封装细节
3.1 前端资源栈:从amazeui到自定义样式
压缩包里的amazeui.min.css是移动端优先的 UI 框架,swiper.min.css负责轮播组件,hunki.css是项目自定义皮肤,index.css、app.css补列表页和详情页的布局。把CSS拆成这样,主要是为了按端加载:公众号端带上微信适配样式,APP 内嵌 WebView 时可以只加载基础部分。前端模板的 head 部分我会这样组织。
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <link rel="stylesheet" href="static/css/amazeui.min.css"> <link rel="stylesheet" href="static/css/swiper.min.css"> <link rel="stylesheet" href="static/css/hunki.css"> <link rel="stylesheet" href="static/css/index.css">user-scalable=no防止 APP WebView 里用户双击放大后布局错位。swiper初始化时我一般会加observeParents: true,因为 H5 嵌入 APP 后父容器宽高会随屏幕变化,不监听这个参数的话 slide 宽度会按旧尺寸计算,首屏滚动就会出现空白页。这类坑在公众号和 APP 里表现还不一样:公众号里是轮播不滑,APP 里是轮播只有一屏。
3.2 公众号授权跳转与菜单联动
公众号后台菜单不能直接填 H5 地址,要先经过一个 PHP 控制器拼接 OAuth 授权链接。这个控制器的任务就是保存来源页面,然后跳转到微信授权页。
public function oauth(string $target): void { $state = base64_encode(json_encode([ 'target' => $target, 'channel' => input('get.channel', ''), ])); $params = [ 'appid' => getenv('WX_APPID'), 'redirect_uri' => getenv('SITE_URL') . '/api/oauth/callback', 'response_type' => 'code', 'scope' => 'snsapi_userinfo', 'state' => $state, ]; header('Location: https://open.weixin.qq.com/connect/oauth2/authorize?' . http_build_query($params) . '#wechat_redirect'); exit; }回调拿到state后先json_decode还原target,再调上一节的wxLogin。微信要求scope=snsapi_userinfo才能拿昵称头像,但公众号里用户取消授权时code会失效,所以回调必须处理code无效的场景而不是直接报 500。公众号后台的“网页授权域名”只能填一个,如果 H5 域名和接口域名不一致,所有授权回调都会失败,这种情况在源码部署时最常见。
3.3 APP封装:WebView桥接与缓存清理
封装APP多数是给 H5 套一个 WebView 壳,但H5如何调原生能力需要约定一个桥对象。源码里的app.apk通常是替你把静态资源和 PHP 入口打包到本地,实际上核心逻辑还是远程 PHP 接口。我在做桥接时会用下面这种兼容写法。
function nativeShare(title, url, img) { var payload = { title: title, url: url, img: img }; if (window.HunkiBridge && typeof window.HunkiBridge.share === 'function') { window.HunkiBridge.share(JSON.stringify(payload)); } else if (navigator.userAgent.indexOf('HunkiApp') > -1) { location.href = 'hunki://share?payload=' + encodeURIComponent(JSON.stringify(payload)); } else { // 浏览器环境复制链接 navigator.clipboard.writeText(url); } }window.HunkiBridge是 APP 原生往 WebView window 上注入的对象,检测不到时用 scheme 兜底,再不行就退化成复制链接。这样在纯 H5 调试时不会因为调用 undefined 方法而中断。还有一个必踩的坑:APP 内获取地理位置不要直接调用微信 JS-SDK,因为 WebView 里没有wx对象;常见做法是 APP 原生拿到经纬度后通过同一桥对象注入,H5 端再读取。另外,更新 H5 静态文件后,如果 APP 里还是旧页面,需要清 WebView 缓存或重新打包,并在 CSS/JS 链接上追加版本号,例如index.css?v=20250601。
4. 三级代理关系链与订单佣金结算
4.1 代理层级关系存储与查询
三级代理的核心不是给用户打标签,而是订单产生后能找到最多三个上级。users表里用parent_id指向直接邀请人,level表示层级。查询上级链时不建议使用递归,三层以内两次 LEFT JOIN 就够:
SELECT u1.id AS top_uid, u2.id AS mid_uid, u3.id AS child_uid FROM users u3 LEFT JOIN users u2 ON u2.id = u3.parent_id LEFT JOIN users u1 ON u1.id = u2.parent_id WHERE u3.id = ?这条 SQL 返回的三列分别对应第三、二、一级代理。注意u3是买家自己,而佣金发放对象是u3.parent_id、u2.parent_id、u1.parent_id,如果你直接把u3当成一级代理,就会给自己发佣金。更稳妥的方式是在订单表里冗余四个字段:buyer_uid、p1_uid、p2_uid、p3_uid,下单那一刻就把代理链快照存进去。这样后续订单售后、退款、二次结算时不会因为代理关系变化导致算错人。
4.2 订单回调幂等与佣金分发
淘宝、京东、拼多多的订单回调可以重复推送,且没有统一顺序。必须假设同一个订单会以“支付成功”“退款”“维权”等状态到达多次。我用 Redis 锁做幂等,再用分成比例把佣金拆给上级链。
public function onOrderCallback(array $order): void { if ($order['status'] !== 'PAID') { return; } $lockKey = 'order:' . $order['trade_id']; if (!$this->redis->set($lockKey, '1', ['NX', 'EX' => 30])) { return; // 已处理或正在处理 } $chain = [$order['p1_uid'], $order['p2_uid'], $order['p3_uid']]; $rates = [0.6, 0.3, 0.1]; foreach ($chain as $i => $uid) { if (!$uid) continue; $this->commissionLog->insert([ 'uid' => $uid, 'trade_id' => $order['trade_id'], 'amount' => bcmul($order['commission'], $rates[$i], 2), 'status' => 'freeze', ]); } $this->redis->del($lockKey); }bcmul是必须的,PHP 浮点乘法会出现0.1+0.2那种精度误差,佣金金额不对会被用户投诉。freeze状态说明这笔钱还没有真正可提现,等到订单超过售后周期后再批量把freeze更新为available。这里不能直接在回调里给用户加余额,因为订单后续可能退款。$rates数组的顺序必须和$chain对应,一级代理拿 60%,二级 30%,三级 10%,具体分成比例应该在后台配置而不是写死在代码里。
4.3 提现状态机与微信企业付款
提现不能只有“申请成功”和“打款失败”两个状态。我在实现中至少要四个:pending、paying、success、fail。用户提交申请后先冻结余额,再调用微信企业付款接口。
public function withdraw(int $uid, int $amountCents): array { if (!$this->balance->freeze($uid, $amountCents)) { return ['code' => 1, 'msg' => '余额不足']; } $wid = $this->withdrawModel->insertGetId([ 'uid' => $uid, 'amount' => $amountCents, 'status' => 'pending', ]); $result = $this->wxPay->transfer([ 'partner_trade_no' => 'WD' . $wid, 'openid' => $this->userModel->getOpenid($uid), 'amount' => $amountCents, 'desc' => '淘客佣金提现', ]); $this->withdrawModel->update($wid, [ 'status' => $result['result_code'] === 'SUCCESS' ? 'paying' : 'fail', ]); }注意amount单位是分,partner_trade_no是你自己的单号,不能只是自增ID拼接,最好加上业务前缀和日期。微信支付 v2 接口返回return_code=SUCCESS只代表报文收到,真正结果要看result_code和后续的回调或主动查单。出现AMOUNT_LIMIT是额度受限制,OPENID_ERROR是 openid 和商户号 appid 不匹配,这些错误码要记录到提现日志,运营后台才查得到失败原因。
5. 部署、伪静态与接口缓存速记
5.1 Apache和Nginx伪静态规则
源码自带.htaccess,说明 Apache 下能直接生效。如果部署到 Nginx,需要从站点配置里加伪静态,否则商品详情页全部 404。
# Apache .htaccess RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?/$1 [L]# Nginx rewrite rule location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php?/$1 last; } }两种规则逻辑相似:请求的文件不存在时交给index.php入口处理。Nginx 下配置后要nginx -s reload,再用curl -I http://你的域名/goods/100.html验证返回码。如果返回 404,多半是 PHP 没开pathinfo或 FastCGI 配置里没有把路径参数传给PHP_SELF。
5.2 环境依赖与证书、域名检查
运行这套源码前先确认 PHP 扩展齐全。
php -m | grep -E "curl|openssl|pdo_mysql|redis"缺少 curl 和 openssl 时联盟 API 调用会直接失败;没有 redis 扩展,订单幂等和缓存功能会退化成每次都要穿透数据库。拿到源码后先做一次目录权限检查,storage/、runtime/这类可写目录必须有写入权限。如果调用接口时报cURL error 60,是 CA 证书验证失败,测试环境可在curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false),生产环境必须更新到最新的 CA 证书包。公众号后台的回调域名和SITE_URL也要一致,不然授权流程会断在第一步。
5.3 商品详情与转链接口的Redis缓存
联盟接口有调用频率限制,商品详情页和转链接口属于高频接口,我一般直接在UnionApiClient外层加一层 Redis 缓存。
$cacheKey = 'union:detail:' . $platform . ':' . $goodsId; if ($data = Redis::get($cacheKey)) { return json_decode($data, true); } $data = (new UnionApiClient($config))->getDetail($platform, $goodsId); Redis::setex($cacheKey, 300, json_encode($data));转链接口因为商品优惠券可能被领完,缓存时间不宜太长,设置 5 分钟比较合适;商品信息缓存可以到 10 分钟。后台修改佣金比例后要主动删除商品相关缓存,否则前台展示的预估佣金和实际结算不一致。部署后用wrk -t4 -c100 -d30s https://你的域名/api/goods/detail?id=100压一下,看 Redis 命中率,稳定在 90% 以上说明缓存策略合理;低于 60% 就需要检查是不是每个请求都带了不同的channel参数,导致缓存键碎片化。
本文还有配套的精品资源,点击获取