由于本次代码变更内容为空,无法生成有效的提交信息。请提供具体的代码变更内容以便生成合适的提交信息。登录、微信登录等认证功能- 添加管理员登录功能
- 实现个人资料更新和密码修改- 配置数据库连接和 Alembic 迁移 - 添加健康检查和系统统计接口 - 实现自定义错误处理和响应格式 - 配置 FastAPI 应用和中间件
This commit is contained in:
41
fastapi-backend/alembic.ini
Normal file
41
fastapi-backend/alembic.ini
Normal file
@@ -0,0 +1,41 @@
|
||||
[alembic]
|
||||
# 模板路径
|
||||
script_location = fastapi-backend/alembic
|
||||
|
||||
# 数据库连接URL
|
||||
sqlalchemy.url = mysql+pymysql://jiebanke:aiot741$12346@nj-cdb-3pwh2kz1.sql.tencentcdb.com:20784/jbkdata
|
||||
|
||||
# 日志配置
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
79
fastapi-backend/alembic/env.py
Normal file
79
fastapi-backend/alembic/env.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# 导入模型
|
||||
from app.models.user import Base
|
||||
|
||||
# 导入配置
|
||||
from app.core.config import settings
|
||||
|
||||
# 确保Python能够找到app模块
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
# 这是Alembic Config对象,它提供对.ini文件中值的访问
|
||||
config = context.config
|
||||
|
||||
# 解释配置文件并设置日志记录器
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# 添加你的模型元数据对象
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# 其他值来自config,可以通过以下方式定义:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... 等等。
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""在'offline'模式下运行迁移。
|
||||
|
||||
这配置了上下文,只需要一个URL,并且不要求引擎可用。
|
||||
跳过引擎创建,甚至不需要DBAPI可用。
|
||||
|
||||
调用context.execute()来执行迁移。
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""在'online'模式下运行迁移。
|
||||
|
||||
在这种情况下,我们创建了一个Engine并将其与迁移上下文关联。
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
62
fastapi-backend/alembic/versions/0001_initial_migration.py
Normal file
62
fastapi-backend/alembic/versions/0001_initial_migration.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""initial migration
|
||||
|
||||
Revision ID: 0001
|
||||
Revises:
|
||||
Create Date: 2025-09-11 16:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0001'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# 创建用户表
|
||||
op.create_table(
|
||||
'users',
|
||||
sa.Column('id', sa.Integer, primary_key=True, index=True),
|
||||
sa.Column('username', sa.String(50), unique=True, index=True, nullable=False),
|
||||
sa.Column('password_hash', sa.String(255), nullable=False),
|
||||
sa.Column('user_type', sa.Enum('farmer', 'merchant', 'admin', 'super_admin'), server_default='farmer'),
|
||||
sa.Column('real_name', sa.String(50)),
|
||||
sa.Column('nickname', sa.String(50)),
|
||||
sa.Column('avatar_url', sa.String(255)),
|
||||
sa.Column('email', sa.String(100), unique=True, index=True),
|
||||
sa.Column('phone', sa.String(20), unique=True, index=True),
|
||||
sa.Column('gender', sa.Enum('male', 'female', 'other'), server_default='other'),
|
||||
sa.Column('birthday', sa.DateTime),
|
||||
sa.Column('status', sa.Enum('active', 'inactive'), server_default='active'),
|
||||
sa.Column('wechat_openid', sa.String(100), unique=True, index=True),
|
||||
sa.Column('wechat_unionid', sa.String(100), unique=True, index=True),
|
||||
sa.Column('level', sa.Integer, server_default='1'),
|
||||
sa.Column('created_at', sa.DateTime, server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), onupdate=sa.func.now()),
|
||||
sa.Column('last_login', sa.DateTime)
|
||||
)
|
||||
|
||||
# 创建管理员表
|
||||
op.create_table(
|
||||
'admins',
|
||||
sa.Column('id', sa.Integer, primary_key=True, index=True),
|
||||
sa.Column('username', sa.String(50), unique=True, index=True, nullable=False),
|
||||
sa.Column('password', sa.String(255), nullable=False),
|
||||
sa.Column('email', sa.String(100), unique=True, index=True),
|
||||
sa.Column('nickname', sa.String(50)),
|
||||
sa.Column('avatar', sa.String(255)),
|
||||
sa.Column('role', sa.Enum('admin', 'super_admin'), server_default='admin'),
|
||||
sa.Column('status', sa.Enum('active', 'inactive'), server_default='active'),
|
||||
sa.Column('last_login', sa.DateTime),
|
||||
sa.Column('created_at', sa.DateTime, server_default=sa.func.now()),
|
||||
sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), onupdate=sa.func.now())
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('admins')
|
||||
op.drop_table('users')
|
||||
121
fastapi-backend/app/api/api.py
Normal file
121
fastapi-backend/app/api/api.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
from app.api.endpoints import auth, users
|
||||
from app.core.config import settings
|
||||
from app.utils.response import error_response
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title=settings.PROJECT_NAME,
|
||||
openapi_url=f"{settings.API_V1_STR}/openapi.json",
|
||||
docs_url="/api-docs",
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
# 配置CORS
|
||||
if settings.BACKEND_CORS_ORIGINS:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
else:
|
||||
# 默认CORS配置
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 创建API路由
|
||||
api_router = APIRouter()
|
||||
|
||||
# 添加各个端点路由
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["用户"])
|
||||
|
||||
# 将API路由添加到应用
|
||||
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
|
||||
|
||||
# 自定义异常处理
|
||||
@app.exception_handler(StarletteHTTPException)
|
||||
async def http_exception_handler(request, exc):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=error_response(
|
||||
message=str(exc.detail),
|
||||
code=exc.status_code
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request, exc):
|
||||
errors = []
|
||||
for error in exc.errors():
|
||||
error_msg = f"{error['loc'][-1]}: {error['msg']}"
|
||||
errors.append(error_msg)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content=error_response(
|
||||
message="请求参数验证失败",
|
||||
code=400,
|
||||
data={"errors": errors}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# 健康检查路由
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
import platform
|
||||
import psutil
|
||||
from datetime import datetime
|
||||
|
||||
return {
|
||||
"status": "OK",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"uptime": psutil.boot_time(),
|
||||
"environment": settings.DEBUG and "development" or "production",
|
||||
"no_db_mode": settings.NO_DB_MODE,
|
||||
"system_info": {
|
||||
"python_version": platform.python_version(),
|
||||
"platform": platform.platform(),
|
||||
"cpu_count": psutil.cpu_count(),
|
||||
"memory": {
|
||||
"total": psutil.virtual_memory().total,
|
||||
"available": psutil.virtual_memory().available,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 系统统计路由
|
||||
@app.get("/system-stats")
|
||||
async def system_stats():
|
||||
import platform
|
||||
import psutil
|
||||
from datetime import datetime
|
||||
|
||||
return {
|
||||
"status": "OK",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"environment": settings.DEBUG and "development" or "production",
|
||||
"python_version": platform.python_version(),
|
||||
"memory_usage": dict(psutil.virtual_memory()._asdict()),
|
||||
"uptime": psutil.boot_time(),
|
||||
"cpu_count": psutil.cpu_count(),
|
||||
"platform": platform.platform(),
|
||||
"architecture": platform.architecture(),
|
||||
"no_db_mode": settings.NO_DB_MODE
|
||||
}
|
||||
115
fastapi-backend/app/api/deps.py
Normal file
115
fastapi-backend/app/api/deps.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from typing import Generator, Optional
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import jwt, JWTError
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import verify_password
|
||||
from app.crud.user import user, admin
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.user import User, Admin
|
||||
from app.schemas.user import TokenPayload
|
||||
|
||||
# OAuth2密码承载令牌
|
||||
oauth2_scheme = OAuth2PasswordBearer(
|
||||
tokenUrl=f"{settings.API_V1_STR}/auth/login"
|
||||
)
|
||||
|
||||
# 获取数据库会话
|
||||
def get_db() -> Generator:
|
||||
try:
|
||||
db = SessionLocal()
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# 获取当前用户
|
||||
def get_current_user(
|
||||
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
|
||||
) -> User:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
token_data = TokenPayload(**payload)
|
||||
|
||||
# 检查令牌是否过期
|
||||
if token_data.exp is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="令牌无效",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
except (JWTError, ValidationError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无法验证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 获取用户
|
||||
current_user = user.get(db, user_id=token_data.sub)
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||
|
||||
# 检查用户是否活跃
|
||||
if not user.is_active(current_user):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账户已被禁用")
|
||||
|
||||
return current_user
|
||||
|
||||
# 获取当前活跃用户
|
||||
def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
if not user.is_active(current_user):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账户已被禁用")
|
||||
return current_user
|
||||
|
||||
# 获取当前管理员
|
||||
def get_current_admin(
|
||||
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
|
||||
) -> Admin:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
token_data = TokenPayload(**payload)
|
||||
except (JWTError, ValidationError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无法验证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 获取管理员
|
||||
current_admin = admin.get(db, admin_id=token_data.sub)
|
||||
if not current_admin:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="管理员不存在")
|
||||
|
||||
# 检查管理员是否活跃
|
||||
if not admin.is_active(current_admin):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账户已被禁用")
|
||||
|
||||
return current_admin
|
||||
|
||||
# 获取当前活跃管理员
|
||||
def get_current_active_admin(
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> Admin:
|
||||
if not admin.is_active(current_admin):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账户已被禁用")
|
||||
return current_admin
|
||||
|
||||
# 获取超级管理员
|
||||
def get_current_super_admin(
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> Admin:
|
||||
if current_admin.role != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要超级管理员权限"
|
||||
)
|
||||
return current_admin
|
||||
332
fastapi-backend/app/api/endpoints/auth.py
Normal file
332
fastapi-backend/app/api/endpoints/auth.py
Normal file
@@ -0,0 +1,332 @@
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.core.config import settings
|
||||
from app.core.security import create_access_token, create_refresh_token
|
||||
from app.crud.user import user, admin
|
||||
from app.schemas.user import (
|
||||
UserCreate, UserResponse, UserLogin, UserWithToken,
|
||||
Token, PasswordChange, AdminWithToken
|
||||
)
|
||||
from app.utils.response import success_response, error_response
|
||||
from app.utils.errors import BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserWithToken, status_code=status.HTTP_201_CREATED)
|
||||
def register(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
user_in: UserCreate,
|
||||
) -> Any:
|
||||
"""
|
||||
用户注册
|
||||
"""
|
||||
# 检查用户名是否已存在
|
||||
if user.get_by_username(db, username=user_in.username):
|
||||
raise BadRequestError("用户名已存在")
|
||||
|
||||
# 检查邮箱是否已存在
|
||||
if user_in.email and user.get_by_email(db, email=user_in.email):
|
||||
raise BadRequestError("邮箱已存在")
|
||||
|
||||
# 检查手机号是否已存在
|
||||
if user_in.phone and user.get_by_phone(db, phone=user_in.phone):
|
||||
raise BadRequestError("手机号已存在")
|
||||
|
||||
# 创建新用户
|
||||
db_user = user.create(db, obj_in=user_in)
|
||||
|
||||
# 更新最后登录时间
|
||||
user.update_last_login(db, db_obj=db_user)
|
||||
|
||||
# 生成令牌
|
||||
access_token = create_access_token(db_user.id)
|
||||
refresh_token = create_refresh_token(db_user.id)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"user": db_user,
|
||||
"token": access_token,
|
||||
"refresh_token": refresh_token
|
||||
},
|
||||
message="注册成功",
|
||||
code=201
|
||||
)
|
||||
|
||||
|
||||
@router.post("/login", response_model=UserWithToken)
|
||||
def login(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
form_data: OAuth2PasswordRequestForm = Depends()
|
||||
) -> Any:
|
||||
"""
|
||||
用户登录
|
||||
"""
|
||||
# 验证用户
|
||||
db_user = user.authenticate(db, username=form_data.username, password=form_data.password)
|
||||
if not db_user:
|
||||
raise UnauthorizedError("用户名或密码错误")
|
||||
|
||||
# 检查用户状态
|
||||
if not user.is_active(db_user):
|
||||
raise ForbiddenError("账户已被禁用")
|
||||
|
||||
# 更新最后登录时间
|
||||
user.update_last_login(db, db_obj=db_user)
|
||||
|
||||
# 生成令牌
|
||||
access_token = create_access_token(db_user.id)
|
||||
refresh_token = create_refresh_token(db_user.id)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"user": db_user,
|
||||
"token": access_token,
|
||||
"refresh_token": refresh_token
|
||||
},
|
||||
message="登录成功"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/login/json", response_model=UserWithToken)
|
||||
def login_json(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
login_in: UserLogin
|
||||
) -> Any:
|
||||
"""
|
||||
用户登录(JSON格式)
|
||||
"""
|
||||
# 验证用户
|
||||
db_user = user.authenticate(db, username=login_in.username, password=login_in.password)
|
||||
if not db_user:
|
||||
raise UnauthorizedError("用户名或密码错误")
|
||||
|
||||
# 检查用户状态
|
||||
if not user.is_active(db_user):
|
||||
raise ForbiddenError("账户已被禁用")
|
||||
|
||||
# 更新最后登录时间
|
||||
user.update_last_login(db, db_obj=db_user)
|
||||
|
||||
# 生成令牌
|
||||
access_token = create_access_token(db_user.id)
|
||||
refresh_token = create_refresh_token(db_user.id)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"user": db_user,
|
||||
"token": access_token,
|
||||
"refresh_token": refresh_token
|
||||
},
|
||||
message="登录成功"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh-token", response_model=Token)
|
||||
def refresh_token(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
refresh_token: str = Body(..., embed=True)
|
||||
) -> Any:
|
||||
"""
|
||||
刷新访问令牌
|
||||
"""
|
||||
try:
|
||||
from jose import jwt
|
||||
from pydantic import ValidationError
|
||||
from app.schemas.user import TokenPayload
|
||||
|
||||
payload = jwt.decode(
|
||||
refresh_token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
token_data = TokenPayload(**payload)
|
||||
|
||||
# 检查令牌类型
|
||||
if token_data.type != "refresh":
|
||||
raise UnauthorizedError("无效的刷新令牌")
|
||||
|
||||
# 检查用户是否存在
|
||||
db_user = user.get(db, user_id=token_data.sub)
|
||||
if not db_user:
|
||||
raise NotFoundError("用户不存在")
|
||||
|
||||
# 检查用户状态
|
||||
if not user.is_active(db_user):
|
||||
raise ForbiddenError("账户已被禁用")
|
||||
|
||||
# 生成新令牌
|
||||
access_token = create_access_token(db_user.id)
|
||||
new_refresh_token = create_refresh_token(db_user.id)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"access_token": access_token,
|
||||
"refresh_token": new_refresh_token,
|
||||
"token_type": "bearer"
|
||||
}
|
||||
)
|
||||
except (jwt.JWTError, ValidationError):
|
||||
raise UnauthorizedError("无效的刷新令牌")
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def get_current_user_info(
|
||||
current_user: UserResponse = Depends(get_current_user)
|
||||
) -> Any:
|
||||
"""
|
||||
获取当前用户信息
|
||||
"""
|
||||
return success_response(data=current_user)
|
||||
|
||||
|
||||
@router.put("/profile", response_model=UserResponse)
|
||||
def update_profile(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserResponse = Depends(get_current_user),
|
||||
profile_in: dict = Body(...)
|
||||
) -> Any:
|
||||
"""
|
||||
更新用户个人信息
|
||||
"""
|
||||
# 更新用户信息
|
||||
db_user = user.update(db, db_obj=current_user, obj_in=profile_in)
|
||||
|
||||
return success_response(
|
||||
data=db_user,
|
||||
message="个人信息更新成功"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/password", response_model=dict)
|
||||
def change_password(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserResponse = Depends(get_current_user),
|
||||
password_in: PasswordChange
|
||||
) -> Any:
|
||||
"""
|
||||
修改密码
|
||||
"""
|
||||
from app.core.security import verify_password
|
||||
|
||||
# 验证当前密码
|
||||
if not verify_password(password_in.current_password, current_user.password_hash):
|
||||
raise UnauthorizedError("当前密码错误")
|
||||
|
||||
# 更新密码
|
||||
user.update_password(db, db_obj=current_user, new_password=password_in.new_password)
|
||||
|
||||
return success_response(message="密码修改成功")
|
||||
|
||||
|
||||
@router.post("/admin/login", response_model=AdminWithToken)
|
||||
def admin_login(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
login_in: UserLogin
|
||||
) -> Any:
|
||||
"""
|
||||
管理员登录
|
||||
"""
|
||||
# 验证管理员
|
||||
db_admin = admin.authenticate(db, username=login_in.username, password=login_in.password)
|
||||
if not db_admin:
|
||||
raise UnauthorizedError("用户名或密码错误")
|
||||
|
||||
# 检查管理员状态
|
||||
if not admin.is_active(db_admin):
|
||||
raise ForbiddenError("账户已被禁用")
|
||||
|
||||
# 更新最后登录时间
|
||||
admin.update_last_login(db, db_obj=db_admin)
|
||||
|
||||
# 生成令牌
|
||||
access_token = create_access_token(db_admin.id)
|
||||
refresh_token = create_refresh_token(db_admin.id)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"admin": db_admin,
|
||||
"token": access_token,
|
||||
"refresh_token": refresh_token
|
||||
},
|
||||
message="管理员登录成功"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/wechat", response_model=UserWithToken)
|
||||
def wechat_login(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
code: str = Body(...),
|
||||
user_info: dict = Body(None)
|
||||
) -> Any:
|
||||
"""
|
||||
微信登录/注册
|
||||
"""
|
||||
# 模拟获取微信用户信息
|
||||
wechat_user_info = {
|
||||
"openid": f"mock_openid_{code}",
|
||||
"unionid": f"mock_unionid_{code}",
|
||||
"nickname": user_info.get("nickName") if user_info else "微信用户",
|
||||
"avatar": user_info.get("avatarUrl") if user_info else "",
|
||||
"gender": "male" if user_info and user_info.get("gender") == 1 else
|
||||
"female" if user_info and user_info.get("gender") == 2 else "other"
|
||||
}
|
||||
|
||||
# 查找是否已存在微信用户
|
||||
db_user = user.get_by_wechat_openid(db, openid=wechat_user_info["openid"])
|
||||
|
||||
if db_user:
|
||||
# 更新最后登录时间
|
||||
user.update_last_login(db, db_obj=db_user)
|
||||
else:
|
||||
# 创建新用户(微信注册)
|
||||
import secrets
|
||||
import string
|
||||
from app.schemas.user import UserCreate
|
||||
|
||||
# 生成随机密码
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
random_password = ''.join(secrets.choice(alphabet) for _ in range(12))
|
||||
|
||||
# 创建用户
|
||||
user_in = UserCreate(
|
||||
username=f"wx_{wechat_user_info['openid'][-8:]}",
|
||||
password=random_password,
|
||||
nickname=wechat_user_info["nickname"],
|
||||
user_type="farmer"
|
||||
)
|
||||
|
||||
db_user = user.create(db, obj_in=user_in)
|
||||
|
||||
# 更新微信信息
|
||||
user.update(db, db_obj=db_user, obj_in={
|
||||
"wechat_openid": wechat_user_info["openid"],
|
||||
"wechat_unionid": wechat_user_info["unionid"],
|
||||
"avatar_url": wechat_user_info["avatar"],
|
||||
"gender": wechat_user_info["gender"]
|
||||
})
|
||||
|
||||
# 生成令牌
|
||||
access_token = create_access_token(db_user.id)
|
||||
refresh_token = create_refresh_token(db_user.id)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"user": db_user,
|
||||
"token": access_token,
|
||||
"refresh_token": refresh_token
|
||||
},
|
||||
message="微信登录成功"
|
||||
)
|
||||
174
fastapi-backend/app/api/endpoints/users.py
Normal file
174
fastapi-backend/app/api/endpoints/users.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from typing import Any, List
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Query, Path, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, get_current_user, get_current_admin, get_current_super_admin
|
||||
from app.crud.user import user
|
||||
from app.models.user import User
|
||||
from app.schemas.user import (
|
||||
UserResponse, UserUpdate, UserListResponse,
|
||||
PaginationResponse, UserStatistics, BatchUserStatusUpdate
|
||||
)
|
||||
from app.utils.response import success_response
|
||||
from app.utils.errors import NotFoundError, BadRequestError
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/profile", response_model=UserResponse)
|
||||
def get_user_profile(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
获取当前用户个人信息
|
||||
"""
|
||||
return success_response(data=current_user)
|
||||
|
||||
|
||||
@router.put("/profile", response_model=UserResponse)
|
||||
def update_user_profile(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
user_in: UserUpdate,
|
||||
) -> Any:
|
||||
"""
|
||||
更新当前用户个人信息
|
||||
"""
|
||||
# 更新用户信息
|
||||
db_user = user.update(db, db_obj=current_user, obj_in=user_in)
|
||||
|
||||
return success_response(
|
||||
data=db_user,
|
||||
message="个人信息更新成功"
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=UserListResponse)
|
||||
def get_users(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
user_type: str = Query(None, description="用户类型"),
|
||||
status: str = Query(None, description="用户状态"),
|
||||
keyword: str = Query(None, description="搜索关键词"),
|
||||
) -> Any:
|
||||
"""
|
||||
获取用户列表(管理员)
|
||||
"""
|
||||
# 计算分页参数
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# 获取用户列表
|
||||
users_list = user.get_multi(
|
||||
db, skip=skip, limit=page_size,
|
||||
user_type=user_type, status=status, keyword=keyword
|
||||
)
|
||||
|
||||
# 获取用户总数
|
||||
total = user.count(
|
||||
db, user_type=user_type, status=status, keyword=keyword
|
||||
)
|
||||
|
||||
# 计算总页数
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
|
||||
# 构建分页信息
|
||||
pagination = PaginationResponse(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total=total,
|
||||
total_pages=total_pages
|
||||
)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"users": users_list,
|
||||
"pagination": pagination
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
def get_user_by_id(
|
||||
user_id: int = Path(..., ge=1),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin),
|
||||
) -> Any:
|
||||
"""
|
||||
获取用户详情(管理员)
|
||||
"""
|
||||
# 获取用户
|
||||
db_user = user.get(db, user_id=user_id)
|
||||
if not db_user:
|
||||
raise NotFoundError("用户不存在")
|
||||
|
||||
return success_response(data=db_user)
|
||||
|
||||
|
||||
@router.get("/statistics", response_model=UserStatistics)
|
||||
def get_user_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin),
|
||||
) -> Any:
|
||||
"""
|
||||
获取用户统计信息(管理员)
|
||||
"""
|
||||
# 获取统计信息
|
||||
statistics = user.get_statistics(db)
|
||||
|
||||
return success_response(data=statistics)
|
||||
|
||||
|
||||
@router.post("/batch-status", response_model=dict)
|
||||
def batch_update_user_status(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin),
|
||||
batch_in: BatchUserStatusUpdate,
|
||||
) -> Any:
|
||||
"""
|
||||
批量操作用户状态(管理员)
|
||||
"""
|
||||
# 检查用户ID列表是否为空
|
||||
if not batch_in.user_ids:
|
||||
raise BadRequestError("用户ID列表不能为空")
|
||||
|
||||
# 批量更新用户状态
|
||||
affected_rows = user.batch_update_status(
|
||||
db, user_ids=batch_in.user_ids, status=batch_in.status
|
||||
)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"message": f"成功更新{affected_rows}个用户的状态",
|
||||
"affected_rows": affected_rows
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{user_id}", response_model=dict)
|
||||
def delete_user(
|
||||
user_id: int = Path(..., ge=1),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_super_admin),
|
||||
) -> Any:
|
||||
"""
|
||||
删除用户(超级管理员)
|
||||
"""
|
||||
# 获取用户
|
||||
db_user = user.get(db, user_id=user_id)
|
||||
if not db_user:
|
||||
raise NotFoundError("用户不存在")
|
||||
|
||||
# 删除用户
|
||||
user.remove(db, user_id=user_id)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"message": "用户删除成功",
|
||||
"user_id": user_id
|
||||
}
|
||||
)
|
||||
59
fastapi-backend/app/core/config.py
Normal file
59
fastapi-backend/app/core/config.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from pydantic import AnyHttpUrl, field_validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 基本配置
|
||||
PROJECT_NAME: str = "结伴客API"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
||||
|
||||
# 服务器配置
|
||||
HOST: str = os.getenv("HOST", "0.0.0.0")
|
||||
PORT: int = int(os.getenv("PORT", "3110"))
|
||||
|
||||
# 安全配置
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "dev-jwt-secret-key-2024")
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080")) # 7天
|
||||
REFRESH_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("REFRESH_TOKEN_EXPIRE_MINUTES", "43200")) # 30天
|
||||
ALGORITHM: str = "HS256"
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST: str = os.getenv("DB_HOST", "nj-cdb-3pwh2kz1.sql.tencentcdb.com")
|
||||
DB_PORT: int = int(os.getenv("DB_PORT", "20784"))
|
||||
DB_USER: str = os.getenv("DB_USER", "jiebanke")
|
||||
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "aiot741$12346")
|
||||
DB_NAME: str = os.getenv("DB_NAME", "jbkdata")
|
||||
|
||||
# 构建数据库URL
|
||||
@property
|
||||
def SQLALCHEMY_DATABASE_URI(self) -> str:
|
||||
return f"mysql+pymysql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
|
||||
# CORS配置
|
||||
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
||||
|
||||
@field_validator("BACKEND_CORS_ORIGINS", mode="before")
|
||||
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
|
||||
if isinstance(v, str) and not v.startswith("["):
|
||||
return [i.strip() for i in v.split(",")]
|
||||
elif isinstance(v, (list, str)):
|
||||
return v
|
||||
raise ValueError(v)
|
||||
|
||||
# 上传文件配置
|
||||
UPLOAD_DIR: str = "uploads"
|
||||
MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 10MB
|
||||
ALLOWED_EXTENSIONS: List[str] = ["jpg", "jpeg", "png", "gif", "webp"]
|
||||
|
||||
# 无数据库模式
|
||||
NO_DB_MODE: bool = os.getenv("NO_DB_MODE", "False").lower() == "true"
|
||||
|
||||
class Config:
|
||||
case_sensitive = True
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
46
fastapi-backend/app/core/security.py
Normal file
46
fastapi-backend/app/core/security.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
# 密码上下文
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
# 验证密码
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
# 获取密码哈希
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
# 创建访问令牌
|
||||
def create_access_token(
|
||||
subject: Union[str, Any], expires_delta: Optional[timedelta] = None
|
||||
) -> str:
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode = {"exp": expire, "sub": str(subject)}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
# 创建刷新令牌
|
||||
def create_refresh_token(
|
||||
subject: Union[str, Any], expires_delta: Optional[timedelta] = None
|
||||
) -> str:
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode = {"exp": expire, "sub": str(subject), "type": "refresh"}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
287
fastapi-backend/app/crud/user.py
Normal file
287
fastapi-backend/app/crud/user.py
Normal file
@@ -0,0 +1,287 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional, Union, List
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, or_
|
||||
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
from app.models.user import User, Admin
|
||||
from app.schemas.user import UserCreate, UserUpdate, AdminCreate, AdminUpdate
|
||||
|
||||
|
||||
# 用户CRUD操作
|
||||
class CRUDUser:
|
||||
# 根据ID获取用户
|
||||
def get(self, db: Session, user_id: int) -> Optional[User]:
|
||||
return db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
# 根据用户名获取用户
|
||||
def get_by_username(self, db: Session, username: str) -> Optional[User]:
|
||||
return db.query(User).filter(User.username == username).first()
|
||||
|
||||
# 根据邮箱获取用户
|
||||
def get_by_email(self, db: Session, email: str) -> Optional[User]:
|
||||
return db.query(User).filter(User.email == email).first()
|
||||
|
||||
# 根据手机号获取用户
|
||||
def get_by_phone(self, db: Session, phone: str) -> Optional[User]:
|
||||
return db.query(User).filter(User.phone == phone).first()
|
||||
|
||||
# 根据微信OpenID获取用户
|
||||
def get_by_wechat_openid(self, db: Session, openid: str) -> Optional[User]:
|
||||
return db.query(User).filter(User.wechat_openid == openid).first()
|
||||
|
||||
# 创建用户
|
||||
def create(self, db: Session, obj_in: UserCreate) -> User:
|
||||
db_obj = User(
|
||||
username=obj_in.username,
|
||||
password_hash=get_password_hash(obj_in.password),
|
||||
user_type=obj_in.user_type,
|
||||
real_name=obj_in.real_name or obj_in.username,
|
||||
nickname=obj_in.nickname or obj_in.username,
|
||||
email=obj_in.email,
|
||||
phone=obj_in.phone,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
# 更新用户
|
||||
def update(
|
||||
self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
|
||||
) -> User:
|
||||
if isinstance(obj_in, dict):
|
||||
update_data = obj_in
|
||||
else:
|
||||
update_data = obj_in.dict(exclude_unset=True)
|
||||
|
||||
# 更新时间
|
||||
update_data["updated_at"] = datetime.now()
|
||||
|
||||
for field in update_data:
|
||||
if field in update_data:
|
||||
setattr(db_obj, field, update_data[field])
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
# 更新用户密码
|
||||
def update_password(self, db: Session, db_obj: User, new_password: str) -> User:
|
||||
db_obj.password_hash = get_password_hash(new_password)
|
||||
db_obj.updated_at = datetime.now()
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
# 更新用户最后登录时间
|
||||
def update_last_login(self, db: Session, db_obj: User) -> User:
|
||||
db_obj.last_login = datetime.now()
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
# 验证用户密码
|
||||
def authenticate(self, db: Session, username: str, password: str) -> Optional[User]:
|
||||
user = self.get_by_username(db, username)
|
||||
if not user:
|
||||
user = self.get_by_email(db, username)
|
||||
if not user:
|
||||
user = self.get_by_phone(db, username)
|
||||
if not user:
|
||||
return None
|
||||
if not verify_password(password, user.password_hash):
|
||||
return None
|
||||
return user
|
||||
|
||||
# 检查用户是否活跃
|
||||
def is_active(self, user: User) -> bool:
|
||||
return user.status == "active"
|
||||
|
||||
# 获取用户列表(带分页)
|
||||
def get_multi(
|
||||
self, db: Session, *, skip: int = 0, limit: int = 100,
|
||||
user_type: Optional[str] = None, status: Optional[str] = None,
|
||||
keyword: Optional[str] = None
|
||||
) -> List[User]:
|
||||
query = db.query(User)
|
||||
|
||||
# 应用过滤条件
|
||||
if user_type:
|
||||
query = query.filter(User.user_type == user_type)
|
||||
if status:
|
||||
query = query.filter(User.status == status)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
or_(
|
||||
User.username.like(f"%{keyword}%"),
|
||||
User.real_name.like(f"%{keyword}%"),
|
||||
User.nickname.like(f"%{keyword}%"),
|
||||
User.email.like(f"%{keyword}%"),
|
||||
User.phone.like(f"%{keyword}%")
|
||||
)
|
||||
)
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
# 获取用户总数
|
||||
def count(
|
||||
self, db: Session, *, user_type: Optional[str] = None,
|
||||
status: Optional[str] = None, keyword: Optional[str] = None
|
||||
) -> int:
|
||||
query = db.query(func.count(User.id))
|
||||
|
||||
# 应用过滤条件
|
||||
if user_type:
|
||||
query = query.filter(User.user_type == user_type)
|
||||
if status:
|
||||
query = query.filter(User.status == status)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
or_(
|
||||
User.username.like(f"%{keyword}%"),
|
||||
User.real_name.like(f"%{keyword}%"),
|
||||
User.nickname.like(f"%{keyword}%"),
|
||||
User.email.like(f"%{keyword}%"),
|
||||
User.phone.like(f"%{keyword}%")
|
||||
)
|
||||
)
|
||||
|
||||
return query.scalar()
|
||||
|
||||
# 批量更新用户状态
|
||||
def batch_update_status(
|
||||
self, db: Session, user_ids: List[int], status: str
|
||||
) -> int:
|
||||
result = db.query(User).filter(User.id.in_(user_ids)).update(
|
||||
{"status": status, "updated_at": datetime.now()},
|
||||
synchronize_session=False
|
||||
)
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
# 删除用户
|
||||
def remove(self, db: Session, *, user_id: int) -> Optional[User]:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user:
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
return user
|
||||
|
||||
# 获取用户统计信息
|
||||
def get_statistics(self, db: Session) -> Dict[str, Any]:
|
||||
total_users = db.query(func.count(User.id)).scalar()
|
||||
farmers = db.query(func.count(User.id)).filter(User.user_type == "farmer").scalar()
|
||||
merchants = db.query(func.count(User.id)).filter(User.user_type == "merchant").scalar()
|
||||
admins = db.query(func.count(User.id)).filter(
|
||||
or_(User.user_type == "admin", User.user_type == "super_admin")
|
||||
).scalar()
|
||||
active_users = db.query(func.count(User.id)).filter(User.status == "active").scalar()
|
||||
inactive_users = db.query(func.count(User.id)).filter(User.status == "inactive").scalar()
|
||||
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"farmers": farmers,
|
||||
"merchants": merchants,
|
||||
"admins": admins,
|
||||
"active_users": active_users,
|
||||
"inactive_users": inactive_users,
|
||||
"date": datetime.now()
|
||||
}
|
||||
|
||||
|
||||
# 管理员CRUD操作
|
||||
class CRUDAdmin:
|
||||
# 根据ID获取管理员
|
||||
def get(self, db: Session, admin_id: int) -> Optional[Admin]:
|
||||
return db.query(Admin).filter(Admin.id == admin_id).first()
|
||||
|
||||
# 根据用户名获取管理员
|
||||
def get_by_username(self, db: Session, username: str) -> Optional[Admin]:
|
||||
return db.query(Admin).filter(Admin.username == username).first()
|
||||
|
||||
# 创建管理员
|
||||
def create(self, db: Session, obj_in: AdminCreate) -> Admin:
|
||||
db_obj = Admin(
|
||||
username=obj_in.username,
|
||||
password=get_password_hash(obj_in.password),
|
||||
email=obj_in.email,
|
||||
nickname=obj_in.nickname or obj_in.username,
|
||||
avatar=obj_in.avatar,
|
||||
role=obj_in.role,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
# 更新管理员
|
||||
def update(
|
||||
self, db: Session, *, db_obj: Admin, obj_in: Union[AdminUpdate, Dict[str, Any]]
|
||||
) -> Admin:
|
||||
if isinstance(obj_in, dict):
|
||||
update_data = obj_in
|
||||
else:
|
||||
update_data = obj_in.dict(exclude_unset=True)
|
||||
|
||||
# 更新时间
|
||||
update_data["updated_at"] = datetime.now()
|
||||
|
||||
for field in update_data:
|
||||
if field in update_data:
|
||||
setattr(db_obj, field, update_data[field])
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
# 更新管理员密码
|
||||
def update_password(self, db: Session, db_obj: Admin, new_password: str) -> Admin:
|
||||
db_obj.password = get_password_hash(new_password)
|
||||
db_obj.updated_at = datetime.now()
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
# 更新管理员最后登录时间
|
||||
def update_last_login(self, db: Session, db_obj: Admin) -> Admin:
|
||||
db_obj.last_login = datetime.now()
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
# 验证管理员密码
|
||||
def authenticate(self, db: Session, username: str, password: str) -> Optional[Admin]:
|
||||
admin = self.get_by_username(db, username)
|
||||
if not admin:
|
||||
return None
|
||||
if not verify_password(password, admin.password):
|
||||
return None
|
||||
return admin
|
||||
|
||||
# 检查管理员是否活跃
|
||||
def is_active(self, admin: Admin) -> bool:
|
||||
return admin.status == "active"
|
||||
|
||||
# 删除管理员
|
||||
def remove(self, db: Session, *, admin_id: int) -> Optional[Admin]:
|
||||
admin = db.query(Admin).filter(Admin.id == admin_id).first()
|
||||
if admin:
|
||||
db.delete(admin)
|
||||
db.commit()
|
||||
return admin
|
||||
|
||||
|
||||
# 实例化CRUD对象
|
||||
user = CRUDUser()
|
||||
admin = CRUDAdmin()
|
||||
29
fastapi-backend/app/db/session.py
Normal file
29
fastapi-backend/app/db/session.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
# 创建数据库引擎
|
||||
engine = create_engine(
|
||||
settings.SQLALCHEMY_DATABASE_URI,
|
||||
pool_pre_ping=True,
|
||||
pool_recycle=3600,
|
||||
pool_size=20,
|
||||
max_overflow=0,
|
||||
echo=settings.DEBUG
|
||||
)
|
||||
|
||||
# 创建会话工厂
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# 创建基础模型类
|
||||
Base = declarative_base()
|
||||
|
||||
# 获取数据库会话
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
44
fastapi-backend/app/models/user.py
Normal file
44
fastapi-backend/app/models/user.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Boolean, Column, String, Integer, DateTime, Enum
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
user_type = Column(Enum('farmer', 'merchant', 'admin', 'super_admin'), default='farmer')
|
||||
real_name = Column(String(50))
|
||||
nickname = Column(String(50))
|
||||
avatar_url = Column(String(255))
|
||||
email = Column(String(100), unique=True, index=True)
|
||||
phone = Column(String(20), unique=True, index=True)
|
||||
gender = Column(Enum('male', 'female', 'other'), default='other')
|
||||
birthday = Column(DateTime)
|
||||
status = Column(Enum('active', 'inactive'), default='active')
|
||||
wechat_openid = Column(String(100), unique=True, index=True)
|
||||
wechat_unionid = Column(String(100), unique=True, index=True)
|
||||
level = Column(Integer, default=1)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
last_login = Column(DateTime)
|
||||
|
||||
|
||||
class Admin(Base):
|
||||
__tablename__ = "admins"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||
password = Column(String(255), nullable=False)
|
||||
email = Column(String(100), unique=True, index=True)
|
||||
nickname = Column(String(50))
|
||||
avatar = Column(String(255))
|
||||
role = Column(Enum('admin', 'super_admin'), default='admin')
|
||||
status = Column(Enum('active', 'inactive'), default='active')
|
||||
last_login = Column(DateTime)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
167
fastapi-backend/app/schemas/user.py
Normal file
167
fastapi-backend/app/schemas/user.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, EmailStr, Field, validator
|
||||
|
||||
|
||||
# 用户基础模式
|
||||
class UserBase(BaseModel):
|
||||
username: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = None
|
||||
user_type: Optional[str] = None
|
||||
real_name: Optional[str] = None
|
||||
nickname: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
gender: Optional[str] = None
|
||||
birthday: Optional[datetime] = None
|
||||
|
||||
|
||||
# 创建用户请求模式
|
||||
class UserCreate(BaseModel):
|
||||
username: str = Field(..., min_length=3, max_length=50)
|
||||
password: str = Field(..., min_length=6)
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = None
|
||||
real_name: Optional[str] = None
|
||||
nickname: Optional[str] = None
|
||||
user_type: str = "farmer"
|
||||
|
||||
|
||||
# 更新用户请求模式
|
||||
class UserUpdate(BaseModel):
|
||||
nickname: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
gender: Optional[str] = None
|
||||
birthday: Optional[datetime] = None
|
||||
email: Optional[EmailStr] = None
|
||||
phone: Optional[str] = None
|
||||
|
||||
|
||||
# 用户登录请求模式
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
# 密码更改请求模式
|
||||
class PasswordChange(BaseModel):
|
||||
current_password: str
|
||||
new_password: str = Field(..., min_length=6)
|
||||
|
||||
|
||||
# 用户响应模式
|
||||
class UserResponse(UserBase):
|
||||
id: int
|
||||
status: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
last_login: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# 带令牌的用户响应模式
|
||||
class UserWithToken(BaseModel):
|
||||
user: UserResponse
|
||||
token: str
|
||||
refresh_token: Optional[str] = None
|
||||
|
||||
|
||||
# 令牌响应模式
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
# 令牌数据模式
|
||||
class TokenPayload(BaseModel):
|
||||
sub: Optional[int] = None
|
||||
exp: Optional[int] = None
|
||||
type: Optional[str] = None
|
||||
|
||||
|
||||
# 管理员基础模式
|
||||
class AdminBase(BaseModel):
|
||||
username: str
|
||||
email: Optional[EmailStr] = None
|
||||
nickname: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
role: str = "admin"
|
||||
|
||||
|
||||
# 创建管理员请求模式
|
||||
class AdminCreate(AdminBase):
|
||||
password: str = Field(..., min_length=6)
|
||||
|
||||
|
||||
# 更新管理员请求模式
|
||||
class AdminUpdate(BaseModel):
|
||||
nickname: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
role: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
# 管理员响应模式
|
||||
class AdminResponse(AdminBase):
|
||||
id: int
|
||||
status: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
last_login: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
# 带令牌的管理员响应模式
|
||||
class AdminWithToken(BaseModel):
|
||||
admin: AdminResponse
|
||||
token: str
|
||||
refresh_token: Optional[str] = None
|
||||
|
||||
|
||||
# 微信登录请求模式
|
||||
class WechatLogin(BaseModel):
|
||||
code: str
|
||||
user_info: Optional[dict] = None
|
||||
|
||||
|
||||
# 分页响应模式
|
||||
class PaginationResponse(BaseModel):
|
||||
page: int
|
||||
page_size: int
|
||||
total: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
# 用户列表响应模式
|
||||
class UserListResponse(BaseModel):
|
||||
users: List[UserResponse]
|
||||
pagination: PaginationResponse
|
||||
|
||||
|
||||
# 用户统计响应模式
|
||||
class UserStatistics(BaseModel):
|
||||
total_users: int
|
||||
farmers: int
|
||||
merchants: int
|
||||
admins: int
|
||||
active_users: int
|
||||
inactive_users: int
|
||||
date: Optional[datetime] = None
|
||||
|
||||
|
||||
# 批量更新用户状态请求模式
|
||||
class BatchUserStatusUpdate(BaseModel):
|
||||
user_ids: List[int]
|
||||
status: str
|
||||
|
||||
@validator("status")
|
||||
def validate_status(cls, v):
|
||||
if v not in ["active", "inactive"]:
|
||||
raise ValueError("状态必须是 'active' 或 'inactive'")
|
||||
return v
|
||||
59
fastapi-backend/app/utils/errors.py
Normal file
59
fastapi-backend/app/utils/errors.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
class AppError(HTTPException):
|
||||
"""
|
||||
应用程序自定义错误类
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
detail: str,
|
||||
status_code: int = status.HTTP_400_BAD_REQUEST,
|
||||
headers: dict = None
|
||||
):
|
||||
super().__init__(status_code=status_code, detail=detail, headers=headers)
|
||||
|
||||
|
||||
# 常用错误
|
||||
class NotFoundError(AppError):
|
||||
"""
|
||||
资源未找到错误
|
||||
"""
|
||||
def __init__(self, detail: str = "资源未找到"):
|
||||
super().__init__(detail=detail, status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class UnauthorizedError(AppError):
|
||||
"""
|
||||
未授权错误
|
||||
"""
|
||||
def __init__(self, detail: str = "未授权"):
|
||||
super().__init__(
|
||||
detail=detail,
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
headers={"WWW-Authenticate": "Bearer"}
|
||||
)
|
||||
|
||||
|
||||
class ForbiddenError(AppError):
|
||||
"""
|
||||
禁止访问错误
|
||||
"""
|
||||
def __init__(self, detail: str = "禁止访问"):
|
||||
super().__init__(detail=detail, status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
class BadRequestError(AppError):
|
||||
"""
|
||||
请求参数错误
|
||||
"""
|
||||
def __init__(self, detail: str = "请求参数错误"):
|
||||
super().__init__(detail=detail, status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ConflictError(AppError):
|
||||
"""
|
||||
资源冲突错误
|
||||
"""
|
||||
def __init__(self, detail: str = "资源冲突"):
|
||||
super().__init__(detail=detail, status_code=status.HTTP_409_CONFLICT)
|
||||
43
fastapi-backend/app/utils/response.py
Normal file
43
fastapi-backend/app/utils/response.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
def success_response(
|
||||
data: Any = None,
|
||||
message: Optional[str] = None,
|
||||
code: int = 200
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
标准成功响应格式
|
||||
"""
|
||||
response = {
|
||||
"success": True,
|
||||
"code": code
|
||||
}
|
||||
|
||||
if data is not None:
|
||||
response["data"] = data
|
||||
|
||||
if message:
|
||||
response["message"] = message
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def error_response(
|
||||
message: str,
|
||||
code: int = 400,
|
||||
data: Any = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
标准错误响应格式
|
||||
"""
|
||||
response = {
|
||||
"success": False,
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
|
||||
if data is not None:
|
||||
response["data"] = data
|
||||
|
||||
return response
|
||||
15
fastapi-backend/main.py
Normal file
15
fastapi-backend/main.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import uvicorn
|
||||
from app.core.config import settings
|
||||
from app.api.api import app as application
|
||||
|
||||
# 导出应用实例
|
||||
app = application
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"app.api.api:app",
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
reload=settings.DEBUG,
|
||||
log_level="info"
|
||||
)
|
||||
74
fastapi-backend/migrations/env.py
Normal file
74
fastapi-backend/migrations/env.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# 导入模型
|
||||
from app.models.user import Base
|
||||
|
||||
# 导入配置
|
||||
from app.core.config import settings
|
||||
|
||||
# 这是Alembic Config对象,它提供对.ini文件中值的访问
|
||||
config = context.config
|
||||
|
||||
# 解释配置文件并设置日志记录器
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# 添加你的模型元数据对象
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# 其他值来自config,可以通过以下方式定义:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... 等等。
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""在'offline'模式下运行迁移。
|
||||
|
||||
这配置了上下文,只需要一个URL,并且不要求引擎可用。
|
||||
跳过引擎创建,甚至不需要DBAPI可用。
|
||||
|
||||
调用context.execute()来执行迁移。
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""在'online'模式下运行迁移。
|
||||
|
||||
在这种情况下,我们创建了一个Engine并将其与迁移上下文关联。
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
16
fastapi-backend/requirements.txt
Normal file
16
fastapi-backend/requirements.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
fastapi==0.110.0
|
||||
uvicorn==0.27.1
|
||||
pydantic==2.6.1
|
||||
pydantic-settings==2.1.0
|
||||
sqlalchemy==2.0.27
|
||||
pymysql==1.1.0
|
||||
cryptography==42.0.2
|
||||
python-jose==3.3.0
|
||||
passlib==1.7.4
|
||||
python-multipart==0.0.9
|
||||
email-validator==2.1.0
|
||||
python-dotenv==1.0.1
|
||||
alembic==1.13.1
|
||||
pytest==7.4.3
|
||||
httpx==0.26.0
|
||||
bcrypt==4.1.2
|
||||
Reference in New Issue
Block a user