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:
Simon
2026-07-11 23:13:10 +08:00
parent 3d152fc8eb
commit bea288e414
928 changed files with 85169 additions and 54205 deletions
+89 -3
View File
@@ -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} — 坐席端统一格式 TokenJSON,含 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步:尝试员工端 Tokenemployee: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步:尝试坐席端统一格式 Tokenuser: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步:尝试坐席端旧格式 Tokenagent: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),
):
"""上传文件到服务器。