setWebsite(__NAMESPACE__); $config = config('easywechat.official_account.default'); $this->app = Factory::officialAccount($config); } // 测试登录 public function testLogin() { if ($this->isTest()) { $userId = 1; $productMid = "web"; $loginToken = new LoginTokenService($this->siteInfo['token_table']); $token = $loginToken->createOnlyOneToken($userId, $productMid); return responseMessage(1001, '', $token); } } public function index() { $this->app->server->push(function ($message) { $msgType = $message['MsgType']; if ($msgType == 'event') { $event = $message['Event']; return $this->msgEvent($event, $message); } else { return $this->msgtType($msgType, $message); } }); $response = $this->app->server->serve(); $response->send(); exit(); } public function index2() { $this->app->server->push(function ($message) { $msgType = $message['MsgType']; $event = $message['Event']; $eventKey = $message['EventKey']; $openid = $message['FromUserName']; // 替换字符串 $eventKey = str_replace('qrscene_', '', $eventKey); /** * 扫描带参数二维码事件 */ if ($msgType == 'event') { $msg = ''; switch ($event) { case 'subscribe': // 1. 用户未关注时,进行关注后的事件推送 case 'SCAN': // 2. 用户已关注时的事件推送 // 保存用户信息 $isSuccess = true;//$this->saveUser($openid, $eventKey); if ($isSuccess) { $msg = "您好!欢迎使用 优速办公!"; } else { $msg = "对不起!扫描失败,请重试!"; } break; case 'unsubscribe': // 取消关注 $this->loginOut($openid, $eventKey); break; } return $msg; } else { return "您好!欢迎使用 优速办公!"; } }); $response = $this->app->server->serve(); $response->send(); exit(); } /** * @param $openid * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException */ private function saveUser($openid, $eventKey) { $userInfo = $this->app->user->get($openid); $qr_scene_str = $eventKey; // 场景值为 productMid:sessionId $sceneStr = base64_decode($qr_scene_str); if (empty($sceneStr)) { abort(500, '登录授权失败,非法操作!'); } // 缓存当前的scene,防止用户重复点击登录 $isClick = Cache::get('clickOne_' . md5($sceneStr)); if (!$isClick) { abort(500, '登录授权失败,请重新扫码!'); } else { Cache::forget('clickOne_' . md5($sceneStr)); } // 分离产品id和session [$productMid, $sessionId] = explode(':', $sceneStr, 2); if (empty($productMid) || empty($sessionId)) { abort(500, '登录授权失败,非法操作!'); } Log::info('$openid == ' . $openid); $nickname = '未知'; // 对应微信的 nickname $avatar = 'https://www.yososoft.com/static/images/softlogo.png'; // 头像网址 $unionid = $userInfo['unionid'] ?? ''; $wxInfo = $userInfo; $isExistUser = DB::table('user_wechat_official_account')->select('id')->where('openid', $openid)->first(); if ($isExistUser) { // 更新头像 $isSuccess = DB::transaction(function () use ($openid, $isExistUser, $unionid, $nickname, $avatar, $wxInfo) { DB::table('user_wechat_official_account')->where('id', $isExistUser->id)->update([ 'openid' => $openid, 'unionid' => $unionid, 'nick' => $nickname, 'wx_avatar' => $avatar, 'wx_info' => json_encode($wxInfo), ]); DB::table('user')->where('id', $isExistUser->id)->update([ 'unionid' => $unionid, 'username' => $nickname, 'avatar' => $avatar, ]); return true; }); $userId = $isExistUser->id; // 用户id } else { // 插入数据 $isSuccess = DB::transaction(function () use ($openid, $nickname, $avatar, $unionid, $wxInfo) { $userData = [ 'username' => $nickname, 'avatar' => $avatar, 'roles' => json_encode([1]), 'unionid' => $unionid, 'status' => 1, 'mid' => Str::random(12), 'created_at' => time(), 'updated_at' => time(), ]; $userId = DB::table('user')->insertGetId($userData); $officialData = [ 'user_id' => $userId, 'openid' => $openid, 'unionid' => $unionid, 'nick' => $nickname, 'wx_avatar' => $avatar, 'wx_info' => json_encode($wxInfo), 'mid' => Str::random(12), 'created_at' => time(), 'updated_at' => time(), ]; DB::table('user_wechat_official_account')->insertGetId($officialData); return $userId; }); $userId = $isSuccess; // 用户id } if ($isSuccess) { // 保存生成token需要的信息 $loginToken = new LoginTokenService($this->siteInfo['token_table']); $token = $loginToken->createOnlyOneToken($userId, $productMid); Cache::put('TOKEN_' . $sceneStr, $token, 5 * 60); // 有效期5分钟 Log::info('33333333333333333333 === ' . 'TOKEN_' . $sceneStr . ' ==== ' . $token); // 如果用户直接点击手机登录链接,而没有扫描,则处理一下当前的扫描状态 if (!Cache::get("SCAN_" . $sceneStr)) { Cache::put("SCAN_" . $sceneStr, true, 2 * 60); // 有效期2分钟 } // 网页跳转,带一个随机参数,然后再通过该参数来换取session $key = md5(microtime() . $sceneStr); Cache::put($key, $token, 2 * 60); // 有效期3分钟 return redirect('/mobile/#/?key=' . $key); } else { abort(500, '登录授权失败,请稍后再试!'); } } private function msgEvent($event, $message) { // 场景值为 productMid:sessionId $sceneStr = $message['EventKey'] ?? ''; // 新关注用户有qrscene_,替换掉 $sceneStr = str_replace('qrscene_', '', $sceneStr); // $openid = Request::input('openid'); switch ($event) { case 'subscribe': // 1. 用户未关注时,进行关注后的事件推送 case 'SCAN': // 2. 用户已关注时的事件推送 if (empty($sceneStr)) { return "欢迎你关注助友办公软件"; } else { $oauthUrl = $this->app->oauth->withState(base64_encode($sceneStr))->redirect(); //$oauthUrl = str_replace('https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx1edb103f32029099&redirect_uri=https%3A%2F%2Fwww.qasimblog.com%2Fapi%2Fwechat%2FoauthCallback&response_type=code&scope=snsapi_userinfo&state=', '', $oauthUrl); $items = [ new NewsItem([ 'title' => '', 'description' => '', 'url' => $oauthUrl, 'image' => 'https://www.qasimblog.com/static/login.jpg', ]), new NewsItem([ 'title' => $oauthUrl, 'description' => '', 'url' => $oauthUrl, 'image' => 'https://www.qasimblog.com/static/logo.png', ]), ]; // 记录当前的扫描状态 Cache::put("SCAN_" . $sceneStr, true, 2 * 60); // 有效期2分钟 return new News($items); // 发送登录链接 } case 'unsubscribe': // 取消关注 break; } return 'success'; } private function msgtType($type, $message) { switch ($type) { case 'text': case 'image': case 'voice': //语音消息 case 'video': //视频消息 case 'shortvideo': // 小视频消息 case 'location': // 地理位置消息 case 'link': // 链接消息 default: $items = [ new NewsItem([ 'title' => '官方客服', 'description' => '需要帮助,请联系客服!', 'url' => 'https://work.weixin.qq.com/kfid/kfc5471afeb3f4331af', 'image' => 'https://www.qasimblog.com/static/images/u10.png', ]), ]; $msg = new News($items); } return $msg; } /** * 创建登录的临时二维码 * * @return JsonResponse */ public function createQrcode() { /** * 场景值为 productMid:sessionId */ $sceneStr = Request::post('scene_str'); // if (empty($sceneStr)) { Log::error("scene_str,为空!"); return responseMessage(2001, '参数错误!'); } $arr = explode(':', $sceneStr, 2); if (count($arr) != 2) { Log::error("scene_str,参数错误2,为空!"); return responseMessage(2002, '参数错误!'); } // 缓存当前的scene,防止用户重复点击登录 Cache::put('clickOne_' . md5($sceneStr), true, 5 * 60); $result = $this->app->qrcode->temporary($sceneStr, 5 * 60); if (isset($result['ticket']) && $result['ticket']) { $url = $this->app->qrcode->url($result['ticket']); return responseMessage(1001, '', ['url' => $url, 'expire_seconds' => $result['expire_seconds']]); } else { Log::error("scene_str,参数错误3,请求错误!"); return responseMessage(2002, '参数错误2!'); } } public function oauthCallback() { $code = Request::input('code'); $state = Request::input('state'); if (empty($state)) { abort(500, '登录授权失败,参数错误!'); } // 场景值为 productMid:sessionId $sceneStr = base64_decode($state); if (empty($sceneStr)) { abort(500, '登录授权失败,非法操作!'); } $oauth = $this->app->oauth; $user = $oauth->userFromCode($code); $openid = $user->getId();// 对应微信的 OPENID $nickname = $user->getNickname(); // 对应微信的 nickname $avatar = $user->getAvatar(); // 头像网址 $tokenResponse = $user->getAttribute('token_response'); $unionid = $tokenResponse['unionid'] ?? ''; $wxInfo = $user->getAttributes(); // 缓存当前的scene,防止用户重复点击登录 $isClick = Cache::get('clickOne_' . md5($sceneStr)); if (!$isClick) { abort(500, '登录授权失败,请重新扫码!'); } else { Cache::forget('clickOne_' . md5($sceneStr)); } // 分离产品id和session [$productMid, $sessionId] = explode(':', $sceneStr, 2); if (empty($productMid) || empty($sessionId)) { abort(500, '登录授权失败,非法操作!'); } Log::info('$openid == ' . $openid); $isExistUser = DB::table('user_wechat_official_account')->select('id')->where('openid', $openid)->first(); if ($isExistUser) { // 更新头像 $isSuccess = DB::transaction(function () use ($openid, $isExistUser, $unionid, $nickname, $avatar, $wxInfo) { DB::table('user_wechat_official_account')->where('id', $isExistUser->id)->update([ 'openid' => $openid, 'unionid' => $unionid, 'nick' => $nickname, 'wx_avatar' => $avatar, 'wx_info' => json_encode($wxInfo), ]); DB::table('user')->where('id', $isExistUser->id)->update([ 'unionid' => $unionid, 'username' => $nickname, 'avatar' => $avatar, ]); return true; }); $userId = $isExistUser->id; // 用户id } else { // 插入数据 $isSuccess = DB::transaction(function () use ($openid, $nickname, $avatar, $unionid, $wxInfo) { $userData = [ 'username' => $nickname, 'avatar' => $avatar, 'roles' => json_encode([1]), 'unionid' => $unionid, 'status' => 1, 'mid' => Str::random(12), 'created_at' => time(), 'updated_at' => time(), ]; $userId = DB::table('user')->insertGetId($userData); $officialData = [ 'user_id' => $userId, 'openid' => $openid, 'unionid' => $unionid, 'nick' => $nickname, 'wx_avatar' => $avatar, 'wx_info' => json_encode($wxInfo), 'mid' => Str::random(12), 'created_at' => time(), 'updated_at' => time(), ]; DB::table('user_wechat_official_account')->insertGetId($officialData); return $userId; }); $userId = $isSuccess; // 用户id } if ($isSuccess) { // 保存生成token需要的信息 $loginToken = new LoginTokenService($this->siteInfo['token_table']); $token = $loginToken->createOnlyOneToken($userId, $productMid); Cache::put('TOKEN_' . $sceneStr, $token, 5 * 60); // 有效期5分钟 Log::info('33333333333333333333 === ' . 'TOKEN_' . $sceneStr . ' ==== ' . $token); // 如果用户直接点击手机登录链接,而没有扫描,则处理一下当前的扫描状态 if (!Cache::get("SCAN_" . $sceneStr)) { Cache::put("SCAN_" . $sceneStr, true, 2 * 60); // 有效期2分钟 } // 网页跳转,带一个随机参数,然后再通过该参数来换取session $key = md5(microtime() . $sceneStr); Cache::put($key, $token, 2 * 60); // 有效期3分钟 return redirect('/mobile/#/?key=' . $key); } else { abort(500, '登录授权失败,请稍后再试!'); } } /** * web 网页登录 * * 通过key换取session */ public function key2token() { $key = Request::post('key'); if (empty($key)) { return responseMessage(2001, '非法操作!'); } else { $token = Cache::get($key); if ($token) { return responseMessage(1001, '', $token); } else { return responseMessage(2002, '登录失败,请重试!'); } } } /** * 验证登录 * * @return JsonResponse */ public function checkLogin() { // 注意: 实际上不是scene_str,是sessionId $sessionId = Request::post('scene_str'); if (empty($sessionId)) { return responseMessage(2001, '参数错误!'); } // 过如果用户已经登录,则验证该用户是否需要更新token,7天有效 $loginToken = new LoginTokenService($this->siteInfo['token_table']); if ($tokenInfo = $loginToken->checkLogin()) { //超过 7天 有效期,刷新token $limitTime = time() - $tokenInfo->updated_at; if ($limitTime > 7 * 24 * 3600) { // 需要重新登录,清除登录缓存 $loginToken->destroyCurrentAccessToken(); return responseMessage(2002, '登录已过期,请重新登录!'); } elseif ($limitTime > 3 * 24 * 3600 && $limitTime < 7 * 24 * 3600) { // 大于3天 小于7天,则刷新token $token = $loginToken->createOnlyOneToken($tokenInfo->user_id, $tokenInfo->name); } else { $token = $loginToken->getToken(); } return responseMessage(1001, '已登录!', $token); } else { $token = Cache::get('TOKEN_' . $sessionId); if (empty($token)) { return responseMessage(2003, '未登录!'); } else { // 如果用户退出登录,而且缓存还没有失效,会出现检测已经登陆, // 解决:前端清除token,后端判断是否已经登陆 if ($loginToken->findToken($token)) { return responseMessage(1001, 'success', $token); } else { //清除多余的缓存 Cache::forget('TOKEN_' . $sessionId); Log::info('清除多余的token缓存 TOKEN_' . $sessionId); return responseMessage(2004, '未登录!'); } } } } /** * 检测是否已经扫描 */ public function checkScan() { /** * 场景值为 productMid:sessionId */ $sceneStr = Request::post('scene_str'); // if (empty($sceneStr)) { return responseMessage(2001, '参数错误!'); } $isScan = Cache::get("SCAN_" . $sceneStr); if ($isScan) { // 删除缓存 Cache::forget("SCAN_" . $sceneStr); return responseMessage(1001, '已扫描'); } else { return responseMessage(2003, '等待中'); } } }