dao = $dao; } /** * 用户登录 * * @throws \Exception */ public function login(array $data): array { // 验证参数 if (empty($data['username']) || empty($data['password'])) { throw new \Exception('用户名和密码不能为空', 400); } // 查找用户(支持用户名、手机号、邮箱登录) $member = $this->dao->query() ->where(function ($query) use ($data) { $query->where('username', $data['username']) ->orWhere('phone', $data['username']) ->orWhere('email', $data['username']); })->first(); if (!$member) { throw new \Exception('用户不存在', 401); } // 验证用户状态 if ($member->enabled !== EnabledStatus::ENABLED->value) { throw new \Exception('用户已被禁用', 401); } // 验证密码 if (!$member->verifyPassword($data['password'])) { throw new \Exception('密码错误', 401); } // 更新登录信息 $this->updateLoginInfo($member); $channel = strtolower((string)request()->header('channel', 'pc')); $clientType = $channel === 'app' ? 'app' : 'web'; $userInfo = [ 'id' => $member->id, 'username' => $member->username, 'nickname' => $member->nickname, 'avatar' => $member->avatar, 'phone' => $member->phone, 'email' => $member->email, 'client' => $clientType, 'access_exp' => 3600, 'refresh_exp' => 604800, ]; // 使用新的 JwtToken 生成 token $jwt = new JwtToken(); $tokenObj = $jwt->generate((string)$member->id, $clientType, $userInfo); $userInfo['access_exp'] = $tokenObj->expiresIn; $token = [ 'access_token' => $tokenObj->accessToken, 'refresh_token' => $tokenObj->refreshToken, 'expires_in' => $tokenObj->expiresIn ]; return array_merge($token, ['user_info' => $userInfo]); } /** * 手机验证码登录 */ public function loginWithMobile(array $data): array { if (empty($data['mobile']) || empty($data['code'])) { throw new \Exception('手机号和验证码不能为空', 400); } // 验证验证码 $this->verifySmsCode($data['mobile'], $data['code']); // 查找用户 $member = $this->dao->query()->where('phone', $data['mobile'])->first(); if (!$member) { // 自动注册 $member = $this->autoRegisterWithMobile($data['mobile']); } // 验证用户状态 if ($member->enabled !== EnabledStatus::ENABLED->value) { throw new \Exception('用户已被禁用', 401); } // 更新登录信息 $this->updateLoginInfo($member); // 生成token $channel = strtolower((string)request()->header('channel', 'pc')); $clientType = $channel === 'app' ? 'app' : 'web'; $userInfo = [ 'id' => $member->id, 'username' => $member->username, 'nickname' => $member->nickname, 'avatar' => $member->avatar, 'phone' => $member->phone, 'email' => $member->email, 'client' => $clientType, 'access_exp' => 3600, 'refresh_exp' => 604800, ]; // 使用新的 JwtToken 生成 token $jwt = new JwtToken(); $tokenObj = $jwt->generate((string)$member->id, $clientType, $userInfo); $userInfo['access_exp'] = $tokenObj->expiresIn; $token = [ 'access_token' => $tokenObj->accessToken, 'refresh_token' => $tokenObj->refreshToken, 'expires_in' => $tokenObj->expiresIn ]; return [ 'token' => $token, 'user_info' => [ 'id' => $member->id, 'username' => $member->username, 'nickname' => $member->nickname, 'avatar' => $member->avatar, 'phone' => $member->phone, 'email' => $member->email, 'points' => $member->points, 'balance' => $member->balance, ], ]; } /** * 用户退出登录 */ public function logout(): array { $request = \request(); $token = $request->header('Authorization'); if ($token) { // 处理 token 前缀 if (str_starts_with($token, 'Bearer ')) { $token = substr($token, 7); } // 使用新的 JwtToken 实现退出登录 try { (new JwtToken())->logout($token); } catch (\Exception $e) { // 忽略异常,确保退出成功 } } return [ 'message' => '退出成功', ]; } /** * 用户注册 */ public function register(array $data): array { // 创建用户 $memberData = [ 'username' => $data['username'], 'password' => $data['password'], 'nickname' => $data['username'], 'phone' => $data['mobile'] ?? '', 'email' => $data['email'] ?? null, 'enabled' => EnabledStatus::ENABLED->value, ]; $member = $this->dao->save($memberData); // 更新登录信息 $this->updateLoginInfo($member); $userInfo = [ 'id' => $member->id, 'username' => $member->username, 'nickname' => $member->nickname, 'avatar' => $member->avatar, 'phone' => $member->phone, 'email' => $member->email, 'client' => 'web', 'access_exp' => 7200, 'refresh_exp' => 604800, ]; // 使用新的 JwtToken 生成 token $jwt = new JwtToken(); $tokenObj = $jwt->generate((string)$member->id, 'api', $userInfo); $token = [ 'access_token' => $tokenObj->accessToken, 'refresh_token' => $tokenObj->refreshToken, 'expires_in' => $tokenObj->expiresIn ]; return array_merge($token, [ 'user_info' => [ 'id' => $member->id, 'username' => $member->username, 'nickname' => $member->nickname, 'avatar' => $member->avatar, 'phone' => $member->phone, 'email' => $member->email, 'points' => $member->points, 'balance' => $member->balance, ], ]); } /** * 手机号注册 */ public function registerWithMobile(array $data): array { if (empty($data['mobile']) || empty($data['code']) || empty($data['password'])) { throw new \Exception('手机号、验证码和密码不能为空', 400); } // 验证验证码 $this->verifySmsCode($data['mobile'], $data['code']); // 检查手机号是否已存在 if ($this->dao->query()->where('phone', $data['mobile'])->exists()) { throw new \Exception('手机号已存在', 400); } // 创建用户 $memberData = [ 'username' => 'user_' . substr($data['mobile'], -4), 'password' => $data['password'], 'nickname' => '用户_' . substr($data['mobile'], -4), 'phone' => $data['mobile'], 'enabled' => EnabledStatus::ENABLED->value, ]; $member = $this->dao->create($memberData); return [ 'user_id' => $member->id, ]; } /** * 绑定手机号 */ public function bindMobile(array $data): array { if (empty($data['mobile']) || empty($data['code'])) { throw new \Exception('手机号和验证码不能为空', 400); } // 验证验证码 $this->verifySmsCode($data['mobile'], $data['code']); // 获取当前用户 $memberId = $this->getCurrentMemberId(); $member = $this->dao->find($memberId); if (!$member) { throw new \Exception('用户不存在', 401); } // 检查手机号是否已被其他用户绑定 $existingMember = $this->dao->query() ->where('phone', $data['mobile']) ->where('id', '!=', $memberId) ->first(); if ($existingMember) { throw new \Exception('手机号已被其他用户绑定', 400); } // 更新手机号 $member->phone = $data['mobile']; $member->save(); return [ 'message' => '绑定成功', ]; } /** * 更新登录信息 */ private function updateLoginInfo(Member $member): void { $member->last_login_time = time(); $member->last_login_ip = \request()->getRealIp(); $member->login_count = $member->login_count + 1; $member->save(); } /** * 生成token */ private function generateToken(int $memberId): string { $token = md5(uniqid() . $memberId . microtime(true)); $expireTime = 7 * 24 * 3600; // 7天过期 // 存储token到Redis Redis::setex('user_token:' . md5($token), $expireTime, json_encode([ 'member_id' => $memberId, 'login_time' => time(), ])); return $token; } /** * 验证短信验证码 */ private function verifySmsCode(string $mobile, string $code): void { $cacheKey = 'sms_code:' . $mobile; $cachedCode = Redis::get($cacheKey); if (!$cachedCode || $cachedCode !== $code) { throw new \Exception('验证码错误或已过期', 400); } // 验证成功后删除验证码 Redis::del($cacheKey); } /** * 验证邮箱和验证码(忘记密码第一步) * * @throws \Exception */ public function verifyEmail(array $data): array { // 验证参数 if (empty($data['email']) || empty($data['code'])) { throw new \Exception('邮箱和验证码不能为空', 400); } // 验证验证码 if (isset($data['captcha_key']) && isset($data['captcha_code'])) { $this->verifyCaptcha($data['captcha_key'], $data['captcha_code']); } // 检查邮箱是否存在 $member = $this->dao->query()->where('email', $data['email'])->first(); // 生成临时token用于第二步密码重置 $resetToken = $this->generateResetToken($member->id); return [ 'reset_token' => $resetToken, 'email' => $member->email, ]; } /** * 重置密码(忘记密码第二步) * * @throws \Exception */ public function forgetPassword(array $data): void { // 验证参数 if (empty($data['email']) || empty($data['code']) || empty($data['new_password'])) { throw new \Exception('邮箱、验证码和新密码不能为空', 400); } // 验证新密码和确认密码是否一致 if (isset($data['confirm_password']) && $data['new_password'] !== $data['confirm_password']) { throw new \Exception('两次输入的密码不一致', 400); } // 验证重置token(如果使用) if (isset($data['reset_token'])) { $this->validateResetToken($data['reset_token']); } // 查找用户 $member = $this->dao->query()->where('email', $data['email'])->first(); if (!$member) { throw new \Exception('用户不存在', 404); } // 验证用户状态 if ($member->enabled !== EnabledStatus::ENABLED->value) { throw new \Exception('用户已被禁用', 401); } // 验证邮箱验证码 $cacheKey = 'email_code:' . $data['email']; $cachedCode = Redis::get($cacheKey); if (!$cachedCode || $cachedCode !== $data['code']) { throw new \Exception('邮箱验证码错误或已过期', 400); } // 更新密码 $member->password = $data['new_password']; $member->save(); // 清除验证码 Redis::del($cacheKey); } /** * 验证图片验证码 * * @throws \Exception */ private function verifyCaptcha(string $key, string $code): void { $cachedCode = Redis::get('captcha:' . $key); if (!$cachedCode || strtoupper($cachedCode) !== strtoupper($code)) { throw new \Exception('验证码错误', -1); } // 验证成功后删除验证码 Redis::del('captcha:' . $key); } /** * 生成密码重置token */ private function generateResetToken(int $memberId): string { $token = md5(uniqid() . $memberId . microtime(true)); // 存储token到Redis,有效期30分钟 Redis::setex('password_reset:' . $token, 1800, json_encode([ 'member_id' => $memberId, 'expire' => time() + 1800, ])); return $token; } /** * 验证密码重置token * * @throws \Exception */ private function validateResetToken(string $token): void { $tokenData = Redis::get('password_reset:' . $token); if (!$tokenData) { throw new \Exception('重置链接已过期,请重新获取', 400); } $data = json_decode($tokenData, true); if ($data['expire'] < time()) { Redis::del('password_reset:' . $token); throw new \Exception('重置链接已过期,请重新获取', 400); } } /** * 发送邮箱验证码 * * @throws \Exception */ public function sendEmailCode(string $email): void { // 验证邮箱格式 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \Exception('邮箱格式不正确', 400); } // 检查邮箱是否存在 $member = $this->dao->query()->where('email', $email)->first(); if (!$member) { throw new \Exception('该邮箱未注册', 404); } // 检查是否频繁发送 $cacheKey = 'email_code_limit:' . $email; $lastSend = Redis::get($cacheKey); if ($lastSend && (time() - $lastSend) < 60) { throw new \Exception('发送过于频繁,请稍后再试', 400); } // 生成验证码 $code = str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT); // 存储验证码到Redis,有效期5分钟 Redis::setex('email_code:' . $email, 300, $code); // 记录发送时间 Redis::setex('email_code_limit:' . $email, 60, time()); // 获取系统配置 $configService = Container::make(ConfigService::class); $siteConfig = $configService->config('web_site_setting'); $siteName = $siteConfig['site_name'] ?? 'madong.tech'; // 构建邮件主题和内容 $subject = $siteName . '社区找回密码'; $content = $this->buildResetPasswordEmailTemplate($code, $member->nickname ?? $member->username); // 发送邮件 /** @var MailService $mailService */ $mailService = Container::make(MailService::class); $mailService->send($email, $subject, $content); } /** * 构建密码重置邮件模板 * * @param string $code 验证码 * @param string $nickname 用户昵称 * * @return string HTML邮件内容 */ private function buildResetPasswordEmailTemplate(string $code, string $nickname): string { // 获取系统配置 $configService = Container::make(ConfigService::class); $siteConfig = $configService->config('web_site_setting'); $siteName = $siteConfig['site_name'] ?? "madong.tech"; $year = date('Y'); return <<