Files
solution-erp/docs/database/database-guide.md
pqhuy1987 49a5f57a50 [CLAUDE] Docs: database-guide + 6 flow diagrams
docs/database/database-guide.md:
- Conventions (naming, data types, audit fields, soft delete)
- Schema hien tai (Identity tables sau migration Init) + seed 12 role + admin
- Schema planned: Phase 1 dot 2 (Supplier/Project/Department + Permission Matrix)
- Schema planned: Phase 3 (Contract + Approval + Comment + Attachment + Template + Clause + CodeSequence)
- Mermaid ERD cho tung phase
- Migration workflow (create/apply/revert)
- Index strategy + unique indexes
- Backup/restore SQL
- Common pitfalls + SQL cheatsheet

docs/flows/ — 6 flow documentation:
- README.md: index
- auth-flow.md: login/refresh/me/logout (IMPLEMENTED, sequence + edge cases + security checklist)
- permission-flow.md: Phase 1 dot 2 - Role x MenuKey x CRUD resolution + FE guard + BE policy
- contract-creation-flow.md: Phase 2 - Drafter flow chon template -> fill -> preview -> save draft
- contract-approval-flow.md: Phase 3 - state machine 9 phase chi tiet + reject flow + timeline UI
- form-render-flow.md: Phase 2 - OpenXml + ClosedXML + LibreOffice PDF convert
- sla-expiry-flow.md: Phase 3 - BackgroundService auto-approve qua SLA + warning notify

Update references:
- CLAUDE.md (root): them 2 row Tai lieu quan trong
- docs/CLAUDE.md: update project layout voi flows/ + database/
- docs/STATUS.md: log docs addition
- docs/changelog/migration-todos.md: tick Phase 0 docs items

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:15:28 +07:00

464 lines
14 KiB
Markdown

# Database Guide — SOLUTION_ERP
> **DB engine:** SQL Server 2022 (LocalDB dev, full instance prod)
> **ORM:** EF Core 10 Code-First migrations
> **DbContext:** [`ApplicationDbContext`](../../src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs) extends `IdentityDbContext<User, Role, Guid>`
## 1. Connection strings
| Env | ConnectionString | DB name |
|---|---|---|
| Dev (default) | `Server=(localdb)\MSSQLLocalDB;Database=SolutionErp_Dev;Trusted_Connection=True` | `SolutionErp_Dev` |
| Design-time (EF CLI) | same server, `Database=SolutionErp_Design` | `SolutionErp_Design` |
| Prod | `Server=<prod>;Database=SolutionErp;User=<>;Password=<>;TrustServerCertificate=true` | `SolutionErp` |
Cả 3 DB dùng chung schema, khác tên → có thể drop `SolutionErp_Design` không mất dữ liệu.
## 2. Conventions
### Naming
| Item | Rule | Ví dụ |
|---|---|---|
| Schema | 1 schema duy nhất: `dbo` | — |
| Table | PascalCase tiếng Anh, số nhiều | `Contracts`, `Suppliers`, `ContractApprovals` |
| Column | PascalCase | `FullName`, `CreatedAt`, `SupplierId` |
| PK | `Id` | `Guid` (không auto-increment) |
| FK | `{EntityName}Id` | `SupplierId`, `ContractId` |
| Index | `IX_{Table}_{Col1}_{Col2}` | `IX_Contracts_Phase_SupplierId` |
| Unique | `UX_{Table}_{Col}` | `UX_Suppliers_Code` |
| Migration | `YYYYMMDDHHMMSS_{Name}` (auto) | `20260421034520_Init` |
### Data types
| C# type | SQL type | Notes |
|---|---|---|
| `Guid` | `uniqueidentifier` | PK + FK default |
| `string` | `nvarchar(max)` default | Luôn set `HasMaxLength(n)` trong config |
| `DateTime` | `datetime2` | UTC — luôn qua `IDateTime.UtcNow` |
| `decimal` (tiền) | `decimal(18,2)` | `HasPrecision(18, 2)` |
| `int` enum | `int` | `HasConversion<int>()` |
| `bool` | `bit` | — |
| JSON blob | `nvarchar(max)` | `HasColumnType("nvarchar(max)")` — dùng `HasConversion` serialize |
### Audit fields (mọi entity extend `BaseEntity`)
```csharp
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime CreatedAt { get; set; } // Set by AuditingInterceptor
public DateTime? UpdatedAt { get; set; } // Set by AuditingInterceptor
public Guid? CreatedBy { get; set; } // From ICurrentUser.UserId
public Guid? UpdatedBy { get; set; } // From ICurrentUser.UserId
```
### Soft delete (entity extend `AuditableEntity`)
```csharp
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public Guid? DeletedBy { get; set; }
```
[`AuditingInterceptor`](../../src/Backend/SolutionErp.Infrastructure/Persistence/Interceptors/AuditingInterceptor.cs) tự động:
- `Added` entity → set `CreatedAt`, `CreatedBy`
- `Modified` entity → set `UpdatedAt`, `UpdatedBy`
- `Deleted` entity kiểu `AuditableEntity` → convert sang `Modified` + set `IsDeleted=true`, `DeletedAt`, `DeletedBy` (soft delete)
**Query filter soft delete:** config `modelBuilder.Entity<T>().HasQueryFilter(e => !e.IsDeleted)` cho mọi `AuditableEntity`. Sẽ làm Phase 1 đợt 2.
## 3. Schema hiện tại (sau migration `Init`)
### Identity tables (đã có sau migration 1)
```mermaid
erDiagram
Users ||--o{ UserRoles : has
Roles ||--o{ UserRoles : has
Users ||--o{ UserClaims : has
Users ||--o{ UserLogins : has
Users ||--o{ UserTokens : has
Roles ||--o{ RoleClaims : has
Users {
uniqueidentifier Id PK
nvarchar FullName "200"
nvarchar Email
nvarchar UserName
nvarchar PasswordHash
nvarchar RefreshToken "512"
datetime2 RefreshTokenExpiresAt
bit IsActive
datetime2 CreatedAt
datetime2 UpdatedAt
bit EmailConfirmed
bit LockoutEnabled
datetime2 LockoutEnd
int AccessFailedCount
}
Roles {
uniqueidentifier Id PK
nvarchar Name
nvarchar NormalizedName
nvarchar Description "500"
datetime2 CreatedAt
}
UserRoles {
uniqueidentifier UserId PK,FK
uniqueidentifier RoleId PK,FK
}
UserClaims {
int Id PK
uniqueidentifier UserId FK
nvarchar ClaimType
nvarchar ClaimValue
}
UserLogins {
nvarchar LoginProvider PK
nvarchar ProviderKey PK
uniqueidentifier UserId FK
}
UserTokens {
uniqueidentifier UserId PK,FK
nvarchar LoginProvider PK
nvarchar Name PK
nvarchar Value
}
RoleClaims {
int Id PK
uniqueidentifier RoleId FK
nvarchar ClaimType
nvarchar ClaimValue
}
```
### Seed data hiện tại
[`DbInitializer`](../../src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs) auto-run khi API start:
**12 role:**
| Role | Ý nghĩa (theo workflow-contract.md) |
|---|---|
| `Admin` | Quản trị hệ thống |
| `Drafter` | Người soạn thảo (QS/NV.PB) |
| `DeptManager` | Trưởng Phòng ban (TPB) |
| `ProjectManager` | Giám đốc Dự án (PM) |
| `Procurement` | Phòng Cung ứng (PRO) |
| `CostControl` | Phòng Kiểm soát Chi phí (CCM) |
| `Finance` | Phòng Tài chính (FIN) |
| `Accounting` | Phòng Kế toán (ACT) |
| `Equipment` | Phòng Thiết bị (EQU) |
| `Director` | Ban Giám đốc (BOD) |
| `AuthorizedSigner` | Người được ủy quyền ký (NĐUQ) |
| `HrAdmin` | Nhân sự - Hành chính (HRA) |
**1 admin user:**
| Field | Value |
|---|---|
| Email | `admin@solutionerp.local` |
| Password | `Admin@123456` |
| FullName | `Administrator` |
| Roles | `[Admin]` |
## 4. Schema dự kiến
### Phase 1 đợt 2 — Master data
```mermaid
erDiagram
Suppliers {
uniqueidentifier Id PK
nvarchar Code "50 UNIQUE"
nvarchar Name "200"
nvarchar TaxCode "20"
nvarchar Phone "20"
nvarchar Email "100"
nvarchar Address "500"
int Type "NCC/NTP/TD/DVDV"
datetime2 CreatedAt
datetime2 UpdatedAt
uniqueidentifier CreatedBy FK
uniqueidentifier UpdatedBy FK
bit IsDeleted
}
Projects {
uniqueidentifier Id PK
nvarchar Code "50 UNIQUE"
nvarchar Name "200"
date StartDate
date EndDate
uniqueidentifier ManagerUserId FK
}
Departments {
uniqueidentifier Id PK
nvarchar Code "50 UNIQUE"
nvarchar Name "200"
uniqueidentifier ManagerUserId FK
}
Users ||--o{ Projects : manages
Users ||--o{ Departments : manages
```
### Phase 1 đợt 2 — Permission Matrix
```mermaid
erDiagram
MenuItems ||--o{ Permissions : has
Roles ||--o{ Permissions : has
MenuItems {
nvarchar Key PK "PascalCase, vd Contracts"
nvarchar Label "200 tieng Viet"
nvarchar ParentKey FK "NULL neu root"
int Order
nvarchar Icon "50 lucide icon name"
}
Permissions {
uniqueidentifier Id PK
uniqueidentifier RoleId FK
nvarchar MenuKey FK
bit CanRead
bit CanCreate
bit CanUpdate
bit CanDelete
}
```
Unique key: `(RoleId, MenuKey)`.
### Phase 3 — Contract + Workflow
```mermaid
erDiagram
Contracts ||--o{ ContractApprovals : has
Contracts ||--o{ ContractComments : has
Contracts ||--o{ ContractAttachments : has
Contracts }o--|| Suppliers : references
Contracts }o--|| Projects : references
Contracts }o--|| Departments : drafted_in
Contracts }o--|| ContractTemplates : uses
Contracts {
uniqueidentifier Id PK
nvarchar MaHopDong "100 nullable until Phase5"
int Type "ContractType enum"
int Phase "ContractPhase enum 9 state"
uniqueidentifier SupplierId FK
uniqueidentifier ProjectId FK
uniqueidentifier DepartmentId FK
uniqueidentifier DrafterUserId FK
uniqueidentifier TemplateId FK
decimal GiaTri "18 2"
bit BypassProcurementAndCCM
datetime2 SlaDeadline "khi nao phase hien tai het han"
nvarchar DraftData "nvarchar max JSON field values"
}
ContractApprovals {
uniqueidentifier Id PK
uniqueidentifier ContractId FK
int Phase
uniqueidentifier ApproverUserId FK
datetime2 ApprovedAt
int Decision "Pending/Approve/Reject/AutoApprove"
nvarchar Comment "1000"
}
ContractComments {
uniqueidentifier Id PK
uniqueidentifier ContractId FK
uniqueidentifier UserId FK
int Phase "phase luc comment"
nvarchar Content "2000"
datetime2 CreatedAt
}
ContractAttachments {
uniqueidentifier Id PK
uniqueidentifier ContractId FK
nvarchar FileName "255"
nvarchar StoragePath "500"
bigint FileSize
nvarchar ContentType "100"
nvarchar Purpose "50 Scan/Export/Template"
datetime2 UploadedAt
}
ContractTemplates {
uniqueidentifier Id PK
nvarchar FormCode "20 UNIQUE vd FO-002.05"
nvarchar Name "200"
int ContractType
nvarchar TemplatePath "500"
nvarchar FieldSpec "nvarchar max JSON"
bit IsActive
}
ContractClauses {
uniqueidentifier Id PK
nvarchar Code "20 UNIQUE vd FO-002.04"
nvarchar Name "200"
nvarchar Content "nvarchar max rich text"
int Version
bit IsActive
}
```
### Phase 3 — Contract Code Generator
`ContractCodeSequence` bảng đơn giản để tránh race condition khi gen seq:
```sql
CREATE TABLE dbo.ContractCodeSequences (
Prefix NVARCHAR(100) NOT NULL PRIMARY KEY, -- "FLOCK 01/HĐTP/SOL&HD" hoặc "2026/NCC/SOL&AKATI"
LastSeq INT NOT NULL,
UpdatedAt DATETIME2 NOT NULL
);
```
Logic trong `ContractCodeGenerator`:
1. Begin transaction `SERIALIZABLE`
2. `SELECT LastSeq FROM ContractCodeSequences WITH (UPDLOCK, HOLDLOCK) WHERE Prefix = @prefix`
3. Nếu không có → INSERT với `LastSeq = 1`
4. Nếu có → `UPDATE ContractCodeSequences SET LastSeq = LastSeq + 1`
5. Return `{prefix}/{LastSeq:D2}`
6. Commit
## 5. Migration workflow
### Tạo migration mới
```powershell
cd D:\Dropbox\CONG_VIEC\SOLUTION\SOLUTION_ERP
dotnet ef migrations add <Name> `
--project src\Backend\SolutionErp.Infrastructure `
--startup-project src\Backend\SolutionErp.Api `
--output-dir Persistence\Migrations
```
Ví dụ Phase 1 đợt 2:
```
dotnet ef migrations add AddMasterData
dotnet ef migrations add AddPermissions
dotnet ef migrations add AddContractDraft
```
Phase 3:
```
dotnet ef migrations add AddContractWorkflow
dotnet ef migrations add AddContractCodeSequence
```
### Apply migration
```powershell
dotnet ef database update `
--project src\Backend\SolutionErp.Infrastructure `
--startup-project src\Backend\SolutionErp.Api
```
Hoặc tự động khi API start (đã config trong `DbInitializer.InitializeAsync``db.Database.MigrateAsync()`).
### Revert migration
```powershell
# Rollback 1 bước
dotnet ef database update <PreviousMigrationName>
# Xóa file migration chưa apply
dotnet ef migrations remove
```
### Revert DB về trạng thái clean
```sql
-- Chạy trong SSMS hoặc sqlcmd
DROP DATABASE SolutionErp_Dev;
-- Hoặc giữ DB, chỉ xóa data:
EXEC sp_MSforeachtable 'DELETE FROM ?';
```
Rồi chạy lại API → tự migrate + seed.
## 6. Index strategy (Phase 3-4 optimize)
### Hot queries cần index
| Query | Index đề xuất |
|---|---|
| Inbox — HĐ theo phase + role của user | `IX_Contracts_Phase_IsDeleted` |
| List HĐ theo NCC | `IX_Contracts_SupplierId_IsDeleted` |
| List HĐ theo dự án | `IX_Contracts_ProjectId_IsDeleted` |
| Approval của 1 HĐ | `IX_ContractApprovals_ContractId_Phase` |
| Comment thread | `IX_ContractComments_ContractId_CreatedAt` |
| Audit log | `IX_AuditLogs_EntityId_CreatedAt` |
### Index unique
| Unique | Cột |
|---|---|
| `UX_Suppliers_Code` | `Code` |
| `UX_Projects_Code` | `Code` |
| `UX_Departments_Code` | `Code` |
| `UX_Contracts_MaHopDong` | `MaHopDong` (WHERE `MaHopDong IS NOT NULL` — filtered) |
| `UX_Users_Email` | đã có qua Identity |
| `UX_MenuItems_Key` | PK |
| `UX_Permissions_Role_Menu` | `(RoleId, MenuKey)` |
## 7. Backup & restore (prod)
### Backup daily (SQL Agent job)
```sql
BACKUP DATABASE SolutionErp
TO DISK = 'D:\Backups\SolutionErp_full.bak'
WITH INIT, COMPRESSION, CHECKSUM;
```
### Backup log mỗi 15 phút (nếu full recovery model)
```sql
BACKUP LOG SolutionErp
TO DISK = 'D:\Backups\SolutionErp_log.trn'
WITH INIT;
```
### Restore
```sql
RESTORE DATABASE SolutionErp
FROM DISK = 'D:\Backups\SolutionErp_full.bak'
WITH REPLACE, RECOVERY;
```
Chi tiết runbook ở Phase 5 `docs/guides/cicd.md`.
## 8. Common pitfalls
- **Quên `HasMaxLength`** → `nvarchar(max)`, gây bloat + index lỗi. Luôn set.
- **Lưu DateTime local thay vì UTC** → sai timezone khi deploy server khác múi giờ. Dùng `IDateTime.UtcNow` (tiêm qua DI), KHÔNG `DateTime.Now`.
- **Soft delete quên filter** → user thấy data đã xóa. Luôn `HasQueryFilter(e => !e.IsDeleted)` ở config.
- **FK không có index** → slow join. EF Core tự tạo index cho FK, nhưng vẫn phải verify sau migration.
- **Migration bị commit thiếu `.Designer.cs`** → team khác không apply được. Commit cả 3 file (`{name}.cs`, `{name}.Designer.cs`, `ApplicationDbContextModelSnapshot.cs`).
- **Chạy `dotnet ef` khi API đang chạy** → lock conflict. Dừng API trước khi migrate ở dev.
- **Gen mã HĐ ngoài transaction** → race condition. Luôn dùng `ContractCodeGenerator` với lock.
## 9. Quick SQL cheatsheet
```sql
-- Xem 10 HĐ mới nhất
SELECT TOP 10 Id, MaHopDong, Phase, CreatedAt FROM Contracts ORDER BY CreatedAt DESC;
-- Đếm HĐ theo phase
SELECT Phase, COUNT(*) FROM Contracts WHERE IsDeleted = 0 GROUP BY Phase;
-- List user + roles
SELECT u.Email, u.FullName, r.Name AS Role
FROM Users u
LEFT JOIN UserRoles ur ON u.Id = ur.UserId
LEFT JOIN Roles r ON ur.RoleId = r.Id
ORDER BY u.Email;
-- Reset admin password (emergency)
-- Cách an toàn: không edit PasswordHash trực tiếp, dùng UserManager.ResetPasswordAsync qua API
-- Clear toàn bộ dev data (giữ migrations)
EXEC sp_MSforeachtable 'DELETE FROM ?';
```