Files
jizhi/docs/app-client-integration.md

8.6 KiB

App 客户端对接文档

本文面向 Android、iOS、Windows、macOS、Linux 等客户端,用于对接 VersionFlow 的公开检查更新接口。

1. 接口概览

客户端不需要登录后台,也不需要 JWT。每个 App 在后台创建后会生成一个公开标识 AppKey,客户端使用它检查是否有新版本。

接口地址:

GET /api/client/v1/update

本地 Docker 环境示例:

GET http://localhost:8080/api/client/v1/update?appKey={appKey}&platform=android&channel=stable&currentBuild=100

生产环境请替换为你的正式域名:

GET https://your-domain.com/api/client/v1/update?appKey={appKey}&platform=android&channel=stable&currentBuild=100

2. 请求参数

参数 类型 必填 说明
appKey string 后台应用详情页中的 AppKey。它是公开客户端标识,不是密钥。
platform string 平台键,例如 androidioswindowsmacoslinux
channel string 发布渠道,默认 stable。可使用 beta 或后台自定义渠道。
currentBuild number 当前客户端整数构建号,默认 0。必须大于等于 0。

注意:版本新旧只比较 buildNumber/currentBuild,不要用 versionName 做大小判断。

3. 成功响应

无可用更新:

{
  "hasUpdate": false,
  "forceUpdate": false,
  "release": null
}

有可用更新:

{
  "hasUpdate": true,
  "forceUpdate": false,
  "release": {
    "id": "2f0f4c1a-7c8f-4d5b-90fa-5cb0b2f3e1a1",
    "versionName": "2.4.0",
    "buildNumber": 240,
    "downloadUrl": "https://downloads.example.com/app-2.4.0.apk",
    "releaseNotes": "- 新增功能\n- 修复问题",
    "sha256": "可选的 64 位 SHA-256",
    "fileSize": 104857600,
    "publishedAt": "2026-07-20T05:00:00+00:00"
  }
}

字段说明:

字段 说明
hasUpdate 是否存在比 currentBuild 更高的已生效版本。
forceUpdate 是否必须升级。后台版本标记强制更新,或当前构建号低于 minimumSupportedBuild 时为 true
release.versionName 展示版本号,例如 2.4.0。仅用于显示。
release.buildNumber 目标版本整数构建号,用于比较版本新旧。
release.downloadUrl 安装包下载外链,系统不上传、不代理安装包。
release.releaseNotes Markdown 更新说明。App 内展示时建议做安全渲染或纯文本展示。
release.sha256 可选校验值。客户端下载后可用它校验安装包完整性。
release.fileSize 可选文件大小,单位字节。
release.publishedAt 发布时间,UTC 时间。

4. 错误响应

HTTP 状态码 场景
400 参数无效,例如缺少 appKeyplatform 为空、currentBuild 为负数。
404 AppKey 不存在、App 已停用、平台不存在/停用、渠道不存在/停用。
429 触发 IP/AppKey 限流。客户端应稍后重试。
500 服务端异常。客户端应降级为“不提示更新”,并记录日志。

错误响应使用 ASP.NET Core 标准 ProblemDetails 结构,常见格式如下:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Application not found",
  "status": 404
}

5. 版本选择规则

服务端会在指定 appKey + platform + channel 下选择已经生效、未归档且构建号最高的版本。

规则如下:

  1. App 停用时,客户端接口返回 404
  2. 草稿、归档版本不会返回给客户端。
  3. 定时发布版本在到达 scheduledAt 之前不可见,到期后可被选中。
  4. 当前客户端 currentBuild >= 最新 buildNumber 时,返回 hasUpdate=false
  5. 存在更新且版本标记了 forceUpdate=true 时,返回 forceUpdate=true
  6. 存在更新且 currentBuild < minimumSupportedBuild 时,返回 forceUpdate=true
  7. stablebeta 和自定义渠道互相隔离,不会串用版本。
  8. Android、iOS、Windows、macOS、Linux 等平台互相隔离,不会串用版本。

6. App 端推荐流程

启动后或进入设置页时检查更新即可,不建议每次前后台切换都请求。

推荐流程:

  1. 读取当前客户端构建号 currentBuild
  2. 根据当前包的平台和渠道组装请求。
  3. 请求失败时静默降级,不阻塞 App 启动。
  4. hasUpdate=false 时不提示。
  5. hasUpdate=true && forceUpdate=false 时显示可取消的更新弹窗。
  6. hasUpdate=true && forceUpdate=true 时显示不可取消的强制更新弹窗。
  7. 用户确认后打开 downloadUrl,或进入系统下载流程。
  8. 如果返回了 sha256,下载安装包后做完整性校验。

7. JavaScript/TypeScript 示例

type UpdateResponse = {
  hasUpdate: boolean
  forceUpdate: boolean
  release: null | {
    id: string
    versionName: string
    buildNumber: number
    downloadUrl: string
    releaseNotes: string
    sha256?: string | null
    fileSize?: number | null
    publishedAt: string
  }
}

export async function checkUpdate() {
  const baseUrl = 'https://your-domain.com'
  const params = new URLSearchParams({
    appKey: 'replace-with-app-key',
    platform: 'android',
    channel: 'stable',
    currentBuild: String(100),
  })

  const response = await fetch(`${baseUrl}/api/client/v1/update?${params}`)
  if (response.status === 404) return null
  if (response.status === 429) throw new Error('检查更新过于频繁,请稍后再试')
  if (!response.ok) throw new Error(`检查更新失败:${response.status}`)

  const result = (await response.json()) as UpdateResponse
  if (!result.hasUpdate || !result.release) return null

  return result
}

8. Android Kotlin 示例

data class UpdateResponse(
    val hasUpdate: Boolean,
    val forceUpdate: Boolean,
    val release: ReleaseInfo?
)

data class ReleaseInfo(
    val id: String,
    val versionName: String,
    val buildNumber: Long,
    val downloadUrl: String,
    val releaseNotes: String,
    val sha256: String?,
    val fileSize: Long?,
    val publishedAt: String
)

// 使用 OkHttp / Retrofit 均可,示例只展示 URL 组装
val url = HttpUrl.Builder()
    .scheme("https")
    .host("your-domain.com")
    .addPathSegments("api/client/v1/update")
    .addQueryParameter("appKey", "replace-with-app-key")
    .addQueryParameter("platform", "android")
    .addQueryParameter("channel", "stable")
    .addQueryParameter("currentBuild", BuildConfig.VERSION_CODE.toString())
    .build()

9. iOS Swift 示例

struct UpdateResponse: Decodable {
    let hasUpdate: Bool
    let forceUpdate: Bool
    let release: ReleaseInfo?
}

struct ReleaseInfo: Decodable {
    let id: String
    let versionName: String
    let buildNumber: Int64
    let downloadUrl: String
    let releaseNotes: String
    let sha256: String?
    let fileSize: Int64?
    let publishedAt: String
}

var components = URLComponents(string: "https://your-domain.com/api/client/v1/update")!
components.queryItems = [
    URLQueryItem(name: "appKey", value: "replace-with-app-key"),
    URLQueryItem(name: "platform", value: "ios"),
    URLQueryItem(name: "channel", value: "stable"),
    URLQueryItem(name: "currentBuild", value: "100")
]

let (data, response) = try await URLSession.shared.data(from: components.url!)
let http = response as! HTTPURLResponse
if http.statusCode == 200 {
    let result = try JSONDecoder().decode(UpdateResponse.self, from: data)
    // 根据 result.hasUpdate / result.forceUpdate 展示更新弹窗
}

10. 后台发布注意事项

为了让客户端能正确收到更新,后台发布版本时请确认:

  1. App 处于启用状态。
  2. 平台和渠道处于启用状态。
  3. buildNumber 大于线上客户端的 currentBuild
  4. 版本状态是已发布,或定时发布时间已经到期。
  5. downloadUrl 是可公开访问的 HTTP/HTTPS 绝对地址。
  6. 强制更新策略按需设置:forceUpdateminimumSupportedBuild

11. 本地调试

启动服务:

docker compose up -d

访问后台:

http://localhost:8080/

首次启动前配置引导管理员:

export Admin__BootstrapUsername='admin'
export Admin__BootstrapPassword='replace-with-a-random-password-of-at-least-12-characters'

首次登录后必须修改密码。正式环境使用 HTTPS,并保持 Admin__CookieSecure=true;不存在固定 默认密码,也不再通过请求头管理密钥登录。

检查更新示例:

curl "http://localhost:8080/api/client/v1/update?appKey=replace-with-app-key&platform=android&channel=stable&currentBuild=0"

停止服务:

docker compose down