Files
zyt/server/app/common/service/wechat/WechatWorkAppMessageService.php
T
2026-05-11 17:49:38 +08:00

193 lines
6.8 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\common\service\wechat;
use think\facade\Cache;
use think\facade\Log;
/**
* 企业微信自建应用:向成员发送应用文本消息
*
* 依赖配置(与登录/绑定可共用 corp_id;发消息必须用「同一自建应用」的 AgentId + Secret):
* - WECHAT_WORK_CORP_ID 或 work_wechat.corp_id
* - WECHAT_WORK_AGENT_ID 或 work_wechat.agent_id
* - WECHAT_WORK_AGENT_SECRET 或 work_wechat.agent_secret(推荐;勿与仅用于网页授权的 secret 混用)
* - 若未配 agent_secret,会回退 work_wechat.secret(若仍无法发消息,请在管理后台复制该应用的 Secret 填到 agent_secret
*
* @see https://developer.work.weixin.qq.com/document/path/90236
*/
class WechatWorkAppMessageService
{
/**
* @return array{0: string, 1: int, 2: string} [corpId, agentId, secret]
*/
private static function resolveMessagingCredentials(): array
{
$corpId = (string) env('WECHAT_WORK_CORP_ID', '');
if ($corpId === '') {
$corpId = (string) env('work_wechat.corp_id', '');
}
$agentIdStr = (string) env('WECHAT_WORK_AGENT_ID', '');
if ($agentIdStr === '') {
$agentIdStr = (string) env('work_wechat.agent_id', '');
}
$agentId = (int) $agentIdStr;
$secret = (string) env('WECHAT_WORK_AGENT_SECRET', '');
if ($secret === '') {
$secret = (string) env('work_wechat.agent_secret', '');
}
if ($secret === '') {
$secret = (string) env('work_wechat.secret', '');
}
return [$corpId, $agentId, $secret];
}
private static function getAccessToken(string $corpId, string $secret): ?string
{
if ($corpId === '' || $secret === '') {
return null;
}
$cacheKey = 'work_wechat_app_msg_token_' . md5($corpId . $secret);
$cached = Cache::get($cacheKey);
if ($cached) {
return (string) $cached;
}
$url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$corpId}&corpsecret={$secret}";
$res = self::httpGetJson($url);
if (!$res || (int) ($res['errcode'] ?? -1) !== 0) {
Log::error('企业微信应用 access_token 失败: ' . ($res['errmsg'] ?? '无响应'));
return null;
}
$token = (string) ($res['access_token'] ?? '');
if ($token === '') {
return null;
}
$ttl = (int) ($res['expires_in'] ?? 7200) - 200;
if ($ttl > 0) {
Cache::set($cacheKey, $token, $ttl);
}
return $token;
}
/**
* @return array<string,mixed>|null
*/
private static function httpGetJson(string $url): ?array
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$result = curl_exec($ch);
curl_close($ch);
if ($result === false || $result === '') {
return null;
}
$decoded = json_decode($result, true);
return is_array($decoded) ? $decoded : null;
}
/**
* @return array<string,mixed>|null
*/
private static function httpPostJson(string $url, array $body): ?array
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json; charset=utf-8']);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$result = curl_exec($ch);
curl_close($ch);
if ($result === false || $result === '') {
return null;
}
$decoded = json_decode($result, true);
return is_array($decoded) ? $decoded : null;
}
/**
* 向指定成员发送文本消息(userid 为企业微信成员账号,与后台 work_wechat_userid 一致)
*
* @return array{ok: bool, message?: string, errcode?: int}
*/
public static function sendTextToUser(string $toUser, string $content): array
{
$toUser = trim($toUser);
$content = trim($content);
if ($toUser === '' || $content === '') {
return ['ok' => false, 'message' => '接收人或消息内容为空'];
}
[$corpId, $agentId, $secret] = self::resolveMessagingCredentials();
if ($corpId === '' || $agentId <= 0 || $secret === '') {
Log::warning('企业微信应用消息未配置完整(corp_id/agent_id/secret),跳过发消息');
return [
'ok' => false,
'message' => '服务端未配置企业微信应用消息(需 corp_id、agent_id 及该应用的 Secret,建议配置 WECHAT_WORK_AGENT_SECRET',
];
}
$token = self::getAccessToken($corpId, $secret);
if (!$token) {
return ['ok' => false, 'message' => '获取企业微信 access_token 失败,请查看服务端日志'];
}
$url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=' . urlencode($token);
$body = [
'touser' => $toUser,
'msgtype' => 'text',
'agentid' => $agentId,
'text' => ['content' => $content],
'safe' => 0,
];
$res = self::httpPostJson($url, $body);
if (!$res) {
Log::error('企业微信发消息无响应');
return ['ok' => false, 'message' => '企业微信接口无响应'];
}
$err = (int) ($res['errcode'] ?? -1);
if ($err !== 0) {
$errmsg = (string) ($res['errmsg'] ?? '');
Log::error('企业微信发消息失败: ' . $errmsg . ' errcode=' . $err);
$hint = '';
if ($err === 81013 || $err === 60111) {
$hint = '请核对开方人绑定的 userid 与企业微信成员账号一致,且成员已安装/可见该应用。';
} elseif ($err === 60020) {
$hint = '请将服务器出口 IP 加入企业微信应用可信 IP。';
} elseif ($err === 60011) {
$hint = '请检查应用是否具备发消息能力及可见范围是否包含该成员。';
}
return [
'ok' => false,
'message' => '企业微信返回:' . $errmsg . 'errcode=' . $err . '' . ($hint !== '' ? ' ' . $hint : ''),
'errcode' => $err,
];
}
return ['ok' => true];
}
}