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

- 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
+29
View File
@@ -17,11 +17,18 @@ import {
import { SESSION_COOKIE, getSessionUser } from "@/lib/auth";
import { addDays, dowOf, todayStr } from "@/lib/dates";
import { passedCount, totalPoints } from "@/lib/grammar";
import {
POINT_EVENT_LABELS,
listPointRules,
pointHistory,
totalPoints as childPointsTotal,
} from "@/lib/points";
import { viewUrl } from "@/lib/oss";
import {
ParentDashboard,
type DevicesData,
type OverviewData,
type PointsData,
type TemplatesData,
type WeeklyRow,
} from "./dashboard";
@@ -136,6 +143,7 @@ export default async function ParentPage() {
title: t.title,
subject: t.subject,
requiresPhoto: t.requiresPhoto,
points: t.points,
mode: t.mode,
dateStart: t.dateStart,
dateEnd: t.dateEnd,
@@ -191,12 +199,33 @@ export default async function ParentPage() {
passed: w.passed,
}));
// ===== 积分管理(规则 + 每孩子总积分/最近流水) =====
const rules = await listPointRules();
const pointsData: PointsData = { rules, kids: [] };
const eventLabel = (t: string) => (t === "checkin" ? "打卡" : POINT_EVENT_LABELS[t] ?? t);
for (const kid of kids) {
const [total, recent] = await Promise.all([childPointsTotal(kid.id), pointHistory(kid.id, 5)]);
pointsData.kids.push({
id: kid.id,
name: kid.name,
total,
recent: recent.map((r) => ({
id: r.id,
label: eventLabel(r.eventType),
remark: r.remark,
points: r.points,
date: r.createdAt.toLocaleDateString("zh-CN", SH),
})),
});
}
return (
<ParentDashboard
overview={overview}
templates={templates}
devices={devices}
weekly={weekly}
points={pointsData}
/>
);
}