Files
suixinkan_uikit/应用启动流程与接口数据存储梳理.md

683 lines
17 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.

# 应用启动流程与接口数据存储梳理
本文按当前 iOS 工程代码梳理应用从冷启动到各首屏展示期间发生的初始化、接口调用顺序,以及接口数据和本地状态的存储方式。范围仅覆盖当前 iOS 实现,不额外补充 Android 中存在但 iOS 尚未接入的能力。
## 1. 冷启动立即发生
冷启动入口在 `AppDelegate.application(_:didFinishLaunchingWithOptions:)`
1. 调用 `UmengBootstrap.configureIfNeeded()`
- 友盟初始化受 `AppStore.shared.privacyAgreementAccepted` 控制。
- 未同意隐私协议时直接返回,不初始化 SDK。
2. 调用 `WeChatManager.shared.registerIfNeeded()`
- 校验 Universal Link。
- 调用微信 SDK `WXApi.registerApp` 注册 AppID。
3.`AppStore.shared.privacyAgreementAccepted == true``AppStore.shared.token` 非空,则调用 `AMapBootstrap.configureIfNeeded()`
- 写入高德隐私合规状态。
- 设置 `AMapServices.shared().apiKey` 和 HTTPS。
4. 此阶段不直接调用任何业务后端接口。
随后系统进入 `SceneDelegate.scene(_:willConnectTo:options:)`
1. 调用 `AppNavigationBarAppearance.applyGlobalAppearance()` 设置全局导航栏样式。
2. 创建 `UIWindow`
3. 设置 `window.rootViewController = AppRouter.makeRootViewController()`
4. `AppRouter` 通过 `AppStore.shared.isLoggedIn` 判断根页面:
- token 非空:进入 `MainTabBarController`
- token 为空:进入 `UINavigationController(rootViewController: LoginViewController())`
5. 注册全局通知:
- `sessionDidExpire`:清除登录态并切回登录页。
- `userDidLogout`:清除登录态并切回登录页。
- `userDidLogin`:切到主 Tab。
## 2. 网络依赖初始化
`NetworkServices.shared` 是全局网络依赖容器,不在 `AppDelegate` 中主动创建,而是在页面或 Tab 首次访问业务 API 时懒加载。
初始化顺序如下:
1. 创建 `APIClient(environment: .current)`
2. 用同一个 `APIClient` 创建各业务 API
- `AuthAPI`
- `ProfileAPI`
- `StatisticsAPI`
- `OrderAPI`
- `HomeAPI`
- 以及其他业务模块 API。
3. 创建 `OSSUploadService(configService: uploadAPI)`
4. 调用 `client.bindAuthTokenProvider`,从 `AppStore.shared.token` 读取 token。
`APIClient` 的公共请求规则:
- `APIEnvironment.current`
- Debug`https://api-test.zhifly.cn`
- Release`https://api.zhifly.cn`
- 每个请求默认写入 Header
- `Content-Type: application/json`
- `Accept: application/json`
- `X-APP-VERSION`
- `X-OS-TYPE: iOS`
- token 非空时写入 `token`
- 响应通过 `APIEnvelope` 解包。
- 成功业务码:`100000`
- 非成功业务码抛出 `APIError.serverCode`
- 认证失效时广播 `sessionDidExpire`,由 `SceneDelegate` 清会话并回登录页。
## 3. 未登录启动与登录流程
未登录时根页面为登录页。
### 3.1 登录页展示前
`LoginViewController.setupUI` 会调用 `viewModel.applyStoredPreferences()`,从 `AppStore` 读取:
- `lastLoginUsername`
- `privacyAgreementAccepted`
这一步只恢复本地表单状态,不调用后端接口。
### 3.2 登录页出现后
`LoginViewController.viewDidAppear` 调用:
```text
GET /api/app/config
```
调用链:
```text
LoginViewController.viewDidAppear
-> LoginViewModel.loadAppConfig(authAPI:)
-> AuthAPI.getAppConfig()
```
用途:
- 读取 App 配置。
- 当前只使用 `enableRegister` 控制验证码登录/注册入口显隐。
- 配置加载失败会静默忽略,不影响密码登录。
- 该结果只保存在 `LoginViewModel.enableRegister` 内存态,不写入 `AppStore`
### 3.3 点击登录后的接口顺序
用户点击登录按钮后,先做本地校验:
- 手机号必须是 11 位且以 `1` 开头。
- 密码不能为空。
- 必须勾选隐私协议。
校验通过后发起登录:
```text
POST /api/app/v9/login
```
调用链:
```text
LoginViewController.performLogin()
-> LoginViewModel.login(authAPI:)
-> AuthAPI.login(username:password:)
```
返回数据包含临时 token 和可选账号列表。随后根据账号数量分流:
- 只有一个账号:立即调用 `set-user` 换正式 token。
- 多个账号:展示账号选择弹窗,用户确认后再调用 `set-user`
正式账号选择接口:
```text
POST /api/app/v9/set-user
```
调用链:
```text
单账号:
LoginViewModel.resolveLoginResponse(...)
-> AuthAPI.setUser(..., tokenOverride: 临时 token)
多账号:
LoginViewController.selectAccount(_:)
-> LoginViewModel.selectAccount(_:authAPI:)
-> AuthAPI.setUser(..., tokenOverride: 临时 token)
```
### 3.4 登录完成后的存储与跳转
登录完成调用:
```text
AuthSessionHelper.completeLogin(...)
```
写入 `AppStore` 的内容:
- `token`
- `lastLoginUsername`
- `privacyAgreementAccepted`
- `userId`
- `userName`
- `realName`
- `avatar`
- `phone`
- `accountType`
- `accountDisplayName`
- `roleCode`
- `roleName`
- `currentScenicId`
- `currentScenicName`
- `currentStoreId`
其中账号上下文来自 `AccountSwitchAccount``V9ScenicUser``V9StoreUser`
如果用户已同意隐私协议,还会再次触发:
- `AMapBootstrap.configureIfNeeded()`
- `UmengBootstrap.configureIfNeeded()`
最后发送:
```text
NotificationName.userDidLogin
```
`SceneDelegate.handleUserDidLogin()` 收到通知后调用:
```text
AppRouter.setRoot(.mainTab, on: window)
```
根页面切换到主 Tab。
## 4. 已登录启动与默认首页首次展示
已登录时,`AppRouter.makeRootViewController()` 创建 `MainTabBarController`
### 4.1 主 Tab 创建
`MainTabBarController.viewDidLoad` 顺序:
1. 设置 `delegate`
2. 配置 TabBar 外观。
3. 调用 `configureTabs()` 创建五个 Tab slot
- 首页:`HomeViewController`
- 订单:`OrdersViewController`
- 中间扫码占位页
- 数据:`StatisticsViewController`
- 我的:`ProfileViewController`
4. 绑定订单角标 ViewModel。
5. 绑定扫码处理器。
6. 调用 `badgeViewModel.refreshPendingWriteOffCount()`
注意:`MainTabBadgeViewModel.refreshPendingWriteOffCount()` 当前仅执行:
```text
pendingWriteOffCount = nil
```
代码注释标明“接口未接入前保持为空”,因此这里不记录为真实后端接口调用。
### 4.2 默认首页首次出现
默认选中首页。`HomeViewController.viewDidAppear` 首次出现时调用:
```text
initializeHome()
```
调用顺序:
1. 展示全局 loading。
2. `HomeViewModel.initialize(api:)`
3. 隐藏 loading。
4. 刷新首页 UI。
5. 延迟评估弹窗。
`HomeViewModel.initialize(api:)` 内部顺序:
1. `locationStateStore.restoreStateIfNeeded()`
-`AppStore` 恢复在线状态、上次位置上报时间、提醒分钟数。
- 如果上次在线状态已过期,会清除在线状态和上次上报时间。
- 不调用后端接口。
2. `refreshLocalDisplayState()`
-`AppStore` 读取当前景区、当前角色。
- 无有效景区时清空 `currentScenicId/currentScenicName`
3. 强制调用 `loadPermissions(api:force: true)`
权限接口:
```text
GET /api/yf-handset-app/role-permission
```
调用链:
```text
HomeViewModel.initialize(api:)
-> HomeViewModel.loadPermissions(api:force: true)
-> HomeAPI.rolePermissions()
```
权限返回后的存储:
- 完整角色权限列表:`AppStore.saveRolePermissionList(_:)`
- 当前匹配角色的景区列表:`AppStore.saveRoleScenicList(_:)`
- 当前匹配角色的扁平权限:`AppStore.savePermissionItems(_:)`
- 匹配角色名:必要时写入 `AppStore.roleName`
这些权限数据都按账号作用域 key 写入 `UserDefaults`,值为 JSON Data。
随后首页重建常用应用:
-`AppStore.permissionItems()` 读取权限。
- `HomeMenuCatalog.visibleMenus(from:)` 生成可见菜单。
- `HomeCommonMenuStore` 读取或写入常用应用 URI。
- 常用应用 key 由 `accountScope + roleCode` 组成。
### 4.3 首页门店接口
权限加载完成后会调用:
```text
HomeViewModel.loadStoreListIfNeeded(api:)
```
只有同时满足以下条件才会请求门店:
- 当前角色为 `storeAdmin`
- `AppStore.currentScenicId > 0`
接口:
```text
GET /api/app/store/all
```
调用链:
```text
HomeViewModel.loadStoreListIfNeeded(api:)
-> HomeAPI.storeList()
```
返回后的处理:
- 根据 `currentScenicId` 和已保存的 `currentStoreId` 匹配当前门店。
- 匹配结果保存在 `HomeViewModel.storeItem`
- 若匹配到门店,写入 `AppStore.currentStoreId`
- 门店列表本身不持久化。
### 4.4 首页弹窗评估与位置详情接口
首页 UI 刷新后调用:
```text
evaluateDialogsWithDelay()
-> HomeViewModel.evaluateDialogs(api:)
```
弹窗优先级:
1. 权限弹窗。
2. 景区弹窗。
3. 位置超时弹窗。
如果不需要权限弹窗、不需要景区弹窗、不是简化顶部角色,并且本地设置了位置提醒分钟数,则可能调用位置详情接口:
```text
GET /api/yf-handset-app/photog/loacation/detail?staff_id=...
```
调用链:
```text
HomeViewModel.evaluateDialogs(api:)
-> HomeViewModel.checkLocationTimeout(api:)
-> HomeAPI.locationDetail(staffId:)
```
返回后的处理:
- 只用于判断是否展示位置超时提醒。
- 结果不写入 `AppStore`
- 弹窗状态保存在 `HomeViewModel.showLocationTimeoutDialog` 等内存属性中。
## 5. 其他 Tab 首次展示时的接口
其他 Tab 的根控制器会在 `MainTabBarController.configureTabs()` 时创建,但各自的数据接口通常在首次展示时才调用。
### 5.1 订单 Tab
`OrdersViewController.viewDidAppear` 首次出现时调用:
```text
initializeList()
```
按当前角色选择接口。
摄影师:
```text
GET /api/yf-handset-app/photog/order/listv2
```
景区管理员:
```text
GET /api/app/scenic-admin/order/list
```
门店管理员:
```text
GET /api/app/store/order/list
```
调用链:
```text
OrdersViewController.initializeList()
-> OrderListViewModel.refreshOrderList(api:)
-> OrderAPI.photographerOrderList(...) 或 OrderAPI.scenicAdminOrderList(...)
OrdersViewController.initializeList()
-> DepositOrderListViewModel.refreshOrderList(api:)
-> OrderAPI.storeOrderList(...)
```
数据存储:
- 摄影师/景区管理员订单列表保存在 `OrderListViewModel.orderList`
- 门店管理员订单列表保存在 `DepositOrderListViewModel.orderList`
- 分页状态、筛选状态、搜索状态都保存在对应 ViewModel 内存属性中。
- 首屏订单列表不写入 `AppStore`
### 5.2 数据 Tab
`StatisticsViewController.viewDidAppear` 首次出现时调用:
```text
initializeStatistics()
-> StatisticsViewModel.initStatistics(api:)
```
内部顺序:
1. 加载汇总数据。
2. 设置日明细日期范围为今日并刷新列表。
摄影师汇总:
```text
GET /api/yf-handset-app/photog/analyse/user
```
摄影师日明细:
```text
GET /api/yf-handset-app/photog/analyse/user/daily
```
景区管理员汇总:
```text
GET /api/app/scenic-admin/analyse
```
景区管理员日明细:
```text
GET /api/app/scenic-admin/analyse/daily
```
门店管理员汇总:
```text
GET /api/app/store/analyse
```
门店管理员日明细:
```text
GET /api/app/store/analyse/daily
```
数据存储:
- 汇总数据保存在 `StatisticsViewModel.statistics`
- 日明细列表保存在 `StatisticsViewModel.statisticsList`
- 当前周期、日期范围、分页状态保存在 `StatisticsViewModel` 内存属性中。
- 统计数据不写入 `AppStore`
### 5.3 我的 Tab
`ProfileViewController.viewWillAppear` 每次出现都会调用:
```text
reloadProfile(showGlobalLoading:)
-> ProfileViewModel.reload(api:)
```
第一步固定调用用户资料接口:
```text
GET /api/yf-handset-app/userinfo
```
调用链:
```text
ProfileViewModel.reload(api:)
-> ProfileAPI.userInfo()
```
返回后的处理:
- 保存在 `ProfileViewModel.userInfo`
- 调用 `AppStore.applyUserInfo(_:)` 回写展示字段:
- `userName`
- `realName`
- `avatar`
- `phone`
- `roleName`,仅在本地为空时由 `applyUserInfo` 写入;随后 `ProfileViewModel.reload` 也会在接口 `roleName` 非空时写入。
如果当前为摄影师角色,则并发调用:
```text
GET /api/yf-handset-app/photog/real-name/info
GET /api/yf-handset-app/photog/wallet/bank-card/info
```
调用链:
```text
async let realName = api.realNameInfo()
async let bankCard = api.bankCardInfo()
```
返回后的处理:
- 实名信息保存在 `ProfileViewModel.realNameInfo`
- 银行卡信息保存在 `ProfileViewModel.bankCardInfo`
- 这两个结果不写入 `AppStore`
## 6. 数据存储总览
### 6.1 AppStore / UserDefaults
`AppStore` 是本地会话和业务快照的统一入口,底层使用 `UserDefaults`
非账号作用域的主要 key
- `key_in_token`
- `key_last_login_username`
- `key_privacy_agreement_accepted`
- `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_code`
- `key_in_role_name`
- `key_in_current_scenic_id`
- `key_in_current_scenic_name`
- `key_in_current_store_id`
账号作用域由 `accountCachePrefix` 生成:
```text
userId_accountType
```
如果没有 userId则使用 `guest`。账号作用域 key 用于存储:
- 旧版角色 ID`key_in_role_id`
- 完整 role-permission 列表:`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`
- 排队叫号相关本地设置。
权限和景区列表以 JSON Data 写入 `UserDefaults`
- `saveRolePermissionList(_:)`
- `savePermissionItems(_:)`
- `saveRoleScenicList(_:)`
### 6.2 HomeCommonMenuStore
首页常用应用由 `HomeCommonMenuStore` 单独管理,底层也是 `UserDefaults`
存储 key
```text
{accountScope}_role_{roleCode}_common_uris
```
值为 URI 字符串数组。
首次有权限但没有常用应用记录时,会从权限列表生成最多 4 个默认常用入口并写入本地。
### 6.3 HomeLocationStateStore
`HomeLocationStateStore` 负责首页在线状态和 2 小时位置上报倒计时。
持久化到 `AppStore` 的字段:
- `onlineStatus`
- `lastLocationReportTime`
- `locationReminderMinutes`
内存态字段:
- `isOnline`
- `elapsedSeconds`
- `nextReportCountdownSeconds`
- `reminderMinutes`
- 倒计时 `Task`
启动恢复逻辑:
- 如果本地 `onlineStatus == false`,停止倒计时。
- 如果没有 `lastLocationReportTime`,清除在线状态。
- 如果距离上次上报已超过 2 小时,清除在线状态和上次上报时间。
- 否则恢复倒计时。
### 6.4 ViewModel 内存态
多数页面接口结果只保存在 ViewModel 内存属性中,不持久化。
典型示例:
- 登录配置:`LoginViewModel.enableRegister`
- 首页门店卡片:`HomeViewModel.storeItem`
- 首页弹窗状态:`HomeViewModel.showPermissionDialog``showScenicDialog``showLocationTimeoutDialog`
- 订单列表:`OrderListViewModel.orderList``DepositOrderListViewModel.orderList`
- 数据统计:`StatisticsViewModel.statistics``statisticsList`
- 我的页面实名/银行卡状态:`ProfileViewModel.realNameInfo``bankCardInfo`
例外是明确调用 `AppStore` 的数据:
- 登录 token 与账号上下文。
- 用户资料展示字段。
- 权限、景区、角色相关快照。
- 门店当前 ID。
- 位置在线状态与倒计时锚点。
## 7. 启动接口顺序速查
### 7.1 未登录冷启动
```text
AppDelegate.didFinishLaunching
-> 友盟/微信/高德 SDK 条件初始化
-> SceneDelegate.willConnect
-> AppRouter.makeRootViewController()
-> LoginViewController.setupUI
-> LoginViewController.viewDidAppear
-> GET /api/app/config
```
点击登录后:
```text
POST /api/app/v9/login
-> POST /api/app/v9/set-user
-> AppStore 写入登录态和账号上下文
-> userDidLogin
-> AppRouter.setRoot(.mainTab)
```
### 7.2 已登录冷启动默认首页
```text
AppDelegate.didFinishLaunching
-> 友盟/微信/高德 SDK 条件初始化
-> SceneDelegate.willConnect
-> AppRouter.makeRootViewController()
-> MainTabBarController.viewDidLoad
-> HomeViewController.viewDidAppear
-> HomeLocationStateStore.restoreStateIfNeeded()
-> GET /api/yf-handset-app/role-permission
-> 条件满足时 GET /api/app/store/all
-> 条件满足时 GET /api/yf-handset-app/photog/loacation/detail?staff_id=...
```
### 7.3 用户首次切到订单 Tab
```text
OrdersViewController.viewDidAppear
-> 摄影师 GET /api/yf-handset-app/photog/order/listv2
或 景区管理员 GET /api/app/scenic-admin/order/list
或 门店管理员 GET /api/app/store/order/list
```
### 7.4 用户首次切到数据 Tab
```text
StatisticsViewController.viewDidAppear
-> 按角色 GET 汇总接口
-> 按角色 GET 日明细接口
```
### 7.5 用户切到我的 Tab
```text
ProfileViewController.viewWillAppear
-> GET /api/yf-handset-app/userinfo
-> 摄影师角色并发:
GET /api/yf-handset-app/photog/real-name/info
GET /api/yf-handset-app/photog/wallet/bank-card/info
```