zjk
2026-09-16 d41fd9347b2ec9a40286a7921f4909ac0b0f4ecc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
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);
  }
};