@windrun-huaiin/backend-core 14.1.1 → 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 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../src/app/api/user/anonymous/init/route.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgoBxD;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,kCAE9C"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../src/app/api/user/anonymous/init/route.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA2rBxD;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,kCAE9C"}
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
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_aggregate_service = require('../../../../../services/aggregate/user.aggregate.service.js');
4
+ var anonymous_aggregate_service = require('../../../../../services/aggregate/anonymous.aggregate.service.js');
5
5
  var server = require('@windrun-huaiin/third-ui/fingerprint/server');
6
6
  var server$1 = require('@clerk/nextjs/server');
7
7
  var server$2 = require('next/server');
@@ -185,6 +185,28 @@ function detectChannelFromPlatform(platform) {
185
185
  return null;
186
186
  }
187
187
  }
188
+ function detectChannelFromUtmMedium(value) {
189
+ const normalized = value === null || value === void 0 ? void 0 : value.trim().toLowerCase();
190
+ if (!normalized) {
191
+ return null;
192
+ }
193
+ if (/^(cpc|ppc|paid|paid_search|display|banner|affiliate|email|newsletter|push|sms)$/.test(normalized)) {
194
+ return 'campaign';
195
+ }
196
+ if (/^(social|social_paid|social-organic|social_organic)$/.test(normalized)) {
197
+ return 'social';
198
+ }
199
+ if (/^(organic|seo|search)$/.test(normalized)) {
200
+ return 'search';
201
+ }
202
+ if (/^(referral|partner)$/.test(normalized)) {
203
+ return 'referral';
204
+ }
205
+ if (/^(ai|llm)$/.test(normalized)) {
206
+ return 'ai';
207
+ }
208
+ return 'campaign';
209
+ }
188
210
  function parseUserAgent(request) {
189
211
  var _a, _b, _c, _d, _e;
190
212
  const userAgentHeader = request.headers.get('user-agent');
@@ -311,17 +333,30 @@ function parseFirstTouchHeader(request) {
311
333
  }
312
334
  }
313
335
  function finalizeAttribution(sourceRef) {
314
- var _a, _b, _c, _d;
336
+ var _a, _b, _c, _d, _e;
315
337
  const landingHost = normalizeHost(sourceRef.landingHost);
316
338
  const refererHost = normalizeHost(sourceRef.refererHost);
317
339
  const internal = isInternalReferer(landingHost, refererHost);
340
+ const hasCampaignMarker = Boolean(sourceRef.utmSource
341
+ || sourceRef.utmMedium
342
+ || sourceRef.utmCampaign
343
+ || sourceRef.utmTerm
344
+ || sourceRef.utmContent
345
+ || sourceRef.utmId
346
+ || sourceRef.ref
347
+ || sourceRef.gclid
348
+ || sourceRef.fbclid
349
+ || sourceRef.msclkid
350
+ || sourceRef.ttclid
351
+ || sourceRef.twclid
352
+ || sourceRef.liFatId);
318
353
  if (internal) {
319
354
  sourceRef.isInternalReferer = true;
320
355
  }
321
356
  const utmPlatform = detectPlatform(sourceRef.utmSource) || detectPlatform(sourceRef.ref);
322
357
  if (utmPlatform) {
323
358
  sourceRef.sourcePlatform = utmPlatform;
324
- sourceRef.sourceChannel = (_b = (_a = detectChannelFromPlatform(utmPlatform)) !== null && _a !== void 0 ? _a : sourceRef.sourceChannel) !== null && _b !== void 0 ? _b : 'campaign';
359
+ 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';
325
360
  sourceRef.sourceType = 'campaign';
326
361
  return;
327
362
  }
@@ -361,10 +396,16 @@ function finalizeAttribution(sourceRef) {
361
396
  sourceRef.sourceType = 'campaign';
362
397
  return;
363
398
  }
399
+ if (hasCampaignMarker) {
400
+ sourceRef.sourcePlatform = 'other';
401
+ sourceRef.sourceChannel = (_d = detectChannelFromUtmMedium(sourceRef.utmMedium)) !== null && _d !== void 0 ? _d : 'campaign';
402
+ sourceRef.sourceType = 'campaign';
403
+ return;
404
+ }
364
405
  if (!internal && refererHost) {
365
406
  const refererPlatform = detectPlatform(refererHost) || detectPlatform(sourceRef.httpRefer);
366
- sourceRef.sourcePlatform = (_c = refererPlatform !== null && refererPlatform !== void 0 ? refererPlatform : getRootDomain(refererHost)) !== null && _c !== void 0 ? _c : refererHost;
367
- sourceRef.sourceChannel = (_d = detectChannelFromPlatform(refererPlatform)) !== null && _d !== void 0 ? _d : 'referral';
407
+ sourceRef.sourcePlatform = refererPlatform !== null && refererPlatform !== void 0 ? refererPlatform : 'other';
408
+ sourceRef.sourceChannel = (_e = detectChannelFromPlatform(refererPlatform)) !== null && _e !== void 0 ? _e : 'referral';
368
409
  sourceRef.sourceType = 'referer';
369
410
  return;
370
411
  }
@@ -486,17 +527,22 @@ function handleFingerprintRequest(request_1) {
486
527
  return createErrorResponse('User not found', 404);
487
528
  }
488
529
  const sourceRef = extractSourceRef(request);
489
- // 创建新的匿名用户
490
- const { newUser, credit } = yield user_aggregate_service.userAggregateService.initAnonymousUser(fingerprintId, { sourceRef: sourceRef !== null && sourceRef !== void 0 ? sourceRef : undefined });
491
- console.log(`Created new anonymous user ${newUser.userId} with fingerprint ${fingerprintId}`);
530
+ const anonymousInitResult = yield anonymous_aggregate_service.anonymousAggregateService.getOrCreateByFingerprintId(fingerprintId, { sourceRef: sourceRef !== null && sourceRef !== void 0 ? sourceRef : undefined });
531
+ if (anonymousInitResult.isNewUser) {
532
+ console.log(`Created new anonymous user ${anonymousInitResult.user.userId} with fingerprint ${fingerprintId}`);
533
+ }
492
534
  // 返回创建结果
493
535
  const response = createSuccessResponse({
494
536
  entities: {
495
- user: newUser,
496
- credit,
497
- subscription: null,
537
+ user: anonymousInitResult.user,
538
+ credit: anonymousInitResult.credit,
539
+ subscription: anonymousInitResult.subscription,
540
+ },
541
+ isNewUser: anonymousInitResult.isNewUser,
542
+ options: {
543
+ totalUsersOnDevice: anonymousInitResult.totalUsersOnDevice,
544
+ hasAnonymousUser: anonymousInitResult.hasAnonymousUser,
498
545
  },
499
- isNewUser: true,
500
546
  });
501
547
  return server$2.NextResponse.json(response);
502
548
  }
@@ -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';
@@ -183,6 +183,28 @@ function detectChannelFromPlatform(platform) {
183
183
  return null;
184
184
  }
185
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
+ }
186
208
  function parseUserAgent(request) {
187
209
  var _a, _b, _c, _d, _e;
188
210
  const userAgentHeader = request.headers.get('user-agent');
@@ -309,17 +331,30 @@ function parseFirstTouchHeader(request) {
309
331
  }
310
332
  }
311
333
  function finalizeAttribution(sourceRef) {
312
- var _a, _b, _c, _d;
334
+ var _a, _b, _c, _d, _e;
313
335
  const landingHost = normalizeHost(sourceRef.landingHost);
314
336
  const refererHost = normalizeHost(sourceRef.refererHost);
315
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);
316
351
  if (internal) {
317
352
  sourceRef.isInternalReferer = true;
318
353
  }
319
354
  const utmPlatform = detectPlatform(sourceRef.utmSource) || detectPlatform(sourceRef.ref);
320
355
  if (utmPlatform) {
321
356
  sourceRef.sourcePlatform = utmPlatform;
322
- sourceRef.sourceChannel = (_b = (_a = detectChannelFromPlatform(utmPlatform)) !== null && _a !== void 0 ? _a : sourceRef.sourceChannel) !== null && _b !== void 0 ? _b : 'campaign';
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';
323
358
  sourceRef.sourceType = 'campaign';
324
359
  return;
325
360
  }
@@ -359,10 +394,16 @@ function finalizeAttribution(sourceRef) {
359
394
  sourceRef.sourceType = 'campaign';
360
395
  return;
361
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
+ }
362
403
  if (!internal && refererHost) {
363
404
  const refererPlatform = detectPlatform(refererHost) || detectPlatform(sourceRef.httpRefer);
364
- sourceRef.sourcePlatform = (_c = refererPlatform !== null && refererPlatform !== void 0 ? refererPlatform : getRootDomain(refererHost)) !== null && _c !== void 0 ? _c : refererHost;
365
- sourceRef.sourceChannel = (_d = detectChannelFromPlatform(refererPlatform)) !== null && _d !== void 0 ? _d : 'referral';
405
+ sourceRef.sourcePlatform = refererPlatform !== null && refererPlatform !== void 0 ? refererPlatform : 'other';
406
+ sourceRef.sourceChannel = (_e = detectChannelFromPlatform(refererPlatform)) !== null && _e !== void 0 ? _e : 'referral';
366
407
  sourceRef.sourceType = 'referer';
367
408
  return;
368
409
  }
@@ -484,17 +525,22 @@ function handleFingerprintRequest(request_1) {
484
525
  return createErrorResponse('User not found', 404);
485
526
  }
486
527
  const sourceRef = extractSourceRef(request);
487
- // 创建新的匿名用户
488
- const { newUser, credit } = yield userAggregateService.initAnonymousUser(fingerprintId, { sourceRef: sourceRef !== null && sourceRef !== void 0 ? sourceRef : undefined });
489
- 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
+ }
490
532
  // 返回创建结果
491
533
  const response = createSuccessResponse({
492
534
  entities: {
493
- user: newUser,
494
- credit,
495
- 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,
496
543
  },
497
- isNewUser: true,
498
544
  });
499
545
  return NextResponse.json(response);
500
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;
@@ -0,0 +1,85 @@
1
+ import { __awaiter } 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 { userService } from '../database/user.service.mjs';
3
+ import { subscriptionService } from '../database/subscription.service.mjs';
4
+ import { creditService } from '../database/credit.service.mjs';
5
+ import { Prisma } from '@prisma/client';
6
+ import { UserStatus, OperationType, CreditType } from '../database/constants.mjs';
7
+ import '../../prisma/prisma.mjs';
8
+ import { runInTransaction } from '../../prisma/prisma-transaction-util.mjs';
9
+ import { freeAmount } from '../../lib/credit-init.mjs';
10
+
11
+ const ANONYMOUS_INIT_LOCK_NAMESPACE = 92831;
12
+ class AnonymousAggregateService {
13
+ lockFingerprintInit(tx, fingerprintId) {
14
+ return __awaiter(this, void 0, void 0, function* () {
15
+ yield tx.$executeRaw `
16
+ SELECT pg_advisory_xact_lock(
17
+ ${Prisma.raw(String(ANONYMOUS_INIT_LOCK_NAMESPACE))},
18
+ hashtext(${fingerprintId})
19
+ )
20
+ `;
21
+ });
22
+ }
23
+ findLatestUserContextByFingerprintId(fingerprintId, tx) {
24
+ return __awaiter(this, void 0, void 0, function* () {
25
+ const existingUsers = yield userService.findListByFingerprintId(fingerprintId, tx);
26
+ if (existingUsers.length === 0) {
27
+ return null;
28
+ }
29
+ const latestUser = existingUsers[0];
30
+ const [credit, subscription] = yield Promise.all([
31
+ creditService.getCredit(latestUser.userId, tx),
32
+ subscriptionService.getActiveSubscription(latestUser.userId, tx),
33
+ ]);
34
+ return {
35
+ user: latestUser,
36
+ credit,
37
+ subscription,
38
+ isNewUser: false,
39
+ totalUsersOnDevice: existingUsers.length,
40
+ hasAnonymousUser: true,
41
+ };
42
+ });
43
+ }
44
+ createAnonymousUser(fingerprintId, tx, options) {
45
+ return __awaiter(this, void 0, void 0, function* () {
46
+ const newUser = yield userService.createUser({
47
+ fingerprintId,
48
+ sourceRef: options === null || options === void 0 ? void 0 : options.sourceRef,
49
+ status: UserStatus.ANONYMOUS,
50
+ }, tx);
51
+ const credit = yield creditService.initializeCreditWithFree({
52
+ userId: newUser.userId,
53
+ feature: 'anonymous_user_init',
54
+ creditType: CreditType.FREE,
55
+ operationType: OperationType.SYS_GIFT,
56
+ operationReferId: newUser.userId,
57
+ creditsChange: freeAmount,
58
+ }, tx);
59
+ yield subscriptionService.initializeSubscription(newUser.userId, tx);
60
+ return {
61
+ user: newUser,
62
+ credit,
63
+ subscription: null,
64
+ isNewUser: true,
65
+ totalUsersOnDevice: 1,
66
+ hasAnonymousUser: true,
67
+ };
68
+ });
69
+ }
70
+ getOrCreateByFingerprintId(fingerprintId, options) {
71
+ return __awaiter(this, void 0, void 0, function* () {
72
+ return runInTransaction((tx) => __awaiter(this, void 0, void 0, function* () {
73
+ yield this.lockFingerprintInit(tx, fingerprintId);
74
+ const existingContext = yield this.findLatestUserContextByFingerprintId(fingerprintId, tx);
75
+ if (existingContext) {
76
+ return existingContext;
77
+ }
78
+ return this.createAnonymousUser(fingerprintId, tx, options);
79
+ }), 'anonymous_get_or_create_by_fingerprint_id');
80
+ });
81
+ }
82
+ }
83
+ const anonymousAggregateService = new AnonymousAggregateService();
84
+
85
+ export { anonymousAggregateService };
@@ -1,3 +1,4 @@
1
1
  export { userAggregateService } from './user.aggregate.service';
2
2
  export { billingAggregateService } from './billing.aggregate.service';
3
+ export { anonymousAggregateService } from './anonymous.aggregate.service';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/aggregate/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/aggregate/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC"}
@@ -2,8 +2,10 @@
2
2
 
3
3
  var user_aggregate_service = require('./user.aggregate.service.js');
4
4
  var billing_aggregate_service = require('./billing.aggregate.service.js');
5
+ var anonymous_aggregate_service = require('./anonymous.aggregate.service.js');
5
6
 
6
7
 
7
8
 
8
9
  exports.userAggregateService = user_aggregate_service.userAggregateService;
9
10
  exports.billingAggregateService = billing_aggregate_service.billingAggregateService;
11
+ exports.anonymousAggregateService = anonymous_aggregate_service.anonymousAggregateService;
@@ -1,2 +1,3 @@
1
1
  export { userAggregateService } from './user.aggregate.service.mjs';
2
2
  export { billingAggregateService } from './billing.aggregate.service.mjs';
3
+ export { anonymousAggregateService } from './anonymous.aggregate.service.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/backend-core",
3
- "version": "14.1.1",
3
+ "version": "14.2.0",
4
4
  "description": "Shared backend primitives: Prisma schema/client, database services, routing helpers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -95,7 +95,7 @@
95
95
  "svix": "^1.86.0",
96
96
  "zod": "^4.3.6",
97
97
  "@windrun-huaiin/lib": "^14.0.0",
98
- "@windrun-huaiin/third-ui": "^14.0.2"
98
+ "@windrun-huaiin/third-ui": "^14.1.0"
99
99
  },
100
100
  "devDependencies": {
101
101
  "@rollup/plugin-alias": "^5.1.1",
@@ -5,7 +5,7 @@
5
5
  return this.toString();
6
6
  };
7
7
 
8
- import { userAggregateService } from '@/aggregate/user.aggregate.service';
8
+ import { anonymousAggregateService } from '@/aggregate/anonymous.aggregate.service';
9
9
  import type { XCredit, XSubscription, XUser } from '@windrun-huaiin/third-ui/fingerprint';
10
10
  import { extractFingerprintFromNextRequest } from '@windrun-huaiin/third-ui/fingerprint/server';
11
11
  import { auth } from '@clerk/nextjs/server';
@@ -306,6 +306,35 @@ function detectChannelFromPlatform(platform: string | null | undefined): string
306
306
  }
307
307
  }
308
308
 
309
+ function detectChannelFromUtmMedium(value: string | null | undefined): string | null {
310
+ const normalized = value?.trim().toLowerCase();
311
+ if (!normalized) {
312
+ return null;
313
+ }
314
+
315
+ if (/^(cpc|ppc|paid|paid_search|display|banner|affiliate|email|newsletter|push|sms)$/.test(normalized)) {
316
+ return 'campaign';
317
+ }
318
+
319
+ if (/^(social|social_paid|social-organic|social_organic)$/.test(normalized)) {
320
+ return 'social';
321
+ }
322
+
323
+ if (/^(organic|seo|search)$/.test(normalized)) {
324
+ return 'search';
325
+ }
326
+
327
+ if (/^(referral|partner)$/.test(normalized)) {
328
+ return 'referral';
329
+ }
330
+
331
+ if (/^(ai|llm)$/.test(normalized)) {
332
+ return 'ai';
333
+ }
334
+
335
+ return 'campaign';
336
+ }
337
+
309
338
  function parseUserAgent(request: NextRequest): Pick<SourceRefData, 'userAgent' | 'deviceType' | 'os' | 'browser' | 'secChUaMobile' | 'secChUaPlatform'> {
310
339
  const userAgentHeader = request.headers.get('user-agent');
311
340
  const secChUaMobile = normalizeQueryParam(request.headers.get('sec-ch-ua-mobile')) ?? undefined;
@@ -430,6 +459,21 @@ function finalizeAttribution(sourceRef: SourceRefData) {
430
459
  const landingHost = normalizeHost(sourceRef.landingHost);
431
460
  const refererHost = normalizeHost(sourceRef.refererHost);
432
461
  const internal = isInternalReferer(landingHost, refererHost);
462
+ const hasCampaignMarker = Boolean(
463
+ sourceRef.utmSource
464
+ || sourceRef.utmMedium
465
+ || sourceRef.utmCampaign
466
+ || sourceRef.utmTerm
467
+ || sourceRef.utmContent
468
+ || sourceRef.utmId
469
+ || sourceRef.ref
470
+ || sourceRef.gclid
471
+ || sourceRef.fbclid
472
+ || sourceRef.msclkid
473
+ || sourceRef.ttclid
474
+ || sourceRef.twclid
475
+ || sourceRef.liFatId
476
+ );
433
477
  if (internal) {
434
478
  sourceRef.isInternalReferer = true;
435
479
  }
@@ -437,7 +481,10 @@ function finalizeAttribution(sourceRef: SourceRefData) {
437
481
  const utmPlatform = detectPlatform(sourceRef.utmSource) || detectPlatform(sourceRef.ref);
438
482
  if (utmPlatform) {
439
483
  sourceRef.sourcePlatform = utmPlatform;
440
- sourceRef.sourceChannel = detectChannelFromPlatform(utmPlatform) ?? sourceRef.sourceChannel ?? 'campaign';
484
+ sourceRef.sourceChannel = detectChannelFromPlatform(utmPlatform)
485
+ ?? detectChannelFromUtmMedium(sourceRef.utmMedium)
486
+ ?? sourceRef.sourceChannel
487
+ ?? 'campaign';
441
488
  sourceRef.sourceType = 'campaign';
442
489
  return;
443
490
  }
@@ -484,9 +531,16 @@ function finalizeAttribution(sourceRef: SourceRefData) {
484
531
  return;
485
532
  }
486
533
 
534
+ if (hasCampaignMarker) {
535
+ sourceRef.sourcePlatform = 'other';
536
+ sourceRef.sourceChannel = detectChannelFromUtmMedium(sourceRef.utmMedium) ?? 'campaign';
537
+ sourceRef.sourceType = 'campaign';
538
+ return;
539
+ }
540
+
487
541
  if (!internal && refererHost) {
488
542
  const refererPlatform = detectPlatform(refererHost) || detectPlatform(sourceRef.httpRefer);
489
- sourceRef.sourcePlatform = refererPlatform ?? getRootDomain(refererHost) ?? refererHost;
543
+ sourceRef.sourcePlatform = refererPlatform ?? 'other';
490
544
  sourceRef.sourceChannel = detectChannelFromPlatform(refererPlatform) ?? 'referral';
491
545
  sourceRef.sourceType = 'referer';
492
546
  return;
@@ -624,22 +678,27 @@ async function handleFingerprintRequest(request: NextRequest, options: { createI
624
678
 
625
679
  const sourceRef = extractSourceRef(request);
626
680
 
627
- // 创建新的匿名用户
628
- const { newUser, credit } = await userAggregateService.initAnonymousUser(
681
+ const anonymousInitResult = await anonymousAggregateService.getOrCreateByFingerprintId(
629
682
  fingerprintId,
630
683
  { sourceRef: sourceRef?? undefined}
631
684
  );
632
685
 
633
- console.log(`Created new anonymous user ${newUser.userId} with fingerprint ${fingerprintId}`);
686
+ if (anonymousInitResult.isNewUser) {
687
+ console.log(`Created new anonymous user ${anonymousInitResult.user.userId} with fingerprint ${fingerprintId}`);
688
+ }
634
689
 
635
690
  // 返回创建结果
636
691
  const response = createSuccessResponse({
637
692
  entities: {
638
- user: newUser,
639
- credit,
640
- subscription: null,
693
+ user: anonymousInitResult.user,
694
+ credit: anonymousInitResult.credit,
695
+ subscription: anonymousInitResult.subscription,
696
+ },
697
+ isNewUser: anonymousInitResult.isNewUser,
698
+ options: {
699
+ totalUsersOnDevice: anonymousInitResult.totalUsersOnDevice,
700
+ hasAnonymousUser: anonymousInitResult.hasAnonymousUser,
641
701
  },
642
- isNewUser: true,
643
702
  });
644
703
  return NextResponse.json(response);
645
704
 
@@ -0,0 +1,113 @@
1
+ import { creditService, subscriptionService, userService } from '@/db';
2
+ import { UserStatus } from '@/db/constants';
3
+ import type { Credit, Subscription, User } from '@/db/prisma-model-type';
4
+ import { freeAmount } from '@/lib/credit-init';
5
+ import { runInTransaction } from '@/prisma/prisma-transaction-util';
6
+ import { CreditType, OperationType } from '@/db/constants';
7
+ import { Prisma } from '@/db/prisma-model-type';
8
+
9
+ const ANONYMOUS_INIT_LOCK_NAMESPACE = 92831;
10
+
11
+ type AnonymousInitContext = {
12
+ user: User;
13
+ credit: Credit | null;
14
+ subscription: Subscription | null;
15
+ isNewUser: boolean;
16
+ totalUsersOnDevice: number;
17
+ hasAnonymousUser: boolean;
18
+ };
19
+
20
+ class AnonymousAggregateService {
21
+ private async lockFingerprintInit(
22
+ tx: Prisma.TransactionClient,
23
+ fingerprintId: string,
24
+ ): Promise<void> {
25
+ await tx.$executeRaw`
26
+ SELECT pg_advisory_xact_lock(
27
+ ${Prisma.raw(String(ANONYMOUS_INIT_LOCK_NAMESPACE))},
28
+ hashtext(${fingerprintId})
29
+ )
30
+ `;
31
+ }
32
+
33
+ private async findLatestUserContextByFingerprintId(
34
+ fingerprintId: string,
35
+ tx: Prisma.TransactionClient,
36
+ ): Promise<AnonymousInitContext | null> {
37
+ const existingUsers = await userService.findListByFingerprintId(fingerprintId, tx);
38
+ if (existingUsers.length === 0) {
39
+ return null;
40
+ }
41
+
42
+ const latestUser = existingUsers[0];
43
+ const [credit, subscription] = await Promise.all([
44
+ creditService.getCredit(latestUser.userId, tx),
45
+ subscriptionService.getActiveSubscription(latestUser.userId, tx),
46
+ ]);
47
+
48
+ return {
49
+ user: latestUser,
50
+ credit,
51
+ subscription,
52
+ isNewUser: false,
53
+ totalUsersOnDevice: existingUsers.length,
54
+ hasAnonymousUser: true,
55
+ };
56
+ }
57
+
58
+ private async createAnonymousUser(
59
+ fingerprintId: string,
60
+ tx: Prisma.TransactionClient,
61
+ options?: { sourceRef?: Prisma.InputJsonValue; },
62
+ ): Promise<AnonymousInitContext> {
63
+ const newUser = await userService.createUser(
64
+ {
65
+ fingerprintId,
66
+ sourceRef: options?.sourceRef,
67
+ status: UserStatus.ANONYMOUS,
68
+ },
69
+ tx,
70
+ );
71
+
72
+ const credit = await creditService.initializeCreditWithFree(
73
+ {
74
+ userId: newUser.userId,
75
+ feature: 'anonymous_user_init',
76
+ creditType: CreditType.FREE,
77
+ operationType: OperationType.SYS_GIFT,
78
+ operationReferId: newUser.userId,
79
+ creditsChange: freeAmount,
80
+ },
81
+ tx,
82
+ );
83
+
84
+ await subscriptionService.initializeSubscription(newUser.userId, tx);
85
+
86
+ return {
87
+ user: newUser,
88
+ credit,
89
+ subscription: null,
90
+ isNewUser: true,
91
+ totalUsersOnDevice: 1,
92
+ hasAnonymousUser: true,
93
+ };
94
+ }
95
+
96
+ async getOrCreateByFingerprintId(
97
+ fingerprintId: string,
98
+ options?: { sourceRef?: Prisma.InputJsonValue; },
99
+ ): Promise<AnonymousInitContext> {
100
+ return runInTransaction(async (tx) => {
101
+ await this.lockFingerprintInit(tx, fingerprintId);
102
+
103
+ const existingContext = await this.findLatestUserContextByFingerprintId(fingerprintId, tx);
104
+ if (existingContext) {
105
+ return existingContext;
106
+ }
107
+
108
+ return this.createAnonymousUser(fingerprintId, tx, options);
109
+ }, 'anonymous_get_or_create_by_fingerprint_id');
110
+ }
111
+ }
112
+
113
+ export const anonymousAggregateService = new AnonymousAggregateService();
@@ -1,2 +1,3 @@
1
1
  export { userAggregateService } from './user.aggregate.service';
2
2
  export { billingAggregateService } from './billing.aggregate.service';
3
+ export { anonymousAggregateService } from './anonymous.aggregate.service';