diff --git a/application/api/controller/Aiarticle.php b/application/api/controller/Aiarticle.php index daffd46..ed5d4e6 100644 --- a/application/api/controller/Aiarticle.php +++ b/application/api/controller/Aiarticle.php @@ -18,9 +18,6 @@ class Aiarticle extends Base { protected $aLogo = ['media_id' => 'Cn8zlXvVB5DwjcA9h40z9fprHDoc3Jqv97SwrInpmyYiilkeRdKvpD63cWqTYHfz','url' => 'http://mmbiz.qpic.cn/mmbiz_jpg/QHFVW13lONaQJxK9QbHU9CtrvTS2ModZnUyeAvuVN67t8XP85DxVJwDJf2YxCTalrsr17jS080xM6xQv5yGiaEQ/0?wx_fmt=jpeg'];//默认头像 - //数据表必填字段[ai_article] - protected $aAiFileds = ['article_id','title_english','title_chinese','journal_issn','covered','digest','research_result','content','highlights','discussion','prospect','research_background','discussion_results','research_method','overview','summary']; - //期刊接口地址 protected $sJournalUrl = 'http://journalapi.tmrjournals.com/public/index.php';//'http://zmzm.journal.dev.com'; // //期刊官网 @@ -53,59 +50,10 @@ class Aiarticle extends Base } /** - * @title 请求OPENAI + * @title 生成公微 * @param aSearch array 模版内容 * @param iTemplateId 模版ID */ - private function _createContentForOpenAI($aSearch = [],$sArticleType = 'default',$isTranslate = 2){ - - if(empty($aSearch) || empty($sArticleType)){ - return json_encode(array('status' => 2,'msg' => 'Please select a template or enter the content you want to consult')); - } - //组织参数 - if(in_array($sArticleType, ['Mini Review','Review'])){ - $sArticleType = 'Review'; - }else{ - $sArticleType = 'default'; - } - if($isTranslate == 1){ - $sArticleType = 1; - } - - //获取问答内容 - $oOpenAi = new OpenAi; - if($sArticleType == 'default'){ - $aMessage = $oOpenAi->buildDefaultPrompt($aSearch); - if(empty($aMessage)){ - return json_encode(['status' => 5,'msg' => 'AI Q&A content not obtained']); - } - $aParam = ['messages' => $aMessage,'model' => empty($aParam['api_model']) ? 'gpt-4.1' : $aParam['api_model'],'redis_id' => empty($aSearch['article_id']) ? 0 : $aSearch['article_id']]; - $aResult = json_decode($oOpenAi->createWechatContent($aParam),true); - } - if($sArticleType == 'Review'){ - $aMessage = $oOpenAi->buildReviewPrompt($aSearch); - if(empty($aMessage)){ - return json_encode(['status' => 5,'msg' => 'AI Q&A content not obtained']); - } - $aParam = ['messages' => $aMessage,'model' => empty($aParam['api_model']) ? 'gpt-4.1' : $aParam['api_model'],'redis_id' => empty($aSearch['article_id']) ? 0 : $aSearch['article_id']]; - $aResult = json_decode($oOpenAi->createWechatContent($aParam),true); - } - if($sArticleType == 1){ - $aMessage = $oOpenAi->buildTranslatePrompt($aSearch); - if(empty($aMessage)){ - return json_encode(['status' => 5,'msg' => 'AI Q&A content not obtained']); - } - $aParam = ['messages' => $aMessage,'model' => empty($aParam['api_model']) ? 'gpt-4.1' : $aParam['api_model']]; - $aResult = json_decode($oOpenAi->curlOpenAI($aParam),true); - } - - //请求OPENAI接口 - $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; - if($iStatus != 1){ - return json_encode($aResult); - } - return json_encode(array('status' => 1,'msg' => 'Content is being generated, please wait')); - } public function create($aParam = []){ //获取参数 @@ -157,8 +105,7 @@ class Aiarticle extends Base return json_encode(['status' => 5,'msg' => 'The data has been generated, please proceed with the next steps']); } //请求OPENAI - $aResult = json_decode($this->createForOpenAi($aArticleContent,$aAiArticle),true); - return json_encode($aResult); + return $this->createForOpenAi($aArticleContent,$aAiArticle); } @@ -198,12 +145,17 @@ class Aiarticle extends Base $aSearch['{#title_chinese#}'] = $this->basic_html_filter($aArticle['title']); $aSearch['{#journal_content#}'] = $this->basic_html_filter($aJournal['journal_content'] ?? ''); $aSearch['{#journal_content#}'] = trim(trim($aSearch['{#journal_content#}'],'.'),'。'); - - //写入Redis 标识已经开始请求OPENAI处理 - $sArticleType = empty($aArticle['type']) ? 'default' : $aArticle['type']; $aSearch['article_id'] = $iArticleId; - $aResult = $this->_createContentForOpenAI($aSearch,$sArticleType); - return $aResult; + $aSearch['prompt_article_type'] = empty($aArticle['type']) ? 'default' : $aArticle['type']; + //获取问答内容 + $oOpenAi = new OpenAi; + $aMessage = $oOpenAi->buildWechatPrompt($aSearch); + if(empty($aMessage)){ + return json_encode(['status' => 5,'msg' => 'AI Q&A content not obtained']); + } + //请求OPENAI接口 + $aParam = ['messages' => $aMessage,'model' => empty($aParam['api_model']) ? 'gpt-4.1' : $aParam['api_model'],'redis_id' => empty($aSearch['article_id']) ? 0 : $aSearch['article_id']]; + return $oOpenAi->createWechatContent($aParam); } /** @@ -277,7 +229,7 @@ class Aiarticle extends Base //是否查询作者 1是2否 $iSelectAuthor = empty($aParam['is_select_author']) ? 2 : $aParam['is_select_author']; //查询AI生成的文章内容 - $aWhere = ['is_delete' => 2]; + $aWhere = ['is_delete' => 2,'is_generate' => 1]; if(!empty($iArticleId)){ $aWhere['article_id'] = $iArticleId; } @@ -390,118 +342,74 @@ class Aiarticle extends Base //获取参数 $aParam = empty($aParam) ? $this->request->post() : $aParam; - //主键ID - $iAiArticleId = empty($aParam['ai_article_id']) ? 0 : $aParam['ai_article_id']; - - //文章ID - $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; - - //查询内容是否存在 - $aWhere = ['is_delete' => 2]; - if(empty($iArticleId) && empty($iArticleId)){ - return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']); + //更新AI文章内容 + $oArticle = new \app\common\Article; + $aResult = json_decode($oArticle->updateAiArticle($aParam),true); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + if($iStatus != 1){ + return json_encode($aResult); } - if(!empty($iArticleId)){ - $aWhere['article_id'] = $iArticleId; - } - if(!empty($iAiArticleId)){ - $aWhere['ai_article_id'] = $iAiArticleId; - } - $aAiArticle = Db::name('ai_article')->where($aWhere)->find(); - if(empty($aAiArticle)){ - return json_encode(['status' => 3,'msg' => 'he article content of WeChat official account has not been generated']); - } - - - //必填参数验证 - $aFields = $this->aAiFileds; - $sFiled = ''; - $aUpdateParam = []; - foreach($aFields as $val){ - if(!isset($aParam[$val])){ - continue; - } - if(is_array($aParam[$val])){ - $aParam[$val] = implode(";",$aParam[$val]); - } - $aUpdateParam[$val] = empty($aParam[$val]) ? '' : addslashes(strip_tags($aParam[$val])); - } - if(empty($aUpdateParam)){ - return json_encode(['status' => 1,'msg' => 'No data currently being processed']); - } - - Db::startTrans(); - //执行入库 - $aUpdateParam['update_time'] = time(); - $aUpdateParam['is_generate'] = 1; - $result = Db::name('ai_article')->where('ai_article_id',$iAiArticleId)->limit(1)->update($aUpdateParam); - if($result === false){ - $aResult = json_encode(['status' => 3,'msg' => 'UPDATEING AI article failed']); - } - $aResult['data'] = $aUpdateParam; //更新作者信息 $aAuthorList = empty($aParam['author_list']) ? []: $aParam['author_list']; $aAuthorList = is_array( $aAuthorList) ? $aAuthorList: json_decode($aAuthorList,true); - if(!empty($aAuthorList)){ - $aAuthorList = array_column($aAuthorList, null,'email'); - //根据邮箱查询作者信息 - $aEmail = array_keys($aAuthorList); + if(empty($aAuthorList)){ + return json_encode($aResult); + } - $aUserInfo = $aUserReviewer = []; - if(!empty($aEmail)){ - $aUserParam = ['email' => ['in',$aEmail]]; - $aUserInfo = Db::name('user')->field('email,user_id')->where($aUserParam)->select(); - if(!empty($aUserInfo)){ - $aUserId = array_column($aUserInfo, 'user_id'); - //查询用户附表 - $aWhere = ['reviewer_id' => ['in',$aUserId]]; - $aUserReviewer = Db::name('user_reviewer_info')->where($aWhere)->column('reviewer_id'); + //根据邮箱查询作者信息 + $aAuthorList = array_column($aAuthorList, null,'email'); + $aEmail = array_keys($aAuthorList); + if(empty($aEmail)){ + return json_encode($aResult); + } + $aUserParam = ['email' => ['in',$aEmail]]; + $aUserInfo = Db::name('user')->field('email,user_id')->where($aUserParam)->select(); + if(empty($aUserInfo)){ + return json_encode($aResult); + } + + //查询用户附表 + $aUserId = array_column($aUserInfo, 'user_id'); + $aWhere = ['reviewer_id' => ['in',$aUserId]]; + $aUserReviewer = Db::name('user_reviewer_info')->where($aWhere)->column('reviewer_id'); + //更新用户信息 + Db::startTrans(); + foreach ($aUserInfo as $key => $value) { + + $aUser = empty($aAuthorList[$value['email']]) ? [] : $aAuthorList[$value['email']]; + if(empty($aUser)){ + continue; + } + //更新作者名字 + if(isset($aUser['author_name'])){ + $aUpdate = ['localname' => $aUser['author_name']]; + Db::name('user')->where('user_id',$value['user_id'])->update($aUpdate); + } + //更新作者简介和单位 + $aUpdateReviewer = []; + if(isset($aUser['technical'])){ + $aUpdateReviewer['technical'] = $aUser['technical']; + } + if(isset($aUser['introduction'])){ + $aUpdateReviewer['introduction'] = $aUser['introduction']; + } + if(isset($aUser['company'])){ + $aUpdateReviewer['company'] = $aUser['company']; + } + if(isset($aUpdateReviewer)){ + if(in_array($value['user_id'], $aUserReviewer)){//更新 + Db::name('user_reviewer_info')->where('reviewer_id',$value['user_id'])->limit(1)->update($aUpdateReviewer); + } + if(!in_array($value['user_id'], $aUserReviewer)){//插入 + $aUpdateReviewer['reviewer_id'] = $value['user_id']; + Db::name('user_reviewer_info')->insert($aUpdateReviewer); } } - - //更新用户信息 - if(!empty($aUserInfo)){ - foreach ($aUserInfo as $key => $value) { - - $aUser = empty($aAuthorList[$value['email']]) ? [] : $aAuthorList[$value['email']]; - if(empty($aUser)){ - continue; - } - //更新作者名字 - if(isset($aUser['author_name'])){ - $aUpdate = ['localname' => $aUser['author_name']]; - Db::name('user')->where('user_id',$value['user_id'])->update($aUpdate); - } - //更新作者简介和单位 - $aUpdateReviewer = []; - if(isset($aUser['technical'])){ - $aUpdateReviewer['technical'] = $aUser['technical']; - } - if(isset($aUser['introduction'])){ - $aUpdateReviewer['introduction'] = $aUser['introduction']; - } - if(isset($aUser['company'])){ - $aUpdateReviewer['company'] = $aUser['company']; - } - if(isset($aUpdateReviewer)){ - - if(in_array($value['user_id'], $aUserReviewer)){//更新 - Db::name('user_reviewer_info')->where('reviewer_id',$value['user_id'])->limit(1)->update($aUpdateReviewer); - } - - if(!in_array($value['user_id'], $aUserReviewer)){//插入 - $aUpdateReviewer['reviewer_id'] = $value['user_id']; - Db::name('user_reviewer_info')->insert($aUpdateReviewer); - } - } - } - } - } Db::commit(); - $aResult['data'] = $aUpdateParam; + $aResult['data'] = $aParam; return json_encode($aResult); } @@ -695,11 +603,11 @@ class Aiarticle extends Base //处理通讯作者信息数据 $aAuthorInfo = json_decode($this->dealTemplateAuthor($aAuthor,$sTemplatePath),true); + $aAuthorInfo = empty($aAuthorInfo['data']) ? [] : $aAuthorInfo['data']; $aSearch['{###author_summary###}'] = empty($aAuthorInfo['author_info']) ? '' : $aAuthorInfo['author_info']; $aSearch['{###author_string###}'] = empty($aAuthorInfo['author_string']) ? '' : ',通讯作者为'.$aAuthorInfo['author_string']; - //处理往期推荐数据 $aPreviousRecommend = json_decode($this->dealTemplatePreviousRecommend($aArticle,$sTemplatePath),true); $aSearch['{###previous_recommend_summary###}'] = empty($aPreviousRecommend['data']) ? '' : $aPreviousRecommend['data']; @@ -741,12 +649,6 @@ class Aiarticle extends Base } $aSearch['{###jabbr###}'] = trim(trim($aSearch['{###jabbr###}'],'.'),'。'); - // $aSearch['{###free_year###}'] = date('Y'); - // $aSearch['{###covered###}'] = '内容涵盖'.str_replace('研究涵盖', '', $aSearch['{###covered###}']); - if(!empty($aSearch['{###covered###}']) && strpos($aSearch['{###covered###}'], '涵盖') === false){ - $aSearch['{###covered###}'] = '内容涵盖'.str_replace('研究涵盖', '', $aSearch['{###covered###}']); - } - //期刊封面 $aSearch['{###journal_icon_info###}'] = $aSearch['{###journal_icon_details###}'] = ''; if(!empty($sJournalIcon)){ @@ -761,7 +663,8 @@ class Aiarticle extends Base $aSearch['{###journal_icon_details###}'] = str_replace('{###journal_icon###}', $aAiArticle['journal_icon']??'', $sTemplateJournalInfo); } } - + $aSearch['{###covered###}'] = empty($aSearch['{###covered###}']) ? '' : '内容涵盖'.$aSearch['{###covered###}']; + $sTemplate = str_replace(array_keys($aSearch), array_values($aSearch), $sTemplate); // $this->writeFile($sTemplate,ROOT_PATH.'public/template_info.html'); return json_encode(['status' => 1, 'msg' => '模版生成成功','data' => ['template' => $sTemplate]]); @@ -788,58 +691,70 @@ class Aiarticle extends Base } $sAuthorTemplate = file_get_contents($sAuthorTemplatePathAuthor); - //处理模版变量 - $aAuthorSerachInfo = []; - $sAuthorInfo = ''; + //默认头像 $aLogo = $this->aLogo; - - //请求AI翻译 - $sContent = ''; - $aSearch['{#content#}'] = json_encode($aAuthor); - $aResult = json_decode($this->_createContentForOpenAI($aSearch,1,1),true); - $aContent = empty($aResult['data']) ? [] : array_column($aResult['data'], null,'email'); + //数据处理 + $aTranslate = $aAuthorSerachInfo = $aTranslateAuthorList = []; + //通讯作者信息 + $sAuthorInfo = ''; foreach ($aAuthor as $key => $value) { - - //请求AI翻译 - $aInfo = empty($aContent[$value['email']]) ? [] : $aContent[$value['email']]; - - //替换模版 - $aAuthorSerach = []; //所属单位处理 - $value['company'] = empty($aInfo['company']) ? $value['company'] : $aInfo['company']; if(!empty($value['company'])){ $value['company'] =str_replace(',', ',', $value['company']); $aCompany = explode(',', $value['company']); } $value['company'] = empty($aCompany[0]) ? $value['company'] : trim($aCompany[0],','); - //简介处理 - $value['introduction'] = empty($aInfo['introduction']) ? $value['introduction'] : $aInfo['introduction']; - //职称处理 - $value['technical'] = empty($aInfo['technical']) ? $value['technical'] : $aInfo['technical']; + $sAuthorInfo .= $value['company'].$value['author_name'].$value['technical'].','; if(empty($value['introduction'])){//简介为空不处理 - $sTechnical = empty($value['technical']) ? '老师' : $value['technical']; - $sAuthorInfo .= $value['company'].$value['author_name'].$sTechnical.','; continue; } - - //缓缓处理进行模版替换 - foreach ($value as $k => $val) { - if($k == 'icon'){//头像拼接处理 - $sIconInfo = empty($val) ? $aLogo['url'] : $val; - $aParsedUrl = parse_url($sIconInfo); - if(empty($aParsedUrl['scheme'])){ - $aAuthorSerach['{###icon_path###}'] = trim($this->sSubmissionUrl,'/').$this->sUserIcon.$val; - }else{ - $aAuthorSerach['{###icon_path###}'] = $sIconInfo; - } - $aAuthorSerach['{###icon_path###}'] = trim($aAuthorSerach['{###icon_path###}'],'/'); - }else{ - $aAuthorSerach['{###'.$k.'###}'] = trim($val,'/'); - } - } - $aAuthorSerachInfo[] = str_replace(array_keys($aAuthorSerach), array_values($aAuthorSerach), $sAuthorTemplate); + $aTranslateAuthorList[$value['email']] = [ + 'author_name' => empty($value['author_name']) ? '' : $value['author_name'], + 'technical' => empty($value['technical']) ? '' : $value['technical'], + 'introduction' => empty($value['introduction']) ? '' : $value['introduction'], + 'company' => empty($value['company']) ? '' : $value['company'] + ]; + $aAuthorSerachInfo[] = $value; + } + //请求AI翻译 + if(!empty($sAuthorInfo)){ + $aTranslate = array_merge(['author_info' => trim($sAuthorInfo,',')],$aTranslate); + } + if(!empty($aTranslateAuthorList)){ + $aTranslate = array_merge($aTranslateAuthorList,$aTranslate); + } + if(!empty($aTranslate)){ + $aSearch['{#content#}'] = json_encode($aTranslate); + $oOpenAi = new OpenAi; + $aTranslate = $oOpenAi->buildTranslatePrompt($aSearch); + $aTranslate = empty($aTranslate['data']) ? [] : $aTranslate['data']; + } + //翻译内容处理 + if(!empty($aAuthorSerachInfo)){ + foreach ($aAuthorSerachInfo as $key => $value) { + $aInfo = empty($aTranslate[$value['email']]) ? [] : $aTranslate[$value['email']]; + $value['author_name'] = empty($aInfo['author_name']) ? $value['author_name'] : $aInfo['author_name']; + $value['technical'] = empty($aInfo['technical']) ? $value['technical'] : $aInfo['technical']; + $value['introduction'] = empty($aInfo['introduction']) ? $value['introduction'] : $aInfo['introduction']; + $value['company'] = empty($aInfo['company']) ? $value['company'] : $aInfo['company']; + $aAuthorSerach = []; + foreach ($value as $k => $val) { + if($k == 'icon'){//头像拼接处理 + $sIconInfo = empty($val) ? $aLogo['url'] : $val; + $aParsedUrl = parse_url($sIconInfo); + if(empty($aParsedUrl['scheme'])){ + $aAuthorSerach['{###icon_path###}'] = trim($this->sSubmissionUrl,'/').$this->sUserIcon.$val; + }else{ + $aAuthorSerach['{###icon_path###}'] = $sIconInfo; + } + $aAuthorSerach['{###icon_path###}'] = trim($aAuthorSerach['{###icon_path###}'],'/'); + }else{ + $aAuthorSerach['{###'.$k.'###}'] = trim($val,'/'); + } + } + $aAuthorSerachInfo[$key] = str_replace(array_keys($aAuthorSerach), array_values($aAuthorSerach), $sAuthorTemplate); + } } - //通讯作者列表 //获取通讯作者汇总模版 $sAuthorTemplatePath = $sTemplatePath.'/author_summary.html'; @@ -855,7 +770,7 @@ class Aiarticle extends Base $sAuthorTemplate = empty($sInfo) ? '' : str_replace('{###author_info###}', $sInfo, $sAuthorTemplate); //通讯作者字符串处理 - $sAuthorInfo = trim($sAuthorInfo,','); + $sAuthorInfo = empty($aTranslate['author_info']) ? '' : $aTranslate['author_info']; return json_encode(['status' => 1,'msg' => 'success','data' => ['author_info' => $sAuthorTemplate,'author_string' => trim($sAuthorInfo,';')]]); } diff --git a/application/api/controller/Aireview.php b/application/api/controller/Aireview.php index d1fd59f..1fb8a4d 100644 --- a/application/api/controller/Aireview.php +++ b/application/api/controller/Aireview.php @@ -5,6 +5,7 @@ namespace app\api\controller; use app\api\controller\Base; use think\Db; use app\common\OpenAi; +use app\common\Article; /** * @title AI审核文章 * @description 对接OPENAI接口 @@ -53,7 +54,7 @@ class Aireview extends Base } //实例化公共方法 - $oOpenAi = new OpenAi; + $oArticle = new Article; if($aArticle['state'] > 4 ){ //查询文章内容 $aWhere['type'] = 0; @@ -70,7 +71,7 @@ class Aireview extends Base } }else{ - $aFile = json_decode($oOpenAi->getFileContent(['article_id' => $iArticleId]),true); + $aFile = json_decode($oArticle->getFileContent(['article_id' => $iArticleId]),true); $aFile = empty($aFile['data']) ? [] : $aFile['data']; $aArticleMain = empty($aFile['mains']) ? [] : $aFile['mains']; } @@ -92,7 +93,7 @@ class Aireview extends Base $aSearch['{journal_name}'] = '传统医学研究'; } //查询期刊内容 - $aJournalPaperArt = json_decode($oOpenAi->getJournalPaperArt($aJournal),true); + $aJournalPaperArt = json_decode($oArticle->getJournalPaperArt($aJournal),true); $sJournalContent = empty($aJournalPaperArt['data']) ? '' : implode('', $aJournalPaperArt['data']); $sJournalContent = empty($sJournalContent) ? $aJournal['scope'] : $sJournalContent; $aSearch['{scope}'] = $sJournalContent;//期刊范围 @@ -222,12 +223,14 @@ class Aireview extends Base } //查询文章 - $aArticle = Db::table('t_article')->field('article_id,abstrart,keywords,journal_id,title,state')->where('article_id',$aParam['article_id'])->find(); + $aArticle = Db::name('article')->field('article_id,abstrart,keywords,journal_id,title,state')->where('article_id',$aParam['article_id'])->find(); if(empty($aArticle)){ return json_encode(array('status' => 3,'msg' => 'No articles requiring review were found' )); } //查询文章审核内容 - $aAiReview = Db::table('t_article_ai_review')->where('article_id',$aParam['article_id'])->find(); - return json_encode(array('status' => 1,'msg' => 'Successfully obtained article review content','data' => $aAiReview,'article_data' => $aArticle)); + $oAireview = new \app\common\Aireview; + $aAireview = $oAireview->get($aParam); + $aAireview = empty($aAireview['data']) ? [] : $aAireview['data']; + return json_encode(array('status' => 1,'msg' => 'Successfully obtained article review content','data' => $aAireview,'article_data' => $aArticle)); } } diff --git a/application/api/job/ArticleAiCreateContent.php b/application/api/job/ArticleAiCreateContent.php index 6a59dfa..cccd965 100644 --- a/application/api/job/ArticleAiCreateContent.php +++ b/application/api/job/ArticleAiCreateContent.php @@ -1,163 +1,125 @@ queueJob = new QueueJob; - // 初始化日志路径 $this->logPath = ROOT_PATH . 'public/queue_log/ArticleAiCreateContent_' . date('Ymd') . '.log'; - } - - /** - * 安全写入日志(带文件锁) - */ - private function log($message) - { - $time = date('H:i:s'); - $logMsg = "[$time] $message\n"; - $fp = fopen($this->logPath, 'w'); - if ($fp) { - flock($fp, LOCK_EX); // 排他锁防止并发写入冲突 - fwrite($fp, $logMsg); - flock($fp, LOCK_UN); - fclose($fp); - } - } - - // 任务日志添加 - public function addLog($aParam = []) - { - //实例化 - return $this->queueJob->addLog($aParam); - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - return $this->queueJob->updateLog($aParam); - } - - /** - * 根据错误信息获取重试延迟 - */ - private function getRetryDelay($errorMsg) - { - $delayMap = [ - 'MySQL server has gone away' => 60, - 'timeout' => 30, - 'OpenAI' => 45, - 'network' => 60 - ]; - foreach ($delayMap as $keyword => $delay) { - if (strpos($errorMsg, $keyword) !== false) { - return $delay; - } - } - return 10; // 默认延迟 + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } public function fire(Job $job, $data) { - //日志 - $this->log("-----------队列任务开始-----------"); + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); + + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); + return; + } // 获取文章ID $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; if (empty($iArticleId)) { - $this->log("无效的article_id,删除任务"); + $this->oQueueJob->log("无效的article_id,删除任务"); $job->delete(); return; } - // 生成唯一任务标识 $sClassName = get_class($this); $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; - $sRedisValue = uniqid() . '_' . getmypid(); // 增加进程ID确保唯一性 + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; - // 尝试获取Redis锁(原子操作) - $isLocked = $this->queueJob->setRedisLock($sRedisKey, $sRedisValue, 86400); + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); if (!$isLocked) { - $currentValue = $this->queueJob->getRedisValue($sRedisKey); - $this->log("任务已被锁定,避免重复执行 | 锁键: {$sRedisKey} | 锁值: {$currentValue}"); - // 检查任务是否已超过最大重试次数 - if ($job->attempts() >= 2) { - $this->log("任务超过最大重试次数,停止重试"); + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); $job->delete(); } else { - $delay = $this->getRetryDelay("任务已锁定"); - $this->log("{$delay}秒后重试任务 | 重试次数: {$job->attempts()}"); - $job->release($delay); + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } } + $this->oQueueJob->flushLog(); return; } - // 任务基础信息 $aParam = [ 'job_id' => $sRedisKey, 'job_class' => $sClassName, - 'status' => 0, // 0:处理中 + 'status' => 0, 'create_time' => time(), - 'params' => json_encode($data, JSON_UNESCAPED_UNICODE) + 'params' => json_encode($data, self::JSON_OPTIONS) ]; - // 创建任务日志记录 - $iLogId = $this->addLog($aParam); - if(!$iLogId) { - $this->log("日志创建失败,释放锁并删除任务:".json_encode($data)); - $this->queueJob->releaseRedisLock($sRedisKey, $sRedisValue); + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); $job->delete(); + $this->oQueueJob->flushLog(); return; } + try { - // 执行核心任务 + //生成内容 $oAiarticle = new Aiarticle; $aResult = json_decode($oAiarticle->create($data),true); - $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; $sMsg = empty($aResult['msg']) ? '内容生成失败' : $aResult['msg']; - //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $this->updateLog($aParam); - //删除任务 - $job->delete(); - $this->log("任务执行成功,已删除任务 | 日志ID: {$iLogId}"); - } catch (\Exception $e) { - - //错误信息 - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $sTrace = empty($e->getTraceAsString()) ? '' : $e->getTraceAsString(); - $this->log("任务执行异常: {$sMsg} | 堆栈: {$sTrace}"); - // 记录失败日志 - $this->updateLog([ + $this->oQueueJob->updateLog([ 'log_id' => $iLogId, - 'status' => 2, + 'status' => 1, 'update_time' => time(), - 'error' => $sMsg.':'.$sTrace, + 'error' => $sMsg ]); - // 重试策略 - $attempts = $job->attempts(); - if ($attempts >= 2) { - $this->log("任务已重试{$attempts}次,停止重试"); - $job->delete(); - } else { - $delay = $this->getRetryDelay($sMsg); - $this->log("{$delay}秒后重试任务 | 重试次数: {$attempts}"); - $job->release($delay); - } + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); + + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\Exception $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); } finally { - // 无论成功失败都释放锁(确保锁值匹配) - $releaseResult = $this->queueJob->releaseRedisLock($sRedisKey, $sRedisValue); - if (!$releaseResult) { - $this->log("释放锁失败 | 锁键: {$sRedisKey} | 锁值: {$sRedisValue}"); - } else { - $this->log("成功释放锁 | 锁键: {$sRedisKey}"); - } + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); gc_collect_cycles(); } } diff --git a/application/api/job/RecommendArticleField.php b/application/api/job/RecommendArticleField.php new file mode 100644 index 0000000..66285a3 --- /dev/null +++ b/application/api/job/RecommendArticleField.php @@ -0,0 +1,124 @@ +logPath = ROOT_PATH . 'public/queue_log/ArticleFiled_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); + } + + public function fire(Job $job, $data) + { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); + + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); + return; + } + + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); + return; + } + + $aParam = [ + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, + 'create_time' => time(), + 'params' => json_encode($data, self::JSON_OPTIONS) + ]; + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + + try { + $oArticle = new Article; + $aResult = json_decode($oArticle->getAiField($data), true); + $sMsg = empty($aResult['msg']) ? '内容生成成功' : $aResult['msg']; + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId}|执行日志:{$sMsg}"); + + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\Exception $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); + } + } +} \ No newline at end of file diff --git a/application/api/job/RecommendReviewer.php b/application/api/job/RecommendReviewer.php index 0aeab06..4986609 100644 --- a/application/api/job/RecommendReviewer.php +++ b/application/api/job/RecommendReviewer.php @@ -1,76 +1,106 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/RecommendReviewer_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 推荐审稿人任务 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/RecommendReviewer_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); - - //获取文章ID - $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ - $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); return; } - //任务数组 - $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, - 'create_time' => time(), - 'params' => json_encode($data, JSON_UNESCAPED_UNICODE) - ]; - //执行任务 - try { - //添加任务日志 - $sMsg = '推荐审稿人任务处理成功'; - $iLogId = $this->addLog($aParam); + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); + return; + } + + $aParam = [ + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, + 'create_time' => time(), + 'params' => json_encode($data, self::JSON_OPTIONS) + ]; + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + + try { + //获取推荐审稿人信息 $aParam = ['article_id' => $iArticleId,'page' => 1,'size' => empty($data['size']) ? 5 : $data['size']]; $oReviewer = new Reviewer; $aResult = json_decode($oReviewer->recommend($aParam),true); $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; $sMsg = empty($aResult['msg']) ? '' : $aResult['msg']; - //处理数据 $iCount = empty($aResult['data']['total']) ? 0 : $aResult['data']['total'];//数量 $iSize = empty($aResult['data']['size']) ? 0 : $aResult['data']['size'];//推荐数量 @@ -100,33 +130,29 @@ class RecommendReviewer $sMsg .= empty($aResult['msg']) ? 'Reviewer data insertion failed' : $aResult['msg']; } } - - //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); - - //删除任务 + //更新日志 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } - } \ No newline at end of file diff --git a/application/api/job/RelatedArticle.php b/application/api/job/RelatedArticle.php index faf5ad6..62d0491 100644 --- a/application/api/job/RelatedArticle.php +++ b/application/api/job/RelatedArticle.php @@ -1,104 +1,129 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/RelatedArticle_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 相关文章发送邮件任务 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/RelatedArticle_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); - - //文章ID - $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; - - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ - $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); return; } - //任务数组 - $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, - 'create_time' => time(), - 'params' => empty($data) ? '暂无参数' : json_encode($data, JSON_UNESCAPED_UNICODE) - ]; - //执行任务 - try { - //添加任务日志 - $sMsg = '关联文章任务处理成功'; - $iLogId = $this->addLog($aParam); + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); + return; + } + + $aParam = [ + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, + 'create_time' => time(), + 'params' => json_encode($data, self::JSON_OPTIONS) + ]; + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + + try { + //查询文章所关联的文章 $oJournalArticle = new JournalArticle; $aResult = json_decode(JournalArticle::get($data),true); $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; $sMsg = empty($aResult['msg']) ? '获取相关文章信息失败' : $aResult['msg']; - //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); - - //删除任务 + //更新日志 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); - + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } - } \ No newline at end of file diff --git a/application/api/job/ReviewerScore.php b/application/api/job/ReviewerScore.php index bb106c5..90ed628 100644 --- a/application/api/job/ReviewerScore.php +++ b/application/api/job/ReviewerScore.php @@ -1,103 +1,133 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/ReviewerScore_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 审稿人评分 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/ReviewerScore_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); - - //参数 - $iArticleId = empty($data['article_id']) ? 0 : $data['article_id'];//文章ID - $iReviewerId = empty($data['reviewer_id']) ? 0 : $data['reviewer_id'];//审稿人ID - $iArtRevId = empty($data['art_rev_id']) ? 0 : $data['art_rev_id'];//主键ID - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId.'/'.$iReviewerId.'/'.$iArtRevId; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ - $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); return; } - //任务数组 - $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, - 'create_time' => time(), - 'params' => json_encode($data, JSON_UNESCAPED_UNICODE) - ]; - //执行任务 - try { - //添加任务日志 - $sMsg = '审稿人评分任务处理成功'; - $iLogId = $this->addLog($aParam); + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + //审稿人ID + $iReviewerId = empty($data['reviewer_id']) ? 0 : $data['reviewer_id']; + //主键ID + $iArtRevId = empty($data['art_rev_id']) ? 0 : $data['art_rev_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + // 生成唯一任务标识 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}:{$iReviewerId}:{$iArtRevId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); + return; + } + + $aParam = [ + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, + 'create_time' => time(), + 'params' => json_encode($data, self::JSON_OPTIONS) + ]; + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + + try { + + // 执行核心任务 $aParam = ['article_id' => $iArticleId,'reviewer_id' => $iReviewerId,'art_rev_id' => $iArtRevId]; $oReviewer = new Reviewer; $aResult = json_decode($oReviewer->score($aParam),true); $sMsg = empty($aResult['msg']) ? '给审稿人评分处理失败' : $aResult['msg']; - - //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); - - //删除任务 - $job->delete(); - - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); - - } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + //更新日志 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); + + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\Exception $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } - } \ No newline at end of file diff --git a/application/api/job/RevisionReviewer.php b/application/api/job/RevisionReviewer.php index e1deb52..5f96bd5 100644 --- a/application/api/job/RevisionReviewer.php +++ b/application/api/job/RevisionReviewer.php @@ -1,100 +1,129 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/RevisionReviewer_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 文章退修任务 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/RevisionReviewer_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); - - //参数 - $iArticleId = empty($data['article_id']) ? 0 : $data['article_id'];//文章ID - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ - $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); return; } - //任务数组 - $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, - 'create_time' => time(), - 'params' => json_encode($data, JSON_UNESCAPED_UNICODE) - ]; - //执行任务 - try { - //添加任务日志 - $sMsg = '文章退修任务处理成功'; - $iLogId = $this->addLog($aParam); + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + // 生成唯一任务标识 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); + return; + } + + $aParam = [ + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, + 'create_time' => time(), + 'params' => json_encode($data, self::JSON_OPTIONS) + ]; + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + + try { + //获取符合条件的文章审稿人信息 $aParam = ['article_id' => $iArticleId]; $oReviewer = new Reviewer; $aResult = json_decode($oReviewer->revisionForReviewer($aParam),true); $sMsg = empty($aResult['msg']) ? '审稿人同意审稿但超时未审的数据失败' : $aResult['msg']; - - //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); - - //删除任务 + + //更新日志 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); - + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } } \ No newline at end of file diff --git a/application/api/job/SendRelatedArticleEmail.php b/application/api/job/SendRelatedArticleEmail.php index 7f7103c..353178c 100644 --- a/application/api/job/SendRelatedArticleEmail.php +++ b/application/api/job/SendRelatedArticleEmail.php @@ -1,41 +1,47 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/SendRelatedArticleEmail_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 关联文章任务 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/SendRelatedArticleEmail_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); + return; + } - //文章ID + // 获取文章ID $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; //作者邮箱 $email = empty($data['email']) ? '' : $data['email']; @@ -57,35 +63,58 @@ class SendRelatedArticleEmail $journal_id = empty($data['journal_id']) ? '' : $data['journal_id']; //期刊issn $journal_issn = empty($data['journal_issn']) ? '' : $data['journal_issn']; - - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId.'/'.$related_article_id.'/'.$article_author_id.'/'.$email; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ + if (empty($iArticleId) || empty($email)) { + $this->oQueueJob->log("无效的article_id,删除任务"); $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); + return; + } + // 生成唯一任务标识 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}:{$related_article_id}:{$article_author_id}:{$email}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); return; } - //任务数组 $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, 'create_time' => time(), - 'params' => empty($data) ? '暂无参数' : json_encode($data, JSON_UNESCAPED_UNICODE) + 'params' => json_encode($data, self::JSON_OPTIONS) ]; - //执行任务 - try { - //添加任务日志 - $sMsg = '关联文章任务处理成功'; - $iLogId = $this->addLog($aParam); + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + try { + //查询是否发送过邮件 $oJournalArticle = new JournalArticle; $aLog = json_decode($oJournalArticle::getLog(['article_id' => $iArticleId,'article_author_id' => $article_author_id,'related_article_id' => $related_article_id,'is_success' => 1]),true); @@ -105,34 +134,30 @@ class SendRelatedArticleEmail //添加邮件发送日志 $iId = JournalArticle::addLog($aEmailLog); } - - //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); - - //删除任务 + + //更新日志 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); - + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } - } \ No newline at end of file diff --git a/application/api/job/SendReviewEmail.php b/application/api/job/SendReviewEmail.php index b8958c8..a140f98 100644 --- a/application/api/job/SendReviewEmail.php +++ b/application/api/job/SendReviewEmail.php @@ -1,40 +1,48 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/SendReviewEmail_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 发送审稿邀请邮件任务 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/SendReviewEmail_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); + return; + } - //文章ID + // 获取文章ID + // 获取文章ID $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; //作者邮箱 $email = empty($data['email']) ? '' : $data['email']; @@ -54,35 +62,59 @@ class SendReviewEmail $reviewer_id = empty($data['reviewer_id']) ? 0 : $data['reviewer_id']; //邮件类型 $type = empty($data['type']) ? 1 : $data['type']; - - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId.'/'.$art_rev_id.'/'.$reviewer_id.'/'.$email; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); return; } - //任务数组 + // 生成唯一任务标识 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}:{$art_rev_id}:{$reviewer_id}:{$email}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); + return; + } + $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, 'create_time' => time(), - 'params' => empty($data) ? '暂无参数' : json_encode($data, JSON_UNESCAPED_UNICODE) + 'params' => json_encode($data, self::JSON_OPTIONS) ]; - //执行任务 + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + try { - //添加任务日志 - $sMsg = '发送审稿邀请邮件任务处理成功'; - $iLogId = $this->addLog($aParam); - //发送邮件 + //查询是否发送过邮件 $oReviewer = new Reviewer; if($type != 3){ @@ -104,34 +136,29 @@ class SendReviewEmail //添加邮件发送日志 $iId = $oReviewer->addLog($aEmailLog); } + //更新日志 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); - //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); - - //删除任务 + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); - + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } - } \ No newline at end of file diff --git a/application/api/job/WechatDraft.php b/application/api/job/WechatDraft.php index ca6f737..08d70ad 100644 --- a/application/api/job/WechatDraft.php +++ b/application/api/job/WechatDraft.php @@ -1,102 +1,128 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/WechatDraft_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 上传文章到草稿箱任务入口 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/WechatDraft_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); - - //文章ID - $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; - - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ - $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); + return; + } + + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); return; } - //任务数组 $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, 'create_time' => time(), - 'params' => empty($data) ? '暂无参数' : json_encode($data, JSON_UNESCAPED_UNICODE) + 'params' => json_encode($data, self::JSON_OPTIONS) ]; - //执行任务 + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + try { - //添加任务日志 - $sMsg = '上传草稿箱处理成功'; - $iLogId = $this->addLog($aParam); //上传草稿箱 $oAiarticle = new Aiarticle; $aResult = json_decode($oAiarticle->syncWechat($data),true); - $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; $sMsg = empty($aResult['msg']) ? '上传草稿箱失败' : $aResult['msg']; - - //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); + + //更新日志 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); - //删除任务 + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); - + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } - } \ No newline at end of file diff --git a/application/api/job/WechatDraftPublish.php b/application/api/job/WechatDraftPublish.php index b5da1a0..60b280e 100644 --- a/application/api/job/WechatDraftPublish.php +++ b/application/api/job/WechatDraftPublish.php @@ -1,104 +1,128 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/WechatDraftPublish_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 发布草稿箱任务入口 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/WechatDraftPublish_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); - - //文章ID - $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; - - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ - $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); + return; + } + + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); return; } - //任务数组 $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, 'create_time' => time(), - 'params' => empty($data) ? '暂无参数' : json_encode($data, JSON_UNESCAPED_UNICODE) + 'params' => json_encode($data, self::JSON_OPTIONS) ]; - //执行任务 + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + try { - //添加任务日志 - $sMsg = '草稿箱发布任务处理成功'; - $iLogId = $this->addLog($aParam); - //发布草稿箱 $oAiarticle = new Aiarticle; - $aResult = json_decode($oAiarticle->publishDraft($data),true); - $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + $aResult = json_decode($oAiarticle->publishDraft($data),true); $sMsg = empty($aResult['msg']) ? '草稿箱发布失败' : $aResult['msg']; - + //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); - - //删除任务 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); - + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } - } \ No newline at end of file diff --git a/application/api/job/WechatMaterial.php b/application/api/job/WechatMaterial.php index 8342174..caa098d 100644 --- a/application/api/job/WechatMaterial.php +++ b/application/api/job/WechatMaterial.php @@ -1,102 +1,128 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/WechatMaterial_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 上传素材任务入口 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/WechatMaterial_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); - - //文章ID - $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; - - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ - $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); + return; + } + + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); return; } - //任务数组 $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, 'create_time' => time(), - 'params' => empty($data) ? '暂无参数' : json_encode($data, JSON_UNESCAPED_UNICODE) + 'params' => json_encode($data, self::JSON_OPTIONS) ]; - //执行任务 + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + try { - //添加任务日志 - $sMsg = '上传素材任务处理成功'; - $iLogId = $this->addLog($aParam); //上传素材 $oAiarticle = new Aiarticle; $aResult = json_decode($oAiarticle->uploadMaterial($data),true); - $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; $sMsg = empty($aResult['msg']) ? '上传素材失败' : $aResult['msg']; //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); - - //删除任务 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); - + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } } \ No newline at end of file diff --git a/application/api/job/WechatQueryStatus.php b/application/api/job/WechatQueryStatus.php index ca41059..aec968f 100644 --- a/application/api/job/WechatQueryStatus.php +++ b/application/api/job/WechatQueryStatus.php @@ -1,103 +1,128 @@ addLog($aParam); - return $iLogId; - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - //实例化 - $oQueueJob = new QueueJob; - return $oQueueJob->updateLog($aParam); + $this->logPath = ROOT_PATH . 'public/queue_log/WechatQueryStatus_' . date('Ymd') . '.log'; + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } - // 草稿箱文章发布状态任务入口 public function fire(Job $job, $data) { + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - $sLogPath = ROOT_PATH.'public/queue_log/WechatQueryStatus_'.date('Ymd').'.log'; - $sTime = date('H:i:s'); - file_put_contents($sLogPath,'-----------Queue job started:'.$sTime.'-----------'); - - //文章ID - $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; - - //获取方法名 - $sClassName = get_class($this); - // 检查任务是否已处理(基于业务唯一标识) - $sRedisKey = $sClassName.'/'.$iArticleId; - $sRedisKey = md5($sRedisKey); - //判断Redis是否存在 - $oQueueJob = new QueueJob; - $result = $oQueueJob->setRedisLabel(['redis_key' => $sRedisKey]); - if($result != 1){ - $job->delete(); - file_put_contents($sLogPath,'-----------Queue job already:'.$result."===".$sRedisKey.'==='.$iArticleId."===".$sTime.'-----------'); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); + return; + } + + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); + if (!$isLocked) { + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } + } + $this->oQueueJob->flushLog(); return; } - //任务数组 $aParam = [ - 'job_id' => $sRedisKey, - 'job_class' => $sClassName, - 'status' => 0, + 'job_id' => $sRedisKey, + 'job_class' => $sClassName, + 'status' => 0, 'create_time' => time(), - 'params' => empty($data) ? '暂无参数' : json_encode($data, JSON_UNESCAPED_UNICODE) + 'params' => json_encode($data, self::JSON_OPTIONS) ]; - //执行任务 + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } + try { - //添加任务日志 - $sMsg = '查询草稿箱文章发布处理成功'; - $iLogId = $this->addLog($aParam); // 查询状态 $oAiarticle = new Aiarticle; $aResult = json_decode($oAiarticle->queryStatus($data),true); - $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; $sMsg = empty($aResult['msg']) ? '查询草稿箱文章是否发布失败' : $aResult['msg']; //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $oQueueJob->updateLog($aParam); - - //删除任务 + $this->oQueueJob->updateLog([ + 'log_id' => $iLogId, + 'status' => 1, + 'update_time' => time(), + 'error' => $sMsg + ]); + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); - file_put_contents($sLogPath,'-----------Queue job end:'.$sTime.'-----------'); - + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); } catch (\Exception $e) { - - // 2. 记录失败日志 - $aParam['status'] = 2; // 标记状态为"失败" - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $aParam['error'] = $sMsg; - $this->addLog($aParam); // 调用日志记录方法 - if ($job->attempts() > $this->tries) { - //如果任务尝试次数超过最大重试次数 - $job->delete(); // 删除任务,不再重试 - } else { - // 3. 如果尝试次数未超过最大重试次数,释放任务回队列 - $job->release(30); // 30秒后重新尝试执行任务 - } - file_put_contents($sLogPath,'-----------Queue job error:'.$sMsg.'-----------'.$sTime); - }finally { - gc_collect_cycles(); // 强制垃圾回收 + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } finally { + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); + gc_collect_cycles(); } } - } \ No newline at end of file diff --git a/application/api/job/createFieldForQueue.php b/application/api/job/createFieldForQueue.php index 734ff92..50fa7a2 100644 --- a/application/api/job/createFieldForQueue.php +++ b/application/api/job/createFieldForQueue.php @@ -1,162 +1,126 @@ queueJob = new QueueJob; - // 初始化日志路径 $this->logPath = ROOT_PATH . 'public/queue_log/createFieldForQueue_' . date('Ymd') . '.log'; - } - - /** - * 安全写入日志(带文件锁) - */ - private function log($message) - { - $time = date('H:i:s'); - $logMsg = "[$time] $message\n"; - $fp = fopen($this->logPath, 'w'); - if ($fp) { - flock($fp, LOCK_EX); // 排他锁防止并发写入冲突 - fwrite($fp, $logMsg); - flock($fp, LOCK_UN); - fclose($fp); - } - } - - // 任务日志添加 - public function addLog($aParam = []) - { - //实例化 - return $this->queueJob->addLog($aParam); - } - - // 任务日志修改 - public function updateLog($aParam = []) - { - return $this->queueJob->updateLog($aParam); - } - - /** - * 根据错误信息获取重试延迟 - */ - private function getRetryDelay($errorMsg) - { - $delayMap = [ - 'MySQL server has gone away' => 60, - 'timeout' => 30, - 'OpenAI' => 45, - 'network' => 60 - ]; - foreach ($delayMap as $keyword => $delay) { - if (strpos($errorMsg, $keyword) !== false) { - return $delay; - } - } - return 10; // 默认延迟 + $this->oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); + // 确保日志目录存在 + $this->oQueueJob->ensureLogDirExists($this->logPath); } public function fire(Job $job, $data) { - //日志 - $this->log("-----------队列任务开始-----------"); + $startTime = microtime(true); + $this->oQueueJob->log("-----------队列任务开始-----------"); - // 获取文章ID - $iRedisId = empty($data['redis_id']) ? 0 : $data['redis_id']; - if (empty($iRedisId)) { - $this->log("无效的redis_id,删除任务"); - $job->delete(); + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->oQueueJob->log("Redis连接失败,10秒后重试"); + $job->release(10); + $this->oQueueJob->flushLog(); return; } - // 生成唯一任务标识 - $sClassName = get_class($this); - $sRedisKey = "queue_job:{$sClassName}:{$iRedisId}"; - $sRedisValue = uniqid() . '_' . getmypid(); // 增加进程ID确保唯一性 + $iRedisId = empty($data['redis_id']) ? 0 : $data['redis_id']; + $sChunkIndex = empty($data['chunkIndex']) ? 0 : $data['chunkIndex']; + if (empty($iRedisId)) { + $this->oQueueJob->log("无效的redis_id,删除任务"); + $job->delete(); + $this->oQueueJob->flushLog(); + return; + } - // 尝试获取Redis锁(原子操作) - $isLocked = $this->queueJob->setRedisLock($sRedisKey, $sRedisValue, 86400); + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iRedisId}:{$sChunkIndex}"; + $sRedisValue = uniqid() . '_' . getmypid(); + $lockExpire = $this->lockExpire; + + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $lockExpire); if (!$isLocked) { - $currentValue = $this->queueJob->getRedisValue($sRedisKey); - $this->log("任务已被锁定,避免重复执行 | 锁键: {$sRedisKey} | 锁值: {$currentValue}"); - // 检查任务是否已超过最大重试次数 - if ($job->attempts() >= 2) { - $this->log("任务超过最大重试次数,停止重试"); + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->oQueueJob->log("任务已完成或失败,删除任务 | 状态: {$jobStatus}"); $job->delete(); } else { - $delay = $this->getRetryDelay("任务已锁定"); - $this->log("{$delay}秒后重试任务 | 重试次数: {$job->attempts()}"); - $job->release($delay); + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->oQueueJob->log("超过最大重试次数,停止重试"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + $this->oQueueJob->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries})"); + $job->release($delay); + } } + $this->oQueueJob->flushLog(); return; } - // 任务基础信息 $aParam = [ 'job_id' => $sRedisKey, 'job_class' => $sClassName, - 'status' => 0, // 0:处理中 + 'status' => 0, 'create_time' => time(), - 'params' => json_encode($data, JSON_UNESCAPED_UNICODE) + 'params' => json_encode($data, self::JSON_OPTIONS) ]; - // 创建任务日志记录 - $iLogId = $this->addLog($aParam); - if(!$iLogId) { - $this->log("日志创建失败,释放锁并删除任务:".json_encode($data)); - $this->queueJob->releaseRedisLock($sRedisKey, $sRedisValue); + + $iLogId = $this->oQueueJob->addLog($aParam); + if (!$iLogId) { + $this->oQueueJob->log("日志创建失败,释放锁并删除任务:".json_encode($data, self::JSON_OPTIONS)); + $this->QueueRedis->releaseRedisLock($sRedisKey, $sRedisValue); $job->delete(); + $this->oQueueJob->flushLog(); return; } + try { - // 执行核心任务 $oOpenAi = new OpenAi; $aResult = json_decode($oOpenAi->createFieldForQueue($data), true); - $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; - $sMsg = empty($aResult['msg']) ? '内容生成失败' : $aResult['msg']; - //更新任务状态 - $aParam = ['log_id' => $iLogId,'status' => 1,'update_time' => time(),'error' => $sMsg]; - $this->updateLog($aParam); - //删除任务 - $job->delete(); - $this->log("任务执行成功,已删除任务 | 日志ID: {$iLogId}"); - } catch (\Exception $e) { - - //错误信息 - $sMsg = empty($e->getMessage()) ? '任务出错' : $e->getMessage(); // 错误信息 - $sTrace = empty($e->getTraceAsString()) ? '' : $e->getTraceAsString(); - $this->log("任务执行异常: {$sMsg} | 堆栈: {$sTrace}"); - // 记录失败日志 - $this->updateLog([ + $sMsg = empty($aResult['msg']) ? '内容生成成功' : $aResult['msg']; + + $this->oQueueJob->updateLog([ 'log_id' => $iLogId, - 'status' => 2, + 'status' => 1, 'update_time' => time(), - 'error' => $sMsg.':'.$sTrace, + 'error' => $sMsg ]); - // 重试策略 - $attempts = $job->attempts(); - if ($attempts >= 2) { - $this->log("任务已重试{$attempts}次,停止重试"); - $job->delete(); - } else { - $delay = $this->getRetryDelay($sMsg); - $this->log("{$delay}秒后重试任务 | 重试次数: {$attempts}"); - $job->release($delay); - } + + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$iLogId} | 执行日志:{$sMsg}"); + + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e, $iLogId, $sRedisKey, $job); + } catch (\Exception $e) { + $this->oQueueJob->handleRetryableException($e, $iLogId, $sRedisKey, $job); } finally { - // 无论成功失败都释放锁(确保锁值匹配) - $releaseResult = $this->queueJob->releaseRedisLock($sRedisKey, $sRedisValue); - if (!$releaseResult) { - $this->log("释放锁失败 | 锁键: {$sRedisKey} | 锁值: {$sRedisValue}"); - } else { - $this->log("成功释放锁 | 锁键: {$sRedisKey}"); - } + $executionTime = microtime(true) - $startTime; + $this->oQueueJob->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + $this->oQueueJob->flushLog(); gc_collect_cycles(); } } diff --git a/application/common/Aireview.php b/application/common/Aireview.php new file mode 100644 index 0000000..2aedceb --- /dev/null +++ b/application/common/Aireview.php @@ -0,0 +1,98 @@ + 2,'msg' => 'Please select the article to be reviewed']; + } + $iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id']; + if(empty($iJournalId)){ + return ['status' => 2,'msg' => 'The journal to which the article belongs cannot be empty']; + } + //返回数组 + $aResult = ['status' => 1,'msg' => 'AI review successful']; + //数据库参数 + $aFields = ['journal_scope','attribute','contradiction','unreasonable','ethics','academic','conclusion','fund_number','hotspot','submit_direction','references_past_three','references_past_five','references_ratio_JCR1','references_ratio_JCR2','registration_assessment','cite_rate','references_num','article_field']; + foreach ($aParam as $key => $value) { + if(empty($value)){ + continue; + } + if(is_array($value)){ + if(!empty($value['assessment'])){ + $sField = $key.'_'.'assessment'; + $sAssessment = empty($value['assessment']) ? '' : $value['assessment']; + if(!empty($sAssessment)){ + $sAssessment = is_array($sAssessment) ? json_encode($sAssessment) : htmlspecialchars($value['assessment']); + } + $aInsert[$sField] = $sAssessment; + } + if(!empty($value['explanation'])){ + $sField = $key.'_'.'explanation'; + $aInsert[$sField] = empty($value['explanation']) ? '' : htmlspecialchars($value['explanation']); + } + }else{ + $aInsert[$key] = empty($value) ? '' : htmlspecialchars($value); + } + } + if(empty($aInsert)){ + return ['status' => 3,'msg' => 'Data is empty']; + } + + //查询文章审核内容-判断新增或修改 + $aWhere = ['article_id' => $iArticleId,'journal_id' => $iJournalId]; + $aAiReview = Db::name('article_ai_review')->field('id')->where($aWhere)->find(); + $iLogId = empty($aAiReview['id']) ? 0 : $aAiReview['id']; + //新增 + if(empty($iLogId)){ + $aInsert['create_time'] = date('Y-m-d H:i:s'); + $aInsert['content'] = $iArticleId; + $iLogId = Db::name('article_ai_review')->insertGetId($aInsert); + if(empty($iLogId)){ + $aResult = ['status' => 4,'msg' => 'Failed to add AI audit content']; + } + $aResult['data'] = ['id' => $iLogId]; + return $aResult; + } + if(!empty($iLogId)){ + $aWhere = ['id' => $iLogId]; + $aInsert['update_time'] = date('Y-m-d H:i:s'); + if(!Db::name('article_ai_review')->where($aWhere)->limit(1)->update($aInsert)){ + $aResult = ['status' => 5,'msg' => 'Failed to add AI audit content']; + } + $aAiReview = Db::table('t_article_ai_review')->where($aWhere)->find(); + $aResult['data'] = $aAiReview; + return $aResult; + } + return ['status' => 6,'msg' => 'illegal request']; + } + + /** + * @title 文章AI审核内容查询 + * @param article_id 文章ID + */ + public function get($aParam = []){ + + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + if(empty($aParam['article_id'])){ + return ['status' => 2,'msg' => 'Please select an article']; + } + //查询文章审核内容 + $aWhere = ['article_id' => $aParam['article_id']]; + $aAiReview = Db::name('article_ai_review')->where($aWhere)->find(); + + return ['status' => 1,'msg' => 'Successfully obtained article review content','data' => $aAiReview]; + } +} diff --git a/application/common/Article.php b/application/common/Article.php new file mode 100644 index 0000000..75f29db --- /dev/null +++ b/application/common/Article.php @@ -0,0 +1,332 @@ + 2,'msg' => 'Please select an article']); + } + + //获取文件内容 + $aWhere = ['article_id' => $iArticleId,'type_name' => 'manuscirpt']; + $aFile = Db::name('article_file')->field('file_url')->where($aWhere)->order('ctime desc')->limit(1)->find(); + if(empty($aFile['file_url'])){ + return json_encode(['status' => 2,'msg' => 'No Manuscript']); + } + + //接口获取上传文件 + $sUrl = $this->sJavaUrl."api/typeset/readDocx"; + $aParam['fileRoute'] = $this->sFileUrl.$aFile['file_url']; + $aResult = object_to_array(json_decode(myPost($sUrl,$aParam))); + return json_encode($aResult); + } + /** + * 获取期刊内容 + */ + public function getJournalPaperArt($aParam = []){ + + //判断文章ID + $sIssn = empty($aParam['issn']) ? [] : $aParam['issn']; + if(empty($sIssn)){ + return json_encode(['status' => 2,'msg' => 'Please select an article']); + } + //接口获取期刊内容 + $sUrl = $this->sTmrUrl."/api/Supplementary/getJournalPaperArt"; + $aParam = ['issn' => $sIssn]; + $aResult = object_to_array(json_decode(myPost($sUrl,$aParam),true)); + return json_encode($aResult); + } + /** + * 更新AI生成内容入库 + * @param $messages 内容 + * @param $model 模型类型 + */ + public function updateAiArticle($aParam = []){ + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + //主键ID + $iAiArticleId = empty($aParam['ai_article_id']) ? 0 : $aParam['ai_article_id']; + + //查询内容是否存在 + $aWhere = ['is_delete' => 2]; + if(empty($iArticleId) && empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']); + } + if(!empty($iArticleId)){ + $aWhere['article_id'] = $iArticleId; + } + if(!empty($iAiArticleId)){ + $aWhere['ai_article_id'] = $iAiArticleId; + } + //查询文章是否生成AI内容 + $aAiArticle = Db::name('ai_article')->field('ai_article_id')->where($aWhere)->find(); + if(empty($aAiArticle)){ + return json_encode(['status' => 3,'msg' => 'The article content of WeChat official account has not been generated']); + } + $iAiArticleId = $aAiArticle['ai_article_id']; + //必填参数验证 + $aFields = ['article_id','article_type','media_type','journal_id','journal_issn','title_english','title_chinese','content','covered','discussion_results','research_method','digest','research_background','overview','summary','highlights','discussion','prospect','is_generate']; + $sFiled = ''; + $aUpdateParam = []; + foreach($aFields as $val){ + if(!isset($aParam[$val])){ + continue; + } + if(is_array($aParam[$val])){ + $aParam[$val] = implode(";",$aParam[$val]); + } + $aUpdateParam[$val] = empty($aParam[$val]) ? '' : addslashes($aParam[$val]); + } + if(empty($aUpdateParam)){ + return json_encode(['status' => 1,'msg' => 'No data currently being processed']); + } + //执行入库 + $aUpdateParam['update_time'] = time(); + $result = Db::name('ai_article')->where('ai_article_id',$iAiArticleId)->limit(1)->update($aUpdateParam); + if($result === false){ + return json_encode(['status' => 4,'msg' => 'UPDATEING AI article failed']); + } + return json_encode(['status' => 1,'msg' => 'No data currently being processed']); + } + + + /** + * 构建文章领域-处理提示词 + */ + private function buildFieldPrompt($aSearch = []){ + + //必填验证 + if(empty($aSearch)){ + return []; + } + + $sSysMessagePrompt = ' + 你是一位资深的医学期刊学术评审专家,负责严谨、客观地评估学术文章。返回格式必须严格遵循以下JSON结构!请根据文章的标题和摘要从目标期刊下所有领域中筛选出符合文章的领域'; + $sSysMessagePrompt .= json_encode([ + "article_field" => [ + "assessment" => [ + "major_id" => "领域ID多个,分隔", + "major_name" => "领域名称多个,分隔" + ], + "explanation" =>"请详细解释说明.请返回中文解释!" + ] + ],JSON_UNESCAPED_UNICODE); + //组装问题 + $sUserPrompt = '根据文章的标题:{title};摘要:{abstrart}从目标期刊:【{journal_name}】包含的领域【json结构】{journal_major}中筛选出最符合文章领域【小于等于3个】'; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); + return [ + ['role' => 'system', 'content' => $sSysMessagePrompt], ['role' => 'user', 'content' => $sUserPrompt] + ]; + } + + /** + * 获取文章领域【AI】 + * @param bool $assoc 是否返回关联数组(默认true) + */ + public function getAiField($aParam = []){ + + $iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(array('status' => 2,'msg' => 'Please select an article' )); + } + + //查询文章 + $aWhere = ['article_id' => $iArticleId]; + $aArticle = json_decode($this->get($aWhere),true); + $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; + if(empty($aArticle)){ + return json_encode(array('status' => 3,'msg' => 'No articles requiring review were found' )); + } + + //获取文章领域 + $aArticleField = $this->getArticleField($aWhere); + // if(!empty($aArticleField['data'])){ + // return json_encode(array('status' => 4,'msg' =>'The article has been added to the field' )); + // } + //文章标题 + $title = empty($aArticle['title']) ? '' : $aArticle['title']; + if(empty($title)){ + return json_encode(array('status' => 5,'msg' => 'The title cannot be empty')); + } + //摘要 + $abstrart = empty($aArticle['abstrart']) ? '' : $aArticle['abstrart']; + if(empty($abstrart)){ + return json_encode(array('status' => 5,'msg' => 'The abstract cannot be empty' )); + } + //查询该期刊下的所有领域 + $aWhere = ['journal_id' => $aArticle['journal_id']]; + $aResult = $this->getJouarnalMajor($aWhere); + //期刊领域 + $aMaJor = empty($aResult['data']['major']) ? [] : $aResult['data']['major']; + //期刊信息 + $aJournal = empty($aResult['data']['journal']) ? [] : $aResult['data']['journal']; + if(empty($aJournal) || empty($aMaJor)){ + return json_encode($aResult); + } + + //变量替换 + $aSearch = []; + $title = empty($aArticle['title']) ? '' : '文章标题:'.$aArticle['title']; + $abstrart = empty($aArticle['abstrart']) ? '' : '文章摘要:'.$aArticle['abstrart']; + $aSearch['{title}'] = 'title:'.$title; + $aSearch['{abstrart}'] = 'abstract:'.$abstrart; + $aSearch['{journal_major}'] = empty($aMaJor) ? '' : json_encode($aMaJor);//期刊领域 + $aSearch['{journal_name}'] = empty($aJournal['title']) ? '' : $aJournal['title']; + $aMessage = $this->buildFieldPrompt($aSearch); + //请求OPENAI接口-非重要维度一次请求获取答案 + if(empty($aMessage)){ + return json_encode(['status' => 6,'msg' => 'AI Q&A content not obtained']); + } + + //请求OPENAI + $oOpenAi = new OpenAi; + $aParam = ['messages' => $aMessage,'model' => empty($aParam['api_model']) ? 'gpt-4.1' : $aParam['api_model'],'url' => $this->sAiUrl]; + $aResult = json_decode($oOpenAi->curlOpenAIStream($aParam),true); + //处理返回信息 + $aData = empty($aResult['data']) ? [] : $aResult['data']; + if(empty($aData)){ + return json_encode(['status' => 7,'msg' => empty($aResult['msg']) ? 'OPENAI returns empty content' : $aResult['msg']]); + } + + //数据处理 + $aData = $oOpenAi->extractAndParse($aData); + if(empty($aData['data'])){ + return json_encode($aData); + } + + //关联文章领域 + $aData = $aData['data']; + $aMaJorId = empty($aData['article_field']['assessment']) ? [] : $aData['article_field']['assessment']; + $sMaJorId = empty($aMaJorId['major_id']) ? '' : $aMaJorId['major_id']; + $aAddResult = $this->addArticleField(['article_field' => $sMaJorId,'article_id' => $iArticleId]); + + //插入文章审核记录表 + $oAireview = new Aireview; + $aData['article_id'] = $iArticleId; + $aData['journal_id'] = $aArticle['journal_id']; + $aResult = $oAireview->addAiReview($aData); + return json_encode($aAddResult); + + } + + /** + * @title 文章内容 + * @param article_id 文章ID + */ + public function get($aParam = []){ + + //获取参数 + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(array('status' => 2,'msg' => 'Please select an article' )); + } + //查询文章 + $aWhere = ['article_id' => $iArticleId]; + $aArticle = Db::name('article')->where($aWhere)->find(); + return json_encode(['status' => 1,'msg' => 'success','data' => $aArticle]); + } + + /** + * @title 获取文章期刊领域 + * @param article_id 文章ID + */ + public function getJouarnalMajor($aParam = []){ + + //期刊ID + $iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id']; + if(empty($iJournalId)){ + return ['status' => 2,'msg' => 'Please select an journal']; + } + //根据期刊ID查询期刊信息 + $aWhere = ['journal_id' => $iJournalId,'state' => 2]; + $aJournal = Db::name('journal')->field('title,issn,journal_id')->where('journal_id', $iJournalId)->find(); + if(empty($aJournal)){ + return ['status' => 3,'msg' => 'This article is not associated with a journal']; + } + $sIssn = empty($aJournal['issn']) ? '' : $aJournal['issn']; + if(empty($sIssn)){ + return ['status' => 4,'msg' => 'The issn of the journal is empty']; + } + //查询期刊领域ID + $aWhere = ['journal_issn' => $sIssn,'mtj_state' => 0]; + $aMaJorId = Db::name('major_to_journal')->where($aWhere)->column('major_id'); + if(empty($aMaJorId)){ + return ['status' => 5,'msg' => 'The field of the journal is empty']; + } + //查询领域名称 + $aWhere = ['major_state' => 0,'major_id' => ['in',$aMaJorId],'pid' => ['>',0]]; + $aMaJor = Db::name('major')->where($aWhere)->column('major_id,major_title'); + return ['status' => 5,'msg' => 'success','data' => ['major' => $aMaJor,'journal' => $aJournal]]; + } + + /** + * 获取文章所属领域 + * @param article_id 文章ID + */ + public function getArticleField($aParam = []){ + + // 文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return ['status' => 2,'msg' => 'Please select a Article']; + } + //查询审稿人领域信息 + $aParam['state'] = 0; + $aWhere = ['article_id' => $iArticleId,'state' => 0]; + $aArticleField = Db::name('major_to_article')->field('major_id')->where($aWhere)->select(); + return ['status' => 1,'msg' => "success",'data' => $aArticleField]; + } + /** + * 添加文章所属领域 + * @param reviewer_id 审核人ID + * @param + */ + public function addArticleField($aParam = []){ + + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return ['status' => 2,'msg' => 'Please select a article']; + } + $sField = empty($aParam['article_field']) ? '' : $aParam['article_field']; + if(empty($sField)){ + return ['status' => 2,'msg' => "Please select the field to which the article belongs"]; + } + $aField = is_array($sField) ? $sField : explode(',', trim($sField,',')); + if(empty($aField)){ + return ['status' => 3,'msg' => "Please select the user's field of expertise"]; + } + + //领域入库 + $aInsertParam = []; + foreach ($aField as $key => $value) { + if(empty($value)){ + continue; + } + $aInsertParam[] = ['major_id' => $value,'article_id' => $iArticleId,'ctime' => time(),'state' => 0]; + } + if(empty($aInsertParam)){ + return ['status' => 4,'msg' => "Please select the user's field of expertise"]; + } + $result = Db::name('major_to_article')->insertAll($aInsertParam); + if($result === false){ + return ['status' => 5,'msg' => "Failed to add article field"]; + } + return ['status' => 1,'msg' => "Successfully added the article field"]; + } + +} diff --git a/application/common/OpenAi.php b/application/common/OpenAi.php index 85c27a4..7d8e2bd 100644 --- a/application/common/OpenAi.php +++ b/application/common/OpenAi.php @@ -3,11 +3,15 @@ namespace app\common; use think\Cache; use think\Db; use think\Queue; +use app\common\Article; +use app\common\QueueRedis; class OpenAi { protected $sApiKey = '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'; protected $proxy = ''; protected $sUrl = 'http://chat.taimed.cn/v1/chat/completions';//'https://api.openai.com/v1/chat/completions'; + //Ai地址 + protected $sAiUrl = "http://125.39.141.154:10002"; protected $curl; protected $sResponesData; protected $sError; @@ -63,46 +67,94 @@ class OpenAi // "criteria" => "根据文章内容{content}分析是否有明显的不合理之处?" // ] ]; - //定义redis连接 - private $redis; - public function __construct() - { + //公微问题模版 + protected $aWechatQuestion = [ - $config = \think\Config::get('queue'); - $this->redis = new \Redis(); - $this->redis->connect($config['host'], $config['port']); - - if (!empty($config['password'])) { - $this->redis->auth($config['password']); - } - - $this->redis->select($config['select']); - // 初始化 Redis 连接 - // $this->redis = Cache::store('redis')->handler(); - } - - /** - * 构建公微模版-处理提示词【默认】 - */ - public function buildDefaultPrompt($aSearch = []){ - - if(empty($aSearch)){ - return []; - } - $sSysMessagePrompt = ' - 你是一位专业的医学学术翻译与分析专家,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,请返回中文解释!返回格式必须严格遵循以下JSON结构:'; - $aQuestion = [ + 'system_message' => '您是一位医学期刊的医学科普转化专家,严格遵循用户要求的结构、语言和专业约束,不编造数据,不夸大结论,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,按照以下严格格式生成结构化[JSON结构]输出[中文]:', + 'public_message' => [ "covered" => "[列出文章涵盖的学科及研究方法,总字数不超过100字,学科和方法之间用逗号分隔,例如:肿瘤学,分子生物学,基因组测序,生物信息学分析]", + "title_chinese" => "[将标题翻译成中文:内容需自然流畅、口语化、连贯性、学术性]" + // , + // "content" => "将内容翻译成中文,需自然流畅、口语化、连贯性、学术性,保留原文的章节结构和图表编号" + ], + 'default' => [ "digest" => "[学术规范翻译并提炼摘要,强调逻辑性、科学术语准确性和表达严谨性,采用段落形式,总字数不超过500字]", "research_background" => "[提炼研究背景,采用连贯的段落形式,总字数超过200字]", "discussion_results" => "[针对文章简单总结讨论和结果,采用连贯的段落形式,总字数超过450字]", "research_method" => "[总结文章的研究方法,采用连贯的段落形式,总字数超过300字]", "prospect" => "[针对稿件内容进行展望撰写,采用连贯的段落形式]", - "highlights" => "[总结归纳亮点,至少3点,每点用分号分隔]", - "title_chinese" => "[将标题翻译成中文:内容需自然流畅、口语化、连贯性、学术性]" - // , - // "content" => "将内容翻译成中文,需自然流畅、口语化、连贯性、学术性,保留原文的章节结构和图表编号" - ]; + "highlights" => "[总结归纳亮点,至少3点,每点用分号分隔]" + ], + 'review' => [ + "overview" => "按照学术规范翻译并提炼文章概述,整体内容应大于1200字,其中应包含文章背景(不少于400字),其他内容提炼更强调逻辑性、科学术语准确性和表达的严谨性,注意内容不要有严重重复,采用连贯的段落形式", + "summary" => "针对文章结论生成一个简单总结,内容不要和文章概述重复,字数150以内", + ] + ]; + //AI审稿提示词 + protected $aReviewQuestion = [ + + 'system_message' => '您是一位资深的医学期刊学术评审专家,请负责严谨、客观地评估学术文章。请根据提供的医学论文信息,按照以下严格格式生成结构化[JSON结构]输出[中文]:', + 'default' => [ + "journal_scope" => "结合标题和摘要以及期刊范围来判断文章是否符合目标期刊?", + "attribute" => "内容是否有科学前沿性和创新性?参照维度[研究内容的原创性:论文中的研究内容是否与已有的研究重复?是否在同样的领域提出了类似的结论,但在方法或结果上有所创新?如果有,作者是否清楚地解释了如何与之前的研究不同,或者如何在原有基础上进行扩展或改进?如果是综述文章,汇总并综合最新的研究成果,尤其是近几年内的重要发现,展示领域内最新的进展成果。作者可以识别出未被充分讨论的问题或提出新的研究问题,而不是简单文献堆砌。文章中的图表创新能否将信息的清晰呈现,方便读者理解复杂研究问题。论文方法创新性评估要点:是否采用了新的实验模型或创新的实验设计,能有效解决当前研究中的难点或空白?是否有合理的对照组和多组实验设计,确保研究结果的可靠性?是否使用了当前前沿的技术(如高通量测序、CRISPR基因编辑等),提高了实验精度或数据分析能力?是否结合了跨学科的方法(如生物信息学、人工智能等)?是否应用了多种验证手段或统计方法,确保结果的可信度?是否通过细胞实验、动物模型等多重验证,确保实验结果的可靠性?结论与数据的创新性:研究结论是否提出了新观点或新见解?是否提供了新的实验数据或观察结果,能够突破当前的研究局限?例如,发现了新的生物标志物,或对已知生物通路的作用机制提供了全新的解释]", + "contradiction" => "内容是否前后矛盾或存在逻辑不一致的问题?", + "unreasonable" => "内容是否有明显的不合理之处?", + "ethics" => "内容是否存在伦理号缺失或明显伦理问题?", + "academic" => "内容是否存在学术不端问题如:抄袭\数据作假\图片伪造等?", + "conclusion" => "根据内容判断文章结论的科学性和可靠性?", + "fund_number" => "内容是否有无基金号?请详细说明", + "hotspot" => "内容有哪些符合目标期刊当下的热点话题", + "submit_direction" =>"根据内容总结文章送审方向", + "references_num" =>"根据内容统计文章参考文献的数量", + "references_past_three" =>"统计内容里近3年的参考文献的数量及所占比例", + "references_past_five" =>"统计内容里近5年的参考文献的数量及所占比例", + "references_ratio_JCR1" =>"根据2024JCR最新分区分析文章内容里的文献判断属于JCR1还是JCR2,统计属于JCR 1区的数量及比例", + "references_ratio_JCR2" =>"根据2024JCR最新分区评估文章内容里的文献判断属于JCR1还是JCR2,统计属于JCR 2区的数量及比例", + "registration_assessment" =>"内容是否存在临床注册号和知情同意书?解释说明", + "cite_rate" => "结合内容分析发表后被引用的概率" + ] + ]; + //定义redis连接 + private $oQueueRedis; + public function __construct() + { + + $this->oQueueRedis = QueueRedis::getInstance(); + } + + /** + * 构建公微模版-处理提示词 + */ + public function buildWechatPrompt($aSearch = []){ + + if(empty($aSearch)){ + return []; + } + //文章类型 + $prompt_article_type = empty($aSearch['prompt_article_type']) ? '' : $aSearch['prompt_article_type']; + if(empty($prompt_article_type)){ + return []; + } + //组织参数 + if(in_array($prompt_article_type, ['Mini Review','Review'])){ + $prompt_article_type = 'review'; + }else{ + $prompt_article_type = 'default'; + } + //获取问题 + $aQuestion = $this->aWechatQuestion; + $aQuestionLists = empty($aQuestion[$prompt_article_type]) ? [] : $aQuestion[$prompt_article_type]; + if(empty($aQuestionLists)){ + return []; + } + //系统角色 + $sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message']; + if(empty($sSysMessagePrompt)){ + return []; + } + //公共问题 + $aPublicQuestion = empty($aQuestion['public_message']) ? [] : $aQuestion['public_message']; + $aQuestion = array_merge($aPublicQuestion,$aQuestionLists); //问题处理 $aMessage = []; foreach($aQuestion as $key => $value){ @@ -129,27 +181,63 @@ class OpenAi } return $aMessage; } - /** - * 构建公微模版-处理提示词【Review】 + * 构建AI翻译内容-处理提示词 */ - public function buildReviewPrompt($aSearch = []){ + public function buildTranslatePrompt($aSearch = []){ if(empty($aSearch)){ return []; } - $sSysMessagePrompt = ' - 你是一位专业的医学学术翻译与分析专家,擅长将复杂的医学研究论文转化为适合微信公众号推送的专业科普内容。请根据提供的医学论文信息,按照以下严格格式生成结构化输出[中文]:'; - $aQuestion = [ - "covered" => "列出文章涵盖的学科及研究方法,总字数不超过100字,学科和方法之间用逗号分隔,例如:肿瘤学,分子生物学,基因组测序,生物信息学分析", - "overview" => "按照学术规范翻译并提炼文章概述,整体内容应大于1200字,其中应包含文章背景(不少于400字),其他内容提炼更强调逻辑性、科学术语准确性和表达的严谨性,注意内容不要有严重重复,采用连贯的段落形式", - "summary" => "针对文章结论生成一个简单总结,内容不要和文章概述重复,字数150以内", - "title_chinese" => "将标题翻译成中文:内容需自然流畅、口语化、连贯性、学术性" - // , - // "content" => "将内容翻译成中文,需自然流畅、口语化、连贯性、学术性,保留原文的章节结构和图表编号" + $sSysMessagePrompt = '你是一位专业的医学翻译专家,请将用户提供的内容准确、流畅地翻译成中文。翻译需自然流畅、口语化、连贯性、学术性,保留原文的专业术语和逻辑结构'; + $sUserPrompt = '"将以下内容翻译为中文,仅返回翻译结果,不要解释:\n {#content#}"'; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); + $aMessage = [ + ['role' => 'system', 'content' => $sSysMessagePrompt], + ['role' => 'user', 'content' => $sUserPrompt] ]; + //模型版本 + $aMessage = [ + 'model' => empty($aSearch['model']) ? 'gpt-4.1' : $aSearch['model'], + 'messages' => $aMessage, + 'temperature' => 0.2,// 降低随机性(0-1,0为最确定) + ]; + $aResult = json_decode($this->curlOpenAIStream($aMessage),true); + $sJsonData = empty($aResult['data']) ? '' : $aResult['data']; + if(empty($sJsonData)){ + return ['status' => 2,'msg' => 'Translation failed']; + } + $aJsonData = json_decode($sJsonData, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return ['status' => 3,'msg' => 'JSON parsing error:'.json_last_error_msg()]; + } + return ['status' => 1,'msg' => 'Translation successful','data' => $aJsonData]; + } + + /** + * 构建AI审稿-处理提示词 + */ + public function buildReviewPrompt($aSearch = []) + { + if(empty($aSearch)){ + return []; + } + + //获取问题 + $aQuestion = $this->aReviewQuestion; + $aQuestionLists = empty($aQuestion['default']) ? [] : $aQuestion['default']; + if(empty($aQuestionLists)){ + return []; + } + //系统角色 + $sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message']; + if(empty($sSysMessagePrompt)){ + return []; + } + //问题处理 $aMessage = []; foreach($aQuestion as $key => $value){ + //修改当前内容 $sInfo = json_encode([$key => $value],JSON_UNESCAPED_UNICODE); $sSysMessagePromptInfo = $sSysMessagePrompt.$sInfo; if($key == "title_chinese"){ @@ -172,101 +260,6 @@ class OpenAi } return $aMessage; } - /** - * 构建AI翻译-处理提示词 - */ - public function buildTranslatePrompt($aSearch = []){ - if(empty($aSearch)){ - return []; - } - $sSysMessagePrompt = '你是一位专业的医学翻译专家,请将用户提供的内容准确、流畅地翻译成中文。翻译需自然流畅、口语化、连贯性、学术性,保留原文的专业术语和逻辑结构'; - $sUserPrompt = '"将以下内容翻译为中文,仅返回翻译结果,不要解释:\n {#content#}"'; - $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); - return [ - ['role' => 'user', 'content' => $sUserPrompt] - ]; - } - - /** - * 构建AI审稿-处理提示词【重要的多次请求问题初始化】 - */ - public function buildReviewPromptImportant($aSearch = [],$aValue = []) { - //必填验证 - if(empty($aSearch) || empty($aValue)){ - return []; - } - //组装问题 - $sUserPrompt = empty($aValue['criteria']) ? '' : $aValue['criteria']; - $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); - $aMessage = [ - ['role' => 'system', 'content' => $aValue['system']], - ['role' => 'user', 'content' => $sUserPrompt] - ]; - //模型版本 - $model = empty($aSearch['model']) ? 'gpt-4.1' : $aSearch['model']; - $aMessage = [ - 'model' => $model, - 'messages' => $aMessage, - 'temperature' => 0.2,// 降低随机性(0-1,0为最确定) - ]; - return $aMessage; - } - - /** - * 构建AI审稿-处理提示词【非重要的一次请求回答】 - */ - public function buildReviewPromptUnimportant($aSearch = []) - { - if(empty($aSearch)){ - return []; - } - $sSysMessagePrompt = ' - 你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。请根据提供的信息,对以下问题进行专业评审 - ```json - { - "ethics": "根据文章内容分析是否存在伦理号缺失或明显伦理问题?", - "academic": "根据文章内容分析是否存在学术不端问题【抄袭、数据作假、图片伪造等】?", - "conclusion": "根据文章内容判断文章结论科学性和可靠性?", - "fund_number":"根据文章内容分析有无基金号?请详细说明", - "hotspot":"根据文章内容分析有哪些符合目标期刊当下的热点话题", - "submit_direction":"根据文章内容总结文章送审方向", - "references_num":"根据文章内容统计文章参考文献的数量", - "references_past_three":"统计文章内容里近3年的参考文献的数量及所占比例", - "references_past_five":"统计文章内容里近5年的参考文献的数量及所占比例", - "references_ratio_JCR1":"根据2024JCR最新分区分析文章内容里的文献判断属于JCR1还是JCR2,统计属于JCR 1区的数量及比例", - "references_ratio_JCR2":"根据2024JCR最新分区评估文章内容里的文献判断属于JCR1还是JCR2,统计属于JCR 2区的数量及比例", - "registration_assessment":"根据文章内容分析是否存在临床注册号和知情同意书?解释说明", - "cite_rate" => "根据文章内容分析文章发表后被引用的概率" - }请针对每个问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下 JSON 结构:{ - "ethics": { - "assessment": "是/否", - "explanation": "请针对文章伦理号及伦理进行分析及说明" - }, - "academic": { - "assessment": "是/否", - "explanation": "请进行相似度分析并指出具体可疑部分" - }, - "conclusion": { - "assessment": "是/否", - "explanation": "是否基于充分证据得出" - }, - "fund_number": "", - "hotspot": "", - "submit_direction": "", - "references_num":"", - "references_past_three": "", - "references_past_five": "", - "references_ratio_JCR1": "", - "references_ratio_JCR2": "", - "registration_assessment":"", - "cite_rate" => "" - }'; - $sUserPrompt = '文章内容{content};目标期刊{journal_name}'; - $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); - return [ - ['role' => 'system', 'content' => $sSysMessagePrompt], ['role' => 'user', 'content' => $sUserPrompt] - ]; - } /** * CURL 发送请求到 OpenAI【单独】 @@ -341,205 +334,6 @@ class OpenAi curl_close($this->curl); return json_encode(['status' => 1,'msg' => 'success','data' => $aContent]); } - /** - * 对接OPENAI接口-并行CURL请求【重要维度单独询问】 - */ - public function curlMultiOpenAIImportant($aSearch = [],$timeout = 120, $iChunkSize = 2) { - // 入参校验 - if (empty($aSearch)) { - return json_encode(['status' => 2, 'msg' => 'Parameter is empty']); - } - //提问问题类型 - $sKey = empty($aSearch['question']) ? '' : $aSearch['question']; - if (empty($sKey)) { - return json_encode(['status' => 2, 'msg' => 'Please select the type of question']); - } - //获取问题 - $aQuestion = $this->$sKey; - if (empty($aQuestion)) { - return json_encode(['status' => 2, 'msg' => 'question is empty']); - } - - //分批处理(核心优化:控制并发量) - $aChunk = array_chunk($aQuestion, $iChunkSize); // 按批次拆分,每批最多5个请求 - //定义空数组用于接收数据 - $aEmptyData = $aLog = $aReturnData = []; - //分批次处理开始 - foreach ($aChunk as $iChunkKey => $item) { - // 初始化多curl句柄 - $oCurlMulti = curl_multi_init(); - $aCurl = []; - // 批量初始化请求 - foreach ($item as $key => $value) { - // 跳过无效参数 - if (empty($value)) { - $aLog[] = [ - 'content' => $iChunkKey.'-'.$key.':Invalid parameter' - ]; - continue; - } - //问题处理-变量替换 - $aQuestionInfo = $this->buildReviewPromptImportant($aSearch,$value); - if(empty($aQuestionInfo)){ - $aLog[] = [ - 'content' => $iChunkKey.'-'.$key.':The problem is empty:'.json_encode($value) - ]; - continue; - } - // 核心配置优化 - $oCurl = curl_init(); - curl_setopt_array($oCurl, [ - CURLOPT_URL => $this->sUrl, - CURLOPT_HTTPHEADER => [ - 'Content-Type: application/json', - 'Authorization: Bearer ' . $this->sApiKey, - 'Expect:', - ], - CURLOPT_PROXY => $this->proxy, - // SSL验证优化:若代理证书不可信,临时关闭(生产环境需配置信任证书) - CURLOPT_SSL_VERIFYPEER => true, // 调试时设为false,生产环境设为true - CURLOPT_SSL_VERIFYHOST => 2, // 调试时设为0,生产环境设为2 - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => json_encode($aQuestionInfo), - CURLOPT_RETURNTRANSFER => true, - // 超时优化:延长响应超时,新增连接超时 - CURLOPT_TIMEOUT => $timeout, // 总超时(秒),建议60-120 - CURLOPT_CONNECTTIMEOUT => 20, // 连接超时(秒),避免无限等待 - CURLOPT_LOW_SPEED_LIMIT => 1024, // 最低速度(字节/秒),低于此值触发超时 - CURLOPT_LOW_SPEED_TIME => 30, // 持续低速时间(秒),超过则终止 - ]); - curl_multi_add_handle($oCurlMulti, $oCurl); - $aCurl[$key] = $oCurl; - } - // 空请求处理 - if (empty($aCurl)) { - curl_multi_close($oCurlMulti); - continue; - } - - // 核心优化:修复curl_multi循环逻辑,确保所有请求完成 - $active = null; - $mrc = CURLM_OK; - // 第一阶段:处理瞬时可完成的请求 - do { - $mrc = curl_multi_exec($oCurlMulti, $active); - } while ($mrc == CURLM_CALL_MULTI_PERFORM); - - // 第二阶段:等待所有活跃请求完成(关键优化) - while ($active > 0 && $mrc == CURLM_OK) { - // 等待事件(超时1秒,避免CPU空转) - if (curl_multi_select($oCurlMulti, 1.0) != -1) { - // 处理就绪的请求 - do { - $mrc = curl_multi_exec($oCurlMulti, $active); - } while ($mrc == CURLM_CALL_MULTI_PERFORM); - } else { - // 无事件时,检查是否超时(防止无限阻塞) - $timedOut = false; - foreach ($aCurl as $oCurl) { - $startTime = curl_getinfo($oCurl, CURLINFO_STARTTRANSFER_TIME); - if ($startTime > 0 && (microtime(true) - $startTime) > $timeout) { - $timedOut = true; - break; - } - } - if ($timedOut) break; // 超时则强制退出 - } - } - - // 处理当前批次结果 - foreach ($aCurl as $key => $oCurl) { - // 1. 捕获curl错误(连接失败、超时等) - $sError = curl_error($oCurl); - if (!empty($sError)) { - $aLog[] = [ - 'content' => "Curl error: {$sError}" - ]; - $aEmptyData[] = $key; - curl_multi_remove_handle($oCurlMulti, $oCurl); - curl_close($oCurl); - continue; - } - - // 2. 获取HTTP状态码(关键优化:处理OpenAI的API错误) - $httpCode = curl_getinfo($oCurl, CURLINFO_HTTP_CODE); - $sContent = curl_multi_getcontent($oCurl); - // 3. 处理非200状态码(如限流、服务不可用) - if ($httpCode != 200) { - $errorMsg = "HTTP {$httpCode}: " . (empty($sContent) ? 'No response' : $sContent); - // 记录关键错误日志(便于调试) - $aLog[] = [ - 'http_code' => $httpCode, - 'content' => $errorMsg, - ]; - $aEmptyData[] = $key; - curl_multi_remove_handle($oCurlMulti, $oCurl); - curl_close($oCurl); - continue; - } - - // 4. 解析响应内容(原逻辑优化) - $aResult = json_decode($sContent, true); - if (json_last_error() != JSON_ERROR_NONE) { - $aLog[] = [ - 'content' => "Invalid JSON: {$sContent}", - ]; - $aEmptyData[] = $key; - curl_multi_remove_handle($oCurlMulti, $oCurl); - curl_close($oCurl); - continue; - } - - // 5. 提取OpenAI的content(简化判断逻辑) - $aOpenAiContent = empty($aResult['choices'][0]['message']['content']) ? '' : $aResult['choices'][0]['message']['content']; - if (empty($aOpenAiContent)) { - $aLog[] = [ - 'content' => "OPENAI returns empty content", - ]; - $aEmptyData[] = $key; - curl_multi_remove_handle($oCurlMulti, $oCurl); - curl_close($oCurl); - continue; - } - - // 6. 处理业务解析(原extractAndParse逻辑) - $aData = $this->extractAndParse($aOpenAiContent); - $aContent = empty($aData['data']) ? [] : $aData['data']; - $sMsg = empty($aData['msg']) ? 'Success' : $aData['msg']; - if (empty($aContent)) { - $aEmptyData[] = $key; - } - $aLog[] = [ - 'content' => $sMsg, - ]; - $aReturnData += $aContent; - // 释放资源 - curl_multi_remove_handle($oCurlMulti, $oCurl); - curl_close($oCurl); - } - - // 关闭当前批次的multi句柄 - curl_multi_close($oCurlMulti); - // 批次间隔(核心优化:避免触发OpenAI限流) - if ($iChunkKey < count($aChunk) - 1) { - usleep(1000000); // 批次间间隔1秒(根据OpenAI配额调整) - } - } - $aParam = [ - 'status' => 1, - 'msg' => 'success', - 'data' => empty($aReturnData) ? [] : $aReturnData, - 'empty_data' => empty($aEmptyData) ? [] : $aEmptyData, - 'log_data' => empty($aLog) ? [] : $aLog, - 'open_ai_id' => empty($aSearch['open_ai_id']) ? 0 : $aSearch['open_ai_id'] - ]; - - //日志记录 - $this->addLog($aParam); - - return json_encode($aParam); - } - /** * CURL 发送请求到 OpenAI【流式】 * @param $messages 内容 @@ -557,7 +351,7 @@ class OpenAi //超时设置 $timeout = empty($aParam['timeout']) ? 300 : $aParam['timeout']; //接口地址 - $sUrl = $this->sUrl; + $sUrl = empty($aParam['url']) ? $this->sUrl : $aParam['url']; //组装数据 $data = [ @@ -624,8 +418,7 @@ class OpenAi /** * 解析流式响应 */ - private function parseMedicalStreamResponse($streamContent) - { + private function parseMedicalStreamResponse($streamContent){ $fullContent = ''; $lines = explode("\n", $streamContent); foreach ($lines as $line) { @@ -639,57 +432,10 @@ class OpenAi return $fullContent; } - /** - * 记录处理进度【Redis】 - */ - private function recordProcessingStart($key,$totalQuestions) - { - $this->redis->hMSet($key, [ - 'status' => 'processing', - 'total' => $totalQuestions, - 'completed' => 0, - 'start_time' => time() - ]); - $this->redis->expire($key, 86400); // 24小时过期 - } - /** - * 更新处理进度【Redis】 - */ - private function updateProcessingProgress($key,$iId,$completed) - { - - $this->redis->hSet($key, 'completed', $completed); - //完成进度 - $iProgress = round(($completed / $this->redis->hGet($key, 'total')) * 100, 2); - if($iProgress == 100){ - $this->recordProcessingComplete($key,$iId); - } - $this->redis->hSet($key, 'progress', $iProgress); - } - /** - * 记录处理完成【Redis】 - */ - private function recordProcessingComplete($key,$iId) - { - $this->redis->hSet($key, 'status', 'completed'); - $this->redis->hSet($key, 'end_time', time()); - $this->wechatGegnerate(['article_id' => $iId]); - } - - /** - * 保存分块进度【Redis】 - */ - private function saveChunkProgress($key, $chunkIndex, $content) - { - $this->redis->hset($key, "chunk_{$chunkIndex}", $content); - $this->redis->expire($key, 86400); // 进度保存24小时 - } - /** * 微信公众号-生成公微内容(CURL) */ - public function createWechatContent($aParam = []) - { + public function createWechatContent($aParam = []){ //主键ID $iId = empty($aParam['redis_id']) ? 0 : $aParam['redis_id']; if(empty($iId)){ @@ -703,16 +449,26 @@ class OpenAi //记录处理开始 $iNum = count($aMessage); $sRedisKey = 'ai_create_article_'.$iId; - $this->recordProcessingStart($sRedisKey,$iNum); + $this->oQueueRedis->recordQuestionProcessingStart($sRedisKey,$iNum); //定义空数组 $aChunkResult = $aFail = []; + $batchId = uniqid(); + $iQueueCount1 = $iQueueCount2 = 0; foreach ($aMessage as $key => $value) { $aParam['messages'] = $value; $aParam['chunkIndex'] = $key; $aParam['count_num'] = $iNum; - Queue::push('app\api\job\createFieldForQueue@fire', $aParam, 'createFieldForQueue'); + // if($key%2 == 0){ + $aParam['key_name'] = 'queue_1_completed'; + Queue::push('app\api\job\createFieldForQueue@fire', $aParam, 'createFieldForQueue'); + // }else{ + // $aParam['url'] = $this->sAiUrl; + // $aParam['key_name'] = 'queue_2_completed'; + // Queue::push('app\api\job\createFieldForQueue@fire', $aParam, 'createFieldForQueueBak'); + // } + } - return json_encode(['status' => 1, 'msg' => 'createFieldForQueue success']); + return json_encode(['status' => 1, 'msg' => 'Content is being generated, please wait']); } /** * 微信公众号-生成内容队列形式 @@ -733,72 +489,65 @@ class OpenAi $iMaxNum = empty($aParam['count_num']) ? 0 : $aParam['count_num']; //请求OPENAI $aResult = $this->curlOpenAIStream($aParam); - //更新处理进度 $iIndex = empty($aParam['chunkIndex']) ? 0 : $aParam['chunkIndex']; $sRedisKey = 'ai_create_article_'.$iId; - $this->updateProcessingProgress($sRedisKey,$iId,$iIndex + 1); - + $sKeyName = empty($aParam['key_name']) ? 'queue_1_completed' : $aParam['key_name']; + $iProgress = $this->oQueueRedis->updateQuestionProcessingProgress($sRedisKey,$sKeyName); //保存内容 $sRedisKey = 'ai_create_article_progress_'.$iId; - $this->saveChunkProgress($sRedisKey, $iIndex,$aResult); + $this->oQueueRedis->saveChunkProgress($sRedisKey, $iIndex,$aResult); //更新入库 $aReturnData = json_decode($aResult,true); $aDataInfo =empty($aReturnData['data']) ? [] : $aReturnData['data']; $aData = empty($aDataInfo) ? [] : $this->extractAndParse($aDataInfo); $aData = empty($aData['data']) ? [] : $aData['data']; - if(!empty($aData)){ + if(!empty($aData)){//更新AI审稿记录表 + if($iProgress >= 100){ + $aData['is_generate'] = 1; + } $aData['article_id'] = $iId; - $this->updateAiArticle($aData); + $this->updateAiContent($aData); } return $aResult; } - /** - * 获取期刊内容 + * 微信公众号-更新AI生成内容 */ - public function getJournalPaperArt($aParam = []){ + private function updateAiContent($aParam = []){ - //判断文章ID - $sIssn = empty($aParam['issn']) ? [] : $aParam['issn']; - if(empty($sIssn)){ - return json_encode(['status' => 2,'msg' => 'Please select an article']); - } - //接口获取期刊内容 - $sUrl = $this->sTmrUrl."/api/Supplementary/getJournalPaperArt"; - $aParam = ['issn' => $sIssn]; - $aResult = object_to_array(json_decode(myPost($sUrl,$aParam),true)); - return json_encode($aResult); - } - - /** - * 获取文章文件内容 - */ - - public function getFileContent($aParam = []){ - - //判断文章ID - $iArticleId = empty($aParam['article_id']) ? [] : $aParam['article_id']; + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; if(empty($iArticleId)){ - return json_encode(['status' => 2,'msg' => 'Please select an article']); + return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']); } + //更新生成状态 + $oArticle = new Article; + $aResult = json_decode($oArticle->updateAiArticle($aParam),true); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + $sMsg = empty($aResult['msg']) ? '更新状态失败' : $aResult['msg']; + //是否生成 + $is_generate = empty($aParam['is_generate']) ? 2 : $aParam['is_generate']; - //获取文件内容 - $aWhere = ['article_id' => $iArticleId,'type_name' => 'manuscirpt']; - $aFile = Db::name('article_file')->field('file_url')->where($aWhere)->order('ctime desc')->limit(1)->find(); - if(empty($aFile['file_url'])){ - return json_encode(['status' => 2,'msg' => 'No Manuscript']); + //内容生成完成推送上传素材队列 + if($is_generate == 1){ + if($iStatus == 1){ + //四小时后推送上传素材并推送草稿箱 + $iDelaySeconds = 4 * 3600; // 4小时的秒数 + Queue::later($iDelaySeconds,'app\api\job\WechatMaterial@fire', ['article_id' => $iArticleId], 'WechatMaterial'); + $sMsg = '文章AI内容生成成功'; + }else{ + $iStatus = 2; + } + //插入日志记录 + $oMaterial = new Material; + $aLogInfo = ['article_id' => $iArticleId,'type' => 5,'msg' =>$sMsg,'status' => $iStatus,'create_time' => time()]; + $result = $oMaterial->addWechatLog($aLogInfo); } - - //接口获取上传文件 - $sUrl = $this->sJavaUrl."api/typeset/readDocx"; - $aParam['fileRoute'] = $this->sFileUrl.$aFile['file_url']; - $aResult = object_to_array(json_decode(myPost($sUrl,$aParam))); return json_encode($aResult); } - /** * 添加接口访问日志 */ @@ -819,87 +568,13 @@ class OpenAi return DB::name('openapi_log')->insertGetId($aInsert); } - /** - * 更新AI生成内容入库 - * @param $messages 内容 - * @param $model 模型类型 - */ - private function updateAiArticle($aParam = []){ - //文章ID - $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; - - //查询内容是否存在 - $aWhere = ['is_delete' => 2]; - if(empty($iArticleId)){ - return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']); - } - $aWhere['article_id'] = $iArticleId; - $aAiArticle = Db::name('ai_article')->field('ai_article_id')->where($aWhere)->find(); - if(empty($aAiArticle)){ - return json_encode(['status' => 3,'msg' => 'he article content of WeChat official account has not been generated']); - } - $iAiArticleId = $aAiArticle['ai_article_id']; - - //必填参数验证 - $aFields = ['article_id','title_english','title_chinese','journal_issn','covered','digest','research_result','content','highlights','discussion','prospect','research_background','discussion_results','research_method','overview','summary','is_generate']; - $sFiled = ''; - $aUpdateParam = []; - foreach($aFields as $val){ - if(!isset($aParam[$val])){ - continue; - } - if(is_array($aParam[$val])){ - $aParam[$val] = implode(";",$aParam[$val]); - } - $aUpdateParam[$val] = empty($aParam[$val]) ? '' : addslashes($aParam[$val]); - } - if(empty($aUpdateParam)){ - return json_encode(['status' => 1,'msg' => 'No data currently being processed']); - } - //执行入库 - $aUpdateParam['update_time'] = time(); - $result = Db::name('ai_article')->where('ai_article_id',$iAiArticleId)->limit(1)->update($aUpdateParam); - if($result === false){ - return json_encode(['status' => 4,'msg' => 'UPDATEING AI article failed']); - } - return json_encode(['status' => 1,'msg' => 'No data currently being processed']); - } - - private function wechatGegnerate($aParam = []){ - - //文章ID - $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; - if(empty($iArticleId)){ - return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']); - } - //更新生成状态 - $aParam['is_generate'] = 1; - $aResult = json_decode($this->updateAiArticle($aParam),true); - $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; - $sMsg = empty($aResult['msg']) ? '更新状态失败' : $aResult['msg']; - if($iStatus == 1){ - //四小时后推送上传素材并推送草稿箱 - $iDelaySeconds = 4 * 3600; // 4小时的秒数 - Queue::later($iDelaySeconds,'app\api\job\WechatMaterial@fire', ['article_id' => $iArticleId], 'WechatMaterial'); - $sMsg = '文章AI内容生成成功'; - }else{ - $iStatus = 2; - } - - //插入日志记录 - $oMaterial = new Material; - $aLogInfo = ['article_id' => $iArticleId,'type' => 5,'msg' =>$sMsg,'status' => $iStatus,'create_time' => time()]; - $result = json_decode($oMaterial->addWechatLog($aLogInfo),true); - return json_encode($aResult); - } - /** * 从文本中提取被```json```和```包裹的JSON内容并解析 * @param string $text 包含JSON代码块的文本 * @param bool $assoc 是否返回关联数组(默认true) * @return array|object 解析后的JSON数据,失败时返回null */ - private function extractAndParse($text, $assoc = true){ + public function extractAndParse($text, $assoc = true){ // 使用正则表达式提取JSON代码块 preg_match('/```json\s*(\{.*?\})\s*```/s', $text, $matches); diff --git a/application/common/QueueJob.php b/application/common/QueueJob.php index 895917c..d4b194f 100644 --- a/application/common/QueueJob.php +++ b/application/common/QueueJob.php @@ -1,129 +1,345 @@ redis = new \Redis(); - $this->redis->connect($config['host'], $config['port']); - - if (!empty($config['password'])) { - $this->redis->auth($config['password']); - } - - $this->redis->select($config['select']); - // 初始化 Redis 连接 - // $this->redis = Cache::store('redis')->handler(); + $this->QueueRedis = QueueRedis::getInstance(); + $this->lastLogTime = time(); } - // 记录任务开始 - public function addLog($aParam = []) { + /** + * 记录任务开始 + * @param array $aParam + * @return int 日志ID,失败返回0 + */ + public function addLog($aParam = []) + { - //数据处理 - $aField = $this->aField; + $sJobId = empty($aParam['job_id']) ? uniqid() : $aParam['job_id']; + return $sJobId; + + // 数据过滤(只保留必填字段) $aInsert = []; - foreach ($aField as $key => $value) { - if(isset($aParam[$value])){ - $aInsert[$value] = $aParam[$value]; - } + foreach ($this->aField as $field) { + if (isset($aParam[$field])) { + $aInsert[$field] = $aParam[$field]; + } } - $result = 0; - if(!empty($aInsert)){ - $result = DB::name('wechat_queue_logs')->insertGetId($aParam); + + // 补充默认值 + if (!isset($aInsert['create_time'])) { + $aInsert['create_time'] = time(); + } + if (!isset($aInsert['update_time'])) { + $aInsert['update_time'] = $aInsert['create_time']; + } + + try { + return Db::name('wechat_queue_logs')->insertGetId($aInsert); + } catch (\Exception $e) { + $this->log("添加任务日志失败: " . $e->getMessage() . " | 参数: " . json_encode($aInsert, self::JSON_OPTIONS)); + return 0; } - return $result; } - // 记录任务成功 - public function updateLog($aParam = []) { - + /** + * 记录任务状态更新 + * @param array $aParam + * @return bool + */ + public function updateLog($aParam = []) + { + return true; $iLogId = empty($aParam['log_id']) ? 0 : $aParam['log_id']; - if(empty($iLogId)){ + if (empty($iLogId)) { + $this->log("更新日志失败: 缺少log_id"); return false; } - //数据处理 - $aField = $this->aField; + + // 数据过滤 $aUpdate = []; - foreach ($aField as $key => $value) { - if(isset($aParam[$value])){ - $aUpdate[$value] = $aParam[$value]; - } + foreach ($this->aField as $field) { + if (isset($aParam[$field])) { + $aUpdate[$field] = $aParam[$field]; + } + } + + // 强制更新时间 + $aUpdate['update_time'] = time(); + + try { + return Db::name('wechat_queue_logs') + ->where('log_id', $iLogId) + ->limit(1) + ->update($aUpdate) > 0; + } catch (\Exception $e) { + $this->log("更新任务日志失败 [ID:{$iLogId}]: " . $e->getMessage()); + return false; } - unset($aParam['log_id']); - $result = DB::name('wechat_queue_logs')->where('log_id',$iLogId)->limit(1)->update($aUpdate); - return $result; } /** - * 写入Reids 防止1小时内重复操作 + * 设置日志路径并确保目录存在 + * @param string $logPath + * @throws \RuntimeException */ + public function ensureLogDirExists($logPath = '') + { + if (empty($logPath)) { + $error = "日志路径不能为空"; + $this->log($error); + return $error; + } - public function setRedisLabel($aParam = []){ + $this->logPath = $logPath; + $logDir = dirname($this->logPath); - //判断数据是否为空 - if(empty($aParam['redis_key'])){ - return 3; + // 检查并创建目录(处理权限问题) + if (!is_dir($logDir)) { + $oldUmask = umask(0); + $created = mkdir($logDir, 0755, true); + umask($oldUmask); + + if (!$created || !is_dir($logDir)) { + $error = "无法创建日志目录: {$logDir} (权限不足)"; + $this->log($error); + return $error; + } } - //获取值 - $sValue = $this->getRedisLabel($aParam['redis_key']); - if($sValue == $aParam['redis_key']){ - return 4; - } - $result = Cache::set($aParam['redis_key'], $aParam['redis_key'], 3600); - if($result == true){ - return 1; - } - //写入 - return 2; } /** - * 获取Reids值 + * 写入日志到缓冲区 + * @param string $message */ - public function getRedisLabel($sRedisKey = ''){ - if(empty($sRedisKey)){ - return ''; + public function log($message) + { + // 防止缓冲区溢出 + if (count($this->logBuffer) >= 1000) { + $this->flushLog(); + } + + $time = date('H:i:s'); + $this->logBuffer[] = "[$time] $message\n"; + + // 缓冲区满或超时则刷新 + if (count($this->logBuffer) >= 50 || time() - $this->lastLogTime > 10) { + $this->flushLog(); } - return Cache::get($sRedisKey); } + /** + * 刷新日志缓冲区到文件 + */ + public function flushLog() + { + if (empty($this->logBuffer)) { + return; + } - // 使用SETNX原子操作设置锁 - public function setRedisLock($key, $value, $expire) - { - return $this->redis->set($key, $value, ['nx', 'ex' => $expire]); + // 检查日志路径是否设置 + if (empty($this->logPath)) { + $this->logBuffer = []; + return; + } + + // 检查文件大小并处理 + $this->checkAndTruncateLog(); + + $fp = fopen($this->logPath, 'a'); + if ($fp === false) { + // 紧急写入失败日志(避免递归) + $errorMsg = "[" . date('H:i:s') . "] 错误: 无法打开日志文件 {$this->logPath}\n"; + error_log($errorMsg); // 写入系统日志 + $this->logBuffer = []; + return; + } + + try { + // 尝试获取文件锁 + if (flock($fp, LOCK_EX)) { + fwrite($fp, implode('', $this->logBuffer)); + flock($fp, LOCK_UN); + } else { + // 无锁情况下尝试写入 + fwrite($fp, implode('', $this->logBuffer)); + $this->logBuffer[] = "[" . date('H:i:s') . "] 警告: 日志写入未加锁,可能存在冲突风险\n"; + } + } catch (\Exception $e) { + $errorMsg = "[" . date('H:i:s') . "] 错误: 写入日志失败: {$e->getMessage()}\n"; + fwrite($fp, $errorMsg); + } finally { + fclose($fp); + } + + $this->logBuffer = []; + $this->lastLogTime = time(); } - - // 获取Redis值 - public function getRedisValue($key) + + /** + * 检查日志文件大小,超过限制则清空 + */ + public function checkAndTruncateLog() { - return $this->redis->get($key); + if (empty($this->logPath) || !file_exists($this->logPath)) { + return; + } + + // 清除文件状态缓存并获取大小 + clearstatcache(true, $this->logPath); + $fileSize = @filesize($this->logPath); + + if ($fileSize === false) { + $this->log("错误: 无法获取日志文件大小 {$this->logPath}"); + return; + } + + if ($fileSize >= $this->logMaxSize) { + $fp = fopen($this->logPath, 'w'); + if ($fp === false) { + $this->log("错误: 无法清空日志文件 {$this->logPath}"); + return; + } + + try { + if (flock($fp, LOCK_EX)) { + // 二次检查文件大小(避免竞态条件) + clearstatcache(true, $this->logPath); + if (filesize($this->logPath) >= $this->logMaxSize) { + ftruncate($fp, 0); + $this->log("日志文件超过" . $this->formatFileSize($this->logMaxSize) . ",已清空"); + } + flock($fp, LOCK_UN); + } + } catch (\Exception $e) { + $this->log("错误: 清空日志文件失败: {$e->getMessage()}"); + } finally { + fclose($fp); + } + } } - - // 安全释放锁(仅当值匹配时删除) - public function releaseRedisLock($key, $value) + + /** + * 格式化文件大小(字节转人类可读格式) + * @param int $bytes + * @return string + */ + public function formatFileSize($bytes) { - - // 使用Lua脚本确保原子性 - $script = <<redis->eval($script, [$key, $value], 1); + if ($bytes <= 0) { + return '0 B'; + } + + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $unitIndex = min(floor(log($bytes, 1024)), count($units) - 1); + $size = $bytes / pow(1024, $unitIndex); + + return number_format($size, 2) . ' ' . $units[$unitIndex]; } -} -?> \ No newline at end of file + + /** + * 获取重试延迟时间 + * @param string $errorMsg + * @return int + */ + public function getRetryDelay($errorMsg) + { + $delayMap = [ + 'MySQL server has gone away' => 60, + 'timeout' => 30, + 'OpenAI' => 45, + 'network' => 60 + ]; + + foreach ($delayMap as $keyword => $delay) { + if (stripos($errorMsg, $keyword) !== false) { // 不区分大小写匹配 + return $delay; + } + } + + return 10; + } + + /** + * 处理可重试异常 + * @param \Exception $e + * @param int $iLogId + * @param string $sRedisKey + * @param \think\queue\Job $job + */ + public function handleRetryableException($e, $iLogId, $sRedisKey, $job) + { + $sMsg = empty($e->getMessage()) ? '可重试异常' : $e->getMessage(); + $sTrace = empty($e->getTraceAsString()) ? '' : $e->getTraceAsString(); + $this->log("可重试异常: {$sMsg} | 堆栈: {$sTrace}"); + + if ($iLogId > 0) { + $this->updateLog([ + 'log_id' => $iLogId, + 'status' => 2, + 'error' => $sMsg . ':' . $sTrace, + ]); + } + + $this->QueueRedis->finishJob($sRedisKey, 'failed', 3600); + + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->log("超过最大重试次数({$this->maxRetries}),停止重试 | 执行日志:{$sMsg}"); + $job->delete(); + } else { + $delay = $this->getRetryDelay($sMsg); + $this->log("{$delay}秒后重试({$attempts}/{$this->maxRetries}) | 执行日志:{$sMsg}"); + $job->release($delay); + } + } + + /** + * 处理不可重试异常 + * @param \Exception $e + * @param int $iLogId + * @param string $sRedisKey + * @param \think\queue\Job $job + */ + public function handleNonRetryableException($e, $iLogId, $sRedisKey, $job) + { + $sMsg = empty($e->getMessage()) ? '不可重试异常' : $e->getMessage(); + $sTrace = empty($e->getTraceAsString()) ? '' : $e->getTraceAsString(); + $this->log("不可重试异常: {$sMsg} | 堆栈: {$sTrace}"); + + if ($iLogId > 0) { + $this->updateLog([ + 'log_id' => $iLogId, + 'status' => 3, // 3:不可重试失败 + 'error' => $sMsg . ':' . $sTrace, + ]); + } + + $this->QueueRedis->finishJob($sRedisKey, 'failed', 3600); + $this->log("不可重试错误,直接删除任务 | 执行日志:{$sMsg}"); + $job->delete(); + } + + /** + * 析构函数:确保最后日志被写入 + */ + public function __destruct() + { + $this->flushLog(); + } +} \ No newline at end of file diff --git a/application/common/QueueRedis.php b/application/common/QueueRedis.php new file mode 100644 index 0000000..c28faf7 --- /dev/null +++ b/application/common/QueueRedis.php @@ -0,0 +1,265 @@ +config = \think\Config::get('queue'); + $this->connect(); + } + + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new self(); + } + return self::$instance; + } + + private function connect() + { + // 只在首次调用或连接断开时创建连接 + if (!$this->redis) { + $this->redis = new \Redis(); + + // 使用长连接(pconnect)避免频繁创建连接 + $connectMethod = $this->config['persistent'] ? 'pconnect' : 'connect'; + $this->redis->$connectMethod( + $this->config['host'] ?? '127.0.0.1', + $this->config['port'] ?? 6379 + ); + // 始终执行认证(空密码会被 Redis 忽略) + if (!empty($this->config['password'])) { + $this->redis->auth($this->config['password']); + } + + + + $this->redis->select($this->config['select'] ?? 0); + } + return $this->redis; + } + + // 使用 SET 命令原子操作设置锁 + public function setRedisLock($key, $value, $expire) + { + try { + return $this->connect()->set($key, $value, ['nx', 'ex' => $expire]); + } catch (\Exception $e) { + return false; + } + } + + // 设置Redis值 + public function setRedisValue($key, $value, $expire = null) + { + try { + $redis = $this->connect(); + if ($expire) { + return $redis->setex($key, $expire, $value); + } else { + return $redis->set($key, $value); + } + } catch (\Exception $e) { + return false; + } + } + + // 获取Redis值 + public function getRedisValue($key) + { + try { + return $this->connect()->get($key); + } catch (\Exception $e) { + return null; + } + } + + // 安全释放锁(仅当值匹配时删除) + public function releaseRedisLock($key, $value) + { + + // 使用Lua脚本确保原子性 + $script = <<connect(); + $result = $redis->eval($script, [$key, $value], 1); + return $result; + } + + // 获取锁剩余时间 + public function getLockTtl($key) + { + try { + return $this->connect()->ttl($key); + } catch (\Exception $e) { + return -1; + } + } + + // 任务开始时的批量操作 + public function startJob($sRedisKey, $sRedisValue, $expire) + { + try { + $redis = $this->connect(); + // 先尝试设置锁,成功后再设置状态 + if ($redis->set($sRedisKey, $sRedisValue, ['nx', 'ex' => $expire])) { + $redis->set($sRedisKey . ':status', 'processing', $expire); + return true; + } + return false; + } catch (\Exception $e) { + Log::error("Redis批量操作失败: {$e->getMessage()}"); + return false; + } + } + + // 任务结束时的批量操作 +public function finishJob($sRedisKey, $status, $expire) +{ + try { + $redis = $this->connect(); + // 使用Lua脚本确保原子性 + $script = <<eval($script, [$sRedisKey, $status, $expire], 1) === 1; + } catch (\Exception $e) { + Log::error("Redis完成任务失败: {$e->getMessage()}"); + return false; + } +} + + // 记录处理进度 + public function recordProcessingStart($key, $totalQuestions) + { + try { + $redis = $this->connect(); + $redis->hMSet($key, [ + 'status' => 'processing', + 'total' => $totalQuestions, + 'completed' => 0, + 'start_time' => time() + ]); + $redis->expire($key, 10800); // 6小时过期 + return true; + } catch (\Exception $e) { + return false; + } + } + // 多问题按条件拆分成两个队列新增日志记录 + public function recordQuestionProcessingStart($key, $totalQuestions) + { + try { + $redis = $this->connect(); + $redis->hMSet($key, [ + 'status' => 'processing', + 'total' => $totalQuestions, + 'completed' => 0, + 'start_time' => time(), + 'queue_1_completed' => 0, + 'queue_2_completed' => 0 + ]); + $redis->expire($key, 10800); // 6小时过期 + return true; + } catch (\Exception $e) { + return false; + } + } + // 多问题按条件拆分成两个队列更新日志记录 + public function updateQuestionProcessingProgress($key, $sKeyName) + { + $redis = $this->connect(); + // 获取总数 + $total = $redis->hGet($key, 'total'); + if (!$total) { + return 0; + } + if(!empty($sKeyName)){ + $redis->hIncrBy($key, $sKeyName, 1); + } + //获取每个队列完成数量 + $queue_1_completed = $redis->hGet($key, 'queue_1_completed'); + $queue_2_completed = $redis->hGet($key, 'queue_2_completed'); + $completed = $queue_1_completed + $queue_2_completed; + if($completed > $total){ + return 100; + } + // 计算进度 + $iProgress = round(($completed / $total) * 100, 2); + // 事务更新多个字段 + $redis->hSet($key, 'completed', $completed); + $redis->hSet($key, 'progress', $iProgress); + if ($iProgress >= 100) { + $redis->hSet($key, 'status', 'completed'); + $redis->hSet($key, 'end_time', time()); + } + return $iProgress; + + } + + // 更新处理进度 + public function updateProcessingProgress($key, $completed) + { + $redis = $this->connect(); + // 获取总数 + $total = $redis->hGet($key, 'total'); + if (!$total) { + return 0; + } + + // 计算进度 + $iProgress = round(($completed / $total) * 100, 2); + // 事务更新多个字段 + $redis->hSet($key, 'completed', $completed); + $redis->hSet($key, 'progress', $iProgress); + if ($iProgress >= 100) { + $redis->hSet($key, 'status', 'completed'); + $redis->hSet($key, 'end_time', time()); + } + return $iProgress; + + } + + // 保存分块进度 + public function saveChunkProgress($key, $chunkIndex, $content) + { + $redis = $this->connect(); + $redis->hSet($key, "chunk_{$chunkIndex}", $content); + // 确保设置过期时间(如果已设置则忽略) + $redis->expire($key, 10800); + return true; + + } + + public function getJobStatus($jobId) + { + try { + return $this->getRedisValue($jobId . ':status'); + } catch (\Exception $e) { + return null; + } + } + + public function getConnectionStatus() + { + try { + return $this->connect()->ping(); + } catch (\Exception $e) { + return false; + } + } + +} +?> \ No newline at end of file diff --git a/application/common/Reviewer.php b/application/common/Reviewer.php index 99ae67e..a5b2182 100644 --- a/application/common/Reviewer.php +++ b/application/common/Reviewer.php @@ -191,6 +191,8 @@ class Reviewer //获取审稿人信息 // 获取时间点 $iSixMonth = strtotime(date('Y-m-d 00:00:00',strtotime('-6 months'))); + // 计算10天之前的时间戳 + $iTeenDaysLater = strtotime('-10 days'); // 先查询符合专业条件的审稿人ID $aWhere = ['state' => 0, 'major_id' => ['in', $aMajorId]]; @@ -213,8 +215,11 @@ class Reviewer if(!empty($aParam['field'])){//根据领域搜索 $aWhere['field'] = ['like',"%" . $aParam["field"] . "%"]; } - $sCompanyQuery = Db::name('user_reviewer_info')->field('reviewer_id,technical,country,introduction,company,field')->where($aWhere)->buildSql(); - + $sCompanyQuery = Db::name('user_reviewer_info')->field('reviewer_id,technical,country,introduction,company,field,last_invite_time') + ->where($aWhere)->where(function($query) use ($iTeenDaysLater) { + $query->where('last_invite_time', '<', $iTeenDaysLater) + ->whereOr('last_invite_time', '=', 0); + })->buildSql(); // 主查询条件 $iSize = empty($aParam['size']) ? $this->iReviewerNum : $aParam['size'];//每页显示条数 $iPage = empty($aParam['page']) ? 1 : $aParam['page'];// 当前页码 @@ -258,7 +263,7 @@ class Reviewer ->field($sSelect) ->fieldRaw(" CASE - WHEN t_reviewer_to_journal.is_yboard = 1 AND t_reviewer_to_journal.ctime < {$iSixMonth} THEN 1 + WHEN t_reviewer_to_journal.is_yboard = 1 AND t_reviewer_to_journal.ctime < {$iSixMonth} THEN 1 ELSE 2 END AS new_level ") @@ -385,20 +390,31 @@ class Reviewer //数据处理 $aInsert = []; + $iNowTime = time(); foreach ($aReviewerId as $value) { if(in_array($value, $aReviewer)){ continue; } - $aInsert[] = ['reviewer_id' => $value,'article_id' => $iArticleId,'editor_act' => 1,'ctime' => time(),'state' => 5,'invited_time' => time()]; + $aInsert[] = ['reviewer_id' => $value,'article_id' => $iArticleId,'editor_act' => 1,'ctime' => time(),'state' => 5,'invited_time' => $iNowTime]; } if(empty($aInsert)){ return json_encode(['status' => 3,'msg' => 'Reviewers have been invited to review, please do not repeat the process']); } //插入审稿人数据 + Db::startTrans(); $result = Db::name('article_reviewer')->insertAll($aInsert); if($result === false){ return json_encode(['status' => 4,'msg' => 'Reviewer data insertion failed:'.json_encode($aInsert)]); } + //更新审稿人最后一次审稿时间 + $aReviewerId = array_column($aInsert, 'reviewer_id'); + $aUpdate = ['last_invite_time'=>$iNowTime]; + $aWhere = ['reviewer_id' => ['in',$aReviewerId]]; + $updateResult = Db::name('user_reviewer_info')->where($aWhere)->limit(count($aReviewerId))->update($aUpdate); + if($updateResult === false){ + return json_encode(['status' => 5,'msg' => 'Invitation time update failed:'.json_encode($aReviewerId)]); + } + Db::commit(); return json_encode(['status' => 1,'msg' => 'Reviewer data insertion successful, execute email queue']); }