From b79a3ff68bae3a690bf8428d44f698a9bd93d56a Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 4 Mar 2026 17:16:42 +0700 Subject: [PATCH] 260304:1716 20260304:1700 update n8n --- .agent/rules/00-project-specs.md | 4 +- .gemini/GEMINI.md | 2 +- AGENTS.md | 2 +- CLAUDE.md | 2 +- specs/03-Data-and-Storage/0.md | 975 ++++++++++++ .../03-05-n8n-migration-setup-guide.md | 1121 ++++---------- .../lcbp3-v1.8.0-migration.sql | 109 ++ .../lcbp3-v1.8.0-schema-01-drop.sql | 215 +++ .../lcbp3-v1.8.0-schema-02-tables.sql | 1344 +++++++++++++++++ .../lcbp3-v1.8.0-schema-03-views-indexes.sql | 516 +++++++ specs/03-Data-and-Storage/n8n.workflow | 562 ++++++- .../lcbp3-v1.8.0-schema.sql | 118 -- 12 files changed, 3956 insertions(+), 1014 deletions(-) create mode 100644 specs/03-Data-and-Storage/0.md create mode 100644 specs/03-Data-and-Storage/lcbp3-v1.8.0-migration.sql create mode 100644 specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-01-drop.sql create mode 100644 specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql create mode 100644 specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-03-views-indexes.sql rename specs/{03-Data-and-Storage => 99-archives}/lcbp3-v1.8.0-schema.sql (96%) diff --git a/.agent/rules/00-project-specs.md b/.agent/rules/00-project-specs.md index 92e4080..e2569bb 100644 --- a/.agent/rules/00-project-specs.md +++ b/.agent/rules/00-project-specs.md @@ -30,7 +30,7 @@ Before generating code or planning a solution, you MUST conceptually load the co 4. **💾 DATABASE & SCHEMA (`specs/03-Data-and-Storage/`)** - _Action:_ - - **Read `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql`** for exact table structures and constraints. + - **Read `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`** for exact table structures and constraints. (Schema split: `01-drop`, `02-tables`, `03-views-indexes`) - **Consult `specs/03-Data-and-Storage/03-01-data-dictionary.md`** for field meanings and business rules. - **Check `specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-basic.sql`** to understand initial data states. - **Check `specs/03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql`** to understand initial permissions states. @@ -70,7 +70,7 @@ When proposing a change or writing code, you must explicitly reference the sourc ### 4. Schema Changes - **DO NOT** create or run TypeORM migration files. -- Modify the schema directly in `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql`. +- Modify the schema directly in `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` (or `01-drop`/`03-views-indexes` as appropriate). - Update `specs/03-Data-and-Storage/03-01-data-dictionary.md` if adding/changing columns. - Notify the user so they can apply the SQL change to the live database manually. - **AI Isolation (ADR-018):** Ollama runs on ASUSTOR only. AI has NO direct DB access, NO write access to uploads. All writes go through DMS API. diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index 7ec8724..b4ff5a0 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -44,7 +44,7 @@ You value **Data Integrity**, **Security**, and **Clean Architecture**. ## 📋 Workflow & Spec Guidelines - Always follow specs in `specs/` (v1.8.0). Priority: `06-Decision-Records` > `05-Engineering-Guidelines` > others. -- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql`** before writing queries. +- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`** before writing queries. (Schema split: `01-drop`, `02-tables`, `03-views-indexes`) - Check data dictionary at **`specs/03-Data-and-Storage/03-01-data-dictionary.md`** for field meanings and business rules. - Check seed data: **`lcbp3-v1.8.0-seed-basic.sql`** (reference data), **`lcbp3-v1.8.0-seed-permissions.sql`** (CASL permissions). - For migration context: **`specs/03-Data-and-Storage/03-04-legacy-data-migration.md`** and **`03-05-n8n-migration-setup-guide.md`**. diff --git a/AGENTS.md b/AGENTS.md index da5e8b6..38298f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,7 @@ You value **Data Integrity**, **Security**, and **Clean Architecture**. ## 📋 Spec Guidelines - Always follow specs in `specs/` (v1.8.0). Priority: `06-Decision-Records` > `05-Engineering-Guidelines` > others. -- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql`** before writing queries. +- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`** before writing queries. (Schema split: `01-drop`, `02-tables`, `03-views-indexes`) - Check data dictionary at **`specs/03-Data-and-Storage/03-01-data-dictionary.md`** for field meanings and business rules. ### ADR Reference (All 17 + Patch) diff --git a/CLAUDE.md b/CLAUDE.md index 7b11cf5..94b09c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ You value **Data Integrity**, **Security**, and **Clean Architecture**. ## 📋 Workflow & Spec Guidelines - Always follow specs in `specs/` (v1.8.0). Priority: `06-Decision-Records` > `05-Engineering-Guidelines` > others. -- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql`** before writing queries. +- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`** before writing queries. (Schema split: `01-drop`, `02-tables`, `03-views-indexes`) - Check data dictionary at **`specs/03-Data-and-Storage/03-01-data-dictionary.md`** for field meanings and business rules. - Check seed data: **`lcbp3-v1.8.0-seed-basic.sql`** (reference data), **`lcbp3-v1.8.0-seed-permissions.sql`** (CASL permissions). - For migration context: **`specs/03-Data-and-Storage/03-04-legacy-data-migration.md`** and **`03-05-n8n-migration-setup-guide.md`**. diff --git a/specs/03-Data-and-Storage/0.md b/specs/03-Data-and-Storage/0.md new file mode 100644 index 0000000..9025b92 --- /dev/null +++ b/specs/03-Data-and-Storage/0.md @@ -0,0 +1,975 @@ + ผมจะสร้างไฟล์ JSON สำหรับ n8n Free Plan (ไม่ใช้ Environment Variables) และปรับปรุงเอกสารคู่มือให้สอดคล้อง โดยใช้ **Static Parameters** และ **Set Node** แทนการอ่านจาก Environment + +--- + +## 📝 ไฟล์ `n8n.workflow` (Free Plan Compatible) + +```json +{ + "name": "LCBP3 Migration Workflow v1.8.0", + "meta": { + "instanceId": "lcbp3-migration-free" + }, + "settings": { + "executionOrder": "v1" + }, + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 22 * * *" + } + ] + } + }, + "id": "trigger-schedule", + "name": "Schedule Trigger (22:00)", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "notes": "เริ่มรันอัตโนมัติทุกวันเวลา 22:00" + }, + { + "parameters": { + "jsCode": "// ============================================\n// CONFIGURATION - แก้ไขค่าที่นี่\n// ============================================\nconst CONFIG = {\n // Ollama Settings\n OLLAMA_HOST: 'http://192.168.20.100:11434',\n OLLAMA_MODEL_PRIMARY: 'llama3.2:3b',\n OLLAMA_MODEL_FALLBACK: 'mistral:7b-instruct-q4_K_M',\n \n // Backend Settings\n BACKEND_URL: 'https://api.np-dms.work',\n MIGRATION_TOKEN: 'Bearer YOUR_MIGRATION_TOKEN_HERE',\n \n // Batch Settings\n BATCH_SIZE: 10,\n BATCH_ID: 'migration_20260226',\n DELAY_MS: 2000,\n \n // Thresholds\n CONFIDENCE_HIGH: 0.85,\n CONFIDENCE_LOW: 0.60,\n MAX_RETRY: 3,\n FALLBACK_THRESHOLD: 5,\n \n // Paths\n STAGING_PATH: '/share/np-dms/staging_ai',\n LOG_PATH: '/share/np-dms/n8n/migration_logs',\n \n // Database\n DB_HOST: '192.168.1.100',\n DB_PORT: 3306,\n DB_NAME: 'lcbp3_production',\n DB_USER: 'migration_bot',\n DB_PASSWORD: 'YOUR_DB_PASSWORD_HERE'\n};\n\n// Store in global workflow data\n$workflow.staticData = $workflow.staticData || {};\n$workflow.staticData.config = CONFIG;\n\nreturn [{ json: { config_loaded: true, timestamp: new Date().toISOString(), config: CONFIG } }];" + }, + "id": "config-setter", + "name": "Set Configuration", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [200, 0], + "notes": "กำหนดค่า Configuration ทั้งหมด - แก้ไขที่นี่ก่อนรัน" + }, + { + "parameters": { + "method": "GET", + "url": "={{$workflow.staticData.config.BACKEND_URL}}/api/meta/categories", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "={{$workflow.staticData.config.MIGRATION_TOKEN}}" + } + ] + }, + "options": { + "timeout": 10000 + } + }, + "id": "preflight-categories", + "name": "Fetch Categories", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [400, 0], + "notes": "ดึง Categories จาก Backend" + }, + { + "parameters": { + "method": "GET", + "url": "={{$workflow.staticData.config.BACKEND_URL}}/api/health", + "options": { + "timeout": 5000 + } + }, + "id": "preflight-health", + "name": "Check Backend Health", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [400, 200], + "notes": "ตรวจสอบ Backend พร้อมใช้งาน", + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst config = $workflow.staticData.config;\n\n// Check file mount\ntry {\n const files = fs.readdirSync(config.STAGING_PATH);\n if (files.length === 0) throw new Error('staging_ai is empty');\n \n // Check write permission to log path\n fs.writeFileSync(`${config.LOG_PATH}/.preflight_ok`, new Date().toISOString());\n \n // Store categories\n const categories = $input.first().json.categories || \n ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\n $workflow.staticData.systemCategories = categories;\n \n return [{ json: { \n preflight_ok: true, \n file_count: files.length,\n system_categories: categories,\n timestamp: new Date().toISOString()\n }}];\n} catch (err) {\n throw new Error(`Pre-flight check failed: ${err.message}`);\n}" + }, + "id": "preflight-check", + "name": "File Mount Check", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [600, 0], + "notes": "ตรวจสอบ File System และเก็บ Categories" + }, + { + "parameters": { + "operation": "executeQuery", +n "host": "={{$workflow.staticData.config.DB_HOST}}", + "port": "={{$workflow.staticData.config.DB_PORT}}", + "database": "={{$workflow.staticData.config.DB_NAME}}", + "user": "={{$workflow.staticData.config.DB_USER}}", + "password": "={{$workflow.staticData.config.DB_PASSWORD}}", + "query": "SELECT last_processed_index, status FROM migration_progress WHERE batch_id = '{{$workflow.staticData.config.BATCH_ID}}' LIMIT 1", + "options": {} + }, + "id": "checkpoint-read", + "name": "Read Checkpoint", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [800, 0], + "notes": "อ่านตำแหน่งล่าสุดที่ประมวลผล", + "onError": "continueErrorOutput" + }, + { + "parameters": { + "operation": "toData", + "binaryProperty": "data", + "options": { + "sheetName": "Sheet1" + } + }, + "id": "excel-reader", + "name": "Read Excel", + "type": "n8n-nodes-base.spreadsheetFile", + "typeVersion": 2, + "position": [800, 200], + "notes": "อ่านไฟล์ Excel รายการเอกสาร" + }, + { + "parameters": { + "jsCode": "const checkpoint = $input.first().json[0] || { last_processed_index: 0, status: 'NEW' };\nconst startIndex = checkpoint.last_processed_index || 0;\nconst config = $workflow.staticData.config;\n\nconst allItems = $('Read Excel').all()[0].json.data || [];\nconst remaining = allItems.slice(startIndex);\nconst currentBatch = remaining.slice(0, config.BATCH_SIZE);\n\n// Encoding Normalization\nconst normalize = (str) => {\n if (!str) return '';\n return String(str).normalize('NFC').trim();\n};\n\nreturn currentBatch.map((item, i) => ({\n json: {\n document_number: normalize(item.document_number || item['Document Number']),\n title: normalize(item.title || item.Title || item['Subject']),\n legacy_number: normalize(item.legacy_number || item['Legacy Number']),\n excel_revision: item.revision || item.Revision || 1,\n original_index: startIndex + i,\n batch_id: config.BATCH_ID,\n file_name: `${normalize(item.document_number)}.pdf`\n }\n}));" + }, + "id": "batch-processor", + "name": "Process Batch + Encoding", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1000, 0], + "notes": "ตัด Batch + Normalize UTF-8" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst path = require('path');\nconst config = $workflow.staticData.config;\n\nconst items = $input.all();\nconst validated = [];\nconst errors = [];\n\nfor (const item of items) {\n const docNum = item.json.document_number;\n \n // Sanitize filename\n const safeName = path.basename(String(docNum).replace(/[^a-zA-Z0-9\\-_.]/g, '_')).normalize('NFC');\n const filePath = path.resolve(config.STAGING_PATH, `${safeName}.pdf`);\n \n // Path traversal check\n if (!filePath.startsWith(config.STAGING_PATH)) {\n errors.push({\n ...item,\n json: { ...item.json, error: 'Path traversal detected', error_type: 'SECURITY', file_exists: false }\n });\n continue;\n }\n \n try {\n if (fs.existsSync(filePath)) {\n const stats = fs.statSync(filePath);\n validated.push({\n ...item,\n json: { ...item.json, file_exists: true, file_size: stats.size, file_path: filePath }\n });\n } else {\n errors.push({\n ...item,\n json: { ...item.json, error: `File not found: ${safeName}.pdf`, error_type: 'FILE_NOT_FOUND', file_exists: false }\n });\n }\n } catch (err) {\n errors.push({\n ...item,\n json: { ...item.json, error: err.message, error_type: 'FILE_ERROR', file_exists: false }\n });\n }\n}\n\n// Output 0: Validated, Output 1: Errors\nreturn [validated, errors];" + }, + "id": "file-validator", + "name": "File Validator", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1200, 0], + "notes": "ตรวจสอบไฟล์ PDF มีอยู่จริง + Sanitize path" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$workflow.staticData.config.DB_HOST}}", + "port": "={{$workflow.staticData.config.DB_PORT}}", + "database": "={{$workflow.staticData.config.DB_NAME}}", + "user": "={{$workflow.staticData.config.DB_USER}}", + "password": "={{$workflow.staticData.config.DB_PASSWORD}}", + "query": "SELECT is_fallback_active, recent_error_count FROM migration_fallback_state WHERE batch_id = '{{$workflow.staticData.config.BATCH_ID}}' LIMIT 1", + "options": {} + }, + "id": "fallback-check", + "name": "Check Fallback State", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [1400, -200], + "notes": "ตรวจสอบว่าต้องใช้ Fallback Model หรือไม่", + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "const config = $workflow.staticData.config;\nconst fallbackState = $input.first().json[0] || { is_fallback_active: false, recent_error_count: 0 };\n\nconst isFallback = fallbackState.is_fallback_active || false;\nconst model = isFallback ? config.OLLAMA_MODEL_FALLBACK : config.OLLAMA_MODEL_PRIMARY;\n\nconst systemCategories = $workflow.staticData.systemCategories || \n ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\n\nconst items = $('File Validator').all();\n\nreturn items.map(item => {\n const systemPrompt = `You are a Document Controller for a large construction project.\nYour task is to validate document metadata.\nYou MUST respond ONLY with valid JSON. No explanation, no markdown, no extra text.\nIf there are no issues, \"detected_issues\" must be an empty array [].`;\n\n const userPrompt = `Validate this document metadata and respond in JSON:\n\nDocument Number: ${item.json.document_number}\nTitle: ${item.json.title}\nExpected Pattern: [ORG]-[TYPE]-[SEQ] e.g. \"TCC-COR-0001\"\nCategory List (MUST match system enum exactly): ${JSON.stringify(systemCategories)}\n\nRespond ONLY with this exact JSON structure:\n{\n \"is_valid\": true | false,\n \"confidence\": 0.0 to 1.0,\n \"suggested_category\": \"\",\n \"detected_issues\": [\"\"],\n \"suggested_title\": \"\"\n}`;\n\n return {\n json: {\n ...item.json,\n active_model: model,\n is_fallback: isFallback,\n system_categories: systemCategories,\n ollama_payload: {\n model: model,\n prompt: `${systemPrompt}\\n\\n${userPrompt}`,\n stream: false,\n format: 'json'\n }\n }\n };\n});" + }, + "id": "prompt-builder", + "name": "Build AI Prompt", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1400, 0], + "notes": "สร้าง Prompt โดยใช้ Categories จาก System" + }, + { + "parameters": { + "method": "POST", + "url": "={{$workflow.staticData.config.OLLAMA_HOST}}/api/generate", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ $json.ollama_payload }}", + "options": { + "timeout": 30000 + } + }, + "id": "ollama-call", + "name": "Ollama AI Analysis", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [1600, 0], + "notes": "เรียก Ollama วิเคราะห์เอกสาร" + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nconst parsed = [];\nconst parseErrors = [];\n\nfor (const item of items) {\n try {\n let raw = item.json.response || '';\n \n // Clean markdown\n raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n const result = JSON.parse(raw);\n \n // Schema Validation\n if (typeof result.is_valid !== 'boolean') throw new Error('is_valid must be boolean');\n if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) {\n throw new Error('confidence must be float 0.0-1.0');\n }\n if (!Array.isArray(result.detected_issues)) throw new Error('detected_issues must be array');\n \n // Enum Validation\n const systemCategories = item.json.system_categories || [];\n if (!systemCategories.includes(result.suggested_category)) {\n throw new Error(`Category \"${result.suggested_category}\" not in system enum`);\n }\n \n parsed.push({\n ...item,\n json: { ...item.json, ai_result: result, parse_error: null }\n });\n } catch (err) {\n parseErrors.push({\n ...item,\n json: {\n ...item.json,\n ai_result: null,\n parse_error: err.message,\n raw_ai_response: item.json.response,\n error_type: 'AI_PARSE_ERROR'\n }\n });\n }\n}\n\nreturn [parsed, parseErrors];" + }, + "id": "json-parser", + "name": "Parse & Validate AI Response", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1800, 0], + "notes": "Parse JSON + Validate Schema + Enum Check" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$workflow.staticData.config.DB_HOST}}", + "port": "={{$workflow.staticData.config.DB_PORT}}", + "database": "={{$workflow.staticData.config.DB_NAME}}", + "user": "={{$workflow.staticData.config.DB_USER}}", + "password": "={{$workflow.staticData.config.DB_PASSWORD}}", + "query": "INSERT INTO migration_fallback_state (batch_id, recent_error_count, is_fallback_active) VALUES ('{{$workflow.staticData.config.BATCH_ID}}', 1, FALSE) ON DUPLICATE KEY UPDATE recent_error_count = recent_error_count + 1, is_fallback_active = CASE WHEN recent_error_count + 1 >= {{$workflow.staticData.config.FALLBACK_THRESHOLD}} THEN TRUE ELSE is_fallback_active END, updated_at = NOW()", + "options": {} +n }, + "id": "fallback-update", + "name": "Update Fallback State", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [2000, 200], + "notes": "เพิ่ม Error count และตรวจสอบ Fallback threshold" + }, + { + "parameters": { + "jsCode": "const config = $workflow.staticData.config;\nconst items = $('Parse & Validate AI Response').all();\n\nconst autoIngest = [];\nconst reviewQueue = [];\nconst rejectLog = [];\nconst errorLog = [];\n\nfor (const item of items) {\n if (item.json.parse_error || !item.json.ai_result) {\n errorLog.push(item);\n continue;\n }\n \n const ai = item.json.ai_result;\n \n // Revision Drift Protection (ถ้ามีข้อมูลจาก DB)\n if (item.json.current_db_revision !== undefined) {\n const expectedRev = item.json.current_db_revision + 1;\n if (parseInt(item.json.excel_revision) !== expectedRev) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Revision drift: Excel=${item.json.excel_revision}, Expected=${expectedRev}` }\n });\n continue;\n }\n }\n \n // Confidence Routing\n if (ai.confidence >= config.CONFIDENCE_HIGH && ai.is_valid === true) {\n autoIngest.push(item);\n } else if (ai.confidence >= config.CONFIDENCE_LOW) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_HIGH}` }\n });\n } else {\n rejectLog.push({\n ...item,\n json: { ...item.json, reject_reason: ai.is_valid === false ? 'AI marked invalid' : `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_LOW}` }\n });\n }\n}\n\n// Output 0: Auto, 1: Review, 2: Reject, 3: Error\nreturn [autoIngest, reviewQueue, rejectLog, errorLog];" + }, + "id": "confidence-router", + "name": "Confidence Router", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [2000, 0], + "notes": "แยกตาม Confidence: Auto(≥0.85) / Review(≥0.60) / Reject(<0.60)" + }, + { + "parameters": { + "method": "POST", + "url": "={{$workflow.staticData.config.BACKEND_URL}}/api/correspondences/import", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "={{$workflow.staticData.config.MIGRATION_TOKEN}}" + }, + { + "name": "Idempotency-Key", + "value": "={{$json.document_number}}:{{$workflow.staticData.config.BATCH_ID}}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"document_number\": \"{{$json.document_number}}\",\n \"title\": \"{{$json.ai_result.suggested_title || $json.title}}\",\n \"category\": \"{{$json.ai_result.suggested_category}}\",\n \"source_file_path\": \"{{$json.file_path}}\",\n \"ai_confidence\": {{$json.ai_result.confidence}},\n \"ai_issues\": {{JSON.stringify($json.ai_result.detected_issues)}},\n \"migrated_by\": \"SYSTEM_IMPORT\",\n \"batch_id\": \"{{$workflow.staticData.config.BATCH_ID}}\",\n \"details\": {\n \"legacy_number\": \"{{$json.legacy_number}}\"\n }\n}", + "options": { + "timeout": 30000 + } + }, + "id": "backend-import", + "name": "Import to Backend", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [2200, -200], + "notes": "ส่งข้อมูลเข้า LCBP3 Backend พร้อม Idempotency-Key" + }, + { + "parameters": { + "jsCode": "const item = $input.first();\nconst shouldCheckpoint = item.json.original_index % 10 === 0;\n\nreturn [{\n json: {\n ...item.json,\n should_update_checkpoint: shouldCheckpoint,\n checkpoint_index: item.json.original_index,\n import_status: 'success',\n timestamp: new Date().toISOString()\n }\n}];" + }, + "id": "checkpoint-flag", + "name": "Flag Checkpoint", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [2400, -200], + "notes": "กำหนดว่าจะบันทึก Checkpoint หรือไม่ (ทุก 10 records)" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$workflow.staticData.config.DB_HOST}}", + "port": "={{$workflow.staticData.config.DB_PORT}}", + "database": "={{$workflow.staticData.config.DB_NAME}}", + "user": "={{$workflow.staticData.config.DB_USER}}", + "password": "={{$workflow.staticData.config.DB_PASSWORD}}", + "query": "INSERT INTO migration_progress (batch_id, last_processed_index, status) VALUES ('{{$workflow.staticData.config.BATCH_ID}}', {{$json.checkpoint_index}}, 'RUNNING') ON DUPLICATE KEY UPDATE last_processed_index = {{$json.checkpoint_index}}, updated_at = NOW()", + "options": {} + }, + "id": "checkpoint-save", + "name": "Save Checkpoint", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [2600, -200], + "notes": "บันทึกความคืบหน้าลง Database" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$workflow.staticData.config.DB_HOST}}", + "port": "={{$workflow.staticData.config.DB_PORT}}", + "database": "={{$workflow.staticData.config.DB_NAME}}", + "user": "={{$workflow.staticData.config.DB_USER}}", + "password": "={{$workflow.staticData.config.DB_PASSWORD}}", + "query": "INSERT INTO migration_review_queue (document_number, title, original_title, ai_suggested_category, ai_confidence, ai_issues, review_reason, status, created_at) VALUES ('{{$json.document_number}}', '{{$json.ai_result.suggested_title || $json.title}}', '{{$json.title}}', '{{$json.ai_result.suggested_category}}', {{$json.ai_result.confidence}}, '{{JSON.stringify($json.ai_result.detected_issues)}}', '{{$json.review_reason}}', 'PENDING', NOW()) ON DUPLICATE KEY UPDATE status = 'PENDING', review_reason = '{{$json.review_reason}}', created_at = NOW()", + "options": {} + }, + "id": "review-queue-insert", + "name": "Insert Review Queue", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4", + "position": [2200, 0], + "notes": "บันทึกรายการที่ต้องตรวจสอบโดยคน (ไม่สร้าง Correspondence)" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst item = $input.first();\nconst config = $workflow.staticData.config;\n\nconst csvPath = `${config.LOG_PATH}/reject_log.csv`;\nconst header = 'timestamp,document_number,title,reject_reason,ai_confidence,ai_issues\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nconst line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.title),\n esc(item.json.reject_reason),\n item.json.ai_result?.confidence ?? 'N/A',\n esc(JSON.stringify(item.json.ai_result?.detected_issues || []))\n].join(',') + '\\n';\n\nfs.appendFileSync(csvPath, line, 'utf8');\n\nreturn [$input.first()];" + }, + "id": "reject-logger", + "name": "Log Reject to CSV", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [2200, 200], + "notes": "บันทึกรายการที่ถูกปฏิเสธลง CSV" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst items = $input.all();\nconst config = $workflow.staticData.config;\n\nconst csvPath = `${config.LOG_PATH}/error_log.csv`;\nconst header = 'timestamp,document_number,error_type,error_message,raw_ai_response\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nfor (const item of items) {\n const line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.error_type || 'UNKNOWN'),\n esc(item.json.error || item.json.parse_error),\n esc(item.json.raw_ai_response || '')\n ].join(',') + '\\n';\n \n fs.appendFileSync(csvPath, line, 'utf8');\n}\n\nreturn items;" + }, + "id": "error-logger-csv", + "name": "Log Error to CSV", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1400, 400], + "notes": "บันทึก Error ลง CSV (จาก File Validator)" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$workflow.staticData.config.DB_HOST}}", + "port": "={{$workflow.staticData.config.DB_PORT}}", + "database": "={{$workflow.staticData.config.DB_NAME}}", + "user": "={{$workflow.staticData.config.DB_USER}}", + "password": "={{$workflow.staticData.config.DB_PASSWORD}}", + "query": "INSERT INTO migration_errors (batch_id, document_number, error_type, error_message, raw_ai_response, created_at) VALUES ('{{$workflow.staticData.config.BATCH_ID}}', '{{$json.document_number}}', '{{$json.error_type || \"UNKNOWN\"}}', '{{$json.error || $json.parse_error}}', '{{$json.raw_ai_response || \"\"}}', NOW())", + "options": {} + }, + "id": "error-logger-db", + "name": "Log Error to DB", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4", + "position": [2000, 400], + "notes": "บันทึก Error ลง MariaDB" + }, + { + "parameters": { + "amount": "={{$workflow.staticData.config.DELAY_MS}}", + "unit": "milliseconds" + }, + "id": "delay-node", + "name": "Delay", + "type": "n8n-nodes-base.wait", + "typeVersion": 1, + "position": [2800, 0], + "notes": "หน่วงเวลาระหว่าง Batches" + } + ], + "connections": { + "Schedule Trigger (22:00)": { + "main": [ + [ + { + "node": "Set Configuration", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set Configuration": { + "main": [ + [ + { + "node": "Fetch Categories", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fetch Categories": { + "main": [ + [ + { + "node": "File Mount Check", + "type": "main", + "index": 0 + } + ] + ] + }, + "File Mount Check": { + "main": [ + [ + { + "node": "Read Checkpoint", + "type": "main", + "index": 0 + } + ] + ] + }, + "Read Checkpoint": { + "main": [ + [ + { + "node": "Process Batch + Encoding", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process Batch + Encoding": { + "main": [ + [ + { + "node": "File Validator", + "type": "main", + "index": 0 + } + ] + ] + }, + "File Validator": { + "main": [ + [ + { + "node": "Build AI Prompt", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log Error to CSV", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build AI Prompt": { + "main": [ + [ + { + "node": "Ollama AI Analysis", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ollama AI Analysis": { + "main": [ + [ + { + "node": "Parse & Validate AI Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse & Validate AI Response": { + "main": [ + [ + { + "node": "Confidence Router", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Update Fallback State", + "type": "main", + "index": 0 + } + ] + ] + }, + "Confidence Router": { + "main": [ + [ + { + "node": "Import to Backend", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Insert Review Queue", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log Reject to CSV", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log Error to DB", + "type": "main", + "index": 0 + } + ] + ] + }, + "Import to Backend": { + "main": [ + [ + { + "node": "Flag Checkpoint", + "type": "main", + "index": 0 + } + ] + ] + }, + "Flag Checkpoint": { + "main": [ + [ + { + "node": "Save Checkpoint", + "type": "main", + "index": 0 + } + ] + ] + }, + "Log Error to CSV": { + "main": [ + [ + { + "node": "Log Error to DB", + "type": "main", + "index": 0 + } + ] + ] + } + } +} +``` + +--- + +## 📄 เอกสารปรับปรุง `03-05-n8n-migration-setup-guide.md` (Free Plan Edition) + +```markdown +# 📋 คู่มือการตั้งค่า n8n สำหรับ Legacy Data Migration (Free Plan Edition) + +> **สำหรับ n8n Free Plan (Self-hosted)** - ไม่ใช้ Environment Variables +> **Version:** 1.8.0-free | **Last Updated:** 2026-03-03 + +--- + +## ⚠️ ความแตกต่างจากเวอร์ชัน Enterprise + +| ฟีเจอร์ | Enterprise | Free Plan (นี้) | +|---------|-----------|----------------| +| Environment Variables | ✅ ใช้ `$env` | ❌ ใช้ `Set Node` + `staticData` | +| External Secrets | ✅ Vault/Secrets Manager | ❌ Hardcode ใน Set Node | +| Multiple Workflows | ✅ Unlimited | ⚠️ รวมเป็น Workflow เดียว | +| Error Handling | ✅ Advanced | ⚠️ Manual Retry | +| Webhook Triggers | ✅ | ✅ ใช้ได้ | + +--- + +## 🏗️ สถาปัตยกรรมใหม่สำหรับ Free Plan + +``` +┌─────────────────────────────────────────────────────────────┐ +│ MIGRATION WORKFLOW v1.8.0-FREE │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ [Schedule Trigger 22:00] │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ ค่า Config ทั้งหมดอยู่ที่นี่ │ +│ │ Set Config │ (แก้ไขใน Code Node นี้เท่านั้น) │ +│ │ (Node 0) │ │ +│ └──────┬──────┘ │ +│ │ │ +│ ┌──────▼──────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │Pre-flight │───▶│Fetch Categories│──▶│File Validator│ │ +│ │Checks │ │from Backend │ │+ Sanitize │ │ +│ └─────────────┘ └──────────────┘ └──────┬───────┘ │ +│ │ │ +│ ┌────────────────────────────┤ │ +│ │ │ │ +│ Valid │ Error │ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ AI Analysis │ │ Error Logger │ │ +│ │ (Ollama) │ │ (CSV + DB) │ │ +│ └────────┬────────┘ └─────────────────┘ │ +│ │ │ +│ ┌────────▼────────┐ │ +│ │ Confidence │ │ +│ │ Router │ │ +│ │ (4 outputs) │ │ +│ └────┬───┬───┬────┘ │ +│ │ │ │ │ +│ ┌─────────┘ │ └─────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌──────┐ ┌──────────┐ ┌────────┐ │ +│ │Auto │ │ Review │ │Reject │ │ +│ │Ingest│ │ Queue │ │Log │ │ +│ │+Chkpt│ │(DB only) │ │(CSV) │ │ +│ └──────┘ └──────────┘ └────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 📝 การตั้งค่า Configuration (สำคัญมาก) + +### ขั้นตอนที่ 1: แก้ไข Node "Set Configuration" + +**เปิด Workflow → คลิก Node "Set Configuration" → แก้ไข Code:** + +```javascript +// ============================================ +// CONFIGURATION - แก้ไขค่าที่นี่เท่านั้น +// ============================================ +const CONFIG = { + // 🔴 สำคัญ: เปลี่ยนทุกค่าที่มี <...> + + // Ollama Settings + OLLAMA_HOST: 'http://192.168.20.100:11434', + OLLAMA_MODEL_PRIMARY: 'llama3.2:3b', + OLLAMA_MODEL_FALLBACK: 'mistral:7b-instruct-q4_K_M', + + // Backend Settings + BACKEND_URL: 'https://api.np-dms.work', + MIGRATION_TOKEN: 'Bearer YOUR_MIGRATION_TOKEN_HERE', // 🔴 เปลี่ยน + + // Batch Settings + BATCH_SIZE: 10, + BATCH_ID: 'migration_20260226', + DELAY_MS: 2000, + + // Thresholds + CONFIDENCE_HIGH: 0.85, + CONFIDENCE_LOW: 0.60, + MAX_RETRY: 3, + FALLBACK_THRESHOLD: 5, + + // Paths (QNAP NAS) + STAGING_PATH: '/share/np-dms/staging_ai', + LOG_PATH: '/share/np-dms/n8n/migration_logs', + + // Database (MariaDB) + DB_HOST: '192.168.1.100', + DB_PORT: 3306, + DB_NAME: 'lcbp3_production', + DB_USER: 'migration_bot', + DB_PASSWORD: 'YOUR_DB_PASSWORD_HERE' // 🔴 เปลี่ยน +}; + +// อย่าแก้โค้ดด้านล่างนี้ +$workflow.staticData = $workflow.staticData || {}; +$workflow.staticData.config = CONFIG; + +return [{ json: { config_loaded: true, timestamp: new Date().toISOString() }}]; +``` + +### ขั้นตอนที่ 2: ตั้งค่า Credentials ใน n8n UI + +เนื่องจาก Free Plan ไม่สามารถซ่อน Sensitive Data ได้ทั้งหมด แนะนำให้: + +1. **สร้าง Dedicated User สำหรับ Migration เท่านั้น** +2. **ใช้ Token ที่มีสิทธิ์จำกัด** (เฉพาะ API ที่จำเป็น) +3. **Rotate Token ทันทีหลัง Migration เสร็จ** + +**การตั้งค่า Credentials (ถ้าใช้):** + +| Credential | Type | ใช้ใน Node | +|-----------|------|-----------| +| Ollama API | HTTP Request | Ollama AI Analysis | +| LCBP3 Backend | HTTP Request | Import to Backend, Fetch Categories | +| MariaDB | MySQL | ทุก Database Node | + +--- + +## 🗄️ การเตรียม Database (เหมือนเดิม) + +รัน SQL นี้บน MariaDB **ก่อน** เริ่มใช้งาน: + +```sql +-- Checkpoint +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 +); + +-- Review Queue +CREATE TABLE IF NOT EXISTS migration_review_queue ( + id INT AUTO_INCREMENT PRIMARY KEY, + document_number VARCHAR(100) NOT NULL, + title TEXT, + original_title TEXT, + ai_suggested_category VARCHAR(50), + ai_confidence DECIMAL(4,3), + ai_issues JSON, + review_reason VARCHAR(255), + status ENUM('PENDING','APPROVED','REJECTED') DEFAULT 'PENDING', + reviewed_by VARCHAR(100), + reviewed_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_doc_number (document_number) +); + +-- Error Log +CREATE TABLE IF NOT EXISTS migration_errors ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id VARCHAR(50), + document_number VARCHAR(100), + error_type ENUM('FILE_NOT_FOUND','AI_PARSE_ERROR','API_ERROR','DB_ERROR','SECURITY','UNKNOWN'), + error_message TEXT, + raw_ai_response TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_batch_id (batch_id), + INDEX idx_error_type (error_type) +); + +-- Fallback State +CREATE TABLE IF NOT EXISTS migration_fallback_state ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id VARCHAR(50) UNIQUE, + recent_error_count INT DEFAULT 0, + is_fallback_active BOOLEAN DEFAULT FALSE, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Idempotency +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) +); +``` + +--- + +## 🐳 Docker Compose สำหรับ QNAP (Free Plan) + +```yaml +version: '3.8' + +services: + n8n: + image: n8nio/n8n:1.78.0 + container_name: n8n-lcbp3 + restart: unless-stopped + ports: + - "5678:5678" + environment: + - TZ=Asia/Bangkok + - NODE_ENV=production + - N8N_BASIC_AUTH_ACTIVE=true + - N8N_BASIC_AUTH_USER=admin + - N8N_BASIC_AUTH_PASSWORD=YOUR_N8N_PASSWORD_HERE + - N8N_ENCRYPTION_KEY=YOUR_ENCRYPTION_KEY_HERE + volumes: + - /share/np-dms/n8n:/home/node/.n8n + - /share/np-dms/n8n/cache:/home/node/.cache + # อ่านอย่างเดียว: ไฟล์ต้นฉบับ + - /share/np-dms/staging_ai:/share/np-dms/staging_ai:ro + # เขียนได้: Logs และ CSV + - /share/np-dms/n8n/migration_logs:/share/np-dms/n8n/migration_logs:rw + networks: + - lcbp3-network + +networks: + lcbp3-network: + external: true +``` + +> **หมายเหตุ:** Free Plan ไม่ต้องใช้ PostgreSQL สำหรับ n8n (ใช้ SQLite ได้) + +--- + +## 🔄 การทำงานของแต่ละ Node + +### Node 0: Set Configuration +- เก็บค่า Config ทั้งหมดใน `$workflow.staticData.config` +- อ่านผ่าน `$workflow.staticData.config.KEY` ใน Node อื่น + +### Node 1-2: Pre-flight Checks +- ตรวจสอบ Backend Health +- ดึง Categories จาก `/api/meta/categories` +- ตรวจ File Mount (Read-only) +- เก็บ Categories ใน `$workflow.staticData.systemCategories` + +### Node 3: Read Checkpoint +- อ่าน `last_processed_index` จาก `migration_progress` +- ถ้าไม่มี เริ่มจาก 0 + +### Node 4: Process Batch +- อ่าน Excel +- Normalize UTF-8 (NFC) +- ตัด Batch ตาม `BATCH_SIZE` + +### Node 5: File Validator +- Sanitize filename (replace special chars) +- Path traversal check +- ตรวจสอบไฟล์มีอยู่จริง +- **Output 2 ทาง**: Valid → AI, Error → Log + +### Node 6: Build AI Prompt +- ดึง Categories จาก `staticData` (ไม่ hardcode) +- เลือก Model ตาม Fallback State +- สร้าง Prompt ตาม Template + +### Node 7: Ollama AI Analysis +- เรียก `POST /api/generate` +- Timeout 30 วินาที +- Retry 3 ครั้ง (n8n built-in) + +### Node 8: Parse & Validate +- Parse JSON Response +- Schema Validation (is_valid, confidence, detected_issues) +- Enum Validation (ตรวจ Category ว่าอยู่ใน List หรือไม่) +- **Output 2 ทาง**: Success → Router, Error → Fallback + +### Node 9: Confidence Router +- **4 Outputs**: + 1. Auto Ingest (confidence ≥ 0.85 && is_valid) + 2. Review Queue (0.60 ≤ confidence < 0.85) + 3. Reject Log (confidence < 0.60 หรือ is_valid = false) + 4. Error Log (parse error) + +### Node 10A: Auto Ingest +- POST `/api/correspondences/import` +- Header: `Idempotency-Key: {doc_num}:{batch_id}` +- บันทึก Checkpoint ทุก 10 records + +### Node 10B: Review Queue +- INSERT เข้า `migration_review_queue` เท่านั้น +- ยังไม่สร้าง Correspondence + +### Node 10C: Reject Log +- เขียน CSV ที่ `/share/np-dms/n8n/migration_logs/reject_log.csv` + +### Node 10D: Error Log +- เขียน CSV + INSERT เข้า `migration_errors` + +--- + +## 🚨 ข้อควรระวังสำหรับ Free Plan + +### 1. Security +- **อย่า Commit ไฟล์นี้เข้า Git** ถ้ามี Password/Token +- ใช้ `.gitignore` สำหรับไฟล์ JSON ที่มี Config +- Rotate Token ทันทีหลังใช้งาน + +### 2. Limitations +- **Execution Timeout**: ตรวจสอบ n8n execution timeout (default 5 นาที) +- **Memory**: จำกัดที่ 2GB (ตาม Docker Compose) +- **Concurrent**: รัน Batch ต่อเนื่อง ไม่ parallel + +### 3. Backup +- สำรอง SQLite database ของ n8n ที่ `/home/node/.n8n` +- สำรอง Logs ที่ `/share/np-dms/n8n/migration_logs` + +--- + +## ✅ Pre-Production Checklist (Free Plan) + +| ลำดับ | รายการ | วิธีตรวจสอบ | +|-------|--------|-------------| +| 1 | Config ถูกต้อง | รัน Test Execution ดูผลลัพธ์ Node 0 | +| 2 | Database Connect ได้ | Test Step ใน Node Read Checkpoint | +| 3 | Ollama พร้อม | `curl http:///api/tags` | +| 4 | Backend Token ใช้ได้ | Test Step ใน Node Fetch Categories | +| 5 | File Mount RO ถูกต้อง | `docker exec n8n ls /share/np-dms/staging_ai` | +| 6 | Log Mount RW ถูกต้อง | `docker exec n8n touch /share/np-dms/n8n/migration_logs/test` | +| 7 | Categories ไม่ hardcode | ดูผลลัพธ์ Node Fetch Categories | +| 8 | Idempotency Key ถูกต้อง | ตรวจ Header ใน Node Import | +| 9 | Checkpoint บันทึก | ตรวจสอบ `migration_progress` หลังรัน | +| 10 | Error Log สร้างไฟล์ | ตรวจสอบ `error_log.csv` | + +--- + +## 🔧 การแก้ไขปัญหาเฉพาะหน้า + +### ปัญหา: Config ไม่ถูกต้อง +**แก้ไข:** แก้ที่ Node "Set Configuration" แล้ว Save → Execute Workflow ใหม่ + +### ปัญหา: Database Connection Error +**ตรวจสอบ:** +```javascript +// ใส่ใน Code Node ชั่วคราวเพื่อ Debug +const config = $workflow.staticData.config; +return [{ json: { + host: config.DB_HOST, + port: config.DB_PORT, + // อย่าแสดง password ใน Production! + test: 'Config loaded: ' + (config ? 'YES' : 'NO') +}}]; +``` + +### ปัญหา: Ollama Timeout +**แก้ไข:** +- เพิ่ม `DELAY_MS` เป็น 3000 หรือ 5000 +- ลด `BATCH_SIZE` เหลือ 5 +- ตรวจสอบ GPU/CPU ของ Ollama Server + +--- + +## 📊 การ Monitor (Manual) + +เนื่องจาก Free Plan ไม่มี Advanced Monitoring: + +```bash +# ดู Progress ล่าสุด +docker exec n8n-lcbp3 sh -c "tail -5 /share/np-dms/n8n/migration_logs/reject_log.csv" + +# ดู Error ล่าสุด +docker exec n8n-lcbp3 sh -c "tail -10 /share/np-dms/n8n/migration_logs/error_log.csv" + +# ดู Checkpoint ใน DB +mysql -h -u migration_bot -p -e "SELECT * FROM migration_progress WHERE batch_id = 'migration_20260226'" +``` + +--- + +**เอกสารฉบับนี้จัดทำขึ้นสำหรับ n8n Free Plan (Self-hosted)** +**Version:** 1.8.0-free | **Last Updated:** 2026-03-03 +``` + +--- + +## 📥 วิธี Import Workflow + +1. บันทึก JSON ด้านบนเป็นไฟล์ `lcbp3-migration-free.json` +2. เข้า n8n UI → **Workflows** → **Import from File** +3. เลือกไฟล์ `lcbp3-migration-free.json` +4. เปิด Workflow → แก้ไข Node **"Set Configuration"** ตามข้อมูลจริง +5. ตั้งค่า **Schedule Trigger** หรือเปลี่ยนเป็น **Manual Trigger** สำหรับทดสอบ +6. **Save** → **Execute Workflow** เพื่อทดสอบ + +ต้องการให้ช่วยปรับแต่งเพิ่มเติมหรือไม่ครับ? เช่น: +- เพิ่ม Node สำหรับส่ง Email แจ้งเตือน +- เพิ่ม Rollback Workflow +- ปรับ Batch Size อัตโนมัติตาม Error Rate diff --git a/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md b/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md index f23845c..9f22aec 100644 --- a/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md +++ b/specs/03-Data-and-Storage/03-05-n8n-migration-setup-guide.md @@ -1,754 +1,267 @@ -# 📋 คู่มือการตั้งค่า n8n สำหรับ Legacy Data Migration +# 📋 คู่มือการตั้งค่า n8n สำหรับ Legacy Data Migration (Free Plan Edition) + +> **สำหรับ n8n Free Plan (Self-hosted)** - ไม่ใช้ Environment Variables +> **Version:** 1.8.0-free | **Last Updated:** 2026-03-04 เอกสารนี้จัดทำขึ้นเพื่อรองรับการ Migration เอกสาร PDF 20,000 ฉบับ ตามแผนใน `03-04-legacy-data-migration.md` และ `ADR-017-ollama-data-migration.md` -> **Note:** Category Enum system-driven, Idempotency-Key Header, Storage Enforcement, Audit Log, Encoding Normalization, Security Hardening, Nginx Rate Limit, Docker Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439), Folder Standard (/share/np-dms/n8n) +--- + +## ⚠️ ความแตกต่างจากเวอร์ชัน Enterprise + +| ฟีเจอร์ | Enterprise | Free Plan (นี้) | +| --------------------- | ----------------------- | ------------------------------ | +| Environment Variables | ✅ ใช้ `$env` | ❌ ใช้ `Set Node` + `staticData` | +| External Secrets | ✅ Vault/Secrets Manager | ❌ Hardcode ใน Set Node | +| Multiple Workflows | ✅ Unlimited | ⚠️ รวมเป็น Workflow เดียว | +| Error Handling | ✅ Advanced | ⚠️ Manual Retry | +| Webhook Triggers | ✅ | ✅ ใช้ได้ | --- -## 📌 ส่วนที่ 1: การติดตั้งและตั้งค่าเบื้องต้น +## 🏗️ สถาปัตยกรรม Free Plan -### 1.1 ปรับปรุง n8n บน QNAP NAS (Docker) - -คุณสามารถเพิ่ม PostgreSQL Service เข้าไปใน `docker-compose-lcbp3-n8n.yml` ปัจจุบันบน QNAP NAS ได้ดังนี้: - -```yaml -version: '3.8' - -x-restart: &restart_policy - restart: unless-stopped - -x-logging: &default_logging - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "5" - -services: - n8n-db: - <<: [*restart_policy, *default_logging] - image: postgres:16-alpine - container_name: n8n-db - environment: - - POSTGRES_USER=n8n - - POSTGRES_PASSWORD= - - POSTGRES_DB=n8n - volumes: - - "/share/np-dms/n8n/postgres-data:/var/lib/postgresql/data" - networks: - lcbp3: {} - healthcheck: - test: ['CMD-SHELL', 'pg_isready -h localhost -U n8n -d n8n'] - interval: 10s - timeout: 5s - retries: 5 - - n8n: - <<: [*restart_policy, *default_logging] - image: n8nio/n8n:1.78.0 - container_name: n8n - depends_on: - n8n-db: - condition: service_healthy - deploy: - resources: - limits: - cpus: "1.5" - memory: 2G - environment: - TZ: "Asia/Bangkok" - NODE_ENV: "production" - N8N_PUBLIC_URL: "https://n8n.np-dms.work/" - WEBHOOK_URL: "https://n8n.np-dms.work/" - N8N_EDITOR_BASE_URL: "https://n8n.np-dms.work/" - N8N_PROTOCOL: "https" - N8N_HOST: "n8n.np-dms.work" - N8N_PORT: 5678 - N8N_PROXY_HOPS: "1" - N8N_DIAGNOSTICS_ENABLED: 'false' - N8N_SECURE_COOKIE: 'true' - N8N_ENCRYPTION_KEY: "9AAIB7Da9DW1qAhJE5/Bz4SnbQjeAngI" - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: 'true' - GENERIC_TIMEZONE: "Asia/Bangkok" - # DB Setup - DB_TYPE: postgresdb - DB_POSTGRESDB_DATABASE: n8n - DB_POSTGRESDB_HOST: n8n-db - DB_POSTGRESDB_PORT: 5432 - DB_POSTGRESDB_USER: n8n - DB_POSTGRESDB_PASSWORD: - # Data Prune - EXECUTIONS_DATA_PRUNE: 'true' - EXECUTIONS_DATA_MAX_AGE: 168 - EXECUTIONS_DATA_PRUNE_TIMEOUT: 60 - ports: - - "5678:5678" - networks: - lcbp3: {} - volumes: - - "/share/np-dms/n8n:/home/node/.n8n" - - "/share/np-dms/n8n/cache:/home/node/.cache" - - "/share/np-dms/n8n/scripts:/scripts" - - "/share/np-dms/n8n/data:/data" - - "/var/run/docker.sock:/var/run/docker.sock" - # read-only: อ่านไฟล์ PDF ต้นฉบับเท่านั้น - - "/share/np-dms/staging_ai:/share/np-dms/staging_ai:ro" - # read-write: เขียน Log และ CSV ทั้งหมด - - "/share/np-dms/n8n/migration_logs:/share/np-dms/n8n/migration_logs:rw" +``` +┌─────────────────────────────────────────────────────────────┐ +│ MIGRATION WORKFLOW v1.8.0-FREE │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ [Schedule Trigger 22:00] │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ ค่า Config ทั้งหมดอยู่ที่นี่ │ +│ │ Set Config │ (แก้ไขใน Code Node นี้เท่านั้น) │ +│ │ (Node 0) │ │ +│ └──────┬──────┘ │ +│ │ │ +│ ┌──────▼──────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │Pre-flight │───▶│Fetch Categories│──▶│File Validator│ │ +│ │Checks │ │from Backend │ │+ Sanitize │ │ +│ └─────────────┘ └──────────────┘ └──────┬───────┘ │ +│ │ │ +│ ┌────────────────────────────┤ │ +│ │ │ │ +│ Valid │ Error │ │ +│ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ AI Analysis │ │ Error Logger │ │ +│ │ (Ollama) │ │ (CSV + DB) │ │ +│ └────────┬────────┘ └─────────────────┘ │ +│ │ │ +│ ┌────────▼────────┐ │ +│ │ Confidence │ │ +│ │ Router │ │ +│ │ (4 outputs) │ │ +│ └────┬───┬───┬────┘ │ +│ │ │ │ │ +│ ┌─────────┘ │ └─────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌──────┐ ┌──────────┐ ┌────────┐ │ +│ │Auto │ │ Review │ │Reject │ │ +│ │Ingest│ │ Queue │ │Log │ │ +│ │+Chkpt│ │(DB only) │ │(CSV) │ │ +│ └──────┘ └──────────┘ └────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ ``` -> ⚠️ **Volume หมายเหตุ:** `/share/np-dms/staging_ai` = **read-only** (อ่านไฟล์ต้นฉบับ) และ `/share/np-dms/n8n/migration_logs` = **read-write** (เขียน Log/CSV) — ห้ามเขียน CSV ลง `staging_ai` เพราะจะ Error ทันที +--- + +## 📌 ส่วนที่ 1: การติดตั้ง + +### 1.1 Docker Compose สำหรับ QNAP + +ดู Config จริงที่ใช้งาน: `specs/04-Infrastructure-OPS/04-00-docker-compose/docker-compose-lcbp3-n8n.yml` + +**สิ่งสำคัญ:** + +| Item | ค่า Production | +| ------------ | ----------------------------------------------------------------------------------------------- | +| Image | `n8nio/n8n:latest` | +| Container | `n8n` | +| Database | PostgreSQL 16 (`n8n-db` service) | +| Network | `lcbp3` (external) | +| File Access | `N8N_RESTRICT_FILE_ACCESS_TO: /home/node/.n8n-files` | +| Staging (RO) | Host: `/share/np-dms-as/Legacy` → Container: `/home/node/.n8n-files/staging_ai:ro` | +| Logs (RW) | Host: `/share/np-dms/n8n/migration_logs` → Container: `/home/node/.n8n-files/migration_logs:rw` | +| Memory Limit | 2GB (reservation 512M) | +| Healthcheck | `wget healthz` (30s interval) | + +> ⚠️ **Volume:** Staging mount = **read-only** (อ่านไฟล์ PDF ต้นฉบับ) — ห้ามเขียน CSV ลง `staging_ai` เพราะจะ Error ทันที + +> ⚠️ **File Access Control:** n8n จำกัดสิทธิ์อ่านไฟล์เฉพาะ `/home/node/.n8n-files` — workflow ต้องใช้ path นี้เท่านั้น ### 1.2 Nginx Rate Limit -เพิ่มใน Nginx config สำหรับ Migration API: - ```nginx # nginx.conf หรือ site config limit_req_zone $binary_remote_addr zone=migration:10m rate=1r/s; -location /api/correspondences/import { +location /api/migration/import { limit_req zone=migration burst=5 nodelay; proxy_pass http://backend:3001; } ``` -### 1.3 Environment Variables - -**Settings → Environment Variables ใน n8n UI:** - -| Variable | ค่าที่แนะนำ | คำอธิบาย | -| --------------------------- | ----------------------------- | ---------------------------------- | -| `OLLAMA_HOST` | `http://192.168.20.100:11434` | URL ของ Ollama (Desktop Desk-5439) | -| `OLLAMA_MODEL_PRIMARY` | `llama3.2:3b` | Model หลัก | -| `OLLAMA_MODEL_FALLBACK` | `mistral:7b-instruct-q4_K_M` | Model สำรอง | -| `MIGRATION_BATCH_SIZE` | `10` | จำนวน Record ต่อ Batch | -| `MIGRATION_DELAY_MS` | `2000` | Delay ระหว่าง Request (ms) | -| `CONFIDENCE_THRESHOLD_HIGH` | `0.85` | Threshold Auto Ingest | -| `CONFIDENCE_THRESHOLD_LOW` | `0.60` | Threshold Review Queue | -| `MAX_RETRY_COUNT` | `3` | จำนวนครั้ง Retry | -| `FALLBACK_ERROR_THRESHOLD` | `5` | Error ที่ trigger Fallback | -| `BACKEND_URL` | `https://` | URL ของ LCBP3 Backend | -| `MIGRATION_BATCH_ID` | `migration_20260226` | ID ของ Batch | - --- -## 📌 ส่วนที่ 2: การเตรียม Database +## 📌 ส่วนที่ 2: การตั้งค่า Configuration (สำคัญมาก) -รัน SQL นี้บน MariaDB **ก่อน** เริ่ม n8n Workflow: +### ขั้นตอนที่ 1: แก้ไข Node "Set Configuration" -```sql --- Checkpoint -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 -); +**เปิด Workflow → คลิก Node "Set Configuration" → แก้ไข Code:** --- Review Queue (Temporary — ไม่ใช่ Business Schema) -CREATE TABLE IF NOT EXISTS migration_review_queue ( - id INT AUTO_INCREMENT PRIMARY KEY, - document_number VARCHAR(100) NOT NULL, - title TEXT, - original_title TEXT, - ai_suggested_category VARCHAR(50), - ai_confidence DECIMAL(4,3), - ai_issues JSON, - review_reason VARCHAR(255), - status ENUM('PENDING','APPROVED','REJECTED') DEFAULT 'PENDING', - reviewed_by VARCHAR(100), - reviewed_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uq_doc_number (document_number) -); - --- Error Log -CREATE TABLE IF NOT EXISTS migration_errors ( - id INT AUTO_INCREMENT PRIMARY KEY, - batch_id VARCHAR(50), - document_number VARCHAR(100), - error_type ENUM('FILE_NOT_FOUND','AI_PARSE_ERROR','API_ERROR','DB_ERROR','UNKNOWN'), - error_message TEXT, - raw_ai_response TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX idx_batch_id (batch_id), - INDEX idx_error_type (error_type) -); - --- Fallback State -CREATE TABLE IF NOT EXISTS migration_fallback_state ( - id INT AUTO_INCREMENT PRIMARY KEY, - batch_id VARCHAR(50) UNIQUE, - recent_error_count INT DEFAULT 0, - is_fallback_active BOOLEAN DEFAULT FALSE, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - --- Idempotency (Patch) -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) -); - --- Daily Summary -CREATE TABLE IF NOT EXISTS migration_daily_summary ( - id INT AUTO_INCREMENT PRIMARY KEY, - batch_id VARCHAR(50), - summary_date DATE, - total_processed INT DEFAULT 0, - auto_ingested INT DEFAULT 0, - sent_to_review INT DEFAULT 0, - rejected INT DEFAULT 0, - errors INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uq_batch_date (batch_id, summary_date) -); -``` - ---- - -## 📌 ส่วนที่ 3: Credentials - -**Credentials → Add New:** - -#### 🔐 Ollama API -| Field | ค่า | -| -------------- | ----------------------------- | -| Name | `Ollama Local API` | -| Type | `HTTP Request` | -| Base URL | `http://192.168.20.100:11434` | -| Authentication | `None` | - -#### 🔐 LCBP3 Backend API -| Field | ค่า | -| -------------- | --------------------------- | -| Name | `LCBP3 Migration Token` | -| Type | `HTTP Request` | -| Base URL | `https:///api` | -| Authentication | `Header Auth` | -| Header Name | `Authorization` | -| Header Value | `Bearer ` | - -#### 🔐 MariaDB -| Field | ค่า | -| -------- | ------------------ | -| Name | `LCBP3 MariaDB` | -| Type | `MariaDB` | -| Host | `` | -| Port | `3306` | -| Database | `lcbp3_production` | -| User | `migration_bot` | -| Password | `` | - ---- - -## 📌 ส่วนที่ 4: Workflow (Step-by-Step) - -### 4.1 โครงสร้างภาพรวม - -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ MIGRATION WORKFLOW v1.8.0 │ -├──────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │ Node 0 │──▶│ Node 1 │──▶│ Node 2 │──▶│ Node 3 │ │ -│ │Pre- │ │ Data │ │ File │ │ AI │ │ -│ │flight + │ │ Reader │ │ Validat.│ │Analysis │ │ -│ │Fetch Cat│ │+Encoding│ │+Sanitize│ │+Enum Chk│ │ -│ └─────────┘ └─────────┘ └──┬──┬───┘ └────┬────┘ │ -│ │ │ │ │ -│ valid │ │ error ┌───▼──────────────┐ │ -│ │ └──────▶ │ Node 3.5 │ │ -│ │ │ Fallback Manager │ │ -│ │ └──────────────────┘ │ -│ ▼ │ │ -│ ┌─────────┐ ┌────▼────┐ │ -│ │ Node 5D │ │ Node 4 │ │ -│ │ Error │ │Confidence│ │ -│ │ Log │ │+Revision │ │ -│ └─────────┘ │ Drift │ │ -│ └┬──┬──┬──┘ │ -│ │ │ │ │ -│ ┌────────────┘ │ └──────┐ │ -│ ▼ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ ┌──────┐ │ -│ │ Node 5A │ │ Node 5B │ │ 5C │ │ -│ │ Auto │ │ Review │ │Reject│ │ -│ │ Ingest │ │ Queue │ │ Log │ │ -│ │+Idempot. │ │(Temp only│ └──────┘ │ -│ └────┬─────┘ └──────────┘ │ -│ │ │ -│ ┌────▼──────┐ │ -│ │ Checkpoint│ │ -│ └───────────┘ │ -└──────────────────────────────────────────────────────────────────────┘ -``` - ---- - -### 4.2 Node 0: Pre-flight + Fetch System Categories - -Fetch System Categories ก่อน Batch ทุกครั้ง - -**Sub-flow:** -``` -[Trigger] → [HTTP: Ollama /api/tags] → [MariaDB: SELECT 1] - → [HTTP: Backend /health] → [Code: File Mount Check] - → [HTTP: GET /api/meta/categories] → [Store in Workflow Variable] - → [IF all pass → Node 1] [ELSE → Stop + Alert] -``` - -**HTTP Node — Fetch Categories:** -```json -{ - "method": "GET", - "url": "={{ $env.BACKEND_URL }}/api/meta/categories", - "authentication": "genericCredentialType", - "genericAuthType": "lcbp3MigrationToken" -} -``` - -**Code Node — Store Categories + File Mount Check:** ```javascript -const fs = require('fs'); +// ============================================ +// CONFIGURATION - แก้ไขค่าที่นี่เท่านั้น +// ============================================ +const CONFIG = { + // 🔴 สำคัญ: เปลี่ยนทุกค่าที่มี <...> -// เก็บ categories ใน Workflow Variable -const categories = $input.first().json.categories; -if (!categories || !Array.isArray(categories) || categories.length === 0) { - throw new Error('Failed to fetch system categories from backend'); -} + // Ollama Settings + OLLAMA_HOST: 'http://192.168.20.100:11434', + OLLAMA_MODEL_PRIMARY: 'llama3.2:3b', + OLLAMA_MODEL_FALLBACK: 'mistral:7b-instruct-q4_K_M', -// Set Workflow Variable เพื่อใช้ใน Node 3 -$workflow.variables = $workflow.variables || {}; -$workflow.variables.system_categories = categories; + // Backend Settings + BACKEND_URL: 'https://api.np-dms.work', + MIGRATION_TOKEN: 'Bearer YOUR_MIGRATION_TOKEN_HERE', // 🔴 เปลี่ยน -// ตรวจ File Mount -try { - const files = fs.readdirSync('/share/np-dms/staging_ai'); - if (files.length === 0) throw new Error('staging_ai is empty'); - fs.writeFileSync('/share/np-dms/n8n/migration_logs/.preflight_ok', new Date().toISOString()); -} catch (err) { - throw new Error(`File mount check failed: ${err.message}`); -} + // Batch Settings + BATCH_SIZE: 10, + BATCH_ID: 'migration_20260226', + DELAY_MS: 2000, -return [{ json: { preflight_ok: true, system_categories: categories } }]; -``` + // Thresholds + CONFIDENCE_HIGH: 0.85, + CONFIDENCE_LOW: 0.60, + MAX_RETRY: 3, + FALLBACK_THRESHOLD: 5, ---- + // Paths (Container paths — ต้องตรงกับ volume mount ใน docker-compose) + STAGING_PATH: '/home/node/.n8n-files/staging_ai', + LOG_PATH: '/home/node/.n8n-files/migration_logs', -### 4.3 Node 1: Load Checkpoint + Read Excel + Encoding Normalization - -**Step 1 — MariaDB Node (Read Checkpoint):** -```sql -SELECT last_processed_index, status -FROM migration_progress -WHERE batch_id = '{{ $env.MIGRATION_BATCH_ID }}' -LIMIT 1; -``` - -**Step 2 — Spreadsheet File Node:** -```json -{ "operation": "toData", "binaryProperty": "data", "options": { "sheetName": "Sheet1" } } -``` - -**Step 3 — Code Node (Checkpoint + Batch + Encoding):** -```javascript -const checkpointResult = $('Read Checkpoint').first(); -let startIndex = 0; -if (checkpointResult && checkpointResult.json.status === 'RUNNING') { - startIndex = checkpointResult.json.last_processed_index || 0; -} - -const allItems = $('Read Excel').all(); -const remaining = allItems.slice(startIndex); -const batchSize = parseInt($env.MIGRATION_BATCH_SIZE) || 10; -const currentBatch = remaining.slice(0, batchSize); - -// Encoding Normalization: Excel → UTF-8 NFC (Patch) -const normalize = (str) => { - if (!str) return ''; - return Buffer.from(String(str), 'utf8').toString('utf8').normalize('NFC'); + // Database (MariaDB) + DB_HOST: '192.168.1.100', + DB_PORT: 3306, + DB_NAME: 'lcbp3_production', + DB_USER: 'migration_bot', + DB_PASSWORD: 'YOUR_DB_PASSWORD_HERE' // 🔴 เปลี่ยน }; -return currentBatch.map((item, i) => ({ - ...item, - json: { - ...item.json, - document_number: normalize(item.json.document_number), - title: normalize(item.json.title), - original_index: startIndex + i - } -})); +// อย่าแก้โค้ดด้านล่างนี้ +$workflow.staticData = $workflow.staticData || {}; +$workflow.staticData.config = CONFIG; + +return [{ json: { config_loaded: true, timestamp: new Date().toISOString() }}]; ``` +### ขั้นตอนที่ 2: ตั้งค่า Credentials ใน n8n UI + +เนื่องจาก Free Plan ไม่สามารถซ่อน Sensitive Data ได้ทั้งหมด แนะนำให้: + +1. **สร้าง Dedicated User สำหรับ Migration เท่านั้น** +2. **ใช้ Token ที่มีสิทธิ์จำกัด** (เฉพาะ API ที่จำเป็น) +3. **Rotate Token ทันทีหลัง Migration เสร็จ** + +**Credentials (ถ้าใช้):** + +| Credential | Type | ใช้ใน Node | +| ---------- | ---- | --------- || +| Ollama API | HTTP Request | Ollama AI Analysis | +| LCBP3 Backend | HTTP Request | Import to Backend, Fetch Categories | +| MariaDB | MySQL | ทุก Database Node | + --- -### 4.4 Node 2: File Validator & Sanitizer +## 📌 ส่วนที่ 3: การเตรียม Database -**Node Type:** `Code` — **2 Outputs** +รัน SQL จากไฟล์แยก **ก่อน** เริ่ม Workflow: -```javascript -const fs = require('fs'); -const path = require('path'); -const items = $input.all(); -const validatedItems = []; -const errorItems = []; - -for (const item of items) { - const docNumber = item.json.document_number; - // Sanitize + Normalize Filename (Patch) - const safeName = path.basename( - String(docNumber).replace(/[^a-zA-Z0-9\-_.]/g, '_') - ).normalize('NFC'); - const filePath = path.resolve('/share/np-dms/staging_ai', `${safeName}.pdf`); - - if (!filePath.startsWith('/share/np-dms/staging_ai/')) { - errorItems.push({ ...item, json: { ...item.json, error: 'Path traversal detected', error_type: 'FILE_NOT_FOUND' } }); - continue; - } - - try { - if (fs.existsSync(filePath)) { - const stats = fs.statSync(filePath); - validatedItems.push({ ...item, json: { ...item.json, file_exists: true, file_size: stats.size, file_path: filePath } }); - } else { - errorItems.push({ ...item, json: { ...item.json, error: `File not found: ${filePath}`, error_type: 'FILE_NOT_FOUND', file_exists: false } }); - } - } catch (err) { - errorItems.push({ ...item, json: { ...item.json, error: err.message, error_type: 'FILE_NOT_FOUND', file_exists: false } }); - } -} - -// Output 0 → Node 3 | Output 1 → Node 5D -return [validatedItems, errorItems]; +```bash +mysql -h -u migration_bot -p lcbp3_production < lcbp3-v1.8.0-migration.sql ``` +> ดูรายละเอียดตาราง: [`lcbp3-v1.8.0-migration.sql`](./lcbp3-v1.8.0-migration.sql) + +**ตารางที่สร้าง (6 ตาราง ชั่วคราว — ลบได้หลัง Migration เสร็จ):** + +| ตาราง | วัตถุประสงค์ | +| -------------------------- | ------------------------------- | +| `migration_progress` | Checkpoint ติดตามความคืบหน้า Batch | +| `migration_review_queue` | รายการที่ต้องตรวจสอบโดยคน | +| `migration_errors` | Error Log | +| `migration_fallback_state` | สถานะ AI Model Fallback | +| `import_transactions` | Idempotency ป้องกัน Import ซ้ำ | +| `migration_daily_summary` | สรุปผลรายวัน | + --- -### 4.5 Node 3: Build Prompt + AI Analysis +## 📌 ส่วนที่ 4: การทำงานของแต่ละ Node -**Step 1 — MariaDB (Read Fallback State):** -```sql -SELECT is_fallback_active FROM migration_fallback_state -WHERE batch_id = '{{ $env.MIGRATION_BATCH_ID }}' LIMIT 1; -``` +### Node 0: Set Configuration +- เก็บค่า Config ทั้งหมดใน `$workflow.staticData.config` +- อ่านผ่าน `$workflow.staticData.config.KEY` ใน Node อื่น -**Step 2 — Code Node (Build Prompt: inject system_categories):** -```javascript -const fallbackState = $('Read Fallback State').first(); -const isFallback = fallbackState?.json?.is_fallback_active || false; -const model = isFallback ? $env.OLLAMA_MODEL_FALLBACK : $env.OLLAMA_MODEL_PRIMARY; +### Node 1-2: Pre-flight Checks +- ตรวจสอบ Backend Health +- ดึง Categories จาก `/api/master/correspondence-types` +- ตรวจ File Mount (Read-only) +- เก็บ Categories ใน `$workflow.staticData.systemCategories` -// ใช้ system_categories จาก Workflow Variable (ไม่ hardcode) -const systemCategories = $workflow.variables?.system_categories - || ['Correspondence','RFA','Drawing','Transmittal','Report','Other']; +### Node 3: Read Checkpoint +- อ่าน `last_processed_index` จาก `migration_progress` +- ถ้าไม่มี เริ่มจาก 0 -const item = $input.first(); +### Node 4: Process Batch +- อ่าน Excel +- Normalize UTF-8 (NFC) +- ตัด Batch ตาม `BATCH_SIZE` -const systemPrompt = `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 [].`; +### Node 5: File Validator +- Sanitize filename (replace special chars) +- Path traversal check +- ตรวจสอบไฟล์มีอยู่จริง +- **Output 2 ทาง**: Valid → AI, Error → Log -const userPrompt = `Validate this document metadata and respond in JSON: +### Node 6: Build AI Prompt +- ดึง Categories จาก `staticData` (ไม่ hardcode) +- เลือก Model ตาม Fallback State +- สร้าง Prompt ตาม Template -Document Number: ${item.json.document_number} -Title: ${item.json.title} -Expected Pattern: [ORG]-[TYPE]-[SEQ] e.g. "TCC-COR-0001" -Category List (MUST match system enum exactly): ${JSON.stringify(systemCategories)} +### Node 7: Ollama AI Analysis +- เรียก `POST /api/generate` +- Timeout 30 วินาที +- Retry 3 ครั้ง (n8n built-in) -Respond ONLY with this exact JSON structure: -{ - "is_valid": true | false, - "confidence": 0.0 to 1.0, - "suggested_category": "", - "detected_issues": [""], - "suggested_title": "" -}`; +### Node 8: Parse & Validate +- Parse JSON Response +- Schema Validation (is_valid, confidence, detected_issues) +- Enum Validation (ตรวจ Category ว่าอยู่ใน List หรือไม่) +- **Output 2 ทาง**: Success → Router, Error → Fallback -return [{ - json: { - ...item.json, - active_model: model, - system_categories: systemCategories, - ollama_payload: { model, prompt: `${systemPrompt}\n\n${userPrompt}`, stream: false, format: 'json' } - } -}]; -``` +### Node 9: Confidence Router +- **4 Outputs**: + 1. Auto Ingest (confidence ≥ 0.85 && is_valid) + 2. Review Queue (0.60 ≤ confidence < 0.85) + 3. Reject Log (confidence < 0.60 หรือ is_valid = false) + 4. Error Log (parse error) -**Step 3 — HTTP Request Node (Ollama):** -```json -{ - "method": "POST", - "url": "={{ $env.OLLAMA_HOST }}/api/generate", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ $json.ollama_payload }}", - "options": { "timeout": 30000, "retry": { "count": 3, "delay": 2000, "backoff": "exponential" } } -} -``` +### Node 10A: Auto Ingest +- POST `/api/migration/import` +- Header: `Idempotency-Key: {doc_num}:{batch_id}` +- บันทึก Checkpoint ทุก 10 records -**Step 4 — Code Node (Parse + Validate: Enum check):** -```javascript -const items = $input.all(); -const parsed = []; -const parseErrors = []; +### Node 10B: Review Queue +- INSERT เข้า `migration_review_queue` เท่านั้น +- ยังไม่สร้าง Correspondence -for (const item of items) { - try { - let raw = item.json.response || ''; - raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim(); - const result = JSON.parse(raw); +### Node 10C: Reject Log +- เขียน CSV ที่ `/home/node/.n8n-files/migration_logs/reject_log.csv` - // Strict Schema Validation - if (typeof result.is_valid !== 'boolean') - throw new Error('is_valid must be boolean'); - if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) - throw new Error('confidence must be float 0.0–1.0'); - if (!Array.isArray(result.detected_issues)) - throw new Error('detected_issues must be array'); - - // Enum Validation ตรง System Categories (Patch) - const systemCategories = item.json.system_categories || []; - if (!systemCategories.includes(result.suggested_category)) - throw new Error(`Category "${result.suggested_category}" not in system enum: [${systemCategories.join(', ')}]`); - - parsed.push({ ...item, json: { ...item.json, ai_result: result, parse_error: null } }); - } catch (err) { - parseErrors.push({ - ...item, - json: { ...item.json, ai_result: null, parse_error: err.message, raw_ai_response: item.json.response, error_type: 'AI_PARSE_ERROR' } - }); - } -} - -// Output 0 → Node 4 | Output 1 → Node 3.5 + Node 5D -return [parsed, parseErrors]; -``` - ---- - -### 4.6 Node 3.5: Fallback Model Manager - -**MariaDB Node:** -```sql -INSERT INTO migration_fallback_state (batch_id, recent_error_count, is_fallback_active) -VALUES ('{{ $env.MIGRATION_BATCH_ID }}', 1, FALSE) -ON DUPLICATE KEY UPDATE - recent_error_count = recent_error_count + 1, - is_fallback_active = CASE - WHEN recent_error_count + 1 >= {{ $env.FALLBACK_ERROR_THRESHOLD }} THEN TRUE - ELSE is_fallback_active - END, - updated_at = NOW(); -``` - -**Code Node (Alert):** -```javascript -const state = $input.first().json; -if (state.is_fallback_active) { - return [{ json: { - ...state, alert: true, - alert_message: `⚠️ Fallback model (${$env.OLLAMA_MODEL_FALLBACK}) activated after ${state.recent_error_count} errors` - }}]; -} -return [{ json: { ...state, alert: false } }]; -``` - ---- - -### 4.7 Node 4: Confidence Router + Revision Drift Protection - -**Node Type:** `Code` — **4 Outputs** - -```javascript -const items = $input.all(); -const autoIngest = []; -const reviewQueue = []; -const rejectLog = []; -const errorLog = []; - -const HIGH = parseFloat($env.CONFIDENCE_THRESHOLD_HIGH) || 0.85; -const LOW = parseFloat($env.CONFIDENCE_THRESHOLD_LOW) || 0.60; - -for (const item of items) { - if (item.json.parse_error || !item.json.ai_result) { - errorLog.push(item); continue; - } - - // Revision Drift Protection - if (item.json.excel_revision !== undefined) { - const expectedRev = (item.json.current_db_revision || 0) + 1; - if (parseInt(item.json.excel_revision) !== expectedRev) { - reviewQueue.push({ - ...item, - json: { ...item.json, review_reason: `Revision drift: Excel=${item.json.excel_revision}, Expected=${expectedRev}` } - }); - continue; - } - } - - const ai = item.json.ai_result; - if (ai.confidence >= HIGH && ai.is_valid === true) { - autoIngest.push(item); - } else if (ai.confidence >= LOW) { - reviewQueue.push({ ...item, json: { ...item.json, review_reason: `Confidence ${ai.confidence.toFixed(2)} < ${HIGH}` } }); - } else { - rejectLog.push({ - ...item, - json: { ...item.json, reject_reason: ai.is_valid === false ? 'AI marked invalid' : `Confidence ${ai.confidence.toFixed(2)} < ${LOW}` } - }); - } -} - -// Output 0: Auto Ingest | 1: Review Queue | 2: Reject Log | 3: Error Log -return [autoIngest, reviewQueue, rejectLog, errorLog]; -``` - ---- - -### 4.8 Node 5A: Auto Ingest + Idempotency + Checkpoint - -**HTTP Request Node (Patch — Idempotency-Key Header + source_file_path):** -```json -{ - "method": "POST", - "url": "={{ $env.BACKEND_URL }}/api/correspondences/import", - "authentication": "genericCredentialType", - "genericAuthType": "lcbp3MigrationToken", - "sendHeaders": true, - "headers": { - "Idempotency-Key": "={{ $json.document_number }}:{{ $env.MIGRATION_BATCH_ID }}" - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": { - "document_number": "={{ $json.document_number }}", - "title": "={{ $json.ai_result.suggested_title || $json.title }}", - "category": "={{ $json.ai_result.suggested_category }}", - "source_file_path": "={{ $json.file_path }}", - "ai_confidence": "={{ $json.ai_result.confidence }}", - "ai_issues": "={{ $json.ai_result.detected_issues }}", - "migrated_by": "SYSTEM_IMPORT", - "batch_id": "={{ $env.MIGRATION_BATCH_ID }}", - "details": { - "legacy_number": "={{ $json.legacy_document_number }}" - } - }, - "options": { "timeout": 30000, "retry": { "count": 3, "delay": 5000 } } -} -``` - -> Backend จะ generate UUID, enforce Storage path `/storage/{project}/{category}/{year}/{month}/{uuid}.pdf`, move file ผ่าน StorageService และบันทึก Audit Log `action=IMPORT, source=MIGRATION` - -**Checkpoint Code Node (ทุก 10 Records):** -```javascript -const item = $input.first(); -return [{ json: { - ...item.json, - should_update_checkpoint: item.json.original_index % 10 === 0, - checkpoint_index: item.json.original_index -}}]; -``` - -**IF Node → MariaDB Checkpoint:** -```sql -INSERT INTO migration_progress (batch_id, last_processed_index, status) -VALUES ('{{ $env.MIGRATION_BATCH_ID }}', {{ $json.checkpoint_index }}, 'RUNNING') -ON DUPLICATE KEY UPDATE last_processed_index = {{ $json.checkpoint_index }}, updated_at = NOW(); -``` - ---- - -### 4.9 Node 5B: Review Queue (Temporary Table) - -> ⚠️ ห้ามสร้าง Correspondence record — รอ Admin Approve แล้วค่อย POST `/api/correspondences/import` - -```sql -INSERT INTO migration_review_queue - (document_number, title, original_title, ai_suggested_category, - ai_confidence, ai_issues, review_reason, status, created_at) -VALUES ( - '{{ $json.document_number }}', - '{{ $json.ai_result.suggested_title || $json.title }}', - '{{ $json.title }}', - '{{ $json.ai_result.suggested_category }}', - {{ $json.ai_result.confidence }}, - '{{ JSON.stringify($json.ai_result.detected_issues) }}', - '{{ $json.review_reason }}', - 'PENDING', NOW() -) -ON DUPLICATE KEY UPDATE status = 'PENDING', review_reason = '{{ $json.review_reason }}'; -``` - ---- - -#### 4.10 Node 5C: Reject Log → `/share/np-dms/n8n/migration_logs/` - -```javascript -const fs = require('fs'); -const item = $input.first(); -const csvPath = '/share/np-dms/n8n/migration_logs/reject_log.csv'; -const header = 'timestamp,document_number,title,reject_reason,ai_confidence,ai_issues\n'; -const esc = (s) => `"${String(s||'').replace(/"/g,'""')}"`; - -if (!fs.existsSync(csvPath)) fs.writeFileSync(csvPath, header, 'utf8'); - -const line = [ - new Date().toISOString(), - esc(item.json.document_number), esc(item.json.title), - esc(item.json.reject_reason), - item.json.ai_result?.confidence ?? 'N/A', - esc(JSON.stringify(item.json.ai_result?.detected_issues || [])) -].join(',') + '\n'; - -fs.appendFileSync(csvPath, line, 'utf8'); -return [$input.first()]; -``` - ---- - -#### 4.11 Node 5D: Error Log → `/share/np-dms/n8n/migration_logs/` + MariaDB - -```javascript -const fs = require('fs'); -const item = $input.first(); -const csvPath = '/share/np-dms/n8n/migration_logs/error_log.csv'; -const header = 'timestamp,document_number,error_type,error_message,raw_ai_response\n'; -const esc = (s) => `"${String(s||'').replace(/"/g,'""')}"`; - -if (!fs.existsSync(csvPath)) fs.writeFileSync(csvPath, header, 'utf8'); - -const line = [ - new Date().toISOString(), - esc(item.json.document_number), - esc(item.json.error_type || 'UNKNOWN'), - esc(item.json.error || item.json.parse_error), - esc(item.json.raw_ai_response || '') -].join(',') + '\n'; - -fs.appendFileSync(csvPath, line, 'utf8'); -return [$input.first()]; -``` - -**MariaDB Node:** -```sql -INSERT INTO migration_errors - (batch_id, document_number, error_type, error_message, raw_ai_response, created_at) -VALUES ( - '{{ $env.MIGRATION_BATCH_ID }}', '{{ $json.document_number }}', - '{{ $json.error_type || "UNKNOWN" }}', '{{ $json.error || $json.parse_error }}', - '{{ $json.raw_ai_response || "" }}', NOW() -); -``` +### Node 10D: Error Log +- เขียน CSV + INSERT เข้า `migration_errors` --- @@ -764,23 +277,27 @@ VALUES ( │ PASS ▼ [MariaDB: Disable Token] -UPDATE users SET is_active = false WHERE username = 'migration_bot'; +UPDATE users SET is_active = 0 WHERE username = 'migration_bot'; │ ▼ -[MariaDB: Delete File Records] -DELETE FROM correspondence_files WHERE correspondence_id IN - (SELECT id FROM correspondences WHERE created_by = 'SYSTEM_IMPORT'); +[MariaDB: Delete Attachments] +DELETE ca FROM correspondence_attachments ca + INNER JOIN correspondences c ON c.id = ca.correspondence_id + WHERE c.created_by = (SELECT user_id FROM users WHERE username = 'migration_bot'); │ ▼ [MariaDB: Delete Correspondence Records] -DELETE FROM correspondences WHERE created_by = 'SYSTEM_IMPORT'; +DELETE FROM correspondences + WHERE created_by = (SELECT user_id FROM users WHERE username = 'migration_bot'); │ ▼ [MariaDB: Clear Idempotency Records] -DELETE FROM import_transactions WHERE batch_id = '{{$env.MIGRATION_BATCH_ID}}'; +DELETE FROM import_transactions WHERE batch_id = ''; │ ▼ [MariaDB: Reset Checkpoint + Fallback State] +DELETE FROM migration_progress WHERE batch_id = ''; +DELETE FROM migration_fallback_state WHERE batch_id = ''; │ ▼ [Email: Rollback Report → Admin] @@ -796,108 +313,7 @@ return $input.all(); --- -## 📌 ส่วนที่ 6: End-of-Night Summary (06:30 ทุกวัน) - -**MariaDB:** -```sql -SELECT - mp.last_processed_index AS total_progress, - (SELECT COUNT(*) FROM correspondences - WHERE created_by = 'SYSTEM_IMPORT' AND DATE(created_at) = CURDATE()) AS auto_ingested, - (SELECT COUNT(*) FROM migration_review_queue WHERE DATE(created_at) = CURDATE()) AS sent_to_review, - (SELECT COUNT(*) FROM migration_errors - WHERE batch_id = '{{ $env.MIGRATION_BATCH_ID }}' AND DATE(created_at) = CURDATE()) AS errors -FROM migration_progress mp WHERE mp.batch_id = '{{ $env.MIGRATION_BATCH_ID }}'; -``` - -**Code Node (Build Report):** -```javascript -const s = $input.first().json; -const total = 20000; -const pct = ((s.total_progress / total) * 100).toFixed(1); -const nightsLeft = Math.ceil((total - s.total_progress) / (8 * 3600 / 3)); - -const report = ` -📊 Migration Night Summary — ${new Date().toLocaleDateString('th-TH')} -${'─'.repeat(50)} -✅ Auto Ingested : ${s.auto_ingested} -🔍 Sent to Review : ${s.sent_to_review} -❌ Errors : ${s.errors} -───────────────────────────────────────────────── -📈 Progress : ${s.total_progress} / ${total} (${pct}%) -🌙 Est. Nights Left: ~${nightsLeft} คืน -${'─'.repeat(50)} -${s.errors > 50 ? '⚠️ WARNING: High error count — investigate before next run' : '✅ Error rate OK'} -`; -return [{ json: { report, stats: s } }]; -``` - ---- - -## 📌 ส่วนที่ 7: Monitoring (Hourly Alert — เฉพาะเมื่อเกิน Threshold) - -**Code Node (Evaluate):** -```javascript -const s = $input.first().json; -const alerts = []; - -if (s.minutes_since_update > 30) - alerts.push(`⚠️ No progress for ${s.minutes_since_update} min — may be stuck`); -if (s.is_fallback_active) - alerts.push(`⚠️ Fallback model active — errors: ${s.recent_error_count}`); -if (s.recent_error_count >= 20) - alerts.push(`🔴 Critical: ${s.recent_error_count} errors — consider stopping`); - -return [{ json: { ...s, has_alerts: alerts.length > 0, alerts } }]; -``` - -**IF `has_alerts = true` → Email Alert ทันที** - ---- - -## 📌 ส่วนที่ 8: Pre-Production Checklist - -| ลำดับ | รายการทดสอบ | ผลลัพธ์ที่คาดหวัง | ✅/❌ | -| --- | --------------------------------------------- | ---------------------- | --- | -| 1 | Pre-flight ผ่านทุก Check | All green | | -| 2 | `GET /api/meta/categories` สำเร็จ | categories array ไม่ว่าง | | -| 3 | Enum ใน Prompt ไม่ hardcode | ตรงกับ Backend | | -| 4 | Idempotency: รัน Batch ซ้ำ | ไม่สร้าง Revision ซ้ำ | | -| 5 | Storage path ตาม Spec | UUID + /year/month/ | | -| 6 | Audit Log มี `action=IMPORT, source=MIGRATION` | Verified | | -| 7 | Review Queue ไม่สร้าง record อัตโนมัติ | Verified | | -| 8 | Revision drift → Review Queue | Verified | | -| 9 | Error ≥ 5 → Fallback Model สลับ | mistral:7b active | | -| 10 | Reject/Error CSV เขียนลง `migration_logs/` | ไม่ใช่ `staging_ai/` | | -| 11 | Rollback Guard ต้องพิมพ์ CONFIRM_ROLLBACK | Block ทำงาน | | -| 12 | Night Summary 06:30 + Est. nights left | Email ถึง Admin | | -| 13 | Monitoring Alert เฉพาะเกิน Threshold | ไม่ spam ทุกชั่วโมง | | -| 14 | Nginx Rate Limit `burst=5` | Configured | | -| 15 | Docker `mem_limit=2g` + log rotation | Configured | | - -**คำสั่งทดสอบ:** -```bash -# Ollama -docker exec -it n8n-migration curl http://:11434/api/tags - -# RO mount -docker exec -it n8n-migration ls /data/dms/staging_ai | head -5 - -# RW mount -docker exec -it n8n-migration sh -c "echo ok > /data/dms/migration_logs/test.txt && echo '✅ rw OK'" - -# DB -docker exec -it n8n-migration mysql -h -u migration_bot -p -e "SELECT 1" - -# Backend + Category endpoint -curl -H "Authorization: Bearer " https:///api/meta/categories -``` - ---- - -## 📌 ส่วนที่ 9: การรันงานจริง - -### 9.1 Daily Operation +## 📌 ส่วนที่ 6: Daily Operation | เวลา | กิจกรรม | ผู้รับผิดชอบ | | ----- | ------------------------------ | ------------------- | @@ -907,11 +323,11 @@ curl -H "Authorization: Bearer " https:///api/meta/categories | 22:00 | Workflow เริ่มรันอัตโนมัติ | System | | 06:30 | Night Summary Report ส่ง Email | System | -### 9.2 Emergency Stop +### Emergency Stop ```bash # 1. หยุด n8n -docker stop n8n-migration +docker stop n8n # 2. Disable Token mysql -h -u root -p \ @@ -924,15 +340,100 @@ mysql -h -u root -p \ # 4. Errors mysql -h -u root -p \ -e "SELECT * FROM migration_errors ORDER BY created_at DESC LIMIT 20;" - -# 5. Rollback ผ่าน Webhook -curl -X POST http://:5678/webhook/rollback \ - -H "Content-Type: application/json" \ - -d '{"confirmation":"CONFIRM_ROLLBACK"}' ``` --- +## 🚨 ข้อควรระวังสำหรับ Free Plan + +### 1. Security +- **อย่า Commit ไฟล์นี้เข้า Git** ถ้ามี Password/Token +- ใช้ `.gitignore` สำหรับไฟล์ JSON ที่มี Config +- Rotate Token ทันทีหลังใช้งาน + +### 2. Limitations +- **Execution Timeout**: ตรวจสอบ n8n execution timeout (default 5 นาที) +- **Memory**: จำกัดที่ 2GB (ตาม Docker Compose) +- **Concurrent**: รัน Batch ต่อเนื่อง ไม่ parallel + +### 3. Backup +- สำรอง PostgreSQL data ที่ `/share/np-dms/n8n/postgres-data` +- สำรอง n8n data ที่ `/share/np-dms/n8n` +- สำรอง Logs ที่ `/share/np-dms/n8n/migration_logs` + +--- + +## ✅ Pre-Production Checklist (Free Plan) + +| ลำดับ | รายการ | วิธีตรวจสอบ | +| --- | ---------------------- | ----------------------------------------------------------------- | +| 1 | Config ถูกต้อง | รัน Test Execution ดูผลลัพธ์ Node 0 | +| 2 | Database Connect ได้ | Test Step ใน Node Read Checkpoint | +| 3 | Ollama พร้อม | `curl http:///api/tags` | +| 4 | Backend Token ใช้ได้ | Test Step ใน Node Fetch Categories | +| 5 | File Mount RO ถูกต้อง | `docker exec n8n ls /home/node/.n8n-files/staging_ai` | +| 6 | Log Mount RW ถูกต้อง | `docker exec n8n touch /home/node/.n8n-files/migration_logs/test` | +| 7 | Categories ไม่ hardcode | ดูผลลัพธ์ Node Fetch Categories | +| 8 | Idempotency Key ถูกต้อง | ตรวจ Header ใน Node Import | +| 9 | Checkpoint บันทึก | ตรวจสอบ `migration_progress` หลังรัน | +| 10 | Error Log สร้างไฟล์ | ตรวจสอบ `error_log.csv` | + +--- + +## 🔧 การแก้ไขปัญหาเฉพาะหน้า + +### ปัญหา: Config ไม่ถูกต้อง +**แก้ไข:** แก้ที่ Node "Set Configuration" แล้ว Save → Execute Workflow ใหม่ + +### ปัญหา: Database Connection Error +**ตรวจสอบ:** +```javascript +// ใส่ใน Code Node ชั่วคราวเพื่อ Debug +const config = $workflow.staticData.config; +return [{ json: { + host: config.DB_HOST, + port: config.DB_PORT, + // อย่าแสดง password ใน Production! + test: 'Config loaded: ' + (config ? 'YES' : 'NO') +}}]; +``` + +### ปัญหา: Ollama Timeout +**แก้ไข:** +- เพิ่ม `DELAY_MS` เป็น 3000 หรือ 5000 +- ลด `BATCH_SIZE` เหลือ 5 +- ตรวจสอบ GPU/CPU ของ Ollama Server + +--- + +## 📊 การ Monitor (Manual) + +เนื่องจาก Free Plan ไม่มี Advanced Monitoring: + +```bash +# ดู Progress ล่าสุด +docker exec n8n sh -c "tail -5 /home/node/.n8n-files/migration_logs/reject_log.csv" + +# ดู Error ล่าสุด +docker exec n8n sh -c "tail -10 /home/node/.n8n-files/migration_logs/error_log.csv" + +# ดู Checkpoint ใน DB +mysql -h -u migration_bot -p -e "SELECT * FROM migration_progress WHERE batch_id = 'migration_20260226'" +``` + +--- + +## 📥 วิธี Import Workflow + +1. เปิดไฟล์ `n8n.workflow` (JSON) จาก `specs/03-Data-and-Storage/` +2. เข้า n8n UI → **Workflows** → **Import from File** +3. เลือกไฟล์ `n8n.workflow` +4. เปิด Workflow → แก้ไข Node **"Set Configuration"** ตามข้อมูลจริง +5. ตั้งค่า **Schedule Trigger** หรือเปลี่ยนเป็น **Manual Trigger** สำหรับทดสอบ +6. **Save** → **Execute Workflow** เพื่อทดสอบ + +--- + ## 📞 การติดต่อสนับสนุน | ปัญหา | ช่องทางติดต่อ | @@ -943,5 +444,5 @@ curl -X POST http://:5678/webhook/rollback \ --- -**เอกสารฉบับนี้จัดทำขึ้นเพื่อรองรับ Migration ตาม ADR-017 และ 03-04** -**Version:** 1.8.0 | **Last Updated:** 2026-02-27 | **Author:** Development Team +**เอกสารฉบับนี้จัดทำขึ้นสำหรับ n8n Free Plan (Self-hosted) ตาม ADR-017 และ 03-04** +**Version:** 1.8.0-free | **Last Updated:** 2026-03-04 | **Author:** Development Team diff --git a/specs/03-Data-and-Storage/lcbp3-v1.8.0-migration.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-migration.sql new file mode 100644 index 0000000..67e04fd --- /dev/null +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-migration.sql @@ -0,0 +1,109 @@ +-- ========================================================== +-- DMS v1.8.0 Migration Support Tables +-- ไฟล์นี้แยกจาก schema หลัก (lcbp3-v1.8.0-schema-01/02/03) +-- ใช้สำหรับ n8n Migration Workflow เท่านั้น +-- ลบได้ทั้งหมดหลัง Migration เสร็จสิ้น +-- ========================================================== +-- รันบน MariaDB **ก่อน** เริ่ม n8n Workflow +SET NAMES utf8mb4; + +-- ===================================================== +-- 1. Checkpoint — ติดตามความคืบหน้าของ Batch +-- ===================================================== +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 +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Migration: ติดตามความคืบหน้า Batch (ลบได้หลัง Migration เสร็จ)'; + +-- ===================================================== +-- 2. Review Queue — รายการที่ต้องตรวจสอบโดยคน +-- ===================================================== +CREATE TABLE IF NOT EXISTS migration_review_queue ( + id INT AUTO_INCREMENT PRIMARY KEY, + document_number VARCHAR(100) NOT NULL, + title TEXT, + original_title TEXT, + ai_suggested_category VARCHAR(50), + ai_confidence DECIMAL(4, 3), + ai_issues JSON, + review_reason VARCHAR(255), + STATUS ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING', + reviewed_by VARCHAR(100), + reviewed_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_doc_number (document_number) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Migration: Review Queue ชั่วคราว (ลบได้หลัง Migration เสร็จ)'; + +-- ===================================================== +-- 3. Error Log — บันทึก Error ระหว่าง Migration +-- ===================================================== +CREATE TABLE IF NOT EXISTS migration_errors ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id VARCHAR(50), + document_number VARCHAR(100), + error_type ENUM( + 'FILE_NOT_FOUND', + 'AI_PARSE_ERROR', + 'API_ERROR', + 'DB_ERROR', + 'SECURITY', + 'UNKNOWN' + ), + error_message TEXT, + raw_ai_response TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_batch_id (batch_id), + INDEX idx_error_type (error_type) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Migration: Error Log (ลบได้หลัง Migration เสร็จ)'; + +-- ===================================================== +-- 4. Fallback State — สถานะ AI Model Fallback +-- ===================================================== +CREATE TABLE IF NOT EXISTS migration_fallback_state ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id VARCHAR(50) UNIQUE, + recent_error_count INT DEFAULT 0, + is_fallback_active BOOLEAN DEFAULT FALSE, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Migration: Fallback Model State (ลบได้หลัง Migration เสร็จ)'; + +-- ===================================================== +-- 5. Idempotency — ป้องกัน Import ซ้ำ +-- ===================================================== +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) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Migration: Idempotency Tracking (ลบได้หลัง Migration เสร็จ)'; + +-- ===================================================== +-- 6. Daily Summary — สรุปผลรายวัน +-- ===================================================== +CREATE TABLE IF NOT EXISTS migration_daily_summary ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id VARCHAR(50), + summary_date DATE, + total_processed INT DEFAULT 0, + auto_ingested INT DEFAULT 0, + sent_to_review INT DEFAULT 0, + rejected INT DEFAULT 0, + errors INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_batch_date (batch_id, summary_date) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Migration: Daily Summary (ลบได้หลัง Migration เสร็จ)'; + +-- ===================================================== +-- Cleanup Script (รันหลัง Migration เสร็จสิ้นทั้งหมด) +-- ===================================================== +-- DROP TABLE IF EXISTS migration_daily_summary; +-- DROP TABLE IF EXISTS import_transactions; +-- DROP TABLE IF EXISTS migration_fallback_state; +-- DROP TABLE IF EXISTS migration_errors; +-- DROP TABLE IF EXISTS migration_review_queue; +-- DROP TABLE IF EXISTS migration_progress; diff --git a/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-01-drop.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-01-drop.sql new file mode 100644 index 0000000..5e84ed9 --- /dev/null +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-01-drop.sql @@ -0,0 +1,215 @@ +-- ========================================================== +-- DMS v1.8.0 Schema Part 1/3: DROP Statements +-- รันไฟล์นี้ก่อน เพื่อล้างตารางเดิมทั้งหมด +-- ========================================================== +-- ========================================================== +-- DMS v1.8.0 Document Management System Database +-- Deploy Script Schema +-- Server: Container Station on QNAP TS-473A +-- Database service: MariaDB 11.8 +-- database web ui: phpmyadmin 5-apache +-- database development ui: DBeaver +-- backend service: NestJS +-- frontend service: next.js +-- reverse proxy: jc21/nginx-proxy-manager:latest +-- cron service: n8n +-- ========================================================== +-- [v1.8.0 UPDATE] Prepare migration +-- Update: Upgraded from v1.7.0 +-- Last Updated: 2026-02-27 +-- Major Changes: +-- 1. ปรับปรุง: +-- 1.1 TABLE correspondences +-- - INDEX idx_doc_number (correspondence_number), +-- - INDEX idx_deleted_at (deleted_at), +-- - INDEX idx_created_by (created_by), +-- ========================================================== +SET NAMES utf8mb4; + +SET time_zone = '+07:00'; + +-- ปิดการตรวจสอบ Foreign Key ชั่วคราวเพื่อให้สามารถลบตารางได้ทั้งหมด +SET FOREIGN_KEY_CHECKS = 0; + +DROP VIEW IF EXISTS v_document_statistics; + +DROP VIEW IF EXISTS v_documents_with_attachments; + +DROP VIEW IF EXISTS v_user_all_permissions; + +DROP VIEW IF EXISTS v_audit_log_details; + +DROP VIEW IF EXISTS v_user_tasks; + +DROP VIEW IF EXISTS v_contract_parties_all; + +DROP VIEW IF EXISTS v_current_rfas; + +DROP VIEW IF EXISTS v_current_correspondences; + +-- DROP PROCEDURE IF EXISTS sp_get_next_document_number; +-- 🗑️ DROP TABLE SCRIPT: LCBP3-DMS v1.4.2 +-- คำเตือน: ข้อมูลทั้งหมดจะหายไป กรุณา Backup ก่อนรันบน Production +SET FOREIGN_KEY_CHECKS = 0; + +-- ============================================================ +-- ส่วนที่ 1: ตาราง System, Logs & Preferences (ตารางปลายทาง/ส่วนเสริม) +-- ============================================================ +DROP TABLE IF EXISTS backup_logs; + +DROP TABLE IF EXISTS search_indices; + +DROP TABLE IF EXISTS notifications; + +DROP TABLE IF EXISTS audit_logs; + +-- [NEW v1.4.2] ตารางการตั้งค่าส่วนตัวของผู้ใช้ (FK -> users) +DROP TABLE IF EXISTS user_preferences; + +-- [NEW v1.4.2] ตารางเก็บ Schema สำหรับ Validate JSON (Stand-alone) +DROP TABLE IF EXISTS json_schemas; + +-- [v1.5.1 NEW] ตาราง Audit และ Error Log สำหรับ Document Numbering +DROP TABLE IF EXISTS document_number_errors; + +DROP TABLE IF EXISTS document_number_audit; + +DROP TABLE IF EXISTS document_number_reservations; + +-- ============================================================ +-- ส่วนที่ 2: ตาราง Junction (เชื่อมโยงข้อมูล M:N) +-- ============================================================ +DROP TABLE IF EXISTS correspondence_tags; + +DROP TABLE IF EXISTS asbuilt_revision_shop_revisions_refs; + +DROP TABLE IF EXISTS shop_drawing_revision_contract_refs; + +DROP TABLE IF EXISTS contract_drawing_subcat_cat_maps; + +-- ============================================================ +-- ส่วนที่ 3: ตารางไฟล์แนบและการเชื่อมโยง (Attachments) +-- ============================================================ +DROP TABLE IF EXISTS contract_drawing_attachments; + +DROP TABLE IF EXISTS circulation_attachments; + +DROP TABLE IF EXISTS shop_drawing_revision_attachments; + +DROP TABLE IF EXISTS asbuilt_drawing_revision_attachments; + +DROP TABLE IF EXISTS correspondence_attachments; + +DROP TABLE IF EXISTS attachments; + +-- ตารางหลักเก็บ path ไฟล์ +-- ============================================================ +-- ส่วนที่ 4: ตาราง Workflow & Routing (Process Logic) +-- ============================================================ +-- Correspondence Workflow +-- ============================================================ +-- ส่วนที่ 5: ตาราง Mapping สิทธิ์และโครงสร้าง (Access Control) +-- ============================================================ +DROP TABLE IF EXISTS role_permissions; + +DROP TABLE IF EXISTS user_assignments; + +DROP TABLE IF EXISTS contract_organizations; + +DROP TABLE IF EXISTS project_organizations; + +-- ============================================================ +-- ส่วนที่ 6: ตารางรายละเอียดของเอกสาร (Revisions & Items) +-- ============================================================ +DROP TABLE IF EXISTS transmittal_items; + +DROP TABLE IF EXISTS shop_drawing_revisions; + +DROP TABLE IF EXISTS asbuilt_drawing_revisions; + +DROP TABLE IF EXISTS rfa_items; + +DROP TABLE IF EXISTS rfa_revisions; + +DROP TABLE IF EXISTS correspondence_references; + +DROP TABLE IF EXISTS correspondence_recipients; + +DROP TABLE IF EXISTS correspondence_revisions; + +-- [Modified v1.4.2] มี Virtual Columns +-- ============================================================ +-- ส่วนที่ 7: ตารางเอกสารหลัก (Core Documents) +-- ============================================================ +DROP TABLE IF EXISTS circulations; + +DROP TABLE IF EXISTS transmittals; + +DROP TABLE IF EXISTS contract_drawings; + +DROP TABLE IF EXISTS shop_drawings; + +DROP TABLE IF EXISTS asbuilt_drawings; + +DROP TABLE IF EXISTS rfas; + +DROP TABLE IF EXISTS correspondences; + +-- ============================================================ +-- ส่วนที่ 8: ตารางหมวดหมู่และข้อมูลหลัก (Master Data) +-- ============================================================ +-- [NEW 6B] ลบตารางใหม่ที่เพิ่มเข้ามาเพื่อป้องกัน Error เวลา Re-deploy +DROP TABLE IF EXISTS correspondence_sub_types; + +DROP TABLE IF EXISTS disciplines; + +DROP TABLE IF EXISTS shop_drawing_sub_categories; + +DROP TABLE IF EXISTS shop_drawing_main_categories; + +DROP TABLE IF EXISTS contract_drawing_sub_cats; + +DROP TABLE IF EXISTS contract_drawing_cats; + +DROP TABLE IF EXISTS contract_drawing_volumes; + +DROP TABLE IF EXISTS circulation_status_codes; + +DROP TABLE IF EXISTS rfa_approve_codes; + +DROP TABLE IF EXISTS rfa_status_codes; + +DROP TABLE IF EXISTS rfa_types; + +DROP TABLE IF EXISTS correspondence_status; + +DROP TABLE IF EXISTS correspondence_types; + +DROP TABLE IF EXISTS document_number_counters; + +-- [Modified v1.4.2] มี version column +DROP TABLE IF EXISTS document_number_formats; + +DROP TABLE IF EXISTS tags; + +-- ============================================================ +-- ส่วนที่ 9: ตารางผู้ใช้ บทบาท และโครงสร้างรากฐาน (Root Tables) +-- ============================================================ +DROP TABLE IF EXISTS organization_roles; + +DROP TABLE IF EXISTS roles; + +DROP TABLE IF EXISTS permissions; + +DROP TABLE IF EXISTS contracts; + +DROP TABLE IF EXISTS projects; + +DROP TABLE IF EXISTS refresh_tokens; + +DROP TABLE IF EXISTS users; + +-- Referenced by user_preferences, audit_logs, etc. +DROP TABLE IF EXISTS organizations; + +-- Referenced by users, projects, etc. diff --git a/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql new file mode 100644 index 0000000..4638909 --- /dev/null +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql @@ -0,0 +1,1344 @@ +-- ========================================================== +-- DMS v1.8.0 Schema Part 2/3: CREATE TABLE Statements +-- รัน: mysql < 01-schema-drop.sql แล้วจึงรัน 02-schema-tables.sql +-- ========================================================== +SET NAMES utf8mb4; +SET time_zone = '+07:00'; +SET FOREIGN_KEY_CHECKS = 0; + +-- ===================================================== +-- 1. 🏢 Core & Master Data (องค์กร, โครงการ, สัญญา) +-- ===================================================== +-- ตาราง Master เก็บประเภทบทบาทขององค์กร +CREATE TABLE organization_roles ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + role_name VARCHAR(20) NOT NULL UNIQUE COMMENT 'ชื่อบทบาท (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD PARTY)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บประเภทบทบาทขององค์กร'; + +-- ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ +CREATE TABLE organizations ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + organization_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสองค์กร', + organization_name VARCHAR(255) NOT NULL COMMENT 'ชื่อองค์กร', + role_id INT COMMENT 'บทบาทขององค์กร', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)', + FOREIGN KEY (role_id) REFERENCES organization_roles (id) ON DELETE + SET NULL +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ'; + +-- ตาราง Master เก็บข้อมูลโครงการ +CREATE TABLE projects ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสโครงการ', + project_name VARCHAR(255) NOT NULL COMMENT 'ชื่อโครงการ', + -- parent_project_id INT COMMENT 'รหัสโครงการหลัก (ถ้ามี)', + -- contractor_organization_id INT COMMENT 'รหัสองค์กรผู้รับเหมา (ถ้ามี)', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + -- FOREIGN KEY (parent_project_id) REFERENCES projects(id) ON DELETE SET NULL, + -- FOREIGN KEY (contractor_organization_id) REFERENCES organizations(id) ON DELETE SET NULL + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลโครงการ'; + +-- ตาราง Master เก็บข้อมูลสัญญา +CREATE TABLE contracts ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL, + contract_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสัญญา', + contract_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสัญญา', + description TEXT COMMENT 'คำอธิบายสัญญา', + start_date DATE COMMENT 'วันที่เริ่มสัญญา', + end_date DATE COMMENT 'วันที่สิ้นสุดสัญญา', + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลสัญญา'; + +-- ===================================================== +-- 2. 👥 Users & RBAC (ผู้ใช้, สิทธิ์, บทบาท) +-- ===================================================== +-- ตาราง Master เก็บข้อมูลผู้ใช้งาน (User) +CREATE TABLE users ( + user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + username VARCHAR(50) NOT NULL UNIQUE COMMENT 'ชื่อผู้ใช้งาน', + password_hash VARCHAR(255) NOT NULL COMMENT 'รหัสผ่าน (Hashed)', + first_name VARCHAR(50) COMMENT 'ชื่อจริง', + last_name VARCHAR(50) COMMENT 'นามสกุล', + email VARCHAR(100) NOT NULL UNIQUE COMMENT 'อีเมล', + line_id VARCHAR(100) COMMENT 'LINE ID', + primary_organization_id INT COMMENT 'สังกัดองค์กร', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + failed_attempts INT DEFAULT 0 COMMENT 'จำนวนครั้งที่ล็อกอินล้มเหลว', + locked_until DATETIME COMMENT 'ล็อกอินไม่ได้จนถึงเวลา', + last_login_at TIMESTAMP NULL COMMENT 'วันที่และเวลาที่ล็อกอินล่าสุด', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL DEFAULT NULL COMMENT 'วันที่ลบ', + FOREIGN KEY (primary_organization_id) REFERENCES organizations (id) ON DELETE + SET NULL +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)'; + +-- ตารางเก็บ Refresh Tokens สำหรับ Authentication +CREATE TABLE refresh_tokens ( + token_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + user_id INT NOT NULL COMMENT 'เจ้าของ Token', + token_hash VARCHAR(255) NOT NULL COMMENT 'Hash ของ Refresh Token', + expires_at DATETIME NOT NULL COMMENT 'วันหมดอายุ', + is_revoked TINYINT(1) DEFAULT 0 COMMENT 'สถานะยกเลิก (1=Revoked)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + replaced_by_token VARCHAR(255) NULL COMMENT 'Token ใหม่ที่มาแทนที่ (Rotation)', + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บ Refresh Tokens สำหรับ Authentication'; + +-- ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ +CREATE TABLE roles ( + role_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + -- role_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสบทบาท (เช่น SUPER_ADMIN, ADMIN, EDITOR, VIEWER)', + role_name VARCHAR(100) NOT NULL COMMENT 'ชื่อบทบาท', + scope ENUM( + 'Global', + 'Organization', + 'Project', + 'Contract' + ) NOT NULL, + -- ขอบเขตของบทบาท (จากข้อ 4.3) + description TEXT COMMENT 'คำอธิบายบทบาท', + is_system BOOLEAN DEFAULT FALSE COMMENT '(1 = บทบาทของระบบ ลบไม่ได้)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ'; + +-- ตาราง Master เก็บ "สิทธิ์" (Permission) หรือ "การกระทำ" ทั้งหมดในระบบ +CREATE TABLE permissions ( + permission_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + permission_name VARCHAR(100) NOT NULL UNIQUE COMMENT 'รหัสสิทธิ์ (เช่น rfas.create, rfas.view)', + description TEXT COMMENT 'คำอธิบายสิทธิ์', + module VARCHAR(50) COMMENT 'โมดูลที่เกี่ยวข้อง', + scope_level ENUM('GLOBAL', 'ORG', 'PROJECT') COMMENT 'ระดับขอบเขตของสิทธิ์', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "สิทธิ์" (Permission) หรือ "การกระทำ" ทั้งหมดในระบบ'; + +-- ตารางเชื่อมระหว่าง roles และ permissions (M:N) +CREATE TABLE role_permissions ( + role_id INT COMMENT 'ID ของบทบาท', + permission_id INT COMMENT 'ID ของสิทธิ์', + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles (role_id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions (permission_id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง roles และ permissions (M :N)'; + +-- search.advanced +-- ตารางเชื่อมผู้ใช้ (users) +CREATE TABLE user_assignments ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + role_id INT NOT NULL, + -- คอลัมน์สำหรับกำหนดขอบเขต (จะใช้เพียงอันเดียวต่อแถว) + organization_id INT NULL, + project_id INT NULL, + contract_id INT NULL, + assigned_by_user_id INT, + -- ผู้ที่มอบหมายบทบาทนี้ + assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles (role_id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE, + FOREIGN KEY (assigned_by_user_id) REFERENCES users (user_id), + -- Constraint เพื่อให้แน่ใจว่ามีเพียงขอบเขตเดียวที่ถูกกำหนดในแต่ละแถว + CONSTRAINT chk_scope CHECK ( + ( + organization_id IS NOT NULL + AND project_id IS NULL + AND contract_id IS NULL + ) + OR ( + organization_id IS NULL + AND project_id IS NOT NULL + AND contract_id IS NULL + ) + OR ( + organization_id IS NULL + AND project_id IS NULL + AND contract_id IS NOT NULL + ) + OR ( + organization_id IS NULL + AND project_id IS NULL + AND contract_id IS NULL + ) -- สำหรับ Global scope + ) +); + +CREATE TABLE project_organizations ( + project_id INT NOT NULL, + organization_id INT NOT NULL, + PRIMARY KEY (project_id, organization_id), + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE +); + +CREATE TABLE contract_organizations ( + contract_id INT NOT NULL, + organization_id INT NOT NULL, + role_in_contract VARCHAR(100), + -- เช่น 'Owner', 'Designer', 'Consultant', 'Contractor ' + PRIMARY KEY (contract_id, organization_id), + FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE +); + +-- ===================================================== +-- 3. ✉️ Correspondences (เอกสารหลัก, Revisions) +-- ===================================================== +-- ตารางเก็บข้อมูลสาขางาน (Disciplines) แยกตามสัญญา +CREATE TABLE disciplines ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + contract_id INT NOT NULL COMMENT 'ผูกกับสัญญา', + discipline_code VARCHAR(10) NOT NULL COMMENT 'รหัสสาขา (เช่น GEN, STR)', + code_name_th VARCHAR(255) COMMENT 'ชื่อไทย', + code_name_en VARCHAR(255) COMMENT 'ชื่ออังกฤษ', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE, + UNIQUE KEY uk_discipline_contract (contract_id, discipline_code) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บข้อมูลสาขางาน (Disciplines) ตาม Req 6B'; + +-- ตาราง Master เก็บประเภทเอกสารโต้ตอบ +CREATE TABLE correspondence_types ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + type_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสประเภท (เช่น RFA, RFI)', + type_name VARCHAR(255) NOT NULL COMMENT 'ชื่อประเภท', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บประเภทเอกสารโต้ตอบ'; + +-- ตารางเก็บประเภทหนังสือย่อย (Sub Types) สำหรับ Mapping เลขรหัส +CREATE TABLE correspondence_sub_types ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + contract_id INT NOT NULL COMMENT 'ผูกกับสัญญา', + correspondence_type_id INT NOT NULL COMMENT 'ผูกกับประเภทเอกสารหลัก (เช่น RFA)', + sub_type_code VARCHAR(20) NOT NULL COMMENT 'รหัสย่อย (เช่น MAT, SHP)', + sub_type_name VARCHAR(255) COMMENT 'ชื่อประเภทหนังสือย่อย', + sub_type_number VARCHAR(10) COMMENT 'เลขรหัสสำหรับ Running Number (เช่น 11, 22)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บประเภทหนังสือย่อย (Sub Types) ตาม Req 6B'; + +-- ตาราง Master เก็บสถานะของเอกสาร +CREATE TABLE correspondence_status ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + status_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสถานะหนังสือ (เช่น DRAFT, SUBOWN)', + status_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสถานะหนังสือ', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บสถานะของเอกสาร'; + +-- ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision +CREATE TABLE correspondences ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (นี่คือ "Master ID" ที่ใช้เชื่อมโยง)', + correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร ใช้แบ่งแยกว่าเป็น RFA หรือ อื่นๆ', + correspondence_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสาร (สร้างจาก DocumentNumberingModule)', + discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)', + is_internal_communication TINYINT(1) DEFAULT 0 COMMENT '(1 = ภายใน, 0 = ภายนอก)', + project_id INT NOT NULL COMMENT 'อยู่ในโครงการ', + originator_id INT COMMENT 'องค์กรผู้ส่ง', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + created_by INT COMMENT 'ผู้สร้าง', + deleted_at DATETIME NULL COMMENT 'สำหรับ Soft Delete', + INDEX idx_corr_number (correspondence_number), + INDEX idx_deleted_at (deleted_at), + INDEX idx_created_by (created_by), + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE RESTRICT, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (originator_id) REFERENCES organizations (id) ON DELETE + SET NULL, + FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE + SET NULL, + -- Foreign Key ที่รวมเข้ามาจาก ALTER (ระบุชื่อ Constraint ตามที่ต้องการ) + CONSTRAINT fk_corr_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE + SET NULL, + UNIQUE KEY uq_corr_no_per_project (project_id, correspondence_number) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision'; + +-- ตารางเชื่อมผู้รับ (TO/CC) สำหรับเอกสารแต่ละฉบับ (M:N) +CREATE TABLE correspondence_recipients ( + correspondence_id INT COMMENT 'ID ของเอกสาร', + recipient_organization_id INT COMMENT 'ID องค์กรผู้รับ', + recipient_type ENUM('TO', 'CC ') COMMENT 'ประเภทผู้รับ (TO หรือ CC)', + PRIMARY KEY ( + correspondence_id, + recipient_organization_id, + recipient_type + ), + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (recipient_organization_id) REFERENCES organizations (id) ON DELETE RESTRICT +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมผู้รับ (TO / CC) สำหรับเอกสารแต่ละฉบับ (M :N)'; + +-- ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1:N) +CREATE TABLE correspondence_revisions ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + correspondence_id INT NOT NULL COMMENT 'Master ID', + revision_number INT NOT NULL COMMENT 'หมายเลข Revision (0, 1, 2...)', + revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)', + is_current BOOLEAN DEFAULT FALSE COMMENT '(1 = Revision ปัจจุบัน)', + -- ข้อมูลเนื้อหาที่เปลี่ยนได้ + correspondence_status_id INT NOT NULL COMMENT 'สถานะของ Revision นี้', + subject VARCHAR(500) NOT NULL COMMENT 'หัวข้อเรื่อง', + description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้', + body TEXT NULL COMMENT 'เนื้อความ (ถ้ามี)', + remarks TEXT COMMENT 'หมายเหตุ', + document_date DATE COMMENT 'วันที่ในเอกสาร', + issued_date DATETIME COMMENT 'วันที่ออกเอกสาร', + received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร', + due_date DATETIME COMMENT 'วันที่ครบกำหนด', + -- Standard Meta Columns + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร', + created_by INT COMMENT 'ผู้สร้าง', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + -- ส่วนของ JSON และ Schema Version + details JSON COMMENT 'ข้อมูลเฉพาะ (เช่น LETTET details)', + schema_version INT DEFAULT 1 COMMENT 'เวอร์ชันของ Schema ที่ใช้กับ details', + -- Generated Virtual Columns (ดึงค่าจาก JSON โดยอัตโนมัติ) + v_ref_project_id INT GENERATED ALWAYS AS ( + CAST( + JSON_UNQUOTE(JSON_EXTRACT(details, '$.projectId')) AS UNSIGNED + ) + ) VIRTUAL COMMENT 'Virtual Column: Project ID จาก JSON', + v_doc_subtype VARCHAR(50) GENERATED ALWAYS AS ( + JSON_UNQUOTE(JSON_EXTRACT(details, '$.subType')) + ) VIRTUAL COMMENT 'Virtual Column: Document Subtype จาก JSON', + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_status_id) REFERENCES correspondence_status (id) ON DELETE RESTRICT, + FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE + SET NULL, + FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE + SET NULL, + UNIQUE KEY uq_master_revision_number (correspondence_id, revision_number), + UNIQUE KEY uq_master_current (correspondence_id, is_current), + INDEX idx_corr_rev_v_project (v_ref_project_id), + INDEX idx_corr_rev_v_subtype (v_doc_subtype) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1 :N)'; + +-- ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ +CREATE TABLE tags ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + tag_name VARCHAR(100) NOT NULL UNIQUE COMMENT 'ชื่อ Tag', + description TEXT COMMENT 'คำอธิบายแท็ก', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ'; + +-- ตารางเชื่อมระหว่าง correspondences และ tags (M:N) +CREATE TABLE correspondence_tags ( + correspondence_id INT COMMENT 'ID ของเอกสาร', + tag_id INT COMMENT 'ID ของ Tag', + PRIMARY KEY (correspondence_id, tag_id), + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง correspondences และ tags (M :N)'; + +-- ตารางเชื่อมการอ้างอิงระหว่างเอกสาร (M:N) +CREATE TABLE correspondence_references ( + src_correspondence_id INT COMMENT 'ID เอกสารต้นทาง', + tgt_correspondence_id INT COMMENT 'ID เอกสารเป้าหมาย', + PRIMARY KEY ( + src_correspondence_id, + tgt_correspondence_id + ), + FOREIGN KEY (src_correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (tgt_correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมการอ้างอิงระหว่างเอกสาร (M :N)'; + +-- ===================================================== +-- 4. 📐 approval: RFA (เอกสารขออนุมัติ, Workflows) +-- ===================================================== +-- ตาราง Master สำหรับประเภท RFA +CREATE TABLE rfa_types ( + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 'ID ของตาราง', + contract_id INT NOT NULL COMMENT 'ผูกกับสัญญา', + type_code VARCHAR(20) NOT NULL COMMENT 'รหัสประเภท RFA (เช่น DWG, DOC, MAT)', + type_name_th VARCHAR(100) NOT NULL COMMENT 'ชื่อประเภท RFA th', + type_name_en VARCHAR(100) NOT NULL COMMENT 'ชื่อประเภท RFA en', + remark TEXT COMMENT 'หมายเหตุ', + -- sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ', + UNIQUE KEY uk_rfa_types_contract_code (contract_id, type_code), + FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับประเภท RFA'; + +-- ตาราง Master สำหรับสถานะ RFA +CREATE TABLE rfa_status_codes ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + status_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสสถานะ RFA (เช่น DFT - Draft, FAP - For Approve)', + status_name VARCHAR(100) NOT NULL COMMENT 'ชื่อสถานะ', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับสถานะ RFA'; + +-- ตาราง Master สำหรับรหัสผลการอนุมัติ RFA +CREATE TABLE rfa_approve_codes ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + approve_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสผลการอนุมัติ ( + เช่น 1A - Approved, + 3R - Revise + and Resubmit + )', + approve_name VARCHAR(100) NOT NULL COMMENT 'ชื่อผลการอนุมัติ', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับรหัสผลการอนุมัติ RFA'; + +CREATE TABLE rfas ( + id INT PRIMARY KEY COMMENT 'ID ของตาราง (RFA Master ID)', + -- ❌ ไม่มี AUTO_INCREMENT + rfa_type_id INT NOT NULL COMMENT 'ประเภท RFA', + -- discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + created_by INT COMMENT 'ผู้สร้าง', + deleted_at DATETIME NULL COMMENT 'สำหรับ Soft Delete', + FOREIGN KEY (rfa_type_id) REFERENCES rfa_types (id), + FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE + SET NULL, + -- CONSTRAINT fk_rfa_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE SET NULL, + CONSTRAINT fk_rfas_parent FOREIGN KEY (id) REFERENCES correspondences (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของ RFA (มีความสัมพันธ์ 1 :N กับ rfa_revisions)'; + +-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ rfas (1:N) +CREATE TABLE rfa_revisions ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + rfa_id INT NOT NULL COMMENT 'Master ID ของ RFA', + revision_number INT NOT NULL COMMENT 'หมายเลข Revision (0, 1, 2...)', + revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)', + is_current BOOLEAN DEFAULT FALSE COMMENT '(1 = Revision ปัจจุบัน)', + -- ข้อมูลเฉพาะของ RFA Revision ที่ซับซ้อน + rfa_status_code_id INT NOT NULL COMMENT 'สถานะ RFA', + rfa_approve_code_id INT COMMENT 'ผลการอนุมัติ', + subject VARCHAR(500) NOT NULL COMMENT 'หัวข้อเรื่อง', + description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้', + body TEXT NULL COMMENT 'เนื้อความ (ถ้ามี)', + remarks TEXT COMMENT 'หมายเหตุ', + document_date DATE COMMENT 'วันที่ในเอกสาร', + issued_date DATE COMMENT 'วันที่ส่งขออนุมัติ', + received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร', + due_date DATETIME COMMENT 'วันที่ครบกำหนด', + approved_date DATE COMMENT 'วันที่อนุมัติ', + -- Standard Meta Columns + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร', + created_by INT COMMENT 'ผู้สร้าง', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + -- ส่วนของ JSON และ Schema Version + details JSON NULL COMMENT 'RFA Specific Details', + schema_version INT DEFAULT 1 COMMENT 'Version ของ JSON Schema', + -- Generated Virtual Columns (ดึงค่าจาก JSON โดยอัตโนมัติ) + v_ref_drawing_count INT GENERATED ALWAYS AS ( + JSON_UNQUOTE( + JSON_EXTRACT(details, '$.drawingCount') + ) + ) VIRTUAL, + FOREIGN KEY (rfa_id) REFERENCES rfas (id) ON DELETE CASCADE, + FOREIGN KEY (rfa_status_code_id) REFERENCES rfa_status_codes (id), + FOREIGN KEY (rfa_approve_code_id) REFERENCES rfa_approve_codes (id) ON DELETE + SET NULL, + FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE + SET NULL, + FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE + SET NULL, + UNIQUE KEY uq_rr_rev_number (rfa_id, revision_number), + UNIQUE KEY uq_rr_current (rfa_id, is_current) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ rfas (1 :N)'; + +-- ตารางเชื่อมระหว่าง rfa_revisions (ที่เป็นประเภท DWG) กับ shop_drawing_revisions (M:N) +CREATE TABLE rfa_items ( + rfa_revision_id INT COMMENT 'ID ของ RFA Revision', + shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', + PRIMARY KEY ( + rfa_revision_id, + shop_drawing_revision_id + ), + FOREIGN KEY (rfa_revision_id) REFERENCES rfa_revisions (id) ON DELETE CASCADE, + FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง rfa_revisions (ที่เป็นประเภท DWG) กับ shop_drawing_revisions (M :N)'; + +-- ===================================================== +-- 5. 📐 Drawings (แบบ, หมวดหมู่) +-- ===================================================== +-- ตาราง Master สำหรับ "เล่ม" ของแบบคู่สัญญา +CREATE TABLE contract_drawing_volumes ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + volume_code VARCHAR(50) NOT NULL COMMENT 'รหัสเล่ม', + volume_name VARCHAR(255) NOT NULL COMMENT 'ชื่อเล่ม', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + UNIQUE KEY ux_volume_project (project_id, volume_code) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "เล่ม" ของแบบคู่สัญญา'; + +-- ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบคู่สัญญา +CREATE TABLE contract_drawing_cats ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + cat_code VARCHAR(50) NOT NULL COMMENT 'รหัสหมวดหมู่หลัก', + cat_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่หลัก', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + UNIQUE KEY ux_cat_project (project_id, cat_code) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบคู่สัญญา'; + +-- ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบคู่สัญญา +CREATE TABLE contract_drawing_sub_cats ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + sub_cat_code VARCHAR(50) NOT NULL COMMENT 'รหัสหมวดหมู่ย่อย', + sub_cat_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่ย่อย', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + UNIQUE KEY ux_subcat_project (project_id, sub_cat_code) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบคู่สัญญา'; + +-- ตารางเชื่อมระหว่าง หมวดหมู่หลัก-ย่อย (M:N) +CREATE TABLE contract_drawing_subcat_cat_maps ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT COMMENT 'ID ของโครงการ', + sub_cat_id INT COMMENT 'ID ของหมวดหมู่ย่อย', + cat_id INT COMMENT 'ID ของหมวดหมู่หลัก', + UNIQUE KEY ux_map_unique (project_id, sub_cat_id, cat_id), + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats (id) ON DELETE CASCADE, + FOREIGN KEY (cat_id) REFERENCES contract_drawing_cats (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง หมวดหมู่หลัก - ย่อย (M :N)'; + +-- ตาราง Master เก็บข้อมูล "แบบคู่สัญญา" +CREATE TABLE contract_drawings ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + condwg_no VARCHAR(255) NOT NULL COMMENT 'เลขที่แบบสัญญา', + title VARCHAR(255) NOT NULL COMMENT 'ชื่อแบบสัญญา', + map_cat_id INT COMMENT 'หมวดหมู่ย่อย', + volume_id INT COMMENT 'เล่ม', + volume_page INT COMMENT 'หน้าที่', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (map_cat_id) REFERENCES contract_drawing_subcat_cat_maps (id) ON DELETE RESTRICT, + FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes (id) ON DELETE RESTRICT, + UNIQUE KEY ux_condwg_no_project (project_id, condwg_no) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบคู่สัญญา"'; + +-- ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบก่อสร้าง +CREATE TABLE shop_drawing_main_categories ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + main_category_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสหมวดหมู่หลัก (เช่น ARCH, STR)', + main_category_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่หลัก', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด ', + FOREIGN KEY (project_id) REFERENCES projects (id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบก่อสร้าง'; + +-- ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบก่อสร้าง +CREATE TABLE shop_drawing_sub_categories ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + sub_category_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสหมวดหมู่ย่อย (เช่น STR - COLUMN)', + sub_category_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่ย่อย', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบก่อสร้าง'; + +-- ตาราง Master เก็บข้อมูล "แบบก่อสร้าง" +CREATE TABLE shop_drawings ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + drawing_number VARCHAR(100) NOT NULL COMMENT 'เลขที่ Shop Drawing', + main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก', + sub_category_id INT NOT NULL COMMENT 'หมวดหมู่ย่อย', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id), + FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id), + FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id), + UNIQUE KEY ux_shop_dwg_no_project (project_id, drawing_number) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบก่อสร้าง"'; + +-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N) +CREATE TABLE shop_drawing_revisions ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + shop_drawing_id INT NOT NULL COMMENT 'Master ID', + revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)', + revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)', + is_current BOOLEAN DEFAULT NULL COMMENT '(TRUE = Revision ปัจจุบัน, NULL = ไม่ใช่ปัจจุบัน)', + revision_date DATE COMMENT 'วันที่ของ Revision', + title VARCHAR(500) NOT NULL COMMENT 'ชื่อแบบ', + description TEXT COMMENT 'คำอธิบายการแก้ไข', + legacy_drawing_number VARCHAR(100) NULL COMMENT 'เลขที่เดิมของ Shop Drawing', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + created_by INT COMMENT 'ผู้สร้าง', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings (id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE + SET NULL, + FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE + SET NULL, + UNIQUE KEY ux_sd_rev_drawing_revision (shop_drawing_id, revision_number), + UNIQUE KEY uq_sd_current (shop_drawing_id, is_current) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N)'; + +-- ตารางเชื่อมระหว่าง shop_drawing_revisions กับ contract_drawings (M:N) +CREATE TABLE shop_drawing_revision_contract_refs ( + shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', + contract_drawing_id INT COMMENT 'ID ของ Contract Drawing', + PRIMARY KEY ( + shop_drawing_revision_id, + contract_drawing_id + ), + FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE, + FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง shop_drawing_revisions กับ contract_drawings (M :N)'; + +-- ตาราง Master เก็บข้อมูล "AS Built" +CREATE TABLE asbuilt_drawings ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + drawing_number VARCHAR(100) NOT NULL COMMENT 'เลขที่ AS Built Drawing', + main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก', + sub_category_id INT NOT NULL COMMENT 'หมวดหมู่ย่อย', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id), + FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id), + FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id), + UNIQUE KEY ux_asbuilt_no_project (project_id, drawing_number) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบ AS Built"'; + +-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ AS Built (1:N) +CREATE TABLE asbuilt_drawing_revisions ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + asbuilt_drawing_id INT NOT NULL COMMENT 'Master ID', + revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)', + revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)', + is_current BOOLEAN DEFAULT NULL COMMENT '(TRUE = Revision ปัจจุบัน, NULL = ไม่ใช่ปัจจุบัน)', + revision_date DATE COMMENT 'วันที่ของ Revision', + title VARCHAR(500) NOT NULL COMMENT 'ชื่อแบบ', + description TEXT COMMENT 'คำอธิบายการแก้ไข', + legacy_drawing_number VARCHAR(100) NULL COMMENT 'เลขที่เดิมของ AS Built Drawing', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + created_by INT COMMENT 'ผู้สร้าง', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (asbuilt_drawing_id) REFERENCES asbuilt_drawings (id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE + SET NULL, + FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE + SET NULL, + UNIQUE KEY ux_asbuilt_rev_drawing_revision (asbuilt_drawing_id, revision_number), + UNIQUE KEY uq_asbuilt_current (asbuilt_drawing_id, is_current) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ asbuilt_drawings (1:N)'; + +-- ตารางเชื่อมระหว่าง asbuilt_drawing_revisions กับ shop_drawing_revisions (M:N) +CREATE TABLE asbuilt_revision_shop_revisions_refs ( + asbuilt_drawing_revision_id INT COMMENT 'ID ของ AS Built Drawing Revision', + shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', + PRIMARY KEY ( + asbuilt_drawing_revision_id, + shop_drawing_revision_id + ), + FOREIGN KEY (asbuilt_drawing_revision_id) REFERENCES asbuilt_drawing_revisions (id) ON DELETE CASCADE, + FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง asbuilt_drawing_revisions กับ shop_drawing_revisions (M :N)'; + +-- ===================================================== +-- View: Shop Drawing พร้อม Current Revision +-- ===================================================== +CREATE OR REPLACE VIEW vw_shop_drawing_current AS +SELECT sd.id, + sd.project_id, + sd.drawing_number, + sd.main_category_id, + sd.sub_category_id, + sd.created_at, + sd.updated_at, + sd.deleted_at, + sd.updated_by, + sdr.id AS revision_id, + sdr.revision_number, + sdr.revision_label, + sdr.revision_date, + sdr.title AS revision_title, + sdr.description AS revision_description, + sdr.legacy_drawing_number, + sdr.created_by AS revision_created_by, + sdr.updated_by AS revision_updated_by +FROM shop_drawings sd + LEFT JOIN shop_drawing_revisions sdr ON sd.id = sdr.shop_drawing_id + AND sdr.is_current = TRUE; + +-- ===================================================== +-- View: As Built Drawing พร้อม Current Revision +-- ===================================================== +CREATE OR REPLACE VIEW vw_asbuilt_drawing_current AS +SELECT ad.id, + ad.project_id, + ad.drawing_number, + ad.main_category_id, + ad.sub_category_id, + ad.created_at, + ad.updated_at, + ad.deleted_at, + ad.updated_by, + adr.id AS revision_id, + adr.revision_number, + adr.revision_label, + adr.revision_date, + adr.title AS revision_title, + adr.description AS revision_description, + adr.legacy_drawing_number, + adr.created_by AS revision_created_by, + adr.updated_by AS revision_updated_by +FROM asbuilt_drawings ad + LEFT JOIN asbuilt_drawing_revisions adr ON ad.id = adr.asbuilt_drawing_id + AND adr.is_current = TRUE; + +-- ===================================================== +-- 6. 🔄 Circulations (ใบเวียนภายใน) +-- ===================================================== +-- ตาราง Master เก็บสถานะใบเวียน +CREATE TABLE circulation_status_codes ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสสถานะการดำเนินงาน', + description VARCHAR(50) NOT NULL COMMENT 'คำอธิบายสถานะการดำเนินงาน', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บสถานะใบเวียน'; + +-- ตาราง "แม่" ของใบเวียนเอกสารภายใน +CREATE TABLE circulations ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตารางใบเวียน', + correspondence_id INT UNIQUE COMMENT 'ID ของเอกสาร (จากตาราง correspondences)', + organization_id INT NOT NULL COMMENT 'ID ขององค์กรณ์ที่เป็นเจ้าของใบเวียนนี้', + circulation_no VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบเวียน', + circulation_subject VARCHAR(500) NOT NULL COMMENT 'เรื่องใบเวียน', + circulation_status_code VARCHAR(20) NOT NULL COMMENT 'รหัสสถานะใบเวียน', + created_by_user_id INT NOT NULL COMMENT 'ID ของผู้สร้างใบเวียน', + submitted_at TIMESTAMP NULL COMMENT 'วันที่ส่งใบเวียน', + closed_at TIMESTAMP NULL COMMENT 'วันที่ปิดใบเวียน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id), + FOREIGN KEY (organization_id) REFERENCES organizations (id), + FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes (code), + FOREIGN KEY (created_by_user_id) REFERENCES users (user_id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของใบเวียนเอกสารภายใน'; + +-- ===================================================== +-- 7. 📤 Transmittals (เอกสารนำส่ง) +-- ===================================================== +-- ตารางข้อมูลเฉพาะของเอกสารนำส่ง (เป็นตารางลูก 1:1 ของ correspondences) +CREATE TABLE transmittals ( + correspondence_id INT PRIMARY KEY COMMENT 'ID ของเอกสาร', + purpose ENUM( + 'FOR_APPROVAL', + 'FOR_INFORMATION', + 'FOR_REVIEW', + 'OTHER ' + ) COMMENT 'วัตถุประสงค์', + remarks TEXT COMMENT 'หมายเหตุ', + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางข้อมูลเฉพาะของเอกสารนำส่ง (เป็นตารางลูก 1 :1 ของ correspondences)'; + +-- ตารางเชื่อมระหว่าง transmittals และเอกสารที่นำส่ง (M:N) +CREATE TABLE transmittal_items ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของรายการ', + transmittal_id INT NOT NULL COMMENT 'ID ของ Transmittal', + item_correspondence_id INT NOT NULL COMMENT 'ID ของเอกสารที่แนบไป', + quantity INT DEFAULT 1 COMMENT 'จำนวน', + remarks VARCHAR(255) COMMENT 'หมายเหตุสำหรับรายการนี้', + FOREIGN KEY (transmittal_id) REFERENCES transmittals (correspondence_id) ON DELETE CASCADE, + FOREIGN KEY (item_correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + UNIQUE KEY ux_transmittal_item ( + transmittal_id, + item_correspondence_id + ) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง transmittals และเอกสารที่นำส่ง (M :N)'; + +-- ===================================================== +-- 8. 📎 File Management (ไฟล์แนบ) +-- ===================================================== +-- ตาราง "กลาง" เก็บไฟล์แนบทั้งหมดของระบบ +-- 2.2 Attachments - Two-Phase Storage & Security +-- รองรับ: Backend Plan T2.2, Req 3.9.1 +-- เหตุผล: จัดการไฟล์ขยะ (Orphan Files) และตรวจสอบความถูกต้องไฟล์ +CREATE TABLE attachments ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของไฟล์แนบ', + original_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ดั้งเดิมตอนอัปโหลด', + stored_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ที่เก็บจริงบน Server (ป้องกันชื่อซ้ำ)', + file_path VARCHAR(500) NOT NULL COMMENT 'Path ที่เก็บไฟล์ (บน QNAP / share / dms - data /)', + mime_type VARCHAR(100) NOT NULL COMMENT 'ประเภทไฟล์ (เช่น application / pdf)', + file_size INT NOT NULL COMMENT 'ขนาดไฟล์ (bytes)', + is_temporary BOOLEAN DEFAULT TRUE COMMENT 'True = ยังไม่ Commit ลง DB จริง', + temp_id VARCHAR(100) NULL COMMENT 'ID ชั่วคราวสำหรับอ้างอิงตอน Upload Phase 1', + uploaded_by_user_id INT NOT NULL COMMENT 'ผู้อัปโหลดไฟล์', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่อัปโหลด', + expires_at DATETIME NULL COMMENT 'เวลาหมดอายุของไฟล์ Temp', + CHECKSUM VARCHAR(64) NULL COMMENT 'SHA-256 Checksum', + reference_date DATE NULL COMMENT 'Date used for folder structure (e.g. Issue Date) to prevent broken paths', + FOREIGN KEY (uploaded_by_user_id) REFERENCES users (user_id) ON DELETE CASCADE, + INDEX idx_attachments_reference_date (reference_date) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "กลาง" เก็บไฟล์แนบทั้งหมดของระบบ'; + +-- ตารางเชื่อม correspondences กับ attachments (M:N) +CREATE TABLE correspondence_attachments ( + correspondence_id INT COMMENT 'ID ของเอกสาร', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + PRIMARY KEY (correspondence_id, attachment_id), + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม correspondences กับ attachments (M :N)'; + +-- ตารางเชื่อม circulations กับ attachments (M:N) +CREATE TABLE circulation_attachments ( + circulation_id INT COMMENT 'ID ของใบเวียน', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลักของใบเวียน)', + PRIMARY KEY (circulation_id, attachment_id), + FOREIGN KEY (circulation_id) REFERENCES circulations (id) ON DELETE CASCADE, + FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม circulations กับ attachments (M :N)'; + +-- ตารางเชื่อม shop_drawing_revisions กับ attachments (M:N) +CREATE TABLE asbuilt_drawing_revision_attachments ( + asbuilt_drawing_revision_id INT COMMENT 'ID ของ asbuilt Drawing Revision', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + file_type ENUM( + 'PDF', + 'DWG', + 'SOURCE', + 'OTHER ' + ) COMMENT 'ประเภทไฟล์', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + PRIMARY KEY ( + asbuilt_drawing_revision_id, + attachment_id + ), + FOREIGN KEY (asbuilt_drawing_revision_id) REFERENCES asbuilt_drawing_revisions (id) ON DELETE CASCADE, + FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม asbuilt_drawing_revisions กับ attachments (M :N)'; + +-- ตารางเชื่อม shop_drawing_revisions กับ attachments (M:N) +CREATE TABLE shop_drawing_revision_attachments ( + shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + file_type ENUM( + 'PDF', + 'DWG', + 'SOURCE', + 'OTHER ' + ) COMMENT 'ประเภทไฟล์', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + PRIMARY KEY ( + shop_drawing_revision_id, + attachment_id + ), + FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE, + FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม shop_drawing_revisions กับ attachments (M :N)'; + +-- ตารางเชื่อม contract_drawings กับ attachments (M:N) +CREATE TABLE contract_drawing_attachments ( + contract_drawing_id INT COMMENT 'ID ของ Contract Drawing', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + file_type ENUM( + 'PDF', + 'DWG', + 'SOURCE', + 'OTHER ' + ) COMMENT 'ประเภทไฟล์', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + PRIMARY KEY ( + contract_drawing_id, + attachment_id + ), + FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings (id) ON DELETE CASCADE, + FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม contract_drawings กับ attachments (M :N)'; + +-- ===================================================== +-- 9. 🔢 Document Numbering (การสร้างเลขที่เอกสาร) +-- ===================================================== +-- ตาราง Master เก็บ "รูปแบบ" Template ของเลขที่เอกสาร +CREATE TABLE document_number_formats ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + correspondence_type_id INT NULL COMMENT 'ประเภทเอกสาร', + discipline_id INT DEFAULT 0 COMMENT 'สาขางาน (0 = ทุกสาขา/ไม่ระบุ)', + format_string VARCHAR(100) NOT NULL COMMENT 'Format pattern (e.g., {ORG}-{TYPE}-{YYYY}-#)', + description TEXT COMMENT 'Format description', + reset_annually BOOLEAN DEFAULT TRUE COMMENT 'เริ่มนับใหม่ทุกปี', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE CASCADE, + UNIQUE KEY unique_format ( + project_id, + correspondence_type_id + ) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "รูปแบบ" Template ของเลขที่เอกสาร'; + +-- ========================================================== +-- [v1.5.1 UPDATE] ตารางเก็บ "ตัวนับ" (Running Number) ล่าสุด +-- เปลี่ยนแปลงหลัก: +-- - PRIMARY KEY: เปลี่ยนจาก 5 คอลัมน์เป็น 8 คอลัมน์ +-- - เพิ่มคอลัมน์: recipient_organization_id, sub_type_id, rfa_type_id +-- - เพิ่ม INDEXES สำหรับ performance +-- - เพิ่ม CONSTRAINTS สำหรับ data validation +-- เหตุผล: รองรับ 10 token types และ granular counter management +-- รองรับ: Backend Plan T2.3, Req 3.11.5, specs v1.5.1 +-- ========================================================== +CREATE TABLE document_number_counters ( + -- [v1.5.1] Composite Primary Key Columns (8 columns total) + project_id INT NOT NULL COMMENT 'โครงการ', + correspondence_type_id INT NULL COMMENT 'ประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.) NULL = default format for project', + originator_organization_id INT NOT NULL COMMENT 'องค์กรผู้ส่ง', + -- เปลี่ยนจาก NULL เป็น DEFAULT 0 + recipient_organization_id INT NOT NULL DEFAULT 0 COMMENT '[v1.7.0] องค์กรผู้รับ 0 = no recipient (RFA)', + sub_type_id INT DEFAULT 0 COMMENT '[v1.5.1 NEW] ประเภทย่อย สำหรับ TRANSMITTAL (0 = ไม่ระบุ)', + rfa_type_id INT DEFAULT 0 COMMENT '[v1.5.1 NEW] ประเภท RFA เช่น SHD, RPT, MAT (0 = ไม่ใช่ RFA)', + discipline_id INT DEFAULT 0 COMMENT 'สาขางาน เช่น TER, STR, GEO (0 = ไม่ระบุ)', + reset_scope VARCHAR(20) NOT NULL COMMENT 'Scope of reset (PROJECT, ORGANIZATION, etc.)', + -- Counter Data + last_number INT DEFAULT 0 NOT NULL COMMENT 'เลขที่ล่าสุดที่ใช้ไปแล้ว (auto-increment)', + version INT DEFAULT 0 NOT NULL COMMENT 'Optimistic Lock Version (TypeORM @VersionColumn)', + -- Metadata + created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6), + updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + -- [v1.7.0 UPDATE] Primary Key: 5 columns -> 8 columns + PRIMARY KEY ( + project_id, + originator_organization_id, + recipient_organization_id, + correspondence_type_id, + sub_type_id, + rfa_type_id, + discipline_id, + reset_scope + ), + -- Foreign Keys + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (originator_organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE CASCADE, + -- Performance Indexes + INDEX idx_counter_lookup (project_id, correspondence_type_id, reset_scope), + INDEX idx_counter_org (originator_organization_id, reset_scope), + -- Constraints + CONSTRAINT chk_last_number_positive CHECK (last_number >= 0), + CONSTRAINT chk_reset_scope_format CHECK ( + reset_scope IN ('NONE') + OR reset_scope LIKE 'YEAR_%' + OR reset_scope LIKE 'MONTH_%' + OR reset_scope LIKE 'CONTRACT_%' + ) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บ Running Number Counters - รองรับ 8-column composite PK'; + +-- ========================================================== +-- ตารางเก็บ Audit Trail สำหรับการสร้างเลขที่เอกสาร +-- เพิ่มตาราง: document_number_audit +-- เหตุผล: บันทึกประวัติการสร้างเลขที่ รองรับ audit requirement ≥ 7 ปี +-- ========================================================== +CREATE TABLE document_number_audit ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID ของ audit record', + -- Document Info + document_id INT NULL COMMENT 'ID ของเอกสารที่สร้างเลขที่ (correspondences.id) - NULL if failed/reserved', + document_type VARCHAR(50), + document_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสารที่สร้าง (ผลลัพธ์)', + operation ENUM( + 'RESERVE', + 'CONFIRM', + 'MANUAL_OVERRIDE', + 'VOID_REPLACE', + 'CANCEL' + ) NOT NULL DEFAULT 'CONFIRM' COMMENT 'ประเภทการดำเนินการ', + STATUS ENUM( + 'RESERVED', + 'CONFIRMED', + 'CANCELLED', + 'VOID', + 'MANUAL' + ) NOT NULL DEFAULT 'RESERVED' COMMENT 'สถานะเลขที่เอกสาร', + counter_key JSON NOT NULL COMMENT 'Counter key ที่ใช้ (JSON format) - 8 fields', + reservation_token VARCHAR(36) NULL, + idempotency_key VARCHAR(128) NULL COMMENT 'Idempotency Key from request', + originator_organization_id INT NULL, + recipient_organization_id INT NULL, + template_used VARCHAR(200) NOT NULL COMMENT 'Template ที่ใช้ในการสร้าง', + old_value TEXT NULL COMMENT 'Previous value for audit', + new_value TEXT NULL COMMENT 'New value for audit', + -- User Info + user_id INT NULL COMMENT 'ผู้ขอสร้างเลขที่', + ip_address VARCHAR(45) COMMENT 'IP address ของผู้ขอ (IPv4/IPv6)', + user_agent TEXT COMMENT 'User agent string (browser info)', + is_success BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่/เวลาที่สร้าง', + -- Performance & Error Tracking + retry_count INT DEFAULT 0 COMMENT 'จำนวนครั้งที่ retry ก่อนสำเร็จ', + lock_wait_ms INT COMMENT 'เวลารอ Redis lock (milliseconds)', + total_duration_ms INT COMMENT 'เวลารวมทั้งหมดในการสร้าง (milliseconds)', + fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE' COMMENT 'Fallback strategy ที่ถูกใช้ (NONE=normal, DB_LOCK=Redis down, RETRY=conflict)', + metadata JSON COMMENT 'Additional context data', + -- Indexes for performance + INDEX idx_document_id (document_id), + INDEX idx_user_id (user_id), + INDEX idx_status (STATUS), + INDEX idx_operation (operation), + INDEX idx_document_number (document_number), + INDEX idx_reservation_token (reservation_token), + INDEX idx_idempotency_key (idempotency_key), + INDEX idx_created_at (created_at), + -- Foreign Keys + FOREIGN KEY (document_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '[v1.5.1 NEW] Audit Trail สำหรับการสร้างเลขที่เอกสาร - เก็บ ≥ 7 ปี'; + +-- ========================================================== +-- [v1.5.1 NEW] ตารางเก็บ Error Logs สำหรับ Document Numbering +-- เพิ่มตาราง: document_number_errors +-- เหตุผล: ติดตาม errors, troubleshooting, monitoring +-- รองรับ: Req 3.11.6, Ops monitoring requirements +-- ========================================================== +CREATE TABLE document_number_errors ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID ของ error record', + -- Error Classification + error_type ENUM( + 'LOCK_TIMEOUT', + -- Redis lock timeout + 'VERSION_CONFLICT', + -- Optimistic lock version mismatch + 'DB_ERROR', + -- Database connection/query error + 'REDIS_ERROR', + -- Redis connection error + 'VALIDATION_ERROR' -- Template/input validation error + ) NOT NULL COMMENT 'ประเภท error (5 types)', + -- Error Details + error_message TEXT COMMENT 'ข้อความ error (stack top)', + stack_trace TEXT COMMENT 'Stack trace แบบเต็ม (สำหรับ debugging)', + context_data JSON COMMENT 'Context ของ request (user, project, counter_key, etc.)', + -- User Info + user_id INT COMMENT 'ผู้ที่เกิด error', + ip_address VARCHAR(45) COMMENT 'IP address', + -- Timestamps + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่เกิด error', + resolved_at TIMESTAMP NULL COMMENT 'วันที่แก้ไขแล้ว (NULL = ยังไม่แก้)', + -- Indexes for troubleshooting + INDEX idx_error_type (error_type), + INDEX idx_created_at (created_at), + INDEX idx_user_id (user_id), + INDEX idx_unresolved (resolved_at) -- Find unresolved errors +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '[v1.5.1 NEW] Error Log สำหรับ Document Numbering System'; + +-- ===================================================== +CREATE TABLE document_number_reservations ( + id INT AUTO_INCREMENT PRIMARY KEY, + -- Reservation Details + token VARCHAR(36) NOT NULL UNIQUE COMMENT 'UUID v4', + document_number VARCHAR(100) NOT NULL UNIQUE, + document_number_status ENUM('RESERVED', 'CONFIRMED', 'CANCELLED', 'VOID') NOT NULL DEFAULT 'RESERVED', + -- Linkage + document_id INT NULL COMMENT 'FK to documents (NULL until confirmed)', + -- Context (for debugging) + project_id INT NOT NULL, + correspondence_type_id INT NOT NULL, + originator_organization_id INT NOT NULL, + recipient_organization_id INT DEFAULT 0, + user_id INT NOT NULL, + -- Timestamps + reserved_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6), + expires_at DATETIME(6) NOT NULL, + confirmed_at DATETIME(6) NULL, + cancelled_at DATETIME(6) NULL, + -- Audit + ip_address VARCHAR(45), + user_agent TEXT, + metadata JSON NULL COMMENT 'Additional context', + -- Indexes + INDEX idx_token (token), + INDEX idx_status (document_number_status), + INDEX idx_status_expires (document_number_status, expires_at), + INDEX idx_document_id (document_id), + INDEX idx_user_id (user_id), + INDEX idx_reserved_at (reserved_at), + -- Foreign Keys + FOREIGN KEY (document_id) REFERENCES correspondence_revisions(id) ON DELETE + SET NULL, + FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Document Number Reservations - Two-Phase Commit'; + +-- ===================================================== +-- 10. ⚙️ System & Logs (ระบบและ Log) +-- ===================================================== +-- 1.1 JSON Schemas Registry +-- รองรับ: Backend Plan T2.5.1, Req 6.11.1 +-- เหตุผล: เพื่อ Validate โครงสร้าง JSON Details ของเอกสารแต่ละประเภทแบบ Centralized +CREATE TABLE json_schemas ( + id INT AUTO_INCREMENT PRIMARY KEY, + schema_code VARCHAR(100) NOT NULL COMMENT 'รหัส Schema (เช่น RFA_DWG)', + version INT NOT NULL DEFAULT 1 COMMENT 'เวอร์ชันของ Schema', + table_name VARCHAR(100) NOT NULL COMMENT 'ชื่อตารางเป้าหมาย (เช่น rfa_revisions)', + schema_definition JSON NOT NULL COMMENT 'โครงสร้าง Data Schema (AJV Standard)', + ui_schema JSON NULL COMMENT 'โครงสร้าง UI Schema สำหรับ Frontend', + virtual_columns JSON NULL COMMENT 'Config สำหรับสร้าง Virtual Columns', + migration_script JSON NULL COMMENT 'Script สำหรับแปลงข้อมูลจากเวอร์ชันก่อนหน้า', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + -- ป้องกัน Schema Code ซ้ำกันใน Version เดียวกัน + UNIQUE KEY uk_schema_version (schema_code, version) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บ JSON Schema และ Configuration'; + +-- 1.2 User Preferences +-- รองรับ: Req 5.5, 6.8.3 +-- เหตุผล: แยกการตั้งค่า Notification และ UI ออกจากตาราง Users หลัก +CREATE TABLE user_preferences ( + user_id INT PRIMARY KEY, + notify_email BOOLEAN DEFAULT TRUE, + notify_line BOOLEAN DEFAULT TRUE, + digest_mode BOOLEAN DEFAULT FALSE COMMENT 'รับแจ้งเตือนแบบรวม (Digest) แทน Real - time', + ui_theme VARCHAR(20) DEFAULT 'light', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_user_prefs_user FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ตารางเก็บบันทึกการกระทำของผู้ใช้ +-- 4.1 Audit Logs Enhancements +-- รองรับ: Req 6.1 +-- เหตุผล: รองรับ Distributed Tracing และระบุความรุนแรง +CREATE TABLE audit_logs ( + audit_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID ของ Log', + request_id VARCHAR(100) NULL COMMENT 'Trace ID linking to app logs', + user_id INT COMMENT 'ผู้กระทำ', + ACTION VARCHAR(100) NOT NULL COMMENT 'การกระทำ ( + เช่น rfa.create, + correspondence.update, + login.success + )', + severity ENUM( + 'INFO', + 'WARN', + 'ERROR', + 'CRITICAL ' + ) DEFAULT 'INFO', + entity_type VARCHAR(50) COMMENT 'ตาราง / โมดูล (เช่น ''rfa '', ''correspondence '')', + entity_id VARCHAR(50) COMMENT 'Primary ID ของระเบียนที่ได้รับผลกระทำ', + details_json JSON COMMENT 'ข้อมูลบริบท', + ip_address VARCHAR(45) COMMENT 'IP Address', + user_agent VARCHAR(255) COMMENT 'User Agent', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาที่กระทำ', + -- [แก้ไข] รวม created_at เข้ามาใน Primary Key เพื่อรองรับ Partition + PRIMARY KEY (audit_id, created_at), + -- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign Key เพื่อไม่ให้ติดข้อจำกัดของ Partition Table + INDEX idx_audit_user (user_id), + INDEX idx_audit_action (ACTION), + INDEX idx_audit_entity (entity_type, entity_id), + INDEX idx_audit_created (created_at) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บบันทึกการกระทำของผู้ใช้' -- [เพิ่ม] คำสั่ง Partition +PARTITION BY RANGE (YEAR(created_at)) ( + PARTITION p_old + VALUES LESS THAN (2024), + PARTITION p2024 + VALUES LESS THAN (2025), + PARTITION p2025 + VALUES LESS THAN (2026), + PARTITION p2026 + VALUES LESS THAN (2027), + PARTITION p2027 + VALUES LESS THAN (2028), + PARTITION p2028 + VALUES LESS THAN (2029), + PARTITION p2029 + VALUES LESS THAN (2030), + PARTITION p2030 + VALUES LESS THAN (2031), + PARTITION p_future + VALUES LESS THAN MAXVALUE +); + +-- ตารางสำหรับจัดการการแจ้งเตือน (Email/Line/System) +CREATE TABLE notifications ( + id INT NOT NULL AUTO_INCREMENT COMMENT 'ID ของการแจ้งเตือน', + user_id INT NOT NULL COMMENT 'ID ผู้ใช้', + title VARCHAR(255) NOT NULL COMMENT 'หัวข้อการแจ้งเตือน', + message TEXT NOT NULL COMMENT 'รายละเอียดการแจ้งเตือน', + notification_type ENUM('EMAIL', 'LINE', 'SYSTEM ') NOT NULL COMMENT 'ประเภท (EMAIL, LINE, SYSTEM)', + is_read BOOLEAN DEFAULT FALSE COMMENT 'สถานะการอ่าน', + entity_type VARCHAR(50) COMMENT 'เช่น ''rfa '', + ''circulation ''', + entity_id INT COMMENT 'ID ของเอนทิตีที่เกี่ยวข้อง', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + -- [แก้ไข] รวม created_at เข้ามาใน Primary Key + PRIMARY KEY (id, created_at), + -- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign Key + INDEX idx_notif_user (user_id), + INDEX idx_notif_type (notification_type), + INDEX idx_notif_read (is_read), + INDEX idx_notif_created (created_at) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการการแจ้งเตือน (Email / Line / System)' -- [เพิ่ม] คำสั่ง Partition +PARTITION BY RANGE (YEAR(created_at)) ( + PARTITION p_old + VALUES LESS THAN (2024), + PARTITION p2024 + VALUES LESS THAN (2025), + PARTITION p2025 + VALUES LESS THAN (2026), + PARTITION p2026 + VALUES LESS THAN (2027), + PARTITION p2027 + VALUES LESS THAN (2028), + PARTITION p2028 + VALUES LESS THAN (2029), + PARTITION p2029 + VALUES LESS THAN (2030), + PARTITION p2030 + VALUES LESS THAN (2031), + PARTITION p_future + VALUES LESS THAN MAXVALUE +); + +-- ตารางสำหรับจัดการดัชนีการค้นหาขั้นสูง (Full-text Search) +CREATE TABLE search_indices ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของดัชนี', + entity_type VARCHAR(50) NOT NULL COMMENT 'ชนิดเอนทิตี (เช่น ''correspondence '', ''rfa '')', + entity_id INT NOT NULL COMMENT 'ID ของเอนทิตี', + content TEXT NOT NULL COMMENT 'เนื้อหาที่จะค้นหา', + indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง / อัปเดตัชนี ' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการดัชนีการค้นหาขั้นสูง (Full - text Search)'; + +-- ตารางสำหรับบันทึกประวัติการสำรองข้อมูล +CREATE TABLE backup_logs ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของการสำรอง', + backup_type ENUM('DATABASE', 'FILES', 'FULL') NOT NULL COMMENT 'ประเภท (DATABASE, FILES, FULL)', + backup_path VARCHAR(500) NOT NULL COMMENT 'ตำแหน่งไฟล์สำรอง', + file_size BIGINT COMMENT 'ขนาดไฟล์', + STATUS ENUM( + 'STARTED', + 'COMPLETED', + 'FAILED' + ) NOT NULL COMMENT 'สถานะ', + started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาเริ่มต้น', + completed_at TIMESTAMP NULL COMMENT 'เวลาเสร็จสิ้น', + error_message TEXT COMMENT 'ข้อความผิดพลาด (ถ้ามี)' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับบันทึกประวัติการสำรองข้อมูล'; + +-- ============================================================ +-- ส่วนที่ 11: Unified Workflow Engine (Phase 6A/Phase 3) +-- ============================================================ +DROP TABLE IF EXISTS workflow_histories; + +DROP TABLE IF EXISTS workflow_instances; + +DROP TABLE IF EXISTS workflow_definitions; + +-- 1. ตารางเก็บนิยาม Workflow (Definition / DSL) +CREATE TABLE workflow_definitions ( + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID ของ Workflow Definition', + workflow_code VARCHAR(50) NOT NULL COMMENT 'รหัส Workflow เช่น RFA_FLOW_V1, CORRESPONDENCE_FLOW_V1', + version INT NOT NULL DEFAULT 1 COMMENT 'หมายเลข Version', + description TEXT NULL COMMENT 'คำอธิบาย Workflow', + dsl JSON NOT NULL COMMENT 'นิยาม Workflow ต้นฉบับ (YAML/JSON Format)', + compiled JSON NOT NULL COMMENT 'โครงสร้าง Execution Tree ที่ Compile แล้ว', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + -- ป้องกันการมี Workflow Code และ Version ซ้ำกัน + UNIQUE KEY uq_workflow_version (workflow_code, version) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บนิยามกฎการเดินเอกสาร (Workflow DSL)'; + +-- สร้าง Index สำหรับการค้นหา Workflow ที่ Active ล่าสุดได้เร็วขึ้น +CREATE INDEX idx_workflow_active ON workflow_definitions (workflow_code, is_active, version); + +-- 2. ตารางเก็บ Workflow Instance (สถานะเอกสารจริง) +CREATE TABLE workflow_instances ( + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID ของ Instance', + definition_id CHAR(36) NOT NULL COMMENT 'อ้างอิง Definition ที่ใช้', + entity_type VARCHAR(50) NOT NULL COMMENT 'ประเภทเอกสาร (rfa_revision, correspondence_revision, circulation)', + entity_id VARCHAR(50) NOT NULL COMMENT 'ID ของเอกสาร (String/Int)', + current_state VARCHAR(50) NOT NULL COMMENT 'สถานะปัจจุบัน', + STATUS ENUM( + 'ACTIVE', + 'COMPLETED', + 'CANCELLED', + 'TERMINATED' + ) DEFAULT 'ACTIVE' COMMENT 'สถานะภาพรวม', + context JSON NULL COMMENT 'ตัวแปร Context สำหรับตัดสินใจ', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_wf_inst_def FOREIGN KEY (definition_id) REFERENCES workflow_definitions (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บสถานะการเดินเรื่องของเอกสาร'; + +CREATE INDEX idx_wf_inst_entity ON workflow_instances (entity_type, entity_id); + +CREATE INDEX idx_wf_inst_state ON workflow_instances (current_state); + +-- 3. ตารางเก็บประวัติ (Audit Log / History) +CREATE TABLE workflow_histories ( + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID', + instance_id CHAR(36) NOT NULL COMMENT 'อ้างอิง Instance', + from_state VARCHAR(50) NOT NULL COMMENT 'สถานะต้นทาง', + to_state VARCHAR(50) NOT NULL COMMENT 'สถานะปลายทาง', + ACTION VARCHAR(50) NOT NULL COMMENT 'Action ที่กระทำ', + action_by_user_id INT NULL COMMENT 'User ID ผู้กระทำ', + COMMENT TEXT NULL COMMENT 'ความเห็น', + metadata JSON NULL COMMENT 'Snapshot ข้อมูล ณ ขณะนั้น', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_wf_hist_inst FOREIGN KEY (instance_id) REFERENCES workflow_instances (id) ON DELETE CASCADE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางประวัติการเปลี่ยนสถานะ Workflow'; + +CREATE INDEX idx_wf_hist_instance ON workflow_histories (instance_id); + +CREATE INDEX idx_wf_hist_user ON workflow_histories (action_by_user_id); + + diff --git a/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-03-views-indexes.sql b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-03-views-indexes.sql new file mode 100644 index 0000000..e5a1d1c --- /dev/null +++ b/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-03-views-indexes.sql @@ -0,0 +1,516 @@ +-- ========================================================== +-- DMS v1.8.0 Schema Part 3/3: Views, Indexes, Partitioning +-- รัน: หลังจาก 02-schema-tables.sql เสร็จ +-- ========================================================== +SET NAMES utf8mb4; +SET time_zone = '+07:00'; + +-- ============================================================ +-- 5. PARTITIONING PREPARATION (Advance - Optional) +-- ============================================================ +-- หมายเหตุ: การทำ Partitioning บนตารางที่มีอยู่แล้ว (audit_logs, notifications) +-- มักจะต้อง Drop Primary Key เดิม แล้วสร้างใหม่โดยรวม Partition Key (created_at) เข้าไป +-- ขั้นตอนนี้ควรทำแยกต่างหากเมื่อระบบเริ่มมีข้อมูลเยอะ หรือทำใน Maintenance Window +-- +-- ตัวอย่าง SQL สำหรับ Audit Logs (Reference Only): +-- ALTER TABLE audit_logs DROP PRIMARY KEY, ADD PRIMARY KEY (audit_id, created_at); +-- ALTER TABLE audit_logs PARTITION BY RANGE (YEAR(created_at)) ( +-- PARTITION p2024 VALUES LESS THAN (2025), +-- PARTITION p2025 VALUES LESS THAN (2026), +-- PARTITION p_future VALUES LESS THAN MAXVALUE +-- ); +-- ===================================================== +-- CREATE INDEXES +-- ===================================================== +-- Indexes for correspondences +CREATE INDEX idx_corr_type ON correspondences(correspondence_type_id); + +CREATE INDEX idx_corr_project ON correspondences(project_id); + +CREATE INDEX idx_rfa_rev_v_drawing_count ON rfa_revisions (v_ref_drawing_count); + +-- Indexes for document_number_formats +CREATE INDEX idx_document_number_formats_project ON document_number_formats (project_id); + +CREATE INDEX idx_document_number_formats_type ON document_number_formats (correspondence_type_id); + +CREATE INDEX idx_document_number_formats_project_type ON document_number_formats (project_id, correspondence_type_id); + +-- Indexes for document_number_counters +CREATE INDEX idx_document_number_counters_project ON document_number_counters (project_id); + +CREATE INDEX idx_document_number_counters_org ON document_number_counters (originator_organization_id); + +CREATE INDEX idx_document_number_counters_type ON document_number_counters (correspondence_type_id); + +-- Indexes for tags +CREATE INDEX idx_tags_name ON tags (tag_name); + +CREATE INDEX idx_tags_created_at ON tags (created_at); + +-- Indexes for correspondence_tags +CREATE INDEX idx_correspondence_tags_correspondence ON correspondence_tags (correspondence_id); + +CREATE INDEX idx_correspondence_tags_tag ON correspondence_tags (tag_id); + +-- Indexes for audit_logs +CREATE INDEX idx_audit_logs_user ON audit_logs (user_id); + +CREATE INDEX idx_audit_logs_action ON audit_logs (ACTION); + +CREATE INDEX idx_audit_logs_entity ON audit_logs (entity_type, entity_id); + +CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at); + +CREATE INDEX idx_audit_logs_ip ON audit_logs (ip_address); + +-- Indexes for notifications +CREATE INDEX idx_notifications_user ON notifications (user_id); + +CREATE INDEX idx_notifications_type ON notifications (notification_type); + +CREATE INDEX idx_notifications_read ON notifications (is_read); + +CREATE INDEX idx_notifications_entity ON notifications (entity_type, entity_id); + +CREATE INDEX idx_notifications_created_at ON notifications (created_at); + +-- Indexes for search_indices +CREATE INDEX idx_search_indices_entity ON search_indices (entity_type, entity_id); + +CREATE INDEX idx_search_indices_indexed_at ON search_indices (indexed_at); + +-- Indexes for backup_logs +CREATE INDEX idx_backup_logs_type ON backup_logs (backup_type); + +CREATE INDEX idx_backup_logs_status ON backup_logs (STATUS); + +CREATE INDEX idx_backup_logs_started_at ON backup_logs (started_at); + +CREATE INDEX idx_backup_logs_completed_at ON backup_logs (completed_at); + +-- ===================================================== +-- Additional Composite Indexes for Performance +-- ===================================================== +-- Composite index for document_number_counters for faster lookups +-- Composite index for notifications for user-specific queries +CREATE INDEX idx_notifications_user_unread ON notifications (user_id, is_read, created_at); + +-- Composite index for audit_logs for reporting +CREATE INDEX idx_audit_logs_reporting ON audit_logs (created_at, entity_type, ACTION); + +-- Composite index for search_indices for entity-based queries +CREATE INDEX idx_search_entities ON search_indices (entity_type, entity_id, indexed_at); + +-- สร้าง Index สำหรับ Cleanup Job +CREATE INDEX idx_attachments_temp_cleanup ON attachments (is_temporary, expires_at); + +CREATE INDEX idx_attachments_temp_id ON attachments (temp_id); + +CREATE INDEX idx_audit_request_id ON audit_logs (request_id); + +-- ===================================================== +-- SQL Script for LCBP3-DMS (V1.4.0) - MariaDB +-- Generated from Data Dictionary +-- ===================================================== +-- ===================================================== +-- 11. 📊 Views & Procedures (วิว และ โปรซีเดอร์) +-- ===================================================== +-- View แสดง Revision "ปัจจุบัน" ของ correspondences ทั้งหมด (ที่ไม่ใช่ RFA) +CREATE VIEW v_current_correspondences AS +SELECT c.id AS correspondence_id, + c.correspondence_number, + c.correspondence_type_id, + ct.type_code AS correspondence_type_code, + ct.type_name AS correspondence_type_name, + c.project_id, + p.project_code, + p.project_name, + c.originator_id, + org.organization_code AS originator_code, + org.organization_name AS originator_name, + cr.id AS revision_id, + cr.revision_number, + cr.revision_label, + cr.subject, + cr.document_date, + cr.issued_date, + cr.received_date, + cr.due_date, + cr.correspondence_status_id, + cs.status_code, + cs.status_name, + cr.created_by, + u.username AS created_by_username, + cr.created_at AS revision_created_at +FROM correspondences c + INNER JOIN correspondence_types ct ON c.correspondence_type_id = ct.id + INNER JOIN projects p ON c.project_id = p.id + LEFT JOIN organizations org ON c.originator_id = org.id + INNER JOIN correspondence_revisions cr ON c.id = cr.correspondence_id + INNER JOIN correspondence_status cs ON cr.correspondence_status_id = cs.id + LEFT JOIN users u ON cr.created_by = u.user_id +WHERE cr.is_current = TRUE + AND c.correspondence_type_id NOT IN( + SELECT id + FROM correspondence_types + WHERE type_code = 'RFA' + ) + AND c.deleted_at IS NULL; + +-- View แสดง Revision "ปัจจุบัน" ของ rfa_revisions ทั้งหมด +CREATE VIEW v_current_rfas AS +SELECT r.id AS rfa_id, + r.rfa_type_id, + rt.type_code AS rfa_type_code, + rt.type_name_th AS rfa_type_name_th, + rt.type_name_en AS rfa_type_name_en, + c.correspondence_number, + c.discipline_id, + -- ✅ ดึงจาก Correspondences + d.discipline_code, + -- ✅ Join เพิ่มเพื่อแสดง code + c.project_id, + p.project_code, + p.project_name, + c.originator_id, + org.organization_name AS originator_name, + rr.id AS revision_id, + rr.revision_number, + rr.revision_label, + rr.subject, + rr.document_date, + rr.issued_date, + rr.received_date, + rr.approved_date, + rr.rfa_status_code_id, + rsc.status_code AS rfa_status_code, + rsc.status_name AS rfa_status_name, + rr.rfa_approve_code_id, + rac.approve_code AS rfa_approve_code, + rac.approve_name AS rfa_approve_name, + rr.created_by, + u.username AS created_by_username, + rr.created_at AS revision_created_at +FROM rfas r + INNER JOIN rfa_types rt ON r.rfa_type_id = rt.id + INNER JOIN rfa_revisions rr ON r.id = rr.rfa_id -- RFA uses shared primary key with correspondences (1:1) + INNER JOIN correspondences c ON r.id = c.id -- [FIX 1] เพิ่มการ Join ตาราง disciplines + LEFT JOIN disciplines d ON c.discipline_id = d.id + INNER JOIN projects p ON c.project_id = p.id + INNER JOIN organizations org ON c.originator_id = org.id + INNER JOIN rfa_status_codes rsc ON rr.rfa_status_code_id = rsc.id + LEFT JOIN rfa_approve_codes rac ON rr.rfa_approve_code_id = rac.id + LEFT JOIN users u ON rr.created_by = u.user_id +WHERE rr.is_current = TRUE + AND r.deleted_at IS NULL + AND c.deleted_at IS NULL; + +-- View แสดงความสัมพันธ์ทั้งหมดระหว่าง Contract, Project, และ Organization +CREATE VIEW v_contract_parties_all AS +SELECT c.id AS contract_id, + c.contract_code, + c.contract_name, + p.id AS project_id, + p.project_code, + p.project_name, + o.id AS organization_id, + o.organization_code, + o.organization_name, + co.role_in_contract +FROM contracts c + INNER JOIN projects p ON c.project_id = p.id + INNER JOIN contract_organizations co ON c.id = co.contract_id + INNER JOIN organizations o ON co.organization_id = o.id +WHERE c.is_active = TRUE; + +-- ============================================================ +-- View: v_user_tasks (Unified Workflow Engine Edition) +-- ============================================================ +-- หน้าที่: รวมรายการงานที่ยังค้างอยู่ (Status = ACTIVE) จากทุกระบบ (RFA, Circulation, Correspondence) +-- เพื่อนำไปแสดงในหน้า Dashboard "My Tasks" +-- ============================================================ +CREATE OR REPLACE VIEW v_user_tasks AS +SELECT -- 1. Workflow Instance Info + wi.id AS instance_id, + wd.workflow_code, + wi.current_state, + wi.status AS workflow_status, + wi.created_at AS assigned_at, + -- 2. Entity Info (Polymorphic Identity) + wi.entity_type, + wi.entity_id, + -- 3. Normalized Document Info (ดึงข้อมูลจริงจากตารางลูกตามประเภท) + -- ใช้ CASE WHEN เพื่อรวมคอลัมน์ที่ชื่อต่างกันให้เป็นชื่อกลาง (document_number, subject) + CASE + WHEN wi.entity_type = 'rfa_revision' THEN rfa_corr.correspondence_number + WHEN wi.entity_type = 'circulation' THEN circ.circulation_no + WHEN wi.entity_type = 'correspondence_revision' THEN corr_corr.correspondence_number + ELSE 'N/A' + END AS document_number, + CASE + WHEN wi.entity_type = 'rfa_revision' THEN rfa_rev.subject + WHEN wi.entity_type = 'circulation' THEN circ.circulation_subject + WHEN wi.entity_type = 'correspondence_revision' THEN corr_rev.subject + ELSE 'Unknown Document' + END AS subject, + -- 4. Context Info (สำหรับ Filter สิทธิ์การมองเห็นที่ Backend) + -- ดึงเป็น JSON String เพื่อให้ Backend ไป Parse หรือใช้ JSON_CONTAINS + JSON_UNQUOTE(JSON_EXTRACT(wi.context, '$.ownerId')) AS owner_id, + JSON_EXTRACT(wi.context, '$.assigneeIds') AS assignee_ids_json +FROM workflow_instances wi + JOIN workflow_definitions wd ON wi.definition_id = wd.id -- 5. Joins for RFA (ซับซ้อนหน่อยเพราะ RFA ผูกกับ Correspondence อีกที) + LEFT JOIN rfa_revisions rfa_rev ON wi.entity_type = 'rfa_revision' + AND wi.entity_id = CAST(rfa_rev.id AS CHAR) + LEFT JOIN correspondences rfa_corr ON rfa_rev.id = rfa_corr.id -- 6. Joins for Circulation + LEFT JOIN circulations circ ON wi.entity_type = 'circulation' + AND wi.entity_id = CAST(circ.id AS CHAR) -- 7. Joins for Correspondence + LEFT JOIN correspondence_revisions corr_rev ON wi.entity_type = 'correspondence_revision' + AND wi.entity_id = CAST(corr_rev.id AS CHAR) + LEFT JOIN correspondences corr_corr ON corr_rev.correspondence_id = corr_corr.id -- 8. Filter เฉพาะงานที่ยัง Active อยู่ +WHERE wi.status = 'ACTIVE'; + +-- View แสดง audit_logs พร้อมข้อมูล username และ email ของผู้กระทำ +CREATE VIEW v_audit_log_details AS +SELECT al.audit_id, + al.user_id, + u.username, + u.email, + u.first_name, + u.last_name, + al.action, + al.entity_type, + al.entity_id, + al.details_json, + al.ip_address, + al.user_agent, + al.created_at +FROM audit_logs al + LEFT JOIN users u ON al.user_id = u.user_id; + +-- View รวมสิทธิ์ทั้งหมด (Global + Project) ของผู้ใช้ทุกคน +CREATE VIEW v_user_all_permissions AS -- Global Permissions +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + NULL AS project_id, + NULL AS contract_id, + 'GLOBAL' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Global scope +WHERE p.is_active = 1 + AND ua.organization_id IS NULL + AND ua.project_id IS NULL + AND ua.contract_id IS NULL +UNION ALL +-- Organization-specific Permissions +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + NULL AS project_id, + NULL AS contract_id, + 'ORGANIZATION' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Organization scope +WHERE p.is_active = 1 + AND ua.organization_id IS NOT NULL + AND ua.project_id IS NULL + AND ua.contract_id IS NULL +UNION ALL +-- Project-specific Permissions +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + ua.project_id, + NULL AS contract_id, + 'PROJECT' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Project scope +WHERE p.is_active = 1 + AND ua.project_id IS NOT NULL + AND ua.contract_id IS NULL +UNION ALL +-- Contract-specific Permissions +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + ua.project_id, + ua.contract_id, + 'CONTRACT' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Contract scope +WHERE p.is_active = 1 + AND ua.contract_id IS NOT NULL; + +-- ===================================================== +-- Additional Useful Views +-- ===================================================== +-- View แสดงเอกสารทั้งหมดที่มีไฟล์แนบ +CREATE VIEW v_documents_with_attachments AS +SELECT 'CORRESPONDENCE' AS document_type, + c.id AS document_id, + c.correspondence_number AS document_number, + c.project_id, + p.project_code, + p.project_name, + COUNT(ca.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM correspondences c + INNER JOIN projects p ON c.project_id = p.id + LEFT JOIN correspondence_attachments ca ON c.id = ca.correspondence_id + LEFT JOIN attachments a ON ca.attachment_id = a.id +WHERE c.deleted_at IS NULL +GROUP BY c.id, + c.correspondence_number, + c.project_id, + p.project_code, + p.project_name +UNION ALL +SELECT 'CIRCULATION' AS document_type, + circ.id AS document_id, + circ.circulation_no AS document_number, + corr.project_id, + p.project_code, + p.project_name, + COUNT(ca.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM circulations circ + INNER JOIN correspondences corr ON circ.correspondence_id = corr.id + INNER JOIN projects p ON corr.project_id = p.id + LEFT JOIN circulation_attachments ca ON circ.id = ca.circulation_id + LEFT JOIN attachments a ON ca.attachment_id = a.id +GROUP BY circ.id, + circ.circulation_no, + corr.project_id, + p.project_code, + p.project_name +UNION ALL +SELECT 'SHOP_DRAWING' AS document_type, + sdr.id AS document_id, + sd.drawing_number AS document_number, + sd.project_id, + p.project_code, + p.project_name, + COUNT(sdra.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM shop_drawing_revisions sdr + INNER JOIN shop_drawings sd ON sdr.shop_drawing_id = sd.id + INNER JOIN projects p ON sd.project_id = p.id + LEFT JOIN shop_drawing_revision_attachments sdra ON sdr.id = sdra.shop_drawing_revision_id + LEFT JOIN attachments a ON sdra.attachment_id = a.id +WHERE sd.deleted_at IS NULL +GROUP BY sdr.id, + sd.drawing_number, + sd.project_id, + p.project_code, + p.project_name +UNION ALL +SELECT 'CONTRACT_DRAWING' AS document_type, + cd.id AS document_id, + cd.condwg_no AS document_number, + cd.project_id, + p.project_code, + p.project_name, + COUNT(cda.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM contract_drawings cd + INNER JOIN projects p ON cd.project_id = p.id + LEFT JOIN contract_drawing_attachments cda ON cd.id = cda.contract_drawing_id + LEFT JOIN attachments a ON cda.attachment_id = a.id +WHERE cd.deleted_at IS NULL +GROUP BY cd.id, + cd.condwg_no, + cd.project_id, + p.project_code, + p.project_name; + +-- View แสดงสถิติเอกสารตามประเภทและสถานะ +CREATE VIEW v_document_statistics AS +SELECT p.id AS project_id, + p.project_code, + p.project_name, + ct.id AS correspondence_type_id, + ct.type_code, + ct.type_name, + cs.id AS status_id, + cs.status_code, + cs.status_name, + COUNT(DISTINCT c.id) AS document_count, + COUNT(DISTINCT cr.id) AS revision_count +FROM projects p + CROSS JOIN correspondence_types ct + CROSS JOIN correspondence_status cs + LEFT JOIN correspondences c ON p.id = c.project_id + AND ct.id = c.correspondence_type_id + LEFT JOIN correspondence_revisions cr ON c.id = cr.correspondence_id + AND cs.id = cr.correspondence_status_id + AND cr.is_current = TRUE +WHERE p.is_active = 1 + AND ct.is_active = 1 + AND cs.is_active = 1 +GROUP BY p.id, + p.project_code, + p.project_name, + ct.id, + ct.type_code, + ct.type_name, + cs.id, + cs.status_code, + cs.status_name; + +-- ===================================================== +-- Indexes for View Performance Optimization +-- ===================================================== +-- Indexes for v_current_correspondences performance +CREATE INDEX idx_correspondences_type_project ON correspondences (correspondence_type_id, project_id); + +CREATE INDEX idx_corr_revisions_current_status ON correspondence_revisions (is_current, correspondence_status_id); + +CREATE INDEX idx_corr_revisions_correspondence_current ON correspondence_revisions (correspondence_id, is_current); + +-- Indexes for v_current_rfas performance +CREATE INDEX idx_rfa_revisions_current_status ON rfa_revisions (is_current, rfa_status_code_id); + +CREATE INDEX idx_rfa_revisions_rfa_current ON rfa_revisions (rfa_id, is_current); + +-- Indexes for document statistics performance +CREATE INDEX idx_correspondences_project_type ON correspondences (project_id, correspondence_type_id); + +CREATE INDEX idx_corr_revisions_status_current ON correspondence_revisions (correspondence_status_id, is_current); + +CREATE INDEX IDX_AUDIT_DOC_ID ON document_number_audit (document_id); + +CREATE INDEX IDX_AUDIT_STATUS ON document_number_audit (STATUS); + +CREATE INDEX IDX_AUDIT_OPERATION ON document_number_audit (operation); + +SET FOREIGN_KEY_CHECKS = 1; + diff --git a/specs/03-Data-and-Storage/n8n.workflow b/specs/03-Data-and-Storage/n8n.workflow index dba3879..662d876 100644 --- a/specs/03-Data-and-Storage/n8n.workflow +++ b/specs/03-Data-and-Storage/n8n.workflow @@ -1,53 +1,206 @@ { + "name": "LCBP3 Migration Workflow v1.8.0", "meta": { - "instanceId": "lcbp3-migration" + "instanceId": "lcbp3-migration-free" + }, + "settings": { + "executionOrder": "v1" }, "nodes": [ { "parameters": {}, - "id": "trigger-1", - "name": "When clicking ‘Execute Workflow’", + "id": "trigger-manual", + "name": "Manual Trigger", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, - "position": [0, 0] + "position": [ + 0, + 0 + ], + "notes": "กดรันด้วยตนเอง" }, { "parameters": { - "operation": "read", - "fileFormat": "xlsx", - "options": {} + "jsCode": "// ============================================\n// CONFIGURATION - แก้ไขค่าที่นี่\n// ============================================\nconst CONFIG = {\n // Ollama Settings\n OLLAMA_HOST: 'http://192.168.20.100:11434',\n OLLAMA_MODEL_PRIMARY: 'llama3.2:3b',\n OLLAMA_MODEL_FALLBACK: 'mistral:7b-instruct-q4_K_M',\n \n // Backend Settings\n BACKEND_URL: 'https://api.np-dms.work',\n MIGRATION_TOKEN: 'Bearer YOUR_MIGRATION_TOKEN_HERE',\n \n // Batch Settings\n BATCH_SIZE: 10,\n BATCH_ID: 'migration_20260226',\n DELAY_MS: 2000,\n \n // Thresholds\n CONFIDENCE_HIGH: 0.85,\n CONFIDENCE_LOW: 0.60,\n MAX_RETRY: 3,\n FALLBACK_THRESHOLD: 5,\n \n // Paths\n STAGING_PATH: '/home/node/.n8n-files/staging_ai',\n LOG_PATH: '/home/node/.n8n-files/migration_logs',\n \n // Database\n DB_HOST: '192.168.1.100',\n DB_PORT: 3306,\n DB_NAME: 'lcbp3_db',\n DB_USER: 'migration_bot',\n DB_PASSWORD: 'YOUR_DB_PASSWORD_HERE'\n};\n\nreturn [{ json: { config_loaded: true, timestamp: new Date().toISOString(), config: CONFIG } }];" }, - "id": "spreadsheet-1", - "name": "Read Excel Data", - "type": "n8n-nodes-base.spreadsheetFile", - "typeVersion": 2, - "position": [200, 0] - }, - { - "parameters": { - "batchSize": 10, - "options": {} - }, - "id": "split-in-batches-1", - "name": "Split In Batches", - "type": "n8n-nodes-base.splitInBatches", - "typeVersion": 3, - "position": [400, 0] - }, - { - "parameters": { - "jsCode": "const item = $input.first();\n\nconst prompt = `You are a Document Controller for a large construction project.\nYour task is to validate document metadata.\nYou MUST respond ONLY with valid JSON. No explanation, no markdown, no extra text.\n\nDocument Number: ${item.json.document_number}\nTitle: ${item.json.title}\nCategory List: [\"Correspondence\",\"RFA\",\"Drawing\",\"Transmittal\",\"Report\",\"Other\"]\n\nRespond ONLY with this exact JSON structure:\n{\n \"is_valid\": true,\n \"confidence\": 0.95,\n \"suggested_category\": \"Correspondence\",\n \"detected_issues\": [],\n \"suggested_title\": null\n}`;\n\nreturn [{\n json: {\n ...item.json,\n ollama_payload: {\n model: \"llama3.2:3b\",\n format: \"json\",\n stream: false,\n prompt: prompt\n }\n }\n}];" - }, - "id": "code-1", - "name": "Build Prompt", + "id": "config-setter", + "name": "Set Configuration", "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [620, 0] + "position": [ + 200, + 0 + ], + "notes": "กำหนดค่า Configuration ทั้งหมด - แก้ไขที่นี่ก่อนรัน" + }, + { + "parameters": { + "method": "GET", + "url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/master/correspondence-types", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "={{$workflow.staticData.config.MIGRATION_TOKEN}}" + } + ] + }, + "options": { + "timeout": 10000 + } + }, + "id": "preflight-categories", + "name": "Fetch Categories", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + 400, + 0 + ], + "notes": "ดึง Categories จาก Backend" + }, + { + "parameters": { + "method": "GET", + "url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/health", + "options": { + "timeout": 5000 + } + }, + "id": "preflight-health", + "name": "Check Backend Health", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + 400, + 200 + ], + "notes": "ตรวจสอบ Backend พร้อมใช้งาน", + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst config = $('Set Configuration').first().json.config;\n\n// Check file mount\ntry {\n const files = fs.readdirSync(config.STAGING_PATH);\n if (files.length === 0) throw new Error('staging_ai is empty');\n \n // Check write permission to log path\n fs.writeFileSync(`${config.LOG_PATH}/.preflight_ok`, new Date().toISOString());\n \n // Store categories\n const categories = $input.first().json.categories || \n ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\n $('File Mount Check').first().json.system_categories = categories;\n \n return [{ json: { \n preflight_ok: true, \n file_count: files.length,\n system_categories: categories,\n timestamp: new Date().toISOString()\n }}];\n} catch (err) {\n throw new Error(`Pre-flight check failed: ${err.message}`);\n}" + }, + "id": "preflight-check", + "name": "File Mount Check", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 600, + 0 + ], + "notes": "ตรวจสอบ File System และเก็บ Categories" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$('Set Configuration').first().json.config.DB_HOST}}", + "port": "={{$('Set Configuration').first().json.config.DB_PORT}}", + "database": "={{$('Set Configuration').first().json.config.DB_NAME}}", + "user": "={{$('Set Configuration').first().json.config.DB_USER}}", + "password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}", + "query": "SELECT last_processed_index, status FROM migration_progress WHERE batch_id = '{{$('Set Configuration').first().json.config.BATCH_ID}}' LIMIT 1", + "options": {} + }, + "id": "checkpoint-read", + "name": "Read Checkpoint", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [ + 800, + 0 + ], + "notes": "อ่านตำแหน่งล่าสุดที่ประมวลผล", + "onError": "continueErrorOutput" + }, + { + "parameters": { + "operation": "toData", + "binaryProperty": "data", + "options": { + "sheetName": "Sheet1" + } + }, + "id": "excel-reader", + "name": "Read Excel", + "type": "n8n-nodes-base.spreadsheetFile", + "typeVersion": 2, + "position": [ + 800, + 200 + ], + "notes": "อ่านไฟล์ Excel รายการเอกสาร" + }, + { + "parameters": { + "jsCode": "const checkpoint = $input.first().json[0] || { last_processed_index: 0, status: 'NEW' };\nconst startIndex = checkpoint.last_processed_index || 0;\nconst config = $('Set Configuration').first().json.config;\n\nconst allItems = $('Read Excel').all()[0].json.data || [];\nconst remaining = allItems.slice(startIndex);\nconst currentBatch = remaining.slice(0, config.BATCH_SIZE);\n\n// Encoding Normalization\nconst normalize = (str) => {\n if (!str) return '';\n return String(str).normalize('NFC').trim();\n};\n\nreturn currentBatch.map((item, i) => ({\n json: {\n document_number: normalize(item.document_number || item['Document Number']),\n title: normalize(item.title || item.Title || item['Subject']),\n legacy_number: normalize(item.legacy_number || item['Legacy Number']),\n excel_revision: item.revision || item.Revision || 1,\n original_index: startIndex + i,\n batch_id: config.BATCH_ID,\n file_name: `${normalize(item.document_number)}.pdf`\n }\n}));" + }, + "id": "batch-processor", + "name": "Process Batch + Encoding", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1000, + 0 + ], + "notes": "ตัด Batch + Normalize UTF-8" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst path = require('path');\nconst config = $('Set Configuration').first().json.config;\n\nconst items = $input.all();\nconst validated = [];\nconst errors = [];\n\nfor (const item of items) {\n const docNum = item.json.document_number;\n \n // Sanitize filename\n const safeName = path.basename(String(docNum).replace(/[^a-zA-Z0-9\\-_.]/g, '_')).normalize('NFC');\n const filePath = path.resolve(config.STAGING_PATH, `${safeName}.pdf`);\n \n // Path traversal check\n if (!filePath.startsWith(config.STAGING_PATH)) {\n errors.push({\n ...item,\n json: { ...item.json, error: 'Path traversal detected', error_type: 'SECURITY', file_exists: false }\n });\n continue;\n }\n \n try {\n if (fs.existsSync(filePath)) {\n const stats = fs.statSync(filePath);\n validated.push({\n ...item,\n json: { ...item.json, file_exists: true, file_size: stats.size, file_path: filePath }\n });\n } else {\n errors.push({\n ...item,\n json: { ...item.json, error: `File not found: ${safeName}.pdf`, error_type: 'FILE_NOT_FOUND', file_exists: false }\n });\n }\n } catch (err) {\n errors.push({\n ...item,\n json: { ...item.json, error: err.message, error_type: 'FILE_ERROR', file_exists: false }\n });\n }\n}\n\n// Output 0: Validated, Output 1: Errors\nreturn [validated, errors];" + }, + "id": "file-validator", + "name": "File Validator", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1200, + 0 + ], + "notes": "ตรวจสอบไฟล์ PDF มีอยู่จริง + Sanitize path" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$('Set Configuration').first().json.config.DB_HOST}}", + "port": "={{$('Set Configuration').first().json.config.DB_PORT}}", + "database": "={{$('Set Configuration').first().json.config.DB_NAME}}", + "user": "={{$('Set Configuration').first().json.config.DB_USER}}", + "password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}", + "query": "SELECT is_fallback_active, recent_error_count FROM migration_fallback_state WHERE batch_id = '{{$('Set Configuration').first().json.config.BATCH_ID}}' LIMIT 1", + "options": {} + }, + "id": "fallback-check", + "name": "Check Fallback State", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [ + 1400, + -200 + ], + "notes": "ตรวจสอบว่าต้องใช้ Fallback Model หรือไม่", + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "const config = $('Set Configuration').first().json.config;\nconst fallbackState = $input.first().json[0] || { is_fallback_active: false, recent_error_count: 0 };\n\nconst isFallback = fallbackState.is_fallback_active || false;\nconst model = isFallback ? config.OLLAMA_MODEL_FALLBACK : config.OLLAMA_MODEL_PRIMARY;\n\nconst systemCategories = $('File Mount Check').first().json.system_categories || \n ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\n\nconst items = $('File Validator').all();\n\nreturn items.map(item => {\n const systemPrompt = `You are a Document Controller for a large construction project.\nYour task is to validate document metadata.\nYou MUST respond ONLY with valid JSON. No explanation, no markdown, no extra text.\nIf there are no issues, \"detected_issues\" must be an empty array [].`;\n\n const userPrompt = `Validate this document metadata and respond in JSON:\n\nDocument Number: ${item.json.document_number}\nTitle: ${item.json.title}\nExpected Pattern: [ORG]-[TYPE]-[SEQ] e.g. \"TCC-COR-0001\"\nCategory List (MUST match system enum exactly): ${JSON.stringify(systemCategories)}\n\nRespond ONLY with this exact JSON structure:\n{\n \"is_valid\": true | false,\n \"confidence\": 0.0 to 1.0,\n \"suggested_category\": \"\",\n \"detected_issues\": [\"\"],\n \"suggested_title\": \"\"\n}`;\n\n return {\n json: {\n ...item.json,\n active_model: model,\n is_fallback: isFallback,\n system_categories: systemCategories,\n ollama_payload: {\n model: model,\n prompt: `${systemPrompt}\\n\\n${userPrompt}`,\n stream: false,\n format: 'json'\n }\n }\n };\n});" + }, + "id": "prompt-builder", + "name": "Build AI Prompt", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1400, + 0 + ], + "notes": "สร้าง Prompt โดยใช้ Categories จาก System" }, { "parameters": { "method": "POST", - "url": "http://192.168.20.100:11434/api/generate", + "url": "={{$('Set Configuration').first().json.config.OLLAMA_HOST}}/api/generate", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ $json.ollama_payload }}", @@ -55,157 +208,404 @@ "timeout": 30000 } }, - "id": "http-1", - "name": "Ollama Local API", + "id": "ollama-call", + "name": "Ollama AI Analysis", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [840, 0] + "position": [ + 1600, + 0 + ], + "notes": "เรียก Ollama วิเคราะห์เอกสาร" }, { "parameters": { - "jsCode": "const items = $input.all();\nconst parsed = [];\n\nfor (const item of items) {\n try {\n let raw = item.json.response || '';\n raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n const aiResult = JSON.parse(raw);\n parsed.push({ json: { ...item.json, ai_result: aiResult } });\n } catch (err) {\n parsed.push({ json: { ...item.json, ai_result: { confidence: 0, is_valid: false, error: err.message } } });\n }\n}\nreturn parsed;" + "jsCode": "const items = $input.all();\nconst parsed = [];\nconst parseErrors = [];\n\nfor (const item of items) {\n try {\n let raw = item.json.response || '';\n \n // Clean markdown\n raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n const result = JSON.parse(raw);\n \n // Schema Validation\n if (typeof result.is_valid !== 'boolean') throw new Error('is_valid must be boolean');\n if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) {\n throw new Error('confidence must be float 0.0-1.0');\n }\n if (!Array.isArray(result.detected_issues)) throw new Error('detected_issues must be array');\n \n // Enum Validation\n const systemCategories = item.json.system_categories || [];\n if (!systemCategories.includes(result.suggested_category)) {\n throw new Error(`Category \"${result.suggested_category}\" not in system enum`);\n }\n \n parsed.push({\n ...item,\n json: { ...item.json, ai_result: result, parse_error: null }\n });\n } catch (err) {\n parseErrors.push({\n ...item,\n json: {\n ...item.json,\n ai_result: null,\n parse_error: err.message,\n raw_ai_response: item.json.response,\n error_type: 'AI_PARSE_ERROR'\n }\n });\n }\n}\n\nreturn [parsed, parseErrors];" }, - "id": "code-2", - "name": "Parse JSON", + "id": "json-parser", + "name": "Parse & Validate AI Response", "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [1040, 0] + "position": [ + 1800, + 0 + ], + "notes": "Parse JSON + Validate Schema + Enum Check" }, { "parameters": { - "conditions": { - "boolean": [ - { - "value1": "={{ $json.ai_result.confidence >= 0.85 && $json.ai_result.is_valid }}", - "value2": true - } - ] - } + "operation": "executeQuery", + "host": "={{$('Set Configuration').first().json.config.DB_HOST}}", + "port": "={{$('Set Configuration').first().json.config.DB_PORT}}", + "database": "={{$('Set Configuration').first().json.config.DB_NAME}}", + "user": "={{$('Set Configuration').first().json.config.DB_USER}}", + "password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}", + "query": "INSERT INTO migration_fallback_state (batch_id, recent_error_count, is_fallback_active) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', 1, FALSE) ON DUPLICATE KEY UPDATE recent_error_count = recent_error_count + 1, is_fallback_active = CASE WHEN recent_error_count + 1 >= {{$('Set Configuration').first().json.config.FALLBACK_THRESHOLD}} THEN TRUE ELSE is_fallback_active END, updated_at = NOW()", + "options": {} }, - "id": "if-1", - "name": "Confidence >= 0.85?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [1240, 0] + "id": "fallback-update", + "name": "Update Fallback State", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [ + 2000, + 200 + ], + "notes": "เพิ่ม Error count และตรวจสอบ Fallback threshold" + }, + { + "parameters": { + "jsCode": "const config = $('Set Configuration').first().json.config;\nconst items = $('Parse & Validate AI Response').all();\n\nconst autoIngest = [];\nconst reviewQueue = [];\nconst rejectLog = [];\nconst errorLog = [];\n\nfor (const item of items) {\n if (item.json.parse_error || !item.json.ai_result) {\n errorLog.push(item);\n continue;\n }\n \n const ai = item.json.ai_result;\n \n // Revision Drift Protection (ถ้ามีข้อมูลจาก DB)\n if (item.json.current_db_revision !== undefined) {\n const expectedRev = item.json.current_db_revision + 1;\n if (parseInt(item.json.excel_revision) !== expectedRev) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Revision drift: Excel=${item.json.excel_revision}, Expected=${expectedRev}` }\n });\n continue;\n }\n }\n \n // Confidence Routing\n if (ai.confidence >= config.CONFIDENCE_HIGH && ai.is_valid === true) {\n autoIngest.push(item);\n } else if (ai.confidence >= config.CONFIDENCE_LOW) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_HIGH}` }\n });\n } else {\n rejectLog.push({\n ...item,\n json: { ...item.json, reject_reason: ai.is_valid === false ? 'AI marked invalid' : `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_LOW}` }\n });\n }\n}\n\n// Output 0: Auto, 1: Review, 2: Reject, 3: Error\nreturn [autoIngest, reviewQueue, rejectLog, errorLog];" + }, + "id": "confidence-router", + "name": "Confidence Router", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2000, + 0 + ], + "notes": "แยกตาม Confidence: Auto(≥0.85) / Review(≥0.60) / Reject(<0.60)" }, { "parameters": { "method": "POST", - "url": "http://:3000/api/migration/import", + "url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/migration/import", "sendHeaders": true, "headerParameters": { "parameters": [ { - "name": "Idempotency-Key", - "value": "={{ $json.document_number }}:BATCH-001" + "name": "Authorization", + "value": "={{$workflow.staticData.config.MIGRATION_TOKEN}}" }, { - "name": "Authorization", - "value": "Bearer " + "name": "Idempotency-Key", + "value": "={{$json.document_number}}:{{$workflow.staticData.config.BATCH_ID}}" } ] }, "sendBody": true, "specifyBody": "json", - "jsonBody": "={\n \"source_file_path\": \"/share/np-dms/staging_ai/{{$json.document_number}}.pdf\",\n \"document_number\": \"{{$json.document_number}}\",\n \"title\": \"{{$json.ai_result.suggested_title || $json.title}}\",\n \"category\": \"{{$json.ai_result.suggested_category}}\",\n \"revision\": 1, \n \"batch_id\": \"BATCH_001\",\n \"ai_confidence\": {{$json.ai_result.confidence}},\n \"ai_issues\": {{$json.ai_result.detected_issues}},\n \"legacy_document_number\": \"{{$json.legacy_number}}\"\n}", - "options": {} + "jsonBody": "={\n \"document_number\": \"{{$json.document_number}}\",\n \"title\": \"{{$json.ai_result.suggested_title || $json.title}}\",\n \"category\": \"{{$json.ai_result.suggested_category}}\",\n \"source_file_path\": \"{{$json.file_path}}\",\n \"ai_confidence\": {{$json.ai_result.confidence}},\n \"ai_issues\": {{JSON.stringify($json.ai_result.detected_issues)}},\n \"migrated_by\": \"SYSTEM_IMPORT\",\n \"batch_id\": \"{{$('Set Configuration').first().json.config.BATCH_ID}}\",\n \"details\": {\n \"legacy_number\": \"{{$json.legacy_number}}\"\n }\n}", + "options": { + "timeout": 30000 + } }, - "id": "http-2", - "name": "LCBP3 Backend (Auto Ingest)", + "id": "backend-import", + "name": "Import to Backend", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [1460, -100] + "position": [ + 2200, + -200 + ], + "notes": "ส่งข้อมูลเข้า LCBP3 Backend พร้อม Idempotency-Key" }, { "parameters": { - "jsCode": "return [{ json: { message: \"Sent to Human Review Queue OR Check AI Error Log\", data: $input.first().json } }];" + "jsCode": "const item = $input.first();\nconst shouldCheckpoint = item.json.original_index % 10 === 0;\n\nreturn [{\n json: {\n ...item.json,\n should_update_checkpoint: shouldCheckpoint,\n checkpoint_index: item.json.original_index,\n import_status: 'success',\n timestamp: new Date().toISOString()\n }\n}];" }, - "id": "code-3", - "name": "Review Queue / Reject Log", + "id": "checkpoint-flag", + "name": "Flag Checkpoint", "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [1460, 100] + "position": [ + 2400, + -200 + ], + "notes": "กำหนดว่าจะบันทึก Checkpoint หรือไม่ (ทุก 10 records)" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$('Set Configuration').first().json.config.DB_HOST}}", + "port": "={{$('Set Configuration').first().json.config.DB_PORT}}", + "database": "={{$('Set Configuration').first().json.config.DB_NAME}}", + "user": "={{$('Set Configuration').first().json.config.DB_USER}}", + "password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}", + "query": "INSERT INTO migration_progress (batch_id, last_processed_index, status) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', {{$json.checkpoint_index}}, 'RUNNING') ON DUPLICATE KEY UPDATE last_processed_index = {{$json.checkpoint_index}}, updated_at = NOW()", + "options": {} + }, + "id": "checkpoint-save", + "name": "Save Checkpoint", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [ + 2600, + -200 + ], + "notes": "บันทึกความคืบหน้าลง Database" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$('Set Configuration').first().json.config.DB_HOST}}", + "port": "={{$('Set Configuration').first().json.config.DB_PORT}}", + "database": "={{$('Set Configuration').first().json.config.DB_NAME}}", + "user": "={{$('Set Configuration').first().json.config.DB_USER}}", + "password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}", + "query": "INSERT INTO migration_review_queue (document_number, title, original_title, ai_suggested_category, ai_confidence, ai_issues, review_reason, status, created_at) VALUES ('{{$json.document_number}}', '{{$json.ai_result.suggested_title || $json.title}}', '{{$json.title}}', '{{$json.ai_result.suggested_category}}', {{$json.ai_result.confidence}}, '{{JSON.stringify($json.ai_result.detected_issues)}}', '{{$json.review_reason}}', 'PENDING', NOW()) ON DUPLICATE KEY UPDATE status = 'PENDING', review_reason = '{{$json.review_reason}}', created_at = NOW()", + "options": {} + }, + "id": "review-queue-insert", + "name": "Insert Review Queue", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [ + 2200, + 0 + ], + "notes": "บันทึกรายการที่ต้องตรวจสอบโดยคน (ไม่สร้าง Correspondence)" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst item = $input.first();\nconst config = $('Set Configuration').first().json.config;\n\nconst csvPath = `${config.LOG_PATH}/reject_log.csv`;\nconst header = 'timestamp,document_number,title,reject_reason,ai_confidence,ai_issues\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nconst line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.title),\n esc(item.json.reject_reason),\n item.json.ai_result?.confidence ?? 'N/A',\n esc(JSON.stringify(item.json.ai_result?.detected_issues || []))\n].join(',') + '\\n';\n\nfs.appendFileSync(csvPath, line, 'utf8');\n\nreturn [$input.first()];" + }, + "id": "reject-logger", + "name": "Log Reject to CSV", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2200, + 200 + ], + "notes": "บันทึกรายการที่ถูกปฏิเสธลง CSV" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst items = $input.all();\nconst config = $('Set Configuration').first().json.config;\n\nconst csvPath = `${config.LOG_PATH}/error_log.csv`;\nconst header = 'timestamp,document_number,error_type,error_message,raw_ai_response\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nfor (const item of items) {\n const line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.error_type || 'UNKNOWN'),\n esc(item.json.error || item.json.parse_error),\n esc(item.json.raw_ai_response || '')\n ].join(',') + '\\n';\n \n fs.appendFileSync(csvPath, line, 'utf8');\n}\n\nreturn items;" + }, + "id": "error-logger-csv", + "name": "Log Error to CSV", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1400, + 400 + ], + "notes": "บันทึก Error ลง CSV (จาก File Validator)" + }, + { + "parameters": { + "operation": "executeQuery", + "host": "={{$('Set Configuration').first().json.config.DB_HOST}}", + "port": "={{$('Set Configuration').first().json.config.DB_PORT}}", + "database": "={{$('Set Configuration').first().json.config.DB_NAME}}", + "user": "={{$('Set Configuration').first().json.config.DB_USER}}", + "password": "={{$('Set Configuration').first().json.config.DB_PASSWORD}}", + "query": "INSERT INTO migration_errors (batch_id, document_number, error_type, error_message, raw_ai_response, created_at) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', '{{$json.document_number}}', '{{$json.error_type || \"UNKNOWN\"}}', '{{$json.error || $json.parse_error}}', '{{$json.raw_ai_response || \"\"}}', NOW())", + "options": {} + }, + "id": "error-logger-db", + "name": "Log Error to DB", + "type": "n8n-nodes-base.mySql", + "typeVersion": 2.4, + "position": [ + 2000, + 400 + ], + "notes": "บันทึก Error ลง MariaDB" + }, + { + "parameters": { + "amount": "={{$('Set Configuration').first().json.config.DELAY_MS}}", + "unit": "milliseconds" + }, + "id": "delay-node", + "name": "Delay", + "type": "n8n-nodes-base.wait", + "typeVersion": 1, + "position": [ + 2800, + 0 + ], + "notes": "หน่วงเวลาระหว่าง Batches" } ], "connections": { - "When clicking ‘Execute Workflow’": { + "Manual Trigger": { "main": [ [ { - "node": "Read Excel Data", + "node": "Set Configuration", "type": "main", "index": 0 } ] ] }, - "Read Excel Data": { + "Set Configuration": { "main": [ [ { - "node": "Split In Batches", + "node": "Fetch Categories", "type": "main", "index": 0 } ] ] }, - "Split In Batches": { + "Fetch Categories": { "main": [ [ { - "node": "Build Prompt", + "node": "File Mount Check", "type": "main", "index": 0 } ] ] }, - "Build Prompt": { + "File Mount Check": { "main": [ [ { - "node": "Ollama Local API", + "node": "Read Checkpoint", "type": "main", "index": 0 } ] ] }, - "Ollama Local API": { + "Read Checkpoint": { "main": [ [ { - "node": "Parse JSON", + "node": "Process Batch + Encoding", "type": "main", "index": 0 } ] ] }, - "Parse JSON": { + "Process Batch + Encoding": { "main": [ [ { - "node": "Confidence >= 0.85?", + "node": "File Validator", "type": "main", "index": 0 } ] ] }, - "Confidence >= 0.85?": { + "File Validator": { "main": [ [ { - "node": "LCBP3 Backend (Auto Ingest)", + "node": "Build AI Prompt", "type": "main", "index": 0 } ], [ { - "node": "Review Queue / Reject Log", + "node": "Log Error to CSV", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build AI Prompt": { + "main": [ + [ + { + "node": "Ollama AI Analysis", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ollama AI Analysis": { + "main": [ + [ + { + "node": "Parse & Validate AI Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse & Validate AI Response": { + "main": [ + [ + { + "node": "Confidence Router", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Update Fallback State", + "type": "main", + "index": 0 + } + ] + ] + }, + "Confidence Router": { + "main": [ + [ + { + "node": "Import to Backend", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Insert Review Queue", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log Reject to CSV", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log Error to DB", + "type": "main", + "index": 0 + } + ] + ] + }, + "Import to Backend": { + "main": [ + [ + { + "node": "Flag Checkpoint", + "type": "main", + "index": 0 + } + ] + ] + }, + "Flag Checkpoint": { + "main": [ + [ + { + "node": "Save Checkpoint", + "type": "main", + "index": 0 + } + ] + ] + }, + "Log Error to CSV": { + "main": [ + [ + { + "node": "Log Error to DB", "type": "main", "index": 0 } diff --git a/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql b/specs/99-archives/lcbp3-v1.8.0-schema.sql similarity index 96% rename from specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql rename to specs/99-archives/lcbp3-v1.8.0-schema.sql index 53bf092..2bf52db 100644 --- a/specs/03-Data-and-Storage/lcbp3-v1.8.0-schema.sql +++ b/specs/99-archives/lcbp3-v1.8.0-schema.sql @@ -19,13 +19,6 @@ -- - INDEX idx_doc_number (correspondence_number), -- - INDEX idx_deleted_at (deleted_at), -- - INDEX idx_created_by (created_by), --- 2. เพิ่ม: --- 2.1 TABLE migration_progress --- 2.2 TABLE import_transactions --- 2.3 TABLE migration_review_queue --- 2.4 TABLE migration_errors --- 2.5 TABLE migration_fallback_state --- 2.6 TABLE migration_daily_summary -- ========================================================== SET NAMES utf8mb4; @@ -55,18 +48,6 @@ DROP VIEW IF EXISTS v_current_correspondences; -- คำเตือน: ข้อมูลทั้งหมดจะหายไป กรุณา Backup ก่อนรันบน Production SET FOREIGN_KEY_CHECKS = 0; -DROP TABLE IF EXISTS migration_progress; - -DROP TABLE IF EXISTS import_transactions; - -DROP TABLE IF EXISTS migration_review_queue; - -DROP TABLE IF EXISTS migration_errors; - -DROP TABLE IF EXISTS migration_fallback_state; - -DROP TABLE IF EXISTS migration_daily_summary; - -- ============================================================ -- ส่วนที่ 1: ตาราง System, Logs & Preferences (ตารางปลายทาง/ส่วนเสริม) -- ============================================================ @@ -1563,24 +1544,6 @@ CREATE INDEX idx_wf_hist_instance ON workflow_histories (instance_id); CREATE INDEX idx_wf_hist_user ON workflow_histories (action_by_user_id); --- 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) -); -- ============================================================ -- 5. PARTITIONING PREPARATION (Advance - Optional) @@ -2071,87 +2034,6 @@ CREATE INDEX idx_correspondences_type_project ON correspondences (correspondence CREATE INDEX idx_corr_revisions_current_status ON correspondence_revisions (is_current, correspondence_status_id); --- ===================================================== --- Migration Tracking Tables (Temporary) --- ===================================================== --- Checkpoint -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 -); - --- Review Queue (Temporary — ไม่ใช่ Business Schema) -CREATE TABLE IF NOT EXISTS migration_review_queue ( - id INT AUTO_INCREMENT PRIMARY KEY, - document_number VARCHAR(100) NOT NULL, - title TEXT, - original_title TEXT, - ai_suggested_category VARCHAR(50), - ai_confidence DECIMAL(4, 3), - ai_issues JSON, - review_reason VARCHAR(255), - STATUS ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING', - reviewed_by VARCHAR(100), - reviewed_at TIMESTAMP NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uq_doc_number (document_number) -); - --- Error Log -CREATE TABLE IF NOT EXISTS migration_errors ( - id INT AUTO_INCREMENT PRIMARY KEY, - batch_id VARCHAR(50), - document_number VARCHAR(100), - error_type ENUM( - 'FILE_NOT_FOUND', - 'AI_PARSE_ERROR', - 'API_ERROR', - 'DB_ERROR', - 'UNKNOWN' - ), - error_message TEXT, - raw_ai_response TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX idx_batch_id (batch_id), - INDEX idx_error_type (error_type) -); - --- Fallback State -CREATE TABLE IF NOT EXISTS migration_fallback_state ( - id INT AUTO_INCREMENT PRIMARY KEY, - batch_id VARCHAR(50) UNIQUE, - recent_error_count INT DEFAULT 0, - is_fallback_active BOOLEAN DEFAULT FALSE, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - --- Idempotency (Patch) -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) -); - --- Daily Summary -CREATE TABLE IF NOT EXISTS migration_daily_summary ( - id INT AUTO_INCREMENT PRIMARY KEY, - batch_id VARCHAR(50), - summary_date DATE, - total_processed INT DEFAULT 0, - auto_ingested INT DEFAULT 0, - sent_to_review INT DEFAULT 0, - rejected INT DEFAULT 0, - errors INT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uq_batch_date (batch_id, summary_date) -); - CREATE INDEX idx_corr_revisions_correspondence_current ON correspondence_revisions (correspondence_id, is_current); -- Indexes for v_current_rfas performance