Files
2025-12-08 11:33:00 +08:00

871 lines
32 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* The model file of search module of ZenTaoPMS.
*
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(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 Chunsheng Wang <chunsheng@cnezsoft.com>
* @package search
* @version $Id: model.php 5082 2013-07-10 01:14:45Z wyd621@gmail.com $
* @link https://www.zentao.net
*/
?>
<?php
class searchModel extends model
{
/**
* 构造搜索表单时调用被搜索模块的方法处理搜索参数。
* Call the method of the searched module to process search parameters when constructing the search form.
*
* @param string $module
* @param bool $cacheSearchFunc // 是否缓存构造搜索参数的方法。默认不缓存以加载真实值。Wheater to cache the method of constructing search parameters. Default is not to cache to load real values.
* @access public
* @return array
*/
public function processSearchParams(string $module, bool $cacheSearchFunc = false): array
{
$cacheKey = $module . 'SearchFunc';
$funcModel = $this->session->$cacheKey['funcModel'] ?? '';
$funcName = $this->session->$cacheKey['funcName'] ?? '';
$funcArgs = $this->session->$cacheKey['funcArgs'] ?? [];
if(!$funcModel || !$funcName || !$funcArgs) return $_SESSION[$module . 'searchParams'] ?? [];
$funcArgs['cacheSearchFunc'] = $cacheSearchFunc;
return $this->loadModel($funcModel)->$funcName(...array_values($funcArgs)); // PHP 8.0以下只能展开索引数组。PHP 8.0 below can only unpack indexed arrays.
}
/**
* 设置搜索参数的session。
* Set search params to session.
*
* @param array $searchConfig
* @access public
* @return void
*/
public function setSearchParams(array &$searchConfig)
{
$module = $searchConfig['module'];
if($this->config->edition != 'open') $searchConfig = $this->searchTao->processBuildinFields($module, $searchConfig);
$searchParams['module'] = $module;
$searchParams['actionURL'] = $searchConfig['actionURL'];
$searchParams['style'] = zget($searchConfig, 'style', 'full');
$searchParams['onMenuBar'] = zget($searchConfig, 'onMenuBar', 'no');
$searchParams['queryID'] = isset($searchConfig['queryID']) ? $searchConfig['queryID'] : 0;
if(empty($_SESSION[$module . 'SearchFunc']))
{
$searchParams['fields'] = $searchConfig['fields'];
$searchParams['params'] = $searchConfig['params'];
}
$this->session->set($module . 'searchParams', $searchParams);
}
/**
* 设置默认的搜索参数。
* Set default params for selection.
*
* @param string $module
* @param array $fields
* @param array $params
* @access public
* @return array
*/
public function setDefaultParams(string $module, array $fields, array $params): array
{
$fields = array_keys($fields);
list($users, $products, $executions) = $this->getParamValues($module, $fields, $params);
foreach($fields as $fieldName)
{
if(!isset($params[$fieldName])) $params[$fieldName] = array('operator' => '=', 'control' => 'input', 'values' => '');
if($params[$fieldName]['values'] == 'users') $params[$fieldName]['values'] = $users;
if($params[$fieldName]['values'] == 'products') $params[$fieldName]['values'] = $products;
if($params[$fieldName]['values'] == 'executions') $params[$fieldName]['values'] = $executions;
/* 处理数组。*/
/* Process array value. */
if(is_array($params[$fieldName]['values']))
{
/* For build right sql when key is 0 and is not null. e.g. confirmed field. */
if(isset($params[$fieldName]['values'][0]) and $params[$fieldName]['values'][0] !== '')
{
$params[$fieldName]['values'] = array('ZERO' => $params[$fieldName]['values'][0]) + $params[$fieldName]['values'];
unset($params[$fieldName]['values'][0]);
}
elseif(empty($params[$fieldName]['values']))
{
$params[$fieldName]['values'] = array('' => '', 'null' => $this->lang->search->null);
}
elseif(empty($params[$fieldName]['nonull']))
{
$params[$fieldName]['values'] = $params[$fieldName]['values'] + array('null' => $this->lang->search->null);
}
}
}
return $params;
}
/**
* 构建查询语句。
* Build the query to execute.
*
* @access public
* @return void
*/
public function buildQuery()
{
/* Init vars. */
$module = $this->post->module;
$searchConfig = $this->processSearchParams($module, true);
$searchFields = $searchConfig['fields'] ?? [] ;
$fieldParams = $searchConfig['params'] ?? [];
$groupItems = $this->config->search->groupItems;
$groupAndOr = strtoupper($this->post->groupAndOr);
if($groupAndOr != 'AND' && $groupAndOr != 'OR') $groupAndOr = 'AND';
$queryForm = $this->searchTao->initSession($module, $searchFields, $fieldParams);
$scoreNum = 0;
$where = '';
for($i = 1; $i <= $groupItems * 2; $i ++)
{
/* The and or between two groups. */
$formIndex = $i - 1;
if($i == 1) $where .= '(( 1 = 1 ';
if($i == $groupItems + 1) $where .= " ) $groupAndOr ( 1 = 1 ";
/* Set var names. */
$fieldName = "field$i";
$andOrName = "andOr$i";
$operatorName = "operator$i";
$valueName = "value$i";
$field = $this->post->$fieldName;
$value = $this->post->$valueName;
$fieldControl = $fieldParams[$field]['control'] ?? '';
if(empty($field) || $value === '' || $value === false) continue; // false means no exist this post item. '' means no search data. ignore it.
if(!preg_match('/^[a-zA-Z0-9]+$/', $field)) continue; // Fix sql injection.
/* 如果是输入框,并且输入框的值为'0',或者 id 的值为'0'将值设置为zero。*/
if($fieldControl == 'input' && $value === '0') $this->post->set($valueName, 'ZERO');
if($field == 'id' && $value === '0') $this->post->set($valueName, 'ZERO');
/* set queryForm. */
list($andOr, $operator, $value) = $this->searchTao->processQueryFormDatas($fieldParams, $field, $andOrName, $operatorName, $valueName);
$queryForm[$formIndex] = array('field' => $field, 'andOr' => strtolower($andOr), 'operator' => $operator, 'value' => $value);
/* Set where. */
$where = $this->searchTao->setWhere($where, $field, $operator, $value, $andOr, $fieldControl);
$scoreNum += 1;
}
foreach($queryForm as $index => $queryField)
{
if(!empty($queryForm[$index]['groupAndOr'])) $queryForm[$index]['groupAndOr'] = strtolower($groupAndOr);
}
$where .=" ))";
$where = $this->searchTao->replaceDynamic($where);
/* Save to session. */
$querySessionName = $this->post->module . 'Query';
$formSessionName = $this->post->module . 'Form';
$this->session->set($querySessionName, $where);
$this->session->set($formSessionName, $queryForm);
if($scoreNum > 2 && !dao::isError()) $this->loadModel('score')->create('search', 'saveQueryAdvanced');
}
/**
* Build the query to execute.
*
* @access public
* @return void
*/
public function buildOldQuery()
{
/* Init vars. */
$where = '';
$groupItems = $this->config->search->groupItems;
$groupAndOr = strtoupper($this->post->groupAndOr);
$module = $this->post->module;
$searchConfig = $this->processSearchParams($module, true);
$fieldParams = $searchConfig['params'] ?? [];
$scoreNum = 0;
if($groupAndOr != 'AND' and $groupAndOr != 'OR') $groupAndOr = 'AND';
for($i = 1; $i <= $groupItems * 2; $i ++)
{
/* The and or between two groups. */
if($i == 1) $where .= '(( 1 = 1 ';
if($i == $groupItems + 1) $where .= " ) $groupAndOr ( 1 = 1 ";
/* Set var names. */
$fieldName = "field$i";
$andOrName = "andOr$i";
$operatorName = "operator$i";
$valueName = "value$i";
/* Fix bug #2704. */
$field = $this->post->$fieldName;
if(!preg_match('/^[a-zA-Z0-9]+$/', $field)) continue; // Fix sql injection.
$fieldControl = $fieldParams[$field]['control'] ?? '';
if($fieldControl == 'input' and $this->post->$valueName === '0') $this->post->set($valueName, 'ZERO');
if($field == 'id' and $this->post->$valueName === '0') $this->post->set($valueName, 'ZERO');
/* Skip empty values. */
if($this->post->$valueName == false) continue;
if($this->post->$valueName == 'ZERO') $this->post->$valueName = 0; // ZERO is special, stands to 0.
if($fieldControl == 'select' and $this->post->$valueName === 'null') $this->post->set($valueName, ''); // Null is special, stands to empty if control is select. Fix bug #3279.
$scoreNum += 1;
/* Set and or. */
$andOr = strtoupper($this->post->$andOrName);
if($andOr != 'AND' and $andOr != 'OR') $andOr = 'AND';
/* Set operator. */
$value = addcslashes(trim($this->post->$valueName), '%');
$operator = $this->post->$operatorName;
if(!isset($this->lang->search->operators[$operator])) $operator = '=';
/* Set condition. */
$condition = '';
if($operator == "include")
{
if($this->post->$fieldName == 'module')
{
$allModules = $this->loadModel('tree')->getAllChildId($value);
if($allModules) $condition = helper::dbIN($allModules);
}
else
{
$condition = $fieldControl == 'select' ? " LIKE CONCAT('%,', '{$value}', ',%')" : ' LIKE ' . $this->dbh->quote("%$value%");
}
}
elseif($operator == "notinclude")
{
if($this->post->$fieldName == 'module')
{
$allModules = $this->loadModel('tree')->getAllChildId($value);
if($allModules) $condition = " NOT " . helper::dbIN($allModules);
}
else
{
$condition = $fieldControl == 'select' ? " NOT LIKE CONCAT('%,', '{$value}', ',%')" : ' NOT LIKE ' . $this->dbh->quote("%$value%");
}
}
elseif($operator == 'belong')
{
if($this->post->$fieldName == 'module')
{
$allModules = $this->loadModel('tree')->getAllChildId($value);
if($allModules) $condition = helper::dbIN($allModules);
}
elseif($this->post->$fieldName == 'dept')
{
$allDepts = $this->loadModel('dept')->getAllChildId($value);
$condition = helper::dbIN($allDepts);
}
else
{
$condition = ' = ' . $this->dbh->quote($value) . ' ';
}
}
else
{
if($operator == 'between' and !isset($this->config->search->dynamic[$value])) $operator = '=';
$condition = $operator . ' ' . $this->dbh->quote($value) . ' ';
if($operator == '=' and $this->post->$fieldName == 'id' and preg_match('/^[0-9]+(,[0-9]*)+$/', $value) and !preg_match('/[\x7f-\xff]+/', $value))
{
$values = array_filter(explode(',', trim($this->dbh->quote($value), "'")));
foreach($values as $value) $value = "'" . $value . "'";
$value = implode(',', $values);
$operator = 'IN';
$condition = $operator . ' (' . $value . ') ';
}
}
/* Processing query criteria. */
if($operator == '=' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value))
{
$condition = '`' . $this->post->$fieldName . "` >= '$value' AND `" . $this->post->$fieldName . "` <= '$value 23:59:59'";
$where .= " $andOr ($condition)";
}
elseif($operator == '!=' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value))
{
$condition = '`' . $this->post->$fieldName . "` < '$value' OR `" . $this->post->$fieldName . "` > '$value 23:59:59'";
$where .= " $andOr ($condition)";
}
elseif($operator == '<=' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value))
{
$where .= " $andOr " . '`' . $this->post->$fieldName . "` <= '$value 23:59:59'";
}
elseif($operator == '>' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value))
{
$where .= " $andOr " . '`' . $this->post->$fieldName . "` > '$value 23:59:59'";
}
elseif(in_array($operator, array('include', 'notinclude')) && $fieldControl == 'select')
{
$where .= " $andOr CONCAT(',', `{$this->post->$fieldName}`, ',') {$condition}";
}
elseif($condition)
{
$where .= " $andOr " . '`' . $this->post->$fieldName . '` ' . $condition;
}
}
$where .=" ))";
$where = $this->replaceDynamic($where);
/* Save to session. */
$querySessionName = $this->post->module . 'Query';
$formSessionName = $this->post->module . 'Form';
$this->session->set($querySessionName, $where);
$this->session->set($formSessionName, $_POST);
if($scoreNum > 2 && !dao::isError()) $this->loadModel('score')->create('search', 'saveQueryAdvanced');
}
/**
* 转换 queryForm 格式以适应 buildQuery 的检查。
* Convert queryForm to fit the buildQuery check.
*
* @param array $queryForm
* @access public
* @return array
*/
public function convertQueryForm(array $queryForm): array
{
if(isset($queryForm['field1'])) return $queryForm;
$convertedForm = array();
foreach($queryForm as $i => $formItem)
{
$i++;
if(isset($formItem['groupAndOr']))
{
$convertedForm['groupAndOr'] = $formItem['groupAndOr'];
}
elseif(isset($formItem['field']))
{
$convertedForm['field' . $i] = $formItem['field'];
$convertedForm['andOr' . $i] = $formItem['andOr'];
$convertedForm['operator' . $i] = $formItem['operator'];
$convertedForm['value' . $i] = $formItem['value'];
}
}
return $convertedForm;
}
/**
* 获取查询。
* Get a query.
*
* @param int $queryID
* @access public
* @return object|bool
*/
public function getQuery(int $queryID): object|bool
{
$queryID = (int)$queryID;
$query = $this->dao->findByID($queryID)->from(TABLE_USERQUERY)->fetch();
if(!$query) return false;
if(in_array($query->module, $this->config->search->oldQuery)) return $this->getOldQuery($queryID);
/* Decode html encode. */
$query->form = htmlspecialchars_decode($query->form, ENT_QUOTES);
/* 如果搜索表单中值有变量把表单值放到post 表单,重新生成 query。*/
/* If form has variable, regenerate query. */
$hasDynamic = str_contains($query->form, '$');
$query->form = unserialize($query->form);
if($hasDynamic)
{
$_POST = $this->convertQueryForm($query->form);
$_POST['module'] = $query->module;
$this->buildQuery();
$querySessionName = $query->module . 'Query';
$query->sql = $_SESSION[$querySessionName];
}
/* 将queryform[filed1] 转换为 queryform[1]['field']。*/
/* Process queryform[filed1] to queryform[1]['field']. */
$queryForm = array();
if(isset($query->form['field1']))
{
foreach($query->form as $field => $value)
{
$index = substr($field, -1);
if(is_numeric($index))
{
$field = substr($field, 0, strlen($field) - 1);
$queryForm[$index][$field] = $value;
}
elseif($field == 'groupAndOr')
{
$queryForm[$field][$field] = $value;
}
}
$query->form = array_values($queryForm);
}
return $query;
}
/**
* Get a query.
*
* @param int $queryID
* @access public
* @return object
*/
public function getOldQuery(int $queryID)
{
$query = $this->dao->findByID($queryID)->from(TABLE_USERQUERY)->fetch();
if(!$query) return false;
/* Decode html encode. */
$query->form = htmlspecialchars_decode($query->form, ENT_QUOTES);
$query->sql = htmlspecialchars_decode($query->sql, ENT_QUOTES);
$hasDynamic = strpos($query->form, '$') !== false;
$query->form = unserialize($query->form);
if($hasDynamic)
{
$_POST = $query->form;
$this->buildQuery();
$querySessionName = $query->form['module'] . 'Query';
$query->sql = $_SESSION[$querySessionName];
}
return $query;
}
/**
* 将搜索条件保存到session中。
* Save query to session.
*
* @param string $module
* @param int $queryID
* @access public
* @return string
*/
public function setQuery(string $module, int $queryID = 0): string
{
$querySessionName = $module . 'Query';
$formSessionName = $module . 'Form';
$queryID = (int)$queryID;
if($queryID)
{
$query = $this->getQuery($queryID);
if($query)
{
$this->session->set($querySessionName, $query->sql);
$this->session->set($formSessionName, $query->form);
}
else
{
$this->session->set($querySessionName, ' 1 = 1');
}
}
else
{
if($this->session->$querySessionName == false) $this->session->set($querySessionName, ' 1 = 1');
}
return $this->session->$querySessionName;
}
/**
* 获取一个查询。
* Get a query.
*
* @param int $queryID
* @access public
* @return object|false
*/
public function getByID(int $queryID): object|false
{
return $this->dao->findByID($queryID)->from(TABLE_USERQUERY)->fetch();
}
/**
* 保存当前的查询。
* Save current query to db.
*
* @access public
* @return void
*/
public function saveQuery()
{
$sqlVar = $this->post->module . 'Query';
$formVar = $this->post->module . 'Form';
$sql = $_SESSION[$sqlVar];
if(!$sql) $sql = ' 1 = 1 ';
$query = fixer::input('post')
->add('account', $this->app->user->account)
->add('form', serialize($_SESSION[$formVar]))
->add('sql', $sql)
->skipSpecial('sql,form')
->remove('onMenuBar')
->get();
if($this->post->onMenuBar) $query->shortcut = '1';
if(in_array($query->module, array('epic', 'requirement'))) $query->module = 'story'; // 用需业需保存为story
$this->dao->insert(TABLE_USERQUERY)->data($query)->autoCheck()->check('title', 'notempty')->exec();
if(dao::isError()) return false;
$queryID = $this->dao->lastInsertID();
$this->loadModel('score')->create('search', 'saveQuery', $queryID);
return $queryID;
}
/**
* 删除保存的查询。
* Delete current query from db.
*
* @param int $queryID
* @access public
* @return bool
*/
public function deleteQuery(int $queryID): bool
{
$this->dao->delete()->from(TABLE_USERQUERY)->where('id')->eq($queryID)->andWhere('account')->eq($this->app->user->account)->exec();
return !dao::isError();
}
/**
* 获取id,title 的键值对。
* Get title => id pairs of a user.
*
* @param string $module
* @access public
* @return array
*/
public function getQueryPairs(string $module): array
{
$queries = $this->dao->select('id, title')->from(TABLE_USERQUERY)->where('module')->eq($module)->andWhere('account', true)->eq($this->app->user->account)->orWhere('common')->eq(1)->markRight(1)->orderBy('id_desc')->fetchPairs();
return array('' => $this->lang->search->myQuery) + $queries;
}
/**
* 获取查询列表。
* Get query list.
*
* @param string $module
* @access public
* @return array
*/
public function getQueryList(string $module): array
{
return $this->dao->select('id, account, title')
->from(TABLE_USERQUERY)
->where('module')->eq($module)
->andWhere('account', true)->eq($this->app->user->account)
->orWhere('common')->eq(1)
->markRight(1)
->orderBy('id_desc')
->fetchAll();
}
/**
* 获取可访问的有索引的模块。
* Get counts of keyword search results.
*
* @param string|array $type
* @access public
* @return array
*/
public function getListCount(array|string $type = 'all'): array
{
$allowedObjects = $this->searchTao->getAllowedObjects($type);
$filterObjects = array();
foreach($allowedObjects as $index => $object)
{
if(strpos(',feedback,ticket,', ",$object,") === false) continue;
unset($allowedObjects[$index]);
$filterObjects[] = $object;
}
$typeCount = $this->dao->select("objectType, COUNT(1) AS objectCount")->from(TABLE_SEARCHINDEX)
->where('((vision')->eq($this->config->vision)
->andWhere('objectType')->in($allowedObjects)
->markRight(1)
->orWhere('(objectType')->in($filterObjects)
->markRight(2)
->andWhere('addedDate')->le(helper::now())
->groupBy('objectType')
->fetchPairs();
arsort($typeCount);
return $typeCount;
}
/**
* 获取搜索结果。
* get search results of keywords.
*
* @param string $keywords
* @param string $type
* @param object $pager
* @access public
* @return array
*/
public function getList(string $keywords, array|string $type, ?object $pager = null): array
{
$filter = $this->app->loadClass('sqlfilter');
$keywords = $filter->getRecommendedFilter($keywords, 'medium');
list($words, $againstCond, $likeCondition) = $this->searchTao->getSqlParams($keywords);
$allowedObjects = $this->searchTao->getAllowedObjects($type);
$filterObjects = array();
foreach($allowedObjects as $index => $object)
{
if(strpos(',feedback,ticket,', ",$object,") === false) continue;
unset($allowedObjects[$index]);
$filterObjects[] = $object;
}
if(in_array($this->config->db->driver, $this->config->pgsqlDriverList))
{
$table = "SELECT *, ts_rank(to_tsvector('pg_catalog.english', coalesce(title, '') || ' ' || coalesce(content, '')), to_tsquery('pg_catalog.english', '{$againstCond}')) AS score FROM " . TABLE_SEARCHINDEX;
$score = '0.02';
}
else
{
$table = "SELECT *, (MATCH(title, content) AGAINST('{$againstCond}' IN BOOLEAN MODE)) AS score FROM " . TABLE_SEARCHINDEX;
$score = '1';
}
$stmt = $this->dao->select('*')->from("({$table})")->alias('t1')
->where("(score >= {$score} {$likeCondition})")
->andWhere('((vision')->eq($this->config->vision)
->andWhere('objectType')->in($allowedObjects)
->markRight(1)
->orWhere('(objectType')->in($filterObjects)
->markRight(2)
->andWhere('addedDate')->le(helper::now())
->orderBy('score_desc, editedDate_desc')
->query();
$results = array();
$idListGroup = array();
while($record = $stmt->fetch())
{
$results[$record->id] = $record;
$module = $record->objectType == 'case' ? 'testcase' : $record->objectType;
$idListGroup[$module][$record->objectID] = $record->objectID;
}
$results = $this->searchTao->checkPriv($results, $idListGroup);
if(empty($results)) return $results;
/* Reset pager total and get this page data. */
if($pager) $results = $this->searchTao->setResultsInPage($results, $pager);
$objectList = $this->searchTao->getobjectList($idListGroup);
return $this->processResults($results, $objectList, $words);
}
/**
* 保存一个索引项。
* Save an index item.
*
* @param string $objectType article|blog|page|product|thread|reply|
* @param object $object
* @access public
* @return bool
*/
public function saveIndex(string $objectType, object $object): bool
{
$fields = $this->config->search->fields->{$objectType};
if(empty($fields)) return true;
/* 如果是文档,将文档的内容追加上。*/
/* If the objectType is doc, append the content of doc. */
if($objectType == 'doc' && $this->config->edition != 'open') $object = $this->searchTao->appendFiles($object);
$index = new stdclass();
$index->objectType = $objectType;
$index->objectID = $object->{$fields->id};
$index->title = $object->{$fields->title};
$index->addedDate = !empty($object->{$fields->addedDate}) ? (!helper::isZeroDate($object->{$fields->addedDate}) ? $object->{$fields->addedDate} : NULL) : NULL;
$index->editedDate = !empty($object->{$fields->editedDate}) ? (!helper::isZeroDate($object->{$fields->editedDate}) ? $object->{$fields->editedDate} : NULL) : NULL;
$index->vision = isset($object->vision) ? $object->vision : 'rnd';
$index->content = '';
$contentFields = explode(',', $fields->content . ',comment');
foreach($contentFields as $field)
{
if(empty($field)) continue;
$index->content .= $object->$field;
}
$spliter = $this->app->loadClass('spliter');
$titleSplited = $spliter->utf8Split($index->title);
$contentSplited = $spliter->utf8Split(strip_tags($index->content));
$index->title = $titleSplited['words'];
$index->content = $contentSplited['words'];
$this->saveDict($titleSplited['dict'] + $contentSplited['dict']);
$this->dao->delete()->from(TABLE_SEARCHINDEX)->where('objectType')->eq($index->objectType)->andWhere('objectID')->eq($index->objectID)->exec();
$this->dao->insert(TABLE_SEARCHINDEX)->data($index)->exec();
return !dao::isError();
}
/**
* 保存搜索字典。
* Save dict info.
*
* @param array $dict
* @access public
* @return bool
*/
public function saveDict(array $dict): bool
{
static $savedDict;
if(empty($savedDict)) $savedDict = $this->dao->select("`key`")->from(TABLE_SEARCHDICT)->fetchPairs();
foreach($dict as $key => $value)
{
if(!is_numeric($key) || empty($value) || strlen($key) != 5 || $key < 0 || $key > 65535) continue;
if(isset($savedDict[$key])) continue;
$this->dao->replace(TABLE_SEARCHDICT)->data(array('key' => $key, 'value' => $value))->exec();
$savedDict[$key] = $key;
}
return !dao::isError();
}
/**
* 构建索引查询。
* Build index query.
*
* @param string $type
* @param bool $testDelete
* @access public
* @return object
*/
public function buildIndexQuery(string $type, bool $testDeleted = true): object
{
$table = $this->config->objectTables[$type];
if($type == 'story' || $type == 'requirement' || $type == 'epic')
{
$query = $this->dao->select('DISTINCT t1.*, t2.spec, t2.verify')->from($table)->alias('t1')
->leftJoin(TABLE_STORYSPEC)->alias('t2')->on('t1.id=t2.story')
->where('t1.deleted')->eq('0')
->andWhere('type')->eq($type)
->andWhere('t1.version=t2.version');
}
elseif($type == 'doc')
{
$query = $this->dao->select('DISTINCT t1.*, t2.content, t2.digest')->from($table)->alias('t1')->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id=t2.doc')->where('t1.deleted')->eq('0')->andWhere('t1.version=t2.version');
}
else
{
$data = '';
if($testDeleted) $data = $this->dao->select('*')->from($table)->limit(1)->fetch();
$query = $this->dao->select('t1.*')->from($table)->alias('t1')
->where('1=1')
->beginIF($type == 'program')->andWhere('type')->eq('program')->fi()
->beginIF($type == 'project')->andWhere('type')->eq('project')->fi()
->beginIF($type == 'execution')->andWhere('type')->in('stage,sprint,kanban')->fi()
->beginIF(isset($data->deleted))->andWhere('t1.deleted')->eq('0')->fi();
}
return $query;
}
/**
* 构建索引。
* Build all search index.
*
* @param string $type
* @param int $lastID
* @access public
* @return array
*/
public function buildAllIndex(string $type = '', int $lastID = 0): array
{
/* 如果类型是空的,获取第一个类型,从第一个类型开始创建索引。*/
/* If the type is empty, get the first type and create index starting from the first type. */
if(empty($type))
{
$this->dao->delete()->from(TABLE_SEARCHINDEX)->exec();
$this->dao->delete()->from(TABLE_SEARCHDICT)->exec();
try
{
$this->dbh->exec('ALTER TABLE ' . TABLE_SEARCHINDEX . ' auto_increment=1');
}
catch(Exception $e){}
$type = key((array)$this->config->search->fields);
}
$limit = 100;
$nextObject = false;
/* 获取某些字段值并且将其保存到索引。*/
/* Get some field value and save it to index. */
foreach($this->config->search->fields as $module => $field)
{
if($module != $type && !$nextObject) continue;
if(!isset($this->config->objectTables[$module])) continue;
if($module == $type) $nextObject = true;
while(true)
{
$query = $this->buildIndexQuery($module);
$dataList = $query->beginIF($lastID)->andWhere('t1.id')->gt($lastID)->fi()->orderBy('t1.id')->limit($limit)->fetchAll('id', false);
if(empty($dataList))
{
$lastID = 0;
break;
}
$dataList = $this->searchTao->processDataList($module, $field, $dataList);
foreach($dataList as $data) $this->saveIndex($module, $data);
return array('type' => $module, 'count' => count($dataList), 'lastID' => max(array_keys($dataList)));
}
}
return array('finished' => true);
}
/**
* 删除索引。
* Delete index of an object.
*
* @param string $objectType
* @param int $objectID
* @access public
* @return bool
*/
public function deleteIndex(string $objectType, int $objectID): bool
{
$this->dao->delete()->from(TABLE_SEARCHINDEX)->where('objectType')->eq($objectType)->andWhere('objectID')->eq($objectID)->exec();
return !dao::isError();
}
}