Hướng dẫn cài đặt ChannelMemberInformationFetcher cung cấp thông tin người dùng cho EzyChat
Back to ezychatEzyChat lưu thành viên của kênh bằng hai giá trị chính:
-
memberType: loại thành viên hoặc nguồn dữ liệu. -
memberId: ID của thành viên trong nguồn dữ liệu đó.
Với người dùng nằm trong một hệ thống bên ngoài, EzyChat không tự biết tên hiển thị, email, số điện thoại hay ảnh đại diện.
ChannelMemberInformationFetcher là điểm mở rộng giúp EzyChat lấy các thông tin này từ plugin hoặc dịch vụ quản lý người dùng của bạn.
Ví dụ, một thành viên có thể được lưu như sau:
memberType = "crm_customers"
memberId = 1205
Khi cần hiển thị chi tiết thành viên, EzyChat tìm fetcher đăng ký cho loại
crm_customers, truyền vào ID 1205 và nhận lại thông tin khách hàng.Luồng hoạt động
sequenceDiagram
participant UI as Giao diện EzyChat
participant Chat as EzyChat
participant Manager as Fetcher manager
participant Fetcher as ChannelMemberInformationFetcher
participant Source as Nguồn dữ liệu người dùng
UI->>Chat: Yêu cầu thông tin thành viên
Chat->>Manager: Tìm fetcher theo memberType
Manager-->>Chat: Fetcher tương ứng
Chat->>Fetcher: Lấy thông tin theo memberId
Fetcher->>Source: Truy vấn người dùng
Source-->>Fetcher: Dữ liệu người dùng
Fetcher-->>Chat: ChannelMemberInformationModel
Chat-->>UI: Thông tin thành viên
Fetcher được chọn theo
memberType, không phải theo Java class hoặc tên plugin. Vì vậy, giá trị do getEntityType() trả về phải trùng chính xác với memberType được lưu trong thành viên của kênh.Thêm dependency
Interface và model cần thiết nằm trong EzyChat SDK. Nếu module của bạn chưa phụ thuộc SDK, thêm dependency sau:
<dependency> <groupId>org.youngmonkeys</groupId> <artifactId>ezychat-sdk</artifactId> <version></version> </dependency>
Nên dùng cùng phiên bản EzyChat đang được cài trên EzyPlatform.
Dữ liệu mà fetcher cung cấp
Fetcher trả về
ChannelMemberInformationModel với các trường:| Trường | Ý nghĩa |
|---|---|
userId | ID tài khoản EzyPlatform đã liên kết |
displayName | Tên hiển thị |
phone | Số điện thoại |
email | |
avatarImageId | ID ảnh đại diện trong hệ thống media |
createdAt | Thời điểm tạo bản ghi, tính bằng millisecond |
updatedAt | Thời điểm cập nhật bản ghi, tính bằng millisecond |
userId có ý nghĩa đặc biệt. Nếu giá trị này trỏ tới một tài khoản EzyPlatform hợp lệ, EzyChat sẽ ưu tiên thông tin của tài khoản đó khi tạo response:- UUID;
- username;
- tên hiển thị;
- số điện thoại;
- email;
- trạng thái;
- ảnh đại diện;
- thời điểm tạo và cập nhật.
Nếu chưa liên kết tài khoản hoặc không tìm thấy tài khoản tương ứng, EzyChat dùng thông tin do fetcher trả về.
Đối với ảnh đại diện, EzyChat ưu tiên ảnh của tài khoản EzyPlatform. Nếu tài khoản không có ảnh, hệ thống mới sử dụng
avatarImageId từ fetcher.Tạo service truy vấn người dùng
Giả sử plugin của bạn quản lý khách hàng CRM bằng model sau:
@Getter @Builder public class CustomerModel { private long id; private long userId; private String displayName; private String phone; private String email; private long avatarImageId; private long createdAt; private long updatedAt; }
Service nên hỗ trợ truy vấn nhiều ID trong một lần:
public interface CustomerService { Map<Long, CustomerModel> getCustomerMapByIds( Collection<Long> customerIds ); void updateUserIdByCustomerId( long customerId, long userId ); }
Truy vấn theo lô giúp tránh lỗi N+1 khi EzyChat cần lấy thông tin của nhiều thành viên.
Cài đặt fetcher
Tạo một bean triển khai
ChannelMemberInformationFetcher:import com.tvd12.ezyfox.bean.annotation.EzySingleton; import lombok.AllArgsConstructor; import org.youngmonkeys.ezychat.sdk.fetcher .ChannelMemberInformationFetcher; import org.youngmonkeys.ezychat.sdk.model .ChannelMemberInformationModel; import java.util.Collection; import java.util.Map; import java.util.stream.Collectors; @EzySingleton @AllArgsConstructor public class CustomerChannelMemberInformationFetcher implements ChannelMemberInformationFetcher { private final CustomerService customerService; @Override public Map<Long, ChannelMemberInformationModel> getMemberInformationMapByEntityIds( Collection<Long> entityIds ) { return customerService .getCustomerMapByIds(entityIds) .values() .stream() .collect( Collectors.toMap( CustomerModel::getId, customer -> ChannelMemberInformationModel.builder() .userId(customer.getUserId()) .displayName( customer.getDisplayName() ) .phone(customer.getPhone()) .email(customer.getEmail()) .avatarImageId( customer.getAvatarImageId() ) .createdAt(customer.getCreatedAt()) .updatedAt(customer.getUpdatedAt()) .build() ) ); } @Override public String getEntityType() { return "crm_customers"; } }
Annotation
@EzySingleton đăng ký implementation vào bean context. EzyChat tự lấy tất cả singleton triển khai interface này và lập bảng ánh xạ theo entity type.Không cần gọi thủ công một hàm kiểu
registerFetcher().Quy tắc của kết quả trả về
Map do
getMemberInformationMapByEntityIds trả về phải dùng entityId làm key:customer ID -> ChannelMemberInformationModel
Ví dụ, khi nhận danh sách
[10, 20], kết quả đúng có dạng:
{
10L -> informationOfCustomer10,
20L -> informationOfCustomer20
}
Không dùng
userId làm key. userId là tài khoản EzyPlatform liên kết với bản ghi bên ngoài, trong khi entityId chính là memberId được lưu trong kênh chat.Nếu một ID không tồn tại, chỉ cần bỏ ID đó khỏi map. Nên trả về map rỗng thay vì
null khi không tìm thấy dữ liệu:@Override public Map<Long, ChannelMemberInformationModel> getMemberInformationMapByEntityIds( Collection<Long> entityIds ) { if (entityIds == null || entityIds.isEmpty()) { return Collections.emptyMap(); } // Truy vấn và chuyển đổi dữ liệu. }
Interface đã cung cấp sẵn cách lấy một thành viên:
ChannelMemberInformationModel information =
fetcher.getMemberInformationByEntityId(entityId);
Phương thức mặc định này gọi API theo lô với một ID. Vì vậy, thông thường bạn chỉ cần triển khai
getMemberInformationMapByEntityIds.Hỗ trợ liên kết với tài khoản EzyPlatform
Giao diện quản trị EzyChat có thể liên kết thành viên bên ngoài với một tài khoản EzyPlatform. Khi đó, EzyChat gọi:
void updateUserIdByEntityId(long entityId, long userId)
Mặc định phương thức này trả về lỗi
memberType: unsupported. Nếu muốn hỗ trợ liên kết tài khoản, hãy override nó:@Override public void updateUserIdByEntityId( long entityId, long userId ) { customerService.updateUserIdByCustomerId( entityId, userId ); }
Phương thức này được dùng cho cả ba trường hợp:
- Liên kết với một tài khoản đã có.
- Tạo tài khoản mới rồi liên kết.
- Bỏ liên kết.
Khi bỏ liên kết, EzyChat truyền
userId = 0. Service của bạn cần hiểu giá trị này là xóa liên kết:
public void updateUserIdByCustomerId( long customerId, long userId ) { Customer customer = customerRepository .findById(customerId); if (customer == null) { throw new ResourceNotFoundException("customer"); } customer.setUserId(userId); customer.setUpdatedAt(System.currentTimeMillis()); customerRepository.save(customer); }
Nếu nguồn dữ liệu không cho phép chỉnh sửa hoặc không cần liên kết tài khoản, không cần override phương thức này. Người quản trị vẫn có thể xem thông tin do fetcher cung cấp, nhưng thao tác liên kết sẽ không được hỗ trợ.
Hỗ trợ nhiều member type bằng một fetcher
Một fetcher có thể phục vụ nhiều giá trị
memberType.Ví dụ, cùng một nguồn dữ liệu xử lý cả khách hàng cá nhân và khách hàng doanh nghiệp:
@Override public String getEntityType() { return "crm_customers"; } @Override public Collection<String> getEntityTypes() { return Arrays.asList( "crm_customers", "crm_business_customers" ); }
EzyChat đăng ký cùng fetcher cho tất cả giá trị được trả về từ
getEntityTypes().getEntityType() vẫn phải được triển khai vì đây là phương thức bắt buộc của interface. Khi chỉ hỗ trợ một loại thành viên, không cần override getEntityTypes().Mỗi entity type chỉ nên có một fetcher. Nếu nhiều bean cùng đăng ký một type, một implementation sẽ ghi đè implementation còn lại khi EzyChat tạo bảng ánh xạ. Không nên phụ thuộc vào thứ tự này.
Đăng ký trong admin và web runtime
EzyPlatform có thể sử dụng bean context riêng cho admin và web. Fetcher chỉ hoạt động trong runtime nơi bean được đăng ký.
Nếu thông tin người dùng chỉ xuất hiện trên màn hình quản trị, hãy đăng ký fetcher trong admin plugin.
Nếu web runtime cũng có API đọc thông tin thành viên, hãy đăng ký fetcher trong web plugin. Một cách tổ chức phù hợp là đặt logic chuyển đổi vào lớp dùng chung, sau đó tạo bean mỏng cho từng runtime.
Ví dụ phần dùng chung:
@AllArgsConstructor public class CustomerInformationFetcher implements ChannelMemberInformationFetcher { private final CustomerService customerService; @Override public Map<Long, ChannelMemberInformationModel> getMemberInformationMapByEntityIds( Collection<Long> entityIds ) { return customerService .getCustomerMapByIds(entityIds) .values() .stream() .collect( Collectors.toMap( CustomerModel::getId, this::toInformation ) ); } @Override public void updateUserIdByEntityId( long entityId, long userId ) { customerService.updateUserIdByCustomerId( entityId, userId ); } @Override public String getEntityType() { return "crm_customers"; } private ChannelMemberInformationModel toInformation( CustomerModel customer ) { return ChannelMemberInformationModel.builder() .userId(customer.getUserId()) .displayName(customer.getDisplayName()) .phone(customer.getPhone()) .email(customer.getEmail()) .avatarImageId(customer.getAvatarImageId()) .createdAt(customer.getCreatedAt()) .updatedAt(customer.getUpdatedAt()) .build(); } }
Bean phía admin:
@EzySingleton public class AdminCustomerInformationFetcher extends CustomerInformationFetcher { public AdminCustomerInformationFetcher( AdminCustomerService customerService ) { super(customerService); } }
Bean phía web:
@EzySingleton public class WebCustomerInformationFetcher extends CustomerInformationFetcher { public WebCustomerInformationFetcher( WebCustomerService customerService ) { super(customerService); } }
Tên service cụ thể phụ thuộc vào cách plugin chia sẻ repository và nghiệp vụ giữa hai runtime.
Nếu cùng một class được scan và tạo bean độc lập trong cả hai context, bạn có thể dùng trực tiếp một implementation thay vì tạo hai lớp con.
Gắn đúng member type khi tạo thành viên
Fetcher chỉ được gọi khi
memberType của thành viên khớp với entity type đã đăng ký.Ví dụ:
SaveChannelMemberModel member = SaveChannelMemberModel.builder()
.channelId(channelId)
.memberType("crm_customers")
.memberId(customerId)
.build();
Quan hệ cần được giữ nhất quán:
memberType = fetcher.getEntityType() memberId = ID mà fetcher có thể truy vấn
Nếu kênh lưu:
memberType = "crm_customer"
nhưng fetcher trả về:
return "crm_customers";
EzyChat sẽ không tìm thấy fetcher. Kết quả là thông tin chi tiết của thành viên có thể bị trống.
Nên khai báo entity type bằng hằng số dùng chung:
public final class CustomerConstants { public static final String CUSTOMER_MEMBER_TYPE = "crm_customers"; private CustomerConstants() {} }
Sau đó sử dụng hằng số ở cả nơi tạo thành viên và fetcher:
@Override public String getEntityType() { return CustomerConstants.CUSTOMER_MEMBER_TYPE; }
Cách EzyChat chọn dữ liệu hiển thị
Dữ liệu cuối cùng được tạo theo thứ tự sau:
flowchart TD
A[Nhận memberType và memberId] --> B[Tìm fetcher]
B --> C{Có fetcher?}
C -- Có --> D[Lấy thông tin bên ngoài]
C -- Không --> E{Là user type mặc định?}
D --> F{Có userId hợp lệ?}
E -- Có --> G[Dùng memberId làm userId]
E -- Không --> H[Thông tin người dùng trống]
F -- Có --> I[Lấy tài khoản EzyPlatform]
F -- Không --> J[Dùng thông tin từ fetcher]
G --> I
I --> K{Tìm thấy tài khoản?}
K -- Có --> L[Ưu tiên hồ sơ tài khoản]
K -- Không --> J
Nếu tìm thấy tài khoản EzyPlatform, các trường hồ sơ của tài khoản được ưu tiên. Fetcher không dùng để ghi đè tên hoặc email của một tài khoản đã liên kết.
avatarImageId có cách xử lý linh hoạt hơn: ảnh của tài khoản được ưu tiên, nhưng ảnh từ fetcher được dùng nếu tài khoản chưa có ảnh.Ví dụ implementation có xử lý dữ liệu thiếu
Nguồn dữ liệu thực tế có thể chứa bản ghi thiếu tên hoặc thông tin liên hệ. Fetcher nên chuyển đổi an toàn:
@EzySingleton @AllArgsConstructor public class CustomerChannelMemberInformationFetcher implements ChannelMemberInformationFetcher { private static final String ENTITY_TYPE = "crm_customers"; private final CustomerService customerService; @Override public Map<Long, ChannelMemberInformationModel> getMemberInformationMapByEntityIds( Collection<Long> entityIds ) { if (entityIds == null || entityIds.isEmpty()) { return Collections.emptyMap(); } return customerService .getCustomerMapByIds(entityIds) .entrySet() .stream() .filter(entry -> entry.getValue() != null) .collect( Collectors.toMap( Map.Entry::getKey, entry -> toInformation(entry.getValue()) ) ); } @Override public void updateUserIdByEntityId( long entityId, long userId ) { customerService.updateUserIdByCustomerId( entityId, userId ); } @Override public String getEntityType() { return ENTITY_TYPE; } private ChannelMemberInformationModel toInformation( CustomerModel customer ) { String displayName = customer.getDisplayName(); if (displayName == null || displayName.isBlank()) { displayName = customer.getPhone(); } if (displayName == null || displayName.isBlank()) { displayName = "Customer #" + customer.getId(); } return ChannelMemberInformationModel.builder() .userId(customer.getUserId()) .displayName(displayName) .phone(customer.getPhone()) .email(customer.getEmail()) .avatarImageId(customer.getAvatarImageId()) .createdAt(customer.getCreatedAt()) .updatedAt(customer.getUpdatedAt()) .build(); } }
Những lỗi thường gặp
Fetcher không được gọi
Kiểm tra class đã được đăng ký thành singleton trong đúng bean context và package chứa class có nằm trong phạm vi scan của plugin hay không.
Thông tin thành viên bị trống
Kiểm tra
getEntityType() có trùng chính xác với memberType của thành viên. Việc so khớp phân biệt chữ hoa và chữ thường.Fetcher tìm thấy dữ liệu nhưng EzyChat không dùng đúng bản ghi
Kiểm tra key của map có phải
entityId hay không. Không dùng userId làm key.Thông tin từ fetcher không xuất hiện sau khi liên kết tài khoản
Đây là hành vi dự kiến. Khi
userId trỏ tới một tài khoản hợp lệ, EzyChat ưu tiên hồ sơ EzyPlatform.Không thể liên kết hoặc bỏ liên kết tài khoản
Bạn chưa override
updateUserIdByEntityId, hoặc service không xử lý trường hợp userId = 0.Admin hiển thị đúng nhưng web không có dữ liệu
Fetcher mới được đăng ký trong admin context. Hãy đăng ký thêm bean tương ứng trong web runtime.
Một member type cho kết quả không ổn định
Có thể nhiều fetcher đang đăng ký cùng entity type. Chỉ giữ một fetcher cho mỗi type.
Checklist cài đặt
Trước khi hoàn tất tích hợp, kiểm tra:
- Module đã phụ thuộc
ezychat-sdk. - Fetcher triển khai
ChannelMemberInformationFetcher. - Fetcher được đăng ký bằng
@EzySingleton. -
getEntityType()trùng vớimemberTypetrong dữ liệu kênh. - Map kết quả dùng
entityIdlàm key. - Phương thức truy vấn hỗ trợ nhiều ID trong một lần.
- Không trả về
nullthay cho map rỗng. -
userIdphản ánh đúng tài khoản EzyPlatform đã liên kết. -
updateUserIdByEntityIdxử lý đượcuserId = 0nếu hỗ trợ bỏ liên kết. - Fetcher được đăng ký trong cả admin và web runtime nếu cả hai đều cần thông tin.
- Mỗi entity type chỉ có một fetcher.
Sau khi fetcher được đăng ký đúng, EzyChat có thể hiển thị thông tin người dùng từ CRM, hệ thống OA, cơ sở dữ liệu khách hàng hoặc bất kỳ nguồn thành viên bên ngoài nào mà không cần sao chép toàn bộ hồ sơ đó vào dữ liệu chat.