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
+75
View File
@@ -0,0 +1,75 @@
"""Alembic migration environment.
The connection URL comes from the application settings rather than from
``alembic.ini``, so ``backend/.env`` stays the single source of truth. The URL
is passed to SQLAlchemy as a :class:`~sqlalchemy.engine.URL` object: rendering
it back to a string would re-introduce the escaping problem around the ``@``
in the TiDB password.
``target_metadata`` is wired to :class:`app.db.base.Base`, so
``alembic revision --autogenerate`` will pick up models as soon as they are
defined and imported by ``app.models``. Today that package is empty, so an
autogenerated revision is expected to contain no operations.
"""
from logging.config import fileConfig
from alembic import context
from sqlalchemy import create_engine, pool
from app.core.config import get_settings
from app.db.base import Base
# Import the models package so that every model module is registered on
# Base.metadata before autogenerate inspects it. The package defines no models
# yet; keeping the import here means the first model added is picked up with no
# further change to this file.
from app import models # noqa: F401
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
settings = get_settings()
def run_migrations_offline() -> None:
"""Emit SQL to stdout without connecting to the database."""
context.configure(
url=settings.sqlalchemy_url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Connect to TiDB and apply migrations."""
connectable = create_engine(
settings.sqlalchemy_url,
poolclass=pool.NullPool,
future=True,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()