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

- 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
+5 -2
View File
@@ -94,7 +94,9 @@ export function WordDayClient({
const [example, setExample] = useState<Record<string, string>>({});
const [testOpen, setTestOpen] = useState(false);
const [answers, setAnswers] = useState<(number | null)[]>(test.map(() => null));
const [result, setResult] = useState<{ score: number; passed: boolean } | null>(passed ? { score: score ?? 0, passed } : null);
const [result, setResult] = useState<{ score: number; passed: boolean; pointsEarned: number } | null>(
passed ? { score: score ?? 0, passed, pointsEarned: 0 } : null,
);
const [pending, startTransition] = useTransition();
const toggleLearn = (word: string) => {
@@ -126,7 +128,7 @@ export function WordDayClient({
const submit = () => {
startTransition(async () => {
const r = await submitWordTest(day, answers.map((a) => a as number));
if (!("error" in r)) setResult({ score: r.score, passed: r.passed });
if (!("error" in r)) setResult({ score: r.score, passed: r.passed, pointsEarned: r.pointsEarned });
});
};
@@ -196,6 +198,7 @@ export function WordDayClient({
{result && (
<span className={cn("text-sm font-bold", result.passed ? "text-emerald-600" : "text-rose-600")}>
{result.score} 分 {result.passed ? "· 已点亮" : ""}
{result.passed && result.pointsEarned > 0 ? ` · 🪙 +${result.pointsEarned} 积分` : ""}
</span>
)}
</div>