- 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>
81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { and, eq, sql } from "drizzle-orm";
|
|
import { getSessionUser } from "@/lib/auth";
|
|
import { gradeAnswers, type AnswerValue } from "@/lib/grammar";
|
|
import { buildWeeklyQuestions, currentWeekKey, getWeeklyRecord } from "@/lib/weekly";
|
|
import { recordWrong } from "@/lib/wrongbook";
|
|
import { db } from "@/db";
|
|
import { taskInstances, weeklyTest } from "@/db/schema";
|
|
import { todayStr } from "@/lib/dates";
|
|
import { awardRulePoints } from "@/lib/points";
|
|
|
|
export type WeeklyResult =
|
|
| { score: number; passed: boolean; correct: number; total: number; detail: boolean[]; pointsEarned: number }
|
|
| { error: string };
|
|
|
|
// 提交每周能力检测:判分、记录、错题入库、达标关联当日英语打卡
|
|
export async function takeWeekly(answers: AnswerValue[]): Promise<WeeklyResult> {
|
|
const user = await getSessionUser();
|
|
if (!user || user.role !== "child") return { error: "会话无效,请重新激活设备" };
|
|
const wk = currentWeekKey();
|
|
const existing = await getWeeklyRecord(user.id, wk);
|
|
if (existing) return { error: "本周已完成检测,下周再来。" };
|
|
|
|
const questions = await buildWeeklyQuestions(user.id, wk);
|
|
if (questions.length === 0) return { error: "暂无可组卷的题目,先学几课吧。" };
|
|
|
|
const g = gradeAnswers(questions, answers);
|
|
questions.forEach((q, i) => {
|
|
if (!g.detail[i]) void recordWrong(user.id, "weekly", wk, `每周检测 ${wk}`, q, answers[i]);
|
|
});
|
|
|
|
await db
|
|
.insert(weeklyTest)
|
|
.values({
|
|
childUserId: user.id,
|
|
weekKey: wk,
|
|
score: g.score,
|
|
correct: g.correct,
|
|
total: g.total,
|
|
passed: g.passed,
|
|
questions: JSON.stringify(questions),
|
|
})
|
|
.onDuplicateKeyUpdate({ set: { score: g.score, correct: g.correct, total: g.total, passed: g.passed } });
|
|
|
|
// 达标关联当日英语打卡,并发积分(首次达标才发)
|
|
let pointsEarned = 0;
|
|
if (g.passed) {
|
|
pointsEarned = await awardRulePoints(user.id, "grammar_weekly", `week-${wk}`, `英语周检测 ${wk}`);
|
|
const today = todayStr();
|
|
const title = `英语周检测 · ${wk}`;
|
|
const [ex] = await db
|
|
.select({ id: taskInstances.id, status: taskInstances.status })
|
|
.from(taskInstances)
|
|
.where(and(eq(taskInstances.childUserId, user.id), eq(taskInstances.date, today), eq(taskInstances.title, title)))
|
|
.limit(1);
|
|
if (ex) {
|
|
if (ex.status !== "done")
|
|
await db.update(taskInstances).set({ status: "done", doneAt: sql`CURRENT_TIMESTAMP` }).where(eq(taskInstances.id, ex.id));
|
|
} else {
|
|
await db.insert(taskInstances).values({
|
|
childUserId: user.id,
|
|
date: today,
|
|
title,
|
|
subject: "english",
|
|
status: "done",
|
|
doneAt: new Date(),
|
|
sortOrder: 98,
|
|
});
|
|
}
|
|
}
|
|
|
|
revalidatePath("/weekly");
|
|
revalidatePath("/kid");
|
|
revalidatePath("/parent");
|
|
revalidatePath("/wrongbook");
|
|
revalidatePath("/points");
|
|
return { ...g, pointsEarned };
|
|
}
|