上线版: 产品表单统一+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:
yangqianqian
2026-06-30 18:08:13 +08:00
parent a77212781c
commit df1856d793
150 changed files with 8616 additions and 1765 deletions

View 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}