Initial commit: Happy Up monorepo through Sprint 5.
Document-driven MVP with FastAPI backend, Vue H5, WeChat mini shell, product demo, and Docker dev stack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.ai.pose_analyzer import analyze_task
|
||||
from app.config import settings
|
||||
from app.db.models import AnalysisTask, Report, Video
|
||||
from app.mock_data import MOCK_MOVEMENT_REPORT, MOCK_REPORT
|
||||
from app.queue.analysis_queue import get_analysis_queue
|
||||
from app.schemas.models import AnalysisTaskCreateRequest, TaskStatus, TaskType
|
||||
from app.services import oss as oss_service
|
||||
from app.services.videos import verify_child_owner
|
||||
|
||||
DISCLAIMER_SCREENING = "本报告用于健康管理建议,不构成医疗诊断。"
|
||||
DISCLAIMER_MOVEMENT = "本报告用于运动训练反馈,不构成医疗诊断。"
|
||||
|
||||
|
||||
def task_to_dict(task: AnalysisTask) -> dict:
|
||||
report_id = None
|
||||
if task.result and isinstance(task.result, dict):
|
||||
report_id = task.result.get("reportId")
|
||||
return {
|
||||
"id": task.id,
|
||||
"status": task.status,
|
||||
"progress": task.progress,
|
||||
"errorCode": task.error_code,
|
||||
"reportId": report_id,
|
||||
"retryCount": task.retry_count,
|
||||
}
|
||||
|
||||
|
||||
def build_screening_report_payload(child_id: int, task_id: int) -> dict:
|
||||
data = {**MOCK_REPORT, "childId": child_id, "taskId": task_id}
|
||||
return data
|
||||
|
||||
|
||||
def build_movement_report_payload(child_id: int, task_id: int) -> dict:
|
||||
return {**MOCK_MOVEMENT_REPORT, "childId": child_id, "taskId": task_id}
|
||||
|
||||
|
||||
def create_report_for_task(db: Session, task: AnalysisTask, analysis: dict) -> Report:
|
||||
report_data = analysis.get("report", {})
|
||||
if task.task_type == TaskType.movement_scoring.value:
|
||||
payload = {**MOCK_MOVEMENT_REPORT, **report_data}
|
||||
report = Report(
|
||||
child_id=task.child_id,
|
||||
task_id=task.id,
|
||||
report_type="movement_scoring",
|
||||
risk_level="medium",
|
||||
summary="跟练打卡报告已生成",
|
||||
metrics=[payload],
|
||||
recommendations=["继续保持训练节奏", "28 天后安排复测"],
|
||||
disclaimer=DISCLAIMER_MOVEMENT,
|
||||
status="published",
|
||||
)
|
||||
else:
|
||||
payload = {**MOCK_REPORT, **report_data, "childId": task.child_id, "taskId": task.id}
|
||||
report = Report(
|
||||
child_id=task.child_id,
|
||||
task_id=task.id,
|
||||
report_type="posture_screening",
|
||||
risk_level=payload.get("riskLevel", "medium"),
|
||||
summary=payload.get("summary", MOCK_REPORT["summary"]),
|
||||
metrics=payload.get("metrics", MOCK_REPORT["metrics"]),
|
||||
recommendations=payload.get("recommendations", MOCK_REPORT["recommendations"]),
|
||||
disclaimer=payload.get("disclaimer", DISCLAIMER_SCREENING),
|
||||
status="published",
|
||||
)
|
||||
db.add(report)
|
||||
db.flush()
|
||||
return report
|
||||
|
||||
|
||||
def process_analysis_task(db: Session, task_id: int) -> AnalysisTask | None:
|
||||
task = db.get(AnalysisTask, task_id)
|
||||
if not task or task.status in (TaskStatus.SUCCEEDED.value, TaskStatus.CANCELLED.value):
|
||||
return task
|
||||
|
||||
task.status = TaskStatus.PROCESSING.value
|
||||
task.progress = 35
|
||||
task.started_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
|
||||
video = db.get(Video, task.video_id)
|
||||
video_path = None
|
||||
if video:
|
||||
video_path = oss_service.resolve_local_path(video.object_key)
|
||||
|
||||
analysis = analyze_task(task.task_type, video_path)
|
||||
task.model_version = analysis.get("modelVersion", "mock-v1")
|
||||
task.progress = 80
|
||||
db.commit()
|
||||
|
||||
report = create_report_for_task(db, task, analysis)
|
||||
task.status = TaskStatus.SUCCEEDED.value
|
||||
task.progress = 100
|
||||
task.finished_at = datetime.now(timezone.utc)
|
||||
task.result = {
|
||||
"reportId": report.id,
|
||||
"engine": analysis.get("engine"),
|
||||
"confidence": analysis.get("confidence"),
|
||||
}
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
|
||||
def enqueue_task(task_id: int, db: Session | None = None) -> None:
|
||||
queue = get_analysis_queue()
|
||||
queue.push(task_id)
|
||||
if not settings.analysis_inline_process:
|
||||
return
|
||||
if db is not None:
|
||||
process_analysis_task(db, task_id)
|
||||
return
|
||||
from app.db.session import SessionLocal
|
||||
|
||||
local_db = SessionLocal()
|
||||
try:
|
||||
process_analysis_task(local_db, task_id)
|
||||
finally:
|
||||
local_db.close()
|
||||
|
||||
|
||||
def create_analysis_task(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
body: AnalysisTaskCreateRequest,
|
||||
idempotency_key: str | None,
|
||||
) -> tuple[AnalysisTask | None, str | None, AnalysisTask | None]:
|
||||
child = verify_child_owner(db, user_id, body.child_id)
|
||||
if not child:
|
||||
return None, "child_not_found", None
|
||||
|
||||
video = db.get(Video, body.video_id)
|
||||
if not video or video.child_id != body.child_id:
|
||||
return None, "video_not_found", None
|
||||
|
||||
if idempotency_key:
|
||||
existing = db.scalar(
|
||||
select(AnalysisTask).where(
|
||||
AnalysisTask.child_id == body.child_id,
|
||||
AnalysisTask.video_id == body.video_id,
|
||||
AnalysisTask.task_type == body.task_type.value,
|
||||
AnalysisTask.idempotency_key == idempotency_key,
|
||||
)
|
||||
)
|
||||
if existing:
|
||||
return existing, "duplicate", existing
|
||||
|
||||
task = AnalysisTask(
|
||||
child_id=body.child_id,
|
||||
video_id=body.video_id,
|
||||
task_type=body.task_type.value,
|
||||
status=TaskStatus.QUEUED.value,
|
||||
progress=0,
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
enqueue_task(task.id, db=db)
|
||||
db.refresh(task)
|
||||
return task, None, None
|
||||
|
||||
|
||||
def get_task_for_user(db: Session, user_id: int, task_id: int) -> AnalysisTask | None:
|
||||
task = db.get(AnalysisTask, task_id)
|
||||
if not task:
|
||||
return None
|
||||
child = verify_child_owner(db, user_id, task.child_id)
|
||||
if not child:
|
||||
return None
|
||||
return task
|
||||
|
||||
|
||||
def cancel_task(db: Session, user_id: int, task_id: int) -> AnalysisTask | None:
|
||||
task = get_task_for_user(db, user_id, task_id)
|
||||
if not task:
|
||||
return None
|
||||
if task.status in (TaskStatus.SUCCEEDED.value, TaskStatus.CANCELLED.value):
|
||||
return task
|
||||
task.status = TaskStatus.CANCELLED.value
|
||||
task.finished_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
|
||||
def handle_webhook(db: Session, task_id: int, status: str) -> None:
|
||||
task = db.get(AnalysisTask, task_id)
|
||||
if not task:
|
||||
return
|
||||
if status == TaskStatus.SUCCEEDED.value:
|
||||
process_analysis_task(db, task_id)
|
||||
elif status == TaskStatus.FAILED.value:
|
||||
task.status = TaskStatus.FAILED.value
|
||||
task.error_code = "worker_failed"
|
||||
task.finished_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user