You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Capacitor 5 iOS端使用Interval导致重复提交问题排查

iOS端离线野火报告重复提交问题排查求助

我开发了一款基于Capacitor 5与Angular的野火报告应用,核心功能:

  • 用户网络不佳时提交的报告会被离线存储
  • 应用通过RxJS interval结合Capawesome Background Task API,每3分钟检测网络状态,在线时自动提交离线报告

目前浏览器及Android端通过本地存储submissionID的重复提交校验正常,但iOS端校验失效,出现相同submissionID的重复提交情况。

已完成的排查:

  • 移除Capawesome后台任务,仅前台运行interval仍会出现重复提交
  • 尝试过Ionic Storage、localStorage、Capacitor Preferences多种存储方式,问题未解决
  • 使用setInterval与RxJS interval均存在相同问题

请求协助排查:是代码逻辑问题,还是Capacitor的iOS特定Bug?


相关代码

rof-title-page.component.ts

ngOnInit(): void {
    if (this.reportOfFirePage.currentPage.instance.id === 'first-page') {
      App.removeAllListeners();
      // run background task
      (async () => {
        await this.backgroundListener();
      })();
    }
  }

async backgroundListener() {
    App.addListener('appStateChange', async ({ isActive }) => {
      if (isActive) {
        return;
      }
      // The app state has been changed to inactive.
      // Start the background task by calling `beforeExit`.
      const taskId = await BackgroundTask.beforeExit(async () => {
        const self = this;

        if(!this.intervalRef) {
          this.intervalRef = interval(30000).subscribe(() => {
            self.checkStoredRoF();
          });
        }

        BackgroundTask.finish({ taskId });
      });
    });
  }

async checkStoredRoF() {
    const self = this;
    // first check do 24 hour check in storage and remove offline RoF if timeframe has elapsed
    await this.commonUtilityService.removeInvalidOfflineRoF();

    // check if the app is online and if so, check for saved offline RoF to be submitted
    await this.commonUtilityService.checkOnlineStatus().then(async (result) => {
      if (result) {
        await this.reportOfFireService.syncDataWithServer().then(response => {
          if(response) self?.intervalRef?.unsubscribe();
        });
        
      };
    });
  }

report-of-fire-service.ts

async syncDataWithServer() {
    let dataSynced = false;
    let submissionID = null;
    let duplicateStored = false;
    let submissionIdList = null;
    let offlineReport = null;

    try {
      // Fetch and submit locally stored data
      offlineReport = this.storageService.getData('offlineReportData')
      submissionIdList = this.storageService.getData('submissionIDList')
      
      if (offlineReport) {
         // Check for duplicate, reject if submissionID has already been stored
        const offlineJson = JSON.parse(offlineReport)
        if(offlineJson?.resource) {
          const resourceJson = JSON.parse(offlineJson.resource)
          submissionID = resourceJson?.submissionID
          if (submissionID && submissionIdList?.includes(submissionID)) {
            duplicateStored = true;
          }
        }

        // Reject duplicate if submissionID has already been stored
        if(duplicateStored) return true;

        // Send the report to the server
        const response =
          await this.submitOfflineReportToServer(offlineReport).then(async response => {
            if (response.success) {
              dataSynced = true;
              // Remove the locally stored data if sync is successful
              this.storageService.removeData('offlineReportData');
              // store submissionID for duplicate check 
              if(submissionID) {
                submissionIdList = submissionIdList ? submissionIdList + ", " +  submissionID : submissionID;
                this.storageService.saveData('submissionIDList', submissionIdList)
              }
              App.removeAllListeners();
            }
          });
      }
    } catch (error) {
      console.error('Sync failed:', error);
    }
    return dataSynced;
  }

async submitOfflineReportToServer(offlineReport?): Promise<any> {
    // retrieve the offline RoF from the device's storage and convert to FormData for submission
    // images will already to converted to base64 string from initial submission
    const rofUrl = this.appConfigService.getConfig().rest['fire-report-api'];
    const rofJson = JSON.parse(offlineReport);
    const resource = rofJson.resource;
    const image1 = rofJson.image1;
    const image2 = rofJson.image2;
    const image3 = rofJson.image3;

    const formData = new FormData();
    if (resource) {
      formData.append('resource', resource);
    }

    if (image1) {
      formData.append('image1', image1);
    }
    if (image2) {
      formData.append('image2', image2);
    }
    if (image3) {
      formData.append('image3', image3);
    }

    try {
      // Make an HTTP POST request to your server's API endpoint
      const response = await fetch(rofUrl, {
        method: 'POST',
        body: formData,
      });

      if (response.ok || response.status == 200) {
        // Remove the locally stored data if sync is successful
        this.storageService.removeData('offlineReportData');
        App.removeAllListeners();
        // The server successfully processed the report
        return { success: true, message: 'Report submitted successfully' };
      } else {
        // The server encountered an error
        return { success: false, message: JSON.stringify(response) };
      }
    } catch (error) {
      // An error occurred during the HTTP request
      return {
        success: false,
        message: 'An error occurred while submitting the report',
      };
    }
  }
}

async submitToStorage(formData: FormData) {
    const object = {};
    formData.forEach((value, key) => (object[key] = value));
    const json = JSON.stringify(object);
    const data = this.storageService.getData('offlineReportData')
    if (data == json) {
      return;
    } else this.storageService.saveData('offlineReportData', json);
  }

local-storage-service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class LocalStorageService {
  public saveData(key: string, value: string) {
    localStorage.setItem(key, value);
  }

  public getData(key: string) {
    return localStorage.getItem(key);
  }
  public removeData(key: string) {
    localStorage.removeItem(key);
  }

  public clearData() {
    localStorage.clear();
  }
}

可能的问题点及修复建议

1. 存储操作的异步竞态问题

iOS WebView的localStorage可能存在读写延迟,建议改用Capacitor Preferences的异步API,并确保读取操作等待完成,避免读取到旧数据:

// 修改存储读取逻辑为异步
offlineReport = await Preferences.get({ key: 'offlineReportData' }).then(res => res.value);
submissionIdList = await Preferences.get({ key: 'submissionIDList' }).then(res => res.value);

2. Interval订阅重复创建

多次触发appStateChange事件时,可能创建多个interval订阅,导致同时触发多次提交。创建前先取消已有订阅:

// 在创建新interval前取消旧订阅
if(this.intervalRef) {
  this.intervalRef.unsubscribe();
}
this.intervalRef = interval(30000).subscribe(() => {
  self.checkStoredRoF();
});

3. 重复校验逻辑漏洞

用逗号分隔字符串存储submissionID可能因特殊字符导致误判,改用数组JSON格式存储更可靠:

// 读取时解析为数组
const submissionIds = submissionIdList ? JSON.parse(submissionIdList) : [];
if(submissionIds.includes(submissionID)) {
  duplicateStored = true;
}

// 保存时更新数组
submissionIds.push(submissionID);
this.storageService.saveData('submissionIDList', JSON.stringify(submissionIds));

4. iOS后台任务执行限制

iOS对BackgroundTask.beforeExit的执行时间有严格限制,在其中创建interval无法持续运行。建议改用Capacitor BackgroundFetch插件实现定期后台同步,符合iOS后台任务规范。

5. 提交成功后的存储操作重复

submitOfflineReportToServer和syncDataWithServer中都调用了removeData('offlineReportData'),可能导致状态不一致,建议只在syncDataWithServer中统一处理存储更新。

内容的提问来源于stack exchange,提问作者user24471980

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 20:22:05