Files
beige/backend/app/services/user_admin_service.py
yangqianqian df1856d793 上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事
- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段);
  修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品
- dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开;
  settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch
- 新增用户管理(users API/admin service/改密页) + alembic 022/023/024
- 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example
- A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:08:13 +08:00

138 lines
5.2 KiB
Python
Raw 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.
"""
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}