Files
john 1aaef71f52 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>
2026-07-23 11:42:40 +08:00

149 lines
4.5 KiB
Python

from datetime import datetime, timezone
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.db.models import Exercise, TrainingPlan, TrainingRecord
from app.schemas.models import TrainingPlanCreateRequest, TrainingRecordCreateRequest
from app.services.videos import verify_child_owner
DEFAULT_EXERCISES = [
{"id": 1, "name": "肩胛稳定训练", "durationMinutes": 5},
{"id": 2, "name": "颈后肌群拉伸", "sets": 3},
]
def plan_to_dict(plan: TrainingPlan) -> dict:
detail = plan.plan_detail or {}
records_count = detail.get("completedDays", 0)
current_day = min(plan.cycle_days, records_count + 1)
return {
"id": plan.id,
"status": plan.status,
"detail": {
**detail,
"goal": plan.goal,
"cycleDays": plan.cycle_days,
"currentDay": current_day,
"childId": plan.child_id,
"reportId": plan.report_id,
},
"startedAt": plan.started_at.isoformat() if plan.started_at else None,
"endedAt": plan.ended_at.isoformat() if plan.ended_at else None,
}
def build_plan_detail(body: TrainingPlanCreateRequest) -> dict:
exercises = DEFAULT_EXERCISES
if body.exercise_ids:
exercises = [{"id": eid, "name": f"动作 #{eid}"} for eid in body.exercise_ids]
return {
"goal": body.goal,
"cycleDays": body.cycle_days,
"currentDay": 1,
"completedDays": 0,
"exercises": exercises,
"constraints": body.constraints or {},
}
def get_plan_for_user(db: Session, user_id: int, plan_id: int) -> TrainingPlan | None:
plan = db.get(TrainingPlan, plan_id)
if not plan or plan.status == "cancelled":
return None
if not verify_child_owner(db, user_id, plan.child_id):
return None
return plan
def get_active_plan_for_child(db: Session, user_id: int, child_id: int) -> TrainingPlan | None:
if not verify_child_owner(db, user_id, child_id):
return None
return db.scalar(
select(TrainingPlan)
.where(TrainingPlan.child_id == child_id, TrainingPlan.status == "active")
.order_by(TrainingPlan.id.desc())
)
def list_plans_for_child(db: Session, user_id: int, child_id: int) -> list[TrainingPlan] | None:
if not verify_child_owner(db, user_id, child_id):
return None
return list(
db.scalars(
select(TrainingPlan)
.where(TrainingPlan.child_id == child_id, TrainingPlan.status != "cancelled")
.order_by(TrainingPlan.id.desc())
.limit(10)
).all()
)
def create_plan(db: Session, user_id: int, body: TrainingPlanCreateRequest) -> TrainingPlan | None:
if not verify_child_owner(db, user_id, body.child_id):
return None
now = datetime.now(timezone.utc)
plan = TrainingPlan(
child_id=body.child_id,
report_id=body.report_id,
goal=body.goal,
cycle_days=body.cycle_days,
status="active",
plan_detail=build_plan_detail(body),
started_at=now,
)
db.add(plan)
db.commit()
db.refresh(plan)
return plan
def create_record(
db: Session, user_id: int, plan_id: int, body: TrainingRecordCreateRequest
) -> TrainingRecord | None:
plan = get_plan_for_user(db, user_id, plan_id)
if not plan:
return None
feedback = {"media": body.media} if body.media else None
record = TrainingRecord(
plan_id=plan_id,
child_id=plan.child_id,
exercise_id=body.exercise_id,
completed=body.completed,
score=body.score,
duration_seconds=body.duration_seconds,
feedback=feedback,
note=body.note,
)
db.add(record)
detail = dict(plan.plan_detail or {})
detail["completedDays"] = detail.get("completedDays", 0) + (1 if body.completed else 0)
detail["lastScore"] = body.score
detail["lastRecordId"] = None
plan.plan_detail = detail
db.commit()
db.refresh(record)
detail["lastRecordId"] = record.id
plan.plan_detail = detail
db.commit()
return record
def ensure_default_exercises(db: Session) -> None:
existing = db.scalar(select(func.count()).select_from(Exercise))
if existing:
return
for item in DEFAULT_EXERCISES:
db.add(
Exercise(
name=item["name"],
category="posture",
target_issue="头前伸/高低肩",
duration_seconds=(item.get("durationMinutes", 3) * 60),
)
)
db.commit()