Files
deploy 6416fb4651 fix(admin): 修复注册设置PUT接口——由独立query参数改为接收JSON body
根因:PUT /admin/users/register-settings 参数定义为 phone_required: bool = None 等独立query参数,
但前端发送的是 JSON body {"phone_required":true,"email_required":true}。
FastAPI无法把JSON body映射到独立参数 → 全部收到None → 循环跳过 → DB无更新(提示成功但无效)。
修复:参数改为 body: dict 统一接收JSON,用 body.get() 取值更新 RegisterSetting 表。
2026-07-15 18:50:36 +08:00

326 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
用户管理 API(管理员)
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
from pydantic import BaseModel, Field
from typing import Optional
from ...database import get_db, SessionLocal
from ...models import User, RegisterSetting
from ...auth import get_current_admin, hash_password, verify_password
from ...middleware.log_service import LogService
from datetime import datetime
class AdminChangePasswordRequest(BaseModel):
old_password: str
new_password: str = Field(min_length=6, max_length=100)
class ResetUserPasswordRequest(BaseModel):
new_password: str = Field(min_length=6, max_length=100)
import uuid
from datetime import datetime
router = APIRouter()
@router.get("/")
async def get_users(
status: str = Query(None, description="用户状态筛选: active/banned"),
membership_level: int = Query(None, description="会员等级筛选"),
search: str = Query(None, description="搜索用户名或邮箱"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取用户列表(管理员)"""
query = db.query(User)
if status:
query = query.filter(User.status == status)
if membership_level is not None:
query = query.filter(User.membership_level == membership_level)
if search:
query = query.filter(
or_(
User.username.ilike(f"%{search}%"),
User.email.ilike(f"%{search}%"),
)
)
total = query.count()
users = query.order_by(User.created_at.desc()).offset(
(page - 1) * page_size
).limit(page_size).all()
return {
"total": total,
"page": page,
"page_size": page_size,
"items": [
{
"id": u.id,
"username": u.username,
"email": u.email,
"phone": u.phone,
"role": u.role,
"status": u.status,
"real_name_status": u.real_name_status,
"real_name": u.real_name,
"membership_level": u.membership_level,
"membership_expire_at": u.membership_expire_at.isoformat() if u.membership_expire_at else None,
"auto_renew": u.auto_renew,
"created_at": u.created_at.isoformat() if u.created_at else None,
"updated_at": u.updated_at.isoformat() if u.updated_at else None,
}
for u in users
],
}
# ============================================================
# 静态路由 - 必须放在 /{id} 动态路由之前
# ============================================================
@router.get("/register-settings")
async def get_register_settings(
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取注册设置"""
setting = db.query(RegisterSetting).first()
if not setting:
return {
"phone_required": True,
"phone_sms_required": True,
"email_required": True,
"email_verify_required": True,
"real_name_required": False,
"aliyun_access_key": "",
"aliyun_endpoint": "https://dysmsapi.aliyuncs.com",
"sms_sign_name": "",
"sms_template_code": "",
}
return {
"id": setting.id,
"phone_required": setting.phone_required,
"phone_sms_required": setting.phone_sms_required,
"email_required": setting.email_required,
"email_verify_required": setting.email_verify_required,
"real_name_required": setting.real_name_required,
"aliyun_access_key": setting.aliyun_access_key,
"aliyun_endpoint": setting.aliyun_endpoint,
"sms_sign_name": getattr(setting, 'sms_sign_name', ''),
"sms_template_code": getattr(setting, 'sms_template_code', ''),
}
@router.put("/register-settings")
async def update_register_settings(
body: dict,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""更新注册设置(接收 JSON Body)"""
setting = db.query(RegisterSetting).first()
if not setting:
setting = RegisterSetting(
id=str(uuid.uuid4()),
phone_required=body.get("phone_required", True),
phone_sms_required=body.get("phone_sms_required", True),
email_required=body.get("email_required", True),
email_verify_required=body.get("email_verify_required", True),
real_name_required=body.get("real_name_required", False),
aliyun_access_key=body.get("aliyun_access_key"),
aliyun_secret_key=body.get("aliyun_secret_key"),
aliyun_endpoint=body.get("aliyun_endpoint"),
sms_sign_name=body.get("sms_sign_name"),
sms_template_code=body.get("sms_template_code"),
)
db.add(setting)
else:
if "phone_required" in body:
setting.phone_required = body["phone_required"]
if "phone_sms_required" in body:
setting.phone_sms_required = body["phone_sms_required"]
if "email_required" in body:
setting.email_required = body["email_required"]
if "email_verify_required" in body:
setting.email_verify_required = body["email_verify_required"]
if "real_name_required" in body:
setting.real_name_required = body["real_name_required"]
if "aliyun_access_key" in body:
setting.aliyun_access_key = body["aliyun_access_key"]
if "aliyun_secret_key" in body:
setting.aliyun_secret_key = body["aliyun_secret_key"]
if "aliyun_endpoint" in body:
setting.aliyun_endpoint = body["aliyun_endpoint"]
if "sms_sign_name" in body:
setting.sms_sign_name = body["sms_sign_name"]
if "sms_template_code" in body:
setting.sms_template_code = body["sms_template_code"]
setting.updated_at = datetime.now()
db.commit()
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="update_register_settings",
target_type="register_setting",
detail="更新注册设置",
)
return {"message": "注册设置已更新"}
# ============================================================
# 动态路由
# ============================================================
@router.get("/{id}")
async def get_user_detail(
id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取用户详情"""
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return {
"id": user.id,
"username": user.username,
"email": user.email,
"phone": user.phone,
"avatar": user.avatar,
"role": user.role,
"status": user.status,
"real_name_status": user.real_name_status,
"real_name": user.real_name,
"id_card": user.id_card,
"membership_plan_id": user.membership_plan_id,
"membership_level": user.membership_level,
"membership_expire_at": user.membership_expire_at.isoformat() if user.membership_expire_at else None,
"auto_renew": user.auto_renew,
"created_at": user.created_at.isoformat() if user.created_at else None,
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
}
@router.put("/{id}/ban")
async def ban_user(
id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""封禁用户"""
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if user.role == "admin":
raise HTTPException(status_code=400, detail="不能封禁管理员账号")
user.status = "banned"
user.updated_at = datetime.now()
db.commit()
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="ban_user",
target_type="user",
target_id=id,
detail=f"封禁用户 {user.username}",
)
return {"message": "用户已封禁"}
@router.put("/{id}/unban")
async def unban_user(
id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""解封用户"""
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
user.status = "active"
user.updated_at = datetime.now()
db.commit()
LogService.log_audit(
admin_id=admin_user.id,
admin_name=admin_user.username,
action="unban_user",
target_type="user",
target_id=id,
detail=f"解封用户 {user.username}",
)
return {"message": "用户已解封"}
# ============================================================
# 密码管理
# ============================================================
@router.put("/change-password")
def admin_change_password(
req: AdminChangePasswordRequest,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""管理员修改自己的密码"""
db_user = db.query(User).filter(User.id == admin_user.id).first()
if not db_user:
raise HTTPException(status_code=404, detail="用户不存在")
if not verify_password(req.old_password, db_user.password_hash):
raise HTTPException(status_code=400, detail="原密码错误")
db_user.password_hash = hash_password(req.new_password)
db_user.updated_at = datetime.now()
LogService.log_operation(
user_id=admin_user.id, username=admin_user.username, action="admin_change_password",
target="user", target_id=admin_user.id,
detail="管理员修改了自己的密码",
)
db.commit()
return {"code": 0, "message": "密码修改成功"}
@router.put("/{id}/reset-password")
def admin_reset_user_password(
id: str,
req: ResetUserPasswordRequest,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""管理员重置用户密码"""
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
user.password_hash = hash_password(req.new_password)
user.updated_at = datetime.now()
LogService.log_operation(
user_id=admin_user.id, username=admin_user.username, action="reset_user_password",
target="user", target_id=id,
detail=f"管理员重置了用户 {user.username} 的密码",
)
db.commit()
return {"code": 0, "message": "密码已重置"}