新增积分系统:打卡/测试达标赚积分,纯累计可配置

- point_rule 规则表 + point_log 流水表(0008 迁移),唯一键防刷:同一事件首次达标才发放,重复静默返回 0
- 打卡分值随任务设置(模板/实例 points 字段,默认 10);测试类 6 种事件按规则表取值,各有默认分(单词 20/数学课 15/语法点 15/周测 50×2/考试通过 100)
- 发放埋点 6 处:打卡、单词检测、数学课、语法知识点、英语/数学周测、考试通过;周测达标联动打卡实例不重复计分
- 孩子端:/kid 总积分徽章,新增 /points 积分页(总积分+流水),测验结果页即时显示「+N 积分」,周测结果页刷新后从流水取回已发积分
- 家长端:功能管理新增「积分管理」Section(规则分值配置+每孩子总积分/最近流水);任务管理内联编辑任务分值
- 修复:drizzle 将 mysql2 重复键错误包进 cause,沿错误链识别 ER_DUP_ENTRY

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-18 17:58:53 +08:00
co-authored by Claude Opus 4.8
parent 841f937b8e
commit 7a11a89031
27 changed files with 592 additions and 41 deletions
+14 -5
View File
@@ -25,7 +25,15 @@ export type SolveDetail = {
};
export type ExamResult =
| { score: number; passed: boolean; correct: number; total: number; detail: boolean[]; solves?: SolveDetail[] }
| {
score: number;
passed: boolean;
correct: number;
total: number;
detail: boolean[];
solves?: SolveDetail[];
pointsEarned: number;
}
| { error: string };
// xuchao: 签发解答题照片直传地址(或本地开发回退)
@@ -109,9 +117,9 @@ export async function takeExam(paperId: string, answers: (AnswerValue | null)[])
questions.forEach((q, i) => {
if (!g.detail[i]) void recordWrong(user.id, "exam", paperId, paper.title, q, answers[i]);
});
await recordExam(user.id, paperId, paper.title, g.score, g.correct, g.total, g.passed, answers);
const pointsEarned = await recordExam(user.id, paperId, paper.title, g.score, g.correct, g.total, g.passed, answers);
revalidateAll(paperId);
return g;
return { ...g, pointsEarned };
}
// xuchao: 混合卷加权判分——解答题只信服务端最近一次判定,不信任客户端上报
@@ -167,9 +175,9 @@ export async function takeExam(paperId: string, answers: (AnswerValue | null)[])
const score = pointSum ? Math.round((earnedSum / pointSum) * 100) : 0;
const passed = score >= 80;
await recordExam(user.id, paperId, paper.title, score, correct, questions.length, passed, storedAnswers);
const pointsEarned = await recordExam(user.id, paperId, paper.title, score, correct, questions.length, passed, storedAnswers);
revalidateAll(paperId);
return { score, passed, correct, total: questions.length, detail, solves };
return { score, passed, correct, total: questions.length, detail, solves, pointsEarned };
}
function revalidateAll(paperId: string) {
@@ -178,4 +186,5 @@ function revalidateAll(paperId: string) {
revalidatePath("/kid");
revalidatePath("/parent");
revalidatePath("/wrongbook");
revalidatePath("/points");
}