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:
@@ -7,6 +7,7 @@
|
||||
# 文件存储路径:./uploads/YYYY/MM/DD/{uuid}.{ext}
|
||||
# =============================================================================
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
@@ -14,11 +15,13 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
import redis.asyncio as aioredis
|
||||
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.utils.response import success_response
|
||||
from app.utils.response import success_response, AppException, ERR_UNAUTHORIZED
|
||||
from app.api.h5 import _get_current_employee
|
||||
from app.dependencies import dep_redis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,6 +53,89 @@ MAX_IMAGE_SIZE = int(os.getenv("MAX_IMAGE_SIZE", str(10 * 1024 * 1024))) # 10MB
|
||||
IMAGE_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 双路认证依赖:同时支持员工端(H5)和坐席端(Agent)Token
|
||||
# --------------------------------------------------------------------------
|
||||
# 做什么:从 Authorization 头提取 token,依次尝试三种 Redis key 查找用户身份
|
||||
# 为什么需要:上传端点同时被 H5 员工端和坐席端调用,两者的 Token 存储格式不同
|
||||
# - H5 员工端:employee:token:{token} → employee_id(字符串)
|
||||
# - 坐席端统一格式:user:token:{token} → JSON({employee_id, roles, ...})
|
||||
# - 坐席端旧格式:agent:token:{token} → user_id(字符串)
|
||||
# 修复记录:2026-07-11 坐席端粘贴/截图/拍照上传报"未授权",原因是原代码只查 employee:token
|
||||
async def _get_current_user_id(
|
||||
authorization: Optional[str] = Header(None, alias="Authorization"),
|
||||
x_employee_id: Optional[str] = Header(None, alias="X-Employee-Id"),
|
||||
redis_client: Optional[aioredis.Redis] = Depends(dep_redis),
|
||||
) -> str:
|
||||
"""双路认证:支持员工端和坐席端两种 Token 格式。
|
||||
|
||||
查找顺序:
|
||||
1. employee:token:{token} — H5 员工端 Token(原逻辑)
|
||||
2. user:token:{token} — 坐席端统一格式 Token(JSON,含 employee_id)
|
||||
3. agent:token:{token} — 坐席端旧格式 Token(直接存储 user_id)
|
||||
|
||||
Args:
|
||||
authorization: 请求头 Authorization 字段(格式:Bearer token)
|
||||
x_employee_id: 请求头 X-Employee-Id 字段(开发降级用)
|
||||
redis_client: Redis 客户端(DI 注入)
|
||||
|
||||
Returns:
|
||||
str: 用户 ID(企微 UserID 或坐席 user_id)
|
||||
|
||||
Raises:
|
||||
AppException(1002, "未授权"): 所有 Token 查找均失败
|
||||
"""
|
||||
# ---- 第1步:尝试员工端 Token(employee:token:{token})----
|
||||
# _get_current_employee 内部已处理 Bearer 前缀、X-Employee-Id 降级等逻辑
|
||||
# 如果成功则直接返回 employee_id;如果抛 AppException 则继续尝试坐席 Token
|
||||
try:
|
||||
employee_id = await _get_current_employee(authorization, x_employee_id, redis_client)
|
||||
return employee_id
|
||||
except AppException:
|
||||
# 员工 Token 查不到,继续尝试坐席 Token
|
||||
pass
|
||||
except Exception:
|
||||
# 其他异常(如 Redis 连接问题)也继续尝试
|
||||
pass
|
||||
|
||||
# ---- 第2步:提取 token(去除 Bearer 前缀)----
|
||||
if not authorization:
|
||||
raise ERR_UNAUTHORIZED
|
||||
token = authorization.replace("Bearer ", "", 1) if authorization.startswith("Bearer ") else authorization
|
||||
if not token:
|
||||
raise ERR_UNAUTHORIZED
|
||||
|
||||
# Redis 不可用时直接拒绝(无法验证坐席 Token)
|
||||
if not redis_client:
|
||||
logger.error("Redis 不可用,无法验证坐席端上传 Token")
|
||||
raise ERR_UNAUTHORIZED
|
||||
|
||||
# ---- 第3步:尝试坐席端统一格式 Token(user:token:{token})----
|
||||
# 值是 JSON 字符串:{"employee_id": "xxx", "roles": [...], ...}
|
||||
try:
|
||||
unified_data = await redis_client.get(f"user:token:{token}")
|
||||
if unified_data:
|
||||
user_info = json.loads(unified_data)
|
||||
employee_id = user_info.get("employee_id")
|
||||
if employee_id:
|
||||
return employee_id
|
||||
except Exception as e:
|
||||
logger.warning(f"查询 user:token 失败: {e}")
|
||||
|
||||
# ---- 第4步:尝试坐席端旧格式 Token(agent:token:{token})----
|
||||
# 值直接是 user_id 字符串
|
||||
try:
|
||||
agent_user_id = await redis_client.get(f"agent:token:{token}")
|
||||
if agent_user_id:
|
||||
# Redis 返回 bytes,需要解码
|
||||
return agent_user_id.decode("utf-8") if isinstance(agent_user_id, bytes) else agent_user_id
|
||||
except Exception as e:
|
||||
logger.warning(f"查询 agent:token 失败: {e}")
|
||||
|
||||
# 所有 Token 查找均失败
|
||||
raise ERR_UNAUTHORIZED
|
||||
|
||||
|
||||
def _get_file_extension(filename: str) -> str:
|
||||
"""从文件名中提取小写扩展名。
|
||||
|
||||
@@ -99,7 +185,7 @@ def _generate_storage_path(extension: str) -> tuple[Path, str]:
|
||||
@router.post("/upload")
|
||||
async def upload_file(
|
||||
file: UploadFile = File(..., description="上传的文件(图片或文档)"),
|
||||
employee_id: str = Depends(_get_current_employee),
|
||||
user_id: str = Depends(_get_current_user_id),
|
||||
):
|
||||
"""上传文件到服务器。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user