feat(iit): Complete REDCap production deployment on Alibaba Cloud ECS
Summary: - Deploy REDCap 15.8.0 on ECS with Docker CE 26.1.3 - Configure RDS MySQL 8.0 database (redcap_prod) - Setup Nginx reverse proxy with HTTPS/SSL - Domain configured: https://redcap.xunzhengyixue.com/ Documentation: - Add ECS deployment guide - Add deployment info record - Update system status document (v4.5 -> v4.6) Status: REDCap production environment fully operational Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
200
backend/scripts/compare-databases.cjs
Normal file
200
backend/scripts/compare-databases.cjs
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 对比本地数据库和RDS数据库的差异
|
||||
*/
|
||||
|
||||
const { Client } = require('pg');
|
||||
|
||||
// 本地数据库配置
|
||||
const localConfig = {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'ai_clinical_research',
|
||||
user: 'postgres',
|
||||
password: 'postgres123',
|
||||
};
|
||||
|
||||
// RDS数据库配置(测试环境)
|
||||
const rdsConfig = {
|
||||
host: 'pgm-2zex1m2y3r23hdn5xo.pg.rds.aliyuncs.com', // 外网地址
|
||||
port: 5432,
|
||||
database: 'ai_clinical_research_test',
|
||||
user: 'airesearch',
|
||||
password: 'Xibahe@fengzhibo117',
|
||||
connectionTimeoutMillis: 30000,
|
||||
};
|
||||
|
||||
async function getSchemas(client) {
|
||||
const result = await client.query(`
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'pg_temp_1', 'pg_toast_temp_1')
|
||||
ORDER BY schema_name
|
||||
`);
|
||||
return result.rows.map(r => r.schema_name);
|
||||
}
|
||||
|
||||
async function getTables(client) {
|
||||
const result = await client.query(`
|
||||
SELECT table_schema, table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_type = 'BASE TABLE'
|
||||
AND table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY table_schema, table_name
|
||||
`);
|
||||
return result.rows.map(r => `${r.table_schema}.${r.table_name}`);
|
||||
}
|
||||
|
||||
async function getExtensions(client) {
|
||||
const result = await client.query(`
|
||||
SELECT extname, extversion FROM pg_extension ORDER BY extname
|
||||
`);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=' .repeat(60));
|
||||
console.log('数据库对比工具');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
// 连接本地数据库
|
||||
console.log('\n[1] 连接本地数据库...');
|
||||
const localClient = new Client(localConfig);
|
||||
try {
|
||||
await localClient.connect();
|
||||
console.log('✅ 本地数据库连接成功');
|
||||
} catch (err) {
|
||||
console.log('❌ 本地数据库连接失败:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 连接RDS数据库
|
||||
console.log('\n[2] 连接RDS数据库...');
|
||||
const rdsClient = new Client(rdsConfig);
|
||||
try {
|
||||
await rdsClient.connect();
|
||||
console.log('✅ RDS数据库连接成功');
|
||||
} catch (err) {
|
||||
console.log('❌ RDS数据库连接失败:', err.message);
|
||||
await localClient.end();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 对比 Extensions
|
||||
console.log('\n' + '=' .repeat(60));
|
||||
console.log('📦 Extensions 对比');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
const localExtensions = await getExtensions(localClient);
|
||||
const rdsExtensions = await getExtensions(rdsClient);
|
||||
|
||||
console.log('\n本地 Extensions:');
|
||||
localExtensions.forEach(e => console.log(` - ${e.extname} (${e.extversion})`));
|
||||
|
||||
console.log('\nRDS Extensions:');
|
||||
rdsExtensions.forEach(e => console.log(` - ${e.extname} (${e.extversion})`));
|
||||
|
||||
// 对比 Schemas
|
||||
console.log('\n' + '=' .repeat(60));
|
||||
console.log('📁 Schema 对比');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
const localSchemas = await getSchemas(localClient);
|
||||
const rdsSchemas = await getSchemas(rdsClient);
|
||||
|
||||
const schemasOnlyInLocal = localSchemas.filter(s => !rdsSchemas.includes(s));
|
||||
const schemasOnlyInRds = rdsSchemas.filter(s => !localSchemas.includes(s));
|
||||
const commonSchemas = localSchemas.filter(s => rdsSchemas.includes(s));
|
||||
|
||||
console.log(`\n共同 Schemas (${commonSchemas.length}个):`);
|
||||
commonSchemas.forEach(s => console.log(` ✅ ${s}`));
|
||||
|
||||
if (schemasOnlyInLocal.length > 0) {
|
||||
console.log(`\n⚠️ 仅在本地存在的 Schemas (${schemasOnlyInLocal.length}个):`);
|
||||
schemasOnlyInLocal.forEach(s => console.log(` 🔴 ${s}`));
|
||||
}
|
||||
|
||||
if (schemasOnlyInRds.length > 0) {
|
||||
console.log(`\n仅在RDS存在的 Schemas (${schemasOnlyInRds.length}个):`);
|
||||
schemasOnlyInRds.forEach(s => console.log(` 🟡 ${s}`));
|
||||
}
|
||||
|
||||
// 对比 Tables
|
||||
console.log('\n' + '=' .repeat(60));
|
||||
console.log('📊 Table 对比');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
const localTables = await getTables(localClient);
|
||||
const rdsTables = await getTables(rdsClient);
|
||||
|
||||
const tablesOnlyInLocal = localTables.filter(t => !rdsTables.includes(t));
|
||||
const tablesOnlyInRds = rdsTables.filter(t => !localTables.includes(t));
|
||||
|
||||
console.log(`\n本地表总数: ${localTables.length}`);
|
||||
console.log(`RDS表总数: ${rdsTables.length}`);
|
||||
|
||||
if (tablesOnlyInLocal.length > 0) {
|
||||
console.log(`\n⚠️ 仅在本地存在的表 (${tablesOnlyInLocal.length}个) - 需要同步到RDS:`);
|
||||
tablesOnlyInLocal.forEach(t => console.log(` 🔴 ${t}`));
|
||||
} else {
|
||||
console.log('\n✅ 没有仅在本地存在的表');
|
||||
}
|
||||
|
||||
if (tablesOnlyInRds.length > 0) {
|
||||
console.log(`\n仅在RDS存在的表 (${tablesOnlyInRds.length}个):`);
|
||||
tablesOnlyInRds.forEach(t => console.log(` 🟡 ${t}`));
|
||||
}
|
||||
|
||||
// 检查关键表的字段差异
|
||||
console.log('\n' + '=' .repeat(60));
|
||||
console.log('🔍 关键表字段检查');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
// 检查 prompt_templates 表的 knowledge_config 字段
|
||||
const checkColumn = async (client, schema, table, column) => {
|
||||
const result = await client.query(`
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = $1 AND table_name = $2 AND column_name = $3
|
||||
`, [schema, table, column]);
|
||||
return result.rows.length > 0;
|
||||
};
|
||||
|
||||
const localHasKnowledgeConfig = await checkColumn(localClient, 'capability_schema', 'prompt_templates', 'knowledge_config');
|
||||
const rdsHasKnowledgeConfig = await checkColumn(rdsClient, 'capability_schema', 'prompt_templates', 'knowledge_config');
|
||||
|
||||
console.log('\ncapability_schema.prompt_templates.knowledge_config:');
|
||||
console.log(` 本地: ${localHasKnowledgeConfig ? '✅ 存在' : '❌ 不存在'}`);
|
||||
console.log(` RDS: ${rdsHasKnowledgeConfig ? '✅ 存在' : '❌ 不存在'}`);
|
||||
|
||||
// 总结
|
||||
console.log('\n' + '=' .repeat(60));
|
||||
console.log('📋 同步建议');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
const needSync = schemasOnlyInLocal.length > 0 || tablesOnlyInLocal.length > 0 || (localHasKnowledgeConfig && !rdsHasKnowledgeConfig);
|
||||
|
||||
if (needSync) {
|
||||
console.log('\n⚠️ 需要同步以下内容到RDS:');
|
||||
if (schemasOnlyInLocal.length > 0) {
|
||||
console.log(` - ${schemasOnlyInLocal.length} 个 Schema`);
|
||||
}
|
||||
if (tablesOnlyInLocal.length > 0) {
|
||||
console.log(` - ${tablesOnlyInLocal.length} 个表`);
|
||||
}
|
||||
if (localHasKnowledgeConfig && !rdsHasKnowledgeConfig) {
|
||||
console.log(' - prompt_templates.knowledge_config 字段');
|
||||
}
|
||||
console.log('\n建议执行: npx prisma migrate deploy');
|
||||
} else {
|
||||
console.log('\n✅ 数据库结构已同步,无需迁移');
|
||||
}
|
||||
|
||||
} finally {
|
||||
await localClient.end();
|
||||
await rdsClient.end();
|
||||
console.log('\n数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user