From fbec0b9112a3f41d6b44af7155123eb3fc911061 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 7 Dec 2025 11:08:37 -0800 Subject: [PATCH] feat(identity): store complete deviceInfo JSON from frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add deviceInfo JSON field to UserDevice table (Prisma schema) - Update DeviceInfo value object to use deviceInfo instead of HardwareInfo - Update repository to save complete JSON with redundant fields for queries - Update mapper to read deviceInfo from database - Update aggregate and handlers to pass deviceInfo through - Allow any fields in DeviceNameInput interface with index signature 100% preserve original device info JSON from frontend without extraction 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .claude/settings.local.json | 10 +- .gitignore | 1 + backend/services/docker-compose.infra.yml | 103 ++++++++++++++++++ .../.claude/settings.local.json | 4 +- .../identity-service/prisma/schema.prisma | 5 +- .../auto-create-account.handler.ts | 14 +-- .../src/application/commands/index.ts | 2 + .../services/user-application.service.ts | 14 +-- .../user-account/user-account.aggregate.ts | 18 +-- .../domain/value-objects/device-info.vo.ts | 1 + .../src/domain/value-objects/index.ts | 37 +++---- .../entities/user-account.entity.ts | 3 +- .../entities/user-device.entity.ts | 8 -- .../mappers/user-account.mapper.ts | 15 +-- .../user-account.repository.impl.ts | 50 ++++----- backend/services/init-multiple-dbs.sh | 26 +++++ 16 files changed, 207 insertions(+), 104 deletions(-) create mode 100644 backend/services/docker-compose.infra.yml delete mode 100644 backend/services/identity-service/src/infrastructure/persistence/entities/user-device.entity.ts create mode 100644 backend/services/init-multiple-dbs.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 56b32008..a3ef332a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -22,7 +22,15 @@ "Bash(git commit -m \"$(cat <<''EOF''\nfeat(admin-service): 增强移动端版本上传功能\n\n- 添加 APK/IPA 文件解析器自动提取版本信息\n- 支持从安装包自动读取 versionName 和 versionCode\n- 添加 adbkit-apkreader 依赖解析 APK 文件\n- 添加 plist 依赖解析 IPA 文件\n- 优化上传接口支持自动填充版本信息\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \nEOF\n)\")", "Bash(git commit:*)", "Bash(grep:*)", - "Bash(ls:*)" + "Bash(ls:*)", + "Bash(wsl -e bash -c:*)", + "Bash(git push:*)", + "Bash(git pull:*)", + "Bash(git stash:*)", + "Bash(cd:*)", + "Bash(curl:*)", + "Bash(git revert:*)", + "Bash(del \"c:\\Users\\dong\\Desktop\\rwadurian\\backend\\services\\identity-service\\src\\infrastructure\\persistence\\entities\\user-device.entity.ts\")" ], "deny": [], "ask": [] diff --git a/.gitignore b/.gitignore index e69de29b..3d9f9207 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +nul diff --git a/backend/services/docker-compose.infra.yml b/backend/services/docker-compose.infra.yml new file mode 100644 index 00000000..2d9511fc --- /dev/null +++ b/backend/services/docker-compose.infra.yml @@ -0,0 +1,103 @@ +# ============================================================================= +# RWA Platform - Shared Infrastructure +# ============================================================================= +# This file defines shared infrastructure services (PostgreSQL, Redis, Kafka) +# that are used by all microservices. +# +# Usage: +# docker compose -f docker-compose.infra.yml up -d +# ============================================================================= + +services: + # PostgreSQL - Shared database server (each service uses different database) + postgres: + image: postgres:16-alpine + container_name: rwa-postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + # Create multiple databases on startup + POSTGRES_MULTIPLE_DATABASES: rwa_identity,rwa_mpc,rwa_blockchain + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init-multiple-dbs.sh:/docker-entrypoint-initdb.d/init-multiple-dbs.sh:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + networks: + - rwa-network + + # Redis - Shared cache/session store + redis: + image: redis:7-alpine + container_name: rwa-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + networks: + - rwa-network + + # Zookeeper - Required by Kafka + zookeeper: + image: confluentinc/cp-zookeeper:7.5.0 + container_name: rwa-zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + healthcheck: + test: ["CMD", "nc", "-z", "localhost", "2181"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + networks: + - rwa-network + + # Kafka - Event streaming platform + kafka: + image: confluentinc/cp-kafka:7.5.0 + container_name: rwa-kafka + depends_on: + zookeeper: + condition: service_healthy + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://kafka:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,PLAINTEXT_INTERNAL://0.0.0.0:29092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT_INTERNAL + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + healthcheck: + test: ["CMD", "kafka-topics", "--bootstrap-server", "localhost:9092", "--list"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + restart: unless-stopped + networks: + - rwa-network + +networks: + rwa-network: + name: rwa-network + driver: bridge + +volumes: + postgres_data: + redis_data: diff --git a/backend/services/identity-service/.claude/settings.local.json b/backend/services/identity-service/.claude/settings.local.json index 95fdc30b..2ef89833 100644 --- a/backend/services/identity-service/.claude/settings.local.json +++ b/backend/services/identity-service/.claude/settings.local.json @@ -36,7 +36,9 @@ "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" show cf308ef --stat)", "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nrefactor: move mnemonic verification from identity-service to blockchain-service\n\n- Add /internal/verify-mnemonic API to blockchain-service\n- Add /internal/derive-from-mnemonic API to blockchain-service \n- Create MnemonicDerivationAdapter for BIP39 mnemonic address derivation\n- Create BlockchainClientService in identity-service to call blockchain-service\n- Remove WalletGeneratorService from identity-service\n- Update recover-by-mnemonic handler to use blockchain-service API\n\nThis enforces proper domain boundaries - all blockchain/crypto operations\nare now handled by blockchain-service.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n)\")", "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"fix(identity-service): remove WalletGeneratorService from app.module.ts\")", - "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"fix(blockchain-service): add @scure/bip39 dependency\")" + "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"fix(blockchain-service): add @scure/bip39 dependency\")", + "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" add backend/services/blockchain-service/src/main.ts backend/services/blockchain-service/Dockerfile backend/services/docker-compose.yml)", + "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nfix(blockchain-service): add global API prefix and increase healthcheck start_period\n\n- Add app.setGlobalPrefix(''api/v1'') to main.ts so health endpoint\n is at /api/v1/health consistent with other services\n- Increase healthcheck start_period to 60s to allow time for\n Prisma migrations on first startup\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n)\")" ], "deny": [], "ask": [] diff --git a/backend/services/identity-service/prisma/schema.prisma b/backend/services/identity-service/prisma/schema.prisma index 6ad31b6b..4089e066 100644 --- a/backend/services/identity-service/prisma/schema.prisma +++ b/backend/services/identity-service/prisma/schema.prisma @@ -54,8 +54,9 @@ model UserDevice { deviceId String @map("device_id") @db.VarChar(100) deviceName String? @map("device_name") @db.VarChar(100) - // Hardware Info - 设备硬件信息 - platform String? @db.VarChar(20) // ios, android, web + // Hardware Info - 设备硬件信息 (JSON 存储完整前端传递的设备信息) + deviceInfo Json? @map("device_info") // 完整的设备信息 JSON + platform String? @db.VarChar(20) // ios, android, web (冗余字段,便于查询) deviceModel String? @map("device_model") @db.VarChar(100) // iPhone 15 Pro, Pixel 8 osVersion String? @map("os_version") @db.VarChar(50) // iOS 17.2, Android 14 appVersion String? @map("app_version") @db.VarChar(20) // 1.0.0 diff --git a/backend/services/identity-service/src/application/commands/auto-create-account/auto-create-account.handler.ts b/backend/services/identity-service/src/application/commands/auto-create-account/auto-create-account.handler.ts index 3328173d..e158f578 100644 --- a/backend/services/identity-service/src/application/commands/auto-create-account/auto-create-account.handler.ts +++ b/backend/services/identity-service/src/application/commands/auto-create-account/auto-create-account.handler.ts @@ -3,7 +3,7 @@ import { AutoCreateAccountCommand } from './auto-create-account.command'; import { UserAccountRepository, USER_ACCOUNT_REPOSITORY } from '@/domain/repositories/user-account.repository.interface'; import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate'; import { AccountSequenceGeneratorService, UserValidatorService } from '@/domain/services'; -import { ReferralCode, AccountSequence, ProvinceCode, CityCode, HardwareInfo } from '@/domain/value-objects'; +import { ReferralCode, AccountSequence, ProvinceCode, CityCode } from '@/domain/value-objects'; import { TokenService } from '@/application/services/token.service'; import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; import { ApplicationError } from '@/shared/exceptions/domain.exception'; @@ -46,28 +46,22 @@ export class AutoCreateAccountHandler { // 4. 生成随机用户名和头像 const identity = generateRandomIdentity(); - // 5. 构建设备名称和硬件信息 + // 5. 构建设备名称,保存完整的设备信息 JSON let deviceNameStr = '未命名设备'; - let hardwareInfo: HardwareInfo | undefined; if (command.deviceName) { const parts: string[] = []; if (command.deviceName.model) parts.push(command.deviceName.model); if (command.deviceName.platform) parts.push(command.deviceName.platform); if (command.deviceName.osVersion) parts.push(command.deviceName.osVersion); if (parts.length > 0) deviceNameStr = parts.join(' '); - hardwareInfo = { - platform: command.deviceName.platform, - deviceModel: command.deviceName.model, - osVersion: command.deviceName.osVersion, - }; } - // 6. 创建账户 + // 6. 创建账户 - 传递完整的 deviceName JSON const account = UserAccount.createAutomatic({ accountSequence, initialDeviceId: command.deviceId, deviceName: deviceNameStr, - hardwareInfo, + deviceInfo: command.deviceName, // 100% 保持原样存储 inviterSequence, province: ProvinceCode.create('DEFAULT'), city: CityCode.create('DEFAULT'), diff --git a/backend/services/identity-service/src/application/commands/index.ts b/backend/services/identity-service/src/application/commands/index.ts index 09c85c28..ab3f80f8 100644 --- a/backend/services/identity-service/src/application/commands/index.ts +++ b/backend/services/identity-service/src/application/commands/index.ts @@ -1,8 +1,10 @@ // ============ Types ============ +// 设备信息输入 - 100% 保持前端传递的原样存储 export interface DeviceNameInput { model?: string; // iPhone 15 Pro, Pixel 8 platform?: string; // ios, android, web osVersion?: string; // iOS 17.2, Android 14 + [key: string]: unknown; // 允许任意其他字段 } // ============ Commands ============ diff --git a/backend/services/identity-service/src/application/services/user-application.service.ts b/backend/services/identity-service/src/application/services/user-application.service.ts index 9d2b1d90..3d713431 100644 --- a/backend/services/identity-service/src/application/services/user-application.service.ts +++ b/backend/services/identity-service/src/application/services/user-application.service.ts @@ -8,7 +8,7 @@ import { } from '@/domain/services'; import { UserId, PhoneNumber, ReferralCode, AccountSequence, ProvinceCode, CityCode, - ChainType, KYCInfo, HardwareInfo, + ChainType, KYCInfo, } from '@/domain/value-objects'; import { TokenService } from './token.service'; import { RedisService } from '@/infrastructure/redis/redis.service'; @@ -89,28 +89,22 @@ export class UserApplicationService { // 4. 生成随机用户名和头像 const identity = generateRandomIdentity(); - // 5. 构建设备名称字符串和硬件信息 + // 5. 构建设备名称字符串 let deviceNameStr = '未命名设备'; - let hardwareInfo: HardwareInfo | undefined; if (command.deviceName) { const parts: string[] = []; if (command.deviceName.model) parts.push(command.deviceName.model); if (command.deviceName.platform) parts.push(command.deviceName.platform); if (command.deviceName.osVersion) parts.push(command.deviceName.osVersion); if (parts.length > 0) deviceNameStr = parts.join(' '); - hardwareInfo = { - platform: command.deviceName.platform, - deviceModel: command.deviceName.model, - osVersion: command.deviceName.osVersion, - }; } - // 6. 创建用户账户 + // 6. 创建用户账户 - deviceInfo 100% 保持前端传递的原样 const account = UserAccount.createAutomatic({ accountSequence, initialDeviceId: command.deviceId, deviceName: deviceNameStr, - hardwareInfo, + deviceInfo: command.deviceName, // 100% 保持原样存储 inviterSequence, province: ProvinceCode.create('DEFAULT'), city: CityCode.create('DEFAULT'), diff --git a/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts b/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts index ecf9b080..5f2abb43 100644 --- a/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts +++ b/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts @@ -1,7 +1,7 @@ import { DomainError } from '@/shared/exceptions/domain.exception'; import { UserId, AccountSequence, PhoneNumber, ReferralCode, ProvinceCode, CityCode, - DeviceInfo, HardwareInfo, ChainType, KYCInfo, KYCStatus, AccountStatus, + DeviceInfo, ChainType, KYCInfo, KYCStatus, AccountStatus, } from '@/domain/value-objects'; import { WalletAddress } from '@/domain/entities/wallet-address.entity'; import { @@ -87,7 +87,7 @@ export class UserAccount { accountSequence: AccountSequence; initialDeviceId: string; deviceName?: string; - hardwareInfo?: HardwareInfo; + deviceInfo?: Record; // 完整的设备信息 JSON inviterSequence: AccountSequence | null; province: ProvinceCode; city: CityCode; @@ -97,7 +97,7 @@ export class UserAccount { const devices = new Map(); devices.set(params.initialDeviceId, new DeviceInfo( params.initialDeviceId, params.deviceName || '未命名设备', new Date(), new Date(), - params.hardwareInfo, + params.deviceInfo, // 传递完整的 JSON )); // UserID将由数据库自动生成(autoincrement),这里使用临时值0 @@ -130,7 +130,7 @@ export class UserAccount { phoneNumber: PhoneNumber; initialDeviceId: string; deviceName?: string; - hardwareInfo?: HardwareInfo; + deviceInfo?: Record; // 完整的设备信息 JSON inviterSequence: AccountSequence | null; province: ProvinceCode; city: CityCode; @@ -138,7 +138,7 @@ export class UserAccount { const devices = new Map(); devices.set(params.initialDeviceId, new DeviceInfo( params.initialDeviceId, params.deviceName || '未命名设备', new Date(), new Date(), - params.hardwareInfo, + params.deviceInfo, )); // UserID将由数据库自动生成(autoincrement),这里使用临时值0 @@ -201,7 +201,7 @@ export class UserAccount { ); } - addDevice(deviceId: string, deviceName?: string, hardwareInfo?: HardwareInfo): void { + addDevice(deviceId: string, deviceName?: string, deviceInfo?: Record): void { this.ensureActive(); if (this._devices.size >= 5 && !this._devices.has(deviceId)) { throw new DomainError('最多允许5个设备同时登录'); @@ -209,12 +209,12 @@ export class UserAccount { if (this._devices.has(deviceId)) { const device = this._devices.get(deviceId)!; device.updateActivity(); - if (hardwareInfo) { - device.updateHardwareInfo(hardwareInfo); + if (deviceInfo) { + device.updateDeviceInfo(deviceInfo); } } else { this._devices.set(deviceId, new DeviceInfo( - deviceId, deviceName || '未命名设备', new Date(), new Date(), hardwareInfo, + deviceId, deviceName || '未命名设备', new Date(), new Date(), deviceInfo, )); this.addDomainEvent(new DeviceAddedEvent({ userId: this.userId.toString(), diff --git a/backend/services/identity-service/src/domain/value-objects/device-info.vo.ts b/backend/services/identity-service/src/domain/value-objects/device-info.vo.ts index 9ce4b236..691f39dd 100644 --- a/backend/services/identity-service/src/domain/value-objects/device-info.vo.ts +++ b/backend/services/identity-service/src/domain/value-objects/device-info.vo.ts @@ -6,6 +6,7 @@ export class DeviceInfo { public readonly deviceName: string, public readonly addedAt: Date, lastActiveAt: Date, + public readonly deviceInfo?: Record, // 完整的设备信息 JSON ) { this._lastActiveAt = lastActiveAt; } diff --git a/backend/services/identity-service/src/domain/value-objects/index.ts b/backend/services/identity-service/src/domain/value-objects/index.ts index 2d122a5f..44b57f81 100644 --- a/backend/services/identity-service/src/domain/value-objects/index.ts +++ b/backend/services/identity-service/src/domain/value-objects/index.ts @@ -144,64 +144,55 @@ export class Mnemonic { } } -// ============ HardwareInfo ============ -export interface HardwareInfo { - platform?: string; // ios, android, web - deviceModel?: string; // iPhone 15 Pro, Pixel 8 - osVersion?: string; // iOS 17.2, Android 14 - appVersion?: string; // 1.0.0 - screenWidth?: number; - screenHeight?: number; - locale?: string; // zh-CN, en-US - timezone?: string; // Asia/Shanghai -} - // ============ DeviceInfo ============ +// deviceInfo: 完整的设备信息 JSON,100% 保持前端传递的原样 export class DeviceInfo { private _lastActiveAt: Date; - private _hardwareInfo: HardwareInfo; + private _deviceInfo: Record; constructor( public readonly deviceId: string, public readonly deviceName: string, public readonly addedAt: Date, lastActiveAt: Date, - hardwareInfo?: HardwareInfo, + deviceInfo?: Record, ) { this._lastActiveAt = lastActiveAt; - this._hardwareInfo = hardwareInfo || {}; + this._deviceInfo = deviceInfo || {}; } get lastActiveAt(): Date { return this._lastActiveAt; } - get hardwareInfo(): HardwareInfo { - return this._hardwareInfo; + // 100% 保持原样的完整设备信息 JSON + get deviceInfo(): Record { + return this._deviceInfo; } + // 便捷访问器 get platform(): string | undefined { - return this._hardwareInfo.platform; + return this._deviceInfo.platform as string | undefined; } get deviceModel(): string | undefined { - return this._hardwareInfo.deviceModel; + return (this._deviceInfo.model || this._deviceInfo.deviceModel) as string | undefined; } get osVersion(): string | undefined { - return this._hardwareInfo.osVersion; + return this._deviceInfo.osVersion as string | undefined; } get appVersion(): string | undefined { - return this._hardwareInfo.appVersion; + return this._deviceInfo.appVersion as string | undefined; } updateActivity(): void { this._lastActiveAt = new Date(); } - updateHardwareInfo(info: HardwareInfo): void { - this._hardwareInfo = { ...this._hardwareInfo, ...info }; + updateDeviceInfo(info: Record): void { + this._deviceInfo = { ...this._deviceInfo, ...info }; } } diff --git a/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts b/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts index aebb6642..c0dd5f48 100644 --- a/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts +++ b/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts @@ -29,7 +29,8 @@ export interface UserDeviceEntity { userId: bigint; deviceId: string; deviceName: string | null; - // Hardware Info + deviceInfo: Record | null; // 完整的设备信息 JSON + // Hardware Info (冗余字段,便于查询) platform: string | null; deviceModel: string | null; osVersion: string | null; diff --git a/backend/services/identity-service/src/infrastructure/persistence/entities/user-device.entity.ts b/backend/services/identity-service/src/infrastructure/persistence/entities/user-device.entity.ts deleted file mode 100644 index 912d73a7..00000000 --- a/backend/services/identity-service/src/infrastructure/persistence/entities/user-device.entity.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface UserDeviceEntity { - id: bigint; - userId: bigint; - deviceId: string; - deviceName: string | null; - addedAt: Date; - lastActiveAt: Date; -} diff --git a/backend/services/identity-service/src/infrastructure/persistence/mappers/user-account.mapper.ts b/backend/services/identity-service/src/infrastructure/persistence/mappers/user-account.mapper.ts index 00247a0f..648c1d55 100644 --- a/backend/services/identity-service/src/infrastructure/persistence/mappers/user-account.mapper.ts +++ b/backend/services/identity-service/src/infrastructure/persistence/mappers/user-account.mapper.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate'; import { WalletAddress } from '@/domain/entities/wallet-address.entity'; -import { DeviceInfo, HardwareInfo, KYCInfo, KYCStatus, AccountStatus, ChainType, AddressStatus } from '@/domain/value-objects'; +import { DeviceInfo, KYCInfo, KYCStatus, AccountStatus, ChainType, AddressStatus } from '@/domain/value-objects'; import { UserAccountEntity } from '../entities/user-account.entity'; import { toMpcSignatureString } from '../entities/wallet-address.entity'; @@ -9,22 +9,13 @@ import { toMpcSignatureString } from '../entities/wallet-address.entity'; export class UserAccountMapper { toDomain(entity: UserAccountEntity): UserAccount { const devices = (entity.devices || []).map((d) => { - const hardwareInfo: HardwareInfo = { - platform: d.platform || undefined, - deviceModel: d.deviceModel || undefined, - osVersion: d.osVersion || undefined, - appVersion: d.appVersion || undefined, - screenWidth: d.screenWidth || undefined, - screenHeight: d.screenHeight || undefined, - locale: d.locale || undefined, - timezone: d.timezone || undefined, - }; + // 直接使用完整的 deviceInfo JSON,100% 保持原样 return new DeviceInfo( d.deviceId, d.deviceName || '未命名设备', d.addedAt, d.lastActiveAt, - hardwareInfo, + d.deviceInfo || undefined, ); }); diff --git a/backend/services/identity-service/src/infrastructure/persistence/repositories/user-account.repository.impl.ts b/backend/services/identity-service/src/infrastructure/persistence/repositories/user-account.repository.impl.ts index ba4b685f..a3bdae07 100644 --- a/backend/services/identity-service/src/infrastructure/persistence/repositories/user-account.repository.impl.ts +++ b/backend/services/identity-service/src/infrastructure/persistence/repositories/user-account.repository.impl.ts @@ -7,7 +7,7 @@ import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggre import { WalletAddress } from '@/domain/entities/wallet-address.entity'; import { UserId, AccountSequence, PhoneNumber, ReferralCode, ChainType, - AccountStatus, KYCStatus, DeviceInfo, HardwareInfo, KYCInfo, AddressStatus, + AccountStatus, KYCStatus, DeviceInfo, KYCInfo, AddressStatus, } from '@/domain/value-objects'; import { toMpcSignatureString, fromMpcSignatureString } from '../entities/wallet-address.entity'; @@ -75,21 +75,26 @@ export class UserAccountRepositoryImpl implements UserAccountRepository { await tx.userDevice.deleteMany({ where: { userId: savedUserId } }); if (devices.length > 0) { await tx.userDevice.createMany({ - data: devices.map((d) => ({ - userId: savedUserId, - deviceId: d.deviceId, - deviceName: d.deviceName, - platform: d.hardwareInfo.platform || null, - deviceModel: d.hardwareInfo.deviceModel || null, - osVersion: d.hardwareInfo.osVersion || null, - appVersion: d.hardwareInfo.appVersion || null, - screenWidth: d.hardwareInfo.screenWidth || null, - screenHeight: d.hardwareInfo.screenHeight || null, - locale: d.hardwareInfo.locale || null, - timezone: d.hardwareInfo.timezone || null, - addedAt: d.addedAt, - lastActiveAt: d.lastActiveAt, - })), + data: devices.map((d) => { + // 从 deviceInfo JSON 中提取冗余字段便于查询 + const info = d.deviceInfo || {}; + return { + userId: savedUserId, + deviceId: d.deviceId, + deviceName: d.deviceName, + deviceInfo: d.deviceInfo || null, // 100% 保存完整 JSON + platform: (info as any).platform || null, + deviceModel: (info as any).model || null, + osVersion: (info as any).osVersion || null, + appVersion: (info as any).appVersion || null, + screenWidth: (info as any).screenWidth || null, + screenHeight: (info as any).screenHeight || null, + locale: (info as any).locale || null, + timezone: (info as any).timezone || null, + addedAt: d.addedAt, + lastActiveAt: d.lastActiveAt, + }; + }), }); } }); @@ -214,22 +219,13 @@ export class UserAccountRepositoryImpl implements UserAccountRepository { private toDomain(data: any): UserAccount { const devices = data.devices.map((d: any) => { - const hardwareInfo: HardwareInfo = { - platform: d.platform || undefined, - deviceModel: d.deviceModel || undefined, - osVersion: d.osVersion || undefined, - appVersion: d.appVersion || undefined, - screenWidth: d.screenWidth || undefined, - screenHeight: d.screenHeight || undefined, - locale: d.locale || undefined, - timezone: d.timezone || undefined, - }; + // 优先使用完整的 deviceInfo JSON,保持原样 return new DeviceInfo( d.deviceId, d.deviceName || '未命名设备', d.addedAt, d.lastActiveAt, - hardwareInfo, + d.deviceInfo || undefined, // 100% 保持原样 ); }); diff --git a/backend/services/init-multiple-dbs.sh b/backend/services/init-multiple-dbs.sh new file mode 100644 index 00000000..7b383f2d --- /dev/null +++ b/backend/services/init-multiple-dbs.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# ============================================================================= +# PostgreSQL - Initialize Multiple Databases +# ============================================================================= +# This script creates multiple databases from POSTGRES_MULTIPLE_DATABASES env var +# ============================================================================= + +set -e +set -u + +function create_database() { + local database=$1 + echo "Creating database '$database'" + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + SELECT 'CREATE DATABASE $database' + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$database')\gexec +EOSQL +} + +if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then + echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" + for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do + create_database $db + done + echo "Multiple databases created" +fi