import { request } from '../utils/request'; import { saveOfflineConfig, getOfflineConfig } from '../utils/db'; const EMPLOYEE_VERIFY_LIST_KEY = 'employee_verify_list'; const EMPLOYEE_FACE_IMAGE_DIR = 'employee-face-images'; let employeeVerifyListSyncPromise = null; const yieldMainThread = (delay = 0) => { return new Promise((resolve) => { setTimeout(resolve, delay); }); }; 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 () => { return new Promise((resolve) => { uni.getNetworkType({ success: (res) => resolve(res.networkType), fail: () => resolve('unknown'), }); }); }; const isLocalEmployeeFacePath = (value = '') => { const normalizedValue = String(value || '').trim(); return /^(file:\/\/|_doc\/|\/storage\/)/.test(normalizedValue); }; const isLocalFileExists = (filePath = '') => { return new Promise((resolve) => { const normalizedFilePath = String(filePath || '').trim(); if (!normalizedFilePath) { resolve(false); return; } // 考虑到不同安卓机型对 plus.io.resolveLocalFileSystemURL 处理绝对路径(/storage/emulated/0/...)的支持度不一致, // 直接返回 true,将真实性校验交给实际图片加载或人脸比对环节。 resolve(true); }); }; const sanitizeBase64String = (value = '') => { return String(value || '') // 清掉常见的零宽字符、BOM、方向控制字符等隐藏脏数据 .replace(/[\u200B-\u200F\u202A-\u202E\u2060\uFEFF]/g, '') .replace(/\s+/g, '') // 仅保留 Base64 合法字符,避免把 dataURL 头或其他脏字符带入原生解码 .replace(/[^A-Za-z0-9+/=]/g, ''); }; const isBase64Image = (value = '') => { const normalizedValue = String(value || '').trim(); if (!normalizedValue) { return false; } if (/data:image\/[a-zA-Z0-9+.-]+;base64,/i.test(normalizedValue)) { return true; } return sanitizeBase64String(normalizedValue).length > 0; }; const getBase64Payload = (value = '') => { const normalizedValue = String(value || '').trim(); const dataUrlMatch = normalizedValue.match(/data:image\/([a-zA-Z0-9+.-]+);base64,/i); if (dataUrlMatch) { const base64Header = dataUrlMatch[0]; const headerStartIndex = normalizedValue.indexOf(base64Header); return { extension: dataUrlMatch[1] === 'jpeg' ? 'jpg' : dataUrlMatch[1], // 兼容 dataURL 前面混入调试字符、零宽字符、换行等脏数据 base64Data: sanitizeBase64String( normalizedValue.slice(headerStartIndex + base64Header.length) ), }; } return { extension: 'png', base64Data: sanitizeBase64String(normalizedValue), }; }; const writeBase64ImageToLocal = (base64Value = '', fileNamePrefix = 'employee-face') => { return new Promise((resolve, reject) => { // #ifdef H5 resolve(base64Value); // #endif // #ifndef H5 try { const { extension, base64Data } = getBase64Payload(base64Value); const normalizedBase64Data = sanitizeBase64String(base64Data); if (!normalizedBase64Data) { reject(new Error('Base64 数据为空')); return; } if (typeof plus === 'undefined' || !plus.nativeObj || !plus.nativeObj.Bitmap) { reject(new Error('当前环境不支持 plus.nativeObj.Bitmap')); return; } const fileName = `${fileNamePrefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${extension || 'png'}`; const filePath = `_doc/${EMPLOYEE_FACE_IMAGE_DIR}/${fileName}`; const imageMimeType = extension === 'jpg' ? 'jpeg' : (extension || 'png'); const imageStr = `data:image/${imageMimeType};base64,${normalizedBase64Data}`; const bitmap = new plus.nativeObj.Bitmap(`employee_face_${Date.now()}`); bitmap.loadBase64Data(imageStr, () => { bitmap.save(filePath, {}, (result) => { bitmap.clear(); const savedFilePath = plus.io.convertLocalFileSystemURL(result.target || filePath); resolve(savedFilePath); }, (error) => { bitmap.clear(); reject(error); }); }, (error) => { bitmap.clear(); reject(error); }); } catch (error) { reject(error); } // #endif }); }; const normalizeEmployeeFaceImage = async (employee = {}, index = 0, localFaceImageMap = new Map()) => { if (!employee || typeof employee !== 'object') { return employee; } const normalizedEmployee = { ...employee }; const rawFaceImage = String(employee.faceImage || '').trim(); if (!rawFaceImage || !isBase64Image(rawFaceImage)) { 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); return normalizedEmployee; } }; 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, localFaceImageMap); normalizedEmployeeList.push(normalizedEmployee); // 每处理一张图就主动让出一次线程,降低首页进入登记页时对 live-pusher 的影响 await yieldMainThread(0); } return normalizedEmployeeList; }; const normalizeEmployeeListResponse = (responseData) => { if (Array.isArray(responseData)) { return responseData; } if (!responseData || typeof responseData !== 'object') { return []; } const candidateKeys = ['records', 'list', 'rows', 'items', 'data']; for (const key of candidateKeys) { if (Array.isArray(responseData[key])) { return responseData[key]; } } return []; }; const sanitizeEmployeeFaceImageCache = async (employee = {}) => { if (!employee || typeof employee !== 'object') { return { employee, invalidLocalFaceImage: false, }; } const normalizedEmployee = { ...employee }; const normalizedFaceImage = String(employee.faceImage || '').trim(); if (!isLocalEmployeeFacePath(normalizedFaceImage)) { return { employee: normalizedEmployee, invalidLocalFaceImage: false, }; } const isFaceImageFileExists = await isLocalFileExists(normalizedFaceImage); if (isFaceImageFileExists) { return { employee: normalizedEmployee, invalidLocalFaceImage: false, }; } // 本地图片文件被系统清理后,必须立刻清空失效路径,避免离线状态继续拿脏地址做人脸比对。 normalizedEmployee.faceImage = ''; return { employee: normalizedEmployee, invalidLocalFaceImage: true, }; }; const sanitizeEmployeeVerifyListCache = async (employeeList = []) => { const sanitizedEmployeeList = []; let hasInvalidLocalFaceImage = false; for (let index = 0; index < employeeList.length; index += 1) { const sanitizeResult = await sanitizeEmployeeFaceImageCache(employeeList[index]); sanitizedEmployeeList.push(sanitizeResult.employee); hasInvalidLocalFaceImage = hasInvalidLocalFaceImage || sanitizeResult.invalidLocalFaceImage; } return { employeeList: sanitizedEmployeeList, hasInvalidLocalFaceImage, }; }; const performSyncEmployeeVerifyList = async () => { const res = await getAllEmployeeVerifyAPI(); if (res && res.code === 200) { const employeeList = normalizeEmployeeListResponse(res.data); // 获取当前本地缓存,用于与接口返回的最新数据进行比对 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; } throw new Error(res?.msg || '同步员工验证列表失败'); }; const runEmployeeVerifyListSync = async () => { if (employeeVerifyListSyncPromise) { return employeeVerifyListSyncPromise; } employeeVerifyListSyncPromise = performSyncEmployeeVerifyList(); try { return await employeeVerifyListSyncPromise; } finally { employeeVerifyListSyncPromise = null; } }; export const getAllEmployeeVerifyAPI = (data = {}) => { return request({ url: '/api/employee/verify/allEmployee', method: 'POST', data, }); }; 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); return []; } }; export const getEmployeeVerifyListFromLocalCache = async () => { const offlineEmployeeList = await getOfflineConfig(EMPLOYEE_VERIFY_LIST_KEY); const storageEmployeeList = uni.getStorageSync(EMPLOYEE_VERIFY_LIST_KEY); const localEmployeeList = Array.isArray(offlineEmployeeList) ? offlineEmployeeList : (Array.isArray(storageEmployeeList) ? storageEmployeeList : []); if (!Array.isArray(localEmployeeList) || localEmployeeList.length === 0) { return []; } const { employeeList: sanitizedEmployeeList, hasInvalidLocalFaceImage, } = await sanitizeEmployeeVerifyListCache(localEmployeeList); if (!hasInvalidLocalFaceImage) { return sanitizedEmployeeList; } await saveEmployeeVerifyListCache(sanitizedEmployeeList); const networkType = await getCurrentNetworkType(); if (networkType !== 'none') { try { return await runEmployeeVerifyListSync(); } catch (error) { console.error('员工人脸缓存失效后自动重同步失败:', error); } } return sanitizedEmployeeList; }; export const getLocalEmployeeVerifyList = async () => { const networkType = await getCurrentNetworkType(); if (networkType !== 'none') { try { // 只要当前还能联网,每次读取员工列表都优先拉最新数据,并同步覆盖本地缓存。 return await runEmployeeVerifyListSync(); } catch (error) { console.error('联网状态刷新员工验证列表失败,回退本地缓存:', error); } } return getEmployeeVerifyListFromLocalCache(); };