| api/employee.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| components/admin/AdminConfigPanel.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| pages/admin-home/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| pages/appointment-scan/n-index.nvue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| utils/syncEngine.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
api/employee.js
@@ -11,9 +11,13 @@ }); }; const saveEmployeeVerifyListCache = async (employeeList = []) => { const EMPLOYEE_VERIFY_LIST_LAST_SYNC_KEY = 'employee_verify_list_last_sync'; const SYNC_CACHE_TTL_MS = 2 * 60 * 60 * 1000; // 2 小时内不重复强拉接口 export const saveEmployeeVerifyListCache = async (employeeList = []) => { await saveOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY, employeeList); uni.setStorageSync(EMPLOYEE_VERIFY_LIST_KEY, employeeList); uni.setStorageSync(EMPLOYEE_VERIFY_LIST_LAST_SYNC_KEY, Date.now()); }; const getCurrentNetworkType = async () => { @@ -38,22 +42,9 @@ return; } // #ifdef APP-PLUS if (typeof plus === 'undefined' || !plus.io || !plus.io.resolveLocalFileSystemURL) { resolve(false); return; } plus.io.resolveLocalFileSystemURL(normalizedFilePath, () => { resolve(true); }, () => { resolve(false); }); // #endif // #ifndef APP-PLUS // 考虑到不同安卓机型对 plus.io.resolveLocalFileSystemURL 处理绝对路径(/storage/emulated/0/...)的支持度不一致, // 直接返回 true,将真实性校验交给实际图片加载或人脸比对环节。 resolve(true); // #endif }); }; @@ -146,7 +137,7 @@ }); }; const normalizeEmployeeFaceImage = async (employee = {}, index = 0) => { const normalizeEmployeeFaceImage = async (employee = {}, index = 0, localFaceImageMap = new Map()) => { if (!employee || typeof employee !== 'object') { return employee; } @@ -158,9 +149,25 @@ return normalizedEmployee; } // 1. 计算当前 Base64 的伪哈希特征值(长度 + 头部 + 尾部),以极低性能开销判断图片是否变化 // 结合姓名一起作为特征,只要姓名和图片不变,就不重复转存 const len = rawFaceImage.length; const imageHash = len < 100 ? `${employee.name}_${rawFaceImage}` : `${employee.name}_${len}_${rawFaceImage.substring(0, 50)}_${rawFaceImage.substring(len - 50)}`; // 2. 比对缓存,如果存在且未变化,直接复用本地路径,跳过耗时的文件写入操作 if (localFaceImageMap.has(imageHash)) { const cachedLocalPath = localFaceImageMap.get(imageHash); normalizedEmployee.faceImage = cachedLocalPath; normalizedEmployee._faceImageHash = imageHash; return normalizedEmployee; } try { const localFaceImagePath = await writeBase64ImageToLocal(rawFaceImage, `employee-${index}`); normalizedEmployee.faceImage = localFaceImagePath; normalizedEmployee._faceImageHash = imageHash; return normalizedEmployee; } catch (error) { console.error('员工人脸图片 base64 转本地文件失败:', error); @@ -168,13 +175,13 @@ } }; const normalizeEmployeeListFaceImages = async (employeeList = []) => { const normalizeEmployeeListFaceImages = async (employeeList = [], localFaceImageMap = new Map()) => { const normalizedEmployeeList = []; for (let index = 0; index < employeeList.length; index += 1) { const employee = employeeList[index]; // 串行处理 base64 转本地文件,避免 Promise.all 并发解码大量图片时卡住 UI 线程 const normalizedEmployee = await normalizeEmployeeFaceImage(employee, index); const normalizedEmployee = await normalizeEmployeeFaceImage(employee, index, localFaceImageMap); normalizedEmployeeList.push(normalizedEmployee); // 每处理一张图就主动让出一次线程,降低首页进入登记页时对 live-pusher 的影响 @@ -256,7 +263,24 @@ const res = await getAllEmployeeVerifyAPI(); if (res && res.code === 200) { const employeeList = normalizeEmployeeListResponse(res.data); const normalizedEmployeeList = await normalizeEmployeeListFaceImages(employeeList); // 获取当前本地缓存,用于与接口返回的最新数据进行比对 const offlineEmployeeList = await getOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY) || []; const storageEmployeeList = uni.getStorageSync(EMPLOYEE_VERIFY_LIST_KEY) || []; const localEmployeeList = Array.isArray(offlineEmployeeList) && offlineEmployeeList.length > 0 ? offlineEmployeeList : (Array.isArray(storageEmployeeList) ? storageEmployeeList : []); // 构建人脸哈希映射表(包含已处理过的本地路径) const localFaceImageMap = new Map(); localEmployeeList.forEach((emp) => { // 只有当本地路径是 _doc 开头的有效本地文件时,才放入复用池 if (emp._faceImageHash && emp.faceImage && String(emp.faceImage).includes('_doc/')) { localFaceImageMap.set(emp._faceImageHash, emp.faceImage); } }); const normalizedEmployeeList = await normalizeEmployeeListFaceImages(employeeList, localFaceImageMap); await saveEmployeeVerifyListCache(normalizedEmployeeList); return normalizedEmployeeList; } @@ -285,8 +309,26 @@ }); }; export const syncEmployeeVerifyList = async () => { export const syncEmployeeVerifyList = async (force = false) => { try { // 检查缓存是否过期 const lastSyncTime = uni.getStorageSync(EMPLOYEE_VERIFY_LIST_LAST_SYNC_KEY) || 0; const isCacheExpired = (Date.now() - lastSyncTime) > SYNC_CACHE_TTL_MS; if (!force && !isCacheExpired) { // 缓存未过期,检查本地是否有数据 const offlineEmployeeList = await getOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY) || []; const storageEmployeeList = uni.getStorageSync(EMPLOYEE_VERIFY_LIST_KEY) || []; const localEmployeeList = Array.isArray(offlineEmployeeList) && offlineEmployeeList.length > 0 ? offlineEmployeeList : (Array.isArray(storageEmployeeList) ? storageEmployeeList : []); if (localEmployeeList.length > 0) { console.log('员工缓存未过期,跳过全量接口同步'); return localEmployeeList; } } return await runEmployeeVerifyListSync(); } catch (error) { console.error('同步员工验证列表失败:', error); components/admin/AdminConfigPanel.vue
@@ -187,6 +187,7 @@ import saveIcon from '../../static/admin-pages/ySqSZ.png'; import closeIcon from '../../static/admin-pages/pDMhJ.png'; import logoutIcon from '../../static/admin-pages/uZNeV.png'; import { syncEmployeeVerifyList } from '../../api/employee'; import { deactivateVisitMachine } from '../../api/device'; import { getLatestVersion } from '../../api/visitorRecord'; import { clearRequestServerConfig, getRequestServerConfig, setRequestServerConfig } from '../../utils/request'; @@ -438,6 +439,13 @@ try { isManualSyncing.value = true; // 强制触发员工档案全量同步,绕过缓存有效期 try { await syncEmployeeVerifyList(true); } catch (e) { console.error('强制同步员工数据异常:', e); } const syncResult = await syncAllPendingRecords(); if (syncResult.reason === 'offline') { pages/admin-home/index.vue
@@ -152,7 +152,7 @@ <view class="summary-top"> <view class="summary-title-row"> <view class="summary-dot checkin-dot"></view> <text class="summary-title checkin-title">已签到</text> <text class="summary-title checkin-title">已签离</text> </view> <view class="summary-accent checkin-accent"></view> </view> pages/appointment-scan/n-index.nvue
@@ -1252,17 +1252,6 @@ } }; const refreshEmployeeVerifyListForHostVerify = async () => { try { // 仅在接待人验证发现本地列表为空时兜底刷新一次: // 联网时会自动拉最新员工并补齐本地图片缓存,离线时仍然回退本地缓存。 employeeVerifyList.value = await getLocalEmployeeVerifyList(); hostMatchedEmployee.value = findMatchedHostEmployee(); } catch (error) { console.error('接待人验证前刷新员工验证列表失败:', error); } }; const hours = Array.from({ length: 24 }, (_, index) => String(index).padStart(2, '0')); const minutes = Array.from({ length: 60 }, (_, index) => String(index).padStart(2, '0')); const provinceKeys = ['京', '津', '晋', '冀', '蒙', '辽', '吉', '黑', '沪', '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', '桂', '琼', '渝', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新', '无']; @@ -2119,24 +2108,21 @@ const startHostVerify = async () => { hostMatchedEmployee.value = null; let candidateFaceEmployees = getHostFaceEmployees(); if (candidateFaceEmployees.length === 0) { await refreshEmployeeVerifyListForHostVerify(); candidateFaceEmployees = getHostFaceEmployees(); } if (candidateFaceEmployees.length === 0) { openNoticeDialog( '未获取到员工人脸照片', '当前员工列表中没有可用于比对的人脸照片,请先同步员工档案' ); return; } resetFacePreviewState().then(async () => { try { hostVerifyStatusText.value = '正在开启摄像头...'; await ensureHostCameraContext(); const candidateFaceEmployees = getHostFaceEmployees(); if (candidateFaceEmployees.length === 0) { openNoticeDialog( '未获取到员工人脸照片', '当前员工列表中没有可用于比对的人脸照片,请联系管理员核实' ); return; } hostVerifyStatusText.value = '请将脸部置于框内'; // 只有判到人脸后,才进入正式抓拍和人脸比对 startJudgeFacePolling({ stepName: 'host', @@ -2964,45 +2950,42 @@ // showPageDebugModal('离线记录写入前', offlineRecord); await insertOfflineRecord(offlineRecord); console.log('离线记录保存成功'); // uni.showModal({ // title: '离线保存诊断', // content: `离线记录已存入本地数据库\nID: ${offlineRecord.id}\n类型: ${offlineRecord.type}`, // showCancel: false // }); // 2. 当前这单仍然要实时调用单次接口,但不能顺带触发历史离线数据的批量补发。 // 因此这里只处理当前记录本身;若实时提交失败,保留离线记录等待后续统一同步。 try { const submitPayload = { ...(offlineRecord.payload || {}), }; // 这里将网络提交异步化,不阻塞主流程,保证页面秒进完成页。 // 异步闭包内独立处理异常,即使进入成功页或退出当前页,接口依然在后台调用。 (async () => { try { const submitPayload = { ...(offlineRecord.payload || {}), }; if (offlineRecord.localImgPath) { const uploadRes = await uploadImage(offlineRecord.localImgPath); if (uploadRes && uploadRes.code === 200 && uploadRes.url) { if (offlineRecord.type === 'signout') { submitPayload.signImageUrl = uploadRes.url; } else { submitPayload.faceImageUrl = uploadRes.url; } } else { throw new Error('当前记录图片上传失败'); } } const submitRes = offlineRecord.type === 'signout' ? await idCardSingOut(submitPayload) : await addVisitorRecord(submitPayload); if (submitRes && submitRes.code === 200) { await markRecordSynced(offlineRecord.id); if (offlineRecord.localImgPath) { await deleteLocalImage(offlineRecord.localImgPath); const uploadRes = await uploadImage(offlineRecord.localImgPath); if (uploadRes && uploadRes.code === 200 && uploadRes.url) { if (offlineRecord.type === 'signout') { submitPayload.signImageUrl = uploadRes.url; } else { submitPayload.faceImageUrl = uploadRes.url; } } else { throw new Error('当前记录图片上传失败'); } } const submitRes = offlineRecord.type === 'signout' ? await idCardSingOut(submitPayload) : await addVisitorRecord(submitPayload); if (submitRes && submitRes.code === 200) { await markRecordSynced(offlineRecord.id); if (offlineRecord.localImgPath) { await deleteLocalImage(offlineRecord.localImgPath); } } } catch (submitError) { console.error('当前登记/签离实时后台提交失败,保留离线记录等待后续同步:', submitError); } } catch (submitError) { console.error('当前登记/签离实时提交失败,保留离线记录等待后续同步:', submitError); } })(); return true; } catch (error) { utils/syncEngine.js
@@ -134,15 +134,15 @@ }); 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 }); }); // 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] 开始调用登记接口:', { @@ -152,15 +152,15 @@ }); 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 }); }); // 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] 单条业务接口返回:', {