Implement live analysis achievements and admin console

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

查看文件

@@ -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
*/