fix(server): keepPlus 排除括号开头、钉钉导入补偿删除收窄并记录日志、rels 大小不可读保守拒绝
All checks were successful
CI / check (pull_request) Successful in 4m40s
Dependency Check / check (pull_request) Successful in 3m17s

This commit is contained in:
2026-08-11 12:50:59 +08:00
parent 257c2065b8
commit 2871407f76
3 changed files with 25 additions and 10 deletions

View File

@@ -107,9 +107,9 @@ export function sanitizeCellText(value: string, keepPlus = false): string {
keepPlus &&
ch === '+' &&
start === 0 &&
// '+' 后不允许公式触发符(= - + @ 制表/换行),'+-86'、'+(1-2)' 这类开头仍会被
// '+' 后不允许公式触发符(= - + @ 制表/换行/括号'+-86'、'+(1-2)' 这类开头仍会被
// Excel 当公式解释,回退到剥离;空格合法(+ 86 138... 也是国际区号写法)
/^\+[^=\-+@\t\r\n][\d\s()-]*\d[\d\s()-]*$/.test(value)
/^\+[^=\-+@\t\r\n(][\d\s()-]*\d[\d\s()-]*$/.test(value)
) {
// 整体形如国际区号(+86 138...、+86 (138) 1234-5678时整串保留
// 含 '.'/'#'/'x' 等分隔符或混入公式触发符('=HYPERLINK(...)')时回退到普通剥离

View File

@@ -91,10 +91,11 @@ async function assertRosterZipSafe(buffer: Buffer): Promise<void> {
const sheetPaths = new Set<string>();
const relsEntry = zip.files['xl/_rels/workbook.xml.rels'];
if (relsEntry && !relsEntry.dir) {
// rels 文件极小(正常 <10KB但声明大小不可信:超阈值直接拒绝,避免先整体解压进内存
// rels 文件极小(正常 <10KB但声明大小不可信且 JSZip 内部结构可能随升级变化:
// 读不到声明大小undefined时保守按超限拒绝避免静默失去防护后整体解压进内存
const declaredSize = (relsEntry as unknown as { _data?: { uncompressedSize?: number } })._data
?.uncompressedSize;
if (typeof declaredSize === 'number' && declaredSize > ROSTER_IMPORT_MAX_RELS_BYTES) {
if (typeof declaredSize !== 'number' || declaredSize > ROSTER_IMPORT_MAX_RELS_BYTES) {
throw new BadRequestException('文件解析失败,请上传有效的 Excel 文件');
}
const relsXml = await relsEntry.async('string');

View File

@@ -1,5 +1,6 @@
import {
Injectable,
Logger,
NotFoundException,
BadRequestException,
ForbiddenException,
@@ -70,6 +71,8 @@ function resolveTeacherSubjects(
@Injectable()
export class ClassesService {
private readonly logger = new Logger(ClassesService.name);
constructor(
@InjectRepository(Class)
private classRepo: Repository<Class>,
@@ -300,12 +303,23 @@ export class ClassesService {
try {
await this.batchImportStudents(savedClassId, users);
} catch (error) {
// 钉钉导入失败会抛错,班级已在事务中提交——补偿删除刚创建的班级(学生/教师成员
// 外键均 CASCADE避免留下孤儿班级且前端重试会重复建班
try {
await this.classRepo.delete(savedClassId);
} catch {
// 补偿删除失败:保留班级,优先抛原始错误
// 数据/校验错误BadRequestException时班级已提交——补偿删除刚创建的班级
// (学生/教师成员外键均 CASCADE避免孤儿班级且前端重试撞 code 唯一索引;
// 第三方临时故障(钉钉网络等)保留班级(与事务化前行为一致),记录日志供排查
if (error instanceof BadRequestException) {
try {
await this.classRepo.delete(savedClassId);
} catch (deleteError) {
this.logger.error(
`补偿删除班级 ${savedClassId} 失败:${(deleteError as Error).message}`,
(deleteError as Error).stack,
);
}
} else {
this.logger.error(
`班级 ${savedClassId} 创建后钉钉导入失败:${(error as Error).message}`,
(error as Error).stack,
);
}
throw error;
}