feat: scaffold FastAPI backend and Vue 3 frontend

Backend (backend/):
- FastAPI app with layered app/{api,core,crud,db,models,schemas} structure
- TiDB connection via SQLAlchemy. The URL is built with URL.create rather
  than string formatting: the password contains '@', which would otherwise be
  parsed as the user/host separator and silently truncate the credential.
- Alembic environment wired to Base.metadata and the app settings, so
  backend/.env stays the single source of truth for credentials
- /api/health endpoint reporting database reachability
- models/ and crud/ are intentionally empty: no model classes are defined and
  no tables are created, at import time or otherwise

Frontend (frontend/):
- Vue 3 + Vite + TypeScript scaffold (create-vue, --bare)
- axios instance with a normalized error shape
- vue-router with a home route and a catch-all 404
- pinia + pinia-plugin-persistedstate; the app store persists selected keys
- Element Plus with its icons registered globally
- dev proxy forwards /api to the FastAPI service on port 8000

The database paper_doc was created in TiDB out of band; no table exists yet.
This commit is contained in:
2026-09-17 12:51:37 +08:00
parent 4438b04ff1
commit 5128e1551c
43 changed files with 3321 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Database engine, session factory, and declarative base."""
+16
View File
@@ -0,0 +1,16 @@
"""Declarative base for ORM models.
No model classes are defined in this project yet, by design: the database
schema is introduced deliberately through Alembic migrations rather than being
created implicitly at import time.
"""
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
"""Common base class for every ORM model.
Subclass this to declare a model. New models must also be imported by
``app.models`` so that ``alembic revision --autogenerate`` can see them.
"""
+35
View File
@@ -0,0 +1,35 @@
"""Database engine, session factory, and the FastAPI session dependency."""
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from app.core.config import get_settings
settings = get_settings()
engine = create_engine(
settings.sqlalchemy_url,
echo=settings.db_echo,
pool_pre_ping=settings.db_pool_pre_ping,
pool_recycle=settings.db_pool_recycle,
future=True,
)
SessionLocal = sessionmaker(
bind=engine,
class_=Session,
autoflush=False,
autocommit=False,
expire_on_commit=False,
)
def get_db() -> Generator[Session, None, None]:
"""Yield a session per request and always close it."""
db = SessionLocal()
try:
yield db
finally:
db.close()