Files
wecom_it_smart_desk/backend/app/api/agent_directory.py
T

163 lines
6.2 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — 坐席端员工目录 API
# =============================================================================
# 说明:为坐席端"邀请参与者"弹窗提供员工搜索和组织架构树接口
# 1. GET /agent/employees/search?keyword=xxx — 模糊搜索员工(姓名/工号)
# 2. GET /agent/org/tree — 获取组织架构树(部门→员工层级)
# 两个端点均需坐席认证(get_current_agent),复用 employee_directory 服务
#
# 路由说明:
# 后端路由不带 /api 前缀(nginx / Vite proxy 负责 strip /api),
# 实际对外 URL 为 /api/agent/employees/search 和 /api/agent/org/tree
# =============================================================================
import logging
from typing import Any, Dict, List, Optional
import redis.asyncio as aioredis
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.agents import get_current_agent
from app.database import get_db
from app.dependencies import dep_redis
from app.models.agent import Agent
from app.services.employee_directory import (
count_tree_employees,
get_org_directory,
get_org_tree_cached,
)
from app.utils.response import AppException, success_response
logger = logging.getLogger(__name__)
# 创建路由器(与 agents.py 一致:路由直接挂在根路径,不加额外 prefix)
router = APIRouter()
# --------------------------------------------------------------------------
# GET /agent/employees/search — 模糊搜索员工
# --------------------------------------------------------------------------
@router.get("/agent/employees/search")
async def search_employees(
keyword: str = Query("", description="搜索关键词(姓名或工号,模糊匹配)"),
current_agent: Agent = Depends(get_current_agent),
db: AsyncSession = Depends(get_db),
redis: Optional[aioredis.Redis] = Depends(dep_redis),
):
"""模糊搜索员工(按姓名或工号)。
复用 get_org_directory() 获取组织目录(优先企微全组织,降级本地 employees 表),
在内存中做大小写不敏感的模糊匹配。排除当前登录坐席自己。
Args:
keyword: 搜索关键词(为空时返回空列表,避免无意义全量返回)
current_agent: 当前坐席(通过 get_current_agent 认证依赖注入)
db: 数据库会话
redis: Redis 客户端(可选,用于企微目录缓存读取)
Returns:
Dict: 统一响应格式,data 为员工列表
[{ "id": "userid", "name": "姓名", "department": "部门" }, ...]
"""
# 去除首尾空格,空关键词直接返回空列表
kw = (keyword or "").strip()
if not kw:
return success_response(data=[])
try:
# 获取组织目录(含 30 分钟 Redis 缓存 + 本地降级)
directory, _ = await get_org_directory(db, redis)
kw_lower = kw.lower()
results: List[Dict[str, Any]] = []
for emp in directory:
# 排除当前坐席自己(避免邀请自己加入会话)
if emp.get("employee_id") == current_agent.user_id:
continue
name = emp.get("name", "") or ""
emp_id = emp.get("employee_id", "") or ""
# 模糊匹配:姓名 或 工号(employee_id)包含关键词(大小写不敏感)
if kw_lower in name.lower() or kw_lower in emp_id.lower():
results.append({
"id": emp_id,
"name": name,
"department": emp.get("department", ""),
})
logger.info(f"员工搜索: keyword='{kw}', 命中 {len(results)}")
return success_response(data=results)
except AppException:
# 业务异常直接抛出(如认证失败)
raise
except Exception as e:
logger.error(f"员工搜索异常: {e}", exc_info=True)
raise AppException(1005, f"搜索失败: {str(e)}")
# --------------------------------------------------------------------------
# GET /agent/org/tree — 获取组织架构树
# --------------------------------------------------------------------------
@router.get("/agent/org/tree")
async def get_org_tree(
current_agent: Agent = Depends(get_current_agent),
db: AsyncSession = Depends(get_db),
redis: Optional[aioredis.Redis] = Depends(dep_redis),
):
"""获取组织架构树(部门层级 + 每个部门下的员工列表)。
利用企微 department/list 返回的 parentid 字段构建真正的层级树,
不再按部门名扁平分组。部门ID加 ``dept_`` 前缀作为唯一 key
避免同名部门合并。员工可以出现在其所属的所有部门下。
树结构示例(多层级):
[
{
"id": "dept_1",
"label": "公司",
"dept_id": 1,
"parentid": 0,
"children": [
{
"id": "dept_2",
"label": "研发一部",
"dept_id": 2,
"parentid": 1,
"children": [
{"id": "zhangsan", "label": "张三", "isLeaf": true, "department": "研发一部"}
]
}
]
}
]
性能优化:
- 树构建结果独立缓存(key: wecom:org_tree:agentTTL 30 分钟)
- 缓存中包含所有员工,读取后过滤掉当前登录坐席自己
Args:
current_agent: 当前坐席
db: 数据库会话
redis: Redis 客户端
Returns:
Dict: 统一响应格式,data 为树节点列表
"""
try:
# 获取组织架构树(含独立缓存 + 排除当前坐席)
tree = await get_org_tree_cached(db, redis, "agent", current_agent.user_id)
total_employees = count_tree_employees(tree)
logger.info(f"组织架构树: {len(tree)} 个顶层节点, 共 {total_employees}")
return success_response(data=tree)
except AppException:
raise
except Exception as e:
logger.error(f"组织架构树获取异常: {e}", exc_info=True)
raise AppException(1005, f"获取组织架构树失败: {str(e)}")