Compare commits

...

2 Commits

Author SHA1 Message Date
Ad-closeNN e9d50e2735 feat: add FastAPI feedback backend 2026-05-23 21:43:40 +08:00
Ad-closeNN b2313d1796 chore: ignore feedback-api/* except tracked files 2026-05-23 21:43:03 +08:00
4 changed files with 137 additions and 1 deletions
+7 -1
View File
@@ -36,4 +36,10 @@ yarn.lock
.cache
build.log
.traces
.traces
# 2026/5/23 Feedback module api backend
feedback-api/*
!feedback-api/database.py
!feedback-api/main.py
!feedback-api/requirements.txt
+57
View File
@@ -0,0 +1,57 @@
import aiosqlite
DB_PATH = "feedback.db"
async def init_db():
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("""
CREATE TABLE IF NOT EXISTS feedbacks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
choice TEXT NOT NULL CHECK(choice IN ('yes', 'no')),
timestamp TEXT NOT NULL,
ip_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
""")
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_feedbacks_ip_hash
ON feedbacks(ip_hash, created_at)
""")
await db.commit()
async def add_feedback(ip_hash: str, url: str, choice: str, timestamp: str):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT INTO feedbacks (url, choice, timestamp, ip_hash) VALUES (?, ?, ?, ?)",
(url, choice, timestamp, ip_hash),
)
await db.commit()
async def check_rate_limit(ip_hash: str, limit: int = 5, window_seconds: int = 3600) -> bool:
async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute(
"SELECT COUNT(*) FROM feedbacks WHERE ip_hash = ? AND created_at > datetime('now', ?)",
(ip_hash, f"-{window_seconds} seconds"),
)
row = await cursor.fetchone()
return row[0] >= limit
async def get_stats() -> dict:
async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute("SELECT COUNT(*) FROM feedbacks")
total = (await cursor.fetchone())[0]
cursor = await db.execute("SELECT choice, COUNT(*) FROM feedbacks GROUP BY choice")
rows = await cursor.fetchall()
counts = {row[0]: row[1] for row in rows}
return {
"total": total,
"yes": counts.get("yes", 0),
"no": counts.get("no", 0),
}
+70
View File
@@ -0,0 +1,70 @@
import hashlib
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from database import add_feedback, check_rate_limit, get_stats, init_db
class FeedbackIn(BaseModel):
url: str
choice: str
timestamp: str
RATE_LIMIT = 5
RATE_WINDOW = 3600
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
def get_client_ip(request: Request) -> str:
cf_ip = request.headers.get("cf-connecting-ip")
if cf_ip:
return cf_ip
forwarded = request.headers.get("x-forwarded-for")
if forwarded:
return forwarded.split(",")[0].strip()
return request.client.host or "unknown"
@app.post("/api/feedback")
async def receive_feedback(body: FeedbackIn, request: Request):
ip = get_client_ip(request)
ip_hash = hashlib.sha256(ip.encode()).hexdigest()
if not await check_rate_limit(ip_hash, RATE_LIMIT, RATE_WINDOW):
await add_feedback(ip_hash, body.url, body.choice, body.timestamp)
return {"ok": True}
@app.get("/api/feedback/stats")
async def stats():
return await get_stats()
@app.get("/api/feedback/health")
async def health():
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8005)
+3
View File
@@ -0,0 +1,3 @@
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
aiosqlite>=0.20.0