家庭守护圈:多人加入、角色权限、24 小时邀请链接、旧设备绑定自动兼容。

家庭动态:老人/家属发言、三种可见范围,家庭内部内容不会播给老人。
共同照护:家庭备忘录、任务创建、认领、完成与交接记录。
用药闭环:计划、未来 7 天提醒实例、确认服用、延后、异常反馈。
风险闭环:红橙黄蓝分级、30 分钟去重、推送、认领、处理与误报记录。
报平安:家属发起、老人语音回应、状态同步。
Web 导航升级为守护、家庭、照护、陪伴、我的。
设备接口已升级为设备令牌认证,UUID 不再单独承担认证。
This commit is contained in:
2026-07-18 19:59:20 +08:00
parent b7579c5947
commit 747dcd359a
33 changed files with 1557 additions and 111 deletions
+13
View File
@@ -0,0 +1,13 @@
import { NextResponse } from "next/server";
import { updateAlert } from "@/lib/family-guard";
import { getCaregiverSession } from "@/lib/session";
export async function PATCH(request: Request) {
try {
const caregiver = await getCaregiverSession();
if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 });
const body = await request.json().catch(() => ({})) as Record<string, unknown>;
const alert = await updateAlert(caregiver.id, String(body.alertId || ""), String(body.action || ""), body.note);
return NextResponse.json({ ok: true, alert });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "更新提醒失败。" }, { status: 400 }); }
}
+22
View File
@@ -0,0 +1,22 @@
import { NextResponse } from "next/server";
import { createCareTask, updateCareTask } from "@/lib/family-guard";
import { getCaregiverSession } from "@/lib/session";
export async function POST(request: Request) {
try {
const caregiver = await getCaregiverSession();
if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 });
const task = await createCareTask(caregiver.id, await request.json().catch(() => ({})));
return NextResponse.json({ ok: true, task });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "创建任务失败。" }, { status: 400 }); }
}
export async function PATCH(request: Request) {
try {
const caregiver = await getCaregiverSession();
if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 });
const body = await request.json().catch(() => ({})) as Record<string, unknown>;
const task = await updateCareTask(caregiver.id, String(body.taskId || ""), String(body.action || ""), body.note);
return NextResponse.json({ ok: true, task });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "更新任务失败。" }, { status: 400 }); }
}
+12
View File
@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { createCheckIn } from "@/lib/family-guard";
import { getCaregiverSession } from "@/lib/session";
export async function POST(request: Request) {
try {
const caregiver = await getCaregiverSession();
if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 });
const checkIn = await createCheckIn(caregiver.id, await request.json().catch(() => ({})));
return NextResponse.json({ ok: true, checkIn });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "发起报平安失败。" }, { status: 400 }); }
}
+18
View File
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { requireDeviceAuth } from "@/lib/device-auth";
import { getDeviceAgenda } from "@/lib/family-guard";
export async function GET(request: Request) {
const deviceUuid = new URL(request.url).searchParams.get("deviceUuid") || "";
try {
await requireDeviceAuth(request, deviceUuid);
const agenda = await getDeviceAgenda(deviceUuid);
return NextResponse.json({
circleName: agenda.circle?.name || null,
posts: agenda.posts.map((post) => ({ id: post.id, content: post.content, authorName: post.author?.nickname || post.author?.username || "家里人", createdAt: post.createdAt })),
memos: agenda.memos.map((memo) => ({ id: memo.id, title: memo.title, content: memo.content, priority: memo.priority })),
doses: agenda.doses.map((dose) => ({ id: dose.id, scheduledFor: dose.scheduledFor, status: dose.status, name: dose.medicationPlan.name, dosage: dose.medicationPlan.dosage, instructions: dose.medicationPlan.instructions })),
checkIns: agenda.checkIns,
});
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "读取今日照护事项失败。" }, { status: 401 }); }
}
+15
View File
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { requireDeviceAuth } from "@/lib/device-auth";
import { createSafetyAlertForDevice } from "@/lib/family-guard";
import { sendGuardAlertPush } from "@/lib/push";
export async function POST(request: Request) {
const body = await request.json().catch(() => ({})) as Record<string, unknown>;
const deviceUuid = typeof body.deviceUuid === "string" ? body.deviceUuid : "";
try {
await requireDeviceAuth(request, deviceUuid);
const result = await createSafetyAlertForDevice(deviceUuid, body);
if (!result.deduplicated) void sendGuardAlertPush(result.alert.id).catch(() => undefined);
return NextResponse.json({ ok: true, alertId: result.alert.id, deduplicated: result.deduplicated });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "创建风险提醒失败。" }, { status: 400 }); }
}
+1
View File
@@ -0,0 +1 @@
export { GET } from "@/app/api/device/agenda/route";
+3 -1
View File
@@ -2,6 +2,7 @@ import { ConversationRole } from "@prisma/client";
import { NextResponse } from "next/server";
import { recordConversationTurns } from "@/lib/monitor-data";
import { requireDeviceAuth } from "@/lib/device-auth";
function toConversationRole(rawRole: unknown) {
switch (rawRole) {
@@ -55,6 +56,7 @@ export async function POST(request: Request) {
}
try {
await requireDeviceAuth(request, body.deviceUuid);
const result = await recordConversationTurns({
deviceUuid: body.deviceUuid,
sessionId: typeof body.sessionId === "string" ? body.sessionId : undefined,
@@ -71,4 +73,4 @@ export async function POST(request: Request) {
{ status: 400 },
);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
import { CheckInStatus, MedicationDoseStatus } from "@prisma/client";
import { NextResponse } from "next/server";
import { requireDeviceAuth } from "@/lib/device-auth";
import { respondCheckIn, updateMedicationDose } from "@/lib/family-guard";
export async function POST(request: Request) {
const body = await request.json().catch(() => ({})) as Record<string, unknown>;
const deviceUuid = typeof body.deviceUuid === "string" ? body.deviceUuid : "";
try {
await requireDeviceAuth(request, deviceUuid);
if (body.type === "MEDICATION") {
const allowed = Object.values(MedicationDoseStatus);
const status = allowed.includes(body.status as MedicationDoseStatus) ? body.status as MedicationDoseStatus : MedicationDoseStatus.NEEDS_FAMILY_CONFIRMATION;
const dose = await updateMedicationDose(deviceUuid, String(body.doseId || ""), status, body.note, Number(body.snoozeMinutes || 10));
return NextResponse.json({ ok: true, dose });
}
if (body.type === "CHECK_IN") {
const status = body.status === CheckInStatus.SAFE
? CheckInStatus.SAFE
: body.status === CheckInStatus.DECLINED
? CheckInStatus.DECLINED
: CheckInStatus.NEEDS_CONTACT;
const checkIn = await respondCheckIn(deviceUuid, String(body.checkInId || ""), status, body.responseText);
return NextResponse.json({ ok: true, checkIn });
}
throw new Error("不支持的设备事件类型。");
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "保存照护反馈失败。" }, { status: 400 }); }
}
+3 -1
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { markFamilyMessagesAsRead } from "@/lib/monitor-data";
import { requireDeviceAuth } from "@/lib/device-auth";
export async function POST(request: Request) {
const body = (await request.json().catch(() => null)) as
@@ -24,6 +25,7 @@ export async function POST(request: Request) {
: undefined;
try {
await requireDeviceAuth(request, body.deviceUuid);
await markFamilyMessagesAsRead(body.deviceUuid, messageIds);
return NextResponse.json({ ok: true });
@@ -36,4 +38,4 @@ export async function POST(request: Request) {
{ status: 400 },
);
}
}
}
+3 -1
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { listFamilyMessagesForDevice } from "@/lib/monitor-data";
import { requireDeviceAuth } from "@/lib/device-auth";
export async function GET(request: NextRequest) {
const rawDeviceValue =
@@ -20,6 +21,7 @@ export async function GET(request: NextRequest) {
}
try {
await requireDeviceAuth(request, rawDeviceValue);
const data = await listFamilyMessagesForDevice(rawDeviceValue, take);
return NextResponse.json({
@@ -45,4 +47,4 @@ export async function GET(request: NextRequest) {
{ status: 400 },
);
}
}
}
+14 -1
View File
@@ -1,6 +1,8 @@
import { NextResponse } from "next/server";
import { createBindUrl, ensureDeviceRegistration } from "@/lib/monitor-data";
import { createDeviceToken, hashDeviceToken } from "@/lib/device-auth";
import { prisma } from "@/lib/prisma";
export async function POST(request: Request) {
const body = (await request.json().catch(() => null)) as
@@ -8,6 +10,7 @@ export async function POST(request: Request) {
deviceUuid?: string;
displayName?: string;
appVersion?: string;
deviceToken?: string;
}
| null;
@@ -26,10 +29,20 @@ export async function POST(request: Request) {
appVersion:
typeof body.appVersion === "string" ? body.appVersion : undefined,
});
const suppliedToken = typeof body.deviceToken === "string" ? body.deviceToken.trim() : "";
const tokenMatches = suppliedToken && device.deviceTokenHash === hashDeviceToken(suppliedToken);
if (device.deviceTokenHash && !tokenMatches) {
return NextResponse.json({ error: "设备认证失败,不能覆盖已注册设备。" }, { status: 401 });
}
const deviceToken = tokenMatches ? suppliedToken : createDeviceToken();
if (!device.deviceTokenHash) {
await prisma.elderDevice.update({ where: { id: device.id }, data: { deviceTokenHash: hashDeviceToken(deviceToken) } });
}
return NextResponse.json({
deviceUuid: device.deviceUuid,
bindUrl: createBindUrl(device.deviceUuid),
deviceToken,
});
} catch (error) {
return NextResponse.json(
@@ -40,4 +53,4 @@ export async function POST(request: Request) {
{ status: 500 },
);
}
}
}
+3 -1
View File
@@ -2,6 +2,7 @@ import { ToolCallStatus } from "@prisma/client";
import { NextResponse } from "next/server";
import { recordToolCall } from "@/lib/monitor-data";
import { requireDeviceAuth } from "@/lib/device-auth";
function toToolCallStatus(rawStatus: unknown) {
return rawStatus === ToolCallStatus.FAILURE
@@ -49,6 +50,7 @@ export async function POST(request: Request) {
}
try {
await requireDeviceAuth(request, body.deviceUuid);
const log = await recordToolCall({
deviceUuid: body.deviceUuid,
toolName: body.toolName,
@@ -70,4 +72,4 @@ export async function POST(request: Request) {
{ status: 400 },
);
}
}
}
+3 -1
View File
@@ -2,6 +2,7 @@ import { UsageEventType } from "@prisma/client";
import { NextResponse } from "next/server";
import { recordUsageEvent } from "@/lib/monitor-data";
import { requireDeviceAuth } from "@/lib/device-auth";
function toUsageEventType(rawType: unknown) {
switch (rawType) {
@@ -46,6 +47,7 @@ export async function POST(request: Request) {
}
try {
await requireDeviceAuth(request, body.deviceUuid);
await recordUsageEvent({
deviceUuid: body.deviceUuid,
eventType: toUsageEventType(body.eventType),
@@ -62,4 +64,4 @@ export async function POST(request: Request) {
{ status: 400 },
);
}
}
}
+12
View File
@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { createFamilyInvitation } from "@/lib/family-guard";
import { getCaregiverSession } from "@/lib/session";
export async function POST(request: Request) {
try {
const caregiver = await getCaregiverSession();
if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 });
const result = await createFamilyInvitation(caregiver.id, await request.json().catch(() => ({})));
return NextResponse.json({ ok: true, ...result });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "创建邀请失败。" }, { status: 400 }); }
}
+13
View File
@@ -0,0 +1,13 @@
import { NextResponse } from "next/server";
import { acceptFamilyInvitation } from "@/lib/family-guard";
import { getCaregiverSession } from "@/lib/session";
export async function POST(request: Request) {
try {
const caregiver = await getCaregiverSession();
if (!caregiver) return NextResponse.json({ error: "请先登录后接受邀请。" }, { status: 401 });
const body = await request.json().catch(() => ({})) as { token?: string };
const circle = await acceptFamilyInvitation(caregiver.id, body.token || "");
return NextResponse.json({ ok: true, circleId: circle.id });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "加入家庭失败。" }, { status: 400 }); }
}
+13
View File
@@ -0,0 +1,13 @@
import { NextResponse } from "next/server";
import { createFamilyPost } from "@/lib/family-guard";
import { getCaregiverSession } from "@/lib/session";
export async function POST(request: Request) {
try {
const caregiver = await getCaregiverSession();
if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 });
const body = await request.json().catch(() => ({}));
const post = await createFamilyPost(caregiver.id, body);
return NextResponse.json({ ok: true, post });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "发布失败。" }, { status: 400 }); }
}
+12
View File
@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { createMedicationPlan } from "@/lib/family-guard";
import { getCaregiverSession } from "@/lib/session";
export async function POST(request: Request) {
try {
const caregiver = await getCaregiverSession();
if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 });
const plan = await createMedicationPlan(caregiver.id, await request.json().catch(() => ({})));
return NextResponse.json({ ok: true, plan });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "创建用药计划失败。" }, { status: 400 }); }
}
+12
View File
@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { createFamilyMemo } from "@/lib/family-guard";
import { getCaregiverSession } from "@/lib/session";
export async function POST(request: Request) {
try {
const caregiver = await getCaregiverSession();
if (!caregiver) return NextResponse.json({ error: "请先登录。" }, { status: 401 });
const memo = await createFamilyMemo(caregiver.id, await request.json().catch(() => ({})));
return NextResponse.json({ ok: true, memo });
} catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : "保存失败。" }, { status: 400 }); }
}