forked from datawhale/whale-town-front
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:
183
scripts/managers/AuthManager.gd
Normal file
183
scripts/managers/AuthManager.gd
Normal 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分钟
|
||||
1
scripts/managers/AuthManager.gd.uid
Normal file
1
scripts/managers/AuthManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dt434a8o6ye2p
|
||||
98
scripts/managers/EncryptionService.gd
Normal file
98
scripts/managers/EncryptionService.gd
Normal file
@@ -0,0 +1,98 @@
|
||||
extends Node
|
||||
|
||||
## 加密服务
|
||||
## 提供密码加密、解密等安全功能
|
||||
|
||||
# 固定的 Salt(用于生成设备密钥)
|
||||
const FIXED_SALT: String = "whaleTown_2024_secret_key"
|
||||
|
||||
# AES 加密密钥(基于设备ID生成)
|
||||
var _encryptionKey: PackedByteArray
|
||||
|
||||
func _ready():
|
||||
_initEncryptionKey()
|
||||
|
||||
## 初始化加密密钥
|
||||
func _initEncryptionKey() -> void:
|
||||
# 使用设备唯一ID + 固定Salt 生成加密密钥
|
||||
var deviceId = OS.get_unique_id()
|
||||
var keyString = deviceId + FIXED_SALT
|
||||
|
||||
# 使用 SHA-256 生成 32 字节密钥
|
||||
var crypto = Crypto.new()
|
||||
var hash = crypto.generate_random_bytes(32) # 临时方案,实际应该用 hash
|
||||
|
||||
# 简单实现:取字符串的 MD5 哈希的前32字节
|
||||
_encryptionKey = keyString.md5_buffer()
|
||||
# 如果需要32字节,重复一次
|
||||
_encryptionKey.append_array(keyString.md5_buffer())
|
||||
|
||||
## 加密字符串(用于本地存储密码)
|
||||
func encrypt(plainText: String) -> String:
|
||||
if plainText.is_empty():
|
||||
return ""
|
||||
|
||||
var crypto = Crypto.new()
|
||||
var aes = AESContext.new()
|
||||
|
||||
# 生成随机 IV(初始化向量)
|
||||
var iv = crypto.generate_random_bytes(16)
|
||||
|
||||
# 转换明文为字节
|
||||
var plainBytes = plainText.to_utf8_buffer()
|
||||
|
||||
# 使用 AES-256-CBC 加密
|
||||
aes.start(AESContext.MODE_CBC_ENCRYPT, _encryptionKey.slice(0, 32), iv)
|
||||
var encryptedBytes = aes.update(plainBytes)
|
||||
aes.finish()
|
||||
|
||||
# 将 IV 和加密数据组合(IV + encrypted_data)
|
||||
var combined = PackedByteArray()
|
||||
combined.append_array(iv)
|
||||
combined.append_array(encryptedBytes)
|
||||
|
||||
# 转换为 Base64 字符串
|
||||
return Marshalls.raw_to_base64(combined)
|
||||
|
||||
## 解密字符串
|
||||
func decrypt(encryptedText: String) -> String:
|
||||
if encryptedText.is_empty():
|
||||
return ""
|
||||
|
||||
# Base64 解码
|
||||
var combined = Marshalls.base64_to_raw(encryptedText)
|
||||
if combined.size() < 16:
|
||||
push_error("加密数据格式错误")
|
||||
return ""
|
||||
|
||||
# 分离 IV 和加密数据
|
||||
var iv = combined.slice(0, 16)
|
||||
var encryptedBytes = combined.slice(16)
|
||||
|
||||
# 解密
|
||||
var aes = AESContext.new()
|
||||
aes.start(AESContext.MODE_CBC_DECRYPT, _encryptionKey.slice(0, 32), iv)
|
||||
var decryptedBytes = aes.update(encryptedBytes)
|
||||
aes.finish()
|
||||
|
||||
# 转换为字符串
|
||||
return decryptedBytes.get_string_from_utf8()
|
||||
|
||||
## 生成密码哈希(用于网络传输)
|
||||
## 注意:这是简单实现,实际应该使用更安全的算法(如 bcrypt, scrypt)
|
||||
func hashPassword(password: String, salt: String = "") -> String:
|
||||
if password.is_empty():
|
||||
return ""
|
||||
|
||||
var combined = password + salt
|
||||
return combined.sha256_text()
|
||||
|
||||
## 验证密码哈希
|
||||
func verifyPassword(password: String, hashedPassword: String, salt: String = "") -> bool:
|
||||
return hashPassword(password, salt) == hashedPassword
|
||||
|
||||
## 生成随机 Salt
|
||||
func generateSalt(length: int = 16) -> String:
|
||||
var crypto = Crypto.new()
|
||||
var randomBytes = crypto.generate_random_bytes(length)
|
||||
return Marshalls.raw_to_base64(randomBytes)
|
||||
1
scripts/managers/EncryptionService.gd.uid
Normal file
1
scripts/managers/EncryptionService.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d1dmca6vem1hn
|
||||
282
scripts/managers/NetworkService.gd
Normal file
282
scripts/managers/NetworkService.gd
Normal file
@@ -0,0 +1,282 @@
|
||||
extends Node
|
||||
|
||||
## 网络服务
|
||||
## 封装 HTTP 请求,提供统一的网络通信接口
|
||||
|
||||
# API 基础 URL
|
||||
const API_BASE_URL: String = "https://whaletownui.angforever.top"
|
||||
|
||||
# 请求超时时间(秒)
|
||||
const REQUEST_TIMEOUT: float = 30.0
|
||||
|
||||
# 最大重试次数
|
||||
const MAX_RETRY: int = 3
|
||||
|
||||
# 是否开启调试日志(显示请求和响应详情)
|
||||
const DEBUG_MODE: bool = true
|
||||
|
||||
## 发送 POST 请求
|
||||
func post(endpoint: String, data: Dictionary, headers: Array = []) -> Dictionary:
|
||||
var url = API_BASE_URL + endpoint
|
||||
return await _request(url, HTTPClient.METHOD_POST, data, headers)
|
||||
|
||||
## 发送 GET 请求
|
||||
func sendGet(endpoint: String, headers: Array = []) -> Dictionary:
|
||||
var url = API_BASE_URL + endpoint
|
||||
return await _request(url, HTTPClient.METHOD_GET, {}, headers)
|
||||
|
||||
## 发送带 Token 的 POST 请求
|
||||
func authenticatedPost(endpoint: String, data: Dictionary) -> Dictionary:
|
||||
var tokenData = StorageService.loadToken()
|
||||
var token = tokenData.get("token", "")
|
||||
|
||||
if token.is_empty():
|
||||
return {
|
||||
"success": false,
|
||||
"error_code": ErrorCode.TOKEN_INVALID,
|
||||
"message": ErrorCode.getMessage(ErrorCode.TOKEN_INVALID)
|
||||
}
|
||||
|
||||
var headers = [
|
||||
"Authorization: Bearer " + token
|
||||
]
|
||||
|
||||
return await post(endpoint, data, headers)
|
||||
|
||||
## 核心请求方法
|
||||
func _request(url: String, method: HTTPClient.Method, data: Dictionary, customHeaders: Array) -> Dictionary:
|
||||
# 创建 HTTPRequest 节点
|
||||
var httpRequest = HTTPRequest.new()
|
||||
add_child(httpRequest)
|
||||
|
||||
# 设置超时
|
||||
httpRequest.timeout = REQUEST_TIMEOUT
|
||||
|
||||
# 构建请求头
|
||||
var headers = [
|
||||
"Content-Type: application/json",
|
||||
"Accept: application/json"
|
||||
]
|
||||
headers.append_array(customHeaders)
|
||||
|
||||
# 准备请求体
|
||||
var body = ""
|
||||
if method == HTTPClient.METHOD_POST or method == HTTPClient.METHOD_PUT:
|
||||
body = JSON.stringify(data)
|
||||
|
||||
# 调试日志:请求信息
|
||||
if DEBUG_MODE:
|
||||
_logRequest(url, method, headers, body)
|
||||
|
||||
# 发送请求
|
||||
var error = httpRequest.request(url, headers, method, body)
|
||||
|
||||
if error != OK:
|
||||
httpRequest.queue_free()
|
||||
return _createErrorResponse(ErrorCode.NETWORK_ERROR, "发送请求失败")
|
||||
|
||||
# 等待响应
|
||||
var result = await httpRequest.request_completed
|
||||
|
||||
# 调试日志:响应信息
|
||||
if DEBUG_MODE:
|
||||
_logResponse(result)
|
||||
|
||||
# 清理
|
||||
httpRequest.queue_free()
|
||||
|
||||
# 解析响应
|
||||
return _parseResponse(result)
|
||||
|
||||
## 解析 HTTP 响应
|
||||
func _parseResponse(result: Array) -> Dictionary:
|
||||
var httpResult: int = result[0]
|
||||
var responseCode: int = result[1]
|
||||
var _headers: PackedStringArray = result[2]
|
||||
var body: PackedByteArray = result[3]
|
||||
|
||||
# 检查 HTTP 请求错误
|
||||
if httpResult != HTTPRequest.RESULT_SUCCESS:
|
||||
return _handleHttpError(httpResult)
|
||||
|
||||
# 检查 HTTP 状态码
|
||||
if responseCode < 200 or responseCode >= 300:
|
||||
return _handleStatusCodeError(responseCode, body)
|
||||
|
||||
# 解析 JSON 响应体
|
||||
var bodyString = body.get_string_from_utf8()
|
||||
if bodyString.is_empty():
|
||||
return _createErrorResponse(ErrorCode.SERVER_ERROR, "服务器返回空响应")
|
||||
|
||||
var json = JSON.new()
|
||||
var parseError = json.parse(bodyString)
|
||||
|
||||
if parseError != OK:
|
||||
push_error("JSON 解析失败: " + bodyString)
|
||||
return _createErrorResponse(ErrorCode.SERVER_ERROR, "响应数据格式错误")
|
||||
|
||||
var responseData = json.data
|
||||
|
||||
# 根据后端API格式解析(通用格式,后续可能需要调整)
|
||||
if typeof(responseData) == TYPE_DICTIONARY:
|
||||
# 检查是否有 success 字段(新版 API 格式)
|
||||
if responseData.has("success"):
|
||||
var success = responseData.get("success", false)
|
||||
if success:
|
||||
return {
|
||||
"success": true,
|
||||
"data": responseData.get("data", {}),
|
||||
"message": responseData.get("message", "操作成功")
|
||||
}
|
||||
else:
|
||||
# 业务错误
|
||||
return {
|
||||
"success": false,
|
||||
"error_code": responseData.get("error_code", ErrorCode.SERVER_ERROR),
|
||||
"message": responseData.get("message", "操作失败")
|
||||
}
|
||||
# 检查是否有 code 字段(旧版 API 格式)
|
||||
elif responseData.has("code"):
|
||||
var code = responseData.get("code", -1)
|
||||
if code == 0 or code == 200: # 成功
|
||||
return {
|
||||
"success": true,
|
||||
"data": responseData.get("data", {}),
|
||||
"message": responseData.get("message", "操作成功")
|
||||
}
|
||||
else: # 业务错误
|
||||
return {
|
||||
"success": false,
|
||||
"error_code": code,
|
||||
"message": responseData.get("message", "操作失败")
|
||||
}
|
||||
else:
|
||||
# 假设整个响应就是数据
|
||||
return {
|
||||
"success": true,
|
||||
"data": responseData,
|
||||
"message": "操作成功"
|
||||
}
|
||||
|
||||
return _createErrorResponse(ErrorCode.SERVER_ERROR, "响应格式不正确")
|
||||
|
||||
## 处理 HTTP 请求错误
|
||||
func _handleHttpError(httpResult: int) -> Dictionary:
|
||||
match httpResult:
|
||||
HTTPRequest.RESULT_TIMEOUT:
|
||||
return _createErrorResponse(ErrorCode.TIMEOUT, "请求超时")
|
||||
HTTPRequest.RESULT_CONNECTION_ERROR:
|
||||
return _createErrorResponse(ErrorCode.NETWORK_ERROR, "网络连接失败")
|
||||
HTTPRequest.RESULT_CANT_CONNECT:
|
||||
return _createErrorResponse(ErrorCode.NETWORK_ERROR, "无法连接到服务器")
|
||||
_:
|
||||
return _createErrorResponse(ErrorCode.NETWORK_ERROR, "网络请求失败: " + str(httpResult))
|
||||
|
||||
## 处理 HTTP 状态码错误
|
||||
func _handleStatusCodeError(statusCode: int, body: PackedByteArray) -> Dictionary:
|
||||
var bodyString = body.get_string_from_utf8()
|
||||
|
||||
# 尝试解析错误信息
|
||||
var json = JSON.new()
|
||||
if json.parse(bodyString) == OK:
|
||||
var data = json.data
|
||||
if typeof(data) == TYPE_DICTIONARY and data.has("message"):
|
||||
var message = data.get("message", "")
|
||||
# 确保 message 是字符串类型
|
||||
if typeof(message) == TYPE_ARRAY:
|
||||
message = ", ".join(message)
|
||||
elif typeof(message) != TYPE_STRING:
|
||||
message = str(message)
|
||||
return _createErrorResponse(statusCode, message)
|
||||
|
||||
# 通用状态码错误处理
|
||||
match statusCode:
|
||||
400:
|
||||
return _createErrorResponse(statusCode, "请求参数错误")
|
||||
401:
|
||||
return _createErrorResponse(statusCode, "未授权,请重新登录")
|
||||
403:
|
||||
return _createErrorResponse(statusCode, "没有权限访问")
|
||||
404:
|
||||
return _createErrorResponse(statusCode, "请求的资源不存在")
|
||||
500:
|
||||
return _createErrorResponse(statusCode, "服务器内部错误")
|
||||
502:
|
||||
return _createErrorResponse(statusCode, "网关错误")
|
||||
503:
|
||||
return _createErrorResponse(statusCode, "服务暂时不可用")
|
||||
_:
|
||||
return _createErrorResponse(statusCode, "HTTP 错误: " + str(statusCode))
|
||||
|
||||
## 创建错误响应
|
||||
func _createErrorResponse(errorCode: int, message: String) -> Dictionary:
|
||||
return {
|
||||
"success": false,
|
||||
"error_code": errorCode,
|
||||
"message": message
|
||||
}
|
||||
|
||||
## 记录请求日志
|
||||
func _logRequest(url: String, method: HTTPClient.Method, headers: Array, body: String) -> void:
|
||||
print("\n========== HTTP 请求 ==========")
|
||||
print("URL: ", url)
|
||||
print("方法: ", _getMethodName(method))
|
||||
print("请求头:")
|
||||
for header in headers:
|
||||
print(" ", header)
|
||||
if not body.is_empty():
|
||||
print("请求体:")
|
||||
# 尝试格式化 JSON
|
||||
var json = JSON.new()
|
||||
if json.parse(body) == OK:
|
||||
print(" ", JSON.stringify(json.data, " "))
|
||||
else:
|
||||
print(" ", body)
|
||||
print("==============================\n")
|
||||
|
||||
## 记录响应日志
|
||||
func _logResponse(result: Array) -> void:
|
||||
var httpResult: int = result[0]
|
||||
var responseCode: int = result[1]
|
||||
var responseHeaders: PackedStringArray = result[2]
|
||||
var body: PackedByteArray = result[3]
|
||||
|
||||
print("\n========== HTTP 响应 ==========")
|
||||
print("请求结果: ", _getHttpResultName(httpResult))
|
||||
print("状态码: ", responseCode)
|
||||
print("响应头:")
|
||||
for header in responseHeaders:
|
||||
print(" ", header)
|
||||
|
||||
var bodyString = body.get_string_from_utf8()
|
||||
if not bodyString.is_empty():
|
||||
print("响应体:")
|
||||
# 尝试格式化 JSON
|
||||
var json = JSON.new()
|
||||
if json.parse(bodyString) == OK:
|
||||
print(" ", JSON.stringify(json.data, " "))
|
||||
else:
|
||||
print(" ", bodyString)
|
||||
else:
|
||||
print("响应体: (空)")
|
||||
print("==============================\n")
|
||||
|
||||
## 获取 HTTP 方法名称
|
||||
func _getMethodName(method: HTTPClient.Method) -> String:
|
||||
match method:
|
||||
HTTPClient.METHOD_GET: return "GET"
|
||||
HTTPClient.METHOD_POST: return "POST"
|
||||
HTTPClient.METHOD_PUT: return "PUT"
|
||||
HTTPClient.METHOD_DELETE: return "DELETE"
|
||||
HTTPClient.METHOD_PATCH: return "PATCH"
|
||||
_: return "UNKNOWN"
|
||||
|
||||
## 获取 HTTP 请求结果名称
|
||||
func _getHttpResultName(result: int) -> String:
|
||||
match result:
|
||||
HTTPRequest.RESULT_SUCCESS: return "成功"
|
||||
HTTPRequest.RESULT_TIMEOUT: return "超时"
|
||||
HTTPRequest.RESULT_CONNECTION_ERROR: return "连接错误"
|
||||
HTTPRequest.RESULT_CANT_CONNECT: return "无法连接"
|
||||
HTTPRequest.RESULT_NO_RESPONSE: return "无响应"
|
||||
_: return "错误 (" + str(result) + ")"
|
||||
1
scripts/managers/NetworkService.gd.uid
Normal file
1
scripts/managers/NetworkService.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ple3a8luflmb
|
||||
124
scripts/managers/StorageService.gd
Normal file
124
scripts/managers/StorageService.gd
Normal file
@@ -0,0 +1,124 @@
|
||||
extends Node
|
||||
|
||||
## 存储服务
|
||||
## 负责本地配置的读写,包括用户账号、密码、Token 等
|
||||
|
||||
# 配置文件路径
|
||||
const CONFIG_PATH: String = "user://user_config.cfg"
|
||||
|
||||
# ConfigFile 实例
|
||||
var _config: ConfigFile
|
||||
|
||||
func _ready():
|
||||
_config = ConfigFile.new()
|
||||
_loadConfig()
|
||||
|
||||
## 加载配置文件
|
||||
func _loadConfig() -> void:
|
||||
var error = _config.load(CONFIG_PATH)
|
||||
if error != OK and error != ERR_FILE_NOT_FOUND:
|
||||
push_error("加载配置文件失败: " + str(error))
|
||||
|
||||
## 保存配置文件
|
||||
func _saveConfig() -> void:
|
||||
var error = _config.save(CONFIG_PATH)
|
||||
if error != OK:
|
||||
push_error("保存配置文件失败: " + str(error))
|
||||
|
||||
## 保存键值对
|
||||
func save(section: String, key: String, value: Variant) -> void:
|
||||
_config.set_value(section, key, value)
|
||||
_saveConfig()
|
||||
|
||||
## 读取值
|
||||
func getValue(section: String, key: String, defaultValue: Variant = null) -> Variant:
|
||||
return _config.get_value(section, key, defaultValue)
|
||||
|
||||
## 保存账号信息(包括加密的密码)
|
||||
func saveAccount(username: String, password: String, remember: bool) -> void:
|
||||
save("account", "last_username", username)
|
||||
|
||||
if remember and not password.is_empty():
|
||||
# 加密密码后保存
|
||||
var encryptedPassword = EncryptionService.encrypt(password)
|
||||
save("account", "saved_password", encryptedPassword)
|
||||
save("account", "remember_password", true)
|
||||
else:
|
||||
# 不记住密码,清除保存的密码
|
||||
clearSavedPassword()
|
||||
|
||||
## 读取保存的账号信息
|
||||
func loadAccount() -> Dictionary:
|
||||
var username = getValue("account", "last_username", "")
|
||||
var rememberPassword = getValue("account", "remember_password", false)
|
||||
var encryptedPassword = getValue("account", "saved_password", "")
|
||||
|
||||
var result = {
|
||||
"username": username,
|
||||
"remember_password": rememberPassword
|
||||
}
|
||||
|
||||
# 如果记住密码,解密密码
|
||||
if rememberPassword and not encryptedPassword.is_empty():
|
||||
var decryptedPassword = EncryptionService.decrypt(encryptedPassword)
|
||||
result["password"] = decryptedPassword
|
||||
else:
|
||||
result["password"] = ""
|
||||
|
||||
return result
|
||||
|
||||
## 清除保存的密码
|
||||
func clearSavedPassword() -> void:
|
||||
save("account", "saved_password", "")
|
||||
save("account", "remember_password", false)
|
||||
|
||||
## 保存 Token
|
||||
func saveToken(token: String, expireTime: int = 0) -> void:
|
||||
save("auth", "token", token)
|
||||
save("auth", "expire_time", expireTime)
|
||||
|
||||
## 读取 Token
|
||||
func loadToken() -> Dictionary:
|
||||
var token = getValue("auth", "token", "")
|
||||
var expireTime = getValue("auth", "expire_time", 0)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"expire_time": expireTime
|
||||
}
|
||||
|
||||
## 清除 Token
|
||||
func clearToken() -> void:
|
||||
save("auth", "token", "")
|
||||
save("auth", "expire_time", 0)
|
||||
|
||||
## 保存用户信息
|
||||
func saveUserData(userData: UserData) -> void:
|
||||
if userData == null:
|
||||
return
|
||||
|
||||
save("user", "id", userData.id)
|
||||
save("user", "username", userData.username)
|
||||
save("user", "email", userData.email)
|
||||
save("user", "level", userData.level)
|
||||
save("user", "exp", userData.exp)
|
||||
save("user", "avatar_url", userData.avatarUrl)
|
||||
|
||||
## 读取用户信息
|
||||
func loadUserData() -> UserData:
|
||||
var userData = UserData.new()
|
||||
userData.id = str(getValue("user", "id", ""))
|
||||
userData.username = getValue("user", "username", "")
|
||||
userData.email = getValue("user", "email", "")
|
||||
userData.level = getValue("user", "level", 1)
|
||||
userData.exp = getValue("user", "exp", 0)
|
||||
userData.avatarUrl = getValue("user", "avatar_url", "")
|
||||
|
||||
return userData if not userData.id.is_empty() else null
|
||||
|
||||
## 清除所有用户数据
|
||||
func clearAll() -> void:
|
||||
clearSavedPassword()
|
||||
clearToken()
|
||||
_config.clear()
|
||||
_saveConfig()
|
||||
1
scripts/managers/StorageService.gd.uid
Normal file
1
scripts/managers/StorageService.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cotaeupq0tpea
|
||||
Reference in New Issue
Block a user