Bỏ qua để đến nội dung

Thiết Kế Module Nền Tảng Cốt Lõi

Module Nền tảng Cốt lõi là nền tảng của BPO ERP. Mọi module khác phụ thuộc vào nó. Nó cung cấp:

  • Vòng đời tenant — cấp, cấu hình, tạm ngừng, offboard
  • Quản lý người dùng — đăng ký, xác thực, hồ sơ, chính sách mật khẩu
  • Ủy quyền — quản lý vai trò và quyền (mô hình được định nghĩa trong Authorization.md)
  • Ghi nhật ký kiểm toán — bản ghi bất biến của tất cả thay đổi dữ liệu trên mọi module
  • Cấu hình hệ thống — cài đặt theo tenant và mặc định nền tảng
  • Hạ tầng công việc nền — lập lịch, thử lại và giám sát

Module này phát hành trong Giai đoạn 1. Các thành phần của nó là điều kiện tiên quyết cho mọi module Giai đoạn 1 khác.


Provisioning → Active → Suspended → Offboarded
Trạng tháiMô tả
ProvisioningBản ghi tenant đã tạo; schema đã seed; chưa cho phép người dùng tenant truy cập
ActiveTrạng thái hoạt động bình thường
SuspendedTruy cập bị chặn (không thanh toán, vi phạm chính sách); dữ liệu được giữ; có thể kích hoạt lại
OffboardedTenant đã chấm dứt hợp đồng; dữ liệu đã xuất và lên lịch xóa sau thời gian lưu trữ

Khi Quản trị viên Hệ thống tạo một tenant mới, nền tảng thực hiện các bước sau theo thứ tự trong một giao dịch duy nhất:

  1. Chèn hàng vào bảng Tenants với Status = 'Provisioning'
  2. Seed Hệ thống Tài khoản VAS (VasChartOfAccounts) cho tenant sử dụng mẫu Thông tư 200 hoặc Thông tư 133 tùy theo quy mô doanh nghiệp đã khai báo của tenant
  3. Seed Hệ thống Tài khoản IFRS (ChartOfAccounts) với mẫu khởi đầu tiêu chuẩn
  4. Seed các TenantRoles tích hợp sẵn (TenantAdministrator, Accountant, Purchasing, Sales, Viewer) và TenantRolePermissions mặc định của chúng
  5. Tạo các hàng FiscalPeriod ban đầu cho năm tài chính hiện tại
  6. Seed hàng TenantEInvoiceConfig mặc định (rỗng — quản trị viên tenant phải hoàn tất cài đặt hóa đơn điện tử)
  7. Đặt Status = 'Active'
  8. Gửi email onboarding đến địa chỉ email quản trị viên tenant được chỉ định với hướng dẫn đăng nhập

Nếu bất kỳ bước nào thất bại, giao dịch được rollback và bản ghi tenant một phần bị xóa. Quản trị viên Hệ thống được thông báo về thất bại với chi tiết lỗi.

Tenants (cấp nền tảng, không áp dụng RLS)
──────────────────────────────────────────────────────
TenantId UNIQUEIDENTIFIER PK
TenantName NVARCHAR(200) NOT NULL
TaxCode NVARCHAR(20) NOT NULL -- Mã số thuế doanh nghiệp Việt Nam
LegalName NVARCHAR(500) NOT NULL -- Tên pháp nhân đầy đủ (Tiếng Việt)
EnterpriseSize NVARCHAR(20) NOT NULL -- 'Small' (C133) hoặc 'Medium_Large' (C200)
FunctionalCurrency CHAR(3) NOT NULL DEFAULT 'VND'
FiscalYearStart NVARCHAR(5) NOT NULL -- 'MM-DD', ví dụ: '01-01' cho ngày 1/1
Status NVARCHAR(20) NOT NULL -- 'Provisioning' | 'Active' | 'Suspended' | 'Offboarded'
CreatedAt DATETIMEOFFSET NOT NULL
SuspendedAt DATETIMEOFFSET NULL
OffboardedAt DATETIMEOFFSET NULL
TenantSettings (có phạm vi tenant, áp dụng RLS)
──────────────────────────────────────────────────────
SettingId UNIQUEIDENTIFIER PK
TenantId UNIQUEIDENTIFIER NOT NULL (RLS-enforced)
SettingKey NVARCHAR(100) NOT NULL
SettingValue NVARCHAR(MAX) NOT NULL
UNIQUE (TenantId, SettingKey)

Người dùng xác thực qua ASP.NET Core Identity. Một tài khoản người dùng tồn tại ở cấp nền tảng (AspNetUsers) và có thể được liên kết với một hoặc nhiều tenant qua các hàng TenantUsers (dành cho tư vấn viên hoặc quản trị viên phục vụ nhiều công ty).

Frontend React được đồng host với ASP.NET Core API (phục vụ từ cùng một origin). Vì lý do này, xác thực cookie ASP.NET Core Identity là mặc định cho phiên web.

  • Cookie là HttpOnly, SecureSameSite=Strict — không thể truy cập từ JavaScript.
  • Không lưu token trong localStorage hoặc sessionStorage được yêu cầu hoặc được phép cho web client.
  • TenantId được lưu dưới dạng claim trong cookie xác thực form được mã hóa (qua ASP.NET Core Data Protection). Nó được đặt khi đăng nhập và xác thực lại ở mỗi yêu cầu.
  • Bảo vệ CSRF được cung cấp bởi chính sách cookie SameSite=Strict. Token chống giả mạo được sử dụng cho các endpoint thay đổi trạng thái như một lớp bổ sung.

Một endpoint JWT/Bearer (/auth/token) có sẵn song song với xác thực cookie cho các trường hợp sử dụng không thể dùng cookie: ứng dụng di động gốc Giai đoạn 3 và bất kỳ tích hợp API bên thứ ba nào. Thực thi chính sách RBAC giống hệt cho cả hai lược đồ xác thực — cấu trúc claims giống nhau bất kể phương thức vận chuyển nào.

Người dùng tenant được cấp phát bởi quản trị viên tenant (hoặc Quản trị viên Hệ thống). Hai chế độ tạo được hỗ trợ:

Mật khẩu do admin đặt (mặc định):

  1. Quản trị viên tenant tạo người dùng trong UI quản trị: email, tên hiển thị, gán vai trò, mật khẩu ban đầu.
  2. Nền tảng tạo hàng AspNetUsers với mật khẩu do admin đặt và các hàng TenantUsers + TenantUserRoles.
  3. Người dùng ngay lập tức hoạt động và có thể đăng nhập.

Luồng mời (tùy chọn):

  1. Quản trị viên tenant tạo người dùng mà không đặt mật khẩu; nền tảng gửi email mời với liên kết kích hoạt có thời hạn (48 giờ).
  2. Người dùng theo liên kết và đặt mật khẩu của họ.
  3. Liên kết mời hết hạn có thể được gửi lại bởi quản trị viên tenant.

Quản trị viên tenant chọn chế độ nào cho từng người dùng. Cả hai chế độ đều tạo ra tài khoản người dùng hoạt động giống hệt nhau.

Tự đăng ký (người dùng tự tạo tài khoản không có sự tham gia của admin) không được hỗ trợ.

Mặc định ASP.NET Core Identity, được thực thi:

  • Tối thiểu 8 ký tự
  • Ít nhất một chữ hoa, một chữ thường, một chữ số, một ký tự đặc biệt
  • Mật khẩu không được tái sử dụng (nhớ 5 lần gần nhất)
  • Tài khoản bị khóa 15 phút sau 5 lần đăng nhập thất bại liên tiếp

MFA (TOTP) được hoãn đến Giai đoạn 2.

  • Phiên xác thực cookie: hết hạn sau 8 giờ không hoạt động (sliding expiry); hết hạn tuyệt đối 24 giờ
  • Khi đổi mật khẩu hoặc vô hiệu hóa tài khoản: cookie phiên của người dùng bị vô hiệu hóa ngay lập tức qua thu hồi key ASP.NET Core Data Protection hoặc danh sách vô hiệu hóa phiên phía server
  • Token làm mới JWT (cho endpoint /auth/token): hết hạn 7 ngày, lưu phía server trong UserRefreshTokens, có thể vô hiệu hóa từng cái hoặc tất cả cùng lúc

  • Chỉ thêm (append-only). Nhật ký kiểm toán không bao giờ được CẬP NHẬT hoặc XÓA qua mã ứng dụng.
  • Bất biến được đảm bảo bởi cơ sở dữ liệu. Người dùng cơ sở dữ liệu ứng dụng không có đặc quyền UPDATE hoặc DELETE trên bảng AuditLog.
  • Bao phủ mọi thay đổi. Mọi INSERT, UPDATEDELETE trên bảng có phạm vi tenant phải tạo ra một bản ghi kiểm toán.
  • Ghi giá trị trước/sau. Giá trị cũ (trước) và giá trị mới (sau) được lưu dưới dạng JSON cho mọi thay đổi.

Các mục kiểm toán được ghi bởi EF Core SaveChanges interceptor (AuditInterceptor) chạy trước khi giao dịch commit. Interceptor:

  1. Kiểm tra ChangeTracker cho tất cả entity đã sửa đổi
  2. Với mỗi entity đã sửa, ghi lại: TenantId, UserId, TableName, RecordId, ChangeType (Insert/Update/Delete), OldValues (JSON), NewValues (JSON), Timestamp (UTC)
  3. Ghi tất cả hàng kiểm toán trong cùng giao dịch với các thay đổi

Interceptor chạy trong cùng giao dịch — hoặc tất cả các thay đổi và bản ghi kiểm toán của chúng được commit cùng nhau, hoặc không có gì cả.

AuditLog (có phạm vi tenant, áp dụng RLS, chỉ INSERT cho người dùng ứng dụng)
──────────────────────────────────────────────────────
AuditId UNIQUEIDENTIFIER PK
TenantId UNIQUEIDENTIFIER NOT NULL (RLS-enforced)
UserId UNIQUEIDENTIFIER NULL FK → AspNetUsers (NULL cho system/background job)
TableName NVARCHAR(100) NOT NULL
RecordId UNIQUEIDENTIFIER NOT NULL
ChangeType NVARCHAR(10) NOT NULL -- 'Insert' | 'Update' | 'Delete'
OldValues NVARCHAR(MAX) NULL -- JSON; NULL cho Insert
NewValues NVARCHAR(MAX) NULL -- JSON; NULL cho Delete
Timestamp DATETIMEOFFSET NOT NULL
PermissionCode NVARCHAR(100) NULL -- quyền được sử dụng cho thay đổi này
RequestId UNIQUEIDENTIFIER NULL -- liên kết tất cả thay đổi trong một HTTP request
JobName NVARCHAR(100) NULL -- điền cho background job; NULL cho user request

RequestId liên kết tất cả thay đổi từ một lệnh gọi API để có thể theo dõi. Background job đặt UserId = NULL và điền JobName.

Quản trị viên tenant có thể lọc nhật ký kiểm toán theo: khoảng thời gian, người dùng, loại bảng/entity, ID bản ghi và loại thay đổi. Giao diện mặc định hiển thị 90 ngày gần nhất. Lịch sử kiểm toán đầy đủ có sẵn trong thời gian lưu trữ 10 năm.

Bản ghi kiểm toán được lưu 10 năm theo Luật Kế toán Việt Nam (Điều 40). Lưu trữ tự động (sang cold storage nén) sau 2 năm giữ cho bảng AuditLog chính hoạt động hiệu quả. Bản ghi đã lưu trữ vẫn có thể truy vấn qua UI quản trị.


ASP.NET Core hosted services với Hangfire (SQL Server backing store). Lý do:

  • SQL Server đã là cơ sở dữ liệu chính — không cần hạ tầng bổ sung
  • Hangfire cung cấp web dashboard để giám sát công việc, thử lại và lịch sử
  • Thư viện được hỗ trợ tốt với tích hợp EF Core và ASP.NET Core
  • Đơn giản để vận hành cho đội ngũ ≤5 kỹ sư
LoạiVí dụLịch trình
Xử lý cuối kỳXác thực đóng kỳ tài chính, điều chỉnh cuối kỳ VASTheo yêu cầu (kích hoạt bởi người dùng)
Kiểm tra hóa đơn điện tửKiểm tra hóa đơn Submitted đang chờMỗi 5 phút
Tạo báo cáoTạo trước báo cáo pháp định theo lịchCó thể cấu hình theo tenant (ví dụ: hàng tháng)
Đồng bộ tỷ giáLấy tỷ giá VND mới nhất từ nguồn NHNNHàng ngày
Lưu trữ kiểm toánChuyển bản ghi kiểm toán cũ hơn 2 năm sang archiveHàng tháng
Xuất dữ liệuXuất dữ liệu tenant để offboardTheo yêu cầu

Mỗi lần chạy công việc được giới hạn trong một tenant (khi áp dụng). Công việc resolve TenantId từ tham số riêng của nó — không bao giờ từ context môi trường. TenantId được đặt trong SESSION_CONTEXT cho kết nối cơ sở dữ liệu dùng trong công việc, đảm bảo RLS hoạt động bình thường. Công việc toàn nền tảng (ví dụ: đồng bộ tỷ giá) dùng PlatformAdminDbContext.

Loại công việcSố lần thử tối đaKhoảng thời gian thử lại
Kiểm tra hóa đơn điện tửThử liên tục5 phút
Tạo báo cáo3 lần thử5 phút, 15 phút, 30 phút
Đồng bộ tỷ giá5 lần thử10 phút (theo cấp số nhân)
Xử lý cuối kỳKhông tự động thử lại (cần người dùng kích hoạt lại)N/A

Công việc hết lần thử được chuyển sang trạng thái dead-letter. Quản trị viên Hệ thống và (khi liên quan đến tenant) quản trị viên tenant được thông báo. Công việc không bị bỏ qua im lặng.


Mặc định toàn nền tảng được lưu trong bảng PlatformSettings (không áp dụng RLS):

PlatformSettings (cấp nền tảng, không áp dụng RLS)
──────────────────────────────────────────────────────
SettingKey NVARCHAR(100) PK
SettingValue NVARCHAR(MAX) NOT NULL
Description NVARCHAR(500) NULL
LastUpdatedAt DATETIMEOFFSET NOT NULL
LastUpdatedBy NVARCHAR(200) NOT NULL

Ví dụ: timeout phiên cookie, ghi đè chính sách mật khẩu, URL nguồn tỷ giá, thời gian lưu trữ kiểm toán.

Tenant có thể ghi đè một tập hợp con các mặc định nền tảng thông qua TenantSettings. ITenantSettingsService resolve giá trị theo thứ tự: ghi đè tenant → mặc định nền tảng → mặc định code.


Các module khác phụ thuộc vào Nền tảng Cốt lõi như sau:

ModulePhụ thuộc Nền tảng Cốt lõi
Tất cả moduleITenantContext (TenantId), thực thi RBAC, Audit interceptor, cột CustomData
Kế toánFiscalPeriods, ChartOfAccounts, VasChartOfAccounts, ExchangeRates
Tất cả moduleDomain event bus (cho hook customization engine Giai đoạn 2)
Hóa đơn điện tửTenantEInvoiceConfigs, EInvoiceProviders
Công cụ Tùy chỉnh (Giai đoạn 2)CustomFieldDefinitions, ValidationRules, WorkflowDefinitions

Nền tảng Cốt lõi phải là module đầu tiên được tích hợp và kiểm thử. Không module nào khác được phát hành cho đến khi các điều kiện sau được xác minh:

  • Cấp phát tenant tạo ra một tenant được cô lập hoàn toàn với dữ liệu seed đúng
  • Các kiểm tra cô lập xuyên tenant đạt (xem multitenancy.md §Kiểm tra)
  • Audit interceptor ghi lại tất cả thay đổi trên tất cả bảng entity cốt lõi
  • Phiên xác thực cookie và thực thi quyền hoạt động end-to-end cho tất cả vai trò tích hợp sẵn
  • Job scheduler nền khởi động, chạy một công việc test và ghi lại kết quả

  1. UX người dùng đa tenant — Người dùng có quyền truy cập nhiều tenant cần màn hình chọn tenant sau khi đăng nhập. Luồng và UI chưa được thiết kế.

  2. MFA — Xác thực đa yếu tố dựa trên TOTP được hoãn đến Giai đoạn 2. TenantSettings nên bao gồm key mfa.required có thể bật theo tenant khi MFA được triển khai.

  3. Xuất dữ liệu khi offboard — Định dạng và nội dung xuất dữ liệu tenant (cho tuân thủ PDPD khi có yêu cầu xóa dữ liệu) cần được định nghĩa trước khi có khách hàng nào offboard. Điều này nên được thiết kế trước khi go-live dù chưa có tenant nào chấm dứt.

  4. Nguồn tỷ giá — NHNN (Ngân hàng Nhà nước Việt Nam) cung cấp tỷ giá hàng ngày. URL feed và định dạng chính xác phải được xác nhận. Tỷ giá VCB (Vietcombank) được các kế toán Việt Nam sử dụng phổ biến như một lựa chọn thay thế; trường ExchangeRates.Source hỗ trợ ghi lại nguồn nào đã được sử dụng.

  5. Kiểm soát truy cập Hangfire dashboard — Web dashboard của Hangfire phải được giới hạn chỉ Quản trị viên Hệ thống và không được truy cập trên các endpoint công khai trong môi trường production.