Sau khi cài đặt EzyArticle, một màn hình quản trị có thể tham gia hệ thống bình luận dữ liệu bằng cách khai báo thực thể đang được hiển thị. Để có trải nghiệm đầy đủ hơn, plugin quản lý dữ liệu nên cung cấp thêm bộ tải thông tin thực thể để EzyArticle lấy tên, biểu tượng và đường dẫn của dữ liệu khi hiển thị danh sách thảo luận.
Quá trình tích hợp gồm hai phần chính:
  • Khai báo thực thể hiện tại trên giao diện quản trị.
  • Đăng ký bộ tải thông tin thực thể ở phía máy chủ.
Phần thứ nhất là điều kiện tối thiểu để tạo và xem bình luận. Phần thứ hai giúp các cuộc thảo luận hiển thị thân thiện và hoạt động tốt trong phạm vi toàn hệ thống.

Điều kiện trước khi tích hợp

Ứng dụng cần đáp ứng các điều kiện sau:
  • EzyArticle đã được cài đặt và kích hoạt trong trang quản trị.
  • Trang sử dụng layout quản trị có khu vực thanh công cụ bên phải.
  • Người dùng đã đăng nhập.
  • Người dùng có quyền sử dụng tính năng quản lý bình luận.
  • Dữ liệu cần bình luận đã được lưu và có mã định danh dạng số dương.
EzyArticle tự nạp giao diện, stylesheet, JavaScript và các thông điệp đa ngôn ngữ cần thiết vào trang quản trị. Plugin tích hợp không cần tự xây dựng hộp thoại bình luận hay gọi các API thảo luận.

Chọn mã loại dữ liệu

Mỗi loại dữ liệu cần có một mã nhận diện ổn định, ví dụ:
inventory_products
sales_orders
support_tickets
Mã loại dữ liệu được sử dụng để:
  • Liên kết cuộc thảo luận với dữ liệu.
  • Lọc bình luận của dữ liệu hiện tại.
  • Tìm bộ tải thông tin tương ứng.
  • Khôi phục tên và đường dẫn của dữ liệu khi xem thảo luận toàn hệ thống.
Nên sử dụng một hằng số dùng chung giữa phía máy chủ và giao diện:
public final class EntityTypes {

    public static final String PRODUCT = "inventory_products";

    private EntityTypes() {}
}
Không nên dùng tên hiển thị như Product hoặc Sản phẩm, vì tên đó có thể thay đổi theo ngôn ngữ. Mã loại dữ liệu phải ổn định trong toàn bộ vòng đời ứng dụng.

Khai báo dữ liệu hiện tại trên giao diện

EzyArticle đọc dữ liệu hiện tại từ biến JavaScript:
ezyadmin.taggableEntity
Tên biến này được dùng chung cho các tính năng làm việc với thực thể trên thanh công cụ quản trị. Để cho phép bình luận một sản phẩm, trang chi tiết hoặc trang chỉnh sửa có thể khai báo:
<script>
ezyadmin.taggableEntity = {
    id: product.id,
    code: product.code,
    displayName: product.name,
    type: 'inventory_products',
    url: '/inventory/products/' + product.id + '/edit'
};
</script>
Trong template được render phía máy chủ, dữ liệu thực tế có thể được đưa vào như sau:
<script th:inline="javascript">
ezyadmin.taggableEntity = {
    id: /*[[]]*/ 0,
    code: /*[[]]*/ '',
    displayName: /*[[]]*/ '',
    type: 'inventory_products',
    url: /*[[@{/inventory/products/{id}/edit(id=)}]]*/ ''
};
</script>
Các thuộc tính có ý nghĩa như sau:
Thuộc tínhBắt buộcÝ nghĩa
idCóMã định danh dạng số dương của dữ liệu
typeCóMã loại dữ liệu, phải khớp với phía máy chủ
displayNameNên cóTên thân thiện hiển thị trên giao diện
codeKhôngMã nghiệp vụ như SKU hoặc mã đơn hàng
urlNên cóĐường dẫn mở trang chi tiết hoặc chỉnh sửa
EzyArticle cũng chấp nhận entityType thay cho type, và có thể đọc đường dẫn từ một trong các thuộc tính url, link, uri hoặc href. Tuy nhiên, nên thống nhất sử dụng type và url để mã tích hợp dễ đọc.
Sau khi biến này được gán, công cụ bình luận sẽ tự động:
  • Lấy số cuộc thảo luận đang mở của dữ liệu.
  • Hiển thị số lượng trên huy hiệu.
  • Tải danh sách thảo luận khi người dùng mở công cụ.
  • Cho phép tạo chủ đề mới.
  • Gửi type và id khi lưu chủ đề.
  • Hiển thị tên và liên kết xem dữ liệu.

Chỉ khai báo dữ liệu đã tồn tại

Không nên bật bình luận trên màn hình tạo mới khi dữ liệu chưa được lưu. Khi chưa có mã định danh, hãy đặt thực thể hiện tại thành null:
ezyadmin.taggableEntity = null;
Sau khi thao tác lưu thành công, ứng dụng có thể chuyển sang trang chỉnh sửa của bản ghi vừa tạo. Tại đó, taggableEntity được khai báo bằng mã định danh chính thức.
Không nên dùng mã tạm, số âm hoặc giá trị 0. Máy chủ chỉ chấp nhận mã định danh lớn hơn 0.

Tích hợp trên trang danh sách

Trang danh sách không có một thực thể cố định. Trong trường hợp này, ứng dụng có thể cập nhật dữ liệu hiện tại khi người dùng chọn một dòng:
function setCurrentProduct(product) {
    ezyadmin.taggableEntity = {
        id: product.id,
        code: product.code,
        displayName: product.name,
        type: 'inventory_products',
        url: '/inventory/products/' + product.id + '/edit'
    };
}
Ví dụ với checkbox:
$('.product-checkbox').on('change', function() {
    var productId = Number(this.value);

    if (this.checked) {
        setCurrentProduct(productById[productId]);
        return;
    }

    var checkedIds = $('.product-checkbox:checked')
        .map(function() {
            return Number(this.value);
        })
        .get();

    if (!checkedIds.length) {
        ezyadmin.taggableEntity = null;
        return;
    }

    setCurrentProduct(
        productById[checkedIds[checkedIds.length - 1]]
    );
});
Nếu có nhiều dòng được chọn, ứng dụng cần xác định rõ thực thể nào là thực thể hiện tại. Một cách đơn giản là chọn dòng được đánh dấu gần nhất.
Việc thay đổi taggableEntity không tự mở hộp thoại. Khi người dùng bấm biểu tượng bình luận, EzyArticle sẽ đọc giá trị mới nhất và tải cuộc thảo luận của thực thể tương ứng.

Đăng ký bộ tải thông tin thực thể

Khai báo phía trình duyệt đủ để bình luận trên trang hiện tại. Tuy nhiên, khi người dùng xem các phạm vi “Toàn hệ thống”, “Do tôi tạo” hoặc “Nhắc đến tôi”, EzyArticle cần dựng lại thông tin của nhiều loại dữ liệu khác nhau.
Plugin nên triển khai CommonEntityFetcher để cung cấp thông tin đó:
import org.youngmonkeys.ezyplatform.fetcher.CommonEntityFetcher;
import org.youngmonkeys.ezyplatform.model.CommonEntityModel;

import java.util.Collection;
import java.util.Map;

public class ProductEntityFetcher
    implements CommonEntityFetcher {

    private final ProductService productService;

    public ProductEntityFetcher(ProductService productService) {
        this.productService = productService;
    }

    @Override
    public String getEntityType() {
        return EntityTypes.PRODUCT;
    }

    @Override
    public CommonEntityModel getEntityById(long entityId) {
        ProductModel product = productService.getProductById(entityId);

        if (product == null) {
            return CommonEntityModel
                .defaultEntity(entityId, getEntityType());
        }

        return toEntityModel(product);
    }

    @Override
    public Map<Long, CommonEntityModel> getEntityMapByIds(
        Collection<Long> entityIds
    ) {
        return productService
            .getProductMapByIds(entityIds)
            .entrySet()
            .stream()
            .collect(
                java.util.stream.Collectors.toMap(
                    Map.Entry::getKey,
                    entry -> toEntityModel(entry.getValue())
                )
            );
    }

    private CommonEntityModel toEntityModel(ProductModel product) {
        return CommonEntityModel.builder()
            .id(product.getId())
            .code(product.getCode())
            .name(getEntityType())
            .displayName(product.getName())
            .url(
                "/inventory/products/"
                    + product.getId()
                    + "/edit"
            )
            .icon("fas fa-box")
            .build();
    }
}
Fetcher cần được đăng ký thành singleton trong container của plugin quản trị, chẳng hạn:
@EzySingleton
public class AdminProductEntityFetcher
    extends ProductEntityFetcher {

    public AdminProductEntityFetcher(ProductService productService) {
        super(productService);
    }
}
Khi khởi tạo, nền tảng tìm tất cả singleton triển khai CommonEntityFetcher và lập chỉ mục theo loại dữ liệu. Vì vậy, giá trị trả về từ getEntityType() phải trùng hoàn toàn với type đã khai báo trên trình duyệt.

Thông tin mà fetcher nên trả về

CommonEntityModel có thể chứa các trường:
CommonEntityModel.builder()
    .id(entityId)
    .code(entityCode)
    .name(entityType)
    .displayName(displayName)
    .url(detailsUrl)
    .icon(iconClass)
    .properties(additionalProperties)
    .build();
Trong đó:
  • id là mã định danh của dữ liệu.
  • code là mã nghiệp vụ dễ nhận biết.
  • name thường được dùng để chứa loại dữ liệu.
  • displayName là tên hiển thị cho người dùng.
  • url là đường dẫn đến trang quản trị của dữ liệu.
  • icon là lớp CSS của biểu tượng.
  • properties chứa thông tin mở rộng nếu các tính năng khác cần sử dụng.
Đối với bình luận dữ liệu, displayName và url là hai thông tin quan trọng nhất. Nếu không có displayName, giao diện phải quay về cách hiển thị kỹ thuật dựa trên loại và mã định danh.

Tối ưu phương thức lấy dữ liệu theo lô

Khi tải danh sách thảo luận, EzyArticle nhóm các mã định danh theo loại dữ liệu rồi gọi:
getEntityMapByIds(Collection<Long> entityIds)
Vì vậy, không nên triển khai phương thức này bằng cách truy vấn cơ sở dữ liệu riêng cho từng mã:
// Không nên: có thể tạo ra N truy vấn.
for (long id : entityIds) {
    productService.getProductById(id);
}
Thay vào đó, nên sử dụng một truy vấn theo tập mã định danh:
Map<Long, ProductModel> productById =
    productService.getProductMapByIds(entityIds);
Cách này tránh vấn đề N+1 khi một trang có nhiều cuộc thảo luận.
Bản đồ kết quả không bắt buộc phải chứa dữ liệu cho mọi mã được yêu cầu. Nếu một thực thể đã bị xóa, có thể bỏ qua nó hoặc trả về mô hình mặc định:
CommonEntityModel.defaultEntity(
    entityId,
    EntityTypes.PRODUCT
);
Cuộc thảo luận vẫn được giữ lại, nhưng giao diện có thể không còn tên và đường dẫn đầy đủ của dữ liệu cũ.

Trường hợp một fetcher hỗ trợ nhiều loại dữ liệu

Nếu các loại dữ liệu có cùng cách tải, fetcher có thể khai báo nhiều loại:
@Override
public String[] getEntityTypes() {
    return new String[] {
        "inventory_products",
        "inventory_product_variants"
    };
}
Khi đó, phương thức tải cần phân biệt được loại dữ liệu theo thiết kế của ứng dụng. Nếu hai loại có repository, đường dẫn hoặc mô hình khác nhau đáng kể, nên tạo fetcher riêng để mã nguồn rõ ràng hơn.
Khi có nhiều fetcher cùng đăng ký một loại, nền tảng sử dụng mức ưu tiên để chọn:
@Override
public int getPriority() {
    return 10;
}
Chỉ nên thay đổi mức ưu tiên khi plugin thực sự cần ghi đè cách cung cấp thông tin của một loại dữ liệu đã tồn tại.

Kiểm tra quyền truy cập

Các API bình luận yêu cầu phiên đăng nhập quản trị và quyền sử dụng chức năng quản lý bình luận. Tuy nhiên, plugin sở hữu dữ liệu vẫn cần quan tâm đến quyền truy cập đối tượng.
Ví dụ, nếu một quản trị viên chỉ được xem sản phẩm thuộc một kho cụ thể, plugin không nên cung cấp đường dẫn hoặc thông tin nhạy cảm của sản phẩm ngoài phạm vi đó.
Cần kiểm tra quyền ở ít nhất hai nơi:
  • Controller của trang chi tiết hoặc chỉnh sửa dữ liệu.
  • Service hoặc fetcher trả thông tin thực thể cho hệ thống.
Việc một người dùng biết entityType và entityId không nên đồng nghĩa với quyền xem toàn bộ dữ liệu tương ứng.

Luồng hoạt động sau khi tích hợp

Khi người dùng mở một trang dữ liệu, luồng xử lý diễn ra như sau:
  1. Trang khai báo ezyadmin.taggableEntity.
  2. Giao diện EzyArticle đọc loại và mã định danh.
  3. Hệ thống lấy số cuộc thảo luận đang mở.
  4. Người dùng mở công cụ bình luận.
  5. Các chủ đề gắn với dữ liệu được tải theo trang.
  6. Khi tạo chủ đề, trình duyệt gửi loại dữ liệu, mã định danh, nội dung và danh sách người được nhắc đến.
  7. EzyArticle lưu chủ đề và liên kết với dữ liệu.
  8. Khi hiển thị danh sách toàn hệ thống, fetcher cung cấp tên, biểu tượng và đường dẫn cho từng thực thể.
Plugin tích hợp không cần tự quản lý trạng thái mở, đã giải quyết, danh sách phản hồi hay thông báo nhắc tên. Những phần này do EzyArticle xử lý.

Những lỗi tích hợp thường gặp

Biểu tượng bình luận xuất hiện nhưng không tạo được chủ đề
Kiểm tra taggableEntity.id. Giá trị phải là số dương và dữ liệu phải được lưu trước.
Không thấy bình luận đã tạo trên dữ liệu hiện tại
Kiểm tra type có được sử dụng nhất quán hay không. Chỉ một khác biệt nhỏ như inventory_product và inventory_products cũng tạo thành hai mục tiêu khác nhau.
Danh sách toàn hệ thống chỉ hiện loại và mã dữ liệu
Fetcher chưa được đăng ký, trả về thiếu displayName, hoặc getEntityType() không khớp với mã phía giao diện.
Liên kết mở dữ liệu không hoạt động
Kiểm tra trường url trong taggableEntity và trong CommonEntityModel. Với loại dữ liệu tùy chỉnh, nên cung cấp URL rõ ràng thay vì phụ thuộc vào đường dẫn mặc định.
Danh sách thảo luận tải chậm
Kiểm tra getEntityMapByIds. Phương thức này nên sử dụng truy vấn theo lô, không nên tải từng bản ghi trong vòng lặp.
Người dùng không gọi được API bình luận
Kiểm tra trạng thái đăng nhập và quyền quản lý bình luận của tài khoản quản trị.

Danh sách kiểm tra tích hợp

Trước khi hoàn tất, hãy xác nhận:
  • EzyArticle đang hoạt động trong module quản trị.
  • Trang sử dụng layout có thanh công cụ bên phải.
  • Mỗi loại dữ liệu có một mã ổn định.
  • taggableEntity.id là số dương.
  • taggableEntity.type khớp với getEntityType().
  • Màn hình tạo mới đặt taggableEntity thành null.
  • Trang danh sách cập nhật thực thể khi lựa chọn thay đổi.
  • Fetcher được đăng ký thành singleton.
  • Fetcher trả về displayName và url.
  • Phương thức lấy nhiều thực thể sử dụng truy vấn theo lô.
  • Controller và service vẫn thực thi chính sách phân quyền của plugin.

Kết luận

Để một loại dữ liệu có thể được bình luận trong EzyArticle, tích hợp tối thiểu chỉ cần khai báo mã định danh và loại dữ liệu trên giao diện. Tuy nhiên, việc bổ sung CommonEntityFetcher là bước nên làm để tên, biểu tượng và liên kết của dữ liệu được khôi phục chính xác trong mọi phạm vi thảo luận.
Mô hình tích hợp này không giới hạn ở bài viết hay chuyên mục. Sản phẩm, đơn hàng, yêu cầu hỗ trợ hoặc bất kỳ dữ liệu quản trị nào có mã định danh ổn định đều có thể tham gia cùng một hệ thống trao đổi nội bộ.