feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
# =============================================================================
|
||||
# 企微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 collections import OrderedDict
|
||||
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 get_org_directory
|
||||
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:
|
||||
# 获取组织目录(含 10 分钟 Redis 缓存 + 本地降级,无需修改 employee_directory.py)
|
||||
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),
|
||||
):
|
||||
"""获取组织架构树(部门层级 + 每个部门下的员工列表)。
|
||||
|
||||
复用 get_org_directory() 获取员工列表(已含 department 字段),
|
||||
在服务端按 department 分组构建树结构。排除当前登录坐席自己。
|
||||
|
||||
树结构示例:
|
||||
[
|
||||
{
|
||||
"id": "研发一部",
|
||||
"label": "研发一部",
|
||||
"children": [
|
||||
{"id": "zhangsan", "label": "张三", "isLeaf": true, "department": "研发一部"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
规则:
|
||||
- department 为空的员工归到"未分配部门"分组
|
||||
- 企微返回多部门(逗号分隔)时,取第一个作为主部门
|
||||
- 部门按名称排序,部门内员工按姓名排序
|
||||
|
||||
Args:
|
||||
current_agent: 当前坐席
|
||||
db: 数据库会话
|
||||
redis: Redis 客户端
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,data 为树节点列表
|
||||
"""
|
||||
try:
|
||||
# 获取组织目录(含 Redis 缓存 + 本地降级)
|
||||
directory, _ = await get_org_directory(db, redis)
|
||||
|
||||
# 按部门分组(OrderedDict 保持稳定插入顺序,后续再排序)
|
||||
dept_groups: "OrderedDict[str, List[Dict[str, Any]]]" = OrderedDict()
|
||||
|
||||
for emp in directory:
|
||||
# 排除当前坐席自己
|
||||
if emp.get("employee_id") == current_agent.user_id:
|
||||
continue
|
||||
|
||||
# 取部门名:为空则归"未分配部门";多部门(逗号分隔)取第一个
|
||||
dept = (emp.get("department") or "").strip()
|
||||
if not dept:
|
||||
dept = "未分配部门"
|
||||
else:
|
||||
dept = dept.split(",")[0].strip()
|
||||
if not dept:
|
||||
dept = "未分配部门"
|
||||
|
||||
if dept not in dept_groups:
|
||||
dept_groups[dept] = []
|
||||
dept_groups[dept].append(emp)
|
||||
|
||||
# 构建树节点(部门按名称排序,员工按姓名排序)
|
||||
tree: List[Dict[str, Any]] = []
|
||||
for dept_name in sorted(dept_groups.keys()):
|
||||
employees = dept_groups[dept_name]
|
||||
if not employees:
|
||||
# 跳过空部门(理论上不会出现,防御性编程)
|
||||
continue
|
||||
|
||||
# 部门内员工按姓名排序
|
||||
employees.sort(key=lambda e: e.get("name", ""))
|
||||
|
||||
tree.append({
|
||||
"id": dept_name,
|
||||
"label": dept_name,
|
||||
"children": [
|
||||
{
|
||||
"id": emp.get("employee_id", ""),
|
||||
"label": emp.get("name", ""),
|
||||
# isLeaf=true 标记为叶子节点(员工),前端 el-tree 据此区分部门/员工
|
||||
"isLeaf": True,
|
||||
"department": dept_name,
|
||||
}
|
||||
for emp in employees
|
||||
],
|
||||
})
|
||||
|
||||
total_employees = sum(len(node["children"]) for node in 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)}")
|
||||
Reference in New Issue
Block a user