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:
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user