import { getPendingSyncRecords, getPendingSyncRecordCount, markRecordSynced, cleanOldRecords, getOfflineConfig, saveOfflineConfig } from './db'; import { deleteLocalImage } from './fs'; import { uploadImage } from '../api/upload'; import { addVisitorRecord, idCardSingOut, pullSigninData, pullSignoutData } from '../api/visitorRecord'; import { getVisitAreaTree } from '../api/visitArea'; let isSyncing = false; const getCurrentNetworkType = async () => { return new Promise(resolve => { uni.getNetworkType({ success: (res) => resolve(res.networkType), fail: () => resolve('unknown') }); }); }; /** * 启动离线数据同步队列 * 核心逻辑: * 1. 查询所有未同步的数据 * 2. 如果有图片路径,先上传图片获取云端 URL * 3. 提交完整业务数据到后端 * 4. 成功后:将数据库记录置为已同步,并删除本地沙盒图片释放内存 */ export const startSyncQueue = async () => { console.log('[Sync] startSyncQueue 进入,当前同步状态:', { isSyncing, }); if (isSyncing) { console.warn('[Sync] 同步队列已在执行中,本次跳过'); return; } // 简单判断当前网络是否可用 const networkType = await getCurrentNetworkType(); console.log('[Sync] 当前网络状态:', networkType); if (networkType === 'none') { console.warn('[Sync] 当前离线,跳过同步队列'); return; } isSyncing = true; try { let pendingRecords = []; try { pendingRecords = await getPendingSyncRecords(); console.log('[Sync] 读取到待同步记录:', { count: pendingRecords.length, records: pendingRecords.map((record) => ({ id: record.id, type: record.type, localImgPath: record.local_img_path, createTime: record.create_time, })), }); } catch (dbErr) { console.error('[Sync] 读取待同步记录失败:', dbErr); return; } if (pendingRecords.length === 0) { console.warn('[Sync] 当前没有待同步记录'); return; } // 分类待同步数据 const signinDataToPull = []; const signoutDataToPull = []; const recordsToSync = []; for (let record of pendingRecords) { // 如果创建时间距离当前超过 5 分钟,认为是离线堆积的旧数据,走批量 pull 接口 const isOfflineOldData = Date.now() - record.create_time > 5 * 60 * 1000; try { let payload = { ...(record.payload || {}), }; // 1. 如果存在本地图片,先上传图片,再把云端地址回填到业务 payload if (record.local_img_path) { console.log('[Sync] 开始上传本地图片:', { recordId: record.id, type: record.type, localImgPath: record.local_img_path, }); const uploadRes = await uploadImage(record.local_img_path); console.log('[Sync] 图片上传结果:', { recordId: record.id, type: record.type, localImgPath: record.local_img_path, uploadRes, }); if (uploadRes && uploadRes.code === 200 && uploadRes.url) { // 根据记录类型,将云端 URL 赋值给不同的业务 payload 字段 if (record.type === 'signout') { payload.signImageUrl = uploadRes.url; } else { payload.faceImageUrl = uploadRes.url; } } else { throw new Error('图片上传失败'); } } if (isOfflineOldData) { // 离线旧数据收集到数组中,准备批量提交 if (record.type === 'signout') { signoutDataToPull.push({ recordId: record.id, localImgPath: record.local_img_path, payload }); } else { signinDataToPull.push({ recordId: record.id, localImgPath: record.local_img_path, payload }); } } else { // 实时/较新的数据走单条实时接口 recordsToSync.push({ recordId: record.id, localImgPath: record.local_img_path, type: record.type, payload }); } } catch (innerErr) { console.error(`[Sync] 处理记录 ${record.id} 的图片或数据时失败:`, innerErr); // 如果处理单条前置(如传图)失败,跳过该条,等待下次轮询 } } // 2. 提交实时单条业务数据 for (let item of recordsToSync) { try { let submitRes; if (item.type === 'signout') { console.log('[Sync] 开始调用签离接口:', { recordId: item.recordId, payload: item.payload, }); submitRes = await idCardSingOut(item.payload); // await new Promise((resolve) => { // uni.showModal({ // title: '调试: 签离接口返回', // content: `记录ID: ${item.recordId}\n结果: ${JSON.stringify(submitRes).substring(0, 500)}`, // showCancel: false, // success: resolve, // fail: resolve // }); // }); } else { // type 为 appointment, online, 或它们加上 _signedout 后缀时,都作为进场记录同步 console.log('[Sync] 开始调用登记接口:', { recordId: item.recordId, type: item.type, payload: item.payload, }); submitRes = await addVisitorRecord(item.payload); // await new Promise((resolve) => { // uni.showModal({ // title: '调试: 登记接口返回', // content: `类型: ${item.type}\n记录ID: ${item.recordId}\n结果: ${JSON.stringify(submitRes).substring(0, 500)}`, // showCancel: false, // success: resolve, // fail: resolve // }); // }); } console.log('[Sync] 单条业务接口返回:', { recordId: item.recordId, type: item.type, submitRes, }); if (submitRes && submitRes.code === 200) { await markRecordSynced(item.recordId); if (item.localImgPath) await deleteLocalImage(item.localImgPath); } } catch (err) { console.error(`[Sync] 单条同步 ${item.recordId} 失败:`, err); } } // 3. 提交离线批量签到数据 if (signinDataToPull.length > 0) { try { const payloads = signinDataToPull.map(item => item.payload); console.log('[Sync] 开始调用批量补发签到接口:', { count: payloads.length, payloads, }); const pullRes = await pullSigninData(payloads); console.log('[Sync] 批量补发签到接口结果:', pullRes); if (pullRes && pullRes.code === 200) { for (let item of signinDataToPull) { await markRecordSynced(item.recordId); if (item.localImgPath) await deleteLocalImage(item.localImgPath); } } } catch (err) { console.error('[Sync] 批量补发签到数据失败:', err); } } // 4. 提交离线批量签离数据 if (signoutDataToPull.length > 0) { try { const payloads = signoutDataToPull.map(item => item.payload); console.log('[Sync] 开始调用批量补发签离接口:', { count: payloads.length, payloads, }); const pullRes = await pullSignoutData(payloads); console.log('[Sync] 批量补发签离接口结果:', pullRes); if (pullRes && pullRes.code === 200) { for (let item of signoutDataToPull) { await markRecordSynced(item.recordId); if (item.localImgPath) await deleteLocalImage(item.localImgPath); } } } catch (err) { console.error('[Sync] 批量补发签离数据失败:', err); } } } catch (error) { console.error('[Sync] 队列执行异常:', error); } finally { isSyncing = false; } }; /** * 手动触发全量离线同步: * 持续执行同步队列,直到待同步记录清空,或检测到本轮没有任何推进为止。 */ export const syncAllPendingRecords = async () => { if (isSyncing) { return { success: false, reason: 'syncing', total: 0, synced: 0, remaining: await getPendingSyncRecordCount(), }; } const networkType = await getCurrentNetworkType(); if (networkType === 'none') { return { success: false, reason: 'offline', total: 0, synced: 0, remaining: await getPendingSyncRecordCount(), }; } const totalPending = await getPendingSyncRecordCount(); if (totalPending === 0) { return { success: true, reason: 'empty', total: 0, synced: 0, remaining: 0, }; } let syncedCount = 0; let remainingCount = totalPending; let round = 0; const maxRounds = 20; while (remainingCount > 0 && round < maxRounds) { const beforeCount = await getPendingSyncRecordCount(); if (beforeCount <= 0) { remainingCount = 0; break; } await startSyncQueue(); const afterCount = await getPendingSyncRecordCount(); const currentRoundSynced = Math.max(beforeCount - afterCount, 0); syncedCount += currentRoundSynced; remainingCount = afterCount; round += 1; // 本轮没有任何推进,说明存在上传失败或脏数据,避免死循环卡死按钮。 if (afterCount >= beforeCount) { break; } } return { success: remainingCount === 0, reason: remainingCount === 0 ? 'completed' : 'partial', total: totalPending, synced: syncedCount, remaining: remainingCount, }; }; /** * 每日凌晨或空闲时清理超期废弃数据并同步最新配置字典 */ export const runDailyCleanup = async () => { // 1. 清理过期业务记录 cleanOldRecords().catch(err => { console.error('[Sync] 清理过期记录失败:', err); }); // 2. 定点同步访问区域树等配置数据 try { const networkType = await new Promise(resolve => { uni.getNetworkType({ success: (res) => resolve(res.networkType) }); }); if (networkType !== 'none') { const res = await getVisitAreaTree(); if (res && res.code === 200 && Array.isArray(res.data)) { await saveOfflineConfig('visit_area_tree', res.data); } } } catch (err) { console.error('[Sync] 每日同步访问区域树失败:', err); } };