升级 AppStore 缓存 Key 并在覆盖安装时强制重新登录。

统一账号作用域 Key 对齐 Android,新增缓存 schema 迁移清理旧数据,并同步更新相关模块与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 09:35:55 +08:00
parent 05804ba7d6
commit c083f1d4b3
31 changed files with 852 additions and 123 deletions

View File

@ -0,0 +1,169 @@
# AppStore 缓存 Key 升级对照
## 结论
- 旧线上工程与重构版工程的主应用 Bundle ID 均为 `com.yuanzhixiang.suixinkan`。通过 App Store 覆盖升级时,系统会保留原应用容器,因此旧版 `UserDefaults` 会继续存在。
- 本次升级策略为:**强制重新登录、旧偏好全部重置、不迁移旧缓存**。
- 新 Key 只对齐 Android 的业务作用域iOS 统一使用 `_` 作为分隔符,不复制 Android MMKV 历史实现中无分隔符拼接的写法。
- 本文中的“旧线上工程”为 `/Users/hanqiu/Desktop/android_project/suixinkan_ios`,“重构版工程”为 `/Users/hanqiu/Desktop/suixinkan`
## Key 占位符
| 占位符 | 含义 |
|---|---|
| `O` | 旧版将 `[accountType, userId, storeId, scenicId]` 中的非空值用 `_` 拼接,再清洗非字母数字字符后得到的 accountKey |
| `P` | 本次实施前重构版使用的 `<userId>_<accountType>` 账号前缀;用户 ID 为空时可能退化为 `guest` |
| `A` | 本次实施后统一使用的 `<accountType>_<userId>` 账号前缀 |
| `S` | 景区 IDscenicId |
| `T` | 排队点位 IDspotId |
| `R` | 角色编码roleCode |
> `suixinkan.session.v1` 是旧工程中真实存在的单个 `UserDefaults` Key其值为 JSON Data。表格中的 `suixinkan.session.v1[token]` 等写法仅表示该 JSON 内的字段,不是独立的 `UserDefaults` Key。
## 新旧工程统一业务 Key 对照
| 业务数据 | 旧线上工程 Key | 重构版实施前 Key | 统一后的 Key | 升级处理 |
|---|---|---|---|---|
| 登录会话 | `suixinkan.session.v1` JSON | 拆分为多个 `key_in_*` | 保持拆分结构 | 删除旧会话,强制登录 |
| Token | `suixinkan.session.v1[token]` | `key_in_token` | `key_in_token` | 首次升级清空 |
| 用户 ID | `suixinkan.session.v1[userId]` | `key_in_user_id` | `key_in_user_id` | 首次升级清空 |
| 用户名 | `suixinkan.session.v1[userName]` | `key_in_user_name` | `key_in_user_name` | 首次升级清空 |
| 头像 | `suixinkan.session.v1[avatar]` | `key_in_avatar` | `key_in_avatar` | 首次升级清空 |
| 手机号 | `suixinkan.session.v1[phone]` | `key_in_phone` | `key_in_phone` | 首次升级清空 |
| 账号类型 | `suixinkan.session.v1[accountType]` | `key_in_account_type` | `key_in_account_type` | 首次升级清空 |
| 账号类型显示名称 | `suixinkan.session.v1[accountDisplayName]` | `key_in_account_display_name` | `key_in_account_display_name` | 首次升级清空 |
| 角色 ID | `suixinkan.session.v1[roleId]` | `P_key_in_role_id` | `A_key_in_role_id` | 不迁移;当前仅用于 legacy 权限匹配兜底 |
| 角色编码 | 无独立 Key | `key_in_role_code` | `A_key_in_role_code` | 改为账号作用域 |
| 角色名称 | `suixinkan.session.v1[roleName]` | `key_in_role_name` | `A_key_in_role_name` | 改为账号作用域 |
| 权限列表 | 无 | `P_key_role_permission_list` | `A_key_role_permission_list` | 不迁移预发布数据 |
| 当前权限 | 无 | `P_key_in_permission` | `A_key_in_permission` | 不迁移预发布数据 |
| 角色景区列表 | 无 | `P_key_current_role_scenic_list` | `A_key_current_role_scenic_list` | 不迁移预发布数据 |
| 当前景区 ID | `suixinkan.session.v1[scenicId]` | `key_in_current_scenic_id` | `A_key_in_current_scenic_id` | 改为账号作用域 |
| 当前景区名称 | `suixinkan.session.v1[scenicName]` | `key_in_current_scenic_name` | `A_key_in_current_scenic_name` | 改为账号作用域 |
| 当前门店 ID | `suixinkan.session.v1[storeId]` | `key_in_current_store_id` | `A_scenic_S_scenic_store_id` | 改为账号+景区作用域 |
| 当前门店名称 | `suixinkan.session.v1[storeName]` | 无 | 不再持久化 | 删除旧值 |
| 常用菜单 | `home.common.menu.uris` | `P_role_R_common_uris` | `A_role_R_common_uris` | 不迁移旧菜单仅统一作用域Android 精确后缀不同 |
| 收款语音 | `payment.voice.broadcast.enabled` | `P_key_is_open_receive_voice` | `A_key_is_open_receive_voice` | 重置为默认值 |
| 在线状态 | 无 | `P_key_online_status` | `A_key_online_status` | 改为账号作用域 |
| 上次位置上报时间 | 无 | `P_key_last_location_report_time` | `A_key_last_location_report_time` | 改为账号作用域 |
| 定位提醒分钟数 | 无 | `P_key_location_reminder_minutes` | `A_key_location_reminder_minutes` | 改为账号作用域 |
| 当前排队点位 ID | `account_O_scenic_S_scenic_queue_selected_spot_id` | `P__scenic_queue_punch_spot_id` | `A_scenic_S_scenic_queue_punch_spot_id` | 不迁移 |
| 当前排队点位名称 | `account_O_scenic_S_scenic_queue_selected_spot_name` | `P__scenic_queue_punch_spot_name` | `A_scenic_S_spot_T_scenic_queue_punch_spot_name` | 不迁移 |
| 排队播报备注 | `account_O_scenic_S_scenic_queue_custom_tts_text_spot_T` | `P__scenic_queue_remark` | `A_scenic_S_spot_T_scenic_queue_remark` | 不迁移 |
| 排队设置快照 | `account_O_scenic_S_scenic_queue_settings_snapshot_spot_T` | `P__scenic_queue_settings_snapshot` | `A_scenic_S_spot_T_scenic_queue_settings_snapshot` | 不迁移 |
| 排队预设语音 | `account_O_scenic_S_scenic_queue_preset_voices_spot_T` | `P__scenic_queue_preset_voices_v1` | `A_scenic_S_spot_T_scenic_queue_preset_voices_v1` | 不迁移 |
| 离线 TTS | 无 | `P__scenic_queue_offline_tts_v1` | `A_scenic_S_scenic_queue_offline_tts_v1` | 默认值对齐 Android 为 `true` |
## 首次升级需要清理的旧 Key
已新增整型版本标记 `suixinkan.cache.schema.version`。当版本小于目标缓存版本时执行一次清理,全部清理完成后再写入目标版本,避免每次启动重复清空用户重新登录后产生的数据。
### 会话与推送
- `suixinkan.session.v1`
- `apns_device_token`
- `apns_uploaded_token`
同时清空重构版预发布阶段可能存在的登录与账号字段,以确保升级后强制重新登录:
- `key_in_token`
- `key_last_login_username`
- `key_in_user_id`
- `key_in_user_name`
- `key_in_real_name`
- `key_in_avatar`
- `key_in_phone`
- `key_in_account_type`
- `key_in_account_display_name`
- `key_in_role_id`
- `key_in_role_code`
- `key_in_role_name`
- `key_in_current_scenic_id`
- `key_in_current_scenic_name`
- `key_in_current_store_id`
`key_privacy_agreement_accepted` 是否清理应服从“旧偏好全部重置”的既定策略,因此本次也应清理。
同时清理早期重构实现可能写入的以下未分域业务字段:
- `key_role_permission_list`
- `key_in_permission`
- `key_current_role_scenic_list`
- `key_online_status`
- `key_last_location_report_time`
- `key_location_reminder_minutes`
- `key_is_open_receive_voice`
### 首页与收款
- `home.common.menu.uris`
- `home.common.menu.android.baseline.v2`
- `home.routing.unknown.records`
- `payment.voice.broadcast.enabled`
### 排队全局偏好
- `scenic_queue_tts_enabled`
- `scenic_queue_background_poll_enabled`
- `scenic_queue_selected_spot_id`
- `scenic_queue_selected_spot_name`
- `scenic_queue_custom_tts_text`
- `scenic_queue_photo_estimate_seconds`
- `scenic_queue_broadcast_interval_seconds`
- `scenic_queue_countdown_threshold_seconds`
- `scenic_queue_show_start_shooting_button`
- `scenic_queue_auto_call_ahead_count`
- `scenic_queue_quick_call_button_enabled`
- `scenic_queue_prepare_call_button_enabled`
- `scenic_queue_config_logs`
- `scenic_queue_settings_snapshot`
- `scenic_queue_preset_voices`
### 排队动态缓存
清理符合以下已知格式的旧 Key不应扩大为删除整个 `UserDefaults` domain
- `account_O_scenic_S_scenic_queue_selected_spot_id`
- `account_O_scenic_S_scenic_queue_selected_spot_name`
- `account_O_scenic_S_scenic_queue_custom_tts_text_spot_T`
- `account_O_scenic_S_scenic_queue_settings_snapshot_spot_T`
- `account_O_scenic_S_scenic_queue_preset_voices_spot_T`
- `scenic_S_spot_T_scenic_queue_settings_snapshot`
- `scenic_S_scenic_queue_settings_snapshot`
- `scenic_S_spot_T_scenic_queue_preset_voices`
### 重构版预发布缓存
清理使用旧 `P` 前缀的以下 Key 家族:
- `P_key_in_role_id`
- `P_key_role_permission_list`
- `P_key_in_permission`
- `P_key_current_role_scenic_list`
- `P_key_online_status`
- `P_key_last_location_report_time`
- `P_key_location_reminder_minutes`
- `P_key_is_open_receive_voice`
- `P_role_R_common_uris`
- `P__scenic_queue_punch_spot_id`
- `P__scenic_queue_punch_spot_name`
- `P__scenic_queue_remark`
- `P__scenic_queue_settings_snapshot`
- `P__scenic_queue_offline_tts_v1`
- `P__scenic_queue_preset_voices_v1`
其中 `P__scenic_queue_*` 的双下划线来自当前 Key 生成器和自带前导下划线的后缀叠加,属于需要淘汰的错误格式。
## 不应清理的内容
- 禁止调用 `removePersistentDomain(forName:)` 清空应用的整个 `UserDefaults` domain应只删除明确列出的 Key 和严格匹配的动态 Key。
- 不删除 `Documents/CloudDownloads`。该目录可能包含用户主动下载的文件,不属于普通缓存。
- 不删除 SDK 缓存、图片缓存或其他未列入本次升级范围的用户文件。
- 不做旧 `suixinkan.session.v1`、排队设置、常用菜单或收款偏好的解码与迁移。
## 发布前备注
- 旧线上版本为 `1.0.1 (2)`;当前覆盖升级版本已设置为 `1.1.4 (1010401)`
- 重构版当前未保留旧工程的 APNs entitlement、远程通知后台能力、回调和设备 Token 上传流程。该问题不是缓存 Key 修改,但若线上仍需要推送,发布前必须单独恢复并验证。清理 `apns_device_token``apns_uploaded_token` 后,恢复推送时应重新绑定设备 Token。
- `Application Support/suixinkan/cloud_transfer_tasks.json` 当前为全账号共享文件,存在账号切换后读取其他账号任务的风险。该问题不是 `UserDefaults` Key 迁移的一部分,应作为独立发布风险处理。
- 本方案已在重构版中实施:应用首次创建 `AppStore` 时按 `suixinkan.cache.schema.version` 执行一次定向清理,并切换到表格中的统一 Key不删除未列入范围的 `UserDefaults` 或用户文件。