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:
john
2026-07-23 11:42:40 +08:00
commit 1aaef71f52
116 changed files with 10550 additions and 0 deletions
+148
View File
@@ -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()