285 lines
8.6 KiB
Markdown
285 lines
8.6 KiB
Markdown
# App 客户端对接文档
|
|
|
|
本文面向 Android、iOS、Windows、macOS、Linux 等客户端,用于对接 VersionFlow 的公开检查更新接口。
|
|
|
|
## 1. 接口概览
|
|
|
|
客户端不需要登录后台,也不需要 JWT。每个 App 在后台创建后会生成一个公开标识 `AppKey`,客户端使用它检查是否有新版本。
|
|
|
|
接口地址:
|
|
|
|
```http
|
|
GET /api/client/v1/update
|
|
```
|
|
|
|
本地 Docker 环境示例:
|
|
|
|
```http
|
|
GET http://localhost:8080/api/client/v1/update?appKey={appKey}&platform=android&channel=stable¤tBuild=100
|
|
```
|
|
|
|
生产环境请替换为你的正式域名:
|
|
|
|
```http
|
|
GET https://your-domain.com/api/client/v1/update?appKey={appKey}&platform=android&channel=stable¤tBuild=100
|
|
```
|
|
|
|
## 2. 请求参数
|
|
|
|
| 参数 | 类型 | 必填 | 说明 |
|
|
| --- | --- | --- | --- |
|
|
| `appKey` | string | 是 | 后台应用详情页中的 AppKey。它是公开客户端标识,不是密钥。 |
|
|
| `platform` | string | 是 | 平台键,例如 `android`、`ios`、`windows`、`macos`、`linux`。 |
|
|
| `channel` | string | 否 | 发布渠道,默认 `stable`。可使用 `beta` 或后台自定义渠道。 |
|
|
| `currentBuild` | number | 否 | 当前客户端整数构建号,默认 `0`。必须大于等于 0。 |
|
|
|
|
注意:版本新旧只比较 `buildNumber/currentBuild`,不要用 `versionName` 做大小判断。
|
|
|
|
## 3. 成功响应
|
|
|
|
无可用更新:
|
|
|
|
```json
|
|
{
|
|
"hasUpdate": false,
|
|
"forceUpdate": false,
|
|
"release": null
|
|
}
|
|
```
|
|
|
|
有可用更新:
|
|
|
|
```json
|
|
{
|
|
"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` | 参数无效,例如缺少 `appKey`、`platform` 为空、`currentBuild` 为负数。 |
|
|
| `404` | AppKey 不存在、App 已停用、平台不存在/停用、渠道不存在/停用。 |
|
|
| `429` | 触发 IP/AppKey 限流。客户端应稍后重试。 |
|
|
| `500` | 服务端异常。客户端应降级为“不提示更新”,并记录日志。 |
|
|
|
|
错误响应使用 ASP.NET Core 标准 ProblemDetails 结构,常见格式如下:
|
|
|
|
```json
|
|
{
|
|
"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. `stable`、`beta` 和自定义渠道互相隔离,不会串用版本。
|
|
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 示例
|
|
|
|
```ts
|
|
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 示例
|
|
|
|
```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 示例
|
|
|
|
```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. 强制更新策略按需设置:`forceUpdate` 或 `minimumSupportedBuild`。
|
|
|
|
## 11. 本地调试
|
|
|
|
启动服务:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
访问后台:
|
|
|
|
```text
|
|
http://localhost:8080/
|
|
```
|
|
|
|
首次启动前配置引导管理员:
|
|
|
|
```bash
|
|
export Admin__BootstrapUsername='admin'
|
|
export Admin__BootstrapPassword='replace-with-a-random-password-of-at-least-12-characters'
|
|
```
|
|
|
|
首次登录后必须修改密码。正式环境使用 HTTPS,并保持 `Admin__CookieSecure=true`;不存在固定
|
|
默认密码,也不再通过请求头管理密钥登录。
|
|
|
|
检查更新示例:
|
|
|
|
```bash
|
|
curl "http://localhost:8080/api/client/v1/update?appKey=replace-with-app-key&platform=android&channel=stable¤tBuild=0"
|
|
```
|
|
|
|
停止服务:
|
|
|
|
```bash
|
|
docker compose down
|
|
```
|