mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-13 21:05:19 +00:00
352 lines
9.8 KiB
Python
352 lines
9.8 KiB
Python
"""
|
||
公告系统 API 路由模块
|
||
|
||
本模块提供系统公告的 RESTful API 接口,支持:
|
||
- 用户端:查看最新公告、标记已读
|
||
- 管理端:公告 CRUD 管理(创建、查询、更新、归档)
|
||
|
||
路由前缀:/api/announcements
|
||
权限控制:
|
||
- 用户接口(/latest, /read):需要 verified 用户身份
|
||
- 管理接口(/, POST, PUT, DELETE):需要 admin 用户身份
|
||
|
||
数据流:
|
||
用户请求 → FastAPI Router → 数据访问层(models/announcements.py)→ 数据库
|
||
"""
|
||
|
||
import logging
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||
from pydantic import BaseModel
|
||
|
||
from open_webui.constants import ERROR_MESSAGES
|
||
from open_webui.env import SRC_LOG_LEVELS
|
||
from open_webui.models.announcements import (
|
||
Announcements,
|
||
AnnouncementReads,
|
||
AnnouncementForm,
|
||
AnnouncementUpdateForm,
|
||
AnnouncementUserView,
|
||
AnnouncementModel,
|
||
)
|
||
from open_webui.models.users import UserResponse, Users
|
||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||
|
||
log = logging.getLogger(__name__)
|
||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
##############################################################################
|
||
# 请求/响应模型
|
||
##############################################################################
|
||
|
||
class AnnouncementReadRequest(BaseModel):
|
||
"""
|
||
标记已读请求模型
|
||
|
||
用于批量标记多个公告为已读
|
||
"""
|
||
ids: list[str]
|
||
|
||
|
||
class AnnouncementWithAuthor(AnnouncementModel):
|
||
"""
|
||
带作者信息的公告模型
|
||
|
||
继承自 AnnouncementModel,额外包含创建者的用户信息
|
||
用于管理端列表接口
|
||
"""
|
||
author: Optional[UserResponse] = None
|
||
|
||
|
||
##############################################################################
|
||
# 用户端 API(需要登录验证)
|
||
##############################################################################
|
||
|
||
@router.get("/latest", response_model=list[AnnouncementUserView])
|
||
async def get_latest_announcements(
|
||
request: Request,
|
||
since: Optional[int] = None,
|
||
limit: Optional[int] = 20,
|
||
user=Depends(get_verified_user),
|
||
):
|
||
"""
|
||
获取最新公告列表(用户端)
|
||
|
||
权限:已验证用户
|
||
方法:GET /api/announcements/latest
|
||
|
||
查询参数:
|
||
- since: 可选,纳秒时间戳。仅返回创建时间晚于此值的公告(用于增量拉取)
|
||
- limit: 可选,限制返回数量,默认 20 条
|
||
|
||
返回:
|
||
- 公告列表(仅 active 状态),每条公告包含:
|
||
- 公告基本信息(id, title, content, status, created_at 等)
|
||
- is_read: 当前用户是否已读
|
||
- read_at: 当前用户的阅读时间(如果已读)
|
||
|
||
用途:
|
||
- 前端首次加载:since=None, limit=20
|
||
- 增量拉取新公告:since=上次拉取的最新公告时间戳
|
||
|
||
示例:
|
||
GET /api/announcements/latest?limit=10
|
||
GET /api/announcements/latest?since=1638316800000000000
|
||
"""
|
||
# 查询激活状态的公告列表
|
||
announcements = Announcements.list(status="active", since=since, limit=limit)
|
||
|
||
# 获取当前用户的阅读记录
|
||
read_map = AnnouncementReads.get_read_map(
|
||
user.id, [announcement.id for announcement in announcements]
|
||
)
|
||
|
||
# 为每条公告附加阅读状态
|
||
return [
|
||
AnnouncementUserView(
|
||
**announcement.model_dump(),
|
||
is_read=announcement.id in read_map,
|
||
read_at=read_map.get(announcement.id).read_at if announcement.id in read_map else None,
|
||
)
|
||
for announcement in announcements
|
||
]
|
||
|
||
|
||
@router.post("/read")
|
||
async def mark_read(
|
||
payload: AnnouncementReadRequest,
|
||
user=Depends(get_verified_user),
|
||
):
|
||
"""
|
||
批量标记公告为已读(用户端)
|
||
|
||
权限:已验证用户
|
||
方法:POST /api/announcements/read
|
||
|
||
请求体:
|
||
{
|
||
"ids": ["uuid1", "uuid2", ...]
|
||
}
|
||
|
||
返回:
|
||
{
|
||
"success": true,
|
||
"updated": 2 // 成功标记的数量
|
||
}
|
||
|
||
注意:
|
||
- 幂等操作,重复标记不会报错
|
||
- 仅标记指定用户的阅读记录,不影响其他用户
|
||
- 用于用户点击公告或滚动浏览时标记已读
|
||
|
||
示例:
|
||
POST /api/announcements/read
|
||
{"ids": ["123e4567-e89b-12d3-a456-426614174000"]}
|
||
"""
|
||
if not payload.ids:
|
||
return {"success": True, "updated": 0}
|
||
|
||
AnnouncementReads.bulk_mark_read(user.id, payload.ids)
|
||
return {"success": True, "updated": len(payload.ids)}
|
||
|
||
|
||
##############################################################################
|
||
# 管理端 API(需要管理员权限)
|
||
##############################################################################
|
||
|
||
# Support both with and without trailing slash for list/create
|
||
@router.get("", response_model=list[AnnouncementWithAuthor])
|
||
@router.get("/", response_model=list[AnnouncementWithAuthor])
|
||
async def list_announcements(
|
||
status: Optional[str] = None,
|
||
user=Depends(get_admin_user),
|
||
):
|
||
"""
|
||
获取公告列表(管理端)
|
||
|
||
权限:管理员
|
||
方法:GET /api/announcements
|
||
|
||
查询参数:
|
||
- status: 可选,过滤状态("active" 或 "archived")。不传则返回所有状态
|
||
|
||
返回:
|
||
- 公告列表,每条包含作者用户信息(id, name, email, role 等)
|
||
- 按创建时间倒序排列
|
||
|
||
用途:
|
||
- 管理员查看所有公告
|
||
- 管理员查看已归档公告:status=archived
|
||
|
||
示例:
|
||
GET /api/announcements
|
||
GET /api/announcements?status=active
|
||
GET /api/announcements?status=archived
|
||
"""
|
||
announcements = Announcements.list(status=status)
|
||
|
||
# 批量查询作者信息(避免 N+1 查询)
|
||
authors = {a.created_by: Users.get_user_by_id(a.created_by) for a in announcements}
|
||
|
||
return [
|
||
AnnouncementWithAuthor(
|
||
**a.model_dump(),
|
||
author=UserResponse(**authors[a.created_by].model_dump()) if authors.get(a.created_by) else None,
|
||
)
|
||
for a in announcements
|
||
]
|
||
|
||
|
||
@router.post("", response_model=AnnouncementModel)
|
||
@router.post("/", response_model=AnnouncementModel)
|
||
async def create_announcement(
|
||
form_data: AnnouncementForm,
|
||
user=Depends(get_admin_user),
|
||
):
|
||
"""
|
||
创建新公告(管理端)
|
||
|
||
权限:管理员
|
||
方法:POST /api/announcements
|
||
|
||
请求体:
|
||
{
|
||
"title": "公告标题",
|
||
"content": "公告内容(支持 Markdown)",
|
||
"status": "active", // 可选,默认 "active"
|
||
"meta": {} // 可选,扩展元数据
|
||
}
|
||
|
||
返回:
|
||
- 创建的完整公告数据(包含自动生成的 id, created_at, updated_at)
|
||
|
||
注意:
|
||
- 自动记录创建者为当前管理员用户
|
||
- 默认状态为 active,创建后立即对用户可见
|
||
|
||
示例:
|
||
POST /api/announcements
|
||
{
|
||
"title": "系统维护通知",
|
||
"content": "系统将于明天凌晨 2:00-4:00 进行维护升级",
|
||
"status": "active"
|
||
}
|
||
"""
|
||
announcement = Announcements.insert(form_data, user.id)
|
||
return announcement
|
||
|
||
|
||
@router.put("/{announcement_id}", response_model=AnnouncementModel)
|
||
async def update_announcement(
|
||
announcement_id: str,
|
||
form_data: AnnouncementUpdateForm,
|
||
user=Depends(get_admin_user),
|
||
):
|
||
"""
|
||
更新公告(管理端)
|
||
|
||
权限:管理员
|
||
方法:PUT /api/announcements/{announcement_id}
|
||
|
||
路径参数:
|
||
- announcement_id: 公告 UUID
|
||
|
||
请求体(所有字段均可选):
|
||
{
|
||
"title": "新标题",
|
||
"content": "新内容",
|
||
"status": "active" | "archived",
|
||
"meta": {}
|
||
}
|
||
|
||
返回:
|
||
- 更新后的完整公告数据
|
||
|
||
异常:
|
||
- 404 NOT_FOUND:公告不存在
|
||
|
||
注意:
|
||
- 仅更新提供的字段,未提供的字段保持原值
|
||
- 自动更新 updated_at 时间戳
|
||
- 可通过设置 status="archived" 归档公告
|
||
|
||
示例:
|
||
PUT /api/announcements/123e4567-e89b-12d3-a456-426614174000
|
||
{"content": "更新后的公告内容"}
|
||
"""
|
||
updated = Announcements.update(announcement_id, form_data)
|
||
if not updated:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||
)
|
||
return updated
|
||
|
||
|
||
@router.delete("/{announcement_id}", response_model=AnnouncementModel)
|
||
async def delete_announcement(
|
||
announcement_id: str,
|
||
user=Depends(get_admin_user),
|
||
):
|
||
"""
|
||
归档公告(管理端)
|
||
|
||
权限:管理员
|
||
方法:DELETE /api/announcements/{announcement_id}
|
||
|
||
路径参数:
|
||
- announcement_id: 公告 UUID
|
||
|
||
返回:
|
||
- 归档后的完整公告数据(status 变为 "archived")
|
||
|
||
异常:
|
||
- 404 NOT_FOUND:公告不存在
|
||
|
||
注意:
|
||
- 软删除,不物理删除数据
|
||
- 归档后的公告不会在用户端显示(/latest 不返回)
|
||
- 管理员仍可通过 GET /?status=archived 查看已归档公告
|
||
- 归档后可通过 PUT 接口重新激活:{"status": "active"}
|
||
|
||
示例:
|
||
DELETE /api/announcements/123e4567-e89b-12d3-a456-426614174000
|
||
"""
|
||
deleted = Announcements.archive(announcement_id)
|
||
if not deleted:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||
)
|
||
return deleted
|
||
|
||
|
||
@router.delete("/{announcement_id}/hard")
|
||
async def hard_delete_announcement(
|
||
announcement_id: str,
|
||
user=Depends(get_admin_user),
|
||
):
|
||
"""
|
||
物理删除公告及其阅读记录(管理端)
|
||
|
||
权限:管理员
|
||
方法:DELETE /api/v1/announcements/{announcement_id}/hard
|
||
|
||
返回:
|
||
- {"success": True} 删除成功
|
||
|
||
注意:
|
||
- 会删除公告本身以及 announcement_read 中所有关联记录
|
||
- 不可恢复,请谨慎调用
|
||
"""
|
||
deleted = Announcements.delete(announcement_id)
|
||
if not deleted:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||
)
|
||
return {"success": True}
|