diff --git a/src/db/schema.ts b/src/db/schema.ts index edaf89f..b936189 100644 --- a/src/db/schema.ts +++ b/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>(), - progressBefore: json('progress_before').$type>(), - progressAfter: json('progress_after').$type>(), - 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>(), // 本次奖励结算快照。 + progressBefore: json('progress_before').$type>(), // 结算前的用户进度快照。 + progressAfter: json('progress_after').$type>(), // 结算后的用户进度快照。 + 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), ]);