8.1 多平台打包配置

8.1.1 项目配置文件

manifest.json 配置

{
  "name": "MyUniApp",
  "appid": "__UNI__XXXXXXXX",
  "description": "一个跨平台应用",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": false,
  "networkTimeout": {
    "request": 60000,
    "connectSocket": 60000,
    "uploadFile": 60000,
    "downloadFile": 60000
  },
  "debug": false,
  "uniStatistics": {
    "enable": false
  },
  "app-plus": {
    "usingComponents": true,
    "nvueStyleCompiler": "uni-app",
    "compilerVersion": 3,
    "splashscreen": {
      "alwaysShowBeforeRender": true,
      "waiting": true,
      "autoclose": true,
      "delay": 0
    },
    "modules": {},
    "distribute": {
      "android": {
        "permissions": [
          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\" />",
          "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\" />",
          "<uses-permission android:name=\"android.permission.VIBRATE\" />",
          "<uses-permission android:name=\"android.permission.READ_LOGS\" />",
          "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />",
          "<uses-feature android:name=\"android.hardware.camera.autofocus\" />",
          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />",
          "<uses-permission android:name=\"android.permission.CAMERA\" />",
          "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\" />",
          "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />",
          "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\" />",
          "<uses-permission android:name=\"android.permission.WAKE_LOCK\" />",
          "<uses-permission android:name=\"android.permission.FLASHLIGHT\" />",
          "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\" />"
        ],
        "abiFilters": ["armeabi-v7a", "arm64-v8a"],
        "targetSdkVersion": 30,
        "minSdkVersion": 21
      },
      "ios": {
        "deploymentTarget": "9.0",
        "dSYMs": false
      },
      "sdkConfigs": {
        "ad": {},
        "oauth": {},
        "payment": {},
        "push": {},
        "share": {},
        "statics": {}
      }
    }
  },
  "quickapp": {},
  "mp-weixin": {
    "appid": "wxXXXXXXXXXXXXXXXX",
    "setting": {
      "urlCheck": false,
      "es6": true,
      "enhance": true,
      "postcss": true,
      "preloadBackgroundData": false,
      "minified": true,
      "newFeature": false,
      "coverView": true,
      "nodeModules": false,
      "autoAudits": false,
      "showShadowRootInWxmlPanel": true,
      "scopeDataCheck": false,
      "uglifyFileName": false,
      "checkInvalidKey": true,
      "checkSiteMap": true,
      "uploadWithSourceMap": true,
      "compileHotReLoad": false,
      "lazyloadPlaceholderEnable": false,
      "useMultiFrameRuntime": true,
      "useApiHook": true,
      "useApiHostProcess": true,
      "babelSetting": {
        "ignore": [],
        "disablePlugins": [],
        "outputPath": ""
      },
      "enableEngineNative": false,
      "useIsolateContext": true,
      "userConfirmedBundleSwitch": false,
      "packNpmManually": false,
      "packNpmRelationList": [],
      "minifyWXSS": true,
      "disableUseStrict": false,
      "minifyWXML": true
    },
    "usingComponents": true,
    "permission": {
      "scope.userLocation": {
        "desc": "你的位置信息将用于小程序位置接口的效果展示"
      }
    },
    "requiredPrivateInfos": ["getLocation"]
  },
  "mp-alipay": {
    "usingComponents": true
  },
  "mp-baidu": {
    "usingComponents": true
  },
  "mp-toutiao": {
    "usingComponents": true
  },
  "uniStatistics": {
    "enable": false
  },
  "vueVersion": "3"
}

pages.json 配置

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页"
      }
    },
    {
      "path": "pages/user/user",
      "style": {
        "navigationBarTitleText": "个人中心"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "MyUniApp",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  },
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#3cc51f",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [
      {
        "pagePath": "pages/index/index",
        "iconPath": "static/tab-home.png",
        "selectedIconPath": "static/tab-home-current.png",
        "text": "首页"
      },
      {
        "pagePath": "pages/user/user",
        "iconPath": "static/tab-user.png",
        "selectedIconPath": "static/tab-user-current.png",
        "text": "我的"
      }
    ]
  },
  "condition": {
    "current": 0,
    "list": [
      {
        "name": "首页",
        "path": "pages/index/index",
        "query": ""
      }
    ]
  }
 }

8.4 版本管理和发布策略

8.4.1 语义化版本控制

// utils/versionManager.js
class VersionManager {
  constructor() {
    this.versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:-(\w+)(?:\.(\d+))?)?$/;
    this.packageJsonPath = 'package.json';
    this.manifestJsonPath = 'manifest.json';
  }
  
  // 解析版本号
  parseVersion(version) {
    const match = version.match(this.versionPattern);
    if (!match) {
      throw new Error(`无效的版本号格式: ${version}`);
    }
    
    return {
      major: parseInt(match[1]),
      minor: parseInt(match[2]),
      patch: parseInt(match[3]),
      prerelease: match[4] || null,
      prereleaseNumber: match[5] ? parseInt(match[5]) : null,
      raw: version
    };
  }
  
  // 比较版本号
  compareVersions(version1, version2) {
    const v1 = this.parseVersion(version1);
    const v2 = this.parseVersion(version2);
    
    // 比较主版本号
    if (v1.major !== v2.major) {
      return v1.major - v2.major;
    }
    
    // 比较次版本号
    if (v1.minor !== v2.minor) {
      return v1.minor - v2.minor;
    }
    
    // 比较修订版本号
    if (v1.patch !== v2.patch) {
      return v1.patch - v2.patch;
    }
    
    // 比较预发布版本
    if (v1.prerelease && !v2.prerelease) return -1;
    if (!v1.prerelease && v2.prerelease) return 1;
    if (v1.prerelease && v2.prerelease) {
      const prereleaseOrder = { alpha: 1, beta: 2, rc: 3 };
      const order1 = prereleaseOrder[v1.prerelease] || 0;
      const order2 = prereleaseOrder[v2.prerelease] || 0;
      
      if (order1 !== order2) {
        return order1 - order2;
      }
      
      if (v1.prereleaseNumber !== v2.prereleaseNumber) {
        return (v1.prereleaseNumber || 0) - (v2.prereleaseNumber || 0);
      }
    }
    
    return 0;
  }
  
  // 增加版本号
  bumpVersion(currentVersion, type = 'patch', prerelease = null) {
    const version = this.parseVersion(currentVersion);
    
    switch (type) {
      case 'major':
        version.major++;
        version.minor = 0;
        version.patch = 0;
        version.prerelease = null;
        version.prereleaseNumber = null;
        break;
        
      case 'minor':
        version.minor++;
        version.patch = 0;
        version.prerelease = null;
        version.prereleaseNumber = null;
        break;
        
      case 'patch':
        version.patch++;
        version.prerelease = null;
        version.prereleaseNumber = null;
        break;
        
      case 'prerelease':
        if (version.prerelease === prerelease) {
          version.prereleaseNumber = (version.prereleaseNumber || 0) + 1;
        } else {
          version.prerelease = prerelease;
          version.prereleaseNumber = 1;
        }
        break;
        
      default:
        throw new Error(`未知的版本类型: ${type}`);
    }
    
    return this.formatVersion(version);
  }
  
  // 格式化版本号
  formatVersion(version) {
    let versionString = `${version.major}.${version.minor}.${version.patch}`;
    
    if (version.prerelease) {
      versionString += `-${version.prerelease}`;
      if (version.prereleaseNumber) {
        versionString += `.${version.prereleaseNumber}`;
      }
    }
    
    return versionString;
  }
  
  // 获取当前版本
  getCurrentVersion() {
    const fs = require('fs');
    const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8'));
    return packageJson.version;
  }
  
  // 更新版本号
  updateVersion(newVersion) {
    const fs = require('fs');
    
    // 更新 package.json
    const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8'));
    packageJson.version = newVersion;
    fs.writeFileSync(this.packageJsonPath, JSON.stringify(packageJson, null, 2));
    
    // 更新 manifest.json
    if (fs.existsSync(this.manifestJsonPath)) {
      const manifestJson = JSON.parse(fs.readFileSync(this.manifestJsonPath, 'utf8'));
      manifestJson.versionName = newVersion;
      
      // 更新 versionCode (用于 Android)
      const version = this.parseVersion(newVersion);
      manifestJson.versionCode = version.major * 10000 + version.minor * 100 + version.patch;
      
      fs.writeFileSync(this.manifestJsonPath, JSON.stringify(manifestJson, null, 2));
    }
    
    console.log(`版本已更新为: ${newVersion}`);
  }
  
  // 创建发布标签
  createReleaseTag(version, message = null) {
    const { execSync } = require('child_process');
    
    const tagName = `v${version}`;
    const tagMessage = message || `Release ${version}`;
    
    try {
      // 创建标签
      execSync(`git tag -a ${tagName} -m "${tagMessage}"`, { stdio: 'inherit' });
      
      // 推送标签
      execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
      
      console.log(`发布标签已创建: ${tagName}`);
    } catch (error) {
      console.error('创建发布标签失败:', error.message);
      throw error;
    }
  }
  
  // 生成变更日志
  generateChangelog(fromVersion, toVersion) {
    const { execSync } = require('child_process');
    
    try {
      const fromTag = fromVersion ? `v${fromVersion}` : '';
      const toTag = `v${toVersion}`;
      
      const gitLogCommand = fromTag 
        ? `git log ${fromTag}..${toTag} --pretty=format:"%h %s" --no-merges`
        : `git log ${toTag} --pretty=format:"%h %s" --no-merges`;
      
      const commits = execSync(gitLogCommand, { encoding: 'utf8' })
        .split('\n')
        .filter(line => line.trim());
      
      const changelog = {
        version: toVersion,
        date: new Date().toISOString().split('T')[0],
        changes: this.categorizeCommits(commits)
      };
      
      return changelog;
    } catch (error) {
      console.error('生成变更日志失败:', error.message);
      return null;
    }
  }
  
  // 分类提交记录
  categorizeCommits(commits) {
    const categories = {
      features: [],
      fixes: [],
      improvements: [],
      others: []
    };
    
    commits.forEach(commit => {
      const [hash, ...messageParts] = commit.split(' ');
      const message = messageParts.join(' ');
      
      if (message.match(/^(feat|feature)/i)) {
        categories.features.push({ hash, message });
      } else if (message.match(/^(fix|bugfix)/i)) {
        categories.fixes.push({ hash, message });
      } else if (message.match(/^(improve|enhancement|perf)/i)) {
        categories.improvements.push({ hash, message });
      } else {
        categories.others.push({ hash, message });
      }
    });
    
    return categories;
  }
  
  // 格式化变更日志
  formatChangelog(changelog) {
    let output = `## ${changelog.version} (${changelog.date})\n\n`;
    
    if (changelog.changes.features.length > 0) {
      output += '### ✨ 新功能\n\n';
      changelog.changes.features.forEach(item => {
        output += `- ${item.message} (${item.hash})\n`;
      });
      output += '\n';
    }
    
    if (changelog.changes.fixes.length > 0) {
      output += '### 🐛 问题修复\n\n';
      changelog.changes.fixes.forEach(item => {
        output += `- ${item.message} (${item.hash})\n`;
      });
      output += '\n';
    }
    
    if (changelog.changes.improvements.length > 0) {
      output += '### 🚀 性能优化\n\n';
      changelog.changes.improvements.forEach(item => {
        output += `- ${item.message} (${item.hash})\n`;
      });
      output += '\n';
    }
    
    if (changelog.changes.others.length > 0) {
      output += '### 📝 其他变更\n\n';
      changelog.changes.others.forEach(item => {
        output += `- ${item.message} (${item.hash})\n`;
      });
      output += '\n';
    }
    
    return output;
  }
  
  // 发布流程
  async release(type = 'patch', prerelease = null, message = null) {
    const currentVersion = this.getCurrentVersion();
    const newVersion = this.bumpVersion(currentVersion, type, prerelease);
    
    console.log(`准备发布版本: ${currentVersion} -> ${newVersion}`);
    
    try {
      // 1. 更新版本号
      this.updateVersion(newVersion);
      
      // 2. 生成变更日志
      const changelog = this.generateChangelog(currentVersion, newVersion);
      if (changelog) {
        const changelogText = this.formatChangelog(changelog);
        console.log('\n变更日志:');
        console.log(changelogText);
        
        // 更新 CHANGELOG.md
        this.updateChangelogFile(changelogText);
      }
      
      // 3. 提交变更
      const { execSync } = require('child_process');
      execSync('git add .', { stdio: 'inherit' });
      execSync(`git commit -m "chore: release ${newVersion}"`, { stdio: 'inherit' });
      
      // 4. 创建标签
      this.createReleaseTag(newVersion, message);
      
      console.log(`\n✅ 版本 ${newVersion} 发布成功!`);
      
    } catch (error) {
      console.error('发布失败:', error.message);
      throw error;
    }
  }
  
  // 更新变更日志文件
  updateChangelogFile(newChangelog) {
    const fs = require('fs');
    const changelogPath = 'CHANGELOG.md';
    
    let existingChangelog = '';
    if (fs.existsSync(changelogPath)) {
      existingChangelog = fs.readFileSync(changelogPath, 'utf8');
    } else {
      existingChangelog = '# 变更日志\n\n';
    }
    
    // 在现有变更日志前插入新的变更
    const lines = existingChangelog.split('\n');
    const headerIndex = lines.findIndex(line => line.startsWith('# '));
    
    if (headerIndex !== -1) {
      lines.splice(headerIndex + 1, 0, '', newChangelog.trim());
    } else {
      lines.unshift(newChangelog.trim(), '');
    }
    
    fs.writeFileSync(changelogPath, lines.join('\n'));
  }
}

export default VersionManager;

8.4.2 分支管理策略

// utils/branchManager.js
class BranchManager {
  constructor() {
    this.branches = {
      main: 'main',
      develop: 'develop',
      feature: 'feature/',
      release: 'release/',
      hotfix: 'hotfix/'
    };
  }
  
  // 获取当前分支
  getCurrentBranch() {
    const { execSync } = require('child_process');
    return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
  }
  
  // 检查分支是否存在
  branchExists(branchName) {
    const { execSync } = require('child_process');
    try {
      execSync(`git rev-parse --verify ${branchName}`, { stdio: 'ignore' });
      return true;
    } catch {
      return false;
    }
  }
  
  // 创建功能分支
  createFeatureBranch(featureName) {
    const { execSync } = require('child_process');
    const branchName = `${this.branches.feature}${featureName}`;
    
    if (this.branchExists(branchName)) {
      throw new Error(`分支 ${branchName} 已存在`);
    }
    
    try {
      // 切换到 develop 分支
      execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
      
      // 拉取最新代码
      execSync('git pull origin develop', { stdio: 'inherit' });
      
      // 创建并切换到功能分支
      execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
      
      console.log(`功能分支 ${branchName} 创建成功`);
      return branchName;
    } catch (error) {
      console.error('创建功能分支失败:', error.message);
      throw error;
    }
  }
  
  // 完成功能分支
  finishFeatureBranch(featureName) {
    const { execSync } = require('child_process');
    const branchName = `${this.branches.feature}${featureName}`;
    
    if (!this.branchExists(branchName)) {
      throw new Error(`分支 ${branchName} 不存在`);
    }
    
    try {
      // 切换到功能分支
      execSync(`git checkout ${branchName}`, { stdio: 'inherit' });
      
      // 推送功能分支
      execSync(`git push origin ${branchName}`, { stdio: 'inherit' });
      
      // 切换到 develop 分支
      execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
      
      // 拉取最新代码
      execSync('git pull origin develop', { stdio: 'inherit' });
      
      // 合并功能分支
      execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
      
      // 推送合并结果
      execSync('git push origin develop', { stdio: 'inherit' });
      
      // 删除本地功能分支
      execSync(`git branch -d ${branchName}`, { stdio: 'inherit' });
      
      // 删除远程功能分支
      execSync(`git push origin --delete ${branchName}`, { stdio: 'inherit' });
      
      console.log(`功能分支 ${branchName} 已完成并合并到 develop`);
    } catch (error) {
      console.error('完成功能分支失败:', error.message);
      throw error;
    }
  }
  
  // 创建发布分支
  createReleaseBranch(version) {
    const { execSync } = require('child_process');
    const branchName = `${this.branches.release}${version}`;
    
    if (this.branchExists(branchName)) {
      throw new Error(`分支 ${branchName} 已存在`);
    }
    
    try {
      // 切换到 develop 分支
      execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
      
      // 拉取最新代码
      execSync('git pull origin develop', { stdio: 'inherit' });
      
      // 创建并切换到发布分支
      execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
      
      // 更新版本号
      const versionManager = new (require('./versionManager.js').default)();
      versionManager.updateVersion(version);
      
      // 提交版本更新
      execSync('git add .', { stdio: 'inherit' });
      execSync(`git commit -m "chore: bump version to ${version}"`, { stdio: 'inherit' });
      
      // 推送发布分支
      execSync(`git push origin ${branchName}`, { stdio: 'inherit' });
      
      console.log(`发布分支 ${branchName} 创建成功`);
      return branchName;
    } catch (error) {
      console.error('创建发布分支失败:', error.message);
      throw error;
    }
  }
  
  // 完成发布分支
  finishReleaseBranch(version) {
    const { execSync } = require('child_process');
    const branchName = `${this.branches.release}${version}`;
    
    if (!this.branchExists(branchName)) {
      throw new Error(`分支 ${branchName} 不存在`);
    }
    
    try {
      // 切换到发布分支
      execSync(`git checkout ${branchName}`, { stdio: 'inherit' });
      
      // 合并到 main 分支
      execSync(`git checkout ${this.branches.main}`, { stdio: 'inherit' });
      execSync('git pull origin main', { stdio: 'inherit' });
      execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
      
      // 创建发布标签
      const versionManager = new (require('./versionManager.js').default)();
      versionManager.createReleaseTag(version);
      
      // 推送 main 分支
      execSync('git push origin main', { stdio: 'inherit' });
      
      // 合并回 develop 分支
      execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
      execSync('git pull origin develop', { stdio: 'inherit' });
      execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
      execSync('git push origin develop', { stdio: 'inherit' });
      
      // 删除发布分支
      execSync(`git branch -d ${branchName}`, { stdio: 'inherit' });
      execSync(`git push origin --delete ${branchName}`, { stdio: 'inherit' });
      
      console.log(`发布分支 ${branchName} 已完成`);
    } catch (error) {
      console.error('完成发布分支失败:', error.message);
      throw error;
    }
  }
  
  // 创建热修复分支
  createHotfixBranch(version) {
    const { execSync } = require('child_process');
    const branchName = `${this.branches.hotfix}${version}`;
    
    if (this.branchExists(branchName)) {
      throw new Error(`分支 ${branchName} 已存在`);
    }
    
    try {
      // 切换到 main 分支
      execSync(`git checkout ${this.branches.main}`, { stdio: 'inherit' });
      
      // 拉取最新代码
      execSync('git pull origin main', { stdio: 'inherit' });
      
      // 创建并切换到热修复分支
      execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
      
      console.log(`热修复分支 ${branchName} 创建成功`);
      return branchName;
    } catch (error) {
      console.error('创建热修复分支失败:', error.message);
      throw error;
    }
  }
  
  // 完成热修复分支
  finishHotfixBranch(version) {
    const { execSync } = require('child_process');
    const branchName = `${this.branches.hotfix}${version}`;
    
    if (!this.branchExists(branchName)) {
      throw new Error(`分支 ${branchName} 不存在`);
    }
    
    try {
      // 切换到热修复分支
      execSync(`git checkout ${branchName}`, { stdio: 'inherit' });
      
      // 更新版本号
      const versionManager = new (require('./versionManager.js').default)();
      versionManager.updateVersion(version);
      
      // 提交版本更新
      execSync('git add .', { stdio: 'inherit' });
      execSync(`git commit -m "chore: bump version to ${version}"`, { stdio: 'inherit' });
      
      // 合并到 main 分支
      execSync(`git checkout ${this.branches.main}`, { stdio: 'inherit' });
      execSync('git pull origin main', { stdio: 'inherit' });
      execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
      
      // 创建发布标签
      versionManager.createReleaseTag(version);
      
      // 推送 main 分支
      execSync('git push origin main', { stdio: 'inherit' });
      
      // 合并回 develop 分支
      execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
      execSync('git pull origin develop', { stdio: 'inherit' });
      execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
      execSync('git push origin develop', { stdio: 'inherit' });
      
      // 删除热修复分支
      execSync(`git branch -d ${branchName}`, { stdio: 'inherit' });
      execSync(`git push origin --delete ${branchName}`, { stdio: 'inherit' });
      
      console.log(`热修复分支 ${branchName} 已完成`);
    } catch (error) {
      console.error('完成热修复分支失败:', error.message);
      throw error;
    }
  }
}

export default BranchManager;

8.5 应用商店上架指南

8.5.1 Android 应用商店

Google Play Store

// utils/playStoreUploader.js
class PlayStoreUploader {
  constructor(config) {
    this.config = {
      packageName: config.packageName,
      serviceAccountKey: config.serviceAccountKey,
      apkPath: config.apkPath,
      track: config.track || 'internal', // internal, alpha, beta, production
      ...config
    };
  }
  
  // 上传 APK 到 Google Play
  async uploadApk() {
    const { google } = require('googleapis');
    const fs = require('fs');
    
    try {
      // 认证
      const auth = new google.auth.GoogleAuth({
        keyFile: this.config.serviceAccountKey,
        scopes: ['https://www.googleapis.com/auth/androidpublisher']
      });
      
      const androidpublisher = google.androidpublisher({
        version: 'v3',
        auth
      });
      
      // 创建编辑会话
      const editResponse = await androidpublisher.edits.insert({
        packageName: this.config.packageName
      });
      
      const editId = editResponse.data.id;
      
      // 上传 APK
      const apkResponse = await androidpublisher.edits.apks.upload({
        packageName: this.config.packageName,
        editId,
        media: {
          mimeType: 'application/vnd.android.package-archive',
          body: fs.createReadStream(this.config.apkPath)
        }
      });
      
      const versionCode = apkResponse.data.versionCode;
      
      // 分配到轨道
      await androidpublisher.edits.tracks.update({
        packageName: this.config.packageName,
        editId,
        track: this.config.track,
        requestBody: {
          releases: [{
            versionCodes: [versionCode],
            status: 'completed'
          }]
        }
      });
      
      // 提交编辑
      await androidpublisher.edits.commit({
        packageName: this.config.packageName,
        editId
      });
      
      console.log(`APK 已成功上传到 ${this.config.track} 轨道`);
      return { success: true, versionCode };
      
    } catch (error) {
      console.error('上传 APK 失败:', error.message);
      throw error;
    }
  }
  
  // 更新应用信息
  async updateListing(listingData) {
    const { google } = require('googleapis');
    
    try {
      const auth = new google.auth.GoogleAuth({
        keyFile: this.config.serviceAccountKey,
        scopes: ['https://www.googleapis.com/auth/androidpublisher']
      });
      
      const androidpublisher = google.androidpublisher({
        version: 'v3',
        auth
      });
      
      // 创建编辑会话
      const editResponse = await androidpublisher.edits.insert({
        packageName: this.config.packageName
      });
      
      const editId = editResponse.data.id;
      
      // 更新应用信息
      for (const [language, data] of Object.entries(listingData)) {
        await androidpublisher.edits.listings.update({
          packageName: this.config.packageName,
          editId,
          language,
          requestBody: data
        });
      }
      
      // 提交编辑
      await androidpublisher.edits.commit({
        packageName: this.config.packageName,
        editId
      });
      
      console.log('应用信息已更新');
      
    } catch (error) {
      console.error('更新应用信息失败:', error.message);
      throw error;
    }
  }
  
  // 获取发布状态
  async getReleaseStatus() {
    const { google } = require('googleapis');
    
    try {
      const auth = new google.auth.GoogleAuth({
        keyFile: this.config.serviceAccountKey,
        scopes: ['https://www.googleapis.com/auth/androidpublisher']
      });
      
      const androidpublisher = google.androidpublisher({
        version: 'v3',
        auth
      });
      
      const response = await androidpublisher.edits.tracks.list({
        packageName: this.config.packageName
      });
      
      return response.data.tracks;
      
    } catch (error) {
      console.error('获取发布状态失败:', error.message);
      throw error;
    }
  }
}

export default PlayStoreUploader;

华为应用市场

// utils/huaweiStoreUploader.js
class HuaweiStoreUploader {
  constructor(config) {
    this.config = {
      clientId: config.clientId,
      clientSecret: config.clientSecret,
      appId: config.appId,
      apkPath: config.apkPath,
      baseUrl: 'https://connect-api.cloud.huawei.com/api/publish/v2',
      ...config
    };
    
    this.accessToken = null;
  }
  
  // 获取访问令牌
  async getAccessToken() {
    const axios = require('axios');
    
    try {
      const response = await axios.post(
        'https://oauth-login.cloud.huawei.com/oauth2/v3/token',
        {
          grant_type: 'client_credentials',
          client_id: this.config.clientId,
          client_secret: this.config.clientSecret
        },
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        }
      );
      
      this.accessToken = response.data.access_token;
      return this.accessToken;
      
    } catch (error) {
      console.error('获取访问令牌失败:', error.message);
      throw error;
    }
  }
  
  // 上传 APK
  async uploadApk() {
    const axios = require('axios');
    const FormData = require('form-data');
    const fs = require('fs');
    
    if (!this.accessToken) {
      await this.getAccessToken();
    }
    
    try {
      const formData = new FormData();
      formData.append('file', fs.createReadStream(this.config.apkPath));
      formData.append('appId', this.config.appId);
      
      const response = await axios.post(
        `${this.config.baseUrl}/upload-url`,
        formData,
        {
          headers: {
            ...formData.getHeaders(),
            'Authorization': `Bearer ${this.accessToken}`
          }
        }
      );
      
      console.log('APK 上传成功');
      return response.data;
      
    } catch (error) {
      console.error('上传 APK 失败:', error.message);
      throw error;
    }
  }
  
  // 提交审核
  async submitForReview() {
    const axios = require('axios');
    
    if (!this.accessToken) {
      await this.getAccessToken();
    }
    
    try {
      const response = await axios.post(
        `${this.config.baseUrl}/app-submit`,
        {
          appId: this.config.appId,
          releaseType: 1 // 1: 全量发布, 2: 分阶段发布
        },
        {
          headers: {
            'Authorization': `Bearer ${this.accessToken}`,
            'Content-Type': 'application/json'
          }
        }
      );
      
      console.log('已提交审核');
      return response.data;
      
    } catch (error) {
      console.error('提交审核失败:', error.message);
      throw error;
    }
  }
}

export default HuaweiStoreUploader;

8.5.2 iOS App Store

// utils/appStoreUploader.js
class AppStoreUploader {
  constructor(config) {
    this.config = {
      apiKey: config.apiKey,
      apiKeyId: config.apiKeyId,
      issuerId: config.issuerId,
      bundleId: config.bundleId,
      ipaPath: config.ipaPath,
      ...config
    };
  }
  
  // 生成 JWT 令牌
  generateJWT() {
    const jwt = require('jsonwebtoken');
    const fs = require('fs');
    
    const privateKey = fs.readFileSync(this.config.apiKey, 'utf8');
    
    const payload = {
      iss: this.config.issuerId,
      exp: Math.floor(Date.now() / 1000) + (20 * 60), // 20 分钟过期
      aud: 'appstoreconnect-v1'
    };
    
    return jwt.sign(payload, privateKey, {
      algorithm: 'ES256',
      header: {
        kid: this.config.apiKeyId
      }
    });
  }
  
  // 上传 IPA 到 App Store Connect
  async uploadIpa() {
    const { execSync } = require('child_process');
    
    try {
      // 使用 altool 上传
      const command = `xcrun altool --upload-app -f "${this.config.ipaPath}" --type ios --apiKey ${this.config.apiKeyId} --apiIssuer ${this.config.issuerId}`;
      
      execSync(command, { stdio: 'inherit' });
      
      console.log('IPA 上传成功');
      
    } catch (error) {
      console.error('上传 IPA 失败:', error.message);
      throw error;
    }
  }
  
  // 创建新版本
  async createVersion(versionString) {
    const axios = require('axios');
    
    try {
      const token = this.generateJWT();
      
      // 获取应用信息
      const appResponse = await axios.get(
        `https://api.appstoreconnect.apple.com/v1/apps?filter[bundleId]=${this.config.bundleId}`,
        {
          headers: {
            'Authorization': `Bearer ${token}`
          }
        }
      );
      
      const appId = appResponse.data.data[0].id;
      
      // 创建新版本
      const versionResponse = await axios.post(
        'https://api.appstoreconnect.apple.com/v1/appStoreVersions',
        {
          data: {
            type: 'appStoreVersions',
            attributes: {
              platform: 'IOS',
              versionString
            },
            relationships: {
              app: {
                data: {
                  type: 'apps',
                  id: appId
                }
              }
            }
          }
        },
        {
          headers: {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json'
          }
        }
      );
      
      console.log(`版本 ${versionString} 创建成功`);
      return versionResponse.data.data;
      
    } catch (error) {
      console.error('创建版本失败:', error.message);
      throw error;
    }
  }
  
  // 提交审核
  async submitForReview(versionId) {
    const axios = require('axios');
    
    try {
      const token = this.generateJWT();
      
      const response = await axios.post(
        'https://api.appstoreconnect.apple.com/v1/appStoreVersionSubmissions',
        {
          data: {
            type: 'appStoreVersionSubmissions',
            relationships: {
              appStoreVersion: {
                data: {
                  type: 'appStoreVersions',
                  id: versionId
                }
              }
            }
          }
        },
        {
          headers: {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json'
          }
        }
      );
      
      console.log('已提交 App Store 审核');
      return response.data.data;
      
    } catch (error) {
      console.error('提交审核失败:', error.message);
      throw error;
    }
  }
}

export default AppStoreUploader;

8.5.3 小程序平台

微信小程序

// utils/wechatMiniUploader.js
class WechatMiniUploader {
  constructor(config) {
    this.config = {
      appId: config.appId,
      privateKey: config.privateKey,
      projectPath: config.projectPath,
      version: config.version,
      desc: config.desc || '',
      ...config
    };
  }
  
  // 上传小程序代码
  async uploadCode() {
    const ci = require('miniprogram-ci');
    
    try {
      const project = new ci.Project({
        appid: this.config.appId,
        type: 'miniProgram',
        projectPath: this.config.projectPath,
        privateKeyPath: this.config.privateKey,
        ignores: ['node_modules/**/*']
      });
      
      const uploadResult = await ci.upload({
        project,
        version: this.config.version,
        desc: this.config.desc,
        setting: {
          es6: true,
          es7: true,
          minifyJS: true,
          minifyWXML: true,
          minifyWXSS: true,
          minify: true,
          codeProtect: true,
          autoPrefixWXSS: true
        },
        onProgressUpdate: (progress) => {
          console.log(`上传进度: ${progress}%`);
        }
      });
      
      console.log('小程序代码上传成功');
      return uploadResult;
      
    } catch (error) {
      console.error('上传小程序代码失败:', error.message);
      throw error;
    }
  }
  
  // 预览小程序
  async preview() {
    const ci = require('miniprogram-ci');
    
    try {
      const project = new ci.Project({
        appid: this.config.appId,
        type: 'miniProgram',
        projectPath: this.config.projectPath,
        privateKeyPath: this.config.privateKey
      });
      
      const previewResult = await ci.preview({
        project,
        desc: this.config.desc,
        setting: {
          es6: true,
          minifyJS: true,
          minifyWXML: true,
          minifyWXSS: true
        },
        qrcodeFormat: 'image',
        qrcodeOutputDest: './preview-qrcode.jpg',
        onProgressUpdate: (progress) => {
          console.log(`预览生成进度: ${progress}%`);
        }
      });
      
      console.log('小程序预览二维码已生成');
      return previewResult;
      
    } catch (error) {
      console.error('生成预览失败:', error.message);
      throw error;
    }
  }
  
  // 提交审核
  async submitAudit() {
    const axios = require('axios');
    
    try {
      // 获取访问令牌
      const tokenResponse = await this.getAccessToken();
      const accessToken = tokenResponse.access_token;
      
      // 提交审核
      const auditResponse = await axios.post(
        `https://api.weixin.qq.com/wxa/submit_audit?access_token=${accessToken}`,
        {
          item_list: [
            {
              address: 'pages/index/index',
              tag: '首页',
              first_class: '工具',
              second_class: '效率',
              title: '应用首页'
            }
          ]
        }
      );
      
      if (auditResponse.data.errcode === 0) {
        console.log('小程序已提交审核');
        return auditResponse.data;
      } else {
        throw new Error(auditResponse.data.errmsg);
      }
      
    } catch (error) {
      console.error('提交审核失败:', error.message);
      throw error;
    }
  }
  
  // 获取访问令牌
  async getAccessToken() {
    const axios = require('axios');
    
    try {
      const response = await axios.get(
        `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.config.appId}&secret=${this.config.appSecret}`
      );
      
      if (response.data.access_token) {
        return response.data;
      } else {
        throw new Error(response.data.errmsg);
      }
      
    } catch (error) {
      console.error('获取访问令牌失败:', error.message);
      throw error;
    }
  }
}

export default WechatMiniUploader;

支付宝小程序

// utils/alipayMiniUploader.js
class AlipayMiniUploader {
  constructor(config) {
    this.config = {
      appId: config.appId,
      privateKey: config.privateKey,
      toolId: config.toolId,
      projectPath: config.projectPath,
      version: config.version,
      ...config
    };
  }
  
  // 上传小程序
  async uploadMini() {
    const { execSync } = require('child_process');
    
    try {
      // 使用支付宝开发者工具命令行上传
      const command = `miniu upload --project ${this.config.projectPath} --appId ${this.config.appId} --privateKey ${this.config.privateKey} --toolId ${this.config.toolId} --clientType alipay --version ${this.config.version}`;
      
      execSync(command, { stdio: 'inherit' });
      
      console.log('支付宝小程序上传成功');
      
    } catch (error) {
      console.error('上传支付宝小程序失败:', error.message);
      throw error;
    }
  }
  
  // 预览小程序
  async preview() {
    const { execSync } = require('child_process');
    
    try {
      const command = `miniu preview --project ${this.config.projectPath} --appId ${this.config.appId} --privateKey ${this.config.privateKey} --toolId ${this.config.toolId} --clientType alipay`;
      
      execSync(command, { stdio: 'inherit' });
      
      console.log('支付宝小程序预览二维码已生成');
      
    } catch (error) {
      console.error('生成预览失败:', error.message);
      throw error;
    }
  }
}

export default AlipayMiniUploader;

8.6 发布后的维护和监控

8.6.1 应用监控

// utils/appMonitor.js
class AppMonitor {
  constructor(config) {
    this.config = {
      appId: config.appId,
      apiKey: config.apiKey,
      platforms: config.platforms || ['android', 'ios', 'h5'],
      ...config
    };
    
    this.metrics = {
      crashes: [],
      performance: [],
      userBehavior: [],
      errors: []
    };
  }
  
  // 初始化监控
  init() {
    this.setupCrashReporting();
    this.setupPerformanceMonitoring();
    this.setupUserBehaviorTracking();
    this.setupErrorTracking();
  }
  
  // 崩溃监控
  setupCrashReporting() {
    // 全局错误捕获
    if (typeof window !== 'undefined') {
      window.addEventListener('error', (event) => {
        this.reportCrash({
          type: 'javascript_error',
          message: event.message,
          filename: event.filename,
          lineno: event.lineno,
          colno: event.colno,
          stack: event.error?.stack,
          timestamp: Date.now(),
          userAgent: navigator.userAgent,
          url: window.location.href
        });
      });
      
      window.addEventListener('unhandledrejection', (event) => {
        this.reportCrash({
          type: 'unhandled_promise_rejection',
          reason: event.reason,
          timestamp: Date.now(),
          userAgent: navigator.userAgent,
          url: window.location.href
        });
      });
    }
    
    // Vue 错误捕获
    if (typeof uni !== 'undefined') {
      const originalOnError = uni.onError;
      uni.onError = (error) => {
        this.reportCrash({
          type: 'vue_error',
          message: error.message,
          stack: error.stack,
          timestamp: Date.now(),
          platform: uni.getSystemInfoSync().platform
        });
        
        if (originalOnError) {
          originalOnError(error);
        }
      };
    }
  }
  
  // 性能监控
  setupPerformanceMonitoring() {
    // 页面加载性能
    if (typeof window !== 'undefined' && window.performance) {
      window.addEventListener('load', () => {
        setTimeout(() => {
          const timing = window.performance.timing;
          const metrics = {
            type: 'page_load',
            loadTime: timing.loadEventEnd - timing.navigationStart,
            domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
            firstPaint: this.getFirstPaint(),
            timestamp: Date.now(),
            url: window.location.href
          };
          
          this.reportPerformance(metrics);
        }, 0);
      });
    }
    
    // API 请求性能
    this.monitorApiPerformance();
  }
  
  // 用户行为追踪
  setupUserBehaviorTracking() {
    // 页面访问
    this.trackPageView = (pageName, params = {}) => {
      this.reportUserBehavior({
        type: 'page_view',
        pageName,
        params,
        timestamp: Date.now(),
        sessionId: this.getSessionId()
      });
    };
    
    // 用户点击
    this.trackClick = (element, data = {}) => {
      this.reportUserBehavior({
        type: 'click',
        element,
        data,
        timestamp: Date.now(),
        sessionId: this.getSessionId()
      });
    };
    
    // 自定义事件
    this.trackEvent = (eventName, properties = {}) => {
      this.reportUserBehavior({
        type: 'custom_event',
        eventName,
        properties,
        timestamp: Date.now(),
        sessionId: this.getSessionId()
      });
    };
  }
  
  // 错误追踪
  setupErrorTracking() {
    this.trackError = (error, context = {}) => {
      this.reportError({
        type: 'custom_error',
        message: error.message,
        stack: error.stack,
        context,
        timestamp: Date.now(),
        sessionId: this.getSessionId()
      });
    };
  }
  
  // 上报崩溃
  reportCrash(crashData) {
    this.metrics.crashes.push(crashData);
    this.sendToServer('crash', crashData);
  }
  
  // 上报性能数据
  reportPerformance(performanceData) {
    this.metrics.performance.push(performanceData);
    this.sendToServer('performance', performanceData);
  }
  
  // 上报用户行为
  reportUserBehavior(behaviorData) {
    this.metrics.userBehavior.push(behaviorData);
    this.sendToServer('behavior', behaviorData);
  }
  
  // 上报错误
  reportError(errorData) {
    this.metrics.errors.push(errorData);
    this.sendToServer('error', errorData);
  }
  
  // 发送数据到服务器
  async sendToServer(type, data) {
    try {
      const payload = {
        appId: this.config.appId,
        type,
        data,
        deviceInfo: this.getDeviceInfo(),
        appVersion: this.getAppVersion()
      };
      
      // 使用 uni.request 发送数据
      if (typeof uni !== 'undefined') {
        uni.request({
          url: `${this.config.apiEndpoint}/monitor/${type}`,
          method: 'POST',
          header: {
            'Authorization': `Bearer ${this.config.apiKey}`,
            'Content-Type': 'application/json'
          },
          data: payload,
          success: (res) => {
            console.log(`监控数据上报成功: ${type}`);
          },
          fail: (err) => {
            console.error(`监控数据上报失败: ${type}`, err);
            // 存储到本地,稍后重试
            this.storeForRetry(type, payload);
          }
        });
      }
    } catch (error) {
      console.error('发送监控数据失败:', error);
      this.storeForRetry(type, data);
    }
  }
  
  // 获取设备信息
  getDeviceInfo() {
    if (typeof uni !== 'undefined') {
      return uni.getSystemInfoSync();
    }
    
    if (typeof window !== 'undefined') {
      return {
        platform: 'h5',
        userAgent: navigator.userAgent,
        screenWidth: window.screen.width,
        screenHeight: window.screen.height,
        windowWidth: window.innerWidth,
        windowHeight: window.innerHeight
      };
    }
    
    return {};
  }
  
  // 获取应用版本
  getAppVersion() {
    // 从 manifest.json 或 package.json 获取版本信息
    return this.config.version || '1.0.0';
  }
  
  // 获取会话ID
  getSessionId() {
    if (!this.sessionId) {
      this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }
    return this.sessionId;
  }
  
  // 获取首次绘制时间
  getFirstPaint() {
    if (typeof window !== 'undefined' && window.performance) {
      const paintEntries = window.performance.getEntriesByType('paint');
      const firstPaint = paintEntries.find(entry => entry.name === 'first-paint');
      return firstPaint ? firstPaint.startTime : null;
    }
    return null;
  }
  
  // 监控 API 性能
  monitorApiPerformance() {
    // 拦截 uni.request
    if (typeof uni !== 'undefined') {
      const originalRequest = uni.request;
      uni.request = (options) => {
        const startTime = Date.now();
        const originalSuccess = options.success;
        const originalFail = options.fail;
        
        options.success = (res) => {
          const endTime = Date.now();
          this.reportPerformance({
            type: 'api_request',
            url: options.url,
            method: options.method || 'GET',
            duration: endTime - startTime,
            statusCode: res.statusCode,
            success: true,
            timestamp: startTime
          });
          
          if (originalSuccess) {
            originalSuccess(res);
          }
        };
        
        options.fail = (err) => {
          const endTime = Date.now();
          this.reportPerformance({
            type: 'api_request',
            url: options.url,
            method: options.method || 'GET',
            duration: endTime - startTime,
            success: false,
            error: err,
            timestamp: startTime
          });
          
          if (originalFail) {
            originalFail(err);
          }
        };
        
        return originalRequest(options);
      };
    }
  }
  
  // 存储失败的数据以便重试
  storeForRetry(type, data) {
    try {
      const key = `monitor_retry_${type}`;
      let retryData = [];
      
      if (typeof uni !== 'undefined') {
        const stored = uni.getStorageSync(key);
        if (stored) {
          retryData = JSON.parse(stored);
        }
      }
      
      retryData.push({
        data,
        timestamp: Date.now(),
        retryCount: 0
      });
      
      // 限制存储数量
      if (retryData.length > 100) {
        retryData = retryData.slice(-100);
      }
      
      if (typeof uni !== 'undefined') {
        uni.setStorageSync(key, JSON.stringify(retryData));
      }
    } catch (error) {
      console.error('存储重试数据失败:', error);
    }
  }
  
  // 重试发送失败的数据
  async retryFailedData() {
    const types = ['crash', 'performance', 'behavior', 'error'];
    
    for (const type of types) {
      try {
        const key = `monitor_retry_${type}`;
        let retryData = [];
        
        if (typeof uni !== 'undefined') {
          const stored = uni.getStorageSync(key);
          if (stored) {
            retryData = JSON.parse(stored);
          }
        }
        
        if (retryData.length > 0) {
          const successfulItems = [];
          
          for (const item of retryData) {
            if (item.retryCount < 3) {
              try {
                await this.sendToServer(type, item.data);
                successfulItems.push(item);
              } catch (error) {
                item.retryCount++;
              }
            } else {
              // 超过重试次数,丢弃
              successfulItems.push(item);
            }
          }
          
          // 移除成功发送的数据
          const remainingData = retryData.filter(item => !successfulItems.includes(item));
          
          if (typeof uni !== 'undefined') {
            if (remainingData.length > 0) {
              uni.setStorageSync(key, JSON.stringify(remainingData));
            } else {
              uni.removeStorageSync(key);
            }
          }
        }
      } catch (error) {
        console.error(`重试 ${type} 数据失败:`, error);
      }
    }
  }
  
  // 生成监控报告
  generateReport(timeRange = 24 * 60 * 60 * 1000) { // 默认24小时
    const now = Date.now();
    const startTime = now - timeRange;
    
    const filterByTime = (items) => {
      return items.filter(item => item.timestamp >= startTime);
    };
    
    const crashes = filterByTime(this.metrics.crashes);
    const performance = filterByTime(this.metrics.performance);
    const userBehavior = filterByTime(this.metrics.userBehavior);
    const errors = filterByTime(this.metrics.errors);
    
    return {
      timeRange: {
        start: new Date(startTime).toISOString(),
        end: new Date(now).toISOString()
      },
      summary: {
        totalCrashes: crashes.length,
        totalErrors: errors.length,
        totalPageViews: userBehavior.filter(item => item.type === 'page_view').length,
        averageLoadTime: this.calculateAverageLoadTime(performance)
      },
      crashes: this.groupCrashesByType(crashes),
      performance: this.analyzePerformance(performance),
      userBehavior: this.analyzeUserBehavior(userBehavior),
      errors: this.groupErrorsByType(errors)
    };
  }
  
  // 计算平均加载时间
  calculateAverageLoadTime(performanceData) {
    const loadTimes = performanceData
      .filter(item => item.type === 'page_load' && item.loadTime)
      .map(item => item.loadTime);
    
    if (loadTimes.length === 0) return 0;
    
    return loadTimes.reduce((sum, time) => sum + time, 0) / loadTimes.length;
  }
  
  // 按类型分组崩溃
  groupCrashesByType(crashes) {
    const groups = {};
    
    crashes.forEach(crash => {
      const type = crash.type || 'unknown';
      if (!groups[type]) {
        groups[type] = [];
      }
      groups[type].push(crash);
    });
    
    return groups;
  }
  
  // 分析性能数据
  analyzePerformance(performanceData) {
    const apiRequests = performanceData.filter(item => item.type === 'api_request');
    const pageLoads = performanceData.filter(item => item.type === 'page_load');
    
    return {
      apiRequests: {
        total: apiRequests.length,
        successful: apiRequests.filter(item => item.success).length,
        failed: apiRequests.filter(item => !item.success).length,
        averageDuration: apiRequests.length > 0 
          ? apiRequests.reduce((sum, item) => sum + item.duration, 0) / apiRequests.length 
          : 0
      },
      pageLoads: {
        total: pageLoads.length,
        averageLoadTime: pageLoads.length > 0 
          ? pageLoads.reduce((sum, item) => sum + item.loadTime, 0) / pageLoads.length 
          : 0
      }
    };
  }
  
  // 分析用户行为
  analyzeUserBehavior(userBehaviorData) {
    const pageViews = userBehaviorData.filter(item => item.type === 'page_view');
    const clicks = userBehaviorData.filter(item => item.type === 'click');
    const customEvents = userBehaviorData.filter(item => item.type === 'custom_event');
    
    return {
      pageViews: {
        total: pageViews.length,
        uniquePages: [...new Set(pageViews.map(item => item.pageName))].length
      },
      clicks: {
        total: clicks.length
      },
      customEvents: {
        total: customEvents.length,
        uniqueEvents: [...new Set(customEvents.map(item => item.eventName))].length
      }
    };
  }
  
  // 按类型分组错误
  groupErrorsByType(errors) {
    const groups = {};
    
    errors.forEach(error => {
      const type = error.type || 'unknown';
      if (!groups[type]) {
        groups[type] = [];
      }
      groups[type].push(error);
    });
    
    return groups;
  }
}

export default AppMonitor;

8.6.2 用户反馈收集

// utils/feedbackCollector.js
class FeedbackCollector {
  constructor(config) {
    this.config = {
      apiEndpoint: config.apiEndpoint,
      appId: config.appId,
      ...config
    };
  }
  
  // 收集用户反馈
  async collectFeedback(feedback) {
    const feedbackData = {
      appId: this.config.appId,
      userId: feedback.userId,
      type: feedback.type, // bug, suggestion, complaint, praise
      title: feedback.title,
      content: feedback.content,
      rating: feedback.rating,
      screenshots: feedback.screenshots || [],
      deviceInfo: this.getDeviceInfo(),
      appVersion: this.getAppVersion(),
      timestamp: Date.now(),
      contact: feedback.contact
    };
    
    try {
      if (typeof uni !== 'undefined') {
        const result = await new Promise((resolve, reject) => {
          uni.request({
            url: `${this.config.apiEndpoint}/feedback`,
            method: 'POST',
            data: feedbackData,
            success: resolve,
            fail: reject
          });
        });
        
        console.log('反馈提交成功');
        return result;
      }
    } catch (error) {
      console.error('反馈提交失败:', error);
      // 存储到本地,稍后重试
      this.storeFeedbackLocally(feedbackData);
      throw error;
    }
  }
  
  // 本地存储反馈
  storeFeedbackLocally(feedbackData) {
    try {
      const key = 'pending_feedback';
      let pendingFeedback = [];
      
      if (typeof uni !== 'undefined') {
        const stored = uni.getStorageSync(key);
        if (stored) {
          pendingFeedback = JSON.parse(stored);
        }
      }
      
      pendingFeedback.push(feedbackData);
      
      if (typeof uni !== 'undefined') {
        uni.setStorageSync(key, JSON.stringify(pendingFeedback));
      }
    } catch (error) {
      console.error('存储反馈失败:', error);
    }
  }
  
  // 重试发送本地反馈
  async retryPendingFeedback() {
    try {
      const key = 'pending_feedback';
      let pendingFeedback = [];
      
      if (typeof uni !== 'undefined') {
        const stored = uni.getStorageSync(key);
        if (stored) {
          pendingFeedback = JSON.parse(stored);
        }
      }
      
      if (pendingFeedback.length > 0) {
        const successfulItems = [];
        
        for (const feedback of pendingFeedback) {
          try {
            await this.collectFeedback(feedback);
            successfulItems.push(feedback);
          } catch (error) {
            console.error('重试反馈失败:', error);
          }
        }
        
        // 移除成功发送的反馈
        const remainingFeedback = pendingFeedback.filter(item => !successfulItems.includes(item));
        
        if (typeof uni !== 'undefined') {
          if (remainingFeedback.length > 0) {
            uni.setStorageSync(key, JSON.stringify(remainingFeedback));
          } else {
            uni.removeStorageSync(key);
          }
        }
      }
    } catch (error) {
      console.error('重试反馈失败:', error);
    }
  }
  
  // 获取设备信息
  getDeviceInfo() {
    if (typeof uni !== 'undefined') {
      return uni.getSystemInfoSync();
    }
    return {};
  }
  
  // 获取应用版本
  getAppVersion() {
    return this.config.version || '1.0.0';
  }
}

export default FeedbackCollector;

8.7 本章总结

本章详细介绍了 UniApp 应用的打包发布流程,涵盖了从开发到上线的完整过程。

学习要点回顾

  1. 多平台打包配置

    • manifest.json 和 pages.json 配置
    • Android 和 iOS 平台特定配置
    • 小程序平台配置
  2. 应用签名和证书

    • Android 签名流程和工具
    • iOS 证书配置和管理
    • 证书安全管理最佳实践
  3. 发布流程和注意事项

    • 发布前检查清单
    • 自动化构建脚本
    • 持续集成配置
  4. 版本管理和发布策略

    • 语义化版本控制
    • Git 分支管理策略
    • 变更日志生成
  5. 应用商店上架指南

    • Google Play Store 上架流程
    • iOS App Store 上架流程
    • 各大应用商店上架要求
    • 小程序平台发布流程
  6. 发布后的维护和监控

    • 应用监控和错误追踪
    • 用户反馈收集
    • 性能监控和优化

实践练习

  1. 配置多平台打包环境

    • 配置 Android 开发环境
    • 配置 iOS 开发环境
    • 设置小程序开发工具
  2. 实现自动化发布流程

    • 编写构建脚本
    • 配置 CI/CD 流水线
    • 实现版本自动管理
  3. 搭建应用监控系统

    • 集成错误监控
    • 实现性能监控
    • 建立用户反馈机制

常见问题解答

Q: 如何处理不同平台的兼容性问题? A: 使用条件编译、平台特定代码、充分测试各平台功能。

Q: 应用签名证书丢失怎么办? A: 建立证书备份机制,使用密钥管理服务,定期检查证书有效期。

Q: 如何提高应用商店审核通过率? A: 遵循平台规范,完善应用信息,提供清晰的隐私政策,确保功能完整。

Q: 发布后发现严重 Bug 怎么处理? A: 立即修复并发布热修复版本,通知用户更新,建立应急响应机制。

Q: 如何进行灰度发布? A: 使用平台提供的分阶段发布功能,监控关键指标,逐步扩大发布范围。

最佳实践建议

发布准备

  • 建立完整的测试流程
  • 制定发布检查清单
  • 准备回滚方案
  • 建立发布文档

版本管理

  • 使用语义化版本号
  • 维护详细的变更日志
  • 建立分支管理规范
  • 定期清理过期分支

质量保证

  • 实施代码审查
  • 建立自动化测试
  • 进行性能测试
  • 验证安全性

监控运维

  • 建立全面的监控体系
  • 设置关键指标告警
  • 定期分析用户反馈
  • 持续优化用户体验

团队协作

  • 建立发布流程规范
  • 明确角色和职责
  • 建立沟通机制
  • 定期总结和改进

通过本章的学习,你应该能够熟练掌握 UniApp 应用的完整发布流程,建立规范的版本管理和发布体系,确保应用的稳定发布和持续运营。


下一章预告:第九章将介绍 UniApp 的进阶开发技巧,包括原生插件开发、混合开发模式、第三方 SDK 集成等高级主题。

8.1.2 平台特定配置

Android 配置

// utils/androidConfig.js
class AndroidConfig {
  constructor() {
    this.config = {
      // 应用基本信息
      packageName: 'com.example.myuniapp',
      versionName: '1.0.0',
      versionCode: 1,
      
      // 权限配置
      permissions: [
        'android.permission.INTERNET',
        'android.permission.ACCESS_NETWORK_STATE',
        'android.permission.ACCESS_WIFI_STATE',
        'android.permission.CAMERA',
        'android.permission.WRITE_EXTERNAL_STORAGE',
        'android.permission.READ_EXTERNAL_STORAGE',
        'android.permission.ACCESS_FINE_LOCATION',
        'android.permission.ACCESS_COARSE_LOCATION',
        'android.permission.VIBRATE',
        'android.permission.WAKE_LOCK'
      ],
      
      // 签名配置
      signing: {
        keystore: 'android.keystore',
        alias: 'myapp',
        storePassword: 'your_store_password',
        keyPassword: 'your_key_password'
      },
      
      // 构建配置
      build: {
        targetSdkVersion: 30,
        minSdkVersion: 21,
        compileSdkVersion: 30,
        buildToolsVersion: '30.0.3'
      },
      
      // 混淆配置
      proguard: {
        enabled: true,
        rules: [
          '-keep class io.dcloud.** { *; }',
          '-keep class uni.** { *; }',
          '-keep class com.example.myuniapp.** { *; }',
          '-dontwarn okhttp3.**',
          '-dontwarn retrofit2.**'
        ]
      }
    };
  }
  
  // 生成 build.gradle 配置
  generateBuildGradle() {
    return `
apply plugin: 'com.android.application'

android {
    compileSdkVersion ${this.config.build.compileSdkVersion}
    buildToolsVersion "${this.config.build.buildToolsVersion}"
    
    defaultConfig {
        applicationId "${this.config.packageName}"
        minSdkVersion ${this.config.build.minSdkVersion}
        targetSdkVersion ${this.config.build.targetSdkVersion}
        versionCode ${this.config.versionCode}
        versionName "${this.config.versionName}"
        
        multiDexEnabled true
        ndk {
            abiFilters "armeabi-v7a", "arm64-v8a", "x86"
        }
    }
    
    signingConfigs {
        release {
            keyAlias '${this.config.signing.alias}'
            keyPassword '${this.config.signing.keyPassword}'
            storeFile file('${this.config.signing.keystore}')
            storePassword '${this.config.signing.storePassword}'
        }
    }
    
    buildTypes {
        release {
            minifyEnabled ${this.config.proguard.enabled}
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
        debug {
            debuggable true
            jniDebuggable true
        }
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
    implementation 'androidx.multidex:multidex:2.0.1'
}
    `;
  }
  
  // 生成 AndroidManifest.xml
  generateManifest() {
    const permissions = this.config.permissions
      .map(permission => `    <uses-permission android:name="${permission}" />`)
      .join('\n');
    
    return `
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="${this.config.packageName}"
    android:versionCode="${this.config.versionCode}"
    android:versionName="${this.config.versionName}">
    
${permissions}
    
    <application
        android:name="io.dcloud.application.DCloudApplication"
        android:allowBackup="true"
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:theme="@style/TranslucentTheme"
        android:hardwareAccelerated="true"
        android:largeHeap="true">
        
        <activity
            android:name="io.dcloud.PandoraEntry"
            android:configChanges="orientation|keyboardHidden|keyboard|navigation"
            android:label="@string/app_name"
            android:launchMode="singleTask"
            android:screenOrientation="user"
            android:theme="@style/TranslucentTheme"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
    </application>
    
</manifest>
    `;
  }
  
  // 生成混淆规则
  generateProguardRules() {
    return this.config.proguard.rules.join('\n');
  }
}

export default AndroidConfig;

iOS 配置

// utils/iosConfig.js
class IOSConfig {
  constructor() {
    this.config = {
      // 应用基本信息
      bundleId: 'com.example.myuniapp',
      version: '1.0.0',
      buildNumber: '1',
      displayName: 'MyUniApp',
      
      // 部署目标
      deploymentTarget: '9.0',
      
      // 权限配置
      permissions: {
        'NSCameraUsageDescription': '需要访问相机来拍照',
        'NSPhotoLibraryUsageDescription': '需要访问相册来选择图片',
        'NSLocationWhenInUseUsageDescription': '需要获取位置信息',
        'NSLocationAlwaysAndWhenInUseUsageDescription': '需要获取位置信息',
        'NSMicrophoneUsageDescription': '需要访问麦克风来录音',
        'NSContactsUsageDescription': '需要访问通讯录',
        'NSCalendarsUsageDescription': '需要访问日历',
        'NSRemindersUsageDescription': '需要访问提醒事项'
      },
      
      // URL Schemes
      urlSchemes: [
        'myuniapp',
        'weixin',
        'alipay'
      ],
      
      // 应用传输安全
      ats: {
        allowArbitraryLoads: true,
        exceptionDomains: {
          'example.com': {
            exceptionAllowsInsecureHTTPLoads: true,
            exceptionMinimumTLSVersion: 'TLSv1.0'
          }
        }
      },
      
      // 后台模式
      backgroundModes: [
        'background-fetch',
        'background-processing'
      ]
    };
  }
  
  // 生成 Info.plist
  generateInfoPlist() {
    const permissions = Object.entries(this.config.permissions)
      .map(([key, value]) => `    <key>${key}</key>\n    <string>${value}</string>`)
      .join('\n');
    
    const urlSchemes = this.config.urlSchemes
      .map(scheme => `                <string>${scheme}</string>`)
      .join('\n');
    
    const backgroundModes = this.config.backgroundModes
      .map(mode => `        <string>${mode}</string>`)
      .join('\n');
    
    return `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>zh_CN</string>
    <key>CFBundleDisplayName</key>
    <string>${this.config.displayName}</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>${this.config.bundleId}</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>$(PRODUCT_NAME)</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>${this.config.version}</string>
    <key>CFBundleVersion</key>
    <string>${this.config.buildNumber}</string>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    
    <!-- 权限配置 -->
${permissions}
    
    <!-- URL Schemes -->
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLName</key>
            <string>${this.config.bundleId}</string>
            <key>CFBundleURLSchemes</key>
            <array>
${urlSchemes}
            </array>
        </dict>
    </array>
    
    <!-- 应用传输安全 -->
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <${this.config.ats.allowArbitraryLoads}/>
    </dict>
    
    <!-- 后台模式 -->
    <key>UIBackgroundModes</key>
    <array>
${backgroundModes}
    </array>
    
    <!-- 支持的设备方向 -->
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    
    <key>UISupportedInterfaceOrientations~ipad</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    
</dict>
</plist>
    `;
  }
  
  // 生成 Podfile
  generatePodfile() {
    return `
platform :ios, '${this.config.deploymentTarget}'

target 'MyUniApp' do
  use_frameworks!
  
  # UniApp 核心依赖
  pod 'DCUniApp'
  
  # 第三方 SDK
  pod 'AFNetworking', '~> 4.0'
  pod 'SDWebImage', '~> 5.0'
  pod 'MJRefresh', '~> 3.7'
  
  # 微信 SDK
  pod 'WechatOpenSDK'
  
  # 支付宝 SDK
  pod 'AlipaySDK-iOS'
  
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '${this.config.deploymentTarget}'
    end
  end
end
    `;
  }
}

export default IOSConfig;

8.1.3 小程序配置

微信小程序配置

// utils/wechatConfig.js
class WechatConfig {
  constructor() {
    this.config = {
      appid: 'wxXXXXXXXXXXXXXXXX',
      projectname: 'MyUniApp',
      
      // 编译设置
      setting: {
        urlCheck: false,
        es6: true,
        enhance: true,
        postcss: true,
        preloadBackgroundData: false,
        minified: true,
        newFeature: false,
        coverView: true,
        nodeModules: false,
        autoAudits: false,
        showShadowRootInWxmlPanel: true,
        scopeDataCheck: false,
        uglifyFileName: false,
        checkInvalidKey: true,
        checkSiteMap: true,
        uploadWithSourceMap: true,
        compileHotReLoad: false,
        lazyloadPlaceholderEnable: false,
        useMultiFrameRuntime: true,
        useApiHook: true,
        useApiHostProcess: true,
        babelSetting: {
          ignore: [],
          disablePlugins: [],
          outputPath: ''
        },
        enableEngineNative: false,
        useIsolateContext: true,
        userConfirmedBundleSwitch: false,
        packNpmManually: false,
        packNpmRelationList: [],
        minifyWXSS: true,
        disableUseStrict: false,
        minifyWXML: true
      },
      
      // 权限配置
      permission: {
        'scope.userLocation': {
          desc: '你的位置信息将用于小程序位置接口的效果展示'
        },
        'scope.userInfo': {
          desc: '你的用户信息将用于小程序个性化服务'
        },
        'scope.camera': {
          desc: '你的摄像头将用于拍照功能'
        },
        'scope.album': {
          desc: '你的相册将用于选择图片功能'
        }
      },
      
      // 隐私设置
      requiredPrivateInfos: [
        'getLocation',
        'chooseLocation',
        'chooseAddress'
      ],
      
      // 云开发配置
      cloudfunctionRoot: 'cloudfunctions/',
      
      // 分包配置
      subpackages: [
        {
          root: 'pages/user',
          name: 'user',
          pages: [
            'profile/profile',
            'settings/settings'
          ]
        },
        {
          root: 'pages/shop',
          name: 'shop',
          pages: [
            'list/list',
            'detail/detail'
          ]
        }
      ],
      
      // 预下载分包
      preloadRule: {
        'pages/index/index': {
          network: 'all',
          packages: ['user']
        }
      }
    };
  }
  
  // 生成 project.config.json
  generateProjectConfig() {
    return JSON.stringify({
      description: '项目配置文件',
      packOptions: {
        ignore: []
      },
      setting: this.config.setting,
      compileType: 'miniprogram',
      libVersion: '2.19.4',
      appid: this.config.appid,
      projectname: this.config.projectname,
      debugOptions: {
        hidedInDevtools: []
      },
      scripts: {},
      isGameTourist: false,
      simulatorType: 'wechat',
      simulatorPluginLibVersion: {},
      condition: {
        search: {
          list: []
        },
        conversation: {
          list: []
        },
        game: {
          list: []
        },
        plugin: {
          list: []
        },
        gamePlugin: {
          list: []
        },
        miniprogram: {
          list: []
        }
      }
    }, null, 2);
  }
  
  // 生成 sitemap.json
  generateSitemap() {
    return JSON.stringify({
      desc: '关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html',
      rules: [
        {
          action: 'allow',
          page: '*'
        }
      ]
    }, null, 2);
  }
  
  // 生成云函数配置
  generateCloudConfig() {
    return JSON.stringify({
      envId: 'your-env-id',
      functionRoot: './cloudfunctions',
      functions: [
        {
          name: 'login',
          timeout: 5,
          envVariables: {},
          runtime: 'Nodejs12.16'
        },
        {
          name: 'getUser',
          timeout: 5,
          envVariables: {},
          runtime: 'Nodejs12.16'
        }
      ]
    }, null, 2);
  }
}

export default WechatConfig;

8.2 应用签名和证书

8.2.1 Android 签名

生成密钥库

# 生成 Android 密钥库
keytool -genkey -v -keystore android.keystore -alias myapp -keyalg RSA -keysize 2048 -validity 10000

# 查看密钥库信息
keytool -list -v -keystore android.keystore

# 导出证书
keytool -export -alias myapp -keystore android.keystore -file myapp.crt

签名工具类

// utils/androidSigning.js
class AndroidSigning {
  constructor() {
    this.keystoreConfig = {
      keystore: 'android.keystore',
      alias: 'myapp',
      storePassword: '',
      keyPassword: ''
    };
  }
  
  // 设置签名配置
  setSigningConfig(config) {
    this.keystoreConfig = { ...this.keystoreConfig, ...config };
  }
  
  // 验证签名配置
  validateSigningConfig() {
    const required = ['keystore', 'alias', 'storePassword', 'keyPassword'];
    const missing = required.filter(key => !this.keystoreConfig[key]);
    
    if (missing.length > 0) {
      throw new Error(`Missing signing config: ${missing.join(', ')}`);
    }
    
    return true;
  }
  
  // 生成签名配置文件
  generateSigningConfig() {
    this.validateSigningConfig();
    
    return `
signingConfigs {
    release {
        keyAlias '${this.keystoreConfig.alias}'
        keyPassword '${this.keystoreConfig.keyPassword}'
        storeFile file('${this.keystoreConfig.keystore}')
        storePassword '${this.keystoreConfig.storePassword}'
    }
    debug {
        keyAlias 'androiddebugkey'
        keyPassword 'android'
        storeFile file('debug.keystore')
        storePassword 'android'
    }
}
    `;
  }
  
  // 签名 APK
  signApk(apkPath, outputPath) {
    const command = `
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
  -keystore ${this.keystoreConfig.keystore} \
  -storepass ${this.keystoreConfig.storePassword} \
  -keypass ${this.keystoreConfig.keyPassword} \
  ${apkPath} ${this.keystoreConfig.alias}

# 对齐 APK
zipalign -v 4 ${apkPath} ${outputPath}
    `;
    
    return command;
  }
  
  // 验证 APK 签名
  verifyApkSignature(apkPath) {
    return `jarsigner -verify -verbose -certs ${apkPath}`;
  }
  
  // 获取 APK 签名信息
  getApkSignatureInfo(apkPath) {
    return `
# 获取签名信息
aapt dump badging ${apkPath}

# 获取证书指纹
keytool -printcert -jarfile ${apkPath}
    `;
  }
}

export default AndroidSigning;

8.2.2 iOS 证书配置

证书管理工具

// utils/iosCertificate.js
class IOSCertificate {
  constructor() {
    this.config = {
      teamId: 'YOUR_TEAM_ID',
      bundleId: 'com.example.myuniapp',
      
      // 证书类型
      certificateTypes: {
        development: 'iOS Development',
        distribution: 'iOS Distribution',
        adhoc: 'Ad Hoc'
      },
      
      // 描述文件
      provisioningProfiles: {
        development: 'MyApp_Development.mobileprovision',
        distribution: 'MyApp_Distribution.mobileprovision',
        adhoc: 'MyApp_AdHoc.mobileprovision'
      }
    };
  }
  
  // 生成 Xcode 项目配置
  generateXcodeConfig(buildType = 'release') {
    const isRelease = buildType === 'release';
    
    return `
// Xcode Build Settings
CODE_SIGN_IDENTITY = "${isRelease ? this.config.certificateTypes.distribution : this.config.certificateTypes.development}";
DEVELOPMENT_TEAM = "${this.config.teamId}";
PRODUCT_BUNDLE_IDENTIFIER = "${this.config.bundleId}";
PROVISIONING_PROFILE_SPECIFIER = "${isRelease ? this.config.provisioningProfiles.distribution : this.config.provisioningProfiles.development}";
CODE_SIGN_STYLE = Manual;

// 其他设置
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
TARGETED_DEVICE_FAMILY = "1,2";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
    `;
  }
  
  // 生成 ExportOptions.plist
  generateExportOptions(method = 'app-store') {
    const methods = {
      'app-store': 'app-store',
      'ad-hoc': 'ad-hoc',
      'enterprise': 'enterprise',
      'development': 'development'
    };
    
    return `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>${methods[method]}</string>
    <key>teamID</key>
    <string>${this.config.teamId}</string>
    <key>uploadBitcode</key>
    <${method === 'app-store' ? 'true' : 'false'}/>
    <key>uploadSymbols</key>
    <${method === 'app-store' ? 'true' : 'false'}/>
    <key>compileBitcode</key>
    <${method === 'app-store' ? 'true' : 'false'}/>
    <key>stripSwiftSymbols</key>
    <true/>
    <key>thinning</key>
    <string>&lt;none&gt;</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>${this.config.bundleId}</key>
        <string>${this.getProvisioningProfile(method)}</string>
    </dict>
</dict>
</plist>
    `;
  }
  
  // 获取对应的描述文件
  getProvisioningProfile(method) {
    switch (method) {
      case 'app-store':
        return this.config.provisioningProfiles.distribution;
      case 'ad-hoc':
        return this.config.provisioningProfiles.adhoc;
      case 'development':
        return this.config.provisioningProfiles.development;
      default:
        return this.config.provisioningProfiles.distribution;
    }
  }
  
  // 验证证书
  validateCertificate() {
    return `
# 查看已安装的证书
security find-identity -v -p codesigning

# 验证描述文件
security cms -D -i ${this.config.provisioningProfiles.distribution}

# 检查证书有效期
openssl x509 -in certificate.pem -text -noout
    `;
  }
  
  // 自动化签名脚本
  generateSigningScript() {
    return `
#!/bin/bash

# iOS 自动签名脚本
set -e

# 配置变量
PROJECT_PATH="MyUniApp.xcodeproj"
SCHEME="MyUniApp"
CONFIGURATION="Release"
ARCHIVE_PATH="build/MyUniApp.xcarchive"
EXPORT_PATH="build/export"
EXPORT_OPTIONS="ExportOptions.plist"

echo "开始构建 iOS 应用..."

# 清理构建目录
rm -rf build
mkdir -p build

# 构建 Archive
echo "正在创建 Archive..."
xcodebuild -project "$PROJECT_PATH" \
           -scheme "$SCHEME" \
           -configuration "$CONFIGURATION" \
           -archivePath "$ARCHIVE_PATH" \
           archive

# 导出 IPA
echo "正在导出 IPA..."
xcodebuild -exportArchive \
           -archivePath "$ARCHIVE_PATH" \
           -exportPath "$EXPORT_PATH" \
           -exportOptionsPlist "$EXPORT_OPTIONS"

echo "构建完成!IPA 文件位于: $EXPORT_PATH"
    `;
  }
}

export default IOSCertificate;

8.2.3 证书管理最佳实践

// utils/certificateManager.js
class CertificateManager {
  constructor() {
    this.certificates = new Map();
    this.expirationWarningDays = 30;
  }
  
  // 添加证书信息
  addCertificate(platform, type, config) {
    const key = `${platform}_${type}`;
    this.certificates.set(key, {
      platform,
      type,
      ...config,
      addedAt: new Date()
    });
  }
  
  // 检查证书有效期
  checkCertificateExpiration() {
    const warnings = [];
    const now = new Date();
    
    for (const [key, cert] of this.certificates) {
      if (cert.expirationDate) {
        const expirationDate = new Date(cert.expirationDate);
        const daysUntilExpiration = Math.ceil((expirationDate - now) / (1000 * 60 * 60 * 24));
        
        if (daysUntilExpiration <= this.expirationWarningDays) {
          warnings.push({
            platform: cert.platform,
            type: cert.type,
            daysUntilExpiration,
            expirationDate: cert.expirationDate
          });
        }
      }
    }
    
    return warnings;
  }
  
  // 生成证书报告
  generateCertificateReport() {
    const report = {
      totalCertificates: this.certificates.size,
      platforms: {},
      expirationWarnings: this.checkCertificateExpiration(),
      generatedAt: new Date().toISOString()
    };
    
    for (const [key, cert] of this.certificates) {
      if (!report.platforms[cert.platform]) {
        report.platforms[cert.platform] = [];
      }
      
      report.platforms[cert.platform].push({
        type: cert.type,
        status: this.getCertificateStatus(cert),
        expirationDate: cert.expirationDate
      });
    }
    
    return report;
  }
  
  // 获取证书状态
  getCertificateStatus(cert) {
    if (!cert.expirationDate) {
      return 'unknown';
    }
    
    const now = new Date();
    const expirationDate = new Date(cert.expirationDate);
    const daysUntilExpiration = Math.ceil((expirationDate - now) / (1000 * 60 * 60 * 24));
    
    if (daysUntilExpiration < 0) {
      return 'expired';
    } else if (daysUntilExpiration <= this.expirationWarningDays) {
      return 'expiring_soon';
    } else {
      return 'valid';
    }
  }
  
  // 自动续期提醒
  setupRenewalReminders() {
    const checkInterval = 24 * 60 * 60 * 1000; // 每天检查一次
    
    setInterval(() => {
      const warnings = this.checkCertificateExpiration();
      
      if (warnings.length > 0) {
        this.sendRenewalNotification(warnings);
      }
    }, checkInterval);
  }
  
  // 发送续期通知
  sendRenewalNotification(warnings) {
    const message = warnings.map(warning => 
      `${warning.platform} ${warning.type} 证书将在 ${warning.daysUntilExpiration} 天后过期`
    ).join('\n');
    
    console.warn('证书过期提醒:', message);
    
    // 这里可以集成邮件、短信或其他通知服务
    // this.emailService.send({
    //   subject: '证书过期提醒',
    //   body: message
    // });
  }
}

export default CertificateManager;

8.3 发布流程和注意事项

8.3.1 发布前检查清单

// utils/releaseChecker.js
class ReleaseChecker {
  constructor() {
    this.checkItems = {
      code: [
        { name: '代码审查完成', required: true },
        { name: '单元测试通过', required: true },
        { name: '集成测试通过', required: true },
        { name: '性能测试通过', required: false },
        { name: '安全扫描通过', required: true },
        { name: '代码覆盖率达标', required: false }
      ],
      config: [
        { name: '生产环境配置正确', required: true },
        { name: 'API 地址配置正确', required: true },
        { name: '第三方服务配置正确', required: true },
        { name: '调试模式已关闭', required: true },
        { name: '日志级别设置正确', required: true }
      ],
      assets: [
        { name: '应用图标完整', required: true },
        { name: '启动页图片完整', required: true },
        { name: '多分辨率图片准备', required: true },
        { name: '字体文件检查', required: false },
        { name: '音频文件检查', required: false }
      ],
      legal: [
        { name: '隐私政策更新', required: true },
        { name: '用户协议更新', required: true },
        { name: '第三方许可证检查', required: true },
        { name: '版权信息完整', required: true }
      ],
      platform: [
        { name: 'Android 权限声明', required: true },
        { name: 'iOS 权限描述', required: true },
        { name: '小程序类目选择', required: true },
        { name: '应用商店描述', required: true },
        { name: '关键词优化', required: false }
      ]
    };
    
    this.results = {};
  }
  
  // 执行检查
  async runChecks() {
    console.log('开始发布前检查...');
    
    for (const [category, items] of Object.entries(this.checkItems)) {
      console.log(`检查类别: ${category}`);
      this.results[category] = await this.checkCategory(category, items);
    }
    
    return this.generateReport();
  }
  
  // 检查单个类别
  async checkCategory(category, items) {
    const results = [];
    
    for (const item of items) {
      const result = await this.checkItem(category, item);
      results.push(result);
    }
    
    return results;
  }
  
  // 检查单个项目
  async checkItem(category, item) {
    try {
      let passed = false;
      
      // 根据类别和项目名称执行具体检查
      switch (category) {
        case 'code':
          passed = await this.checkCodeItem(item.name);
          break;
        case 'config':
          passed = await this.checkConfigItem(item.name);
          break;
        case 'assets':
          passed = await this.checkAssetsItem(item.name);
          break;
        case 'legal':
          passed = await this.checkLegalItem(item.name);
          break;
        case 'platform':
          passed = await this.checkPlatformItem(item.name);
          break;
      }
      
      return {
        name: item.name,
        required: item.required,
        passed,
        message: passed ? '检查通过' : '检查失败'
      };
    } catch (error) {
      return {
        name: item.name,
        required: item.required,
        passed: false,
        message: `检查出错: ${error.message}`
      };
    }
  }
  
  // 检查代码相关项目
  async checkCodeItem(itemName) {
    switch (itemName) {
      case '调试模式已关闭':
        // 检查是否还有 console.log 或调试代码
        return !this.hasDebugCode();
      case '单元测试通过':
        // 运行单元测试
        return await this.runUnitTests();
      default:
        return true; // 默认通过,需要手动确认
    }
  }
  
  // 检查配置相关项目
  async checkConfigItem(itemName) {
    switch (itemName) {
      case '生产环境配置正确':
        return this.checkProductionConfig();
      case 'API 地址配置正确':
        return this.checkApiConfig();
      default:
        return true;
    }
  }
  
  // 检查资源相关项目
  async checkAssetsItem(itemName) {
    switch (itemName) {
      case '应用图标完整':
        return this.checkAppIcons();
      case '启动页图片完整':
        return this.checkSplashScreens();
      default:
        return true;
    }
  }
  
  // 检查法律相关项目
  async checkLegalItem(itemName) {
    // 这些通常需要手动确认
    return true;
  }
  
  // 检查平台相关项目
  async checkPlatformItem(itemName) {
    switch (itemName) {
      case 'Android 权限声明':
        return this.checkAndroidPermissions();
      case 'iOS 权限描述':
        return this.checkIOSPermissions();
      default:
        return true;
    }
  }
  
  // 检查是否有调试代码
  hasDebugCode() {
    // 这里应该扫描代码文件
    // 简化实现
    return false;
  }
  
  // 运行单元测试
  async runUnitTests() {
    try {
      // 这里应该执行实际的测试命令
      // 简化实现
      return true;
    } catch (error) {
      return false;
    }
  }
  
  // 检查生产环境配置
  checkProductionConfig() {
    // 检查环境变量、配置文件等
    return true;
  }
  
  // 检查 API 配置
  checkApiConfig() {
    // 检查 API 地址是否为生产环境
    return true;
  }
  
  // 检查应用图标
  checkAppIcons() {
    const requiredIcons = {
      android: ['48x48', '72x72', '96x96', '144x144', '192x192'],
      ios: ['29x29', '40x40', '58x58', '60x60', '80x80', '87x87', '120x120', '180x180']
    };
    
    // 检查图标文件是否存在
    return true;
  }
  
  // 检查启动页
  checkSplashScreens() {
    // 检查启动页图片是否存在
    return true;
  }
  
  // 检查 Android 权限
  checkAndroidPermissions() {
    // 检查 manifest.json 中的权限配置
    return true;
  }
  
  // 检查 iOS 权限
  checkIOSPermissions() {
    // 检查权限描述是否完整
    return true;
  }
  
  // 生成检查报告
  generateReport() {
    const report = {
      timestamp: new Date().toISOString(),
      summary: {
        total: 0,
        passed: 0,
        failed: 0,
        requiredFailed: 0
      },
      categories: {},
      canRelease: true
    };
    
    for (const [category, results] of Object.entries(this.results)) {
      const categoryReport = {
        total: results.length,
        passed: 0,
        failed: 0,
        requiredFailed: 0,
        items: results
      };
      
      results.forEach(result => {
        report.summary.total++;
        categoryReport.total++;
        
        if (result.passed) {
          report.summary.passed++;
          categoryReport.passed++;
        } else {
          report.summary.failed++;
          categoryReport.failed++;
          
          if (result.required) {
            report.summary.requiredFailed++;
            categoryReport.requiredFailed++;
            report.canRelease = false;
          }
        }
      });
      
      report.categories[category] = categoryReport;
    }
    
    return report;
  }
  
  // 打印报告
  printReport(report) {
    console.log('\n=== 发布前检查报告 ===');
    console.log(`检查时间: ${report.timestamp}`);
    console.log(`总计: ${report.summary.total} 项`);
    console.log(`通过: ${report.summary.passed} 项`);
    console.log(`失败: ${report.summary.failed} 项`);
    console.log(`必需项失败: ${report.summary.requiredFailed} 项`);
    console.log(`可以发布: ${report.canRelease ? '是' : '否'}`);
    
    for (const [category, categoryReport] of Object.entries(report.categories)) {
      console.log(`\n--- ${category} ---`);
      categoryReport.items.forEach(item => {
        const status = item.passed ? '✓' : '✗';
        const required = item.required ? '[必需]' : '[可选]';
        console.log(`${status} ${required} ${item.name}: ${item.message}`);
      });
    }
  }
}

export default ReleaseChecker;

8.3.2 自动化构建脚本

// scripts/build.js
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

class BuildScript {
  constructor() {
    this.config = {
      platforms: ['h5', 'mp-weixin', 'app-plus'],
      outputDir: 'dist',
      backupDir: 'backup',
      version: this.getVersion()
    };
  }
  
  // 获取版本号
  getVersion() {
    const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
    return packageJson.version;
  }
  
  // 主构建流程
  async build(platforms = this.config.platforms) {
    console.log('开始构建流程...');
    
    try {
      // 1. 预构建检查
      await this.preBuildCheck();
      
      // 2. 清理输出目录
      this.cleanOutputDir();
      
      // 3. 构建各平台
      for (const platform of platforms) {
        await this.buildPlatform(platform);
      }
      
      // 4. 后构建处理
      await this.postBuildProcess();
      
      console.log('构建完成!');
      
    } catch (error) {
      console.error('构建失败:', error.message);
      process.exit(1);
    }
  }
  
  // 预构建检查
  async preBuildCheck() {
    console.log('执行预构建检查...');
    
    // 检查依赖
    if (!fs.existsSync('node_modules')) {
      console.log('安装依赖...');
      execSync('npm install', { stdio: 'inherit' });
    }
    
    // 检查配置文件
    const requiredFiles = ['manifest.json', 'pages.json'];
    for (const file of requiredFiles) {
      if (!fs.existsSync(file)) {
        throw new Error(`缺少必需文件: ${file}`);
      }
    }
    
    // 运行测试
    try {
      execSync('npm run test', { stdio: 'inherit' });
    } catch (error) {
      console.warn('测试失败,但继续构建');
    }
  }
  
  // 清理输出目录
  cleanOutputDir() {
    console.log('清理输出目录...');
    
    if (fs.existsSync(this.config.outputDir)) {
      // 备份之前的构建
      const backupPath = path.join(this.config.backupDir, `build_${Date.now()}`);
      fs.mkdirSync(path.dirname(backupPath), { recursive: true });
      fs.renameSync(this.config.outputDir, backupPath);
    }
    
    fs.mkdirSync(this.config.outputDir, { recursive: true });
  }
  
  // 构建指定平台
  async buildPlatform(platform) {
    console.log(`构建 ${platform} 平台...`);
    
    const startTime = Date.now();
    
    try {
      // 设置环境变量
      process.env.UNI_PLATFORM = platform;
      process.env.NODE_ENV = 'production';
      
      // 执行构建命令
      const buildCommand = this.getBuildCommand(platform);
      execSync(buildCommand, { stdio: 'inherit' });
      
      // 平台特定的后处理
      await this.platformPostProcess(platform);
      
      const duration = Date.now() - startTime;
      console.log(`${platform} 构建完成,耗时: ${duration}ms`);
      
    } catch (error) {
      throw new Error(`${platform} 构建失败: ${error.message}`);
    }
  }
  
  // 获取构建命令
  getBuildCommand(platform) {
    const commands = {
      'h5': 'npm run build:h5',
      'mp-weixin': 'npm run build:mp-weixin',
      'mp-alipay': 'npm run build:mp-alipay',
      'mp-baidu': 'npm run build:mp-baidu',
      'mp-toutiao': 'npm run build:mp-toutiao',
      'app-plus': 'npm run build:app-plus'
    };
    
    return commands[platform] || `npm run build:${platform}`;
  }
  
  // 平台特定后处理
  async platformPostProcess(platform) {
    switch (platform) {
      case 'h5':
        await this.processH5Build();
        break;
      case 'mp-weixin':
        await this.processWechatBuild();
        break;
      case 'app-plus':
        await this.processAppBuild();
        break;
    }
  }
  
  // 处理 H5 构建
  async processH5Build() {
    const h5Dir = path.join(this.config.outputDir, 'build', 'h5');
    
    if (fs.existsSync(h5Dir)) {
      // 压缩静态资源
      await this.compressAssets(h5Dir);
      
      // 生成部署脚本
      this.generateDeployScript('h5', h5Dir);
    }
  }
  
  // 处理微信小程序构建
  async processWechatBuild() {
    const mpDir = path.join(this.config.outputDir, 'build', 'mp-weixin');
    
    if (fs.existsSync(mpDir)) {
      // 检查文件大小
      await this.checkMiniProgramSize(mpDir);
      
      // 生成上传脚本
      this.generateUploadScript('mp-weixin', mpDir);
    }
  }
  
  // 处理 App 构建
  async processAppBuild() {
    const appDir = path.join(this.config.outputDir, 'build', 'app-plus');
    
    if (fs.existsSync(appDir)) {
      // 复制原生资源
      await this.copyNativeAssets(appDir);
      
      // 生成打包脚本
      this.generatePackageScript('app-plus', appDir);
    }
  }
  
  // 压缩静态资源
  async compressAssets(dir) {
    console.log('压缩静态资源...');
    
    // 这里可以集成 gzip、brotli 等压缩
    // 简化实现
  }
  
  // 检查小程序大小
  async checkMiniProgramSize(dir) {
    const maxSize = 2 * 1024 * 1024; // 2MB
    const totalSize = this.getDirectorySize(dir);
    
    if (totalSize > maxSize) {
      console.warn(`警告: 小程序包大小 ${(totalSize / 1024 / 1024).toFixed(2)}MB 超过建议大小`);
    }
  }
  
  // 获取目录大小
  getDirectorySize(dir) {
    let totalSize = 0;
    
    const files = fs.readdirSync(dir);
    for (const file of files) {
      const filePath = path.join(dir, file);
      const stats = fs.statSync(filePath);
      
      if (stats.isDirectory()) {
        totalSize += this.getDirectorySize(filePath);
      } else {
        totalSize += stats.size;
      }
    }
    
    return totalSize;
  }
  
  // 复制原生资源
  async copyNativeAssets(dir) {
    const nativeDir = 'src/native';
    
    if (fs.existsSync(nativeDir)) {
      const targetDir = path.join(dir, 'native');
      this.copyDirectory(nativeDir, targetDir);
    }
  }
  
  // 复制目录
  copyDirectory(src, dest) {
    if (!fs.existsSync(dest)) {
      fs.mkdirSync(dest, { recursive: true });
    }
    
    const files = fs.readdirSync(src);
    for (const file of files) {
      const srcPath = path.join(src, file);
      const destPath = path.join(dest, file);
      
      if (fs.statSync(srcPath).isDirectory()) {
        this.copyDirectory(srcPath, destPath);
      } else {
        fs.copyFileSync(srcPath, destPath);
      }
    }
  }
  
  // 生成部署脚本
  generateDeployScript(platform, buildDir) {
    const script = `
#!/bin/bash
# ${platform} 部署脚本

echo "开始部署 ${platform}..."

# 上传到服务器
# rsync -avz ${buildDir}/ user@server:/var/www/html/

# 或者上传到 CDN
# aws s3 sync ${buildDir} s3://your-bucket/

echo "部署完成!"
    `;
    
    const scriptPath = path.join(this.config.outputDir, `deploy-${platform}.sh`);
    fs.writeFileSync(scriptPath, script);
    fs.chmodSync(scriptPath, '755');
  }
  
  // 生成上传脚本
  generateUploadScript(platform, buildDir) {
    const script = `
#!/bin/bash
# ${platform} 上传脚本

echo "开始上传 ${platform}..."

# 微信开发者工具命令行上传
# /Applications/wechatwebdevtools.app/Contents/MacOS/cli upload --project ${buildDir}

echo "上传完成!"
    `;
    
    const scriptPath = path.join(this.config.outputDir, `upload-${platform}.sh`);
    fs.writeFileSync(scriptPath, script);
    fs.chmodSync(scriptPath, '755');
  }
  
  // 生成打包脚本
  generatePackageScript(platform, buildDir) {
    const script = `
#!/bin/bash
# ${platform} 打包脚本

echo "开始打包 ${platform}..."

# Android 打包
# cd android && ./gradlew assembleRelease

# iOS 打包
# xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp archive

echo "打包完成!"
    `;
    
    const scriptPath = path.join(this.config.outputDir, `package-${platform}.sh`);
    fs.writeFileSync(scriptPath, script);
    fs.chmodSync(scriptPath, '755');
  }
  
  // 后构建处理
  async postBuildProcess() {
    console.log('执行后构建处理...');
    
    // 生成构建报告
    this.generateBuildReport();
    
    // 清理临时文件
    this.cleanupTempFiles();
    
    // 发送通知
    await this.sendBuildNotification();
  }
  
  // 生成构建报告
  generateBuildReport() {
    const report = {
      version: this.config.version,
      timestamp: new Date().toISOString(),
      platforms: this.config.platforms,
      outputDir: this.config.outputDir,
      buildSizes: this.getBuildSizes()
    };
    
    const reportPath = path.join(this.config.outputDir, 'build-report.json');
    fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
  }
  
  // 获取构建大小
  getBuildSizes() {
    const sizes = {};
    const buildDir = path.join(this.config.outputDir, 'build');
    
    if (fs.existsSync(buildDir)) {
      const platforms = fs.readdirSync(buildDir);
      for (const platform of platforms) {
        const platformDir = path.join(buildDir, platform);
        if (fs.statSync(platformDir).isDirectory()) {
          sizes[platform] = this.getDirectorySize(platformDir);
        }
      }
    }
    
    return sizes;
  }
  
  // 清理临时文件
  cleanupTempFiles() {
    // 清理构建过程中产生的临时文件
  }
  
  // 发送构建通知
  async sendBuildNotification() {
    // 发送邮件、Slack 消息等
    console.log('构建通知已发送');
  }
}

// 命令行接口
if (require.main === module) {
  const args = process.argv.slice(2);
  const platforms = args.length > 0 ? args : undefined;
  
  const builder = new BuildScript();
  builder.build(platforms).catch(console.error);
}

module.exports = BuildScript;

8.3.3 持续集成配置

GitHub Actions 配置

# .github/workflows/build.yml
name: Build and Deploy

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  release:
    types: [ published ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '16'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm run test
    
    - name: Run linting
      run: npm run lint
    
    - name: Check code coverage
      run: npm run coverage
  
  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push' || github.event_name == 'release'
    
    strategy:
      matrix:
        platform: [h5, mp-weixin, app-plus]
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '16'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Build ${{ matrix.platform }}
      run: npm run build:${{ matrix.platform }}
      env:
        NODE_ENV: production
    
    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: build-${{ matrix.platform }}
        path: dist/build/${{ matrix.platform }}
    
    - name: Deploy to staging
      if: github.ref == 'refs/heads/develop'
      run: |
        echo "Deploy to staging environment"
        # 部署到测试环境的脚本
    
    - name: Deploy to production
      if: github.event_name == 'release'
      run: |
        echo "Deploy to production environment"
        # 部署到生产环境的脚本
  
  android-build:
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Java
      uses: actions/setup-java@v3
      with:
        distribution: 'temurin'
        java-version: '11'
    
    - name: Setup Android SDK
      uses: android-actions/setup-android@v2
    
    - name: Download app-plus build
      uses: actions/download-artifact@v3
      with:
        name: build-app-plus
        path: dist/build/app-plus
    
    - name: Build Android APK
      run: |
        cd android
        ./gradlew assembleRelease
    
    - name: Sign APK
      run: |
        echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 -d > android.keystore
        jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
          -keystore android.keystore \
          -storepass ${{ secrets.KEYSTORE_PASSWORD }} \
          -keypass ${{ secrets.KEY_PASSWORD }} \
          android/app/build/outputs/apk/release/app-release-unsigned.apk \
          ${{ secrets.KEY_ALIAS }}
    
    - name: Upload APK
      uses: actions/upload-artifact@v3
      with:
        name: android-apk
        path: android/app/build/outputs/apk/release/
  
  ios-build:
    needs: build
    runs-on: macos-latest
    if: github.event_name == 'release'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Xcode
      uses: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: latest-stable
    
    - name: Download app-plus build
      uses: actions/download-artifact@v3
      with:
        name: build-app-plus
        path: dist/build/app-plus
    
    - name: Install certificates
      run: |
        echo "${{ secrets.IOS_CERTIFICATE }}" | base64 -d > certificate.p12
        echo "${{ secrets.IOS_PROVISIONING_PROFILE }}" | base64 -d > profile.mobileprovision
        
        # 安装证书
        security create-keychain -p "" build.keychain
        security import certificate.p12 -k build.keychain -P "${{ secrets.CERTIFICATE_PASSWORD }}" -A
        security list-keychains -s build.keychain
        security default-keychain -s build.keychain
        security unlock-keychain -p "" build.keychain
        
        # 安装描述文件
        mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
        cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
    
    - name: Build iOS
      run: |
        cd ios
        xcodebuild -workspace MyApp.xcworkspace \
          -scheme MyApp \
          -configuration Release \
          -archivePath MyApp.xcarchive \
          archive
        
        xcodebuild -exportArchive \
          -archivePath MyApp.xcarchive \
          -exportPath export \
          -exportOptionsPlist ExportOptions.plist
    
    - name: Upload IPA
      uses: actions/upload-artifact@v3
      with:
        name: ios-ipa
        path: ios/export/

Jenkins 配置

// Jenkinsfile
pipeline {
    agent any
    
    environment {
        NODE_VERSION = '16'
        ANDROID_HOME = '/opt/android-sdk'
        JAVA_HOME = '/usr/lib/jvm/java-11-openjdk'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Setup') {
            steps {
                script {
                    // 安装 Node.js
                    sh "nvm use ${NODE_VERSION}"
                    
                    // 安装依赖
                    sh 'npm ci'
                }
            }
        }
        
        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test'
                    }
                    post {
                        always {
                            publishTestResults testResultsPattern: 'test-results.xml'
                        }
                    }
                }
                
                stage('Lint') {
                    steps {
                        sh 'npm run lint'
                    }
                }
                
                stage('Security Scan') {
                    steps {
                        sh 'npm audit'
                    }
                }
            }
        }
        
        stage('Build') {
            parallel {
                stage('H5') {
                    steps {
                        sh 'npm run build:h5'
                        archiveArtifacts artifacts: 'dist/build/h5/**', fingerprint: true
                    }
                }
                
                stage('WeChat Mini Program') {
                    steps {
                        sh 'npm run build:mp-weixin'
                        archiveArtifacts artifacts: 'dist/build/mp-weixin/**', fingerprint: true
                    }
                }
                
                stage('App Plus') {
                    steps {
                        sh 'npm run build:app-plus'
                        archiveArtifacts artifacts: 'dist/build/app-plus/**', fingerprint: true
                    }
                }
            }
        }
        
        stage('Package') {
            when {
                anyOf {
                    branch 'main'
                    tag pattern: 'v\\d+\\.\\d+\\.\\d+', comparator: 'REGEXP'
                }
            }
            
            parallel {
                stage('Android') {
                    steps {
                        script {
                            // 构建 Android APK
                            sh '''
                                cd android
                                ./gradlew clean assembleRelease
                            '''
                            
                            // 签名 APK
                            withCredentials([
                                file(credentialsId: 'android-keystore', variable: 'KEYSTORE_FILE'),
                                string(credentialsId: 'keystore-password', variable: 'KEYSTORE_PASSWORD'),
                                string(credentialsId: 'key-password', variable: 'KEY_PASSWORD'),
                                string(credentialsId: 'key-alias', variable: 'KEY_ALIAS')
                            ]) {
                                sh '''
                                    jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
                                        -keystore $KEYSTORE_FILE \
                                        -storepass $KEYSTORE_PASSWORD \
                                        -keypass $KEY_PASSWORD \
                                        android/app/build/outputs/apk/release/app-release-unsigned.apk \
                                        $KEY_ALIAS
                                    
                                    zipalign -v 4 \
                                        android/app/build/outputs/apk/release/app-release-unsigned.apk \
                                        android/app/build/outputs/apk/release/app-release.apk
                                '''
                            }
                            
                            archiveArtifacts artifacts: 'android/app/build/outputs/apk/release/*.apk', fingerprint: true
                        }
                    }
                }
                
                stage('iOS') {
                    agent {
                        label 'macos'
                    }
                    
                    steps {
                        script {
                            // 构建 iOS
                            withCredentials([
                                file(credentialsId: 'ios-certificate', variable: 'CERTIFICATE_FILE'),
                                file(credentialsId: 'ios-provisioning-profile', variable: 'PROVISIONING_PROFILE'),
                                string(credentialsId: 'certificate-password', variable: 'CERTIFICATE_PASSWORD')
                            ]) {
                                sh '''
                                    # 安装证书和描述文件
                                    security create-keychain -p "" build.keychain
                                    security import $CERTIFICATE_FILE -k build.keychain -P $CERTIFICATE_PASSWORD -A
                                    security list-keychains -s build.keychain
                                    security default-keychain -s build.keychain
                                    security unlock-keychain -p "" build.keychain
                                    
                                    mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
                                    cp $PROVISIONING_PROFILE ~/Library/MobileDevice/Provisioning\ Profiles/
                                    
                                    # 构建
                                    cd ios
                                    xcodebuild -workspace MyApp.xcworkspace \
                                        -scheme MyApp \
                                        -configuration Release \
                                        -archivePath MyApp.xcarchive \
                                        archive
                                    
                                    xcodebuild -exportArchive \
                                        -archivePath MyApp.xcarchive \
                                        -exportPath export \
                                        -exportOptionsPlist ExportOptions.plist
                                '''
                            }
                            
                            archiveArtifacts artifacts: 'ios/export/*.ipa', fingerprint: true
                        }
                    }
                }
            }
        }
        
        stage('Deploy') {
            when {
                branch 'main'
            }
            
            steps {
                script {
                    // 部署到生产环境
                    sh 'npm run deploy:production'
                    
                    // 发送通知
                    slackSend(
                        channel: '#deployments',
                        color: 'good',
                        message: "✅ 部署成功: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
                    )
                }
            }
        }
    }
    
    post {
        always {
            // 清理工作空间
            cleanWs()
        }
        
        failure {
            // 发送失败通知
            slackSend(
                channel: '#deployments',
                color: 'danger',
                message: "❌ 构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
            )
        }
    }
}