""" app/services/user_admin_service.py — 管理员账号管理 service 仅 admin 可调(路由层 require_admin 守卫)。 建号/列号/启禁用,全部限定在调用者所在 workspace 内。 禁用=软删除(is_active=False),可随时恢复(倩倩姐2026-06-30拍板);不物理删除。 """ import logging from sqlalchemy import func from sqlalchemy.orm import Session from app.constants.enums import UserRole from app.core.response import raise_business, raise_not_found, raise_param_error from app.models.user import LoginRecord, User from app.models.workspace import WorkspaceMember from app.services.auth_service import DEFAULT_SEED_PASSWORD, hash_password logger = logging.getLogger(__name__) _VALID_ROLES = {r.value for r in UserRole} def list_workspace_users(db: Session, workspace_id: int) -> list[dict]: """列出本 workspace 的所有成员(含禁用),带角色与最后登录时间。""" rows = ( db.query(User, WorkspaceMember.role) .join(WorkspaceMember, WorkspaceMember.user_id == User.id) .filter(WorkspaceMember.workspace_id == workspace_id) .order_by(User.id) .all() ) result: list[dict] = [] for user, role in rows: last = ( db.query(func.max(LoginRecord.created_at)) .filter(LoginRecord.user_id == user.id) .scalar() ) # role 可能是 UserRole 枚举对象,统一取字符串值给前端匹配 ROLE_LABELS role_str = role.value if hasattr(role, "value") else str(role) result.append({ "id": user.id, "username": user.username, "email": user.email, "role": role_str, "is_active": bool(user.is_active), "must_change_password": bool(user.must_change_password), "last_login_at": last.isoformat() if last else None, }) return result def create_workspace_user( db: Session, workspace_id: int, username: str, email: str, role: str, init_password: str, ) -> dict: """管理员建号:建 User + 绑 WorkspaceMember,强制首登改密。""" username, email = username.strip(), email.strip().lower() if not username or not email: raise_param_error("用户名和邮箱不能为空") if role not in _VALID_ROLES: raise_param_error(f"角色非法,仅支持 {sorted(_VALID_ROLES)}") if len(init_password) < 8: raise_param_error("初始密码至少 8 位") if init_password == DEFAULT_SEED_PASSWORD: raise_param_error("初始密码不能使用系统默认密码") if db.query(User).filter(User.username == username).first(): raise_business("用户名已存在") if db.query(User).filter(User.email == email).first(): raise_business("邮箱已被占用") user = User( username=username, email=email, hashed_password=hash_password(init_password), is_active=True, must_change_password=True, ) db.add(user) db.flush() db.add(WorkspaceMember(workspace_id=workspace_id, user_id=user.id, role=role)) db.commit() logger.info("admin created user=%s role=%s in ws=%s", username, role, workspace_id) return {"id": user.id, "username": user.username, "role": role} def set_user_active( db: Session, workspace_id: int, target_user_id: int, is_active: bool, operator_user_id: int, ) -> dict: """启用/禁用账号(软删除)。禁止管理员禁用自己。""" if target_user_id == operator_user_id and not is_active: raise_business("不能禁用当前登录的管理员账号") member = ( db.query(WorkspaceMember) .filter( WorkspaceMember.workspace_id == workspace_id, WorkspaceMember.user_id == target_user_id, ) .first() ) if not member: raise_not_found("该账号不在当前 workspace") user = db.query(User).filter(User.id == target_user_id).first() if not user: raise_not_found("账号不存在") user.is_active = is_active db.commit() logger.info("admin set user=%s active=%s", target_user_id, is_active) return {"id": user.id, "is_active": is_active} def delete_workspace_user( db: Session, workspace_id: int, target_user_id: int, operator_user_id: int, ) -> dict: """物理删除账号:先清关联(login_record+workspace_member)再删 user。禁止删自己。""" if target_user_id == operator_user_id: raise_business("不能删除当前登录的管理员账号") member = ( db.query(WorkspaceMember) .filter( WorkspaceMember.workspace_id == workspace_id, WorkspaceMember.user_id == target_user_id, ) .first() ) if not member: raise_not_found("该账号不在当前 workspace") user = db.query(User).filter(User.id == target_user_id).first() if not user: raise_not_found("账号不存在") username = user.username db.query(LoginRecord).filter(LoginRecord.user_id == target_user_id).delete() db.query(WorkspaceMember).filter(WorkspaceMember.user_id == target_user_id).delete() db.delete(user) db.commit() logger.info("admin DELETED user=%s(%s) from ws=%s", target_user_id, username, workspace_id) return {"id": target_user_id, "deleted": True}