@jolibox/implement 1.2.4 → 1.2.5
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/.rush/temp/package-deps_build.json +24 -18
- package/dist/common/cache/__tests__/request-cache-service.test.d.ts +1 -0
- package/dist/common/cache/request-cache-service.d.ts +111 -0
- package/dist/common/report/base-tracker.d.ts +2 -1
- package/dist/common/rewards/cached-fetch-reward.d.ts +46 -0
- package/dist/common/rewards/cached-reward-service.d.ts +24 -0
- package/dist/common/rewards/fetch-reward.d.ts +2 -3
- package/dist/common/rewards/index.d.ts +2 -0
- package/dist/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.d.ts +34 -0
- package/dist/common/rewards/registers/utils/coins/jolicoin/fetch-balance.d.ts +2 -1
- package/dist/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.d.ts +34 -0
- package/dist/common/rewards/registers/utils/coins/joligem/fetch-gem-balance.d.ts +2 -1
- package/dist/index.js +9 -9
- package/dist/index.native.js +33 -33
- package/dist/native/payment/payment-service.d.ts +36 -30
- package/implement.build.log +2 -2
- package/package.json +5 -5
- package/src/common/cache/__tests__/request-cache-service.test.ts +686 -0
- package/src/common/cache/request-cache-service.ts +393 -0
- package/src/common/report/base-tracker.ts +2 -2
- package/src/common/rewards/cached-fetch-reward.ts +241 -0
- package/src/common/rewards/cached-reward-service.ts +255 -0
- package/src/common/rewards/fetch-reward.ts +17 -93
- package/src/common/rewards/index.ts +3 -0
- package/src/common/rewards/registers/utils/coins/commands/use-payment.ts +8 -0
- package/src/common/rewards/registers/utils/coins/jolicoin/cached-fetch-balance.ts +177 -0
- package/src/common/rewards/registers/utils/coins/jolicoin/fetch-balance.ts +13 -1
- package/src/common/rewards/registers/utils/coins/jolicoin/jolicoin-handler.ts +2 -0
- package/src/common/rewards/registers/utils/coins/joligem/cached-fetch-gem-balance.ts +181 -0
- package/src/common/rewards/registers/utils/coins/joligem/fetch-gem-balance.ts +13 -1
- package/src/common/rewards/registers/utils/coins/joligem/gem-handler.ts +2 -0
- package/src/h5/api/ads.ts +5 -0
- package/src/h5/api/storage.ts +2 -2
- package/src/h5/http/index.ts +2 -2
- package/src/h5/report/event-tracker.ts +2 -2
- package/src/native/api/ads.ts +7 -1
- package/src/native/api/payment.ts +4 -4
- package/src/native/payment/__tests__/payment-service-simple.test.ts +97 -31
- package/src/native/payment/payment-service.ts +224 -210
- package/src/native/rewards/ui/payment-modal.ts +14 -7
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用请求缓存服务基类
|
|
3
|
+
* 支持部分数据缓存,部分数据实时获取的混合模式
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CacheConfig<TCacheData, TServerData> {
|
|
7
|
+
/** 缓存持续时间,默认10分钟 */
|
|
8
|
+
duration?: number;
|
|
9
|
+
/** 请求超时时间,默认3秒 */
|
|
10
|
+
timeout?: number;
|
|
11
|
+
/** 缓存键生成函数 */
|
|
12
|
+
keyGenerator?: <TRequest>(endpoint: string, params?: TRequest) => string;
|
|
13
|
+
/** 数据一致性校验函数 */
|
|
14
|
+
consistencyChecker?: (cached: TCacheData, serverData: TServerData) => boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CacheEntry<T> {
|
|
18
|
+
data: T;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
expiresAt: number;
|
|
21
|
+
key: string;
|
|
22
|
+
sequence: number; // 请求序列号
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CacheStats {
|
|
27
|
+
cacheCount: number;
|
|
28
|
+
validCount: number;
|
|
29
|
+
expiredCount: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RequestAdapter<TRequest, TResponse, TCacheData, TRealTimeData> {
|
|
33
|
+
/** 执行实际的网络请求 */
|
|
34
|
+
execute(endpoint: string, options?: TRequest): Promise<TResponse>;
|
|
35
|
+
/** 从响应中提取需要缓存的部分(不变数据) */
|
|
36
|
+
extractCacheableData?(response: TResponse): TCacheData;
|
|
37
|
+
/** 从响应中提取实时数据部分(变化数据) */
|
|
38
|
+
extractRealTimeData?(response: TResponse): TRealTimeData;
|
|
39
|
+
/** 合并缓存数据和实时数据 */
|
|
40
|
+
mergeData?(cached: TCacheData, realTime: TRealTimeData): TResponse;
|
|
41
|
+
/** 处理纯缓存场景的数据,当没有新的网络请求时 */
|
|
42
|
+
processCachedData?(cached: TCacheData): TResponse;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export abstract class RequestCacheService<TRequest, TResponse, TCacheData, TRealTimeData> {
|
|
46
|
+
private cache = new Map<string, CacheEntry<TCacheData>>();
|
|
47
|
+
private readonly config: {
|
|
48
|
+
duration: number;
|
|
49
|
+
timeout: number;
|
|
50
|
+
keyGenerator: <TReq>(endpoint: string, params?: TReq) => string;
|
|
51
|
+
consistencyChecker: (cached: TCacheData, serverData: TCacheData) => boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// 请求去重:每个key只允许同时有一个正在进行的请求
|
|
55
|
+
private pendingRequests = new Map<string, Promise<TResponse>>();
|
|
56
|
+
|
|
57
|
+
// 请求序列号:防止晚到的请求覆盖早发出的请求结果
|
|
58
|
+
private requestSequence = 0;
|
|
59
|
+
private cacheSequences = new Map<string, number>();
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
protected requestAdapter: RequestAdapter<TRequest, TResponse, TCacheData, TRealTimeData>,
|
|
63
|
+
config: Partial<CacheConfig<TCacheData, TCacheData>> = {}
|
|
64
|
+
) {
|
|
65
|
+
this.config = {
|
|
66
|
+
duration: 10 * 60 * 1000, // 10分钟
|
|
67
|
+
timeout: 3000, // 3秒
|
|
68
|
+
keyGenerator: <TReq>(endpoint: string, _params?: TReq) => endpoint,
|
|
69
|
+
consistencyChecker: () => true,
|
|
70
|
+
...config
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 带缓存的请求方法 - 缓存优先 + 实时数据混合模式 + 请求去重
|
|
76
|
+
*/
|
|
77
|
+
async request(endpoint: string, options?: TRequest): Promise<TResponse> {
|
|
78
|
+
const cacheKey = this.config.keyGenerator(endpoint, options);
|
|
79
|
+
|
|
80
|
+
// 检查是否有正在进行的请求,如果有则等待其完成
|
|
81
|
+
const pendingRequest = this.pendingRequests.get(cacheKey);
|
|
82
|
+
if (pendingRequest) {
|
|
83
|
+
console.log(`[RequestCacheService] Reusing pending request for ${endpoint}`);
|
|
84
|
+
return pendingRequest;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// internal request promise for background update
|
|
88
|
+
const internalRequestPromise = this.requestInternal(endpoint, options);
|
|
89
|
+
|
|
90
|
+
// createTimeoutPromise
|
|
91
|
+
const timeoutPromise = this.createTimeoutPromise();
|
|
92
|
+
|
|
93
|
+
// usePromiseRace
|
|
94
|
+
const racedPromise = Promise.race([internalRequestPromise, timeoutPromise]).catch(async (error) => {
|
|
95
|
+
// if timeout error, try to get data from cache as fallback
|
|
96
|
+
if (error.message.includes('timeout')) {
|
|
97
|
+
const cachedData = this.getValidCachedData(cacheKey);
|
|
98
|
+
if (cachedData) {
|
|
99
|
+
console.warn(`[RequestCacheService] Request timeout, falling back to cache for ${endpoint}`);
|
|
100
|
+
return this.requestAdapter.processCachedData
|
|
101
|
+
? this.requestAdapter.processCachedData(cachedData)
|
|
102
|
+
: (cachedData as TResponse);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.pendingRequests.set(cacheKey, racedPromise);
|
|
109
|
+
|
|
110
|
+
// ensure internal request can be completed in background for cache update
|
|
111
|
+
internalRequestPromise
|
|
112
|
+
.then((result) => {
|
|
113
|
+
// 成功时不需要特殊处理,requestInternal 已经更新了缓存
|
|
114
|
+
console.log(`[RequestCacheService] Background request completed successfully for ${endpoint}`);
|
|
115
|
+
})
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
console.warn(`[RequestCacheService] Background request failed for ${endpoint}:`, error);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await racedPromise;
|
|
122
|
+
return result;
|
|
123
|
+
} finally {
|
|
124
|
+
// 请求完成后清理 pending request
|
|
125
|
+
this.pendingRequests.delete(cacheKey);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async requestInternal(endpoint: string, options?: TRequest): Promise<TResponse> {
|
|
130
|
+
const cacheKey = this.config.keyGenerator(endpoint, options);
|
|
131
|
+
const currentSequence = ++this.requestSequence; // 生成请求序列号
|
|
132
|
+
|
|
133
|
+
const cachedData = this.getValidCachedData(cacheKey);
|
|
134
|
+
|
|
135
|
+
console.log('[RequestCacheService] requestInternal start', endpoint, 'cached:', cachedData);
|
|
136
|
+
if (cachedData) {
|
|
137
|
+
if (this.requestAdapter.extractRealTimeData && this.requestAdapter.mergeData) {
|
|
138
|
+
try {
|
|
139
|
+
const freshResponse = await this.requestAdapter.execute(endpoint, options);
|
|
140
|
+
const realTimeData = this.requestAdapter.extractRealTimeData(freshResponse);
|
|
141
|
+
|
|
142
|
+
const serverCacheableData = this.requestAdapter.extractCacheableData?.(freshResponse);
|
|
143
|
+
if (serverCacheableData && !this.config.consistencyChecker(cachedData, serverCacheableData)) {
|
|
144
|
+
console.log(`[RequestCacheService] Cache invalidated due to data change for ${endpoint}`);
|
|
145
|
+
this.setCachedDataWithSequence(cacheKey, serverCacheableData, currentSequence);
|
|
146
|
+
return freshResponse;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return this.requestAdapter.mergeData(cachedData, realTimeData);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.warn(
|
|
152
|
+
`[RequestCacheService] Failed to fetch real-time data, using cache only for ${endpoint}:`,
|
|
153
|
+
error
|
|
154
|
+
);
|
|
155
|
+
return this.requestAdapter.processCachedData
|
|
156
|
+
? this.requestAdapter.processCachedData(cachedData)
|
|
157
|
+
: (cachedData as TResponse);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`[RequestCacheService] use cache for ${endpoint}`);
|
|
162
|
+
return this.requestAdapter.processCachedData
|
|
163
|
+
? this.requestAdapter.processCachedData(cachedData)
|
|
164
|
+
: (cachedData as TResponse);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(
|
|
168
|
+
`[RequestCacheService] Cache miss, fetching fresh data for ${endpoint} (sequence: ${currentSequence})`
|
|
169
|
+
);
|
|
170
|
+
const freshResponse = await this.requestAdapter.execute(endpoint, options);
|
|
171
|
+
|
|
172
|
+
const serverCacheableData = this.requestAdapter.extractCacheableData?.(freshResponse);
|
|
173
|
+
if (serverCacheableData) {
|
|
174
|
+
this.setCachedDataWithSequence(cacheKey, serverCacheableData, currentSequence);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return freshResponse;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* timeout
|
|
182
|
+
*/
|
|
183
|
+
private createTimeoutPromise(): Promise<never> {
|
|
184
|
+
return new Promise((_, reject) => {
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
reject(
|
|
187
|
+
new Error(`[RequestCacheService] Request timeout after ${this.config.timeout / 1000} seconds`)
|
|
188
|
+
);
|
|
189
|
+
}, this.config.timeout);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* get valid cached data
|
|
195
|
+
*/
|
|
196
|
+
private getValidCachedData(key: string): TCacheData | null {
|
|
197
|
+
const cached = this.cache.get(key);
|
|
198
|
+
|
|
199
|
+
if (!cached) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 检查过期
|
|
204
|
+
if (Date.now() > cached.expiresAt) {
|
|
205
|
+
this.cache.delete(key);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return cached.data;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 设置缓存数据 (带序列号检查)
|
|
214
|
+
*/
|
|
215
|
+
private setCachedDataWithSequence(
|
|
216
|
+
key: string,
|
|
217
|
+
data: TCacheData,
|
|
218
|
+
sequence: number,
|
|
219
|
+
metadata?: Record<string, unknown>
|
|
220
|
+
): void {
|
|
221
|
+
const existing = this.cache.get(key);
|
|
222
|
+
|
|
223
|
+
// 如果已有缓存且序列号更新(说明当前请求是过时的),则不更新缓存
|
|
224
|
+
if (existing && existing.sequence > sequence) {
|
|
225
|
+
console.log(
|
|
226
|
+
`[RequestCacheService] Ignoring stale cache update for ${key} (current: ${existing.sequence}, incoming: ${sequence})`
|
|
227
|
+
);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
this.cache.set(key, {
|
|
233
|
+
data: JSON.parse(JSON.stringify(data)), // 深拷贝
|
|
234
|
+
timestamp: now,
|
|
235
|
+
expiresAt: now + this.config.duration,
|
|
236
|
+
key,
|
|
237
|
+
sequence,
|
|
238
|
+
metadata
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
this.cacheSequences.set(key, sequence);
|
|
242
|
+
console.log(`[RequestCacheService] Updated cache for ${key} with sequence ${sequence}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* refresh cache
|
|
247
|
+
*/
|
|
248
|
+
async refreshCache(endpoint: string, options?: TRequest): Promise<TResponse> {
|
|
249
|
+
const cacheKey = this.config.keyGenerator(endpoint, options);
|
|
250
|
+
this.cache.delete(cacheKey);
|
|
251
|
+
|
|
252
|
+
return this.request(endpoint, options);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* force request and update cache
|
|
257
|
+
*/
|
|
258
|
+
async forceRequest(endpoint: string, options?: TRequest): Promise<TResponse> {
|
|
259
|
+
const cacheKey = this.config.keyGenerator(endpoint, options);
|
|
260
|
+
const currentSequence = ++this.requestSequence;
|
|
261
|
+
|
|
262
|
+
console.log(`[RequestCacheService] Force request for ${endpoint} (sequence: ${currentSequence})`);
|
|
263
|
+
|
|
264
|
+
const freshResponse = await this.requestAdapter.execute(endpoint, options);
|
|
265
|
+
|
|
266
|
+
const serverCacheableData = this.requestAdapter.extractCacheableData?.(freshResponse);
|
|
267
|
+
|
|
268
|
+
if (serverCacheableData) {
|
|
269
|
+
this.setCachedDataWithSequence(cacheKey, serverCacheableData, currentSequence);
|
|
270
|
+
console.log(`[RequestCacheService] Force updated cache for ${endpoint}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return freshResponse;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* warmup cache - 避免与正在进行的请求冲突
|
|
278
|
+
*/
|
|
279
|
+
async warmupCache(endpoint: string, options?: TRequest): Promise<void> {
|
|
280
|
+
const cacheKey = this.config.keyGenerator(endpoint, options);
|
|
281
|
+
|
|
282
|
+
// 如果有正在进行的请求,跳过 warmup
|
|
283
|
+
if (this.pendingRequests.has(cacheKey)) {
|
|
284
|
+
console.log(`[RequestCacheService] Skipping warmup for ${endpoint} - request in progress`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 如果已有有效缓存,跳过 warmup
|
|
289
|
+
if (this.hasValidCache(cacheKey)) {
|
|
290
|
+
console.log(`[RequestCacheService] Skipping warmup for ${endpoint} - valid cache exists`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
console.log(`[RequestCacheService] Warming up cache for ${endpoint}`);
|
|
296
|
+
await this.forceRequest(endpoint, options);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.warn(`[RequestCacheService] Cache warmup failed for ${endpoint}:`, error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* clear cache with target key
|
|
304
|
+
*/
|
|
305
|
+
clearCache(key?: string): void {
|
|
306
|
+
if (key) {
|
|
307
|
+
this.cache.delete(key);
|
|
308
|
+
this.cacheSequences.delete(key);
|
|
309
|
+
this.pendingRequests.delete(key);
|
|
310
|
+
console.log(`[RequestCacheService] Cleared cache for ${key}`);
|
|
311
|
+
} else {
|
|
312
|
+
this.cache.clear();
|
|
313
|
+
this.cacheSequences.clear();
|
|
314
|
+
this.pendingRequests.clear();
|
|
315
|
+
this.requestSequence = 0; // 重置序列号
|
|
316
|
+
console.log('[RequestCacheService] Cleared all cache');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* clear expired cache
|
|
322
|
+
*/
|
|
323
|
+
clearExpiredCache(): void {
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
for (const [key, entry] of this.cache) {
|
|
326
|
+
if (now > entry.expiresAt) {
|
|
327
|
+
this.cache.delete(key);
|
|
328
|
+
this.cacheSequences.delete(key);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
console.log('[RequestCacheService] Cleared expired cache entries');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* get cache stats
|
|
336
|
+
*/
|
|
337
|
+
getCacheStats(): CacheStats & { pendingRequestCount: number } {
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
let validCount = 0;
|
|
340
|
+
let expiredCount = 0;
|
|
341
|
+
|
|
342
|
+
for (const entry of this.cache.values()) {
|
|
343
|
+
if (now > entry.expiresAt) {
|
|
344
|
+
expiredCount++;
|
|
345
|
+
} else {
|
|
346
|
+
validCount++;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
cacheCount: this.cache.size,
|
|
352
|
+
validCount,
|
|
353
|
+
expiredCount,
|
|
354
|
+
pendingRequestCount: this.pendingRequests.size
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* get all cache keys
|
|
360
|
+
*/
|
|
361
|
+
getCacheKeys(): string[] {
|
|
362
|
+
return Array.from(this.cache.keys());
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* check if the target key has valid cache
|
|
367
|
+
*/
|
|
368
|
+
hasValidCache(key: string): boolean {
|
|
369
|
+
const cached = this.cache.get(key);
|
|
370
|
+
return cached ? Date.now() <= cached.expiresAt : false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* get pending request keys for debugging
|
|
375
|
+
*/
|
|
376
|
+
getPendingRequestKeys(): string[] {
|
|
377
|
+
return Array.from(this.pendingRequests.keys());
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* check if a request is currently pending
|
|
382
|
+
*/
|
|
383
|
+
hasPendingRequest(key: string): boolean {
|
|
384
|
+
return this.pendingRequests.has(key);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* get cache sequence for a specific key
|
|
389
|
+
*/
|
|
390
|
+
getCacheSequence(key: string): number | undefined {
|
|
391
|
+
return this.cacheSequences.get(key);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -22,12 +22,12 @@ export abstract class EventTracker {
|
|
|
22
22
|
|
|
23
23
|
private samplesConfig: ISamplesConfig | null = null;
|
|
24
24
|
|
|
25
|
-
constructor() {
|
|
25
|
+
constructor(readonly apiHost: string) {
|
|
26
26
|
this.fetchSamplesConfig();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
private async fetchSamplesConfig() {
|
|
30
|
-
const host =
|
|
30
|
+
const host = this.apiHost;
|
|
31
31
|
const path = `${host}/api/fe-configs/js-sdk/samples-config`;
|
|
32
32
|
const samplesConfig = (await (await fetch(path)).json()) as ISamplesConfig;
|
|
33
33
|
this.samplesConfig = samplesConfig;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { IHttpClient } from '../http';
|
|
2
|
+
import { RewardsHelper, RewardType } from './reward-helper';
|
|
3
|
+
import { CachedRewardService } from './cached-reward-service';
|
|
4
|
+
import { IUnlockOption } from './type';
|
|
5
|
+
import {
|
|
6
|
+
UnlockOptionsEventName,
|
|
7
|
+
UseModalFrequencyEventName,
|
|
8
|
+
rewardsEmitter,
|
|
9
|
+
DefaltJoliCoinUseAndCharge,
|
|
10
|
+
DefaltLoginGuide
|
|
11
|
+
} from './reward-emitter';
|
|
12
|
+
import { hostEmitter } from '@jolibox/common';
|
|
13
|
+
import { StandardResponse, GlobalConfig } from '@jolibox/types';
|
|
14
|
+
|
|
15
|
+
const priority = () => {
|
|
16
|
+
return (a: RewardType, b: RewardType) => {
|
|
17
|
+
// Priority order: GEM > JOLI_COIN > ADS
|
|
18
|
+
const priorityMap: Record<RewardType, number> = {
|
|
19
|
+
JOLI_GEM: 3,
|
|
20
|
+
JOLI_GEM_ONLY: 3,
|
|
21
|
+
JOLI_COIN: 2,
|
|
22
|
+
JOLI_COIN_ONLY: 2,
|
|
23
|
+
ADS: 1
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return priorityMap[b] - priorityMap[a];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const sortRewards = (rewardsTypes: RewardType[]): RewardType[] => {
|
|
31
|
+
if (!rewardsTypes.length) return ['ADS'];
|
|
32
|
+
if (rewardsTypes.includes('JOLI_GEM') && rewardsTypes.length <= 1) return ['JOLI_GEM_ONLY'];
|
|
33
|
+
if (rewardsTypes.includes('JOLI_COIN') && rewardsTypes.length <= 1) return ['JOLI_COIN_ONLY'];
|
|
34
|
+
|
|
35
|
+
return rewardsTypes.sort(priority());
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// 创建缓存奖励服务的单例
|
|
39
|
+
let cachedRewardServiceInstance: CachedRewardService | null = null;
|
|
40
|
+
|
|
41
|
+
function getCachedRewardService(httpClient: IHttpClient): CachedRewardService {
|
|
42
|
+
if (!cachedRewardServiceInstance) {
|
|
43
|
+
cachedRewardServiceInstance = new CachedRewardService(httpClient);
|
|
44
|
+
}
|
|
45
|
+
return cachedRewardServiceInstance;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 使用缓存的奖励获取器
|
|
50
|
+
* 优化版本:unlockOptions 使用缓存,余额信息实时获取
|
|
51
|
+
*/
|
|
52
|
+
export const createCachedRewardFetcher = (rewardsHelper: RewardsHelper) => {
|
|
53
|
+
rewardsHelper.registerRewardsFetcher(async (httpClient: IHttpClient) => {
|
|
54
|
+
const defaultRewards: RewardType[] = ['ADS'];
|
|
55
|
+
const cachedRewardService = getCachedRewardService(httpClient);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const res = await cachedRewardService.getUnlockOptions({});
|
|
59
|
+
|
|
60
|
+
if (res.code !== 'SUCCESS') {
|
|
61
|
+
return defaultRewards;
|
|
62
|
+
}
|
|
63
|
+
// 发送事件通知
|
|
64
|
+
rewardsEmitter.emit(UnlockOptionsEventName, {
|
|
65
|
+
options: res.data?.unlockOptions || [],
|
|
66
|
+
userJoliCoin: res.extra?.joliCoin || {
|
|
67
|
+
balance: 0,
|
|
68
|
+
enableAutoDeduct: false
|
|
69
|
+
},
|
|
70
|
+
userGem: res.extra?.joliGem || {
|
|
71
|
+
balance: 0,
|
|
72
|
+
enableAutoDeduct: false
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const rewardsTypes =
|
|
77
|
+
res.data?.unlockOptions?.map((option: IUnlockOption) => option.type) || Array.from(defaultRewards);
|
|
78
|
+
|
|
79
|
+
return sortRewards(rewardsTypes);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error('getRewardOptions error (cached):', e);
|
|
82
|
+
return defaultRewards;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 使用缓存的频率配置获取器
|
|
89
|
+
* 优化版本:全局配置使用纯缓存
|
|
90
|
+
*/
|
|
91
|
+
export const createCachedRewardFrequencyConfigFetcher = (rewardsHelper: RewardsHelper) => {
|
|
92
|
+
rewardsHelper.registerRewardFrequencyConfigFetcher(async (httpClient: IHttpClient) => {
|
|
93
|
+
const cachedRewardService = getCachedRewardService(httpClient);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const res = await cachedRewardService.getGlobalConfig();
|
|
97
|
+
|
|
98
|
+
// 发送事件通知
|
|
99
|
+
rewardsEmitter.emit(UseModalFrequencyEventName, {
|
|
100
|
+
joliCoinUseAndCharge: res.data?.joliCoinUseAndCharge || DefaltJoliCoinUseAndCharge,
|
|
101
|
+
loginGuide: res.data?.loginGuide || DefaltLoginGuide
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
res.data &&
|
|
105
|
+
hostEmitter.emit('onGlobalConfigChanged', {
|
|
106
|
+
globalConfig: res.data
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
joliCoinUseAndCharge: res.data?.joliCoinUseAndCharge || DefaltJoliCoinUseAndCharge,
|
|
111
|
+
loginGuide: res.data?.loginGuide || DefaltLoginGuide
|
|
112
|
+
};
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error('getGlobalConfig error (cached):', e);
|
|
115
|
+
// 发送兜底事件通知
|
|
116
|
+
rewardsEmitter.emit(UseModalFrequencyEventName, {
|
|
117
|
+
joliCoinUseAndCharge: DefaltJoliCoinUseAndCharge,
|
|
118
|
+
loginGuide: DefaltLoginGuide
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
hostEmitter.emit('onGlobalConfigChanged', {
|
|
122
|
+
globalConfig: {
|
|
123
|
+
joliCoinUseAndCharge: DefaltJoliCoinUseAndCharge,
|
|
124
|
+
loginGuide: DefaltLoginGuide
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
joliCoinUseAndCharge: DefaltJoliCoinUseAndCharge,
|
|
129
|
+
loginGuide: DefaltLoginGuide
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 获取缓存服务实例(用于手动管理缓存)
|
|
137
|
+
*/
|
|
138
|
+
export const getRewardCacheService = (httpClient: IHttpClient): CachedRewardService => {
|
|
139
|
+
return getCachedRewardService(httpClient);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 强制刷新奖励数据缓存
|
|
144
|
+
*/
|
|
145
|
+
export const refreshRewardCache = async (httpClient: IHttpClient) => {
|
|
146
|
+
const cachedRewardService = getCachedRewardService(httpClient);
|
|
147
|
+
await cachedRewardService.refreshUnlockOptions({});
|
|
148
|
+
console.log('Reward cache refreshed');
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 强制刷新全局配置缓存
|
|
153
|
+
*/
|
|
154
|
+
export const refreshGlobalConfigCache = async (httpClient: IHttpClient) => {
|
|
155
|
+
const cachedRewardService = getCachedRewardService(httpClient);
|
|
156
|
+
await cachedRewardService.refreshGlobalConfig();
|
|
157
|
+
console.log('Global config cache refreshed');
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 预热所有缓存
|
|
162
|
+
*/
|
|
163
|
+
export const warmupRewardCache = async (httpClient: IHttpClient) => {
|
|
164
|
+
const cachedRewardService = getCachedRewardService(httpClient);
|
|
165
|
+
await cachedRewardService.warmupCache();
|
|
166
|
+
console.log('Reward cache warmed up');
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 清除所有奖励相关缓存
|
|
171
|
+
*/
|
|
172
|
+
export const clearRewardCache = (httpClient: IHttpClient) => {
|
|
173
|
+
const cachedRewardService = getCachedRewardService(httpClient);
|
|
174
|
+
cachedRewardService.clearAllCache();
|
|
175
|
+
console.log('All reward cache cleared');
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 获取缓存统计信息
|
|
180
|
+
*/
|
|
181
|
+
export const getRewardCacheStats = (httpClient: IHttpClient) => {
|
|
182
|
+
const cachedRewardService = getCachedRewardService(httpClient);
|
|
183
|
+
return cachedRewardService.getCacheStats();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// 保持原有的创建器以便向后兼容
|
|
187
|
+
export const createRewardFetcher = (rewardsHelper: RewardsHelper) => {
|
|
188
|
+
rewardsHelper.registerRewardsFetcher(async (httpClient: IHttpClient) => {
|
|
189
|
+
const defaultRewards: RewardType[] = ['ADS'];
|
|
190
|
+
try {
|
|
191
|
+
const res = await httpClient.post('/api/games/unlock-options', {});
|
|
192
|
+
if (res.code !== 'SUCCESS') {
|
|
193
|
+
return defaultRewards;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log('-----res fetch reward-----', res);
|
|
197
|
+
rewardsEmitter.emit(UnlockOptionsEventName, {
|
|
198
|
+
options: res.data?.unlockOptions || [],
|
|
199
|
+
userJoliCoin: res.extra?.joliCoin || {
|
|
200
|
+
balance: 0,
|
|
201
|
+
enableAutoDeduct: false
|
|
202
|
+
},
|
|
203
|
+
userGem: res.extra?.joliGem || {
|
|
204
|
+
balance: 0,
|
|
205
|
+
enableAutoDeduct: false
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const rewardsTypes =
|
|
210
|
+
res.data?.unlockOptions?.map((option: IUnlockOption) => option.type) || Array.from(defaultRewards);
|
|
211
|
+
return sortRewards(rewardsTypes);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.error('getRewardOptions error:', e);
|
|
214
|
+
return defaultRewards;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const createRewardFrequencyConfigFetcher = (rewardsHelper: RewardsHelper) => {
|
|
220
|
+
rewardsHelper.registerRewardFrequencyConfigFetcher(async (httpClient: IHttpClient) => {
|
|
221
|
+
const res: StandardResponse<GlobalConfig> = await httpClient.get(
|
|
222
|
+
'/api/fe-configs/web-common/global-config',
|
|
223
|
+
{}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
rewardsEmitter.emit(UseModalFrequencyEventName, {
|
|
227
|
+
joliCoinUseAndCharge: res.data?.joliCoinUseAndCharge || DefaltJoliCoinUseAndCharge,
|
|
228
|
+
loginGuide: res.data?.loginGuide || DefaltLoginGuide
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
res.data &&
|
|
232
|
+
hostEmitter.emit('onGlobalConfigChanged', {
|
|
233
|
+
globalConfig: res.data
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
joliCoinUseAndCharge: res.data?.joliCoinUseAndCharge || DefaltJoliCoinUseAndCharge,
|
|
238
|
+
loginGuide: res.data?.loginGuide || DefaltLoginGuide
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
};
|