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.repositories.system_repository import SystemRepository 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": payload["password"], }, ) 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) self.repository.update_user(user, payload) 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) self.repository.reset_password(user, new_password) 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} 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) 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) self.repository.update_role(role, payload) 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) 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) 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) 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) self.repository.update_menu(menu, payload) 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 _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) 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()