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 -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(git commit:*)",
|
||||||
"Bash(grep:*)",
|
"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": [],
|
"deny": [],
|
||||||
"ask": []
|
"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\" 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 \"$(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(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": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,9 @@ model UserDevice {
|
||||||
deviceId String @map("device_id") @db.VarChar(100)
|
deviceId String @map("device_id") @db.VarChar(100)
|
||||||
deviceName String? @map("device_name") @db.VarChar(100)
|
deviceName String? @map("device_name") @db.VarChar(100)
|
||||||
|
|
||||||
// Hardware Info - 设备硬件信息
|
// Hardware Info - 设备硬件信息 (JSON 存储完整前端传递的设备信息)
|
||||||
platform String? @db.VarChar(20) // ios, android, web
|
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
|
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
|
osVersion String? @map("os_version") @db.VarChar(50) // iOS 17.2, Android 14
|
||||||
appVersion String? @map("app_version") @db.VarChar(20) // 1.0.0
|
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 { UserAccountRepository, USER_ACCOUNT_REPOSITORY } from '@/domain/repositories/user-account.repository.interface';
|
||||||
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
|
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
|
||||||
import { AccountSequenceGeneratorService, UserValidatorService } from '@/domain/services';
|
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 { TokenService } from '@/application/services/token.service';
|
||||||
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
|
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
|
||||||
import { ApplicationError } from '@/shared/exceptions/domain.exception';
|
import { ApplicationError } from '@/shared/exceptions/domain.exception';
|
||||||
|
|
@ -46,28 +46,22 @@ export class AutoCreateAccountHandler {
|
||||||
// 4. 生成随机用户名和头像
|
// 4. 生成随机用户名和头像
|
||||||
const identity = generateRandomIdentity();
|
const identity = generateRandomIdentity();
|
||||||
|
|
||||||
// 5. 构建设备名称和硬件信息
|
// 5. 构建设备名称,保存完整的设备信息 JSON
|
||||||
let deviceNameStr = '未命名设备';
|
let deviceNameStr = '未命名设备';
|
||||||
let hardwareInfo: HardwareInfo | undefined;
|
|
||||||
if (command.deviceName) {
|
if (command.deviceName) {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (command.deviceName.model) parts.push(command.deviceName.model);
|
if (command.deviceName.model) parts.push(command.deviceName.model);
|
||||||
if (command.deviceName.platform) parts.push(command.deviceName.platform);
|
if (command.deviceName.platform) parts.push(command.deviceName.platform);
|
||||||
if (command.deviceName.osVersion) parts.push(command.deviceName.osVersion);
|
if (command.deviceName.osVersion) parts.push(command.deviceName.osVersion);
|
||||||
if (parts.length > 0) deviceNameStr = parts.join(' ');
|
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({
|
const account = UserAccount.createAutomatic({
|
||||||
accountSequence,
|
accountSequence,
|
||||||
initialDeviceId: command.deviceId,
|
initialDeviceId: command.deviceId,
|
||||||
deviceName: deviceNameStr,
|
deviceName: deviceNameStr,
|
||||||
hardwareInfo,
|
deviceInfo: command.deviceName, // 100% 保持原样存储
|
||||||
inviterSequence,
|
inviterSequence,
|
||||||
province: ProvinceCode.create('DEFAULT'),
|
province: ProvinceCode.create('DEFAULT'),
|
||||||
city: CityCode.create('DEFAULT'),
|
city: CityCode.create('DEFAULT'),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
// ============ Types ============
|
// ============ Types ============
|
||||||
|
// 设备信息输入 - 100% 保持前端传递的原样存储
|
||||||
export interface DeviceNameInput {
|
export interface DeviceNameInput {
|
||||||
model?: string; // iPhone 15 Pro, Pixel 8
|
model?: string; // iPhone 15 Pro, Pixel 8
|
||||||
platform?: string; // ios, android, web
|
platform?: string; // ios, android, web
|
||||||
osVersion?: string; // iOS 17.2, Android 14
|
osVersion?: string; // iOS 17.2, Android 14
|
||||||
|
[key: string]: unknown; // 允许任意其他字段
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Commands ============
|
// ============ Commands ============
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from '@/domain/services';
|
} from '@/domain/services';
|
||||||
import {
|
import {
|
||||||
UserId, PhoneNumber, ReferralCode, AccountSequence, ProvinceCode, CityCode,
|
UserId, PhoneNumber, ReferralCode, AccountSequence, ProvinceCode, CityCode,
|
||||||
ChainType, KYCInfo, HardwareInfo,
|
ChainType, KYCInfo,
|
||||||
} from '@/domain/value-objects';
|
} from '@/domain/value-objects';
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
import { RedisService } from '@/infrastructure/redis/redis.service';
|
import { RedisService } from '@/infrastructure/redis/redis.service';
|
||||||
|
|
@ -89,28 +89,22 @@ export class UserApplicationService {
|
||||||
// 4. 生成随机用户名和头像
|
// 4. 生成随机用户名和头像
|
||||||
const identity = generateRandomIdentity();
|
const identity = generateRandomIdentity();
|
||||||
|
|
||||||
// 5. 构建设备名称字符串和硬件信息
|
// 5. 构建设备名称字符串
|
||||||
let deviceNameStr = '未命名设备';
|
let deviceNameStr = '未命名设备';
|
||||||
let hardwareInfo: HardwareInfo | undefined;
|
|
||||||
if (command.deviceName) {
|
if (command.deviceName) {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (command.deviceName.model) parts.push(command.deviceName.model);
|
if (command.deviceName.model) parts.push(command.deviceName.model);
|
||||||
if (command.deviceName.platform) parts.push(command.deviceName.platform);
|
if (command.deviceName.platform) parts.push(command.deviceName.platform);
|
||||||
if (command.deviceName.osVersion) parts.push(command.deviceName.osVersion);
|
if (command.deviceName.osVersion) parts.push(command.deviceName.osVersion);
|
||||||
if (parts.length > 0) deviceNameStr = parts.join(' ');
|
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({
|
const account = UserAccount.createAutomatic({
|
||||||
accountSequence,
|
accountSequence,
|
||||||
initialDeviceId: command.deviceId,
|
initialDeviceId: command.deviceId,
|
||||||
deviceName: deviceNameStr,
|
deviceName: deviceNameStr,
|
||||||
hardwareInfo,
|
deviceInfo: command.deviceName, // 100% 保持原样存储
|
||||||
inviterSequence,
|
inviterSequence,
|
||||||
province: ProvinceCode.create('DEFAULT'),
|
province: ProvinceCode.create('DEFAULT'),
|
||||||
city: CityCode.create('DEFAULT'),
|
city: CityCode.create('DEFAULT'),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { DomainError } from '@/shared/exceptions/domain.exception';
|
import { DomainError } from '@/shared/exceptions/domain.exception';
|
||||||
import {
|
import {
|
||||||
UserId, AccountSequence, PhoneNumber, ReferralCode, ProvinceCode, CityCode,
|
UserId, AccountSequence, PhoneNumber, ReferralCode, ProvinceCode, CityCode,
|
||||||
DeviceInfo, HardwareInfo, ChainType, KYCInfo, KYCStatus, AccountStatus,
|
DeviceInfo, ChainType, KYCInfo, KYCStatus, AccountStatus,
|
||||||
} from '@/domain/value-objects';
|
} from '@/domain/value-objects';
|
||||||
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
|
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
|
||||||
import {
|
import {
|
||||||
|
|
@ -87,7 +87,7 @@ export class UserAccount {
|
||||||
accountSequence: AccountSequence;
|
accountSequence: AccountSequence;
|
||||||
initialDeviceId: string;
|
initialDeviceId: string;
|
||||||
deviceName?: string;
|
deviceName?: string;
|
||||||
hardwareInfo?: HardwareInfo;
|
deviceInfo?: Record<string, unknown>; // 完整的设备信息 JSON
|
||||||
inviterSequence: AccountSequence | null;
|
inviterSequence: AccountSequence | null;
|
||||||
province: ProvinceCode;
|
province: ProvinceCode;
|
||||||
city: CityCode;
|
city: CityCode;
|
||||||
|
|
@ -97,7 +97,7 @@ export class UserAccount {
|
||||||
const devices = new Map<string, DeviceInfo>();
|
const devices = new Map<string, DeviceInfo>();
|
||||||
devices.set(params.initialDeviceId, new DeviceInfo(
|
devices.set(params.initialDeviceId, new DeviceInfo(
|
||||||
params.initialDeviceId, params.deviceName || '未命名设备', new Date(), new Date(),
|
params.initialDeviceId, params.deviceName || '未命名设备', new Date(), new Date(),
|
||||||
params.hardwareInfo,
|
params.deviceInfo, // 传递完整的 JSON
|
||||||
));
|
));
|
||||||
|
|
||||||
// UserID将由数据库自动生成(autoincrement),这里使用临时值0
|
// UserID将由数据库自动生成(autoincrement),这里使用临时值0
|
||||||
|
|
@ -130,7 +130,7 @@ export class UserAccount {
|
||||||
phoneNumber: PhoneNumber;
|
phoneNumber: PhoneNumber;
|
||||||
initialDeviceId: string;
|
initialDeviceId: string;
|
||||||
deviceName?: string;
|
deviceName?: string;
|
||||||
hardwareInfo?: HardwareInfo;
|
deviceInfo?: Record<string, unknown>; // 完整的设备信息 JSON
|
||||||
inviterSequence: AccountSequence | null;
|
inviterSequence: AccountSequence | null;
|
||||||
province: ProvinceCode;
|
province: ProvinceCode;
|
||||||
city: CityCode;
|
city: CityCode;
|
||||||
|
|
@ -138,7 +138,7 @@ export class UserAccount {
|
||||||
const devices = new Map<string, DeviceInfo>();
|
const devices = new Map<string, DeviceInfo>();
|
||||||
devices.set(params.initialDeviceId, new DeviceInfo(
|
devices.set(params.initialDeviceId, new DeviceInfo(
|
||||||
params.initialDeviceId, params.deviceName || '未命名设备', new Date(), new Date(),
|
params.initialDeviceId, params.deviceName || '未命名设备', new Date(), new Date(),
|
||||||
params.hardwareInfo,
|
params.deviceInfo,
|
||||||
));
|
));
|
||||||
|
|
||||||
// UserID将由数据库自动生成(autoincrement),这里使用临时值0
|
// 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();
|
this.ensureActive();
|
||||||
if (this._devices.size >= 5 && !this._devices.has(deviceId)) {
|
if (this._devices.size >= 5 && !this._devices.has(deviceId)) {
|
||||||
throw new DomainError('最多允许5个设备同时登录');
|
throw new DomainError('最多允许5个设备同时登录');
|
||||||
|
|
@ -209,12 +209,12 @@ export class UserAccount {
|
||||||
if (this._devices.has(deviceId)) {
|
if (this._devices.has(deviceId)) {
|
||||||
const device = this._devices.get(deviceId)!;
|
const device = this._devices.get(deviceId)!;
|
||||||
device.updateActivity();
|
device.updateActivity();
|
||||||
if (hardwareInfo) {
|
if (deviceInfo) {
|
||||||
device.updateHardwareInfo(hardwareInfo);
|
device.updateDeviceInfo(deviceInfo);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._devices.set(deviceId, new DeviceInfo(
|
this._devices.set(deviceId, new DeviceInfo(
|
||||||
deviceId, deviceName || '未命名设备', new Date(), new Date(), hardwareInfo,
|
deviceId, deviceName || '未命名设备', new Date(), new Date(), deviceInfo,
|
||||||
));
|
));
|
||||||
this.addDomainEvent(new DeviceAddedEvent({
|
this.addDomainEvent(new DeviceAddedEvent({
|
||||||
userId: this.userId.toString(),
|
userId: this.userId.toString(),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export class DeviceInfo {
|
||||||
public readonly deviceName: string,
|
public readonly deviceName: string,
|
||||||
public readonly addedAt: Date,
|
public readonly addedAt: Date,
|
||||||
lastActiveAt: Date,
|
lastActiveAt: Date,
|
||||||
|
public readonly deviceInfo?: Record<string, unknown>, // 完整的设备信息 JSON
|
||||||
) {
|
) {
|
||||||
this._lastActiveAt = lastActiveAt;
|
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 ============
|
||||||
|
// deviceInfo: 完整的设备信息 JSON,100% 保持前端传递的原样
|
||||||
export class DeviceInfo {
|
export class DeviceInfo {
|
||||||
private _lastActiveAt: Date;
|
private _lastActiveAt: Date;
|
||||||
private _hardwareInfo: HardwareInfo;
|
private _deviceInfo: Record<string, unknown>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly deviceId: string,
|
public readonly deviceId: string,
|
||||||
public readonly deviceName: string,
|
public readonly deviceName: string,
|
||||||
public readonly addedAt: Date,
|
public readonly addedAt: Date,
|
||||||
lastActiveAt: Date,
|
lastActiveAt: Date,
|
||||||
hardwareInfo?: HardwareInfo,
|
deviceInfo?: Record<string, unknown>,
|
||||||
) {
|
) {
|
||||||
this._lastActiveAt = lastActiveAt;
|
this._lastActiveAt = lastActiveAt;
|
||||||
this._hardwareInfo = hardwareInfo || {};
|
this._deviceInfo = deviceInfo || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
get lastActiveAt(): Date {
|
get lastActiveAt(): Date {
|
||||||
return this._lastActiveAt;
|
return this._lastActiveAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hardwareInfo(): HardwareInfo {
|
// 100% 保持原样的完整设备信息 JSON
|
||||||
return this._hardwareInfo;
|
get deviceInfo(): Record<string, unknown> {
|
||||||
|
return this._deviceInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 便捷访问器
|
||||||
get platform(): string | undefined {
|
get platform(): string | undefined {
|
||||||
return this._hardwareInfo.platform;
|
return this._deviceInfo.platform as string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get deviceModel(): string | undefined {
|
get deviceModel(): string | undefined {
|
||||||
return this._hardwareInfo.deviceModel;
|
return (this._deviceInfo.model || this._deviceInfo.deviceModel) as string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get osVersion(): string | undefined {
|
get osVersion(): string | undefined {
|
||||||
return this._hardwareInfo.osVersion;
|
return this._deviceInfo.osVersion as string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get appVersion(): string | undefined {
|
get appVersion(): string | undefined {
|
||||||
return this._hardwareInfo.appVersion;
|
return this._deviceInfo.appVersion as string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateActivity(): void {
|
updateActivity(): void {
|
||||||
this._lastActiveAt = new Date();
|
this._lastActiveAt = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHardwareInfo(info: HardwareInfo): void {
|
updateDeviceInfo(info: Record<string, unknown>): void {
|
||||||
this._hardwareInfo = { ...this._hardwareInfo, ...info };
|
this._deviceInfo = { ...this._deviceInfo, ...info };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ export interface UserDeviceEntity {
|
||||||
userId: bigint;
|
userId: bigint;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
deviceName: string | null;
|
deviceName: string | null;
|
||||||
// Hardware Info
|
deviceInfo: Record<string, unknown> | null; // 完整的设备信息 JSON
|
||||||
|
// Hardware Info (冗余字段,便于查询)
|
||||||
platform: string | null;
|
platform: string | null;
|
||||||
deviceModel: string | null;
|
deviceModel: string | null;
|
||||||
osVersion: 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 { Injectable } from '@nestjs/common';
|
||||||
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
|
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
|
||||||
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
|
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 { UserAccountEntity } from '../entities/user-account.entity';
|
||||||
import { toMpcSignatureString } from '../entities/wallet-address.entity';
|
import { toMpcSignatureString } from '../entities/wallet-address.entity';
|
||||||
|
|
||||||
|
|
@ -9,22 +9,13 @@ import { toMpcSignatureString } from '../entities/wallet-address.entity';
|
||||||
export class UserAccountMapper {
|
export class UserAccountMapper {
|
||||||
toDomain(entity: UserAccountEntity): UserAccount {
|
toDomain(entity: UserAccountEntity): UserAccount {
|
||||||
const devices = (entity.devices || []).map((d) => {
|
const devices = (entity.devices || []).map((d) => {
|
||||||
const hardwareInfo: HardwareInfo = {
|
// 直接使用完整的 deviceInfo JSON,100% 保持原样
|
||||||
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,
|
|
||||||
};
|
|
||||||
return new DeviceInfo(
|
return new DeviceInfo(
|
||||||
d.deviceId,
|
d.deviceId,
|
||||||
d.deviceName || '未命名设备',
|
d.deviceName || '未命名设备',
|
||||||
d.addedAt,
|
d.addedAt,
|
||||||
d.lastActiveAt,
|
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 { WalletAddress } from '@/domain/entities/wallet-address.entity';
|
||||||
import {
|
import {
|
||||||
UserId, AccountSequence, PhoneNumber, ReferralCode, ChainType,
|
UserId, AccountSequence, PhoneNumber, ReferralCode, ChainType,
|
||||||
AccountStatus, KYCStatus, DeviceInfo, HardwareInfo, KYCInfo, AddressStatus,
|
AccountStatus, KYCStatus, DeviceInfo, KYCInfo, AddressStatus,
|
||||||
} from '@/domain/value-objects';
|
} from '@/domain/value-objects';
|
||||||
import { toMpcSignatureString, fromMpcSignatureString } from '../entities/wallet-address.entity';
|
import { toMpcSignatureString, fromMpcSignatureString } from '../entities/wallet-address.entity';
|
||||||
|
|
||||||
|
|
@ -75,21 +75,26 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
|
||||||
await tx.userDevice.deleteMany({ where: { userId: savedUserId } });
|
await tx.userDevice.deleteMany({ where: { userId: savedUserId } });
|
||||||
if (devices.length > 0) {
|
if (devices.length > 0) {
|
||||||
await tx.userDevice.createMany({
|
await tx.userDevice.createMany({
|
||||||
data: devices.map((d) => ({
|
data: devices.map((d) => {
|
||||||
userId: savedUserId,
|
// 从 deviceInfo JSON 中提取冗余字段便于查询
|
||||||
deviceId: d.deviceId,
|
const info = d.deviceInfo || {};
|
||||||
deviceName: d.deviceName,
|
return {
|
||||||
platform: d.hardwareInfo.platform || null,
|
userId: savedUserId,
|
||||||
deviceModel: d.hardwareInfo.deviceModel || null,
|
deviceId: d.deviceId,
|
||||||
osVersion: d.hardwareInfo.osVersion || null,
|
deviceName: d.deviceName,
|
||||||
appVersion: d.hardwareInfo.appVersion || null,
|
deviceInfo: d.deviceInfo || null, // 100% 保存完整 JSON
|
||||||
screenWidth: d.hardwareInfo.screenWidth || null,
|
platform: (info as any).platform || null,
|
||||||
screenHeight: d.hardwareInfo.screenHeight || null,
|
deviceModel: (info as any).model || null,
|
||||||
locale: d.hardwareInfo.locale || null,
|
osVersion: (info as any).osVersion || null,
|
||||||
timezone: d.hardwareInfo.timezone || null,
|
appVersion: (info as any).appVersion || null,
|
||||||
addedAt: d.addedAt,
|
screenWidth: (info as any).screenWidth || null,
|
||||||
lastActiveAt: d.lastActiveAt,
|
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 {
|
private toDomain(data: any): UserAccount {
|
||||||
const devices = data.devices.map((d: any) => {
|
const devices = data.devices.map((d: any) => {
|
||||||
const hardwareInfo: HardwareInfo = {
|
// 优先使用完整的 deviceInfo JSON,保持原样
|
||||||
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,
|
|
||||||
};
|
|
||||||
return new DeviceInfo(
|
return new DeviceInfo(
|
||||||
d.deviceId,
|
d.deviceId,
|
||||||
d.deviceName || '未命名设备',
|
d.deviceName || '未命名设备',
|
||||||
d.addedAt,
|
d.addedAt,
|
||||||
d.lastActiveAt,
|
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