EzyPlatform quản lý thông báo thế nào?

Thông báo (notification) cũng là một trong những tính năng rất cơ bản mà phần mềm nào cũng có để thông báo một thứ gì đó cho người dùng và quản trị viên, vậy nên EzyPlatform cũng đã đóng gói phần này để các nhà phát triển yên tâm sử dụng.Thiết kế bảng Sẽ có hai bảng tham gia vào nghiệp vụ thông báo này: ezy_notifications: Bảng này chứa thông tin của các thông báo. ezy_notification_receivers: Bảng này chứa thông tin người nhận thông báo. Một thông báo có thể gửi đến nhiều người. Một người cũng có thể nhận được nhiều thông báo khác nhau. Mã nguồn SQL của hai bảng này như sau: CREATE TABLE IF NOT EXISTS `ezy_notifications` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `notification_type` varchar(25) COLLATE utf8mb4_unicode_520_ci, `title` varchar(300) COLLATE utf8mb4_unicode_520_ci NOT NULL, `content` varchar(3000) COLLATE utf8mb4_unicode_520_ci NOT NULL, `deep_link` varchar(300) COLLATE utf8mb4_unicode_520_ci, `icon_image` varchar(120) COLLATE utf8mb4_unicode_520_ci, `from_admin_id` bigint unsigned, `from_user_id` bigint unsigned, `status` varchar(25) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'DRAFT', `created_at` datetime NOT NULL, PRIMARY KEY (`id`), INDEX `index_notification_type` (`notification_type`), INDEX `index_from_admin_id` (`from_admin_id`), INDEX `index_from_user_id` (`from_user_id`), INDEX `index_status` (`status`), INDEX `index_created_at` (`created_at`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; CREATE TABLE IF NOT EXISTS `ezy_notification_receivers` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `notification_id` bigint unsigned NOT NULL, `to_admin_id` bigint unsigned, `to_user_id` bigint unsigned, `confidence_level` varchar(25) COLLATE utf8mb4_unicode_520_ci, `important_level` varchar(25) COLLATE utf8mb4_unicode_520_ci, `status` varchar(25) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'SENT', `sent_at` datetime NOT NULL, `received_at` datetime, `read_at` datetime, PRIMARY KEY (`id`), UNIQUE KEY `key_notification_to_admin_id` (`notification_id`, `to_admin_id`), UNIQUE KEY `key_notification_to_user_id` (`notification_id`, `to_user_id`), INDEX `index_notification_id` (`notification_id`), INDEX `index_to_admin_id` (`to_admin_id`), INDEX `index_to_user_id` (`to_user_id`), INDEX `index_confidence_level` (`confidence_level`), INDEX `index_important_level` (`important_level`), INDEX `index_status` (`status`), INDEX `index_sent_at` (`sent_at`), INDEX `index_received_at` (`received_at`), INDEX `index_read_at` (`read_at`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; Một số trường bạn có thể lưu ý đó là: notification_type: Là loại thông báo, ví dụ order_approved, order_rejected. title: Là tiêu đề của thông báo, bởi vì trên giao diện thông báo thường được hiển thị trong không gian hẹp nên bạn hãy giữ cho tiêu đề thật ngắn gọn. content: Là nội dung của thông báo, bạn cũng nên giữ cho nội dung của thông báo ngắn gọn. deep_link: Là đường dẫn sẽ được mở khi click vào thông báo, ví dụ bạn có thể dẫn người dùng đến hòm thư khi họ xem thông báo chẳng hạn. icon_image: Bạn sẽ thường dùng font icon để hiển thị. confidence_level: Là mức độ bảo mật của thông báo để người đọc có thể cảnh giác, thông thường bạn sẽ không dùng đến trường này. important_level: Là mức độ quản trọng của thông báo để người đọc có thể cảnh giác, thông thường bạn sẽ không dùng đến trường này. Thiết kế lớp Đây là một thiết kế lớp tương đối phức tạp với sự tham ra của rất nhiều lớp khác nhau: Notfication: Lớp này ánh xạ đến bảng ezy_notifications. NotificationReceiver: Lớp này ánh xạ đến bảng ezy_notification_receivers. NotificationRepository: Giao diện để truy vấn bảng ezy_notifications. NotificationReceiverRepository: Giao diện để truy vấn bảng ezy_notification_receivers. NotificationService: Giao diện cung cấp các hàm cơ sở cho nghiệp vụ thông báo. DefaultNotificationService: Lớp cài đặt các hàm cơ sở cho nghiệp vụ thông báo. AdminNotificationService: Lớp thừa kế DefaultNotificationService để sử dụng cho admin. WebNotificationService: Lớp thừa kế DefaultNotificationService để sử dụng cho web. PaginationNotificationRepository: Lớp cung cấp phân trang dữ liệu cho bảng ezy_notifications. AdminPaginationNotificationRepository: Lớp thừa kế PaginationNotificationRepository và sử dụng cho admin. WebPaginationNotificationRepository: Lớp thừa kế PaginationNotificationRepository và sử dụng cho web. PaginationNotificationService: Lớp cung cấp phân trang dữ liệu các thông báo. AdminPaginationNotificationService: Lớp thừa kế PaginationNotificationService và sử dụng cho admin. AdminPaginationNotificationService: Lớp thừa kế PaginationNotificationService và sử dụng cho web. Lớp AdminNotificationControllerService: Lớp cung cấp các hàm tổng hợp dữ liệu cho nghiệp vụ thông báo dùng cho admin. Lớp WebNotificationControllerService: Lớp cung cấp các hàm tổng hợp dữ liệu cho nghiệp vụ thông báo dùng cho web. AdminNotificationsController: Lớp cung cấp các API cho tính năng thông báo phía admin. WebApiNotificationController: Lớp cung cấp các API cho tính năng thông báo phía web, tuy nhiên bạn sẽ cần phải thừa kế lại lớp này và khai báo @Controller("/api/v1") thì mới có API để sử dụng. Sử dụng trong thực tế Bạn có thể tạo tài khoản trên ezyplatform.com, tạo một dự án và yêu cầu xem xét, bạn có thể nhận được các thông báo thế này: Như bạn có thể thấy thông báo cũng chỉ hiển thị icon, tiêu đề và thời gian trong một popup nhỏ, vậy nên chúng ta cần lựa chọn một tiêu đề ngắn gọn và đủ hiểu. Tổng kết Tính năng thông báo mặc dù cơ bản nhưng lại cần một thiết kế tương đối phức tạp. EzyPlatform đã đóng gói khá kỹ tính năng này với tương đối nhiều lớp để các nhà phát triển có thể sử dụng 1 cách dễ dàng.

EzyPlatform đã hoàn thành dự án vận chuyển hàng xuyên biên giới trong 6 ngày thế nào? Phần 2

Phần tạo mã vận đơn không chỉ phức tạp ở phần nhập liệu rồi in ấn mà còn phức tạp ở logic xử lý ở bên trong do yêu cầu của khách hàng đó là: Một mã vận đơn chỉ được tối đa 10 kg do yêu cầu của hải quan vậy nên nếu lúc nhập mà quá 10 kg thì phải tự động chia nhỏ thành các mã vận đơn khác nhau, ví dụ nếu nhập vào là 31kg thì tự động phải tách thành các vận đơn 10, 10, 10 và 1kg. Giá tiền sẽ cần random từ 2 đến 100. Vận đơn được gắn ngẫu nhiên vào một kho nào đó ở đầu Trung Quốc. Thông tin người nhận cũng sẽ được gắn ngẫu nhiên. Giải quyết vấn đề tuỳ biến cài đặt Yêu cầu của khách hàng có thể thay đổi từ 10kg lên 20kg hay các thông số khác cũng vậy nên tốt hơn hết là mình nên đưa vào phần cài đặt để tuỳ chỉnh khi cần: Cho phép tuỳ chỉnh số cân tối đa của vận đơn. Cho phép cài đặt khoảng giá random. Cho phép cài đặt loại tiền. Cho phép cài đặt quốc gia đầu vào để lấy danh sách kho ngẫu nhiên. Và kết quả là một giao diện cài đặt thế này: Giải quyết vấn đề lấy ngẫu nhiên dữ liệu Tạo ngẫu nhiên giá tiền thì đơn giản rồi, nhưng làm thế nào để lấy được ngẫu nhiên kho và người nhận? Cách 1: Lấy ngẫu nhiên từ min id và max id của bản ghi kho và người dùng sau đó random: Cách này cũng ổn nhưng giả sử có kho hoặc người dùng đã bị xoá thì lại phải random nhiều lần, tiêu cực có thể random mãi. Cách 2: Truy vấn toàn bộ kho ở quốc gia đầu vào và người dùng sau đó random: Cách này chỉ phù hợp với bộ dữ liệu nhỏ, danh sách người nhận mà khách hàng gửi lên đến vài chục nghìn. Cách 3: Lưu cache thông tin kho và người dùng: Cách này là phù hợp vì mặc dù dữ liệu nhiều nhưng dung lượng nhỏ, không cần phải truy vấn cơ sở dữ liệu nhiều lần nên rất nhanh. Mình đã tạo ra hai lớp lưu cache như sau: Lớp lưu cache các kho từ quốc gia đầu vào Mình tạo ra một lớp có tên AdminEzyDeliveryShopWarehouseService với mã nguồn như sau: public class AdminEzyDeliveryShopWarehouseService extends EzyDeliveryShopWarehouseService { private final AdminScheduler scheduler; private final AdminEzyDeliverySettingService settingService; private final AdminShopService shopService; private final AdminEzyDeliveryWarehouseRepository warehouseRepository; private final AtomicBoolean deliveryOrderInputWarehouseIdCachingStarted = new AtomicBoolean(); private final AtomicReference<Long> lastDeliveryOrderWarehouseShopId = new AtomicReference<>(); private final AtomicReference<Long> lastDeliveryOrderWarehouseCountryId = new AtomicReference<>(); private final AtomicReference<LocalDateTime> lastDeliveryOrderWarehouseUpdatedAt = new AtomicReference<>(); private final List<Long> cachedDeliveryOrderWarehouseIds = new ArrayList<>(); public void startCacheDeliveryOrderWarehouseIdsSchedule() { if (deliveryOrderInputWarehouseIdCachingStarted.compareAndSet(false, true)) { scheduler.scheduleAtFixRate( this::cacheDeliveryOrderWarehouseIds, 0, 5, TimeUnit.SECONDS ); } } @Override public long randomInputDeliveryOrderFromWarehouseId() { synchronized (cachedDeliveryOrderWarehouseIds) { int index = ThreadLocalRandom .current() .nextInt(cachedDeliveryOrderWarehouseIds.size()); return cachedDeliveryOrderWarehouseIds.get(index); } } private void cacheDeliveryOrderWarehouseIds() { long countryId = settingService.getInputDeliveryOrderCountryId(); if (countryId <= 0) { return; } long shopId = shopService.getDefaultShopId(); if (shopId <= 0) { return; } AdminShopWarehouseUpdatedAtResult result = warehouseRepository .findLastUpdatedAtByShopIdAndCountryIdOrderByUpdatedAtDesc( shopId, countryId ); if (result == null) { return; } Long lastShopId = lastDeliveryOrderWarehouseShopId.get(); Long lastCountryId = lastDeliveryOrderWarehouseCountryId.get(); LocalDateTime lastTime = lastDeliveryOrderWarehouseUpdatedAt.get(); LocalDateTime updatedAt = result.getUpdatedAt(); boolean needToCache = lastShopId == null || !lastShopId.equals(shopId) || lastCountryId == null || !lastCountryId.equals(countryId) || lastTime == null || updatedAt.isAfter(lastTime); if (needToCache) { doCacheDeliveryOrderWarehouseIds(shopId, countryId); lastDeliveryOrderWarehouseShopId.set(shopId); lastDeliveryOrderWarehouseCountryId.set(countryId); lastDeliveryOrderWarehouseUpdatedAt.set(updatedAt); } } private void doCacheDeliveryOrderWarehouseIds(long shopId, long countryId) { List<Long> warehouseIds = newArrayList( warehouseRepository.findShopWarehouseIdsByShopIdAndCountryId( shopId, countryId ), IdResult::getId ); synchronized (cachedDeliveryOrderWarehouseIds) { cachedDeliveryOrderWarehouseIds.clear(); cachedDeliveryOrderWarehouseIds.addAll(warehouseIds); } } } Ở đây khi có sự thay đổi của quốc gia đầu vào hay một kho mới được thêm sẽ dẫn đến cập nhật lại cache. Còn khi sử dụng sẽ chỉ cần gọi hàm randomInputDeliveryOrderFromWarehouseId để lấy ra id của một kho ngẫu nhiên. Lớp lưu cache danh sách người nhận hàng Mình cũng tạo ra một lớp có tên AdminEzyDeliveryReceiverService với mã nguồn như sau: public class AdminEzyDeliveryReceiverService extends EzyDeliveryReceiverService { private final AdminScheduler scheduler; private final AdminEventHandlerManager eventHandlerManager; private final AdminUserService userService; private final AdminEzyDeliveryUserRoleRepository userRoleRepository; private final AtomicBoolean fakeDeliveryOrderReceiversCachingStarted = new AtomicBoolean(); private final AtomicReference<LocalDateTime> lastFakeDeliveryOrderReceiverTime = new AtomicReference<>(); private final List<EzyDeliveryReceiverModel> cachedFakeDeliveryOrderReceivers = new ArrayList<>(); public void startFakeDeliveryReceiversSchedule() { if (fakeDeliveryOrderReceiversCachingStarted.compareAndSet(false, true)) { scheduler.scheduleAtFixRate( this::cacheFakeDeliveryReceivers, 0, 5, TimeUnit.SECONDS ); } } @Override public EzyDeliveryReceiverModel randomFakeDeliveryOrderReceiver() { synchronized (cachedFakeDeliveryOrderReceivers) { int index = ThreadLocalRandom .current() .nextInt(cachedFakeDeliveryOrderReceivers.size()); return cachedFakeDeliveryOrderReceivers.get(index); } } private void cacheFakeDeliveryReceivers() { AdminEzyDeliveryUserRoleCreatedAtResult result = userRoleRepository .findLastFakeReceiverRoleCreatedAt(); if (result == null) { return; } LocalDateTime lastTime = lastFakeDeliveryOrderReceiverTime.get(); LocalDateTime createdAt = result.getCreatedAt(); boolean needToCache = lastTime == null || createdAt.isAfter(lastTime); if (needToCache) { doCacheFakeDeliveryReceivers(); lastFakeDeliveryOrderReceiverTime.set(createdAt); } } private void doCacheFakeDeliveryReceivers() { List<Long> userIds = newArrayList( userRoleRepository.findFakeReceiverIds(), IdResult::getId ); List<UserModel> users = userService.getUserListByIds(userIds); Map<Long, String> addressByUserId = eventHandlerManager .handleEvent( INTERNAL_EVENT_NAME_FETCH_CUSTOMER_ADDRESS_MAP_BY_ID, userIds ); if (addressByUserId == null) { addressByUserId = Collections.emptyMap(); } Map<Long, String> addressByUserIdFinal = addressByUserId; List<EzyDeliveryReceiverModel> receivers = newArrayList( users, it -> EzyDeliveryReceiverModel.builder() .displayName(it.getName()) .phoneNumber(it.getPhone()) .email(it.getEmail()) .address( addressByUserIdFinal.getOrDefault( it.getId(), UNKNOWN ) ) .build() ); synchronized (cachedFakeDeliveryOrderReceivers) { cachedFakeDeliveryOrderReceivers.clear(); cachedFakeDeliveryOrderReceivers.addAll(receivers); } } } Khi có một người dùng với quyền fake_receiver được thêm mới sẽ dẫn đến cache được load lại. Bạn cần lưu ý rằng mình sẽ không lưu toàn bộ danh sách người dùng vì như thế có thể rất nhiều và có thể vi phạm chính sách bảo mật dữ liệu của người dùng thật. Khi cần sử dụng sẽ chỉ đơn giản là gọi hàm randomFakeDeliveryOrderReceiver để lấy ra ngẫu nhiên một người nhận hàng mà thôi. Rõ ràng là một dự án thực tế, đặc biệt là trong dự án liên quan đến logistics thế này thì yêu cầu khách hàng đặt ra là phức tạp và tỉ mỉ, nếu không có sẵn một kiến trúc mã nguồn tốt thì rất khó để đáp ứng được nhu cầu của khách hàng.

EzyPlatform đã hoàn thành dự án vận chuyển hàng xuyên biên giới trong 6 ngày thế nào? Phần 1

Khách hàng tìm đến EzyPlatform là người đã có hệ thống kho bãi ở cả Trung Quốc lẫn Việt Nam. Để tối ưu được thời gian vận chuyển hàng hoá từ TQ khách hàng cần một hệ thống với các tính năng như sau: Nhập được vận đơn vào hệ thống. Tạo được các gói hàng từ các đơn giao vận. Quản lý được danh sách các xe hàng. Có khả năng in ấn các đơn giao và gói hàng để gắn vào các gói hàng và bao. Tạo được tập tin excel để khai báo hải quan. Quản trị được quản trị viên, ai có quyền gì thì xem được cái đó. Phải nhập được danh sách người nhận hàng để khi nhập vận đơn sẽ gắn thông tin người nhận vào. Vì hệ thống dùng cho cả TQ lần VN nên cần đa ngôn ngữ. Cần triển khai trước trên hạ tầng máy chủ 2CPU, 2GB RAM. EzyPlatform đã làm thế nào? Với EzyPlatform bài toán này không có gì phức tạp vì: Với ecommerce plugin đã cung cấp sẵn các tính năng quản lý vận đơn rồi. EzyPlatform cũng đã cung cấp các tính năng phân trang dữ liệu nên không có khó khăn gì đối với lượng dữ liệu lớn. EzyPlatform cũng đã cung cấp sẵn tính năng quản lý quản trị viên và phân quyền chi tiết, quản trị viên được phân quyền gì thì sẽ nhìn thấy cái đó trên giao diện, không thừa, không thiếu. EzyPlatform cũng đã cung cấp sẵn tính năng import một danh sách người dùng từ tập tin CSV. EzyPlatform đã hỗ trợ sẵn đa ngôn ngữ, chỉ cần bổ sung tập tin messages_zh.properties là có cho tiếng TQ. Công việc còn lại chỉ là: Bổ sung thêm bảng để quản lý xe. Tạo giao diện cho phép nhập mã vận đơn vào hệ thống phù hợp với mong muốn của khách hàng. Tạo các giao diện quản lý tương ứng với từng chức năng khách hàng yêu cầu. Tạo tính năng cho phép xuất dữ liệu ra tập tin excel để khách hàng mang đi khai báo hải quan. Thường xuyên thảo luận với khách hàng để tránh đi chệch hướng. Triển khai EzyPlatform. Hướng dẫn khách hàng sử dụng. Các chức năng đã hoàn thiện trông như thế nào? Đầu tiên là chức năng nhập mã vận đơn: Đây là chức năng phức tạp nhất vì nó liên quan đến nhiều bảng: Bảng ecommerce_orders: Để lưu thông tin về đơn hàng. Bảng ecommerce_order_products: Để lưu thông tin ánh xạ giữa đơn hàng và sản phẩm. Bảng ecommerce_products: Để lưu thông tin sản phẩm. Bảng ecommerce_product_prices: Để lưu thông tin về giá của sản phẩm. Bảng ecommerce_deliverable_products: Để lưu thông tin đặc điểm của sản phẩm có thể vận chuyển. Bảng ecommerce_delivery_orders: Để lưu thông tin vận đơn. Bảng ecommerce_delivery_order_histories: Để lưu thông tin lịch sử vận đơn. Việc đồng bộ dữ liệu vào các bảng tiêu chuẩn của ecommerce sẽ có ích khi sau này nghiệp vụ của khách hàng mở rộng ra, ví dụ như khách hàng muốn bổ sung thêm tính năng tạo vận đơn ở phía Việt Nam chẳng hạn. Có một số cái khá tinh tế ở đây mà khách hàng yêu cầu: Mã vận đơn sẽ được khách hàng quét mã vạch, nghĩa là nó tự động được nhập, nên sẽ tự động xuống phần nhập ở dưới khi quét xong. Ở ô cuối cùng khi khách hàng nhấn enter thì sẽ tự động tạo mã vận đơn. Tem in sẽ được hiện lên để in khi mã vận đơn được tạo. Sẽ có âm thanh ting khi mã vận đơn được quét thành công và có âm thanh cảnh báo nên mã vận đơn có vấn đề. Tiếp theo là chức năng in tem cho mã vận đơn. Tem này sẽ được gắn trên sản phẩm: Khổ giấy của tem là 10x20cm, để tạo mã vạch thì mình đã sử dụng thư viện JsBarcode.all.min.js. Vì tem có thể bị rách nên cần tạo 2 mã vạch trên dưới để sử dụng thay nhau khi cần. Mã khách hàng cần in to để nhìn từ xa cũng thấy. Ở đây có một điều thú vị là khách hàng báo nhầm khổ giấy là 8x12cm dẫn đến mất rất nhiều thời gian để test, thay đổi mã nguồn, cuối cùng mình nghĩ ra hay vấn đề nằm ở khổ giấy, thay đổi thành 10x20 thì mới ổn, sau đó thì mình cũng cho phép cấu hình khổ giấy để không phải thay đổi mã nguồn nữa. Tem này có thể được in lại khi nhấn vào biểu tượng in ở phía bên tay phải.

EzyPlatform quản lý log thế nào?

Log hay nhật ký hoạt động là một trong những thông tin quan trọng để chúng ta có thể xem xét lại khi có vấn đề gì đó xảy ra, tuy nhiên cũng cần phải hạn chế việc ghi log để làm tiêu tốn quá nhiều dung lượng ổ cứng. Cũng giống như các tính năng common khác, EzyPlatform cũng quản lý tính năng log này để có thể hoạt động một cách hiệu quả.Ghi riêng log của từng thành phần EzyPlatform không ghi toàn bộ log vào một tập tin duy nhất mà tách ra thành từng tập tin riêng tương ứng với admin, web và socket: Điều này để tránh bị xung đột giữa các luồng ghi tập tin log, giúp thông tin được ghi chính xác, tập tin log cũng nhỏ hơn và một phần nào đó cũng giúp cho tốc độ ghi log nhanh hơn. Mỗi một thành phần lại sử dụng riêng tập tin logback.xml của mình để có khả năng cấu hình khác nhau khi cần thiết. Cấu hình tập tin logback.xml EzyPlatform sử dụng logback, chính vì vậy mà tập tin cấu hình có tên là logback.xml. Ví dụ cấu hình của admin sẽ như sau: <configuration> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} %msg%n </pattern> </encoder> </appender> <appender name="fileRolling" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/admin-server.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/admin-server.log.%d{yyyy-MM-dd}</fileNamePattern> </rollingPolicy> <encoder> <pattern> %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} %msg%n </pattern> </encoder> </appender> <appender name="file" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="fileRolling" /> </appender> <appender name="composite" class="org.youngmonkeys.ezyplatform.logback.CompositeLogbackAppender" /> <root level="info"> <appender-ref ref="console"/> <appender-ref ref="file"/> <appender-ref ref="composite"/> </root> </configuration> Ở đây chúng ta có: Định dạng của một dòng log sẽ là: %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} %msg%n. Log ban đầu sẽ được lưu ở một tập tin logs/admin-server.log, tuy nhiên sau 1 ngày thì sẽ được tạo thành tập tin mới tương ứng với ngày đó. Level log mặc định là info, nghĩa là những log từ debug trở lại sẽ không được ghi. Log không được xoá định kỳ, nếu bạn muốn định kỳ xoá log bạn có thể cài đặt Log Monitor plugin. Lớp CompositeLogbackAppender Như bạn có thể thấy trong tập tin logback.xml, đây là một lớp được phát triển bởi EzyPlatform, không phải mặc định của sẵn của logback, mã nguồn của nó chỉ đơn giản thế này: public class CompositeLogbackAppender extends LogbackAppender { @Override protected void append(ILoggingEvent event) { LogbackAppenderManager .getInstance() .forEach(it -> it.doAppend(event)); } } Nó sẽ gọi đến các lớp Appender được cài đặt bởi cả EzyPlatform lẫn các plugin để cho phép can thiệp vào quá trình log, ví dụ để gửi log WARN, ERROR vào hệ thống giám sát chẳng hạn. Tổng kết lại EzyPlatform quản lý log tương đối đơn giản nhưng đủ để các nhà phát triển yên tâm sử dụng. EzyPlatform cũng không quá ôm đồm nhiều việc để quản lý log quá chi tiết, thay vào đó thông qua lớp CompositeLogbackAppender nó sẽ cho phép các plugin bổ sung thêm các tính năng quản lý log nâng cao hơn.

Quá trình ra quyết định mua hàng của người tiêu dùng

Theo bạn, đâu là giai đoạn quan trọng nhất trong mỗi lần bán hàng? Nhiều người cho rằng giai đoạn quan trọng nhất trong một lần bán hàng là khi khách hàng rút tiền từ ví của họ và rồi đưa tiền cho doanh nghiệp của bạn. Nhưng thực tế, khi nói đến hành vi ra quyết định mua hàng của người tiêu dùng, mọi bước trong quá trình ra quyết định của người tiêu dùng đều đóng vai trò quan trọng.Vậy quy trình này bao gồm những gì? Tại sao doanh nghiệp cần hiểu biết về quy trình ra quyết định mua hàng? Cách thức để thấu hiểu khách hàng nhằm tối ưu hóa từng giai đoạn là gì? Tất cả sẽ được trả lời trong bài blog này!  Quá trình quyết định mua hàng là gì?Quá trình ra quyết định của người tiêu dùng là quá trình mà người tiêu dùng nhận thức và xác định nhu cầu của mình; thu thập thông tin về cách giải quyết tốt nhất những nhu cầu này; đánh giá các lựa chọn thay thế có sẵn; đưa ra quyết định mua hàng; và đánh giá việc mua hàng của họ.Tại sao doanh nghiệp cần nghiên cứu quá trình quyết định mua hàng?Việc nghiên cứu kỹ quá trình này giúp doanh nghiệp của bạn đạt được một số mục đích sau:Thấu hiểu khách hàng: Biết rõ nhu cầu, mong muốn và những yếu tố ảnh hưởng đến quyết định mua hàng.Xây dựng chiến lược marketing hiệu quả: Tạo ra nội dung hoạt động, sản phẩm, hoặc dịch vụ phù hợp với từng giai đoạn của quá trình.Tăng tỷ lệ chuyển đổi: Nắm bắt tâm lý khách hàng đúng thời điểm, thúc đẩy hành động mua hàng.Duy trì mối quan hệ lâu dài: Tăng sự hài lòng và lòng trung thành của khách hàng sau khi mua.Thông thường, quá trình ra quyết định mua hàng của người tiêu dùng đi qua 5 bước như sauNhận biết nhu cầu (Awareness)Mọi giao dịch bán hàng bắt đầu từ việc người tiêu dùng nhận ra họ có nhu cầu.Có thể chia nhu cầu thành 2 loại đó là nhu cầu cấp bách và nhu cầu tiềm ẩn:Nhu cầu cấp bách: Thiếu sản phẩm dịch vụ này người tiêu dùng không thể hoàn thành được việc trước mắt. Ví dụ như một bạn tân sinh viên ngành Công nghệ thông tin, nhu cầu cần mua một chiếc laptop để phục vụ học tập và công việc sau này.Nhu cầu tiềm ẩn: Được khơi gợi thông qua giáo dục nhận thức hoặc các quảng cáo sáng tạo.Nhu cầu được sinh ra từ các kích thích bên trong, thường là nhu cầu sinh lý hoặc cảm xúc, chẳng hạn như đói, khát, ốm đau, buồn ngủ, buồn bã, ghen tị, … với các kích thích bên ngoài như xu hướng, mùi thức ăn ngon hấp dẫn,…Tìm kiếm thông tin (Research)Khi nhận ra nhu cầu, người tiêu dùng sẽ tìm kiếm thông tin qua các kênh như mạng xã hội, các trang web của các thương hiệu và thông qua truyền miệng, khi người tiêu dùng hỏi người thân hoặc bạn bè. Đây là cơ hội để doanh nghiệp cung cấp nội dung hữu ích, trả lời đúng mối quan tâm của khách hàng.Đánh giá các phương án thay thế (Consideration)Trong giai đoạn này, người tiêu dùng sẽ so sánh các lựa chọn. Họ cân nhắc yếu tố như giá cả, tính năng, uy tín thương hiệu và dịch vụ hậu mãi...Đây cũng chính là lý do giải thích vì sao nội dung đánh giá trải nghiệm sản phẩm - review luôn là một tuyến nội dung được ưa chuộng. Người tiêu dùng ngày này dễ bị ảnh hưởng và định hướng bởi những video, bài viết và những bình luận ngắn gọn về sản phẩm dịch vụ. Họ tin vào một bên trung gian đứng ra nói về sản phẩm dịch vụ của thương hiệu. Sự đánh giá và so sánh phương án không chỉ nằm ở yếu tố giá cả nữa, giờ đây nó còn phụ thuộc nhiều vào số lượng người nói tốt về sản phẩm dịch vụ trên các kênh online mạng xã hội và sàn thương mại điện tử.Quyết định mua hàng (Purchase)Đây là lúc hành vi mua hàng chuyển thành hành động thực tế. Những yếu tố như trải nghiệm thanh toán mượt mà, ưu đãi hấp dẫn, hoặc sự tư vấn tận tình sẽ giúp khách hàng ra quyết định nhanh hơn.Đánh giá sau khi mua hàng (Post-Purchase)Sau khi mua, người tiêu dùng sẽ đánh giá xem sản phẩm hoặc dịch vụ có đáp ứng kỳ vọng không. Họ sẽ quyết định liệu có quay lại mua lần nữa hay giới thiệu cho người khác không.Ví dụ thực tếVí dụ như những khách hàng hiện tại của Young Monkeys:Bắt đầu từ việc đọc được các bài viết nói về tầm quan trọng của việc xây dựng website, chủ những doanh nghiệp mới nhận ra không có website làm giảm khả năng tiếp cận khách hàng. Từ đó nhu cầu xây dựng một website chuyên nghiệp để nâng cao uy tín và kết nối với khách hàng được hình thành.Họ bắt tay vào tìm kiếm nền tảng thiết kế website qua Google và trên mạng xã hội. Những kết quả khiến họ thấy rắc rối. Nên thông những mối quan hệ xung quanh và sự kết nối trên mạng xã hội, họ tìm được những người chuyên cung cấp dịch vụ xây dựng website.So sánh giữa việc thuê freelancer giá rẻ hơn, sử dụng những bên cung cấp sản phẩm đóng gói, hoặc lựa chọn trở thành đối tác của Young Monkeys, sử dụng EzyPlatform để xây dựng trang web.Với họ, do EzyPlatform được bảo đảm về mặt chuyên môn từ những founder uy tín, có giao diện trực quan và nhiều bài viết hỗ trợ toàn diện, chủ doanh nghiệp lựa chọn EzyPlatform là phương án tối ưu nhất cho những doanh nghiệp vừa và nhỏ như mình.Sau khi sử dụng, những đối tác doanh nghiệp cho biết họ thấy rất hài lòng với dịch vụ website được cung cấp. Ngoài ra, khả năng và tốc độ hỗ trợ kỹ thuật luôn được Young Monkeys quan tâm nên những khách hàng thường duy trì hợp tác lâu dài và chủ động trở thành nguồn truyền miệng uy tín cho những mối quan hệ xung quanh.Gợi ý các hoạt động áp dụng cho từng giai đoạnVới giai đoạn nhận biết nhu cầu (Awareness), làm sao để thu hút sự chú ý và khơi gợi nhu cầu tiềm ẩn của khách hàng, bạn có thể tham khảo các hoạt động: Tạo nội dung truyền thông sáng tạo, bắt mắt (video, bài viết blog, infographic).Đẩy mạnh chiến dịch quảng cáo nhắm đến đúng đối tượng qua các nền tảng như Google Ads, Facebook, và Instagram.Sử dụng influencer marketing để tăng độ phủ thương hiệu.Tìm kiếm thông tin (Research), với Mục tiêu là Cung cấp thông tin đầy đủ, rõ ràng, và thuyết phục để khách hàng cảm thấy tin tưởng:Tối ưu hóa website với SEO để tăng khả năng xuất hiện trong kết quả tìm kiếm.Cung cấp nội dung chuyên sâu: bài viết blog, hướng dẫn, video giải thích, hoặc các case study liên quan.Đảm bảo các đánh giá và phản hồi tích cực từ khách hàng trên các trang mạng xã hội, diễn đàn, hoặc nền tảng review.Xây dựng chatbot hoặc hệ thống hỗ trợ trực tuyến để trả lời các câu hỏi của khách hàng ngay lập tức.Đánh giá các phương án thay thế (Consideration)Để thuyết phục khách hàng rằng sản phẩm/dịch vụ của bạn là lựa chọn tốt nhất, bạn có thể đẩy mạnh chương trình ưu đãi đặc biệt: giảm giá giới hạn, quà tặng kèm,…Quyết định mua hàng (Purchase)Doanh nghiệp của bạn cần Thiết kế quy trình thanh toán đơn giản, thuận tiện và bảo mật. Đồng thời, đưa ra các chính sách đổi trả, hoàn tiền rõ ràng để khách hàng yên tâm.Đánh giá sau khi mua hàng (Post-Purchase)Bạn có thể áp dụng các hoạt động phổ biến như tạo các chương trình khách hàng thân thiết (loyalty program) với điểm thưởng hoặc ưu đãi mua hàng tiếp theo.Kết luậnQuá trình ra quyết định mua hàng của người tiêu dùng là một hành trình phức tạp nhưng đầy tiềm năng để khai thác. Với mỗi giai đoạn, doanh nghiệp có thể tạo sự khác biệt bằng cách đặt khách hàng làm trung tâm và không ngừng cải thiện trải nghiệm.

EzyPlatform quản lý việc thực thi SQL script thế nào?

Trong một dự án thông thường, mỗi khi có yêu cầu mới cần thay đổi cơ sở dữ liệu ví dụ như: Thêm bảng. Thêm trường. Thêm index. .. Chúng ta sẽ thường tạo một tập lệnh SQL (SQL script), nâng cấp mã nguồn, đến khi triển khai thì sẽ chạy SQL script trước rồi sau đó nâng cấp mã nguồn lên phiên bản mới sau. Vì EzyPlatform phải cung cấp cho nhiều phần mềm khác nhau, cho nhiều người khác nhau, thậm chí là cả những nhà phát triển không thành thạo với công cụ và SQL nên nó sẽ phải tự động hoá việc thực thi và quản lý các SQL Script. Quá trình thực thi SQL Script EzyPlatform sẽ thực thi các SQL Script theo một quá trình như sau: Khi khởi động EzyPlatform sẽ tìm đến các thư mục admin/resources/scripts và thư mục resources/scripts của các plugin. EzyPlatform sẽ kiểm tra xem tập tin script đã được thực thi trước đó chưa bằng cách truy vấn vào bảng ezy_run_script_histories. Nếu tập tin script chưa được thực thi thì EzyPlatform đọc tập tin, lấy nội dung SQL, cắt thành các đoạn ngăn cách nhau bằng dấu ;. EzyPlatform gửi các câu lệnh SQL đến database để thực thi. EzyPlatform tạo câu truy vấn đề thêm 1 bản ghi cho tập tin đã được thực thi vào bảng ezy_run_script_histories. Bạn cần phải lưu ý rằng SQL Script của bạn có lỗi sẽ làm cho EzyPlatform không khởi động được, vậy nên hãy kiểm tra thật kỹ, nếu bạn muốn EzyPlatform vẫn khởi động được ngay cả khi SQL Script có lỗi, bạn hãy để các script trong tập tin có chứa từ alter, ví dụ alter_my_table.sql chẳng hạn. Bảng ezy_run_script_histories có gì? Bảng này có định nghĩa như sau: CREATE TABLE IF NOT EXISTS `ezy_run_script_histories` ( `module_name` varchar(120) NOT NULL, `module_type` varchar(15) NOT NULL, `script_name` varchar(120) NOT NULL, `run_at` datetime NOT NULL, PRIMARY KEY (`module_name`, `module_type`, `script_name`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; Các trường có ý nghĩa thế này: module_name: Là tên của plugin. module_type: Là kiểu của plugin, ví dụ admin, web, socket. script_name: Là tên của file script. run_at: Là thời gian mà script được thực thi. Bạn có cần can thiệp gì không? Quá trình thực thi SQL được EzyPlatform hoàn toàn tự động 100%, các script chỉ được thực thi 1 lần duy nhất nên bạn có thể yên tâm không có trường hợp trùng nhau gây lỗi. Tổng kết Quản lý việc thực thi SQL script là một tính năng cơ bản nhưng rất quan trọng với EzyPlatform để nó có thể cập nhật, nâng cấp cơ sở dữ liệu một cách dễ dàng giúp các nhà phát triển không bị quá áp lực vào việc tạo ra các bảng hoàn hảo ngay từ ban đầu.

Danh sách các bảng core của EzyPlatform

EzyPlatform có 25 bảng core và có thể được bổ sung trong tương lai có thể được bổ sung thêm. Các bảng này sẽ phục vụ cho những nghiệp vụ đơn giản nhất mà phần mềm nào cũng có. Tuy nhiên EzyPlatform cũng sẽ không nhồi nhét nhiều bảng nhất có thể vào core, thay vào đó nó sẽ nhường cho các plugin để đảm bảo nguyên tắc Open/Close.Danh sách các bảng và ý nghĩa ezy_admin_activity_histories: Theo dõi lịch sử hoạt động của quản trị viên, bao gồm các thao tác trên URI, phương thức (GET/POST/...), tính năng liên quan, và thời gian thực hiện. ezy_admin_meta: Quản lý các thông tin bổ sung (metadata) liên quan đến quản trị viên, Ví dụ bạn cần bổ sung thêm địa chỉ của admin bạn có thể lưu dữ liệu vào bảng này. ezy_admin_projects: Dùng để lưu thông tin về các plugin được cài. ezy_admin_role_names: Danh sách các vai trò của quản trị viên với tên vai trò, tên hiển thị, và mức độ ưu tiên. ezy_admin_roles: Liên kết giữa quản trị viên và các vai trò mà họ được gán. ezy_admin_access_tokens: Lưu trữ access token của quản trị viên, trạng thái và thời gian hết hạn. ezy_admins: Lưu thông tin quản trị viên, bao gồm username, email, số điện thoại, trạng thái, ảnh đại diện, v.v. ezy_content_templates: Quản lý các mẫu nội dung (content templates), bao gồm tiêu đề, nội dung, trạng thái, và thông tin người tạo. ezy_data_indices: Chứa dữ liệu chỉ số (index) cho các loại dữ liệu khác nhau, hỗ trợ tìm kiếm và sắp xếp theo từ khóa, mức độ ưu tiên, và thời gian. ezy_data_meta: Quản lý metadata liên quan đến dữ liệu, bao gồm các cặp giá trị meta_key và meta_value, hỗ trợ cho các nhà phát triển có thể lưu thông tin bổ sung của một dữ liệu nào đó nếu bảng bị thiếu trường và nhà phát triển không muốn alter bảng. ezy_letter_receivers: Quản lý thông tin người nhận thư, trạng thái thư (đã gửi, đã nhận, đã đọc), và mức độ quan trọng. ezy_letters: Lưu trữ thông tin thư, bao gồm loại thư, tiêu đề, nội dung, người gửi, trạng thái, và thời gian tạo. ezy_links: Lưu trữ các liên kết (URI), bao gồm thông tin nguồn, hình ảnh liên kết, và mô tả chi tiết. Bảng này sẽ rất có ích cho SEO. ezy_medias: Quản lý dữ liệu phương tiện (media) như hình ảnh, video, với thông tin về người tải lên, loại file, và các thuộc tính khác. ezy_notification_receivers: Lưu thông tin về người nhận thông báo, trạng thái (đã gửi, nhận, hoặc đọc), mức độ tin cậy, và mức độ quan trọng. ezy_notifications: Quản lý các thông báo gửi đến người dùng hoặc quản trị viên, bao gồm tiêu đề, nội dung, và trạng thái thông báo. ezy_role_features: Định nghĩa các quyền và tính năng (feature) mà mỗi vai trò được phép sử dụng. ezy_run_script_histories: Lưu trữ lịch sử chạy SQL script của các module, bao gồm tên module, loại module, tên script, và thời gian chạy để đảm bảo mỗi script của cả EzyPlatform và các plugin chỉ được chạy 1 lần. ezy_settings: Quản lý các thiết lập hệ thống, bao gồm tên thiết lập, kiểu dữ liệu, và giá trị thiết lập. ezy_user_access_tokens: Lưu trữ token truy cập của người dùng, trạng thái và thời gian hết hạn. ezy_user_keywords: Lưu trữ các từ khóa liên quan đến người dùng, với mức độ ưu tiên và ngày tạo. ezy_user_meta: Quản lý các thông tin bổ sung (metadata) của người dùng. ezy_user_role_names: Danh sách vai trò người dùng, bao gồm tên vai trò và mức độ ưu tiên. ezy_user_roles: Liên kết giữa người dùng và các vai trò của họ. ezy_users: Lưu trữ thông tin người dùng, bao gồm username, email, mật khẩu, trạng thái tài khoản, v.v. Sơ đồ liên kết các bảng EzyPlatform mặc định không tạo liên kết khoá ngoại cho các bảng vì nó không biết điều này có phù hợp với mọi dự án phần mềm hay không, tuy nhiên mình cũng sẽ cố gắng vẽ một vài liên kết giữa các bảng như sau: Nếu bạn cảm thấy cần phải tạo khoá ngoại để ràng buộc dữ liệu chặt chẽ hơn, bạn hoàn toàn có thể nghiên cứu các mối liên kết giữa các bảng và tạo khoá ngoại. EzyPlatform sẽ chỉ khởi tạo cơ sở dữ liệu 1 lần vậy nên bạn không cần lo lắng rằng tạo khoá ngoại rồi lại bị xoá sau mỗi lẫn nâng cấp. Tổng kết Các bảng core của EzyPlatform tương đối nhiều tuy nhiên bạn cũng không cần quá lo lắng khi EzyPlatform tự tạo các bảng này cho bạn khi bạn cài đặt. Nếu bạn thấy rằng các bảng này là chưa đủ, bạn hoàn toàn có thể cài đặt và sử dụng các bảng của các plugin có trên chợ hoặc do bạn tự tạo ra nhé.

Các vấn đề chăm sóc khách hàng qua Zalo và Giải pháp hiệu quả

Ngày 5/11, theo báo cáo The Connected Consumer (Người tiêu dùng số) quý III/2024 do Decision Lab công bố, Zalo tiếp tục dẫn đầu các nền tảng nhắn tin tại Việt Nam về tỷ lệ sử dụng (Penetration rate) và mức độ yêu thích (Preference rate).  Chính nhờ sự phổ biến rộng rãi và tính năng đa dạng, Zalo đang trở thành một công cụ đắc lực trong việc chăm sóc khách hàng (CSKH). Tuy nhiên, không ít doanh nghiệp gặp khó khăn trong việc tận dụng hết tiềm năng của nền tảng này. Bài viết này sẽ giúp bạn nhận diện các vấn đề thường gặp và đưa ra giải pháp cụ thể để nâng cao hiệu quả CSKH qua Zalo.Các vấn đề thường gặp phải khi chăm sóc khách hàng qua Zalo1. Bộ phận CSKH trả lời chậm hoặc không đồng nhấtDù Zalo cho phép tương tác nhanh, nhưng nhiều doanh nghiệp vẫn để khách hàng chờ lâu. Điều này thường xảy ra do:Không có nhân sự trực 24/7.Quy trình xử lý chưa được chuẩn hóa.Ví dụ: Một khách hàng hỏi về thông tin sản phẩm lúc 9 giờ tối nhưng phải đợi đến sáng hôm sau mới nhận được phản hồi. Khi đó, khách hàng dễ mất kiên nhẫn và chuyển sang đối thủ khác.2. Quá tải tin nhắnVới các doanh nghiệp có lượng khách hàng lớn, việc xử lý thủ công toàn bộ tin nhắn trên Zalo là không khả thi. Tình trạng bỏ sót tin nhắn hoặc phản hồi không kịp thời rất dễ xảy ra.3. Thiếu cá nhân hóa trong CSKHKhi gửi thông báo về những ưu đãi mới hay giới thiệu chính sách, sản phẩm mới, bộ phận CSKH sẽ thường viết một tin nhắn mẫu và gửi hàng loạt. Bởi vì vậy, tin nhắn thường có những cụm từ như “anh/chị” để đảm bảo tính bao quát được các tệp khách hàng. Việc gửi tin nhắn hàng loạt không được tùy chỉnh này dễ tạo cảm giác “máy móc” và không quan tâm đến từng khách hàng. Điều này làm giảm sự gắn kết và lòng trung thành của khách hàng.4. Chưa khai thác triệt để tính năng Zalo OA (Official Account)Nhiều doanh nghiệp chỉ sử dụng Zalo như một kênh trò chuyện đơn thuần, bỏ qua các tính năng quan trọng như:Gửi tin nhắn chăm sóc định kỳ.Tích hợp chatbot tự động.Phân tích dữ liệu khách hàng.Giải Pháp Tối Ưu CSKH Qua Zalo1. Tích hợp AI vào chăm sóc khách hàngVới sự phát triển mạnh mẽ của trí tuệ nhân tạo (AI), việc ứng dụng AI vào chăm sóc khách hàng đã trở thành xu hướng tất yếu. AI không chỉ giúp tự động hóa các quy trình mà còn mang lại những cải tiến vượt trội về mặt trải nghiệm khách hàng.Xây dựng một hệ thống Chatbot giúp tự động hóa việc trả lời các câu hỏi phổ biến như:Giờ mở cửa, đóng cửa, giờ hành chính khi làm việcThông tin sản phẩmSố lượng hàng còn hay hếtHướng dẫn mua hàng, thanh toánĐiều này không chỉ giảm tải cho nhân viên mà còn đảm bảo khách hàng nhận được phản hồi ngay lập tức.Ví dụ: Khi khách hàng hỏi “Còn size M của mẫu áo này không?”, chatbot có thể tự động kiểm tra và trả lời chỉ trong vài giây.2. Tích hợp CRM để quản lý khách hàng hiệu quảCRM (Quản lý quan hệ khách hàng) kết hợp với Zalo OA sẽ giúp bạn:Lưu trữ lịch sử trò chuyện.Theo dõi trạng thái khách hàng.Cá nhân hóa các chiến dịch chăm sóc.3. Áp dụng tính năng phân loại khách hàngZalo OA cho phép bạn tạo danh sách khách hàng theo các nhóm khác nhau. Điều này giúp bạn gửi thông điệp phù hợp với từng đối tượng.Ví dụ: Khách hàng mới sẽ nhận được tin nhắn chào mừng, trong khi khách hàng cũ có thể nhận ưu đãi đặc biệt. 4. Xây dựng đội ngũ trực đa kênhĐào tạo nhân viên CSKH để đảm bảo:Luôn lịch sự, thân thiện.Phản hồi đồng nhất trên mọi kênh.Thể hiện sự chuyên nghiệp của doanh nghiệpNgoài ra, sắp xếp lịch trực hợp lý, đặc biệt vào giờ cao điểm, để không bỏ lỡ khách hàng.5. Theo dõi và cải tiến liên tụcZalo OA cung cấp dữ liệu thống kê như tỷ lệ phản hồi, lượt xem tin nhắn, và phản hồi từ khách hàng. Hãy phân tích các dữ liệu này để điều chỉnh chiến lược phù hợp.Kết LuậnZalo không chỉ là công cụ nhắn tin mà còn là cầu nối giúp doanh nghiệp xây dựng mối quan hệ bền vững với khách hàng. Bằng cách giải quyết các vấn đề nêu trên, doanh nghiệp có thể tối ưu hóa CSKH, nâng cao sự hài lòng và tăng doanh thu.Zalo như một kênh chiến lược – không chỉ để trò chuyện, mà còn để tạo ra giá trị dài hạn cho thương hiệu của bạn!

EzyPlatform quản lý các mẫu thế nào?

Quản lý các mẫu (template) ví dụ mẫu thư (email), mẫu thông báo, mẫu tập tin hay bất kỳ mẫu nào là một trong những nghiệp vụ hết sức cơ bản và gần như phần mềm nào cũng có, chính vì vậy mà EzyPlatform đã đóng gói lại phần này để hỗ trợ các nhà phát triển không cần phải khởi tạo lại nữa.Thiết kế cơ sở dữ liệu Cũng không có gì phức tạp, EzyPlatform sử dụng một bảng có tên ezy_content_templates để quản lý toàn bộ các mẫu. Bảng này có mã nguồn SQL như sau: CREATE TABLE IF NOT EXISTS `ezy_content_templates` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `template_type` varchar(25) COLLATE utf8mb4_unicode_520_ci NOT NULL, `template_name` varchar(300) COLLATE utf8mb4_unicode_520_ci NOT NULL, `title_template` varchar(1200) COLLATE utf8mb4_unicode_520_ci NOT NULL, `content_template` varchar(12000) COLLATE utf8mb4_unicode_520_ci NOT NULL, `creator_id` bigint unsigned NOT NULL, `status` varchar(25) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'DRAFT', `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `key_template_type_name` (`template_type`, `template_name`), INDEX `index_template_type` (`template_type`), INDEX `index_template_name` (`template_name`), INDEX `index_creator_id` (`creator_id`), INDEX `index_status` (`status`), INDEX `index_created_at` (`created_at`), INDEX `index_updated_at` (`updated_at`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; Ý nghĩa của một số trường đó là: template_type: Loại mẫu, ví dụ MAIL, LETTER, NOTIFICATION. template_name: Là tên của mẫu, ví dụ: new_orderslà mẫu để thông báo có đơn hàng mới qua email cho quản trị viên. title_template: Là tiêu đề của mẫu. content_template: Là nội dung của mẫu. creator_id: Là id của admin đã tạo ra mẫu. status: Trạng thái của mẫu có thể là DRAFT (đang nháp) hay COMPLETED (đã hoàn thành, có thể đưa vào sử dụng). Bạn cần lưu ý rằng trong một loại mẫu sẽ không chấp nhận tên trùng nhau, nhưng trong nhiều loại mẫu thì có thể. Thiết kế lớp Từ bảng ezy_content_templates EzyPlatform sẽ thiết kế các lớp như sau: ContentTemplate: Là lớp entity ánh xạ với bảng ezy_content_templates. ContentTemplateRepository: Là giao diện repository để tương tác với cơ sở dữ liệu. ContentTemplateService: Là giao diện cơ sở chứa một số hàm cơ bản để lấy dữ liệu mẫu. DefaultContentTemplateService: Là hàm cài đặt ContentTemplateService và sử dụng ContentTemplateRepository lưu hoặc lấy dữ liệu mẫu từ cơ sở dữ liệu. AdminContentTemplateService: Là lớp bạn sẽ dùng khi làm việc với admin plugin. WebContentTemplateService: Là lớp bạn sẽ dùng khi làm việc với web plugin. Sử dụng Trong dự án thực tế thường bạn sẽ trải qua các bước sau để sử dụng các mẫu: Là định nghĩa sẵn mẫu. Cài đặt logic sử dụng mẫu. Cho phép người sử dụng tuỳ chỉnh mẫu nếu họ muốn. Định nghĩa sẵn mẫu Giả sử bạn đang cần một mẫu để gửi thông báo cho admin khi có đơn hàng mới, bạn có thể tạo một tập tin có tên mail_template_new_orders_for_managers.html trong thứ mục admin plugin/resources của bạn với nội dung như sau: <!DOCTYPE html> <html> <body> <p>Hi managers,</p> <p>There are ${newOrders} new orders and ${waitingForConfirmationOrders} orders need to confirm.<p> <p>Please access <a href="${adminUrl}/ecommerce/orders">here</a> to review.<p> <p>Thank you!</p> </body> </html> Sau đó bạn có thể tạo ra một lớp config để đọc và lưu mẫu này vào cơ sở dữ liệu như sau: @AllArgsConstructor @EzyConfigurationAfter public class AdminEcommerceMailConfig implements EzyBeanConfig { private final EzyInputStreamLoader inputStreamLoader; private final AdminContentTemplateService contentTemplateService; @Override public void config() { addMailTemplates(); } @SuppressWarnings("MethodLength") private void addMailTemplates() { contentTemplateService.addTemplateIfAbsent( ContentTemplateType.MAIL.toString(), TEMPLATE_NEW_ORDERS_FOR_MANAGERS, () -> AddContentTemplateModel.builder() .templateName(TEMPLATE_NEW_ORDERS_FOR_MANAGERS) .titleTemplate("${orders} new orders need to review") .contentTemplate( readMailTemplateContentOrDefault( TEMPLATE_NEW_ORDERS_FOR_MANAGERS, "${orders} new orders need to review" ) ) .status(ContentTemplateStatus.COMPLETED.toString()) .build() ); } private String readMailTemplateContentOrDefault( String templateName, String defaultContent ) { try { String templateFullName = getMailTemplateFullName( templateName ); return EzyInputStreams.toStringUtf8( inputStreamLoader.load( "ecommerce/" + templateFullName + ".html" ) ); } catch (IOException e) { return defaultContent; } } } Cài đặt logic sử dụng mẫu Giải sử bạn có một đối tượng lập lịch để định kỳ kiểm tra có đơn hàng mới hay không sau đó gửi mail, bạn có thể cài đặt mã nguồn như sau: @EzySingleton public class AdminNewOrdersMailAppender extends AdminDataAppender<Order, Order, Void> { private final AdminMailServiceProxy mailServiceProxy; private final AdminEventHandlerManager eventHandlerManager; private final AdminEcommerceSettingService ecommerceSettingService; private final AdminSettingService settingService; private final AdminOrderRepository orderRepository; private final AtomicLong lastSendMailTime = new AtomicLong( System.currentTimeMillis() ); public AdminNewOrdersMailAppender( AdminMailServiceProxy mailServiceProxy, AdminEventHandlerManager eventHandlerManager, ObjectMapper objectMapper, AdminEcommerceSettingService ecommerceSettingService, AdminSettingService settingService, AdminOrderRepository orderRepository ) { super(objectMapper, settingService); this.mailServiceProxy = mailServiceProxy; this.eventHandlerManager = eventHandlerManager; this.ecommerceSettingService = ecommerceSettingService; this.settingService = settingService; this.orderRepository = orderRepository; } @Override protected void doAppend() { int period = ecommerceSettingService.getNotifyNewOrdersPeriod(); long sendMailTime = lastSendMailTime.get() + period * 60 * 1000L; long now = System.currentTimeMillis(); if (sendMailTime > now) { return; } long lastOrderId = ecommerceSettingService .getLastNotifiedNewOrderId(); if (lastOrderId == 0) { Order lastOrder = orderRepository.findLast(); lastOrderId = lastOrder != null ? lastOrder.getId() : 0L; } List<Order> orders = orderRepository.findByIdGtOrStatus( lastOrderId, OrderStatus.WAITING_FOR_CONFIRMATION.toString(), Next.limit(LIMIT_300_RECORDS) ); if (orders.isEmpty()) { return; } long waitingForConfirmationOrders = orders .stream() .filter(it -> it .getStatus() .equals(OrderStatus.WAITING_FOR_CONFIRMATION.toString()) ) .count(); int ordersSize = orders.size(); long newOrders = ordersSize - waitingForConfirmationOrders; Map<String, Object> parameters = EzyMapBuilder.mapBuilder() .put("orders", ordersSize) .put("newOrders", newOrders) .put("waitingForConfirmationOrders", waitingForConfirmationOrders) .put("adminUrl", settingService.getAdminUrl()) .toMap(); Collection<String> orderManagementEmails = ecommerceSettingService .getOrderManagementEmails(); if (orderManagementEmails.size() > 0) { mailServiceProxy.send( AdminMailModel.builder() .templateName(TEMPLATE_NEW_ORDERS_FOR_MANAGERS) .to(orderManagementEmails) .parameters(parameters) .build() ); } eventHandlerManager.handleEvent( INTERNAL_EVENT_NAME_NEW_ORDERS_APPEND, parameters ); lastSendMailTime.set(now); ecommerceSettingService.setLastNotifiedNewOrderId(now); long newLastOrderId = last(orders).getId(); if (newLastOrderId > lastOrderId) { ecommerceSettingService.setLastNotifiedNewOrderId( newLastOrderId ); } } @Override protected List<Order> getValueList(Void unused) { throw new UnsupportedOperationException("unused"); } @Override protected Void extractNewLastPageToken(List<Order> list, Void unused) { throw new UnsupportedOperationException("unused"); } @Override protected String getAppenderNamePrefix() { return TEMPLATE_NEW_ORDERS_FOR_MANAGERS; } @Override protected Void defaultPageToken() { throw new UnsupportedOperationException("unused"); } @Override protected Class<Void> pageTokenType() { return Void.class; } } Cho phép người dùng tuỳ chỉnh Bạn có thể cài đặt EzySupport plugin, nó sẽ cung cấp giao diện cho phép quản trị viên quản lý và thay đổi các mẫu theo ý muốn. Tổng kết Quản lý mẫu là một trong những tính năng cơ bản nhưng cực kỳ quan trọng đối với bất kỳ dự án phần mềm nào. Với EzyPlatform các nhà phát triển sẽ không cần phải tạo lại tính năng này nữa mà sẽ chỉ cần tập trung vào sử dụng với nghiệp vụ tuỳ ý của mình.

Giới thiệu về Java Logging

Log là gì? Log là quá trình ghi lại những thông tin được thông báo, lưu lại quá trình hoạt động của một ứng dụng. Mục đích là có thể xem lại các thông tin hoạt động của ứng dụng trong quá khứ như debug khi có lỗi xảy ra, check health, xem infor, error, warning, ... Có nhiều cách để ghi log: có thể lưu vào file, console, database, ... Các thành phần Hình bên dưới đại diện cho các thành phần cốt lõi và luồng điều khiển của API ghi nhật ký trong Java Application Là nguồn gốc tạo ra các thông điệp log. Trong ứng dụng bạn tạo ra các Logger (như info(), error(), debug() ) để ghi lại các sự kiện hoặc trạng thái của hệ thống. Logger Là thành phần trung tâm của hệ thống logging. Nó nhận các thông điệp log từ ứng dụng và chuyển chúng tới các Handler. Nhiệm vụ: Xác định cấp độ (Level): Logger kiểm tra cấp độ log của thông điệp( INFO, WARNING, SEVERE, ...) so với cấu hình của nó. Nếu thông điệp không đạt mức ưu tiên, nó sẽ bị bỏ qua. Gửi đến Handler: Logger chuyển thông điệp đến các Handler tương ứng. Áp dụng bộ lọc (Filter): Logger có thể có các bộ lọc để loạ bỏ những log không cần thiết. Ví dụ: Logger logger = Logger.getLogger("newLoggerName"); logger.setLevel(Level.WARNING); // Chỉ log từ WARNING trở lên logger.info("This is an INFO message."); // Bị bỏ qua logger.warning("This is a WARNING message."); // Được xử lý logger.severe("This is a SEVERE message."); // Được xử lý Phương thức getLogger() của lớp Logger được sử dụng để tìm hoặc tạo mới Logger. Đối số chuỗi khai báo tên của trình ghi nhật ký. Ở đây, điều này tạo ra một đối tượng Logger mới hoặc trả về một đối tượng hiện có Logger cùng tên. Đó là một quy ước để định nghĩa một Logger sau lớp hiện tại đang sử dụng class.getName() Logger logger = Logger.getLogger(MyClass.class.getName()); Mỗi loại Logger có một mức khai báo tầm quan trọng của thông báo nhật ký. Có 7 cấp độ nhật ký cơ bản: SEVERE: (1000)thất bại nghiêm trọng WARNING: (900)thông báo cảnh báo, một vấn đề tiềm ẩn INFO: (800)thông tin thời gian chạy chung CONFIG: (700)thông tin cấu hình FINE: (500)thông tin chung về nhà phát triển (theo dõi thông báo) FINER: (400)thông tin chi tiết về nhà phát triển (thông báo theo dõi) FINEST: (300)thông tin nhà phát triển rất chi tiết (theo dõi thông báo) OFF: tắt ghi nhật ký cho tất cả các cấp (không ghi gì) ALL: bật ghi nhật ký cho tất cả các cấp (nắm bắt mọi thứ) Mỗi cấp độ nhật ký có một giá trị số nguyên xác định mức độ nghiêm trọng của chúng ngoại trừ hai cấp độ nhật ký đặc biệt OFF và ALL Handler Handler (trong Java Util Logging) hoặc Appender (trong Logback/Log4j) chịu trách nhiệm xử lý thông điệp log và quyết định nơi chúng sẽ được gửi đến (console, file, database, hoặc hệ thống bên ngoài). Một Logger có thể có nhiều Handler. Nhiệm vụ: Áp dụng thêm bộ lọc (Filter): Mỗi Handler có thể áp dụng bộ lọc riêng để quyết định thông điệp log nào sẽ được xử lý. Định dạng log (Formatter): Handler sử dụng Formatter để định dạng thông điệp log theo mẫu mong muốn trước khi gửi. Gửi log đến hệ thống đích: Handler quyết định nơi gửi log, chẳng hạn như Consoler(gửi log đến màn hình console), File(ghi log vào file) Filter Bộ lọc được sử dụng để kiểm soát những log nào sẽ được xử lý ở cả Logger và Handler. Filter giúp giảm bớt những thông điệp log không cần thiết, tối ưu hóa hiệu suất. Formatter Formatter định dạng thông điệp log thành chuỗi văn bản dễ đọc trước khi được gửi đến hệ thống đích. Bạn có thể tùy chỉnh định dạng để bao gồm: thời gian (timestamp), cấp độ (level), tên logger (logger name), thông điệp (message) External System Là bất kỳ hệ thống bên ngoài nào nhận, lưu trữ, hoặc xử lý các thông điệp nhật ký từ ứng dụng. Đây là nơi các log được gửi đến sau khi đã được xử lý qua Logger và Handler. Cách sử dụng - Sử dụng Maven thêm phần phụ thuộc này vào file pom.xml <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.13</version> </dependency> - Logger public class EmployeeDAO { private static final Logger logger = LoggerFactory.getLogger(EmployeeDAO.class); public List<Employee> listEmployees() { try (Session session = DatabaseUtil.getSessionFactory().openSession()) { logger.info("Fetching all employees..."); return session.createQuery("from Employee", Employee.class).list(); } catch (Exception e) { logger.error("Error fetching employees: {}", e.getMessage(), e); return null; } } } - Appender <?xml version="1.0" encoding="UTF-8"?> <configuration> //Appender này ghi log ra console (terminal) <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender> //Appender này ghi log vào file và hỗ trợ tính năng "Rolling" (tự động xoay vòng file log theo thời gian) <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/application.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/application-%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>7</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="STDOUT" ></appender-ref> <appender-ref ref="FILE" ></appender-ref> </root> </configuration> - Sau khi chạy chương trình, refesh lại project ta sẽ thấy file log được tạo ra trong project

EzyPlatform hỗ trợ đánh index dữ liệu thế nào?

Tìm kiểm theo kiểu dùng toán tử LIKE %keyword% là đơn giản nhất trong tất cả các cách để tìm kiếm các bản ghi trong một bảng nào đó trong cơ sở dữ liệu. Tuy nhiên nó chỉ phù hợp khi số lượng bản ghi còn nhỏ, khi số lượng bản ghi tăng lên hàng trăm nghìn thì tốc độ truy vấn sẽ không còn đảm bảo nữa vì toán tử LIKE %keyword% có thể dẫn đến tìm kiếm toàn bộ các bản ghi trong một bảng, nghĩa là thực hiện một vòng for để tìm kiếm dữ liệu có chứa từ khoá, và đây là một trong những hành động thiêu đốt CPU.Thiết kế cơ sở dữ liệu EzyPlatform cung cấp 2 bảng để đánh chỉ mục (index) là: ezy_user_keywords: Lưu dữ liệu người dùng được đánh chỉ mục. ezy_data_indices: Lưu dữ liệu được đánh chỉ mục nói chung. Với mã nguồn như sau: CREATE TABLE IF NOT EXISTS `ezy_user_keywords` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `user_id` bigint unsigned NOT NULL, `keyword` varchar(120) COLLATE utf8mb4_unicode_520_ci NOT NULL, `priority` int unsigned NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `key_user_id_keyword` (`user_id`, `keyword`), INDEX `index_user_id` (`user_id`), INDEX `index_keyword` (`keyword`), INDEX `index_priority` (`priority`), INDEX `index_user_id_keyword_priority` (`user_id`, `keyword`, `priority`), INDEX `index_created_at` (`created_at`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; CREATE TABLE IF NOT EXISTS `ezy_data_indices` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `data_type` varchar(120) COLLATE utf8mb4_unicode_520_ci NOT NULL, `data_id` bigint unsigned NOT NULL, `keyword` varchar(300) COLLATE utf8mb4_unicode_520_ci NOT NULL, `priority` int unsigned NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `key_data_type_id_keyword` (`data_type`, `data_id`, `keyword`), INDEX `index_data_type` (`data_type`), INDEX `index_data_id` (`data_id`), INDEX `index_keyword` (`keyword`), INDEX `index_priority` (`priority`), INDEX `index_key_data_type_id_keyword_priority` (`data_type`, `data_id`, `keyword`, `priority`), INDEX `index_created_at` (`created_at`), INDEX `index_updated_at` (`updated_at`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; Tuy nhiên mình muốn các bạn tập trung sự chú ý nhiều hơn vào bảng ezy_data_indices, ý nghĩa của các trường trong bảng này là: id: Là trường dữ liệu được tăng dần. data_type: Là kiểu của dữ liệu, thông thường bạn sẽ dùng tên bảng cần được đánh chỉ cho trường này, ví dụ bạn muốn đánh chỉ mục cho dữ liệu của bảng ecommerce_products (lưu thông tin của sản phẩm) thì data type sẽ bằng ecommerce_products. data_id: Là id của bản ghi, ví dụ bạn có một sản phẩm có id là 3 thì data_id sẽ bằng 3. keyword: Là từ khoá gắn với data_id, bạn sẽ cần tách một dữ liệu dài ra thành các từ khoá nhỏ hơn để lưu vào bảng ezy_data_indices. priority: Là độ ưu tiên của từ khoá, ví dụ bạn có thể lưu Red car có priority là 7 còn Red là 3 để khi truy vấn thì có sắp xếp theo priority, kết quả có độ khớp cao hơn sẽ được xếp lên đầu. Thiết kế lớp Từ bảng ezy_data_indices EzyPlatform sẽ sinh ra các lớp sau để hỗ trợ các nhà phát triển không cần phải khởi tạo lại nữa: DataIndex: Đây là lớp Entity ánh xạ với bảng ezy_data_indices. DataIndexRepository: Interface này cung cấp một số hàm cơ bản CRUD cho DataIndex. Lớp DataIndexTransactionalRepository: Cung cấp hàm lưu DataIndex, vì sẽ có nhiều luồng ghi các chỉ mục vào bảng ezy_data_indices nên cần sử dụng transaction để đảm bảo tính ACID. DataIndexService: Interface này cung cấp các hàm cơ bản CRUD cho các chỉ mục. DefaultDataIndexService: Lớp này cài đặt interface DataIndexService và sử dụng cả DataIndexRepository lẫn DataIndexTransactionalRepository để cài đặt các hàm cần thiết. AdminDataIndexService: Đây là lớp mà bạn sẽ hay sử dụng nhất để lưu chỉ mục vào bảng ezy_data_indices. Các bước lưu dữ liệu chỉ mục vào cơ sở dữ liệu Các bước này bao gồm: Tách dữ liệu lớn thành các từ khoá ngắn. Set độ ưu tiên cho từ khoá. Lưu các chỉ mục vào cơ sở dữ liệu. Ví dụ bạn cần lưu dữ liệu chỉ mục của các sản phẩm vào cơ sở dữ liệu bạn có thể làm như sau: List<String> keywords = new ArrayList<>(); String productCode = value.getProductCode(); if (isNotBlank(productCode)) { keywords.addAll(toKeywords(productCode)); } String productName = value.getProductName(); if (isNotBlank(productName)) { keywords.addAll(toKeywords(productName)); } long productId = value.getId(); List<ProductCategory> categories = productCategoryRepository .findByProductId(productId); for (ProductCategory category : categories) { keywords.addAll(toKeywords(category.getDisplayName())); } List<ProductTag> tags = productTagRepository .findByProductId(productId); for (ProductTag tag : tags) { keywords.addAll(toKeywords(tag.getName())); } logger.info( "extracted keyword of productId {}: {}", productId, keywords ); List<String> distinctKeywords = keywords .stream() .distinct() .collect(Collectors.toList()); List<SaveDataKeywordModel> dataRecords = distinctKeywords.stream() .map(keyword -> SaveDataKeywordModel.builder() .dataId(productId) .keyword(keyword) .priority(keyword.length()) .build() ) .collect(Collectors.toList()); this.dataIndexService.saveKeywords("ecommerce_products", dataRecords); Ở đây bạn đang lấy tất cả các thông tin từ mã sản phẩm, tên danh mục, tag cho đến tên sản phẩm thông qua hàm Keywords.toKeywords do EzyPlatform cung cấp để tách thành các từ khoá nhỏ, độ ưu tiên được set theo độ dài của từ khoá và sau đó lưu tất cả vào cơ sở dữ liệu. Cách hàm toKeywords tách từ Hàm này cung cấp thuật toán để tách các từ lớn thành các từ nhỏ có độ dài tối đa, mặc định tối đa là 300 ký tự ví dụ bạn có từ Lucky Wheel Game nó sẽ tách thành các từ có độ dài khác nhau như: "lucky wheel game", "lucky wheel", "lucky", "wheel", "game", "luc", "whe", "gam", "lu", "wh", "ga" Hay a/b/c sẽ được tách thành: "a b c", "a b", "a", "b", "c" Tổng kết Vì không thể lạm dụng công nghệ một cách tràn lan nên EzyPlatform không thể đưa các framework đánh chỉ mục dữ liệu như Elasticsearch vào phần core được nên việc sử dụng bảng ezy_data_indices là một giải pháp nhẹ nhàng và phù hợp. Nếu dữ liệu của bạn hoặc khách hàng bạn có thể lớn đến hàng chục hay trăm nghìn bản ghi, hãy cân nhắc sử dụng các bảng và các lớp EzyPlatform cung cấp để đánh chỉ mục dữ liệu nhé.

EzyPlatform quản lý đa ngôn ngữ thế nào?

Ngay từ đầu khi được thiết kế, EzyPlatform đã hướng tới mục tiêu phục vụ đa quốc gia, nghĩa là sẽ cần cung cấp giao diện với nhiều ngôn ngữ khác nhau và điều này đặt ra những thách thức không nhỏ cho Young Monkeys.EzyPlatform được thừa hưởng từ EzyHTTP Bản thân EzyPlatform không thực sự cung cấp thuật toán và các thành phần để quản lý đa ngôn ngữ mà nó được thừa hưởng từ EzyHTTP. EzyPlatform chỉ đơn giản là cung cấp đường dẫn đến thư mục resources/messages của thành phần của các plugin hay theme mà nó quản lý cho EzyHTTP sau đó sử dụng các lớp của EzyHTTP để đọc messages và trả về cho EzyHTTP. Có một điều thú vị ở đây là EzyPlatform lại không điều khiển EzyPlatform, nghĩa là không trực tiếp gọi đến EzyHTTP mà cài đặt một giao diện có tên MessageProvider, đây là một trong những ứng dụng thú vị của Đảo ngược điều khiển - IoC. Quá trình các message đa ngôn ngữ được đưa vào quản lý Các tập tin message sẽ ở dạng key value, ví dụ: copy_url=Copy URL cpu_usage=CPU usage search_result.title=Search Result server_error.title=Server Error Chú ý: Chúng tôi khuyến khích bạn sử dụng các key ở dạng snake_case hoặc dot.case cho thống nhất. Đối với các ngôn ngữ khác nhau sẽ lưu ở các file có dạng messages_[mã ngôn ngữ].properties ví dụ: messages_vi.properties: dành cho tiếng Việt. messages_zh.properties: dành cho tiếng Trung. Mặc định thì sẽ lấy các các message ở tập tin messages.properties. Nếu không tìm thấy tin nhắn nào thì giá trị sẽ được lấy bằng cách chuyển đổi từ key sang value, ví dụ hello world sẽ được chuyển thành Hello World. Chú ý: Nếu bạn truyền một key rỗng sẽ gây ra lỗi, vậy nên hãy đảm bảo rằng bạn sẽ luôn truyền một key có độ dài ít nhất là 1. Các tập tin messages sẽ bắt buộc phải đặt trong thư mục resources/messages của các plugin hay theme. Các bước để đưa message vào quản lý sẽ như sau: Vì admin và web tồn tại độc lập nên các message cũng được quản lý độc lập. EzyPlatform sẽ cung cấp đường dẫn của thư mục resources/messages của thành phần hiện tại là admin web. EzyPlatform sử dụng lớp MessageReader của EzyHTTP để đọc toàn bộ các messages của các ngôn ngữ. EzyPlatform duyệt qua toàn bộ các plugin và theme đã được kích hoạt và cung cấp đường dẫn đến thư mục resources/messages của các plugin hay theme này và lại sử dụng MessageReader để đọc. Cuối cùng là tổng hợp thành một Map<String, Properties> giữa ngôn ngữ và các message để trả lại cho EzyHTTP. EzyHTTP sẽ đưa các message vào quản lý. Bạn có thể tham khảo mã nguồn của lớp AbstractMessageProvider để biết chi tiết hơn các bước: @AllArgsConstructor public abstract class AbstractMessageProvider implements MessageProvider { private final FileSystemManager fileSystemManager; private static final String MESSAGES_FOLDER = "resources/messages"; @Override public Map<String, Properties> provide() { String ezyplatformHomePath = fileSystemManager.getEzyHomePathString(); Map<String, Properties> answer = new HashMap<>(); TargetType targetType = getInclusiveTargetType(); readAndAppendMessages( answer, Paths.get( ezyplatformHomePath, targetType.getName(), MESSAGES_FOLDER ) ); ModuleType[] moduleTypes = getInclusiveModuleTypes(); Map<ModuleType, List<String>> modulesMap = getInclusiveModulesMapByModuleTypes( moduleTypes ); for (ModuleType moduleType : moduleTypes) { List<String> modules = modulesMap.getOrDefault( moduleType, Collections.emptyList() ); for (String module : modules) { readAndAppendMessages( answer, Paths.get( ezyplatformHomePath, moduleType.getTargetFolder(), module, MESSAGES_FOLDER ) ); } } return answer; } private void readAndAppendMessages( Map<String, Properties> messages, Path messageFolder ) { if (!Files.exists(messageFolder)) { return; } MessageReader messageReader = MessageReader.getDefault(); Map<String, Properties> map = messageReader.read( messageFolder.toString() ); for (Map.Entry<String, Properties> e : map.entrySet()) { String lang = e.getKey(); messages .computeIfAbsent(lang, k -> new Properties()) .putAll(e.getValue()); } } protected abstract TargetType getInclusiveTargetType(); protected abstract ModuleType[] getInclusiveModuleTypes(); protected abstract Map<ModuleType, List<String>> getInclusiveModulesMapByModuleTypes( ModuleType[] moduleTypes ); } Bên trong EzyHTTP quản lý đã ngôn ngữ thế nào? Cũng rất đơn giản thôi như bạn vừa thấy, EzyHTTP sẽ tổng hợp tất cả các message từ các MessageProvider và đưa vào một Map<String, Properties> duy nhất ánh xạ giữa ngôn ngữ và các message, mã nguồn tổng hợp sẽ kiểu thế này: private Map<String, Properties> collectMessages() { Map<String, Properties> answer = new HashMap<>(); mergeAnswerMessages(answer, readMessages()); for (MessageProvider provider : messageProviders) { mergeAnswerMessages(answer, provider.provide()); } return answer; } Khi bạn sử dụng một key và một ngôn ngữ, EzyHTTP sẽ lục tìm trong map cho bạn, nếu không thấy thì nó sẽ lấy trong các message mặc định, nếu không thấy nữa thì nó sẽ chuyển đổi message từ key cho bạn. Tổng kết Quản lý đa ngôn ngữ thông qua các tập tin messages.properties đối với các nhà phát triển tương đối đơn giản. Tuy nhiên ẩn sau đó là các kỹ thuật phức tạp để tổ chức và lấy ra được đúng giá trị của key mà bạn truyền vào. Mình sẽ dành các bài khác để nói về cách sử dụng và các chi tiết sâu xa hơn trong việc quản lý đa ngôn ngữ trong EzyPlatform nhé.