Files
beabigegg aa987adfb9 後端代碼清理:移除冗餘註解和調試代碼
清理內容:
- 移除所有開發元資訊(Author, Version, DocID, Rationale等)
- 刪除註解掉的代碼片段(力導向演算法等24行)
- 移除調試用的 logger.debug 語句
- 簡化冗餘的內聯註解(emoji、"重要"等標註)
- 刪除 TDD 文件引用

清理檔案:
- backend/main.py - 移除調試日誌和元資訊
- backend/importer.py - 移除詳細類型檢查調試
- backend/export.py - 簡化 docstring
- backend/schemas.py - 移除元資訊
- backend/renderer.py - 移除 TDD 引用
- backend/renderer_timeline.py - 移除註解代碼和冗餘註解
- backend/path_planner.py - 簡化策略註解

保留內容:
- 所有函數的 docstring(功能說明、參數、返回值)
- 必要的業務邏輯註解
- 簡潔的模組功能說明

效果:
- 刪除約 100+ 行冗餘註解
- 代碼更加簡潔專業
- 功能完整性 100% 保留

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 12:22:29 +08:00

452 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
FastAPI 主程式
本模組提供時間軸設計工具的 REST API 服務。
"""
import os
import tempfile
from pathlib import Path
from typing import List, Optional
from datetime import datetime
import logging
from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from .schemas import (
Event, TimelineConfig, ExportOptions, Theme,
ImportResult, RenderResult, APIResponse,
ThemeStyle, ExportFormat
)
from .importer import CSVImporter, ImporterError
from .renderer_timeline import ClassicTimelineRenderer
from .export import TimelineExporter, ExportError
# 設定日誌
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 建立 FastAPI 應用
app = FastAPI(
title="TimeLine Designer API",
description="時間軸設計工具 REST API",
version="1.0.0",
docs_url="/api/docs",
redoc_url="/api/redoc"
)
# 設定 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 在生產環境應該限制為特定來源
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 全域儲存(簡單起見,使用記憶體儲存,實際應用應使用資料庫)
events_store: List[Event] = []
# 初始化服務
csv_importer = CSVImporter()
timeline_renderer = ClassicTimelineRenderer()
timeline_exporter = TimelineExporter()
# ==================== 健康檢查 ====================
@app.get("/health", tags=["System"])
async def health_check():
"""健康檢查端點"""
return APIResponse(
success=True,
message="Service is healthy",
data={
"version": "1.0.0",
"timestamp": datetime.now().isoformat()
}
)
# ==================== 匯入 API ====================
@app.post("/api/import", response_model=ImportResult, tags=["Import"])
async def import_events(file: UploadFile = File(...)):
"""
匯入事件資料
對應 SDD.md - POST /import
支援 CSV 和 XLSX 格式
Args:
file: 上傳的檔案
Returns:
ImportResult: 匯入結果
"""
try:
# 驗證檔案類型
if not file.filename:
raise HTTPException(status_code=400, detail="未提供檔案名稱")
file_ext = Path(file.filename).suffix.lower()
if file_ext not in ['.csv', '.xlsx', '.xls']:
raise HTTPException(
status_code=400,
detail=f"不支援的檔案格式: {file_ext},僅支援 CSV 和 XLSX"
)
# 儲存上傳檔案到臨時目錄
with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as tmp_file:
content = await file.read()
tmp_file.write(content)
tmp_path = tmp_file.name
try:
# 匯入資料
result = csv_importer.import_file(tmp_path)
if result.success:
# 更新全域儲存
global events_store
events_store = result.events
logger.info(f"成功匯入 {result.imported_count} 筆事件")
return result
finally:
# 清理臨時檔案
os.unlink(tmp_path)
except HTTPException:
# Re-raise HTTP exceptions (from validation)
raise
except ImporterError as e:
logger.error(f"匯入失敗: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"未預期的錯誤: {str(e)}")
raise HTTPException(status_code=500, detail=f"伺服器錯誤: {str(e)}")
# ==================== 事件管理 API ====================
@app.get("/api/events", response_model=List[Event], tags=["Events"])
async def get_events():
"""
取得事件列表
對應 SDD.md - GET /events
Returns:
List[Event]: 事件列表
"""
return events_store
@app.get("/api/events/raw", tags=["Events"])
async def get_raw_events():
"""
取得原始事件資料(用於前端 D3.js 渲染)
返回不經過任何布局計算的原始事件資料,
供前端 D3 Force-Directed Layout 使用。
Returns:
dict: 包含原始事件資料的字典
"""
return {
"success": True,
"events": [
{
"id": i,
"start": event.start.isoformat(),
"end": event.end.isoformat() if event.end else None,
"title": event.title,
"description": event.description or "",
"color": event.color or "#3B82F6",
"layer": i % 4 # 簡單的層級分配0-3 循環
}
for i, event in enumerate(events_store)
],
"count": len(events_store)
}
@app.post("/api/events", response_model=Event, tags=["Events"])
async def add_event(event: Event):
"""
新增單一事件
Args:
event: 事件物件
Returns:
Event: 新增的事件
"""
global events_store
events_store.append(event)
logger.info(f"新增事件: {event.id} - {event.title}")
return event
@app.delete("/api/events/{event_id}", tags=["Events"])
async def delete_event(event_id: str):
"""
刪除事件
Args:
event_id: 事件ID
Returns:
APIResponse: 操作結果
"""
global events_store
original_count = len(events_store)
events_store = [e for e in events_store if e.id != event_id]
if len(events_store) < original_count:
logger.info(f"刪除事件: {event_id}")
return APIResponse(success=True, message=f"成功刪除事件 {event_id}")
else:
raise HTTPException(status_code=404, detail=f"找不到事件: {event_id}")
@app.delete("/api/events", tags=["Events"])
async def clear_events():
"""
清空所有事件
Returns:
APIResponse: 操作結果
"""
global events_store
count = len(events_store)
events_store = []
logger.info(f"清空事件,共 {count}")
return APIResponse(success=True, message=f"成功清空 {count} 筆事件")
# ==================== 渲染 API ====================
class RenderRequest(BaseModel):
"""渲染請求模型"""
events: Optional[List[Event]] = None
config: TimelineConfig = TimelineConfig()
@app.post("/api/render", response_model=RenderResult, tags=["Render"])
async def render_timeline(request: RenderRequest):
"""
生成時間軸 JSON
對應 SDD.md - POST /render
生成 Plotly JSON 格式的時間軸資料
Args:
request: 渲染請求(可選事件列表與配置)
Returns:
RenderResult: Plotly JSON 資料
"""
try:
# 使用請求中的事件或全域事件
events = request.events if request.events is not None else events_store
if not events:
logger.warning("嘗試渲染空白事件列表")
# 渲染
result = timeline_renderer.render(events, request.config)
if result.success:
logger.info(f"成功渲染 {len(events)} 筆事件")
else:
logger.error("渲染失敗")
return result
except Exception as e:
logger.error(f"渲染錯誤: {str(e)}")
raise HTTPException(status_code=500, detail=f"渲染失敗: {str(e)}")
# ==================== 匯出 API ====================
class ExportRequest(BaseModel):
"""匯出請求模型"""
plotly_data: dict
plotly_layout: dict
options: ExportOptions
filename: Optional[str] = None
@app.post("/api/export", tags=["Export"])
async def export_timeline(request: ExportRequest, background_tasks: BackgroundTasks):
"""
導出時間軸圖
對應 SDD.md - POST /export
匯出為 PNG、PDF 或 SVG 格式
Args:
request: 匯出請求
background_tasks: 背景任務(用於清理臨時檔案)
Returns:
FileResponse: 圖檔
"""
try:
# 建立臨時輸出目錄
temp_dir = Path(tempfile.gettempdir()) / "timeline_exports"
temp_dir.mkdir(exist_ok=True)
# 生成檔名
if request.filename:
filename = request.filename
else:
filename = timeline_exporter.generate_default_filename(request.options.fmt)
output_path = temp_dir / filename
# 匯出
result_path = timeline_exporter.export_from_plotly_json(
request.plotly_data,
request.plotly_layout,
output_path,
request.options
)
logger.info(f"成功匯出: {result_path}")
# 設定背景任務清理檔案1小時後
def cleanup_file():
try:
if result_path.exists():
os.unlink(result_path)
logger.info(f"清理臨時檔案: {result_path}")
except Exception as e:
logger.warning(f"清理檔案失敗: {str(e)}")
background_tasks.add_task(cleanup_file)
# 回傳檔案
media_type_map = {
ExportFormat.PNG: "image/png",
ExportFormat.PDF: "application/pdf",
ExportFormat.SVG: "image/svg+xml",
}
return FileResponse(
path=str(result_path),
media_type=media_type_map.get(request.options.fmt, "application/octet-stream"),
filename=result_path.name
)
except ExportError as e:
logger.error(f"匯出失敗: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"未預期的錯誤: {str(e)}")
raise HTTPException(status_code=500, detail=f"伺服器錯誤: {str(e)}")
# ==================== 主題 API ====================
@app.get("/api/themes", response_model=List[Theme], tags=["Themes"])
async def get_themes():
"""
取得主題列表
對應 SDD.md - GET /themes
Returns:
List[Theme]: 主題列表
"""
themes = [
Theme(
name="現代風格",
style=ThemeStyle.MODERN,
primary_color="#3B82F6",
background_color="#FFFFFF",
text_color="#1F2937"
),
Theme(
name="經典風格",
style=ThemeStyle.CLASSIC,
primary_color="#6366F1",
background_color="#F9FAFB",
text_color="#374151"
),
Theme(
name="極簡風格",
style=ThemeStyle.MINIMAL,
primary_color="#000000",
background_color="#FFFFFF",
text_color="#000000"
),
Theme(
name="企業風格",
style=ThemeStyle.CORPORATE,
primary_color="#1F2937",
background_color="#F3F4F6",
text_color="#111827"
),
]
return themes
# ==================== 錯誤處理 ====================
@app.exception_handler(404)
async def not_found_handler(request, exc):
"""404 錯誤處理"""
return JSONResponse(
status_code=404,
content=APIResponse(
success=False,
message="找不到請求的資源",
error_code="NOT_FOUND"
).dict()
)
@app.exception_handler(500)
async def internal_error_handler(request, exc):
"""500 錯誤處理"""
logger.error(f"內部伺服器錯誤: {str(exc)}")
return JSONResponse(
status_code=500,
content=APIResponse(
success=False,
message="內部伺服器錯誤",
error_code="INTERNAL_ERROR"
).dict()
)
# ==================== 啟動事件 ====================
@app.on_event("startup")
async def startup_event():
"""應用啟動時執行"""
logger.info("TimeLine Designer API 啟動")
logger.info("API 文檔: http://localhost:8000/api/docs")
@app.on_event("shutdown")
async def shutdown_event():
"""應用關閉時執行"""
logger.info("TimeLine Designer API 關閉")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")