# 2026-06-02 全系统 Bug 扫描与修复

## 日期
2026-06-02

## 变更摘要
对全系统进行静态代码扫描，发现并修复 **7 个 HIGH 级 Bug**。

---

## HIGH-1: `download_file` SSH 连接在流传输前被过早释放

**文件**: `server/api/sync_v2.py`

**问题**: `finally` 块中 `ssh_pool.release()` 在 `StreamingResponse` 生成器尚未开始执行时就关闭连接，导致文件下载中断。

**修复**: 将 `release()` 移入 `file_generator()` 的 `finally` 子句，在流传输完成或客户端断开后再释放；提前退出（404/400/413）在各自分支 `await release()`。

---

## HIGH-2: WebSocket JWT `tv` 默认值 `-1` 与 HTTP 中间件 `0` 不一致

**文件**: `server/api/websocket.py`

**问题**: `_verify_ws_token` 对无 `tv` 字段的旧 token 使用默认值 `-1`，而数据库 `token_version` 为 `NULL/0`，导致 `-1 != 0` 判断失败，合法用户 WebSocket 连接被拒。

**修复**: 对齐 HTTP 中间件逻辑，改为 `payload.get("tv") or 0`，DB 侧 `admin.token_version or 0`。

---

## HIGH-3: `retry_runner` 达到 `max_retries` 后任务永久僵尸

**文件**: `server/background/retry_runner.py`

**问题**: 重试次数达到 `max_retries` 时，任务状态仍维持 `pending` 且 `retry_count < max_retries` 条件不再触发，形成永不被处理的僵尸记录。

**修复**: 成功/失败分支均检查 `retry_count >= max_retries`，达到上限时将 `status` 置为 `failed`，记录原因日志。

---

## HIGH-4: `sync_v2.py` 和 `sync_engine_v2.py` 多处 `stderr` 为 `None` 引发 `TypeError`

**文件**: `server/api/sync_v2.py`, `server/application/services/sync_engine_v2.py`

**问题**: 多处直接执行 `result["stderr"][:N]`，当 SSH 执行结果中 `stderr` 为 `None` 时（如连接超时）抛 `TypeError: 'NoneType' object is not subscriptable`，导致 API 返回 500。

**修复**: 所有 `result["stderr"][:N]` 统一改为 `(result.get("stderr") or "")[:N]`，共修复 5 处。

---

## HIGH-5: `auth_service._verify_password` bcrypt 抛 `ValueError` 导致 500

**文件**: `server/application/services/auth_service.py`

**问题**: DB 中若存在格式异常的 `password_hash`，`bcrypt.checkpw` 会抛 `ValueError`，导致登录接口 500 而非 401。

**修复**: 用 `try/except` 包裹 `bcrypt.checkpw`，异常时返回 `False`（密码不匹配）。

---

## HIGH-6: `install.py /lock` 硬编码查询 `"admin"` 用户名

**文件**: `server/api/install.py`

**问题**: `/lock` 端点通过 `get_by_username("admin")` 验证管理员是否创建，若用户在步骤 4 使用了非 `admin` 的用户名（如 `administrator`），会错误地拒绝锁定（状态 400），且 DB 不可达时会静默放行锁定操作。

**修复**: 改为 `SELECT COUNT(*) FROM admins WHERE is_active = 1` 验证活跃管理员存在；DB 不可达时改为拒绝锁定（503）而非放行。

---

## HIGH-7: `heartbeat_flush.py` 单条失败污染整批事务

**文件**: `server/background/heartbeat_flush.py`

**问题**: 所有服务器在同一 `AsyncSession` 中批量更新，单条 `UPDATE` 失败时仅打日志，但会话处于需要 rollback 的状态，最终 `commit()` 使整批失败，日志显示 "N 台更新" 实际全部失败。同时修复 `datetime` 时区感知问题（naive datetime 写入 MySQL）。

**修复**: 每台服务器使用独立的 `AsyncSessionLocal()` 上下文，单条失败不影响其他；`fromisoformat` 后统一补 `tzinfo=timezone.utc`。

---

## 涉及文件

| 文件 | 修复项 |
|------|-------|
| `server/api/sync_v2.py` | HIGH-1（下载连接）、HIGH-4（stderr None, 3 处） |
| `server/api/websocket.py` | HIGH-2（WebSocket tv） |
| `server/api/install.py` | HIGH-6（/lock 用户名） |
| `server/application/services/auth_service.py` | HIGH-5（bcrypt ValueError） |
| `server/application/services/sync_engine_v2.py` | HIGH-4（stderr None, 2 处） |
| `server/background/retry_runner.py` | HIGH-3（僵尸任务） |
| `server/background/heartbeat_flush.py` | HIGH-7（事务污染、时区） |

## 需要迁移/重启
- 后端重启生效（`supervisorctl restart nexus`）
- 无数据库 migration 变更

## 验证方式
1. `python -c "import server.main"` — 导入无报错 ✓
2. 登录测试 WebSocket 实时推送
3. 下载文件功能测试
4. 查看 `push_retry_jobs` 表确认无 `status=pending` 的僵尸记录
