Hướng dẫn cài đặt một kịch bản phản hồi tin nhắn webhook gọi đến EzyOA
Back to ezyoaEzyOA cho phép plugin khác cài đặt một kịch bản tùy chỉnh để xử lý tin nhắn văn bản nhận từ webhook. Plugin chỉ cần hiện thực
OATextMessageResponseScenario và đăng ký implementation dưới dạng singleton trong đúng runtime.Khi webhook đến, EzyOA vẫn đảm nhiệm việc xác thực, nhận diện dịch vụ OA, tìm người gửi, lưu hội thoại và gửi phản hồi. Kịch bản của plugin chỉ chịu trách nhiệm tạo
OAResponseMessageModel.Luồng xử lý
sequenceDiagram
participant U as Người dùng
participant P as Nền tảng OA
participant O as EzyOA
participant S as Kịch bản của plugin
U->>P: Gửi tin nhắn
P->>O: Gọi webhook
O->>O: Xác thực và chuẩn hóa payload
O->>O: Tìm hoặc tạo OA user
O->>O: Tạo kênh hội thoại
O->>S: makeResponseMessage(...)
S-->>O: OAResponseMessageModel
O->>O: Lưu tin nhắn đến
O->>P: Gửi nội dung phản hồi
P-->>U: Hiển thị phản hồi
O->>O: Lưu tin nhắn trả lời nếu được bật
Kịch bản chỉ được gọi khi:
- Webhook được xác định là sự kiện người dùng gửi tin nhắn.
- Tin nhắn có nội dung văn bản hợp lệ.
- EzyOA đang bật phản hồi tự động.
- Dịch vụ OA cho phép phản hồi webhook đó.
- Tên kịch bản đang chọn khớp với
getName()của một scenario đã đăng ký.
Interface kịch bản
Plugin cần hiện thực interface:
import org.youngmonkeys.ezyoa.model.OAReceivedMessageModel; import org.youngmonkeys.ezyoa.model.OAResponseMessageModel; import org.youngmonkeys.ezyoa.scenario.OATextMessageResponseScenario; public class GreetingMessageResponseScenario implements OATextMessageResponseScenario { @Override public OAResponseMessageModel makeResponseMessage( String serviceCode, OAReceivedMessageModel receivedMessage ) throws Exception { return OAResponseMessageModel.builder() .message("Xin chào! Chúng tôi có thể giúp gì cho bạn?") .build(); } @Override public String getName() { return "greeting"; } }
getName() trả về mã định danh của kịch bản. Giá trị này:- Phải ổn định giữa các lần triển khai.
- Không nên chứa thông tin dành riêng cho một người dùng.
- Phải trùng với kịch bản được chọn trong phần cài đặt EzyOA.
- Không nên trùng với tên một kịch bản khác.
Dữ liệu tin nhắn nhận được
OAReceivedMessageModel cung cấp các thông tin đã được EzyOA chuẩn hóa:| Thuộc tính | Kiểu | Ý nghĩa |
|---|---|---|
senderOaUserIdText | String | ID người gửi do nền tảng OA cấp |
senderOaUserId | long | ID bản ghi OA user trong EzyOA |
senderUserId | long | ID người dùng EzyPlatform đã liên kết, có thể bằng 0 |
textMessage | String | Nội dung văn bản nhận được |
channelId | long | ID kênh hội thoại trong hệ thống |
Ví dụ đọc dữ liệu:
@Override public OAResponseMessageModel makeResponseMessage( String serviceCode, OAReceivedMessageModel receivedMessage ) { String textMessage = receivedMessage.getTextMessage(); String oaRecipientId = receivedMessage.getSenderOaUserIdText(); long userId = receivedMessage.getSenderUserId(); long channelId = receivedMessage.getChannelId(); // Xử lý nghiệp vụ và tạo phản hồi. }
Không nên nhầm:
-
senderOaUserIdTextlà ID bên nền tảng OA. -
senderOaUserIdlà khóa nội bộ của bản ghi OA user. -
senderUserIdlà ID tài khoản EzyPlatform đã liên kết.
Tạo kịch bản trong module dùng chung
Nếu plugin có cả admin plugin và web plugin, nên đặt logic chung trong SDK của plugin:
package com.example.notification.scenario; import org.youngmonkeys.ezyoa.model.OAReceivedMessageModel; import org.youngmonkeys.ezyoa.model.OAResponseMessageModel; import org.youngmonkeys.ezyoa.scenario.OATextMessageResponseScenario; public class OrderMessageResponseScenario implements OATextMessageResponseScenario { public static final String NAME = "order_support"; @Override public OAResponseMessageModel makeResponseMessage( String serviceCode, OAReceivedMessageModel receivedMessage ) throws Exception { String message = receivedMessage .getTextMessage() .trim(); if ("tra cứu đơn hàng".equalsIgnoreCase(message)) { return OAResponseMessageModel.builder() .templateName("order_lookup_requested") .message( "Vui lòng gửi mã đơn hàng cần tra cứu." ) .build(); } return OAResponseMessageModel.builder() .templateName("order_support_default") .message( "Bạn có thể nhập “tra cứu đơn hàng” để bắt đầu." ) .build(); } @Override public String getName() { return NAME; } }
templateName không tự động dịch template thành nội dung. Trong mô hình phản hồi này, plugin đã phải cung cấp message hoàn chỉnh. templateName chủ yếu được EzyOA lưu cùng lịch sử để nhận biết loại phản hồi đã sử dụng.Đăng ký trong web runtime
Webhook được xử lý tại web runtime. Vì vậy, kịch bản bắt buộc phải được đăng ký trong web plugin.
Tạo một lớp con và đánh dấu bằng
@EzySingleton:package com.example.notification.web.scenario; import com.example.notification.scenario .OrderMessageResponseScenario; import com.tvd12.ezyfox.bean.annotation.EzySingleton; @EzySingleton public class WebOrderMessageResponseScenario extends OrderMessageResponseScenario {}
Khi web plugin khởi động, lớp này được đưa vào singleton factory. EzyOA sẽ tự tìm tất cả singleton triển khai
OATextMessageResponseScenario và lập chỉ mục theo tên scenario.Đây là cách đăng ký được khuyến nghị. Plugin không cần gọi event và cũng không cần sửa mã nguồn EzyOA.
Đăng ký trong admin runtime
Để kịch bản xuất hiện trong giao diện cấu hình của EzyOA, nên đăng ký cùng một scenario trong admin plugin:
package com.example.notification.admin.scenario; import com.example.notification.scenario .OrderMessageResponseScenario; import com.tvd12.ezyfox.bean.annotation.EzySingleton; @EzySingleton public class AdminOrderMessageResponseScenario extends OrderMessageResponseScenario {}
Admin runtime sử dụng scenario để:
- Liệt kê các kịch bản có thể chọn.
- Lấy mẫu tham số cấu hình.
- Hỗ trợ kiểm tra phản hồi trong trang quản trị.
Web runtime mới là nơi thực sự xử lý webhook. Vì vậy:
- Chỉ có bản admin: scenario xuất hiện trong cấu hình nhưng webhook không thực thi được.
- Chỉ có bản web: webhook có thể thực thi nếu tên được cấu hình thủ công, nhưng scenario có thể không xuất hiện trong giao diện quản trị.
- Có cả hai: đầy đủ chức năng cấu hình và xử lý.
Hai implementation phải trả về cùng một giá trị từ
getName().
Đăng ký trực tiếp bằng manager
Ngoài cơ chế singleton, plugin có thể đăng ký trực tiếp bằng service manager.
Trong web plugin:
import com.tvd12.ezyfox.bean.EzyBeanConfig; import com.tvd12.ezyfox.bean.annotation.EzyConfigurationAfter; import lombok.AllArgsConstructor; import org.youngmonkeys.ezyoa.web.manager .WebOATextMessageResponseScenarioManager; @EzyConfigurationAfter @AllArgsConstructor public class WebOaScenarioConfig implements EzyBeanConfig { private final WebOATextMessageResponseScenarioManager scenarioManager; @Override public void config() { scenarioManager.addOaMessageResponseScenario( new OrderMessageResponseScenario() ); } }
Trong admin plugin:
import com.tvd12.ezyfox.bean.EzyBeanConfig; import com.tvd12.ezyfox.bean.annotation.EzyConfigurationAfter; import lombok.AllArgsConstructor; import org.youngmonkeys.ezyoa.admin.manager .AdminOATextMessageResponseScenarioManager; @EzyConfigurationAfter @AllArgsConstructor public class AdminOaScenarioConfig implements EzyBeanConfig { private final AdminOATextMessageResponseScenarioManager scenarioManager; @Override public void config() { scenarioManager.addOaMessageResponseScenario( new OrderMessageResponseScenario() ); } }
Cách đăng ký trực tiếp hữu ích khi:
- Scenario được tạo theo cấu hình động.
- Plugin cần chọn implementation lúc khởi động.
- Scenario không thể được khai báo trực tiếp thành singleton.
Nếu scenario cần dependency injection, đăng ký bằng
@EzySingleton thường đơn giản và an toàn hơn so với tự gọi constructor.
Khi hai scenario có cùng tên, scenario được thêm sau sẽ thay thế scenario trước trong manager. Vì vậy tên scenario cần có tính duy nhất.
Inject service nghiệp vụ vào scenario
Scenario có thể sử dụng các service của plugin để tra cứu dữ liệu và tạo câu trả lời.
import lombok.AllArgsConstructor; import org.youngmonkeys.ezyoa.model.OAReceivedMessageModel; import org.youngmonkeys.ezyoa.model.OAResponseMessageModel; import org.youngmonkeys.ezyoa.scenario.OATextMessageResponseScenario; @AllArgsConstructor public class OrderMessageResponseScenario implements OATextMessageResponseScenario { private final OrderService orderService; @Override public OAResponseMessageModel makeResponseMessage( String serviceCode, OAReceivedMessageModel receivedMessage ) { String orderCode = receivedMessage .getTextMessage() .trim(); OrderModel order = orderService .getOrderByCode(orderCode); if (order == null) { return OAResponseMessageModel.builder() .templateName("order_not_found") .message( "Không tìm thấy đơn hàng " + orderCode + "." ) .build(); } return OAResponseMessageModel.builder() .templateName("order_status") .message( "Đơn hàng " + order.getCode() + " đang ở trạng thái " + order.getDisplayStatus() + "." ) .build(); } @Override public String getName() { return "order_status"; } }
Sau đó khai báo implementation riêng cho từng runtime:
@EzySingleton public class WebOrderMessageResponseScenario extends OrderMessageResponseScenario { public WebOrderMessageResponseScenario( WebOrderService orderService ) { super(orderService); } }
@EzySingleton public class AdminOrderMessageResponseScenario extends OrderMessageResponseScenario { public AdminOrderMessageResponseScenario( AdminOrderService orderService ) { super(orderService); } }
Không nên gọi API chậm hoặc thực hiện tác vụ nền kéo dài ngay trong scenario. Scenario đang nằm trên đường xử lý webhook; thời gian tạo phản hồi quá lâu có thể làm nhà cung cấp OA hết thời gian chờ và gửi lại webhook.
Phản hồi theo từng dịch vụ OA
Một scenario có thể được dùng chung cho nhiều dịch vụ. Tham số
serviceCode cho biết webhook thuộc dịch vụ nào:@Override public OAResponseMessageModel makeResponseMessage( String serviceCode, OAReceivedMessageModel receivedMessage ) { String response; switch (serviceCode.toLowerCase()) { case "zalo": response = "Cảm ơn bạn đã liên hệ Zalo OA."; break; case "telegram": response = "Cảm ơn bạn đã liên hệ Telegram Bot."; break; default: response = "Cảm ơn bạn đã liên hệ."; } return OAResponseMessageModel.builder() .message(response) .build(); }
Nếu không thực sự cần khác biệt giữa các nền tảng, nên giữ logic chung để giảm số nhánh phải bảo trì.
Không gửi phản hồi
Trả về
null khi plugin muốn EzyOA tiếp nhận và lưu tin nhắn nhưng không gửi câu trả lời:@Override public OAResponseMessageModel makeResponseMessage( String serviceCode, OAReceivedMessageModel receivedMessage ) { String message = receivedMessage.getTextMessage(); if (!message.startsWith("/")) { return null; } return OAResponseMessageModel.builder() .message("Đã nhận lệnh: " + message) .build(); }
Nếu trả về một model không có cả nội dung lẫn media, EzyOA cũng không gửi phản hồi:
return OAResponseMessageModel.builder().build();
Trả về
null thể hiện ý định bỏ qua rõ ràng hơn.Gửi phản hồi kèm media
Scenario có thể trả về nội dung cùng danh sách media:
import org.youngmonkeys.ezyoa.model.OAResponseMediaModel; import org.youngmonkeys.ezyoa.model.OAResponseMessageModel; import java.util.Collections; return OAResponseMessageModel.builder() .templateName("payment_qr") .message("Bạn có thể thanh toán bằng mã QR sau:") .medias( Collections.singletonList( OAResponseMediaModel.builder() .type("image") .fileName("payment-qr.png") .filePath("images/payment-qr.png") .build() ) ) .build();
Một media có thể chứa:
| Thuộc tính | Ý nghĩa |
|---|---|
type | Loại media, chẳng hạn image |
url | URL của media |
fileName | Tên file |
filePath | Đường dẫn file trong hệ thống |
EzyOA chuyển các thuộc tính này sang
OAService.sendMessage. Việc trường nào được hỗ trợ phụ thuộc vào từng dịch vụ OA.Nếu danh sách media không rỗng, EzyOA gọi phiên bản
sendMessage có media, kể cả khi nội dung văn bản rỗng.Khai báo mẫu tham số cấu hình
Scenario có thể cung cấp mẫu tham số bằng
getParametersTemplate():import java.util.LinkedHashMap; import java.util.Map; @Override public Map<String, Object> getParametersTemplate() { Map<String, Object> template = new LinkedHashMap<>(); template.put("welcomeMessage", ""); template.put("supportUrl", ""); return template; }
Admin runtime sử dụng map này để tạo cấu trúc tham số ban đầu khi quản trị viên chọn scenario.
Đây chỉ là mẫu cấu hình, không phải giá trị cấu hình thực tế của từng OA. Nếu scenario cần đọc giá trị đã lưu cho một dịch vụ, nó phải sử dụng service cấu hình OA với
serviceCode hiện tại.Không nên đặt secret, access token hoặc API key vào mẫu tham số scenario.
Chọn kịch bản trong EzyOA
Sau khi cài plugin:
- Mở phần cài đặt EzyOA.
- Bật chức năng phản hồi tự động.
- Mở phần cấu hình kịch bản.
- Chọn tên scenario do plugin cung cấp.
- Nếu scenario có tham số, cấu hình tham số cho từng dịch vụ OA.
- Lưu thay đổi.
- Gửi một tin nhắn thật đến OA để kiểm tra.
Tên hiển thị trong danh sách chính là giá trị do
getName() trả về.
Lựa chọn kịch bản phản hồi văn bản là cấu hình chung. Nếu cùng một scenario phục vụ nhiều OA, scenario có thể dùng
serviceCode để đọc cấu hình riêng của từng dịch vụ.Cách EzyOA sử dụng kết quả
Sau khi scenario trả về
OAResponseMessageModel, EzyOA xử lý như sau:- Lấy
templateName,messagevàmedias. - Lưu tin nhắn webhook vào lịch sử.
- Nếu có nội dung văn bản và không có media, gọi
OAService.sendMessage(recipientId, message). - Nếu có media, gọi overload gửi tin có media.
- Nếu gửi thành công và tính năng lưu phản hồi đang bật, lưu câu trả lời vào cùng kênh hội thoại.
- Nếu scenario trả về
null, chỉ lưu tin nhắn đến và không gửi phản hồi.
EzyOA gửi phản hồi bằng chính
OAService đã tiếp nhận webhook. Plugin không cần tự lấy service hoặc tự gọi sendMessage bên trong scenario.
Việc tự gửi trong
makeResponseMessage rồi vẫn trả về một response có thể làm người dùng nhận hai tin nhắn.Xử lý exception
makeResponseMessage cho phép ném Exception:@Override public OAResponseMessageModel makeResponseMessage( String serviceCode, OAReceivedMessageModel receivedMessage ) throws Exception { return externalService.createResponse( receivedMessage.getTextMessage() ); }
Nếu exception thoát khỏi scenario, luồng xử lý webhook có thể thất bại. Tùy nền tảng, webhook có thể được gửi lại và gây xử lý lặp.
Với lỗi có thể phục hồi, nên xử lý ngay trong scenario:
@Override public OAResponseMessageModel makeResponseMessage( String serviceCode, OAReceivedMessageModel receivedMessage ) { try { return responseService.makeResponse( receivedMessage.getTextMessage() ); } catch (Exception e) { logger.warn( "Không thể tạo phản hồi OA, service: {}", serviceCode, e ); return OAResponseMessageModel.builder() .message( "Hệ thống đang bận. Vui lòng thử lại sau." ) .build(); } }
Không nên bắt mọi lỗi rồi âm thầm trả về
null nếu lỗi đó cần được theo dõi vận hành.Kiểm tra tích hợp
Nên kiểm tra các trường hợp sau:
- Scenario xuất hiện trong giao diện quản trị.
- Scenario đã được chọn và lưu thành công.
- Phản hồi tự động đang bật.
- Tin nhắn văn bản tạo đúng câu trả lời.
- Tin nhắn không khớp điều kiện không tạo phản hồi.
-
serviceCodeđúng với OA nhận webhook. - OA user chưa liên kết vẫn được xử lý an toàn.
- Media được gửi đúng trên từng nền tảng.
- Tin nhắn đến và phản hồi được lưu đúng kênh.
- Exception không làm phát sinh vòng lặp retry ngoài ý muốn.
- Cùng một webhook không gây tác dụng phụ nghiệp vụ lặp lại.
Giới hạn cần lưu ý
Kịch bản phản hồi văn bản mặc định có các giới hạn:
- Chỉ chạy cho webhook có nội dung văn bản hợp lệ.
- Media đến có thể được lưu nhưng không được truyền vào
OAReceivedMessageModel. - Chỉ một tên scenario phản hồi văn bản được chọn tại một thời điểm.
- Scenario chạy đồng bộ trong luồng xử lý webhook.
- Scenario không tự chống xử lý trùng webhook.
- EzyOA không tự giới hạn thời gian thực thi logic của plugin.
-
templateNamechỉ là metadata nếu plugin tự tạo nội dung phản hồi. - Khả năng gửi media phụ thuộc vào từng
OAService.
Kịch bản nên tập trung vào việc quyết định phản hồi và tạo dữ liệu trả về. Những tác vụ lâu, gửi email, đồng bộ dữ liệu lớn hoặc xử lý nền nên được chuyển sang một worker hoặc hàng đợi riêng.