流程图集 · Momenta iOS · 需求 R2
相机「激活」流程(现状)· Mermaid 图集
含「何时会显示激活界面」的完整判定树 · 代码未做任何改动
对象 momenta/ios @ develop (13578ebf)
行号 均为该基线下的实际行号
日期 2026-10-14
提示 图下方折叠块里是 Mermaid 源码,可直接复制
图 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"]
这张图里的三个「隐藏条件」值得单独记住
isShowActivate == false(调试工具)会整段跳过激活界面 —— 但它只是 UI 旁路,不阻止服务端查询/提交,也不改门禁变量(所以不能当开关用)。
shouldNavigateToPreview == false(诊断页会这样设置,见 PTHelpDiagnosisViewController.swift:468)时,未激活也不显示激活界面,直接关闭搜索页。
- 激活页里的「强制登录层」只在「服务端已激活 + 设备端未激活 + 未登录」三者同时成立时出现 —— 换言之:登录要求来自"服务端说激活了但相机说没激活"这种错配,不是无条件要求登录。
图 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["激活页 失败页 强制登录层 未激活先升级 全部保留代码但不可达"]
读图要点(一句话版)
- 要进拍摄页,必须过
readyForCapture;而它由服务端激活态决定(设备端不一致只是触发静默同步,不是阻断)。
- 「激活界面」有两种出现路径:FlowManager 主动弹搜索页(服务端未激活 / 错配且未登录),以及用户自己点连接成功页的「激活」按钮。
- 登录只在「服务端已激活 + 设备端未激活 + 未登录」时被强制 —— 这是唯一的登录硬门槛。
- 激活失败后有后台自愈:静默同步激活每 3 秒重试
activateWIFI,成功即置位并停止。