chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 统一 Token 服务
|
||||
# =============================================================================
|
||||
# 说明:统一 Token 管理,支持以下功能:
|
||||
# 1. 创建统一格式的 Token(包含角色信息)
|
||||
# 2. 验证 Token 并获取用户信息
|
||||
# 3. 切换当前角色
|
||||
# 4. 兼容旧格式 Token(employee:token 和 agent:token)
|
||||
# =============================================================================
|
||||
|
||||
import json
|
||||
import logging
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Token TTL(8小时)
|
||||
TOKEN_TTL_SECONDS = 8 * 60 * 60
|
||||
|
||||
# 统一 Token Key 前缀
|
||||
UNIFIED_TOKEN_PREFIX = "user:token:"
|
||||
|
||||
# 旧格式 Token Key 前缀(兼容)
|
||||
EMPLOYEE_TOKEN_PREFIX = "employee:token:"
|
||||
AGENT_TOKEN_PREFIX = "agent:token:"
|
||||
|
||||
|
||||
class TokenService:
|
||||
"""统一 Token 服务。
|
||||
|
||||
管理用户 Token 的创建、验证、角色切换等操作。
|
||||
"""
|
||||
|
||||
def __init__(self, redis_client: aioredis.Redis):
|
||||
"""初始化 Token 服务。
|
||||
|
||||
Args:
|
||||
redis_client: Redis 异步客户端
|
||||
"""
|
||||
self.redis = redis_client
|
||||
|
||||
async def create_token(
|
||||
self,
|
||||
employee_id: str,
|
||||
name: str,
|
||||
roles: List[str],
|
||||
department: Optional[str] = None,
|
||||
avatar: Optional[str] = None,
|
||||
login_source: str = "portal",
|
||||
) -> str:
|
||||
"""创建统一格式的 Token。
|
||||
|
||||
Args:
|
||||
employee_id: 企微 UserID
|
||||
name: 用户姓名
|
||||
roles: 角色列表(如 ["user", "agent"])
|
||||
department: 部门(可选)
|
||||
avatar: 头像URL(可选)
|
||||
login_source: 登录来源(portal/agent/h5)
|
||||
|
||||
Returns:
|
||||
str: Token 字符串
|
||||
"""
|
||||
# 生成 Token
|
||||
token = secrets.token_urlsafe(32)
|
||||
|
||||
# 构建 Token 数据
|
||||
token_data = {
|
||||
"employee_id": employee_id,
|
||||
"name": name,
|
||||
"department": department or "",
|
||||
"avatar": avatar or "",
|
||||
"roles": roles,
|
||||
"current_role": self._get_default_role(roles),
|
||||
"login_source": login_source,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"last_active": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# 存入 Redis(统一格式)
|
||||
await self.redis.setex(
|
||||
f"{UNIFIED_TOKEN_PREFIX}{token}",
|
||||
TOKEN_TTL_SECONDS,
|
||||
json.dumps(token_data, ensure_ascii=False),
|
||||
)
|
||||
|
||||
# 同时存入旧格式(兼容性)
|
||||
if login_source == "agent":
|
||||
await self.redis.setex(
|
||||
f"{AGENT_TOKEN_PREFIX}{token}",
|
||||
TOKEN_TTL_SECONDS,
|
||||
employee_id,
|
||||
)
|
||||
else:
|
||||
await self.redis.setex(
|
||||
f"{EMPLOYEE_TOKEN_PREFIX}{token}",
|
||||
TOKEN_TTL_SECONDS,
|
||||
employee_id,
|
||||
)
|
||||
|
||||
logger.info(f"创建 Token: employee_id={employee_id}, roles={roles}, source={login_source}")
|
||||
|
||||
return token
|
||||
|
||||
async def get_user_info(self, token: str) -> Optional[Dict]:
|
||||
"""获取 Token 对应的用户信息。
|
||||
|
||||
支持新旧两种 Token 格式。
|
||||
|
||||
Args:
|
||||
token: Token 字符串
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: 用户信息字典,如果 Token 无效返回 None
|
||||
"""
|
||||
# 1. 尝试统一格式
|
||||
data = await self.redis.get(f"{UNIFIED_TOKEN_PREFIX}{token}")
|
||||
if data:
|
||||
try:
|
||||
user_info = json.loads(data)
|
||||
# 更新最后活跃时间
|
||||
user_info["last_active"] = datetime.now().isoformat()
|
||||
await self.redis.setex(
|
||||
f"{UNIFIED_TOKEN_PREFIX}{token}",
|
||||
TOKEN_TTL_SECONDS,
|
||||
json.dumps(user_info, ensure_ascii=False),
|
||||
)
|
||||
return user_info
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Token 数据解析失败: {token[:10]}...")
|
||||
return None
|
||||
|
||||
# 2. 尝试旧格式(employee:token)
|
||||
employee_id = await self.redis.get(f"{EMPLOYEE_TOKEN_PREFIX}{token}")
|
||||
if employee_id:
|
||||
employee_id = employee_id.decode("utf-8") if isinstance(employee_id, bytes) else employee_id
|
||||
# 获取员工信息缓存
|
||||
info_data = await self.redis.get(f"employee:info:{employee_id}")
|
||||
if info_data:
|
||||
try:
|
||||
info = json.loads(info_data)
|
||||
return {
|
||||
"employee_id": employee_id,
|
||||
"name": info.get("employee_name", ""),
|
||||
"department": info.get("department", ""),
|
||||
"avatar": info.get("avatar", ""),
|
||||
"roles": ["user"],
|
||||
"current_role": "user",
|
||||
"login_source": "h5",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"last_active": datetime.now().isoformat(),
|
||||
}
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 降级:只有 employee_id
|
||||
return {
|
||||
"employee_id": employee_id,
|
||||
"name": "",
|
||||
"department": "",
|
||||
"avatar": "",
|
||||
"roles": ["user"],
|
||||
"current_role": "user",
|
||||
"login_source": "h5",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"last_active": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# 3. 尝试旧格式(agent:token)
|
||||
agent_id = await self.redis.get(f"{AGENT_TOKEN_PREFIX}{token}")
|
||||
if agent_id:
|
||||
agent_id = agent_id.decode("utf-8") if isinstance(agent_id, bytes) else agent_id
|
||||
return {
|
||||
"employee_id": agent_id,
|
||||
"name": "",
|
||||
"department": "",
|
||||
"avatar": "",
|
||||
"roles": ["user", "agent"],
|
||||
"current_role": "agent",
|
||||
"login_source": "agent",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"last_active": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
async def switch_role(self, token: str, new_role: str) -> bool:
|
||||
"""切换当前角色。
|
||||
|
||||
Args:
|
||||
token: Token 字符串
|
||||
new_role: 目标角色标识
|
||||
|
||||
Returns:
|
||||
bool: 是否切换成功
|
||||
"""
|
||||
# 获取当前用户信息
|
||||
user_info = await self.get_user_info(token)
|
||||
if not user_info:
|
||||
return False
|
||||
|
||||
# 验证用户是否有目标角色
|
||||
if new_role not in user_info.get("roles", []):
|
||||
logger.warning(f"用户 {user_info['employee_id']} 没有 {new_role} 角色")
|
||||
return False
|
||||
|
||||
# 更新当前角色
|
||||
user_info["current_role"] = new_role
|
||||
user_info["last_active"] = datetime.now().isoformat()
|
||||
|
||||
# 保存到 Redis(统一格式)
|
||||
await self.redis.setex(
|
||||
f"{UNIFIED_TOKEN_PREFIX}{token}",
|
||||
TOKEN_TTL_SECONDS,
|
||||
json.dumps(user_info, ensure_ascii=False),
|
||||
)
|
||||
|
||||
# 同时更新旧格式(如果存在)
|
||||
# 注意:旧格式只存储 employee_id,不需要更新
|
||||
|
||||
logger.info(f"用户 {user_info['employee_id']} 切换角色到 {new_role}")
|
||||
return True
|
||||
|
||||
async def invalidate_token(self, token: str) -> None:
|
||||
"""使 Token 失效。
|
||||
|
||||
删除统一格式和旧格式的 Token。
|
||||
|
||||
Args:
|
||||
token: Token 字符串
|
||||
"""
|
||||
# 删除统一格式
|
||||
await self.redis.delete(f"{UNIFIED_TOKEN_PREFIX}{token}")
|
||||
|
||||
# 删除旧格式
|
||||
await self.redis.delete(f"{EMPLOYEE_TOKEN_PREFIX}{token}")
|
||||
await self.redis.delete(f"{AGENT_TOKEN_PREFIX}{token}")
|
||||
|
||||
logger.info(f"Token 已失效: {token[:10]}...")
|
||||
|
||||
def _get_default_role(self, roles: List[str]) -> str:
|
||||
"""获取默认角色。
|
||||
|
||||
优先级:admin > agent > user
|
||||
|
||||
Args:
|
||||
roles: 角色列表
|
||||
|
||||
Returns:
|
||||
str: 默认角色标识
|
||||
"""
|
||||
if "admin" in roles:
|
||||
return "admin"
|
||||
elif "agent" in roles:
|
||||
return "agent"
|
||||
else:
|
||||
return "user"
|
||||
Reference in New Issue
Block a user