新增精灵图贴纸类型及内置资源 优化分类图标使用SVG替代emoji 实现分页预加载功能 修复API基础地址与客户端一致 新增健康生活、国学经典服务模块 扩展Feed频道至44种并整合互动统计 修正多处UI显示问题及逻辑错误
1655 lines
72 KiB
PHP
1655 lines
72 KiB
PHP
<?php
|
||
|
||
/**
|
||
* @name 全量搜索API接口
|
||
* @author AI Coder
|
||
* @date 2026-04-29
|
||
* @desc 提供全量搜索功能,支持精确/模糊/相关搜索、条件筛选、ID反查、ID查询,聚合18+种数据源
|
||
* @update v1.1 统一getById/getByIds返回格式与Feed detail一致; 新增fullDetail/relatedRecommend/highlight接口; 补充createtime/互动计数/交互状态等APP扩展字段
|
||
*/
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\controller\Api;
|
||
use think\Db;
|
||
use think\Cache;
|
||
|
||
class Searchall extends Api
|
||
{
|
||
protected $noNeedLogin = ['*'];
|
||
protected $noNeedRight = ['*'];
|
||
|
||
private static $sourceMap = [
|
||
'poetry' => ['table' => 'poetry', 'name' => '古诗词', 'icon' => '📜', 'search' => ['name','content','author'], 'return' => ['id','name as title','author','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'wisdom' => ['table' => 'wisdom', 'name' => '名言金句', 'icon' => '💡', 'search' => ['name','content'], 'return' => ['id','name as author','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'story' => ['table' => 'story', 'name' => '故事', 'icon' => '📚', 'search' => ['title','content'], 'return' => ['id','title','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'hitokoto' => ['table' => 'hitokoto', 'name' => '一言', 'icon' => '💬', 'search' => ['hitokoto','from_source'], 'return' => ['id','hitokoto as content','from_source','from_who as author','type_name','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'riddle' => ['table' => 'riddle', 'name' => '谜语', 'icon' => '🧩', 'search' => ['riddle','interpret'], 'return' => ['id','riddle as title','interpret as answer','miidii as hint','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'efs' => ['table' => 'efs', 'name' => '歇后语', 'icon' => '🎭', 'search' => ['facet','undertone'], 'return' => ['id','facet as title','undertone as content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'brainteaser' => ['table' => 'brainteaser', 'name' => '脑筋急转弯', 'icon' => '🧠', 'search' => ['topic','answer'], 'return' => ['id','topic as title','answer','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'saying' => ['table' => 'saying', 'name' => '俗语', 'icon' => '🗣️', 'search' => ['saying','content'], 'return' => ['id','saying as title','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'lyric' => ['table' => 'lyric', 'name' => '歌词', 'icon' => '🎵', 'search' => ['title','singer','content'],'return' => ['id','title','singer as author','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'why' => ['table' => 'why', 'name' => '十万个为什么','icon' => '❓', 'search' => ['title','content'], 'return' => ['id','title','content','views'], 'status' => ['field'=>'status','value'=>0]],
|
||
'composition' => ['table' => 'composition', 'name' => '作文', 'icon' => '✍️', 'search' => ['title','content'], 'return' => ['id','title','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'couplet' => ['table' => 'couplet', 'name' => '对联', 'icon' => '🏮', 'search' => ['hp','sl'], 'return' => ['id','hp as upper','sl as lower','xl as horizontal','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'cs' => ['table' => 'cs', 'name' => '常识', 'icon' => '📖', 'search' => ['title','content'], 'return' => ['id','title','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'drug' => ['table' => 'drug', 'name' => '药品', 'icon' => '💊', 'search' => ['name','goods_name'], 'return' => ['id','name','goods_name','syz as indication','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'herbal' => ['table' => 'herbal', 'name' => '中草药', 'icon' => '🌿', 'search' => ['name','name_alias'], 'return' => ['id','name','name_alias','effect','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'food' => ['table' => 'food', 'name' => '食物', 'icon' => '🍽️', 'search' => ['sw'], 'return' => ['id','sw as name','yh as effect','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'wine' => ['table' => 'wine', 'name' => '酒方', 'icon' => '🍷', 'search' => ['name','effect'], 'return' => ['id','name as title','effect as content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'article' => ['table' => 'article', 'name' => '文章', 'icon' => '📰', 'search' => ['title','summary'], 'return' => ['id','title','summary','views','likes','user_id'], 'status' => ['field'=>'status','value'=>'normal']],
|
||
'chengyu' => ['table' => 'cy', 'name' => '成语', 'icon' => '🔤', 'search' => ['cy','cyjs','cyzy'], 'return' => ['id','cy as title','cypy as pinyin','cyjs as content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'hanzi' => ['table' => 'hanzi', 'name' => '汉字', 'icon' => '🈯', 'search' => ['zi','pinyin','bushou'], 'return' => ['id','zi as title','pinyin','wubi','bushou','bihua','pv as views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'cidian' => ['table' => 'zc', 'name' => '词典', 'icon' => '📚', 'search' => ['zc','zcjs'], 'return' => ['id','zc as title','zcpy as pinyin','zcjs as content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'prescription'=> ['table' => 'prescription','name' => '偏方', 'icon' => '🧪', 'search' => ['title','content'], 'return' => ['id','title','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'tisana' => ['table' => 'tisana', 'name' => '药茶', 'icon' => '🍵', 'search' => ['name','effect'], 'return' => ['id','name as title','effect as content','recipe','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'joke' => ['table' => 'joke', 'name' => '笑话', 'icon' => '😄', 'search' => ['title','content'], 'return' => ['id','title','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'zgjm' => ['table' => 'zgjm', 'name' => '周公解梦', 'icon' => '🌙', 'search' => ['name','content'], 'return' => ['id','name as title','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'lunyu' => ['table' => 'lunyu', 'name' => '论语', 'icon' => '📖', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'hdnj' => ['table' => 'hdnj', 'name' => '黄帝内经', 'icon' => '⚕️', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'jgj' => ['table' => 'jgj', 'name' => '金刚经', 'icon' => '📿', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'mz' => ['table' => 'mz', 'name' => '孟子', 'icon' => '📜', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'zz' => ['table' => 'zz', 'name' => '庄子', 'icon' => '🦋', 'search' => ['original','translation'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'zuozhuan' => ['table' => 'zuozhuan', 'name' => '左传', 'icon' => '竹', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'sj' => ['table' => 'sj', 'name' => '史记', 'icon' => '🏛️', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'sgz' => ['table' => 'sgz', 'name' => '三国志', 'icon' => '⚔️', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'sbbf' => ['table' => 'sbbf', 'name' => '孙膑兵法', 'icon' => '🗡️', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'warring' => ['table' => 'warring', 'name' => '兵法', 'icon' => '🛡️', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'illness' => ['table' => 'illness', 'name' => '疾病', 'icon' => '🩺', 'search' => ['jb','zz','yqys'], 'return' => ['id','jb as title','zz as content','yqys','zlff','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'word' => ['table' => 'word', 'name' => '英语单词', 'icon' => '🔤', 'search' => ['word','jbjs'], 'return' => ['id','word as title','british','american','jbjs as content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'abbr' => ['table' => 'abbr', 'name' => '缩写', 'icon' => '📝', 'search' => ['abbr','full','meaning'], 'return' => ['id','abbr as title','full','meaning as content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'surname' => ['table' => 'surname', 'name' => '姓氏', 'icon' => '👤', 'search' => ['surname','content'], 'return' => ['id','surname as title','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'jieqi' => ['table' => 'jieqi', 'name' => '节气', 'icon' => '🌤️', 'search' => ['name','content'], 'return' => ['id','name as title','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'nation' => ['table' => 'nation', 'name' => '国家', 'icon' => '🌍', 'search' => ['title','content'], 'return' => ['id','title','capital','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'wlyh' => ['table' => 'wlyh', 'name' => '网络用语', 'icon' => '💬', 'search' => ['title','original'], 'return' => ['id','title','original as content','translation','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'jiufang' => ['table' => 'jiufang', 'name' => '酒方', 'icon' => '🍶', 'search' => ['name','source'], 'return' => ['id','name as title','source','method as content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
'bot' => ['table' => 'bot', 'name' => '星座', 'icon' => '⭐', 'search' => ['title','chinese'], 'return' => ['id','title','chinese','content','views'], 'status' => ['field'=>'switch','value'=>1]],
|
||
];
|
||
|
||
public function _initialize()
|
||
{
|
||
if (isset($_SERVER['HTTP_ORIGIN'])) {
|
||
header('Access-Control-Expose-Headers: __token__');
|
||
}
|
||
check_cors_request();
|
||
parent::_initialize();
|
||
}
|
||
|
||
private function _validateKeyword($keyword)
|
||
{
|
||
$keyword = trim($keyword);
|
||
if (empty($keyword)) {
|
||
$this->error('请输入搜索关键词');
|
||
}
|
||
if (mb_strlen($keyword) > 100) {
|
||
$this->error('关键词不能超过100个字符');
|
||
}
|
||
if (preg_match('/[<>"\']/', $keyword)) {
|
||
$this->error('关键词包含非法字符');
|
||
}
|
||
return $keyword;
|
||
}
|
||
|
||
private function _applyStatus($query, $config)
|
||
{
|
||
$sf = $config['status']['field'];
|
||
$sv = $config['status']['value'];
|
||
if (!empty($sf)) {
|
||
$query->where($sf, $sv);
|
||
}
|
||
}
|
||
|
||
private function _formatItem($type, $row)
|
||
{
|
||
$config = self::$sourceMap[$type];
|
||
$item = [
|
||
'feed_type' => $type,
|
||
'feed_name' => $config['name'],
|
||
'feed_icon' => $config['icon'],
|
||
'id' => $row['id'] ?? 0,
|
||
'title' => '',
|
||
'author' => '',
|
||
'content' => '',
|
||
'summary' => '',
|
||
'views' => intval($row['views'] ?? 0),
|
||
'createtime' => isset($row['createtime']) ? intval($row['createtime']) : 0,
|
||
'updatetime' => isset($row['updatetime']) ? intval($row['updatetime']) : 0,
|
||
'extra' => new \stdClass(),
|
||
];
|
||
|
||
switch ($type) {
|
||
case 'poetry':
|
||
$item['title'] = $row['title'] ?? ($row['name'] ?? '');
|
||
$item['author'] = $row['author'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
$item['extra'] = [
|
||
'dynasty' => $row['dynasty'] ?? '',
|
||
'tag' => $row['tag'] ?? '',
|
||
'ywjzsy' => $row['ywjzsy'] ?? '',
|
||
'ywckzly' => $row['ywckzly'] ?? '',
|
||
'czbj' => $row['czbj'] ?? '',
|
||
'czckzl' => $row['czckzl'] ?? '',
|
||
'jsy' => $row['jsy'] ?? '',
|
||
'jsckzly' => $row['jsckzly'] ?? '',
|
||
'sxy' => $row['sxy'] ?? '',
|
||
'jx' => $row['jx'] ?? '',
|
||
'sxckzly' => $row['sxckzly'] ?? '',
|
||
'pj' => $row['pj'] ?? '',
|
||
'sxe' => $row['sxe'] ?? '',
|
||
'sxckzle' => $row['sxckzle'] ?? '',
|
||
'ywjzse' => $row['ywjzse'] ?? '',
|
||
'ywckzle' => $row['ywckzle'] ?? '',
|
||
'jse' => $row['jse'] ?? '',
|
||
'jsckzle' => $row['jsckzle'] ?? '',
|
||
'jj' => $row['jj'] ?? '',
|
||
'wyzs' => $row['wyzs'] ?? '',
|
||
'yj' => $row['yj'] ?? '',
|
||
'xzsf' => $row['xzsf'] ?? '',
|
||
'xzckzl' => $row['xzckzl'] ?? '',
|
||
'dj' => $row['dj'] ?? '',
|
||
];
|
||
break;
|
||
case 'wisdom':
|
||
$item['author'] = $row['author'] ?? ($row['name'] ?? '');
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['title'] = mb_substr($row['content'] ?? '', 0, 30) . '...';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
break;
|
||
case 'story':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
break;
|
||
case 'hitokoto':
|
||
$item['content'] = $row['content'] ?? ($row['hitokoto'] ?? '');
|
||
$item['author'] = $row['author'] ?? ($row['from_who'] ?? '');
|
||
$item['title'] = mb_substr($item['content'], 0, 30);
|
||
$item['summary'] = mb_substr($item['content'], 0, 100);
|
||
$item['extra'] = [
|
||
'from_source' => $row['from_source'] ?? '',
|
||
'type_name' => $row['type_name'] ?? '',
|
||
'hitokoto' => $row['hitokoto'] ?? '',
|
||
'from_who' => $row['from_who'] ?? '',
|
||
'uuid' => $row['uuid'] ?? '',
|
||
];
|
||
break;
|
||
case 'riddle':
|
||
$item['title'] = $row['title'] ?? ($row['riddle'] ?? '');
|
||
$item['content'] = $row['answer'] ?? ($row['interpret'] ?? '');
|
||
$item['summary'] = $item['title'];
|
||
$item['extra'] = [
|
||
'hint' => $row['hint'] ?? ($row['miidii'] ?? ''),
|
||
'riddle' => $row['riddle'] ?? '',
|
||
'interpret' => $row['interpret'] ?? '',
|
||
];
|
||
break;
|
||
case 'efs':
|
||
$item['title'] = $row['title'] ?? ($row['facet'] ?? '');
|
||
$item['content'] = $row['content'] ?? ($row['undertone'] ?? '');
|
||
$item['summary'] = $item['title'] . ' —— ' . $item['content'];
|
||
$item['extra'] = [
|
||
'facet' => $row['facet'] ?? '',
|
||
'undertone' => $row['undertone'] ?? '',
|
||
];
|
||
break;
|
||
case 'brainteaser':
|
||
$item['title'] = $row['title'] ?? ($row['topic'] ?? '');
|
||
$item['content'] = $row['answer'] ?? '';
|
||
$item['summary'] = $item['title'];
|
||
$item['extra'] = [
|
||
'topic' => $row['topic'] ?? '',
|
||
'answer' => $row['answer'] ?? '',
|
||
];
|
||
break;
|
||
case 'saying':
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['title'] = mb_substr(strip_tags($item['content']), 0, 30);
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'saying' => $row['saying'] ?? '',
|
||
];
|
||
break;
|
||
case 'lyric':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['author'] = $row['author'] ?? ($row['singer'] ?? '');
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
break;
|
||
case 'why':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
break;
|
||
case 'composition':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
break;
|
||
case 'couplet':
|
||
$item['title'] = ($row['upper'] ?? ($row['hp'] ?? '')) . ' / ' . ($row['lower'] ?? ($row['sl'] ?? ''));
|
||
$item['content'] = ($row['horizontal'] ?? ($row['xl'] ?? ''));
|
||
$item['summary'] = $item['title'];
|
||
$item['extra'] = [
|
||
'hp' => $row['hp'] ?? '',
|
||
'sl' => $row['sl'] ?? '',
|
||
'xl' => $row['xl'] ?? '',
|
||
'yy' => $row['yy'] ?? '',
|
||
];
|
||
break;
|
||
case 'cs':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
break;
|
||
case 'drug':
|
||
$item['title'] = $row['name'] ?? '';
|
||
$item['content'] = $row['syz'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'name' => $row['name'] ?? '',
|
||
'goods_name' => $row['goods_name'] ?? '',
|
||
'syz' => $row['syz'] ?? '',
|
||
'gg' => $row['gg'] ?? '',
|
||
'cf' => $row['cf'] ?? '',
|
||
'yfyl' => $row['yfyl'] ?? '',
|
||
'blfy' => $row['blfy'] ?? '',
|
||
'jj' => $row['jj'] ?? '',
|
||
'zysx' => $row['zysx'] ?? '',
|
||
'pzwh' => $row['pzwh'] ?? '',
|
||
'scqy' => $row['scqy'] ?? '',
|
||
];
|
||
break;
|
||
case 'herbal':
|
||
$item['title'] = $row['name'] ?? '';
|
||
$item['content'] = $row['effect'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'effect' => $row['effect'] ?? '',
|
||
'name_alias' => $row['name_alias'] ?? '',
|
||
'image' => $row['image'] ?? '',
|
||
'spell' => $row['spell'] ?? '',
|
||
'english_name' => $row['english_name'] ?? '',
|
||
'medicinal_parts' => $row['medicinal_parts'] ?? '',
|
||
'plant_morphology' => $row['plant_morphology'] ?? '',
|
||
'origin_distribution' => $row['origin_distribution'] ?? '',
|
||
'harvest_processing' => $row['harvest_processing'] ?? '',
|
||
'drug_properties' => $row['drug_properties'] ?? '',
|
||
'tropism_taste' => $row['tropism_taste'] ?? '',
|
||
'clinic' => $row['clinic'] ?? '',
|
||
'pharmacology' => $row['pharmacology'] ?? '',
|
||
'chemical_component' => $row['chemical_component'] ?? '',
|
||
'usage_taboo' => $row['usage_taboo'] ?? '',
|
||
'prescription' => $row['prescription'] ?? '',
|
||
];
|
||
break;
|
||
case 'food':
|
||
$item['title'] = $row['sw'] ?? '';
|
||
$item['content'] = $row['yh'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'sw' => $row['sw'] ?? '',
|
||
'yh' => $row['yh'] ?? '',
|
||
];
|
||
break;
|
||
case 'wine':
|
||
$item['title'] = $row['name'] ?? '';
|
||
$item['content'] = $row['effect'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'name' => $row['name'] ?? '',
|
||
'effect' => $row['effect'] ?? '',
|
||
'source' => $row['source'] ?? '',
|
||
'stock' => $row['stock'] ?? '',
|
||
'make' => $row['make'] ?? '',
|
||
'usages' => $row['usages'] ?? '',
|
||
'taboo' => $row['taboo'] ?? '',
|
||
];
|
||
break;
|
||
case 'article':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['summary'] ?? '';
|
||
$item['summary'] = $row['summary'] ?? '';
|
||
$item['likes'] = intval($row['likes'] ?? 0);
|
||
$item['extra'] = [
|
||
'user_id' => $row['user_id'] ?? 0,
|
||
'image' => $row['image'] ?? '',
|
||
'tags' => $row['tags'] ?? '',
|
||
'favorites' => intval($row['favorites'] ?? 0),
|
||
'comments' => intval($row['comments'] ?? 0),
|
||
'rating_sum' => intval($row['rating_sum'] ?? 0),
|
||
'rating_count' => intval($row['rating_count'] ?? 0),
|
||
];
|
||
break;
|
||
case 'chengyu':
|
||
$item['title'] = $row['cy'] ?? '';
|
||
$item['content'] = $row['cyjs'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'cy' => $row['cy'] ?? '',
|
||
'cypy' => $row['cypy'] ?? '',
|
||
'cyzy' => $row['cyzy'] ?? '',
|
||
'cycx' => $row['cycx'] ?? '',
|
||
'cynd' => $row['cynd'] ?? '',
|
||
'ywfy' => $row['ywfy'] ?? '',
|
||
'cyjs' => $row['cyjs'] ?? '',
|
||
'cycc' => $row['cycc'] ?? '',
|
||
'cylz' => $row['cylz'] ?? '',
|
||
'pinyin' => $row['cypy'] ?? '',
|
||
'origin' => $row['cyzy'] ?? '',
|
||
'example' => $row['cylz'] ?? '',
|
||
'synonym' => $row['cycx'] ?? '',
|
||
'antonym' => $row['cynd'] ?? '',
|
||
'grammar' => $row['ywfy'] ?? '',
|
||
'usage' => $row['cycc'] ?? '',
|
||
];
|
||
break;
|
||
case 'hanzi':
|
||
$item['title'] = $row['zi'] ?? '';
|
||
$item['content'] = $row['jj'] ?? '';
|
||
$item['summary'] = $item['title'];
|
||
$item['extra'] = [
|
||
'image' => $row['image'] ?? '',
|
||
'smallimage' => $row['smallimage'] ?? '',
|
||
'pinyin' => $row['pinyin'] ?? '',
|
||
'py' => $row['py'] ?? '',
|
||
'wubi' => $row['wubi'] ?? '',
|
||
'bushou' => $row['bushou'] ?? '',
|
||
'bihua' => $row['bihua'] ?? 0,
|
||
'bishun' => $row['bishun'] ?? '',
|
||
'fantizi' => $row['fantizi'] ?? '',
|
||
'zy' => $row['zy'] ?? '',
|
||
'wuxing' => $row['wuxing'] ?? '',
|
||
'jg' => $row['jg'] ?? '',
|
||
'unicode' => $row['unicode'] ?? '',
|
||
'bishunm' => $row['bishunm'] ?? '',
|
||
'xiefa' => $row['xiefa'] ?? '',
|
||
'ziyi' => $row['ziyi'] ?? '',
|
||
'zm' => $row['zm'] ?? '',
|
||
];
|
||
break;
|
||
case 'cidian':
|
||
$item['title'] = $row['zc'] ?? '';
|
||
$item['content'] = $row['zcjs'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'zc' => $row['zc'] ?? '',
|
||
'zcpy' => $row['zcpy'] ?? '',
|
||
'zcpywu' => $row['zcpywu'] ?? '',
|
||
'zczy' => $row['zczy'] ?? '',
|
||
'zcjs' => $row['zcjs'] ?? '',
|
||
'pinyin' => $row['zcpy'] ?? '',
|
||
'pinyin_wu' => $row['zcpywu'] ?? '',
|
||
'zy' => $row['zczy'] ?? '',
|
||
'szm' => $row['zcszm'] ?? '',
|
||
'cx' => $row['cx'] ?? '',
|
||
'wljs' => $row['wljs'] ?? '',
|
||
];
|
||
break;
|
||
case 'prescription':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
break;
|
||
case 'tisana':
|
||
$item['title'] = $row['name'] ?? '';
|
||
$item['content'] = $row['effect'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'effect' => $row['effect'] ?? '',
|
||
'recipe' => $row['recipe'] ?? '',
|
||
'source' => $row['source'] ?? '',
|
||
'usages' => $row['usages'] ?? '',
|
||
'taboo' => $row['taboo'] ?? '',
|
||
];
|
||
break;
|
||
case 'joke':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
break;
|
||
case 'zgjm':
|
||
$item['title'] = $row['name'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
break;
|
||
case 'lunyu':
|
||
case 'hdnj':
|
||
case 'jgj':
|
||
case 'mz':
|
||
case 'zz':
|
||
case 'zuozhuan':
|
||
case 'sj':
|
||
case 'sgz':
|
||
case 'sbbf':
|
||
case 'warring':
|
||
case 'wlyh':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['original'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'original' => $row['original'] ?? '',
|
||
'translation' => $row['translation'] ?? '',
|
||
'category_id' => $row['category_id'] ?? '',
|
||
];
|
||
break;
|
||
case 'illness':
|
||
$item['title'] = $row['jb'] ?? '';
|
||
$item['content'] = $row['zz'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'jb' => $row['jb'] ?? '',
|
||
'zz' => $row['zz'] ?? '',
|
||
'yqys' => $row['yqys'] ?? '',
|
||
'zlff' => $row['zlff'] ?? '',
|
||
];
|
||
break;
|
||
case 'word':
|
||
$item['title'] = $row['word'] ?? '';
|
||
$item['content'] = $row['jbjs'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'word' => $row['word'] ?? '',
|
||
'jbjs' => $row['jbjs'] ?? '',
|
||
'british' => $row['british'] ?? '',
|
||
'american' => $row['american'] ?? '',
|
||
'cx' => $row['cx'] ?? '',
|
||
'lz' => $row['lz'] ?? '',
|
||
'wlsy' => $row['wlsy'] ?? '',
|
||
];
|
||
break;
|
||
case 'abbr':
|
||
$item['title'] = $row['abbr'] ?? '';
|
||
$item['content'] = $row['full'] ?? '';
|
||
$item['summary'] = $item['title'] . ' - ' . ($row['full'] ?? '');
|
||
$item['extra'] = [
|
||
'abbr' => $row['abbr'] ?? '',
|
||
'full' => $row['full'] ?? '',
|
||
'prc' => $row['prc'] ?? '',
|
||
'meaning' => $row['meaning'] ?? '',
|
||
'category_id' => $row['category_id'] ?? '',
|
||
];
|
||
break;
|
||
case 'surname':
|
||
$item['title'] = $row['surname'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'image' => $row['image'] ?? '',
|
||
'initial' => $row['initial'] ?? '',
|
||
];
|
||
break;
|
||
case 'jieqi':
|
||
$item['title'] = $row['name'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'image' => $row['image'] ?? '',
|
||
'english' => $row['english'] ?? '',
|
||
'section' => $row['section'] ?? '',
|
||
'sanhou' => $row['sanhou'] ?? '',
|
||
'origin' => $row['origin'] ?? '',
|
||
'convention' => $row['convention'] ?? '',
|
||
'poem' => $row['poem'] ?? '',
|
||
'legend' => $row['legend'] ?? '',
|
||
'health' => $row['health'] ?? '',
|
||
'note' => $row['note'] ?? '',
|
||
];
|
||
break;
|
||
case 'nation':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'capital' => $row['capital'] ?? '',
|
||
'initial' => $row['initial'] ?? '',
|
||
'image' => $row['image'] ?? '',
|
||
'subhead' => $row['subhead'] ?? '',
|
||
'vae' => $row['vae'] ?? '',
|
||
'langue' => $row['langue'] ?? '',
|
||
'acreage' => $row['acreage'] ?? '',
|
||
'currency' => $row['currency'] ?? '',
|
||
'region' => $row['region'] ?? '',
|
||
];
|
||
break;
|
||
case 'jiufang':
|
||
$item['title'] = $row['name'] ?? '';
|
||
$item['content'] = $row['usage'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'name' => $row['name'] ?? '',
|
||
'source' => $row['source'] ?? '',
|
||
'usage' => $row['usage'] ?? '',
|
||
'ingredients' => $row['ingredients'] ?? '',
|
||
'categories' => $row['categories'] ?? '',
|
||
'components' => $row['components'] ?? '',
|
||
];
|
||
break;
|
||
case 'bot':
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr(strip_tags($item['content']), 0, 100);
|
||
$item['extra'] = [
|
||
'chinese' => $row['chinese'] ?? '',
|
||
'image' => $row['image'] ?? '',
|
||
];
|
||
break;
|
||
default:
|
||
$item['title'] = $row['title'] ?? '';
|
||
$item['content'] = $row['content'] ?? '';
|
||
$item['summary'] = mb_substr($row['content'] ?? '', 0, 100);
|
||
}
|
||
|
||
$item['createtime'] = isset($row['createtime']) ? intval($row['createtime']) : 0;
|
||
$item['updatetime'] = isset($row['updatetime']) ? intval($row['updatetime']) : 0;
|
||
|
||
return $item;
|
||
}
|
||
|
||
/**
|
||
* @name 格式化详情(含互动统计)
|
||
* @desc 在_formatItem基础上补充互动计数和用户交互状态
|
||
*/
|
||
private function _formatDetailItem($type, $row, $withInteraction = true)
|
||
{
|
||
$item = $this->_formatItem($type, $row);
|
||
|
||
if ($withInteraction) {
|
||
$feedId = $row['id'] ?? 0;
|
||
try {
|
||
$likeCount = Db::name('feed_interaction')
|
||
->where('feed_type', $type)->where('feed_id', $feedId)->where('action', 'like')->count();
|
||
$favCount = Db::name('feed_interaction')
|
||
->where('feed_type', $type)->where('feed_id', $feedId)->where('action', 'favorite')->count();
|
||
$commentCount = Db::name('feed_interaction')
|
||
->where('feed_type', $type)->where('feed_id', $feedId)->where('action', 'comment')->count();
|
||
$shareCount = Db::name('feed_interaction')
|
||
->where('feed_type', $type)->where('feed_id', $feedId)->where('action', 'share')->count();
|
||
|
||
$item['like_count'] = $likeCount;
|
||
$item['favorite_count'] = $favCount;
|
||
$item['comment_count'] = $commentCount;
|
||
$item['share_count'] = $shareCount;
|
||
} catch (\Exception $e) {
|
||
$item['like_count'] = 0;
|
||
$item['favorite_count'] = 0;
|
||
$item['comment_count'] = 0;
|
||
$item['share_count'] = 0;
|
||
}
|
||
|
||
$userId = $this->_getUserId();
|
||
if ($userId) {
|
||
try {
|
||
$myActions = Db::name('feed_interaction')
|
||
->where('user_id', $userId)->where('feed_type', $type)->where('feed_id', $feedId)
|
||
->column('action');
|
||
$item['is_liked'] = in_array('like', $myActions);
|
||
$item['is_favorited'] = in_array('favorite', $myActions);
|
||
$item['my_actions'] = $myActions ?: [];
|
||
} catch (\Exception $e) {
|
||
$item['is_liked'] = false;
|
||
$item['is_favorited'] = false;
|
||
$item['my_actions'] = [];
|
||
}
|
||
} else {
|
||
$item['is_liked'] = false;
|
||
$item['is_favorited'] = false;
|
||
$item['my_actions'] = [];
|
||
}
|
||
}
|
||
|
||
return $item;
|
||
}
|
||
|
||
private function _getUserId()
|
||
{
|
||
$token = $this->request->header('token', '');
|
||
if ($token) {
|
||
try {
|
||
$tokenData = \app\common\library\Token::get($token);
|
||
if ($tokenData && isset($tokenData['user_id'])) {
|
||
return $tokenData['user_id'];
|
||
}
|
||
} catch (\Exception $e) {}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
private function _recordSearch($keyword)
|
||
{
|
||
$dir = RUNTIME_PATH . 'search_hot';
|
||
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||
$today = date('Y-m-d');
|
||
$file = $dir . DS . $today . '.txt';
|
||
$data = [];
|
||
if (file_exists($file)) {
|
||
$data = json_decode(file_get_contents($file), true) ?: [];
|
||
}
|
||
if (!isset($data[$keyword])) $data[$keyword] = 0;
|
||
$data[$keyword]++;
|
||
file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE));
|
||
|
||
$userId = $this->_getUserId();
|
||
$dir2 = RUNTIME_PATH . 'search_history';
|
||
if (!is_dir($dir2)) mkdir($dir2, 0755, true);
|
||
$file2 = $dir2 . DS . ($userId ?: md5($this->request->ip())) . '.txt';
|
||
$history = [];
|
||
if (file_exists($file2)) {
|
||
$history = json_decode(file_get_contents($file2), true) ?: [];
|
||
}
|
||
$history = array_diff($history, [$keyword]);
|
||
array_unshift($history, $keyword);
|
||
$history = array_slice($history, 0, 50);
|
||
file_put_contents($file2, json_encode($history, JSON_UNESCAPED_UNICODE));
|
||
}
|
||
|
||
/**
|
||
* @name 全量搜索
|
||
* @desc 聚合所有数据源搜索,支持精确/模糊/相关模式,返回统一格式结果
|
||
* @param string keyword 关键词
|
||
* @param string type 数据类型(all或具体类型)
|
||
* @param string mode 搜索模式: exact/fuzzy/related
|
||
* @param string field 指定搜索字段(auto或具体字段名)
|
||
* @param int page 页码
|
||
* @param int limit 每页数量
|
||
* @param string sort 排序(relevance/views/newest)
|
||
*/
|
||
public function search()
|
||
{
|
||
$keyword = input('get.keyword', '', 'trim');
|
||
$type = input('get.type', 'all', 'trim');
|
||
$mode = input('get.mode', 'fuzzy', 'trim');
|
||
$field = input('get.field', 'auto', 'trim');
|
||
$page = max(1, input('get.page', 1, 'intval'));
|
||
$limit = min(50, max(1, input('get.limit', 20, 'intval')));
|
||
$sort = input('get.sort', 'relevance', 'trim');
|
||
|
||
$keyword = $this->_validateKeyword($keyword);
|
||
|
||
$validModes = ['exact', 'fuzzy', 'related'];
|
||
if (!in_array($mode, $validModes)) {
|
||
$this->error('不支持的搜索模式,可选: ' . implode('/', $validModes));
|
||
}
|
||
|
||
$this->_recordSearch($keyword);
|
||
|
||
$types = ($type === 'all') ? array_keys(self::$sourceMap) : explode(',', $type);
|
||
$allItems = [];
|
||
$typeStats = [];
|
||
|
||
foreach ($types as $t) {
|
||
if (!isset(self::$sourceMap[$t])) continue;
|
||
$config = self::$sourceMap[$t];
|
||
|
||
try {
|
||
$query = Db::name($config['table']);
|
||
$this->_applyStatus($query, $config);
|
||
|
||
$searchFields = $config['search'];
|
||
if ($field !== 'auto' && in_array($field, $searchFields)) {
|
||
$searchFields = [$field];
|
||
}
|
||
|
||
if ($mode === 'exact') {
|
||
$query->where(function ($q) use ($searchFields, $keyword) {
|
||
foreach ($searchFields as $f) {
|
||
$q->whereOr($f, '=', $keyword);
|
||
}
|
||
});
|
||
} elseif ($mode === 'fuzzy') {
|
||
$like = '%' . $keyword . '%';
|
||
$query->where(function ($q) use ($searchFields, $like) {
|
||
foreach ($searchFields as $f) {
|
||
$q->whereOr($f, 'like', $like);
|
||
}
|
||
});
|
||
} elseif ($mode === 'related') {
|
||
$keywords = $this->_extractKeywords($keyword);
|
||
if (!empty($keywords)) {
|
||
$query->where(function ($q) use ($searchFields, $keywords) {
|
||
foreach ($keywords as $kw) {
|
||
$like = '%' . $kw . '%';
|
||
foreach ($searchFields as $f) {
|
||
$q->whereOr($f, 'like', $like);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
$typeTotal = $query->count();
|
||
|
||
if ($typeTotal > 0) {
|
||
$typeStats[] = [
|
||
'type' => $t,
|
||
'name' => $config['name'],
|
||
'icon' => $config['icon'],
|
||
'count' => $typeTotal,
|
||
];
|
||
|
||
$orderField = 'views';
|
||
$orderDir = 'desc';
|
||
if ($sort === 'newest') {
|
||
$orderField = 'id';
|
||
$orderDir = 'desc';
|
||
} elseif ($sort === 'relevance') {
|
||
$orderField = 'views';
|
||
$orderDir = 'desc';
|
||
}
|
||
|
||
$list = $query->field($config['return'])
|
||
->page($page, $limit)
|
||
->order($orderField, $orderDir)
|
||
->select();
|
||
|
||
foreach ($list as $row) {
|
||
$item = $this->_formatItem($t, $row);
|
||
$item['_relevance'] = $this->_calcRelevance($item, $keyword, $mode);
|
||
$allItems[] = $item;
|
||
}
|
||
}
|
||
} catch (\Exception $e) {
|
||
$typeStats[] = [
|
||
'type' => $t,
|
||
'name' => $config['name'],
|
||
'icon' => $config['icon'],
|
||
'count' => 0,
|
||
'error' => $e->getMessage(),
|
||
];
|
||
}
|
||
}
|
||
|
||
if ($sort === 'relevance') {
|
||
usort($allItems, function ($a, $b) {
|
||
if ($b['_relevance'] !== $a['_relevance']) {
|
||
return $b['_relevance'] - $a['_relevance'];
|
||
}
|
||
return ($b['views'] ?? 0) - ($a['views'] ?? 0);
|
||
});
|
||
}
|
||
|
||
foreach ($allItems as &$item) {
|
||
unset($item['_relevance']);
|
||
}
|
||
unset($item);
|
||
|
||
$totalItems = count($allItems);
|
||
$allItems = array_slice($allItems, 0, $limit);
|
||
|
||
$this->success('搜索完成', [
|
||
'keyword' => $keyword,
|
||
'type' => $type,
|
||
'mode' => $mode,
|
||
'sort' => $sort,
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'total' => $totalItems,
|
||
'list' => $allItems,
|
||
'type_stats' => $typeStats,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 精确搜索
|
||
* @desc 完全匹配搜索,使用SQL = 操作符
|
||
*/
|
||
public function exact()
|
||
{
|
||
$keyword = input('get.keyword', '', 'trim');
|
||
$type = input('get.type', 'all', 'trim');
|
||
$field = input('get.field', 'auto', 'trim');
|
||
$limit = min(50, max(1, input('get.limit', 20, 'intval')));
|
||
|
||
$keyword = $this->_validateKeyword($keyword);
|
||
$this->_recordSearch($keyword);
|
||
|
||
$types = ($type === 'all') ? array_keys(self::$sourceMap) : explode(',', $type);
|
||
$results = [];
|
||
|
||
foreach ($types as $t) {
|
||
if (!isset(self::$sourceMap[$t])) continue;
|
||
$config = self::$sourceMap[$t];
|
||
|
||
try {
|
||
$query = Db::name($config['table']);
|
||
$this->_applyStatus($query, $config);
|
||
|
||
$searchFields = $config['search'];
|
||
if ($field !== 'auto' && in_array($field, $searchFields)) {
|
||
$searchFields = [$field];
|
||
}
|
||
|
||
$query->where(function ($q) use ($searchFields, $keyword) {
|
||
foreach ($searchFields as $f) {
|
||
$q->whereOr($f, '=', $keyword);
|
||
}
|
||
});
|
||
|
||
$count = $query->count();
|
||
if ($count > 0) {
|
||
$list = $query->field($config['return'])->limit($limit)->order('views desc')->select();
|
||
$items = [];
|
||
foreach ($list as $row) {
|
||
$items[] = $this->_formatItem($t, $row);
|
||
}
|
||
$results[] = [
|
||
'type' => $t,
|
||
'name' => $config['name'],
|
||
'icon' => $config['icon'],
|
||
'count' => $count,
|
||
'list' => $items,
|
||
];
|
||
}
|
||
} catch (\Exception $e) {}
|
||
}
|
||
|
||
$totalMatched = array_sum(array_column($results, 'count'));
|
||
$this->success('精确搜索完成', [
|
||
'keyword' => $keyword,
|
||
'mode' => 'exact',
|
||
'total_matched' => $totalMatched,
|
||
'type_count' => count($results),
|
||
'results' => $results,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 模糊搜索
|
||
* @desc LIKE关键词匹配搜索
|
||
*/
|
||
public function fuzzy()
|
||
{
|
||
$keyword = input('get.keyword', '', 'trim');
|
||
$type = input('get.type', 'all', 'trim');
|
||
$field = input('get.field', 'auto', 'trim');
|
||
$page = max(1, input('get.page', 1, 'intval'));
|
||
$limit = min(50, max(1, input('get.limit', 20, 'intval')));
|
||
|
||
$keyword = $this->_validateKeyword($keyword);
|
||
$this->_recordSearch($keyword);
|
||
|
||
$types = ($type === 'all') ? array_keys(self::$sourceMap) : explode(',', $type);
|
||
$results = [];
|
||
|
||
foreach ($types as $t) {
|
||
if (!isset(self::$sourceMap[$t])) continue;
|
||
$config = self::$sourceMap[$t];
|
||
|
||
try {
|
||
$query = Db::name($config['table']);
|
||
$this->_applyStatus($query, $config);
|
||
|
||
$searchFields = $config['search'];
|
||
if ($field !== 'auto' && in_array($field, $searchFields)) {
|
||
$searchFields = [$field];
|
||
}
|
||
|
||
$like = '%' . $keyword . '%';
|
||
$query->where(function ($q) use ($searchFields, $like) {
|
||
foreach ($searchFields as $f) {
|
||
$q->whereOr($f, 'like', $like);
|
||
}
|
||
});
|
||
|
||
$count = $query->count();
|
||
if ($count > 0) {
|
||
$list = $query->field($config['return'])
|
||
->page($page, $limit)
|
||
->order('views desc')
|
||
->select();
|
||
$items = [];
|
||
foreach ($list as $row) {
|
||
$items[] = $this->_formatItem($t, $row);
|
||
}
|
||
$results[] = [
|
||
'type' => $t,
|
||
'name' => $config['name'],
|
||
'icon' => $config['icon'],
|
||
'count' => $count,
|
||
'list' => $items,
|
||
];
|
||
}
|
||
} catch (\Exception $e) {}
|
||
}
|
||
|
||
$totalMatched = array_sum(array_column($results, 'count'));
|
||
$this->success('模糊搜索完成', [
|
||
'keyword' => $keyword,
|
||
'mode' => 'fuzzy',
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'total_matched' => $totalMatched,
|
||
'type_count' => count($results),
|
||
'results' => $results,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 相关搜索
|
||
* @desc 基于关键词分词的相关搜索,扩大搜索范围
|
||
*/
|
||
public function related()
|
||
{
|
||
$keyword = input('get.keyword', '', 'trim');
|
||
$type = input('get.type', 'all', 'trim');
|
||
$limit = min(50, max(1, input('get.limit', 20, 'intval')));
|
||
|
||
$keyword = $this->_validateKeyword($keyword);
|
||
$this->_recordSearch($keyword);
|
||
|
||
$keywords = $this->_extractKeywords($keyword);
|
||
if (empty($keywords)) {
|
||
$this->error('未能提取有效关键词');
|
||
}
|
||
|
||
$types = ($type === 'all') ? array_keys(self::$sourceMap) : explode(',', $type);
|
||
$results = [];
|
||
|
||
foreach ($types as $t) {
|
||
if (!isset(self::$sourceMap[$t])) continue;
|
||
$config = self::$sourceMap[$t];
|
||
|
||
try {
|
||
$query = Db::name($config['table']);
|
||
$this->_applyStatus($query, $config);
|
||
|
||
$query->where(function ($q) use ($config, $keywords) {
|
||
foreach ($keywords as $kw) {
|
||
$like = '%' . $kw . '%';
|
||
foreach ($config['search'] as $f) {
|
||
$q->whereOr($f, 'like', $like);
|
||
}
|
||
}
|
||
});
|
||
|
||
$count = $query->count();
|
||
if ($count > 0) {
|
||
$list = $query->field($config['return'])->limit($limit)->order('views desc')->select();
|
||
$items = [];
|
||
foreach ($list as $row) {
|
||
$item = $this->_formatItem($t, $row);
|
||
$item['_score'] = $this->_calcRelevance($item, $keyword, 'related');
|
||
$items[] = $item;
|
||
}
|
||
usort($items, function ($a, $b) {
|
||
return $b['_score'] - $a['_score'];
|
||
});
|
||
foreach ($items as &$it) {
|
||
unset($it['_score']);
|
||
}
|
||
unset($it);
|
||
$results[] = [
|
||
'type' => $t,
|
||
'name' => $config['name'],
|
||
'icon' => $config['icon'],
|
||
'count' => $count,
|
||
'list' => $items,
|
||
];
|
||
}
|
||
} catch (\Exception $e) {}
|
||
}
|
||
|
||
$totalMatched = array_sum(array_column($results, 'count'));
|
||
$this->success('相关搜索完成', [
|
||
'keyword' => $keyword,
|
||
'mode' => 'related',
|
||
'keywords' => $keywords,
|
||
'total_matched' => $totalMatched,
|
||
'type_count' => count($results),
|
||
'results' => $results,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 条件搜索
|
||
* @desc 支持多条件组合筛选搜索
|
||
* @param string type 数据类型(必填)
|
||
* @param array conditions 条件列表JSON [{"field":"author","op":"like","value":"李白"}]
|
||
* @param string sort 排序字段
|
||
* @param string order 排序方向(asc/desc)
|
||
* @param int page 页码
|
||
* @param int limit 每页数量
|
||
*/
|
||
public function condition()
|
||
{
|
||
$type = input('get.type', '', 'trim');
|
||
$conditions = input('get.conditions', '', 'trim');
|
||
$sort = input('get.sort', 'views', 'trim');
|
||
$order = input('get.order', 'desc', 'trim');
|
||
$page = max(1, input('get.page', 1, 'intval'));
|
||
$limit = min(50, max(1, input('get.limit', 20, 'intval')));
|
||
|
||
if (empty($type) || !isset(self::$sourceMap[$type])) {
|
||
$allowed = implode('/', array_keys(self::$sourceMap));
|
||
$this->error('请指定有效的数据类型,可选: ' . $allowed);
|
||
}
|
||
|
||
if (empty($conditions)) {
|
||
$this->error('请提供搜索条件');
|
||
}
|
||
|
||
$condList = json_decode($conditions, true);
|
||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($condList)) {
|
||
$this->error('条件格式错误,需为JSON数组');
|
||
}
|
||
|
||
$config = self::$sourceMap[$type];
|
||
$query = Db::name($config['table']);
|
||
$this->_applyStatus($query, $config);
|
||
|
||
$validOps = ['=', '!=', '>', '<', '>=', '<=', 'like', 'not like', 'in', 'not in', 'between'];
|
||
$searchFields = $config['search'];
|
||
$allFields = array_merge($searchFields, ['id', 'views', 'createtime', 'updatetime']);
|
||
|
||
foreach ($condList as $cond) {
|
||
if (!isset($cond['field']) || !isset($cond['value'])) continue;
|
||
$f = trim($cond['field']);
|
||
$op = isset($cond['op']) ? strtolower(trim($cond['op'])) : '=';
|
||
$v = $cond['value'];
|
||
|
||
if (!in_array($op, $validOps)) {
|
||
$op = '=';
|
||
}
|
||
|
||
if ($op === 'like') {
|
||
$v = '%' . $v . '%';
|
||
}
|
||
|
||
try {
|
||
$query->where($f, $op, $v);
|
||
} catch (\Exception $e) {}
|
||
}
|
||
|
||
$total = $query->count();
|
||
$list = $query->field($config['return'])
|
||
->page($page, $limit)
|
||
->order($sort, $order)
|
||
->select();
|
||
|
||
$items = [];
|
||
foreach ($list as $row) {
|
||
$items[] = $this->_formatItem($type, $row);
|
||
}
|
||
|
||
$this->success('条件搜索完成', [
|
||
'type' => $type,
|
||
'name' => $config['name'],
|
||
'icon' => $config['icon'],
|
||
'conditions' => $condList,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'list' => $items,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name ID查询
|
||
* @desc 根据类型和ID获取单条内容详情
|
||
* @param string type 数据类型
|
||
* @param int id 内容ID
|
||
*/
|
||
public function getById()
|
||
{
|
||
$type = input('get.type', '', 'trim');
|
||
$id = input('get.id', 0, 'intval');
|
||
|
||
if (empty($type) || !isset(self::$sourceMap[$type])) {
|
||
$allowed = implode('/', array_keys(self::$sourceMap));
|
||
$this->error('请指定有效的数据类型,可选: ' . $allowed);
|
||
}
|
||
if (!$id) {
|
||
$this->error('请提供内容ID');
|
||
}
|
||
|
||
$config = self::$sourceMap[$type];
|
||
$row = Db::name($config['table'])->where('id', $id)->find();
|
||
|
||
if (!$row) {
|
||
$this->error('内容不存在');
|
||
}
|
||
|
||
Db::name($config['table'])->where('id', $id)->setInc('views');
|
||
|
||
$item = $this->_formatDetailItem($type, $row);
|
||
|
||
$this->success('查询成功', $item);
|
||
}
|
||
|
||
/**
|
||
* @name ID反查
|
||
* @desc 根据ID列表批量查询多条内容,支持跨类型
|
||
* @param string ids ID列表,格式: type_id,type_id (如 poetry_1,story_5)
|
||
* @param int lite 轻量模式(1=只返回summary)
|
||
*/
|
||
public function getByIds()
|
||
{
|
||
$ids = input('get.ids', '', 'trim');
|
||
$lite = input('get.lite', 0, 'intval');
|
||
|
||
if (empty($ids)) {
|
||
$this->error('请提供ID列表,格式: type_id,type_id');
|
||
}
|
||
|
||
$pairs = explode(',', $ids);
|
||
if (count($pairs) > 50) {
|
||
$this->error('最多查询50条');
|
||
}
|
||
|
||
$grouped = [];
|
||
foreach ($pairs as $pair) {
|
||
$parts = explode('_', $pair, 2);
|
||
if (count($parts) === 2 && isset(self::$sourceMap[$parts[0]])) {
|
||
$grouped[$parts[0]][] = intval($parts[1]);
|
||
}
|
||
}
|
||
|
||
$items = [];
|
||
$notFound = [];
|
||
|
||
foreach ($grouped as $type => $typeIds) {
|
||
$config = self::$sourceMap[$type];
|
||
try {
|
||
$query = Db::name($config['table'])->where('id', 'in', $typeIds);
|
||
$this->_applyStatus($query, $config);
|
||
$list = $query->field($config['return'])->select();
|
||
|
||
$foundIds = [];
|
||
foreach ($list as $row) {
|
||
$item = $this->_formatItem($type, $row);
|
||
if ($lite) {
|
||
unset($item['content']);
|
||
}
|
||
$items[] = $item;
|
||
$foundIds[] = $row['id'];
|
||
}
|
||
|
||
$missing = array_diff($typeIds, $foundIds);
|
||
foreach ($missing as $mid) {
|
||
$notFound[] = $type . '_' . $mid;
|
||
}
|
||
} catch (\Exception $e) {
|
||
foreach ($typeIds as $mid) {
|
||
$notFound[] = $type . '_' . $mid;
|
||
}
|
||
}
|
||
}
|
||
|
||
$this->success('查询完成', [
|
||
'total' => count($items),
|
||
'list' => $items,
|
||
'not_found' => $notFound,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 搜索建议
|
||
* @desc 输入前缀返回搜索建议
|
||
*/
|
||
public function suggest()
|
||
{
|
||
$keyword = input('get.keyword', '', 'trim');
|
||
$limit = min(20, max(1, input('get.limit', 10, 'intval')));
|
||
|
||
if (empty($keyword)) $this->error('请输入关键词');
|
||
if (mb_strlen($keyword) > 30) $this->error('关键词过长');
|
||
|
||
$like = $keyword . '%';
|
||
$suggestions = [];
|
||
|
||
$suggestConfig = [
|
||
'chengyu' => ['table' => 'cy', 'field' => 'cy', 'label' => '成语'],
|
||
'poetry' => ['table' => 'poetry', 'field' => 'name', 'label' => '诗词'],
|
||
'hanzi' => ['table' => 'hanzi', 'field' => 'zi', 'label' => '汉字'],
|
||
'wisdom' => ['table' => 'wisdom', 'field' => 'name', 'label' => '名言'],
|
||
'story' => ['table' => 'story', 'field' => 'title', 'label' => '故事'],
|
||
'lyric' => ['table' => 'lyric', 'field' => 'title', 'label' => '歌词'],
|
||
'drug' => ['table' => 'drug', 'field' => 'name', 'label' => '药品'],
|
||
'herbal' => ['table' => 'herbal', 'field' => 'name', 'label' => '中草药'],
|
||
'article' => ['table' => 'article', 'field' => 'title', 'label' => '文章'],
|
||
];
|
||
|
||
foreach ($suggestConfig as $type => $sc) {
|
||
if (count($suggestions) >= $limit) break;
|
||
try {
|
||
$query = Db::name($sc['table'])->where($sc['field'], 'like', $like);
|
||
if (isset(self::$sourceMap[$type])) {
|
||
$this->_applyStatus($query, self::$sourceMap[$type]);
|
||
}
|
||
$list = $query->limit($limit - count($suggestions))->column($sc['field'] . ' as text');
|
||
foreach ($list as $text) {
|
||
$suggestions[] = ['text' => $text, 'type' => $type, 'label' => $sc['label']];
|
||
}
|
||
} catch (\Exception $e) {}
|
||
}
|
||
|
||
$suggestions = array_slice($suggestions, 0, $limit);
|
||
|
||
$this->success('成功', ['suggestions' => $suggestions]);
|
||
}
|
||
|
||
/**
|
||
* @name 热门搜索
|
||
* @desc 返回热门搜索关键词
|
||
*/
|
||
public function hot()
|
||
{
|
||
$limit = min(50, max(1, input('get.limit', 20, 'intval')));
|
||
$days = min(7, max(1, input('get.days', 1, 'intval')));
|
||
|
||
$dir = RUNTIME_PATH . 'search_hot';
|
||
$merged = [];
|
||
|
||
for ($i = 0; $i < $days; $i++) {
|
||
$date = date('Y-m-d', strtotime("-{$i} days"));
|
||
$file = $dir . DS . $date . '.txt';
|
||
if (file_exists($file)) {
|
||
$data = json_decode(file_get_contents($file), true) ?: [];
|
||
foreach ($data as $word => $count) {
|
||
if (!isset($merged[$word])) $merged[$word] = 0;
|
||
$merged[$word] += $count;
|
||
}
|
||
}
|
||
}
|
||
|
||
arsort($merged);
|
||
$hotList = [];
|
||
$i = 0;
|
||
foreach ($merged as $word => $count) {
|
||
$hotList[] = ['keyword' => $word, 'count' => $count];
|
||
$i++;
|
||
if ($i >= $limit) break;
|
||
}
|
||
|
||
$this->success('成功', ['hot_list' => $hotList, 'days' => $days]);
|
||
}
|
||
|
||
/**
|
||
* @name 搜索历史
|
||
* @desc 获取/清除用户搜索历史
|
||
*/
|
||
public function history()
|
||
{
|
||
$action = input('get.action', 'list', 'trim');
|
||
$userId = $this->_getUserId();
|
||
|
||
$dir = RUNTIME_PATH . 'search_history';
|
||
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||
|
||
$file = $dir . DS . ($userId ?: md5($this->request->ip())) . '.txt';
|
||
|
||
if ($action === 'clear') {
|
||
if (file_exists($file)) unlink($file);
|
||
$this->success('已清除');
|
||
}
|
||
|
||
$history = [];
|
||
if (file_exists($file)) {
|
||
$history = json_decode(file_get_contents($file), true) ?: [];
|
||
}
|
||
|
||
$limit = min(50, max(1, input('get.limit', 20, 'intval')));
|
||
$history = array_slice($history, 0, $limit);
|
||
|
||
$this->success('成功', ['history' => $history]);
|
||
}
|
||
|
||
/**
|
||
* @name 数据源列表
|
||
* @desc 返回所有可搜索的数据源及字段信息
|
||
*/
|
||
public function sources()
|
||
{
|
||
$sources = [];
|
||
foreach (self::$sourceMap as $key => $config) {
|
||
$count = 0;
|
||
try {
|
||
$query = Db::name($config['table']);
|
||
$this->_applyStatus($query, $config);
|
||
$count = $query->count();
|
||
} catch (\Exception $e) {}
|
||
|
||
$sources[] = [
|
||
'key' => $key,
|
||
'name' => $config['name'],
|
||
'icon' => $config['icon'],
|
||
'total' => $count,
|
||
'search_fields'=> $config['search'],
|
||
'return_fields'=> $config['return'],
|
||
];
|
||
}
|
||
|
||
$this->success('成功', [
|
||
'total_sources' => count($sources),
|
||
'sources' => $sources,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 多字段搜索
|
||
* @desc 在指定类型的指定字段中搜索,支持字段级精确/模糊控制
|
||
* @param string type 数据类型(必填)
|
||
* @param string field 搜索字段(必填)
|
||
* @param string keyword 关键词
|
||
* @param string op 操作符(=/like)
|
||
* @param int page 页码
|
||
* @param int limit 每页数量
|
||
*/
|
||
public function fieldSearch()
|
||
{
|
||
$type = input('get.type', '', 'trim');
|
||
$field = input('get.field', '', 'trim');
|
||
$keyword = input('get.keyword', '', 'trim');
|
||
$op = input('get.op', 'like', 'trim');
|
||
$page = max(1, input('get.page', 1, 'intval'));
|
||
$limit = min(50, max(1, input('get.limit', 20, 'intval')));
|
||
|
||
if (empty($type) || !isset(self::$sourceMap[$type])) {
|
||
$this->error('请指定有效的数据类型');
|
||
}
|
||
if (empty($field)) {
|
||
$this->error('请指定搜索字段');
|
||
}
|
||
if (empty($keyword)) {
|
||
$this->error('请输入搜索关键词');
|
||
}
|
||
|
||
$config = self::$sourceMap[$type];
|
||
if (!in_array($field, $config['search']) && !in_array($field, ['id', 'views'])) {
|
||
$this->error('不支持的字段: ' . $field . ',可选: ' . implode(',', $config['search']));
|
||
}
|
||
|
||
$query = Db::name($config['table']);
|
||
$this->_applyStatus($query, $config);
|
||
|
||
if ($op === '=') {
|
||
$query->where($field, '=', $keyword);
|
||
} else {
|
||
$query->where($field, 'like', '%' . $keyword . '%');
|
||
}
|
||
|
||
$total = $query->count();
|
||
$list = $query->field($config['return'])
|
||
->page($page, $limit)
|
||
->order('views desc')
|
||
->select();
|
||
|
||
$items = [];
|
||
foreach ($list as $row) {
|
||
$items[] = $this->_formatItem($type, $row);
|
||
}
|
||
|
||
$this->success('搜索完成', [
|
||
'type' => $type,
|
||
'name' => $config['name'],
|
||
'icon' => $config['icon'],
|
||
'field' => $field,
|
||
'op' => $op,
|
||
'keyword'=> $keyword,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'list' => $items,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 全量信息详情
|
||
* @desc 返回指定类型+ID的完整原始数据,包含数据库所有字段,用于APP详情页展示全量信息
|
||
* @param string type 数据类型
|
||
* @param int id 内容ID
|
||
* @param int formatted 是否格式化(1=统一格式,0=原始字段)
|
||
*/
|
||
public function fullDetail()
|
||
{
|
||
$type = input('get.type', '', 'trim');
|
||
$id = input('get.id', 0, 'intval');
|
||
$formatted = input('get.formatted', 1, 'intval');
|
||
|
||
if (empty($type) || !isset(self::$sourceMap[$type])) {
|
||
$this->error('请指定有效的数据类型');
|
||
}
|
||
if (!$id) {
|
||
$this->error('请提供内容ID');
|
||
}
|
||
|
||
$config = self::$sourceMap[$type];
|
||
$row = Db::name($config['table'])->where('id', $id)->find();
|
||
|
||
if (!$row) {
|
||
$this->error('内容不存在');
|
||
}
|
||
|
||
Db::name($config['table'])->where('id', $id)->setInc('views');
|
||
|
||
if ($formatted) {
|
||
$item = $this->_formatDetailItem($type, $row);
|
||
$rawData = [];
|
||
foreach ($row as $k => $v) {
|
||
if (!array_key_exists($k, $item)) {
|
||
$rawData[$k] = $v;
|
||
}
|
||
}
|
||
$item['raw_data'] = $rawData;
|
||
} else {
|
||
$item = $row;
|
||
$item['feed_type'] = $type;
|
||
$item['feed_name'] = $config['name'];
|
||
$item['feed_icon'] = $config['icon'];
|
||
}
|
||
|
||
$this->success('查询成功', $item);
|
||
}
|
||
|
||
/**
|
||
* @name 关联推荐
|
||
* @desc 根据指定内容推荐相关内容,用于详情页底部"相关推荐"
|
||
* @param string type 数据类型
|
||
* @param int id 内容ID
|
||
* @param int limit 返回数量
|
||
*/
|
||
public function relatedRecommend()
|
||
{
|
||
$type = input('get.type', '', 'trim');
|
||
$id = input('get.id', 0, 'intval');
|
||
$limit = min(20, max(1, input('get.limit', 5, 'intval')));
|
||
|
||
if (empty($type) || !isset(self::$sourceMap[$type])) {
|
||
$this->error('请指定有效的数据类型');
|
||
}
|
||
if (!$id) {
|
||
$this->error('请提供内容ID');
|
||
}
|
||
|
||
$config = self::$sourceMap[$type];
|
||
$source = Db::name($config['table'])->where('id', $id)->find();
|
||
if (!$source) {
|
||
$this->error('内容不存在');
|
||
}
|
||
|
||
$items = [];
|
||
$searchFields = $config['search'];
|
||
$keywords = [];
|
||
|
||
foreach ($searchFields as $field) {
|
||
$val = $source[$field] ?? '';
|
||
if (!empty($val) && mb_strlen($val) >= 2) {
|
||
$keywords[] = mb_substr($val, 0, 20);
|
||
}
|
||
}
|
||
|
||
if (!empty($keywords)) {
|
||
$query = Db::name($config['table'])->where('id', '<>', $id);
|
||
$this->_applyStatus($query, $config);
|
||
|
||
$query->where(function ($q) use ($searchFields, $keywords) {
|
||
foreach ($searchFields as $field) {
|
||
foreach ($keywords as $kw) {
|
||
$q->whereOr($field, 'like', '%' . $kw . '%');
|
||
}
|
||
}
|
||
});
|
||
|
||
$rows = $query->field($config['return'])
|
||
->order('views desc')
|
||
->limit($limit)
|
||
->select();
|
||
|
||
foreach ($rows as $row) {
|
||
$items[] = $this->_formatItem($type, $row);
|
||
}
|
||
}
|
||
|
||
if (count($items) < $limit) {
|
||
$need = $limit - count($items);
|
||
$existIds = array_column($items, 'id');
|
||
$existIds[] = $id;
|
||
|
||
$query = Db::name($config['table'])
|
||
->where('id', 'not in', $existIds);
|
||
$this->_applyStatus($query, $config);
|
||
|
||
$extraRows = $query->field($config['return'])
|
||
->order('views desc')
|
||
->limit($need)
|
||
->select();
|
||
|
||
foreach ($extraRows as $row) {
|
||
$items[] = $this->_formatItem($type, $row);
|
||
}
|
||
}
|
||
|
||
$this->success('成功', [
|
||
'source_type' => $type,
|
||
'source_id' => $id,
|
||
'total' => count($items),
|
||
'list' => $items,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 搜索高亮
|
||
* @desc 对搜索结果文本进行关键词高亮标记,返回带em标签的结果
|
||
* @param string keyword 关键词
|
||
* @param string type 数据类型
|
||
* @param string mode 搜索模式
|
||
* @param int limit 每页数量
|
||
* @param string tag 高亮标签(默认em)
|
||
*/
|
||
public function highlight()
|
||
{
|
||
$keyword = input('get.keyword', '', 'trim');
|
||
$type = input('get.type', 'all', 'trim');
|
||
$mode = input('get.mode', 'fuzzy', 'trim');
|
||
$limit = min(20, max(1, input('get.limit', 10, 'intval')));
|
||
$tag = input('get.tag', 'em', 'trim');
|
||
|
||
$keyword = $this->_validateKeyword($keyword);
|
||
|
||
$types = ($type === 'all') ? array_keys(self::$sourceMap) : explode(',', $type);
|
||
$allItems = [];
|
||
|
||
foreach ($types as $t) {
|
||
if (!isset(self::$sourceMap[$t])) continue;
|
||
$config = self::$sourceMap[$t];
|
||
|
||
try {
|
||
$query = Db::name($config['table']);
|
||
$this->_applyStatus($query, $config);
|
||
|
||
$searchFields = $config['search'];
|
||
$like = '%' . $keyword . '%';
|
||
$query->where(function ($q) use ($searchFields, $like) {
|
||
foreach ($searchFields as $f) {
|
||
$q->whereOr($f, 'like', $like);
|
||
}
|
||
});
|
||
|
||
$list = $query->field($config['return'])
|
||
->order('views desc')
|
||
->limit($limit)
|
||
->select();
|
||
|
||
foreach ($list as $row) {
|
||
$item = $this->_formatItem($t, $row);
|
||
$item['title_highlight'] = $this->_highlightText($item['title'], $keyword, $tag);
|
||
$item['content_highlight'] = $this->_highlightText($item['summary'] ?: $item['content'], $keyword, $tag);
|
||
$item['author_highlight'] = $this->_highlightText($item['author'], $keyword, $tag);
|
||
$allItems[] = $item;
|
||
}
|
||
} catch (\Exception $e) {}
|
||
}
|
||
|
||
$this->success('搜索完成', [
|
||
'keyword' => $keyword,
|
||
'tag' => $tag,
|
||
'total' => count($allItems),
|
||
'list' => array_slice($allItems, 0, $limit),
|
||
]);
|
||
}
|
||
|
||
private function _highlightText($text, $keyword, $tag = 'em')
|
||
{
|
||
if (empty($text) || empty($keyword)) return $text;
|
||
$kw = preg_quote($keyword, '/');
|
||
return preg_replace('/(' . $kw . ')/iu', '<' . $tag . '>$1</' . $tag . '>', $text);
|
||
}
|
||
|
||
private function _extractKeywords($text)
|
||
{
|
||
$text = preg_replace('/[^\x{4e00}-\x{9fa5}a-zA-Z0-9]/u', ' ', $text);
|
||
$text = trim($text);
|
||
if (empty($text)) return [];
|
||
|
||
$words = preg_split('/\s+/', $text);
|
||
$keywords = [];
|
||
foreach ($words as $w) {
|
||
$w = trim($w);
|
||
if (mb_strlen($w) >= 2) {
|
||
$keywords[] = $w;
|
||
}
|
||
}
|
||
|
||
if (empty($keywords) && mb_strlen($text) >= 2) {
|
||
$len = mb_strlen($text);
|
||
for ($i = 0; $i < $len - 1; $i++) {
|
||
$keywords[] = mb_substr($text, $i, 2);
|
||
}
|
||
}
|
||
|
||
return array_unique($keywords);
|
||
}
|
||
|
||
private function _calcRelevance($item, $keyword, $mode)
|
||
{
|
||
$score = 0;
|
||
$title = $item['title'] ?? ($item['name'] ?? '');
|
||
$content = $item['content'] ?? ($item['summary'] ?? '');
|
||
$author = $item['author'] ?? '';
|
||
|
||
if ($mode === 'exact') {
|
||
if ($title === $keyword) $score += 100;
|
||
elseif (strpos($title, $keyword) !== false) $score += 60;
|
||
if ($author === $keyword) $score += 50;
|
||
if (strpos($content, $keyword) !== false) $score += 30;
|
||
} else {
|
||
if ($title === $keyword) $score += 100;
|
||
elseif (strpos($title, $keyword) !== false) $score += 60;
|
||
if ($author === $keyword) $score += 50;
|
||
elseif (strpos($author, $keyword) !== false) $score += 30;
|
||
if (strpos($content, $keyword) !== false) $score += 20;
|
||
|
||
$keywords = $this->_extractKeywords($keyword);
|
||
foreach ($keywords as $kw) {
|
||
if (strpos($title, $kw) !== false) $score += 15;
|
||
if (strpos($author, $kw) !== false) $score += 10;
|
||
if (strpos($content, $kw) !== false) $score += 5;
|
||
}
|
||
}
|
||
|
||
$score += min(10, intval(($item['views'] ?? 0) / 1000));
|
||
|
||
return $score;
|
||
}
|
||
}
|