From 5e4e5dcb641c9cb67859d814263a162a4cd54b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=BB=8D=E5=BA=B7?= <3052147989@qq.com> Date: Wed, 25 Feb 2026 10:23:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=81=E7=A7=BB=20accounts=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=89=B9=E6=AC=A1=201=20=E5=88=B0=20controll?= =?UTF-8?q?ers=20=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 迁移的路由(12个): - 基础 CRUD:api_get_accounts, api_get_account, api_add_account, api_update_account, api_delete_account, api_delete_account_by_email - 批量操作:api_search_accounts, api_batch_update_account_group, api_batch_delete_accounts, api_batch_manage_tags - 导出功能:api_export_all_accounts, api_export_selected_accounts, api_generate_export_verify_token 修复: - parse_account_string 函数支持 refresh_token 包含 '----' 分隔符 - api_generate_export_verify_token 使用正确的 auth 模块函数 剩余 accounts 模块路由(8个)暂保留 legacy 实现: - Token 刷新相关:refresh, refresh-all, retry-refresh, refresh-failed - 刷新日志相关:refresh-logs, account-refresh-logs, refresh-stats, trigger-scheduled-refresh Co-Authored-By: Claude Opus 4.6 --- ...件管理工具-Legacy迁移到Controllers层TODO.md | 635 ++++++++++++++++ outlook_web/controllers/accounts.py | 702 ++++++++++++++++++ outlook_web/routes/accounts.py | 33 +- 3 files changed, 1357 insertions(+), 13 deletions(-) create mode 100644 docs/TODO/Outlook邮件管理工具-Legacy迁移到Controllers层TODO.md create mode 100644 outlook_web/controllers/accounts.py diff --git a/docs/TODO/Outlook邮件管理工具-Legacy迁移到Controllers层TODO.md b/docs/TODO/Outlook邮件管理工具-Legacy迁移到Controllers层TODO.md new file mode 100644 index 0000000..715464e --- /dev/null +++ b/docs/TODO/Outlook邮件管理工具-Legacy迁移到Controllers层TODO.md @@ -0,0 +1,635 @@ +# Outlook 邮件管理工具|Legacy 迁移到 Controllers 层 TODO(待办清单) + +- 文档状态:草案 +- 版本:V1.0 +- 日期:2026-02-24 +- 对齐 PRD:`docs/PRD/PRD-00003-Legacy代码拆分到Controllers层.md` +- 对齐 FD:`docs/FD/Outlook邮件管理工具-Legacy迁移到Controllers层FD.md` +- 对齐 TDD:`docs/TDD/Outlook邮件管理工具-Legacy迁移到Controllers层TDD.md` + +--- + +## P0(必须完成) + +### 阶段 0:文档与基线准备 + +#### 0.1 文档准备 +- [x] PRD 文档已完成 +- [x] FD 文档已完成 +- [x] TDD 文档已完成 +- [ ] 阅读并理解所有文档 +- [ ] 明确验收标准和成功指标 + +#### 0.2 环境准备 +- [ ] 确认开发环境正常(Python 3.x, Flask, SQLite) +- [ ] 确认所有依赖已安装:`pip install -r requirements.txt` +- [ ] 确认数据库正常:`data/outlook_accounts.db` 存在且可访问 + +#### 0.3 基线测试 +- [ ] 运行所有现有测试:`python -m unittest discover -s tests -v` +- [ ] 确认所有测试通过(记录通过的测试数量) +- [ ] 手动验证核心功能正常: + - [ ] 登录功能 + - [ ] 分组管理 + - [ ] 账号管理 + - [ ] 邮件读取 + - [ ] Token 刷新 + +#### 0.4 Git 准备 +- [ ] 创建功能分支:`git checkout -b feature/migrate-to-controllers` +- [ ] 确认当前分支干净:`git status` +- [ ] 记录当前 commit hash(用于回滚) + +--- + +### 阶段 1:基础模块迁移(低复杂度,无依赖) + +**目标:** 迁移 groups, tags, settings, system, audit, pages 模块(共 19 个路由) + +**预计时间:** 2-3 天 + +#### 1.1 创建 Controllers 目录结构 + +- [ ] 创建 `outlook_web/controllers/` 目录 +- [ ] 创建 `outlook_web/controllers/__init__.py` 文件 +- [ ] 在 `__init__.py` 中添加模块导出(如果需要) + +#### 1.2 迁移 Groups 模块(6 个路由) + +**文件:** `outlook_web/controllers/groups.py` + +- [ ] 创建 `controllers/groups.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_get_groups()` - 获取所有分组 + - [ ] `api_get_group(group_id)` - 获取单个分组 + - [ ] `api_add_group()` - 添加分组 + - [ ] `api_update_group(group_id)` - 更新分组 + - [ ] `api_delete_group(group_id)` - 删除分组 + - [ ] `api_export_group(group_id)` - 导出分组账号 +- [ ] 调整导入语句: + - [ ] 导入 `flask` 相关模块 + - [ ] 导入 `outlook_web.security.auth` + - [ ] 导入 `outlook_web.repositories.groups` + - [ ] 导入 `outlook_web.errors` +- [ ] 更新 `routes/groups.py`: + - [ ] 移除 `impl` 参数 + - [ ] 导入 `controllers.groups` + - [ ] 更新所有路由注册 +- [ ] 更新 `app.py`: + - [ ] 移除 `groups.ueprint(impl=legacy)` + - [ ] 改为 `groups.create_blueprint()` +- [ ] 测试验证: + - [ ] 运行单元测试:`python -m unittest tests.test_smoke_contract -v` + - [ ] 手动测试所有 6 个 API: + - [ ] GET /api/groups + - [ ] GET /api/groups/ + - [ ] POST /api/groups + - [ ] PUT /api/groups/ + - [ ] DELETE /api/groups/ + - [ ] GET /api/groups//export +- [ ] 提交 Git:`git commit -m "feat: 迁移 groups 模块到 controllers 层"` + +#### 1.3 迁移 Tags 模块(4 个路由) + +**文件:** `outlook_web/controllers/tags.py` + +- [ ] 创建 `controllers/tags.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_get_tags()` - 获取所有标签 + - [ ] `api_add_tag()` - 添加标签 + - [ ] `api_delete_tag(tag_id)` - 删除标签 + - [ ] `api_batch_manage_tags()` - 批量管理标签 +- [ ] 调整导入语句 +- [ ] 更新 `routes/tags.py` +- [ ] 更新 `app.py` +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 4 个 API +- [ ] 提交 Git:`git commit -m "feat: 迁移 tags 模块到 controllers 层"` + +#### 1.4 迁移 Settings 模块(3 个路由) + +**文件:** `outlook_web/controllers/settings.py` + +- [ ] 创建 `controllers/settings.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_get_settings()` - 获取系统设置 + - [ ] `api_update_settings()` - 更新系统设置 + - [ ] `api_validate_cron()` - 验证 Cron 表达式 +- [ ] 调整导入语句 +- [ ] 更新 `routes/settings.py` +- [ ] 更新 `app.py` +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 3 个 API +- [ ] 提交 Git:`git commit -m "feat: 迁移 settings 模块到 controllers 层"` + +#### 1.5 迁移 System 模块(3 个路由) + +**文件:** `outlook_web/controllers/system.py` + +- [ ] 创建 `controllers/system.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_health_check()` - 健康检查 + - [ ] `api_get_diagnostics()` - 获取诊断信息 + - [ ] `api_get_upgrade_status()` - 获取升级状态 +- [ ] 调整导入语句 +- [ ] 更新 `routes/system.py` +- [ ] 更新 `app.py` +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 3 个 API +- [ ] 提交 Git:`git commit -m "feat: 迁移 system 模块到 controllers 层"` + +#### 1.6 迁移 Audit 模块(1 个路由) + +**文件:** `outlook_web/controllers/audit.py` + +- [ ] 创建 `controllers/audit.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_get_audit_logs()` - 获取审计日志 +- [ ] 调整导入语句 +- [ ] 更新 `routes/audit.py` +- [ ] 更新 `app.py` +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试 API +- [ ] 提交 Git:`git commit -m "feat: 迁移 audit 模块到 controllers 层"` + +#### 1.7 迁移 Pages 模块(3 个路由) + +**文件:** `outlook_web/controllers/pages.py` + +- [ ] 创建 `controllers/pages.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `login()` - 登录页面 + - [ ] `logout()` - 登出 + - [ ] `index()` - 首页 +- [ ] 调整导入语句 +- [ ] 更新 `routes/pages.py` +- [ ] 更新 `app.py` +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 3 个页面 +- [ ] 提交 Git:`git commit -m "feat: 迁移 pages 模块到 controllers 层"` + +#### 1.8 阶段 1 验收 + +- [ ] 运行所有测试:`python -m unittest discover -s tests -v` +- [ ] 确认所有测试通过 +- [ ] 手动回归测试: + - [ ] 登录功能正常 + - [ ] 分组 CRUD 正常 + - [ ] 标签 CRUD 正常 + - [ ] 系统设置正常 + - [ ] 审计日志正常 +- [ ] 性能测试: + - [ ] 响应时间无明显增加 + - [ ] 内存使用无明显增加 +- [ ] 代码审查: + - [ ] 代码风格一致 + - [ ] 无重复代码 + - [ ] 注释清晰 +- [ ] 合并到主分支(可选):`git merge feature/migrate-to-controllers` + +--- + +### 阶段 2:独立功能模块迁移 + +**目标:** 迁移 temp_emails, oauth, scheduler 模块(共 6 个路由) + +**预计时间:** 1-2 天 + +#### 2.1 迁移 Temp Emails 模块(3 个路由) + +**文件:** `outlook_web/controllers/temp_emails.py` + +- [ ] 创建 `controllers/temp_emails.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_get_temp_emails()` - 获取临时邮箱列表 + - [ ] `api_generate_temp_email()` - 生成临时邮箱 + - [ ] `api_get_temp_email_messages(email_id)` - 获取临时邮箱消息 +- [ ] 调整导入语句 +- [ ] 更新 `routes/temp_emails.py` +- [ ] 更新 `app.py` +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 3 个 API +- [ ] 提交 Git:`git commit -m "feat: 迁移 temp_emails 模块到 controllers 层"` + +#### 2.2 迁移 OAuth 模块(2 个路由) + +**文件:** `outlook_web/controllers/oauth.py` + +- [ ] 创建 `controllers/oauth.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_get_oauth_auth_url()` - 获取 OAuth 授权 URL + - [ ] `api_exchange_oauth_token()` - 交换 OAuth Token +- [ ] 调整导入语句 +- [ ] 更新 `routes/oauth.py` +- [ ] 更新 `app.py` +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试 OAuth 流程 +- [ ] 提交 Git:`git commit -m "feat: 迁移 oauth 模块到 controllers 层"` + +#### 2.3 迁移 Scheduler 模块(1 个路由) + +**文件:** `outlook_web/controllers/scheduler.py` + +- [ ] 创建 `controllers/scheduler.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_get_scheduler_status()` - 获取调度器状态 +- [ ] 调整导入语句 +- [ ] 更新 `routes/scheduler.py` +- [ ] 更新 `app.py` +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试 API +- [ ] 提交 Git:`git commit -m "feat: 迁移 scheduler 模块到 controllers 层"` + +#### 2.4 阶段 2 验收 + +- [ ] 运行所有测试:`python -m unittest discover -s tests -v` +- [ ] 确认所有测试通过 +- [ ] 手动回归测试: + - [ ] 临时邮箱功能正常 + - [ ] OAuth 授权流程正常 + - [ ] 调度器状态查询正常 +- [ ] 性能测试无异常 +- [ ] 代码审查通过 + +--- + +(文档未完,待续...) +### 阶段 3:核心复杂模块迁移 + +**目标:** 迁移 emails, accounts 模块(共 24 个路由) + +**预计时间:** 3-4 天 + +#### 3.1 迁移 Emails 模块(4 个路由) + +**文件:** `outlook_web/controllers/emails.py` + +- [ ] 创建 `controllers/emails.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_get_emails(email_addr)` - 获取邮件列表 + - [ ] `api_get_email_detail(email_addr, message_id)` - 获取邮件详情 + - [ ] `api_delete_emails()` - 删除邮件 + - [ ] `api_extract_verification(email_addr)` - 提取验证码 +- [ ] 调整导入语句: + - [ ] 导入 `outlook_web.services.graph` + - [ ] 导入 `outlook_web.services.imap` + - [ ] 导入 `outlook_web.repositories.accounts` +- [ ] 确保回退机制正常(Graph API → IMAP) +- [ ] 更新 `routes/emails.py` +- [ ] 更新 `app.py` +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 4 个 API + - [ ] 测试 Graph API 成功场景 + - [ ] 测试 IMAP 回退场景 + - [ ] 测试代理配置 +- [ ] 提交 Git:`git commit -m "feat: 迁移 emails 模块到 controllers 层"` + +#### 3.2 迁移 Accounts 模块(20 个路由) + +**文件:** `outlook_web/controllers/accounts.py` + +**注意:** 这是最复杂的模块,建议分批迁移 + +##### 3.2.1 基础 CRUD(5 个路由) + +- [ ] 创建 `controllers/accounts.py` 文件 +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_get_accounts()` - 获取账号列表 + - [ ] `api_get_account(account_id)` - 获取单个账号 + - [ ] `api_add_account()` - 添加账号 + - [ ] `api_update_account(account_id)` - 更新账号 + - [ ] `api_delete_account(account_id)` - 删除账号 +- [ ] 调整导入语句 +- [ ] 确保数据脱敏正常(client_id, refresh_token) +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 5 个 API + - [ ] 验证数据脱敏 + +##### 3.2.2 批量操作(4 个路由) + +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_delete_account_by_email(email_addr)` - 按邮箱删除账号 + - [ ] `api_batch_delete_accounts()` - 批量删除账号 + - [ ] `api_batch_update_account_group()` - 批量更新分组 + - [ ] `api_search_accounts()` - 搜索账号 +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 4 个 API + - [ ] 测试批量操作性能 + +##### 3.2.3 导出功能(3 个路由) + +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_export_all_accounts()` - 导出所有账号 + - [ ] `api_export_selected_accounts()` - 导出选中账号 + - [ ] `api_generate_export_verify_token()` - 生成导出验证 Token +- [ ] 确保导出验证机制正常 +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 3 个 API + - [ ] 测试导出验证流程 + +##### 3.2.4 Token 刷新(8 个路由) + +- [ ] 从 `legacy.py` 提取以下函数: + - [ ] `api_refresh_account(account_id)` - 刷新单个账号 + - [ ] `api_refresh_all_accounts()` - 刷新所有账号 + - [ ] `api_retry_refresh_account(account_id)` - 重试刷新账号 + - [ ] `api_refresh_failed_accounts()` - 刷新失败账号 + - [ ] `api_trigger_scheduled_refresh()` - 触发定时刷新 + - [ ] `api_get_refresh_logs()` - 获取刷新日志 + - [ ] `api_get_account_refresh_logs(account_id)` - 获取账号刷新日志 + - [ ] `api_get_refresh_stats()` - 获取刷新统计 +- [ ] 确保刷新机制正常 +- [ ] 测试验证: + - [ ] 运行单元测试 + - [ ] 手动测试所有 8 个 API + - [ ] 测试刷新流程 + - [ ] 测试刷新日志 + +##### 3.2.5 更新 Routes 和 App + +- [ ] 更新 `routes/accounts.py` +- [ ] 更新 `app.py` +- [ ] 提交 Git:`git commit -m "feat: 迁移 accounts 模块到 controllers 层"` + +#### 3.3 阶段 3 验收 + +- [ ] 运行所有测试:`python -m unittest discover -s tests -v` +- [ ] 确认所有测试通过 +- [ ] 手动回归测试: + - [ ] 邮件读取功能正常 + - [ ] 邮件删除功能正常 + - [ ] 验证码提取功能正常 + - [ ] 账号 CRUD 功能正常 + - [ ] 批量操作功能正常 + - [ ] 导出功能正常 + - [ ] Token 刷新功能正常 + - [ ] 刷新日志功能正常 +- [ ] 性能测试: + - [ ] 邮件读取响应时间 < 2 秒 + - [ ] 账号列表响应时间 < 500ms + - [ ] 批量操作响应时间 < 1 秒 +- [ ] 代码审查通过 + +--- + +### 阶段 4:清理和优化 + +**目标:** 删除 legacy.py,迁移工具函数和中间件 + +**预计时间:** 1 天 + +#### 4.1 验证所有路由已迁移 + +- [ ] 检查 `legacy.py` 中是否还有 `api_*` 函数 +- [ ] 确认所有 54 个路由都已迁移到 controllers/ +- [ ] 确认所有 routes/ 文件都已更新 +- [ ] 确认 app.py 中所有 Blueprint 注册都已更新 + +#### 4.2 迁移工具函数(可选) + +**目标:** 将 legacy.py 中的工具函数迁移到 utils/ + +- [ ] 创建 `outlook_web/utils/` 目录 +- [ ] 创建 `utils/__init__.py` +- [ ] 迁移以下工具函数: + - [ ] `sanitize_input()` → `utils/sanitize.py` + - [ ] `decode_header_value()` → `utils/sanitize.py` + - [ ] `get_email_body()` → `utils/email_parser.py` + - [ ] `parse_account_string()` → `utils/email_parser.py` + - [ ] `utcnow()` → `utils/datetime.py` + - [ ] `build_proxies()` → `utils/proxy.py` +- [ ] 更新所有引用这些函数的地方 +- [ ] 测试验证 +- [ ] 提交 Git:`git commit -m "refactor: 迁移工具函数到 utils 模块"` + +#### 4.3 迁移中间件(可选) + +**目标:** 将 legacy.py 中的中间件迁移到 middleware/ + +- [ ] 创建 `outlook_web/middleware/` 目录 +- [ ] 创建 `middleware/__init__.py` +- [ ] 迁移以下中间件函数: + - [ ] `ensure_trace_id()` → `middleware/trace.py` + - [ ] `attach_trace_id_and_normalize_errors()` → `middleware/trace.py` + - [ ] `handle_http_exception()` → `middleware/error_handler.py` + - [ ] `handle_exception()` → `middleware/error_handler.py` +- [ ] 更新 app.py 中的中间件注册 +- [ ] 测试验证 +- [ ] 提交 Git:`git commit -m "refactor: 迁移中间件到 middleware 模块"` + +#### 4.4 删除 legacy.py + +- [ ] 确认所有函数都已迁移 +- [ ] 备份 legacy.py:`cp outlook_web/legacy.py outlook_web/legacy.py.bak` +- [ ] 删除 legacy.py:`rm outlook_web/legacy.py` +- [ ] 更新所有导入 legacy 的地方 +- [ ] 运行所有测试 +- [ ] 确认应用正常启动 +- [ ] 提交 Git:`git commit -m "refactor: 删除 legacy.py"` + +#### 4.5 更新文档 + +- [ ] 更新 `CLAUDE.md`: + - [ ] 更新项目架构说明 + - [ ] 更新目录结构说明 +- [ ] 更新 `docs/DEV/00002-前后端拆分-开发者指南.md`: + - [ ] 更新开发指南 + - [ ] 更新新增 API 的推荐路径 +- [ ] 更新 `README.md`(如果需要) +- [ ] 提交 Git:`git commit -m "docs: 更新文档"` + +#### 4.6 阶段 4 验收 + +- [ ] 运行所有测试:`python -m unittest discover -s tests -v` +- [ ] 确认所有测试通过 +- [ ] 手动回归测试所有功能 +- [ ] 性能测试无异常 +- [ ] 代码审查通过 +- [ ] 文档审查通过 + +--- + +## P1(重要但不紧急) + +### 优化和改进 + +#### 5.1 代码优化 + +- [ ] 检查是否有重复代码,提取公共函数 +- [ ] 检查是否有过长的函数,拆分为小函数 +- [ ] 检查是否有复杂的条件判断,简化逻辑 +- [ ] 检查是否有魔法数字,提取为常量 + +#### 5.2 测试覆盖率提升 + +- [ ] 为每个 controller 编写单元测试 +- [ ] 为关键功能编写集成测试 +- [ ] 使用 coverage 工具检查测试覆盖率 +- [ ] 目标:测试覆盖率 > 80% + +#### 5.3 性能优化 + +- [ ] 使用 profiler 工具分析性能瓶颈 +- [ ] 优化数据库查询(如果需要) +- [ ] 优化 API 响应时间(如果需要) +- [ ] 添加缓存(如果需要) + +#### 5.4 文档完善 + +- [ ] 为每个 controller 添加详细的文档字符串 +- [ ] 为每个函数添加参数说明和返回值说明 +- [ ] 编写 API 文档(如果需要) +- [ ] 编写开发者指南(如果需要) + +--- + +## P2(可选) + +### 进一步改进 + +#### 6.1 引入类型注解 + +- [ ] 为所有函数添加类型注解 +- [ ] 使用 mypy 进行类型检查 +- [ ] 修复类型错误 + +#### 6.2 引入日志系统 + +- [ ] 使用 Python logging 模块 +- [ ] 为关键操作添加日志 +- [ ] 配置日志级别和输出格式 + +#### 6.3 引入监控和告警 + +- [ ] 集成 Sentry 或其他监控工具 +- [ ] 配置错误告警 +- [ ] 配置性能监控 + +--- + +## 验收标准 + +### 功能验收 + +- [ ] 所有 54 个 API 路由都已迁移到 controllers/ +- [ ] legacy.py 已删除 +- [ ] 所有功能手动测试通过 +- [ ] 前端功能正常,无需修改 + +### 质量验收 + +- [ ] 所有单元测试通过 +- [ ] 所有集成测试通过 +- [ ] 代码审查通过 +- [ ] 性能测试通过(响应时间 < 迁移前 110%) + +### 文档验收 + +- [ ] CLAUDE.md 已更新 +- [ ] 开发者指南已更新 +- [ ] 迁移文档已归档 + +--- + +## 风险和注意事项 + +### 风险识别 + +1. **路由映射错误** + - 风险:URL 路径或 HTTP 方法错误 + - 应对:充分测试,使用自动化测试 + +2. **参数解析错误** + - 风险:参数类型或名称错误 + - 应对:保持函数签名一致,单元测试覆盖 + +3. **依赖关系错误** + - 风险:循环依赖或导入错误 + - 应对:遵循依赖方向规则,先迁移无依赖模块 + +4. **性能下降** + - 风险:响应时间增加 + - 应对:性能测试对比,优化瓶颈 + +5. **功能回归** + - 风险:某些功能不正常 + - 应对:完整的回归测试 + +### 注意事项 + +1. **每个阶段完成后提交 Git** + - 便于回滚 + - 便于代码审查 + +2. **保留 legacy.py 直到所有模块迁移完成** + - 作为参考 + - 便于回滚 + +3. **充分测试** + - 单元测试 + - 集成测试 + - 手动测试 + +4. **性能监控** + - 对比迁移前后的响应时间 + - 对比迁移前后的内存使用 + +5. **文档同步更新** + - 及时更新文档 + - 保持文档和代码一致 + +--- + +## 进度跟踪 + +### 总体进度 + +- [x] 阶段 0:文档与基线准备(100%) +- [x] 阶段 1:基础模块迁移(100%) +- [x] 阶段 2:独立功能模块迁移(100%) +- [ ] 阶段 3:核心复杂模块迁移(60%) +- [ ] 阶段 4:清理和优化(0%) + +### 模块迁移进度 + +- [x] groups(6/6 路由)✅ +- [x] tags(4/4 路由)✅ +- [x] settings(3/3 路由)✅ +- [x] system(3/3 路由)✅ +- [x] audit(1/1 路由)✅ +- [x] pages(3/3 路由)✅ +- [x] temp_emails(3/3 路由)✅ +- [x] oauth(2/2 路由)✅ +- [x] scheduler(1/1 路由)✅ +- [x] emails(4/4 路由)✅ +- [ ] accounts(12/20 路由)- 批次 1 已完成:基础 CRUD + 批量操作 + 导出 + +**总计:** 42/54 路由已迁移(78%) + +--- + +## 参考资料 + +- [PRD-00003-Legacy代码拆分到Controllers层](../PRD/PRD-00003-Legacy代码拆分到Controllers层.md) +- [Outlook邮件管理工具-Legacy迁移到Controllers层FD](../FD/Outlook邮件管理工具-Legacy迁移到Controllers层FD.md) +- [Outlook邮件管理工具-Legacy迁移到Controllers层TDD](../TDD/Outlook邮件管理工具-Legacy迁移到Controllers层TDD.md) +- [00002-前后端拆分-开发者指南](../DEV/00002-前后端拆分-开发者指南.md) +- [CLAUDE.md](../../CLAUDE.md) + +--- + +**文档版本:** v1.0 +**最后更新:** 2026-02-24 +**维护者:** 开发团队 diff --git a/outlook_web/controllers/accounts.py b/outlook_web/controllers/accounts.py new file mode 100644 index 0000000..378c99d --- /dev/null +++ b/outlook_web/controllers/accounts.py @@ -0,0 +1,702 @@ +from __future__ import annotations + +import html +import json +import re +from datetime import datetime +from typing import Any, Dict, List, Optional +from urllib.parse import quote + +from flask import Response, jsonify, request + +from outlook_web.audit import log_audit +from outlook_web.db import get_db +from outlook_web.errors import build_error_payload +from outlook_web.repositories import accounts as accounts_repo +from outlook_web.repositories import groups as groups_repo +from outlook_web.repositories import tags as tags_repo +from outlook_web.security.auth import login_required + + +def sanitize_input(text: str, max_length: int = 500) -> str: + """ + 净化用户输入,防止XSS攻击 + - 转义HTML特殊字符 + - 限制长度 + - 移除控制字符 + """ + if not text: + return "" + + # 限制长度 + text = text[:max_length] + + # 移除控制字符(保留换行和制表符) + text = ''.join(char for char in text if char.isprintable() or char in '\n\t') + + # 转义HTML特殊字符 + text = html.escape(text, quote=True) + + return text + + +# ==================== 账号基础 CRUD API ==================== + + +@login_required +def api_get_accounts() -> Any: + """获取所有账号""" + group_id = request.args.get('group_id', type=int) + accounts = accounts_repo.load_accounts(group_id) + + # 获取每个账号的最后刷新状态(批量查询,避免 N+1) + db = get_db() + last_log_by_account: Dict[int, Dict[str, Any]] = {} + try: + account_ids = [int(a.get('id')) for a in accounts if a.get('id') is not None] + except Exception: + account_ids = [] + + if account_ids: + try: + placeholders = ",".join(["?"] * len(account_ids)) + rows = db.execute(f''' + SELECT l.account_id, l.status, l.error_message, l.created_at + FROM account_refresh_logs l + JOIN ( + SELECT account_id, MAX(id) as max_id + FROM account_refresh_logs + WHERE account_id IN ({placeholders}) + GROUP BY account_id + ) latest + ON l.account_id = latest.account_id AND l.id = latest.max_id + ''', account_ids).fetchall() + for r in rows: + try: + last_log_by_account[int(r['account_id'])] = dict(r) + except Exception: + continue + except Exception: + last_log_by_account = {} + + # 返回时隐藏敏感信息 + safe_accounts = [] + for acc in accounts: + acc_id = acc.get('id') + try: + acc_id_int = int(acc_id) + except Exception: + acc_id_int = None + last_refresh_log = last_log_by_account.get(acc_id_int) if acc_id_int is not None else None + + safe_accounts.append({ + 'id': acc['id'], + 'email': acc['email'], + 'client_id': acc['client_id'][:8] + '...' if len(acc['client_id']) > 8 else acc['client_id'], + 'group_id': acc.get('group_id'), + 'group_name': acc.get('group_name', '默认分组'), + 'group_color': acc.get('group_color', '#666666'), + 'remark': acc.get('remark', ''), + 'status': acc.get('status', 'active'), + 'last_refresh_at': acc.get('last_refresh_at', ''), + 'last_refresh_status': last_refresh_log.get('status') if last_refresh_log else None, + 'last_refresh_error': last_refresh_log.get('error_message') if last_refresh_log else None, + 'created_at': acc.get('created_at', ''), + 'updated_at': acc.get('updated_at', ''), + 'tags': acc.get('tags', []) + }) + return jsonify({'success': True, 'accounts': safe_accounts}) + + +@login_required +def api_get_account(account_id: int) -> Any: + """获取单个账号详情""" + account = accounts_repo.get_account_by_id(account_id) + if not account: + return jsonify({'success': False, 'error': '账号不存在'}) + + return jsonify({ + 'success': True, + 'account': { + 'id': account['id'], + 'email': account['email'], + # 敏感字段默认不回显(避免泄露);如需查看请走"导出+二次验证" + 'password': '', + 'client_id': account['client_id'], + 'refresh_token': '', + 'has_password': bool(account.get('password')), + 'has_refresh_token': bool(account.get('refresh_token')), + 'group_id': account.get('group_id'), + 'group_name': account.get('group_name', '默认分组'), + 'remark': account.get('remark', ''), + 'status': account.get('status', 'active'), + 'created_at': account.get('created_at', ''), + 'updated_at': account.get('updated_at', '') + } + }) + + +@login_required +def api_add_account() -> Any: + """添加账号""" + data = request.json + account_str = data.get('account_string', '') + group_id = data.get('group_id', 1) + + if not account_str: + return jsonify({'success': False, 'error': '请输入账号信息'}) + + # 校验分组 + target_group = groups_repo.get_group_by_id(group_id) + if not target_group: + return jsonify({'success': False, 'error': '分组不存在'}) + if target_group.get('is_system'): + return jsonify({'success': False, 'error': '不能导入到系统分组'}) + + def sanitize_credential_field(value: Any, max_length: int) -> str: + if value is None: + return '' + text = str(value) + text = text.replace('\r', '').replace('\n', '').replace('\t', '') + text = text.strip() + if len(text) > max_length: + text = text[:max_length] + # 移除不可见控制字符 + text = ''.join(ch for ch in text if ch.isprintable()) + return text + + def parse_account_string(line: str) -> Optional[Dict[str, str]]: + """解析账号字符串(格式:email----password----client_id----refresh_token)""" + parts = line.strip().split('----') + if len(parts) >= 4: + return { + 'email': parts[0].strip(), + 'password': parts[1], + 'client_id': parts[2].strip(), + # refresh_token 可能包含 '----',这里把剩余部分合并回去 + 'refresh_token': '----'.join(parts[3:]).strip() + } + return None + + # 支持批量导入(多行)+ 逐行校验与错误定位 + raw_lines = account_str.splitlines() + imported = 0 + failed = 0 + errors: List[Dict[str, Any]] = [] + errors_total = 0 + max_error_details = 50 + + db = get_db() + for line_no, raw in enumerate(raw_lines, start=1): + line = (raw or '').strip() + if not line: + continue + + parsed = parse_account_string(line) + if not parsed: + failed += 1 + errors_total += 1 + if len(errors) < max_error_details: + errors.append({'line': line_no, 'error': '格式错误,应为:邮箱----密码----client_id----refresh_token'}) + continue + + email_addr = sanitize_credential_field(parsed.get('email'), 320) + password = sanitize_credential_field(parsed.get('password'), 500) + client_id = sanitize_credential_field(parsed.get('client_id'), 200) + refresh_token = sanitize_credential_field(parsed.get('refresh_token'), 4096) + + if not email_addr or not client_id or not refresh_token: + failed += 1 + errors_total += 1 + if len(errors) < max_error_details: + errors.append({'line': line_no, 'email': email_addr, 'error': '邮箱、Client ID、Refresh Token 不能为空'}) + continue + + # 基础邮箱格式校验 + if not re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', email_addr): + failed += 1 + errors_total += 1 + if len(errors) < max_error_details: + errors.append({'line': line_no, 'email': email_addr, 'error': '邮箱格式不正确'}) + continue + + ok = accounts_repo.add_account(email_addr, password, client_id, refresh_token, group_id, db=db, commit=False) + if ok: + imported += 1 + continue + + failed += 1 + errors_total += 1 + reason = '写入失败' + try: + exists = db.execute('SELECT 1 FROM accounts WHERE email = ? LIMIT 1', (email_addr,)).fetchone() + if exists: + reason = '邮箱已存在' + except Exception: + pass + if len(errors) < max_error_details: + errors.append({'line': line_no, 'email': email_addr, 'error': reason}) + + summary = { + 'group_id': group_id, + 'total_lines': len(raw_lines), + 'imported': imported, + 'failed': failed, + 'errors_total': errors_total, + 'errors_returned': len(errors), + 'errors_truncated': errors_total > len(errors) + } + + message = f'导入完成:成功 {imported} 个,失败 {failed} 个' + + if imported > 0: + try: + db.commit() + except Exception: + try: + db.rollback() + except Exception: + pass + return jsonify({'success': False, 'error': '数据库写入失败,请重试'}) + log_audit('import', 'account', None, f"{message},目标分组ID={group_id}") + return jsonify({'success': True, 'message': message, 'summary': summary, 'errors': errors}) + + return jsonify({'success': False, 'error': message, 'summary': summary, 'errors': errors}) + + +@login_required +def api_update_account(account_id: int) -> Any: + """更新账号""" + data = request.json + + # 检查是否只更新状态 + if 'status' in data and len(data) == 1: + # 只更新状态 + return _api_update_account_status(account_id, data['status']) + + email_addr = (data.get('email') or '').strip() + password = data.get('password') + client_id = data.get('client_id') + refresh_token = data.get('refresh_token') + try: + group_id = int(data.get('group_id', 1) or 1) + except Exception: + group_id = 1 + remark = sanitize_input(data.get('remark', ''), max_length=200) + status = data.get('status', 'active') + + if not email_addr: + return jsonify({'success': False, 'error': '邮箱不能为空'}) + + target_group = groups_repo.get_group_by_id(group_id) + if not target_group: + error_payload = build_error_payload( + code="GROUP_NOT_FOUND", + message="分组不存在", + err_type="NotFoundError", + status=404, + details=f"group_id={group_id}", + ) + return jsonify({'success': False, 'error': error_payload}), 404 + + if target_group.get('is_system'): + error_payload = build_error_payload( + code="SYSTEM_GROUP_PROTECTED", + message="不能移动到系统分组", + err_type="ForbiddenError", + status=403, + details=f"group_id={group_id}", + ) + return jsonify({'success': False, 'error': error_payload}), 403 + + if accounts_repo.update_account(account_id, email_addr, password, client_id, refresh_token, group_id, remark, status): + changed_fields = [] + if isinstance(client_id, str) and client_id.strip(): + changed_fields.append('client_id') + if isinstance(password, str) and password.strip(): + changed_fields.append('password') + if isinstance(refresh_token, str) and refresh_token.strip(): + changed_fields.append('refresh_token') + details = json.dumps({ + "email": email_addr, + "group_id": group_id, + "status": status, + "changed_fields": changed_fields + }, ensure_ascii=False) + log_audit('update', 'account', str(account_id), details) + return jsonify({'success': True, 'message': '账号更新成功'}) + else: + return jsonify({'success': False, 'error': '更新失败'}) + + +def _api_update_account_status(account_id: int, status: str) -> Any: + """只更新账号状态""" + db = get_db() + try: + db.execute(''' + UPDATE accounts + SET status = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (status, account_id)) + db.commit() + return jsonify({'success': True, 'message': '状态更新成功'}) + except Exception: + return jsonify({'success': False, 'error': '更新失败'}) + + +@login_required +def api_delete_account(account_id: int) -> Any: + """删除账号""" + email_addr = '' + try: + db = get_db() + row = db.execute('SELECT email FROM accounts WHERE id = ?', (account_id,)).fetchone() + email_addr = row['email'] if row else '' + except Exception: + email_addr = '' + if accounts_repo.delete_account_by_id(account_id): + log_audit('delete', 'account', str(account_id), f"删除账号:{email_addr}" if email_addr else "删除账号") + return jsonify({'success': True}) + else: + return jsonify({'success': False, 'error': '删除失败'}) + + +@login_required +def api_delete_account_by_email(email_addr: str) -> Any: + """根据邮箱地址删除账号""" + if accounts_repo.delete_account_by_email(email_addr): + log_audit('delete', 'account', email_addr, f"删除账号:{email_addr}") + return jsonify({'success': True}) + else: + return jsonify({'success': False, 'error': '删除失败'}) + + +@login_required +def api_batch_delete_accounts() -> Any: + """ + 批量删除账号 API + + 功能:支持一次性删除多个账号,记录审计日志 + """ + data = request.get_json() + account_ids = data.get('account_ids', []) + + if not account_ids: + return jsonify({'success': False, 'error': '请选择要删除的账号'}) + + if not isinstance(account_ids, list): + return jsonify({'success': False, 'error': '参数格式错误'}) + + deleted_count = 0 + failed_count = 0 + + for account_id in account_ids: + try: + # 获取邮箱地址用于审计日志 + db = get_db() + row = db.execute('SELECT email FROM accounts WHERE id = ?', (account_id,)).fetchone() + email_addr = row['email'] if row else '' + + if accounts_repo.delete_account_by_id(account_id): + log_audit('delete', 'account', str(account_id), f"批量删除账号:{email_addr}" if email_addr else "批量删除账号") + deleted_count += 1 + else: + failed_count += 1 + except Exception: + failed_count += 1 + + return jsonify({ + 'success': True, + 'message': f'成功删除 {deleted_count} 个账号' + (f',失败 {failed_count} 个' if failed_count > 0 else ''), + 'deleted_count': deleted_count, + 'failed_count': failed_count + }) + + +# ==================== 批量操作 API ==================== + + +@login_required +def api_batch_manage_tags() -> Any: + """批量管理账号标签""" + data = request.json + account_ids: List[int] = data.get('account_ids', []) + tag_id = data.get('tag_id') + action = data.get('action') # add, remove + + if not account_ids or not tag_id or not action: + return jsonify({'success': False, 'error': '参数不完整'}) + + count = 0 + for acc_id in account_ids: + if action == 'add': + if tags_repo.add_account_tag(acc_id, tag_id): + count += 1 + elif action == 'remove': + if tags_repo.remove_account_tag(acc_id, tag_id): + count += 1 + + try: + details = json.dumps({'action': action, 'tag_id': tag_id, 'accounts': len(account_ids), 'affected': count}, ensure_ascii=False) + except Exception: + details = f"action={action} tag_id={tag_id} accounts={len(account_ids)} affected={count}" + log_audit('update', 'account_tags', str(tag_id), details) + return jsonify({'success': True, 'message': f'成功处理 {count} 个账号'}) + + +@login_required +def api_batch_update_account_group() -> Any: + """批量更新账号分组""" + data = request.json + account_ids = data.get('account_ids', []) + group_id = data.get('group_id') + + if not account_ids: + return jsonify({'success': False, 'error': '请选择要修改的账号'}) + + if not group_id: + return jsonify({'success': False, 'error': '请选择目标分组'}) + + # 验证分组存在 + group = groups_repo.get_group_by_id(group_id) + if not group: + return jsonify({'success': False, 'error': '目标分组不存在'}) + + # 检查是否是临时邮箱分组(系统保留分组) + if group.get('is_system'): + return jsonify({'success': False, 'error': '不能移动到系统分组'}) + + # 批量更新 + db = get_db() + try: + placeholders = ','.join('?' * len(account_ids)) + db.execute(f''' + UPDATE accounts SET group_id = ?, updated_at = CURRENT_TIMESTAMP + WHERE id IN ({placeholders}) + ''', [group_id] + account_ids) + db.commit() + log_audit('update', 'account_group', str(group_id), f"批量移动分组:账号数={len(account_ids)}") + return jsonify({ + 'success': True, + 'message': f'已将 {len(account_ids)} 个账号移动到「{group["name"]}」分组' + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + + +@login_required +def api_search_accounts() -> Any: + """全局搜索账号""" + query = request.args.get('q', '').strip() + + if not query: + return jsonify({'success': True, 'accounts': []}) + + db = get_db() + # 支持搜索邮箱、备注和标签 + cursor = db.execute(''' + SELECT DISTINCT a.*, g.name as group_name, g.color as group_color + FROM accounts a + LEFT JOIN groups g ON a.group_id = g.id + LEFT JOIN account_tags at ON a.id = at.account_id + LEFT JOIN tags t ON at.tag_id = t.id + WHERE a.email LIKE ? OR a.remark LIKE ? OR t.name LIKE ? + ORDER BY a.created_at DESC + ''', (f'%{query}%', f'%{query}%', f'%{query}%')) + + rows = cursor.fetchall() + + # 批量加载标签与最后刷新状态,避免 N+1 查询 + account_rows: List[Dict[str, Any]] = [dict(r) for r in rows] + try: + account_ids = [int(a.get('id')) for a in account_rows if a.get('id') is not None] + except Exception: + account_ids = [] + + tags_by_account: Dict[int, List[Dict[str, Any]]] = {} + last_log_by_account: Dict[int, Dict[str, Any]] = {} + if account_ids: + try: + placeholders = ",".join(["?"] * len(account_ids)) + tag_rows = db.execute(f''' + SELECT at.account_id as account_id, t.* + FROM account_tags at + JOIN tags t ON t.id = at.tag_id + WHERE at.account_id IN ({placeholders}) + ORDER BY at.account_id ASC, t.created_at DESC + ''', account_ids).fetchall() + for tr in tag_rows: + tag_dict = dict(tr) + acc_id = tag_dict.pop('account_id', None) + if acc_id is None: + continue + tags_by_account.setdefault(int(acc_id), []).append(tag_dict) + except Exception: + tags_by_account = {} + + try: + placeholders = ",".join(["?"] * len(account_ids)) + log_rows = db.execute(f''' + SELECT l.account_id, l.status, l.error_message, l.created_at + FROM account_refresh_logs l + JOIN ( + SELECT account_id, MAX(id) as max_id + FROM account_refresh_logs + WHERE account_id IN ({placeholders}) + GROUP BY account_id + ) latest + ON l.account_id = latest.account_id AND l.id = latest.max_id + ''', account_ids).fetchall() + for lr in log_rows: + try: + last_log_by_account[int(lr['account_id'])] = dict(lr) + except Exception: + continue + except Exception: + last_log_by_account = {} + + safe_accounts = [] + for acc in account_rows: + acc_id = acc.get('id') + try: + acc_id_int = int(acc_id) + except Exception: + acc_id_int = None + + tags = tags_by_account.get(acc_id_int, []) if acc_id_int is not None else [] + last_refresh_log = last_log_by_account.get(acc_id_int) if acc_id_int is not None else None + + safe_accounts.append({ + 'id': acc['id'], + 'email': acc['email'], + 'client_id': acc['client_id'][:8] + '...' if len(acc['client_id']) > 8 else acc['client_id'], + 'group_id': acc['group_id'], + 'group_name': acc['group_name'] if acc['group_name'] else '默认分组', + 'group_color': acc['group_color'] if acc['group_color'] else '#666666', + 'remark': acc['remark'] if acc['remark'] else '', + 'status': acc['status'] if acc['status'] else 'active', + 'created_at': acc['created_at'] if acc['created_at'] else '', + 'updated_at': acc['updated_at'] if acc['updated_at'] else '', + 'tags': tags, + 'last_refresh_status': last_refresh_log.get('status') if last_refresh_log else None, + 'last_refresh_error': last_refresh_log.get('error_message') if last_refresh_log else None + }) + + return jsonify({'success': True, 'accounts': safe_accounts}) + + +# ==================== 导出功能 API ==================== + + +@login_required +def api_export_all_accounts() -> Any: + """导出所有邮箱账号为 TXT 文件(需要二次验证)""" + from outlook_web.security.auth import consume_export_verify_token + + # 检查二次验证token(一次性) + verify_token = request.args.get('verify_token') + ok, error_message = consume_export_verify_token(verify_token) + if not ok: + return jsonify({'success': False, 'error': error_message, 'need_verify': True}), 401 + + # 使用 load_accounts 获取所有账号(自动解密) + accounts = accounts_repo.load_accounts() + + if not accounts: + return jsonify({'success': False, 'error': '没有邮箱账号'}) + + # 记录审计日志 + log_audit('export', 'all_accounts', None, f"导出所有账号,共 {len(accounts)} 个") + + # 生成导出内容(格式:email----password----client_id----refresh_token) + lines = [] + for acc in accounts: + line = f"{acc['email']}----{acc.get('password', '')}----{acc['client_id']}----{acc['refresh_token']}" + lines.append(line) + + content = '\n'.join(lines) + + # 生成文件名(使用 URL 编码处理中文) + filename = f"all_accounts_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" + encoded_filename = quote(filename) + + # 返回文件下载响应 + return Response( + content, + mimetype='text/plain; charset=utf-8', + headers={ + 'Content-Disposition': f"attachment; filename*=UTF-8''{encoded_filename}" + } + ) + + +@login_required +def api_export_selected_accounts() -> Any: + """导出选中分组的邮箱账号为 TXT 文件(需要二次验证)""" + from outlook_web.security.auth import consume_export_verify_token + + data = request.json + group_ids = data.get('group_ids', []) + verify_token = data.get('verify_token') + + ok, error_message = consume_export_verify_token(verify_token) + if not ok: + return jsonify({'success': False, 'error': error_message, 'need_verify': True}), 401 + + if not group_ids: + return jsonify({'success': False, 'error': '请选择要导出的分组'}) + + # 获取选中分组下的所有账号(使用 load_accounts 自动解密) + all_accounts = [] + for group_id in group_ids: + accounts = accounts_repo.load_accounts(group_id) + all_accounts.extend(accounts) + + if not all_accounts: + return jsonify({'success': False, 'error': '选中的分组下没有邮箱账号'}) + + # 记录审计日志 + log_audit('export', 'selected_groups', ','.join(map(str, group_ids)), f"导出选中分组的 {len(all_accounts)} 个账号") + + # 生成导出内容 + lines = [] + for acc in all_accounts: + line = f"{acc['email']}----{acc.get('password', '')}----{acc['client_id']}----{acc['refresh_token']}" + lines.append(line) + + content = '\n'.join(lines) + + # 生成文件名 + filename = f"selected_accounts_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" + encoded_filename = quote(filename) + + # 返回文件下载响应 + return Response( + content, + mimetype='text/plain; charset=utf-8', + headers={ + 'Content-Disposition': f"attachment; filename*=UTF-8''{encoded_filename}" + } + ) + + +@login_required +def api_generate_export_verify_token() -> Any: + """���成导出验证token(二次验证)""" + from outlook_web.security.auth import issue_export_verify_token, get_client_ip, get_user_agent + from outlook_web.repositories import settings as settings_repo + from outlook_web.security.crypto import verify_password + + data = request.json + password = data.get('password', '') + + # 验证密码 + stored_password = settings_repo.get_login_password() + if not verify_password(password, stored_password): + return jsonify({'success': False, 'error': '密码错误'}) + + # 生成一次性 token + client_ip = get_client_ip() + user_agent = get_user_agent() + verify_token = issue_export_verify_token(client_ip, user_agent) + return jsonify({'success': True, 'verify_token': verify_token}) diff --git a/outlook_web/routes/accounts.py b/outlook_web/routes/accounts.py index 5ed76ff..dc1fce6 100644 --- a/outlook_web/routes/accounts.py +++ b/outlook_web/routes/accounts.py @@ -2,32 +2,39 @@ from __future__ import annotations from flask import Blueprint +from outlook_web.controllers import accounts as accounts_controller + def create_blueprint(*, impl) -> Blueprint: bp = Blueprint("accounts", __name__) - bp.add_url_rule("/api/accounts", view_func=impl.api_get_accounts, methods=["GET"]) - bp.add_url_rule("/api/accounts", view_func=impl.api_add_account, methods=["POST"]) - bp.add_url_rule("/api/accounts/", view_func=impl.api_get_account, methods=["GET"]) - bp.add_url_rule("/api/accounts/", view_func=impl.api_update_account, methods=["PUT"]) - bp.add_url_rule("/api/accounts/", view_func=impl.api_delete_account, methods=["DELETE"]) - bp.add_url_rule("/api/accounts/email/", view_func=impl.api_delete_account_by_email, methods=["DELETE"]) + # 基础 CRUD(已迁移到 controllers) + bp.add_url_rule("/api/accounts", view_func=accounts_controller.api_get_accounts, methods=["GET"]) + bp.add_url_rule("/api/accounts", view_func=accounts_controller.api_add_account, methods=["POST"]) + bp.add_url_rule("/api/accounts/", view_func=accounts_controller.api_get_account, methods=["GET"]) + bp.add_url_rule("/api/accounts/", view_func=accounts_controller.api_update_account, methods=["PUT"]) + bp.add_url_rule("/api/accounts/", view_func=accounts_controller.api_delete_account, methods=["DELETE"]) + bp.add_url_rule("/api/accounts/email/", view_func=accounts_controller.api_delete_account_by_email, methods=["DELETE"]) - bp.add_url_rule("/api/accounts/search", view_func=impl.api_search_accounts, methods=["GET"]) - bp.add_url_rule("/api/accounts/batch-update-group", view_func=impl.api_batch_update_account_group, methods=["POST"]) - bp.add_url_rule("/api/accounts/batch-delete", view_func=impl.api_batch_delete_accounts, methods=["POST"]) - bp.add_url_rule("/api/accounts/tags", view_func=impl.api_batch_manage_tags, methods=["POST"]) + # 批量操作(已迁移到 controllers) + bp.add_url_rule("/api/accounts/search", view_func=accounts_controller.api_search_accounts, methods=["GET"]) + bp.add_url_rule("/api/accounts/batch-update-group", view_func=accounts_controller.api_batch_update_account_group, methods=["POST"]) + bp.add_url_rule("/api/accounts/batch-delete", view_func=accounts_controller.api_batch_delete_accounts, methods=["POST"]) + bp.add_url_rule("/api/accounts/tags", view_func=accounts_controller.api_batch_manage_tags, methods=["POST"]) - bp.add_url_rule("/api/accounts/export", view_func=impl.api_export_all_accounts, methods=["GET"]) - bp.add_url_rule("/api/accounts/export-selected", view_func=impl.api_export_selected_accounts, methods=["POST"]) - bp.add_url_rule("/api/export/verify", view_func=impl.api_generate_export_verify_token, methods=["POST"]) + # 导出功能(已迁移到 controllers) + bp.add_url_rule("/api/accounts/export", view_func=accounts_controller.api_export_all_accounts, methods=["GET"]) + bp.add_url_rule("/api/accounts/export-selected", view_func=accounts_controller.api_export_selected_accounts, methods=["POST"]) + bp.add_url_rule("/api/export/verify", view_func=accounts_controller.api_generate_export_verify_token, methods=["POST"]) + # Token 刷新(暂保留 legacy 实现,后续迁移) bp.add_url_rule("/api/accounts//refresh", view_func=impl.api_refresh_account, methods=["POST"]) bp.add_url_rule("/api/accounts/refresh-all", view_func=impl.api_refresh_all_accounts, methods=["GET"]) bp.add_url_rule("/api/accounts//retry-refresh", view_func=impl.api_retry_refresh_account, methods=["POST"]) bp.add_url_rule("/api/accounts/refresh-failed", view_func=impl.api_refresh_failed_accounts, methods=["POST"]) bp.add_url_rule("/api/accounts/trigger-scheduled-refresh", view_func=impl.api_trigger_scheduled_refresh, methods=["GET"]) + # 刷新日志(暂保留 legacy 实现,后续迁移) bp.add_url_rule("/api/accounts/refresh-logs", view_func=impl.api_get_refresh_logs, methods=["GET"]) bp.add_url_rule("/api/accounts//refresh-logs", view_func=impl.api_get_account_refresh_logs, methods=["GET"]) bp.add_url_rule("/api/accounts/refresh-logs/failed", view_func=impl.api_get_failed_refresh_logs, methods=["GET"])