62 KiB
3.11 Document Numbering Management (การจัดการเลขที่เอกสาร)
title: 'Functional Requirements: Document Numbering Management' version: 1.5.1 status: draft owner: Nattanin Peancharoen last_updated: 2025-12-02 related:
- specs/01-requirements/01-objectives.md
- specs/01-requirements/02-architecture.md
- specs/01-requirements/03-functional-requirements.md
- specs/03-implementation/document-numbering.md
- specs/04-operations/document-numbering-operations.md
- specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md
📖 เอกสารที่เกี่ยวข้อง
- Implementation Guide: document-numbering.md - รายละเอียดการ implement ด้วย NestJS, TypeORM, Redis
- Operations Guide: document-numbering-operations.md - Monitoring, Troubleshooting, Maintenance Procedures
3.11.1. วัตถุประสงค์
- ระบบต้องสามารถสร้างเลขที่เอกสาร (Running Number) ได้โดยอัตโนมัติและยืดหยุ่นสูง
- ระบบต้องสามารถกำหนดรูปแบบ (template) เลขที่เอกสารได้ สำหรับแต่ละโครงการ, ชนิดเอกสาร, ประเภทเอกสาร
- ระบบต้องรับประกัน Uniqueness ของเลขที่เอกสารในทุกสถานการณ์
- ระบบต้องรองรับการทำงานแบบ concurrent ได้อย่างปลอดภัย
3.11.2. Logic การนับเลข (Counter Logic)
การนับเลขจะแยกตาม Counter Key ที่ประกอบด้วยหลายส่วน ขึ้นกับประเภทเอกสาร
Counter Key Components
| Component | Required? | Description | Database Source | Default if NULL |
|---|---|---|---|---|
project_id |
✅ Yes | ID โครงการ | Derived from user context or organization | - |
originator_organization_id |
✅ Yes | ID องค์กรผู้ส่ง | correspondences.originator_id |
- |
recipient_organization_id |
Depends on type | ID องค์กรผู้รับหลัก (TO) | correspondence_recipients where recipient_type = 'TO' |
NULL for RFA |
correspondence_type_id |
✅ Yes | ID ประเภทเอกสาร | correspondence_types.id |
- |
sub_type_id |
TRANSMITTAL only | ID ประเภทย่อย | correspondence_sub_types.id |
0 |
rfa_type_id |
RFA only | ID ประเภท RFA | rfa_types.id |
0 |
discipline_id |
RFA only | ID สาขางาน | disciplines.id |
0 |
current_year |
✅ Yes | ปี ค.ศ. | System year (ปัจจุบัน) | - |
Counter Key แยกตามประเภทเอกสาร
LETTER / RFI / MEMO / EMAIL / MOM / INSTRUCTION / NOTICE / OTHER:
(project_id, originator_organization_id, recipient_organization_id,
correspondence_type_id, 0, 0, 0, current_year)
หมายเหตุ: ไม่ใช้ discipline_id, sub_type_id, rfa_type_id
TRANSMITTAL:
(project_id, originator_organization_id, recipient_organization_id,
correspondence_type_id, sub_type_id, 0, 0, current_year)
หมายเหตุ: ใช้ sub_type_id เพิ่มเติม
RFA:
(project_id, originator_organization_id, NULL,
correspondence_type_id, 0, rfa_type_id, discipline_id, current_year)
หมายเหตุ: RFA ไม่ใช้ recipient_organization_id เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER)
วิธีการหา project_id
เนื่องจาก Template ของ LETTER/TRANSMITTAL ไม่มี {PROJECT} token ระบบจะหา project_id จาก:
-
User Context (แนะนำ):
- เมื่อ User สร้างเอกสาร UI จะให้เลือก Project/Contract ก่อน
- ใช้
project_idจาก Context ที่เลือก
-
จาก Organization:
- Query
project_organizationsหรือcontract_organizations - ใช้
originator_organization_idหา project ที่เกี่ยวข้อง - ถ้ามีหลาย project ให้ User เลือก
- Query
-
Validation:
- ตรวจสอบว่า organization มีสิทธิ์ใน project นั้น
- ตรวจสอบว่า project/contract เป็น active
Fallback สำหรับค่า NULL
discipline_id: ใช้0(ไม่ระบุสาขางาน)sub_type_id: ใช้0(ไม่มีประเภทย่อย)rfa_type_id: ใช้0(ไม่ระบุประเภท RFA)recipient_organization_id: ใช้NULLสำหรับ RFA, Required สำหรับ LETTER/TRANSMITTAL
3.11.3. Format Templates by Correspondence Type
📝 หมายเหตุสำคัญ
- Templates ด้านล่างเป็น ตัวอย่าง สำหรับประเภทเอกสารหลัก
- ระบบรองรับ ทุกประเภทเอกสาร ที่อยู่ใน
correspondence_typestable- หากมีการเพิ่มประเภทใหม่ในอนาคต สามารถใช้งานได้โดยอัตโนมัติ
- Admin สามารถกำหนด Template เฉพาะสำหรับแต่ละประเภทผ่าน Admin Panel
3.11.3.1. Letter (TYPE = LETTER)
Template:
{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}
Example: คคง.-สคฉ.3-0001-2568
Token Breakdown:
คคง.= {ORIGINATOR} = รหัสองค์กรผู้ส่งสคฉ.3= {RECIPIENT} = รหัสองค์กรผู้รับหลัก (TO)0001= {SEQ:4} = Running number (เริ่ม 0001, 0002, ...)2568= {YEAR:B.E.} = ปี พ.ศ.
⚠️ Template vs Counter Separation
- {CORR_TYPE} ไม่แสดงใน template เพื่อความกระชับ
- แต่ระบบยังใช้ correspondence_type_id ใน Counter Key เพื่อแยก counter
- LETTER, MEMO, RFI มี counter แยกกัน แม้ template format เหมือนกัน
Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)
3.11.3.2. Transmittal (TYPE = TRANSMITTAL)
Template:
{ORIGINATOR}-{RECIPIENT}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.}
Example: คคง.-สคฉ.3-21-0117-2568
Token Breakdown:
คคง.= {ORIGINATOR}สคฉ.3= {RECIPIENT}21= {SUB_TYPE} = หมายเลขประเภทย่อย (11=MAT, 12=SHP, 13=DWG, 14=MET, ...)0117= {SEQ:4}2568= {YEAR:B.E.}
⚠️ Template vs Counter Separation
- {CORR_TYPE} ไม่แสดงใน template (เหมือน LETTER)
- TRANSMITTAL มี counter แยกจาก LETTER
Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, sub_type_id, 0, 0, year)
3.11.3.3. RFA (Request for Approval)
Template:
{PROJECT}-{CORR_TYPE}-{DISCIPLINE}-{RFA_TYPE}-{SEQ:4}-{REV}
Example: LCBP3-C2-RFA-TER-RPT-0001-A
Token Breakdown:
LCBP3-C2= {PROJECT} = รหัสโครงการRFA= {CORR_TYPE} = ประเภทเอกสาร (แสดงใน RFA template)TER= {DISCIPLINE} = รหัสสาขางาน (TER=Terminal, STR=Structure, ...)RPT= {RFA_TYPE} = ประเภท RFA (RPT=Report, SDW=Shop Drawing, ...)0001= {SEQ:4}A= {REV} = Revision code
📋 RFA Workflow
- RFA เป็น เอกสารโครงการ (Project-level document)
- Workflow: CONTRACTOR → CONSULTANT → OWNER
- ไม่มี specific
recipient_idเพราะเป็น workflow ที่กำหนดไว้แล้ว
Counter Key: (project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id, discipline_id, year)
3.11.3.4. Drawing
Status: 🚧 To Be Determined
Drawing Numbering ยังไม่ได้กำหนด Template เนื่องจาก:
- มีความซับซ้อนสูง (Contract Drawing และ Shop Drawing มีกฎต่างกัน)
- อาจต้องใช้ระบบ Numbering แยกต่างหาก
- ต้องพิจารณาร่วมกับ RFA ที่เกี่ยวข้อง
3.11.3.5. Other Correspondence Types
Applicable to: RFI, MEMO, EMAIL, MOM, INSTRUCTION, NOTICE, OTHER
Template:
{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}
Example (RFI): คคง.-สคฉ.3-0042-2568
Example (MEMO): คคง.-ผรม.1-0001-2568
🔑 Counter Separation
- แม้ template format เหมือนกับ LETTER
- แต่แต่ละ type มี counter แยกกัน ผ่าน
correspondence_type_id- RFI counter ≠ MEMO counter ≠ LETTER counter
Counter Key: (project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)
หมายเหตุ: ทุกประเภทที่ไม่ได้ระบุเฉพาะจะใช้ Template นี้ ถ้ามีการเพิ่ม correspondence type ใหม่ใน correspondence_types table จะใช้ Template นี้โดยอัตโนมัติ
3.11.4. Supported Token Types
| Token | Description | Example | Database Source |
|---|---|---|---|
{PROJECT} |
รหัสโครงการ | LCBP3, LCBP3-C2 |
projects.project_code |
{ORIGINATOR} |
รหัสองค์กรผู้ส่ง | คคง., ผรม.1 |
organizations.organization_code via correspondences.originator_id |
{RECIPIENT} |
รหัสองค์กรผู้รับหลัก (TO) | สคฉ.3, กทท. |
organizations.organization_code via correspondence_recipients where recipient_type = 'TO' |
{CORR_TYPE} |
รหัสประเภทเอกสาร | RFA, TRANSMITTAL, LETTER |
correspondence_types.type_code |
{SUB_TYPE} |
หมายเลขประเภทย่อย | 11, 12, 21 |
correspondence_sub_types.sub_type_number |
{RFA_TYPE} |
รหัสประเภท RFA | SDW, RPT, MAT |
rfa_types.type_code |
{DISCIPLINE} |
รหัสสาขาวิชา | STR, TER, GEO |
disciplines.discipline_code |
{SEQ:n} |
Running number (n = จำนวนหลัก) | 0001, 0029, 0985 |
Based on document_number_counters.last_number + 1 |
{YEAR:B.E.} |
ปี พ.ศ. | 2568 |
document_number_counters.current_year + 543 |
{YEAR:A.D.} |
ปี ค.ศ. | 2025 |
document_number_counters.current_year |
{REV} |
Revision Code | A, B, AA |
correspondence_revisions.revision_label |
Token Usage Notes
{SEQ:n}:
n= จำนวนหลักที่ต้องการ (typically 4)- Counter เริ่มจาก 0001 และเพิ่มทีละ 1 (0001, 0002, 0003, ...)
- Padding ด้วย 0 ทางซ้าย
- Reset ทุกปี (ตาม
current_yearใน Counter Key)
{RECIPIENT}:
- ใช้เฉพาะผู้รับที่มี
recipient_type = 'TO'เท่านั้น - ถ้ามีหลาย TO ให้ใช้คนแรก (ตาม sort order)
- ไม่ใช้สำหรับ RFA (RFA ไม่มี {RECIPIENT} ใน template)
{CORR_TYPE}:
- รองรับทุกค่าจาก
correspondence_types.type_code - ถ้าม<EFBFBD>การเพิ่มประเภทใหม่ จะใช้งานได้ทันที
- แสดงใน template: RFA only
- ไม่แสดงแต่ใช้ใน counter: LETTER, TRANSMITTAL, และ Other types
Deprecated Tokens (ไม่ควรใช้):
→ ใช้{ORG}{ORIGINATOR}หรือ{RECIPIENT}แทน→ ใช้{TYPE}{CORR_TYPE},{SUB_TYPE}, หรือ{RFA_TYPE}แทน (ตามบริบท)→ ไม่ได้ใช้งานในระบบปัจจุบัน{CATEGORY}
3.11.5. Security & Data Integrity Requirements
3.11.5.1. Concurrency Control
Requirements:
- ระบบต้องป้องกัน race condition เมื่อมีการสร้างเลขที่เอกสารพร้อมกัน
- ระบบต้องรับประกัน uniqueness ของเลขที่เอกสารในทุกสถานการณ์
- ระบบควรใช้ Distributed Lock (Redis) เป็นกลไก primary
- ระบบต้องมี fallback mechanism เมื่อ Redis ไม่พร้อมใช้งาน
Implementation Details: ดู Implementation Guide - Section 2.3
3.11.5.2. Data Integrity
Requirements:
- ระบบต้องใช้ Optimistic Locking เพื่อตรวจจับ concurrent updates
- ระบบต้องมี database constraints เพื่อป้องกันข้อมูลผิดพลาด:
- Unique constraint บน
document_number - Foreign key constraints ทุก relationship
- Check constraints สำหรับ business rules
- Unique constraint บน
3.11.5.3. Authorization
Requirements:
- เฉพาะ authenticated users เท่านั้นที่สามารถ request document number
- เฉพาะ Project Admin เท่านั้นที่แก้ไข template ได้
- เฉพาะ Super Admin เท่านั้นที่ reset counter ได้ (requires approval)
3.11.6. Error Handling Requirements
3.11.6.1. Retry Mechanism
Requirements:
ระบบต้องจัดการ error scenarios ต่อไปนี้:
| Scenario | Strategy | Max Retries | Expected Response |
|---|---|---|---|
| Redis Unavailable | Fallback to DB Lock | 0 | Continue (degraded performance) |
| Lock Timeout | Exponential Backoff | 5 | HTTP 503 after final retry |
| Version Conflict | Immediate Retry | 2 | HTTP 409 after final retry |
| DB Connection Error | Exponential Backoff | 3 | HTTP 500 after final retry |
Implementation Details: ดู Implementation Guide - Section 2.5
3.11.6.2. User Experience
Requirements:
- Error messages ต้องเป็นภาษาไทย และเข้าใจง่าย
- HTTP status codes ต้องสื่อความหมายที่ถูกต้อง
- Frontend ควรแสดง retry option สำหรับ transient errors
3.11.7. Configuration Management Requirements
3.11.7.1. Template Management
Requirements:
- Project Admin ต้องสามารถกำหนด/แก้ไข template ผ่าน Admin Panel
- ระบบต้องvalidate template ก่อนบันทึก
- การเปลี่ยนแปลง template ต้องไม่ส่งผลต่อเอกสารที่สร้างไว้แล้ว
3.11.7.2. Template Versioning
Requirements:
- ระบบต้องเก็บ history ของ template changes
- ระบบต้องบันทึก user, timestamp, และเหตุผลในการเปลี่ยนแปลง
- ระบบควรสามารถ rollback ไปเวอร์ชันก่อนหน้าได้
3.11.7.3. Counter Reset Policy
Requirements:
- Counter ต้องreset ตามปี (อัตโนมัติ)
- Admin ต้องสามารถ manual reset counter ได้ (require approval + audit log)
Implementation Details: ดู Implementation Guide - Section 4
3.11.8. Audit Trail Requirements
3.11.8.1. Audit Logging
Requirements:
ระบบต้องบันทึกข้อมูลต่อไปนี้สำหรับทุก document number generation:
document_id- เอกสารที่ถูกสร้างgenerated_number- เลขที่ถูกสร้างcounter_key- key ที่ใช้ในการนับ (JSON format)template_used- template ที่ใช้user_id- ผู้ที่ requestip_address- IP address ของผู้ requesttimestamp- เวลาที่สร้างretry_count- จำนวนครั้งที่ retryperformance_metrics- Lock wait time, total duration
3.11.8.2. Error Logging
Requirements:
- ระบบต้องบันทึก error แยกต่างหาก พร้อม error type classification
- ระบบควรalert ops team สำหรับ critical errors
3.11.8.3. Retention Policy
Requirements:
- Audit log ต้องเก็บอย่างน้อย 7 ปี (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
3.11.9. Performance Requirements
3.11.9.1. Response Time
SLA Targets:
| Metric | Target | Notes |
|---|---|---|
| 95th percentile | ≤ 2 วินาที | ตั้งแต่ request ถึง response |
| 99th percentile | ≤ 5 วินาที | รวม retry attempts |
| Normal operation | ≤ 500ms | ไม่มี retry |
3.11.9.2. Throughput
Capacity Targets:
| Load Level | Target | Notes |
|---|---|---|
| Normal load | ≥ 50 req/s | ใช้งานปกติ |
| Peak load | ≥ 100 req/s | ช่วงเร่งงาน |
3.11.9.3. Availability
SLA Targets:
- Uptime: ≥ 99.5% (excluding planned maintenance)
- Maximum downtime: ≤ 3.6 ชั่วโมง/เดือน
- RTO: ≤ 30 นาที
- RPO: ≤ 5 นาที
Operations Details: ดู Operations Guide - Section 1
3.11.10. Monitoring & Alerting Requirements
3.11.10.1. Metrics
Requirements:
ระบบต้องcollect metrics ต่อไปนี้:
- Lock acquisition time (p50, p95, p99)
- Lock acquisition success/failure rate
- Counter generation latency
- Retry count distribution
- Redis connection status
- Database connection pool usage
3.11.10.2. Alerts
Requirements:
ระบบต้องalert สำหรับ conditions ต่อไปนี้:
| Severity | Condition | Action |
|---|---|---|
| 🔴 Critical | Redis unavailable > 1 minute | PagerDuty + Slack |
| 🔴 Critical | Lock failures > 10% in 5 min | PagerDuty + Slack |
| 🟡 Warning | Lock failures > 5% in 5 min | Slack |
| 🟡 Warning | Avg lock wait time > 1 sec | Slack |
| 🟡 Warning | Retry count > 100/hour | Slack |
3.11.10.3. Dashboard
Requirements:
- Ops team ต้องมี real-time dashboard แสดง:
- Lock acquisition success rate
- Lock wait time percentiles
- Generation rate (per minute)
- Error rate by type
- Connection health status
Operations Details: ดู Operations Guide - Section 3
3.11.11. API Reference
เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้:
Document Number Generation
POST /api/v1/documents/{documentId}/generate-number
สร้างเลขที่เอกสารสำหรับ document ที่ระบุ
Request Body:
{
"counterKey": {
"projectId": 2,
"originatorOrgId": 22,
"recipientOrgId": 10,
"correspondenceTypeId": 6,
"subTypeId": 0,
"rfaTypeId": 0,
"disciplineId": 0,
"year": 2025
}
}
Response:
{
"documentNumber": "คคง.-สคฉ.3-0001-2568",
"generatedAt": "2025-12-02T15:30:00Z"
}
Template Management
GET /api/v1/document-numbering/configs
ดูรายการ template configuration ทั้งหมด
PUT /api/v1/document-numbering/configs/{configId}
แก้ไข template (Project Admin only)
POST /api/v1/document-numbering/configs/{configId}/reset-counter
Reset counter (Super Admin only, requires approval)
รายละเอียดเพิ่มเติม: ดู API Design
3.11.12. Database Schema Reference
เอกสารนี้อ้างอิงถึง database tables ต่อไปนี้:
Core Tables
document_number_counters- เก็บ counter values และ template configurationdocument_number_audit- เก็บ audit trail ของการ generate เลขที่document_number_errors- เก็บ error logs
Related Tables
documents- เก็บ document number ที่ถูกสร้าง (column:document_numberUNIQUE)correspondence_types- ประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.)correspondence_sub_types- ประเภทย่อย (สำหรับ TRANSMITTAL)rfa_types- ประเภท RFA (SHD, RPT, MAT, etc.)disciplines- สาขาวิชา (TER, STR, GEO, etc.)projects- โครงการorganizations- องค์กร
Schema Details: ดู Implementation Guide - Section 1
3.11.13. Database Schema Requirements
3.11.13.1. Counter Table Schema Requirements
Primary Table: document_number_counters
Required Columns:
- Composite primary key:
(project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, current_year) version- สำหรับ optimistic lockinglast_number- counter value (เริ่มจาก 0)
Important Notes:
- ใช้
COALESCE(recipient_organization_id, 0)ใน Primary Key เพื่อรองรับ NULL - Counter reset ทุกปี (เมื่อ
current_yearเปลี่ยน) - ต้องมี seed data สำหรับ
correspondence_types,rfa_types,disciplinesก่อน
Schema Details: ดู Implementation Guide - Section 1.1
3.11.13.2. Audit Table Requirements
Primary Table: document_number_audit
Required Columns:
document_id,generated_number,counter_key(JSON)template_used,user_id,ip_address- Performance metrics:
retry_count,lock_wait_ms,total_duration_ms fallback_used- tracking fallback scenarios
Retention: ≥ 7 ปี
3.11.13.3. Error Log Requirements
Primary Table: document_number_errors
Required Columns:
error_type- ENUM classificationerror_message,stack_trace,context_data(JSON)user_id,ip_address,created_at,resolved_at
3.11.14. Security Considerations
3.11.14.1. Authorization
- เฉพาะ authenticated users เท่านั้นที่สามารถ request document number
- เฉพาะ Project Admin เท่านั้นที่แก้ไข template ได้
- เฉพาะ Super Admin เท่านั้นที่ reset counter ได้
3.11.14.2. Rate Limiting
Requirements:
- Limit ต่อ user: 10 requests/minute (prevent abuse)
- Limit ต่อ IP: 50 requests/minute
Implementation Details: ดู Implementation Guide - Section 5
3.11.14.3. Audit & Compliance
Requirements:
- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering
- เก็บ audit log อย่างน้อย 7 ปี (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)
- Audit log ต้องไม่สามารถแก้ไขได้ (immutable)
References
lock:docnum:{project_id}:{org_id}:{recip_id}:{type_id}:{sub}:{rfa}:{disc}:{year}
Lock Configuration:
- TTL: 5 วินาที (auto-release เมื่อ timeout)
- Acquisition Timeout: 10 วินาที
- Retry Delay: 100ms (exponential backoff)
- Drift Factor: 0.01 (Redlock algorithm)
Implementation (NestJS):
// src/document-numbering/services/document-numbering-lock.service.ts
import Redlock from 'redlock';
import { Injectable } from '@nestjs/common';
@Injectable()
export class DocumentNumberingLockService {
private redlock: Redlock;
async acquireLock(counterKey: CounterKey): Promise<Lock> {
const lockKey = this.buildLockKey(counterKey);
return await this.redlock.acquire([lockKey], 5000); // 5s TTL
}
private buildLockKey(key: CounterKey): string {
return `lock:docnum:${key.projectId}:${key.originatorOrgId}:` +
`${key.recipientOrgId ?? 0}:${key.correspondenceTypeId}:` +
`${key.subTypeId}:${key.rfaTypeId}:${key.disciplineId}:${key.year}`;
}
}
3.11.5.2. Optimistic Locking
ใช้ TypeORM Optimistic Lock ร่วมกับ @Version() decorator:
Entity Definition:
// src/document-numbering/entities/document-number-counter.entity.ts
import { Entity, Column, PrimaryColumn, VersionColumn } from 'typeorm';
@Entity('document_number_counters')
export class DocumentNumberCounter {
@PrimaryColumn({ name: 'project_id' })
projectId: number;
@PrimaryColumn({ name: 'originator_organization_id' })
originatorOrganizationId: number;
@PrimaryColumn({ name: 'recipient_organization_id', nullable: true })
recipientOrganizationId: number | null;
@PrimaryColumn({ name: 'correspondence_type_id' })
correspondenceTypeId: number;
@PrimaryColumn({ name: 'sub_type_id', default: 0 })
subTypeId: number;
@PrimaryColumn({ name: 'rfa_type_id', default: 0 })
rfaTypeId: number;
@PrimaryColumn({ name: 'discipline_id', default: 0 })
disciplineId: number;
@PrimaryColumn({ name: 'current_year' })
currentYear: number;
@VersionColumn({ name: 'version' })
version: number;
@Column({ name: 'last_number', default: 0 })
lastNumber: number;
}
Transaction Handling:
// ใช้ TypeORM Transaction + Optimistic Lock
await this.connection.transaction(async (manager) => {
const counter = await manager.findOne(DocumentNumberCounter, {
where: counterKey
});
counter.lastNumber += 1;
await manager.save(counter); // auto-check version
});
หาก version conflict → TypeORM throw OptimisticLockVersionMismatchError → retry
3.11.5.3. Database Constraints
Unique Constraints:
-- บน documents table
ALTER TABLE documents
ADD CONSTRAINT uq_document_number UNIQUE (document_number);
Foreign Key Constraints:
project_id→projects(id)ON DELETE CASCADEoriginator_organization_id→organizations(id)ON DELETE CASCADErecipient_organization_id→organizations(id)ON DELETE CASCADEcorrespondence_type_id→correspondence_types(id)ON DELETE CASCADE
Check Constraints:
-- ตรวจสอบว่า last_number ≥ 0
ALTER TABLE document_number_counters
ADD CONSTRAINT chk_last_number_positive CHECK (last_number >= 0);
-- ตรวจสอบว่า current_year เป็นปี ค.ศ. ที่สมเหตุสมผล
ALTER TABLE document_number_counters
ADD CONSTRAINT chk_current_year_valid
CHECK (current_year BETWEEN 2020 AND 2100);
3.11.6. Retry Mechanism & Error Handling
3.11.6.1. Scenario 1: Redis Unavailable
Fallback Strategy: Database-only Pessimistic Locking
Implementation:
// src/document-numbering/services/document-numbering.service.ts
@Injectable()
export class DocumentNumberingService {
async generateDocumentNumber(dto: GenerateNumberDto): Promise<string> {
try {
// พยายามใช้ Redis lock ก่อน
return await this.generateWithRedisLock(dto);
} catch (error) {
if (error instanceof RedisConnectionError) {
// Fallback: ใช้ database lock
this.logger.warn('Redis unavailable, falling back to DB lock');
await this.alertOpsTeam('redis_unavailable');
return await this.generateWithDbLock(dto);
}
throw error;
}
}
private async generateWithDbLock(dto: GenerateNumberDto): Promise<string> {
return await this.connection.transaction(async (manager) => {
// SELECT ... FOR UPDATE = Pessimistic Lock
const counter = await manager
.createQueryBuilder(DocumentNumberCounter, 'c')
.where(counterKeyCondition)
.setLock('pessimistic_write')
.getOne();
counter.lastNumber += 1;
await manager.save(counter);
return this.formatNumber(counter);
});
}
}
Monitoring:
- Log warning พร้อม context (project_id, user_id, timestamp)
- Alert Ops Team ผ่าน Slack/Email
- ระบบยังใช้งานได้แต่ performance อาจลดลง 30-50%
3.11.6.2. Scenario 2: Lock Acquisition Timeout
Retry Strategy: Exponential Backoff with Jitter
// ใช้ @nestjs/common Retry Decorator หรือ custom retry logic
import { retry } from 'rxjs/operators';
const RETRY_CONFIG = {
maxRetries: 5,
delays: [1000, 2000, 4000, 8000, 16000], // exponential backoff
jitter: 0.1 // เพิ่ม randomness ป้องกัน thundering herd
};
async acquireLockWithRetry(key: CounterKey): Promise<Lock> {
for (let i = 0; i < RETRY_CONFIG.maxRetries; i++) {
try {
return await this.lockService.acquireLock(key);
} catch (error) {
if (i === RETRY_CONFIG.maxRetries - 1) {
throw new ServiceUnavailableException(
'ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง'
);
}
const delay = RETRY_CONFIG.delays[i];
const jitter = delay * RETRY_CONFIG.jitter * Math.random();
await this.sleep(delay + jitter);
}
}
}
Response:
-
HTTP Status:
503 Service Temporarily Unavailable -
Response Body:
{ "statusCode": 503, "message": "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง", "error": "Service Unavailable", "retryAfter": 30 }
3.11.6.3. Scenario 3: Version Conflict (Optimistic Lock)
Retry Strategy: Immediate Retry (2 attempts)
async incrementCounter(counterKey: CounterKey): Promise<number> {
const MAX_RETRIES = 2;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return await this.connection.transaction(async (manager) => {
const counter = await manager.findOne(
DocumentNumberCounter,
{ where: counterKey }
);
counter.lastNumber += 1;
await manager.save(counter); // Version check ที่นี่
return counter.lastNumber;
});
} catch (error) {
if (error instanceof OptimisticLockVersionMismatchError) {
this.logger.warn(`Version conflict, retry ${attempt + 1}/${MAX_RETRIES}`);
if (attempt === MAX_RETRIES - 1) {
throw new ConflictException('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่');
}
// Retry ทันที (ไม่มี delay)
continue;
}
throw error;
}
}
}
Response:
- HTTP Status:
409 Conflict - Frontend Action: Auto-retry หรือแสดง toast notification
3.11.6.4. Scenario 4: Database Connection Error
Retry Strategy: Exponential Backoff (3 attempts)
const DB_RETRY_CONFIG = {
maxRetries: 3,
delays: [1000, 2000, 4000]
};
// TypeORM connection retry (กำหนดใน ormconfig)
{
type: 'mysql',
extra: {
connectionLimit: 10,
acquireTimeout: 10000,
// Retry connection 3 ครั้ง
retryAttempts: 3,
retryDelay: 1000
}
}
Response:
-
HTTP Status:
500 Internal Server Error -
Response Body:
{ "statusCode": 500, "message": "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ", "error": "Internal Server Error", "ref": "ERR-20250102-1234-ABCD" } -
Alerting: ส่ง PagerDuty/Slack alert ทันที (severity: CRITICAL)
3.11.7. Configuration Management
3.11.7.1. Admin Panel Configuration
Features:
- Project Admin สามารถกำหนด/แก้ไข template ผ่าน Web UI
- Preview document number ก่อนบันทึก
- Template validation แบบ real-time
Template Validation Logic:
// src/document-numbering/validators/template.validator.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class TemplateValidator {
private readonly ALLOWED_TOKENS = [
'PROJECT', 'ORIGINATOR', 'RECIPIENT', 'CORR_TYPE',
'SUB_TYPE', 'RFA_TYPE', 'DISCIPLINE', 'SEQ', 'YEAR', 'REV'
];
validate(template: string, correspondenceType: string): ValidationResult {
const tokens = this.extractTokens(template);
const errors: string[] = [];
// ตรวจสอบ Token ที่ไม่รู้จัก
for (const token of tokens) {
if (!this.ALLOWED_TOKENS.includes(token.name)) {
errors.push(`Unknown token: {${token.name}}`);
}
}
// กฎพิเศษสำหรับแต่ละประเภท
if (correspondenceType === 'RFA') {
if (!tokens.some(t => t.name === 'PROJECT')) {
errors.push('RFA template ต้องมี {PROJECT}');
}
}
if (correspondenceType === 'TRANSMITTAL') {
if (!tokens.some(t => t.name === 'SUB_TYPE')) {
errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}');
}
}
return { valid: errors.length === 0, errors };
}
}
API Endpoint:
// PUT /api/v1/document-numbering/configs/:configId
@Put('configs/:configId')
@Roles('PROJECT_ADMIN')
async updateTemplate(
@Param('configId') configId: number,
@Body() dto: UpdateTemplateDto
): Promise<DocumentNumberConfig> {
// Validate template
const validation = await this.templateValidator.validate(
dto.template,
dto.correspondenceType
);
if (!validation.valid) {
throw new BadRequestException(validation.errors);
}
// บันทึก template (ไม่ส่งผลต่อเอกสารที่สร้างแล้ว)
return await this.configService.update(configId, dto);
}
3.11.7.2. Template Versioning
Database Table: document_number_config_history
CREATE TABLE document_number_config_history (
id INT AUTO_INCREMENT PRIMARY KEY,
config_id INT NOT NULL,
template_before TEXT,
template_after TEXT NOT NULL,
changed_by INT NOT NULL,
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
change_reason TEXT,
FOREIGN KEY (config_id) REFERENCES document_number_configs(id),
FOREIGN KEY (changed_by) REFERENCES users(id)
) ENGINE=InnoDB COMMENT='Template Change History';
Audit Trail Implementation:
@Injectable()
export class ConfigHistoryService {
async recordChange(
configId: number,
oldTemplate: string,
newTemplate: string,
userId: number,
reason: string
): Promise<void> {
await this.historyRepo.save({
configId,
templateBefore: oldTemplate,
templateAfter: newTemplate,
changedBy: userId,
changeReason: reason
});
}
async rollback(configId: number, historyId: number): Promise<void> {
const history = await this.historyRepo.findOne({ where: { id: historyId }});
await this.configService.update(configId, {
template: history.templateBefore
});
}
}
3.11.7.3. Counter Reset Policy
Automatic Reset:
-
Yearly Reset: ทุกวันที่ 1 มกราคม (00:00:00 ICT)
- ใช้ BullMQ Cron Job:
// src/document-numbering/jobs/counter-reset.job.ts @Processor('document-numbering') export class CounterResetJob { @Cron('0 0 1 1 *') // 1 Jan every year async handleYearlyReset() { const newYear = new Date().getFullYear(); // ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว // แค่เตรียม counter สำหรับปีใหม่ this.logger.log(`Year changed to ${newYear}, counters ready`); } }
Manual Reset (Admin only):
// POST /api/v1/document-numbering/configs/:configId/reset-counter
@Post('configs/:configId/reset-counter')
@Roles('SUPER_ADMIN')
@RequireApproval() // Custom decorator: ต้อง approve จาก 2 admins
async resetCounter(
@Param('configId') configId: number,
@Body() dto: ResetCounterDto
): Promise<void> {
// Validate reason
if (!dto.reason || dto.reason.length < 20) {
throw new BadRequestException('ต้องระบุเหตุผลอย่างน้อย 20 ตัวอักษร');
}
// Audit log
await this.auditService.logCounterReset({
configId,
userId: req.user.id,
reason: dto.reason,
previousValue: counter.lastNumber
});
// Reset
await this.counterService.reset(configId);
}
## 3.11.8. Audit Trail
### 3.11.8.1. การบันทึก Audit Log
**Database Table**: `document_number_audit`
```sql
CREATE TABLE document_number_audit (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
document_id INT NOT NULL,
generated_number VARCHAR(100) NOT NULL,
counter_key JSON NOT NULL COMMENT 'Counter key used (JSON format)',
template_used VARCHAR(200) NOT NULL,
user_id INT NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Performance & Error Tracking
retry_count INT DEFAULT 0,
lock_wait_ms INT COMMENT 'Lock acquisition time in milliseconds',
total_duration_ms INT COMMENT 'Total generation time',
fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE',
INDEX idx_document_id (document_id),
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at),
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB COMMENT='Document Number Generation Audit Trail';
Audit Service Implementation:
// src/document-numbering/services/audit.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
@Injectable()
export class DocumentNumberAuditService {
async logGeneration(data: AuditLogData): Promise<void> {
await this.auditRepo.save({
documentId: data.documentId,
generatedNumber: data.number,
counterKey: JSON.stringify(data.counterKey),
templateUsed: data.template,
userId: data.userId,
ipAddress: data.ipAddress,
userAgent: data.userAgent,
retryCount: data.retryCount ?? 0,
lockWaitMs: data.lockWaitMs,
totalDurationMs: data.totalDurationMs,
fallbackUsed: data.fallbackUsed ?? 'NONE'
});
}
}
Usage in Service:
@Injectable()
export class DocumentNumberingService {
async generateDocumentNumber(dto: GenerateNumberDto, req: Request) {
const startTime = Date.now();
let lockWaitMs = 0;
let retryCount = 0;
let fallbackUsed = 'NONE';
try {
// ... generate logic ...
const number = await this.doGenerate(dto);
// Audit log
await this.auditService.logGeneration({
documentId: dto.documentId,
number,
counterKey: dto.counterKey,
template: config.template,
userId: req.user.id,
ipAddress: req.ip,
userAgent: req.headers['user-agent'],
retryCount,
lockWaitMs,
totalDurationMs: Date.now() - startTime,
fallbackUsed
});
return number;
} catch (error) {
// Log error separately
await this.errorLogService.log(error, dto);
throw error;
}
}
}
3.11.8.2. Conflict & Error Logging
Separate Error Log Table: document_number_errors
CREATE TABLE document_number_errors (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
error_type ENUM(
'LOCK_TIMEOUT',
'VERSION_CONFLICT',
'DB_ERROR',
'REDIS_ERROR',
'VALIDATION_ERROR'
) NOT NULL,
error_message TEXT,
stack_trace TEXT,
context_data JSON COMMENT 'Request context (user, project, etc.)',
user_id INT,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
resolved_at TIMESTAMP NULL,
INDEX idx_error_type (error_type),
INDEX idx_created_at (created_at),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB COMMENT='Document Numbering Error Log';
Error Logging Service:
@Injectable()
export class ErrorLogService {
async log(error: Error, context: any): Promise<void> {
const errorType = this.classifyError(error);
await this.errorRepo.save({
errorType,
errorMessage: error.message,
stackTrace: error.stack,
contextData: JSON.stringify(context),
userId: context.userId,
ipAddress: context.ipAddress
});
// Alert if critical
if (this.isCritical(errorType)) {
await this.alertService.sendAlert({
severity: 'CRITICAL',
title: `Document Numbering Error: ${errorType}`,
details: error.message
});
}
}
private classifyError(error: Error): string {
if (error instanceof LockTimeoutError) return 'LOCK_TIMEOUT';
if (error instanceof OptimisticLockVersionMismatchError) return 'VERSION_CONFLICT';
if (error instanceof QueryFailedError) return 'DB_ERROR';
if (error instanceof RedisConnectionError) return 'REDIS_ERROR';
return 'UNKNOWN';
}
}
## 3.11.9. Performance Requirements
### 3.11.9.1. Response Time
**Target Response Times**:
- **95th percentile**: ≤ 2 วินาที
- **99th percentile**: ≤ 5 วินาที
- **Normal operation** (ไม่มี retry): ≤ 500ms
**Performance Optimization Strategies**:
```typescript
// 1. Database Connection Pooling
{
type: 'mysql',
extra: {
connectionLimit: 20, // Pool size
queueLimit: 0, // Unlimited queue
acquireTimeout: 10000 // 10s timeout
}
}
// 2. Redis Connection Pooling
import IORedis from 'ioredis';
const redis = new IORedis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
maxRetriesPerRequest: 3,
enableReadyCheck: true,
lazyConnect: false,
// Connection pool
poolSize: 10
});
// 3. Query Optimization
// ใช้ Index-covered queries
const counter = await this.counterRepo
.createQueryBuilder('c')
.where('c.project_id = :projectId', { projectId })
.andWhere('c.correspondence_type_id = :typeId', { typeId })
.andWhere('c.current_year = :year', { year })
.useIndex('idx_counter_lookup') // Force index usage
.getOne();
Performance Monitoring:
// Prometheus metrics
import { Counter, Histogram } from 'prom-client';
const generationDuration = new Histogram({
name: 'docnum_generation_duration_seconds',
help: 'Document number generation duration',
labelNames: ['project', 'type', 'status'],
buckets: [0.1, 0.5, 1, 2, 5, 10]
});
// Usage
const timer = generationDuration.startTimer();
try {
const number = await this.generate(dto);
timer({ status: 'success' });
} catch (error) {
timer({ status: 'error' });
throw error;
}
3.11.9.2. Throughput
Capacity Requirements:
- Normal load: ≥ 50 requests/second
- Peak load: ≥ 100 requests/second (ช่วงเร่งงาน)
- Burst capacity: ≥ 200 requests/second (short duration)
Load Balancing Strategy:
# docker-compose.yml
services:
backend:
image: lcbp3-backend:latest
deploy:
replicas: 3 # 3 instances
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- "80:80"
# nginx.conf - Load Balancing Configuration
upstream backend {
least_conn; # Least connections algorithm
server backend:3000 max_fails=3 fail_timeout=30s;
server backend:3001 max_fails=3 fail_timeout=30s;
server backend:3002 max_fails=3 fail_timeout=30s;
}
server {
location /api/v1/documents/ {
proxy_pass http://backend;
proxy_next_upstream error timeout;
proxy_connect_timeout 10s;
proxy_read_timeout 30s;
}
}
Rate Limiting:
// ใช้ @nestjs/throttler
import { ThrottlerGuard } from '@nestjs/throttler';
@Controller('document-numbering')
@UseGuards(ThrottlerGuard)
export class DocumentNumberingController {
@Throttle(10, 60) // 10 requests per 60 seconds per user
@Post('generate')
async generate(@Body() dto: GenerateNumberDto) {
return await this.service.generate(dto);
}
}
3.11.9.3. Availability
SLA Targets:
- Uptime: ≥ 99.5% (excluding planned maintenance)
- Maximum downtime: ≤ 3.6 ชั่วโมง/เดือน
- Recovery Time Objective (RTO): ≤ 30 นาที
- Recovery Point Objective (RPO): ≤ 5 นาที
High Availability Setup:
# High Availability Architecture
services:
# MariaDB - Master/Replica
mariadb-master:
image: mariadb:11.8
environment:
MYSQL_REPLICATION_MODE: master
mariadb-replica:
image: mariadb:11.8
environment:
MYSQL_REPLICATION_MODE: slave
MYSQL_MASTER_HOST: mariadb-master
# Redis - Sentinel Mode
redis-master:
image: redis:7-alpine
command: redis-server --appendonly yes
redis-replica:
image: redis:7-alpine
command: redis-server --replicaof redis-master 6379
redis-sentinel:
image: redis:7-alpine
command: >
redis-sentinel /etc/redis/sentinel.conf
--sentinel monitor mymaster redis-master 6379 2
Health Checks:
// src/health/health.controller.ts
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.redis.pingCheck('redis'),
() => this.customHealthCheck()
]);
}
private async customHealthCheck() {
// ทดสอบ generate document number
const canGenerate = await this.testGeneration();
return { documentNumbering: { status: canGenerate ? 'up' : 'down' }};
}
}
## 3.11.10. Monitoring & Alerting
### 3.11.10.1. Metrics Collection
**Prometheus Metrics Implementation**:
```typescript
// src/document-numbering/metrics/metrics.service.ts
import { Injectable } from '@nestjs/common';
import { Counter, Histogram, Gauge } from 'prom-client';
@Injectable()
export class DocumentNumberingMetrics {
// Lock acquisition metrics
private lockAcquisitionDuration = new Histogram({
name: 'docnum_lock_acquisition_duration_ms',
help: 'Lock acquisition time in milliseconds',
labelNames: ['project', 'type'],
buckets: [10, 50, 100, 200, 500, 1000, 2000, 5000]
});
private lockAcquisitionFailures = new Counter({
name: 'docnum_lock_acquisition_failures_total',
help: 'Total number of lock acquisition failures',
labelNames: ['project', 'type', 'reason']
});
// Generation metrics
private generationDuration = new Histogram({
name: 'docnum_generation_duration_ms',
help: 'Total document number generation time',
labelNames: ['project', 'type', 'status'],
buckets: [100, 200, 500, 1000, 2000, 5000]
});
private retryCount = new Histogram({
name: 'docnum_retry_count',
help: 'Number of retries per generation',
labelNames: ['project', 'type'],
buckets: [0, 1, 2, 3, 5, 10]
});
// Connection health
private redisConnectionStatus = new Gauge({
name: 'docnum_redis_connection_status',
help: 'Redis connection status (1=up, 0=down)'
});
private dbConnectionPoolUsage = new Gauge({
name: 'docnum_db_connection_pool_usage',
help: 'Database connection pool usage percentage'
});
}
3.11.10.2. Alert Rules
Prometheus Alert Rules (prometheus/alerts.yml):
groups:
- name: document_numbering_alerts
interval: 30s
rules:
# CRITICAL: Redis unavailable
- alert: RedisUnavailable
expr: docnum_redis_connection_status == 0
for: 1m
labels:
severity: critical
component: document-numbering
annotations:
summary: "Redis is unavailable for document numbering"
description: "System is falling back to DB-only locking"
# CRITICAL: High lock failure rate
- alert: HighLockFailureRate
expr: |
rate(docnum_lock_acquisition_failures_total[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "Lock acquisition failure rate > 10%"
description: "Check Redis and database performance"
# WARNING: Elevated lock failure rate
- alert: ElevatedLockFailureRate
expr: |
rate(docnum_lock_acquisition_failures_total[5m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "Lock acquisition failure rate > 5%"
# WARNING: Slow lock acquisition
- alert: SlowLockAcquisition
expr: |
histogram_quantile(0.95,
rate(docnum_lock_acquisition_duration_ms_bucket[5m])
) > 1000
for: 5m
labels:
severity: warning
annotations:
summary: "P95 lock acquisition time > 1 second"
# WARNING: High retry count
- alert: HighRetryCount
expr: |
sum by (project) (
rate(docnum_retry_count_sum[1h])
) > 100
for: 1h
labels:
severity: warning
annotations:
summary: "Retry count > 100 per hour in project {{ $labels.project }}"
# WARNING: Slow generation
- alert: SlowDocumentNumberGeneration
expr: |
histogram_quantile(0.95,
rate(docnum_generation_duration_ms_bucket[5m])
) > 2000
for: 5m
labels:
severity: warning
annotations:
summary: "P95 generation time > 2 seconds"
AlertManager Configuration (alertmanager/config.yml):
route:
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'ops-team'
routes:
# CRITICAL alerts → PagerDuty + Slack
- match:
severity: critical
receiver: 'pagerduty-critical'
continue: true
# WARNING alerts → Slack only
- match:
severity: warning
receiver: 'slack-warnings'
receivers:
- name: 'pagerduty-critical'
pagerduty_configs:
- service_key: <SERVICE_KEY>
description: '{{ .CommonAnnotations.summary }}'
- name: 'slack-warnings'
slack_configs:
- api_url: <SLACK_WEBHOOK>
channel: '#lcbp3-alerts'
title: '⚠️ {{ .GroupLabels.alertname }}'
text: '{{ .CommonAnnotations.description }}'
- name: 'ops-team'
email_configs:
- to: 'ops@example.com'
3.11.10.3. Grafana Dashboard
Dashboard Configuration (grafana/dashboards/document-numbering.json):
{
"title": "Document Numbering Performance",
"panels": [
{
"title": "Lock Acquisition Success Rate",
"targets": [{
"expr": "1 - (rate(docnum_lock_acquisition_failures_total[5m]) / rate(docnum_lock_acquisition_total[5m]))"
}],
"type": "graph",
"gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 }
},
{
"title": "Lock Acquisition Time (Percentiles)",
"targets": [
{
"expr": "histogram_quantile(0.50, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))",
"legendFormat": "P50"
},
{
"expr": "histogram_quantile(0.95, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))",
"legendFormat": "P95"
},
{
"expr": "histogram_quantile(0.99, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))",
"legendFormat": "P99"
}
],
"type": "graph",
"gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 }
},
{
"title": "Generation Rate (per minute)",
"targets": [{
"expr": "sum(rate(docnum_generation_duration_ms_count[1m])) * 60"
}],
"type": "stat",
"gridPos": { "x": 0, "y": 8, "w": 6, "h": 4 }
},
{
"title": "Redis Connection Status",
"targets": [{
"expr": "docnum_redis_connection_status"
}],
"type": "stat",
"gridPos": { "x": 6, "y": 8, "w": 6, "h": 4 },
"thresholds": {
"mode": "absolute",
"steps": [
{ "value": 0, "color": "red" },
{ "value": 1, "color": "green" }
]
}
},
{
"title": "Error Rate by Type",
"targets": [{
"expr": "sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))"
}],
"type": "graph",
"gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 }
}
]
}
Key Dashboard Panels:
- Lock Acquisition Success Rate: Real-time success %
- Lock Wait Time Percentiles: P50, P95, P99 latency
- Generation Rate: Documents/minute
- Error Breakdown: By error type (LOCK_TIMEOUT, VERSION_CONFLICT, etc.)
- Redis/DB Health: Connection status
- Retry Distribution: Histogram of retry counts
3.11.12. API Reference
เอกสารนี้อ้างอิงถึง API endpoints ต่อไปนี้ (รายละเอียดใน specs/02-architecture/api-design.md):
POST /api/v1/documents/{documentId}/generate-number- สร้างเลขที่เอกสารGET /api/v1/document-numbering/configs- ดูการตั้งค่า templatePUT /api/v1/document-numbering/configs/{configId}- แก้ไข template (Admin only)POST /api/v1/document-numbering/configs/{configId}/reset-counter- Reset counter (Admin only)
3.11.13. Database Schema Reference
เอกสารนี้อ้างอิงถึง tables ต่อไปนี้ (รายละเอียดใน specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md):
document_number_configs- เก็บ template และ counter configurationdocument_number_counters- เก็บ current counter valuedocument_number_audit- เก็บ audit traildocuments- เก็บ document number ที่ถูกสร้าง
3.11.14. Database Schema Requirements
3.11.14.1. Counter Table Schema
ตาราง document_number_counters ต้องมีโครงสร้างดังนี้:
CREATE TABLE document_number_counters (
project_id INT NOT NULL,
originator_organization_id INT NOT NULL,
recipient_organization_id INT NULL, -- NULL for RFA
correspondence_type_id INT NOT NULL,
sub_type_id INT DEFAULT 0, -- for TRANSMITTAL
rfa_type_id INT DEFAULT 0, -- for RFA
discipline_id INT DEFAULT 0, -- for RFA
current_year INT NOT NULL,
version INT DEFAULT 0 NOT NULL, -- Optimistic Lock
last_number INT DEFAULT 0,
PRIMARY KEY (
project_id,
originator_organization_id,
COALESCE(recipient_organization_id, 0),
correspondence_type_id,
sub_type_id,
rfa_type_id,
discipline_id,
current_year
),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (originator_organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci
COMMENT = 'ตารางเก็บ Running Number Counters';
3.11.14.2. Index Requirements
-- Index สำหรับ Performance
CREATE INDEX idx_counter_lookup
ON document_number_counters (
project_id,
correspondence_type_id,
current_year
);
-- Index สำหรับ Originator lookup
CREATE INDEX idx_counter_org
ON document_number_counters (
originator_organization_id,
current_year
);
3.11.14.3. Important Notes
💡 Counter Key Design
- ใช้
COALESCE(recipient_organization_id, 0)ใน Primary Key เพื่อรองรับ NULLversioncolumn สำหรับ Optimistic Locking (ป้องกัน race condition)last_numberเริ่มจาก 0 และเพิ่มขึ้นทีละ 1- Counter reset ทุกปี (เมื่อ
current_yearเปลี่ยน)
⚠️ Migration Notes
- ไม่มีข้อมูลเก่า ไม่ต้องทำ backward compatibility
- สามารถสร้าง table ใหม่ได้เลยตาม schema ข้างต้น
- ต้องมี seed data สำหรับ
correspondence_types,rfa_types,disciplinesก่อน
3.11.14.4. Example Counter Records
-- Example: LETTER from คคง. to สคฉ.3 in LCBP3-C2 year 2025
INSERT INTO document_number_counters (
project_id, originator_organization_id, recipient_organization_id,
correspondence_type_id, sub_type_id, rfa_type_id, discipline_id,
current_year, version, last_number
) VALUES (
2, -- LCBP3-C2
22, -- คคง.
10, -- สคฉ.3
6, -- LETTER
0, 0, 0,
2025, 0, 0
);
-- Example: RFA from ผรม.2 in LCBP3-C2, discipline TER, type RPT, year 2025
INSERT INTO document_number_counters (
project_id, originator_organization_id, recipient_organization_id,
correspondence_type_id, sub_type_id, rfa_type_id, discipline_id,
current_year, version, last_number
) VALUES (
2, -- LCBP3-C2
42, -- ผรม.2
NULL, -- RFA ไม่มี specific recipient
1, -- RFA
0,
18, -- RPT (Report)
5, -- TER (Terminal)
2025, 0, 0
);
3.11.15. Security Considerations
3.11.14.1. Authorization
- เฉพาะ authenticated users เท่านั้นที่สามารถ request document number
- เฉพาะ Project Admin เท่านั้นที่แก้ไข template ได้
- เฉพาะ Super Admin เท่านั้นที่ reset counter ได้
3.11.14.2. Rate Limiting
- Limit ต่อ user: 10 requests/minute (prevent abuse)
- Limit ต่อ IP: 50 requests/minute
3.11.14.3. Audit & Compliance
- บันทึกทุก API call ที่เกี่ยวข้องกับ document numbering
- เก็บ audit log อย่างน้อย 7 ปี (ตาม พ.ร.บ. ข้อมูลอิเล็กทรอนิกส์)