Files
lcbp3/backend/src/modules/ai/ai-migration-checkpoint.service.ts
T
admin 1564f8648d
CI / CD Pipeline / build (push) Successful in 4m10s
CI / CD Pipeline / deploy (push) Successful in 3m52s
690524:1919 ADR-028-228-migration #04
2026-05-24 19:19:46 +07:00

159 lines
5.4 KiB
TypeScript

// File: src/modules/ai/ai-migration-checkpoint.service.ts
// Change Log:
// - 2026-05-23: สร้าง service จัดการ Migration Checkpoint, Queue และ Error log ผ่าน API (ADR-023A)
// - 2026-05-24: เพิ่มฟังก์ชันค้นหาและแปลง UUID เป็นตัวเลข ID จริงใน upsertQueueRecord เพื่อป้องกันการเขียนทับด้วย undefined
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import {
MigrationProgress,
MigrationProgressStatus,
} from './entities/migration-progress.entity';
import {
MigrationReviewRecord,
MigrationReviewRecordStatus,
} from './entities/migration-review.entity';
import {
MigrationErrorLogDto,
MigrationQueueRecordDto,
SaveCheckpointDto,
} from './dto/migration-checkpoint.dto';
/** Response DTO สำหรับ Checkpoint */
export interface CheckpointResponse {
batchId: string;
lastProcessedIndex: number;
status: MigrationProgressStatus;
updatedAt: Date | null;
}
@Injectable()
export class AiMigrationCheckpointService {
private readonly logger = new Logger(AiMigrationCheckpointService.name);
constructor(
@InjectRepository(MigrationProgress)
private readonly progressRepo: Repository<MigrationProgress>,
@InjectRepository(MigrationReviewRecord)
private readonly reviewRepo: Repository<MigrationReviewRecord>,
private readonly dataSource: DataSource
) {}
/**
* ดึง Checkpoint ปัจจุบันของ Batch — ถ้ายังไม่มีให้คืนค่า default
*/
async getCheckpoint(batchId: string): Promise<CheckpointResponse> {
const record = await this.progressRepo.findOne({ where: { batchId } });
if (!record) {
return {
batchId,
lastProcessedIndex: 0,
status: MigrationProgressStatus.RUNNING,
updatedAt: null,
};
}
return {
batchId: record.batchId,
lastProcessedIndex: record.lastProcessedIndex,
status: record.status,
updatedAt: record.updatedAt,
};
}
/**
* บันทึกหรืออัพเดต Checkpoint ของ Batch (Upsert)
*/
async saveCheckpoint(dto: SaveCheckpointDto): Promise<CheckpointResponse> {
const existing = await this.progressRepo.findOne({
where: { batchId: dto.batchId },
});
const record =
existing ?? this.progressRepo.create({ batchId: dto.batchId });
record.lastProcessedIndex = dto.lastProcessedIndex;
record.status = dto.status ?? MigrationProgressStatus.RUNNING;
const saved = await this.progressRepo.save(record);
this.logger.log(
`Checkpoint saved — batchId=${dto.batchId} index=${dto.lastProcessedIndex}`
);
return {
batchId: saved.batchId,
lastProcessedIndex: saved.lastProcessedIndex,
status: saved.status,
updatedAt: saved.updatedAt,
};
}
/**
* บันทึกรายการเข้า Review Queue (Upsert โดยใช้ idempotencyKey)
*/
async upsertQueueRecord(
dto: MigrationQueueRecordDto
): Promise<{ publicId: string }> {
const idempotencyKey =
dto.idempotencyKey ?? `${dto.batchId}:${dto.documentNumber}`;
const existing = await this.reviewRepo.findOne({
where: { idempotencyKey },
});
const record = existing ?? this.reviewRepo.create({ idempotencyKey });
record.batchId = dto.batchId;
record.originalFileName = dto.documentNumber;
if (dto.tempAttachmentId) {
if (typeof dto.tempAttachmentId === 'number') {
record.tempAttachmentId = dto.tempAttachmentId;
} else {
const rows = await this.dataSource.manager.query<{ id: number }[]>(
'SELECT id FROM attachments WHERE uuid = ? LIMIT 1',
[dto.tempAttachmentId]
);
if (rows && rows.length > 0) {
record.tempAttachmentId = rows[0].id;
}
}
}
record.confidenceScore = dto.confidence ?? undefined;
record.status =
dto.status === 'PENDING_REVIEW'
? MigrationReviewRecordStatus.PENDING_REVIEW
: MigrationReviewRecordStatus.PENDING;
record.errorReason = dto.reviewReason ?? undefined;
record.extractedMetadata = {
documentNumber: dto.documentNumber,
subject: dto.subject,
originalSubject: dto.originalSubject,
...(dto.aiResult ?? {}),
};
const saved = await this.reviewRepo.save(record);
this.logger.log(
`Queue record upserted — batchId=${dto.batchId} doc=${dto.documentNumber} status=${dto.status}`
);
return { publicId: saved.publicId };
}
/**
* บันทึก Error Log สำหรับเอกสารที่ประมวลผลไม่สำเร็จ
*/
async logError(dto: MigrationErrorLogDto): Promise<{ id: number }> {
const result = await this.dataSource.query<{ insertId: number }[]>(
`INSERT INTO migration_errors (batch_id, document_number, error_type, error_message, created_at)
VALUES (?, ?, ?, ?, NOW())`,
[
dto.batchId,
dto.documentNumber,
dto.errorType ?? 'UNKNOWN',
dto.errorMessage ?? '',
]
);
this.logger.warn(
`Error logged — batchId=${dto.batchId} doc=${dto.documentNumber} type=${dto.errorType}`
);
return { id: result[0]?.insertId ?? 0 };
}
}