Files
SmartUp/optimization-review.md
T
2026-05-17 04:12:00 +08:00

9.0 KiB
Raw Blame History

SmartUp 项目优化审查报告

基于代码审计整理的优化点,按严重程度分级。
审查范围:backend/ 全部 Python 代码、frontend/src/ 全部 TS/Vue 代码、Dockerfile、docker-compose.yml。


🔴 严重 —— 生产稳定性 & 安全风险

1. SQLite 并发写导致锁冲突

位置backend/app/services/scheduler.py:28-91_check_upstream
问题APScheduler BackgroundScheduler 使用线程池执行检测任务,每个线程独立调用 SessionLocal() 创建 SQLAlchemy session。SQLite 的锁粒度是整个文件,多线程并发 commit()(尤其是 website_sync.sync_binding() 内多次提交)必然出现 database is locked 错误。

# scheduler.py:28 — 每个 job 开独立 session
db: Session = SessionLocal()
...

# website_sync.py:178-180 — 内部多次 commit
db.commit()
...
db.commit()

建议:上游检测改为单线程串行队列(apscheduler 设置 max_instances=1 + 单 worker),或将 SQLite 替换为 PostgreSQL。

2. CORS 通配符 + credentials 违反规范

位置backend/app/main.py:48-53
问题allow_origins=["*"]allow_credentials=True 同时设置,浏览器端会忽略通配符(CORS 规范要求显式 origin)。当前实际行为取决于 FastAPI 中间件实现,不可靠。

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],        # ← 通配符
    allow_credentials=True,     # ← 不允许与通配符共存
    ...
)

建议allow_origins 设为显式列表(从环境变量读取),或关闭 allow_credentials

3. HTTP 客户端每次请求新建连接,无连接池

位置

  • backend/app/services/upstream_client.pyUpstreamClient._request,约 L230
  • backend/app/services/website_client.pySub2ApiWebsiteClient._request,约 L95

问题:每次 API 调用都创建新的 httpx.Client(),用完即销毁。一个上游检测周期(login → groups → rates)建立 3 个独立 TCP 连接;N 个上游即 3N 个连接。无连接复用、无 DNS 缓存。

# upstream_client.py
def _request(self, method, path, body=None, auth=True):
    with httpx.Client(timeout=self.timeout) as client:   # ← 每个请求新建
        resp = client.request(...)

建议:在 __init__ 中创建 httpx.AsyncClient 或复用 Client,析构时关闭。

4. 快照表无限增长

位置backend/app/models/snapshot.pyUpstreamRateSnapshot 模型)
问题:每次检测成功都 INSERT 一行,无任何清理策略。按每 10 分钟一次、数据量约 2KB/行估算:

  • 1 个上游 × 1 年 ≈ 52K 行 ≈ 100MB+
  • 10 个上游 × 1 年 ≈ 0.5M 行 ≈ 1GB+

snapshot_jsonText 字段存全量 JSON,查询 ORDER BY captured_at DESC 扫描行数随时间线性增长。

建议:定时清理(保留最近 N 条或最近 M 天),或只保存 diff 增量。

5. 弱默认凭据 + 无登录限流

位置

  • backend/app/config.py:7-8
  • backend/app/routers/auth.py:12-20
  • backend/app/utils/auth.py:41-57

问题

  • admin_password 默认值 "changeme"jwt_secret 默认值 "change-me-in-production"
  • /api/auth/login 无任何速率限制,可暴力枚举
  • /api/auth/logout 是空操作(JWT 无黑名单),token 过期前一直有效
  • JWT 不包含 jti 等唯一标识,无法按需吊销
# config.py
admin_password: str = "changeme"
jwt_secret: str = "change-me-in-production"

# auth.py
@router.post("/logout")
def logout():
    return {"message": "logged out"}  # ← 纯空操作

建议:登录端点加限流(内存令牌桶或 slowapi);密码至少 8 位校验;JWT 加入 jti + 内存/DB 黑名单。

6. 密码被 bcrypt 静默截断到 72 字节

位置backend/app/utils/auth.py:12-18
问题hash_passwordverify_password 都做 [:72] 截断。用户设置超长密码时无提示,登录时输入完整密码也能匹配——用户永远不会发现密码被截断了。

pw = password.encode("utf-8")[:72]
return bcrypt.hashpw(pw, bcrypt.gensalt()).decode("utf-8")

建议:在设置/修改密码时检查长度并给出提示,或改用非截断的哈希方案。


🟡 中等 —— 代码质量 & 维护性

7. 重复工具函数

位置

  • backend/app/services/upstream_client.py:69-80_decimal_str
  • backend/app/services/website_client.py:17-28decimal_string

问题:同一份 Decimal 格式化逻辑在两个文件中重复实现,行为略有差异(函数名不同但逻辑相同)。修改一个必然漏掉另一个。

建议:抽取到 app/utils/number.py 统一引用。

8. 事务边界模糊,部分失败导致状态不一致

位置backend/app/services/scheduler.py:55-88
问题_check_upstream 在同一个 DB session 内顺序执行:

  1. 写 snapshot → db.commit()
  2. 调用 webhook_service.send_rate_changed() → 内部 httpx.post + db.commit()
  3. 调用 website_sync.sync_affected_bindings() → 内部多次 httpx.put + 多次 db.commit()

如果步骤 2 或 3 失败,步骤 1 的 snapshot 已经提交,但 webhook 和网站同步丢失——数据处于"检测到了变化但没通知"的不一致状态。

建议:检测(只读 + snapshot 写入)与通知/同步(webhook + 网站写回)分离为两个独立事务。

9. 硬编码上游 IP 迁移逻辑

位置backend/app/database.py:58-65
问题_migrate_custom_pages() 包含针对特定 IP 的数据迁移:

"host": "%://170.106.100.210",

这是特定部署的遗留数据修正,放在通用数据库初始化代码中。其他部署者每次启动都会跑这段无用代码。

建议:移出到独立 migration 脚本,或在检测到实际数据时按需执行。

10. Scheduler 关闭不等待进行中的任务

位置backend/app/services/scheduler.py:120
问题_scheduler.shutdown(wait=False) 立即返回,正在执行的 job 可能被中断(正在写 DB 或发 HTTP 请求)。

def stop_scheduler() -> None:
    if _scheduler.running:
        _scheduler.shutdown(wait=False)  # ← 不等待

建议:设 wait=True 或给一个合理的 grace period(如 30s)。

11. consecutive_failures 未在启用/编辑时重置

位置backend/app/services/scheduler.py:60-75
问题:失败计数只在检测成功时归零。如果一个上游被禁用(enabled=False)、修改配置、再启用,旧的失败计数会延续——可能立即标记为 unhealthy。同样,手动 check-now 成功也没有重置计数器。

建议:编辑上游或重新启用时重置计数;check-now 成功路径也一并重置。


🟢 轻量 —— 体验 & 运维改进

12. 前端 localStorage key 易冲突

位置frontend/src/api/index.ts:14-15
问题:硬编码 'smartup_token' / 'smartup_email',同域名下多个实例会互相覆盖。

建议:用环境变量或 import.meta.env.VITE_APP_KEY 做 key 前缀。

13. 健康检查开销大

位置docker-compose.yml:23-28
问题CMD ["python", "-c", "import urllib.request; ..."] 每 30 秒启动一次完整 Python 解释器,每次约 50-100ms 启动耗时。

建议Dockerfile 安装 curl,改为 curl -f http://localhost:8000/healthz

14. 缺少 .dockerignore

位置:项目根目录
问题:无 .dockerignorenode_modules/__pycache__data/.venv/ 等大规模目录都会被打包进入 Docker build context,拖慢构建。

建议:添加 .dockerignore,排除 node_modules/data/*.pyc.git/.venv/ 等。

15. 前端请求无网络重试

位置frontend/src/api/index.ts:6-20
问题:axios 拦截器遇到 401 直接跳登录页。网络瞬断、502/503 等临时故障无重试。

建议:引入 axios-retry 实现指数退避重试(对非 401/4xx 可重试)。

16. 快照分页 API 实现微妙

位置backend/app/routers/upstreams.py:256-294
问题list_snapshots 为了计算 diff 数量多取 1 行,与 offset + limit 配合时逻辑复杂:

rows = (
    db.query(...)
    .offset(offset)
    .limit(limit + 1)   # 多取 1 行用于 diff
    .all()
)

offset > 0 时,多取的 1 行是相对于 offset 的额外行,并不一定是前一页的最后一行——diff 结果可能不准。

建议:放弃"附带 diff 计数"设计,改为前端单独调 diff 接口,或直接在前次快照 id 上做 diff。


📋 优先级实施建议

优先级 编号 估算工时 影响面
P0 1, 2, 3, 4, 5 1-2 天 DB 锁死 / 安全漏洞 / 性能衰减
P1 6, 7, 8, 10, 11 0.5 天 数据不一致 / 维护负担
P2 9, 12, 13, 14, 15, 16 0.5 天 部署体验 / 代码整洁

生成日期:2026-06-13 · 审查范围:commit HEAD