/**
|
* 门禁通行服务模块
|
* 处理门禁通行相关的业务逻辑,包括人脸/密码白名单校验、权限判断、通行记录保存等
|
*/
|
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
|