You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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 是空的)。

两件事要一起做:

  1. 改回退逻辑:安卓端兜底只留 google(iOS 只留 apple),不要「拿不到配置就全展示」
  2. 后台配置: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,可复用

建议的动手顺序

  1. 去 Play Console 申请 targetSdk 与 Billing 的延期(两条都是 8/31 到期)
  2. 16KB 对齐(改 1 个 gradle + 1 个 CMakeLists,最确定,改完用 check_16kb.py 验)
  3. 支付回退逻辑 + 后台 paychannels 配置
  4. 权限瘦身(先删 MEDIA_CONTENT_CONTROL / GET_TASKS,再处理 QUERY_ALL_PACKAGES)
  5. build_channels.sh 的 OUTPUT_DIR 改本机路径
  6. targetSdk 36 + 真机回归
  7. 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)。