Initial AlphaPulse implementation: news crawler and Binance klines
- Backend (FastAPI, SQLAlchemy 2, Alembic): single-admin cookie auth, news/crawler/market APIs with SSE and WebSocket streaming - Crawler (Scrapy + Scrapy-Redis): wscn_news, wscn_live, cls_depth, cls_telegraph spiders, idempotent upserts with revisions, raw response archiving, per-source subprocess supervision, restart and backoff - Market worker: full-history Binance spot/USD-M kline backfill, realtime WS stream, hourly and on-reconnect catch-up - Frontend (Vue 3, Element Plus, UnoCSS, KLineChart): news feed, crawler console with live logs, Binance-style chart page - Deploy: docker compose, nginx site, migrate and daily backup scripts
This commit is contained in:
commit
aabc81ba5b
88 files changed
+12603
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,59 @@
|
||||
import getpass
|
||||
import sys
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import jwt
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
from fastapi import HTTPException, Request, WebSocket, status
|
||||
|
||||
from alphapulse.core.config import get_settings
|
||||
|
||||
COOKIE_NAME = "ap_token"
|
||||
_hasher = PasswordHasher()
|
||||
|
||||
|
||||
def verify_password(password: str, password_hash: str) -> bool:
|
||||
try:
|
||||
return _hasher.verify(password_hash, password)
|
||||
except VerifyMismatchError:
|
||||
return False
|
||||
|
||||
|
||||
def create_token(username: str) -> str:
|
||||
settings = get_settings()
|
||||
expire = datetime.now(UTC) + timedelta(days=settings.jwt_expire_days)
|
||||
return jwt.encode({"sub": username, "exp": expire}, settings.jwt_secret, algorithm="HS256")
|
||||
|
||||
|
||||
def _username_from_token(token: str | None) -> str | None:
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
payload = jwt.decode(token, get_settings().jwt_secret, algorithms=["HS256"])
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
return payload["sub"]
|
||||
|
||||
|
||||
def require_admin(request: Request) -> str:
|
||||
username = _username_from_token(request.cookies.get(COOKIE_NAME))
|
||||
if username != get_settings().admin_username:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "未登录或登录已过期")
|
||||
return username
|
||||
|
||||
|
||||
def websocket_admin(websocket: WebSocket) -> str | None:
|
||||
# WebSocket 握手阶段不能用 HTTPException,由调用方自行关闭连接
|
||||
username = _username_from_token(websocket.cookies.get(COOKIE_NAME))
|
||||
return username if username == get_settings().admin_username else None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 用法:python -m alphapulse.core.auth hash-password,输出写入 .env 的 ADMIN_PASSWORD_HASH
|
||||
if sys.argv[1:] != ["hash-password"]:
|
||||
sys.exit("用法:python -m alphapulse.core.auth hash-password")
|
||||
password = getpass.getpass("新密码:")
|
||||
if password != getpass.getpass("再次输入:"):
|
||||
sys.exit("两次输入不一致")
|
||||
print(_hasher.hash(password))
|
||||
@@ -0,0 +1,20 @@
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 容器内由 compose 的 env_file 注入;只有一套配置,见仓库根目录 .env
|
||||
model_config = SettingsConfigDict(extra="ignore")
|
||||
|
||||
database_url: str
|
||||
redis_url: str
|
||||
jwt_secret: str
|
||||
admin_username: str
|
||||
admin_password_hash: str = ""
|
||||
jwt_expire_days: int = 7
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
@@ -0,0 +1,19 @@
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
from alphapulse.core.config import get_settings
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
engine = create_async_engine(get_settings().database_url, pool_pre_ping=True)
|
||||
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
async def get_session() -> AsyncIterator[AsyncSession]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
@@ -0,0 +1,8 @@
|
||||
from redis.asyncio import Redis
|
||||
|
||||
from alphapulse.core.config import get_settings
|
||||
|
||||
# DB 14 与 AFFiNE 共用:所有 key 必须带此前缀,禁止 FLUSHDB
|
||||
KEY_PREFIX = "ap:"
|
||||
|
||||
redis = Redis.from_url(get_settings().redis_url, decode_responses=True)
|
||||
Reference in new issue
Block a user