/** * 门禁通行服务模块 * 处理门禁通行相关的业务逻辑,包括人脸/密码白名单校验、权限判断、通行记录保存等 */ import logger from "../../dxmodules/dxLogger.js" import std from "../../dxmodules/dxStd.js" import config from "../../dxmodules/dxConfig.js" import common from "../../dxmodules/dxCommon.js" import map from '../../dxmodules/dxMap.js' import bus from '../../dxmodules/dxEventBus.js' import driver from "../driver.js" import mqttService from "./mqttService.js" import sqliteService from "./sqliteService.js" import utils from '../common/utils/utils.js' import http from "../../dxmodules/dxHttp.js" import grainService from './grainService.js' const accessService = {} /** * 将十进制数转换为小端序十六进制字符串 * @param {number} decimalNumber - 十进制数 * @param {number} byteSize - 字节大小 * @returns {string} 小端序十六进制字符串 */ function decimalToLittleEndianHex(decimalNumber, byteSize) { const littleEndianBytes = []; for (let i = 0; i < byteSize; i++) { littleEndianBytes.push(decimalNumber & 0xFF); decimalNumber >>= 8; // 相当于除以256 } const littleEndianHex = littleEndianBytes .map((byte) => byte.toString(16).padStart(2, '0')) .join(''); return littleEndianHex; } /** * 将数据包转换为字符串格式 * @param {object} pack - 数据包对象 * @returns {string} 转换后的字符串 */ function pack2str(pack) { pack.data = (!pack.data) ? [] : pack.data.match(/.{2}/g) let len = decimalToLittleEndianHex(pack.data.length, 2) let str = "55aa" + pack.cmd + pack.result + len + pack.data.join('') let crc = common.calculateBcc([0x55, 0xaa, parseInt(pack.cmd, 16), parseInt(pack.result, 16), pack.data.length % 256, pack.data.length / 256].concat(pack.data.map(v => parseInt(v, 16)))) return str + crc.toString(16).padStart(2, '0') } /** * 人脸/密码白名单校验 * @param {object} data - 通行数据,包含type(码制)和code(码内容) * @param {string} fileName - 通行图片文件路径 * @param {boolean|undefined} similarity - 相似度验证结果,false表示验证失败 * @returns {number|string|boolean} -1(参数错误),0(通行成功),1(在线验证),string(校验失败的原因),false(通行加锁) */ accessService.access = function (data, fileName, similarity) { // 通行加锁,防止重复验证 let lockMap = map.get("access_lock") if (lockMap.get("access_lock")) { logger.error("[access]: 通行加锁,请稍后再试") return false } lockMap.put("access_lock", true) try { // 记录通行时间戳 data.time = Math.floor(Date.parse(new Date()) / 1000) // 根据code查询凭证 let res if (data.type == "300") { // 人脸类型,使用userId查询 res = sqliteService.d1_voucher.findByUserIdAndType(data.code, data.type) } else { // 其他类型,使用code查询 res = sqliteService.d1_voucher.findByCodeAndType(data.code, data.type) } // 权限认证结果 let ret = true // 是否是陌生人 let isStranger = false if (similarity === false) { // 如果相似度验证失败,则不进行认证 ret = false isStranger = true } else { // 验证凭证是否存在 if (res.length == 0) { logger.error("[access]: 通行失败,没查询到凭证!") ret = false isStranger = true } else { // 凭证存在,获取用户信息 data.userId = res[0].userId data.keyId = res[0].id // 根据userId查询人员信息 res = sqliteService.d1_person.findByUserId(data.userId) if (res.length == 0) { logger.error("[access]: 通行失败,没查询到人员!") ret = false isStranger = true } else { // 获取人员姓名、身份证号和身份类型 let idCard let userType = 0 try { idCard = JSON.parse(res[0].extra).idCard userType = JSON.parse(res[0].extra).type || 0 } catch (error) { logger.error("无身份证号或类型") } data.extra = { name: res[0].name, idCard: idCard, type: userType } } // 处理双人认证信息 if (data.dualAuthInfo) { logger.info("[access]: 处理双人认证信息: " + JSON.stringify(data.dualAuthInfo)) // 存储第一用户信息 let firstUserId = data.dualAuthInfo.firstUserId // 查询第一用户的详细信息 let res1 = sqliteService.d1_person.findByUserId(firstUserId) if (res1.length > 0) { // 获取第一用户的姓名、身份证号和身份类型 let idCard1 let firstUserType = 0 try { idCard1 = JSON.parse(res1[0].extra).idCard firstUserType = JSON.parse(res1[0].extra).type || 0 } catch (error) { logger.error("无第一用户身份证号或类型") } data.userId = firstUserId data.extra = { name: res1[0].name, idCard: idCard1, type: firstUserType } } else { // 如果没有查询到第一用户信息,使用默认值 data.userId = firstUserId data.extra = { name: data.dualAuthInfo.firstUserName, idCard: "", type: 0 } } // 存储第二用户信息 data.userId2 = data.dualAuthInfo.secondUserId // 查询第二用户的详细信息 let res2 = sqliteService.d1_person.findByUserId(data.dualAuthInfo.secondUserId) if (res2.length > 0) { // 获取第二用户的姓名和身份证号 let idCard2 let secondUserType = 0 try { idCard2 = JSON.parse(res2[0].extra).idCard secondUserType = JSON.parse(res2[0].extra).type || 0 } catch (error) { logger.error("无第二用户身份证号或类型") } data.extra2 = { name: res2[0].name, idCard: idCard2 } // 存储第二用户的权限ID(身份类型) data.permissionId2 = secondUserType.toString() } else { // 如果没有查询到第二用户信息,使用默认值 data.extra2 = { name: data.dualAuthInfo.secondUserName, idCard: "" } data.permissionId2 = "" } // 处理第一用户的人脸图片 if (data.firstUserFileName) { let firstUserSrc = `/app/data/passRecord/${firstUserId}_${data.time}_1.jpg` std.ensurePathExists(firstUserSrc) // 确保目录存在 if (std.exist(data.firstUserFileName)) { // 移动图片到指定目录 common.systemBrief(`mv ${data.firstUserFileName} ${firstUserSrc}`) // 更新data中的code为第一用户的图片路径 data.code = firstUserSrc } } // 处理第二用户的人脸图片 if (fileName) { let secondUserSrc = `/app/data/passRecord/${data.userId2}_${data.time}_2.jpg` std.ensurePathExists(secondUserSrc) // 确保目录存在 if (std.exist(fileName)) { // 移动图片到指定目录 common.systemBrief(`mv ${fileName} ${secondUserSrc}`) // 更新data中的code2为第二用户的图片路径 data.code2 = secondUserSrc } } } } // 验证权限 if (ret) { // 根据userId查询权限 res = sqliteService.d1_permission.findByUserId(data.userId) if (res && res.length > 0 && judgmentPermission(res)) { logger.info("[access]: 权限认证通过") ret = true // 存储用户身份类型作为权限ID(本系统中身份即权限) let userType = 0 try { if (data.extra) { userType = data.extra.type || 0 } else { // 从数据库查询用户信息获取身份类型 let userRes = sqliteService.d1_person.findByUserId(data.userId) if (userRes && userRes.length > 0) { userType = JSON.parse(userRes[0].extra).type || 0 } } } catch (error) { logger.error("解析用户类型失败") } data.permissionId = userType.toString() // 暂停人脸认证功能 driver.face.status(false) logger.info("[access]: 暂停人脸认证功能") } else { logger.info("[access]: 无权限") ret = false } } // 无权限时,尝试在线验证 if (!ret && config.get('mqtt.onlinecheck') == 1 && driver.mqtt.getStatus()) { logger.info("[access]: 无权限,走在线验证") let serialNo = std.genRandomStr(10) driver.mqtt.send("access_device/v2/event/access_online", JSON.stringify(mqttService.mqttReply(serialNo, data, mqttService.CODE.S_000))) driver.alsa.play(`/app/code/resource/${config.get("base.language") == "CN" ? "CN" : "EN"}/wav/verify.wav`) // 等待在线验证结果 let payload = driver.mqtt.getOnlinecheck() if (payload && payload.serialNo == serialNo && payload.code == '000000') { ret = true // 暂停人脸认证功能 driver.face.status(false) logger.info("[access]: 暂停人脸认证功能") } else { logger.info("[access]: 在线验证失败") ret = false } } } // 确定语音文件名称 let alsaFile = (data.type).toString().startsWith("10") ? '10x' : data.type //暂停人脸认证功能 if (ret == true) { // 验证气体浓度 grainService.checkGasConcentration(function() { // 从存储的气体数据中获取验证结果 const gasData = grainService.getGasData() if(gasData && gasData.data && gasData.data.status === "0") { logger.info("[access]: 气体浓度验证合格") // 通行成功处理 driver.screen.accessSuccess() logger.info("[access]: 通行成功") // 显示通行成功结果 bus.fire("showAccessResult", { faceAuth: true, gasConcentration: true, accessAllowed: true, message: "*仓内气体浓度合格,允许通行*" }) // 触发通行成功事件,通知UI更新 bus.fire("accessSuccess", { data: data, fileName: fileName }) driver.alsa.play(`/app/code/resource/${config.get("base.language") == "CN" ? "CN" : "EN"}/wav/access_s.wav`) driver.gpio.open() // 开门 savePassPic(data, fileName) // 保存通行图片 reply(data, true) // 上报通行记录 } else { logger.info("[access]: 气体浓度验证不合格") // 通行失败处理 driver.screen.accessFail() logger.error("[access]: 通行失败") // 触发失败弹窗 bus.fire("showAccessResult", { faceAuth: true, gasConcentration: false, accessAllowed: false, message: "*仓内气体浓度不合格,禁止通行*" }) if (utils.isEmpty(similarity)) { driver.alsa.play(`/app/code/resource/${config.get("base.language") == "CN" ? "CN" : "EN"}/wav/access_f.wav`) } // if (isStranger && !config.get("sys.strangerImage")) { // // 陌生人不保存照片 // } else { // savePassPic(data, fileName) // } savePassPic(data, fileName) // 添加气体浓度失败信息 data.message = "气体浓度不合格" reply(data, false) // 上报通行记录 } }) } else { // 通行失败处理 driver.screen.accessFail() logger.error("[access]: 通行失败") // 触发失败弹窗 bus.fire("showAccessResult", { faceAuth: true, gasConcentration: false, accessAllowed: false, message: "*权限认证失败,禁止通行*" }) if (utils.isEmpty(similarity)) { driver.alsa.play(`/app/code/resource/${config.get("base.language") == "CN" ? "CN" : "EN"}/wav/recg_f.wav`) } // if (isStranger && !config.get("sys.strangerImage")) { // // 陌生人不保存照片 // } else { // savePassPic(data, fileName) // } savePassPic(data, fileName) reply(data, false) // 上报通行记录 } } catch (error) { logger.error(error) } // 语音播报需要时间,所以延迟1秒解锁 std.sleep(1000) lockMap.put("access_lock", false) logger.error("[access]: 解锁成功") // 触发通行解锁完成事件,通知UI重置 // bus.fire("accessUnlockComplete") } /** * 保存通行图片 * @param {object} data - 通行数据 * @param {string} fileName - 图片文件路径 */ function savePassPic(data, fileName) { if (data.type == "300") { // 仅处理人脸类型 // 如果是双人认证,已经在处理双人认证信息时保存了图片,这里跳过 if (data.dualAuthInfo) { return } let src = `/app/data/passRecord/${data.userId}_${data.time}.jpg` std.ensurePathExists(src) // 确保目录存在 if (std.exist(fileName)) { // 移动图片到指定目录 common.systemBrief(`mv ${fileName} ${src}`) // 只清理原始临时图片文件(以时间戳命名的文件),保留调整后的图片文件 common.systemBrief(`rm -rf /app/data/user/temp/[0-9]*.jpg`) // 更新data中的code为图片路径 data.code = src } else { logger.error("[access]: 通行失败,图片不存在!!!!!!!!!!!!!!!" + fileName) } } } /** * 校验权限时间是否可以通行 * @param {array} permissions - 权限记录数组 * @returns {boolean} true表示有权限,false表示无权限 */ function judgmentPermission(permissions) { let currentTime = Math.floor(Date.now() / 1000) for (let permission of permissions) { if (permission.timeType == 0) { // 永久权限 return true } else if (permission.beginTime <= currentTime && currentTime <= permission.endTime) { if (permission.timeType == 1) { // 时间段权限 return true } if (permission.timeType == 2) { // 每日权限 let seconds = Math.floor((new Date() - new Date().setHours(0, 0, 0, 0)) / 1000); if (permission.repeatBeginTime <= seconds && seconds <= permission.repeatEndTime) { return true } } if (permission.timeType == 3 && permission.period) { // 周重复权限 let dayTimes = JSON.parse(permission.period)[new Date().getDay() + 1] if (dayTimes && dayTimes.split("|").some((dayTime) => isCurrentTimeInTimeRange(dayTime))) { return true } } } } return false } /** * 判断当前时间是否在时间段内 * @param {string} time - 时间段字符串,格式如"15:00-19:00" * @returns {boolean} true表示当前时间在时间段内,false表示不在 */ function isCurrentTimeInTimeRange(time) { // 分割开始时间和结束时间 let [startTime, endTime] = time.split('-'); // 解析开始时间的小时和分钟 let [startHour, startMinute] = startTime.split(':'); // 解析结束时间的小时和分钟 let [endHour, endMinute] = endTime.split(':'); // 获取当前时间 let currentTime = new Date(); // 创建开始时间的日期对象 let startDate = new Date(); startDate.setHours(parseInt(startHour, 10)); startDate.setMinutes(parseInt(startMinute, 10)); // 创建结束时间的日期对象 let endDate = new Date(); endDate.setHours(parseInt(endHour, 10)); endDate.setMinutes(parseInt(endMinute, 10)); // 检查当前时间是否在时间范围内 return currentTime >= startDate && currentTime <= endDate; } /** * 通行记录上报 * @param {object} data - 通行数据 * @param {boolean} result - 通行结果,true表示成功,false表示失败 */ function reply(data, result) { // 使用线程处理整个reply函数,避免堵塞主进程 std.setTimeout(() => { try { // 构建通行记录 let record = { id: std.genRandomStr(16), result: result ? 0 : 1, // 0表示成功,1表示失败 } // 复制data中的字段,排除extra和extra2 for (const key in data) { if (key !== 'extra' && key !== 'extra2' && !(key in record)) { record[key] = data[key] } } // 处理extra字段 if (data.extra) { record.extra = JSON.stringify(data.extra) } // 处理extra2字段 if (data.extra2) { record.extra2 = JSON.stringify(data.extra2) } // 存储通行记录,判断上限 let count = sqliteService.d1_pass_record.count() let configNum = config.get("access.offlineAccessNum"); configNum = configNum ? configNum : 2000; if (configNum > 0) { if (count >= configNum) { // 达到最大存储数量,删除最旧的记录 let lastRecord = sqliteService.d1_pass_record.findAllOrderBytimeDesc({ page: 0, size: 1 }) if (lastRecord && lastRecord.length == 1) { // 如果是人脸记录,删除对应的图片文件 if (lastRecord[0].type == 300) { common.systemBrief(`rm -rf ${lastRecord[0].code}`) } sqliteService.d1_pass_record.deleteByid(lastRecord[0].id) } } // 保存新记录 sqliteService.d1_pass_record.save(record) } // // 生成序列号 // let serialNo = std.genRandomStr(10) // // 处理人脸图片 // if (record.type == 300) { // let m = map.get("faceAccesss") // m.del(serialNo) // m.put(serialNo, record.code ? record.code : "") // // 将图片转换为base64格式 // record.code = driver.face.fileToBase64(record.code) // } // // 构建MQTT消息并发送 // let payload = mqttService.mqttReply(serialNo, [record], mqttService.CODE.S_000) // driver.mqtt.send("access_device/v2/event/access", JSON.stringify(payload)) } catch (error) { logger.error(`[accessService]: 处理通行记录失败: ${error.message}`) } }, 0) } export default accessService