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);
|
}
|
};
|