import json from datetime import datetime, timezone from pathlib import Path from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session from backend.app.core.config import get_settings from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException from backend.app.core.security import create_access_token, decode_access_token, verify_password from backend.app.repositories.system_repository import SystemRepository from backend.app.services.demo_store import demo_store class AuthService: DEFAULT_ROLE_MENUS = { "salesman": [{"menu_name": "我的订单", "menu_path": "/orders"}], "manager": [{"menu_name": "审批中心", "menu_path": "/pages/approve-list"}], "driver": [{"menu_name": "我的任务", "menu_path": "/pages/task-list"}], "admin": [{"menu_name": "后台首页", "menu_path": "/dashboard"}], } DEFAULT_ROLE_PERMISSIONS = { "salesman": ["order:create", "order:list", "order:submit", "customer:list"], "manager": ["order:list", "order:approve", "order:cancel-approve", "logistics:task:create"], "driver": ["driver:task:list", "driver:task:accept", "driver:task:pickup", "driver:task:deliver"], "admin": ["system:user:list", "system:role:list", "system:menu:list", "order:list"], } def __init__(self) -> None: self.repository = SystemRepository() self.settings = get_settings() self.token_store_path = Path(self.settings.auth_token_store_path) self.token_store_path.parent.mkdir(parents=True, exist_ok=True) self.revoked_tokens = self._load_revoked_tokens() def login(self, username: str, password: str, role_type: str, session: Session | None = None) -> dict | None: if session is not None: try: user = self.repository.get_user_by_username(session, username) if user is None or not verify_password(password, user.password_hash): raise AppException( code=ErrorCode.UNAUTHORIZED, message="账号不存在或密码错误", status_code=401, ) role = self.repository.get_role(session, user.role_id) if role is None: raise AppException(code=ErrorCode.NOT_FOUND, message="角色不存在", status_code=404) if user.status != 1: raise AppException(code=ErrorCode.FORBIDDEN, message="账号已停用", status_code=403) if role.status != 1 or role.role_code != role_type: raise AppException(code=ErrorCode.FORBIDDEN, message="登录角色与账号不匹配", status_code=403) return self._build_profile(session, user, role, include_token=True) except AppException: raise except SQLAlchemyError: pass return demo_store.login(username, role_type) def get_me(self, token: str, session: Session | None = None) -> dict | None: if session is not None: try: if self._is_token_revoked(token): return None payload = decode_access_token(token) if payload is None: return None user_id = int(payload["user_id"]) role_code = str(payload["role_code"]) user = self.repository.get_user(session, user_id) if user is None or user.status != 1: return None role = self.repository.get_role(session, user.role_id) if role is None or role.status != 1 or role.role_code != role_code: return None return self._build_profile(session, user, role, include_token=False) except (SQLAlchemyError, ValueError, KeyError, TypeError): pass return demo_store.get_me(token) def logout(self, token: str) -> dict: if token: # token 失效需要跨进程持久化,避免服务重启后已退出的 token 又重新可用。 self._revoke_token(token) return {"success": True} def _build_profile(self, session: Session, user, role, include_token: bool) -> dict: menus = self._load_role_menus(session, role.role_code) permissions = self._load_role_permissions(session, role.role_code) return { "user_id": user.id, "username": user.username, "real_name": user.real_name, "mobile": user.mobile, "role_id": role.id, "role_name": role.role_name, "role_code": role.role_code, "token": create_access_token({"user_id": user.id, "role_code": role.role_code}) if include_token else None, "menus": menus, "permissions": permissions, } def _load_role_menus(self, session: Session, role_code: str) -> list[dict]: menus = self.repository.list_menus(session, {"status": 1}) if not menus: return self.DEFAULT_ROLE_MENUS.get(role_code, []) if role_code == "admin": return [{"menu_name": menu.menu_name, "menu_path": menu.menu_path} for menu in menus] role = self.repository.get_role_by_code(session, role_code) if role is not None: relations = self.repository.list_role_menu_relations(session, role.id) if relations: allowed_menu_ids = {item.menu_id for item in relations} filtered = [ {"menu_name": menu.menu_name, "menu_path": menu.menu_path} for menu in menus if menu.id in allowed_menu_ids and menu.menu_path ] if filtered: return filtered allowed_paths = {item["menu_path"] for item in self.DEFAULT_ROLE_MENUS.get(role_code, [])} filtered = [ {"menu_name": menu.menu_name, "menu_path": menu.menu_path} for menu in menus if menu.menu_path in allowed_paths ] return filtered or self.DEFAULT_ROLE_MENUS.get(role_code, []) def _load_role_permissions(self, session: Session, role_code: str) -> list[str]: menus = self.repository.list_menus(session, {"status": 1}) if not menus: return self.DEFAULT_ROLE_PERMISSIONS.get(role_code, []) if role_code == "admin": permissions = [menu.permission_code for menu in menus if menu.permission_code] return permissions or self.DEFAULT_ROLE_PERMISSIONS.get(role_code, []) role = self.repository.get_role_by_code(session, role_code) if role is not None: relations = self.repository.list_role_menu_relations(session, role.id) if relations: allowed_menu_ids = {item.menu_id for item in relations} permissions = [ menu.permission_code for menu in menus if menu.id in allowed_menu_ids and menu.permission_code ] if permissions: return permissions allowed = set(self.DEFAULT_ROLE_PERMISSIONS.get(role_code, [])) permissions = [menu.permission_code for menu in menus if menu.permission_code in allowed] return permissions or self.DEFAULT_ROLE_PERMISSIONS.get(role_code, []) def _load_revoked_tokens(self) -> dict[str, int]: if not self.token_store_path.exists(): return {} try: payload = json.loads(self.token_store_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return {} now_ts = int(datetime.now(timezone.utc).timestamp()) tokens = { token: int(exp) for token, exp in payload.items() if isinstance(token, str) and isinstance(exp, int) and exp > now_ts } if len(tokens) != len(payload): self._save_revoked_tokens(tokens) return tokens def _save_revoked_tokens(self, tokens: dict[str, int]) -> None: self.token_store_path.write_text(json.dumps(tokens, ensure_ascii=False, indent=2), encoding="utf-8") def _is_token_revoked(self, token: str) -> bool: if not token: return False now_ts = int(datetime.now(timezone.utc).timestamp()) expired_tokens = [item for item, exp in self.revoked_tokens.items() if exp <= now_ts] if expired_tokens: for item in expired_tokens: self.revoked_tokens.pop(item, None) self._save_revoked_tokens(self.revoked_tokens) return token in self.revoked_tokens def _revoke_token(self, token: str) -> None: payload = decode_access_token(token) expire_at = int(payload.get("exp", 0)) if payload else int(datetime.now(timezone.utc).timestamp()) + 60 self.revoked_tokens[token] = expire_at self._save_revoked_tokens(self.revoked_tokens) auth_service = AuthService()