Files
rssKeeper/rssKeeper-code-review.md
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

519 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
- **风险等级**:🔴 高
- **现状**
```python
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`
- **风险等级**:🟠 中
- **现状**
```js
const formatContent = (content) => {
if (!content) return '<p style="color: #718096;">暂无内容</p>'
return content.replace(/\n/g, '<br>').replace(/ /g, '&nbsp;&nbsp;')
}
```
- **问题**
- `v-html` + 直接拼接原始内容;
- 后端 `clean_html` 用 `BeautifulSoup.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`
- **风险等级**:🟡 低
- **现状**
```python
@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`
- **风险等级**:🟡 中
- **现状**
```python
# rss_fetcher.py
published_at = datetime(*entry.published_parsed[:6], tzinfo=timezone.utc).replace(tzinfo=None)
```
- **问题**
- 拿到 UTC 时间后**直接剥掉时区信息**,再存入 `DateTime`naive)字段;
- 数据库里所有时间都是 UTC,但 `models.py:48` `health_status` 又用 `datetime.utcnow()` 比较;
- **跨时区用户**看到"最后抓取时间"会有偏差;
- 前端 `formatTime` 用 `toLocaleString` 会按浏览器时区渲染 → 前后端时间基准不一致会导致"未来时间"。
- **建议**DB 全部存 `datetime`(naive UTC),并在返回时显式带 `Z` 后缀或转换为本地时区;或改用 `datetime.now(timezone.utc)` 统一时区。
### 2.2 ArticleDetail ID 类型校验缺失
- **位置**`frontend/src/views/ArticleDetail.vue:56`
- **风险等级**:🟡 中
- **现状**
```js
const id = parseInt(route.params.id)
if (!id) return
```
- **问题**:若 `id` 是 `"3abc"``parseInt` 会得到 `3`,然后请求失败但 UI 静默。
- **建议**:使用正则校验或 `Number.isInteger`。
### 2.3 全表级重复检测 + 唯一约束冲突
- **位置**`backend/rss_fetcher.py:229-241`
- **风险等级**:🟠 中
- **现状**
```python
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 NOTHING`SQLite 支持)。
### 2.4 article_count 统计代价高
- **位置**`backend/rss_fetcher.py:248`
- **风险等级**:🟡 中
- **现状**
```python
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`
- **风险等级**:🟡 中
- **现状**
```python
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`
- **风险等级**:🟡 中
- **现状**
```python
_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`
- **风险等级**:🟠 中
- **现状**
```python
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`
- **风险等级**:🟡 中
- **现状**
```python
query = query.replace('"', '""').strip()
```
- **问题**:仅转义双引号,但 FTS5 语法里 `*` `:` `(` `)` `OR` `AND` `NOT` 都有特殊含义:
- 用户输入 `python AND java` 会被解释为布尔操作符,可能报错或返回意外结果。
- **建议**:用 `fts5` 安全的 `query_quote` 或对短词加 `""` 包裹。
### 2.9 重复 / 延迟 import 掩盖问题
- **位置**`backend/routers/articles.py:133`
- **风险等级**:🟠 中
- **现状**
```python
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`
- **风险等级**:🟡 中
- **现状**
```python
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`
- **风险等级**:🟡 中
- **现状**
```python
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`
- **风险等级**:🟡 中
- **现状**
```js
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`
- **风险等级**:🟡 中
- **现状**
```python
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
- **风险等级**:🟢 低
- **现状**
```js
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`
- **风险等级**:🔴 高
- **现状**
```js
// 前端
importOpml: (content) => api.post('/api/feeds/import-opml', { opml_content: content })
```
```python
# 后端
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`
- **风险等级**:🟡 中
- **现状**
```python
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`
- **风险等级**:🟢 低
- **现状**
```python
last_period = max(truncated.rfind("。"), truncated.rfind(". "), truncated.rfind("! "), truncated.rfind("? "))
```
- **问题**:中文使用 `` 但**无空格**,后三个都是英文符号 → 中文文本几乎走 fallback `+ "..."`。
- **建议**:增加中文标点 ``、`` 及 `` 的匹配。
### 4.4 未使用 import
- **位置**`backend/rss_fetcher.py:5`
- **风险等级**:🟢 低
- **现状**
```python
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`
- **风险等级**:🟡 中
- **现状**
```dockerfile
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+ 行为可能报错。
- **建议**
```dockerfile
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`
- **风险等级**:🟠 中
- **现状**
```yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
```
- **问题**`python:3.12-slim` 镜像默认**没有** `curl`healthcheck 会一直返回非 0 → 容器会被反复标记 unhealthy。
- **建议**
```yaml
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`
- **风险等级**:🟢 低
- **问题**
```yaml
- DATABASE_URL=/app/data/rsskeeper.db
```
```python
engine = 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 管理系统。从代码风格、模块拆分来看,作者具备良好的工程素养;但要在生产环境长期运行,建议优先解决以下三类问题:
1. **安全基线**CORS / 鉴权 / XSS —— 任何对外暴露的服务都必须先补齐;
2. **正确性 Bug**:OPML 导入功能当前不可用(`/feeds/import-opml`),属于必须立即修复的 P0;
3. **部署可靠性**Docker healthcheck 失效会导致容器反复重启,看似无关紧要但容易掩盖真实故障。
后续若要扩展功能(多用户、订阅推送、标签、阅读列表等),建议先把"测试 + CI + 日志"这套工程基座补齐,再做功能叠加,避免技术债快速累积。