Implement live analysis achievements and admin console

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

查看文件

@@ -3,6 +3,7 @@ import { appRouter } from "./routers";
import { COOKIE_NAME } from "../shared/const";
import type { TrpcContext } from "./_core/context";
import * as db from "./db";
import * as trainingAutomation from "./trainingAutomation";
import { ENV } from "./_core/env";
import { sdk } from "./_core/sdk";
@@ -957,3 +958,173 @@ describe("vision.seedLibrary", () => {
await expect(caller.vision.seedLibrary()).rejects.toThrow();
});
});
describe("achievement.list", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("returns achievement progress for authenticated users", async () => {
const user = createTestUser({ id: 12 });
const { ctx } = createMockContext(user);
const caller = appRouter.createCaller(ctx);
const listSpy = vi.spyOn(db, "listUserAchievements").mockResolvedValueOnce([
{
id: 1,
key: "training_day_1",
name: "开练",
description: "完成首个训练日",
category: "consistency",
rarity: "common",
icon: "🎾",
metricKey: "training_days",
targetValue: 1,
tier: 1,
isHidden: 0,
isActive: 1,
sortOrder: 1,
createdAt: new Date(),
updatedAt: new Date(),
currentValue: 1,
progressPct: 100,
unlockedAt: new Date(),
unlocked: true,
},
] as any);
const result = await caller.achievement.list();
expect(listSpy).toHaveBeenCalledWith(12);
expect(result).toHaveLength(1);
expect((result[0] as any).key).toBe("training_day_1");
});
});
describe("analysis.liveSessionSave", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("persists a live session and syncs training data", async () => {
const user = createTestUser({ id: 5 });
const { ctx } = createMockContext(user);
const caller = appRouter.createCaller(ctx);
const createSessionSpy = vi.spyOn(db, "createLiveAnalysisSession").mockResolvedValueOnce(101);
const createSegmentsSpy = vi.spyOn(db, "createLiveActionSegments").mockResolvedValueOnce();
const syncSpy = vi.spyOn(trainingAutomation, "syncLiveTrainingData").mockResolvedValueOnce({
recordId: 88,
unlocked: ["training_day_1"],
});
const result = await caller.analysis.liveSessionSave({
title: "实时分析 正手",
sessionMode: "practice",
startedAt: Date.now() - 4_000,
endedAt: Date.now(),
durationMs: 4_000,
dominantAction: "forehand",
overallScore: 84,
postureScore: 82,
balanceScore: 78,
techniqueScore: 86,
footworkScore: 75,
consistencyScore: 80,
totalActionCount: 3,
effectiveSegments: 2,
totalSegments: 3,
unknownSegments: 1,
feedback: ["节奏稳定"],
metrics: { sampleCount: 12 },
segments: [
{
actionType: "forehand",
isUnknown: false,
startMs: 500,
endMs: 2_500,
durationMs: 2_000,
confidenceAvg: 0.82,
score: 84,
peakScore: 90,
frameCount: 24,
issueSummary: ["击球点前移"],
keyFrames: [500, 1500, 2500],
clipLabel: "正手挥拍 00:00 - 00:02",
},
],
});
expect(createSessionSpy).toHaveBeenCalledTimes(1);
expect(createSegmentsSpy).toHaveBeenCalledTimes(1);
expect(syncSpy).toHaveBeenCalledWith(expect.objectContaining({
userId: 5,
sessionId: 101,
dominantAction: "forehand",
sessionMode: "practice",
}));
expect(result).toEqual({ sessionId: 101, trainingRecordId: 88 });
});
});
describe("rating.refreshMine", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("creates an async NTRP refresh task for the current user", async () => {
const user = createTestUser({ id: 22 });
const { ctx } = createMockContext(user);
const caller = appRouter.createCaller(ctx);
const createTaskSpy = vi.spyOn(db, "createBackgroundTask").mockResolvedValueOnce();
const result = await caller.rating.refreshMine();
expect(createTaskSpy).toHaveBeenCalledWith(expect.objectContaining({
userId: 22,
type: "ntrp_refresh_user",
payload: { targetUserId: 22 },
}));
expect(result.taskId).toBeTruthy();
});
});
describe("admin.users", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("rejects non-admin users", async () => {
const user = createTestUser({ role: "user" });
const { ctx } = createMockContext(user);
const caller = appRouter.createCaller(ctx);
await expect(caller.admin.users({ limit: 20 })).rejects.toThrow();
});
it("returns user list for admin users", async () => {
const admin = createTestUser({ id: 1, role: "admin", name: "H1" });
const { ctx } = createMockContext(admin);
const caller = appRouter.createCaller(ctx);
const usersSpy = vi.spyOn(db, "listUsersForAdmin").mockResolvedValueOnce([
{
id: 1,
name: "H1",
role: "admin",
ntrpRating: 3.4,
totalSessions: 10,
totalMinutes: 320,
totalShots: 240,
currentStreak: 6,
longestStreak: 12,
createdAt: new Date(),
lastSignedIn: new Date(),
},
] as any);
const result = await caller.admin.users({ limit: 20 });
expect(usersSpy).toHaveBeenCalledWith(20);
expect(result).toHaveLength(1);
expect((result[0] as any).name).toBe("H1");
});
});