diff --git a/backend/services/presence-service/prisma/migrations/20250305000000_add_device_fields_to_event_log/migration.sql b/backend/services/presence-service/prisma/migrations/20250305000000_add_device_fields_to_event_log/migration.sql new file mode 100644 index 00000000..38996c4b --- /dev/null +++ b/backend/services/presence-service/prisma/migrations/20250305000000_add_device_fields_to_event_log/migration.sql @@ -0,0 +1,11 @@ +-- AlterTable: add device context columns to analytics_event_log +ALTER TABLE "analytics_event_log" + ADD COLUMN "device_brand" VARCHAR(64), + ADD COLUMN "device_model" VARCHAR(64), + ADD COLUMN "device_os" VARCHAR(32), + ADD COLUMN "app_version" VARCHAR(32), + ADD COLUMN "locale" VARCHAR(16); + +-- CreateIndex +CREATE INDEX "idx_event_log_device_brand" ON "analytics_event_log"("device_brand"); +CREATE INDEX "idx_event_log_app_version" ON "analytics_event_log"("app_version"); diff --git a/backend/services/presence-service/prisma/schema.prisma b/backend/services/presence-service/prisma/schema.prisma index 58d962b3..1d0a850b 100644 --- a/backend/services/presence-service/prisma/schema.prisma +++ b/backend/services/presence-service/prisma/schema.prisma @@ -13,18 +13,25 @@ datasource db { // 事件日志表 (append-only) model EventLog { - id BigInt @id @default(autoincrement()) - userId String? @map("user_id") @db.VarChar(20) // userSerialNum, e.g. "D25121400005" - installId String @map("install_id") @db.VarChar(64) - eventName String @map("event_name") @db.VarChar(64) - eventTime DateTime @map("event_time") @db.Timestamptz() - properties Json? @db.JsonB - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz() + id BigInt @id @default(autoincrement()) + userId String? @map("user_id") @db.VarChar(20) // userSerialNum, e.g. "D25121400005" + installId String @map("install_id") @db.VarChar(64) + eventName String @map("event_name") @db.VarChar(64) + eventTime DateTime @map("event_time") @db.Timestamptz() + deviceBrand String? @map("device_brand") @db.VarChar(64) + deviceModel String? @map("device_model") @db.VarChar(64) + deviceOs String? @map("device_os") @db.VarChar(32) + appVersion String? @map("app_version") @db.VarChar(32) + locale String? @map("locale") @db.VarChar(16) + properties Json? @db.JsonB + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz() @@index([eventTime], name: "idx_event_log_event_time") @@index([eventName], name: "idx_event_log_event_name") @@index([eventName, eventTime], name: "idx_event_log_event_name_time") @@index([userId], name: "idx_event_log_user_id") + @@index([deviceBrand], name: "idx_event_log_device_brand") + @@index([appVersion], name: "idx_event_log_app_version") @@map("analytics_event_log") } diff --git a/backend/services/presence-service/src/api/dto/request/batch-events.dto.ts b/backend/services/presence-service/src/api/dto/request/batch-events.dto.ts index 981e8b75..50a9b1b6 100644 --- a/backend/services/presence-service/src/api/dto/request/batch-events.dto.ts +++ b/backend/services/presence-service/src/api/dto/request/batch-events.dto.ts @@ -20,7 +20,32 @@ export class EventItemDto { @IsNumber() clientTs: number; - @ApiPropertyOptional({ description: '事件属性' }) + @ApiPropertyOptional({ description: '设备品牌', example: 'Xiaomi' }) + @IsOptional() + @IsString() + deviceBrand?: string; + + @ApiPropertyOptional({ description: '设备型号', example: 'Redmi Note 12' }) + @IsOptional() + @IsString() + deviceModel?: string; + + @ApiPropertyOptional({ description: '操作系统版本', example: '13' }) + @IsOptional() + @IsString() + deviceOs?: string; + + @ApiPropertyOptional({ description: 'App版本号', example: '1.2.0' }) + @IsOptional() + @IsString() + appVersion?: string; + + @ApiPropertyOptional({ description: '语言/地区', example: 'zh_CN' }) + @IsOptional() + @IsString() + locale?: string; + + @ApiPropertyOptional({ description: '事件专属属性' }) @IsOptional() @IsObject() properties?: Record; diff --git a/backend/services/presence-service/src/application/commands/record-events/record-events.command.ts b/backend/services/presence-service/src/application/commands/record-events/record-events.command.ts index 92553b81..c4bf94bd 100644 --- a/backend/services/presence-service/src/application/commands/record-events/record-events.command.ts +++ b/backend/services/presence-service/src/application/commands/record-events/record-events.command.ts @@ -3,6 +3,11 @@ export interface EventItemDto { userId?: string; installId: string; clientTs: number; + deviceBrand?: string; + deviceModel?: string; + deviceOs?: string; + appVersion?: string; + locale?: string; properties?: Record; } diff --git a/backend/services/presence-service/src/application/commands/record-events/record-events.handler.ts b/backend/services/presence-service/src/application/commands/record-events/record-events.handler.ts index dddc3b79..40535001 100644 --- a/backend/services/presence-service/src/application/commands/record-events/record-events.handler.ts +++ b/backend/services/presence-service/src/application/commands/record-events/record-events.handler.ts @@ -72,9 +72,9 @@ export class RecordEventsHandler implements ICommandHandler log.installId.value, log.eventTime, { - appVersion: log.properties.appVersion, - os: log.properties.os, - osVersion: log.properties.osVersion, + appVersion: log.appVersion ?? log.properties.appVersion, + os: log.deviceOs ?? log.properties.os, + osVersion: log.deviceOs ?? log.properties.osVersion, }, ), ); @@ -101,6 +101,11 @@ export class RecordEventsHandler implements ICommandHandler installId: InstallId.fromString(dto.installId), eventName: EventName.fromString(dto.eventName), eventTime: new Date(dto.clientTs * 1000), + deviceBrand: dto.deviceBrand ?? null, + deviceModel: dto.deviceModel ?? null, + deviceOs: dto.deviceOs ?? null, + appVersion: dto.appVersion ?? null, + locale: dto.locale ?? null, properties: EventProperties.fromData(dto.properties ?? {}), }); } diff --git a/backend/services/presence-service/src/domain/entities/event-log.entity.ts b/backend/services/presence-service/src/domain/entities/event-log.entity.ts index dabeb63e..1917a0e7 100644 --- a/backend/services/presence-service/src/domain/entities/event-log.entity.ts +++ b/backend/services/presence-service/src/domain/entities/event-log.entity.ts @@ -8,6 +8,11 @@ export class EventLog { private _installId: InstallId; private _eventName: EventName; private _eventTime: Date; + private _deviceBrand: string | null; + private _deviceModel: string | null; + private _deviceOs: string | null; + private _appVersion: string | null; + private _locale: string | null; private _properties: EventProperties; private _createdAt: Date; @@ -34,6 +39,26 @@ export class EventLog { return this._eventTime; } + get deviceBrand(): string | null { + return this._deviceBrand; + } + + get deviceModel(): string | null { + return this._deviceModel; + } + + get deviceOs(): string | null { + return this._deviceOs; + } + + get appVersion(): string | null { + return this._appVersion; + } + + get locale(): string | null { + return this._locale; + } + get properties(): EventProperties { return this._properties; } @@ -56,6 +81,11 @@ export class EventLog { installId: InstallId; eventName: EventName; eventTime: Date; + deviceBrand?: string | null; + deviceModel?: string | null; + deviceOs?: string | null; + appVersion?: string | null; + locale?: string | null; properties?: EventProperties; }): EventLog { const log = new EventLog(); @@ -64,6 +94,11 @@ export class EventLog { log._installId = props.installId; log._eventName = props.eventName; log._eventTime = props.eventTime; + log._deviceBrand = props.deviceBrand ?? null; + log._deviceModel = props.deviceModel ?? null; + log._deviceOs = props.deviceOs ?? null; + log._appVersion = props.appVersion ?? null; + log._locale = props.locale ?? null; log._properties = props.properties ?? EventProperties.empty(); log._createdAt = new Date(); return log; @@ -76,6 +111,11 @@ export class EventLog { installId: InstallId; eventName: EventName; eventTime: Date; + deviceBrand: string | null; + deviceModel: string | null; + deviceOs: string | null; + appVersion: string | null; + locale: string | null; properties: EventProperties; createdAt: Date; }): EventLog { @@ -85,6 +125,11 @@ export class EventLog { log._installId = props.installId; log._eventName = props.eventName; log._eventTime = props.eventTime; + log._deviceBrand = props.deviceBrand; + log._deviceModel = props.deviceModel; + log._deviceOs = props.deviceOs; + log._appVersion = props.appVersion; + log._locale = props.locale; log._properties = props.properties; log._createdAt = props.createdAt; return log; diff --git a/backend/services/presence-service/src/infrastructure/persistence/mappers/event-log.mapper.ts b/backend/services/presence-service/src/infrastructure/persistence/mappers/event-log.mapper.ts index 2e0a0dda..640a5dc4 100644 --- a/backend/services/presence-service/src/infrastructure/persistence/mappers/event-log.mapper.ts +++ b/backend/services/presence-service/src/infrastructure/persistence/mappers/event-log.mapper.ts @@ -16,6 +16,11 @@ export class EventLogMapper { installId: InstallId.fromString(prisma.installId), eventName: EventName.fromString(prisma.eventName), eventTime: prisma.eventTime, + deviceBrand: prisma.deviceBrand, + deviceModel: prisma.deviceModel, + deviceOs: prisma.deviceOs, + appVersion: prisma.appVersion, + locale: prisma.locale, properties: EventProperties.fromData((prisma.properties as EventPropertiesData) ?? {}), createdAt: prisma.createdAt, }); @@ -27,6 +32,11 @@ export class EventLogMapper { installId: domain.installId.value, eventName: domain.eventName.value, eventTime: domain.eventTime, + deviceBrand: domain.deviceBrand, + deviceModel: domain.deviceModel, + deviceOs: domain.deviceOs, + appVersion: domain.appVersion, + locale: domain.locale, properties: domain.properties.toJSON() as Prisma.InputJsonValue, }; } diff --git a/frontend/mining-app/lib/core/telemetry/models/telemetry_event.dart b/frontend/mining-app/lib/core/telemetry/models/telemetry_event.dart index a2695209..bb280eda 100644 --- a/frontend/mining-app/lib/core/telemetry/models/telemetry_event.dart +++ b/frontend/mining-app/lib/core/telemetry/models/telemetry_event.dart @@ -118,15 +118,31 @@ class TelemetryEvent extends Equatable { }; } - /// 转换为服务端 API 格式 + /// 转换为服务端 API 格式(Amplitude 风格:设备字段为顶层独立列,方便服务端索引) + /// 顶层字段: eventName, userId, installId, clientTs, deviceBrand, deviceModel, + /// deviceOs, appVersion, locale + /// properties: 仅保留事件专属数据(页面名、金额等) Map toServerJson() { + // 从 properties 中提取设备字段(提升为顶层),剩余为事件专属数据 + final props = Map.from(properties ?? {}); + final deviceBrand = props.remove('device_brand'); + final deviceModel = props.remove('device_model'); + final deviceOs = props.remove('device_os'); + final appVersion = props.remove('app_version'); + final locale = props.remove('locale'); + return { 'eventName': name, 'userId': userId, 'installId': installId, 'clientTs': timestamp.millisecondsSinceEpoch ~/ 1000, + 'deviceBrand': deviceBrand, + 'deviceModel': deviceModel, + 'deviceOs': deviceOs, + 'appVersion': appVersion, + 'locale': locale, 'properties': { - ...?properties, + ...props, 'eventId': eventId, 'type': type.name, 'level': level.name, diff --git a/frontend/mobile-app/lib/core/telemetry/models/telemetry_event.dart b/frontend/mobile-app/lib/core/telemetry/models/telemetry_event.dart index ca83a878..e7bd1bcb 100644 --- a/frontend/mobile-app/lib/core/telemetry/models/telemetry_event.dart +++ b/frontend/mobile-app/lib/core/telemetry/models/telemetry_event.dart @@ -118,21 +118,31 @@ class TelemetryEvent extends Equatable { }; } - /// 转换为服务端 API 格式 - /// 后端 presence-service 期望的格式: - /// - eventName: string (required) - /// - userId: string (optional) - userSerialNum, e.g. "D25121400005" - /// - installId: string (required) - /// - clientTs: number (required) - Unix timestamp in seconds - /// - properties: object (optional) + /// 转换为服务端 API 格式(Amplitude 风格:设备字段为顶层独立列,方便服务端索引) + /// 顶层字段: eventName, userId, installId, clientTs, deviceBrand, deviceModel, + /// deviceOs, appVersion, locale + /// properties: 仅保留事件专属数据(页面名、金额等) Map toServerJson() { + // 从 properties 中提取设备字段(提升为顶层),剩余为事件专属数据 + final props = Map.from(properties ?? {}); + final deviceBrand = props.remove('device_brand'); + final deviceModel = props.remove('device_model'); + final deviceOs = props.remove('device_os'); + final appVersion = props.remove('app_version'); + final locale = props.remove('locale'); + return { 'eventName': name, 'userId': userId, 'installId': installId, 'clientTs': timestamp.millisecondsSinceEpoch ~/ 1000, + 'deviceBrand': deviceBrand, + 'deviceModel': deviceModel, + 'deviceOs': deviceOs, + 'appVersion': appVersion, + 'locale': locale, 'properties': { - ...?properties, + ...props, 'eventId': eventId, 'type': type.name, 'level': level.name,