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,148 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user