1aaef71f52
Document-driven MVP with FastAPI backend, Vue H5, WeChat mini shell, product demo, and Docker dev stack. Co-authored-by: Cursor <cursoragent@cursor.com>
149 lines
4.5 KiB
Python
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()
|