refactor(migration): rename data migration files and update migration logic

This commit renames the data migration files for clarity, changing `dataMigrate.html` to `dataRefactorMigrate.html` and updating the corresponding service imports. It also enhances the migration logic by implementing a new `app_state` table structure and removing deprecated migration files, streamlining the overall migration process.
This commit is contained in:
fullex
2025-08-09 22:37:20 +08:00
parent ff965402cd
commit 92eb5aed7f
17 changed files with 138 additions and 246 deletions
@@ -0,0 +1,564 @@
import dbService from '@data/db/DbService'
import { appStateTable } from '@data/db/schemas/appState'
import { loggerService } from '@logger'
import { isDev } from '@main/constant'
import BackupManager from '@main/services/BackupManager'
import { IpcChannel } from '@shared/IpcChannel'
import { eq } from 'drizzle-orm'
import { app, BrowserWindow, ipcMain } from 'electron'
import { app as electronApp } from 'electron'
import { join } from 'path'
import { PreferencesMigrator } from './migrators/PreferencesMigrator'
const logger = loggerService.withContext('DataRefactorMigrateService')
const DATA_REFACTOR_MIGRATION_STATUS = 'data_refactor_migration_status'
// Data refactor migration status interface
interface DataRefactorMigrationStatus {
completed: boolean
completedAt?: number
version?: string
}
interface MigrationProgress {
stage: string
progress: number
total: number
message: string
}
interface MigrationResult {
success: boolean
error?: string
migratedCount: number
}
class DataRefactorMigrateService {
private static instance: DataRefactorMigrateService | null = null
private migrateWindow: BrowserWindow | null = null
private backupManager: BackupManager
private backupCompletionResolver: ((value: boolean) => void) | null = null
private backupTimeout: NodeJS.Timeout | null = null
private db = dbService.getDb()
private currentProgress: MigrationProgress = {
stage: 'idle',
progress: 0,
total: 100,
message: 'Ready to migrate'
}
private isMigrating: boolean = false
constructor() {
this.backupManager = new BackupManager()
}
/**
* Get backup manager instance for integration with existing backup system
*/
public getBackupManager(): BackupManager {
return this.backupManager
}
/**
* Register migration-specific IPC handlers
* This creates an isolated IPC environment only for migration operations
*/
public registerMigrationIpcHandlers(): void {
logger.info('Registering migration-specific IPC handlers')
// Only register the minimal IPC handlers needed for migration
ipcMain.handle(IpcChannel.DataMigrate_CheckNeeded, async () => {
try {
return await this.isMigrated()
} catch (error) {
logger.error('IPC handler error: checkMigrationNeeded', error as Error)
throw error
}
})
ipcMain.handle(IpcChannel.DataMigrate_StartMigration, async () => {
try {
return await this.runMigration()
} catch (error) {
logger.error('IPC handler error: runMigration', error as Error)
throw error
}
})
ipcMain.handle(IpcChannel.DataMigrate_GetProgress, () => {
try {
return this.getCurrentProgress()
} catch (error) {
logger.error('IPC handler error: getCurrentProgress', error as Error)
throw error
}
})
ipcMain.handle(IpcChannel.DataMigrate_Cancel, async () => {
try {
return await this.cancelMigration()
} catch (error) {
logger.error('IPC handler error: cancelMigration', error as Error)
throw error
}
})
ipcMain.handle(IpcChannel.DataMigrate_BackupCompleted, () => {
try {
this.notifyBackupCompleted()
return true
} catch (error) {
logger.error('IPC handler error: notifyBackupCompleted', error as Error)
throw error
}
})
ipcMain.handle(IpcChannel.DataMigrate_ShowBackupDialog, () => {
try {
// Show the backup dialog/interface
// This could integrate with existing backup UI or create a new backup interface
logger.info('Backup dialog request received')
return true
} catch (error) {
logger.error('IPC handler error: showBackupDialog', error as Error)
throw error
}
})
logger.info('Migration IPC handlers registered successfully')
}
/**
* Remove migration-specific IPC handlers
* Clean up when migration is complete or cancelled
*/
public unregisterMigrationIpcHandlers(): void {
logger.info('Unregistering migration-specific IPC handlers')
try {
ipcMain.removeAllListeners(IpcChannel.DataMigrate_CheckNeeded)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartMigration)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_GetProgress)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_Cancel)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_BackupCompleted)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_ShowBackupDialog)
logger.info('Migration IPC handlers unregistered successfully')
} catch (error) {
logger.warn('Error unregistering migration IPC handlers', error as Error)
}
}
public static getInstance(): DataRefactorMigrateService {
if (!DataRefactorMigrateService.instance) {
DataRefactorMigrateService.instance = new DataRefactorMigrateService()
}
return DataRefactorMigrateService.instance
}
/**
* Check if migration is needed
*/
async isMigrated(): Promise<boolean> {
try {
const isMigrated = await this.isMigrationCompleted()
if (isMigrated) {
logger.info('Data Refactor Migration already completed')
return true
}
return false
} catch (error) {
logger.error('Failed to check migration status', error as Error)
return false
}
}
/**
* Check if migration is already completed
*/
private async isMigrationCompleted(): Promise<boolean> {
try {
const result = await this.db
.select()
.from(appStateTable)
.where(eq(appStateTable.key, DATA_REFACTOR_MIGRATION_STATUS))
.limit(1)
if (result.length === 0) return false
const status = result[0].value as DataRefactorMigrationStatus
return status.completed === true
} catch (error) {
logger.warn('Failed to check migration state', error as Error)
return false
}
}
/**
* Mark migration as completed
*/
private async markMigrationCompleted(): Promise<void> {
try {
const migrationStatus: DataRefactorMigrationStatus = {
completed: true,
completedAt: Date.now(),
version: electronApp.getVersion()
}
await this.db
.insert(appStateTable)
.values({
key: DATA_REFACTOR_MIGRATION_STATUS,
value: migrationStatus, // drizzle handles JSON serialization automatically
description: 'Data refactoring migration status from legacy format (ElectronStore + Redux persist) to SQLite',
createdAt: Date.now(),
updatedAt: Date.now()
})
.onConflictDoUpdate({
target: appStateTable.key,
set: {
value: migrationStatus,
updatedAt: Date.now()
}
})
logger.info('Migration marked as completed in app_state table', {
version: migrationStatus.version,
completedAt: migrationStatus.completedAt
})
} catch (error) {
logger.error('Failed to mark migration as completed', error as Error)
throw error
}
}
/**
* Create and show migration window
*/
private createMigrateWindow(): BrowserWindow {
if (this.migrateWindow && !this.migrateWindow.isDestroyed()) {
this.migrateWindow.show()
return this.migrateWindow
}
// Register migration-specific IPC handlers before creating window
this.registerMigrationIpcHandlers()
this.migrateWindow = new BrowserWindow({
width: 600,
height: 500,
resizable: false,
maximizable: false,
minimizable: false,
show: false,
autoHideMenuBar: true,
titleBarStyle: 'hidden',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false,
contextIsolation: true
}
})
// Load the migration window
if (isDev && process.env['ELECTRON_RENDERER_URL']) {
this.migrateWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/dataRefactorMigrate.html')
} else {
this.migrateWindow.loadFile(join(__dirname, '../renderer/dataRefactorMigrate.html'))
}
this.migrateWindow.once('ready-to-show', () => {
this.migrateWindow?.show()
if (!app.isPackaged) {
this.migrateWindow?.webContents.openDevTools()
}
})
this.migrateWindow.on('closed', () => {
this.migrateWindow = null
// Clean up IPC handlers when window is closed
this.unregisterMigrationIpcHandlers()
})
logger.info('Migration window created')
return this.migrateWindow
}
/**
* Run the complete migration process
*/
async runMigration(): Promise<void> {
if (this.isMigrating) {
logger.warn('Migration already in progress')
return
}
try {
this.isMigrating = true
logger.info('Starting migration process')
// Create migration window
const window = this.createMigrateWindow()
// Wait for window to be ready
await new Promise<void>((resolve) => {
if (window.webContents.isLoading()) {
window.webContents.once('did-finish-load', () => resolve())
} else {
resolve()
}
})
// Start the migration flow
await this.executeMigrationFlow()
} catch (error) {
logger.error('Migration process failed', error as Error)
throw error
} finally {
this.isMigrating = false
}
}
/**
* Execute the complete migration flow
*/
private async executeMigrationFlow(): Promise<void> {
try {
// Step 1: Enforce backup
await this.updateProgress('backup', 0, 'Starting backup process...')
const backupSuccess = await this.enforceBackup()
if (!backupSuccess) {
throw new Error('Backup process failed or was cancelled by user')
}
await this.updateProgress('backup', 100, 'Backup completed successfully')
// Step 2: Execute migration
await this.updateProgress('migration', 0, 'Starting data migration...')
const migrationResult = await this.executeMigration()
if (!migrationResult.success) {
throw new Error(migrationResult.error || 'Migration failed')
}
await this.updateProgress(
'migration',
100,
`Migration completed: ${migrationResult.migratedCount} items migrated`
)
// Step 3: Mark as completed
await this.markMigrationCompleted()
await this.updateProgress('completed', 100, 'Migration completed! App will restart in 3 seconds...')
// Wait a moment to show success message, then restart the app
await new Promise<void>((resolve) => {
setTimeout(() => {
logger.info('Migration completed successfully, restarting application')
this.restartApplication()
resolve()
}, 3000)
})
} catch (error) {
logger.error('Migration flow failed', error as Error)
await this.updateProgress(
'error',
0,
`Migration failed: ${error instanceof Error ? error.message : String(error)}. Please restart the app to try again.`
)
// Wait a moment to show error message, then close migration window
// Do NOT restart on error - let user handle the situation
await new Promise<void>((resolve) => {
setTimeout(() => {
this.closeMigrateWindow()
resolve()
}, 8000) // Show error for longer (8 seconds) to give user time to read
})
throw error
}
}
/**
* Enforce backup before migration
*/
private async enforceBackup(): Promise<boolean> {
try {
logger.info('Enforcing backup before migration')
await this.updateProgress('backup', 0, 'Backup is required before migration')
// Send backup requirement to renderer
if (this.migrateWindow && !this.migrateWindow.isDestroyed()) {
this.migrateWindow.webContents.send(IpcChannel.DataMigrate_RequireBackup)
}
// Wait for user to complete backup
const backupResult = await this.waitForBackupCompletion()
if (backupResult) {
await this.updateProgress('backup', 100, 'Backup completed successfully')
return true
} else {
await this.updateProgress('backup', 0, 'Backup is required to proceed with migration')
return false
}
} catch (error) {
logger.error('Backup enforcement failed', error as Error)
await this.updateProgress('backup', 0, 'Backup process failed')
return false
}
}
/**
* Wait for user to complete backup
*/
private async waitForBackupCompletion(): Promise<boolean> {
return new Promise((resolve) => {
// Store resolver for later use
this.backupCompletionResolver = resolve
// Set up timeout (5 minutes)
this.backupTimeout = setTimeout(() => {
logger.warn('Backup completion timeout')
this.backupCompletionResolver = null
this.backupTimeout = null
resolve(false)
}, 300000) // 5 minutes
// The actual completion will be triggered by notifyBackupCompleted() method
})
}
/**
* Notify that backup has been completed (called from IPC handler)
*/
public notifyBackupCompleted(): void {
if (this.backupCompletionResolver) {
logger.info('Backup completed by user')
// Clear timeout if it exists
if (this.backupTimeout) {
clearTimeout(this.backupTimeout)
this.backupTimeout = null
}
this.backupCompletionResolver(true)
this.backupCompletionResolver = null
}
}
/**
* Execute the actual migration
*/
private async executeMigration(): Promise<MigrationResult> {
try {
logger.info('Executing migration')
// Create preferences migrator
const preferencesMigrator = new PreferencesMigrator()
// Execute preferences migration with progress updates
const result = await preferencesMigrator.migrate((progress, message) => {
this.updateProgress('migration', progress, message)
})
logger.info('Migration execution completed', result)
return {
success: result.success,
migratedCount: result.migratedCount,
error: result.errors.length > 0 ? result.errors.map((e) => e.error).join('; ') : undefined
}
} catch (error) {
logger.error('Migration execution failed', error as Error)
return {
success: false,
error: error instanceof Error ? error.message : String(error),
migratedCount: 0
}
}
}
/**
* Update migration progress and broadcast to window
*/
private async updateProgress(stage: string, progress: number, message: string): Promise<void> {
this.currentProgress = {
stage,
progress,
total: 100,
message
}
if (this.migrateWindow && !this.migrateWindow.isDestroyed()) {
this.migrateWindow.webContents.send(IpcChannel.DataMigrateProgress, this.currentProgress)
}
logger.debug('Progress updated', this.currentProgress)
}
/**
* Get current migration progress
*/
getCurrentProgress(): MigrationProgress {
return this.currentProgress
}
/**
* Cancel migration process
*/
async cancelMigration(): Promise<void> {
if (!this.isMigrating) {
return
}
logger.info('Cancelling migration process')
this.isMigrating = false
await this.updateProgress('cancelled', 0, 'Migration cancelled by user')
this.closeMigrateWindow()
}
/**
* Close migration window
*/
private closeMigrateWindow(): void {
if (this.migrateWindow && !this.migrateWindow.isDestroyed()) {
this.migrateWindow.close()
this.migrateWindow = null
}
// Clean up migration-specific IPC handlers
this.unregisterMigrationIpcHandlers()
}
/**
* Restart the application after successful migration
*/
private restartApplication(): void {
try {
logger.info('Restarting application after migration completion')
// Clean up migration window and handlers before restart
this.closeMigrateWindow()
// Restart the app using Electron's relaunch mechanism
app.relaunch()
app.exit(0)
} catch (error) {
logger.error('Failed to restart application', error as Error)
// Fallback: just close migration window and let user manually restart
this.closeMigrateWindow()
}
}
}
// Export singleton instance
export const dataRefactorMigrateService = DataRefactorMigrateService.getInstance()
@@ -0,0 +1,345 @@
import dbService from '@data/db/DbService'
import { preferenceTable } from '@data/db/schemas/preference'
import { loggerService } from '@logger'
import { and, eq } from 'drizzle-orm'
import { configManager } from '../../../../services/ConfigManager'
const logger = loggerService.withContext('PreferencesMigrator')
export interface MigrationItem {
originalKey: string
targetKey: string
type: string
defaultValue: any
source: 'electronStore' | 'redux'
sourceCategory: string
}
export interface MigrationResult {
success: boolean
migratedCount: number
errors: Array<{
key: string
error: string
}>
}
export class PreferencesMigrator {
private db = dbService.getDb()
/**
* Execute preferences migration from all sources
*/
async migrate(onProgress?: (progress: number, message: string) => void): Promise<MigrationResult> {
logger.info('Starting preferences migration')
const result: MigrationResult = {
success: true,
migratedCount: 0,
errors: []
}
try {
// Get migration items from classification.json
const migrationItems = await this.loadMigrationItems()
const totalItems = migrationItems.length
logger.info(`Found ${totalItems} items to migrate`)
for (let i = 0; i < migrationItems.length; i++) {
const item = migrationItems[i]
try {
await this.migrateItem(item)
result.migratedCount++
const progress = Math.floor(((i + 1) / totalItems) * 100)
onProgress?.(progress, `Migrated: ${item.targetKey}`)
} catch (error) {
logger.error('Failed to migrate item', { item, error })
result.errors.push({
key: item.originalKey,
error: error instanceof Error ? error.message : String(error)
})
result.success = false
}
}
logger.info('Preferences migration completed', {
migratedCount: result.migratedCount,
errorCount: result.errors.length
})
} catch (error) {
logger.error('Preferences migration failed', error as Error)
result.success = false
result.errors.push({
key: 'global',
error: error instanceof Error ? error.message : String(error)
})
}
return result
}
/**
* Load migration items from the generated preferences.ts mappings
* For now, we'll use a simplified set based on the current generated migration code
*/
private async loadMigrationItems(): Promise<MigrationItem[]> {
// This is a simplified implementation. In the full version, this would read from
// the classification.json and apply the same deduplication logic as the generators
const items: MigrationItem[] = [
// ElectronStore items (from generated migration code)
{
originalKey: 'Language',
targetKey: 'app.language',
sourceCategory: 'Language',
type: 'unknown',
defaultValue: null,
source: 'electronStore'
},
{
originalKey: 'SelectionAssistantFollowToolbar',
targetKey: 'feature.selection.follow_toolbar',
sourceCategory: 'SelectionAssistantFollowToolbar',
type: 'unknown',
defaultValue: null,
source: 'electronStore'
},
{
originalKey: 'SelectionAssistantRemeberWinSize',
targetKey: 'feature.selection.remember_win_size',
sourceCategory: 'SelectionAssistantRemeberWinSize',
type: 'unknown',
defaultValue: null,
source: 'electronStore'
},
{
originalKey: 'ZoomFactor',
targetKey: 'app.zoom_factor',
sourceCategory: 'ZoomFactor',
type: 'unknown',
defaultValue: null,
source: 'electronStore'
}
]
// Add some sample Redux items (in full implementation, these would be loaded from classification.json)
const reduxItems: MigrationItem[] = [
{
originalKey: 'theme',
targetKey: 'app.theme.mode',
sourceCategory: 'settings',
type: 'string',
defaultValue: 'ThemeMode.system',
source: 'redux'
},
{
originalKey: 'language',
targetKey: 'app.language',
sourceCategory: 'settings',
type: 'string',
defaultValue: 'en',
source: 'redux'
}
]
items.push(...reduxItems)
return items
}
/**
* Migrate a single preference item
*/
private async migrateItem(item: MigrationItem): Promise<void> {
logger.debug('Migrating preference item', { item })
let originalValue: any
// Read value from the appropriate source
if (item.source === 'electronStore') {
originalValue = await this.readFromElectronStore(item.originalKey)
} else if (item.source === 'redux') {
originalValue = await this.readFromReduxPersist(item.sourceCategory, item.originalKey)
} else {
throw new Error(`Unknown source: ${item.source}`)
}
// Use default value if original value is not found
let valueToMigrate = originalValue
if (originalValue === undefined || originalValue === null) {
valueToMigrate = item.defaultValue
}
// Convert value to appropriate type
const convertedValue = this.convertValue(valueToMigrate, item.type)
// Write to preferences table using Drizzle
await this.writeToPreferences(item.targetKey, convertedValue)
logger.debug('Successfully migrated preference item', {
targetKey: item.targetKey,
originalValue,
convertedValue
})
}
/**
* Read value from ElectronStore (via ConfigManager)
*/
private async readFromElectronStore(key: string): Promise<any> {
try {
return configManager.get(key)
} catch (error) {
logger.warn('Failed to read from ElectronStore', { key, error })
return undefined
}
}
/**
* Read value from Redux persist data
*/
private async readFromReduxPersist(category: string, key: string): Promise<any> {
try {
// This is a simplified implementation
// In the full version, we would need to properly parse the leveldb files
// For now, we'll return undefined to use default values
logger.debug('Redux persist read not fully implemented', { category, key })
return undefined
} catch (error) {
logger.warn('Failed to read from Redux persist', { category, key, error })
return undefined
}
}
/**
* Convert value to the specified type
*/
private convertValue(value: any, targetType: string): any {
if (value === null || value === undefined) {
return null
}
try {
switch (targetType) {
case 'boolean':
return this.toBoolean(value)
case 'string':
return this.toString(value)
case 'number':
return this.toNumber(value)
case 'array':
case 'unknown[]':
return this.toArray(value)
case 'object':
case 'Record<string, unknown>':
return this.toObject(value)
default:
return value
}
} catch (error) {
logger.warn('Type conversion failed, using original value', { value, targetType, error })
return value
}
}
private toBoolean(value: any): boolean {
if (typeof value === 'boolean') return value
if (typeof value === 'string') {
const lower = value.toLowerCase()
return lower === 'true' || lower === '1' || lower === 'yes'
}
if (typeof value === 'number') return value !== 0
return Boolean(value)
}
private toString(value: any): string {
if (typeof value === 'string') return value
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
if (typeof value === 'object') return JSON.stringify(value)
return String(value)
}
private toNumber(value: any): number {
if (typeof value === 'number') return value
if (typeof value === 'string') {
const parsed = parseFloat(value)
return isNaN(parsed) ? 0 : parsed
}
if (typeof value === 'boolean') return value ? 1 : 0
return 0
}
private toArray(value: any): any[] {
if (Array.isArray(value)) return value
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : [value]
} catch {
return [value]
}
}
return [value]
}
private toObject(value: any): Record<string, any> {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return value
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : { value }
} catch {
return { value }
}
}
return { value }
}
/**
* Write value to preferences table using direct Drizzle operations
*/
private async writeToPreferences(targetKey: string, value: any): Promise<void> {
const scope = 'default'
try {
// Check if preference already exists
const existing = await this.db
.select()
.from(preferenceTable)
.where(and(eq(preferenceTable.scope, scope), eq(preferenceTable.key, targetKey)))
.limit(1)
if (existing.length > 0) {
// Update existing preference
await this.db
.update(preferenceTable)
.set({
value: value, // drizzle handles JSON serialization automatically
updatedAt: Date.now()
})
.where(and(eq(preferenceTable.scope, scope), eq(preferenceTable.key, targetKey)))
} else {
// Insert new preference
await this.db.insert(preferenceTable).values({
scope,
key: targetKey,
value: value, // drizzle handles JSON serialization automatically
createdAt: Date.now(),
updatedAt: Date.now()
})
}
logger.debug('Successfully wrote to preferences table', { targetKey, value })
} catch (error) {
logger.error('Failed to write to preferences table', { targetKey, value, error })
throw error
}
}
}
@@ -0,0 +1,72 @@
/**
* Migration helper utilities
* Generated at: 2025-08-09T07:20:05.912Z
*/
import { loggerService } from '@logger'
const logger = loggerService.withContext('MigrationHelpers')
export interface BackupInfo {
timestamp: string
version: string
dataSize: number
backupPath: string
}
export class MigrationHelpers {
/**
* 创建数据备份
*/
static async createBackup(): Promise<BackupInfo> {
logger.info('开始创建数据备份')
// 实现备份逻辑
const timestamp = new Date().toISOString()
const backupInfo: BackupInfo = {
timestamp,
version: process.env.npm_package_version || 'unknown',
dataSize: 0,
backupPath: ''
}
// TODO: 实现具体的备份逻辑
logger.info('数据备份完成', backupInfo)
return backupInfo
}
/**
* 验证备份完整性
*/
static async validateBackup(backupInfo: BackupInfo): Promise<boolean> {
logger.info('验证备份完整性', backupInfo)
// TODO: 实现备份验证逻辑
return true
}
/**
* 恢复备份
*/
static async restoreBackup(backupInfo: BackupInfo): Promise<boolean> {
logger.info('开始恢复备份', backupInfo)
// TODO: 实现备份恢复逻辑
logger.info('备份恢复完成')
return true
}
/**
* 清理临时文件
*/
static async cleanup(): Promise<void> {
logger.info('清理迁移临时文件')
// TODO: 实现清理逻辑
logger.info('清理完成')
}
}
@@ -0,0 +1,125 @@
/**
* Type conversion utilities for migration
* Generated at: 2025-08-09T07:20:05.912Z
*/
import { loggerService } from '@logger'
const logger = loggerService.withContext('TypeConverter')
export class TypeConverter {
/**
* 转换值到指定类型
*/
convert(value: any, targetType: string): any {
if (value === null || value === undefined) {
return null
}
try {
switch (targetType) {
case 'boolean':
return this.toBoolean(value)
case 'string':
return this.toString(value)
case 'number':
return this.toNumber(value)
case 'array':
case 'unknown[]':
return this.toArray(value)
case 'object':
case 'Record<string, unknown>':
return this.toObject(value)
default:
// 未知类型,保持原样
logger.debug('未知类型,保持原值', { targetType, value })
return value
}
} catch (error) {
logger.error('类型转换失败', { value, targetType, error })
return value
}
}
private toBoolean(value: any): boolean {
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'string') {
const lower = value.toLowerCase()
return lower === 'true' || lower === '1' || lower === 'yes'
}
if (typeof value === 'number') {
return value !== 0
}
return Boolean(value)
}
private toString(value: any): string {
if (typeof value === 'string') {
return value
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value)
}
if (typeof value === 'object') {
return JSON.stringify(value)
}
return String(value)
}
private toNumber(value: any): number {
if (typeof value === 'number') {
return value
}
if (typeof value === 'string') {
const parsed = parseFloat(value)
if (isNaN(parsed)) {
logger.warn('字符串无法转换为数字', { value })
return 0
}
return parsed
}
if (typeof value === 'boolean') {
return value ? 1 : 0
}
return 0
}
private toArray(value: any): any[] {
if (Array.isArray(value)) {
return value
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : [value]
} catch {
return [value]
}
}
return [value]
}
private toObject(value: any): Record<string, any> {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return value
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
? parsed
: { value }
} catch {
return { value }
}
}
return { value }
}
}