nanobanana初版
0 parents
Showing
12 changed files
with
825 additions
and
0 deletions
AGENTS.md
0 → 100644
| 1 | <!-- OPENSPEC:START --> | ||
| 2 | # OpenSpec Instructions | ||
| 3 | |||
| 4 | These instructions are for AI assistants working in this project. | ||
| 5 | |||
| 6 | Always open `@/openspec/AGENTS.md` when the request: | ||
| 7 | - Mentions planning or proposals (words like proposal, spec, change, plan) | ||
| 8 | - Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work | ||
| 9 | - Sounds ambiguous and you need the authoritative spec before coding | ||
| 10 | |||
| 11 | Use `@/openspec/AGENTS.md` to learn: | ||
| 12 | - How to create and apply change proposals | ||
| 13 | - Spec format and conventions | ||
| 14 | - Project structure and guidelines | ||
| 15 | |||
| 16 | Keep this managed block so 'openspec update' can refresh the instructions. | ||
| 17 | |||
| 18 | <!-- OPENSPEC:END --> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
CLAUDE.md
0 → 100644
| 1 | <!-- OPENSPEC:START --> | ||
| 2 | # OpenSpec Instructions | ||
| 3 | |||
| 4 | These instructions are for AI assistants working in this project. | ||
| 5 | |||
| 6 | Always open `@/openspec/AGENTS.md` when the request: | ||
| 7 | - Mentions planning or proposals (words like proposal, spec, change, plan) | ||
| 8 | - Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work | ||
| 9 | - Sounds ambiguous and you need the authoritative spec before coding | ||
| 10 | |||
| 11 | Use `@/openspec/AGENTS.md` to learn: | ||
| 12 | - How to create and apply change proposals | ||
| 13 | - Spec format and conventions | ||
| 14 | - Project structure and guidelines | ||
| 15 | |||
| 16 | Keep this managed block so 'openspec update' can refresh the instructions. | ||
| 17 | |||
| 18 | <!-- OPENSPEC:END --> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
README.md
0 → 100644
| 1 | # Gemini Image Generator | ||
| 2 | |||
| 3 | 简洁的跨平台桌面应用,使用 Google Gemini API 生成图片。 | ||
| 4 | |||
| 5 | ## 功能特性 | ||
| 6 | |||
| 7 | - **用户认证**: 基于 MySQL 的登录系统,确保只有授权用户可访问 | ||
| 8 | - 输入文本 Prompt 生成图片 | ||
| 9 | - 支持上传多张参考图片 | ||
| 10 | - 多种图片比例选择(1:1, 16:9, 4:3 等) | ||
| 11 | - 多种分辨率选择(1K, 2K, 4K) | ||
| 12 | - 支持两种模型:gemini-2.5-flash-image 和 gemini-3-pro-image-preview | ||
| 13 | - 实时预览生成的图片 | ||
| 14 | - 一键下载生成的图片 | ||
| 15 | - 跨平台支持(Windows 和 macOS) | ||
| 16 | |||
| 17 | ## 系统要求 | ||
| 18 | |||
| 19 | - Python 3.8 或更高版本 | ||
| 20 | - 有效的 Google AI API 密钥 | ||
| 21 | - MySQL 数据库访问权限(用于用户认证) | ||
| 22 | |||
| 23 | ## 快速开始 | ||
| 24 | |||
| 25 | ### 开发模式运行 | ||
| 26 | |||
| 27 | 1. 安装依赖: | ||
| 28 | ```bash | ||
| 29 | pip install -r requirements.txt | ||
| 30 | ``` | ||
| 31 | |||
| 32 | 2. 配置数据库和 API 密钥: | ||
| 33 | - 编辑 `config.json` 文件,配置以下字段: | ||
| 34 | ```json | ||
| 35 | { | ||
| 36 | "api_key": "你的Google AI API密钥", | ||
| 37 | "db_config": { | ||
| 38 | "host": "你的MySQL主机地址", | ||
| 39 | "port": 3306, | ||
| 40 | "user": "数据库用户名", | ||
| 41 | "password": "数据库密码", | ||
| 42 | "database": "数据库名", | ||
| 43 | "table": "nano_banana_users" | ||
| 44 | }, | ||
| 45 | "last_user": "" | ||
| 46 | } | ||
| 47 | ``` | ||
| 48 | |||
| 49 | 3. 创建数据库表: | ||
| 50 | ```sql | ||
| 51 | CREATE TABLE `nano_banana_users` ( | ||
| 52 | `user_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, | ||
| 53 | `passwd` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, | ||
| 54 | `status` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL | ||
| 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | ||
| 56 | ``` | ||
| 57 | |||
| 58 | 4. 添加初始用户: | ||
| 59 | ```bash | ||
| 60 | python user_util.py add admin your_password | ||
| 61 | ``` | ||
| 62 | |||
| 63 | 5. 运行应用: | ||
| 64 | ```bash | ||
| 65 | python image_generator.py | ||
| 66 | ``` | ||
| 67 | |||
| 68 | 首次运行会显示登录界面,使用创建的账户登录。 | ||
| 69 | |||
| 70 | ### 打包为可执行文件 | ||
| 71 | |||
| 72 | #### Windows | ||
| 73 | |||
| 74 | 双击运行 `build_windows.bat` 或在命令行执行: | ||
| 75 | ```cmd | ||
| 76 | build_windows.bat | ||
| 77 | ``` | ||
| 78 | |||
| 79 | 生成的可执行文件位于 `dist\GeminiImageGenerator.exe` | ||
| 80 | |||
| 81 | #### macOS | ||
| 82 | |||
| 83 | 在终端执行: | ||
| 84 | ```bash | ||
| 85 | chmod +x build_mac.sh | ||
| 86 | ./build_mac.sh | ||
| 87 | ``` | ||
| 88 | |||
| 89 | 生成的应用程序位于 `dist/GeminiImageGenerator.app` | ||
| 90 | |||
| 91 | ## 配置文件位置 | ||
| 92 | |||
| 93 | 应用会根据运行模式自动选择配置文件位置: | ||
| 94 | |||
| 95 | **开发模式**(直接运行 Python): | ||
| 96 | - 配置文件:`./config.json`(当前目录) | ||
| 97 | |||
| 98 | **打包后的应用**: | ||
| 99 | - **macOS**: `~/Library/Application Support/ZB100ImageGenerator/config.json` | ||
| 100 | - **Windows**: `%APPDATA%\ZB100ImageGenerator\config.json` | ||
| 101 | - **Linux**: `~/.config/zb100imagegenerator/config.json` | ||
| 102 | |||
| 103 | 首次运行打包应用时,会自动从打包的模板复制 API 密钥到用户目录。 | ||
| 104 | |||
| 105 | ## 用户管理 | ||
| 106 | |||
| 107 | 应用内置用户管理工具 `user_util.py`(仅管理员使用,不随客户端分发)。 | ||
| 108 | |||
| 109 | ### 用户管理命令 | ||
| 110 | |||
| 111 | ```bash | ||
| 112 | # 添加新用户 | ||
| 113 | python user_util.py add <username> <password> | ||
| 114 | |||
| 115 | # 列出所有用户 | ||
| 116 | python user_util.py list | ||
| 117 | |||
| 118 | # 禁用用户 | ||
| 119 | python user_util.py disable <username> | ||
| 120 | |||
| 121 | # 启用用户 | ||
| 122 | python user_util.py enable <username> | ||
| 123 | |||
| 124 | # 重置密码 | ||
| 125 | python user_util.py reset <username> <new_password> | ||
| 126 | ``` | ||
| 127 | |||
| 128 | ### 示例 | ||
| 129 | |||
| 130 | ```bash | ||
| 131 | # 添加管理员账户 | ||
| 132 | python user_util.py add admin MySecurePass123 | ||
| 133 | |||
| 134 | # 查看所有用户 | ||
| 135 | python user_util.py list | ||
| 136 | |||
| 137 | # 禁用某个用户 | ||
| 138 | python user_util.py disable testuser | ||
| 139 | |||
| 140 | # 重置用户密码 | ||
| 141 | python user_util.py reset admin NewPassword456 | ||
| 142 | ``` | ||
| 143 | |||
| 144 | ### 安全说明 | ||
| 145 | |||
| 146 | - 密码使用 SHA256 哈希存储,数据库和本地均不保存明文 | ||
| 147 | - 所有数据库操作使用参数化查询,防止 SQL 注入 | ||
| 148 | - user_util.py 工具仅供管理员使用,不应分发给普通用户 | ||
| 149 | - 应用会记住上次登录的用户名(可选),但不会保存密码 | ||
| 150 | |||
| 151 | ## 使用说明 | ||
| 152 | |||
| 153 | 1. **登录应用** | ||
| 154 | - 应用启动时显示登录界面 | ||
| 155 | - 输入用户名和密码 | ||
| 156 | - 可勾选"记住用户名"选项,下次自动填充用户名 | ||
| 157 | - 登录成功后进入主界面 | ||
| 158 | |||
| 159 | 2. **配置 API 密钥** | ||
| 160 | - 编辑配置文件中的 `api_key` 字段 | ||
| 161 | - 或通过应用界面的收藏提示词功能自动保存 | ||
| 162 | |||
| 163 | 3. **上传参考图片(可选)** | ||
| 164 | - 点击 "添加图片" 按钮选择一张或多张参考图片 | ||
| 165 | - 这些图片将作为生成图片的参考 | ||
| 166 | - 可以单独删除每张图片 | ||
| 167 | |||
| 168 | 3. **输入 Prompt** | ||
| 169 | - 在提示词文本框中输入你想生成的图片描述 | ||
| 170 | - 可以使用 "收藏" 功能保存常用提示词 | ||
| 171 | - 从下拉菜单快速选择已保存的提示词 | ||
| 172 | |||
| 173 | 4. **选择生成参数** | ||
| 174 | - **宽高比**: 选择图片的宽高比(1:1, 16:9 等) | ||
| 175 | - **图片尺寸**: 选择图片的分辨率(1K/2K/4K) | ||
| 176 | - **AI 模型**: 选择使用的模型 | ||
| 177 | |||
| 178 | 5. **生成图片** | ||
| 179 | - 点击 "生成图片" 按钮 | ||
| 180 | - 等待生成完成,生成的图片会显示在预览区域 | ||
| 181 | - 双击预览图可用系统默认查看器打开 | ||
| 182 | |||
| 183 | 6. **下载图片** | ||
| 184 | - 点击 "下载图片" 按钮 | ||
| 185 | - 选择保存位置和文件格式(PNG/JPEG) | ||
| 186 | |||
| 187 | ## 项目结构 | ||
| 188 | |||
| 189 | ``` | ||
| 190 | Nano_Banana_App/ | ||
| 191 | ├── image_generator.py # 主程序文件(含登录界面和数据库认证) | ||
| 192 | ├── user_util.py # 用户管理工具(管理员专用) | ||
| 193 | ├── requirements.txt # Python 依赖 | ||
| 194 | ├── config.json # 配置文件(API密钥+数据库配置) | ||
| 195 | ├── build_windows.bat # Windows 打包脚本 | ||
| 196 | ├── build_mac.sh # macOS 打包脚本 | ||
| 197 | └── README.md # 本文件 | ||
| 198 | ``` | ||
| 199 | |||
| 200 | ## 技术栈 | ||
| 201 | |||
| 202 | - **GUI 框架**: Tkinter(Python 内置) | ||
| 203 | - **图片处理**: Pillow | ||
| 204 | - **API 客户端**: google-genai | ||
| 205 | - **数据库**: PyMySQL | ||
| 206 | - **打包工具**: PyInstaller | ||
| 207 | |||
| 208 | ## 获取 API 密钥 | ||
| 209 | |||
| 210 | 访问 [Google AI Studio](https://makersuite.google.com/app/apikey) 获取免费的 API 密钥。 | ||
| 211 | |||
| 212 | ## 注意事项 | ||
| 213 | |||
| 214 | - API 密钥会保存在 `config.json` 文件中,请妥善保管 | ||
| 215 | - 使用 API 可能会产生费用,请查看 Google AI 的定价信息 | ||
| 216 | - 生成高分辨率图片(4K)需要更多时间和 API 配额 | ||
| 217 | |||
| 218 | ## 故障排查 | ||
| 219 | |||
| 220 | ### 登录相关问题 | ||
| 221 | |||
| 222 | **无法登录 / 数据库连接失败** | ||
| 223 | - 检查 `config.json` 中的 `db_config` 配置是否正确 | ||
| 224 | - 确认数据库服务器可访问(检查防火墙/网络) | ||
| 225 | - 验证数据库用户名和密码是否正确 | ||
| 226 | - 确认表 `nano_banana_users` 已创建 | ||
| 227 | |||
| 228 | **"未找到数据库配置" 错误** | ||
| 229 | - 确保 `config.json` 包含 `db_config` 字段 | ||
| 230 | - 参考快速开始章节的配置示例 | ||
| 231 | |||
| 232 | **"用户名或密码错误" 提示** | ||
| 233 | - 使用 `python user_util.py list` 查看用户列表 | ||
| 234 | - 确认用户状态为 'active' | ||
| 235 | - 使用 `user_util.py` 重置密码或创建新用户 | ||
| 236 | |||
| 237 | **密码不匹配** | ||
| 238 | - 确保数据库中存储的是 SHA256 哈希值,而非明文密码 | ||
| 239 | - 使用 `user_util.py add` 添加用户,会自动计算哈希 | ||
| 240 | |||
| 241 | ### 配置文件只读错误(macOS/Windows 打包应用) | ||
| 242 | **问题**: 提示 "read-only file system: config.json" | ||
| 243 | |||
| 244 | **原因**: 打包后的应用资源目录是只读的,无法在应用包内写入文件 | ||
| 245 | |||
| 246 | **解决方案**: | ||
| 247 | - ✅ 已修复:应用现在会自动将配置保存到用户目录 | ||
| 248 | - macOS: `~/Library/Application Support/ZB100ImageGenerator/config.json` | ||
| 249 | - Windows: `%APPDATA%\ZB100ImageGenerator\config.json` | ||
| 250 | - 首次运行会自动创建配置文件夹和文件 | ||
| 251 | |||
| 252 | ### 生成失败 | ||
| 253 | - 检查 API 密钥是否正确 | ||
| 254 | - 检查网络连接是否正常 | ||
| 255 | - 查看错误信息,确认是否超出配额 | ||
| 256 | |||
| 257 | ### 找不到 API 密钥 | ||
| 258 | - 开发模式:检查项目目录下的 `config.json` 文件 | ||
| 259 | - 打包应用:检查用户目录下的配置文件(见上方配置文件位置) | ||
| 260 | - 手动创建配置文件并添加完整配置(参考快速开始章节) | ||
| 261 | |||
| 262 | ### 打包失败 | ||
| 263 | - 确保安装了所有依赖:`pip install -r requirements.txt` | ||
| 264 | - 检查 Python 版本是否符合要求(3.8+) | ||
| 265 | - Windows: 确保有 `zb100_kehuan.ico` 图标文件(或修改打包脚本移除 `--icon` 参数) | ||
| 266 | - 注意: `user_util.py` 不应打包进客户端版本(仅管理员使用) | ||
| 267 | |||
| 268 | ## 技术设计原则 | ||
| 269 | |||
| 270 | 本项目遵循 Linus Torvalds 的设计哲学: | ||
| 271 | |||
| 272 | - **简洁至上**: 使用 Tkinter 内置 GUI,避免重型框架 | ||
| 273 | - **零特殊情况**: 统一的错误处理和数据流 | ||
| 274 | - **实用主义**: 直接解决问题,不过度设计 | ||
| 275 | - **清晰数据结构**: 简单的配置管理和图片数据流 | ||
| 276 | |||
| 277 | ## 许可证 | ||
| 278 | |||
| 279 | MIT License |
ZB100ImageGenerator.spec
0 → 100644
| 1 | # -*- mode: python ; coding: utf-8 -*- | ||
| 2 | |||
| 3 | |||
| 4 | a = Analysis( | ||
| 5 | ['image_generator.py'], | ||
| 6 | pathex=[], | ||
| 7 | binaries=[], | ||
| 8 | datas=[('config.json', '.')], | ||
| 9 | hiddenimports=[], | ||
| 10 | hookspath=[], | ||
| 11 | hooksconfig={}, | ||
| 12 | runtime_hooks=[], | ||
| 13 | excludes=[], | ||
| 14 | noarchive=False, | ||
| 15 | optimize=0, | ||
| 16 | ) | ||
| 17 | pyz = PYZ(a.pure) | ||
| 18 | |||
| 19 | exe = EXE( | ||
| 20 | pyz, | ||
| 21 | a.scripts, | ||
| 22 | a.binaries, | ||
| 23 | a.datas, | ||
| 24 | [], | ||
| 25 | name='ZB100ImageGenerator', | ||
| 26 | debug=False, | ||
| 27 | bootloader_ignore_signals=False, | ||
| 28 | strip=False, | ||
| 29 | upx=True, | ||
| 30 | upx_exclude=[], | ||
| 31 | runtime_tmpdir=None, | ||
| 32 | console=False, | ||
| 33 | disable_windowed_traceback=False, | ||
| 34 | argv_emulation=False, | ||
| 35 | target_arch=None, | ||
| 36 | codesign_identity=None, | ||
| 37 | entitlements_file=None, | ||
| 38 | icon=['zb100_kehuan.ico'], | ||
| 39 | ) |
build_mac.sh
0 → 100644
| 1 | #!/bin/bash | ||
| 2 | # macOS Build Script for Gemini Image Generator | ||
| 3 | |||
| 4 | echo "================================" | ||
| 5 | echo "Building Gemini Image Generator" | ||
| 6 | echo "================================" | ||
| 7 | |||
| 8 | # Check if virtual environment exists | ||
| 9 | if [ ! -d "venv" ]; then | ||
| 10 | echo "Creating virtual environment..." | ||
| 11 | python3 -m venv venv | ||
| 12 | fi | ||
| 13 | |||
| 14 | # Activate virtual environment | ||
| 15 | echo "Activating virtual environment..." | ||
| 16 | source venv/bin/activate | ||
| 17 | |||
| 18 | # Install dependencies | ||
| 19 | echo "Installing dependencies..." | ||
| 20 | pip install --upgrade pip | ||
| 21 | pip install -r requirements.txt | ||
| 22 | |||
| 23 | # Clean previous builds | ||
| 24 | echo "Cleaning previous builds..." | ||
| 25 | rm -rf build dist *.spec | ||
| 26 | |||
| 27 | # Build executable | ||
| 28 | echo "Building executable..." | ||
| 29 | pyinstaller --name="ZB100ImageGenerator" \ | ||
| 30 | --onefile \ | ||
| 31 | --windowed \ | ||
| 32 | --add-data "config.json:." \ | ||
| 33 | image_generator_qt.py | ||
| 34 | |||
| 35 | # Check if build was successful | ||
| 36 | if [ -f "dist/ZB100ImageGenerator.app/Contents/MacOS/ZB100ImageGenerator" ] || [ -f "dist/ZB100ImageGenerator" ]; then | ||
| 37 | echo "================================" | ||
| 38 | echo "Build successful!" | ||
| 39 | echo "Application: dist/ZB100ImageGenerator.app (or dist/ZB100ImageGenerator)" | ||
| 40 | echo "================================" | ||
| 41 | else | ||
| 42 | echo "================================" | ||
| 43 | echo "Build failed!" | ||
| 44 | echo "================================" | ||
| 45 | fi |
build_windows.bat
0 → 100644
| 1 | @echo off | ||
| 2 | REM Windows Build Script for Gemini Image Generator | ||
| 3 | |||
| 4 | echo ================================ | ||
| 5 | echo Building Gemini Image Generator | ||
| 6 | echo ================================ | ||
| 7 | |||
| 8 | REM Check if virtual environment exists | ||
| 9 | if not exist "venv" ( | ||
| 10 | echo Creating virtual environment... | ||
| 11 | python -m venv venv | ||
| 12 | ) | ||
| 13 | |||
| 14 | REM Activate virtual environment | ||
| 15 | echo Activating virtual environment... | ||
| 16 | call venv\Scripts\activate.bat | ||
| 17 | |||
| 18 | REM Install dependencies | ||
| 19 | echo Installing dependencies... | ||
| 20 | pip install --upgrade pip | ||
| 21 | pip install -r requirements.txt | ||
| 22 | |||
| 23 | REM Clean previous builds | ||
| 24 | echo Cleaning previous builds... | ||
| 25 | if exist "build" rd /s /q build | ||
| 26 | if exist "dist" rd /s /q dist | ||
| 27 | if exist "*.spec" del /q *.spec | ||
| 28 | |||
| 29 | REM Build executable | ||
| 30 | echo Building executable... | ||
| 31 | pyinstaller --name="ZB100ImageGenerator" ^ | ||
| 32 | --onefile ^ | ||
| 33 | --windowed ^ | ||
| 34 | --icon=zb100_kehuan.ico ^ | ||
| 35 | --add-data "config.json;." ^ | ||
| 36 | image_generator_qt.py | ||
| 37 | |||
| 38 | REM Check if build was successful | ||
| 39 | if exist "dist\ZB100ImageGenerator.exe" ( | ||
| 40 | echo ================================ | ||
| 41 | echo Build successful! | ||
| 42 | echo Executable: dist\ZB100ImageGenerator.exe | ||
| 43 | echo ================================ | ||
| 44 | ) else ( | ||
| 45 | echo ================================ | ||
| 46 | echo Build failed! | ||
| 47 | echo ================================ | ||
| 48 | ) | ||
| 49 | |||
| 50 | pause |
export_requirements.bat
0 → 100644
image_generator.py
0 → 100644
This diff is collapsed.
Click to expand it.
requirements.txt
0 → 100644
setup_mac.sh
0 → 100644
| 1 | #!/bin/bash | ||
| 2 | # Mac 环境设置脚本 | ||
| 3 | |||
| 4 | echo "================================" | ||
| 5 | echo "Mac 环境设置 - Qt 版本" | ||
| 6 | echo "================================" | ||
| 7 | |||
| 8 | # 检查 Python 版本 | ||
| 9 | python_version=$(python3 --version 2>&1) | ||
| 10 | echo "Python 版本: $python_version" | ||
| 11 | |||
| 12 | # 创建虚拟环境 | ||
| 13 | if [ ! -d "venv" ]; then | ||
| 14 | echo "创建虚拟环境..." | ||
| 15 | python3 -m venv venv | ||
| 16 | fi | ||
| 17 | |||
| 18 | # 激活虚拟环境 | ||
| 19 | echo "激活虚拟环境..." | ||
| 20 | source venv/bin/activate | ||
| 21 | |||
| 22 | # 升级 pip | ||
| 23 | echo "升级 pip..." | ||
| 24 | pip install --upgrade pip | ||
| 25 | |||
| 26 | # 卸载可能冲突的旧版本 | ||
| 27 | echo "清理旧版本..." | ||
| 28 | pip uninstall -y google-genai Pillow PyQt5 pyinstaller 2>/dev/null | ||
| 29 | |||
| 30 | # 安装依赖 | ||
| 31 | echo "================================" | ||
| 32 | echo "安装依赖..." | ||
| 33 | echo "================================" | ||
| 34 | |||
| 35 | # 如果有锁定版本文件,优先使用 | ||
| 36 | if [ -f "requirements-lock.txt" ]; then | ||
| 37 | echo "使用锁定版本 (requirements-lock.txt)..." | ||
| 38 | pip install -r requirements-lock.txt | ||
| 39 | else | ||
| 40 | echo "使用范围版本 (requirements.txt)..." | ||
| 41 | pip install -r requirements.txt | ||
| 42 | fi | ||
| 43 | |||
| 44 | # 验证安装 | ||
| 45 | echo "================================" | ||
| 46 | echo "验证安装..." | ||
| 47 | echo "================================" | ||
| 48 | |||
| 49 | pip list | grep -E "google-genai|Pillow|PyQt5|pyinstaller" | ||
| 50 | |||
| 51 | echo "================================" | ||
| 52 | echo "设置完成!" | ||
| 53 | echo "================================" | ||
| 54 | echo "" | ||
| 55 | echo "运行应用: python3 image_generator_qt.py" | ||
| 56 | echo "" |
user_util.py
0 → 100644
| 1 | #!/usr/bin/env python3 | ||
| 2 | # -*- coding: utf-8 -*- | ||
| 3 | """ | ||
| 4 | 用户管理工具 - 管理员专用 | ||
| 5 | |||
| 6 | 用法: | ||
| 7 | python user_util.py add <username> <password> # 添加用户 | ||
| 8 | python user_util.py list # 列出用户 | ||
| 9 | python user_util.py disable <username> # 禁用用户 | ||
| 10 | python user_util.py enable <username> # 启用用户 | ||
| 11 | python user_util.py reset <username> <password> # 重置密码 | ||
| 12 | |||
| 13 | 安全提示: | ||
| 14 | - 此工具仅供管理员使用,请勿分发给用户 | ||
| 15 | - 避免在命令行直接输入密码(可被 shell 历史记录) | ||
| 16 | - 建议使用环境变量或交互式输入密码 | ||
| 17 | """ | ||
| 18 | |||
| 19 | import hashlib | ||
| 20 | import pymysql | ||
| 21 | import json | ||
| 22 | import sys | ||
| 23 | import os | ||
| 24 | from pathlib import Path | ||
| 25 | |||
| 26 | # Windows 控制台编码修复 | ||
| 27 | if sys.platform == 'win32': | ||
| 28 | import codecs | ||
| 29 | sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict') | ||
| 30 | sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict') | ||
| 31 | |||
| 32 | |||
| 33 | def hash_password(password: str) -> str: | ||
| 34 | """使用 SHA256 哈希密码""" | ||
| 35 | return hashlib.sha256(password.encode('utf-8')).hexdigest() | ||
| 36 | |||
| 37 | |||
| 38 | class UserManager: | ||
| 39 | """用户管理类""" | ||
| 40 | def __init__(self, db_config): | ||
| 41 | self.config = db_config | ||
| 42 | |||
| 43 | def add_user(self, username, password): | ||
| 44 | """添加新用户""" | ||
| 45 | hashed = hash_password(password) | ||
| 46 | conn = None | ||
| 47 | try: | ||
| 48 | conn = pymysql.connect( | ||
| 49 | host=self.config['host'], | ||
| 50 | port=self.config.get('port', 3306), | ||
| 51 | user=self.config['user'], | ||
| 52 | password=self.config['password'], | ||
| 53 | database=self.config['database'], | ||
| 54 | connect_timeout=5 | ||
| 55 | ) | ||
| 56 | |||
| 57 | with conn.cursor() as cursor: | ||
| 58 | sql = f"INSERT INTO {self.config['table']} (user_name, passwd, status) VALUES (%s, %s, %s)" | ||
| 59 | cursor.execute(sql, (username, hashed, 'active')) | ||
| 60 | conn.commit() | ||
| 61 | print(f"✓ 用户 '{username}' 添加成功") | ||
| 62 | print(f" 密码哈希: {hashed[:16]}...") | ||
| 63 | |||
| 64 | except pymysql.IntegrityError: | ||
| 65 | print(f"✗ 用户 '{username}' 已存在") | ||
| 66 | except pymysql.OperationalError as e: | ||
| 67 | print(f"✗ 数据库连接失败: {e}") | ||
| 68 | except Exception as e: | ||
| 69 | print(f"✗ 添加用户失败: {e}") | ||
| 70 | finally: | ||
| 71 | if conn: | ||
| 72 | conn.close() | ||
| 73 | |||
| 74 | def list_users(self): | ||
| 75 | """列出所有用户""" | ||
| 76 | conn = None | ||
| 77 | try: | ||
| 78 | conn = pymysql.connect( | ||
| 79 | host=self.config['host'], | ||
| 80 | port=self.config.get('port', 3306), | ||
| 81 | user=self.config['user'], | ||
| 82 | password=self.config['password'], | ||
| 83 | database=self.config['database'], | ||
| 84 | connect_timeout=5 | ||
| 85 | ) | ||
| 86 | |||
| 87 | with conn.cursor() as cursor: | ||
| 88 | sql = f"SELECT user_name, passwd, status FROM {self.config['table']}" | ||
| 89 | cursor.execute(sql) | ||
| 90 | results = cursor.fetchall() | ||
| 91 | |||
| 92 | if not results: | ||
| 93 | print("没有找到任何用户") | ||
| 94 | return | ||
| 95 | |||
| 96 | # 打印表格 | ||
| 97 | print("\n用户列表:") | ||
| 98 | print(" ┌────────────────────────┬──────────┬──────────────────┐") | ||
| 99 | print(" │ 用户名 │ 状态 │ 密码哈希(前8位) │") | ||
| 100 | print(" ├────────────────────────┼──────────┼──────────────────┤") | ||
| 101 | |||
| 102 | for row in results: | ||
| 103 | username = row[0] or "" | ||
| 104 | passwd_hash = row[1] or "" | ||
| 105 | status = row[2] or "NULL" | ||
| 106 | |||
| 107 | # 填充空格使对齐 | ||
| 108 | username_display = username[:20].ljust(20) | ||
| 109 | status_display = status[:8].ljust(8) | ||
| 110 | hash_display = passwd_hash[:16] if passwd_hash else "N/A".ljust(16) | ||
| 111 | |||
| 112 | print(f" │ {username_display} │ {status_display} │ {hash_display} │") | ||
| 113 | |||
| 114 | print(" └────────────────────────┴──────────┴──────────────────┘") | ||
| 115 | print(f"\n总计: {len(results)} 个用户\n") | ||
| 116 | |||
| 117 | except pymysql.OperationalError as e: | ||
| 118 | print(f"✗ 数据库连接失败: {e}") | ||
| 119 | except Exception as e: | ||
| 120 | print(f"✗ 查询用户失败: {e}") | ||
| 121 | finally: | ||
| 122 | if conn: | ||
| 123 | conn.close() | ||
| 124 | |||
| 125 | def disable_user(self, username): | ||
| 126 | """禁用用户""" | ||
| 127 | self._update_user_status(username, 'disabled', "已禁用") | ||
| 128 | |||
| 129 | def enable_user(self, username): | ||
| 130 | """启用用户""" | ||
| 131 | self._update_user_status(username, 'active', "已启用") | ||
| 132 | |||
| 133 | def _update_user_status(self, username, status, action_name): | ||
| 134 | """更新用户状态""" | ||
| 135 | conn = None | ||
| 136 | try: | ||
| 137 | conn = pymysql.connect( | ||
| 138 | host=self.config['host'], | ||
| 139 | port=self.config.get('port', 3306), | ||
| 140 | user=self.config['user'], | ||
| 141 | password=self.config['password'], | ||
| 142 | database=self.config['database'], | ||
| 143 | connect_timeout=5 | ||
| 144 | ) | ||
| 145 | |||
| 146 | with conn.cursor() as cursor: | ||
| 147 | sql = f"UPDATE {self.config['table']} SET status=%s WHERE user_name=%s" | ||
| 148 | affected = cursor.execute(sql, (status, username)) | ||
| 149 | |||
| 150 | if affected > 0: | ||
| 151 | conn.commit() | ||
| 152 | print(f"✓ 用户 '{username}' {action_name}") | ||
| 153 | else: | ||
| 154 | print(f"✗ 用户 '{username}' 不存在") | ||
| 155 | |||
| 156 | except pymysql.OperationalError as e: | ||
| 157 | print(f"✗ 数据库连接失败: {e}") | ||
| 158 | except Exception as e: | ||
| 159 | print(f"✗ 更新用户状态失败: {e}") | ||
| 160 | finally: | ||
| 161 | if conn: | ||
| 162 | conn.close() | ||
| 163 | |||
| 164 | def reset_password(self, username, new_password): | ||
| 165 | """重置用户密码""" | ||
| 166 | hashed = hash_password(new_password) | ||
| 167 | conn = None | ||
| 168 | try: | ||
| 169 | conn = pymysql.connect( | ||
| 170 | host=self.config['host'], | ||
| 171 | port=self.config.get('port', 3306), | ||
| 172 | user=self.config['user'], | ||
| 173 | password=self.config['password'], | ||
| 174 | database=self.config['database'], | ||
| 175 | connect_timeout=5 | ||
| 176 | ) | ||
| 177 | |||
| 178 | with conn.cursor() as cursor: | ||
| 179 | sql = f"UPDATE {self.config['table']} SET passwd=%s WHERE user_name=%s" | ||
| 180 | affected = cursor.execute(sql, (hashed, username)) | ||
| 181 | |||
| 182 | if affected > 0: | ||
| 183 | conn.commit() | ||
| 184 | print(f"✓ 用户 '{username}' 密码已重置") | ||
| 185 | print(f" 新密码哈希: {hashed[:16]}...") | ||
| 186 | else: | ||
| 187 | print(f"✗ 用户 '{username}' 不存在") | ||
| 188 | |||
| 189 | except pymysql.OperationalError as e: | ||
| 190 | print(f"✗ 数据库连接失败: {e}") | ||
| 191 | except Exception as e: | ||
| 192 | print(f"✗ 重置密码失败: {e}") | ||
| 193 | finally: | ||
| 194 | if conn: | ||
| 195 | conn.close() | ||
| 196 | |||
| 197 | |||
| 198 | def load_db_config(): | ||
| 199 | """从 config.json 加载数据库配置""" | ||
| 200 | config_path = Path('config.json') | ||
| 201 | |||
| 202 | if not config_path.exists(): | ||
| 203 | print(f"✗ 配置文件不存在: {config_path}") | ||
| 204 | print(f" 请确保在项目目录下运行此工具") | ||
| 205 | return None | ||
| 206 | |||
| 207 | try: | ||
| 208 | with open(config_path, 'r', encoding='utf-8') as f: | ||
| 209 | config = json.load(f) | ||
| 210 | db_config = config.get('db_config') | ||
| 211 | |||
| 212 | if not db_config: | ||
| 213 | print("✗ 配置文件中未找到 db_config 字段") | ||
| 214 | return None | ||
| 215 | |||
| 216 | required_fields = ['host', 'user', 'password', 'database', 'table'] | ||
| 217 | missing = [f for f in required_fields if f not in db_config] | ||
| 218 | |||
| 219 | if missing: | ||
| 220 | print(f"✗ db_config 缺少必需字段: {', '.join(missing)}") | ||
| 221 | return None | ||
| 222 | |||
| 223 | return db_config | ||
| 224 | |||
| 225 | except json.JSONDecodeError: | ||
| 226 | print(f"✗ 配置文件格式错误: {config_path}") | ||
| 227 | return None | ||
| 228 | except Exception as e: | ||
| 229 | print(f"✗ 加载配置失败: {e}") | ||
| 230 | return None | ||
| 231 | |||
| 232 | |||
| 233 | def print_help(): | ||
| 234 | """打印帮助信息""" | ||
| 235 | print(__doc__) | ||
| 236 | |||
| 237 | |||
| 238 | def main(): | ||
| 239 | """主函数""" | ||
| 240 | if len(sys.argv) < 2: | ||
| 241 | print_help() | ||
| 242 | sys.exit(1) | ||
| 243 | |||
| 244 | command = sys.argv[1].lower() | ||
| 245 | |||
| 246 | # 加载数据库配置 | ||
| 247 | db_config = load_db_config() | ||
| 248 | if not db_config: | ||
| 249 | sys.exit(1) | ||
| 250 | |||
| 251 | manager = UserManager(db_config) | ||
| 252 | |||
| 253 | # 处理命令 | ||
| 254 | if command == 'add': | ||
| 255 | if len(sys.argv) != 4: | ||
| 256 | print("用法: python user_util.py add <username> <password>") | ||
| 257 | sys.exit(1) | ||
| 258 | username = sys.argv[2] | ||
| 259 | password = sys.argv[3] | ||
| 260 | manager.add_user(username, password) | ||
| 261 | |||
| 262 | elif command == 'list': | ||
| 263 | manager.list_users() | ||
| 264 | |||
| 265 | elif command == 'disable': | ||
| 266 | if len(sys.argv) != 3: | ||
| 267 | print("用法: python user_util.py disable <username>") | ||
| 268 | sys.exit(1) | ||
| 269 | username = sys.argv[2] | ||
| 270 | manager.disable_user(username) | ||
| 271 | |||
| 272 | elif command == 'enable': | ||
| 273 | if len(sys.argv) != 3: | ||
| 274 | print("用法: python user_util.py enable <username>") | ||
| 275 | sys.exit(1) | ||
| 276 | username = sys.argv[2] | ||
| 277 | manager.enable_user(username) | ||
| 278 | |||
| 279 | elif command == 'reset': | ||
| 280 | if len(sys.argv) != 4: | ||
| 281 | print("用法: python user_util.py reset <username> <password>") | ||
| 282 | sys.exit(1) | ||
| 283 | username = sys.argv[2] | ||
| 284 | new_password = sys.argv[3] | ||
| 285 | manager.reset_password(username, new_password) | ||
| 286 | |||
| 287 | else: | ||
| 288 | print(f"✗ 未知命令: {command}") | ||
| 289 | print_help() | ||
| 290 | sys.exit(1) | ||
| 291 | |||
| 292 | |||
| 293 | if __name__ == '__main__': | ||
| 294 | main() |
zb100_kehuan.ico
0 → 100644
No preview for this file type
-
Please register or sign in to post a comment