where('delete_time', null) ->order('id', 'asc') ->find(); return $row ? (string)$row['value'] : ''; } private function smsCodeCacheKey(string $scene, string $tenantName, string $account, string $phone): string { return 'sms_code:' . md5($scene . '|' . $tenantName . '|' . $account . '|' . $phone); } private function smsCodeThrottleKey(string $scene, string $tenantName, string $account, string $phone): string { return 'sms_code_throttle:' . md5($scene . '|' . $tenantName . '|' . $account . '|' . $phone); } private function postJson(string $url, array $payload, array $headers): array { $body = json_encode($payload, JSON_UNESCAPED_UNICODE); if ($body === false) { return ['ok' => false, 'error' => 'json_encode failed']; } $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => implode("\r\n", $headers), 'content' => $body, 'timeout' => 10, 'ignore_errors' => true, ], ]); $respBody = @file_get_contents($url, false, $context); $status = 0; if (!empty($http_response_header) && is_array($http_response_header)) { foreach ($http_response_header as $line) { if (preg_match('#^HTTP/\\S+\\s+(\\d+)#', $line, $m)) { $status = (int)$m[1]; break; } } } if ($respBody === false) { $last = error_get_last(); return ['ok' => false, 'status' => $status, 'error' => (string)($last['message'] ?? 'http request failed')]; } $decoded = json_decode((string)$respBody, true); if (!is_array($decoded)) { $decoded = []; } return [ 'ok' => $status >= 200 && $status < 300, 'status' => $status, 'body' => (string)$respBody, 'json' => $decoded, ]; } private function sendSmsCodeInternal(string $scene, string $tenantName, string $account, string $phone): Json { $throttleKey = $this->smsCodeThrottleKey($scene, $tenantName, $account, $phone); if (Cache::has($throttleKey)) { return json(['code' => 429, 'msg' => '验证码发送过于频繁,请稍后再试']); } $backendUrl = $this->getSiteSettingValue('backendUrl'); $apiKey = $this->getSiteSettingValue('apiKey'); if ($backendUrl === '' || $apiKey === '') { return json(['code' => 500, 'msg' => '短信网关配置缺失,请联系管理员']); } $code = (string)random_int(100000, 999999); $sceneTextMap = [ 'register' => '注册', 'reset' => '找回密码', 'login' => '登录', ]; $sceneText = $sceneTextMap[$scene] ?? '验证'; $content = "【云泽网】{$sceneText}验证码:{$code},5分钟内有效。"; $enqueueUrl = rtrim($backendUrl, '/') . '/api/v1/business/outbound-tasks'; $resp = $this->postJson($enqueueUrl, [ 'phone' => $phone, 'content' => $content, ], [ 'X-Api-Key: ' . $apiKey, 'Content-Type: application/json; charset=utf-8', 'Accept: application/json', ]); if (empty($resp['ok'])) { return json([ 'code' => 500, 'msg' => '验证码短信发送失败', 'detail' => [ 'http_status' => $resp['status'] ?? null, 'body_preview' => isset($resp['body']) ? substr((string)$resp['body'], 0, 200) : '', ] ]); } $codeKey = $this->smsCodeCacheKey($scene, $tenantName, $account, $phone); Cache::set($codeKey, $code, self::SMS_CODE_TTL_SECONDS); Cache::set($throttleKey, 1, self::SMS_CODE_RESEND_SECONDS); return json(['code' => 200, 'msg' => '验证码已发送']); } private function buildLoginSuccessResponse($user, $tenant): Json { $tid = (int)$tenant->id; try { $loginCount = isset($user['login_count']) && $user['login_count'] !== null ? (int)$user['login_count'] : 0; AdminUser::where('id', $user['id'])->update([ 'login_count' => $loginCount + 1, 'last_login_ip' => $this->request->ip(), 'last_login_time' => date('Y-m-d H:i:s') ]); } catch (\Exception $e) { error_log('更新登录信息失败: ' . $e->getMessage()); } $userInfo = [ 'id' => $user['id'], 'account' => $user['account'], 'name' => $user['name'], 'group_id' => $user['group_id'], 'tid' => $tid, 'tenant' => $tenant ]; if ($user['group_id']) { $userGroup = AdminUserGroup::where('id', $user['group_id'])->find(); if ($userGroup && $userGroup->rights) { $userInfo['rights'] = json_decode($userGroup->rights, true); } } try { $token = $this->generateToken($userInfo); } catch (\Exception $e) { $this->logFail('登录管理', '登录', 'Token生成失败: ' . $e->getMessage()); return json(['code' => 500, 'msg' => '登录失败,请稍后重试']); } try { $cacheKey = 'admin_user_' . $user['id'] . '_' . $tid; \think\facade\Cache::set($cacheKey, $userInfo, 86400 * 7); } catch (\Exception $e) { error_log('用户缓存写入失败: ' . $e->getMessage()); } try { $this->logSuccess('登录管理', '登录', [ 'id' => $user['id'], 'tid' => $tid, 'tenant' => $tenant ], $userInfo); } catch (\Exception $e) { error_log('登录日志记录失败: ' . $e->getMessage()); } return json([ 'code' => 200, 'msg' => '登录成功', 'data' => [ 'token' => $token, 'user' => $userInfo ] ]); } /** * 登录接口 * @return Json */ public function login(): Json { try { $data = $this->request->param(); // 1. 先验证租户名称是否传入(新增:必须传租户名称) try { $this->validate($data, [ 'tenant_name|租户名称' => 'require|length:1,128', // 匹配租户表tenant字段长度 'account|账号' => 'require|length:3,32', 'password|密码' => 'require|length:6,32' ]); } catch (ValidateException $e) { $this->logFail('登录管理', '登录', $e->getMessage()); return json([ 'code' => 400, 'msg' => $e->getError() ]); } // 2. 处理账号兼容(邮箱/手机号转account) if (isset($data['email'])) { $data['account'] = $data['email']; } elseif (isset($data['phone'])) { $data['account'] = $data['phone']; } // 3. 查询租户ID(新增:只查正常状态的租户 status=1) $tenant = Tenant::where('tenant_name', $data['tenant_name']) ->where('status', 1) // 过滤停用/删除的租户 ->field(['id', 'tenant_name']) // 只查需要的字段,提升性能 ->find(); // ThinkPHP 使用 find() // 4. 验证租户是否存在 if (!$tenant) { $this->logFail('登录管理', '登录', '租户不存在或已禁用,租户名称:' . $data['tenant_name']); return json([ 'code' => 401, 'msg' => '租户不存在或已禁用' ]); } $tid = $tenant->id; $tenant_name = $tenant->tenant_name; // 5. 查询用户(新增:关联租户ID,确保用户属于该租户) $user = AdminUser::where('account', $data['account']) ->where('tid', $tid) // 核心:验证用户所属租户 ->where('status', 1) ->find(); // 6. 验证用户是否存在 if (!$user) { $this->logFail('登录管理', '登录', '账号不存在/已禁用,或不属于当前租户,账号:' . $data['account'] . ',租户:' . $tenant_name); return json([ 'code' => 401, 'msg' => '账号不存在或已禁用,或不属于当前租户' ]); } // 7. 验证密码 if (md5($data['password']) !== $user['password']) { $this->logFail('登录管理', '登录', '密码错误,账号:' . $data['account'] . ',租户:' . $tenant_name); return json([ 'code' => 401, 'msg' => '密码错误' ]); } // 8. 更新登录次数和IP try { $loginCount = isset($user['login_count']) && $user['login_count'] !== null ? (int)$user['login_count'] : 0; AdminUser::where('id', $user['id'])->update([ 'login_count' => $loginCount + 1, 'last_login_ip' => $this->request->ip(), 'last_login_time' => date('Y-m-d H:i:s') // 新增:记录最后登录时间,更实用 ]); } catch (\Exception $e) { error_log('更新登录信息失败: ' . $e->getMessage()); } // 9. 组装用户信息(新增:加入租户ID和租户名称和角色权限) $userInfo = [ 'id' => $user['id'], 'account' => $user['account'], 'name' => $user['name'], 'group_id' => $user['group_id'], 'tid' => $tid, // 新增:租户ID 'tenant' => $tenant // 新增:租户名称 ]; // 获取角色权限 if ($user['group_id']) { $userGroup = AdminUserGroup::where('id', $user['group_id'])->find(); if ($userGroup && $userGroup->rights) { $userInfo['rights'] = json_decode($userGroup->rights, true); } } // 10. 生成Token(Token中已包含租户信息,后续可通过Token解析获取) try { $token = $this->generateToken($userInfo); } catch (\Exception $e) { $this->logFail('登录管理', '登录', 'Token生成失败: ' . $e->getMessage()); return json([ 'code' => 500, 'msg' => '登录失败,请稍后重试' ]); } // 11. 写入用户数据缓存(核心:缓存包含租户信息,示例用Redis,可根据你的缓存工具调整) try { $cacheKey = 'admin_user_' . $user['id'] . '_' . $tid; // 缓存键加入租户ID,避免多租户冲突 $cacheExpire = 86400 * 7; // 缓存7天,可根据需求调整 // 写入缓存(这里假设你使用thinkphp的Cache类,若用其他工具可替换) \think\facade\Cache::set($cacheKey, $userInfo, $cacheExpire); } catch (\Exception $e) { error_log('用户缓存写入失败: ' . $e->getMessage()); // 缓存写入失败不影响登录,但记录日志 } // 12. 记录登录成功日志 try { $this->logSuccess('登录管理', '登录', [ 'id' => $user['id'], 'tid' => $tid, 'tenant' => $tenant ], $userInfo); } catch (\Exception $e) { error_log('登录日志记录失败: ' . $e->getMessage()); } // 13. 返回登录结果(包含租户信息) return json([ 'code' => 200, 'msg' => '登录成功', 'data' => [ 'token' => $token, 'user' => $userInfo // 前端可直接获取租户ID和名称 ] ]); } catch (\Exception $e) { $errorMsg = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine(); error_log('登录失败: ' . $errorMsg); try { $this->logFail('登录管理', '登录', $errorMsg); } catch (\Exception $logError) { error_log('记录登录失败日志也失败: ' . $logError->getMessage()); } return json([ 'code' => 500, 'msg' => '登录失败:' . $e->getMessage() ]); } } /** * 发送手机号登录验证码 */ public function sendLoginCode(): Json { try { $data = $this->request->post(); $this->validate($data, [ 'tenant_name|租户名称' => 'require|length:1,128', 'phone|手机号' => 'require|mobile', ]); $tenant = Tenant::where('tenant_name', (string)$data['tenant_name']) ->where('status', 1) ->find(); if (!$tenant) { return json(['code' => 400, 'msg' => '租户不存在或已禁用']); } $user = AdminUser::where('tid', (int)$tenant['id']) ->where('phone', (string)$data['phone']) ->where('status', 1) ->where('delete_time', null) ->find(); if (!$user) { return json(['code' => 404, 'msg' => '手机号未绑定可用账号']); } return $this->sendSmsCodeInternal('login', (string)$data['tenant_name'], (string)$data['phone'], (string)$data['phone']); } catch (ValidateException $e) { return json(['code' => 400, 'msg' => $e->getError()]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => '发送失败:' . $e->getMessage()]); } } /** * 手机号验证码登录 */ public function loginBySms(): Json { try { $data = $this->request->post(); $this->validate($data, [ 'tenant_name|租户名称' => 'require|length:1,128', 'phone|手机号' => 'require|mobile', 'sms_code|短信验证码' => 'require|length:4,8', ]); $codeKey = $this->smsCodeCacheKey('login', (string)$data['tenant_name'], (string)$data['phone'], (string)$data['phone']); $cachedCode = (string)Cache::get($codeKey, ''); if ($cachedCode === '' || $cachedCode !== (string)$data['sms_code']) { return json(['code' => 400, 'msg' => '短信验证码错误或已过期']); } $tenant = Tenant::where('tenant_name', (string)$data['tenant_name']) ->where('status', 1) ->field(['id', 'tenant_name']) ->find(); if (!$tenant) { return json(['code' => 401, 'msg' => '租户不存在或已禁用']); } $user = AdminUser::where('tid', (int)$tenant['id']) ->where('phone', (string)$data['phone']) ->where('status', 1) ->where('delete_time', null) ->find(); if (!$user) { return json(['code' => 401, 'msg' => '手机号未绑定可用账号']); } Cache::delete($codeKey); return $this->buildLoginSuccessResponse($user, $tenant); } catch (ValidateException $e) { return json(['code' => 400, 'msg' => $e->getError()]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => '登录失败:' . $e->getMessage()]); } } /** * 退出登录 * @return Json */ public function logout(): Json { $authHeader = $this->request->header('Authorization', ''); $userInfo = null; if (preg_match('/Bearer\s+(.+)/i', $authHeader, $matches)) { $tokenData = $this->verifyToken($matches[1]); if ($tokenData && isset($tokenData['user'])) { $userInfo = (array)$tokenData['user']; } } if ($userInfo && isset($userInfo['id'])) { $this->logSuccess('登录管理', '退出登录', ['result' => 'success'], $userInfo); } else { OperationLog::create([ 'user_id' => 0, 'user_account' => '', 'user_name' => '未知用户', 'module' => '登录管理', 'action' => '退出登录', 'method' => 'POST', 'url' => $this->request->url(true), 'ip' => $this->request->ip(), 'user_agent' => $this->request->header('user-agent', ''), 'request_data' => null, 'response_data' => json_encode(['result' => 'success'], JSON_UNESCAPED_UNICODE), 'status' => 1, 'error_message' => '', 'execution_time' => 0.0, ]); } return json([ 'code' => 200, 'msg' => '退出成功' ]); } /** * 获取当前登录用户信息 * @return Json */ public function userInfo(): Json { $authHeader = $this->request->header('Authorization', ''); if (!preg_match('/Bearer\s+(.+)/i', $authHeader, $matches)) { return json([ 'code' => 401, 'msg' => '未登录' ]); } $tokenData = $this->verifyToken($matches[1]); if (!$tokenData || !isset($tokenData['user'])) { return json([ 'code' => 401, 'msg' => 'Token无效' ]); } $user = (array)$tokenData['user']; $user_id = $user['id']; $userData = AdminUser::where('id', $user_id) ->where('delete_time', null) ->field('id, account, name, phone, qq, sex, group_id, status, create_time, update_time') ->find(); if (!$userData) { return json([ 'code' => 404, 'msg' => '用户不存在' ]); } return json([ 'code' => 200, 'msg' => '获取成功', 'data' => $userData->toArray() ]); } public function getAdminUserFromToken(): array { return JwtService::getUserFromHeader($this->request->header('Authorization', '')); } public function loginInfo() { $loginInfo = SystemSiteSettings::select(); return json([ 'code' => 200, 'msg' => '获取成功', 'data' => $loginInfo ]); } /** * 注册账号(按租户维度) */ public function sendRegisterCode(): Json { try { $data = $this->request->post(); $this->validate($data, [ 'tenant_name|租户名称' => 'require|length:1,128', 'account|账号' => 'require|length:3,32', 'phone|手机号' => 'require|mobile', ]); return $this->sendSmsCodeInternal('register', (string)$data['tenant_name'], (string)$data['account'], (string)$data['phone']); } catch (ValidateException $e) { return json(['code' => 400, 'msg' => $e->getError()]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => '发送失败:' . $e->getMessage()]); } } public function register(): Json { try { $data = $this->request->post(); $this->validate($data, [ 'tenant_name|租户名称' => 'require|length:1,128', 'account|账号' => 'require|length:3,32', 'name|姓名' => 'require|length:2,32', 'password|密码' => 'require|length:6,32', 'confirm_password|确认密码' => 'require', 'phone|手机号' => 'require|mobile', 'sms_code|短信验证码' => 'require|length:4,8', ]); if ((string)$data['password'] !== (string)$data['confirm_password']) { return json(['code' => 400, 'msg' => '两次输入的密码不一致']); } $codeKey = $this->smsCodeCacheKey('register', (string)$data['tenant_name'], (string)$data['account'], (string)$data['phone']); $cachedCode = (string)Cache::get($codeKey, ''); if ($cachedCode === '' || $cachedCode !== (string)$data['sms_code']) { return json(['code' => 400, 'msg' => '短信验证码错误或已过期']); } $tenant = Tenant::where('tenant_name', (string)$data['tenant_name']) ->where('status', 1) ->find(); if (!$tenant) { return json(['code' => 400, 'msg' => '租户不存在或已禁用']); } $exists = AdminUser::where('tid', (int)$tenant['id']) ->where('account', (string)$data['account']) ->where('delete_time', null) ->find(); if ($exists) { return json(['code' => 400, 'msg' => '账号已存在']); } $now = date('Y-m-d H:i:s'); $user = new AdminUser(); $user->save([ 'tid' => (int)$tenant['id'], 'account' => (string)$data['account'], 'name' => (string)$data['name'], 'phone' => (string)$data['phone'], 'email' => (string)($data['email'] ?? ''), 'password' => md5((string)$data['password']), 'status' => 1, 'group_id' => 0, 'login_count' => 0, 'create_time' => $now, 'update_time' => $now, ]); Cache::delete($codeKey); return json(['code' => 200, 'msg' => '注册成功']); } catch (ValidateException $e) { return json(['code' => 400, 'msg' => $e->getError()]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => '注册失败:' . $e->getMessage()]); } } /** * 忘记密码:通过租户 + 账号 + 手机号重置密码 */ public function sendResetCode(): Json { try { $data = $this->request->post(); $this->validate($data, [ 'tenant_name|租户名称' => 'require|length:1,128', 'account|账号' => 'require|length:3,32', 'phone|手机号' => 'require|mobile', ]); $tenant = Tenant::where('tenant_name', (string)$data['tenant_name']) ->where('status', 1) ->find(); if (!$tenant) { return json(['code' => 400, 'msg' => '租户不存在或已禁用']); } $user = AdminUser::where('tid', (int)$tenant['id']) ->where('account', (string)$data['account']) ->where('phone', (string)$data['phone']) ->where('delete_time', null) ->find(); if (!$user) { return json(['code' => 404, 'msg' => '账号不存在或手机号不匹配']); } return $this->sendSmsCodeInternal('reset', (string)$data['tenant_name'], (string)$data['account'], (string)$data['phone']); } catch (ValidateException $e) { return json(['code' => 400, 'msg' => $e->getError()]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => '发送失败:' . $e->getMessage()]); } } public function resetPassword(): Json { try { $data = $this->request->post(); $this->validate($data, [ 'tenant_name|租户名称' => 'require|length:1,128', 'account|账号' => 'require|length:3,32', 'phone|手机号' => 'require|mobile', 'new_password|新密码' => 'require|length:6,32', 'confirm_password|确认密码' => 'require', 'sms_code|短信验证码' => 'require|length:4,8', ]); if ((string)$data['new_password'] !== (string)$data['confirm_password']) { return json(['code' => 400, 'msg' => '两次输入的密码不一致']); } $codeKey = $this->smsCodeCacheKey('reset', (string)$data['tenant_name'], (string)$data['account'], (string)$data['phone']); $cachedCode = (string)Cache::get($codeKey, ''); if ($cachedCode === '' || $cachedCode !== (string)$data['sms_code']) { return json(['code' => 400, 'msg' => '短信验证码错误或已过期']); } $tenant = Tenant::where('tenant_name', (string)$data['tenant_name']) ->where('status', 1) ->find(); if (!$tenant) { return json(['code' => 400, 'msg' => '租户不存在或已禁用']); } $user = AdminUser::where('tid', (int)$tenant['id']) ->where('account', (string)$data['account']) ->where('phone', (string)$data['phone']) ->where('delete_time', null) ->find(); if (!$user) { return json(['code' => 404, 'msg' => '账号不存在或手机号不匹配']); } AdminUser::where('id', (int)$user['id'])->update([ 'password' => md5((string)$data['new_password']), 'update_time' => date('Y-m-d H:i:s'), ]); Cache::delete($codeKey); return json(['code' => 200, 'msg' => '密码重置成功']); } catch (ValidateException $e) { return json(['code' => 400, 'msg' => $e->getError()]); } catch (\Throwable $e) { return json(['code' => 500, 'msg' => '重置失败:' . $e->getMessage()]); } } /** * 获取极验3.0的id和key * @return Json */ public function getGeetest3Infos() { // 定义你需要的 label 列表 $targetLabels = [ 'geetest3ID', 'geetest3KEY' ]; $legalInfos = SystemSiteSettings::where('delete_time', null) ->whereIn('label', $targetLabels) // 仅筛选指定的 label ->field('label, value') ->select(); $this->logSuccess('站点设置管理', '查看极验3.0的id和key', [], $this->getAdminUserInfo()); return json([ 'code' => 200, 'msg' => '获取成功', 'data' => $legalInfos->toArray() ]); } /** * 获取极验4.0的id和key * @return Json */ public function getGeetest4Infos() { // 定义你需要的 label 列表 $targetLabels = [ 'geetest4ID', 'geetest4KEY' ]; $legalInfos = SystemSiteSettings::where('delete_time', null) ->whereIn('label', $targetLabels) // 仅筛选指定的 label ->field('label, value') ->select() ->toArray(); // 转换为前端需要的格式 $data = []; foreach ($legalInfos as $item) { if ($item['label'] === 'geetest4ID') { $data['captcha_id'] = $item['value']; } if ($item['label'] === 'geetest4KEY') { $data['captcha_key'] = $item['value']; } } $this->logSuccess('站点设置管理', '查看极验4.0的id和key', [], $this->getAdminUserInfo()); return json([ 'code' => 200, 'msg' => '获取成功', 'data' => $data ]); } /** * 判断是否开启验证 * @return Json */ public function getOpenVerify() { // 定义你需要的 label 列表 $targetLabels = [ 'openVerify', 'verifyModel' ]; $legalInfos = SystemSiteSettings::where('delete_time', null) ->whereIn('label', $targetLabels) // 仅筛选指定的 label ->field('label, value') ->select(); return json([ 'code' => 200, 'msg' => '获取成功', 'data' => $legalInfos->toArray() ]); } }