Scaffold V2 backend foundation
This commit is contained in:
1
ai_knowledge_base_v2/apps/backend/app/__init__.py
Normal file
1
ai_knowledge_base_v2/apps/backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
ai_knowledge_base_v2/apps/backend/app/api/__init__.py
Normal file
1
ai_knowledge_base_v2/apps/backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
30
ai_knowledge_base_v2/apps/backend/app/api/auth.py
Normal file
30
ai_knowledge_base_v2/apps/backend/app/api/auth.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_token_payload
|
||||
from app.core.responses import api_success
|
||||
from app.schemas.auth import LoginRequest, LoginResponse, SendSmsRequest
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/sms/send")
|
||||
def send_sms(payload: SendSmsRequest) -> dict:
|
||||
AuthService.send_sms_code(payload.phone)
|
||||
return api_success(message="发送成功")
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
def login(payload: LoginRequest, db: Session = Depends(get_db)) -> dict:
|
||||
result = AuthService.login_with_sms(db, payload.phone, payload.code)
|
||||
return api_success(LoginResponse.model_validate(result).model_dump(mode="json"))
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(token_payload: dict = Depends(get_current_token_payload)) -> dict:
|
||||
AuthService.logout(token_payload)
|
||||
return api_success()
|
||||
94
ai_knowledge_base_v2/apps/backend/app/api/chat.py
Normal file
94
ai_knowledge_base_v2/apps/backend/app/api/chat.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.core.responses import api_success
|
||||
from app.models.user import User
|
||||
from app.schemas.chat import (
|
||||
ChatCompletionRequest,
|
||||
ChatMessageRead,
|
||||
ChatSessionRead,
|
||||
CreateSessionResponse,
|
||||
StopChatRequest,
|
||||
UpdateSessionTitleRequest,
|
||||
)
|
||||
from app.services.chat_service import ChatService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/session")
|
||||
def create_session(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)) -> dict:
|
||||
session = ChatService.create_session(db, current_user)
|
||||
return api_success(CreateSessionResponse(sessionId=session.id).model_dump())
|
||||
|
||||
|
||||
@router.get("/session/list")
|
||||
def list_sessions(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)) -> dict:
|
||||
sessions = ChatService.list_sessions(db, current_user)
|
||||
return api_success([ChatSessionRead.model_validate(session).model_dump() for session in sessions])
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
def history(
|
||||
sessionId: int = Query(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
messages = ChatService.get_history(db, current_user, sessionId)
|
||||
return api_success([ChatMessageRead.model_validate(message).model_dump(mode="json") for message in messages])
|
||||
|
||||
|
||||
@router.put("/session/title")
|
||||
def update_title(
|
||||
payload: UpdateSessionTitleRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
session = ChatService.update_title(db, current_user, payload.sessionId, payload.title)
|
||||
return api_success(ChatSessionRead.model_validate(session).model_dump())
|
||||
|
||||
|
||||
@router.delete("/session/{session_id}")
|
||||
def delete_session(
|
||||
session_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
ChatService.delete_session(db, current_user, session_id)
|
||||
return api_success()
|
||||
|
||||
|
||||
@router.post("/completions")
|
||||
def completions(
|
||||
payload: ChatCompletionRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> StreamingResponse:
|
||||
answer = ChatService.create_mock_answer(db, current_user, payload.sessionId, payload.message)
|
||||
return StreamingResponse(_sse_chunks(answer), media_type="text/event-stream")
|
||||
|
||||
|
||||
@router.post("/stop")
|
||||
def stop(
|
||||
payload: StopChatRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
ChatService.stop_generation(db, current_user, payload.sessionId)
|
||||
return api_success()
|
||||
|
||||
|
||||
def _sse_chunks(answer: str) -> Iterator[str]:
|
||||
chunk_size = 12
|
||||
for index in range(0, len(answer), chunk_size):
|
||||
chunk = answer[index : index + chunk_size]
|
||||
yield f"data: {json.dumps({'content': chunk}, ensure_ascii=False)}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
12
ai_knowledge_base_v2/apps/backend/app/api/health.py
Normal file
12
ai_knowledge_base_v2/apps/backend/app/api/health.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.core.responses import api_success
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health() -> dict:
|
||||
return api_success({"status": "ok"})
|
||||
11
ai_knowledge_base_v2/apps/backend/app/api/router.py
Normal file
11
ai_knowledge_base_v2/apps/backend/app/api/router.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api import auth, chat, health, user
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(user.router, prefix="/user", tags=["user"])
|
||||
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
||||
15
ai_knowledge_base_v2/apps/backend/app/api/user.py
Normal file
15
ai_knowledge_base_v2/apps/backend/app/api/user.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.core.dependencies import get_current_user
|
||||
from app.core.responses import api_success
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserProfile
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
def profile(current_user: User = Depends(get_current_user)) -> dict:
|
||||
return api_success(UserProfile.model_validate(current_user).model_dump(mode="json"))
|
||||
1
ai_knowledge_base_v2/apps/backend/app/core/__init__.py
Normal file
1
ai_knowledge_base_v2/apps/backend/app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
42
ai_knowledge_base_v2/apps/backend/app/core/config.py
Normal file
42
ai_knowledge_base_v2/apps/backend/app/core/config.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
||||
|
||||
app_name: str = "AI Knowledge Base V2 Backend"
|
||||
app_env: str = "local"
|
||||
debug: bool = True
|
||||
api_prefix: str = "/api"
|
||||
cors_origins: list[str] = Field(
|
||||
default_factory=lambda: [
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
]
|
||||
)
|
||||
|
||||
database_url: str = "mysql+pymysql://ai_kb:ai_kb@127.0.0.1:3306/ai_knowledge_base_v2?charset=utf8mb4"
|
||||
auto_create_tables: bool = False
|
||||
|
||||
jwt_secret_key: str = "change-me-in-production"
|
||||
jwt_algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 60 * 24 * 30
|
||||
|
||||
mock_sms_enabled: bool = True
|
||||
mock_sms_code: str = "123456"
|
||||
sms_code_expire_minutes: int = 5
|
||||
|
||||
default_daily_chat_limit: int = 100
|
||||
default_user_name_prefix: str = "用户"
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
26
ai_knowledge_base_v2/apps/backend/app/core/database.py
Normal file
26
ai_knowledge_base_v2/apps/backend/app/core/database.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.models import Base
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
engine = create_engine(settings.database_url, pool_pre_ping=True, future=True)
|
||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def create_tables() -> None:
|
||||
Base.metadata.create_all(bind=engine)
|
||||
42
ai_knowledge_base_v2/apps/backend/app/core/dependencies.py
Normal file
42
ai_knowledge_base_v2/apps/backend/app/core/dependencies.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import decode_access_token
|
||||
from app.models.user import User
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
bearer_scheme = HTTPBearer(auto_error=True)
|
||||
|
||||
|
||||
def get_current_token_payload(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> dict:
|
||||
payload = decode_access_token(credentials.credentials)
|
||||
if AuthService.is_token_revoked(payload):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已退出")
|
||||
return payload
|
||||
|
||||
|
||||
def get_current_user(
|
||||
payload: dict = Depends(get_current_token_payload),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
if payload.get("type") != "user":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="当前凭证不是用户登录态")
|
||||
|
||||
user_id = payload.get("sub")
|
||||
user = db.get(User, int(user_id)) if user_id else None
|
||||
if user is None or user.is_deleted:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在")
|
||||
if user.status != 1:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已禁用")
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
if user.effective_at and user.effective_at > now:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号尚未生效")
|
||||
if user.expired_at and user.expired_at < now:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已过期")
|
||||
return user
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.core.responses import api_error
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI) -> None:
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
||||
return JSONResponse(status_code=exc.status_code, content=api_error(_code_from_status(exc.status_code), str(exc.detail)))
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||
return JSONResponse(status_code=422, content=api_error(10000, "请求参数不正确", exc.errors()))
|
||||
|
||||
|
||||
def _code_from_status(status_code: int) -> int:
|
||||
if status_code in {401, 403}:
|
||||
return 20000 if status_code == 401 else 40000
|
||||
if status_code == 400:
|
||||
return 10000
|
||||
return 50000
|
||||
11
ai_knowledge_base_v2/apps/backend/app/core/responses.py
Normal file
11
ai_knowledge_base_v2/apps/backend/app/core/responses.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def api_success(data: Any = None, message: str = "success") -> dict[str, Any]:
|
||||
return {"code": 0, "message": message, "data": data}
|
||||
|
||||
|
||||
def api_error(code: int, message: str, data: Any = None) -> dict[str, Any]:
|
||||
return {"code": code, "message": message, "data": data}
|
||||
33
ai_knowledge_base_v2/apps/backend/app/core/security.py
Normal file
33
ai_knowledge_base_v2/apps/backend/app/core/security.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
import jwt
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
def create_access_token(subject: str, token_type: str = "user") -> tuple[str, datetime]:
|
||||
settings = get_settings()
|
||||
expires_at = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
|
||||
payload = {
|
||||
"sub": subject,
|
||||
"type": token_type,
|
||||
"jti": uuid4().hex,
|
||||
"exp": expires_at,
|
||||
"iat": datetime.now(UTC),
|
||||
}
|
||||
token = jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
|
||||
return token, expires_at
|
||||
|
||||
|
||||
def decode_access_token(token: str) -> dict:
|
||||
settings = get_settings()
|
||||
try:
|
||||
return jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
|
||||
except jwt.ExpiredSignatureError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已过期") from exc
|
||||
except jwt.InvalidTokenError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效登录凭证") from exc
|
||||
40
ai_knowledge_base_v2/apps/backend/app/main.py
Normal file
40
ai_knowledge_base_v2/apps/backend/app/main.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.api.router import api_router
|
||||
from app.core.config import get_settings
|
||||
from app.core.database import create_tables
|
||||
from app.core.exception_handlers import register_exception_handlers
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
settings = get_settings()
|
||||
if settings.auto_create_tables:
|
||||
create_tables()
|
||||
yield
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
app = FastAPI(title=settings.app_name, version="0.1.0", lifespan=lifespan)
|
||||
register_exception_handlers(app)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(api_router, prefix=settings.api_prefix)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root() -> dict[str, str]:
|
||||
return {"name": settings.app_name, "status": "ok"}
|
||||
23
ai_knowledge_base_v2/apps/backend/app/models/__init__.py
Normal file
23
ai_knowledge_base_v2/apps/backend/app/models/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from app.models.admin import Admin, Role
|
||||
from app.models.ai_config import ModelConfig, Prompt, SystemConfig
|
||||
from app.models.base import Base
|
||||
from app.models.chat import ChatMessage, ChatSession
|
||||
from app.models.knowledge import Knowledge, UserKnowledgePermission
|
||||
from app.models.logs import AiRequestLog, OperationLog
|
||||
from app.models.user import User
|
||||
|
||||
__all__ = [
|
||||
"Admin",
|
||||
"AiRequestLog",
|
||||
"Base",
|
||||
"ChatMessage",
|
||||
"ChatSession",
|
||||
"Knowledge",
|
||||
"ModelConfig",
|
||||
"OperationLog",
|
||||
"Prompt",
|
||||
"Role",
|
||||
"SystemConfig",
|
||||
"User",
|
||||
"UserKnowledgePermission",
|
||||
]
|
||||
33
ai_knowledge_base_v2/apps/backend/app/models/admin.py
Normal file
33
ai_knowledge_base_v2/apps/backend/app/models/admin.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class Role(Base, TimestampMixin):
|
||||
__tablename__ = "sys_role"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
code: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
|
||||
admins: Mapped[list["Admin"]] = relationship("Admin", back_populates="role")
|
||||
|
||||
|
||||
class Admin(Base, TimestampMixin):
|
||||
__tablename__ = "sys_admin"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
username: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False)
|
||||
password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
role_id: Mapped[int | None] = mapped_column(ForeignKey("sys_role.id"), nullable=True)
|
||||
status: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
role: Mapped[Role | None] = relationship("Role", back_populates="admins")
|
||||
43
ai_knowledge_base_v2/apps/backend/app/models/ai_config.py
Normal file
43
ai_knowledge_base_v2/apps/backend/app/models/ai_config.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, Integer, Numeric, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class Prompt(Base):
|
||||
__tablename__ = "sys_prompt"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
prompt_content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
updated_by: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
|
||||
class ModelConfig(Base):
|
||||
__tablename__ = "sys_model"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
provider: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
api_url: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
api_key: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
temperature: Mapped[Decimal | None] = mapped_column(Numeric(4, 2), nullable=True)
|
||||
max_token: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
timeout_second: Mapped[int] = mapped_column(Integer, default=30, nullable=False)
|
||||
enabled: Mapped[int] = mapped_column(default=0, nullable=False)
|
||||
|
||||
|
||||
class SystemConfig(Base):
|
||||
__tablename__ = "sys_system_config"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
config_key: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||
config_value: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
updated_by: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
19
ai_knowledge_base_v2/apps/backend/app/models/base.py
Normal file
19
ai_knowledge_base_v2/apps/backend/app/models/base.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, func
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
|
||||
class SoftDeleteMixin:
|
||||
is_deleted: Mapped[int] = mapped_column(default=0, nullable=False)
|
||||
40
ai_knowledge_base_v2/apps/backend/app/models/chat.py
Normal file
40
ai_knowledge_base_v2/apps/backend/app/models/chat.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class ChatSession(Base, TimestampMixin):
|
||||
__tablename__ = "sys_chat_session"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("sys_user.id"), index=True, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
message_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
last_message_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
is_deleted: Mapped[int] = mapped_column(default=0, nullable=False)
|
||||
|
||||
user: Mapped["User"] = relationship("User", back_populates="chat_sessions")
|
||||
messages: Mapped[list["ChatMessage"]] = relationship("ChatMessage", back_populates="session")
|
||||
|
||||
|
||||
class ChatMessage(Base):
|
||||
__tablename__ = "sys_chat_message"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
session_id: Mapped[int] = mapped_column(ForeignKey("sys_chat_session.id"), index=True, nullable=False)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("sys_user.id"), index=True, nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
message_status: Mapped[str] = mapped_column(String(20), default="FINISHED", nullable=False)
|
||||
token_input: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
token_output: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
response_time_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
model_id: Mapped[int | None] = mapped_column(ForeignKey("sys_model.id"), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
||||
|
||||
session: Mapped[ChatSession] = relationship("ChatSession", back_populates="messages")
|
||||
37
ai_knowledge_base_v2/apps/backend/app/models/knowledge.py
Normal file
37
ai_knowledge_base_v2/apps/backend/app/models/knowledge.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class Knowledge(Base, TimestampMixin):
|
||||
__tablename__ = "sys_knowledge"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
feishu_space_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
feishu_node_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
status: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
|
||||
user_permissions: Mapped[list["UserKnowledgePermission"]] = relationship(
|
||||
"UserKnowledgePermission", back_populates="knowledge"
|
||||
)
|
||||
|
||||
|
||||
class UserKnowledgePermission(Base, TimestampMixin):
|
||||
__tablename__ = "sys_user_kb"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("sys_user.id"), index=True, nullable=False)
|
||||
knowledge_id: Mapped[int] = mapped_column(ForeignKey("sys_knowledge.id"), index=True, nullable=False)
|
||||
effective_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
expired_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_by: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
|
||||
user: Mapped["User"] = relationship("User", back_populates="knowledge_permissions")
|
||||
knowledge: Mapped[Knowledge] = relationship("Knowledge", back_populates="user_permissions")
|
||||
43
ai_knowledge_base_v2/apps/backend/app/models/logs.py
Normal file
43
ai_knowledge_base_v2/apps/backend/app/models/logs.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
class AiRequestLog(Base):
|
||||
__tablename__ = "sys_ai_request_log"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
session_id: Mapped[int | None] = mapped_column(BigInteger, index=True, nullable=True)
|
||||
message_id: Mapped[int | None] = mapped_column(BigInteger, index=True, nullable=True)
|
||||
user_id: Mapped[int | None] = mapped_column(BigInteger, index=True, nullable=True)
|
||||
model_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
prompt: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
knowledge_ids: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
retrieve_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
input_token: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
output_token: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
total_token: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
cost_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
||||
|
||||
|
||||
class OperationLog(Base):
|
||||
__tablename__ = "sys_operation_log"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
admin_id: Mapped[int | None] = mapped_column(BigInteger, index=True, nullable=True)
|
||||
module: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
action: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
target_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
before_json: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
after_json: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
ip: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
result: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
||||
28
ai_knowledge_base_v2/apps/backend/app/models/user.py
Normal file
28
ai_knowledge_base_v2/apps/backend/app/models/user.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class User(Base, TimestampMixin, SoftDeleteMixin):
|
||||
__tablename__ = "sys_user"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
phone: Mapped[str] = mapped_column(String(20), unique=True, index=True, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
nickname: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
status: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||
daily_chat_limit: Mapped[int] = mapped_column(Integer, default=100, nullable=False)
|
||||
daily_chat_used: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
effective_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
expired_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
knowledge_permissions: Mapped[list["UserKnowledgePermission"]] = relationship(
|
||||
"UserKnowledgePermission", back_populates="user"
|
||||
)
|
||||
chat_sessions: Mapped[list["ChatSession"]] = relationship("ChatSession", back_populates="user")
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
22
ai_knowledge_base_v2/apps/backend/app/schemas/auth.py
Normal file
22
ai_knowledge_base_v2/apps/backend/app/schemas/auth.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.schemas.user import UserProfile
|
||||
|
||||
|
||||
class SendSmsRequest(BaseModel):
|
||||
phone: str = Field(min_length=11, max_length=20)
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
phone: str = Field(min_length=11, max_length=20)
|
||||
code: str = Field(min_length=4, max_length=8)
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
token: str
|
||||
expiredAt: datetime
|
||||
user: UserProfile
|
||||
42
ai_knowledge_base_v2/apps/backend/app/schemas/chat.py
Normal file
42
ai_knowledge_base_v2/apps/backend/app/schemas/chat.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.schemas.common import ORMModel
|
||||
|
||||
|
||||
class CreateSessionResponse(BaseModel):
|
||||
sessionId: int
|
||||
|
||||
|
||||
class ChatSessionRead(ORMModel):
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
|
||||
id: int
|
||||
title: str
|
||||
messageCount: int = Field(alias="message_count")
|
||||
updatedAt: datetime = Field(alias="updated_at")
|
||||
|
||||
|
||||
class ChatMessageRead(ORMModel):
|
||||
id: int
|
||||
role: str
|
||||
content: str
|
||||
message_status: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class UpdateSessionTitleRequest(BaseModel):
|
||||
sessionId: int
|
||||
title: str = Field(min_length=1, max_length=100)
|
||||
|
||||
|
||||
class ChatCompletionRequest(BaseModel):
|
||||
sessionId: int
|
||||
message: str = Field(min_length=1, max_length=4000)
|
||||
|
||||
|
||||
class StopChatRequest(BaseModel):
|
||||
sessionId: int
|
||||
7
ai_knowledge_base_v2/apps/backend/app/schemas/common.py
Normal file
7
ai_knowledge_base_v2/apps/backend/app/schemas/common.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class ORMModel(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
20
ai_knowledge_base_v2/apps/backend/app/schemas/user.py
Normal file
20
ai_knowledge_base_v2/apps/backend/app/schemas/user.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from app.schemas.common import ORMModel
|
||||
|
||||
|
||||
class UserProfile(ORMModel):
|
||||
id: int
|
||||
phone: str
|
||||
name: str
|
||||
nickname: str | None = None
|
||||
dailyLimit: int = Field(alias="daily_chat_limit")
|
||||
todayUsed: int = Field(alias="daily_chat_used")
|
||||
status: int
|
||||
effective_at: datetime | None = None
|
||||
expired_at: datetime | None = None
|
||||
last_login_at: datetime | None = None
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.security import create_access_token
|
||||
from app.models.user import User
|
||||
from app.services.sms_code_service import SmsCodeService
|
||||
|
||||
|
||||
class AuthService:
|
||||
_revoked_token_jti: set[str] = set()
|
||||
|
||||
@classmethod
|
||||
def send_sms_code(cls, phone: str) -> None:
|
||||
SmsCodeService.send_code(phone)
|
||||
|
||||
@classmethod
|
||||
def login_with_sms(cls, db: Session, phone: str, code: str) -> dict:
|
||||
SmsCodeService.verify_code(phone, code)
|
||||
user = cls._get_or_create_user(db, phone)
|
||||
cls._ensure_user_can_login(user)
|
||||
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
user.last_login_at = now
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
token, expired_at = create_access_token(str(user.id), "user")
|
||||
return {"token": token, "expiredAt": expired_at, "user": user}
|
||||
|
||||
@classmethod
|
||||
def logout(cls, token_payload: dict) -> None:
|
||||
jti = token_payload.get("jti")
|
||||
if jti:
|
||||
cls._revoked_token_jti.add(str(jti))
|
||||
|
||||
@classmethod
|
||||
def is_token_revoked(cls, token_payload: dict) -> bool:
|
||||
jti = token_payload.get("jti")
|
||||
return bool(jti and str(jti) in cls._revoked_token_jti)
|
||||
|
||||
@classmethod
|
||||
def _get_or_create_user(cls, db: Session, phone: str) -> User:
|
||||
user = db.scalar(select(User).where(User.phone == phone, User.is_deleted == 0))
|
||||
if user is not None:
|
||||
return user
|
||||
|
||||
settings = get_settings()
|
||||
user = User(
|
||||
phone=phone,
|
||||
name=f"{settings.default_user_name_prefix}{phone[-4:]}",
|
||||
daily_chat_limit=settings.default_daily_chat_limit,
|
||||
daily_chat_used=0,
|
||||
status=1,
|
||||
is_deleted=0,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def _ensure_user_can_login(user: User) -> None:
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
if user.status != 1:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已禁用")
|
||||
if user.effective_at and user.effective_at > now:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号尚未生效")
|
||||
if user.expired_at and user.expired_at < now:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已过期")
|
||||
116
ai_knowledge_base_v2/apps/backend/app/services/chat_service.py
Normal file
116
ai_knowledge_base_v2/apps/backend/app/services/chat_service.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.chat import ChatMessage, ChatSession
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class ChatService:
|
||||
@staticmethod
|
||||
def create_session(db: Session, user: User) -> ChatSession:
|
||||
now = _now()
|
||||
session = ChatSession(
|
||||
user_id=user.id,
|
||||
title="新聊天",
|
||||
message_count=0,
|
||||
last_message_at=now,
|
||||
is_deleted=0,
|
||||
)
|
||||
db.add(session)
|
||||
db.commit()
|
||||
db.refresh(session)
|
||||
return session
|
||||
|
||||
@staticmethod
|
||||
def list_sessions(db: Session, user: User) -> list[ChatSession]:
|
||||
return list(
|
||||
db.scalars(
|
||||
select(ChatSession)
|
||||
.where(ChatSession.user_id == user.id, ChatSession.is_deleted == 0)
|
||||
.order_by(ChatSession.updated_at.desc())
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_history(db: Session, user: User, session_id: int) -> list[ChatMessage]:
|
||||
session = ChatService._get_user_session(db, user, session_id)
|
||||
return list(
|
||||
db.scalars(
|
||||
select(ChatMessage)
|
||||
.where(ChatMessage.session_id == session.id, ChatMessage.user_id == user.id)
|
||||
.order_by(ChatMessage.created_at.asc(), ChatMessage.id.asc())
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_title(db: Session, user: User, session_id: int, title: str) -> ChatSession:
|
||||
session = ChatService._get_user_session(db, user, session_id)
|
||||
session.title = title.strip()
|
||||
db.add(session)
|
||||
db.commit()
|
||||
db.refresh(session)
|
||||
return session
|
||||
|
||||
@staticmethod
|
||||
def delete_session(db: Session, user: User, session_id: int) -> None:
|
||||
session = ChatService._get_user_session(db, user, session_id)
|
||||
session.is_deleted = 1
|
||||
db.add(session)
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def create_mock_answer(db: Session, user: User, session_id: int, question: str) -> str:
|
||||
session = ChatService._get_user_session(db, user, session_id)
|
||||
now = _now()
|
||||
answer = (
|
||||
"这是 mock AI 回复。当前阶段已经跑通后端聊天链路,后续会替换为:权限过滤、飞书实时检索、"
|
||||
"Prompt 组装和真实大模型 SSE 输出。"
|
||||
)
|
||||
user_message = ChatMessage(
|
||||
session_id=session.id,
|
||||
user_id=user.id,
|
||||
role="user",
|
||||
content=question.strip(),
|
||||
message_status="FINISHED",
|
||||
created_at=now,
|
||||
)
|
||||
assistant_message = ChatMessage(
|
||||
session_id=session.id,
|
||||
user_id=user.id,
|
||||
role="assistant",
|
||||
content=answer,
|
||||
message_status="FINISHED",
|
||||
created_at=now,
|
||||
)
|
||||
session.message_count += 2
|
||||
session.last_message_at = now
|
||||
if session.title == "新聊天":
|
||||
session.title = _title_from_question(question)
|
||||
db.add_all([user_message, assistant_message, session])
|
||||
db.commit()
|
||||
return answer
|
||||
|
||||
@staticmethod
|
||||
def stop_generation(db: Session, user: User, session_id: int) -> None:
|
||||
ChatService._get_user_session(db, user, session_id)
|
||||
|
||||
@staticmethod
|
||||
def _get_user_session(db: Session, user: User, session_id: int) -> ChatSession:
|
||||
session = db.get(ChatSession, session_id)
|
||||
if session is None or session.user_id != user.id or session.is_deleted:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="会话不存在")
|
||||
return session
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(UTC).replace(tzinfo=None)
|
||||
|
||||
|
||||
def _title_from_question(question: str) -> str:
|
||||
title = question.strip().replace("\n", " ")
|
||||
return title[:20] if title else "新聊天"
|
||||
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from re import fullmatch
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
@dataclass
|
||||
class SmsCodeRecord:
|
||||
code: str
|
||||
expires_at: datetime
|
||||
|
||||
|
||||
class SmsCodeService:
|
||||
_codes: dict[str, SmsCodeRecord] = {}
|
||||
|
||||
@classmethod
|
||||
def send_code(cls, phone: str) -> None:
|
||||
cls._validate_phone(phone)
|
||||
settings = get_settings()
|
||||
if not settings.mock_sms_enabled:
|
||||
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="真实短信服务尚未接入")
|
||||
cls._codes[phone] = SmsCodeRecord(
|
||||
code=settings.mock_sms_code,
|
||||
expires_at=datetime.now(UTC) + timedelta(minutes=settings.sms_code_expire_minutes),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def verify_code(cls, phone: str, code: str) -> None:
|
||||
cls._validate_phone(phone)
|
||||
record = cls._codes.get(phone)
|
||||
settings = get_settings()
|
||||
if settings.mock_sms_enabled and record is None and code == settings.mock_sms_code:
|
||||
return
|
||||
if record is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码不存在或已过期")
|
||||
if record.expires_at < datetime.now(UTC):
|
||||
cls._codes.pop(phone, None)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码已过期")
|
||||
if record.code != code:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码错误")
|
||||
cls._codes.pop(phone, None)
|
||||
|
||||
@staticmethod
|
||||
def _validate_phone(phone: str) -> None:
|
||||
if fullmatch(r"1[3-9]\d{9}", phone) is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号格式不正确")
|
||||
Reference in New Issue
Block a user