Implement live analysis achievements and admin console
这个提交包含在:
159
drizzle/0007_grounded_live_ops.sql
普通文件
159
drizzle/0007_grounded_live_ops.sql
普通文件
@@ -0,0 +1,159 @@
|
||||
ALTER TABLE `training_records`
|
||||
ADD `exerciseType` varchar(64),
|
||||
ADD `sourceType` varchar(32) DEFAULT 'manual',
|
||||
ADD `sourceId` varchar(64),
|
||||
ADD `videoId` int,
|
||||
ADD `linkedPlanId` int,
|
||||
ADD `matchConfidence` float,
|
||||
ADD `actionCount` int DEFAULT 0,
|
||||
ADD `metadata` json;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `live_analysis_sessions` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`userId` int NOT NULL,
|
||||
`title` varchar(256) NOT NULL,
|
||||
`sessionMode` enum('practice','pk') NOT NULL DEFAULT 'practice',
|
||||
`status` enum('active','completed','aborted') NOT NULL DEFAULT 'completed',
|
||||
`startedAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`endedAt` timestamp,
|
||||
`durationMs` int NOT NULL DEFAULT 0,
|
||||
`dominantAction` varchar(64),
|
||||
`overallScore` float,
|
||||
`postureScore` float,
|
||||
`balanceScore` float,
|
||||
`techniqueScore` float,
|
||||
`footworkScore` float,
|
||||
`consistencyScore` float,
|
||||
`unknownActionRatio` float,
|
||||
`totalSegments` int NOT NULL DEFAULT 0,
|
||||
`effectiveSegments` int NOT NULL DEFAULT 0,
|
||||
`totalActionCount` int NOT NULL DEFAULT 0,
|
||||
`videoId` int,
|
||||
`videoUrl` text,
|
||||
`summary` text,
|
||||
`feedback` json,
|
||||
`metrics` json,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `live_analysis_sessions_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `live_action_segments` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`sessionId` int NOT NULL,
|
||||
`actionType` varchar(64) NOT NULL,
|
||||
`isUnknown` int NOT NULL DEFAULT 0,
|
||||
`startMs` int NOT NULL,
|
||||
`endMs` int NOT NULL,
|
||||
`durationMs` int NOT NULL,
|
||||
`confidenceAvg` float,
|
||||
`score` float,
|
||||
`peakScore` float,
|
||||
`frameCount` int NOT NULL DEFAULT 0,
|
||||
`issueSummary` json,
|
||||
`keyFrames` json,
|
||||
`clipLabel` varchar(128),
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `live_action_segments_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `live_action_segments_session_start_idx` UNIQUE(`sessionId`,`startMs`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `daily_training_aggregates` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`dayKey` varchar(32) NOT NULL,
|
||||
`userId` int NOT NULL,
|
||||
`trainingDate` varchar(10) NOT NULL,
|
||||
`totalMinutes` int NOT NULL DEFAULT 0,
|
||||
`sessionCount` int NOT NULL DEFAULT 0,
|
||||
`analysisCount` int NOT NULL DEFAULT 0,
|
||||
`liveAnalysisCount` int NOT NULL DEFAULT 0,
|
||||
`recordingCount` int NOT NULL DEFAULT 0,
|
||||
`pkCount` int NOT NULL DEFAULT 0,
|
||||
`totalActions` int NOT NULL DEFAULT 0,
|
||||
`effectiveActions` int NOT NULL DEFAULT 0,
|
||||
`unknownActions` int NOT NULL DEFAULT 0,
|
||||
`totalScore` float NOT NULL DEFAULT 0,
|
||||
`averageScore` float NOT NULL DEFAULT 0,
|
||||
`metadata` json,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `daily_training_aggregates_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `daily_training_aggregates_dayKey_unique` UNIQUE(`dayKey`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `ntrp_snapshots` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`snapshotKey` varchar(64) NOT NULL,
|
||||
`userId` int NOT NULL,
|
||||
`snapshotDate` varchar(10) NOT NULL,
|
||||
`rating` float NOT NULL,
|
||||
`triggerType` enum('analysis','daily','manual') NOT NULL DEFAULT 'daily',
|
||||
`taskId` varchar(64),
|
||||
`dimensionScores` json,
|
||||
`sourceSummary` json,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `ntrp_snapshots_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `ntrp_snapshots_snapshotKey_unique` UNIQUE(`snapshotKey`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `achievement_definitions` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`key` varchar(64) NOT NULL,
|
||||
`name` varchar(128) NOT NULL,
|
||||
`description` text,
|
||||
`category` varchar(32) NOT NULL,
|
||||
`rarity` varchar(16) NOT NULL DEFAULT 'common',
|
||||
`icon` varchar(16) NOT NULL DEFAULT '🎾',
|
||||
`metricKey` varchar(64) NOT NULL,
|
||||
`targetValue` float NOT NULL,
|
||||
`tier` int NOT NULL DEFAULT 1,
|
||||
`isHidden` int NOT NULL DEFAULT 0,
|
||||
`isActive` int NOT NULL DEFAULT 1,
|
||||
`sortOrder` int NOT NULL DEFAULT 0,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `achievement_definitions_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `achievement_definitions_key_unique` UNIQUE(`key`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user_achievements` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`progressKey` varchar(96) NOT NULL,
|
||||
`userId` int NOT NULL,
|
||||
`achievementKey` varchar(64) NOT NULL,
|
||||
`currentValue` float NOT NULL DEFAULT 0,
|
||||
`progressPct` float NOT NULL DEFAULT 0,
|
||||
`unlockedAt` timestamp,
|
||||
`lastEvaluatedAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `user_achievements_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `user_achievements_progressKey_unique` UNIQUE(`progressKey`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `background_tasks`
|
||||
MODIFY COLUMN `type` enum('media_finalize','training_plan_generate','training_plan_adjust','analysis_corrections','pose_correction_multimodal','ntrp_refresh_user','ntrp_refresh_all') NOT NULL;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `admin_audit_logs` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`adminUserId` int NOT NULL,
|
||||
`actionType` varchar(64) NOT NULL,
|
||||
`entityType` varchar(64) NOT NULL,
|
||||
`entityId` varchar(96),
|
||||
`targetUserId` int,
|
||||
`payload` json,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `admin_audit_logs_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `app_settings` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`settingKey` varchar(64) NOT NULL,
|
||||
`label` varchar(128) NOT NULL,
|
||||
`description` text,
|
||||
`value` json,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `app_settings_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `app_settings_settingKey_unique` UNIQUE(`settingKey`)
|
||||
);
|
||||
@@ -50,6 +50,13 @@
|
||||
"when": 1773510000000,
|
||||
"tag": "0006_solid_vision_library",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "5",
|
||||
"when": 1773543600000,
|
||||
"tag": "0007_grounded_live_ops",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
在新工单中引用
屏蔽一个用户