feat(system): 实现用户管理接口
需求 2.5.1 用户账号管理,全部要求管理员权限(require_admin 依赖):
- 接口:列表/详情/新增/编辑/删除/重置密码/启停/角色下拉,
角色下拉放 /users/meta/roles 以避开 /{user_id} 的路由匹配冲突
- 删除、禁用、改角色三处自我保护:不允许对当前登录账号执行,
避免管理员误操作后立刻失去全部管理权限且无法自行恢复
- 重置密码与禁用均提升 token_version,使已签发令牌立即失效,
不依赖 Redis 黑名单
- 密码按字节数校验(bcrypt 5.0 硬上限 72 字节,中文一字 3 字节)
- 用户列表按 id 倒序:create_time 为秒级精度,同秒数据分页结果不确定
- 修复 replace_roles 未同步关系集合的缺陷:改用 user.user_roles
集合追加,此前 session.add(UserRole(...)) 会表现为保存成功但返回空角色
- update_user 的自我保护原先依赖返回 None 的占位函数,恒不生效,
改为显式传入 operator_id
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
ad5fe4a411
commit
e22c4bcdc9
7 files changed
+650
-4
No files matched your search
@@ -0,0 +1,7 @@
|
||||
"""系统管理路由(需求 2.5.1)。
|
||||
|
||||
含用户管理、角色查询、操作日志查询三部分,全部要求管理员权限。
|
||||
|
||||
与 `api/v1/auth.py` 的分工:`auth` 是登录链路(未认证即可访问的验证码与登录,
|
||||
以及任何登录用户都能访问的 `/me` 与登出);本包是**管理功能**。
|
||||
"""
|
||||
@@ -0,0 +1,131 @@
|
||||
"""用户管理接口(需求 2.5.1:用户账号管理)。
|
||||
|
||||
全部要求管理员权限 —— 按需求「系统管理员(全部权限)、普通用户(业务操作)」,
|
||||
用户管理属管理员专属。
|
||||
|
||||
删除、禁用、改角色三处都有**自我保护**:不允许对当前登录账号执行,
|
||||
否则管理员误操作会立刻失去全部管理权限且无法自行恢复。
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Path, Query
|
||||
|
||||
from app.api.deps import AdminUser, DbSession
|
||||
from app.core.response import ApiResponse, PageResult, ok
|
||||
from app.schemas.user import (
|
||||
PasswordResetIn,
|
||||
RoleOut,
|
||||
UserCreateIn,
|
||||
UserOut,
|
||||
UserQuery,
|
||||
UserStatusIn,
|
||||
UserUpdateIn,
|
||||
)
|
||||
from app.services import user_service
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["系统管理 · 用户"])
|
||||
|
||||
|
||||
@router.get("", response_model=ApiResponse[PageResult[UserOut]], summary="用户列表")
|
||||
def list_users(
|
||||
session: DbSession,
|
||||
_: AdminUser,
|
||||
query: Annotated[UserQuery, Query()],
|
||||
) -> ApiResponse[PageResult[UserOut]]:
|
||||
"""分页查询用户,支持按用户名/姓名模糊、状态与角色筛选。"""
|
||||
return ok(user_service.list_users(session, query))
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=ApiResponse[UserOut], summary="用户详情")
|
||||
def get_user(
|
||||
session: DbSession,
|
||||
_: AdminUser,
|
||||
user_id: Annotated[int, Path(ge=1)],
|
||||
) -> ApiResponse[UserOut]:
|
||||
"""查询用户详情。"""
|
||||
return ok(user_service.get_user(session, user_id))
|
||||
|
||||
|
||||
@router.post("", response_model=ApiResponse[UserOut], summary="新增用户")
|
||||
def create_user(
|
||||
session: DbSession,
|
||||
_: AdminUser,
|
||||
payload: UserCreateIn,
|
||||
) -> ApiResponse[UserOut]:
|
||||
"""新增用户。用户名须唯一,至少指定一个角色。"""
|
||||
return ok(user_service.create_user(session, payload), message="用户已创建")
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=ApiResponse[UserOut], summary="编辑用户")
|
||||
def update_user(
|
||||
session: DbSession,
|
||||
current_user: AdminUser,
|
||||
user_id: Annotated[int, Path(ge=1)],
|
||||
payload: UserUpdateIn,
|
||||
) -> ApiResponse[UserOut]:
|
||||
"""编辑用户的基本信息与角色。
|
||||
|
||||
**不含用户名与密码**:用户名是审计追溯的锚点;密码走专门的重置接口。
|
||||
"""
|
||||
return ok(
|
||||
user_service.update_user(session, user_id, payload, operator_id=current_user.id),
|
||||
message="已保存",
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{user_id}", response_model=ApiResponse[None], summary="删除用户")
|
||||
def delete_user(
|
||||
session: DbSession,
|
||||
current_user: AdminUser,
|
||||
user_id: Annotated[int, Path(ge=1)],
|
||||
) -> ApiResponse[None]:
|
||||
"""删除用户。
|
||||
|
||||
物理删除。历史操作日志通过 `user_id` 置空 + 冗余用户名保留可追溯性。
|
||||
业务上更推荐用「禁用」代替删除 —— 禁用保留完整审计线索。
|
||||
"""
|
||||
user_service.delete_user(session, user_id, operator_id=current_user.id)
|
||||
return ok(message="用户已删除")
|
||||
|
||||
|
||||
@router.put("/{user_id}/password", response_model=ApiResponse[None], summary="重置密码")
|
||||
def reset_password(
|
||||
session: DbSession,
|
||||
_: AdminUser,
|
||||
user_id: Annotated[int, Path(ge=1)],
|
||||
payload: PasswordResetIn,
|
||||
) -> ApiResponse[None]:
|
||||
"""重置用户密码。
|
||||
|
||||
会同时提升该用户的令牌版本,使其**已签发的令牌立即失效** ——
|
||||
否则改密后旧令牌在有效期内仍可用。
|
||||
"""
|
||||
user_service.reset_password(session, user_id, payload.new_password)
|
||||
return ok(message="密码已重置,该用户需重新登录")
|
||||
|
||||
|
||||
@router.put("/{user_id}/status", response_model=ApiResponse[UserOut], summary="启用/禁用用户")
|
||||
def set_status(
|
||||
session: DbSession,
|
||||
current_user: AdminUser,
|
||||
user_id: Annotated[int, Path(ge=1)],
|
||||
payload: UserStatusIn,
|
||||
) -> ApiResponse[UserOut]:
|
||||
"""启用或禁用用户。
|
||||
|
||||
禁用会提升令牌版本使已签发令牌立即失效,否则被禁用者仍可凭旧令牌
|
||||
继续访问直到过期(最长 2 小时)。
|
||||
"""
|
||||
result = user_service.set_status(session, user_id, payload.status, operator_id=current_user.id)
|
||||
return ok(result, message="已启用" if payload.status == 1 else "已禁用")
|
||||
|
||||
|
||||
@router.get("/meta/roles", response_model=ApiResponse[list[RoleOut]], summary="角色下拉选项")
|
||||
def list_roles(session: DbSession, _: AdminUser) -> ApiResponse[list[RoleOut]]:
|
||||
"""取全部启用角色,供用户表单的角色选择使用。
|
||||
|
||||
路径放在 `/users/meta/roles` 而非 `/roles`,是为了避免与
|
||||
`/{user_id}` 的路由匹配冲突(`roles` 会被当成 user_id 解析而报 422)。
|
||||
"""
|
||||
return ok(user_service.list_roles(session))
|
||||
Reference in new issue
Block a user