From a9d1b0e416cbf3f961e3663faf8a4797b96d44f5 Mon Sep 17 00:00:00 2001 From: simonkoson <28867558@qq.com> Date: Mon, 11 May 2026 10:43:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=201=20=E5=90=8E=E7=AB=AF=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6=E4=B8=BB=E4=BD=93=E5=85=A5=E5=BA=93(=E8=A1=A5=20backe?= =?UTF-8?q?nd/app=20=E4=B8=BB=E4=BD=93=E4=B8=8E=20requirements.txt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上轮 3 次 commit 只入库了修补文件,主体目录漏入库。本次补入 backend/app/、backend/requirements.txt、backend/scripts/__init__.py。同步在 .gitignore 加 homePC/(制片人家用机带过来的参考文件,不进库)。 --- .gitignore | 3 +- backend/app/__init__.py | 0 backend/app/api/__init__.py | 0 backend/app/api/auth.py | 46 +++++++++++++++++++++++++++ backend/app/core/__init__.py | 0 backend/app/core/config.py | 19 +++++++++++ backend/app/core/deps.py | 56 +++++++++++++++++++++++++++++++++ backend/app/core/security.py | 17 ++++++++++ backend/app/db/__init__.py | 0 backend/app/db/session.py | 20 ++++++++++++ backend/app/main.py | 36 +++++++++++++++++++++ backend/app/models/__init__.py | 0 backend/app/models/user.py | 44 ++++++++++++++++++++++++++ backend/app/schemas/__init__.py | 0 backend/app/schemas/auth.py | 25 +++++++++++++++ backend/requirements.txt | 12 +++++++ backend/scripts/__init__.py | 0 17 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/auth.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/deps.py create mode 100644 backend/app/core/security.py create mode 100644 backend/app/db/__init__.py create mode 100644 backend/app/db/session.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/user.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/auth.py create mode 100644 backend/requirements.txt create mode 100644 backend/scripts/__init__.py diff --git a/.gitignore b/.gitignore index 409e7cd..9adf2c4 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ cookie.txt # 临时文件 *.tmp -*.bak \ No newline at end of file +*.bak +homePC/ diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py new file mode 100644 index 0000000..45ab4f9 --- /dev/null +++ b/backend/app/api/auth.py @@ -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 \ No newline at end of file diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..f7f4487 --- /dev/null +++ b/backend/app/core/config.py @@ -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() \ No newline at end of file diff --git a/backend/app/core/deps.py b/backend/app/core/deps.py new file mode 100644 index 0000000..339b2a6 --- /dev/null +++ b/backend/app/core/deps.py @@ -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 \ No newline at end of file diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..b6cbedb --- /dev/null +++ b/backend/app/core/security.py @@ -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) \ No newline at end of file diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/db/session.py b/backend/app/db/session.py new file mode 100644 index 0000000..f4a4b6c --- /dev/null +++ b/backend/app/db/session.py @@ -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 \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..7ff8baf --- /dev/null +++ b/backend/app/main.py @@ -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) \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..0b13e9d --- /dev/null +++ b/backend/app/models/user.py @@ -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()), + ) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py new file mode 100644 index 0000000..3ae772d --- /dev/null +++ b/backend/app/schemas/auth.py @@ -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 \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..54ea26d --- /dev/null +++ b/backend/requirements.txt @@ -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 \ No newline at end of file diff --git a/backend/scripts/__init__.py b/backend/scripts/__init__.py new file mode 100644 index 0000000..e69de29