| """ |
| Database-aware Project and TODO Board Management Service |
| Replaces the in-memory ProjectManager with persistent database storage |
| """ |
|
|
| import time |
| import logging |
| from typing import Dict, List, Optional, Any |
| from datetime import datetime |
| from sqlalchemy.ext.asyncio import AsyncSession |
|
|
| from ..api.models import ( |
| PPTProject, TodoBoard, TodoStage, ProjectListResponse, |
| PPTGenerationRequest, PPTOutline, EnhancedPPTOutline |
| ) |
| from ..database.service import DatabaseService |
| from ..database.database import get_async_db |
|
|
| |
| logger = logging.getLogger(__name__) |
|
|
|
|
| class DatabaseProjectManager: |
| """Database-aware project manager with persistent storage""" |
|
|
| def __init__(self): |
| pass |
|
|
| async def _get_db_service(self) -> DatabaseService: |
| """Get database service instance with a new session""" |
| from ..database.database import AsyncSessionLocal |
| session = AsyncSessionLocal() |
| return DatabaseService(session) |
| |
| async def create_project(self, request: PPTGenerationRequest, user_id: Optional[int] = None) -> PPTProject: |
| """Create a new PPT project with TODO board""" |
| db_service = await self._get_db_service() |
| try: |
| project = await db_service.create_project(request, user_id=user_id) |
| logger.info(f"Created project {project.project_id}: {project.title}") |
| return project |
| finally: |
| await db_service.session.close() |
| |
| async def update_todo_board_after_requirements( |
| self, |
| project_id: str, |
| confirmed_requirements: Dict[str, Any], |
| user_id: Optional[int] = None, |
| ) -> bool: |
| """Update TODO board after requirements confirmation. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| |
| project = await db_service.get_project(project_id, user_id=user_id) |
| if not project: |
| return False |
|
|
| |
| await db_service.project_repo.update(project_id, { |
| "confirmed_requirements": confirmed_requirements |
| }, user_id=user_id) |
|
|
| |
| await db_service.update_stage_status( |
| project_id, |
| "requirements_confirmation", |
| "completed", |
| 100.0, |
| confirmed_requirements, |
| user_id=user_id, |
| ) |
|
|
| logger.info(f"Updated TODO board for project {project_id} after requirements confirmation") |
| return True |
|
|
| except Exception as e: |
| logger.error(f"Error updating TODO board: {e}") |
| return False |
| finally: |
| await db_service.session.close() |
|
|
| async def update_todo_board_with_confirmed_requirements( |
| self, |
| project_id: str, |
| confirmed_requirements: Dict[str, Any], |
| user_id: Optional[int] = None, |
| ) -> bool: |
| """Compatibility method for EnhancedPPTService""" |
| return await self.update_todo_board_after_requirements(project_id, confirmed_requirements, user_id=user_id) |
| |
| async def get_project(self, project_id: str, user_id: Optional[int] = None) -> Optional[PPTProject]: |
| """Get project by ID. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| return await db_service.get_project(project_id, user_id=user_id) |
| finally: |
| await db_service.session.close() |
| |
| async def list_projects(self, page: int = 1, page_size: int = 10, |
| status: Optional[str] = None, |
| user_id: Optional[int] = None) -> ProjectListResponse: |
| """List projects with pagination. If user_id is provided, filters by owner.""" |
| db_service = await self._get_db_service() |
| try: |
| return await db_service.list_projects(page, page_size, status, user_id=user_id) |
| finally: |
| await db_service.session.close() |
| |
| async def update_project_status(self, project_id: str, status: str, user_id: Optional[int] = None) -> bool: |
| """Update project status. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.update_project_status(project_id, status, user_id=user_id) |
|
|
| if success: |
| logger.info(f"Updated project {project_id} status to {status}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
| |
| async def get_todo_board(self, project_id: str, user_id: Optional[int] = None) -> Optional[TodoBoard]: |
| """Get TODO board for project. If user_id is provided, enforces ownership.""" |
| project = await self.get_project(project_id, user_id=user_id) |
| return project.todo_board if project else None |
| |
| async def update_stage_status(self, project_id: str, stage_id: str, |
| status: str, progress: float = None, |
| result: Dict[str, Any] = None, |
| user_id: Optional[int] = None) -> bool: |
| """Update stage status in TODO board. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.update_stage_status(project_id, stage_id, status, progress, result, user_id=user_id) |
|
|
| if success: |
| logger.info(f"Updated stage {stage_id} to {status}, progress: {progress}%") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
| |
| async def save_project_outline(self, project_id: str, outline: Dict[str, Any]) -> bool: |
| """Save project outline""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.save_project_outline(project_id, outline) |
|
|
| if success: |
| logger.info(f"Saved outline for project {project_id}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
| |
| async def save_project_slides(self, project_id: str, slides_html: str, |
| slides_data: List[Dict[str, Any]] = None) -> bool: |
| """Save project slides using optimized batch update""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.save_project_slides(project_id, slides_html, slides_data) |
|
|
| if success: |
| logger.info(f"Saved slides for project {project_id}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
|
|
| async def batch_save_slides(self, project_id: str, slides_data: List[Dict[str, Any]]) -> bool: |
| """批量保存幻灯片 - 高效版本""" |
| db_service = await self._get_db_service() |
| try: |
| |
| slides_records = [] |
| for i, slide_data in enumerate(slides_data): |
| slide_record = { |
| "project_id": project_id, |
| "slide_index": i, |
| "slide_id": slide_data.get("slide_id", f"slide_{i}"), |
| "title": slide_data.get("title", f"Slide {i+1}"), |
| "content_type": slide_data.get("content_type", "content"), |
| "html_content": slide_data.get("html_content", ""), |
| "slide_metadata": slide_data.get("metadata", {}), |
| "is_user_edited": slide_data.get("is_user_edited", False) |
| } |
| slides_records.append(slide_record) |
|
|
| |
| success = await db_service.slide_repo.batch_upsert_slides(project_id, slides_records) |
|
|
| if success: |
| logger.info(f"Batch saved {len(slides_data)} slides for project {project_id}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
|
|
| async def replace_all_project_slides(self, project_id: str, slides_html: str, |
| slides_data: List[Dict[str, Any]] = None) -> bool: |
| """完全替换项目的所有幻灯片 - 用于重新生成PPT等场景""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.replace_all_project_slides(project_id, slides_html, slides_data) |
|
|
| if success: |
| logger.info(f"Replaced all slides for project {project_id}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
|
|
| async def cleanup_excess_slides( |
| self, |
| project_id: str, |
| current_slide_count: int, |
| user_id: Optional[int] = None, |
| ) -> int: |
| """清理多余的幻灯片。If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| deleted_count = await db_service.cleanup_excess_slides( |
| project_id, |
| current_slide_count, |
| user_id=user_id, |
| ) |
| logger.info(f"Cleaned up {deleted_count} excess slides for project {project_id}") |
| return deleted_count |
| finally: |
| await db_service.session.close() |
|
|
| async def save_single_slide(self, project_id: str, slide_index: int, slide_data: Dict[str, Any], skip_if_user_edited: bool = False) -> bool: |
| """Save a single slide to database immediately |
| |
| Args: |
| skip_if_user_edited: If True, skip updating slides that have is_user_edited=True. |
| Generator should pass True, editor should pass False. |
| """ |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.save_single_slide(project_id, slide_index, slide_data, skip_if_user_edited=skip_if_user_edited) |
|
|
| if success: |
| logger.info(f"Saved slide {slide_index + 1} for project {project_id}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
|
|
| async def get_single_slide(self, project_id: str, slide_index: int) -> Optional[Dict[str, Any]]: |
| """Get a single slide from database by project_id and slide_index""" |
| db_service = await self._get_db_service() |
| try: |
| slide = await db_service.slide_repo.get_slide_by_index(project_id, slide_index) |
| if slide: |
| return { |
| "page_number": slide.slide_index + 1, |
| "title": slide.title, |
| "html_content": slide.html_content, |
| "slide_type": slide.content_type, |
| "is_user_edited": slide.is_user_edited, |
| "slide_id": slide.slide_id, |
| "metadata": slide.slide_metadata |
| } |
| return None |
| finally: |
| await db_service.session.close() |
|
|
| async def list_slides(self, project_id: str, user_id: Optional[int] = None) -> List[Dict[str, Any]]: |
| """List all slides for a project in order. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| project = await db_service.get_project(project_id, user_id=user_id) |
| if not project: |
| return [] |
|
|
| slides = await db_service.slide_repo.get_slides_by_project_id(project_id) |
| results: List[Dict[str, Any]] = [] |
| for slide in slides: |
| results.append({ |
| "page_number": slide.slide_index + 1, |
| "title": slide.title, |
| "html_content": slide.html_content, |
| "slide_type": slide.content_type, |
| "is_user_edited": slide.is_user_edited, |
| "slide_id": slide.slide_id, |
| "metadata": slide.slide_metadata |
| }) |
| return results |
| finally: |
| await db_service.session.close() |
|
|
| async def get_stage_status(self, project_id: str, stage_id: str, user_id: Optional[int] = None) -> Optional[Dict[str, Any]]: |
| """Get a stage status by project_id and stage_id. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| project = await db_service.get_project(project_id, user_id=user_id) |
| if not project: |
| return None |
|
|
| stage = await db_service.todo_stage_repo.get_stage_by_project_and_stage(project_id, stage_id) |
| if not stage: |
| return None |
|
|
| return { |
| "stage_id": stage.stage_id, |
| "status": stage.status, |
| "progress": getattr(stage, "progress", None), |
| "result": getattr(stage, "result", None), |
| "updated_at": getattr(stage, "updated_at", None), |
| } |
| finally: |
| await db_service.session.close() |
|
|
| async def update_project_data(self, project_id: str, update_data: Dict[str, Any], user_id: Optional[int] = None) -> bool: |
| """Update project data without affecting individual slides. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.update_project(project_id, update_data, user_id=user_id) |
|
|
| if success: |
| logger.info(f"Updated project data for project {project_id}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
|
|
| async def update_project(self, project_id: str, update_data: Dict[str, Any], user_id: Optional[int] = None) -> bool: |
| """Alias for update_project_data for backward compatibility""" |
| return await self.update_project_data(project_id, update_data, user_id=user_id) |
| |
| async def save_project_version(self, project_id: str, version_data: Dict[str, Any], user_id: Optional[int] = None) -> bool: |
| """Save a version of the project. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.save_project_version(project_id, version_data, user_id=user_id) |
|
|
| if success: |
| logger.info(f"Saved new version for project {project_id}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
| |
| async def get_project_versions(self, project_id: str, user_id: Optional[int] = None) -> List[Dict[str, Any]]: |
| """Get all versions of a project. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| project = await db_service.get_project(project_id, user_id=user_id) |
|
|
| if not project: |
| return [] |
|
|
| return project.versions |
| finally: |
| await db_service.session.close() |
| |
| async def save_confirmed_requirements(self, project_id: str, requirements: Dict[str, Any], user_id: Optional[int] = None) -> bool: |
| """Save confirmed requirements for a project. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.project_repo.update(project_id, { |
| "confirmed_requirements": requirements |
| }, user_id=user_id) |
|
|
| if success: |
| logger.info(f"Saved confirmed requirements for project {project_id}") |
|
|
| return success is not None |
| finally: |
| await db_service.session.close() |
| |
| async def get_confirmed_requirements(self, project_id: str, user_id: Optional[int] = None) -> Optional[Dict[str, Any]]: |
| """Get confirmed requirements for a project. If user_id is provided, enforces ownership.""" |
| project = await self.get_project(project_id, user_id=user_id) |
| return project.confirmed_requirements if project else None |
| |
| async def delete_project(self, project_id: str, user_id: Optional[int] = None) -> bool: |
| """Delete a project. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.project_repo.delete(project_id, user_id=user_id) |
|
|
| if success: |
| logger.info(f"Deleted project {project_id}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
| |
| async def update_project_metadata(self, project_id: str, metadata: Dict[str, Any], user_id: Optional[int] = None) -> bool: |
| """Update project metadata. If user_id is provided, enforces ownership.""" |
| db_service = await self._get_db_service() |
| try: |
| success = await db_service.project_repo.update(project_id, {"project_metadata": metadata}, user_id=user_id) |
|
|
| if success: |
| logger.info(f"Updated metadata for project {project_id}") |
|
|
| return success |
| finally: |
| await db_service.session.close() |
|
|
| async def archive_project(self, project_id: str, user_id: Optional[int] = None) -> bool: |
| """Archive a project. If user_id is provided, enforces ownership.""" |
| return await self.update_project_status(project_id, "archived", user_id=user_id) |
| |
| async def complete_project(self, project_id: str, user_id: Optional[int] = None) -> bool: |
| """Mark project as completed. If user_id is provided, enforces ownership.""" |
| return await self.update_project_status(project_id, "completed", user_id=user_id) |
| |
| async def start_stage(self, project_id: str, stage_id: str) -> bool: |
| """Start a specific stage""" |
| return await self.update_stage_status(project_id, stage_id, "running", 0.0) |
| |
| async def complete_stage(self, project_id: str, stage_id: str, result: Dict[str, Any] = None) -> bool: |
| """Complete a specific stage""" |
| return await self.update_stage_status(project_id, stage_id, "completed", 100.0, result) |
| |
| async def fail_stage(self, project_id: str, stage_id: str, error_message: str) -> bool: |
| """Mark a stage as failed""" |
| result = {"error": error_message, "timestamp": time.time()} |
| return await self.update_stage_status(project_id, stage_id, "failed", 0.0, result) |
| |
| async def close(self): |
| """Close database connections - no longer needed as we use per-request sessions""" |
| pass |
|
|