dingdanquanliucheng/backend/app/services/system_service.py

653 lines
28 KiB
Python
Raw Normal View History

from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
from backend.app.core.security import hash_password
from backend.app.repositories.system_repository import SystemRepository
from backend.app.services.audit_service import audit_service
class SystemService:
VALID_MENU_TYPES = {"catalog", "page", "button"}
def __init__(self) -> None:
self.repository = SystemRepository()
def list_users(self, session: Session | None = None, filters: dict | None = None) -> dict:
if session is not None:
try:
rows = self.repository.list_users(session, filters or {})
return {
"total": len(rows),
"page_no": 1,
"page_size": len(rows) or 20,
"list": [self._build_user_row(user, role) for user, role in rows],
}
except SQLAlchemyError:
pass
return {
"total": 1,
"page_no": 1,
"page_size": 20,
"list": [
{
"user_id": 1,
"username": "sales01",
"real_name": "张三",
"mobile": "13800000000",
"role_id": 2,
"role_name": "业务员",
"role_code": "salesman",
"status": 1,
"created_at": "2026-05-14 10:00:00",
"updated_at": "2026-05-14 10:00:00",
}
],
}
def create_user(self, payload: dict, session: Session | None = None) -> dict:
self._validate_create_user(payload)
if session is not None:
try:
existed = self.repository.get_user_by_username(session, payload["username"])
if existed is not None:
raise AppException(code=ErrorCode.DUPLICATE, message="用户名已存在", status_code=400)
role = self.repository.get_role(session, payload["role_id"])
if role is None or role.status != 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在或已停用", status_code=404)
user = self.repository.create_user(
session,
{
**payload,
"password_hash": hash_password(payload["password"]),
},
)
audit_service.write_log(
session,
{
"operate_type": "system_user_create",
"biz_type": "sys_user",
"biz_id": user.id,
"before_value": None,
"after_value": self._build_user_row(user, role),
"remark": f"新增用户 {user.username}",
},
)
session.commit()
return {
"user_id": user.id,
"username": user.username,
"real_name": user.real_name,
"mobile": user.mobile,
"role_id": user.role_id,
"status": user.status,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {
"user_id": 2,
"username": payload["username"],
"real_name": payload["real_name"],
"mobile": payload.get("mobile") or "",
"role_id": payload["role_id"],
"status": payload.get("status", 1),
}
def update_user(self, user_id: int, payload: dict, session: Session | None = None) -> dict:
self._validate_update_user(payload)
if session is not None:
try:
user = self.repository.get_user(session, user_id)
if user is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="用户不存在", status_code=404)
role = self.repository.get_role(session, payload["role_id"])
if role is None or role.status != 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在或已停用", status_code=404)
before_snapshot = self._build_user_row(user, self.repository.get_role(session, user.role_id))
self.repository.update_user(user, payload)
audit_service.write_log(
session,
{
"operate_type": "system_user_update",
"biz_type": "sys_user",
"biz_id": user.id,
"before_value": before_snapshot,
"after_value": self._build_user_row(user, role),
"remark": f"更新用户 {user.username}",
},
)
session.commit()
return {"user_id": user_id, "updated": True}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"user_id": user_id, "updated": True}
def reset_password(self, user_id: int, new_password: str, session: Session | None = None) -> dict:
self._validate_password(new_password)
if session is not None:
try:
user = self.repository.get_user(session, user_id)
if user is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="用户不存在", status_code=404)
before_hash = user.password_hash
self.repository.reset_password(user, hash_password(new_password))
audit_service.write_log(
session,
{
"operate_type": "system_user_reset_password",
"biz_type": "sys_user",
"biz_id": user.id,
"before_value": {"password_hash": before_hash},
"after_value": {"password_hash": user.password_hash},
"remark": f"重置用户密码 {user.username}",
},
)
session.commit()
return {"user_id": user_id, "reset": True}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"user_id": user_id, "reset": True}
2026-05-28 22:59:04 +08:00
def change_user_status(self, user_id: int, status: int, session: Session | None = None, current_user: dict | None = None) -> dict:
if status not in (0, 1):
raise AppException(code=ErrorCode.PARAM_ERROR, message="状态值必须为 0 或 1", status_code=400)
if session is not None:
try:
user = self.repository.get_user(session, user_id)
if user is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="用户不存在", status_code=404)
if current_user and current_user.get("user_id") == user_id:
raise AppException(code=ErrorCode.PARAM_ERROR, message="不能修改自己的状态", status_code=400)
if status == 0:
role = self.repository.get_role(session, user.role_id)
if role and role.role_code == "admin":
from sqlalchemy import func, select
from backend.app.models.system import SysUser
active_admin_count = session.execute(
select(func.count()).select_from(SysUser).where(
SysUser.role_id == user.role_id,
SysUser.status == 1,
SysUser.deleted == 0,
SysUser.id != user_id,
)
).scalar() or 0
if active_admin_count == 0:
raise AppException(code=ErrorCode.PARAM_ERROR, message="不能停用最后一个管理员", status_code=400)
before_status = user.status
user.status = status
session.add(user)
audit_service.write_log(
session,
{
"operate_type": "system_user_status_change",
"biz_type": "sys_user",
"biz_id": user.id,
"before_value": {"status": before_status},
"after_value": {"status": status},
"remark": f"{'启用' if status == 1 else '停用'}用户 {user.username}",
},
)
session.commit()
return {"user_id": user_id, "status": status}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"user_id": user_id, "status": status}
def list_roles(self, session: Session | None = None, filters: dict | None = None) -> dict:
if session is not None:
try:
roles = self.repository.list_roles(session, filters or {})
return {
"total": len(roles),
"page_no": 1,
"page_size": len(roles) or 20,
"list": [
{
"role_id": role.id,
"role_name": role.role_name,
"role_code": role.role_code,
"status": role.status,
"remark": role.remark,
"created_at": role.created_at.strftime("%Y-%m-%d %H:%M:%S") if role.created_at else "",
}
for role in roles
],
}
except SQLAlchemyError:
pass
return {
"total": 2,
"page_no": 1,
"page_size": 20,
"list": [
{"role_id": 1, "role_name": "管理员", "role_code": "admin", "status": 1, "remark": "系统管理"},
{"role_id": 2, "role_name": "业务员", "role_code": "salesman", "status": 1, "remark": "业务录单"},
],
}
def create_role(self, payload: dict, session: Session | None = None) -> dict:
self._validate_create_role(payload)
if session is not None:
try:
existed = self.repository.get_role_by_code(session, payload["role_code"])
if existed is not None:
raise AppException(code=ErrorCode.DUPLICATE, message="角色编码已存在", status_code=400)
role = self.repository.create_role(session, payload)
audit_service.write_log(
session,
{
"operate_type": "system_role_create",
"biz_type": "sys_role",
"biz_id": role.id,
"before_value": None,
"after_value": self._build_role_row(role),
"remark": f"新增角色 {role.role_code}",
},
)
session.commit()
return {
"role_id": role.id,
"role_name": role.role_name,
"role_code": role.role_code,
"status": role.status,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {
"role_id": 3,
"role_name": payload["role_name"],
"role_code": payload["role_code"],
"status": payload.get("status", 1),
}
def update_role(self, role_id: int, payload: dict, session: Session | None = None) -> dict:
self._validate_update_role(payload)
if session is not None:
try:
role = self.repository.get_role(session, role_id)
if role is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在", status_code=404)
if role.role_code == "admin" and payload.get("status", 1) != 1:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="管理员角色不能停用", status_code=400)
if payload.get("status", 1) != 1 and self.repository.count_active_users_by_role(session, role_id) > 0:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="当前角色下仍有启用用户", status_code=400)
before_snapshot = self._build_role_row(role)
self.repository.update_role(role, payload)
audit_service.write_log(
session,
{
"operate_type": "system_role_update",
"biz_type": "sys_role",
"biz_id": role.id,
"before_value": before_snapshot,
"after_value": self._build_role_row(role),
"remark": f"更新角色 {role.role_code}",
},
)
session.commit()
return {"role_id": role_id, "updated": True}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"role_id": role_id, "updated": True}
def get_role_menu_assignment(self, role_id: int, session: Session | None = None) -> dict:
if session is not None:
try:
relations = self.repository.list_role_menu_relations(session, role_id)
menu_ids = [item.menu_id for item in relations]
menus = self.repository.list_menus_by_ids(session, menu_ids)
return {
"role_id": role_id,
"menu_ids": menu_ids,
"permission_codes": [menu.permission_code for menu in menus if menu.permission_code],
}
except SQLAlchemyError:
pass
return {
"role_id": role_id,
"menu_ids": [],
"permission_codes": [],
}
def assign_role_menus(self, role_id: int, payload: dict, session: Session | None = None) -> dict:
if session is not None:
try:
role = self.repository.get_role(session, role_id)
if role is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在", status_code=404)
before_assignment = self.get_role_menu_assignment(role_id, session)
menu_ids = self._resolve_assign_menu_ids(session, payload)
self._validate_assign_role_menus(session, role.role_code, menu_ids)
self.repository.replace_role_menu_relations(session, role_id, menu_ids)
audit_service.write_log(
session,
{
"operate_type": "system_role_assign_menus",
"biz_type": "sys_role",
"biz_id": role.id,
"before_value": before_assignment,
"after_value": {
"role_id": role_id,
"menu_ids": menu_ids,
"permission_codes": payload.get("permission_codes", []),
},
"remark": f"更新角色授权 {role.role_code}",
},
)
session.commit()
return {
"role_id": role_id,
"menu_count": len(menu_ids),
"permission_count": len(payload.get("permission_codes", [])),
"updated": True,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
menu_ids = payload.get("menu_ids", [])
return {
"role_id": role_id,
"menu_count": len(menu_ids),
"permission_count": len(payload.get("permission_codes", [])),
"updated": True,
}
def list_menus(self, session: Session | None = None, filters: dict | None = None) -> list[dict]:
if session is not None:
try:
menus = self.repository.list_menus(session, filters or {})
if menus:
return self._build_menu_tree(
[
{
"menu_id": menu.id,
"parent_id": menu.parent_id,
"menu_name": menu.menu_name,
"menu_path": menu.menu_path,
"menu_type": menu.menu_type,
"permission_code": menu.permission_code,
"icon": menu.icon,
"sort_no": menu.sort_no,
"status": menu.status,
}
for menu in menus
]
)
except SQLAlchemyError:
pass
return [
{
"menu_id": 1,
"parent_id": 0,
"menu_name": "订单管理",
"menu_path": "/orders",
"menu_type": "page",
"permission_code": "order:list",
"icon": "order",
"sort_no": 10,
"status": 1,
"children": [],
}
]
def create_menu(self, payload: dict, session: Session | None = None) -> dict:
self._validate_menu_payload(payload)
if session is not None:
try:
self._validate_menu_parent(session, payload["parent_id"], None)
existed = self.repository.get_menu_by_permission_code(session, payload["permission_code"])
if existed is not None:
raise AppException(code=ErrorCode.DUPLICATE, message="权限编码已存在", status_code=400)
menu = self.repository.create_menu(session, payload)
audit_service.write_log(
session,
{
"operate_type": "system_menu_create",
"biz_type": "sys_menu",
"biz_id": menu.id,
"before_value": None,
"after_value": self._build_menu_row(menu),
"remark": f"新增菜单 {menu.permission_code}",
},
)
session.commit()
return {
"menu_id": menu.id,
"menu_name": menu.menu_name,
"permission_code": menu.permission_code,
"status": menu.status,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {
"menu_id": 2,
"menu_name": payload["menu_name"],
"permission_code": payload["permission_code"],
"status": payload.get("status", 1),
}
def update_menu(self, menu_id: int, payload: dict, session: Session | None = None) -> dict:
self._validate_menu_payload(payload)
if session is not None:
try:
menu = self.repository.get_menu(session, menu_id)
if menu is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="菜单不存在", status_code=404)
self._validate_menu_parent(session, payload["parent_id"], menu_id)
existed = self.repository.get_menu_by_permission_code(session, payload["permission_code"])
if existed is not None and existed.id != menu_id:
raise AppException(code=ErrorCode.DUPLICATE, message="权限编码已存在", status_code=400)
before_snapshot = self._build_menu_row(menu)
self.repository.update_menu(menu, payload)
audit_service.write_log(
session,
{
"operate_type": "system_menu_update",
"biz_type": "sys_menu",
"biz_id": menu.id,
"before_value": before_snapshot,
"after_value": self._build_menu_row(menu),
"remark": f"更新菜单 {menu.permission_code}",
},
)
session.commit()
return {"menu_id": menu_id, "updated": True}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {"menu_id": menu_id, "updated": True}
def _build_user_row(self, user: object, role: object | None) -> dict:
return {
"user_id": user.id,
"username": user.username,
"real_name": user.real_name,
"mobile": user.mobile,
"role_id": user.role_id,
"role_name": role.role_name if role else "",
"role_code": role.role_code if role else "",
"status": user.status,
"created_at": user.created_at.strftime("%Y-%m-%d %H:%M:%S") if user.created_at else "",
"updated_at": user.updated_at.strftime("%Y-%m-%d %H:%M:%S") if user.updated_at else "",
}
def _build_menu_tree(self, menu_list: list[dict]) -> list[dict]:
nodes = {item["menu_id"]: {**item, "children": []} for item in menu_list}
roots: list[dict] = []
for item in nodes.values():
parent_id = item["parent_id"]
if parent_id and parent_id in nodes:
nodes[parent_id]["children"].append(item)
else:
roots.append(item)
return roots
def _build_role_row(self, role: object) -> dict:
return {
"role_id": role.id,
"role_name": role.role_name,
"role_code": role.role_code,
"status": role.status,
"remark": role.remark,
"created_at": role.created_at.strftime("%Y-%m-%d %H:%M:%S") if getattr(role, "created_at", None) else "",
}
def _build_menu_row(self, menu: object) -> dict:
return {
"menu_id": menu.id,
"parent_id": menu.parent_id,
"menu_name": menu.menu_name,
"menu_path": menu.menu_path,
"menu_type": menu.menu_type,
"permission_code": menu.permission_code,
"icon": menu.icon,
"sort_no": menu.sort_no,
"status": menu.status,
}
def _validate_create_user(self, payload: dict) -> None:
if not payload["username"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="用户名不能为空", status_code=400)
self._validate_password(payload["password"])
self._validate_update_user(payload)
def _validate_update_user(self, payload: dict) -> None:
if not payload["real_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="姓名不能为空", status_code=400)
if not payload.get("role_id"):
raise AppException(code=ErrorCode.PARAM_ERROR, message="角色不能为空", status_code=400)
def _validate_password(self, password: str) -> None:
if len(password.strip()) < 6:
raise AppException(code=ErrorCode.PARAM_ERROR, message="密码长度不能少于6位", status_code=400)
def _validate_create_role(self, payload: dict) -> None:
if not payload["role_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="角色名称不能为空", status_code=400)
if not payload["role_code"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="角色编码不能为空", status_code=400)
def _validate_update_role(self, payload: dict) -> None:
if not payload["role_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="角色名称不能为空", status_code=400)
def _validate_menu_payload(self, payload: dict) -> None:
if not payload["menu_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="菜单名称不能为空", status_code=400)
if not payload["permission_code"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="权限编码不能为空", status_code=400)
if payload["menu_type"] not in self.VALID_MENU_TYPES:
raise AppException(code=ErrorCode.PARAM_ERROR, message="菜单类型不合法", status_code=400)
def _validate_menu_parent(self, session: Session, parent_id: int, menu_id: int | None) -> None:
if not parent_id:
return
if menu_id is not None and parent_id == menu_id:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="父级菜单不能是自身", status_code=400)
parent_menu = self.repository.get_menu(session, parent_id)
if parent_menu is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="父级菜单不存在", status_code=404)
if menu_id is not None:
self._ensure_menu_parent_not_cyclic(session, parent_menu.id, menu_id)
def _ensure_menu_parent_not_cyclic(self, session: Session, parent_id: int, menu_id: int) -> None:
current_parent_id = parent_id
visited_ids: set[int] = set()
# 沿父级链一路向上检查,既要拦住“挂到自己的子级上”,也要拦住异常数据导致的循环链路。
while current_parent_id:
if current_parent_id == menu_id:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="父级菜单不能设置为自身或子级", status_code=400)
if current_parent_id in visited_ids:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="菜单层级存在循环引用", status_code=400)
visited_ids.add(current_parent_id)
current_menu = self.repository.get_menu(session, current_parent_id)
if current_menu is None:
break
current_parent_id = current_menu.parent_id
def _resolve_assign_menu_ids(self, session: Session, payload: dict) -> list[int]:
menu_ids = {int(menu_id) for menu_id in payload.get("menu_ids", [])}
permission_codes = {item.strip() for item in payload.get("permission_codes", []) if item.strip()}
if permission_codes:
menus = self.repository.list_menus(session, {"status": 1})
for menu in menus:
if menu.permission_code in permission_codes:
menu_ids.add(menu.id)
return sorted(menu_ids)
def _validate_assign_role_menus(self, session: Session, role_code: str, menu_ids: list[int]) -> None:
menus = self.repository.list_menus_by_ids(session, menu_ids)
if len(menus) != len(menu_ids):
raise AppException(code=ErrorCode.NOT_FOUND, message="菜单不存在", status_code=404)
for menu in menus:
if menu.status != 1:
raise AppException(code=ErrorCode.NOT_FOUND, message="菜单不存在或已停用", status_code=404)
if role_code == "admin" and not menu_ids:
raise AppException(code=ErrorCode.BUSINESS_RULE_FAILED, message="管理员角色至少保留一个权限菜单", status_code=400)
system_service = SystemService()