* @package zai * @link https://www.zentao.net */ class zaiModel extends model { /** * 获取当前用户ZAI Authorization Token。 * Get ZAI Authorization Token of current user. * * @access public * @param object|null $zaiConfig * @param bool $admin * @return array */ public function getToken($zaiConfig = null, $admin = false) { $zaiConfig = $zaiConfig ? $zaiConfig : $this->getSetting($admin); if(!isset($zaiConfig->token) || !isset($zaiConfig->appID) || ($admin && !isset($zaiConfig->adminToken))) { return array( 'result' => 'fail', 'message' => $this->lang->zai->configurationUnavailable ); } if(!$this->loadModel('user')->isLogon()) { return array( 'result' => 'fail', 'message' => $this->lang->zai->illegalZentaoUser ); } $token = $admin ? $zaiConfig->adminToken : $zaiConfig->token; $userID = $this->app->user->account; $zaiTokenTTL = isset($zaiConfig->zaiTokenTTL) ? $zaiConfig->zaiTokenTTL : 1200; $expiredTime = time() + $zaiTokenTTL; $origin = $token . $zaiConfig->appID . $userID . $expiredTime; $hash = md5($origin); return array( 'result' => 'success', 'data' => array( 'hash' => $hash, 'expiredTime' => $expiredTime, 'appID' => $zaiConfig->appID, 'userID' => $userID ) ); } /** * 格式化旧的ZAI设置。 * Format old ZAI settings. * * @access public * @param object|null $setting * @return object|null */ public function formatOldSetting($setting) { if(empty($setting)) return null; if(empty($setting->host) && !empty($setting->apiBaseUrl)) { $apiBaseUrl = str_replace('///', '', $setting->apiBaseUrl); $apiBaseUrl = str_replace('http://', '', $apiBaseUrl); $apiBaseUrl = str_replace('https://', '', $apiBaseUrl); $apiBaseUrl = str_replace('/v1', '', $apiBaseUrl); $urlParts = explode(':', $apiBaseUrl); $setting->host = $urlParts[0]; $setting->port = isset($urlParts[1]) ? $urlParts[1] : 0; } if(empty($setting->token) && !empty($setting->appToken)) { $setting->token = $setting->appToken; unset($setting->appToken); } return $setting; } /** * 获取ZAI设置。 * Get ZAI settings. * * @access public * @param bool $includeAdmin * @return object|null */ public function getSetting($includeAdmin = false) { $settingJson = $this->loadModel('setting')->getItem("owner=system&module=zai§ion=global&key=setting"); $setting = json_decode($settingJson); if(!is_object($setting) && isset($this->config->zai)) { $setting = $this->formatOldSetting($this->config->zai); } if(empty($setting) || empty($setting->host) || empty($setting->appID) || empty($setting->token)) { return null; } $vectorizedInfo = $this->getVectorizedInfo(); if(!empty($vectorizedInfo->key)) { $setting->globalMemory = 'zentao:global'; } if(!$includeAdmin) unset($setting->adminToken); return $setting; } /** * 设置ZAI设置。 * Set ZAI settings. * * @access public * @param object|null $setting */ public function setSetting($setting) { if(!is_object($setting)) { $this->loadModel('setting')->setItem('system.zai.global.setting', ''); return; } $this->loadModel('setting')->setItem('system.zai.global.setting', empty($setting) ? '' : json_encode($setting)); } /** * 获取禅道数据向量化信息。 * Get information of vectorized data of ZenTao. * * @access public * @return object */ public function getVectorizedInfo() { $infoJson = $this->loadModel('setting')->getItem("owner=system&module=zai§ion=kb&key=systemVectorization"); $info = json_decode($infoJson); if(!$info) { $info = new stdClass(); $info->key = ''; $info->status = 'disabled'; $info->syncedTime = 0; $info->syncedCount = 0; $info->syncFailedCount = 0; $info->syncTime = 0; $info->syncingType = zaiModel::getNextSyncType(); $info->syncingID = 0; $info->syncDetails = new stdClass(); $info->createdAt = time(); $info->createdBy = $this->app->user->account; } return $info; } /** * 设置禅道数据向量化信息。 * Set information of vectorized data of ZenTao. * * @access public * @param object $info - The information of vectorized data. 禅道数据向量化信息对象。 */ public function setVectorizedInfo($info) { if(!is_string($info)) $info = json_encode($info); $this->loadModel('setting')->setItem('system.zai.kb.systemVectorization', $info); } /** * 调用 ZAI API。 * Call ZAI API. * * @access public * @param string $path * @param string $method * @param array|null $params * @param array|null $postData * @param bool $admin * @return array 通过 result 属性返回调用结果,通过 data 属性返回调用数据,通过 message 属性返回调用错误信息,通过 code 属性返回调用错误代码。 Return array with result, data, message and code. */ public function callAPI($path, $method = 'POST', $params = null, $postData = null, $admin = false) { $setting = $this->getSetting($admin); if(empty($setting)) return array('result' => 'fail', 'message' => $this->lang->zai->configurationUnavailable); $tokenInfo = $this->getToken($setting, $admin); if($tokenInfo['result'] != 'success') return $tokenInfo; $tokenData = array('hash' => $tokenInfo['data']['hash'], 'expired_time' => $tokenInfo['data']['expiredTime'], 'app_id' => $tokenInfo['data']['appID'], 'user_id' => $tokenInfo['data']['userID']); $token = ($admin ? 'ak-' : 'ek-') . base64_encode(json_encode($tokenData)); /* Check if the request is HTTPS. */ if($path[0] != '/') $path = '/' . $path; $isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'; if(!$isHttps) $isHttps = !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'; if(!$isHttps) $isHttps = !empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443; $protocol = $isHttps ? 'https://' : 'http://'; $host = $setting->host; $port = $setting->port; $url = $protocol . $host . ($port ? ":$port" : '') . $path; if($params) $url = $url . '?' . http_build_query($params); $curl = curl_init($url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HEADER, true); if($method === 'GET') { curl_setopt($curl, CURLOPT_HTTPGET, true); } elseif($method === 'POST') { curl_setopt($curl, CURLOPT_POST, true); } else { curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); } $hasFile = false; if($postData !== null) // Post data must set even if it is empty { foreach($postData as $value) { if($value instanceof CURLFile) { $hasFile = true; break; } } curl_setopt($curl, CURLOPT_POSTFIELDS, $hasFile ? $postData : json_encode($postData, JSON_UNESCAPED_UNICODE)); } $headers = ['Authorization: Bearer ' . $token]; if(!$hasFile) $headers[] = 'Content-Type: application/json'; curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($curl); $error = ''; $data = null; $info = curl_getinfo($curl); $code = isset($info['http_code']) ? $info['http_code'] : curl_getinfo($curl, CURLINFO_HTTP_CODE); if(curl_errno($curl)) { $error = curl_error($curl); } else { $headerSize = isset($info['header_size']) ? $info['header_size'] : curl_getinfo($curl, CURLINFO_HEADER_SIZE); $data = substr($response, $headerSize); } curl_close($curl); if($code == 404) return array('result' => 'fail', 'data' => null, 'message' => $this->lang->notFound, 'code' => $code); if($code == 401) return array('result' => 'fail', 'data' => null, 'message' => $this->lang->zai->authenticationFailed, 'code' => $code); if($error || $code != 200) { return array('result' => 'fail', 'data' => $data, 'code' => $code, 'postData' => $postData, 'message' => sprintf($this->lang->zai->callZaiAPIFailed, $url, ($this->app->config->debug ? $error : '') . "(code: $code, response: $response)")); } if(empty($data)) $data = $response; if(is_string($data)) $data = json_decode($data, true); return array('result' => 'success', 'data' => $data); } /** * 调用ZAI管理API。 * Call ZAI admin API. * * @access public * @param string $path * @param string $method * @param array|null $params * @param array|null $postData * @return array 通过 result 属性返回调用结果,通过 data 属性返回调用数据,通过 message 属性返回调用错误信息,通过 code 属性返回调用错误代码。 Return array with result, data, message and code. */ public function callAdminAPI($path, $method = 'POST', $params = null, $postData = null) { return $this->callAPI($path, $method, $params, $postData, true); } /** * 启用数据向量化。 * Enable data vectorization. * * @access public * @return array */ public function enableVectorization($force = false) { $info = $this->getVectorizedInfo(); if($info->status != 'disabled' && !$force) return array('result' => 'fail', 'message' => $this->lang->zai->vectorizedAlreadyEnabled, 'info' => $info); $suffix = '_' . time(); $postData = array('name' => 'zentao' . $suffix, 'description' => $this->app->lang->zai->zentaoVectorization); $result = $this->callAdminAPI('/v8/memories', 'POST', null, $postData); if($result['result'] != 'success') return $result; if(empty($result['data']['id'])) return array('result' => 'fail', 'message' => $this->lang->zai->vectorizedFailed); $info->status = 'wait'; $info->key = $result['data']['id']; $info->syncedTime = 0; $info->syncedCount = 0; $info->syncFailedCount = 0; $info->syncTime = 0; $info->syncingType = zaiModel::getNextSyncType(); $info->syncingID = 0; $info->syncDetails = new stdClass(); $this->setVectorizedInfo($info); return array('result' => 'success', 'info' => $info, 'data' => $result['data'], 'message' => $this->lang->zai->vectorizedEnabled); } /** * 获取下一个指定类型的对象。 * 从数据库查询对应 $syncingType 的下一个待同步的对象,即 ID 大于 $syncingID 的且没有被删除的下一个对象。如果未查询到,则返回 null。 * * @access public * @param string $type * @param int $id * @return object|null */ public function getNextTarget($type, $id) { if(!isset(static::$syncTables[$type])) return null; $table = static::$syncTables[$type]; $target = $this->dao->select('*')->from($table)->where('id')->ge($id)->andWhere('deleted')->eq(0)->orderby('id_asc')->fetch(); if(!$target) return null; return $target; } /** * 同步下一个目标。 * Sync next target. * * @access public * @param string $memoryID 知识库 ID。The ID of knowledge base. * @param string $type * @param int $id * @return array|null */ public function syncNextTarget($memoryID, $type, $id) { $target = $this->getNextTarget($type, $id); if(!$target) return null; $markdownData = static::convertTargetToMarkdown($type, $target); $syncData = array(); $syncData['content'] = $markdownData['content']; $syncData['content_type'] = 'markdown'; $syncData['key'] = "$type-$target->id"; $syncData['attrs'] = array('objectType' => $type, 'objectID' => $target->id); if(isset($markdownData['attrs'])) $syncData['attrs'] = array_merge($syncData['attrs'], $markdownData['attrs']); $result = $this->callAdminAPI("/v8/memories/$memoryID/contents", 'POST', null, $syncData); if($result['result'] != 'success') { $code = isset($result['code']) ? $result['code'] : 0; /* 当指定 memoryID 的知识库不存在时创建一个新的。 */ /* If kb with the given memoryID not exists, then create a new one. */ if($code == 412) { $result['message'] = $this->lang->zai->cannotFindMemoryInZai; $result['code'] = 'cannotFindMemoryInZai'; } if($code !== 100 && ($code >= 400 || $code <= 500)) $result['fatal'] = true; return $result; } return array('result' => 'success', 'target' => $target, 'id' => $target->id, 'data' => $result['data'], 'syncedData' => $syncData); } /** * 获取知识库在 ZAI 的 key。 * Get key of knowledge base in ZAI. * * @access public * @param string|int $libID * @return string */ public function getCollectionKey($libID) { if($libID === 'global') { $vectorizedInfo = $this->getVectorizedInfo(); return empty($vectorizedInfo->key) ? '' : $vectorizedInfo->key; } return ''; } /** * 创建知识库。 * Create knowledge library. * * @access public * @param string $name * @param string $description * @param array|null $options * @return array 通过 result 属性返回调用结果,通过 data 属性返回调用数据,通过 message 属性返回调用错误信息,通过 code 属性返回调用错误代码。 Return array with result, data, message and code. */ public function createKnowledgeLib($name, $description = '', $options = null) { $postData = ['name' => $name, 'description' => $description]; if($options) $postData = array_merge($postData, $options); $result = $this->callAdminAPI('/v8/memories', 'POST', null, $postData); if($result['result'] !== 'success' || empty($result['data']['id'])) return null; return $result['data']; } /** * 删除知识库。 * Delete knowledge library. * * @access public * @param string $memoryID * @return bool */ public function deleteKnowledgeLib($memoryID) { $result = $this->callAdminAPI("/v8/memories/$memoryID", 'DELETE'); return $result['result'] === 'success'; } /** * 更新知识内容。 * Update knowledge content. * * @access public * @param string $memoryID * @param string $key * @param string $content * @param string $contentType * @param array|null $attrs * @return array|null */ public function updateKnowledgeItem($memoryID, $key, $content, $contentType = 'markdown', $attrs = null) { $postData = ['content' => $content, 'content_type' => $contentType, 'key' => $key, 'attrs' => $attrs]; $result = $this->callAdminAPI("/v8/memories/$memoryID/contents", 'POST', null, $postData); if($result['result'] === 'success') { if(!empty($result['data']['content_id'])) return $result['data']['content_id']; $contentsResult = $this->callAdminAPI("/v8/memories/$memoryID/contents", 'GET'); if($contentsResult['result'] !== 'success') return null; $contentList = empty($contentsResult['data']) ? array() : $contentsResult['data']; foreach($contentList as $content) { if($content['key'] === $key) return $content['id']; } } return null; } /** * 删除知识内容。 * Delete knowledge content. * * @access public * @param string $memoryID * @param string $contentID * @return bool */ public function deleteKnowledgeItem($memoryID, $contentID) { $result = $this->callAdminAPI("/v8/memories/$memoryID/contents/$contentID", 'DELETE'); return $result['result'] === 'success'; } /** * 获取知识内容块列表。 * Get list of knowledge content chunks. * * @access public * @param string $memoryID * @param string $contentID * @return array|null */ public function getKnowledgeChunks($memoryID, $contentID) { $result = $this->callAdminAPI("/v8/memories/$memoryID/contents/$contentID/chunks", 'GET'); if($result['result'] !== 'success') return null; return empty($result['data']) ? array() : $result['data']; } /** * 搜索知识库。 * Search knowledge base. * * @access public * @param string $query * @param string $collection * @param array $filter * @param int $limit * @param float $minSimilarity * @param bool $filterByPriv * @return array */ public function searchKnowledges($query, $collection, $filter, $limit = 20, $minSimilarity = 0.5, $filterByPriv = true) { $postData = array(); $postData['query'] = $query; $postData['limit'] = $limit; $postData['min_similarity'] = $minSimilarity; $postData['content_filter'] = $filter; $result = $this->callAdminAPI('/v8/memories/' . $collection . '/embeddings-search-contents', 'POST', null, $postData); if($result['result'] != 'success') return array(); $knowledges = empty($result['data']) ? array() : $result['data']; if($filterByPriv) $knowledges = $this->filterKnowledgesByPriv($knowledges, 'content'); return $knowledges; } /** * 搜索知识块。 * Search knowledge chunks. * * @access public * @param string $query * @param string $collection * @param array $filter * @param int $limit * @param float $minSimilarity * @param bool $filterByPriv * @return array */ public function searchKnowledgeChunks($query, $collection, $filter = array(), $limit = 20, $minSimilarity = 0.5, $filterByPriv = true) { $postData = array(); $postData['query'] = $query; $postData['limit'] = $limit; $postData['min_similarity'] = $minSimilarity; $postData['content_filter'] = $filter; $result = $this->callAdminAPI('/v8/memories/' . $collection . '/embeddings-search-chunks', 'POST', null, $postData); if($result['result'] != 'success') return array(); $chunks = empty($result['data']) ? array() : $result['data']; if($filterByPriv) $chunks = $this->filterKnowledgesByPriv($chunks, 'chunk'); return $chunks; } /** * 在多个知识库中搜索知识。 * Search knowledges in multiple collections. * * @access public * @param string $query * @param string $type * @param array $filters * @param int $limit * @param float $minSimilarity * @return array */ public function searchKnowledgesInCollections($query, $filters, $type = 'content', $limit = 20, $minSimilarity = 0.5) { $knowledges = array(); foreach($filters as $collection => $setting) { $key = $this->getCollectionKey($collection); if(empty($key)) continue; /* 不进行权限过滤,按匹配度排序后会统一过滤,减少循环次数。 */ $searchKnowledges = $type === 'chunk' ? $this->searchKnowledgeChunks($query, $key, $setting, $limit + 10, $minSimilarity, false) : $this->searchKnowledges($query, $key, $setting, $limit + 10, $minSimilarity, false); // 比预设的数目多搜索 10 个,避免因过滤导致无法匹配到。 if($searchKnowledges) $knowledges = array_merge($knowledges, $searchKnowledges); } array_multisort(array_column($knowledges, 'similarity'), SORT_DESC, $knowledges); return $this->filterKnowledgesByPriv($knowledges, $type, $limit); } /** * 判断用户是否可以查看对象。 * Check if user can view object. * * @access public * @param string $objectType * @param int $objectID * @return bool * @param mixed[]|null $attrs */ public function canViewObject($objectType, $objectID, $attrs = null) { if($this->app->user->admin) return true; if(isset(static::$objectViews[$objectType][$objectID])) return static::$objectViews[$objectType][$objectID]; if(!isset(static::$objectViews[$objectType])) static::$objectViews[$objectType] = array(); if($attrs === null) $attrs = array(); $canView = false; if($objectType === 'story' || $objectType === 'demand') { $table = $objectType === 'story' ? TABLE_STORY : TABLE_DEMAND; $product = isset($attrs['product']) ? $attrs['product'] : 0; if(!$product) $product = $this->dao->select('product')->from($table)->where('id')->eq($objectID)->fetch('product'); $canView = strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false; } elseif($objectType === 'bug') { $product = isset($attrs['product']) ? $attrs['product'] : 0; if(!$product) $product = $this->dao->select('product')->from(TABLE_BUG)->where('id')->eq($objectID)->fetch('product'); $canView = strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false; if(!$canView) { $project = isset($attrs['project']) ? $attrs['project'] : 0; if(!$project) $project = $this->dao->select('project')->from(TABLE_BUG)->where('id')->eq($objectID)->fetch('project'); $canView = strpos(',' . $this->app->user->view->projects . ',', ",$project,") !== false; } } elseif($objectType === 'task') { $project = isset($attrs['project']) ? $attrs['project'] : 0; if(!$project) $project = $this->dao->select('project')->from(TABLE_TASK)->where('id')->eq($objectID)->fetch('project'); $canView = strpos(',' . $this->app->user->view->projects . ',', ",$project,") !== false; } elseif($objectType === 'feedback') { $product = isset($attrs['product']) ? $attrs['product'] : 0; if(!$product) $product = $this->dao->select('product')->from(TABLE_FEEDBACK)->where('id')->eq($objectID)->fetch('product'); $canView = strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false; } elseif($objectType === 'doc') { $api = isset($attrs['docType']) && $attrs['docType'] === 'api'; $doc = $this->loadModel($api ? 'api' : 'doc')->getByID($objectID); $canView = $this->loadModel('doc')->checkPrivDoc($doc); } elseif($objectType === 'case') { $case = $this->loadModel('testcase')->getById($objectID); if(!$case) { static::$objectViews[$objectType][$objectID] = false; return false; } $project = isset($case->project) ? $case->project : 0; $product = isset($case->product) ? $case->product : 0; if($project && strpos(',' . $this->app->user->view->projects . ',', ",$project,") !== false) $canView = true; if(!$canView && $product && strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false) $canView = true; if($canView) { static::$objectViews[$objectType][$objectID] = true; return true; } $libID = $case->lib; if($libID) { $lib = $this->loadModel('caselib')->getByID($libID); if($lib) { $project = isset($lib->project) ? $lib->project : 0; $product = isset($lib->product) ? $lib->product : 0; if($project && strpos(',' . $this->app->user->view->projects . ',', ",$project,") !== false) $canView = true; if(!$canView && $product && strpos(',' . $this->app->user->view->products . ',', ",$product,") !== false) $canView = true; } } } static::$objectViews[$objectType][$objectID] = $canView; return $canView; } /** * 过滤出有权限的知识。 * Filter knowledges which has privilege. * * @access public * @param array $knowledges * @param string $type content|chunk * @param int $limit * @return array */ public function filterKnowledgesByPriv($knowledges, $type = 'content', $limit = 0) { $filteredKnowledges = array(); $isChunk = $type == 'chunk'; $keyName = $isChunk ? 'content_key' : 'key'; $attrsName = $isChunk ? 'content_attrs' : 'attrs'; foreach($knowledges as $knowledge) { $key = isset($knowledge[$keyName]) ? $knowledge[$keyName] : ''; if(empty($key)) continue; $keyParts = explode('-', $key); if(count($keyParts) < 2) continue; [$objectType, $objectID] = $keyParts; $attrs = isset($knowledge[$attrsName]) ? $knowledge[$attrsName] : null; if(!empty($attrs['objectType'])) $objectType = $attrs['objectType']; if(!empty($attrs['objectID'])) $objectID = $attrs['objectID']; if(!$this->canViewObject($objectType, $objectID, $attrs)) continue; $filteredKnowledges[] = $knowledge; if($limit > 0 && count($filteredKnowledges) >= $limit) break; } return $filteredKnowledges; } /** * 提取文件内容。 * Extract file content. * * @access public * @param int|object $file * @return array */ public function extractFileContent($file) { if(!is_object($file)) $file = $this->loadModel('file')->getByID($file); if(!is_object($file) || empty($file->realPath)) return null; $filePath = $file->realPath; $cFile = new CURLFile($filePath, null, $file->title); $result = $this->callAdminAPI('/v8/files/extract', 'POST', null, ['file' => $cFile]); if($result['result'] != 'success') return null; return $result['data']; } /** * 用户对象可查看缓存配置。 * User object view cache configuration. * * @access public * @var array */ static $objectViews = array(); /** * 同步表。 * Sync tables. * * @access public * @var array */ static $syncTables = array ( 'story' => TABLE_STORY, 'demand' => TABLE_DEMAND, 'bug' => TABLE_BUG, 'doc' => TABLE_DOC, 'design' => TABLE_DESIGN, 'feedback' => TABLE_FEEDBACK ); /** * 获取可同步的类型。 * Get syncable types. * * @access public * @return array */ public static function getSyncTypes() { global $app, $config; $types = $app->lang->zai->syncingTypeList; if($config->edition == 'open') unset($types['feedback']); if($config->edition != 'ipd') unset($types['demand']); return $types; } /** * 获取下一个同步类型。 * Get next sync type. * * @access public * @param string $currentType * @return string 下一个同步类型。The next sync type. */ public static function getNextSyncType($currentType = '') { global $app; $types = array_keys(zaiModel::getSyncTypes()); if(empty($currentType)) return $types[0]; $currentIndex = array_search($currentType, $types); if($currentIndex === false) return $types[0]; return isset($types[$currentIndex + 1]) ? $types[$currentIndex + 1] : null; } /** * 将目标对象转换为 Markdown 格式。 * Convert target object to Markdown format. * * @access public * @param string $type * @param object $target * @param array $langData * @return array */ public static function convertTargetToMarkdown($type, $target, $langData = []) { global $app; $funcName = 'convert' . ucfirst($type) . 'ToMarkdown'; if(method_exists(static::class, $funcName)) { $markdown = static::$funcName($target, $langData); } elseif($type === 'practice' || $type === 'component') { $markdown = static::convertDocToMarkdown($target); } else { $markdown = static::convertGenericToMarkdown($type, $target); } if(!isset($markdown['attrs'])) $markdown['attrs'] = array(); if(!isset($markdown['attrs']['objectType'])) $markdown['attrs']['objectType'] = $type; if(!isset($markdown['attrs']['objectID'])) $markdown['attrs']['objectID'] = $target->id; if(!isset($markdown['attrs']['objectKey'])) $markdown['attrs']['objectKey'] = $type . '-' . $target->id; return $markdown; } /** * 将通用对象转换为 Markdown。 * Convert generic object to Markdown format. * * @access public * @param string $type * @param object $target * @return array */ public static function convertGenericToMarkdown($type, $target) { global $app; $typeName = zget($app->lang->zai->syncingTypeList, $type, ucfirst($type)); $title = ''; if(isset($target->title) && $target->title !== '') $title = $target->title; elseif(isset($target->name) && $target->name !== '') $title = $target->name; $id = isset($target->id) ? $target->id : 0; $objectTitle = trim("$typeName #$id $title"); return array( 'id' => $id, 'title' => $objectTitle, 'content' => json_encode($target, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'attrs' => array('objectTitle' => $objectTitle) ); } /** * 获取字段标签。 * * @param array $langData * @param string $field * @return string */ public static function getFieldLabel($langData, $field) { return isset($langData['fields'][$field]) ? $langData['fields'][$field] : ''; } /** * 获取章节标签。 * * @param array $langData * @param string $section * @return string */ public static function getSectionLabel($langData, $section) { return isset($langData['sections'][$section]) ? $langData['sections'][$section] : ''; } /** * 格式化字段值。 * * @param array $langData * @param string $field * @param mixed $value * @return string */ public static function formatFieldValue($langData, $field, $value) { if($value === null) return ''; if(is_array($value)) { $parts = array(); foreach($value as $item) { $formatted = static::formatFieldValue($langData, $field, $item); if($formatted !== '') $parts[] = $formatted; } return $parts ? implode(',', $parts) : ''; } if(is_object($value)) { $value = json_decode(json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true); return static::formatFieldValue($langData, $field, $value); } $valueString = (string)$value; if($valueString === '') return ''; if(isset($langData['maps'][$field]) && isset($langData['maps'][$field][$valueString])) { return (string)$langData['maps'][$field][$valueString]; } return $valueString; } /** * 附加字段列表。 * * @param array $content * @param array $langData * @param array $fieldPairs * @return void */ public static function appendFieldList(&$content, $langData, $fieldPairs) { foreach($fieldPairs as $field => $value) { $label = static::getFieldLabel($langData, $field); if($label === '') continue; $formatted = static::formatFieldValue($langData, $field, $value); $content[] = "* {$label}: {$formatted}"; } } /** * 附加详情章节。 * * @param array $content * @param array $langData * @param string $sectionKey * @param mixed $rawValue * @return void */ public static function appendDetailSection(&$content, $langData, $sectionKey, $rawValue) { if($rawValue === null) return; if(is_array($rawValue) || is_object($rawValue)) { $rawValue = json_encode($rawValue, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } $text = trim((string)$rawValue); $plain = $text === '' ? '' : trim(strip_tags($text)); $label = static::getSectionLabel($langData, $sectionKey); if($label === '') $label = static::getFieldLabel($langData, $sectionKey); if($label === '') return; $content[] = "\n## {$label}\n\n" . $plain; } /** * 附加里程碑章节。 * * @param array $content * @param array $langData * @param mixed $milestones * @return void */ public static function appendMilestoneSection(&$content, $langData, $milestones) { if(empty($milestones)) return; $label = static::getSectionLabel($langData, 'milestone'); if($label === '') $label = static::getFieldLabel($langData, 'milestone'); if($label === '') return; $content[] = "\n## {$label}"; foreach((array)$milestones as $milestone) { if(is_object($milestone)) $milestone = (array)$milestone; $name = isset($milestone['name']) ? trim((string)$milestone['name']) : ''; $date = isset($milestone['date']) ? trim((string)$milestone['date']) : ''; $line = $name; if($date !== '') $line = $line === '' ? $date : "{$line} ({$date})"; if($line !== '') $content[] = "- {$line}"; } } /** * 构建用例步骤文本。 * Build test case steps markdown text. * * @param mixed $steps * @param array $langData * @return string */ public static function buildCaseStepsText($steps, $langData) { if(empty($steps)) return ''; if(is_string($steps)) return trim($steps); if(is_object($steps)) $steps = json_decode(json_encode($steps, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true); if(!is_array($steps)) return ''; $lines = array(); $index = 1; $expectLabel = static::getFieldLabel($langData, 'expect'); if($expectLabel === '') $expectLabel = static::getFieldLabel($langData, 'expects'); foreach($steps as $step) { if(is_object($step)) $step = (array)$step; if(!is_array($step)) continue; $desc = trim((string)($step['desc'] ?? $step['title'] ?? $step['step'] ?? '')); $expect = trim((string)($step['expect'] ?? $step['expects'] ?? '')); $line = $desc !== '' ? "{$index}. {$desc}" : "{$index}."; if($expect !== '') { $label = $expectLabel !== '' ? $expectLabel : 'Expect'; $line .= " ({$label}: {$expect})"; } $lines[] = $line; $index++; } return $lines ? implode("\n", $lines) : ''; } /** * 收集需要展示的字段列表。 * Collect field-value pairs for markdown listing. * * @param string $objectType * @param array $langData * @param object $target * @return array */ public static function collectFieldPairs($objectType, $langData, $target) { if(empty($langData['fields']) || !is_array($langData['fields'])) return array(); $pairs = array(); $skipFields = array('desc', 'prevention', 'remedy', 'resolution', 'history', 'resolutionComment', 'steps', 'precondition', 'expect'); foreach($langData['fields'] as $fieldKey => $label) { if($fieldKey === 'actions') continue; if(in_array($fieldKey, $skipFields, true)) continue; $pairs[$fieldKey] = static::extractFieldValue($objectType, $fieldKey, $target); } return $pairs; } /** * 获取指定字段的值。 * Get field value from target with alias support. * * @param string $objectType * @param string $field * @param object $target * @return mixed */ public static function extractFieldValue($objectType, $field, $target) { $data = (array)$target; if(array_key_exists($field, $data)) return $data[$field]; $aliases = static::getFieldAliasMap($objectType); if(isset($aliases[$field])) { foreach((array)$aliases[$field] as $alias) { if(array_key_exists($alias, $data)) return $data[$alias]; } } $snake = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $field)); if($snake !== $field && array_key_exists($snake, $data)) return $data[$snake]; if(array_key_exists($field . 'Text', $data)) return $data[$field . 'Text']; return null; } /** * 获取字段别名映射。 * Get alias map for field values. * * @param string $objectType * @return array */ public static function getFieldAliasMap($objectType) { static $aliasMap = array( 'issue' => array( 'assetCreatedBy' => array('assetCreatedBy', 'createdBy', 'openedBy'), 'assetCreatedDate' => array('assetCreatedDate', 'createdDate', 'openedDate'), 'issueType' => array('issueType', 'type'), 'execution' => array('execution', 'executionName'), 'project' => array('project', 'projectName'), 'resolution' => array('resolution', 'resolutionType'), 'deadline' => array('deadline', 'deadLine'), ), 'risk' => array( 'assetCreatedBy' => array('assetCreatedBy', 'createdBy', 'openedBy'), 'assetCreatedDate' => array('assetCreatedDate', 'createdDate', 'openedDate'), 'project' => array('project', 'projectName'), 'execution' => array('execution', 'executionName'), ), 'opportunity' => array( 'assetCreatedBy' => array('assetCreatedBy', 'createdBy', 'openedBy'), 'assetCreatedDate' => array('assetCreatedDate', 'createdDate', 'openedDate'), 'project' => array('project', 'projectName'), 'opportunityType' => array('opportunityType', 'type'), ), 'plan' => array( 'begin' => array('begin', 'start', 'beginDate'), 'end' => array('end', 'finish', 'endDate'), 'owner' => array('owner', 'assignedTo'), 'stories' => array('stories', 'storyCount', 'story'), 'bugs' => array('bugs', 'bugCount', 'bug'), 'project' => array('project', 'projectName'), 'product' => array('product', 'productName'), ), 'release' => array( 'system' => array('system', 'systemName'), 'project' => array('project', 'projectName'), 'build' => array('build', 'buildName'), 'releasedDate' => array('releasedDate', 'releaseDate'), ), 'ticket' => array( 'openedBy' => array('openedBy', 'createdBy'), 'openedDate' => array('openedDate', 'createdDate'), 'subStatus' => array('subStatus'), 'closedReason' => array('closedReason'), 'project' => array('project', 'projectName'), 'product' => array('product', 'productName'), 'ticketType' => array('ticketType', 'type'), ), 'feedback' => array( 'feedbackType' => array('feedbackType', 'type'), ), 'case' => array( 'execution' => array('execution', 'executionName'), 'module' => array('module', 'moduleName'), 'story' => array('story', 'storyID'), 'lastEditedBy' => array('lastEditedBy', 'editedBy'), 'lastEditedDate'=> array('lastEditedDate', 'editedDate'), ), 'story' => array( 'plan' => array('plan', 'planTitle'), 'planTitle' => array('planTitle', 'plan'), 'openedBy' => array('openedBy', 'createdBy'), 'openedDate' => array('openedDate', 'createdDate'), 'assignedTo' => array('assignedTo', 'owner'), 'branch' => array('branch', 'branchName'), 'module' => array('module', 'moduleName'), 'stage' => array('stage', 'statusStage'), ), 'bug' => array( 'openedBy' => array('openedBy', 'createdBy'), 'openedDate' => array('openedDate', 'createdDate'), 'resolvedBy' => array('resolvedBy'), 'resolvedDate' => array('resolvedDate'), 'assignedTo' => array('assignedTo'), ), ); return $aliasMap[$objectType] ?? array(); } /** * 获取用户字段映射。 * Get user fields map for object type. * * @param string $objectType * @return array */ public static function getUserFieldsMap($objectType) { $map = array( 'story' => array('openedBy', 'assignedTo', 'reviewedBy', 'stagedBy'), 'bug' => array('openedBy', 'assignedTo', 'resolvedBy', 'closedBy', 'feedbackBy'), 'task' => array('openedBy', 'assignedTo', 'finishedBy', 'canceledBy', 'closedBy'), 'feedback' => array('openedBy', 'assignedTo', 'reviewedBy', 'closedBy', 'processedBy', 'feedbackBy'), 'ticket' => array('openedBy', 'assignedTo', 'startedBy', 'closedBy'), 'issue' => array('assetCreatedBy', 'assignedTo'), 'risk' => array('assetCreatedBy', 'assignedTo'), 'opportunity'=> array('assetCreatedBy', 'assignedTo'), 'plan' => array('owner'), 'case' => array('openedBy', 'lastEditedBy', 'lastRunner'), 'release' => array('owner'), 'demand' => array('createdBy', 'assignedTo'), 'design' => array('createdBy', 'assignedTo'), 'doc' => array('owner', 'addedBy', 'editedBy'), ); return $map[$objectType] ?? array(); } /** * 转换用户字段为真实姓名。 * Convert user account fields to realname. * * @param string $objectType * @param object $target * @return object */ public static function convertUserFieldToRealname($objectType, $target) { $userFields = static::getUserFieldsMap($objectType); if(empty($userFields)) return $target; global $app; $userModel = $app->loadTarget('user', '', 'model'); $userAccounts = array(); foreach($userFields as $field) { $value = static::extractFieldValue($objectType, $field, $target); if(!empty($value)) { $accounts = is_array($value) ? $value : explode(',', trim($value)); foreach($accounts as $account) { $account = trim($account); if($account && $account !== '') $userAccounts[$account] = $account; } } } if(empty($userAccounts)) return $target; $userPairs = $userModel->getPairs('realname,noletter', '', 0, array_keys($userAccounts)); $converted = clone $target; foreach($userFields as $field) { $value = static::extractFieldValue($objectType, $field, $target); if(empty($value)) continue; $accounts = is_array($value) ? $value : explode(',', trim($value)); $realnames = array(); foreach($accounts as $account) { $account = trim($account); if($account && isset($userPairs[$account])) { $realnames[] = $userPairs[$account]; } elseif($account) { $realnames[] = $account; } } if(!empty($realnames)) $converted->$field = implode(', ', $realnames); } return $converted; } /** * 将 STORY 对象转换为 Markdown 格式。 * Convert story object to Markdown format. * * @access public * @param object $story * @return array * @param mixed[] $langData */ public static function convertStoryToMarkdown($story, $langData = []) { global $app; $story = static::convertUserFieldToRealname('story', $story); $app->loadLang('story'); $lang = $app->lang; $spec = $app->dao->select('title,spec,verify,files,docs,docVersions')->from(TABLE_STORYSPEC)->where('story')->eq($story->id)->andWhere('version')->eq($story->version)->fetch(); if(empty($spec)) $spec = (object)array('title' => $story->title, 'spec' => '', 'verify' => '', 'files' => '', 'docs' => '', 'docVersions' => ''); $planValue = $story->plan; if(!empty($planValue)) { $planIDs = explode(',', trim($planValue)); $planNames = array(); foreach($planIDs as $planID) { $planID = trim($planID); if(empty($planID) || !is_numeric($planID)) continue; $plan = $app->dao->select('title')->from(TABLE_PRODUCTPLAN)->where('id')->eq($planID)->fetch(); if($plan && !empty($plan->title)) { $planNames[] = $plan->title; } else { $planNames[] = $planID; } } if(!empty($planNames)) { $planValue = implode(', ', $planNames); } } $content = array(); $content[] = "# {$lang->SRCommon} #$story->id $story->title\n"; $content[] = "## {$lang->story->legendBasicInfo}\n"; $content[] = "* {$lang->story->status}: " . zget($lang->story->statusList, $story->status); $content[] = "* {$lang->story->stage}: " . zget($lang->story->stageList, $story->stage); $content[] = "* {$lang->story->pri}: " . zget($lang->story->priList, $story->pri); $content[] = "* {$lang->story->version}: $story->version"; $content[] = "* {$lang->story->category}: " . zget($lang->story->categoryList, $story->category); $content[] = "* {$lang->story->source}: " . zget($lang->story->sourceList, $story->source); $content[] = "* {$lang->story->estimate}: $story->estimate"; $content[] = "* {$lang->story->product}: $story->product"; $content[] = "* {$lang->story->plan}: $planValue"; $content[] = "* {$lang->story->branch}: $story->branch"; $content[] = "* {$lang->story->parent}: " . (is_array($story->parent) ? implode(',', $story->parent) : $story->parent); $content[] = "* {$lang->story->module}: $story->module"; $content[] = "* {$lang->story->keywords}: $story->keywords"; $content[] = "* {$lang->story->assign}: $story->assignedTo"; $content[] = "* {$lang->story->assignedDate}: $story->assignedDate"; $content[] = "* {$lang->story->reviewedDate}: $story->reviewedDate"; $content[] = "* {$lang->story->reviewedBy}: $story->reviewedBy"; $content[] = "* {$lang->story->openedBy}: $story->openedBy"; $content[] = "* {$lang->story->openedDate}: $story->openedDate"; $content[] = "* {$lang->story->stagedBy}: $story->stagedBy"; $content[] = "\n## {$lang->story->spec}\n"; $content[] = strip_tags($spec->spec) . "\n"; $content[] = "## {$lang->story->verify}\n"; $content[] = strip_tags($spec->verify) . "\n"; $markdown = array('id' => $story->id, 'title' => "$lang->SRCommon #$story->id $spec->title"); $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array( 'product' => $story->product, 'parentStory' => $story->parent, 'productModule' => $story->module, 'productBranch' => $story->branch, 'productPlan' => $planValue, 'status' => $story->status, 'stage' => $story->stage ); if(isset($story->lib)) $markdown['attrs']['lib'] = $story->lib; return $markdown; } /** * 将 CASE 对象转换为 Markdown 格式。 * Convert case object to Markdown format. * * @access public * @param object $case * @param array $langData * @return array */ public static function convertCaseToMarkdown($case, $langData = []) { $case = static::convertUserFieldToRealname('case', $case); $id = $case->id ?? 0; $title = trim((string)($case->title ?? $case->name ?? '')); $typeName = isset($langData['common']) ? $langData['common'] : ''; $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title"); if($header === '') $header = "#$id $title"; $markdown = array('id' => $id, 'title' => $header); $content = array(); $content[] = "# {$header}\n"; $sectionBasic = static::getSectionLabel($langData, 'basic'); if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n"; $fieldPairs = static::collectFieldPairs('case', $langData, $case); static::appendFieldList($content, $langData, $fieldPairs); static::appendDetailSection($content, $langData, 'precondition', $case->precondition ?? null); $stepsText = static::buildCaseStepsText($case->steps ?? null, $langData); if($stepsText !== '') static::appendDetailSection($content, $langData, 'steps', $stepsText); static::appendDetailSection($content, $langData, 'expect', $case->expect ?? ($case->expects ?? null)); $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array( 'product' => $case->product ?? '', 'module' => $case->module ?? '', 'project' => $case->project ?? '', 'execution' => $case->execution ?? '', 'branch' => $case->branch ?? '', 'path' => $case->path ?? '', 'story' => $case->story ?? '', 'storyVersion' => $case->storyVersion ?? '', 'pri' => $case->pri ?? '', 'type' => $case->type ?? '', 'status' => $case->status ?? '', 'stage' => $case->stage ?? '', 'lib' => $case->lib ?? '' ); return $markdown; } /** * 将 DEMAND 对象转换为 Markdown 格式。 * Convert demand object to Markdown format. * * @access public * @param object $demand * @return array * @param mixed[] $langData */ public static function convertDemandToMarkdown($demand, $langData = []) { global $app; $demand = static::convertUserFieldToRealname('demand', $demand); $app->loadLang('demand'); $lang = $app->lang; $spec = $app->dao->select('title,spec,verify')->from(TABLE_DEMANDSPEC)->where('demand')->eq($demand->id)->andWhere('version')->eq($demand->version)->fetch(); if(empty($spec)) $spec = (object)array('title' => $demand->title, 'spec' => '', 'verify' => ''); $markdown = array('id' => $demand->id, 'title' => "$lang->SRCommon #$demand->id $spec->title"); $content = array(); $content[] = "# {$lang->demand->common} #$demand->id $demand->title\n"; $content[] = "## {$lang->demand->legendBasicInfo}\n"; $content[] = "* {$lang->demand->status}: " . zget($lang->demand->statusList, $demand->status); $content[] = "* {$lang->demand->stage}: " . zget($lang->demand->stageList, $demand->stage); $content[] = "* {$lang->demand->pri}: " . zget($lang->demand->priList, $demand->pri); $content[] = "* {$lang->demand->version}: $demand->version"; $content[] = "* {$lang->demand->category}: " . zget($lang->demand->categoryList, $demand->category); $content[] = "* {$lang->demand->source}: " . zget($lang->demand->sourceList, $demand->source); $content[] = "* {$lang->demand->product}: $demand->product"; $content[] = "* {$lang->demand->parent}: $demand->parent"; $content[] = "* {$lang->demand->module}: $demand->module"; $content[] = "* {$lang->demand->keywords}: $demand->keywords"; $content[] = "* {$lang->demand->assignedTo}: $demand->assignedTo"; $content[] = "* {$lang->demand->assignedDate}: $demand->assignedDate"; $content[] = "* {$lang->demand->createdBy}: $demand->createdBy"; $content[] = "* {$lang->demand->createdDate}: $demand->createdDate"; $content[] = "* {$lang->demand->changedBy}: $demand->changedBy"; $content[] = "* {$lang->demand->changedDate}: $demand->changedDate"; $content[] = "* {$lang->demand->closedBy}: $demand->closedBy"; $content[] = "* {$lang->demand->closedDate}: $demand->closedDate"; $content[] = "* {$lang->demand->closedReason}: $demand->closedReason"; $content[] = "* {$lang->demand->submitedBy}: $demand->submitedBy"; $content[] = "* {$lang->demand->distributedBy}: $demand->distributedBy"; $content[] = "* {$lang->demand->distributedDate}: $demand->distributedDate"; $content[] = "## {$lang->demand->spec}\n"; $content[] = strip_tags($spec->spec) . "\n"; $content[] = "## {$lang->demand->verify}\n"; $content[] = strip_tags($spec->verify) . "\n"; $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array('product' => $demand->product, 'parentDemand' => $demand->parent, 'productModule' => $demand->module, 'status' => $demand->status, 'stage' => $demand->stage); return $markdown; } /** * 将 BUG 对象转换为 Markdown 格式。 * Convert bug object to markdown format. * * @access public * @param object $bug * @return array * @param mixed[] $langData */ public static function convertBugToMarkdown($bug, $langData = []) { global $app; $bug = static::convertUserFieldToRealname('bug', $bug); $app->loadLang('bug'); $lang = $app->lang; $markdown = array('id' => $bug->id, 'title' => "{$lang->bug->common} #$bug->id $bug->title"); $content = array(); $content[] = "# {$lang->bug->common} #$bug->id $bug->title\n"; $content[] = "## {$lang->bug->legendBasicInfo}\n"; $content[] = "* {$lang->bug->pri}: " . zget($lang->bug->priList, $bug->pri); $content[] = "* {$lang->bug->severity}: " . zget($lang->bug->severityList, $bug->severity); $content[] = "* {$lang->bug->status}: " . zget($lang->bug->statusList, $bug->status); $content[] = "* {$lang->bug->resolution}: " . zget($lang->bug->resolutionList, $bug->resolution); $content[] = "* {$lang->bug->type}: " . zget($lang->bug->typeList, $bug->type); $content[] = "* {$lang->bug->product}: $bug->product"; $content[] = "* {$lang->bug->project}: $bug->project"; $content[] = "* {$lang->bug->execution}: $bug->execution"; $content[] = "* {$lang->bug->module}: $bug->module"; $content[] = "* {$lang->bug->branch}: $bug->branch"; $content[] = "* {$lang->bug->plan}: $bug->plan"; $content[] = "* {$lang->bug->story}: $bug->story"; $content[] = "* {$lang->bug->relatedBug}: $bug->relatedBug"; $content[] = "* {$lang->bug->keywords}: $bug->keywords"; $content[] = "* {$lang->bug->resolvedBy}: $bug->resolvedBy"; $content[] = "* {$lang->bug->resolvedDate}: $bug->resolvedDate"; $content[] = "* {$lang->bug->resolvedBuild}: $bug->resolvedBuild"; $content[] = "* {$lang->bug->openedBy}: $bug->openedBy"; $content[] = "* {$lang->bug->openedDate}: $bug->openedDate"; $content[] = "* {$lang->bug->openedBuild}: $bug->openedBuild"; $content[] = "* {$lang->bug->assignedTo}: $bug->assignedTo"; $content[] = "* {$lang->bug->assignedDate}: $bug->assignedDate"; $content[] = "* {$lang->bug->closedBy}: $bug->closedBy"; $content[] = "* {$lang->bug->closedDate}: $bug->closedDate"; $content[] = "* {$lang->bug->feedbackBy}: $bug->feedbackBy"; $content[] = "* {$lang->bug->activatedDate}: $bug->activatedDate"; $content[] = "\n## {$lang->bug->steps}\n"; $content[] = strip_tags($bug->steps) . "\n"; $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array('product' => $bug->product, 'module' => $bug->module, 'branch' => $bug->branch, 'plan' => $bug->plan, 'relatedBug' => $bug->relatedBug, 'story' => $bug->story, 'task' => $bug->task); return $markdown; } /** * 将任务对象转换为 Markdown 格式。 * Convert task object to markdown format. * * @param object $task * @access public * @return array * @param mixed[] $langData */ public static function convertTaskToMarkdown($task, $langData = []) { global $app; $task = static::convertUserFieldToRealname('task', $task); $app->loadLang('task'); $lang = $app->lang; $markdown = array('id' => $task->id, 'title' => "{$lang->task->common} #$task->id $task->name"); $content = array(); $content[] = "# {$lang->task->common} #$task->id $task->name\n"; $content[] = "## {$lang->task->legendBasic}\n"; $content[] = "* {$lang->task->project}: $task->project"; $content[] = "* {$lang->task->execution}: $task->execution"; $content[] = "* {$lang->task->module}: $task->module"; $content[] = "* {$lang->task->fromBug}: $task->fromBug"; $content[] = "* {$lang->task->feedback}: $task->feedback"; $content[] = "* {$lang->task->story}: $task->storyID"; $content[] = "* {$lang->task->assignedTo}: $task->assignedTo"; $content[] = "* {$lang->task->assignedDate}: $task->assignedDate"; $content[] = "* {$lang->task->type}: " . zget($lang->task->typeList, $task->type); $content[] = "* {$lang->task->status}: " . zget($lang->task->statusList, $task->status); $content[] = "* {$lang->task->pri}: " . zget($lang->task->priList, $task->pri); $content[] = "* {$lang->task->keywords}: $task->keywords"; $content[] = "* {$lang->task->mailto}: $task->mailto"; $content[] = "## {$lang->task->legendEffort}\n"; $content[] = "* {$lang->task->estimate}: $task->estimate"; $content[] = "* {$lang->task->consumed}: $task->consumed"; $content[] = "* {$lang->task->left}: $task->left"; $content[] = "* {$lang->task->estStarted}: $task->estStarted"; $content[] = "* {$lang->task->realStarted}: $task->realStarted"; $content[] = "* {$lang->task->deadline}: $task->deadline"; $content[] = "## {$lang->task->legendLife}\n"; $content[] = "* {$lang->task->openedBy}: $task->openedBy"; $content[] = "* {$lang->task->openedDate}: $task->openedDate"; $content[] = "* {$lang->task->finishedBy}: $task->finishedBy"; $content[] = "* {$lang->task->finishedDate}: $task->finishedDate"; $content[] = "* {$lang->task->canceledBy}: $task->canceledBy"; $content[] = "* {$lang->task->canceledDate}: $task->canceledDate"; $content[] = "* {$lang->task->closedBy}: $task->closedBy"; $content[] = "* {$lang->task->closedDate}: $task->closedDate"; $content[] = "* {$lang->task->closedReason}: $task->closedReason"; $content[] = "\n## {$lang->task->desc}\n"; $content[] = strip_tags($task->desc) . "\n"; $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array('project' => $task->project, 'module' => $task->module, 'design' => $task->design, 'fromBug' => $task->fromBug, 'story' => $task->storyID); return $markdown; } /** * 将 DOC 对象转换为 Markdown 格式。 * Convert doc object to markdown format. * * @access public * @param object $doc * @return array * @param mixed[] $langData */ public static function convertDocToMarkdown($doc, $langData = []) { global $app; $doc = static::convertUserFieldToRealname('doc', $doc); $app->loadLang('doc'); $lang = $app->lang; if(isset($doc->protocol) || !empty($doc->api)) { $app->loadLang('api'); $markdown = array('id' => $doc->id, 'title' => "{$lang->doc->common} #$doc->id $doc->title"); $content = array(); $content[] = "# {$app->lang->api->common} #$doc->id $doc->title\n"; $content[] = "## {$lang->doc->basicInfo}\n"; $content[] = "* {$lang->doc->product}: $doc->product"; $content[] = "* {$lang->doc->lib}: $doc->lib"; $content[] = "* {$app->lang->api->module}: $doc->module"; $content[] = "* {$app->lang->api->title}: $doc->title"; $content[] = "* {$app->lang->api->path}: $doc->path"; $content[] = "* {$app->lang->api->protocol}: $doc->protocol"; $content[] = "* {$app->lang->api->method}: $doc->method"; $content[] = "* {$app->lang->api->requestType}: $doc->requestType"; $content[] = "* {$app->lang->api->status}: " . zget($app->lang->api->statusOptions, $doc->status); $content[] = "* {$app->lang->api->owner}: $doc->owner"; $content[] = "* {$app->lang->api->version}: $doc->version"; if(!empty($doc->params->header)) { $content[] = "\n## {$app->lang->api->header}\n"; $content[] = "| {$app->lang->api->req->name} | {$app->lang->api->req->required} | {$app->lang->api->req->desc} |\n"; $content[] = "|------|---|---------|\n"; foreach($doc->params->header as $item) { if(is_array($item)) $item = (object)$item; if(!is_object($item)) continue; $desc = strip_tags($item->desc); $required = zget($lang->api->boolList, $item->required); $content[] = "| {$item->field} | $required | $desc |\n"; } } if(!empty($doc->params->query)) { $content[] = "\n## {$app->lang->api->query}\n"; $content[] = "| {$app->lang->api->req->name} | {$app->lang->api->req->required} | {$app->lang->api->req->desc} |\n"; $content[] = "|------|---|---------|\n"; foreach($doc->params->query as $item) { if(is_array($item)) $item = (object)$item; if(!is_object($item)) continue; $desc = strip_tags($item->desc); $required = zget($lang->api->boolList, $item->required); $content[] = "| {$item->field} | $required | $desc |\n"; } } if(!empty($doc->params->params)) { $content[] = "\n## {$app->lang->api->params}\n"; $content[] = "| {$app->lang->api->req->name} | {$app->lang->api->req->type} | {$app->lang->api->req->required} | {$app->lang->api->req->desc} |\n"; $content[] = "|------|---|---|---------|\n"; foreach($doc->params->params as $item) { if(is_array($item)) $item = (object)$item; if(!is_object($item)) continue; $desc = strip_tags($item->desc); $required = zget($lang->api->boolList, $item->required); $content[] = "| {$item->field} | {$item->paramsType} | $required | $desc |\n"; } } if(!empty($doc->paramsExample)) { $content[] = "\n## {$app->lang->api->paramsExample}\n"; $content[] = "```json\n" . htmlspecialchars_decode($doc->paramsExample) . "\n```"; } if(!empty($doc->response)) { $content[] = "\n## {$app->lang->api->response}\n"; $content[] = "| {$app->lang->api->req->name} | {$app->lang->api->req->required} | {$app->lang->api->req->desc} |\n"; $content[] = "|------|---|---------|\n"; foreach($doc->response as $item) { if(is_array($item)) $item = (object)$item; if(!is_object($item)) continue; $desc = strip_tags($item->desc); $required = zget($lang->api->boolList, $item->required); $content[] = "| {$item->field} | $required | $desc |\n"; } } if(!empty($doc->responseExample)) { $content[] = "\n## {$app->lang->api->responseExample}\n"; $content[] = "```json\n" . htmlspecialchars_decode($doc->responseExample) . "\n```"; } $content[] = "\n## {$app->lang->api->desc}\n"; $content[] = strip_tags($doc->desc) . "\n"; $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array('product' => $doc->product, 'lib' => $doc->lib, 'module' => $doc->module, 'version' => $doc->version, 'docType' => 'api'); } else { $docContent = $app->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($doc->id)->andWhere('version')->eq($doc->version)->fetch(); if(empty($docContent)) { $docContent = new stdClass(); $docContent->title = $doc->title; $docContent->content = isset($doc->content) ? $doc->content : ''; } $markdown = array('id' => $doc->id, 'title' => "{$lang->doc->common} #$doc->id $docContent->title"); $content = array(); $content[] = "# {$lang->doc->common} #$doc->id $docContent->title\n"; $content[] = "## {$lang->doc->basicInfo}\n"; $content[] = "* {$lang->doc->title}: $docContent->title"; $content[] = "* {$lang->doc->type}: " . zget($lang->doc->typeList, $doc->type); $content[] = "* {$lang->doc->product}: $doc->product"; $content[] = "* {$lang->doc->project}: $doc->project"; $content[] = "* {$lang->doc->execution}: $doc->execution"; $content[] = "* {$lang->doc->version}: $doc->version"; $content[] = "* {$lang->doc->lib}: $doc->lib"; $content[] = "* {$lang->doc->module}: $doc->module"; $content[] = "\n---\n"; $content[] = strip_tags($docContent->content) . "\n"; $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array('objectTitle' => $markdown['title'], 'product' => $doc->product, 'lib' => $doc->lib, 'module' => $doc->module, 'project' => $doc->project, 'execution' => $doc->execution, 'type' => $doc->type, 'version' => $doc->version); } return $markdown; } /** * 将 DESIGN 对象转换为 Markdown 格式。 * Convert design object to markdown format. * * @access public * @param object $design * @return array * @param mixed[] $langData */ public static function convertDesignToMarkdown($design, $langData = []) { global $app; $design = static::convertUserFieldToRealname('design', $design); $app->loadLang('design'); $lang = $app->lang; $designSpec = $app->dao->select('*')->from(TABLE_DESIGNSPEC)->where('design')->eq($design->id)->andWhere('version')->eq($design->version)->fetch(); if(empty($designSpec)) $designSpec = (object)array('name' => $design->name, 'desc' => ''); $markdown = array('id' => $design->id, 'title' => "{$lang->design->common} #$design->id $designSpec->name"); $content = array(); $content[] = "# {$lang->design->common} #$design->id $designSpec->name\n"; $content[] = "## {$lang->design->basicInfo}\n"; $content[] = "* {$lang->design->type}: " . zget($lang->design->typeList, $design->type); $content[] = "* {$lang->design->product}: $design->product"; $content[] = "* {$lang->design->project}: $design->project"; $content[] = "* {$lang->design->story}: $design->story"; $content[] = "* {$lang->design->version}: $design->version"; $content[] = "* {$lang->design->assignedTo}: $design->assignedTo"; $content[] = "* {$lang->design->createdBy}: $design->createdBy"; $content[] = "* {$lang->design->createdDate}: $design->createdDate"; $content[] = "\n## {$lang->design->desc}\n"; $content[] = strip_tags($designSpec->desc) . "\n"; $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array('objectTitle' => $markdown['title'],'product' => $design->product, 'story' => $design->story, 'project' => $design->project, 'execution' => $design->execution, 'type' => $design->type); return $markdown; } /** * 将 FEEDBACK 对象转换为 Markdown 格式。 * Convert feedback object to markdown format. * * @access public * @param object $feedback * @return array * @param mixed[] $langData */ public static function convertFeedbackToMarkdown($feedback, $langData = []) { global $app; $feedback = static::convertUserFieldToRealname('feedback', $feedback); $app->loadLang('feedback'); $lang = $app->lang; $markdown = array('id' => $feedback->id, 'title' => "{$lang->feedback->common} #$feedback->id $feedback->title"); $content = array(); $feedbackType = $feedback->feedbackType ?? $feedback->type ?? ''; $content[] = "# {$lang->feedback->common} #$feedback->id $feedback->title\n"; $content[] = "## {$lang->feedback->labelBasic}\n"; $content[] = "* {$lang->feedback->feedbackBy}: $feedback->feedbackBy"; $content[] = "* {$lang->feedback->type}: " . zget($lang->feedback->typeList, $feedbackType); $content[] = "* {$lang->feedback->pri}: " . zget($lang->feedback->priList, $feedback->pri); $content[] = "* {$lang->feedback->status}: " . zget($lang->feedback->statusList, $feedback->status); $content[] = "* {$lang->feedback->solution}: " . zget($lang->feedback->solutionList, $feedback->solution); $content[] = "* {$lang->feedback->product}: $feedback->product"; $content[] = "* {$lang->feedback->module}: $feedback->module"; $content[] = "* {$lang->feedback->openedBy}: $feedback->openedBy"; $content[] = "* {$lang->feedback->openedDate}: $feedback->openedDate"; $content[] = "* {$lang->feedback->assignedTo}: $feedback->assignedTo"; $content[] = "* {$lang->feedback->assignedDate}: $feedback->assignedDate"; $content[] = "* {$lang->feedback->reviewedBy}: $feedback->reviewedBy"; $content[] = "* {$lang->feedback->reviewedDate}: $feedback->reviewedDate"; $content[] = "* {$lang->feedback->closedBy}: $feedback->closedBy"; $content[] = "* {$lang->feedback->closedDate}: $feedback->closedDate"; $content[] = "* {$lang->feedback->closedReason}: " . zget($lang->feedback->closedReasonList, $feedback->closedReason); $content[] = "* {$lang->feedback->processedBy}: $feedback->processedBy"; $content[] = "* {$lang->feedback->processedDate}: $feedback->processedDate"; $content[] = "* {$lang->feedback->source}: $feedback->source"; $content[] = "* {$lang->feedback->result}: $feedback->result"; $content[] = "* {$lang->feedback->keywords}: $feedback->keywords"; $content[] = "* {$lang->feedback->faq}: $feedback->faq"; $content[] = "\n## {$lang->feedback->desc}\n"; $content[] = strip_tags($feedback->desc) . "\n"; $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array('objectTitle' => $markdown['title'], 'product' => $feedback->product, 'module' => $feedback->module, 'type' => $feedbackType, 'status' => $feedback->status, 'pri' => $feedback->pri); return $markdown; } /** * 将 ISSUE 对象转换为 Markdown。 * Convert issue object to Markdown format. * * @access public * @param object $issue * @param array $langData * @return array */ public static function convertIssueToMarkdown($issue, $langData = []) { $issue = static::convertUserFieldToRealname('issue', $issue); $id = $issue->id ?? 0; $title = trim((string)($issue->title ?? $issue->name ?? '')); $typeName = isset($langData['common']) ? $langData['common'] : ''; $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title"); if($header === '') $header = "#$id $title"; $markdown = array('id' => $id, 'title' => $header); $content = array(); $content[] = "# {$header}\n"; $sectionBasic = static::getSectionLabel($langData, 'basic'); if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n"; $fieldPairs = static::collectFieldPairs('issue', $langData, $issue); static::appendFieldList($content, $langData, $fieldPairs); static::appendDetailSection($content, $langData, 'desc', $issue->desc ?? null); static::appendDetailSection($content, $langData, 'solution', $issue->resolutionComment ?? null); $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array( 'objectTitle' => $markdown['title'], 'status' => $issue->status ?? '', 'pri' => $issue->pri ?? '', 'severity' => $issue->severity ?? '', 'project' => $issue->project ?? '', 'execution' => $issue->execution ?? '', 'assignedTo' => $issue->assignedTo ?? '', 'resolution' => $issue->resolution ?? '', 'issueType' => $issue->issueType ?? ($issue->type ?? ''), ); return $markdown; } /** * 将 RISK 对象转换为 Markdown。 * Convert risk object to Markdown format. * * @access public * @param object $risk * @param array $langData * @return array */ public static function convertRiskToMarkdown($risk, $langData = []) { $risk = static::convertUserFieldToRealname('risk', $risk); $id = $risk->id ?? 0; $title = trim((string)($risk->name ?? '')); $typeName = isset($langData['common']) ? $langData['common'] : ''; $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title"); if($header === '') $header = "#$id $title"; $markdown = array('id' => $id, 'title' => $header); $content = array(); $content[] = "# {$header}\n"; $sectionBasic = static::getSectionLabel($langData, 'basic'); if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n"; $fieldPairs = static::collectFieldPairs('risk', $langData, $risk); static::appendFieldList($content, $langData, $fieldPairs); static::appendDetailSection($content, $langData, 'desc', $risk->desc ?? null); static::appendDetailSection($content, $langData, 'prevention', $risk->prevention ?? null); static::appendDetailSection($content, $langData, 'remedy', $risk->remedy ?? null); $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array( 'objectTitle' => $markdown['title'], 'status' => $risk->status ?? '', 'probability' => $risk->probability ?? ($risk->chance ?? ''), 'impact' => $risk->impact ?? '', 'strategy' => $risk->strategy ?? '', 'assignedTo' => $risk->assignedTo ?? '', 'project' => $risk->project ?? '', 'execution' => $risk->execution ?? '', ); return $markdown; } /** * 将 OPPORTUNITY 对象转换为 Markdown。 * Convert opportunity object to Markdown format. * * @access public * @param object $opportunity * @param array $langData * @return array */ public static function convertOpportunityToMarkdown($opportunity, $langData = []) { $opportunity = static::convertUserFieldToRealname('opportunity', $opportunity); $id = $opportunity->id ?? 0; $title = trim((string)($opportunity->name ?? '')); $typeName = isset($langData['common']) ? $langData['common'] : ''; $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title"); if($header === '') $header = "#$id $title"; $markdown = array('id' => $id, 'title' => $header); $content = array(); $content[] = "# {$header}\n"; $sectionBasic = static::getSectionLabel($langData, 'basic'); if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n"; $fieldPairs = static::collectFieldPairs('opportunity', $langData, $opportunity); static::appendFieldList($content, $langData, $fieldPairs); static::appendDetailSection($content, $langData, 'desc', $opportunity->desc ?? null); static::appendDetailSection($content, $langData, 'prevention', $opportunity->prevention ?? null); $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array( 'objectTitle' => $markdown['title'], 'status' => $opportunity->status ?? '', 'benefit' => $opportunity->benefit ?? ($opportunity->impact ?? ''), 'chance' => $opportunity->chance ?? '', 'type' => $opportunity->type ?? '', 'strategy' => $opportunity->strategy ?? '', 'assignedTo' => $opportunity->assignedTo ?? '', 'project' => $opportunity->project ?? '', ); return $markdown; } /** * 将 PLAN 对象转换为 Markdown。 * Convert plan object to Markdown format. * * @access public * @param object $plan * @param array $langData * @return array */ public static function convertPlanToMarkdown($plan, $langData = []) { $plan = static::convertUserFieldToRealname('plan', $plan); $id = $plan->id ?? 0; $title = trim((string)($plan->title ?? $plan->name ?? '')); $typeName = isset($langData['common']) ? $langData['common'] : ''; $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title"); if($header === '') $header = "#$id $title"; $markdown = array('id' => $id, 'title' => $header); $content = array(); $content[] = "# {$header}\n"; $sectionBasic = static::getSectionLabel($langData, 'basic'); if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n"; $fieldPairs = static::collectFieldPairs('plan', $langData, $plan); static::appendFieldList($content, $langData, $fieldPairs); static::appendDetailSection($content, $langData, 'desc', $plan->desc ?? null); static::appendMilestoneSection($content, $langData, $plan->milestones ?? null); $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array( 'objectTitle' => $markdown['title'], 'status' => $plan->status ?? '', 'product' => $plan->product ?? '', 'project' => $plan->project ?? '', 'begin' => $plan->begin ?? ($plan->start ?? ''), 'end' => $plan->end ?? '', 'owner' => $plan->owner ?? ($plan->assignedTo ?? ''), ); return $markdown; } /** * 将 RELEASE 对象转换为 Markdown。 * Convert release object to Markdown format. * * @access public * @param object $release * @param array $langData * @return array */ public static function convertReleaseToMarkdown($release, $langData = []) { $release = static::convertUserFieldToRealname('release', $release); $id = $release->id ?? 0; $title = trim((string)($release->name ?? '')); $typeName = isset($langData['common']) ? $langData['common'] : ''; $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title"); if($header === '') $header = "#$id $title"; $markdown = array('id' => $id, 'title' => $header); $content = array(); $content[] = "# {$header}\n"; $sectionBasic = static::getSectionLabel($langData, 'basic'); if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n"; $fieldPairs = static::collectFieldPairs('release', $langData, $release); static::appendFieldList($content, $langData, $fieldPairs); static::appendDetailSection($content, $langData, 'desc', $release->desc ?? null); $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array( 'objectTitle' => $markdown['title'], 'system' => $release->system ?? '', 'project' => $release->project ?? '', 'build' => $release->build ?? '', 'status' => $release->status ?? '', ); return $markdown; } /** * 将 TICKET 对象转换为 Markdown。 * Convert ticket object to Markdown format. * * @access public * @param object $ticket * @param array $langData * @return array */ public static function convertTicketToMarkdown($ticket, $langData = []) { $ticket = static::convertUserFieldToRealname('ticket', $ticket); $id = $ticket->id ?? 0; $title = trim((string)($ticket->title ?? $ticket->name ?? '')); $typeName = isset($langData['common']) ? $langData['common'] : ''; $header = trim(($typeName !== '' ? "{$typeName} " : '') . "#$id $title"); if($header === '') $header = "#$id $title"; $markdown = array('id' => $id, 'title' => $header); $content = array(); $content[] = "# {$header}\n"; $sectionBasic = static::getSectionLabel($langData, 'basic'); if($sectionBasic !== '') $content[] = "## {$sectionBasic}\n"; $fieldPairs = static::collectFieldPairs('ticket', $langData, $ticket); static::appendFieldList($content, $langData, $fieldPairs); static::appendDetailSection($content, $langData, 'desc', $ticket->desc ?? null); static::appendDetailSection($content, $langData, 'resolution', $ticket->resolution ?? null); static::appendDetailSection($content, $langData, 'history', $ticket->history ?? null); $ticketType = $ticket->ticketType ?? $ticket->type ?? ''; $markdown['content'] = implode("\n", $content); $markdown['attrs'] = array( 'objectTitle' => $markdown['title'], 'status' => $ticket->status ?? '', 'type' => $ticketType, 'pri' => $ticket->pri ?? '', 'assignedTo' => $ticket->assignedTo ?? '', 'product' => $ticket->product ?? '', 'project' => $ticket->project ?? '', 'customer' => $ticket->customer ?? '', ); return $markdown; } }