find(); if (!$user) { throw new ApiException('手机号未注册'); } if ((int) $user->status !== self::STATUS_NORMAL) { throw new ApiException('账号已被禁用'); } $hashed = $this->hashPassword($password); if ($user->password !== $hashed) { throw new ApiException('密码错误'); } $userArr = $user->hidden(['password'])->toArray(); UserCache::setUser((int) $user->id, $userArr); $userToken = $this->generateUserToken((int) $user->id); return [ 'user' => $userArr, 'user-token' => $userToken, 'user_id' => (int) $user->id, ]; } /** * 注册:手机号 + 密码(+60),创建玩家并返回用户信息与 user-token,写入 Redis */ public function register(string $phone, string $password, ?string $nickname = null): array { self::validatePhone($phone); if (strlen($password) < 6) { throw new ApiException('密码至少 6 位'); } $exists = DicePlayer::where('phone', $phone)->find(); if ($exists) { throw new ApiException('该手机号已注册'); } $user = new DicePlayer(); $user->phone = $phone; $user->username = $phone; if ($nickname !== null && $nickname !== '') { $user->name = $nickname; } // name 未传时由 DicePlayer::onBeforeInsert 默认设为 uid $user->password = $this->hashPassword($password); $user->status = self::STATUS_NORMAL; $user->save(); $userArr = $user->hidden(['password'])->toArray(); UserCache::setUser((int) $user->id, $userArr); $userToken = $this->generateUserToken((int) $user->id); return [ 'user' => $userArr, 'user-token' => $userToken, 'user_id' => (int) $user->id, ]; } /** * 与 DicePlayerLogic 一致的密码加密:md5(salt . password) */ private function hashPassword(string $password): string { return md5(self::PASSWORD_SALT . $password); } /** * 生成 user-token(JWT,plat=api_user,id=user_id) */ private function generateUserToken(int $userId): string { $exp = config('api.user_token_exp', 604800); $result = JwtToken::generateToken([ 'id' => $userId, 'plat' => 'api_user', 'access_exp' => $exp, ]); return $result['access_token']; } /** * 从请求中解析 user-token(header: user-token 或 Authorization: Bearer) * @param object $request 需有 header(string $name) 方法 */ public static function getTokenFromRequest(object $request): string { $token = $request->header('user-token') ?? ''; if ($token !== '') { return trim((string) $token); } $auth = $request->header('authorization'); if ($auth && stripos($auth, 'Bearer ') === 0) { return trim(substr($auth, 7)); } return ''; } /** * 从请求获取当前用户 ID:优先 request->user_id,否则从 header 的 user-token 解析 * 中间件未正确注入时仍可兜底解析 * @param object $request 需有 user_id 属性及 header() 方法 */ public static function getUserIdFromRequest(object $request): ?int { $id = $request->user_id ?? null; if ($id !== null && (int) $id > 0) { return (int) $id; } $token = self::getTokenFromRequest($request); if ($token === '') { return null; } return self::getUserIdFromToken($token); } /** * 根据 user-token 获取 user_id(不写缓存,仅解析 JWT) * 若 token 已通过退出接口加入黑名单,返回 null */ public static function getUserIdFromToken(string $userToken): ?int { if (UserCache::isTokenBlacklisted($userToken)) { return null; } try { $decoded = JwtToken::verify(1, $userToken); $extend = $decoded['extend'] ?? []; if (($extend['plat'] ?? '') !== 'api_user') { return null; } $id = $extend['id'] ?? null; return $id !== null ? (int) $id : null; } catch (\Throwable $e) { return null; } } /** * 退出登录:将当前 user-token 加入黑名单,使该 token 失效 */ public static function logout(string $userToken): bool { try { $decoded = JwtToken::verify(1, $userToken); $exp = (int) ($decoded['exp'] ?? 0); $ttl = $exp > time() ? $exp - time() : 86400; return UserCache::addTokenToBlacklist($userToken, $ttl); } catch (\Throwable $e) { return false; } } /** * 从 Redis 获取用户信息(key = base64(user_id)),未命中则查 DicePlayer 并回写缓存 */ public static function getCachedUser(int $userId): array { $cached = UserCache::getUser($userId); if (!empty($cached)) { return $cached; } $user = DicePlayer::find($userId); if (!$user) { return []; } $arr = $user->hidden(['password'])->toArray(); UserCache::setUser($userId, $arr); return $arr; } }