🚀 低版本API接口数据迁移到高版本:实战经验分享

44 阅读6分钟

🚀 低版本API接口数据迁移到高版本:实战经验分享

记录一次真实业务场景中的数据迁移过程,从21个接口的批量迁移到自动化脚本的完整实现

📋 前言

最近接手了一个API管理系统的升级项目,需要将旧版本系统中的21个接口配置完整迁移到新系统中。这个过程涉及到两个不同版本系统的数据兼容性处理、自动化迁移脚本编写以及完整的数据校验。今天就来分享一下这次数据迁移的完整经验和实战代码。

🎯 项目背景

系统概况

  • 源系统:API管理系统 v1.2(即将下线)
  • 目标系统:API管理系统 v2.5(新架构)
  • 迁移接口数量:21个
  • 接口类型:物流轨迹、社交媒体、电商平台等多种数据查询接口

迁移挑战

  1. 接口结构差异:新旧系统API数据结构不完全一致
  2. 字段映射:部分字段需要重命名或格式转换
  3. 批量处理:需要自动化完成21个接口的迁移
  4. 数据校验:迁移后需要确保功能完整性

🔧 技术方案设计

整体架构

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   源系统 v1.2    │────▶│  数据迁移脚本    │────▶│   目标系统 v2.5  │
│   (176.56.0.78)  │    │   (Node.js)     │    │   (179.233.0.78) │
└─────────────────┘    └─────────────────┘    └─────────────────┘

迁移流程

  1. 数据提取 → 2. 数据清洗 → 3. 数据转换 → 4. 数据加载

💻 核心代码实现

1. 配置文件设计

// config.js - 安全配置管理
const config = {
  // 源系统配置(示例值)
  source: {
    baseUrl: 'http://your-source-server:10588',
    api: {
      list: '/prod-api/scj/dataapi/list',
      detail: '/prod-api/scj/dataapi/detail'
    },
    auth: {
      token: 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJleGFtcGxlX2tleV9pZCJ9...',
      realm: '202412191656034690EXAMPLE'
    }
  },
  
  // 目标系统配置(示例值)
  target: {
    baseUrl: 'http://your-target-server:30588',
    api: {
      create: '/prod-api/standard/dataapi/create'
    },
    auth: {
      token: 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ0YXJnZXRfa2V5X2lkIn0...',
      realm: '202412191656034690EXAMPLE'
    }
  },
  
  // 公共配置
  common: {
    cookie: 'JSESSIONID=EXAMPLE_SESSION_ID',
    clientId: 'bridge',
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
  },
  
  // 迁移控制
  migration: {
    batchSize: 5, // 每次批量处理数量
    delay: 1000,  // 请求间隔(ms)
    maxRetries: 3 // 失败重试次数
  }
};

2. 核心迁移类

// ApiMigrationEngine.js - 迁移引擎核心
class ApiMigrationEngine {
  constructor(config) {
    this.config = config;
    this.results = {
      total: 0,
      success: 0,
      failed: 0,
      skipped: 0,
      details: []
    };
    this.logger = new MigrationLogger();
  }
  
  /**
   * 主迁移方法
   */
  async migrate() {
    this.logger.start('🚀 API迁移任务开始');
    
    try {
      // 1. 获取源系统接口列表
      const apiList = await this.fetchApiList();
      this.results.total = apiList.length;
      
      // 2. 批量处理接口迁移
      await this.batchProcess(apiList);
      
      // 3. 生成迁移报告
      await this.generateReport();
      
      this.logger.success('🎉 迁移任务完成');
      return this.results;
    } catch (error) {
      this.logger.error(`迁移失败: ${error.message}`);
      throw error;
    }
  }
  
  /**
   * 批量处理接口
   */
  async batchProcess(apiList) {
    for (let i = 0; i < apiList.length; i += this.config.migration.batchSize) {
      const batch = apiList.slice(i, i + this.config.migration.batchSize);
      await this.processBatch(batch, i);
      
      // 批次间延迟
      if (i + this.config.migration.batchSize < apiList.length) {
        await this.delay(this.config.migration.delay);
      }
    }
  }
  
  /**
   * 处理单个批次
   */
  async processBatch(batch, startIndex) {
    const promises = batch.map((api, index) => 
      this.migrateSingleApi(api, startIndex + index + 1)
    );
    
    await Promise.allSettled(promises);
  }
  
  /**
   * 迁移单个API接口
   */
  async migrateSingleApi(api, sequence) {
    const apiId = api.id;
    const apiName = api.name;
    
    this.logger.info(`📦 处理接口 [${sequence}] ${apiName}`, {
      id: apiId,
      type: api.protocol
    });
    
    try {
      // 1. 获取接口详情
      const detail = await this.retryOperation(
        () => this.fetchApiDetail(apiId),
        `获取详情 ${apiName}`
      );
      
      if (!detail) {
        this.recordResult(api, 'skipped', '获取详情失败');
        return;
      }
      
      // 2. 数据清洗与转换
      const cleanedData = this.transformData(detail);
      
      // 3. 创建新接口
      const result = await this.retryOperation(
        () => this.createNewApi(cleanedData),
        `创建接口 ${apiName}`
      );
      
      if (result) {
        this.recordResult(api, 'success');
        this.logger.success(`✅ ${apiName} 迁移成功`);
      } else {
        this.recordResult(api, 'failed', '创建接口失败');
      }
    } catch (error) {
      this.logger.error(`❌ ${apiName} 迁移异常: ${error.message}`);
      this.recordResult(api, 'failed', error.message);
    }
  }
  
  /**
   * 数据转换方法
   */
  transformData(sourceData) {
    // 深拷贝原始数据
    const transformed = JSON.parse(JSON.stringify(sourceData));
    
    // 删除不需要的字段
    delete transformed.id;
    delete transformed.status;
    delete transformed.createTime;
    
    // 字段重命名映射
    const fieldMappings = {
      'reqJson': 'requestParams',
      'respJson': 'responseParams',
      'jarPath': 'jarLocation'
    };
    
    // 执行字段重命名
    Object.keys(fieldMappings).forEach(oldKey => {
      if (transformed[oldKey] !== undefined) {
        transformed[fieldMappings[oldKey]] = transformed[oldKey];
        delete transformed[oldKey];
      }
    });
    
    // 添加新系统必要字段
    transformed.version = '2.0';
    transformed.migrated = true;
    transformed.migrationTime = new Date().toISOString();
    
    return transformed;
  }
  
  // ... 其他辅助方法
}

3. 数据验证模块

// DataValidator.js - 数据校验器
class DataValidator {
  static validateApiData(apiData) {
    const errors = [];
    const warnings = [];
    
    // 必需字段检查
    const requiredFields = ['name', 'protocol', 'path'];
    requiredFields.forEach(field => {
      if (!apiData[field]) {
        errors.push(`缺少必需字段: ${field}`);
      }
    });
    
    // 协议类型验证
    if (!['http', 'https', 'grpc'].includes(apiData.protocol)) {
      errors.push(`不支持的协议类型: ${apiData.protocol}`);
    }
    
    // URL格式验证
    if (apiData.path && !this.isValidUrl(apiData.path)) {
      warnings.push(`路径格式可能无效: ${apiData.path}`);
    }
    
    // 请求参数结构验证
    if (apiData.requestParams && !Array.isArray(apiData.requestParams)) {
      errors.push('requestParams必须是数组格式');
    }
    
    return {
      isValid: errors.length === 0,
      errors,
      warnings
    };
  }
  
  static isValidUrl(string) {
    try {
      new URL(string);
      return true;
    } catch (_) {
      return false;
    }
  }
}

4. 迁移报告生成器

// ReportGenerator.js - 迁移报告
class ReportGenerator {
  static generateHTMLReport(results, startTime, endTime) {
    const duration = endTime - startTime;
    const successRate = ((results.success / results.total) * 100).toFixed(2);
    
    return `
<!DOCTYPE html>
<html>
<head>
    <title>API迁移报告 - ${new Date().toLocaleDateString()}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .header { background: #f5f5f5; padding: 20px; border-radius: 8px; }
        .stats { display: flex; gap: 20px; margin: 20px 0; }
        .stat-card { 
            flex: 1; padding: 20px; border-radius: 8px; 
            text-align: center; font-weight: bold;
        }
        .success { background: #d4edda; color: #155724; }
        .failed { background: #f8d7da; color: #721c24; }
        .skipped { background: #fff3cd; color: #856404; }
        .total { background: #d1ecf1; color: #0c5460; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
        th { background-color: #f2f2f2; }
        .status-success { color: green; }
        .status-failed { color: red; }
        .status-skipped { color: orange; }
    </style>
</head>
<body>
    <div class="header">
        <h1>🎯 API接口迁移报告</h1>
        <p>迁移时间: ${new Date(startTime).toLocaleString()} - ${new Date(endTime).toLocaleString()}</p>
        <p>总耗时: ${(duration / 1000).toFixed(2)} 秒</p>
    </div>
    
    <div class="stats">
        <div class="stat-card total">
            <div class="count">${results.total}</div>
            <div class="label">接口总数</div>
        </div>
        <div class="stat-card success">
            <div class="count">${results.success}</div>
            <div class="label">迁移成功</div>
        </div>
        <div class="stat-card failed">
            <div class="count">${results.failed}</div>
            <div class="label">迁移失败</div>
        </div>
        <div class="stat-card skipped">
            <div class="count">${results.skipped}</div>
            <div class="label">跳过</div>
        </div>
    </div>
    
    <h2>迁移成功率: ${successRate}%</h2>
    
    <h3>详细迁移记录</h3>
    <table>
        <thead>
            <tr>
                <th>序号</th>
                <th>接口名称</th>
                <th>接口ID</th>
                <th>状态</th>
                <th>说明</th>
                <th>协议</th>
                <th>迁移时间</th>
            </tr>
        </thead>
        <tbody>
            ${results.details.map((detail, index) => `
            <tr>
                <td>${index + 1}</td>
                <td>${detail.name}</td>
                <td><code>${detail.id.substring(0, 8)}...</code></td>
                <td class="status-${detail.status}">${this.getStatusText(detail.status)}</td>
                <td>${detail.reason || '迁移完成'}</td>
                <td>${detail.protocol || 'http'}</td>
                <td>${new Date().toLocaleTimeString()}</td>
            </tr>
            `).join('')}
        </tbody>
    </table>
</body>
</html>
    `;
  }
  
  static getStatusText(status) {
    const statusMap = {
      'success': '✅ 成功',
      'failed': '❌ 失败',
      'skipped': '⚠️ 跳过'
    };
    return statusMap[status] || status;
  }
}

🛠️ 实用工具函数

1. 重试机制

// utils/retry.js
async function retryOperation(operation, operationName, maxRetries = 3, delay = 1000) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`${operationName} - 第 ${attempt} 次尝试`);
      return await operation();
    } catch (error) {
      if (attempt === maxRetries) {
        console.error(`${operationName} - 所有重试失败:`, error.message);
        throw error;
      }
      
      console.warn(`${operationName} - 第 ${attempt} 次失败,${delay}ms后重试`);
      await new Promise(resolve => setTimeout(resolve, delay * attempt));
    }
  }
}

2. 进度显示

// utils/progress.js
class ProgressBar {
  constructor(total, width = 40) {
    this.total = total;
    this.width = width;
    this.current = 0;
  }
  
  update(current) {
    this.current = current;
    const percentage = (current / this.total) * 100;
    const filledWidth = Math.round((this.width * current) / this.total);
    const bar = '█'.repeat(filledWidth) + '░'.repeat(this.width - filledWidth);
    
    process.stdout.write(
      `\r${bar} | ${current}/${this.total} (${percentage.toFixed(1)}%)`
    );
    
    if (current === this.total) {
      process.stdout.write('\n');
    }
  }
}

🚦 执行流程

1. 安装依赖

# 创建项目目录
mkdir api-migration-tool
cd api-migration-tool

# 初始化项目
npm init -y

# 安装依赖
npm install axios dotenv
npm install --save-dev @types/node

2. 配置文件

# .env 环境变量配置文件
SOURCE_BASE_URL=http://your-source-server:10588
SOURCE_TOKEN=your_source_token_here
TARGET_BASE_URL=http://your-target-server:30588
TARGET_TOKEN=your_target_token_here

# 其他配置...

3. 运行迁移

# 直接运行
node migrate.js

# 或者使用npm脚本
npm run migrate

# 带参数运行(调试模式)
node migrate.js --debug --dry-run

📊 迁移结果示例

🚀 API迁移任务开始
📊 总共发现 21 个待迁移接口
═══════════════════════════════════════

📦 处理接口 [1] 货车定位查询
   ├── ✅ 获取详情成功
   ├── 🔧 数据转换完成
   ├── ✅ 创建接口成功
   └── 🎉 迁移完成

📦 处理接口 [2] 快递轨迹查询
   ├── ✅ 获取详情成功
   ├── 🔧 数据转换完成
   ├── ⚠️  接口创建失败(重试中...)
   ├── ✅ 第2次重试成功
   └── 🎉 迁移完成

... 处理其他接口 ...

═══════════════════════════════════════
🎉 迁移任务完成!
📈 统计结果:
   • 接口总数: 21
   • 迁移成功: 19 (90.5%)
   • 迁移失败: 1 (4.8%)
   • 跳过: 1 (4.8%)
   • 总耗时: 45.23 秒
📄 详细报告已生成: migration-report-20231220.html

💡 经验总结

成功经验

  1. 提前规划数据结构映射:在开始编码前,先分析新旧系统的数据结构差异
  2. 实现健壮的错误处理:网络请求、数据转换等关键步骤都要有完善的错误处理
  3. 添加重试机制:对于网络不稳定的环境,重试机制能大幅提高成功率
  4. 详细的日志记录:便于排查问题和生成迁移报告

遇到的坑

  1. Token过期问题:脚本运行时间较长时,token可能过期,需要实现自动刷新
  2. 数据格式不一致:某些接口的JSON结构存在嵌套差异,需要特殊处理
  3. 网络延迟影响:批量请求时需要合理设置间隔,避免对服务器造成压力

优化建议

  1. 添加数据验证阶段:迁移前先验证数据完整性
  2. 实现增量迁移:支持只迁移变更的接口
  3. 添加回滚机制:迁移失败时可以回滚到之前状态
  4. 性能优化:对于大量接口,可以采用并行处理

🎯 后续规划

  1. 开发Web管理界面:提供可视化的迁移控制台
  2. 添加API测试功能:迁移后自动测试接口可用性
  3. 支持更多系统版本:扩展到其他系统版本的数据迁移
  4. 性能监控:实时监控迁移进度和资源使用情况

📚 相关资源


作者寄语:数据迁移就像搬新家,前期规划越细致,搬家过程越顺利。记得备份好数据,测试好新环境,一步一步来,总会成功的!✨

如果这篇文章对你有帮助,欢迎点赞、收藏和分享!如果有任何问题或建议,欢迎在评论区留言讨论~