fastadmin+xunsearch 题库系统搭建教程

  • A+
所属分类:系统文档

前言

因为前段时间需要搭建一套题库系统,但是网上并没有很好的开源题库机缘巧合之下,看到 fastadmin 整合的 xunsearch 插件(提供了很好的搜索引擎)由此我决定尝试使用 fastadmin 来搭建一套题库系统,而且 fastadmin 便捷的 api 给了搜题系统很大的接口支持。可以很简单的写出需要的接口,还不用担心鉴权的问题(接口收费、接口鉴权等各种功能都很好写,都有对应的插件,简化接口搭建时间)

本文利用fastadmin后台框架 + xunsearch 插件,快速搭建题库系统

一、开始整活

服务器使用的是腾讯云的 99 一年的服务器本文搭建基于 CentOS8.0(xunsearch 服务器端必须使用 linux 系统,官网也是强烈推荐)服务器环境使用宝塔搭建

提示:xunsearch 服务端可以和题库系统分开部署在不同服务器上(作者并未尝试),有需求可以自己探究,本文仅展示 fastadmin 框架与 xunsearch 插件同时部署在一台服务器上。

二、开始搭建

宝塔与 fastadmin 的搭建就不再介绍了,这两个搭建很简单,网上都有教程。

1.fastadmin 配置

我们要将 fastadmin 插件市场中 xunsearch 插件安装进我们的框架

2.xunsearch 服务端安装

离线安装:讯搜官网在线安装(Linux 系统下):

curl -O http://www.xunsearch.com/download/xunsearch-full-latest.tar.bz2
tar -xvf xunsearch-full-latest.tar.bz2

安装 1.4.15 版本

cd xunsearch-full-1.4.15
sh setup.sh

接下来就是等待安装完成安装出现的问题:

(一).openssl 的版本与 libevent 版本不对应

在 centos8 中升级了 openssl,导致 xunseach 内 libevent 版本对应不上导致报错

解决方案

手动替换 libevent 文件下载 libevent 文件 2.1.12 版本(我替换了这个版本解决了这个问题,其他版本尚未确认)

wget https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz

将下载的文件打包

tar -zxvf libevent-2.1.12-stable.tar.gz

复制一份到 xunsearch 的 packages 文件夹内(删除旧版本的 libevent)

cp libevent-2.1.12-stable.tar.bz2  packages/

替换后

这样重新安装就不会出现版本不一致导致 libevent 出现问题

2.安装完成后配置

启动服务当安装完成 Xunsearch 后,我们可以通过以下命令进行启动服务

cd xunsearch-full-1.4.15
cd bin
./xs-ctl.sh start

启动完成后就可以和我们的 fastadmin 进行交互了(使用 fastadmin 默认配置即可)接下来我们进入 fastadmin 添加一个项目

字段配置必须要添加(id/title/body)这三个类型,不创建将导致配置生成失败

接下来我们生成一下配置

这时我们的题库已经搭建完成了,我们点击前台即可进入搜索页面.

但是这个时候我们的题库并没有数据,别着急请接着完成下面数据库的搭建!

3.数据库搭建

我们进入宝塔管理面板,去创建一个新的数据库

数据表创建与字段设置(请与上方 fastadmin 里 xunsearch 项目字段保持一致)

这时候请不要把题库导入该数据库

4.fastadmin 生成与代码修改

(一).fastadmin 数据表、控制器、菜单创建与生成

这时我们来到最后一步用 fastadmin 中的在线命令管理(这个也是个插件,需要在插件市场安装后使用),生成该数据表的控制器

菜单创建

我们就可以看到菜单出现(如果没有多出一个菜单请刷新缓存)

我这个是修改过菜单名称的,初始名称并不是这样,不过这个并不重要搭建到这个步骤可以看到使用这个菜单可以往数据库中加入数据,但是这样并不会往搜索引擎中加入索引数据

(二).fastadmin 控制器修改,调用 api 添加索引数据库

以下内容使用到fastadmin的xunsearch文档中关于api的调用

我们打开宝塔,进入网站根目录,用默认的宝塔文件管理打开目录为:/www/wwwroot/shouti/application/admin/controller/sou/Tiku.php 如果你一键生成的控制器不和我相同,那么最后文件目录也会有差异,这一步需要根据自己配置来找

打开后修改内容

默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法
因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑
需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改

我们可以在该文件中看到这个提示,根据提示,我们从[application/admin/library/traits/Backend.php]中找到添加、编辑、删除的代码


   public function add()
   {
       if ($this->request->isPost()) {
           $params = $this->request->post("row/a");
           if ($params) {
               $params = $this->preExcludeFields($params);

               if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
                   $params[$this->dataLimitField] = $this->auth->id;
               }
               $result = false;
               Db::startTrans();
               try {
                   
                   if ($this->modelValidate) {
                       $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
                       $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate;
                       $this->model->validateFailException(true)->validate($validate);
                   }
                   $result = $this->model->allowField(true)->save($params);
                   Db::commit();
               } catch (ValidateException $e) {
                   Db::rollback();
                   $this->error($e->getMessage());
               } catch (PDOException $e) {
                   Db::rollback();
                   $this->error($e->getMessage());
               } catch (Exception $e) {
                   Db::rollback();
                   $this->error($e->getMessage());
               }
               if ($result !== false) {
                   $this->success();
               } else {
                   $this->error(__('No rows were inserted'));
               }
           }
           $this->error(__('Parameter %s can not be empty', ''));
       }
       return $this->view->fetch();
   }

   
   public function edit($ids = null)
   {
       $row = $this->model->get($ids);
       if (!$row) {
           $this->error(__('No Results were found'));
       }
       $adminIds = $this->getDataLimitAdminIds();
       if (is_array($adminIds)) {
           if (!in_array($row[$this->dataLimitField], $adminIds)) {
               $this->error(__('You have no permission'));
           }
       }
       if ($this->request->isPost()) {
           $params = $this->request->post("row/a");
           if ($params) {
               $params = $this->preExcludeFields($params);
               $result = false;
               Db::startTrans();
               try {
                   
                   if ($this->modelValidate) {
                       $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
                       $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
                       $row->validateFailException(true)->validate($validate);
                   }
                   $result = $row->allowField(true)->save($params);
                   Db::commit();
               } catch (ValidateException $e) {
                   Db::rollback();
                   $this->error($e->getMessage());
               } catch (PDOException $e) {
                   Db::rollback();
                   $this->error($e->getMessage());
               } catch (Exception $e) {
                   Db::rollback();
                   $this->error($e->getMessage());
               }
               if ($result !== false) {
                   $this->success();
               } else {
                   $this->error(__('No rows were updated'));
               }
           }
           $this->error(__('Parameter %s can not be empty', ''));
       }
       $this->view->assign("row", $row);
       return $this->view->fetch();
   }

   
   public function del($ids = "")
   {
       if (!$this->request->isPost()) {
           $this->error(__("Invalid parameters"));
       }
       $ids = $ids ? $ids : $this->request->post("ids");
       if ($ids) {
           $pk = $this->model->getPk();
           $adminIds = $this->getDataLimitAdminIds();
           if (is_array($adminIds)) {
               $this->model->where($this->dataLimitField, 'in', $adminIds);
           }
           $list = $this->model->where($pk, 'in', $ids)->select();

           $count = 0;
           Db::startTrans();
           try {
               foreach ($list as $k => $v) {
                   $count += $v->delete();
               }
               Db::commit();
           } catch (PDOException $e) {
               Db::rollback();
               $this->error($e->getMessage());
           } catch (Exception $e) {
               Db::rollback();
               $this->error($e->getMessage());
           }
           if ($count) {
               $this->success();
           } else {
               $this->error(__('No rows were deleted'));
           }
       }
       $this->error(__('Parameter %s can not be empty', 'ids'));
   }

我们将这段代码复制进刚刚打开的文件中

同时将这些方法根据 fastadmin 提供的 api 进行修改,达到添加 xunsearch 数据库索引的效果添加修改后代码


    public function add()
    {
        $search = \addons\xunsearch\library\Xunsearch::instance("souti");


        if ($this->request->isPost()) {
            $params = $this->request->post("row/a");
            if ($params) {
                $params = $this->preExcludeFields($params);

                if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
                    $params[$this->dataLimitField] = $this->auth->id;
                }
                $result = false;

                Db::startTrans();
                try {
                    
                    if ($this->modelValidate) {
                        $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
                        $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate;
                        $this->model->validateFailException(true)->validate($validate);
                    }
                    $result = $this->model->allowField(true)->save($params);
                    Db::commit();
                    $lastid = Db::table('sou_tiku')->where('id>0')->max('id');
                    $data = [
                        'id'=>$lastid,
                        'title'=>"[ID".$lastid."]:".$params['title'],
                        'body'=>$params['body'],
                        'ans'=> $params['ans']
                    ];
                    $search->add($data);
                } catch (ValidateException $e) {
                    Db::rollback();
                    $this->error($e->getMessage());
                } catch (PDOException $e) {
                    Db::rollback();
                    $this->error($e->getMessage());
                } catch (Exception $e) {
                    Db::rollback();
                    $this->error($e->getMessage());
                }
                if ($result !== false) {
                    $this->success();
                } else {
                    $this->error(__('No rows were inserted'));
                }
            }
            $this->error(__('Parameter %s can not be empty', ''));
        }

        return $this->view->fetch();
    }

添加部分解析我首先获取到最后一位 id(添加进索引数据库必须拥有一个自己的 id,通过 id 才可以修改此条题目状态),这个 id 我放到题目上,如果用户投诉答案出错,可以向开发者提供此题目 id,开发者可以根据 id 快速定位该题目然后接下来就是根据 fastadmin 写好的,获取每一条数据,根据 fastadmin 提供的 api 导入进索引数据库

$lastid = Db::table('sou_tiku')->where('id>0')->max('id');
                    $data = [
                        'id'=>$lastid,
                        'title'=>"[ID".$lastid."]:".$params['title'],
                        'body'=>$params['body'],
                        'ans'=> $params['ans']
                    ];
                    $search->add($data);

编辑修改后代码


    public function edit($ids = null)
    {
        $search = \addons\xunsearch\library\Xunsearch::instance("souti");
        $row = $this->model->get($ids);
        if (!$row) {
            $this->error(__('No Results were found'));
        }
        $adminIds = $this->getDataLimitAdminIds();
        if (is_array($adminIds)) {
            if (!in_array($row[$this->dataLimitField], $adminIds)) {
                $this->error(__('You have no permission'));
            }
        }
        if ($this->request->isPost()) {
            $params = $this->request->post("row/a");
            if ($params) {
                $params = $this->preExcludeFields($params);
                $result = false;
                Db::startTrans();
                try {
                    
                    if ($this->modelValidate) {
                        $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
                        $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
                        $row->validateFailException(true)->validate($validate);
                    }
                    $result = $row->allowField(true)->save($params);
                    Db::commit();
                    $data = [
                        'id'=>$ids,
                        'title'=>"[ID".$ids."]:".$params['title'],
                        'body'=>$params['body'],
                        'ans'=>$params['ans']
                    ];
                    $search->update($data);
                } catch (ValidateException $e) {
                    Db::rollback();
                    $this->error($e->getMessage());
                } catch (PDOException $e) {
                    Db::rollback();
                    $this->error($e->getMessage());
                } catch (Exception $e) {
                    Db::rollback();
                    $this->error($e->getMessage());
                }
                if ($result !== false) {
                    $this->success();
                } else {
                    $this->error(__('No rows were updated'));
                }
            }
            $this->error(__('Parameter %s can not be empty', ''));
        }
        $this->view->assign("row", $row);
        return $this->view->fetch();
    }

编辑部分解析编辑该数据的同时,我们将此次编辑也通过 api 进行修改到索引数据库

$data = [
  'id'=>$ids,
  'title'=>"[ID".$ids."]:".$params['title'],
  'body'=>$params['body'],
  'ans'=>$params['ans']
];
$search->update($data);

删除修改后代码


    public function del($ids = "")
    {
        $search = \addons\xunsearch\library\Xunsearch::instance("souti");
        if (!$this->request->isPost()) {
            $this->error(__("Invalid parameters"));
        }
        $ids = $ids ? $ids : $this->request->post("ids");
        if ($ids) {
            $pk = $this->model->getPk();
            $adminIds = $this->getDataLimitAdminIds();
            if (is_array($adminIds)) {
                $this->model->where($this->dataLimitField, 'in', $adminIds);
            }
            $list = $this->model->where($pk, 'in', $ids)->select();

            $count = 0;
            Db::startTrans();
            try {
                foreach ($list as $k => $v) {
                    $count += $v->delete();
                }
                Db::commit();
                $search->del($ids);
            } catch (PDOException $e) {
                Db::rollback();
                $this->error($e->getMessage());
            } catch (Exception $e) {
                Db::rollback();
                $this->error($e->getMessage());
            }
            if ($count) {
                $this->success();
            } else {
                $this->error(__('No rows were deleted'));
            }
        }
        $this->error(__('Parameter %s can not be empty', 'ids'));
    }

删除部分解析这里是当数据表中数据删除的时候,我们也应该删除索引数据库里面的数据

$search->del($ids);

至此从 fastadmin 中添加/编辑/删除的操作,全部会同步到题库系统中.重要提示:在 fastadmin 控制台只能一条一条的插入题库,因为导入的代码并未修改,所以导入进去的数据是无法查询的

这里 id 不一致是因为添加的时候 id 没有+1,上面代码未修改此问题,请自行修改[添加]部分代码.

(三).接口搭建

因为一个一个导入真的是太慢了.我这边解决方案就是搭建一个接口,通过接口上传题目

我这边创建了一个新的接口,如果是小白,可以把接口搭建在 demo 中

同时将接口鉴权关闭加入题目函数

public function insertans()
    {
        $search = \addons\xunsearch\library\Xunsearch::instance("souti");

        $title = $this->request->request('title');
        $body = $this->request->request('body');
        $ans = $this->request->request('ans');
        $lastid = Db::table('sou_tiku')->where('id>0')->max('id');
        $data = ['title' => $title, 'body' => $body , 'ans' => $ans];
        Db::startTrans();
        try {
                Db::table('sou_tiku')->insert($data);
                Db::commit();
                $sdata = [
                    'id'=>$lastid,
                    'title'=>"[ID".$lastid."]:".$title,
                    'body'=>$body,
                    'ans'=> $ans
                ];
                $search->add($sdata);
            } catch (ValidateException $e) {
                Db::rollback();
                $this->error($e->getMessage());
            } catch (PDOException $e) {
                Db::rollback();
                $this->error($e->getMessage());
            } catch (Exception $e) {
                Db::rollback();
                $this->error($e->getMessage());
            }
        $this->success('返回成功',$res);
        
    }

搜索题目返回函数

public function searchans()
    {
        $que = $this->request->request('q');
        $search = \addons\xunsearch\library\Xunsearch::instance("souti");
        $res = $search->search($que,1,10);
        $this->success('返回成功',$res);
    }

如果你实在不会写,请在该目录下创建 sou.php 并加入以下代码

<?php

namespace app\api\controller;

use app\common\controller\Api;
use think\Db;


class Sou extends Api
{

    
    
    
    
    
    protected $noNeedLogin = ['searchans','insertans'];
    
    protected $noNeedRight = [];

    public function insertans()
    {
        $search = \addons\xunsearch\library\Xunsearch::instance("souti");

        $title = $this->request->request('title');
        $body = $this->request->request('body');
        $ans = $this->request->request('ans');
        $lastid = Db::table('sou_tiku')->where('id>0')->max('id');
        $data = ['title' => $title, 'body' => $body , 'ans' => $ans];
        Db::startTrans();
        try {
                Db::table('sou_tiku')->insert($data);
                Db::commit();
                $sdata = [
                    'id'=>$lastid,
                    'title'=>"[ID".$lastid."]:".$title,
                    'body'=>$body,
                    'ans'=> $ans
                ];
                $search->add($sdata);
            } catch (ValidateException $e) {
                Db::rollback();
                $this->error($e->getMessage());
            } catch (PDOException $e) {
                Db::rollback();
                $this->error($e->getMessage());
            } catch (Exception $e) {
                Db::rollback();
                $this->error($e->getMessage());
            }
        $this->success('返回成功',$res);
        
    }
    public function searchans()
    {
        $que = $this->request->request('q');
        $search = \addons\xunsearch\library\Xunsearch::instance("souti");
        $res = $search->search($que,1,10);
        $this->success('返回成功',$res);
    }
}

具体代码含义,请自行查询 thinkphp

(四).接口测试

1.插入题目

调用网址:https://你的网站/api/sou/insertans?title=&body=&ans=中文必须进行 url 编码,可以使用 get 或者 post 进行提交,title 为题目,body 为选项,ans 为答案具体字段可以自行修改

2.查询题目

调用网址:http://你的网站/api/sou/searchans?q="zg"返回的是一段 json 代码,可以根据需求自己解析使用

总结

搭建比较繁琐,新手建议先看fastadmin官方给的教程,bilibili 上也有的小白请先看上面教程,不然会安装到你崩溃看完前几集其实就差不多了,可以对比此教程看视频,学会控制器修改,api 接口即可搭建。

在 thinkphp 方面作者也是个小白,教程中一些地方写的并不是很标准,目的只是搭建能够查询的题库,更安全更标准的搭建方式,请自行探索

  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin