feat(identity): store complete deviceInfo JSON from frontend
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
592f13e939
commit
fbec0b9112
|
|
@ -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 <noreply@anthropic.com>\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": []
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
nul
|
||||
|
|
@ -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:
|
||||
|
|
@ -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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\nEOF\n)\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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 ============
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>; // 完整的设备信息 JSON
|
||||
inviterSequence: AccountSequence | null;
|
||||
province: ProvinceCode;
|
||||
city: CityCode;
|
||||
|
|
@ -97,7 +97,7 @@ export class UserAccount {
|
|||
const devices = new Map<string, DeviceInfo>();
|
||||
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<string, unknown>; // 完整的设备信息 JSON
|
||||
inviterSequence: AccountSequence | null;
|
||||
province: ProvinceCode;
|
||||
city: CityCode;
|
||||
|
|
@ -138,7 +138,7 @@ export class UserAccount {
|
|||
const devices = new Map<string, DeviceInfo>();
|
||||
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<string, unknown>): 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(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export class DeviceInfo {
|
|||
public readonly deviceName: string,
|
||||
public readonly addedAt: Date,
|
||||
lastActiveAt: Date,
|
||||
public readonly deviceInfo?: Record<string, unknown>, // 完整的设备信息 JSON
|
||||
) {
|
||||
this._lastActiveAt = lastActiveAt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
|
||||
constructor(
|
||||
public readonly deviceId: string,
|
||||
public readonly deviceName: string,
|
||||
public readonly addedAt: Date,
|
||||
lastActiveAt: Date,
|
||||
hardwareInfo?: HardwareInfo,
|
||||
deviceInfo?: Record<string, unknown>,
|
||||
) {
|
||||
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<string, unknown> {
|
||||
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<string, unknown>): void {
|
||||
this._deviceInfo = { ...this._deviceInfo, ...info };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ export interface UserDeviceEntity {
|
|||
userId: bigint;
|
||||
deviceId: string;
|
||||
deviceName: string | null;
|
||||
// Hardware Info
|
||||
deviceInfo: Record<string, unknown> | null; // 完整的设备信息 JSON
|
||||
// Hardware Info (冗余字段,便于查询)
|
||||
platform: string | null;
|
||||
deviceModel: string | null;
|
||||
osVersion: string | null;
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
export interface UserDeviceEntity {
|
||||
id: bigint;
|
||||
userId: bigint;
|
||||
deviceId: string;
|
||||
deviceName: string | null;
|
||||
addedAt: Date;
|
||||
lastActiveAt: Date;
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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% 保持原样
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue