Files
akmon/pages/sport/teacher/migration-tool.uvue
2026-01-20 08:04:15 +08:00

379 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>