import { mysqlTable, char, mysqlEnum, varchar, int, tinyint, smallint, decimal, text, json, date, datetime, uniqueIndex, foreignKey, index, } from 'drizzle-orm/mysql-core'; 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`), // 更新时间。 }, (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(), // 面向 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(), // 干扰选项列表。 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'), // 题目来源。 creatorId: char('creator_id', { length: 36 }), 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`), // 更新时间。 }, (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`), // 更新时间。 }, (table) => [ uniqueIndex('uk_question').on(table.questionId), foreignKey({ columns: [table.questionId], foreignColumns: [questions.id] }), ]); // ── 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`), // 答题时间。 }, (table) => [ index('idx_user_answered').on(table.userId, table.answeredAt), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), foreignKey({ columns: [table.questionId], foreignColumns: [questions.id] }), ]); // ── 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(), // 关卡标题。 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`), // 创建时间。 }, (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'), // 最近一次通关时间。 }, (table) => [ uniqueIndex('uk_user_chapter').on(table.userId, table.chapterId), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), foreignKey({ columns: [table.chapterId], foreignColumns: [skillTree.id] }), ]); // ── Challenge Sessions ───────────────────────────────────────────── // 用户挑战组会话数据,服务端以 5 题为一组裁决进度和奖励。 export const challengeSessions = mysqlTable('challenge_sessions', { id: char('id', { length: 36 }).primaryKey(), userId: char('user_id', { length: 36 }).notNull(), trackId: varchar('track_id', { length: 50 }).notNull(), // 客户端选择的学习路线。 categoryId: varchar('category_id', { length: 50 }).notNull(), // 本组题目所属主题分类。 chapterId: char('chapter_id', { length: 36 }), // 绑定的技能树节点或章节。 status: mysqlEnum('status', ['pending', 'in_progress', 'completed', 'abandoned', 'expired']).default('pending'), // 挑战组状态。 clientRequestId: varchar('client_request_id', { length: 80 }).notNull(), // 创建挑战组的客户端幂等请求号。 completeRequestId: varchar('complete_request_id', { length: 80 }), // 完成结算的客户端幂等请求号。 questionIds: json('question_ids').$type().notNull(), // 本组题目 ID 快照,不包含正确答案。 totalQuestions: tinyint('total_questions').default(5), // 本组题目总数。 answeredCount: tinyint('answered_count').default(0), // 已提交答案数量。 correctCount: tinyint('correct_count').default(0), // 当前正确数量。 highRewardEligible: tinyint('high_reward_eligible').default(1), // 是否消耗并享受每日高奖励次数。 rewardSnapshot: json('reward_snapshot').$type>(), // 完成结算后的奖励快照。 progressBefore: json('progress_before').$type>(), // 创建或结算前的资源快照。 progressAfter: json('progress_after').$type>(), // 完成结算后的资源快照。 expiresAt: datetime('expires_at'), // 会话过期时间。 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_challenge_session_user_client_request').on(table.userId, table.clientRequestId), uniqueIndex('uk_challenge_session_user_complete_request').on(table.userId, table.completeRequestId), index('idx_challenge_session_user_status_created').on(table.userId, table.status, table.createdAt), index('idx_challenge_session_chapter_status').on(table.chapterId, table.status), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), foreignKey({ columns: [table.categoryId], foreignColumns: [categories.id] }), foreignKey({ columns: [table.chapterId], foreignColumns: [skillTree.id] }), ]); // 用户挑战组内的单题提交记录,支撑重复提交幂等和组完成结算。 export const challengeSessionAnswers = mysqlTable('challenge_session_answers', { id: char('id', { length: 36 }).primaryKey(), sessionId: char('session_id', { length: 36 }).notNull(), userId: char('user_id', { length: 36 }).notNull(), questionId: char('question_id', { length: 36 }).notNull(), submitRequestId: varchar('submit_request_id', { length: 80 }).notNull(), // 单题提交的客户端幂等请求号。 answerOrder: tinyint('answer_order').notNull(), // 本题在挑战组中的顺序。 answer: varchar('answer', { length: 500 }), // 用户提交答案。 correct: tinyint('correct').notNull(), // 本次提交是否正确。 timeMs: int('time_ms'), // 答题耗时,单位毫秒。 comboCount: tinyint('combo_count').default(0), // 提交后组内连续答对数。 resultSnapshot: json('result_snapshot').$type>(), // 返回客户端的本题裁决快照。 submittedAt: datetime('submitted_at').default(sql`CURRENT_TIMESTAMP`), // 提交时间。 }, (table) => [ uniqueIndex('uk_challenge_answer_session_question').on(table.sessionId, table.questionId), uniqueIndex('uk_challenge_answer_session_request').on(table.sessionId, table.submitRequestId), index('idx_challenge_answer_user_submitted').on(table.userId, table.submittedAt), foreignKey({ columns: [table.sessionId], foreignColumns: [challengeSessions.id] }), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), foreignKey({ columns: [table.questionId], foreignColumns: [questions.id] }), ]); // ── Wallets and Inventory ────────────────────────────────────────── // 用户金币钱包,所有余额变化必须通过流水记录追踪。 export const userWallets = mysqlTable('user_wallets', { userId: char('user_id', { length: 36 }).primaryKey(), coinsBalance: int('coins_balance').default(0), // 当前金币余额。 lifetimeCoinsEarned: int('lifetime_coins_earned').default(0), // 历史累计获得金币。 lifetimeCoinsSpent: int('lifetime_coins_spent').default(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.userId], foreignColumns: [users.id] }), ]); // 用户道具库存,以 item_id 聚合当前可用数量和最近有效期。 export const userInventoryItems = mysqlTable('user_inventory_items', { id: char('id', { length: 36 }).primaryKey(), userId: char('user_id', { length: 36 }).notNull(), itemId: mysqlEnum('item_id', ['streak_shield', 'double_xp_potion', 'heart_supply', 'hint_feather', 'mascot_outfit']).notNull(), // 道具标识。 quantity: int('quantity').default(0), // 当前库存数量。 activeUntil: datetime('active_until'), // 时效型道具的生效截止时间。 metadata: json('metadata').$type>(), // 装扮、头像框等展示权益的扩展信息。 createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。 updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。 }, (table) => [ uniqueIndex('uk_inventory_user_item').on(table.userId, table.itemId), index('idx_inventory_user_active').on(table.userId, table.activeUntil), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), ]); // 钱包和道具库存流水,记录金币与道具的获得、消耗和调整来源。 export const inventoryTransactions = mysqlTable('inventory_transactions', { id: char('id', { length: 36 }).primaryKey(), userId: char('user_id', { length: 36 }).notNull(), inventoryItemId: char('inventory_item_id', { length: 36 }), itemId: mysqlEnum('item_id', ['coins', 'streak_shield', 'double_xp_potion', 'heart_supply', 'hint_feather', 'mascot_outfit']).notNull(), // 流水涉及的资源。 direction: mysqlEnum('direction', ['grant', 'consume', 'adjust']).notNull(), // 获得、消耗或运营调整。 quantityDelta: int('quantity_delta').notNull(), // 资源数量变化,消耗为负数。 balanceAfter: int('balance_after'), // 变更后的金币余额或道具库存。 sourceType: mysqlEnum('source_type', ['challenge', 'daily_task', 'level_up', 'theme_node', 'chest', 'shop_purchase', 'ad_recovery', 'subscription', 'admin_grant', 'system_adjust', 'leaderboard_settlement']).notNull(), // 资源变化来源。 sourceId: varchar('source_id', { length: 120 }), // 来源业务 ID,如挑战组、订单或广告会话。 idempotencyKey: varchar('idempotency_key', { length: 160 }), // 幂等边界,防止重复发放或重复扣减。 snapshot: json('snapshot').$type>(), // 本次变更的上下文快照。 createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。 }, (table) => [ uniqueIndex('uk_inventory_transaction_idempotency').on(table.userId, table.idempotencyKey), index('idx_inventory_transaction_user_created').on(table.userId, table.createdAt), index('idx_inventory_transaction_source').on(table.sourceType, table.sourceId), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), foreignKey({ name: 'fk_inv_tx_item', columns: [table.inventoryItemId], foreignColumns: [userInventoryItems.id] }), ]); // ── Reward Ledger ───────────────────────────────────────────────── // 统一奖励结算流水,记录奖励来源、幂等边界、快照和发放前后状态。 export const rewardLedger = mysqlTable('reward_ledger', { id: char('id', { length: 36 }).primaryKey(), userId: char('user_id', { length: 36 }).notNull(), sourceType: mysqlEnum('source_type', ['challenge_answer', 'challenge_completion', 'daily_task', 'streak_milestone', 'level_up', 'theme_node', 'knowledge_card', 'chest', 'shop_purchase', 'ad_recovery', 'leaderboard_settlement', 'subscription', 'admin_grant', 'system_adjust']).notNull(), // 奖励来源。 sourceId: varchar('source_id', { length: 120 }), // 来源业务 ID。 idempotencyKey: varchar('idempotency_key', { length: 160 }).notNull(), // 奖励结算幂等 key。 status: mysqlEnum('status', ['pending', 'settling', 'completed', 'failed', 'reversed']).default('pending'), // 奖励结算状态。 rewardSnapshot: json('reward_snapshot').$type>().notNull(), // 计划发放或已发放的奖励快照。 resourceDeltas: json('resource_deltas').$type>(), // XP、金币、红心、道具等资源变化。 stateBefore: json('state_before').$type>(), // 发放前用户资源状态。 stateAfter: json('state_after').$type>(), // 发放后用户资源状态。 failureReason: varchar('failure_reason', { length: 120 }), // 结算失败或回滚原因。 settledAt: datetime('settled_at'), // 奖励完成结算时间。 createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。 updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。 }, (table) => [ uniqueIndex('uk_reward_ledger_user_idempotency').on(table.userId, table.idempotencyKey), index('idx_reward_ledger_user_status_created').on(table.userId, table.status, table.createdAt), index('idx_reward_ledger_source').on(table.sourceType, table.sourceId), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), ]); // ── Daily Progress ──────────────────────────────────────────────── // 用户每日游戏化进度,聚合首组挑战、每日任务和高奖励次数。 export const userDailyProgress = mysqlTable('user_daily_progress', { id: char('id', { length: 36 }).primaryKey(), userId: char('user_id', { length: 36 }).notNull(), progressDate: date('progress_date').notNull(), // 统计日期,服务端按 UTC 自然日写入。 timezone: varchar('timezone', { length: 50 }).default('UTC'), // 客户端或服务端用于展示的时区。 firstChallengeSessionId: char('first_challenge_session_id', { length: 36 }), // 当日首组完成挑战。 firstChallengeCompletedAt: datetime('first_challenge_completed_at'), // 当日首组挑战完成时间。 challengeSessionsCompleted: smallint('challenge_sessions_completed').default(0), // 当日完成挑战组数。 highRewardSessionsMax: smallint('high_reward_sessions_max').default(3), // 当日高奖励挑战次数上限。 highRewardSessionsUsed: smallint('high_reward_sessions_used').default(0), // 当日已消耗高奖励次数。 highRewardSessionsRestored: smallint('high_reward_sessions_restored').default(0), // 当日通过广告等方式恢复的高奖励次数。 dailyTasksCompleted: smallint('daily_tasks_completed').default(0), // 当日已完成任务数。 dailyTasksRewardClaimed: smallint('daily_tasks_reward_claimed').default(0), // 当日已领取任务奖励数。 xpEarned: int('xp_earned').default(0), // 当日获得 XP。 coinsEarned: int('coins_earned').default(0), // 当日获得金币。 streakCounted: tinyint('streak_counted').default(0), // 当日是否已计入连续学习。 metadata: json('metadata').$type>(), // 每日宝箱、降级倍率等扩展状态。 createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。 updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。 }, (table) => [ uniqueIndex('uk_daily_progress_user_date').on(table.userId, table.progressDate), index('idx_daily_progress_date').on(table.progressDate), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), foreignKey({ name: 'fk_daily_progress_session', columns: [table.firstChallengeSessionId], foreignColumns: [challengeSessions.id] }), ]); // 用户每日任务进度,用于幂等记录任务完成和奖励领取。 export const userDailyTasks = mysqlTable('user_daily_tasks', { id: char('id', { length: 36 }).primaryKey(), dailyProgressId: char('daily_progress_id', { length: 36 }).notNull(), userId: char('user_id', { length: 36 }).notNull(), taskDate: date('task_date').notNull(), // 任务归属日期,服务端按 UTC 自然日写入。 taskId: varchar('task_id', { length: 80 }).notNull(), // 每日任务配置标识。 taskType: mysqlEnum('task_type', ['complete_challenge', 'earn_xp', 'answer_correct', 'review_explanation', 'use_item', 'watch_ad']).notNull(), // 任务类型。 targetCount: smallint('target_count').default(1), // 任务目标次数。 currentCount: smallint('current_count').default(0), // 当前完成次数。 status: mysqlEnum('status', ['active', 'completed', 'reward_claimed', 'expired']).default('active'), // 任务状态。 rewardSnapshot: json('reward_snapshot').$type>(), // 任务奖励快照。 completedAt: datetime('completed_at'), // 任务完成时间。 rewardClaimedAt: datetime('reward_claimed_at'), // 奖励领取时间。 createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。 updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。 }, (table) => [ uniqueIndex('uk_daily_task_user_date_task').on(table.userId, table.taskDate, table.taskId), index('idx_daily_task_progress_status').on(table.dailyProgressId, table.status), foreignKey({ columns: [table.dailyProgressId], foreignColumns: [userDailyProgress.id] }), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), ]); // ── 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`), // 创建时间。 }, (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`), // 创建时间。 }); // ── 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`), // 创建时间。 }); // 用户已解锁成就数据。 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`), // 解锁时间。 }, (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'), // 当前排名。 groupId: varchar('group_id', { length: 80 }), // 周榜分组 ID。 league: varchar('league', { length: 50 }), // 所属联赛或榜单分组。 rewardSnapshot: json('reward_snapshot').$type>(), // 周结算奖励预览或实际发放快照。 settledAt: datetime('settled_at'), // 周榜结算时间。 weekStart: date('week_start').notNull(), // 统计周开始日期,按 UTC 自然周一。 weekEnd: date('week_end').notNull(), // 统计周结束日期,按 UTC 自然周日。 createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。 }, (table) => [ uniqueIndex('uk_leaderboard_snapshot_user_week').on(table.userId, table.weekStart), index('idx_leaderboard_snapshot_group_rank').on(table.groupId, table.weekStart, table.rank), ]); // 用户每周 XP 统计,作为当前周排行榜和历史快照的累计数据源。 export const userWeeklyXp = mysqlTable('user_weekly_xp', { id: char('id', { length: 36 }).primaryKey(), userId: char('user_id', { length: 36 }).notNull(), weekStart: date('week_start').notNull(), // UTC 自然周一。 weekEnd: date('week_end').notNull(), // UTC 自然周日。 timezone: varchar('timezone', { length: 50 }).default('UTC'), // 周期展示时区。 xpEarned: int('xp_earned').default(0), // 本周累计 XP。 challengeSessionsCompleted: int('challenge_sessions_completed').default(0), // 本周完成挑战组数。 groupId: varchar('group_id', { length: 80 }), // 分配到的周榜分组 ID。 rank: int('rank'), // 当前组内排名缓存。 settled: tinyint('settled').default(0), // 本周是否已完成结算。 settledAt: datetime('settled_at'), // 结算时间。 lastXpAt: datetime('last_xp_at'), // 最近一次 XP 累加时间。 nextRefreshAt: datetime('next_refresh_at'), // 下一次周榜刷新时间。 createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), // 创建时间。 updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), // 更新时间。 }, (table) => [ uniqueIndex('uk_weekly_xp_user_week').on(table.userId, table.weekStart), index('idx_weekly_xp_group_rank').on(table.groupId, table.weekStart, table.xpEarned), index('idx_weekly_xp_week_settled').on(table.weekStart, table.settled), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), ]); // ── 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`), // 更新时间。 }, (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`), // 更新时间。 }, (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), foreignKey({ columns: [table.userId], foreignColumns: [users.id] }), ]); // ── Account Migrations ───────────────────────────────────────────── // 游客账号关联正式账号的迁移记录,用于幂等追踪。 export const accountMigrations = mysqlTable('account_migrations', { id: char('id', { length: 36 }).primaryKey(), guestUserId: char('guest_user_id', { length: 36 }).notNull(), formalUserId: char('formal_user_id', { length: 36 }), provider: mysqlEnum('provider', ['apple', 'google', 'phone']).notNull(), providerUserId: varchar('provider_user_id', { length: 255 }).notNull(), clientMigrationId: varchar('client_migration_id', { length: 80 }).notNull(), status: mysqlEnum('status', ['in_progress', 'completed', 'failed']).default('in_progress'), migrationSummary: json('migration_summary').$type<{ policy: string; imported: Record; skipped: Record; conflicts: readonly string[]; }>(), createdAt: datetime('created_at').default(sql`CURRENT_TIMESTAMP`), completedAt: datetime('completed_at'), }, (table) => [ uniqueIndex('uk_migration_guest_client').on(table.guestUserId, table.clientMigrationId), uniqueIndex('uk_migration_guest_provider').on(table.guestUserId, table.provider), foreignKey({ columns: [table.guestUserId], foreignColumns: [users.id] }), foreignKey({ columns: [table.formalUserId], foreignColumns: [users.id] }), ]); // ── 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 }), // 操作来源 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`), // 更新时间。 }, (table) => [ uniqueIndex('uk_admin_username').on(table.username), ]); // ── App Settings ─────────────────────────────────────────────────── // 应用运行时配置,支持热重载(修改后立即生效,无需重启)。 export const appSettings = mysqlTable('app_settings', { key: varchar('key', { length: 100 }).primaryKey(), value: text('value').notNull(), description: varchar('description', { length: 300 }), updatedAt: datetime('updated_at').default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`), });