c59dd304f7
端口: - 服务端口 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 配置文件
21 KiB
21 KiB
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, ' ') } - 问题:
v-html+ 直接拼接原始内容;- 后端
clean_html用BeautifulSoup.get_text()提取的纯文本目前不会注入 XSS,但一旦后端切换到保留 HTML(如想支持代码块、链接)就立即变成漏洞。
- 建议:
- 显式 escape
<>&"',或 - 用
marked+DOMPurify走白名单富文本路径,并明确文档化"内容已 sanitize"。
- 显式 escape
1.3 缺少鉴权 / 限流
- 位置:所有
/api与/api/v1/external端点 - 风险等级:🔴 高
- 问题:
- 后端没有任何认证、限流、防滥用机制;
routers/external_api.py明确说"供 AI/外部系统调用",却无 API Key / Token / 速率限制;- 任何能访问 8000 端口的客户端都能增删 RSS 源、删除文章、触发抓取;
import-opml接受任意字符串并解析 XML,存在 XXE 风险(Python 3.xxml.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, 111、models.py:48、health_checker.py:25, 30, 95、external_api.py:24, 139 - 风险等级:🟡 中
- 现状:
# rss_fetcher.py published_at = datetime(*entry.published_parsed[:6], tzinfo=timezone.utc).replace(tzinfo=None) - 问题:
- 拿到 UTC 时间后直接剥掉时区信息,再存入
DateTime(naive)字段; - 数据库里所有时间都是 UTC,但
models.py:48health_status又用datetime.utcnow()比较; - 跨时区用户看到"最后抓取时间"会有偏差;
- 前端
formatTime用toLocaleString会按浏览器时区渲染 → 前后端时间基准不一致会导致"未来时间"。
- 拿到 UTC 时间后直接剥掉时区信息,再存入
- 建议: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.linkunique 约束异常(外层try/except会回滚整个 feed 的入库),该 feed 当次所有新文章都不会保存。
- 每次抓取每篇文章都触发一次
- 建议:
- 先在内存中
set(article_data["link"] for ...)去重; - 或用
bulk_save_objects配合INSERT ... ON CONFLICT DO NOTHING(SQLite 支持)。
- 先在内存中
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),最终报错信息对用户非常不友好。
- 建议:区分
OperationalErrorvsProgrammingError;失败时显式logger.error。
2.8 FTS5 用户输入转义不完整
- 位置:
backend/fulltext_search.py:14 - 风险等级:🟡 中
- 现状:
query = query.replace('"', '""').strip() - 问题:仅转义双引号,但 FTS5 语法里
*:()ORANDNOT都有特殊含义:- 用户输入
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_stats会feeds.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:36vsbackend/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.ElementTree或xml.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 转换 → 改字段时极易漏改。
- 建议:定义
FeedOutPydantic 模型并直接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/ .会清空WORKDIR,npm 已生成的缓存被丢; 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 installwheel 包。
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镜像默认没有curl;healthcheck 会一直返回非 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.dbengine = create_engine( f"sqlite:///{DATABASE_URL}", ... )- 实际拼成
sqlite:////app/data/rsskeeper.db(4 个/)—— SQLite 接受但易读性差。
- 实际拼成
6.6 缺日志配置
- 位置:后端全局
- 风险等级:🟡 中
- 问题:后端只靠
print输出一条 FTS5 警告。生产环境应配置logging(file/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_article、generate_summary、clean_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:36 ↔ backend/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 管理系统。从代码风格、模块拆分来看,作者具备良好的工程素养;但要在生产环境长期运行,建议优先解决以下三类问题:
- 安全基线:CORS / 鉴权 / XSS —— 任何对外暴露的服务都必须先补齐;
- 正确性 Bug:OPML 导入功能当前不可用(
/feeds/import-opml),属于必须立即修复的 P0; - 部署可靠性:Docker healthcheck 失效会导致容器反复重启,看似无关紧要但容易掩盖真实故障。
后续若要扩展功能(多用户、订阅推送、标签、阅读列表等),建议先把"测试 + CI + 日志"这套工程基座补齐,再做功能叠加,避免技术债快速累积。