src/modules/push/controller/app/open.ts
@@ -1,11 +1,10 @@
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'; //
/**
 * 开放无需验证的上报数据接口
@@ -14,6 +13,9 @@
  prefix: '/open/push',
})
export class AppPushOpenController extends BaseController {
  @Inject()
  ctx: Context;
  @Inject()
  pushOpenService: PushOpenService;
@@ -88,16 +90,146 @@
    }
  }
  @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' };
    }
  }
}