Files
solution-erp/docs/database/database-guide.md
pqhuy1987 309dcd9768
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m58s
[CLAUDE] Docs: chot final session 3 — audit MD + session log + minor fixes
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.
2026-04-25 23:40:11 +07:00

14 KiB

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 extends IdentityDbContext<User, Role, Guid> Current schema + ERD: xem 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)

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)

public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public Guid? DeletedBy { get; set; }

AuditingInterceptor 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)

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 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

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

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

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:

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

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

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.InitializeAsyncdb.Database.MigrateAsync()).

Revert migration

# 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

-- 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)

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)

BACKUP LOG SolutionErp
TO DISK = 'D:\Backups\SolutionErp_log.trn'
WITH INIT;

Restore

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 HasMaxLengthnvarchar(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

-- 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 ?';