Files
suixinkan_uikit/APPSTORE_CACHE_KEY_MIGRATION.md
汉秋 c083f1d4b3 升级 AppStore 缓存 Key 并在覆盖安装时强制重新登录。
统一账号作用域 Key 对齐 Android,新增缓存 schema 迁移清理旧数据,并同步更新相关模块与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 09:35:55 +08:00

170 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` 或用户文件。