From e8cd12bb75d97395163fba26200cc62db311bde4 Mon Sep 17 00:00:00 2001 From: "gao.zhiyuan" Date: Fri, 16 Jun 2023 14:36:02 +0800 Subject: [PATCH] =?UTF-8?q?fix=20=E5=BE=85=E5=8A=9E=E6=89=93=E4=B8=8D?= =?UTF-8?q?=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/.vscode/launch.json | 2 +- api/app/lib/schedule/index.js | 5 +- api/app/lib/schedule/workorder_statistics.js | 63 +++++++++++ api/app/lib/utils/parseProcessData.js | 106 ++++++++++++++++++ api/config.js | 9 +- .../analysis/containers/workorderData.jsx | 99 +++++++++++++++- web/client/src/sections/analysis/routes.js | 49 ++++---- 7 files changed, 301 insertions(+), 32 deletions(-) create mode 100644 api/app/lib/schedule/workorder_statistics.js create mode 100644 api/app/lib/utils/parseProcessData.js diff --git a/api/.vscode/launch.json b/api/.vscode/launch.json index 5d1120d..5b5aebc 100644 --- a/api/.vscode/launch.json +++ b/api/.vscode/launch.json @@ -26,7 +26,6 @@ "--redisPort 6379", "--apMergeDeVeAnxinProjectId 1,2,3", "--axyApiUrl http://127.0.0.1:4100", - "--apiEmisUrl http://10.8.30.112:14000", // 测试 // "--apiEmisUrl http://10.8.30.161:1111", // "--apiEmisUrl http://10.8.30.161:31111/", @@ -71,6 +70,7 @@ "--clickHouseVcmp video_access_dev", "--clickHouseDataAlarm default", "--clickHouseIot iota", + "--clickHouseCamworkflow camworkflow", "--confirmAlarmAnxinUserId 1", "--vcmpAppId 5048b08d-c449-4d7f-b1ec-f741012aefe8", "--vcmpAppSecret 5ba8c0ab-9fbd-4f07-9817-c48017c3cbad", diff --git a/api/app/lib/schedule/index.js b/api/app/lib/schedule/index.js index 121b8a9..b976d6d 100644 --- a/api/app/lib/schedule/index.js +++ b/api/app/lib/schedule/index.js @@ -7,8 +7,11 @@ const nodeSchedule = require('node-schedule'); module.exports = async function (app, opts) { const scheduleInit = ({ - interval, immediate, proRun, + interval, immediate, proRun, disabled }, callback) => { + if (disabled) { + return; + } if (proRun && opts.dev) { return; } diff --git a/api/app/lib/schedule/workorder_statistics.js b/api/app/lib/schedule/workorder_statistics.js new file mode 100644 index 0000000..50e27fd --- /dev/null +++ b/api/app/lib/schedule/workorder_statistics.js @@ -0,0 +1,63 @@ +const moment = require('moment') + +let isDev = false +// let isDev = true + +module.exports = function (app, opts) { + const workorderStatistics = app.fs.scheduleInit( + { + interval: '24 */1 * * * *', + immediate: isDev, + proRun: !isDev, + disabled: true + }, + async () => { + try { + const { models, ORM: sequelize } = app.fs.dc + const { apMergeDeVeAnxinProjectId = '' } = opts + const { clickHouse } = app.fs + const { database: camWorkflow } = clickHouse.camWorkflow.opts.config + const { parseProcessData } = app.fs.utils + + const attendanceRes = await clickHouse.pepEmis.query( + ` + SELECT + story.id AS historyId, + story.apply_user AS pepUserId, + story.form_data AS formData, + story.submit_form_data AS submitFormData, + fform.form_schema AS formSchema, + fprocess.name AS processName, + procin.state_ AS state, + fform.id AS formId, + fversion.id AS versionId, + fgroup.name AS groupName + FROM + workflow_process_history AS story + INNER JOIN workflow_process_version AS fversion + ON fversion.id = story.version_id + INNER JOIN workflow_process_form AS fform + ON fform.id = fversion.form_id + INNER JOIN workflow_process AS fprocess + ON fprocess.id = fform.process_id + INNER JOIN workflow_group AS fgroup + ON fgroup.id = fprocess.group_id + AND fgroup.name = '运维中台表单' + INNER JOIN ${camWorkflow}.act_hi_procinst AS procin + ON procin.id_ = story.procinst_id + ${existOvertimeCount || existVacateCount ? + `WHERE story.create_at > '2023-03-16 00:00:00'` + : ''} + ` + ).toPromise() + + } catch (error) { + console.error(error); + } + } + ) + + return { + workorderStatistics, + } +} \ No newline at end of file diff --git a/api/app/lib/utils/parseProcessData.js b/api/app/lib/utils/parseProcessData.js new file mode 100644 index 0000000..22a2f99 --- /dev/null +++ b/api/app/lib/utils/parseProcessData.js @@ -0,0 +1,106 @@ +'use strict'; + +const schemaRecursionObj = (obj, target, schemaPath) => { + let schemaPath_ = JSON.parse(JSON.stringify(schemaPath)) + if (obj.properties) { + for (let prKey in obj.properties) { + if (obj.properties[prKey].title == target) { + schemaPath_.push({ + prKey, + ...obj.properties[prKey] + }) + return schemaPath_ + } + const hasProperties = obj.properties[prKey].properties + const isGroup = obj.properties[prKey].type == 'array' && obj.properties[prKey].title == '分组' + if (hasProperties || isGroup) { + schemaPath_.push({ + prKey, + ...obj.properties[prKey], + isGroup: isGroup, + }) + schemaPath_ = schemaRecursionObj( + isGroup ? + obj.properties[prKey].items + : obj.properties[prKey], + target, + schemaPath_ + ) + if (!schemaPath_) { + return [] + } + if (schemaPath_.length > schemaPath.length) { + return schemaPath_ + } + } + } + } else { + return schemaPath_ + } +} + +const dataRecursionObj = (dataObj, index, needData, lastKeyObj, nd) => { + const keyObj = needData[nd].schemaPath[index] + if (dataObj.hasOwnProperty(keyObj.prKey)) { + if (lastKeyObj.prKey == keyObj.prKey) { + let gotValue = dataObj[keyObj.prKey] + if (keyObj.enum && !needData[nd].fromDataSource) { + let vIndex = keyObj.enum.findIndex(ke => ke == gotValue) + gotValue = keyObj.enumNames[vIndex] + } + return gotValue + } else { + if (keyObj.isGroup) { + for (let item of dataObj[keyObj.prKey]) { + const gotValue = dataRecursionObj(item, index + 1, needData, lastKeyObj, nd) + if (gotValue) { + return gotValue + } + } + } else { + return dataRecursionObj(dataObj[keyObj.prKey], index + 1, needData, lastKeyObj, nd) + } + } + } +} + +const getData = (applyDetail, needData) => { + for (let nd in needData) { + if (needData[nd].noProcess) { + continue + } + needData[nd].schemaPath = schemaRecursionObj(applyDetail.formSchema.jsonSchema, needData[nd]['keyWord'], []) + if (needData[nd].schemaPath && needData[nd].schemaPath.length) { + const lastKeyObj = needData[nd].schemaPath[ + needData[nd].schemaPath.length - 1 + ] + needData[nd].value = dataRecursionObj(applyDetail.formData, 0, needData, lastKeyObj, nd) + } else { + // 记录错误 关键数据没找到 + } + } +} + +module.exports = function (app, opts) { + + const parseProcessData = (applyDetail, pomsNeedData = { + title: { + keyWord: '标题', + }, + pomsProjectId: { + keyWord: '关联项目', + fromDataSource: true + }, + expectTime: { + keyWord: '期望完成时间' + } + }) => { + let needData = JSON.parse(JSON.stringify(pomsNeedData)) + getData(applyDetail, needData) + return needData + } + + return { + parseProcessData + } +} \ No newline at end of file diff --git a/api/config.js b/api/config.js index 46055d0..d00c5ce 100644 --- a/api/config.js +++ b/api/config.js @@ -46,6 +46,7 @@ args.option('clickHouseProjectManage', 'clickHouse 项目管理数据库名称') args.option('clickHouseVcmp', 'clickHouse 视频平台数据库名称'); args.option('clickHouseDataAlarm', 'clickHouse 视频平台数据告警库名称'); args.option('clickHouseIot', 'clickHouse IOT平台设备信息库名称'); +args.option('clickHouseCamworkflow', 'clickHouse 工作流数据库名称'); args.option('confirmAlarmAnxinUserId', '确认告警时保存到 ES 的安心云的用户的 id'); @@ -104,6 +105,7 @@ const CLICKHOUST_PROJECT_MANAGE = process.env.CLICKHOUST_PROJECT_MANAGE || flags const CLICKHOUST_VCMP = process.env.CLICKHOUST_VCMP || flags.clickHouseVcmp const CLICKHOUST_DATA_ALARM = process.env.CLICKHOUST_DATA_ALARM || flags.clickHouseDataAlarm const CLICKHOUST_IOT = process.env.CLICKHOUST_IOT || flags.clickHouseIot +const CLICKHOUST_CAM_WORKFLOW = process.env.CLICKHOUST_CAM_WORKFLOW || flags.clickHouseCamworkflow const CONFIRM_ALARM_ANXIN_USER_ID = process.env.CONFIRM_ALARM_ANXIN_USER_ID || flags.confirmAlarmAnxinUserId @@ -125,6 +127,7 @@ if ( || !QINIU_DOMAIN_QNDMN_RESOURCE || !QINIU_BUCKET_RESOURCE || !QINIU_AK || !QINIU_SK || !CLICKHOUST_URL || !CLICKHOUST_PORT || !CLICKHOUST_ANXINCLOUD || !CLICKHOUST_PEP_EMIS || !CLICKHOUST_PROJECT_MANAGE || !CLICKHOUST_VCMP || !CLICKHOUST_DATA_ALARM || !CLICKHOUST_IOT + // || !CLICKHOUST_CAM_WORKFLOW || !CONFIRM_ALARM_ANXIN_USER_ID || !VCMP_APP_ID || !VCMP_APP_SECRET ) { @@ -248,7 +251,11 @@ const product = { }, { name: 'iot', db: CLICKHOUST_IOT - } + }, + { + name: 'camWorkflow', + db: CLICKHOUST_CAM_WORKFLOW + }, ] } } diff --git a/web/client/src/sections/analysis/containers/workorderData.jsx b/web/client/src/sections/analysis/containers/workorderData.jsx index 52ebff6..42a390e 100644 --- a/web/client/src/sections/analysis/containers/workorderData.jsx +++ b/web/client/src/sections/analysis/containers/workorderData.jsx @@ -43,6 +43,7 @@ const WorkOrderData = (props) => { nameGap: 20 }, legend: {}, + tooltip: {}, grid: { left: '3%', right: '4%', @@ -61,9 +62,10 @@ const WorkOrderData = (props) => { }, { t: '工单完成情况分析', echartOption: { + tooltip: {}, series: [ { - name: 'xx', + name: '工单完成率', type: 'gauge', progress: { show: true @@ -77,10 +79,14 @@ const WorkOrderData = (props) => { value: 50, name: '工单完成率' } - ] + ], + title: { + show: true, + offsetCenter: ["0", "100%"] + } }, { - name: 'YY', + name: '延期数', type: 'gauge', progress: { show: true @@ -94,7 +100,11 @@ const WorkOrderData = (props) => { value: 50, name: '延期数' } - ] + ], + title: { + show: true, + offsetCenter: ["0", "100%"] + } }, ], media: [ @@ -111,10 +121,87 @@ const WorkOrderData = (props) => { }, }, { t: '工单处理分析', - echartOption: {}, + echartOption: { + xAxis: { + type: 'category', + data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + }, + yAxis: { + type: 'value', + name: "单位:个", + nameLocation: "end", + nameGap: 20 + }, + legend: {}, + tooltip: {}, + grid: { + left: '3%', + right: '4%', + bottom: '1%', + // top: '24%', + containLabel: true + }, + series: [ + { + data: [120, 200, 150, 80, 70, 110, 130], + type: 'bar', + name: '发起数' + }, + { + data: [120, 200, 150, 80, 70, 110, 130], + type: 'bar', + name: '解决数' + } + ] + }, }, { t: '工单类型分析', - echartOption: {}, + echartOption: { + tooltip: {}, + series: [ + { + name: '设备类型', + type: 'pie', + radius: '50%', + data: [ + { value: 1048, name: 'Search Engine' }, + { value: 735, name: 'Direct' }, + { value: 580, name: 'Email' }, + { value: 484, name: 'Union Ads' }, + { value: 300, name: 'Video Ads' } + ], + label: { + + } + }, + { + name: '解决方案', + type: 'pie', + radius: '50%', + data: [ + { value: 1048, name: 'Search Engine' }, + { value: 735, name: 'Direct' }, + { value: 580, name: 'Email' }, + { value: 484, name: 'Union Ads' }, + { value: 300, name: 'Video Ads' } + ], + label: { + + } + }, + ], + media: [ + { + query: { minAspectRatio: 1 }, + option: { + series: [ + { center: ['25%', '50%'] }, + { center: ['75%', '50%'] } + ] + } + }, + ] + }, },].map((c, idx) => (