zjk
2026-07-23 7835feaeb33f333fd27bfe953cf3549d64baa1d1
feat(hardware): 重构扫码逻辑,新增全局常驻扫码架构

添加HardwareAPI全局扫码管理相关方法,支持全局启动扫码、注册/清除扫描结果处理器
新增全局扫码状态防重复启动逻辑,停止扫码时重置扫描状态标记
调整App.vue启动与显示逻辑,设备激活后自动启动全局扫码
重构首页、预约扫码页面的扫码处理流程,改用全局处理器接收扫码结果
在设备初始化页面添加全局扫码启动逻辑
修改原生扫码调用超时时间从2000ms至1500ms
优化调试弹窗代码,新增员工人脸缓存调试弹窗并注释旧的二维码调试弹窗
5个文件已修改
394 ■■■■■ 已修改文件
App.vue 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/appointment-scan/n-index.nvue 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/device-init/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/home/index.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
utils/visitorDevice.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
App.vue
@@ -5,101 +5,115 @@
import { getDeviceActivationStorage, hasActivatedDevice } from './utils/request';
export default {
    onLaunch() {
        console.log('App Launch');
    onLaunch() {
        console.log('App Launch');
        this.checkDeviceActivation();
        this.checkDeviceActivation();
        HardwareAPI.init().then(() => {
            console.log('硬件插件全局初始化完成');
            this.restoreOfflineActivation();
        }).catch((err) => {
            console.error('硬件插件全局初始化失败:', err);
        });
        HardwareAPI.init().then(async () => {
            console.log('硬件插件全局初始化完成');
            await this.restoreOfflineActivation();
            await this.startGlobalScanIfActivated();
        }).catch((err) => {
            console.error('硬件插件全局初始化失败:', err);
        });
        initDB().then(() => {
            console.log('数据库初始化完毕');
            startSyncQueue();
        }).catch((err) => {
            console.error('数据库初始化失败:', err);
        });
        initDB().then(() => {
            console.log('数据库初始化完毕');
            startSyncQueue();
        }).catch((err) => {
            console.error('数据库初始化失败:', err);
        });
        uni.onNetworkStatusChange((res) => {
            if (res.isConnected && res.networkType !== 'none') {
                console.log('[App] 监听到网络恢复,触发同步队列');
                startSyncQueue();
            }
        });
        uni.onNetworkStatusChange((res) => {
            if (res.isConnected && res.networkType !== 'none') {
                console.log('[App] 监听到网络恢复,触发同步队列');
                startSyncQueue();
            }
        });
        runDailyCleanup();
        runDailyCleanup();
        setInterval(() => {
            startSyncQueue();
        }, 5 * 60 * 1000);
    },
    onShow() {
        console.log('App Show');
        this.checkDeviceActivation();
    },
    onHide() {
        console.log('App Hide');
        // 扫码硬件只在应用退到后台/关闭时统一停止,页面切换过程中不主动 stop
        HardwareAPI.stopScan();
    },
    methods: {
        async restoreOfflineActivation() {
            // 已激活设备在每次 App 启动时都补调一次离线激活,确保原生插件状态恢复
            if (!hasActivatedDevice()) {
                return;
            }
        setInterval(() => {
            startSyncQueue();
        }, 5 * 60 * 1000);
    },
    onShow() {
        console.log('App Show');
        this.checkDeviceActivation();
        this.startGlobalScanIfActivated();
    },
    onHide() {
        console.log('App Hide');
        // 扫码硬件只在应用退到后台/关闭时统一停止,页面切换过程中不主动 stop
        HardwareAPI.stopScan();
    },
    methods: {
        async restoreOfflineActivation() {
            // 已激活设备在每次 App 启动时都补调一次离线激活,确保原生插件状态恢复
            if (!hasActivatedDevice()) {
                return;
            }
            const { deviceFingerprint } = getDeviceActivationStorage();
            if (!deviceFingerprint) {
                return;
            }
            const { deviceFingerprint } = getDeviceActivationStorage();
            if (!deviceFingerprint) {
                return;
            }
            try {
                await HardwareAPI.offlineActivateDevice({
                    code: deviceFingerprint,
                });
                console.log('已激活设备启动时离线激活成功');
            } catch (error) {
                console.error('已激活设备启动时离线激活失败:', error);
            }
        },
        checkDeviceActivation() {
            const pages = getCurrentPages();
            if (pages.length > 0) {
                const currentPage = pages[pages.length - 1];
                if (currentPage.route === 'pages/device-init/index') {
                    return;
                }
            }
            try {
                await HardwareAPI.offlineActivateDevice({
                    code: deviceFingerprint,
                });
                console.log('已激活设备启动时离线激活成功');
            } catch (error) {
                console.error('已激活设备启动时离线激活失败:', error);
            }
        },
        async startGlobalScanIfActivated() {
            if (!hasActivatedDevice()) {
                return;
            }
            // 只有“服务地址 + 设备ID + 设备指纹”都存在时,才认为设备已完成激活
            if (!hasActivatedDevice()) {
                console.log('未检测到设备激活信息,强制跳转激活页');
                setTimeout(() => {
                    uni.reLaunch({
                        url: '/pages/device-init/index',
                    });
                }, 100);
            }
        },
    },
            try {
                await HardwareAPI.startGlobalScan();
                console.log('全局扫码器启动成功');
            } catch (error) {
                console.error('全局扫码器启动失败:', error);
            }
        },
        checkDeviceActivation() {
            const pages = getCurrentPages();
            if (pages.length > 0) {
                const currentPage = pages[pages.length - 1];
                if (currentPage.route === 'pages/device-init/index') {
                    return;
                }
            }
            // 只有“服务地址 + 设备ID + 设备指纹”都存在时,才认为设备已完成激活
            if (!hasActivatedDevice()) {
                console.log('未检测到设备激活信息,强制跳转激活页');
                setTimeout(() => {
                    uni.reLaunch({
                        url: '/pages/device-init/index',
                    });
                }, 100);
            }
        },
    },
};
</script>
<style>
page {
    width: 100%;
    height: 100%;
    font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif;
    background: #f4f7fc;
    width: 100%;
    height: 100%;
    font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif;
    background: #f4f7fc;
}
view,
text {
    box-sizing: border-box;
    box-sizing: border-box;
}
</style>
pages/appointment-scan/n-index.nvue
@@ -787,6 +787,7 @@
});
let noticeDialogTimer = null;
let noticeDialogCloseHandler = null;
let appointmentScanResultHandler = null;
const INACTIVITY_AUTO_BACK_MS = 60 * 1000;
let inactivityAutoBackTimer = null;
@@ -2337,17 +2338,17 @@
  });
};
const showQrcodeParseDebugModal = (scanData, decryptedResult = {}) => {
  return new Promise((resolve) => {
    uni.showModal({
      title: '二维码解析结果',
      content: `原始内容:${scanData}\n\n解密结果:${decryptedResult.plainText || '无'}\n\n解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`,
      showCancel: false,
      success: () => resolve(),
      fail: () => resolve(),
    });
  });
};
// const showQrcodeParseDebugModal = (scanData, decryptedResult = {}) => {
//   return new Promise((resolve) => {
//     uni.showModal({
//       title: '二维码解析结果',
//       content: `原始内容:${scanData}\n\n解密结果:${decryptedResult.plainText || '无'}\n\n解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`,
//       showCancel: false,
//       success: () => resolve(),
//       fail: () => resolve(),
//     });
//   });
// };
const handleAppointmentQrcodeValidation = (payload) => {
  const validationResult = validateAppointmentQrcodePayload(payload);
@@ -2376,6 +2377,49 @@
    ''
  );
  return false;
};
const registerAppointmentScanHandler = () => {
  appointmentScanResultHandler = async (scanData) => {
    // 全局扫码器常驻运行,只有预约登记二维码步骤才消费扫码结果。
    if (currentStepName.value !== 'qr' || mode.value !== 'appointment') {
      return;
    }
    resetInactivityTimer();
    try {
      const decryptedResult = await parseHikvisionQrcodePayload(scanData);
      await showQrcodeParseDebugModal(scanData, decryptedResult);
      // 保存解密后的预约码数据,后续身份证读卡和接口校验都直接复用
      appointmentQrcodePayload.value = decryptedResult.payload;
      saveAppointmentQrPayload(decryptedResult.payload);
      console.log('扫码成功,原始预约码:', scanData);
      console.log('预约码解密结果:', decryptedResult.plainText);
      console.log('预约码解析对象:', decryptedResult.payload);
      if (!handleAppointmentQrcodeValidation(decryptedResult.payload)) {
        return;
      }
      nextStep();
    } catch (error) {
      console.error('预约码解密失败:', error);
      openNoticeDialog(
        '未识别二维码,请联系工作人员',
        ''
      );
    }
  };
  HardwareAPI.setScanResultHandler(appointmentScanResultHandler);
};
const unregisterAppointmentScanHandler = () => {
  HardwareAPI.clearScanResultHandler(appointmentScanResultHandler);
  appointmentScanResultHandler = null;
};
// -------------------- 区域数据加载 --------------------
@@ -2410,6 +2454,7 @@
// ----------------------------------------------------
onMounted(async () => {
  resetInactivityTimer();
  registerAppointmentScanHandler();
  await loadSystemFlowConfig();
  await loadEmployeeVerifyList();
  await loadVisitAreas();
@@ -2511,11 +2556,10 @@
onUnmounted(() => {
  clearInactivityTimer();
  unregisterAppointmentScanHandler();
  dismissNameInput();
  stopScanLineAnimation();
  stopNoticeDialogCountdown();
  // 扫码硬件改为在页面退出时统一关闭,步骤内切换与识别成功跳步都不主动 stop。
  HardwareAPI.stopScan();
  HardwareAPI.stopReadCard();
  HardwareAPI.stopJudgeFace();
  HardwareAPI.stopFaceVerify();
@@ -2579,56 +2623,6 @@
// 按步骤统一拉起对应的硬件动作,避免逻辑散落在多个 watch / onMounted 里
const triggerHardwareByStep = (stepName) => {
  // 只有预约登记第一步才需要扫码
  if (mode.value === 'appointment' && stepName === 'qr') {
    // 二次进入扫码步骤前先复位扫码硬件,避免沿用上一次扫码残留状态。
    HardwareAPI.stopScan();
    HardwareAPI.startScan(async (scanData) => {
      // 不再在步骤切换时主动 stopScan,所以这里只允许二维码步骤消费扫码结果
      if (currentStepName.value !== 'qr' || mode.value !== 'appointment') {
        return;
      }
      resetInactivityTimer();
      try {
        const decryptedResult = await parseHikvisionQrcodePayload(scanData);
        await showQrcodeParseDebugModal(scanData, decryptedResult);
        // 保存解密后的预约码数据,后续身份证读卡和接口校验都直接复用
        appointmentQrcodePayload.value = decryptedResult.payload;
        saveAppointmentQrPayload(decryptedResult.payload);
        console.log('扫码成功,原始预约码:', scanData);
        console.log('预约码解密结果:', decryptedResult.plainText);
        console.log('预约码解析对象:', decryptedResult.payload);
        // await new Promise((resolve) => {
        //   uni.showModal({
        //     title: '二维码解析结果',
        //     content: `解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`,
        //     showCancel: false,
        //     success: () => resolve(),
        //     fail: () => resolve(),
        //   });
        // });
        if (!handleAppointmentQrcodeValidation(decryptedResult.payload)) {
          return;
        }
        nextStep();
      } catch (error) {
        console.error('预约码解密失败:', error);
        openNoticeDialog(
          '未识别二维码,请联系工作人员',
          ''
        );
      }
    });
    return;
  }
  if (stepName === 'face') {
    // 访客进入人脸步骤后自动开始判脸和比对
    startFaceVerify();
pages/device-init/index.vue
@@ -128,6 +128,8 @@
            code: deviceFingerprint.value,
        });
        await HardwareAPI.startGlobalScan();
        uni.setStorageSync('device_id', machineId);
        uni.setStorageSync('device_fingerprint', deviceFingerprint.value);
        uni.hideLoading();
pages/home/index.vue
@@ -170,6 +170,7 @@
let pressTimer = null;
let homeScanSessionId = 0;
let isHandlingHomeScan = false;
let homeScanResultHandler = null;
const handleTouchStart = () => {
  pressTimer = setTimeout(() => {
@@ -228,11 +229,34 @@
  noticeDialog.value.visible = false;
};
const showQrcodeParseDebugModal = (scanData, decryptedResult = {}) => {
// const showQrcodeParseDebugModal = (scanData, decryptedResult = {}) => {
//   return new Promise((resolve) => {
//     uni.showModal({
//       title: '二维码解析结果',
//       content: `原始内容:${scanData}\n\n解密结果:${decryptedResult.plainText || '无'}\n\n解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`,
//       showCancel: false,
//       success: () => resolve(),
//       fail: () => resolve(),
//     });
//   });
// };
const showEmployeeFaceCacheDebugModal = (employeeList = []) => {
  const localFaceEmployees = (Array.isArray(employeeList) ? employeeList : [])
    .filter((employee) => /^(file:\/\/|_doc\/|\/storage\/)/.test(String(employee?.faceImage || '').trim()));
  const previewEmployees = localFaceEmployees.slice(0, 5);
  const previewContent = previewEmployees.length > 0
    ? previewEmployees.map((employee, index) => {
      const employeeName = employee?.employeeName || employee?.name || employee?.userName || `员工${index + 1}`;
      return `${index + 1}. ${employeeName}\n${employee.faceImage}`;
    }).join('\n\n')
    : '本次未发现已转为本地路径的员工人脸图片';
  return new Promise((resolve) => {
    uni.showModal({
      title: '二维码解析结果',
      content: `原始内容:${scanData}\n\n解密结果:${decryptedResult.plainText || '无'}\n\n解析对象:${JSON.stringify(decryptedResult.payload || {}, null, 2)}`,
      title: `员工人脸本地缓存(共 ${localFaceEmployees.length} 条)`,
      content: previewContent,
      showCancel: false,
      success: () => resolve(),
      fail: () => resolve(),
@@ -240,15 +264,12 @@
  });
};
const initHomeScanner = async () => {
const registerHomeScanHandler = () => {
  homeScanSessionId += 1;
  const currentSessionId = homeScanSessionId;
  isHandlingHomeScan = false;
  // 扫码硬件在二次进入首页时需要先复位一次,否则原生层可能残留旧的扫码状态。
  await HardwareAPI.stopScan();
  HardwareAPI.startScan(async (scanData) => {
  homeScanResultHandler = async (scanData) => {
    if (currentSessionId !== homeScanSessionId || isHandlingHomeScan) {
      return;
    }
@@ -292,7 +313,16 @@
        isHandlingHomeScan = false;
      }
    }
  });
  };
  HardwareAPI.setScanResultHandler(homeScanResultHandler);
};
const unregisterHomeScanHandler = () => {
  homeScanSessionId += 1;
  isHandlingHomeScan = false;
  HardwareAPI.clearScanResultHandler(homeScanResultHandler);
  homeScanResultHandler = null;
};
const initHomeData = async () => {
@@ -325,6 +355,7 @@
  // 员工图片 base64 转本地文件比较重,放到首页后台静默处理,避免阻塞后续 live-pusher 拉起
  setTimeout(() => {
    syncEmployeeVerifyList()
      .then((employeeList) => showEmployeeFaceCacheDebugModal(employeeList))
      .catch((error) => {
        console.error('首页后台同步员工验证列表失败:', error);
      });
@@ -336,13 +367,11 @@
});
onShow(() => {
  initHomeScanner();
  registerHomeScanHandler();
});
onHide(() => {
  homeScanSessionId += 1;
  isHandlingHomeScan = false;
  HardwareAPI.stopScan();
  unregisterHomeScanHandler();
});
</script>
utils/visitorDevice.js
@@ -14,6 +14,8 @@
let initResult = null;
const PLUGIN_DEBUG_ENABLED = true;
let debugModalQueue = Promise.resolve();
let globalScanStarted = false;
let currentScanResultHandler = null;
const getPluginMethod = (methodNames = []) => {
  if (!HardwarePlugin) {
@@ -197,6 +199,16 @@
    });
  },
  setScanResultHandler(handler) {
    currentScanResultHandler = typeof handler === 'function' ? handler : null;
  },
  clearScanResultHandler(handler) {
    if (!handler || currentScanResultHandler === handler) {
      currentScanResultHandler = null;
    }
  },
  /**
   * 开始扫描 (二维码)
   * 建议安卓端使用 invokeAndKeepAlive
@@ -210,7 +222,7 @@
    // #ifdef APP-PLUS
    if (HardwarePlugin) {
      HardwarePlugin.startScan({ duration: 2000 }, (res) => {
      HardwarePlugin.startScan({ duration: 1500 }, (res) => {
        // showPluginDebugModal('扫码回调', res);
        if (res) {
          callback(res);
@@ -235,6 +247,7 @@
      // #ifdef APP-PLUS
      if (HardwarePlugin) {
        HardwarePlugin.stopScan({});
        globalScanStarted = false;
        resolve();
      } else {
        resolve();
@@ -243,6 +256,38 @@
      // #ifndef APP-PLUS
      console.log('[Mock] 停止扫码');
      globalScanStarted = false;
      resolve();
      // #endif
    });
  },
  startGlobalScan() {
    if (globalScanStarted) {
      return Promise.resolve();
    }
    return new Promise((resolve) => {
      // #ifdef APP-PLUS
      if (HardwarePlugin) {
        HardwarePlugin.startScan({ duration: 2000 }, (res) => {
          if (!res) {
            console.error('扫码失败或状态更新:', res);
            return;
          }
          if (typeof currentScanResultHandler === 'function') {
            currentScanResultHandler(res);
          }
        });
        globalScanStarted = true;
      }
      resolve();
      // #endif
      // #ifndef APP-PLUS
      console.log('[Mock] 全局开始扫码,等待回调...');
      globalScanStarted = true;
      resolve();
      // #endif
    });