ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP对接企业微信开发指南:从access_token到消息回调

PHP对接企业微信开发指南:从access_token到消息回调 做后台开发这几年经常看到群里冒出这类问题“PHP 怎么给企业微信发消息”“为什么我拿到的 access_token 一下子就失效了”“为什么回调地址一直验证不通过”说实话我第一次对接企业微信时也被绕得够呛后台里又是 corpid 又是 agentid又是 secret 又是可信 IP文档写得模棱两可报错信息还特别容易让人瞎猜。后来接二连三做了几个企业内部小工具才把这条链路彻底摸顺。这篇文章就按“庖丁解牛”的思路把 PHP 对接企业微信这件事拆开讲清楚先帮你判断到底该走哪条对接路线再带你把环境、参数、可信 IP 准备好然后手把手把 access_token、消息发送、文件上传、回调接收这几块硬骨头一块块啃下来最后附上一份报错排查清单。适合理没接触过企业微信接口的 PHP 开发者也适合写运维告警、内部通知系统时拿来做参考。1. 先搞清楚对接企业微信到底有几种姿势很多新手上来就搜“企业微信 PHP SDK”其实 SDK 只是把官方接口包装了一层真正让你迷惑的是你到底该对接哪个入口企业微信提供的对接方式不止一种选错了后面所有配置都会对不上。1.1 自建应用企业内部的机器人/小程序在企业管理后台的应用管理里可以创建一个“自建应用”。建好之后你会拿到一组专属参数应用 IDagentid、应用密钥secret再配合企业 IDcorpid就能调用企业微信开放接口给指定成员或部门发送消息也可以接收成员在应用会话里发来的消息、事件回调。自建应用适合做“企业内部工具”比如订单提醒、工单通知、简短审批流、服务器告警。它最大的特点是双向的既能主动推消息给员工也能收到员工在应用里触发的事件。缺点是配置稍微多一点而且消息权限、可见范围都要先设置好。1.2 群机器人 Webhook跳过 access_token 的偷懒方案如果只是想让某个企业微信群收到通知那连应用都不用建直接在群聊里添加一个“群机器人”拿到一个 webhook 地址相当于一个专属的 HTTP 入口。你用 PHP 往这个地址 POST 一段 JSON群里就能收到文本、markdown 甚至图片消息。群机器人最大的优点是简单不需要 corpid、secret不需要获取 access_token连签名都不用做。缺点是只能“推”不能“收”而且消息是以机器人身份发的分不清具体是谁。这种方案特别适合服务器监控告警、定时任务通知、日志汇总这些单向场景。1.3 第三方应用/服务商模式前期先不要碰如果你是给很多不同企业做同一套产品需要拿到企业授权然后以服务商身份调用接口那是另一套逻辑。它涉及企业授权、永久授权码、套件等概念开发复杂度明显更高。零基础阶段别急着碰服务商模式否则光授权流程就能劝退你。先把自建应用和群机器人玩熟后面需要再扩展。三种方式我整理了一下方便你按场景选对接方式需要 access_token能不能主动发消息能不能收消息/事件典型场景开发成本自建应用需要能能配置回调后可以内部通知、工单、审批提醒中群机器人 Webhook不需要只能推不能群告警、定时任务播报低第三方服务商需要且要授权流程能能给多家企业做 SaaS 产品高我在实际项目里有个经验如果需求里带“企业微信”三个字先别急。问清楚是要“群里某个机器人发条通知”还是“公司内部某个应用给指定人发消息”。后者才值得上自建应用的整套配置前者直接用 webhook 半小时就搞定。2. 动手前的前置准备环境、三个参数、可信 IP 一个都不能少确认好对接方式后接下来把最基本的环境和参数准备好。很多人对接失败不是代码问题而是这个阶段漏了东西。2.1 PHP 运行环境怎么选我用的是 phpStudy小皮面板这类集成环境PHP 版本直接选 8.2。企业微信接口走的是 HTTPS所以 PHP 的 curl 扩展和 openssl 扩展必须开启后面做回调解密时openssl 也是刚需。如果你用的是宝塔面板装 PHP 时记得把扩展都勾上。这里有个 Windows 下特别常见的坑在终端运行php -v时提示类似vcruntime140.dll 14.0 is not compatible说明系统里缺 VC 运行库这不是 PHP 本身的问题。先去把微软官方 VC 2015-2022 运行库装上再重开终端就好了。另外改完php.ini后一定要重启 PHP 进程否则扩展不生效排错时会让人怀疑人生。2.2 从管理后台抄下三个关键参数以自建应用为例你需要三件套corpid在管理后台“我的企业”页面底部是企业唯一标识像一串乱码但其实就是个 ID。agentid在“应用管理 → 自建应用 → 你自己的应用”里是一个数字比如 1000002。secret同一个应用详情页里点击“查看”后会显示一串密钥。这个 secret 相当于应用的密码只能放在服务器端绝对不能泄露也不能从前端页面直接调用。我见过不少新人把 secret 复制的时候带上空格或漏掉结尾字符后面所有接口都返回 40001 或 40014排查半天才发现是参数抄错了。建议把这几个参数单独写进一个config.php里统一管理不要把值硬编码在业务代码里。2.3 可信 IP 没配上后面全是 60020在企业微信自建应用的详情页里有一个“企业可信 IP”配置项。这个配置很多人容易忽略它是用来限制“哪些服务器 IP 可以调用这个应用的接口”。你需要填的是服务器的公网出口 IP不是内网 IP也不是绑定域名的那台机器的主机名。判断出口 IP 最简单的方法在服务器上执行curl ifconfig.me之类的命令看返回的地址。拿到后填进应用配置里。没配置或填错的话调用接口时大概率会返回60020“not allow to access from your ip”意思是来源 IP 不在白名单里。需要注意的是改完配置后一般需要等一小段时间才会生效别一改完马上重试失败就以为是自己改错了。3. 庖丁解牛第一步把 access_token 请出来并管好它企业微信几乎所有应用级接口的调用都需要带上 access_token。可以把 access_token 理解成一把“临时钥匙”钥匙本身有有效期而且获取钥匙的接口不能无限制地刷。理解这一层后面写代码就顺手了。3.1 gettoken 接口与最基础的 HTTP 封装获取 token 的接口长这样GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpidIDcorpsecretSECRET返回大致是{ errcode: 0, errmsg: ok, access_token: xxxxxx, expires_in: 7200 }expires_in是 7200 秒也就是 2 小时。实际使用中不能等它真过期了再去刷新最好提前一两分钟换新的防止在临界点出现调用失败。先写一个通用的 HTTP 请求函数。不管你后面要调多少接口这个函数都够用function http_request(string $url, string $method GET, ?array $params null, bool $isJson false): array { $ch curl_init(); curl_setopt_array($ch, [ CURLOPT_URL $url, CURLOPT_RETURNTRANSFER true, CURLOPT_TIMEOUT 10, CURLOPT_SSL_VERIFYPEER true, ]); if ($method POST) { curl_setopt($ch, CURLOPT_POST, true); } if ($params ! null) { if ($isJson) { $data json_encode($params, JSON_UNESCAPED_UNICODE); curl_setopt($ch, CURLOPT_HTTPHEADER, [Content-Type: application/json; charsetutf-8]); } else { $data http_build_query($params); } curl_setopt($ch, CURLOPT_POSTFIELDS, $data); } $resp curl_exec($ch); if (curl_errno($ch)) { $error curl_error($ch); curl_close($ch); return [errcode -1, errmsg curl error: . $error]; } curl_close($ch); $json json_decode($resp, true); return is_array($json) ? $json : [errcode -2, errmsg $resp]; }很多旧代码里CURLOPT_SSL_VERIFYPEER会设成 false本地调试图省事可以但生产环境我还是建议改成 true避免中间人问题。如果服务器上没有配置根证书可以下载一份 cacert.pem然后在 curl 设置里指定CURLOPT_CAINFO指向它。3.2 缓存策略token 有效期 7200 秒不能每次都申请获取 token 的接口本身有调用频率限制而且企业微信也不建议频繁调用。所以 token 一定要缓存。最简单的方案是写到文件里function getAccessToken(): string { $cacheFile __DIR__ . /access_token.json; if (file_exists($cacheFile)) { $cache json_decode(file_get_contents($cacheFile), true); if ($cache isset($cache[expire_time]) $cache[expire_time] time() 120) { return $cache[access_token]; } } $url https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid . CORP_ID . corpsecret . APP_SECRET; $data http_request($url); if (($data[errcode] ?? -1) ! 0) { throw new RuntimeException(获取 access_token 失败 . json_encode($data, JSON_UNESCAPED_UNICODE)); } $cacheData [ access_token $data[access_token], expire_time time() $data[expires_in], ]; file_put_contents($cacheFile, json_encode($cacheData), LOCK_EX); return $data[access_token]; }time() 120这一段就是提前 120 秒过期相当于给 token 留了 2 分钟安全缓冲。生产环境如果有多台服务器建议把 token 存到 Redis 或 Memcached 这种公共存储里避免每台机器各取各的互相把对方打失效。但对零基础项目来说文件缓存已经够用。3.3 关于 token 的三个常见误解第一个误解以为 token 是永久的。实际上它 2 小时就失效失效后接口会返回 42001 或 40014这时候要做的是重新拉取而不是反复用同一个 token 重试。第二个误解以为每次请求前临时获取一次最稳妥。恰恰相反高频调用 gettoken 容易触发频率限制。就算没触发限制每台机器各自刷新也会出现不同机器拿着不同 token 访问的情况反而增加排查成本。第三个误解token 是跟着“应用”走的不是跟着“人”走的。同一个企业的不同自建应用secret 不同拿到的 token 也各不相同。如果应用 A 的 token 拿去调应用 B 的接口大概率会报错。所以代码里一定要把 corpid、agentid、secret 对应清楚别图省事写死在多个文件里导致串了。4. 庖丁解牛第二步把消息发出去token 拿到手以后企业微信这块“骨头”的最核心部分就啃完了。剩下的无非是组合参数、调接口、看返回。4.1 发送文本消息给指定人发送应用消息的接口是POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_tokenACCESS_TOKEN请求体是个 JSON至少包含 touser、msgtype、agentid 以及对应消息类型的结构。一个最小可用的发送函数长这样function sendAppMessage(string $touser, string $content, string $msgType text): array { $accessToken getAccessToken(); $msg [ touser $touser, msgtype $msgType, agentid APP_AGENT_ID, ]; if ($msgType text) { $msg[text] [content $content]; } elseif ($msgType markdown) { $msg[markdown] [content $content]; } $url https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token . $accessToken; return http_request($url, POST, $msg, true); } // 用法示例 $result sendAppMessage(ZhangSan, 你好这是一条来自 PHP 的消息。);touser传的是成员 UserID不是姓名也不是手机号。要发给多个人时可以用竖线分隔比如ZhangSan|LiSi|WangWu。如果想发给所有人传all但这会在全员群里轰炸非必要别乱用。4.2 markdown 格式与富文本消息应用消息支持 markdown 格式这对通知类场景特别实用。比如一条部署成功的告警可以直接写成$content ### 发布通知\n . 项目会员中心\n . 环境生产环境\n . 状态font color\info\发布成功/font\n . 耗时2分15秒\n; sendAppMessage(ZhangSan, $content, markdown);企业微信的 markdown 消息并不是完整的 markdown 语法它支持标题、引用、加粗、字体颜色等有限格式。如果你想发一条更“正式”的卡片式消息还可以试试 textcard 类型带标题、描述和跳转链接适合做审批待办$msg[textcard] [ title 你有新的待办事项, description 请尽快处理 3 条待审批记录, url https://your-domain.com/approval, btntxt 去处理, ];这里会暴露一个touser和可见范围的关系如果接收人不在应用的可见范围内接口会返回60011或48002之类的错误。所以自建应用建好后记得把可见范围设置成目标部门或全员否则写再多代码也是白搭。4.3 发文件和其他媒体先上传素材再发消息企业微信发文件不是直接把本地路径塞到消息里而是分两步先把文件上传到企业微信的临时素材库拿到一个 media_id再通过消息接口把 media_id 发出去。这个流程一开始会让人不太习惯但理解后其实很简单。上传素材接口POST https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_tokenACCESS_TOKENtypefile注意这里是普通的 multipart/form-data 文件上传不是 JSON。PHP 下用 CURLFile 就能搞定function uploadMedia(string $filePath): string { $accessToken getAccessToken(); $url https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token . $accessToken . typefile; $ch curl_init(); $postData [ file new CURLFile($filePath), ]; curl_setopt_array($ch, [ CURLOPT_URL $url, CURLOPT_POST true, CURLOPT_POSTFIELDS $postData, CURLOPT_RETURNTRANSFER true, CURLOPT_TIMEOUT 30, ]); $resp curl_exec($ch); curl_close($ch); $data json_decode($resp, true); if (($data[errcode] ?? -1) ! 0) { throw new RuntimeException(上传素材失败 . json_encode($data, JSON_UNESCAPED_UNICODE)); } return $data[media_id]; } // 上传成功后发送文件消息 $mediaId uploadMedia(/tmp/report.xlsx); $fileMsg [ touser ZhangSan, msgtype file, agentid APP_AGENT_ID, file [media_id $mediaId], ]; http_request( https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token . getAccessToken(), POST, $fileMsg, true );临时素材一般有有效期官方文档写的是 3 天。适合发日报、日志、导出文件这类场景如果你要长期保存素材并重复使用就得考虑上传到企业微信的素材库永久素材那个接口和这里不太一样需要的时候再单独看文档。4.4 返回码检查errcode 不等于 0 就是失败企业微信接口正常时返回{errcode:0,errmsg:ok}很多初学者习惯只看有没有返回数组不检查 errcode。结果明明发送失败代码还在走“成功”分支等到用户反馈收不到消息才去查日志。我在封装接口时有个习惯凡是非 0 的 errcode直接抛异常并记录完整请求参数和返回内容。这样异常场景能被尽早暴露出来而不是静默失败。5. 庖丁解牛第三步接收消息回调与被动回复前面几章都在讲“主动发”这是企业微信对接里相对简单的方向。真正让零基础开发者头疼的是“接收成员发来的消息或事件”也就是回调。其实把回调拆开看也就三件事验证 URL、解密消息、返回响应。5.1 配置 API 接收的完整流程在自建应用详情页找到“接收消息”那一栏点击“设置 API 接收”。需要填写三个东西URL你的服务端接口地址建议直接用 HTTPS比如https://your-domain.com/callback.php。Token你自己定的一个随机字符串用来做签名校验。EncodingAESKey可以随机生成是一把对称加密的密钥用来解密企业微信推给你的消息内容。保存时企业微信会向你的 URL 发送一个 GET 请求带上一堆参数目的是验证这个地址真的属于你。验证通过后后续的消息都会以 POST 方式推送到这个地址。第一次配置时建议先在服务器上放一个最简脚本能正常响应验证请求即可然后再逐步加业务逻辑。不要一上来就把完整的框架业务逻辑堆进去不然连“到底是配置问题还是代码问题”都分不清。5.2 验证 URL 时的签名计算方式企业微信的验证请求会带这几个参数msg_signature、timestamp、nonce、echostr。其中msg_signature是用你的 Token、timestamp、nonce 三者排序后拼接做 SHA1 得到的。所以第一步要做的是算签名第二步是解密echostr并返回明文。缩略代码如下if (isset($_GET[echostr])) { $token 你在后台填写的Token; $encodingAesKey 后台生成的EncodingAESKey; $corpId CORP_ID; $msgSignature $_GET[msg_signature] ?? ; $timestamp $_GET[timestamp] ?? ; $nonce $_GET[nonce] ?? ; $echostr $_GET[echostr] ?? ; // 如果只是校验签名 $tmpArr [$token, $timestamp, $nonce]; sort($tmpArr, SORT_STRING); $signature sha1(implode(, $tmpArr)); if ($signature ! $msgSignature) { http_response_code(403); exit(sign error); } // 实际要用官方加解密库对 echostr 解密并返回明文 // include_once __DIR__ . /WXBizMsgCrypt.php; // $crypt new WXBizMsgCrypt($token, $encodingAesKey, $corpId); // $replyEchoStr ; // $errCode $crypt-VerifyURL($msgSignature, $timestamp, $nonce, $echostr, $replyEchoStr); // if ($errCode 0) echo $replyEchoStr; exit; }官方提供了 PHP 版的加解密库WXBizMsgCrypt直接下载把文件放项目里引用就行。有个坑是旧版本示例代码时间比较早在 PHP 8 下可能会有函数废弃或类名冲突的报错遇到时优先找基于 openssl 的新版改造包不要自己去改 AES 算法。解密的原理没必要死磕理解成“企业微信把内容用 AES 加密后塞给你你拿 key 解开再处理”就够了。5.3 POST 回调消息的解密与被动回复当成员发消息或触发事件时企业微信会向你的 URL 发送 POST 请求请求体是加密后的 XML 字符串。处理流程是固定的接收原始 bodyfile_get_contents(php://input)。提取 URL 上的msg_signature、timestamp、nonce。调用WXBizMsgCrypt的DecryptMsg方法解密得到明文 XML。解析 XML根据MsgType和Event决定业务逻辑。如果需要被动回复把回复内容用EncryptMsg加密后输出。一个典型的解密与响应骨架大概是// 普通消息回调 $rawXml file_get_contents(php://input); $msgSignature $_GET[msg_signature] ?? ; $timestamp $_GET[timestamp] ?? ; $nonce $_GET[nonce] ?? ; $crypt new WXBizMsgCrypt($token, $encodingAesKey, $corpId); $decryptMsg ; $errCode $crypt-DecryptMsg($msgSignature, $timestamp, $nonce, $rawXml, $decryptMsg); if ($errCode ! 0) { http_response_code(403); exit(decrypt error); } // $decryptMsg 此时是明文 XML可以交给业务解析 // 比如xmlToUserNameFromUserNameMsgTypeContent/xml // 解析完若需要自动回复构造回复 XML 后调用 $crypt-EncryptMsg 输出这一部分零基础理解起来确实有门槛但好消息是它“套路化”程度非常高所有企业回调都长一个样子。你只要把骨架跑通后面的业务逻辑都是自由发挥。5.4 回调调试时的三个提醒第一一定要打日志。把原始 body、URL 参数、解密后的 XML、你的处理结果都记录下来。回调出问题时没有日志基本等于盲人摸象。第二不要在本地局域网直接拿这个 URL 去后台保存。企业微信的服务器要能访问到你的地址所以测试阶段最好有一个公网可达的服务器或者使用已经部署好的测试环境。等回调机制完全跑通再回本地开发也不迟。第三响应要干净。验证 URL 和被动回复时PHP 文件不能输出多余的空格、BOM 以及调试打印内容否则企业微信那边会因为响应格式不合法而判定失败。很多“明明配置没问题却一直保存失败”的案例最后都是因为 PHP 文件开头多了一个不可见字符。6. 从零到一最常踩的报错和排查清单技术问题到最后基本都是排查问题。与其等报错来了再慌不如先把最容易踩的坑提前排掉。6.1 常见错误码速查表错误码含义常见原因处理方向40001不合法的 secretsecret 抄错、过期、或不属于该应用重新复制 secret确认和 corpid/agentid 配套40014不合法的 access_tokentoken 传错或已失效检查 token 是否来自当前应用重新获取并缓存41001缺少 access_token 参数URL 拼接少了参数检查请求 URL确认带上了 token42001access_token 过期token 超过 2 小时重新获取检查缓存策略45009接口调用超过限额gettoken 或发送频繁加缓存、控频、减少重复调用48002API 使用权限被禁用应用类型不支持该接口或未在后台开启检查应用类型、权限配置60011没有权限操作该成员touser 不在可见范围去后台调整应用可见范围60020来源 IP 不在白名单可信 IP 没配置或配错填写服务器公网出口 IP 并等待生效这张表覆盖了绝大多数开发阶段会碰到的报错。如果你遇到的错误码不在表里最佳做法是把errcode原样复制到搜索引擎里搜企业微信的文档和社区反馈都能帮你定位不要自己硬猜。6.2 URL 验证一直失败的排查链路如果你在后台保存回调配置时一直提示验证失败按这个顺序检查确认 URL 能被外网访问。直接在浏览器或 curl 访问你的回调地址看能不能正常响应而不是返回 404 或 502。检查 Token 和 EncodingAESKey 是否和后台完全一致。多一个空格都不行。检查 PHP 脚本是否有任何输出污染。包括文件头部的 BOM、编辑器自动加的换行、调试用的 echo。检查服务端日志。把 GET 回调时的参数按刚才说的签名流程自己算一遍看和msg_signature是否一致。如果签名一致但依然失败重点看解密环节。确认使用官方加解密库时的参数顺序、corpid 是否正确。我曾经遇到一个项目本地怎么测都没问题部署到服务器后就验证失败。最后发现是 PHP 文件保存成了 UTF-8 with BOM响应头一开始就多了三个字节。用编辑器把文件转成 UTF-8 without BOM 后问题立刻消失。这种坑很隐性建议一开始就用无 BOM 的格式写代码。6.3 三个值得长期坚持的编码习惯第一个习惯封装的接口函数统一做返回码判断。写一个assertSuccess($result)内部检查errcode 0不是就打日志、抛异常。这样业务代码不会散落大量if ($result[errcode] ! 0)排查也集中。第二个习惯把日志函数提前写好。哪怕只是简单的file_put_contents追加写入也要保证每次调用企业微信接口前后都有记录。特别是回调场景日志就是你夜里被用户喊起来排查时唯一的救兵。第三个习惯上线前做一次“冒烟测试”。发一条真实的文本消息、传一个文件、触发一次回调确认全链路走通再交付。我在实际项目里吃过教训白天改完代码觉得没问题晚上定时任务跑完用户才发现根本没收到消息仔细一看是把测试环境的 secret 带上线了。这种错误如果提前发测试消息一眼就能看出来。企业微信对接这件事说穿了就是把官方接口的流程走熟配置环境、拿凭证、发请求、处理回调做成一个个小模块后你会发现它和对接大部分 HTTP API 没有本质区别。希望这份庖丁解牛式的拆解能帮你少走一点弯路。
返回列表