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
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
||||
import {
|
||||
CALL_QUICK_PHRASES,
|
||||
@@ -21,6 +22,7 @@ import { useRealtimeStore } from '@/stores/realtime';
|
||||
import UrgentClassWarning from '@/components/UrgentClassWarning.vue';
|
||||
|
||||
const realtime = useRealtimeStore();
|
||||
const route = useRoute();
|
||||
|
||||
const loading = ref(false);
|
||||
const classes = ref<ClassDto[]>([]);
|
||||
@@ -290,8 +292,10 @@ async function doPublish(classIds: string[], confirmDuringClass: boolean): Promi
|
||||
reportPublishResult(successCount, reported);
|
||||
|
||||
if (successCount > 0) {
|
||||
// 有班级发布成功:关掉弹窗并刷新列表(仍需二次确认时保留弹窗内的数据)
|
||||
formVisible.value = !pendingConfirm;
|
||||
// 有班级发布成功:关掉弹窗并刷新列表;只有"还有班级等着二次确认"时才保留弹窗内的数据。
|
||||
// 注意这里是 pendingConfirm 而不是 !pendingConfirm —— 写成取反会让弹窗在**发布成功后仍然开着**,
|
||||
// 老师以为没发出去再点一次「立即发布」,同一批班级就重复收到通知(还会重复广播与推 ClassIsland)。
|
||||
formVisible.value = pendingConfirm;
|
||||
await loadNotifications();
|
||||
}
|
||||
}
|
||||
@@ -343,10 +347,29 @@ function onNotificationEvent(): void {
|
||||
void loadNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从仪表盘点「最新通知」跳过来时带的 `?highlight=<通知 id>`:
|
||||
* 高亮那一行并滚动过去。之前这个参数没有任何人消费,
|
||||
* 用户点完只是跳到通知列表,看不出去的是哪一条。
|
||||
*/
|
||||
const highlightedId = computed(() => {
|
||||
const value = route.query.highlight;
|
||||
return typeof value === 'string' ? value : '';
|
||||
});
|
||||
|
||||
function rowClassName({ row }: { row: NotificationDto }): string {
|
||||
return row.id === highlightedId.value ? 'row-highlight' : '';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadClasses();
|
||||
await loadNotifications();
|
||||
realtime.on(SOCKET_EVENTS.notificationNew, onNotificationEvent);
|
||||
// 列表渲染完再滚动,否则表格行还没生成
|
||||
if (highlightedId.value) {
|
||||
await nextTick();
|
||||
document.querySelector('.row-highlight')?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -399,7 +422,12 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="table-card">
|
||||
<el-table v-loading="loading" :data="notifications" empty-text="暂无通知">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="notifications"
|
||||
empty-text="暂无通知"
|
||||
:row-class-name="rowClassName"
|
||||
>
|
||||
<el-table-column label="优先级" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
@@ -560,3 +588,10 @@ onUnmounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 从仪表盘点「最新通知」跳转过来的定位高亮(?highlight=<通知 id>) */
|
||||
:deep(.row-highlight) td {
|
||||
background: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user