Files
xianyan/docs/toolsapi/application/api/controller/Webapi.php
Developer 283950ea07 chore: 批量代码优化与功能迭代更新
本次提交包含大量代码优化、功能新增与服务端配置更新:
1. 修复分析报告统计数据,调整CMake策略设置
2. 优化APP权限配置、编辑器与聊天界面组件
3. 更新依赖库版本与pubspec配置
4. 新增文件传输服务端、信令服务器相关配置与脚本
5. 完善用户注销功能与数据库迁移脚本
6. 优化多处动画效果、代码风格与日志输出
7. 新增多种调试与部署脚本,修复已知BUG
2026-05-12 06:28:04 +08:00

1904 lines
70 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\api\controller;
use app\common\controller\Api;
use QL\QueryList;
use think\Db;
use think\Request;
use think\Image;
use think\Cache;
use DateTime;
class Webapi extends Api
{
protected $noNeedLogin = ['*'];
protected $noNeedRight = ['*'];
// 获取网站 TDK 信息
public function getTDK($url) {
// 记录接口开始时间
$startTime = microtime(true);
// 使用 QueryList 获取网页内容
$html = QueryList::get($url)->getHtml();
// 创建 QueryList 对象
$ql = QueryList::html($html)?QueryList::html($html):$this->error('未获取到网站信息');
$tdk = [];
$tdk['url'] = rtrim($url, '/');
// 获取页面中的ICO文件链接
$tdk['ico'] = $ql->find('link[rel*=icon]')->attrs('href');
// 网站名称
$tdk ['name'] = $ql->find('title')->text()?trim(tags($ql->find('title')->text())[0]):'未获取到标题';
// 获取页面标题
$tdk['title'] = $ql->find('title')->text();
// 获取页面关键词
$tdk['keywords'] = $ql->find('meta[name=keywords],meta[name=Keywords],meta[name=KeyWords],meta[http-equiv=keywords]')->attr('content');
// 获取页面描述
$tdk['description'] = $ql->find('meta[name=description],meta[name=Description],meta[http-equiv=description]')->attr('content');
if(empty($tdk['ico'])){
$tdk['ico'] = '未获取到ico';
}else{
if(@getimagesize($tdk['ico']) !== true){
$tdk['ico'] = str_replace(['[', ']', '\\', '"'], '', $tdk['url'].$tdk['ico']);
}
}
empty($tdk['title'])&&($tdk['title'] = '未获取到标题');
empty($tdk['keywords'])&&($tdk['keywords'] = $tdk['name']);
empty($tdk['description'])&&($tdk['description'] = '未获取到网站简介');
$tdk['keywords'] = implode(',', tags($tdk['keywords']));
// 记录接口结束时间
$endTime = microtime(true);
// 计算接口执行时间(单位:秒)
$tdk['execute'] = round($endTime - $startTime,2);
$this->success('成功',$tdk);
}
// 百度普通推送
public function BaiDuPuTongTuiSong()
{
$db = input('get.');
$db['urls'] = tags($db['urls']);
$api = 'http://data.zz.baidu.com/urls?site='.$db['url'].'&token='.$db['token'];
$ch = curl_init();
$options = [
CURLOPT_URL => $api,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => implode("\n", $db['urls']),
CURLOPT_HTTPHEADER => array('Content-Type: text/plain'),
];
curl_setopt_array($ch, $options);
$result = curl_exec($ch);
$result = json_decode($result,true);
if(isset($result['error'])){
$this->error('失败','失败:'.$result['message']);
}
$text = '剩余配额:'.$result['remain'].''.'成功推送:'.$result['success'];
$this->success('成功',$text);
}
// 必应普通推送
public function BingTuiSong()
{
$db = input('get.');
$db['urls'] = tags($db['urls']);
// 网站域名
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://ssl.bing.com//webmaster/api.svc/json/SubmitUrlbatch?apikey=".$db['token']);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'siteUrl' => $db['url'],
'urlList' => $db['urls']
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Host: ssl.bing.com',
'Content-Type: application/json; charset=utf-8'
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code == 200 && json_decode($response)->d == null) {
$txt = "成功推送:".count($db['urls']).'条链接';
$this->error('推送成功',$txt);
} else {
$txt = "推送失败:推送配额不足!";
$this->error('推送失败',$txt);
}
}
// 神马推送
public function ShenMaTuiSong()
{
$db = input('get.');
$db['urls'] = tags($db['urls']);
$api = "https://data.zhanzhang.sm.cn/push?site=".$db['url']."&user_name=".$db['user']."&resource_name=mip_add&token=".$db['token'];
$ch = curl_init();
$options = [
CURLOPT_URL => $api,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => implode("\n", $db['urls']),
CURLOPT_HTTPHEADER => ['Content-Type: text/plain'],
];
curl_setopt_array($ch, $options);
$result = json_decode(curl_exec($ch));
if($result->returnCode == 200){
$this->success('成功',"成功推送:".count($db['urls']).'条');
}elseif ($result->returnCode == 201) {
$this->error('推送失败',"token不合法");
}elseif($result->returnCode == 202){
$this->error('推送失败',"配额已用尽!");
}elseif($result->returnCode == 400){
$this->error('推送失败',"请求参数有误");
}elseif($result->returnCode == 500){
$this->error('推送失败',"服务器内部错误");
}
$this->success('成功',$result->returnCode);
}
// 外链数据
public function linkData()
{
$urls = @file('links.txt');
if (!$urls) {
$this->error('链接文件不存在');
}
$count = count($urls);
$page = 1;
$num = 15;
$reqPage = $this->request->param('page', 1);
$reqNum = $this->request->param('num', 15);
if (is_numeric($reqPage) && $reqPage > 0) {
$page = intval($reqPage);
}
if (is_numeric($reqNum) && $reqNum <= 30 && $reqNum > 0) {
$num = intval($reqNum);
}
$maxpage = ceil($count / $num);
if ($page > $maxpage) {
$this->success('已完成', ['list' => [], 'count' => $count, 'page' => $page]);
} else {
$data = [];
foreach (array_slice($urls, ($page - 1) * $num, $num) as $value) {
$data[] = trim($value);
}
$this->success('成功', ['list' => $data, 'count' => $count, 'page' => $page, 'num' => $num]);
}
}
// 网站提交
public function submitYourSite()
{
$db = input('post.');
$data = [
'image' => $db['ico'],
'url' => $db['url'],
'name' => $db['name'],
'title' => $db['title'],
'keywords' => $db['keywords'],
'description' => $db['description'],
'category_id' => $db['category'],
'updatetime' => time(),
'createtime' => time()
];
foreach ($data as $value) {
if (empty($value)) {
$this->error($value.'不能为空!');
break;
}
}
$data['switch'] = 0;
$check = Db::name('site')->where('url','like','%'.urls($data['url']).'%')->value('id');
if(!$check){
$res = Db::name('site')->insert($data);
if($res){
$this->success('提交成功');
}else{
$this->error('提交失败');
}
}else{
$this->error('已提交过该网站!~');
}
}
// 网站点击量
public function siteHits()
{
$id = input('post.id');
if(empty($id) || !isset($id)){
$this->error('参数错误');
}
$data = Db::name('site')->where('id',$id)->setInc('hit');
if($data){
$this->success('成功');
}
$this->error('失败');
}
// 检测端口方法
function checkDomainPort($url,$port)
{
$portNames = [
80 => 'HTTP',
443 => 'HTTPS',
53 => 'DNS',
4000 => 'QQ Client',
994 => 'SMTP:SSL',
3389 => 'TERMINAL',
123 => 'NTP',
22 => 'SSH',
3306 => 'MYSQL',
8080 => 'WWW代理',
43 => 'Whois',
1433 => 'MSSQL/udp',
993 => 'IMAP:SSL',
995 => 'POP3:SSL',
21 => 'FTP',
23 => 'TELNET',
110 => 'POP3',
25 => 'SMTP',
143 => 'IMAP'
];
$portName = isset($portNames[$port]) ? $portNames[$port] : '未知';
$timeout = 3; // 设置超时时间
$start_time = microtime(true); // 记录开始时间
$fp = @fsockopen($url, $port, $errno, $errstr, $timeout);
$data = [
'port' => $port,
'portName' => $portName,
];
if ($fp) {
// 连接成功
fclose($fp);
$end_time = microtime(true); // 记录结束时间
$query_time = round(($end_time - $start_time) * 1000); // 计算查询时间(毫秒)
$data['result'] = '开启';
$data['responseTime'] = $query_time;
} else {
$end_time = microtime(true); // 记录结束时间
$query_time = round(($end_time - $start_time) * 1000); // 计算查询时间(毫秒)
$data['result'] = '关闭';
$data['responseTime'] = $query_time;
}
$this->success('成功',$data);
}
protected function is_china_ip($ip) {
$url = "https://api.ip.sb/geoip/$ip";
$json = file_get_contents($url);
$data = json_decode($json, true);
if (isset($data['country_code']) && $data['country_code'] === 'CN') {
return true;
} else {
return false;
}
}
// 获取网页头部信息
public function GetHeaders()
{
$url = input('post.url');
if(!isset($url)){
$this->error('缺少参数');
}
$pattern = '/^(?:https?:\/\/)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/';
if(!preg_match($pattern, $url)){
$this->error('域名不合法!');
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
// 设置超时时间为 5 秒
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
// 检查是否发生超时
if (curl_errno($ch) == CURLE_OPERATION_TIMEDOUT) {
$this->error('超时响应,请检查该网站是否正常打开');
}
// 获取头部信息
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $headerSize);
// 打印头部信息
$this->success('获取成功',$header);
curl_close($ch);
}
// 随机生成User Agent信息
public function RandUA()
{
$db = input('post.');
$userAgents = [];
if(!isset($db['count'])){
$this->error('缺少参数');
}
if(isset($db['type'])){
for ($i = 0; $i < (int)$db['count']; $i++) {
$userAgents[] = \Campo\UserAgent::random(['os_type' => $db['type']]);
}
}else{
for ($i = 0; $i < (int)$db['count']; $i++) {
$userAgents[] = \Campo\UserAgent::random();
}
}
$this->success('生成成功',$userAgents);
}
// 图片压缩
public function ImgCompress()
{
// 获取表单上传文件
$file = request()->file('file');
$db = input('post.');
if(!isset($db['quality']) || $db['quality'] < 0){
$db['quality'] = 80;//质量
}
if(empty($db['width']) || $db['width'] < 0){
$db['width'] = 0;
}
if(empty($db['height']) || $db['height'] < 0){
$db['height'] = 0;
}
// 校验规则
$validateRule = ['image' => 'require|image|fileExt:jpg,png,gif'];
//校验器,判断图片格式是否正确
if (true !== $this->validate(['image' => $file], $validateRule)) {
$this->error('图片格式');
} else {
// 移动到框架应用根目录/public/compress/目录下
$info = $file->rule('uniqid')->move(ROOT_PATH . 'public' . DS . 'compress');
if ($info) {
$res = [];
// 存入相对路径 /compress/文件名
$data = DS . 'compress' . DS . $info->getSaveName();
$res['image'] = $data;
// 压缩前图片大小
$res['raw'] = round(filesize('.' . $data) / 1024, 2) . 'KB';
// 压缩图片判断
if(compressedImage('.'.$res['image'],(int)$db['width'],(int)$db['height'],(int)$db['quality'])){
// 执行刷新操作
clearstatcache(true, '.'.$res['image']);
// 压缩后图片大小
$res['queen'] = round(filesize('.' . $res['image']) / 1024, 2) . 'KB';
$res['width'] = $db['width'];
$res['height']= $db['height'];
$res['quality']= $db['quality'];
$this->success('压缩成功',$res);
}else{
$this->error('压缩失败');
}
} else {
// 上传失败获取错误信息
$this->error('错误信息',$file->getError());
}
}
}
// ocr图片识别
public function ocr()
{
if(Cache::has('cacheKey')){
$count = Cache::get('cacheKey');
}else{
$count = 0;
}
// 若超过最大调用次数,返回错误信息
if ($count >= 66) {
$this->error('今日接口调用次数已达上限,明天再来吧!');
}
// 获取上传图片文件
$file = request()->file('file');
// 移动到框架应用根目录/public/compress/目录下
$data ="";
$info = $file->rule('uniqid')->move(ROOT_PATH . 'public' . DS . 'ocr');
if($info){
// 存入相对路径 /compress/文件名
$data = '.'.DS . 'ocr' . DS . $info->getSaveName();
}else{
// 上传失败获取错误信息
$this->error('错误信息',$file->getError());
die;
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic?access_token={$this->getAccessToken()}",
CURLOPT_TIMEOUT => 30,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => 'image='.urlencode(base64_encode(file_get_contents($data))).'&detect_direction=false&paragraph=false&probability=false',
CURLOPT_HTTPHEADER => array(
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json'
),
]);
$response =curl_exec($curl);
curl_close($curl);
$res = json_decode($response,true);
// 增加接口调用次数计数
$count++;
Cache::set('cacheKey', $count, 86400); // 保存一天
if(isset($res['words_result'])){
$this->success('识别成功',$res['words_result']);
}else{
$this->error('识别错误',$response);
}
}
// ocr图片识别授权方法
private function getAccessToken(){
$API_KEY = "BctyMxvfj4HAFCr6iCHIxUa9";
$SECRET_KEY = "zvEGiF1isnBQ0UiiU32sAcv1EfwPlpPT";
$curl = curl_init();
$postData = array(
'grant_type' => 'client_credentials',
'client_id' => $API_KEY,
'client_secret' => $SECRET_KEY
);
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://aip.baidubce.com/oauth/2.0/token',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query($postData)
));
$response = curl_exec($curl);
curl_close($curl);
$rtn = json_decode($response);
return $rtn->access_token;
}
// 窗帘计算器
public function curtain()
{
$db = input('post.');
if(empty($db['ch']) || empty($db['cw']) || empty($db['cn']) || empty($db['clw']) || empty($db['clp'])){
$this->error('参数错误!');
}
// 输入参数
$windowHeight = $db['ch']; // 窗户高度(米)
$windowWidth = $db['cw']; // 窗户宽度(米)
$windowCount = $db['cn']; // 窗户数量
$fabricWidth = $db['clw']; // 布料宽度(米)
$fabricPrice = $db['clp']; // 布料单价(元/米)
// 计算布料数量
$totalFabricLength = ((floatval($windowWidth) + floatval(0.15) * 2) * 2) / $fabricWidth
* (floatval(0.15) + floatval($windowHeight) + floatval(0.5) + floatval(0.2)) * $windowCount;
$clothCount = ceil($totalFabricLength);
// 计算布料总价
$clothTotal = $clothCount * $fabricPrice;
// 格式化金额
function MilliFormat($money){
return number_format($money, 2);
}
$clothTotalFormatted = MilliFormat($clothTotal);
$data['clothCount'] = $clothCount;
$data['clothTotalFormatted'] = $clothTotalFormatted;
$this->success('计算成功',$data);
}
// 地砖计算器
public function tiles()
{
$db = input('post.');
if(empty($db['rl']) || empty($db['rw']) || empty($db['tl']) || empty($db['tw']) || empty($db['tp'])){
$this->error('参数错误!');
}
$roomLength = $db['rl'];
$roomWidth = $db['rw'];
$tileLength = $db['tl'];
$tileWidth = $db['tw'];
$tilePrice = $db['tp'];
$roomNum = 0;
$roomTotal = 0;
$rate = 1.05;
$jgspanArea = $roomLength * $roomWidth;
$jgspanArea = number_format($jgspanArea, 2);
$roomLength = $roomLength * 1000;
$roomWidth = $roomWidth * 1000;
$roomNum = ceil(($roomLength / $tileLength) * ($roomWidth / $tileWidth) * $rate);
$roomTotal = $roomNum * $tilePrice;
$roomTotal = number_format($roomTotal, 2);
$data['roomNum'] = $roomNum;
$data['roomTotal'] = $roomTotal;
$this->success('计算成功',$data);
}
// 壁纸计算器
public function wallpaper()
{
$db = input('post.');
if(empty($db['rh']) || empty($db['rl']) || empty($db['rn']) || empty($db['rv']) || empty($db['rp'])){
$this->error('参数错误!');
}
$room_height = $db['rh']; // 墙面高度
$room_long = $db['rl']; // 墙面长度
$room_num = $db['rn']; // 墙面数量
$room_vol = $db['rv']; // 壁纸规格(平方米/卷)
$room_price = $db['rp']; // 壁纸单价(元/卷)
// 计算壁纸数量和总价
$room_block = ceil($room_long * $room_height * $room_num * 1.1 / $room_vol);
$room_total = $room_block * $room_price;
$room_total = number_format($room_total, 2);
$data['room_block'] = $room_block;
$data['room_total'] = $room_total;
$this->success('计算成功',$data);
}
// 地板计算器
public function floors()
{
$db = input('post.');
if(empty($db['fl']) || empty($db['fw']) || empty($db['fll']) || empty($db['flw']) || empty($db['fp'])){
$this->error('参数错误!');
}
// 输入数据
$room_length = $db['fl']; // 房间长度
$room_width = $db['fw']; // 房间宽度
$floor_length = $db['fll']; // 地板长度
$floor_width = $db['flw']; // 地板宽度
$floor_type = empty($db['ft'])?1.08:1.05; // 地板类型0=实木地板或1=复合地板)
$floor_price = $db['fp']; // 地板单价(元/块)
// 计算房间面积
$jgspan_area = $room_length * $room_width;
// 将单位转换为毫米
$room_length *= 1000;
$room_width *= 1000;
// 计算地板数量和总价
$floornum = ceil(($room_length / $floor_length) * ($room_width / $floor_width) * $floor_type);
$floorprice = $floornum * $floor_price;
$data['floornum'] = number_format($floornum, 0); // 这里重新将结果格式化为字符串
$data['floorprice'] = number_format($floorprice, 2); // 这里重新将结果格式化为字符串
if(empty($data)){
$this->error('计算失败');
}
$this->success('计算成功', $data);
}
// 墙砖计算器
public function brick()
{
$db = input('post.');
if(empty($db['bh']) || empty($db['rl']) || empty($db['rw']) || empty($db['dh']) || empty($db['dw']) || empty($db['dn']) || empty($db['wh']) || empty($db['ww']) || empty($db['wn']) || empty($db['bl']) || empty($db['bw']) || empty($db['bp'])){
$this->error('参数错误!');
}
$wall_height = $db['bh']; // 墙面高度
$room_long = $db['rl']; // 房间长度
$room_width = $db['rw']; // 房间宽度
$door_height = $db['dh']; // 房门高度
$door_width = $db['dw']; // 房门宽度
$door_num = $db['dn']; // 门的数量
$window_height = $db['wh']; // 窗户高度
$window_width = $db['ww']; // 窗户宽度
$window_num = $db['wn']; // 窗户数量
$brick_long = $db['bl']; // 砖块长度/毫米
$brick_width = $db['bw']; // 砖块宽度/毫米
$brick_price = $db['bp']; // 砖块单价
$room_num = 0;
$room_total = 0;
$room_long = $room_long*1000;
$wall_height = $wall_height*1000;
$room_width = $room_width*1000;
$door_height = $door_height*1000;
$door_width = $door_width*1000;
$window_height = $window_height*1000;
$window_width = $window_width*1000;
$room_num = ($room_long/$brick_long)*($wall_height/$brick_width)*2;
$room_num = floatval($room_num)+floatval(($room_width/$brick_long)*($wall_height/$brick_width)*2);
$room_num = floatval($room_num)-floatval(($window_height/$brick_long)*($window_width/$brick_width)*$window_num);
$room_num = floatval($room_num)-floatval(($door_height/$brick_long)*($door_width/$brick_width)*$door_num);
$room_num = ceil($room_num*1.05);
$room_total = $room_num*floatval($brick_price);
$room_total = number_format($room_total, 2, '.', ',');
$data['room_num'] = $room_num?$room_num:null;
$data['room_total'] = $room_total?$room_total:null;
if(empty($data)){
$this->error('计算失败');
}
$this->success('计算成功',$data);
}
// 涂料计算器
public function paint()
{
$db = input('post.');
if(empty($db['ph']) || empty($db['rh']) || empty($db['rw']) || empty($db['dh']) || empty($db['dw']) || empty($db['dn']) || empty($db['wh']) || empty($db['ww']) || empty($db['wn']) || empty($db['cover']) || empty($db['pp'])){
$this->error('参数错误!');
}
$room_long = $db['ph']; // 墙面长度(米)
$wall_height = $db['rh']; // 房间长度(米)
$room_width = $db['rw']; // 房间宽度(米)
$door_height = $db['dh']; // 门高度(米)
$door_width = $db['dw']; // 门宽度(米)
$door_num = $db['dn']; // 门数量(个)
$window_height = $db['wh']; // 窗户高度(米)
$window_width = $db['ww']; // 窗户宽度(米)
$window_num = $db['wn']; // 窗户数量(个)
$paint_cover = $db['cover']; // 涂料覆盖率(平方米/升)
$room_price = $db['pp']; // 涂料的价格(元/升)
$room_num = 0;
$room_total = 0;
// 计算房间面积
$room_area = ($room_long + $room_width) * 2 * $wall_height + ($room_long * $room_width);
$room_area -= ($door_height * $door_width * $door_num);
$room_area -= ($window_height * $window_width * $window_num);
// 计算需要的涂料总量
$room_num = round($room_area / $paint_cover, 2);
// 计算涂料费用
$room_total = number_format($room_num * $room_price, 2);
$data['room_num'] = $room_num?$room_num:null;
$data['room_total'] = $room_total?$room_total:null;
if(empty($data)){
$this->error('计算失败');
}
$this->success('计算成功',$data);
}
// 提交补充纠错接口(旧版,保留兼容)
public function perfectData()
{
$content = input('post.content');
$mail = input('post.mail');
if(empty($content)){
$this->error('参数错误!'); // 参数为空的情况
}
if(mb_strlen($content) <= 10){ // 使用mb_strlen进行多字节字符长度判断
$this->error('内容过少,请重新编辑!'); // 内容过少的情况
}
$data = [
'content' => nl2br($content),
'mail' => $mail?$mail:null,
'switch' => 0,
'ip' => Request::instance()->ip(),
'updatetime' => time(),
'createtime' => time()
];
$result = Db::name('perfect')->insert($data); // 插入数据
if($result){
$this->success('提交成功,等待管理员修正问题!');
} else {
$this->error('补充纠错失败,稍后再试!'); // 插入失败的情况
}
}
/**
* @name 纠错提交API新版支持来源信息
* @desc 独立纠错接口保留来源页面字段用于App和独立页面调用
* @param string content 纠错内容必填10-2000字
* @param string mail 联系邮箱(可选)
* @param string source_url 来源页面URL可选
* @param string source_type 数据类型如poetry/chengyu等可选
* @param int source_id 数据ID可选
*/
public function correction_submit()
{
$this->_apiCount('correction_submit');
$content = input('post.content', '', 'trim');
$mail = input('post.mail', '', 'trim');
$source_url = input('post.source_url', '', 'trim');
$source_type = input('post.source_type', '', 'trim');
$source_id = input('post.source_id', 0, 'intval');
if (empty($content)) {
$this->error('请输入纠错内容');
}
if (mb_strlen($content) < 10) {
$this->error('内容过少,请详细描述问题');
}
if (mb_strlen($content) > 2000) {
$this->error('内容过长请控制在2000字以内');
}
$fullContent = '';
if (!empty($source_url)) {
$fullContent = '来源页面: ' . $source_url . "\n";
}
if (!empty($source_type) && $source_id > 0) {
$fullContent .= '数据类型: ' . $source_type . ' | ID: ' . $source_id . "\n";
}
$fullContent .= "\n" . $content;
$data = [
'content' => nl2br(htmlspecialchars($fullContent)),
'mail' => $mail ?: null,
'source_url' => $source_url ?: null,
'source_type' => $source_type ?: null,
'source_id' => $source_id > 0 ? $source_id : 0,
'switch' => 0,
'ip' => Request::instance()->ip(),
'updatetime' => time(),
'createtime' => time(),
];
$result = Db::name('perfect')->insert($data);
if ($result) {
$this->success('提交成功,感谢您的反馈!管理员会及时处理。');
} else {
$this->error('提交失败,请稍后再试');
}
}
/**
* @name 纠错记录列表API
* @desc 获取已处理的纠错记录列表
* @param int page 页码
* @param int limit 每页条数
* @param int status 状态 0待处理 1已处理
*/
public function correction_list()
{
$this->_apiCount('correction_list');
$page = input('get.page', 1, 'intval');
$limit = input('get.limit', 10, 'intval');
$status = input('get.status', -1, 'intval');
$query = Db::name('perfect');
if ($status >= 0) {
$query->where('switch', $status);
}
$total = $query->count();
$list = $query->order('createtime desc')
->page($page, $limit)
->field('id,content,mail,source_url,source_type,source_id,switch,ip,createtime,updatetime')
->select();
foreach ($list as &$item) {
$item['content'] = strip_tags($item['content']);
$item['status_text'] = $item['switch'] == 1 ? '已处理' : '待处理';
$item['createtime_text'] = date('Y-m-d H:i:s', $item['createtime']);
}
unset($item);
$this->success('成功', [
'total' => $total,
'list' => $list,
'page' => $page,
'limit' => $limit,
]);
}
/**
* @name 用户注销申请API
* @desc 用户提交账号注销申请
* @param string token 用户登录token
* @param string reason 注销原因(可选)
*/
private function _getUserByToken($token)
{
if (empty($token)) {
return null;
}
$tokenData = \app\common\library\Token::get($token);
if (!$tokenData || !isset($tokenData['user_id'])) {
return null;
}
$user = Db::name('user')->where('id', $tokenData['user_id'])->find();
return $user;
}
public function user_deletion_apply()
{
$this->_apiCount('user_deletion_apply');
$token = input('post.token', '', 'trim');
if (empty($token)) {
$token = $this->request->header('token', '');
}
$reason = input('post.reason', '', 'trim');
if (empty($token)) {
$this->error('请先登录');
}
$user = $this->_getUserByToken($token);
if (!$user) {
$this->error('用户不存在或登录已过期');
}
$existing = Db::name('user_deletion')
->where('user_id', $user['id'])
->where('status', 0)
->find();
if ($existing) {
$this->error('您已有待审核的注销申请');
}
$threeDaysLater = time() + 3 * 24 * 3600;
$data = [
'user_id' => $user['id'],
'username' => $user['username'] ?? $user['nickname'] ?? '',
'reason' => $reason ?: '用户主动申请注销',
'status' => 0,
'admin_id' => 0,
'admin_remark' => '',
'auto_delete_time' => $threeDaysLater,
'createtime' => time(),
'updatetime' => time(),
'deletetime' => null,
];
$result = Db::name('user_deletion')->insert($data);
if ($result) {
$this->success('注销申请已提交管理员3天内审核未审核将自动注销');
} else {
$this->error('提交失败');
}
}
/**
* @name 用户注销状态查询API
* @desc 查询当前用户的注销申请状态
* @param string token 用户登录token
*/
public function user_deletion_status()
{
$this->_apiCount('user_deletion_status');
$token = input('get.token', '', 'trim');
if (empty($token)) {
$token = $this->request->header('token', '');
}
if (empty($token)) {
$this->error('请先登录');
}
$user = $this->_getUserByToken($token);
if (!$user) {
$this->error('用户不存在或登录已过期');
}
$record = Db::name('user_deletion')
->where('user_id', $user['id'])
->order('createtime desc')
->find();
if (!$record) {
$this->success('无注销申请', ['has_request' => false]);
}
$statusMap = [0 => '待审核', 1 => '已通过(已注销)', 2 => '已拒绝', 3 => '已自动注销'];
$record['status_text'] = $statusMap[$record['status']] ?? '未知';
$record['createtime_text'] = date('Y-m-d H:i:s', $record['createtime']);
$record['auto_delete_time_text'] = date('Y-m-d H:i:s', $record['auto_delete_time']);
$this->success('成功', ['has_request' => true, 'record' => $record]);
}
/**
* @name 用户撤销注销申请API
* @desc 用户撤销待审核的注销申请
* @param string token 用户登录token
*/
public function user_deletion_cancel()
{
$this->_apiCount('user_deletion_cancel');
$token = input('post.token', '', 'trim');
if (empty($token)) {
$token = $this->request->header('token', '');
}
if (empty($token)) {
$this->error('请先登录');
}
$user = $this->_getUserByToken($token);
if (!$user) {
$this->error('用户不存在或登录已过期');
}
$record = Db::name('user_deletion')
->where('user_id', $user['id'])
->where('status', 0)
->find();
if (!$record) {
$this->error('没有待审核的注销申请');
}
$result = Db::name('user_deletion')
->where('id', $record['id'])
->update(['status' => 2, 'admin_remark' => '用户主动撤销', 'updatetime' => time()]);
if ($result) {
$this->success('已撤销注销申请');
} else {
$this->error('撤销失败');
}
}
// 验证验证码
public function checkCaptcha()
{
$code = input('post.code');
if (captcha_check($code)) {
$this->success('验证码正确');
} else {
$this->error('验证码错误!');
}
}
// 综合热榜
public function HotRefresh()
{
$id = input('post.t');
if(empty($id) && !isset($id)){
$this->error('参数错误');
}
$way = Db::name('hot')->where('id',$id)->value('way');
if($way){
$result = action($way);
if(strpos($result, "写入失败") !== false){
$this->error($result);
}
if(strpos($result, "写入成功") !== false){
$this->success($result);
}
if(strpos($result, "采集失败") !== false){
$this->error($result);
}
}else{
$this->error('参数错误');
}
}
// 图片转base64
public function ImageToBase()
{
// 获取上传的文件
$file = request()->file('image');
$type = $file->getInfo('type');
// 移动上传的文件到临时目录
$info = $file->move(ROOT_PATH . 'public' . DS . 'base64');
if (!$info) {
// 上传失败,返回错误信息
$this->error('上传失败',$file->getError());
}
// 获取文件路径
$filePath = './base64'. DS .$info->getSaveName();
// 将图片转换为Base64编码
$base64 = base64_encode(file_get_contents($filePath));
if(empty($base64)){
$this->error('转换失败');
}
$this->success('转换成功','data:'.$type.';base64,'.$base64);
}
// 爬虫模拟抓取工具
public function crawlerTool()
{
// 获取输入
$url = input('post.url');
$userAgent = input('post.userAgent');
// 简单的输入验证
if (!isset($url) || !isset($userAgent)) {
$this->error('参数缺少','请提供URL和User-Agent');
}
// 初始化cURL会话
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true); // 包含头部信息
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
// 执行cURL会话
$response = curl_exec($ch);
$curl_errno = curl_errno($ch);
$curl_error = curl_error($ch);
// 获取头部大小
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
// 关闭cURL会话
curl_close($ch);
if ($curl_errno) {
$this->error("页面抓取错误", " ({$curl_errno}): {$curl_error}");
}
// 根据headerSize分割响应的头部和内容
$header = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
// 如果需要的话,可以对内容进行编码转换
// 这一步骤可能不是必须的,取决于响应本身的编码
$body = mb_convert_encoding($body, 'UTF-8', mb_detect_encoding($body));
// 使用响应主体的长度来获取大小,变量$body已经包含了内容
$contentSizeBytes = strlen($body);
// 将大小转换为 kilobytes (KB)
$contentSizeKb = $contentSizeBytes / 1024;
$data = [
'head' => trim($header),
'content' => $body,
'size' => round($contentSizeKb, 2).'KB'
];
// 返回成功响应
$this->success('页面抓取成功', $data);
}
// 在线生成ICO图标
public function favicons()
{
$data['upmsg'] = '';
if (request()->isPost()) {
$upimage = input('file.upimage');
$getInfo = $upimage->getInfo();
if (isset($getInfo['tmp_name']) && $getInfo['tmp_name'] && is_uploaded_file($getInfo['tmp_name'])) {
if ((int)$getInfo['size'] > 2048000) {
return "<font color=\"red\">你上传的文件体积超过了限制 最大不能超过1MB</font><script>setTimeout(function() { history.back(); }, 2000);</script>";
} else {
$fileext = array("image/pjpeg", "image/gif", "image/x-png", "image/png", "image/jpeg", "image/jpg");
if (!in_array($getInfo['type'], $fileext)) {
return "<font color=\"red\">你上传的文件格式不正确 仅支持 jpggifpng</font><script>setTimeout(function() { history.back(); }, 2000);</script>";
} else {
$type = substr(strrchr($getInfo['name'], '.'), 1);
switch ($type) {
case 'pjpeg':
case 'jpeg':
case 'jpg':
$im = imagecreatefromjpeg($getInfo['tmp_name']);
break;
case 'x-png':
case 'png':
$im = imagecreatefrompng($getInfo['tmp_name']);
break;
case 'gif':
$im = imagecreatefromgif($getInfo['tmp_name']);
break;
default:
$im = null;
}
if ($im) {
$imginfo = getimagesize($getInfo['tmp_name']);
if (!is_array($imginfo)) {
return "<font color=\"red\">图形格式错误!</font><script>setTimeout(function() { history.back(); }, 2000);</script>";
} else {
switch (input('favicon_size')) {
case 1;
$resize_im = imagecreatetruecolor(16, 16);
$size = 16;
break;
case 2;
$resize_im = imagecreatetruecolor(32, 32);
$size = 32;
break;
case 3;
$resize_im = imagecreatetruecolor(48, 48);
$size = 48;
break;
case 4;
$resize_im = imagecreatetruecolor(64, 64);
$size = 64;
break;
case 5;
$resize_im = imagecreatetruecolor(128, 128);
$size = 128;
break;
default;
$resize_im = imagecreatetruecolor(32, 32);
$size = 32;
break;
}
imagecopyresampled($resize_im, $im, 0, 0, 0, 0, $size, $size, $imginfo[0], $imginfo[1]);
$icon = new \Net\Ico();
$gd_image_array = array($resize_im);
$icon_data = $icon->GD2ICOstring($gd_image_array);
header("Accept-Ranges: bytes");
header("Accept-Length: " . strlen($icon_data));
header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=" . 'favicon.ico');
return $icon_data;
}
} else {
return "<font color=\"red\">生成错误请重试!</font><script>setTimeout(function() { history.back(); }, 2000);</script>";
}
}
}
}else{
return "<font color=\"red\">请上传图片</font><script>setTimeout(function() { history.back(); }, 2000);</script>";
}
}
}
// 赞赏链接跳转方法
public function reward_link()
{
$id = input('post.id');
$url = Db::name('appreciation')->where('id',$id)->where('switch',1)->value('url');
if(empty($url)){
$this->error('不存在链接!');
}
Db::name('appreciation')->where('id',$id)->setInc('hits');
$this->success('跳转',$url.'?rel='.Request::instance()->domain());
}
// ip地址查下
public function ip()
{
$ip = input('post.ip');
if (!$ip) {
$ip = $this->request->ip();
}
$ips = new \Net\Ips('./qqwry.dat');
$data['ip'] = $ip;
$data['domain'] = gethostbyname($ip);
$domain = preg_replace('/(\d+)..*/', '\\1', $data['domain']);
if ('1' <= $domain && $domain <= '126') {
$data['fw'] = '1.0.0.1 - 126.155.255.254';
} elseif ('128' <= $domain && $domain <= '191') {
$data['fw'] = '128.0.0.1 - 191.255.255.254';
} elseif ('192' <= $domain && $domain <= '223') {
$data['fw'] = '192.0.0.1 - 223.255.255.254';
}
$fwq = $ips->Getlocation($data['domain']);
$data['city'] = strToUTF8($fwq['country'] . ' ' . $fwq['area']);
$data['num'] = ip2long($data['domain']);
if($data){
$this->success('查询成功',$data);
}
$this->error('查询错误');
}
// 单页采集
public function pages()
{
$url = input('post.url');
if (empty($url)) {
$this->error('请提供采集地址(url参数)');
}
$urls = urls($url);
// iconv('GBK','UTF-8//IGNORE',file_get_contents($url))
$ql = QueryList::get($url,[],[
'timeout'=>30,//超时设置
'headers'=>[
'User-Agent'=>'Mozilla/5.0(Windows NT 10.0;Win64; x64)AppleWebkit/537.36(KHTML,like Gecko) Chrome/80.0.3987.163 Safari/537.36',
]
]);
$res = Db::name('gather')->where('url',"like","%".$urls."%")->field('rule,tag,classify')->find();//获取采集规则
if($res){
// encoding('UTF-8','GB2312')->removeHead()->
$data = $ql->rules(json_decode($res['rule'],true))->queryData();//采集开始
if(!empty($data)){
// 查询分类名
$cla = Db::name('category')->where('id',$res['classify'])->field('id,name')->find();
$data['sort'] = $cla;//赋值分类ID
if(!empty($res['tag'])){
$keys = explode(",",$res['tag']);
foreach ($keys as $value){
$data['content'] = str_ireplace($value,'',$data['content']);
}
}
// if(isset($data['title'])){
// $data['title'] = str_replace("作文", "", $data['title']);
// }
if(isset($data['content']) && !empty($data['content'])){
if (strpos($data['content'], '<img') !== false) {
// 使用正则表达式替换图片标签中的相对地址
$pattern = "/<img(.*?)src=[\"'](.*?)[\"'](.*?)>/i";
$data['content'] = preg_replace_callback($pattern, function($matches) use ($url) {
$imageUrl = $matches[2];
if (preg_match('/^([a-z]+:\/\/[^\/]+)/i', $url, $matche)) {
$http = $matche[1];
} else {
$http = '';
}
// 判断图片地址是否为相对地址
if (strpos($imageUrl, 'http') !== 0) {
// 将相对地址转换为绝对地址
return "<img{$matches[1]}src='" . $http . "/$imageUrl'{$matches[3]}>";
} else {
return "<img{$matches[1]}src='{$imageUrl}'{$matches[3]}>";
}
}, $data['content']);
}
$data['content'] = preg_replace('/<p>\s*<\/p>/', '', $data['content']);
$data['content'] = preg_replace('/<p>\s*<br\s*\/>\s*<\/p>/', '', $data['content']);///<p>\s*<br \/>\s*<\/p>/
}
$this->success('成功',$data);
}else{
$this->error('失败',$data);
}
}else{
$this->error('不存在采集规则');
}
}
// 生成网页快捷方式
public function create()
{
// 获取传递过来的参数值
$db = input('get.');
// 拼接快捷方式的文件路径和名称
$file_path = './shortcut/' . $db['name'] . '.url';
// 生成快捷方式文件内容
$content = "[InternetShortcut]\r\nURL={$db['url']}";
// 将文件内容写入到指定路径的文件中
file_put_contents($file_path, $content);
// 设置响应头信息
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $db['name'] . '.url');
header('Content-Length: ' . filesize($file_path));
// 输出文件并删除生成的快捷方式文件
readfile($file_path);
// unlink($file_path);
}
/**
* TTS文字转语音接口
* 基于浏览器端Web Speech API此接口提供语音参数配置和文本预处理
*/
public function ttsConfig()
{
$text = input('post.text', '', 'trim');
$rate = input('post.rate', 1.0, 'floatval');
$pitch = input('post.pitch', 1.0, 'floatval');
$volume = input('post.volume', 1.0, 'floatval');
$lang = input('post.lang', 'zh-CN', 'trim');
if (empty($text)) {
$this->error('请输入要转换的文本');
}
if (mb_strlen($text) > 5000) {
$this->error('文本长度不能超过5000字');
}
if ($rate < 0.5 || $rate > 2.0) {
$this->error('语速范围: 0.5-2.0');
}
if ($pitch < 0.5 || $pitch > 2.0) {
$this->error('音调范围: 0.5-2.0');
}
if ($volume < 0 || $volume > 1.0) {
$this->error('音量范围: 0-1.0');
}
$allowedLangs = ['zh-CN', 'zh-TW', 'en-US', 'en-GB', 'ja-JP', 'ko-KR', 'fr-FR', 'de-DE', 'es-ES', 'ru-RU'];
if (!in_array($lang, $allowedLangs)) {
$this->error('不支持的语言: ' . $lang);
}
$text = strip_tags($text);
$text = htmlspecialchars_decode($text, ENT_QUOTES);
$this->success('配置成功', [
'text' => $text,
'rate' => $rate,
'pitch' => $pitch,
'volume' => $volume,
'lang' => $lang,
'charCount' => mb_strlen($text),
'engine' => 'Web Speech API',
]);
}
public function jiufang_search()
{
$keyword = input('keyword', '', 'trim');
$category = input('category', '', 'trim');
$component = input('component', '', 'trim');
$page = input('page', 1, 'intval');
$pageSize = input('pageSize', 20, 'intval');
$pageSize = min(max($pageSize, 1), 50);
$where = ['switch' => 1];
$query = Db::name('jiufang')->where($where);
if (!empty($keyword)) {
if (mb_strlen($keyword) > 50) {
$this->error('关键词不能超过50个字符');
}
$query->where(function ($q) use ($keyword) {
$q->whereOr('name', 'like', '%' . $keyword . '%')
->whereOr('source', 'like', '%' . $keyword . '%')
->whereOr('usage', 'like', '%' . $keyword . '%')
->whereOr('ingredients', 'like', '%' . $keyword . '%')
->whereOr('categories', 'like', '%' . $keyword . '%')
->whereOr('components', 'like', '%' . $keyword . '%');
});
}
if (!empty($category)) {
$query->where('categories', 'like', '%' . $category . '%');
}
if (!empty($component)) {
$query->where('components', 'like', '%' . $component . '%');
}
$total = $query->count();
$list = $query->page($page, $pageSize)
->field('id,name,source,categories,components,views')
->order('views desc')
->select();
$this->success('成功', [
'total' => $total,
'list' => $list,
'page' => $page,
'pageSize' => $pageSize,
]);
}
public function site_search()
{
$keyword = input('keyword', '', 'trim');
$type = input('type', 'tool', 'trim');
if (empty($keyword) || mb_strlen($keyword) > 50) {
$this->error('请输入有效的搜索关键词');
}
$results = [];
$kw = '%' . $keyword . '%';
if ($type === 'tool' || $type === 'all') {
$tools = Db::name('category')
->where('name|description', 'like', $kw)
->where('status', 'normal')
->field('id,name,nickname,description')
->limit(10)
->select();
foreach ($tools as $t) {
$results[] = [
'title' => $t['name'],
'desc' => $t['description'],
'url' => '/' . $t['nickname'] . '.html',
'type_label' => '工具',
];
}
}
if ($type === 'article' || $type === 'all') {
$articles = Db::name('article')
->where('title|summary', 'like', $kw)
->where('status', 'normal')
->field('id,title,summary')
->limit(10)
->select();
foreach ($articles as $a) {
$results[] = [
'title' => $a['title'],
'desc' => $a['summary'],
'url' => '/article/' . $a['id'] . '.html',
'type_label' => '文章',
];
}
}
if ($type === 'jiufang' || $type === 'all') {
$jiufang = Db::name('jiufang')
->where('name|ingredients|usage', 'like', $kw)
->where('switch', 1)
->field('id,name,categories')
->limit(10)
->select();
foreach ($jiufang as $j) {
$results[] = [
'title' => $j['name'],
'desc' => $j['categories'],
'url' => '/jiufang/' . $j['id'] . '.html',
'type_label' => '酒方',
];
}
}
if ($type === 'hanzi' || $type === 'all') {
$hanzi = Db::name('hanzi')
->where('zi|pinyin|bushou', 'like', $kw)
->where('switch', 1)
->field('id,zi,pinyin,bushou')
->limit(10)
->select();
foreach ($hanzi as $h) {
$results[] = [
'title' => $h['zi'] . ' (' . $h['pinyin'] . ')',
'desc' => '部首: ' . ($h['bushou'] ?? ''),
'url' => '/cidian/' . $h['id'] . '.html',
'type_label' => '汉字',
];
}
}
if ($type === 'poetry' || $type === 'all') {
$poetry = Db::name('poetry')
->where('name|content|author', 'like', $kw)
->where('switch', 1)
->field('id,name,author')
->limit(10)
->select();
foreach ($poetry as $p) {
$results[] = [
'title' => $p['name'],
'desc' => '作者: ' . $p['author'],
'url' => '/poetry/' . $p['id'] . '.html',
'type_label' => '诗词',
];
}
}
if ($type === 'drug' || $type === 'all') {
$drug = Db::name('drug')
->where('name|syz', 'like', $kw)
->where('switch', 1)
->field('id,name')
->limit(10)
->select();
foreach ($drug as $d) {
$results[] = [
'title' => $d['name'],
'desc' => '',
'url' => '/drug/' . $d['id'] . '.html',
'type_label' => '药品',
];
}
}
$this->success('成功', $results);
}
/**
* @name 接口调用计数
* @desc 使用txt文件记录接口调用次数不创建新表
*/
private function _apiCount($apiName)
{
$dir = RUNTIME_PATH . 'api_count';
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$today = date('Y-m-d');
$file = $dir . DS . $today . '.txt';
$data = [];
if (file_exists($file)) {
$content = file_get_contents($file);
$data = json_decode($content, true) ?: [];
}
if (!isset($data[$apiName])) {
$data[$apiName] = 0;
}
$data[$apiName]++;
file_put_contents($file, json_encode($data));
return $data[$apiName];
}
/**
* @name 读取接口调用计数
* @desc 读取txt文件中的接口调用统计
*/
private function _getApiCountData($date = null)
{
$dir = RUNTIME_PATH . 'api_count';
if (!is_dir($dir)) {
return [];
}
if ($date === null) {
$date = date('Y-m-d');
}
$file = $dir . DS . $date . '.txt';
if (!file_exists($file)) {
return [];
}
$content = file_get_contents($file);
return json_decode($content, true) ?: [];
}
/**
* @name 站点统计概览
* @desc 返回站点核心统计数据(今日/本周/本月/累计)
*/
public function stats_overview()
{
$this->_apiCount('stats_overview');
$todayStart = strtotime('today');
$weekStart = strtotime('monday this week');
$monthStart = strtotime('first day of this month 00:00:00');
$data = [
'user' => $this->_timeStats('user', $todayStart, $weekStart, $monthStart),
'article' => $this->_timeStats('article', $todayStart, $weekStart, $monthStart),
'signin' => $this->_timeStats('user_signin', $todayStart, $weekStart, $monthStart),
'favorite'=> $this->_timeStats('user_favorite', $todayStart, $weekStart, $monthStart),
'note' => $this->_timeStats('user_note', $todayStart, $weekStart, $monthStart),
];
$data['tool'] = [
'total' => Db::name('category')->where('pid', '>', 0)->count(),
'normal' => Db::name('category')->where('pid', '>', 0)->where('status', 'normal')->count(),
'hidden' => Db::name('category')->where('pid', '>', 0)->where('status', 'hidden')->count(),
];
$data['attachment'] = [
'total' => Db::name('attachment')->count(),
'size' => Db::name('attachment')->sum('filesize'),
];
$contentTables = ['hanzi','poetry','drug','jiufang','cy','zc','story','wisdom','efs','couplet','riddle','brainteaser','zgjm','word'];
$contentTotal = 0;
$contentDetail = [];
foreach ($contentTables as $ct) {
try {
$c = Db::name($ct)->where('switch', 1)->count();
if ($c === 0) {
$c = Db::name($ct)->count();
}
$contentTotal += $c;
$contentDetail[$ct] = $c;
} catch (\Exception $e) {
$contentDetail[$ct] = 0;
}
}
$data['content'] = [
'total' => $contentTotal,
'detail' => $contentDetail,
];
$data['timestamp'] = time();
$data['date'] = date('Y-m-d H:i:s');
$this->success('成功', $data);
}
/**
* @name 按时间维度统计
* @desc 统计指定表今日/本周/本月/累计数据
*/
private function _timeStats($table, $todayStart, $weekStart, $monthStart)
{
$tableName = $table;
$total = Db::name($tableName)->count();
$today = Db::name($tableName)->where('createtime', '>=', $todayStart)->count();
$week = Db::name($tableName)->where('createtime', '>=', $weekStart)->count();
$month = Db::name($tableName)->where('createtime', '>=', $monthStart)->count();
$result = [
'total' => $total,
'today' => $today,
'week' => $week,
'month' => $month,
];
try {
$viewsTotal = Db::name($tableName)->sum('views');
if ($viewsTotal > 0) {
$result['views_total'] = intval($viewsTotal);
}
} catch (\Exception $e) {}
return $result;
}
/**
* @name 热门工具排行
* @desc 返回浏览量最高的工具列表
*/
public function stats_hot_tools()
{
$this->_apiCount('stats_hot_tools');
$limit = input('limit', 20, 'intval');
$limit = min($limit, 100);
$list = Db::name('category')
->where('pid', '>', 0)
->where('status', 'normal')
->order('views desc, id asc')
->field('id,name,nickname,views,description')
->limit($limit)
->select();
foreach ($list as &$item) {
$item['url'] = '/' . $item['nickname'] . '.html';
}
$this->success('成功', $list);
}
/**
* @name 热门文章排行
* @desc 返回浏览量最高的文章列表
*/
public function stats_hot_articles()
{
$this->_apiCount('stats_hot_articles');
$limit = input('limit', 20, 'intval');
$limit = min($limit, 100);
$list = Db::name('article')
->where('status', 'normal')
->order('views desc, id desc')
->field('id,title,views,likes,favorites,createtime')
->limit($limit)
->select();
foreach ($list as &$item) {
$item['url'] = '/article/' . $item['id'] . '.html';
$item['createtime_text'] = $item['createtime'] ? date('Y-m-d', intval($item['createtime'])) : '-';
}
$this->success('成功', $list);
}
/**
* @name 内容数据统计
* @desc 返回各内容类型的数据量和浏览量统计
*/
public function stats_content()
{
$this->_apiCount('stats_content');
$tables = [
['table' => 'hanzi', 'name' => '汉字', 'icon' => '📖'],
['table' => 'poetry', 'name' => '诗词', 'icon' => '📜'],
['table' => 'drug', 'name' => '药品', 'icon' => '💊'],
['table' => 'jiufang', 'name' => '酒方', 'icon' => '🍶'],
['table' => 'cy', 'name' => '成语', 'icon' => '🔤'],
['table' => 'zc', 'name' => '词典/组词', 'icon' => '📚'],
['table' => 'story', 'name' => '故事', 'icon' => '📖'],
['table' => 'wisdom', 'name' => '名言', 'icon' => '💡'],
['table' => 'efs', 'name' => '歇后语', 'icon' => '😄'],
['table' => 'couplet', 'name' => '对联', 'icon' => '🧧'],
['table' => 'riddle', 'name' => '谜语', 'icon' => '❓'],
['table' => 'brainteaser','name' => '脑筋急转弯', 'icon' => '🧠'],
['table' => 'zgjm', 'name' => '周公解梦', 'icon' => '🌙'],
['table' => 'word', 'name' => '词语', 'icon' => '📝'],
];
$result = [];
foreach ($tables as $t) {
try {
$count = Db::name($t['table'])->where('switch', 1)->count();
if ($count === 0) {
$count = Db::name($t['table'])->count();
}
$views = 0;
try {
$views = intval(Db::name($t['table'])->sum('views'));
} catch (\Exception $e) {}
$result[] = [
'table' => $t['table'],
'name' => $t['name'],
'icon' => $t['icon'],
'count' => $count,
'views' => $views,
];
} catch (\Exception $e) {
$result[] = [
'table' => $t['table'],
'name' => $t['name'],
'icon' => $t['icon'],
'count' => 0,
'views' => 0,
'error' => $e->getMessage(),
];
}
}
$this->success('成功', $result);
}
/**
* @name 用户活跃度统计
* @desc 返回用户签到/笔记/收藏等活跃数据
*/
public function stats_user_activity()
{
$this->_apiCount('stats_user_activity');
$todayStart = strtotime('today');
$weekStart = strtotime('monday this week');
$data = [
'signin' => [
'today' => Db::name('user_signin')->where('createtime', '>=', $todayStart)->count(),
'week' => Db::name('user_signin')->where('createtime', '>=', $weekStart)->count(),
'total' => Db::name('user_signin')->count(),
],
'note' => [
'today' => Db::name('user_note')->where('createtime', '>=', $todayStart)->count(),
'week' => Db::name('user_note')->where('createtime', '>=', $weekStart)->count(),
'total' => Db::name('user_note')->count(),
],
'favorite' => [
'today' => Db::name('user_favorite')->where('createtime', '>=', $todayStart)->count(),
'week' => Db::name('user_favorite')->where('createtime', '>=', $weekStart)->count(),
'total' => Db::name('user_favorite')->count(),
],
'article' => [
'today' => Db::name('article')->where('createtime', '>=', $todayStart)->count(),
'week' => Db::name('article')->where('createtime', '>=', $weekStart)->count(),
'total' => Db::name('article')->count(),
],
];
$data['user_total'] = Db::name('user')->count();
$data['user_active_week'] = Db::name('user')->where('logintime', '>=', $weekStart)->count();
$this->success('成功', $data);
}
/**
* @name 趋势数据统计
* @desc 返回最近7天/30天的每日数据趋势
*/
public function stats_trend()
{
$this->_apiCount('stats_trend');
$days = input('days', 7, 'intval');
$days = min($days, 30);
$days = max($days, 7);
$result = [];
for ($i = $days - 1; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-{$i} days"));
$dayStart = strtotime($date);
$dayEnd = $dayStart + 86400;
$result[] = [
'date' => $date,
'user' => Db::name('user')->where('createtime', '>=', $dayStart)->where('createtime', '<', $dayEnd)->count(),
'article' => Db::name('article')->where('createtime', '>=', $dayStart)->where('createtime', '<', $dayEnd)->count(),
'signin' => Db::name('user_signin')->where('createtime', '>=', $dayStart)->where('createtime', '<', $dayEnd)->count(),
'note' => Db::name('user_note')->where('createtime', '>=', $dayStart)->where('createtime', '<', $dayEnd)->count(),
'favorite'=> Db::name('user_favorite')->where('createtime', '>=', $dayStart)->where('createtime', '<', $dayEnd)->count(),
];
}
$this->success('成功', $result);
}
/**
* @name 接口调用统计
* @desc 返回接口调用次数统计基于txt文件计数
*/
public function stats_api_usage()
{
$this->_apiCount('stats_api_usage');
$days = input('days', 7, 'intval');
$days = min($days, 30);
$days = max($days, 1);
$result = [];
$allApis = [];
for ($i = $days - 1; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-{$i} days"));
$data = $this->_getApiCountData($date);
$dayTotal = 0;
foreach ($data as $api => $count) {
if (!in_array($api, $allApis)) {
$allApis[] = $api;
}
$dayTotal += $count;
}
$result[] = [
'date' => $date,
'total' => $dayTotal,
'apis' => $data,
];
}
$apiSummary = [];
foreach ($allApis as $api) {
$total = 0;
foreach ($result as $day) {
$total += isset($day['apis'][$api]) ? $day['apis'][$api] : 0;
}
$apiSummary[] = [
'api' => $api,
'total' => $total,
];
}
usort($apiSummary, function($a, $b) {
return $b['total'] - $a['total'];
});
$grandTotal = 0;
foreach ($result as $day) {
$grandTotal += $day['total'];
}
$this->success('成功', [
'daily' => $result,
'api_summary'=> $apiSummary,
'grand_total'=> $grandTotal,
'api_count' => count($allApis),
]);
}
/**
* @name 实时请求统计
* @desc 返回累计请求总量和每秒请求数支持app端轮询
*/
public function stats_realtime()
{
$dir = RUNTIME_PATH . 'api_count';
if (!is_dir($dir)) {
$this->success('成功', [
'total_requests' => 0,
'today_requests' => 0,
'qps' => 0,
'apis' => [],
'timestamp' => time(),
]);
}
$grandTotal = 0;
$files = glob($dir . DS . '*.txt');
$todayData = [];
foreach ($files as $f) {
$content = file_get_contents($f);
$data = json_decode($content, true) ?: [];
foreach ($data as $api => $count) {
$grandTotal += $count;
}
$basename = basename($f, '.txt');
if ($basename === date('Y-m-d')) {
$todayData = $data;
}
}
$todayTotal = 0;
foreach ($todayData as $count) {
$todayTotal += $count;
}
$qpsFile = $dir . DS . '_qps.json';
$qps = 0;
$now = time();
if (file_exists($qpsFile)) {
$prev = json_decode(file_get_contents($qpsFile), true) ?: [];
if (isset($prev['last_time']) && ($now - $prev['last_time']) > 0) {
$diff = $todayTotal - ($prev['last_count'] ?? 0);
$elapsed = $now - $prev['last_time'];
if ($elapsed > 0 && $diff >= 0) {
$qps = round($diff / $elapsed, 2);
}
}
}
$qpsData = ['last_time' => $now, 'last_count' => $todayTotal, 'qps' => $qps];
file_put_contents($qpsFile, json_encode($qpsData));
$apis = [];
foreach ($todayData as $api => $count) {
$apis[] = ['name' => $api, 'count' => $count];
}
usort($apis, function($a, $b) { return $b['count'] - $a['count']; });
$this->success('成功', [
'total_requests' => $grandTotal,
'today_requests' => $todayTotal,
'qps' => $qps,
'apis' => $apis,
'timestamp' => time(),
'date' => date('Y-m-d H:i:s'),
]);
}
}