diff --git a/.gitignore b/.gitignore index 0af181c..6e431e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,58 @@ # Godot 4+ specific ignores .godot/ /android/ + +# Python cache files +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.so + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Godot executable files (should not be in version control) +Godot/ +*.exe +*.app +*.dmg + +# Logs and temporary files +*.log +*.tmp +*.temp + +# Build outputs +build/ +dist/ +*.zip +*.tar.gz + +# Environment files +.env +.env.local +.env.*.local + +# Test coverage reports +coverage/ +*.coverage +.nyc_output/ + +# Dependency directories +node_modules/ +vendor/ diff --git a/README.md b/README.md index 24f1063..ad8b357 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,274 @@ -# whaleTown +# 🐋 whaleTown -一个使用 Godot 4.5 引擎开发的游戏项目。 +一个使用 Godot 4.5 引擎开发的现代化像素游戏项目,集成了完整的用户认证系统和API接口。 -## 项目信息 +## 🎮 项目信息 -- **引擎版本**: Godot 4.5 +- **引擎版本**: Godot 4.5.1 - **渲染器**: Forward Plus -- **项目类型**: 2D 游戏 +- **项目类型**: 2D 像素游戏 +- **架构模式**: 模块化 + 事件驱动 +- **后端集成**: RESTful API + 用户认证 -## 项目结构 +## 🚀 快速开始 -``` -whaleTown/ -├── addons/ # Godot 插件目录 -├── assets/ # 游戏资源文件(图片、音频等) -├── data/ # 游戏数据文件(配置、关卡数据等) -├── docs/ # 项目文档 -├── scenes/ # 游戏场景文件 -│ └── main_scene.tscn # 主场景 -├── scripts/ # GDScript 脚本文件 -├── tests/ # 测试文件 -├── icon.svg # 项目图标 -└── project.godot # Godot 项目配置文件 -``` - -## 开始使用 - -### 前置要求 - -- [Godot Engine 4.5](https://godotengine.org/download) 或更高版本 +### 环境要求 +- [Godot Engine 4.5+](https://godotengine.org/download) +- Python 3.7+ (用于API测试) ### 运行项目 - -1. 克隆或下载此项目 -2. 使用 Godot 编辑器打开项目 -3. 在编辑器中点击"运行"按钮或按 F5 键启动游戏 - -### 开发指南 - -- **场景文件**: 所有场景文件存放在 `scenes/` 目录 -- **脚本文件**: 所有 GDScript 脚本存放在 `scripts/` 目录 -- **资源文件**: 图片、音频等资源存放在 `assets/` 目录 -- **游戏数据**: 配置文件、关卡数据等存放在 `data/` 目录 - -### 命名规范 - -本项目遵循统一的命名规范以保持代码一致性: - -**核心规则**: - -- **场景文件**:`下划线_scene.tscn` 或 `下划线_prefab.tscn` - - 示例:`main_scene.tscn`、`player_prefab.tscn` -- **脚本文件**:`PascalCase.gd`(大驼峰) - - 示例:`PlayerController.gd`、`UI_MainMenu.gd` -- **节点名称**:`camelCase`(小驼峰) - - 示例:`playerHpBar`、`startButton` -- **变量/函数**:`camelCase`(小驼峰) - - 示例:`var moveSpeed`、`func getPlayerPos()` -- **常量**:`UPPER_CASE`(全大写 + 下划线) - - 示例:`const MAX_HEALTH = 100` -- **资源文件**:`lower_case`(小写 + 下划线) - - 示例:`bg_main_menu.png`、`sound_jump.wav` - -📖 查看完整的 [命名规范文档](docs/naming_convention.md) - -### Git 提交规范 - -本项目遵循统一的 Git 提交信息格式:`<类型>:<描述>` - -**常用提交类型**: - -- `init`:项目初始化 -- `feat`:新增功能 -- `fix`:修复 Bug -- `docs`:文档更新 -- `scene`:场景文件相关 -- `asset`:资源文件相关 -- `ui`:UI 界面相关 -- `gameplay`:游戏玩法相关 -- `refactor`:代码重构 -- `perf`:性能优化 - -**提交示例**: - ```bash -git commit -m "init:项目初始化,搭建Godot文件结构" -git commit -m "feat:实现玩家角色的移动和跳跃" -git commit -m "fix:修复敌人穿墙的碰撞问题" -git commit -m "scene:创建战斗场景并配置相机" +# 1. 克隆项目 +git clone +cd whale-town + +# 2. 使用Godot编辑器打开项目 +# 3. 按F5运行或点击"运行"按钮 + +# 4. 测试API接口(可选) +python tests/api/simple_api_test.py ``` -📖 查看完整的 [Git 提交规范文档](docs/git_commit_guide.md) +## 🏗️ 项目架构 -## 贡献 +### 核心设计理念 +- **场景独立性** - 每个场景都是独立的功能模块 +- **高度解耦** - 通过事件系统和管理器通信 +- **组件复用** - 可复用组件统一管理 +- **标准化** - 统一的命名规范和目录结构 +- **测试驱动** - 完整的测试体系和文档 -欢迎提交 Issue 和 Pull Request! +### 目录结构 +``` +whaleTown/ +├── 🎬 scenes/ # 游戏场景 +│ ├── auth_scene.tscn # 用户认证场景 +│ ├── main_scene.tscn # 主游戏场景 +│ └── prefabs/ # 预制体组件 +├── 🔧 core/ # 核心系统(自动加载) +│ ├── managers/ # 全局管理器 +│ ├── systems/ # 系统组件 +│ └── utils/ # 工具类 +├── 📝 scripts/ # 业务逻辑脚本 +│ ├── scenes/ # 场景脚本 +│ ├── network/ # 网络相关 +│ └── ui/ # UI组件脚本 +├── 🧩 module/ # 可复用模块 +│ ├── UI/ # UI组件模块 +│ ├── Character/ # 角色模块 +│ ├── Combat/ # 战斗模块 +│ ├── Dialogue/ # 对话模块 +│ └── Inventory/ # 背包模块 +├── 🎨 assets/ # 游戏资源 +│ ├── sprites/ # 精灵图资源 +│ ├── audio/ # 音频文件 +│ ├── ui/ # UI界面资源 +│ ├── fonts/ # 字体资源 +│ ├── materials/ # 材质资源 +│ └── shaders/ # 着色器资源 +├── 📊 data/ # 配置数据 +│ ├── configs/ # 游戏配置 +│ ├── localization/ # 本地化文件 +│ ├── characters/ # 角色数据 +│ ├── items/ # 物品数据 +│ ├── levels/ # 关卡数据 +│ └── dialogues/ # 对话数据 +├── 🧪 tests/ # 测试文件 +│ ├── api/ # API接口测试 +│ ├── auth/ # 认证UI测试 +│ ├── unit/ # 单元测试 +│ ├── integration/ # 集成测试 +│ └── performance/ # 性能测试 +└── 📚 docs/ # 项目文档 + ├── auth/ # 认证相关文档 + ├── api-documentation.md # API接口文档 + ├── project_structure.md # 项目结构说明 + ├── naming_convention.md # 命名规范 + ├── code_comment_guide.md # 代码注释规范 + └── git_commit_guide.md # Git提交规范 +``` -## 许可证 +### 核心系统 +项目包含以下自动加载的核心系统: -[在此添加许可证信息] +- **GameManager** - 全局游戏状态管理 +- **SceneManager** - 场景切换管理 +- **EventSystem** - 事件通信系统 + +使用示例: +```gdscript +# 状态管理 +GameManager.change_state(GameManager.GameState.IN_GAME) + +# 场景切换 +SceneManager.change_scene("battle") + +# 事件通信 +EventSystem.emit_event("player_health_changed", 80) +EventSystem.connect_event("player_died", _on_player_died) +``` + +## ✨ 主要功能 + +### 🔐 用户认证系统 +- **用户注册** - 支持邮箱验证码验证 +- **用户登录** - 多种登录方式(用户名/邮箱/手机号) +- **密码管理** - 密码重置和修改功能 +- **GitHub OAuth** - 第三方登录集成 +- **错误处理** - 完整的错误提示和频率限制 + +### 🎮 游戏功能 +- **主场景** - 游戏主界面和菜单系统 +- **认证场景** - 完整的登录注册界面 +- **状态管理** - 用户状态和游戏状态管理 +- **网络通信** - RESTful API集成 + +### 🧪 测试体系 +- **API测试** - 完整的接口测试脚本 +- **UI测试** - 认证界面的交互测试 +- **错误场景** - 边界条件和异常处理测试 + +## 🔧 开发规范 + +### 命名规范 +- **场景文件**: `snake_case_scene.tscn` (如: `auth_scene.tscn`) +- **脚本文件**: `PascalCase.gd` (如: `AuthScene.gd`) +- **节点名称**: `camelCase` (如: `loginButton`) +- **变量/函数**: `camelCase` (如: `playerHealth`) +- **常量**: `UPPER_CASE` (如: `MAX_HEALTH`) +- **资源文件**: `snake_case` (如: `bg_auth_scene.png`) + +### 代码注释规范 +- **文件头注释**: 说明文件用途、主要功能和依赖关系 +- **函数注释**: 包含参数说明、返回值和使用示例 +- **复杂逻辑**: 添加行内注释解释业务逻辑和设计决策 +- **特殊标记**: 使用 TODO、FIXME、NOTE 等标准标记 +- **AI辅助**: 支持AI补充注释,提供详细的上下文信息 + +### Git 提交规范 +使用格式:`<类型>:<描述>` + +常用类型:`feat` `fix` `docs` `refactor` `scene` `asset` `ui` `test` + +```bash +git commit -m "feat:实现用户登录功能" +git commit -m "fix:修复429错误处理" +git commit -m "test:添加API接口测试" +git commit -m "docs:更新项目文档" +``` + +## 📚 项目文档 + +### 核心文档 +- 📋 [项目结构详解](docs/project_structure.md) - 完整的架构说明 +- 📝 [命名规范](docs/naming_convention.md) - 详细的命名规则 +- 💬 [代码注释规范](docs/code_comment_guide.md) - 注释标准和AI辅助指南 +- 🔀 [Git提交规范](docs/git_commit_guide.md) - 提交信息标准 + +### API和测试文档 +- 🔌 [API接口文档](docs/api-documentation.md) - 完整的API说明和测试指南 +- 🔐 [认证系统文档](docs/auth/) - 用户认证相关文档 +- 🧪 [API测试指南](tests/api/README.md) - API测试使用方法 +- 🎮 [认证UI测试](tests/auth/README.md) - UI测试场景说明 + +## 🛠️ 开发指南 + +### 添加新场景 +1. 在 `scenes/` 创建场景文件 +2. 在 `scripts/scenes/` 创建对应脚本 +3. 在 `SceneManager` 中注册场景路径 +4. 使用 `SceneManager.change_scene()` 切换 + +### 创建可复用组件 +1. 在 `module/` 对应分类下创建组件 +2. 实现标准接口 +3. 通过 `EventSystem` 与其他模块通信 +4. 在 `scenes/prefabs/` 创建预制体 + +### 资源管理 +- 图片资源放入 `assets/sprites/` 对应分类 +- 音频文件放入 `assets/audio/` 对应分类 +- UI资源放入 `assets/ui/` 对应分类 +- 配置文件放入 `data/configs/` +- 遵循命名规范,使用英文小写+下划线 + +### API接口测试 +项目提供了完整的Python测试脚本来验证API接口: + +```bash +# 快速测试API连通性 +python tests/api/simple_api_test.py + +# 完整的API功能测试 +python tests/api/api_test.py --verbose + +# 自定义服务器地址测试 +python tests/api/simple_api_test.py https://your-api-server.com +``` + +测试脚本会验证: +- ✅ 应用状态检查 +- ✅ 用户注册和登录功能 +- ✅ 邮箱验证码发送和验证 +- ✅ 错误处理和频率限制(429错误) +- ✅ 管理员功能和权限控制 +- ✅ 用户状态管理 +- ✅ 安全功能测试 + +📖 查看 [API测试文档](tests/api/README.md) 了解详细使用方法 + +### 认证UI测试 +项目还提供了Godot内置的UI测试场景: + +1. 在Godot编辑器中打开 `tests/auth/auth_ui_test.tscn` +2. 运行场景进行交互式测试 +3. 测试各种错误场景和边界条件 + +📖 查看 [认证UI测试文档](tests/auth/README.md) 了解详细使用方法 + +## 🔧 技术栈 + +- **游戏引擎**: Godot 4.5.1 +- **脚本语言**: GDScript +- **架构模式**: 模块化 + 事件驱动 +- **状态管理**: 单例管理器模式 +- **通信机制**: 全局事件系统 +- **API集成**: RESTful API + JSON +- **测试框架**: Python + Godot内置测试 + +## 🤝 贡献指南 + +1. Fork 项目 +2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) +3. 遵循项目的命名规范和架构设计 +4. 添加相应的测试用例 +5. 更新相关文档 +6. 提交更改 (`git commit -m 'feat:添加某个功能'`) +7. 推送到分支 (`git push origin feature/AmazingFeature`) +8. 开启 Pull Request + +### 贡献类型 +- 🐛 Bug修复 +- ✨ 新功能开发 +- 📚 文档改进 +- 🧪 测试用例 +- 🎨 UI/UX改进 +- ⚡ 性能优化 + +## 📄 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 + +--- + +## 🎯 项目状态 + +- ✅ 基础架构搭建完成 +- ✅ 用户认证系统完成 +- ✅ API接口集成完成 +- ✅ 测试体系建立完成 +- ✅ 文档体系完善 +- 🚧 游戏核心玩法开发中 +- 🚧 更多功能模块开发中 + +**最后更新**: 2025-12-24 diff --git a/addons/.gitkeep b/addons/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/assets/.gitkeep b/assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/assets/audio/music/.gitkeep b/assets/audio/music/.gitkeep new file mode 100644 index 0000000..9aa01b8 --- /dev/null +++ b/assets/audio/music/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 音乐资源目录 \ No newline at end of file diff --git a/assets/audio/sounds/.gitkeep b/assets/audio/sounds/.gitkeep new file mode 100644 index 0000000..52a719a --- /dev/null +++ b/assets/audio/sounds/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 音效资源目录 \ No newline at end of file diff --git a/assets/audio/voice/.gitkeep b/assets/audio/voice/.gitkeep new file mode 100644 index 0000000..c7ee643 --- /dev/null +++ b/assets/audio/voice/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 语音资源目录 \ No newline at end of file diff --git a/assets/fonts/.gitkeep b/assets/fonts/.gitkeep new file mode 100644 index 0000000..800c4ba --- /dev/null +++ b/assets/fonts/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 字体资源目录 \ No newline at end of file diff --git a/assets/materials/.gitkeep b/assets/materials/.gitkeep new file mode 100644 index 0000000..4673e1d --- /dev/null +++ b/assets/materials/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 材质资源目录 \ No newline at end of file diff --git a/assets/shaders/.gitkeep b/assets/shaders/.gitkeep new file mode 100644 index 0000000..b5f3ee3 --- /dev/null +++ b/assets/shaders/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 着色器资源目录 \ No newline at end of file diff --git a/assets/sprites/characters/.gitkeep b/assets/sprites/characters/.gitkeep new file mode 100644 index 0000000..ec3e024 --- /dev/null +++ b/assets/sprites/characters/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 角色精灵资源目录 \ No newline at end of file diff --git a/assets/sprites/effects/.gitkeep b/assets/sprites/effects/.gitkeep new file mode 100644 index 0000000..ae28815 --- /dev/null +++ b/assets/sprites/effects/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 特效精灵资源目录 \ No newline at end of file diff --git a/assets/sprites/environment/.gitkeep b/assets/sprites/environment/.gitkeep new file mode 100644 index 0000000..71129f4 --- /dev/null +++ b/assets/sprites/environment/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 环境精灵资源目录 \ No newline at end of file diff --git a/assets/ui/auth/bg_auth_scene.png b/assets/ui/auth/bg_auth_scene.png new file mode 100644 index 0000000..de86a4f Binary files /dev/null and b/assets/ui/auth/bg_auth_scene.png differ diff --git a/assets/ui/auth/bg_auth_scene.png.import b/assets/ui/auth/bg_auth_scene.png.import new file mode 100644 index 0000000..53e377b --- /dev/null +++ b/assets/ui/auth/bg_auth_scene.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bx17oy8lvaca4" +path="res://.godot/imported/bg_auth_scene.png-818065fcc20397e855c75507c1313623.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/ui/auth/bg_auth_scene.png" +dest_files=["res://.godot/imported/bg_auth_scene.png-818065fcc20397e855c75507c1313623.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/ui/auth/login_frame_smart_transparent.png b/assets/ui/auth/login_frame_smart_transparent.png new file mode 100644 index 0000000..5e39df6 Binary files /dev/null and b/assets/ui/auth/login_frame_smart_transparent.png differ diff --git a/assets/ui/auth/login_frame_smart_transparent.png.import b/assets/ui/auth/login_frame_smart_transparent.png.import new file mode 100644 index 0000000..745d370 --- /dev/null +++ b/assets/ui/auth/login_frame_smart_transparent.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://de4q4s1gxivtf" +path="res://.godot/imported/login_frame_smart_transparent.png-e5d0fd05b4713ddd3beae8223f2abb80.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/ui/auth/login_frame_smart_transparent.png" +dest_files=["res://.godot/imported/login_frame_smart_transparent.png-e5d0fd05b4713ddd3beae8223f2abb80.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/core/components/.gitkeep b/core/components/.gitkeep new file mode 100644 index 0000000..a6022c8 --- /dev/null +++ b/core/components/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 组件脚本目录 \ No newline at end of file diff --git a/core/input_box/.gitkeep b/core/input_box/.gitkeep new file mode 100644 index 0000000..00e5009 --- /dev/null +++ b/core/input_box/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 输入框组件目录 \ No newline at end of file diff --git a/core/interfaces/.gitkeep b/core/interfaces/.gitkeep new file mode 100644 index 0000000..44d4798 --- /dev/null +++ b/core/interfaces/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 接口定义目录 \ No newline at end of file diff --git a/core/managers/GameManager.gd b/core/managers/GameManager.gd new file mode 100644 index 0000000..e636a5b --- /dev/null +++ b/core/managers/GameManager.gd @@ -0,0 +1,50 @@ +extends Node + +# 游戏管理器 - 全局游戏状态管理 +# 单例模式,管理游戏的整体状态和生命周期 + +signal game_state_changed(new_state: GameState) + +enum GameState { + LOADING, # 加载中 + AUTH, # 认证状态 + MAIN_MENU, # 主菜单 + IN_GAME, # 游戏中 + PAUSED, # 暂停 + SETTINGS # 设置 +} + +var current_state: GameState = GameState.LOADING +var previous_state: GameState = GameState.LOADING +var current_user: String = "" +var game_version: String = "1.0.0" + +func _ready(): + print("GameManager 初始化完成") + change_state(GameState.AUTH) + +func change_state(new_state: GameState): + if current_state == new_state: + return + + previous_state = current_state + current_state = new_state + + print("游戏状态变更: ", GameState.keys()[previous_state], " -> ", GameState.keys()[current_state]) + game_state_changed.emit(new_state) + +func get_current_state() -> GameState: + return current_state + +func get_previous_state() -> GameState: + return previous_state + +func set_current_user(username: String): + current_user = username + print("当前用户设置为: ", username) + +func get_current_user() -> String: + return current_user + +func is_user_logged_in() -> bool: + return not current_user.is_empty() diff --git a/core/managers/GameManager.gd.uid b/core/managers/GameManager.gd.uid new file mode 100644 index 0000000..a637acf --- /dev/null +++ b/core/managers/GameManager.gd.uid @@ -0,0 +1 @@ +uid://c6bl6k5kkfah6 diff --git a/core/managers/SceneManager.gd b/core/managers/SceneManager.gd new file mode 100644 index 0000000..f68d05c --- /dev/null +++ b/core/managers/SceneManager.gd @@ -0,0 +1,75 @@ +extends Node + +# 场景管理器 - 负责场景切换和管理 +# 提供场景切换的统一接口 + +signal scene_changed(scene_name: String) +signal scene_change_started(scene_name: String) + +var current_scene_name: String = "" +var is_changing_scene: bool = false + +# 场景路径映射 +var scene_paths: Dictionary = { + "main": "res://scenes/main_scene.tscn", + "auth": "res://scenes/auth_scene.tscn", + "game": "res://scenes/game_scene.tscn", + "battle": "res://scenes/battle_scene.tscn", + "inventory": "res://scenes/inventory_scene.tscn", + "shop": "res://scenes/shop_scene.tscn", + "settings": "res://scenes/settings_scene.tscn" +} + +func _ready(): + print("SceneManager 初始化完成") + +func change_scene(scene_name: String, use_transition: bool = true): + if is_changing_scene: + print("场景切换中,忽略新的切换请求") + return false + + if not scene_paths.has(scene_name): + print("错误: 未找到场景 ", scene_name) + return false + + var scene_path = scene_paths[scene_name] + print("开始切换场景: ", current_scene_name, " -> ", scene_name) + + is_changing_scene = true + scene_change_started.emit(scene_name) + + if use_transition: + await show_transition() + + var error = get_tree().change_scene_to_file(scene_path) + if error != OK: + print("场景切换失败: ", error) + is_changing_scene = false + return false + + current_scene_name = scene_name + is_changing_scene = false + scene_changed.emit(scene_name) + + if use_transition: + await hide_transition() + + print("场景切换完成: ", scene_name) + return true + +func get_current_scene_name() -> String: + return current_scene_name + +func register_scene(scene_name: String, scene_path: String): + scene_paths[scene_name] = scene_path + print("注册场景: ", scene_name, " -> ", scene_path) + +func show_transition(): + # TODO: 实现场景切换过渡效果 + print("显示场景切换过渡效果") + await get_tree().create_timer(0.2).timeout + +func hide_transition(): + # TODO: 隐藏场景切换过渡效果 + print("隐藏场景切换过渡效果") + await get_tree().create_timer(0.2).timeout diff --git a/core/managers/SceneManager.gd.uid b/core/managers/SceneManager.gd.uid new file mode 100644 index 0000000..7e8c271 --- /dev/null +++ b/core/managers/SceneManager.gd.uid @@ -0,0 +1 @@ +uid://bf5bmaqwstpuq diff --git a/core/systems/EventSystem.gd b/core/systems/EventSystem.gd new file mode 100644 index 0000000..76f091b --- /dev/null +++ b/core/systems/EventSystem.gd @@ -0,0 +1,80 @@ +extends Node + +# 全局事件系统 - 提供解耦的事件通信机制 +# 允许不同模块之间通过事件进行通信,避免直接依赖 + +# 事件监听器存储 +var event_listeners: Dictionary = {} + +func _ready(): + print("EventSystem 初始化完成") + +# 注册事件监听器 +func connect_event(event_name: String, callback: Callable, target: Node = null): + if not event_listeners.has(event_name): + event_listeners[event_name] = [] + + var listener_info = { + "callback": callback, + "target": target + } + + event_listeners[event_name].append(listener_info) + print("注册事件监听器: ", event_name, " -> ", callback) + +# 移除事件监听器 +func disconnect_event(event_name: String, callback: Callable, target: Node = null): + if not event_listeners.has(event_name): + return + + var listeners = event_listeners[event_name] + for i in range(listeners.size() - 1, -1, -1): + var listener = listeners[i] + if listener.callback == callback and listener.target == target: + listeners.remove_at(i) + print("移除事件监听器: ", event_name, " -> ", callback) + break + +# 发送事件 +func emit_event(event_name: String, data: Variant = null): + print("发送事件: ", event_name, " 数据: ", data) + + if not event_listeners.has(event_name): + return + + var listeners = event_listeners[event_name] + for listener_info in listeners: + var target = listener_info.target + var callback = listener_info.callback + + # 检查目标节点是否仍然有效 + if target != null and not is_instance_valid(target): + continue + + # 调用回调函数 + if data != null: + callback.call(data) + else: + callback.call() + +# 清理无效的监听器 +func cleanup_invalid_listeners(): + for event_name in event_listeners.keys(): + var listeners = event_listeners[event_name] + for i in range(listeners.size() - 1, -1, -1): + var listener = listeners[i] + var target = listener.target + if target != null and not is_instance_valid(target): + listeners.remove_at(i) + print("清理无效监听器: ", event_name) + +# 获取事件监听器数量 +func get_listener_count(event_name: String) -> int: + if not event_listeners.has(event_name): + return 0 + return event_listeners[event_name].size() + +# 清空所有事件监听器 +func clear_all_listeners(): + event_listeners.clear() + print("清空所有事件监听器") diff --git a/core/systems/EventSystem.gd.uid b/core/systems/EventSystem.gd.uid new file mode 100644 index 0000000..d2494dd --- /dev/null +++ b/core/systems/EventSystem.gd.uid @@ -0,0 +1 @@ +uid://csuxtwgni1dmf diff --git a/core/utils/StringUtils.gd b/core/utils/StringUtils.gd new file mode 100644 index 0000000..bee8874 --- /dev/null +++ b/core/utils/StringUtils.gd @@ -0,0 +1,105 @@ +class_name StringUtils + +# 字符串工具类 - 提供常用的字符串处理功能 + +# 验证邮箱格式 +static func is_valid_email(email: String) -> bool: + var regex = RegEx.new() + regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") + return regex.search(email) != null + +# 验证用户名格式(字母、数字、下划线) +static func is_valid_username(username: String) -> bool: + if username.is_empty() or username.length() > 50: + return false + + var regex = RegEx.new() + regex.compile("^[a-zA-Z0-9_]+$") + return regex.search(username) != null + +# 验证密码强度 +static func validate_password_strength(password: String) -> Dictionary: + var result = {"valid": false, "message": "", "strength": 0} + + if password.length() < 8: + result.message = "密码长度至少8位" + return result + + if password.length() > 128: + result.message = "密码长度不能超过128位" + return result + + var has_letter = false + var has_digit = false + var has_special = false + + for i in range(password.length()): + var char = password[i] + if char >= 'a' and char <= 'z' or char >= 'A' and char <= 'Z': + has_letter = true + elif char >= '0' and char <= '9': + has_digit = true + elif char in "!@#$%^&*()_+-=[]{}|;:,.<>?": + has_special = true + + var strength = 0 + if has_letter: + strength += 1 + if has_digit: + strength += 1 + if has_special: + strength += 1 + if password.length() >= 12: + strength += 1 + + result.strength = strength + + if not (has_letter and has_digit): + result.message = "密码必须包含字母和数字" + return result + + result.valid = true + result.message = "密码强度: " + ["弱", "中", "强", "很强"][min(strength - 1, 3)] + return result + +# 截断字符串 +static func truncate(text: String, max_length: int, suffix: String = "...") -> String: + if text.length() <= max_length: + return text + return text.substr(0, max_length - suffix.length()) + suffix + +# 首字母大写 +static func capitalize_first(text: String) -> String: + if text.is_empty(): + return text + return text[0].to_upper() + text.substr(1) + +# 转换为标题格式(每个单词首字母大写) +static func to_title_case(text: String) -> String: + var words = text.split(" ") + var result = [] + for word in words: + if not word.is_empty(): + result.append(capitalize_first(word.to_lower())) + return " ".join(result) + +# 移除HTML标签 +static func strip_html_tags(html: String) -> String: + var regex = RegEx.new() + regex.compile("<[^>]*>") + return regex.sub(html, "", true) + +# 格式化文件大小 +static func format_file_size(bytes: int) -> String: + var units = ["B", "KB", "MB", "GB", "TB"] + var size = float(bytes) + var unit_index = 0 + + while size >= 1024.0 and unit_index < units.size() - 1: + size /= 1024.0 + unit_index += 1 + + if unit_index == 0: + return str(int(size)) + " " + units[unit_index] + else: + return "%.1f %s" % [size, units[unit_index]] diff --git a/core/utils/StringUtils.gd.uid b/core/utils/StringUtils.gd.uid new file mode 100644 index 0000000..d266e49 --- /dev/null +++ b/core/utils/StringUtils.gd.uid @@ -0,0 +1 @@ +uid://bturviv4bm4yk diff --git a/data/.gitkeep b/data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/data/characters/.gitkeep b/data/characters/.gitkeep new file mode 100644 index 0000000..2945a8f --- /dev/null +++ b/data/characters/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 角色数据目录 \ No newline at end of file diff --git a/data/configs/game_config.json b/data/configs/game_config.json new file mode 100644 index 0000000..a0a2485 --- /dev/null +++ b/data/configs/game_config.json @@ -0,0 +1,29 @@ +{ + "game": { + "name": "whaleTown", + "version": "1.0.0", + "debug_mode": true + }, + "network": { + "api_base_url": "https://whaletownend.xinghangee.icu", + "timeout": 30, + "retry_count": 3 + }, + "ui": { + "default_font_size": 14, + "toast_duration": 2.0, + "transition_duration": 0.3 + }, + "gameplay": { + "auto_save_interval": 300, + "max_inventory_slots": 50, + "default_player_stats": { + "level": 1, + "coins": 100, + "exp": 0, + "max_exp": 100, + "energy": 100, + "max_energy": 100 + } + } +} diff --git a/data/dialogues/.gitkeep b/data/dialogues/.gitkeep new file mode 100644 index 0000000..7b26e82 --- /dev/null +++ b/data/dialogues/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 对话数据目录 \ No newline at end of file diff --git a/data/items/.gitkeep b/data/items/.gitkeep new file mode 100644 index 0000000..83475d5 --- /dev/null +++ b/data/items/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 物品数据目录 \ No newline at end of file diff --git a/data/levels/.gitkeep b/data/levels/.gitkeep new file mode 100644 index 0000000..ae36434 --- /dev/null +++ b/data/levels/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 关卡数据目录 \ No newline at end of file diff --git a/data/localization/zh_CN.json b/data/localization/zh_CN.json new file mode 100644 index 0000000..82d2280 --- /dev/null +++ b/data/localization/zh_CN.json @@ -0,0 +1,36 @@ +{ + "ui": { + "login": "登录", + "register": "注册", + "username": "用户名", + "password": "密码", + "email": "邮箱", + "confirm_password": "确认密码", + "verification_code": "验证码", + "send_code": "发送验证码", + "forgot_password": "忘记密码", + "enter_town": "进入小镇", + "logout": "退出登录" + }, + "messages": { + "login_success": "登录成功!正在进入鲸鱼镇...", + "register_success": "注册成功!欢迎加入鲸鱼镇", + "network_error": "网络连接失败,请检查网络连接", + "invalid_username": "用户名只能包含字母、数字和下划线", + "invalid_email": "请输入有效的邮箱地址", + "password_too_short": "密码长度至少8位", + "password_mismatch": "两次输入的密码不一致", + "verification_code_sent": "验证码已发送到您的邮箱,请查收" + }, + "game": { + "level": "等级", + "coins": "金币", + "experience": "经验", + "energy": "体力", + "explore": "探索小镇", + "inventory": "背包", + "shop": "商店", + "friends": "好友", + "settings": "设置" + } +} \ No newline at end of file diff --git a/docs/.gitkeep b/docs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/api-documentation.md b/docs/api-documentation.md new file mode 100644 index 0000000..3d9b792 --- /dev/null +++ b/docs/api-documentation.md @@ -0,0 +1,2116 @@ +# Pixel Game Server API接口文档 + +## 概述 + +本文档描述了像素游戏服务器的完整API接口,包括用户认证、管理员后台、应用状态等功能。 + +**基础URL**: `http://localhost:3000` +**API文档地址**: `http://localhost:3000/api-docs` +**项目名称**: Pixel Game Server +**版本**: 1.0.0 + +## 通用响应格式 + +所有API接口都遵循统一的响应格式: + +```json +{ + "success": boolean, + "data": object | null, + "message": string, + "error_code": string | null +} +``` + +### 字段说明 + +- `success`: 请求是否成功 +- `data`: 响应数据(成功时返回) +- `message`: 响应消息 +- `error_code`: 错误代码(失败时返回) + +## 接口分类 + +### 1. 应用状态接口 (App) +- `GET /` - 获取应用状态 + +### 2. 用户认证接口 (Auth) +- `POST /auth/login` - 用户登录 +- `POST /auth/register` - 用户注册 +- `POST /auth/github` - GitHub OAuth登录 +- `POST /auth/forgot-password` - 发送密码重置验证码 +- `POST /auth/reset-password` - 重置密码 +- `PUT /auth/change-password` - 修改密码 +- `POST /auth/send-email-verification` - 发送邮箱验证码 +- `POST /auth/verify-email` - 验证邮箱验证码 +- `POST /auth/resend-email-verification` - 重新发送邮箱验证码 +- `POST /auth/debug-verification-code` - 调试验证码信息(开发环境) +- `POST /auth/debug-clear-throttle` - 清除限流记录(开发环境) + +### 3. 管理员接口 (Admin) +- `POST /admin/auth/login` - 管理员登录 +- `GET /admin/users` - 获取用户列表 +- `GET /admin/users/:id` - 获取用户详情 +- `POST /admin/users/:id/reset-password` - 重置用户密码 +- `GET /admin/logs/runtime` - 获取运行日志 +- `GET /admin/logs/archive` - 下载日志压缩包 + +### 4. 用户管理接口 (User Management) +- `PUT /admin/users/:id/status` - 修改用户状态 +- `POST /admin/users/batch-status` - 批量修改用户状态 +- `GET /admin/users/status-stats` - 获取用户状态统计 + +## 接口列表 + +### 应用状态接口 + +#### 1. 获取应用状态 + +**接口地址**: `GET /` + +**功能描述**: 返回应用的基本运行状态信息,用于健康检查和监控 + +#### 请求参数 + +无 + +#### 响应示例 + +**成功响应** (200): +```json +{ + "service": "Pixel Game Server", + "version": "1.0.0", + "status": "running", + "timestamp": "2025-12-23T10:00:00.000Z", + "uptime": 3600, + "environment": "development", + "storage_mode": "memory" +} +``` + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| service | string | 服务名称 | +| version | string | 服务版本 | +| status | string | 运行状态 (running/starting/stopping/error) | +| timestamp | string | 当前时间戳 | +| uptime | number | 运行时间(秒) | +| environment | string | 运行环境 (development/production/test) | +| storage_mode | string | 存储模式 (database/memory) | + +### 用户认证接口 + +#### 1. 用户登录 + +**接口地址**: `POST /auth/login` + +**功能描述**: 用户登录,支持用户名、邮箱或手机号登录 + +#### 请求参数 + +```json +{ + "identifier": "testuser", + "password": "password123" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| identifier | string | 是 | 登录标识符,支持用户名、邮箱或手机号 | +| password | string | 是 | 用户密码 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "user": { + "id": "1", + "username": "testuser", + "nickname": "测试用户", + "email": "test@example.com", + "phone": "+8613800138000", + "avatar_url": "https://example.com/avatar.jpg", + "role": 1, + "created_at": "2025-12-17T10:00:00.000Z" + }, + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "is_new_user": false, + "message": "登录成功" + }, + "message": "登录成功" +} +``` + +**失败响应** (401): +```json +{ + "success": false, + "message": "用户名或密码错误", + "error_code": "LOGIN_FAILED" +} +``` + +#### 2. 用户注册 + +**接口地址**: `POST /auth/register` + +**功能描述**: 创建新用户账户 + +**重要说明**: +- 如果提供邮箱,必须先调用发送验证码接口获取验证码 +- 验证码验证失败会返回400状态码,而不是201 +- 注册成功返回201,失败返回400 + +#### 请求参数 + +```json +{ + "username": "testuser", + "password": "password123", + "nickname": "测试用户", + "email": "test@example.com", + "email_verification_code": "123456" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| username | string | 是 | 用户名,只能包含字母、数字和下划线,长度1-50字符 | +| password | string | 是 | 密码,必须包含字母和数字,长度8-128字符 | +| nickname | string | 是 | 用户昵称,长度1-50字符 | +| email | string | 否 | 邮箱地址(如果提供,必须先获取验证码) | +| phone | string | 否 | 手机号码 | +| email_verification_code | string | 条件必填 | 邮箱验证码,提供邮箱时必填 | + +#### 响应示例 + +**成功响应** (201): +```json +{ + "success": true, + "data": { + "user": { + "id": "2", + "username": "testuser", + "nickname": "测试用户", + "email": "test@example.com", + "phone": "+8613800138000", + "avatar_url": null, + "role": 1, + "created_at": "2025-12-17T10:00:00.000Z" + }, + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "is_new_user": true, + "message": "注册成功" + }, + "message": "注册成功" +} +``` + +**失败响应** (400): +```json +{ + "success": false, + "message": "提供邮箱时必须提供邮箱验证码", + "error_code": "REGISTER_FAILED" +} +``` + +**频率限制响应** (429): +```json +{ + "success": false, + "message": "注册请求过于频繁,请5分钟后再试", + "error_code": "TOO_MANY_REQUESTS", + "throttle_info": { + "limit": 10, + "window_seconds": 300, + "current_requests": 10, + "reset_time": "2025-12-24T11:26:41.136Z" + } +} +``` + +#### 3. GitHub OAuth登录 + +**接口地址**: `POST /auth/github` + +**功能描述**: 使用GitHub账户登录或注册 + +#### 请求参数 + +```json +{ + "github_id": "12345678", + "username": "octocat", + "nickname": "The Octocat", + "email": "octocat@github.com", + "avatar_url": "https://github.com/images/error/octocat_happy.gif" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| github_id | string | 是 | GitHub用户ID | +| username | string | 是 | GitHub用户名 | +| nickname | string | 是 | GitHub显示名称 | +| email | string | 否 | GitHub邮箱地址 | +| avatar_url | string | 否 | GitHub头像URL | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "user": { + "id": "3", + "username": "octocat", + "nickname": "The Octocat", + "email": "octocat@github.com", + "phone": null, + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "role": 1, + "created_at": "2025-12-17T10:00:00.000Z" + }, + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "is_new_user": true, + "message": "GitHub账户绑定成功" + }, + "message": "GitHub账户绑定成功" +} +``` + +#### 4. 发送密码重置验证码 + +**接口地址**: `POST /auth/forgot-password` + +**功能描述**: 向用户邮箱或手机发送密码重置验证码 + +#### 请求参数 + +```json +{ + "identifier": "test@example.com" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| identifier | string | 是 | 邮箱或手机号 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "verification_code": "123456" + }, + "message": "验证码已发送,请查收" +} +``` + +**注意**: 实际应用中不应返回验证码,这里仅用于演示。 + +#### 5. 重置密码 + +**接口地址**: `POST /auth/reset-password` + +**功能描述**: 使用验证码重置用户密码 + +#### 请求参数 + +```json +{ + "identifier": "test@example.com", + "verification_code": "123456", + "new_password": "newpassword123" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| identifier | string | 是 | 邮箱或手机号 | +| verification_code | string | 是 | 6位数字验证码 | +| new_password | string | 是 | 新密码,必须包含字母和数字,长度8-128字符 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "message": "密码重置成功" +} +``` + +#### 6. 修改密码 + +**接口地址**: `PUT /auth/change-password` + +**功能描述**: 用户修改自己的密码(需要提供旧密码) + +#### 请求参数 + +```json +{ + "user_id": "1", + "old_password": "oldpassword123", + "new_password": "newpassword123" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| user_id | string | 是 | 用户ID(实际应用中应从JWT令牌中获取) | +| old_password | string | 是 | 当前密码 | +| new_password | string | 是 | 新密码,必须包含字母和数字,长度8-128字符 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "message": "密码修改成功" +} +``` + +#### 7. 发送邮箱验证码 + +**接口地址**: `POST /auth/send-email-verification` + +**功能描述**: 向指定邮箱发送验证码,用于注册时的邮箱验证 + +#### 请求参数 + +```json +{ + "email": "test@example.com" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| email | string | 是 | 邮箱地址 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "verification_code": "123456", + "is_test_mode": false + }, + "message": "验证码已发送,请查收" +} +``` + +**测试模式响应** (206): +```json +{ + "success": false, + "data": { + "verification_code": "059174", + "is_test_mode": true + }, + "message": "⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。", + "error_code": "TEST_MODE_ONLY" +} +``` + +#### 8. 验证邮箱验证码 + +**接口地址**: `POST /auth/verify-email` + +**功能描述**: 使用验证码验证邮箱 + +#### 请求参数 + +```json +{ + "email": "test@example.com", + "verification_code": "123456" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| email | string | 是 | 邮箱地址 | +| verification_code | string | 是 | 6位数字验证码 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "message": "邮箱验证成功" +} +``` + +#### 9. 重新发送邮箱验证码 + +**接口地址**: `POST /auth/resend-email-verification` + +**功能描述**: 重新向指定邮箱发送验证码 + +#### 请求参数 + +```json +{ + "email": "test@example.com" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| email | string | 是 | 邮箱地址 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "verification_code": "123456", + "is_test_mode": false + }, + "message": "验证码已重新发送,请查收" +} +``` + +#### 10. 调试验证码信息 + +**接口地址**: `POST /auth/debug-verification-code` + +**功能描述**: 获取验证码的详细调试信息(仅开发环境使用) + +#### 请求参数 + +```json +{ + "email": "test@example.com" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| email | string | 是 | 邮箱地址 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "email": "test@example.com", + "verification_code": "123456", + "expires_at": "2025-12-23T10:15:00.000Z", + "created_at": "2025-12-23T10:00:00.000Z" + }, + "message": "调试信息获取成功" +} +``` + +#### 11. 清除限流记录 + +**接口地址**: `POST /auth/debug-clear-throttle` + +**功能描述**: 清除所有限流记录(仅开发环境使用) + +**注意**: 此接口用于开发测试,清除所有IP的频率限制记录 + +#### 请求参数 + +无 + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "message": "限流记录已清除" +} +``` + +### 管理员接口 + +**注意**:所有管理员接口都需要在 Header 中携带 `Authorization: Bearer `,且用户角色必须为管理员 (role=9)。 + +#### 1. 管理员登录 + +**接口地址**: `POST /admin/auth/login` + +**功能描述**: 管理员登录,仅允许 role=9 的账户登录后台 + +#### 请求参数 + +```json +{ + "identifier": "admin", + "password": "Admin123456" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| identifier | string | 是 | 登录标识符(用户名/邮箱/手机号) | +| password | string | 是 | 密码 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "admin": { + "id": "1", + "username": "admin", + "nickname": "管理员", + "role": 9 + }, + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_at": 1766102400000 + }, + "message": "管理员登录成功" +} +``` + +#### 2. 获取用户列表 + +**接口地址**: `GET /admin/users` + +**功能描述**: 分页获取所有注册用户列表 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| limit | number | 否 | 每页数量,默认100 | +| offset | number | 否 | 偏移量,默认0 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "users": [ + { + "id": "1", + "username": "user1", + "nickname": "小明", + "email": "user1@example.com", + "email_verified": false, + "phone": "+8613800138000", + "avatar_url": "https://example.com/avatar.png", + "role": 1, + "created_at": "2025-12-19T00:00:00.000Z", + "updated_at": "2025-12-19T00:00:00.000Z" + } + ], + "limit": 100, + "offset": 0 + }, + "message": "用户列表获取成功" +} +``` + +#### 3. 获取用户详情 + +**接口地址**: `GET /admin/users/:id` + +**功能描述**: 获取指定用户的详细信息 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | string | 是 | 用户ID(路径参数) | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "user": { + "id": "1", + "username": "user1", + "nickname": "小明", + "email": "user1@example.com", + "email_verified": false, + "phone": "+8613800138000", + "avatar_url": "https://example.com/avatar.png", + "role": 1, + "created_at": "2025-12-19T00:00:00.000Z", + "updated_at": "2025-12-19T00:00:00.000Z" + } + }, + "message": "用户信息获取成功" +} +``` + +#### 4. 重置用户密码 + +**接口地址**: `POST /admin/users/:id/reset-password` + +**功能描述**: 管理员强制重置指定用户的密码 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | string | 是 | 用户ID(路径参数) | +| new_password | string | 是 | 新密码(至少8位,包含字母和数字) | + +```json +{ + "new_password": "NewPass1234" +} +``` + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "message": "密码重置成功" +} +``` + +#### 5. 获取运行日志 + +**接口地址**: `GET /admin/logs/runtime` + +**功能描述**: 从 logs/ 目录读取最近的日志行 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| lines | number | 否 | 返回行数,默认200,最大2000 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "file": "dev.log", + "updated_at": "2025-12-19T19:10:15.000Z", + "lines": [ + "[2025-12-19 19:10:15] INFO: Server started", + "[2025-12-19 19:10:16] INFO: Database connected" + ] + }, + "message": "运行日志获取成功" +} +``` + +#### 6. 下载日志压缩包 + +**接口地址**: `GET /admin/logs/archive` + +**功能描述**: 将 logs/ 目录打包为 tar.gz 并下载 + +#### 请求参数 + +无 + +#### 响应示例 + +**成功响应** (200): +- Content-Type: `application/gzip` +- Content-Disposition: `attachment; filename="logs-2025-12-23T10-00-00-000Z.tar.gz"` +- 返回二进制流(tar.gz 文件) + +### 用户管理接口 + +**注意**:所有用户管理接口都需要管理员权限,需要在 Header 中携带 `Authorization: Bearer `。 + +#### 1. 修改用户状态 + +**接口地址**: `PUT /admin/users/:id/status` + +**功能描述**: 管理员修改指定用户的账户状态,支持激活、锁定、禁用等操作 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | string | 是 | 用户ID(路径参数) | +| status | string | 是 | 用户状态枚举值 | +| reason | string | 否 | 修改原因 | + +```json +{ + "status": "locked", + "reason": "用户违反社区规定" +} +``` + +**用户状态枚举值:** +- `active` - 正常状态,可以正常使用 +- `inactive` - 未激活,需要邮箱验证 +- `locked` - 已锁定,临时禁用 +- `banned` - 已禁用,管理员操作 +- `deleted` - 已删除,软删除状态 +- `pending` - 待审核,需要管理员审核 + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "user": { + "id": "1", + "username": "testuser", + "nickname": "测试用户", + "status": "locked", + "status_description": "已锁定", + "updated_at": "2025-12-24T10:00:00.000Z" + }, + "reason": "用户违反社区规定" + }, + "message": "用户状态修改成功" +} +``` + +#### 2. 批量修改用户状态 + +**接口地址**: `POST /admin/users/batch-status` + +**功能描述**: 管理员批量修改多个用户的账户状态 + +#### 请求参数 + +```json +{ + "user_ids": ["1", "2", "3"], + "status": "locked", + "reason": "批量处理违规用户" +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| user_ids | array | 是 | 用户ID列表(1-100个) | +| status | string | 是 | 用户状态枚举值 | +| reason | string | 否 | 批量修改原因 | + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "result": { + "success_users": [ + { + "id": "1", + "username": "user1", + "nickname": "用户1", + "status": "locked", + "status_description": "已锁定", + "updated_at": "2025-12-24T10:00:00.000Z" + } + ], + "failed_users": [ + { + "user_id": "999", + "error": "用户不存在" + } + ], + "success_count": 1, + "failed_count": 1, + "total_count": 2 + }, + "reason": "批量处理违规用户" + }, + "message": "批量用户状态修改完成,成功:1,失败:1" +} +``` + +#### 3. 获取用户状态统计 + +**接口地址**: `GET /admin/users/status-stats` + +**功能描述**: 获取各种用户状态的数量统计信息 + +#### 请求参数 + +无 + +#### 响应示例 + +**成功响应** (200): +```json +{ + "success": true, + "data": { + "stats": { + "active": 1250, + "inactive": 45, + "locked": 12, + "banned": 8, + "deleted": 3, + "pending": 15, + "total": 1333 + }, + "timestamp": "2025-12-24T10:00:00.000Z" + }, + "message": "用户状态统计获取成功" +} + +## 测试指南和边界条件 + +### 🧪 **前端测试建议** + +为了确保前端应用的稳定性,建议对以下场景进行全面测试: + +#### **1. 用户认证测试** + +##### **注册功能测试** +```javascript +// 正常注册流程 +const testNormalRegister = async () => { + // 1. 发送邮箱验证码 + const codeResponse = await sendEmailVerification('test@example.com'); + + // 2. 使用验证码注册 + const registerResponse = await register({ + username: 'testuser123', + password: 'Test123456', + nickname: '测试用户', + email: 'test@example.com', + email_verification_code: codeResponse.data.verification_code + }); + + expect(registerResponse.success).toBe(true); + expect(registerResponse.data.user.username).toBe('testuser123'); + expect(registerResponse.data.access_token).toBeDefined(); +}; + +// 边界条件测试 +const testRegisterEdgeCases = async () => { + // 密码强度测试 + await expectError(register({ + username: 'test', + password: '123', // 太短 + nickname: '测试' + }), 'REGISTER_FAILED'); + + // 用户名重复测试 + await expectError(register({ + username: 'existinguser', // 已存在 + password: 'Test123456', + nickname: '测试' + }), 'REGISTER_FAILED'); + + // 邮箱验证码错误测试 + await expectError(register({ + username: 'newuser', + password: 'Test123456', + nickname: '测试', + email: 'test@example.com', + email_verification_code: '000000' // 错误验证码 + }), 'REGISTER_FAILED'); +}; +``` + +##### **登录功能测试** +```javascript +// 多种登录方式测试 +const testLoginMethods = async () => { + // 用户名登录 + await testLogin('testuser', 'password123'); + + // 邮箱登录 + await testLogin('test@example.com', 'password123'); + + // 手机号登录(如果支持) + await testLogin('+8613800138000', 'password123'); +}; + +// 登录失败场景测试 +const testLoginFailures = async () => { + // 用户不存在 + await expectError(login('nonexistent', 'password'), 'LOGIN_FAILED'); + + // 密码错误 + await expectError(login('testuser', 'wrongpassword'), 'LOGIN_FAILED'); + + // 账户被锁定 + await expectError(login('lockeduser', 'password'), 'LOGIN_FAILED'); +}; +``` + +#### **2. 验证码功能测试** + +##### **验证码生成和验证** +```javascript +// 验证码频率限制测试 +const testVerificationRateLimit = async () => { + const email = 'test@example.com'; + + // 第一次发送 - 应该成功 + const response1 = await sendEmailVerification(email); + expect(response1.success).toBe(true); + + // 立即再次发送 - 应该被限制 + await expectError( + sendEmailVerification(email), + 'TOO_MANY_REQUESTS', + 429 + ); + + // 等待冷却时间后再次发送 + await sleep(60000); // 等待1分钟 + const response2 = await sendEmailVerification(email); + expect(response2.success).toBe(true); +}; + +// 验证码尝试次数限制测试 +const testVerificationAttempts = async () => { + const email = 'test@example.com'; + const response = await sendEmailVerification(email); + const correctCode = response.data.verification_code; + + // 错误尝试3次 + for (let i = 0; i < 3; i++) { + await expectError( + verifyEmail(email, '000000'), + 'VERIFICATION_FAILED' + ); + } + + // 第4次尝试,即使验证码正确也应该失败 + await expectError( + verifyEmail(email, correctCode), + 'VERIFICATION_FAILED' + ); +}; +``` + +#### **3. 管理员功能测试** + +##### **权限验证测试** +```javascript +// 管理员登录测试 +const testAdminLogin = async () => { + // 正确的管理员凭据 + const response = await adminLogin('admin', 'Admin123456'); + expect(response.success).toBe(true); + expect(response.data.admin.role).toBe(9); + + // 普通用户尝试管理员登录 + await expectError( + adminLogin('normaluser', 'password'), + 'ADMIN_LOGIN_FAILED', + 403 + ); +}; + +// 管理员操作权限测试 +const testAdminOperations = async () => { + const adminToken = await getAdminToken(); + + // 有效token的操作 + const users = await getUserList(adminToken); + expect(users.success).toBe(true); + + // 无效token的操作 + await expectError( + getUserList('invalid_token'), + 'UNAUTHORIZED', + 401 + ); + + // 普通用户token的操作 + const userToken = await getUserToken(); + await expectError( + getUserList(userToken), + 'FORBIDDEN', + 403 + ); +}; +``` + +#### **4. 用户状态管理测试** + +##### **状态变更测试** +```javascript +// 用户状态修改测试 +const testUserStatusUpdate = async () => { + const adminToken = await getAdminToken(); + const userId = '1'; + + // 锁定用户 + const lockResponse = await updateUserStatus(adminToken, userId, { + status: 'locked', + reason: '违反社区规定' + }); + expect(lockResponse.success).toBe(true); + expect(lockResponse.data.user.status).toBe('locked'); + + // 被锁定用户尝试登录 + await expectError( + login('lockeduser', 'password'), + 'LOGIN_FAILED', + 403 + ); + + // 恢复用户状态 + await updateUserStatus(adminToken, userId, { + status: 'active', + reason: '恢复正常' + }); +}; + +// 批量状态修改测试 +const testBatchStatusUpdate = async () => { + const adminToken = await getAdminToken(); + + const response = await batchUpdateUserStatus(adminToken, { + user_ids: ['1', '2', '999'], // 包含不存在的用户ID + status: 'locked', + reason: '批量处理' + }); + + expect(response.success).toBe(true); + expect(response.data.result.success_count).toBe(2); + expect(response.data.result.failed_count).toBe(1); + expect(response.data.result.failed_users[0].user_id).toBe('999'); +}; +``` + +#### **5. 安全功能测试** + +##### **频率限制测试** +```javascript +// 登录频率限制测试 +const testLoginRateLimit = async () => { + // 快速连续登录尝试 + for (let i = 0; i < 3; i++) { + try { + await login('testuser', 'wrongpassword'); + } catch (error) { + if (error.status === 429) { + expect(error.message).toContain('Too Many Requests'); + break; + } + } + } +}; + +// 维护模式测试 +const testMaintenanceMode = async () => { + // 模拟维护模式开启 + // 所有请求都应该返回503 + await expectError( + getAppStatus(), + 'SERVICE_UNAVAILABLE', + 503 + ); +}; +``` + +#### **6. 错误处理测试** + +##### **网络错误处理** +```javascript +// 超时处理测试 +const testTimeout = async () => { + // 模拟长时间操作 + await expectError( + slowOperation(), + 'REQUEST_TIMEOUT', + 408 + ); +}; + +// 内容类型验证测试 +const testContentType = async () => { + // 错误的Content-Type + await expectError( + fetch('/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: 'invalid data' + }), + 'UNSUPPORTED_MEDIA_TYPE', + 415 + ); +}; +``` + +### 📋 **测试检查清单** + +#### **功能测试** +- [ ] 用户注册(正常流程) +- [ ] 用户注册(邮箱验证流程) +- [ ] 用户登录(用户名/邮箱/手机号) +- [ ] GitHub OAuth登录 +- [ ] 密码重置流程 +- [ ] 密码修改功能 +- [ ] 邮箱验证码发送和验证 +- [ ] 管理员登录 +- [ ] 用户列表查询 +- [ ] 用户详情查询 +- [ ] 用户密码重置(管理员) +- [ ] 用户状态管理 +- [ ] 批量用户状态修改 +- [ ] 用户状态统计 +- [ ] 运行日志查询 +- [ ] 日志文件下载 + +#### **边界条件测试** +- [ ] 密码强度验证(太短、太简单) +- [ ] 用户名格式验证(特殊字符、长度) +- [ ] 邮箱格式验证 +- [ ] 验证码格式验证(非6位数字) +- [ ] 用户名重复检查 +- [ ] 邮箱重复检查 +- [ ] 不存在用户的操作 +- [ ] 无效验证码验证 +- [ ] 过期验证码验证 +- [ ] 验证码尝试次数限制 + +#### **安全测试** +- [ ] 频率限制(登录、发送验证码) +- [ ] 权限验证(管理员接口) +- [ ] Token有效性验证 +- [ ] 用户状态检查(锁定、禁用用户登录) +- [ ] 维护模式功能 +- [ ] 内容类型验证 +- [ ] 请求超时处理 + +#### **错误处理测试** +- [ ] 网络连接错误 +- [ ] 服务器内部错误(500) +- [ ] 请求超时(408) +- [ ] 频率限制(429) +- [ ] 权限不足(403) +- [ ] 资源不存在(404) +- [ ] 参数验证错误(400) +- [ ] 维护模式(503) + +### 🔧 **测试工具推荐** + +#### **API测试工具** +- **Postman**: 手动API测试和文档 +- **Insomnia**: 轻量级API客户端 +- **curl**: 命令行测试 +- **HTTPie**: 用户友好的命令行工具 + +#### **自动化测试框架** +- **Jest**: JavaScript测试框架 +- **Cypress**: 端到端测试 +- **Playwright**: 现代Web测试 +- **Supertest**: Node.js HTTP测试 + +#### **测试脚本示例** +项目提供了现成的测试脚本: +- `test-api.ps1` - Windows PowerShell测试脚本 +- `test-api.sh` - Linux/macOS Bash测试脚本 + +运行测试脚本: +```bash +# Windows +.\test-api.ps1 + +# Linux/macOS +./test-api.sh + +# 自定义参数 +.\test-api.ps1 -BaseUrl "http://localhost:3000" -TestEmail "custom@example.com" +``` + +### 📊 **测试数据管理** + +#### **测试环境配置** +- **内存模式**: 数据重启后清空,适合快速测试 +- **数据库模式**: 数据持久化,适合完整功能测试 +- **测试模式**: 邮件不真实发送,验证码在响应中返回 + +#### **测试数据清理** +```javascript +// 清理测试数据 +const cleanupTestData = async () => { + // 删除测试用户 + await deleteTestUsers(); + + // 清理Redis验证码 + await clearVerificationCodes(); + + // 重置计数器 + await resetRateLimitCounters(); +}; +``` + +### ⚠️ **测试注意事项** + +1. **频率限制**: 测试时注意API频率限制,避免被限制 +2. **测试隔离**: 每个测试用例使用独立的测试数据 +3. **异步操作**: 注意验证码生成和验证的时序 +4. **错误恢复**: 测试失败后要清理测试数据 +5. **环境差异**: 开发、测试、生产环境的配置差异 +6. **数据一致性**: 并发测试时注意数据竞争条件 + +### 🚀 **性能测试建议** + +#### **负载测试场景** +- 并发用户注册 +- 高频验证码发送 +- 大量用户同时登录 +- 管理员批量操作 +- 日志文件下载 + +#### **性能指标** +- 响应时间 < 2秒(正常操作) +- 吞吐量 > 100 req/s +- 错误率 < 1% +- 内存使用稳定 +- CPU使用率 < 80% + +### **通用错误代码** + +| 错误代码 | HTTP状态码 | 说明 | 触发条件 | +|----------|------------|------|----------| +| LOGIN_FAILED | 401 | 登录失败 | 用户名不存在、密码错误、账户被锁定 | +| REGISTER_FAILED | 400/409 | 注册失败 | 用户名已存在、密码强度不足、验证码错误 | +| GITHUB_OAUTH_FAILED | 401 | GitHub OAuth失败 | GitHub认证信息无效 | +| SEND_CODE_FAILED | 400 | 发送验证码失败 | 邮箱格式错误、发送服务异常 | +| RESET_PASSWORD_FAILED | 400 | 重置密码失败 | 验证码无效、密码强度不足 | +| CHANGE_PASSWORD_FAILED | 400 | 修改密码失败 | 旧密码错误、新密码强度不足 | +| TEST_MODE_ONLY | 206 | 测试模式 | 邮件服务未配置,验证码未真实发送 | + +### **管理员相关错误代码** + +| 错误代码 | HTTP状态码 | 说明 | 触发条件 | +|----------|------------|------|----------| +| ADMIN_LOGIN_FAILED | 401/403 | 管理员登录失败 | 非管理员用户、凭据错误 | +| ADMIN_USERS_FAILED | 500 | 获取用户列表失败 | 数据库查询异常 | +| ADMIN_OPERATION_FAILED | 400/500 | 管理员操作失败 | 参数错误、系统异常 | + +### **用户状态相关错误代码** + +| 错误代码 | HTTP状态码 | 说明 | 触发条件 | +|----------|------------|------|----------| +| USER_STATUS_UPDATE_FAILED | 400/404 | 用户状态修改失败 | 用户不存在、状态值无效 | +| BATCH_USER_STATUS_UPDATE_FAILED | 400 | 批量用户状态修改失败 | 用户ID列表为空、状态值无效 | +| USER_STATUS_STATS_FAILED | 500 | 用户状态统计失败 | 数据库查询异常 | + +### **安全相关错误代码** + +| 错误代码 | HTTP状态码 | 说明 | 触发条件 | +|----------|------------|------|----------| +| SERVICE_UNAVAILABLE | 503 | 系统维护中 | 维护模式开启 | +| TOO_MANY_REQUESTS | 429 | 请求过于频繁 | 触发频率限制 | +| REQUEST_TIMEOUT | 408 | 请求超时 | 操作执行时间过长 | +| UNSUPPORTED_MEDIA_TYPE | 415 | 不支持的媒体类型 | Content-Type不正确 | +| UNAUTHORIZED | 401 | 未授权 | Token无效或过期 | +| FORBIDDEN | 403 | 权限不足 | 非管理员访问管理员接口 | + +### **验证码相关错误代码** + +| 错误代码 | HTTP状态码 | 说明 | 触发条件 | +|----------|------------|------|----------| +| VERIFICATION_CODE_EXPIRED | 400 | 验证码已过期 | 验证码超过有效期(5分钟) | +| VERIFICATION_CODE_INVALID | 400 | 验证码无效 | 验证码格式错误或不存在 | +| VERIFICATION_CODE_ATTEMPTS_EXCEEDED | 400 | 验证码尝试次数过多 | 错误尝试超过3次 | +| VERIFICATION_CODE_RATE_LIMITED | 429 | 验证码发送频率限制 | 1分钟内重复发送 | +| VERIFICATION_CODE_HOURLY_LIMIT | 429 | 验证码每小时限制 | 1小时内发送超过5次 | + +### **详细错误响应格式** + +#### **标准错误响应** +```json +{ + "success": false, + "message": "具体错误描述", + "error_code": "ERROR_CODE", + "timestamp": "2025-12-24T10:00:00.000Z" +} +``` + +#### **验证错误响应** +```json +{ + "success": false, + "message": "参数验证失败", + "error_code": "VALIDATION_FAILED", + "errors": [ + { + "field": "password", + "message": "密码长度至少8位" + }, + { + "field": "email", + "message": "邮箱格式不正确" + } + ] +} +``` + +#### **频率限制错误响应** +```json +{ + "success": false, + "message": "请求过于频繁,请稍后再试", + "error_code": "TOO_MANY_REQUESTS", + "retry_after": 60, + "limit_info": { + "limit": 5, + "remaining": 0, + "reset_time": "2025-12-24T10:01:00.000Z" + } +} +``` + +#### **维护模式错误响应** +```json +{ + "success": false, + "message": "系统正在维护中,请稍后再试", + "error_code": "SERVICE_UNAVAILABLE", + "maintenance_info": { + "start_time": "2025-12-24T10:00:00.000Z", + "estimated_end_time": "2025-12-24T12:00:00.000Z", + "retry_after": 1800, + "reason": "系统升级维护" + } +} +``` + +## 数据验证规则 + +### 用户名规则 +- 长度:1-50字符 +- 格式:只能包含字母、数字和下划线 +- 正则表达式:`^[a-zA-Z0-9_]+$` + +### 密码规则 +- 长度:8-128字符 +- 格式:必须包含字母和数字 +- 正则表达式:`^(?=.*[a-zA-Z])(?=.*\d)` + +### 验证码规则 +- 长度:6位数字 +- 正则表达式:`^\d{6}$` +- 有效期:通常为5-15分钟 + +### 邮箱规则 +- 格式:符合标准邮箱格式 +- 验证:支持邮箱验证码验证 + +### 管理员权限 +- 角色:role=9 为管理员 +- 认证:需要 JWT Token +- 权限:可管理所有用户数据 + +## 使用示例 + +### JavaScript/TypeScript 示例 + +```typescript +// 获取应用状态 +const statusResponse = await fetch('http://localhost:3000/'); +const statusData = await statusResponse.json(); +console.log('服务状态:', statusData.status); + +// 用户登录 +const loginResponse = await fetch('http://localhost:3000/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + identifier: 'testuser', + password: 'password123' + }) +}); + +const loginData = await loginResponse.json(); +if (loginData.success) { + const token = loginData.data.access_token; + // 保存token用于后续请求 + localStorage.setItem('token', token); +} + +// 用户注册(带邮箱验证) +// 1. 先发送邮箱验证码 +const sendCodeResponse = await fetch('http://localhost:3000/auth/send-email-verification', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: 'newuser@example.com' + }) +}); + +// 2. 用户输入验证码后进行注册 +const registerResponse = await fetch('http://localhost:3000/auth/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: 'newuser', + password: 'password123', + nickname: '新用户', + email: 'newuser@example.com', + email_verification_code: '123456' + }) +}); + +// 管理员登录 +const adminLoginResponse = await fetch('http://localhost:3000/admin/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + identifier: 'admin', + password: 'Admin123456' + }) +}); + +const adminData = await adminLoginResponse.json(); +if (adminData.success) { + const adminToken = adminData.data.access_token; + + // 获取用户列表 + const usersResponse = await fetch('http://localhost:3000/admin/users?limit=10&offset=0', { + headers: { + 'Authorization': `Bearer ${adminToken}` + } + }); + + const usersData = await usersResponse.json(); + console.log('用户列表:', usersData.data.users); +} +``` + +### cURL 示例 + +```bash +# 获取应用状态 +curl -X GET http://localhost:3000/ + +# 用户登录 +curl -X POST http://localhost:3000/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "identifier": "testuser", + "password": "password123" + }' + +# 发送邮箱验证码 +curl -X POST http://localhost:3000/auth/send-email-verification \ + -H "Content-Type: application/json" \ + -d '{ + "email": "newuser@example.com" + }' + +# 用户注册 +curl -X POST http://localhost:3000/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "newuser", + "password": "password123", + "nickname": "新用户", + "email": "newuser@example.com", + "email_verification_code": "123456" + }' + +# 管理员登录 +curl -X POST http://localhost:3000/admin/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "identifier": "admin", + "password": "Admin123456" + }' + +# 获取用户列表(需要管理员Token) +curl -X GET "http://localhost:3000/admin/users?limit=10&offset=0" \ + -H "Authorization: Bearer YOUR_ADMIN_TOKEN" + +# 重置用户密码 +curl -X POST http://localhost:3000/admin/users/1/reset-password \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ + -d '{ + "new_password": "NewPass1234" + }' + +# 获取运行日志 +curl -X GET "http://localhost:3000/admin/logs/runtime?lines=100" \ + -H "Authorization: Bearer YOUR_ADMIN_TOKEN" + +# 下载日志压缩包 +curl -X GET http://localhost:3000/admin/logs/archive \ + -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ + -o logs.tar.gz +``` + +## 安全特性 + +### 1. 频率限制 (Rate Limiting) + +系统实现了基于IP地址的频率限制,防止恶意攻击和滥用: + +#### 限制策略 + +| 接口类型 | 限制规则 | 时间窗口 | 说明 | +|----------|----------|----------|------| +| 登录接口 | 5次/分钟 | 60秒 | 防止暴力破解 | +| 注册接口 | 10次/5分钟 | 300秒 | 防止批量注册(开发环境已放宽) | +| 发送验证码 | 1次/分钟 | 60秒 | 防止验证码滥发 | +| 密码重置 | 3次/小时 | 3600秒 | 限制密码重置频率 | +| 管理员操作 | 10次/分钟 | 60秒 | 限制管理员操作频率 | +| 一般接口 | 30次/分钟 | 60秒 | 通用API限制 | 100次/分钟 | 60秒 | 防止接口滥用 | + +#### 响应示例 + +当触发频率限制时,返回 **429 Too Many Requests**: + +```json +{ + "success": false, + "message": "注册请求过于频繁,请5分钟后再试", + "error_code": "TOO_MANY_REQUESTS", + "throttle_info": { + "limit": 10, + "window_seconds": 300, + "current_requests": 10, + "reset_time": "2025-12-24T11:26:41.136Z" + } +} +``` + +**重要说明**: +- 频率限制基于IP地址 +- 超过限制后需要等待到重置时间才能再次请求 +- 开发环境下注册接口限制已放宽至10次/5分钟 + +### 2. 维护模式 (Maintenance Mode) + +系统支持维护模式,在系统升级或维护期间暂停服务: + +#### 启用方式 + +设置环境变量: +```bash +MAINTENANCE_MODE=true +MAINTENANCE_START_TIME=2025-12-24T10:00:00.000Z +MAINTENANCE_END_TIME=2025-12-24T12:00:00.000Z +MAINTENANCE_REASON=系统升级维护 +MAINTENANCE_RETRY_AFTER=1800 +``` + +#### 响应示例 + +维护模式下所有请求返回 **503 Service Unavailable**: + +```json +{ + "success": false, + "message": "系统正在维护中,请稍后再试", + "error_code": "SERVICE_UNAVAILABLE", + "maintenance_info": { + "start_time": "2025-12-24T10:00:00.000Z", + "estimated_end_time": "2025-12-24T12:00:00.000Z", + "retry_after": 1800, + "reason": "系统升级维护" + } +} +``` + +### 3. 内容类型验证 (Content Type Validation) + +系统验证POST/PUT请求的Content-Type头: + +#### 支持的内容类型 + +- `application/json` - JSON数据 +- `application/x-www-form-urlencoded` - 表单数据 +- `multipart/form-data` - 文件上传 + +#### 响应示例 + +不支持的内容类型返回 **415 Unsupported Media Type**: + +```json +{ + "statusCode": 415, + "message": "不支持的媒体类型", + "error": "Unsupported Media Type" +} +``` + +### 4. 请求超时控制 (Request Timeout) + +系统为不同类型的操作设置了超时限制: + +#### 超时配置 + +| 操作类型 | 超时时间 | 说明 | +|----------|----------|------| +| 普通操作 | 30秒 | 一般API请求 | +| 数据库查询 | 60秒 | 复杂查询操作 | +| 慢操作 | 120秒 | 批量处理等耗时操作 | + +#### 响应示例 + +请求超时返回 **408 Request Timeout**: + +```json +{ + "statusCode": 408, + "message": "请求超时", + "error": "Request Timeout" +} +``` + +### 5. 用户状态管理 (User Status Management) + +系统支持细粒度的用户状态控制: + +#### 用户状态枚举 + +| 状态值 | 状态名称 | 说明 | +|--------|----------|------| +| active | 正常 | 用户可以正常使用所有功能 | +| inactive | 未激活 | 新注册用户,需要邮箱验证 | +| locked | 锁定 | 临时锁定,可以解锁 | +| banned | 禁用 | 永久禁用,需要管理员处理 | +| deleted | 删除 | 软删除状态,数据保留 | +| pending | 待审核 | 需要管理员审核激活 | + +#### 状态检查 + +登录时系统会检查用户状态: + +- `active`: 正常登录 +- `inactive`: 提示需要邮箱验证 +- `locked`: 返回账户锁定错误 +- `banned`: 返回账户禁用错误 +- `deleted`: 返回账户不存在错误 +- `pending`: 返回账户待审核错误 + +### 6. 安全最佳实践 + +#### JWT Token 安全 + +- Token 有效期:8小时 +- 使用 HS256 算法签名 +- 包含用户ID、角色等关键信息 +- 建议在客户端安全存储 + +#### 密码安全 + +- 使用 bcrypt 加密存储 +- 支持密码强度验证 +- 不在日志中记录明文密码 +- 支持密码重置功能 + +#### API 安全 + +- 所有管理员接口需要身份验证 +- 支持跨域资源共享 (CORS) +- 实现请求日志记录 +- 敏感信息自动脱敏 + +## 注意事项 + +1. **安全性**: 实际应用中应使用HTTPS协议 +2. **令牌**: 示例中的access_token是JWT格式,需要妥善保存 +3. **验证码**: + - 实际应用中不应在响应中返回验证码 + - 测试模式下会在控制台显示验证码 + - 验证码有效期通常为5-15分钟 +4. **用户ID**: 修改密码接口中的user_id应从JWT令牌中获取,而不是从请求体中传递 +5. **错误处理**: 建议在客户端实现适当的错误处理和用户提示 +6. **限流**: 建议对登录、注册等接口实施限流策略 +7. **管理员权限**: + - 管理员接口需要 role=9 的用户权限 + - 需要在请求头中携带有效的JWT Token + - Token格式:`Authorization: Bearer ` +8. **存储模式**: + - 数据库模式:数据持久化存储在MySQL + - 内存模式:数据存储在内存中,重启后丢失 +9. **邮箱验证**: + - 注册时如果提供邮箱,需要先获取验证码 + - 支持重新发送验证码功能 + - 调试接口仅用于开发环境 + +## 常见测试场景 + +### 🔍 **前端开发者必测场景** + +#### **1. 用户注册完整流程** +```javascript +// 场景:新用户完整注册流程 +const testCompleteRegistration = async () => { + const email = 'newuser@example.com'; + + // Step 1: 发送邮箱验证码 + const codeResponse = await fetch('/auth/send-email-verification', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); + + expect(codeResponse.status).toBe(206); // 测试模式 + const codeData = await codeResponse.json(); + expect(codeData.success).toBe(false); + expect(codeData.error_code).toBe('TEST_MODE_ONLY'); + + // Step 2: 使用验证码注册 + const registerResponse = await fetch('/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: 'newuser123', + password: 'SecurePass123', + nickname: '新用户', + email: email, + email_verification_code: codeData.data.verification_code + }) + }); + + expect(registerResponse.status).toBe(201); + const registerData = await registerResponse.json(); + expect(registerData.success).toBe(true); + expect(registerData.data.access_token).toBeDefined(); +}; +``` + +#### **2. 登录失败处理** +```javascript +// 场景:各种登录失败情况 +const testLoginFailures = async () => { + // 用户不存在 + const response1 = await fetch('/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + identifier: 'nonexistent', + password: 'password123' + }) + }); + + expect(response1.status).toBe(200); // 业务错误返回200 + const data1 = await response1.json(); + expect(data1.success).toBe(false); + expect(data1.error_code).toBe('LOGIN_FAILED'); + + // 密码错误 + const response2 = await fetch('/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + identifier: 'existinguser', + password: 'wrongpassword' + }) + }); + + expect(response2.status).toBe(200); + const data2 = await response2.json(); + expect(data2.success).toBe(false); + expect(data2.error_code).toBe('LOGIN_FAILED'); +}; +``` + +#### **3. 频率限制测试** +```javascript +// 场景:验证码发送频率限制 +const testRateLimit = async () => { + const email = 'test@example.com'; + + // 第一次发送 - 成功 + const response1 = await fetch('/auth/send-email-verification', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); + expect(response1.status).toBe(206); // 测试模式 + + // 立即再次发送 - 被限制 + const response2 = await fetch('/auth/send-email-verification', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); + expect(response2.status).toBe(429); + + const data2 = await response2.json(); + expect(data2.message).toContain('请等待'); +}; +``` + +#### **4. 管理员权限测试** +```javascript +// 场景:管理员权限验证 +const testAdminPermissions = async () => { + // 普通用户尝试访问管理员接口 + const userToken = 'user_token_here'; + + const response = await fetch('/admin/users', { + headers: { 'Authorization': `Bearer ${userToken}` } + }); + + expect(response.status).toBe(403); + + // 无效token访问 + const response2 = await fetch('/admin/users', { + headers: { 'Authorization': 'Bearer invalid_token' } + }); + + expect(response2.status).toBe(401); + + // 正确的管理员token + const adminToken = await getAdminToken(); + const response3 = await fetch('/admin/users', { + headers: { 'Authorization': `Bearer ${adminToken}` } + }); + + expect(response3.status).toBe(200); +}; +``` + +#### **5. 用户状态影响登录** +```javascript +// 场景:不同用户状态的登录测试 +const testUserStatusLogin = async () => { + // 正常用户登录 + const activeResponse = await login('activeuser', 'password'); + expect(activeResponse.success).toBe(true); + + // 锁定用户登录 + const lockedResponse = await login('lockeduser', 'password'); + expect(lockedResponse.success).toBe(false); + expect(lockedResponse.message).toContain('锁定'); + + // 禁用用户登录 + const bannedResponse = await login('banneduser', 'password'); + expect(bannedResponse.success).toBe(false); + expect(bannedResponse.message).toContain('禁用'); +}; +``` + +### 📝 **边界条件测试清单** + +#### **输入验证测试** +- [ ] 空字符串输入 +- [ ] 超长字符串输入(用户名>50字符) +- [ ] 特殊字符输入(SQL注入尝试) +- [ ] 无效邮箱格式 +- [ ] 弱密码(少于8位、纯数字、纯字母) +- [ ] 无效验证码格式(非6位数字) + +#### **状态边界测试** +- [ ] 验证码过期边界(5分钟) +- [ ] 验证码尝试次数边界(3次) +- [ ] 频率限制边界(1分钟、1小时) +- [ ] Token过期边界(8小时) +- [ ] 用户状态变更后的立即登录 + +#### **并发测试** +- [ ] 同时发送多个验证码请求 +- [ ] 同时使用相同验证码验证 +- [ ] 并发用户注册相同用户名 +- [ ] 并发管理员操作同一用户 + +### 🚨 **错误恢复测试** + +#### **网络异常处理** +```javascript +// 场景:网络中断恢复 +const testNetworkRecovery = async () => { + // 模拟网络中断 + mockNetworkError(); + + try { + await login('testuser', 'password'); + fail('应该抛出网络错误'); + } catch (error) { + expect(error.message).toContain('网络'); + } + + // 恢复网络 + restoreNetwork(); + + // 重试应该成功 + const response = await login('testuser', 'password'); + expect(response.success).toBe(true); +}; +``` + +#### **服务降级测试** +```javascript +// 场景:邮件服务不可用时的降级 +const testEmailServiceDegradation = async () => { + // 邮件服务不可用时,应该返回测试模式 + const response = await fetch('/auth/send-email-verification', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: 'test@example.com' }) + }); + + expect(response.status).toBe(206); + const data = await response.json(); + expect(data.error_code).toBe('TEST_MODE_ONLY'); + expect(data.data.is_test_mode).toBe(true); +}; +``` + +### 🔧 **自动化测试脚本** + +#### **快速冒烟测试** +```bash +#!/bin/bash +# 快速验证所有关键接口 + +BASE_URL="http://localhost:3000" + +echo "🚀 开始API冒烟测试..." + +# 1. 应用状态检查 +echo "1. 检查应用状态..." +curl -f "$BASE_URL/" > /dev/null || exit 1 + +# 2. 验证码发送 +echo "2. 测试验证码发送..." +RESPONSE=$(curl -s -X POST "$BASE_URL/auth/send-email-verification" \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com"}') + +CODE=$(echo "$RESPONSE" | jq -r '.data.verification_code') +if [ "$CODE" = "null" ]; then + echo "❌ 验证码发送失败" + exit 1 +fi + +# 3. 用户注册 +echo "3. 测试用户注册..." +USERNAME="smoketest_$(date +%s)" +curl -f -X POST "$BASE_URL/auth/register" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"$USERNAME\",\"password\":\"Test123456\",\"nickname\":\"冒烟测试\",\"email\":\"test@example.com\",\"email_verification_code\":\"$CODE\"}" > /dev/null || exit 1 + +# 4. 用户登录 +echo "4. 测试用户登录..." +curl -f -X POST "$BASE_URL/auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"identifier\":\"$USERNAME\",\"password\":\"Test123456\"}" > /dev/null || exit 1 + +echo "✅ 冒烟测试通过!" +``` + +#### **性能基准测试** +```bash +#!/bin/bash +# 简单的性能基准测试 + +echo "📊 开始性能基准测试..." + +# 并发登录测试 +echo "测试并发登录性能..." +ab -n 100 -c 10 -T 'application/json' -p login_data.json http://localhost:3000/auth/login + +# 验证码发送性能测试 +echo "测试验证码发送性能..." +ab -n 50 -c 5 -T 'application/json' -p email_data.json http://localhost:3000/auth/send-email-verification + +echo "📈 性能测试完成,请查看上述结果" +``` + +### 💡 **测试技巧和建议** + +#### **1. 测试数据管理** +- 使用时间戳生成唯一的测试用户名 +- 测试完成后清理测试数据 +- 使用专门的测试邮箱域名 + +#### **2. 异步操作处理** +- 验证码生成后立即使用,避免过期 +- 注意频率限制的时间窗口 +- 使用适当的等待时间 + +#### **3. 错误场景覆盖** +- 测试所有可能的HTTP状态码 +- 验证错误消息的准确性 +- 测试错误恢复机制 + +#### **4. 安全测试** +- 尝试SQL注入和XSS攻击 +- 测试权限绕过 +- 验证敏感信息不泄露 + +这些测试场景和边界条件将帮助前端开发者进行全面的API测试,确保应用的稳定性和安全性。 + +- **v1.0.0** (2025-12-24): + - **完整的API文档更新** + - 重新整理接口分类,将用户管理接口独立分类 + - 确保文档与实际运行的服务完全一致 + - 验证所有接口的请求参数和响应格式 + - **修复HTTP状态码问题**:所有接口现在根据业务结果返回正确状态码 + - **更新限流配置**:注册接口限制调整为10次/5分钟(开发环境) + - **应用状态接口** (1个) + - `GET /` - 获取应用状态 + - **用户认证接口** (11个) + - 用户登录、注册、GitHub OAuth + - 密码重置和修改功能 + - 邮箱验证相关接口 + - 调试验证码接口 + - **新增**:清除限流记录接口(开发环境) + - **管理员接口** (6个) + - 管理员登录和用户管理 + - 用户列表和详情查询 + - 密码重置功能 + - 日志管理和下载 + - **用户管理接口** (3个) + - 用户状态管理 (active/inactive/locked/banned/deleted/pending) + - 单个用户状态修改接口 + - 批量用户状态修改接口 + - 用户状态统计接口 + - **安全增强功能** + - 频率限制中间件 (Rate Limiting) - 已调整配置 + - 维护模式中间件 (Maintenance Mode) + - 内容类型验证中间件 (Content Type Validation) + - 请求超时拦截器 (Request Timeout) + - 用户状态检查和权限控制 + - **修复**:HTTP状态码现在正确反映业务执行结果 + - **总计接口数量**: 21个API接口 + - 完善错误代码和使用示例 + - 修复路由冲突问题 + - 确保文档与实际测试效果一致 + - **重要修复**:解决了业务失败但返回成功状态码的问题 + diff --git a/docs/auth/form_validation.md b/docs/auth/form_validation.md new file mode 100644 index 0000000..a5b8a51 --- /dev/null +++ b/docs/auth/form_validation.md @@ -0,0 +1,324 @@ +# 表单验证规范文档 + +## 概述 + +本文档详细说明了登录和注册表单的验证规则、UI交互规范和错误处理机制。 + +## 验证规则 + +### 1. 用户名验证 + +#### 规则 +- **必填项**: 是 +- **长度**: 1-50字符 +- **格式**: 只能包含字母、数字和下划线 +- **正则表达式**: `^[a-zA-Z0-9_]+$` + +#### 错误提示 +- 空值: "用户名不能为空" +- 长度不符: "用户名长度应为1-50字符" +- 格式错误: "用户名只能包含字母、数字和下划线" + +### 2. 邮箱验证 + +#### 规则 +- **必填项**: 是(注册时) +- **格式**: 标准邮箱格式 +- **正则表达式**: `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` + +#### 错误提示 +- 空值: "邮箱不能为空" +- 格式错误: "请输入有效的邮箱地址" + +### 3. 密码验证 + +#### 规则 +- **必填项**: 是 +- **长度**: 8-128字符 +- **复杂度**: 必须包含字母和数字 +- **安全性**: 不允许纯数字或纯字母 + +#### 错误提示 +- 空值: "密码不能为空" +- 长度不足: "密码长度至少8位" +- 长度超限: "密码长度不能超过128位" +- 复杂度不够: "密码必须包含字母和数字" + +### 4. 确认密码验证 + +#### 规则 +- **必填项**: 是(注册时) +- **一致性**: 必须与密码字段完全一致 + +#### 错误提示 +- 空值: "确认密码不能为空" +- 不一致: "两次输入的密码不一致" + +### 5. 验证码验证 + +#### 规则 +- **必填项**: 是(注册时) +- **长度**: 6位数字 +- **格式**: 纯数字 +- **有效性**: 必须先获取验证码 + +#### 错误提示 +- 空值: "验证码不能为空" +- 长度错误: "验证码必须是6位数字" +- 格式错误: "验证码必须是6位数字" +- 未获取: "请先获取邮箱验证码" + +## UI交互规范 + +### 1. 必填项标识 + +所有必填字段都使用水平布局来显示标签、红色星号和右对齐的错误提示: + +#### 结构设计 +``` +VBoxContainer (字段容器) +├── HBoxContainer (标签和错误提示容器) +│ ├── Label (字段名称) +│ ├── Label (红色星号 " *") +│ ├── Control (弹性空间) [size_flags_horizontal = 3] +│ └── Label (红色错误提示) [右对齐,默认隐藏] +└── LineEdit (输入框) +``` + +#### 视觉效果 +- **标签**: 黑色文本,如 "用户名"、"邮箱"、"密码" +- **星号**: 红色文本 `Color(1, 0.2, 0.2, 1)`,内容为 " *" +- **弹性空间**: Control节点,`size_flags_horizontal = 3`,占据剩余空间 +- **错误提示**: 红色文本,12px字体,右对齐显示 (`horizontal_alignment = 2`) +- **布局**: 标签和星号左对齐,错误提示右对齐 + +#### 交互行为 +- **正常状态**: 显示 `用户名 * ` +- **错误状态**: 显示 `用户名 * 用户名不能为空` +- **输入时**: 错误提示自动隐藏,回到正常状态 + +#### 优势 +- 错误提示右对齐,与输入框右边缘对齐,视觉更整齐 +- 利用弹性空间实现左右分布,布局更美观 +- 不占用额外的垂直空间 +- 避免UI布局被拉长导致溢出 +- 保持界面紧凑美观 +- 红色星号和错误提示颜色一致,视觉统一 +- 错误提示位置固定,不会因为文本长度变化而影响布局 + +### 2. 实时验证 + +#### 失焦验证 +- 当用户离开输入框时(`focus_exited`)触发验证 +- 立即显示相应的错误提示 +- 错误提示显示在输入框下方 + +#### 输入时验证 +- 当用户开始输入时(`text_changed`)隐藏错误提示 +- 提供即时的视觉反馈 +- 避免在用户输入过程中显示错误 + +### 3. 错误提示样式 + +#### 视觉设计 +- **颜色**: 红色 `Color(1, 0.2, 0.2, 1)` +- **字体大小**: 12px +- **位置**: 输入框正下方 +- **显示状态**: 默认隐藏,验证失败时显示 + +#### 错误信息特点 +- 简洁明了,直接指出问题 +- 提供解决建议 +- 使用友好的语言 + +### 4. 按钮状态管理 + +#### 发送验证码按钮 +- **正常状态**: "发送验证码" +- **冷却状态**: "重新发送(60)" (倒计时) +- **禁用状态**: 请求进行中时禁用 + +#### 提交按钮 +- **正常状态**: 可点击 +- **禁用状态**: 表单验证失败或请求进行中时禁用 +- **加载状态**: 显示"正在注册..."等提示 + +## 验证流程 + +### 1. 登录表单验证流程 + +``` +用户输入 → 失焦验证 → 显示错误(如有) → 点击登录 → 整体验证 → 提交请求 +``` + +#### 验证步骤 +1. 检查用户名是否为空 +2. 检查密码是否为空 +3. 所有验证通过后提交登录请求 + +### 2. 注册表单验证流程 + +``` +用户输入 → 失焦验证 → 显示错误(如有) → 获取验证码 → 输入验证码 → 点击注册 → 整体验证 → 邮箱验证 → 用户注册 +``` + +#### 验证步骤 +1. 验证用户名格式和长度 +2. 验证邮箱格式 +3. 验证密码强度 +4. 验证确认密码一致性 +5. 验证验证码格式和有效性 +6. 发送邮箱验证请求 +7. 验证成功后发送注册请求 + +### 3. 验证码获取流程 + +``` +输入邮箱 → 验证邮箱格式 → 检查冷却时间 → 发送验证码 → 开始冷却计时 +``` + +#### 冷却机制 +- **冷却时间**: 60秒 +- **按钮状态**: 显示倒计时 +- **重复发送**: 冷却结束后可重新发送 + +## 错误处理机制 + +### 1. 客户端验证 + +#### 即时验证 +- 在用户输入过程中提供即时反馈 +- 防止用户提交无效数据 +- 提升用户体验 + +#### 提交前验证 +- 在发送请求前进行完整验证 +- 确保所有必填项都已正确填写 +- 避免无效的网络请求 + +### 2. 服务器验证 + +#### 后端验证 +- 服务器端进行二次验证 +- 处理服务器返回的错误信息 +- 显示相应的错误提示 + +#### 网络错误处理 +- 连接失败提示 +- 超时处理 +- 服务器错误提示 + +### 3. 用户引导 + +#### 错误恢复 +- 清晰的错误提示 +- 提供解决方案 +- 引导用户正确操作 + +#### 成功反馈 +- 注册成功提示 +- 自动跳转到登录界面 +- 自动填充用户名 + +## 代码实现 + +### 1. 验证函数 + +每个字段都有对应的验证函数: +- `validate_username(username: String) -> Dictionary` +- `validate_email(email: String) -> Dictionary` +- `validate_password(password: String) -> Dictionary` +- `validate_confirm_password(password: String, confirm: String) -> Dictionary` +- `validate_verification_code(code: String) -> Dictionary` + +### 2. 事件处理 + +#### 失焦事件 +- `_on_register_username_focus_exited()` +- `_on_register_email_focus_exited()` +- `_on_register_password_focus_exited()` +- `_on_register_confirm_focus_exited()` +- `_on_verification_focus_exited()` + +#### 输入事件 +- `_on_register_username_text_changed(new_text: String)` +- `_on_register_email_text_changed(new_text: String)` +- `_on_register_password_text_changed(new_text: String)` +- `_on_register_confirm_text_changed(new_text: String)` +- `_on_verification_text_changed(new_text: String)` + +### 3. 整体验证 + +- `validate_login_form() -> bool` +- `validate_register_form() -> bool` + +## 测试用例 + +### 1. 用户名测试 + +| 输入 | 预期结果 | 错误信息 | +|------|----------|----------| +| "" | 失败 | "用户名不能为空" | +| "a" | 成功 | - | +| "test_user123" | 成功 | - | +| "test@user" | 失败 | "用户名只能包含字母、数字和下划线" | +| "a".repeat(51) | 失败 | "用户名长度应为1-50字符" | + +### 2. 邮箱测试 + +| 输入 | 预期结果 | 错误信息 | +|------|----------|----------| +| "" | 失败 | "邮箱不能为空" | +| "test@example.com" | 成功 | - | +| "invalid-email" | 失败 | "请输入有效的邮箱地址" | +| "test@" | 失败 | "请输入有效的邮箱地址" | + +### 3. 密码测试 + +| 输入 | 预期结果 | 错误信息 | +|------|----------|----------| +| "" | 失败 | "密码不能为空" | +| "1234567" | 失败 | "密码长度至少8位" | +| "12345678" | 失败 | "密码必须包含字母和数字" | +| "abcdefgh" | 失败 | "密码必须包含字母和数字" | +| "abc12345" | 成功 | - | + +### 4. 验证码测试 + +| 输入 | 预期结果 | 错误信息 | +|------|----------|----------| +| "" | 失败 | "验证码不能为空" | +| "12345" | 失败 | "验证码必须是6位数字" | +| "1234567" | 失败 | "验证码必须是6位数字" | +| "12345a" | 失败 | "验证码必须是6位数字" | +| "123456" | 成功 | - | + +## 最佳实践 + +### 1. 用户体验 + +- **即时反馈**: 在用户输入时提供即时的视觉反馈 +- **清晰提示**: 错误信息要简洁明了,易于理解 +- **引导操作**: 通过UI引导用户完成正确的操作流程 + +### 2. 性能优化 + +- **避免过度验证**: 不在每次字符输入时都进行复杂验证 +- **合理的验证时机**: 在失焦和提交时进行验证 +- **缓存验证结果**: 避免重复验证相同的内容 + +### 3. 安全考虑 + +- **客户端+服务端**: 双重验证确保数据安全 +- **敏感信息**: 密码等敏感信息不在客户端明文存储 +- **防止暴力破解**: 验证码冷却机制防止频繁请求 + +### 4. 可维护性 + +- **模块化设计**: 每个验证规则独立成函数 +- **统一的错误处理**: 使用统一的错误显示机制 +- **可配置的规则**: 验证规则可以根据需求调整 + +## 更新日志 + +- **v1.0.0** (2025-12-24): 初始版本,包含完整的表单验证规范 \ No newline at end of file diff --git a/docs/auth/testing_guide.md b/docs/auth/testing_guide.md new file mode 100644 index 0000000..e7fb94d --- /dev/null +++ b/docs/auth/testing_guide.md @@ -0,0 +1,283 @@ +# 表单验证测试指南 + +## 测试概述 + +本文档提供了完整的表单验证功能测试步骤,确保所有验证规则和用户交互都能正常工作。 + +## 测试环境准备 + +### 1. 启动后端服务 +```bash +# 确保后端服务运行在 https://whaletownend.xinghangee.icu +# 或者本地开发环境: http://localhost:3000 +npm start +# 或者你的后端启动命令 +``` + +### 2. 打开Godot项目 +- 启动Godot编辑器 +- 打开鲸鱼镇项目 +- 运行主场景 + +## 登录表单测试 + +### 测试用例1: 必填项验证 + +#### 步骤 +1. 点击"注册居民身份"进入登录界面 +2. 不输入任何内容,直接点击用户名输入框外的区域 +3. 不输入任何内容,直接点击密码输入框外的区域 +4. 点击"进入小镇"或"密码登录"按钮 + +#### 预期结果 +- 用户名输入框下方显示红色错误提示:"用户名不能为空" +- 密码输入框下方显示红色错误提示:"密码不能为空" +- 按钮点击无效,不会提交表单 + +### 测试用例2: 实时反馈 + +#### 步骤 +1. 触发上述错误提示后 +2. 在用户名输入框中输入任意字符 +3. 在密码输入框中输入任意字符 + +#### 预期结果 +- 开始输入时,对应的错误提示立即消失 +- 提供即时的视觉反馈 + +### 测试用例3: 正常登录 + +#### 步骤 +1. 输入有效的用户名:"testuser" +2. 输入任意密码:"password123" +3. 点击"进入小镇" + +#### 预期结果 +- 无错误提示显示 +- 成功进入游戏主界面 +- 显示用户名信息 + +## 注册表单测试 + +### 测试用例4: 用户名验证 + +#### 测试数据 +| 输入 | 预期结果 | 错误信息 | +|------|----------|----------| +| (空) | 失败 | "用户名不能为空" | +| "test@user" | 失败 | "用户名只能包含字母、数字和下划线" | +| "a".repeat(51) | 失败 | "用户名长度应为1-50字符" | +| "test_user123" | 成功 | 无错误提示 | + +#### 步骤 +1. 点击"注册居民身份" +2. 依次输入上述测试数据 +3. 每次输入后点击其他输入框(触发失焦验证) + +### 测试用例5: 邮箱验证 + +#### 测试数据 +| 输入 | 预期结果 | 错误信息 | +|------|----------|----------| +| (空) | 失败 | "邮箱不能为空" | +| "invalid-email" | 失败 | "请输入有效的邮箱地址" | +| "test@" | 失败 | "请输入有效的邮箱地址" | +| "test@example.com" | 成功 | 无错误提示 | + +#### 步骤 +1. 在邮箱输入框中依次输入测试数据 +2. 每次输入后点击其他输入框 + +### 测试用例6: 密码验证 + +#### 测试数据 +| 输入 | 预期结果 | 错误信息 | +|------|----------|----------| +| (空) | 失败 | "密码不能为空" | +| "1234567" | 失败 | "密码长度至少8位" | +| "12345678" | 失败 | "密码必须包含字母和数字" | +| "abcdefgh" | 失败 | "密码必须包含字母和数字" | +| "abc12345" | 成功 | 无错误提示 | + +#### 步骤 +1. 在密码输入框中依次输入测试数据 +2. 每次输入后点击其他输入框 + +### 测试用例7: 确认密码验证 + +#### 步骤 +1. 在密码框输入:"abc12345" +2. 在确认密码框输入:""(空) +3. 点击其他输入框 +4. 在确认密码框输入:"different123" +5. 点击其他输入框 +6. 在确认密码框输入:"abc12345" +7. 点击其他输入框 + +#### 预期结果 +- 步骤3: 显示"确认密码不能为空" +- 步骤5: 显示"两次输入的密码不一致" +- 步骤7: 无错误提示 + +### 测试用例8: 验证码功能 + +#### 步骤 +1. 不输入邮箱,直接点击"发送验证码" +2. 输入无效邮箱"invalid",点击"发送验证码" +3. 输入有效邮箱"test@example.com",点击"发送验证码" +4. 立即再次点击"发送验证码" +5. 在验证码输入框输入"12345"(5位) +6. 在验证码输入框输入"12345a"(包含字母) +7. 在验证码输入框输入"123456"(正确格式) + +#### 预期结果 +- 步骤1: 邮箱框显示"邮箱不能为空",聚焦到邮箱输入框 +- 步骤2: 邮箱框显示"请输入有效的邮箱地址" +- 步骤3: 显示"正在发送验证码...",按钮变为禁用状态 +- 步骤4: 显示冷却提示"请等待 XX 秒后再次发送" +- 步骤5: 验证码框显示"验证码必须是6位数字" +- 步骤6: 验证码框显示"验证码必须是6位数字" +- 步骤7: 无错误提示 + +### 测试用例9: 完整注册流程 + +#### 步骤 +1. 填写所有必填信息: + - 用户名: "testuser" + - 邮箱: "test@example.com" + - 密码: "password123" + - 确认密码: "password123" +2. 点击"发送验证码" +3. 查看控制台获取验证码 +4. 输入验证码 +5. 点击"注册"按钮 + +#### 预期结果 +- 所有验证通过,无错误提示 +- 显示"正在验证邮箱..." +- 显示"邮箱验证成功,正在注册..." +- 显示"注册成功!" +- 自动返回登录界面 +- 用户名自动填入登录表单 + +## 边界情况测试 + +### 测试用例10: 网络异常 + +#### 步骤 +1. 关闭后端服务 +2. 尝试发送验证码 +3. 尝试注册 + +#### 预期结果 +- 显示"网络连接失败,请检查服务器是否启动" +- 按钮重新启用 + +### 测试用例11: 快速操作 + +#### 步骤 +1. 快速连续点击"发送验证码"按钮 +2. 快速连续点击"注册"按钮 + +#### 预期结果 +- 按钮在请求期间被禁用 +- 防止重复提交 + +### 测试用例12: 表单切换 + +#### 步骤 +1. 在注册表单中输入信息并触发错误提示 +2. 点击"返回登录" +3. 再次点击"注册居民身份" + +#### 预期结果 +- 切换到登录界面时,注册表单状态保持 +- 再次进入注册界面时,表单被清空,错误提示被隐藏 + +## 性能测试 + +### 测试用例13: 响应速度 + +#### 步骤 +1. 快速在各个输入框之间切换 +2. 快速输入和删除文本 +3. 观察错误提示的显示和隐藏速度 + +#### 预期结果 +- 验证响应迅速,无明显延迟 +- UI更新流畅,无卡顿现象 + +## 兼容性测试 + +### 测试用例14: 不同输入法 + +#### 步骤 +1. 使用中文输入法输入用户名 +2. 使用英文输入法输入用户名 +3. 输入特殊字符 + +#### 预期结果 +- 中文字符被正确验证为无效格式 +- 英文字符正常通过验证 +- 特殊字符按规则验证 + +## 测试检查清单 + +### 功能完整性 +- [ ] 所有必填项都有红色星号标识 +- [ ] 失焦验证正常工作 +- [ ] 实时输入反馈正常 +- [ ] 错误提示准确显示 +- [ ] 验证码发送和验证功能正常 +- [ ] 注册流程完整可用 + +### 用户体验 +- [ ] 错误提示清晰易懂 +- [ ] 按钮状态管理正确 +- [ ] 表单切换流畅 +- [ ] 成功反馈及时 + +### 错误处理 +- [ ] 网络异常处理正确 +- [ ] 服务器错误处理正确 +- [ ] 边界情况处理正确 + +### 性能表现 +- [ ] 验证响应速度快 +- [ ] UI更新流畅 +- [ ] 内存使用正常 + +## 测试报告模板 + +### 测试环境 +- Godot版本: +- 操作系统: +- 后端服务状态: +- 测试时间: + +### 测试结果 +| 测试用例 | 状态 | 备注 | +|----------|------|------| +| 登录表单验证 | ✅/❌ | | +| 注册表单验证 | ✅/❌ | | +| 验证码功能 | ✅/❌ | | +| 网络异常处理 | ✅/❌ | | +| 完整注册流程 | ✅/❌ | | + +### 发现的问题 +1. 问题描述 + - 重现步骤 + - 预期结果 + - 实际结果 + - 严重程度 + +### 改进建议 +1. 功能改进 +2. 用户体验优化 +3. 性能优化 + +--- + +**测试完成日期**: ___________ +**测试人员**: ___________ +**测试版本**: v1.0.0 \ No newline at end of file diff --git a/docs/code_comment_guide.md b/docs/code_comment_guide.md new file mode 100644 index 0000000..fb37615 --- /dev/null +++ b/docs/code_comment_guide.md @@ -0,0 +1,433 @@ +# GDScript 代码注释规范 + +本文档详细说明了 whaleTown 项目中 GDScript 代码的注释规范,旨在提高代码可读性和维护性。 + +## 目录 + +- [基本原则](#基本原则) +- [文件头注释](#文件头注释) +- [类和函数注释](#类和函数注释) +- [变量注释](#变量注释) +- [行内注释](#行内注释) +- [特殊注释标记](#特殊注释标记) +- [AI辅助注释指南](#ai辅助注释指南) + +## 基本原则 + +### 注释的目的 +- **解释为什么**,而不是解释做什么 +- **提供上下文**,帮助理解代码意图 +- **记录重要决策**和设计考虑 +- **警告潜在问题**和注意事项 + +### 注释质量标准 +- 简洁明了,避免冗余 +- 使用中文,保持一致性 +- 及时更新,与代码同步 +- 避免显而易见的注释 + +## 文件头注释 + +每个 GDScript 文件都应该包含文件头注释,说明文件的用途和基本信息。 + +### 标准格式 + +```gdscript +# ============================================================================ +# 文件名: PlayerController.gd +# 作用: 玩家角色控制器,处理玩家输入和移动逻辑 +# +# 主要功能: +# - 处理键盘和手柄输入 +# - 控制角色移动和跳跃 +# - 管理角色状态切换 +# - 处理碰撞检测 +# +# 依赖: MovementComponent, AnimationComponent +# 作者: [开发者名称] +# 创建时间: 2024-12-24 +# ============================================================================ + +extends CharacterBody2D +``` + +### 管理器类文件头 + +```gdscript +# ============================================================================ +# 游戏管理器 - GameManager.gd +# +# 全局单例管理器,负责游戏状态管理和生命周期控制 +# +# 核心职责: +# - 游戏状态切换 (加载、认证、游戏中、暂停等) +# - 用户信息管理 +# - 全局配置访问 +# - 系统初始化和清理 +# +# 使用方式: +# GameManager.change_state(GameManager.GameState.IN_GAME) +# GameManager.set_current_user("player123") +# +# 注意事项: +# - 作为自动加载单例,全局可访问 +# - 状态变更会触发 game_state_changed 信号 +# ============================================================================ + +extends Node +``` +## 类和函数注释 + +### 类注释 + +```gdscript +# 玩家数据类 +# +# 存储和管理玩家的基本属性和状态信息 +# 支持数据序列化和反序列化,用于存档系统 +class_name PlayerData + +# 武器组件类 +# +# 为角色提供武器功能,包括攻击、换弹、特殊技能等 +# 可以挂载到任何具有攻击能力的角色上 +# +# 使用示例: +# var weapon = WeaponComponent.new() +# weapon.setup_weapon("sword", 50) +# add_child(weapon) +class_name WeaponComponent +``` + +### 函数注释格式 + +```gdscript +# 处理玩家输入并更新移动状态 +# +# 参数: +# delta: float - 帧时间间隔 +# +# 返回值: 无 +# +# 注意事项: +# - 需要在 _physics_process 中调用 +# - 会自动处理重力和碰撞 +func handle_movement(delta: float) -> void: + +# 验证用户输入的邮箱格式 +# +# 参数: +# email: String - 待验证的邮箱地址 +# +# 返回值: +# Dictionary - 包含验证结果和错误信息 +# { +# "valid": bool, # 是否有效 +# "message": String # 错误信息(如果无效) +# } +# +# 使用示例: +# var result = validate_email("test@example.com") +# if result.valid: +# print("邮箱格式正确") +static func validate_email(email: String) -> Dictionary: +``` +## 变量注释 + +### 成员变量注释 + +```gdscript +# 玩家基础属性 +@export var max_health: int = 100 # 最大生命值 +@export var move_speed: float = 200.0 # 移动速度 (像素/秒) +@export var jump_force: float = -400.0 # 跳跃力度 (负值向上) + +# 状态管理 +var current_health: int # 当前生命值 +var is_grounded: bool = false # 是否在地面上 +var can_double_jump: bool = true # 是否可以二段跳 + +# 节点引用 - 在 _ready() 中初始化 +@onready var sprite: Sprite2D = $Sprite2D +@onready var collision: CollisionShape2D = $CollisionShape2D +@onready var animation_player: AnimationPlayer = $AnimationPlayer + +# 私有变量 - 内部使用 +var _velocity: Vector2 = Vector2.ZERO # 当前速度向量 +var _last_direction: int = 1 # 最后面向方向 (1=右, -1=左) +``` + +### 常量注释 + +```gdscript +# 游戏配置常量 +const GRAVITY: float = 980.0 # 重力加速度 (像素/秒²) +const MAX_FALL_SPEED: float = 1000.0 # 最大下落速度 +const COYOTE_TIME: float = 0.1 # 土狼时间 (离开平台后仍可跳跃的时间) + +# 输入动作名称 +const ACTION_MOVE_LEFT: String = "move_left" +const ACTION_MOVE_RIGHT: String = "move_right" +const ACTION_JUMP: String = "jump" + +# 动画名称常量 +const ANIM_IDLE: String = "idle" +const ANIM_WALK: String = "walk" +const ANIM_JUMP: String = "jump" +const ANIM_FALL: String = "fall" +``` + +## 行内注释 + +### 复杂逻辑注释 + +```gdscript +func update_movement(delta: float): + # 处理水平移动输入 + var input_direction = Input.get_axis("move_left", "move_right") + + if input_direction != 0: + velocity.x = input_direction * move_speed + _last_direction = sign(input_direction) # 记录面向方向 + else: + # 应用摩擦力,逐渐停止 + velocity.x = move_toward(velocity.x, 0, friction * delta) + + # 应用重力 (只在空中时) + if not is_on_floor(): + velocity.y += gravity * delta + velocity.y = min(velocity.y, max_fall_speed) # 限制最大下落速度 + + # 处理跳跃输入 + if Input.is_action_just_pressed("jump"): + if is_on_floor(): + velocity.y = jump_force # 普通跳跃 + elif can_double_jump: + velocity.y = jump_force * 0.8 # 二段跳 (力度稍小) + can_double_jump = false + + # 重置二段跳能力 + if is_on_floor() and not can_double_jump: + can_double_jump = true +``` + +### 临时解决方案注释 + +```gdscript +func handle_collision(body: Node2D): + # TODO: 重构碰撞处理逻辑,当前实现过于复杂 + # FIXME: 在某些情况下会出现重复碰撞检测 + # HACK: 临时解决方案,等待 Godot 4.6 修复相关 bug + + if body.has_method("take_damage"): + # NOTE: 伤害计算需要考虑护甲和抗性 + var damage = calculate_damage(base_damage) + body.take_damage(damage) +``` +## 特殊注释标记 + +使用标准化的标记来标识不同类型的注释: + +### 标记类型 + +```gdscript +# TODO: 待实现的功能 +# TODO: 添加音效播放功能 +# TODO: 实现存档系统 + +# FIXME: 需要修复的问题 +# FIXME: 内存泄漏问题,需要及时释放资源 +# FIXME: 在低帧率下移动不流畅 + +# HACK: 临时解决方案 +# HACK: 绕过 Godot 引擎的已知 bug +# HACK: 临时方案,等待更好的实现 + +# NOTE: 重要说明 +# NOTE: 此函数会修改全局状态,谨慎使用 +# NOTE: 性能敏感代码,避免频繁调用 + +# WARNING: 警告信息 +# WARNING: 不要在 _ready() 之前调用此函数 +# WARNING: 此操作不可逆,请确认后执行 + +# OPTIMIZE: 性能优化点 +# OPTIMIZE: 可以使用对象池优化内存分配 +# OPTIMIZE: 考虑使用缓存提高查询效率 +``` + +### 版本和兼容性标记 + +```gdscript +# @since 1.0.0 - 添加基础移动功能 +# @deprecated 使用 new_movement_system() 替代 +# @godot_version 4.5+ - 需要 Godot 4.5 或更高版本 +# @platform_specific Windows/Linux - 仅在桌面平台可用 +``` + +## AI辅助注释指南 + +### 为AI提供的注释模板 + +当需要AI帮助补充注释时,请使用以下模板: + +```gdscript +# [AI_COMMENT_REQUEST] +# 请为以下函数添加详细注释,包括: +# - 功能描述 +# - 参数说明 +# - 返回值说明 +# - 使用示例 +# - 注意事项 +func complex_function(param1: String, param2: int) -> Dictionary: + # 复杂的业务逻辑... + pass +``` + +### AI注释补充规则 + +1. **保持一致性** - 遵循项目现有的注释风格 +2. **关注业务逻辑** - 重点解释业务含义,而非语法 +3. **提供上下文** - 说明函数在整个系统中的作用 +4. **标注复杂度** - 对复杂算法提供额外说明 + +### 示例:AI补充前后对比 + +**补充前:** +```gdscript +func calculate_damage(base_damage: int, armor: int, resistance: float) -> int: + var final_damage = base_damage + final_damage -= armor + final_damage *= (1.0 - resistance) + return max(1, final_damage) +``` + +**AI补充后:** +```gdscript +# 计算最终伤害值 +# +# 根据基础伤害、护甲值和抗性计算实际造成的伤害 +# 伤害计算公式: (基础伤害 - 护甲) × (1 - 抗性) +# +# 参数: +# base_damage: int - 基础伤害值 +# armor: int - 目标护甲值 (减少固定伤害) +# resistance: float - 目标抗性 (0.0-1.0, 减少百分比伤害) +# +# 返回值: +# int - 最终伤害值 (最小为1,确保至少造成1点伤害) +# +# 使用示例: +# var damage = calculate_damage(100, 20, 0.3) # 结果: 56 +# +# 注意事项: +# - 护甲为负值时会增加伤害 +# - 抗性超过1.0时可能导致负伤害,但会被限制为1 +func calculate_damage(base_damage: int, armor: int, resistance: float) -> int: + var final_damage = base_damage + final_damage -= armor # 减去护甲值 + final_damage *= (1.0 - resistance) # 应用抗性 + return max(1, final_damage) # 确保最小伤害为1 +``` +## 注释最佳实践 + +### 什么时候需要注释 + +**必须添加注释的情况:** +- 复杂的算法和业务逻辑 +- 非显而易见的设计决策 +- 临时解决方案和已知问题 +- 公共API和接口函数 +- 性能敏感的代码段 +- 平台特定或版本特定的代码 + +**不需要注释的情况:** +- 显而易见的代码 (`var count = 0 # 计数器` ❌) +- 重复函数名的注释 (`func get_name() -> String: # 获取名称` ❌) +- 过时或错误的注释 + +### 注释维护原则 + +```gdscript +# ✅ 好的注释 - 解释为什么这样做 +# 使用二分查找提高大数组的查询效率 +# 当数组长度超过100时,线性查找性能会显著下降 +func binary_search(array: Array, target: Variant) -> int: + +# ❌ 坏的注释 - 重复代码内容 +# 遍历数组查找目标值 +func linear_search(array: Array, target: Variant) -> int: +``` + +### 团队协作注释 + +```gdscript +# 多人协作时的注释规范 +class_name NetworkManager + +# @author: 张三 - 网络连接模块 +# @author: 李四 - 数据同步模块 +# @reviewer: 王五 - 代码审查 +# @last_modified: 2024-12-24 + +# 网络连接状态枚举 +# +# 定义了客户端与服务器的连接状态 +# 状态转换: DISCONNECTED -> CONNECTING -> CONNECTED -> DISCONNECTED +enum ConnectionState { + DISCONNECTED, # 未连接 + CONNECTING, # 连接中 + CONNECTED, # 已连接 + RECONNECTING # 重连中 - @added by 李四 2024-12-20 +} +``` + +## 注释检查清单 + +在提交代码前,请检查以下项目: + +### 文件级别检查 +- [ ] 文件头注释完整(文件名、作用、主要功能) +- [ ] 依赖关系说明清楚 +- [ ] 作者和创建时间已填写 + +### 函数级别检查 +- [ ] 公共函数有完整的参数和返回值说明 +- [ ] 复杂函数有使用示例 +- [ ] 特殊情况和注意事项已标注 + +### 代码级别检查 +- [ ] 复杂逻辑有行内注释说明 +- [ ] 魔法数字有常量定义和注释 +- [ ] TODO/FIXME 标记有明确的处理计划 + +### 质量检查 +- [ ] 注释内容准确,与代码一致 +- [ ] 中文表达清晰,无错别字 +- [ ] 注释格式符合项目规范 + +## 注释工具和插件 + +### Godot编辑器设置 +``` +# 在 Godot 编辑器中设置注释快捷键 +# 编辑器设置 -> 快捷键 -> 注释/取消注释: Ctrl+/ +``` + +### 推荐的注释插件 +- **GDScript Language Server** - 提供注释语法高亮 +- **Code Formatter** - 自动格式化注释 +- **Documentation Generator** - 自动生成API文档 + +--- + +## 总结 + +良好的注释是代码质量的重要组成部分。遵循本规范可以: + +1. **提高代码可读性** - 帮助团队成员快速理解代码 +2. **降低维护成本** - 减少后期修改和调试时间 +3. **促进知识传承** - 保留设计思路和业务逻辑 +4. **支持AI辅助开发** - 为AI提供更好的上下文信息 + +记住:**好的注释解释为什么,而不是做什么。** \ No newline at end of file diff --git a/docs/naming_convention.md b/docs/naming_convention.md index 2501812..558c113 100644 --- a/docs/naming_convention.md +++ b/docs/naming_convention.md @@ -404,6 +404,37 @@ FONT_MAIN.ttf - 全部使用小写字母 - 同系列资源使用数字后缀,如 `tile_01.png`、`tile_02.png` +### 扩展资源类型 + +``` +✅ 正确 +# 材质资源 +material_metal.tres # 金属材质 +material_wood.tres # 木材材质 +material_water.tres # 水材质 + +# 着色器资源 +shader_water.gdshader # 水着色器 +shader_fire.gdshader # 火焰着色器 +shader_outline.gdshader # 轮廓着色器 + +# 特效资源 +fx_explosion.png # 爆炸特效 +fx_magic_circle.png # 魔法阵特效 +fx_damage_numbers.png # 伤害数字特效 + +# 环境资源 +obj_tree.png # 树木对象 +obj_rock.png # 岩石对象 +tile_grass_01.png # 草地瓦片 +tile_stone_01.png # 石头瓦片 + +❌ 错误 +MetalMaterial.tres # 不使用大驼峰 +material-wood.tres # 不使用连字符 +SHADER_WATER.gdshader # 不使用全大写 +``` + --- ## 目录结构 @@ -421,11 +452,33 @@ assets/ # 资源目录 sounds/ # 音效 music/ # 音乐 fonts/ # 字体 + materials/ # 材质 + shaders/ # 着色器 data/ # 数据目录 levels/ # 关卡数据 configs/ # 配置文件 + dialogues/ # 对话数据 + localization/ # 本地化数据 +core/ # 核心系统目录 + managers/ # 管理器 + systems/ # 系统组件 + utils/ # 工具类 + components/ # 通用组件 + interfaces/ # 接口定义 +module/ # 模块目录 + UI/ # UI模块 + Character/ # 角色模块 + Inventory/ # 背包模块 + Combat/ # 战斗模块 + Dialogue/ # 对话模块 addons/ # 插件目录 tests/ # 测试目录 + unit/ # 单元测试 + integration/ # 集成测试 + ui/ # UI测试 + performance/ # 性能测试 +docs/ # 文档目录 + auth/ # 认证相关文档 ❌ 错误 Scenes/ # 不使用大写 diff --git a/docs/project_structure.md b/docs/project_structure.md new file mode 100644 index 0000000..461ee0b --- /dev/null +++ b/docs/project_structure.md @@ -0,0 +1,505 @@ +# Godot 项目结构说明 + +本文档详细说明了 whaleTown 项目的文件结构设计,采用"场景+通用工具+其他"的架构模式,确保每个场景清晰独立且高度解耦。 + +## 设计理念 + +### 核心原则 +- **场景独立性**:每个场景都是独立的功能模块,可以单独开发和测试 +- **高度解耦**:场景之间通过事件系统和管理器进行通信,避免直接依赖 +- **组件复用**:可复用的组件放在通用模块中,提高开发效率 +- **资源管理**:统一的资源管理和命名规范,便于维护 + +## 项目架构概览 + +``` +whaleTown/ +├── 🎬 scenes/ # 场景层:独立的游戏场景 +├── 🔧 core/ # 核心层:通用工具和系统 +├── 🎨 assets/ # 资源层:静态资源文件 +├── 📊 data/ # 数据层:配置和游戏数据 +├── 📝 scripts/ # 脚本层:业务逻辑代码 +├── 🧩 module/ # 模块层:可复用组件 +├── 🧪 tests/ # 测试层:单元测试和集成测试 +└── 📚 docs/ # 文档层:项目文档 +``` + +## 1. 场景层 (scenes/) + +场景层包含所有独立的游戏场景,每个场景都是完整的功能模块。 + +### 1.1 场景分类 + +#### 主要场景 (Main Scenes) +``` +scenes/ +├── main_scene.tscn # 主场景:游戏入口 +├── auth_scene.tscn # 认证场景:登录注册 +├── menu_scene.tscn # 菜单场景:主菜单界面 +├── game_scene.tscn # 游戏场景:主要游戏玩法 +├── battle_scene.tscn # 战斗场景:战斗系统 +├── inventory_scene.tscn # 背包场景:物品管理 +├── shop_scene.tscn # 商店场景:购买系统 +└── settings_scene.tscn # 设置场景:游戏设置 +``` + +#### 预制体场景 (Prefabs) +``` +scenes/prefabs/ +├── ui/ +│ ├── dialog_prefab.tscn # 对话框预制体 +│ ├── button_prefab.tscn # 按钮预制体 +│ ├── health_bar_prefab.tscn # 血条预制体 +│ └── notification_prefab.tscn # 通知预制体 +├── characters/ +│ ├── player_prefab.tscn # 玩家角色预制体 +│ ├── npc_prefab.tscn # NPC预制体 +│ └── enemy_prefab.tscn # 敌人预制体 +├── items/ +│ ├── weapon_prefab.tscn # 武器预制体 +│ ├── consumable_prefab.tscn # 消耗品预制体 +│ └── collectible_prefab.tscn # 收集品预制体 +└── effects/ + ├── explosion_prefab.tscn # 爆炸特效预制体 + ├── particle_prefab.tscn # 粒子特效预制体 + └── damage_text_prefab.tscn # 伤害数字预制体 +``` + +### 1.2 场景设计原则 + +- **单一职责**:每个场景只负责一个主要功能 +- **独立运行**:场景可以独立启动和测试 +- **标准接口**:场景间通过标准化接口通信 +- **资源隔离**:场景相关资源放在对应子目录 +## 2. 核心层 (core/) + +核心层提供通用的工具类、管理器和系统组件,为整个项目提供基础服务。 + +### 2.1 核心系统结构 + +``` +core/ +├── managers/ # 管理器系统 +│ ├── GameManager.gd # 游戏管理器:全局游戏状态 +│ ├── SceneManager.gd # 场景管理器:场景切换 +│ ├── AudioManager.gd # 音频管理器:音效音乐 +│ ├── InputManager.gd # 输入管理器:输入处理 +│ ├── SaveManager.gd # 存档管理器:数据存储 +│ ├── UIManager.gd # UI管理器:界面管理 +│ └── NetworkManager.gd # 网络管理器:网络通信 +├── systems/ # 系统组件 +│ ├── EventSystem.gd # 事件系统:全局事件 +│ ├── StateMachine.gd # 状态机系统 +│ ├── ObjectPool.gd # 对象池系统 +│ ├── ResourceLoader.gd # 资源加载系统 +│ └── LocalizationSystem.gd # 本地化系统 +├── utils/ # 工具类 +│ ├── MathUtils.gd # 数学工具 +│ ├── StringUtils.gd # 字符串工具 +│ ├── FileUtils.gd # 文件工具 +│ ├── TimeUtils.gd # 时间工具 +│ └── DebugUtils.gd # 调试工具 +├── components/ # 通用组件 +│ ├── HealthComponent.gd # 生命值组件 +│ ├── MovementComponent.gd # 移动组件 +│ ├── AnimationComponent.gd # 动画组件 +│ └── CollisionComponent.gd # 碰撞组件 +└── interfaces/ # 接口定义 + ├── IInteractable.gd # 可交互接口 + ├── IDamageable.gd # 可受伤接口 + ├── ICollectable.gd # 可收集接口 + └── ISaveable.gd # 可存储接口 +``` + +### 2.2 核心系统职责 + +#### 管理器 (Managers) +- **单例模式**:全局唯一实例 +- **生命周期管理**:负责系统初始化和清理 +- **状态维护**:维护全局状态信息 +- **服务提供**:为其他模块提供服务 + +#### 系统组件 (Systems) +- **功能封装**:封装特定功能逻辑 +- **可插拔设计**:可以独立启用或禁用 +- **事件驱动**:通过事件系统通信 +- **性能优化**:提供高效的实现方案 +## 3. 模块层 (module/) + +模块层包含可复用的功能模块,这些模块可以在不同场景中重复使用。 + +### 3.1 模块分类 + +``` +module/ +├── UI/ # UI模块 +│ ├── components/ # UI组件 +│ │ ├── Button/ # 按钮组件 +│ │ │ ├── CustomButton.gd +│ │ │ └── custom_button.tscn +│ │ ├── Dialog/ # 对话框组件 +│ │ │ ├── DialogBox.gd +│ │ │ └── dialog_box.tscn +│ │ ├── ProgressBar/ # 进度条组件 +│ │ │ ├── CustomProgressBar.gd +│ │ │ └── custom_progress_bar.tscn +│ │ └── InputField/ # 输入框组件 +│ │ ├── CustomInputField.gd +│ │ └── custom_input_field.tscn +│ ├── layouts/ # 布局组件 +│ │ ├── GridLayout.gd +│ │ ├── FlexLayout.gd +│ │ └── ResponsiveLayout.gd +│ └── animations/ # UI动画 +│ ├── FadeTransition.gd +│ ├── SlideTransition.gd +│ └── ScaleTransition.gd +├── Character/ # 角色模块 +│ ├── Player/ # 玩家角色 +│ │ ├── PlayerController.gd +│ │ ├── PlayerStats.gd +│ │ └── PlayerAnimator.gd +│ ├── NPC/ # NPC角色 +│ │ ├── NPCController.gd +│ │ ├── NPCDialogue.gd +│ │ └── NPCBehavior.gd +│ └── Enemy/ # 敌人角色 +│ ├── EnemyAI.gd +│ ├── EnemyStats.gd +│ └── EnemyBehavior.gd +├── Inventory/ # 背包模块 +│ ├── InventorySystem.gd +│ ├── Item.gd +│ ├── ItemSlot.gd +│ └── ItemDatabase.gd +├── Combat/ # 战斗模块 +│ ├── CombatSystem.gd +│ ├── Weapon.gd +│ ├── Skill.gd +│ └── DamageCalculator.gd +└── Dialogue/ # 对话模块 + ├── DialogueSystem.gd + ├── DialogueNode.gd + ├── DialogueParser.gd + └── DialogueUI.gd +``` + +### 3.2 模块设计原则 + +- **高内聚**:模块内部功能紧密相关 +- **低耦合**:模块间依赖最小化 +- **可配置**:通过配置文件自定义行为 +- **可扩展**:支持功能扩展和定制 +## 4. 资源层 (assets/) + +资源层统一管理所有静态资源文件,采用分类存储和标准化命名。 + +### 4.1 资源目录结构 + +``` +assets/ +├── sprites/ # 精灵图资源 +│ ├── characters/ # 角色精灵 +│ │ ├── player/ +│ │ │ ├── sprite_player_idle.png +│ │ │ ├── sprite_player_walk.png +│ │ │ └── sprite_player_attack.png +│ │ ├── enemies/ +│ │ │ ├── sprite_enemy_goblin_idle.png +│ │ │ └── sprite_enemy_orc_walk.png +│ │ └── npcs/ +│ │ ├── sprite_npc_merchant.png +│ │ └── sprite_npc_guard.png +│ ├── ui/ # UI精灵 +│ │ ├── buttons/ +│ │ │ ├── ui_button_normal.png +│ │ │ ├── ui_button_hover.png +│ │ │ └── ui_button_pressed.png +│ │ ├── icons/ +│ │ │ ├── icon_sword.png +│ │ │ ├── icon_shield.png +│ │ │ └── icon_potion.png +│ │ └── panels/ +│ │ ├── ui_panel_main.png +│ │ └── ui_panel_dialog.png +│ ├── environment/ # 环境精灵 +│ │ ├── backgrounds/ +│ │ │ ├── bg_forest.png +│ │ │ └── bg_dungeon.png +│ │ ├── tiles/ +│ │ │ ├── tile_grass_01.png +│ │ │ └── tile_stone_01.png +│ │ └── objects/ +│ │ ├── obj_tree.png +│ │ └── obj_rock.png +│ └── effects/ # 特效精灵 +│ ├── fx_explosion.png +│ ├── fx_magic_circle.png +│ └── fx_damage_numbers.png +├── audio/ # 音频资源 +│ ├── music/ # 背景音乐 +│ │ ├── music_main_menu.ogg +│ │ ├── music_battle.ogg +│ │ └── music_peaceful.ogg +│ ├── sounds/ # 音效 +│ │ ├── ui/ +│ │ │ ├── sound_button_click.wav +│ │ │ └── sound_menu_open.wav +│ │ ├── combat/ +│ │ │ ├── sound_sword_hit.wav +│ │ │ └── sound_explosion.wav +│ │ └── ambient/ +│ │ ├── sound_footsteps.wav +│ │ └── sound_wind.wav +│ └── voice/ # 语音 +│ ├── voice_npc_greeting.wav +│ └── voice_player_hurt.wav +├── fonts/ # 字体资源 +│ ├── font_main.ttf # 主要字体 +│ ├── font_title.ttf # 标题字体 +│ └── font_ui.ttf # UI字体 +├── materials/ # 材质资源 +│ ├── material_metal.tres +│ ├── material_wood.tres +│ └── material_water.tres +└── shaders/ # 着色器资源 + ├── shader_water.gdshader + ├── shader_fire.gdshader + └── shader_outline.gdshader +``` + +### 4.2 资源命名规范 + +#### 图片资源命名 +- **精灵图**:`sprite_[类别]_[名称]_[状态].png` + - 示例:`sprite_player_idle.png`、`sprite_enemy_goblin_walk.png` +- **UI图片**:`ui_[类型]_[名称]_[状态].png` + - 示例:`ui_button_normal.png`、`ui_panel_main.png` +- **图标**:`icon_[名称].png` + - 示例:`icon_sword.png`、`icon_health.png` +- **背景**:`bg_[场景名称].png` + - 示例:`bg_forest.png`、`bg_dungeon.png` +- **瓦片**:`tile_[材质]_[编号].png` + - 示例:`tile_grass_01.png`、`tile_stone_02.png` + +#### 音频资源命名 +- **音乐**:`music_[场景/情境].ogg` + - 示例:`music_battle.ogg`、`music_peaceful.ogg` +- **音效**:`sound_[动作/事件].wav` + - 示例:`sound_jump.wav`、`sound_explosion.wav` +- **语音**:`voice_[角色]_[内容].wav` + - 示例:`voice_npc_greeting.wav`、`voice_player_hurt.wav` + +#### 其他资源命名 +- **字体**:`font_[用途].ttf` + - 示例:`font_main.ttf`、`font_title.ttf` +- **材质**:`material_[材质名].tres` + - 示例:`material_metal.tres`、`material_wood.tres` +- **着色器**:`shader_[效果名].gdshader` + - 示例:`shader_water.gdshader`、`shader_fire.gdshader` +## 5. 脚本层 (scripts/) + +脚本层包含所有业务逻辑代码,按功能模块组织。 + +### 5.1 脚本目录结构 + +``` +scripts/ +├── scenes/ # 场景脚本 +│ ├── MainScene.gd # 主场景脚本 +│ ├── AuthScene.gd # 认证场景脚本 +│ ├── GameScene.gd # 游戏场景脚本 +│ └── BattleScene.gd # 战斗场景脚本 +├── ui/ # UI脚本 +│ ├── MainMenu.gd # 主菜单脚本 +│ ├── SettingsPanel.gd # 设置面板脚本 +│ ├── InventoryUI.gd # 背包界面脚本 +│ └── DialogueUI.gd # 对话界面脚本 +├── characters/ # 角色脚本 +│ ├── PlayerController.gd # 玩家控制器 +│ ├── EnemyAI.gd # 敌人AI +│ └── NPCBehavior.gd # NPC行为 +├── gameplay/ # 游戏玩法脚本 +│ ├── CombatSystem.gd # 战斗系统 +│ ├── QuestSystem.gd # 任务系统 +│ ├── InventorySystem.gd # 背包系统 +│ └── DialogueSystem.gd # 对话系统 +├── network/ # 网络脚本 +│ ├── NetworkClient.gd # 网络客户端 +│ ├── NetworkServer.gd # 网络服务器 +│ └── NetworkProtocol.gd # 网络协议 +└── data/ # 数据脚本 + ├── GameData.gd # 游戏数据 + ├── PlayerData.gd # 玩家数据 + ├── ItemData.gd # 物品数据 + └── ConfigData.gd # 配置数据 +``` + +## 6. 数据层 (data/) + +数据层存储游戏配置、关卡数据和其他静态数据文件。 + +### 6.1 数据目录结构 + +``` +data/ +├── configs/ # 配置文件 +│ ├── game_config.json # 游戏配置 +│ ├── audio_config.json # 音频配置 +│ ├── input_config.json # 输入配置 +│ └── graphics_config.json # 图形配置 +├── levels/ # 关卡数据 +│ ├── level_01.json # 第一关数据 +│ ├── level_02.json # 第二关数据 +│ └── level_boss.json # Boss关数据 +├── items/ # 物品数据 +│ ├── weapons.json # 武器数据 +│ ├── armor.json # 装备数据 +│ └── consumables.json # 消耗品数据 +├── characters/ # 角色数据 +│ ├── player_stats.json # 玩家属性 +│ ├── enemy_stats.json # 敌人属性 +│ └── npc_data.json # NPC数据 +├── dialogues/ # 对话数据 +│ ├── main_story.json # 主线对话 +│ ├── side_quests.json # 支线对话 +│ └── npc_dialogues.json # NPC对话 +└── localization/ # 本地化数据 + ├── en.json # 英文文本 + ├── zh_CN.json # 简体中文文本 + └── zh_TW.json # 繁体中文文本 +``` + +## 7. 测试层 (tests/) + +测试层包含单元测试、集成测试和功能测试。 + +### 7.1 测试目录结构 + +``` +tests/ +├── unit/ # 单元测试 +│ ├── test_player_controller.gd +│ ├── test_inventory_system.gd +│ └── test_combat_system.gd +├── integration/ # 集成测试 +│ ├── test_scene_transitions.gd +│ ├── test_save_load.gd +│ └── test_network_sync.gd +├── ui/ # UI测试 +│ ├── test_main_menu.gd +│ ├── test_inventory_ui.gd +│ └── test_dialogue_ui.gd +└── performance/ # 性能测试 + ├── test_memory_usage.gd + ├── test_frame_rate.gd + └── test_loading_times.gd +``` +## 8. 场景间通信机制 + +### 8.1 事件系统 + +使用全局事件系统实现场景间的松耦合通信: + +```gdscript +# 事件定义示例 +signal player_health_changed(new_health: int) +signal scene_transition_requested(scene_name: String) +signal item_collected(item_id: String) +signal quest_completed(quest_id: String) + +# 事件发送 +EventSystem.emit_signal("player_health_changed", 80) + +# 事件监听 +EventSystem.connect("player_health_changed", _on_player_health_changed) +``` + +### 8.2 管理器模式 + +通过单例管理器实现全局状态管理: + +```gdscript +# 场景切换 +SceneManager.change_scene("battle_scene") + +# 音频播放 +AudioManager.play_sound("sound_button_click") + +# 数据保存 +SaveManager.save_game_data(player_data) +``` + +## 9. 开发工作流 + +### 9.1 新场景开发流程 + +1. **创建场景文件**:在 `scenes/` 目录创建 `.tscn` 文件 +2. **编写场景脚本**:在 `scripts/scenes/` 创建对应脚本 +3. **添加UI组件**:使用 `module/UI/` 中的可复用组件 +4. **配置场景数据**:在 `data/` 目录添加相关配置 +5. **编写测试用例**:在 `tests/` 目录添加测试 +6. **更新文档**:更新相关文档说明 + +### 9.2 新功能模块开发流程 + +1. **设计模块接口**:定义模块的公共接口 +2. **实现核心逻辑**:在 `module/` 目录实现功能 +3. **添加管理器支持**:在 `core/managers/` 添加管理器 +4. **创建测试场景**:创建独立的测试场景 +5. **集成到主项目**:将模块集成到现有场景 +6. **性能优化**:进行性能测试和优化 + +## 10. 最佳实践 + +### 10.1 代码组织 + +- **单一职责**:每个类只负责一个功能 +- **依赖注入**:通过构造函数或属性注入依赖 +- **接口隔离**:使用接口定义模块间的契约 +- **配置外置**:将配置信息放在数据文件中 + +### 10.2 性能优化 + +- **对象池**:复用频繁创建的对象 +- **延迟加载**:按需加载资源和场景 +- **批量处理**:合并相似的操作 +- **内存管理**:及时释放不需要的资源 + +### 10.3 调试和测试 + +- **单元测试**:为核心逻辑编写单元测试 +- **集成测试**:测试模块间的交互 +- **性能监控**:监控内存和CPU使用情况 +- **日志记录**:记录关键操作和错误信息 + +## 11. 扩展指南 + +### 11.1 添加新场景 + +1. 在 `scenes/` 目录创建场景文件 +2. 在 `scripts/scenes/` 创建场景脚本 +3. 在 `SceneManager` 中注册新场景 +4. 添加场景切换逻辑 + +### 11.2 添加新模块 + +1. 在 `module/` 目录创建模块文件夹 +2. 实现模块的核心功能 +3. 创建模块管理器(如需要) +4. 编写模块文档和示例 + +### 11.3 添加新资源类型 + +1. 在 `assets/` 目录创建对应分类 +2. 更新命名规范文档 +3. 在资源加载器中添加支持 +4. 更新导入设置 + +--- + +## 总结 + +这个项目结构设计遵循了模块化、可扩展、易维护的原则。通过清晰的分层架构和标准化的命名规范,确保了项目的可读性和可维护性。每个开发者都应该遵循这个结构进行开发,以保持项目的一致性和质量。 + +如有任何疑问或建议,请参考相关文档或联系项目维护者。 \ No newline at end of file diff --git a/module/Character/.gitkeep b/module/Character/.gitkeep new file mode 100644 index 0000000..b2d0d88 --- /dev/null +++ b/module/Character/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 角色模块目录 \ No newline at end of file diff --git a/module/Combat/.gitkeep b/module/Combat/.gitkeep new file mode 100644 index 0000000..5f5d6c1 --- /dev/null +++ b/module/Combat/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 战斗模块目录 \ No newline at end of file diff --git a/module/Dialogue/.gitkeep b/module/Dialogue/.gitkeep new file mode 100644 index 0000000..0c50b0c --- /dev/null +++ b/module/Dialogue/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 对话模块目录 \ No newline at end of file diff --git a/module/Inventory/.gitkeep b/module/Inventory/.gitkeep new file mode 100644 index 0000000..dfdb180 --- /dev/null +++ b/module/Inventory/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 背包模块目录 \ No newline at end of file diff --git a/module/UI/animations/.gitkeep b/module/UI/animations/.gitkeep new file mode 100644 index 0000000..442951d --- /dev/null +++ b/module/UI/animations/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - UI动画目录 \ No newline at end of file diff --git a/module/UI/components/.gitkeep b/module/UI/components/.gitkeep new file mode 100644 index 0000000..1226e70 --- /dev/null +++ b/module/UI/components/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - UI组件目录 \ No newline at end of file diff --git a/module/UI/layouts/.gitkeep b/module/UI/layouts/.gitkeep new file mode 100644 index 0000000..bb708d7 --- /dev/null +++ b/module/UI/layouts/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - UI布局目录 \ No newline at end of file diff --git a/project.godot b/project.godot index 5645744..51655e5 100644 --- a/project.godot +++ b/project.godot @@ -11,5 +11,20 @@ config_version=5 [application] config/name="whaleTown" +run/main_scene="res://scenes/main_scene.tscn" config/features=PackedStringArray("4.5", "Forward Plus") config/icon="res://icon.svg" + +[autoload] + +GameManager="*res://core/managers/GameManager.gd" +SceneManager="*res://core/managers/SceneManager.gd" +EventSystem="*res://core/systems/EventSystem.gd" + +[display] + +window/size/viewport_width=1376 +window/size/viewport_height=768 +window/size/mode=2 +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" diff --git a/scenes/.gitkeep b/scenes/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/scenes/auth_scene.tscn b/scenes/auth_scene.tscn new file mode 100644 index 0000000..5383f92 --- /dev/null +++ b/scenes/auth_scene.tscn @@ -0,0 +1,523 @@ +[gd_scene load_steps=10 format=3 uid="uid://by7m8snb4xllf"] + +[ext_resource type="Texture2D" uid="uid://bx17oy8lvaca4" path="res://assets/ui/auth/bg_auth_scene.png" id="1_background"] +[ext_resource type="Texture2D" uid="uid://de4q4s1gxivtf" path="res://assets/ui/auth/login_frame_smart_transparent.png" id="2_frame"] +[ext_resource type="Script" uid="uid://nv8eitxieqtm" path="res://scripts/scenes/AuthScene.gd" id="3_script"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hover"] +bg_color = Color(0.3, 0.6, 0.9, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.2, 0.5, 0.8, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_normal"] +bg_color = Color(0.2, 0.5, 0.8, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.15, 0.4, 0.7, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pressed"] +bg_color = Color(0.4, 0.7, 1, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.3, 0.6, 0.9, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="Theme" id="Theme_main_button"] +Button/colors/font_color = Color(1, 1, 1, 1) +Button/colors/font_hover_color = Color(1, 1, 1, 1) +Button/colors/font_pressed_color = Color(1, 1, 1, 1) +Button/font_sizes/font_size = 18 +Button/styles/hover = SubResource("StyleBoxFlat_hover") +Button/styles/normal = SubResource("StyleBoxFlat_normal") +Button/styles/pressed = SubResource("StyleBoxFlat_pressed") + +[sub_resource type="Theme" id="Theme_button"] +Button/colors/font_color = Color(1, 1, 1, 1) +Button/colors/font_hover_color = Color(1, 1, 1, 1) +Button/colors/font_pressed_color = Color(1, 1, 1, 1) +Button/styles/hover = SubResource("StyleBoxFlat_hover") +Button/styles/normal = SubResource("StyleBoxFlat_normal") +Button/styles/pressed = SubResource("StyleBoxFlat_pressed") + +[node name="AuthScene" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("3_script") + +[node name="HTTPRequest" type="HTTPRequest" parent="."] + +[node name="BackgroundImage" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("1_background") +expand_mode = 1 +stretch_mode = 6 + +[node name="WhaleFrame" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -300.0 +offset_top = -300.0 +offset_right = 300.0 +offset_bottom = 300.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("2_frame") +expand_mode = 1 +stretch_mode = 5 + +[node name="CenterContainer" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -175.0 +offset_top = -184.0 +offset_right = 175.0 +offset_bottom = 236.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="LoginPanel" type="Panel" parent="CenterContainer"] +custom_minimum_size = Vector2(350, 400) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxEmpty_1") + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/LoginPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 30.0 +offset_top = 30.0 +offset_right = -30.0 +offset_bottom = -30.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="TitleLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_font_sizes/font_size = 24 +text = "Whaletown" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="SubtitleLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_font_sizes/font_size = 14 +text = "开始你的小镇之旅!" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 + +[node name="LoginForm" type="VBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="UsernameContainer" type="VBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm"] +layout_mode = 2 + +[node name="UsernameLabelContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer"] +layout_mode = 2 + +[node name="UsernameLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "用户名/手机/邮箱" + +[node name="RequiredStar" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="UsernameError" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "用户名不能为空" +horizontal_alignment = 2 + +[node name="UsernameInput" type="LineEdit" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "用户名/手机/邮箱" + +[node name="PasswordContainer" type="VBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm"] +layout_mode = 2 + +[node name="PasswordLabelContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer"] +layout_mode = 2 + +[node name="PasswordLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "密码" + +[node name="RequiredStar" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PasswordError" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "密码不能为空" +horizontal_alignment = 2 + +[node name="PasswordInput" type="LineEdit" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入密码" +secret = true + +[node name="CheckboxContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm"] +layout_mode = 2 + +[node name="RememberPassword" type="CheckBox" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/CheckboxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "记住密码" + +[node name="AutoLogin" type="CheckBox" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/CheckboxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "自动登录" + +[node name="HSeparator2" type="HSeparator" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 + +[node name="MainButton" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer"] +custom_minimum_size = Vector2(280, 50) +layout_mode = 2 +theme = SubResource("Theme_main_button") +text = "进入小镇" + +[node name="HSeparator3" type="HSeparator" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 + +[node name="ButtonContainer" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="LoginBtn" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer/ButtonContainer"] +custom_minimum_size = Vector2(100, 35) +layout_mode = 2 +theme = SubResource("Theme_button") +text = "密码登录" + +[node name="ToRegisterBtn" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer/ButtonContainer"] +custom_minimum_size = Vector2(100, 35) +layout_mode = 2 +theme = SubResource("Theme_button") +text = "验证码登录" + +[node name="BottomLinks" type="HBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="ForgotPassword" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer/BottomLinks"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "忘记密码?" +flat = true + +[node name="RegisterLink" type="Button" parent="CenterContainer/LoginPanel/VBoxContainer/BottomLinks"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "注册居民身份" +flat = true + +[node name="RegisterPanel" type="Panel" parent="CenterContainer"] +visible = false +custom_minimum_size = Vector2(400, 570) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxEmpty_1") + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 30.0 +offset_top = 75.0 +offset_right = -30.0 +offset_bottom = -72.0 +grow_horizontal = 2 +grow_vertical = 2 +alignment = 1 + +[node name="TitleLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_font_sizes/font_size = 20 +text = "注册新居民" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 + +[node name="RegisterForm" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +alignment = 1 + +[node name="UsernameContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="UsernameLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer"] +layout_mode = 2 + +[node name="UsernameLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "用户名" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="UsernameError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "用户名不能为空" +horizontal_alignment = 2 + +[node name="UsernameInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入用户名" + +[node name="EmailContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="EmailLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer"] +layout_mode = 2 + +[node name="EmailLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "邮箱" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="EmailError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "邮箱不能为空" +horizontal_alignment = 2 + +[node name="EmailInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入邮箱地址" + +[node name="PasswordContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="PasswordLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer"] +layout_mode = 2 + +[node name="PasswordLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "密码" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="PasswordError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "密码不能为空" +horizontal_alignment = 2 + +[node name="PasswordInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入密码(至少8位)" +secret = true + +[node name="ConfirmContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="ConfirmLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer"] +layout_mode = 2 + +[node name="ConfirmLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "确认密码" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="ConfirmError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "确认密码不能为空" +horizontal_alignment = 2 + +[node name="ConfirmInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请再次输入密码" +secret = true + +[node name="VerificationContainer" type="VBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm"] +layout_mode = 2 + +[node name="VerificationLabelContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer"] +layout_mode = 2 + +[node name="VerificationLabel" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "邮箱验证码" + +[node name="RequiredStar" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +text = " *" + +[node name="Spacer" type="Control" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="VerificationError" type="Label" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer"] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 12 +text = "验证码不能为空" +horizontal_alignment = 2 + +[node name="VerificationInputContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer"] +layout_mode = 2 + +[node name="VerificationInput" type="LineEdit" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +placeholder_text = "请输入6位验证码" +max_length = 6 + +[node name="SendCodeBtn" type="Button" parent="CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer"] +layout_mode = 2 +text = "发送验证码" + +[node name="HSeparator2" type="HSeparator" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 + +[node name="ButtonContainer" type="HBoxContainer" parent="CenterContainer/RegisterPanel/VBoxContainer"] +layout_mode = 2 +alignment = 1 + +[node name="RegisterBtn" type="Button" parent="CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 45) +layout_mode = 2 +theme = SubResource("Theme_button") +text = "注册" + +[node name="ToLoginBtn" type="Button" parent="CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer"] +custom_minimum_size = Vector2(120, 45) +layout_mode = 2 +theme = SubResource("Theme_button") +text = "返回登录" + +[node name="ToastContainer" type="Control" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 diff --git a/scenes/main_scene.tscn b/scenes/main_scene.tscn index a525467..e08e7d2 100644 --- a/scenes/main_scene.tscn +++ b/scenes/main_scene.tscn @@ -1,8 +1,188 @@ -[gd_scene load_steps=2 format=3 uid="uid://4ptgx76y83mx"] +[gd_scene load_steps=4 format=3 uid="uid://4ptgx76y83mx"] -[sub_resource type="AudioStream" id="AudioStream_o3jxj"] +[ext_resource type="Texture2D" uid="uid://bx17oy8lvaca4" path="res://assets/ui/auth/bg_auth_scene.png" id="1_background"] +[ext_resource type="PackedScene" uid="uid://by7m8snb4xllf" path="res://scenes/auth_scene.tscn" id="2_main"] +[ext_resource type="Script" uid="uid://cejrxy23ldhug" path="res://scripts/scenes/MainScene.gd" id="3_script"] -[node name="Node2D" type="Node2D"] +[node name="Main" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("3_script") -[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] -stream = SubResource("AudioStream_o3jxj") +[node name="BackgroundImage" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("1_background") +expand_mode = 1 +stretch_mode = 6 + +[node name="AuthScene" parent="." instance=ExtResource("2_main")] +layout_mode = 1 + +[node name="MainGameUI" type="Control" parent="."] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="UIOverlay" type="ColorRect" parent="MainGameUI"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0.3) + +[node name="TopBar" type="Panel" parent="MainGameUI"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 60.0 +grow_horizontal = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="MainGameUI/TopBar"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = 10.0 +offset_right = -20.0 +offset_bottom = -10.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="WelcomeLabel" type="Label" parent="MainGameUI/TopBar/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "🐋 欢迎来到鲸鱼镇!" +vertical_alignment = 1 + +[node name="UserLabel" type="Label" parent="MainGameUI/TopBar/HBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "当前用户: [用户名]" +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="LogoutButton" type="Button" parent="MainGameUI/TopBar/HBoxContainer"] +layout_mode = 2 +text = "退出" + +[node name="MainContent" type="Control" parent="MainGameUI"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = 60.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="CenterContainer" type="CenterContainer" parent="MainGameUI/MainContent"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="MainGameUI/MainContent/CenterContainer"] +custom_minimum_size = Vector2(600, 400) +layout_mode = 2 + +[node name="GameTitle" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_font_sizes/font_size = 24 +text = "🏘️ 鲸鱼镇主界面" +horizontal_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"] +layout_mode = 2 + +[node name="GameMenuGrid" type="GridContainer" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +columns = 2 + +[node name="ExploreButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"] +custom_minimum_size = Vector2(280, 80) +layout_mode = 2 +text = "🗺️ 探索小镇" + +[node name="InventoryButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"] +custom_minimum_size = Vector2(280, 80) +layout_mode = 2 +text = "🎒 背包物品" + +[node name="ShopButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"] +custom_minimum_size = Vector2(280, 80) +layout_mode = 2 +text = "🏪 商店购物" + +[node name="FriendsButton" type="Button" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid"] +custom_minimum_size = Vector2(280, 80) +layout_mode = 2 +text = "👥 好友列表" + +[node name="HSeparator2" type="HSeparator" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"] +layout_mode = 2 + +[node name="StatusPanel" type="Panel" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer"] +custom_minimum_size = Vector2(0, 120) +layout_mode = 2 + +[node name="StatusContainer" type="VBoxContainer" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = 10.0 +offset_right = -20.0 +offset_bottom = -10.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="StatusTitle" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "📊 玩家状态" +horizontal_alignment = 1 + +[node name="StatusGrid" type="GridContainer" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer"] +layout_mode = 2 +columns = 2 + +[node name="LevelLabel" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "等级: 1" + +[node name="CoinsLabel" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "金币: 100" + +[node name="ExpLabel" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "经验: 0/100" + +[node name="EnergyLabel" type="Label" parent="MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "体力: 100/100" diff --git a/scenes/prefabs/characters/.gitkeep b/scenes/prefabs/characters/.gitkeep new file mode 100644 index 0000000..fb0695e --- /dev/null +++ b/scenes/prefabs/characters/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 角色预制体目录 \ No newline at end of file diff --git a/scenes/prefabs/effects/.gitkeep b/scenes/prefabs/effects/.gitkeep new file mode 100644 index 0000000..6691578 --- /dev/null +++ b/scenes/prefabs/effects/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 特效预制体目录 \ No newline at end of file diff --git a/scenes/prefabs/items/.gitkeep b/scenes/prefabs/items/.gitkeep new file mode 100644 index 0000000..2e3c48c --- /dev/null +++ b/scenes/prefabs/items/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 物品预制体目录 \ No newline at end of file diff --git a/scenes/prefabs/ui/.gitkeep b/scenes/prefabs/ui/.gitkeep new file mode 100644 index 0000000..884109f --- /dev/null +++ b/scenes/prefabs/ui/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - UI预制体目录 \ No newline at end of file diff --git a/scripts/.gitkeep b/scripts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/scripts/characters/.gitkeep b/scripts/characters/.gitkeep new file mode 100644 index 0000000..5dde4ab --- /dev/null +++ b/scripts/characters/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 角色脚本目录 \ No newline at end of file diff --git a/scripts/data/.gitkeep b/scripts/data/.gitkeep new file mode 100644 index 0000000..626ed81 --- /dev/null +++ b/scripts/data/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 数据脚本目录 \ No newline at end of file diff --git a/scripts/gameplay/.gitkeep b/scripts/gameplay/.gitkeep new file mode 100644 index 0000000..37630f0 --- /dev/null +++ b/scripts/gameplay/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 游戏玩法脚本目录 \ No newline at end of file diff --git a/scripts/network/NetworkTest.gd b/scripts/network/NetworkTest.gd new file mode 100644 index 0000000..6b81ca7 --- /dev/null +++ b/scripts/network/NetworkTest.gd @@ -0,0 +1,31 @@ +extends Node + +# 简单的API测试脚本 +const API_BASE_URL = "https://whaletownend.xinghangee.icu" + +func _ready(): + print("API测试脚本已加载") + print("服务器地址: ", API_BASE_URL) + + # 测试服务器连接 + test_server_status() + +func test_server_status(): + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_on_status_request_completed) + + print("正在测试服务器连接...") + var error = http_request.request(API_BASE_URL) + if error != OK: + print("请求失败: ", error) + +func _on_status_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): + var response_text = body.get_string_from_utf8() + print("服务器状态响应: ", response_code) + print("响应内容: ", response_text) + + if response_code == 200: + print("✅ 服务器连接正常") + else: + print("❌ 服务器连接失败") diff --git a/scripts/network/NetworkTest.gd.uid b/scripts/network/NetworkTest.gd.uid new file mode 100644 index 0000000..e8a640d --- /dev/null +++ b/scripts/network/NetworkTest.gd.uid @@ -0,0 +1 @@ +uid://bsfrdqpsvwgtb diff --git a/scripts/scenes/AuthScene.gd b/scripts/scenes/AuthScene.gd new file mode 100644 index 0000000..a4e467e --- /dev/null +++ b/scripts/scenes/AuthScene.gd @@ -0,0 +1,1098 @@ +extends Control + +# 信号定义 +signal login_success(username: String) + +# API配置 +const API_BASE_URL = "https://whaletownend.xinghangee.icu" + +# UI节点引用 +@onready var background_image: TextureRect = $BackgroundImage +@onready var login_panel: Panel = $CenterContainer/LoginPanel +@onready var register_panel: Panel = $CenterContainer/RegisterPanel +@onready var title_label: Label = $CenterContainer/LoginPanel/VBoxContainer/TitleLabel +@onready var subtitle_label: Label = $CenterContainer/LoginPanel/VBoxContainer/SubtitleLabel +@onready var whale_frame: TextureRect = $WhaleFrame + +# 登录表单 +@onready var login_username: LineEdit = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameInput +@onready var login_password: LineEdit = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordInput +@onready var login_username_error: Label = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer/UsernameLabelContainer/UsernameError +@onready var login_password_error: Label = $CenterContainer/LoginPanel/VBoxContainer/LoginForm/PasswordContainer/PasswordLabelContainer/PasswordError +@onready var main_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/MainButton +@onready var login_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/ButtonContainer/LoginBtn +@onready var to_register_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/ButtonContainer/ToRegisterBtn +@onready var forgot_password_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/BottomLinks/ForgotPassword +@onready var register_link_btn: Button = $CenterContainer/LoginPanel/VBoxContainer/BottomLinks/RegisterLink + +# 注册表单 +@onready var register_username: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameInput +@onready var register_email: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailInput +@onready var register_password: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordInput +@onready var register_confirm: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmInput +@onready var verification_input: LineEdit = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer/VerificationInput +@onready var send_code_btn: Button = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationInputContainer/SendCodeBtn +@onready var register_btn: Button = $CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer/RegisterBtn +@onready var to_login_btn: Button = $CenterContainer/RegisterPanel/VBoxContainer/ButtonContainer/ToLoginBtn + +# 错误提示标签 +@onready var register_username_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/UsernameContainer/UsernameLabelContainer/UsernameError +@onready var register_email_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/EmailContainer/EmailLabelContainer/EmailError +@onready var register_password_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/PasswordContainer/PasswordLabelContainer/PasswordError +@onready var register_confirm_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/ConfirmContainer/ConfirmLabelContainer/ConfirmError +@onready var verification_error: Label = $CenterContainer/RegisterPanel/VBoxContainer/RegisterForm/VerificationContainer/VerificationLabelContainer/VerificationError + +# HTTP请求节点 +var http_request: HTTPRequest + +# Toast消息节点 +@onready var toast_container: Control = $ToastContainer + +# Toast管理 +var active_toasts: Array = [] +var toast_counter: int = 0 + +# 验证码状态 +var verification_codes_sent: Dictionary = {} # 存储每个邮箱的发送状态 {email: {sent: bool, time: float}} +var code_cooldown: float = 60.0 # 60秒冷却时间 +var current_request_type: String = "" # 跟踪当前请求类型 +var register_data: Dictionary = {} # 存储注册数据 +var cooldown_timer: Timer = null # 倒计时定时器 +var current_email: String = "" # 当前正在倒计时的邮箱 + +func _ready(): + # 获取HTTP请求节点 + http_request = $HTTPRequest + http_request.request_completed.connect(_on_http_request_completed) + + # 连接信号 + connect_signals() + + # 初始显示登录界面 + show_login_panel() + + # 测试Toast系统(延迟一下确保节点已初始化) + await get_tree().process_frame + print("测试Toast系统...") + show_toast("认证系统已加载", true) + + # 测试网络连接 + test_network_connection() + +# 测试网络连接 +func test_network_connection(): + print("=== 测试网络连接 ===") + var url = API_BASE_URL + "/" + var headers = ["Content-Type: application/json"] + + print("测试URL: ", url) + current_request_type = "network_test" + + var error = http_request.request(url, headers, HTTPClient.METHOD_GET) + print("网络测试请求发送结果: ", error) + + if error != OK: + print("网络测试请求发送失败: ", error) + show_toast("网络连接测试失败", false) + else: + print("网络测试请求已发送,等待响应...") + +func connect_signals(): + # 主要按钮 + main_btn.pressed.connect(_on_main_button_pressed) + + # 登录界面按钮 + login_btn.pressed.connect(_on_login_pressed) + to_register_btn.pressed.connect(_on_to_register_pressed) + forgot_password_btn.pressed.connect(_on_forgot_password_pressed) + register_link_btn.pressed.connect(_on_register_link_pressed) + + # 注册界面按钮 + register_btn.pressed.connect(_on_register_pressed) + to_login_btn.pressed.connect(_on_to_login_pressed) + send_code_btn.pressed.connect(_on_send_code_pressed) + + # 回车键登录 + login_password.text_submitted.connect(_on_login_enter) + + # 登录表单失焦验证 + login_username.focus_exited.connect(_on_login_username_focus_exited) + login_password.focus_exited.connect(_on_login_password_focus_exited) + + # 注册表单失焦验证 + register_username.focus_exited.connect(_on_register_username_focus_exited) + register_email.focus_exited.connect(_on_register_email_focus_exited) + register_password.focus_exited.connect(_on_register_password_focus_exited) + register_confirm.focus_exited.connect(_on_register_confirm_focus_exited) + verification_input.focus_exited.connect(_on_verification_focus_exited) + + # 实时输入验证 + register_username.text_changed.connect(_on_register_username_text_changed) + register_email.text_changed.connect(_on_register_email_text_changed) + register_password.text_changed.connect(_on_register_password_text_changed) + register_confirm.text_changed.connect(_on_register_confirm_text_changed) + verification_input.text_changed.connect(_on_verification_text_changed) + +func show_login_panel(): + login_panel.visible = true + register_panel.visible = false + login_username.grab_focus() + +func show_register_panel(): + login_panel.visible = false + register_panel.visible = true + register_username.grab_focus() + +func _on_main_button_pressed(): + if not validate_login_form(): + return + + var username = login_username.text.strip_edges() + var password = login_password.text + + # 显示加载状态 + show_loading(main_btn, "登录中...") + show_toast('正在验证登录信息...', true) + + # 发送登录请求 + send_login_request(username, password) + +func _on_login_pressed(): + if not validate_login_form(): + return + + var username = login_username.text.strip_edges() + var password = login_password.text + + # 显示加载状态 + show_loading(login_btn, "登录中...") + show_toast('正在验证登录信息...', true) + + # 发送登录请求 + send_login_request(username, password) + +func _on_register_pressed(): + print("注册按钮被点击") + + if not validate_register_form(): + print("注册表单验证失败") + show_toast('请检查并完善注册信息', false) + return + + print("注册表单验证通过,开始注册流程") + + var username = register_username.text.strip_edges() + var email = register_email.text.strip_edges() + var password = register_password.text + var verification_code = verification_input.text.strip_edges() + + # 显示加载状态 + show_loading(register_btn, "注册中...") + show_toast('正在验证邮箱验证码...', true) + + # 先验证邮箱验证码,然后注册 + verify_email_then_register(username, email, password, verification_code) + +func _on_send_code_pressed(): + var email = register_email.text.strip_edges() + + # 验证邮箱 + var email_validation = validate_email(email) + if not email_validation.valid: + show_toast(email_validation.message, false) + register_email.grab_focus() + return + + hide_field_error(register_email_error) + + # 检查该邮箱的冷却时间 + var current_time = Time.get_time_dict_from_system() + var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second + + if verification_codes_sent.has(email): + var email_data = verification_codes_sent[email] + if email_data.sent and (current_timestamp - email_data.time) < code_cooldown: + var remaining = code_cooldown - (current_timestamp - email_data.time) + show_toast('该邮箱请等待 %d 秒后再次发送' % remaining, false) + return + + # 如果当前有其他邮箱在倒计时,需要切换到新邮箱 + if current_email != email: + # 停止当前倒计时 + stop_current_cooldown() + current_email = email + + # 立即开始倒计时并禁用按钮 + if not verification_codes_sent.has(email): + verification_codes_sent[email] = {} + + verification_codes_sent[email].sent = true + verification_codes_sent[email].time = current_timestamp + start_cooldown_timer(email) + + # 发送验证码请求 + send_verification_code_request(email) + +# 发送登录请求 +func send_login_request(username: String, password: String): + var url = API_BASE_URL + "/auth/login" + var headers = ["Content-Type: application/json"] + var body = JSON.stringify({ + "username": username, + "password": password + }) + + current_request_type = "login" + + var error = http_request.request(url, headers, HTTPClient.METHOD_POST, body) + if error != OK: + show_toast('网络请求失败', false) + restore_button(login_btn, "密码登录") + current_request_type = "" + +# 发送邮箱验证码请求 +func send_verification_code_request(email: String): + var url = API_BASE_URL + "/auth/send-email-verification" + var headers = ["Content-Type: application/json"] + var body = JSON.stringify({"email": email}) + + print("=== 发送验证码请求 ===") + print("URL: ", url) + print("Headers: ", headers) + print("Body: ", body) + + current_request_type = "send_code" + + var error = http_request.request(url, headers, HTTPClient.METHOD_POST, body) + print("HTTP请求发送结果: ", error, " (OK=", OK, ")") + + if error != OK: + print("HTTP请求发送失败,错误代码: ", error) + show_toast('网络请求失败', false) + restore_button(send_code_btn, "发送验证码") + current_request_type = "" + else: + print("HTTP请求已发送,等待响应...") + +# 验证邮箱然后注册 +func verify_email_then_register(username: String, email: String, password: String, verification_code: String): + var url = API_BASE_URL + "/auth/verify-email" + var headers = ["Content-Type: application/json"] + var body = JSON.stringify({ + "email": email, + "verification_code": verification_code + }) + + current_request_type = "verify_email" + + # 保存注册信息,验证成功后使用 + register_data = { + "username": username, + "email": email, + "password": password, + "verification_code": verification_code + } + + var error = http_request.request(url, headers, HTTPClient.METHOD_POST, body) + if error != OK: + show_toast('网络请求失败', false) + restore_button(register_btn, "注册") + current_request_type = "" + +# 发送注册请求 +func send_register_request(username: String, email: String, password: String, verification_code: String = ""): + var url = API_BASE_URL + "/auth/register" + var headers = ["Content-Type: application/json"] + var body_data = { + "username": username, + "password": password, + "nickname": username, # 使用用户名作为昵称 + "email": email + } + + # 如果提供了验证码,则添加到请求体中 + if verification_code != "": + body_data["email_verification_code"] = verification_code + + var body = JSON.stringify(body_data) + + current_request_type = "register" + + var error = http_request.request(url, headers, HTTPClient.METHOD_POST, body) + if error != OK: + show_toast('网络请求失败', false) + restore_button(register_btn, "注册") + current_request_type = "" + +# HTTP请求完成回调 +func _on_http_request_completed(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray): + var response_text = body.get_string_from_utf8() + + print("=== HTTP响应接收 ===") + print("请求类型: ", current_request_type) + print("响应状态码: ", response_code) + print("响应头: ", _headers) + print("响应体: ", response_text) + print("响应体长度: ", body.size(), " 字节") + + # 恢复按钮状态(排除验证码按钮,因为它有自己的状态管理) + if current_request_type != "send_code": + restore_button(send_code_btn, "发送验证码") + restore_button(register_btn, "注册") + restore_button(login_btn, "密码登录") + restore_button(main_btn, "进入小镇") + + # 处理网络连接失败 + if response_code == 0: + show_toast('网络连接失败,请检查网络连接', false) + current_request_type = "" + return + + # 解析JSON响应 + var json = JSON.new() + var parse_result = json.parse(response_text) + if parse_result != OK: + show_toast('服务器响应格式错误', false) + current_request_type = "" + return + + var response_data = json.data + + # 根据请求类型处理响应 + var request_type = current_request_type + current_request_type = "" # 提前清空,避免异步调用时的竞态条件 + + match request_type: + "network_test": + handle_network_test_response(response_code, response_data) + "login": + handle_login_response(response_code, response_data) + "send_code": + handle_verification_code_response(response_code, response_data) + "verify_email": + handle_verify_email_response(response_code, response_data) + "register": + handle_register_response(response_code, response_data) + +# 处理网络测试响应 +func handle_network_test_response(response_code: int, data: Dictionary): + print("=== 网络测试响应 ===") + print("状态码: ", response_code) + print("响应数据: ", data) + + if response_code == 200: + print("✅ 网络连接正常,后端服务可访问") + show_toast("网络连接正常", true) + else: + print("❌ 网络连接异常,状态码: ", response_code) + show_toast("网络连接异常: " + str(response_code), false) + +# 处理登录响应 +func handle_login_response(response_code: int, data: Dictionary): + match response_code: + 200: + show_toast('登录成功!正在进入鲸鱼镇...', true) + # 获取用户信息 + var username = login_username.text.strip_edges() + if data.has("data") and data.data.has("user"): + var user_data = data.data.user + if user_data.has("username"): + username = user_data.username + + # 清空登录表单 + login_username.text = "" + login_password.text = "" + hide_field_error(login_username_error) + hide_field_error(login_password_error) + + # 延迟一下再发送信号,让用户看到成功消息 + await get_tree().create_timer(1.0).timeout + + # 发送登录成功信号 + login_success.emit(username) + 400: + # 参数错误,统一使用Toast显示 + var message = data.get("message", "登录参数错误") + if "用户名" in message or "username" in message.to_lower(): + show_toast('用户名格式错误', false) + elif "密码" in message or "password" in message.to_lower(): + show_toast('密码格式错误', false) + else: + show_toast('登录信息有误,请检查后重试', false) + 401: + # 认证失败 + show_toast('用户名或密码错误,请检查后重试', false) + 404: + # 用户不存在 + show_toast('用户不存在,请先注册', false) + 429: + # 请求过频 + show_toast('登录请求过于频繁,请稍后再试', false) + 500: + # 服务器错误 + show_toast('服务器繁忙,请稍后再试', false) + _: + var message = data.get("message", "登录失败") + show_toast(message, false) + +# 处理验证码响应 +func handle_verification_code_response(response_code: int, data: Dictionary): + match response_code: + 200: + show_toast('验证码已发送到您的邮箱,请查收', true) + + # 开发环境下显示验证码(仅用于测试) + if data.has("data") and data.data.has("verification_code"): + print("开发环境验证码: ", data.data.verification_code) + 206: + # 测试模式 + show_toast('测试模式:验证码已生成,请查看控制台', true) + if data.has("data") and data.data.has("verification_code"): + print("测试模式验证码: ", data.data.verification_code) + 400: + # 根据具体错误信息显示相应的Toast + var message = data.get("message", "发送验证码失败") + var error_code = data.get("error_code", "") + + # 根据错误代码或消息内容判断具体错误类型 + if "邮箱格式" in message or "INVALID_EMAIL" in error_code: + show_toast('请输入有效的邮箱地址', false) + elif "每小时发送次数" in message or "HOURLY_LIMIT" in error_code: + show_toast('每小时发送次数已达上限,请稍后再试', false) + elif "频率" in message or "RATE_LIMITED" in error_code: + show_toast('发送过于频繁,请稍后再试', false) + else: + # 未知400错误,显示通用消息 + show_toast('发送验证码失败,请检查邮箱地址或稍后再试', false) + + reset_verification_button() + 429: + # 频率限制 + var message = data.get("message", "请求过于频繁,请稍后再试") + show_toast(message, false) + reset_verification_button() + 500: + show_toast('服务器繁忙,请稍后再试', false) + reset_verification_button() + _: + var message = data.get("message", "发送验证码失败") + show_toast(message, false) + reset_verification_button() + +# 处理邮箱验证响应 +func handle_verify_email_response(response_code: int, data: Dictionary): + match response_code: + 200: + show_toast('邮箱验证成功,正在注册...', true) + # 邮箱验证成功,继续注册 + if register_data.has("username") and register_data.has("email") and register_data.has("password") and register_data.has("verification_code"): + send_register_request(register_data.username, register_data.email, register_data.password, register_data.verification_code) + else: + show_toast('注册数据丢失,请重新填写', false) + 400: + show_toast('验证码错误或已过期', false) + 404: + show_toast('请先获取验证码', false) + 500: + show_toast('验证失败,请稍后再试', false) + _: + var message = data.get("message", "邮箱验证失败") + show_toast(message, false) + +# 处理注册响应 +func handle_register_response(response_code: int, data: Dictionary): + match response_code: + 201: + show_toast('注册成功!欢迎加入鲸鱼镇', true) + # 清空表单 + clear_register_form() + # 返回登录界面 + show_login_panel() + # 自动填入用户名 + login_username.text = register_data.get("username", "") + register_data.clear() + 400: + # 根据具体错误处理 + var message = data.get("message", "参数验证失败") + + # 针对常见错误提供友好提示,统一使用Toast显示 + if "邮箱验证码" in message or "verification_code" in message: + show_toast('请先获取并输入邮箱验证码', false) + elif "用户名" in message: + show_toast('用户名格式不正确', false) + elif "邮箱" in message: + show_toast('邮箱格式不正确', false) + elif "密码" in message: + show_toast('密码格式不符合要求', false) + elif "验证码" in message: + show_toast('验证码错误或已过期', false) + else: + # 显示用户友好的通用错误信息 + show_toast('注册信息有误,请检查后重试', false) + 409: + # 用户名或邮箱已存在 + var message = data.get("message", "用户名或邮箱已被使用") + if "用户名" in message: + show_toast('用户名已被使用,请换一个', false) + elif "邮箱" in message: + show_toast('邮箱已被使用,请换一个', false) + else: + show_toast('用户名或邮箱已被使用,请换一个', false) + 429: + # 注册请求过于频繁 + var message = data.get("message", "注册请求过于频繁,请稍后再试") + show_toast(message, false) + 500: + show_toast('注册失败,请稍后再试', false) + _: + var message = data.get("message", "注册失败") + show_toast(message, false) + +# 开始冷却计时器 +func start_cooldown_timer(email: String): + # 清理之前的计时器 + if cooldown_timer != null: + cooldown_timer.queue_free() + + # 设置当前邮箱 + current_email = email + + # 立即设置按钮状态 + send_code_btn.disabled = true + send_code_btn.text = "重新发送(60)" + + # 创建新的计时器 + cooldown_timer = Timer.new() + add_child(cooldown_timer) + cooldown_timer.wait_time = 1.0 + cooldown_timer.timeout.connect(_on_cooldown_timer_timeout) + cooldown_timer.start() + +func _on_cooldown_timer_timeout(): + # 检查当前邮箱输入框的邮箱 + var input_email = register_email.text.strip_edges() + + # 如果用户换了邮箱,停止当前倒计时 + if input_email != current_email: + stop_current_cooldown() + return + + # 检查当前邮箱的剩余时间 + if verification_codes_sent.has(current_email): + var current_time = Time.get_time_dict_from_system() + var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second + var email_data = verification_codes_sent[current_email] + var remaining = code_cooldown - (current_timestamp - email_data.time) + + if remaining > 0: + send_code_btn.text = "重新发送(%d)" % remaining + else: + # 倒计时结束,恢复按钮 + send_code_btn.text = "重新发送" + send_code_btn.disabled = false + + # 清理计时器 + if cooldown_timer != null: + cooldown_timer.queue_free() + cooldown_timer = null + current_email = "" + +# 停止当前倒计时 +func stop_current_cooldown(): + if cooldown_timer != null: + cooldown_timer.queue_free() + cooldown_timer = null + + # 恢复按钮状态 + send_code_btn.disabled = false + send_code_btn.text = "发送验证码" + current_email = "" + +# 重置验证码按钮状态(发送失败时调用) +func reset_verification_button(): + # 清除当前邮箱的发送状态 + if current_email != "" and verification_codes_sent.has(current_email): + verification_codes_sent[current_email].sent = false + + stop_current_cooldown() + +# 清空注册表单 +func clear_register_form(): + register_username.text = "" + register_email.text = "" + register_password.text = "" + register_confirm.text = "" + verification_input.text = "" + + # 重置验证码状态 + stop_current_cooldown() + verification_codes_sent.clear() + + # 隐藏所有错误提示 + hide_field_error(register_username_error) + hide_field_error(register_email_error) + hide_field_error(register_password_error) + hide_field_error(register_confirm_error) + hide_field_error(verification_error) + +# ============ Toast消息系统 ============ + +# 显示Toast消息 +func show_toast(message: String, is_success: bool = true): + print("显示Toast消息: ", message, " 成功: ", is_success) + + # 确保容器存在 + if toast_container == null: + print("错误: toast_container 节点不存在") + return + + # 创建新的Toast实例 + create_toast_instance(message, is_success) + +# 创建Toast实例 +func create_toast_instance(message: String, is_success: bool): + toast_counter += 1 + + # 创建Toast Panel + var toast_panel = Panel.new() + toast_panel.name = "Toast_" + str(toast_counter) + + # 设置Toast样式 + var style = StyleBoxFlat.new() + if is_success: + style.bg_color = Color(0.2, 0.8, 0.2, 0.95) # 绿色背景 + else: + style.bg_color = Color(0.8, 0.2, 0.2, 0.95) # 红色背景 + + style.border_width_left = 2 + style.border_width_top = 2 + style.border_width_right = 2 + style.border_width_bottom = 2 + style.border_color = Color(1, 1, 1, 0.8) # 白色边框 + style.corner_radius_top_left = 8 + style.corner_radius_top_right = 8 + style.corner_radius_bottom_left = 8 + style.corner_radius_bottom_right = 8 + + toast_panel.add_theme_stylebox_override("panel", style) + + # 设置Toast尺寸和位置(右上角外侧开始) + var toast_width = 280 + var toast_height = 50 + var margin = 20 + var start_x = get_viewport().get_visible_rect().size.x # 屏幕外右侧 + var final_x = get_viewport().get_visible_rect().size.x - toast_width - margin # 最终位置 + var y_position = margin + (active_toasts.size() * (toast_height + 10)) # 垂直堆叠 + + toast_panel.position = Vector2(start_x, y_position) + toast_panel.size = Vector2(toast_width, toast_height) + + # 创建Label + var toast_label = Label.new() + toast_label.text = message + toast_label.add_theme_color_override("font_color", Color(1, 1, 1, 1)) + toast_label.add_theme_font_size_override("font_size", 14) + toast_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + toast_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + toast_label.autowrap_mode = TextServer.AUTOWRAP_OFF # 禁用自动换行 + + # 设置Label的位置和大小(不使用anchors_preset) + toast_label.position = Vector2(10, 5) + toast_label.size = Vector2(toast_width - 20, toast_height - 10) # 留出边距 + + # 组装Toast + toast_panel.add_child(toast_label) + toast_container.add_child(toast_panel) + active_toasts.append(toast_panel) + + # 执行滑入动画(快慢快) + animate_toast_in(toast_panel, final_x) + +# Toast滑入动画(快慢快) +func animate_toast_in(toast_panel: Panel, final_x: float): + var tween = create_tween() + tween.set_ease(Tween.EASE_OUT) # 开始快,结束慢 + tween.set_trans(Tween.TRANS_BACK) # 回弹效果 + + # 滑入动画 + tween.tween_property(toast_panel, "position:x", final_x, 0.5) + + # 等待2秒后滑出 + await get_tree().create_timer(2.0).timeout + animate_toast_out(toast_panel) + +# Toast滑出动画 +func animate_toast_out(toast_panel: Panel): + if not is_instance_valid(toast_panel): + return + + var tween = create_tween() + tween.set_ease(Tween.EASE_IN) # 开始慢,结束快 + tween.set_trans(Tween.TRANS_QUART) + + # 滑出到右侧屏幕外 + var end_x = get_viewport().get_visible_rect().size.x + 50 + tween.tween_property(toast_panel, "position:x", end_x, 0.3) + + # 动画完成后清理 + await tween.finished + cleanup_toast(toast_panel) + +# 清理Toast实例 +func cleanup_toast(toast_panel: Panel): + if not is_instance_valid(toast_panel): + return + + # 从活动列表中移除 + active_toasts.erase(toast_panel) + + # 重新排列剩余的Toast + rearrange_toasts() + + # 删除节点 + toast_panel.queue_free() + +# 重新排列Toast位置 +func rearrange_toasts(): + var margin = 20 + var toast_height = 50 + + for i in range(active_toasts.size()): + var toast = active_toasts[i] + if is_instance_valid(toast): + var new_y = margin + (i * (toast_height + 10)) + var tween = create_tween() + tween.tween_property(toast, "position:y", new_y, 0.2) + +# 显示加载状态 +func show_loading(button: Button, loading_text: String): + button.disabled = true + button.text = loading_text + +# 恢复按钮状态 +func restore_button(button: Button, original_text: String): + button.disabled = false + button.text = original_text + +# 验证邮箱格式 +func is_valid_email(email: String) -> bool: + var regex = RegEx.new() + regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") + return regex.search(email) != null + +# 显示错误信息 +func show_field_error(error_label: Label, message: String): + error_label.text = message + error_label.visible = true + +# 隐藏错误信息 +func hide_field_error(error_label: Label): + error_label.visible = false + +# 验证用户名 +func validate_username(username: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if username.is_empty(): + result.message = "用户名不能为空" + return result + + if username.length() < 1 or username.length() > 50: + result.message = "用户名长度应为1-50字符" + return result + + # 检查用户名格式(字母、数字、下划线) + var regex = RegEx.new() + regex.compile("^[a-zA-Z0-9_]+$") + if not regex.search(username): + result.message = "用户名只能包含字母、数字和下划线" + return result + + result.valid = true + return result + +# 验证邮箱 +func validate_email(email: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if email.is_empty(): + result.message = "邮箱不能为空" + return result + + if not is_valid_email(email): + result.message = "请输入有效的邮箱地址" + return result + + result.valid = true + return result + +# 验证密码 +func validate_password(password: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if password.is_empty(): + result.message = "密码不能为空" + return result + + if password.length() < 8: + result.message = "密码长度至少8位" + return result + + if password.length() > 128: + result.message = "密码长度不能超过128位" + return result + + # 验证密码格式(必须包含字母和数字) + var has_letter = false + var has_digit = false + for i in range(password.length()): + var character = password[i] + if character >= 'a' and character <= 'z' or character >= 'A' and character <= 'Z': + has_letter = true + elif character >= '0' and character <= '9': + has_digit = true + + if not (has_letter and has_digit): + result.message = "密码必须包含字母和数字" + return result + + result.valid = true + return result + +# 验证确认密码 +func validate_confirm_password(password: String, confirm: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if confirm.is_empty(): + result.message = "确认密码不能为空" + return result + + if password != confirm: + result.message = "两次输入的密码不一致" + return result + + result.valid = true + return result + +# 验证验证码 +func validate_verification_code(code: String) -> Dictionary: + var result = {"valid": false, "message": ""} + + if code.is_empty(): + result.message = "验证码不能为空" + return result + + if code.length() != 6: + result.message = "验证码必须是6位数字" + return result + + # 验证是否为纯数字 + for i in range(code.length()): + var character = code[i] + if not (character >= '0' and character <= '9'): + result.message = "验证码必须是6位数字" + return result + + result.valid = true + return result + +# ============ 登录表单验证事件 ============ + +func _on_login_username_focus_exited(): + var username = login_username.text.strip_edges() + if username.is_empty(): + show_field_error(login_username_error, "用户名不能为空") + else: + hide_field_error(login_username_error) + +func _on_login_password_focus_exited(): + var password = login_password.text + if password.is_empty(): + show_field_error(login_password_error, "密码不能为空") + else: + hide_field_error(login_password_error) + +# ============ 注册表单验证事件 ============ + +func _on_register_username_focus_exited(): + var username = register_username.text.strip_edges() + var validation = validate_username(username) + if not validation.valid: + show_field_error(register_username_error, validation.message) + else: + hide_field_error(register_username_error) + +func _on_register_email_focus_exited(): + var email = register_email.text.strip_edges() + var validation = validate_email(email) + if not validation.valid: + show_field_error(register_email_error, validation.message) + else: + hide_field_error(register_email_error) + +func _on_register_password_focus_exited(): + var password = register_password.text + var validation = validate_password(password) + if not validation.valid: + show_field_error(register_password_error, validation.message) + else: + hide_field_error(register_password_error) + # 如果确认密码已填写,重新验证确认密码 + if not register_confirm.text.is_empty(): + _on_register_confirm_focus_exited() + +func _on_register_confirm_focus_exited(): + var password = register_password.text + var confirm = register_confirm.text + var validation = validate_confirm_password(password, confirm) + if not validation.valid: + show_field_error(register_confirm_error, validation.message) + else: + hide_field_error(register_confirm_error) + +func _on_verification_focus_exited(): + var code = verification_input.text.strip_edges() + var validation = validate_verification_code(code) + if not validation.valid: + show_field_error(verification_error, validation.message) + else: + hide_field_error(verification_error) + +# ============ 实时输入验证事件 ============ + +func _on_register_username_text_changed(new_text: String): + # 输入时隐藏错误提示 + if register_username_error.visible and not new_text.is_empty(): + hide_field_error(register_username_error) + +func _on_register_email_text_changed(new_text: String): + if register_email_error.visible and not new_text.is_empty(): + hide_field_error(register_email_error) + +func _on_register_password_text_changed(new_text: String): + if register_password_error.visible and not new_text.is_empty(): + hide_field_error(register_password_error) + +func _on_register_confirm_text_changed(new_text: String): + if register_confirm_error.visible and not new_text.is_empty(): + hide_field_error(register_confirm_error) + +func _on_verification_text_changed(new_text: String): + if verification_error.visible and not new_text.is_empty(): + hide_field_error(verification_error) + +# ============ 表单整体验证 ============ + +# 验证登录表单 +func validate_login_form() -> bool: + var is_valid = true + + var username = login_username.text.strip_edges() + var password = login_password.text + + if username.is_empty(): + show_field_error(login_username_error, "用户名不能为空") + is_valid = false + else: + hide_field_error(login_username_error) + + if password.is_empty(): + show_field_error(login_password_error, "密码不能为空") + is_valid = false + else: + hide_field_error(login_password_error) + + return is_valid + +# 验证注册表单 +func validate_register_form() -> bool: + print("开始验证注册表单") + var is_valid = true + + var username = register_username.text.strip_edges() + var email = register_email.text.strip_edges() + var password = register_password.text + var confirm = register_confirm.text + var verification_code = verification_input.text.strip_edges() + + print("表单数据: 用户名='%s', 邮箱='%s', 密码长度=%d, 确认密码长度=%d, 验证码='%s'" % [username, email, password.length(), confirm.length(), verification_code]) + + # 验证用户名 + var username_validation = validate_username(username) + if not username_validation.valid: + print("用户名验证失败: ", username_validation.message) + show_field_error(register_username_error, username_validation.message) + is_valid = false + else: + hide_field_error(register_username_error) + + # 验证邮箱 + var email_validation = validate_email(email) + if not email_validation.valid: + print("邮箱验证失败: ", email_validation.message) + show_field_error(register_email_error, email_validation.message) + is_valid = false + else: + hide_field_error(register_email_error) + + # 验证密码 + var password_validation = validate_password(password) + if not password_validation.valid: + print("密码验证失败: ", password_validation.message) + show_field_error(register_password_error, password_validation.message) + is_valid = false + else: + hide_field_error(register_password_error) + + # 验证确认密码 + var confirm_validation = validate_confirm_password(password, confirm) + if not confirm_validation.valid: + print("确认密码验证失败: ", confirm_validation.message) + show_field_error(register_confirm_error, confirm_validation.message) + is_valid = false + else: + hide_field_error(register_confirm_error) + + # 验证验证码 + var code_validation = validate_verification_code(verification_code) + if not code_validation.valid: + print("验证码格式验证失败: ", code_validation.message) + show_field_error(verification_error, code_validation.message) + is_valid = false + else: + hide_field_error(verification_error) + + # 检查是否已发送验证码(检查当前邮箱) + var current_email_input = register_email.text.strip_edges() + var has_sent_code = false + + if verification_codes_sent.has(current_email_input): + var email_data = verification_codes_sent[current_email_input] + has_sent_code = email_data.get("sent", false) + + if not has_sent_code: + print("当前邮箱验证码未发送,email = ", current_email_input) + show_toast("请先获取邮箱验证码", false) + is_valid = false + + print("表单验证结果: ", is_valid) + return is_valid + +func _on_to_register_pressed(): + show_toast('验证码登录功能待实现', false) + +func _on_forgot_password_pressed(): + show_toast('忘记密码功能待实现', false) + +func _on_register_link_pressed(): + show_register_panel() + +func _on_to_login_pressed(): + show_login_panel() + +func _on_login_enter(_text: String): + _on_login_pressed() + +func _input(event): + if event.is_action_pressed("ui_cancel"): + get_tree().quit() diff --git a/scripts/scenes/AuthScene.gd.uid b/scripts/scenes/AuthScene.gd.uid new file mode 100644 index 0000000..341b55c --- /dev/null +++ b/scripts/scenes/AuthScene.gd.uid @@ -0,0 +1 @@ +uid://nv8eitxieqtm diff --git a/scripts/scenes/MainScene.gd b/scripts/scenes/MainScene.gd new file mode 100644 index 0000000..004ae0d --- /dev/null +++ b/scripts/scenes/MainScene.gd @@ -0,0 +1,116 @@ +extends Control + +# 场景节点引用 +@onready var auth_scene: Control = $AuthScene +@onready var main_game_ui: Control = $MainGameUI +@onready var user_label: Label = $MainGameUI/TopBar/HBoxContainer/UserLabel +@onready var logout_button: Button = $MainGameUI/TopBar/HBoxContainer/LogoutButton + +# 游戏功能按钮 +@onready var explore_button: Button = $MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid/ExploreButton +@onready var inventory_button: Button = $MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid/InventoryButton +@onready var shop_button: Button = $MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid/ShopButton +@onready var friends_button: Button = $MainGameUI/MainContent/CenterContainer/VBoxContainer/GameMenuGrid/FriendsButton + +# 状态标签 +@onready var level_label: Label = $MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid/LevelLabel +@onready var coins_label: Label = $MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid/CoinsLabel +@onready var exp_label: Label = $MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid/ExpLabel +@onready var energy_label: Label = $MainGameUI/MainContent/CenterContainer/VBoxContainer/StatusPanel/StatusContainer/StatusGrid/EnergyLabel + +# 游戏状态 +enum GameState { + AUTH, # 登录/注册状态 + MAIN_GAME # 主游戏状态 +} + +var current_state: GameState = GameState.AUTH +var current_user: String = "" + +# 玩家数据 +var player_level: int = 1 +var player_coins: int = 100 +var player_exp: int = 0 +var player_max_exp: int = 100 +var player_energy: int = 100 +var player_max_energy: int = 100 + +func _ready(): + # 初始化游戏状态 + setup_game() + + # 连接登录成功信号 + auth_scene.login_success.connect(_on_login_success) + + # 连接按钮信号 + logout_button.pressed.connect(_on_logout_pressed) + explore_button.pressed.connect(_on_explore_pressed) + inventory_button.pressed.connect(_on_inventory_pressed) + shop_button.pressed.connect(_on_shop_pressed) + friends_button.pressed.connect(_on_friends_pressed) + +func setup_game(): + # 设置初始状态为登录界面 + show_auth_scene() + +func show_auth_scene(): + current_state = GameState.AUTH + auth_scene.visible = true + main_game_ui.visible = false + +func show_main_game(): + current_state = GameState.MAIN_GAME + auth_scene.visible = false + main_game_ui.visible = true + user_label.text = "当前用户: " + current_user + update_player_status() + print("进入主游戏界面") + +func update_player_status(): + level_label.text = "等级: " + str(player_level) + coins_label.text = "金币: " + str(player_coins) + exp_label.text = "经验: " + str(player_exp) + "/" + str(player_max_exp) + energy_label.text = "体力: " + str(player_energy) + "/" + str(player_max_energy) + +func _on_login_success(username: String): + # 登录成功后的处理 + current_user = username + print("用户 ", username, " 登录成功!") + show_main_game() + +func _on_logout_pressed(): + # 登出处理 + current_user = "" + show_auth_scene() + +# 游戏功能按钮处理 +func _on_explore_pressed(): + print("探索小镇功能") + show_game_message("🗺️ 探索功能开发中...") + +func _on_inventory_pressed(): + print("背包功能") + show_game_message("🎒 背包功能开发中...") + +func _on_shop_pressed(): + print("商店功能") + show_game_message("🏪 商店功能开发中...") + +func _on_friends_pressed(): + print("好友功能") + show_game_message("👥 好友功能开发中...") + +func show_game_message(message: String): + print("游戏消息: ", message) + # 这里可以添加UI提示框显示消息 + +# 处理全局输入 +func _input(event): + if event.is_action_pressed("ui_cancel"): + match current_state: + GameState.AUTH: + # 在登录界面按ESC退出游戏 + get_tree().quit() + GameState.MAIN_GAME: + # 在游戏中按ESC可能显示菜单或返回登录 + show_auth_scene() diff --git a/scripts/scenes/MainScene.gd.uid b/scripts/scenes/MainScene.gd.uid new file mode 100644 index 0000000..44c5673 --- /dev/null +++ b/scripts/scenes/MainScene.gd.uid @@ -0,0 +1 @@ +uid://cejrxy23ldhug diff --git a/scripts/ui/.gitkeep b/scripts/ui/.gitkeep new file mode 100644 index 0000000..ea7947f --- /dev/null +++ b/scripts/ui/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - UI脚本目录 \ No newline at end of file diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/api/README.md b/tests/api/README.md new file mode 100644 index 0000000..d519929 --- /dev/null +++ b/tests/api/README.md @@ -0,0 +1,150 @@ +# API接口测试 + +本目录包含用于测试whaleTown项目API接口的测试脚本。 + +## 测试脚本说明 + +### 1. `simple_api_test.py` - 简化版测试 +快速验证API接口的基本连通性和功能。 + +**使用方法:** +```bash +# 使用默认服务器地址 +python tests/api/simple_api_test.py + +# 使用自定义服务器地址 +python tests/api/simple_api_test.py http://localhost:3000 +``` + +**测试内容:** +- ✅ 应用状态检查 (`GET /`) +- ✅ 发送邮箱验证码 (`POST /auth/send-email-verification`) +- ✅ 用户注册 (`POST /auth/register`) +- ✅ 用户登录 (`POST /auth/login`) +- ✅ 无效登录测试 +- ✅ 管理员登录测试 + +### 2. `api_test.py` - 完整版测试 +全面的API接口测试,包括边界条件和错误处理。 + +**使用方法:** +```bash +# 基础测试 +python tests/api/api_test.py + +# 详细日志 +python tests/api/api_test.py --verbose + +# 自定义参数 +python tests/api/api_test.py --base-url http://localhost:3000 --test-email custom@example.com +``` + +**测试内容:** +- 应用状态接口测试 +- 用户认证功能测试 +- 管理员功能测试 +- 用户状态管理测试 +- 错误处理测试 +- 频率限制测试 + +## 测试结果示例 + +### 成功的测试输出 +``` +[18:28:55] 🚀 开始基础API测试... +[18:28:55] 测试目标: https://whaletownend.xinghangee.icu +[18:28:55] ✅ 应用状态检查 +[18:28:55] 状态码: 200 +[18:28:55] 响应: Pixel Game Server is running! +[18:28:55] ✅ 发送邮箱验证码 +[18:28:55] 状态码: 200 +[18:28:55] 📧 获取到验证码: 046189 +[18:28:55] ✅ 用户注册 +[18:28:55] 状态码: 201 +[18:28:55] 🎯 基础测试完成! +``` + +### API接口验证结果 + +根据测试结果,以下接口已验证可用: + +#### ✅ 可用接口 +1. **应用状态** - `GET /` + - 状态码: 200 + - 响应: "Pixel Game Server is running!" + +2. **发送邮箱验证码** - `POST /auth/send-email-verification` + - 状态码: 200 + - 功能: 正常发送验证码 + - 频率限制: 1分钟内限制重复发送 + +3. **用户注册** - `POST /auth/register` + - 状态码: 201 (成功) / 400 (参数错误) + - 需要邮箱验证码 + - 支持完整的参数验证 + +4. **用户登录** - `POST /auth/login` + - 状态码: 200 + - 支持用户名/邮箱登录 + - 正确的错误处理 + +#### ❌ 不可用接口 +1. **管理员登录** - `POST /admin/auth/login` + - 状态码: 404 + - 错误: "Cannot POST /admin/auth/login" + - 可能的原因: 路径不正确或功能未实现 + +## 测试发现的问题 + +### 1. 管理员接口路径问题 +- 文档中的 `/admin/auth/login` 返回404 +- 需要确认正确的管理员登录路径 + +### 2. 频率限制功能正常 +- 验证码发送有1分钟的频率限制 +- 错误消息清晰:"请等待 XX 秒后再试" + +### 3. 参数验证严格 +- 注册时必须提供邮箱验证码 +- 错误消息准确:"提供邮箱时必须提供邮箱验证码" + +## 建议的测试流程 + +### 开发环境测试 +1. 运行简化版测试验证基本功能 +2. 检查所有接口的连通性 +3. 验证错误处理是否正确 + +### 生产环境测试 +1. 使用完整版测试进行全面验证 +2. 测试所有边界条件 +3. 验证安全功能(频率限制、权限控制) + +## 扩展测试 + +### 添加新的测试用例 +1. 在对应的测试脚本中添加新的测试方法 +2. 遵循现有的测试模式 +3. 添加适当的错误处理 + +### 自定义测试参数 +```python +# 修改测试配置 +tester = SimpleAPITester("http://your-server.com") +tester.run_basic_tests() +``` + +## 注意事项 + +1. **频率限制**: 测试时注意API的频率限制,避免被限制 +2. **测试数据**: 使用时间戳生成唯一的测试数据 +3. **网络超时**: 设置合适的请求超时时间 +4. **错误处理**: 测试脚本包含完整的错误处理逻辑 + +## 依赖要求 + +```bash +pip install requests +``` + +测试脚本使用Python标准库和requests库,无需额外依赖。 \ No newline at end of file diff --git a/tests/api/api_test.py b/tests/api/api_test.py new file mode 100644 index 0000000..fa4bcfb --- /dev/null +++ b/tests/api/api_test.py @@ -0,0 +1,506 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +API接口测试脚本 + +根据 docs/api-documentation.md 文档自动测试所有API接口 +支持完整的功能测试、边界条件测试和错误处理测试 + +使用方法: + python tests/api/api_test.py + python tests/api/api_test.py --base-url http://localhost:3000 + python tests/api/api_test.py --verbose --test-email custom@example.com +""" + +import requests +import json +import time +import argparse +import sys +from datetime import datetime +from typing import Dict, Any, Optional, List +import uuid + + +class APITester: + """API接口测试类""" + + def __init__(self, base_url: str = "https://whaletownend.xinghangee.icu", + test_email: str = "test@example.com", verbose: bool = False): + self.base_url = base_url.rstrip('/') + self.test_email = test_email + self.verbose = verbose + self.session = requests.Session() + self.session.headers.update({ + 'Content-Type': 'application/json', + 'User-Agent': 'whaleTown-API-Tester/1.0' + }) + + # 测试数据存储 + self.test_data = { + 'admin_token': None, + 'user_token': None, + 'test_user_id': None, + 'verification_code': None + } + + # 测试结果统计 + self.stats = { + 'total': 0, + 'passed': 0, + 'failed': 0, + 'errors': [] + } + + def log(self, message: str, level: str = "INFO"): + """日志输出""" + timestamp = datetime.now().strftime("%H:%M:%S") + if level == "ERROR" or self.verbose: + print(f"[{timestamp}] {level}: {message}") + + def make_request(self, method: str, endpoint: str, data: Dict = None, + headers: Dict = None, expected_status: int = None) -> Dict: + """发送HTTP请求""" + url = f"{self.base_url}{endpoint}" + request_headers = self.session.headers.copy() + if headers: + request_headers.update(headers) + + try: + if method.upper() == 'GET': + response = self.session.get(url, headers=request_headers, timeout=30) + elif method.upper() == 'POST': + response = self.session.post(url, json=data, headers=request_headers, timeout=30) + elif method.upper() == 'PUT': + response = self.session.put(url, json=data, headers=request_headers, timeout=30) + else: + raise ValueError(f"不支持的HTTP方法: {method}") + + # 检查状态码 + if expected_status and response.status_code != expected_status: + self.log(f"状态码不匹配: 期望 {expected_status}, 实际 {response.status_code}", "ERROR") + + # 尝试解析JSON + try: + result = response.json() + except json.JSONDecodeError: + result = {"raw_response": response.text, "status_code": response.status_code} + + result['status_code'] = response.status_code + return result + + except requests.exceptions.RequestException as e: + self.log(f"请求失败: {str(e)}", "ERROR") + return {"error": str(e), "status_code": 0} + + def assert_response(self, response: Dict, expected_success: bool = True, + expected_fields: List[str] = None, test_name: str = ""): + """验证响应结果""" + self.stats['total'] += 1 + + try: + # 检查基本结构 + if 'error' in response: + raise AssertionError(f"请求错误: {response['error']}") + + # 检查success字段 + if 'success' in response: + if response['success'] != expected_success: + raise AssertionError(f"success字段不匹配: 期望 {expected_success}, 实际 {response['success']}") + + # 检查必需字段 + if expected_fields: + for field in expected_fields: + if field not in response: + raise AssertionError(f"缺少必需字段: {field}") + + self.stats['passed'] += 1 + self.log(f"✅ {test_name} - 通过") + return True + + except AssertionError as e: + self.stats['failed'] += 1 + error_msg = f"❌ {test_name} - 失败: {str(e)}" + self.log(error_msg, "ERROR") + self.stats['errors'].append(error_msg) + return False + + def test_app_status(self): + """测试应用状态接口""" + self.log("开始测试应用状态接口...") + + response = self.make_request('GET', '/') + + # 应用状态接口可能返回不同的字段结构 + if response.get('status_code') == 200: + # 检查是否有基本的状态信息 + has_status_info = any(key in response for key in ['service', 'status', 'version', 'timestamp']) + if has_status_info: + self.stats['total'] += 1 + self.stats['passed'] += 1 + self.log(f"✅ 获取应用状态 - 通过") + else: + self.stats['total'] += 1 + self.stats['failed'] += 1 + error_msg = f"❌ 获取应用状态 - 失败: 响应格式不正确" + self.log(error_msg, "ERROR") + self.stats['errors'].append(error_msg) + else: + self.stats['total'] += 1 + self.stats['failed'] += 1 + error_msg = f"❌ 获取应用状态 - 失败: HTTP状态码 {response.get('status_code')}" + self.log(error_msg, "ERROR") + self.stats['errors'].append(error_msg) + + def test_send_verification_code(self) -> Optional[str]: + """测试发送邮箱验证码""" + self.log("开始测试发送邮箱验证码...") + + response = self.make_request('POST', '/auth/send-email-verification', { + 'email': self.test_email + }) + + # 测试模式下返回206状态码,success为false但有验证码 + if response.get('status_code') == 206: + if 'data' in response and 'verification_code' in response['data']: + verification_code = response['data']['verification_code'] + self.test_data['verification_code'] = verification_code + self.log(f"获取到验证码: {verification_code}") + self.stats['total'] += 1 + self.stats['passed'] += 1 + self.log(f"✅ 发送邮箱验证码(测试模式) - 通过") + return verification_code + elif response.get('success') == True: + # 正常模式 + self.assert_response( + response, + expected_success=True, + expected_fields=['data'], + test_name="发送邮箱验证码" + ) + if 'data' in response and 'verification_code' in response['data']: + return response['data']['verification_code'] + + # 测试失败 + self.stats['total'] += 1 + self.stats['failed'] += 1 + error_msg = f"❌ 发送邮箱验证码 - 失败: 无法获取验证码" + self.log(error_msg, "ERROR") + self.stats['errors'].append(error_msg) + return None + + def test_user_register(self) -> Optional[str]: + """测试用户注册""" + self.log("开始测试用户注册...") + + # 先获取验证码 + verification_code = self.test_send_verification_code() + if not verification_code: + self.log("无法获取验证码,跳过注册测试", "ERROR") + return None + + # 生成唯一用户名 + username = f"testuser_{int(time.time())}" + + response = self.make_request('POST', '/auth/register', { + 'username': username, + 'password': 'Test123456', + 'nickname': '测试用户', + 'email': self.test_email + }) + + if self.assert_response( + response, + expected_success=True, + expected_fields=['data'], + test_name="用户注册" + ): + if 'data' in response and 'user' in response['data']: + user_id = response['data']['user'].get('id') + self.test_data['test_user_id'] = user_id + self.log(f"注册成功,用户ID: {user_id}") + + # 保存用户token + if 'access_token' in response['data']: + self.test_data['user_token'] = response['data']['access_token'] + + return user_id + + return None + + def test_user_login(self): + """测试用户登录""" + self.log("开始测试用户登录...") + + # 使用已注册的用户登录 + username = f"testuser_{int(time.time() - 1)}" # 使用之前的用户名 + + response = self.make_request('POST', '/auth/login', { + 'identifier': username, + 'password': 'Test123456' + }) + + # 如果用户不存在,尝试注册后登录 + if not response.get('success'): + self.log("用户不存在,先注册用户...") + self.test_user_register() + + # 重新尝试登录 + response = self.make_request('POST', '/auth/login', { + 'identifier': username, + 'password': 'Test123456' + }) + + self.assert_response( + response, + expected_success=True, + expected_fields=['data'], + test_name="用户登录" + ) + + def test_admin_login(self) -> Optional[str]: + """测试管理员登录""" + self.log("开始测试管理员登录...") + + # 尝试不同的管理员登录路径 + admin_endpoints = ['/admin/auth/login', '/admin/login'] + + for endpoint in admin_endpoints: + response = self.make_request('POST', endpoint, { + 'identifier': 'admin', + 'password': 'Admin123456' + }) + + if response.get('status_code') != 404: + if self.assert_response( + response, + expected_success=True, + expected_fields=['data'], + test_name=f"管理员登录({endpoint})" + ): + if 'data' in response and 'access_token' in response['data']: + admin_token = response['data']['access_token'] + self.test_data['admin_token'] = admin_token + self.log("管理员登录成功") + return admin_token + break + + # 如果所有端点都失败 + self.stats['total'] += 1 + self.stats['failed'] += 1 + error_msg = f"❌ 管理员登录 - 失败: 所有管理员登录端点都不可用" + self.log(error_msg, "ERROR") + self.stats['errors'].append(error_msg) + return None + + def test_admin_get_users(self): + """测试获取用户列表""" + self.log("开始测试获取用户列表...") + + admin_token = self.test_data.get('admin_token') + if not admin_token: + admin_token = self.test_admin_login() + + if not admin_token: + self.log("无管理员token,跳过用户列表测试", "ERROR") + return + + response = self.make_request('GET', '/admin/users?limit=10&offset=0', + headers={'Authorization': f'Bearer {admin_token}'}) + + self.assert_response( + response, + expected_success=True, + expected_fields=['data'], + test_name="获取用户列表" + ) + + def test_user_status_management(self): + """测试用户状态管理""" + self.log("开始测试用户状态管理...") + + admin_token = self.test_data.get('admin_token') + user_id = self.test_data.get('test_user_id') + + if not admin_token: + admin_token = self.test_admin_login() + + if not user_id: + user_id = self.test_user_register() + + if not admin_token or not user_id: + self.log("缺少必要数据,跳过用户状态管理测试", "ERROR") + return + + # 测试修改用户状态 + response = self.make_request('PUT', f'/admin/users/{user_id}/status', + data={ + 'status': 'locked', + 'reason': '测试锁定' + }, + headers={'Authorization': f'Bearer {admin_token}'}) + + self.assert_response( + response, + expected_success=True, + expected_fields=['data'], + test_name="修改用户状态" + ) + + # 测试用户状态统计 + response = self.make_request('GET', '/admin/users/status-stats', + headers={'Authorization': f'Bearer {admin_token}'}) + + self.assert_response( + response, + expected_success=True, + expected_fields=['data'], + test_name="用户状态统计" + ) + + def test_error_cases(self): + """测试错误情况""" + self.log("开始测试错误情况...") + + # 测试无效登录 + response = self.make_request('POST', '/auth/login', { + 'identifier': 'nonexistent', + 'password': 'wrongpassword' + }) + + self.assert_response( + response, + expected_success=False, + expected_fields=['error_code'], + test_name="无效用户登录" + ) + + # 测试无效验证码 + response = self.make_request('POST', '/auth/verify-email', { + 'email': self.test_email, + 'verification_code': '000000' + }) + + self.assert_response( + response, + expected_success=False, + test_name="无效验证码验证" + ) + + # 测试无权限访问管理员接口 + response = self.make_request('GET', '/admin/users') + + # 应该返回401或403 + if response.get('status_code') in [401, 403]: + self.assert_response( + response, + expected_success=False, + test_name="无权限访问管理员接口" + ) + + def test_rate_limiting(self): + """测试频率限制""" + self.log("开始测试频率限制...") + + # 快速连续发送验证码请求 + for i in range(3): + response = self.make_request('POST', '/auth/send-email-verification', { + 'email': f'ratelimit{i}@example.com' + }) + + if response.get('status_code') == 429: + self.assert_response( + response, + expected_success=False, + test_name="频率限制触发" + ) + break + + time.sleep(0.1) # 短暂延迟 + + def run_all_tests(self): + """运行所有测试""" + self.log("🚀 开始API接口测试...") + self.log(f"测试目标: {self.base_url}") + self.log(f"测试邮箱: {self.test_email}") + + start_time = time.time() + + try: + # 基础功能测试 + self.test_app_status() + + # 认证相关测试 + self.test_send_verification_code() + self.test_user_register() + self.test_user_login() + + # 管理员功能测试 + self.test_admin_login() + self.test_admin_get_users() + self.test_user_status_management() + + # 错误处理测试 + self.test_error_cases() + + # 安全功能测试 + self.test_rate_limiting() + + except KeyboardInterrupt: + self.log("测试被用户中断", "ERROR") + except Exception as e: + self.log(f"测试过程中发生异常: {str(e)}", "ERROR") + + # 输出测试结果 + end_time = time.time() + duration = end_time - start_time + + print("\n" + "="*60) + print("📊 测试结果统计") + print("="*60) + print(f"总测试数: {self.stats['total']}") + print(f"通过数量: {self.stats['passed']}") + print(f"失败数量: {self.stats['failed']}") + print(f"成功率: {(self.stats['passed']/self.stats['total']*100):.1f}%" if self.stats['total'] > 0 else "0%") + print(f"测试耗时: {duration:.2f}秒") + + if self.stats['errors']: + print("\n❌ 失败的测试:") + for error in self.stats['errors']: + print(f" {error}") + + if self.stats['failed'] == 0: + print("\n🎉 所有测试通过!") + return True + else: + print(f"\n⚠️ 有 {self.stats['failed']} 个测试失败") + return False + + +def main(): + """主函数""" + parser = argparse.ArgumentParser(description='whaleTown API接口测试工具') + parser.add_argument('--base-url', default='https://whaletownend.xinghangee.icu', + help='API服务器地址 (默认: https://whaletownend.xinghangee.icu)') + parser.add_argument('--test-email', default='test@example.com', + help='测试邮箱地址 (默认: test@example.com)') + parser.add_argument('--verbose', '-v', action='store_true', + help='显示详细日志') + + args = parser.parse_args() + + # 创建测试器并运行测试 + tester = APITester( + base_url=args.base_url, + test_email=args.test_email, + verbose=args.verbose + ) + + success = tester.run_all_tests() + + # 根据测试结果设置退出码 + sys.exit(0 if success else 1) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tests/api/simple_api_test.py b/tests/api/simple_api_test.py new file mode 100644 index 0000000..1fca453 --- /dev/null +++ b/tests/api/simple_api_test.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +简化版API接口测试脚本 + +快速验证API接口的基本连通性和功能 +""" + +import requests +import json +import time +from datetime import datetime + + +class SimpleAPITester: + def __init__(self, base_url="https://whaletownend.xinghangee.icu"): + self.base_url = base_url.rstrip('/') + self.session = requests.Session() + self.session.headers.update({ + 'Content-Type': 'application/json', + 'User-Agent': 'whaleTown-Simple-Tester/1.0' + }) + + def log(self, message): + timestamp = datetime.now().strftime("%H:%M:%S") + print(f"[{timestamp}] {message}") + + def test_endpoint(self, method, endpoint, data=None, description=""): + """测试单个端点""" + url = f"{self.base_url}{endpoint}" + + try: + if method.upper() == 'GET': + response = self.session.get(url, timeout=10) + elif method.upper() == 'POST': + response = self.session.post(url, json=data, timeout=10) + + self.log(f"✅ {description}") + self.log(f" 状态码: {response.status_code}") + + try: + result = response.json() + self.log(f" 响应: {json.dumps(result, ensure_ascii=False, indent=2)[:200]}...") + return result + except: + self.log(f" 响应: {response.text[:100]}...") + return {"status_code": response.status_code, "text": response.text} + + except Exception as e: + self.log(f"❌ {description} - 错误: {str(e)}") + return None + + def run_basic_tests(self): + """运行基础测试""" + self.log("🚀 开始基础API测试...") + self.log(f"测试目标: {self.base_url}") + + # 1. 测试应用状态 + self.test_endpoint('GET', '/', description="应用状态检查") + + # 2. 测试发送验证码 + result = self.test_endpoint('POST', '/auth/send-email-verification', + {'email': 'test@example.com'}, + "发送邮箱验证码") + + # 3. 如果获取到验证码,测试注册 + if result and 'data' in result and 'verification_code' in result['data']: + verification_code = result['data']['verification_code'] + self.log(f"📧 获取到验证码: {verification_code}") + + # 测试用户注册(需要验证码) + username = f"testuser_{int(time.time())}" + self.test_endpoint('POST', '/auth/register', { + 'username': username, + 'password': 'Test123456', + 'nickname': '测试用户', + 'email': 'test@example.com', + 'email_verification_code': verification_code # 添加验证码 + }, "用户注册") + + # 测试用户登录 + self.test_endpoint('POST', '/auth/login', { + 'identifier': username, + 'password': 'Test123456' + }, "用户登录") + + # 4. 测试错误情况 + self.test_endpoint('POST', '/auth/login', { + 'identifier': 'nonexistent', + 'password': 'wrongpassword' + }, "无效登录测试") + + # 5. 测试管理员登录(可能失败) + self.test_endpoint('POST', '/admin/auth/login', { + 'identifier': 'admin', + 'password': 'Admin123456' + }, "管理员登录测试") + + self.log("🎯 基础测试完成!") + + +if __name__ == '__main__': + import sys + + base_url = sys.argv[1] if len(sys.argv) > 1 else "https://whaletownend.xinghangee.icu" + + tester = SimpleAPITester(base_url) + tester.run_basic_tests() \ No newline at end of file diff --git a/tests/auth/README.md b/tests/auth/README.md new file mode 100644 index 0000000..817e81f --- /dev/null +++ b/tests/auth/README.md @@ -0,0 +1,165 @@ +# 认证UI测试 + +这个文件夹包含了登录和注册界面的UI响应测试,具有美观优雅的反馈系统。 + +## 文件说明 + +- `auth_ui_test.tscn` - 测试场景文件 +- `auth_ui_test.gd` - 测试脚本 +- `README.md` - 本说明文件 + +## 如何运行测试 + +### 方法1:在Godot编辑器中运行 +1. 在Godot编辑器中打开 `tests/auth/auth_ui_test.tscn` 文件 +2. 点击编辑器右上角的"播放场景"按钮(或按F6键) +3. 如果仍然跳转到主场景,请尝试以下步骤: + - 确保当前打开的是测试场景文件 + - 在场景面板中右键点击根节点,选择"Change Scene" + - 或者在项目设置中临时更改主场景为测试场景 + +### 方法2:通过代码运行 +在任何脚本中添加以下代码来切换到测试场景: +```gdscript +get_tree().change_scene_to_file("res://tests/auth/auth_ui_test.tscn") +``` + +## 界面布局 + +### 左侧:认证界面 +- 完整的登录和注册界面 +- 实时表单验证 +- Toast消息显示 +- 字段错误提示 + +### 右侧:测试控制面板 +#### 🧪 UI响应测试标题 +#### 📋 测试反馈面板 +- **智能反馈系统**:实时显示测试执行状态 +- **颜色编码**: + - 🔵 蓝色:信息提示 + - 🟢 绿色:成功响应 + - 🔴 红色:错误响应 +- **详细信息**: + - 测试名称和类型 + - HTTP状态码 + - 响应消息和错误代码 + - 预期的UI反馈效果 +- **自动滚动**:新消息自动滚动到可见区域 + +#### 🎯 测试用例按钮 +16个精美设计的测试按钮,按功能分组: + +## 测试功能 + +### 登录测试(4个) +- ✅ **登录成功**(状态码200) + - 预期:绿色Toast "登录成功!正在进入鲸鱼镇...",跳转到主场景 +- ❌ **用户名或密码错误**(状态码401) + - 预期:红色Toast "用户名或密码错误,请检查后重试" +- ❌ **用户不存在**(状态码404) + - 预期:红色Toast "用户不存在,请先注册" +- ❌ **参数错误**(状态码400) + - 预期:红色Toast,根据具体错误显示 + +### 发送验证码测试(5个) +- ✅ **发送成功**(状态码200) + - 预期:绿色Toast "验证码已发送到您的邮箱" +- ❌ **邮箱格式错误**(状态码400) + - 预期:红色Toast "请输入有效的邮箱地址" +- ❌ **请求过频**(状态码429) + - 预期:红色Toast "请求过于频繁,请稍后再试" +- ❌ **服务器错误**(状态码500) + - 预期:红色Toast "服务器繁忙,请稍后再试" +- ❌ **网络错误**(状态码0) + - 预期:红色Toast "网络连接失败,请检查网络连接" + +### 邮箱验证测试(3个) +- ✅ **验证成功**(状态码200) + - 预期:绿色Toast "邮箱验证成功,正在注册..." +- ❌ **验证码错误**(状态码400) + - 预期:红色Toast "验证码错误或已过期" +- ❌ **验证码不存在**(状态码404) + - 预期:红色Toast "请先获取验证码" + +### 注册测试(6个) +- ✅ **注册成功**(状态码201) + - 预期:绿色Toast "注册成功!欢迎加入鲸鱼镇",切换到登录界面 +- ❌ **参数错误**(状态码400) + - 预期:红色Toast,根据具体错误显示 +- ❌ **缺少验证码**(状态码400) + - 预期:红色Toast "请先获取并输入邮箱验证码" +- ❌ **用户已存在**(状态码409) + - 预期:红色Toast "用户名或邮箱已被使用,请换一个" +- ❌ **请求过频**(状态码429) + - 预期:红色Toast "注册请求过于频繁,请稍后再试" +- ❌ **服务器错误**(状态码500) + - 预期:红色Toast "注册失败,请稍后再试" + +## 测试步骤 + +1. **启动测试场景** + - 运行测试场景后,右侧反馈面板会显示欢迎信息和使用说明 + +2. **准备测试数据** + - 在左侧认证界面切换到注册界面 + - 填写测试数据: + - 用户名:testuser + - 邮箱:test@example.com + - 密码:password123 + - 确认密码:password123 + - 验证码:123456 + +3. **执行测试** + - 点击右侧对应的测试按钮 + - 观察反馈面板中的详细信息: + - 🚀 测试开始提示 + - 📡 请求类型和状态码 + - ⚡ HTTP响应模拟 + - 🎯 预期UI反馈说明 + - ✅/❌ 测试完成状态 + +4. **观察UI反馈** + - 左侧界面的实际反馈效果 + - Toast消息的颜色和内容 + - 字段错误提示的显示 + - 按钮状态的变化 + +## 反馈系统特色 + +### 🎨 美观设计 +- 使用Emoji图标增强视觉效果 +- 颜色编码区分不同状态 +- 圆角边框和阴影效果 +- 按钮分组和分隔符 + +### 📊 详细分析 +- 实时显示测试执行过程 +- 预期vs实际效果对比 +- HTTP响应详细解析 +- 错误代码和消息说明 + +### 🔄 交互体验 +- 自动滚动到最新消息 +- 按钮悬停效果 +- 平滑的颜色过渡 +- 响应式布局 + +## 注意事项 + +- 测试场景会自动断开`login_success`信号,避免跳转到主场景 +- 按ESC键可以优雅退出测试(显示退出提示) +- 所有测试都是模拟的,不会发送真实的网络请求 +- 反馈面板会保留测试历史,便于对比分析 +- 测试按钮按功能分组,每组之间有分隔符 + +## 预期行为验证 + +这个测试系统不仅模拟后端响应,还会告诉你每种情况下应该看到什么UI反馈,帮助你验证: + +1. **Toast消息系统**是否正常工作 +2. **字段验证错误**是否正确显示 +3. **按钮状态管理**是否符合预期 +4. **用户体验流程**是否顺畅 + +通过对比预期效果和实际效果,可以快速发现UI反馈系统中的问题并进行修复。 \ No newline at end of file diff --git a/tests/auth/auth_ui_test.gd b/tests/auth/auth_ui_test.gd new file mode 100644 index 0000000..7930df0 --- /dev/null +++ b/tests/auth/auth_ui_test.gd @@ -0,0 +1,464 @@ +extends Control + +# 认证UI测试场景 +# 用于测试登录和注册界面的各种响应情况 + +@onready var auth_scene: Control = $AuthScene +@onready var test_panel: Panel = $TestPanel +@onready var test_buttons: VBoxContainer = $TestPanel/VBoxContainer/ScrollContainer/TestButtons +@onready var feedback_panel: Panel = $TestPanel/VBoxContainer/FeedbackPanel +@onready var feedback_text: RichTextLabel = $TestPanel/VBoxContainer/FeedbackPanel/FeedbackContainer/FeedbackScroll/FeedbackText + +# 反馈样式 - 动态创建 +var feedback_styles = {} + +var test_scenarios = [ + { + "name": "✅ 登录成功", + "type": "login", + "response_code": 200, + "response_data": { + "success": true, + "data": { + "user": { + "id": "123", + "username": "testuser", + "email": "test@example.com" + }, + "token": "jwt_token_here" + }, + "message": "登录成功" + } + }, + { + "name": "❌ 登录失败-用户名或密码错误", + "type": "login", + "response_code": 401, + "response_data": { + "success": false, + "message": "用户名或密码错误", + "error_code": "INVALID_CREDENTIALS" + } + }, + { + "name": "❌ 登录失败-用户不存在", + "type": "login", + "response_code": 404, + "response_data": { + "success": false, + "message": "用户不存在", + "error_code": "USER_NOT_FOUND" + } + }, + { + "name": "❌ 登录失败-参数错误", + "type": "login", + "response_code": 400, + "response_data": { + "success": false, + "message": "用户名格式错误", + "error_code": "INVALID_PARAMETERS" + } + }, + { + "name": "✅ 发送验证码成功", + "type": "send_code", + "response_code": 200, + "response_data": { + "success": true, + "data": {"verification_code": "123456"}, + "message": "验证码已发送" + } + }, + { + "name": "❌ 发送验证码-邮箱格式错误", + "type": "send_code", + "response_code": 400, + "response_data": { + "success": false, + "message": "邮箱格式错误", + "error_code": "INVALID_EMAIL" + } + }, + { + "name": "❌ 发送验证码-请求过频", + "type": "send_code", + "response_code": 429, + "response_data": { + "success": false, + "message": "请求过于频繁", + "error_code": "TOO_MANY_REQUESTS" + } + }, + { + "name": "❌ 发送验证码-服务器错误", + "type": "send_code", + "response_code": 500, + "response_data": { + "success": false, + "message": "服务器内部错误", + "error_code": "INTERNAL_ERROR" + } + }, + { + "name": "❌ 发送验证码-网络错误", + "type": "send_code", + "response_code": 0, + "response_data": {} + }, + { + "name": "✅ 邮箱验证成功", + "type": "verify_email", + "response_code": 200, + "response_data": { + "success": true, + "message": "邮箱验证成功" + } + }, + { + "name": "❌ 邮箱验证-验证码错误", + "type": "verify_email", + "response_code": 400, + "response_data": { + "success": false, + "message": "验证码错误或已过期", + "error_code": "INVALID_CODE" + } + }, + { + "name": "❌ 邮箱验证-验证码不存在", + "type": "verify_email", + "response_code": 404, + "response_data": { + "success": false, + "message": "验证码不存在", + "error_code": "CODE_NOT_FOUND" + } + }, + { + "name": "✅ 注册成功", + "type": "register", + "response_code": 201, + "response_data": { + "success": true, + "data": { + "user": { + "id": "123", + "username": "testuser", + "email": "test@example.com" + } + }, + "message": "注册成功" + } + }, + { + "name": "❌ 注册失败-参数错误", + "type": "register", + "response_code": 400, + "response_data": { + "success": false, + "message": "用户名格式错误", + "error_code": "INVALID_USERNAME" + } + }, + { + "name": "❌ 注册失败-缺少验证码", + "type": "register", + "response_code": 400, + "response_data": { + "success": false, + "message": "提供邮箱时必须提供邮箱验证码", + "error_code": "REGISTER_FAILED" + } + }, + { + "name": "❌ 注册失败-用户已存在", + "type": "register", + "response_code": 409, + "response_data": { + "success": false, + "message": "用户名已存在", + "error_code": "USER_EXISTS" + } + }, + { + "name": "❌ 注册失败-请求过频", + "type": "register", + "response_code": 429, + "response_data": { + "success": false, + "message": "注册请求过于频繁,请5分钟后再试", + "error_code": "TOO_MANY_REQUESTS", + "throttle_info": { + "limit": 3, + "window_seconds": 300, + "current_requests": 3, + "reset_time": "2025-12-24T11:26:41.136Z" + } + } + }, + { + "name": "❌ 注册失败-服务器错误", + "type": "register", + "response_code": 500, + "response_data": { + "success": false, + "message": "服务器内部错误", + "error_code": "INTERNAL_ERROR" + } + } +] + +func _ready(): + print("🧪 认证UI测试场景已加载") + + # 初始化反馈样式 + create_feedback_styles() + + # 初始化反馈面板 + show_feedback("🎯 测试场景已准备就绪", "info") + add_feedback_line("💡 使用说明:") + add_feedback_line("1️⃣ 点击下方测试按钮模拟后端响应") + add_feedback_line("2️⃣ 观察左侧认证界面的UI反馈") + add_feedback_line("3️⃣ 按ESC键退出测试") + add_feedback_line("") + + # 确保auth_scene已准备好 + if auth_scene == null: + show_feedback("❌ 错误:无法找到AuthScene节点", "error") + return + + # 等待一帧确保所有节点都已初始化 + await get_tree().process_frame + setup_test_buttons() + + # 断开auth_scene的login_success信号,避免跳转到主场景 + if auth_scene.has_signal("login_success"): + var connections = auth_scene.get_signal_connection_list("login_success") + for connection in connections: + auth_scene.disconnect("login_success", connection.callable) + add_feedback_line("🔗 已断开login_success信号连接") + +# 创建反馈样式 +func create_feedback_styles(): + # 成功样式 - 绿色 + var success_style = StyleBoxFlat.new() + success_style.bg_color = Color(0.2, 0.8, 0.3, 0.9) + success_style.border_color = Color(0.1, 0.6, 0.2, 1) + success_style.border_width_left = 2 + success_style.border_width_top = 2 + success_style.border_width_right = 2 + success_style.border_width_bottom = 2 + success_style.corner_radius_top_left = 8 + success_style.corner_radius_top_right = 8 + success_style.corner_radius_bottom_left = 8 + success_style.corner_radius_bottom_right = 8 + + # 错误样式 - 红色 + var error_style = StyleBoxFlat.new() + error_style.bg_color = Color(0.8, 0.2, 0.2, 0.9) + error_style.border_color = Color(0.6, 0.1, 0.1, 1) + error_style.border_width_left = 2 + error_style.border_width_top = 2 + error_style.border_width_right = 2 + error_style.border_width_bottom = 2 + error_style.corner_radius_top_left = 8 + error_style.corner_radius_top_right = 8 + error_style.corner_radius_bottom_left = 8 + error_style.corner_radius_bottom_right = 8 + + # 信息样式 - 蓝色 + var info_style = StyleBoxFlat.new() + info_style.bg_color = Color(0.2, 0.5, 0.8, 0.9) + info_style.border_color = Color(0.1, 0.4, 0.6, 1) + info_style.border_width_left = 2 + info_style.border_width_top = 2 + info_style.border_width_right = 2 + info_style.border_width_bottom = 2 + info_style.corner_radius_top_left = 8 + info_style.corner_radius_top_right = 8 + info_style.corner_radius_bottom_left = 8 + info_style.corner_radius_bottom_right = 8 + + feedback_styles = { + "success": success_style, + "error": error_style, + "info": info_style + } + +func setup_test_buttons(): + # 创建测试按钮 + for i in range(test_scenarios.size()): + var scenario = test_scenarios[i] + var button = Button.new() + button.text = scenario.name + button.custom_minimum_size.y = 35 + button.pressed.connect(_on_test_button_pressed.bind(scenario)) + + # 为按钮添加样式 + var style_normal = StyleBoxFlat.new() + style_normal.bg_color = Color(0.9, 0.9, 0.9, 1) + style_normal.border_color = Color(0.7, 0.7, 0.7, 1) + style_normal.border_width_left = 1 + style_normal.border_width_top = 1 + style_normal.border_width_right = 1 + style_normal.border_width_bottom = 1 + style_normal.corner_radius_top_left = 4 + style_normal.corner_radius_top_right = 4 + style_normal.corner_radius_bottom_left = 4 + style_normal.corner_radius_bottom_right = 4 + + var style_hover = style_normal.duplicate() + style_hover.bg_color = Color(0.8, 0.8, 0.8, 1) + + button.add_theme_stylebox_override("normal", style_normal) + button.add_theme_stylebox_override("hover", style_hover) + button.add_theme_color_override("font_color", Color(0.2, 0.2, 0.2, 1)) + + test_buttons.add_child(button) + + # 添加分隔符(每4个按钮一组) + if (i + 1) % 4 == 0 and i < test_scenarios.size() - 1: + var separator = HSeparator.new() + separator.custom_minimum_size.y = 10 + test_buttons.add_child(separator) + + add_feedback_line("✨ 已创建 %d 个测试按钮" % test_scenarios.size()) + +func _on_test_button_pressed(scenario: Dictionary): + # 显示测试开始信息 + show_feedback("🚀 执行测试: %s" % scenario.name, "info") + add_feedback_line("📡 请求类型: %s" % scenario.type) + add_feedback_line("📊 状态码: %d" % scenario.response_code) + + # 确保auth_scene存在 + if auth_scene == null: + show_feedback("❌ 错误:AuthScene节点不存在", "error") + return + + # 模拟HTTP响应 + simulate_http_response(scenario) + +func simulate_http_response(scenario: Dictionary): + # 设置当前请求类型 + auth_scene.current_request_type = scenario.type + + # 准备模拟数据 + var response_code = scenario.response_code + var response_data = scenario.response_data + var response_text = JSON.stringify(response_data) + var response_body = response_text.to_utf8_buffer() + var headers = PackedStringArray() + + add_feedback_line("⚡ 模拟HTTP响应...") + + # 模拟HTTP请求完成 + auth_scene._on_http_request_completed(0, response_code, headers, response_body) + + # 分析响应结果 + analyze_response(scenario, response_data) + +func analyze_response(scenario: Dictionary, response_data: Dictionary): + var response_code = scenario.response_code + var is_success = response_code >= 200 and response_code < 300 + + # 显示响应分析 + if is_success: + show_feedback("✅ 测试完成 - 成功响应", "success") + else: + show_feedback("❌ 测试完成 - 错误响应", "error") + + # 显示详细信息 + if response_data.has("message"): + add_feedback_line("💬 响应消息: %s" % response_data.message) + + if response_data.has("error_code"): + add_feedback_line("🏷️ 错误代码: %s" % response_data.error_code) + + if response_data.has("data"): + add_feedback_line("📦 响应数据: %s" % str(response_data.data)) + + # 预期的UI反馈 + var expected_feedback = get_expected_ui_feedback(scenario) + add_feedback_line("🎯 预期UI反馈: %s" % expected_feedback) + + add_feedback_line("─────────────────────") + +func get_expected_ui_feedback(scenario: Dictionary) -> String: + var response_code = scenario.response_code + var type = scenario.type + + match type: + "login": + match response_code: + 200: + return "绿色Toast: 登录成功,跳转到主场景" + 400: + return "红色Toast: 登录信息格式错误" + 401: + return "红色Toast: 用户名或密码错误" + 404: + return "红色Toast: 用户不存在" + 429: + return "红色Toast: 登录请求过于频繁" + 500: + return "红色Toast: 服务器繁忙" + "send_code": + match response_code: + 200, 206: + return "绿色Toast: 验证码已发送" + 400: + return "红色Toast: 邮箱格式错误" + 429: + return "红色Toast: 请求过于频繁" + 500: + return "红色Toast: 服务器繁忙" + 0: + return "红色Toast: 网络连接失败" + "verify_email": + match response_code: + 200: + return "绿色Toast: 邮箱验证成功" + 400: + return "红色Toast: 验证码错误" + 404: + return "红色Toast: 请先获取验证码" + 500: + return "红色Toast: 验证失败" + "register": + match response_code: + 201: + return "绿色Toast: 注册成功,切换到登录界面" + 400: + return "红色Toast: 根据具体错误显示(验证码缺失、格式错误等)" + 409: + return "红色Toast: 用户名或邮箱已存在" + 429: + return "红色Toast: 注册请求过于频繁,请稍后再试" + 500: + return "红色Toast: 注册失败" + + return "未知响应" + +# 反馈面板功能 +func show_feedback(message: String, type: String = "info"): + # 更改面板样式 + if feedback_styles.has(type): + feedback_panel.add_theme_stylebox_override("panel", feedback_styles[type]) + + # 清空并显示新消息 + feedback_text.text = "[b]%s[/b]" % message + +func add_feedback_line(message: String): + feedback_text.text += "\n" + message + # 自动滚动到底部 + await get_tree().process_frame + var scroll = feedback_text.get_parent() + if scroll is ScrollContainer: + scroll.scroll_vertical = scroll.get_v_scroll_bar().max_value + +func _input(event): + if event.is_action_pressed("ui_cancel"): + show_feedback("👋 退出测试场景", "info") + await get_tree().create_timer(0.5).timeout + get_tree().quit() diff --git a/tests/auth/auth_ui_test.gd.uid b/tests/auth/auth_ui_test.gd.uid new file mode 100644 index 0000000..6255f7d --- /dev/null +++ b/tests/auth/auth_ui_test.gd.uid @@ -0,0 +1 @@ +uid://ddb8v5c6aeqe7 diff --git a/tests/auth/auth_ui_test.tscn b/tests/auth/auth_ui_test.tscn new file mode 100644 index 0000000..2cd159e --- /dev/null +++ b/tests/auth/auth_ui_test.tscn @@ -0,0 +1,117 @@ +[gd_scene load_steps=4 format=3 uid="uid://bvn8y7x2qkqxe"] + +[ext_resource type="Script" uid="uid://ddb8v5c6aeqe7" path="res://tests/auth/auth_ui_test.gd" id="1_test_script"] +[ext_resource type="PackedScene" uid="uid://by7m8snb4xllf" path="res://scenes/auth_scene.tscn" id="2_auth_scene"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_feedback_info"] +bg_color = Color(0.2, 0.5, 0.8, 0.9) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.1, 0.4, 0.6, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[node name="AuthUITest" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_test_script") + +[node name="AuthScene" parent="." instance=ExtResource("2_auth_scene")] +layout_mode = 1 + +[node name="TestPanel" type="Panel" parent="."] +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -350.0 +offset_top = 20.0 +offset_right = -20.0 +offset_bottom = 620.0 +grow_horizontal = 0 + +[node name="VBoxContainer" type="VBoxContainer" parent="TestPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 10.0 +offset_top = 10.0 +offset_right = -10.0 +offset_bottom = -10.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="TitleLabel" type="Label" parent="TestPanel/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.2, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 18 +text = "🧪 UI响应测试" +horizontal_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="TestPanel/VBoxContainer"] +layout_mode = 2 + +[node name="FeedbackPanel" type="Panel" parent="TestPanel/VBoxContainer"] +custom_minimum_size = Vector2(0, 120) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_feedback_info") + +[node name="FeedbackContainer" type="VBoxContainer" parent="TestPanel/VBoxContainer/FeedbackPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 10.0 +offset_top = 8.0 +offset_right = -10.0 +offset_bottom = -8.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="FeedbackTitle" type="Label" parent="TestPanel/VBoxContainer/FeedbackPanel/FeedbackContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_font_sizes/font_size = 14 +text = "📋 测试反馈" + +[node name="FeedbackScroll" type="ScrollContainer" parent="TestPanel/VBoxContainer/FeedbackPanel/FeedbackContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="FeedbackText" type="RichTextLabel" parent="TestPanel/VBoxContainer/FeedbackPanel/FeedbackContainer/FeedbackScroll"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_colors/default_color = Color(1, 1, 1, 1) +theme_override_font_sizes/normal_font_size = 12 +bbcode_enabled = true +text = "点击下方测试按钮开始测试..." +fit_content = true +scroll_following = true + +[node name="HSeparator2" type="HSeparator" parent="TestPanel/VBoxContainer"] +layout_mode = 2 + +[node name="ButtonsLabel" type="Label" parent="TestPanel/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.2, 0.2, 0.2, 1) +theme_override_font_sizes/font_size = 14 +text = "🎯 测试用例" +horizontal_alignment = 1 + +[node name="ScrollContainer" type="ScrollContainer" parent="TestPanel/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="TestButtons" type="VBoxContainer" parent="TestPanel/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep new file mode 100644 index 0000000..d1d5dd3 --- /dev/null +++ b/tests/integration/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 集成测试目录 \ No newline at end of file diff --git a/tests/performance/.gitkeep b/tests/performance/.gitkeep new file mode 100644 index 0000000..956129d --- /dev/null +++ b/tests/performance/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 性能测试目录 \ No newline at end of file diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep new file mode 100644 index 0000000..13e2516 --- /dev/null +++ b/tests/unit/.gitkeep @@ -0,0 +1 @@ +# 保持目录结构 - 单元测试目录 \ No newline at end of file