This commit is contained in:
2026-04-01 10:12:37 +08:00
parent 2a6b14f698
commit e4eab9ad89
9 changed files with 1444 additions and 39 deletions
+440
View File
@@ -19,6 +19,9 @@ use app\model\System\OperationLog;
class LoginController extends BaseController
{
private const SMS_CODE_TTL_SECONDS = 300; // 5分钟
private const SMS_CODE_RESEND_SECONDS = 60; // 60秒可重发
private function generateToken($userInfo): string
{
return JwtService::generateToken($userInfo);
@@ -29,6 +32,186 @@ class LoginController extends BaseController
return JwtService::verifyToken($token);
}
private function getSiteSettingValue(string $label): string
{
$row = SystemSiteSettings::where('label', $label)
->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
@@ -191,6 +374,87 @@ class LoginController extends BaseController
}
}
/**
* 发送手机号登录验证码
*/
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
@@ -298,6 +562,182 @@ class LoginController extends BaseController
]);
}
/**
* 注册账号(按租户维度)
*/
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