12 KiB
Google Play 上架待办清单
排查时间:2026-08-29。基线产物:
build/app/outputs/flutter-apk/app-release.apk(1.0.1+4,Channel=CHINA_XIAOMI)。 所有结论都是在构建产物上实测的(合并后的 AndroidManifest、APK 里的 ELF 段对齐),不是读源码猜的。
进度(2026-09-12 更新)
基线产物换成 build_outputs/EAIMAR_0912_1.0.1_google.aab(1.0.1+7,Channel=GLOBAL_GOOGLE),
下面每条都是在这个 aab 上复验过的,不是改完就算数。
| 项 | 状态 | 复验方式 |
|---|---|---|
| 1.1 targetSdk 36 | ✅ 已改 | 合并 manifest targetSdkVersion="36" |
| 1.2 Play Billing 8 | ✅ 已改 | aab 的 dependencies.pb 里 com.android.billingclient:billing 8.0.0 |
| 1.3 16KB 页对齐 | ✅ 已改 | python3 check_16kb.py <aab> → 64 位 .so 44/44 合规 |
| 2. 支付回退 | ✅ 已改 | Google 渠道硬闸门,见下 |
| 3. 权限瘦身 | 🟡 部分 | GET_TASKS、MEDIA_CONTENT_CONTROL 已移除(44→40 条),其余待定 |
| 4. 构建脚本 | ✅ 已改 | ./build_channels.sh google 实测跑通 |
| 5. Play Console 事项 | ⬜ 未做 | 外部系统 |
⚠️ Billing 8 的迁移成本远低于本文当初的估计:0.5.0 唯一的破坏性变更是移除
queryPurchaseHistory,本项目零引用。实际改动就是 in_app_purchase: ^3.3.0 +
in_app_purchase_android: ^0.5.0 两行,没碰一行业务代码。
⚠️ 支付回退的修法与本文第 2 节的建议不同,刻意的:本文建议「安卓端兜底只留 google」,
但那会连带改掉国内五家渠道的兜底行为(后端没下发 paychannels 时国内用户就付不了款)。
实际实现是只对 Google 渠道加硬闸门:_isGooglePlayBuild 为真时直接返回
[google],既不信后端下发的渠道列表、也不走任何回退分支;其它渠道逻辑原封不动。
渠道判定读的是编译期烧进包里的 .env 的 Channel,⚠️ 不要换成
VersionUpdateService.cachedChannel——那个值在版本检查跑完前会回退成 GLOBAL_GOOGLE,
用它会在国内包启动初期把微信/支付宝误杀。
⚠️ check_16kb.py 当初并没有入库(本文 1.3 节说「仓库里已有」是错的),
2026-09-12 才补写并提交。
仍然待办
ACCESS_BACKGROUND_LOCATION+WRITE_SETTINGS主要由amap_service插件拖进来 (ACCESS_BACKGROUND_LOCATION另有location_service和主工程两个来源)。 而NavigationManager全项目只在initial_binding.dart用Get.lazyPut注册过, 没有任何一处Get.find——高德导航在当前 App 里是走不到的死代码。 所以「Google 渠道不引入amap_service」能一次干掉这两条最难过审的权限, 且不会有功能损失(也顺带证明了排除高德.so不会崩:没有代码路径去加载它)。QUERY_ALL_PACKAGES(主工程)、SYSTEM_ALERT_WINDOW(主工程 +floating_ui_plugin)、SCHEDULE_EXACT_ALARM(主工程)仍需声明表或删除。- targetSdk 36 的真机回归没做(edge-to-edge / 前台服务类型校验),见 1.1 节的警告。
- iOS 侧有同类的支付政策风险:本次的闸门只加在 Google 渠道,App Store 包在
paychannels缺失时仍会回退成展示微信/支付宝/PayPal。
0. 先决定:要不要申请延期
有两条硬期限是 2026-08-31,也就是排查次日。两条都可以在 Play Console 申请延期到 2026-11-01。
建议先去 Play Console 把延期申请了,再按下面的顺序改。否则 8/31 之后新包传不上去,改了也白改。
1. 硬阻塞
1.1 targetSdk 35 → 36(期限 2026-08-31)
- 位置:android/app/build.gradle.kts
defaultConfig.targetSdk = 35 compileSdk已经是 36,改一个数字即可- ⚠️ 但 Android 16 有行为变更,改完必须真机回归:
- 强制 edge-to-edge(状态栏/导航栏不再自动留白,页面可能被遮)
- 方向与尺寸可调整性 API 变更(大屏/折叠屏)
- 前台服务类型校验更严
1.2 Play Billing 7.1.1 → 8.0+(期限 2026-08-31)
- 当前:
in_app_purchase_android依赖com.android.billingclient:billing:7.1.1 - 要升到带 Billing Library 8 的
in_app_purchase_android版本 - ⚠️ BL8 有破坏性 API 变更,是这份清单里改动量最大的一项。来不及就务必申请延期。
- 迁移指南:https://developer.android.com/google/play/billing/migrate-gpblv8
1.3 16KB 页对齐(已于 2025-11-01 生效,无延期)
实测 arm64-v8a 共 26 个 .so,3 个不合规(LOAD 段 p_align = 0x1000,需 ≥ 0x4000):
| 库 | 来源 | 处理 |
|---|---|---|
libAMapSDK_NAVI_v10_0_800.so |
高德导航(预编译) | ✅ 已处理——build.gradle.kts 的 jniLibs.excludes 在 Channel=GLOBAL_GOOGLE 时排除 **/libAMapSDK*.so |
libg722_jni.so |
自己的源码 | ❌ 待修 |
libmsbc_jni.so |
自己的源码 | ❌ 待修 |
后两个来自 local_plugins/bluetooth_manager/android/src/main/cpp(g722_jni.cpp / msbc_jni.c,CMake 工程),源码在仓库里,好修。
根因:该插件的 android/build.gradle 没有声明 ndkVersion,所以没继承主工程的 NDK 28.2.13676358。NDK r28 起默认就是 16KB 对齐,用了老 NDK 才退回 4KB。
两种改法(建议都加,双保险):
// local_plugins/bluetooth_manager/android/build.gradle
android {
ndkVersion = "28.2.13676358" // 与主工程一致,r28 默认 16KB 对齐
defaultConfig {
externalNativeBuild {
cmake {
cppFlags ""
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" // NDK r27+ 支持
}
}
}
}
兜底(任何 NDK 版本都生效),在 src/main/cpp/CMakeLists.txt 里给两个 target 加:
target_link_options(g722_jni PRIVATE "-Wl,-z,max-page-size=16384")
target_link_options(msbc_jni PRIVATE "-Wl,-z,max-page-size=16384")
⚠️ 改完要清 local_plugins/bluetooth_manager/android/.cxx/(里面有旧的构建缓存),否则可能直接复用旧产物。
armeabi-v7a 的不合规项不用管:16KB 只约束 64 位(arm64-v8a / x86_64)。v7a 里的 libneonui_shared.so、libneonuijni_public.so 是 32 位专有,不影响上架。
验证方式(仓库里已有现成脚本):
python3 check_16kb.py build/app/outputs/bundle/release/app-release.aab
2. 政策风险:支付回退逻辑(优先级仅次于硬阻塞)
这是最容易被下架的一条。
lib/modules/goods/controllers/goods_base_controller.dart 的 _getEnabledPayMethods():当后端没下发 paychannels 时,回退成展示 微信 + 支付宝 + PayPal + Google。
// 若后端未下发或全部被平台过滤,则回退为展示所有平台支持的方式
if (methods.isEmpty) {
final fallback = <_PayMethod>[
_PayMethod.wechat,
_PayMethod.alipay,
_PayMethod.paypal
];
...
}
数字商品在 Google Play 必须走 Play 结算,摆出微信/支付宝是最典型的下架理由。而且这个回退恰好在配置缺失时触发——排查当天测试环境就正处于这个状态(渠道配置没建,paychannels 是空的)。
两件事要一起做:
- 改回退逻辑:安卓端兜底只留
google(iOS 只留apple),不要「拿不到配置就全展示」 - 后台配置:console 后台给
GLOBAL_GOOGLE渠道的paychannels只填google
3. 权限瘦身
合并后的 AndroidManifest 共 44 条权限,下面这些会被 Play 拦或需要额外声明:
| 权限 | 处理建议 |
|---|---|
QUERY_ALL_PACKAGES |
需提交声明表;通常要求改成 <queries> 精确列出微信/支付宝包名(来源应是 fluwx / tobias) |
ACCESS_BACKGROUND_LOCATION |
需声明表 + 演示视频,审核最严。先确认是否真需要后台定位,不需要就删 |
SYSTEM_ALERT_WINDOW |
悬浮窗(floating_ui_plugin),需说明用途 |
WRITE_SETTINGS |
很少被批准,确认是否真需要 |
SCHEDULE_EXACT_ALARM |
有闹钟功能可解释,准备好说明 |
MEDIA_CONTENT_CONTROL |
建议直接删——这是系统签名权限,普通应用根本拿不到,声明了也无效,反而显得可疑 |
GET_TASKS |
建议直接删——API 21 起已废弃并被系统忽略 |
源清单里的 SEND_SMS、MANAGE_EXTERNAL_STORAGE 已经是注释状态,合并产物里没有,不用担心。
查合并后清单的命令:
AAPT=$(ls ~/Library/Android/sdk/build-tools/*/aapt2 | tail -1)
$AAPT dump permissions build/app/outputs/flutter-apk/app-release.apk | grep '^uses-permission'
4. 构建流程的坑
.env的Channel必须是GLOBAL_GOOGLE(当前是CHINA_XIAOMI)。./build_channels.sh google会自动改;手工跑flutter build appbundle不会,那样 AMap 的.so也不会被排除 → 16KB 检查直接挂。- build_channels.sh 的
OUTPUT_DIR写死成/Users/liuxiamin/work/build_outputs,本机不存在,脚本会失败。改成本机路径或改成相对路径。 - Google 渠道排除了 AMap
.so,但调用代码还在。要验证海外包点到地图/导航相关功能不会崩。 - 产物是 AAB 不是 APK(
build_channels.sh的 google 分支已经处理成aab)。
5. Play Console 首次上架事项
- Play App Signing(上传密钥用
android/app/sign/eaimar.keystore) - 数据安全表单(Data safety)
- 内容分级问卷
- 目标受众与内容声明
- 隐私政策 URL —— iOS 那边已在用
https://yimai.games/privacy.html,可复用
建议的动手顺序
- 去 Play Console 申请 targetSdk 与 Billing 的延期(两条都是 8/31 到期)
- 16KB 对齐(改 1 个 gradle + 1 个 CMakeLists,最确定,改完用
check_16kb.py验) - 支付回退逻辑 + 后台
paychannels配置 - 权限瘦身(先删
MEDIA_CONTENT_CONTROL/GET_TASKS,再处理QUERY_ALL_PACKAGES) build_channels.sh的OUTPUT_DIR改本机路径- targetSdk 36 + 真机回归
- Billing 8 迁移(改动最大,放最后)
附:本次会话的其他遗留(与 Google Play 无关)
- iOS 包
1.0.1 (5)已打好待上传:build/ios/ipa/EAIMAR.ipa。已修掉通讯录缺用途说明(ITMS-90683),并移除了 ATT/IDFA(NSUserTrackingUsageDescription等三个 key、app_tracking_transparency依赖、splash 里的授权请求)。 ⚠️ 打 iOS 包要带上自备的 ExportOptions(manageAppVersionAndBuildNumber=false),否则构建号会被 Xcode 自动改掉:flutter build ipa --release --export-options-plist=<path>/ExportOptions.plist⚠️ 只要动了
ios/Runner/Info.plist里的*UsageDescription,就必须清一次 DerivedData 与 SwiftPM 清单缓存,否则permission_handler会按缓存里的旧宏编译,留下无用途说明的系统 API 引用 → 又一次 ITMS-90683:rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-* rm -rf ~/Library/Caches/org.swift.swiftpm/manifests ~/Library/org.swift.swiftpm/cache - 游客登录版本开关已上线到阿龙测试环境(app/console/admin 三个服务),但后台渠道配置还没建,所以入口目前一定是隐藏的。要验证需在 console 后台「全局默认」作用域下配 App Store 与 荣耀 两条,生效版本填
1.0.1。详见 CLAUDE.md 的「游客登录入口的显隐」一节。 - 两台测试机装的是
1.0.1 (4)(含 ATT 的旧版),需要的话要重装(5)。