dd/app/Services/Article/CacheTg.php
2026-01-15 13:35:05 +08:00

489 lines
18 KiB
PHP
Executable File
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
declare(strict_types=1);
namespace App\Services\Article;
use App\Models\ArticleCache as ModelArticleCache;
use App\Models\ArticleSchema as ModelArticleSchema;
use App\Models\ArticleCacheTime as ModelArticleCacheTime;
use App\Models\TelegramKey as ModelTelegramKey;
use App\Services\TomTool\Flow;
use App\Services\SlaveMap as ServiceSlaveMap;
use App\Services\Article\Cmd as ServiceArticleCmd;
final class CacheTg
{
public static function start()
{
return new self();
}
public function add($sGroupCode, $sContent)
{
$oResult = Flow::make("cacheAdd");
$oModelArticleCache = new ModelArticleCache();
$oModelArticleCache->content = $sContent;
$oModelArticleCache->group_code = $sGroupCode;
$oModelArticleCache->created_at = time();
$r = $oModelArticleCache->save();
if (!$r) {
return $oResult->fail("写入缓存失败");
}
$oModelArticleCacheTime = ModelArticleCacheTime::first();
$oModelArticleCacheTime->time = time();
$r = $oModelArticleCacheTime->save();
if (!$r) {
return $oResult->fail("更新cacheTime失败");
}
return $sGroupCode."写入一条缓存";
}
public function saveToSchema(): Flow
{
$oFlow = Flow::start("save");
$oArticleCache = ModelArticleCache::where("status", 1)->get();
// 第一步格式化通用tgSchema
$aDataMix["data"] = [];
foreach ($oArticleCache as $oArticleCacheRow) {
if (!isset($sFromPaichu)) { // 等于只是第一次each会赋值
$sFromPaichu = $oArticleCacheRow->group_code;
}
if ($oArticleCacheRow->group_code !== $sFromPaichu) {
continue; // 实现一次只save一种group_code
}
$aContent = json_decode($oArticleCacheRow->content, true);
$aReturn = $this->_schemaByCache_common($aContent, $oArticleCacheRow->group_code);
foreach ($aReturn as $aData) {
$aDataMix["data"][] = $aData;
}
$sGroupCode = $oArticleCacheRow->group_code; // 这里等于直接提取后一个group_code不算完美但没大问题
}
// 第二步进一步格式化为对应form的Schema
$method = '_schemaWithFrom_' . $oArticleCacheRow->group_code;
if (method_exists($this, $method)) {
$aDataMix = $this->$method($aDataMix, $sGroupCode);
} else {
// 没有对应直接不处理
}
$y = date("Y");
$sYqian = substr($y, 1, 2);
$sYhou = substr($y, 3, 1);
if ($oArticleCacheRow->code) {
$sArticleCode = $oArticleCacheRow->code;
} else {
$sArticleCode = $sGroupCode."-".$sYqian.date("mdHis").$sYhou;
}
$oArticleSchema = ModelArticleSchema::where("code", $sArticleCode)->first();
if (!$oArticleSchema) {
$oArticleSchema = new ModelArticleSchema();
}
$oArticleSchema->schema = json_encode($aDataMix["data"]);
$oArticleSchema->code = $sArticleCode;
$oArticleSchema->group_code = $sGroupCode;
$oArticleSchema->name = $aDataMix["name"] ?? "none";
$oArticleSchema->save();
ModelArticleCache::where("status", 1)->where("group_code", $sFromPaichu)->update([
"status" => 0, // 上线改0
"code" => $sArticleCode,
]);
ServiceArticleCmd::startWithO($oArticleSchema)->syncOpen()->save("new");
return $oFlow->done();
}
private function _schemaByCache_common($aContent, $sGroupCode)
{
$aDataMix = [];
if (isset($aContent["message"]["document"]["mime_type"])) { // 包含文档
$sDocumentType = $aContent["message"]["document"]["mime_type"];
$aDataTmp = []; // new data init
$aDataTmp["type"] = "str";
$aDataTmp["name"] = "str";
$aDataTmp["content"]["base"] = "";
if (strpos($sDocumentType, "image") !== false) { // 包含图片
$sDocumentName = $aContent["message"]["document"]["file_name"];
// img data init
$aDataTmp["type"] = "img";
$aDataTmp["name"] = "img_cesu";
$aDataTmp["content"]["base"] = $sDocumentName;
$aDataTmp["content"]["img"] = [
"title" => $sDocumentName,
"base" => '',
"thumb" => '',
];
$aCacheImgId = [
"base" => $aContent["message"]["document"]["file_id"],
"thumb" => $aContent["message"]["document"]["thumb"]["file_id"],
];
$sBotToken = ModelTelegramKey::botTokenByCode($sGroupCode);
$sPathUpload = $_ENV["dir_base"]."public/upload/";
if (!is_dir($sPathUpload)) {
mkdir($sPathUpload, 0777, true);
}
foreach ($aCacheImgId as $k => $aImgIdRow) {
// 第一步:获取 file_path
$getFileUrl = "https://api.telegram.org/bot".$sBotToken."/getFile?file_id=".$aImgIdRow;
$response = file_get_contents($getFileUrl);
$data = json_decode($response, true);
if (!$data['ok']) {
$aDataTmp["content"]["img"][$k] = "获取图片途径失败";
continue;
}
$filePath = $data['result']['file_path'];
// 第二步:下载文件内容
$downloadUrl = "https://api.telegram.org/file/bot".$sBotToken."/".$filePath;
$imageData = file_get_contents($downloadUrl);
if ($imageData === false) {
$aDataTmp["content"]["img"][$k] = "下载文件失败";
continue;
}
// 第三步:保存到服务器
// $savePath = 'upload/';
// if (!is_dir($savePath)) {
// mkdir($savePath, 0777, true);
// }
$fileName = basename($filePath);
$fileName = uniqid($sGroupCode.'-'.$k.'-'.time().'-').$fileName; // 加前缀并提高熵值
file_put_contents($sPathUpload . $fileName, $imageData);
$aDataTmp["content"]["img"][$k] = $_ENV["APP_URL"].'/upload/'.$fileName;;
}
} else { // 包含图片 end
$aDataTmp["content"]["base"] = "未处理类型{$sDocumentType}";
}
$aDataMix[] = $aDataTmp;
} // 包含文档 end
// 包含文本
if (isset($aContent["message"]["caption"])) {
$sStr = $aContent["message"]["caption"];
$aEntities = $aContent["message"]["caption_entities"] ?? [];
foreach ($aEntities as $aEntitiesRow) {
$iEntitiesOffset = $aEntitiesRow["offset"] ?? 0;
$iEntitiesLen = $aEntitiesRow["length"] ?? 0;
$sEntitiesType = $aEntitiesRow["type"] ?? "";
if ($sGroupCode == "ffq_tg_a") { // 按说都改替换,缝补就这样
if ($sEntitiesType == "text_link") {
$sEntitiesUrl = $aEntitiesRow["url"] ?? "";
$sLinkText = $this->tg_replace($sStr, "", $iEntitiesOffset, $iEntitiesLen, true);
$sLinkHtml = "<a class='text_link' href='".$sEntitiesUrl."'>".$sLinkText."</a>";
$sStr = $this->tg_replace($sStr, $sLinkHtml, $iEntitiesOffset, $iEntitiesLen, false);
}
}
}
$aDataTmp = [];
$aDataTmp["type"] = "str";
$aDataTmp["name"] = "str";
$aDataTmp["content"]["base"] = $sStr;
$aDataMix[] = $aDataTmp;
}
if (count($aDataMix) == 0) {
$aDataTmp = [];
$aDataTmp["type"] = "loss";
$aDataTmp["name"] = "loss";
$aDataTmp["content"]["base"] = "提取失败:".json_encode($aContent);
$aDataMix[] = $aDataTmp;
}
return $aDataMix;
}
private function _schemaWithFrom_ffq_tg_a($aDataMix, $sGroupCode)
{
$aDataMixNew = [];
$aDataMixNew["name"] = '';
$aDataMixNew["title"] = '';
$aDataMixNew["data"] = [];
foreach ($aDataMix["data"] as $aDataMixRow) {
$sContent = $aDataMixRow["content"]["base"] ?? "⚠️ 没有content";
$sType = $aDataMixRow['type'] ?? "str";
if ($sType == 'str') {
$sRowName = 'str';
// $sContent = str_replace("<br />", " ", $sContent);
$aContent = $this->ffq_tg_str_fetch($sContent);
$sArticleName = $aContent["name"] ?? '';
$sStrTiqu = $aContent["tiqu"] ?? '';
$sStrBaoliu = $aContent["baoliu"] ?? '';
$sStrBaoliu = str_replace("翻翻墙", "{{sAuthorName}}", $sStrBaoliu);
$sStrBaoliu = str_replace("😀", "{{sIconText}}", $sStrBaoliu);
$sStrBaoliu = str_replace("📊", "{{sIconText}}", $sStrBaoliu);
if (!$sStrTiqu) {
$aDataMixNew["data"][] = [
"type" => "str",
"name" => "tag_tiqu",
"content" => [
"base" => $sStrTiqu,
],
];
}
if ($sArticleName) {
$aDataMixNew["name"] = $sArticleName;
$aDataMixNew["data"][] = [
"type" => "str",
"name" => "schema_name",
"content" => [
"base" => $sArticleName,
],
];
}
$sStrBaoliu = nl2br($sStrBaoliu);
$sStrBaoliu = str_replace(array("\r", "\n"), '', $sStrBaoliu);
$tmp = "<br \/><br \/>";
if (preg_match('/机场自介(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) {
$sStr = $matches[1];
$aDataMixNew["data"][] = [
"type" => "str",
"name" => "jichangzijie",
"content" => [
"title" => "机场自介",
"base" => $sStr
],
];
}
if (preg_match('/链接导航(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) {
$sStr = $matches[1];
$aDataMixNew["data"][] = [
"type" => "str",
"name" => "lianjiedaohang",
"content" => [
"title" => "链接导航",
"base" => $sStr,
],
];
}
if (preg_match('/主观点评(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) {
$sStr = $matches[1];
$aDataMixNew["data"][] = [
"type" => "str",
"name" => "zhuguandianping",
"content" => [
"title" => "{{sAuthorName}}主观点评",
"base" => $sStr
],
];
}
if (preg_match('/主观评分(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) {
$sStr = $matches[1];
$aDataMixNew["data"][] = [
"type" => "str",
"name" => "zhuguanpingfen",
"content" => [
"title" => "{{sAuthorName}}主观评分",
"base" => $sStr
],
];
}
// if (preg_match('/提醒您(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) {
// $sStr = $matches[1];
// $aDataMixNew["data"][] = [
// "type" => "str",
// "name" => "tixingnin",
// "content" => [
// "title" => "提醒",
// "base" => $sStr
// ],
// ];
// }
$aDataMixNew["data"][] = [
"type" => "str",
"name" => "tixingnin",
"content" => [
"title" => "提醒",
"base" => "任何机场都有跑路可能,建议仅月付,以降低风险。(不针对任何机场)",
],
];
//// 本地不爬,上线取消注释
preg_match('/论坛[:]\s*(https?:\/\/[^\s]*?\.html)/u', $sStrBaoliu, $matches);
if (isset($matches[1])) {
$sUrl = $matches[1];
$aReturn = $this->getTaocan_ffq_tg($sUrl);
if (Flow::isFailResult($aReturn)) {
$tmp = "⚠️ 套餐table获取失败";
}
$tmp = $aReturn["sData"];
$aDataTmp = [];
$aDataTmp["type"] = "str";
$aDataTmp["name"] = "table_taocan";
$aDataTmp["content"]["base"] = $tmp;
$aDataMixNew["data"][] = $aDataTmp;
}
if (str_contains($sStrBaoliu, "评分")) {
$sRowName = "str_jieshao";
}
$aDataMixNew["data"][] = [
"type" => "str",
"name" => $sRowName,
"content" => [
"base" => $sStrBaoliu,
],
];
} else if ($sType == 'img') {
// $aDataMixNew["images"][] = $aDataMixRow;
$aDataMixNew["data"][] = $aDataMixRow;
} else {
$aDataMixNew["data"][] = $aDataMixRow;
}
}
return $aDataMixNew;
}
private function ffq_tg_str_fetch($str) {
$arr = [
"tiqu" => '',
"baoliu" => $str,
"name" => '',
];
if (preg_match('/#([^📊]+)📊/u', $str, $matches)) {
$arr["tiqu"] = $matches[1]; // 提取内容(不含 # 和 📊)
$arr["baoliu"] = preg_replace('/#([^📊]+)📊/u', '', $str); // 去掉提取部分
$tmp = explode("#", $arr["tiqu"]);
$arr["name"] = $tmp[0] ?? '';
$arr["name"] = trim($arr["name"]);
}
return $arr;
}
private function getTaocan_ffq_tg($url)
{
$oResult = Flow::make("getTaocan_ffq_tg");
// 初始化 cURL
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/115.0 Safari/537.36',
]);
$html = curl_exec($ch);
curl_close($ch);
if ($html === false) {
return $oResult->step("html=false")->sData("⚠️ 无法读取网页内容")->done();
}
// 解析 HTML
libxml_use_internal_errors(true);
$dom = new \DOMDocument();
$dom->loadHTML($html);
libxml_clear_errors();
$xpath = new \DOMXPath($dom);
// 找到第一个 class 为 markdown-body 的 div
$markdownDiv = $xpath->query('//div[contains(@class, "markdown-body")]')->item(0);
if (!$markdownDiv) {
return $oResult->step("div loss")->sData("⚠️ 未找到 class 为 'markdown-body' 的 div")->done();
}
// 找到该 div 中的第一个 table
$table = null;
foreach ($markdownDiv->getElementsByTagName('table') as $t) {
$table = $t;
break;
}
if (!$table) {
return $oResult->step("table loss")->sData("⚠️ 未找到 table 标签")->done();
}
$sHtml = $dom->saveHTML($table);
return $oResult->sData($sHtml)->done();
}
function tg_replace($text, $repl, $offset, $length, $onlyReturn = false) {
// 1. 统一转为 UTF-16LE 字节流,对齐 TG 的索引逻辑
$u16 = mb_convert_encoding($text, 'UTF-16LE', 'UTF-8');
// 2. 锁定靶区TG offset * 2 字节)
if ($onlyReturn) {
// 模式 A侦察目标
$target = substr($u16, $offset * 2, $length * 2);
return mb_convert_encoding($target, 'UTF-8', 'UTF-16LE');
}
// 模式 B定点清除
$pre = substr($u16, 0, $offset * 2);
$suf = substr($u16, ($offset + $length) * 2);
$replU16 = mb_convert_encoding($repl, 'UTF-16LE', 'UTF-8');
return mb_convert_encoding($pre . $replU16 . $suf, 'UTF-8', 'UTF-16LE');
}
}