WIP-CHECKPOINT[auth-refactor]: 固化工程师崩溃前部分成果 + 同树其他未提交WIP(仅源码,不含密钥/二进制)-- 待重激活工程师续作
This commit is contained in:
@@ -20,6 +20,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.agent import Agent
|
||||
from app.models.conversation import Conversation
|
||||
from app.services.avatar_service import clean_avatar_url
|
||||
from app.services.wecom_service import WecomService
|
||||
from app.utils.response import (
|
||||
AppException,
|
||||
@@ -260,6 +261,66 @@ class SessionService:
|
||||
|
||||
return conversation
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 自动分配空闲坐席(排队系统核心)
|
||||
# --------------------------------------------------------------------------
|
||||
async def auto_assign_agent(
|
||||
self, conversation_id: UUID
|
||||
) -> Optional[Agent]:
|
||||
"""自动分配空闲坐席。
|
||||
|
||||
查找当前负载最低的空闲坐席进行分配。
|
||||
|
||||
流程:
|
||||
1. 查询所有状态为online且未满负荷的坐席
|
||||
2. 按current_load升序排列(负载最低的优先)
|
||||
3. 分配给负载最低的坐席
|
||||
4. 更新会话状态为serving
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
|
||||
Returns:
|
||||
Agent: 分配成功的坐席对象;None表示无空闲坐席
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
from app.models.agent import Agent
|
||||
|
||||
# 1. 查询空闲坐席(在线且未满负荷)
|
||||
stmt = select(Agent).where(
|
||||
Agent.status == "online",
|
||||
Agent.current_load < Agent.max_load
|
||||
).order_by(Agent.current_load.asc())
|
||||
|
||||
result = await self.db.execute(stmt)
|
||||
agents = result.scalars().all()
|
||||
|
||||
if not agents:
|
||||
logger.info(f"无空闲坐席: conv_id={conversation_id}")
|
||||
return None
|
||||
|
||||
# 2. 选择负载最低的坐席
|
||||
agent = agents[0]
|
||||
|
||||
# 3. 分配坐席
|
||||
conversation = await self._get_conversation(conversation_id)
|
||||
conversation.status = "serving"
|
||||
conversation.assigned_agent_id = agent.user_id
|
||||
conversation.updated_at = datetime.now()
|
||||
self.db.add(conversation)
|
||||
|
||||
# 4. 更新坐席负载
|
||||
agent.current_load += 1
|
||||
self.db.add(agent)
|
||||
|
||||
await self.db.flush()
|
||||
|
||||
logger.info(
|
||||
f"自动分配坐席: conv_id={conversation_id}, agent={agent.user_id}, load={agent.current_load}/{agent.max_load}"
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 结单
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -822,8 +883,8 @@ class SessionService:
|
||||
# 邀请功能(P0-09~P0-11):坐席邀请员工/部门加入会话
|
||||
# ======================================================================
|
||||
|
||||
# 头像缓存 TTL:7天
|
||||
AVATAR_CACHE_TTL = 7 * 24 * 60 * 60
|
||||
# 头像缓存 TTL:1天(要求 B:原 7 天过长,长期缓存过期/失效 URL 导致前端裂图)
|
||||
AVATAR_CACHE_TTL = 1 * 24 * 60 * 60
|
||||
|
||||
async def _get_employee_avatar(self, employee_id: str) -> str:
|
||||
"""获取员工头像URL(带Redis缓存)。
|
||||
@@ -831,7 +892,11 @@ class SessionService:
|
||||
优先级:
|
||||
1. Redis 缓存(最快)
|
||||
2. employees 表
|
||||
3. 企微API(获取后存入Redis缓存)
|
||||
3. 企微API(获取后存入Redis缓存 + 回写 DB 稳定 URL)
|
||||
|
||||
头像 URL 稳定性处理(要求 B):
|
||||
- 缓存 TTL 由 7 天缩短为 1 天,避免长期缓存过期/失效 URL。
|
||||
- 返回前清理企微头像 URL 的多余查询参数,保留稳定部分,降低 404 概率。
|
||||
|
||||
Args:
|
||||
employee_id: 企微员工UserID
|
||||
@@ -846,14 +911,16 @@ class SessionService:
|
||||
try:
|
||||
cached_avatar = await self.redis_client.get(cache_key)
|
||||
if cached_avatar:
|
||||
logger.debug(f"从Redis缓存获取头像: employee_id={employee_id}")
|
||||
return cached_avatar.decode("utf-8") if isinstance(cached_avatar, bytes) else cached_avatar
|
||||
# 兼容历史缓存中可能带查询参数,统一清理后返回
|
||||
return clean_avatar_url(
|
||||
cached_avatar.decode("utf-8") if isinstance(cached_avatar, bytes) else cached_avatar
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"从Redis获取头像缓存失败: employee_id={employee_id}, error={e}")
|
||||
|
||||
# 2. 从 employees 表获取(需要匹配 corp_id)
|
||||
from app.models.employee import Employee
|
||||
from app.core.config import settings
|
||||
from app.config import settings
|
||||
result = await self.db.execute(
|
||||
select(Employee.avatar).where(
|
||||
Employee.employee_id == employee_id,
|
||||
@@ -862,14 +929,15 @@ class SessionService:
|
||||
)
|
||||
row = result.first()
|
||||
if row and row[0]:
|
||||
logger.info(f"从employees表获取头像: employee_id={employee_id}, avatar={row[0][:50]}...")
|
||||
# 存入 Redis 缓存
|
||||
cleaned = clean_avatar_url(row[0])
|
||||
logger.info(f"从employees表获取头像: employee_id={employee_id}, avatar={cleaned[:50]}...")
|
||||
# 存入 Redis 缓存(已清理的稳定 URL)
|
||||
if self.redis_client:
|
||||
try:
|
||||
await self.redis_client.setex(cache_key, self.AVATAR_CACHE_TTL, row[0])
|
||||
await self.redis_client.setex(cache_key, self.AVATAR_CACHE_TTL, cleaned)
|
||||
except Exception as e:
|
||||
logger.warning(f"存入Redis头像缓存失败: employee_id={employee_id}, error={e}")
|
||||
return row[0]
|
||||
return cleaned
|
||||
else:
|
||||
logger.info(f"employees表无头像记录: employee_id={employee_id}")
|
||||
|
||||
@@ -878,7 +946,8 @@ class SessionService:
|
||||
if self.wecom_service:
|
||||
try:
|
||||
user_info = await self.wecom_service.get_user_info(employee_id)
|
||||
avatar = user_info.get("avatar", "")
|
||||
raw = user_info.get("avatar", "")
|
||||
avatar = clean_avatar_url(raw)
|
||||
logger.info(f"企微API返回头像: employee_id={employee_id}, avatar={'有值(' + str(len(avatar)) + '字符)' if avatar else '空'}")
|
||||
# 存入 Redis 缓存(即使为空也缓存,避免频繁请求API)
|
||||
if self.redis_client and avatar:
|
||||
@@ -886,6 +955,21 @@ class SessionService:
|
||||
await self.redis_client.setex(cache_key, self.AVATAR_CACHE_TTL, avatar)
|
||||
except Exception as e:
|
||||
logger.warning(f"存入Redis头像缓存失败: employee_id={employee_id}, error={e}")
|
||||
# 回写 DB:把稳定 URL 落库,下次直接从 DB 读取,减少企微 API 调用
|
||||
if avatar:
|
||||
try:
|
||||
from sqlalchemy import update as sa_update
|
||||
await self.db.execute(
|
||||
sa_update(Employee)
|
||||
.where(
|
||||
Employee.employee_id == employee_id,
|
||||
Employee.corp_id == settings.wecom_corp_id,
|
||||
)
|
||||
.values(avatar=avatar, avatar_updated_at=datetime.utcnow())
|
||||
)
|
||||
await self.db.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"回写员工头像到DB失败: employee_id={employee_id}, error={e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"从企微API获取头像失败: employee_id={employee_id}, error={e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user