fix(server): keepPlus 排除括号开头、钉钉导入补偿删除收窄并记录日志、rels 大小不可读保守拒绝
This commit is contained in:
@@ -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(...)')时回退到普通剥离
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user