smsService = $smsService; $this->weChatService = $weChatService; } /** * 获取真实IP(支持nginx代理) */ private function getRealIp(Request $request): string { $realIp = $request->header('X-Real-IP'); return $realIp ?: $request->ip(); } /** * 用户注册 */ public function register(Request $request) { $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'password' => [ 'required', 'string', 'min:8', function ($attribute, $value, $fail) { $errors = User::validatePasswordStrength($value); if (!empty($errors)) { $fail($errors[0]); } }, ], 'password_confirmation' => 'required|same:password', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password), ]); $token = $user->createToken('auth_token')->plainTextToken; $this->logLogin($user, $request, 'password', 'success'); return response()->json([ 'code' => 201, 'data' => [ 'user' => $this->formatUser($user), 'token' => $token, ], 'message' => '注册成功', ], 201); } /** * 账号密码登录 */ public function login(Request $request) { $validator = Validator::make($request->all(), [ 'username' => 'required|string', 'password' => 'required|string', 'captcha' => 'nullable|string', 'device_id' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } $username = $request->username; $password = $request->password; $deviceId = $request->device_id; // 根据用户名查找用户(支持邮箱或用户名) $user = User::where('email', $username) ->orWhere('name', $username) ->orWhere('phone', $username) ->first(); // 检查登录失败次数 (针对该用户名的IP) $ip = $this->getRealIp($request); $failKey = "login_fail:{$username}:{$ip}"; $failCount = (int) Cache::get($failKey, 0); // 图形验证码检查 (失败2次后要求) if ($failCount >= 2) { $captcha = $request->captcha; $captchaKey = "captcha:{$deviceId}"; if (!$captcha) { return response()->json([ 'code' => 400, 'message' => '请输入图形验证码', 'data' => [ 'require_captcha' => true, ], ], 400); } $storedCaptcha = Cache::get($captchaKey); if (!$storedCaptcha || strtolower($captcha) !== strtolower($storedCaptcha)) { return response()->json([ 'code' => 400, 'message' => '图形验证码错误', 'data' => [ 'require_captcha' => true, ], ], 400); } } // 连续失败5次,锁定10分钟 if ($failCount >= 5) { $lockedMinutes = 10; Cache::put($failKey, $failCount, now()->addMinutes($lockedMinutes)); $this->logLogin($user, $request, 'password', 'failed', '连续登录失败,账号已锁定'); return response()->json([ 'code' => 429, 'message' => "连续登录失败次数过多,账号已锁定{$lockedMinutes}分钟", ], 429); } // 验证账号 if (!$user || !Hash::check($password, $user->password)) { Cache::put($failKey, $failCount + 1, now()->addHour()); $this->logLogin($user, $request, 'password', 'failed', '用户名或密码错误'); return response()->json([ 'code' => 401, 'message' => '用户名或密码错误', 'data' => [ 'fail_count' => $failCount + 1, 'require_captcha' => $failCount + 1 >= 2, ], ], 401); } // 检查用户状态 if ($user->isLocked()) { $remaining = $user->getLockedRemainingSeconds(); $minutes = ceil($remaining / 60); $this->logLogin($user, $request, 'password', 'failed', '账号已被锁定'); return response()->json([ 'code' => 403, 'message' => "账号已被锁定,请{$minutes}分钟后再试", 'data' => [ 'locked_until' => $user->locked_until, 'locked_remaining' => $remaining, ], ], 403); } // 登录成功 Cache::forget($failKey); $user->recordLogin($ip); // 检查是否为授信设备(异地登录检测) $deviceHash = UserDevice::generateHash($request->userAgent() ?? '', $ip); $trustedDevice = UserDevice::where('user_id', $user->id) ->where('device_hash', $deviceHash) ->where('is_trusted', true) ->first(); // 如果设备未授信或不存在,要求配对码验证 if (!$trustedDevice) { // 记录新设备登录 $this->logLogin($user, $request, 'password', 'pending', '新设备登录,待配对码验证'); // 自动发送配对码邮件 $expireMinutes = 10; $code = \App\Models\LoginVerifyCode::generateCode(); $deviceHashForCode = UserDevice::generateHash($request->userAgent() ?? '', $ip); // 保存配对码 $verifyCode = \App\Models\LoginVerifyCode::create([ 'user_id' => $user->id, 'code' => $code, 'type' => 'pair', 'device_hash' => $deviceHashForCode, 'ip' => $ip, 'user_agent' => $request->userAgent(), 'expires_at' => now()->addMinutes($expireMinutes), ]); // 发送邮件 $mailService = new MailService(); $sent = $mailService->sendPairCode($user->email, $code, $expireMinutes); if (!$sent) { \Log::warning("自动发送配对码邮件失败: {$code}"); } return response()->json([ 'code' => 200, 'data' => [ 'require_pair_code' => true, 'device_hash' => $deviceHash, 'email' => $user->email, 'pair_code' => $code, // 开发环境返回,方便测试 'message' => '检测到新设备登录,配对码已发送至绑定邮箱', ], 'message' => '检测到新设备登录,配对码已发送至绑定邮箱', ]); } // 授信设备,直接登录成功 $token = $user->createToken('auth_token')->plainTextToken; // 更新设备最后登录时间 $trustedDevice->update(['last_login_at' => now()]); $this->logLogin($user, $request, 'password', 'success'); return response()->json([ 'code' => 200, 'data' => [ 'user' => $this->formatUser($user), 'token' => $token, ], 'message' => '登录成功', ]); } /** * 短信验证码登录 - 发送验证码 */ public function sendSmsCode(Request $request) { $validator = Validator::make($request->all(), [ 'phone' => ['required', 'string', 'regex:/^1[3-9]\d{9}$/'], 'captcha' => 'nullable|string', 'device_id' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } $phone = $request->phone; $deviceId = $request->device_id; // 检查是否需要图形验证码 (短信连续失败2次后) $smsFailKey = "sms_fail:{$phone}"; $smsFailCount = (int) Cache::get($smsFailKey, 0); if ($smsFailCount >= 2) { $captcha = $request->captcha; $captchaKey = "captcha:{$deviceId}"; if (!$captcha) { return response()->json([ 'code' => 400, 'message' => '请输入图形验证码', 'data' => ['require_captcha' => true], ], 400); } $storedCaptcha = Cache::get($captchaKey); if (!$storedCaptcha || strtolower($captcha) !== strtolower($storedCaptcha)) { return response()->json([ 'code' => 400, 'message' => '图形验证码错误', 'data' => ['require_captcha' => true], ], 400); } } // 发送短信 $result = $this->smsService->sendCode($phone, 'login'); if (!$result['success']) { return response()->json([ 'code' => 429, 'message' => $result['message'], ], 429); } return response()->json([ 'code' => 200, 'message' => $result['message'], ]); } /** * 短信验证码登录 - 验证登录 */ public function smsLogin(Request $request) { $validator = Validator::make($request->all(), [ 'phone' => ['required', 'string', 'regex:/^1[3-9]\d{9}$/'], 'code' => 'required|string|size:6', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } $phone = $request->phone; $code = $request->code; // 验证短信验证码 if (!$this->smsService->verifyCode($phone, $code, 'login')) { $failKey = "sms_fail:{$phone}"; $failCount = (int) Cache::get($failKey, 0) + 1; Cache::put($failKey, $failCount, now()->addHour()); $this->logLogin(null, $request, 'sms', 'failed', '验证码错误'); return response()->json([ 'code' => 401, 'message' => '验证码错误', 'data' => ['fail_count' => $failCount], ], 401); } // 查找或创建用户 $user = User::findByPhone($phone); if (!$user) { // 新用户自动注册(可选,根据业务需求) $user = User::create([ 'name' => '用户_' . substr($phone, -4), 'email' => "phone_{$phone}@erp.local", 'password' => Hash::make(bin2hex(random_bytes(8))), 'phone' => $phone, ]); } $user->recordLogin($this->getRealIp($request)); $token = $user->createToken('sms_auth')->plainTextToken; $this->logLogin($user, $request, 'sms', 'success'); return response()->json([ 'code' => 200, 'data' => [ 'user' => $this->formatUser($user), 'token' => $token, ], 'message' => '登录成功', ]); } /** * 获取微信登录二维码 */ public function wechatQr(Request $request) { $result = $this->weChatService->getLoginQr(); if (!$result['success']) { return response()->json([ 'code' => 500, 'message' => $result['message'], ], 500); } return response()->json([ 'code' => 200, 'data' => $result['data'], 'message' => '获取成功', ]); } /** * 检查微信扫码状态 */ public function wechatCheck(Request $request) { $validator = Validator::make($request->all(), [ 'scene_str' => 'required|string', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } $result = $this->weChatService->checkScanStatus($request->scene_str); return response()->json([ 'code' => 200, 'data' => $result['data'], 'message' => 'success', ]); } /** * 微信扫码登录 - 模拟扫码登录(开发测试用) */ public function wechatMockScan(Request $request) { $validator = Validator::make($request->all(), [ 'scene_str' => 'required|string', 'openid' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } $this->weChatService->mockScan($request->scene_str, $request->openid); return response()->json([ 'code' => 200, 'message' => '模拟扫码成功', ]); } /** * 微信回调处理 */ public function wechatCallback(Request $request) { $code = $request->get('code'); $state = $request->get('state'); if (!$code) { return response()->json([ 'code' => 400, 'message' => '缺少授权码', ], 400); } $result = $this->weChatService->handleCallback($code); if (!$result['success']) { return response()->json([ 'code' => 500, 'message' => $result['message'], ], 500); } $this->logLogin($result['data']['user'], $request, 'wechat', 'success'); return response()->json([ 'code' => 200, 'data' => [ 'user' => $this->formatUser($result['data']['user']), 'token' => $result['data']['token'], ], 'message' => '登录成功', ]); } /** * 获取当前用户信息 */ public function me(Request $request) { return response()->json([ 'code' => 200, 'data' => $this->formatUser($request->user()), 'message' => 'success', ]); } /** * 登出 */ public function logout(Request $request) { $request->user()->currentAccessToken()->delete(); return response()->json([ 'code' => 200, 'message' => '登出成功', ]); } /** * 刷新 Token */ public function refresh(Request $request) { $user = $request->user(); $user->tokens()->delete(); $token = $user->createToken('auth_token')->plainTextToken; return response()->json([ 'code' => 200, 'data' => [ 'token' => $token, 'user' => $this->formatUser($user), ], 'message' => '刷新成功', ]); } /** * 更新个人资料 */ public function updateProfile(Request $request) { $user = $request->user(); $validator = Validator::make($request->all(), [ 'name' => 'sometimes|string|max:255', 'email' => ['sometimes', 'email', 'max:255', 'unique:users,email,' . $user->id], 'phone' => ['sometimes', 'string', 'regex:/^1[3-9]\d{9}$/', 'unique:users,phone,' . $user->id], 'avatar' => 'sometimes|string|url|max:500', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } // 如果要修改邮箱 if ($request->has('email') && $request->email !== $user->email) { $newEmail = $request->email; // 发送邮箱验证邮件 $code = \App\Models\LoginVerifyCode::generateCode(); $deviceHash = UserDevice::generateHash($request->userAgent() ?? '', $this->getRealIp($request) ?? ''); \App\Models\LoginVerifyCode::create([ 'user_id' => $user->id, 'code' => $code, 'type' => 'email_change', 'device_hash' => $deviceHash, 'ip' => $this->getRealIp($request), 'user_agent' => $request->userAgent(), 'expires_at' => now()->addMinutes(30), 'extra' => json_encode(['new_email' => $newEmail]), ]); // 发送验证邮件到新邮箱 $mailService = new MailService(); $mailService->sendEmailChangeCode($newEmail, $code, $user->name); return response()->json([ 'code' => 200, 'data' => [ 'require_email_verify' => true, 'message' => '验证码已发送到新邮箱,请查收', ], 'message' => '验证码已发送到新邮箱,请查收', ]); } $user->update($request->only(['name', 'phone', 'avatar'])); return response()->json([ 'code' => 200, 'data' => $this->formatUser($user), 'message' => '更新成功', ]); } /** * 发送邮箱修改验证码 */ public function sendEmailChangeCode(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|email', ]); if ($validator->fails()) { return response()->json(['code' => 422, 'message' => '验证失败', 'errors' => $validator->errors()], 422); } $newEmail = $request->email; $user = $request->user(); // 检查邮箱是否已被其他用户使用 $exists = User::where('email', $newEmail)->where('id', '!=', $user->id)->exists(); if ($exists) { return response()->json(['code' => 400, 'message' => '该邮箱已被其他用户使用'], 400); } // 生成6位数字验证码 $code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT); // 保存验证码 LoginVerifyCode::create([ 'user_id' => $user->id, 'code' => $code, 'type' => 'email_change', 'extra' => json_encode(['new_email' => $newEmail]), 'ip' => $this->getRealIp($request), 'user_agent' => $request->userAgent(), 'expires_at' => now()->addMinutes(10), ]); // 发送邮件 $sent = $this->mailService->sendEmailChangeCode($newEmail, $code, $user->name); if (!$sent) { return response()->json(['code' => 500, 'message' => '邮件发送失败,请检查邮箱地址是否正确'], 500); } return response()->json([ 'code' => 200, 'message' => '验证码已发送到新邮箱', ]); } /** * 验证邮箱修改 */ public function verifyEmailChange(Request $request) { $validator = Validator::make($request->all(), [ 'code' => 'required|string|size:6', ]); if ($validator->fails()) { return response()->json(['code' => 422, 'message' => '验证失败', 'errors' => $validator->errors()], 422); } $user = $request->user(); $verifyCode = \App\Models\LoginVerifyCode::findValidCode($user, $request->code, 'email_change'); if (!$verifyCode) { return response()->json(['code' => 400, 'message' => '验证码无效或已过期'], 400); } // 解析新邮箱 $extra = json_decode($verifyCode->extra, true); $newEmail = $extra['new_email'] ?? null; if (!$newEmail) { return response()->json(['code' => 400, 'message' => '无效的验证数据'], 400); } // 标记已使用 $verifyCode->markAsUsed(); // 更新邮箱 $user->email = $newEmail; $user->save(); return response()->json([ 'code' => 200, 'data' => $this->formatUser($user), 'message' => '邮箱修改成功', ]); } /** * 修改密码 */ public function changePassword(Request $request) { $validator = Validator::make($request->all(), [ 'current_password' => 'required|string', 'new_password' => [ 'required', 'string', 'min:8', function ($attribute, $value, $fail) { $errors = User::validatePasswordStrength($value); if (!empty($errors)) { $fail($errors[0]); } }, ], 'new_password_confirmation' => 'required|same:new_password', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } $user = $request->user(); if (!Hash::check($request->current_password, $user->password)) { return response()->json([ 'code' => 400, 'message' => '当前密码错误', ], 400); } $user->password = Hash::make($request->new_password); $user->save(); return response()->json([ 'code' => 200, 'message' => '密码修改成功', ]); } /** * 图形验证码生成 */ public function captcha(Request $request) { $deviceId = $request->get('device_id', $this->getRealIp($request)); // 生成4位随机字符 $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'; $code = ''; for ($i = 0; $i < 4; $i++) { $code .= $chars[random_int(0, strlen($chars) - 1)]; } // 存储验证码 Cache::put("captcha:{$deviceId}", $code, now()->addMinutes(5)); // 生成简单验证码图片 (使用 SVG) $svg = $this->generateCaptchaSvg($code); return response($svg, 200, [ 'Content-Type' => 'image/svg+xml', 'Cache-Control' => 'no-cache, no-store', ]); } /** * 生成简单 SVG 验证码 */ private function generateCaptchaSvg(string $code): string { $width = 120; $height = 40; // 随机颜色 $bgColor = sprintf('#%06X', random_int(0, 0xF0F0F0)); $textColor = sprintf('#%06X', random_int(0, 0x333333)); $svg = << {$code[0]} {$code[1]} {$code[2]} {$code[3]} SVG; return $svg; } /** * 绑定微信到当前账号 */ public function bindWechat(Request $request) { $validator = Validator::make($request->all(), [ 'code' => 'required|string', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } $result = $this->weChatService->handleCallback($request->code); if (!$result['success']) { return response()->json([ 'code' => 500, 'message' => $result['message'], ], 500); } $user = $request->user(); $bindResult = $this->weChatService->bindToUser( $user, $result['data']['openid'], $result['data']['unionid'] ?? null ); if (!$bindResult) { return response()->json([ 'code' => 400, 'message' => '该微信已被其他账号绑定', ], 400); } return response()->json([ 'code' => 200, 'message' => '绑定成功', ]); } /** * 解绑微信 */ public function unbindWechat(Request $request) { $user = $request->user(); if (!$user->wechat_openid) { return response()->json([ 'code' => 400, 'message' => '未绑定微信', ], 400); } $this->weChatService->unbind($user); return response()->json([ 'code' => 200, 'message' => '解绑成功', ]); } /** * 忘记密码 - 发送验证码 */ public function sendResetCode(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|email|exists:users,email', ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } // 使用短信服务发送验证码到用户绑定的手机号 $user = User::where('email', $request->email)->first(); if (!$user || !$user->phone) { return response()->json([ 'code' => 400, 'message' => '该账号未绑定手机号,请联系管理员', ], 400); } $result = $this->smsService->sendCode($user->phone, 'reset_password'); if (!$result['success']) { return response()->json([ 'code' => 429, 'message' => $result['message'], ], 429); } return response()->json([ 'code' => 200, 'message' => '验证码已发送', ]); } /** * 重置密码 */ public function resetPassword(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|email|exists:users,email', 'code' => 'required|string|size:6', 'new_password' => [ 'required', 'string', 'min:8', function ($attribute, $value, $fail) { $errors = User::validatePasswordStrength($value); if (!empty($errors)) { $fail($errors[0]); } }, ], ]); if ($validator->fails()) { return response()->json([ 'code' => 422, 'message' => '验证失败', 'errors' => $validator->errors(), ], 422); } $user = User::where('email', $request->email)->first(); if (!$this->smsService->verifyCode($user->phone, $request->code, 'reset_password')) { return response()->json([ 'code' => 401, 'message' => '验证码错误或已过期', ], 401); } $user->password = Hash::make($request->new_password); $user->save(); return response()->json([ 'code' => 200, 'message' => '密码重置成功', ]); } /** * 获取登录日志 */ public function loginLogs(Request $request) { $query = LoginLog::where('user_id', $request->user()->id) ->orderBy('created_at', 'desc'); $total = $query->count(); $logs = $query->limit(20)->get(); return response()->json([ 'code' => 200, 'data' => [ 'list' => $logs, 'total' => $total, ], 'message' => 'success', ]); } /** * 获取授信设备列表 */ public function getTrustedDevices(Request $request) { $devices = UserDevice::where('user_id', $request->user()->id) ->where('is_trusted', true) ->orderBy('last_login_at', 'desc') ->get(); return response()->json([ 'code' => 200, 'data' => $devices, 'message' => 'success', ]); } /** * 取消设备授信 */ public function revokeDevice(Request $request, $id) { $device = UserDevice::where('user_id', $request->user()->id) ->where('id', $id) ->first(); if (!$device) { return response()->json([ 'code' => 404, 'message' => '设备不存在', ], 404); } $device->is_trusted = false; $device->save(); return response()->json([ 'code' => 200, 'message' => '已取消授信', ]); } /** * 批准设备申请 */ public function approveDevice(Request $request, $id) { $device = UserDevice::where('user_id', $request->user()->id) ->where('id', $id) ->first(); if (!$device) { return response()->json([ 'code' => 404, 'message' => '设备不存在', ], 404); } $device->is_trusted = true; $device->trusted_at = now(); $device->save(); return response()->json([ 'code' => 200, 'message' => '已批准', ]); } /** * 记录登录日志 */ private function logLogin(?User $user, Request $request, string $type, string $status, string $reason = null): void { // 获取真实IP(优先从nginx传递的X-Real-IP获取) $realIp = $this->getRealIp($request); try { LoginLog::create([ 'user_id' => $user?->id, 'username' => $request->username ?? $request->phone ?? ($user ? $user->email : 'unknown'), 'login_type' => $type, 'status' => $status, 'ip' => $realIp, 'user_agent' => $request->userAgent(), 'device_id' => $request->device_id, 'failure_reason' => $reason, ]); } catch (\Exception $e) { Log::error('记录登录日志失败: ' . $e->getMessage()); } } /** * 格式化用户数据 */ private function formatUser(User $user): array { $roles = $user->roles; return [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'phone' => $user->phone, 'avatar' => $user->avatar, 'status' => $user->status, 'is_test_user' => $user->isTestUser(), 'roleCode' => $roles->first()?->slug, 'roleName' => $roles->first()?->name, 'roles' => $roles->map(fn($r) => ['id' => $r->id, 'name' => $r->name, 'slug' => $r->slug])->toArray(), 'warehouseIds' => $user->warehouse_ids ?? [], 'created_at' => $user->created_at?->toIso8601String(), 'last_login_at' => $user->last_login_at?->toIso8601String(), ]; } /** * 发送异地登录配对码 */ public function sendPairCode(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|email', 'expireMinutes' => 'nullable|integer|min:5|max:60', ]); if ($validator->fails()) { return response()->json(['code' => 422, 'message' => '验证失败', 'errors' => $validator->errors()], 422); } $user = User::where('email', $request->email)->first(); if (!$user) { return response()->json(['code' => 404, 'message' => '用户不存在'], 404); } // 频率限制:60秒内只能发送一次 $rateKey = "pair_code_rate:{$user->id}"; if (Cache::has($rateKey)) { $remaining = Cache::get($rateKey . '_ttl', 0); return response()->json([ 'code' => 429, 'message' => "发送太频繁,请{$remaining}秒后再试", ], 429); } $expireMinutes = $request->input('expireMinutes', 10); $code = \App\Models\LoginVerifyCode::generateCode(); $deviceHash = UserDevice::generateHash($request->userAgent() ?? '', $this->getRealIp($request) ?? ''); // 保存配对码 $verifyCode = \App\Models\LoginVerifyCode::create([ 'user_id' => $user->id, 'code' => $code, 'type' => 'pair', 'device_hash' => $deviceHash, 'ip' => $this->getRealIp($request), 'user_agent' => $request->userAgent(), 'expires_at' => now()->addMinutes($expireMinutes), ]); // 设置频率限制 Cache::put($rateKey, 1, now()->addSeconds(60)); Cache::put($rateKey . '_ttl', 60, now()->addSeconds(60)); // 发送邮件 $mailService = new MailService(); $sent = $mailService->sendPairCode($user->email, $code, $expireMinutes); if (!$sent) { \Log::warning("配对码邮件发送失败,但已生成配对码: {$code}"); } return response()->json([ 'code' => 200, 'data' => [ 'code' => $code, // 开发环境返回,方便测试 'expiresAt' => $verifyCode->expires_at->toIso8601String(), 'email' => $user->email, ], 'message' => '配对码已发送至绑定邮箱', ]); } /** * 验证配对码 */ public function verifyPairCode(Request $request) { $validator = Validator::make($request->all(), [ 'code' => 'required|string|size:8', // 改为8位 'email' => 'required|email', ]); if ($validator->fails()) { return response()->json(['code' => 422, 'message' => '验证失败', 'errors' => $validator->errors()], 422); } $user = User::where('email', $request->email)->first(); if (!$user) { return response()->json(['code' => 404, 'message' => '用户不存在'], 404); } $verifyCode = \App\Models\LoginVerifyCode::findValidCode($user, $request->code, 'pair'); if (!$verifyCode) { // 配对码无效,可能已被锁定或过期 return response()->json([ 'code' => 400, 'message' => '配对码无效或已过期', ], 400); } // 检查失败次数 if ($verifyCode->fail_count >= 5) { // 失败次数过多,标记为已使用(锁定) $verifyCode->markAsUsed(); return response()->json([ 'code' => 429, 'message' => '验证失败次数过多,请重新获取配对码', ], 429); } // 验证失败,增加失败计数 $verifyCode->increment('fail_count'); if ($verifyCode->fail_count >= 5) { $verifyCode->markAsUsed(); return response()->json([ 'code' => 429, 'message' => '验证失败次数过多,请重新获取配对码', ], 429); } // 标记已使用 $verifyCode->markAsUsed(); // 记录授信设备 $deviceInfo = UserDevice::parseDeviceInfo($request->userAgent() ?? ''); $deviceHash = UserDevice::generateHash($request->userAgent() ?? '', $this->getRealIp($request) ?? ''); $device = UserDevice::findByHash($user->id, $deviceHash); if ($device) { $device->trust(); $device->recordLogin(); } else { UserDevice::create([ 'user_id' => $user->id, 'device_hash' => $deviceHash, 'device_name' => ($deviceInfo['browser'] ?? 'Unknown') . ' on ' . ($deviceInfo['os'] ?? 'Unknown'), 'device_type' => $deviceInfo['device_type'] ?? 'desktop', 'os' => $deviceInfo['os'] ?? 'Unknown', 'browser' => $deviceInfo['browser'] ?? 'Unknown', 'ip' => $this->getRealIp($request), 'is_trusted' => true, 'trusted_at' => now(), 'first_login_at' => now(), 'last_login_at' => now(), ]); } // 创建token $token = $user->createToken('auth_token')->plainTextToken; // 记录登录日志 $this->logLogin($user, $request, 'pair', 'success'); return response()->json([ 'code' => 200, 'data' => [ 'user' => $this->formatUser($user), 'token' => $token, ], 'message' => '验证成功,已登录', ]); } /** * 登出所有设备 */ public function logoutAllDevices(Request $request) { $user = $request->user(); // 删除所有 token $user->tokens()->delete(); // 取消所有设备授信 UserDevice::where('user_id', $user->id)->update(['is_trusted' => false, 'trusted_at' => null]); return response()->json([ 'code' => 200, 'message' => '已登出所有设备', ]); } }