where('type', $filters['type']); } if (!empty($filters['group_id'])) { $searchBuilder->where('group_id', $filters['group_id']); } if (!empty($filters['uploaded_by'])) { $searchBuilder->where('uploaded_by', $filters['uploaded_by']); } // 执行搜索并获取结果 $results = $searchBuilder->get(); // 应用用户权限过滤 return $this->filterByUserPermissions($results, $user); } catch (\Exception $e) { Log::error('文档搜索失败', [ 'query' => $query, 'user_id' => $user->id, 'filters' => $filters, 'error' => $e->getMessage(), ]); // 搜索失败时返回空集合 return new Collection(); } } /** * 根据用户权限过滤搜索结果 * 确保用户只能看到有权限访问的文档 * * @param Collection $results 搜索结果 * @param User $user 当前用户 * @return Collection */ public function filterByUserPermissions(Collection $results, User $user): Collection { // 获取用户所属的所有分组 ID $userGroupIds = $user->groups()->pluck('groups.id')->toArray(); return $results->filter(function (Document $document) use ($userGroupIds) { // 全局文档对所有用户可见 if ($document->type === 'global') { return true; } // 专用文档只对所属分组的用户可见 if ($document->type === 'dedicated') { return in_array($document->group_id, $userGroupIds); } return false; }); } /** * 准备文档的可搜索数据 * 包含完整的 Markdown 内容用于索引 * * @param Document $document 文档模型 * @return array */ public function prepareSearchableData(Document $document): array { return [ 'id' => $document->id, 'title' => $document->title, 'description' => $document->description, 'markdown_content' => $document->getMarkdownContent(), 'type' => $document->type, 'group_id' => $document->group_id, 'uploaded_by' => $document->uploaded_by, 'created_at' => $document->created_at?->timestamp, 'updated_at' => $document->updated_at?->timestamp, ]; } /** * 索引文档到 Meilisearch * 读取 Markdown 文件并创建搜索索引 * * @param Document $document 文档模型 * @return void */ public function indexDocument(Document $document): void { try { // 只索引已完成转换的文档 if (!$document->shouldBeSearchable()) { Log::info('文档未完成转换,跳过索引', [ 'document_id' => $document->id, 'conversion_status' => $document->conversion_status, ]); return; } // 使用 Scout 的 searchable 方法进行索引 $document->searchable(); Log::info('文档索引成功', [ 'document_id' => $document->id, 'title' => $document->title, ]); } catch (\Exception $e) { Log::error('文档索引失败', [ 'document_id' => $document->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); // 索引失败不影响文档的正常使用,只记录错误 } } /** * 更新文档在 Meilisearch 中的索引 * * @param Document $document 文档模型 * @return void */ public function updateDocumentIndex(Document $document): void { try { // 如果文档应该被索引,则更新索引 if ($document->shouldBeSearchable()) { $document->searchable(); Log::info('文档索引更新成功', [ 'document_id' => $document->id, 'title' => $document->title, ]); } else { // 如果文档不应该被索引(例如转换失败),则从索引中移除 $this->removeDocumentFromIndex($document); } } catch (\Exception $e) { Log::error('文档索引更新失败', [ 'document_id' => $document->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); // 索引更新失败不影响文档的正常使用,只记录错误 } } /** * 从 Meilisearch 中移除文档索引 * * @param Document $document 文档模型 * @return void */ public function removeDocumentFromIndex(Document $document): void { try { // 使用 Scout 的 unsearchable 方法移除索引 $document->unsearchable(); Log::info('文档索引移除成功', [ 'document_id' => $document->id, 'title' => $document->title, ]); } catch (\Exception $e) { Log::error('文档索引移除失败', [ 'document_id' => $document->id, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); // 索引移除失败不影响文档的正常删除,只记录错误 } } }