Add challenge session schema

This commit is contained in:
Wang Zhuoxuan 2026-05-11 17:39:06 +08:00
parent 8382183ee5
commit 5570973f74
2 changed files with 58 additions and 1 deletions

View File

@ -30,7 +30,7 @@
| # | 任务 | 状态 | 验收标准 | | # | 任务 | 状态 | 验收标准 |
|---|------|------|----------| |---|------|------|----------|
| G0-1 | 梳理游戏化规则常量模块 | [x] | 新增集中规则定义覆盖红心、挑战组、XP、等级、金币、道具、广告恢复、周榜周期 | | G0-1 | 梳理游戏化规则常量模块 | [x] | 新增集中规则定义覆盖红心、挑战组、XP、等级、金币、道具、广告恢复、周榜周期 |
| G0-2 | 新增挑战组数据模型 | [ ] | 支持 challenge session、session answers、组状态、正确数、完成时间、幂等提交 | | G0-2 | 新增挑战组数据模型 | [x] | 支持 challenge session、session answers、组状态、正确数、完成时间、幂等提交 |
| G0-3 | 新增钱包和道具库存模型 | [ ] | 支持金币余额、道具库存、道具获得/消耗流水 | | G0-3 | 新增钱包和道具库存模型 | [ ] | 支持金币余额、道具库存、道具获得/消耗流水 |
| G0-4 | 新增奖励流水模型 | [ ] | 记录奖励来源、幂等 key、奖励快照、发放前后状态 | | G0-4 | 新增奖励流水模型 | [ ] | 记录奖励来源、幂等 key、奖励快照、发放前后状态 |
| G0-5 | 新增每日任务或每日进度模型 | [ ] | 可统计每日首组挑战、每日任务完成、每日高奖励次数 | | G0-5 | 新增每日任务或每日进度模型 | [ ] | 可统计每日首组挑战、每日任务完成、每日高奖励次数 |

View File

@ -153,6 +153,63 @@ export const userChapterProgress = mysqlTable('user_chapter_progress', {
foreignKey({ columns: [table.chapterId], foreignColumns: [skillTree.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<readonly string[]>().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<Record<string, unknown>>(), // 完成结算后的奖励快照。
progressBefore: json('progress_before').$type<Record<string, unknown>>(), // 创建或结算前的资源快照。
progressAfter: json('progress_after').$type<Record<string, unknown>>(), // 完成结算后的资源快照。
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<Record<string, unknown>>(), // 返回客户端的本题裁决快照。
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] }),
]);
// ── Question Ratings ────────────────────────────────────────────── // ── Question Ratings ──────────────────────────────────────────────
// 用户对题目的好坏反馈数据。 // 用户对题目的好坏反馈数据。