OfficialNotify.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. <?php
  2. namespace App\Http\Controllers\Api\Wechat;
  3. use App\Http\Controllers\Controller;
  4. use App\Models\Api\Personnel\EmployeeOpenid as EmployeeOpenidModel;
  5. use App\Facades\Servers\Logs\Log;
  6. use App\Servers\Wechat\Official;
  7. class OfficialNotify extends Controller
  8. {
  9. /**
  10. * 公众号关注回调 - 自动绑定用户公众号OpenID
  11. * @author 唐远望
  12. * @version 1.0
  13. * @date 2026-03-10
  14. */
  15. public function callback(EmployeeOpenidModel $EmployeeOpenidModel)
  16. {
  17. // 1. 处理微信服务器验证(GET请求)
  18. if ($_SERVER['REQUEST_METHOD'] === 'GET') {
  19. return $this->checkSignature();
  20. }
  21. // 2. 处理微信事件推送(POST请求)
  22. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  23. return $this->handleEvent($EmployeeOpenidModel);
  24. }
  25. return 'success';
  26. }
  27. /**
  28. * 处理微信事件
  29. */
  30. private function handleEvent($EmployeeOpenidModel)
  31. {
  32. // 获取微信推送的原始数据
  33. $xmlData = file_get_contents('php://input');
  34. $xml = simplexml_load_string($xmlData, 'SimpleXMLElement', LIBXML_NOCDATA);
  35. if (!$xml) {
  36. return 'success';
  37. }
  38. $xmlData = $this->xmlToArray($xmlData);
  39. $response_data_xml = $this->decryptMsg($xmlData['Encrypt']);
  40. $response_data = $this->xmlToArray($response_data_xml);
  41. Log::info('wechat_subscribe_info', '微信公众号事件,解密内容', ['xmlData' => $xmlData, 'response_data' => $response_data]);
  42. // 提取关键信息
  43. $fromUsername = (string)$response_data['FromUserName']; // 用户的公众号OpenID
  44. $toUsername = (string)$response_data['ToUserName']; // 公众号原始ID
  45. $event = (string)$response_data['Event']; // 事件类型
  46. // 处理关注事件
  47. if ($event == 'subscribe') {
  48. try {
  49. // 尝试获取用户UnionID
  50. $Official = new Official();
  51. $official_user_info = $Official->getApp()->user->get($fromUsername);
  52. Log::info('wechat_subscribe_info', '微信公众号事件,获取用户信息', ['FromUserName' => $fromUsername, 'response_data' => $official_user_info]);
  53. $unionid = isset($official_user_info['unionid']) ? $official_user_info['unionid'] : '';
  54. if ($unionid) {
  55. // 1. 有UnionID,直接绑定公众号OpenID
  56. $user_open_data = $EmployeeOpenidModel->where(['unionid' => $unionid])->first();
  57. if ($user_open_data) {
  58. $user_open_data->official_openid = $fromUsername;
  59. $user_open_data->save();
  60. } else {
  61. //新增记录
  62. $insert_data = [
  63. 'unionid' => $unionid,
  64. 'official_openid' => $fromUsername,
  65. 'insert_time' => time(),
  66. 'type' => 1
  67. ];
  68. $EmployeeOpenidModel->insertGetId($insert_data);
  69. }
  70. } else {
  71. Log::info('wechat_subscribe_error', '获取UnionID失败', [
  72. 'data' => $fromUsername,
  73. 'request_data' => $official_user_info
  74. ]);
  75. }
  76. } catch (\Exception $e) {
  77. Log::error('wechat_subscribe_exception', '处理异常', [
  78. 'error' => $e->getMessage()
  79. ]);
  80. }
  81. }
  82. // 重要:必须返回success
  83. return 'success';
  84. }
  85. /**
  86. * 解密消息
  87. */
  88. private function decryptMsg($encrypt)
  89. {
  90. try {
  91. // Base64解码
  92. $encrypted = base64_decode($encrypt);
  93. $encodingAesKey = config('wechat.openplat.aes_key', '');
  94. $appId = config('wechat.openplat.app_id', '');
  95. // 解密
  96. $key = base64_decode($encodingAesKey . '=');
  97. $iv = substr($key, 0, 16);
  98. $decrypted = openssl_decrypt(
  99. $encrypted,
  100. 'AES-256-CBC',
  101. $key,
  102. OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
  103. $iv
  104. );
  105. // 去除填充的字符
  106. $pad = ord(substr($decrypted, -1));
  107. $decrypted = substr($decrypted, 0, -$pad);
  108. // 解析内容
  109. $content = substr($decrypted, 16); // 去掉16个随机字节
  110. $xmlLen = unpack('N', substr($content, 0, 4))[1];
  111. // 验证AppId
  112. $appId = substr($content, 4 + $xmlLen);
  113. if ($appId != $appId) {
  114. Log::error('wechat_appid_error', 'AppId不匹配', ['got' => $appId, 'expect' => $appId]);
  115. return false;
  116. }
  117. // 返回XML内容
  118. return substr($content, 4, $xmlLen);
  119. } catch (\Exception $e) {
  120. Log::error('wechat_decrypt_error', '解密异常', ['error' => $e->getMessage()]);
  121. return false;
  122. }
  123. }
  124. /**
  125. * 验证服务器地址有效性
  126. */
  127. private function checkSignature()
  128. {
  129. $signature = $_GET["signature"] ?? '';
  130. $timestamp = $_GET["timestamp"] ?? '';
  131. $nonce = $_GET["nonce"] ?? '';
  132. $echostr = $_GET["echostr"] ?? '';
  133. if (!$signature || !$timestamp || !$nonce) {
  134. return 'Invalid request';
  135. }
  136. $token = config('wechat.openplat.token', 'your_token_here');
  137. $tmpArr = [$token, $timestamp, $nonce];
  138. sort($tmpArr, SORT_STRING);
  139. $tmpStr = sha1(implode($tmpArr));
  140. if ($tmpStr == $signature) {
  141. return $echostr; // 验证成功,返回echostr
  142. } else {
  143. return 'Invalid signature';
  144. }
  145. }
  146. /**
  147. * XML转数组
  148. * @param string $xml XML字符串
  149. * @return array
  150. */
  151. private function xmlToArray($xml)
  152. {
  153. // 禁止引用外部xml实体
  154. libxml_disable_entity_loader(true);
  155. // 加载XML字符串
  156. $xmlObject = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
  157. if (!$xmlObject) {
  158. return [];
  159. }
  160. // 将SimpleXMLElement对象转换为JSON,再转换为数组
  161. $json = json_encode($xmlObject);
  162. $array = json_decode($json, true);
  163. return $array ?: [];
  164. }
  165. }