feat:实现登录系统和用户认证功能

- 添加登录场景(login_scene.tscn)和主菜单场景(main_menu_scene.tscn)
- 实现认证管理器(AuthManager)用于用户登录和会话管理
- 添加核心服务:加密服务、存储服务、网络服务
- 配置项目主场景为登录场景
- 添加自动加载服务到项目配置
- 添加开发环境配置(VSCode、Claude)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-23 01:15:33 +08:00
parent 8ed260b413
commit d623c705b6
28 changed files with 1455 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
extends Node
## 认证管理器
## 统一管理用户登录、登出、Token等认证相关功能
# 登录状态枚举
enum LoginState {
NOT_LOGGED_IN, # 未登录
LOGGING_IN, # 登录中
LOGGED_IN, # 已登录
GUEST # 游客模式
}
# 信号
signal login_success(userData: UserData)
signal login_failed(errorCode: int, errorMessage: String)
signal logout_success()
# 当前登录状态
var currentState: LoginState = LoginState.NOT_LOGGED_IN
# Token
var accessToken: String = ""
var tokenExpireTime: int = 0
# 当前用户信息
var currentUser: UserData = null
# 是否正在登录(防止重复请求)
var _isLoggingIn: bool = false
func _ready():
# 游戏启动时尝试从本地加载 Token 和用户信息
_tryLoadLocalAuth()
## 登录
func login(username: String, password: String, rememberPassword: bool) -> Dictionary:
# 防止重复登录
if _isLoggingIn:
return {
"success": false,
"error_code": ErrorCode.INVALID_INPUT,
"message": "登录中,请稍候..."
}
_isLoggingIn = true
currentState = LoginState.LOGGING_IN
# 构建登录请求数据
var requestData = {
"identifier": username,
"password": password
}
# 发送登录请求
var response = await NetworkService.post("/api/auth/login", requestData)
_isLoggingIn = false
# 处理响应
if response.success:
# 解析用户数据和 Token
var data = response.get("data", {})
var token = data.get("access_token", "")
var userData = data.get("user", {})
if token.is_empty():
var errorMsg = "服务器返回数据格式错误"
login_failed.emit(ErrorCode.SERVER_ERROR, errorMsg)
currentState = LoginState.NOT_LOGGED_IN
return {
"success": false,
"error_code": ErrorCode.SERVER_ERROR,
"message": errorMsg
}
# 保存 Token
accessToken = token
tokenExpireTime = int(Time.get_unix_time_from_system()) + 3600 * 24 # 假设24小时过期
StorageService.saveToken(accessToken, tokenExpireTime)
# 保存用户信息
currentUser = UserData.fromDict(userData)
StorageService.saveUserData(currentUser)
# 如果勾选记住密码,保存账号密码
if rememberPassword:
StorageService.saveAccount(username, password, true)
else:
# 只保存用户名,不保存密码
StorageService.save("account", "last_username", username)
StorageService.clearSavedPassword()
# 更新状态
currentState = LoginState.LOGGED_IN
# 发送登录成功信号
login_success.emit(currentUser)
return {
"success": true,
"message": "登录成功",
"user": currentUser
}
else:
# 登录失败
var errorCode = response.get("error_code", ErrorCode.WRONG_CREDENTIALS)
var errorMsg = response.get("message", "登录失败")
currentState = LoginState.NOT_LOGGED_IN
login_failed.emit(errorCode, errorMsg)
return {
"success": false,
"error_code": errorCode,
"message": errorMsg
}
## 登出
func logout() -> void:
# 清除 Token 和用户信息
accessToken = ""
tokenExpireTime = 0
currentUser = null
# 清除本地存储(保留用户名和记住密码设置)
StorageService.clearToken()
# 更新状态
currentState = LoginState.NOT_LOGGED_IN
# 发送登出成功信号
logout_success.emit()
print("用户已登出")
## 检查是否已登录
func isLoggedIn() -> bool:
return currentState == LoginState.LOGGED_IN and currentUser != null
## 获取当前用户
func getCurrentUser() -> UserData:
return currentUser
## 获取当前 Token
func getToken() -> String:
return accessToken
## 尝试从本地加载认证信息
func _tryLoadLocalAuth() -> void:
# 加载 Token
var tokenData = StorageService.loadToken()
var token = tokenData.get("token", "")
var expireTime = tokenData.get("expire_time", 0)
# 检查 Token 是否有效
var currentTime = int(Time.get_unix_time_from_system())
if token.is_empty() or expireTime <= currentTime:
print("本地 Token 无效或已过期")
return
# Token 有效,加载用户信息
var userData = StorageService.loadUserData()
if userData == null:
print("本地用户信息不存在")
return
# 恢复登录状态
accessToken = token
tokenExpireTime = expireTime
currentUser = userData
currentState = LoginState.LOGGED_IN
print("从本地恢复登录状态: ", currentUser.username)
# 可选:静默刷新 Token如果接近过期
# _refreshTokenIfNeeded()
## 检查 Token 是否即将过期
func isTokenExpiring() -> bool:
var currentTime = int(Time.get_unix_time_from_system())
var timeLeft = tokenExpireTime - currentTime
return timeLeft < 300 # 少于5分钟