251121:1700 Backend T3 wait testt
This commit is contained in:
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -1,11 +1,10 @@
|
||||
{ "recommendations": [
|
||||
{ "recommendations": [
|
||||
"aaron-bond.better-comments",
|
||||
"anbuselvanrocky.bootstrap5-vscode",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"christian-kohler.path-intellisense",
|
||||
"codezombiech.gitignore",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"dsznajder.es7-react-js-snippets",
|
||||
"dunstontc.vscode-docker-syntax",
|
||||
@@ -18,7 +17,6 @@
|
||||
"formulahendry.auto-rename-tag",
|
||||
"github.copilot",
|
||||
"github.copilot-chat",
|
||||
"google.geminicodeassist",
|
||||
"hansuxdev.bootstrap5-snippets",
|
||||
"heybourn.headwind",
|
||||
"humao.rest-client",
|
||||
@@ -34,7 +32,6 @@
|
||||
"mikestead.dotenv",
|
||||
"ms-azuretools.vscode-containers",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"ms-edgedevtools.vscode-edge-devtools",
|
||||
"ms-python.debugpy",
|
||||
"ms-python.python",
|
||||
"ms-vscode-remote.remote-containers",
|
||||
|
||||
25
.vscode/nap-dms.lcbp3.code-workspace
vendored
Normal file
25
.vscode/nap-dms.lcbp3.code-workspace
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"mysqlOptions": {
|
||||
"authProtocol": "default",
|
||||
"enableSsl": "Disabled"
|
||||
},
|
||||
"ssh": "Disabled",
|
||||
"previewLimit": 50,
|
||||
"server": "localhost",
|
||||
"port": 3306,
|
||||
"driver": "MySQL",
|
||||
"name": "lcbp3_dev",
|
||||
"database": "lcbp3_dev",
|
||||
"username": "root"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@@ -8,5 +8,21 @@
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"mysqlOptions": {
|
||||
"authProtocol": "default",
|
||||
"enableSsl": "Disabled"
|
||||
},
|
||||
"ssh": "Disabled",
|
||||
"previewLimit": 50,
|
||||
"server": "localhost",
|
||||
"port": 3306,
|
||||
"driver": "MySQL",
|
||||
"name": "lcbp3_dev",
|
||||
"database": "lcbp3_dev",
|
||||
"username": "root"
|
||||
}
|
||||
]
|
||||
}
|
||||
81
.vscode/tasks.json
vendored
81
.vscode/tasks.json
vendored
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Backend: Dev",
|
||||
"type": "shell",
|
||||
"command": "pnpm run start:dev",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Backend: Build",
|
||||
"type": "shell",
|
||||
"command": "pnpm run build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
},
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Backend: Lint",
|
||||
"type": "shell",
|
||||
"command": "pnpm run lint",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
},
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Backend: Test",
|
||||
"type": "shell",
|
||||
"command": "pnpm run test",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
},
|
||||
"group": "test",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Backend: Test E2E",
|
||||
"type": "shell",
|
||||
"command": "pnpm run test:e2e",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
},
|
||||
"group": "test",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -339,32 +339,55 @@ CREATE TABLE users (
|
||||
SET NULL
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)';
|
||||
-- Initial SUPER_ADMIN user
|
||||
INSERT INTO users (username, password_hash, email, is_active)
|
||||
INSERT INTO `users` (
|
||||
`user_id`,
|
||||
`username`,
|
||||
`password_hash`,
|
||||
`first_name`,
|
||||
`last_name`,
|
||||
`email`,
|
||||
`line_id`,
|
||||
`primary_organization_id`
|
||||
)
|
||||
VALUES (
|
||||
1,
|
||||
'superadmin',
|
||||
'$2y$10$0kjBMxWq7E4G7P.dc8r5i.cjiPBiup553AsFpDfxUt31gKg9h',
|
||||
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
|
||||
'Super',
|
||||
'Admin',
|
||||
'superadmin @example.com',
|
||||
NULL,
|
||||
NULL
|
||||
),
|
||||
(
|
||||
2,
|
||||
'admin',
|
||||
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
|
||||
'Admin',
|
||||
'คคง.',
|
||||
'admin@example.com',
|
||||
NULL,
|
||||
1
|
||||
) ON DUPLICATE KEY
|
||||
UPDATE email =
|
||||
VALUES(email),
|
||||
is_active =
|
||||
VALUES(is_active);
|
||||
-- Create editor01 user
|
||||
INSERT IGNORE INTO users (username, password_hash, email, is_active)
|
||||
VALUES (
|
||||
),
|
||||
(
|
||||
3,
|
||||
'editor01',
|
||||
'$2y$10$0kjBMxWq7E4G7P.dc8r5i.cjiPBiup553AsFpDfxUt31gKg9h',
|
||||
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
|
||||
'DC',
|
||||
'C1',
|
||||
'editor01 @example.com',
|
||||
1
|
||||
);
|
||||
-- Create viewer01 user (password hash placeholder, must change later)
|
||||
INSERT IGNORE INTO users (username, password_hash, email, is_active)
|
||||
VALUES (
|
||||
NULL,
|
||||
41
|
||||
),
|
||||
(
|
||||
4,
|
||||
'viewer01',
|
||||
'$2y$10$0kjBMxWq7E4G7P.dc8r5i.cjiPBiup553AsFpDfxUt31gKg9h',
|
||||
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
|
||||
'Viewer',
|
||||
'สคฉ.03',
|
||||
'viewer01 @example.com',
|
||||
1
|
||||
NULL,
|
||||
10
|
||||
);
|
||||
-- ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ
|
||||
CREATE TABLE roles (
|
||||
@@ -903,6 +926,33 @@ CREATE TABLE user_assignments (
|
||||
) -- สำหรับ Global scope
|
||||
)
|
||||
);
|
||||
INSERT INTO `user_assignments` (
|
||||
`id`,
|
||||
`user_id`,
|
||||
`role_id`,
|
||||
`organization_id`,
|
||||
`project_id`,
|
||||
`contract_id`,
|
||||
`assigned_by_user_id`
|
||||
)
|
||||
VALUES (
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
),
|
||||
(
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
1,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
CREATE TABLE project_organizations (
|
||||
project_id INT NOT NULL,
|
||||
organization_id INT NOT NULL,
|
||||
@@ -1127,12 +1177,7 @@ VALUES ('RFA', 'Request for Approval', 1, 1),
|
||||
('MEMO', 'Memorandum', 7, 1),
|
||||
('MOM', 'Minutes of Meeting', 8, 1),
|
||||
('NOTICE', 'Notice', 9, 1),
|
||||
(
|
||||
'OTHER',
|
||||
'Other',
|
||||
10,
|
||||
1
|
||||
);
|
||||
('OTHER', 'Other', 10, 1);
|
||||
-- ตาราง Master เก็บสถานะของเอกสาร
|
||||
CREATE TABLE correspondence_status (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||
|
||||
772
Documnets/Project/0_Requirements_V1_4_3.md
Normal file
772
Documnets/Project/0_Requirements_V1_4_3.md
Normal file
@@ -0,0 +1,772 @@
|
||||
# 📝 **Documents Management System Version 1.4.2: Application Requirements Specification**
|
||||
|
||||
**สถานะ:** FINAL
|
||||
**วันที่:** 2025-11-19
|
||||
**อ้างอิงพื้นฐาน:** v1.4.2
|
||||
**Classification:** Internal Technical Documentation
|
||||
|
||||
## 📌 **1. วัตถุประสงค์**
|
||||
|
||||
สร้างเว็บแอปพลิเคชันสำหรับระบบบริหารจัดการเอกสารโครงการ (Document Management System - DMS) แบบครบวงจร ที่เน้นความปลอดภัยสูงสุด ความถูกต้องของข้อมูล (Data Integrity) และรองรับการขยายตัวในอนาคต (Scalability) โดยแก้ไขปัญหา Race Condition และเพิ่มความเสถียรในการจัดการไฟล์และ Workflow
|
||||
|
||||
- มีฟังก์ชันหลักในการอัปโหลด จัดเก็บ ค้นหา แชร์ และควบคุมสิทธิ์การเข้าถึงเอกสาร
|
||||
- ช่วยลดการใช้เอกสารกระดาษ เพิ่มความปลอดภัยในการจัดเก็บข้อมูล
|
||||
- เพิ่มความสะดวกในการทำงานร่วมกันระหว่างองกรณ์
|
||||
- **เสริม:** ปรับปรุงความปลอดภัยของระบบด้วยมาตรการป้องกันที่ทันสมัย
|
||||
- **เสริม:** เพิ่มความทนทานของระบบด้วยกลไก resilience patterns
|
||||
- **เสริม:** สร้างระบบ monitoring และ observability ที่ครอบคลุม
|
||||
|
||||
## 🛠️ **2. สถาปัตยกรรมและเทคโนโลยี (System Architecture & Technology Stack)**
|
||||
|
||||
ใช้สถาปัตยกรรมแบบ Headless/API-First ที่ทันสมัย ทำงานทั้งหมดบน QNAP Server ผ่าน Container Station เพื่อความสะดวกในการจัดการและบำรุงรักษา
|
||||
|
||||
**Domain:** `np-dms.work`, `www.np-dms.work`
|
||||
**IP:** 159.192.126.103
|
||||
**Docker Network:** ทุก Service จะเชื่อมต่อผ่านเครือข่ายกลางชื่อ `lcbp3` เพื่อให้สามารถสื่อสารกันได้
|
||||
|
||||
### **2.1 Infrastructure & Environment:**
|
||||
|
||||
- **Server:** QNAP (Model: TS-473A, RAM: 32GB, CPU: AMD Ryzen V1500B)
|
||||
- **Containerization:** Container Station (Docker & Docker Compose) ใช้ UI ของ Container Station เป็นหลัก ในการ configuration และการรัน docker command
|
||||
- **Development Environment:** VS Code/Cursor on Windows 11
|
||||
- **Data Storage:** `/share/dms-data` บน QNAP
|
||||
- **ข้อจำกัด:** ไม่สามารถใช้ .env ในการกำหนดตัวแปรภายนอกได้ ต้องกำหนดใน docker-compose.yml เท่านั้น
|
||||
|
||||
### **2.2 การจัดการ Configuration (ปรับปรุง):**
|
||||
|
||||
- ใช้ `docker-compose.yml` สำหรับ environment variables ตามข้อจำกัดของ QNAP
|
||||
- **Secrets Management (ใหม่):**
|
||||
- ห้ามระบุ Sensitive Secrets (Password, Keys) ใน `docker-compose.yml` หลัก
|
||||
- ต้องใช้ไฟล์ `docker-compose.override.yml` (ที่ถูก gitignore) สำหรับ Inject Environment Variables ที่เป็นความลับในแต่ละ Environment (Dev/Prod)
|
||||
- ไฟล์ `docker-compose.yml` หลักให้ใส่ค่า Dummy หรือว่างไว้
|
||||
- **แต่ต้องมี mechanism สำหรับจัดการ sensitive secrets อย่างปลอดภัย** โดยใช้:
|
||||
- Docker secrets (ถ้ารองรับ)
|
||||
- External secret management (Hashicorp Vault) หรือ
|
||||
- Encrypted environment variables
|
||||
- Development environment ยังใช้ .env ได้ แต่ต้องไม่ commit เข้า version control
|
||||
- ต้องมี configuration validation during application startup
|
||||
- ต้องแยก configuration ตาม environment (development, staging, production)
|
||||
|
||||
### **2.3 Core Services:**
|
||||
|
||||
- **Code Hosting:** Gitea (Self-hosted on QNAP)
|
||||
- Application name: git
|
||||
- Service name: gitea
|
||||
- Domain: `git.np-dms.work`
|
||||
- หน้าที่: เป็นศูนย์กลางในการเก็บและจัดการเวอร์ชันของโค้ด (Source Code) สำหรับทุกส่วน
|
||||
|
||||
- **Backend / Data Platform:** NestJS
|
||||
- Application name: lcbp3-backend
|
||||
- Service name: backend
|
||||
- Domain: `backend.np-dms.work`
|
||||
- Framework: NestJS (Node.js, TypeScript, ESM)
|
||||
- หน้าที่: จัดการโครงสร้างข้อมูล (Data Models), สร้าง API, จัดการสิทธิ์ผู้ใช้ (Roles & Permissions), และสร้าง Workflow ทั้งหมดของระบบ
|
||||
|
||||
- **Database:** MariaDB 10.11
|
||||
- Application name: lcbp3-db
|
||||
- Service name: mariadb
|
||||
- Domain: `db.np-dms.work`
|
||||
- หน้าที่: ฐานข้อมูลหลักสำหรับเก็บข้อมูลทั้งหมด
|
||||
- Tooling: DBeaver (Community Edition), phpmyadmin สำหรับการออกแบบและจัดการฐานข้อมูล
|
||||
|
||||
- **Database Management:** phpMyAdmin
|
||||
- Application name: lcbp3-db
|
||||
- Service: phpmyadmin:5-apache
|
||||
- Service name: pma
|
||||
- Domain: `pma.np-dms.work`
|
||||
- หน้าที่: จัดการฐานข้อมูล mariadb ผ่าน Web UI
|
||||
|
||||
- **Frontend:** Next.js
|
||||
- Application name: lcbp3-frontend
|
||||
- Service name: frontend
|
||||
- Domain: `lcbp3.np-dms.work`
|
||||
- Framework: Next.js (App Router, React, TypeScript, ESM)
|
||||
- Styling: Tailwind CSS + PostCSS
|
||||
- Component Library: shadcn/ui
|
||||
- หน้าที่: สร้างหน้าตาเว็บแอปพลิเคชันสำหรับให้ผู้ใช้งานเข้ามาดู Dashboard, จัดการเอกสาร, และติดตามงาน โดยจะสื่อสารกับ Backend ผ่าน API
|
||||
|
||||
- **Workflow Automation:** n8n
|
||||
- Application name: lcbp3-n8n
|
||||
- Service: n8nio/n8n:latest
|
||||
- Service name: n8n
|
||||
- Domain: `n8n.np-dms.work`
|
||||
- หน้าที่: จัดการ workflow ระหว่าง Backend และ Line
|
||||
|
||||
- **Reverse Proxy:** Nginx Proxy Manager
|
||||
- Application name: lcbp3-npm
|
||||
- Service: Nginx Proxy Manager (nginx-proxy-manage: latest)
|
||||
- Service name: npm
|
||||
- Domain: `npm.np-dms.work`
|
||||
- หน้าที่: เป็นด่านหน้าในการรับ-ส่งข้อมูล จัดการโดเมนทั้งหมด, ทำหน้าที่เป็น Proxy ชี้ไปยัง Service ที่ถูกต้อง, และจัดการ SSL Certificate (HTTPS) ให้อัตโนมัติ
|
||||
|
||||
- **Search Engine:** Elasticsearch
|
||||
- **Cache:** Redis
|
||||
|
||||
### **2.4 Business Logic & Consistency (ปรับปรุง):**
|
||||
|
||||
- **2.4.1 ตรรกะทางธุรกิจที่ซับซ้อนทั้งหมด** (เช่น การเปลี่ยนสถานะ Workflow [cite: 3.5.4, 3.6.5], การบังคับใช้สิทธิ์ [cite: 4.4], การตรวจสอบ Deadline [cite: 3.2.5]) **จะถูกจัดการในฝั่ง Backend (NestJS)** [cite: 2.3] เพื่อให้สามารถบำรุงรักษาและทดสอบได้ง่าย (Testability)
|
||||
|
||||
- **2.4.2 Unified Workflow Engine (ใหม่):** รวม Logic การเดินเอกสารของ `CorrespondenceRouting` และ `RfaWorkflow` ให้ใช้ Core Engine เดียวกันเพื่อลดความซ้ำซ้อนและง่ายต่อการบำรุงรักษา
|
||||
|
||||
- **2.4.3 Idempotency Keys (ใหม่):** API ที่สำคัญ (เช่น Submit Document, Approve) ต้องบังคับส่ง Header `Idempotency-Key` เพื่อป้องกันการทำรายการซ้ำจากการกดปุ่มรัวๆ หรือ Network Retry
|
||||
|
||||
- **2.4.4 Optimistic Locking (ใหม่):** ใช้ Version Column ใน Database ควบคู่กับ Redis Lock สำหรับการสร้างเลขที่เอกสาร เพื่อเป็น Safety Net ชั้นสุดท้าย
|
||||
|
||||
- **2.4.5** **จะไม่มีการใช้ SQL Triggers** เพื่อป้องกันตรรกะซ่อนเร้น (Hidden Logic) และความซับซ้อนในการดีบัก
|
||||
|
||||
### **2.5 Data Migration และ Schema Versioning:**
|
||||
|
||||
- ต้องมี database migration scripts สำหรับทุก schema change โดยใช้ TypeORM migrations
|
||||
- ต้องรองรับ rollback ของ migration ได้
|
||||
- ต้องมี data seeding strategy สำหรับ environment ต่างๆ (development, staging, production)
|
||||
- ต้องมี version compatibility between schema versions
|
||||
- Migration scripts ต้องผ่านการทดสอบใน staging environment ก่อน production
|
||||
- ต้องมี database backup ก่อนทำ migration ใน production
|
||||
|
||||
### **2.6 กลยุทธ์ความทนทานและการจัดการข้อผิดพลาด (Resilience & Error Handling Strategy)**
|
||||
|
||||
- 2.6.1 Circuit Breaker Pattern: ใช้สำหรับ external service calls (Email, LINE, Elasticsearch)
|
||||
- 2.6.2 Retry Mechanism: ด้วย exponential backoff สำหรับ transient failures
|
||||
- 2.6.3 Fallback Strategies: Graceful degradation เมื่อบริการภายนอกล้มเหลว
|
||||
- 2.6.4 Error Handling: Error messages ต้องไม่เปิดเผยข้อมูล sensitive
|
||||
- 2.6.5 Monitoring: Centralized error monitoring และ alerting system
|
||||
|
||||
## **📦 3. ข้อกำหนดด้านฟังก์ชันการทำงาน (Functional Requirements)**
|
||||
|
||||
### **3.1. การจัดการโครงสร้างโครงการและองค์กร**
|
||||
|
||||
- 3.1.1. โครงการ (Projects): ระบบต้องสามารถจัดการเอกสารภายในหลายโครงการได้ (ปัจจุบันมี 4 โครงการ และจะเพิ่มขึ้นในอนาคต)
|
||||
- 3.1.2. สัญญา (Contracts): ระบบต้องสามารถจัดการเอกสารภายในแต่ละสัญญาได้ ในแต่ละโครงการ มีได้หลายสัญญา หรืออย่างน้อย 1 สัญญา
|
||||
- 3.1.3. องค์กร (Organizations):
|
||||
- มีหลายองค์กรในโครงการ องค์กรณ์ที่เป็น Owner, Designer และ Consultant สามารถอยู่ในหลายโครงการและหลายสัญญาได้
|
||||
- Contractor จะถือ 1 สัญญา และอยู่ใน 1 โครงการเท่านั้น
|
||||
|
||||
### **3.2. การจัดการเอกสารโต้ตอบ (Correspondence Management)**
|
||||
|
||||
- 3.2.1. วัตถุประสงค์: เอกสารโต้ตอบ (correspondences) ระหว่างองกรณื-องกรณ์ ภายใน โครงการ (Projects) และระหว่าง องค์กร-องค์กร ภายนอก โครงการ (Projects), รองรับ To (ผู้รับหลัก) และ CC (ผู้รับสำเนา) หลายองค์กร
|
||||
- 3.2.2. ประเภทเอกสาร: ระบบต้องรองรับเอกสารรูปแบบ ไฟล์ PDF หลายประเภท (Types) เช่น จดหมาย (Letter), อีเมล์ (Email), Request for Information (RFI), และสามารถเพิ่มประเภทใหม่ได้ในภายหลัง
|
||||
- 3.2.3. การสร้างเอกสาร (Correspondence):
|
||||
- ผู้ใช้ที่มีสิทธิ์ (เช่น Document Control) สามารถสร้างเอกสารรอไว้ในสถานะ ฉบับร่าง" (Draft) ได้ ซึ่งผู้ใช้งานต่างองค์กรจะมองไม่เห็น
|
||||
- เมื่อกด "Submitted" แล้ว การแก้ไข, ถอนเอกสารกลับไปสถานะ Draft, หรือยกเลิก (Cancel) จะต้องทำโดยผู้ใช้ระดับ Admin ขึ้นไป พร้อมระบุเหตุผล
|
||||
- 3.2.4. การอ้างอิงและจัดกลุ่ม:
|
||||
- เอกสารสามารถอ้างถึง (Reference) เอกสารฉบับก่อนหน้าได้หลายฉบับ
|
||||
- สามารถกำหนด Tag ได้หลาย Tag เพื่อจัดกลุ่มและใช้ในการค้นหาขั้นสูง
|
||||
- 3.2.5. Correspondence Routing & Workflow
|
||||
- 3.2.5.1 Routing Templates (แม่แบบการส่งต่อ)
|
||||
- ผู้ดูแลระบบต้องสามารถสร้างแม่แบบการส่งต่อได้
|
||||
- แม่แบบสามารถเป็นแบบทั่วไป (ใช้ได้ทุกโครงการ) หรือเฉพาะโครงการ
|
||||
- แต่ละแม่แบบประกอบด้วยลำดับขั้นตอนการส่งต่อ
|
||||
- การส่งจาก Originator -> Organization 1 -> Organization 2 -> Organization 3 แล้วส่งผลกลับตามลำดับเดิม (โดยถ้า องกรณ์ใดใน Wouting ให้ส่งกลับ ก็สามารถส่งผลกลับตามลำดับเดิมโดยไม่ต้องรอให้ถึง องกรณืในลำดับถัดไป)
|
||||
- 3.2.5.2 Routing Steps (ขั้นตอนการส่งต่อ) แต่ละขั้นตอนในแม่แบบต้องกำหนด:
|
||||
- **ลำดับขั้นตอน** (Sequence)
|
||||
- **องค์กรผู้รับ** (To Organization)
|
||||
- **วัตถุประสงค์** (Purpose): เพื่ออนุมัติ (FOR_APPROVAL), เพื่อตรวจสอบ (FOR_REVIEW), เพื่อทราบ (FOR_INFORMATION), เพื่อดำเนินการ (FOR_ACTION)
|
||||
- **ระยะเวลาที่คาดหวัง** (Expected Duration)
|
||||
- 3.2.5.3 Actual Routing Execution (การส่งต่อจริง) เมื่อสร้างเอกสารและเลือกใช้แม่แบบ ระบบต้อง:
|
||||
- สร้างลำดับการส่งต่อตามแม่แบบ
|
||||
- ติดตามสถานะของแต่ละขั้นตอน: ส่งแล้ว (SENT), กำลังดำเนินการ (IN_PROGRESS), ดำเนินการแล้ว (ACTIONED), ส่งต่อแล้ว (FORWARDED), ตอบกลับแล้ว (REPLIED)
|
||||
- ระบุวันครบกำหนด (Due Date) สำหรับแต่ละขั้นตอน
|
||||
- บันทึกผู้ดำเนินการและเวลาที่ดำเนินการ
|
||||
- 3.2.5.4 Routing Flexibility (ความยืดหยุ่น)
|
||||
- สามารถข้ามขั้นตอนได้ในกรณีพิเศษ (โดยผู้มีสิทธิ์)
|
||||
- สามารถส่งกลับขั้นตอนก่อนหน้าได้
|
||||
- สามารถเพิ่มความคิดเห็นในแต่ละขั้นตอน
|
||||
- แจ้งเตือนอัตโนมัติเมื่อถึงขั้นตอนใหม่หรือใกล้ครบกำหนด
|
||||
- 3.2.6. การจัดการ: มีการจัดการอย่างน้อยดังนี้
|
||||
- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบของ องกรณ์ ที่เป็นผู้รับได้
|
||||
- มีระบบแจ้งเตือน ให้ผู้รับผิดชอบขององกรณ์ที่เป็น ผู้รับ/ผู้ส่ง ทราบ เมื่อมีเอกสารใหม่ หรือมีการเปลี่ยนสถานะ
|
||||
|
||||
### **3.3. การจัดกาแบบคู่สัญญา (Contract Drawing)**
|
||||
|
||||
- 3.3.1. วัตถุประสงค์: แบบคู่สัญญา (Contract Drawing) ใช้เพื่ออ้างอิงและใช้ในการตรวจสอบ
|
||||
- 3.3.2. ประเภทเอกสาร: ไฟล์ PDF
|
||||
- 3.3.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
|
||||
- 3.3.4. การอ้างอิงและจัดกลุ่ม: ใช้สำหรับอ้างอิง ใน Shop Drawings, มีการจัดหมวดหมู่ของ Contract Drawing
|
||||
|
||||
### **3.4. การจัดกาแบบก่อสร้าง (Shop Drawing)**
|
||||
|
||||
- 3.4.1. วัตถุประสงค์: แบบก่อสร้าง (Shop Drawing) ใช้เในการตรวจสอบ โดยจัดส่งด้วย Request for Approval (RFA)
|
||||
- 3.4.2. ประเภทเอกสาร: ไฟล์ PDF
|
||||
- 3.4.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
|
||||
- 3.4.4. การอ้างอิงและจัดกลุ่ม: ช้สำหรับอ้างอิง ใน Shop Drawings, มีการจัดหมวดหมู่ของ Shop Drawings
|
||||
|
||||
### **3.5. การจัดการเอกสารขออนุมัติ (Request for Approval & Workflow)**
|
||||
|
||||
- 3.5.1. วัตถุประสงค์: เอกสารขออนุมัติ (Request for Approval) ใช้ในการส่งเอกสารเพิอขออนุมัติ
|
||||
- 3.5.2. ประเภทเอกสาร: Request for Approval (RFA) เป็นชนิดหนึ่งของ Correspondence ที่มีลักษณะเฉพาะที่ต้องได้รับการอนุมัติ มีประเภทดังนี้:
|
||||
- Request for Drawing Approval (RFA_DWG)
|
||||
- Request for Document Approval (RFA_DOC)
|
||||
- Request for Method statement Approval (RFA_MES)
|
||||
- Request for Material Approval (RFA_MAT)
|
||||
- 3.5.2. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
|
||||
- 3.5.4. การอ้างอิงและจัดกลุ่ม: การจัดการ Drawing (RFA_DWG):
|
||||
- เอกสาร RFA_DWG จะประกอบไปด้วย Shop Drawing (shop_drawings) หลายแผ่น ซึ่งแต่ละแผ่นมี Revision ของตัวเอง
|
||||
- Shop Drawing แต่ละ Revision สามารถอ้างอิงถึง Contract Drawing (Ccontract_drawings) หลายแผ่น หรือไม่อ้างถึงก็ได้
|
||||
- ระบบต้องมีส่วนสำหรับจัดการข้อมูล Master Data ของทั้ง Shop Drawing และ Contract Drawing แยกจากกัน
|
||||
- 3.5.5. Workflow การอนุมัติ: ต้องรองรับกระบวนการอนุมัติที่ซับซ้อนและเป็นลำดับ เช่น
|
||||
- ส่งจาก Originator -> Organization 1 -> Organization 2 -> Organization 3 แล้วส่งผลกลับตามลำดับเดิม (โดยถ้า องกรณ์ใดใน Workflow ให้ส่งกลับ ก็สามารถส่งผลกลับตามลำดับเดิมโดยไม่ต้องรอให้ถึง องกรณืในลำดับถัดไป)
|
||||
- 3.5.6. การจัดการ: มีการจัดการอย่างน้อยดังนี้
|
||||
- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบของ องกรณ์ ที่อยู่ใน Workflow ได้
|
||||
- มีระบบแจ้งเตือน ให้ผู้รับผิดชอบของ องกรณ์ ที่อยู่ใน Workflow ทราบ เมื่อมี RFA ใหม่ หรือมีการเปลี่ยนสถานะ
|
||||
|
||||
### **3.6.การจัดการเอกสารนำส่ง (Transmittals)**
|
||||
|
||||
- 3.6.1. วัตถุประสงค์: เอกสารนำส่ง ใช้สำหรับ นำส่ง Request for Approval (RFAS) หลายฉบับ ไปยังองค์กรอื่น
|
||||
- 3.6.2. ประเภทเอกสาร: ไฟล์ PDF
|
||||
- 3.6.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
|
||||
- 3.6.4. การอ้างอิงและจัดกลุ่ม: เอกสารนำส่ง เป็นส่วนหนึ่งใน Correspondence
|
||||
|
||||
### **3.7. ใบเวียนเอกสาร (Circulation Sheet)**
|
||||
|
||||
- 3.7.1. วัตถุประสงค์: การสื่อสาร เอกสาร (Correspondence) ทุกฉบับ จะมีใบเวียนเอกสารเพื่อควบคุมและมอบหมายงานภายในองค์กร (สามารถดูและแก้ไขได้เฉพาะคนในองค์กร)
|
||||
- 3.7.2. ประเภทเอกสาร: ไฟล์ PDF
|
||||
- 3.7.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ในองค์กรนั้น สามารถสร้างและแก้ไขได้
|
||||
- 3.7.4. การอ้างอิงและจัดกลุ่ม: การระบุผู้รับผิดชอบ:
|
||||
- ผู้รับผิดชอบหลัก (Main): มีได้หลายคน
|
||||
- ผู้ร่วมปฏิบัติงาน (Action): มีได้หลายคน
|
||||
- ผู้ที่ต้องรับทราบ (Information): มีได้หลายคน
|
||||
- 3.7.5. การติดตามงาน:
|
||||
- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบประเภท Main และ Action ได้
|
||||
- มีระบบแจ้งเตือนเมื่อมี Circulation ใหม่ และแจ้งเตือนล่วงหน้าก่อนถึงวันแล้วเสร็จ
|
||||
- สามารถปิด Circulation ได้เมื่อดำเนินการตอบกลับไปยังองค์กรผู้ส่ง (Originator) แล้ว หรือ รับทราบแล้ว (For Information)
|
||||
|
||||
### **3.8. ประวัติการแก้ไข (Revisions):** ระบบจะเก็บประวัติการสร้างและแก้ไข เอกสารทั้งหมด
|
||||
|
||||
### **3.9. การจัดเก็บไฟล์ (File Handling - ปรับปรุงใหญ่)**
|
||||
|
||||
- **3.9.1 Two-Phase Storage Strategy:**
|
||||
1. **Phase 1 (Upload):** ไฟล์ถูกอัปโหลดเข้าโฟลเดอร์ `temp/` และได้รับ `temp_id`
|
||||
2. **Phase 2 (Commit):** เมื่อ User กด Submit ฟอร์มสำเร็จ ระบบจะย้ายไฟล์จาก `temp/` ไปยัง `permanent/{YYYY}/{MM}/` และบันทึกลง Database ภายใน Transaction เดียวกัน
|
||||
3. **Cleanup:** มี Cron Job ลบไฟล์ใน `temp/` ที่ค้างเกิน 24 ชม. (Orphan Files)
|
||||
|
||||
- **3.9.2 Security:**
|
||||
- Virus Scan (ClamAV) ก่อนย้ายเข้า Permanent
|
||||
- Whitelist File Types: PDF, DWG, DOCX, XLSX, ZIP
|
||||
- Max Size: 50MB
|
||||
- Access Control: ตรวจสอบสิทธิ์ผ่าน Junction Table ก่อนให้ Download Link
|
||||
|
||||
- **3.9.3 ความปลอดภัยของการจัดเก็บไฟล์:**
|
||||
- ต้องมีการ scan virus สำหรับไฟล์ที่อัปโหลดทั้งหมด โดยใช้ ClamAV หรือบริการ third-party
|
||||
- จำกัดประเภทไฟล์ที่อนุญาต: PDF, DWG, DOCX, XLSX, ZIP (ต้องระบุรายการที่ชัดเจน)
|
||||
- ขนาดไฟล์สูงสุด: 50MB ต่อไฟล์
|
||||
- ไฟล์ต้องถูกเก็บนอก web root และเข้าถึงได้ผ่าน authenticated endpoint เท่านั้น
|
||||
- ต้องมี file integrity check (checksum) เพื่อป้องกันการแก้ไขไฟล์
|
||||
- Download links ต้องมี expiration time (default: 24 ชั่วโมง)
|
||||
- ต้องบันทึก audit log ทุกครั้งที่มีการดาวน์โหลดไฟล์สำคัญ
|
||||
|
||||
### **3.10. การจัดการเลขที่เอกสาร (Document Numbering - ปรับปรุง)**
|
||||
|
||||
- 3.10.1. ระบบต้องสามารถสร้างเลขที่เอกสาร (เช่น correspondence_number) ได้โดยอัตโนมัติ
|
||||
- 3.10.2. การนับเลข Running Number (SEQ) จะต้องนับแยกตาม Key ดังนี้: **โครงการ (Project)**, **องค์กรผู้ส่ง (Originator Organization)**, **ประเภทเอกสาร (Document Type)** และ **ปีปัจจุบัน (Year)**
|
||||
- 3.10.3. ผู้ดูแลระบบ (Admin) ต้องสามารถกำหนด "รูปแบบ" (Format Template) ของเลขที่เอกสารได้ (เช่น {ORG_CODE}-{TYPE_CODE}-{YEAR_SHORT}-{SEQ:4}) โดยกำหนดแยกตามโครงการและประเภทเอกสาร
|
||||
- 3.10.4. **กลไก:** ใช้ **Redis Distributed Lock** เป็นด่านแรก
|
||||
- 3.10.5. **Safety Net:** เพิ่ม **Optimistic Locking** (ตรวจสอบ Version/Last Number ใน DB ขณะ Update) เพื่อป้องกันกรณี Redis ล่ม หรือ Race Condition หลุดรอด
|
||||
- 3.10.6. ต้องมี retry mechanism และ fallback strategy เมื่อการ generate เลขที่เอกสารล้มเหลว
|
||||
|
||||
### **3.11 การจัดการ JSON Details (JSON & Performance - ปรับปรุง)**
|
||||
|
||||
- **3.11.1 วัตถุประสงค์**
|
||||
- จัดเก็บข้อมูลแบบไดนามิกที่เฉพาะเจาะจงกับแต่ละประเภทของเอกสาร
|
||||
- รองรับการขยายตัวของระบบโดยไม่ต้องเปลี่ยนแปลง database schema
|
||||
- จัดการ metadata และข้อมูลประกอบสำหรับ correspondence, routing, และ workflows
|
||||
|
||||
- **3.11.2 โครงสร้าง JSON Schema**
|
||||
ระบบต้องมี predefined JSON schemas สำหรับประเภทเอกสารต่างๆ:
|
||||
- **3.11.2.1 Correspondence Types**
|
||||
- **GENERIC**: ข้อมูลพื้นฐานสำหรับเอกสารทั่วไป
|
||||
- **RFI**: รายละเอียดคำถามและข้อมูลทางเทคนิค
|
||||
- **RFA**: ข้อมูลการขออนุมัติแบบและวัสดุ
|
||||
- **TRANSMITTAL**: รายการเอกสารที่ส่งต่อ
|
||||
- **LETTER**: ข้อมูลจดหมายทางการ
|
||||
- **EMAIL**: ข้อมูลอีเมล
|
||||
- **3.11.2.2 Routing Types**
|
||||
- **ROUTING_TEMPLATE**: กฎและเงื่อนไขการส่งต่อ
|
||||
- **ROUTING_INSTANCE**: สถานะและประวัติการส่งต่อ
|
||||
- **ROUTING_ACTION**: การดำเนินการในแต่ละขั้นตอน
|
||||
- **3.11.2.3 Audit Types**
|
||||
- **AUDIT_LOG**: ข้อมูลการตรวจสอบ
|
||||
- **SECURITY_SCAN**: ผลการตรวจสอบความปลอดภัย
|
||||
|
||||
- **3.11.3 Virtual Columns (ใหม่):** สำหรับ Field ใน JSON ที่ต้องใช้ในการค้นหา (Search) หรือจัดเรียง (Sort) บ่อยๆ **ต้องสร้าง Generated Column (Virtual Column)** ใน Database และทำ Index ไว้ เพื่อประสิทธิภาพสูงสุด
|
||||
|
||||
- **3.11.4 Validation Rules**
|
||||
- ต้องมี JSON schema validation สำหรับแต่ละประเภท
|
||||
- ต้องรองรับ versioning ของ schema
|
||||
- ต้องมี default values สำหรับ field ที่ไม่บังคับ
|
||||
- ต้องตรวจสอบ data types และ format ให้ถูกต้อง
|
||||
|
||||
- **3.11.5 Performance Requirements**
|
||||
- JSON field ต้องมีขนาดไม่เกิน 50KB
|
||||
- ต้องรองรับ indexing สำหรับ field ที่ใช้ค้นหาบ่อย
|
||||
- ต้องมี compression สำหรับ JSON ขนาดใหญ่
|
||||
|
||||
- **3.11.6 Security Requirements**
|
||||
- ต้อง sanitize JSON input เพื่อป้องกัน injection attacks
|
||||
- ต้อง validate JSON structure ก่อนบันทึก
|
||||
- ต้อง encrypt sensitive data ใน JSON fields
|
||||
|
||||
### **3.12 ข้อกำหนดพิเศษ**
|
||||
- **3.12.1. การจัดการเอกสารโต้ตอบ (Correspondence Management)**
|
||||
- ผู้ใช้งานที่มีสิทธิ์ระดับสูง (Superadmin) หรือผู้ได้รับอนุญาตเป็นกรณีพิเศษ สามารถเลือก 'สร้างในนามองค์กร (Create on behalf of)' ได้ เพื่อให้สามารถออกเลขที่เอกสาร (Running Number) ขององค์กรอื่นได้โดยไม่ต้องล็อกอินใหม่
|
||||
- **3.12.2 การสร้างเอกสารขออนุมัติ**
|
||||
- ผู้ใช้งานที่มีสิทธิ์ระดับสูง (Superadmin) หรือผู้ได้รับอนุญาตเป็นกรณีพิเศษ สามารถเลือก 'สร้างในนามองค์กร (Create on behalf of)' ได้ เพื่อให้สามารถออกเลขที่เอกสาร (Running Number) ขององค์กรอื่นได้โดยไม่ต้องล็อกอินใหม่
|
||||
- **3.12.3 การจัดการเอกสารนำส่ง (Transmittals)**
|
||||
- Workflow การอนุมัติ
|
||||
- **3.12.4 ใบเวียนเอกสาร (Circulation Sheet)**
|
||||
- Routing & Workflow
|
||||
## **🔐 4. ข้อกำหนดด้านสิทธิ์และการเข้าถึง (Access Control Requirements)**
|
||||
|
||||
### **4.1. ภาพรวม:** ผู้ใช้และองค์กรสามารถดูและแก้ไขเอกสารได้ตามสิทธิ์ที่ได้รับ โดยระบบสิทธิ์จะเป็นแบบ Role-Based Access Control (RBAC)
|
||||
|
||||
### **4.2. ลำดับชั้นของสิทธิ์ (Permission Hierarchy)**
|
||||
|
||||
- Global: สิทธิ์สูงสุดของระบบ
|
||||
- Organization: สิทธิ์ภายในองค์กร เป็นสิทธิ์พื้นฐานของผู้ใช้
|
||||
- Project: สิทธิ์เฉพาะในโครงการ จะถูกพิจารณาเมื่อผู้ใช้อยู่ในโครงการนั้น
|
||||
- Contract: สิทธิ์เฉพาะในสัญญา จะถูกพิจารณาเมื่อผู้ใช้อยู่ในสัญญานั้น (สัญญาเป็นส่วนหนึ่งของโครงการ)
|
||||
|
||||
กฎการบังคับใช้: เมื่อตรวจสอบสิทธิ์ ระบบจะพิจารณาสิทธิ์จากทุกระดับที่ผู้ใช้มี และใช้ สิทธิ์ที่มากที่สุด (Most Permissive) เป็นตัวตัดสิน
|
||||
|
||||
ตัวอย่าง: ผู้ใช้ A เป็น Viewer ในองค์กร แต่ถูกมอบหมายเป็น Editor ในโครงการ X เมื่ออยู่ในโครงการ X ผู้ใช้ A จะมีสิทธิ์แก้ไขได้
|
||||
|
||||
### **4.3. การกำหนดบทบาท (Roles) และขอบเขต (Scope)**
|
||||
|
||||
| บทบาท (Role) | ขอบเขต (Scope) | คำอธิบาย | สิทธิ์หลัก (Key Permissions) |
|
||||
| :------------------- | :------------- | :------------------ | :----------------------------------------------------------------------------- |
|
||||
| **Superadmin** | Global | ผู้ดูแลระบบสูงสุด | ทำทุกอย่างในระบบ, จัดการองค์กร, จัดการข้อมูลหลักระดับ Global |
|
||||
| **Org Admin** | Organization | ผู้ดูแลองค์กร | จัดการผู้ใช้ในองค์กร, จัดการบทบาท/สิทธิ์ภายในองค์กร, ดูรายงานขององค์กร |
|
||||
| **Document Control** | Organization | ควบคุมเอกสารขององค์กร | เพิ่ม/แก้ไข/ลบเอกสาร, กำหนดสิทธิ์เอกสารภายในองค์กร |
|
||||
| **Editor** | Organization | ผู้แก้ไขเอกสารขององค์กร | เพิ่ม/แก้ไขเอกสารที่ได้รับมอบหมาย |
|
||||
| **Viewer** | Organization | ผู้ดูเอกสารขององค์กร | ดูเอกสารที่มีสิทธิ์เข้าถึง |
|
||||
| **Project Manager** | Project | ผู้จัดการโครงการ | จัดการสมาชิกในโครงการ (เพิ่ม/ลบ/มอบบทบาท), สร้าง/จัดการสัญญาในโครงการ, ดูรายงานโครงการ |
|
||||
| **Contract Admin** | Contract | ผู้ดูแลสัญญา | จัดการสมาชิกในสัญญา, สร้าง/จัดการข้อมูลหลักเฉพาะสัญญา (ถ้ามี), อนุมัติเอกสารในสัญญา |
|
||||
|
||||
### **4.4. Token Management (ปรับปรุง)**
|
||||
|
||||
- **Payload Optimization:** ใน JWT Access Token ให้เก็บเฉพาะ `userId` และ `scope` ปัจจุบันเท่านั้น
|
||||
- **Permission Caching:** สิทธิ์ละเอียด (Permissions List) ให้เก็บใน **Redis** และดึงมาตรวจสอบเมื่อ Request เข้ามา เพื่อลดขนาด Token และเพิ่มความเร็ว
|
||||
|
||||
### **4.5. กระบวนการเริ่มต้นใช้งาน (Onboarding Workflow) ที่สมบูรณ์**
|
||||
|
||||
- **4.5.1. สร้างองค์กร (Organization)**
|
||||
- **Superadmin** สร้างองค์กรใหม่ (เช่น บริษัท A)
|
||||
- **Superadmin** แต่งตั้งผู้ใช้อย่างน้อย 1 คนให้เป็น **Org Admin** หรือ **Document Control** ของบริษัท A
|
||||
- **4.5.2. เพิ่มผู้ใช้ในองค์กร**
|
||||
- **Org Admin** ของบริษัท A เพิ่มผู้ใช้อื่นๆ (Editor, Viewer) เข้ามาในองค์กรของตน
|
||||
- **4.5.3. มอบหมายผู้ใช้ให้กับโครงการ (Project)**
|
||||
- **Project Manager** ของโครงการ X (ซึ่งอาจมาจากบริษัท A หรือบริษัทอื่น) ทำการ "เชิญ" หรือ "มอบหมาย" ผู้ใช้จากองค์กรต่างๆ ที่เกี่ยวข้องเข้ามาในโครงการ X
|
||||
- ในขั้นตอนนี้ **Project Manager** จะกำหนด **บทบาทระดับโครงการ** (เช่น Project Member, หรืออาจไม่มีบทบาทพิเศษ ให้ใช้สิทธิ์จากระดับองค์กรไปก่อน)
|
||||
- **4.5.4. เมอบหมายผู้ใช้ให้กับสัญญา (Contract)**
|
||||
- **Contract Admin** ของสัญญา Y (ซึ่งเป็นส่วนหนึ่งของโครงการ X) ทำการเลือกผู้ใช้ที่อยู่ในโครงการ X แล้ว มอบหมายให้เข้ามาในสัญญา Y
|
||||
- ในขั้นตอนนี้ **Contract Admin** จะกำหนด **บทบาทระดับสัญญา** (เช่น Contract Member) และสิทธิ์เฉพาะที่จำเป็น
|
||||
- **4.5.5 Security Onboarding:**
|
||||
- ต้องบังคับเปลี่ยน password ครั้งแรกสำหรับผู้ใช้ใหม่
|
||||
- ต้องมี security awareness training สำหรับผู้ใช้ที่มีสิทธิ์สูง
|
||||
- ต้องมี process สำหรับการรีเซ็ต password ที่ปลอดภัย
|
||||
- ต้องบันทึก audit log ทุกครั้งที่มีการเปลี่ยนแปลง permissions
|
||||
|
||||
### **4.6. การจัดการข้อมูลหลัก (Master Data Management) ที่แบ่งตามระดับ**
|
||||
|
||||
| ข้อมูลหลัก | ผู้มีสิทธิ์จัดการ | ระดับ |
|
||||
| :---------------------------------- | :------------------------------ | :------------------------------ |
|
||||
| ประเภทเอกสาร (Correspondence, RFA) | **Superadmin** | Global |
|
||||
| สถานะเอกสาร (Draft, Approved, etc.) | **Superadmin** | Global |
|
||||
| หมวดหมู่แบบ (Shop Drawing) | **Project Manager** | Project (สร้างใหม่ได้ภายในโครงการ) |
|
||||
| Tags | **Org Admin / Project Manager** | Organization / Project |
|
||||
| บทบาทและสิทธิ์ (Custom Roles) | **Superadmin / Org Admin** | Global / Organization |
|
||||
| Document Numbering Formats | **Superadmin / Admin** | Global / Organization |
|
||||
|
||||
## **👥 5. ข้อกำหนดด้านผู้ใช้งาน (User Interface & Experience)**
|
||||
|
||||
### **5.1. Layout หลัก:** หน้าเว็บใช้รูปแบบ App Shell ที่ประกอบด้วย
|
||||
|
||||
- Navbar (ส่วนบน): แสดงชื่อระบบ, เมนูผู้ใช้ (Profile), เมนูสำหรับ Document Control/เมนูสำหรับ Admin/Superadmin (จัดการผู้ใช้, จัดการสิทธิ์), และปุ่ม Login/Logout
|
||||
- Sidebar (ด้านข้าง): เป็นเมนูหลักสำหรับเข้าถึงส่วนที่เกี่ยวข้องกับเอกสารทั้งหมด เช่น Dashboard, Correspondences, RFA, Drawings
|
||||
- Main Content Area: พื้นที่สำหรับแสดงเนื้อหาหลักของหน้าที่เลือก
|
||||
|
||||
### **5.2. หน้า Landing Page:** เป็นหน้าแรกที่แสดงข้อมูลบางส่วนของโครงการสำหรับผู้ใช้ที่ยังไม่ได้ล็อกอิน
|
||||
|
||||
### **5.3. หน้า Dashboard:** เป็นหน้าแรกหลังจากล็อกอิน ประกอบด้วย
|
||||
|
||||
- การ์ดสรุปภาพรวม (KPI Cards): แสดงข้อมูลสรุปที่สำคัญขององค์กร เช่น จำนวนเอกสาร, งานที่เกินกำหนด
|
||||
- ตาราง "งานของฉัน" (My Tasks Table): แสดงรายการงานทั้งหมดจาก Circulation ที่ผู้ใช้ต้องดำเนินการ
|
||||
- Security Metrics: แสดงจำนวน files scanned, security incidents, failed login attempts
|
||||
|
||||
### **5.4. การติดตามสถานะ:** องค์กรสามารถติดตามสถานะเอกสารทั้งของตนเอง (Originator) และสถานะเอกสารที่ส่งมาถึงตนเอง (Recipient)
|
||||
|
||||
### **5.5. การจัดการข้อมูลส่วนตัว (Profile Page):** ผู้ใช้สามารถจัดการข้อมูลส่วนตัวและเปลี่ยนรหัสผ่านของตนเองได้
|
||||
|
||||
### **5.6. การจัดการเอกสารทางเทคนิค (RFA & Workflow):** ผู้ใช้สามารถดู RFA ในรูปแบบ Workflow ทั้งหมดได้ในหน้าเดียว, ขั้นตอนที่ยังไม่ถึงหรือผ่านไปแล้วจะเป็นรูปแบบ diable, สามารถดำเนินการได้เฉพาะในขั้นตอนที่ได้รับมอบหมายงาน (active) เช่น ตรวจสอบแล้ว เพื่อไปยังขั้นตอนต่อไป, สิทธิ์ Document Control ขึ้นไป สามรถกด ไปยังขั้นตอนต่อไป ได้ทุกขั้นตอน, การย้อนกลับ ไปขั้นตอนก่อนหน้า สามารถทำได้โดย สิทธิ์ Document Control ขึ้นไป
|
||||
|
||||
### **5.7. การจัดการใบเวียนเอกสาร (Circulation):** ผู้ใช้สามารถดู Circulation ในรูปแบบ Workflow ทั้งหมดได้ในหน้าเดียว,ขั้นตอนที่ยังไม่ถึงหรือผ่านไปแล้วจะเป็นรูปแบบ diable, สามารถดำเนินการได้เฉพาะในขั้นตอนที่ได้รับมอบหมายงาน (active) เช่น ตรวจสอบแล้ว เพื่อไปยังขั้นตอนต่อไป, สิทธิ์ Document Control ขึ้นไป สามรถกด ไปยังขั้นตอนต่อไป ได้ทุกขั้นตอน, การย้อนกลับ ไปขั้นตอนก่อนหน้า สามารถทำได้โดย สิทธิ์ Document Control ขึ้นไป
|
||||
|
||||
### **5.8. การจัดการเอกสารนำส่ง (Transmittals):** ผู้ใช้สามารถดู Transmittals ในรูปแบบรายการทั้งหมดได้ในหน้าเดียว
|
||||
|
||||
### **5.9. ข้อกำหนด UI/UX การแนบไฟล์ (File Attachment UX):**
|
||||
|
||||
- ระบบต้องรองรับการอัปโหลดไฟล์หลายไฟล์พร้อมกัน (Multi-file upload) เช่น การลากและวาง (Drag-and-Drop)
|
||||
- ในหน้าอัปโหลด (เช่น สร้าง RFA หรือ Correspondence) ผู้ใช้ต้องสามารถกำหนดได้ว่าไฟล์ใดเป็น "เอกสารหลัก" (Main Document เช่น PDF) และไฟล์ใดเป็น "เอกสารแนบประกอบ" (Supporting Attachments เช่น .dwg, .docx, .zip)
|
||||
- **Security Feedback:** แสดง security warnings สำหรับ file types ที่เสี่ยงหรือ files ที่ fail virus scan
|
||||
- **File Type Indicators:** แสดง file type icons และ security status
|
||||
|
||||
### **5.10 Form & Interaction (ใหม่)**
|
||||
|
||||
- **Dynamic Form Generator:** ใช้ Component กลางที่รับ JSON Schema แล้ว Render Form ออกมาอัตโนมัติ เพื่อลดความซ้ำซ้อนของโค้ดหน้าบ้าน และรองรับเอกสารประเภทใหม่ๆ ได้ทันที
|
||||
- **Optimistic Updates:** การเปลี่ยนสถานะ (เช่น กด Approve, กด Read) ให้ UI เปลี่ยนสถานะทันทีให้ผู้ใช้เห็นก่อนรอ API Response (Rollback ถ้า Failed)
|
||||
|
||||
### **5.11 Mobile Responsiveness (ใหม่)**
|
||||
|
||||
- **Table Visualization:** บนหน้าจอมือถือ ตารางข้อมูลที่มีหลาย Column (เช่น Correspondence List) ต้องเปลี่ยนการแสดงผลเป็นแบบ **Card View** อัตโนมัติ
|
||||
- **Navigation:** Sidebar ต้องเป็นแบบ Collapsible Drawer
|
||||
|
||||
### **5.12 Resilience & Offline Support (ใหม่)**
|
||||
|
||||
- **Auto-Save Draft:** ระบบต้องบันทึกข้อมูลฟอร์มที่กำลังกรอกลง **LocalStorage** อัตโนมัติ เพื่อป้องกันข้อมูลหายกรณีเน็ตหลุดหรือปิด Browser โดยไม่ได้ตั้งใจ
|
||||
- **Graceful Degradation:** หาก Service รอง (เช่น Search, Notification) ล่ม ระบบหลัก (CRUD) ต้องยังทำงานต่อได้
|
||||
|
||||
## **🛡️ 6. ข้อกำหนดที่ไม่ใช่ฟังก์ชันการทำงาน (Non-Functional Requirements)**
|
||||
|
||||
### **6.1. การบันทึกการกระทำ (Audit Log):** ทุกการกระทำที่สำคัญของผู้ใช้ (สร้าง, แก้ไข, ลบ, ส่ง) จะถูกบันทึกไว้ใน audit_logs เพื่อการตรวจสอบย้อนหลัง
|
||||
|
||||
- **6.1.1 ขอบเขตการบันทึก Audit Log:**
|
||||
- ทุกการสร้าง/แก้ไข/ลบ ข้อมูลสำคัญ (correspondences, RFAs, drawings, users, permissions)
|
||||
- ทุกการเข้าถึงข้อมูล sensitive (user data, financial information)
|
||||
- ทุกการเปลี่ยนสถานะ workflow (status transitions)
|
||||
- ทุกการดาวน์โหลดไฟล์สำคัญ (contract documents, financial reports)
|
||||
- ทุกการเปลี่ยนแปลง permission และ role assignment
|
||||
- ทุกการล็อกอินที่สำเร็จและล้มเหลว
|
||||
- ทุกการส่งคำขอ API ที่สำคัญ
|
||||
|
||||
- **6.1.2 ข้อมูลที่ต้องบันทึกใน Audit Log:**
|
||||
- ผู้ใช้งาน (user_id)
|
||||
- การกระทำ (action)
|
||||
- ชนิดของ entity (entity_type)
|
||||
- ID ของ entity (entity_id)
|
||||
- ข้อมูลก่อนการเปลี่ยนแปลง (old_values) - สำหรับ update operations
|
||||
- ข้อมูลหลังการเปลี่ยนแปลง (new_values) - สำหรับ update operations
|
||||
- IP address
|
||||
- User agent
|
||||
- Timestamp
|
||||
- Request ID สำหรับ tracing
|
||||
|
||||
### **6.2. Data Archiving & Partitioning (ใหม่)**
|
||||
|
||||
- สำหรับตารางที่มีขนาดใหญ่และโตเร็ว (เช่น `audit_logs`, `notifications`, `correspondence_revisions`) ต้องออกแบบโดยรองรับ **Table Partitioning** (แบ่งตาม Range วันที่ หรือ List) เพื่อประสิทธิภาพในระยะยาว
|
||||
|
||||
### **6.3. การค้นหา (Search):** ระบบต้องมีฟังก์ชันการค้นหาขั้นสูง ที่สามารถค้นหาเอกสาร **correspondence**, **rfa**, **shop_drawing**, **contract-drawing**, **transmittal** และ **ใบเวียน (Circulations)** จากหลายเงื่อนไขพร้อมกันได้ เช่น ค้นหาจากชื่อเรื่อง, ประเภท, วันที่, และ Tag
|
||||
|
||||
### **6.4. การทำรายงาน (Reporting):** สามารถจัดทำรายงานสรุปแยกประเภทของ Correspondence ประจำวัน, สัปดาห์, เดือน, และปีได้
|
||||
|
||||
### **6.5. ประสิทธิภาพ (Performance):** มีการใช้ Caching กับข้อมูลที่เรียกใช้บ่อย และใช้ Pagination ในตารางข้อมูลเพื่อจัดการข้อมูลจำนวนมาก
|
||||
|
||||
- **6.5.1 ตัวชี้วัดประสิทธิภาพ:**
|
||||
- **API Response Time:** < 200ms (90th percentile) สำหรับ operation ทั่วไป
|
||||
- **Search Query Performance:** < 500ms สำหรับการค้นหาขั้นสูง
|
||||
- **File Upload Performance:** < 30 seconds สำหรับไฟล์ขนาด 50MB
|
||||
- **Concurrent Users:** รองรับผู้ใช้พร้อมกันอย่างน้อย 100 คน
|
||||
- **Database Connection Pool:** ขนาดเหมาะสมกับ workload (default: min 5, max 20 connections)
|
||||
- **Cache Hit Ratio:** > 80% สำหรับ cached data
|
||||
- **Application Startup Time:** < 30 seconds
|
||||
|
||||
- **6.5.2 Caching Strategy:**
|
||||
- **Master Data Cache:** Roles, Permissions, Organizations, Project metadata (TTL: 1 hour)
|
||||
- **User Session Cache:** User permissions และ profile data (TTL: 30 minutes)
|
||||
- **Search Result Cache:** Frequently searched queries (TTL: 15 minutes)
|
||||
- **File Metadata Cache:** Attachment metadata (TTL: 1 hour)
|
||||
- **Document Cache:** Frequently accessed document metadata (TTL: 30 minutes)
|
||||
- **ต้องมี cache invalidation strategy ที่ชัดเจน:**
|
||||
- Invalidate on update/delete operations
|
||||
- Time-based expiration
|
||||
- Manual cache clearance สำหรับ admin operations
|
||||
- ใช้ Redis เป็น distributed cache backend
|
||||
- ต้องมี cache monitoring (hit/miss ratios)
|
||||
|
||||
### **6.6. ความปลอดภัย (Security):**
|
||||
|
||||
- มีระบบ Rate Limiting เพื่อป้องกันการโจมตีแบบ Brute-force
|
||||
- การจัดการ Secret (เช่น รหัสผ่าน DB, JWT Secret) จะต้องทำผ่าน Environment Variable ของ Docker เพื่อความปลอดภัยสูงสุด
|
||||
|
||||
- **6.6.1 Rate Limiting Strategy:**
|
||||
- **Anonymous Endpoints:** 100 requests/hour ต่อ IP address
|
||||
- **Authenticated Endpoints:**
|
||||
- Viewer: 500 requests/hour
|
||||
- Editor: 1000 requests/hour
|
||||
- Document Control: 2000 requests/hour
|
||||
- Admin/Superadmin: 5000 requests/hour
|
||||
- **File Upload Endpoints:** 50 requests/hour ต่อ user
|
||||
- **Search Endpoints:** 500 requests/hour ต่อ user
|
||||
- **Authentication Endpoints:** 10 requests/minute ต่อ IP address
|
||||
- **ต้องมี mechanism สำหรับยกเว้น rate limiting สำหรับ trusted services**
|
||||
- ต้องบันทึก log เมื่อมีการ trigger rate limiting
|
||||
|
||||
- **6.6.2 Error Handling และ Resilience:**
|
||||
- ต้องมี circuit breaker pattern สำหรับ external service calls
|
||||
- ต้องมี retry mechanism ด้วย exponential backoff
|
||||
- ต้องมี graceful degradation เมื่อบริการภายนอกล้มเหลว
|
||||
- Error messages ต้องไม่เปิดเผยข้อมูล sensitive
|
||||
|
||||
- **6.6.3 Input Validation:**
|
||||
- ต้องมี input validation ทั้งฝั่ง client และ server (defense in depth)
|
||||
- ต้องป้องกัน OWASP Top 10 vulnerabilities:
|
||||
- SQL Injection (ใช้ parameterized queries ผ่าน ORM)
|
||||
- XSS (input sanitization และ output encoding)
|
||||
- CSRF (CSRF tokens สำหรับ state-changing operations)
|
||||
- ต้อง validate file uploads:
|
||||
- File type (white-list approach)
|
||||
- File size
|
||||
- File content (magic number verification)
|
||||
- ต้อง sanitize user inputs ก่อนแสดงผลใน UI
|
||||
- ต้องใช้ content security policy (CSP) headers
|
||||
- ต้องมี request size limits เพื่อป้องกัน DoS attacks
|
||||
|
||||
- **6.6.4 Session และ Token Management:**
|
||||
- **JWT token expiration:** 8 hours สำหรับ access token
|
||||
- **Refresh token expiration:** 7 days
|
||||
- **Refresh token mechanism:** ต้องรองรับ token rotation และ revocation
|
||||
- **Token revocation on logout:** ต้องบันทึก revoked tokens จนกว่าจะ expire
|
||||
- **Concurrent session management:**
|
||||
- จำกัดจำนวน session พร้อมกันได้ (default: 5 devices)
|
||||
- ต้องแจ้งเตือนเมื่อมี login จาก device/location ใหม่
|
||||
- **Device fingerprinting:** สำหรับ security และ audit purposes
|
||||
- **Password policy:**
|
||||
- ความยาวขั้นต่ำ: 8 characters
|
||||
- ต้องมี uppercase, lowercase, number, special character
|
||||
- ต้องเปลี่ยน password ทุก 90 วัน
|
||||
- ต้องป้องกันการใช้ password ที่เคยใช้มาแล้ว 5 ครั้งล่าสุด
|
||||
|
||||
### **6.7. การสำรองข้อมูลและการกู้คืน (Backup & Recovery):**
|
||||
|
||||
- ระบบจะต้องมีกลไกการสำรองข้อมูลอัตโนมัติสำหรับฐานข้อมูล MariaDB [cite: 2.4] และไฟล์เอกสารทั้งหมดใน /share/dms-data [cite: 2.1] (เช่น ใช้ HBS 3 ของ QNAP หรือสคริปต์สำรองข้อมูล) อย่างน้อยวันละ 1 ครั้ง
|
||||
- ต้องมีแผนการกู้คืนระบบ (Disaster Recovery Plan) ในกรณีที่ Server หลัก (QNAP) ใช้งานไม่ได้
|
||||
|
||||
- **6.7.1 ขั้นตอนการกู้คืน:**
|
||||
- **Database Restoration Procedure:**
|
||||
- สร้างจาก full backup ล่าสุด
|
||||
- Apply transaction logs ถึง point-in-time ที่ต้องการ
|
||||
- Verify data integrity post-restoration
|
||||
- **File Storage Restoration Procedure:**
|
||||
- Restore จาก QNAP snapshot หรือ backup
|
||||
- Verify file integrity และ permissions
|
||||
- **Application Redeployment Procedure:**
|
||||
- Deploy จาก version ล่าสุดที่รู้ว่าทำงานได้
|
||||
- Verify application health
|
||||
- **Data Integrity Verification Post-Recovery:**
|
||||
- Run data consistency checks
|
||||
- Verify critical business data
|
||||
- **Recovery Time Objective (RTO):** < 4 ชั่วโมง
|
||||
- **Recovery Point Objective (RPO):** < 1 ชั่วโมง
|
||||
|
||||
### **6.8. กลยุทธ์การแจ้งเตือน (Notification Strategy - ปรับปรุง):**
|
||||
|
||||
- **6.8.1 ระบบจะส่งการแจ้งเตือน (ผ่าน Email หรือ Line [cite: 2.7]) เมื่อมีการกระทำที่สำคัญ** ดังนี้:
|
||||
1. เมื่อมีเอกสารใหม่ (Correspondence, RFA) ถูกส่งมาถึงองค์กรณ์ของเรา
|
||||
2. เมื่อมีใบเวียน (Circulation) ใหม่ มอบหมายงานมาที่เรา
|
||||
3. (ทางเลือก) เมื่อเอกสารที่เราส่งไป ถูกดำเนินการ (เช่น อนุมัติ/ปฏิเสธ)
|
||||
4. (ทางเลือก) เมื่อใกล้ถึงวันครบกำหนด (Deadline) [cite: 3.2.5, 3.6.6, 3.7.5]
|
||||
|
||||
- **6.8.2 Grouping/Digest (ใหม่):** กรณีมีการแจ้งเตือนประเภทเดียวกันจำนวนมากในช่วงเวลาสั้นๆ (เช่น Approve เอกสาร 10 ฉบับรวด) ระบบต้อง **รวม (Batch)** เป็น 1 Email/Line Notification เพื่อไม่ให้รบกวนผู้ใช้ (Spamming)
|
||||
|
||||
- **6.8.3 Notification Delivery Guarantees:**
|
||||
- **At-least-once delivery:** สำหรับ important notifications
|
||||
- **Retry mechanism:** ด้วย exponential backoff (max 3 reties)
|
||||
- **Dead letter queue:** สำหรับ notifications ที่ส่งไม่สำเร็จหลังจาก retries
|
||||
- **Delivery status tracking:** ต้องบันทึกสถานะการส่ง notifications
|
||||
- **Fallback channels:** ถ้า Email ล้มเหลว ให้ส่งผ่าน SYSTEM notification
|
||||
- **Notification preferences:** ผู้ใช้ต้องสามารถกำหนด channel preferences ได้
|
||||
|
||||
### **6.9. Maintenance Mode (ใหม่)**
|
||||
|
||||
- ระบบต้องมีกลไก **Maintenance Mode** ที่ Admin สามารถเปิดใช้งานได้
|
||||
- เมื่อเปิด: ผู้ใช้ทั่วไปจะเห็นหน้า "ปิดปรับปรุง" และไม่สามารถเรียก API ได้ (ยกเว้น Admin)
|
||||
- ใช้สำหรับช่วง Deploy Version ใหม่ หรือ Database Migration
|
||||
|
||||
### **6.10. Monitoring และ Observability**
|
||||
|
||||
- **6.10.1 Application Monitoring:**
|
||||
- **Health checks:** /health endpoint สำหรับ load balancer
|
||||
- **Metrics collection:** Response times, error rates, throughput
|
||||
- **Distributed tracing:** สำหรับ request tracing across services
|
||||
- **Log aggregation:** Structured logging ด้วย JSON format
|
||||
- **Alerting:** สำหรับ critical errors และ performance degradation
|
||||
- **6.10.2 Business Metrics:**
|
||||
- จำนวน documents created ต่อวัน
|
||||
- Workflow completion rates
|
||||
- User activity metrics
|
||||
- System utilization rates
|
||||
- Search query performance
|
||||
- **6.10.3 Security Monitoring:**
|
||||
- Failed login attempts
|
||||
- Rate limiting triggers
|
||||
- Virus scan results
|
||||
- File download activities
|
||||
- Permission changes
|
||||
|
||||
### **6.11 JSON Processing & Validation**
|
||||
|
||||
- **6.11.1 JSON Schema Management**
|
||||
- ต้องมี centralized JSON schema registry
|
||||
- ต้องรองรับ schema versioning และ migration
|
||||
- ต้องมี schema validation during runtime
|
||||
- **6.11.2 Performance Optimization**
|
||||
- **Caching:** Cache parsed JSON structures
|
||||
- **Compression:** ใช้ compression สำหรับ JSON ขนาดใหญ่
|
||||
- **Indexing:** Support JSON path indexing สำหรับ query
|
||||
- **6.11.3 Error Handling**
|
||||
- ต้องมี graceful degradation เมื่อ JSON validation ล้มเหลว
|
||||
- ต้องมี default fallback values
|
||||
- ต้องบันทึก error logs สำหรับ validation failures
|
||||
|
||||
## **🧪 7. ข้อกำหนดด้านการทดสอบ (Testing Requirements)**
|
||||
|
||||
### **7.1. Unit Testing:**
|
||||
|
||||
- ต้องมี unit tests สำหรับ business logic ทั้งหมด
|
||||
- Code coverage อย่างน้อย 70% สำหรับ backend services
|
||||
- ต้องทดสอบ RBAC permission logic ทุกระดับ
|
||||
|
||||
### **7.2. Integration Testing:**
|
||||
|
||||
- ทดสอบการทำงานร่วมกันของ modules
|
||||
- ทดสอบ database migrations และ data integrity
|
||||
- ทดสอบ API endpoints ด้วย realistic data
|
||||
|
||||
### **7.3. End-to-End Testing:**
|
||||
|
||||
- ทดสอบ complete user workflows
|
||||
- ทดสอบ document lifecycle จาก creation ถึง archival
|
||||
- ทดสอบ cross-module integrations
|
||||
|
||||
### **7.4. Security Testing:**
|
||||
|
||||
- **Penetration Testing:** ทดสอบ OWASP Top 10 vulnerabilities
|
||||
- **Security Audit:** Review code สำหรับ security flaws
|
||||
- **Virus Scanning Test:** ทดสอบ file upload security
|
||||
- **Rate Limiting Test:** ทดสอบ rate limiting functionality
|
||||
|
||||
### **7.5. Performance Testing:**
|
||||
|
||||
- **Load Testing:** ทดสอบด้วย realistic workloads
|
||||
- **Stress Testing:** หา breaking points ของระบบ
|
||||
- **Endurance Testing:** ทดสอบการทำงานต่อเนื่องเป็นเวลานาน
|
||||
|
||||
### **7.6. Disaster Recovery Testing:**
|
||||
|
||||
- ทดสอบ backup และ restoration procedures
|
||||
- ทดสอบ failover mechanisms
|
||||
- ทดสอบ data integrity หลังการ recovery
|
||||
|
||||
### **7.7 Specific Scenario Testing (เพิ่ม)**
|
||||
|
||||
- **Race Condition Test:** ทดสอบยิง Request ขอเลขที่เอกสารพร้อมกัน 100 Request
|
||||
- **Transaction Test:** ทดสอบปิดเน็ตระหว่าง Upload ไฟล์ (ตรวจสอบว่าไม่มี Orphan File หรือ Broken Link)
|
||||
- **Permission Test:** ทดสอบ CASL Integration ทั้งฝั่ง Backend และ Frontend ให้ตรงกัน
|
||||
|
||||
## **8. ข้อกำหนดด้านการบำรุงรักษา (Maintenance Requirements)**
|
||||
|
||||
### **8.1. Log Retention:**
|
||||
|
||||
- Audit logs: 7 ปี
|
||||
- Application logs: 1 ปี
|
||||
- Performance metrics: 2 ปี
|
||||
|
||||
### **8.2. Monitoring และ Alerting:**
|
||||
|
||||
- ต้องมี proactive monitoring สำหรับ critical systems
|
||||
- ต้องมี alerting สำหรับ security incidents
|
||||
- ต้องมี performance degradation alerts
|
||||
|
||||
### **8.3. Patch Management:**
|
||||
|
||||
- ต้องมี process สำหรับ security patches
|
||||
- ต้องทดสอบ patches ใน staging environment
|
||||
- ต้องมี rollback plan สำหรับ failed updates
|
||||
|
||||
### **8.4. Capacity Planning:**
|
||||
|
||||
- ต้อง monitor resource utilization
|
||||
- ต้องมี scaling strategy สำหรับ growth
|
||||
- ต้องมี performance baselines และ trending
|
||||
|
||||
## **9. ข้อกำหนดด้านการปฏิบัติตามกฎระเบียบ (Compliance Requirements)**
|
||||
|
||||
### **9.1. Data Privacy:**
|
||||
|
||||
- ต้องปฏิบัติตามกฎหมายคุ้มครองข้อมูลส่วนบุคคล
|
||||
- ต้องมี data retention policies
|
||||
- ต้องมี data deletion procedures
|
||||
|
||||
### **9.2. Audit Compliance:**
|
||||
|
||||
- ต้องรองรับ internal และ external audits
|
||||
- ต้องมี comprehensive audit trails
|
||||
- ต้องมี reporting capabilities สำหรับ compliance
|
||||
|
||||
### **9.3. Security Standards:**
|
||||
|
||||
- ต้องปฏิบัติตาม organizational security policies
|
||||
- ต้องมี security incident response plan
|
||||
- ต้องมี regular security assessments
|
||||
|
||||
## **📋 สรุปการปรับปรุงจากเวอร์ชันก่อนหน้า**
|
||||
|
||||
### **Security Enhancements:**
|
||||
|
||||
1. **File Upload Security** - Virus scanning, file type validation, access controls
|
||||
2. **Input Validation** - OWASP Top 10 protection, XSS/CSRF prevention
|
||||
3. **Rate Limiting** - Comprehensive rate limiting strategy
|
||||
4. **Secrets Management** - Secure handling of sensitive configuration
|
||||
|
||||
### **Architecture Improvements:**
|
||||
|
||||
1. **Document Numbering** - Changed from Stored Procedure to Application-level Locking with Optimistic Locking safety net
|
||||
2. **Resilience Patterns** - Circuit breaker, retry mechanisms, fallback strategies
|
||||
3. **Monitoring & Observability** - Health checks, metrics, distributed tracing
|
||||
4. **Caching Strategy** - Comprehensive caching with proper invalidation
|
||||
5. **Two-Phase File Storage** - Temp -> Permanent storage with transaction safety
|
||||
6. **Unified Workflow Engine** - Consolidated routing logic for better maintainability
|
||||
|
||||
### **Performance Targets:**
|
||||
|
||||
1. **API Response Time** - < 200ms (90th percentile)
|
||||
2. **Search Performance** - < 500ms
|
||||
3. **File Upload** - < 30 seconds for 50MB files
|
||||
4. **Cache Hit Ratio** - > 80%
|
||||
|
||||
### **Operational Excellence:**
|
||||
|
||||
1. **Disaster Recovery** - RTO < 4 hours, RPO < 1 hour
|
||||
2. **Backup Procedures** - Comprehensive backup and restoration
|
||||
3. **Security Testing** - Penetration testing and security audits
|
||||
4. **Performance Testing** - Load testing with realistic workloads
|
||||
5. **Maintenance Mode** - Graceful system maintenance capabilities
|
||||
|
||||
### **User Experience Improvements:**
|
||||
|
||||
1. **Dynamic Form Generator** - Reduced code duplication and better schema support
|
||||
2. **Mobile Responsiveness** - Card view for tables on mobile devices
|
||||
3. **Auto-Save Draft** - LocalStorage integration for form resilience
|
||||
4. **Notification Digest** - Reduced notification spam
|
||||
|
||||
### **Data Management:**
|
||||
|
||||
1. **Virtual Columns** - Improved JSON field search performance
|
||||
2. **Table Partitioning** - Support for large-scale data growth
|
||||
3. **Idempotency Keys** - Prevention of duplicate transactions
|
||||
|
||||
เอกสารนี้สะท้อนถึงความมุ่งมั่นในการสร้างระบบที่มีความปลอดภัย, มีความทนทาน, และมีประสิทธิภาพสูง พร้อมรองรับการเติบโตในอนาคตและความต้องการทางธุรกิจที่เปลี่ยนแปลงไป
|
||||
|
||||
**หมายเหตุ:** Requirements นี้จะถูกทบทวนและปรับปรุงเป็นระยะตาม feedback จากทีมพัฒนาและความต้องการทางธุรกิจที่เปลี่ยนแปลงไป
|
||||
|
||||
## **Document Control:**
|
||||
|
||||
- **Document:** Application Requirements Specification v1.4.2
|
||||
- **Version:** 1.4
|
||||
- **Date:** 2025-11-19
|
||||
- **Author:** NAP LCBP3-DMS & Gemini
|
||||
- **Status:** FINAL
|
||||
- **Classification:** Internal Technical Documentation
|
||||
- **Approved By:** Nattanin
|
||||
|
||||
---
|
||||
|
||||
`End of Requirements Specification v1.4.2`
|
||||
@@ -86,9 +86,9 @@ mkdir -p src/database/seeds
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, // บังคับใช้ Strict Mode
|
||||
"noImplicitAny": true, // ห้ามใช้ Any โดยไม่จำเป็น
|
||||
"strictNullChecks": true, // ตรวจสอบค่า Null อย่างเคร่งครัด
|
||||
"strict": true, // บังคับใช้ Strict Mode
|
||||
"noImplicitAny": true, // ห้ามใช้ Any โดยไม่จำเป็น
|
||||
"strictNullChecks": true, // ตรวจสอบค่า Null อย่างเคร่งครัด
|
||||
"strictBindCallApply": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
@@ -151,7 +151,7 @@ pnpm --version
|
||||
|
||||
(ควรจะขึ้นเป็นตัวเลขเวอร์ชัน เช่น `9.x.x`)
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
### ทางเลือก: ติดตั้งผ่าน Corepack (สำหรับ Node.js เวอร์ชันใหม่)
|
||||
|
||||
@@ -162,16 +162,16 @@ corepack enable
|
||||
corepack prepare pnpm@latest --activate
|
||||
```
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
### เมื่อติดตั้งเสร็จแล้ว
|
||||
|
||||
คุณสามารถใช้ `pnpm` แทน `npm` ได้เลยครับ เช่น:
|
||||
|
||||
* **ติดตั้ง Nest CLI (ถ้ายังไม่ได้ลง):** `pnpm add -g @nestjs/cli`
|
||||
* **สร้างโปรเจกต์ NestJS:** `nest new lcbp3-backend` (แล้วเลือก pnpm ตอนระบบถาม)
|
||||
* **รันโปรเจกต์:** `pnpm start:dev`
|
||||
* **ติดตั้ง Library เพิ่ม:** `pnpm add [package-name]` (เช่น `pnpm add typeorm`)
|
||||
- **ติดตั้ง Nest CLI (ถ้ายังไม่ได้ลง):** `pnpm add -g @nestjs/cli`
|
||||
- **สร้างโปรเจกต์ NestJS:** `nest new lcbp3-backend` (แล้วเลือก pnpm ตอนระบบถาม)
|
||||
- **รันโปรเจกต์:** `pnpm start:dev`
|
||||
- **ติดตั้ง Library เพิ่ม:** `pnpm add [package-name]` (เช่น `pnpm add typeorm`)
|
||||
|
||||
ติดตั้งเสร็จแล้ว แจ้งผมได้เลยนะครับ จะได้ไปต่อที่ขั้นตอน **Config Database** กันครับ
|
||||
|
||||
@@ -196,7 +196,7 @@ pnpm add @nestjs/typeorm typeorm mysql2 @nestjs/config
|
||||
สร้างไฟล์ `docker-compose.yml` ที่ root ของโปรเจกต์ (ถ้ายังไม่มี):
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
mariadb:
|
||||
@@ -209,7 +209,7 @@ services:
|
||||
MYSQL_USER: admin
|
||||
MYSQL_PASSWORD: password123
|
||||
ports:
|
||||
- '3306:3306'
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
networks:
|
||||
@@ -222,7 +222,7 @@ services:
|
||||
environment:
|
||||
PMA_HOST: mariadb
|
||||
ports:
|
||||
- '8080:80'
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- mariadb
|
||||
networks:
|
||||
@@ -248,36 +248,36 @@ docker-compose up -d
|
||||
|
||||
```typescript
|
||||
// src/app.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// 1. Load Config Module
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true, // ให้เรียกใช้ได้ทุกที่โดยไม่ต้อง import ใหม่
|
||||
envFilePath: '.env', // อ่านค่าจากไฟล์ .env
|
||||
envFilePath: ".env", // อ่านค่าจากไฟล์ .env
|
||||
}),
|
||||
|
||||
// 2. Setup TypeORM Connection (Async เพื่อรออ่าน Config ก่อน)
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
type: 'mariadb', // หรือ 'mysql' ก็ได้เพราะใช้ driver เดียวกัน
|
||||
host: configService.get<string>('DB_HOST'),
|
||||
port: configService.get<number>('DB_PORT'),
|
||||
username: configService.get<string>('DB_USERNAME'),
|
||||
password: configService.get<string>('DB_PASSWORD'),
|
||||
database: configService.get<string>('DB_DATABASE'),
|
||||
type: "mariadb", // หรือ 'mysql' ก็ได้เพราะใช้ driver เดียวกัน
|
||||
host: configService.get<string>("DB_HOST"),
|
||||
port: configService.get<number>("DB_PORT"),
|
||||
username: configService.get<string>("DB_USERNAME"),
|
||||
password: configService.get<string>("DB_PASSWORD"),
|
||||
database: configService.get<string>("DB_DATABASE"),
|
||||
// Auto Load Entities: โหลด Entity ทั้งหมดที่อยู่ในโปรเจกต์อัตโนมัติ
|
||||
autoLoadEntities: true,
|
||||
// Synchronize: true เฉพาะ Dev environment (ห้ามใช้ใน Prod)
|
||||
synchronize: configService.get<string>('NODE_ENV') === 'development',
|
||||
synchronize: configService.get<string>("NODE_ENV") === "development",
|
||||
// Logging: เปิดดู Query SQL ตอน Dev
|
||||
logging: configService.get<string>('NODE_ENV') === 'development',
|
||||
logging: configService.get<string>("NODE_ENV") === "development",
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
@@ -317,7 +317,7 @@ pnpm start:dev
|
||||
|
||||
ถ้า Terminal ขึ้นข้อความสีเขียวๆ ว่า `[NestApplication] Nest application successfully started` และไม่มี Error เกี่ยวกับ TypeORM แสดงว่าเชื่อมต่อ MariaDB บน Localhost สำเร็จแล้วครับ\! 🎉
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
**ขั้นตอนถัดไป:**
|
||||
เราควรสร้าง **Base Entity** (T1.1) และ **Users Entity** (T1.3) เพื่อเริ่มทำระบบ Login ไหมครับ?
|
||||
@@ -334,7 +334,7 @@ pnpm start:dev
|
||||
openssl rand -base64 64
|
||||
```
|
||||
|
||||
*ผลลัพธ์จะได้ String ยาวๆ ที่อ่านไม่รู้เรื่อง เช่น:*
|
||||
_ผลลัพธ์จะได้ String ยาวๆ ที่อ่านไม่รู้เรื่อง เช่น:_
|
||||
`h78/sT5...xYz==`
|
||||
|
||||
### วิธีที่ 2: ใช้ Node.js Crypto (ง่ายสำหรับ Developer)
|
||||
@@ -347,13 +347,13 @@ openssl rand -base64 64
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
```
|
||||
|
||||
*ผลลัพธ์จะได้ Hex String ยาว 64 ตัวอักษร*
|
||||
_ผลลัพธ์จะได้ Hex String ยาว 64 ตัวอักษร_
|
||||
|
||||
### วิธีที่ 3: ใช้ Password Manager Generator
|
||||
|
||||
หากใช้ 1Password, LastPass หรือ Bitwarden สามารถใช้ฟีเจอร์ "Generate Password" โดยตั้งค่าความยาว 64 ตัวอักษร (รวมตัวเลขและสัญลักษณ์) ก็ใช้ได้เช่นกันครับ
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
### ⚙️ การนำไปใช้งานในโปรเจกต์
|
||||
|
||||
@@ -397,14 +397,14 @@ pnpm add @nestjs/config joi
|
||||
|
||||
```typescript
|
||||
// File: src/common/config/env.validation.ts
|
||||
import Joi from 'joi';
|
||||
import Joi from "joi";
|
||||
|
||||
// สร้าง Schema สำหรับตรวจสอบค่า Environment Variables
|
||||
export const envValidationSchema = Joi.object({
|
||||
// 1. Application Environment
|
||||
NODE_ENV: Joi.string()
|
||||
.valid('development', 'production', 'test', 'provision')
|
||||
.default('development'),
|
||||
.valid("development", "production", "test", "provision")
|
||||
.default("development"),
|
||||
PORT: Joi.number().default(3000),
|
||||
|
||||
// 2. Database Configuration (MariaDB)
|
||||
@@ -417,8 +417,11 @@ export const envValidationSchema = Joi.object({
|
||||
|
||||
// 3. Security (JWT)
|
||||
// ต้องมีค่า และควรยาวพอ (ตรวจสอบความยาวได้ถ้าระบุ min)
|
||||
JWT_SECRET: Joi.string().required().min(32).message('JWT_SECRET must be at least 32 characters long for security.'),
|
||||
JWT_EXPIRATION: Joi.string().default('8h'),
|
||||
JWT_SECRET: Joi.string()
|
||||
.required()
|
||||
.min(32)
|
||||
.message("JWT_SECRET must be at least 32 characters long for security."),
|
||||
JWT_EXPIRATION: Joi.string().default("8h"),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -428,19 +431,19 @@ export const envValidationSchema = Joi.object({
|
||||
|
||||
```typescript
|
||||
// File: src/app.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { envValidationSchema } from './common/config/env.validation.js'; // สังเกต .js สำหรับ ESM
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
import { envValidationSchema } from "./common/config/env.validation.js"; // สังเกต .js สำหรับ ESM
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// 1. Setup Config Module พร้อม Validation
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true, // เรียกใช้ได้ทั่วทั้ง App ไม่ต้อง import ซ้ำ
|
||||
envFilePath: '.env', // อ่านไฟล์ .env (สำหรับ Dev)
|
||||
envFilePath: ".env", // อ่านไฟล์ .env (สำหรับ Dev)
|
||||
validationSchema: envValidationSchema, // ใช้ Schema ที่เราสร้างเพื่อตรวจสอบ
|
||||
validationOptions: {
|
||||
// ถ้ามีค่าไหนไม่ผ่าน Validation ให้ Error และหยุดทำงานทันที
|
||||
@@ -453,15 +456,15 @@ import { envValidationSchema } from './common/config/env.validation.js'; // ส
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
type: 'mariadb',
|
||||
host: configService.get<string>('DB_HOST'),
|
||||
port: configService.get<number>('DB_PORT'),
|
||||
username: configService.get<string>('DB_USERNAME'),
|
||||
password: configService.get<string>('DB_PASSWORD'),
|
||||
database: configService.get<string>('DB_DATABASE'),
|
||||
type: "mariadb",
|
||||
host: configService.get<string>("DB_HOST"),
|
||||
port: configService.get<number>("DB_PORT"),
|
||||
username: configService.get<string>("DB_USERNAME"),
|
||||
password: configService.get<string>("DB_PASSWORD"),
|
||||
database: configService.get<string>("DB_DATABASE"),
|
||||
autoLoadEntities: true,
|
||||
// synchronize: true เฉพาะตอน Dev เท่านั้น ห้ามใช้บน Prod
|
||||
synchronize: configService.get<string>('NODE_ENV') === 'development',
|
||||
synchronize: configService.get<string>("NODE_ENV") === "development",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
@@ -478,7 +481,7 @@ export class AppModule {}
|
||||
สร้างไฟล์: `docker-compose.override.yml.example` ที่ root project:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
# Override ค่า Config ของ Service Backend (เมื่อเราสร้าง Container Backend ในอนาคต)
|
||||
@@ -510,18 +513,18 @@ App **ต้อง Crash** และแสดง Error Message ชัดเจ
|
||||
|
||||
ถ้าขึ้นแบบนี้แสดงว่าระบบ **Secure Configuration** ของเราทำงานถูกต้องตามแผน T0.1 แล้วครับ! 🎉
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
## **T0.2 Redis & Queue Infrastructure**
|
||||
|
||||
เป้าหมายของ Task นี้คือเตรียม **Redis** สำหรับทำ 2 เรื่องสำคัญ:
|
||||
|
||||
1. **Distributed Locking (Redlock):** ป้องกัน Race Condition เวลาออกเลขที่เอกสาร
|
||||
2. **Message Queue (BullMQ):** สำหรับจัดการงานเบื้องหลัง (Background Jobs) เช่น การรวมอีเมลแจ้งเตือน (Digest Notification)
|
||||
1. **Distributed Locking (Redlock):** ป้องกัน Race Condition เวลาออกเลขที่เอกสาร
|
||||
2. **Message Queue (BullMQ):** สำหรับจัดการงานเบื้องหลัง (Background Jobs) เช่น การรวมอีเมลแจ้งเตือน (Digest Notification)
|
||||
|
||||
มาเริ่มกันเลยครับ
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
### 1. เพิ่ม Redis ใน Docker Compose
|
||||
|
||||
@@ -530,7 +533,7 @@ App **ต้อง Crash** และแสดง Error Message ชัดเจ
|
||||
**ไฟล์: `docker-compose.yml`**
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
# ... (mariadb & pma เดิม) ...
|
||||
@@ -543,7 +546,7 @@ services:
|
||||
# ใช้ Command นี้เพื่อตั้ง Password
|
||||
command: redis-server --requirepass "redis_password_secure"
|
||||
ports:
|
||||
- '6379:6379'
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
@@ -611,20 +614,20 @@ pnpm add @nestjs/bullmq bullmq
|
||||
**ไฟล์: `src/app.module.ts`**
|
||||
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BullModule } from '@nestjs/bullmq'; // Import BullModule
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { envValidationSchema } from './common/config/env.validation.js';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { BullModule } from "@nestjs/bullmq"; // Import BullModule
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
import { envValidationSchema } from "./common/config/env.validation.js";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// 1. Config (เดิม)
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
envFilePath: ".env",
|
||||
validationSchema: envValidationSchema,
|
||||
validationOptions: { abortEarly: true },
|
||||
}),
|
||||
@@ -634,14 +637,14 @@ import { envValidationSchema } from './common/config/env.validation.js';
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
type: 'mariadb',
|
||||
host: configService.get<string>('DB_HOST'),
|
||||
port: configService.get<number>('DB_PORT'),
|
||||
username: configService.get<string>('DB_USERNAME'),
|
||||
password: configService.get<string>('DB_PASSWORD'),
|
||||
database: configService.get<string>('DB_DATABASE'),
|
||||
type: "mariadb",
|
||||
host: configService.get<string>("DB_HOST"),
|
||||
port: configService.get<number>("DB_PORT"),
|
||||
username: configService.get<string>("DB_USERNAME"),
|
||||
password: configService.get<string>("DB_PASSWORD"),
|
||||
database: configService.get<string>("DB_DATABASE"),
|
||||
autoLoadEntities: true,
|
||||
synchronize: configService.get<string>('NODE_ENV') === 'development',
|
||||
synchronize: configService.get<string>("NODE_ENV") === "development",
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -651,9 +654,9 @@ import { envValidationSchema } from './common/config/env.validation.js';
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
connection: {
|
||||
host: configService.get<string>('REDIS_HOST'),
|
||||
port: configService.get<number>('REDIS_PORT'),
|
||||
password: configService.get<string>('REDIS_PASSWORD'),
|
||||
host: configService.get<string>("REDIS_HOST"),
|
||||
port: configService.get<number>("REDIS_PORT"),
|
||||
password: configService.get<string>("REDIS_PASSWORD"),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
@@ -674,32 +677,34 @@ pnpm start:dev
|
||||
|
||||
ถ้าไม่มี Error ขึ้น และ App Start ได้ปกติ แสดงว่า **T0.2 เสร็จสมบูรณ์** ครับ\! ตอนนี้เรามีทั้ง Database และ Redis พร้อมสำหรับการพัฒนา Business Logic แล้ว
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
### 6. **แก้ไข Error**
|
||||
|
||||
ปัญหานี้เกิดจากยังไม่ได้ **Import** `BullModule` เข้ามาในไฟล์ `src/app.module.ts` ครับ (TypeScript เลยไม่รู้จักว่า `BullModule` คืออะไร)
|
||||
|
||||
ให้เพิ่มบรรทัดนี้ที่ส่วนบนสุดของไฟล์ `src/app.module.ts` ครับ:
|
||||
|
||||
```typescript
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
```
|
||||
|
||||
หรือลองตรวจสอบไฟล์ `src/app.module.ts` ให้ครบถ้วนตามนี้ครับ:
|
||||
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { BullModule } from '@nestjs/bullmq'; // <--- ต้องมีบรรทัดนี้ครับ
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { envValidationSchema } from './common/config/env.validation.js';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { BullModule } from "@nestjs/bullmq"; // <--- ต้องมีบรรทัดนี้ครับ
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
import { envValidationSchema } from "./common/config/env.validation.js";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
envFilePath: ".env",
|
||||
validationSchema: envValidationSchema,
|
||||
validationOptions: { abortEarly: true },
|
||||
}),
|
||||
@@ -707,14 +712,14 @@ import { envValidationSchema } from './common/config/env.validation.js';
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
type: 'mariadb',
|
||||
host: configService.get<string>('DB_HOST'),
|
||||
port: configService.get<number>('DB_PORT'),
|
||||
username: configService.get<string>('DB_USERNAME'),
|
||||
password: configService.get<string>('DB_PASSWORD'),
|
||||
database: configService.get<string>('DB_DATABASE'),
|
||||
type: "mariadb",
|
||||
host: configService.get<string>("DB_HOST"),
|
||||
port: configService.get<number>("DB_PORT"),
|
||||
username: configService.get<string>("DB_USERNAME"),
|
||||
password: configService.get<string>("DB_PASSWORD"),
|
||||
database: configService.get<string>("DB_DATABASE"),
|
||||
autoLoadEntities: true,
|
||||
synchronize: configService.get<string>('NODE_ENV') === 'development',
|
||||
synchronize: configService.get<string>("NODE_ENV") === "development",
|
||||
}),
|
||||
}),
|
||||
// Queue Configuration
|
||||
@@ -723,9 +728,9 @@ import { envValidationSchema } from './common/config/env.validation.js';
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
connection: {
|
||||
host: configService.get<string>('REDIS_HOST'),
|
||||
port: configService.get<number>('REDIS_PORT'),
|
||||
password: configService.get<string>('REDIS_PASSWORD'),
|
||||
host: configService.get<string>("REDIS_HOST"),
|
||||
port: configService.get<number>("REDIS_PORT"),
|
||||
password: configService.get<string>("REDIS_PASSWORD"),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
@@ -1505,6 +1505,177 @@ export class UserService {
|
||||
|
||||
ถ้าทุกอย่างเรียบร้อย เราจะปิดจ็อบ Phase 1 แล้วไปขึ้น **Phase 2: High-Integrity Data (File Upload)** กันต่อไหมครับ?
|
||||
|
||||
### **"มอบหมายบทบาท (Assign Role)"**
|
||||
**ถูกต้องที่สุดครับ!** คุณเข้าใจคอนเซปต์ของระบบ RBAC ในระบบนี้ได้อย่างทะลุปรุโปร่งครับ 👍
|
||||
|
||||
ถ้าเราสร้าง User ลงในตาราง `users` เพียงอย่างเดียว User คนนั้นจะ:
|
||||
|
||||
1. ✅ **Login ได้** (เพราะมี Username/Password)
|
||||
2. ❌ **ทำอะไรไม่ได้เลย** (เพราะไม่มี Row ใน `user_assignments` ระบบสิทธิ์ `v_user_all_permissions` จึงว่างเปล่า)
|
||||
|
||||
ดังนั้น การ **"มอบหมายบทบาท (Assign Role)"** จึงเป็นขั้นตอนที่ขาดไม่ได้ครับ
|
||||
|
||||
-----
|
||||
|
||||
#### 🛠️ สิ่งที่เราต้องทำเพิ่ม (เพื่อให้ Admin มอบสิทธิ์ได้)
|
||||
|
||||
ใน Task T1.3 เราทำแค่ `Create User` (ลงตาราง users) ไปแล้ว ตอนนี้เราต้องทำ API สำหรับ **Assign Role** (ลงตาราง user\_assignments) ครับ
|
||||
|
||||
มาสร้างฟังก์ชันนี้กันครับ:
|
||||
|
||||
##### 1. สร้าง DTO สำหรับการมอบสิทธิ์
|
||||
|
||||
สร้างไฟล์: `src/modules/user/dto/assign-role.dto.ts`
|
||||
|
||||
```typescript
|
||||
import { IsInt, IsNotEmpty, IsOptional, ValidateIf } from 'class-validator';
|
||||
|
||||
export class AssignRoleDto {
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
roleId!: number;
|
||||
|
||||
// Scope (ต้องส่งมาอย่างน้อย 1 อัน หรือไม่ส่งเลยถ้าเป็น Global)
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
organizationId?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
projectId?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
contractId?: number;
|
||||
}
|
||||
```
|
||||
|
||||
##### 2. สร้าง `UserAssignmentService` (Logic การบันทึก)
|
||||
|
||||
เราควรแยก Service นี้ออกมาเพื่อความเป็นระเบียบครับ (หรือจะใส่ใน UserService ก็ได้ แต่แยกดีกว่า)
|
||||
|
||||
สร้างไฟล์: `src/modules/user/user-assignment.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserAssignment } from './entities/user-assignment.entity.js'; // ต้องไปสร้าง Entity นี้ก่อน (ดูข้อ 3)
|
||||
import { AssignRoleDto } from './dto/assign-role.dto.js';
|
||||
import { User } from './entities/user.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserAssignmentService {
|
||||
constructor(
|
||||
@InjectRepository(UserAssignment)
|
||||
private assignmentRepo: Repository<UserAssignment>,
|
||||
) {}
|
||||
|
||||
async assignRole(dto: AssignRoleDto, assigner: User) {
|
||||
// Validation: ตรวจสอบกฎเหล็ก (เลือกได้แค่ Scope เดียว)
|
||||
const scopes = [dto.organizationId, dto.projectId, dto.contractId].filter(v => v != null);
|
||||
if (scopes.length > 1) {
|
||||
throw new BadRequestException('Cannot assign multiple scopes at once. Choose one of Org, Project, or Contract.');
|
||||
}
|
||||
|
||||
// สร้าง Assignment
|
||||
const assignment = this.assignmentRepo.create({
|
||||
userId: dto.userId,
|
||||
roleId: dto.roleId,
|
||||
organizationId: dto.organizationId,
|
||||
projectId: dto.projectId,
|
||||
contractId: dto.contractId,
|
||||
assignedByUserId: assigner.user_id, // เก็บ Log ว่าใครเป็นคนให้สิทธิ์
|
||||
});
|
||||
|
||||
return this.assignmentRepo.save(assignment);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 3. สร้าง Entity `UserAssignment` (ถ้ายังไม่ได้สร้าง)
|
||||
|
||||
สร้างไฟล์: `src/modules/user/entities/user-assignment.entity.ts`
|
||||
|
||||
```typescript
|
||||
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
|
||||
import { User } from './user.entity.js';
|
||||
// Import Role, Org, Project, Contract entities...
|
||||
|
||||
@Entity('user_assignments')
|
||||
export class UserAssignment {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'user_id' })
|
||||
userId!: number;
|
||||
|
||||
@Column({ name: 'role_id' })
|
||||
roleId!: number;
|
||||
|
||||
@Column({ name: 'organization_id', nullable: true })
|
||||
organizationId?: number;
|
||||
|
||||
@Column({ name: 'project_id', nullable: true })
|
||||
projectId?: number;
|
||||
|
||||
@Column({ name: 'contract_id', nullable: true })
|
||||
contractId?: number;
|
||||
|
||||
@Column({ name: 'assigned_by_user_id', nullable: true })
|
||||
assignedByUserId?: number;
|
||||
|
||||
@CreateDateColumn({ name: 'assigned_at' })
|
||||
assignedAt!: Date;
|
||||
|
||||
// Relation กลับไปหา User (เจ้าของสิทธิ์)
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user?: User;
|
||||
}
|
||||
```
|
||||
|
||||
##### 4. เพิ่ม Endpoint ใน `UserController`
|
||||
|
||||
```typescript
|
||||
// src/modules/user/user.controller.ts
|
||||
// ... imports ...
|
||||
import { AssignRoleDto } from './dto/assign-role.dto.js';
|
||||
import { UserAssignmentService } from './user-assignment.service.js'; // อย่าลืม Inject เข้ามาใน Constructor
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class UserController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly assignmentService: UserAssignmentService, // <--- Inject เพิ่ม
|
||||
) {}
|
||||
|
||||
// ... Endpoints เดิม ...
|
||||
|
||||
@Post('assign-role')
|
||||
@RequirePermission('permission.assign') // เฉพาะคนมีสิทธิ์ Assign (เช่น Admin)
|
||||
assignRole(@Body() dto: AssignRoleDto, @Request() req: any) {
|
||||
return this.assignmentService.assignRole(dto, req.user);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
#### 📝 สรุปภาพรวม
|
||||
|
||||
1. **สร้าง User:** `POST /users` -\> ได้ User เปล่าๆ (Login ได้ แต่ทำไรไม่ได้)
|
||||
2. **กำหนดสิทธิ์:** `POST /users/assign-role` -\> ยิง API นี้เพื่อบอกว่า "User นี้ เป็น Project Manager ของ Project A นะ"
|
||||
* Body: `{ "userId": 5, "roleId": 6, "projectId": 1 }`
|
||||
3. **ใช้งาน:** เมื่อ User ID 5 ล็อกอิน ระบบจะไปเช็คใน `user_assignments` เห็นว่ามีสิทธิ์ Project Manager ของ Project 1 จึงอนุญาตให้ทำงานได้ครับ
|
||||
|
||||
ระบบ RBAC ของเรายืดหยุ่นและรองรับ Real-world scenario มากๆ ครับ (เช่น คนหนึ่งคนเป็น Admin ใน Project A แต่เป็นแค่ Viewer ใน Project B ก็ทำได้ เพราะเรามีตาราง `user_assignments` ตัวนี้แหละครับ)
|
||||
|
||||
## Task T1.3 Error
|
||||
|
||||
### ERROR [HttpExceptionFilter] Http Status: 404 Error Message: "Cannot GET /api/users"
|
||||
@@ -1564,6 +1735,43 @@ export class AppModule {}
|
||||
|
||||
คราวนี้น่าจะเจอข้อมูลแล้วครับ\! 🎉
|
||||
|
||||
#### ปัญหานี้เกิดจากเราได้สร้าง `UserAssignmentService` และเรียกใช้ใน Controller แล้ว แต่เรา **ลืมลงทะเบียน (Register)** Service นี้และ Entity ของมันใน `UserModule` ครับ NestJS เลยไม่รู้ว่าจะเอา Service นี้มาจากไหน
|
||||
|
||||
##### 🛠️ วิธีแก้ไข
|
||||
|
||||
ให้แก้ไขไฟล์ `src/modules/user/user.module.ts` โดยเพิ่ม `UserAssignment` (Entity) และ `UserAssignmentService` (Provider) เข้าไปครับ
|
||||
|
||||
**ไฟล์: `src/modules/user/user.module.ts`**
|
||||
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserService } from './user.service.js';
|
||||
import { UserAssignmentService } from './user-assignment.service.js'; // <--- 1. Import Service
|
||||
import { UserController } from './user.controller.js';
|
||||
import { User } from './entities/user.entity.js';
|
||||
import { UserAssignment } from './entities/user-assignment.entity.js'; // <--- 2. Import Entity
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// 3. ลงทะเบียน Entity ทั้ง User และ UserAssignment
|
||||
TypeOrmModule.forFeature([User, UserAssignment]),
|
||||
],
|
||||
controllers: [UserController],
|
||||
providers: [
|
||||
UserService,
|
||||
UserAssignmentService // <--- 4. ลงทะเบียน Service เป็น Provider
|
||||
],
|
||||
exports: [
|
||||
UserService,
|
||||
UserAssignmentService // <--- 5. Export เผื่อที่อื่นใช้
|
||||
],
|
||||
})
|
||||
export class UserModule {}
|
||||
```
|
||||
|
||||
ลอง Save แล้วรัน `pnpm start:dev` อีกครั้งครับ Error นี้จะหายไป และคุณจะสามารถใช้ฟังก์ชัน Assign Role ได้แล้วครับ\!
|
||||
|
||||
## T1.4 RBAC Guard (4-Level Authorization)
|
||||
|
||||
ยินดีด้วยครับ\! 👏👏👏
|
||||
1386
T2-0 Setting Project.md
Normal file
1386
T2-0 Setting Project.md
Normal file
File diff suppressed because it is too large
Load Diff
72
T2-Postman.md
Normal file
72
T2-Postman.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# **Manual Integration Testing (Postman Checklist)** สำหรับ **Phase 2: High-Integrity Data & Security** โดยเฉพาะครับ
|
||||
|
||||
Phase นี้เน้นเรื่อง **การจัดการไฟล์ (File Storage)**, **ความปลอดภัย (Security)** และ **ระบบตรวจสอบข้อมูล (JSON Schema)** ครับ
|
||||
|
||||
-----
|
||||
|
||||
## 📋 Phase 2 Integration Test Plan
|
||||
|
||||
**Pre-requisites (เตรียมข้อมูลก่อนเริ่ม):**
|
||||
|
||||
1. **Server:** รัน `pnpm start:dev`
|
||||
2. **Auth:** Login ด้วย `admin` เพื่อขอ Access Token (ใช้แนบใน Header: `Authorization: Bearer <token>`)
|
||||
|
||||
-----
|
||||
|
||||
### 🧪 Scenario 1: File Storage (T2.2)
|
||||
|
||||
**เป้าหมาย:** ทดสอบว่าระบบอัปโหลดไฟล์ทำงานถูกต้อง (Two-Phase Storage)
|
||||
|
||||
| Step | Action (API Endpoint) | Method | Body (Form-Data) | Expected Result |
|
||||
| :------ | :-------------------------------------------------- | :----- | :---------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **1.1** | **Upload Valid File**<br>`/api/files/upload` | POST | Key: `file` (Type: File)<br>Value: (เลือกไฟล์ PDF/IMG ขนาด \< 50MB) | - **Status: 201 Created**<br>- Response มี `id`, `originalFilename`<br>- `isTemporary`: **true**<br>- `tempId`: (มีค่า UUID) |
|
||||
| **1.2** | **Upload Invalid File Type**<br>`/api/files/upload` | POST | Key: `file` (Type: File)<br>Value: (เลือกไฟล์ .exe หรือ .bat) | - **Status: 400 Bad Request**<br>- Message: "Validation failed... expected type is..." |
|
||||
| **1.3** | **Upload Too Large File**<br>`/api/files/upload` | POST | Key: `file` (Type: File)<br>Value: (ไฟล์ขนาด \> 50MB) | - **Status: 413 Payload Too Large** หรือ **400 Bad Request** |
|
||||
|
||||
*หมายเหตุ: การ Commit ไฟล์ (ย้ายจาก Temp -\> Permanent) จะเกิดขึ้นอัตโนมัติเมื่อเรานำไฟล์ไปผูกกับเอกสารใน Phase 3*
|
||||
|
||||
-----
|
||||
|
||||
### 🧪 Scenario 2: JSON Schema Validation (T2.5)
|
||||
|
||||
**เป้าหมาย:** ทดสอบระบบตรวจสอบโครงสร้างข้อมูล JSON
|
||||
|
||||
| Step | Action (API Endpoint) | Method | Body (JSON) | Expected Result |
|
||||
| :------ | :-------------------------------------------------------------------- | :----- | :------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------- |
|
||||
| **2.1** | **Register Schema**<br>`/api/json-schemas/TEST_SCHEMA` | POST | `{ "type": "object", "properties": { "age": { "type": "integer" } }, "required": ["age"] }` | - **Status: 201 Created**<br>- Response มี `id`, `schemaCode`: "TEST\_SCHEMA" |
|
||||
| **2.2** | **Validate Valid Data**<br>`/api/json-schemas/TEST_SCHEMA/validate` | POST | `{ "age": 25 }` | - **Status: 201 Created**<br>- Response: `{ "valid": true }` |
|
||||
| **2.3** | **Validate Invalid Data**<br>`/api/json-schemas/TEST_SCHEMA/validate` | POST | `{ "age": "twenty-five" }` | - **Status: 400 Bad Request**<br>- Message: "JSON Validation Failed..." |
|
||||
|
||||
-----
|
||||
|
||||
### 🧪 Scenario 3: Security & Rate Limiting (T2.4)
|
||||
|
||||
**เป้าหมาย:** ทดสอบระบบป้องกันการโจมตี
|
||||
|
||||
| Step | Action (API Endpoint) | Method | Details | Expected Result |
|
||||
| :------ | :------------------------------------------------- | :----- | :-------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **3.1** | **Brute Force Login**<br>`/api/auth/login` | POST | กด Send รัวๆ เกิน 5 ครั้ง ภายใน 1 นาที | - **ครั้งที่ 1-5:** Status 201/401 (ปกติ)<br>- **ครั้งที่ 6+:** **Status 429 Too Many Requests**<br>- Message: "ThrottlerException: Too Many Requests" |
|
||||
| **3.2** | **Security Headers**<br>(ตรวจสอบ Response Headers) | ANY | ยิง Request อะไรก็ได้ | - Header `X-Powered-By` **ต้องไม่มี** (ถูก Helmet ซ่อน)<br>- Header `Content-Security-Policy` **ต้องมี** |
|
||||
|
||||
-----
|
||||
|
||||
### 🧪 Scenario 4: Document Numbering (T2.3)
|
||||
|
||||
**เป้าหมาย:** ทดสอบการรันเลขที่เอกสาร (ทดสอบผ่านการสร้างเอกสารใน Phase 3)
|
||||
|
||||
*เนื่องจาก Service นี้เป็น Internal เราจะทดสอบผ่านการสร้าง Correspondence*
|
||||
|
||||
| Step | Action (API Endpoint) | Method | Body (JSON) | Expected Result |
|
||||
| :------ | :------------------------------------------------- | :----- | :---------------------------------------------------------- | :----------------------------------------------------------------- |
|
||||
| **4.1** | **Generate Number**<br>`/api/correspondences` | POST | `{ "projectId": 1, "typeId": 1, "title": "Test Number 1" }` | - `correspondenceNumber` ลงท้ายด้วย **0001** (หรือเลขล่าสุด +1) |
|
||||
| **4.2** | **Generate Next Number**<br>`/api/correspondences` | POST | `{ "projectId": 1, "typeId": 1, "title": "Test Number 2" }` | - `correspondenceNumber` ต้องเป็นเลขถัดไป (เช่น **0002**) ห้ามซ้ำกับข้อ 4.1 |
|
||||
|
||||
-----
|
||||
|
||||
### ✅ Checklist การตรวจสอบใน Server (Files)
|
||||
|
||||
1. ไปที่โฟลเดอร์โปรเจกต์
|
||||
2. ตรวจสอบโฟลเดอร์ `uploads/temp`
|
||||
3. **สิ่งที่ต้องเจอ:** ไฟล์ที่อัปโหลดในข้อ **1.1** ต้องปรากฏอยู่ในนี้ โดยชื่อไฟล์จะเป็น UUID (ไม่ใช่ชื่อเดิม)
|
||||
|
||||
ถ้าผ่านครบทุกข้อนี้ แสดงว่า **Phase 2 (Infrastructure & Integrity)** แข็งแกร่งพร้อมใช้งานครับ\!
|
||||
3272
T3-0 Setting Project.md
Normal file
3272
T3-0 Setting Project.md
Normal file
File diff suppressed because it is too large
Load Diff
74
T3-Postman.md
Normal file
74
T3-Postman.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# **Manual Integration Testing (Postman Checklist)** สำหรับ **Phase 3: Unified Workflow Engine** ที่คุณสามารถนำไปใช้ทดสอบผ่าน Postman ได้ทันทีครับ
|
||||
|
||||
แผนนี้ออกแบบมาเพื่อทดสอบการทำงานร่วมกันของ `Create` -\> `Submit` -\> `Process Action` ให้ครอบคลุมทั้งกรณีปกติ (Happy Path) และกรณีขัดแย้ง (Edge Cases) ครับ
|
||||
|
||||
-----
|
||||
|
||||
## 📋 Phase 3 Integration Test Plan: Correspondence Workflow
|
||||
|
||||
**Pre-requisites (เตรียมข้อมูลก่อนเริ่ม):**
|
||||
|
||||
1. **Users:**
|
||||
* `admin` (Superadmin) - เอาไว้สร้าง Master Data
|
||||
* `user_org1` (อยู่ Org ID: 1) - เป็นคนสร้างเอกสาร (Originator)
|
||||
* `user_org2` (อยู่ Org ID: 2) - เป็นคนอนุมัติ (Reviewer)
|
||||
2. **Master Data:**
|
||||
* มี `correspondence_types` (เช่น ID: 1 = RFA)
|
||||
* มี `correspondence_status` (เช่น ID: 1 = DRAFT)
|
||||
* มี `organizations` (ID: 1 และ 2)
|
||||
3. **Template:**
|
||||
* รัน SQL Seed สร้าง Template ID: 1 (Step 1 -\> Org 1, Step 2 -\> Org 2) ตามที่เคยทำไป
|
||||
|
||||
-----
|
||||
|
||||
### 🧪 Scenario 1: Happy Path (Create -\> Submit -\> Approve -\> Complete)
|
||||
|
||||
**เป้าหมาย:** ทดสอบการไหลของงานปกติจนจบกระบวนการ
|
||||
|
||||
| Step | Action (API Endpoint) | Method | Actor (Token) | Body (JSON) | Expected Result |
|
||||
| :------ | :---------------------------------------------------------------------- | :----- | :----------------------------- | :---------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **1.1** | **Create Document**<br>`/api/correspondences` | POST | `user_org1` | `{ "projectId": 1, "typeId": 1, "title": "Test Workflow 01", "details": {} }` | - Status: `201 Created`<br>- Response มี `id` (จดไว้ สมมติ `10`)<br>- Response มี `correspondenceNumber` |
|
||||
| **1.2** | **Submit Document**<br>`/api/correspondences/10/submit` | POST | `user_org1` | `{ "templateId": 1 }` | - Status: `201 Created`<br>- Response คือ `CorrespondenceRouting`<br>- `sequence`: 1<br>- `status`: "SENT"<br>- `toOrganizationId`: 1 (ส่งหาตัวเองก่อนตาม Template) |
|
||||
| **1.3** | **Approve Step 1**<br>`/api/correspondences/10/workflow/action` | POST | `user_org1` | `{ "action": "APPROVE", "comments": "Review passed" }` | - Status: `201 Created`<br>- **Result:** "Action processed successfully"<br>- มีการสร้าง Step ถัดไป (Sequence 2) ส่งไปหา Org 2 |
|
||||
| **1.4** | **Approve Step 2 (Final)**<br>`/api/correspondences/10/workflow/action` | POST | `user_org2`<br>*(เปลี่ยน Token)* | `{ "action": "APPROVE", "comments": "Final Approval" }` | - Status: `201 Created`<br>- **Result:** "Action processed successfully"<br>- **ไม่สร้าง Step ถัดไป** (เพราะหมดแล้ว)<br>- Workflow จบสมบูรณ์ |
|
||||
|
||||
-----
|
||||
|
||||
### 🧪 Scenario 2: Rejection Flow (การปฏิเสธเอกสาร)
|
||||
|
||||
**เป้าหมาย:** ทดสอบว่าเมื่อกด Reject แล้ว Workflow ต้องหยุดทันที
|
||||
|
||||
| Step | Action (API Endpoint) | Method | Actor (Token) | Body (JSON) | Expected Result |
|
||||
| :------ | :--------------------------------------------------------------- | :----- | :------------ | :----------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- |
|
||||
| **2.1** | **Create Document**<br>`/api/correspondences` | POST | `user_org1` | `{ "projectId": 1, "typeId": 1, "title": "Test Reject 01" }` | - ได้ `id` ใหม่ (สมมติ `11`) |
|
||||
| **2.2** | **Submit Document**<br>`/api/correspondences/11/submit` | POST | `user_org1` | `{ "templateId": 1 }` | - สร้าง Routing Sequence 1 |
|
||||
| **2.3** | **Reject Document**<br>`/api/correspondences/11/workflow/action` | POST | `user_org1` | `{ "action": "REJECT", "comments": "Invalid Data" }` | - Status: `201 Created`<br>- Step 1 Status เปลี่ยนเป็น `REJECTED`<br>- **ไม่มีการสร้าง Step 2** (Workflow หยุด) |
|
||||
|
||||
-----
|
||||
|
||||
### 🧪 Scenario 3: Security Check (ข้ามหน้าข้ามตา)
|
||||
|
||||
**เป้าหมาย:** ทดสอบว่าคนนอก (User ที่ไม่อยู่ใน Org ปลายทาง) จะไม่สามารถกดอนุมัติได้
|
||||
|
||||
| Step | Action (API Endpoint) | Method | Actor (Token) | Body (JSON) | Expected Result |
|
||||
| :------ | :------------------------------------------------ | :----- | :--------------------------------- | :------------------------ | :------------------------------------------------------------------------------------------------------------- |
|
||||
| **3.1** | **Create & Submit** | POST | `user_org1` | *(ทำเหมือน 1.1 และ 1.2)* | - ได้ `id` ใหม่ (สมมติ `12`)<br>- Routing Seq 1 (ส่งหา Org 1) |
|
||||
| **3.2** | **Approve Step 1** | POST | `user_org1` | `{ "action": "APPROVE" }` | - ผ่าน (เพราะ User 1 อยู่ Org 1)<br>- สร้าง Seq 2 (ส่งหา Org 2) |
|
||||
| **3.3** | **Try to Approve Step 2**<br>*(โดยใช้ User Org 1)* | POST | **`user_org1`**<br>*(เจตนาใช้ผิดคน)* | `{ "action": "APPROVE" }` | - **Status: `400 Bad Request`**<br>- Message: "You are not authorized to process this step"<br>*(ป้องกันสำเร็จ\!)* |
|
||||
|
||||
-----
|
||||
|
||||
### ✅ Checklist การตรวจสอบผลลัพธ์ใน Database
|
||||
|
||||
หลังจากรัน Scenario 1 จบแล้ว ให้ลอง Query ดูใน Database เพื่อความมั่นใจครับ:
|
||||
|
||||
```sql
|
||||
SELECT * FROM correspondence_routings WHERE correspondence_id = 10 ORDER BY sequence;
|
||||
```
|
||||
|
||||
**สิ่งที่ควรเจอ:**
|
||||
|
||||
1. Row 1: `sequence`=1, `status`='ACTIONED', `comments`='Review passed'
|
||||
2. Row 2: `sequence`=2, `status`='ACTIONED', `comments`='Final Approval'
|
||||
|
||||
ถ้าผลการทดสอบเป็นไปตามนี้ทั้งหมด แสดงว่า **Phase 3 สมบูรณ์แบบ** พร้อมไปต่อ Phase 4 (RFA) ได้เลยครับ\!
|
||||
@@ -16,11 +16,22 @@ import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
|
||||
import { RbacGuard } from '../../common/auth/rbac.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
|
||||
@Controller('correspondences')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class CorrespondenceController {
|
||||
constructor(private readonly correspondenceService: CorrespondenceService) {}
|
||||
|
||||
@Post(':id/workflow/action')
|
||||
@RequirePermission('workflow.action_review') // สิทธิ์ในการกดอนุมัติ/ตรวจสอบ
|
||||
processAction(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() actionDto: WorkflowActionDto,
|
||||
@Request() req: any,
|
||||
) {
|
||||
return this.correspondenceService.processAction(id, actionDto, req.user);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('correspondence.create') // 🔒 ต้องมีสิทธิ์สร้าง
|
||||
create(@Body() createDto: CreateCorrespondenceDto, @Request() req: any) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { DocumentNumberingModule } from '../document-numbering/document-numberin
|
||||
import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details
|
||||
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
|
||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
|
||||
@@ -18,6 +18,10 @@ import { User } from '../user/entities/user.entity.js';
|
||||
|
||||
// DTOs
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
|
||||
|
||||
// Interfaces
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface.js';
|
||||
|
||||
// Services
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
|
||||
@@ -46,15 +50,9 @@ export class CorrespondenceService {
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* สร้างเอกสารใหม่ (Create Correspondence)
|
||||
* - ตรวจสอบสิทธิ์และข้อมูลพื้นฐาน
|
||||
* - Validate JSON Details ตาม Type
|
||||
* - ขอเลขที่เอกสาร (Redis Lock)
|
||||
* - บันทึกข้อมูลลง DB (Transaction)
|
||||
*/
|
||||
// --- 1. CREATE DOCUMENT ---
|
||||
async create(createDto: CreateCorrespondenceDto, user: User) {
|
||||
// 1. ตรวจสอบข้อมูลพื้นฐาน (Type, Status, Org)
|
||||
// 1.1 Validate Basic Info
|
||||
const type = await this.typeRepo.findOne({
|
||||
where: { id: createDto.typeId },
|
||||
});
|
||||
@@ -64,39 +62,29 @@ export class CorrespondenceService {
|
||||
where: { statusCode: 'DRAFT' },
|
||||
});
|
||||
if (!statusDraft) {
|
||||
throw new InternalServerErrorException(
|
||||
'Status DRAFT not found in Master Data',
|
||||
);
|
||||
throw new InternalServerErrorException('Status DRAFT not found');
|
||||
}
|
||||
|
||||
const userOrgId = user.primaryOrganizationId;
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create documents',
|
||||
);
|
||||
throw new BadRequestException('User must belong to an organization');
|
||||
}
|
||||
|
||||
// 2. Validate JSON Details (ถ้ามี)
|
||||
// 1.2 Validate JSON Details
|
||||
if (createDto.details) {
|
||||
try {
|
||||
// ใช้ Type Code เป็น Key ในการค้นหา Schema (เช่น 'RFA', 'LETTER')
|
||||
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
|
||||
} catch (error: any) {
|
||||
// บันทึก Warning หรือ Throw Error ตามนโยบาย (ในที่นี้ให้ผ่านไปก่อนถ้ายังไม่สร้าง Schema)
|
||||
console.warn(
|
||||
`Schema validation warning for ${type.typeCode}: ${error.message}`,
|
||||
);
|
||||
console.warn(`Schema validation warning: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. เริ่ม Transaction
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 3.1 ขอเลขที่เอกสาร (Double-Lock Mechanism)
|
||||
// Mock ค่า replacements ไว้ก่อน (จริงๆ ต้อง Join เอา Org Code มา)
|
||||
// 1.3 Generate Document Number (Double-Lock)
|
||||
const docNumber = await this.numberingService.generateNextNumber(
|
||||
createDto.projectId,
|
||||
userOrgId,
|
||||
@@ -104,11 +92,11 @@ export class CorrespondenceService {
|
||||
new Date().getFullYear(),
|
||||
{
|
||||
TYPE_CODE: type.typeCode,
|
||||
ORG_CODE: 'ORG', // TODO: Fetch real organization code
|
||||
ORG_CODE: 'ORG', // In real app, fetch user's org code
|
||||
},
|
||||
);
|
||||
|
||||
// 3.2 สร้าง Correspondence (หัวจดหมาย)
|
||||
// 1.4 Save Head
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: createDto.typeId,
|
||||
@@ -119,7 +107,7 @@ export class CorrespondenceService {
|
||||
});
|
||||
const savedCorr = await queryRunner.manager.save(correspondence);
|
||||
|
||||
// 3.3 สร้าง Revision แรก (Rev 0)
|
||||
// 1.5 Save First Revision
|
||||
const revision = queryRunner.manager.create(CorrespondenceRevision, {
|
||||
correspondenceId: savedCorr.id,
|
||||
revisionNumber: 0,
|
||||
@@ -132,7 +120,6 @@ export class CorrespondenceService {
|
||||
});
|
||||
await queryRunner.manager.save(revision);
|
||||
|
||||
// 4. Commit Transaction
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
@@ -140,7 +127,6 @@ export class CorrespondenceService {
|
||||
currentRevision: revision,
|
||||
};
|
||||
} catch (err) {
|
||||
// Rollback หากเกิดข้อผิดพลาด
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -148,37 +134,29 @@ export class CorrespondenceService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึงข้อมูลเอกสารทั้งหมด (สำหรับ List Page)
|
||||
*/
|
||||
// --- READ ---
|
||||
async findAll() {
|
||||
return this.correspondenceRepo.find({
|
||||
relations: ['revisions', 'type', 'project', 'originator'],
|
||||
relations: ['revisions', 'type', 'project'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึงข้อมูลเอกสารรายตัว (Detail Page)
|
||||
*/
|
||||
async findOne(id: number) {
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['revisions', 'type', 'project', 'originator'],
|
||||
relations: ['revisions', 'type', 'project'],
|
||||
});
|
||||
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException(`Correspondence with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return correspondence;
|
||||
}
|
||||
|
||||
/**
|
||||
* ส่งเอกสาร (Submit) เพื่อเริ่ม Workflow การอนุมัติ/ส่งต่อ
|
||||
*/
|
||||
// --- 2. SUBMIT WORKFLOW ---
|
||||
async submit(correspondenceId: number, templateId: number, user: User) {
|
||||
// 1. ดึงข้อมูลเอกสารและหา Revision ปัจจุบัน
|
||||
// 2.1 Get Document & Current Revision
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
relations: ['revisions'],
|
||||
@@ -188,13 +166,12 @@ export class CorrespondenceService {
|
||||
throw new NotFoundException('Correspondence not found');
|
||||
}
|
||||
|
||||
// หา Revision ที่เป็น current
|
||||
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
|
||||
if (!currentRevision) {
|
||||
throw new NotFoundException('Current revision not found');
|
||||
}
|
||||
|
||||
// 2. ดึงข้อมูล Template และ Steps
|
||||
// 2.2 Get Template Config
|
||||
const template = await this.templateRepo.findOne({
|
||||
where: { id: templateId },
|
||||
relations: ['steps'],
|
||||
@@ -202,12 +179,9 @@ export class CorrespondenceService {
|
||||
});
|
||||
|
||||
if (!template || !template.steps?.length) {
|
||||
throw new BadRequestException(
|
||||
'Invalid routing template or no steps defined',
|
||||
);
|
||||
throw new BadRequestException('Invalid routing template');
|
||||
}
|
||||
|
||||
// 3. เริ่ม Transaction
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
@@ -215,29 +189,23 @@ export class CorrespondenceService {
|
||||
try {
|
||||
const firstStep = template.steps[0];
|
||||
|
||||
// 3.1 สร้าง Routing Record แรก (Log การส่งต่อ)
|
||||
// 2.3 Create First Routing Record
|
||||
const routing = queryRunner.manager.create(CorrespondenceRouting, {
|
||||
correspondenceId: currentRevision.id, // เชื่อมกับ Revision ID
|
||||
correspondenceId: currentRevision.id,
|
||||
templateId: template.id, // ✅ Save templateId for reference
|
||||
sequence: 1,
|
||||
fromOrganizationId: user.primaryOrganizationId,
|
||||
toOrganizationId: firstStep.toOrganizationId,
|
||||
stepPurpose: firstStep.stepPurpose,
|
||||
status: 'SENT', // สถานะเริ่มต้นของการส่ง
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
),
|
||||
processedByUserId: user.user_id, // ผู้ส่ง (User ปัจจุบัน)
|
||||
processedByUserId: user.user_id,
|
||||
processedAt: new Date(),
|
||||
});
|
||||
await queryRunner.manager.save(routing);
|
||||
|
||||
// 3.2 (Optional) อัปเดตสถานะของ Revision เป็น 'SUBMITTED'
|
||||
// const statusSubmitted = await this.statusRepo.findOne({ where: { statusCode: 'SUBMITTED' } });
|
||||
// if (statusSubmitted) {
|
||||
// currentRevision.statusId = statusSubmitted.id;
|
||||
// await queryRunner.manager.save(currentRevision);
|
||||
// }
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return routing;
|
||||
} catch (err) {
|
||||
@@ -247,4 +215,138 @@ export class CorrespondenceService {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. PROCESS ACTION (Approve/Reject/Return) ---
|
||||
async processAction(
|
||||
correspondenceId: number,
|
||||
dto: WorkflowActionDto,
|
||||
user: User,
|
||||
) {
|
||||
// 3.1 Find Active Routing Step
|
||||
// Find correspondence first to ensure it exists
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
relations: ['revisions'],
|
||||
});
|
||||
|
||||
if (!correspondence)
|
||||
throw new NotFoundException('Correspondence not found');
|
||||
|
||||
const currentRevision = correspondence.revisions?.find((r) => r.isCurrent);
|
||||
if (!currentRevision)
|
||||
throw new NotFoundException('Current revision not found');
|
||||
|
||||
// Find the latest routing step
|
||||
const currentRouting = await this.routingRepo.findOne({
|
||||
where: {
|
||||
correspondenceId: currentRevision.id,
|
||||
// In real scenario, we might check status 'SENT' or 'RECEIVED'
|
||||
},
|
||||
order: { sequence: 'DESC' },
|
||||
relations: ['toOrganization'],
|
||||
});
|
||||
|
||||
if (
|
||||
!currentRouting ||
|
||||
currentRouting.status === 'ACTIONED' ||
|
||||
currentRouting.status === 'REJECTED'
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'No active workflow step found or step already processed',
|
||||
);
|
||||
}
|
||||
|
||||
// 3.2 Check Permissions
|
||||
// User must belong to the target organization of the current step
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'You are not authorized to process this step',
|
||||
);
|
||||
}
|
||||
|
||||
// 3.3 Load Template to find Next Step Config
|
||||
if (!currentRouting.templateId) {
|
||||
throw new InternalServerErrorException(
|
||||
'Routing record missing templateId',
|
||||
);
|
||||
}
|
||||
|
||||
const template = await this.templateRepo.findOne({
|
||||
where: { id: currentRouting.templateId },
|
||||
relations: ['steps'],
|
||||
});
|
||||
|
||||
if (!template || !template.steps) {
|
||||
throw new InternalServerErrorException('Template definition not found');
|
||||
}
|
||||
|
||||
const totalSteps = template.steps.length;
|
||||
const currentSeq = currentRouting.sequence;
|
||||
|
||||
// 3.4 Calculate Next State using Workflow Engine
|
||||
const result = this.workflowEngine.processAction(
|
||||
currentSeq,
|
||||
totalSteps,
|
||||
dto.action,
|
||||
dto.returnToSequence,
|
||||
);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 3.5 Update Current Step
|
||||
currentRouting.status =
|
||||
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
|
||||
currentRouting.processedByUserId = user.user_id;
|
||||
currentRouting.processedAt = new Date();
|
||||
currentRouting.comments = dto.comments;
|
||||
|
||||
await queryRunner.manager.save(currentRouting);
|
||||
|
||||
// 3.6 Create Next Step (If exists and not rejected)
|
||||
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
|
||||
// ✅ Find config for next step from Template
|
||||
const nextStepConfig = template.steps.find(
|
||||
(s) => s.sequence === result.nextStepSequence,
|
||||
);
|
||||
|
||||
if (!nextStepConfig) {
|
||||
throw new InternalServerErrorException(
|
||||
`Configuration for step ${result.nextStepSequence} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const nextRouting = queryRunner.manager.create(CorrespondenceRouting, {
|
||||
correspondenceId: currentRevision.id,
|
||||
templateId: template.id,
|
||||
sequence: result.nextStepSequence,
|
||||
fromOrganizationId: user.primaryOrganizationId, // Forwarded by current user
|
||||
toOrganizationId: nextStepConfig.toOrganizationId, // ✅ Real Target from Template
|
||||
stepPurpose: nextStepConfig.stepPurpose, // ✅ Real Purpose from Template
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() +
|
||||
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
),
|
||||
});
|
||||
await queryRunner.manager.save(nextRouting);
|
||||
}
|
||||
|
||||
// 3.7 Update Document Status (Optional - if Engine suggests)
|
||||
if (result.shouldUpdateStatus) {
|
||||
// Example: Update revision status to APPROVED or REJECTED
|
||||
// await this.updateDocumentStatus(currentRevision, result.documentStatus);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { message: 'Action processed successfully', result };
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IsEnum, IsString, IsOptional, IsInt } from 'class-validator';
|
||||
import { WorkflowAction } from '../../workflow-engine/interfaces/workflow.interface.js';
|
||||
|
||||
export class WorkflowActionDto {
|
||||
@IsEnum(WorkflowAction)
|
||||
action!: WorkflowAction; // APPROVE, REJECT, RETURN, ACKNOWLEDGE
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comments?: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
returnToSequence?: number; // ใช้กรณี action = RETURN
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { RoutingTemplate } from './routing-template.entity.js'; // <--- ✅ เพิ่ม Import นี้ครับ
|
||||
|
||||
@Entity('correspondence_routings')
|
||||
export class CorrespondenceRouting {
|
||||
@@ -16,7 +17,10 @@ export class CorrespondenceRouting {
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'correspondence_id' })
|
||||
correspondenceId!: number; // FK -> CorrespondenceRevision
|
||||
correspondenceId!: number;
|
||||
|
||||
@Column({ name: 'template_id', nullable: true })
|
||||
templateId?: number;
|
||||
|
||||
@Column()
|
||||
sequence!: number;
|
||||
@@ -31,7 +35,7 @@ export class CorrespondenceRouting {
|
||||
stepPurpose!: string;
|
||||
|
||||
@Column({ default: 'SENT' })
|
||||
status!: string; // SENT, RECEIVED, ACTIONED, FORWARDED, REPLIED
|
||||
status!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
comments?: string;
|
||||
@@ -53,6 +57,10 @@ export class CorrespondenceRouting {
|
||||
@JoinColumn({ name: 'correspondence_id' })
|
||||
correspondenceRevision?: CorrespondenceRevision;
|
||||
|
||||
@ManyToOne(() => RoutingTemplate) // ตอนนี้ TypeScript จะรู้จัก RoutingTemplate แล้ว
|
||||
@JoinColumn({ name: 'template_id' })
|
||||
template?: RoutingTemplate;
|
||||
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'from_organization_id' })
|
||||
fromOrganization?: Organization;
|
||||
|
||||
24
backend/src/modules/user/dto/assign-role.dto.ts
Normal file
24
backend/src/modules/user/dto/assign-role.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IsInt, IsNotEmpty, IsOptional, ValidateIf } from 'class-validator';
|
||||
|
||||
export class AssignRoleDto {
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
roleId!: number;
|
||||
|
||||
// Scope (ต้องส่งมาอย่างน้อย 1 อัน หรือไม่ส่งเลยถ้าเป็น Global)
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
organizationId?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
projectId?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
contractId?: number;
|
||||
}
|
||||
42
backend/src/modules/user/entities/user-assignment.entity.ts
Normal file
42
backend/src/modules/user/entities/user-assignment.entity.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity.js';
|
||||
// Import Role, Org, Project, Contract entities...
|
||||
|
||||
@Entity('user_assignments')
|
||||
export class UserAssignment {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'user_id' })
|
||||
userId!: number;
|
||||
|
||||
@Column({ name: 'role_id' })
|
||||
roleId!: number;
|
||||
|
||||
@Column({ name: 'organization_id', nullable: true })
|
||||
organizationId?: number;
|
||||
|
||||
@Column({ name: 'project_id', nullable: true })
|
||||
projectId?: number;
|
||||
|
||||
@Column({ name: 'contract_id', nullable: true })
|
||||
contractId?: number;
|
||||
|
||||
@Column({ name: 'assigned_by_user_id', nullable: true })
|
||||
assignedByUserId?: number;
|
||||
|
||||
@CreateDateColumn({ name: 'assigned_at' })
|
||||
assignedAt!: Date;
|
||||
|
||||
// Relation กลับไปหา User (เจ้าของสิทธิ์)
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user?: User;
|
||||
}
|
||||
38
backend/src/modules/user/user-assignment.service.ts
Normal file
38
backend/src/modules/user/user-assignment.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserAssignment } from './entities/user-assignment.entity.js'; // ต้องไปสร้าง Entity นี้ก่อน (ดูข้อ 3)
|
||||
import { AssignRoleDto } from './dto/assign-role.dto.js';
|
||||
import { User } from './entities/user.entity.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserAssignmentService {
|
||||
constructor(
|
||||
@InjectRepository(UserAssignment)
|
||||
private assignmentRepo: Repository<UserAssignment>,
|
||||
) {}
|
||||
|
||||
async assignRole(dto: AssignRoleDto, assigner: User) {
|
||||
// Validation: ตรวจสอบกฎเหล็ก (เลือกได้แค่ Scope เดียว)
|
||||
const scopes = [dto.organizationId, dto.projectId, dto.contractId].filter(
|
||||
(v) => v != null,
|
||||
);
|
||||
if (scopes.length > 1) {
|
||||
throw new BadRequestException(
|
||||
'Cannot assign multiple scopes at once. Choose one of Org, Project, or Contract.',
|
||||
);
|
||||
}
|
||||
|
||||
// สร้าง Assignment
|
||||
const assignment = this.assignmentRepo.create({
|
||||
userId: dto.userId,
|
||||
roleId: dto.roleId,
|
||||
organizationId: dto.organizationId,
|
||||
projectId: dto.projectId,
|
||||
contractId: dto.contractId,
|
||||
assignedByUserId: assigner.user_id, // เก็บ Log ว่าใครเป็นคนให้สิทธิ์
|
||||
});
|
||||
|
||||
return this.assignmentRepo.save(assignment);
|
||||
}
|
||||
}
|
||||
@@ -8,40 +8,48 @@ import {
|
||||
Delete,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
Request, // <--- อย่าลืม Import Request
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service.js';
|
||||
import { CreateUserDto } from './dto/create-user.dto.js';
|
||||
import { UpdateUserDto } from './dto/update-user.dto.js';
|
||||
import { AssignRoleDto } from './dto/assign-role.dto.js'; // <--- Import DTO
|
||||
import { UserAssignmentService } from './user-assignment.service.js'; // <--- Import Service ใหม่
|
||||
|
||||
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
import { RbacGuard } from '../../common/auth/rbac.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
@Controller('users')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard) // 🔒 เพิ่ม RbacGuard ต่อท้าย) // 🔒 บังคับ Login ทุก Endpoints ในนี้
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly assignmentService: UserAssignmentService, // <--- ✅ Inject Service เข้ามา
|
||||
) {}
|
||||
|
||||
// --- User CRUD ---
|
||||
|
||||
// 1. สร้างผู้ใช้ใหม่
|
||||
@Post()
|
||||
@RequirePermission('user.create') // 🔒 ต้องมีสิทธิ์ user.create ถึงจะเข้าได้
|
||||
@RequirePermission('user.create')
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.userService.create(createUserDto);
|
||||
}
|
||||
|
||||
// 2. ดูรายชื่อผู้ใช้ทั้งหมด
|
||||
@Get()
|
||||
@RequirePermission('user.view')
|
||||
findAll() {
|
||||
return this.userService.findAll();
|
||||
}
|
||||
|
||||
// 3. ดูข้อมูลผู้ใช้รายคน (ตาม ID)
|
||||
@Get(':id')
|
||||
@RequirePermission('user.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.userService.findOne(id);
|
||||
}
|
||||
|
||||
// 4. แก้ไขข้อมูลผู้ใช้
|
||||
@Patch(':id')
|
||||
@RequirePermission('user.edit')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
@@ -49,9 +57,17 @@ export class UserController {
|
||||
return this.userService.update(id, updateUserDto);
|
||||
}
|
||||
|
||||
// 5. ลบผู้ใช้ (Soft Delete)
|
||||
@Delete(':id')
|
||||
@RequirePermission('user.delete')
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.userService.remove(id);
|
||||
}
|
||||
|
||||
// --- Role Assignment ---
|
||||
|
||||
@Post('assign-role') // <--- ✅ ต้องมี @ เสมอครับ
|
||||
@RequirePermission('permission.assign')
|
||||
assignRole(@Body() dto: AssignRoleDto, @Request() req: any) {
|
||||
return this.assignmentService.assignRole(dto, req.user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,22 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserService } from './user.service.js';
|
||||
import { UserController } from './user.controller.js'; // 1. Import Controller
|
||||
import { User } from './entities/user.entity.js';
|
||||
import { UserAssignmentService } from './user-assignment.service.js';
|
||||
import { UserAssignment } from './entities/user-assignment.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])], // จดทะเบียน Entity
|
||||
// 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
|
||||
imports: [
|
||||
// 3. ลงทะเบียน Entity ทั้ง User และ UserAssignment
|
||||
TypeOrmModule.forFeature([User, UserAssignment]),
|
||||
], // 2. เพิ่มบรรทัดนี้ เพื่อบอก NestJS ว่ามี Controller นี้อยู่
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
exports: [UserService], // Export ให้ AuthModule เรียกใช้ได้
|
||||
providers: [
|
||||
UserService,
|
||||
UserAssignmentService, // <--- 4. ลงทะเบียน Service เป็น Provider
|
||||
],
|
||||
exports: [
|
||||
UserService,
|
||||
UserAssignmentService, // <--- 5. Export เผื่อที่อื่นใช้
|
||||
], // Export ให้ AuthModule เรียกใช้ได้
|
||||
})
|
||||
export class UserModule {}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { WorkflowEngineService } from './workflow-engine.service';
|
||||
import { WorkflowAction } from './interfaces/workflow.interface';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
describe('WorkflowEngineService', () => {
|
||||
let service: WorkflowEngineService;
|
||||
@@ -15,4 +17,50 @@ describe('WorkflowEngineService', () => {
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('processAction', () => {
|
||||
// 🟢 กรณี: อนุมัติทั่วไป (ไปขั้นต่อไป)
|
||||
it('should move to next step on APPROVE', () => {
|
||||
const result = service.processAction(1, 3, WorkflowAction.APPROVE);
|
||||
expect(result.nextStepSequence).toBe(2);
|
||||
expect(result.shouldUpdateStatus).toBe(false);
|
||||
});
|
||||
|
||||
// 🟢 กรณี: อนุมัติขั้นตอนสุดท้าย (จบงาน)
|
||||
it('should complete workflow on APPROVE at last step', () => {
|
||||
const result = service.processAction(3, 3, WorkflowAction.APPROVE);
|
||||
expect(result.nextStepSequence).toBeNull(); // ไม่มีขั้นต่อไป
|
||||
expect(result.shouldUpdateStatus).toBe(true);
|
||||
expect(result.documentStatus).toBe('COMPLETED');
|
||||
});
|
||||
|
||||
// 🔴 กรณี: ปฏิเสธ (จบงานทันที)
|
||||
it('should stop workflow on REJECT', () => {
|
||||
const result = service.processAction(1, 3, WorkflowAction.REJECT);
|
||||
expect(result.nextStepSequence).toBeNull();
|
||||
expect(result.shouldUpdateStatus).toBe(true);
|
||||
expect(result.documentStatus).toBe('REJECTED');
|
||||
});
|
||||
|
||||
// 🟠 กรณี: ส่งกลับ (ย้อนกลับ 1 ขั้น)
|
||||
it('should return to previous step on RETURN', () => {
|
||||
const result = service.processAction(2, 3, WorkflowAction.RETURN);
|
||||
expect(result.nextStepSequence).toBe(1);
|
||||
expect(result.shouldUpdateStatus).toBe(true);
|
||||
expect(result.documentStatus).toBe('REVISE_REQUIRED');
|
||||
});
|
||||
|
||||
// 🟠 กรณี: ส่งกลับ (ระบุขั้น)
|
||||
it('should return to specific step on RETURN', () => {
|
||||
const result = service.processAction(3, 5, WorkflowAction.RETURN, 1);
|
||||
expect(result.nextStepSequence).toBe(1);
|
||||
});
|
||||
|
||||
// ❌ กรณี: Error (ส่งกลับต่ำกว่า 1)
|
||||
it('should throw error if return step is invalid', () => {
|
||||
expect(() => {
|
||||
service.processAction(1, 3, WorkflowAction.RETURN);
|
||||
}).toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
BIN
backend/uploads/temp/d60d9807-a22d-4ca0-b99a-5d5d8b81b3e8.pdf
Normal file
BIN
backend/uploads/temp/d60d9807-a22d-4ca0-b99a-5d5d8b81b3e8.pdf
Normal file
Binary file not shown.
0
lcbp3_dev.session.sql
Normal file
0
lcbp3_dev.session.sql
Normal file
16
temp.ts
Normal file
16
temp.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// ใน function submit()
|
||||
// 2.1 สร้าง Routing Record แรก
|
||||
const routing = queryRunner.manager.create(CorrespondenceRouting, {
|
||||
correspondenceId: currentRevision.id,
|
||||
templateId: template.id, // ✅ บันทึก templateId ไว้ใช้อ้างอิง
|
||||
sequence: 1,
|
||||
fromOrganizationId: user.primaryOrganizationId,
|
||||
toOrganizationId: firstStep.toOrganizationId,
|
||||
stepPurpose: firstStep.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
),
|
||||
processedByUserId: user.user_id,
|
||||
processedAt: new Date(),
|
||||
});
|
||||
173
users.sql
Normal file
173
users.sql
Normal file
@@ -0,0 +1,173 @@
|
||||
-- phpMyAdmin SQL Dump
|
||||
-- version 5.2.3
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- Host: mariadb
|
||||
-- Generation Time: Nov 21, 2025 at 03:33 AM
|
||||
-- Server version: 11.8.5-MariaDB-ubu2404
|
||||
-- PHP Version: 8.3.27
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */
|
||||
;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */
|
||||
;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */
|
||||
;
|
||||
/*!40101 SET NAMES utf8mb4 */
|
||||
;
|
||||
--
|
||||
-- Database: `lcbp3_dev`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
--
|
||||
-- Table structure for table `users`
|
||||
--
|
||||
|
||||
CREATE TABLE `users` (
|
||||
`user_id` int(11) NOT NULL COMMENT 'ID ของตาราง',
|
||||
`username` varchar(50) NOT NULL COMMENT 'ชื่อผู้ใช้งาน',
|
||||
`password_hash` varchar(255) NOT NULL COMMENT 'รหัสผ่าน (Hashed)',
|
||||
`first_name` varchar(50) DEFAULT NULL COMMENT 'ชื่อจริง',
|
||||
`last_name` varchar(50) DEFAULT NULL COMMENT 'นามสกุล',
|
||||
`email` varchar(100) NOT NULL COMMENT 'อีเมล',
|
||||
`line_id` varchar(100) DEFAULT NULL COMMENT 'LINE ID',
|
||||
`primary_organization_id` int(11) DEFAULT NULL COMMENT 'สังกัดองค์กร',
|
||||
`is_active` tinyint(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน',
|
||||
`failed_attempts` int(11) DEFAULT 0 COMMENT 'จำนวนครั้งที่ล็อกอินล้มเหลว',
|
||||
`locked_until` datetime DEFAULT NULL COMMENT 'ล็อกอินไม่ได้จนถึงเวลา',
|
||||
`last_login_at` timestamp NULL DEFAULT NULL COMMENT 'วันที่และเวลาที่ล็อกอินล่าสุด',
|
||||
`created_at` timestamp NULL DEFAULT current_timestamp() COMMENT 'วันที่สร้าง',
|
||||
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'วันที่แก้ไขล่าสุด',
|
||||
`deleted_at` datetime DEFAULT NULL COMMENT 'วันที่ลบ'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)';
|
||||
--
|
||||
-- Dumping data for table `users`
|
||||
--
|
||||
|
||||
INSERT INTO `users` (
|
||||
`user_id`,
|
||||
`username`,
|
||||
`password_hash`,
|
||||
`first_name`,
|
||||
`last_name`,
|
||||
`email`,
|
||||
`line_id`,
|
||||
`primary_organization_id`,
|
||||
`is_active`,
|
||||
`failed_attempts`,
|
||||
`locked_until`,
|
||||
`last_login_at`,
|
||||
`created_at`,
|
||||
`updated_at`,
|
||||
`deleted_at`
|
||||
)
|
||||
VALUES (
|
||||
1,
|
||||
'superadmin',
|
||||
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
|
||||
'Super',
|
||||
'Admin',
|
||||
'superadmin @example.com',
|
||||
NULL,
|
||||
NULL,
|
||||
1,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
'2025-11-19 08:47:47',
|
||||
'2025-11-21 03:02:20',
|
||||
NULL
|
||||
),
|
||||
(
|
||||
2,
|
||||
'editor01',
|
||||
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
|
||||
'DC',
|
||||
'C1',
|
||||
'editor01 @example.com',
|
||||
NULL,
|
||||
41,
|
||||
1,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
'2025-11-19 08:47:47',
|
||||
'2025-11-20 02:57:04',
|
||||
NULL
|
||||
),
|
||||
(
|
||||
3,
|
||||
'viewer01',
|
||||
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
|
||||
'Viewer',
|
||||
'สคฉ.03',
|
||||
'viewer01 @example.com',
|
||||
NULL,
|
||||
10,
|
||||
1,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
'2025-11-19 08:47:47',
|
||||
'2025-11-20 02:55:50',
|
||||
NULL
|
||||
),
|
||||
(
|
||||
5,
|
||||
'admin',
|
||||
'$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW',
|
||||
'Admin',
|
||||
'คคง.',
|
||||
'admin@example.com',
|
||||
NULL,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
'2025-11-19 08:57:20',
|
||||
'2025-11-21 02:56:02',
|
||||
NULL
|
||||
);
|
||||
--
|
||||
-- Indexes for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Indexes for table `users`
|
||||
--
|
||||
ALTER TABLE `users`
|
||||
ADD PRIMARY KEY (`user_id`),
|
||||
ADD UNIQUE KEY `username` (`username`),
|
||||
ADD UNIQUE KEY `email` (`email`),
|
||||
ADD KEY `primary_organization_id` (`primary_organization_id`);
|
||||
--
|
||||
-- AUTO_INCREMENT for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- AUTO_INCREMENT for table `users`
|
||||
--
|
||||
ALTER TABLE `users`
|
||||
MODIFY `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||
AUTO_INCREMENT = 6;
|
||||
--
|
||||
-- Constraints for dumped tables
|
||||
--
|
||||
|
||||
--
|
||||
-- Constraints for table `users`
|
||||
--
|
||||
ALTER TABLE `users`
|
||||
ADD CONSTRAINT `users_ibfk_1` FOREIGN KEY (`primary_organization_id`) REFERENCES `organizations` (`id`) ON DELETE
|
||||
SET NULL;
|
||||
COMMIT;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */
|
||||
;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */
|
||||
;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */
|
||||
;
|
||||
Reference in New Issue
Block a user