返回笔记首页

App更新通知和升级策略完整方案

主题配置

一、更新通知方式

1.1 应用内推送(主要方式)

启动时检查更新

javascript
// App.vue / main.js
export default {
  onLaunch() {
    // 延迟检查,避免影响启动速度
    setTimeout(() => {
      this.checkAppUpdate()
    }, 2000)
  },

  methods: {
    async checkAppUpdate() {
      try {
        const res = await uni.request({
          url: 'https://api.xxx.com/app/version/check',
          data: {
            platform: uni.getSystemInfoSync().platform,
            currentVersion: this.getCurrentVersion(),
            deviceId: this.getDeviceId(),
            userId: this.getUserId()
          }
        })

        const {
          hasUpdate,
          versionName,
          versionCode,
          downloadUrl,
          updateType,  // force/recommended/silent
          updateInfo,
          fileSize
        } = res.data

        if (hasUpdate) {
          switch(updateType) {
            case 'force':
              this.showForceUpdateDialog(versionName, downloadUrl, updateInfo)
              break
            case 'recommended':
              this.showRecommendedUpdateDialog(versionName, downloadUrl, updateInfo)
              break
            case 'silent':
              this.silentDownload(downloadUrl)
              break
          }
        }
      } catch (error) {
        console.error('检查更新失败:', error)
      }
    },

    showForceUpdateDialog(version, url, info) {
      uni.showModal({
        title: '版本更新',
        content: `发现新版本 ${version}\n\n${info}\n\n必须更新才能继续使用`,
        showCancel: false,
        confirmText: '立即更新',
        success: () => {
          this.downloadAndInstall(url)
        }
      })
    },

    showRecommendedUpdateDialog(version, url, info) {
      uni.showModal({
        title: '版本更新',
        content: `发现新版本 ${version}\n\n${info}`,
        confirmText: '立即更新',
        cancelText: '稍后提醒',
        success: (res) => {
          if (res.confirm) {
            this.downloadAndInstall(url)
          } else {
            // 记录跳过次数
            this.recordSkipUpdate()
          }
        }
      })
    },

    recordSkipUpdate() {
      let skipCount = uni.getStorageSync('update_skip_count') || 0
      skipCount++
      uni.setStorageSync('update_skip_count', skipCount)

      // 跳过3次后,下次变成强制更新
      if (skipCount >= 3) {
        uni.setStorageSync('force_update_next_time', true)
      }
    },

    getCurrentVersion() {
      // #ifdef APP-PLUS
      return plus.runtime.version
      // #endif
      return '1.0.0'
    }
  }
}

后台激活时检查

javascript
// App.vue
export default {
  onShow() {
    // 从后台恢复到前台时
    const lastCheckTime = uni.getStorageSync('last_check_update_time') || 0
    const now = Date.now()

    // 距离上次检查超过1小时才再次检查
    if (now - lastCheckTime > 60 * 60 * 1000) {
      this.checkAppUpdate()
      uni.setStorageSync('last_check_update_time', now)
    }
  }
}

手动检查入口

vue
<!-- pages/settings/index.vue -->
<template>
  <view class="setting-item" @click="checkUpdate">
    <text>检查更新</text>
    <text class="version">当前版本 v{{ version }}</text>
    <text class="arrow">></text>
  </view>
</template>

<script>
export default {
  data() {
    return {
      version: '1.0.0'
    }
  },

  onLoad() {
    // #ifdef APP-PLUS
    this.version = plus.runtime.version
    // #endif
  },

  methods: {
    async checkUpdate() {
      uni.showLoading({ title: '检查中...' })

      try {
        const res = await uni.request({
          url: 'https://api.xxx.com/app/version/check',
          data: {
            platform: uni.getSystemInfoSync().platform,
            currentVersion: this.version
          }
        })

        uni.hideLoading()

        if (res.data.hasUpdate) {
          // 显示更新弹窗
        } else {
          uni.showToast({
            title: '已是最新版本',
            icon: 'success'
          })
        }
      } catch (error) {
        uni.hideLoading()
        uni.showToast({
          title: '检查失败',
          icon: 'none'
        })
      }
    }
  }
}
</script>

1.2 系统推送通知

配置推送服务

javascript
// 使用个推、极光推送等第三方服务

// utils/push.js
class PushService {
  init() {
    // #ifdef APP-PLUS
    const main = plus.android.runtimeMainActivity()
    const context = main.getApplicationContext()

    // 初始化推送SDK
    const PushManager = plus.android.importClass('com.igexin.sdk.PushManager')
    PushManager.getInstance().initialize(context)

    // 监听推送消息
    plus.push.addEventListener('click', (msg) => {
      this.handlePushClick(msg)
    })
    // #endif
  }

  handlePushClick(msg) {
    const { payload } = msg

    if (payload.type === 'app_update') {
      // 跳转到更新页面
      uni.navigateTo({
        url: '/pages/update/index?version=' + payload.version
      })
    }
  }

  // 获取推送token
  getClientId() {
    return new Promise((resolve) => {
      // #ifdef APP-PLUS
      plus.push.getClientInfo((info) => {
        resolve(info.clientid)
      })
      // #endif
    })
  }
}

export default new PushService()

服务端推送

javascript
// server/push-notification.js
const request = require('request')

// 个推服务端推送
function sendUpdateNotification(userTokens, versionInfo) {
  const data = {
    appkey: 'your_app_key',
    master_secret: 'your_master_secret',
    audience: {
      cid: userTokens  // 用户设备token列表
    },
    notification: {
      title: '版本更新',
      content: `${versionInfo.name} 版本已发布,点击查看详情`,
      click_type: 'startapp',
      intent: 'intent://app.update#Intent;scheme=myapp;launchFlags=0x4000000;end'
    },
    push_info: {
      payload: {
        type: 'app_update',
        version: versionInfo.name,
        url: versionInfo.downloadUrl
      }
    }
  }

  request({
    url: 'https://restapi.getui.com/v2/push/single/cid',
    method: 'POST',
    json: true,
    body: data
  }, (error, response, body) => {
    if (error) {
      console.error('推送失败:', error)
    } else {
      console.log('推送成功:', body)
    }
  })
}

// 批量推送给所有用户
async function pushToAllUsers(versionInfo) {
  // 从数据库获取所有用户的推送token
  const users = await User.find({ pushToken: { $exists: true } })

  // 分批推送,每批1000个
  const batchSize = 1000
  for (let i = 0; i < users.length; i += batchSize) {
    const batch = users.slice(i, i + batchSize)
    const tokens = batch.map(u => u.pushToken)

    await sendUpdateNotification(tokens, versionInfo)

    // 延迟避免频率限制
    await sleep(1000)
  }
}

1.3 短信通知(重要更新)

javascript
// server/sms-notification.js
const tencentcloud = require('tencentcloud-sdk-nodejs')

async function sendUpdateSMS(phoneNumbers, versionInfo) {
  const SmsClient = tencentcloud.sms.v20210111.Client

  const client = new SmsClient({
    credential: {
      secretId: 'your_secret_id',
      secretKey: 'your_secret_key'
    },
    region: 'ap-guangzhou'
  })

  const params = {
    PhoneNumberSet: phoneNumbers,
    SmsSdkAppId: 'your_app_id',
    SignName: '您的应用名',
    TemplateId: 'template_id',
    TemplateParamSet: [
      versionInfo.name,
      versionInfo.updateInfo
    ]
  }

  try {
    const response = await client.SendSms(params)
    console.log('短信发送成功:', response)
  } catch (error) {
    console.error('短信发送失败:', error)
  }
}

// 短信模板示例:
// 【应用名】尊敬的用户,{1}版本已发布,{2}。请及时更新体验新功能。

1.4 邮件通知

javascript
// server/email-notification.js
const nodemailer = require('nodemailer')

async function sendUpdateEmail(emails, versionInfo) {
  const transporter = nodemailer.createTransport({
    host: 'smtp.qq.com',
    port: 465,
    secure: true,
    auth: {
      user: 'your@email.com',
      pass: 'your_password'
    }
  })

  const mailOptions = {
    from: '"应用名" <your@email.com>',
    to: emails.join(','),
    subject: `${versionInfo.name} 版本更新通知`,
    html: `
      <h2>版本更新</h2>
      <p>尊敬的用户,${versionInfo.name} 版本已发布!</p>
      <h3>更新内容:</h3>
      <p>${versionInfo.updateInfo}</p>
      <p>
        <a href="${versionInfo.downloadUrl}" style="
          display: inline-block;
          padding: 10px 20px;
          background: #007aff;
          color: white;
          text-decoration: none;
          border-radius: 5px;
        ">立即更新</a>
      </p>
      <p>或在应用内检查更新</p>
    `
  }

  try {
    await transporter.sendMail(mailOptions)
    console.log('邮件发送成功')
  } catch (error) {
    console.error('邮件发送失败:', error)
  }
}

1.5 应用内消息中心

vue
<!-- pages/message/index.vue -->
<template>
  <view class="message-list">
    <view class="message-item"
          v-for="msg in messages"
          :key="msg.id"
          @click="handleMessageClick(msg)">
      <image :src="msg.icon" class="icon" />
      <view class="content">
        <text class="title">{{ msg.title }}</text>
        <text class="desc">{{ msg.content }}</text>
        <text class="time">{{ formatTime(msg.time) }}</text>
      </view>
      <view class="badge" v-if="!msg.read"></view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      messages: []
    }
  },

  onLoad() {
    this.loadMessages()
  },

  methods: {
    async loadMessages() {
      const res = await uni.request({
        url: 'https://api.xxx.com/messages',
        data: {
          userId: this.getUserId()
        }
      })

      this.messages = res.data.list
    },

    handleMessageClick(msg) {
      if (msg.type === 'app_update') {
        // 标记为已读
        this.markAsRead(msg.id)

        // 跳转更新
        uni.navigateTo({
          url: '/pages/update/index?version=' + msg.version
        })
      }
    }
  }
}
</script>

二、更新策略分类

2.1 强制更新(Force Update)

使用场景

  • 严重安全漏洞
  • 接口不兼容(旧版本无法正常使用)
  • 重大bug影响核心功能
  • 监管要求必须更新
实现方案
javascript
// utils/force-update.js
class ForceUpdateManager {
  check(serverVersion, currentVersion) {
    // 服务端配置强制更新的最低版本
    const minRequiredVersion = serverVersion.minRequired

    if (this.compareVersion(currentVersion, minRequiredVersion) < 0) {
      return {
        needUpdate: true,
        force: true,
        reason: '当前版本过低,必须更新才能继续使用'
      }
    }

    return { needUpdate: false }
  }

  showForceUpdateDialog(versionInfo) {
    uni.showModal({
      title: '必须更新',
      content: `当前版本过低,必须更新到 ${versionInfo.name} 才能继续使用\n\n更新内容:\n${versionInfo.updateInfo}`,
      showCancel: false,
      confirmText: '立即更新',
      success: () => {
        this.startDownload(versionInfo.downloadUrl)
      }
    })
  }

  startDownload(url) {
    // 禁用返回键
    // #ifdef APP-PLUS
    plus.key.addEventListener('backbutton', () => {
      // 阻止返回
      return false
    })
    // #endif

    uni.showLoading({
      title: '下载中 0%',
      mask: true
    })

    const downloadTask = uni.downloadFile({
      url,
      success: (res) => {
        if (res.statusCode === 200) {
          uni.hideLoading()
          this.installApp(res.tempFilePath)
        }
      }
    })

    downloadTask.onProgressUpdate((res) => {
      uni.showLoading({
        title: `下载中 ${res.progress}%`,
        mask: true
      })
    })
  }

  installApp(filePath) {
    // #ifdef APP-PLUS
    plus.runtime.install(filePath, {
      force: true
    }, () => {
      // 安装成功后重启
      plus.runtime.restart()
    }, (error) => {
      uni.showModal({
        title: '安装失败',
        content: '请手动安装更新包',
        showCancel: false
      })
    })
    // #endif
  }

  compareVersion(v1, v2) {
    const arr1 = v1.split('.').map(Number)
    const arr2 = v2.split('.').map(Number)

    for (let i = 0; i < 3; i++) {
      if (arr1[i] > arr2[i]) return 1
      if (arr1[i] < arr2[i]) return -1
    }
    return 0
  }
}

export default new ForceUpdateManager()
服务端配置
javascript
// server/version-config.js
const versionConfig = {
  latest: {
    versionName: '2.0.0',
    versionCode: 200,
    downloadUrl: 'https://cdn.xxx.com/app-2.0.0.apk',
    updateInfo: '1. 新增功能\n2. 修复bug',
    releaseTime: '2024-01-01 10:00:00'
  },

  // 强制更新配置
  minRequired: {
    versionName: '1.5.0',
    versionCode: 150,
    reason: '旧版本存在严全漏洞',
    forceTime: '2024-01-05 00:00:00'  // 该时间后强制更新
  },

  // 根据时间逐步强制
  forceUpdateSchedule: {
    '2024-01-01': 10,   // 1月1日:10%用户强制
    '2024-01-03': 50,   // 1月3日:50%用户强制
    '2024-01-05': 100   // 1月5日:100%用户强制
  }
}

2.2 推荐更新(Recommended Update)

使用场景

  • 新功能上线
  • 性能优化
  • 一般bug修复
  • UI改版
实现方案
javascript
// utils/recommended-update.js
class RecommendedUpdateManager {
  showUpdateDialog(versionInfo) {
    // 检查用户是否多次跳过
    const skipCount = uni.getStorageSync('update_skip_count') || 0

    // 跳过3次后,下次弹窗更明显
    const isUrgent = skipCount >= 3

    uni.showModal({
      title: isUrgent ? '强烈建议更新' : '发现新版本',
      content: `版本 ${versionInfo.name}\n\n${versionInfo.updateInfo}\n\n文件大小:${this.formatSize(versionInfo.fileSize)}`,
      confirmText: '立即更新',
      cancelText: isUrgent ? '忽略' : '稍后',
      confirmColor: isUrgent ? '#FF0000' : '#007aff',
      success: (res) => {
        if (res.confirm) {
          this.startUpdate(versionInfo)
          // 清空跳过次数
          uni.removeStorageSync('update_skip_count')
        } else {
          this.handleSkip()
        }
      }
    })
  }

  handleSkip() {
    let skipCount = uni.getStorageSync('update_skip_count') || 0
    skipCount++
    uni.setStorageSync('update_skip_count', skipCount)

    // 设置下次提醒时间
    let remindDelay
    if (skipCount === 1) {
      remindDelay = 24 * 60 * 60 * 1000  // 24小时后
    } else if (skipCount === 2) {
      remindDelay = 12 * 60 * 60 * 1000  // 12小时后
    } else {
      remindDelay = 6 * 60 * 60 * 1000   // 6小时后
    }

    const nextRemindTime = Date.now() + remindDelay
    uni.setStorageSync('next_remind_time', nextRemindTime)
  }

  shouldShowDialog() {
    const nextRemindTime = uni.getStorageSync('next_remind_time') || 0
    return Date.now() >= nextRemindTime
  }

  formatSize(bytes) {
    if (bytes < 1024 * 1024) {
      return (bytes / 1024).toFixed(1) + ' KB'
    }
    return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
  }
}

export default new RecommendedUpdateManager()

2.3 静默更新(Silent Update)

使用场景

  • 小版本更新(bug修复)
  • 资源文件更新
  • 配置更新
  • 不影响用户使用的更新
实现方案
javascript
// utils/silent-update.js
class SilentUpdateManager {
  async checkAndDownload() {
    try {
      const res = await uni.request({
        url: 'https://api.xxx.com/app/version/check',
        data: {
          currentVersion: this.getCurrentVersion()
        }
      })

      if (res.data.hasUpdate && res.data.updateType === 'silent') {
        // 静默下载
        this.downloadInBackground(res.data.downloadUrl)
      }
    } catch (error) {
      console.error('检查更新失败:', error)
    }
  }

  downloadInBackground(url) {
    // 只在 WiFi 下自动下载
    const networkType = uni.getSystemInfoSync().networkType
    if (networkType !== 'wifi') {
      console.log('非WiFi环境,跳过静默更新')
      return
    }

    console.log('开始静默下载')

    const downloadTask = uni.downloadFile({
      url,
      success: (res) => {
        if (res.statusCode === 200) {
          // 下载完成,保存文件路径
          uni.setStorageSync('update_package_path', res.tempFilePath)

          // 显示小提示(不打扰)
          this.showUpdateTip()
        }
      },
      fail: (error) => {
        console.error('静默下载失败:', error)
      }
    })

    // 不显示进度,完全后台下载
    downloadTask.onProgressUpdate((res) => {
      console.log('下载进度:', res.progress)
    })
  }

  showUpdateTip() {
    // 显示一个不打扰的提示
    uni.showToast({
      title: '新版本已准备就绪',
      icon: 'none',
      duration: 2000
    })

    // 设置角标或红点
    uni.setTabBarBadge({
      index: 3,  // 设置页的索引
      text: '1'
    })
  }

  // 用户下次启动时安装
  installOnNextLaunch() {
    const packagePath = uni.getStorageSync('update_package_path')
    if (packagePath) {
      uni.showModal({
        title: '更新提示',
        content: '检测到新版本已下载完成,是否立即安装?',
        success: (res) => {
          if (res.confirm) {
            this.installUpdate(packagePath)
          }
        }
      })
    }
  }

  installUpdate(filePath) {
    // #ifdef APP-PLUS
    plus.runtime.install(filePath, {}, () => {
      // 清除缓存
      uni.removeStorageSync('update_package_path')
      // 重启应用
      plus.runtime.restart()
    })
    // #endif
  }
}

export default new SilentUpdateManager()

2.4 热更新(Hot Update)

使用场景

  • 紧急bug修复
  • H5页面更新
  • 业务逻辑调整
  • 不需要审核的更新
uni-app wgt包热更新
javascript
// utils/hot-update.js
class HotUpdateManager {
  async check() {
    try {
      const res = await uni.request({
        url: 'https://api.xxx.com/wgt/version',
        data: {
          platform: plus.os.name,
          currentVersion: plus.runtime.version
        }
      })

      if (res.data.hasUpdate) {
        this.downloadWgt(res.data.wgtUrl, res.data.version)
      }
    } catch (error) {
      console.error('检查热更新失败:', error)
    }
  }

  downloadWgt(url, version) {
    // 后台静默下载
    uni.downloadFile({
      url,
      success: (res) => {
        if (res.statusCode === 200) {
          // 安装wgt包
          plus.runtime.install(res.tempFilePath, {
            force: false
          }, () => {
            console.log('wgt安装成功')

            // 提示用户重启
            uni.showModal({
              title: '更新完成',
              content: '应用需要重启以完成更新',
              showCancel: false,
              success: () => {
                plus.runtime.restart()
              }
            })
          }, (error) => {
            console.error('wgt安装失败:', error)
          })
        }
      }
    })
  }
}

export default new HotUpdateManager()

三、灰度发布策略

3.1 按用户百分比灰度

javascript
// server/gray-release.js
class GrayReleaseManager {
  checkUserInGray(userId, grayPercentage) {
    // 使用userId的hash值决定是否在灰度范围
    const hash = this.hashCode(userId)
    const bucket = hash % 100

    return bucket < grayPercentage
  }

  hashCode(str) {
    let hash = 0
    for (let i = 0; i < str.length; i++) {
      hash = ((hash << 5) - hash) + str.charCodeAt(i)
      hash = hash & hash
    }
    return Math.abs(hash)
  }

  getUpdateInfo(userId, currentVersion) {
    const config = {
      // 灰度配置
      grayRelease: {
        enabled: true,
        percentage: 20,  // 20%用户
        startTime: '2024-01-01 00:00:00',
        endTime: '2024-01-03 23:59:59'
      },

      // 最新版本
      latestVersion: {
        versionName: '2.0.0',
        versionCode: 200,
        downloadUrl: 'https://cdn.xxx.com/app-2.0.0.apk'
      },

      // 稳定版本
      stableVersion: {
        versionName: '1.9.0',
        versionCode: 190,
        downloadUrl: 'https://cdn.xxx.com/app-1.9.0.apk'
      }
    }

    // 灰度期间
    if (config.grayRelease.enabled) {
      const now = Date.now()
      const start = new Date(config.grayRelease.startTime).getTime()
      const end = new Date(config.grayRelease.endTime).getTime()

      if (now >= start && now <= end) {
        // 判断用户是否在灰度范围
        if (this.checkUserInGray(userId, config.grayRelease.percentage)) {
          return config.latestVersion
        } else {
          return config.stableVersion
        }
      }
    }

    // 灰度结束,全量推送
    return config.latestVersion
  }
}

3.2 按地区灰度

javascript
class RegionGrayRelease {
  getUpdateInfo(userId, region) {
    const config = {
      grayRegions: ['beijing', 'shanghai', 'guangzhou'],
      latestVersion: { /* ... */ },
      stableVersion: { /* ... */ }
    }

    if (config.grayRegions.includes(region)) {
      return config.latestVersion
    } else {
      return config.stableVersion
    }
  }
}

3.3 按设备灰度

javascript
class DeviceGrayRelease {
  getUpdateInfo(deviceInfo) {
    const config = {
      // Android 设备灰度
      android: {
        brands: ['xiaomi', 'huawei', 'oppo'],
        minVersion: '8.0'
      },
      // iOS 设备灰度
      ios: {
        minVersion: '13.0'
      }
    }

    if (deviceInfo.platform === 'android') {
      const brandMatch = config.android.brands.includes(deviceInfo.brand)
      const versionMatch = this.compareVersion(
        deviceInfo.osVersion,
        config.android.minVersion
      ) >= 0

      if (brandMatch && versionMatch) {
        return this.getLatestVersion()
      }
    }

    return this.getStableVersion()
  }
}

3.4 白名单机制

javascript
class WhitelistManager {
  constructor() {
    this.whitelist = [
      'user_test_001',  // 测试账号
      'user_test_002',
      'user_vip_001'    // VIP用户
    ]

    this.blacklist = [
      'user_problem_001'  // 问题用户
    ]
  }

  checkAccess(userId) {
    // 黑名单直接拒绝
    if (this.blacklist.includes(userId)) {
      return {
        allow: false,
        reason: 'blacklist'
      }
    }

    // 白名单直接通过
    if (this.whitelist.includes(userId)) {
      return {
        allow: true,
        priority: 'high'
      }
    }

    return { allow: true }
  }

  getUpdateInfo(userId) {
    const access = this.checkAccess(userId)

    if (!access.allow) {
      return this.getStableVersion()
    }

    if (access.priority === 'high') {
      return this.getLatestVersion()
    }

    // 普通用户走灰度逻辑
    return this.getGrayVersion(userId)
  }
}

四、更新通知时机

4.1 启动时通知(最常用)

javascript
// App.vue
export default {
  onLaunch() {
    // 延迟2秒检查,避免影响启动体验
    setTimeout(() => {
      this.checkUpdate()
    }, 2000)
  }
}

优点

  • 覆盖率高,每次启动都能检查
  • 用户体验好,不打断使用
缺点
  • 频繁启动的用户可能觉得烦

4.2 定时检查

javascript
class ScheduledUpdateChecker {
  constructor() {
    this.checkInterval = 24 * 60 * 60 * 1000  // 24小时
    this.timer = null
  }

  start() {
    this.check()

    this.timer = setInterval(() => {
      this.check()
    }, this.checkInterval)
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer)
    }
  }

  async check() {
    const lastCheckTime = uni.getStorageSync('last_check_time') || 0
    const now = Date.now()

    // 距离上次检查超过24小时才检查
    if (now - lastCheckTime > this.checkInterval) {
      await this.checkUpdate()
      uni.setStorageSync('last_check_time', now)
    }
  }
}

4.3 关键操作时提示

javascript
// 用户在关键操作时提示更新
class CriticalOperationUpdateChecker {
  checkBeforeOperation(operationType) {
    const needUpdate = this.shouldUpdate()

    if (needUpdate) {
      uni.showModal({
        title: '建议更新',
        content: `检测到新版本,建议更新后再${operationType}`,
        confirmText: '立即更新',
        cancelText: '继续操作',
        success: (res) => {
          if (res.confirm) {
            this.startUpdate()
          } else {
            this.continueOperation()
          }
        }
      })
    } else {
      this.continueOperation()
    }
  }
}

// 使用示例
// 用户要发起支付时
onPayment() {
  updateChecker.checkBeforeOperation('支付')
}

// 用户要上传文件时
onUpload() {
  updateChecker.checkBeforeOperation('上传文件')
}

4.4 空闲时检查

javascript
class IdleUpdateChecker {
  constructor() {
    this.idleTime = 5 * 60 * 1000  // 5分钟空闲
    this.lastActivityTime = Date.now()
    this.timer = null
  }

  start() {
    // 监听用户活动
    this.addActivityListeners()

    // 定时检查是否空闲
    this.timer = setInterval(() => {
      this.checkIdle()
    }, 60 * 1000)  // 每分钟检查一次
  }

  addActivityListeners() {
    // 监听页面切换
    uni.$on('page-changed', () => {
      this.updateActivityTime()
    })

    // 监听触摸事件(需要在各页面添加)
    // onTouchStart() { this.updateActivityTime() }
  }

  updateActivityTime() {
    this.lastActivityTime = Date.now()
  }

  checkIdle() {
    const idleDuration = Date.now() - this.lastActivityTime

    if (idleDuration >= this.idleTime) {
      // 用户空闲,检查更新
      this.checkUpdate()
    }
  }
}

五、业界最佳实践

5.1 微信更新策略

特点

  1. 启动时静默检查更新
  2. 有更新时显示红点提示
  3. 用户手动进入才弹窗
  4. 下载在后台进行
  5. 安装需要用户确认
借鉴点
  • 不主动打断用户
  • 用红点吸引用户注意
  • 给用户选择权

5.2 抖音更新策略

特点

  1. 启动时检查更新
  2. WiFi下自动下载
  3. 下载完成后角标提示
  4. 下次启动时提示安装
借鉴点
  • 静默下载不打扰
  • 角标提示很温和
  • 延迟到下次启动安装

5.3 淘宝更新策略

特点

  1. 强制更新很少用
  2. 推荐更新可跳过多次
  3. 跳过3次后加强提示
  4. 关键功能前再次提示
借鉴点
  • 给用户多次选择机会
  • 逐步加强提示力度
  • 在关键节点提醒

5.4 推荐策略组合

javascript
// 综合最佳实践
class BestPracticeUpdateManager {
  async checkUpdate() {
    const updateInfo = await this.getUpdateInfo()

    if (!updateInfo.hasUpdate) {
      return
    }

    // 1. 强制更新:直接弹窗,无法跳过
    if (updateInfo.updateType === 'force') {
      this.showForceUpdate(updateInfo)
      return
    }

    // 2. 推荐更新:根据用户跳过次数决定策略
    const skipCount = this.getSkipCount()

    if (skipCount === 0) {
      // 首次提示:温和
      this.showGentleReminder(updateInfo)
    } else if (skipCount < 3) {
      // 2-3次:稍微强化
      this.showNormalReminder(updateInfo)
    } else {
      // 3次以上:加强提示
      this.showStrongReminder(updateInfo)
    }

    // 3. 静默更新:WiFi下后台下载
    if (updateInfo.updateType === 'silent') {
      this.silentDownload(updateInfo)
    }
  }

  showGentleReminder(info) {
    // 只在设置页显示红点
    uni.setTabBarBadge({ index: 3, text: '1' })

    // 不弹窗,用户主动进入设置才看到
  }

  showNormalReminder(info) {
    // 启动时弹窗,可跳过
    setTimeout(() => {
      uni.showModal({
        title: '发现新版本',
        content: info.updateInfo,
        confirmText: '更新',
        cancelText: '稍后'
      })
    }, 2000)
  }

  showStrongReminder(info) {
    // 立即弹窗,突出显示
    uni.showModal({
      title: '强烈建议更新',
      content: `${info.updateInfo}\n\n已连续跳过${this.getSkipCount()}次`,
      confirmText: '立即更新',
      cancelText: '忽略',
      confirmColor: '#FF0000'
    })
  }
}

六、服务端实现

6.1 版本管理接口

javascript
// server/routes/version.js
const express = require('express')
const router = express.Router()

// 检查更新
router.get('/check', async (req, res) => {
  const {
    platform,      // android/ios
    currentVersion,
    userId,
    deviceId,
    region,
    channel
  } = req.query

  try {
    // 获取版本配置
    const versionConfig = await getVersionConfig(platform)

    // 判断是否需要更新
    const needUpdate = compareVersion(
      currentVersion,
      versionConfig.latest.versionCode
    ) < 0

    if (!needUpdate) {
      return res.json({ hasUpdate: false })
    }

    // 判断更新类型
    const updateType = determineUpdateType(
      currentVersion,
      userId,
      versionConfig
    )

    // 灰度判断
    const targetVersion = checkGrayRelease(
      userId,
      region,
      versionConfig
    )

    res.json({
      hasUpdate: true,
      versionName: targetVersion.versionName,
      versionCode: targetVersion.versionCode,
      downloadUrl: targetVersion.downloadUrl,
      updateType,  // force/recommended/silent
      updateInfo: targetVersion.updateInfo,
      fileSize: targetVersion.fileSize,
      md5: targetVersion.md5
    })

    // 记录检查日志
    await logUpdateCheck({
      userId,
      deviceId,
      currentVersion,
      targetVersion: targetVersion.versionName,
      updateType
    })

  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

// 记录更新行为
router.post('/action', async (req, res) => {
  const {
    userId,
    action,  // skip/download/install
    version
  } = req.body

  await logUpdateAction({
    userId,
    action,
    version,
    timestamp: Date.now()
  })

  res.json({ success: true })
})

// 获取更新统计
router.get('/stats', async (req, res) => {
  const { version } = req.query

  const stats = await UpdateStats.findOne({ version })

  res.json({
    version,
    totalUsers: stats.totalUsers,
    updatedUsers: stats.updatedUsers,
    updateRate: (stats.updatedUsers / stats.totalUsers * 100).toFixed(2) + '%',
    skipCount: stats.skipCount,
    downloadCount: stats.downloadCount,
    installCount: stats.installCount
  })
})

module.exports = router

6.2 版本配置管理

javascript
// server/models/version-config.js
const versionConfig = {
  android: {
    latest: {
      versionName: '2.0.0',
      versionCode: 200,
      downloadUrl: 'https://cdn.xxx.com/app-2.0.0.apk',
      updateInfo: '1. 新增视频功能\n2. 优化性能\n3. 修复已知问题',
      fileSize: 50 * 1024 * 1024,  // 50MB
      md5: 'abc123...',
      releaseTime: '2024-01-01 10:00:00',

      // 更新策略
      updateStrategy: {
        // 强制更新最低版本
        forceUpdateMin: '1.5.0',

        // 推荐更新
        recommendedUpdate: true,

        // 静默更新(小版本)
        silentUpdate: false
      }
    },

    // 灰度配置
    grayRelease: {
      enabled: true,
      percentage: 20,
      whitelist: ['user_001', 'user_002'],
      blacklist: ['user_999'],
      regions: ['beijing', 'shanghai'],
      startTime: '2024-01-01 00:00:00',
      endTime: '2024-01-03 23:59:59'
    },

    // 渠道配置
    channels: {
      huawei: {
        downloadUrl: 'https://cdn.xxx.com/app-huawei-2.0.0.apk'
      },
      xiaomi: {
        downloadUrl: 'https://cdn.xxx.com/app-xiaomi-2.0.0.apk'
      }
    }
  },

  ios: {
    latest: {
      versionName: '2.0.0',
      versionCode: 200,
      appStoreUrl: 'https://apps.apple.com/app/idxxxxx',
      updateInfo: '1. 新增视频功能\n2. 优化性能\n3. 修复已知问题',
      releaseTime: '2024-01-01 10:00:00'
    }
  }
}

6.3 推送通知管理

javascript
// server/services/push-notification.js
class PushNotificationService {
  async sendUpdateNotification(userIds, versionInfo) {
    // 批量推送
    const batchSize = 1000

    for (let i = 0; i < userIds.length; i += batchSize) {
      const batch = userIds.slice(i, i + batchSize)

      await this.pushToBatch(batch, {
        title: '版本更新',
        content: `${versionInfo.name} 已发布,快来体验新功能!`,
        payload: {
          type: 'app_update',
          version: versionInfo.name,
          url: versionInfo.downloadUrl
        }
      })

      // 避免频率限制
      await this.sleep(1000)
    }
  }

  async pushToBatch(userIds, notification) {
    // 获取用户的推送token
    const users = await User.find({
      _id: { $in: userIds },
      pushToken: { $exists: true }
    })

    const tokens = users.map(u => u.pushToken)

    // 调用推送服务
    await this.callPushAPI(tokens, notification)
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

七、数据监控和分析

7.1 更新漏斗分析

javascript
// 监控更新的每个环节
const updateFunnel = {
  // 1. 检查更新
  checkUpdate: 100000,      // 10万用户检查更新

  // 2. 发现更新
  hasUpdate: 80000,         // 8万用户有更新

  // 3. 看到弹窗
  showDialog: 70000,        // 7万用户看到弹窗

  // 4. 点击更新
  clickUpdate: 50000,       // 5万用户点击更新

  // 5. 开始下载
  startDownload: 48000,     // 4.8万开始下载

  // 6. 下载完成
  downloadComplete: 45000,  // 4.5万下载完成

  // 7. 开始安装
  startInstall: 43000,      // 4.3万开始安装

  // 8. 安装完成
  installComplete: 40000,   // 4万安装完成

  // 转化率
  conversionRate: {
    updateRate: '40%',       // 最终更新率
    downloadRate: '96%',     // 下载完成率
    installRate: '93%'       // 安装完成率
  }
}

7.2 数据埋点

javascript
// utils/analytics.js
class UpdateAnalytics {
  // 检查更新
  trackCheckUpdate(currentVersion) {
    this.report('update_check', {
      current_version: currentVersion,
      timestamp: Date.now()
    })
  }

  // 显示弹窗
  trackShowDialog(version, updateType) {
    this.report('update_dialog_show', {
      version,
      update_type: updateType
    })
  }

  // 点击更新
  trackClickUpdate(version) {
    this.report('update_click', {
      version
    })
  }

  // 跳过更新
  trackSkipUpdate(version, skipCount) {
    this.report('update_skip', {
      version,
      skip_count: skipCount
    })
  }

  // 开始下载
  trackStartDownload(version) {
    this.report('update_download_start', {
      version,
      network_type: uni.getSystemInfoSync().networkType
    })
  }

  // 下载进度
  trackDownloadProgress(version, progress) {
    // 每10%上报一次
    if (progress % 10 === 0) {
      this.report('update_download_progress', {
        version,
        progress
      })
    }
  }

  // 下载完成
  trackDownloadComplete(version, duration) {
    this.report('update_download_complete', {
      version,
      duration
    })
  }

  // 安装完成
  trackInstallComplete(version) {
    this.report('update_install_complete', {
      version
    })
  }

  // 更新失败
  trackUpdateError(version, error, step) {
    this.report('update_error', {
      version,
      error: error.message,
      step  // download/install
    })
  }

  report(eventName, data) {
    uni.request({
      url: 'https://analytics.xxx.com/track',
      method: 'POST',
      data: {
        event: eventName,
        properties: {
          ...data,
          user_id: this.getUserId(),
          device_id: this.getDeviceId(),
          platform: uni.getSystemInfoSync().platform
        }
      }
    })
  }
}

export default new UpdateAnalytics()

八、总结和建议

推荐策略组合

10万用户的更新方案

  1. 第1天:灰度10%(1万用户)
    • 方式:应用内启动检查
    • 类型:推荐更新
    • 监控:密切观察崩溃率、反馈
  2. 第3天:灰度50%(5万用户)
    • 方式:应用内启动检查 + 推送通知
    • 类型:推荐更新
    • 监控:持续观察数据
  3. 第5天:全量100%(10万用户)
    • 方式:应用内启动检查 + 推送通知 + 应用内消息
    • 类型:推荐更新
    • 逐步加强:跳过3次后变强提示
  4. 第7天:加强提示
    • 对未更新用户加强提示
    • 关键功能前再次提醒
    • 考虑短信/邮件通知
  5. 第10天:考虑强制
    • 如果重要更新,开始强制
    • 给用户最后期限
    • 旧版本逐步不可用

不同场景选择

更新类型 通知方式 更新策略 适用场景
严重bug 强制更新 立即强制 安全漏洞、核心功能不可用
重要功能 推送+应用内 推荐更新 新功能上线
性能优化 应用内 推荐更新 性能提升
小bug修复 静默更新 后台下载 不影响使用
资源更新 热更新 wgt包 H5页面、配置

关键数据指标

javascript
const updateMetrics = {
  // 覆盖率
  coverageRate: '检查更新用户数 / 总用户数',

  // 更新率
  updateRate: '完成更新用户数 / 检查更新用户数',

  // 转化率
  conversionRate: '点击更新用户数 / 看到弹窗用户数',

  // 下载成功率
  downloadSuccessRate: '下载完成数 / 开始下载数',

  // 安装成功率
  installSuccessRate: '安装完成数 / 开始安装数',

  // 平均更新时长
  averageUpdateTime: '总更新时长 / 更新用户数',

  // 跳过率
  skipRate: '跳过更新次数 / 弹窗显示次数'
}

// 健康指标参考
const healthMetrics = {
  coverageRate: '> 90%',       // 覆盖率要高
  updateRate: '> 60%',         // 更新率要达标
  downloadSuccessRate: '> 95%', // 下载成功率要高
  installSuccessRate: '> 90%',  // 安装成功率要高
  skipRate: '< 30%'            // 跳过率要低
}