260322:1648 Correct Coresspondence / Doing RFA / Correct CI
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s

This commit is contained in:
admin
2026-03-22 16:48:12 +07:00
parent e5deedb42e
commit 11984bfa29
683 changed files with 105251 additions and 29068 deletions
@@ -222,20 +222,9 @@ CREATE TABLE workflow_histories (
```typescript
// workflow-engine.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([
WorkflowDefinition,
WorkflowInstance,
WorkflowHistory,
]),
UserModule,
],
imports: [TypeOrmModule.forFeature([WorkflowDefinition, WorkflowInstance, WorkflowHistory]), UserModule],
controllers: [WorkflowEngineController],
providers: [
WorkflowEngineService,
WorkflowDslService,
WorkflowEventService,
],
providers: [WorkflowEngineService, WorkflowDslService, WorkflowEventService],
exports: [WorkflowEngineService],
})
export class WorkflowEngineModule {}
@@ -275,12 +264,7 @@ export class WorkflowEngineService {
payload: Record<string, unknown> = {}
) {
// Evaluation via WorkflowDslService
const evaluation = this.dslService.evaluate(
compiled,
instance.currentState,
action,
context
);
const evaluation = this.dslService.evaluate(compiled, instance.currentState, action, context);
// Update state to target State
instance.currentState = evaluation.nextState;
@@ -196,22 +196,23 @@ CREATE TABLE document_number_audit (
| Token | Description | Example Value | Database Source |
| -------------- | ------------------------- | ------------------------------ | --------------------------------------------------------------------- |
| `{PROJECT}` | รหัสโครงการ | `LCBP3`, `LCBP3-C2` | `projects.project_code` |
| `{ORIGINATOR}` | รหัสองค์กรผู้ส่ง | `คคง.`, `ผรม.1` | `organizations.organization_code` via `correspondences.originator_id` |
| `{RECIPIENT}` | รหัสองค์กรผู้รับหลัก (TO) | `สคฉ.3`, `กทท.` | `organizations.organization_code` via `correspondence_recipients` |
| `{CORR_TYPE}` | รหัสประเภทเอกสาร | `RFA`, `TRANSMITTAL`, `LETTER` | `correspondence_types.type_code` |
| `{SUB_TYPE}` | หมายเลขประเภทย่อย | `11`, `12`, `21` | `correspondence_sub_types.sub_type_number` |
| `{RFA_TYPE}` | รหัสประเภท RFA | `SDW`, `RPT`, `MAT` | `rfa_types.type_code` |
| `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `TER`, `GEO` | `disciplines.discipline_code` |
| `{PROJECT}` | รหัสโครงการ | `LCBP3`, `LCBP3-C2` | `projects.project_code` |
| `{ORIGINATOR}` | รหัสองค์กรผู้ส่ง | `คคง.`, `ผรม.1` | `organizations.organization_code` via `correspondences.originator_id` |
| `{RECIPIENT}` | รหัสองค์กรผู้รับหลัก (TO) | `สคฉ.3`, `กทท.` | `organizations.organization_code` via `correspondence_recipients` |
| `{CORR_TYPE}` | รหัสประเภทเอกสาร | `RFA`, `TRANSMITTAL`, `LETTER` | `correspondence_types.type_code` |
| `{SUB_TYPE}` | หมายเลขประเภทย่อย | `11`, `12`, `21` | `correspondence_sub_types.sub_type_number` |
| `{RFA_TYPE}` | รหัสประเภท RFA | `SDW`, `RPT`, `MAT` | `rfa_types.type_code` |
| `{DISCIPLINE}` | รหัสสาขาวิชา | `STR`, `TER`, `GEO` | `disciplines.discipline_code` |
| `{SEQ:n}` | Running number (n digits) | `0001`, `0029`, `0985` | `document_number_counters.last_number + 1` |
| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` | `current_year + 543` |
| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` | `current_year` |
| `{YEAR:B.E.}` | ปี พ.ศ. | `2568` | `current_year + 543` |
| `{YEAR:A.D.}` | ปี ค.ศ. | `2025` | `current_year` |
| `{REV}` | Revision Code | `A`, `B`, `AA` | `correspondence_revisions.revision_label` |
> [!WARNING]
> **Deprecated Token Names (DO NOT USE)**
>
> The following tokens were used in earlier drafts but are now **deprecated**:
>
> - ~~`{ORG}`~~ → Use `{ORIGINATOR}` or `{RECIPIENT}` (explicit roles)
> - ~~`{TYPE}`~~ → Use `{CORR_TYPE}`, `{SUB_TYPE}`, or `{RFA_TYPE}` (context-specific)
> - ~~`{CATEGORY}`~~ → Not used in current system
@@ -221,6 +222,7 @@ CREATE TABLE document_number_audit (
### Format Resolution Strategy (Fallback Logic)
The system resolves the numbering format using the following priority:
1. **Specific Format:** Search for a record matching both `project_id` and `correspondence_type_id`.
2. **Default Format:** If not found, search for a record with matching `project_id` where `correspondence_type_id` is `NULL`.
3. **System Fallback:** If neither exists, use the hardcoded system default: `{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}`.
@@ -267,6 +269,7 @@ Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, sub
```
**Token Breakdown:**
- `คคง.` = `{ORIGINATOR}` - ผู้ส่ง
- `สคฉ.3` = `{RECIPIENT}` - ผู้รับหลัก (TO)
- `21` = `{SUB_TYPE}` - หมายเลขประเภทย่อย (11=MAT, 12=SHP, 13=DWG, 21=...)
@@ -284,6 +287,7 @@ Counter Key: (project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id,
```
**Token Breakdown:**
- `LCBP3-C2` = `{PROJECT}` - รหัสโครงการ
- `RFA` = `{CORR_TYPE}` - ประเภทเอกสาร (**แสดง**ในtemplate สำหรับ RFA เท่านั้น)
- `TER` = `{DISCIPLINE}` - รหัสสาขา (TER=Terminal, STR=Structure, GEO=Geotechnical)
@@ -340,8 +344,8 @@ export class DocumentNumberingService {
async generateNextNumber(context: NumberingContext): Promise<string> {
const year = context.year || new Date().getFullYear() + 543; // พ.ศ.
const subTypeId = context.subTypeId || 0; // Fallback for NULL
const disciplineId = context.disciplineId || 0; // Fallback for NULL
const subTypeId = context.subTypeId || 0; // Fallback for NULL
const disciplineId = context.disciplineId || 0; // Fallback for NULL
// Build Redis lock key
const lockKey = this.buildLockKey(
@@ -355,14 +359,8 @@ export class DocumentNumberingService {
// Retry with exponential backoff (Scenario 2 & 3)
return this.retryWithBackoff(
async () => await this.generateNumberWithLock(
lockKey,
context,
year,
subTypeId,
disciplineId
),
5, // Max 5 retries
async () => await this.generateNumberWithLock(lockKey, context, year, subTypeId, disciplineId),
5, // Max 5 retries
1000 // Initial delay 1s
);
}
@@ -444,12 +442,7 @@ export class DocumentNumberingService {
}
// Step 4: Generate formatted number
const config = await this.getConfig(
context.projectId,
context.docTypeId,
subTypeId,
disciplineId
);
const config = await this.getConfig(context.projectId, context.docTypeId, subTypeId, disciplineId);
const formattedNumber = await this.formatNumber(config.template, {
...context,
@@ -471,7 +464,6 @@ export class DocumentNumberingService {
this.logger.log(`Generated: ${formattedNumber} (wait: ${lockWaitMs}ms)`);
return formattedNumber;
} finally {
// Step 6: Release Redis lock
if (lock) {
@@ -506,35 +498,29 @@ export class DocumentNumberingService {
}
private buildLockKey(...parts: Array<number | string | null | undefined>): string {
return `doc_num:${parts.filter(p => p !== null && p !== undefined).join(':')}`;
return `doc_num:${parts.filter((p) => p !== null && p !== undefined).join(':')}`;
}
// Scenario 2: Lock Acquisition Timeout - Exponential Backoff
private async retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number,
initialDelay: number
): Promise<T> {
private async retryWithBackoff<T>(fn: () => Promise<T>, maxRetries: number, initialDelay: number): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const isRetryable =
error instanceof ConflictException ||
error.code === 'ECONNREFUSED' || // Scenario 4
error.code === 'ETIMEDOUT'; // Scenario 4
error.code === 'ECONNREFUSED' || // Scenario 4
error.code === 'ETIMEDOUT'; // Scenario 4
if (!isRetryable || attempt === maxRetries) {
if (attempt === maxRetries) {
throw new ServiceUnavailableException(
'ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง'
);
throw new ServiceUnavailableException('ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง');
}
throw error;
}
const delay = initialDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise((resolve) => setTimeout(resolve, delay));
this.logger.warn(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
}
}
@@ -721,8 +707,8 @@ sequenceDiagram
### Alert Conditions
| Severity | Condition | Action |
| ---------- | ---------------------------- | ------------------ |
| Severity | Condition | Action |
| ----------- | ---------------------------- | ------------------ |
| 🔴 Critical | Redis unavailable >1 minute | Page ops team |
| 🔴 Critical | Lock failures >10% in 5 min | Page ops team |
| 🟡 Warning | Lock failures >5% in 5 min | Alert ops team |
@@ -823,7 +809,7 @@ describe('DocumentNumberingService - Concurrent Generation', () => {
expect(unique.size).toBe(100);
// Check format
results.forEach(num => {
results.forEach((num) => {
expect(num).toMatch(/^LCBP3-C2-RFI-STR-\d{4}-A$/);
});
});
@@ -874,9 +860,7 @@ describe('DocumentNumberingService - Concurrent Generation', () => {
it('should throw 503 after max lock acquisition retries', async () => {
jest.spyOn(redlock, 'acquire').mockRejectedValue(new Error('Lock timeout'));
await expect(service.generateNextNumber(context))
.rejects
.toThrow(ServiceUnavailableException);
await expect(service.generateNextNumber(context)).rejects.toThrow(ServiceUnavailableException);
});
});
```
@@ -977,4 +961,4 @@ ensure:
| 1.0 | 2025-11-30 | Initial decision |
| 2.0 | 2025-12-02 | Updated with comprehensive error scenarios, monitoring, security, and all token types |
| 3.0 | 2025-12-17 | Aligned with Requirements v1.6.2: updated counter schema, token definitions, Number State Machine |
| 4.0 | 2026-03-21 | Added discipline_id to formats, implemented automated Upsert logic for template management |
| 4.0 | 2026-03-21 | Added discipline_id to formats, implemented automated Upsert logic for template management |
@@ -89,31 +89,31 @@ LCBP3-DMS ต้องเลือก Technology Stack สำหรับพั
#### Backend Stack
| Component | Technology | Rationale |
| :----------------- | :-------------- | :------------------------------------------------------------------------- |
| **Runtime** | Node.js 20 LTS | Stable, modern features, long-term support |
| **Framework** | NestJS 11 | Modular, TypeScript-first, Express v5 support |
| **HTTP Engine** | Express 5 | Path param changes, improved error handling |
| **Language** | TypeScript 5.x | Type safety, better DX |
| **ORM** | TypeORM | TypeScript support, migrations, repositories |
| **Database** | MariaDB 11.8 | JSON support, virtual columns, QNAP compatible |
| **Validation** | class-validator | Decorator-based, integrates with NestJS |
| **Authentication** | Passport + JWT | Standard, well-supported |
| Component | Technology | Rationale |
| :----------------- | :-------------- | :-------------------------------------------------------------------------- |
| **Runtime** | Node.js 20 LTS | Stable, modern features, long-term support |
| **Framework** | NestJS 11 | Modular, TypeScript-first, Express v5 support |
| **HTTP Engine** | Express 5 | Path param changes, improved error handling |
| **Language** | TypeScript 5.x | Type safety, better DX |
| **ORM** | TypeORM | TypeScript support, migrations, repositories |
| **Database** | MariaDB 11.8 | JSON support, virtual columns, QNAP compatible |
| **Validation** | class-validator | Decorator-based, integrates with NestJS |
| **Authentication** | Passport + JWT | Standard, well-supported |
| **Authorization** | CASL **6.7.5+** | Flexible RBAC implementation ⚠️ Patched CVE-2026-1774 (Prototype Pollution) |
| **Documentation** | Swagger/OpenAPI | Auto-generated from decorators |
| **Testing** | Jest | Built-in with NestJS |
| **Documentation** | Swagger/OpenAPI | Auto-generated from decorators |
| **Testing** | Jest | Built-in with NestJS |
#### Frontend Stack
| Component | Technology | Rationale |
| :-------------------- | :------------------ | :------------------------------------- |
| **Framework** | Next.js 16.2.0 | App Router, SSR/SSG, React integration |
| **UI Library** | React 19.2.4 | Industry standard, large ecosystem |
| **Language** | TypeScript 5.x | Consistency with backend |
| **Styling** | Tailwind CSS 4.2.2 | Utility-first, fast development |
| **Component Library** | shadcn/ui | Accessible, customizable, TypeScript |
| **State Management** | TanStack Query | Server state management |
| **Form Handling** | React Hook Form 7.71.2 | Performance, ต้ validation with Zod |
| Component | Technology | Rationale |
| :-------------------- | :------------------------ | :------------------------------------- |
| **Framework** | Next.js 16.2.0 | App Router, SSR/SSG, React integration |
| **UI Library** | React 19.2.4 | Industry standard, large ecosystem |
| **Language** | TypeScript 5.x | Consistency with backend |
| **Styling** | Tailwind CSS 4.2.2 | Utility-first, fast development |
| **Component Library** | shadcn/ui | Accessible, customizable, TypeScript |
| **State Management** | TanStack Query | Server state management |
| **Form Handling** | React Hook Form 7.71.2 | Performance, ต้ validation with Zod |
| **Testing** | Vitest 4.1.0 + Playwright | Fast unit tests, reliable E2E |
#### Infrastructure
@@ -125,12 +125,7 @@ try {
// Key: user:{user_id}:permissions
// Value: JSON array of CASL rules
// TTL: 30 minutes
await redis.set(
`user:${userId}:permissions`,
JSON.stringify(abilityRules),
'EX',
1800
);
await redis.set(`user:${userId}:permissions`, JSON.stringify(abilityRules), 'EX', 1800);
```
**Invalidation Strategy:**
@@ -253,10 +253,28 @@ export class EmailProcessor {
<html>
<head>
<style>
body { font-family: Arial, sans-serif; } .container { max-width: 600px;
margin: 0 auto; padding: 20px; } .header { background: #007bff; color:
white; padding: 20px; } .content { padding: 20px; } .button { background:
#007bff; color: white; padding: 10px 20px; text-decoration: none; }
body {
font-family: Arial, sans-serif;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: #007bff;
color: white;
padding: 20px;
}
.content {
padding: 20px;
}
.button {
background: #007bff;
color: white;
padding: 10px 20px;
text-decoration: none;
}
</style>
</head>
<body>
@@ -159,16 +159,9 @@ git commit -m "feat: add discipline_id to correspondences"
```typescript
// File: backend/src/migrations/1234567890-AddDisciplineIdToCorrespondences.ts
import {
MigrationInterface,
QueryRunner,
TableColumn,
TableForeignKey,
} from 'typeorm';
import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from 'typeorm';
export class AddDisciplineIdToCorrespondences1234567890
implements MigrationInterface
{
export class AddDisciplineIdToCorrespondences1234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Add column
await queryRunner.addColumn(
@@ -192,21 +185,15 @@ export class AddDisciplineIdToCorrespondences1234567890
);
// Add index
await queryRunner.query(
'CREATE INDEX idx_correspondences_discipline_id ON correspondences(discipline_id)'
);
await queryRunner.query('CREATE INDEX idx_correspondences_discipline_id ON correspondences(discipline_id)');
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Reverse order: index → FK → column
await queryRunner.query(
'DROP INDEX idx_correspondences_discipline_id ON correspondences'
);
await queryRunner.query('DROP INDEX idx_correspondences_discipline_id ON correspondences');
const table = await queryRunner.getTable('correspondences');
const foreignKey = table.foreignKeys.find(
(fk) => fk.columnNames.indexOf('discipline_id') !== -1
);
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf('discipline_id') !== -1);
await queryRunner.dropForeignKey('correspondences', foreignKey);
await queryRunner.dropColumn('correspondences', 'discipline_id');
@@ -303,9 +290,7 @@ describe('Migrations', () => {
// Verify tables exist
const tables = await dataSource.query('SHOW TABLES');
expect(tables).toContainEqual(
expect.objectContaining({ Tables_in_lcbp3: 'correspondences' })
);
expect(tables).toContainEqual(expect.objectContaining({ Tables_in_lcbp3: 'correspondences' }));
});
it('should rollback all migrations successfully', async () => {
@@ -131,9 +131,7 @@ export const logger = winston.createLogger({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
return `${timestamp} [${level}]: ${message} ${
Object.keys(meta).length ? JSON.stringify(meta) : ''
}`;
return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`;
})
),
}),
@@ -240,10 +238,7 @@ export class RequestLoggerMiddleware implements NestMiddleware {
// File: backend/src/config/database.config.ts
export default {
// ...
logging:
process.env.NODE_ENV === 'development'
? 'all'
: ['error', 'warn', 'schema'],
logging: process.env.NODE_ENV === 'development' ? 'all' : ['error', 'warn', 'schema'],
logger: 'advanced-console',
maxQueryExecutionTime: 1000, // Warn if query > 1s
};
@@ -299,12 +294,7 @@ logger.verbose('Cache hit', { key, ttl });
```typescript
// File: backend/src/common/interceptors/performance.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { logger } from 'src/config/logger.config';
@@ -119,10 +119,7 @@ export const correspondenceSchema = z.object({
.min(5, 'Subject must be at least 5 characters')
.max(255, 'Subject must not exceed 255 characters'),
description: z
.string()
.min(10, 'Description must be at least 10 characters')
.optional(),
description: z.string().min(10, 'Description must be at least 10 characters').optional(),
document_type_id: z.number({
required_error: 'Document type is required',
@@ -402,16 +399,10 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ success: true });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', issues: error.errors },
{ status: 400 }
);
return NextResponse.json({ error: 'Validation failed', issues: error.errors }, { status: 400 });
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
```
@@ -219,10 +219,7 @@ export const useNotificationStore = create<NotificationState>((set) => ({
addNotification: (notification) =>
set((state) => ({
notifications: [
...state.notifications,
{ ...notification, id: Math.random().toString() },
],
notifications: [...state.notifications, { ...notification, id: Math.random().toString() }],
})),
removeNotification: (id) =>
@@ -312,8 +309,7 @@ export const useUIStore = create<UIState>()(
sidebarCollapsed: false,
theme: 'light',
toggleSidebar: () =>
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
setTheme: (theme) => set({ theme }),
}),
@@ -39,7 +39,7 @@ LCBP3-DMS จัดการเอกสารสำคัญของโปร
**Chosen:** **JWT (JSON Web Tokens) with Bearer Token Strategy (Stored in LocalStorage via Zustand)**
*Note: Initial plan was HTTP-only cookies, but shifted to Bearer tokens to ease cross-domain Next.js to NestJS communication.*
_Note: Initial plan was HTTP-only cookies, but shifted to Bearer tokens to ease cross-domain Next.js to NestJS communication._
```typescript
// File: src/auth/auth.service.ts
@@ -85,10 +85,7 @@ export class AuthService {
if (!user) return null;
// Use bcrypt for password comparison
const isValid = await bcrypt.compare(
credentials.password,
user.password_hash
);
const isValid = await bcrypt.compare(credentials.password, user.password_hash);
return isValid ? user : null;
}
@@ -99,7 +96,7 @@ export class AuthService {
**Strategy:** **bcrypt with salt rounds = 10 (Current implementation defaults to 10 via `genSalt()`)**
*Note: Code currently uses `bcrypt.genSalt()` without arguments, defaulting to 10 rounds. If 12 is strictly required, codebase needs updating.*
_Note: Code currently uses `bcrypt.genSalt()` without arguments, defaulting to 10 rounds. If 12 is strictly required, codebase needs updating._
```typescript
import * as bcrypt from 'bcrypt';
@@ -112,10 +109,7 @@ async function hashPassword(password: string): Promise<string> {
}
// Verify password
async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
```
@@ -177,11 +171,7 @@ function encrypt(text: string): { encrypted: string; iv: string; tag: string } {
}
function decrypt(encrypted: string, iv: string, tag: string): string {
const decipher = crypto.createDecipheriv(
algorithm,
key,
Buffer.from(iv, 'hex')
);
const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(tag, 'hex'));
@@ -5,11 +5,13 @@
**Version:** 1.8.0
**Decision Makers:** Development Team, DevOps Engineer
**Related Documents:**
- [Legacy Data Migration Plan](../03-Data-and-Storage/03-04-legacy-data-migration.md)
- [n8n Migration Setup Guide](../03-Data-and-Storage/03-05-n8n-migration-setup-guide.md)
- [Software Architecture](../02-Architecture/02-02-software-architecture.md)
- [Data Dictionary](../03-Data-and-Storage/03-01-data-dictionary.md)
> **Note:** ADR-017 is clarified and hardened by ADR-018 regarding AI physical isolation. Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439).
> **Note:** ADR-017 is clarified and hardened by ADR-018 regarding AI physical isolation. Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439).
---
## Context and Problem Statement
@@ -19,6 +21,7 @@
ความท้าทายหลักคือ **Data Integrity และความถูกต้องของ Metadata** เนื่องจากข้อมูลเก่ามีโอกาสเกิด Human Error เราจึงต้องการ AI ช่วย Validate ก่อนนำเข้า
การส่งข้อมูลขึ้น Cloud AI Provider มีปัญหา 2 ประการ:
1. **Data Privacy:** เอกสารก่อสร้างท่าเรือเป็นความลับ ห้ามออกนอกเครือข่าย
2. **Cost:** ~$0.010.03 ต่อ Record = อาจสูงถึง $600 สำหรับ 20,000 records
@@ -44,6 +47,7 @@
**Pros:** ไม่ต้องจัดหา Hardware เพิ่ม, AI ฉลาดสูง
**Cons:**
- ❌ ผิดนโยบาย Data Privacy
- ❌ ค่าใช้จ่ายสูง (~$600)
- ❌ Code สกปรก ปะปนกับ Source Code หลัก
@@ -53,12 +57,14 @@
**Pros:** เร็ว ไม่มีค่าใช้จ่าย
**Cons:**
- ❌ ความแม่นยำต่ำ ตรวจได้แค่ Format
- ❌ ต้องใช้ Manual Review จำนวนมาก
### Option 3: Local AI Model (Ollama) + n8n ⭐ (Selected)
**Pros:**
- ✅ Privacy Guaranteed
- ✅ Zero Cost
- ✅ Clean Architecture
@@ -67,6 +73,7 @@
- ✅ Structured Output ด้วย JSON Schema
**Cons:**
- ❌ ต้องเปิด Desktop ทิ้งไว้ดูแล GPU Temperature
- ❌ Model เล็กอาจแม่นน้อยกว่า Cloud AI → ต้องมี Human Review Queue
@@ -82,19 +89,19 @@
## Implementation Summary
| Component | รายละเอียด |
| ---------------------- | ------------------------------------------------------------------------------- |
| Migration Orchestrator | n8n (Docker บน QNAP NAS) |
| AI Model Primary | Ollama `llama3.2:3b` (Validation, Summarization, Tagging) |
| AI Model Fallback | Ollama `mistral:7b-instruct-q4_K_M` |
| Hardware | QNAP NAS (Orchestrator) + Desktop Desk-5439 (AI Processing, RTX 2060 SUPER 8GB) |
| DB Lookup (n8n) | n8n ทำการ Query `project_id`, `organization_id` และดึง `Tags` จาก DB ให้ AI |
| Component | รายละเอียด |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| Migration Orchestrator | n8n (Docker บน QNAP NAS) |
| AI Model Primary | Ollama `llama3.2:3b` (Validation, Summarization, Tagging) |
| AI Model Fallback | Ollama `mistral:7b-instruct-q4_K_M` |
| Hardware | QNAP NAS (Orchestrator) + Desktop Desk-5439 (AI Processing, RTX 2060 SUPER 8GB) |
| DB Lookup (n8n) | n8n ทำการ Query `project_id`, `organization_id` และดึง `Tags` จาก DB ให้ AI |
| Data Ingestion | 1. Staging ลง `migration_review_queue` -> 2. กดยืนยันผ่าน Frontend Management UI -> 3. Final Commit ผ่าน API |
| Concurrency (n8n) | Sequential — Batch Size 50-100 ป้องกัน DB Connection Overload |
| Checkpoint | MariaDB `migration_progress` และการใช้ `ON DUPLICATE KEY UPDATE` ใน Staging |
| Fallback | Auto-switch Model เมื่อ Error ≥ Threshold |
| Storage | Two-Phase Storage: 1. `POST /api/storage/upload` (Temp) -> 2. Commit ภายหลัง |
| Expected Runtime | ~16.6 ชั่วโมง (~3–4 คืน) สำหรับ 20,000 records |
| Concurrency (n8n) | Sequential — Batch Size 50-100 ป้องกัน DB Connection Overload |
| Checkpoint | MariaDB `migration_progress` และการใช้ `ON DUPLICATE KEY UPDATE` ใน Staging |
| Fallback | Auto-switch Model เมื่อ Error ≥ Threshold |
| Storage | Two-Phase Storage: 1. `POST /api/storage/upload` (Temp) -> 2. Commit ภายหลัง |
| Expected Runtime | ~16.6 ชั่วโมง (~3–4 คืน) สำหรับ 20,000 records |
---
@@ -115,14 +122,14 @@
}
```
| Field | Type | คำอธิบาย |
| -------------------- | ------------------------- | --------------------------- |
| `is_valid` | boolean | เอกสารผ่านการตรวจสอบหรือไม่ (เปรียบเทียบ subject vs pdf) |
| `confidence` | float (0.01.0) | ความมั่นใจของ AI |
| `suggested_category` | string (enum จาก Backend) | หมวดหมู่ที่ AI แนะนำ |
| `detected_issues` | string[] | รายการปัญหา (array ว่างถ้าไม่มี) |
| `suggested_title` | string \| null | Title ที่แก้ไขแล้ว หรือ null |
| `summary` | string | สรุปเนื้อหา 4-5 ประโยค สำหรับใส่ใน `body` |
| Field | Type | คำอธิบาย |
| -------------------- | ------------------------- | ---------------------------------------------------------------- |
| `is_valid` | boolean | เอกสารผ่านการตรวจสอบหรือไม่ (เปรียบเทียบ subject vs pdf) |
| `confidence` | float (0.01.0) | ความมั่นใจของ AI |
| `suggested_category` | string (enum จาก Backend) | หมวดหมู่ที่ AI แนะนำ |
| `detected_issues` | string[] | รายการปัญหา (array ว่างถ้าไม่มี) |
| `suggested_title` | string \| null | Title ที่แก้ไขแล้ว หรือ null |
| `summary` | string | สรุปเนื้อหา 4-5 ประโยค สำหรับใส่ใน `body` |
| `suggested_tags` | array of objects | รายการ Tags ที่จับคู่ได้ หรือ แนะนำให้สร้างใหม่ (`is_new: true`) |
> ⚠️ **Patch:** `suggested_category` ต้องตรงกับ System Enum จาก `GET /api/meta/categories` เท่านั้น — ห้าม hardcode Category List ใน Prompt
@@ -133,13 +140,13 @@
**ข้อมูลทุกชุดจาก n8n จะต้องถูกส่งเข้าตาราง `migration_review_queue` เสมอ** โดยจัดสถานะเบื้องต้นตาม Confidence:
| ระดับ Confidence | สถานะใน Review Queue |
| ------------------------------- | --------------------------------------- |
| `>= 0.85` และ `is_valid = true` | `PENDING` (พร้อมให้ Admin เลือก Batch Import) |
| `0.600.84` | `PENDING` (ไฮไลต์แจ้งให้ Admin ตรวจสอบข้อมูลก่อน) |
| `< 0.60` หรือ `is_valid = false` | `REJECTED` (รอให้ Admin แก้ไขข้อมูล Manual) |
| AI Parse Error | ส่งไป Error Log + Trigger Fallback Logic |
| Revision Drift | `PENDING` พร้อมระบุ reason: "Revision drift" |
| ระดับ Confidence | สถานะใน Review Queue |
| -------------------------------- | ------------------------------------------------- |
| `>= 0.85` และ `is_valid = true` | `PENDING` (พร้อมให้ Admin เลือก Batch Import) |
| `0.600.84` | `PENDING` (ไฮไลต์แจ้งให้ Admin ตรวจสอบข้อมูลก่อน) |
| `< 0.60` หรือ `is_valid = false` | `REJECTED` (รอให้ Admin แก้ไขข้อมูล Manual) |
| AI Parse Error | ส่งไป Error Log + Trigger Fallback Logic |
| Revision Drift | `PENDING` พร้อมระบุ reason: "Revision drift" |
> ⚠️ **Tag Review:** ข้อมูลใดที่มี `is_new: true` ใน `suggested_tags` จะถูกบังคับให้ Admin ตรวจสอบบน Frontend UI ก่อน เพื่อป้องกัน AI สร้าง Tags ขยะซ้ำซ้อน
@@ -148,11 +155,13 @@
## Idempotency Contract
**HTTP Header ที่ต้องส่งทุก Request:**
```
Idempotency-Key: <document_number>:<batch_id>
```
**Backend Logic:**
```
IF idempotency_key EXISTS in import_transactions → RETURN HTTP 200 (no action)
ELSE → Process normally → INSERT import_transactions → RETURN HTTP 201
@@ -167,6 +176,7 @@ ELSE → Process normally → INSERT import_transactions → RETURN HTTP 201
Bypass Duplicate **Validation Error**
Hard Rules:
- ❌ Migration Token ไม่สามารถ Overwrite Revision ที่มีอยู่
- ❌ Migration Token ไม่สามารถ Delete Revision ก่อนหน้า
- ✅ Migration Token trigger Revision increment logic ตามปกติเท่านั้น
@@ -176,6 +186,7 @@ Hard Rules:
## Storage Governance (Two-Phase Storage)
**ข้อห้าม:**
```
❌ mv /data/dms/staging_ai/TCC-COR-0001.pdf /final/path/...
```
@@ -183,6 +194,7 @@ Hard Rules:
**ข้อบังคับ (Two-Phase Strategy):**
**Phase 1: Temp Upload (โดย n8n)**
```
✅ POST /api/storage/upload
(Upload ไฟล์ PDF ได้ผลลัพธ์เป็น attachment_id เช่น 1024)
@@ -190,12 +202,14 @@ Hard Rules:
```
**Phase 2: Final Commit (โดย Frontend UI -> Backend API)**
```
✅ POST /api/migration/commit_batch
body: { queue_ids: [1, 2, 3] }
```
Backend จะทำหน้าที่:
1. อ่านข้อมูลจาก `migration_review_queue` ซึ่งมี `temp_attachment_id` อยู่
2. นำ `temp_attachment_id` ไปเชื่อมกับเอกสาร (Link to `correspondence_attachments`)
3. เปลี่ยนสถานะอัพเดต `is_temporary = FALSE`
@@ -206,8 +220,8 @@ Backend จะทำหน้าที่:
## Review Queue Contract & Frontend UI
- `migration_review_queue` เป็น **Staging Table หลัก** (ไม่ auto-ingest ข้ามขั้นตอนนี้)
- ห้ามสร้าง Correspondence record จนกว่า Admin จะสั่ง Execute การ Import จากหน้าจอ
- **Approval Flow:**
- ห้ามสร้าง Correspondence record จนกว่า Admin จะสั่ง Execute การ Import จากหน้าจอ
- **Approval Flow:**
1. N8N Insert เข้า `migration_review_queue` (พร้อม `temp_attachment_id`)
2. Admin Review บน Frontend UI (ให้ความสำคัญกับการเช็ค `is_new: true` Tags)
3. Admin เลือก Rows แล้วกด **"Execute Import"**
@@ -218,6 +232,7 @@ Backend จะทำหน้าที่:
## Revision Drift Protection
ถ้า Excel มี revision column:
```
IF excel_revision != current_db_revision + 1
→ ROUTE ไป Review Queue พร้อม reason: "Revision drift"
@@ -227,20 +242,21 @@ IF excel_revision != current_db_revision + 1
## Execution Time Estimate
| Parameter | ค่า |
| -------------------- | ---------------------------- |
| Delay ระหว่าง Request | 2 วินาที |
| Inference Time (avg) | ~1 วินาที |
| เวลาต่อ Record | ~3 วินาที |
| จำนวน Record | 20,000 |
| เวลารวม | ~60,000 วินาที (~16.6 ชั่วโมง) |
| **จำนวนคืนที่ต้องใช้** | **~34 คืน** (รัน 22:0006:00) |
| Parameter | ค่า |
| ---------------------- | ------------------------------ |
| Delay ระหว่าง Request | 2 วินาที |
| Inference Time (avg) | ~1 วินาที |
| เวลาต่อ Record | ~3 วินาที |
| จำนวน Record | 20,000 |
| เวลารวม | ~60,000 วินาที (~16.6 ชั่วโมง) |
| **จำนวนคืนที่ต้องใช้** | **~34 คืน** (รัน 22:0006:00) |
---
## Encoding Normalization
ก่อน Ingestion ทุกครั้ง:
- Excel data → Convert เป็น **UTF-8**
- Filename → Normalize เป็น **NFC UTF-8** ป้องกันปัญหาภาษาไทยเพี้ยนข้าม OS
@@ -274,7 +290,7 @@ IF excel_revision != current_db_revision + 1
### 🟢 A. Infrastructure Validation
| Check | Expected | ✅ |
| Check | Expected | ✅ |
| ---------------------------- | ------------- | --- |
| Ollama `/api/tags` reachable | HTTP 200 | |
| Backend `/health` OK | HTTP 200 | |
@@ -286,7 +302,7 @@ IF excel_revision != current_db_revision + 1
### 🟢 B. Security Validation
| Check | Expected | ✅ |
| Check | Expected | ✅ |
| -------------------------------------- | -------- | --- |
| Migration Token expiry ≤ 7 days | Verified | |
| Token IP Whitelist = NAS IP only | Verified | |
@@ -298,7 +314,7 @@ IF excel_revision != current_db_revision + 1
### 🟢 C. Data Integrity Validation
| Check | Expected | ✅ |
| Check | Expected | ✅ |
| ---------------------------------------------- | -------------- | --- |
| Enum fetched from `/api/meta/categories` | Not hardcoded | |
| `Idempotency-Key` header enforced | Verified | |
@@ -309,7 +325,7 @@ IF excel_revision != current_db_revision + 1
### 🟢 D. Workflow Validation (Dry Run 20 Records)
| Check | Expected | ✅ |
| Check | Expected | ✅ |
| ---------------------------------------- | ------------ | --- |
| JSON parse success rate | > 95% | |
| Confidence distribution reasonable | Mean 0.70.9 | |
@@ -321,7 +337,7 @@ IF excel_revision != current_db_revision + 1
### 🟢 E. Performance Validation
| Check | Expected | ✅ |
| Check | Expected | ✅ |
| ------------------------------- | -------- | --- |
| 10 records processed < 1 minute | Verified | |
| GPU temp < 80°C | Verified | |
@@ -330,7 +346,7 @@ IF excel_revision != current_db_revision + 1
### 🟢 F. Rollback Test (Mandatory)
| Check | Expected | ✅ |
| Check | Expected | ✅ |
| ------------------------------------ | ----------------- | --- |
| Disable token works | is_active = false | |
| Delete `SYSTEM_IMPORT` records works | COUNT = 0 | |
@@ -343,12 +359,14 @@ IF excel_revision != current_db_revision + 1
## GO / NO-GO Criteria
**GO ถ้า:**
- A, B, C ทุก Check = PASS
- Dry run error rate < 10%
- JSON parse failure < 5%
- Revision conflict < 3%
**NO-GO ถ้า:**
- Enum mismatch (Category hardcoded)
- Idempotency ไม่ได้ implement
- Storage bypass (move file โดยตรง)
@@ -358,8 +376,8 @@ IF excel_revision != current_db_revision + 1
## Final Architectural Assessment
| Area | Status |
| ------------------ | ------------------------------------------------ |
| Area | Status |
| ------------------ | ------------------------------------------------- |
| ADR Compliance | ✅ Fully aligned |
| Security | ✅ Hardened (IP Whitelist, Rate Limit, Docker) |
| Data Integrity | ✅ Controlled (Idempotency, Revision Drift, Enum) |
@@ -368,4 +386,4 @@ IF excel_revision != current_db_revision + 1
---
*สำหรับขั้นตอนปฏิบัติงานแบบละเอียด ดูที่ `03-04-legacy-data-migration.md` และ `03-05-n8n-migration-setup-guide.md`*
_สำหรับขั้นตอนปฏิบัติงานแบบละเอียด ดูที่ `03-04-legacy-data-migration.md` และ `03-05-n8n-migration-setup-guide.md`_
@@ -5,6 +5,7 @@
**Version:** 1.8.1
**Decision Makers:** Development Team, Database Architect
**Related Documents:**
- [Data Dictionary](../03-Data-and-Storage/03-01-data-dictionary.md)
- [Database Schema](../03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql)
- [ADR-005: Technology Stack](ADR-005-technology-stack.md)
@@ -41,9 +42,11 @@
### Option 1: Replace INT with UUID as Primary Key
**Pros:**
- ✅ Opaque identifier ทุกที่
**Cons:**
- ❌ FK ทั้งหมดต้องเปลี่ยนเป็น BINARY(16) — Migration ซับซ้อนมาก
- ❌ JOIN Performance แย่ลง (16 bytes vs 4 bytes)
- ❌ InnoDB Clustered Index ไม่เรียงลำดับตาม INSERT Time (UUIDv4)
@@ -53,9 +56,11 @@
### Option 2: UUID as String Column (CHAR(36))
**Pros:**
- ✅ Human-readable
**Cons:**
- ❌ ใช้พื้นที่ 36 bytes ต่อ row (vs 16 bytes สำหรับ BINARY)
- ❌ Index ใหญ่ ช้ากว่า BINARY(16) อย่างมีนัยสำคัญ
- ❌ Collation issues กับ case-sensitivity
@@ -63,6 +68,7 @@
### Option 3: Hybrid INT + UUID (MariaDB Native) ⭐ (Selected)
**Pros:**
- ✅ INT PK ยังเป็น Internal ID → Performance ไม่เปลี่ยน
- ✅ UUID เป็น External ID → ปลอดภัย + Space-efficient (BINARY(16) ภายใน)
- ✅ ไม่ต้อง Migrate FK Relationships
@@ -71,6 +77,7 @@
- ✅ ไม่กระทบ Migration Tables (Temporary)
**Cons:**
- ❌ ต้องเพิ่ม Column ใหม่ + UNIQUE INDEX ทุก Public-Facing Table
- ❌ Application Layer ต้อง Generate UUIDv7 ตอน INSERT
- ❌ API Layer ต้อง Resolve UUID → INT สำหรับ Internal Queries
@@ -89,14 +96,14 @@
### 1. UUID Format
| Property | Value |
|----------|-------|
| **Type** | MariaDB Native `UUID` (available since 10.7) |
| **Storage** | `BINARY(16)` internally (automatic) |
| **DB Default** | `UUID()` — generates UUID v1 (time-based, fallback for seed data) |
| **App Generation** | NestJS `@BeforeInsert()` generates UUIDv7 (RFC 9562) for time-ordering |
| **Display** | Auto-converts to string format (8-4-4-4-12) — no conversion function needed |
| **Index** | `UNIQUE INDEX` on `uuid` column |
| Property | Value |
| ------------------ | --------------------------------------------------------------------------- |
| **Type** | MariaDB Native `UUID` (available since 10.7) |
| **Storage** | `BINARY(16)` internally (automatic) |
| **DB Default** | `UUID()` — generates UUID v1 (time-based, fallback for seed data) |
| **App Generation** | NestJS `@BeforeInsert()` generates UUIDv7 (RFC 9562) for time-ordering |
| **Display** | Auto-converts to string format (8-4-4-4-12) — no conversion function needed |
| **Index** | `UNIQUE INDEX` on `uuid` column |
### 2. Column Specification
@@ -116,38 +123,38 @@ UNIQUE INDEX idx_{table}_uuid (uuid)
#### Tier 1 — Core Entity Tables (Own UUID Column)
| # | Table Name | Current PK | UUID Column | Notes |
|---|-----------|-----------|-------------|-------|
| 1 | `users` | `user_id INT AI` | `uuid UUID` | User profiles |
| 2 | `organizations` | `id INT AI` | `uuid UUID` | Organization data |
| 3 | `projects` | `id INT AI` | `uuid UUID` | Project data |
| 4 | `contracts` | `id INT AI` | `uuid UUID` | Contract data |
| 5 | `correspondences` | `id INT AI` | `uuid UUID` | Main document entity |
| 6 | `correspondence_revisions` | `id INT AI` | `uuid UUID` | Document versions |
| 7 | `circulations` | `id INT AI` | `uuid UUID` | Internal circulations |
| 8 | `shop_drawings` | `id INT AI` | `uuid UUID` | Shop drawing master |
| 9 | `shop_drawing_revisions` | `id INT AI` | `uuid UUID` | Shop drawing versions |
| 10 | `contract_drawings` | `id INT AI` | `uuid UUID` | Contract drawing master |
| 11 | `asbuilt_drawings` | `id INT AI` | `uuid UUID` | As-built drawing master |
| 12 | `asbuilt_drawing_revisions` | `id INT AI` | `uuid UUID` | As-built drawing versions |
| 13 | `attachments` | `id INT AI` | `uuid UUID` | File attachments |
| 14 | `notifications` | `id INT AI` (partitioned) | `uuid UUID` | User notifications |
| # | Table Name | Current PK | UUID Column | Notes |
| --- | --------------------------- | ------------------------- | ----------- | ------------------------- |
| 1 | `users` | `user_id INT AI` | `uuid UUID` | User profiles |
| 2 | `organizations` | `id INT AI` | `uuid UUID` | Organization data |
| 3 | `projects` | `id INT AI` | `uuid UUID` | Project data |
| 4 | `contracts` | `id INT AI` | `uuid UUID` | Contract data |
| 5 | `correspondences` | `id INT AI` | `uuid UUID` | Main document entity |
| 6 | `correspondence_revisions` | `id INT AI` | `uuid UUID` | Document versions |
| 7 | `circulations` | `id INT AI` | `uuid UUID` | Internal circulations |
| 8 | `shop_drawings` | `id INT AI` | `uuid UUID` | Shop drawing master |
| 9 | `shop_drawing_revisions` | `id INT AI` | `uuid UUID` | Shop drawing versions |
| 10 | `contract_drawings` | `id INT AI` | `uuid UUID` | Contract drawing master |
| 11 | `asbuilt_drawings` | `id INT AI` | `uuid UUID` | As-built drawing master |
| 12 | `asbuilt_drawing_revisions` | `id INT AI` | `uuid UUID` | As-built drawing versions |
| 13 | `attachments` | `id INT AI` | `uuid UUID` | File attachments |
| 14 | `notifications` | `id INT AI` (partitioned) | `uuid UUID` | User notifications |
#### Tier 2 — Shared-PK Tables (Inherit UUID from Parent)
| # | Table Name | Shared PK Source | UUID Resolution |
|---|-----------|-----------------|-----------------|
| 1 | `rfas` | `correspondences.id` | Use `correspondences.uuid` |
| 2 | `rfa_revisions` | `correspondence_revisions.id` | Use `correspondence_revisions.uuid` |
| 3 | `transmittals` | `correspondences.id` | Use `correspondences.uuid` |
| # | Table Name | Shared PK Source | UUID Resolution |
| --- | --------------- | ----------------------------- | ----------------------------------- |
| 1 | `rfas` | `correspondences.id` | Use `correspondences.uuid` |
| 2 | `rfa_revisions` | `correspondence_revisions.id` | Use `correspondence_revisions.uuid` |
| 3 | `transmittals` | `correspondences.id` | Use `correspondences.uuid` |
#### Already Using UUID — No Changes Needed
| Table Name | Current PK |
|-----------|-----------|
| Table Name | Current PK |
| ---------------------- | --------------- |
| `workflow_definitions` | `CHAR(36) UUID` |
| `workflow_instances` | `CHAR(36) UUID` |
| `workflow_histories` | `CHAR(36) UUID` |
| `workflow_instances` | `CHAR(36) UUID` |
| `workflow_histories` | `CHAR(36) UUID` |
#### Excluded Tables (Internal/Master/Junction)
@@ -372,14 +379,14 @@ ALTER TABLE notifications DROP INDEX idx_notifications_uuid, DROP COLUMN uuid;
## Storage Impact Analysis
| Item | Size |
|------|------|
| UUID (BINARY(16) internal) per row | 16 bytes |
| UNIQUE INDEX per row | ~16 bytes (key) + ~6 bytes (pointer) ≈ 22 bytes |
| **Total per row** | **~38 bytes** |
| Estimated rows (all 14 tables combined) | ~100,000 (Year 1) |
| **Total additional storage** | **~3.8 MB** |
| Impact on QNAP NAS | **Negligible** |
| Item | Size |
| --------------------------------------- | ----------------------------------------------- |
| UUID (BINARY(16) internal) per row | 16 bytes |
| UNIQUE INDEX per row | ~16 bytes (key) + ~6 bytes (pointer) ≈ 22 bytes |
| **Total per row** | **~38 bytes** |
| Estimated rows (all 14 tables combined) | ~100,000 (Year 1) |
| **Total additional storage** | **~3.8 MB** |
| Impact on QNAP NAS | **Negligible** |
---
@@ -387,12 +394,12 @@ ALTER TABLE notifications DROP INDEX idx_notifications_uuid, DROP COLUMN uuid;
### UUIDv7 vs UUIDv4 for B-tree Index
| Property | UUIDv4 | UUIDv7 |
|----------|--------|--------|
| Ordering | Random | Time-ordered |
| B-tree insert | Random page splits | Sequential append |
| Index fragmentation | High | Low |
| Cache efficiency | Poor | Good |
| Property | UUIDv4 | UUIDv7 |
| ------------------- | ------------------ | ----------------- |
| Ordering | Random | Time-ordered |
| B-tree insert | Random page splits | Sequential append |
| Index fragmentation | High | Low |
| Cache efficiency | Poor | Good |
**UUIDv7 ถูกเลือกเพราะ Time-ordering** ทำให้ INSERT ไม่ทำให้เกิด Random Page Split บน InnoDB B-tree ซึ่งสำคัญมากสำหรับ QNAP NAS ที่มี I/O จำกัด
@@ -416,46 +423,50 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
## Security Benefits
| Threat | Before (INT) | After (Hybrid) |
|--------|-------------|----------------|
| ID Enumeration | ❌ Vulnerable (`/api/users/1,2,3...`) | ✅ Opaque UUID |
| Record Count Leak | ❌ `id=500` reveals ~500 records | ✅ UUID reveals nothing |
| BOLA Attack Surface | ❌ Predictable IDs | ✅ 2^122 possible values |
| Cross-System Collision | ❌ Possible | ✅ Globally unique |
| Threat | Before (INT) | After (Hybrid) |
| ---------------------- | ------------------------------------- | ------------------------ |
| ID Enumeration | ❌ Vulnerable (`/api/users/1,2,3...`) | ✅ Opaque UUID |
| Record Count Leak | ❌ `id=500` reveals ~500 records | ✅ UUID reveals nothing |
| BOLA Attack Surface | ❌ Predictable IDs | ✅ 2^122 possible values |
| Cross-System Collision | ❌ Possible | ✅ Globally unique |
---
## Compatibility with Existing ADRs
| ADR | Impact | Notes |
|-----|--------|-------|
| ADR-002 (Doc Numbering) | ✅ None | Document numbers (VARCHAR) are business identifiers, unaffected |
| ADR-005 (Tech Stack) | ✅ Compatible | uuid npm package + MariaDB native UUID type |
| ADR-006 (Redis Caching) | ⚠️ Minor | Cache keys should use UUID instead of INT for public-facing data |
| ADR-009 (DB Migration) | ✅ Compatible | ADD COLUMN is a safe, non-destructive migration |
| ADR-016 (Security) | ✅ Enhanced | Strengthens OWASP BOLA defense |
| ADR-017 (Ollama Migration) | ✅ None | Migration tables are temporary, excluded from UUID scope |
| ADR | Impact | Notes |
| -------------------------- | ------------- | ---------------------------------------------------------------- |
| ADR-002 (Doc Numbering) | ✅ None | Document numbers (VARCHAR) are business identifiers, unaffected |
| ADR-005 (Tech Stack) | ✅ Compatible | uuid npm package + MariaDB native UUID type |
| ADR-006 (Redis Caching) | ⚠️ Minor | Cache keys should use UUID instead of INT for public-facing data |
| ADR-009 (DB Migration) | ✅ Compatible | ADD COLUMN is a safe, non-destructive migration |
| ADR-016 (Security) | ✅ Enhanced | Strengthens OWASP BOLA defense |
| ADR-017 (Ollama Migration) | ✅ None | Migration tables are temporary, excluded from UUID scope |
---
## Transition Strategy
### Phase 1: Database (Schema Change)
- เพิ่ม `uuid UUID` column (MariaDB native type) กับ UNIQUE INDEX ใน 14 ตาราง
- Existing rows ได้รับ UUID อัตโนมัติจาก DB DEFAULT
### Phase 2: Backend (Dual-Mode)
- เพิ่ม `uuid` field ใน TypeORM Entities
- สร้าง `BaseUuidEntity` class
- API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern
- API Response รวม UUID เป็น `id` field
### Phase 3: Frontend (Gradual Migration)
- Frontend เปลี่ยนจากใช้ `id` (INT) เป็น `id` (UUID) ใน API response
- URL parameters เปลี่ยนเป็น UUID
- ไม่ต้อง Big-Bang migration — ค่อยๆ เปลี่ยนทีละ Module
### Phase 4: Cleanup
- ลบ INT ID จาก API Response (DTO)
- ลบ INT-based route handlers
- Update API Documentation
@@ -464,15 +475,15 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
## Final Assessment
| Area | Status |
|------|--------|
| Security | ✅ Eliminates ID enumeration |
| Performance | ✅ No impact on internal JOINs |
| Migration Risk | ✅ Low — ADD COLUMN only |
| Storage Impact | ✅ Negligible (~3.8 MB) |
| Backward Compatibility | ✅ Dual-mode transition |
| ADR Compliance | ✅ Compatible with all existing ADRs |
| Area | Status |
| ---------------------- | ------------------------------------ |
| Security | ✅ Eliminates ID enumeration |
| Performance | ✅ No impact on internal JOINs |
| Migration Risk | ✅ Low — ADD COLUMN only |
| Storage Impact | ✅ Negligible (~3.8 MB) |
| Backward Compatibility | ✅ Dual-mode transition |
| ADR Compliance | ✅ Compatible with all existing ADRs |
---
*สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน ADR-019-implementation-plan.md*
_สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน ADR-019-implementation-plan.md_
+64 -65
View File
@@ -1,5 +1,4 @@
สรุป Patch 1.8.1
---
## สรุป Patch 1.8.1
# 📘 1) Formal Spec — Version 1.8.1
@@ -14,9 +13,9 @@
Spec 1.8.1 แก้ความไม่สอดคล้องระหว่าง:
* 03-04-legacy-data-migration.md
* 03-05-n8n-migration-setup-guide.md
* ADR-017-ollama-data-migration.md
- 03-04-legacy-data-migration.md
- 03-05-n8n-migration-setup-guide.md
- ADR-017-ollama-data-migration.md
และกำหนด Production Boundary ที่ชัดเจน
@@ -26,33 +25,33 @@ Spec 1.8.1 แก้ความไม่สอดคล้องระหว่
### Infrastructure Layout
| Component | Host | Responsibility |
| ------------------ | ------------- | -------------- |
| DMS Frontend | QNAP | Production UI |
| DMS Backend | QNAP | Core API |
| MariaDB | QNAP | Authoritative DB |
| Redis | QNAP | Cache / BullMQ |
| Elasticsearch | QNAP | Full-text Search |
| Nginx Proxy Manager| QNAP | Public ingress / SSL |
| n8n + n8n-db | QNAP | Automation engine |
| Tika | QNAP | OCR / PDF extraction |
| Gitea | QNAP | Git + CI/CD |
| RocketChat | QNAP | Team communication |
| Grafana | ASUSTOR | Metrics dashboard |
| Prometheus | ASUSTOR | Metrics collection |
| Loki | ASUSTOR | Log aggregation |
| Promtail | ASUSTOR | Log shipper |
| uptime-kuma | ASUSTOR | Service availability |
| Gitea Runner | ASUSTOR | CI/CD build agent |
| Docker Registry | ASUSTOR | Image storage |
| Cloudflared | ASUSTOR | Tunnel / remote access |
| Ollama | Admin Desktop | AI processing only (i9-9900K, RTX 2060 SUPER 8GB) |
| Component | Host | Responsibility |
| ------------------- | ------------- | ------------------------------------------------- |
| DMS Frontend | QNAP | Production UI |
| DMS Backend | QNAP | Core API |
| MariaDB | QNAP | Authoritative DB |
| Redis | QNAP | Cache / BullMQ |
| Elasticsearch | QNAP | Full-text Search |
| Nginx Proxy Manager | QNAP | Public ingress / SSL |
| n8n + n8n-db | QNAP | Automation engine |
| Tika | QNAP | OCR / PDF extraction |
| Gitea | QNAP | Git + CI/CD |
| RocketChat | QNAP | Team communication |
| Grafana | ASUSTOR | Metrics dashboard |
| Prometheus | ASUSTOR | Metrics collection |
| Loki | ASUSTOR | Log aggregation |
| Promtail | ASUSTOR | Log shipper |
| uptime-kuma | ASUSTOR | Service availability |
| Gitea Runner | ASUSTOR | CI/CD build agent |
| Docker Registry | ASUSTOR | Image storage |
| Cloudflared | ASUSTOR | Tunnel / remote access |
| Ollama | Admin Desktop | AI processing only (i9-9900K, RTX 2060 SUPER 8GB) |
**Constraints:**
* Ollama MUST NOT run on QNAP (production server)
* AI containers MUST NOT access production DB directly
* n8n calls Ollama via internal VLAN HTTP only
- Ollama MUST NOT run on QNAP (production server)
- AI containers MUST NOT access production DB directly
- n8n calls Ollama via internal VLAN HTTP only
---
@@ -88,10 +87,10 @@ Migration MUST fail if required fields invalid.
Automation must:
* Check existence by rfa_number
* Validate file hash
* UPDATE instead of INSERT if exists
* Prevent duplicate revision chain
- Check existence by rfa_number
- Validate file hash
- UPDATE instead of INSERT if exists
- Prevent duplicate revision chain
---
@@ -171,10 +170,10 @@ No DB commit until validation approved.
AI-based migration using Ollama introduces:
* DB corruption risk
* Hallucinated metadata
* Unauthorized modification
* Privilege escalation risk
- DB corruption risk
- Hallucinated metadata
- Unauthorized modification
- Privilege escalation risk
Production DMS must remain authoritative.
@@ -186,11 +185,11 @@ Production DMS must remain authoritative.
Ollama must:
* Run on **Admin Desktop only** (NOT on QNAP)
* Have NO DB credentials
* Have NO write access to uploads
* Access only `/staging_ai`
* Output JSON only
- Run on **Admin Desktop only** (NOT on QNAP)
- Have NO DB credentials
- Have NO write access to uploads
- Access only `/staging_ai`
- Output JSON only
---
@@ -212,9 +211,9 @@ AI never writes directly.
All writes must go through:
* Authenticated DMS API
* RBAC enforced
* Audit log recorded
- Authenticated DMS API
- RBAC enforced
- Audit log recorded
---
@@ -222,10 +221,10 @@ All writes must go through:
AI output must:
* Match schema
* Pass validation script
* Fail on missing required fields
* Reject unknown users
- Match schema
- Pass validation script
- Fail on missing required fields
- Reject unknown users
---
@@ -244,14 +243,14 @@ AI output must:
Pros:
* Production safe
* Predictable migration
* Audit trail preserved
- Production safe
- Predictable migration
- Audit trail preserved
Cons:
* Slightly slower pipeline
* Requires validation layer
- Slightly slower pipeline
- Requires validation layer
---
@@ -288,7 +287,7 @@ Cons:
Batch size recommendation:
* 2050 RFAs per batch
- 2050 RFAs per batch
Process:
@@ -330,11 +329,11 @@ When all batches pass:
Monitor:
* DB errors
* Duplicate insert
* Missing files
* AI extraction errors
* API error rate
- DB errors
- Duplicate insert
- Missing files
- AI extraction errors
- API error rate
If anomaly >5% → trigger rollback plan.
@@ -368,11 +367,11 @@ Target RTO: < 2 hours
System may go live only if:
* All dry-run tests pass
* 100% required fields valid
* 0 duplicate RFA
* Sample QA pass >95%
* Backup verified
- All dry-run tests pass
- 100% required fields valid
- 0 duplicate RFA
- Sample QA pass >95%
- Backup verified
---
+19 -19
View File
@@ -28,42 +28,42 @@ Architecture Decision Records (ADRs) เป็นเอกสารที่บ
### Core Architecture Decisions
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | --------------------------- | ---------- | ---------- | ------------------------------------------------------------------------- |
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | --------------------------- | ----------- | ---------- | ---------------------------------------------------------------------------- |
| [ADR-001](./ADR-001-unified-workflow-engine.md) | Unified Workflow Engine | ✅ Accepted | 2026-02-24 | ใช้ DSL-based Workflow Engine สำหรับ Correspondences, RFAs, และ Circulations |
| [ADR-002](./ADR-002-document-numbering-strategy.md) | Document Numbering Strategy | ✅ Accepted | 2026-02-24 | Double-lock mechanism (Redis + DB Optimistic Lock) สำหรับเลขที่เอกสาร |
| [ADR-002](./ADR-002-document-numbering-strategy.md) | Document Numbering Strategy | ✅ Accepted | 2026-02-24 | Double-lock mechanism (Redis + DB Optimistic Lock) สำหรับเลขที่เอกสาร |
### Security & Access Control
| ADR | Title | Status | Date | Summary |
| ----------------------------------------------- | ---------------------------------- | ---------- | ---------- | -------------------------------------------- |
| ADR | Title | Status | Date | Summary |
| ----------------------------------------------- | ---------------------------------- | ----------- | ---------- | -------------------------------------------- |
| [ADR-016](./ADR-016-security-authentication.md) | Security & Authentication Strategy | ✅ Accepted | 2026-02-24 | JWT + bcrypt + OWASP Security Best Practices |
### Technology & Infrastructure
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ------------------------------------ | -------------------- | ---------- | ------------------------------------------------------------ |
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ------------------------------------ | --------------------- | ---------- | --------------------------------------------------------------- |
| [ADR-005](./ADR-005-technology-stack.md) | Technology Stack Selection | ✅ Accepted | 2026-02-24 | Full Stack TypeScript: NestJS 11 + Next.js 16 + MariaDB + Redis |
| [ADR-006](./ADR-006-redis-caching-strategy.md) | Redis Usage & Caching Strategy | ✅ Accepted | 2026-02-24 | Redis สำหรับ Distributed Lock, Cache, Queue, และ Rate Limiting |
| [ADR-009](./ADR-009-database-migration-strategy.md) | Database Migration & Deployment | ✅ Accepted (Pending) | 2026-02-24 | TypeORM Migrations พร้อม Blue-Green Deployment |
| [ADR-015](./ADR-015-deployment-infrastructure.md) | Deployment & Infrastructure Strategy | ✅ Accepted | 2026-02-24 | Docker Compose with Blue-Green Deployment on QNAP |
| [ADR-006](./ADR-006-redis-caching-strategy.md) | Redis Usage & Caching Strategy | ✅ Accepted | 2026-02-24 | Redis สำหรับ Distributed Lock, Cache, Queue, และ Rate Limiting |
| [ADR-009](./ADR-009-database-migration-strategy.md) | Database Migration & Deployment | ✅ Accepted (Pending) | 2026-02-24 | TypeORM Migrations พร้อม Blue-Green Deployment |
| [ADR-015](./ADR-015-deployment-infrastructure.md) | Deployment & Infrastructure Strategy | ✅ Accepted | 2026-02-24 | Docker Compose with Blue-Green Deployment on QNAP |
### API & Integration
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | --------------------------- | ---------- | --------------------------------------------------------------------------- |
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | ---------------------------- | ---------- | ----------------------------------------------------------------------------- |
| [ADR-008](./ADR-008-email-notification-strategy.md) | Email & Notification Strategy | ✅ Accepted (Pending Review) | 2026-02-24 | BullMQ + Redis Queue สำหรับ Multi-channel Notifications (Email, LINE, In-app) |
### Observability
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | -------------------- | ---------- | ------------------------------------------------------------ |
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | --------------------- | ---------- | ------------------------------------------------------------- |
| [ADR-010](./ADR-010-logging-monitoring-strategy.md) | Logging & Monitoring Strategy | ✅ Accepted (Pending) | 2026-02-24 | Winston Structured Logging พร้อม Future ELK Stack Integration |
### Frontend Architecture
| ADR | Title | Status | Date | Summary |
| ------------------------------------------------ | -------------------------------- | ---------- | ---------- | ----------------------------------------------------- |
| ADR | Title | Status | Date | Summary |
| ------------------------------------------------ | -------------------------------- | ----------- | ---------- | ----------------------------------------------------- |
| [ADR-011](./ADR-011-nextjs-app-router.md) | Next.js App Router & Routing | ✅ Accepted | 2025-12-01 | App Router with Server Components and Nested Layouts |
| [ADR-012](./ADR-012-ui-component-library.md) | UI Component Library (Shadcn/UI) | ✅ Accepted | 2026-02-24 | Shadcn/UI + Tailwind CSS for Full Component Ownership |
| [ADR-013](./ADR-013-form-handling-validation.md) | Form Handling & Validation | ✅ Accepted | 2026-02-24 | React Hook Form + Zod for Type-Safe Forms |
@@ -71,9 +71,9 @@ Architecture Decision Records (ADRs) เป็นเอกสารที่บ
### Data & Identity
| ADR | Title | Status | Date | Summary |
| ------------------------------------------------------------ | ---------------------------- | ---------- | ---------- | -------------------------------------------------------- |
| [ADR-019](./ADR-019-hybrid-identifier-strategy.md) | Hybrid Identifier Strategy | ✅ Accepted | 2026-03-11 | INT PK (internal) + UUIDv7 (public API) on 14 tables |
| ADR | Title | Status | Date | Summary |
| -------------------------------------------------- | -------------------------- | ----------- | ---------- | ---------------------------------------------------- |
| [ADR-019](./ADR-019-hybrid-identifier-strategy.md) | Hybrid Identifier Strategy | ✅ Accepted | 2026-03-11 | INT PK (internal) + UUIDv7 (public API) on 14 tables |
---