| | |
| | | import { Inject, Post, Body, Headers } from '@midwayjs/core'; |
| | | import { Inject, Post, Get, Body, Headers, Fields, Files } from '@midwayjs/core'; |
| | | import { CoolController, BaseController } from '@cool-midway/core'; |
| | | import { PushOpenService } from '../../service/open'; |
| | | import { WeightUploadDto } from '../../dto/weight.dto'; |
| | | import * as crypto from 'crypto'; |
| | | import { BaseSysParamService } from '../../../base/service/sys/param'; |
| | | |
| | | |
| | | import { Context } from '@midwayjs/koa'; // |
| | | |
| | | /** |
| | | * 开放无需验证的上报数据接口 |
| | |
| | | prefix: '/open/push', |
| | | }) |
| | | export class AppPushOpenController extends BaseController { |
| | | @Inject() |
| | | ctx: Context; |
| | | |
| | | @Inject() |
| | | pushOpenService: PushOpenService; |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | @Post('/yingshiyun', { summary: '萤石云Webhook上报接口' }) |
| | | async saveYingshiyunMessage( |
| | | @Body() body: any |
| | | ) { |
| | | |
| | | const result = await this.pushOpenService.saveYingshiyunMessage(body); |
| | | //return this.ok(result); |
| | | |
| | | return { "messageId": result } |
| | | |
| | | async saveYingshiyunMessage() { |
| | | const req = this.ctx.req; |
| | | const rawBody = await new Promise<string>((resolve, reject) => { |
| | | let data = ''; |
| | | const timer = setTimeout(() => reject(new Error('读取流超时')), 5000); |
| | | req.on('data', chunk => data += chunk); |
| | | req.on('end', () => { |
| | | clearTimeout(timer); |
| | | resolve(data); |
| | | }); |
| | | req.on('error', err => { |
| | | clearTimeout(timer); |
| | | reject(err); |
| | | }); |
| | | }).catch(err => { |
| | | console.error('读取stream异常:', err); |
| | | this.ctx.status = 200; |
| | | this.ctx.body = { messageId: '' }; |
| | | return ''; |
| | | }); |
| | | if (!rawBody) { |
| | | this.ctx.status = 200; |
| | | this.ctx.body = { messageId: '' }; |
| | | return; |
| | | } |
| | | console.log('【萤石云原始报文】:', rawBody.slice(0, 500)); |
| | | let msg: any; |
| | | try { |
| | | msg = JSON.parse(rawBody); |
| | | } catch (e) { |
| | | console.error('JSON解析失败:', e); |
| | | this.ctx.status = 200; |
| | | this.ctx.body = { messageId: '' }; |
| | | return; |
| | | } |
| | | const messageId = msg?.header?.messageId; |
| | | const msgType = msg?.header?.type; // "ys.onoffline" / "ys.alarm" / ... |
| | | console.log('消息类型:', msgType, '| messageId:', messageId); |
| | | if (msgType === 'ys.alarm') { |
| | | console.log('告警类型:', msg.body?.alarmType, '| 描述:', msg.body?.describe); |
| | | } else if (msgType === 'ys.onoffline') { |
| | | console.log('设备上下线:', msg.body?.msgType, '| 设备:', msg.body?.deviceName); |
| | | } |
| | | try { |
| | | await this.pushOpenService.saveYingshiyunMessage(msg); |
| | | } catch (e) { |
| | | console.error('数据处理失败:', e); |
| | | // 即使存库失败,也要回 200 + messageId,避免萤石重试 |
| | | } |
| | | this.ctx.status = 200; |
| | | this.ctx.set('Content-Type', 'application/json'); |
| | | this.ctx.body = { messageId: messageId || '' }; |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 摄像机主动推送告警事件(移动侦测VMD、人体侦测,XML multipart格式) |
| | | * 地址:POST /open/push/cameraEvent |
| | | */ |
| | | @Post('/cameraEvent', { summary: '摄像头主动上报告警事件' }) |
| | | async receiveCameraEvent() { |
| | | const req = this.ctx.req; |
| | | let rawBody = ''; |
| | | |
| | | try { |
| | | rawBody = await new Promise<string>((resolve, reject) => { |
| | | let data = ''; |
| | | const timer = setTimeout(() => reject(new Error('读取流超时')), 5000); |
| | | req.on('data', (chunk) => (data += chunk)); |
| | | req.on('end', () => { |
| | | clearTimeout(timer); |
| | | resolve(data); |
| | | }); |
| | | req.on('error', (err) => { |
| | | clearTimeout(timer); |
| | | reject(err); |
| | | }); |
| | | }); |
| | | } catch (err) { |
| | | console.error('【摄像头事件】读取stream异常:', err); |
| | | this.ctx.status = 200; |
| | | this.ctx.body = { code: -1, msg: 'read error' }; |
| | | return; |
| | | } |
| | | if (!rawBody) { |
| | | this.ctx.status = 200; |
| | | this.ctx.body = { code: -1, msg: 'empty body' }; |
| | | return; |
| | | } |
| | | |
| | | // 截取完整XML文本 |
| | | const xmlReg = /<\?xml[\s\S]*?<\/EventNotificationAlert>/; |
| | | const xmlMatch = rawBody.match(xmlReg); |
| | | if (!xmlMatch) { |
| | | console.warn('【摄像头事件】未匹配到告警XML内容'); |
| | | this.ctx.status = 200; |
| | | this.ctx.body = { code: -1 }; |
| | | return; |
| | | } |
| | | const xmlStr = xmlMatch[0]; |
| | | |
| | | // 通用XML节点提取(支持标签前后换行空格) |
| | | const getVal = (xml: string, tag: string) => { |
| | | const reg = new RegExp(`<${tag}>\\s*([\\s\\S]*?)\\s*<\\/${tag}>`); |
| | | const res = xml.match(reg); |
| | | return res ? res[1].trim() : ''; |
| | | }; |
| | | |
| | | // 组装告警实体 |
| | | const cameraEvent = { |
| | | ipAddress: getVal(xmlStr, 'ipAddress'), |
| | | macAddress: getVal(xmlStr, 'macAddress'), |
| | | portNo: getVal(xmlStr, 'portNo'), |
| | | channelID: getVal(xmlStr, 'channelID'), |
| | | channelName: getVal(xmlStr, 'channelName'), |
| | | dateTime: getVal(xmlStr, 'dateTime'), |
| | | eventType: getVal(xmlStr, 'eventType'), // VMD=移动侦测 |
| | | eventState: getVal(xmlStr, 'eventState'), |
| | | eventDescription: getVal(xmlStr, 'eventDescription'), |
| | | targetType: getVal(xmlStr, 'targetType'), // human 人体 |
| | | targetID: getVal(xmlStr, 'targetID'), |
| | | // 目标坐标框 |
| | | rectX: getVal(xmlStr, 'X'), |
| | | rectY: getVal(xmlStr, 'Y'), |
| | | rectWidth: getVal(xmlStr, 'width'), |
| | | rectHeight: getVal(xmlStr, 'height'), |
| | | }; |
| | | |
| | | try { |
| | | // 调用service处理摄像头告警事件,建议新建service方法 |
| | | const result = await this.pushOpenService.saveCameraAlarmEvent(cameraEvent); |
| | | |
| | | this.ctx.status = 200; |
| | | this.ctx.set('Content-Type', 'application/json'); |
| | | this.ctx.body = { success: true, messageId: result }; |
| | | } catch (serviceErr) { |
| | | console.error('【摄像头事件】业务处理失败:', serviceErr); |
| | | this.ctx.status = 200; |
| | | this.ctx.body = { code: -1, msg: 'handle fail' }; |
| | | } |
| | | } |
| | | } |