refactor: 提取搜索逻辑到 Model Scope

- User: 新增 scopeSearchByKeyword
  - Project: 新增 scopeSearchByKeyword
  - ProjectTask: 新增 scopeSearchByKeyword
  - File: 新增 scopeSearchByKeyword, scopeSharedToUser
  - WebSocketDialogMsg: 新增 scopeSearchByKeyword, scopeAccessibleByUser
  - SearchController: 使用新的 Model Scope 简化 MySQL 回退逻辑
This commit is contained in:
kuaifan 2026-01-03 07:58:11 +00:00
parent 67fc0781e5
commit ec0db3a76c
6 changed files with 181 additions and 99 deletions

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use DB;
use Request; use Request;
use App\Models\File; use App\Models\File;
use App\Models\Project; use App\Models\Project;
@ -10,7 +9,6 @@ use App\Models\ProjectTask;
use App\Models\User; use App\Models\User;
use App\Models\WebSocketDialog; use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg; use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser;
use App\Module\Base; use App\Module\Base;
use App\Module\Apps; use App\Module\Apps;
use App\Module\Manticore\ManticoreFile; use App\Module\Manticore\ManticoreFile;
@ -93,28 +91,13 @@ class SearchController extends AbstractController
*/ */
private function searchContactByMysql(string $key, int $take): array private function searchContactByMysql(string $key, int $take): array
{ {
$builder = User::select(User::$basicField) $users = User::select(User::$basicField)
->where('bot', 0) ->where('bot', 0)
->whereNull('disable_at'); ->whereNull('disable_at')
->searchByKeyword($key)
if (str_contains($key, "@")) { ->orderByDesc('line_at')
$builder->where("email", "like", "%{$key}%"); ->take($take)
} elseif (Base::isNumber($key)) { ->get();
$builder->where(function ($query) use ($key) {
$query->where("userid", intval($key))
->orWhere("nickname", "like", "%{$key}%")
->orWhere("pinyin", "like", "%{$key}%")
->orWhere("profession", "like", "%{$key}%");
});
} else {
$builder->where(function ($query) use ($key) {
$query->where("nickname", "like", "%{$key}%")
->orWhere("pinyin", "like", "%{$key}%")
->orWhere("profession", "like", "%{$key}%");
});
}
$users = $builder->orderByDesc('line_at')->take($take)->get();
return $users->map(function ($user) { return $users->map(function ($user) {
return array_merge($user->toArray(), [ return array_merge($user->toArray(), [
@ -193,12 +176,12 @@ class SearchController extends AbstractController
{ {
$projects = Project::authData() $projects = Project::authData()
->whereNull('projects.archived_at') ->whereNull('projects.archived_at')
->where("projects.name", "like", "%{$key}%") ->searchByKeyword($key)
->orderByDesc('projects.id') ->orderByDesc('projects.id')
->take($take) ->take($take)
->get(); ->get();
return $projects->map(function ($project) use ($userid) { return $projects->map(function ($project) {
$array = $project->toArray(); $array = $project->toArray();
$array['relevance'] = 0; $array['relevance'] = 0;
$array['desc_preview'] = null; $array['desc_preview'] = null;
@ -275,29 +258,18 @@ class SearchController extends AbstractController
*/ */
private function searchTaskByMysql(int $userid, string $key, int $take): array private function searchTaskByMysql(int $userid, string $key, int $take): array
{ {
$builder = ProjectTask::with(['taskUser', 'taskTag']) $tasks = ProjectTask::with(['taskUser', 'taskTag'])
->whereIn('project_tasks.project_id', function ($query) use ($userid) { ->whereIn('project_tasks.project_id', function ($query) use ($userid) {
$query->select('project_id') $query->select('project_id')
->from('project_users') ->from('project_users')
->where('userid', $userid); ->where('userid', $userid);
}) })
->whereNull('project_tasks.archived_at') ->whereNull('project_tasks.archived_at')
->whereNull('project_tasks.deleted_at'); ->whereNull('project_tasks.deleted_at')
->searchByKeyword($key)
if (Base::isNumber($key)) { ->orderByDesc('project_tasks.id')
$builder->where(function ($query) use ($key) { ->take($take)
$query->where("project_tasks.id", intval($key)) ->get();
->orWhere("project_tasks.name", "like", "%{$key}%")
->orWhere("project_tasks.desc", "like", "%{$key}%");
});
} else {
$builder->where(function ($query) use ($key) {
$query->where("project_tasks.name", "like", "%{$key}%")
->orWhere("project_tasks.desc", "like", "%{$key}%");
});
}
$tasks = $builder->orderByDesc('project_tasks.id')->take($take)->get();
return $tasks->map(function ($task) { return $tasks->map(function ($task) {
$array = $task->toArray(); $array = $task->toArray();
@ -381,15 +353,10 @@ class SearchController extends AbstractController
$results = []; $results = [];
// 搜索用户自己的文件 // 搜索用户自己的文件
$builder = File::where('userid', $userid); $ownFiles = File::where('userid', $userid)
if (Base::isNumber($key)) { ->searchByKeyword($key)
$builder->where(function ($query) use ($key) { ->take($take)
$query->where("id", $key)->orWhere("name", "like", "%{$key}%"); ->get();
});
} else {
$builder->where("name", "like", "%{$key}%");
}
$ownFiles = $builder->take($take)->get();
foreach ($ownFiles as $file) { foreach ($ownFiles as $file) {
$results[] = array_merge($file->toArray(), [ $results[] = array_merge($file->toArray(), [
@ -401,24 +368,11 @@ class SearchController extends AbstractController
// 搜索共享给用户的文件 // 搜索共享给用户的文件
$remaining = $take - count($results); $remaining = $take - count($results);
if ($remaining > 0) { if ($remaining > 0) {
$builder = File::whereIn('pshare', function ($queryA) use ($userid) { $sharedFiles = File::sharedToUser($userid)
$queryA->select('files.id') ->searchByKeyword($key)
->from('files') ->take($remaining)
->join('file_users', 'files.id', '=', 'file_users.file_id') ->get();
->where('files.userid', '!=', $userid)
->where(function ($queryB) use ($userid) {
$queryB->whereIn('file_users.userid', [0, $userid]);
});
});
if (Base::isNumber($key)) {
$builder->where(function ($query) use ($key) {
$query->where("id", $key)->orWhere("name", "like", "%{$key}%");
});
} else {
$builder->where("name", "like", "%{$key}%");
}
$sharedFiles = $builder->take($remaining)->get();
foreach ($sharedFiles as $file) { foreach ($sharedFiles as $file) {
$temp = $file->toArray(); $temp = $file->toArray();
if ($file->pshare === $file->id) { if ($file->pshare === $file->id) {
@ -501,32 +455,28 @@ class SearchController extends AbstractController
*/ */
private function searchMessageByMysql(int $userid, string $key, int $take, int $dialogId = 0): array private function searchMessageByMysql(int $userid, string $key, int $take, int $dialogId = 0): array
{ {
$builder = DB::table('web_socket_dialog_msgs as m') $builder = WebSocketDialogMsg::select([
->select([ 'id as msg_id',
'm.id as msg_id', 'dialog_id',
'm.dialog_id', 'userid',
'm.userid', 'type',
'm.type', 'msg',
'm.msg', 'created_at',
'm.created_at',
]) ])
->join('web_socket_dialog_users as u', 'm.dialog_id', '=', 'u.dialog_id') ->accessibleByUser($userid)
->where('u.userid', $userid) ->where('bot', 0)
->where('m.bot', 0) ->searchByKeyword($key);
->where('m.key', 'like', "%{$key}%");
if ($dialogId > 0) { if ($dialogId > 0) {
$builder->where('m.dialog_id', $dialogId); $builder->where('dialog_id', $dialogId);
} }
$items = $builder->orderByDesc('m.id') $items = $builder->orderByDesc('id')
->limit($take) ->limit($take)
->get() ->get();
->all();
$results = []; return $items->map(function ($item) {
foreach ($items as $item) { return [
$results[] = [
'msg_id' => $item->msg_id, 'msg_id' => $item->msg_id,
'dialog_id' => $item->dialog_id, 'dialog_id' => $item->dialog_id,
'userid' => $item->userid, 'userid' => $item->userid,
@ -536,9 +486,7 @@ class SearchController extends AbstractController
'relevance' => 0, 'relevance' => 0,
'content_preview' => null, 'content_preview' => null,
]; ];
} })->toArray();
return $results;
} }
/** /**

View File

@ -128,6 +128,45 @@ class File extends AbstractModel
*/ */
const zipMaxSize = 1024 * 1024 * 1024; // 1G const zipMaxSize = 1024 * 1024 * 1024; // 1G
/**
* 按关键词搜索文件Scope
* 支持文件ID纯数字、文件名
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $keyword 搜索关键词
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSearchByKeyword($query, string $keyword)
{
if (is_numeric($keyword)) {
return $query->where(function ($q) use ($keyword) {
$q->where("id", intval($keyword))
->orWhere("name", "like", "%{$keyword}%");
});
}
return $query->where("name", "like", "%{$keyword}%");
}
/**
* 筛选用户可访问的共享文件Scope
* 不包括用户自己的文件,仅返回他人共享给该用户的文件
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $userid 用户ID
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSharedToUser($query, int $userid)
{
return $query->whereIn('pshare', function ($subQuery) use ($userid) {
$subQuery->select('files.id')
->from('files')
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $userid)
->where(function ($q) use ($userid) {
$q->whereIn('file_users.userid', [0, $userid]);
});
});
}
/** /**
* 获取文件列表 * 获取文件列表

View File

@ -164,6 +164,18 @@ class Project extends AbstractModel
return $query; return $query;
} }
/**
* 按关键词搜索项目Scope
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $keyword 搜索关键词
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSearchByKeyword($query, string $keyword)
{
return $query->where("projects.name", "like", "%{$keyword}%");
}
/** /**
* 获取任务统计数据 * 获取任务统计数据
* @param $userid * @param $userid

View File

@ -353,6 +353,32 @@ class ProjectTask extends AbstractModel
return $query; return $query;
} }
/**
* 按关键词搜索任务Scope
* 支持任务ID纯数字、任务名称、描述
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $keyword 搜索关键词
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSearchByKeyword($query, string $keyword)
{
if (is_numeric($keyword)) {
// 纯数字匹配任务ID 或 名称/描述
return $query->where(function ($q) use ($keyword) {
$q->where("project_tasks.id", intval($keyword))
->orWhere("project_tasks.name", "like", "%{$keyword}%")
->orWhere("project_tasks.desc", "like", "%{$keyword}%");
});
}
// 普通文本:搜索名称/描述
return $query->where(function ($q) use ($keyword) {
$q->where("project_tasks.name", "like", "%{$keyword}%")
->orWhere("project_tasks.desc", "like", "%{$keyword}%");
});
}
/** /**
* 生成描述 * 生成描述
* @param $content * @param $content

View File

@ -773,24 +773,51 @@ class User extends AbstractModel
}); });
} }
/**
* 按关键词搜索用户Scope
* 支持:邮箱(含@、用户ID纯数字、昵称/拼音/职业
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $keyword 搜索关键词
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSearchByKeyword($query, string $keyword)
{
if (str_contains($keyword, "@")) {
// 包含 @ 按邮箱搜索
return $query->where("email", "like", "%{$keyword}%");
}
if (is_numeric($keyword)) {
// 纯数字匹配用户ID 或 昵称/拼音/职业
return $query->where(function ($q) use ($keyword) {
$q->where("userid", intval($keyword))
->orWhere("nickname", "like", "%{$keyword}%")
->orWhere("pinyin", "like", "%{$keyword}%")
->orWhere("profession", "like", "%{$keyword}%");
});
}
// 普通文本:搜索昵称/拼音/职业
return $query->where(function ($q) use ($keyword) {
$q->where("nickname", "like", "%{$keyword}%")
->orWhere("pinyin", "like", "%{$keyword}%")
->orWhere("profession", "like", "%{$keyword}%");
});
}
/** /**
* 搜索用户 * 搜索用户
* @param $key * @param $key
* @param $take * @param $take
* @return User[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection * @return User[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
* @deprecated 建议使用 scopeSearchByKeyword
*/ */
public static function searchUser($key, $take = 20) public static function searchUser($key, $take = 20)
{ {
return User::select(User::$basicField) return User::select(User::$basicField)
->where(function ($query) use ($key) { ->searchByKeyword($key)
if (str_contains($key, "@")) { ->orderBy('userid')
$query->where("email", "like", "%{$key}%");
} else {
$query->where("nickname", "like", "%{$key}%")
->orWhere("pinyin", "like", "%{$key}%")
->orWhere("profession", "like", "%{$key}%");
}
})->orderBy('userid')
->take($take) ->take($take)
->get(); ->get();
} }

View File

@ -111,6 +111,36 @@ class WebSocketDialogMsg extends AbstractModel
return $this->hasOne(User::class, 'userid', 'userid'); return $this->hasOne(User::class, 'userid', 'userid');
} }
/**
* 按关键词搜索消息Scope
* 搜索 key 字段(消息的可搜索内容)
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $keyword 搜索关键词
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeSearchByKeyword($query, string $keyword)
{
return $query->where('key', 'like', "%{$keyword}%");
}
/**
* 筛选用户可访问的对话消息Scope
* 通过 web_socket_dialog_users 表验证用户对对话的访问权限
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $userid 用户ID
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeAccessibleByUser($query, int $userid)
{
return $query->whereIn('dialog_id', function ($subQuery) use ($userid) {
$subQuery->select('dialog_id')
->from('web_socket_dialog_users')
->where('userid', $userid);
});
}
/** /**
* 阅读占比 * 阅读占比
* @return int|mixed * @return int|mixed