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

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&currentBuild=100
```
生产环境请替换为你的正式域名:
```http
GET https://your-domain.com/api/client/v1/update?appKey={appKey}&platform=android&channel=stable&currentBuild=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&currentBuild=0"
```
停止服务:
```bash
docker compose down
```