上线版: 产品表单统一+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>
This commit is contained in:
137
backend/app/services/user_admin_service.py
Normal file
137
backend/app/services/user_admin_service.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
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}
|
||||
Reference in New Issue
Block a user