第一期:家庭自学打卡系统
- 家长手机号登录 + 孩子设备一次性激活(长期会话) - 周模板自动生成每日任务快照,支持拍照凭证直传对象存储 - 连击计算(空白日自动达成)、家长补卡带标记 - 家长端三标签:今日概览 / 任务模板 / 设备管理 - Docker 独立输出,经 Traefik 接入 jump.hn12301.net Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"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";
|
||||
|
||||
// 取当前孩子会话,非法则抛错(客户端会显示)
|
||||
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) {
|
||||
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));
|
||||
|
||||
revalidatePath("/kid");
|
||||
}
|
||||
|
||||
// 签发照片直传地址(浏览器 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");
|
||||
}
|
||||
Reference in New Issue
Block a user