people-reading/backend/app/api/v1/endpoints/reports.py
2026-05-12 20:50:15 +08:00

195 lines
7.5 KiB
Python

from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Response, status
from sqlalchemy import desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import AsyncSessionLocal, get_db
from app.core.security import get_current_user
from app.models.palm_report import PalmReport
from app.models.share_image_job import ShareImageJob
from app.models.uploaded_image import UploadedImage
from app.models.user import User
from app.schemas.share_image import ShareImageJobResponse
from app.schemas.report import ReportCreate, ReportDetail, ReportSummary
from app.services.image_service import ImageService
from app.services.quota_service import QuotaService
from app.services.report_service import ReportService
from app.services.share_poster_service import SharePosterService
router = APIRouter()
async def generate_report_task(report_id: str) -> None:
async with AsyncSessionLocal() as session:
await ReportService().generate(session, report_id)
async def generate_share_image_task(job_id: str) -> None:
async with AsyncSessionLocal() as session:
result = await session.execute(select(ShareImageJob).where(ShareImageJob.id == job_id))
job = result.scalar_one()
job.status = "processing"
await session.flush()
try:
report_result = await session.execute(select(PalmReport).where(PalmReport.id == job.report_id))
report = report_result.scalar_one()
image_result = await session.execute(select(UploadedImage).where(UploadedImage.id == report.image_id))
image = image_result.scalar_one_or_none()
png = await SharePosterService().render_ai_or_fallback(report, image)
share_dir = Path("storage/share_images")
share_dir.mkdir(parents=True, exist_ok=True)
storage_key = f"{job.id}.png"
(share_dir / storage_key).write_bytes(png)
job.storage_key = storage_key
job.status = "completed"
job.error_message = None
except Exception as exc:
job.status = "failed"
job.error_message = str(exc)
await session.commit()
@router.post("", response_model=ReportDetail, status_code=status.HTTP_201_CREATED)
async def create_report(
payload: ReportCreate,
background_tasks: BackgroundTasks,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
image_result = await db.execute(
select(UploadedImage).where(UploadedImage.id == payload.image_id, UploadedImage.user_id == user.id)
)
image = image_result.scalar_one_or_none()
if image is None:
raise HTTPException(status_code=404, detail="Image not found")
await QuotaService().consume(db, user.id)
report = PalmReport(user_id=user.id, image_id=image.id, hand_side=payload.hand_side, status="pending")
db.add(report)
await db.flush()
await db.refresh(report)
await db.commit()
background_tasks.add_task(generate_report_task, report.id)
return report
@router.get("/share-image-jobs/{job_id}", response_model=ShareImageJobResponse)
async def get_share_image_job(
job_id: str,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
job = await _get_owned_share_job(db, job_id, user.id)
return job
@router.get("/share-image-jobs/{job_id}/image")
async def get_share_image_job_image(
job_id: str,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
job = await _get_owned_share_job(db, job_id, user.id)
if job.status != "completed" or not job.storage_key:
raise HTTPException(status_code=400, detail="Share image is not ready")
path = Path("storage/share_images") / job.storage_key
if not path.exists():
raise HTTPException(status_code=404, detail="Share image file not found")
return Response(content=path.read_bytes(), media_type="image/png")
@router.get("", response_model=list[ReportSummary])
async def list_reports(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(PalmReport).where(PalmReport.user_id == user.id).order_by(desc(PalmReport.created_at)).limit(50)
)
reports = result.scalars().all()
return [
ReportSummary(
id=report.id,
status=report.status,
hand_side=report.hand_side,
created_at=report.created_at,
overall_summary=(report.report_data or {}).get("overall_summary") if report.report_data else None,
)
for report in reports
]
@router.get("/{report_id}", response_model=ReportDetail)
async def get_report(report_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
report = await _get_owned_report(db, report_id, user.id)
return report
@router.post("/{report_id}/share-image-jobs", response_model=ShareImageJobResponse, status_code=status.HTTP_201_CREATED)
async def create_share_image_job(
report_id: str,
background_tasks: BackgroundTasks,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
report = await _get_owned_report(db, report_id, user.id)
if report.status != "completed" or not report.report_data:
raise HTTPException(status_code=400, detail="Report is not ready")
job = ShareImageJob(user_id=user.id, report_id=report.id, status="pending")
db.add(job)
await db.flush()
await db.refresh(job)
await db.commit()
background_tasks.add_task(generate_share_image_task, job.id)
return job
@router.get("/{report_id}/share-image")
async def get_report_share_image(
report_id: str,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
report = await _get_owned_report(db, report_id, user.id)
if report.status != "completed" or not report.report_data:
raise HTTPException(status_code=400, detail="Report is not ready")
image_result = await db.execute(select(UploadedImage).where(UploadedImage.id == report.image_id))
image = image_result.scalar_one_or_none()
png = await SharePosterService().render_ai_or_fallback(report, image)
return Response(
content=png,
media_type="image/png",
headers={"Content-Disposition": f'inline; filename="palm-report-{report.id}.png"'},
)
@router.delete("/{report_id}")
async def delete_report(report_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
report = await _get_owned_report(db, report_id, user.id)
image_result = await db.execute(select(UploadedImage).where(UploadedImage.id == report.image_id))
image = image_result.scalar_one_or_none()
await db.delete(report)
await db.flush()
if image:
ImageService().delete(image.storage_key)
await db.delete(image)
return {"status": "deleted"}
async def _get_owned_report(db: AsyncSession, report_id: str, user_id: str) -> PalmReport:
result = await db.execute(select(PalmReport).where(PalmReport.id == report_id, PalmReport.user_id == user_id))
report = result.scalar_one_or_none()
if report is None:
raise HTTPException(status_code=404, detail="Report not found")
return report
async def _get_owned_share_job(db: AsyncSession, job_id: str, user_id: str) -> ShareImageJob:
result = await db.execute(select(ShareImageJob).where(ShareImageJob.id == job_id, ShareImageJob.user_id == user_id))
job = result.scalar_one_or_none()
if job is None:
raise HTTPException(status_code=404, detail="Share image job not found")
return job