Initial commit: stock market platform

This commit is contained in:
admin
2026-06-11 01:41:47 +08:00
commit 63718906e9
62 changed files with 8962 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
from pydantic_settings import BaseSettings
from pydantic import field_validator
from typing import List
import os
class Settings(BaseSettings):
# App
APP_NAME: str = "Stock Platform API"
DEBUG: bool = False
API_V1_PREFIX: str = "/api/v1"
# Database
DATABASE_URL: str = "postgresql+asyncpg://stock:stockpass@localhost:5432/stockdb"
# Redis
REDIS_URL: str = "redis://localhost:6379/0"
CELERY_BROKER_URL: str = "redis://localhost:6379/1"
# Auth
SECRET_KEY: str = "change-me-in-production"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
# CORS
ALLOWED_ORIGINS: str = "http://localhost:3000,http://localhost:5173"
@property
def cors_origins(self) -> List[str]:
return [o.strip() for o in self.ALLOWED_ORIGINS.split(",")]
# Tushare (optional)
TUSHARE_TOKEN: str = ""
class Config:
env_file = ".env"
extra = "ignore"
settings = Settings()

View File

@@ -0,0 +1,37 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings
engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG,
pool_pre_ping=True,
pool_size=10,
max_overflow=20,
)
AsyncSessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False,
)
class Base(DeclarativeBase):
pass
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
async def init_db() -> None:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

22
backend/app/core/redis.py Normal file
View File

@@ -0,0 +1,22 @@
import redis.asyncio as aioredis
from app.core.config import settings
_redis_pool: aioredis.Redis | None = None
async def get_redis() -> aioredis.Redis:
global _redis_pool
if _redis_pool is None:
_redis_pool = aioredis.from_url(
settings.REDIS_URL,
encoding="utf-8",
decode_responses=True,
)
return _redis_pool
async def close_redis() -> None:
global _redis_pool
if _redis_pool:
await _redis_pool.aclose()
_redis_pool = None

View File

@@ -0,0 +1,37 @@
from datetime import datetime, timedelta, timezone
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(subject: str | int, expires_delta: Optional[timedelta] = None) -> str:
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
)
payload = {"sub": str(subject), "exp": expire, "type": "access"}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def create_refresh_token(subject: str | int) -> str:
expire = datetime.now(timezone.utc) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
payload = {"sub": str(subject), "exp": expire, "type": "refresh"}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def decode_token(token: str) -> Optional[dict]:
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
return None