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:
胡绍康andClaude Opus 4.6 committed 2026-02-25 10:23:07 +08:00
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
**维护者:** 开发团队
+702
View File
@@ -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})
+20 -13
View File
@@ -2,32 +2,39 @@ from __future__ import annotations
from flask import Blueprint from flask import Blueprint
from outlook_web.controllers import accounts as accounts_controller
def create_blueprint(*, impl) -> Blueprint: def create_blueprint(*, impl) -> Blueprint:
bp = Blueprint("accounts", __name__) bp = Blueprint("accounts", __name__)
bp.add_url_rule("/api/accounts", view_func=impl.api_get_accounts, methods=["GET"]) # 基础 CRUD(已迁移到 controllers)
bp.add_url_rule("/api/accounts", view_func=impl.api_add_account, methods=["POST"]) bp.add_url_rule("/api/accounts", view_func=accounts_controller.api_get_accounts, methods=["GET"])
bp.add_url_rule("/api/accounts/<int:account_id>", view_func=impl.api_get_account, 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=impl.api_update_account, methods=["PUT"]) 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=impl.api_delete_account, methods=["DELETE"]) bp.add_url_rule("/api/accounts/<int:account_id>", view_func=accounts_controller.api_update_account, methods=["PUT"])
bp.add_url_rule("/api/accounts/email/<email_addr>", view_func=impl.api_delete_account_by_email, methods=["DELETE"]) 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"]) # 批量操作(已迁移到 controllers)
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/search", view_func=accounts_controller.api_search_accounts, methods=["GET"])
bp.add_url_rule("/api/accounts/batch-delete", view_func=impl.api_batch_delete_accounts, methods=["POST"]) 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/tags", view_func=impl.api_batch_manage_tags, 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"]) # 导出功能(已迁移到 controllers)
bp.add_url_rule("/api/accounts/export-selected", view_func=impl.api_export_selected_accounts, methods=["POST"]) bp.add_url_rule("/api/accounts/export", view_func=accounts_controller.api_export_all_accounts, methods=["GET"])
bp.add_url_rule("/api/export/verify", view_func=impl.api_generate_export_verify_token, methods=["POST"]) 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/<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/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/<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/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"]) 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_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/<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"]) bp.add_url_rule("/api/accounts/refresh-logs/failed", view_func=impl.api_get_failed_refresh_logs, methods=["GET"])