AI相关调整

This commit is contained in:
chengxl
2025-08-15 15:17:52 +08:00
parent 603deadbb4
commit f16ac88ec4
6 changed files with 942 additions and 647 deletions

View File

@@ -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;//素材上传完成

View File

@@ -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 = '/<img\s+[^>]*?>/i';
$sContent = preg_replace($pattern, '', $sContent);
// 关键修复:将分隔符从 / 改为 #,避免与 < > 冲突
$pattern = '#<b>Reference</b>|<b>Reference:</b>|<b><i>References:</i></b>|<b><i>References</i></b>|<b>References</b>|<b>References:</b>#';
// 查找标识在文本中的位置
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' ));
}

View File

@@ -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'];

View File

@@ -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;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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}");
}
}
}