From f16ac88ec403ce67767877b046eac1bfe526912e Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 15 Aug 2025 15:17:52 +0800 Subject: [PATCH] =?UTF-8?q?AI=E7=9B=B8=E5=85=B3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Aiarticle.php | 4 +- application/api/controller/Aireview.php | 320 ++++---- application/common/Aireview.php | 27 +- application/common/Article.php | 53 +- application/common/OpenAi.php | 992 +++++++++++++---------- application/common/QueueJob.php | 193 ++++- 6 files changed, 942 insertions(+), 647 deletions(-) diff --git a/application/api/controller/Aiarticle.php b/application/api/controller/Aiarticle.php index 4e3020e..6c77c19 100644 --- a/application/api/controller/Aiarticle.php +++ b/application/api/controller/Aiarticle.php @@ -1103,14 +1103,14 @@ class Aiarticle extends Base $aLog = empty($aLog['data']) ? [] : $aLog['data']; if(empty($aLog)){ $iStatus = 6;//未上传 - return json_encode(['status' => $iStatus,'msg' => 'Material not uploaded']); + // return json_encode(['status' => $iStatus,'msg' => 'Material not uploaded']); } if(!empty($aLog) && $aLog['status_name'] == 'processing'){ $iStatus = 3;//素材上传中 } if(!empty($aLog) && $aLog['status_name'] == 'fail'){ $iStatus = 5;//失败 - return json_encode(['status' => $iStatus,'msg' => empty($aLog['msg']) ? 'fail' : $aLog['msg']]); + // return json_encode(['status' => $iStatus,'msg' => empty($aLog['msg']) ? 'fail' : $aLog['msg']]); } if(!empty($aLog) && $aLog['status_name'] == 'finish'){ $iStatus = 4;//素材上传完成 diff --git a/application/api/controller/Aireview.php b/application/api/controller/Aireview.php index 1fb8a4d..3a8ba46 100644 --- a/application/api/controller/Aireview.php +++ b/application/api/controller/Aireview.php @@ -21,193 +21,213 @@ class Aireview extends Base * @param model 接口模型 * @param stream 是否流式输出 true是false否 */ - public function review(){ + public function review($aParam = []){ //获取参数 - $aParam = $this->request->post(); + $aParam = empty($aParam) ? $this->request->post() : $aParam; $iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id']; if(empty($iArticleId)){ return json_encode(array('status' => 2,'msg' => 'Please select an article' )); } + + //问题等级 + $sQuestionLevel = empty($aParam['question_level']) ? 'C' : $aParam['question_level']; + //需要处理的AI问题字段 + $sQuestionFields = empty($aParam['queue_fields']) ? 'hotspot' : $aParam['queue_fields']; + $aQuestionFields = explode(',', $sQuestionFields); //查询文章及AI审稿 $aWhere = ['article_id' => $aParam['article_id']]; $aResult = json_decode($this->get($aWhere),true); - //文章信息 $aArticle = empty($aResult['article_data']) ? [] : $aResult['article_data']; if(empty($aArticle)){ - json_encode(array('status' => 3,'msg' => 'No articles requiring review were found' )); + return json_encode(array('status' => 3,'msg' => 'No articles requiring review were found' )); } - - //AI审稿信息 + // //AI审稿信息 $aAiReview = empty($aResult['data']) ? [] : $aResult['data']; - if(!empty($aAiReview)){ + if(!empty($aAiReview['is_finish']) && $aAiReview['is_finish'] == 1){ return json_encode(array('status' => 1,'msg' => 'AI has been reviewed','data' => $aAiReview)); } - //根据期刊ID查询期刊信息 - $aJournal = Db::table('t_journal')->field('title,scope,issn,journal_id')->where('journal_id',$aArticle['journal_id'])->find(); - if(empty($aJournal)){ - return json_encode(array('status' => 4,'msg' => 'This article is not associated with a journal' )); - } - //实例化公共方法 - $oArticle = new Article; - if($aArticle['state'] > 4 ){ - //查询文章内容 - $aWhere['type'] = 0; - $aWhere['content'] = ['<>','']; - $aWhere['state'] = 0; - $aArticleMain = Db::table('t_article_main')->where($aWhere)->column('content'); - //查询参考文献 - $aWhere = ['state' => ['in',[0,2]],'article_id' => $iArticleId]; - $aProductionArticle = Db::name('production_article')->field('p_article_id')->where($aWhere)->find(); - if(!empty($aProductionArticle)){ - $aWhere = ['state' => 0,'p_article_id' => $aProductionArticle['p_article_id']]; - $aRefer = Db::name('production_article_refer')->field('refer_content')->where($aWhere)->column('refer_content'); - $aArticleMain = empty($aRefer) ? $aArticleMain : array_merge($aRefer,$aArticleMain); + $oHelperFunction = new \app\common\HelperFunction; + //根据期刊ID查询期刊信息 + $aJournalIssn = []; + $sIssn = $sScope = ''; + if(in_array('journal_scope', $aQuestionFields)){ + $aWhere = ['journal_id' => $aArticle['journal_id'],'state' => 0]; + $aJournal = Db::table('t_journal')->field('title as targeted_journals,scope as journal_scope,issn')->where($aWhere)->find(); + if(empty($aJournal)){ + return json_encode(array('status' => 4,'msg' => 'This article is not associated with a journal' )); } - }else{ - - $aFile = json_decode($oArticle->getFileContent(['article_id' => $iArticleId]),true); - $aFile = empty($aFile['data']) ? [] : $aFile['data']; - $aArticleMain = empty($aFile['mains']) ? [] : $aFile['mains']; + $aArticle += $aJournal; + $sIssn = empty($aJournal['issn']) ? '' : $aJournal['issn']; + $sScope = empty($aJournal['journal_scope']) ? '' : $aJournal['journal_scope']; + $aJournalIssn = array_merge($aJournalIssn,[$sIssn]); } - //获取提问AI的内容 - $aSearch = []; - $title = empty($aArticle['title']) ? '' : $aArticle['title'];//简介 - $abstrart = empty($aArticle['abstrart']) ? '' : $aArticle['abstrart'];//简介 - $keywords = empty($aArticle['keywords']) ? '' : $aArticle['keywords'];//关键词 - $aSearch['{title}'] = $title; - $aSearch['{abstrart}'] = $abstrart; - $aSearch['{keywords}'] = $keywords; - //文章内容 - $sContent = empty($aArticleMain) ? '' : implode('', array_unique($aArticleMain)); - $sContent = preg_replace('/[\r\n]+/', '', $sContent); - $sContent = preg_replace('/ +/', ' ', $sContent); // 合并连续空格 - $aSearch['{content}'] = $sContent; - $aSearch['{journal_name}'] = empty($aJournal['title']) ? '' : $aJournal['title'];//期刊名 - if($aJournal['journal_id'] == 1){ - $aSearch['{journal_name}'] = '传统医学研究'; + if(in_array('other_journal', $aQuestionFields)){ + $aWhere = ['journal_id' => ['<>',$aArticle['journal_id']],'state' => 0]; + $aJournalOther = Db::table('t_journal')->field('title as targeted_journals,scope as journal_scope,issn')->where($aWhere)->select(); + if(!empty($aJournalOther)){ + $aJournalIssn = array_merge($aJournalIssn,array_column($aJournalOther, 'issn')); + } + } + //查询期刊范围 + if(!empty($aJournalIssn)){ + //查询期刊内容 + $oArticle = new Article; + $aJournalPaperArt = json_decode($oArticle->getJournalPaperArt(['issn' => $aJournalIssn]),true); + $aJournalPaperArt = empty($aJournalPaperArt['data']) ? [] : $aJournalPaperArt['data']; + $aArticle['journal_scope'] = empty($aJournalPaperArt[$sIssn]) ? $sScope : $oHelperFunction->filterAllTags($aJournalPaperArt[$sIssn]); + if(!empty($aJournalPaperArt[$sIssn])){ + unset($aJournalPaperArt[$sIssn]); + } + $aArticle['other_journal'] = empty($aJournalPaperArt) ? '' : json_encode($aJournalPaperArt,JSON_UNESCAPED_UNICODE); } - //查询期刊内容 - $aJournalPaperArt = json_decode($oArticle->getJournalPaperArt($aJournal),true); - $sJournalContent = empty($aJournalPaperArt['data']) ? '' : implode('', $aJournalPaperArt['data']); - $sJournalContent = empty($sJournalContent) ? $aJournal['scope'] : $sJournalContent; - $aSearch['{scope}'] = $sJournalContent;//期刊范围 - //获取重要维度的问答信息 + //获取文章的领域 + if($sQuestionFields == 'hotspot'){ + $oArticle = new Article; + $aArticleField = $oArticle->getArticleField(['article_id' => $iArticleId]); + $aArticleField = empty($aArticleField['data']) ? [] : $aArticleField['data']; + if(empty($aArticleField)){ + return json_encode(array('status' => 4,'msg' => 'Domain where article not found:'.$sQuestionFields)); + } + //数据处理 + $aArticleFieldData = []; + foreach ($aArticleField as $key => $value) { + $aArticleFieldData[] = getMajorStr($value['major_id']); + } + $aArticle['hotspot'] = json_encode($aArticleFieldData,JSON_UNESCAPED_UNICODE); + } + //获取提问AI话术 $oOpenAi = new OpenAi; - //请求OPENAI接口-重要维度单独请求获取答案 - $aSearch['question'] = 'aArticleImportantPrompt'; - $aSearch['open_ai_id'] = $iArticleId; - $aResult = json_decode($oOpenAi->curlMultiOpenAIImportant($aSearch),true); - $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; - if($iStatus != 1){ - return json_encode($aResult); - } - //处理返回信息 - $aData = empty($aResult['data']) ? [] : $aResult['data']; - if(empty($aData)){ - return json_encode(['status' => 6,'msg' => 'OPENAI returns empty content']); - } - //执行数据入库 - $aData['article_id'] = $iArticleId; - $aData['journal_id'] = $aArticle['journal_id']; - $aResult = $this->addAiReview($aData); - if(empty($aResult['data'])){ - return json_encode($aResult); + //获取文章内容 + $aChunkContent = []; + if(!in_array($sQuestionFields, ['journal_scope','other_journal','hotspot'])){ + //实例化公共方法 + $oArticle = new Article; + if($aArticle['state'] > 4 ){ + //查询文章内容 + $aWhere['type'] = 0; + $aWhere['content'] = ['<>','']; + $aWhere['state'] = 0; + $aArticleMain = Db::table('t_article_main')->where($aWhere)->column('content'); + //查询参考文献 + $aWhere = ['state' => ['in',[0,2]],'article_id' => $iArticleId]; + $aProductionArticle = Db::name('production_article')->field('p_article_id')->where($aWhere)->find(); + if(!empty($aProductionArticle)){ + $aWhere = ['state' => 0,'p_article_id' => $aProductionArticle['p_article_id']]; + $aRefer = Db::name('production_article_refer')->field('refer_content')->where($aWhere)->column('refer_content'); + $aArticleMain = empty($aRefer) ? $aArticleMain : array_merge($aRefer,$aArticleMain); + } + }else{ + $aFile = json_decode($oArticle->getFileContent(['article_id' => $iArticleId]),true); + $aFile = empty($aFile['data']) ? [] : $aFile['data']; + $aArticleMain = empty($aFile['mains']) ? [] : $aFile['mains']; + } + $sContent = empty($aArticleMain) ? '' : implode("", array_unique($aArticleMain)); + if(empty($sContent)){ + return json_encode(array('status' => 4,'msg' => 'No article content found:'.$sQuestionFields)); + } + + + //处理内容 + //过滤字符串 + $sContent = $oHelperFunction->filterAllTags($sContent); + //将文章内容拆分参考文献 + $aDealContent = $this->dealContent($sContent); + $sBefore= empty($aDealContent['before']) ? '' : $aDealContent['before']; + $sReference = empty($aDealContent['after']) ? '' : $aDealContent['after']; + if(in_array($sQuestionFields, ['attribute'])){//科学性和创新性 + $aChunkContent = $oHelperFunction->splitContent($sContent); + } + if($sQuestionFields == 'reference'){//参考文献 + if(empty($sReference)){ + return json_encode(array('status' => 5,'msg' => 'Reference not matched successfully' )); + } + $aChunkContent = [$sReference]; + } + if(!in_array($sQuestionFields, ['attribute','reference'])){ + $aChunkContent = $oHelperFunction->splitContent($sBefore); + } + $aArticle['reference'] = $sReference; + $aArticle['content'] = empty($aChunkContent[0]) ? '' : $aChunkContent[0]; + if(count($aChunkContent) > 1){ + $aArticle['content'] = $aChunkContent; + } } - //请求OPENAI接口-非重要维度一次请求获取答案 - //获取提示词 - $aMessage = $oOpenAi->buildReviewPromptUnimportant($aSearch); + //内容处理 + $aArticle['queue_fields'] = $sQuestionFields; + $aArticle['question_level'] = $sQuestionLevel; + if(count($aChunkContent) <= 1){ + $aMessage = $oOpenAi->buildReviewPrompt($aArticle); + }else{ + $aMessage = $oOpenAi->buildReviewPromptChunk($aArticle); + } 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); - //处理返回信息 - $aData = empty($aResult['data']) ? [] : $aResult['data']; - if(empty($aData)){ - return json_encode(['status' => 6,'msg' => empty($aResult['msg']) ? 'OPENAI returns empty content' : $aResult['msg']]); + //请求OPENAI接口 + $aParam = [ + 'messages' => $aMessage, + 'model' => empty($aParam['api_model']) ? 'DeepSeek-V3' : $aParam['api_model'], + 'article_id' => empty($aArticle['article_id']) ? 0 : $aArticle['article_id'], + 'queue_fields' => $sQuestionFields, + 'question_level' => $sQuestionLevel, + 'journal_id' => empty($aArticle['journal_id']) ? 0 : $aArticle['journal_id'] + ]; + if(count($aChunkContent) > 1){ + return $oOpenAi->articleReviewDealChunk($aParam); + }else{ + return $oOpenAi->articleReviewDeal($aParam); } - //执行数据入库 - $aData['article_id'] = $iArticleId; - $aData['journal_id'] = $aArticle['journal_id']; - $aResult = $this->addAiReview($aData); - return json_encode($aResult); - } - /** - * @title AI审核内容入库 - * @param article_id 文章ID - * @param content 内容 + * @title 将文章内容拆分参考文献 + * @param sContent 文章内容 */ - private function addAiReview($aParam = array()){ - - $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; - if(empty($iArticleId)){ - return ['status' => 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']; - foreach ($aParam as $key => $value) { - if(empty($value)){ - continue; - } - if(is_array($value)){ - if(!empty($value['assessment'])){ - $sField = $key.'_'.'assessment'; - $aInsert[$sField] = empty($value['assessment']) ? '' : htmlspecialchars($value['assessment']); - } - 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']; + private function dealContent($sContent = ''){ + if(empty($sContent)){ + return []; } - //查询文章审核内容-判断新增或修改 - $aWhere = ['article_id' => $iArticleId,'journal_id' => $iJournalId]; - $aAiReview = Db::table('t_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; + $pattern = '/]*?>/i'; + $sContent = preg_replace($pattern, '', $sContent); + + // 关键修复:将分隔符从 / 改为 #,避免与 < > 冲突 + $pattern = '#Reference|Reference:|References:|References|References|References:#'; + + // 查找标识在文本中的位置 + preg_match($pattern, $sContent, $matches, PREG_OFFSET_CAPTURE); + + if (empty($matches)) { + // 未找到任何标识 + $before = $sContent; + $after = ''; + } else { + // 截取标识前后内容 + $marker = $matches[0][0]; + $markerStart = $matches[0][1]; + $before = substr($sContent, 0, $markerStart); + $after = substr($sContent, $markerStart + strlen($marker)); } - 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']; + if(empty($after)){ + $lastPos = strrpos($sContent, 'Reference'); + if ($lastPos === false) { + // 未找到 "Reference",返回原字符串和空 + return [$sContent, '']; } - $aAiReview = Db::table('t_article_ai_review')->where($aWhere)->find(); - $aResult['data'] = $aAiReview; - return $aResult; + + // 拆分:关键词之前的内容 + $before = substr($sContent, 0, $lastPos); + // 关键词及之后的内容(包含关键词本身) + $after = substr($sContent, $lastPos); } - return ['status' => 6,'msg' => 'illegal request']; + return ['before' => $before,'after' => $after,'content' => $sContent]; } /** @@ -223,7 +243,9 @@ class Aireview extends Base } //查询文章 - $aArticle = Db::name('article')->field('article_id,abstrart,keywords,journal_id,title,state')->where('article_id',$aParam['article_id'])->find(); + $oArticle = new Article; + $aArticle = json_decode($oArticle->get($aParam),true); + $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; if(empty($aArticle)){ return json_encode(array('status' => 3,'msg' => 'No articles requiring review were found' )); } diff --git a/application/common/Aireview.php b/application/common/Aireview.php index 2aedceb..d654580 100644 --- a/application/common/Aireview.php +++ b/application/common/Aireview.php @@ -21,30 +21,22 @@ class Aireview if(empty($iJournalId)){ return ['status' => 2,'msg' => 'The journal to which the article belongs cannot be empty']; } + + $oArticle = new \app\common\Article; //返回数组 $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']; + $aFields = ['journal_scope_assessment','journal_scope_explanation','other_journal_assessment','other_journal_issn','other_journal_explanation','attribute_assessment','attribute_explanation','contradiction_assessment','contradiction_explanation','unreasonable_assessment','unreasonable_explanation','ethics_assessment','ethics_explanation','academic_assessment','academic_explanation','conclusion_assessment','conclusion_explanation','fund_number','hotspot','submit_direction','references_past_three','references_num','references_past_five','references_ratio_JCR1','references_ratio_JCR2','registration_assessment','registration_explanation','cite_rate','article_field_assessment','article_field_explanation','is_finish','article_id','journal_id']; + $oHelperFunction = new \app\common\HelperFunction; 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(!in_array($key, $aFields)){ + continue; } + $value = is_array($value) ? json_encode($value) : $value; + $aInsert[$key] = $oHelperFunction->func_safe($value); } if(empty($aInsert)){ return ['status' => 3,'msg' => 'Data is empty']; @@ -71,8 +63,7 @@ class Aireview 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; + $aResult['data'] = $aInsert; return $aResult; } return ['status' => 6,'msg' => 'illegal request']; diff --git a/application/common/Article.php b/application/common/Article.php index ac6a529..bf7c741 100644 --- a/application/common/Article.php +++ b/application/common/Article.php @@ -3,6 +3,7 @@ namespace app\common; use think\Db; use app\common\OpenAi; use app\common\Aireview; +use app\common\HelperFunction; class Article { //JAVA接口 @@ -10,7 +11,9 @@ class Article //官网文件地址 protected $sFileUrl = "https://submission.tmrjournals.com/public/"; //Ai地址 - protected $sAiUrl = "http://125.39.141.154:10002"; + protected $sAiUrl = "http://chat.taimed.cn/v1/chat/completions"; + //tmr + protected $sTmrUrl = "http://journalapi.tmrjournals.com/public/index.php";//"http://zmzm.journal.dev.com/"; // /** * 获取文章文件内容 */ @@ -48,7 +51,8 @@ class Article //接口获取期刊内容 $sUrl = $this->sTmrUrl."/api/Supplementary/getJournalPaperArt"; $aParam = ['issn' => $sIssn]; - $aResult = object_to_array(json_decode(myPost($sUrl,$aParam),true)); + + $aResult = object_to_array(json_decode(myPost1($sUrl,$aParam),true)); return json_encode($aResult); } /** @@ -83,6 +87,8 @@ class Article $aFields = ['article_id','article_type','media_type','journal_id','journal_issn','title_english','title_chinese','covered','research_method','digest','research_background','overview','summary','conclusion','is_generate']; $sFiled = ''; $aUpdateParam = []; + + $oHelperFunction = new HelperFunction; foreach($aFields as $val){ if(!isset($aParam[$val])){ continue; @@ -90,7 +96,7 @@ class Article if(is_array($aParam[$val])){ $aParam[$val] = implode(";",$aParam[$val]); } - $aUpdateParam[$val] = empty($aParam[$val]) ? '' : $this->func_safe($aParam[$val]); + $aUpdateParam[$val] = empty($aParam[$val]) ? '' : $oHelperFunction->func_safe($aParam[$val]); } if(empty($aUpdateParam)){ return json_encode(['status' => 1,'msg' => 'No data currently being processed']); @@ -119,11 +125,11 @@ class Article 你是一位资深的医学期刊学术评审专家,负责严谨、客观地评估学术文章。返回格式必须严格遵循以下JSON结构!请根据文章的标题和摘要从目标期刊下所有领域中筛选出符合文章的领域'; $sSysMessagePrompt .= json_encode([ "article_field" => [ - "assessment" => [ + "article_field_assessment" => [ "major_id" => "领域ID多个,分隔", "major_name" => "领域名称多个,分隔" ], - "explanation" =>"请详细解释说明.请返回中文解释!" + "article_field_explanation" =>"请详细解释说明.请返回中文解释!" ] ],JSON_UNESCAPED_UNICODE); //组装问题 @@ -155,9 +161,9 @@ class Article //获取文章领域 $aArticleField = $this->getArticleField($aWhere); - if(!empty($aArticleField['data'])){ - return json_encode(array('status' => 4,'msg' =>'The article has been added to the field' )); - } + // 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)){ @@ -204,14 +210,15 @@ class Article } //数据处理 - $aData = $oOpenAi->extractAndParse($aData); + $oHelperFunction = new HelperFunction; + $aData = $oHelperFunction->extractAndParse($aData); if(empty($aData['data'])){ return json_encode($aData); } //关联文章领域 - $aData = $aData['data']; - $aMaJorId = empty($aData['article_field']['assessment']) ? [] : $aData['article_field']['assessment']; + $aData = empty($aData['data']['article_field']) ? [] : $aData['data']['article_field']; + $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]); @@ -360,6 +367,7 @@ class Article $aFields = ['article_id','title','content','current_am_id','next_am_id','ami_id']; $sFiled = ''; $aUpdateParam = []; + $oHelperFunction = new HelperFunction; foreach($aFields as $val){ if(!isset($aParam[$val])){ continue; @@ -367,7 +375,7 @@ class Article if(is_array($aParam[$val])){ $aParam[$val] = implode(";",$aParam[$val]); } - $aUpdateParam[$val] = empty($aParam[$val]) ? '' : $this->func_safe($aParam[$val]); + $aUpdateParam[$val] = empty($aParam[$val]) ? '' : $oHelperFunction->func_safe($aParam[$val]); } if(empty($aUpdateParam)){ return json_encode(['status' => 1,'msg' => 'No data currently being processed']); @@ -384,26 +392,5 @@ class Article } return json_encode(['status' => 1,'msg' => 'success']); } - /** - * 字符串过滤 - * @param $messages 内容 - * @param $model 模型类型 - */ - private function func_safe($data,$ignore_magic_quotes=false){ - if(is_string($data)){ - $data=trim(htmlspecialchars($data));//防止被挂马,跨站攻击 - if(($ignore_magic_quotes==true)||(!get_magic_quotes_gpc())){ - $data = addslashes($data);//防止sql注入 - } - return $data; - }else if(is_array($data)){//如果是数组采用递归过滤 - foreach($data as $key=>$value){ - $data[$key]=func_safe($value); - } - return $data; - }else{ - return $data; - } - } } diff --git a/application/common/OpenAi.php b/application/common/OpenAi.php index 238a2f6..c30f183 100644 --- a/application/common/OpenAi.php +++ b/application/common/OpenAi.php @@ -5,6 +5,7 @@ use think\Db; use think\Queue; use app\common\Article; use app\common\QueueRedis; +use app\common\HelperFunction; class OpenAi { protected $sApiKey = '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'; @@ -22,51 +23,6 @@ class OpenAi protected $sFileUrl = "https://submission.tmrjournals.com/public/"; //官网接口地址 protected $sTmrUrl = "http://journalapi.tmrjournals.com/public/index.php";//"http://zmzm.journal.dev.com/";//; - protected $aArticleImportantPrompt = [ - "journal_scope" => [ - 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 - 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ - "journal_scope": { - "assessment": "是/否", - "explanation": "请详细解释说明" - } - }', - "criteria" => "根据文章的标题:{title};摘要:{abstrart}以及期刊范围:{scope}来判断文章是否符合目标期刊{journal_name}" - ], - "attribute" => [ - 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 - 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ - "attribute": { - "assessment": "是/否", - "explanation": "请总结归纳分析" - } - }', - "criteria" => "请结合以下几点【研究内容的原创性:论文中的研究内容是否与已有的研究重复?是否在同样的领域提出了类似的结论,但在方法或结果上有所创新?如果有,作者是否清楚地解释了如何与之前的研究不同,或者如何在原有基础上进行扩展或改进?如果是综述文章,汇总并综合最新的研究成果,尤其是近几年内的重要发现,展示领域内最新的进展成果。作者可以识别出未被充分讨论的问题或提出新的研究问题,而不是简单文献堆砌。文章中的图表创新能否将信息的清晰呈现,方便读者理解复杂研究问题。论文方法创新性评估要点:是否采用了新的实验模型或创新的实验设计,能有效解决当前研究中的难点或空白?是否有合理的对照组和多组实验设计,确保研究结果的可靠性?是否使用了当前前沿的技术(如高通量测序、CRISPR基因编辑等),提高了实验精度或数据分析能力?是否结合了跨学科的方法(如生物信息学、人工智能等)?是否应用了多种验证手段或统计方法,确保结果的可信度?是否通过细胞实验、动物模型等多重验证,确保实验结果的可靠性?结论与数据的创新性:研究结论是否提出了新观点或新见解?是否提供了新的实验数据或观察结果,能够突破当前的研究局限?例如,发现了新的生物标志物,或对已知生物通路的作用机制提供了全新的解释】评估文章内容{content}是否有科学前沿性和创新性?" - ] - //, - // "contradiction" => [ - - // 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 - // 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ - // "contradiction": { - // "assessment": "是/否", - // "explanation": "请引用具体段落说明矛盾之处" - // } - // }', - // "criteria" => "根据文章内容{content}分析是否前后矛盾或存在逻辑不一致的问题?" - // ], - // "unreasonable" => [ - - // 'system' => '你是一位资深的学术评审专家,负责严谨、客观地评估学术文章。 - // 请针对问题提供客观、专业的评估,并给出简要的理由。请返回中文解释!返回格式必须严格遵循以下JSON结构:{ - // "unreasonable": { - // "assessment": "是/否", - // "explanation": "包括实验设计、数据分析、结论推导等方面的问题" - // } - // }', - // "criteria" => "根据文章内容{content}分析是否有明显的不合理之处?" - // ] - ]; //公微问题模版 protected $aWechatQuestion = [ @@ -90,39 +46,81 @@ class OpenAi ] ]; //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" => "结合内容分析发表后被引用的概率" + private $aReviewQuestion = [ + 'system_message' => '您是一位资深的医学期刊学术评审专家,请负责严谨、客观地评估学术文章。请根据提供的医学论文信息,严格按要求生成内容[中文、可解析的JSON结构]:', + 'A' => [ + 'journal_scope' => '结合标题:title摘要:abstrart以及期刊范围journal_scope来判断文章是否符合目标期刊targeted_journals', + 'other_journal' =>'结合标题:title摘要:abstrart从期刊列表other_journal中根据期刊范围筛选出最符合的期刊【小于等于1个】' + ], + 'B' => [ + 'ethics' => '分析content', + 'registration' => '分析content', + 'academic' => '分析content是否存在学术不端问题【包括但不限于:1. 抄袭:是否存在未经引用的重复内容2. 数据作假:数据是否矛盾、不合理或无来源3. 图片伪造:是否提及可疑的图片/图表(如无原始数据支持)】', + 'contradiction' => '分析content是否存在前后矛盾或逻辑不一致的问题【包括但不限于1. 性别与疾病不匹配(如女性疾病出现男性患者、男性疾病出现女性患者)2. 数量不一致(如病人数量、动物数量在不同位置描述冲突)3. 种类不一致(如实验动物种类前后描述不同)4. 其他明显的逻辑矛盾(如时间线冲突、因果关系矛盾)】', + 'fund_number' => '分析content', + ], + 'C' => [ + 'attribute' => '分析content', + 'hotspot' => '结合标题:title摘要:abstrart从领域列表hotspot分析是否包含热点领域', + 'reference' => '分析reference参考文献数量', ] ]; + private $aScopeReturn = [ + 'journal_scope' => [ + 'journal_scope_assessment' => '是/否', + 'journal_scope_explanation' => '请详细解释说明' + ], + 'other_journal' => [ + "other_journal_assessment" => "是/否", + "other_journal_issn" => "期刊issn", + 'other_journal_explanation' => '请详细解释说明' + ], + 'ethics' => [ + 'ethics_assessment' => "内容是否存在伦理号缺失或明显伦理问题[返回是/否]", + 'ethics_explanation' => "1.[解释说明是否需要伦理号]2.[分析内容是否存在伦理号缺失或明显伦理问题]", + ], + 'registration' => [ + 'registration_assessment' => "是否缺失临床注册号和知情同意书[返回是/否]", + 'registration_explanation' => "1.[解释说明是否需要临床注册号和知情同意书]2.[分析内容是否存在临床注册号和知情同意书]", + ], + 'academic' => [ + 'academic_assessment' => "是/否", + 'academic_explanation' => "解释说明", + ], + 'contradiction' => [ + 'contradiction_assessment' => "是/否", + 'contradiction_explanation' => "解释说明", + ], + 'fund_number' => [ + 'fund_number' => "1.[内容是否有基金号]2.[解释说明]" + ], + 'attribute' => [ + 'attribute_assessment' => "内容是否有科学性和创新性[包括但不限于科学性(结论是否科学、参考文献是否新颖等);创新性(结论与当前研究水平相比是否有明显突破、参考文献的时间)][返回是/否]", + 'attribute_explanation' => "1.科学性[结论是否科学、参考文献是否新颖等]2.创新性[结论与当前研究水平相比是否有明显突破、参考文献的时间]", + ], + 'hotspot' => [ + 'hotspot' => "[逐个判断领域是否为热点领域并解释说明[返回格式:领域[解释说明],多个;分隔]]" + ], + 'reference' => [ + 'references_num' => "统计文章参考文献的数量[返回格式数量]", + 'references_past_three' => "统计内容里近3年的参考文献的数量及所占比例[返回格式数量/占比]", + 'references_past_five' => "统计内容里近5年的参考文献的数量及所占比例[返回格式数量/占比]", + 'references_ratio_JCR1' => "根据2024JCR最新分区统计属于JCR 1区的数量及比例[返回格式数量/占比]", + 'references_ratio_JCR2' => "根据2024JCR最新分区统计属于JCR 2区的数量及比例[返回格式数量/占比]", + ] + ]; + //定义redis连接 private $oQueueRedis; + private $oHelperFunction; public function __construct() { - $this->oQueueRedis = QueueRedis::getInstance(); + $this->oHelperFunction = new HelperFunction; } /** - * 构建公微模版-处理提示词 + * 微信公众号-构建公微模版[处理提示词] */ public function buildWechatPrompt($aSearch = []){ @@ -185,7 +183,7 @@ class OpenAi return $aMessage; } /** - * 处理文章内容 + * 微信公众号-处理文章内容 */ private function dealArticleContent($aParam = []){ //内容 @@ -207,52 +205,21 @@ class OpenAi if(empty($sValue) || $sValue == " "){ continue; } - $sKey = $this->isIdInRange($value['sort'],$aArticleMainH1); + $sKey = $this->oHelperFunction->isIdInRange($value['sort'],$aArticleMainH1); if(!empty($sKey)){ - $sKey = strtolower(str_replace(' ', '_', strip_tags($sKey))); - $value['content'] = strip_tags($value['content']); + $sKey = strtolower(str_replace(' ', '_', $this->oHelperFunction->filterAllTags($sKey))); + $value['content'] = $this->oHelperFunction->filterAllTags($value['content']); if($value['type'] == 1){ $aMainImageList[$sKey][] = $value['ami_id']; } $aMainList[$sKey][] = $value; }else{ - $value['content'] = strip_tags($value['content']); + $value['content'] = $this->oHelperFunction->filterAllTags($value['content']); $aMainList['digest'][] = $value; } } return ['main_list' => $aMainList,'main_image_list' => $aMainImageList]; } - - /** - * 判断ID是否在目标区间数组的范围内 - * @param int $id 待判断的ID - * @param array $rangeMap 区间规则数组 - * @return bool|string 存在则返回所属标题,否则返回false - */ - private function isIdInRange($iSort,$rangeMap = []) { - foreach ($rangeMap as $title => $range) { - // 解析区间值(处理字符串和数字两种格式) - $rangeStr = is_string($range['range']) ? $range['range'] : (string)$range['range']; - $rangeParts = explode(',', $rangeStr); - // 单个值:表示 >= 该值 - if (count($rangeParts) == 1) { - $min = (int)$rangeParts[0]; - if ($iSort > $min) { - return $title; // 返回所属标题 - } - } - // 两个值:表示 [min, max] 闭区间(包含两端) - elseif (count($rangeParts) == 2) { - $min = (int)$rangeParts[0]; - $max = (int)$rangeParts[1]; - if ($iSort >= $min && $iSort <= $max) { - return $title; // 返回所属标题 - } - } - } - return ''; // 不在任何区间 - } - /** * 处理文章内容-研究结果 */ @@ -305,7 +272,7 @@ class OpenAi } /** - * 组装问题【默认】 + * 微信公众号-组装问题【默认】 */ private function dealDefaultQuestion($aParam = [],$aMainImageList = []){ @@ -357,7 +324,7 @@ class OpenAi } //用户角色 - $sUserPrompt = ['title' => strip_tags($k),'content' => $sContentBetween]; + $sUserPrompt = ['title' => $this->oHelperFunction->filterAllTags($k),'content' => $sContentBetween]; $aUserPrompt = ['results' =>['title' => '[将标题直接翻译中文,无需针对内容总结翻译]','content' => '['.$sQuestionInfo.']']]; //系统角色 @@ -384,7 +351,7 @@ class OpenAi return $aMessage; } /** - * 组装问题【reviwew】 + * 微信公众号-组装问题【reviwew】 */ private function dealReviewQuestion($aParam = [],$aMainImageList = []){ @@ -450,54 +417,6 @@ class OpenAi } return $aMessage; } - - /** - * 构建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"){ - $sUserPrompt = '{#title_chinese#}'; - $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); - } - if($key == "content"){ - $sUserPrompt = '{#content#}'; - $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); - } - if(!in_array($key,["title_chinese","content"])){ - $sUserPrompt = '标题:{#title_chinese#} 摘要: {#abstract#} 内容: {#content#}'; - $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); - } - - $aMessage[] = [ - ['role' => 'system', 'content' => $sSysMessagePromptInfo], - ['role' => 'user', 'content' => $sUserPrompt] - ]; - } - return $aMessage; - } - /** * CURL 发送请求到 OpenAI【单独】 * @param $messages 内容 @@ -565,7 +484,7 @@ class OpenAi return json_encode(array('status' => 7,'msg' => 'OPENAI did not return data')); } //数据转换 - $aData = $this->extractAndParse($aData); + $aData = $this->oHelperFunction->extractAndParse($aData); $aContent = empty($aData['data']) ? [] : $aData['data']; $sMsg = empty($aData['msg']) ? 'OPENAI did not return data' : $aData['msg']; if(empty($aContent)){ @@ -591,12 +510,13 @@ class OpenAi $timeout = empty($aParam['timeout']) ? 600 : $aParam['timeout']; // 进一步延长超时到15分钟 $sUrl = empty($aParam['url']) ? $this->sUrl : $aParam['url']; $iTemperature = empty($aParam['temperature']) ? '0.2' : $aParam['temperature']; + $iTop = empty($aParam['top_p']) ? '0.9' : $aParam['top_p']; // 组装数据 - 增加流式传输必要参数 $data = [ 'model' => $model, 'messages' => $aMessage, 'temperature' => $iTemperature, - 'top_p' => 0.8, + 'top_p' => $iTop, 'frequency_penalty' => 0.3, 'presence_penalty' => 0.2, 'stream' => true, @@ -611,7 +531,9 @@ class OpenAi 'Authorization: Bearer ' . $this->sApiKey, 'Accept: text/event-stream', 'Cache-Control: no-cache', - 'Connection: keep-alive' // 保持长连接 + 'Connection: keep-alive', // 保持长连接 + // 'Content-Length: ' . strlen(json_encode($data)), // 显式指定内容长度 + // 'Expect: ' // 取消"100 Continue"预期,避免服务器等待确认 ]); // 代理与SSL配置优化 @@ -625,11 +547,12 @@ class OpenAi curl_setopt($this->curl, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($this->curl, CURLOPT_TIMEOUT, $timeout); curl_setopt($this->curl, CURLOPT_CONNECTTIMEOUT, 30); + // 设置超时时间(单位:秒),根据业务场景调整 curl_setopt($this->curl, CURLOPT_NOSIGNAL, true); // 禁用信号处理,解决超时精度问题 curl_setopt($this->curl, CURLOPT_BUFFERSIZE, 32); // 更小的缓冲区,更及时的处理 curl_setopt($this->curl, CURLOPT_FRESH_CONNECT, true); curl_setopt($this->curl, CURLOPT_FORBID_REUSE, true); // 禁止重用连接 - + // 流式响应处理 - 增加分块校验 $streamContent = ''; $isComplete = false; @@ -666,12 +589,18 @@ class OpenAi return strlen($data); // 必须返回完整长度,否则curl会中断 }); - // 执行请求 $result = curl_exec($this->curl); $curlErrno = curl_errno($this->curl); $httpCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); $totalReceived = curl_getinfo($this->curl, CURLINFO_SIZE_DOWNLOAD); + // 其他错误 + $error = '接口错误'; + if(curl_error($this->curl) !== 0) { + $error = curl_error($this->curl); // 正确使用有效句柄 + $httpCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); + $error = "AI审稿传输失败:{$error},HTTP状态码:{$httpCode}"; + } curl_close($this->curl); // 处理最后剩余的不完整数据(关键修复) @@ -686,7 +615,7 @@ class OpenAi if (!empty($curlErrno)) { // 超时但已完成 if ($curlErrno == CURLE_OPERATION_TIMEDOUT && $isComplete) { - $sStreamResponse = $this->parseMedicalStreamResponse($streamContent); + $sStreamResponse = $this->oHelperFunction->parseMedicalStreamResponse($streamContent); return json_encode([ 'status' => 1, 'msg' => 'success', @@ -700,17 +629,15 @@ class OpenAi return json_encode([ 'status' => 3, 'msg' => "超时,已接收{$receivedChunks}个分块", - 'partial_data' => $this->parseMedicalStreamResponse($streamContent), + 'partial_data' => $this->oHelperFunction->parseMedicalStreamResponse($streamContent), 'is_complete' => $isComplete ? 1 : 0, 'chunks' => $receivedChunks, 'received_bytes' => $totalReceived ]); } - - // 其他错误 return json_encode([ 'status' => 4, - 'msg' => "CURL错误: " . curl_error($this->curl), + 'msg' => "CURL错误: " . $error, 'error_code' => $curlErrno, 'http_code' => $httpCode ]); @@ -727,7 +654,7 @@ class OpenAi } // 处理正常结果 - $sStreamResponse = $this->parseMedicalStreamResponse($streamContent); + $sStreamResponse = $this->oHelperFunction->parseMedicalStreamResponse($streamContent); return json_encode([ 'status' => 1, 'msg' => 'success', @@ -738,240 +665,6 @@ class OpenAi ]); } - /** - * 增强版流式响应解析 - 解决JSON片段拼接问题 - */ - private function parseMedicalStreamResponse($streamContent){ - $fullContent = ''; - $lines = explode("\n", $streamContent); - $validLines = 0; - $errorLines = 0; - - foreach ($lines as $line) { - $line = trim($line); - if (empty($line)) continue; - - // 处理DeepSeek的SSE格式 - if (strpos($line, 'data: ') === 0) { - // 检查结束标记 - if ($line === 'data: [DONE]') { - break; - } - - $jsonStr = substr($line, 6); - $jsonData = json_decode($jsonStr, true); - - // 解析错误处理与修复 - if (json_last_error() !== JSON_ERROR_NONE) { - $errorLines++; - // 针对DeepSeek常见的JSON格式问题进行修复 - $jsonStr = $this->fixDeepSeekJson($jsonStr); - $jsonData = json_decode($jsonStr, true); - } - - // 提取内容(兼容DeepSeek的响应结构) - if (isset($jsonData['choices'][0]['delta']['content'])) { - $fullContent .= $jsonData['choices'][0]['delta']['content']; - $validLines++; - } elseif (isset($jsonData['choices'][0]['text'])) { - $fullContent .= $jsonData['choices'][0]['text']; - $validLines++; - } - } - } - - // 记录解析统计,便于调试 - error_log("流式解析: 有效行{$validLines}, 错误行{$errorLines}"); - return $fullContent; - } - - /** - * 高性能DeepSeek JSON修复函数(终极版) - * 确保修复后的JSON字符串100%可解析,同时保持最优性能 - */ - private function fixDeepSeekJson($jsonStr) { - // 基础处理:去除首尾空白并处理空字符串(高效操作) - $jsonStr = trim($jsonStr); - if (empty($jsonStr)) { - return '{}'; - } - - // 1. 预处理:清除首尾干扰字符(减少正则使用) - $len = strlen($jsonStr); - $start = 0; - // 跳过开头的逗号和空白 - while ($start < $len && ($jsonStr[$start] === ',' || ctype_space($jsonStr[$start]))) { - $start++; - } - $end = $len - 1; - // 跳过结尾的逗号和空白 - while ($end >= $start && ($jsonStr[$end] === ',' || ctype_space($jsonStr[$end]))) { - $end--; - } - if ($start > 0 || $end < $len - 1) { - $jsonStr = substr($jsonStr, $start, $end - $start + 1); - $len = strlen($jsonStr); - // 处理截取后可能为空的情况 - if ($len === 0) { - return '{}'; - } - } - - // 2. 括号平衡修复(核心逻辑保持,减少计算) - $braceDiff = substr_count($jsonStr, '{') - substr_count($jsonStr, '}'); - if ($braceDiff !== 0) { - if ($braceDiff > 0) { - $jsonStr .= str_repeat('}', $braceDiff); - } else { - // 仅在必要时使用正则移除多余括号 - $jsonStr = preg_replace('/}(?=([^"]*"[^"]*")*[^"]*$)/', '', $jsonStr, -$braceDiff); - } - } - - $bracketDiff = substr_count($jsonStr, '[') - substr_count($jsonStr, ']'); - if ($bracketDiff !== 0) { - if ($bracketDiff > 0) { - $jsonStr .= str_repeat(']', $bracketDiff); - } else { - $jsonStr = preg_replace('/](?=([^"]*"[^"]*")*[^"]*$)/', '', $jsonStr, -$bracketDiff); - } - } - - // 3. 控制字符清理(合并为单次处理) - $jsonStr = preg_replace( - '/([\x00-\x1F\x7F]|[^\x20-\x7E\xA0-\xFF]|\\\\u001f|\\\\u0000)/', - '', - $jsonStr - ); - - // 4. 引号处理(仅在有引号时处理,减少操作) - if (strpos($jsonStr, '"') !== false) { - // 修复未转义引号(优化正则) - $jsonStr = preg_replace('/(?handleJsonError($jsonStr, $errorCode); - $attempts++; - } - - // 终极容错:如果所有尝试都失败,返回空JSON对象 - return '{}'; - } - - /** - * 根据JSON解析错误类型进行针对性修复 - */ - private function handleJsonError($jsonStr, $errorCode) { - switch ($errorCode) { - case JSON_ERROR_SYNTAX: - // 语法错误:尝试更激进的清理 - $jsonStr = preg_replace('/[^\w{}[\]":,.\s\\\]/', '', $jsonStr); - $jsonStr = preg_replace('/,\s*([}\]])/', ' $1', $jsonStr); - break; - - case JSON_ERROR_CTRL_CHAR: - // 控制字符错误:进一步清理控制字符 - $jsonStr = preg_replace('/[\x00-\x1F\x7F]/u', '', $jsonStr); - break; - - case JSON_ERROR_UTF8: - // UTF8编码错误:尝试重新编码 - $jsonStr = utf8_encode(utf8_decode($jsonStr)); - break; - - default: - // 其他错误:使用备用修复策略 - $jsonStr = $this->fallbackJsonFix($jsonStr); - } - - return $jsonStr; - } - - /** - * 备用JSON修复策略(更激进的修复方式) - * 当主修复逻辑失败时使用 - */ - private function fallbackJsonFix($jsonStr) { - // 更彻底的清理 - $jsonStr = preg_replace('/[^\w{}[\]":,.\s\\\]/u', '', $jsonStr); - - - if (!preg_match('/^[\[{]/', $jsonStr)) { - $jsonStr = '{' . $jsonStr . '}'; - } - - // 最后尝试平衡括号 - $openBrace = substr_count($jsonStr, '{'); - $closeBrace = substr_count($jsonStr, '}'); - $jsonStr .= str_repeat('}', max(0, $openBrace - $closeBrace)); - - $openBracket = substr_count($jsonStr, '['); - $closeBracket = substr_count($jsonStr, ']'); - $jsonStr .= str_repeat(']', max(0, $openBracket - $closeBracket)); - - // 确保结尾正确 - $lastChar = substr($jsonStr, -1); - if ($lastChar !== '}' && $lastChar !== ']') { - $jsonStr .= preg_match('/^\{/', $jsonStr) ? '}' : ']'; - } - - return $jsonStr; - } - /** * 微信公众号-生成公微内容(CURL) */ @@ -1048,6 +741,7 @@ class OpenAi } //请求OPENAI $aParam['temperature'] = '0.6'; + $aParam['top_p'] = '0.8'; $aResult = $this->curlOpenAIStream($aParam); //更新处理进度 $iIndex = empty($aParam['chunkIndex']) ? 0 : $aParam['chunkIndex']; @@ -1061,7 +755,7 @@ class OpenAi //更新入库 $aReturnData = json_decode($aResult,true); $aDataInfo =empty($aReturnData['data']) ? [] : $aReturnData['data']; - $aData = empty($aDataInfo) ? [] : $this->extractAndParse($aDataInfo); + $aData = empty($aDataInfo) ? [] : $this->oHelperFunction->extractAndParse($aDataInfo); if(!empty($aData) && is_string($aData)){ $aData = json_decode($aData,true); } @@ -1145,39 +839,481 @@ class OpenAi } /** - * 从文本中提取被```json```和```包裹的JSON内容并解析 - * @param string $text 包含JSON代码块的文本 - * @param bool $assoc 是否返回关联数组(默认true) - * @return array|object 解析后的JSON数据,失败时返回null + * 构建AI审稿-处理提示词 */ - public function extractAndParse($text, $assoc = true){ - // 尝试提取标准JSON代码块 - preg_match('/```json\s*(\{.*?\})\s*```/s', $text, $matches); - $jsonContent = empty($matches[1]) ? $text : $matches[1]; - - // 若未提取到,尝试宽松匹配(允许没有json标记) - if (empty($jsonContent)) { - preg_match('/```\s*(\{.*?\})\s*```/s', $text, $matches); - $jsonContent = empty($matches[1]) ? $text : $matches[1]; + public function buildReviewPrompt($aSearch = []) + { + if(empty($aSearch)){ + return []; } - // 清理JSON内容,去除多余标记和控制字符 - $jsonContent = trim(trim($jsonContent, '```json'), '```'); - $jsonContent = preg_replace('/[\x00-\x1F\x7F]/', '', $jsonContent); // 过滤所有控制字符 + //问题等级 + $sQuestionLevel = empty($aSearch['question_level']) ? '' : $aSearch['question_level']; + //提问字段 + $aQuestionFields = empty($aSearch['queue_fields']) ? [] : explode(',', $aSearch['queue_fields']); + if(empty($aQuestionFields) || empty($sQuestionLevel)){ + return []; + } + unset($aSearch['queue_fields']); + //获取问题 + $aQuestion = $this->aReviewQuestion; + //系统角色 + $sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message']; + if(empty($sSysMessagePrompt)){ + return []; + } + $aQuestion = empty($aQuestion[$sQuestionLevel]) ? [] : $aQuestion[$sQuestionLevel]; + if(empty($aQuestion)){ + return []; + } + $aScopeReturn = $this->aScopeReturn;//返回json格式 + //处理数据 + $aMessage = []; + foreach ($aQuestion as $key => $value) { + if(!in_array($key, $aQuestionFields) || empty($value)){ + continue; + } + //系统角色 + $sSysMessageInfo = empty($aScopeReturn[$key]) ? '' : json_encode($aScopeReturn[$key],JSON_UNESCAPED_UNICODE); + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $value); + $aMessage[] = [ + ['role' => 'system', 'content' => $sSysMessagePrompt.$sSysMessageInfo], + ['role' => 'user', 'content' => $sUserPrompt], + 'field_name' => $key + ]; + + } + return $aMessage; + } + /** + * AI审稿-队列处理 + */ + public function articleReviewDeal($aParam = []){ + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an article']); + } + //期刊ID + $iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id']; + if(empty($iJournalId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an Journal']); + } + //提问信息 + $aMessage = empty($aParam['messages']) ? [] : $aParam['messages']; + if (empty($aMessage)) { + return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']); + } + //记录处理开始 + foreach ($aMessage as $key => $value) { + $aParam['field_name'] = $value['field_name']; + unset($value['field_name']); + $aParam['messages'] = $value; + $aParam['chunkIndex'] = $aParam['field_name']; + $aParam['key_name'] = 'queue_1_completed'; + Queue::push('app\api\job\ArticleReviewForQueue@fire', $aParam, 'ArticleReviewForQueue'); + } + return json_encode(['status' => 1, 'msg' => 'AI review in progress, please wait']); + } + /** + * AI审稿-队列形式 + */ + public function articleReviewForQueue($aParam = []){ - // 解析JSON - $aData = json_decode($jsonContent, $assoc); - - // 检查解析结果 - if (json_last_error() !== JSON_ERROR_NONE) { - return [ - 'status' => 2, - 'msg' => "API返回无效JSON: " . json_last_error_msg() . '===============' . $jsonContent, - 'data' => null + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an article']); + } + //期刊ID + $iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id']; + if(empty($iJournalId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an Journal']); + } + + //提问信息 + $aMessage = empty($aParam['messages']) ? [] : $aParam['messages']; + if (empty($aMessage)) { + return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']); + } + //问题等级 + $sQuestionLevel = empty($aParam['question_level']) ? '' : $aParam['question_level']; + //字段名 + $sFieldsName = empty($aParam['field_name']) ? '' : $aParam['field_name']; + if(empty($sFieldsName)){ + return json_encode(['status' => 3, 'msg' => 'Field is empty']); + } + + //判断是否执行过 + $sRedisKey = 'queue_job:review_progress:'.$iArticleId.'_'.$iJournalId; + $sChunkProgress = $this->oQueueRedis->getChunkProgress($sRedisKey, $sFieldsName); + $aResult = $sChunkProgress; + if(empty($sChunkProgress)){ + //请求OPENAI + $aParam['temperature'] = '0.1'; + $aParam['top_p'] = '0.9'; + $aResult = $this->curlOpenAIStream($aParam); + //保存内容 + $this->oQueueRedis->saveChunkProgress($sRedisKey, $sFieldsName,$aResult); + } + //更新入库 + $aReturnData = json_decode($aResult,true); + $aDataInfo =empty($aReturnData['data']) ? [] : $aReturnData['data']; + $aData = empty($aDataInfo) ? [] : $this->oHelperFunction->extractAndParse($aDataInfo,$sChunkProgress); + if(!empty($aData) && is_string($aData)){ + $aData = json_decode($aData,true); + } + $aData = empty($aData['data']) ? [] : $aData['data']; + if(is_object($aData)){ + $aData = get_object_vars($aData); + } + if(!empty($aData)){//更新AI审稿记录表 + $aData['article_id'] = $iArticleId; + $aData['journal_id'] = $iJournalId; + $aData['question_level'] = $sQuestionLevel; + $aData['field_name'] = $sFieldsName; + $result = $this->addAiReview($aData); + } + return $aResult; + } + /** + * 构建AI审稿-处理提示词[大文本] + */ + public function buildReviewPromptChunk($aSearch = []){ + if(empty($aSearch)){ + return []; + } + + //问题等级 + $sQuestionLevel = empty($aSearch['question_level']) ? '' : $aSearch['question_level']; + //提问字段 + $sQuestionFields = empty($aSearch['queue_fields']) ? '' : $aSearch['queue_fields']; + if(empty($sQuestionFields) || empty($sQuestionLevel)){ + return []; + } + unset($aSearch['queue_fields']); + //获取问题 + $aQuestion = $this->aReviewQuestion; + //系统角色 + $sSysMessagePrompt = empty($aQuestion['system_message']) ? '' : $aQuestion['system_message']; + if(empty($sSysMessagePrompt)){ + return []; + } + $aQuestion = empty($aQuestion[$sQuestionLevel]) ? [] : $aQuestion[$sQuestionLevel]; + if(empty($aQuestion)){ + return []; + } + + $aScopeReturn = $this->aScopeReturn;//返回json格式 + //处理数据 + $aMessage = []; + + //系统角色 + $sSysMessageInfo = empty($aScopeReturn[$sQuestionFields]) ? '' : json_encode($aScopeReturn[$sQuestionFields],JSON_UNESCAPED_UNICODE); + if(empty($sSysMessageInfo)){ + return []; + } + + //处理数据 + $aContent = empty($aSearch['content']) ? '' : $aSearch['content']; + if(empty($aContent)){ + return []; + } + foreach ($aContent as $key => $value) { + $aSearch['content'] = $value; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $aQuestion[$sQuestionFields]); + $aMessage[] = [ + ['role' => 'system', 'content' => $sSysMessagePrompt.$sSysMessageInfo], + ['role' => 'user', 'content' => $sUserPrompt], ]; } - - return ['status' => 1, 'msg' => 'success', 'data' => $aData]; + return $aMessage; + } + /** + * 微信公众号-生成公微内容(CURL) + */ + public function articleReviewDealChunk($aParam = []){ + + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an article']); + } + //期刊ID + $iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id']; + if(empty($iJournalId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an Journal']); + } + //提问信息 + $aMessage = empty($aParam['messages']) ? [] : $aParam['messages']; + if (empty($aMessage)) { + return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']); + } + //提问字段 + $sQuestionFields = empty($aParam['queue_fields']) ? '' : $aParam['queue_fields']; + if (empty($sQuestionFields)) { + return json_encode(['status' => 3, 'msg' => 'AI Q&A content not obtained']); + } + + //记录处理开始 + $iNum = count($aMessage); + $sRedisKey = 'queue_job:review_chunk:'.$iArticleId.'_'.$iJournalId.'_'.$sQuestionFields; + $result = $this->oQueueRedis->recordQuestionProcessingStart($sRedisKey,$iNum); + $result = empty($result) ? 0 : $result; + if($result == 1){ + //定义空数组 + foreach ($aMessage as $key => $value) { + $aParam['messages'] = $value; + $aParam['chunkIndex'] = $key; + $aParam['key_name'] = 'queue_1_completed'; + Queue::push('app\api\job\ArticleReviewForQueueChunk@fire', $aParam, 'ArticleReviewForQueueChunk'); + } + return json_encode(['status' => 1, 'msg' => 'AI review in progress, please wait']); + } + if($result == 2){ + return json_encode(['status' => 3, 'msg' => 'The data has been generated, please proceed with the next steps']); + } + if($result == 3){ + return json_encode(['status' => 4, 'msg' => 'AI review in progress, please wait']); + } + return json_encode(['status' => 5, 'msg' => 'Redis write failure']); + } + /** + * 微信公众号-审稿大文本分片传输队列 + */ + public function articleReviewForQueueChunk($aParam = []){ + + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an article']); + } + //期刊ID + $iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id']; + if(empty($iJournalId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an Journal']); + } + + //提问信息 + $aMessage = empty($aParam['messages']) ? [] : $aParam['messages']; + if (empty($aMessage)) { + return json_encode(['status' => 2, 'msg' => 'AI Q&A content not obtained']); + } + //问题等级 + $sQuestionLevel = empty($aParam['question_level']) ? '' : $aParam['question_level']; + //字段名 + $sFieldsName = empty($aParam['queue_fields']) ? '' : $aParam['queue_fields']; + if(empty($sFieldsName)){ + return json_encode(['status' => 3, 'msg' => 'Field is empty']); + } + + //请求OPENAI + $aParam['temperature'] = '0.1'; + $aParam['top_p'] = '0.9'; + $aResult = $this->curlOpenAIStream($aParam); + + //更新处理进度 + $iIndex = empty($aParam['chunkIndex']) ? 0 : $aParam['chunkIndex']; + $sRedisKey = 'queue_job:review_chunk:'.$iArticleId.'_'.$iJournalId.'_'.$sFieldsName; + $sKeyName = empty($aParam['key_name']) ? 'queue_1_completed' : $aParam['key_name']; + $iProgress = $this->oQueueRedis->updateQuestionProcessingProgress($sRedisKey,$sKeyName); + //保存内容 + $sRedisKey = 'queue_job:review_progress_chunk:'.$iArticleId.'_'.$iJournalId.'_'.$sFieldsName; + $this->oQueueRedis->saveChunkProgress($sRedisKey, $iIndex,$aResult); + + //判断是否执行完成 + if($iProgress >= 100){ + $aChunkProgress = $this->oQueueRedis->getChunkProgress($sRedisKey); + $aChunkProgress = $this->dealChunkProgres($aChunkProgress,$sFieldsName); + if(!empty($aChunkProgress)){ + //判断是否执行过 + $sRedisKey = 'queue_job:review_progress:'.$iArticleId.'_'.$iJournalId; + $this->oQueueRedis->saveChunkProgress($sRedisKey, $sFieldsName,json_encode($aChunkProgress)); + //入库数据 + $aChunkProgress['article_id'] = $iArticleId; + $aChunkProgress['journal_id'] = $iJournalId; + $aChunkProgress['question_level'] = $sQuestionLevel; + $aChunkProgress['field_name'] = $sFieldsName; + $result = $this->addAiReview($aChunkProgress); + } + } + return $aResult; + } + /** + * 微信公众号-审稿大文本分片传输[内容整合处理] + */ + private function dealChunkProgres($aParam = [],$sFieldsName = ''){ + if(empty($aParam) || empty($sFieldsName)){ + return []; + } + foreach ($aParam as $key => $value) { + //更新入库 + $aReturnData = json_decode($value,true); + $aDataInfo =empty($aReturnData['data']) ? [] : $aReturnData['data']; + $aData = empty($aDataInfo) ? [] : $this->oHelperFunction->extractAndParse($aDataInfo); + if(!empty($aData) && is_string($aData)){ + $aData = json_decode($aData,true); + } + $aData = empty($aData['data']) ? [] : $aData['data']; + if(is_object($aData)){ + $aData = get_object_vars($aData); + } + if(empty($aData)){ + continue; + } + $aChunkData[$key] = $aData; + } + if(empty($aChunkData)){ + return []; + } + //处理结果 + return $this->aggregateYesNoDimension($aChunkData,$sFieldsName); + } + /** + * 微信公众号-审稿大文本分片传输[内容整合处理] + */ + private function aggregateYesNoDimension($aParam = [],$sFieldsName = '') + { + $aAssessments = $aExplanations = []; + foreach ($aParam as $key => $value) { + $sKey = $sFieldsName.'_assessment'; + if(!isset($value[$sKey])){ + $sKey = $sFieldsName; + } + if(empty($value[$sKey])){ + continue; + } + $aAssessments[] = $value[$sKey]; + $sKey = $sFieldsName.'_explanation'; + if(isset($value[$sKey])){ + if(empty($value[$sKey])){ + continue; + } + $aExplanations[] = $value[$sKey]; + } + } + if(!empty($aAssessments) && empty($aExplanations)){ + return [ + $sFieldsName => implode("\n", array_unique($aAssessments)) + ]; + } + if(!empty($aAssessments) && !empty($aExplanations)){ + return [ + $sFieldsName.'_assessment' => in_array('是', $aAssessments) ? '是' : (empty($aAssessments) ? '无' : '否'), + $sFieldsName.'_explanation' => implode("\n", array_unique($aExplanations)) + ]; + } + return []; } + /** + * AI审稿-处理返回数据及入库 + */ + private function addAiReview($aData = [],$sChunkProgress = ''){ + if(empty($aData)){ + return ['status' => 2,'msg' => '参数错误']; + } + //文章ID + $iArticleId = empty($aData['article_id']) ? 0 : $aData['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an article']); + } + //期刊ID + $iJournalId = empty($aData['journal_id']) ? 0 : $aData['journal_id']; + if(empty($iJournalId)){ + return json_encode(['status' => 2, 'msg' => 'Please select an Journal']); + } + //问题等级 + $sQuestionLevel = empty($aData['question_level']) ? '' : $aData['question_level']; + //字段名 + $sFieldsName = empty($aData['field_name']) ? '' : $aData['field_name']; + if(empty($sFieldsName)){ + return json_encode(['status' => 3, 'msg' => 'Field is empty']); + } + if($sFieldsName == 'journal_scope'){ + $sKey = $sFieldsName.'_assessment';//是/否 + $sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey]; + if($sResult == '否'){ + $aQueueParam = ['article_id' => $iArticleId,'question_level' => $sQuestionLevel,'queue_fields' => 'other_journal']; + } + if($sResult == '是'){ + $aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'ethics']; + } + Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview'); + } + if($sFieldsName == 'other_journal'){ + $sKey = $sFieldsName.'_assessment';//是/否 + $sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey]; + if($sResult == '是'){ + $aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'ethics']; + Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview'); + }else{ + $aData['is_finish'] = 1; + } + } + if($sFieldsName == 'ethics'){//伦理问题 + $sKey = $sFieldsName.'_assessment';//是/否 + $sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey]; + if($sResult == '否'){ + $aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'registration']; + Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview'); + }else{ + $aData['is_finish'] = 1; + } + } + if($sFieldsName == 'registration'){//临床注册号及知情同意书 + $sKey = $sFieldsName.'_assessment';//是/否 + $sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey]; + if($sResult == '否'){ + $aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'academic']; + Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview'); + }else{ + $aData['is_finish'] = 1; + } + } + if($sFieldsName == 'academic'){//学术问题 + $sKey = 'academic_assessment';//是/否 + $sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey]; + if($sResult == '否'){ + $aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'contradiction']; + Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview'); + }else{ + $aData['is_finish'] = 1; + } + } + if($sFieldsName == 'contradiction'){//矛盾问题 + $sKey = 'contradiction_assessment';//是/否 + $sResult = empty($aData[$sKey]) ? '否' : $aData[$sKey]; + if($sResult == '否'){ + $aQueueParam = ['article_id' => $iArticleId,'question_level' => 'B','queue_fields' => 'fund_number']; + Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview'); + }else{ + $aData['is_finish'] = 1; + } + } + if($sFieldsName == 'fund_number'){//矛盾问题 + // $sKey = 'fund_number';//是/否 + // $sResult = empty($aData[$sKey]) ? '' : $aData[$sKey]; + // if(!empty($sResult)){ + //获取C级问题 + $aQuestion = $this->aReviewQuestion; + $aQuestion = empty($aQuestion['C']) ? [] : $aQuestion['C']; + if(!empty($aQuestion)){ + foreach ($aQuestion as $key => $value) { + $aQueueParam = ['article_id' => $iArticleId,'question_level' => 'C','queue_fields' => $key]; + Queue::push('app\api\job\ArticleReview@fire', $aQueueParam, 'ArticleReview'); + } + } + // } + } + //审稿记录入库 + if(empty($sChunkProgress)){ + if($sQuestionLevel == 'C' && $sFieldsName = 'reference'){ + $aData['is_finish'] = 1; + } + $oAireview = new \app\common\Aireview; + $result = $oAireview->addAiReview($aData); + } + return $result; + } } diff --git a/application/common/QueueJob.php b/application/common/QueueJob.php index c83e558..a334a27 100644 --- a/application/common/QueueJob.php +++ b/application/common/QueueJob.php @@ -4,22 +4,73 @@ namespace app\common; use think\Db; use think\Cache; use app\common\QueueRedis; -use app\common\traits\QueueDbHATrait; class QueueJob { // 必填参数 protected $aField = ['job_id', 'job_class', 'status', 'create_time', 'update_time', 'error', 'params']; private $logPath; private $QueueRedis; - private $maxRetries = 2; + private $maxRetries = 3;//最大重试次数 const JSON_OPTIONS = JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR; - // 引入高可用数据库管理 trait - use QueueDbHATrait; + private $warningThreshold = 120; // 进程超时预警阈值(秒) + // 进程最大运行时间(秒) + protected $maxRunTime = 21600; // 6个小时,短于数据库wait_timeout + private $lockExpire = 180; // 锁过期时间(3分钟,根据任务实际耗时调整) + private $maxDelay = 300; // 最大重试延迟(5分钟) + // 静态变量:记录进程启动时间(跨任务共享,每个进程仅初始化一次) + protected static $processStartTime = null; + private $startTime; public function __construct() { $this->QueueRedis = QueueRedis::getInstance(); + // 初始化进程启动时间(仅在进程首次启动时执行) + if (is_null(self::$processStartTime)) { + self::$processStartTime = time(); + // 增加进程ID标识 + $pid = getmypid(); + $this->log("队列进程启动 [PID:{$pid}],启动时间:" . date('Y-m-d H:i:s', self::$processStartTime)); + } + //任务开始时间 + $this->startTime = microtime(true); } + /** + * 任务初始化验证 + * @return bool + */ + public function init($job){ + // 检查进程是否已超时,提前退出 + if ($this->isProcessTimeout(self::$processStartTime)) { + $this->log("进程已超时,放弃处理任务"); + $job->release(15); // 短延迟后重新入队 + return; + } + + // 进程超时预警 + $this->checkProcessTimeoutWarning(self::$processStartTime); + + // 检查Redis连接状态 + if (!$this->QueueRedis->getConnectionStatus()) { + $this->log("Redis连接失败,10秒后重试"); + $job->release(15); + return; + } + } + /** + * 任务结束 + * @return bool + */ + public function finnal(){ + $executionTime = microtime(true) - $this->startTime; + $this->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒"); + gc_collect_cycles(); + + // 任务完成后,检查进程是否超时 + if ($this->isProcessTimeout(self::$processStartTime)) { + $this->log("进程已运行超过{$this->maxRunTime}秒,任务完成后自动退出以刷新连接"); + exit(1); // 退出进程,触发supervisor重启 + } + } /** * 写入日志到缓冲区 * @param string $message @@ -43,13 +94,11 @@ class QueueJob 'OpenAI' => 45, 'network' => 60 ]; - foreach ($delayMap as $keyword => $delay) { if (stripos($errorMsg, $keyword) !== false) { // 不区分大小写匹配 return $delay; } } - return 10; } @@ -60,25 +109,32 @@ class QueueJob * @param string $sRedisValue * @param \think\queue\Job $job */ - public function handleRetryableException($e,$sRedisKey,$sRedisValue,$job) + public function handleRetryableException($e, $sRedisKey, $sRedisValue, $job) { $sMsg = empty($e->getMessage()) ? '可重试异常' : $e->getMessage(); $sTrace = empty($e->getTraceAsString()) ? '' : $e->getTraceAsString(); $this->log("可重试异常: {$sMsg} | 堆栈: {$sTrace}"); - $this->QueueRedis->finishJob($sRedisKey, 'failed', 3600,$sRedisValue); + if ($this->isFatalDatabaseError($e)) { + $this->handleDatabaseErrorAndRestartWithRetry($e, $sRedisKey, $sRedisValue, $job); + return; + } + + // 原子化更新任务状态 + $this->QueueRedis->atomicJobUpdate($sRedisKey, 'failed', 3600, $sRedisValue); $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}"); + $delay = $this->getRetryDelay($sMsg, $attempts); // 动态延迟 + $this->log("{$delay}秒后重试({$attempts}/{$this->maxRetries})"); $job->release($delay); } } - /** * 处理不可重试异常 * @param \Exception $e @@ -96,15 +152,118 @@ class QueueJob $job->delete(); } + // 判断是否为需要重启的致命数据库错误 + private function isFatalDatabaseError(\Exception $e) + { + // 1. 检查是否为PDO异常 + if ($e instanceof \PDOException) { + $fatalCodes = [2006, 2013, 2002]; // 2006=连接断开,2013=连接丢失,2002=无法连接 + $errorCode = (int)$e->getCode(); + if (in_array($errorCode, $fatalCodes)) { + return true; + } + } + // 2. 检查错误消息关键词(覆盖非PDOException的数据库错误) + $errorMsg = strtolower($e->getMessage()); + $fatalKeywords = [ + 'mysql server has gone away', + 'lost connection to mysql server', + 'error while sending stmt_prepare packet', + 'database connection failed', + 'sqlstate[hy000]' // 通用数据库错误前缀 + ]; + foreach ($fatalKeywords as $keyword) { + if (strpos($errorMsg, $keyword) !== false) { + return true; + } + } + return false; + } + + //处理数据库错误,释放任务后重启(保留任务) + private function handleDatabaseErrorAndRestartWithRetry($e, $sRedisKey, $sRedisValue, $job) + { + $this->log("检测到致命数据库错误,释放任务后重启队列 | 错误: {$e->getMessage()}"); + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->log("数据库错误重试达上限,标记任务失败"); + $this->QueueRedis->finishJob($sRedisKey, 'failed', 3600, $sRedisValue); + $job->delete(); + exit(1); + } + // 1. 释放Redis锁(必须先释放,否则新进程无法获取锁) + if (!empty($sRedisKey) && !empty($sRedisValue)) { + $this->QueueRedis->forceReleaseLock($sRedisKey, $sRedisValue); + } + // 2. 释放任务回队列(设置短延迟,避免重启前被其他进程处理) + $job->release(60); // 60秒后重新入队,给进程重启留时间 + $this->log("任务已释放回队列,等待新进程处理"); + // 3. 强制退出进程,触发Supervisor重启 + $this->log("数据库错误,重启进程以刷新连接,新进程将处理释放的任务"); + exit(1); + } /** - * 数据库连接检查与重建(高可用版) - * 解决 MySQL server has gone away 等连接超时问题 - * @param bool $force 是否强制检查(忽略缓存时间) - * @return bool 连接是否有效 + * 获取分布式锁 + * @param string $sRedisKey + * @param string $sRedisValue + * @param Job $job + * @return bool */ - public function checkDbConnection($force = false) + public function acquireLock($sRedisKey, $sRedisValue, $job) { - return $this->checkDbConnectionTrait(); + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $this->lockExpire); + + if (!$isLocked) { + $currentLockValue = $this->QueueRedis->getRedisValue($sRedisKey); // 获取当前锁值 + $jobStatus = $this->QueueRedis->getJobStatus($sRedisKey); + + // 若锁值为空或过期,强制抢占锁 + if (empty($currentLockValue) || $jobStatus === false) { + $this->log("数据为空 | 状态: {$currentLockValue} | 键: {$jobStatus}"); + $isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $this->lockExpire); + if ($isLocked) return true; + } + if (in_array($jobStatus, ['completed', 'failed'])) { + $this->log("任务已完成或失败,删除任务 | 状态: {$jobStatus} | 键: {$sRedisKey}"); + $job->delete(); + } else { + $attempts = $job->attempts(); + if ($attempts >= $this->maxRetries) { + $this->log("超过最大重试次数({$this->maxRetries}),停止重试 | 键: {$sRedisKey}"); + $job->delete(); + } else { + $lockTtl = $this->QueueRedis->getLockTtl($sRedisKey); + $delay = $lockTtl > 0 ? $lockTtl + 5 : 30; + // 限制最大延迟时间 + $delay = min($delay, $this->maxDelay); + $this->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries}) | 键: {$sRedisKey}"); + $job->release($delay); + } + } + return false; + } + $this->log("写入成功 | 状态: {$sRedisKey} | 键: {$sRedisValue}"); + return true; + } + + /** + * 检查进程是否超时 + * @return bool + */ + public function isProcessTimeout($processStartTime) + { + return time() - $processStartTime > $this->maxRunTime; + } + + /** + * 检查进程超时预警 + */ + public function checkProcessTimeoutWarning($processStartTime) + { + $remainingTime = $this->maxRunTime - (time() - $processStartTime); + if ($remainingTime > 0 && $remainingTime < $this->warningThreshold) { + $this->log("进程即将超时,剩余时间:{$remainingTime}秒"); + } } } \ No newline at end of file