"use server"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { db } from "@/db"; import { photos, taskInstances } from "@/db/schema"; import { todayStr } from "@/lib/dates"; import { photoKey, uploadUrl } from "@/lib/oss"; import { getSessionUser } from "@/lib/auth"; import { awardPoints } from "@/lib/points"; // 取当前孩子会话,非法则抛错(客户端会显示) async function requireChild() { const user = await getSessionUser(); if (!user || user.role !== "child") throw new Error("会话无效,请重新激活设备"); return user; } async function ownedTask(childId: number, instanceId: number) { const [task] = await db .select() .from(taskInstances) .where(and(eq(taskInstances.id, instanceId), eq(taskInstances.childUserId, childId))) .limit(1); if (!task) throw new Error("任务不存在"); return task; } // 勾选 / 取消勾选(仅限今天;需凭证的任务必须有照片才能勾);首次勾选发积分 export async function toggleTask(instanceId: number, done: boolean): Promise<{ pointsEarned: number }> { const child = await requireChild(); const task = await ownedTask(child.id, instanceId); if (task.date !== todayStr()) throw new Error("只能给今天的任务打卡"); if (done && task.requiresPhoto) { const [photo] = await db .select({ id: photos.id }) .from(photos) .where(eq(photos.taskInstanceId, instanceId)) .limit(1); if (!photo) throw new Error("这个任务要先拍照凭证哦"); } await db .update(taskInstances) .set({ status: done ? "done" : "pending", doneAt: done ? new Date() : null }) .where(eq(taskInstances.id, instanceId)); // xuchao: 首次完成才发分(唯一键防刷),分值取任务快照上的设置 const pointsEarned = done ? await awardPoints(child.id, "checkin", `inst-${instanceId}`, task.points, task.title) : 0; revalidatePath("/kid"); revalidatePath("/points"); return { pointsEarned }; } // 签发照片直传地址(浏览器 PUT 到对象存储) export async function getUploadUrl(instanceId: number) { const child = await requireChild(); const task = await ownedTask(child.id, instanceId); const count = await db .select({ id: photos.id }) .from(photos) .where(eq(photos.taskInstanceId, instanceId)); if (count.length >= 3) throw new Error("最多上传 3 张照片"); const key = photoKey(child.id, task.date); return { url: uploadUrl(key, "image/jpeg"), key }; } // 直传完成后登记照片 export async function confirmPhoto(instanceId: number, key: string) { const child = await requireChild(); await ownedTask(child.id, instanceId); if (!key.startsWith(`photos/${child.id}/`)) throw new Error("照片路径非法"); await db.insert(photos).values({ taskInstanceId: instanceId, ossKey: key }); revalidatePath("/kid"); }