2165 lines
86 KiB
PHP
2165 lines
86 KiB
PHP
<?php
|
||
/**
|
||
* The model file of zai module of ZenTaoPMS.
|
||
*
|
||
* @copyright Copyright 2009-2025 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
|
||
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||
* @author tenghuaian <tenghuaian@chandao.com>
|
||
* @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(string|int $libID): string
|
||
{
|
||
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(string $name, string $description = '', ?array $options = null): ?array
|
||
{
|
||
$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(string $memoryID): bool
|
||
{
|
||
$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(string $memoryID, string $key, string $content, string $contentType = 'markdown', ?array $attrs = null): ?string
|
||
{
|
||
$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(string $memoryID, string $contentID): bool
|
||
{
|
||
$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(string $memoryID, string $contentID): ?array
|
||
{
|
||
$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(string $query, string $collection, array $filter, int $limit = 20, float $minSimilarity = 0.5, bool $filterByPriv = true): array
|
||
{
|
||
$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(string $query, string $collection, array $filter = array(), int $limit = 20, float $minSimilarity = 0.5, bool $filterByPriv = true): array
|
||
{
|
||
$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(string $query, array $filters, string $type = 'content', int $limit = 20, float $minSimilarity = 0.5): array
|
||
{
|
||
$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
|
||
*/
|
||
public function canViewObject(string $objectType, int $objectID, ?array $attrs = null): bool
|
||
{
|
||
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(array $knowledges, string $type = 'content', int $limit = 0): array
|
||
{
|
||
$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(int|object $file): array|null
|
||
{
|
||
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, array $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(string $type, object $target): array
|
||
{
|
||
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(array $langData, string $field): string
|
||
{
|
||
return isset($langData['fields'][$field]) ? $langData['fields'][$field] : '';
|
||
}
|
||
|
||
/**
|
||
* 获取章节标签。
|
||
*
|
||
* @param array $langData
|
||
* @param string $section
|
||
* @return string
|
||
*/
|
||
public static function getSectionLabel(array $langData, string $section): string
|
||
{
|
||
return isset($langData['sections'][$section]) ? $langData['sections'][$section] : '';
|
||
}
|
||
|
||
/**
|
||
* 格式化字段值。
|
||
*
|
||
* @param array $langData
|
||
* @param string $field
|
||
* @param mixed $value
|
||
* @return string
|
||
*/
|
||
public static function formatFieldValue(array $langData, string $field, $value): string
|
||
{
|
||
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(array &$content, array $langData, array $fieldPairs): void
|
||
{
|
||
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(array &$content, array $langData, string $sectionKey, $rawValue): void
|
||
{
|
||
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(array &$content, array $langData, $milestones): void
|
||
{
|
||
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, array $langData): string
|
||
{
|
||
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(string $objectType, array $langData, object $target): array
|
||
{
|
||
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(string $objectType, string $field, object $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(string $objectType): array
|
||
{
|
||
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(string $objectType): array
|
||
{
|
||
$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(string $objectType, object $target): object
|
||
{
|
||
$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
|
||
*/
|
||
public static function convertStoryToMarkdown($story, array $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, array $langData = []): array
|
||
{
|
||
$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
|
||
*/
|
||
public static function convertDemandToMarkdown($demand, array $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
|
||
*/
|
||
public static function convertBugToMarkdown($bug, array $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
|
||
*/
|
||
public static function convertTaskToMarkdown($task, array $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
|
||
*/
|
||
public static function convertDocToMarkdown($doc, array $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
|
||
*/
|
||
public static function convertDesignToMarkdown($design, array $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
|
||
*/
|
||
public static function convertFeedbackToMarkdown($feedback, array $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, array $langData = []): array
|
||
{
|
||
$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, array $langData = []): array
|
||
{
|
||
$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, array $langData = []): array
|
||
{
|
||
$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, array $langData = []): array
|
||
{
|
||
$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, array $langData = []): array
|
||
{
|
||
$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, array $langData = []): array
|
||
{
|
||
$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;
|
||
}
|
||
}
|