690523:2327 ADR-028-228-migration #01
CI / CD Pipeline / build (push) Successful in 4m38s
CI / CD Pipeline / deploy (push) Successful in 3m6s

This commit is contained in:
2026-05-23 23:27:52 +07:00
parent ff5cadc9f2
commit 5a17f969b8
23 changed files with 1169 additions and 252 deletions
@@ -0,0 +1,145 @@
// File: src/modules/ai/ai-migration-checkpoint.service.ts
// Change Log:
// - 2026-05-23: สร้าง service จัดการ Migration Checkpoint, Queue และ Error log ผ่าน API (ADR-023A)
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;
record.tempAttachmentId = dto.tempAttachmentId ?? undefined;
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 };
}
}
+51
View File
@@ -7,6 +7,7 @@
// - 2026-05-21: เพิ่ม GET /ai/admin/health สำหรับดึงสถานะสุขภาพ AI Infrastructure (T028).
// - 2026-05-21: เพิ่ม POST /ai/admin/sandbox/extract endpoint สำหรับ Superadmin OCR sandbox (T041 & T042)
// - 2026-05-21: แก้ไขข้อห้ามใช้ parseInt โดยการใช้ Number แทนตามกฎ Tier 1
// - 2026-05-23: เพิ่ม Migration Checkpoint API endpoints แทน MySQL direct access (ADR-023A)
// Controller สำหรับ AI Gateway Endpoints (ADR-023)
import {
@@ -79,6 +80,12 @@ import { AiEnabledGuard } from './guards/ai-enabled.guard';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
import { FileStorageService } from '../../common/file-storage/file-storage.service';
import { AiMigrationCheckpointService } from './ai-migration-checkpoint.service';
import {
MigrationErrorLogDto,
MigrationQueueRecordDto,
SaveCheckpointDto,
} from './dto/migration-checkpoint.dto';
@ApiTags('AI Gateway')
@Controller('ai')
@@ -91,6 +98,7 @@ export class AiController {
private readonly aiSettingsService: AiSettingsService,
private readonly aiToolRegistryService: AiToolRegistryService,
private readonly fileStorageService: FileStorageService,
private readonly migrationCheckpointService: AiMigrationCheckpointService,
@InjectRedis() private readonly redis: Redis
) {}
@@ -682,4 +690,47 @@ export class AiController {
user.user_id
);
}
// ─── Migration Checkpoint API (ADR-023A) ──────────────────────────────────
@Get('migration/checkpoint/:batchId')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Migration: ดึง Checkpoint ของ Batch (ADR-023A)' })
@ApiParam({
name: 'batchId',
description: 'Batch ID ที่ต้องการดึง Checkpoint',
})
async getMigrationCheckpoint(@Param('batchId') batchId: string) {
return this.migrationCheckpointService.getCheckpoint(batchId);
}
@Post('migration/checkpoint')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Migration: บันทึก/อัพเดต Checkpoint (ADR-023A)' })
async saveMigrationCheckpoint(@Body() dto: SaveCheckpointDto) {
return this.migrationCheckpointService.saveCheckpoint(dto);
}
@Post('migration/queue/record')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Migration: บันทึกรายการเข้า Review Queue (ADR-023A)',
})
async upsertMigrationQueueRecord(@Body() dto: MigrationQueueRecordDto) {
return this.migrationCheckpointService.upsertQueueRecord(dto);
}
@Post('migration/errors')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Migration: บันทึก Error Log (ADR-023A)' })
async logMigrationError(@Body() dto: MigrationErrorLogDto) {
return this.migrationCheckpointService.logError(dto);
}
}
+6
View File
@@ -6,6 +6,7 @@
// - 2026-05-19: เพิ่ม AiToolModule (ADR-025 AI Tool Layer).
// - 2026-05-21: ลงทะเบียน SystemSetting, AiSettingsService และ AiEnabledGuard สำหรับ ADR-027.
// - 2026-05-22: นำเข้าและลงทะเบียน CleanupTempFilesWorker (T016) เพื่อลบไฟล์แนบชั่วคราวหมดอายุ
// - 2026-05-23: ลงทะเบียน MigrationProgress + AiMigrationCheckpointService (ADR-023A)
// Module สำหรับ AI Gateway — ลงทะเบียน Services และ Controllers (ADR-023)
import { Logger, Module, OnModuleInit } from '@nestjs/common';
@@ -33,7 +34,9 @@ import { EmbeddingService } from './services/embedding.service';
import { MigrationLog } from './entities/migration-log.entity';
import { AiAuditLog } from './entities/ai-audit-log.entity';
import { MigrationReviewRecord } from './entities/migration-review.entity';
import { MigrationProgress } from './entities/migration-progress.entity';
import { SystemSetting } from './entities/system-setting.entity';
import { AiMigrationCheckpointService } from './ai-migration-checkpoint.service';
import { AiEnabledGuard } from './guards/ai-enabled.guard';
import { UserModule } from '../user/user.module';
import { MigrationModule } from '../migration/migration.module';
@@ -67,6 +70,7 @@ import {
AiAuditLog,
AuditLog,
MigrationReviewRecord,
MigrationProgress,
SystemSetting,
Attachment,
Project,
@@ -129,6 +133,7 @@ import {
AiService,
AiSettingsService,
AiIngestService,
AiMigrationCheckpointService,
AiQueueService,
AiQdrantService,
AiValidationService,
@@ -151,6 +156,7 @@ import {
AiService,
AiSettingsService,
AiIngestService,
AiMigrationCheckpointService,
AiQueueService,
AiQdrantService,
AiValidationService,
@@ -0,0 +1,97 @@
// File: src/modules/ai/dto/migration-checkpoint.dto.ts
// Change Log:
// - 2026-05-23: สร้าง DTOs สำหรับ Migration Checkpoint API endpoints (ADR-023A)
import {
IsEnum,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator';
import { MigrationProgressStatus } from '../entities/migration-progress.entity';
/** DTO สำหรับบันทึก/อัพเดต Checkpoint */
export class SaveCheckpointDto {
@IsString()
@IsNotEmpty()
batchId!: string;
@IsNumber()
@Min(0)
lastProcessedIndex!: number;
@IsEnum(MigrationProgressStatus)
@IsOptional()
status?: MigrationProgressStatus;
}
/** DTO สำหรับบันทึกรายการเข้า Review Queue */
export class MigrationQueueRecordDto {
@IsString()
@IsNotEmpty()
batchId!: string;
@IsString()
@IsNotEmpty()
documentNumber!: string;
@IsString()
@IsOptional()
subject?: string;
@IsString()
@IsOptional()
originalSubject?: string;
@IsNumber()
@IsOptional()
tempAttachmentId?: number;
@IsNumber()
@Min(0)
@Max(1)
@IsOptional()
confidence?: number;
@IsString()
@IsOptional()
reviewReason?: string;
@IsEnum(['PENDING', 'PENDING_REVIEW'])
status!: 'PENDING' | 'PENDING_REVIEW';
@IsObject()
@IsOptional()
aiResult?: Record<string, unknown>;
@IsString()
@IsOptional()
idempotencyKey?: string;
}
/** DTO สำหรับบันทึก Error Log */
export class MigrationErrorLogDto {
@IsString()
@IsNotEmpty()
batchId!: string;
@IsString()
@IsNotEmpty()
documentNumber!: string;
@IsString()
@IsOptional()
errorType?: string;
@IsString()
@IsOptional()
errorMessage?: string;
@IsString()
@IsOptional()
jobId?: string;
}
@@ -0,0 +1,32 @@
// File: src/modules/ai/entities/migration-progress.entity.ts
// Change Log:
// - 2026-05-23: สร้าง entity สำหรับ migration_progress table (Checkpoint management ADR-023A)
import { Column, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';
export enum MigrationProgressStatus {
RUNNING = 'RUNNING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
}
/** ตารางติดตามความคืบหน้า Batch Migration — ใช้สำหรับ Resume กรณี n8n หยุดกลางทาง */
@Entity('migration_progress')
export class MigrationProgress {
@PrimaryColumn({ name: 'batch_id', type: 'varchar', length: 50 })
batchId!: string;
@Column({ name: 'last_processed_index', type: 'int', default: 0 })
lastProcessedIndex!: number;
@Column({
name: 'STATUS',
type: 'enum',
enum: MigrationProgressStatus,
default: MigrationProgressStatus.RUNNING,
})
status!: MigrationProgressStatus;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
}
@@ -15,6 +15,7 @@ import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
export enum MigrationReviewRecordStatus {
PENDING = 'PENDING',
PENDING_REVIEW = 'PENDING_REVIEW',
IMPORTED = 'IMPORTED',
REJECTED = 'REJECTED',
}