OfficialNotify.php 6.8 KB

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