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

EzyPlatform quản lý các phiên bản plugin và theme thế nào?

Sức mạnh của EzyPlatform không chỉ nằm ở phần core mà còn nằm ở các plugin và theme do chính Young Monkeys hoặc cộng đồng phát triển. Có một thực tế phũ phàng rằng phát triển phần mềm là một quá trình không có điểm dừng do những yêu cầu thay đổi liên tục từ bộ phận kinh doanh hay người dùng, chính vì thế mà các phiên bản được sinh ra để đóng gói lại những tính năng tại một thời điểm nhất định. Chính vì điều này EzyPlatfom cần cung cấp cơ chế để quản lý và cho phép các plugin và theme được cập nhật phiên bản mới một cách dễ dàng đúng như từ Easy trong EzyPlatform.Cơ chế tương tự như quản lý EzyPlatform Trong bài trước mình đã trình bày về cách mà EzyPlatform quản lý phiên bản, điều này cũng tương tự như quản lý plugin và theme: Khi bạn nâng cấp một plugin hay theme thì EzyPlatform của bạn sẽ tải file zip chứa phiên bản hiện tại của plugin hay theme. Cũng tương tự như tải phiên bản EzyPlatform mới, nó sử dụng giao thức HTTP cho đơn giản. File zip tải xuống sẽ được lưu vào thư mục upload sau đó được giải nén cũng tại thư mục upload này. Để cập nhật phiên bản mới nhất của plugin hay theme bạn sẽ cần chọn áp dung phiên bản [mới] khi đó EzyPlatform của bạn sẽ gọi đến script khởi động lại. EzyPlatform sẽ sao chép các tập tin tương ứng của các plugin hay theme được nâng cấp mới tải về vào các thư mục plugin hay theme tương ứng ở admin, web và socket nếu có. Các bước thực hiện việc nâng câp plugin hay theme Bước 1: Bạn có thể chọn các plugin hay theme mà mình muốn nâng cấp ở sidebar. Bước 2: Khi một plugin hay theme có phiên bản mới nó hiển thị Update to [phiên bản] nếu bạn ở tiếng anh và Cập nhật lên [phiên bản] ở tiếng Việt, ví dụ trong hình của tôi là có plugin Ecommerce có phiên bản mới là 0.3.9. Bạn cũng có thể vào chi tiết một plugin và cũng sẽ thấy nút Update to [phiên bản]: Bước 3: Bạn có thể nhấn vào nút Update to [phiên bản] để tải xuống phiên bản mới nhất của plugin hay theme, tuy nhiên bạn cần có EzyPlatform phiên bản mới nhất nếu không nó sẽ có cảnh báo thế này: Bởi vì tất cả các plugin hay theme hầu hết đều phụ thuộc vào các thư viện của EzyPlatform nên để tránh trường hợp một phiên bản mới nào đó của plugin hay theme lại phụ thuộc vào các thư viện mới nhất của EzyPlatform sẽ có thể gây lỗi, nên việc bắt buộc cập nhật EzyPlatform lên mới nhất trước là hợp lý. Bước 4. Hãy nói bạn đã cập nhật EzyPlatform lên phiên bản mới nhất, EzyPlatform sẽ tải xuống phiên bản mới nhất cho bạn, lúc này có một nút mới Apply to [phiên bản xuất hiện, ví dụ của tôi là Ecommerce phiên bản 0.3.9: Vậy là xong. Tuy nhiên có một điểm đáng chú là nếu plugin hay theme của bạn bị phụ thuộc vào các plugin khác cũng sẽ có cảnh báo hiện lên yêu cầu bạn phải nâng cấp các plugin phụ thuộc trước, ví dụ plugin Elearning bị phụ thuộc vào plugin Ecommerce, thì nó sẽ hiện cảnh báo thế này: Và bạn sẽ buộc phải nâng cấp plugin Ecommerce trước. Tổng kết Nâng cấp plugin hay theme là công việc tương đối dễ dàng đối với các nhà phát triển khi sử dụng EzyPlatform. Điều này có được là nhờ EzyPlatform lưu trữ các plugin hay theme tập trung tại ezyplatform.com, đồng thời đã đóng gói lại sự phức tạp vào bên trong. Để cập nhật được các plugin hay theme lên phiên bản mới nhất bạn cũng sẽ cần cập nhật EzyPlatform và các plugin phụ thuộc lên phiên bản mới nhất trước. Sẽ còn nhiều thứ thú vị liên quan đến phần quản lý plugin và theme này và mình sẽ nói trong các bài viết khác nhé.

Connection Pool

Thiết lập kết nối cơ sở dữ liệu là một quá trình rất tốn tài nguyên và đòi hỏi nhiều chi phí. Hơn nữa, trong một môi trường đa luồng, việc mở và đóng nhiều kết nối thường xuyên và liên tục ảnh hưởng rất nhiều đến performance và tài nguyên của ứng dụng. Bài này sẽ giới thiệu với các bạn Connection Pool trong ứng dụng Java.1. Connection Pooling là gì? Connection pool (vùng kết nối) : là kỹ thuật cho phép tạo và duy trì 1 tập các kết nối dùng chung nhằm tăng hiệu suất cho các ứng dụng bằng cách sử dụng lại các kết nối khi có yêu cầu thay vì việc tạo kết nối mới. 2. Cách làm việc của Connection pooling? Connection Pool Manager (CPM) là trình quản lý vùng kết nối, một khi ứng dụng được chạy thì Connection pool tạo ra một vùng kết nối, trong vùng kết nối đó có các kết nối do chúng ta tạo ra sẵn. Và như vậy, một khi có một request đến thì CPM kiểm tra xem có kết nối nào đang rỗi không? Nếu có nó sẽ dùng kết nối đó còn không thì nó sẽ đợi cho đến khi có kết nối nào đó rỗi hoặc kết nối khác bị timeout. Kết nối sau khi sử dụng sẽ không đóng lại ngay mà sẽ được trả về CPM để dùng lại khi được yêu cầu trong tương lai. Ví dụ: Một connection pool có tối đa 10 connection trong pool. Bây giờ user kết nối tới database (DB), hệ thống sẽ kiểm tra trong connection pool có kết nối nào đang rảnh không? Trường hợp chưa có kết nối nào trong connection pool hoặc tất cả các kết nối đều bận (đang được sử dụng bởi user khác) và số lượng connection trong connection < 10 thì sẽ tạo một connection mới tới DB để kết nối tới DB đồng thời kết nối đó sẽ được đưa vào connection pool. Trường hợp tất cả các kết nối đang bận và số lượng connection trong connection pool = 10 thì người dùng phải đợi cho các user dùng xong để được dùng. Sau khi một kết nối được tạo và sử dụng xong nó sẽ không đóng lại mà sẽ duy trì trong connection pool để dùng lại cho lần sau và chỉ thực sự bị đóng khi hết thời gian timeout (lâu quá không dùng đến nữa). 3. So sánh sử dụng Connection pool với không sử dụng Sử dụng Connection Pool Connection Pool là một tập hợp các kết nối được tái sử dụng để giao tiếp với cơ sở dữ liệu. Khi một ứng dụng cần truy cập vào cơ sở dữ liệu, nó sẽ lấy một kết nối từ pool thay vì tạo một kết nối mới. Ưu điểm: Hiệu suất cao hơn: kết nối được tái sử dụng, giảm thời gian thiết lập kết nối (overhead) khi thực hiện truy vấn Quản lý tài nguyên tốt hơn: Pool giới hạn số lượng kết nối tối đa, tránh việc quá tải tài nguyên cho máy chủ cơ sở dữ liệu Ổn định hơn: ngăn trạng tình trạng bùng nổ kết nối khi có nhiều yêu cầu đồng thời Tuỳ chỉnh linh hoạt: có thể cấu hình số lượng kết nối tối thiểu, tối đa, thời gian chờ, và cơ chế kiểm tra kết nối để duy trì hiệu suất ổn định Nhược điểm: Bộ nhớ chiếm dụng: Connection pool tiêu tốn bộ nhớ để duy trì các kết nối Không dùng Connection Pool Khi không sử dụng connection pool, mỗi lần ứng dụng cần truy cập cơ sở dữ liệu, nó sẽ tạo một kết nối mới và đóng kết nối sau khi hoàn thành. Ưu điểm: Đơn giản: không cần cấu hình, chỉ cần gọi trực tiếp API tạo kết nối. Phù hợp với hệ thống nhỏ: hệ thống có ít yêu cầu đồng thời hoặc chỉ chạy các tác vụ không thường xuyên Nhược điểm: Hiệu suất kém hơn: mỗi lần kết nối cơ sở dữ liệu cần thiết lập lại từ đầu, gây tốn thời gian và tài nguyên Tiêu tốn tài nguyên: dễ dẫn đến bùng nổ kết nối nếu có nhiều yêu cầu đồng thời Không ổn định: hệ thống dễ bị quá tải khi số lượng kết nối tăng đột ngột Không tối ưu cho ứng dụng lớn: gây chậm trễ và làm giảm khả năng mở rộng 4. Tại sao cần Connection Pool? - Database Connection được khởi tạo với chi phí đắt đỏ, nên thay vì tạo chúng ở mỗi request thì ta sẽ khởi tạo trước và gọi đến bất cứ khi nào cần truy cập vào Database. Database là tài nguyên được chia sẻ, do đó việc tạo 1 pool của connections và chia sẻ chúng trên tất cả các transaction là điều hợp lý. Datatabase Connection Pool thì giới hạn lượng truy cập vào Database tại 1 thời điểm giúp giảm thiểu việc Database Server bị treo. 5. Tổng kết Sử dụng connection pool là cần thiết cho các ứng dụng lớn hoặc có lượng truy cập cao, trong khi không sử dụng có thể phù hợp với các ứng dụng nhỏ, đơn giản.

EzyPlatform quản lý phiên bản thế nào?

Vấn đề của các sản phẩm như EzyPlatform là nó phải được nâng cấp thường xuyên để nâng cấp, vá lỗi đặc biệt là các lỗi liên quan đến rủi ro bảo mật thông tin, chính vì vậy mà các phiên bản mới sẽ được ra đời định kỳ hoặc đột xuất. Để đảm bảo mọi thứ hoạt động ổn định, EzyPlatform sẽ cần có cơ chế để quản lý các phiên bản.Cách thức hoạt động Nó cũng tương đối đơn giản, không cần thiết phải mở port lằng nhằng. Các phương bản của EzyPlatform sẽ được lưu tập trung tại ezyplatform.com. EzyPlatform của bạn sẽ thi thoảng lại gọi đến ezyplatform.com để lấy thông tin phiên bản mới nhất, nếu so sánh phiên bản hiện tại mà nhỏ hơn thì trên giao diện admin bạn sẽ thấy một mũi tên màu vàng xuất hiện ở phía trên cùng sidebar. Khi bạn chọn tải về phiên bản mới nhất thì EzyPlatform của bạn sẽ tải về phiên bản mới nhất và giải nén ra thư mục upload/platforms. Khi bạn chọn khởi động lại thì EzyPlatform của bạn sẽ gọi đến file update.sh để cập nhật lên phiên bản mới nhất. Lưu ý rằng đối với Windows bạn sẽ phải gọi tập tin update.bat. Bạn có thể tham khảo hướng dẫn này để biết thêm chi tiết. EzyPlatform sử dụng giao thức gì để tải phiên bản mới? Đơn giản là dùng giao thức HTTP để đơn giản cho tất cả mọi người. Nếu sử dụng một giao thức nào đó yêu cầu phải mở một cổng để tải phiên bản mới nhất có thể dẫn đến lỗ hổng bảo mật. Ngoài ra HTTP cũng rất dễ cài đặt. Các bước bạn có thể thực hiện Bước 1: Bạn sẽ truy cập vào đường dẫn [url trang admin của bạn]/ezyplatform ví dụ http://localhost:9090/ezyplatform. Bạn có thể click vào logo của EzyPlatform ở trên cùng sidebar. Bước 2: Bạn có thể nhấn vào tải phiên bản mới nhất, ví dụ là phiên bản 0.3.3: Hãy kiên nhẫn chờ một chút để EzyPlatform tải xuống phiên bản mới nhất cho bạn. Bước 3. Khi đã tải xong, bạn sẽ thấy giao diện mới trông thế này: Bạn có thể nhấn vào nút Cài đặt phiên bản xxx ví dụ trong hình là Cài đặt phiên bản 0.0.3, tuỳ vào tốc độ máy chủ của bạn mà việc khởi động lại sẽ diễn ra nhanh hay chậm, thông thường sẽ mất khoảng 30 giây để EzyPlatform hoàn thành việc khởi động lại để nâng cấp phiên bản mới nhất. Bạn có thể xem xét mã nguồn SQL mới nhất của EzyPlatform để xem có thay đổi gì không, hoặc bạn cảm thấy nâng cấp phiên bản này quá rủi ro, bạn có thể xoá phiên bản EzyPlatform vừa tải. Khôi phục lại phiên bản cũ Khi bạn cài đặt một phiên bản EzyPlatform nào đó hoặc nâng cấp một plugin, một theme nào đó thì EzyPlatform sẽ lưu trữ lại phiên bản cũ trong mục Backup History, bạn có thể truy cập vào đường dẫn [url trang admin của bạn]/ezyplatform/history ví dụ http://localhost:9090/ezyplatform/history, hoặc click vào nút Lịch sử (History) ở trang nâng cấp Ezyplatform. Bạn sẽ thấy danh sách các phiên bản cũ: Bạn có thể chọn một phiên bản bất kỳ, sẽ có một popup hiển thị lên kiểu thế này: Bạn có thể nhấn vào xác nhận và chờ đợi 1 lúc để EzyPlatform khởi động lại để khôi phục phiên bản cũ. Tổng kết EzyPlatform sẽ thường xuyên được nâng cấp, chính vì vậy việc quản lý phiên bản là cực kỳ quan trọng. Thông qua giao thức HTTP để tải xuống, một chút kỹ thuật DevOps EzyPlatform sẽ giúp người sử dụng dễ dàng cập nhật EzyPlatform của mình lên phiên bản mới nhất.

Hiểu rõ 3 mục tiêu để xây dựng chiến lược marketing hiệu quả

Dù mỗi doanh nghiệp đều mang những đặc thù riêng biệt và dù chúng ta có chủ động tìm kiếm, nghiên cứu các thông tin, thực trạng về thị trường, khách hàng thì ở trong bối cảnh cạnh tranh gay gắt ngày này, người làm marketing vẫn không tránh khỏi sự bối rối mỗi khi lập chiến lược marketing. Để xây dựng được một chiến lược marketing hiệu quả đòi hỏi người làm marketing không chỉ có kiến thức nền tảng vững chắc mà còn cần sự linh hoạt và khả năng thích ứng trong cả quá trình hoạch định và triển khai kế hoạch. Do đó, blog này sẽ trả lời câu hỏi “Xây dựng chiến lược marketing phải bắt đầu từ đâu đây?”.3 mục tiêu quan trọng bạn cần phải biếtGốc rễ của chiến lược marketing là đạt được mục tiêu đã định ra của doanh nghiệp công ty bạn. Đó chính là lý do quan trọng khiến bạn phải hiểu rõ các khái niệm cốt lõi như Business Objective, Marketing Objective và Communication Objective. Ba khái niệm này nói về ba mục tiêu cốt lõi của bất kỳ doanh nghiệp nào. Chúng không chỉ tồn tại độc lập mà còn liên kết chặt chẽ với nhau, ảnh hưởng lẫn nhau trong suốt quá trình phát triển chiến lược marketing. Việc nắm vững mối liên hệ này sẽ giúp bạn tạo ra những kế hoạch marketing hiệu quả và phù hợp với thực tiễn của doanh nghiệp.1. Business Objective - Mục tiêu kinh doanhMục tiêu kinh doanh là nền tảng của mọi chiến lược marketing. Đây là những gì mà công ty mong muốn đạt được. Chúng có thể là:Lợi nhuận: Tăng trưởng doanh thu và giảm chi phí.Thị phần: Mở rộng sự hiện diện trên thị trường.Chất lượng sản phẩm/dịch vụ: Cải thiện trải nghiệm của khách hàng.Tăng trưởng: Định hướng cho sự phát triển bền vững.Việc xác định rõ ràng Business Objective sẽ giúp định hướng cho các bước tiếp theo trong việc phát triển chiến lược marketing.2. Marketing Objective - Mục tiêu marketingTừ Business Objective, chúng ta chuyển sang Marketing Objective. Chính những mục tiêu kinh doanh ở trên sẽ định hướng cụ thể mục tiêu marketing. Một số mục tiêu marketing phổ biến bao gồm:Tăng doanh thu: Thúc đẩy doanh số bán hàng.Xây dựng thương hiệu: Tạo dựng và gia tăng giá trị thương hiệu.Mở rộng thị trường: Thâm nhập vào các thị trường mới.Tăng sự nhận diện thương hiệu: Nâng cao khả năng nhận biết của khách hàng về thương hiệu.Từ mục tiêu marketing, bạn có thể lựa chọn một số chiến lược marketing như:Chiến lược marketing mix với mô hình 4P, 7P, 4E, 4C, SAVE,...Chiến lược thâm nhập thị trườngChiến lược phát triển sản phẩmChiến lược tăng cường sự ủng hộ từ khách hàng3. Communication Objective - Mục tiêu truyền thôngCuối cùng, từ Marketing Objective - những mục tiêu marketing đã xác định, chúng ta tiến tới Communication Objective - Mục tiêu truyền thông. Đây là những mục tiêu cụ thể cho các hoạt động truyền thông nhằm hỗ trợ cho các mục tiêu marketing. Một số mục tiêu truyền thông có thể có:Tăng cường nhận thức về thương hiệu: Đảm bảo rằng thương hiệu được biết đến rộng rãi.Tạo lòng trung thành với thương hiệu: Khuyến khích khách hàng quay lại mua hàng.Thay đổi hình ảnh công ty: Điều chỉnh cách nhìn nhận của công chúng về thương hiệu.Để đạt được những mục tiêu này, các chiến lược truyền thông có thể bao gồm:Chiến lược truyền thông tích hợp (IMC)Chiến lược truyền thông lan truyềnChiến lược truyền thông trả phíChiến lược truyền thông sở hữuKết luậnHiểu rõ ba mục tiêu này sẽ giúp marketer xây dựng được một chiến lược marketing vững chắc và hiệu quả. 3 mục tiêu này không tồn tại độc lập mà liên kết chặt chẽ với nhau vì vậy giúp cho chúng ta dễ dàng đo lường được hiệu quả.Việc thực hiện tốt Mục tiêu truyền thông sẽ giúp hình ảnh câu chuyện của thương hiệu doanh nghiệp bạn được lan rộng hơn với đối tượng mục tiêu từ đó tạo ra những thay đổi trong nhận thức và hành vi của khách hàng mục tiêu nhờ vào các hoạt động của chiến lược marketing và rồi từ từ hoàn thành mục tiêu kinh doanh.Điều này một lần nữa nhấn mạnh rằng, một chiến lược hiệu quả cần phải có sự đồng bộ giữa các mục tiêu ngay từ khi bắt đầu.<!--Bootstrap JS -->

EzyPlatform quản lý cài đặt thế nào?

Trong một dự án bình thường thì bạn sẽ thường lưu các cài đặt (setting) trong tập tin properties hoặc yaml, và khi cần thay đổi hay bổ sung thêm cài đặt mới bạn sẽ cần cập nhật các tập tin cài đặt sau đó build lại dự án, triển khai và khởi động lại máy chủ. Tuy nhiên đối với EzyPlatform thì khác, nó phải đảm bảo hạn chế tối đa việc khởi động lại để giữ cho phần mềm của người dùng hoạt động ổn định. Chính vì vậy việc quản lý cài đặt cũng sẽ phải khác.Sử dụng cơ sở dữ liệu Cụ thể thì EzyPlatform sinh ra một bảng có tên là ezy_settings với mã nguồn như sau: CREATE TABLE IF NOT EXISTS `ezy_settings` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `setting_name` varchar(120) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '', `data_type` varchar(12) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'STRING', `setting_value` varchar(2048) COLLATE utf8mb4_unicode_520_ci NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `key_setting_name` (`setting_name`), INDEX `index_data_type` (`data_type`), 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 các trường trong bảng sẽ như sau: id: Là id của setting được tăng dần. setting_name: Là tên của cài đặt, thường được yêu cầu các nhà phát triển viết dưới dạng snake_case (viết thường và gạch dưới). data_type: Là kiểu dữ liệu của cài đặt, trường này sẽ hỗ trợ việc serialize và deserialize giá trị của setting. setting_value: Là giá trị của cài đặt, thường là bất kỳ giá trị nào cũng được. created_at: Là thời gian điểm cài đặt được tạo ra. updated_at: Là thời điểm cài đặt được cập nhật. Bạn có thể sử dụng trường này để cập nhật cache cài đặt khi có thay đổi. Thiết kế lớp Mặc dù có một bảng nhưng sẽ có tương đối nhiều lớp được tạo ra để quản lý cài đặt: Setting: Đây là lớp entity ánh xạ với bảng ezy_settings. SettingRepository: Đây là lớp repository cung cấp các hàm truy vấn lấy các cài đặt từ cơ sở dữ liệu. SettingService: Đây là một giao diện cung cấp các hàm cơ bản thêm sửa xoá cài đặt. DefaultSettingService: Đây là lớp cài đặt sẵn các hàm mặc định của SettingService. Lớp WebSettingService: Đây là lớp chuyên dùng cho web. SocketSettingService: Đây là lớp chuyên dùng cho socket. AdminSettingService: Đây là lớp chuyên dùng cho admin, trong lớp này cung cấp thêm một số hàm đặc trưng cho admin mà web hay socket không có. AdminSettingsController: Cung cấp giao diện cho màn hình cài đặt trên giao diện quản trị. AdminApiSettingsController: Cung cấp các API cho quản lý cài đặt. Bạn cần lưu ý rằng nên hạn chế gọi thay đổi cài đặt ở web hay socket để tránh rủi ro về bảo mật, việc thay đổi hay thêm mới setting thường sẽ diễn ra ở admin. Sử dụng Bạn có thể tìm thấy các menu dành cài đặt ở phần cuối của sidebar: Bạn có thể xem danh sách các cài đặt: Bổ sung một cài đặt: Hay sửa đổi một cài đặt: Bạn có thể sử dụng các cài đặt trong mã nguồn của mình ví dụ: public void setInputDeliveryOrderCountryId(long countryId) { settingService.setLongValue( SETTING_KEY_INPUT_DELIVERY_ORDER_COUNTRY_ID, countryId ); } public long getInputDeliveryOrderCountryId() { return settingService.getLongValue( SETTING_KEY_INPUT_DELIVERY_ORDER_COUNTRY_ID ); } Tổng kết Việc sử dụng các cài đặt được lưu trong cơ sở dữ liệu sẽ giúp EzyPlatform trở nên động hơn và hạn chế việc khởi động lại. Cái giá phải trả là việc cài đặt tương đối nhiều lớp, tuy nhiên EzyPlatform đã làm công việc đó cho bạn. Để đảm bảo hiệu năng thì EzyPlatform còn cho phép cache các cài đặt, mình sẽ chia sẻ vấn đề này ở các bài tiếp theo nhé.

JPA là gì?

1. JPA là gì JPA là viết tắt của Java Persistence API (sau này đổi tên thành Jakarta Persistence API) Nó cung cấp một framework cho các nhà phát triển để quản lý dữ liệu liên quan đến cơ sở dữ liệu một cách hiệu quả và trực quan. JPA định nghĩa các đối tượng Java (entities) được lưu trữ, truy xuất, cập nhật, xoá trong cơ sở dữ liệu. JPA là một đặc tả Java cho việc ánh xạ giữa các đối tượng Java với cơ sở dữ liệu quan hệ sử dụng công nghệ phổ biến là ORM (Object Relational Mapping). 2. ORM là gì ORM (Object-Relational Mapping) là một kỹ thuật trong lập trình để ánh xạ các đối tượng Java vào cơ sở dữ liệu quan hệ. Khi sử dụng ORM, chúng ta có thể làm việc với cơ sở dữ liệu thông qua các đối tượng Java, thay vì phải sử dụng các câu truy vấn SQL trực tiếp. Một framework như Hibernate ORM sẽ thực thi quá trình đó bên trong 1 thư viện, framework , hoặc 1 layer như một phần trong cấu trúc của chương trình, lớp ORM có vai trò quản lý sự chuyển đổi qua lại giữa các đối tượng (objects) với các bảng (tables) và cột (colums) trong cơ sở dữ liệu quan hệ (relational database). Trong Java, lớp ORM biến đổi các lớp (class) và đối tượng (object) Java để chúng có thể được lưu trữ và quản lý trong cơ sở dữ liệu. 3. Cấu tạo JPA JPA sử dụng metadata để ánh xạ các đối tượng persistence với các bảng trong cơ sở dữ liệu. JPA hỗ trợ SQL như là một ngôn ngữ truy vấn để dễ dàng xử lý các truy vấn cơ sở dữ liệu. Ngôn ngữ truy vấn JPA có thể dùng thực thi cả truy vấn tĩnh và truy vấn động. JPA bao gồm ba thành phần chính là: Entity, Entity Manager, và Entity Manager Factory. Ngoài ra còn có, EntityTransaction, Persistence, Query. Entity Entity là các đối tượng thể hiện tương ứng 1 table trong cơ sở dữ liệu. Entity thường là các class POJO đơn giản, chỉ gồm các phương thức getter, setter. @Entity @Table(name = "employees") public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @Column(name = "name", nullable = false) private String name; @Column(name = "salary", nullable = false) private double salary; @Column(name = "workingdays", nullable = false) private double workingdays; Entity Manager Entity Manager là một interface cung cấp các API cho việc tương tác với các Entity. Entity Manager Factory Được dùng để tạo ra một instance của EntityManager. Entity Transaction Một Transaction là một tập hợp các thao tác trong đó tất cả các thao tác phải được thực hiện thành công hoặc tất cả thất bại. Một database transaction bao gồm một tập hợp các câu lệnh SQL được committed hoặc rolled back trong một unit. Entity Transaction có quan hệ 1-1 với EntityManager. Bất kỳ thao tác nào được bắt đầu thông qua đối tượng Entity Manager đều được đặt trong một Transaction. Đối tượng Entity Manager giúp tạo Entity Transaction. public void updateEmployee(int id, String name, double salary, double workingDays, double receipt, double payment) { try { executeTransaction(session -> { Employee employee = session.get(Employee.class, id); if (employee != null) { employee.setName(name); employee.setSalary(salary); employee.setWorkingdays(workingDays); employee.setReceipt(receipt); employee.setPayment(payment); session.update(employee); } else { System.out.println("Employee not found with ID: " + id); } }); } catch (Exception e) { System.err.println("Error updating employee with ID: " + id); e.printStackTrace(); } } Persistence Một Persistence định nghĩa một tập hợp các Entity class được quản lý bởi 1 instacne của EntityManager trong ứng dụng. Query Đây là một interface, được mỗi nhà cung cấp JPA implement để có được các đối tượng quan hệ đáp ứng các tiêu chí (criteria) truy vấn. 4. Cài đặt và thiết lập JPA Thêm dependency vào file pom.xml <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.6.14.Final</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.4.0</version> </dependency> <dependency> <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> <version>2.2</version> </dependency> </dependencies> Cấu hình liên kết database trong file persisence.xml hoặc cấu hình Java (Annotation-based configuration) ở đây dùng cấu hình java private static final SessionFactory sessionFactory; static { try { Configuration configuration = new Configuration(); configuration.setProperty("hibernate.connection.driver_class", "com.mysql.cj.jdbc.Driver"); configuration.setProperty("hibernate.connection.url", "jdbc:mysql://localhost:3306/accounting_system"); configuration.setProperty("hibernate.connection.username", "root"); configuration.setProperty("hibernate.connection.password", "123456"); configuration.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect"); configuration.setProperty("hibernate.hbm2ddl.auto", "update"); configuration.setProperty("hibernate.show_sql", "true"); configuration.setProperty("hibernate.format_sql", "true"); configuration.addAnnotatedClass(Employee.class); ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder() .applySettings(configuration.getProperties()) .build(); sessionFactory = configuration.buildSessionFactory(serviceRegistry); } catch (Throwable ex) { throw new ExceptionInInitializerError(ex); } } public static SessionFactory getSessionFactory() { return sessionFactory; } 5. Tổng kết JPA là một công cụ mạnh mẽ giúp các lập trình viên Java xử lý dữ liệu một cách dễ dàng và hiệu quả hơn. Bằng cách trừu tượng hóa các thao tác cơ sở dữ liệu thông qua các thực thể hướng đối tượng, JPA không chỉ giảm bớt mã nguồn mà còn tăng cường tính bảo trì và khả năng mở rộng của ứng dụng. Nếu bạn đang phát triển một ứng dụng Java và cần làm việc với cơ sở dữ liệu, JPA chắc chắn là một lựa chọn đáng cân nhắc.

Mô hình SAVE: Hướng tiếp cận mới trong Marketing mix

Bối cảnh SAVE ra đời  Trong các bài viết trước, mình đã giới thiệu về các mô hình marketing mix phổ biến như 4P, 7P, 4E và 4C. Bắt nguồn từ mô hình 4P, những mô hình mới ra đời không chỉ để áp dụng cho đa dạng các ngành nghề có tính chất khác nhau mà phần lớn còn do sự biến đổi trong sự tiếp nhận thông tin của khách hàng.Có lẽ bởi vậy, trong thời đại công nghệ khi mọi thứ đều diễn ra trên internet, thực trạng “dư thừa” thông tin đã khiến nhu cầu về sản phẩm, dịch vụ của khách hàng ngày càng khác biệt nhau.Hoạt động kinh doanh cũng luôn xoay chuyển theo những thay đổi nhanh chóng trong hành vi tiêu dùng, đặc biệt là trong việc phục vụ những nhu cầu ngày càng mang tính cá nhân hóa cao. Khi ai cũng thích được “khác biệt”, độc nhất thì hoạt động tiếp thị không thể như cũ được nữa. Nó yêu cầu sự linh hoạt cao gấp nhiều lần. Vì vậy, mô hình SAVE ra đời như một bước chuyển mình cần thiết.Vậy mô hình SAVE là gì?Solution - Giải phápĐặt khách hàng làm trọng tâm, mô hình này khuyến khích doanh nghiệp không chỉ bán sản phẩm mà còn cung cấp những giải pháp thực sự đáp ứng nhu cầu cụ thể của khách hàng. "Solution" không chỉ đơn thuần là sản phẩm, mà là cách doanh nghiệp giải quyết vấn đề.Access - Tiếp cậnThời đại số, mọi thứ đều online được, doanh nghiệp không còn gò bó trong địa điểm cố định, mà hướng tới việc khách hàng có thể tiếp cận sản phẩm mọi lúc, mọi nơi. Vậy nên nhiệm vụ của bạn, những người làm marketing, là làm sao cung cấp thông tin mọi lúc, mọi nơi hoặc ngay khi có yêu cầu của khách hàng.Value - Giá trị"Value" tập trung vào lợi ích mà khách hàng nhận được thay vì chỉ nói về giá cả. Giá trị trong mô hình SAVE không chỉ dừng lại ở giá cả mà còn ở những giá trị như:Giá trị cảm nhận: Khách hàng cần cảm nhận được giá trị từ sản phẩm hoặc dịch vụ, bao gồm chất lượng, dịch vụ khách hàng, và những lợi ích bổ sung.Giá trị lâu dài: Doanh nghiệp nên làm cho khách hàng thấy được giá trị lâu dài của sản phẩm, không chỉ trong thời điểm mua mà còn trong suốt quá trình sử dụng.Education - Giáo dụcGiáo dục khách hàng là một phần quan trọng trong mô hình SAVE. Doanh nghiệp cần cung cấp thông tin rõ ràng và dễ hiểu về sản phẩm, dịch vụ và cách chúng có thể cải thiện đời sống của khách hàng.Không chỉ vậy, những doanh nghiệp nào đào tạo khách hàng về cách sử dụng sản phẩm một cách hiệu quả, dễ hiểu cũng sẽ giúp tăng cường sự hài lòng và lòng trung thành.Lưu ý để ứng dụng hiệu quả mô hình SAVE Giải pháp (Solution) - Tập trung giải quyết nhu cầu cá nhân hóaNghiên cứu và phân tích hành vi người tiêu dùng để xác định những khó khăn họ đang gặp phải.Thiết kế sản phẩm hoặc dịch vụ đáp ứng trực tiếp các vấn đề đó.Tiếp cận (Access) - Mở rộng khả năng truy cậpXây dựng hệ thống bán hàng đa kênh (omnichannel) tích hợp: website, ứng dụng di động, mạng xã hội, cửa hàng vật lý.Cải thiện trải nghiệm người dùng với giao diện thân thiện, dễ sử dụng.Giá trị (Value) - Tạo ra lợi ích vượt trộiThay vì cạnh tranh về giá, doanh nghiệp nên tập trung vào giá trị mà sản phẩm mang lại. Giá trị này không chỉ đến từ chất lượng mà còn từ dịch vụ hậu mãi, trải nghiệm thương hiệu, và sự độc đáo của sản phẩm.Giáo dục (Education) - Xây dựng sự tin tưởngXây dựng nội dung chất lượng cao như bài viết blog, video hướng dẫn, hội thảo trực tuyến, hoặc email tư vấn.Cung cấp thông tin giá trị giúp khách hàng hiểu rõ hơn về sản phẩm/dịch vụ và cách chúng giải quyết vấn đề của họ.Kết luậnMô hình SAVE không chỉ là một công cụ tiếp thị mà còn là kim chỉ nam giúp doanh nghiệp xây dựng mối quan hệ bền vững với khách hàng, tạo nên giá trị lâu dài và sự khác biệt trong thị trường đầy cạnh tranh.