feat: Phase 1 后端骨架主体入库(补 backend/app 主体与 requirements.txt)

上轮 3 次 commit 只入库了修补文件,主体目录漏入库。本次补入 backend/app/、backend/requirements.txt、backend/scripts/__init__.py。同步在 .gitignore 加 homePC/(制片人家用机带过来的参考文件,不进库)。
This commit is contained in:
simonkoson
2026-05-11 10:43:18 +08:00
parent 110e26f752
commit a9d1b0e416
17 changed files with 277 additions and 1 deletions
+2 -1
View File
@@ -32,4 +32,5 @@ cookie.txt
# 临时文件 # 临时文件
*.tmp *.tmp
*.bak *.bak
homePC/
View File
View File
+46
View File
@@ -0,0 +1,46 @@
"""
认证路由 — 登录 / 登出 / 当前用户
"""
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlmodel import Session, select
from app.core.deps import get_current_user
from app.core.security import verify_password
from app.db.session import get_session
from app.models.user import User
from app.schemas.auth import LoginRequest, UserResponse
router = APIRouter(prefix="/api/auth", tags=["认证"])
@router.post("/login", response_model=UserResponse)
def login(body: LoginRequest, request: Request, session: Session = Depends(get_session)):
"""账号登录。验证成功后写入 session,返回用户信息。"""
statement = select(User).where(User.username == body.username)
user = session.exec(statement).first()
if user is None or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
if not verify_password(body.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
request.session["user_id"] = user.id
return user
@router.post("/logout")
def logout(request: Request):
"""退出登录,清空当前 session。"""
request.session.clear()
return {"message": "已退出登录"}
@router.get("/me", response_model=UserResponse)
def get_me(user: User = Depends(get_current_user)):
"""获取当前登录用户的信息。"""
return user
View File
+19
View File
@@ -0,0 +1,19 @@
"""
应用配置 — 用 pydantic-settings 的 BaseSettings 读取 .env
"""
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str = "change-me-to-a-random-string-in-production"
SESSION_MAX_AGE: int = 86400 # 秒,默认 1 天
model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8",
}
settings = Settings()
+56
View File
@@ -0,0 +1,56 @@
"""
FastAPI 依赖注入工具 — 获取当前用户、角色鉴权
"""
from fastapi import Depends, HTTPException, Request, status
from sqlmodel import Session
from app.db.session import get_session
from app.models.user import User, UserRole
def get_current_user(
request: Request,
session: Session = Depends(get_session),
) -> User:
"""从 Session 中取 user_id,查库返回当前用户。
未登录或用户不存在 / 已停用时抛出 401。
"""
user_id = request.session.get("user_id")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="请先登录",
)
user = session.get(User, user_id)
if user is None or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在或已被停用",
)
return user
def require_role(*allowed_roles: UserRole):
"""返回一个 Depends 依赖,要求当前用户拥有指定的角色之一。
使用示例:
@router.get("/admin-only")
def admin_endpoint(user=Depends(require_role(UserRole.zhipianren))):
...
@router.get("/staff")
def staff_endpoint(user=Depends(require_role(UserRole.zhipianren, UserRole.zebian))):
...
"""
def check_role(user: User = Depends(get_current_user)) -> User:
if user.role not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="权限不足,无法执行此操作",
)
return user
return check_role
+17
View File
@@ -0,0 +1,17 @@
"""
认证安全工具 — passlib + bcrypt
"""
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain: str) -> str:
"""用 bcrypt 生成密码哈希。"""
return pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
"""验证明文密码与哈希是否匹配。"""
return pwd_context.verify(plain, hashed)
View File
+20
View File
@@ -0,0 +1,20 @@
"""
数据库会话 — SQLModel 同步引擎 + Session 工厂
"""
from sqlmodel import Session, SQLModel, create_engine
from app.core.config import settings
engine = create_engine(settings.DATABASE_URL, echo=False)
def init_db():
"""创建所有 SQLModel 表(仅开发用,生产由手写 SQL 管理)。"""
SQLModel.metadata.create_all(engine)
def get_session():
"""FastAPI 依赖注入: 每个请求获取一个 Session,用完后关闭。"""
with Session(engine) as session:
yield session
+36
View File
@@ -0,0 +1,36 @@
"""
FastAPI 应用入口 — 挂载中间件、路由
"""
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from app.api.auth import router as auth_router
from app.core.config import settings
app = FastAPI(title="军事科技工作台", version="0.1.0")
# ---- CORS 中间件 ----
# 显式列 origins 不能用 ['*'],否则 credentials=True 时浏览器拒绝携带 cookie
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ---- Session 中间件 ----
# 密钥来自 .env,用于签名 session cookie
app.add_middleware(
SessionMiddleware,
secret_key=settings.SECRET_KEY,
session_cookie="milsci_session",
max_age=settings.SESSION_MAX_AGE,
same_site="lax",
https_only=False, # 本地开发用 HTTP
)
# ---- 注册路由 ----
app.include_router(auth_router)
View File
+44
View File
@@ -0,0 +1,44 @@
"""
用户模型 — SQLModel
角色体系(不再改):
- zhipianren = 制片人 / 管理员 = 全部权限
- zebian = 责编 = 维护仪表盘 / 知识库 / 排期
- biandao = 编导 = 使用 TPS / 查看知识库 / 生成报题单 / 个人热点雷达
"""
from enum import Enum
from datetime import datetime, timezone
from sqlalchemy import Column
from sqlalchemy import DateTime as SADateTime
from sqlalchemy.sql import func as sa_func
from sqlmodel import Field, SQLModel
class UserRole(str, Enum):
"""用户角色枚举,值与数据库 CHECK 约束一致。"""
zhipianren = "zhipianren" # 制片人 / 管理员
zebian = "zebian" # 责编
biandao = "biandao" # 编导
class User(SQLModel, table=True):
__tablename__ = "users"
id: int | None = Field(default=None, primary_key=True)
username: str = Field(max_length=50, unique=True, index=True)
display_name: str = Field(max_length=100)
password_hash: str = Field(max_length=128)
role: UserRole = Field(default=UserRole.biandao, max_length=20)
profile_text: str | None = Field(default=None)
is_active: bool = Field(default=True)
created_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
updated_at: datetime | None = Field(
default=None,
sa_column=Column(SADateTime(timezone=True), nullable=False, server_default=sa_func.now()),
)
View File
+25
View File
@@ -0,0 +1,25 @@
"""
认证相关 Pydantic Schema — 请求 / 响应模型
"""
from pydantic import BaseModel
from app.models.user import UserRole
class LoginRequest(BaseModel):
username: str
password: str
class UserResponse(BaseModel):
"""返回给前端的用户信息(不含 password_hash)。"""
id: int
username: str
display_name: str
role: UserRole
profile_text: str | None = None
is_active: bool = True
class Config:
from_attributes = True
+12
View File
@@ -0,0 +1,12 @@
fastapi==0.111.0
uvicorn[standard]==0.29.0
sqlmodel==0.0.19
psycopg2-binary==2.9.9
pgvector==0.2.5
passlib==1.7.4
bcrypt==4.0.1
itsdangerous==2.2.0
python-multipart==0.0.9
pydantic-settings==2.2.1
python-dotenv==1.0.1
httpx==0.27.0
View File