
简介这是一套面向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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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%ssecret%scode%sgrant_typeauthorization_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一般是分享链接里的?channelINVITE88在生成微信授权链接时要把这个值塞进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 nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno link relstylesheet hrefstatic/css/amazeui.min.css link relstylesheet hrefstatic/css/swiper.min.css link relstylesheet hrefstatic/css/hunki.css link relstylesheet hrefstatic/css/index.cssuser-scalableno防止 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。微信要求scopesnsapi_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?v20250601。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.10.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_codeSUCCESS只代表报文收到真正结果要看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?id100压一下看 Redis 命中率稳定在 90% 以上说明缓存策略合理低于 60% 就需要检查是不是每个请求都带了不同的channel参数导致缓存键碎片化。本文还有配套的精品资源点击获取