# ============================================================================= # 企微IT智能服务台 — 企微 JS-SDK 签名 API (v0.5.4 应急页用) # ============================================================================= # 说明:提供前端 wx.config / wx.agentConfig 所需的鉴权签名。 # 对应企微文档:https://developer.work.weixin.qq.com/document/path/90506 # # 流程: # 1. 前端调 GET /api/wecom/jsapi-config?url=xxx 拿签名 # 2. 后端用 jsapi_ticket + url 算 sha1 签名 # 3. 前端用 wx.config({...}) 鉴权后,即可调企微 JS-SDK(如 wx.agentConfig) # # BC/DR 设计:不依赖 session/auth,公开访问(只返回签名,不返回敏感数据) # ============================================================================= import logging import secrets import time from fastapi import APIRouter, Query from app.config import settings from app.dependencies import get_shared_wecom_service from app.utils.response import AppException, success_response logger = logging.getLogger(__name__) router = APIRouter() @router.get("/wecom/jsapi-config") async def get_jsapi_config( url: str = Query(..., description="当前页面 URL(不含 # 及其后)"), ): """获取企微 JS-SDK 鉴权配置。 供前端 wx.config 和 wx.agentConfig 使用。 Returns: { "code": 0, "data": { "corp_id": "wwa8c87970b2011f41", "agent_id": "1000133", "timestamp": 1718500000, "nonce_str": "5K8264ILTKCH...", "signature": "f7c8e9..." } } """ try: wecom_service = get_shared_wecom_service() # 1. 获取 jsapi_ticket ticket = await wecom_service.get_jsapi_ticket() # 2. 生成时间戳和随机串 timestamp = int(time.time()) nonce_str = secrets.token_hex(8) # 16 字符 # 3. 计算签名 signature = wecom_service.generate_jsapi_signature( ticket=ticket, nonce_str=nonce_str, timestamp=timestamp, url=url, ) logger.info( f"生成 JS-SDK 签名: url={url[:80]}... timestamp={timestamp}" ) return success_response( { "corp_id": settings.wecom_corp_id, "agent_id": str(settings.wecom_agent_id), "timestamp": timestamp, "nonce_str": nonce_str, "signature": signature, } ) except Exception as e: logger.error(f"生成 JS-SDK 签名失败: {e}", exc_info=True) raise AppException( code=5001, message=f"生成 JS-SDK 签名失败: {str(e)}", ) from e # ============================================================================= # 应急页身份检测 (v0.5.4) # ============================================================================= # 流程: # 1. 前端用 wx.agentConfig 拿到当前 userid # 2. 前端调 GET /api/wecom/check-role?userid=xxx # 3. 后端用企微通讯录 API 查 userid 是否在"IT支持-咨询坐席"标签里 # 4. 返回 "user" 或 "agent" # ============================================================================= @router.get("/wecom/check-role") async def check_emergency_role( userid: str = Query(..., description="企微 userid"), ): """检测当前账号在应急页场景下的角色。 实现方式(优先级递减): 1. 企微通讯录标签检测(若配置 WECOM_AGENT_TAG_ID) 2. 后台硬编码名单(若配置 WECOM_AGENT_USERIDS 环境变量) 3. 默认 "user" (兜底) Args: userid: 企微 userid(从 wx.agentConfig 拿) Returns: { "code": 0, "data": { "role": "user" | "agent", "userid": "...", "method": "tag" | "hardcoded" | "default" } } """ wecom_service = get_shared_wecom_service() # 方式 1:企微标签检测 tag_id = getattr(settings, "wecom_agent_tag_id", None) user_info = None role = "user" method = "default" if tag_id: try: access_token = await wecom_service.get_access_token() url = f"https://qyapi.weixin.qq.com/cgi-bin/tag/get?access_token={access_token}&tagid={tag_id}" import httpx async with httpx.AsyncClient(timeout=5.0) as client: resp = await client.get(url) result = resp.json() if result.get("errcode", 0) == 0: user_list = result.get("userlist", []) # userlist 元素可能是 str(老版)或 dict(新版带 name) user_ids = [ u if isinstance(u, str) else u.get("userid", "") for u in user_list ] if userid in user_ids: logger.info(f"标签检测: userid={userid} 是坐席") role = "agent" method = "tag" else: logger.info(f"标签检测: userid={userid} 是员工") role = "user" method = "tag" else: logger.warning( f"标签 API 失败: errcode={result.get('errcode')}, " f"errmsg={result.get('errmsg')}, 降级到硬编码" ) except Exception as e: logger.warning(f"标签检测失败(降级): {e}") # 方式 2:硬编码名单 hardcoded = getattr(settings, "wecom_agent_userids", None) if hardcoded: agent_ids = [x.strip() for x in hardcoded.split(",") if x.strip()] if userid in agent_ids: logger.info(f"硬编码名单: userid={userid} 是坐席") role = "agent" method = "hardcoded" else: role = "user" method = "hardcoded" # 获取用户详细信息(名称、头像)- 添加超时,避免长时间阻塞 user_info = None try: import asyncio import httpx # 设置获取 access_token 的超时时间 access_token = await asyncio.wait_for( wecom_service.get_access_token(), timeout=2.0 # 2秒超时 ) user_url = f"https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token={access_token}&userid={userid}" async with httpx.AsyncClient(timeout=2.0) as client: user_resp = await client.get(user_url) user_result = user_resp.json() if user_result.get("errcode", 0) == 0: user_info = { "name": user_result.get("name", ""), "avatar": user_result.get("avatar", ""), "department": "" # 简化:暂不获取部门名称 } logger.info(f"获取用户信息成功: userid={userid}, name={user_info['name']}") except asyncio.TimeoutError: logger.warning(f"获取用户信息超时: userid={userid}") except Exception as e: logger.warning(f"获取用户信息失败: {e}") # 构建返回数据 response_data = { "role": role, "userid": userid, "method": method } # 添加用户信息(如果有) if user_info: response_data.update(user_info) # 方式 3:默认 user(当未配置检测方式时) if method == "default": logger.info(f"未配置检测方式, userid={userid} 默认 user") return success_response(response_data)