@warriorteam/redai-zalo-sdk 1.3.0 → 1.4.1
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/README.md +565 -550
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/services/article.service.d.ts +1 -1
- package/dist/services/article.service.d.ts.map +1 -1
- package/dist/services/article.service.js +24 -16
- package/dist/services/article.service.js.map +1 -1
- package/dist/services/auth.service.d.ts +1 -0
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +23 -9
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/consultation.service.d.ts +63 -16
- package/dist/services/consultation.service.d.ts.map +1 -1
- package/dist/services/consultation.service.js +264 -49
- package/dist/services/consultation.service.js.map +1 -1
- package/dist/services/general-message.service.d.ts +2 -25
- package/dist/services/general-message.service.d.ts.map +1 -1
- package/dist/services/general-message.service.js +11 -112
- package/dist/services/general-message.service.js.map +1 -1
- package/dist/services/group-management.service.d.ts +1 -1
- package/dist/services/group-management.service.d.ts.map +1 -1
- package/dist/services/group-management.service.js +59 -27
- package/dist/services/group-management.service.js.map +1 -1
- package/dist/services/group-message.service.d.ts +1 -1
- package/dist/services/group-message.service.d.ts.map +1 -1
- package/dist/services/group-message.service.js +49 -23
- package/dist/services/group-message.service.js.map +1 -1
- package/dist/services/message-management.service.d.ts +21 -2
- package/dist/services/message-management.service.d.ts.map +1 -1
- package/dist/services/message-management.service.js +83 -7
- package/dist/services/message-management.service.js.map +1 -1
- package/dist/services/oa.service.d.ts +1 -0
- package/dist/services/oa.service.d.ts.map +1 -1
- package/dist/services/oa.service.js +23 -5
- package/dist/services/oa.service.js.map +1 -1
- package/dist/services/user.service.d.ts +108 -18
- package/dist/services/user.service.d.ts.map +1 -1
- package/dist/services/user.service.js +260 -59
- package/dist/services/user.service.js.map +1 -1
- package/dist/services/video-upload.service.d.ts +1 -1
- package/dist/services/video-upload.service.d.ts.map +1 -1
- package/dist/services/video-upload.service.js +11 -8
- package/dist/services/video-upload.service.js.map +1 -1
- package/dist/services/zns.service.d.ts +88 -21
- package/dist/services/zns.service.d.ts.map +1 -1
- package/dist/services/zns.service.js +157 -42
- package/dist/services/zns.service.js.map +1 -1
- package/dist/types/group.d.ts +5 -5
- package/dist/types/group.d.ts.map +1 -1
- package/dist/types/user.d.ts +155 -12
- package/dist/types/user.d.ts.map +1 -1
- package/dist/types/user.js.map +1 -1
- package/dist/types/webhook.d.ts +8 -0
- package/dist/types/webhook.d.ts.map +1 -1
- package/dist/types/webhook.js.map +1 -1
- package/dist/types/zns.d.ts +67 -33
- package/dist/types/zns.d.ts.map +1 -1
- package/dist/zalo-sdk.d.ts +3 -11
- package/dist/zalo-sdk.d.ts.map +1 -1
- package/dist/zalo-sdk.js +0 -10
- package/dist/zalo-sdk.js.map +1 -1
- package/docs/CONSULTATION_SERVICE.md +512 -330
- package/docs/GROUP_MANAGEMENT.md +2 -2
- package/docs/USER_MANAGEMENT.md +481 -1248
- package/docs/WEBHOOK_EVENTS.md +55 -3
- package/package.json +1 -1
- package/dist/services/tag.service.d.ts +0 -144
- package/dist/services/tag.service.d.ts.map +0 -1
- package/dist/services/tag.service.js +0 -184
- package/dist/services/tag.service.js.map +0 -1
- package/dist/services/user-management.service.d.ts +0 -117
- package/dist/services/user-management.service.d.ts.map +0 -1
- package/dist/services/user-management.service.js +0 -239
- package/dist/services/user-management.service.js.map +0 -1
package/docs/USER_MANAGEMENT.md
CHANGED
|
@@ -1,1248 +1,481 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
## Tổng
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- 👥 **
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
###
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
// Lấy
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"user-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
//
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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.
|
|
1
|
+
# User Management - Hướng Dẫn Sử Dụng
|
|
2
|
+
|
|
3
|
+
## Tổng Quan
|
|
4
|
+
|
|
5
|
+
`UserService` cung cấp các công cụ toàn diện để quản lý người dùng Zalo Official Account, bao gồm:
|
|
6
|
+
|
|
7
|
+
- 👥 **User Information** - Lấy thông tin chi tiết người dùng
|
|
8
|
+
- 📋 **User Lists** - Quản lý danh sách người dùng với phân trang
|
|
9
|
+
- 🏷️ **Tag Management** - Gắn và quản lý tags cho users
|
|
10
|
+
- 📊 **Custom Info** - Quản lý thông tin tùy chỉnh
|
|
11
|
+
- 🔄 **Bulk Operations** - Xử lý hàng loạt users
|
|
12
|
+
|
|
13
|
+
**Endpoints sử dụng:**
|
|
14
|
+
- User Info: `https://openapi.zalo.me/v3.0/oa/user/detail`
|
|
15
|
+
- User List: `https://openapi.zalo.me/v3.0/oa/user/getlist`
|
|
16
|
+
- Update User: `https://openapi.zalo.me/v3.0/oa/user/update`
|
|
17
|
+
- Custom Info: `https://openapi.zalo.me/v3.0/oa/user/getcustominfo`
|
|
18
|
+
- Tags: `https://openapi.zalo.me/v3.0/oa/tag/gettagsofoa`
|
|
19
|
+
|
|
20
|
+
## Khởi Tạo Service
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { UserService } from "@warriorteam/redai-zalo-sdk";
|
|
24
|
+
import { ZaloClient } from "@warriorteam/redai-zalo-sdk";
|
|
25
|
+
|
|
26
|
+
const client = new ZaloClient();
|
|
27
|
+
const userService = new UserService(client);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Các Phương Thức Chính
|
|
31
|
+
|
|
32
|
+
### 1. Lấy Thông Tin User Chi Tiết
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// Lấy thông tin chi tiết của một user
|
|
36
|
+
const userInfo = await userService.getUserInfo(
|
|
37
|
+
accessToken,
|
|
38
|
+
"user-id-here"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
console.log("User Info:", {
|
|
42
|
+
userId: userInfo.user_id,
|
|
43
|
+
displayName: userInfo.display_name,
|
|
44
|
+
avatar: userInfo.avatar,
|
|
45
|
+
userGender: userInfo.user_gender,
|
|
46
|
+
userAlias: userInfo.user_alias,
|
|
47
|
+
isFollower: userInfo.is_follower,
|
|
48
|
+
sharedInfo: userInfo.shared_info
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Tham số:**
|
|
53
|
+
- `accessToken`: Access token của OA
|
|
54
|
+
- `userId`: ID người dùng cần lấy thông tin
|
|
55
|
+
|
|
56
|
+
### 2. Lấy Danh Sách Users
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// Lấy danh sách users với filter
|
|
60
|
+
const userList = await userService.getUserList(accessToken, {
|
|
61
|
+
offset: 0,
|
|
62
|
+
count: 50,
|
|
63
|
+
tag_name: "VIP_CUSTOMER", // Lọc theo tag (tùy chọn)
|
|
64
|
+
is_follower: true, // Chỉ lấy followers (tùy chọn)
|
|
65
|
+
last_interaction_period: "L7D" // Tương tác trong 7 ngày qua
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log("Total users:", userList.total);
|
|
69
|
+
console.log("Count:", userList.count);
|
|
70
|
+
console.log("Offset:", userList.offset);
|
|
71
|
+
|
|
72
|
+
userList.users.forEach(user => {
|
|
73
|
+
console.log(`${user.display_name} - ${user.user_id}`);
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Tham số:**
|
|
78
|
+
- `offset`: Vị trí bắt đầu (0-based)
|
|
79
|
+
- `count`: Số lượng users mỗi page (tối đa 50)
|
|
80
|
+
- `tag_name`: Lọc theo tag (tùy chọn)
|
|
81
|
+
- `is_follower`: true/false để lọc followers
|
|
82
|
+
- `last_interaction_period`: "TODAY", "YESTERDAY", "L7D", "L30D"
|
|
83
|
+
|
|
84
|
+
### 3. Lấy Tất Cả Users (Auto Pagination)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Lấy tất cả users với phân trang tự động
|
|
88
|
+
const allUsers = await userService.getAllUsers(accessToken, {
|
|
89
|
+
is_follower: true,
|
|
90
|
+
tag_name: "PREMIUM"
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log(`Total users fetched: ${allUsers.length}`);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Lưu ý:**
|
|
97
|
+
- Zalo giới hạn max offset = 9951 (tương ứng ~10000 users)
|
|
98
|
+
- Method này tự động handle pagination và warning khi vượt giới hạn
|
|
99
|
+
|
|
100
|
+
### 4. Lấy Danh Sách User IDs (Tối Ưu)
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Chỉ lấy user IDs, không lấy thông tin chi tiết (nhanh hơn)
|
|
104
|
+
const userIds = await userService.getAllUserIds(accessToken, {
|
|
105
|
+
is_follower: true
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log(`Total user IDs: ${userIds.length}`);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 5. Helper Methods
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Lấy chỉ followers
|
|
115
|
+
const followers = await userService.getFollowers(accessToken, 0, 50);
|
|
116
|
+
|
|
117
|
+
// Lấy users theo tag
|
|
118
|
+
const vipUsers = await userService.getUsersByTag(accessToken, "VIP", 0, 50);
|
|
119
|
+
|
|
120
|
+
// Lấy users theo thời gian tương tác
|
|
121
|
+
const recentUsers = await userService.getUsersByInteraction(
|
|
122
|
+
accessToken,
|
|
123
|
+
"L7D", // 7 ngày qua
|
|
124
|
+
0,
|
|
125
|
+
50
|
|
126
|
+
);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## User Management Operations
|
|
130
|
+
|
|
131
|
+
### 6. Cập Nhật Thông Tin User
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Cập nhật thông tin user
|
|
135
|
+
const success = await userService.updateUser(accessToken, {
|
|
136
|
+
user_id: "user-id-here",
|
|
137
|
+
notes: "Customer quan tâm sản phẩm cao cấp",
|
|
138
|
+
// Các field khác có thể cập nhật
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
console.log("Update success:", success);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 7. Xóa Thông Tin User
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Xóa thông tin user khỏi OA (không ảnh hưởng tài khoản Zalo)
|
|
148
|
+
const success = await userService.deleteUserInfo(accessToken, "user-id-here");
|
|
149
|
+
console.log("Delete success:", success);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 8. Quản Lý Custom Info
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Lấy custom info của user
|
|
156
|
+
const customInfo = await userService.getUserCustomInfo(accessToken, {
|
|
157
|
+
user_id: "user-id-here",
|
|
158
|
+
fields_to_export: ["customer_tier", "total_spent"] // Tùy chọn
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log("Custom Info:", customInfo.custom_info);
|
|
162
|
+
|
|
163
|
+
// Cập nhật custom info
|
|
164
|
+
const updateSuccess = await userService.updateUserCustomInfo(accessToken, {
|
|
165
|
+
user_id: "user-id-here",
|
|
166
|
+
custom_info: {
|
|
167
|
+
customer_tier: "VIP",
|
|
168
|
+
total_spent: "10000000",
|
|
169
|
+
last_purchase: "2024-12-01"
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Lưu ý:** Cấu trúc `custom_info` phụ thuộc vào thiết lập OA
|
|
175
|
+
|
|
176
|
+
## Tag Management
|
|
177
|
+
|
|
178
|
+
### 9. Quản Lý Tags
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// Lấy danh sách tất cả tags của OA
|
|
182
|
+
const tags = await userService.getLabels(accessToken);
|
|
183
|
+
console.log("Available tags:", tags);
|
|
184
|
+
|
|
185
|
+
// Thêm tag cho user
|
|
186
|
+
const addSuccess = await userService.addTagToUser(accessToken, {
|
|
187
|
+
user_id: "user-id-here",
|
|
188
|
+
tag_name: "VIP_CUSTOMER"
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Xóa tag khỏi user
|
|
192
|
+
const removeSuccess = await userService.removeTagFromUser(accessToken, {
|
|
193
|
+
user_id: "user-id-here",
|
|
194
|
+
tag_name: "OLD_TAG"
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Xóa tag khỏi OA (xóa hoàn toàn)
|
|
198
|
+
const deleteSuccess = await userService.deleteLabel(accessToken, "UNUSED_TAG");
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 10. Bulk Operations
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Bulk add tag cho nhiều users
|
|
205
|
+
const bulkAddResult = await userService.bulkAddTag(
|
|
206
|
+
accessToken,
|
|
207
|
+
["user1", "user2", "user3"],
|
|
208
|
+
"FLASH_SALE_2024"
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
console.log("Success:", bulkAddResult.success);
|
|
212
|
+
console.log("Failed:", bulkAddResult.failed);
|
|
213
|
+
|
|
214
|
+
// Bulk remove tag
|
|
215
|
+
const bulkRemoveResult = await userService.bulkRemoveTag(
|
|
216
|
+
accessToken,
|
|
217
|
+
["user1", "user2", "user3"],
|
|
218
|
+
"OLD_CAMPAIGN"
|
|
219
|
+
);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## API Methods Summary
|
|
223
|
+
|
|
224
|
+
| Method | Description | Parameters |
|
|
225
|
+
|--------|-------------|------------|
|
|
226
|
+
| `getUserInfo` | Lấy thông tin chi tiết user | `accessToken`, `userId` |
|
|
227
|
+
| `getUserList` | Lấy danh sách users với pagination | `accessToken`, `request` |
|
|
228
|
+
| `getAllUsers` | Lấy tất cả users (auto pagination) | `accessToken`, `filters?` |
|
|
229
|
+
| `getAllUserIds` | Lấy tất cả user IDs (tối ưu) | `accessToken`, `filters?` |
|
|
230
|
+
| `updateUser` | Cập nhật thông tin user | `accessToken`, `request` |
|
|
231
|
+
| `deleteUserInfo` | Xóa thông tin user | `accessToken`, `userId` |
|
|
232
|
+
| `getUserCustomInfo` | Lấy custom info | `accessToken`, `request` |
|
|
233
|
+
| `updateUserCustomInfo` | Cập nhật custom info | `accessToken`, `request` |
|
|
234
|
+
| `getLabels` | Lấy danh sách tags | `accessToken` |
|
|
235
|
+
| `addTagToUser` | Thêm tag cho user | `accessToken`, `request` |
|
|
236
|
+
| `removeTagFromUser` | Xóa tag khỏi user | `accessToken`, `request` |
|
|
237
|
+
| `deleteLabel` | Xóa tag khỏi OA | `accessToken`, `tagName` |
|
|
238
|
+
| `getUsersByTag` | Lấy users theo tag | `accessToken`, `tagName`, `offset?`, `count?` |
|
|
239
|
+
| `getFollowers` | Lấy chỉ followers | `accessToken`, `offset?`, `count?` |
|
|
240
|
+
| `getUsersByInteraction` | Lấy users theo thời gian tương tác | `accessToken`, `period`, `offset?`, `count?` |
|
|
241
|
+
| `bulkAddTag` | Bulk add tag | `accessToken`, `userIds`, `tagName` |
|
|
242
|
+
| `bulkRemoveTag` | Bulk remove tag | `accessToken`, `userIds`, `tagName` |
|
|
243
|
+
|
|
244
|
+
## Ví Dụ Thực Tế
|
|
245
|
+
|
|
246
|
+
### 1. Customer Support System
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
class CustomerSupportSystem {
|
|
250
|
+
constructor(private userService: UserService, private accessToken: string) {}
|
|
251
|
+
|
|
252
|
+
async handleNewCustomer(userId: string) {
|
|
253
|
+
// Lấy thông tin customer
|
|
254
|
+
const userInfo = await this.userService.getUserInfo(this.accessToken, userId);
|
|
255
|
+
|
|
256
|
+
// Thêm tag "NEW_CUSTOMER"
|
|
257
|
+
await this.userService.addTagToUser(this.accessToken, {
|
|
258
|
+
user_id: userId,
|
|
259
|
+
tag_name: "NEW_CUSTOMER"
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Cập nhật custom info
|
|
263
|
+
await this.userService.updateUserCustomInfo(this.accessToken, {
|
|
264
|
+
user_id: userId,
|
|
265
|
+
custom_info: {
|
|
266
|
+
registration_date: new Date().toISOString(),
|
|
267
|
+
customer_tier: "STANDARD",
|
|
268
|
+
total_interactions: "1"
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
console.log(`New customer ${userInfo.display_name} processed`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async promoteToVIP(userId: string) {
|
|
276
|
+
// Remove old tags
|
|
277
|
+
await this.userService.removeTagFromUser(this.accessToken, {
|
|
278
|
+
user_id: userId,
|
|
279
|
+
tag_name: "STANDARD"
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Add VIP tag
|
|
283
|
+
await this.userService.addTagToUser(this.accessToken, {
|
|
284
|
+
user_id: userId,
|
|
285
|
+
tag_name: "VIP"
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Update custom info
|
|
289
|
+
await this.userService.updateUserCustomInfo(this.accessToken, {
|
|
290
|
+
user_id: userId,
|
|
291
|
+
custom_info: {
|
|
292
|
+
customer_tier: "VIP",
|
|
293
|
+
promotion_date: new Date().toISOString()
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### 2. Bulk Campaign System
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
class BulkCampaignSystem {
|
|
306
|
+
constructor(private userService: UserService, private accessToken: string) {}
|
|
307
|
+
|
|
308
|
+
async runSegmentedCampaign(campaignConfig: {
|
|
309
|
+
segments: string[];
|
|
310
|
+
message: string;
|
|
311
|
+
batchSize?: number;
|
|
312
|
+
}) {
|
|
313
|
+
const results = {
|
|
314
|
+
totalSent: 0,
|
|
315
|
+
totalFailed: 0,
|
|
316
|
+
segmentResults: new Map<string, any>()
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
for (const segment of campaignConfig.segments) {
|
|
320
|
+
console.log(`Processing segment: ${segment}`);
|
|
321
|
+
|
|
322
|
+
// Get users by tag
|
|
323
|
+
const users = await this.userService.getUsersByTag(
|
|
324
|
+
this.accessToken,
|
|
325
|
+
segment,
|
|
326
|
+
0,
|
|
327
|
+
1000
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Bulk add campaign tag
|
|
331
|
+
const userIds = users.users.map(u => u.user_id);
|
|
332
|
+
const bulkResult = await this.userService.bulkAddTag(
|
|
333
|
+
this.accessToken,
|
|
334
|
+
userIds,
|
|
335
|
+
`CAMPAIGN_${Date.now()}`
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
results.segmentResults.set(segment, {
|
|
339
|
+
totalUsers: users.total,
|
|
340
|
+
tagged: bulkResult.success.length,
|
|
341
|
+
failed: bulkResult.failed.length
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
results.totalSent += bulkResult.success.length;
|
|
345
|
+
results.totalFailed += bulkResult.failed.length;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Error Handling
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { ZaloSDKError } from "@warriorteam/redai-zalo-sdk";
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const userInfo = await userService.getUserInfo(accessToken, "user-id");
|
|
360
|
+
console.log("User found:", userInfo.display_name);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (error instanceof ZaloSDKError) {
|
|
363
|
+
switch (error.code) {
|
|
364
|
+
case -216:
|
|
365
|
+
console.error("Access token không hợp lệ");
|
|
366
|
+
break;
|
|
367
|
+
case -233:
|
|
368
|
+
console.error("User không tồn tại hoặc đã unfollow");
|
|
369
|
+
break;
|
|
370
|
+
case -201:
|
|
371
|
+
console.error("Tham số không hợp lệ");
|
|
372
|
+
break;
|
|
373
|
+
default:
|
|
374
|
+
console.error("Lỗi khác:", error.message);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
console.error("Unexpected error:", error);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Giới Hạn API
|
|
383
|
+
|
|
384
|
+
### Zalo API Limits
|
|
385
|
+
|
|
386
|
+
1. **User List Pagination**:
|
|
387
|
+
- Max offset: 9951
|
|
388
|
+
- Max count per request: 50
|
|
389
|
+
- Tối đa ~10000 users có thể lấy được
|
|
390
|
+
|
|
391
|
+
2. **Rate Limiting**:
|
|
392
|
+
- Nên có delay giữa các requests
|
|
393
|
+
- Sử dụng batch operations khi có thể
|
|
394
|
+
|
|
395
|
+
3. **Custom Info**:
|
|
396
|
+
- Cấu trúc phụ thuộc vào thiết lập OA
|
|
397
|
+
- Tất cả giá trị đều là string
|
|
398
|
+
|
|
399
|
+
## Best Practices
|
|
400
|
+
|
|
401
|
+
### 1. Performance Optimization
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// ✅ Sử dụng getAllUserIds cho performance tốt hơn
|
|
405
|
+
const userIds = await userService.getAllUserIds(accessToken);
|
|
406
|
+
|
|
407
|
+
// ✅ Batch operations
|
|
408
|
+
const bulkResult = await userService.bulkAddTag(
|
|
409
|
+
accessToken,
|
|
410
|
+
userIds.slice(0, 100),
|
|
411
|
+
"NEW_TAG"
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// ✅ Pagination với reasonable page size
|
|
415
|
+
const users = await userService.getUserList(accessToken, {
|
|
416
|
+
offset: 0,
|
|
417
|
+
count: 50 // Không quá 50
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### 2. Error Handling
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// ✅ Validate user ID format
|
|
425
|
+
function validateUserId(userId: string): boolean {
|
|
426
|
+
return /^[0-9]+$/.test(userId) && userId.length > 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ✅ Safe operations
|
|
430
|
+
async function safeGetUser(userId: string) {
|
|
431
|
+
if (!validateUserId(userId)) {
|
|
432
|
+
throw new Error("Invalid user ID format");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
return await userService.getUserInfo(accessToken, userId);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (error.code === -233) {
|
|
439
|
+
return null; // User not found
|
|
440
|
+
}
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Troubleshooting
|
|
447
|
+
|
|
448
|
+
### Common Issues
|
|
449
|
+
|
|
450
|
+
**Q: "User not found" error**
|
|
451
|
+
```
|
|
452
|
+
A: User có thể đã unfollow OA hoặc chặn OA.
|
|
453
|
+
Kiểm tra is_follower field trước.
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Q: Không lấy được phone number**
|
|
457
|
+
```
|
|
458
|
+
A: Phone chỉ có khi user đã share với OA.
|
|
459
|
+
Sử dụng request_user_info để yêu cầu.
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Q: getAllUsers không lấy hết**
|
|
463
|
+
```
|
|
464
|
+
A: Zalo giới hạn max offset = 9951.
|
|
465
|
+
Chỉ lấy được ~10000 users đầu tiên.
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Q: Custom info không cập nhật**
|
|
469
|
+
```
|
|
470
|
+
A: Kiểm tra cấu trúc custom_info trong OA settings.
|
|
471
|
+
Tất cả values phải là string.
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Tài Liệu Liên Quan
|
|
475
|
+
|
|
476
|
+
- [Zalo Official Account API](https://developers.zalo.me/docs/api/official-account-api)
|
|
477
|
+
- [User Management API](https://developers.zalo.me/docs/api/official-account-api/quan-ly-nguoi-dung)
|
|
478
|
+
- [Tag Management API](https://developers.zalo.me/docs/api/official-account-api/quan-ly-nhan)
|
|
479
|
+
- [Consultation Service](./CONSULTATION_SERVICE.md)
|
|
480
|
+
- [Tag Management](./TAG_MANAGEMENT.md)
|
|
481
|
+
|