feat(homeworks,island,classisland,desktop,web,server): 作业按天查看与教室机器录入 + 提醒上课时段暂存规则 + 灵动岛边距/跟随鼠标屏幕

- 作业:新增 Homework.assignDate(本地日期 YYYY-MM-DD;UTC 切片在晚上会把今天的作业算成明天),列表按天过滤与排序,新增 GET /homeworks?date= 与 GET /homeworks/days(日期高亮)供客户端与 Web 共用;客户端作业页改为"看作业 + 录作业"(不再显示完成状态),支持快捷短语(存本机配置,「设置 → 作业录入」可增删/恢复默认)
- 教室机器录入:POST /homeworks 去掉 requireRole,权限下沉到 homeworks.service(staff 或本班班级账号);班级账号的 sub 是班级 id,写 createdBy 前换成该班班主任,避免撞 User 外键
- 提醒类型:ClassIslandPush.kind(notification / call)。通知类上课时段暂存、下课补弹,叫人/紧急立即弹;客户端灵动岛与 ClassIsland 插件共用同一套规则
- 灵动岛:新增 marginX / marginY(默认仍 8px,不动老外观)与「跟随鼠标屏幕」;文案统一成"灵动岛 / ClassIsland"
- 设置页:新增「ClassIsland 联动」(联动状态 + 通知模式)与「作业录入」分区
- 校验:verify:e2e 增补 assignDate 默认当天 / ?date= 过滤 / days / 非法日期 422 / 班级账号录入 201 与跨班 403;verify:classisland 与插件静态契约增补上课时段暂存断言(57/57)
- 文档:README 与 AGENTS.md 同步(作业按天、上课时段策略、两个方向的开关表、新增踩坑)

门禁:pnpm lint 0 error、pnpm typecheck 四包通过、verify:classisland-plugin 57/57 通过。
未跑:verify:e2e / verify:classisland(需后端启动)、verify:web / verify:desktop(需先构建产物)。
This commit is contained in:
LoveChengke committed 2026-09-27 16:34:01 +08:00
1 parent d0830f6830
commit 7536abfc95
33 files changed
+1236 -162

No files matched your search

@@ -304,6 +304,12 @@ async function main() {
published.status === 201 && Boolean(publishPush),
`status=${published.status} 待提醒=${(pendingAfterPublish.payload?.data?.notifications ?? []).length}`,
);
// kind 决定插件"上课时段弹不弹":通知类会被暂存,叫人属于主动通知要立刻弹
record(
'通知类推送的 kind=notification(上课时段插件会暂存)',
publishPush?.kind === 'notification',
`kind=${publishPush?.kind}`,
);
if (publishPush) {
await api('/integrations/classisland/ack', {
method: 'POST',
@@ -326,6 +332,11 @@ async function main() {
Boolean(firstStudent) && call.status === 201 && Boolean(callPush),
firstStudent ? `status=${call.status}` : '班级里没有学生,跳过',
);
record(
'叫人推送的 kind=call(算主动通知,上课时段也立刻弹)',
callPush?.kind === 'call',
`kind=${callPush?.kind}`,
);
if (callPush) {
await api('/integrations/classisland/ack', {
method: 'POST',
+70
View File
@@ -455,6 +455,36 @@ async function main() {
createdHomework.payload?.data?.dueAt === undefined,
`dueAt=${String(createdHomework.payload?.data?.dueAt)}`,
);
// 按天查看:作业带"所属日期"(本地日期),并且能按它过滤
const assignDate = createdHomework.payload?.data?.assignDate;
record(
'作业带所属日期(assignDate,默认服务器当天)',
typeof assignDate === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(assignDate),
`assignDate=${assignDate}`,
);
const byDay = await api(`/homeworks?classId=${classId}&date=${assignDate}`, { token: teacherToken });
record(
'按天查看(?date=)只返回该天的作业',
byDay.status === 200 &&
(byDay.payload?.data ?? []).length > 0 &&
(byDay.payload?.data ?? []).every((item) => item.assignDate === assignDate),
`当天条数=${(byDay.payload?.data ?? []).length}`,
);
const otherDay = await api(`/homeworks?classId=${classId}&date=2000-01-01`, { token: teacherToken });
record(
'没有作业的日期返回空列表',
otherDay.status === 200 && (otherDay.payload?.data ?? []).length === 0,
`条数=${(otherDay.payload?.data ?? []).length}`,
);
const dayMarks = await api(`/homeworks/days?classId=${classId}`, { token: teacherToken });
const marks = dayMarks.payload?.data?.days ?? [];
record(
'作业日历返回有作业的日期(供日期选择器高亮)',
dayMarks.status === 200 && marks.some((item) => item.date === assignDate && item.count >= 1),
`日期数=${marks.length} 命中=${marks.find((item) => item.date === assignDate)?.count ?? 0}`,
);
const badDay = await api(`/homeworks/days?classId=${classId}&from=2026-13-45`, { token: teacherToken });
record('作业日历非法日期被拒(422)', badDay.status === 422, `status=${badDay.status}`);
try {
const { payload, elapsed } = await homeworkWait;
@@ -1867,6 +1897,46 @@ async function main() {
`status=${classScheduleWrite.status}`,
);
// 教室机器录入作业(需求:支持作业在客户端录入):班级账号可以发,跨班不行
const classHomeworkCreate = await api('/homeworks', {
method: 'POST',
token: classToken,
body: {
classId,
title: '教室录入验证作业',
content: 'P 大本 第 3 课',
assignDate: '2026-03-02',
},
});
record(
'班级账号可在教室机器上录入作业',
classHomeworkCreate.status === 201 && classHomeworkCreate.payload?.data?.assignDate === '2026-03-02',
`status=${classHomeworkCreate.status} assignDate=${classHomeworkCreate.payload?.data?.assignDate}`,
);
if (foreignClass) {
const classHomeworkCreateCross = await api('/homeworks', {
method: 'POST',
token: classToken,
body: { classId: foreignClass.id, title: '跨班录入作业', content: '不应成功' },
});
record(
'班级账号跨班录入作业被拒绝(403)',
classHomeworkCreateCross.status === 403,
`status=${classHomeworkCreateCross.status}`,
);
}
if (classHomeworkCreate.payload?.data?.id) {
const cleanupHomework = await api(`/homeworks/${classHomeworkCreate.payload.data.id}`, {
method: 'DELETE',
token: teacherToken,
});
record(
'清理:教室录入的验证作业已删除',
cleanupHomework.status === 200,
`status=${cleanupHomework.status}`,
);
}
// 班级设备代全班操作:通知已读
const classNameNotice = await api('/notifications', {
method: 'POST',