@windrun-huaiin/backend-core 14.1.0 → 14.2.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.
@@ -1,5 +1,5 @@
1
1
  import { __awaiter, __rest } from '../../../../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.mjs';
2
- import { userAggregateService } from '../../../../../services/aggregate/user.aggregate.service.mjs';
2
+ import { anonymousAggregateService } from '../../../../../services/aggregate/anonymous.aggregate.service.mjs';
3
3
  import { extractFingerprintFromNextRequest } from '@windrun-huaiin/third-ui/fingerprint/server';
4
4
  import { auth } from '@clerk/nextjs/server';
5
5
  import { NextResponse } from 'next/server';
@@ -23,6 +23,9 @@ function createErrorResponse(message, status = 400) {
23
23
  }
24
24
  const SOURCE_REF_MAX_LENGTH = 2048;
25
25
  const QUERY_PARAM_MAX_LENGTH = 512;
26
+ const USER_AGENT_MAX_LENGTH = 1024;
27
+ const FIRST_TOUCH_HEADER_MAX_LENGTH = 4096;
28
+ const FIRST_TOUCH_HEADER_NAME = 'x-first-touch';
26
29
  function normalizeSourceRef(ref) {
27
30
  if (!ref) {
28
31
  return null;
@@ -47,6 +50,28 @@ function normalizeQueryParam(value) {
47
50
  ? trimmed.slice(0, QUERY_PARAM_MAX_LENGTH)
48
51
  : trimmed;
49
52
  }
53
+ function decodeHeaderValue(value) {
54
+ try {
55
+ return decodeURIComponent(value);
56
+ }
57
+ catch (_a) {
58
+ return null;
59
+ }
60
+ }
61
+ function mergeSourceRef(target, source) {
62
+ if (!source) {
63
+ return;
64
+ }
65
+ const entries = Object.entries(source);
66
+ for (const [key, value] of entries) {
67
+ if (value === undefined || value === null) {
68
+ continue;
69
+ }
70
+ if (target[key] === undefined) {
71
+ target[key] = value;
72
+ }
73
+ }
74
+ }
50
75
  function applySearchParams(sourceRef, params) {
51
76
  const setIfEmpty = (key, value) => {
52
77
  if (sourceRef[key] !== undefined) {
@@ -62,26 +87,350 @@ function applySearchParams(sourceRef, params) {
62
87
  setIfEmpty('utmCampaign', params.get('utm_campaign'));
63
88
  setIfEmpty('utmTerm', params.get('utm_term'));
64
89
  setIfEmpty('utmContent', params.get('utm_content'));
90
+ setIfEmpty('utmId', params.get('utm_id'));
65
91
  setIfEmpty('ref', params.get('ref'));
92
+ setIfEmpty('gclid', params.get('gclid'));
93
+ setIfEmpty('fbclid', params.get('fbclid'));
94
+ setIfEmpty('msclkid', params.get('msclkid'));
95
+ setIfEmpty('ttclid', params.get('ttclid'));
96
+ setIfEmpty('twclid', params.get('twclid'));
97
+ setIfEmpty('liFatId', params.get('li_fat_id'));
98
+ }
99
+ function normalizeHost(host) {
100
+ if (!host) {
101
+ return null;
102
+ }
103
+ return host.trim().toLowerCase() || null;
104
+ }
105
+ function getRootDomain(host) {
106
+ const normalizedHost = normalizeHost(host);
107
+ if (!normalizedHost) {
108
+ return null;
109
+ }
110
+ const hostname = normalizedHost.split(':')[0];
111
+ if (hostname === 'localhost' || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
112
+ return hostname;
113
+ }
114
+ const parts = hostname.split('.').filter(Boolean);
115
+ if (parts.length <= 2) {
116
+ return hostname;
117
+ }
118
+ return parts.slice(-2).join('.');
119
+ }
120
+ function isInternalReferer(landingHost, refererHost) {
121
+ const normalizedLandingHost = normalizeHost(landingHost);
122
+ const normalizedRefererHost = normalizeHost(refererHost);
123
+ if (!normalizedLandingHost || !normalizedRefererHost) {
124
+ return false;
125
+ }
126
+ if (normalizedLandingHost === normalizedRefererHost) {
127
+ return true;
128
+ }
129
+ return normalizedLandingHost.endsWith(`.${normalizedRefererHost}`)
130
+ || normalizedRefererHost.endsWith(`.${normalizedLandingHost}`);
131
+ }
132
+ function detectPlatform(value) {
133
+ const normalized = value === null || value === void 0 ? void 0 : value.trim().toLowerCase();
134
+ if (!normalized) {
135
+ return null;
136
+ }
137
+ const matcherList = [
138
+ { pattern: /chatgpt|chat-openai|openai/, platform: 'openai', channel: 'ai' },
139
+ { pattern: /claude|anthropic/, platform: 'anthropic', channel: 'ai' },
140
+ { pattern: /perplexity/, platform: 'perplexity', channel: 'ai' },
141
+ { pattern: /gemini/, platform: 'gemini', channel: 'ai' },
142
+ { pattern: /copilot/, platform: 'copilot', channel: 'ai' },
143
+ { pattern: /google/, platform: 'google', channel: 'search' },
144
+ { pattern: /bing/, platform: 'bing', channel: 'search' },
145
+ { pattern: /baidu/, platform: 'baidu', channel: 'search' },
146
+ { pattern: /yahoo/, platform: 'yahoo', channel: 'search' },
147
+ { pattern: /duckduckgo/, platform: 'duckduckgo', channel: 'search' },
148
+ { pattern: /facebook/, platform: 'facebook', channel: 'social' },
149
+ { pattern: /instagram/, platform: 'instagram', channel: 'social' },
150
+ { pattern: /x\.com|twitter/, platform: 'x', channel: 'social' },
151
+ { pattern: /linkedin/, platform: 'linkedin', channel: 'social' },
152
+ { pattern: /reddit/, platform: 'reddit', channel: 'social' },
153
+ { pattern: /youtube/, platform: 'youtube', channel: 'social' },
154
+ ];
155
+ const matched = matcherList.find(({ pattern }) => pattern.test(normalized));
156
+ if (!matched) {
157
+ return null;
158
+ }
159
+ return matched.platform;
160
+ }
161
+ function detectChannelFromPlatform(platform) {
162
+ switch (platform) {
163
+ case 'openai':
164
+ case 'anthropic':
165
+ case 'perplexity':
166
+ case 'gemini':
167
+ case 'copilot':
168
+ return 'ai';
169
+ case 'google':
170
+ case 'bing':
171
+ case 'baidu':
172
+ case 'yahoo':
173
+ case 'duckduckgo':
174
+ return 'search';
175
+ case 'facebook':
176
+ case 'instagram':
177
+ case 'x':
178
+ case 'linkedin':
179
+ case 'reddit':
180
+ case 'youtube':
181
+ return 'social';
182
+ default:
183
+ return null;
184
+ }
185
+ }
186
+ function detectChannelFromUtmMedium(value) {
187
+ const normalized = value === null || value === void 0 ? void 0 : value.trim().toLowerCase();
188
+ if (!normalized) {
189
+ return null;
190
+ }
191
+ if (/^(cpc|ppc|paid|paid_search|display|banner|affiliate|email|newsletter|push|sms)$/.test(normalized)) {
192
+ return 'campaign';
193
+ }
194
+ if (/^(social|social_paid|social-organic|social_organic)$/.test(normalized)) {
195
+ return 'social';
196
+ }
197
+ if (/^(organic|seo|search)$/.test(normalized)) {
198
+ return 'search';
199
+ }
200
+ if (/^(referral|partner)$/.test(normalized)) {
201
+ return 'referral';
202
+ }
203
+ if (/^(ai|llm)$/.test(normalized)) {
204
+ return 'ai';
205
+ }
206
+ return 'campaign';
207
+ }
208
+ function parseUserAgent(request) {
209
+ var _a, _b, _c, _d, _e;
210
+ const userAgentHeader = request.headers.get('user-agent');
211
+ const secChUaMobile = (_a = normalizeQueryParam(request.headers.get('sec-ch-ua-mobile'))) !== null && _a !== void 0 ? _a : undefined;
212
+ const secChUaPlatform = (_b = normalizeQueryParam(request.headers.get('sec-ch-ua-platform'))) !== null && _b !== void 0 ? _b : undefined;
213
+ const userAgent = (_d = (_c = normalizeSourceRef(userAgentHeader)) === null || _c === void 0 ? void 0 : _c.slice(0, USER_AGENT_MAX_LENGTH)) !== null && _d !== void 0 ? _d : undefined;
214
+ const ua = (_e = userAgent === null || userAgent === void 0 ? void 0 : userAgent.toLowerCase()) !== null && _e !== void 0 ? _e : '';
215
+ let deviceType = 'desktop';
216
+ if (!ua) {
217
+ deviceType = 'unknown';
218
+ }
219
+ else if (/bot|spider|crawler|curl|wget|headless/.test(ua)) {
220
+ deviceType = 'bot';
221
+ }
222
+ else if (/ipad|tablet/.test(ua)) {
223
+ deviceType = 'tablet';
224
+ }
225
+ else if (/mobi|iphone|android/.test(ua) || secChUaMobile === '?1') {
226
+ deviceType = 'mobile';
227
+ }
228
+ let os = 'Unknown';
229
+ if (/iphone|ipad|ipod/.test(ua)) {
230
+ os = 'iOS';
231
+ }
232
+ else if (/android/.test(ua)) {
233
+ os = 'Android';
234
+ }
235
+ else if (/windows nt/.test(ua)) {
236
+ os = 'Windows';
237
+ }
238
+ else if (/mac os x|macintosh/.test(ua)) {
239
+ os = 'macOS';
240
+ }
241
+ else if (/cros/.test(ua)) {
242
+ os = 'Chrome OS';
243
+ }
244
+ else if (/linux/.test(ua)) {
245
+ os = 'Linux';
246
+ }
247
+ if (secChUaPlatform) {
248
+ const normalizedPlatform = secChUaPlatform.replaceAll('"', '');
249
+ if (normalizedPlatform && normalizedPlatform !== 'Unknown') {
250
+ os = normalizedPlatform;
251
+ }
252
+ }
253
+ let browser = 'Unknown';
254
+ if (/edg\//.test(ua)) {
255
+ browser = 'Edge';
256
+ }
257
+ else if (/opr\//.test(ua) || /opera/.test(ua)) {
258
+ browser = 'Opera';
259
+ }
260
+ else if (/samsungbrowser\//.test(ua)) {
261
+ browser = 'Samsung Internet';
262
+ }
263
+ else if (/crios\//.test(ua) || /chrome\//.test(ua)) {
264
+ browser = 'Chrome';
265
+ }
266
+ else if (/firefox\//.test(ua)) {
267
+ browser = 'Firefox';
268
+ }
269
+ else if (/safari\//.test(ua) && !/chrome\//.test(ua) && !/crios\//.test(ua)) {
270
+ browser = 'Safari';
271
+ }
272
+ return {
273
+ userAgent,
274
+ deviceType,
275
+ os,
276
+ browser,
277
+ secChUaMobile,
278
+ secChUaPlatform,
279
+ };
280
+ }
281
+ function parseFirstTouchHeader(request) {
282
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
283
+ const rawHeader = request.headers.get(FIRST_TOUCH_HEADER_NAME);
284
+ const normalizedHeader = (_a = normalizeSourceRef(rawHeader)) === null || _a === void 0 ? void 0 : _a.slice(0, FIRST_TOUCH_HEADER_MAX_LENGTH);
285
+ if (!normalizedHeader) {
286
+ return null;
287
+ }
288
+ const decodedHeader = decodeHeaderValue(normalizedHeader);
289
+ if (!decodedHeader) {
290
+ return null;
291
+ }
292
+ try {
293
+ const parsed = JSON.parse(decodedHeader);
294
+ const sourceRef = {};
295
+ sourceRef.capturedAt = (_b = normalizeQueryParam(typeof parsed.capturedAt === 'string' ? parsed.capturedAt : null)) !== null && _b !== void 0 ? _b : undefined;
296
+ sourceRef.landingUrl = (_c = normalizeSourceRef(typeof parsed.landingUrl === 'string' ? parsed.landingUrl : null)) !== null && _c !== void 0 ? _c : undefined;
297
+ sourceRef.landingPath = (_d = normalizeSourceRef(typeof parsed.landingPath === 'string' ? parsed.landingPath : null)) !== null && _d !== void 0 ? _d : undefined;
298
+ sourceRef.landingHost = (_e = normalizeHost(typeof parsed.landingHost === 'string' ? parsed.landingHost : null)) !== null && _e !== void 0 ? _e : undefined;
299
+ sourceRef.ref = (_f = normalizeQueryParam(typeof parsed.ref === 'string' ? parsed.ref : null)) !== null && _f !== void 0 ? _f : undefined;
300
+ sourceRef.utmSource = (_g = normalizeQueryParam(typeof parsed.utmSource === 'string' ? parsed.utmSource : null)) !== null && _g !== void 0 ? _g : undefined;
301
+ sourceRef.utmMedium = (_h = normalizeQueryParam(typeof parsed.utmMedium === 'string' ? parsed.utmMedium : null)) !== null && _h !== void 0 ? _h : undefined;
302
+ sourceRef.utmCampaign = (_j = normalizeQueryParam(typeof parsed.utmCampaign === 'string' ? parsed.utmCampaign : null)) !== null && _j !== void 0 ? _j : undefined;
303
+ sourceRef.utmTerm = (_k = normalizeQueryParam(typeof parsed.utmTerm === 'string' ? parsed.utmTerm : null)) !== null && _k !== void 0 ? _k : undefined;
304
+ sourceRef.utmContent = (_l = normalizeQueryParam(typeof parsed.utmContent === 'string' ? parsed.utmContent : null)) !== null && _l !== void 0 ? _l : undefined;
305
+ sourceRef.utmId = (_m = normalizeQueryParam(typeof parsed.utmId === 'string' ? parsed.utmId : null)) !== null && _m !== void 0 ? _m : undefined;
306
+ sourceRef.gclid = (_o = normalizeQueryParam(typeof parsed.gclid === 'string' ? parsed.gclid : null)) !== null && _o !== void 0 ? _o : undefined;
307
+ sourceRef.fbclid = (_p = normalizeQueryParam(typeof parsed.fbclid === 'string' ? parsed.fbclid : null)) !== null && _p !== void 0 ? _p : undefined;
308
+ sourceRef.msclkid = (_q = normalizeQueryParam(typeof parsed.msclkid === 'string' ? parsed.msclkid : null)) !== null && _q !== void 0 ? _q : undefined;
309
+ sourceRef.ttclid = (_r = normalizeQueryParam(typeof parsed.ttclid === 'string' ? parsed.ttclid : null)) !== null && _r !== void 0 ? _r : undefined;
310
+ sourceRef.twclid = (_s = normalizeQueryParam(typeof parsed.twclid === 'string' ? parsed.twclid : null)) !== null && _s !== void 0 ? _s : undefined;
311
+ sourceRef.liFatId = (_t = normalizeQueryParam(typeof parsed.liFatId === 'string' ? parsed.liFatId : null)) !== null && _t !== void 0 ? _t : undefined;
312
+ const externalReferrer = normalizeSourceRef(typeof parsed.externalReferrer === 'string' ? parsed.externalReferrer : null);
313
+ if (externalReferrer) {
314
+ sourceRef.httpRefer = externalReferrer;
315
+ try {
316
+ const refererUrl = new URL(externalReferrer);
317
+ sourceRef.refererHost = (_u = normalizeHost(refererUrl.host)) !== null && _u !== void 0 ? _u : undefined;
318
+ sourceRef.refererPath = (_v = normalizeSourceRef(refererUrl.pathname)) !== null && _v !== void 0 ? _v : undefined;
319
+ sourceRef.refererDomain = (_w = getRootDomain(refererUrl.host)) !== null && _w !== void 0 ? _w : undefined;
320
+ applySearchParams(sourceRef, refererUrl.searchParams);
321
+ }
322
+ catch (error) {
323
+ console.warn('Failed to parse first-touch referrer url:', error);
324
+ }
325
+ }
326
+ return Object.keys(sourceRef).length > 0 ? sourceRef : null;
327
+ }
328
+ catch (error) {
329
+ console.warn('Failed to parse first-touch header:', error);
330
+ return null;
331
+ }
332
+ }
333
+ function finalizeAttribution(sourceRef) {
334
+ var _a, _b, _c, _d, _e;
335
+ const landingHost = normalizeHost(sourceRef.landingHost);
336
+ const refererHost = normalizeHost(sourceRef.refererHost);
337
+ const internal = isInternalReferer(landingHost, refererHost);
338
+ const hasCampaignMarker = Boolean(sourceRef.utmSource
339
+ || sourceRef.utmMedium
340
+ || sourceRef.utmCampaign
341
+ || sourceRef.utmTerm
342
+ || sourceRef.utmContent
343
+ || sourceRef.utmId
344
+ || sourceRef.ref
345
+ || sourceRef.gclid
346
+ || sourceRef.fbclid
347
+ || sourceRef.msclkid
348
+ || sourceRef.ttclid
349
+ || sourceRef.twclid
350
+ || sourceRef.liFatId);
351
+ if (internal) {
352
+ sourceRef.isInternalReferer = true;
353
+ }
354
+ const utmPlatform = detectPlatform(sourceRef.utmSource) || detectPlatform(sourceRef.ref);
355
+ if (utmPlatform) {
356
+ sourceRef.sourcePlatform = utmPlatform;
357
+ sourceRef.sourceChannel = (_c = (_b = (_a = detectChannelFromPlatform(utmPlatform)) !== null && _a !== void 0 ? _a : detectChannelFromUtmMedium(sourceRef.utmMedium)) !== null && _b !== void 0 ? _b : sourceRef.sourceChannel) !== null && _c !== void 0 ? _c : 'campaign';
358
+ sourceRef.sourceType = 'campaign';
359
+ return;
360
+ }
361
+ if (sourceRef.gclid) {
362
+ sourceRef.sourcePlatform = 'google';
363
+ sourceRef.sourceChannel = 'search';
364
+ sourceRef.sourceType = 'campaign';
365
+ return;
366
+ }
367
+ if (sourceRef.msclkid) {
368
+ sourceRef.sourcePlatform = 'bing';
369
+ sourceRef.sourceChannel = 'search';
370
+ sourceRef.sourceType = 'campaign';
371
+ return;
372
+ }
373
+ if (sourceRef.fbclid) {
374
+ sourceRef.sourcePlatform = 'facebook';
375
+ sourceRef.sourceChannel = 'social';
376
+ sourceRef.sourceType = 'campaign';
377
+ return;
378
+ }
379
+ if (sourceRef.ttclid) {
380
+ sourceRef.sourcePlatform = 'tiktok';
381
+ sourceRef.sourceChannel = 'social';
382
+ sourceRef.sourceType = 'campaign';
383
+ return;
384
+ }
385
+ if (sourceRef.twclid) {
386
+ sourceRef.sourcePlatform = 'x';
387
+ sourceRef.sourceChannel = 'social';
388
+ sourceRef.sourceType = 'campaign';
389
+ return;
390
+ }
391
+ if (sourceRef.liFatId) {
392
+ sourceRef.sourcePlatform = 'linkedin';
393
+ sourceRef.sourceChannel = 'social';
394
+ sourceRef.sourceType = 'campaign';
395
+ return;
396
+ }
397
+ if (hasCampaignMarker) {
398
+ sourceRef.sourcePlatform = 'other';
399
+ sourceRef.sourceChannel = (_d = detectChannelFromUtmMedium(sourceRef.utmMedium)) !== null && _d !== void 0 ? _d : 'campaign';
400
+ sourceRef.sourceType = 'campaign';
401
+ return;
402
+ }
403
+ if (!internal && refererHost) {
404
+ const refererPlatform = detectPlatform(refererHost) || detectPlatform(sourceRef.httpRefer);
405
+ sourceRef.sourcePlatform = refererPlatform !== null && refererPlatform !== void 0 ? refererPlatform : 'other';
406
+ sourceRef.sourceChannel = (_e = detectChannelFromPlatform(refererPlatform)) !== null && _e !== void 0 ? _e : 'referral';
407
+ sourceRef.sourceType = 'referer';
408
+ return;
409
+ }
410
+ sourceRef.sourcePlatform = 'direct';
411
+ sourceRef.sourceChannel = 'direct';
412
+ sourceRef.sourceType = 'direct';
66
413
  }
67
414
  // 提取用户首次访问来源
68
415
  function extractSourceRef(request) {
416
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
69
417
  const headerRef = request.headers.get('referer') || request.headers.get('referrer');
70
418
  const customRef = request.headers.get('x-source-ref');
71
419
  const queryRef = request.nextUrl.searchParams.get('ref');
72
- console.log({
73
- headerRef,
74
- customRef,
75
- queryRef
76
- });
77
- const sourceRef = {};
420
+ const firstTouchRef = parseFirstTouchHeader(request);
421
+ const sourceRef = Object.assign({}, parseUserAgent(request));
422
+ mergeSourceRef(sourceRef, firstTouchRef);
423
+ sourceRef.landingUrl = (_b = (_a = sourceRef.landingUrl) !== null && _a !== void 0 ? _a : normalizeSourceRef(request.nextUrl.toString())) !== null && _b !== void 0 ? _b : undefined;
424
+ sourceRef.landingPath = (_d = (_c = sourceRef.landingPath) !== null && _c !== void 0 ? _c : normalizeSourceRef(request.nextUrl.pathname)) !== null && _d !== void 0 ? _d : undefined;
425
+ sourceRef.landingHost = (_f = (_e = sourceRef.landingHost) !== null && _e !== void 0 ? _e : normalizeHost(request.nextUrl.host)) !== null && _f !== void 0 ? _f : undefined;
426
+ sourceRef.ref = (_h = (_g = sourceRef.ref) !== null && _g !== void 0 ? _g : normalizeQueryParam(queryRef)) !== null && _h !== void 0 ? _h : undefined;
78
427
  let normalizedHttpRef = null;
79
- const candidates = [headerRef, customRef, queryRef];
428
+ const candidates = [customRef, headerRef];
80
429
  for (const candidate of candidates) {
81
430
  const normalized = normalizeSourceRef(candidate);
82
431
  if (normalized) {
83
432
  normalizedHttpRef = normalized;
84
- sourceRef.httpRefer = normalized;
433
+ sourceRef.httpRefer = (_j = sourceRef.httpRefer) !== null && _j !== void 0 ? _j : normalized;
85
434
  break;
86
435
  }
87
436
  }
@@ -90,12 +439,16 @@ function extractSourceRef(request) {
90
439
  if (normalizedHttpRef) {
91
440
  try {
92
441
  const refererUrl = new URL(normalizedHttpRef);
442
+ sourceRef.refererHost = (_l = (_k = sourceRef.refererHost) !== null && _k !== void 0 ? _k : normalizeHost(refererUrl.host)) !== null && _l !== void 0 ? _l : undefined;
443
+ sourceRef.refererPath = (_o = (_m = sourceRef.refererPath) !== null && _m !== void 0 ? _m : normalizeSourceRef(refererUrl.pathname)) !== null && _o !== void 0 ? _o : undefined;
444
+ sourceRef.refererDomain = (_q = (_p = sourceRef.refererDomain) !== null && _p !== void 0 ? _p : getRootDomain(refererUrl.host)) !== null && _q !== void 0 ? _q : undefined;
93
445
  applySearchParams(sourceRef, refererUrl.searchParams);
94
446
  }
95
447
  catch (error) {
96
448
  console.warn('Failed to parse referer url for utm/ref:', error);
97
449
  }
98
450
  }
451
+ finalizeAttribution(sourceRef);
99
452
  return Object.keys(sourceRef).length > 0 ? sourceRef : null;
100
453
  }
101
454
  /**
@@ -172,17 +525,22 @@ function handleFingerprintRequest(request_1) {
172
525
  return createErrorResponse('User not found', 404);
173
526
  }
174
527
  const sourceRef = extractSourceRef(request);
175
- // 创建新的匿名用户
176
- const { newUser, credit } = yield userAggregateService.initAnonymousUser(fingerprintId, { sourceRef: sourceRef !== null && sourceRef !== void 0 ? sourceRef : undefined });
177
- console.log(`Created new anonymous user ${newUser.userId} with fingerprint ${fingerprintId}`);
528
+ const anonymousInitResult = yield anonymousAggregateService.getOrCreateByFingerprintId(fingerprintId, { sourceRef: sourceRef !== null && sourceRef !== void 0 ? sourceRef : undefined });
529
+ if (anonymousInitResult.isNewUser) {
530
+ console.log(`Created new anonymous user ${anonymousInitResult.user.userId} with fingerprint ${fingerprintId}`);
531
+ }
178
532
  // 返回创建结果
179
533
  const response = createSuccessResponse({
180
534
  entities: {
181
- user: newUser,
182
- credit,
183
- subscription: null,
535
+ user: anonymousInitResult.user,
536
+ credit: anonymousInitResult.credit,
537
+ subscription: anonymousInitResult.subscription,
538
+ },
539
+ isNewUser: anonymousInitResult.isNewUser,
540
+ options: {
541
+ totalUsersOnDevice: anonymousInitResult.totalUsersOnDevice,
542
+ hasAnonymousUser: anonymousInitResult.hasAnonymousUser,
184
543
  },
185
- isNewUser: true,
186
544
  });
187
545
  return NextResponse.json(response);
188
546
  }
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ var apilog_service = require('./services/database/apilog.service.js');
12
12
  var constants = require('./services/database/constants.js');
13
13
  var user_aggregate_service = require('./services/aggregate/user.aggregate.service.js');
14
14
  var billing_aggregate_service = require('./services/aggregate/billing.aggregate.service.js');
15
+ var anonymous_aggregate_service = require('./services/aggregate/anonymous.aggregate.service.js');
15
16
  var userContextService = require('./services/context/user-context-service.js');
16
17
  var webhookHandler = require('./services/stripe/webhook-handler.js');
17
18
  var moneyPriceConfig = require('./lib/money-price-config.js');
@@ -60,6 +61,7 @@ exports.isValidTransactionType = constants.isValidTransactionType;
60
61
  exports.isValidUserStatus = constants.isValidUserStatus;
61
62
  exports.userAggregateService = user_aggregate_service.userAggregateService;
62
63
  exports.billingAggregateService = billing_aggregate_service.billingAggregateService;
64
+ exports.anonymousAggregateService = anonymous_aggregate_service.anonymousAggregateService;
63
65
  exports.applyUserMockContext = userContextService.applyUserMockContext;
64
66
  exports.buildInitUserContextFromEntities = userContextService.buildInitUserContextFromEntities;
65
67
  exports.fetchLatestUserContextByFingerprintId = userContextService.fetchLatestUserContextByFingerprintId;
package/dist/index.mjs CHANGED
@@ -10,6 +10,7 @@ export { Apilogger, apilogService } from './services/database/apilog.service.mjs
10
10
  export { BillingReason, CreditType, OperationType, OrderStatus, PaySupplier, PaymentStatus, SubscriptionStatus, TransactionType, UserStatus, isValidBillingReason, isValidCreditType, isValidOperationType, isValidOrderStatus, isValidPaymentStatus, isValidSubscriptionStatus, isValidTransactionType, isValidUserStatus } from './services/database/constants.mjs';
11
11
  export { userAggregateService } from './services/aggregate/user.aggregate.service.mjs';
12
12
  export { billingAggregateService } from './services/aggregate/billing.aggregate.service.mjs';
13
+ export { anonymousAggregateService } from './services/aggregate/anonymous.aggregate.service.mjs';
13
14
  export { applyUserMockContext, buildInitUserContextFromEntities, fetchLatestUserContextByFingerprintId, fetchUserContextByClerkUserId, mapCreditToXCredit, mapSubscriptionToXSubscription, mapUserToXUser } from './services/context/user-context-service.mjs';
14
15
  export { handleStripeEvent } from './services/stripe/webhook-handler.mjs';
15
16
  export { getActiveProviderConfig, getCreditsFromPriceId, getPriceConfig, moneyPriceConfig } from './lib/money-price-config.mjs';
@@ -0,0 +1,21 @@
1
+ import type { Credit, Subscription, User } from '@/db/prisma-model-type';
2
+ import { Prisma } from '@/db/prisma-model-type';
3
+ type AnonymousInitContext = {
4
+ user: User;
5
+ credit: Credit | null;
6
+ subscription: Subscription | null;
7
+ isNewUser: boolean;
8
+ totalUsersOnDevice: number;
9
+ hasAnonymousUser: boolean;
10
+ };
11
+ declare class AnonymousAggregateService {
12
+ private lockFingerprintInit;
13
+ private findLatestUserContextByFingerprintId;
14
+ private createAnonymousUser;
15
+ getOrCreateByFingerprintId(fingerprintId: string, options?: {
16
+ sourceRef?: Prisma.InputJsonValue;
17
+ }): Promise<AnonymousInitContext>;
18
+ }
19
+ export declare const anonymousAggregateService: AnonymousAggregateService;
20
+ export {};
21
+ //# sourceMappingURL=anonymous.aggregate.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anonymous.aggregate.service.d.ts","sourceRoot":"","sources":["../../../src/services/aggregate/anonymous.aggregate.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAIzE,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAIhD,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,cAAM,yBAAyB;YACf,mBAAmB;YAYnB,oCAAoC;YAyBpC,mBAAmB;IAsC3B,0BAA0B,CAC9B,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;KAAE,GAC/C,OAAO,CAAC,oBAAoB,CAAC;CAYjC;AAED,eAAO,MAAM,yBAAyB,2BAAkC,CAAC"}
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ var tslib_es6 = require('../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js');
4
+ var user_service = require('../database/user.service.js');
5
+ var subscription_service = require('../database/subscription.service.js');
6
+ var credit_service = require('../database/credit.service.js');
7
+ var client = require('@prisma/client');
8
+ var constants = require('../database/constants.js');
9
+ require('../../prisma/prisma.js');
10
+ var prismaTransactionUtil = require('../../prisma/prisma-transaction-util.js');
11
+ var creditInit = require('../../lib/credit-init.js');
12
+
13
+ const ANONYMOUS_INIT_LOCK_NAMESPACE = 92831;
14
+ class AnonymousAggregateService {
15
+ lockFingerprintInit(tx, fingerprintId) {
16
+ return tslib_es6.__awaiter(this, void 0, void 0, function* () {
17
+ yield tx.$executeRaw `
18
+ SELECT pg_advisory_xact_lock(
19
+ ${client.Prisma.raw(String(ANONYMOUS_INIT_LOCK_NAMESPACE))},
20
+ hashtext(${fingerprintId})
21
+ )
22
+ `;
23
+ });
24
+ }
25
+ findLatestUserContextByFingerprintId(fingerprintId, tx) {
26
+ return tslib_es6.__awaiter(this, void 0, void 0, function* () {
27
+ const existingUsers = yield user_service.userService.findListByFingerprintId(fingerprintId, tx);
28
+ if (existingUsers.length === 0) {
29
+ return null;
30
+ }
31
+ const latestUser = existingUsers[0];
32
+ const [credit, subscription] = yield Promise.all([
33
+ credit_service.creditService.getCredit(latestUser.userId, tx),
34
+ subscription_service.subscriptionService.getActiveSubscription(latestUser.userId, tx),
35
+ ]);
36
+ return {
37
+ user: latestUser,
38
+ credit,
39
+ subscription,
40
+ isNewUser: false,
41
+ totalUsersOnDevice: existingUsers.length,
42
+ hasAnonymousUser: true,
43
+ };
44
+ });
45
+ }
46
+ createAnonymousUser(fingerprintId, tx, options) {
47
+ return tslib_es6.__awaiter(this, void 0, void 0, function* () {
48
+ const newUser = yield user_service.userService.createUser({
49
+ fingerprintId,
50
+ sourceRef: options === null || options === void 0 ? void 0 : options.sourceRef,
51
+ status: constants.UserStatus.ANONYMOUS,
52
+ }, tx);
53
+ const credit = yield credit_service.creditService.initializeCreditWithFree({
54
+ userId: newUser.userId,
55
+ feature: 'anonymous_user_init',
56
+ creditType: constants.CreditType.FREE,
57
+ operationType: constants.OperationType.SYS_GIFT,
58
+ operationReferId: newUser.userId,
59
+ creditsChange: creditInit.freeAmount,
60
+ }, tx);
61
+ yield subscription_service.subscriptionService.initializeSubscription(newUser.userId, tx);
62
+ return {
63
+ user: newUser,
64
+ credit,
65
+ subscription: null,
66
+ isNewUser: true,
67
+ totalUsersOnDevice: 1,
68
+ hasAnonymousUser: true,
69
+ };
70
+ });
71
+ }
72
+ getOrCreateByFingerprintId(fingerprintId, options) {
73
+ return tslib_es6.__awaiter(this, void 0, void 0, function* () {
74
+ return prismaTransactionUtil.runInTransaction((tx) => tslib_es6.__awaiter(this, void 0, void 0, function* () {
75
+ yield this.lockFingerprintInit(tx, fingerprintId);
76
+ const existingContext = yield this.findLatestUserContextByFingerprintId(fingerprintId, tx);
77
+ if (existingContext) {
78
+ return existingContext;
79
+ }
80
+ return this.createAnonymousUser(fingerprintId, tx, options);
81
+ }), 'anonymous_get_or_create_by_fingerprint_id');
82
+ });
83
+ }
84
+ }
85
+ const anonymousAggregateService = new AnonymousAggregateService();
86
+
87
+ exports.anonymousAggregateService = anonymousAggregateService;