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 配置文件
This commit is contained in:
congsh
2026-06-11 14:31:29 +08:00
parent 54e7db0ef0
commit c59dd304f7
17 changed files with 701 additions and 106 deletions
+27 -9
View File
@@ -1,6 +1,6 @@
"""RSS 源管理 API"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from pydantic import BaseModel, HttpUrl
from sqlalchemy.orm import Session
from database import get_db
@@ -103,7 +103,11 @@ def list_categories(db: Session = Depends(get_db)):
@router.post("", response_model=dict)
def create_feed(data: FeedCreate, db: Session = Depends(get_db)):
def create_feed(
data: FeedCreate,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
):
"""添加 RSS 源"""
# 检查是否已存在
existing = db.query(Feed).filter(Feed.url == data.url).first()
@@ -126,10 +130,10 @@ def create_feed(data: FeedCreate, db: Session = Depends(get_db)):
if feed.is_active:
add_feed_job(feed.id, feed.fetch_interval_minutes)
# 立即抓取一次
fetch_and_store_feed(feed.id)
# 后台异步首次抓取,不阻塞 HTTP 响应
background_tasks.add_task(fetch_and_store_feed, feed.id)
return {"id": feed.id, "message": "RSS 源添加成功", "url": feed.url}
return {"id": feed.id, "message": "RSS 源添加成功,正在后台抓取", "url": feed.url}
@router.post("/discover")
@@ -217,13 +221,25 @@ def trigger_fetch(feed_id: int, db: Session = Depends(get_db)):
return result
class OpmlImport(BaseModel):
opml_content: str
@router.post("/import-opml")
def import_opml(opml_content: str, db: Session = Depends(get_db)):
def import_opml(data: OpmlImport, db: Session = Depends(get_db)):
"""导入 OPML 文件内容"""
import xml.etree.ElementTree as ET
content = data.opml_content.strip()
if not content:
raise HTTPException(status_code=400, detail="OPML 内容不能为空")
# 限制大小(防止滥用)
if len(content) > 5_000_000: # 5MB
raise HTTPException(status_code=413, detail="OPML 文件过大")
try:
root = ET.fromstring(opml_content)
root = ET.fromstring(content)
except ET.ParseError:
raise HTTPException(status_code=400, detail="无效的 OPML 文件")
@@ -261,12 +277,14 @@ def import_opml(opml_content: str, db: Session = Depends(get_db)):
@router.get("/export-opml")
def export_opml(db: Session = Depends(get_db)):
"""导出 OPML 文件内容"""
from xml.sax.saxutils import escape
feeds = db.query(Feed).all()
lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<opml version="2.0">', '<head><title>rssKeeper Feeds</title></head>', '<body>']
for feed in feeds:
title = (feed.title or feed.url).replace('"', '&quot;')
lines.append(f' <outline type="rss" text="{title}" xmlUrl="{feed.url}" />')
title = escape(feed.title or feed.url, {'"': '&quot;'})
url = escape(feed.url)
lines.append(f' <outline type="rss" text="{title}" xmlUrl="{url}" />')
lines.append('</body>')
lines.append('</opml>')