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

Công cụ tùy chỉnh

Lưu ý: Kiến trúc plugin (mở rộng mã trong môi trường sandbox cho các trường hợp cấu hình không đủ) chưa được quyết định và không được đề cập ở đây. Sẽ được ghi lại riêng khi phương án được chọn.


BPO ERP loại bỏ việc phân nhánh mã theo từng tenant thông qua công cụ tùy chỉnh dựa trên cấu hình. Hành vi đặc thù của tenant — trường tùy chỉnh, quy tắc xác thực và bước quy trình — được biểu diễn dưới dạng metadata lưu trữ và được áp dụng tại runtime bởi nền tảng. Không thay đổi mã; không cần triển khai lại.

Công cụ tùy chỉnh xuất hiện trong Giai đoạn 2. Tuy nhiên, một số cam kết nền tảng phải được xây dựng trong Giai đoạn 1 — được liệt kê rõ ràng trong §6 bên dưới.


2. Tại sao quyết định Giai đoạn 2 ảnh hưởng đến Giai đoạn 1

Phần tiêu đề “2. Tại sao quyết định Giai đoạn 2 ảnh hưởng đến Giai đoạn 1”

Việc cải tiến cơ sở hạ tầng tùy chỉnh vào schema production với dữ liệu tenant thực sẽ rất tốn kém. Ba quyết định phải được xác định trong Giai đoạn 1:

  1. Cột CustomData trên mỗi bảng entity cốt lõi. Thêm cột JSON vào bảng có hàng triệu hàng trong production đòi hỏi migration schema kéo dài. Nó phải có từ migration đầu tiên, dù không có tenant nào điền vào đến Giai đoạn 2.

  2. Điểm hook trước/sau khi lưu trong mỗi domain service. Engine quy tắc xác thực (Giai đoạn 2) cần một điểm gọi nhất quán, được xác định trong mỗi domain service để đưa vào đánh giá của nó. Nếu domain service Giai đoạn 1 được viết không có các hook này, Giai đoạn 2 phải sửa đổi từng service — một refactor rủi ro cao.

  3. Domain event bus (dù chưa có subscriber). Workflow trigger (Giai đoạn 2) lắng nghe domain event (PurchaseOrderSubmitted, InvoiceApproved, v.v.). Nếu domain service Giai đoạn 1 không phát các event này, Giai đoạn 2 phải thêm chúng vào sau, chạm đến mọi command handler.

Không có gì trong số này hiển thị với người dùng trong Giai đoạn 1. Chúng là hạ tầng để Giai đoạn 2 ra mắt suôn sẻ.


Cột JSON (CustomData NVARCHAR(MAX)) trên mỗi bảng entity có phạm vi tenant, được quản lý bởi bảng metadata CustomFieldDefinitions theo loại entity theo tenant.

Phương ánMô tảLý do bị loại
EAV (Entity-Attribute-Value)Mỗi giá trị field là một hàng riêng trong bảng CustomFieldValues chungHiệu năng truy vấn rất kém; join qua bảng EAV chậm và phức tạp; lập index từng giá trị khó
Shadow tablesBảng {Entity}CustomFields riêng cho từng loại entityYêu cầu thay đổi schema (DDL) cho mỗi loại entity mới — phủ nhận mục tiêu “không cần triển khai lại”
Cột JSON (được chọn)CustomData NVARCHAR(MAX) trên mỗi entity; hàm JSON của SQL Server cho xác thực và lập indexKhông thay đổi schema khi thêm field mới; SQL Server hỗ trợ truy vấn JSON_VALUE và index cột tính toán trên đường dẫn JSON cụ thể
CustomFieldDefinitions (Định nghĩa trường tùy chỉnh)
────────────────────────────────────────────────
DefinitionId UNIQUEIDENTIFIER PK
TenantId UNIQUEIDENTIFIER NOT NULL (RLS bắt buộc)
EntityType NVARCHAR(100) NOT NULL -- 'SalesOrder', 'Product', 'Supplier', v.v.
FieldKey NVARCHAR(100) NOT NULL -- khóa dùng trong blob JSON
FieldLabel NVARCHAR(200) NOT NULL -- nhãn hiển thị (có thể bản địa hóa qua i18n key)
DataType NVARCHAR(50) NOT NULL -- 'Text', 'Number', 'Boolean', 'Date', 'List'
IsRequired BIT NOT NULL
DefaultValue NVARCHAR(500) NULL
ListOptions NVARCHAR(MAX) NULL -- JSON array của {value, label} cho loại 'List'
DisplayOrder INT NOT NULL
IsActive BIT NOT NULL
UNIQUE (TenantId, EntityType, FieldKey)

Mỗi bảng entity có phạm vi tenant mang:

CustomData NVARCHAR(MAX) NULL
-- Đối tượng JSON. Khóa phải khớp với giá trị FieldKey đang hoạt động trong CustomFieldDefinitions
-- cho EntityType và TenantId tương ứng.
-- Ví dụ: { "MaKhuVuc": "HCM", "MaPheDuyet": "PD-2026-0042" }

Khi ghi, hook trước-lưu của domain service gọi dịch vụ xác thực CustomField, dịch vụ này:

  1. Lấy các CustomFieldDefinitions đang hoạt động cho loại entity và tenant
  2. Xác thực các trường bắt buộc có mặt
  3. Xác thực kiểu dữ liệu và các ràng buộc cấp field
  4. Trả về lỗi xác thực có cấu trúc (cùng định dạng với lỗi xác thực cốt lõi)

Trường tùy chỉnh không bao giờ được truy cập trực tiếp theo khóa trong mã nền tảng — luôn qua metadata định nghĩa. Điều này đảm bảo mã nền tảng không biết về ngữ nghĩa trường đặc thù của tenant.


Cây điều kiện lưu dưới dạng JSON, được đánh giá bởi engine quy tắc nền tảng tại điểm hook trước-lưu của domain service.

Quy tắc biểu diễn ràng buộc nghiệp vụ trong DSL có cấu trúc — so sánh field, kiểm tra phạm vi, khớp regex, tham chiếu chéo field và tham chiếu đến trường tùy chỉnh. Không có mã thực thi; nền tảng đánh giá cây.

ValidationRules (Quy tắc xác thực)
────────────────────────────────────────────────
RuleId UNIQUEIDENTIFIER PK
TenantId UNIQUEIDENTIFIER NOT NULL (RLS bắt buộc)
EntityType NVARCHAR(100) NOT NULL
RuleName NVARCHAR(200) NOT NULL
Description NVARCHAR(500) NULL
Condition NVARCHAR(MAX) NOT NULL -- cây điều kiện JSON (xem DSL bên dưới)
ErrorMessage NVARCHAR(500) NOT NULL -- hiển thị cho người dùng khi vi phạm; có thể bản địa hóa
IsActive BIT NOT NULL

Điều kiện được tạo từ các toán tử and, ornot, với các nút lá tham chiếu đến field entity hoặc field custom data:

{
"and": [
{ "field": "TotalAmount", "op": "gt", "value": 0 },
{
"or": [
{ "field": "CustomData.MaPheDuyet", "op": "notEmpty" },
{ "field": "TotalAmount", "op": "lte", "value": 5000000 }
]
}
]
}

Các toán tử được hỗ trợ: eq, neq, gt, gte, lt, lte, in, notIn, isEmpty, notEmpty, matches (regex), contains.

Tham chiếu field dùng ký hiệu chấm: TotalAmount tham chiếu field cốt lõi; CustomData.MaPheDuyet tham chiếu trường tùy chỉnh.


Định nghĩa quy trình lưu dưới dạng cấu hình. Nền tảng cung cấp engine thực thi; tenant cung cấp các bước.

Một quy trình được kích hoạt bởi domain event trên loại entity cụ thể. Nó thực thi một chuỗi bước — phê duyệt, thông báo, cập nhật field — mỗi bước có thể có điều kiện bảo vệ sử dụng cùng DSL với quy tắc xác thực.

WorkflowDefinitions (Định nghĩa quy trình)
────────────────────────────────────────────────
WorkflowId UNIQUEIDENTIFIER PK
TenantId UNIQUEIDENTIFIER NOT NULL (RLS bắt buộc)
WorkflowName NVARCHAR(200) NOT NULL
TriggerEntity NVARCHAR(100) NOT NULL -- ví dụ: 'PurchaseOrder'
TriggerEvent NVARCHAR(100) NOT NULL -- ví dụ: 'Submitted', 'Approved', 'Created'
IsActive BIT NOT NULL
WorkflowSteps (Bước quy trình)
────────────────────────────────────────────────
StepId UNIQUEIDENTIFIER PK
WorkflowId UNIQUEIDENTIFIER NOT NULL FK → WorkflowDefinitions
StepOrder INT NOT NULL
StepType NVARCHAR(50) NOT NULL
-- 'Approval' → yêu cầu vai trò được đặt tên phê duyệt trước khi tiếp tục
-- 'Notification' → gửi thông báo email/trong-ứng-dụng đến vai trò hoặc người dùng
-- 'FieldUpdate' → đặt giá trị field trên entity
Config NVARCHAR(MAX) NOT NULL -- JSON; cấu trúc tùy theo StepType
Condition NVARCHAR(MAX) NULL -- cây điều kiện JSON; bước bị bỏ qua nếu đánh giá false
// Bước phê duyệt
{ "approverRole": "TruongPhongMuaHang", "timeoutHours": 48, "escalateTo": "GiamDoc" }
// Bước thông báo
{ "recipientRole": "KeToan", "template": "hoa_don_duoc_duyet", "channels": ["email", "in-app"] }
// Bước cập nhật field
{ "field": "Status", "value": "DuocDuyetThanhToan" }

Mọi lệnh nghiệp vụ có thể kích hoạt quy trình phải phát domain event sau khi lưu thành công. Giai đoạn 1 phải định nghĩa và phát các event này dù chưa có subscriber quy trình nào đến Giai đoạn 2:

EntityCác event
PurchaseOrderPurchaseOrderCreated, PurchaseOrderSubmitted, PurchaseOrderApproved, PurchaseOrderCancelled
SupplierInvoiceSupplierInvoiceReceived, SupplierInvoiceMatched, SupplierInvoiceApproved
SalesOrderSalesOrderCreated, SalesOrderConfirmed, SalesOrderCancelled
CustomerInvoiceCustomerInvoiceIssued, CustomerInvoicePaid
GoodsReceiptGoodsReceiptPosted
StockAdjustmentStockAdjustmentPosted

Những điều sau phải xuất hiện trong Giai đoạn 1 dù engine tùy chỉnh xuất hiện trong Giai đoạn 2:

Cam kếtPhạm viLý do
Cột CustomData NVARCHAR(MAX) NULL trên mỗi bảng entity cốt lõi có phạm vi tenantEF Core migrationsThêm vào schema production với dữ liệu thực yêu cầu backfill migration và khóa bảng — tránh được nếu có từ đầu
Bảng CustomFieldDefinitions, ValidationRules, WorkflowDefinitions, WorkflowStepsEF Core migrationsSchema phải tồn tại để engine Giai đoạn 2 ghi vào; giao diện quản trị có thể là Giai đoạn 2
Interface IPreSaveHookIPostSaveHook trong domain serviceDomain layer (Core.Domain)Engine quy tắc xác thực Giai đoạn 2 cần điểm gọi nhất quán
Domain event bus + phát event trong mọi command handler Giai đoạn 1Core platformSubscription trigger quy trình trong Giai đoạn 2 cần event để tồn tại
Pipeline TenantId claim → ITenantContextDbContextCore platformCần cho cô lập trường tùy chỉnh và quy tắc xác thực (giống RLS; đã cam kết trong multitenancy.md)