1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
/**
 * 门禁通行服务模块
 * 处理门禁通行相关的业务逻辑,包括人脸/密码白名单校验、权限判断、通行记录保存等
 */
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