feat(server,web,shared,desktop): 上传超限正确报错 + 两个登录入口都限流 + 作业按天深链,文档与验证脚本同步
服务端加固: - express.json 上限 2mb → 12mb:表格导入把文件以 base64 放进 JSON(约 4/3 倍), 8MB 的声明上限原先到 1.5MB 就被拦掉;超限现在由 error 中间件映射成 413 IMPORT_TOO_LARGE(中文提示"请拆分后分批导入"),不再落进 500 兜底分支抛英文原文; - loginRateLimiter 同时挂 /auth/login 与 /auth/class-login(班级密码默认 123456, 班级登录曾是唯一没有暴力破解防护的入口),两个路径共享同一计数器; - CORS_ORIGIN=* 时启动告警(不拦截启动,避免挡住教室机 file:// 的合法部署); - 删除已无用的 DEFAULT_STUDENT_PASSWORD;班级 termWeeks 上限 40 → 30, 与 MAX_TERM_WEEK / weekNumberSchema 对齐(原先对外承诺了系统兑现不了的学期长度)。 Web 端: - 作业页 ?date= 深链在同路由再推 query 时也生效(watch route.query.date); - 表格导入对话框适配 8MB 上限与 413 提示。 冒烟与文档: - 新增 scripts/lib/smoke-class.mjs:验证用临时班级,避免探针通知弹到用户自己的客户端与灵动岛; - README 与 AGENTS.md 同步本轮全部行为变更(含 schedule source、学生账号清理、凭据门禁)。
This commit is contained in:
1 parent
3880eee08e
commit
20fcca0388
28 files changed
+1649
-915
No files matched your search
@@ -105,33 +105,52 @@ function close(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
/** 模板下载:带上 JWT 直接取二进制,避免暴露匿名下载入口 */
|
||||
/**
|
||||
* 模板下载。
|
||||
*
|
||||
* 两种格式在服务端的返回形态**不同**,必须分开处理:
|
||||
* - xlsx:直接 `res.send(Buffer)` 返回二进制,因此这里用 fetch 取流;
|
||||
* - csv:走统一 JSON 响应体(`{ data: { content } }`),必须解析出 content 再自己生成文件。
|
||||
* 之前两种格式共用同一段 `response.blob()`,于是"下载 CSV 模板"得到的是一个
|
||||
* 名为 template-grades.csv、内容却是一整行 JSON 的文件,老师照它填完再导入必然失败。
|
||||
*/
|
||||
async function downloadTemplate(format: 'csv' | 'xlsx'): Promise<void> {
|
||||
try {
|
||||
if (format === 'csv') {
|
||||
const result = await importApi.template(props.kind, 'csv');
|
||||
saveBlob(new Blob([result.content], { type: 'text/csv;charset=utf-8' }), result.fileName);
|
||||
ElMessage.success('模板已开始下载');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem(STORAGE_KEYS.token) ?? '';
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}${API_PATHS.imports}/template?kind=${props.kind}&format=${format}`,
|
||||
`${API_BASE_URL}${API_PATHS.imports}/template?kind=${props.kind}&format=xlsx`,
|
||||
{ headers: token ? { Authorization: `Bearer ${token}` } : {} },
|
||||
);
|
||||
if (!response.ok) {
|
||||
ElMessage.error(`模板下载失败(${response.status})`);
|
||||
return;
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = `template-${props.kind}.${format}`;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
saveBlob(await response.blob(), `template-${props.kind}.xlsx`);
|
||||
ElMessage.success('模板已开始下载');
|
||||
} catch (error) {
|
||||
ElMessage.error(`模板下载失败:${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 触发浏览器保存一个 Blob */
|
||||
function saveBlob(blob: Blob, fileName: string): void {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = fileName;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function pickFile(): void {
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
@@ -180,6 +199,10 @@ async function onFileChange(event: Event): Promise<void> {
|
||||
} catch {
|
||||
preview.value = null;
|
||||
contentBase64.value = '';
|
||||
// 必须把 input 的值也清掉:否则修好表格后重选**同一个文件名**时 @change 不会触发
|
||||
// (input 的 value 没变),界面会停在失败状态、"重新选择文件"按钮点了没反应。
|
||||
if (fileInputRef.value) fileInputRef.value.value = '';
|
||||
fileName.value = '';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user