Files
rssKeeper/rssKeeper-code-review.md
T
congsh c59dd304f7 fix: 端口更换 & 代码审核修复
端口:
- 服务端口 8000 → 7329
- 前端开发端口 5173 → 7330

安全:
- CORS 收紧为白名单,关闭 credentials
- SPA 路由白名单完善
- 前端 XSS 转义

可靠性:
- 时区统一为 datetime.now(timezone.utc)
- 文章入库改为内存去重 + 增量计数
- OPML 导入改为 body 参数接收
- OPML 导出 URL XML 转义
- 首次抓取改为 BackgroundTasks 异步
- articles.py HTTPException 移到顶部 import
- FTS5 异常显式日志
- FTS5 查询加引号包裹防布尔注入
- 中文摘要支持中文标点
- 去掉未使用的 hashlib import

部署:
- Dockerfile 锁 python:3.12.7-slim
- requirements 锁定具体版本
- healthcheck 不用 curl(镜像里没有)
- docker-compose 使用 .env 文件
- 新增 .env 配置文件
2026-06-11 14:31:29 +08:00

21 KiB
Raw Blame History

rssKeeper 代码审核报告

审核日期:2026-06-11 审核范围:后端(Python / FastAPI+ 前端(Vue 3+ 部署(Docker) 审核版本:54e7db0 feat: init rssKeeper - RSS 抓取、管理与检索系统


0. 项目概览

项目 信息
项目名 rssKeeper — RSS 抓取、管理与检索系统
后端技术栈 Python 3.12 + FastAPI + SQLAlchemy 2.0 + APScheduler + SQLite (FTS5)
前端技术栈 Vue 3 + Vue Router 4 + Element Plus + Vite
部署 Docker 多阶段构建(前端 Node 20 + 后端 python:3.12-slim
代码规模 后端 8 个核心文件 + 4 个 router,前端 1 个根组件 + 4 个 view

整体评价:架构清晰、模块划分合理、命名规范统一,技术栈选型恰当。但安全、可靠性、配置三方面存在多个需立即处理的问题。

综合评分

维度 评分 说明
架构清晰度 分层明确,routers/services 分离好
可读性 函数短小、命名合理;个别处手写 dict 偏多
安全性 CORS、鉴权、XSS 均有隐患
可靠性 缺关键单测;部分异常被静默吞掉
性能 数据量小时无问题,规模化时 count/contains/同步抓取是瓶颈
工程化 缺测试、CI、lint、日志、类型检查

1. 安全问题(高优先级)

1.1 CORS 策略过于宽松

  • 位置backend/main.py:39-45
  • 风险等级🔴
  • 现状
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        ...
    )
    
  • 问题
    • allow_origins=["*"]allow_credentials=True 同时启用本身已违反 CORS 规范(部分浏览器会拒绝);
    • 对外暴露的 /api/v1/external/* 形同裸奔;
    • 任意来源页面都能携带用户凭据调用 API。
  • 建议:改为白名单(如 ["http://localhost:5173", "https://your-domain"]),并去掉 credentials。

1.2 XSS 风险

  • 位置frontend/src/views/ArticleDetail.vue:47-53
  • 风险等级🟠
  • 现状
    const formatContent = (content) => {
      if (!content) return '<p style="color: #718096;">暂无内容</p>'
      return content.replace(/\n/g, '<br>').replace(/  /g, '&nbsp;&nbsp;')
    }
    
  • 问题
    • v-html + 直接拼接原始内容;
    • 后端 clean_htmlBeautifulSoup.get_text() 提取的纯文本目前不会注入 XSS,但一旦后端切换到保留 HTML(如想支持代码块、链接)就立即变成漏洞。
  • 建议
    • 显式 escape <>&"',或
    • marked + DOMPurify 走白名单富文本路径,并明确文档化"内容已 sanitize"。

1.3 缺少鉴权 / 限流

  • 位置:所有 /api/api/v1/external 端点
  • 风险等级🔴
  • 问题
    • 后端没有任何认证、限流、防滥用机制;
    • routers/external_api.py 明确说"供 AI/外部系统调用",却无 API Key / Token / 速率限制;
    • 任何能访问 8000 端口的客户端都能增删 RSS 源、删除文章、触发抓取;
    • import-opml 接受任意字符串并解析 XML存在 XXE 风险Python 3.x xml.etree 默认禁止外部实体,影响较小但需关注)。
  • 建议
    • 至少加一个 X-API-Key 中间件;
    • 或在 external_api 下使用独立的密钥前缀;
    • OPML 导入限制单文件大小、条目数量。

1.4 静态文件 SPA 兜底白名单不完整

  • 位置backend/main.py:65-74
  • 风险等级🟡
  • 现状
    @app.get("/{full_path:path}")
    async def serve_spa(full_path: str):
        if full_path.startswith("api/") or full_path.startswith("docs") or full_path.startswith("openapi.json"):
            return {"detail": "Not found"}
        ...
        return FileResponse(index_path)
    
  • 问题:因为最终只返回 index.html没有目录穿越风险;但白名单不完整(漏了 redoc/docs/oauth2-redirect、OpenAPI 变体等)。
  • 建议:显式 mount SPA 路由前缀,或用 APIRouter 把所有非 API 路由兜底。

2. 可靠性 / 健壮性(高优先级)

2.1 时区处理不一致

  • 位置backend/rss_fetcher.py:106, 111models.py:48health_checker.py:25, 30, 95external_api.py:24, 139
  • 风险等级🟡
  • 现状
    # rss_fetcher.py
    published_at = datetime(*entry.published_parsed[:6], tzinfo=timezone.utc).replace(tzinfo=None)
    
  • 问题
    • 拿到 UTC 时间后直接剥掉时区信息,再存入 DateTimenaive)字段;
    • 数据库里所有时间都是 UTC,但 models.py:48 health_status 又用 datetime.utcnow() 比较;
    • 跨时区用户看到"最后抓取时间"会有偏差;
    • 前端 formatTimetoLocaleString 会按浏览器时区渲染 → 前后端时间基准不一致会导致"未来时间"。
  • 建议DB 全部存 datetime(naive UTC),并在返回时显式带 Z 后缀或转换为本地时区;或改用 datetime.now(timezone.utc) 统一时区。

2.2 ArticleDetail ID 类型校验缺失

  • 位置frontend/src/views/ArticleDetail.vue:56
  • 风险等级🟡
  • 现状
    const id = parseInt(route.params.id)
    if (!id) return
    
  • 问题:若 id"3abc"parseInt 会得到 3,然后请求失败但 UI 静默。
  • 建议:使用正则校验或 Number.isInteger

2.3 全表级重复检测 + 唯一约束冲突

  • 位置backend/rss_fetcher.py:229-241
  • 风险等级🟠
  • 现状
    existing = db.query(Article).filter(Article.link == article_data["link"]).first()
    if existing:
        existing.title = article_data["title"] or existing.title
        ...
    else:
        article = Article(**article_data)
        db.add(article)
        new_count += 1
    
  • 问题
    • 每次抓取每篇文章都触发一次 SELECT,大数据量下性能差;
    • 一次抓取未提交 → 同一 feed 内出现 link 重复时会触发 Article.link unique 约束异常(外层 try/except 会回滚整个 feed 的入库),该 feed 当次所有新文章都不会保存
  • 建议
    • 先在内存中 set(article_data["link"] for ...) 去重;
    • 或用 bulk_save_objects 配合 INSERT ... ON CONFLICT DO NOTHINGSQLite 支持)。

2.4 article_count 统计代价高

  • 位置backend/rss_fetcher.py:248
  • 风险等级🟡
  • 现状
    feed.article_count = db.query(Article).filter(Article.feed_id == feed_id).count()
    
  • 问题:每次成功抓取都 COUNT(*)article 表大时不可接受。
  • 建议:维护时使用 feed.article_count += new_count,或在 Article 上加 trigger 维护计数。

2.5 同步"添加后立即抓取"行为

  • 位置backend/routers/feeds.py:127, 130
  • 风险等级🟡
  • 现状
    add_feed_job(feed.id, feed.fetch_interval_minutes)
    fetch_and_store_feed(feed.id)   # 同步阻塞 HTTP 请求
    
  • 问题
    • 在线程池里抓取会阻塞请求线程数秒~数十秒;
    • 1000 个 RSS 源批量导入时(import_opml),每个都同步抓取 → 整个 HTTP 请求会卡死几分钟
  • 建议:把首次抓取改为后台任务(BackgroundTasks 或直接交给 scheduler 的 next_run_time)。

2.6 全局调度器状态与启动顺序耦合

  • 位置backend/scheduler.py:10-15, 60-65
  • 风险等级🟡
  • 现状
    _scheduler = None
    
    def get_scheduler():
        global _scheduler
        if _scheduler is None:
            _scheduler = BackgroundScheduler()
        return _scheduler
    
    def stop_scheduler():
        global _scheduler
        if _scheduler and _scheduler.running:
            _scheduler.shutdown(wait=False)
            _scheduler = None
    
  • 问题
    • BackgroundScheduler 默认会先 start 再 add_job(实际是惰性的),但 FastAPI 进程与 lifespan 启动顺序耦合;
    • add_feed_job 写入与 init_feed_jobs 启动之间没有互斥;
    • stop_scheduler_scheduler = None,但 APScheduler 的 executor/shutdown 可能尚未真正结束 → 下次启动会创建第二个实例。

2.7 FTS5 初始化异常静默吞掉

  • 位置backend/database.py:52-54
  • 风险等级🟠
  • 现状
    try:
        cursor.execute("SELECT sqlite_compileoption_used('ENABLE_FTS5')")
        has_fts5 = cursor.fetchone()[0]
        if not has_fts5:
            print("警告: ...")
            return
    except Exception:
        pass
    
  • 问题
    • pass 静默吞掉异常,FTS5 检测失败时会继续往下执行(进 CREATE VIRTUAL TABLE),最终报错信息对用户非常不友好。
  • 建议:区分 OperationalError vs ProgrammingError;失败时显式 logger.error

2.8 FTS5 用户输入转义不完整

  • 位置backend/fulltext_search.py:14
  • 风险等级🟡
  • 现状
    query = query.replace('"', '""').strip()
    
  • 问题:仅转义双引号,但 FTS5 语法里 * : ( ) OR AND NOT 都有特殊含义:
    • 用户输入 python AND java 会被解释为布尔操作符,可能报错或返回意外结果。
  • 建议:用 fts5 安全的 query_quote 或对短词加 "" 包裹。

2.9 重复 / 延迟 import 掩盖问题

  • 位置backend/routers/articles.py:133
  • 风险等级🟠
  • 现状
    from fastapi import HTTPException     # 文件末尾
    
  • 问题
    • articles.py:3 只 import 了 APIRouter, Depends
    • 文件中 HTTPException 在 line 90/115 用到,靠底部 line 133 那个延迟 import 才工作
    • bug 风险:重构时一旦删掉 line 133 就 500。
  • 建议:在文件顶部一次性 import。

3. 性能问题(中优先级)

3.1 三字段 LIKE 全表扫描

  • 位置backend/routers/feeds.py:69
  • 风险等级🟡
  • 现状
    query = query.filter(
        Feed.title.contains(search) | Feed.url.contains(search) | Feed.description.contains(search)
    )
    
  • 问题:三字段 OR + 前导通配符,全表扫描;数据量大时是瓶颈。
  • 建议:用 SQLite FTS5(已有 articles_fts,扩展一个 feeds_fts)。

3.2 created_at 排序无索引

  • 位置backend/routers/feeds.py:73
  • 风险等级🟡
  • 问题Feed 上没在 created_at 建索引 → 分页排序会随数据量变慢。

3.3 recent_activity join + 排序未优化

  • 位置backend/routers/dashboard.py:40-42
  • 风险等级🟡
  • 现状
    logs = db.query(FetchLog, Feed.title.label("feed_title")).join(Feed).order_by(
        desc(FetchLog.created_at)
    ).limit(limit).all()
    
  • 建议:复合索引 (feed_id, created_at DESC)。同时 get_overall_statsfeeds.all() 拉全表 → feed 数万时内存里逐个调 health_status() 很慢。

3.4 discover_feed_url 顺序 HEAD 请求

  • 位置backend/rss_fetcher.py:60-89
  • 风险等级🟡
  • 问题:对每个常见 path 都做一次 requests.head,没并发。当用户填入的 URL 响应慢时整体阻塞

3.5 前端全文搜索不分页

  • 位置frontend/src/views/Articles.vue:127-129
  • 风险等级🟡
  • 现状
    if (searchQuery.value && searchQuery.value.trim()) {
      const res = await articlesApi.search(searchQuery.value.trim())
      articles.value = res.items || []
    }
    
  • 问题:全文搜索 API 本来支持 skip/limit,但前端未传 → 大量结果时无分页。

3.6 整体统计拉全表再 sum

  • 位置backend/health_checker.py:79-80
  • 风险等级🟡
  • 现状
    total_articles = db.query(Feed).with_entities(Feed.article_count).all()
    total_articles_count = sum(a[0] for a in total_articles) if total_articles else 0
    
  • 建议:直接 db.query(func.sum(Feed.article_count)).scalar() 即可。

3.7 前端图标重复注册

  • 位置frontend/src/main.js:29-31 + 各 view 的 import
  • 风险等级🟢
  • 现状
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
      app.component(key, component)
    }
    
  • 问题:在 main.js 中注册了所有 Element Plus 图标作为全局组件;又各 view 里 import { Plus, Upload, ... } 按需引入 → 重复且浪费 bundle 大小
  • 建议:要么按需全局注册用到的几个,要么去 view 里的按需 import。

4. 功能性 Bug

4.1 OPML 导入 API body/query 不一致

  • 位置frontend/src/api/index.js:36 vs backend/routers/feeds.py:221
  • 风险等级🔴
  • 现状
    // 前端
    importOpml: (content) => api.post('/api/feeds/import-opml', { opml_content: content })
    
    # 后端
    def import_opml(opml_content: str, db: Session = Depends(get_db)):
    
  • 问题
    • 后端期望 opml_content 作为 query 参数(因为只有 db 一个 Depends);
    • 前端却通过 body 传 → 导入功能实际不可用
    • 当前未被发现可能因为没人实际点过这个按钮。
  • 修复方案:在后端加 Pydantic model class OpmlImport(BaseModel): opml_content: str,前端保持 body 传。

4.2 OPML 导出未 escape URL

  • 位置backend/routers/feeds.py:262-272
  • 风险等级🟡
  • 现状
    lines.append(f'  <outline type="rss" text="{title}" xmlUrl="{feed.url}" />')
    
  • 问题title 做了 " 转义,但 feed.url 没做 → URL 含 & 时会破坏 XML。
  • 建议:用 xml.etree.ElementTreexml.sax.saxutils.escape

4.3 中文摘要截断不准确

  • 位置backend/rss_fetcher.py:177
  • 风险等级🟢
  • 现状
    last_period = max(truncated.rfind("。"), truncated.rfind(". "), truncated.rfind("! "), truncated.rfind("? "))
    
  • 问题:中文使用 无空格,后三个都是英文符号 → 中文文本几乎走 fallback + "..."
  • 建议:增加中文标点 的匹配。

4.4 未使用 import

  • 位置backend/rss_fetcher.py:5
  • 风险等级🟢
  • 现状
    import hashlib
    
  • 问题:代码中未使用。

5. 代码质量 / 可维护性

5.1 重复 import

  • 位置backend/routers/dashboard.py:37, 96
  • 风险等级🟢
  • 问题:函数体内局部 import from models import FetchLog,本可顶部一次性 import。

5.2 health_status() 耦合时间源

  • 位置backend/models.py:34-55
  • 风险等级🟢
  • 问题:业务逻辑与 datetime.utcnow() 耦合 → 不易测试。
  • 建议:应接受 now: datetime 参数或单独抽出函数。

5.3 重复字段映射

  • 位置backend/routers/feeds.py:51-95, 142-165
  • 风险等级🟢
  • 问题:都有大量手动 dict 转换 → 改字段时极易漏改。
  • 建议:定义 FeedOut Pydantic 模型并直接 from_attributes

5.4 缺少 TypeScript

  • 位置:前端
  • 风险等级🟡
  • 问题.vue + js,字段拼写错误(如 articles_count 写成 articlesCount)不会在编译期暴露。
  • 建议:至少加 vetur/Volar + JSDoc 注解。

5.5 OpenAPI 文档丰富度

  • 位置backend/routers/external_api.py
  • 风险等级🟢
  • 问题:是给 AI 用的,但完全没有 response_model → OpenAPI schema 弱化(虽然 description 写了用途,但没 sample)。
  • 建议:加 response_model=ExternalRecent 等显式 schema让 AI 消费端更易集成

6. 配置 / 部署问题

6.1 Docker 多阶段 COPY 覆盖

  • 位置Dockerfile:5-8
  • 风险等级🟡
  • 现状
    WORKDIR /app/frontend
    COPY frontend/package.json frontend/package-lock.json* ./
    RUN npm install
    COPY frontend/ .
    
  • 问题
    • 第二次 COPY frontend/ .清空 WORKDIRnpm 已生成的缓存被丢;
    • frontend/package-lock.json*(带星号)若文件不存在,Docker 18+ 行为可能报错。
  • 建议
    RUN --mount=type=cache,target=/app/frontend/node_modules npm ci
    

6.2 运行时保留构建依赖

  • 位置Dockerfile:14-19
  • 风险等级🟡
  • 问题gcc/libxml2-dev/libxslt1-dev 仅在 pip install 时需要,运行时不需要,镜像变大。
  • 建议:用 BuildKit 多阶段把 builder 拆出来,最终镜像只 pip install wheel 包。

6.3 Python 基础镜像未锁版本

  • 位置Dockerfile:11
  • 风险等级🟡
  • 问题python:3.12-slim 会拉 latest,未来构建可能产生不同结果。
  • 建议:锁版本(python:3.12.4-slim)。

6.4 healthcheck 用了 curl,但镜像没装

  • 位置docker-compose.yml:24-27 + Dockerfile
  • 风险等级🟠
  • 现状
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
    
  • 问题python:3.12-slim 镜像默认没有 curlhealthcheck 会一直返回非 0 → 容器会被反复标记 unhealthy。
  • 建议
    test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
    

6.5 数据库 URL 拼接双斜杠

  • 位置docker-compose.yml:14-15 + backend/database.py:8
  • 风险等级🟢
  • 问题
    - DATABASE_URL=/app/data/rsskeeper.db
    
    engine = create_engine(
        f"sqlite:///{DATABASE_URL}",
        ...
    )
    
    • 实际拼成 sqlite:////app/data/rsskeeper.db4 个 /)—— SQLite 接受但易读性差。

6.6 缺日志配置

  • 位置:后端全局
  • 风险等级🟡
  • 问题:后端只靠 print 输出一条 FTS5 警告。生产环境应配置 loggingfile/stdout structured JSON);APScheduler 默认会打 INFO 日志到 stderr。

6.7 requirements 缺版本上界

  • 位置backend/requirements.txt
  • 风险等级🟡
  • 问题7 个包全部是 >=;一旦上游做不兼容变更,构建会失败且不易复现。
  • 建议:锁 == 或加 ~=

6.8 docker-compose 环境变量硬编码

  • 位置docker-compose.yml:14-22
  • 风险等级🟢
  • 建议:将可调参数抽到 .env 文件。

7. 测试 / 工程化

7.1 无任何测试

  • 位置:项目根目录
  • 风险等级🟠
  • 问题:没有 tests/ 目录、pytest/unittest 都没有。
  • 建议
    • parse_articlegenerate_summaryclean_html 单元测试;
    • feeds.py 的路由级集成测试(用 httpx.AsyncClient)。

7.2 无 CI / lint 配置

  • 位置:项目根目录
  • 风险等级🟡
  • 问题:没有 .github/workflows、ESLint、Prettier、black、ruff、mypy 配置。
  • 建议:至少加 ruff check + black --check 到 PR 流程。

8. 优先修复 Top 10

# 位置 严重度 一句话
1 backend/main.py:39-45 🔴 收紧 CORS
2 frontend/src/api/index.js:36backend/routers/feeds.py:221 🔴 OPML 导入 body/query 不一致,功能不可用
3 backend/routers/external_api.py 全部 🔴 外部 API 缺鉴权 / 限流
4 backend/database.py:41-89 🟠 FTS5 初始化异常静默吞掉
5 backend/rss_fetcher.py:229-241 🟠 入库 unique 冲突会回滚整批
6 backend/routers/articles.py:133 🟠 底部 import 掩盖顶部未导入
7 docker-compose.yml:24-27 🟠 healthcheck curl 镜像里没有
8 backend/routers/feeds.py:130 🟡 添加源同步抓取会阻塞 HTTP
9 backend/rss_fetcher.py:106 🟡 时区处理导致显示偏差
10 backend/routers/feeds.py:262-272 🟡 OPML 导出未 escape URL

9. 总结

rssKeeper 是一个定位明确、规模适中的自用型 RSS 管理系统。从代码风格、模块拆分来看,作者具备良好的工程素养;但要在生产环境长期运行,建议优先解决以下三类问题:

  1. 安全基线CORS / 鉴权 / XSS —— 任何对外暴露的服务都必须先补齐;
  2. 正确性 BugOPML 导入功能当前不可用(/feeds/import-opml),属于必须立即修复的 P0
  3. 部署可靠性Docker healthcheck 失效会导致容器反复重启,看似无关紧要但容易掩盖真实故障。

后续若要扩展功能(多用户、订阅推送、标签、阅读列表等),建议先把"测试 + CI + 日志"这套工程基座补齐,再做功能叠加,避免技术债快速累积。