docs: annotate database schema fields
This commit is contained in:
parent
2649b24277
commit
94b807ad16
248
src/db/schema.ts
248
src/db/schema.ts
@ -19,82 +19,86 @@ import { sql } from 'drizzle-orm';
|
||||
|
||||
// ── Users ──────────────────────────────────────────────────────────
|
||||
|
||||
// 用户账号与学习状态数据。
|
||||
export const users = mysqlTable('users', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
authType: mysqlEnum('auth_type', ['huawei', 'guest', 'phone', 'apple', 'google']).notNull(),
|
||||
authId: varchar('auth_id', { length: 255 }).notNull(),
|
||||
nickname: varchar('nickname', { length: 50 }),
|
||||
avatarUrl: varchar('avatar_url', { length: 500 }),
|
||||
tier: mysqlEnum('tier', ['free', 'pro', 'proplus']).default('free'),
|
||||
xpTotal: int('xp_total').default(0),
|
||||
streakDays: int('streak_days').default(0),
|
||||
streakLastDate: date('streak_last_date'),
|
||||
heartsRemaining: tinyint('hearts_remaining').default(5),
|
||||
heartsLastRestore: datetime('hearts_last_restore'),
|
||||
dailyXpGoal: smallint('daily_xp_goal').default(50),
|
||||
dailyXpEarned: smallint('daily_xp_earned').default(0),
|
||||
dailyXpDate: date('daily_xp_date'),
|
||||
currentTheme: varchar('current_theme', { length: 20 }).default('inkTeal'),
|
||||
activeTrackId: varchar('active_track_id', { length: 50 }),
|
||||
dailyAttemptsLeft: smallint('daily_attempts_left').default(5),
|
||||
dailyAttemptsDate: date('daily_attempts_date'),
|
||||
checkInDays: int('check_in_days').default(0),
|
||||
lastCheckInDate: date('last_check_in_date'),
|
||||
streakProtectedUntil: datetime('streak_protected_until'),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`),
|
||||
authType: mysqlEnum('auth_type', ['huawei', 'guest', 'phone', 'apple', 'google']).notNull(), // 登录方式。
|
||||
authId: varchar('auth_id', { length: 255 }).notNull(), // 第三方平台或游客身份标识。
|
||||
nickname: varchar('nickname', { length: 50 }), // 用户昵称。
|
||||
avatarUrl: varchar('avatar_url', { length: 500 }), // 头像图片地址。
|
||||
tier: mysqlEnum('tier', ['free', 'pro', 'proplus']).default('free'), // 订阅等级。
|
||||
xpTotal: int('xp_total').default(0), // 累计经验值。
|
||||
streakDays: int('streak_days').default(0), // 当前连续学习天数。
|
||||
streakLastDate: date('streak_last_date'), // 最近一次计入连续学习的日期。
|
||||
heartsRemaining: tinyint('hearts_remaining').default(5), // 当前剩余红心数。
|
||||
heartsLastRestore: datetime('hearts_last_restore'), // 最近一次红心自然恢复时间。
|
||||
dailyXpGoal: smallint('daily_xp_goal').default(50), // 每日经验目标。
|
||||
dailyXpEarned: smallint('daily_xp_earned').default(0), // 当日已获得经验值。
|
||||
dailyXpDate: date('daily_xp_date'), // 每日经验统计日期。
|
||||
currentTheme: varchar('current_theme', { length: 20 }).default('inkTeal'), // 当前界面主题。
|
||||
activeTrackId: varchar('active_track_id', { length: 50 }), // 当前学习路径。
|
||||
dailyAttemptsLeft: smallint('daily_attempts_left').default(5), // 当日剩余挑战次数。
|
||||
dailyAttemptsDate: date('daily_attempts_date'), // 每日挑战次数统计日期。
|
||||
checkInDays: int('check_in_days').default(0), // 累计签到天数。
|
||||
lastCheckInDate: date('last_check_in_date'), // 最近一次签到日期。
|
||||
streakProtectedUntil: datetime('streak_protected_until'), // 连续学习保护失效时间。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。
|
||||
}, (table) => [
|
||||
uniqueIndex('uk_auth').on(table.authType, table.authId),
|
||||
]);
|
||||
|
||||
// ── Categories ─────────────────────────────────────────────────────
|
||||
|
||||
// 题目分类与学习主题数据。
|
||||
export const categories = mysqlTable('categories', {
|
||||
id: varchar('id', { length: 50 }).primaryKey(),
|
||||
name: varchar('name', { length: 100 }).notNull(),
|
||||
slug: varchar('slug', { length: 100 }).notNull(),
|
||||
parentId: varchar('parent_id', { length: 50 }),
|
||||
sortOrder: int('sort_order').default(0),
|
||||
questionCount: int('question_count').default(0),
|
||||
status: mysqlEnum('status', ['active', 'inactive']).default('active'),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`),
|
||||
name: varchar('name', { length: 100 }).notNull(), // 分类名称。
|
||||
slug: varchar('slug', { length: 100 }).notNull(), // 面向 URL 或导入数据的唯一标识。
|
||||
parentId: varchar('parent_id', { length: 50 }), // 父级分类。
|
||||
sortOrder: int('sort_order').default(0), // 排序权重,数值越小越靠前。
|
||||
questionCount: int('question_count').default(0), // 分类下题目数量缓存。
|
||||
status: mysqlEnum('status', ['active', 'inactive']).default('active'), // 分类启用状态。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。
|
||||
}, (table) => [
|
||||
uniqueIndex('uk_slug').on(table.slug),
|
||||
]);
|
||||
|
||||
// ── Questions ──────────────────────────────────────────────────────
|
||||
|
||||
// 题库题目与答题统计数据。
|
||||
export const questions = mysqlTable('questions', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
stem: json('stem').notNull(),
|
||||
contentType: mysqlEnum('content_type', ['text', 'image', 'video', 'audio']).notNull(),
|
||||
correctAnswer: varchar('correct_answer', { length: 500 }).notNull(),
|
||||
distractors: json('distractors').notNull(),
|
||||
stem: json('stem').notNull(), // 题干内容,支持多语言或富媒体结构。
|
||||
contentType: mysqlEnum('content_type', ['text', 'image', 'video', 'audio']).notNull(), // 题目内容类型。
|
||||
correctAnswer: varchar('correct_answer', { length: 500 }).notNull(), // 正确答案。
|
||||
distractors: json('distractors').notNull(), // 干扰选项列表。
|
||||
categoryId: varchar('category_id', { length: 50 }).notNull(),
|
||||
difficulty: tinyint('difficulty'),
|
||||
dynamicDifficulty: decimal('dynamic_difficulty', { precision: 3, scale: 1 }),
|
||||
source: mysqlEnum('source', ['system', 'ugc']).default('system'),
|
||||
difficulty: tinyint('difficulty'), // 人工配置的难度等级。
|
||||
dynamicDifficulty: decimal('dynamic_difficulty', { precision: 3, scale: 1 }), // 根据答题表现计算的动态难度。
|
||||
source: mysqlEnum('source', ['system', 'ugc']).default('system'), // 题目来源。
|
||||
creatorId: char('creator_id', { length: 36 }),
|
||||
status: mysqlEnum('status', ['draft', 'reviewing', 'published', 'archived']).default('draft'),
|
||||
status: mysqlEnum('status', ['draft', 'reviewing', 'published', 'archived']).default('draft'), // 题目发布状态。
|
||||
stats: json('stats').$type<{ timesAnswered: number; correctRate: number; avgTimeMs: number }>()
|
||||
.default({ timesAnswered: 0, correctRate: 0, avgTimeMs: 0 }),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`),
|
||||
.default({ timesAnswered: 0, correctRate: 0, avgTimeMs: 0 }), // 答题次数、正确率和平均耗时统计。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。
|
||||
}, (table) => [
|
||||
foreignKey({ columns: [table.categoryId], foreignColumns: [categories.id] }),
|
||||
]);
|
||||
|
||||
// ── Knowledge Cards ────────────────────────────────────────────────
|
||||
|
||||
// 题目关联的知识讲解卡片数据。
|
||||
export const knowledgeCards = mysqlTable('knowledge_cards', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
questionId: char('question_id', { length: 36 }).notNull(),
|
||||
summary: varchar('summary', { length: 300 }).notNull(),
|
||||
deepDive: text('deep_dive'),
|
||||
sourceRef: varchar('source_ref', { length: 500 }),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`),
|
||||
summary: varchar('summary', { length: 300 }).notNull(), // 答题后的知识点摘要。
|
||||
deepDive: text('deep_dive'), // 延伸讲解内容。
|
||||
sourceRef: varchar('source_ref', { length: 500 }), // 参考来源或出处。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。
|
||||
}, (table) => [
|
||||
uniqueIndex('uk_question').on(table.questionId),
|
||||
foreignKey({ columns: [table.questionId], foreignColumns: [questions.id] }),
|
||||
@ -102,13 +106,14 @@ export const knowledgeCards = mysqlTable('knowledge_cards', {
|
||||
|
||||
// ── User Progress ──────────────────────────────────────────────────
|
||||
|
||||
// 用户单题答题记录数据。
|
||||
export const userProgress = mysqlTable('user_progress', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
userId: char('user_id', { length: 36 }).notNull(),
|
||||
questionId: char('question_id', { length: 36 }).notNull(),
|
||||
correct: tinyint('correct').notNull(),
|
||||
timeMs: int('time_ms'),
|
||||
answeredAt: datetime('answered_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
correct: tinyint('correct').notNull(), // 本次答题是否正确。
|
||||
timeMs: int('time_ms'), // 答题耗时,单位毫秒。
|
||||
answeredAt: datetime('answered_at').default(sql`CURRENT_TIMESTAMP`), // 答题时间。
|
||||
}, (table) => [
|
||||
index('idx_user_answered').on(table.userId, table.answeredAt),
|
||||
foreignKey({ columns: [table.userId], foreignColumns: [users.id] }),
|
||||
@ -117,29 +122,31 @@ export const userProgress = mysqlTable('user_progress', {
|
||||
|
||||
// ── Skill Tree ─────────────────────────────────────────────────────
|
||||
|
||||
// 分类下的学习关卡树数据。
|
||||
export const skillTree = mysqlTable('skill_tree', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
categoryId: varchar('category_id', { length: 50 }).notNull(),
|
||||
title: varchar('title', { length: 100 }).notNull(),
|
||||
title: varchar('title', { length: 100 }).notNull(), // 关卡标题。
|
||||
parentId: char('parent_id', { length: 36 }),
|
||||
sortOrder: int('sort_order').default(0),
|
||||
questionsRequired: tinyint('questions_required').default(4),
|
||||
passThreshold: tinyint('pass_threshold').default(2),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
sortOrder: int('sort_order').default(0), // 同级关卡排序权重。
|
||||
questionsRequired: tinyint('questions_required').default(4), // 完成本关需要回答的题目数。
|
||||
passThreshold: tinyint('pass_threshold').default(2), // 判定通关所需正确题数。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
}, (table) => [
|
||||
foreignKey({ columns: [table.categoryId], foreignColumns: [categories.id] }),
|
||||
]);
|
||||
|
||||
// ── User Chapter Progress ──────────────────────────────────────────
|
||||
|
||||
// 用户在学习关卡中的进度数据。
|
||||
export const userChapterProgress = mysqlTable('user_chapter_progress', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
userId: char('user_id', { length: 36 }).notNull(),
|
||||
chapterId: char('chapter_id', { length: 36 }).notNull(),
|
||||
status: mysqlEnum('status', ['locked', 'unlocked', 'passed', 'perfect']).default('locked'),
|
||||
bestCorrectCount: tinyint('best_correct_count').default(0),
|
||||
attempts: int('attempts').default(0),
|
||||
completedAt: datetime('completed_at'),
|
||||
status: mysqlEnum('status', ['locked', 'unlocked', 'passed', 'perfect']).default('locked'), // 用户在关卡中的进度状态。
|
||||
bestCorrectCount: tinyint('best_correct_count').default(0), // 历史最好正确题数。
|
||||
attempts: int('attempts').default(0), // 累计挑战次数。
|
||||
completedAt: datetime('completed_at'), // 最近一次通关时间。
|
||||
}, (table) => [
|
||||
uniqueIndex('uk_user_chapter').on(table.userId, table.chapterId),
|
||||
foreignKey({ columns: [table.userId], foreignColumns: [users.id] }),
|
||||
@ -148,104 +155,111 @@ export const userChapterProgress = mysqlTable('user_chapter_progress', {
|
||||
|
||||
// ── Question Ratings ──────────────────────────────────────────────
|
||||
|
||||
// 用户对题目的好坏反馈数据。
|
||||
export const questionRatings = mysqlTable('question_ratings', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
userId: char('user_id', { length: 36 }).notNull(),
|
||||
questionId: char('question_id', { length: 36 }).notNull(),
|
||||
rating: mysqlEnum('rating', ['good', 'bad']).notNull(),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
rating: mysqlEnum('rating', ['good', 'bad']).notNull(), // 用户对题目的反馈评价。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
}, (table) => [
|
||||
uniqueIndex('uk_user_question_rating').on(table.userId, table.questionId),
|
||||
]);
|
||||
|
||||
// ── User Feedback ──────────────────────────────────────────────────
|
||||
|
||||
// 用户提交的产品反馈数据。
|
||||
export const userFeedback = mysqlTable('user_feedback', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
userId: char('user_id', { length: 36 }).notNull(),
|
||||
content: text('content').notNull(),
|
||||
contact: varchar('contact', { length: 255 }),
|
||||
pageContext: varchar('page_context', { length: 200 }),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
content: text('content').notNull(), // 用户提交的反馈内容。
|
||||
contact: varchar('contact', { length: 255 }), // 用户留下的联系方式。
|
||||
pageContext: varchar('page_context', { length: 200 }), // 反馈来源页面或场景。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
});
|
||||
|
||||
// ── Achievements ──────────────────────────────────────────────────
|
||||
|
||||
// 可解锁的成就配置数据。
|
||||
export const achievements = mysqlTable('achievements', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
type: mysqlEnum('type', ['knowledge', 'behavior']).notNull(),
|
||||
name: varchar('name', { length: 100 }).notNull(),
|
||||
description: varchar('description', { length: 300 }).notNull(),
|
||||
iconUrl: varchar('icon_url', { length: 500 }),
|
||||
condition: json('condition').notNull(),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
type: mysqlEnum('type', ['knowledge', 'behavior']).notNull(), // 成就类型。
|
||||
name: varchar('name', { length: 100 }).notNull(), // 成就名称。
|
||||
description: varchar('description', { length: 300 }).notNull(), // 成就说明。
|
||||
iconUrl: varchar('icon_url', { length: 500 }), // 成就图标地址。
|
||||
condition: json('condition').notNull(), // 解锁条件配置。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
});
|
||||
|
||||
// 用户已解锁成就数据。
|
||||
export const userAchievements = mysqlTable('user_achievements', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
userId: char('user_id', { length: 36 }).notNull(),
|
||||
achievementId: char('achievement_id', { length: 36 }).notNull(),
|
||||
unlockedAt: datetime('unlocked_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
unlockedAt: datetime('unlocked_at').default(sql`CURRENT_TIMESTAMP`), // 解锁时间。
|
||||
}, (table) => [
|
||||
uniqueIndex('uk_user_achievement').on(table.userId, table.achievementId),
|
||||
]);
|
||||
|
||||
// ── Leaderboard Snapshots ──────────────────────────────────────────
|
||||
|
||||
// 用户排行榜周期快照数据。
|
||||
export const leaderboardSnapshots = mysqlTable('leaderboard_snapshots', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
userId: char('user_id', { length: 36 }).notNull(),
|
||||
tier: mysqlEnum('tier', ['bronze', 'silver', 'gold', 'platinum', 'diamond', 'master', 'grandmaster', 'champion', 'legend', 'mythic']).notNull(),
|
||||
weeklyXp: int('weekly_xp').default(0),
|
||||
rank: int('rank'),
|
||||
league: varchar('league', { length: 50 }),
|
||||
weekStart: date('week_start'),
|
||||
weekEnd: date('week_end'),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
tier: mysqlEnum('tier', ['bronze', 'silver', 'gold', 'platinum', 'diamond', 'master', 'grandmaster', 'champion', 'legend', 'mythic']).notNull(), // 排行榜段位。
|
||||
weeklyXp: int('weekly_xp').default(0), // 本周累计经验值。
|
||||
rank: int('rank'), // 当前排名。
|
||||
league: varchar('league', { length: 50 }), // 所属联赛或榜单分组。
|
||||
weekStart: date('week_start'), // 统计周开始日期。
|
||||
weekEnd: date('week_end'), // 统计周结束日期。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
}, (table) => [
|
||||
index('idx_user_week').on(table.userId, table.weekStart),
|
||||
]);
|
||||
|
||||
// ── Subscriptions ──────────────────────────────────────────────────
|
||||
|
||||
// 用户订阅权益与平台购买数据。
|
||||
export const subscriptions = mysqlTable('subscriptions', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
userId: char('user_id', { length: 36 }).notNull(),
|
||||
tier: mysqlEnum('tier', ['free', 'pro', 'proplus']).default('free'),
|
||||
platform: mysqlEnum('platform', ['huawei', 'apple', 'google']),
|
||||
purchaseToken: varchar('purchase_token', { length: 500 }),
|
||||
expiresAt: datetime('expires_at'),
|
||||
autoRenew: tinyint('auto_renew').default(0),
|
||||
status: mysqlEnum('status', ['active', 'expired', 'cancelled']).default('active'),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`),
|
||||
tier: mysqlEnum('tier', ['free', 'pro', 'proplus']).default('free'), // 订阅等级。
|
||||
platform: mysqlEnum('platform', ['huawei', 'apple', 'google']), // 购买平台。
|
||||
purchaseToken: varchar('purchase_token', { length: 500 }), // 平台购买凭证。
|
||||
expiresAt: datetime('expires_at'), // 订阅到期时间。
|
||||
autoRenew: tinyint('auto_renew').default(0), // 是否自动续订。
|
||||
status: mysqlEnum('status', ['active', 'expired', 'cancelled']).default('active'), // 订阅状态。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。
|
||||
}, (table) => [
|
||||
uniqueIndex('uk_subscription_user').on(table.userId),
|
||||
]);
|
||||
|
||||
// ── Rewarded Ad Recovery Sessions ─────────────────────────────────
|
||||
|
||||
// 激励广告恢复奖励的创建与结算会话数据。
|
||||
export const adRecoverySessions = mysqlTable('ad_recovery_sessions', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
userId: char('user_id', { length: 36 }).notNull(),
|
||||
type: mysqlEnum('type', ['hearts', 'bonusAttempts', 'streakProtection']).notNull(),
|
||||
status: mysqlEnum('status', ['pending', 'settling', 'completed', 'failed', 'expired']).default('pending'),
|
||||
clientRequestId: varchar('client_request_id', { length: 80 }).notNull(),
|
||||
completeRequestId: varchar('complete_request_id', { length: 80 }),
|
||||
platform: mysqlEnum('platform', ['ios', 'android', 'harmony', 'web']).notNull(),
|
||||
adProvider: varchar('ad_provider', { length: 50 }).notNull(),
|
||||
adPlacementId: varchar('ad_placement_id', { length: 120 }).notNull(),
|
||||
providerRewardToken: varchar('provider_reward_token', { length: 500 }),
|
||||
rewardSnapshot: json('reward_snapshot').$type<Record<string, unknown>>(),
|
||||
progressBefore: json('progress_before').$type<Record<string, unknown>>(),
|
||||
progressAfter: json('progress_after').$type<Record<string, unknown>>(),
|
||||
failureReason: varchar('failure_reason', { length: 80 }),
|
||||
providerError: varchar('provider_error', { length: 500 }),
|
||||
duplicateCount: int('duplicate_count').default(0),
|
||||
expiresAt: datetime('expires_at').notNull(),
|
||||
completedAt: datetime('completed_at'),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`),
|
||||
type: mysqlEnum('type', ['hearts', 'bonusAttempts', 'streakProtection']).notNull(), // 广告恢复奖励类型。
|
||||
status: mysqlEnum('status', ['pending', 'settling', 'completed', 'failed', 'expired']).default('pending'), // 会话处理状态。
|
||||
clientRequestId: varchar('client_request_id', { length: 80 }).notNull(), // 客户端创建会话时的幂等请求号。
|
||||
completeRequestId: varchar('complete_request_id', { length: 80 }), // 客户端完成结算时的幂等请求号。
|
||||
platform: mysqlEnum('platform', ['ios', 'android', 'harmony', 'web']).notNull(), // 发起广告恢复的平台。
|
||||
adProvider: varchar('ad_provider', { length: 50 }).notNull(), // 广告服务提供方。
|
||||
adPlacementId: varchar('ad_placement_id', { length: 120 }).notNull(), // 广告位标识。
|
||||
providerRewardToken: varchar('provider_reward_token', { length: 500 }), // 广告平台返回的奖励凭证。
|
||||
rewardSnapshot: json('reward_snapshot').$type<Record<string, unknown>>(), // 本次奖励结算快照。
|
||||
progressBefore: json('progress_before').$type<Record<string, unknown>>(), // 结算前的用户进度快照。
|
||||
progressAfter: json('progress_after').$type<Record<string, unknown>>(), // 结算后的用户进度快照。
|
||||
failureReason: varchar('failure_reason', { length: 80 }), // 业务失败原因。
|
||||
providerError: varchar('provider_error', { length: 500 }), // 广告平台错误信息。
|
||||
duplicateCount: int('duplicate_count').default(0), // 重复完成请求次数。
|
||||
expiresAt: datetime('expires_at').notNull(), // 会话过期时间。
|
||||
completedAt: datetime('completed_at'), // 会话完成时间。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。
|
||||
}, (table) => [
|
||||
uniqueIndex('uk_ad_recovery_user_client_request').on(table.userId, table.clientRequestId),
|
||||
index('idx_ad_recovery_user_type_status_created').on(table.userId, table.type, table.status, table.createdAt),
|
||||
@ -254,27 +268,29 @@ export const adRecoverySessions = mysqlTable('ad_recovery_sessions', {
|
||||
|
||||
// ── Admin Audit Log ────────────────────────────────────────────────
|
||||
|
||||
// 管理端操作审计日志数据。
|
||||
export const adminAuditLog = mysqlTable('admin_audit_log', {
|
||||
id: int('id').primaryKey().autoincrement(),
|
||||
adminId: varchar('admin_id', { length: 36 }).notNull(),
|
||||
action: varchar('action', { length: 10 }).notNull(),
|
||||
resource: varchar('resource', { length: 500 }),
|
||||
details: json('details'),
|
||||
ipAddress: varchar('ip_address', { length: 45 }),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
action: varchar('action', { length: 10 }).notNull(), // 操作类型。
|
||||
resource: varchar('resource', { length: 500 }), // 被操作的资源。
|
||||
details: json('details'), // 操作详情。
|
||||
ipAddress: varchar('ip_address', { length: 45 }), // 操作来源 IP。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
});
|
||||
|
||||
// ── Admin Users ───────────────────────────────────────────────────────
|
||||
|
||||
// 管理端账号数据。
|
||||
export const adminUsers = mysqlTable('admin_users', {
|
||||
id: char('id', { length: 36 }).primaryKey(),
|
||||
username: varchar('username', { length: 50 }).notNull(),
|
||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||
role: mysqlEnum('role', ['admin', 'super_admin']).default('admin'),
|
||||
isActive: tinyint('is_active').default(1),
|
||||
lastLoginAt: datetime('last_login_at'),
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`),
|
||||
username: varchar('username', { length: 50 }).notNull(), // 管理员登录名。
|
||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(), // 密码哈希。
|
||||
role: mysqlEnum('role', ['admin', 'super_admin']).default('admin'), // 管理员角色。
|
||||
isActive: tinyint('is_active').default(1), // 账号是否启用。
|
||||
lastLoginAt: datetime('last_login_at'), // 最近登录时间。
|
||||
createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。
|
||||
updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。
|
||||
}, (table) => [
|
||||
uniqueIndex('uk_admin_username').on(table.username),
|
||||
]);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user