41 lines
3.8 KiB
TypeScript
41 lines
3.8 KiB
TypeScript
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import ExcelJS from 'exceljs';
|
|
import { createHash, randomUUID } from 'node:crypto';
|
|
import { extname } from 'node:path';
|
|
import { FilesService } from '../files/files.service';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { SmsConfigService } from '../sms-config/sms-config.service';
|
|
import type { AnalyzeImportOptions, CreateImportProfileDto, CreateReportBatchDto, EmbeddedImage, ImportCommitDto, ImportMapping, PagedQuery, ReportBatchInspection, ReportBatchTarget, ReviewImportItemsDto } from './report-materials.contracts';
|
|
import { profileData, validateProfile, loadWorkbook, assertSafeWorkbook, safeSpreadsheetText, readEmbeddedImages, suggestMappings, remapProfileColumns, signatureCoreMapping, drainageCoreMapping, normalizeHeader, normalizeFieldCode, clamp, normalizePage, normalizePageSize, dateRange, cellText, transformValue, mappedCoreValue, dynamicValues, jsonRecord, hasValue, isFileRef, resolveExportValue, applyExportTransform, styleHeader, normalizeImageExtension, imageContentType, safeFileName, normalizeBatchIdempotencyKey, jsonStringArray, jsonSafe } from './report-materials.helpers';
|
|
|
|
|
|
/** R4 report-materials domain service composed behind ReportMaterialsService. */
|
|
export class ReportBatchOperationService {
|
|
constructor(private readonly prisma: PrismaService, private readonly files: FilesService, private readonly smsConfig: SmsConfigService) {}
|
|
|
|
async claimBatchOperation(idempotencyKey: string, fingerprint: string, userId?: string) {
|
|
return this.prisma.$transaction(async (tx) => {
|
|
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${idempotencyKey}))`;
|
|
const existing = await tx.operationLog.findFirst({ where: { action: 'report_material.batch_generation', resource: 'report_material_batch', resourceId: idempotencyKey }, orderBy: { createdAt: 'desc' } });
|
|
if (existing) {
|
|
const detail = jsonRecord(existing.detail);
|
|
if (detail.fingerprint !== fingerprint) throw new ConflictException({ code: 'IDEMPOTENCY_KEY_REUSED', message: '该幂等键已用于不同的报备范围' });
|
|
if (detail.status === 'completed' && detail.result) return { operationId: existing.id, replayed: true as const, result: { ...jsonRecord(detail.result), replayed: true, operationId: existing.id } };
|
|
throw new ConflictException({ code: 'REPORT_BATCH_IN_PROGRESS', message: detail.status === 'failed' ? '上次生成失败,请使用新的操作单重试' : '该报备操作正在处理中,请勿重复提交' });
|
|
}
|
|
const operation = await tx.operationLog.create({ data: { userId, action: 'report_material.batch_generation', resource: 'report_material_batch', resourceId: idempotencyKey, detail: { status: 'processing', fingerprint } } });
|
|
return { operationId: operation.id, replayed: false as const, result: null };
|
|
}, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
|
|
}
|
|
|
|
async completeBatchOperation(operationId: string, batchId: string, result: Record<string, unknown>) {
|
|
await this.prisma.operationLog.update({ where: { id: operationId }, data: { detail: { status: 'completed', batchId, fingerprint: jsonRecord((await this.prisma.operationLog.findUnique({ where: { id: operationId } }))?.detail).fingerprint, result: jsonSafe(result) } as Prisma.InputJsonValue } });
|
|
}
|
|
|
|
async failBatchOperation(operationId: string, message: string, batchId?: string) {
|
|
const operation = await this.prisma.operationLog.findUnique({ where: { id: operationId } });
|
|
await this.prisma.operationLog.update({ where: { id: operationId }, data: { detail: { ...jsonRecord(operation?.detail), status: 'failed', batchId, message } as Prisma.InputJsonValue } });
|
|
}
|
|
}
|