
简介这是一套基于ThinkPHP6.0开发的微信公众号多域名回调系统面向中高级PHP开发者及微信生态集成工程师解决公众号后台仅允许配置2个授权域名、支付宝开放平台仅支持1个回调域名等平台限制问题。系统提供三大核心能力多域名统一回调代理、access_token集中管理与分发、微信/企业微信消息事件多路转发显著提升多站点共用同一公众号或支付宝应用的可行性与稳定性。资源包含430个文件以329个PHP业务逻辑文件为主体辅以17个Markdown说明文档、13个HTML管理界面模板、12个JSON配置文件及SQL数据库脚本等结构完整、模块清晰总大小949KB。已有2736人学习下载可直接部署于PHP7.4MySQL5.6环境开箱即用附带白名单管理、登录审计、伪静态配置指引等实用功能适合需快速落地多站微信集成的企业级项目或技术验证场景。1. 微信公众号多域名回调系统不是绕过限制而是重构授权链路微信公众号后台的「JS接口安全域名」和「网页授权域名」各仅允许配置2个这个硬性限制让很多中大型企业束手无策——一个品牌下多个子站、活动页、H5应用、小程序跳转页全得挤在这两个名额里。更麻烦的是每次增删域名都要人工审核上线节奏被卡死。但本系统并非用技术手段“绕开”微信校验而是通过反向代理统一鉴权事件分发三层架构把“域名白名单”从微信侧转移到自身业务系统中。它本质是一个轻量级的微信网关服务所有外部站点不再直连微信API而是统一接入该系统由系统完成token中控、签名验证、事件路由与回调分发。适用于已有多个独立站点如营销页、会员中心、CRM后台需共用同一公众号能力的场景也适合需要集中审计登录行为、统一管理access_token生命周期的运维团队。2. ThinkPHP6.0网关架构设计与核心模块拆解2.1 为什么选ThinkPHP6.0而非Laravel或原生PHP本系统选择ThinkPHP6.0核心在于其对「中间件管道」和「多应用模式」的成熟支持这对构建网关类服务至关重要。微信回调请求具有强时效性签名验证必须在5秒内完成、高并发特征活动爆发期每秒数百次事件推送而TP6的middleware机制可将验签、解密、日志记录、限流等逻辑以插件方式注入请求生命周期避免业务代码污染。更重要的是TP6的app/multi多应用目录结构天然适配「多租户域名白名单」管理——每个授权域名可映射为独立应用实例共享核心网关逻辑隔离配置与路由。对比Laravel需手动实现多租户路由绑定TP6通过config/app.php中的default_app与multi_app开关即可切换而原生PHP则需自行维护路由分发器与上下文隔离开发成本陡增。此外TP6对Swoole协程支持完善后续可平滑升级为常驻进程提升吞吐这是当前版本已预留的扩展路径。2.2 域名白名单管理模块动态路由与HTTPS强制校验系统通过数据库表wx_domain_whitelist存储授权域名字段包括id,domain,status(1启用/0禁用),created_at,updated_at。关键设计在于路由注册时机非在框架启动时静态加载而是在每次HTTP请求进入app/middleware/DomainCheck.php中间件时从Redis缓存读取白名单缓存失效时间设为300秒若缓存未命中则查询MySQL并写入。此举避免每次请求都查库同时保证域名变更5分钟内生效。// app/middleware/DomainCheck.php ?php declare (strict_types 1); namespace app\middleware; use think\facade\Cache; use think\facade\Db; use think\Request; class DomainCheck { public function handle(Request $request, \Closure $next) { $host $request-host(); // 从Redis获取白名单键名为 wx:domain:whitelist $whitelist Cache::get(wx:domain:whitelist, []); if (empty($whitelist)) { $whitelist Db::name(domain_whitelist) -where(status, 1) -column(domain); // 返回一维数组 [site-a.com, site-b.net] Cache::set(wx:domain:whitelist, $whitelist, 300); } if (!in_array($host, $whitelist)) { return json([code 403, msg Domain not authorized]); } // 强制HTTPS校验微信回调要求必须HTTPS if (!$request-isSecure()) { return json([code 400, msg HTTPS required]); } return $next($request); } }注意$request-host()返回的是HTTP头中的Host字段攻击者可能伪造。因此生产环境必须配合Nginx配置proxy_set_header Host $host;并关闭underscores_in_headers off;防止Header注入。同时数据库中存储的域名应标准化为小写且不带协议如example.com而非https://example.com避免因大小写或协议差异导致匹配失败。2.3 access_token中控模块分布式锁与双缓存策略微信access_token有效期2小时且调用/cgi-bin/token接口有频率限制每日2000次。若多个业务站点各自刷新极易触发限频或token覆盖。本系统采用「本地内存Redis双缓存」策略首次请求时由app/service/WxTokenService.php生成唯一锁键wx:token:lock使用Redis的SET key value NX PX 10000命令加锁10秒超时成功后调用微信API获取新token并同时写入PHP的apcu_add()本地缓存TTL7000秒和Rediswx:token:valueTTL7200秒。后续请求优先读APCu未命中再读Redis双重保障毫秒级响应。// app/service/WxTokenService.php ?php namespace app\service; use think\facade\Cache; use think\facade\Db; use think\facade\Log; class WxTokenService { private $cacheKey wx:token:value; private $lockKey wx:token:lock; public function getToken() { // Step 1: 尝试读APCu $localToken apcu_fetch(wx_token); if ($localToken !empty($localToken[access_token])) { return $localToken; } // Step 2: 尝试读Redis $redisToken Cache::get($this-cacheKey); if ($redisToken time() $redisToken[expires_in] $redisToken[created_at]) { apcu_store(wx_token, $redisToken, 7000); // 同步到本地 return $redisToken; } // Step 3: 获取分布式锁 $lockValue uniqid(, true); $lockResult Cache::store(redis)-set($this-lockKey, $lockValue, 10); if (!$lockResult) { // 锁获取失败等待100ms后重试避免雪崩 usleep(100000); return $this-getToken(); } try { // Step 4: 调用微信API $appid config(wx.appid); $secret config(wx.secret); $url https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid{$appid}secret{$secret}; $response json_decode(file_get_contents($url), true); if (isset($response[access_token])) { $tokenData [ access_token $response[access_token], expires_in $response[expires_in], created_at time() ]; // 写入RedisTTL7200秒预留200秒缓冲 Cache::set($this-cacheKey, $tokenData, 7200); // 写入APCu apcu_store(wx_token, $tokenData, 7000); return $tokenData; } else { Log::error(WX Token fetch failed: . json_encode($response)); throw new \Exception(WX API token error); } } finally { // 释放锁Lua脚本保证原子性 $lua if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; Cache::store(redis)-eval($lua, [$this-lockKey], [$lockValue]); } } }提示APCu缓存在PHP-FPM多进程环境下是进程隔离的因此每个Worker进程都有自己的缓存副本。此处apcu_store仅作加速不作为主数据源Redis才是权威存储。若服务器重启导致APCu清空系统会自动降级为只读Redis不影响功能。3. 多域名回调分发与消息事件路由实战3.1 回调入口统一化Nginx反向代理与路径重写微信后台配置的服务器URL只能是单个域名如https://gateway.example.com/wx/callback但实际需承接来自site-a.com、site-b.net等不同域名的请求。解决方案是在Nginx层做路径重写将各域名的请求映射为带X-Original-Host头的统一路径# Nginx配置片段 server { listen 443 ssl; server_name site-a.com; ssl_certificate /path/to/site-a.crt; ssl_certificate_key /path/to/site-a.key; location / { proxy_pass https://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Original-Host site-a.com; # 关键透传原始域名 proxy_set_header X-Forwarded-Proto $scheme; } } server { listen 443 ssl; server_name site-b.net; ssl_certificate /path/to/site-b.crt; ssl_certificate_key /path/to/site-b.key; location / { proxy_pass https://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Original-Host site-b.net; # 透传原始域名 proxy_set_header X-Forwarded-Proto $scheme; } }注意X-Original-Host头必须由Nginx显式设置不能依赖$host变量因为$host在反向代理后变为上游地址127.0.0.1。此头将被PHP代码读取用于匹配白名单及路由分发。3.2 消息事件分发引擎基于XML解析与异步队列微信推送的用户消息文本、图片、事件为XML格式需解析后分发至对应域名的业务URL。系统在app/controller/WxCallbackController.php中实现// app/controller/WxCallbackController.php ?php namespace app\controller; use think\Controller; use think\facade\Cache; use think\facade\Db; use think\queue\Job; class WxCallbackController extends Controller { public function index() { // 1. 验证签名微信标准流程 $signature input(signature); $timestamp input(timestamp); $nonce input(nonce); $echostr input(echostr); $token config(wx.token); if (!$this-checkSignature($signature, $timestamp, $nonce, $token)) { return invalid signature; } // 2. 处理验证请求 if ($echostr !$this-isPostRequest()) { return $echostr; } // 3. 解析POST XML $xml file_get_contents(php://input); $data simplexml_load_string($xml, SimpleXMLElement, LIBXML_NOCDATA); if (!$data) { return invalid xml; } // 4. 提取原始域名来自Nginx X-Original-Host $originalHost $_SERVER[HTTP_X_ORIGINAL_HOST] ?? ; if (empty($originalHost)) { return missing X-Original-Host; } // 5. 查询该域名对应的业务回调URL $callbackUrl Db::name(domain_callback)-where(domain, $originalHost)-value(callback_url); if (!$callbackUrl) { return no callback url configured for . $originalHost; } // 6. 异步分发避免阻塞微信响应 dispatch(new \app\job\WxMessageDispatch([ xml $xml, callback_url $callbackUrl, original_host $originalHost ])); // 7. 立即返回success微信要求2秒内响应 return success; } private function checkSignature($signature, $timestamp, $nonce, $token) { $tmpArr [$token, $timestamp, $nonce]; sort($tmpArr, SORT_STRING); $tmpStr implode($tmpArr); $tmpStr sha1($tmpStr); return $tmpStr $signature; } private function isPostRequest() { return $_SERVER[REQUEST_METHOD] POST; } }3.3 异步分发任务Curl并发与失败重试机制app/job/WxMessageDispatch.php负责将XML消息投递给业务方URL。为防止单点故障采用Curl并发指数退避重试// app/job/WxMessageDispatch.php ?php namespace app\job; use think\queue\Job; use think\facade\Log; use think\facade\Cache; class WxMessageDispatch { public function fire(Job $job, $data) { $xml $data[xml]; $url $data[callback_url]; $host $data[original_host]; // 最多重试3次间隔1s, 2s, 4s for ($i 0; $i 3; $i) { $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); curl_setopt($ch, CURLOPT_HTTPHEADER, [Content-Type: text/xml]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 生产环境应开启证书校验 $response curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode 200) { Log::info(Wx message dispatched to {$url} successfully); $job-delete(); // 成功则删除任务 return; } // 指数退避 $sleepTime pow(2, $i) * 1000000; // 微秒 usleep($sleepTime); } // 3次失败后记录到失败队列供人工干预 $failKey wx:dispatch:fail: . date(Ymd); Cache::push($failKey, [ url $url, host $host, xml substr($xml, 0, 200) . ..., failed_at date(Y-m-d H:i:s) ]); Log::error(Wx message dispatch failed after 3 retries: {$url}); $job-delete(); // 删除避免重复执行 } }提示CURLOPT_SSL_VERIFYPEER在测试环境可设为false但生产环境必须设为true并配置CURLOPT_CAINFO指向可信CA证书包否则存在中间人攻击风险。微信官方证书链可通过openssl s_client -connect api.weixin.qq.com:443 -showcerts导出。4. 支付宝开放平台多域名适配与跨平台统一网关4.1 支付宝回调兼容设计签名算法差异与参数映射支付宝开放平台的回调URL同样受限每个AppID仅1个授权域名但其签名机制与微信不同微信用SHA1支付宝用RSA-SHA256。本系统通过抽象app/service/AlipayCallbackHandler.php统一处理// app/service/AlipayCallbackHandler.php ?php namespace app\service; use think\facade\Db; use think\facade\Log; class AlipayCallbackHandler { public function verifyAndRoute($params) { // 1. 提取支付宝公钥从数据库按app_id查询 $appId $params[app_id] ?? ; $pubKey Db::name(alipay_app)-where(app_id, $appId)-value(public_key); if (!$pubKey) { Log::error(Alipay app_id {$appId} not found); return false; } // 2. 构造待签名字符串支付宝规则参数名升序拼接 $signData $this-buildSignString($params); $sign $params[sign] ?? ; // 3. RSA验签 $res openssl_pkey_get_public($pubKey); $result openssl_verify($signData, base64_decode($sign), $res, OPENSSL_ALGO_SHA256); openssl_free_key($res); if ($result ! 1) { Log::error(Alipay sign verify failed for app_id {$appId}); return false; } // 4. 查询该app_id对应的业务域名 $domain Db::name(alipay_app)-where(app_id, $appId)-value(domain); if (!$domain) { Log::error(Alipay domain not configured for app_id {$appId}); return false; } // 5. 转发至业务URL复用WxMessageDispatch逻辑 dispatch(new \app\job\AlipayMessageDispatch([ params $params, callback_url https://{$domain}/alipay/callback ])); return true; } private function buildSignString($params) { // 过滤sign、sign_type等签名无关参数 $filtered []; foreach ($params as $k $v) { if (!in_array($k, [sign, sign_type])) { $filtered[$k] $v; } } ksort($filtered); $str ; foreach ($filtered as $k $v) { $str . {$k}{$v}; } return rtrim($str, ); } }注意支付宝回调参数中notify_time、notify_type、notify_id等字段需保留业务方可能依赖这些字段判断通知类型如trade_status_sync。系统不做参数过滤仅做路由分发确保语义完整性。4.2 统一网关控制台白名单管理与实时日志追踪系统提供Web界面/admin/domain管理域名白名单支持批量导入、状态切换、操作日志审计。关键功能是实时回调日志追踪通过WebSocket推送最新100条记录// public/static/js/admin/log-tracker.js $(document).ready(function() { const socket new WebSocket(wss:// window.location.host /ws/log); socket.onmessage function(event) { const log JSON.parse(event.data); const row tr td${log.timestamp}/td td${log.domain}/td td${log.type}/td !-- wx or alipay -- td${log.status}/td !-- success or failed -- td${log.response_time}ms/td /tr ; $(#log-table tbody).prepend(row); // 限制显示100行 if ($(#log-table tbody tr).length 100) { $(#log-table tbody tr:last).remove(); } }; socket.onopen function() { console.log(Log websocket connected); }; });后端WebSocket服务app/command/WebSocketCommand.php监听Redis频道wx:log:channel由各分发任务发布日志// 在WxMessageDispatch::fire()末尾添加 Cache::store(redis)-publish(wx:log:channel, json_encode([ timestamp date(Y-m-d H:i:s), domain $host, type wx, status $httpCode 200 ? success : failed, response_time round((microtime(true) - $startTime) * 1000) ]));5. 生产部署关键配置与HTTPS证书自动化5.1 Nginx伪静态与ThinkPHP6.0运行目录规范ThinkPHP6.0要求Web服务器根目录指向public/且需启用PATH_INFO支持。Nginx配置必须包含以下关键指令server { listen 443 ssl http2; server_name gateway.example.com; root /var/www/wechat-gateway/public; # 必须指向public目录 index index.php; # 伪静态规则ThinkPHP6标准 location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; # 关键传递PATH_INFO给PHP fastcgi_split_path_info ^(.\.php)(/.)$; fastcgi_param PATH_INFO $fastcgi_path_info; } # 防止敏感文件被访问 location ~* \.(git|htaccess|env|xml.dist|editorconfig|gitattributes|gitignore)$ { deny all; } }提示fastcgi_split_path_info指令必须存在否则$_SERVER[PATH_INFO]为空TP6路由无法解析。若使用PHP-FPM池隔离需在www.conf中设置security.limit_extensions .php .php7 .php8避免任意文件执行。5.2 多域名SSL证书自动化Certbot与Nginx热重载面对数十个子域名手动维护证书不可行。采用Certbot自动续期并通过--deploy-hook触发Nginx重载# 一次性申请所有域名证书 sudo certbot certonly \ --nginx \ -d site-a.com \ -d site-b.net \ -d site-c.org \ --deploy-hook sudo nginx -s reload # 设置自动续期加入crontab # 0 12 * * * /usr/bin/certbot renew --quiet --post-hook nginx -s reload证书文件路径需在Nginx中统一引用ssl_certificate /etc/letsencrypt/live/gateway.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/gateway.example.com/privkey.pem;注意Certbot的--deploy-hook在证书更新后执行但若Nginx配置语法错误nginx -s reload会失败并回滚。建议在hook中加入语法检查--deploy-hook nginx -t nginx -s reload || echo Nginx config error。5.3 MySQL表结构与索引优化要点系统核心表wx_domain_whitelist需高频查询必须建立复合索引CREATE TABLE wx_domain_whitelist ( id int(11) unsigned NOT NULL AUTO_INCREMENT, domain varchar(128) NOT NULL DEFAULT , status tinyint(1) NOT NULL DEFAULT 1, created_at int(11) NOT NULL DEFAULT 0, updated_at int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_domain_status (domain,status) -- 覆盖查询条件 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;对于日志表wx_login_log记录扫码登录行为按日期分区提升查询效率CREATE TABLE wx_login_log ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL DEFAULT , domain varchar(128) NOT NULL DEFAULT , ip varchar(45) NOT NULL DEFAULT , created_at int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id, created_at), KEY idx_openid (openid), KEY idx_domain_created (domain, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 PARTITION BY RANGE (created_at) ( PARTITION p2023 VALUES LESS THAN (1704067200), -- 2023-01-01 PARTITION p2024 VALUES LESS THAN (1735689600), -- 2024-01-01 PARTITION p2025 VALUES LESS THAN MAXVALUE );提示created_at字段存储Unix时间戳int类型比datetime类型节省空间且索引效率更高。分区键必须是主键或唯一索引的一部分因此主键定义为PRIMARY KEY (id, created_at)。本文还有配套的精品资源点击获取