feat: 新增多类型搜索源支持及后台管理功能

- 新增全网搜索线路配置功能,支持自定义搜索线路
- 新增多类型搜索源支持:API接口、TG频道、网页爬虫
- 新增后台搜索线路管理界面
- 优化搜索架构,由固定接口改为可配置模式
- 支持搜索线路权重设置,支持优先级调整
This commit is contained in:
admin
2025-05-15 17:14:56 +08:00
parent 52eac07a9b
commit e6b632c1f2
9 changed files with 1649 additions and 193 deletions

View File

@@ -18,6 +18,13 @@
## 🚀 更新日志
### v3.2
- 🔄 新增 全网搜索线路配置功能,支持自定义搜索线路
- 🌐 新增 多类型搜索源支持API接口、TG频道、网页爬虫
- ⚙️ 新增 后台搜索线路管理界面
- 🛠 优化 搜索架构,由固定接口改为可配置模式
- 📊 搜索线路权重设置,支持优先级调整
### v3.1
- 🔍 优化 全网搜索模式:由直接展示转存结果,改为先显示第三方搜索结果,用户点击后再转存分享
- 🌐 全网搜索支持 夸克网盘 和 百度网盘

View File

@@ -0,0 +1,258 @@
<?php
namespace app\admin\controller;
use think\App;
use app\admin\QfShop;
use app\model\ApiList as ApiListModel;
class ApiList extends QfShop
{
public function __construct(App $app)
{
parent::__construct($app);
//查询列表时允许的字段
$this->selectList = "*";
//查询详情时允许的字段
$this->selectDetail = "*";
//筛选字段
$this->searchFilter = [
];
$this->insertFields = [
//允许添加的字段列表
"name","type","url","method","fixed_params","headers","field_map","status","weight","pantype","count","html_item","html_title","html_url","html_type","html_url2"
];
$this->updateFields = [
//允许更新的字段列表
"name","type","url","method","fixed_params","headers","field_map","status","weight","pantype","count","html_item","html_title","html_url","html_type","html_url2"
];
$this->insertRequire = [
//添加时必须填写的字段
// "字段名称"=>"该字段不能为空"
"name"=>"分类名称必须填写",
];
$this->updateRequire = [
//修改时必须填写的字段
// "字段名称"=>"该字段不能为空"
"name"=>"分类名称必须填写",
];
$this->model = new ApiListModel();
}
/**
* 获取列表接口
*
* @return void
*/
public function getList()
{
//校验Access与RBAC
$error = $this->access();
if ($error) {
return $error;
}
//查询数据
$dataList = $this->model->order('weight', 'desc')->select();
return jok('数据获取成功', $dataList);
}
/**
* 获取详情基类 子类自动继承 如有特殊需求 可重写到子类 请勿修改父类方法
*
* @return void
*/
public function detail()
{
//校验Access与RBAC
$error = $this->access();
if ($error) {
return $error;
}
$id = input("id");
if (!$id) {
return jerr("ID参数必须填写", 400);
}
//根据主键获取一行数据
$item = $this->model->where("id", $id)->field($this->selectDetail)->find();
if (empty($item)) {
return jerr("没有查询到数据", 404);
}
return jok('数据加载成功', $item);
}
/**
* 添加接口基类 子类自动继承 如有特殊需求 可重写到子类 请勿修改父类方法
*
* @return void
*/
public function add()
{
//校验Access与RBAC
$error = $this->access();
if ($error) {
return $error;
}
//校验Insert字段是否填写
$error = $this->validateInsertFields();
if ($error) {
return $error;
}
//从请求中获取Insert数据
$data = $this->getInsertDataFromRequest();
//添加这行数据
$data['create_time'] = time();
$data['update_time'] = time();
$this->model->insertGetId($data);
return jok('添加成功');
}
/**
* 修改接口基类 子类自动继承 如有特殊需求 可重写到子类 请勿修改父类方法
*
* @return void
*/
public function update()
{
//校验Access与RBAC
$error = $this->access();
if ($error) {
return $error;
}
$id = input("id");
if (!$id) {
return jerr("ID参数必须填写", 400);
}
//根据主键获取一行数据
$item = $this->model->where("id", $id)->field($this->selectDetail)->find();
if (empty($item)) {
return jerr("数据查询失败", 404);
}
//校验Update字段是否填写
$error = $this->validateUpdateFields();
if ($error) {
return $error;
}
//从请求中获取Update数据
$data = $this->getUpdateDataFromRequest();
//根据主键更新这条数据
$data['update_time'] = time();
$this->model->where("id", $id)->update($data);
return jok('修改成功');
}
/**
* 删除接口基类 子类自动继承 如有特殊需求 可重写到子类 请勿修改父类方法
*
* @return void
*/
public function delete()
{
//校验Access与RBAC
$error = $this->access();
if ($error) {
return $error;
}
$id = input("id");
if (!$id) {
return jerr("ID参数必须填写", 400);
}
//根据主键获取一行数据
$item = $this->model->where("id", $id)->field($this->selectDetail)->find();
if (empty($item)) {
return jerr("数据查询失败", 404);
}
//单个操作
$map = ["id" => $id];
$this->model->where($map)->delete();
return jok('删除成功');
}
/**
* 禁用接口基类 子类自动继承 如有特殊需求 可重写到子类 请勿修改父类方法
*
* @return void
*/
public function disable()
{
//校验Access与RBAC
$error = $this->access();
if ($error) {
return $error;
}
$id = input("id");
if (!$id) {
return jerr("ID参数必须填写", 400);
}
$d = [
"status" => 0,
"update_time" => time(),
];
if (isInteger($id)) {
//根据主键获取一行数据
$item = $this->model->where("id", $id)->field($this->selectDetail)->find();
if (empty($item)) {
return jerr("数据查询失败", 404);
}
//单个操作
$map = ["id" => $id];
$this->model->where($map)->update($d);
} else {
//批量操作
$list = explode(',', $id);
$this->model->where("id", 'in', $list)->update($d);
}
return jok("禁用成功");
}
/**
* 启用接口基类 子类自动继承 如有特殊需求 可重写到子类 请勿修改父类方法
*
* @return void
*/
public function enable()
{
//校验Access与RBAC
$error = $this->access();
if ($error) {
return $error;
}
$id = input("id");
if (!$id) {
return jerr("ID参数必须填写", 400);
}
$d = [
"status" => 1,
"update_time" => time(),
];
if (isInteger($id)) {
//根据主键获取一行数据
$item = $this->model->where("id", $id)->field($this->selectDetail)->find();
if (empty($item)) {
return jerr("数据查询失败", 404);
}
//单个操作
$map = ["id" => $id];
$this->model->where($map)->update($d);
} else {
//批量操作
$list = explode(',', $id);
$this->model->where("id", 'in', $list)->update($d);
}
return jok("启用成功");
}
}

View File

@@ -8,6 +8,7 @@ use think\facade\Request;
use app\api\QfShop;
use app\model\Source as SourceModel;
use app\model\Days as DaysModel;
use app\model\ApiList as ApiListModel;
class Other extends QfShop
{
@@ -15,104 +16,804 @@ class Other extends QfShop
{
parent::__construct($app);
$this->model = new SourceModel();
$this->ApiListModel = new ApiListModel();
}
public function search1()
/**
* 全网搜索 该接口用户网页端使用
*
* @return void
*/
public function web_search()
{
$searchdata = input('');
if (empty($title)) {
if (empty($searchdata['title'])) {
return jerr("请输入名称");
}
$title = $searchdata['title'];
}
$apiType = $searchdata['num']??0;
$is_type = $searchdata['is_type']??0;
$searchList = []; // 查询的结果集
$num_total = 10; // 最多想要几条结果
$num_success = 0;
$sources = ['source1'];
// 遍历所有源
foreach ($sources as $source) {
if ($num_success >= $num_total) {
break; // 有效结果数量已达到
}
foreach ($source(Config('qfshop.is_quan_zc'),$title,$is_type,5,$apiType) as $value) {
if ($num_success >= $num_total) {
break; // 有效结果数量已达到
}
// 如果 URL 不存在则新增 $value
if (!$this->urlExists($searchList, $value['url'])) {
$searchList[] = $value;
$num_success++;
}
}
}
// 设置 SSE 响应头
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no'); // 防止 Nginx 缓冲
$searchList = array_map(function($value) {
$value['is_type'] = determineIsType($value['url']);
if(Config('qfshop.is_quan_type') != 1){
$value['url'] = encryptObject($value['url']);
}
return $value;
}, $searchList);
$title = input('title', '');
if (empty($title)) {
echo "data: [DONE] 无搜索词\n\n";
ob_flush();
flush();
exit;
}
$is_type = input('is_type', 0); //0夸克 2百度
$is_show = input('is_show', 0); //0加密网址 1显示网址
// 查找一条可用线路
$lines = $this->ApiListModel->where('status', 1)->where('pantype', $is_type)->order('weight desc')->select()->toArray();
return jok('网盘资源均来源于互联网,资源内容与本站无关', $searchList);
// 获取自定义线路并合并到线路列表前面
$lines = array_merge($this->getCustomLines(), $lines);
if (!$lines || count($lines) == 0) {
echo "data: [DONE] 暂无可用线路\n\n";
ob_flush();
flush();
exit;
}
foreach ($lines as $line) {
$result = [];
$type = $line['type'] ?? 'api';
if ($type === 'tg') {
$result = $this->handleTg($line, $title);
} else if ($type === 'api') {
$result = $this->handleApi($line, $title);
} else if ($type === 'html') {
$result = $this->handleWeb($line, $title);
} else if ($type === 'kk') {
$result = $this->handleKk($line, $title, $line['num']);
}
foreach ($result as $item) {
$item['is_type'] = determineIsType($item['url']);
if(Config('qfshop.is_quan_zc')==1){
//检测是否有效
$infoData = $this->verificationUrl($item['url']);
if (!empty($infoData['stoken'])) {
$item['stoken'] = $infoData['stoken'];
}
if($infoData === 0) {
continue;
}
}
if(config('qfshop.is_quan_type') != 1 && $is_show != 1){
$item['url'] = encryptObject($item['url']);
}
echo "data: " . str_replace(["\n", "\r"], '', json_encode($item, JSON_UNESCAPED_UNICODE)) . "\n\n";
ob_flush();
flush();
}
}
echo "data: [DONE]\n\n";
ob_flush();
flush();
exit;
}
public function search2()
/**
* 全网搜索 该接口仅用于机器人和微信对话时使用
*
* @return void
*/
public function all_search($param='')
{
$searchdata = input('');
$title = $param ?: input('title', '');
if (empty($title)) {
if (empty($searchdata['title'])) {
return jerr("请输入名称");
}
$title = $searchdata['title'];
return jerr("请输入要看的内容");
}
$is_type = $searchdata['is_type']??0;
$is_type = 0; //0夸克 2百度
$map[] = ['status', '=', 1];
$map[] = ['is_delete', '=', 0];
$map[] = ['is_time', '=', 1];
$map[] = ['title|description', 'like', '%' . trim($title) . '%'];
$urls = $this->model->where($map)->field('source_id as id, title, url,is_time')->order('update_time', 'desc')->limit(5)->select()->toArray();
if (!empty($urls)) {
$ids = array_column($urls, 'id');
$this->model->whereIn('source_id', $ids)->update(['update_time' => time()]);
return !empty($param) ? $urls : jok('临时资源获取成功', $urls);
}
//同一个搜索内容锁机
if (Cache::has($title)) {
// 检查缓存中是否已有结果
return !empty($param) ? Cache::get($title) : jok('临时资源获取成功', Cache::get($title));
}
// 检查是否有正在处理的请求
if (Cache::has($title . '_processing')) {
// 如果当前正在处理相同关键词的请求,等待结果
$startTime = time(); // 记录开始时间
while (Cache::has($title . '_processing')) {
usleep(1000000); // 暂停1秒
// 检查是否超过60秒
if (time() - $startTime > 60) {
return !empty($param) ? [] : jok('临时资源获取成功', []);
}
}
return !empty($param) ? Cache::get($title) : jok('临时资源获取成功', Cache::get($title));
}
// 设置处理状态为正在处理
Cache::set($title . '_processing', true, 60); // 锁定60秒
$searchList = []; // 查询的结果集
$num_total = 10; // 最多想要几条结果
$$typeV = input('type', 0);
$searchList = []; //查询的结果集
$datas = []; //最终转存后的数据
$num_total = 2; //最多想要几条转存后的结果
$num_success = 0;
$datas_zc = []; //最终未转存的数据
$num_total_zc = $$typeV==1?3:0; //最多想要几条未转存的结果
$num_success_zc = 0;
// 查找一条可用线路
$lines = $this->ApiListModel->where('status', 1)->where('pantype', $is_type)->order('weight desc')->select()->toArray();;
// 定义源的顺序
$sources = ['source2'];
// 获取自定义线路并合并到线路列表前面
$lines = array_merge($this->getCustomLines(), $lines);
if (!$lines || count($lines) == 0) {
Cache::set($title, $datas, 60); // 缓存结果60秒
Cache::delete($title . '_processing'); // 解锁
return !empty($param) ? $datas : jok('临时资源获取成功', $datas);
}
foreach ($lines as $line) {
if ($num_success >= $num_total && $num_success_zc >= $num_total_zc) {
break;
}
$result = [];
$type = $line['type'] ?? 'api';
if ($type === 'tg') {
$result = $this->handleTg($line, $title);
} else if ($type === 'api') {
$result = $this->handleApi($line, $title);
} else if ($type === 'html') {
$result = $this->handleWeb($line, $title);
} else if ($type === 'kk') {
$result = $this->handleKk($line, $title, $line['num']);
}
foreach ($result as $item) {
if ($num_success < $num_total) {
//检测是否有效
$infoData = $this->verificationUrl($item['url']);
if (!empty($infoData['stoken'])) {
$item['stoken'] = $infoData['stoken'];
}
if ($infoData !== 0) {
if (!$this->urlExists($searchList, $item['url'])) {
$searchList[] = $item;
$this->processUrl($item, $num_success, $datas);
}
}
}else if($num_success_zc < $num_total_zc){
//检测是否有效
$infoData = $this->verificationUrl($item['url']);
if (!empty($infoData['stoken'])) {
$item['stoken'] = $infoData['stoken'];
}
if ($infoData !== 0) {
if (!$this->urlExists($searchList, $item['url'])) {
$titles = array_column($searchList, 'title');
if (!in_array($item['title'], $titles)) {
$searchList[] = $item;
$datas_zc[] = $item;
$num_success_zc++;
}
}
}
}
}
}
Cache::set($title, $datas, 60); // 缓存结果60秒
Cache::delete($title . '_processing'); // 解锁
if($$typeV == 1){
$datas = array_merge($datas, $datas_zc);
}
// 遍历所有源
foreach ($sources as $source) {
if ($num_success >= $num_total) {
break; // 有效结果数量已达到
return !empty($param) ? $datas : jok('临时资源获取成功', $datas);
}
/**
* 获取自定义线路配置
* @return array 自定义线路数组
*/
private function getCustomLines()
{
// 自定义线路 - 线路一
// $customLines = array_map(function ($i) {
// return [
// 'name' => '自定义线路一',
// 'pantype' => 0,
// 'type' => 'kk',
// 'count' => 5,
// 'num' => $i,
// ];
// }, range(1, 6));
// 可以在这里添加更多自定义线路
// 例如:
/*
$customLines[] = [
'name' => '自定义线路二',
'pantype' => 0,
'type' => 'GG',
'count' => 5,
];
*/
return $customLines??[];
}
/**
* 接口类型处理
*/
private function handleApi($line, $title)
{
$type = $line['pantype'];
$maxCount = $line['count'];
// 根据类型选择搜索参数
$panType = [
0 => 'quark', // 夸克
2 => 'baidu' // 百度
];
if (!isset($panType[$type]) || $maxCount <= 0) {
return [];
}
$url = $line['url'];
$method = strtoupper($line['method']);
$headers = json_decode($line['headers'], true) ?? [];
$params = json_decode($line['fixed_params'], true) ?? [];
// 替换 {keyword}
foreach ($params as &$val) {
$val = str_replace('{keyword}', $title, $val);
}
// headers 转为 curl 格式
$headerArr = [];
foreach ($headers as $k => $v) {
$headerArr[] = "$k: $v";
}
// 确保POST请求有正确的Content-Type
if ($method === 'POST' && !isset($headers['Content-Type'])) {
$headerArr[] = "Content-Type: application/x-www-form-urlencoded";
}
// 简化参数处理
$queryParams = $method === 'GET' ? $params : [];
// 处理POST数据
if ($method === 'POST' && !empty($params)) {
$postData = http_build_query($params);
$result = curlHelper($url, $method, $postData, $headerArr, $queryParams);
} else {
$result = curlHelper($url, $method, $method === 'GET' ? null : $params, $headerArr, $queryParams);
}
if (empty($result['body'])) {
return [];
}
$fieldMap = json_decode($line['field_map'], true);
$response = json_decode($result['body'], true);
$results = $this->extractList($response, $fieldMap, $type);
return array_slice($results, 0, $maxCount);
}
/**
* 提取字段
*/
protected function extractList($response, $fieldMap, $type)
{
$listPath = explode('.', $fieldMap['list_path'] ?? '');
$listData = $response;
foreach ($listPath as $key) {
if (isset($listData[$key])) {
$listData = $listData[$key];
} else {
return [];
}
}
$fields = $fieldMap['fields'] ?? [];
$result = [];
foreach ($listData as $item) {
$row = [];
foreach ($fields as $targetKey => $sourcePath) {
$value = $item;
foreach (explode('.', $sourcePath) as $p) {
$value = $value[$p] ?? null;
}
$row[$targetKey] = $value;
if($targetKey=='url'){
// 将任何类型的值转换为字符串
$stringValue = '';
if(is_array($value)) {
// 原始数组转字符串
$stringValue = json_encode($value, JSON_UNESCAPED_UNICODE);
// JSON 中链接中的 / 会变成 \/,替换回来
$stringValue = str_replace('\/', '/', $stringValue);
} else {
$stringValue = (string)$value;
}
// 从字符串中提取夸克网盘链接
if($type === 0 && preg_match('/https:\/\/pan\.quark\.cn\/s\/[a-zA-Z0-9]+/', $stringValue, $urlMatch)) {
$row['url'] = trim($urlMatch[0]);
}
// 从字符串中提取百度网盘链接
else if($type === 2 && preg_match('/https:\/\/pan\.baidu\.com\/s\/[a-zA-Z0-9_-]+(\?pwd=[a-zA-Z0-9]+)?/', $stringValue, $urlMatch)) {
$row['url'] = trim($urlMatch[0]);
// 检查URL中是否已有pwd参数如果没有但字符串中有pwd字段则添加
if(!strpos($row['url'], '?pwd=') && preg_match('/["\'](pwd|code)["\']\s*:\s*["\']([^"\']+)["\']/', $stringValue, $pwdMatches)) {
$row['url'] .= '?pwd=' . $pwdMatches[2];
}
}
else {
$row['url'] = '';
}
}
}
if(!empty($row['url'])){
$result[] = $row;
}
}
return $result;
}
/**
* TG频道类型处理
*/
private function handleTg($line, $title)
{
$type = $line['pantype'];
$maxCount = $line['count'];
// 根据类型选择搜索参数
$panType = [
0 => 'quark', // 夸克
2 => 'baidu' // 百度
];
if (!isset($panType[$type]) || $maxCount <= 0) {
return [];
}
$results = [];
$url = 'https://t.me/s/NewQuark?q='.urlencode($title);
$dom = getDom($url);
$finder = new \DomXPath($dom);
$nodes = $finder->query('//div[contains(@class, "tgme_widget_message_text")]');
foreach ($nodes as $node) {
// 获取 HTML 内容
$htmlContent = $dom->saveHTML($node);
// 提取标题名称xxx
if (preg_match('/名称:(.+?)<br/i', $htmlContent, $titleMatch)) {
$parsedItem['title'] = trim(strip_tags($titleMatch[1]));
} else {
$parsedItem['title'] = $title;
}
foreach ($source(Config('qfshop.is_quan_zc'),$title,$is_type) as $value) {
if ($num_success >= $num_total) {
break; // 有效结果数量已达到
}
// 提取夸克链接(可支持百度扩展)
$parsedItem['url'] = '';
if ($type === 0 && preg_match('/https:\/\/pan\.quark\.cn\/s\/[a-zA-Z0-9]+/', $htmlContent, $urlMatch)) {
$parsedItem['url'] = trim($urlMatch[0]);
} elseif ($type === 2 && preg_match('/https:\/\/pan\.baidu\.com\/s\/[a-zA-Z0-9_-]+(\?pwd=[a-zA-Z0-9]+)?/', $htmlContent, $urlMatch)) {
$parsedItem['url'] = trim($urlMatch[0]);
}
// 如果 URL 不存在则新增 $value
if (!$this->urlExists($searchList, $value['url'])) {
$searchList[] = $value;
$num_success++;
// 过滤不合法或无效链接
if ($parsedItem['title'] && $parsedItem['url']) {
$results[] = $parsedItem;
}
if (count($results) >= $maxCount) {
return $results;
}
}
return $results;
}
/**
* 网页类型处理
*/
private function handleWeb($line, $title)
{
$results = [];
// 替换搜索关键词并获取配置参数
$url = str_replace('{keyword}', urlencode($title), $line['url']);
$parts = explode('+', $line['html_item'], 2);
$tag = $parts[0] ?? '';
$classString = $parts[1] ?? '';
$partsTitle = explode('+', $line['html_title'], 2);
$tagTitle = $partsTitle[0] ?? '';
$classStringTitle = $partsTitle[1] ?? '';
$partsUrl = explode('+', $line['html_url2'], 2);
$tagUrl = $partsUrl[0] ?? '';
$classStringUrl = $partsUrl[1] ?? '';
$maxCount = $line['count'] ?? 10;
$type = $line['pantype'];
// 定义网盘链接匹配规则
$panPatterns = [
0 => '/https:\/\/pan\.quark\.cn\/s\/[a-zA-Z0-9]+/', // 夸克
2 => '/https:\/\/pan\.baidu\.com\/s\/[a-zA-Z0-9_-]+(\?pwd=[a-zA-Z0-9]+)?/' // 百度(包含提取码)
];
// 获取DOM并设置XPath查询
$dom = getDom($url);
if (!$dom) {
return $results;
}
$finder = new \DomXPath($dom);
$xpath = $this->buildXPathQuery($tag, $classString);
$nodes = $finder->query($xpath);
foreach ($nodes as $node) {
if (count($results) >= $maxCount) {
break;
}
$html = $dom->saveHTML($node);
$item = [
'title' => '',
'url' => '',
];
// 提取资源标题
$item['title'] = $this->extractTitle($html, $tagTitle, $classStringTitle);
// 尝试直接从当前HTML中提取网盘链接
if (preg_match($panPatterns[$type], $html, $match)) {
$item['url'] = trim($match[0]);
} else {
// 根据配置决定是否需要进入详情页
if ($line['html_type'] == 1) {
$item['url'] = $this->extractUrlFromDetailPage($html, $line, $url, $tagUrl, $classStringUrl, $panPatterns[$type]);
} else {
$item['url'] = $this->extractUrlFromListPage($html, $tagUrl, $classStringUrl, $panPatterns[$type]);
}
}
// 只添加同时有标题和URL的结果
if ($item['title'] && $item['url']) {
$results[] = $item;
}
}
return $results;
}
/**
* 构建XPath查询语句
*
* @param string $tag 标签名
* @param string $classString 类名字符串
* @return string XPath查询语句
*/
private function buildXPathQuery($tag, $classString)
{
$classArray = explode(' ', trim($classString));
$xpathConditions = [];
foreach ($classArray as $cls) {
if (!empty($cls)) {
$xpathConditions[] = "contains(concat(' ', normalize-space(@class), ' '), ' {$cls} ')";
}
}
return "//{$tag}" . (empty($xpathConditions) ? "" : "[" . implode(' and ', $xpathConditions) . "]");
}
/**
* 从HTML中提取标题
*
* @param string $html HTML内容
* @param string $tagTitle 标题标签
* @param string $classStringTitle 标题类名
* @return string 提取的标题
*/
private function extractTitle($html, $tagTitle, $classStringTitle)
{
// 尝试匹配"名称xxx 描述:"格式
if (preg_match('/名称:(.*?)\n\n描述/s', $html, $match)) {
return trim(strip_tags($match[1]));
}
// 尝试根据标签和类名匹配
$escapedClass = preg_quote($classStringTitle, '#');
$escapedTag = preg_quote($tagTitle, '#');
$pattern = '#<'.$escapedTag.'[^>]*class=["\'][^"\']*' . $escapedClass . '[^"\']*["\'][^>]*>(.*?)</'.$escapedTag.'>#s';
if (preg_match($pattern, $html, $titleMatch)) {
return trim(strip_tags($titleMatch[1]));
}
return '';
}
/**
* 从详情页提取URL
*
* @param string $html 列表页HTML
* @param array $line 配置信息
* @param string $baseUrl 基础URL
* @param string $tagUrl URL标签
* @param string $classStringUrl URL类名
* @param string $panPattern 网盘链接匹配模式
* @return string 提取的URL
*/
private function extractUrlFromDetailPage($html, $line, $baseUrl, $tagUrl, $classStringUrl, $panPattern)
{
list($tagD, $classStringD) = explode('+', $line['html_url'], 2);
// 构建匹配详情页链接的正则表达式
$detailUrlPattern = $this->buildHrefPattern($tagD, $classStringD);
if (!preg_match($detailUrlPattern, $html, $match)) {
return '';
}
// 处理相对URL
$detailUrl = trim($match[1]);
$fullDetailUrl = $this->buildFullUrl($detailUrl, $baseUrl);
// 获取详情页内容
$dom2 = getDom($fullDetailUrl);
if (!$dom2) {
return '';
}
$finder2 = new \DomXPath($dom2);
$xpath2 = $this->buildXPathQuery($tagUrl, $classStringUrl);
$nodes2 = $finder2->query($xpath2);
// 遍历详情页节点查找网盘链接
foreach ($nodes2 as $node2) {
$html2 = $dom2->saveHTML($node2);
// 尝试从内容中提取
$escapedClass = preg_quote($classStringUrl, '#');
$escapedTag = preg_quote($tagUrl, '#');
$contentPattern = '#<'.$escapedTag.'[^>]*class=["\'][^"\']*' . $escapedClass . '[^"\']*["\'][^>]*>(.*?)</'.$escapedTag.'>#s';
if (preg_match($contentPattern, $html2, $titleMatch)) {
$extractedUrl = trim(strip_tags($titleMatch[1]));
if (preg_match($panPattern, $extractedUrl, $urlMatch)) {
return trim($urlMatch[0]);
}
}
// 尝试从href属性中提取
$hrefPattern = $this->buildHrefPattern($tagUrl, $classStringUrl);
if (preg_match($hrefPattern, $html2, $match)) {
$extractedUrl = trim($match[1]);
if (preg_match($panPattern, $extractedUrl, $urlMatch)) {
return trim($urlMatch[0]);
}
}
}
return '';
}
/**
* 从列表页直接提取URL
*
* @param string $html HTML内容
* @param string $tagUrl URL标签
* @param string $classStringUrl URL类名
* @param string $panPattern 网盘链接匹配模式
* @return string 提取的URL
*/
private function extractUrlFromListPage($html, $tagUrl, $classStringUrl, $panPattern)
{
// 尝试从内容中提取
$escapedClass = preg_quote($classStringUrl, '#');
$escapedTag = preg_quote($tagUrl, '#');
$contentPattern = '#<'.$escapedTag.'[^>]*class=["\'][^"\']*' . $escapedClass . '[^"\']*["\'][^>]*>(.*?)</'.$escapedTag.'>#s';
if (preg_match($contentPattern, $html, $titleMatch)) {
$extractedUrl = trim(strip_tags($titleMatch[1]));
if (preg_match($panPattern, $extractedUrl, $urlMatch)) {
return trim($urlMatch[0]);
}
}
// 尝试从href属性中提取
$hrefPattern = $this->buildHrefPattern($tagUrl, $classStringUrl);
if (preg_match($hrefPattern, $html, $match)) {
$extractedUrl = trim($match[1]);
if (preg_match($panPattern, $extractedUrl, $urlMatch)) {
return trim($urlMatch[0]);
}
}
return '';
}
/**
* 构建href属性匹配模式
*
* @param string $tag 标签名
* @param string $classString 类名
* @return string 正则表达式
*/
private function buildHrefPattern($tag, $classString)
{
$escapedClass = preg_quote($classString, '#');
$escapedTag = preg_quote($tag, '#');
if (empty($escapedClass)) {
return '#<' . $escapedTag . '[^>]*href=["\']([^"\']+)["\']#i';
} else {
return '#<' . $escapedTag . '[^>]*class=["\'][^"\']*' . $escapedClass . '[^"\']*["\'][^>]*href=["\']([^"\']+)["\']#i';
}
}
/**
* 构建完整URL
*
* @param string $url 可能是相对URL
* @param string $baseUrl 基础URL
* @return string 完整URL
*/
private function buildFullUrl($url, $baseUrl)
{
if (strpos($url, 'http') !== 0) {
$parsed = parse_url($baseUrl);
$base = $parsed['scheme'] . '://' . $parsed['host'];
return $base . $url;
}
return $url;
}
/**
* 自定义接口
*/
private function handleKk($line, $title, $apiType = 0)
{
$type = $line['pantype'];
$maxCount = $line['count'];
$url2 = [];
$urlDefault = "https://m.kkkba.com";
// 网盘链接匹配正则
$pattern = [
0 => '/https:\/\/pan\.quark\.cn\/[^\s]+/', // 夸克
2 => '/https:\/\/pan\.baidu\.com\/[^\s]+/', // 百度
];
if (!isset($pattern[$type])) {
return [];
}
try {
$res = curlHelper($urlDefault."/v/api/getToken", "GET", null, [], "", "", 5)['body'] ?? null;
if (!$res) return $url2;
} catch (Exception $err) {
return $url2;
}
$res = json_decode($res, true);
$token = $res['token'] ?? '';
if(empty($token)){
return $url2;
}
// 所有接口列表
$allApiList = [
1 => "/v/api/getJuzi",
2 => "/v/api/search",
// 3 => "/v/api/getXiaoyu",
// 4 => "/v/api/getDJ",
// 5 => "/v/api/getKK"
];
// 根据 apiType 确定要调用的接口列表
if ($apiType == 0) {
// 全部接口
$apiList = array_values($allApiList);
} elseif (isset($allApiList[$apiType])) {
// 指定某个接口
$apiList = [$allApiList[$apiType]];
} else {
// 错误类型,直接返回空
return [];
}
// 请求头
$urlData = array(
'name' => $title,
'token' => $token
);
$headers = ['Content-Type: application/json'];
foreach ($apiList as $apiUrl) {
try {
$response = curlHelper($urlDefault.$apiUrl, "POST", json_encode($urlData), $headers, "", "", 5);
$res = isset($response['body']) ? json_decode($response['body'], true) : null;
} catch (Exception $err) {
continue;
}
if (empty($res['list']) || !is_array($res['list'])) {
continue;
}
foreach ($res['list'] as $value) {
if (preg_match($pattern[$type], $value['answer'], $matches)) {
$link = $matches[0];
if (preg_match('/提取码[:]?\s*([a-zA-Z0-9]{4})/', $value['answer'], $codeMatch)) {
$link .= '?pwd=' . $codeMatch[1];
}
$titleText = preg_replace('/\s*[\(]?(夸克|百度)?[\)]?\s*/u', '', $value['answer']??'');
$url2[] = [
'title' => $titleText,
'url' => $link
];
if (count($url2) >= $maxCount) {
return $url2;
}
}
}
}
$searchList = array_map(function($value) {
$value['is_type'] = determineIsType($value['url']);
if(Config('qfshop.is_quan_type') != 1){
$value['url'] = encryptObject($value['url']);
}
return $value;
}, $searchList);
return $url2;
}
/**
* 验证夸克地址是否有效
* @return array
*/
private function verificationUrl($url) {
$code = '';
if (preg_match('/\?pwd=([^,\s&]+)/', $url, $pwdMatch)) {
$code = trim($pwdMatch[1]);
}
$urlData = [
'url' => $url,
'code' => $code,
'isType' => 1
];
$transfer = new \netdisk\Transfer();
$res = $transfer->transfer($urlData);
if ($res['code'] !== 200) {
return 0;
}
return jok('网盘资源均来源于互联网,资源内容与本站无关', $searchList);
return $res['data'];
}
/**
@@ -186,86 +887,6 @@ class Other extends QfShop
}
}
/**
* 全网搜索 该接口仅用于微信自动回复
*
* @return void
*/
public function all_search($param='')
{
$title = $param ?: input('post.title', '');
if (empty($title)) {
return jerr("请输入要看的内容");
}
$map[] = ['status', '=', 1];
$map[] = ['is_delete', '=', 0];
$map[] = ['is_time', '=', 1];
$map[] = ['title|description', 'like', '%' . trim($title) . '%'];
$urls = $this->model->where($map)->field('source_id as id, title, url,is_time')->order('update_time', 'desc')->limit(5)->select()->toArray();
if (!empty($urls)) {
$ids = array_column($urls, 'id');
$this->model->whereIn('source_id', $ids)->update(['update_time' => time()]);
return !empty($param) ? $urls : jok('临时资源获取成功', $urls);
}
//同一个搜索内容锁机
if (Cache::has($title)) {
// 检查缓存中是否已有结果
return !empty($param) ? Cache::get($title) : jok('临时资源获取成功', Cache::get($title));
}
// 检查是否有正在处理的请求
if (Cache::has($title . '_processing')) {
// 如果当前正在处理相同关键词的请求,等待结果
$startTime = time(); // 记录开始时间
while (Cache::has($title . '_processing')) {
usleep(1000000); // 暂停1秒
// 检查是否超过60秒
if (time() - $startTime > 60) {
return !empty($param) ? [] : jok('临时资源获取成功', []);
}
}
return !empty($param) ? Cache::get($title) : jok('临时资源获取成功', Cache::get($title));
}
// 设置处理状态为正在处理
Cache::set($title . '_processing', true, 60); // 锁定60秒
$searchList = []; //查询的结果集
$datas = []; //最终数据
$num_total = 2; //最多想要几条结果
$num_success = 0;
// 定义源的顺序
$sources = ['source1', 'source2'];
foreach ($sources as $source) {
if ($num_success >= $num_total) {
break;
}
foreach ($source(true,$title) as $value) {
if ($num_success >= $num_total) {
break;
}
if (!$this->urlExists($searchList, $value['url'])) {
$searchList[] = $value;
$this->processUrl($value, $num_success, $datas);
}
}
}
Cache::set($title, $datas, 60); // 缓存结果60秒
Cache::delete($title . '_processing'); // 解锁
return !empty($param) ? $datas : jok('临时资源获取成功', $datas);
}
// 检查 URL 是否已存在(忽略查询参数)
public function urlExists($searchList, $urlToCheck) {
// 解析待检查的 URL

View File

@@ -723,9 +723,14 @@ function getDom($url)
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// 设置超时时间
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 连接超时5秒
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // 响应超时5秒
// 临时跳过 SSL 验证(测试用)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // 避免跳转被拦
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0'); // 模拟浏览器UA
$html = curl_exec($ch);
curl_close($ch);

9
app/model/ApiList.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
namespace app\model;
use app\model\QfShop;
class ApiList extends QfShop
{
}

View File

@@ -278,6 +278,7 @@ INSERT INTO `qf_node` VALUES (112, '账号管理', '', 'qfadmin', 'source', 'dep
INSERT INTO `qf_node` VALUES (113, '资源日志', '', 'qfadmin', 'source', 'log', 108, 8, 1, 'el-icon-discover', NULL, 0, 1712208103, 1726195583);
INSERT INTO `qf_node` VALUES (114, '用户需求', '', 'qfadmin', 'source', 'feedback', 108, 1, 1, 'el-icon-edit', NULL, 0, 1712230638, 1712230717);
INSERT INTO `qf_node` VALUES (118, '分类管理', '', 'qfadmin', 'source', 'category', 108, 9, 1, 'el-icon-s-operation', NULL, 0, 1716363477, 1726190129);
INSERT INTO `qf_node` VALUES (119, '接口配置', '', 'qfadmin', 'source', 'apilist', '108', '1', '1', 'el-icon-link', NULL, '0', '1747119102', '1747119102');
-- ----------------------------
-- Table structure for qf_source
@@ -365,6 +366,30 @@ CREATE TABLE `qf_token` (
PRIMARY KEY (`token_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '授权信息表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `qf_api_list`;
CREATE TABLE `qf_api_list` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(100) NOT NULL COMMENT '线路名称',
`type` varchar(20) NOT NULL DEFAULT 'api' COMMENT '接口类型api接口、html网页、tgTG频道',
`pantype` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0 夸克 2百度',
`url` varchar(255) DEFAULT NULL COMMENT '请求地址或入口URL',
`method` varchar(10) DEFAULT 'GET' COMMENT '请求方式GET/POST仅用于api/html类型',
`fixed_params` text COMMENT '固定请求参数JSON格式',
`headers` text COMMENT '请求头信息JSON格式',
`field_map` text COMMENT '返回字段映射JSON格式',
`count` int(11) DEFAULT '0' COMMENT '最多取多少个资源',
`html_item` varchar(255) DEFAULT NULL,
`html_title` varchar(255) DEFAULT NULL,
`html_url` varchar(255) DEFAULT NULL,
`html_type` tinyint(4) DEFAULT '0',
`html_url2` varchar(255) DEFAULT NULL,
`weight` int(11) DEFAULT '0' COMMENT '权重,数值越大优先级越高',
`status` tinyint(1) DEFAULT '1' COMMENT '是否启用1启用0禁用',
`create_time` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间',
`update_time` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='多线路接口配置表';
-- ----------------------------
-- Table structure for qf_user
-- ----------------------------

View File

@@ -239,7 +239,7 @@
}
}
let cancelSource = null;
let currentEventSource = null;
function setType(type) {
app.selectBtn()
if(type == app.is_type) return
@@ -252,47 +252,48 @@
app.currentSource = source;
if(app.currentSource == 1){
if(app.QLoading || app.QList.length>0) return
if (cancelSource) {
cancelSource.cancel('用户切换了数据源,取消上次请求');
setTimeout(() => {
app.QLoading = true
}, 100);
}
cancelSource = axios.CancelToken.source();
app.QLoading = true
// search1线路 有5个接口
const searchTypes = [1, 2, 3, 4, 5];
const promises = [
...searchTypes.map(num => searchRequest('search1', num)),
searchRequest('search2')
];
Promise.all(promises)
.then(() => {
app.QList = []
// 创建新的 EventSource 连接前,确保关闭旧的连接
if(currentEventSource) {
currentEventSource.close();
}
// 创建 EventSource 连接
const params = new URLSearchParams({
title: app.keyword,
is_type: app.is_type
})
currentEventSource = new EventSource(`/api/other/web_search?${params.toString()}`)
// 监听消息
currentEventSource.onmessage = function(event) {
if(event.data.includes('[DONE]')) {
currentEventSource.close()
currentEventSource = null
app.QLoading = false
})
.catch(error => {
app.QLoading = false;
});
return
}
try {
const data = JSON.parse(event.data)
app.QList.push(data)
} catch(e) {
console.error('解析数据失败:', e)
}
}
// 错误处理
currentEventSource.onerror = function(error) {
console.error('SSE 连接错误:', error)
currentEventSource.close()
currentEventSource = null
app.QLoading = false
}
}
}
function searchRequest(api, num = null) {
const params = {
title: app.keyword,
is_type: app.is_type,
}
if(num) params.num = num
return axios.post(`/api/other/${api}`, params, {
cancelToken: cancelSource.token // 直接用全局变量
})
.then(res => {
app.QList = app.QList.concat(res.data.data)
})
}
</script>
</body>
</html>

View File

@@ -0,0 +1,497 @@
<!DOCTYPE html>
<html>
<head>
<title>接口配置</title>
{include file="common/header"/}
<el-form :inline="true">
<el-form-item>
<el-button icon="el-icon-plus" size="small" @click="clickAdd" plain>添加</el-button>
</el-form-item>
</el-form>
<el-table :data="dataList" v-loading="loading">
<el-table-column prop="id" label="ID" width="60">
</el-table-column>
<el-table-column label="网盘类型" width="100" prop="name">
<template slot-scope="scope">
<span v-if="scope.row.pantype==2">百度网盘</span>
<span v-else>夸克网盘</span>
</template>
</el-table-column>
<el-table-column label="线路名称" prop="name"></el-table-column>
<el-table-column label="地址" prop="url"></el-table-column>
<el-table-column label="类型" width="100" prop="name">
<template slot-scope="scope">
<span v-if="scope.row.type=='html'">网页</span>
<span v-else-if="scope.row.type=='tg'">TG频道</span>
<span v-else>接口</span>
</template>
</el-table-column>
<el-table-column prop="count" label="获取资源数" width="150" align="center">
</el-table-column>
<el-table-column label="是否开启" width="100" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.status==1?true:false"
@change="clickStatus(scope.row)">
</el-switch>
</template>
</el-table-column>
<el-table-column prop="weight" label="排序" width="100" align="center">
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-link type="primary" @click="clickEdit(scope.row)" :underline="false">编辑</el-link>&nbsp;&nbsp;&nbsp;&nbsp;
<el-link type="danger" @click="clickDelete(scope.row)" :underline="false">删除</el-link>
</template>
</el-table-column>
</el-table>
<!-- 添加框 -->
<el-dialog title="添加线路" :visible.sync="dialogFormAdd" width="750px" :modal-append-to-body='false' append-to-body :close-on-click-modal='false'>
<el-form :model="formAdd" :rules="rules" ref="formAdd">
<el-form-item prop="pantype" label="网盘类型" :label-width="formLabelWidth">
<el-radio-group v-model="formAdd.pantype">
<el-radio :label="0">夸克网盘</el-radio>
<el-radio :label="2">百度网盘</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="name" label="线路名称" :label-width="formLabelWidth">
<el-input size="medium" autocomplete="off" v-model="formAdd.name"></el-input>
</el-form-item>
<el-form-item prop="count" label="总数限制" :label-width="formLabelWidth">
<el-input-number v-model="formAdd.count" :min="0" :max="999" size="medium" style="width: 120px;"
controls-position="right"></el-input-number>
<span style="color: #999;">建议最大设置为5即该接口最多只获取5条数据</span>
</el-form-item>
<el-form-item prop="type" label="类型" :label-width="formLabelWidth">
<el-radio-group v-model="formAdd.type">
<el-radio label="api">API接口</el-radio>
<el-radio label="tg">TG频道</el-radio>
<el-radio label="html">网页爬虫</el-radio>
</el-radio-group>
<p style="color: red;" v-if="formAdd.type=='html'">不推荐网页爬虫配置复杂请自行看描述功能由AI编写如无法获取也没法😄</p>
<p style="color: red;" v-else-if="formAdd.type=='tg'">国内服务器无法使用比如https://t.me/s/NewQuark 就填写NewQuark即可</p>
</el-form-item>
<el-form-item prop="url" label="TG频道" :label-width="formLabelWidth" v-if="formAdd.type === 'tg'">
<el-input size="medium" autocomplete="off" v-model="formAdd.url"></el-input>
</el-form-item>
<el-form-item prop="url" label="目标网址" :label-width="formLabelWidth" v-else-if="formAdd.type === 'html'">
<el-input size="medium" autocomplete="off" v-model="formAdd.url"></el-input>
<span style="color: #999;">比如https://www.baidu.com/s?wd={keyword}{keyword}是固定词请勿修改</span>
</el-form-item>
<el-form-item prop="url" label="接口地址" :label-width="formLabelWidth" v-else>
<el-input size="medium" autocomplete="off" v-model="formAdd.url"></el-input>
</el-form-item>
<block v-if="formAdd.type === 'api'">
<el-form-item prop="method" label="请求方式" :label-width="formLabelWidth">
<el-radio-group v-model="formAdd.method">
<el-radio label="GET">GET</el-radio>
<el-radio label="POST">POST</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="headers" label="请求头" :label-width="formLabelWidth">
<el-input type="textarea" v-model="formAdd.headers" :rows="3" placeholder='{
"User-Agent": "..."
}' />
</el-form-item>
<el-form-item prop="fixed_params" label="接口参数" :label-width="formLabelWidth">
<el-input type="textarea" v-model="formAdd.fixed_params" :rows="3" placeholder='{"search": "{keyword}"}'></el-input>
<span style="color: #999;">将search改为实际的搜索字段即可{keyword}请勿修改</span>
</el-form-item>
<el-form-item prop="field_map" label="字段映射" :label-width="formLabelWidth">
<el-input type="textarea" v-model="formAdd.field_map" :rows="7" placeholder='{"title": "xxx", "url": "xxx"}'></el-input>
<span style="color: #999;">结构参数名请勿修改仅将中文修改为真实接口的字段名即可数组字段一般是data或者data.list</span>
</el-form-item>
</block>
<block v-if="formAdd.type === 'html'">
<el-form-item prop="html_item" label="内容标签" :label-width="formLabelWidth">
<el-input size="medium" autocomplete="off" v-model="formAdd.html_item"></el-input>
<span style="color: #999;line-height: 1.4;display: block;">
请填写用于循环内容的 HTML 标签和 class用于提取数据列表。格式标签名+class名
<br>
示例:<br>
div+item (选择所有 class="item" 的 div 标签)<br>
article+post (选择所有 class="post" 的 article 标签)<br>
li+result-item hot选择所有 class 同时包含 "result-item" 和 "hot" 的 li 标签)
</span>
</el-form-item>
<el-form-item prop="html_title" label="标题标签" :label-width="formLabelWidth">
<el-input size="medium" autocomplete="off" v-model="formAdd.html_title"></el-input>
<span style="color: #999; line-height: 1.4; display: block;">
标题标签是循环项中用于提取标题文字的元素;格式:标签名+class名。<br>
例如:<br>
h3+title 表示选择 `&lt;h3 class="title"&gt;` 标签作为标题内容<br>
a+link 表示 `&lt;a class="link"&gt;` 标签中的文本作为标题<br>
div+content-title 表示 `&lt;div class="content-title"&gt;` 标签作为标题
</span>
</el-form-item>
<el-form-item prop="html_type" label="详情页" :label-width="formLabelWidth">
<el-radio-group v-model="formAdd.html_type">
<el-radio :label="0">不需要</el-radio>
<el-radio :label="1">需要</el-radio>
</el-radio-group>
<p style="color: #999;">如果网盘链接不在列表页中,而是需要进入详情页后才能获取,请选择“需要”</p>
</el-form-item>
<el-form-item prop="html_url" label="详情页标签" :label-width="formLabelWidth" v-if="formAdd.html_type==1">
<el-input size="medium" autocomplete="off" v-model="formAdd.html_url"></el-input>
<span style="color: #999; line-height: 1.4; display: block;">
详情页标签是循环项中用于提取详情页网址的元素格式a+class名。只能a标签<br>
例如:<br>
a+post_url 表示获取元素 `&lt;a class="post_url" href="https://.."&gt;` 上的href的值
</span>
</el-form-item>
<el-form-item prop="html_url2" label="网盘链接" :label-width="formLabelWidth">
<el-input size="medium" autocomplete="off" v-model="formAdd.html_url2"></el-input>
<span style="color: #999; line-height: 1.4; display: block;">
网盘链接标签是循环项中用于提取网盘链接的元素;格式:标签名+class名。<br>
将提取该元素第一个出现的网盘链接
</span>
</el-form-item>
</block>
<el-form-item prop="weight" label="排序" :label-width="formLabelWidth">
<el-input-number v-model="formAdd.weight" :min="0" :max="999" size="medium" style="width: 120px;"
controls-position="right" />
</el-form-item>
<el-form-item prop="status" label="是否启用" :label-width="formLabelWidth">
<el-switch v-model="formAdd.status" size="medium" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="postAdd()">确认添加</el-button>
</div>
</el-dialog>
<!-- 修改框 -->
<el-dialog title="修改线路" :visible.sync="dialogFormEdit" width="750px" :modal-append-to-body='false' append-to-body :close-on-click-modal='false'>
<el-form :model="formEdit" :rules="rules" ref="formEdit">
<el-form-item prop="pantype" label="网盘类型" :label-width="formLabelWidth">
<el-radio-group v-model="formEdit.pantype">
<el-radio :label="0">夸克网盘</el-radio>
<el-radio :label="2">百度网盘</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="name" label="线路名称" :label-width="formLabelWidth">
<el-input size="medium" autocomplete="off" v-model="formEdit.name"></el-input>
</el-form-item>
<el-form-item prop="count" label="总数限制" :label-width="formLabelWidth">
<el-input-number v-model="formEdit.count" :min="0" :max="999" size="medium" style="width: 120px;"
controls-position="right"></el-input-number>
<span style="color: #999;">建议最大设置为5即该接口最多只获取5条数据</span>
</el-form-item>
<el-form-item prop="type" label="类型" :label-width="formLabelWidth">
<el-radio-group v-model="formEdit.type">
<el-radio label="api">API接口</el-radio>
<el-radio label="tg">TG频道</el-radio>
<el-radio label="html">网页爬虫</el-radio>
</el-radio-group>
<p style="color: red;" v-if="formEdit.type=='html'">不推荐网页爬虫配置复杂请自行看描述功能由AI编写如无法获取也没法😄</p>
</el-form-item>
<el-form-item prop="url" label="TG频道" :label-width="formLabelWidth" v-if="formEdit.type === 'tg'">
<el-input size="medium" autocomplete="off" v-model="formEdit.url"></el-input>
</el-form-item>
<el-form-item prop="url" label="目标网址" :label-width="formLabelWidth" v-else-if="formEdit.type === 'html'">
<el-input size="medium" autocomplete="off" v-model="formEdit.url"></el-input>
<span style="color: #999;">比如https://www.baidu.com/s?wd={keyword}{keyword}是固定词请勿修改</span>
</el-form-item>
<el-form-item prop="url" label="接口地址" :label-width="formLabelWidth" v-else>
<el-input size="medium" autocomplete="off" v-model="formEdit.url"></el-input>
</el-form-item>
<block v-if="formEdit.type === 'api'">
<el-form-item prop="method" label="请求方式" :label-width="formLabelWidth">
<el-radio-group v-model="formEdit.method">
<el-radio label="GET">GET</el-radio>
<el-radio label="POST">POST</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="headers" label="请求头" :label-width="formLabelWidth">
<el-input type="textarea" v-model="formEdit.headers" :rows="3" placeholder='{
"User-Agent": "..."
}' />
</el-form-item>
<el-form-item prop="fixed_params" label="接口参数" :label-width="formLabelWidth">
<el-input type="textarea" v-model="formEdit.fixed_params" :rows="3" placeholder='{"search": "{keyword}"}'></el-input>
<span style="color: #999;">将search改为实际的搜索字段即可{keyword}请勿修改</span>
</el-form-item>
<el-form-item prop="field_map" label="字段映射" :label-width="formLabelWidth">
<el-input type="textarea" v-model="formEdit.field_map" :rows="7" placeholder='{"title": "xxx", "url": "xxx"}'></el-input>
<span style="color: #999;">结构参数名请勿修改仅将中文修改为真实接口的字段名即可数组字段一般是data或者data.list</span>
</el-form-item>
</block>
<block v-if="formEdit.type === 'html'">
<el-form-item prop="html_item" label="内容标签" :label-width="formLabelWidth">
<el-input size="medium" autocomplete="off" v-model="formEdit.html_item"></el-input>
<span style="color: #999;line-height: 1.4;display: block;">
请填写用于循环内容的 HTML 标签和 class用于提取数据列表。格式标签名+class名
<br>
示例:<br>
div+item (选择所有 class="item" 的 div 标签)<br>
article+post (选择所有 class="post" 的 article 标签)<br>
li+result-item hot选择所有 class 同时包含 "result-item" 和 "hot" 的 li 标签)
</span>
</el-form-item>
<el-form-item prop="html_title" label="标题标签" :label-width="formLabelWidth">
<el-input size="medium" autocomplete="off" v-model="formEdit.html_title"></el-input>
<span style="color: #999; line-height: 1.4; display: block;">
标题标签是循环项中用于提取标题文字的元素;格式:标签名+class名。<br>
例如:<br>
h3+title 表示选择 `&lt;h3 class="title"&gt;` 标签作为标题内容<br>
a+link 表示 `&lt;a class="link"&gt;` 标签中的文本作为标题<br>
div+content-title 表示 `&lt;div class="content-title"&gt;` 标签作为标题
</span>
</el-form-item>
<el-form-item prop="html_type" label="详情页" :label-width="formLabelWidth">
<el-radio-group v-model="formEdit.html_type">
<el-radio :label="0">不需要</el-radio>
<el-radio :label="1">需要</el-radio>
</el-radio-group>
<p style="color: #999;">如果网盘链接不在列表页中,而是需要进入详情页后才能获取,请选择“需要”</p>
</el-form-item>
<el-form-item prop="html_url" label="详情页标签" :label-width="formLabelWidth" v-if="formEdit.html_type==1">
<el-input size="medium" autocomplete="off" v-model="formEdit.html_url"></el-input>
<span style="color: #999; line-height: 1.4; display: block;">
详情页标签是循环项中用于提取详情页网址的元素格式a+class名。只能a标签<br>
例如:<br>
a+post_url 表示获取元素 `&lt;a class="post_url" href="https://.."&gt;` 上的href的值
</span>
</el-form-item>
<el-form-item prop="html_url2" label="网盘链接" :label-width="formLabelWidth">
<el-input size="medium" autocomplete="off" v-model="formEdit.html_url2"></el-input>
<span style="color: #999; line-height: 1.4; display: block;">
网盘链接标签是循环项中用于提取网盘链接的元素;格式:标签名+class名。<br>
将提取该元素第一个出现的网盘链接
</span>
</el-form-item>
</block>
<el-form-item prop="weight" label="排序" :label-width="formLabelWidth">
<el-input-number v-model="formEdit.weight" :min="0" :max="999" size="medium" style="width: 120px;"
controls-position="right" />
</el-form-item>
<el-form-item prop="status" label="是否启用" :label-width="formLabelWidth">
<el-switch v-model="formEdit.status" size="medium" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="postEdit()">确认修改</el-button>
</div>
</el-dialog>
{include file="common/footer"/}
<script>
var app = new Vue({
el: '#app',
data() {
this.getList();
return {
formLabelWidth: '90px',
dialogFormAdd: false,
dialogFormEdit: false,
loading: true,
dataList: [],
selectList: [],
formAdd: {
},
formEdit: {
},
rules: {
name: [ { required: true, message: '请输入分类名称', trigger: 'blur' }],
},
}
},
methods: {
postEdit() {
var that = this;
that.$refs["formEdit"].validate((valid) => {
if (valid) {
axios.post('/admin/api_list/update', Object.assign({}, PostBase, that.formEdit))
.then(function (response) {
that.getList();
if (response.data.code == CODE_SUCCESS) {
that.$message({
message: response.data.message,
type: 'success'
});
that.dialogFormEdit = false;
} else {
that.$message.error(response.data.message);
}
})
.catch(function (error) {
that.$message.error('服务器内部错误');
});
}
});
},
postAdd() {
var that = this;
that.$refs['formAdd'].validate((valid) => {
if (valid) {
axios.post('/admin/api_list/add', Object.assign({}, PostBase, that.formAdd))
.then(function (response) {
that.getList();
if (response.data.code == CODE_SUCCESS) {
that.$message({
message: response.data.message,
type: 'success'
});
that.dialogFormAdd = false;
} else {
that.$message.error(response.data.message);
}
})
.catch(function (error) {
that.$message.error('服务器内部错误');
});
}
});
},
clickAdd() {
var that = this;
that.formAdd = {
pantype: 0,
type: 'api',
method: 'GET',
weight: 0,
count: 5,
status: true,
html_type: 0,
fixed_params: `{
"search": "{keyword}"
}`,
field_map: `{
"list_path": "数组字段",
"fields": {
"title": "资源名称",
"url": "资源地址"
}
}`
};
that.dialogFormAdd = true;
},
clickDelete(row) {
var that = this;
this.$confirm('即将删除这个分类, 是否确认?', '删除提醒', {
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.post('/admin/api_list/delete', Object.assign({}, PostBase, {
id: row.id
}))
.then(function (response) {
that.getList();
if (response.data.code == CODE_SUCCESS) {
that.$message({
message: response.data.message,
type: 'success'
});
} else {
that.$message.error(response.data.message);
}
})
.catch(function (error) {
that.$message.error('服务器内部错误');
});
}).catch(() => {
});
},
clickEdit(row) {
var that = this;
that.formEdit = row;
axios.post('/admin/api_list/detail', Object.assign({}, PostBase, {
id: row.id
}))
.then(function (response) {
if (response.data.code == CODE_SUCCESS) {
that.formEdit = response.data.data;
that.formEdit.status = that.formEdit.status==1?true:false;
that.dialogFormEdit = true;
} else {
that.$message.error(response.data.message);
}
})
.catch(function (error) {
that.$message.error('服务器内部错误');
});
},
getList() {
var that = this;
that.loading = true;
axios.post('/admin/api_list/getList', Object.assign({}, PostBase))
.then(function (response) {
that.loading = false;
if (response.data.code == CODE_SUCCESS) {
that.dataList = response.data.data;
} else {
that.$message.error(response.data.message);
}
})
.catch(function (error) {
that.loading = false;
that.$message.error('服务器内部错误');
});
},
clickStatus(row) {
var that = this;
let url = row.status ? '/admin/api_list/disable' : '/admin/api_list/enable'
axios.post(url, Object.assign({}, PostBase, {
id: row.id
}))
.then(function (response) {
that.getList();
if (response.data.code == CODE_SUCCESS) {
that.$message({
message: response.data.message,
type: 'success'
});
} else {
that.$message.error(response.data.message);
}
})
.catch(function (error) {
that.$message.error('服务器内部错误');
});
},
}
})
</script>
</html>

33
更新说明 Normal file
View File

@@ -0,0 +1,33 @@
3.1升级3.2 重装 或 更新文件和表 涉及更新文件如下
app/common.php
app/api/controller/Other.php
app/model/ApiList.php
app/admin/controller/ApiList.php
public/views/qfadmin/source/apilist.html
public/views/index/news/list.html
CREATE TABLE qf_api_list (
id int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
name varchar(100) NOT NULL COMMENT '线路名称',
type varchar(20) NOT NULL DEFAULT 'api' COMMENT '接口类型api接口、html网页、tgTG频道',
pantype tinyint(1) NOT NULL DEFAULT '0' COMMENT '0 夸克 2百度',
url varchar(255) DEFAULT NULL COMMENT '请求地址或入口URL',
method varchar(10) DEFAULT 'GET' COMMENT '请求方式GET/POST仅用于api/html类型',
fixed_params text COMMENT '固定请求参数JSON格式',
headers text COMMENT '请求头信息JSON格式',
field_map text COMMENT '返回字段映射JSON格式',
count int(11) DEFAULT '0' COMMENT '最多取多少个资源',
html_item varchar(255) DEFAULT NULL,
html_title varchar(255) DEFAULT NULL,
html_url varchar(255) DEFAULT NULL,
html_type tinyint(4) DEFAULT '0',
html_url2 varchar(255) DEFAULT NULL,
weight int(11) DEFAULT '0' COMMENT '权重,数值越大优先级越高',
status tinyint(1) DEFAULT '1' COMMENT '是否启用1启用0禁用',
create_time int(11) NOT NULL DEFAULT '0' COMMENT '创建时间',
update_time int(11) NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='多线路接口配置表';
INSERT INTO `qf_node` VALUES (119, '接口配置', '', 'qfadmin', 'source', 'apilist', '108', '1', '1', 'el-icon-link', NULL, '0', '1747119102', '1747119102');