* @package metric * @link https://www.zentao.net */ class metricZen extends metric { private $validObjects; /** * 构建创建度量的数据。 * Build metric data for create. * * @access protected * @return object */ protected function buildMetricForCreate() { return form::data($this->config->metric->form->create) ->setDefault('createdBy', $this->app->user->account) ->setDefault('createdDate', helper::now()) ->get(); } /** * 构建编辑度量的数据。 * Build metric data for edit. * * @access protected * @return object */ protected function buildMetricForEdit() { return form::data($this->config->metric->form->create) ->setDefault('editedBy', $this->app->user->account) ->setDefault('editedDate', helper::now()) ->get(); } /** * 成功插入度量数据后,其他的额外操作。 * Process after create metric. * * @param int $metricID * @param string $afterCreate * @param string $from metric|metriclib * @param string $location * @access protected * @return array */ protected function responseAfterCreate($metricID, $afterCreate, $from, $location) { if($afterCreate == 'back' && $location) return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => $location); $location = $this->createLink('metric', 'implement', "metricID=$metricID&from=$from"); $callback = array('name' => 'loadImplement', 'params' => $location); return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'callback' => $callback); } /** * 成功编辑度量数据后,其他的额外操作。 * Process after edit metric. * * @access protected * @return array */ protected function responseAfterEdit($metricID, $afterEdit, $location) { if($afterEdit == 'back' && $location) return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => $location); $location = $this->createLink('metric', 'implement', "metricID=$metricID"); $callback = array('name' => 'loadImplement', 'params' => $location); return array('result' => 'success', 'message' => $this->lang->saveSuccess, 'callback' => $callback); } /** * 根据分类后的度量项,准备数据源句柄。 * Prepare the data source handle based on the classified measures. * * @param object $calcGroup * @access protected * @return array */ protected function prepareDataset($calcGroup) { $dao = $this->metric->getDAO(); $dataSource = $calcGroup->dataset; $calcList = $calcGroup->calcList; foreach($calcList as $calc) { $calc->setDAO($dao); if($calc->useSCM && ($this->config->inQuickon || $this->config->inCompose)) { $scm = $this->app->loadClass('scm'); $calc->setSCM($scm); $calc->setGitFoxRepos($this->loadModel('repo')->getGitFoxRepos()); } } if(empty($dataSource)) { $calc = current($calcList); $calc->setDAO($dao); return $calc->getStatement(); } foreach($calcList as $calc) { $calc->setHolidays($this->loadModel('holiday')->getList()); $calc->setWeekend(isset($this->config->project->weekend) ? $this->config->project->weekend : 2); } $dataset = $this->metric->getDataset($dao); $fieldList = $this->metric->uniteFieldList($calcList); return $dataset->$dataSource($fieldList); } /** * 构建模块树数据。 * Prepare module tree data. * * @param string $scope * @param string $stage * @param array $modules * @access protected * @return void */ protected function prepareTree($scope, $stage, $modules) { $sortedModules = array(); $groupModules = array(); foreach($modules as $module) { $groupModules[$module->object][] = $module; } foreach(array_keys($this->lang->metric->objectList) as $object) { if(isset($groupModules[$object]) and !empty($groupModules[$object])) { $sortedModules = array_merge($sortedModules, $groupModules[$object]); } } $moduleTree = array(); foreach($sortedModules as $module) { $object = $module->object; $purpose = $module->purpose; $moduleTree[$object] = (object)array ( 'id' => $object, 'parent' => '0', 'name' => $this->lang->metric->objectList[$object], 'url' => $this->inlink('browse', "scope=$scope&stage=$stage¶m=$object&type=byTree") ); $moduleTree["{$object}_{$purpose}"] = (object)array ( 'id' => "{$object}_{$purpose}", 'parent' => $object, 'name' => $this->lang->metric->purposeList[$purpose], 'url' => $this->inlink('browse', "scope=$scope&stage=$stage¶m={$object}_{$purpose}&type=byTree") ); } return $moduleTree; } /** * 构建范围下拉数据。 * Prepare scope picker data. * * @access protected * @return void */ protected function prepareScopeList() { $scopeList = array(); foreach($this->lang->metric->scopeList as $scope => $scopeText) $scopeList[] = array('key' => $scope, 'text' => $scopeText); return $scopeList; } /** * 开始计时。 * * @access private * @return float */ private function startTime() { return microtime(true); } /** * 结束计时。 * * @param float $beginTime * @access private * @return string */ private function endTime($beginTime) { $time = microtime(true) - $beginTime; return number_format((float)$time, 5, '.', '0'); } /** * 获取有效的对象。 * Get valid objects. * * @access private * @return array */ private function getValidObjects() { if($this->validObjects !== null) return $this->validObjects; /* 保证逻辑集中,这里直接使用sql查询获取数据,保证查询性能。*/ /* To ensure logical concentration, here we directly use sql to get data to ensure query performance. */ $productList = $this->dao->select('id,closedDate')->from(TABLE_PRODUCT) ->where('deleted')->eq(0) ->andWhere('status')->ne('closed') ->andWhere('shadow')->eq(0) ->fetchPairs('id', 'closedDate'); $projectList = $this->dao->select('id,closedDate')->from(TABLE_PROJECT) ->where('deleted')->eq(0) ->andWhere('status')->notIN('closed,done') ->andWhere('type')->eq('project') ->fetchPairs('id', 'closedDate'); $executionList = $this->dao->select('id,closedDate')->from(TABLE_EXECUTION) ->where('deleted')->eq(0) ->andWhere('status')->notIN('closed,done') ->andWhere('type')->in('sprint,stage,kanban') ->fetchPairs('id', 'closedDate'); $this->validObjects = array('product' => $productList, 'project' => $projectList, 'execution' => $executionList); return $this->validObjects; } /** * 计算度量数据。 * Calculate metric data. * * @param array $classifiedCalcGroup * @access protected * @return void */ protected function calculateMetric($classifiedCalcGroup) { set_time_limit(0); $calcBeginTime = $this->startTime(); foreach($classifiedCalcGroup as $calcGroup) { try { /* 开始计算度量数据。*/ /* Start calculating metric data. */ $beginTime = $this->startTime(); $statement = $this->prepareDataset($calcGroup); $this->calcMetric($statement, $calcGroup->calcList); $recordWithCode = $this->prepareMetricRecord($calcGroup->calcList); $calcTime = $this->endTime($beginTime); /* 开始插入度量数据。*/ /* Start inserting metric data. */ $beginTime = $this->startTime(); $this->metric->insertMetricLib($recordWithCode); $executeTime = $this->endTime($beginTime); /* 记录度量数据计算和插入的时间以及sql语句。*/ /* Record the time of metric data calculation and insertion and sql statement. */ $total = 0; $codes = ''; $sql = $statement ? $statement->get() : ''; foreach($recordWithCode as $code => $records) { $count = count($records); $codes .= "$code($count), "; $total += $count; } $this->metric->saveLogs("Calculate consumed time(seconds): calc: $calcTime, insert: $executeTime, total: $total, sql: $sql, $codes"); } catch(Exception $e) { a($this->formatException($e)); } catch(Error $e) { a($this->formatException($e)); } } /* 记录度量数据计算的总时间。*/ /* Record the total time of metric data calculation. */ $executeTime = $this->endTime($calcBeginTime); $this->metric->saveLogs("Calculate all consumed time: {$executeTime} seconds"); /* 开始去重。*/ /* Start deduplication. */ $beginTime = $this->startTime(); $metrics = $this->metric->getExecutableMetric(); foreach($metrics as $code) { $deduplicationBeginTime = $this->startTime(); $this->metric->deduplication($code); $executeTime = $this->endTime($deduplicationBeginTime); $this->metric->saveLogs("Deduplication consumed time: {$executeTime} seconds, the code: $code"); } /* 重建主键。*/ /* Rebuild the primary key. */ $this->metric->rebuildPrimaryKey(); /* 记录去重的时间。*/ /* Record the time of deduplication. */ $executeTime = $this->endTime($beginTime); $this->metric->saveLogs("Deduplicate all consumed time: {$executeTime} seconds"); } /** * 根据度量项计算的结果,构建可插入表的度量数据。 * Build measurements that can be inserted into tables based on the results of the measurements computed. * * @param array $calcList * @access protected * @return array */ protected function prepareMetricRecord($calcList) { /* 获取今天和昨天的日期。*/ /* Get the date of today and yesterday. */ $yesterday = date('j', strtotime('-1 day', strtotime('today'))); $today = date('j'); $options = array('year' => date('Y'), 'month' => date('n'), 'week' => substr(date('oW'), -2), 'day' => "$today,$yesterday"); /* 获取未关闭的对象id。*/ /* Get the id of the unclosed objects. */ $this->getValidObjects(); $validObjects = $this->validObjects; $records = array(); $yesterday = strtotime('-1 day'); foreach($calcList as $code => $calc) { $metric = $this->metric->getByCode($code); /* 如果度量项是复用的,则计算复用的度量数据。*/ /* If the metric is reused, calculate the reused metric data. */ if($calc->reuse) $this->prepareReuseMetricResult($calc, $options); /* 判断是否统计关闭时的对象的度量数据。*/ $endWithClosing = preg_match('/_when_closing$/', $code); $results = $calc->getResult($options); $records[$code] = array(); if(is_array($results)) { $scope = $metric->scope; foreach($results as $record) { $record = (object)$record; if(!is_numeric($record->value) || empty($record->value)) continue; /* 如果度量项是产品、项目、执行且不统计关闭时的对象的度量数据,则过滤掉已关闭的数据。*/ /* If the metric is product, project, execution, filter out the closed data. */ $closedDate = isset($validObjects[$scope]) && !helper::isZeroDate(zget($validObjects[$scope], $record->$scope, '')) ? $validObjects[$scope][$record->$scope] : null; $isClosedEarlierThanYesterday = $closedDate ? strtotime($closedDate) < $yesterday : false; if(!$endWithClosing && $closedDate !== null && $isClosedEarlierThanYesterday) continue; $record->metricID = $calc->id; $record->metricCode = $code; $record->date = helper::now(); $record->system = $metric->scope == 'system' ? 1 : 0; $records[$code][] = $record; } } } return $records; } /** * 获取复用的度量数据。 * Prepare metric result for reuse metric. * * @param object $calc * @param array $options * @access protected * @return array */ protected function prepareReuseMetricResult($calc, $options) { $reuseMetrics = array(); foreach($calc->reuseMetrics as $key => $reuseMetric) $reuseMetrics[$key] = $this->metric->getResultByCode($reuseMetric, $options); $calc->calculate($reuseMetrics); } /** * 获取某度量项某日期的度量数据。 * Get metric record by code and date. * * @param string $code * @param object $calc * @param string $date * @param string $type * @access protected * @return array */ protected function getRecordByCodeAndDate($code, $calc, $date, $type = 'single') { $now = helper::now(); $metric = $this->metric->getByCode($code); $dateType = $this->metric->getDateTypeByCode($code); if($dateType == 'nodate') return array(); if($type == 'all' && $this->metric->checkHasInferenceOfDate($code, $dateType, $date)) return array(); $records = array(); $dateConfig = $this->metric->parseDateStr($date, $dateType); $results = $calc->getResult($dateConfig); if(is_array($results)) { foreach($results as $record) { $record = (object)$record; if(empty($record->value)) continue; $record->metricID = $metric->id; $record->metricCode = $code; $record->date = $now; $record->system = $metric->scope == 'system' ? 1 : 0; $records[] = $record; } } return $records; } /** * 根据度量项编码,初始化度量数据。 * Initialize metric data based on metric code. * * @param string $code * @param string $dateType * @param string $date * @access protected * @return array */ protected function initMetricRecords($recordCommon, $scope, $date = 'now') { $records = array(); if($scope == 'system') { $record = clone $recordCommon; //$record->system = 1; $uniqueKey = $this->getUniqueKeyByRecord($record); $records[$uniqueKey] = $record; } else { if($date == 'now') $date = helper::now(); $scopeList = $this->metric->getPairsByScopeAndDate($scope, $date); foreach($scopeList as $key => $value) { $record = clone $recordCommon; $record->$scope = $key; $uniqueKey = $this->getUniqueKeyByRecord($record); $records[$uniqueKey] = $record; } } return $records; } /** * 根据度量项范围,构建度量数据的通用字段。 * Build common fields of metric data based on metric scope. * * @param int $metricID * @param string $code * @param string $date * @param array $dateValues * @access protected * @return array */ protected function buildRecordCommonFields($metricID, $code, $date, $dateValues) { $record = new stdclass(); $record->value = 0; $record->date = $date; $record->calcType = 'cron'; $record->calculatedBy = 'system'; $record = (object)array_merge($dateValues, (array)$record); return $record; } /** * 补全缺失的度量数据。 * Complete missing metric data. * * @param array $records * @param array $metric * @access protected * @return array */ protected function completeMissingRecords($records, $metric) { $now = helper::now(); $dateValues = $this->metric->parseDateStr($now); $dateType = $metric->dateType; $recordCommon = $this->buildRecordCommonFields($metric->id, $metric->code, $now, $dateValues->$dateType); $initRecords = $this->initMetricRecords($recordCommon, $metric->scope); foreach($records as $record) { $uniqueKey = $this->getUniqueKeyByRecord($record, $metric->scope); if(!isset($initRecords[$uniqueKey])) { $initRecords[$uniqueKey] = $record; continue; } $initRecords[$uniqueKey]->value = $record->value; } return array_values($initRecords); } /** * 根据度量数据,获取度量数据的唯一键。 * Get the unique key of metric data based on metric data. * * @param object $record * @param string $scope * @access protected * @return string */ protected function getUniqueKeyByRecord($record, $scope = '') { $record = (array)$record; $uniqueKeys = array(); $ignoreFields = array('value', 'metricID', 'metricCode', 'calcType', 'calculatedBy', 'date'); if($scope == 'system') $ignoreFields[] = 'id'; foreach($record as $field => $value) { if(in_array($field, $ignoreFields) || empty($value)) continue; $uniqueKeys[] = $field . $value; } return implode('_', $uniqueKeys); } /** * 遍历数据,对每个度量项计算每一行数据。 * Calculate metric for every row. * * @param array $rows * @param array $calcList * @access protected * @return void */ protected function calcMetric($statement, $calcList) { if(empty($statement)) return; $dbType = $this->config->metricDB->type; if($dbType == 'duckdb') { $this->loadModel('bi'); $sql = $statement->get(); $dbh = $this->app->loadDriver('duckdb'); $rows = $dbh->query($sql)->fetchAll(); foreach($rows as $row) { foreach($calcList as $calc) { if(!$calc->reuse) { $record = $this->getCalcFields($calc, $row); $calc->calculate($record); } } } return; } $statement = $statement->query(); while($row = $statement->fetch()) { foreach($calcList as $code => $calc) { if(!$calc->reuse) { $record = $this->getCalcFields($calc, $row); $calc->calculate($record); } } } } protected function getCalcFields($calc, $row) { if(!isset($calc->dataset) || empty($calc->dataset)) return (object)$row; $pureRow = new stdclass(); foreach($calc->fieldList as $field) { if(strpos(strtoupper($field), ' AS ') !== false) { $pos = strpos(strtoupper($field), ' AS '); $tag = substr($field, $pos, 4); $extractField = explode($tag, $field); $pureField = end($extractField); $aliasField = $pureField; } else { $extractField = explode('.', $field); $pureField = end($extractField); $aliasField = str_replace('.', '_', $field); } $pureRow->$pureField = $row->$aliasField; } if(isset($row->defaultHours)) $pureRow->defaultHours = $row->defaultHours; return $pureRow; } /** * 获取度量项的基本信息。 * Get the basic information of the metric. * * @param object $view * @param string $fields * @access protected * @return array */ protected function getBasicInfo($view, $fields = 'scope,object,purpose,dateType,name,alias,code,unit,stage') { extract((array)$view); $isOldMetric = $this->metric->isOldMetric($metric); $unit = $isOldMetric ? $metric->oldUnit : zget($this->lang->metric->unitList, $metric->unit); $legendBasic = array(); if(strpos($fields, 'scope') !== false) $legendBasic['scope'] = array('name' => $this->lang->metric->scope, 'text' => zget($this->lang->metric->scopeList, $metric->scope)); if(strpos($fields, 'object') !== false) $legendBasic['object'] = array('name' => $this->lang->metric->object, 'text' => zget($this->lang->metric->objectList, $metric->object)); if(strpos($fields, 'purpose') !== false) $legendBasic['purpose'] = array('name' => $this->lang->metric->purpose, 'text' => zget($this->lang->metric->purposeList, $metric->purpose)); if(strpos($fields, 'dateType') !== false) $legendBasic['dateType'] = array('name' => $this->lang->metric->dateType, 'text' => zget($this->lang->metric->dateTypeList, $metric->dateType)); if(strpos($fields, 'name') !== false) $legendBasic['name'] = array('name' => $this->lang->metric->name, 'text' => $metric->name); if(strpos($fields, 'alias') !== false) $legendBasic['alias'] = array('name' => $this->lang->metric->alias, 'text' => $metric->alias); if(strpos($fields, 'code') !== false) $legendBasic['code'] = array('name' => $this->lang->metric->code, 'text' => $metric->code); if(strpos($fields, 'unit') !== false) $legendBasic['unit'] = array('name' => $this->lang->metric->unit, 'text' => $unit); if($isOldMetric) { $legendBasic['collectType'] = array('name' => $this->lang->metric->collectType, 'text' => zget($this->lang->metric->old->collectTypeList, $metric->collectType)); $legendBasic['collectConf'] = array('name' => $this->lang->metric->collectConf, 'text' => $this->metric->getCollectConfText($metric)); } if(strpos($fields, 'desc') !== false) $legendBasic['desc'] = array('name' => $this->lang->metric->desc, 'text' => $metric->desc); if(strpos($fields, 'definition') !== false) $legendBasic['definition'] = array('name' => $this->lang->metric->definition, 'text' => $metric->definition); if(strpos($fields, 'stage') !== false) $legendBasic['stage'] = array('name' => $this->lang->metric->stage, 'text' => zget($this->lang->metric->stageList, $metric->stage)); return $legendBasic; } /** * 获取度量项的创建和编辑信息。 * Get the create and edit information of the metric. * * @param object $view * @param string $fields * @access protected * @return array */ protected function getCreateEditInfo($view, $fields = 'createdBy,implementedBy,offlineBy,lastEdited') { extract((array)$view); $users = $this->loadModel('user')->getPairs('noletter'); $users['system'] = 'system'; $createEditInfo = array(); if(strpos($fields, 'createdBy') !== false) $createEditInfo['createdBy'] = array('name' => $this->lang->metric->createdBy, 'text' => zget($users, $metric->createdBy) . ($metric->createdBy ? $this->lang->at . $metric->createdDate : '')); if(strpos($fields, 'implementedBy') !== false) $createEditInfo['implementedBy'] = array('name' => $this->lang->metric->implementedBy, 'text' => zget($users, $metric->implementedBy) . ($metric->implementedBy ? $this->lang->at . $metric->implementedDate : '')); if(strpos($fields, 'offlineBy') !== false) $createEditInfo['offlineBy'] = array('name' => $this->lang->metric->offlineBy, 'text' => zget($users, $metric->delistedBy) . ($metric->delistedBy ? $this->lang->at . $metric->delistedDate : '')); if(strpos($fields, 'lastEdited') !== false) $createEditInfo['lastEdited'] = array('name' => $this->lang->metric->lastEdited, 'text' => zget($users, $metric->editedBy) . ($metric->editedBy ? $this->lang->at . $metric->editedDate : '')); return $createEditInfo; } /** * 获取旧版详情区块。 * Get old metric info panel. * * @param int $oldMetricID * @access protected * @return array */ protected function getOldMetricInfo($oldMetricID) { $oldMetric = $this->metric->getOldMetricByID($oldMetricID); $oldMetricInfo = array(); $oldMetricInfo['scope'] = array('name' => $this->lang->metric->scope, 'text' => zget($this->lang->metric->old->scopeList, $oldMetric->scope)); $oldMetricInfo['object'] = array('name' => $this->lang->metric->object, 'text' => zget($this->lang->metric->old->objectList, $oldMetric->object)); $oldMetricInfo['purpose'] = array('name' => $this->lang->metric->purpose, 'text' => zget($this->lang->metric->old->purposeList, $oldMetric->purpose)); $oldMetricInfo['code'] = array('name' => $this->lang->metric->code, 'text' => $oldMetric->code); $oldMetricInfo['unit'] = array('name' => $this->lang->metric->unit, 'text' => $oldMetric->unit); $oldMetricInfo['collectType'] = array('name' => $this->lang->metric->collectType, 'text' => zget($this->lang->metric->old->collectTypeList, $oldMetric->collectType)); $oldMetricInfo['collectConf'] = array('name' => $this->lang->metric->collectConf, 'text' => $this->metric->getCollectConfText($oldMetric)); $oldMetricInfo['definition'] = array('name' => $this->lang->metric->declaration, 'text' => $oldMetric->definition); $oldMetricInfo['sql'] = array('name' => $this->lang->metric->sqlStatement, 'text' => $oldMetric->configure); return $oldMetricInfo; } /** * 处理周定时配置。 * Process week configuration. * * @param string $dateConf * @access protected * @return string */ protected function processWeekConf($dateConf) { $days = explode(',', $dateConf); $dateConfNames = array(); foreach($days as $day) { $dateConfNames[] = $this->lang->metric->oldMetric->dayNames[$day]; } $dateConf = implode('、', $dateConfNames); return $dateConf; } /** * 获取度量数据表的宽度。 * Get width of result table in view. * * @param array $headers * @access protected * @return int */ protected function getViewTableWidth($headers) { $width = 0; foreach($headers as $header) $width += isset($header['width']) ? $header['width'] : 160; /* Add a little redundancy. */ $width ++; return $width; } /** * Get pager extra. * * @param string $tableWidth * @access protected * @return string */ public function getPagerExtra($tableWidth) { return ($tableWidth > 300) ? '' : 'shortPageSize'; } /** * 格式化异常为字符串。 * Format exception to string. * * @param Exception $e * @access protected * @return string */ public function formatException($e) { $message = $e->getMessage(); $line = $e->getLine(); $file = $e->getFile(); return "Error: $message in $file on line $line"; } /** * 根据后台配置的估算单位对列表赋值。 * Assign unitList['measure'] by custom hourPoint. * * @access protected * @return void */ protected function processUnitList() { $this->app->loadLang('custom'); $key = zget($this->config->custom, 'hourPoint', '0'); $this->lang->metric->unitList['measure'] = $this->lang->custom->conceptOptions->hourPoint[$key]; } /** * 构建操作权限。 * Prepare action priv. * * @param array $metrics * @access protected * @return array */ protected function prepareActionPriv($metrics) { $this->loadModel('screen'); foreach($metrics as $metric) { $metric->canEdit = $metric->stage == 'wait'; $metric->canImplement = ($metric->stage == 'wait' && !$this->metric->isOldMetric($metric) && $metric->builtin == '0'); $metric->canDelist = $metric->stage == 'released' && $metric->builtin == '0'; $metric->canRecalculate = $metric->stage == 'released' && !empty($metric->dateType) && $metric->dateType != 'nodate'; $metric->isUsed = $this->screen->checkIFChartInUse($metric->id, 'metric'); } return $metrics; } }