mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-28 15:52:44 +08:00
feat: 优化迁移表逻辑
This commit is contained in:
@@ -451,18 +451,33 @@ export function checkMigrationNeeded(): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行所有数据库的迁移
|
* 迁移失败的数据库信息
|
||||||
* @returns 迁移结果
|
|
||||||
*/
|
*/
|
||||||
export function migrateAllDatabases(): { success: boolean; migratedCount: number; error?: string } {
|
interface MigrationFailure {
|
||||||
|
sessionId: string
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行所有数据库的迁移
|
||||||
|
* 即使部分数据库迁移失败,也会继续处理其他数据库
|
||||||
|
* @returns 迁移结果,包含成功数量和失败列表
|
||||||
|
*/
|
||||||
|
export function migrateAllDatabases(): {
|
||||||
|
success: boolean
|
||||||
|
migratedCount: number
|
||||||
|
failures: MigrationFailure[]
|
||||||
|
error?: string
|
||||||
|
} {
|
||||||
const { sessionIds, forceRepairIds } = checkMigrationNeeded()
|
const { sessionIds, forceRepairIds } = checkMigrationNeeded()
|
||||||
const forceRepairSet = new Set(forceRepairIds)
|
const forceRepairSet = new Set(forceRepairIds)
|
||||||
|
|
||||||
if (sessionIds.length === 0) {
|
if (sessionIds.length === 0) {
|
||||||
return { success: true, migratedCount: 0 }
|
return { success: true, migratedCount: 0, failures: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
let migratedCount = 0
|
let migratedCount = 0
|
||||||
|
const failures: MigrationFailure[] = []
|
||||||
|
|
||||||
for (const sessionId of sessionIds) {
|
for (const sessionId of sessionIds) {
|
||||||
try {
|
try {
|
||||||
@@ -473,14 +488,22 @@ export function migrateAllDatabases(): { success: boolean; migratedCount: number
|
|||||||
migratedCount++
|
migratedCount++
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Database] Failed to migrate ${sessionId}:`, error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
return {
|
console.error(`[Database] Failed to migrate ${sessionId}:`, errorMessage)
|
||||||
success: false,
|
failures.push({ sessionId, error: errorMessage })
|
||||||
migratedCount,
|
|
||||||
error: `迁移 ${sessionId} 失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, migratedCount }
|
// 如果有失败的数据库,返回部分成功状态
|
||||||
|
if (failures.length > 0) {
|
||||||
|
const failedIds = failures.map((f) => f.sessionId.split('_').slice(-1)[0]).join(', ')
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
migratedCount,
|
||||||
|
failures,
|
||||||
|
error: `${failures.length} 个数据库迁移失败(ID: ${failedIds})。建议在侧边栏中删除这些损坏的会话。`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, migratedCount, failures: [] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,31 @@ function setSchemaVersion(db: Database.Database, version: number): void {
|
|||||||
db.prepare('UPDATE meta SET schema_version = ?').run(version)
|
db.prepare('UPDATE meta SET schema_version = ?').run(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查数据库结构是否完整(meta 表必须存在)
|
||||||
|
* 如果 meta 表不存在,说明数据库损坏或不完整
|
||||||
|
*/
|
||||||
|
function checkDatabaseIntegrity(db: Database.Database): { valid: boolean; error?: string } {
|
||||||
|
try {
|
||||||
|
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='meta'").all() as Array<{
|
||||||
|
name: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
if (tables.length === 0) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: '数据库结构不完整:缺少 meta 表。建议删除此数据库文件后重新导入。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { valid: true }
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `数据库检查失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行数据库迁移
|
* 执行数据库迁移
|
||||||
* 自动检测当前版本并执行所有需要的迁移
|
* 自动检测当前版本并执行所有需要的迁移
|
||||||
@@ -186,8 +211,15 @@ function setSchemaVersion(db: Database.Database, version: number): void {
|
|||||||
* @param db 数据库连接
|
* @param db 数据库连接
|
||||||
* @param forceRepair 是否强制修复(即使版本号已是最新也重新执行迁移脚本)
|
* @param forceRepair 是否强制修复(即使版本号已是最新也重新执行迁移脚本)
|
||||||
* @returns 是否执行了迁移
|
* @returns 是否执行了迁移
|
||||||
|
* @throws 如果数据库结构不完整
|
||||||
*/
|
*/
|
||||||
export function migrateDatabase(db: Database.Database, forceRepair = false): boolean {
|
export function migrateDatabase(db: Database.Database, forceRepair = false): boolean {
|
||||||
|
// 首先检查数据库结构完整性
|
||||||
|
const integrity = checkDatabaseIntegrity(db)
|
||||||
|
if (!integrity.valid) {
|
||||||
|
throw new Error(integrity.error)
|
||||||
|
}
|
||||||
|
|
||||||
const currentVersion = getSchemaVersion(db)
|
const currentVersion = getSchemaVersion(db)
|
||||||
|
|
||||||
// 如果不是强制修复模式,检查版本号
|
// 如果不是强制修复模式,检查版本号
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
"upgradeContent": "Upgrade content:",
|
"upgradeContent": "Upgrade content:",
|
||||||
"upgradeNow": "Upgrade Now",
|
"upgradeNow": "Upgrade Now",
|
||||||
"upgrading": "Upgrading...",
|
"upgrading": "Upgrading...",
|
||||||
"failed": "Migration failed"
|
"failed": "Migration failed",
|
||||||
|
"partialFailed": "Some databases failed to upgrade",
|
||||||
|
"errorHint": "This is usually caused by corrupted database files. Please delete the failed sessions from the sidebar and re-import them.",
|
||||||
|
"close": "Close",
|
||||||
|
"retry": "Retry"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
"upgradeContent": "本次升级内容:",
|
"upgradeContent": "本次升级内容:",
|
||||||
"upgradeNow": "立即升级",
|
"upgradeNow": "立即升级",
|
||||||
"upgrading": "正在升级...",
|
"upgrading": "正在升级...",
|
||||||
"failed": "迁移失败"
|
"failed": "迁移失败",
|
||||||
|
"partialFailed": "部分数据库升级失败",
|
||||||
|
"errorHint": "这通常是因为数据库文件损坏。建议在侧边栏中删除失败的会话,然后重新导入。",
|
||||||
|
"close": "关闭",
|
||||||
|
"retry": "重试"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
@@ -11,10 +11,15 @@ const { migrationCount, pendingMigrations, isMigrating } = storeToRefs(sessionSt
|
|||||||
// 弹窗状态
|
// 弹窗状态
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const migrationError = ref<string | null>(null)
|
const migrationError = ref<string | null>(null)
|
||||||
|
const migrationPartialSuccess = ref(false) // 部分成功(有些数据库迁移失败)
|
||||||
|
|
||||||
|
// 是否允许关闭弹窗(有错误时允许)
|
||||||
|
const canClose = computed(() => migrationError.value !== null)
|
||||||
|
|
||||||
// 执行迁移
|
// 执行迁移
|
||||||
async function handleMigration() {
|
async function handleMigration() {
|
||||||
migrationError.value = null
|
migrationError.value = null
|
||||||
|
migrationPartialSuccess.value = false
|
||||||
const result = await sessionStore.runMigration()
|
const result = await sessionStore.runMigration()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showModal.value = false
|
showModal.value = false
|
||||||
@@ -22,6 +27,17 @@ async function handleMigration() {
|
|||||||
await sessionStore.loadSessions()
|
await sessionStore.loadSessions()
|
||||||
} else {
|
} else {
|
||||||
migrationError.value = result.error || t('home.migration.failed')
|
migrationError.value = result.error || t('home.migration.failed')
|
||||||
|
// 如果有部分成功迁移的数据库,标记为部分成功
|
||||||
|
migrationPartialSuccess.value = true
|
||||||
|
// 重新加载会话列表(成功迁移的会话可以正常使用)
|
||||||
|
await sessionStore.loadSessions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗(仅在有错误时可用)
|
||||||
|
function handleClose() {
|
||||||
|
if (canClose.value) {
|
||||||
|
showModal.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,24 +51,35 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UModal :open="showModal" :ui="{ content: 'max-w-md' }" :prevent-close="true">
|
<UModal :open="showModal" :ui="{ content: 'max-w-md' }" :prevent-close="!canClose">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="p-6 text-center">
|
<div class="p-6 text-center">
|
||||||
<div
|
<div
|
||||||
class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30"
|
class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full"
|
||||||
|
:class="migrationError ? 'bg-amber-100 dark:bg-amber-900/30' : 'bg-blue-100 dark:bg-blue-900/30'"
|
||||||
>
|
>
|
||||||
<UIcon name="i-heroicons-arrow-up-circle" class="h-7 w-7 text-blue-500" />
|
<UIcon
|
||||||
|
:name="migrationError ? 'i-heroicons-exclamation-triangle' : 'i-heroicons-arrow-up-circle'"
|
||||||
|
:class="migrationError ? 'h-7 w-7 text-amber-500' : 'h-7 w-7 text-blue-500'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">{{ t('home.migration.title') }}</h3>
|
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
<p class="mb-3 text-sm text-gray-500 dark:text-gray-400">
|
{{ migrationError ? t('home.migration.partialFailed') : t('home.migration.title') }}
|
||||||
|
</h3>
|
||||||
|
<p v-if="!migrationError" class="mb-3 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{ t('home.migration.description', { count: migrationCount }) }}
|
{{ t('home.migration.description', { count: migrationCount }) }}
|
||||||
<br />
|
<br />
|
||||||
{{ t('home.migration.note') }}
|
{{ t('home.migration.note') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- 升级内容列表 -->
|
<!-- 升级内容列表 -->
|
||||||
<div v-if="pendingMigrations.length > 0" class="mb-4 rounded-lg bg-gray-50 p-3 text-left dark:bg-gray-800">
|
<div
|
||||||
<p class="mb-2 text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('home.migration.upgradeContent') }}</p>
|
v-if="pendingMigrations.length > 0 && !migrationError"
|
||||||
|
class="mb-4 rounded-lg bg-gray-50 p-3 text-left dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<p class="mb-2 text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('home.migration.upgradeContent') }}
|
||||||
|
</p>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
<li v-for="migration in pendingMigrations" :key="migration.version" class="flex items-start gap-2">
|
<li v-for="migration in pendingMigrations" :key="migration.version" class="flex items-start gap-2">
|
||||||
<UIcon name="i-heroicons-check-circle" class="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
|
<UIcon name="i-heroicons-check-circle" class="mt-0.5 h-4 w-4 shrink-0 text-green-500" />
|
||||||
@@ -69,15 +96,42 @@ onMounted(async () => {
|
|||||||
v-if="migrationError"
|
v-if="migrationError"
|
||||||
class="mb-4 rounded-lg bg-red-50 p-3 text-left text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400"
|
class="mb-4 rounded-lg bg-red-50 p-3 text-left text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400"
|
||||||
>
|
>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<UIcon name="i-heroicons-exclamation-circle" class="mt-0.5 h-4 w-4 shrink-0" />
|
<UIcon name="i-heroicons-exclamation-circle" class="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
<span>{{ migrationError }}</span>
|
<span>{{ migrationError }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-xs text-red-500 dark:text-red-400">
|
||||||
|
{{ t('home.migration.errorHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UButton color="primary" size="lg" :loading="isMigrating" class="w-full" @click="handleMigration">
|
<!-- 按钮区域 -->
|
||||||
{{ isMigrating ? t('home.migration.upgrading') : t('home.migration.upgradeNow') }}
|
<div class="flex gap-3">
|
||||||
|
<!-- 有错误时显示关闭按钮 -->
|
||||||
|
<UButton
|
||||||
|
v-if="canClose"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
class="flex-1"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
|
{{ t('home.migration.close') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
||||||
|
<!-- 升级/重试按钮 -->
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
size="lg"
|
||||||
|
:loading="isMigrating"
|
||||||
|
:class="canClose ? 'flex-1' : 'w-full'"
|
||||||
|
@click="handleMigration"
|
||||||
|
>
|
||||||
|
{{ isMigrating ? t('home.migration.upgrading') : migrationError ? t('home.migration.retry') : t('home.migration.upgradeNow') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UModal>
|
</UModal>
|
||||||
|
|||||||
Reference in New Issue
Block a user