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'; //
|
|
/**
|
* 开放无需验证的上报数据接口
|
*/
|
@CoolController({
|
prefix: '/open/push',
|
})
|
export class AppPushOpenController extends BaseController {
|
@Inject()
|
ctx: Context;
|
|
@Inject()
|
pushOpenService: PushOpenService;
|
|
@Inject()
|
baseSysParamService: BaseSysParamService;
|
|
|
@Post('/weight', { summary: '原始称重上报接口' })
|
async uploadWeight(
|
@Headers() headers: any,
|
@Body() data: WeightUploadDto,
|
) {
|
|
const CLIENT_ID = String(await this.baseSysParamService.dataByKey('yaohua.weight.clientid')); //'10001';
|
const SECRET = String(await this.baseSysParamService.dataByKey('yaohua.weight.secret')); //'9001ac9676b8';
|
|
|
const clientId = headers['clientid'];
|
const sign = headers['sign'];
|
|
// 1. clientId 校验
|
if (clientId !== CLIENT_ID) {
|
await this.pushOpenService.writePushAuditLog("weight", { "deviceNo": clientId, "requestContent": data, "errorMessage": "非法 clientId" });
|
return {
|
code: 1001,
|
msg: '非法 clientId',
|
data: null,
|
};
|
}
|
|
// 2. 签名校验
|
const signStr =
|
[
|
clientId,
|
data.deviceNo,
|
data.uploadTime,
|
data.garbageType,
|
data.garbageWeight,
|
SECRET,
|
].join(',');
|
|
const realSign = crypto
|
.createHash('sha256')
|
.update(signStr)
|
.digest('hex')
|
.toLowerCase();
|
|
if (realSign !== sign?.toLowerCase()) {
|
await this.pushOpenService.writePushAuditLog("weight", { "deviceNo": data.deviceNo, "requestContent": data, "errorMessage": "签名验证失败" });
|
|
return {
|
code: 1002,
|
msg: '签名验证失败',
|
data: null,
|
};
|
}
|
try {
|
await this.pushOpenService.reportWeight(data);
|
return {
|
code: 0,
|
msg: '',
|
data: 'success',
|
};
|
} catch (err) {
|
await this.pushOpenService.writePushAuditLog("weight", { "deviceNo": data.deviceNo, "requestContent": data, "errorMessage": "系统异常,写入失败." });
|
|
return {
|
code: 500,
|
msg: '系统异常',
|
data: null,
|
};
|
}
|
}
|
|
@Post('/yingshiyun', { summary: '萤石云Webhook上报接口' })
|
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' };
|
}
|
}
|
}
|