"""用户管理业务编排。 **事务边界在本层**:每个公开方法自行决定何时 commit。 repository 只 flush,事务由这里控制。 设计取舍: - 删除采用**物理删除**,与「删除即禁用」并不矛盾 —— 后者是对业务方的建议 (停用账号应走禁用而非删除,以保留审计线索),但需求 2.5.1 明确列了 「增删改查」,故保留删除能力;`operation_logs` 通过 SET NULL + 冗余 username 保证删除后仍可追溯 - 不允许删除或禁用**自己** —— 管理员误操作会立刻失去全部管理权限, 且没有其他账号可以救回来 """ import logging from sqlalchemy.orm import Session from app.core.constants import ROLE_ADMIN from app.core.exceptions import BadRequestError, ForbiddenError, NotFoundError from app.core.response import PageResult from app.core.security import hash_password from app.models import User from app.repositories import user_repository from app.schemas.user import RoleOut, UserCreateIn, UserOut, UserQuery, UserUpdateIn logger = logging.getLogger(__name__) def _to_out(user: User) -> UserOut: """ORM 对象转响应模型。 禁止把 ORM 对象直接当 response_model —— 表结构变更会直接泄漏到接口契约上。 """ return UserOut( id=user.id, username=user.username, real_name=user.real_name, phone=user.phone, email=user.email, status=user.status, roles=user_repository.get_role_codes(user), last_login_time=user.last_login_time, remark=user.remark, create_time=user.create_time, ) def list_users(session: Session, query: UserQuery) -> PageResult[UserOut]: """分页查询用户列表。""" users = user_repository.list_users( session, offset=query.offset, limit=query.page_size, username=query.username, real_name=query.real_name, status=query.status, role_code=query.role_code, ) total = user_repository.count_users( session, username=query.username, real_name=query.real_name, status=query.status, role_code=query.role_code, ) return PageResult[UserOut]( records=[_to_out(user) for user in users], total=total, page_num=query.page_num, page_size=query.page_size, ) def get_user(session: Session, user_id: int) -> UserOut: """查询用户详情。 :raises NotFoundError: 用户不存在 """ user = user_repository.get_by_id(session, user_id) if user is None: raise NotFoundError("用户不存在") return _to_out(user) def create_user(session: Session, payload: UserCreateIn) -> UserOut: """新增用户。 :raises BadRequestError: 用户名已存在,或角色编码无效 """ if user_repository.get_by_username(session, payload.username) is not None: raise BadRequestError(f"用户名 {payload.username!r} 已存在") roles = _resolve_roles(session, payload.roles) user = User( username=payload.username, password=hash_password(payload.password), real_name=payload.real_name, phone=payload.phone, email=payload.email, status=1, remark=payload.remark, ) user_repository.create(session, user) user_repository.replace_roles(session, user, roles) session.commit() logger.info("新增用户 | username=%s | roles=%s", user.username, payload.roles) return _to_out(user) def update_user( session: Session, user_id: int, payload: UserUpdateIn, *, operator_id: int ) -> UserOut: """编辑用户的基本信息与角色。 **不含用户名与密码**:用户名是审计追溯的锚点;密码走专门的重置接口, 以免"改资料"顺手改掉密码这类越权操作。 :raises NotFoundError: 用户不存在 :raises BadRequestError: 角色编码无效 """ user = user_repository.get_by_id(session, user_id) if user is None: raise NotFoundError("用户不存在") if payload.real_name is not None: user.real_name = payload.real_name if payload.phone is not None: user.phone = payload.phone if payload.email is not None: user.email = payload.email if payload.remark is not None: user.remark = payload.remark # 不允许把自己降级掉管理角色,否则改完立刻失去管理权限 if payload.roles is not None and user_id == operator_id and ROLE_ADMIN not in payload.roles: raise ForbiddenError("不能移除自己的管理员角色") session.flush() if payload.roles is not None: roles = _resolve_roles(session, payload.roles) user_repository.replace_roles(session, user, roles) session.commit() logger.info("编辑用户 | user_id=%s | roles=%s", user_id, payload.roles) return _to_out(user) def reset_password(session: Session, user_id: int, new_password: str) -> None: """重置用户密码。 同时把 `token_version` 加一,使该用户**已签发的令牌立即失效** —— 改密后旧令牌仍可用是常见疏漏,这里与「离职即失效」复用同一机制。 :raises NotFoundError: 用户不存在 """ user = user_repository.get_by_id(session, user_id) if user is None: raise NotFoundError("用户不存在") user.password = hash_password(new_password) user.token_version += 1 session.commit() logger.info("重置密码 | user_id=%s(令牌版本已提升)", user_id) def set_status(session: Session, user_id: int, status: int, *, operator_id: int) -> UserOut: """启用或禁用用户。 禁用时同样提升 `token_version`,使已签发令牌立即失效 —— 否则被禁用者仍可凭旧令牌访问直到过期(最长 2 小时)。 :raises NotFoundError: 用户不存在 :raises ForbiddenError: 试图禁用自己 """ user = user_repository.get_by_id(session, user_id) if user is None: raise NotFoundError("用户不存在") if status == 0 and user_id == operator_id: raise ForbiddenError("不能禁用自己") user.status = status if status == 0: user.token_version += 1 session.commit() logger.info("变更用户状态 | user_id=%s | status=%s", user_id, status) return _to_out(user) def delete_user(session: Session, user_id: int, *, operator_id: int) -> None: """删除用户。 物理删除。历史操作日志通过 `user_id` 置空 + 冗余 `username` 保留可追溯性。 :raises NotFoundError: 用户不存在 :raises ForbiddenError: 试图删除自己 """ user = user_repository.get_by_id(session, user_id) if user is None: raise NotFoundError("用户不存在") if user_id == operator_id: raise ForbiddenError("不能删除自己") username = user.username user_repository.delete(session, user) session.commit() logger.info("删除用户 | username=%s", username) def list_roles(session: Session) -> list[RoleOut]: """取全部启用角色,供前端下拉选择。""" return [RoleOut.model_validate(role) for role in user_repository.get_all_roles(session)] def _resolve_roles(session: Session, role_codes: list[str]) -> list: """把角色编码解析为角色对象,并校验全部存在。 :raises BadRequestError: 存在无效编码 """ if not role_codes: raise BadRequestError("至少需要指定一个角色") roles = user_repository.get_roles_by_codes(session, role_codes) if len(roles) != len(set(role_codes)): valid = {role.role_code for role in roles} invalid = sorted(set(role_codes) - valid) raise BadRequestError(f"角色编码无效:{invalid}") return list(roles)