388 lines
12 KiB
Python
388 lines
12 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — 欢迎与引导配置 API
|
||
# =============================================================================
|
||
# 说明:管理员工端H5的欢迎页、引导视频、互动教学、主题模板配置
|
||
# GET /api/admin/welcome-config - 获取全部配置
|
||
# PUT /api/admin/welcome-config - 更新配置
|
||
# POST /api/admin/welcome-video - 上传视频
|
||
# DELETE /api/admin/welcome-video - 删除视频
|
||
# =============================================================================
|
||
|
||
import json
|
||
import logging
|
||
import os
|
||
import uuid
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
|
||
from fastapi.responses import FileResponse
|
||
from pydantic import BaseModel
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.api.agents import get_current_agent
|
||
from app.database import get_db
|
||
from app.models.agent import Agent
|
||
from app.models.system_config import SystemConfig
|
||
from app.utils.response import AppException, success_response
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 创建路由器
|
||
router = APIRouter(prefix="/admin/welcome-config", tags=["欢迎与引导配置"])
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 请求/响应模型
|
||
# --------------------------------------------------------------------------
|
||
|
||
# 欢迎页配置
|
||
class WelcomePageConfig(BaseModel):
|
||
enabled: bool = False
|
||
title: str = "欢迎使用IT服务台"
|
||
content: str = "在这里您可以提交IT问题申请,获取快速支持"
|
||
background_color: str = "#07C160"
|
||
|
||
|
||
# 引导视频配置
|
||
class WelcomeVideoConfig(BaseModel):
|
||
url: str
|
||
filename: str
|
||
uploaded_at: str
|
||
|
||
|
||
# 教程步骤
|
||
class TutorialStep(BaseModel):
|
||
id: str
|
||
title: str
|
||
description: str
|
||
|
||
|
||
# 互动教学配置
|
||
class TutorialConfig(BaseModel):
|
||
enabled: bool = False
|
||
steps: list[TutorialStep] = []
|
||
|
||
|
||
# 主题配置
|
||
class ThemeConfig(BaseModel):
|
||
theme: str = "default" # default/blue/orange/red
|
||
|
||
|
||
# 完整配置
|
||
class WelcomeConfigData(BaseModel):
|
||
welcome_page: WelcomePageConfig = WelcomePageConfig()
|
||
welcome_video: Optional[WelcomeVideoConfig] = None
|
||
tutorial: TutorialConfig = TutorialConfig()
|
||
theme: ThemeConfig = ThemeConfig()
|
||
|
||
|
||
# 更新配置请求
|
||
class UpdateWelcomeConfigRequest(BaseModel):
|
||
welcome_page: Optional[WelcomePageConfig] = None
|
||
welcome_video: Optional[WelcomeVideoConfig] = None
|
||
tutorial: Optional[TutorialConfig] = None
|
||
theme: Optional[ThemeConfig] = None
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 视频存储配置
|
||
# --------------------------------------------------------------------------
|
||
VIDEO_UPLOAD_DIR = Path(os.getenv("VIDEO_UPLOAD_DIR", "./uploads/videos"))
|
||
VIDEO_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||
MAX_VIDEO_SIZE = int(os.getenv("MAX_VIDEO_SIZE", str(50 * 1024 * 1024))) # 50MB
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 权限校验依赖
|
||
# --------------------------------------------------------------------------
|
||
async def require_admin(
|
||
agent: Agent = Depends(get_current_agent),
|
||
) -> Agent:
|
||
"""管理后台权限校验:仅 role='admin' 可访问。"""
|
||
if agent.role != "admin":
|
||
raise AppException(1004, "无管理权限")
|
||
return agent
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 辅助函数
|
||
# --------------------------------------------------------------------------
|
||
|
||
async def _get_welcome_config(db: AsyncSession) -> WelcomeConfigData:
|
||
"""从数据库获取欢迎配置。"""
|
||
stmt = select(SystemConfig).where(
|
||
SystemConfig.config_key == "welcome_config"
|
||
)
|
||
result = await db.execute(stmt)
|
||
config_row = result.scalars().first()
|
||
|
||
if config_row:
|
||
try:
|
||
data = json.loads(config_row.config_value)
|
||
return WelcomeConfigData(**data)
|
||
except (json.JSONDecodeError, TypeError):
|
||
pass
|
||
|
||
# 返回默认配置
|
||
return WelcomeConfigData()
|
||
|
||
|
||
async def _save_welcome_config(
|
||
db: AsyncSession,
|
||
config_data: WelcomeConfigData,
|
||
) -> None:
|
||
"""保存欢迎配置到数据库。"""
|
||
stmt = select(SystemConfig).where(
|
||
SystemConfig.config_key == "welcome_config"
|
||
)
|
||
result = await db.execute(stmt)
|
||
config_row = result.scalars().first()
|
||
|
||
config_value = config_data.model_dump_json()
|
||
|
||
if config_row:
|
||
config_row.config_value = config_value
|
||
config_row.updated_at = datetime.now()
|
||
else:
|
||
new_config = SystemConfig(
|
||
id=str(uuid.uuid4()),
|
||
config_key="welcome_config",
|
||
config_value=config_value,
|
||
description="员工端H5欢迎与引导配置",
|
||
)
|
||
db.add(new_config)
|
||
|
||
await db.flush()
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# API 端点
|
||
# --------------------------------------------------------------------------
|
||
|
||
# ---------- GET /api/v1/admin/welcome-config ----------
|
||
@router.get("")
|
||
async def get_welcome_config(
|
||
admin: Agent = Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> dict:
|
||
"""获取欢迎与引导配置。
|
||
|
||
Args:
|
||
admin: 管理员(权限校验)
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
Dict: 统一响应格式,包含全部配置
|
||
"""
|
||
config = await _get_welcome_config(db)
|
||
return success_response(data=config.model_dump())
|
||
|
||
|
||
# ---------- PUT /api/v1/admin/welcome-config ----------
|
||
@router.put("")
|
||
async def update_welcome_config(
|
||
body: UpdateWelcomeConfigRequest,
|
||
admin: Agent = Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> dict:
|
||
"""更新欢迎与引导配置。
|
||
|
||
Args:
|
||
body: 更新请求体
|
||
admin: 管理员(权限校验)
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
Dict: 统一响应格式,包含更新后的配置
|
||
"""
|
||
# 获取当前配置
|
||
current_config = await _get_welcome_config(db)
|
||
|
||
# 合并更新
|
||
if body.welcome_page is not None:
|
||
current_config.welcome_page = body.welcome_page
|
||
if body.welcome_video is not None:
|
||
current_config.welcome_video = body.welcome_video
|
||
if body.tutorial is not None:
|
||
current_config.tutorial = body.tutorial
|
||
if body.theme is not None:
|
||
current_config.theme = body.theme
|
||
|
||
# 保存配置
|
||
await _save_welcome_config(db, current_config)
|
||
await db.commit()
|
||
|
||
logger.info(f"管理员更新欢迎配置: admin={admin.user_id}")
|
||
return success_response(data=current_config.model_dump())
|
||
|
||
|
||
# ---------- POST /api/v1/admin/welcome-video ----------
|
||
@router.post("/video")
|
||
async def upload_welcome_video(
|
||
file: UploadFile = File(..., description="引导视频文件"),
|
||
admin: Agent = Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> dict:
|
||
"""上传引导视频。
|
||
|
||
Args:
|
||
file: 视频文件
|
||
admin: 管理员(权限校验)
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
Dict: 统一响应格式,包含视频信息
|
||
"""
|
||
# 校验文件类型
|
||
allowed_extensions = {"mp4"}
|
||
ext = file.filename.split(".")[-1].lower() if file.filename else ""
|
||
|
||
if ext not in allowed_extensions:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"不支持的视频格式: .{ext},仅支持 .mp4",
|
||
)
|
||
|
||
# 校验文件大小
|
||
content = await file.read()
|
||
file_size = len(content)
|
||
|
||
if file_size > MAX_VIDEO_SIZE:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail=f"视频大小 {file_size / 1024 / 1024:.1f}MB 超过限制(50MB)",
|
||
)
|
||
|
||
# 生成存储路径(使用与普通文件上传相同的目录结构)
|
||
now = datetime.now()
|
||
file_id = uuid.uuid4().hex[:12]
|
||
filename = f"{file_id}.{ext}"
|
||
# 按日期分目录:uploads/videos/YYYY/MM/DD/
|
||
video_dir = VIDEO_UPLOAD_DIR / f"{now.year}" / f"{now.month:02d}" / f"{now.day:02d}"
|
||
video_dir.mkdir(parents=True, exist_ok=True)
|
||
storage_path = video_dir / filename
|
||
|
||
# 保存文件
|
||
try:
|
||
with open(storage_path, "wb") as f:
|
||
f.write(content)
|
||
except OSError as e:
|
||
logger.error(f"视频保存失败: {e}")
|
||
raise HTTPException(status_code=500, detail="视频保存失败,请重试")
|
||
|
||
# 构建访问 URL(与普通文件上传格式一致)
|
||
video_url = f"/api/media/videos/{now.year}/{now.month:02d}/{now.day:02d}/{filename}"
|
||
|
||
# 更新数据库配置
|
||
config = await _get_welcome_config(db)
|
||
config.welcome_video = WelcomeVideoConfig(
|
||
url=video_url,
|
||
filename=file.filename or filename,
|
||
uploaded_at=now.isoformat(),
|
||
)
|
||
await _save_welcome_config(db, config)
|
||
await db.commit()
|
||
|
||
logger.info(f"管理员上传引导视频: {file.filename}, admin={admin.user_id}")
|
||
|
||
return success_response(data={
|
||
"url": video_url,
|
||
"filename": file.filename or filename,
|
||
"uploaded_at": now.isoformat(),
|
||
})
|
||
|
||
|
||
# ---------- DELETE /api/admin/welcome-video ----------
|
||
@router.delete("/video")
|
||
async def delete_welcome_video(
|
||
admin: Agent = Depends(require_admin),
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> dict:
|
||
"""删除引导视频。
|
||
|
||
Args:
|
||
admin: 管理员(权限校验)
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
Dict: 统一响应格式
|
||
"""
|
||
# 获取当前配置
|
||
config = await _get_welcome_config(db)
|
||
|
||
if not config.welcome_video:
|
||
raise AppException(4001, "暂无引导视频")
|
||
|
||
# 删除文件
|
||
video_url = config.welcome_video.url
|
||
if video_url and video_url.startswith("/api/media/videos/"):
|
||
# 解析URL: /api/media/videos/YYYY/MM/DD/filename
|
||
parts = video_url.split("/")
|
||
if len(parts) >= 8:
|
||
year = parts[-4]
|
||
month = parts[-3]
|
||
day = parts[-2]
|
||
filename = parts[-1]
|
||
file_path = VIDEO_UPLOAD_DIR / year / month / day / filename
|
||
try:
|
||
if file_path.exists():
|
||
file_path.unlink()
|
||
except OSError as e:
|
||
logger.warning(f"删除视频文件失败: {e}")
|
||
|
||
# 清除配置
|
||
config.welcome_video = None
|
||
await _save_welcome_config(db, config)
|
||
await db.commit()
|
||
|
||
logger.info(f"管理员删除引导视频: admin={admin.user_id}")
|
||
return success_response(data=None, message="视频已删除")
|
||
|
||
|
||
# ---------- GET /api/media/videos/{year}/{month}/{day}/{filename} ----------
|
||
@router.get("/media/videos/{year}/{month}/{day}/{filename}")
|
||
async def serve_welcome_video(
|
||
year: str,
|
||
month: str,
|
||
day: str,
|
||
filename: str,
|
||
):
|
||
"""提供引导视频的静态访问。
|
||
|
||
Args:
|
||
year: 年份
|
||
month: 月份
|
||
day: 日期
|
||
filename: 文件名
|
||
|
||
Returns:
|
||
FileResponse: 视频文件响应
|
||
"""
|
||
file_path = VIDEO_UPLOAD_DIR / year / month / day / filename
|
||
|
||
# 安全检查:防止路径遍历攻击
|
||
try:
|
||
resolved = file_path.resolve()
|
||
upload_root = VIDEO_UPLOAD_DIR.resolve()
|
||
if not str(resolved).startswith(str(upload_root)):
|
||
raise HTTPException(status_code=403, detail="禁止访问")
|
||
except (ValueError, OSError):
|
||
raise HTTPException(status_code=403, detail="禁止访问")
|
||
|
||
if not file_path.exists():
|
||
raise HTTPException(status_code=404, detail="视频文件不存在")
|
||
|
||
# 根据扩展名设置 Content-Type
|
||
content_type = "video/mp4"
|
||
return FileResponse(file_path, media_type=content_type)
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 注册路由到主路由
|
||
# --------------------------------------------------------------------------
|
||
def register_welcome_routes(main_router: APIRouter) -> None:
|
||
"""将欢迎配置路由注册到主路由。"""
|
||
main_router.include_router(router)
|