dingdanquanliucheng/backend/app/services/auth_service.py

245 lines
10 KiB
Python
Raw Normal View History

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
2026-05-14 13:51:06 +08:00
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",
2026-05-19 14:48:47 +08:00
"logistics:task:list",
"logistics:task:create",
"logistics:trace:list",
"logistics:trace:create",
"report:performance:view",
"report:performance:export",
"ai:recognize",
"ai:correct",
],
"driver": [
"driver:task:list",
"driver:task:accept",
"driver:task:pickup",
"driver:task:deliver",
"logistics:trace:list",
],
"admin": [
"system:view",
"system:user:list",
"system:user:create",
"system:user:update",
"system:user:reset-password",
"system:role:list",
"system:role:create",
"system:role:update",
"system:role:assign-menus",
"system:menu:list",
"system:menu:create",
"system:menu:update",
"report:performance:view",
"report:performance:export",
2026-05-19 14:48:47 +08:00
"logistics:task:list",
"logistics:task:create",
"logistics:trace:list",
"logistics:trace:create",
"ai:recognize",
"ai:correct",
"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
2026-05-14 13:51:06 +08:00
return demo_store.login(username, role_type)
def get_me(self, token: str, session: Session | None = None) -> dict | None:
2026-05-19 10:37:06 +08:00
if self._is_token_revoked(token):
return None
if session is not None:
try:
payload = decode_access_token(token)
2026-05-19 10:37:06 +08:00
if payload is not 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
2026-05-14 13:51:06 +08:00
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]
default_permissions = self.DEFAULT_ROLE_PERMISSIONS.get(role_code, [])
merged = list(dict.fromkeys([*default_permissions, *permissions]))
return merged or default_permissions
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)
2026-05-14 13:51:06 +08:00
auth_service = AuthService()