refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构

- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -0,0 +1,46 @@
"""Announcement system routers."""
from __future__ import annotations
from fastapi import APIRouter
from starlette.routing import BaseRoute
from .routes import router as announcement_router
_RUST_OWNED_ANNOUNCEMENT_ROUTE_SIGNATURES = frozenset(
{
("GET", "/api/announcements"),
("GET", "/api/announcements/active"),
("POST", "/api/announcements"),
("GET", "/api/announcements/{announcement_id}"),
("PATCH", "/api/announcements/{announcement_id}/read-status"),
("PUT", "/api/announcements/{announcement_id}"),
("DELETE", "/api/announcements/{announcement_id}"),
("GET", "/api/announcements/users/me/unread-count"),
}
)
def _route_is_rust_owned(route: BaseRoute) -> bool:
path = getattr(route, "path", None)
methods = getattr(route, "methods", None)
if not isinstance(path, str) or not methods:
return False
return any(
(method, path) in _RUST_OWNED_ANNOUNCEMENT_ROUTE_SIGNATURES
for method in methods
if method not in {"HEAD", "OPTIONS"}
)
def _build_python_announcement_router() -> APIRouter:
router = APIRouter()
router.include_router(announcement_router)
router.routes = [route for route in router.routes if not _route_is_rust_owned(route)]
return router
python_announcement_router = _build_python_announcement_router()
router = python_announcement_router
__all__ = ["python_announcement_router", "router"]

View File

@@ -0,0 +1,410 @@
"""公告系统 API 端点。"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from pydantic import ValidationError
from sqlalchemy.orm import Session
from src.api.base.adapter import ApiAdapter, ApiMode
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.authenticated_adapter import AuthenticatedApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.pipeline import get_pipeline
from src.core.exceptions import InvalidRequestException, translate_pydantic_error
from src.database import get_db
from src.models.api import CreateAnnouncementRequest, UpdateAnnouncementRequest
from src.models.database import User
from src.services.system.announcement import AnnouncementService
from src.utils.auth_utils import authenticate_user_from_bearer_token
router = APIRouter(prefix="/api/announcements", tags=["Announcements"])
pipeline = get_pipeline()
# ============== 公共端点(所有用户可访问) ==============
@router.get("")
async def list_announcements(
request: Request,
active_only: bool = Query(True, description="只返回有效公告"),
limit: int = Query(50, description="返回数量限制"),
offset: int = Query(0, description="偏移量"),
db: Session = Depends(get_db),
) -> Any:
"""
获取公告列表
获取公告列表,支持分页和筛选。如果用户已登录,返回包含已读状态。
**查询参数**:
- `active_only`: 是否只返回有效公告,默认 true
- `limit`: 返回数量限制,默认 50
- `offset`: 分页偏移量,默认 0
**返回字段**:
- `items`: 公告列表,每条公告包含:
- `id`: 公告 ID
- `title`: 标题
- `content`: 内容
- `type`: 类型info/warning/error/success
- `priority`: 优先级
- `is_pinned`: 是否置顶
- `is_read`: 是否已读(仅登录用户)
- `author`: 作者信息
- `start_time`: 生效开始时间
- `end_time`: 生效结束时间
- `created_at`: 创建时间
- `total`: 总数
- `unread_count`: 未读数量(仅登录用户)
"""
adapter = ListAnnouncementsAdapter(active_only=active_only, limit=limit, offset=offset)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/active")
async def get_active_announcements(
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
获取当前有效的公告
获取当前时间范围内有效的公告列表,用于首页展示。
**返回字段**:
- `items`: 有效公告列表
- `total`: 有效公告总数
- `unread_count`: 未读数量(仅登录用户)
"""
adapter = GetActiveAnnouncementsAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{announcement_id}")
async def get_announcement(
announcement_id: str, # UUID
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
获取单个公告详情
获取指定公告的详细信息。
**路径参数**:
- `announcement_id`: 公告 IDUUID
**返回字段**:
- `id`: 公告 ID
- `title`: 标题
- `content`: 内容
- `type`: 类型info/warning/error/success
- `priority`: 优先级
- `is_pinned`: 是否置顶
- `author`: 作者信息id, username
- `start_time`: 生效开始时间
- `end_time`: 生效结束时间
- `created_at`: 创建时间
- `updated_at`: 更新时间
"""
adapter = GetAnnouncementAdapter(announcement_id=announcement_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.patch("/{announcement_id}/read-status")
async def mark_announcement_as_read(
announcement_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
标记公告为已读
将指定公告标记为当前用户已读。需要登录。
**路径参数**:
- `announcement_id`: 公告 IDUUID
**返回字段**:
- `message`: 操作结果信息
"""
adapter = MarkAnnouncementReadAdapter(announcement_id=announcement_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
# ============== 管理员端点 ==============
@router.post("")
async def create_announcement(
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
创建公告
创建新的系统公告。需要管理员权限。
**请求体字段**:
- `title`: 公告标题(必填)
- `content`: 公告内容(必填)
- `type`: 公告类型info/warning/error/success默认 info
- `priority`: 优先级0-100默认 0
- `is_pinned`: 是否置顶,默认 false
- `start_time`: 生效开始时间(可选)
- `end_time`: 生效结束时间(可选)
**返回字段**:
- `id`: 新创建的公告 ID
- `title`: 公告标题
- `message`: 操作结果信息
"""
adapter = CreateAnnouncementAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.put("/{announcement_id}")
async def update_announcement(
announcement_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
更新公告
更新指定公告的信息。需要管理员权限。
**路径参数**:
- `announcement_id`: 公告 IDUUID
**请求体字段(均为可选)**:
- `title`: 公告标题
- `content`: 公告内容
- `type`: 公告类型info/warning/error/success
- `priority`: 优先级0-100
- `is_active`: 是否启用
- `is_pinned`: 是否置顶
- `start_time`: 生效开始时间
- `end_time`: 生效结束时间
**返回字段**:
- `message`: 操作结果信息
"""
adapter = UpdateAnnouncementAdapter(announcement_id=announcement_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.delete("/{announcement_id}")
async def delete_announcement(
announcement_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
删除公告
删除指定的公告。需要管理员权限。
**路径参数**:
- `announcement_id`: 公告 IDUUID
**返回字段**:
- `message`: 操作结果信息
"""
adapter = DeleteAnnouncementAdapter(announcement_id=announcement_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
# ============== 用户公告端点 ==============
@router.get("/users/me/unread-count")
async def get_my_unread_announcement_count(
request: Request,
db: Session = Depends(get_db),
) -> Any:
"""
获取我的未读公告数量
获取当前用户的未读公告数量。需要登录。
**返回字段**:
- `unread_count`: 未读公告数量
"""
adapter = UnreadAnnouncementCountAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
# ============== Pipeline 适配器 ==============
class AnnouncementOptionalAuthAdapter(ApiAdapter):
"""允许匿名访问但可选解析Bearer以获取用户上下文。"""
mode = ApiMode.PUBLIC
async def authorize(self, context: ApiRequestContext) -> None: # type: ignore[override]
context.extra["optional_user"] = await self._resolve_optional_user(context)
return None
async def _resolve_optional_user(self, context: ApiRequestContext) -> User | None:
if context.user:
return context.user
authorization = context.request.headers.get("authorization")
if not authorization or not authorization.lower().startswith("bearer "):
return None
token = authorization[7:].strip()
try:
return await authenticate_user_from_bearer_token(token, context.db, context.request)
except Exception:
return None
def get_optional_user(self, context: ApiRequestContext) -> User | None:
return context.extra.get("optional_user")
@dataclass
class ListAnnouncementsAdapter(AnnouncementOptionalAuthAdapter):
active_only: bool
limit: int
offset: int
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
optional_user = self.get_optional_user(context)
return AnnouncementService.get_announcements(
db=context.db,
user_id=optional_user.id if optional_user else None,
active_only=self.active_only,
include_read_status=True if optional_user else False,
limit=self.limit,
offset=self.offset,
)
class GetActiveAnnouncementsAdapter(AnnouncementOptionalAuthAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
optional_user = self.get_optional_user(context)
return AnnouncementService.get_active_announcements(
db=context.db,
user_id=optional_user.id if optional_user else None,
)
@dataclass
class GetAnnouncementAdapter(AnnouncementOptionalAuthAdapter):
announcement_id: str
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
announcement = AnnouncementService.get_announcement(context.db, self.announcement_id)
return {
"id": announcement.id,
"title": announcement.title,
"content": announcement.content,
"type": announcement.type,
"priority": announcement.priority,
"is_pinned": announcement.is_pinned,
"author": {"id": announcement.author.id, "username": announcement.author.username},
"start_time": announcement.start_time,
"end_time": announcement.end_time,
"created_at": announcement.created_at,
"updated_at": announcement.updated_at,
}
class AnnouncementUserAdapter(AuthenticatedApiAdapter):
"""需要登录但不要求管理员的公告适配器基类。"""
pass
class MarkAnnouncementReadAdapter(AnnouncementUserAdapter):
def __init__(self, announcement_id: str):
self.announcement_id = announcement_id
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
AnnouncementService.mark_as_read(context.db, self.announcement_id, context.user.id)
return {"message": "公告已标记为已读"}
class UnreadAnnouncementCountAdapter(AnnouncementUserAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
result = AnnouncementService.get_announcements(
db=context.db,
user_id=context.user.id,
active_only=True,
include_read_status=True,
limit=1,
offset=0,
)
return {"unread_count": result.get("unread_count", 0)}
class CreateAnnouncementAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
payload = context.ensure_json_body()
try:
req = CreateAnnouncementRequest.model_validate(payload)
except ValidationError as e:
errors = e.errors()
if errors:
raise InvalidRequestException(translate_pydantic_error(errors[0]))
raise InvalidRequestException("请求数据验证失败")
announcement = AnnouncementService.create_announcement(
db=context.db,
author_id=context.user.id,
title=req.title,
content=req.content,
type=req.type,
priority=req.priority,
is_pinned=req.is_pinned,
start_time=req.start_time,
end_time=req.end_time,
)
return {"id": announcement.id, "title": announcement.title, "message": "公告创建成功"}
@dataclass
class UpdateAnnouncementAdapter(AdminApiAdapter):
announcement_id: str
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
payload = context.ensure_json_body()
try:
req = UpdateAnnouncementRequest.model_validate(payload)
except ValidationError as e:
errors = e.errors()
if errors:
raise InvalidRequestException(translate_pydantic_error(errors[0]))
raise InvalidRequestException("请求数据验证失败")
AnnouncementService.update_announcement(
db=context.db,
announcement_id=self.announcement_id,
user_id=context.user.id,
title=req.title,
content=req.content,
type=req.type,
priority=req.priority,
is_active=req.is_active,
is_pinned=req.is_pinned,
start_time=req.start_time,
end_time=req.end_time,
)
return {"message": "公告更新成功"}
@dataclass
class DeleteAnnouncementAdapter(AdminApiAdapter):
announcement_id: str
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
AnnouncementService.delete_announcement(context.db, self.announcement_id, context.user.id)
return {"message": "公告已删除"}