[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>
This commit is contained in:
463
docs/database/database-guide.md
Normal file
463
docs/database/database-guide.md
Normal file
@ -0,0 +1,463 @@
|
||||
# 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 ?';
|
||||
```
|
||||
Reference in New Issue
Block a user