feat(migration): ADR-028 migration architecture refactor
- เพิ่ม POST /api/ai/jobs + GET /api/ai/jobs/:jobId endpoints (FR-001, FR-002) - เพิ่ม BullMQ Worker MigrateDocumentWorker + OCR auto-detect (FR-003, FR-004) - เพิ่ม cleanup-temp-files + expire-pending-reviews workers (FR-005, FR-005a/b) - สร้าง SQL deltas: tags, correspondence_tags, alter migration_review_queue (FR-006, ADR-009) - เพิ่ม MigrationReviewService.commitRecord() + SELECT FOR UPDATE (FR-007, FR-007a) - เพิ่ม CASL permission migration.commit + MigrationReviewController (FR-007) - สร้าง TagsModule + TagsService + TagsController (US3) - สร้าง Migration Review Queue frontend page + ReviewQueueTable (US2) - อัปเดต n8n guide: deterministic Idempotency-Key + token pre-flight (FR-001a, FR-010a/b) - สร้าง spec.md, plan.md, tasks.md, data-model.md, contracts/, quickstart.md - สร้าง ADR-028 document + validation-report.md (PASS 32/32 tasks, 173/173 tests)
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
ValidationException,
|
||||
SystemException,
|
||||
BusinessException,
|
||||
ConflictException,
|
||||
} from '../../common/exceptions';
|
||||
import {
|
||||
MigrationLog,
|
||||
@@ -32,6 +33,9 @@ import { MigrationUpdateDto } from './dto/migration-update.dto';
|
||||
import { MigrationQueryDto } from './dto/migration-query.dto';
|
||||
import { AiValidationService } from './ai-validation.service';
|
||||
import { CreateAiJobDto } from './dto/create-ai-job.dto';
|
||||
import { SubmitAiJobDto } from './dto/submit-ai-job.dto';
|
||||
import { ImportTransaction } from '../migration/entities/import-transaction.entity';
|
||||
import { Project } from '../project/entities/project.entity';
|
||||
import {
|
||||
QUEUE_AI_BATCH,
|
||||
QUEUE_AI_REALTIME,
|
||||
@@ -159,6 +163,8 @@ export class AiService {
|
||||
private readonly aiAuditLogRepo: Repository<AiAuditLog>,
|
||||
@InjectRepository(AuditLog)
|
||||
private readonly auditLogRepo: Repository<AuditLog>,
|
||||
@InjectRepository(ImportTransaction)
|
||||
private readonly importTransactionRepo: Repository<ImportTransaction>,
|
||||
@Optional()
|
||||
@InjectQueue(QUEUE_AI_REALTIME)
|
||||
private readonly aiRealtimeQueue?: Queue<AiRealtimeJobData>,
|
||||
@@ -254,6 +260,71 @@ export class AiService {
|
||||
}
|
||||
}
|
||||
|
||||
/** ส่งคำขอเปิดงานประมวลผลการย้ายเอกสารของ AI (migrate-document) เข้า BullMQ */
|
||||
async submitMigrationJob(
|
||||
dto: SubmitAiJobDto,
|
||||
idempotencyKey: string
|
||||
): Promise<AiQueueResult> {
|
||||
if (!this.aiBatchQueue) {
|
||||
const error = new Error('AI batch queue is not registered');
|
||||
this.logger.error('AI job queue failed', {
|
||||
documentPublicId: dto.payload.tempAttachmentId,
|
||||
error,
|
||||
});
|
||||
return { success: false, error };
|
||||
}
|
||||
const existingTx = await this.importTransactionRepo.findOne({
|
||||
where: {
|
||||
documentNumber: dto.payload.documentNumber,
|
||||
batchId: dto.payload.batchId,
|
||||
},
|
||||
});
|
||||
if (existingTx && existingTx.statusCode !== 500) {
|
||||
throw new ConflictException(
|
||||
'MIGRATION_DUPLICATE_TRANSACTION',
|
||||
`Document ${dto.payload.documentNumber} already imported in batch ${dto.payload.batchId}`,
|
||||
'เอกสารนี้ได้รับการนำเข้าในระบบ Staging/Production แล้ว'
|
||||
);
|
||||
}
|
||||
const activeJob = await this.aiBatchQueue.getJob(idempotencyKey);
|
||||
if (activeJob) {
|
||||
return { success: true, jobId: String(activeJob.id) };
|
||||
}
|
||||
const defaultProject = await this.importTransactionRepo.manager.findOne(
|
||||
Project,
|
||||
{ where: {} }
|
||||
);
|
||||
const projectPublicId =
|
||||
defaultProject?.publicId ?? '00000000-0000-0000-0000-000000000000';
|
||||
try {
|
||||
const job = await this.aiBatchQueue.add(
|
||||
'migrate-document',
|
||||
{
|
||||
jobType: 'migrate-document',
|
||||
documentPublicId: dto.payload.tempAttachmentId,
|
||||
projectPublicId,
|
||||
payload: {
|
||||
documentNumber: dto.payload.documentNumber,
|
||||
title: dto.payload.title,
|
||||
batchId: dto.payload.batchId,
|
||||
existingTags: dto.payload.existingTags,
|
||||
systemCategories: dto.payload.systemCategories,
|
||||
},
|
||||
idempotencyKey,
|
||||
},
|
||||
{ jobId: idempotencyKey }
|
||||
);
|
||||
return { success: true, jobId: String(job.id) };
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
this.logger.error('AI job queue failed', {
|
||||
documentPublicId: dto.payload.tempAttachmentId,
|
||||
error,
|
||||
});
|
||||
return { success: false, error };
|
||||
}
|
||||
}
|
||||
|
||||
/** อ่านสถานะ job จาก ai-realtime หรือ ai-batch เพื่อให้ frontend polling ได้ */
|
||||
async getAiJobStatus(jobId: string): Promise<AiJobStatusResult> {
|
||||
const realtimeJob = await this.aiRealtimeQueue?.getJob(jobId);
|
||||
|
||||
Reference in New Issue
Block a user