@warriorteam/redai-zalo-sdk 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/API_REFERENCE.md +680 -0
- package/docs/AUTHENTICATION.md +709 -0
- package/docs/MESSAGE_SERVICES.md +1224 -0
- package/docs/TAG_MANAGEMENT.md +1462 -0
- package/docs/USER_MANAGEMENT.md +1248 -0
- package/docs/ZNS_SERVICE.md +985 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1248 @@
|
|
|
1
|
+
# RedAI Zalo SDK - User Management Guide
|
|
2
|
+
|
|
3
|
+
## Tổng quan
|
|
4
|
+
|
|
5
|
+
User Management trong RedAI Zalo SDK cung cấp các công cụ toàn diện để quản lý người dùng, bao gồm:
|
|
6
|
+
|
|
7
|
+
- 👥 **UserService** - Truy cập thông tin user social và OA followers
|
|
8
|
+
- 🏷️ **UserManagementService** - Quản lý user profiles và interactions
|
|
9
|
+
- 📊 **User Analytics** - Phân tích hành vi và tương tác
|
|
10
|
+
- 🔍 **User Search** - Tìm kiếm và lọc users
|
|
11
|
+
- 📋 **Bulk Operations** - Xử lý hàng loạt users
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## UserService
|
|
16
|
+
|
|
17
|
+
Truy cập thông tin user từ Social API và OA followers.
|
|
18
|
+
|
|
19
|
+
### Khởi tạo
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { ZaloSDK } from "@warriorteam/redai-zalo-sdk";
|
|
23
|
+
|
|
24
|
+
const zalo = new ZaloSDK({
|
|
25
|
+
appId: "your-app-id",
|
|
26
|
+
appSecret: "your-app-secret"
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Access user service
|
|
30
|
+
const userService = zalo.user;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 1. Lấy thông tin user Social
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Lấy thông tin user từ Social API
|
|
37
|
+
const socialUserInfo = await zalo.getSocialUserInfo(
|
|
38
|
+
socialAccessToken,
|
|
39
|
+
"id,name,picture,birthday,gender,locale"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
console.log("User ID:", socialUserInfo.id);
|
|
43
|
+
console.log("Name:", socialUserInfo.name);
|
|
44
|
+
console.log("Avatar:", socialUserInfo.picture?.data.url);
|
|
45
|
+
console.log("Birthday:", socialUserInfo.birthday);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Lấy thông tin user OA
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Lấy thông tin user đã tương tác với OA
|
|
52
|
+
const userInfo = await zalo.user.getUserInfo(
|
|
53
|
+
oaAccessToken,
|
|
54
|
+
"zalo-user-id"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
console.log("User Info:", {
|
|
58
|
+
userId: userInfo.user_id,
|
|
59
|
+
displayName: userInfo.display_name,
|
|
60
|
+
avatar: userInfo.avatar,
|
|
61
|
+
userGender: userInfo.user_gender,
|
|
62
|
+
userAlias: userInfo.user_alias
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Lấy danh sách users
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// Lấy danh sách users với filter
|
|
70
|
+
const userList = await zalo.user.getUserList(oaAccessToken, {
|
|
71
|
+
offset: 0,
|
|
72
|
+
count: 50,
|
|
73
|
+
tag_name: "VIP_CUSTOMER", // Lọc theo tag (tùy chọn)
|
|
74
|
+
is_follower: true // Chỉ lấy followers (tùy chọn)
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.log("Total users:", userList.total);
|
|
78
|
+
userList.data.forEach(user => {
|
|
79
|
+
console.log(`${user.display_name} - ${user.user_id}`);
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. Lấy danh sách followers
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Lấy tất cả followers của OA
|
|
87
|
+
const followers = await zalo.user.getFollowers(oaAccessToken, {
|
|
88
|
+
offset: 0,
|
|
89
|
+
count: 100
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
console.log("Total followers:", followers.total);
|
|
93
|
+
followers.data.forEach(follower => {
|
|
94
|
+
console.log(`Follower: ${follower.display_name}`);
|
|
95
|
+
console.log(`Follow time: ${new Date(follower.user_id_by_app).toLocaleString()}`);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## UserManagementService
|
|
102
|
+
|
|
103
|
+
Quản lý user profiles chi tiết và interactions.
|
|
104
|
+
|
|
105
|
+
### Khởi tạo
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const userManagement = zalo.userManagement;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 1. Lấy user profile chi tiết
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Lấy profile đầy đủ của user
|
|
115
|
+
const userProfile = await zalo.userManagement.getUserProfile(
|
|
116
|
+
oaAccessToken,
|
|
117
|
+
"user-zalo-id"
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
console.log("User Profile:", {
|
|
121
|
+
userId: userProfile.user_id,
|
|
122
|
+
displayName: userProfile.display_name,
|
|
123
|
+
avatar: userProfile.avatar,
|
|
124
|
+
phone: userProfile.shared_info?.phone,
|
|
125
|
+
address: userProfile.shared_info?.address,
|
|
126
|
+
tags: userProfile.tags,
|
|
127
|
+
notes: userProfile.notes,
|
|
128
|
+
lastInteraction: userProfile.last_interaction_time
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 2. Cập nhật user profile
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Cập nhật thông tin user
|
|
136
|
+
const updatedProfile = await zalo.userManagement.updateUserProfile(
|
|
137
|
+
oaAccessToken,
|
|
138
|
+
"user-zalo-id",
|
|
139
|
+
{
|
|
140
|
+
notes: "Customer quan tâm sản phẩm cao cấp",
|
|
141
|
+
custom_fields: {
|
|
142
|
+
customer_tier: "VIP",
|
|
143
|
+
preferred_contact: "zalo",
|
|
144
|
+
last_purchase_date: "2024-12-01",
|
|
145
|
+
total_spent: "5000000"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 3. Lấy lịch sử tương tác
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// Lấy tất cả interactions với user
|
|
155
|
+
const interactions = await zalo.userManagement.getUserInteractions(
|
|
156
|
+
oaAccessToken,
|
|
157
|
+
"user-zalo-id",
|
|
158
|
+
{
|
|
159
|
+
from_time: Date.now() - (30 * 24 * 60 * 60 * 1000), // 30 days ago
|
|
160
|
+
to_time: Date.now(),
|
|
161
|
+
interaction_type: "all", // message, call, order, etc.
|
|
162
|
+
limit: 100
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
interactions.forEach(interaction => {
|
|
167
|
+
console.log(`${interaction.type}: ${interaction.content} at ${new Date(interaction.time).toLocaleString()}`);
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 4. Phân tích user analytics
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Lấy analytics của user
|
|
175
|
+
const userAnalytics = await zalo.userManagement.getUserAnalytics(
|
|
176
|
+
oaAccessToken,
|
|
177
|
+
"user-zalo-id"
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
console.log("User Analytics:", {
|
|
181
|
+
totalMessages: userAnalytics.total_messages_sent,
|
|
182
|
+
totalMessagesReceived: userAnalytics.total_messages_received,
|
|
183
|
+
avgResponseTime: userAnalytics.avg_response_time,
|
|
184
|
+
engagementScore: userAnalytics.engagement_score,
|
|
185
|
+
lastSeenTime: userAnalytics.last_seen_time,
|
|
186
|
+
preferredTime: userAnalytics.most_active_time
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 5. Tìm kiếm users
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// Tìm kiếm users theo điều kiện
|
|
194
|
+
const searchResult = await zalo.userManagement.searchUsers(
|
|
195
|
+
oaAccessToken,
|
|
196
|
+
{
|
|
197
|
+
query: "Nguyễn", // Tên hoặc phone
|
|
198
|
+
tags: ["VIP", "Premium"], // Tags
|
|
199
|
+
interaction_period: 30, // Tương tác trong 30 ngày qua
|
|
200
|
+
min_order_value: 1000000, // Đơn hàng tối thiểu
|
|
201
|
+
location: "Ho Chi Minh", // Địa điểm
|
|
202
|
+
gender: "male", // Giới tính
|
|
203
|
+
age_range: "25-35", // Độ tuổi
|
|
204
|
+
limit: 50,
|
|
205
|
+
offset: 0
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
console.log(`Found ${searchResult.total} users matching criteria`);
|
|
210
|
+
searchResult.users.forEach(user => {
|
|
211
|
+
console.log(`${user.display_name} - Score: ${user.match_score}`);
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## User Segmentation & Analytics
|
|
218
|
+
|
|
219
|
+
### 1. User Segmentation Service
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
class UserSegmentationService {
|
|
223
|
+
constructor(private zalo: ZaloSDK, private accessToken: string) {}
|
|
224
|
+
|
|
225
|
+
// Phân đoạn users theo hành vi mua hàng
|
|
226
|
+
async segmentUsersByPurchaseBehavior(): Promise<UserSegments> {
|
|
227
|
+
const allUsers = await this.getAllUsers();
|
|
228
|
+
const segments = {
|
|
229
|
+
highValue: [], // > 10M VND
|
|
230
|
+
mediumValue: [], // 1M - 10M VND
|
|
231
|
+
lowValue: [], // < 1M VND
|
|
232
|
+
inactive: [] // Không mua trong 90 ngày
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
for (const user of allUsers) {
|
|
236
|
+
const analytics = await this.zalo.userManagement.getUserAnalytics(
|
|
237
|
+
this.accessToken,
|
|
238
|
+
user.user_id
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const totalSpent = analytics.total_spent || 0;
|
|
242
|
+
const daysSinceLastPurchase = analytics.days_since_last_purchase || 999;
|
|
243
|
+
|
|
244
|
+
if (daysSinceLastPurchase > 90) {
|
|
245
|
+
segments.inactive.push(user);
|
|
246
|
+
} else if (totalSpent > 10000000) {
|
|
247
|
+
segments.highValue.push(user);
|
|
248
|
+
} else if (totalSpent > 1000000) {
|
|
249
|
+
segments.mediumValue.push(user);
|
|
250
|
+
} else {
|
|
251
|
+
segments.lowValue.push(user);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return segments;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Phân đoạn theo engagement
|
|
259
|
+
async segmentUsersByEngagement(): Promise<EngagementSegments> {
|
|
260
|
+
const users = await this.getAllUsers();
|
|
261
|
+
const segments = {
|
|
262
|
+
champions: [], // High value + High engagement
|
|
263
|
+
loyalists: [], // High engagement
|
|
264
|
+
potential: [], // Medium engagement
|
|
265
|
+
atRisk: [], // Low recent engagement
|
|
266
|
+
lost: [] // No recent engagement
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
for (const user of users) {
|
|
270
|
+
const analytics = await this.zalo.userManagement.getUserAnalytics(
|
|
271
|
+
this.accessToken,
|
|
272
|
+
user.user_id
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const engagementScore = analytics.engagement_score || 0;
|
|
276
|
+
const daysSinceLastInteraction = analytics.days_since_last_interaction || 999;
|
|
277
|
+
const totalSpent = analytics.total_spent || 0;
|
|
278
|
+
|
|
279
|
+
if (engagementScore > 80 && totalSpent > 5000000) {
|
|
280
|
+
segments.champions.push(user);
|
|
281
|
+
} else if (engagementScore > 70) {
|
|
282
|
+
segments.loyalists.push(user);
|
|
283
|
+
} else if (engagementScore > 40) {
|
|
284
|
+
segments.potential.push(user);
|
|
285
|
+
} else if (daysSinceLastInteraction < 30) {
|
|
286
|
+
segments.atRisk.push(user);
|
|
287
|
+
} else {
|
|
288
|
+
segments.lost.push(user);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return segments;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async getAllUsers(): Promise<UserProfile[]> {
|
|
296
|
+
const allUsers = [];
|
|
297
|
+
let offset = 0;
|
|
298
|
+
const limit = 100;
|
|
299
|
+
|
|
300
|
+
while (true) {
|
|
301
|
+
const userList = await this.zalo.user.getUserList(this.accessToken, {
|
|
302
|
+
offset,
|
|
303
|
+
count: limit
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
allUsers.push(...userList.data);
|
|
307
|
+
|
|
308
|
+
if (userList.data.length < limit) break;
|
|
309
|
+
offset += limit;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return allUsers;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### 2. Customer Lifetime Value Analysis
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
class CLVAnalysisService {
|
|
321
|
+
constructor(private zalo: ZaloSDK, private accessToken: string) {}
|
|
322
|
+
|
|
323
|
+
async calculateUserCLV(userId: string): Promise<CLVMetrics> {
|
|
324
|
+
const analytics = await this.zalo.userManagement.getUserAnalytics(
|
|
325
|
+
this.accessToken,
|
|
326
|
+
userId
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const interactions = await this.zalo.userManagement.getUserInteractions(
|
|
330
|
+
this.accessToken,
|
|
331
|
+
userId,
|
|
332
|
+
{ interaction_type: "purchase", limit: 1000 }
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const purchases = interactions.filter(i => i.type === "purchase");
|
|
336
|
+
|
|
337
|
+
if (purchases.length === 0) {
|
|
338
|
+
return { clv: 0, tier: "inactive", recommendations: [] };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Tính toán CLV
|
|
342
|
+
const totalSpent = purchases.reduce((sum, p) => sum + (p.value || 0), 0);
|
|
343
|
+
const avgOrderValue = totalSpent / purchases.length;
|
|
344
|
+
const purchaseFrequency = this.calculatePurchaseFrequency(purchases);
|
|
345
|
+
const customerLifespan = this.calculateCustomerLifespan(purchases);
|
|
346
|
+
|
|
347
|
+
const clv = avgOrderValue * purchaseFrequency * customerLifespan;
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
clv,
|
|
351
|
+
avgOrderValue,
|
|
352
|
+
purchaseFrequency,
|
|
353
|
+
customerLifespan,
|
|
354
|
+
totalOrders: purchases.length,
|
|
355
|
+
totalSpent,
|
|
356
|
+
tier: this.determineTier(clv),
|
|
357
|
+
recommendations: this.generateRecommendations(clv, analytics)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private calculatePurchaseFrequency(purchases: any[]): number {
|
|
362
|
+
if (purchases.length < 2) return 0;
|
|
363
|
+
|
|
364
|
+
const timespan = purchases[0].time - purchases[purchases.length - 1].time;
|
|
365
|
+
const days = timespan / (24 * 60 * 60 * 1000);
|
|
366
|
+
|
|
367
|
+
return purchases.length / (days / 365); // Purchases per year
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private calculateCustomerLifespan(purchases: any[]): number {
|
|
371
|
+
if (purchases.length < 2) return 1;
|
|
372
|
+
|
|
373
|
+
const timespan = purchases[0].time - purchases[purchases.length - 1].time;
|
|
374
|
+
return Math.max(1, timespan / (365 * 24 * 60 * 60 * 1000)); // Years
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private determineTier(clv: number): string {
|
|
378
|
+
if (clv > 50000000) return "diamond";
|
|
379
|
+
if (clv > 20000000) return "gold";
|
|
380
|
+
if (clv > 5000000) return "silver";
|
|
381
|
+
if (clv > 1000000) return "bronze";
|
|
382
|
+
return "standard";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private generateRecommendations(clv: number, analytics: any): string[] {
|
|
386
|
+
const recommendations = [];
|
|
387
|
+
|
|
388
|
+
if (clv > 20000000) {
|
|
389
|
+
recommendations.push("Assign dedicated account manager");
|
|
390
|
+
recommendations.push("Offer exclusive products and early access");
|
|
391
|
+
recommendations.push("Provide VIP customer service");
|
|
392
|
+
} else if (clv > 5000000) {
|
|
393
|
+
recommendations.push("Implement loyalty program");
|
|
394
|
+
recommendations.push("Send personalized offers");
|
|
395
|
+
recommendations.push("Prioritize customer support");
|
|
396
|
+
} else if (clv < 1000000) {
|
|
397
|
+
recommendations.push("Focus on engagement and education");
|
|
398
|
+
recommendations.push("Offer entry-level products");
|
|
399
|
+
recommendations.push("Encourage repeat purchases");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return recommendations;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Bulk Operations
|
|
410
|
+
|
|
411
|
+
### 1. Bulk User Operations
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
// Thực hiện bulk operations trên nhiều users
|
|
415
|
+
const bulkResult = await zalo.userManagement.bulkUserOperation(
|
|
416
|
+
oaAccessToken,
|
|
417
|
+
{
|
|
418
|
+
operation: "update_tags",
|
|
419
|
+
user_ids: ["user1", "user2", "user3"],
|
|
420
|
+
data: {
|
|
421
|
+
add_tags: ["FLASH_SALE_2024"],
|
|
422
|
+
remove_tags: ["OLD_CAMPAIGN"]
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
console.log(`Updated ${bulkResult.successful_count} users`);
|
|
428
|
+
console.log(`Failed: ${bulkResult.failed_count}`);
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 2. Bulk Message Service
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
class BulkMessageService {
|
|
435
|
+
constructor(private zalo: ZaloSDK, private accessToken: string) {}
|
|
436
|
+
|
|
437
|
+
async sendBulkConsultationMessages(
|
|
438
|
+
userMessages: Array<{userId: string, message: any}>
|
|
439
|
+
): Promise<BulkMessageResult> {
|
|
440
|
+
const results = {
|
|
441
|
+
successful: 0,
|
|
442
|
+
failed: 0,
|
|
443
|
+
errors: [] as Array<{userId: string, error: string}>
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const batchSize = 10; // Process 10 users at a time
|
|
447
|
+
|
|
448
|
+
for (let i = 0; i < userMessages.length; i += batchSize) {
|
|
449
|
+
const batch = userMessages.slice(i, i + batchSize);
|
|
450
|
+
|
|
451
|
+
const promises = batch.map(async ({userId, message}) => {
|
|
452
|
+
try {
|
|
453
|
+
await this.zalo.consultation.sendTextMessage(
|
|
454
|
+
this.accessToken,
|
|
455
|
+
{ user_id: userId },
|
|
456
|
+
message
|
|
457
|
+
);
|
|
458
|
+
return { userId, success: true };
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return { userId, success: false, error: error.message };
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const batchResults = await Promise.all(promises);
|
|
465
|
+
|
|
466
|
+
batchResults.forEach(result => {
|
|
467
|
+
if (result.success) {
|
|
468
|
+
results.successful++;
|
|
469
|
+
} else {
|
|
470
|
+
results.failed++;
|
|
471
|
+
results.errors.push({
|
|
472
|
+
userId: result.userId,
|
|
473
|
+
error: result.error || 'Unknown error'
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Delay between batches to avoid rate limiting
|
|
479
|
+
if (i + batchSize < userMessages.length) {
|
|
480
|
+
await this.delay(1000);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return results;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Gửi tin nhắn theo segments
|
|
488
|
+
async sendSegmentedCampaign(campaign: Campaign): Promise<CampaignResult> {
|
|
489
|
+
const segments = await this.getUserSegments(campaign.targetSegments);
|
|
490
|
+
const results = new Map<string, BulkMessageResult>();
|
|
491
|
+
|
|
492
|
+
for (const [segmentName, users] of segments) {
|
|
493
|
+
console.log(`Sending campaign to ${segmentName}: ${users.length} users`);
|
|
494
|
+
|
|
495
|
+
const segmentMessage = this.personalizeMessageForSegment(
|
|
496
|
+
campaign.message,
|
|
497
|
+
segmentName
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
const userMessages = users.map(user => ({
|
|
501
|
+
userId: user.user_id,
|
|
502
|
+
message: this.personalizeMessage(segmentMessage, user)
|
|
503
|
+
}));
|
|
504
|
+
|
|
505
|
+
const segmentResult = await this.sendBulkConsultationMessages(userMessages);
|
|
506
|
+
results.set(segmentName, segmentResult);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return this.aggregateResults(results);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private async getUserSegments(targetSegments: string[]): Promise<Map<string, UserProfile[]>> {
|
|
513
|
+
const segments = new Map();
|
|
514
|
+
|
|
515
|
+
for (const segment of targetSegments) {
|
|
516
|
+
const users = await this.zalo.userManagement.searchUsers(
|
|
517
|
+
this.accessToken,
|
|
518
|
+
{ tags: [segment], limit: 1000 }
|
|
519
|
+
);
|
|
520
|
+
segments.set(segment, users.users);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return segments;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private personalizeMessageForSegment(template: string, segment: string): any {
|
|
527
|
+
// Customize message based on segment
|
|
528
|
+
const customizations = {
|
|
529
|
+
'VIP': {
|
|
530
|
+
greeting: 'Kính chào Quý khách VIP',
|
|
531
|
+
offer: 'ưu đãi đặc biệt dành riêng cho VIP'
|
|
532
|
+
},
|
|
533
|
+
'Premium': {
|
|
534
|
+
greeting: 'Xin chào khách hàng Premium',
|
|
535
|
+
offer: 'chương trình ưu đãi Premium'
|
|
536
|
+
},
|
|
537
|
+
'Standard': {
|
|
538
|
+
greeting: 'Xin chào',
|
|
539
|
+
offer: 'chương trình khuyến mại'
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const custom = customizations[segment] || customizations['Standard'];
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
type: "text",
|
|
547
|
+
text: template
|
|
548
|
+
.replace('{greeting}', custom.greeting)
|
|
549
|
+
.replace('{offer}', custom.offer)
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private personalizeMessage(template: any, user: UserProfile): any {
|
|
554
|
+
return {
|
|
555
|
+
...template,
|
|
556
|
+
text: template.text.replace('{name}', user.display_name || 'bạn')
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private delay(ms: number): Promise<void> {
|
|
561
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## User Journey Tracking
|
|
569
|
+
|
|
570
|
+
### 1. Journey Mapping Service
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
class UserJourneyService {
|
|
574
|
+
constructor(private zalo: ZaloSDK, private accessToken: string) {}
|
|
575
|
+
|
|
576
|
+
async mapUserJourney(userId: string): Promise<UserJourney> {
|
|
577
|
+
const interactions = await this.zalo.userManagement.getUserInteractions(
|
|
578
|
+
this.accessToken,
|
|
579
|
+
userId,
|
|
580
|
+
{ limit: 1000 }
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
const journey = this.analyzeJourneyStages(interactions);
|
|
584
|
+
const touchpoints = this.identifyTouchpoints(interactions);
|
|
585
|
+
const conversion = this.calculateConversionMetrics(interactions);
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
userId,
|
|
589
|
+
currentStage: journey.currentStage,
|
|
590
|
+
stages: journey.stages,
|
|
591
|
+
touchpoints,
|
|
592
|
+
conversion,
|
|
593
|
+
recommendations: this.generateJourneyRecommendations(journey, conversion)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private analyzeJourneyStages(interactions: any[]): JourneyStages {
|
|
598
|
+
const stages = {
|
|
599
|
+
awareness: [],
|
|
600
|
+
consideration: [],
|
|
601
|
+
purchase: [],
|
|
602
|
+
retention: [],
|
|
603
|
+
advocacy: []
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const currentStage = this.determineCurrentStage(interactions);
|
|
607
|
+
|
|
608
|
+
// Classify interactions by journey stage
|
|
609
|
+
interactions.forEach(interaction => {
|
|
610
|
+
const stage = this.classifyInteractionStage(interaction);
|
|
611
|
+
stages[stage].push(interaction);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
return { currentStage, stages };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private identifyTouchpoints(interactions: any[]): Touchpoint[] {
|
|
618
|
+
const touchpointMap = new Map();
|
|
619
|
+
|
|
620
|
+
interactions.forEach(interaction => {
|
|
621
|
+
const touchpoint = this.getTouchpointFromInteraction(interaction);
|
|
622
|
+
|
|
623
|
+
if (touchpointMap.has(touchpoint.type)) {
|
|
624
|
+
touchpointMap.get(touchpoint.type).count++;
|
|
625
|
+
touchpointMap.get(touchpoint.type).lastInteraction = interaction.time;
|
|
626
|
+
} else {
|
|
627
|
+
touchpointMap.set(touchpoint.type, {
|
|
628
|
+
...touchpoint,
|
|
629
|
+
count: 1,
|
|
630
|
+
lastInteraction: interaction.time
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
return Array.from(touchpointMap.values()).sort((a, b) => b.count - a.count);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private calculateConversionMetrics(interactions: any[]): ConversionMetrics {
|
|
639
|
+
const totalInteractions = interactions.length;
|
|
640
|
+
const purchases = interactions.filter(i => i.type === 'purchase');
|
|
641
|
+
const inquiries = interactions.filter(i => i.type === 'product_inquiry');
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
conversionRate: purchases.length / totalInteractions,
|
|
645
|
+
inquiryToPurchase: purchases.length / (inquiries.length || 1),
|
|
646
|
+
avgTimeToConversion: this.calculateAvgTimeToConversion(interactions),
|
|
647
|
+
totalPurchases: purchases.length,
|
|
648
|
+
totalValue: purchases.reduce((sum, p) => sum + (p.value || 0), 0)
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private generateJourneyRecommendations(
|
|
653
|
+
journey: JourneyStages,
|
|
654
|
+
conversion: ConversionMetrics
|
|
655
|
+
): string[] {
|
|
656
|
+
const recommendations = [];
|
|
657
|
+
|
|
658
|
+
switch (journey.currentStage) {
|
|
659
|
+
case 'awareness':
|
|
660
|
+
recommendations.push('Send educational content about products');
|
|
661
|
+
recommendations.push('Showcase customer testimonials and reviews');
|
|
662
|
+
break;
|
|
663
|
+
|
|
664
|
+
case 'consideration':
|
|
665
|
+
recommendations.push('Provide detailed product comparisons');
|
|
666
|
+
recommendations.push('Offer consultation sessions');
|
|
667
|
+
recommendations.push('Send limited-time offers to encourage decision');
|
|
668
|
+
break;
|
|
669
|
+
|
|
670
|
+
case 'purchase':
|
|
671
|
+
recommendations.push('Streamline checkout process');
|
|
672
|
+
recommendations.push('Offer multiple payment options');
|
|
673
|
+
recommendations.push('Provide immediate support');
|
|
674
|
+
break;
|
|
675
|
+
|
|
676
|
+
case 'retention':
|
|
677
|
+
recommendations.push('Send onboarding and education materials');
|
|
678
|
+
recommendations.push('Implement loyalty program');
|
|
679
|
+
recommendations.push('Request feedback and reviews');
|
|
680
|
+
break;
|
|
681
|
+
|
|
682
|
+
case 'advocacy':
|
|
683
|
+
recommendations.push('Encourage referrals with incentives');
|
|
684
|
+
recommendations.push('Feature as case study');
|
|
685
|
+
recommendations.push('Invite to exclusive events');
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (conversion.conversionRate < 0.1) {
|
|
690
|
+
recommendations.push('Focus on engagement improvement');
|
|
691
|
+
recommendations.push('Personalize content based on interests');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return recommendations;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## User Retention & Re-engagement
|
|
702
|
+
|
|
703
|
+
### 1. Retention Analysis
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
class UserRetentionService {
|
|
707
|
+
constructor(private zalo: ZaloSDK, private accessToken: string) {}
|
|
708
|
+
|
|
709
|
+
async analyzeUserRetention(period: 'weekly' | 'monthly'): Promise<RetentionAnalysis> {
|
|
710
|
+
const cohorts = await this.getCohorts(period);
|
|
711
|
+
const retentionRates = new Map();
|
|
712
|
+
|
|
713
|
+
for (const [cohortDate, users] of cohorts) {
|
|
714
|
+
const retention = await this.calculateCohortRetention(users, cohortDate, period);
|
|
715
|
+
retentionRates.set(cohortDate, retention);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
period,
|
|
720
|
+
cohorts: Array.from(retentionRates.entries()),
|
|
721
|
+
averageRetention: this.calculateAverageRetention(retentionRates),
|
|
722
|
+
insights: this.generateRetentionInsights(retentionRates)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Identify users at risk of churning
|
|
727
|
+
async identifyChurnRisk(): Promise<ChurnRiskAnalysis> {
|
|
728
|
+
const allUsers = await this.getAllActiveUsers();
|
|
729
|
+
const riskUsers = [];
|
|
730
|
+
|
|
731
|
+
for (const user of allUsers) {
|
|
732
|
+
const riskScore = await this.calculateChurnRiskScore(user.user_id);
|
|
733
|
+
|
|
734
|
+
if (riskScore > 0.7) {
|
|
735
|
+
riskUsers.push({
|
|
736
|
+
...user,
|
|
737
|
+
riskScore,
|
|
738
|
+
riskFactors: await this.identifyRiskFactors(user.user_id)
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
totalUsers: allUsers.length,
|
|
745
|
+
highRiskUsers: riskUsers.filter(u => u.riskScore > 0.9),
|
|
746
|
+
mediumRiskUsers: riskUsers.filter(u => u.riskScore > 0.7 && u.riskScore <= 0.9),
|
|
747
|
+
recommendations: this.generateChurnPreventionStrategies(riskUsers)
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private async calculateChurnRiskScore(userId: string): Promise<number> {
|
|
752
|
+
const analytics = await this.zalo.userManagement.getUserAnalytics(
|
|
753
|
+
this.accessToken,
|
|
754
|
+
userId
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
let score = 0;
|
|
758
|
+
|
|
759
|
+
// Days since last interaction
|
|
760
|
+
const daysSinceLastInteraction = analytics.days_since_last_interaction || 0;
|
|
761
|
+
if (daysSinceLastInteraction > 30) score += 0.3;
|
|
762
|
+
if (daysSinceLastInteraction > 60) score += 0.2;
|
|
763
|
+
if (daysSinceLastInteraction > 90) score += 0.3;
|
|
764
|
+
|
|
765
|
+
// Declining engagement
|
|
766
|
+
const engagementTrend = analytics.engagement_trend || 0;
|
|
767
|
+
if (engagementTrend < -0.2) score += 0.2;
|
|
768
|
+
|
|
769
|
+
// Reduced purchase frequency
|
|
770
|
+
const purchaseTrend = analytics.purchase_frequency_trend || 0;
|
|
771
|
+
if (purchaseTrend < -0.3) score += 0.3;
|
|
772
|
+
|
|
773
|
+
// Support issues
|
|
774
|
+
const supportIssues = analytics.recent_support_issues || 0;
|
|
775
|
+
if (supportIssues > 2) score += 0.2;
|
|
776
|
+
|
|
777
|
+
return Math.min(1, score);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Re-engagement campaign for inactive users
|
|
781
|
+
async runReengagementCampaign(
|
|
782
|
+
inactiveUsers: string[],
|
|
783
|
+
campaignType: 'win_back' | 'survey' | 'special_offer'
|
|
784
|
+
): Promise<ReengagementResult> {
|
|
785
|
+
const results = {
|
|
786
|
+
contacted: 0,
|
|
787
|
+
responded: 0,
|
|
788
|
+
reactivated: 0,
|
|
789
|
+
errors: []
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
for (const userId of inactiveUsers) {
|
|
793
|
+
try {
|
|
794
|
+
const message = this.createReengagementMessage(campaignType, userId);
|
|
795
|
+
|
|
796
|
+
await this.zalo.consultation.sendTextMessage(
|
|
797
|
+
this.accessToken,
|
|
798
|
+
{ user_id: userId },
|
|
799
|
+
message
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
results.contacted++;
|
|
803
|
+
|
|
804
|
+
// Track if user responds within campaign period
|
|
805
|
+
setTimeout(async () => {
|
|
806
|
+
const isReactivated = await this.checkUserReactivation(userId);
|
|
807
|
+
if (isReactivated) {
|
|
808
|
+
results.reactivated++;
|
|
809
|
+
}
|
|
810
|
+
}, 7 * 24 * 60 * 60 * 1000); // Check after 7 days
|
|
811
|
+
|
|
812
|
+
} catch (error) {
|
|
813
|
+
results.errors.push({ userId, error: error.message });
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return results;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private createReengagementMessage(
|
|
821
|
+
campaignType: string,
|
|
822
|
+
userId: string
|
|
823
|
+
): any {
|
|
824
|
+
const messages = {
|
|
825
|
+
win_back: {
|
|
826
|
+
type: "text",
|
|
827
|
+
text: "Chúng tôi nhớ bạn! 🥺\nBạn đã không tương tác với chúng tôi một thời gian rồi. Có gì chúng tôi có thể giúp bạn không?"
|
|
828
|
+
},
|
|
829
|
+
survey: {
|
|
830
|
+
type: "text",
|
|
831
|
+
text: "Xin chào! 👋\nChúng tôi muốn cải thiện dịch vụ. Bạn có thể chia sẻ lý do tại sao ít tương tác với chúng tôi gần đây không?"
|
|
832
|
+
},
|
|
833
|
+
special_offer: {
|
|
834
|
+
type: "text",
|
|
835
|
+
text: "🎁 Ưu đãi đặc biệt dành cho bạn!\nGiảm 50% cho lần mua hàng tiếp theo. Mã: COMEBACK50\nChỉ còn 3 ngày!"
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
return messages[campaignType] || messages.win_back;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
## Data Privacy & GDPR Compliance
|
|
847
|
+
|
|
848
|
+
### 1. User Consent Management
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
class UserConsentService {
|
|
852
|
+
constructor(private zalo: ZaloSDK, private accessToken: string) {}
|
|
853
|
+
|
|
854
|
+
async recordUserConsent(
|
|
855
|
+
userId: string,
|
|
856
|
+
consentType: ConsentType,
|
|
857
|
+
granted: boolean
|
|
858
|
+
): Promise<void> {
|
|
859
|
+
await this.zalo.userManagement.updateUserProfile(
|
|
860
|
+
this.accessToken,
|
|
861
|
+
userId,
|
|
862
|
+
{
|
|
863
|
+
custom_fields: {
|
|
864
|
+
[`consent_${consentType}`]: granted.toString(),
|
|
865
|
+
[`consent_${consentType}_date`]: new Date().toISOString(),
|
|
866
|
+
[`consent_${consentType}_ip`]: this.getCurrentIP()
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async getUserConsents(userId: string): Promise<UserConsents> {
|
|
873
|
+
const profile = await this.zalo.userManagement.getUserProfile(
|
|
874
|
+
this.accessToken,
|
|
875
|
+
userId
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
return {
|
|
879
|
+
marketing: profile.custom_fields?.consent_marketing === 'true',
|
|
880
|
+
analytics: profile.custom_fields?.consent_analytics === 'true',
|
|
881
|
+
data_processing: profile.custom_fields?.consent_data_processing === 'true',
|
|
882
|
+
third_party_sharing: profile.custom_fields?.consent_third_party === 'true'
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async exportUserData(userId: string): Promise<UserDataExport> {
|
|
887
|
+
const profile = await this.zalo.userManagement.getUserProfile(
|
|
888
|
+
this.accessToken,
|
|
889
|
+
userId
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
const interactions = await this.zalo.userManagement.getUserInteractions(
|
|
893
|
+
this.accessToken,
|
|
894
|
+
userId,
|
|
895
|
+
{ limit: 10000 }
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
const analytics = await this.zalo.userManagement.getUserAnalytics(
|
|
899
|
+
this.accessToken,
|
|
900
|
+
userId
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
personal_data: {
|
|
905
|
+
user_id: profile.user_id,
|
|
906
|
+
display_name: profile.display_name,
|
|
907
|
+
avatar: profile.avatar,
|
|
908
|
+
phone: profile.shared_info?.phone,
|
|
909
|
+
address: profile.shared_info?.address
|
|
910
|
+
},
|
|
911
|
+
interaction_history: interactions,
|
|
912
|
+
analytics_data: analytics,
|
|
913
|
+
consent_records: await this.getUserConsents(userId),
|
|
914
|
+
export_date: new Date().toISOString(),
|
|
915
|
+
retention_period: "36_months"
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async deleteUserData(userId: string): Promise<DeletionResult> {
|
|
920
|
+
// Implement GDPR-compliant data deletion
|
|
921
|
+
const deletionTasks = [
|
|
922
|
+
this.deleteUserProfile(userId),
|
|
923
|
+
this.deleteUserInteractions(userId),
|
|
924
|
+
this.deleteUserAnalytics(userId),
|
|
925
|
+
this.removeFromMarketingLists(userId)
|
|
926
|
+
];
|
|
927
|
+
|
|
928
|
+
const results = await Promise.allSettled(deletionTasks);
|
|
929
|
+
|
|
930
|
+
return {
|
|
931
|
+
userId,
|
|
932
|
+
deleted: results.every(r => r.status === 'fulfilled'),
|
|
933
|
+
deletion_date: new Date().toISOString(),
|
|
934
|
+
retention_logs: this.createDeletionLog(userId, results)
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
---
|
|
941
|
+
|
|
942
|
+
## Testing User Management
|
|
943
|
+
|
|
944
|
+
### 1. Unit Tests
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
// user-management.test.ts
|
|
948
|
+
import { ZaloSDK } from '@warriorteam/redai-zalo-sdk';
|
|
949
|
+
|
|
950
|
+
describe('User Management', () => {
|
|
951
|
+
const zalo = new ZaloSDK({
|
|
952
|
+
appId: 'test_app_id',
|
|
953
|
+
appSecret: 'test_app_secret'
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it('should get user profile', async () => {
|
|
957
|
+
const mockProfile = {
|
|
958
|
+
user_id: 'test_user',
|
|
959
|
+
display_name: 'Test User',
|
|
960
|
+
avatar: 'https://example.com/avatar.jpg'
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
jest.spyOn(zalo.userManagement, 'getUserProfile').mockResolvedValue(mockProfile);
|
|
964
|
+
|
|
965
|
+
const profile = await zalo.userManagement.getUserProfile('test_token', 'test_user');
|
|
966
|
+
|
|
967
|
+
expect(profile.user_id).toBe('test_user');
|
|
968
|
+
expect(profile.display_name).toBe('Test User');
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it('should search users with filters', async () => {
|
|
972
|
+
const mockResult = {
|
|
973
|
+
total: 5,
|
|
974
|
+
users: [
|
|
975
|
+
{ user_id: '1', display_name: 'User 1' },
|
|
976
|
+
{ user_id: '2', display_name: 'User 2' }
|
|
977
|
+
]
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
jest.spyOn(zalo.userManagement, 'searchUsers').mockResolvedValue(mockResult);
|
|
981
|
+
|
|
982
|
+
const result = await zalo.userManagement.searchUsers('test_token', {
|
|
983
|
+
query: 'test',
|
|
984
|
+
tags: ['VIP']
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
expect(result.total).toBe(5);
|
|
988
|
+
expect(result.users).toHaveLength(2);
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
---
|
|
994
|
+
|
|
995
|
+
## Performance Optimization
|
|
996
|
+
|
|
997
|
+
### 1. Caching User Data
|
|
998
|
+
|
|
999
|
+
```typescript
|
|
1000
|
+
class UserDataCache {
|
|
1001
|
+
private cache = new Map<string, CachedUserData>();
|
|
1002
|
+
private readonly ttl = 10 * 60 * 1000; // 10 minutes
|
|
1003
|
+
|
|
1004
|
+
async getUserProfile(
|
|
1005
|
+
zalo: ZaloSDK,
|
|
1006
|
+
accessToken: string,
|
|
1007
|
+
userId: string
|
|
1008
|
+
): Promise<UserProfile> {
|
|
1009
|
+
const cacheKey = `profile_${userId}`;
|
|
1010
|
+
const cached = this.get(cacheKey);
|
|
1011
|
+
|
|
1012
|
+
if (cached) {
|
|
1013
|
+
return cached.data;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const profile = await zalo.userManagement.getUserProfile(accessToken, userId);
|
|
1017
|
+
this.set(cacheKey, profile);
|
|
1018
|
+
|
|
1019
|
+
return profile;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
private get(key: string): CachedUserData | null {
|
|
1023
|
+
const item = this.cache.get(key);
|
|
1024
|
+
|
|
1025
|
+
if (!item) return null;
|
|
1026
|
+
|
|
1027
|
+
if (Date.now() - item.timestamp > this.ttl) {
|
|
1028
|
+
this.cache.delete(key);
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return item;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private set(key: string, data: any): void {
|
|
1036
|
+
this.cache.set(key, {
|
|
1037
|
+
data,
|
|
1038
|
+
timestamp: Date.now()
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Batch load multiple users
|
|
1043
|
+
async batchLoadUsers(
|
|
1044
|
+
zalo: ZaloSDK,
|
|
1045
|
+
accessToken: string,
|
|
1046
|
+
userIds: string[]
|
|
1047
|
+
): Promise<Map<string, UserProfile>> {
|
|
1048
|
+
const results = new Map();
|
|
1049
|
+
const uncachedIds = [];
|
|
1050
|
+
|
|
1051
|
+
// Check cache first
|
|
1052
|
+
for (const userId of userIds) {
|
|
1053
|
+
const cached = this.get(`profile_${userId}`);
|
|
1054
|
+
if (cached) {
|
|
1055
|
+
results.set(userId, cached.data);
|
|
1056
|
+
} else {
|
|
1057
|
+
uncachedIds.push(userId);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Batch load uncached users
|
|
1062
|
+
if (uncachedIds.length > 0) {
|
|
1063
|
+
const batchSize = 5; // API rate limiting
|
|
1064
|
+
|
|
1065
|
+
for (let i = 0; i < uncachedIds.length; i += batchSize) {
|
|
1066
|
+
const batch = uncachedIds.slice(i, i + batchSize);
|
|
1067
|
+
|
|
1068
|
+
const promises = batch.map(async (userId) => {
|
|
1069
|
+
try {
|
|
1070
|
+
const profile = await zalo.userManagement.getUserProfile(accessToken, userId);
|
|
1071
|
+
this.set(`profile_${userId}`, profile);
|
|
1072
|
+
return { userId, profile };
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
console.error(`Failed to load user ${userId}:`, error);
|
|
1075
|
+
return { userId, profile: null };
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
const batchResults = await Promise.all(promises);
|
|
1080
|
+
|
|
1081
|
+
batchResults.forEach(({ userId, profile }) => {
|
|
1082
|
+
if (profile) {
|
|
1083
|
+
results.set(userId, profile);
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// Delay between batches
|
|
1088
|
+
if (i + batchSize < uncachedIds.length) {
|
|
1089
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return results;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
---
|
|
1100
|
+
|
|
1101
|
+
## Best Practices
|
|
1102
|
+
|
|
1103
|
+
### 1. Data Management Best Practices
|
|
1104
|
+
|
|
1105
|
+
```typescript
|
|
1106
|
+
// ✅ Good practices
|
|
1107
|
+
class UserDataBestPractices {
|
|
1108
|
+
// Always validate user IDs
|
|
1109
|
+
private validateUserId(userId: string): boolean {
|
|
1110
|
+
return /^[0-9]+$/.test(userId) && userId.length > 0;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Implement proper error handling
|
|
1114
|
+
async safeGetUserProfile(
|
|
1115
|
+
zalo: ZaloSDK,
|
|
1116
|
+
accessToken: string,
|
|
1117
|
+
userId: string
|
|
1118
|
+
): Promise<UserProfile | null> {
|
|
1119
|
+
if (!this.validateUserId(userId)) {
|
|
1120
|
+
throw new Error('Invalid user ID format');
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
try {
|
|
1124
|
+
return await zalo.userManagement.getUserProfile(accessToken, userId);
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (error.code === -233) {
|
|
1127
|
+
console.log(`User ${userId} not found`);
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
throw error;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Always paginate large datasets
|
|
1135
|
+
async getAllUsersWithPagination(
|
|
1136
|
+
zalo: ZaloSDK,
|
|
1137
|
+
accessToken: string
|
|
1138
|
+
): Promise<UserProfile[]> {
|
|
1139
|
+
const allUsers = [];
|
|
1140
|
+
let offset = 0;
|
|
1141
|
+
const limit = 50; // Reasonable page size
|
|
1142
|
+
|
|
1143
|
+
while (true) {
|
|
1144
|
+
const page = await zalo.user.getUserList(accessToken, {
|
|
1145
|
+
offset,
|
|
1146
|
+
count: limit
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
allUsers.push(...page.data);
|
|
1150
|
+
|
|
1151
|
+
if (page.data.length < limit) break;
|
|
1152
|
+
offset += limit;
|
|
1153
|
+
|
|
1154
|
+
// Rate limiting
|
|
1155
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return allUsers;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Implement data validation
|
|
1162
|
+
private validateUserData(userData: any): boolean {
|
|
1163
|
+
const required = ['user_id', 'display_name'];
|
|
1164
|
+
return required.every(field => userData[field]);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
### 2. Privacy-First Approach
|
|
1170
|
+
|
|
1171
|
+
```typescript
|
|
1172
|
+
class PrivacyCompliantUserService {
|
|
1173
|
+
// Always check consent before processing
|
|
1174
|
+
async sendMarketingMessage(
|
|
1175
|
+
userId: string,
|
|
1176
|
+
message: any
|
|
1177
|
+
): Promise<boolean> {
|
|
1178
|
+
const consents = await this.getUserConsents(userId);
|
|
1179
|
+
|
|
1180
|
+
if (!consents.marketing) {
|
|
1181
|
+
console.log(`User ${userId} has not consented to marketing messages`);
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Proceed with sending message
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Minimize data collection
|
|
1190
|
+
async updateUserProfile(
|
|
1191
|
+
userId: string,
|
|
1192
|
+
updates: Partial<UserProfile>
|
|
1193
|
+
): Promise<void> {
|
|
1194
|
+
// Only update necessary fields
|
|
1195
|
+
const allowedFields = ['notes', 'tags', 'preferences'];
|
|
1196
|
+
const filteredUpdates = Object.keys(updates)
|
|
1197
|
+
.filter(key => allowedFields.includes(key))
|
|
1198
|
+
.reduce((obj, key) => {
|
|
1199
|
+
obj[key] = updates[key];
|
|
1200
|
+
return obj;
|
|
1201
|
+
}, {});
|
|
1202
|
+
|
|
1203
|
+
if (Object.keys(filteredUpdates).length === 0) {
|
|
1204
|
+
throw new Error('No valid fields to update');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Proceed with update
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
---
|
|
1213
|
+
|
|
1214
|
+
## Troubleshooting
|
|
1215
|
+
|
|
1216
|
+
### Common Issues
|
|
1217
|
+
|
|
1218
|
+
**Q: "User not found" error khi lấy profile**
|
|
1219
|
+
```
|
|
1220
|
+
A: User có thể đã unfollow OA hoặc chặn OA.
|
|
1221
|
+
Kiểm tra danh sách followers trước khi truy cập profile.
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
**Q: Không lấy được thông tin phone number**
|
|
1225
|
+
```
|
|
1226
|
+
A: Phone number chỉ có sẵn nếu user đã share với OA.
|
|
1227
|
+
Cần yêu cầu user cung cấp thông tin qua request_user_info.
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
**Q: Search results không accurate**
|
|
1231
|
+
```
|
|
1232
|
+
A: Zalo search có giới hạn. Implement local caching và filtering
|
|
1233
|
+
để có kết quả search tốt hơn.
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
---
|
|
1237
|
+
|
|
1238
|
+
## Next Steps
|
|
1239
|
+
|
|
1240
|
+
Sau khi nắm vững User Management:
|
|
1241
|
+
|
|
1242
|
+
1. **[Tag Management](./TAG_MANAGEMENT.md)** - User tagging và segmentation
|
|
1243
|
+
2. **[Group Management](./GROUP_MANAGEMENT.md)** - Quản lý Zalo groups
|
|
1244
|
+
3. **[Webhook Events](./WEBHOOK_EVENTS.md)** - Xử lý user events
|
|
1245
|
+
4. **[Error Handling](./ERROR_HANDLING.md)** - Xử lý lỗi toàn diện
|
|
1246
|
+
5. **[Video Upload](./VIDEO_UPLOAD.md)** - Upload và manage media
|
|
1247
|
+
|
|
1248
|
+
Tham khảo **[API Reference](./API_REFERENCE.md)** để biết chi tiết về tất cả user management methods.
|