action() 比较,不区分大小写)。 * 路由/网关若把 action 变成全小写,严格 in_array('bindWorkWechat') 会失败,导致绑定接口被误判为 code=10,扫码页反复刷新。 */ public static function isWorkWechatBindExemptActionName(string $action): bool { $a = strtolower(trim($action)); return in_array($a, [ 'bindworkwechat', 'unbindworkwechat', 'myself', 'logout', ], true); } /** * @notes 管理员账号登录 * @param $params * @return false|mixed * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException * @author 令狐冲 * @date 2021/6/30 17:00 */ public function login($params) { $time = time(); $admin = Admin::where('account', '=', $params['account'])->find(); //用户表登录信息更新 $admin->login_time = $time; $admin->login_ip = request()->ip(); $admin->save(); //设置token $adminInfo = AdminTokenService::setToken($admin->id, $params['terminal'], $admin->multipoint_login); //返回登录信息 $avatar = $admin->avatar ? $admin->avatar : Config::get('project.default_image.admin_avatar'); $avatar = FileService::getFileUrl($avatar); $row = [ 'name' => $adminInfo['name'], 'avatar' => $avatar, 'role_name' => $adminInfo['role_name'], 'token' => $adminInfo['token'], 'is_paw' => $admin->is_paw ?? 1, // 0=需要修改密码,1=正常 ]; $row['need_bind_work_wechat'] = self::adminMustBindWorkWechat([ 'root' => (int) ($admin->root ?? 0), 'work_wechat_userid' => (string) ($admin->work_wechat_userid ?? ''), ]); return $row; } /** * 从 auth/getuserinfo 响应解析企业成员 userid。 * 非通讯录成员时接口可能只返回 openid / external_userid,无 userid。 */ public static function workWechatUserIdFromAuthResponse(array $response): string { return trim((string) ($response['userid'] ?? $response['UserId'] ?? '')); } /** * @notes 企业微信授权登录 */ public function workWechatLogin($params) { $code = $params['code']; $terminal = $params['terminal'] ?? 1; $corpId = env('work_wechat.corp_id', ''); $secret = env('work_wechat.secret', ''); if (empty($corpId) || empty($secret)) { self::setError('企业微信未配置'); return false; } // 获取 access_token $accessToken = self::getWorkWechatAccessToken($corpId, $secret); if (!$accessToken) { return false; } // 用 code 换取用户身份 $url = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token={$accessToken}&code={$code}"; $response = self::httpGet($url); if (!$response || $response['errcode'] != 0) { $errMsg = $response['errmsg'] ?? '未知错误'; Log::error('企业微信获取用户身份失败: ' . $errMsg); self::setError('企业微信授权失败: ' . $errMsg); return false; } $wxUserId = self::workWechatUserIdFromAuthResponse($response); if ($wxUserId === '') { $hasOpenId = trim((string) ($response['openid'] ?? $response['OpenId'] ?? '')) !== ''; self::setError( $hasOpenId ? '当前身份非企业通讯录成员(或未同步到通讯录),无法用此账号登录后台,请使用企业内成员账号或联系管理员将你加入通讯录' : '未获取到企业微信成员 userid,请检查自建应用 Secret、可信域名是否与当前扫码应用一致' ); return false; } // 通过 work_wechat_userid 匹配管理员(SoftDelete trait 自动排除已删除记录) $admin = Admin::where('work_wechat_userid', '=', $wxUserId)->find(); if (!$admin) { self::setError('该企业微信账号未绑定管理员,请联系管理员绑定(UserId: ' . $wxUserId . ')'); return false; } if ($admin->disable === 1) { self::setError('账号已被禁用'); return false; } // 更新登录信息 $time = time(); $admin->login_time = $time; $admin->login_ip = request()->ip(); $admin->save(); // 设置 token $adminInfo = AdminTokenService::setToken($admin->id, $terminal, $admin->multipoint_login); $avatar = $admin->avatar ?: Config::get('project.default_image.admin_avatar'); $avatar = FileService::getFileUrl($avatar); return [ 'name' => $adminInfo['name'], 'avatar' => $avatar, 'role_name' => $adminInfo['role_name'], 'token' => $adminInfo['token'], 'is_paw' => $admin->is_paw ?? 1, // 0=需要修改密码,1=正常 // 企微扫码登录即已绑定 userid 'need_bind_work_wechat' => false, ]; } /** * @notes 获取企业微信 access_token(带缓存),供外部(如绑定流程)调用 */ public static function getWorkWechatAccessTokenStatic(string $corpId, string $secret) { return self::getWorkWechatAccessToken($corpId, $secret); } /** * @notes 获取企业微信 access_token(带缓存) */ private static function getWorkWechatAccessToken(string $corpId, string $secret) { $cacheKey = 'work_wechat_access_token_' . md5($corpId . $secret); $cached = Cache::get($cacheKey); if ($cached) { return $cached; } $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$corpId}&corpsecret={$secret}"; $response = self::httpGet($url); if (!$response || $response['errcode'] != 0) { $errMsg = $response['errmsg'] ?? '未知错误'; Log::error('获取企业微信access_token失败: ' . $errMsg); self::setError('获取企业微信凭证失败: ' . $errMsg); return false; } $accessToken = $response['access_token']; Cache::set($cacheKey, $accessToken, $response['expires_in'] - 200); return $accessToken; } /** * @notes HTTP GET 请求 */ private static function httpGet(string $url) { $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, 10); $result = curl_exec($ch); curl_close($ch); if ($result === false) { return null; } return json_decode($result, true); } /** * @notes 退出登录 * @param $adminInfo * @return bool * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException * @author 令狐冲 * @date 2021/7/5 14:34 */ public function logout($adminInfo) { //token不存在,不注销 if (!isset($adminInfo['token'])) { return false; } //设置token过期 return AdminTokenService::expireToken($adminInfo['token']); } /** * @notes 首次登录修改密码 * @param string $token * @param string $password * @return bool */ public function changeFirstPassword($token, $password) { try { // 通过 token 获取管理员信息 $adminSession = \app\common\model\auth\AdminSession::where('token', '=', $token) ->where('expire_time', '>', time()) ->find(); if (!$adminSession) { self::setError('登录已过期,请重新登录'); return false; } $admin = Admin::find($adminSession->admin_id); if (!$admin) { self::setError('管理员不存在'); return false; } // 使用系统的密码加密方式 $passwordSalt = Config::get('project.unique_identification'); $encryptedPassword = create_password($password, $passwordSalt); // 更新密码和 is_paw 标记 $admin->password = $encryptedPassword; $admin->is_paw = 1; $admin->save(); // 使当前 token 过期,要求重新登录 AdminTokenService::expireToken($token); return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } }