feat: 迁移 accounts 模块批次 1 到 controllers 层
迁移的路由(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 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c1920d7c1
commit
5e4e5dcb64
3 files changed
+1357
-13
No files matched your search
@@ -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/<id>
|
||||
- [ ] POST /api/groups
|
||||
- [ ] PUT /api/groups/<id>
|
||||
- [ ] DELETE /api/groups/<id>
|
||||
- [ ] GET /api/groups/<id>/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
|
||||
**维护者:** 开发团队
|
||||
@@ -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})
|
||||
@@ -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/<int:account_id>", view_func=impl.api_get_account, methods=["GET"])
|
||||
bp.add_url_rule("/api/accounts/<int:account_id>", view_func=impl.api_update_account, methods=["PUT"])
|
||||
bp.add_url_rule("/api/accounts/<int:account_id>", view_func=impl.api_delete_account, methods=["DELETE"])
|
||||
bp.add_url_rule("/api/accounts/email/<email_addr>", 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/<int:account_id>", view_func=accounts_controller.api_get_account, methods=["GET"])
|
||||
bp.add_url_rule("/api/accounts/<int:account_id>", view_func=accounts_controller.api_update_account, methods=["PUT"])
|
||||
bp.add_url_rule("/api/accounts/<int:account_id>", view_func=accounts_controller.api_delete_account, methods=["DELETE"])
|
||||
bp.add_url_rule("/api/accounts/email/<email_addr>", 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/<int:account_id>/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/<int:account_id>/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/<int:account_id>/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"])
|
||||
|
||||
Reference in new issue
Block a user