refactor(offline-booking): 优化离线预约缓存轮询逻辑
- 拆分 normalize_options 为纯函数和保存逻辑函数 - 改进网络监听器注册逻辑,增加返回值判断 - 优化轮询启动逻辑,支持网络恢复时重启 - 完善状态同步规则和注释说明
Showing
2 changed files
with
90 additions
and
36 deletions
| ... | @@ -150,8 +150,11 @@ export const build_offline_qr_list = (bill) => { | ... | @@ -150,8 +150,11 @@ export const build_offline_qr_list = (bill) => { |
| 150 | * - 仅在有授权且网络可用时调用 | 150 | * - 仅在有授权且网络可用时调用 |
| 151 | * - 成功后将数据存储到本地缓存(key: OFFLINE_BOOKING_DATA) | 151 | * - 成功后将数据存储到本地缓存(key: OFFLINE_BOOKING_DATA) |
| 152 | * @param {boolean} force - 是否强制刷新,默认为 false. force 参数的核心作用是控制是否忽略 “正在进行的缓存请求”, 管的是 “是否允许重复发起请求”,不管 “请求能不能成功执行缓存”。 | 152 | * @param {boolean} force - 是否强制刷新,默认为 false. force 参数的核心作用是控制是否忽略 “正在进行的缓存请求”, 管的是 “是否允许重复发起请求”,不管 “请求能不能成功执行缓存”。 |
| 153 | - * @returns {Promise<void>} | 153 | + * @returns 不同情况返回值不一样 |
| 154 | + * - 成功时包含格式化后的预约记录项列表 | ||
| 155 | + * - 失败时包含错误信息(如网络错误、授权失败等) | ||
| 154 | */ | 156 | */ |
| 157 | + | ||
| 155 | export const refresh_offline_booking_cache = async ({ force = false } = {}) => { | 158 | export const refresh_offline_booking_cache = async ({ force = false } = {}) => { |
| 156 | // 1. 检查是否有正在进行的刷新请求 | 159 | // 1. 检查是否有正在进行的刷新请求 |
| 157 | // 2. 如果有,且 force 为 false,则直接返回该 Promise | 160 | // 2. 如果有,且 force 为 false,则直接返回该 Promise |
| ... | @@ -179,6 +182,7 @@ export const refresh_offline_booking_cache = async ({ force = false } = {}) => { | ... | @@ -179,6 +182,7 @@ export const refresh_offline_booking_cache = async ({ force = false } = {}) => { |
| 179 | // 过滤出状态为3(已完成)的记录 | 182 | // 过滤出状态为3(已完成)的记录 |
| 180 | const normalized = data.map(normalize_bill_item).filter((item) => item && item.pay_id && item.status == 3) | 183 | const normalized = data.map(normalize_bill_item).filter((item) => item && item.pay_id && item.status == 3) |
| 181 | if (normalized.length > 0) { | 184 | if (normalized.length > 0) { |
| 185 | + // TAG: 核心逻辑:将过滤后的记录存储到本地缓存 | ||
| 182 | Taro.setStorageSync(OFFLINE_BOOKING_CACHE_KEY, normalized) | 186 | Taro.setStorageSync(OFFLINE_BOOKING_CACHE_KEY, normalized) |
| 183 | } | 187 | } |
| 184 | } | 188 | } | ... | ... |
| ... | @@ -19,6 +19,14 @@ import { get_network_type, is_usable_network } from '@/utils/network' | ... | @@ -19,6 +19,14 @@ import { get_network_type, is_usable_network } from '@/utils/network' |
| 19 | * @property {Boolean} has_network_listener 是否已注册网络监听器 | 19 | * @property {Boolean} has_network_listener 是否已注册网络监听器 |
| 20 | * @property {Function} network_listener 网络监听器 | 20 | * @property {Function} network_listener 网络监听器 |
| 21 | * @property {Promise} network_listener_promise 网络监听器Promise | 21 | * @property {Promise} network_listener_promise 网络监听器Promise |
| 22 | + * | ||
| 23 | + * 状态同步规则(app_enabled 与 ref_count): | ||
| 24 | + * - app_enabled = true 时,ref_count >= 1(至少有一个使用者) | ||
| 25 | + * - app_enabled = false 时,ref_count = 0(无使用者) | ||
| 26 | + * - enable_offline_booking_cache_polling 会设置 app_enabled = true | ||
| 27 | + * - disable_offline_booking_cache_polling 会设置 app_enabled = false | ||
| 28 | + * - acquire_polling_ref 会增加 ref_count | ||
| 29 | + * - release_polling_ref 会减少 ref_count,降为0时触发清理 | ||
| 22 | */ | 30 | */ |
| 23 | 31 | ||
| 24 | /** @type {PollingState} */ | 32 | /** @type {PollingState} */ |
| ... | @@ -36,13 +44,21 @@ const polling_state = { | ... | @@ -36,13 +44,21 @@ const polling_state = { |
| 36 | } | 44 | } |
| 37 | 45 | ||
| 38 | /** | 46 | /** |
| 39 | - * @description: 缓存最后一次 options(用于网络恢复时重启轮询) | 47 | + * @description: 规范化选项参数(纯函数,无副作用) |
| 40 | * @param {Object} options 选项 | 48 | * @param {Object} options 选项 |
| 41 | - * @return {Object} 最后一次选项 | 49 | + * @return {Object} 规范化后的选项 |
| 42 | */ | 50 | */ |
| 43 | - | ||
| 44 | const normalize_options = (options) => { | 51 | const normalize_options = (options) => { |
| 45 | - polling_state.last_options = options || polling_state.last_options || {} | 52 | + return options || {} |
| 53 | +} | ||
| 54 | + | ||
| 55 | +/** | ||
| 56 | + * @description: 保存最后一次选项(用于网络恢复时重启轮询) | ||
| 57 | + * @param {Object} options 选项 | ||
| 58 | + * @return {Object} 保存后的选项 | ||
| 59 | + */ | ||
| 60 | +const save_last_options = (options) => { | ||
| 61 | + if (options) polling_state.last_options = options | ||
| 46 | return polling_state.last_options | 62 | return polling_state.last_options |
| 47 | } | 63 | } |
| 48 | 64 | ||
| ... | @@ -58,13 +74,16 @@ const normalize_options = (options) => { | ... | @@ -58,13 +74,16 @@ const normalize_options = (options) => { |
| 58 | * @param {Boolean} options.force 是否强制刷新 | 74 | * @param {Boolean} options.force 是否强制刷新 |
| 59 | */ | 75 | */ |
| 60 | const run_refresh_once = async (options) => { | 76 | const run_refresh_once = async (options) => { |
| 61 | - if (polling_state.in_flight) return // 核心防重复——如果正在刷新,直接返回 | 77 | + // 前置检查:不满足轮询条件时直接返回(网络不可用或无引用) |
| 62 | - // 标记为“正在刷新” | 78 | + if (!should_run_polling()) return |
| 79 | + // 核心防重复——如果正在刷新,直接返回 | ||
| 80 | + if (polling_state.in_flight) return | ||
| 81 | + // 标记为"正在刷新" | ||
| 63 | polling_state.in_flight = true | 82 | polling_state.in_flight = true |
| 64 | try { | 83 | try { |
| 65 | await refresh_offline_booking_cache({ force: !!options?.force }) | 84 | await refresh_offline_booking_cache({ force: !!options?.force }) |
| 66 | } finally { | 85 | } finally { |
| 67 | - // 刷新完成后,标记为“刷新完成” | 86 | + // 刷新完成后,标记为"刷新完成" |
| 68 | polling_state.in_flight = false | 87 | polling_state.in_flight = false |
| 69 | } | 88 | } |
| 70 | } | 89 | } |
| ... | @@ -81,8 +100,16 @@ const update_network_usable = async () => { | ... | @@ -81,8 +100,16 @@ const update_network_usable = async () => { |
| 81 | /** | 100 | /** |
| 82 | * @description: 判断是否需要运行轮询 | 101 | * @description: 判断是否需要运行轮询 |
| 83 | * @return {Boolean} 是否需要运行轮询 | 102 | * @return {Boolean} 是否需要运行轮询 |
| 103 | + * | ||
| 104 | + * 返回 false 的条件: | ||
| 105 | + * 1. ref_count <= 0:无使用者,无需轮询 | ||
| 106 | + * 2. network_usable === false:网络不可用,无需轮询 | ||
| 107 | + * 3. network_usable === null:网络状态未初始化,避免在无监听器时误判 | ||
| 108 | + * | ||
| 109 | + * 返回 true 的条件: | ||
| 110 | + * 1. ref_count > 0:至少有一个使用者 | ||
| 111 | + * 2. network_usable === true:网络可用 | ||
| 84 | */ | 112 | */ |
| 85 | - | ||
| 86 | const should_run_polling = () => { | 113 | const should_run_polling = () => { |
| 87 | if (polling_state.ref_count <= 0) return false | 114 | if (polling_state.ref_count <= 0) return false |
| 88 | if (polling_state.network_usable === false) return false | 115 | if (polling_state.network_usable === false) return false |
| ... | @@ -92,9 +119,8 @@ const should_run_polling = () => { | ... | @@ -92,9 +119,8 @@ const should_run_polling = () => { |
| 92 | 119 | ||
| 93 | /** | 120 | /** |
| 94 | * @description: 确保网络监听器已注册 | 121 | * @description: 确保网络监听器已注册 |
| 95 | - * @return {Promise} 网络监听器Promise | 122 | + * @return {Promise<Boolean>} 是否注册成功(true=成功,false=失败) |
| 96 | */ | 123 | */ |
| 97 | - | ||
| 98 | const ensure_network_listener = async () => { | 124 | const ensure_network_listener = async () => { |
| 99 | /** | 125 | /** |
| 100 | * 代码优先通过两个条件判断避免重复执行监听器逻辑 | 126 | * 代码优先通过两个条件判断避免重复执行监听器逻辑 |
| ... | @@ -104,12 +130,13 @@ const ensure_network_listener = async () => { | ... | @@ -104,12 +130,13 @@ const ensure_network_listener = async () => { |
| 104 | 130 | ||
| 105 | if (polling_state.has_network_listener) { | 131 | if (polling_state.has_network_listener) { |
| 106 | await update_network_usable() | 132 | await update_network_usable() |
| 107 | - return | 133 | + return true |
| 108 | } | 134 | } |
| 109 | 135 | ||
| 110 | if (polling_state.network_listener_promise) { | 136 | if (polling_state.network_listener_promise) { |
| 111 | await polling_state.network_listener_promise | 137 | await polling_state.network_listener_promise |
| 112 | - return | 138 | + // 等待注册完成后检查是否成功 |
| 139 | + return polling_state.has_network_listener | ||
| 113 | } | 140 | } |
| 114 | 141 | ||
| 115 | // 立即执行异步的监听器注册流程(标记状态→更新网络可用性→定义回调→注册监听) | 142 | // 立即执行异步的监听器注册流程(标记状态→更新网络可用性→定义回调→注册监听) |
| ... | @@ -125,14 +152,16 @@ const ensure_network_listener = async () => { | ... | @@ -125,14 +152,16 @@ const ensure_network_listener = async () => { |
| 125 | const type = res?.networkType || 'unknown' | 152 | const type = res?.networkType || 'unknown' |
| 126 | polling_state.network_usable = is_connected && is_usable_network(type) | 153 | polling_state.network_usable = is_connected && is_usable_network(type) |
| 127 | 154 | ||
| 128 | - // 网络不可用时,停止轮询 | 155 | + // 改进:不再主动停止轮询,由 run_refresh_once 中的 should_run_polling() 前置检查控制 |
| 129 | - if (!polling_state.network_usable) { | 156 | + // 优势: |
| 130 | - stop_offline_booking_cache_polling() | 157 | + // 1. 避免网络恢复时需要额外的重启逻辑 |
| 131 | - return | 158 | + // 2. 保持定时器稳定,避免频繁启动/停止 |
| 132 | - } | 159 | + // 3. 网络不可用时,刷新操作会在 run_refresh_once 中被前置检查过滤掉 |
| 133 | 160 | ||
| 134 | - if (should_run_polling()) { | 161 | + // 网络恢复时,确保轮询正在运行(处理之前因网络不可用而可能停止的轮询) |
| 135 | - start_offline_booking_cache_polling(polling_state.last_options || {}) | 162 | + if (polling_state.network_usable && should_run_polling()) { |
| 163 | + // 传入 restart: true,支持重启逻辑 | ||
| 164 | + start_offline_booking_cache_polling({ ...(polling_state.last_options || {}), restart: true }) | ||
| 136 | } | 165 | } |
| 137 | } | 166 | } |
| 138 | 167 | ||
| ... | @@ -148,12 +177,15 @@ const ensure_network_listener = async () => { | ... | @@ -148,12 +177,15 @@ const ensure_network_listener = async () => { |
| 148 | })() | 177 | })() |
| 149 | 178 | ||
| 150 | try { | 179 | try { |
| 151 | - // 等待网络监听器初始化完成, Taro.onNetworkStatusChange | 180 | + // 等待网络监听器初始化完成 |
| 152 | await polling_state.network_listener_promise | 181 | await polling_state.network_listener_promise |
| 153 | } finally { | 182 | } finally { |
| 154 | - // 等待注册流程完成后,强制清空 Promise 缓存(finally 块),保证下次执行逻辑时状态干净。 | 183 | + // 等待注册流程完成后,强制清空 Promise 缓存(finally 块),保证下次执行逻辑时状态干净 |
| 155 | polling_state.network_listener_promise = null | 184 | polling_state.network_listener_promise = null |
| 156 | } | 185 | } |
| 186 | + | ||
| 187 | + // 返回注册是否成功 | ||
| 188 | + return polling_state.has_network_listener | ||
| 157 | } | 189 | } |
| 158 | 190 | ||
| 159 | /** | 191 | /** |
| ... | @@ -201,15 +233,27 @@ const teardown_network_listener = () => { | ... | @@ -201,15 +233,27 @@ const teardown_network_listener = () => { |
| 201 | * @param {Number} options.interval_ms 轮询间隔,单位毫秒 | 233 | * @param {Number} options.interval_ms 轮询间隔,单位毫秒 |
| 202 | * @param {Boolean} options.immediate 是否立即刷新一次 | 234 | * @param {Boolean} options.immediate 是否立即刷新一次 |
| 203 | * @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache) | 235 | * @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache) |
| 236 | + * @param {Boolean} options.restart 是否为重启操作(网络恢复时调用) | ||
| 204 | */ | 237 | */ |
| 205 | - | ||
| 206 | const start_offline_booking_cache_polling = (options) => { | 238 | const start_offline_booking_cache_polling = (options) => { |
| 207 | - normalize_options(options) | 239 | + options = normalize_options(options) |
| 208 | if (!should_run_polling()) return // 不满足轮询条件直接返回 | 240 | if (!should_run_polling()) return // 不满足轮询条件直接返回 |
| 241 | + | ||
| 209 | const interval_ms = Number(options?.interval_ms || 60000) | 242 | const interval_ms = Number(options?.interval_ms || 60000) |
| 210 | - if (polling_state.running) return // 核心防重复——如果已经在轮询,直接返回 | 243 | + const is_restart = options?.restart === true |
| 244 | + | ||
| 245 | + // 改进:区分首次启动和重启的防重逻辑 | ||
| 246 | + // 首次启动时,如果已经在轮询则直接返回(防重复启动) | ||
| 247 | + // 重启时,需要清除旧定时器并重新建立(支持网络恢复时重启) | ||
| 248 | + if (polling_state.running && !is_restart) return | ||
| 211 | 249 | ||
| 212 | - polling_state.running = true // 标记为“正在轮询” | 250 | + // 如果是重启或定时器已存在,先清除旧定时器 |
| 251 | + if (is_restart && polling_state.timer_id) { | ||
| 252 | + clearInterval(polling_state.timer_id) | ||
| 253 | + polling_state.timer_id = null | ||
| 254 | + } | ||
| 255 | + | ||
| 256 | + polling_state.running = true // 标记为"正在轮询" | ||
| 213 | 257 | ||
| 214 | // 立即刷新一次,确保轮询开始时数据是最新的 | 258 | // 立即刷新一次,确保轮询开始时数据是最新的 |
| 215 | if (options?.immediate !== false) { | 259 | if (options?.immediate !== false) { |
| ... | @@ -245,15 +289,18 @@ const stop_offline_booking_cache_polling = () => { | ... | @@ -245,15 +289,18 @@ const stop_offline_booking_cache_polling = () => { |
| 245 | 289 | ||
| 246 | /** | 290 | /** |
| 247 | * @description: 增加轮询引用计数 | 291 | * @description: 增加轮询引用计数 |
| 248 | - * 核心动作:将全局的 ref_count 加 1,代表 “又多了一个场景需要使用轮询功能”。 | 292 | + * 核心动作:将全局的 ref_count 加 1,代表 "又多了一个场景需要使用轮询功能"。 |
| 249 | * @param {Object} options 选项 | 293 | * @param {Object} options 选项 |
| 250 | */ | 294 | */ |
| 251 | - | ||
| 252 | const acquire_polling_ref = (options) => { | 295 | const acquire_polling_ref = (options) => { |
| 253 | - normalize_options(options) | 296 | + save_last_options(options) |
| 254 | polling_state.ref_count += 1 | 297 | polling_state.ref_count += 1 |
| 255 | - // 增加引用计数后,确保网络监听器已注册并初始化完成,然后启动轮询 | 298 | + // 改进:检查网络监听器注册结果,只有成功后才启动轮询 |
| 256 | - ensure_network_listener().then(() => start_offline_booking_cache_polling(polling_state.last_options || {})) | 299 | + ensure_network_listener().then((success) => { |
| 300 | + if (success && polling_state.last_options) { | ||
| 301 | + start_offline_booking_cache_polling(polling_state.last_options) | ||
| 302 | + } | ||
| 303 | + }) | ||
| 257 | } | 304 | } |
| 258 | 305 | ||
| 259 | /** | 306 | /** |
| ... | @@ -277,16 +324,19 @@ const release_polling_ref = () => { | ... | @@ -277,16 +324,19 @@ const release_polling_ref = () => { |
| 277 | * @param {Boolean} options.immediate 是否立即刷新一次 | 324 | * @param {Boolean} options.immediate 是否立即刷新一次 |
| 278 | * @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache) | 325 | * @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache) |
| 279 | */ | 326 | */ |
| 280 | - | ||
| 281 | export const enable_offline_booking_cache_polling = (options) => { | 327 | export const enable_offline_booking_cache_polling = (options) => { |
| 282 | - normalize_options(options) | 328 | + save_last_options(options) |
| 283 | /** | 329 | /** |
| 284 | - * 核心目的:对 app_enabled=true 的场景做兜底,确保轮询在 “已启用但异常停止” 时能被主动恢复,而非被动等待网络变化; | 330 | + * 核心目的:对 app_enabled=true 的场景做兜底,确保轮询在 "已启用但异常停止" 时能被主动恢复,而非被动等待网络变化; |
| 285 | * 执行逻辑:先保证网络监听器(轮询的依赖)就绪,再尝试启动轮询,且利用 start_offline_booking_cache_polling 的幂等性避免重复; | 331 | * 执行逻辑:先保证网络监听器(轮询的依赖)就绪,再尝试启动轮询,且利用 start_offline_booking_cache_polling 的幂等性避免重复; |
| 286 | - * 设计思维:体现了 “主动调用需即时生效” 的用户体验考量,以及 “依赖前置检查” 的工程化思维 —— 先保证依赖(监听器)就绪,再执行核心操作(启动轮询)。 | 332 | + * 设计思维:体现了 "主动调用需即时生效" 的用户体验考量,以及 "依赖前置检查" 的工程化思维 —— 先保证依赖(监听器)就绪,再执行核心操作(启动轮询)。 |
| 287 | */ | 333 | */ |
| 288 | if (polling_state.app_enabled) { | 334 | if (polling_state.app_enabled) { |
| 289 | - ensure_network_listener().then(() => start_offline_booking_cache_polling(polling_state.last_options || {})) | 335 | + ensure_network_listener().then((success) => { |
| 336 | + if (success && polling_state.last_options) { | ||
| 337 | + start_offline_booking_cache_polling(polling_state.last_options) | ||
| 338 | + } | ||
| 339 | + }) | ||
| 290 | return | 340 | return |
| 291 | } | 341 | } |
| 292 | polling_state.app_enabled = true | 342 | polling_state.app_enabled = true | ... | ... |
-
Please register or login to post a comment