refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)

配置 SSOT(TOML + .env)
统一错误契约
Auth 与事务边界
Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client
可观测性(OpenTelemetry + LGTM)
This commit is contained in:
Sully
2026-05-22 13:44:50 +08:00
committed by GitHub
parent f09ae248f9
commit 53e0065e3e
298 changed files with 15247 additions and 4344 deletions

View File

@@ -3,12 +3,13 @@
import asyncio
from typing import List, Optional
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from app.agents.state_schema import narrative_coverage_state
from app.agents.state_schema import MemoirStateSchema, narrative_coverage_state
from app.core.db import transactional
from app.core.errors import AuthorizationError, BadRequestError, NotFoundError
from app.core.logging import get_logger
from app.core.storage_purge import delete_object_storage_keys_best_effort
from app.features.memoir import repo
@@ -104,18 +105,18 @@ class MemoirService:
book = await repo.get_current_book(user_id, self._db)
if not book:
return {"status": "ok", "message": "No book found"}
book.has_update = False
await self._db.commit()
async with transactional(self._db):
book.has_update = False
return {"status": "ok"}
async def update_book(self, book_id: str, user_id: str, title: str) -> dict:
book = await self._db.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
raise NotFoundError("Book not found")
if book.user_id != user_id:
raise HTTPException(status_code=403, detail="无权更新此回忆录")
book.title = title
await self._db.commit()
raise AuthorizationError("无权更新此回忆录")
async with transactional(self._db):
book.title = title
await self._db.refresh(book)
return {
"id": book.id,
@@ -132,9 +133,9 @@ class MemoirService:
book = await self._db.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
raise NotFoundError("Book not found")
if book.user_id != user_id:
raise HTTPException(status_code=403, detail="无权导出此回忆录")
raise AuthorizationError("无权导出此回忆录")
stmt = (
select(Chapter)
.where(Chapter.user_id == user_id, Chapter.is_active == True)
@@ -195,16 +196,16 @@ class MemoirService:
async def get_chapter(self, chapter_id: str, user_id: str) -> dict:
chapter = await repo.get_chapter_by_id(chapter_id, self._db)
if not chapter:
raise HTTPException(status_code=404, detail="Chapter not found")
raise NotFoundError("Chapter not found")
if chapter.user_id != user_id:
raise HTTPException(status_code=403, detail="无权访问此章节")
raise AuthorizationError("无权访问此章节")
if not chapter.is_active:
raise HTTPException(status_code=404, detail="Chapter not found")
raise NotFoundError("Chapter not found")
chapter, md_override = prepare_chapter_read_view(chapter)
if not chapter_meets_minimum_display(
chapter, canonical_markdown_override=md_override
):
raise HTTPException(status_code=404, detail="Chapter not found")
raise NotFoundError("Chapter not found")
asset_map = await signed_urls_for_asset_ids(
self._db, collect_asset_ids_for_chapter(chapter)
)
@@ -217,12 +218,12 @@ class MemoirService:
async def disable_chapter(self, chapter_id: str, user_id: str) -> dict:
chapter = await repo.get_chapter_by_id(chapter_id, self._db)
if not chapter:
raise HTTPException(status_code=404, detail="Chapter not found")
raise NotFoundError("Chapter not found")
if chapter.user_id != user_id:
raise HTTPException(status_code=403, detail="无权操作此章节")
raise AuthorizationError("无权操作此章节")
cos_keys = await repo.collect_cos_storage_keys_for_chapter(self._db, chapter)
chapter.is_active = False
await self._db.commit()
async with transactional(self._db):
chapter.is_active = False
delete_object_storage_keys_best_effort(
self._object_storage,
cos_keys,
@@ -235,32 +236,41 @@ class MemoirService:
) -> dict:
chapter = await self._db.get(Chapter, chapter_id)
if not chapter:
raise HTTPException(status_code=404, detail="Chapter not found")
raise NotFoundError("Chapter not found")
if chapter.user_id != user_id:
raise HTTPException(status_code=403, detail="无权操作此章节")
raise AuthorizationError("无权操作此章节")
if not chapter.is_active:
raise HTTPException(status_code=404, detail="Chapter not found")
raise NotFoundError("Chapter not found")
try:
await repo.replace_chapter_story_links_async(
self._db,
chapter_id=chapter_id,
user_id=user_id,
story_ids=story_ids,
)
async with transactional(self._db):
await repo.replace_chapter_story_links_async(
self._db,
chapter_id=chapter_id,
user_id=user_id,
story_ids=story_ids,
)
ch = await repo.get_chapter_with_story_links_for_compose(
chapter_id, self._db
)
if not ch:
raise NotFoundError("Chapter not found")
if not ch.story_links:
md = ""
else:
md = materialize_chapter_markdown_from_loaded_chapter(ch)
await repo.append_chapter_compose_version_async(self._db, ch, md)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
ch = await repo.get_chapter_with_story_links_for_compose(chapter_id, self._db)
if not ch:
raise HTTPException(status_code=404, detail="Chapter not found")
if not ch.story_links:
md = ""
else:
md = materialize_chapter_markdown_from_loaded_chapter(ch)
await repo.append_chapter_compose_version_async(self._db, ch, md)
await self._db.commit()
msg = str(exc)
if "not found" in msg.lower() or "access denied" in msg.lower():
raise NotFoundError(msg) from exc
raise BadRequestError(msg) from exc
return {"status": "ok", "chapter_id": chapter_id, "story_count": len(story_ids)}
async def get_or_create_memoir_state(self, user_id: str) -> MemoirStateSchema:
from app.features.memoir.state_service import get_or_create_state
return await get_or_create_state(user_id, self._db)
async def get_memoir_state(self, user_id: str) -> dict:
from app.features.memoir.state_service import get_or_create_state
@@ -316,14 +326,14 @@ class MemoirService:
async def mark_memoir_read(self, user_id: str) -> dict:
stmt = select(Chapter).where(Chapter.user_id == user_id, Chapter.is_new == True)
result = await self._db.execute(stmt)
for chapter in result.scalars().all():
chapter.is_new = False
stmt_book = (
select(Book).where(Book.user_id == user_id).order_by(Book.updated_at.desc())
)
result_book = await self._db.execute(stmt_book)
book = result_book.scalar_one_or_none()
if book:
book.has_update = False
await self._db.commit()
async with transactional(self._db):
for chapter in result.scalars().all():
chapter.is_new = False
if book:
book.has_update = False
return {"status": "ok"}