Files
lcbp3/specs/03-Data-and-Storage/03-04-legacy-data-migration.md
admin 276d06e950
All checks were successful
Build and Deploy / deploy (push) Successful in 2m49s
260228:1412 20260228: setup n8n
2026-02-28 14:12:48 +07:00

19 KiB
Raw Permalink Blame History

03-04: Legacy Data Migration Plan (PDF 20k Docs)

description version
legacy PDF document migration to system v1.8.0 uses n8n and Ollama 1.8.0

Note: Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439), Folder Standard (/share/np-dms/n8n)


1. วัตถุประสงค์ (Objectives)

  • นำเข้าเอกสาร PDF 20,000 ฉบับ พร้อม Metadata จาก Excel (Legacy system export) เข้าสู่ระบบ LCBP3-DMS
  • ใช้ AI (Ollama Local Model) เพื่อตรวจสอบความถูกต้องของลักษณะข้อมูล (Data format, Title consistency) ก่อนการนำเข้า
  • รักษาโครงสร้างความสัมพันธ์ (Project / Contract / Ref No.) และระบบการทำ Revision ตาม Business Rules
  • Checkpoint Support: รองรับการหยุดและเริ่มงานต่อ (Resume) จากจุดที่ค้างอยู่ได้กรณีเกิดเหตุขัดข้อง

Note: เอกสารนี้ขยายความถึงวิธีปฏิบัติ (Implementation) จากการตัดสินใจทางสถาปัตยกรรมใน ADR-017: Ollama Data Migration Architecture


2. โครงสร้างพื้นฐาน (Migration Infrastructure)

  • Migration Orchestrator: n8n (รันจาก Docker Container บน QNAP NAS)
  • AI Validator: Ollama (รันใน Internal Network บน Desktop Desk-5439, RTX 2060 SUPER 8GB)
  • Target Database: MariaDB (correspondences table) บน QNAP NAS
  • Target Storage: QNAP File System — ผ่าน Backend StorageService API เท่านั้น (ห้าม move file โดยตรง)
  • Connection: 2.5G LAN + LACP / Internal VLAN

3. ขั้นตอนการดำเนินงาน (Implementation Steps)

Phase 1: การเตรียม Infrastructure และ Storage (สัปดาห์ที่ 1)

File Migration:

  • ย้ายไฟล์ PDF ทั้งหมดจากแหล่งเก็บไปยัง Folder ชั่วคราวบน NAS (QNAP)
  • Target Path: /share/np-dms/staging_ai/

Mount Folder:

  • Bind Mount /share/np-dms/staging_ai/ เข้ากับ n8n Container แบบ read-only
  • สร้าง /share/np-dms/n8n/migration_logs/ Volume แยกสำหรับเขียน Log แบบ read-write

Ollama Config:

  • ติดตั้ง Ollama บน Desktop (Desk-5439, RTX 2060 SUPER 8GB)
  • No DB credentials, Internal network only
# แนะนำ: llama3.2:3b (เร็ว, VRAM ~3GB, เหมาะ Classification) หรือ ollama run llama3.2:3b
ollama pull llama3.2:3b

# Fallback: mistral:7b-instruct-q4_K_M (แม่นกว่า, VRAM ~5GB)
# ollama pull mistral:7b-instruct-q4_K_M

ทดสอบ Ollama:

curl http://192.168.20.100:11434/api/generate \
  -d '{"model":"llama3.2:3b","prompt":"reply: ok","stream":false}'

Concurrency Configuration:

  • Sequential: Batch Size = 1, Delay ≥ 2 วินาที, ปิด Parallel Execution
  • เพิ่ม Health Check Node ก่อนเริ่ม Batch เพื่อป้องกัน Workflow ค้างหาก Desktop Sleep หรือ Overheat

Phase 2: การเตรียม Target Database และ API (สัปดาห์ที่ 1)

SQL Indexing:

ALTER TABLE correspondences ADD INDEX idx_doc_number (document_number);
ALTER TABLE correspondences ADD INDEX idx_deleted_at (deleted_at);
ALTER TABLE correspondences ADD INDEX idx_created_by (created_by);

Checkpoint Table:

CREATE TABLE IF NOT EXISTS migration_progress (
    batch_id             VARCHAR(50) PRIMARY KEY,
    last_processed_index INT DEFAULT 0,
    status               ENUM('RUNNING','COMPLETED','FAILED') DEFAULT 'RUNNING',
    updated_at           TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Idempotency Table :

CREATE TABLE IF NOT EXISTS import_transactions (
    id               INT AUTO_INCREMENT PRIMARY KEY,
    idempotency_key  VARCHAR(255) UNIQUE NOT NULL,
    document_number  VARCHAR(100),
    batch_id         VARCHAR(100),
    status_code      INT DEFAULT 201,
    created_at       TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_idem_key (idempotency_key)
);

Idempotency Logic: ถ้า idempotency_key ซ้ำ → Backend คืน HTTP 200 ทันที (ไม่สร้าง Revision ซ้ำ) ถ้าไม่ซ้ำ → ประมวลผลปกติ

API Authentication — Migration Token:

INSERT INTO users (username, email, role, is_active)
VALUES ('migration_bot', 'migration@system.internal', 'SYSTEM_ADMIN', true);

Scope ของ Migration Token (Patch — คำนิยามชัดเจน):

สิทธิ์ ปกติ Migration Token หมายเหตุ
Bypass File Virus Scan ไฟล์ผ่าน Scan มาแล้วก่อน Import
Bypass Duplicate Validation Error Revision Logic ยัง enforce ปกติ
Bypass Created-by User validation
Overwrite existing revision ห้ามโดยเด็ดขาด
Delete previous revision ห้ามโดยเด็ดขาด
ลบ / แก้ไข Record อื่น ห้ามโดยเด็ดขาด

⚠️ Patch Clarification: "Bypass Duplicate Number Check" ถูกแทนด้วย "Bypass Duplicate Validation Error" — Revision increment logic ยังทำงานตามปกติทุกกรณี

  • Token Expiry: ไม่เกิน 7 วัน ต้อง Revoke ทันทีหลัง Migration เสร็จ
  • IP Whitelist: ใช้ได้เฉพาะจาก <NAS_IP> เท่านั้น
  • Audit: ทุก Request บันทึก created_by = 'SYSTEM_IMPORT'

Phase 3: การออกแบบ n8n Workflow (The Migration Logic)

Node 0: Pre-flight Health Check + Fetch System Categories

ตรวจสอบทุก dependency ก่อน Batch:

  1. HTTP GET Ollama /api/tags → ต้อง HTTP 200
  2. MariaDB SELECT 1 → ต้องเชื่อมได้
  3. HTTP GET Backend /health → ต้อง HTTP 200
  4. File Mount Check → staging_ai มีไฟล์, migration_logs เขียนได้

Fetch System Categories (Patch — ห้าม hardcode):

GET /api/meta/categories
Authorization: Bearer <MIGRATION_TOKEN>

Response:

{ "categories": ["Correspondence","RFA","Drawing","Transmittal","Report","Other"] }

n8n ต้องเก็บ categories นี้ไว้ใน Workflow Variable (system_categories) และ inject เข้า AI Prompt ทุก Request

Node 1: Data Reader & Checkpoint

  • อ่าน Checkpoint จาก MariaDB Node แยก (ไม่ใช่ async call ใน Code Node)
  • Batch ทีละ 1020 แถว ตาม $env.MIGRATION_BATCH_SIZE
  • ติด original_index ทุก Item

Encoding Normalization:

// Normalize ข้อมูลจาก Excel เป็น UTF-8 NFC ก่อนประมวลผล
const normalize = (str) => {
  if (!str) return '';
  return Buffer.from(str, 'utf8').toString('utf8').normalize('NFC');
};

return items.map(item => ({
  ...item,
  json: {
    ...item.json,
    document_number: normalize(item.json.document_number),
    title: normalize(item.json.title),
    // Mapping เลขอ้างอิงเก่า (Legacy Number) เพื่อนำไปเก็บใน details JSON
    legacy_document_number: item.json.document_number
  }
}));

Node 2: File Validator & Sanitizer

  • ตรวจสอบไฟล์ PDF มีอยู่จริงบน NAS
  • Normalize ชื่อไฟล์เป็น UTF-8 NFC
  • Path Traversal Guard: resolved path ต้องอยู่ใน /share/np-dms/staging_ai เท่านั้น
  • Output 0 → valid → Node 3
  • Output 1 → error → Node 5D (ไม่หายเงียบ)

Node 3: AI Analysis (Sequential เท่านั้น)

System Prompt:

You are a Document Controller for a large construction project.
Your task is to validate document metadata.
You MUST respond ONLY with valid JSON. No explanation, no markdown, no extra text.
If there are no issues, "detected_issues" must be an empty array [].

User Prompt (Category List มาจาก Backend ไม่ hardcode):

Validate this document metadata and respond in JSON:

Document Number: {{$json.document_number}}
Title: {{$json.title}}
Expected Pattern: [ORG]-[TYPE]-[SEQ] e.g. "TCC-COR-0001"
Category List (MUST match system enum exactly): {{$workflow.variables.system_categories}}

Respond ONLY with this exact JSON structure:
{
  "is_valid": true | false,
  "confidence": 0.0 to 1.0,
  "suggested_category": "<one from Category List>",
  "detected_issues": ["<issue1>"],
  "suggested_title": "<corrected title or null>"
}

JSON Validation (ตรวจ Category ตรง Enum):

const systemCategories = $workflow.variables.system_categories;
if (!systemCategories.includes(result.suggested_category)) {
  throw new Error(`Category "${result.suggested_category}" not in system enum: ${systemCategories.join(', ')}`);
}

Node 3.5: Fallback Model Manager

  • อัปเดต migration_fallback_state ทุกครั้งที่เกิด Parse Error
  • Auto-switch ไป OLLAMA_MODEL_FALLBACK เมื่อ Error ≥ FALLBACK_ERROR_THRESHOLD
  • ส่ง Alert Email เมื่อ Fallback ถูก Activate

Node 4: Confidence Router (4 outputs)

เงื่อนไข การดำเนินการ
confidence >= 0.85 และ is_valid = true Output 0 → Auto Ingest
confidence >= 0.60 และ < 0.85 Output 1 → Review Queue
confidence < 0.60 หรือ is_valid = false Output 2 → Reject Log
Parse Error / AI ไม่ตอบ Output 3 → Error Log
Fallback: Error > 5 ใน 10 Request สลับ Model / หยุด Workflow + Alert

Revision Drift Protection:

// ถ้า Excel มี revision column — ตรวจสอบก่อน route
if (item.json.excel_revision !== undefined) {
  const expectedRevision = (item.json.current_db_revision || 0) + 1;
  if (parseInt(item.json.excel_revision) !== expectedRevision) {
    item.json.review_reason = `Revision drift: Excel=${item.json.excel_revision}, Expected=${expectedRevision}`;
    reviewQueue.push(item);
    continue;
  }
}

Node 5A: Auto Ingest — Backend API

⚠️ Storage Enforcement: n8n ส่งแค่ source_file_path — Backend จะ generate UUID, enforce path strategy (/share/np-dms/staging_ai/...), และ move file atomically ผ่าน StorageService

POST /api/correspondences/import
Authorization: Bearer <MIGRATION_TOKEN>
Idempotency-Key: <document_number>:<batch_id>
Content-Type: application/json

Payload:

{
  "document_number":   "{{document_number}}",
  "title":             "{{ai_result.suggested_title || title}}",
  "category":          "{{ai_result.suggested_category}}",
  "source_file_path":  "{{file_path}}",
  "ai_confidence":     "{{ai_result.confidence}}",
  "ai_issues":         "{{ai_result.detected_issues}}",
  "migrated_by":       "SYSTEM_IMPORT",
  "batch_id":          "{{$env.MIGRATION_BATCH_ID}}"
}

Audit Log ที่ Backend ต้องสร้าง:

{
  "action":     "IMPORT",
  "source":     "MIGRATION",
  "batch_id":   "migration_20260226",
  "created_by": "SYSTEM_IMPORT",
  "metadata": {
    "migration":     true,
    "batch_id":      "migration_20260226",
    "ai_confidence": 0.91
  }
}

Checkpoint Update (ทุก 10 Records — ผ่าน IF Node + MariaDB Node):

INSERT INTO migration_progress (batch_id, last_processed_index, status)
VALUES ('{{$env.MIGRATION_BATCH_ID}}', {{checkpoint_index}}, 'RUNNING')
ON DUPLICATE KEY UPDATE
  last_processed_index = {{checkpoint_index}},
  updated_at = NOW();

Node 5B: Review Queue

⚠️ migration_review_queue เป็น Temporary Table เท่านั้น — ห้ามสร้าง Correspondence record จนกว่า Admin จะ Approve

Approval Flow:

Review → Admin Approve → POST /api/correspondences/import (เหมือน Auto Ingest)
Admin Reject → ลบออกจาก queue ไม่สร้าง record

Node 5C: Reject Log → /share/np-dms/n8n/migration_logs/reject_log.csv

Node 5D: Error Log → /share/np-dms/n8n/migration_logs/error_log.csv + MariaDB


Phase 4: แผนการทดสอบ (Testing & QA)

Dry Run Policy (Mandatory):

  • All migrations MUST run with --dry-run
  • No DB commit until validation approved

Dry Run Validation (2050 แถว):

  • JSON Parse Success Rate > 95%
  • Category ที่ AI ตอบตรงกับ System Enum ทุกรายการ
  • รัน Batch เดิมซ้ำ 2 รอบ → ต้องไม่สร้าง Duplicate หรือ Revision ซ้ำ (Idempotency Test)
  • Storage Path ตรงตาม Core Storage Spec v1.8.0
  • Revision Drift ถูก route ไป Review Queue

Integrity Check:

-- ตรวจยอด
SELECT COUNT(*) FROM correspondences WHERE created_by = 'SYSTEM_IMPORT';

-- ตรวจ Revision ซ้ำ
SELECT document_number, COUNT(*) as cnt
FROM correspondences WHERE created_by = 'SYSTEM_IMPORT'
GROUP BY document_number HAVING cnt > 1;

-- ตรวจ Idempotency Key ไม่ซ้ำ
SELECT idempotency_key, COUNT(*) as cnt
FROM import_transactions GROUP BY idempotency_key HAVING cnt > 1;

-- ตรวจ Audit Log ครบ
SELECT COUNT(*) FROM audit_logs
WHERE created_by = 'SYSTEM_IMPORT' AND action = 'IMPORT';

Phase 5: การรันงานจริง (Execution & Monitoring)

  • Scheduling: รันอัตโนมัติ 22:0006:00
  • Expected Runtime: ~3 วินาที/record (2 sec delay + ~1 sec inference) → 20,000 records ≈ 60,000 วินาที (~16.6 ชั่วโมง) → ใช้เวลาประมาณ 34 คืน
  • Daily Check: Admin ตรวจ Review Queue และ Reject Log ทุกเช้าจาก Night Summary Email
  • Progress Tracking: อัปเดต migration_progress ทุก 10 Records

4. Rollback Plan

Step 1: หยุด n8n และ Disable Token

UPDATE users SET is_active = false WHERE username = 'migration_bot';

Step 2: ลบ Records (Transaction)

START TRANSACTION;
DELETE FROM correspondence_files
WHERE correspondence_id IN (SELECT id FROM correspondences WHERE created_by = 'SYSTEM_IMPORT');
DELETE FROM correspondences WHERE created_by = 'SYSTEM_IMPORT';
DELETE FROM import_transactions WHERE batch_id = 'migration_20260226';
SELECT ROW_COUNT();
COMMIT;

Step 3: ย้ายไฟล์กลับ /share/np-dms/staging_ai/ ผ่าน Script แยก

Step 4: Reset State

UPDATE migration_progress
SET status = 'FAILED', last_processed_index = 0
WHERE batch_id = 'migration_20260226';

UPDATE migration_fallback_state
SET recent_error_count = 0, is_fallback_active = FALSE
WHERE batch_id = 'migration_20260226';

5. แผนรับมือความเสี่ยง (Risk Management)

ลำดับที่ ความเสี่ยง การจัดการ (Mitigation)
1 AI Node หรือ GPU ค้าง Timeout 30 วินาที, Retry 3 รอบ, Delay 60 วินาที
2 Ollama ตอบไม่ใช่ JSON JSON Pre-processor + ส่ง Human Review Queue
3 Category ไม่ตรง System Enum Fetch /api/meta/categories ก่อน Batch ทุกครั้ง
4 Idempotency ซ้ำ import_transactions table + Backend คืน HTTP 200
5 Revision Drift ตรวจ Excel revision column → Route ไป Review Queue
6 Storage bypass ห้าม move file โดยตรง — ผ่าน Backend API เท่านั้น
7 GPU VRAM Overflow ใช้เฉพาะ Quantized Model (q4_K_M)
8 ดิสก์ NAS เต็ม ปิด "Save Successful Executions" ใน n8n
9 Migration Token ถูกขโมย Token 7 วัน, IP Whitelist <NAS_IP> เท่านั้น
10 ไฟดับ/ล่มกลางคัน Checkpoint Table → Resume จากจุดที่ค้าง

6. Post-Migration Verification

-- 1. ตรวจยอดครบ 20,000
SELECT COUNT(*) FROM correspondences WHERE created_by = 'SYSTEM_IMPORT';

-- 2. ตรวจ Revision ผิดปกติ
SELECT document_number, COUNT(*) FROM correspondences
WHERE created_by = 'SYSTEM_IMPORT'
GROUP BY document_number HAVING COUNT(*) > 5;

-- 3. ตรวจ Audit Log ครบ
SELECT COUNT(*) FROM audit_logs
WHERE created_by = 'SYSTEM_IMPORT' AND action = 'IMPORT';

-- 4. ตรวจ Idempotency ไม่มีซ้ำ
SELECT idempotency_key, COUNT(*) FROM import_transactions
GROUP BY idempotency_key HAVING COUNT(*) > 1;

ข้อแนะนำด้าน Physical Storage: ไฟล์ PDF ทั้ง 20,000 ไฟล์จะถูก move โดย Backend StorageService ไปยัง path ที่ถูกต้องโดยอัตโนมัติ ไม่ปล่อยค้างไว้ที่ /share/np-dms/staging_ai/