All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m58s
Audit toan bo MD theo yeu cau 'review cap nhat va tai cau truc'. Minor fixes outdated: - docs/database/database-guide.md: header note '47 bang 13 migration' + redirect schema-diagram cho actual schema. Email demo + admin@solutions.com.vn (thay solutionerp.local). - docs/guides/runbook.md: reset-password email moi. Session log moi: docs/changelog/sessions/2026-04-25-1430-chot-session-3- final-cleanup.md - Cleanup outcomes: archive 2 raw + compact migration-todos -71% + compact STATUS In Progress -50% + update outdated refs. - Skill audit 6 skill: tat ca Active (3 skill da update voi PE/G-084 cross-ref session truoc). - Stats session 3 cong don 3 phien 23-24-25: 47 DB tables, 13 migrations, ~113 endpoints, 38 gotchas, ~30 commits. - Priority 0 session 4: 3 PE feature gap (Designer UI, Y kien 4 phong ban, Export PDF) + Ops blockers. - Notes session 4: email moi, 3 domain prod, old fallback, G-084, SSH.
467 lines
14 KiB
Markdown
467 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 — **47 bảng, 13 migration** (Init → AddPurchaseEvaluationCodeSequences)
|
|
> **DbContext:** [`ApplicationDbContext`](../../src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs) extends `IdentityDbContext<User, Role, Guid>`
|
|
> **Current schema + ERD:** xem [`schema-diagram.md`](schema-diagram.md) (luôn cập nhật)
|
|
>
|
|
> File này = conventions + migration workflow + cheatsheet. Section "Schema dự kiến" (§4) là Phase 0 historical — actual schema xem `schema-diagram.md`.
|
|
|
|
## 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@solutions.com.vn` |
|
|
| 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 ?';
|
|
```
|