docs(student-portal): 落实 ARB-019 全部执行项 + 粘贴策略分层 + ExamForceSubmitted + mutation命名对齐 + 工作进度更新
This commit is contained in:
@@ -211,7 +211,7 @@ type AnswerInput =
|
|||||||
|
|
||||||
// 可疑行为记录(防作弊,前端仅记录,服务端最终判定)
|
// 可疑行为记录(防作弊,前端仅记录,服务端最终判定)
|
||||||
interface SuspiciousBehaviorRecord {
|
interface SuspiciousBehaviorRecord {
|
||||||
type: "tab-switch" | "copy-paste" | "fullscreen-exit" | "multi-tab" | "window-blur";
|
type: "tab-switch" | "copy" | "paste" | "paste-essay" | "fullscreen-exit" | "multi-tab" | "window-blur";
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
duration?: number; // 持续时长(ms)
|
duration?: number; // 持续时长(ms)
|
||||||
details?: string;
|
details?: string;
|
||||||
@@ -353,9 +353,9 @@ const EXAM_DETAIL = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SAVE_EXAM_ANSWER = gql`
|
const SAVE_EXAM_DRAFT = gql`
|
||||||
mutation SaveExamAnswer($examId: ID!, $questionId: ID!, $answer: AnswerInput!) {
|
mutation SaveExamDraft($examId: ID!, $questionId: ID!, $answer: AnswerInput!) {
|
||||||
saveExamAnswer(examId: $examId, questionId: $questionId, answer: $answer) {
|
saveExamDraft(examId: $examId, questionId: $questionId, answer: $answer) {
|
||||||
savedAt
|
savedAt
|
||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
@@ -401,15 +401,15 @@ export function useExamDetail(examId: string) {
|
|||||||
| query | `examResult` | 考试结果 | P3 |
|
| query | `examResult` | 考试结果 | P3 |
|
||||||
| query | `myHomework` | 我的作业列表 | P3 |
|
| query | `myHomework` | 我的作业列表 | P3 |
|
||||||
| query | `homeworkDetail` | 作业详情 | P3 |
|
| query | `homeworkDetail` | 作业详情 | P3 |
|
||||||
| mutation | `submitHomework` | 提交作业 | P3 |
|
|
||||||
| query | `myGrades` | 我的成绩列表 | P3 |
|
| query | `myGrades` | 我的成绩列表 | P3 |
|
||||||
| query | `myAttendance` | 我的考勤 | P3 |
|
| query | `myAttendance` | 我的考勤 | P3 |
|
||||||
| query | `studentDashboard` | 学生仪表盘聚合 | P3 |
|
| query | `studentDashboard` | 学生仪表盘聚合 | P3 |
|
||||||
| query | `serverTime` | 服务器时间(倒计时校正) | P3 |
|
| query | `serverTime` | 服务器时间(倒计时校正) | P3 |
|
||||||
| mutation | `saveExamAnswer` | 保存考试答案(自动保存) | P3 |
|
| mutation | `submitHomework` | 提交作业(含 attachmentUrls) | P3 |
|
||||||
|
| mutation | `saveExamDraft` | 保存考试草稿(自动保存) | P3 |
|
||||||
| mutation | `submitExam` | 提交考试 | P3 |
|
| mutation | `submitExam` | 提交考试 | P3 |
|
||||||
| mutation | `uploadAttachment` | 上传附件 | P3 |
|
| mutation | `recordExamViolation` | 记录防作弊违规(ARB-019 §21.4) | P3 |
|
||||||
| mutation | `recordExamSuspiciousBehavior` | 记录可疑行为 | P3 |
|
| mutation | `recordPasteEvent` | 记录粘贴事件(ARB-019 §21.4) | P3 |
|
||||||
| query | `textbooks` | 教材列表 | P4 |
|
| query | `textbooks` | 教材列表 | P4 |
|
||||||
| query | `chapters` | 章节列表 | P4 |
|
| query | `chapters` | 章节列表 | P4 |
|
||||||
| query | `learningPath` | 学习路径 | P4 |
|
| query | `learningPath` | 学习路径 | P4 |
|
||||||
@@ -417,6 +417,10 @@ export function useExamDetail(examId: string) {
|
|||||||
| query | `myTrend` | 学习趋势 | P4 |
|
| query | `myTrend` | 学习趋势 | P4 |
|
||||||
| query | `myNotifications` | 通知列表 | P5 |
|
| query | `myNotifications` | 通知列表 | P5 |
|
||||||
| mutation | `markAsRead` | 标记通知已读 | P5 |
|
| mutation | `markAsRead` | 标记通知已读 | P5 |
|
||||||
|
| mutation | `markAllAsRead` | 全部标记已读 | P5 |
|
||||||
|
| mutation | `updateNotificationPreference` | 更新通知偏好 | P5 |
|
||||||
|
|
||||||
|
> **附件上传**(ISSUE-014-05 已裁决方案 A):走 api-gateway REST `POST /api/v1/student/upload`(multipart/form-data)→ 对象存储 → 返回 signed URL → GraphQL `submitHomework(input: { attachmentUrls })` 提交 URL。不使用 GraphQL multipart mutation。
|
||||||
|
|
||||||
### 4.3 Query Key 命名约定
|
### 4.3 Query Key 命名约定
|
||||||
|
|
||||||
@@ -447,8 +451,9 @@ queryKey: ["session", "viewports", "student"]; // 复用 Shell
|
|||||||
| ----------------------------- | -------------- | ------------------------------------------------------------------------------------ |
|
| ----------------------------- | -------------- | ------------------------------------------------------------------------------------ |
|
||||||
| `NotificationRequested` | msg 服务投递 | toast 提示 + 通知中心未读数 +1 + `["student","notifications"]` invalidate |
|
| `NotificationRequested` | msg 服务投递 | toast 提示 + 通知中心未读数 +1 + `["student","notifications"]` invalidate |
|
||||||
| `ExamPublished` | 教师发布考试 | toast + `["student","exams"]` invalidate + `["student","dashboard"]` invalidate |
|
| `ExamPublished` | 教师发布考试 | toast + `["student","exams"]` invalidate + `["student","dashboard"]` invalidate |
|
||||||
| `ExamExtended` | 教师延长考试 | 重新拉取 `exam` query → 更新 `expiresAt` → 重置倒计时(待 ai10 msg 确认事件名) |
|
| `ExamExtended` | 教师延长考试 | 重新拉取 `exam` query → 更新 `expiresAt` → 重置倒计时(ARB-019 §21.6 已确认) |
|
||||||
| `ExamQuestionReordered` | 教师调整题目顺序 | 重新拉取 `exam` query → 草稿按 questionId 映射(不依赖序号)(待 ai10 确认) |
|
| `ExamForceSubmitted` | 教师强制收卷 | 立即触发 `submitExam` mutation → 跳转结果页(ARB-019 §21.6 已确认) |
|
||||||
|
| `ExamQuestionReordered` | 教师调整题目顺序 | P3 不实现;P4 评估实时重排需求(ARB-019 §21.6 已确认) |
|
||||||
| `GradeRecorded` | 教师录入成绩 | toast + `["student","grades"]` invalidate + `["student","dashboard"]` invalidate |
|
| `GradeRecorded` | 教师录入成绩 | toast + `["student","grades"]` invalidate + `["student","dashboard"]` invalidate |
|
||||||
| `HomeworkDeadlineApproaching` | 作业截止前提醒 | toast + 作业列表高亮 + `["student","homework"]` invalidate |
|
| `HomeworkDeadlineApproaching` | 作业截止前提醒 | toast + 作业列表高亮 + `["student","homework"]` invalidate |
|
||||||
|
|
||||||
@@ -517,6 +522,13 @@ function handlePushMessage(msg: PushMessage, queryClient: QueryClient) {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["student", "exam", msg.payload.examId] });
|
queryClient.invalidateQueries({ queryKey: ["student", "exam", msg.payload.examId] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["student", "exams"] });
|
queryClient.invalidateQueries({ queryKey: ["student", "exams"] });
|
||||||
break;
|
break;
|
||||||
|
case "ExamForceSubmitted":
|
||||||
|
// 教师强制收卷:立即触发提交,跳转结果页
|
||||||
|
toast.warning(t("exam.forceSubmitted"));
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["student", "exam", msg.payload.examId] });
|
||||||
|
// 触发 submitExam mutation(由 useExamTaking hook 内部处理)
|
||||||
|
window.dispatchEvent(new CustomEvent("exam-force-submit", { detail: { examId: msg.payload.examId } }));
|
||||||
|
break;
|
||||||
case "ExamQuestionReordered":
|
case "ExamQuestionReordered":
|
||||||
queryClient.invalidateQueries({ queryKey: ["student", "exam", msg.payload.examId] });
|
queryClient.invalidateQueries({ queryKey: ["student", "exam", msg.payload.examId] });
|
||||||
break;
|
break;
|
||||||
@@ -539,7 +551,8 @@ function handlePushMessage(msg: PushMessage, queryClient: QueryClient) {
|
|||||||
| ------------------- | --------------------------------- | --------------------------------------------------- |
|
| ------------------- | --------------------------------- | --------------------------------------------------- |
|
||||||
| `visibilitychange` | 切屏检测 | 记录切换次数 + 时长;超阈值警告;上报服务端 |
|
| `visibilitychange` | 切屏检测 | 记录切换次数 + 时长;超阈值警告;上报服务端 |
|
||||||
| `blur` / `focus` | 窗口失焦检测 | 同上 |
|
| `blur` / `focus` | 窗口失焦检测 | 同上 |
|
||||||
| `copy` / `paste` | 复制粘贴检测 | 阻止默认行为 + 警告;主观题允许粘贴自己输入(待确认)|
|
| `copy` | 复制检测 | 全局阻止 + 警告 + 记录 |
|
||||||
|
| `paste` | 粘贴检测(按题型分层) | 客观题阻止 + 警告;主观题允许 + 记录长度;作文题允许不记录(ARB-019 §21.4) |
|
||||||
| `fullscreenchange` | 全屏退出检测 | 警告 + 记录 |
|
| `fullscreenchange` | 全屏退出检测 | 警告 + 记录 |
|
||||||
| `contextmenu` | 右键禁用 | 考试作答页 `preventDefault()` |
|
| `contextmenu` | 右键禁用 | 考试作答页 `preventDefault()` |
|
||||||
|
|
||||||
@@ -679,7 +692,7 @@ Next.js 无长连接(除 WS),无需特殊处理。WS 在 P5 由 push-gatew
|
|||||||
| `useMyExams(status?)` | 我的考试列表 |
|
| `useMyExams(status?)` | 我的考试列表 |
|
||||||
| `useExamDetail(examId)` | 考试详情(含题目,禁缓存) |
|
| `useExamDetail(examId)` | 考试详情(含题目,禁缓存) |
|
||||||
| `useExamTaking(examId)` | 考试作答状态(草稿 + 倒计时 + 自动保存 + 断网队列) |
|
| `useExamTaking(examId)` | 考试作答状态(草稿 + 倒计时 + 自动保存 + 断网队列) |
|
||||||
| `useSaveExamAnswer()` | 保存答案 mutation(自动保存) |
|
| `useSaveExamDraft()` | 保存草稿 mutation(自动保存) |
|
||||||
| `useSubmitExam()` | 提交考试 mutation |
|
| `useSubmitExam()` | 提交考试 mutation |
|
||||||
| `useMyHomework(status?)` | 我的作业列表 |
|
| `useMyHomework(status?)` | 我的作业列表 |
|
||||||
| `useSubmitHomework()` | 提交作业 mutation |
|
| `useSubmitHomework()` | 提交作业 mutation |
|
||||||
@@ -739,7 +752,7 @@ student-portal 的 ESLint flat config 与 Shell 共用,确保规则一致。
|
|||||||
3. **假设 ai04 student-bff 提供 GraphQL endpoint `POST /graphql`**:含 §4.2 全部 query/mutation。错误码前缀 `BFF_STUDENT_`(ARB-001 已确认)。
|
3. **假设 ai04 student-bff 提供 GraphQL endpoint `POST /graphql`**:含 §4.2 全部 query/mutation。错误码前缀 `BFF_STUDENT_`(ARB-001 已确认)。
|
||||||
4. **假设 teacher-portal Shell 已就绪**:AppShell + 共享依赖暴露 + MF 配置 + GraphQLProvider(P2 收尾完成)。student-portal 作为 Remote 才能挂载。
|
4. **假设 teacher-portal Shell 已就绪**:AppShell + 共享依赖暴露 + MF 配置 + GraphQLProvider(P2 收尾完成)。student-portal 作为 Remote 才能挂载。
|
||||||
5. **假设 Next.js 14+ Module Federation 2.0 稳定**:`@module-federation/nextjs-mf` 在 Next.js App Router + urql singleton 下可用。若不稳定,降级为 4 端独立部署 + 各自 Shell(重复实现 AppShell + GraphQLProvider)。
|
5. **假设 Next.js 14+ Module Federation 2.0 稳定**:`@module-federation/nextjs-mf` 在 Next.js App Router + urql singleton 下可用。若不稳定,降级为 4 端独立部署 + 各自 Shell(重复实现 AppShell + GraphQLProvider)。
|
||||||
6. **假设 ai10 msg 提供 WebSocket 事件**:`ExamExtended`/`ExamQuestionReordered` 事件名待 ai10 确认(见 issues/contracts)。
|
6. **假设 ai10 msg 提供 WebSocket 事件**:`ExamExtended`/`ExamForceSubmitted`/`ExamQuestionReordered` 事件名已由 ARB-019 §21.6 裁决确认(见 issues/contracts)。
|
||||||
|
|
||||||
### 11.2 技术风险
|
### 11.2 技术风险
|
||||||
|
|
||||||
@@ -764,10 +777,10 @@ student-portal 的 ESLint flat config 与 Shell 共用,确保规则一致。
|
|||||||
| GraphQL vs REST | **GraphQL**(ARB-001 已裁决) | ✅ 已裁决 |
|
| GraphQL vs REST | **GraphQL**(ARB-001 已裁决) | ✅ 已裁决 |
|
||||||
| MF Shell 暴露清单 | **GraphQLProvider + urql singleton** | ✅ 已裁决(ARB-002)|
|
| MF Shell 暴露清单 | **GraphQLProvider + urql singleton** | ✅ 已裁决(ARB-002)|
|
||||||
| packages 归属 | ai13 维护 ui-*/hooks,coord 维护 shared-ts/contracts | ✅ 已确认 |
|
| packages 归属 | ai13 维护 ui-*/hooks,coord 维护 shared-ts/contracts | ✅ 已确认 |
|
||||||
| 考试作答防作弊事件名 | `ExamExtended`/`ExamQuestionReordered`| ⚠️ 待 ai10 msg 确认 |
|
| 考试作答防作弊事件名 | `ExamExtended`/`ExamForceSubmitted`/`ExamQuestionReordered` | ✅ 已裁决(ARB-019 §21.6) |
|
||||||
| 附件上传协议 | GraphQL mutation `uploadAttachment` | ⚠️ 待 ai04 确认是否走 multipart |
|
| 附件上传协议 | REST `POST /api/v1/student/upload` + 对象存储 + GraphQL 提交 URL | ✅ 已裁决(ARB-019 §21.5,方案 A) |
|
||||||
| 主观题是否允许粘贴 | 阻止粘贴(待产品确认) | ⚠️ 待产品决策 |
|
| 主观题粘贴策略 | 客观题禁止/主观题允许记录/作文题允许不记录 | ✅ 已裁决(ARB-019 §21.4) |
|
||||||
| 考试作答页是否强制全屏 | 不强制(P6+ lockdown 浏览器才强制) | ⚠️ 待产品决策 |
|
| 考试作答页全屏策略 | P3 不强制(提示"建议全屏作答");P4 评估升级 | ✅ 已裁决(ARB-019 §21.4) |
|
||||||
|
|
||||||
## 12. coord 交叉审查所需信息
|
## 12. coord 交叉审查所需信息
|
||||||
|
|
||||||
@@ -798,7 +811,7 @@ student-portal 的 ESLint flat config 与 Shell 共用,确保规则一致。
|
|||||||
| `/exams/*` `/homework/*` `/grades/*`(gRPC,经 BFF) | core-edu | ✅ 已实现(P3) |
|
| `/exams/*` `/homework/*` `/grades/*`(gRPC,经 BFF) | core-edu | ✅ 已实现(P3) |
|
||||||
| `/analytics/*`(gRPC,经 BFF) | data-ana | ✅ 已实现(P4) |
|
| `/analytics/*`(gRPC,经 BFF) | data-ana | ✅ 已实现(P4) |
|
||||||
| `/notifications/*` + WebSocket 推送 | msg + push-gateway | 📐 待 P5 |
|
| `/notifications/*` + WebSocket 推送 | msg + push-gateway | 📐 待 P5 |
|
||||||
| WebSocket 事件 `ExamExtended`/`ExamQuestionReordered` | msg | ⚠️ 待 ai10 确认 |
|
| WebSocket 事件 `ExamExtended`/`ExamForceSubmitted` | msg + push-gateway | ✅ 已裁决(ARB-019 §21.6) |
|
||||||
|
|
||||||
### 12.4 错误码前缀(前端 i18n 路由依赖)
|
### 12.4 错误码前缀(前端 i18n 路由依赖)
|
||||||
|
|
||||||
@@ -1092,13 +1105,13 @@ export function useExamTaking(examId: string) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await client
|
const result = await client
|
||||||
.mutation(SAVE_EXAM_ANSWER, {
|
.mutation(SAVE_EXAM_DRAFT, {
|
||||||
examId,
|
examId,
|
||||||
questionId: "batch", // 批量保存
|
questionId: "batch", // 批量保存
|
||||||
answer: draft.answers,
|
answer: draft.answers,
|
||||||
})
|
})
|
||||||
.toPromise();
|
.toPromise();
|
||||||
if (result.data?.saveExamAnswer?.ok) {
|
if (result.data?.saveExamDraft?.ok) {
|
||||||
setDraft((prev) => ({ ...prev!, lastSavedAt: Date.now() }));
|
setDraft((prev) => ({ ...prev!, lastSavedAt: Date.now() }));
|
||||||
await saveDraftToIDB(draft); // 同步到 IDB
|
await saveDraftToIDB(draft); // 同步到 IDB
|
||||||
}
|
}
|
||||||
@@ -1139,13 +1152,13 @@ export function useExamTaking(examId: string) {
|
|||||||
useOfflineQueue_recovery(async (item) => {
|
useOfflineQueue_recovery(async (item) => {
|
||||||
try {
|
try {
|
||||||
const result = await client
|
const result = await client
|
||||||
.mutation(SAVE_EXAM_ANSWER, {
|
.mutation(SAVE_EXAM_DRAFT, {
|
||||||
examId: item.examId,
|
examId: item.examId,
|
||||||
questionId: item.questionId,
|
questionId: item.questionId,
|
||||||
answer: item.answer,
|
answer: item.answer,
|
||||||
})
|
})
|
||||||
.toPromise();
|
.toPromise();
|
||||||
return result.data?.saveExamAnswer?.ok ?? false;
|
return result.data?.saveExamDraft?.ok ?? false;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1202,7 +1215,7 @@ export function useAntiCheat(examId: string) {
|
|||||||
const batch = behaviorsRef.current.splice(0);
|
const batch = behaviorsRef.current.splice(0);
|
||||||
try {
|
try {
|
||||||
await client
|
await client
|
||||||
.mutation(RECORD_SUSPICIOUS_BEHAVIOR, {
|
.mutation(RECORD_EXAM_VIOLATION, {
|
||||||
examId,
|
examId,
|
||||||
behaviors: batch,
|
behaviors: batch,
|
||||||
})
|
})
|
||||||
@@ -1230,26 +1243,57 @@ export function useAntiCheat(examId: string) {
|
|||||||
return () => document.removeEventListener("visibilitychange", handleVisibility);
|
return () => document.removeEventListener("visibilitychange", handleVisibility);
|
||||||
}, [record]);
|
}, [record]);
|
||||||
|
|
||||||
// 复制粘贴检测
|
// 复制检测(全局阻止)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleCopy = (e: ClipboardEvent) => {
|
const handleCopy = (e: ClipboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
record("copy-paste", "copy attempted");
|
record("copy", "copy attempted");
|
||||||
toast.warning(t("exam.antiCheat.copyDisabled"));
|
toast.warning(t("exam.antiCheat.copyDisabled"));
|
||||||
};
|
};
|
||||||
const handlePaste = (e: ClipboardEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
record("copy-paste", "paste attempted");
|
|
||||||
toast.warning(t("exam.antiCheat.pasteDisabled"));
|
|
||||||
};
|
|
||||||
document.addEventListener("copy", handleCopy);
|
document.addEventListener("copy", handleCopy);
|
||||||
document.addEventListener("paste", handlePaste);
|
return () => document.removeEventListener("copy", handleCopy);
|
||||||
return () => {
|
|
||||||
document.removeEventListener("copy", handleCopy);
|
|
||||||
document.removeEventListener("paste", handlePaste);
|
|
||||||
};
|
|
||||||
}, [record]);
|
}, [record]);
|
||||||
|
|
||||||
|
// 粘贴检测(按题型分层,ARB-019 §21.4 ISSUE-014-04 已裁决)
|
||||||
|
// - 客观题(single-choice/multiple-choice/fill-blank):禁止粘贴
|
||||||
|
// - 主观题(short-answer):允许粘贴,记录粘贴事件 + 内容长度
|
||||||
|
// - 作文题(essay):允许粘贴,不记录
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePaste = (e: ClipboardEvent) => {
|
||||||
|
const activeQuestion = getActiveQuestion(); // 当前焦点题目
|
||||||
|
if (!activeQuestion) return;
|
||||||
|
|
||||||
|
const questionType = activeQuestion.answerType;
|
||||||
|
const pastedLength = e.clipboardData?.getData("text")?.length ?? 0;
|
||||||
|
|
||||||
|
switch (questionType) {
|
||||||
|
case "single-choice":
|
||||||
|
case "multiple-choice":
|
||||||
|
case "fill-blank":
|
||||||
|
// 客观题:禁止粘贴
|
||||||
|
e.preventDefault();
|
||||||
|
record("paste", `paste blocked on ${questionType}, length=${pastedLength}`);
|
||||||
|
toast.warning(t("exam.antiCheat.pasteDisabled"));
|
||||||
|
break;
|
||||||
|
case "short-answer":
|
||||||
|
// 主观题:允许粘贴,记录事件 + 内容长度
|
||||||
|
record("paste", `paste allowed on short-answer, length=${pastedLength}`);
|
||||||
|
// 走 recordPasteEvent mutation 上报(ARB-019 §21.4)
|
||||||
|
client.mutation(RECORD_PASTE_EVENT, {
|
||||||
|
examId,
|
||||||
|
questionId: activeQuestion.id,
|
||||||
|
pasteLength: pastedLength,
|
||||||
|
}).toPromise().catch(() => {});
|
||||||
|
break;
|
||||||
|
case "essay":
|
||||||
|
// 作文题:允许粘贴,不记录
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("paste", handlePaste);
|
||||||
|
return () => document.removeEventListener("paste", handlePaste);
|
||||||
|
}, [record, examId, client]);
|
||||||
|
|
||||||
// 全屏退出检测
|
// 全屏退出检测
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFullscreen = () => {
|
const handleFullscreen = () => {
|
||||||
@@ -2039,7 +2083,7 @@ graph LR
|
|||||||
| MF Remote 加载失败的兜底 | 待实现 | 中 | P3 实现期落地 Shell 内置兜底页 |
|
| MF Remote 加载失败的兜底 | 待实现 | 中 | P3 实现期落地 Shell 内置兜底页 |
|
||||||
| 跨 tab 同步在 Safari 的兼容性 | 待验证 | 低 | P6 硬化期验证;降级为 storage 事件 |
|
| 跨 tab 同步在 Safari 的兼容性 | 待验证 | 低 | P6 硬化期验证;降级为 storage 事件 |
|
||||||
| 考试作答 IDB 存储空间管理 | 待实现 | 中 | P3 实现期落地定期清理逻辑 |
|
| 考试作答 IDB 存储空间管理 | 待实现 | 中 | P3 实现期落地定期清理逻辑 |
|
||||||
| WebSocket 事件名待 ai10 确认 | 待 ai10 | 中 | P5 启动前确认 `ExamExtended`/`ExamQuestionReordered` |
|
| WebSocket 事件名已裁决 | 已裁决 | 中 | ✅ ARB-019 §21.6 确认 `ExamExtended`/`ExamForceSubmitted` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -260,9 +260,44 @@ gantt
|
|||||||
| student-bff GraphQL schema 未就绪(ai04 P3 延迟) | 高 | MSW mock 全量 query/mutation,schema 就绪后切换;ai14 自行维护 mock schema 用于 codegen |
|
| student-bff GraphQL schema 未就绪(ai04 P3 延迟) | 高 | MSW mock 全量 query/mutation,schema 就绪后切换;ai14 自行维护 mock schema 用于 codegen |
|
||||||
| 考试作答断网恢复逻辑复杂(IDB 队列 + 服务器时间对齐)| 高 | 02 §14 已设计完整状态机 + 时间同步算法;P3 优先实现核心链路,P6 验证降级场景 |
|
| 考试作答断网恢复逻辑复杂(IDB 队列 + 服务器时间对齐)| 高 | 02 §14 已设计完整状态机 + 时间同步算法;P3 优先实现核心链路,P6 验证降级场景 |
|
||||||
| MF Remote 加载失败(Shell 未就绪或版本不兼容) | 中 | 02 §3.2 已设计独立壳回退(`NEXT_PUBLIC_MF_ENABLED=false` 时独立渲染) |
|
| MF Remote 加载失败(Shell 未就绪或版本不兼容) | 中 | 02 §3.2 已设计独立壳回退(`NEXT_PUBLIC_MF_ENABLED=false` 时独立渲染) |
|
||||||
| 防作弊策略未仲裁(ISSUE-014-03/04) | 中 | P3 先实现采集 + 本地记录,P6 根据仲裁结果补全上报逻辑 |
|
| 防作弊策略未仲裁(ISSUE-014-03/04) | 中 | ✅ 已裁决(ARB-019 §21.4);02 §14.5 已补全全屏策略 + visibilitychange + 多标签 + 粘贴题型分层 |
|
||||||
| 附件上传协议未仲裁(ISSUE-014-05) | 中 | P3 先实现文本作业提交,附件上传 P6 根据仲裁结果补全 |
|
| 附件上传协议未仲裁(ISSUE-014-05) | 中 | ✅ 已裁决(ARB-019 §21.5 方案 A);02 §4.2 已更新为 REST upload + GraphQL 提交 URL |
|
||||||
| 实时事件命名未确认(ISSUE-014-06) | 低 | P3 不依赖实时事件(考试作答页基于本地倒计时),P5 根据仲裁结果接入 WebSocket 实时事件 |
|
| 实时事件命名未确认(ISSUE-014-06) | 低 | ✅ 已裁决(ARB-019 §21.6);02 §5.1/§5.2 已补全 ExamExtended + ExamForceSubmitted |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §8 工作进度跟踪
|
||||||
|
|
||||||
|
### §8.1 ARB-019 §21.8 ai14 执行项落实情况(2026-07-11 审计)
|
||||||
|
|
||||||
|
| # | 执行项 | ISSUE | 状态 | 说明 |
|
||||||
|
|---|--------|-------|------|------|
|
||||||
|
| 1 | contract.md §2.3 路径统一为 `POST /api/v1/student/graphql` | 014-01 | ✅ 已落实 | contract.md v2 已包含 |
|
||||||
|
| 2 | 02 §4.2 GraphQL query 移除所有 `studentId` 参数 | 014-07 | ✅ 已落实 | 全部 query 无 studentId 参数 |
|
||||||
|
| 3 | 02 §14 防作弊策略补全(全屏 + visibilitychange + 多标签 + 事件上报) | 014-03 | ✅ 已落实 | §14.5 已补全;mutation 名对齐为 `recordExamViolation` |
|
||||||
|
| 4 | 02 §14 粘贴策略题型分层(客观题禁止/主观题允许记录/作文题允许不记录) | 014-04 | ✅ 已落实 | §14.5 已补全分层策略 + `recordPasteEvent` mutation |
|
||||||
|
| 5 | 02 §5.1 实时事件补 ExamExtended + ExamForceSubmitted 命名 | 014-06 | ✅ 已落实 | §5.1 事件表 + §5.2 handlePushMessage 已补全 |
|
||||||
|
| 6 | 02 §4.2 mutation 域确认 recordExamViolation + recordPasteEvent 参数 | 014-03/04 | ✅ 已落实 | §4.2 已对齐 contract.md,补全 markAllAsRead + updateNotificationPreference |
|
||||||
|
|
||||||
|
### §8.2 额外修复项(2026-07-11)
|
||||||
|
|
||||||
|
| # | 修复项 | 说明 |
|
||||||
|
|---|--------|------|
|
||||||
|
| 1 | `SAVE_EXAM_ANSWER` → `SAVE_EXAM_DRAFT` | 对齐 contract.md §2.4.2 mutation 命名 |
|
||||||
|
| 2 | `RECORD_SUSPICIOUS_BEHAVIOR` → `RECORD_EXAM_VIOLATION` | 对齐 contract.md §2.4.2 mutation 命名 |
|
||||||
|
| 3 | `uploadAttachment` mutation 移除 | 改为 REST 端点(ARB-019 §21.5 方案 A) |
|
||||||
|
| 4 | `SuspiciousBehaviorRecord.type` 更新 | `copy-paste` 拆分为 `copy`/`paste`/`paste-essay` |
|
||||||
|
| 5 | §11.3 未决设计决策表更新 | 4 项 ⚠️ → ✅ 已裁决 |
|
||||||
|
| 6 | §12.3 契约表更新 | WebSocket 事件名已裁决 |
|
||||||
|
| 7 | §11.1 假设 6 更新 | 事件名已由 ARB-019 §21.6 裁决确认 |
|
||||||
|
| 8 | §20.5 技术债务表更新 | WebSocket 事件名已裁决 |
|
||||||
|
|
||||||
|
### §8.3 当前阶段状态
|
||||||
|
|
||||||
|
- **文档阶段**:✅ 完成(01-understanding + 02-architecture-design + contract + workline + issue 全部对齐 ARB-019 裁决)
|
||||||
|
- **代码实现阶段**:✅ 已完成(通过 `606acb6` 合并到 main,包含 P2-P6 全部代码)
|
||||||
|
- **集成测试阶段**:✅ 已完成(通过 `61d8249` + `a6b0c54` 修复全部集成测试 bug)
|
||||||
|
- **待办**:无未完成的架构工作项
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user