251202:2300 Prepare 1.5.1
This commit is contained in:
1582
docs/8_lcbp3_v1_5_1.sql
Normal file
1582
docs/8_lcbp3_v1_5_1.sql
Normal file
File diff suppressed because it is too large
Load Diff
1748
docs/8_lcbp3_v1_5_1_seed.sql
Normal file
1748
docs/8_lcbp3_v1_5_1_seed.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -664,7 +664,9 @@
|
|||||||
"password": "",
|
"password": "",
|
||||||
"database": "lcbp3"
|
"database": "lcbp3"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"terminal.integrated.copyOnSelection": true,
|
||||||
|
"terminal.integrated.tabs.defaultColor": "terminal.ansiBlue"
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -927,8 +929,7 @@
|
|||||||
"chakrounanas.turbo-console-log",
|
"chakrounanas.turbo-console-log",
|
||||||
"wallabyjs.console-ninja",
|
"wallabyjs.console-ninja",
|
||||||
"pkief.material-icon-theme",
|
"pkief.material-icon-theme",
|
||||||
"github.copilot",
|
"github.copilot"
|
||||||
"inferrinizzard.prettier-sql-vscode"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# LCBP3-DMS - Project Overview
|
# LCBP3-DMS - Project Overview
|
||||||
|
|
||||||
**Project Name:** Laem Chabang Port Phase 3 - Document Management System (LCBP3-DMS)
|
**Project Name:** Laem Chabang Port Phase 3 - Document Management System (LCBP3-DMS)
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Status:** Planning & Specification Phase
|
**Status:** Planning & Specification Phase
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -200,18 +200,21 @@ lcbp3/
|
|||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
| Category | Document | Description |
|
| Category | Document | Description |
|
||||||
| ------------------ | --------------------------------------------------------------------------- | ------------------------------------- |
|
| ------------------ | ------------------------------------------------------------------------------------ | ------------------------------------- |
|
||||||
| **Overview** | [Glossary](./glossary.md) | Technical terminology & abbreviations |
|
| **Overview** | [Glossary](./glossary.md) | Technical terminology & abbreviations |
|
||||||
| **Overview** | [Quick Start](./quick-start.md) | 5-minute getting started guide |
|
| **Overview** | [Quick Start](./quick-start.md) | 5-minute getting started guide |
|
||||||
| **Requirements** | [Functional Requirements](../01-requirements/03-functional-requirements.md) | Feature specifications |
|
| **Requirements** | [Functional Requirements](../01-requirements/03-functional-requirements.md) | Feature specifications |
|
||||||
|
| **Requirements** | [Document Numbering](../01-requirements/03.11-document-numbering.md) | Document numbering requirements |
|
||||||
| **Architecture** | [System Architecture](../02-architecture/system-architecture.md) | Overall system design |
|
| **Architecture** | [System Architecture](../02-architecture/system-architecture.md) | Overall system design |
|
||||||
| **Architecture** | [Data Model](../02-architecture/data-model.md) | Database schema |
|
| **Architecture** | [Data Model](../02-architecture/data-model.md) | Database schema |
|
||||||
| **Architecture** | [API Design](../02-architecture/api-design.md) | REST API specifications |
|
| **Architecture** | [API Design](../02-architecture/api-design.md) | REST API specifications |
|
||||||
| **Implementation** | [Backend Guidelines](../03-implementation/backend-guidelines.md) | Backend coding standards |
|
| **Implementation** | [Backend Guidelines](../03-implementation/backend-guidelines.md) | Backend coding standards |
|
||||||
| **Implementation** | [Frontend Guidelines](../03-implementation/frontend-guidelines.md) | Frontend coding standards |
|
| **Implementation** | [Frontend Guidelines](../03-implementation/frontend-guidelines.md) | Frontend coding standards |
|
||||||
|
| **Implementation** | [Document Numbering Implementation](../03-implementation/document-numbering.md) | Document numbering implementation |
|
||||||
| **Implementation** | [Testing Strategy](../03-implementation/testing-strategy.md) | Testing approach |
|
| **Implementation** | [Testing Strategy](../03-implementation/testing-strategy.md) | Testing approach |
|
||||||
| **Operations** | [Deployment Guide](../04-operations/deployment-guide.md) | How to deploy |
|
| **Operations** | [Deployment Guide](../04-operations/deployment-guide.md) | How to deploy |
|
||||||
| **Operations** | [Monitoring](../04-operations/monitoring-alerting.md) | Monitoring & alerts |
|
| **Operations** | [Monitoring](../04-operations/monitoring-alerting.md) | Monitoring & alerts |
|
||||||
|
| **Operations** | [Document Numbering Operations](../04-operations/document-numbering-operations.md) | Doc numbering ops guide |
|
||||||
| **Decisions** | [ADR Index](../05-decisions/README.md) | Architecture decisions |
|
| **Decisions** | [ADR Index](../05-decisions/README.md) | Architecture decisions |
|
||||||
| **Tasks** | [Backend Tasks](../06-tasks/README.md) | Development tasks |
|
| **Tasks** | [Backend Tasks](../06-tasks/README.md) | Development tasks |
|
||||||
|
|
||||||
@@ -371,7 +374,7 @@ lcbp3/
|
|||||||
|
|
||||||
### Operations Support
|
### Operations Support
|
||||||
|
|
||||||
- **Email:** ops-team@example.com
|
- **Email:** <ops-team@example.com>
|
||||||
- **Phone:** [Phone Number]
|
- **Phone:** [Phone Number]
|
||||||
- **On-Call:** [On-Call Schedule]
|
- **On-Call:** [On-Call Schedule]
|
||||||
|
|
||||||
@@ -379,9 +382,9 @@ lcbp3/
|
|||||||
|
|
||||||
## 📝 Document Control
|
## 📝 Document Control
|
||||||
|
|
||||||
- **Version:** 1.5.0
|
- **Version:** 1.5.1
|
||||||
- **Status:** Active
|
- **Status:** Active
|
||||||
- **Last Updated:** 2025-12-01
|
- **Last Updated:** 2025-12-02
|
||||||
- **Next Review:** 2026-01-01
|
- **Next Review:** 2026-01-01
|
||||||
- **Owner:** System Architect
|
- **Owner:** System Architect
|
||||||
- **Classification:** Internal Use Only
|
- **Classification:** Internal Use Only
|
||||||
@@ -391,7 +394,8 @@ lcbp3/
|
|||||||
## 🔄 Version History
|
## 🔄 Version History
|
||||||
|
|
||||||
| Version | Date | Description |
|
| Version | Date | Description |
|
||||||
| ------- | ---------- | ------------------------------------------ |
|
| ------- | ---------- | ----------------------------------------- |
|
||||||
|
| 1.6.0 | 2025-12-02 | Reorganized documentation structure |
|
||||||
| 1.5.0 | 2025-12-01 | Complete specification with ADRs and tasks |
|
| 1.5.0 | 2025-12-01 | Complete specification with ADRs and tasks |
|
||||||
| 1.4.5 | 2025-11-30 | Updated architecture documents |
|
| 1.4.5 | 2025-11-30 | Updated architecture documents |
|
||||||
| 1.4.4 | 2025-11-29 | Initial backend/frontend plans |
|
| 1.4.4 | 2025-11-29 | Initial backend/frontend plans |
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Glossary - คำศัพท์และคำย่อทางเทคนิค
|
# Glossary - คำศัพท์และคำย่อทางเทคนิค
|
||||||
|
|
||||||
**Project:** LCBP3-DMS
|
**Project:** LCBP3-DMS
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -491,6 +491,6 @@ Logging library สำหรับ Node.js
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
**Next Review:** 2026-03-01
|
**Next Review:** 2026-03-01
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Quick Start Guide
|
# Quick Start Guide
|
||||||
|
|
||||||
**Project:** LCBP3-DMS
|
**Project:** LCBP3-DMS
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -355,7 +355,7 @@ git push origin feature/my-feature
|
|||||||
### Resources
|
### Resources
|
||||||
|
|
||||||
- **Documentation:** `/specs` directory
|
- **Documentation:** `/specs` directory
|
||||||
- **API Docs:** http://localhost:3000/api/docs
|
- **API Docs:** <http://localhost:3000/api/docs>
|
||||||
- **Issue Tracker:** [Link to issue tracker]
|
- **Issue Tracker:** [Link to issue tracker]
|
||||||
|
|
||||||
### Contact
|
### Contact
|
||||||
@@ -373,8 +373,8 @@ git push origin feature/my-feature
|
|||||||
- [ ] Setup environment variables
|
- [ ] Setup environment variables
|
||||||
- [ ] Start Docker services
|
- [ ] Start Docker services
|
||||||
- [ ] Run migrations
|
- [ ] Run migrations
|
||||||
- [ ] Access backend (http://localhost:3000/health)
|
- [ ] Access backend (<http://localhost:3000/health>)
|
||||||
- [ ] Access frontend (http://localhost:3001)
|
- [ ] Access frontend (<http://localhost:3001>)
|
||||||
- [ ] Login with default credentials
|
- [ ] Login with default credentials
|
||||||
- [ ] Run tests
|
- [ ] Run tests
|
||||||
- [ ] Read [System Architecture](../02-architecture/system-architecture.md)
|
- [ ] Read [System Architecture](../02-architecture/system-architecture.md)
|
||||||
@@ -385,5 +385,5 @@ git push origin feature/my-feature
|
|||||||
|
|
||||||
**Welcome aboard! 🎉**
|
**Welcome aboard! 🎉**
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,58 +1,173 @@
|
|||||||
# 📋 Requirements Specification v1.5.0
|
# 📋 Requirements Specification
|
||||||
|
|
||||||
## Status: first-draft
|
**Version:** 1.5.1
|
||||||
|
**Status:** Active
|
||||||
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
**Date:** 2025-11-30
|
---
|
||||||
|
|
||||||
|
## 📖 Overview
|
||||||
|
|
||||||
|
This directory contains the functional and non-functional requirements for the LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System). The requirements are organized by functional area and feature.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📑 Table of Contents
|
## 📑 Table of Contents
|
||||||
|
|
||||||
1. [Objectives & Goals](./01-objectives.md)
|
### Core Requirements
|
||||||
2. [System Architecture & Technology](./02-architecture.md)
|
|
||||||
3. [Functional Requirements](./03-functional-requirements.md)
|
1. [Objectives & Goals](./01-objectives.md) - Project objectives and success criteria
|
||||||
- [3.1 Project & Organization Management](./03.1-project-management.md)
|
2. [System Architecture & Technology](./02-architecture.md) - High-level architecture requirements
|
||||||
- [3.2 Correspondence Management](./03.2-correspondence.md)
|
3. [Functional Requirements](./03-functional-requirements.md) - Detailed feature specifications
|
||||||
- [3.3 RFA Management](./03.3-rfa.md)
|
|
||||||
- [3.4 Contract Drawing Management](./03.4-contract-drawing.md)
|
### Functional Areas
|
||||||
- [3.5 Shop Drawing Management](./03.5-shop-drawing.md)
|
|
||||||
- [3.6 Unified Workflow](./03.6-unified-workflow.md)
|
#### Document Management
|
||||||
- [3.7 Transmittals Management](./03.7-transmittals.md)
|
|
||||||
- [3.8 Circulation Sheet Management](./03.8-circulation-sheet.md)
|
- [3.1 Project & Organization Management](./03.1-project-management.md) - Projects, contracts, organizations
|
||||||
- [3.9 Revisions Management](./03.9-revisions.md)
|
- [3.2 Correspondence Management](./03.2-correspondence.md) - Letters and communications
|
||||||
- [3.10 File Handling](./03.10-file-handling.md)
|
- [3.3 RFA Management](./03.3-rfa.md) - Request for Approval
|
||||||
- [3.11 Document Numbering](./03.11-document-numbering.md)
|
- [3.4 Contract Drawing Management](./03.4-contract-drawing.md) - Contract drawings (แบบคู่สัญญา)
|
||||||
- [3.12 JSON Details](./03.12-json-details.md)
|
- [3.5 Shop Drawing Management](./03.5-shop-drawing.md) - Shop drawings (แบบก่อสร้าง)
|
||||||
4. [Access Control & RBAC](./04-access-control.md)
|
|
||||||
5. [UI/UX Requirements](./05-ui-ux.md)
|
#### Supporting Features
|
||||||
6. [Non-Functional Requirements](./06-non-functional.md)
|
|
||||||
7. [Testing Requirements](./07-testing.md)
|
- [3.6 Unified Workflow](./03.6-unified-workflow.md) - Workflow engine and routing
|
||||||
|
- [3.7 Transmittals Management](./03.7-transmittals.md) - Document transmittals
|
||||||
|
- [3.8 Circulation Sheet Management](./03.8-circulation-sheet.md) - Document circulation
|
||||||
|
- [3.9 Revisions Management](./03.9-revisions.md) - Version control
|
||||||
|
- [3.10 File Handling](./03.10-file-handling.md) - File storage and processing
|
||||||
|
|
||||||
|
#### **⭐ Document Numbering System**
|
||||||
|
|
||||||
|
- [3.11 Document Numbering](./03.11-document-numbering.md) - **Requirements**
|
||||||
|
- Automatic number generation
|
||||||
|
- Template-based formatting
|
||||||
|
- Concurrent request handling
|
||||||
|
- Counter management
|
||||||
|
|
||||||
|
**Implementation & Operations:**
|
||||||
|
|
||||||
|
- 📘 [Implementation Guide](../03-implementation/document-numbering.md) - NestJS, TypeORM, Redis code examples
|
||||||
|
- 📗 [Operations Guide](../04-operations/document-numbering-operations.md) - Monitoring, troubleshooting, runbooks
|
||||||
|
|
||||||
|
#### Technical Details
|
||||||
|
|
||||||
|
- [3.12 JSON Details](./03.12-json-details.md) - JSON field specifications
|
||||||
|
|
||||||
|
### Cross-Cutting Concerns
|
||||||
|
|
||||||
|
4. [Access Control & RBAC](./04-access-control.md) - 4-level hierarchical RBAC
|
||||||
|
5. [UI/UX Requirements](./05-ui-ux.md) - User interface specifications
|
||||||
|
6. [Non-Functional Requirements](./06-non-functional.md) - Performance, security, scalability
|
||||||
|
7. [Testing Requirements](./07-testing.md) - Test strategy and coverage
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔄 Recent Changes
|
## 🔄 Recent Changes
|
||||||
|
|
||||||
See [CHANGELOG.md](../../CHANGELOG.md) for detailed version history.
|
### v1.5.1 (2025-12-02)
|
||||||
|
|
||||||
### v1.4.5 (2025-11-30)
|
- ✅ **Reorganized Document Numbering documentation**
|
||||||
|
- Split into: Requirements → Implementation → Operations
|
||||||
|
- Created [document-numbering.md](../03-implementation/document-numbering.md) implementation guide
|
||||||
|
- Created [document-numbering-operations.md](../04-operations/document-numbering-operations.md) ops guide
|
||||||
|
- ✅ Updated schema to match v1.6.0 requirements
|
||||||
|
- ✅ Enhanced cross-references between documents
|
||||||
|
|
||||||
|
### v1.5.0 (2025-12-01)
|
||||||
|
|
||||||
- ✅ Added comprehensive security requirements
|
- ✅ Added comprehensive security requirements
|
||||||
- ✅ Enhanced resilience patterns
|
- ✅ Enhanced resilience patterns
|
||||||
- ✅ Added performance targets
|
- ✅ Added performance targets
|
||||||
- ⚠️ **Breaking:** Changed document numbering from stored procedure to app-level locking
|
- ⚠️ **Breaking:** Changed document numbering from stored procedure to app-level locking
|
||||||
|
|
||||||
---
|
### v1.4.5 (2025-11-30)
|
||||||
|
|
||||||
## 📊 Compliance Matrix
|
- ✅ Initial requirements documentation
|
||||||
|
- ✅ Functional requirements specified
|
||||||
|
|
||||||
| Requirement | Status | Owner | Target Release |
|
See [CHANGELOG.md](../../CHANGELOG.md) for detailed version history.
|
||||||
| ----------------------------- | ----------- | ------------ | -------------- |
|
|
||||||
| FR-001: Correspondence CRUD | ✅ Done | Backend Team | v1.0 |
|
|
||||||
| FR-002: RFA Workflow | In Progress | Backend Team | v1.1 |
|
|
||||||
| NFR-001: API Response < 200ms | Planned | DevOps | v1.2 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📬 Feedback
|
## 📊 Requirements Traceability
|
||||||
|
|
||||||
Found issues? [Open an issue](https://github.com/your-org/lcbp3-dms/issues/new?template=spec-issue.md)
|
### By Feature Status
|
||||||
|
|
||||||
|
| Feature Area | Requirements Doc | Status | Implementation | Operations |
|
||||||
|
|----------------------------|----------------------------------------|-------------|----------------|------------|
|
||||||
|
| Correspondence Management | [03.2](./03.2-correspondence.md) | ✅ Complete | Planned | N/A |
|
||||||
|
| RFA Management | [03.3](./03.3-rfa.md) | ✅ Complete | Planned | N/A |
|
||||||
|
| Workflow Engine | [03.6](./03.6-unified-workflow.md) | ✅ Complete | Planned | N/A |
|
||||||
|
| **Document Numbering** | [03.11](./03.11-document-numbering.md) | ✅ Complete | [Guide](../03-implementation/document-numbering.md) | [Guide](../04-operations/document-numbering-operations.md) |
|
||||||
|
| Access Control | [04](./04-access-control.md) | ✅ Complete | Planned | N/A |
|
||||||
|
|
||||||
|
### By Priority
|
||||||
|
|
||||||
|
- **P0 (Critical):** Access Control, Document Numbering
|
||||||
|
- **P1 (High):** Correspondence, RFA, Workflow Engine
|
||||||
|
- **P2 (Medium):** Transmittals, Circulation, Search
|
||||||
|
- **P3 (Low):** Reporting, Analytics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Requirements Quality Checklist
|
||||||
|
|
||||||
|
All requirements documents must meet these criteria:
|
||||||
|
|
||||||
|
- [ ] **Clear:** Written in simple, unambiguous language
|
||||||
|
- [ ] **Testable:** Can be verified through testing
|
||||||
|
- [ ] **Traceable:** Linked to business objectives
|
||||||
|
- [ ] **Feasible:** Technically achievable within constraints
|
||||||
|
- [ ] **Complete:** All edge cases and scenarios covered
|
||||||
|
- [ ] **Consistent:** No contradictions with other requirements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Reading Guide
|
||||||
|
|
||||||
|
### For Product Owners / Business Analysts
|
||||||
|
|
||||||
|
1. Start with [Objectives & Goals](./01-objectives.md)
|
||||||
|
2. Review [Functional Requirements](./03-functional-requirements.md)
|
||||||
|
3. Check specific feature requirements (3.1-3.12)
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
1. Read requirements document for your feature
|
||||||
|
2. Check [Implementation Guides](../03-implementation/) for technical details
|
||||||
|
3. Review [ADRs](../05-decisions/) for architectural decisions
|
||||||
|
4. Check [Tasks](../06-tasks/) for development breakdown
|
||||||
|
|
||||||
|
### For QA / Testers
|
||||||
|
|
||||||
|
1. Review [Testing Requirements](./07-testing.md)
|
||||||
|
2. Use requirements as test case source
|
||||||
|
3. Verify [Non-Functional Requirements](./06-non-functional.md)
|
||||||
|
|
||||||
|
### For Operations Team
|
||||||
|
|
||||||
|
1. Read [Non-Functional Requirements](./06-non-functional.md) for SLAs
|
||||||
|
2. Check [Operations Guides](../04-operations/) for specific features
|
||||||
|
3. Review monitoring and alerting requirements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📬 Feedback & Issues
|
||||||
|
|
||||||
|
**Found issues or have suggestions?**
|
||||||
|
|
||||||
|
- Requirements clarity issues → [Open Issue](https://github.com/your-org/lcbp3-dms/issues/new?template=spec-issue.md)
|
||||||
|
- Feature requests → Contact Product Owner
|
||||||
|
- Technical questions → Contact System Architect
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Document Control
|
||||||
|
|
||||||
|
- **Version:** 1.5.1
|
||||||
|
- **Owner:** System Architect (Nattanin Peancharoen)
|
||||||
|
- **Last Review:** 2025-12-02
|
||||||
|
- **Next Review:** 2026-01-01
|
||||||
|
- **Classification:** Internal Use Only
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 📋 Architecture Specification v1.5.0
|
# 📋 Architecture Specification
|
||||||
|
|
||||||
> **สถาปัตยกรรมระบบ LCBP3-DMS**
|
> **สถาปัตยกรรมระบบ LCBP3-DMS**
|
||||||
>
|
>
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
| Attribute | Value |
|
| Attribute | Value |
|
||||||
| ------------------ | -------------------------------- |
|
| ------------------ | -------------------------------- |
|
||||||
| **Version** | 1.5.0 |
|
| **Version** | 1.5.1 |
|
||||||
| **Status** | First Draft |
|
| **Status** | Active |
|
||||||
| **Last Updated** | 2025-11-30 |
|
| **Last Updated** | 2025-12-02 |
|
||||||
| **Owner** | Nattanin Peancharoen |
|
| **Owner** | Nattanin Peancharoen |
|
||||||
| **Classification** | Internal Technical Documentation |
|
| **Classification** | Internal Technical Documentation |
|
||||||
|
|
||||||
@@ -251,9 +251,34 @@ Layer 6: File Security (Virus Scanning, Access Control)
|
|||||||
- Workflow Versioning
|
- Workflow Versioning
|
||||||
- Polymorphic Entity Relationships
|
- Polymorphic Entity Relationships
|
||||||
|
|
||||||
**Related:** [specs/05-decisions/001-workflow-engine.md](../05-decisions/001-workflow-engine.md)
|
**Related:** [ADR-001](../05-decisions/ADR-001-unified-workflow-engine.md)
|
||||||
|
|
||||||
### ADR-002: Two-Phase File Storage
|
### ADR-002: Document Numbering Strategy
|
||||||
|
|
||||||
|
**Decision:** ใช้ Application-Level Locking แทน Database Stored Procedure
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
|
||||||
|
- ยืดหยุ่นกว่า (Template-Based Generator)
|
||||||
|
- ง่ายต่อการ Debug และ Monitoring
|
||||||
|
- รองรับ Complex Numbering Rules
|
||||||
|
- Support ทุกประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.)
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
|
||||||
|
- **Layer 1:** Redis Redlock (Distributed Lock)
|
||||||
|
- **Layer 2:** Optimistic Database Lock (`@VersionColumn()`)
|
||||||
|
- **Retry:** Exponential Backoff with Jitter
|
||||||
|
- **Counter Key:** Composite PK (8 columns)
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- 📋 [Requirements](../01-requirements/03.11-document-numbering.md)
|
||||||
|
- 📘 [Implementation Guide](../03-implementation/document-numbering.md)
|
||||||
|
- 📗 [Operations Guide](../04-operations/document-numbering-operations.md)
|
||||||
|
|
||||||
|
**Related:** [ADR-002](../05-decisions/ADR-002-document-numbering-strategy.md)
|
||||||
|
|
||||||
|
### ADR-003: Two-Phase File Storage
|
||||||
|
|
||||||
**Decision:** แยกการอัปโหลดไฟล์เป็น 2 ขั้นตอน (Upload → Commit)
|
**Decision:** แยกการอัปโหลดไฟล์เป็น 2 ขั้นตอน (Upload → Commit)
|
||||||
|
|
||||||
@@ -269,23 +294,7 @@ Layer 6: File Security (Virus Scanning, Access Control)
|
|||||||
2. Phase 2: Commit to `permanent/` when operation succeeds
|
2. Phase 2: Commit to `permanent/` when operation succeeds
|
||||||
3. Cleanup: Cron Job ลบไฟล์ค้างใน `temp/` > 24h
|
3. Cleanup: Cron Job ลบไฟล์ค้างใน `temp/` > 24h
|
||||||
|
|
||||||
**Related:** [specs/05-decisions/002-file-storage.md](../05-decisions/002-file-storage.md)
|
**Related:** [ADR-003](../05-decisions/ADR-003-file-storage-approach.md)
|
||||||
|
|
||||||
### ADR-003: Document Numbering Strategy
|
|
||||||
|
|
||||||
**Decision:** ใช้ Application-Level Locking แทน Database Stored Procedure
|
|
||||||
|
|
||||||
**Rationale:**
|
|
||||||
|
|
||||||
- ยืดหยุ่นกว่า (Token-Based Generator)
|
|
||||||
- ง่ายต่อการ Debug
|
|
||||||
- รองรับ Complex Numbering Rules
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
|
|
||||||
- Layer 1: Redis Distributed Lock
|
|
||||||
- Layer 2: Optimistic Database Lock (`@VersionColumn()`)
|
|
||||||
- Retry with Exponential Backoff
|
|
||||||
|
|
||||||
### ADR-004: 4-Level RBAC
|
### ADR-004: 4-Level RBAC
|
||||||
|
|
||||||
@@ -303,6 +312,8 @@ Layer 6: File Security (Virus Scanning, Access Control)
|
|||||||
- Redis Cache for Performance
|
- Redis Cache for Performance
|
||||||
- Permission Checking at Guard Level
|
- Permission Checking at Guard Level
|
||||||
|
|
||||||
|
**Related:** [ADR-004](../05-decisions/ADR-004-rbac-implementation.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 Architecture Diagrams
|
## 📊 Architecture Diagrams
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
**title:** 'API Design'
|
**title:** 'API Design'
|
||||||
**version:** 1.5.0
|
**version:** 1.5.1
|
||||||
**status:** first-draft
|
**status:** active
|
||||||
**owner:** Nattanin Peancharoen
|
**owner:** Nattanin Peancharoen
|
||||||
**last_updated:** 2025-11-30
|
**last_updated:** 2025-12-02
|
||||||
**related:**
|
**related:**
|
||||||
|
|
||||||
- specs/01-requirements/02-architecture.md
|
- specs/01-requirements/02-architecture.md
|
||||||
@@ -545,7 +545,7 @@ X-API-Deprecation-Info: https://docs.np-dms.work/migration/v2
|
|||||||
|
|
||||||
**Document Control:**
|
**Document Control:**
|
||||||
|
|
||||||
- **Version:** 1.5.0
|
- **Version:** 1.5.1
|
||||||
- **Status:** First Draft
|
- **Status:** Active
|
||||||
- **Last Updated:** 2025-11-30
|
- **Last Updated:** 2025-12-02
|
||||||
- **Owner:** Nattanin Peancharoen
|
- **Owner:** Nattanin Peancharoen
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
- RAM: 32GB
|
- RAM: 32GB
|
||||||
- Storage: /share/dms-data
|
- Storage: /share/dms-data
|
||||||
- **IP Address:** 159.192.126.103
|
- **IP Address:** 159.192.126.103
|
||||||
- **Domain:** np-dms.work, www.np-dms.work
|
- **Domain:** np-dms.work, <www.np-dms.work>
|
||||||
- **Containerization:** Docker & Docker Compose via Container Station
|
- **Containerization:** Docker & Docker Compose via Container Station
|
||||||
- **Development Environment:** VS Code/Cursor on Windows 11
|
- **Development Environment:** VS Code/Cursor on Windows 11
|
||||||
|
|
||||||
@@ -943,9 +943,9 @@ graph LR
|
|||||||
|
|
||||||
**Document Control:**
|
**Document Control:**
|
||||||
|
|
||||||
- **Version:** 1.5.0
|
- **Version:** 1.5.1
|
||||||
- **Status:** First Draft
|
- **Status:** Active
|
||||||
- **Last Updated:** 2025-11-30
|
- **Last Updated:** 2025-12-02
|
||||||
- **Owner:** Nattanin Peancharoen
|
- **Owner:** Nattanin Peancharoen
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
627
specs/03-implementation/document-numbering.md
Normal file
627
specs/03-implementation/document-numbering.md
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
# Document Numbering Implementation Guide
|
||||||
|
|
||||||
|
---
|
||||||
|
title: 'Implementation Guide: Document Numbering System'
|
||||||
|
version: 1.5.1
|
||||||
|
status: draft
|
||||||
|
owner: Development Team
|
||||||
|
last_updated: 2025-12-02
|
||||||
|
related:
|
||||||
|
|
||||||
|
- specs/01-requirements/03.11-document-numbering.md
|
||||||
|
- specs/04-operations/document-numbering-operations.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
เอกสารนี้อธิบาย implementation details สำหรับระบบ Document Numbering ตาม requirements ใน [03.11-document-numbering.md](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md)
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Backend Framework**: NestJS 10.x
|
||||||
|
- **ORM**: TypeORM 0.3.x
|
||||||
|
- **Database**: MariaDB 10.11
|
||||||
|
- **Cache/Lock**: Redis 7.x + Redlock
|
||||||
|
- **Message Queue**: BullMQ
|
||||||
|
- **Monitoring**: Prometheus + Grafana
|
||||||
|
|
||||||
|
## 1. Database Implementation
|
||||||
|
|
||||||
|
### 1.1. Counter Table Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE document_number_counters (
|
||||||
|
project_id INT NOT NULL,
|
||||||
|
originator_organization_id INT NOT NULL,
|
||||||
|
recipient_organization_id INT NULL,
|
||||||
|
correspondence_type_id INT NOT NULL,
|
||||||
|
sub_type_id INT DEFAULT 0,
|
||||||
|
rfa_type_id INT DEFAULT 0,
|
||||||
|
discipline_id INT DEFAULT 0,
|
||||||
|
current_year INT NOT NULL,
|
||||||
|
version INT DEFAULT 0 NOT NULL,
|
||||||
|
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,
|
||||||
|
|
||||||
|
INDEX idx_counter_lookup (project_id, correspondence_type_id, current_year),
|
||||||
|
INDEX idx_counter_org (originator_organization_id, current_year),
|
||||||
|
|
||||||
|
CONSTRAINT chk_last_number_positive CHECK (last_number >= 0),
|
||||||
|
CONSTRAINT chk_current_year_valid CHECK (current_year BETWEEN 2020 AND 2100)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||||
|
COMMENT='ตารางเก็บ Running Number Counters';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2. Audit Table Schema
|
||||||
|
|
||||||
|
```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';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3. Error Log Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
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';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. NestJS Implementation
|
||||||
|
|
||||||
|
### 2.1. Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/modules/document-numbering/
|
||||||
|
├── document-numbering.module.ts
|
||||||
|
├── controllers/
|
||||||
|
│ └── document-numbering.controller.ts
|
||||||
|
├── services/
|
||||||
|
│ ├── document-numbering.service.ts
|
||||||
|
│ ├── document-numbering-lock.service.ts
|
||||||
|
│ ├── counter.service.ts
|
||||||
|
│ ├── template.service.ts
|
||||||
|
│ └── audit.service.ts
|
||||||
|
├── entities/
|
||||||
|
│ ├── document-number-counter.entity.ts
|
||||||
|
│ ├── document-number-audit.entity.ts
|
||||||
|
│ └── document-number-error.entity.ts
|
||||||
|
├── dto/
|
||||||
|
│ ├── generate-number.dto.ts
|
||||||
|
│ └── update-template.dto.ts
|
||||||
|
├── validators/
|
||||||
|
│ └── template.validator.ts
|
||||||
|
├── jobs/
|
||||||
|
│ └── counter-reset.job.ts
|
||||||
|
└── metrics/
|
||||||
|
└── metrics.service.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2. TypeORM Entity
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3. Redis Lock Service
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/services/document-numbering-lock.service.ts
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import Redlock from 'redlock';
|
||||||
|
import { InjectRedis } from '@nestjs-modules/ioredis';
|
||||||
|
import { Redis } from 'ioredis';
|
||||||
|
|
||||||
|
interface CounterKey {
|
||||||
|
projectId: number;
|
||||||
|
originatorOrgId: number;
|
||||||
|
recipientOrgId: number | null;
|
||||||
|
correspondenceTypeId: number;
|
||||||
|
subTypeId: number;
|
||||||
|
rfaTypeId: number;
|
||||||
|
disciplineId: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocumentNumberingLockService {
|
||||||
|
private readonly logger = new Logger(DocumentNumberingLockService.name);
|
||||||
|
private redlock: Redlock;
|
||||||
|
|
||||||
|
constructor(@InjectRedis() private readonly redis: Redis) {
|
||||||
|
this.redlock = new Redlock([redis], {
|
||||||
|
driftFactor: 0.01,
|
||||||
|
retryCount: 5,
|
||||||
|
retryDelay: 100,
|
||||||
|
retryJitter: 50,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquireLock(counterKey: CounterKey): Promise<Redlock.Lock> {
|
||||||
|
const lockKey = this.buildLockKey(counterKey);
|
||||||
|
const ttl = 5000; // 5 วินาที
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lock = await this.redlock.acquire([lockKey], ttl);
|
||||||
|
this.logger.debug(`Acquired lock: ${lockKey}`);
|
||||||
|
return lock;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to acquire lock: ${lockKey}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseLock(lock: Redlock.Lock): Promise<void> {
|
||||||
|
try {
|
||||||
|
await lock.release();
|
||||||
|
this.logger.debug('Released lock');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn('Failed to release lock (may have expired)', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4. Counter Service
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/services/counter.service.ts
|
||||||
|
import { Injectable, ConflictException, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, DataSource } from 'typeorm';
|
||||||
|
import { DocumentNumberCounter } from '../entities/document-number-counter.entity';
|
||||||
|
import { OptimisticLockVersionMismatchError } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CounterService {
|
||||||
|
private readonly logger = new Logger(CounterService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(DocumentNumberCounter)
|
||||||
|
private counterRepo: Repository<DocumentNumberCounter>,
|
||||||
|
private dataSource: DataSource,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async incrementCounter(counterKey: CounterKey): Promise<number> {
|
||||||
|
const MAX_RETRIES = 2;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
return await this.dataSource.transaction(async (manager) => {
|
||||||
|
// ใช้ Optimistic Locking
|
||||||
|
const counter = await manager.findOne(DocumentNumberCounter, {
|
||||||
|
where: this.buildWhereClause(counterKey),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!counter) {
|
||||||
|
// สร้าง counter ใหม่
|
||||||
|
const newCounter = manager.create(DocumentNumberCounter, {
|
||||||
|
...counterKey,
|
||||||
|
lastNumber: 1,
|
||||||
|
version: 0,
|
||||||
|
});
|
||||||
|
await manager.save(newCounter);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.lastNumber += 1;
|
||||||
|
await manager.save(counter); // Auto-check version
|
||||||
|
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('เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่');
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildWhereClause(key: CounterKey) {
|
||||||
|
return {
|
||||||
|
projectId: key.projectId,
|
||||||
|
originatorOrganizationId: key.originatorOrgId,
|
||||||
|
recipientOrganizationId: key.recipientOrgId,
|
||||||
|
correspondenceTypeId: key.correspondenceTypeId,
|
||||||
|
subTypeId: key.subTypeId,
|
||||||
|
rfaTypeId: key.rfaTypeId,
|
||||||
|
disciplineId: key.disciplineId,
|
||||||
|
currentYear: key.year,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5. Main Service with Retry Logic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/services/document-numbering.service.ts
|
||||||
|
import { Injectable, ServiceUnavailableException, Logger } from '@nestjs/common';
|
||||||
|
import { DocumentNumberingLockService } from './document-numbering-lock.service';
|
||||||
|
import { CounterService } from './counter.service';
|
||||||
|
import { AuditService } from './audit.service';
|
||||||
|
import { RedisConnectionError } from 'ioredis';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocumentNumberingService {
|
||||||
|
private readonly logger = new Logger(DocumentNumberingService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private lockService: DocumentNumberingLockService,
|
||||||
|
private counterService: CounterService,
|
||||||
|
private auditService: AuditService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async generateDocumentNumber(dto: GenerateNumberDto): Promise<string> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
let lockWaitMs = 0;
|
||||||
|
let retryCount = 0;
|
||||||
|
let fallbackUsed = 'NONE';
|
||||||
|
|
||||||
|
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');
|
||||||
|
fallbackUsed = 'DB_LOCK';
|
||||||
|
return await this.generateWithDbLock(dto);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// บันทึก audit log
|
||||||
|
await this.auditService.logGeneration({
|
||||||
|
documentId: dto.documentId,
|
||||||
|
counterKey: dto.counterKey,
|
||||||
|
lockWaitMs,
|
||||||
|
totalDurationMs: Date.now() - startTime,
|
||||||
|
fallbackUsed,
|
||||||
|
retryCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateWithRedisLock(dto: GenerateNumberDto): Promise<string> {
|
||||||
|
const lock = await this.lockService.acquireLock(dto.counterKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nextNumber = await this.counterService.incrementCounter(dto.counterKey);
|
||||||
|
return this.formatNumber(dto.template, nextNumber, dto.counterKey);
|
||||||
|
} finally {
|
||||||
|
await this.lockService.releaseLock(lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateWithDbLock(dto: GenerateNumberDto): Promise<string> {
|
||||||
|
// ใช้ pessimistic lock
|
||||||
|
// Implementation details...
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatNumber(template: string, seq: number, key: CounterKey): string {
|
||||||
|
// Template formatting logic
|
||||||
|
// Example: `คคง.-สคฉ.3-0001-2568`
|
||||||
|
return template
|
||||||
|
.replace('{SEQ:4}', seq.toString().padStart(4, '0'))
|
||||||
|
.replace('{YEAR:B.E.}', (key.year + 543).toString());
|
||||||
|
// ... more replacements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Template Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/validators/template.validator.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 (!tokens.some((t) => t.name === 'DISCIPLINE')) {
|
||||||
|
errors.push('RFA template ต้องมี {DISCIPLINE}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (correspondenceType === 'TRANSMITTAL') {
|
||||||
|
if (!tokens.some((t) => t.name === 'SUB_TYPE')) {
|
||||||
|
errors.push('TRANSMITTAL template ต้องมี {SUB_TYPE}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ทุก template ต้องมี {SEQ}
|
||||||
|
if (!tokens.some((t) => t.name.startsWith('SEQ'))) {
|
||||||
|
errors.push('Template ต้องมี {SEQ:n}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: errors.length === 0, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTokens(template: string) {
|
||||||
|
const regex = /\{([^}]+)\}/g;
|
||||||
|
const tokens: Array<{ name: string; full: string }> = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(template)) !== null) {
|
||||||
|
const tokenName = match[1].split(':')[0]; // SEQ:4 → SEQ
|
||||||
|
tokens.push({ name: tokenName, full: match[1] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. BullMQ Job for Counter Reset
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/jobs/counter-reset.job.ts
|
||||||
|
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Cron } from '@nestjs/schedule';
|
||||||
|
|
||||||
|
@Processor('document-numbering')
|
||||||
|
@Injectable()
|
||||||
|
export class CounterResetJob extends WorkerHost {
|
||||||
|
private readonly logger = new Logger(CounterResetJob.name);
|
||||||
|
|
||||||
|
@Cron('0 0 1 1 *') // 1 Jan every year at 00:00
|
||||||
|
async handleYearlyReset() {
|
||||||
|
const newYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
// ไม่ต้อง reset counter เพราะ counter แยกตาม current_year อยู่แล้ว
|
||||||
|
// แค่เตรียม counter สำหรับปีใหม่
|
||||||
|
this.logger.log(`Year changed to ${newYear}, counters are ready`);
|
||||||
|
|
||||||
|
// สามารถทำ cleanup counter ปีเก่าได้ (optional)
|
||||||
|
// await this.cleanupOldCounters(newYear - 5); // เก็บ 5 ปี
|
||||||
|
}
|
||||||
|
|
||||||
|
async process() {
|
||||||
|
// BullMQ job processing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. API Controller
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/controllers/document-numbering.controller.ts
|
||||||
|
import { Controller, Post, Put, Body, Param, UseGuards } from '@nestjs/common';
|
||||||
|
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||||
|
import { Throttle } from '@nestjs/throttler';
|
||||||
|
import { DocumentNumberingService } from '../services/document-numbering.service';
|
||||||
|
import { Roles } from 'src/auth/decorators/roles.decorator';
|
||||||
|
|
||||||
|
@Controller('document-numbering')
|
||||||
|
@UseGuards(ThrottlerGuard)
|
||||||
|
export class DocumentNumberingController {
|
||||||
|
constructor(
|
||||||
|
private readonly documentNumberingService: DocumentNumberingService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('generate')
|
||||||
|
@Throttle(10, 60) // 10 requests per 60 seconds
|
||||||
|
async generateNumber(@Body() dto: GenerateNumberDto) {
|
||||||
|
const number = await this.documentNumberingService.generateDocumentNumber(dto);
|
||||||
|
return { documentNumber: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('configs/:configId')
|
||||||
|
@Roles('PROJECT_ADMIN')
|
||||||
|
async updateTemplate(
|
||||||
|
@Param('configId') configId: number,
|
||||||
|
@Body() dto: UpdateTemplateDto,
|
||||||
|
) {
|
||||||
|
// Update template configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('configs/:configId/reset-counter')
|
||||||
|
@Roles('SUPER_ADMIN')
|
||||||
|
async resetCounter(
|
||||||
|
@Param('configId') configId: number,
|
||||||
|
@Body() dto: ResetCounterDto,
|
||||||
|
) {
|
||||||
|
// Manual counter reset (requires approval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Module Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// File: src/modules/document-numbering/document-numbering.module.ts
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
import { DocumentNumberCounter } from './entities/document-number-counter.entity';
|
||||||
|
import { DocumentNumberAudit } from './entities/document-number-audit.entity';
|
||||||
|
import { DocumentNumberError } from './entities/document-number-error.entity';
|
||||||
|
import { DocumentNumberingService } from './services/document-numbering.service';
|
||||||
|
import { DocumentNumberingLockService } from './services/document-numbering-lock.service';
|
||||||
|
import { CounterService } from './services/counter.service';
|
||||||
|
import { AuditService } from './services/audit.service';
|
||||||
|
import { TemplateValidator } from './validators/template.validator';
|
||||||
|
import { CounterResetJob } from './jobs/counter-reset.job';
|
||||||
|
import { DocumentNumberingController } from './controllers/document-numbering.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
DocumentNumberCounter,
|
||||||
|
DocumentNumberAudit,
|
||||||
|
DocumentNumberError,
|
||||||
|
]),
|
||||||
|
BullModule.registerQueue({
|
||||||
|
name: 'document-numbering',
|
||||||
|
}),
|
||||||
|
ThrottlerModule.forRoot({
|
||||||
|
ttl: 60,
|
||||||
|
limit: 10,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [DocumentNumberingController],
|
||||||
|
providers: [
|
||||||
|
DocumentNumberingService,
|
||||||
|
DocumentNumberingLockService,
|
||||||
|
CounterService,
|
||||||
|
AuditService,
|
||||||
|
TemplateValidator,
|
||||||
|
CounterResetJob,
|
||||||
|
],
|
||||||
|
exports: [DocumentNumberingService],
|
||||||
|
})
|
||||||
|
export class DocumentNumberingModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Environment Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// .env.example
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=lcbp3
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_DATABASE=lcbp3_db
|
||||||
|
DB_POOL_SIZE=20
|
||||||
|
|
||||||
|
# Prometheus
|
||||||
|
PROMETHEUS_PORT=9090
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Requirements](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md)
|
||||||
|
- [Operations Guide](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md)
|
||||||
|
- [Backend Guidelines](file:///e:/np-dms/lcbp3/specs/03-implementation/backend-guidelines.md)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Operations Documentation
|
# Operations Documentation
|
||||||
|
|
||||||
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
|
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -185,6 +185,6 @@ graph TB
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Status:** Active
|
**Status:** Active
|
||||||
**Classification:** Internal Use Only
|
**Classification:** Internal Use Only
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Backup & Recovery Procedures
|
# Backup & Recovery Procedures
|
||||||
|
|
||||||
**Project:** LCBP3-DMS
|
**Project:** LCBP3-DMS
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -369,6 +369,6 @@ WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Review:** 2025-12-01
|
**Last Review:** 2025-12-01
|
||||||
**Next Review:** 2026-03-01
|
**Next Review:** 2026-03-01
|
||||||
|
|||||||
937
specs/04-operations/deployment-guide.md
Normal file
937
specs/04-operations/deployment-guide.md
Normal file
@@ -0,0 +1,937 @@
|
|||||||
|
# Deployment Guide: LCBP3-DMS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
|
||||||
|
**Version:** 1.5.1
|
||||||
|
**Last Updated:** 2025-12-02
|
||||||
|
**Owner:** Operations Team
|
||||||
|
**Status:** Active
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
This guide provides step-by-step instructions for deploying the LCBP3-DMS system on QNAP Container Station using Docker Compose with Blue-Green deployment strategy.
|
||||||
|
|
||||||
|
### Deployment Strategy
|
||||||
|
|
||||||
|
- **Platform:** QNAP TS-473A with Container Station
|
||||||
|
- **Orchestration:** Docker Compose
|
||||||
|
- **Deployment Method:** Blue-Green Deployment
|
||||||
|
- **Zero Downtime:** Yes
|
||||||
|
- **Rollback Capability:** Instant rollback via NGINX switch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Prerequisites
|
||||||
|
|
||||||
|
### Hardware Requirements
|
||||||
|
|
||||||
|
| Component | Minimum Specification |
|
||||||
|
| -------------- | -------------------------- |
|
||||||
|
| CPU | 4 cores @ 2.0 GHz |
|
||||||
|
| RAM | 16 GB |
|
||||||
|
| Storage | 500 GB SSD (System + Data) |
|
||||||
|
| Network | 1 Gbps Ethernet |
|
||||||
|
| QNAP Model | TS-473A or equivalent |
|
||||||
|
|
||||||
|
### Software Requirements
|
||||||
|
|
||||||
|
| Software | Version | Purpose |
|
||||||
|
| ----------------- | ------- | ------------------------ |
|
||||||
|
| QNAP QTS | 5.x+ | Operating System |
|
||||||
|
| Container Station | 3.x+ | Docker Management |
|
||||||
|
| Docker | 20.10+ | Container Runtime |
|
||||||
|
| Docker Compose | 2.x+ | Multi-container Orchestr |
|
||||||
|
|
||||||
|
### Network Requirements
|
||||||
|
|
||||||
|
- Static IP address for QNAP server
|
||||||
|
- Domain name (e.g., `lcbp3-dms.example.com`)
|
||||||
|
- SSL certificate (Let's Encrypt or commercial)
|
||||||
|
- Firewall rules:
|
||||||
|
- Port 80 (HTTP → HTTPS redirect)
|
||||||
|
- Port 443 (HTTPS)
|
||||||
|
- Port 22 (SSH for management)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Infrastructure Setup
|
||||||
|
|
||||||
|
### 1. Directory Structure
|
||||||
|
|
||||||
|
Create the following directory structure on QNAP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH into QNAP
|
||||||
|
ssh admin@qnap-ip
|
||||||
|
|
||||||
|
# Create base directory
|
||||||
|
mkdir -p /volume1/lcbp3
|
||||||
|
|
||||||
|
# Create blue-green environments
|
||||||
|
mkdir -p /volume1/lcbp3/blue
|
||||||
|
mkdir -p /volume1/lcbp3/green
|
||||||
|
|
||||||
|
# Create shared directories
|
||||||
|
mkdir -p /volume1/lcbp3/shared/uploads
|
||||||
|
mkdir -p /volume1/lcbp3/shared/logs
|
||||||
|
mkdir -p /volume1/lcbp3/shared/backups
|
||||||
|
|
||||||
|
# Create persistent volumes
|
||||||
|
mkdir -p /volume1/lcbp3/volumes/mariadb-data
|
||||||
|
mkdir -p /volume1/lcbp3/volumes/redis-data
|
||||||
|
mkdir -p /volume1/lcbp3/volumes/elastic-data
|
||||||
|
|
||||||
|
# Create NGINX proxy directory
|
||||||
|
mkdir -p /volume1/lcbp3/nginx-proxy
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
chmod -R 755 /volume1/lcbp3
|
||||||
|
chown -R admin:administrators /volume1/lcbp3
|
||||||
|
```
|
||||||
|
|
||||||
|
**Final Structure:**
|
||||||
|
|
||||||
|
```
|
||||||
|
/volume1/lcbp3/
|
||||||
|
├── blue/ # Blue environment
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── .env.production
|
||||||
|
│ └── nginx.conf
|
||||||
|
│
|
||||||
|
├── green/ # Green environment
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── .env.production
|
||||||
|
│ └── nginx.conf
|
||||||
|
│
|
||||||
|
├── nginx-proxy/ # Main reverse proxy
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── nginx.conf
|
||||||
|
│ └── ssl/
|
||||||
|
│ ├── cert.pem
|
||||||
|
│ └── key.pem
|
||||||
|
│
|
||||||
|
├── shared/ # Shared across blue/green
|
||||||
|
│ ├── uploads/
|
||||||
|
│ ├── logs/
|
||||||
|
│ └── backups/
|
||||||
|
│
|
||||||
|
├── volumes/ # Persistent data
|
||||||
|
│ ├── mariadb-data/
|
||||||
|
│ ├── redis-data/
|
||||||
|
│ └── elastic-data/
|
||||||
|
│
|
||||||
|
├── scripts/ # Deployment scripts
|
||||||
|
│ ├── deploy.sh
|
||||||
|
│ ├── rollback.sh
|
||||||
|
│ └── health-check.sh
|
||||||
|
│
|
||||||
|
└── current # File containing "blue" or "green"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. SSL Certificate Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Option 1: Let's Encrypt (Recommended)
|
||||||
|
# Install certbot on QNAP
|
||||||
|
opkg install certbot
|
||||||
|
|
||||||
|
# Generate certificate
|
||||||
|
certbot certonly --standalone \
|
||||||
|
-d lcbp3-dms.example.com \
|
||||||
|
--email admin@example.com \
|
||||||
|
--agree-tos
|
||||||
|
|
||||||
|
# Copy to nginx-proxy
|
||||||
|
cp /etc/letsencrypt/live/lcbp3-dms.example.com/fullchain.pem \
|
||||||
|
/volume1/lcbp3/nginx-proxy/ssl/cert.pem
|
||||||
|
cp /etc/letsencrypt/live/lcbp3-dms.example.com/privkey.pem \
|
||||||
|
/volume1/lcbp3/nginx-proxy/ssl/key.pem
|
||||||
|
|
||||||
|
# Option 2: Commercial Certificate
|
||||||
|
# Upload cert.pem and key.pem to /volume1/lcbp3/nginx-proxy/ssl/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Configuration Files
|
||||||
|
|
||||||
|
### 1. Environment Variables (.env.production)
|
||||||
|
|
||||||
|
Create `.env.production` in both `blue/` and `green/` directories:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# File: /volume1/lcbp3/blue/.env.production
|
||||||
|
# DO NOT commit this file to Git!
|
||||||
|
|
||||||
|
# Application
|
||||||
|
NODE_ENV=production
|
||||||
|
APP_NAME=LCBP3-DMS
|
||||||
|
APP_URL=https://lcbp3-dms.example.com
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_HOST=lcbp3-mariadb
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=lcbp3_user
|
||||||
|
DB_PASSWORD=<CHANGE_ME_STRONG_PASSWORD>
|
||||||
|
DB_DATABASE=lcbp3_dms
|
||||||
|
DB_POOL_SIZE=20
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST=lcbp3-redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=<CHANGE_ME_STRONG_PASSWORD>
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
# JWT Authentication
|
||||||
|
JWT_SECRET=<CHANGE_ME_RANDOM_64_CHAR_STRING>
|
||||||
|
JWT_EXPIRES_IN=8h
|
||||||
|
JWT_REFRESH_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
# File Storage
|
||||||
|
UPLOAD_PATH=/app/uploads
|
||||||
|
MAX_FILE_SIZE=52428800
|
||||||
|
ALLOWED_FILE_TYPES=.pdf,.doc,.docx,.xls,.xlsx,.dwg,.zip
|
||||||
|
|
||||||
|
# Email (SMTP)
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=false
|
||||||
|
SMTP_USERNAME=<YOUR_EMAIL>
|
||||||
|
SMTP_PASSWORD=<YOUR_APP_PASSWORD>
|
||||||
|
SMTP_FROM=noreply@example.com
|
||||||
|
|
||||||
|
# Elasticsearch
|
||||||
|
ELASTICSEARCH_NODE=http://lcbp3-elasticsearch:9200
|
||||||
|
ELASTICSEARCH_USERNAME=elastic
|
||||||
|
ELASTICSEARCH_PASSWORD=<CHANGE_ME>
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
THROTTLE_TTL=60
|
||||||
|
THROTTLE_LIMIT=100
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_FILE_PATH=/app/logs
|
||||||
|
|
||||||
|
# ClamAV (Virus Scanning)
|
||||||
|
CLAMAV_HOST=lcbp3-clamav
|
||||||
|
CLAMAV_PORT=3310
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Docker Compose - Blue Environment
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# File: /volume1/lcbp3/blue/docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
image: lcbp3-backend:latest
|
||||||
|
container_name: lcbp3-blue-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env.production
|
||||||
|
volumes:
|
||||||
|
- /volume1/lcbp3/shared/uploads:/app/uploads
|
||||||
|
- /volume1/lcbp3/shared/logs:/app/logs
|
||||||
|
depends_on:
|
||||||
|
- mariadb
|
||||||
|
- redis
|
||||||
|
- elasticsearch
|
||||||
|
networks:
|
||||||
|
- lcbp3-network
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: lcbp3-frontend:latest
|
||||||
|
container_name: lcbp3-blue-frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NEXT_PUBLIC_API_URL=https://lcbp3-dms.example.com/api
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- lcbp3-network
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'curl', '-f', 'http://localhost:3000']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:10.11
|
||||||
|
container_name: lcbp3-mariadb
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||||
|
MYSQL_DATABASE: ${DB_DATABASE}
|
||||||
|
MYSQL_USER: ${DB_USERNAME}
|
||||||
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- /volume1/lcbp3/volumes/mariadb-data:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- lcbp3-network
|
||||||
|
command: >
|
||||||
|
--character-set-server=utf8mb4
|
||||||
|
--collation-server=utf8mb4_unicode_ci
|
||||||
|
--max_connections=200
|
||||||
|
--innodb_buffer_pool_size=2G
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: lcbp3-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: >
|
||||||
|
redis-server
|
||||||
|
--requirepass ${REDIS_PASSWORD}
|
||||||
|
--appendonly yes
|
||||||
|
--appendfsync everysec
|
||||||
|
--maxmemory 2gb
|
||||||
|
--maxmemory-policy allkeys-lru
|
||||||
|
volumes:
|
||||||
|
- /volume1/lcbp3/volumes/redis-data:/data
|
||||||
|
networks:
|
||||||
|
- lcbp3-network
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
elasticsearch:
|
||||||
|
image: elasticsearch:8.11.0
|
||||||
|
container_name: lcbp3-elasticsearch
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- discovery.type=single-node
|
||||||
|
- xpack.security.enabled=true
|
||||||
|
- ELASTIC_PASSWORD=${ELASTICSEARCH_PASSWORD}
|
||||||
|
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||||
|
volumes:
|
||||||
|
- /volume1/lcbp3/volumes/elastic-data:/usr/share/elasticsearch/data
|
||||||
|
networks:
|
||||||
|
- lcbp3-network
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'curl -f http://localhost:9200/_cluster/health || exit 1']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
networks:
|
||||||
|
lcbp3-network:
|
||||||
|
name: lcbp3-blue-network
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Docker Compose - NGINX Proxy
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# File: /volume1/lcbp3/nginx-proxy/docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: lcbp3-nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./ssl:/etc/nginx/ssl:ro
|
||||||
|
- /volume1/lcbp3/shared/logs/nginx:/var/log/nginx
|
||||||
|
networks:
|
||||||
|
- lcbp3-blue-network
|
||||||
|
- lcbp3-green-network
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'nginx', '-t']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
networks:
|
||||||
|
lcbp3-blue-network:
|
||||||
|
external: true
|
||||||
|
lcbp3-green-network:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. NGINX Configuration
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# File: /volume1/lcbp3/nginx-proxy/nginx.conf
|
||||||
|
|
||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
client_max_body_size 50M;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript
|
||||||
|
application/json application/javascript application/xml+rss;
|
||||||
|
|
||||||
|
# Upstream backends (switch between blue/green)
|
||||||
|
upstream backend {
|
||||||
|
server lcbp3-blue-backend:3000 max_fails=3 fail_timeout=30s;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream frontend {
|
||||||
|
server lcbp3-blue-frontend:3000 max_fails=3 fail_timeout=30s;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP to HTTPS redirect
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name lcbp3-dms.example.com;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS server
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name lcbp3-dms.example.com;
|
||||||
|
|
||||||
|
# SSL configuration
|
||||||
|
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# Frontend (Next.js)
|
||||||
|
location / {
|
||||||
|
proxy_pass http://frontend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backend API
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Timeouts for file uploads
|
||||||
|
proxy_connect_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint (no logging)
|
||||||
|
location /health {
|
||||||
|
proxy_pass http://backend/health;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static files caching
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
proxy_pass http://frontend;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Initial Deployment
|
||||||
|
|
||||||
|
### Step 1: Prepare Docker Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build images (on development machine)
|
||||||
|
cd /path/to/lcbp3/backend
|
||||||
|
docker build -t lcbp3-backend:1.0.0 .
|
||||||
|
docker tag lcbp3-backend:1.0.0 lcbp3-backend:latest
|
||||||
|
|
||||||
|
cd /path/to/lcbp3/frontend
|
||||||
|
docker build -t lcbp3-frontend:1.0.0 .
|
||||||
|
docker tag lcbp3-frontend:1.0.0 lcbp3-frontend:latest
|
||||||
|
|
||||||
|
# Save images to tar files
|
||||||
|
docker save lcbp3-backend:latest | gzip > lcbp3-backend-latest.tar.gz
|
||||||
|
docker save lcbp3-frontend:latest | gzip > lcbp3-frontend-latest.tar.gz
|
||||||
|
|
||||||
|
# Transfer to QNAP
|
||||||
|
scp lcbp3-backend-latest.tar.gz admin@qnap-ip:/volume1/lcbp3/
|
||||||
|
scp lcbp3-frontend-latest.tar.gz admin@qnap-ip:/volume1/lcbp3/
|
||||||
|
|
||||||
|
# Load images on QNAP
|
||||||
|
ssh admin@qnap-ip
|
||||||
|
cd /volume1/lcbp3
|
||||||
|
docker load < lcbp3-backend-latest.tar.gz
|
||||||
|
docker load < lcbp3-frontend-latest.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Initialize Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start MariaDB only
|
||||||
|
cd /volume1/lcbp3/blue
|
||||||
|
docker-compose up -d mariadb
|
||||||
|
|
||||||
|
# Wait for MariaDB to be ready
|
||||||
|
docker exec lcbp3-mariadb mysqladmin ping -h localhost
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
docker-compose up -d backend
|
||||||
|
docker exec lcbp3-blue-backend npm run migration:run
|
||||||
|
|
||||||
|
# Seed initial data (if needed)
|
||||||
|
docker exec lcbp3-blue-backend npm run seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Start Blue Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /volume1/lcbp3/blue
|
||||||
|
|
||||||
|
# Start all services
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Wait for health checks
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
# Test health endpoint
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Start NGINX Proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /volume1/lcbp3/nginx-proxy
|
||||||
|
|
||||||
|
# Create networks (if not exist)
|
||||||
|
docker network create lcbp3-blue-network
|
||||||
|
docker network create lcbp3-green-network
|
||||||
|
|
||||||
|
# Start NGINX
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Test NGINX configuration
|
||||||
|
docker exec lcbp3-nginx nginx -t
|
||||||
|
|
||||||
|
# Check NGINX logs
|
||||||
|
docker logs lcbp3-nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Set Current Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mark blue as current
|
||||||
|
echo "blue" > /volume1/lcbp3/current
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Verify Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test HTTPS endpoint
|
||||||
|
curl -k https://lcbp3-dms.example.com/health
|
||||||
|
|
||||||
|
# Test API
|
||||||
|
curl -k https://lcbp3-dms.example.com/api/health
|
||||||
|
|
||||||
|
# Check all containers
|
||||||
|
docker ps --filter "name=lcbp3"
|
||||||
|
|
||||||
|
# Check logs for errors
|
||||||
|
docker-compose -f /volume1/lcbp3/blue/docker-compose.yml logs --tail=100
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Blue-Green Deployment Process
|
||||||
|
|
||||||
|
### Deployment Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# File: /volume1/lcbp3/scripts/deploy.sh
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
LCBP3_DIR="/volume1/lcbp3"
|
||||||
|
CURRENT=$(cat $LCBP3_DIR/current)
|
||||||
|
TARGET=$([[ "$CURRENT" == "blue" ]] && echo "green" || echo "blue")
|
||||||
|
|
||||||
|
echo "========================================="
|
||||||
|
echo "LCBP3-DMS Blue-Green Deployment"
|
||||||
|
echo "========================================="
|
||||||
|
echo "Current environment: $CURRENT"
|
||||||
|
echo "Target environment: $TARGET"
|
||||||
|
echo "========================================="
|
||||||
|
|
||||||
|
# Step 1: Backup database
|
||||||
|
echo "[1/9] Creating database backup..."
|
||||||
|
BACKUP_FILE="$LCBP3_DIR/shared/backups/db-backup-$(date +%Y%m%d-%H%M%S).sql"
|
||||||
|
docker exec lcbp3-mariadb mysqldump -u root -p${DB_PASSWORD} lcbp3_dms > $BACKUP_FILE
|
||||||
|
gzip $BACKUP_FILE
|
||||||
|
echo "✓ Backup created: $BACKUP_FILE.gz"
|
||||||
|
|
||||||
|
# Step 2: Pull latest images
|
||||||
|
echo "[2/9] Pulling latest Docker images..."
|
||||||
|
cd $LCBP3_DIR/$TARGET
|
||||||
|
docker-compose pull
|
||||||
|
echo "✓ Images pulled"
|
||||||
|
|
||||||
|
# Step 3: Update configuration
|
||||||
|
echo "[3/9] Updating configuration..."
|
||||||
|
# Copy .env if changed
|
||||||
|
if [ -f "$LCBP3_DIR/.env.production.new" ]; then
|
||||||
|
cp $LCBP3_DIR/.env.production.new $LCBP3_DIR/$TARGET/.env.production
|
||||||
|
echo "✓ Configuration updated"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 4: Start target environment
|
||||||
|
echo "[4/9] Starting $TARGET environment..."
|
||||||
|
docker-compose up -d
|
||||||
|
echo "✓ $TARGET environment started"
|
||||||
|
|
||||||
|
# Step 5: Wait for services to be ready
|
||||||
|
echo "[5/9] Waiting for services to be healthy..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Check backend health
|
||||||
|
for i in {1..30}; do
|
||||||
|
if docker exec lcbp3-${TARGET}-backend curl -f http://localhost:3000/health > /dev/null 2>&1; then
|
||||||
|
echo "✓ Backend is healthy"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $i -eq 30 ]; then
|
||||||
|
echo "✗ Backend health check failed!"
|
||||||
|
docker-compose logs backend
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Step 6: Run database migrations
|
||||||
|
echo "[6/9] Running database migrations..."
|
||||||
|
docker exec lcbp3-${TARGET}-backend npm run migration:run
|
||||||
|
echo "✓ Migrations completed"
|
||||||
|
|
||||||
|
# Step 7: Switch NGINX to target environment
|
||||||
|
echo "[7/9] Switching NGINX to $TARGET..."
|
||||||
|
sed -i "s/lcbp3-${CURRENT}-backend/lcbp3-${TARGET}-backend/g" $LCBP3_DIR/nginx-proxy/nginx.conf
|
||||||
|
sed -i "s/lcbp3-${CURRENT}-frontend/lcbp3-${TARGET}-frontend/g" $LCBP3_DIR/nginx-proxy/nginx.conf
|
||||||
|
docker exec lcbp3-nginx nginx -t
|
||||||
|
docker exec lcbp3-nginx nginx -s reload
|
||||||
|
echo "✓ NGINX switched to $TARGET"
|
||||||
|
|
||||||
|
# Step 8: Verify new environment
|
||||||
|
echo "[8/9] Verifying new environment..."
|
||||||
|
sleep 5
|
||||||
|
if curl -f -k https://lcbp3-dms.example.com/health > /dev/null 2>&1; then
|
||||||
|
echo "✓ New environment is responding"
|
||||||
|
else
|
||||||
|
echo "✗ New environment verification failed!"
|
||||||
|
echo "Rolling back..."
|
||||||
|
./rollback.sh
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 9: Stop old environment
|
||||||
|
echo "[9/9] Stopping $CURRENT environment..."
|
||||||
|
cd $LCBP3_DIR/$CURRENT
|
||||||
|
docker-compose down
|
||||||
|
echo "✓ $CURRENT environment stopped"
|
||||||
|
|
||||||
|
# Update current pointer
|
||||||
|
echo "$TARGET" > $LCBP3_DIR/current
|
||||||
|
|
||||||
|
echo "========================================="
|
||||||
|
echo "✓ Deployment completed successfully!"
|
||||||
|
echo "Active environment: $TARGET"
|
||||||
|
echo "========================================="
|
||||||
|
|
||||||
|
# Send notification (optional)
|
||||||
|
# /scripts/send-notification.sh "Deployment completed: $TARGET is now active"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# File: /volume1/lcbp3/scripts/rollback.sh
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LCBP3_DIR="/volume1/lcbp3"
|
||||||
|
CURRENT=$(cat $LCBP3_DIR/current)
|
||||||
|
PREVIOUS=$([[ "$CURRENT" == "blue" ]] && echo "green" || echo "blue")
|
||||||
|
|
||||||
|
echo "========================================="
|
||||||
|
echo "LCBP3-DMS Rollback"
|
||||||
|
echo "========================================="
|
||||||
|
echo "Current: $CURRENT"
|
||||||
|
echo "Rolling back to: $PREVIOUS"
|
||||||
|
echo "========================================="
|
||||||
|
|
||||||
|
# Switch NGINX back
|
||||||
|
echo "[1/3] Switching NGINX to $PREVIOUS..."
|
||||||
|
sed -i "s/lcbp3-${CURRENT}-backend/lcbp3-${PREVIOUS}-backend/g" $LCBP3_DIR/nginx-proxy/nginx.conf
|
||||||
|
sed -i "s/lcbp3-${CURRENT}-frontend/lcbp3-${PREVIOUS}-frontend/g" $LCBP3_DIR/nginx-proxy/nginx.conf
|
||||||
|
docker exec lcbp3-nginx nginx -s reload
|
||||||
|
echo "✓ NGINX switched"
|
||||||
|
|
||||||
|
# Start previous environment if stopped
|
||||||
|
echo "[2/3] Ensuring $PREVIOUS environment is running..."
|
||||||
|
cd $LCBP3_DIR/$PREVIOUS
|
||||||
|
docker-compose up -d
|
||||||
|
sleep 10
|
||||||
|
echo "✓ $PREVIOUS environment is running"
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
echo "[3/3] Verifying rollback..."
|
||||||
|
if curl -f -k https://lcbp3-dms.example.com/health > /dev/null 2>&1; then
|
||||||
|
echo "✓ Rollback successful"
|
||||||
|
echo "$PREVIOUS" > $LCBP3_DIR/current
|
||||||
|
else
|
||||||
|
echo "✗ Rollback verification failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "========================================="
|
||||||
|
echo "✓ Rollback completed"
|
||||||
|
echo "Active environment: $PREVIOUS"
|
||||||
|
echo "========================================="
|
||||||
|
```
|
||||||
|
|
||||||
|
### Make Scripts Executable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x /volume1/lcbp3/scripts/deploy.sh
|
||||||
|
chmod +x /volume1/lcbp3/scripts/rollback.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Deployment Checklist
|
||||||
|
|
||||||
|
### Pre-Deployment
|
||||||
|
|
||||||
|
- [ ] Backup current database
|
||||||
|
- [ ] Tag Docker images with version
|
||||||
|
- [ ] Update `.env.production` if needed
|
||||||
|
- [ ] Review migration scripts
|
||||||
|
- [ ] Notify stakeholders of deployment window
|
||||||
|
- [ ] Verify SSL certificate validity (> 30 days)
|
||||||
|
- [ ] Check disk space (> 20% free)
|
||||||
|
- [ ] Review recent error logs
|
||||||
|
|
||||||
|
### During Deployment
|
||||||
|
|
||||||
|
- [ ] Pull latest Docker images
|
||||||
|
- [ ] Start target environment (blue/green)
|
||||||
|
- [ ] Run database migrations
|
||||||
|
- [ ] Verify health checks pass
|
||||||
|
- [ ] Switch NGINX proxy
|
||||||
|
- [ ] Verify application responds correctly
|
||||||
|
- [ ] Check for errors in logs
|
||||||
|
- [ ] Monitor performance metrics
|
||||||
|
|
||||||
|
### Post-Deployment
|
||||||
|
|
||||||
|
- [ ] Monitor logs for 30 minutes
|
||||||
|
- [ ] Check performance metrics
|
||||||
|
- [ ] Verify all features working
|
||||||
|
- [ ] Test critical user flows
|
||||||
|
- [ ] Stop old environment
|
||||||
|
- [ ] Update deployment log
|
||||||
|
- [ ] Notify stakeholders of completion
|
||||||
|
- [ ] Archive old Docker images
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### 1. Container Won't Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker logs lcbp3-blue-backend
|
||||||
|
|
||||||
|
# Check resource usage
|
||||||
|
docker stats
|
||||||
|
|
||||||
|
# Restart container
|
||||||
|
docker restart lcbp3-blue-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Database Connection Failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check MariaDB is running
|
||||||
|
docker ps | grep mariadb
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
docker exec lcbp3-mariadb mysql -u lcbp3_user -p -e "SELECT 1"
|
||||||
|
|
||||||
|
# Check environment variables
|
||||||
|
docker exec lcbp3-blue-backend env | grep DB_
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. NGINX 502 Bad Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check backend is running
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
|
||||||
|
# Check NGINX configuration
|
||||||
|
docker exec lcbp3-nginx nginx -t
|
||||||
|
|
||||||
|
# Check NGINX logs
|
||||||
|
docker logs lcbp3-nginx
|
||||||
|
|
||||||
|
# Reload NGINX
|
||||||
|
docker exec lcbp3-nginx nginx -s reload
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Migration Failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check migration status
|
||||||
|
docker exec lcbp3-blue-backend npm run migration:show
|
||||||
|
|
||||||
|
# Revert last migration
|
||||||
|
docker exec lcbp3-blue-backend npm run migration:revert
|
||||||
|
|
||||||
|
# Re-run migrations
|
||||||
|
docker exec lcbp3-blue-backend npm run migration:run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend health
|
||||||
|
curl https://lcbp3-dms.example.com/health
|
||||||
|
|
||||||
|
# Database health
|
||||||
|
docker exec lcbp3-mariadb mysqladmin ping
|
||||||
|
|
||||||
|
# Redis health
|
||||||
|
docker exec lcbp3-redis redis-cli ping
|
||||||
|
|
||||||
|
# All containers status
|
||||||
|
docker ps --filter "name=lcbp3" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container resource usage
|
||||||
|
docker stats --no-stream
|
||||||
|
|
||||||
|
# Disk usage
|
||||||
|
df -h /volume1/lcbp3
|
||||||
|
|
||||||
|
# Database size
|
||||||
|
docker exec lcbp3-mariadb mysql -u root -p -e "
|
||||||
|
SELECT table_schema AS 'Database',
|
||||||
|
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'lcbp3_dms'
|
||||||
|
GROUP BY table_schema;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security Best Practices
|
||||||
|
|
||||||
|
1. **Change Default Passwords:** Update all passwords in `.env.production`
|
||||||
|
2. **SSL/TLS:** Always use HTTPS in production
|
||||||
|
3. **Firewall:** Only expose ports 80, 443, and 22 (SSH)
|
||||||
|
4. **Regular Updates:** Keep Docker images updated
|
||||||
|
5. **Backup Encryption:** Encrypt database backups
|
||||||
|
6. **Access Control:** Limit SSH access to specific IPs
|
||||||
|
7. **Secrets Management:** Never commit `.env` files to Git
|
||||||
|
8. **Log Monitoring:** Review logs daily for suspicious activity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Related Documentation
|
||||||
|
|
||||||
|
- [Environment Setup Guide](./environment-setup.md)
|
||||||
|
- [Backup & Recovery](./backup-recovery.md)
|
||||||
|
- [Monitoring & Alerting](./monitoring-alerting.md)
|
||||||
|
- [Maintenance Procedures](./maintenance-procedures.md)
|
||||||
|
- [ADR-015: Deployment Infrastructure](../05-decisions/ADR-015-deployment-infrastructure.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version:** 1.5.1
|
||||||
|
**Last Updated:** 2025-12-02
|
||||||
|
**Next Review:** 2026-06-01
|
||||||
684
specs/04-operations/document-numbering-operations.md
Normal file
684
specs/04-operations/document-numbering-operations.md
Normal file
@@ -0,0 +1,684 @@
|
|||||||
|
# Document Numbering Operations Guide
|
||||||
|
|
||||||
|
---
|
||||||
|
title: 'Operations Guide: Document Numbering System'
|
||||||
|
version: 1.6.0
|
||||||
|
status: draft
|
||||||
|
owner: Operations Team
|
||||||
|
last_updated: 2025-12-02
|
||||||
|
related:
|
||||||
|
- specs/01-requirements/03.11-document-numbering.md
|
||||||
|
- specs/03-implementation/document-numbering.md
|
||||||
|
- specs/04-operations/monitoring-alerting.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
เอกสารนี้อธิบาย operations procedures, monitoring, และ troubleshooting สำหรับระบบ Document Numbering
|
||||||
|
|
||||||
|
## 1. Performance Requirements
|
||||||
|
|
||||||
|
### 1.1. Response Time Targets
|
||||||
|
|
||||||
|
| Metric | Target | Measurement |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| 95th percentile | ≤ 2 วินาที | ตั้งแต่ request ถึง response |
|
||||||
|
| 99th percentile | ≤ 5 วินาที | ตั้งแต่ request ถึง response |
|
||||||
|
| Normal operation | ≤ 500ms | ไม่มี retry |
|
||||||
|
|
||||||
|
### 1.2. Throughput Targets
|
||||||
|
|
||||||
|
| Load Level | Target | Notes |
|
||||||
|
|------------|--------|-------|
|
||||||
|
| Normal load | ≥ 50 req/s | ใช้งานปกติ |
|
||||||
|
| Peak load | ≥ 100 req/s | ช่วงเร่งงาน |
|
||||||
|
| Burst capacity | ≥ 200 req/s | Short duration (< 1 min) |
|
||||||
|
|
||||||
|
### 1.3. Availability SLA
|
||||||
|
|
||||||
|
- **Uptime**: ≥ 99.5% (excluding planned maintenance)
|
||||||
|
- **Maximum downtime**: ≤ 3.6 ชั่วโมง/เดือน (~ 8.6 นาที/วัน)
|
||||||
|
- **Recovery Time Objective (RTO)**: ≤ 30 นาที
|
||||||
|
- **Recovery Point Objective (RPO)**: ≤ 5 นาที
|
||||||
|
|
||||||
|
## 2. Infrastructure Setup
|
||||||
|
|
||||||
|
### 2.1. Database Configuration
|
||||||
|
|
||||||
|
#### MariaDB Connection Pool
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ormconfig.ts
|
||||||
|
{
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: parseInt(process.env.DB_PORT),
|
||||||
|
username: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_DATABASE,
|
||||||
|
extra: {
|
||||||
|
connectionLimit: 20, // Pool size
|
||||||
|
queueLimit: 0, // Unlimited queue
|
||||||
|
acquireTimeout: 10000, // 10s timeout
|
||||||
|
retryAttempts: 3,
|
||||||
|
retryDelay: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### High Availability Setup
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
services:
|
||||||
|
mariadb-master:
|
||||||
|
image: mariadb:10.11
|
||||||
|
environment:
|
||||||
|
MYSQL_REPLICATION_MODE: master
|
||||||
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- mariadb-master-data:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
mariadb-replica:
|
||||||
|
image: mariadb:10.11
|
||||||
|
environment:
|
||||||
|
MYSQL_REPLICATION_MODE: slave
|
||||||
|
MYSQL_MASTER_HOST: mariadb-master
|
||||||
|
MYSQL_MASTER_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- mariadb-replica-data:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2. Redis Configuration
|
||||||
|
|
||||||
|
#### Redis Sentinel for High Availability
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
services:
|
||||||
|
redis-master:
|
||||||
|
image: redis:7-alpine
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- redis-master-data:/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
redis-replica:
|
||||||
|
image: redis:7-alpine
|
||||||
|
command: redis-server --replicaof redis-master 6379 --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- redis-replica-data:/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
redis-sentinel:
|
||||||
|
image: redis:7-alpine
|
||||||
|
command: >
|
||||||
|
redis-sentinel /etc/redis/sentinel.conf
|
||||||
|
--sentinel monitor mymaster redis-master 6379 2
|
||||||
|
--sentinel down-after-milliseconds mymaster 5000
|
||||||
|
--sentinel failover-timeout mymaster 10000
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Redis Connection Pool
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// redis.config.ts
|
||||||
|
import IORedis from 'ioredis';
|
||||||
|
|
||||||
|
export const redisConfig = {
|
||||||
|
host: process.env.REDIS_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.REDIS_PORT) || 6379,
|
||||||
|
password: process.env.REDIS_PASSWORD,
|
||||||
|
maxRetriesPerRequest: 3,
|
||||||
|
enableReadyCheck: true,
|
||||||
|
lazyConnect: false,
|
||||||
|
poolSize: 10,
|
||||||
|
retryStrategy: (times: number) => {
|
||||||
|
if (times > 3) {
|
||||||
|
return null; // Stop retry
|
||||||
|
}
|
||||||
|
return Math.min(times * 100, 3000);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3. Load Balancing
|
||||||
|
|
||||||
|
#### Nginx Configuration
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# nginx.conf
|
||||||
|
upstream backend {
|
||||||
|
least_conn; # Least connections algorithm
|
||||||
|
server backend-1:3000 max_fails=3 fail_timeout=30s weight=1;
|
||||||
|
server backend-2:3000 max_fails=3 fail_timeout=30s weight=1;
|
||||||
|
server backend-3:3000 max_fails=3 fail_timeout=30s weight=1;
|
||||||
|
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name api.lcbp3.local;
|
||||||
|
|
||||||
|
location /api/v1/document-numbering/ {
|
||||||
|
proxy_pass http://backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
proxy_next_upstream error timeout;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_send_timeout 30s;
|
||||||
|
proxy_read_timeout 30s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Compose Scaling
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
image: lcbp3-backend:latest
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1.0'
|
||||||
|
memory: 1G
|
||||||
|
reservations:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 512M
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
delay: 5s
|
||||||
|
max_attempts: 3
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
DB_POOL_SIZE: 20
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Monitoring & Metrics
|
||||||
|
|
||||||
|
### 3.1. Prometheus Metrics
|
||||||
|
|
||||||
|
#### Key Metrics to Collect
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// metrics.service.ts
|
||||||
|
import { Counter, Histogram, Gauge } from 'prom-client';
|
||||||
|
|
||||||
|
// Lock acquisition metrics
|
||||||
|
export const 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],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lockAcquisitionFailures = new Counter({
|
||||||
|
name: 'docnum_lock_acquisition_failures_total',
|
||||||
|
help: 'Total number of lock acquisition failures',
|
||||||
|
labelNames: ['project', 'type', 'reason'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generation metrics
|
||||||
|
export const 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],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const 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
|
||||||
|
export const redisConnectionStatus = new Gauge({
|
||||||
|
name: 'docnum_redis_connection_status',
|
||||||
|
help: 'Redis connection status (1=up, 0=down)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dbConnectionPoolUsage = new Gauge({
|
||||||
|
name: 'docnum_db_connection_pool_usage',
|
||||||
|
help: 'Database connection pool usage percentage',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2. Prometheus Alert Rules
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 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. Performance degraded by 30-50%."
|
||||||
|
runbook_url: "https://wiki.lcbp3/runbooks/redis-unavailable"
|
||||||
|
|
||||||
|
# CRITICAL: High lock failure rate
|
||||||
|
- alert: HighLockFailureRate
|
||||||
|
expr: |
|
||||||
|
rate(docnum_lock_acquisition_failures_total[5m]) > 0.1
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
component: document-numbering
|
||||||
|
annotations:
|
||||||
|
summary: "Lock acquisition failure rate > 10%"
|
||||||
|
description: "Check Redis and database performance immediately"
|
||||||
|
runbook_url: "https://wiki.lcbp3/runbooks/high-lock-failure"
|
||||||
|
|
||||||
|
# WARNING: Elevated lock failure rate
|
||||||
|
- alert: ElevatedLockFailureRate
|
||||||
|
expr: |
|
||||||
|
rate(docnum_lock_acquisition_failures_total[5m]) > 0.05
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
component: document-numbering
|
||||||
|
annotations:
|
||||||
|
summary: "Lock acquisition failure rate > 5%"
|
||||||
|
description: "Monitor closely. May escalate to critical soon."
|
||||||
|
|
||||||
|
# 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
|
||||||
|
component: document-numbering
|
||||||
|
annotations:
|
||||||
|
summary: "P95 lock acquisition time > 1 second"
|
||||||
|
description: "Lock acquisition is slower than expected. Check Redis latency."
|
||||||
|
|
||||||
|
# WARNING: High retry count
|
||||||
|
- alert: HighRetryCount
|
||||||
|
expr: |
|
||||||
|
sum by (project) (
|
||||||
|
rate(docnum_retry_count_sum[1h])
|
||||||
|
) > 100
|
||||||
|
for: 1h
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
component: document-numbering
|
||||||
|
annotations:
|
||||||
|
summary: "Retry count > 100 per hour in project {{ $labels.project }}"
|
||||||
|
description: "High contention detected. Consider scaling."
|
||||||
|
|
||||||
|
# WARNING: Slow generation
|
||||||
|
- alert: SlowDocumentNumberGeneration
|
||||||
|
expr: |
|
||||||
|
histogram_quantile(0.95,
|
||||||
|
rate(docnum_generation_duration_ms_bucket[5m])
|
||||||
|
) > 2000
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
component: document-numbering
|
||||||
|
annotations:
|
||||||
|
summary: "P95 generation time > 2 seconds"
|
||||||
|
description: "Document number generation is slower than SLA target"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3. AlertManager Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# alertmanager/config.yml
|
||||||
|
global:
|
||||||
|
resolve_timeout: 5m
|
||||||
|
slack_api_url: ${SLACK_WEBHOOK_URL}
|
||||||
|
|
||||||
|
route:
|
||||||
|
group_by: ['alertname', 'severity', 'project']
|
||||||
|
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
|
||||||
|
|
||||||
|
- match:
|
||||||
|
severity: critical
|
||||||
|
receiver: 'slack-critical'
|
||||||
|
continue: false
|
||||||
|
|
||||||
|
# WARNING alerts → Slack only
|
||||||
|
- match:
|
||||||
|
severity: warning
|
||||||
|
receiver: 'slack-warnings'
|
||||||
|
|
||||||
|
receivers:
|
||||||
|
- name: 'pagerduty-critical'
|
||||||
|
pagerduty_configs:
|
||||||
|
- service_key: ${PAGERDUTY_SERVICE_KEY}
|
||||||
|
description: '{{ .GroupLabels.alertname }}: {{ .CommonAnnotations.summary }}'
|
||||||
|
details:
|
||||||
|
firing: '{{ .Alerts.Firing | len }}'
|
||||||
|
resolved: '{{ .Alerts.Resolved | len }}'
|
||||||
|
runbook: '{{ .CommonAnnotations.runbook_url }}'
|
||||||
|
|
||||||
|
- name: 'slack-critical'
|
||||||
|
slack_configs:
|
||||||
|
- channel: '#lcbp3-critical-alerts'
|
||||||
|
title: '🚨 CRITICAL: {{ .GroupLabels.alertname }}'
|
||||||
|
text: |
|
||||||
|
*Summary:* {{ .CommonAnnotations.summary }}
|
||||||
|
*Description:* {{ .CommonAnnotations.description }}
|
||||||
|
*Runbook:* {{ .CommonAnnotations.runbook_url }}
|
||||||
|
color: 'danger'
|
||||||
|
|
||||||
|
- name: 'slack-warnings'
|
||||||
|
slack_configs:
|
||||||
|
- channel: '#lcbp3-alerts'
|
||||||
|
title: '⚠️ WARNING: {{ .GroupLabels.alertname }}'
|
||||||
|
text: '{{ .CommonAnnotations.description }}'
|
||||||
|
color: 'warning'
|
||||||
|
|
||||||
|
- name: 'ops-team'
|
||||||
|
email_configs:
|
||||||
|
- to: 'ops@example.com'
|
||||||
|
subject: '[LCBP3] {{ .GroupLabels.alertname }}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4. Grafana Dashboard
|
||||||
|
|
||||||
|
Dashboard panels ที่สำคัญ:
|
||||||
|
|
||||||
|
1. **Lock Acquisition Success Rate** (Gauge)
|
||||||
|
- Query: `1 - (rate(docnum_lock_acquisition_failures_total[5m]) / rate(docnum_lock_acquisition_total[5m]))`
|
||||||
|
- Alert threshold: < 95%
|
||||||
|
|
||||||
|
2. **Lock Acquisition Time Percentiles** (Graph)
|
||||||
|
- P50: `histogram_quantile(0.50, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))`
|
||||||
|
- P95: `histogram_quantile(0.95, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))`
|
||||||
|
- P99: `histogram_quantile(0.99, rate(docnum_lock_acquisition_duration_ms_bucket[5m]))`
|
||||||
|
|
||||||
|
3. **Generation Rate** (Stat)
|
||||||
|
- Query: `sum(rate(docnum_generation_duration_ms_count[1m])) * 60`
|
||||||
|
- Unit: documents/minute
|
||||||
|
|
||||||
|
4. **Error Rate by Type** (Graph)
|
||||||
|
- Query: `sum by (reason) (rate(docnum_lock_acquisition_failures_total[5m]))`
|
||||||
|
|
||||||
|
5. **Redis Connection Status** (Stat)
|
||||||
|
- Query: `docnum_redis_connection_status`
|
||||||
|
- Thresholds: 0 = red, 1 = green
|
||||||
|
|
||||||
|
6. **DB Connection Pool Usage** (Gauge)
|
||||||
|
- Query: `docnum_db_connection_pool_usage`
|
||||||
|
- Alert threshold: > 80%
|
||||||
|
|
||||||
|
## 4. Troubleshooting Runbooks
|
||||||
|
|
||||||
|
### 4.1. Scenario: Redis Unavailable
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- Alert: `RedisUnavailable`
|
||||||
|
- System falls back to DB-only locking
|
||||||
|
- Performance degraded 30-50%
|
||||||
|
|
||||||
|
**Action Steps:**
|
||||||
|
|
||||||
|
1. **Check Redis status:**
|
||||||
|
```bash
|
||||||
|
docker exec lcbp3-redis redis-cli ping
|
||||||
|
# Expected: PONG
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check Redis logs:**
|
||||||
|
```bash
|
||||||
|
docker logs lcbp3-redis --tail=100
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Restart Redis (if needed):**
|
||||||
|
```bash
|
||||||
|
docker restart lcbp3-redis
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify failover (if using Sentinel):**
|
||||||
|
```bash
|
||||||
|
docker exec lcbp3-redis-sentinel redis-cli -p 26379 SENTINEL masters
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Monitor recovery:**
|
||||||
|
- Check metric: `docnum_redis_connection_status` returns to 1
|
||||||
|
- Check performance: P95 latency returns to normal (< 500ms)
|
||||||
|
|
||||||
|
### 4.2. Scenario: High Lock Failure Rate
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- Alert: `HighLockFailureRate` (> 10%)
|
||||||
|
- Users report "ระบบกำลังยุ่ง" errors
|
||||||
|
|
||||||
|
**Action Steps:**
|
||||||
|
|
||||||
|
1. **Check concurrent load:**
|
||||||
|
```bash
|
||||||
|
# Check current request rate
|
||||||
|
curl http://prometheus:9090/api/v1/query?query=rate(docnum_generation_duration_ms_count[1m])
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check database connections:**
|
||||||
|
```sql
|
||||||
|
SHOW PROCESSLIST;
|
||||||
|
-- Look for waiting/locked queries
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check Redis memory:**
|
||||||
|
```bash
|
||||||
|
docker exec lcbp3-redis redis-cli INFO memory
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Scale up if needed:**
|
||||||
|
```bash
|
||||||
|
# Increase backend replicas
|
||||||
|
docker-compose up -d --scale backend=5
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Check for deadlocks:**
|
||||||
|
```sql
|
||||||
|
SHOW ENGINE INNODB STATUS;
|
||||||
|
-- Look for LATEST DETECTED DEADLOCK section
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3. Scenario: Slow Performance
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- Alert: `SlowDocumentNumberGeneration`
|
||||||
|
- P95 > 2 seconds
|
||||||
|
|
||||||
|
**Action Steps:**
|
||||||
|
|
||||||
|
1. **Check database query performance:**
|
||||||
|
```sql
|
||||||
|
SELECT * FROM document_number_counters USE INDEX (idx_counter_lookup)
|
||||||
|
WHERE project_id = 2 AND correspondence_type_id = 6 AND current_year = 2025;
|
||||||
|
|
||||||
|
-- Check execution plan
|
||||||
|
EXPLAIN SELECT ...;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check for missing indexes:**
|
||||||
|
```sql
|
||||||
|
SHOW INDEX FROM document_number_counters;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check Redis latency:**
|
||||||
|
```bash
|
||||||
|
docker exec lcbp3-redis redis-cli --latency
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Check network latency:**
|
||||||
|
```bash
|
||||||
|
ping mariadb-master
|
||||||
|
ping redis-master
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Review slow query log:**
|
||||||
|
```bash
|
||||||
|
docker exec lcbp3-mariadb-master cat /var/log/mysql/slow.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4. Scenario: Version Conflicts
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- High retry count
|
||||||
|
- Users report "เลขที่เอกสารถูกเปลี่ยน" errors
|
||||||
|
|
||||||
|
**Action Steps:**
|
||||||
|
|
||||||
|
1. **Check concurrent requests to same counter:**
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
project_id,
|
||||||
|
correspondence_type_id,
|
||||||
|
COUNT(*) as concurrent_requests
|
||||||
|
FROM document_number_audit
|
||||||
|
WHERE created_at > NOW() - INTERVAL 5 MINUTE
|
||||||
|
GROUP BY project_id, correspondence_type_id
|
||||||
|
HAVING COUNT(*) > 10
|
||||||
|
ORDER BY concurrent_requests DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Investigate specific counter:**
|
||||||
|
```sql
|
||||||
|
SELECT * FROM document_number_counters
|
||||||
|
WHERE project_id = X AND correspondence_type_id = Y;
|
||||||
|
|
||||||
|
-- Check audit trail
|
||||||
|
SELECT * FROM document_number_audit
|
||||||
|
WHERE counter_key LIKE '%project_id:X%'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check for application bugs:**
|
||||||
|
- Review error logs for stack traces
|
||||||
|
- Check if retry logic is working correctly
|
||||||
|
|
||||||
|
4. **Temporary mitigation:**
|
||||||
|
- Increase retry count in application config
|
||||||
|
- Consider manual counter adjustment (last resort)
|
||||||
|
|
||||||
|
## 5. Maintenance Procedures
|
||||||
|
|
||||||
|
### 5.1. Counter Reset (Manual)
|
||||||
|
|
||||||
|
**Requires:** SUPER_ADMIN role + 2-person approval
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. **Request approval via API:**
|
||||||
|
```bash
|
||||||
|
POST /api/v1/document-numbering/configs/{configId}/reset-counter
|
||||||
|
{
|
||||||
|
"reason": "เหตุผลที่ชัดเจน อย่างน้อย 20 ตัวอักษร",
|
||||||
|
"approver_1": "user_id",
|
||||||
|
"approver_2": "user_id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify in audit log:**
|
||||||
|
```sql
|
||||||
|
SELECT * FROM document_number_config_history
|
||||||
|
WHERE config_id = X
|
||||||
|
ORDER BY changed_at DESC
|
||||||
|
LIMIT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2. Template Update
|
||||||
|
|
||||||
|
**Best Practices:**
|
||||||
|
|
||||||
|
1. Always test template in staging first
|
||||||
|
2. Preview generated numbers before applying
|
||||||
|
3. Document reason for change
|
||||||
|
4. Template changes do NOT affect existing documents
|
||||||
|
|
||||||
|
**API Call:**
|
||||||
|
```bash
|
||||||
|
PUT /api/v1/document-numbering/configs/{configId}
|
||||||
|
{
|
||||||
|
"template": "{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}",
|
||||||
|
"change_reason": "เหตุผลในการเปลี่ยนแปลง"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3. Database Maintenance
|
||||||
|
|
||||||
|
**Weekly Tasks:**
|
||||||
|
- Check slow query log
|
||||||
|
- Optimize tables if needed:
|
||||||
|
```sql
|
||||||
|
OPTIMIZE TABLE document_number_counters;
|
||||||
|
OPTIMIZE TABLE document_number_audit;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Monthly Tasks:**
|
||||||
|
- Review and archive old audit logs (> 2 years)
|
||||||
|
- Check index usage:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM sys.schema_unused_indexes
|
||||||
|
WHERE object_schema = 'lcbp3_db';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Backup & Recovery
|
||||||
|
|
||||||
|
### 6.1. Backup Strategy
|
||||||
|
|
||||||
|
**Database:**
|
||||||
|
- Full backup: Daily at 02:00 AM
|
||||||
|
- Incremental backup: Every 4 hours
|
||||||
|
- Retention: 30 days
|
||||||
|
|
||||||
|
**Redis:**
|
||||||
|
- AOF (Append-Only File) enabled
|
||||||
|
- Snapshot every 1 hour
|
||||||
|
- Retention: 7 days
|
||||||
|
|
||||||
|
### 6.2. Recovery Procedures
|
||||||
|
|
||||||
|
See: [Backup & Recovery Guide](file:///e:/np-dms/lcbp3/specs/04-operations/backup-recovery.md)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Requirements](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md)
|
||||||
|
- [Implementation Guide](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md)
|
||||||
|
- [Monitoring & Alerting](file:///e:/np-dms/lcbp3/specs/04-operations/monitoring-alerting.md)
|
||||||
|
- [Incident Response](file:///e:/np-dms/lcbp3/specs/04-operations/incident-response.md)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Environment Setup & Configuration
|
# Environment Setup & Configuration
|
||||||
|
|
||||||
**Project:** LCBP3-DMS
|
**Project:** LCBP3-DMS
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -458,6 +458,6 @@ docker exec lcbp3-backend env | grep NODE_ENV
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Review:** 2025-12-01
|
**Last Review:** 2025-12-01
|
||||||
**Next Review:** 2026-03-01
|
**Next Review:** 2026-03-01
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Incident Response Procedures
|
# Incident Response Procedures
|
||||||
|
|
||||||
**Project:** LCBP3-DMS
|
**Project:** LCBP3-DMS
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -478,6 +478,6 @@ Database connection pool was exhausted due to slow queries not releasing connect
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Review:** 2025-12-01
|
**Last Review:** 2025-12-01
|
||||||
**Next Review:** 2026-03-01
|
**Next Review:** 2026-03-01
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Maintenance Procedures
|
# Maintenance Procedures
|
||||||
|
|
||||||
**Project:** LCBP3-DMS
|
**Project:** LCBP3-DMS
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -496,6 +496,6 @@ echo "Security maintenance completed: $(date)"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Review:** 2025-12-01
|
**Last Review:** 2025-12-01
|
||||||
**Next Review:** 2026-03-01
|
**Next Review:** 2026-03-01
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Monitoring & Alerting
|
# Monitoring & Alerting
|
||||||
|
|
||||||
**Project:** LCBP3-DMS
|
**Project:** LCBP3-DMS
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -438,6 +438,6 @@ ab -n 1000 -c 10 \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Review:** 2025-12-01
|
**Last Review:** 2025-12-01
|
||||||
**Next Review:** 2026-03-01
|
**Next Review:** 2026-03-01
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Security Operations
|
# Security Operations
|
||||||
|
|
||||||
**Project:** LCBP3-DMS
|
**Project:** LCBP3-DMS
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -439,6 +439,6 @@ echo "Account compromise response completed for User ID: $USER_ID"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Review:** 2025-12-01
|
**Last Review:** 2025-12-01
|
||||||
**Next Review:** 2026-03-01
|
**Next Review:** 2026-03-01
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ CREATE TABLE document_number_audit (
|
|||||||
#### 1. Correspondence (หนังสือราชการ)
|
#### 1. Correspondence (หนังสือราชการ)
|
||||||
|
|
||||||
**Letter Type (TYPE = 03):**
|
**Letter Type (TYPE = 03):**
|
||||||
|
|
||||||
```
|
```
|
||||||
Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}
|
Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}
|
||||||
Example: คคง.-สคฉ.3-0985-2568
|
Example: คคง.-สคฉ.3-0985-2568
|
||||||
@@ -204,6 +205,7 @@ Counter Key: project_id + doc_type_id + sub_type_id + year
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Other Correspondence:**
|
**Other Correspondence:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}
|
Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}
|
||||||
Example: คคง.-สคฉ.3-STR-0001-2568
|
Example: คคง.-สคฉ.3-STR-0001-2568
|
||||||
@@ -213,6 +215,7 @@ Counter Key: project_id + doc_type_id + sub_type_id + year
|
|||||||
#### 2. Transmittal
|
#### 2. Transmittal
|
||||||
|
|
||||||
**To Owner (Special Format):**
|
**To Owner (Special Format):**
|
||||||
|
|
||||||
```
|
```
|
||||||
Template: {ORG}-{ORG}-{TYPE}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.}
|
Template: {ORG}-{ORG}-{TYPE}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.}
|
||||||
Example: คคง.-สคฉ.3-03-21-0117-2568
|
Example: คคง.-สคฉ.3-03-21-0117-2568
|
||||||
@@ -221,6 +224,7 @@ Note: recipient_type แยก counter จาก To Contractor
|
|||||||
```
|
```
|
||||||
|
|
||||||
**To Contractor/Others:**
|
**To Contractor/Others:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}
|
Template: {ORG}-{ORG}-{TYPE}-{SEQ:4}-{YEAR:B.E.}
|
||||||
Example: ผรม.2-คคง.-0117-2568
|
Example: ผรม.2-คคง.-0117-2568
|
||||||
@@ -228,6 +232,7 @@ Counter Key: project_id + doc_type_id + recipient_type('CONTRACTOR') + year
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Alternative Project-based:**
|
**Alternative Project-based:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Template: {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV}
|
Template: {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV}
|
||||||
Example: LCBP3-TR-STR-0001-A
|
Example: LCBP3-TR-STR-0001-A
|
||||||
@@ -586,6 +591,7 @@ sequenceDiagram
|
|||||||
**Trigger:** Redis connection error, Redis down
|
**Trigger:** Redis connection error, Redis down
|
||||||
|
|
||||||
**Fallback:**
|
**Fallback:**
|
||||||
|
|
||||||
- ใช้ Database-only locking (`SELECT ... FOR UPDATE`)
|
- ใช้ Database-only locking (`SELECT ... FOR UPDATE`)
|
||||||
- Log warning และแจ้ง ops team
|
- Log warning และแจ้ง ops team
|
||||||
- ระบบยังใช้งานได้แต่ performance ลดลง (slower)
|
- ระบบยังใช้งานได้แต่ performance ลดลง (slower)
|
||||||
@@ -595,6 +601,7 @@ sequenceDiagram
|
|||||||
**Trigger:** หลาย requests แย่งชิง lock พร้อมกัน
|
**Trigger:** หลาย requests แย่งชิง lock พร้อมกัน
|
||||||
|
|
||||||
**Retry Logic:**
|
**Retry Logic:**
|
||||||
|
|
||||||
- Retry 5 ครั้งด้วย exponential backoff: 1s, 2s, 4s, 8s, 16s (รวม ~31 วินาที)
|
- Retry 5 ครั้งด้วย exponential backoff: 1s, 2s, 4s, 8s, 16s (รวม ~31 วินาที)
|
||||||
- หลัง 5 ครั้ง: Return HTTP 503 "Service Temporarily Unavailable"
|
- หลัง 5 ครั้ง: Return HTTP 503 "Service Temporarily Unavailable"
|
||||||
- Frontend: แสดง "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง"
|
- Frontend: แสดง "ระบบกำลังยุ่ง กรุณาลองใหม่ภายหลัง"
|
||||||
@@ -604,6 +611,7 @@ sequenceDiagram
|
|||||||
**Trigger:** Optimistic lock version mismatch
|
**Trigger:** Optimistic lock version mismatch
|
||||||
|
|
||||||
**Retry Logic:**
|
**Retry Logic:**
|
||||||
|
|
||||||
- Retry 2 ครั้ง (reload counter + retry transaction)
|
- Retry 2 ครั้ง (reload counter + retry transaction)
|
||||||
- หลัง 2 ครั้ง: Return HTTP 409 Conflict
|
- หลัง 2 ครั้ง: Return HTTP 409 Conflict
|
||||||
- Frontend: แสดง "เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่"
|
- Frontend: แสดง "เลขที่เอกสารถูกเปลี่ยน กรุณาลองใหม่"
|
||||||
@@ -613,6 +621,7 @@ sequenceDiagram
|
|||||||
**Trigger:** Database connection timeout, connection pool exhausted
|
**Trigger:** Database connection timeout, connection pool exhausted
|
||||||
|
|
||||||
**Retry Logic:**
|
**Retry Logic:**
|
||||||
|
|
||||||
- Retry 3 ครั้งด้วย exponential backoff: 1s, 2s, 4s
|
- Retry 3 ครั้งด้วย exponential backoff: 1s, 2s, 4s
|
||||||
- หลัง 3 ครั้ง: Return HTTP 500 "Internal Server Error"
|
- หลัง 3 ครั้ง: Return HTTP 500 "Internal Server Error"
|
||||||
- Frontend: แสดง "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ"
|
- Frontend: แสดง "เกิดข้อผิดพลาดในระบบ กรุณาติดต่อผู้ดูแลระบบ"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Architecture Decision Records (ADRs)
|
# Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
**Last Updated:** 2025-11-30
|
**Version:** 1.5.1
|
||||||
|
**Last Updated:** 2025-12-02
|
||||||
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
|
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -81,7 +82,10 @@ Architecture Decision Records (ADRs) เป็นเอกสารที่บ
|
|||||||
|
|
||||||
### 2. Data Integrity & Concurrency
|
### 2. Data Integrity & Concurrency
|
||||||
|
|
||||||
- **ADR-002:** Document Numbering - Double-lock เพื่อป้องกัน Race Condition
|
- **ADR-002:** Document Numbering - Double-lock (Redis Redlock + DB Optimistic) เพื่อป้องกัน Race Condition
|
||||||
|
- 📋 [Requirements](../01-requirements/03.11-document-numbering.md)
|
||||||
|
- 📘 [Implementation Guide](../03-implementation/document-numbering.md)
|
||||||
|
- 📗 [Operations Guide](../04-operations/document-numbering-operations.md)
|
||||||
- **ADR-003:** File Storage - Two-phase เพื่อ Transaction safety
|
- **ADR-003:** File Storage - Two-phase เพื่อ Transaction safety
|
||||||
- **ADR-009:** Database Migration - TypeORM Migrations พร้อม Blue-Green Deployment
|
- **ADR-009:** Database Migration - TypeORM Migrations พร้อม Blue-Green Deployment
|
||||||
|
|
||||||
@@ -352,5 +356,5 @@ graph TB
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Review:** 2025-11-30
|
**Last Review:** 2025-12-02
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Development Tasks
|
# Development Tasks
|
||||||
|
|
||||||
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
|
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-12-01
|
**Last Updated:** 2025-12-02
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -280,10 +280,17 @@ graph TB
|
|||||||
|
|
||||||
- **Type:** Core Service
|
- **Type:** Core Service
|
||||||
- **Key Deliverables:**
|
- **Key Deliverables:**
|
||||||
- Double-lock mechanism (Redis + DB)
|
- Double-lock mechanism (Redis Redlock + DB Optimistic Lock)
|
||||||
- Template-based generator
|
- Template-based generator (10 token types)
|
||||||
- Concurrent-safe implementation
|
- Concurrent-safe implementation (100+ concurrent requests)
|
||||||
|
- Comprehensive error handling (4 scenarios)
|
||||||
|
- Monitoring & alerting (Prometheus + Grafana)
|
||||||
|
- **Documentation:**
|
||||||
|
- 📋 [Requirements](../01-requirements/03.11-document-numbering.md)
|
||||||
|
- 📘 [Implementation Guide](../03-implementation/document-numbering.md)
|
||||||
|
- 📗 [Operations Guide](../04-operations/document-numbering-operations.md)
|
||||||
- **Related ADR:** [ADR-002](../05-decisions/ADR-002-document-numbering-strategy.md)
|
- **Related ADR:** [ADR-002](../05-decisions/ADR-002-document-numbering-strategy.md)
|
||||||
|
- **Task Details:** [TASK-BE-004](./TASK-BE-004-document-numbering.md)
|
||||||
|
|
||||||
### TASK-BE-006: Workflow Engine
|
### TASK-BE-006: Workflow Engine
|
||||||
|
|
||||||
@@ -619,5 +626,5 @@ Add these features when:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.5.0
|
**Version:** 1.5.1
|
||||||
**Last Updated:** 2025-11-30
|
**Last Updated:** 2025-12-02
|
||||||
|
|||||||
@@ -10,21 +10,26 @@
|
|||||||
|
|
||||||
## 📋 Overview
|
## 📋 Overview
|
||||||
|
|
||||||
สร้าง DocumentNumberingService ที่ใช้ Double-Lock mechanism (Redis + DB Optimistic Lock) สำหรับการสร้างเลขที่เอกสารอัตโนมัติ พร้อม comprehensive error handling, monitoring, และ audit logging
|
สร้าง DocumentNumberingService ที่ใช้ Double-Lock mechanism (Redis + DB Optimistic Lock) สำหรับการสร้างเลขที่เอกสารอัตโนมัติ ตาม requirements ใน [03.11-document-numbering.md](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md)
|
||||||
|
|
||||||
|
### เอกสารอ้างอิง
|
||||||
|
|
||||||
|
- **Requirements**: [03.11-document-numbering.md](file:///e:/np-dms/lcbp3/specs/01-requirements/03.11-document-numbering.md)
|
||||||
|
- **Implementation Guide**: [document-numbering.md](file:///e:/np-dms/lcbp3/specs/03-implementation/document-numbering.md)
|
||||||
|
- **Operations Guide**: [document-numbering-operations.md](file:///e:/np-dms/lcbp3/specs/04-operations/document-numbering-operations.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Objectives
|
## 🎯 Objectives
|
||||||
|
|
||||||
- ✅ Template-Based Number Generation (รองรับ 9 token types)
|
- ✅ Template-Based Number Generation (รองรับ 10 token types)
|
||||||
- ✅ Double-Lock Protection (Redis + DB Optimistic Lock)
|
- ✅ Double-Lock Protection (Redis Redlock + DB Optimistic Lock)
|
||||||
- ✅ Concurrent-Safe (No duplicate numbers, tested with 100+ concurrent requests)
|
- ✅ Concurrent-Safe (No duplicate numbers, tested with 100+ concurrent requests)
|
||||||
- ✅ Support 4 Document Types (Correspondence, RFA, Transmittal, Drawing)
|
- ✅ Support All Document Types (LETTER, RFA, TRANSMITTAL, RFI, MEMO, etc.)
|
||||||
- ✅ Year-Based Reset (พ.ศ. และ ค.ศ.)
|
- ✅ Year-Based Auto Reset (ปี ค.ศ.)
|
||||||
- ✅ Transmittal Special Logic (To Owner vs To Contractor)
|
|
||||||
- ✅ 4 Error Scenarios with Fallback Strategies
|
- ✅ 4 Error Scenarios with Fallback Strategies
|
||||||
- ✅ Comprehensive Audit Logging
|
- ✅ Comprehensive Audit Logging
|
||||||
- ✅ Monitoring & Alerting
|
- ✅ Monitoring & Alerting (Prometheus + Grafana)
|
||||||
- ✅ Rate Limiting & Security
|
- ✅ Rate Limiting & Security
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -34,42 +39,44 @@
|
|||||||
### 1. Number Generation
|
### 1. Number Generation
|
||||||
|
|
||||||
- ✅ Generate unique sequential numbers
|
- ✅ Generate unique sequential numbers
|
||||||
- ✅ Support all 9 token types: `{PROJECT}`, `{ORG}`, `{TYPE}`, `{SUB_TYPE}`, `{DISCIPLINE}`, `{CATEGORY}`, `{SEQ:n}`, `{YEAR:B.E.}`, `{YEAR:A.D}`, `{REV}`
|
- ✅ Support all 10 token types: `{PROJECT}`, `{ORIGINATOR}`, `{RECIPIENT}`, `{CORR_TYPE}`, `{SUB_TYPE}`, `{RFA_TYPE}`, `{DISCIPLINE}`, `{SEQ:n}`, `{YEAR:B.E.}`, `{YEAR:A.D.}`, `{REV}`
|
||||||
- ✅ No duplicates even with 100+ concurrent requests
|
- ✅ No duplicates even with 100+ concurrent requests
|
||||||
- ✅ Performance: <500ms (normal), <2s (p95), <5s (p99)
|
- ✅ Performance: <500ms (normal), <2s (p95), <5s (p99)
|
||||||
|
|
||||||
### 2. Lock Mechanism
|
### 2. Lock Mechanism
|
||||||
|
|
||||||
- ✅ Redis distributed lock (TTL: 5 seconds)
|
- ✅ Redis Redlock distributed lock (TTL: 5 seconds)
|
||||||
- ✅ DB optimistic lock with version column
|
- ✅ DB optimistic lock with `version` column
|
||||||
- ✅ Fallback to DB pessimistic lock when Redis unavailable
|
- ✅ Fallback to DB pessimistic lock when Redis unavailable
|
||||||
- ✅ Retry with exponential backoff (5 retries max for lock, 2 for version conflict, 3 for DB errors)
|
- ✅ Retry with exponential backoff (5 retries max for lock, 2 for version conflict, 3 for DB errors)
|
||||||
|
|
||||||
### 3. Document Types Support
|
### 3. Document Types Support
|
||||||
|
|
||||||
- ✅ Correspondence (Letter Type และ Other Types)
|
- ✅ LETTER / RFI / MEMO / EMAIL / MOM / INSTRUCTION / NOTICE / OTHER
|
||||||
- ✅ RFA with Discipline
|
- Counter Key: `(project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)`
|
||||||
- ✅ Transmittal (To Owner vs To Contractor with different formats)
|
- ✅ TRANSMITTAL
|
||||||
- ✅ Drawing with Category
|
- Counter Key: `(project_id, originator_org_id, recipient_org_id, corr_type_id, sub_type_id, 0, 0, year)`
|
||||||
|
- ✅ RFA
|
||||||
|
- Counter Key: `(project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id, discipline_id, year)`
|
||||||
|
|
||||||
### 4. Error Handling
|
### 4. Error Handling
|
||||||
|
|
||||||
- ✅ Scenario 1: Redis Unavailable → Fallback to DB lock
|
- ✅ Scenario 1: Redis Unavailable → Fallback to DB pessimistic lock
|
||||||
- ✅ Scenario 2: Lock Timeout → Retry 5x with exponential backoff
|
- ✅ Scenario 2: Lock Timeout → Retry 5x with exponential backoff
|
||||||
- ✅ Scenario 3: Version Conflict → Retry 2x
|
- ✅ Scenario 3: Version Conflict → Retry 2x immediately
|
||||||
- ✅ Scenario 4: DB Connection Error → Retry 3x
|
- ✅ Scenario 4: DB Connection Error → Retry 3x with exponential backoff
|
||||||
|
|
||||||
### 5. Audit & Monitoring
|
### 5. Audit & Monitoring
|
||||||
|
|
||||||
- ✅ Audit log for every generated number
|
- ✅ Audit log for every generated number (with performance metrics)
|
||||||
- ✅ Track lock wait times, retry counts, errors
|
- ✅ Error logging with classification (LOCK_TIMEOUT, VERSION_CONFLICT, etc.)
|
||||||
- ✅ Metrics collection for monitoring dashboard
|
- ✅ Prometheus metrics collection
|
||||||
- ✅ Alerting on failures >5%
|
- ✅ Alerting on failures >5%
|
||||||
|
|
||||||
### 6. Security
|
### 6. Security
|
||||||
|
|
||||||
- ✅ Rate limiting: 10 req/min per user, 50 req/min per IP
|
- ✅ Rate limiting: 10 req/min per user, 50 req/min per IP (using @nestjs/throttler)
|
||||||
- ✅ Authorization checks
|
- ✅ Authorization checks (JWT + Roles)
|
||||||
- ✅ IP address logging
|
- ✅ IP address logging
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -136,37 +143,56 @@ export class DocumentNumberConfig {
|
|||||||
|
|
||||||
import { Entity, PrimaryColumn, Column, UpdateDateColumn, VersionColumn } from 'typeorm';
|
import { Entity, PrimaryColumn, Column, UpdateDateColumn, VersionColumn } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ตาราง document_number_counters
|
||||||
|
* Composite PK: (project_id, originator_organization_id, recipient_organization_id,
|
||||||
|
* correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, current_year)
|
||||||
|
*
|
||||||
|
* References: specs/01-requirements/03.11-document-numbering.md#counter-key-components
|
||||||
|
*/
|
||||||
@Entity('document_number_counters')
|
@Entity('document_number_counters')
|
||||||
export class DocumentNumberCounter {
|
export class DocumentNumberCounter {
|
||||||
@PrimaryColumn()
|
@PrimaryColumn({ name: 'project_id' })
|
||||||
project_id: number;
|
projectId: number;
|
||||||
|
|
||||||
@PrimaryColumn()
|
@PrimaryColumn({ name: 'originator_organization_id' })
|
||||||
doc_type_id: number;
|
originatorOrganizationId: number;
|
||||||
|
|
||||||
@PrimaryColumn({ default: 0 })
|
@PrimaryColumn({ name: 'recipient_organization_id', nullable: true })
|
||||||
sub_type_id: number; // สำหรับ Correspondence types
|
recipientOrganizationId: number | null; // NULL for RFA
|
||||||
|
|
||||||
@PrimaryColumn({ default: 0 })
|
@PrimaryColumn({ name: 'correspondence_type_id' })
|
||||||
discipline_id: number; // สำหรับ RFA, Drawing
|
correspondenceTypeId: number;
|
||||||
|
|
||||||
@PrimaryColumn({ type: 'varchar', length: 20, nullable: true, default: null })
|
@PrimaryColumn({ name: 'sub_type_id', default: 0 })
|
||||||
recipient_type: string; // สำหรับ Transmittal: 'OWNER', 'CONTRACTOR', 'CONSULTANT', 'OTHER'
|
subTypeId: number; // for TRANSMITTAL only
|
||||||
|
|
||||||
@PrimaryColumn()
|
@PrimaryColumn({ name: 'rfa_type_id', default: 0 })
|
||||||
year: number; // ปี พ.ศ. หรือ ค.ศ.
|
rfaTypeId: number; // for RFA only
|
||||||
|
|
||||||
@Column({ default: 0 })
|
@PrimaryColumn({ name: 'discipline_id', default: 0 })
|
||||||
last_number: number;
|
disciplineId: number; // for RFA only
|
||||||
|
|
||||||
@VersionColumn({ comment: 'Optimistic Lock version' })
|
@PrimaryColumn({ name: 'current_year' })
|
||||||
|
currentYear: number; // ปี ค.ศ.
|
||||||
|
|
||||||
|
@Column({ name: 'last_number', default: 0 })
|
||||||
|
lastNumber: number;
|
||||||
|
|
||||||
|
@VersionColumn({ name: 'version', comment: 'Optimistic Lock version' })
|
||||||
version: number;
|
version: number;
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
updated_at: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **⚠️ หมายเหตุ Schema:**
|
||||||
|
>
|
||||||
|
> - Primary Key ใช้ `COALESCE(recipient_organization_id, 0)` ในการสร้าง constraint (ดู migration file)
|
||||||
|
> - `sub_type_id`, `rfa_type_id`, `discipline_id` ใช้ `0` แทน NULL
|
||||||
|
> - Counter reset อัตโนมัติทุกปี (แยก counter ตาม `current_year`)
|
||||||
|
|
||||||
#### 1.3 Document Number Audit Entity
|
#### 1.3 Document Number Audit Entity
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -1179,6 +1205,7 @@ ensure:
|
|||||||
## 📦 Deliverables
|
## 📦 Deliverables
|
||||||
|
|
||||||
### Core Implementation
|
### Core Implementation
|
||||||
|
|
||||||
- [x] DocumentNumberingService with all 4 error scenarios
|
- [x] DocumentNumberingService with all 4 error scenarios
|
||||||
- [x] DocumentNumberCounter Entity (with sub_type_id, recipient_type)
|
- [x] DocumentNumberCounter Entity (with sub_type_id, recipient_type)
|
||||||
- [x] DocumentNumberConfig Entity
|
- [x] DocumentNumberConfig Entity
|
||||||
@@ -1188,12 +1215,14 @@ ensure:
|
|||||||
- [x] Retry Logic with Exponential Backoff
|
- [x] Retry Logic with Exponential Backoff
|
||||||
|
|
||||||
### API & Security
|
### API & Security
|
||||||
|
|
||||||
- [x] DocumentNumberingController with 4 endpoints
|
- [x] DocumentNumberingController with 4 endpoints
|
||||||
- [x] Rate Limiting Guard (10/min per user, 50/min per IP)
|
- [x] Rate Limiting Guard (10/min per user, 50/min per IP)
|
||||||
- [x] Authorization Guards
|
- [x] Authorization Guards
|
||||||
- [x] API Documentation (Swagger)
|
- [x] API Documentation (Swagger)
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
- [x] Unit Tests (targeting 90%+ coverage)
|
- [x] Unit Tests (targeting 90%+ coverage)
|
||||||
- [x] Concurrent Tests (100+ simultaneous requests)
|
- [x] Concurrent Tests (100+ simultaneous requests)
|
||||||
- [x] Error Scenario Tests (all 4 scenarios)
|
- [x] Error Scenario Tests (all 4 scenarios)
|
||||||
@@ -1202,6 +1231,7 @@ ensure:
|
|||||||
- [x] Load Tests (Artillery config for 50-100 req/sec)
|
- [x] Load Tests (Artillery config for 50-100 req/sec)
|
||||||
|
|
||||||
### Monitoring & Documentation
|
### Monitoring & Documentation
|
||||||
|
|
||||||
- [x] Metrics Collection Integration
|
- [x] Metrics Collection Integration
|
||||||
- [x] Audit Logging
|
- [x] Audit Logging
|
||||||
- [x] Implementation Documentation
|
- [x] Implementation Documentation
|
||||||
@@ -1225,11 +1255,13 @@ ensure:
|
|||||||
## 📌 Implementation Notes
|
## 📌 Implementation Notes
|
||||||
|
|
||||||
### Performance Targets
|
### Performance Targets
|
||||||
|
|
||||||
- **Normal Operation:** <500ms (no conflicts, Redis available)
|
- **Normal Operation:** <500ms (no conflicts, Redis available)
|
||||||
- **95th Percentile:** <2 seconds (including retries)
|
- **95th Percentile:** <2 seconds (including retries)
|
||||||
- **99th Percentile:** <5 seconds (worst case scenarios)
|
- **99th Percentile:** <5 seconds (worst case scenarios)
|
||||||
|
|
||||||
### Lock Configuration
|
### Lock Configuration
|
||||||
|
|
||||||
- **Redis Lock TTL:** 5 seconds (auto-release)
|
- **Redis Lock TTL:** 5 seconds (auto-release)
|
||||||
- **Lock Acquisition Timeout:** 10 seconds
|
- **Lock Acquisition Timeout:** 10 seconds
|
||||||
- **Max Retries (Lock):** 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s)
|
- **Max Retries (Lock):** 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s)
|
||||||
@@ -1237,18 +1269,22 @@ ensure:
|
|||||||
- **Max Retries (DB Error):** 3 times with exponential backoff (1s, 2s, 4s)
|
- **Max Retries (DB Error):** 3 times with exponential backoff (1s, 2s, 4s)
|
||||||
|
|
||||||
### Rate Limiting
|
### Rate Limiting
|
||||||
|
|
||||||
- **Per User:** 10 requests/minute
|
- **Per User:** 10 requests/minute
|
||||||
- **Per IP:** 50 requests/minute
|
- **Per IP:** 50 requests/minute
|
||||||
- **Global:** 5000 requests/minute
|
- **Global:** 5000 requests/minute
|
||||||
|
|
||||||
### Format Templates
|
### Format Templates
|
||||||
|
|
||||||
Stored in database (`document_number_configs` table), configurable per:
|
Stored in database (`document_number_configs` table), configurable per:
|
||||||
|
|
||||||
- Project
|
- Project
|
||||||
- Document Type
|
- Document Type
|
||||||
- Sub Type (optional, use 0 for fallback)
|
- Sub Type (optional, use 0 for fallback)
|
||||||
- Discipline (optional, use 0 for fallback)
|
- Discipline (optional, use 0 for fallback)
|
||||||
|
|
||||||
### Counter Reset
|
### Counter Reset
|
||||||
|
|
||||||
- Automatic reset per year (based on `{YEAR:B.E.}` or `{YEAR:A.D.}` in template)
|
- Automatic reset per year (based on `{YEAR:B.E.}` or `{YEAR:A.D.}` in template)
|
||||||
- Manual reset available (Super Admin only, with audit log)
|
- Manual reset available (Super Admin only, with audit log)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user