需求是"改完 Android 端怎么送到手机上"。不发应用商店,也不想每次手动传包,
于是做了最小闭环:后端存一份安装包清单,App 启动时查、下载、交给系统安装器。
后端
- 新增 /app/release/latest 与 /app/release/download/{file},都免登录:
还没装上新包的客户端未必带着有效令牌,检查更新不该被登录挡住
- download_url 用相对路径,客户端拿自己配置的 API 地址拼——同一份清单要在
内网、公网、模拟器之间共用,写死域名只会让其中一个坏掉
- has_update 只比对 version_code,不比版本名字符串(0.10.0 与 0.9.0 的字典序是坑)
- 下载只认清单里登记的文件名,含 ..%2F 一律 9001
- scripts/publish_apk.py 负责复制 APK 并写 latest.json(sha256/size/published_at),
backend/static/apk/ 不进版本库
安卓
- AppUpdater:查清单 → 下到应用自己的 updates 目录(先写 .part 再改名)→
校验 sha256 → 经 FileProvider 交给系统安装器
- UpdateDialog:版本号、更新说明、下载进度、失败重试;「允许安装未知应用」
是系统开关,只跳设置页,不代点
- 启动时自动查一次,首页底部另有「检查更新」入口
- 图标换成和 Web 的 <Logo/> 同一套图形:墨绿圆角底、三条米白圆头线、陶土点;
同时出 mipmap 各密度 PNG、自适应图标与 512px 预览
- versionCode=2 / versionName=0.2.0;发布签名从 android/keystore.properties 读,
没有该文件时回落调试签名,保证别人 clone 下来也能 assembleRelease
文档
- API.md 第 7 节:两个接口、清单字段、签名与 sha256 的约定、9001
- DEPLOY.md 第 5 节:出包命令(JDK 17 + Gradle 8.9 + 干净的 GRADLE_USER_HOME)、
发布清单、传到服务器、验证
- ROADMAP 第 6 节记录本次范围与本机踩过的构建环境坑
验证
- 后端 pytest 103 passed;ruff check / format --check 全绿
- assembleRelease 成功,app-release.apk 7.4 MB,apksigner 验签显示 CN=Dadao Task
- 本地与生产 GET /app/release/latest?version_code=1 → has_update=true,
version_code=2 → false;下载返回 200 / application/vnd.android.package-archive
This commit is contained in:
@@ -26,8 +26,9 @@ android {
|
||||
applicationId = "com.dadao.task"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
// 版本号是 OTA 的唯一判据:后端只比对 version_code,改这里才算"有新版本"
|
||||
versionCode = 2
|
||||
versionName = "0.2.0"
|
||||
buildConfigField("String", "API_BASE", "\"$apiBase\"")
|
||||
}
|
||||
|
||||
@@ -51,6 +52,37 @@ android {
|
||||
keyAlias = "androiddebugkey"
|
||||
keyPassword = "android"
|
||||
}
|
||||
/**
|
||||
* 发布签名从 android/keystore.properties 读,证书本身不进仓库。
|
||||
*
|
||||
* 没有这个文件(别人 clone 下来、CI 没注入)时回落到调试签名:
|
||||
* assembleRelease 依然能跑,只是装不上已经装了正式版的机器——
|
||||
* 而这正是应该发生的:发布包的签名只能由发布者本人持有。
|
||||
*/
|
||||
create("release") {
|
||||
val file = rootProject.file("keystore.properties")
|
||||
if (file.exists()) {
|
||||
val props = Properties()
|
||||
file.inputStream().use { props.load(it) }
|
||||
storeFile = rootProject.file(props.getProperty("storeFile"))
|
||||
storePassword = props.getProperty("storePassword")
|
||||
keyAlias = props.getProperty("keyAlias")
|
||||
keyPassword = props.getProperty("keyPassword")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// 没有正式证书时回落调试签名,保证任何机器都能 assembleRelease
|
||||
signingConfig = if (rootProject.file("keystore.properties").exists()) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -69,6 +101,8 @@ dependencies {
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.activity:activity-compose:1.9.3")
|
||||
// FileProvider(应用内升级把安装包交给系统安装器)就在 core 里,显式声明不靠传递依赖
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
||||
|
||||
Reference in New Issue
Block a user