文件
tennis-training-hub/drizzle/schema.ts
2026-03-15 00:12:26 +08:00

337 行
13 KiB
TypeScript

import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, json, float } from "drizzle-orm/mysql-core";
/**
* Core user table - supports both OAuth and simple username login
*/
export const users = mysqlTable("users", {
id: int("id").autoincrement().primaryKey(),
openId: varchar("openId", { length: 64 }).notNull().unique(),
name: text("name"),
email: varchar("email", { length: 320 }),
loginMethod: varchar("loginMethod", { length: 64 }),
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
/** Tennis skill level */
skillLevel: mysqlEnum("skillLevel", ["beginner", "intermediate", "advanced"]).default("beginner"),
/** User's training goals */
trainingGoals: text("trainingGoals"),
/** NTRP rating (1.0 - 5.0) */
ntrpRating: float("ntrpRating").default(1.5),
/** Total training sessions completed */
totalSessions: int("totalSessions").default(0),
/** Total training minutes */
totalMinutes: int("totalMinutes").default(0),
/** Current consecutive check-in streak */
currentStreak: int("currentStreak").default(0),
/** Longest ever streak */
longestStreak: int("longestStreak").default(0),
/** Total shots across all analyses */
totalShots: int("totalShots").default(0),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
});
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
/**
* Simple username-based login accounts
*/
export const usernameAccounts = mysqlTable("username_accounts", {
id: int("id").autoincrement().primaryKey(),
username: varchar("username", { length: 64 }).notNull().unique(),
userId: int("userId").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type UsernameAccount = typeof usernameAccounts.$inferSelect;
/**
* Training plans generated for users
*/
export const trainingPlans = mysqlTable("training_plans", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
title: varchar("title", { length: 256 }).notNull(),
skillLevel: mysqlEnum("skillLevel", ["beginner", "intermediate", "advanced"]).notNull(),
/** Plan duration in days */
durationDays: int("durationDays").notNull().default(7),
/** JSON array of training exercises */
exercises: json("exercises").notNull(),
/** Whether this plan is currently active */
isActive: int("isActive").notNull().default(1),
/** Auto-adjustment notes from AI analysis */
adjustmentNotes: text("adjustmentNotes"),
/** Plan generation version for tracking adjustments */
version: int("version").notNull().default(1),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type TrainingPlan = typeof trainingPlans.$inferSelect;
export type InsertTrainingPlan = typeof trainingPlans.$inferInsert;
/**
* Training videos uploaded by users
*/
export const trainingVideos = mysqlTable("training_videos", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
title: varchar("title", { length: 256 }).notNull(),
/** S3 file key */
fileKey: varchar("fileKey", { length: 512 }).notNull(),
/** CDN URL for the video */
url: text("url").notNull(),
/** Video format: webm or mp4 */
format: varchar("format", { length: 16 }).notNull(),
/** File size in bytes */
fileSize: int("fileSize"),
/** Duration in seconds */
duration: float("duration"),
/** Type of exercise in the video */
exerciseType: varchar("exerciseType", { length: 64 }),
/** Analysis status */
analysisStatus: mysqlEnum("analysisStatus", ["pending", "analyzing", "completed", "failed"]).default("pending"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type TrainingVideo = typeof trainingVideos.$inferSelect;
export type InsertTrainingVideo = typeof trainingVideos.$inferInsert;
/**
* Pose analysis results from MediaPipe - enhanced with tennis_analysis features
*/
export const poseAnalyses = mysqlTable("pose_analyses", {
id: int("id").autoincrement().primaryKey(),
videoId: int("videoId").notNull(),
userId: int("userId").notNull(),
/** Overall pose score (0-100) */
overallScore: float("overallScore"),
/** JSON object with detailed joint angles and metrics */
poseMetrics: json("poseMetrics"),
/** JSON array of detected issues */
detectedIssues: json("detectedIssues"),
/** JSON array of correction suggestions */
corrections: json("corrections"),
/** Exercise type analyzed */
exerciseType: varchar("exerciseType", { length: 64 }),
/** Number of frames analyzed */
framesAnalyzed: int("framesAnalyzed"),
/** --- tennis_analysis inspired fields --- */
/** Number of swings/shots detected */
shotCount: int("shotCount").default(0),
/** Average swing speed (estimated from keypoint displacement, px/frame) */
avgSwingSpeed: float("avgSwingSpeed"),
/** Max swing speed detected */
maxSwingSpeed: float("maxSwingSpeed"),
/** Total body movement distance in pixels */
totalMovementDistance: float("totalMovementDistance"),
/** Stroke consistency score (0-100) */
strokeConsistency: float("strokeConsistency"),
/** Footwork score (0-100) */
footworkScore: float("footworkScore"),
/** Fluidity/smoothness score (0-100) */
fluidityScore: float("fluidityScore"),
/** JSON array of key moments [{frame, type, description}] */
keyMoments: json("keyMoments"),
/** JSON array of movement trajectory points [{x, y, frame}] */
movementTrajectory: json("movementTrajectory"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type PoseAnalysis = typeof poseAnalyses.$inferSelect;
export type InsertPoseAnalysis = typeof poseAnalyses.$inferInsert;
/**
* Training session records for progress tracking
*/
export const trainingRecords = mysqlTable("training_records", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
planId: int("planId"),
/** Exercise name/type */
exerciseName: varchar("exerciseName", { length: 128 }).notNull(),
/** Duration in minutes */
durationMinutes: int("durationMinutes"),
/** Completion status */
completed: int("completed").notNull().default(0),
/** Optional notes */
notes: text("notes"),
/** Pose score if video was analyzed */
poseScore: float("poseScore"),
/** Date of training */
trainingDate: timestamp("trainingDate").defaultNow().notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type TrainingRecord = typeof trainingRecords.$inferSelect;
export type InsertTrainingRecord = typeof trainingRecords.$inferInsert;
/**
* NTRP Rating history - tracks rating changes over time
*/
export const ratingHistory = mysqlTable("rating_history", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
/** NTRP rating at this point */
rating: float("rating").notNull(),
/** What triggered this rating update */
reason: varchar("reason", { length: 256 }),
/** JSON breakdown of dimension scores */
dimensionScores: json("dimensionScores"),
/** Reference analysis ID if applicable */
analysisId: int("analysisId"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type RatingHistory = typeof ratingHistory.$inferSelect;
export type InsertRatingHistory = typeof ratingHistory.$inferInsert;
/**
* Daily check-in records for streak tracking
*/
export const dailyCheckins = mysqlTable("daily_checkins", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
/** Check-in date (YYYY-MM-DD stored as string for easy comparison) */
checkinDate: varchar("checkinDate", { length: 10 }).notNull(),
/** Current streak at the time of check-in */
streakCount: int("streakCount").notNull().default(1),
/** Optional notes for the day */
notes: text("notes"),
/** Training minutes logged this day */
minutesTrained: int("minutesTrained").default(0),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type DailyCheckin = typeof dailyCheckins.$inferSelect;
export type InsertDailyCheckin = typeof dailyCheckins.$inferInsert;
/**
* Achievement badges earned by users
*/
export const userBadges = mysqlTable("user_badges", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
/** Badge identifier key */
badgeKey: varchar("badgeKey", { length: 64 }).notNull(),
/** When the badge was earned */
earnedAt: timestamp("earnedAt").defaultNow().notNull(),
});
export type UserBadge = typeof userBadges.$inferSelect;
export type InsertUserBadge = typeof userBadges.$inferInsert;
/**
* Tutorial video library - professional coaching reference videos
*/
export const tutorialVideos = mysqlTable("tutorial_videos", {
id: int("id").autoincrement().primaryKey(),
title: varchar("title", { length: 256 }).notNull(),
category: varchar("category", { length: 64 }).notNull(),
skillLevel: mysqlEnum("skillLevel", ["beginner", "intermediate", "advanced"]).default("beginner"),
description: text("description"),
keyPoints: json("keyPoints"),
commonMistakes: json("commonMistakes"),
videoUrl: text("videoUrl"),
thumbnailUrl: text("thumbnailUrl"),
duration: int("duration"),
sortOrder: int("sortOrder").default(0),
isPublished: int("isPublished").default(1),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type TutorialVideo = typeof tutorialVideos.$inferSelect;
export type InsertTutorialVideo = typeof tutorialVideos.$inferInsert;
/**
* User tutorial progress tracking
*/
export const tutorialProgress = mysqlTable("tutorial_progress", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
tutorialId: int("tutorialId").notNull(),
watched: int("watched").default(0),
comparisonVideoId: int("comparisonVideoId"),
selfScore: float("selfScore"),
notes: text("notes"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type TutorialProgress = typeof tutorialProgress.$inferSelect;
export type InsertTutorialProgress = typeof tutorialProgress.$inferInsert;
/**
* Training reminders
*/
export const trainingReminders = mysqlTable("training_reminders", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
reminderType: varchar("reminderType", { length: 32 }).notNull(),
title: varchar("title", { length: 256 }).notNull(),
message: text("message"),
timeOfDay: varchar("timeOfDay", { length: 5 }).notNull(),
daysOfWeek: json("daysOfWeek").notNull(),
isActive: int("isActive").default(1),
lastTriggered: timestamp("lastTriggered"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type TrainingReminder = typeof trainingReminders.$inferSelect;
export type InsertTrainingReminder = typeof trainingReminders.$inferInsert;
/**
* Notification log
*/
export const notificationLog = mysqlTable("notification_log", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
reminderId: int("reminderId"),
notificationType: varchar("notificationType", { length: 32 }).notNull(),
title: varchar("title", { length: 256 }).notNull(),
message: text("message"),
isRead: int("isRead").default(0),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type NotificationLogEntry = typeof notificationLog.$inferSelect;
export type InsertNotificationLog = typeof notificationLog.$inferInsert;
/**
* Background task queue for long-running or retryable work.
*/
export const backgroundTasks = mysqlTable("background_tasks", {
id: varchar("id", { length: 36 }).primaryKey(),
userId: int("userId").notNull(),
type: mysqlEnum("type", [
"media_finalize",
"training_plan_generate",
"training_plan_adjust",
"analysis_corrections",
"pose_correction_multimodal",
]).notNull(),
status: mysqlEnum("status", ["queued", "running", "succeeded", "failed"]).notNull().default("queued"),
title: varchar("title", { length: 256 }).notNull(),
message: text("message"),
progress: int("progress").notNull().default(0),
payload: json("payload").notNull(),
result: json("result"),
error: text("error"),
attempts: int("attempts").notNull().default(0),
maxAttempts: int("maxAttempts").notNull().default(3),
workerId: varchar("workerId", { length: 96 }),
runAfter: timestamp("runAfter").defaultNow().notNull(),
lockedAt: timestamp("lockedAt"),
startedAt: timestamp("startedAt"),
completedAt: timestamp("completedAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type BackgroundTask = typeof backgroundTasks.$inferSelect;
export type InsertBackgroundTask = typeof backgroundTasks.$inferInsert;