379 lines
11 KiB
Plaintext
379 lines
11 KiB
Plaintext
<template>
|
||
<view class="migration-container">
|
||
<view class="page-header">
|
||
<text class="page-title">数据库迁移工具</text>
|
||
<text class="page-subtitle">评分标准JSON结构化迁移</text>
|
||
</view>
|
||
|
||
<scroll-view class="scroll-container" direction="vertical">
|
||
<view class="content-wrapper">
|
||
<!-- 迁移状态 -->
|
||
<view class="status-section">
|
||
<view class="section-title">迁移状态</view>
|
||
<view class="status-item" :class="{ 'success': migrationCompleted, 'pending': !migrationCompleted }">
|
||
<text class="status-text">{{ migrationStatus }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 迁移操作 -->
|
||
<view class="action-section">
|
||
<view class="section-title">执行迁移</view>
|
||
<button @click="executeMigration" class="migration-btn" :disabled="isExecuting">
|
||
<text>{{ isExecuting ? '执行中...' : '执行评分标准JSON迁移' }}</text>
|
||
</button>
|
||
<text class="help-text">此操作将把评分标准从字符串格式迁移到JSON格式</text>
|
||
</view>
|
||
|
||
<!-- 验证操作 -->
|
||
<view class="action-section">
|
||
<view class="section-title">验证结果</view>
|
||
<button @click="validateMigration" class="validate-btn" :disabled="isValidating">
|
||
<text>{{ isValidating ? '验证中...' : '验证迁移结果' }}</text>
|
||
</button>
|
||
<text class="help-text">检查迁移是否成功完成</text>
|
||
</view>
|
||
|
||
<!-- 结果显示 -->
|
||
<view v-if="migrationResult" class="result-section">
|
||
<view class="section-title">执行结果</view>
|
||
<view class="result-content">
|
||
<text class="result-text">{{ migrationResult }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 验证结果 -->
|
||
<view v-if="validationResult" class="result-section">
|
||
<view class="section-title">验证结果</view>
|
||
<view class="result-content">
|
||
<text class="result-text">{{ validationResult }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 警告信息 -->
|
||
<view class="warning-section">
|
||
<text class="warning-title">⚠️ 重要说明</text>
|
||
<text class="warning-text">• 迁移会自动创建备份表</text>
|
||
<text class="warning-text">• 确保在非生产环境先测试</text>
|
||
<text class="warning-text">• 迁移完成后可删除此页面</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="uts">
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
isExecuting: false,
|
||
isValidating: false,
|
||
migrationCompleted: false,
|
||
migrationStatus: '等待执行迁移',
|
||
migrationResult: '',
|
||
validationResult: ''
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
async executeMigration() {
|
||
this.isExecuting = true
|
||
this.migrationResult = ''
|
||
this.migrationStatus = '正在执行迁移...'
|
||
|
||
try {
|
||
// 执行迁移SQL脚本
|
||
const migrationSQL = `
|
||
-- 步骤1:创建备份表
|
||
CREATE TABLE IF NOT EXISTS ak_training_projects_backup_20250611 AS
|
||
SELECT * FROM ak_training_projects;
|
||
|
||
-- 步骤2:确保 scoring_criteria 字段为 JSONB 类型
|
||
DO $$
|
||
BEGIN
|
||
IF EXISTS (
|
||
SELECT 1 FROM information_schema.columns
|
||
WHERE table_name = 'ak_training_projects'
|
||
AND column_name = 'scoring_criteria'
|
||
AND data_type != 'jsonb'
|
||
) THEN
|
||
ALTER TABLE ak_training_projects
|
||
ALTER COLUMN scoring_criteria TYPE JSONB
|
||
USING CASE
|
||
WHEN scoring_criteria IS NULL OR scoring_criteria = '' THEN NULL
|
||
WHEN scoring_criteria ~ '^[\\s\\t\\n\\r]*\\{.*\\}[\\s\\t\\n\\r]*$' THEN scoring_criteria::JSONB
|
||
ELSE jsonb_build_object('legacy_text', scoring_criteria)
|
||
END;
|
||
END IF;
|
||
END $$;
|
||
|
||
-- 步骤3:为没有评分标准的项目添加默认JSON结构
|
||
UPDATE ak_training_projects
|
||
SET scoring_criteria = jsonb_build_object(
|
||
'criteria', jsonb_build_array(
|
||
jsonb_build_object(
|
||
'min_score', 90,
|
||
'max_score', 100,
|
||
'description', '优秀:表现卓越,超出预期'
|
||
),
|
||
jsonb_build_object(
|
||
'min_score', 80,
|
||
'max_score', 89,
|
||
'description', '良好:表现良好,符合要求'
|
||
),
|
||
jsonb_build_object(
|
||
'min_score', 70,
|
||
'max_score', 79,
|
||
'description', '及格:基本达标,有待改进'
|
||
),
|
||
jsonb_build_object(
|
||
'min_score', 0,
|
||
'max_score', 69,
|
||
'description', '不及格:未达标准,需要重练'
|
||
)
|
||
),
|
||
'scoring_method', 'comprehensive',
|
||
'weight_distribution', jsonb_build_object(
|
||
'technique', 0.4,
|
||
'effort', 0.3,
|
||
'improvement', 0.3
|
||
)
|
||
)
|
||
WHERE scoring_criteria IS NULL
|
||
OR scoring_criteria = '{}'
|
||
OR jsonb_typeof(scoring_criteria) != 'object'
|
||
OR NOT (scoring_criteria ? 'criteria');
|
||
|
||
-- 步骤4:创建索引
|
||
CREATE INDEX IF NOT EXISTS idx_ak_training_projects_scoring_criteria
|
||
ON ak_training_projects USING GIN (scoring_criteria);
|
||
`
|
||
|
||
// 使用RPC执行SQL
|
||
const result = await supa.rpc('exec_sql', { sql: migrationSQL })
|
||
|
||
if (result.status >= 200 && result.status < 300) {
|
||
this.migrationCompleted = true
|
||
this.migrationStatus = '迁移执行完成'
|
||
this.migrationResult = '✅ 评分标准JSON迁移执行成功!\n\n已完成:\n• 创建备份表\n• 转换字段类型为JSONB\n• 添加默认评分标准\n• 创建性能索引'
|
||
|
||
uni.showToast({
|
||
title: '迁移成功',
|
||
icon: 'success'
|
||
})
|
||
} else {
|
||
throw new Error(result.error?.message ?? '迁移执行失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('迁移执行失败:', error)
|
||
this.migrationStatus = '迁移执行失败'
|
||
this.migrationResult = `❌ 迁移执行失败:${error}\n\n请手动在数据库中执行 migrate_scoring_criteria_simple.sql 脚本`
|
||
|
||
uni.showToast({
|
||
title: '迁移失败',
|
||
icon: 'error'
|
||
})
|
||
} finally {
|
||
this.isExecuting = false
|
||
}
|
||
},
|
||
|
||
async validateMigration() {
|
||
this.isValidating = true
|
||
this.validationResult = ''
|
||
|
||
try {
|
||
// 验证迁移结果
|
||
const validationSQL = `SELECT
|
||
COUNT(*) as total_projects,
|
||
COUNT(CASE WHEN scoring_criteria ? 'criteria' THEN 1 END) as json_format_count,
|
||
COUNT(CASE WHEN jsonb_typeof(scoring_criteria) = 'object' THEN 1 END) as valid_json_count
|
||
FROM ak_training_projects;`
|
||
|
||
const result = await supa.rpc('exec_sql', { sql: validationSQL })
|
||
const status = result['status'] as number | null
|
||
if (status != null && status >= 200 && status < 300 && result['data'] != null) {
|
||
const data = Array.isArray(result['data']) ? (result['data'] as Array<UTSJSONObject>)[0] : result['data'] as UTSJSONObject
|
||
const totalProjects = (data['total_projects'] as number) ?? 0
|
||
const jsonFormatCount = (data['json_format_count'] as number) ?? 0
|
||
const validJsonCount = (data['valid_json_count'] as number) ?? 0
|
||
|
||
this.validationResult = ` 验证结果:\n\n• 总项目数:${totalProjects}\n• JSON格式项目:${jsonFormatCount}\n• 有效JSON结构:${validJsonCount}\n\n${
|
||
jsonFormatCount === totalProjects && validJsonCount === totalProjects
|
||
? '✅ 所有项目都已成功迁移到JSON格式!'
|
||
: '⚠️ 部分项目可能需要检查'
|
||
}`
|
||
|
||
// 获取示例数据
|
||
const exampleSQL = `
|
||
SELECT title, scoring_criteria->'criteria' as criteria
|
||
FROM ak_training_projects
|
||
WHERE scoring_criteria IS NOT NULL
|
||
LIMIT 2;
|
||
`
|
||
const exampleResult = await supa.rpc('exec_sql', { sql: exampleSQL }) as UTSJSONObject
|
||
const exampleStatus = exampleResult['status'] as number | null
|
||
if (exampleStatus != null && exampleStatus >= 200 && exampleStatus < 300 && exampleResult['data'] != null) {
|
||
this.validationResult += '\n\n 示例数据:\n'
|
||
const examples = Array.isArray(exampleResult['data']) ? (exampleResult['data'] as Array<UTSJSONObject>) : [exampleResult['data'] as UTSJSONObject]
|
||
examples.forEach((item: UTSJSONObject, index: number) => {
|
||
const title = item['title'] as string
|
||
const criteria = item['criteria'] as string
|
||
this.validationResult += `\n${index + 1}. ${title}\n 评分标准:${JSON.stringify(criteria, null, 2)}\n`
|
||
})
|
||
}
|
||
} else {
|
||
throw new Error('验证查询失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('验证失败:', error)
|
||
this.validationResult = `❌ 验证失败:${error}\n\n请手动检查数据库中的 ak_training_projects 表`
|
||
} finally {
|
||
this.isValidating = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.migration-container {
|
||
flex: 1;
|
||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.page-header {
|
||
padding: 40rpx 30rpx 30rpx;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #FFFFFF;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.page-subtitle {
|
||
font-size: 28rpx;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.scroll-container {
|
||
flex: 1;
|
||
}
|
||
|
||
.content-wrapper {
|
||
padding: 30rpx;
|
||
background: #F8FAFC;
|
||
border-radius: 30rpx 30rpx 0 0;
|
||
margin-top: 20rpx;
|
||
min-height: calc(100vh - 160rpx);
|
||
}
|
||
|
||
.status-section,
|
||
.action-section,
|
||
.result-section,
|
||
.warning-section {
|
||
background: #FFFFFF;
|
||
padding: 30rpx;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #1E293B;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.status-item {
|
||
padding: 20rpx;
|
||
border-radius: 15rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.status-item.pending {
|
||
background: #FEF3C7;
|
||
border: 2rpx solid #F59E0B;
|
||
}
|
||
|
||
.status-item.success {
|
||
background: #D1FAE5;
|
||
border: 2rpx solid #10B981;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 28rpx;
|
||
font-weight: 400;
|
||
color: #374151;
|
||
}
|
||
|
||
.migration-btn,
|
||
.validate-btn {
|
||
width: 100%;
|
||
height: 80rpx;
|
||
border-radius: 15rpx;
|
||
font-size: 32rpx;
|
||
font-weight: 400;
|
||
border: none;
|
||
margin-bottom: 15rpx;
|
||
}
|
||
|
||
.migration-btn {
|
||
background-image: linear-gradient(to bottom right, #6366F1, #8B5CF6);
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.validate-btn {
|
||
background-image: linear-gradient(to bottom right, #10B981, #059669);
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.migration-btn:disabled,
|
||
.validate-btn:disabled {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.help-text {
|
||
font-size: 24rpx;
|
||
color: #6B7280;
|
||
text-align: center;
|
||
}
|
||
|
||
.result-content {
|
||
background: #F9FAFB;
|
||
padding: 20rpx;
|
||
border-radius: 15rpx;
|
||
border: 2rpx solid #E5E7EB;
|
||
}
|
||
|
||
.result-text {
|
||
font-size: 26rpx;
|
||
color: #374151;
|
||
line-height: 1.6;
|
||
white-space: pre-line;
|
||
}
|
||
|
||
.warning-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #F59E0B;
|
||
margin-bottom: 15rpx;
|
||
}
|
||
|
||
.warning-text {
|
||
font-size: 26rpx;
|
||
color: #6B7280;
|
||
margin-bottom: 10rpx;
|
||
line-height: 1.5;
|
||
}
|
||
</style>
|