后端fastapi搭建,login接口-v1.0

pull/42/head
echo 2 months ago
parent ab74172af1
commit 0ea547da22

@ -6,4 +6,7 @@ load_dotenv()
DATABASE_URL = os.getenv(
"DATABASE_URL",
"postgresql+asyncpg://postgres:password@localhost:5432/hadoop_fault_db",
)
)
JWT_SECRET = os.getenv("JWT_SECRET", "dev-secret")
JWT_EXPIRE_MINUTES = int(os.getenv("JWT_EXPIRE_MINUTES", "60"))

@ -1,5 +1,5 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.config import DATABASE_URL
from .config import DATABASE_URL
engine = create_async_engine(DATABASE_URL, echo=False, pool_pre_ping=True)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
@ -7,4 +7,4 @@ SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSe
async def get_db() -> AsyncSession:
"""获取一个异步数据库会话,用于依赖注入。"""
async with SessionLocal() as session:
yield session
yield session

@ -1,6 +1,6 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import health, clusters, faults, logs
from .routers import auth, health
app = FastAPI(title="Hadoop Fault Detecting API", version="v1")
@ -13,6 +13,4 @@ app.add_middleware(
)
app.include_router(health.router, prefix="/api/v1")
app.include_router(clusters.router, prefix="/api/v1")
app.include_router(faults.router, prefix="/api/v1")
app.include_router(logs.router, prefix="/api/v1")
app.include_router(auth.router, prefix="/api/v1")

@ -1,4 +1,4 @@
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass

@ -1,34 +0,0 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Integer
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy import TIMESTAMP
from app.models import Base
class Cluster(Base):
__tablename__ = "clusters"
id: Mapped[int] = mapped_column(primary_key=True)
uuid: Mapped[str] = mapped_column(UUID(as_uuid=False), unique=True)
name: Mapped[str] = mapped_column(String(100), unique=True)
type: Mapped[str] = mapped_column(String(50))
node_count: Mapped[int] = mapped_column(Integer, default=0)
health_status: Mapped[str] = mapped_column(String(20), default="unknown")
description: Mapped[str | None] = mapped_column(String, nullable=True)
config_info: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
def to_dict(self) -> dict:
"""将集群对象转换为可序列化字典。"""
return {
"id": self.id,
"uuid": self.uuid,
"name": self.name,
"type": self.type,
"node_count": self.node_count,
"health_status": self.health_status,
"description": self.description,
"config_info": self.config_info,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}

@ -1,39 +0,0 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Integer
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy import TIMESTAMP, Text
from app.models import Base
class ExecLog(Base):
__tablename__ = "exec_logs"
id: Mapped[int] = mapped_column(primary_key=True)
exec_id: Mapped[str] = mapped_column(String(32), unique=True)
fault_id: Mapped[str] = mapped_column(String(32))
command_type: Mapped[str] = mapped_column(String(50))
script_path: Mapped[str | None] = mapped_column(String(255), nullable=True)
command_content: Mapped[str] = mapped_column(Text)
target_nodes: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
risk_level: Mapped[str] = mapped_column(String(20), default="medium")
execution_status: Mapped[str] = mapped_column(String(20), default="pending")
start_time: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True)
end_time: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True)
duration: Mapped[int | None] = mapped_column(Integer, nullable=True)
stdout_log: Mapped[str | None] = mapped_column(Text, nullable=True)
stderr_log: Mapped[str | None] = mapped_column(Text, nullable=True)
exit_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
operator: Mapped[str] = mapped_column(String(50), default="system")
created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
def to_dict(self) -> dict:
"""将执行日志转换为可序列化字典。"""
return {
"exec_id": self.exec_id,
"fault_id": self.fault_id,
"command_type": self.command_type,
"execution_status": self.execution_status,
"start_time": self.start_time.isoformat() if self.start_time else None,
"end_time": self.end_time.isoformat() if self.end_time else None,
"exit_code": self.exit_code,
}

@ -1,38 +0,0 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy import TIMESTAMP
from app.models import Base
class FaultRecord(Base):
__tablename__ = "fault_records"
id: Mapped[int] = mapped_column(primary_key=True)
fault_id: Mapped[str] = mapped_column(String(32), unique=True)
cluster_id: Mapped[int | None] = mapped_column(nullable=True)
fault_type: Mapped[str] = mapped_column(String(50))
fault_level: Mapped[str] = mapped_column(String(20), default="medium")
title: Mapped[str] = mapped_column(String(200))
description: Mapped[str | None] = mapped_column(String, nullable=True)
affected_nodes: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
affected_clusters: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
root_cause: Mapped[str | None] = mapped_column(String, nullable=True)
repair_suggestion: Mapped[str | None] = mapped_column(String, nullable=True)
status: Mapped[str] = mapped_column(String(20), default="detected")
assignee: Mapped[str | None] = mapped_column(String(50), nullable=True)
reporter: Mapped[str] = mapped_column(String(50), default="system")
created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
resolved_at: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True)
def to_dict(self) -> dict:
"""将故障记录转换为可序列化字典。"""
return {
"fault_id": self.fault_id,
"cluster_id": self.cluster_id,
"fault_type": self.fault_type,
"fault_level": self.fault_level,
"title": self.title,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
}

@ -1,33 +0,0 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Boolean
from sqlalchemy import TIMESTAMP, Text
from app.models import Base
class SystemLog(Base):
__tablename__ = "system_logs"
id: Mapped[int] = mapped_column(primary_key=True)
log_id: Mapped[str] = mapped_column(String(32), unique=True)
fault_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
cluster_id: Mapped[int | None] = mapped_column(nullable=True)
timestamp: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
host: Mapped[str] = mapped_column(String(100))
service: Mapped[str] = mapped_column(String(50))
log_level: Mapped[str] = mapped_column(String(10))
message: Mapped[str] = mapped_column(Text)
exception: Mapped[str | None] = mapped_column(Text, nullable=True)
raw_log: Mapped[str | None] = mapped_column(Text, nullable=True)
processed: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
def to_dict(self) -> dict:
"""将系统日志转换为可序列化字典。"""
return {
"log_id": self.log_id,
"cluster_id": self.cluster_id,
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
"service": self.service,
"log_level": self.log_level,
"message": self.message,
"processed": self.processed,
}

@ -0,0 +1,17 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Boolean
from sqlalchemy import TIMESTAMP
from . import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(50), unique=True)
email: Mapped[str] = mapped_column(String(100), unique=True)
password_hash: Mapped[str] = mapped_column(String(255))
full_name: Mapped[str] = mapped_column(String(100))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_login: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True)
created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))

@ -0,0 +1,30 @@
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, func
from ..db import get_db
from ..models.users import User
from passlib.hash import bcrypt
router = APIRouter()
class LoginRequest(BaseModel):
username: str
password: str
@router.post("/user/login")
async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
"""处理登录请求,验证用户名与密码。"""
result = await db.execute(select(User).where(User.username == req.username).limit(1))
user = result.scalars().first()
if not user:
raise HTTPException(status_code=401, detail="invalid_credentials")
if not user.is_active:
raise HTTPException(status_code=403, detail="inactive_user")
if not bcrypt.verify(req.password, user.password_hash):
raise HTTPException(status_code=401, detail="invalid_credentials")
await db.execute(
update(User).where(User.id == user.id).values(last_login=func.now(), updated_at=func.now())
)
await db.commit()
return {"ok": True, "username": user.username, "fullName": user.full_name}

@ -1,14 +0,0 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.db import get_db
from app.models.clusters import Cluster
router = APIRouter()
@router.get("/clusters")
async def list_clusters(db: AsyncSession = Depends(get_db)):
"""查询集群列表。"""
result = await db.execute(select(Cluster).limit(100))
rows = result.scalars().all()
return {"total": len(rows), "list": [c.to_dict() for c in rows]}

@ -1,14 +0,0 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.db import get_db
from app.models.fault_records import FaultRecord
router = APIRouter()
@router.get("/faults")
async def list_faults(db: AsyncSession = Depends(get_db)):
"""查询故障记录。"""
result = await db.execute(select(FaultRecord).limit(100))
rows = result.scalars().all()
return {"total": len(rows), "list": [f.to_dict() for f in rows]}

@ -1,11 +1,8 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_db
from fastapi import APIRouter
router = APIRouter()
@router.get("/health")
async def health_check(db: AsyncSession = Depends(get_db)):
"""健康检查:测试数据库连通性。"""
await db.execute("SELECT 1")
return {"status": "ok"}
async def health_check():
"""简单健康检查,用于开发阶段验证服务可用。"""
return {"status": "ok"}

@ -1,23 +0,0 @@
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.db import get_db
from app.models.system_logs import SystemLog
router = APIRouter()
@router.get("/logs")
async def list_logs(
db: AsyncSession = Depends(get_db),
level: str | None = Query(None),
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100),
):
"""查询系统日志,支持按级别筛选与分页。"""
stmt = select(SystemLog)
if level:
stmt = stmt.where(SystemLog.log_level == level)
stmt = stmt.offset((page - 1) * pageSize).limit(pageSize)
result = await db.execute(stmt)
rows = result.scalars().all()
return {"total": len(rows), "list": [l.to_dict() for l in rows]}

@ -0,0 +1,6 @@
fastapi
uvicorn[standard]
SQLAlchemy
asyncpg
python-dotenv
passlib[bcrypt]

@ -476,7 +476,7 @@ INSERT INTO user_cluster_mapping (user_id, cluster_id, role_id)
SELECT u.id, c.id, r.id FROM users u, clusters c, roles r WHERE u.username = 'admin' AND c.name = 'Hadoop主集群' AND r.role_key = 'cluster_admin';
INSERT INTO user_cluster_mapping (user_id, cluster_id, role_id)
SELECT u.id, c.id, r.id FROM users u, clusters c, roles r WHERE u.username = 'admin' AND c.name = 'Hadoop测试集群' AND r.role_key = 'cluster_admin';
-- =====================================================
-- 脚本执行完成
-- =====================================================

@ -1411,6 +1411,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -1425,6 +1426,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@ -1484,6 +1486,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",

@ -7,4 +7,4 @@ SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSe
async def get_db() -> AsyncSession:
"""获取一个异步数据库会话,用于依赖注入。"""
async with SessionLocal() as session:
yield session
yield session

@ -1,6 +1,6 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .routers import health, clusters, faults, logs
from .routers import user
app = FastAPI(title="Hadoop Fault Detecting API", version="v1")
@ -12,7 +12,4 @@ app.add_middleware(
allow_headers=["*"],
)
app.include_router(health.router, prefix="/api/v1")
app.include_router(clusters.router, prefix="/api/v1")
app.include_router(faults.router, prefix="/api/v1")
app.include_router(logs.router, prefix="/api/v1")
app.include_router(user.router, prefix="/api/v1")

@ -1,4 +1,4 @@
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass

@ -0,0 +1,17 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Boolean
from sqlalchemy import TIMESTAMP
from . import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(50), unique=True)
email: Mapped[str] = mapped_column(String(100), unique=True)
password_hash: Mapped[str] = mapped_column(String(255))
full_name: Mapped[str] = mapped_column(String(100))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_login: Mapped[str | None] = mapped_column(TIMESTAMP(timezone=True), nullable=True)
created_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))
updated_at: Mapped[str] = mapped_column(TIMESTAMP(timezone=True))

@ -0,0 +1,30 @@
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, func
from ..db import get_db
from ..models.users import User
from passlib.hash import bcrypt
router = APIRouter()
class LoginRequest(BaseModel):
username: str
password: str
@router.post("/user/login")
async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
"""处理登录请求,验证用户名与密码。"""
result = await db.execute(select(User).where(User.username == req.username).limit(1))
user = result.scalars().first()
if not user:
raise HTTPException(status_code=401, detail="invalid_credentials")
if not user.is_active:
raise HTTPException(status_code=403, detail="inactive_user")
if not bcrypt.verify(req.password, user.password_hash):
raise HTTPException(status_code=401, detail="invalid_credentials")
await db.execute(
update(User).where(User.id == user.id).values(last_login=func.now(), updated_at=func.now())
)
await db.commit()
return {"ok": True, "username": user.username, "fullName": user.full_name}

@ -3,4 +3,4 @@ uvicorn[standard]
SQLAlchemy
asyncpg
python-dotenv
psycopg2-binary
passlib[bcrypt]

Loading…
Cancel
Save