新增功能
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
<?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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user