Bootstrap

fastadmin+xunsearch题库系统搭建教程

前言

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

本文利用 + 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添加索引数据库

以下内容使用到

我们打开宝塔,进入网站根目录,用默认的宝塔文件管理打开目录为:/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);
        //Db::table('sou_tiku')->insert($data);
    }

搜索题目返回函数

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并加入以下代码

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);
        //Db::table('sou_tiku')->insert($data);
    }
    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代码,可以根据需求自己解析使用

总结

搭建比较繁琐,新手建议先看,bilibili上也有的小白请先看上面教程,不然会安装到你崩溃看完前几集其实就差不多了,可以对比此教程看视频,学会控制器修改,api接口即可搭建在thinkphp方面作者也是个小白,教程中一些地方写的并不是很标准,目的只是搭建能够查询的题库,更安全更标准的搭建方式,请自行探索