diff --git a/code/VideoAccess-VCMP/api/app/lib/controllers/status/push.js b/code/VideoAccess-VCMP/api/app/lib/controllers/status/push.js index a2b4055..91f5253 100644 --- a/code/VideoAccess-VCMP/api/app/lib/controllers/status/push.js +++ b/code/VideoAccess-VCMP/api/app/lib/controllers/status/push.js @@ -11,20 +11,55 @@ async function edit (ctx) { receiver, cameraId } = ctx.request.body; + let configId_ = configId + let receiverSorted = receiver.sort() + let cameraIdSorted = cameraId.sort() + let noticeWaySorted = noticeWay.sort() + let editDataExist = false // 判重 - + const configAllRes = await models.CameraStatusPushConfig.findAll({ + include: [{ + model: models.CameraStatusPushMonitor, + attributes: ['cameraId'], + required: false, + duplicating: true, + }, { + model: models.CameraStatusPushReceiver, + attributes: ['receiver'], + duplicating: false, + required: false, + },] + }) + for (let c of configAllRes) { + if (configId_ && c.id == configId_) { + editDataExist = true + continue + } else if (c.name == name) { + throw '已有同名称配置信息' + } + const cReceiver = c.cameraStatusPushReceivers.map(r => r.receiver) + const cCameraId = c.cameraStatusPushMonitors.map(m => m.cameraId) + const cReceiverSorted = cReceiver.sort() + const cCameraIdSorted = cCameraId.sort() + if ( + receiverSorted.join(',') == cReceiverSorted.join(',') && cameraIdSorted.join(',') == cCameraIdSorted.join(',') && + c.pushWay == pushWay && + c.noticeWay.sort().join(',') == noticeWaySorted.join(',') && + ( + (!c.timing && !timing) || + c.timing == timing + ) + ) { + throw '已有相同配置信息' + } + } let config = { name, pushWay, noticeWay, timing } if (configId) { - const configRes = await models.CameraStatusPushConfig.findOne({ - where: { - id: configId - } - }) - if (!configRes) { + if (!editDataExist) { throw '参数错误' } else { // DO UPDATE @@ -89,7 +124,7 @@ async function get (ctx) { try { const models = ctx.fs.dc.models; const { userId, token } = ctx.fs.api - const { limit, page, orderBy, orderDirection, keyword, pushWay } = ctx.query + const { limit, page, orderBy, orderDirection, name, pushWay } = ctx.query const sequelize = ctx.fs.dc.ORM; let findOption = { @@ -104,13 +139,16 @@ async function get (ctx) { if (page && limit) { findOption.offset = page * limit } - if (keyword) { + if (name) { findOption.where['$or'] = { name: { - $like: `%${keyword}%` + $like: `%${name}%` }, } } + if (pushWay) { + findOption.where.pushWay = pushWay + } const configLimitRes = await models.CameraStatusPushConfig.findAll(findOption) @@ -131,6 +169,7 @@ async function get (ctx) { 'cameraStatusPushMonitors.id', 'cameraStatusPushLogs.id', 'cameraStatusPushReceivers.id', + 'cameraStatusPushMonitors->camera.id', ] findOption.order = [ [orderBy || 'id', orderDirection || 'DESC'] @@ -141,6 +180,10 @@ async function get (ctx) { attributes: ['cameraId'], required: false, duplicating: true, + include: [{ + model: models.Camera, + attributes: ['name'], + }] }, { model: models.CameraStatusPushLog, diff --git a/code/VideoAccess-VCMP/api/app/lib/index.js b/code/VideoAccess-VCMP/api/app/lib/index.js index b50280c..3b9484e 100644 --- a/code/VideoAccess-VCMP/api/app/lib/index.js +++ b/code/VideoAccess-VCMP/api/app/lib/index.js @@ -46,15 +46,6 @@ module.exports.entry = function (app, router, opts) { module.exports.models = function (dc) { // dc = { orm: Sequelize对象, ORM: Sequelize, models: {} } // 加载定义模型 历史写法 // require('./models/nvr')(dc); - // require('./models/camera_ability')(dc); - // require('./models/camera_kind')(dc); - // require('./models/camera')(dc); - // require('./models/camera_ability_bind')(dc); - // require('./models/vender')(dc); - // require('./models/secret_yingshi')(dc); - // require('./models/gb_camera')(dc); - // require('./models/ax_project')(dc); - // require('./models/camera_remark')(dc); // 模型关系摘出来 初始化之后再定义关系才行 fs.readdirSync(path.join(__dirname, '/models')).forEach((filename) => { @@ -64,7 +55,7 @@ module.exports.models = function (dc) { // dc = { orm: Sequelize对象, ORM: Seq const { Nvr, Camera, CameraAbility, CameraAbilityBind, CameraKind, CameraRemark, GbCamera, SecretYingshi, Vender, CameraStatus, CameraStatusResolve, CameraStatusLog, - CameraStatusPushConfig, CameraStatusPushMonitor, CameraStatusPushLog, CameraStatusPushReceiver, + CameraStatusPushConfig, CameraStatusPushMonitor, CameraStatusPushLog, CameraStatusPushReceiver, CameraStatusOfflineLog } = dc.models; // Nvr.belongsTo(User, { foreignKey: 'userId', targetKey: 'id' }); @@ -105,9 +96,15 @@ module.exports.models = function (dc) { // dc = { orm: Sequelize对象, ORM: Seq CameraStatusPushMonitor.belongsTo(CameraStatusPushConfig, { foreignKey: 'configId', targetKey: 'id' }); CameraStatusPushConfig.hasMany(CameraStatusPushMonitor, { foreignKey: 'configId', sourceKey: 'id' }); + CameraStatusPushMonitor.belongsTo(Camera, { foreignKey: 'cameraId', targetKey: 'id' }); + Camera.hasMany(CameraStatusPushMonitor, { foreignKey: 'cameraId', sourceKey: 'id' }); + CameraStatusPushLog.belongsTo(CameraStatusPushConfig, { foreignKey: 'pushConfigId', targetKey: 'id' }); CameraStatusPushConfig.hasMany(CameraStatusPushLog, { foreignKey: 'pushConfigId', sourceKey: 'id' }); CameraStatusPushReceiver.belongsTo(CameraStatusPushConfig, { foreignKey: 'configId', targetKey: 'id' }); CameraStatusPushConfig.hasMany(CameraStatusPushReceiver, { foreignKey: 'configId', sourceKey: 'id' }); + + CameraStatusOfflineLog.belongsTo(Camera, { foreignKey: 'cameraId', targetKey: 'id' }); + Camera.hasMany(CameraStatusOfflineLog, { foreignKey: 'cameraId', sourceKey: 'id' }); }; diff --git a/code/VideoAccess-VCMP/api/app/lib/models/camera_status_offline_log.js b/code/VideoAccess-VCMP/api/app/lib/models/camera_status_offline_log.js new file mode 100644 index 0000000..5d27b2c --- /dev/null +++ b/code/VideoAccess-VCMP/api/app/lib/models/camera_status_offline_log.js @@ -0,0 +1,52 @@ +/* eslint-disable*/ +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const CameraStatusOfflineLog = sequelize.define("cameraStatusOfflineLog", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: true, + unique: "camera_status_offline_log_id_uindex" + }, + cameraId: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: false, + field: "camera_id", + autoIncrement: false + }, + status: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: false, + field: "status", + autoIncrement: false + }, + time: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: false, + field: "time", + autoIncrement: false + } + }, { + tableName: "camera_status_offline_log", + comment: "", + indexes: [] + }); + dc.models.CameraStatusOfflineLog = CameraStatusOfflineLog; + return CameraStatusOfflineLog; +}; \ No newline at end of file diff --git a/code/VideoAccess-VCMP/api/app/lib/schedule/cameraStatePush.js b/code/VideoAccess-VCMP/api/app/lib/schedule/cameraStatePush.js index 72c389e..252b305 100644 --- a/code/VideoAccess-VCMP/api/app/lib/schedule/cameraStatePush.js +++ b/code/VideoAccess-VCMP/api/app/lib/schedule/cameraStatePush.js @@ -3,16 +3,178 @@ const moment = require('moment') module.exports = function (app, opts) { const cameraOnlinePush = app.fs.scheduleInit( { - // interval: '* * 4 * * *', - interval: '*/15 * * * *', + interval: '* */15 * * * *', + // interval: '*/15 * * * * *', immediate: false, + proRun: false, }, async () => { try { - + const { models } = app.fs.dc + const { pushBySms, pushByEmail } = app.fs.utils + const configRes = await models.CameraStatusPushConfig.findAll({ + where: { + forbidden: false + }, + include: [ + { + model: models.CameraStatusPushMonitor, + attributes: ['cameraId'], + required: false, + duplicating: true + }, + { + model: models.CameraStatusPushReceiver, + attributes: ['receiver'], + duplicating: false, + required: false, + }, + ], + }) + + const timeNow = moment().format() + for (let c of configRes) { + // 查配置信息所对应的摄像头15min内的在离线状态 + const cameraIds = c.cameraStatusPushMonitors.map(m => m.cameraId) + const offlineStatusRes = await models.CameraStatusOfflineLog.findAll({ + where: { + cameraId: { $in: cameraIds }, + time: { + $between: [moment(timeNow).subtract(15, 'minutes').format(), timeNow] + } + }, + include: [{ + model: models.Camera, + attributes: ['name'] + }], + order: [['time', 'ASC']], + }) + + + + if (offlineStatusRes.length) { + const cameraStatusMap = {} + // 当前逻辑 + // 只要最后状态是离线 就做离线推送 + // 只要出现一次上线 就做上线推送 + for (let s of offlineStatusRes) { + if (cameraStatusMap[s.cameraId]) { + cameraStatusMap.status.push({ + cameraId: s.cameraId, + status: s.status, + time: s.time, + }) + } else { + cameraStatusMap[s.cameraId] = { + status: [{ + cameraId: s.cameraId, + status: s.status, + time: s.time, + }], + name: s.camera.name, + } + } + } + let offArr = [] + let onArr = [] + for (let k in Object.keys(cameraStatusMap).sort()) { + const data = cameraStatusMap[k] + if (data.status[0].status == 'OFF') { + offArr.push({ + name: data.name, + time: data.status[0].time, + }) + } + const onLineIndex = data.status.findIndex(s => s.status == 'ON') + if (onLineIndex >= 0) { + const onLineLastOffIndex = data.status.findIndex((s, i) => s.status == 'OFF' && i > onLineIndex) + let onlineData = { + cameraId: data.status[onLineIndex].cameraId, + name: data.status[onLineIndex].name, + time: data.status[onLineIndex].time, + } + if (onLineLastOffIndex >= 0) { + onlineData.offTime = data.status[onLineLastOffIndex].time + } + onArr.push(onlineData) + } + } + + // 查当前配置由谁配置 + const corUser = await app.fs.authRequest.get(`user/${c.createUser}/message`, { + query: { + // TODO 这里拿不到 token 去鉴权 怎么能系统间鉴权呢 + // 暂时取消了鉴权系统对这里的判断 + token: '' + } + }) + + const receiver = c.cameraStatusPushReceivers.map(r => r.receiver) + // 离线推送 + if (offArr.length && c.noticeWay && c.noticeWay.includes('offline')) { + if (c.pushWay == 'email') { + // 邮件 + let text = `【${corUser[0].namePresent}】账号下的设备,截止【${moment(timeNow).format('YYYY年HH时mm分')}】,有${offArr.length}个设备掉线:\n` + text += offArr.map(o => `【${o.name}】于【${o.time}】掉线,`).join('\n') + text += `\n请及时处理!` + await pushByEmail({ + email: receiver, + title: '', + text, + }) + } else if (c.pushWay == 'phone') { + // 短信 + let text = `【${corUser[0].namePresent}】账号下的` + if (offArr.length == 1) { + text += `【${offArr[0].name}】离线,请及时处理!【${moment(offArr[0].time).format('YYYY年HH时mm分')}】` + } else { + text += offArr.map(o => `【${o.name}】`).join('') + if (text.length > 35) { + text = text.substring(0, 35) + '...' + text += `等${offArr.length}个摄像头离线` + } + text += `,请及时处理!【${moment().format('MM月DD日HH时mm分')}-${moment(timeNow).subtract(15, 'minutes').format('MM月DD日HH时mm分')}】` + } + await pushBySms({ + phone: receiver, + templateCode: '', + templateParam: '', + text + }) + } + } + + // 上线推送 + if (onArr.length && c.noticeWay && c.noticeWay.includes('online')) { + if (c.pushWay == 'email') { + // 邮件 + // const outTimeCameraOff = onArr.filter(a => !a.offTime) + // if (outTimeCameraOff.length) { + // let cameraIds = outTimeCameraOff.map(a => a.cameraId) + // const lastOfflineRes = await models.CameraStatus.findAll({ + // where: { + // cameraId: { $in: cameraIds }, + // status: 'OFF', + // time: { + // $lt: moment(timeNow).subtract(15, 'minutes').format() + // } + // }, + // group: ['cameraId'], + // attributes: ['cameraId', [sequelize.fn('max', sequelize.col('time')), 'time']], + // }) + + // } + + } else if (c.pushWay == 'phone') { + // 短信 + } + } + } + } + console.log('cameraOnlinePush') } catch (error) { - + } } ) diff --git a/code/VideoAccess-VCMP/api/app/lib/schedule/freshYingshiMsg.js b/code/VideoAccess-VCMP/api/app/lib/schedule/freshYingshiMsg.js index b6dd362..941015e 100644 --- a/code/VideoAccess-VCMP/api/app/lib/schedule/freshYingshiMsg.js +++ b/code/VideoAccess-VCMP/api/app/lib/schedule/freshYingshiMsg.js @@ -4,9 +4,10 @@ const moment = require('moment') module.exports = function (app, opts) { const freshYingshiState = app.fs.scheduleInit( { - // interval: '* * 4 * * *', - interval: '*/10 * * * *', + interval: '* */5 * * * *', + // interval: '*/10 * * * *', immediate: false, + proRun: true, }, async () => { @@ -49,22 +50,39 @@ module.exports = function (app, opts) { if (existD.online != storageD.online) { // 状态更新 if (storageD.online == 'ON' && !existD.playUrl) { + // 播放地址更新 const playUrlRes = await getYingshiPlayUrl({ deviceSerial: d.deviceSerial, token: tokenYingshi }) storageD.playUrl = playUrlRes } + // 在离线状态更新 await models.GbCamera.update(storageD, { where: { id: existD.id } }) - // 状态更新 END - - // 状态推送 + // 状态向前端页面推送 cameraStatePush({ gbId: existD.id, online: storageD.online, ipctype: storageD.ipctype, }) + // 记录日志 + const cameraRes = await models.Camera.findAll({ + where: { + gbId: existD.id + } + }) + const nowTime = moment().format() + let bulkCreateData = cameraRes.map(c => { + return { + cameraId: c.id, + status: storageD.online, + time: nowTime + } + }) + if (bulkCreateData.length) { + await models.CameraStatusOfflineLog.bulkCreate(bulkCreateData) + } } } else { const yingshiRes = await models.GbCamera.create(storageD) @@ -99,6 +117,7 @@ module.exports = function (app, opts) { // interval: '*/30 * * * *', interval: '0 34 5 1 * *', immediate: true, + proRun: true, }, async () => { try { diff --git a/code/VideoAccess-VCMP/api/app/lib/schedule/index.js b/code/VideoAccess-VCMP/api/app/lib/schedule/index.js index d28db8c..e8f4137 100644 --- a/code/VideoAccess-VCMP/api/app/lib/schedule/index.js +++ b/code/VideoAccess-VCMP/api/app/lib/schedule/index.js @@ -7,10 +7,10 @@ const nodeSchedule = require('node-schedule'); module.exports = async function (app, opts) { const scheduleInit = ({ - interval, immediate + interval, immediate, proRun, }, callback) => { const j = nodeSchedule.scheduleJob(interval, callback); - if (immediate && !opts.dev) { + if (immediate && (!proRun || (proRun && !opts.dev))) { setTimeout(callback, 0) } return j; diff --git a/code/VideoAccess-VCMP/api/app/lib/utils/push.js b/code/VideoAccess-VCMP/api/app/lib/utils/push.js index a46dc9e..cb2e9b5 100644 --- a/code/VideoAccess-VCMP/api/app/lib/utils/push.js +++ b/code/VideoAccess-VCMP/api/app/lib/utils/push.js @@ -5,7 +5,7 @@ const Core = require('@alicloud/pop-core'); const nodemailer = require('nodemailer') module.exports = function (app, opts) { - const pushBySms = async ({ phone = [] } = {}) => { + const pushBySms = async ({ phone = [], templateCode, templateParam } = {}) => { try { if (phone.length) { const client = new Core({ @@ -29,7 +29,7 @@ module.exports = function (app, opts) { } } - const pushByEmail = async () => { + const pushByEmail = async ({ email = [], title, text = '', html = '', attachments = undefined } = {}) => { try { let transporter = nodemailer.createTransport({ host: opts.email.host, @@ -44,7 +44,7 @@ module.exports = function (app, opts) { // send mail with defined transport object await transporter.sendMail({ from: 'grazing-stars@qq.com', // sender address - to: email, // list of receivers 逗号分隔字符串 + to: email.join(','), // list of receivers 逗号分隔字符串 subject: title, // Subject line text: text, // plain text body html: html, // html body diff --git a/code/VideoAccess-VCMP/api/sequelize-automate.config.js b/code/VideoAccess-VCMP/api/sequelize-automate.config.js index efc91da..aaf2887 100644 --- a/code/VideoAccess-VCMP/api/sequelize-automate.config.js +++ b/code/VideoAccess-VCMP/api/sequelize-automate.config.js @@ -26,7 +26,7 @@ module.exports = { dir: './app/lib/models', // 指定输出 models 文件的目录 typesDir: 'models', // 指定输出 TypeScript 类型定义的文件目录,只有 TypeScript / Midway 等会有类型定义 emptyDir: false, // !!! 谨慎操作 生成 models 之前是否清空 `dir` 以及 `typesDir` - tables: ['camera_status_push_log'], // 指定生成哪些表的 models,如 ['user', 'user_post'];如果为 null,则忽略改属性 + tables: ['camera_status_offline_log'], // 指定生成哪些表的 models,如 ['user', 'user_post'];如果为 null,则忽略改属性 skipTables: [], // 指定跳过哪些表的 models,如 ['user'];如果为 null,则忽略改属性 tsNoCheck: false, // 是否添加 `@ts-nocheck` 注释到 models 文件中 ignorePrefix: [], // 生成的模型名称忽略的前缀,因为 项目中有以下表名是以 t_ 开头的,在实际模型中不需要, 可以添加多个 [ 't_data_', 't_',] ,长度较长的 前缀放前面