Implement live analysis achievements and admin console

这个提交包含在:
cryptocommuniums-afk
2026-03-15 01:39:34 +08:00
父节点 d1b6603061
当前提交 edc66ea5bc
修改 23 个文件,包含 4033 行新增1022 行删除

查看文件

@@ -1,4 +1,4 @@
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, json, float } from "drizzle-orm/mysql-core";
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, json, float, uniqueIndex } from "drizzle-orm/mysql-core";
/**
* Core user table - supports both OAuth and simple username login
@@ -152,6 +152,18 @@ export const trainingRecords = mysqlTable("training_records", {
planId: int("planId"),
/** Exercise name/type */
exerciseName: varchar("exerciseName", { length: 128 }).notNull(),
exerciseType: varchar("exerciseType", { length: 64 }),
/** Source of the training fact */
sourceType: varchar("sourceType", { length: 32 }).default("manual"),
/** Reference id from source system */
sourceId: varchar("sourceId", { length: 64 }),
/** Optional linked video */
videoId: int("videoId"),
/** Optional linked plan match */
linkedPlanId: int("linkedPlanId"),
matchConfidence: float("matchConfidence"),
actionCount: int("actionCount").default(0),
metadata: json("metadata"),
/** Duration in minutes */
durationMinutes: int("durationMinutes"),
/** Completion status */
@@ -168,6 +180,94 @@ export const trainingRecords = mysqlTable("training_records", {
export type TrainingRecord = typeof trainingRecords.$inferSelect;
export type InsertTrainingRecord = typeof trainingRecords.$inferInsert;
/**
* Live analysis sessions captured from the realtime camera workflow.
*/
export const liveAnalysisSessions = mysqlTable("live_analysis_sessions", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
title: varchar("title", { length: 256 }).notNull(),
sessionMode: mysqlEnum("sessionMode", ["practice", "pk"]).default("practice").notNull(),
status: mysqlEnum("status", ["active", "completed", "aborted"]).default("completed").notNull(),
startedAt: timestamp("startedAt").defaultNow().notNull(),
endedAt: timestamp("endedAt"),
durationMs: int("durationMs").default(0).notNull(),
dominantAction: varchar("dominantAction", { length: 64 }),
overallScore: float("overallScore"),
postureScore: float("postureScore"),
balanceScore: float("balanceScore"),
techniqueScore: float("techniqueScore"),
footworkScore: float("footworkScore"),
consistencyScore: float("consistencyScore"),
unknownActionRatio: float("unknownActionRatio"),
totalSegments: int("totalSegments").default(0).notNull(),
effectiveSegments: int("effectiveSegments").default(0).notNull(),
totalActionCount: int("totalActionCount").default(0).notNull(),
videoId: int("videoId"),
videoUrl: text("videoUrl"),
summary: text("summary"),
feedback: json("feedback"),
metrics: json("metrics"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type LiveAnalysisSession = typeof liveAnalysisSessions.$inferSelect;
export type InsertLiveAnalysisSession = typeof liveAnalysisSessions.$inferInsert;
/**
* Action segments extracted from a realtime analysis session.
*/
export const liveActionSegments = mysqlTable("live_action_segments", {
id: int("id").autoincrement().primaryKey(),
sessionId: int("sessionId").notNull(),
actionType: varchar("actionType", { length: 64 }).notNull(),
isUnknown: int("isUnknown").default(0).notNull(),
startMs: int("startMs").notNull(),
endMs: int("endMs").notNull(),
durationMs: int("durationMs").notNull(),
confidenceAvg: float("confidenceAvg"),
score: float("score"),
peakScore: float("peakScore"),
frameCount: int("frameCount").default(0).notNull(),
issueSummary: json("issueSummary"),
keyFrames: json("keyFrames"),
clipLabel: varchar("clipLabel", { length: 128 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
}, (table) => ({
sessionIndex: uniqueIndex("live_action_segments_session_start_idx").on(table.sessionId, table.startMs),
}));
export type LiveActionSegment = typeof liveActionSegments.$inferSelect;
export type InsertLiveActionSegment = typeof liveActionSegments.$inferInsert;
/**
* Daily training aggregate used for streaks, achievements and daily NTRP refresh.
*/
export const dailyTrainingAggregates = mysqlTable("daily_training_aggregates", {
id: int("id").autoincrement().primaryKey(),
dayKey: varchar("dayKey", { length: 32 }).notNull().unique(),
userId: int("userId").notNull(),
trainingDate: varchar("trainingDate", { length: 10 }).notNull(),
totalMinutes: int("totalMinutes").default(0).notNull(),
sessionCount: int("sessionCount").default(0).notNull(),
analysisCount: int("analysisCount").default(0).notNull(),
liveAnalysisCount: int("liveAnalysisCount").default(0).notNull(),
recordingCount: int("recordingCount").default(0).notNull(),
pkCount: int("pkCount").default(0).notNull(),
totalActions: int("totalActions").default(0).notNull(),
effectiveActions: int("effectiveActions").default(0).notNull(),
unknownActions: int("unknownActions").default(0).notNull(),
totalScore: float("totalScore").default(0).notNull(),
averageScore: float("averageScore").default(0).notNull(),
metadata: json("metadata"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type DailyTrainingAggregate = typeof dailyTrainingAggregates.$inferSelect;
export type InsertDailyTrainingAggregate = typeof dailyTrainingAggregates.$inferInsert;
/**
* NTRP Rating history - tracks rating changes over time
*/
@@ -188,6 +288,25 @@ export const ratingHistory = mysqlTable("rating_history", {
export type RatingHistory = typeof ratingHistory.$inferSelect;
export type InsertRatingHistory = typeof ratingHistory.$inferInsert;
/**
* Daily NTRP snapshots generated by async refresh jobs.
*/
export const ntrpSnapshots = mysqlTable("ntrp_snapshots", {
id: int("id").autoincrement().primaryKey(),
snapshotKey: varchar("snapshotKey", { length: 64 }).notNull().unique(),
userId: int("userId").notNull(),
snapshotDate: varchar("snapshotDate", { length: 10 }).notNull(),
rating: float("rating").notNull(),
triggerType: mysqlEnum("triggerType", ["analysis", "daily", "manual"]).default("daily").notNull(),
taskId: varchar("taskId", { length: 64 }),
dimensionScores: json("dimensionScores"),
sourceSummary: json("sourceSummary"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type NtrpSnapshot = typeof ntrpSnapshots.$inferSelect;
export type InsertNtrpSnapshot = typeof ntrpSnapshots.$inferInsert;
/**
* Daily check-in records for streak tracking
*/
@@ -223,6 +342,49 @@ export const userBadges = mysqlTable("user_badges", {
export type UserBadge = typeof userBadges.$inferSelect;
export type InsertUserBadge = typeof userBadges.$inferInsert;
/**
* Achievement definitions that can scale beyond the legacy badge system.
*/
export const achievementDefinitions = mysqlTable("achievement_definitions", {
id: int("id").autoincrement().primaryKey(),
key: varchar("key", { length: 64 }).notNull().unique(),
name: varchar("name", { length: 128 }).notNull(),
description: text("description"),
category: varchar("category", { length: 32 }).notNull(),
rarity: varchar("rarity", { length: 16 }).default("common").notNull(),
icon: varchar("icon", { length: 16 }).default("🎾").notNull(),
metricKey: varchar("metricKey", { length: 64 }).notNull(),
targetValue: float("targetValue").notNull(),
tier: int("tier").default(1).notNull(),
isHidden: int("isHidden").default(0).notNull(),
isActive: int("isActive").default(1).notNull(),
sortOrder: int("sortOrder").default(0).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type AchievementDefinition = typeof achievementDefinitions.$inferSelect;
export type InsertAchievementDefinition = typeof achievementDefinitions.$inferInsert;
/**
* User achievement progress and unlock records.
*/
export const userAchievements = mysqlTable("user_achievements", {
id: int("id").autoincrement().primaryKey(),
progressKey: varchar("progressKey", { length: 96 }).notNull().unique(),
userId: int("userId").notNull(),
achievementKey: varchar("achievementKey", { length: 64 }).notNull(),
currentValue: float("currentValue").default(0).notNull(),
progressPct: float("progressPct").default(0).notNull(),
unlockedAt: timestamp("unlockedAt"),
lastEvaluatedAt: timestamp("lastEvaluatedAt").defaultNow().notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type UserAchievement = typeof userAchievements.$inferSelect;
export type InsertUserAchievement = typeof userAchievements.$inferInsert;
/**
* Tutorial video library - professional coaching reference videos
*/
@@ -313,6 +475,8 @@ export const backgroundTasks = mysqlTable("background_tasks", {
"training_plan_adjust",
"analysis_corrections",
"pose_correction_multimodal",
"ntrp_refresh_user",
"ntrp_refresh_all",
]).notNull(),
status: mysqlEnum("status", ["queued", "running", "succeeded", "failed"]).notNull().default("queued"),
title: varchar("title", { length: 256 }).notNull(),
@@ -335,6 +499,39 @@ export const backgroundTasks = mysqlTable("background_tasks", {
export type BackgroundTask = typeof backgroundTasks.$inferSelect;
export type InsertBackgroundTask = typeof backgroundTasks.$inferInsert;
/**
* Admin audit trail for privileged actions.
*/
export const adminAuditLogs = mysqlTable("admin_audit_logs", {
id: int("id").autoincrement().primaryKey(),
adminUserId: int("adminUserId").notNull(),
actionType: varchar("actionType", { length: 64 }).notNull(),
entityType: varchar("entityType", { length: 64 }).notNull(),
entityId: varchar("entityId", { length: 96 }),
targetUserId: int("targetUserId"),
payload: json("payload"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type AdminAuditLog = typeof adminAuditLogs.$inferSelect;
export type InsertAdminAuditLog = typeof adminAuditLogs.$inferInsert;
/**
* App settings editable from the admin console.
*/
export const appSettings = mysqlTable("app_settings", {
id: int("id").autoincrement().primaryKey(),
settingKey: varchar("settingKey", { length: 64 }).notNull().unique(),
label: varchar("label", { length: 128 }).notNull(),
description: text("description"),
value: json("value"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type AppSetting = typeof appSettings.$inferSelect;
export type InsertAppSetting = typeof appSettings.$inferInsert;
/**
* Vision reference library - canonical public tennis images used for multimodal evaluation
*/