获取更新
Android 客户端版本更新检查接口,支持多渠道、灰度发布和强制更新
概述
check-update 是 Android 客户端调用的版本更新检查接口。客户端上报当前版本号、设备信息和渠道,函数查询数据库中符合条件的最新版本并返回更新信息。
| 属性 | 值 |
|---|---|
| Slug | check-update |
| 方法 | POST |
| JWT 校验 | 是 |
| 状态 | Active |
核心逻辑
整个检查流程可以分为以下几个阶段:
参数校验
接收客户端 JSON 请求体,提取并校验必填字段。device_id 和 channel 为必填项,缺失或非法时直接返回 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 对应的活跃渠道这个机制允许将特定设备加入自定义渠道(如 nightly、internal),实现精确的定向推送,无需修改客户端代码。
查询候选版本
从 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 时(默认),所有设备都能收到版本排序与选择
通过后过滤的候选版本按以下优先级排序,取第一个作为推荐更新版本:
- 自定义渠道优先 — 非 stable/beta 的特殊渠道排在前面
- 版本号降序 — 优先推荐最新版本
- stable 优先于 beta — 同版本号时 stable 优先
- 创建时间降序 — 兜底排序
构建响应
根据选中的版本构建返回数据。通过比较 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 状态码 | 场景 | 响应体 |
|---|---|---|
| 400 | device_id 或 channel 缺失/非法 | {"error": "Missing or invalid required fields"} |
| 500 | 服务端环境变量未配置 | {"error": "Server env is not configured"} |
| 500 | 数据库查询异常 | {"error": "具体错误信息"} |
关联数据表
app_versions
存储每个版本的发布信息。
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
version_code | integer | 版本号(用于比较大小) |
version_name | text | 版本名(展示用,如 "1.3.0") |
channel | text | 发布渠道(stable / beta / 自定义) |
min_sdk | integer | 最低 Android SDK 版本要求 |
min_supported_code | integer | 最低支持的客户端版本号(低于此值强制更新),默认 1 |
staged_rollout_percent | integer | 灰度发布百分比 0-100,默认 100 |
apk_data | jsonb | APK 信息(url、size、filename、md5、splits_data) |
release_info_id | bigint | 关联 app_release_infos 表 |
created_at | timestamptz | 创建时间 |
app_release_infos
存储版本的更新日志和公告信息。
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
version_name | text | 版本名 |
release_notes | text | 更新日志 |
announcement | text | 版本公告 |
created_at | timestamptz | 创建时间 |
updated_at | timestamptz | 更新时间 |
app_channel_members
用于将特定设备加入自定义渠道,实现定向推送。
| 字段 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
device_id | text | 设备唯一标识 |
channel | text | 渠道名称 |
note | text | 备注 |
is_active | boolean | 是否生效,默认 true |
created_at | timestamptz | 创建时间 |
灰度发布说明
灰度发布基于设备 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_update 为 true。
客户端收到 force_update: true 后应阻止用户继续使用旧版本,强制引导升级。典型场景包括:
- 数据库 schema 发生破坏性变更
- 修复了严重的安全漏洞
- 下线了旧版本依赖的 API
版本选择优先级
当多个候选版本同时满足条件时,排序规则如下:
| 优先级 | 规则 | 说明 |
|---|---|---|
| 1 | 自定义渠道 > stable/beta | 定向推送的特殊渠道始终优先 |
| 2 | version_code 降序 | 推荐最新版本 |
| 3 | stable > beta | 同版本号时优先推荐稳定版 |
| 4 | created_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) }
}