Hướng dẫn cài đặt một dịch vụ OA mở rộng theo chuẩn của EzyOA
Back to ezyoaEzyOA cho phép tích hợp thêm một nền tảng nhắn tin mới bằng cách hiện thực chuẩn
OAService. Tuy nhiên, cách được khuyến nghị là kế thừa AbstractOAService để tái sử dụng các nghiệp vụ chung như định tuyến sự kiện, gửi tin hàng loạt, xử lý template, theo dõi kết quả gửi và sinh URL webhook.Kiến trúc mở rộng
Một dịch vụ OA đóng vai trò adapter giữa EzyOA và API của nhà cung cấp bên ngoài.
flowchart LR
Provider["Nhà cung cấp OA"] -->|Webhook| Controller["Webhook chung của EzyOA"]
Controller --> Service["OAService mở rộng"]
Service --> Listener["OA Listener"]
Listener --> Business["Chatbot hoặc nghiệp vụ"]
Business --> Service
Service -->|HTTP API| Provider
Dịch vụ mở rộng chịu trách nhiệm:
- Xác thực webhook.
- Chuẩn hóa loại sự kiện.
- Tạo dữ liệu webhook giả để kiểm thử.
- Gửi tin nhắn qua API của nhà cung cấp.
- Lấy thông tin người dùng OA.
- Đồng bộ người theo dõi nếu nền tảng hỗ trợ.
- Xử lý OAuth nếu nền tảng yêu cầu.
- Sao chép service khi nhiều tài khoản OA cùng dùng một implementation.
Các nghiệp vụ như lưu người dùng, lưu hội thoại, chatbot và xử lý template nên được giao cho các thành phần chung của EzyOA.
Chọn cách triển khai
Có hai cách tạo dịch vụ mới.
Hiện thực trực tiếp OAService
Cách này cho phép kiểm soát hoàn toàn hành vi của service nhưng phải tự hiện thực toàn bộ contract, bao gồm cả các nghiệp vụ EzyOA đã hỗ trợ sẵn.
Chỉ nên chọn cách này khi:
- Dịch vụ có mô hình vận hành rất khác EzyOA.
- Không muốn sử dụng cơ chế listener của EzyOA.
- Có yêu cầu đặc biệt về gửi hàng loạt hoặc template.
- Cần thay thế hoàn toàn luồng xử lý mặc định.
Kế thừa AbstractOAService
Đây là cách được khuyến nghị:
public class ExampleOAService extends AbstractOAService { public static final String SERVICE_CODE = "example"; public ExampleOAService() { this(SERVICE_CODE); } protected ExampleOAService(String code) { super(code); } }
Khi kế thừa lớp cơ sở, service được dùng chung các nghiệp vụ:
- Quản lý mã dịch vụ.
- Sinh webhook URL.
- Sinh OAuth callback URL.
- Phân phối sự kiện tới listener.
- Gửi tin nhắn qua transport mở rộng.
- Gửi tin tới toàn bộ người dùng theo từng lô.
- Gửi nội dung dựa trên template.
- Gửi notification dựa trên template.
- Thử gửi message trước rồi fallback sang notification.
- Tra cứu người nhận theo tài khoản hoặc số điện thoại.
- Lưu thời điểm và trạng thái của lần gửi gần nhất.
- Sao chép các dependency chung sang service mới.
Nhờ đó, implementation mới chủ yếu tập trung vào API và định dạng dữ liệu của nhà cung cấp.
Các phương thức bắt buộc
Một lớp kế thừa
AbstractOAService cần hiện thực các nhóm chức năng dưới đây.Xử lý xác thực OAuth
@Override public void handleAuthentication( Map<String, String> headers, Enumeration<String> parameterNames, Map<String, String> parameters, OAServiceSettingModel setting, String secretKey ) throws Exception { // Đổi authorization code lấy access token. // Kiểm tra state nếu nhà cung cấp sử dụng OAuth. // Lưu token thông qua service cấu hình bảo mật. }
Nếu nền tảng không sử dụng OAuth, có thể trả về lỗi không hỗ trợ:
@Override public void handleAuthentication( Map<String, String> headers, Enumeration<String> parameterNames, Map<String, String> parameters, OAServiceSettingModel setting, String secretKey ) { throw new UnsupportedOperationException("unsupported"); } @Override public String getAuthenticationCallbackUrl() { return ""; }
Không nên để callback URL mặc định nếu nhà cung cấp không có luồng OAuth, vì giao diện có thể khiến quản trị viên hiểu rằng chức năng này được hỗ trợ.
Tạo webhook giả để kiểm thử
EzyOA sử dụng
makeUserMessageFromText để mô phỏng một webhook mà không cần gửi tin từ nền tảng thật.@Override public Map<String, Object> makeUserMessageFromText( String oaUserId, String textMessage ) { return Map.of( "event", "message", "sender", Map.of("id", oaUserId), "message", Map.of("text", textMessage) ); }
Dữ liệu trả về phải có cùng cấu trúc với payload webhook thực tế. Nhờ đó, luồng kiểm thử đi qua đúng listener và bộ trích xuất dữ liệu.
Contract còn có phiên bản nhận thêm
mediaId. Mặc định, EzyOA từ chối media nếu service không ghi đè phương thức này. Chỉ cần hiện thực khi nền tảng hỗ trợ kiểm thử tin nhắn có tệp đính kèm.Xác thực webhook
@Override public OAVerifyResultModel verifyUserMessage( Map<String, String> headers, Enumeration<String> parameterNames, Map<String, String> parameters, String rawMessage, Map<String, Object> message ) { String signature = findHeaderIgnoreCase( headers, "X-Example-Signature" ); boolean valid = verifySignature( rawMessage, signature ); if (valid) { return OAVerifyResultModel.builder() .status(OAVerifyStatus.SUCCESS) .build(); } return OAVerifyResultModel.builder() .status(OAVerifyStatus.FAILED) .errorResponse( OAResponseModel.builder() .code(401) .data(Map.of("signature", "invalid")) .build() ) .build(); }
Kết quả xác thực có ba trạng thái về mặt xử lý:
-
SUCCESS: EzyOA tiếp tục xử lý sự kiện. -
FAILED: EzyOA trả response lỗi đã cung cấp. - Trạng thái khác: EzyOA bỏ qua payload nhưng vẫn phản hồi thành công.
Khi nhà cung cấp sử dụng chữ ký HMAC, phải tính chữ ký trên
rawMessage, không nên serialize lại message. Việc serialize lại JSON có thể thay đổi khoảng trắng hoặc thứ tự trường và làm chữ ký không khớp.
Tên header cũng nên được đọc không phân biệt chữ hoa, chữ thường vì cách biểu diễn header có thể thay đổi theo web container hoặc proxy.
Xác định loại sự kiện
AbstractOAService dùng tên sự kiện để tìm listener tương ứng:@Override protected String extractEventName( Map<String, Object> message ) { String event = (String) message.get("event"); if ("message.created".equals(event)) { return "user_send_text"; } if ("user.followed".equals(event)) { return "follow"; } if ("user.unfollowed".equals(event)) { return "unfollow"; } return ""; }
Tên trả về phải khớp với
getEventNames() của listener.Không nên thực hiện toàn bộ nghiệp vụ trong phương thức này. Nó chỉ nên đọc payload và trả về tên sự kiện đã chuẩn hóa.
Nếu tên sự kiện không có listener tương ứng,
AbstractOAService vẫn phản hồi thành công nhưng không chạy thêm nghiệp vụ nào.Gửi tin nhắn văn bản
Service phải hiện thực phương thức gửi tin cấp thấp:
@Override protected Object doSendMessage( long byAdminId, long byUserId, String recipientId, String message ) throws Exception { Map<String, Object> requestBody = Map.of( "recipient", recipientId, "message", Map.of("text", message) ); Object response = callProviderApi( "/messages", requestBody ); validateSendResponse(response); return response; }
Không nên ghi đè
sendMessage nếu không thực sự cần thiết. Phương thức public này đã bao bọc doSendMessage và cập nhật:- Thời điểm hệ thống gửi tin gần nhất.
- Trạng thái thành công hoặc thất bại.
- Transport được sử dụng.
- Thông tin người kích hoạt thao tác gửi.
Nếu API trả HTTP 200 nhưng body chứa mã lỗi, implementation phải tự kiểm tra body và ném exception. EzyOA chỉ coi thao tác thành công khi phương thức kết thúc mà không có exception.
Gửi tin nhắn có media
@Override protected Object doSendMessage( long byAdminId, long byUserId, String recipientId, String message, List<Map<String, Object>> medias ) throws Exception { Object lastResponse = null; if (message != null && !message.isBlank()) { lastResponse = doSendMessage( byAdminId, byUserId, recipientId, message ); } if (medias != null) { for (Map<String, Object> media : medias) { String type = (String) media.get("type"); String url = (String) media.get("url"); String fileName = (String) media.get("fileName"); lastResponse = sendMedia( recipientId, type, url, fileName ); } } return lastResponse; }
Một media thường có các trường:
-
type: loại nội dung nhưimage,audio,videohoặcdocument. -
url: URL tải nội dung. -
fileName: tên file. -
filePath: đường dẫn nội bộ nếu media đã được lưu trong hệ thống.
Implementation phải tự ánh xạ các loại media chung sang loại mà nhà cung cấp hỗ trợ. Đồng thời cần giới hạn kích thước, MIME type và nguồn URL trước khi tải hoặc gửi file.
Lấy thông tin người dùng OA
Khi nhận tin từ một người dùng chưa tồn tại, listener chung có thể yêu cầu service lấy thông tin tài khoản từ nhà cung cấp:
@Override public SaveOAUserModel getOAUserDetails( String oaUserId ) throws Exception { Map<String, Object> profile = fetchProfile(oaUserId); return SaveOAUserModel.builder() .serviceCode(getCode()) .oaUserId(oaUserId) .displayName((String) profile.get("displayName")) .avatarUrl((String) profile.get("avatarUrl")) .status("ACTIVATED") .build(); }
Hai trường quan trọng nhất là:
-
serviceCode: phải trả vềgetCode(). -
oaUserId: định danh người dùng do nhà cung cấp cấp.
Không nên giả định ID người dùng giống nhau giữa các dịch vụ OA. Cặp
serviceCode và oaUserId mới là định danh đầy đủ.
Đồng bộ người theo dõi
@Override public void fetchAllFollowers() throws Exception { String cursor = null; do { FollowerPage page = fetchFollowers(cursor); for (ProviderUser user : page.getUsers()) { saveOrUpdateFollower(user); } cursor = page.getNextCursor(); } while (cursor != null); }
Nếu nhà cung cấp không có API lấy danh sách người theo dõi, có thể để phương thức không thực hiện thao tác:
@Override public void fetchAllFollowers() { // Nền tảng không hỗ trợ đồng bộ danh sách follower. }
Không nên tự động suy ra toàn bộ người dùng từ lịch sử nếu điều đó không phù hợp với định nghĩa follower của nhà cung cấp.
Sao chép service
clone là một phần bắt buộc của contract:@Override public OAService clone(String newCode) { ExampleOAService clone = new ExampleOAService(newCode); cloneExampleFieldsTo(clone); return clone; } protected void cloneExampleFieldsTo( ExampleOAService target ) { cloneCommonFieldsTo(target); target.httpClient = this.httpClient; target.oaSettingService = this.oaSettingService; target.mediaService = this.mediaService; }
cloneCommonFieldsTo sao chép các dependency chung do AbstractOAService quản lý. Các dependency riêng của implementation vẫn phải được sao chép thủ công.Đây là bước dễ bị bỏ sót. Service được tạo động bằng
clone không đi qua quá trình khởi tạo bean thông thường, vì vậy dependency riêng có thể là null nếu không được sao chép.Mẫu service hoàn chỉnh
Ví dụ dưới đây thể hiện cấu trúc tối thiểu nên có. Các phần giao tiếp HTTP được để ở dạng hàm mô tả vì request thực tế phụ thuộc vào từng nhà cung cấp.
@Setter public class ExampleOAService extends AbstractOAService { public static final String SERVICE_CODE = "example"; @EzyAutoBind protected ExampleOASettingService oaSettingService; @EzyAutoBind protected ExampleHttpClient httpClient; public ExampleOAService() { this(SERVICE_CODE); } protected ExampleOAService(String code) { super(code); } @Override public void handleAuthentication( Map<String, String> headers, Enumeration<String> parameterNames, Map<String, String> parameters, OAServiceSettingModel setting, String secretKey ) throws Exception { String authorizationCode = parameters.get("code"); String state = parameters.get("state"); verifyState(state, setting); exchangeAndSaveAccessToken( authorizationCode, secretKey ); } @Override public Map<String, Object> makeUserMessageFromText( String oaUserId, String textMessage ) { return Map.of( "event", "message.created", "sender", Map.of("id", oaUserId), "message", Map.of("text", textMessage) ); } @Override public OAVerifyResultModel verifyUserMessage( Map<String, String> headers, Enumeration<String> parameterNames, Map<String, String> parameters, String rawMessage, Map<String, Object> message ) { boolean valid = verifyWebhookSignature( headers, rawMessage ); if (valid) { return OAVerifyResultModel.builder() .status(OAVerifyStatus.SUCCESS) .build(); } return OAVerifyResultModel.builder() .status(OAVerifyStatus.FAILED) .errorResponse( OAResponseModel.builder() .code(401) .data(Map.of("webhook", "invalid")) .build() ) .build(); } @Override protected String extractEventName( Map<String, Object> message ) { String event = (String) message.get("event"); if ("message.created".equals(event)) { return "user_send_text"; } if ("user.followed".equals(event)) { return "follow"; } if ("user.unfollowed".equals(event)) { return "unfollow"; } return ""; } @Override protected Object doSendMessage( long byAdminId, long byUserId, String recipientId, String message ) throws Exception { return sendTextToProvider( recipientId, message ); } @Override protected Object doSendMessage( long byAdminId, long byUserId, String recipientId, String message, List<Map<String, Object>> medias ) throws Exception { Object result = null; if (message != null && !message.isBlank()) { result = doSendMessage( byAdminId, byUserId, recipientId, message ); } if (medias != null) { for (Map<String, Object> media : medias) { result = sendMediaToProvider( recipientId, media ); } } return result; } @Override public void fetchAllFollowers() throws Exception { synchronizeFollowers(); } @Override public SaveOAUserModel getOAUserDetails( String oaUserId ) throws Exception { ExampleUser profile = fetchUserProfile(oaUserId); return SaveOAUserModel.builder() .serviceCode(getCode()) .oaUserId(oaUserId) .displayName(profile.getDisplayName()) .avatarUrl(profile.getAvatarUrl()) .status("ACTIVATED") .build(); } @Override public OAService clone(String newCode) { ExampleOAService clone = new ExampleOAService(newCode); cloneCommonFieldsTo(clone); clone.oaSettingService = this.oaSettingService; clone.httpClient = this.httpClient; return clone; } }
Đăng ký service vào runtime
EzyOA có runtime riêng cho trang quản trị và web. Service cần xuất hiện trong cả hai nếu muốn:
- Hiển thị và thao tác trong trang quản trị.
- Nhận webhook tại web runtime.
- Gửi tin từ cả luồng quản trị lẫn luồng tự động.
Tạo lớp đăng ký cho web runtime:
@Service public class WebExampleOAService extends ExampleOAService { }
Tạo lớp đăng ký tương ứng cho module quản trị:
@Service public class AdminExampleOAService extends ExampleOAService { }
Trình quản lý của EzyOA tự tìm các bean hiện thực
OAService và lập chỉ mục theo getCode(). Vì AbstractOAService đã hiện thực getCode() dựa trên giá trị truyền vào constructor, service không cần ghi đè phương thức này.Mã dịch vụ phải:
- Không rỗng.
- Ổn định sau khi triển khai.
- Không trùng với service khác.
- Được dùng nhất quán trong listener và các sender mở rộng.
- Tránh chỉ khác nhau ở chữ hoa và chữ thường.
EzyOA hỗ trợ tra cứu mã service không phân biệt hoa thường, vì vậy hai implementation có mã chỉ khác kiểu chữ có thể gây hành vi không xác định.
Cài đặt listener nhận tin nhắn
AbstractOAService chỉ định tuyến sự kiện. Để xử lý nội dung tin nhắn, cần đăng ký một OAListener.Nếu payload có cấu trúc mới hoàn toàn, có thể kế thừa listener nhận tin chung và hiện thực phần trích xuất:
public class ExampleOAUserSendMessageListener extends OAUserSendMessageListener { private final String serviceCode; public ExampleOAUserSendMessageListener() { this(ExampleOAService.SERVICE_CODE); } protected ExampleOAUserSendMessageListener( String serviceCode ) { this.serviceCode = serviceCode; } @SuppressWarnings("unchecked") @Override protected String extractSenderId( Map<String, Object> message ) { Map<String, Object> sender = (Map<String, Object>) message.get("sender"); return String.valueOf(sender.get("id")); } @SuppressWarnings("unchecked") @Override protected String extractTextMessage( Map<String, Object> message ) { Map<String, Object> messageData = (Map<String, Object>) message.get("message"); return (String) messageData.get("text"); } @Override public String getServiceCode() { return serviceCode; } @Override public String[] getEventNames() { return new String[]{"user_send_text"}; } @Override public OAListener clone(String newServiceCode) { ExampleOAUserSendMessageListener clone = new ExampleOAUserSendMessageListener( newServiceCode ); copyCommonFieldsTo(clone); return clone; } }
Listener nhận tin chung của EzyOA có thể tái sử dụng các nghiệp vụ:
- Tạo người dùng OA khi nhận tin lần đầu.
- Tạo hoặc lấy kênh hội thoại.
- Lưu nội dung và media.
- Chạy kịch bản phản hồi.
- Gọi chatbot.
- Gửi câu trả lời.
- Lưu tin nhắn phản hồi.
Tên sự kiện mà service trả về phải trùng với sự kiện listener đăng ký:
extractEventName(message) = "user_send_text" getEventNames() = ["user_send_text"]
Nếu hai giá trị không khớp, webhook vẫn có thể nhận phản hồi HTTP thành công nhưng tin nhắn sẽ không được xử lý.
Listener cũng cần được đăng ký thành bean trong web runtime. Có thể đăng ký thêm ở module quản trị nếu cần sử dụng chức năng gửi webhook giả tại trang quản trị.
Xác minh webhook subscription
Một số nền tảng gửi
GET tới webhook để xác minh endpoint. Khi đó cần ghi đè:@Override public OAResponseModel verifyWebhookSubscription( Map<String, String> parameters ) { String mode = parameters.get("mode"); String token = parameters.get("verify_token"); String challenge = parameters.get("challenge"); if ("subscribe".equals(mode) && getWebhookVerifyToken().equals(token) ) { return OAResponseModel.builder() .code(200) .data(challenge) .build(); } return OAResponseModel.builder() .code(403) .data("forbidden") .build(); }
Có thể cung cấp token qua:
@Override public String getWebhookVerifyToken() { return loadWebhookVerifyToken(getCode()); }
Nếu nhà cung cấp không có bước xác minh bằng
GET, không cần ghi đè. Phương thức mặc định báo không hỗ trợ.OAuth và URL tích hợp
AbstractOAService tự sinh:<web-url>/api/v1/oa/<service-code>/webhook <web-url>/oa/<service-code>/auth-callback
Để hiển thị URL bắt đầu OAuth, ghi đè:
@Override public String getOauthUrl() { return buildAuthorizationUrl( getAuthenticationCallbackUrl() ); }
Khi xây dựng OAuth URL cần:
- Dùng HTTPS.
- Tạo và kiểm tra
state. - Encode đầy đủ query parameter.
- Sử dụng đúng callback URL mà EzyOA hiển thị.
- Không đưa client secret vào URL.
- Lưu token bằng kiểu dữ liệu bảo mật.
- Hỗ trợ refresh token nếu nhà cung cấp yêu cầu.
Notification và template riêng của nhà cung cấp
Nếu nền tảng có API notification dựa trên template, ghi đè:
@Override public Object sendTemplatedNotification( long byAdminId, long byUserId, String transportType, String recipientId, String oaTemplateId, Map<String, Object> parameters ) throws Exception { return callNotificationApi( recipientId, oaTemplateId, parameters ); }
Phần ánh xạ từ content template của EzyPlatform sang template OA đã được
AbstractOAService xử lý. Implementation mới chỉ cần gửi:- ID người nhận.
- ID template của nhà cung cấp.
- Bộ tham số đã được chuẩn bị.
- Thông tin transport nếu có nhiều đường gửi.
Nếu nền tảng không hỗ trợ notification template, nên ném exception rõ ràng thay vì trả về thành công giả.
Typing indicator
Nếu nhà cung cấp hỗ trợ trạng thái đang nhập:
@Override public void sendTypingIndicator( String recipientId ) throws Exception { callSenderActionApi( recipientId, "typing_on" ); } @Override public long getTypingIndicatorIntervalInMillis() { return 5_000L; }
Giá trị interval bằng
0 nghĩa là không gửi typing indicator. Không nên đặt khoảng thời gian quá ngắn vì có thể vượt rate limit của nhà cung cấp.Upload media
Service có thể hỗ trợ upload media được lưu trong hệ thống:
@Override public OAUploadMediaResultModel uploadMediaById( long byAdminId, long byUserId, String transportType, long mediaId ) throws Exception { ProviderMedia response = uploadMedia(mediaId); return OAUploadMediaResultModel.builder() .mediaId(response.getId()) .build(); }
Nếu không ghi đè, implementation mặc định trả về một kết quả rỗng. Vì vậy phía gọi không nên xem object khác
null là bằng chứng upload đã thành công; cần kiểm tra dữ liệu kết quả cụ thể.Hỗ trợ nhiều tài khoản cùng loại
Một implementation có thể phục vụ nhiều tài khoản OA cùng nhà cung cấp.
Ví dụ,
example là service gốc. Trong trang quản trị có thể tạo:-
example_sales -
example_support -
example_internal
Mỗi cấu hình mới chọn
example làm dịch vụ xử lý. Khi được truy cập lần đầu, EzyOA:
- Tìm service gốc.
- Gọi
clone(newCode). - Sao chép listener.
- Sao chép extension sender.
- Sao chép template message sender.
- Sao chép template notification sender.
- Lưu service đã clone để tái sử dụng.
Mỗi service clone sử dụng mã riêng để đọc token, webhook secret và cấu hình riêng.
Vì vậy, không được lưu token của một tài khoản trong field mutable của service gốc. Nên tra cứu token theo
getCode() tại thời điểm gửi request.Luồng webhook hoàn chỉnh
sequenceDiagram
participant P as Nhà cung cấp OA
participant W as Web runtime
participant S as OAService
participant L as OA Listener
participant B as Nghiệp vụ
P->>W: POST webhook
W->>W: Tìm dịch vụ đang hoạt động
W->>S: verifyUserMessage(...)
alt Xác thực thất bại
S-->>W: FAILED và error response
W-->>P: Trả lỗi
else Bỏ qua sự kiện
S-->>W: Trạng thái bỏ qua
W-->>P: 200 OK
else Xác thực thành công
S-->>W: SUCCESS
W->>S: handleUserMessage(message)
S->>S: extractEventName(message)
S->>L: onMessageReceived(...)
L->>B: Lưu tin hoặc chạy kịch bản
B-->>L: Kết quả
S-->>W: 200 OK
W-->>P: Phản hồi webhook
end
Webhook chỉ được xử lý khi:
- Tìm thấy
OAService. - Có cấu hình dịch vụ tương ứng.
- Dịch vụ đang ở trạng thái hoạt động.
- Payload là JSON hợp lệ.
- Bước xác thực webhook cho phép xử lý.
Checklist triển khai
Trước khi đưa service mới vào sử dụng, cần kiểm tra:
- Service kế thừa
AbstractOAService. - Mã service là duy nhất và ổn định.
- Có constructor mặc định và constructor nhận mã service.
- Service được đăng ký ở cả admin và web runtime.
-
verifyUserMessagekiểm tra đúng chữ ký hoặc secret. - Chữ ký được tính từ raw request body.
-
extractEventNametrả đúng tên mà listener đăng ký. -
makeUserMessageFromTexttạo payload giống webhook thật. - Cả gửi text và gửi media đều xử lý lỗi response.
-
getOAUserDetailstrả đúngserviceCodevàoaUserId. -
clonegọicloneCommonFieldsTo. -
clonesao chép tất cả dependency riêng. - Listener và sender mở rộng cũng hỗ trợ clone nếu cần.
- Token được tra cứu theo
getCode(). - Không ghi token hoặc secret vào log.
- Các API gọi ra ngoài có timeout.
- Có xử lý rate limit và token hết hạn.
- Webhook trả response trong thời gian nhà cung cấp yêu cầu.
Khi nào không nên dùng AbstractOAService
Có thể hiện thực trực tiếp
OAService nếu cần thay đổi hoàn toàn:- Cơ chế phân phối sự kiện.
- Chiến lược gửi hàng loạt.
- Quy trình xử lý content template.
- Cơ chế tìm người nhận.
- Theo dõi trạng thái gửi.
- Mô hình clone service.
Tuy nhiên, implementation trực tiếp phải tự cung cấp toàn bộ phương thức trong contract. Điều này làm tăng đáng kể khối lượng code và nguy cơ hành vi không nhất quán với các dịch vụ OA có sẵn.
Trong hầu hết trường hợp, nên kế thừa
AbstractOAService và chỉ tùy biến các điểm giao tiếp với nhà cung cấp.Kết luận
Một dịch vụ OA mở rộng tốt nên hoạt động như một adapter mỏng: xác thực request, chuyển payload thành sự kiện chuẩn, gọi API gửi tin và lấy thông tin người dùng. Các nghiệp vụ chung như lưu hội thoại, chatbot, template, gửi hàng loạt và theo dõi trạng thái nên được giao cho
AbstractOAService và hệ thống listener của EzyOA.Cách tiếp cận này giúp service mới có hành vi nhất quán với Zalo OA, Messenger và Telegram Bot, đồng thời dễ bảo trì và có thể tái sử dụng cho nhiều tài khoản cùng nhà cung cấp.