流程图集 · Momenta iOS · 需求 R2

相机「激活」流程(现状)· Mermaid 图集
含「何时会显示激活界面」的完整判定树 · 代码未做任何改动

对象 momenta/ios @ develop (13578ebf) 行号 均为该基线下的实际行号 日期 2026-10-14 提示 图下方折叠块里是 Mermaid 源码,可直接复制
图目录
  1. 图 1 · 总览:四个入口与三处门禁
  2. 图 2 · activateDevice() 主流程(现状全分支)
  3. 图 3 · 「何时显示激活界面」判定树(两张:连接成功自动检查 / 搜索页自身)
  4. 图 4 · 三份激活状态与静默同步激活的重试循环
  5. 图 5 · 后续「去激活」改造地图(4 个短路点,仅供参考,未实施)

图 1 · 总览:四个入口与三处门禁

激活不是一个页面,而是四条入口共同作用于一个门禁变量 PTCameraModel.isActivated;门禁结果为真的唯一放行动作是 readyForCapture(进拍摄/预览)。

flowchart TD CONN["BLE 配对 / Wi-Fi(AP) 配网 / WebSocket 就绪"] --> E{"激活入口"} E -->|"入口 A
搜索页按钮/重试"| A1["PTCameraSearchView:733 / 784 / 816"] E -->|"入口 B
连接成功自动检查"| B1["PTCameraConnectionFlowManager
checkFirmwareUpgrade:200+"] E -->|"入口 C
连 WiFi 前预查"| C1["PTCameraSearchStrategySD2402:139"] E -->|"入口 D
补偿重试"| D1["PTSilentActivationSyncCoordinator
每 3s 重试 activateWIFI"] A1 --> VM["PTCameraSearchViewModel:176"] VM --> ACT["PTDeviceActivationCapable.activateDevice()"] B1 --> CHK["checkActivationStatus → performActivationStatusCheck:288"] C1 --> Q CHK --> Q["PTDeviceNetWorkService.queryServiceActivateStatus():729"] D1 --> DW["PTSystemService.activateWIFI(accountId)"] ACT --> Q ACT --> SERVER["POST /activate/activateDevice
PTDeviceNetWorkService:427"] ACT --> DEV["设备侧:sendUserId(accountId)
+ activateWIFI(accountId)"] Q --> GATE{"门禁判定
isActivate && isActivated && bleActivated"} GATE -->|"通过"| OK["readyForCapture:进拍摄/预览"] GATE -->|"未通过"| NG["activationRequired:弹激活页 / 强制登录层"] SERVER -.->|"写入缓存 cacheServiceActivatedStatus(true)"| GATE DEV -.->|"setActivatedManually(true)"| GATE
Mermaid 源码(图 1)
flowchart TD CONN["BLE 配对 / Wi-Fi(AP) 配网 / WebSocket 就绪"] --> E{"激活入口"} E -->|"入口 A 搜索页按钮/重试"| A1["PTCameraSearchView:733 / 784 / 816"] E -->|"入口 B 连接成功自动检查"| B1["PTCameraConnectionFlowManager checkFirmwareUpgrade"] E -->|"入口 C 连 WiFi 前预查"| C1["PTCameraSearchStrategySD2402:139"] E -->|"入口 D 补偿重试"| D1["PTSilentActivationSyncCoordinator 每 3s 重试 activateWIFI"] A1 --> VM["PTCameraSearchViewModel:176"] VM --> ACT["PTDeviceActivationCapable.activateDevice()"] B1 --> CHK["checkActivationStatus → performActivationStatusCheck:288"] C1 --> Q CHK --> Q["PTDeviceNetWorkService.queryServiceActivateStatus():729"] D1 --> DW["PTSystemService.activateWIFI(accountId)"] ACT --> Q ACT --> SERVER["POST /activate/activateDevice PTDeviceNetWorkService:427"] ACT --> DEV["设备侧:sendUserId(accountId) + activateWIFI(accountId)"] Q --> GATE{"门禁判定 isActivate 与 isActivated 与 bleActivated"} GATE -->|"通过"| OK["readyForCapture:进拍摄/预览"] GATE -->|"未通过"| NG["activationRequired:弹激活页 / 强制登录层"] SERVER -.->|"写缓存 cacheServiceActivatedStatus(true)"| GATE DEV -.->|"setActivatedManually(true)"| GATE

图 2 · activateDevice() 主流程(现状全分支)

四个前置检查按顺序短路;通过后先查服务端、必要时再提交激活,最后做设备侧两条指令。注意第一个前置判的是「移动数据」可达而不是普通联网。

flowchart TD S["activateDevice()
+Activation.swift:12"] --> T0["trackActivationStartEvent()
updateConnectMode(.activateState(.activating)):14-15"] T0 --> P1{"① isMobileDataAvailable?"} P1 -->|"否"| F1["failed(.firmwareVersionInvalidNetworkUnavailable)
:18-21"] P1 -->|"是"| P2{"② isLoggedIn?"} P2 -->|"否"| F2["failed(.notLoggedIn) :23-27
→ presentLoginView()"] P2 -->|"是"| P3{"③ SN 非空?"} P3 -->|"否"| F3["failed(.snEmpty) :31-36"] P3 -->|"是"| P4{"④ 固件版本合法?
isActivationFirmwareVersionInvalid() :291"} P4 -->|"空/四路不一致/命中强制升级清单"| F4["查 updateInfo → 需升级则弹升级引导
否则 failed(.firmwareVersionInvalid*)"] P4 -->|"合法"| Q1["queryServiceActivateStatus()
GET /device/isActivated/{sn}"] Q1 --> Q2{"查询成功?"} Q2 -->|"否 网络不可用"| F5["failed(.networkUnavailable) :68-75"] Q2 -->|"否 业务错误码"| F6["failed(.activationFailed) :77-86"] Q2 -->|"是"| A0{"服务端 isActivated?"} A0 -->|"false"| A1["activateDevice(sn, mac: nil)
POST /activate/activateDevice :98"] A0 -->|"true"| OKC["cacheServiceActivatedStatus(true)
updateConnectMode(.success) :130-134"] A1 --> A2{"提交成功?"} A2 -->|"错误码 500 且含 已激活"| F7["failed(.deviceAlreadyActivated) :108-113"] A2 -->|"其他失败"| F8["failed(.activationFailed) :114-122"] A2 -->|"成功"| OKC OKC --> D1["sendUserId(accountId) :137
→ isbindUser / isCameraLocked"] D1 --> D2{"设备端 isActivated?"} D2 -->|"false"| D3["activateWIFI(accountId) :150"] D2 -->|"true"| DONE["markActivatedAndStop() :184"] D3 --> D4{"设备返回?"} D4 -->|"already activated"| DONE D4 -->|"失败"| D5["startSilentActivationSyncIfNeeded() :170
后台每 3s 重试"] D4 -->|"成功"| DONE
Mermaid 源码(图 2)
flowchart TD S["activateDevice() +Activation.swift:12"] --> T0["trackActivationStartEvent; updateConnectMode activating :14-15"] T0 --> P1{"① isMobileDataAvailable?"} P1 -->|"否"| F1["failed firmwareVersionInvalidNetworkUnavailable :18-21"] P1 -->|"是"| P2{"② isLoggedIn?"} P2 -->|"否"| F2["failed notLoggedIn :23-27 然后 presentLoginView"] P2 -->|"是"| P3{"③ SN 非空?"} P3 -->|"否"| F3["failed snEmpty :31-36"] P3 -->|"是"| P4{"④ 固件版本合法? isActivationFirmwareVersionInvalid :291"} P4 -->|"空或四路不一致或命中强制升级清单"| F4["查 updateInfo 需升级则弹升级引导 否则 failed firmwareVersionInvalid"] P4 -->|"合法"| Q1["queryServiceActivateStatus GET /device/isActivated/sn"] Q1 --> Q2{"查询成功?"} Q2 -->|"否 网络不可用"| F5["failed networkUnavailable :68-75"] Q2 -->|"否 业务错误码"| F6["failed activationFailed :77-86"] Q2 -->|"是"| A0{"服务端 isActivated?"} A0 -->|"false"| A1["activateDevice(sn, mac nil) POST /activate/activateDevice :98"] A0 -->|"true"| OKC["cacheServiceActivatedStatus(true) 与 success :130-134"] A1 --> A2{"提交成功?"} A2 -->|"错误码 500 且含已激活"| F7["failed deviceAlreadyActivated :108-113"] A2 -->|"其他失败"| F8["failed activationFailed :114-122"] A2 -->|"成功"| OKC OKC --> D1["sendUserId(accountId) :137 得 isbindUser isCameraLocked"] D1 --> D2{"设备端 isActivated?"} D2 -->|"false"| D3["activateWIFI(accountId) :150"] D2 -->|"true"| DONE["markActivatedAndStop() :184"] D3 --> D4{"设备返回?"} D4 -->|"already activated"| DONE D4 -->|"失败"| D5["startSilentActivationSyncIfNeeded() :170 后台每 3s 重试"] D4 -->|"成功"| DONE

图 3 · 「何时显示激活界面」判定树

这里有两条独立的分流:连接成功后由 PTCameraConnectionFlowManager 决定「弹不弹激活页」;而激活页真正弹出后,由 PTCameraSearchView 内部再决定显示哪一种界面。两条链路都会受「服务端激活态 / 设备端激活态 / 蓝牙配置激活态」三者组合影响。

3.1 连接成功后:弹不弹激活页(FlowManager 侧)

flowchart TD W["Wi-Fi 连接成功事件
runConnectionSuccessActivationFlow"] --> SKIP{"前置跳过条件?"} SKIP -->|"拍摄页已在前台"| X1["不打扰"] SKIP -->|"前台已有 搜索页/未激活升级页/强升弹窗"| X2["交给它们自己的流程"] SKIP -->|"否"| CF["checkFirmwareUpgrade"] CF --> CF2{"needUpgrade?"} CF2 -->|"升级检查失败"| CA["继续 checkActivationStatus"] CF2 -->|"true"| U1{"!isActivated
且 isRecoveryBlockedFirmwareVersion()?"} U1 -->|"是"| UV["弹 PTUnActivateView
未激活先升级 :241"] U1 -->|"否"| U2{"forceToUpgrade 且 非升级中 且 未在拍录?"} U2 -->|"是"| FV["弹 PTFirmwareUpgradeNewVersionView"] U2 -->|"否"| CA CF2 -->|"false"| CA CA --> QR["PTDeviceNetWorkService.queryServiceActivateStatus()"] QR --> ER{"返回 errorMessage?"} ER -->|"是"| T1["toast 错误信息 + completion(.flowHandled)
既不进拍摄也不弹激活页"] ER -->|"否"| ACT{"服务端 isActivate?"} ACT -->|"false"| REQ["completion(.activationRequired)
→ 弹 PTCameraSearchView(.connectCompleted(nil))"] ACT -->|"true"| MIS{"设备端/BLE 配置 存在错配?"} MIS -->|"错配 且 未登录"| REQ2["completion(.activationRequired)
→ 激活页里出现强制登录层"] MIS -->|"错配 且 已登录"| SYNC["启动静默同步激活
→ completion(.readyForCapture)"] MIS -->|"没错配"| OKP["completion(.readyForCapture)"]
Mermaid 源码(图 3.1)
flowchart TD W["Wi-Fi 连接成功事件 runConnectionSuccessActivationFlow"] --> SKIP{"前置跳过条件?"} SKIP -->|"拍摄页已在前台"| X1["不打扰"] SKIP -->|"前台已有 搜索页/未激活升级页/强升弹窗"| X2["交给它们自己的流程"] SKIP -->|"否"| CF["checkFirmwareUpgrade"] CF --> CF2{"needUpgrade?"} CF2 -->|"升级检查失败"| CA["继续 checkActivationStatus"] CF2 -->|"true"| U1{"未激活 且 isRecoveryBlockedFirmwareVersion?"} U1 -->|"是"| UV["弹 PTUnActivateView 未激活先升级 :241"] U1 -->|"否"| U2{"forceToUpgrade 且 非升级中 且 未在拍录?"} U2 -->|"是"| FV["弹 PTFirmwareUpgradeNewVersionView"] U2 -->|"否"| CA CF2 -->|"false"| CA CA --> QR["queryServiceActivateStatus()"] QR --> ER{"返回 errorMessage?"} ER -->|"是"| T1["toast 错误信息 且 flowHandled:既不进拍摄也不弹激活页"] ER -->|"否"| ACT{"服务端 isActivate?"} ACT -->|"false"| REQ["activationRequired 弹 PTCameraSearchView(.connectCompleted(nil))"] ACT -->|"true"| MIS{"设备端或 BLE 配置 存在错配?"} MIS -->|"错配且未登录"| REQ2["activationRequired 激活页里出现强制登录层"] MIS -->|"错配且已登录"| SYNC["启动静默同步激活 然后 readyForCapture"] MIS -->|"没错配"| OKP["readyForCapture"]

3.2 激活页内部:显示哪一种界面(SearchView 侧)

flowchart TD SV["PTCameraSearchView 进入"] --> DBG{"PTDebugToolsManager.shared.isShowActivate
== false ? :959"} DBG -->|"是(调试开关关闭)"| NAV["navigateAfterCameraConnected()
完全跳过激活界面"] DBG -->|"否"| CHK["vm.checkActivationStatus :966"] CHK --> E1{"回调带 errorMessage?"} E1 -->|"是"| TO["showErrorAcitvateToast(errorMessage)
停在连接成功页 :969-975"] E1 -->|"否"| I1{"isActivated == true ?"} I1 -->|"是"| L1{"!PTCameraModel.isActivated
且 !isLoggedIn ?"} L1 -->|"是"| GATE["showActivateUnLoginGateView()
强制登录层 :979-982"] L1 -->|"否"| NAV2["navigateAfterCameraConnected() :984"] I1 -->|"否"| N1{"shouldNavigateToPreview == false ?"} N1 -->|"是"| DIS["dismissStayingOnCurrentPage()
即使未激活也关闭搜索页 :986"] N1 -->|"否"| SHOW["显示连接成功页(含「激活」按钮)
connectingView 隐藏 :989-991"] SHOW --> RES["用户点激活 → 结果态映射 :1276-1300"] RES --> R1{".activateState(?)"} R1 -->|"success"| V1["activateSuccessView"] R1 -->|"failed(.notLoggedIn)"| V2["connectSuccessView + presentLoginView() :911"] R1 -->|"failed(networkUnavailable / firmwareVersionInvalid*)"| V3["activationFailedStateView
按 reason 配置 :900-908"] R1 -->|"failed(其他) 或 activating"| V4["activateFailView(失败页,可重试/上传日志)"] V4 --> TOAST["deviceAlreadyActivated / activationFailed
/ snEmpty / activateWIFIFailed → toast :896-900"]
Mermaid 源码(图 3.2)
flowchart TD SV["PTCameraSearchView 进入"] --> DBG{"PTDebugToolsManager.shared.isShowActivate == false ? :959"} DBG -->|"是(调试开关关闭)"| NAV["navigateAfterCameraConnected 完全跳过激活界面"] DBG -->|"否"| CHK["vm.checkActivationStatus :966"] CHK --> E1{"回调带 errorMessage?"} E1 -->|"是"| TO["showErrorAcitvateToast 停在连接成功页 :969-975"] E1 -->|"否"| I1{"isActivated == true ?"} I1 -->|"是"| L1{"设备端未激活 且 未登录 ?"} L1 -->|"是"| GATE["showActivateUnLoginGateView 强制登录层 :979-982"] L1 -->|"否"| NAV2["navigateAfterCameraConnected :984"] I1 -->|"否"| N1{"shouldNavigateToPreview == false ?"} N1 -->|"是"| DIS["dismissStayingOnCurrentPage 即使未激活也关闭搜索页 :986"] N1 -->|"否"| SHOW["显示连接成功页 含激活按钮 :989-991"] SHOW --> RES["用户点激活 结果态映射 :1276-1300"] RES --> R1{"activateState 取值?"} R1 -->|"success"| V1["activateSuccessView"] R1 -->|"failed notLoggedIn"| V2["connectSuccessView 与 presentLoginView :911"] R1 -->|"failed networkUnavailable 或 firmwareVersionInvalid*"| V3["activationFailedStateView 按 reason 配置 :900-908"] R1 -->|"failed 其他 或 activating"| V4["activateFailView 可重试或上传日志"] V4 --> TOAST["deviceAlreadyActivated 等 直接 toast :896-900"]
这张图里的三个「隐藏条件」值得单独记住

图 4 · 三份激活状态与静默同步激活的重试循环

三份状态互相独立,判断时必须组合;错配(服务端=true、设备端/BLE=false)是触发「静默同步」与「强制登录层」的唯一原因。

flowchart LR S1["服务端激活态
GET /device/isActivated/{sn}
+ UserDefaults 缓存"] --> CMP{"三者组合判定"} S2["设备端激活态
BLE 上报 is_activated
PTCameraModel.isActivated:400"] --> CMP S3["蓝牙配置激活态
PTDeviceManager.currentWiFiConfiguration.isActivated
PTBluetoothTypes:411"] --> CMP CMP -->|"服务端 true / 设备端或BLE false"| MISMATCH["错配"] CMP -->|"三者一致 true"| PASS["无需处理"] MISMATCH -->|"已登录"| SIL["PTSilentActivationSyncCoordinator.startIfNeeded()
FlowManager:53"] MISMATCH -->|"未登录"| LOGIN["强制登录层"] SIL --> LOOP["perform():activateWIFI(accountId)"] LOOP -->|"成功 或 返回 already activated"| GOOD["markActivatedAndStop()
→ setActivatedManually(true)"] LOOP -->|"失败"| WAIT["retryCount+=1
schedule(delay: 3.0)"] --> LOOP GOOD --> ST{"停止条件"} ST -->|"isActivated 已为真"| END["stop():workItem 取消、retryCount 归零"]
Mermaid 源码(图 4)
flowchart LR S1["服务端激活态 GET /device/isActivated/sn 与 UserDefaults 缓存"] --> CMP{"三者组合判定"} S2["设备端激活态 BLE 上报 is_activated 写入 PTCameraModel.isActivated:400"] --> CMP S3["蓝牙配置激活态 PTDeviceManager.currentWiFiConfiguration.isActivated"] --> CMP CMP -->|"服务端 true 而 设备端或BLE false"| MISMATCH["错配"] CMP -->|"三者一致 true"| PASS["无需处理"] MISMATCH -->|"已登录"| SIL["PTSilentActivationSyncCoordinator.startIfNeeded FlowManager:53"] MISMATCH -->|"未登录"| LOGIN["强制登录层"] SIL --> LOOP["perform:activateWIFI(accountId)"] LOOP -->|"成功 或返回 already activated"| GOOD["markActivatedAndStop 然后 setActivatedManually(true)"] LOOP -->|"失败"| WAIT["retryCount 加 1 schedule delay 3.0"] --> LOOP GOOD --> ST{"停止条件"} ST -->|"isActivated 已为真"| END["stop:workItem 取消 retryCount 归零"]

图 5 · 后续「去激活」改造地图(规划,未实施)

仅为标注位置用:去激活=把下面 4 个红框短路掉,同时保留蓝色设备侧指令与所有 UI 代码(不可达即可)。

flowchart TD IN["连接/激活入口"] --> X1{{"短路点 1
+Activation.swift:12
跳过 4 项前置 + 两个云端调用"}} X1 --> DEV1["保留:sendUserId(accountId)"] X1 --> DEV2["保留:activateWIFI(accountId)"] IN --> X2{{"短路点 2
PTDeviceNetWorkService:729
queryServiceActivateStatus 直接回 true"}} IN --> X3{{"短路点 3
PTCameraModel:1454
isActivated 写入恒真"}} IN --> X4{{"短路点 4
PTCameraSearchStrategySD2402:139
连 WiFi 前预查短路"}} X2 --> R["不再请求 /device/isActivated/{sn}"] X4 --> R2["不再因预查失败打成 网络错误"] X3 --> R3["所有门禁自然放行
Moment 素材 / 智慧感知快速预览 / Flutter 状态"] R --> FIN["readyForCapture"] R3 --> FIN FIN --> DEL["激活页、失败页、强制登录层、未激活先升级
全部保留代码但不可达"]
Mermaid 源码(图 5)
flowchart TD IN["连接/激活入口"] --> X1{{"短路点 1 +Activation.swift:12 跳过 4 项前置与两个云端调用"}} X1 --> DEV1["保留:sendUserId(accountId)"] X1 --> DEV2["保留:activateWIFI(accountId)"] IN --> X2{{"短路点 2 PTDeviceNetWorkService:729 queryServiceActivateStatus 直接回 true"}} IN --> X3{{"短路点 3 PTCameraModel:1454 isActivated 写入恒真"}} IN --> X4{{"短路点 4 PTCameraSearchStrategySD2402:139 连 WiFi 前预查短路"}} X2 --> R["不再请求 /device/isActivated/sn"] X4 --> R2["不再因预查失败打成网络错误"] X3 --> R3["所有门禁自然放行 Moment 素材 智慧感知快速预览 Flutter 状态"] R --> FIN["readyForCapture"] R3 --> FIN FIN --> DEL["激活页 失败页 强制登录层 未激活先升级 全部保留代码但不可达"]
读图要点(一句话版)