新增功能

This commit is contained in:
Your Name
2026-04-01 09:46:25 +08:00
parent 099bc1dd22
commit a780356908
332 changed files with 7845 additions and 866 deletions
@@ -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];
}
}