Logo花火漫画开发文档
函数详情

获取更新

Android 客户端版本更新检查接口,支持多渠道、灰度发布和强制更新

概述

check-update 是 Android 客户端调用的版本更新检查接口。客户端上报当前版本号、设备信息和渠道,函数查询数据库中符合条件的最新版本并返回更新信息。

属性
Slugcheck-update
方法POST
JWT 校验
状态Active

核心逻辑

整个检查流程可以分为以下几个阶段:

参数校验

接收客户端 JSON 请求体,提取并校验必填字段。device_idchannel 为必填项,缺失或非法时直接返回 400。

// 请求体字段
const deviceId = payload.device_id   // 必填,设备唯一标识
const currentCode = payload.current_version_code  // 当前版本号,默认 0
const sdkInt = payload.sdk_int       // Android SDK 版本,默认 0
const channel = payload.channel      // 必填,"stable" | "beta"

确定候选渠道

根据客户端上报的 channel 确定基础候选渠道列表,然后查询 app_channel_members 表,将该设备额外加入的渠道也纳入候选范围。

// beta 用户同时接收 stable 和 beta 更新
// stable 用户只接收 stable 更新
const candidateChannels = channel === "beta"
  ? ["stable", "beta"]
  : ["stable"]

// 追加设备级别的自定义渠道(如内测、灰度渠道)
// 从 app_channel_members 表查询 device_id 对应的活跃渠道

这个机制允许将特定设备加入自定义渠道(如 nightlyinternal),实现精确的定向推送,无需修改客户端代码。

查询候选版本

app_versions 表查询所有候选渠道中 version_code 大于当前版本的记录,同时关联 app_release_infos 获取更新日志。

SELECT
  channel, version_code, version_name,
  min_sdk, min_supported_code, staged_rollout_percent,
  apk_data, created_at,
  app_release_infos (release_notes, announcement)
FROM app_versions
WHERE channel IN (候选渠道列表)
  AND version_code > 当前版本号
ORDER BY version_code DESC

过滤与灰度

对查询结果进行两轮过滤:

SDK 版本过滤 — 如果候选版本设置了 min_sdk,则当前设备的 sdk_int 必须满足最低要求,不满足的直接排除。

灰度发布过滤 — 使用设备 ID 的 SHA-256 哈希前 4 字节取模 100 得到一个 0-99 的「桶号」,只有桶号小于 staged_rollout_percent 的设备才能收到该版本。

async function getDeviceBucket(deviceId: string): Promise<number> {
  const data = new TextEncoder().encode(deviceId)
  const hashBuffer = await crypto.subtle.digest("SHA-256", data)
  const hashPrefix = new DataView(hashBuffer).getUint32(0, false)
  return hashPrefix % 100
}

// 当 staged_rollout_percent = 10 时,只有桶号 0-9 的设备收到更新
// 当 staged_rollout_percent = 100 时(默认),所有设备都能收到

版本排序与选择

通过后过滤的候选版本按以下优先级排序,取第一个作为推荐更新版本:

  1. 自定义渠道优先 — 非 stable/beta 的特殊渠道排在前面
  2. 版本号降序 — 优先推荐最新版本
  3. stable 优先于 beta — 同版本号时 stable 优先
  4. 创建时间降序 — 兜底排序

构建响应

根据选中的版本构建返回数据。通过比较 min_supported_code 与当前版本号判断是否为强制更新。

// min_supported_code > currentCode → 强制更新
const isForceUpdate = validUpdate.min_supported_code != null
  ? toSafeNumber(validUpdate.min_supported_code, 0) > currentCode
  : false

请求格式

请求头

POST /functions/v1/check-update
Content-Type: application/json
Authorization: Bearer <ANON_KEY 或用户 JWT>

请求体

Prop

Type

请求示例

{
  "device_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "current_version_code": 25,
  "sdk_int": 34,
  "channel": "stable"
}

响应格式

无可用更新

{
  "update_available": false
}

有可用更新

Prop

Type

响应示例

{
  "update_available": true,
  "version_code": 27,
  "version_name": "1.3.0",
  "release_notes": "- 修复阅读器闪退问题\n- 新增夜间模式",
  "announcement": "",
  "apk_url": "https://cdn.example.com/releases/hanabimg-1.3.0.apk",
  "md5": "d41d8cd98f00b204e9800998ecf8427e",
  "splits_data": [
    {
      "url": "https://cdn.example.com/releases/hanabimg-1.3.0-arm64-v8a.apk",
      "size": 5242880,
      "filename": "hanabimg-1.3.0-arm64-v8a.apk",
      "arch": "arm64-v8a",
      "md5": "098f6bcd4621d373cade4e832627b4f6"
    }
  ],
  "force_update": false,
  "created_at": "2026-03-10T08:00:00.000Z"
}

错误响应

HTTP 状态码场景响应体
400device_idchannel 缺失/非法{"error": "Missing or invalid required fields"}
500服务端环境变量未配置{"error": "Server env is not configured"}
500数据库查询异常{"error": "具体错误信息"}

关联数据表

app_versions

存储每个版本的发布信息。

字段类型说明
idbigint主键
version_codeinteger版本号(用于比较大小)
version_nametext版本名(展示用,如 "1.3.0")
channeltext发布渠道(stable / beta / 自定义)
min_sdkinteger最低 Android SDK 版本要求
min_supported_codeinteger最低支持的客户端版本号(低于此值强制更新),默认 1
staged_rollout_percentinteger灰度发布百分比 0-100,默认 100
apk_datajsonbAPK 信息(url、size、filename、md5、splits_data)
release_info_idbigint关联 app_release_infos
created_attimestamptz创建时间

app_release_infos

存储版本的更新日志和公告信息。

字段类型说明
idbigint主键
version_nametext版本名
release_notestext更新日志
announcementtext版本公告
created_attimestamptz创建时间
updated_attimestamptz更新时间

app_channel_members

用于将特定设备加入自定义渠道,实现定向推送。

字段类型说明
idbigint主键
device_idtext设备唯一标识
channeltext渠道名称
notetext备注
is_activeboolean是否生效,默认 true
created_attimestamptz创建时间

灰度发布说明

灰度发布基于设备 ID 哈希实现,具有以下特点:

  • 确定性 — 同一设备 ID 始终得到相同的桶号,不会出现同一设备反复在「有更新/无更新」之间跳动的情况
  • 均匀分布 — SHA-256 哈希保证桶号在 0-99 之间均匀分布
  • 逐步放量 — 将 staged_rollout_percent 从 10 → 30 → 50 → 100 递增即可实现逐步放量,先收到更新的设备不受影响

灰度百分比的调整只能增不能减。如果从 50 降回 10,之前桶号 10-49 的设备虽然已经收到过更新提示,但再次检查时会被告知「无更新」,体验不一致。如需紧急回滚,应发布新版本而非调低百分比。

强制更新机制

app_versions 表中某条记录的 min_supported_code 大于客户端当前的 current_version_code 时,响应中 force_updatetrue

客户端收到 force_update: true 后应阻止用户继续使用旧版本,强制引导升级。典型场景包括:

  • 数据库 schema 发生破坏性变更
  • 修复了严重的安全漏洞
  • 下线了旧版本依赖的 API

版本选择优先级

当多个候选版本同时满足条件时,排序规则如下:

优先级规则说明
1自定义渠道 > stable/beta定向推送的特殊渠道始终优先
2version_code 降序推荐最新版本
3stable > beta同版本号时优先推荐稳定版
4created_at 降序兜底规则

Android 客户端调用示例

suspend fun checkUpdate(context: Context): UpdateResult {
    val supabase = /* your Supabase client */

    val response = supabase.functions.invoke(
        function = "check-update",
        body = buildJsonObject {
            put("device_id", getDeviceId(context))
            put("current_version_code", BuildConfig.VERSION_CODE)
            put("sdk_int", Build.VERSION.SDK_INT)
            put("channel", getUpdateChannel()) // "stable" or "beta"
        }
    )

    return response.body
        .toString()
        .let { Json.decodeFromString<UpdateResult>(it) }
}

On this page