From 2be05942bdd7957aac90cec64e790818d3e637a1 Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 19 Nov 2025 14:39:58 +0800 Subject: [PATCH 01/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Finalreview.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/application/api/controller/Finalreview.php b/application/api/controller/Finalreview.php index 7d1d2d29..95b72cbd 100644 --- a/application/api/controller/Finalreview.php +++ b/application/api/controller/Finalreview.php @@ -1083,7 +1083,7 @@ class Finalreview extends Base //查询复审 $aReviewerRepeatLists = []; $aWhere = ['art_rev_id' => ['in',$aArtRevId],'recommend' => ['between',[1,3]]]; - $aReviewerRepeat = Db::name('article_reviewer_repeat')->field('art_rev_rep_id,art_rev_id,recommend,ctime,stime')->where($aWhere)->select(); + $aReviewerRepeat = Db::name('article_reviewer_repeat')->field('art_rev_rep_id,art_rev_id,recommend,ctime,stime,state')->where($aWhere)->select(); if(!empty($aReviewerRepeat)){ foreach ($aReviewerRepeat as $key => $value) { $aReviewerRepeatLists[$value['art_rev_id']][] = $value; @@ -1100,6 +1100,18 @@ class Finalreview extends Base $value['ctime'] = empty($aQuestionData['ctime']) ? $value['ctime'] : $aQuestionData['ctime']; $value['score'] = empty($aQuestionData['score']) ? 0 : $aQuestionData['score']; $value['repeat'] = empty($aReviewerRepeatLists[$value['art_rev_id']]) ? [] : $aReviewerRepeatLists[$value['art_rev_id']]; + //判断是否复审 + $value['can_repeat'] = 1; + if(!empty($value['repeat'])){ + $aEnd = end($value['repeat']); + $iState = empty($aEnd['state']) ? 0 : $aEnd['state']; + $iRecommend = empty($aEnd['recommend']) ? 0 : $aEnd['recommend']; + if($iState == 1 && $iRecommend == 3){ + $value['can_repeat'] = 1; + }else{ + $value['can_repeat'] = 0; + } + } $value['rated'] = empty($aQuestionData['rated']) ? 0 : $aQuestionData['rated']; $value['realname'] = empty($aUser[$value['reviewer_id']]) ? '' : $aUser[$value['reviewer_id']]; $value['recommend'] = empty($aQuestionData['recommend']) ? 0 : $aQuestionData['recommend']; From 4d111fd9e9d566a47670d74e01c1c5e00d26633e Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 19 Nov 2025 14:40:49 +0800 Subject: [PATCH 02/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 63 ++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index 1eb84311..f1e67aa4 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -400,4 +400,67 @@ class Workbench extends Base Db::commit(); return json_encode(['status' => 1,'msg' => 'Update successful']); } + + /** + * 获取审稿记录详情 + */ + public function getArticleReviewDetail(){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + //获取文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select the article']); + } + + //获取审稿记录ID + $iArtRevId = empty($aParam['art_rev_id']) ? 0 : $aParam['art_rev_id']; + if(empty($iArtRevId)){ + return json_encode(['status' => 2,'msg' => 'Please select a record']); + } + //获取复审记录ID + $iArtRevRepId = empty($aParam['art_rev_rep_id']) ? 0 : $aParam['art_rev_rep_id']; + if(empty($iArtRevId) && empty($iArtRevRepId)){ + return json_encode(['status' => 2,'msg' => 'Please select a record']); + } + + //查询文章 + $aArticle = json_decode($this->get($aParam),true); + $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; + if($iStatus != 1){ + return json_encode($aArticle); + } + $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; + + //查询审稿记录 + $aWhere = ['art_rev_id' => $iArtRevId,'article_id' => $iArticleId]; + $aArticleReviewer = Db::name('article_reviewer')->field('art_rev_id,reviewer_id,ctime')->where($aWhere)->find(); + if(empty($aArticleReviewer)){ + return json_encode(['status' => 3,'msg' => 'Review record does not exist']); + } + if(empty($iArtRevRepId)){ + //查询初审问卷 + $aWhere = ['art_rev_id' => $iArtRevId]; + $aQuestion = Db::name('article_reviewer_question')->field('is_anonymous')->where($aWhere)->find(); + $aArticleReviewer['is_anonymous'] = empty($aQuestion['is_anonymous']) ? 0 : $aQuestion['is_anonymous']; + } + //查询审稿人姓名 + $iUserId = empty($aArticleReviewer['reviewer_id']) ? 0 : $aArticleReviewer['reviewer_id']; + if(!empty($iUserId)){ + $aWhere = ['user_id' => $iUserId,'state' => 0]; + $aUser = Db::name('user')->field('realname,email')->where($aWhere)->find(); + $aArticleReviewer['realname'] = empty($aUser['realname']) ? '' : $aUser['realname']; + $aArticleReviewer['email'] = empty($aUser['email']) ? '' : $aUser['email']; + } + + //组装信息 + $aData = ['article' => $aArticle,'article_reviewer' => $aArticleReviewer]; + //查询复审信息 + if(!empty($iArtRevRepId)){ + //查询初审问卷 + $aWhere = ['art_rev_id' => $iArtRevId,'art_rev_rep_id' => $iArtRevRepId]; + $aData['article_reviewer_repeat'] = Db::name('article_reviewer_repeat')->where($aWhere)->find(); + } + return json_encode(['status' => 1,'msg' => 'success','data' => $aData]); + } } From 0876328264e31cd7f2f73a5a6e0d70481b419c87 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 20 Nov 2025 15:25:03 +0800 Subject: [PATCH 03/53] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=8F=82=E8=80=83?= =?UTF-8?q?=E6=96=87=E7=8C=AE=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/Preaccept.php | 170 ++++++++++++++++++++++- 1 file changed, 167 insertions(+), 3 deletions(-) diff --git a/application/api/controller/Preaccept.php b/application/api/controller/Preaccept.php index 0d13b834..a918dff0 100644 --- a/application/api/controller/Preaccept.php +++ b/application/api/controller/Preaccept.php @@ -30,7 +30,8 @@ class Preaccept extends Base if (!$rule->check($data)) { return jsonError($rule->getError()); } - $production_info = $this->production_article_obj->where('article_id', $data['article_id'])->find(); + $aWhere = ['article_id' => $data['article_id'],'state' => 0]; + $production_info = $this->production_article_obj->where($aWhere)->find(); if ($production_info == null) { return jsonError("Object is null"); } @@ -758,8 +759,8 @@ class Preaccept extends Base return jsonSuccess($re); } $order_info = $this->order_obj->where("article_id",$article_info['article_id'])->find(); - if($order_info==null){ - $re['state'] = 0; + if($order_info==null){//这里有疑问 + $re['state'] = 1; $re['order'] = null; $re["fee"] = 0; return jsonSuccess($re); @@ -1591,5 +1592,168 @@ return null; $re['list'] = $mains; return jsonSuccess($re); } + //作者上传参考文献 - 新 + public function addRefersByExcelNew(){ + $data = $this->request->post(); + $rule = new Validate([ + "article_id" => "require", + "referFile" => "require" + ]); + if (!$rule->check($data)) { + return jsonError($rule->getError()); + } + //判断数据是否存在 不存在直接入库数据 chengxiaoling 20251119 start + $aWhere = ['article_id' => $data['article_id'],'state' => 0]; + $production_info = $this->production_article_obj->where($aWhere)->find(); + if (!$production_info) { + $aResult = $this->addProductionEx($data['article_id']); + $aResult = empty($aResult->getData()) ? [] : $aResult->getData(); + $iCode = isset($aResult['code']) ? $aResult['code'] : -1; + $sMsg = empty($aResult['msg']) ? 'Data processing failed' : $aResult['msg']; + if($iCode != 0){ + return jsonError($sMsg); + } + } + $production_info = $this->production_article_obj->where($aWhere)->find(); + if(!$production_info){ + return jsonError("Object is null"); + } + //判断数据是否存在 不存在直接入库数据 chengxiaoling 20251119 end + $file = ROOT_PATH . 'public' . DS . "referFile" . DS . $data['referFile']; + $list = $this->readRefersExcelNew($file,$production_info['p_article_id'],$data['article_id']); + if(!empty($list)){ + $result = $this->production_article_refer_obj->insertAll($list); + } + //写入获取参考文献详情队列 + \think\Queue::push('app\api\job\ArticleReferQueue@fire',['article_id' => $data['article_id'],'p_article_id' => $production_info['p_article_id']], 'ArticleReferQueue'); + return jsonSuccess([]); + } + public function readRefersExcelNew($afile,$p_article_id = 0,$article_id = 0){ + if(empty($p_article_id)){ + return []; + } + //查询参考文献 + $aWhere = ['p_article_id' => $p_article_id,'article_id' => $article_id,'state' => 0,'refer_doi' => ['<>','']]; + $aRefer = Db::name('production_article_refer')->where($aWhere)->column('refer_doi'); + $extension = substr($afile, strrpos($afile, '.') + 1); + vendor("PHPExcel.PHPExcel"); + if ($extension == 'xlsx') { + $objReader = new \PHPExcel_Reader_Excel2007(); + $objPHPExcel = $objReader->load($afile); + } else if ($extension == 'xls') { + $objReader = new \PHPExcel_Reader_Excel5(); + $objPHPExcel = $objReader->load($afile); + } + $sheet = $objPHPExcel->getSheet(0); + $highestRow = $sheet->getHighestRow(); + $frag = []; + $k = 0; + for ($i = 2; $i <= $highestRow; $i++) { + if ($objPHPExcel->getActiveSheet()->getCell("A" . $i)->getValue() == '') { + continue; + } + $aa['refer_content'] = $objPHPExcel->getActiveSheet()->getCell("A" . $i)->getValue(); + $aa['refer_doi'] = $objPHPExcel->getActiveSheet()->getCell("B" . $i)->getValue(); + if(empty($aa['refer_doi'])){ + continue; + } + $aa['refer_doi'] = trim($aa['refer_doi']); + $aa['refer_content'] = trim($aa['refer_content']); + //判断是否添加过 + $doiLinkPattern = '/^(https?:\/\/)?([a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+\.[a-zA-Z]{2,}(\/.*)?$/i'; + if(preg_match($doiLinkPattern, $aa['refer_doi'], $matches)){ + if(in_array($aa['refer_doi'], $aRefer)){ + continue; + } + } + $aa['p_article_id'] = $p_article_id; + $aa['article_id'] = $article_id; + $aa['index'] = $k; + $aa['ctime'] = time(); + $aa['index'] = $k; + $aa['refer_frag'] = $aa['refer_content']; + $aa['refer_type'] = 'other'; + $aa['is_change'] = 2; + $k++; + $frag[] = $aa; + } + return $frag; + } + //获取参考文献的状态 + public function getReferState(){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + //获取文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select the article']); + } + $aWhere = ['article_id' => $iArticleId,'state' => 0]; + $aProductionArticle = $this->production_article_obj->field('p_article_id')->where($aWhere)->find(); + $iPArticleId = empty($aProductionArticle['p_article_id']) ? 0 : $aProductionArticle['p_article_id']; + if(empty($iPArticleId)) { + return json_encode(array('status' => 2,'msg' => 'No articles found')); + } + //查询参考文献数据 + $aWhere = ['p_article_id' => $iPArticleId,'article_id' => $iArticleId,'state' => 0]; + $iCount = Db::name('production_article_refer')->where($aWhere)->count(); + if(empty($iCount)){ + return json_encode(array('status' => 3,'msg' => 'Reference is empty','data' => ['total' => 0,'unprocessed_total' => 0,'processed_total' => 0])); + } + //获取未处理的数据 + $aWhere['is_deal'] = 2; + $aUnprocessed = Db::name('production_article_refer')->where($aWhere)->select(); + //获取已处理的数据 + $aWhere['is_deal'] = 1; + $aProcessed = Db::name('production_article_refer')->where($aWhere)->select(); + //未处理的数量 + $iUnprocessed = empty($aUnprocessed) ? 0 : count($aUnprocessed); + //已处理的数量 + $iProcessed = empty($aProcessed) ? 0 : count($aProcessed); + + //数据组合 + $aRefer = array_merge($aUnprocessed,$aProcessed); + $aRefer = ['total' => $iCount,'unprocessed_total' => $iUnprocessed,'processed_total' => $iProcessed,'refer' => $aRefer]; + return json_encode(['status' => 1,'msg' => 'success','data' => $aRefer]); + } + + //使文章进入生产状态 + public function beginProduce(){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + //获取文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return jsonError('Please select the article'); + exit(); + } + //查询是否缴费 + $aWhere = ['article_id' => $iArticleId,'state' => ['in',[5,6]]]; + $aArticle = $this->article_obj->field('is_buy')->where($aWhere)->find(); + if(empty($aArticle)){ + return jsonError('No article information found'); + exit(); + } + $iIsBuy = empty($aArticle['is_buy']) ? 0 : $aArticle['is_buy']; + if($iIsBuy != 1){ + return jsonError('Article unpaid'); + exit(); + } + $aWhere = ['article_id' => $iArticleId,'state' => 0]; + $aProductionArticle = $this->production_article_obj->field('p_article_id')->where($aWhere)->find(); + if(!empty($aProductionArticle)) { + return jsonError('The article has entered production'); + exit(); + } + $aResult = $this->addProductionEx($iArticleId); + $aResult = empty($aResult->getData()) ? [] : $aResult->getData(); + $iCode = isset($aResult['code']) ? $aResult['code'] : -1; + $sMsg = empty($aResult['msg']) ? 'Data processing failed' : $aResult['msg']; + if($iCode != 0){ + return jsonError($sMsg); + exit(); + } + return jsonSuccess([]); + } } From eabde1d1385430cd02c03b4c0c77b8f333860f1a Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 20 Nov 2025 15:27:42 +0800 Subject: [PATCH 04/53] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=8F=82=E8=80=83?= =?UTF-8?q?=E6=96=87=E7=8C=AE=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/Preaccept.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/application/api/controller/Preaccept.php b/application/api/controller/Preaccept.php index a918dff0..e1e2de6b 100644 --- a/application/api/controller/Preaccept.php +++ b/application/api/controller/Preaccept.php @@ -1726,25 +1726,21 @@ return null; $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; if(empty($iArticleId)){ return jsonError('Please select the article'); - exit(); } //查询是否缴费 $aWhere = ['article_id' => $iArticleId,'state' => ['in',[5,6]]]; $aArticle = $this->article_obj->field('is_buy')->where($aWhere)->find(); if(empty($aArticle)){ return jsonError('No article information found'); - exit(); } $iIsBuy = empty($aArticle['is_buy']) ? 0 : $aArticle['is_buy']; if($iIsBuy != 1){ return jsonError('Article unpaid'); - exit(); } $aWhere = ['article_id' => $iArticleId,'state' => 0]; $aProductionArticle = $this->production_article_obj->field('p_article_id')->where($aWhere)->find(); if(!empty($aProductionArticle)) { return jsonError('The article has entered production'); - exit(); } $aResult = $this->addProductionEx($iArticleId); $aResult = empty($aResult->getData()) ? [] : $aResult->getData(); @@ -1752,7 +1748,6 @@ return null; $sMsg = empty($aResult['msg']) ? 'Data processing failed' : $aResult['msg']; if($iCode != 0){ return jsonError($sMsg); - exit(); } return jsonSuccess([]); } From 99ada381148b69727679c863f293be2b01f9fbf5 Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 24 Nov 2025 17:43:41 +0800 Subject: [PATCH 05/53] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Finalreview.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/application/api/controller/Finalreview.php b/application/api/controller/Finalreview.php index 95b72cbd..bbb57a83 100644 --- a/application/api/controller/Finalreview.php +++ b/application/api/controller/Finalreview.php @@ -1081,15 +1081,17 @@ class Finalreview extends Base $aQuestion = empty($aQuestion) ? [] : array_column($aQuestion, null,'art_rev_id'); //查询复审 - $aReviewerRepeatLists = []; - $aWhere = ['art_rev_id' => ['in',$aArtRevId],'recommend' => ['between',[1,3]]]; + $aReviewerRepeatLists = $aReviewerRepeatListsData = []; + $aWhere = ['art_rev_id' => ['in',$aArtRevId],'recommend' => ['between',[0,3]]]; $aReviewerRepeat = Db::name('article_reviewer_repeat')->field('art_rev_rep_id,art_rev_id,recommend,ctime,stime,state')->where($aWhere)->select(); if(!empty($aReviewerRepeat)){ foreach ($aReviewerRepeat as $key => $value) { - $aReviewerRepeatLists[$value['art_rev_id']][] = $value; + if(in_array($value['recommend'], [1,2,3])){ + $aReviewerRepeatLists[$value['art_rev_id']][] = $value; + } + $aReviewerRepeatListsData[$value['art_rev_id']][] = $value; } } - //查询作者信息 $aUserId = array_unique(array_column($aArticleReviewer, 'reviewer_id')); $aWhere = ['user_id' => ['in',$aUserId],'state' => 0]; @@ -1101,9 +1103,9 @@ class Finalreview extends Base $value['score'] = empty($aQuestionData['score']) ? 0 : $aQuestionData['score']; $value['repeat'] = empty($aReviewerRepeatLists[$value['art_rev_id']]) ? [] : $aReviewerRepeatLists[$value['art_rev_id']]; //判断是否复审 - $value['can_repeat'] = 1; - if(!empty($value['repeat'])){ - $aEnd = end($value['repeat']); + $value['can_repeat'] = empty($aQuestionData) ? 0 : 1; + if(!empty($aReviewerRepeatListsData[$value['art_rev_id']])){ + $aEnd = end($aReviewerRepeatListsData[$value['art_rev_id']]); $iState = empty($aEnd['state']) ? 0 : $aEnd['state']; $iRecommend = empty($aEnd['recommend']) ? 0 : $aEnd['recommend']; if($iState == 1 && $iRecommend == 3){ From bd2305d83df7b7959acc5514182db487a2841c7e Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 26 Nov 2025 17:34:47 +0800 Subject: [PATCH 06/53] =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/OpenAi.php | 50 ++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/application/common/OpenAi.php b/application/common/OpenAi.php index 258ac87b..bd290203 100644 --- a/application/common/OpenAi.php +++ b/application/common/OpenAi.php @@ -445,16 +445,21 @@ class OpenAi $model = empty($aParam['model']) ? 'gpt-4.1' : $aParam['model']; //接口地址 - $sUrl = $this->sUrl; + $sUrl = empty($aParam['url']) ? $this->sUrl : $aParam['url']; // 降低随机性(0-1,0为最确定) - $iTemperature = empty($aParam['temperature']) ? '0.1' : $aParam['temperature']; - + $iTemperature = empty($aParam['temperature']) ? 0 : $aParam['temperature']; + $iTop = empty($aParam['top_p']) ? 0.9 : $aParam['top_p']; + $sApiKey = empty($aParam['api_key']) ? '' : $aParam['api_key']; //组装数据 $data = [ 'model' => $model, 'messages' => $aMessage, 'temperature' => $iTemperature, + "max_tokens" => 1500, // 足够容纳结构化结论,避免截断 + // "top_p" => $iTop, // 控制多样性,1.0 表示不限制 + // "frequency_penalty" => 0.0, // 避免重复内容 + // "presence_penalty" => 0.0 ]; $this->curl = curl_init(); @@ -463,7 +468,7 @@ class OpenAi // 设置头信息 curl_setopt($this->curl, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', - 'Authorization: Bearer ' . $this->sApiKey + 'Authorization: Bearer ' . $sApiKey ]); curl_setopt($this->curl, CURLOPT_PROXY,$this->proxy); curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER,true); @@ -522,6 +527,7 @@ class OpenAi $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']; + $sApiKey = empty($aParam['api_key']) ? '' : $aParam['api_key']; // 组装数据 - 增加流式传输必要参数 $data = [ 'model' => $model, @@ -539,7 +545,7 @@ class OpenAi curl_setopt($this->curl, CURLOPT_URL, $sUrl); curl_setopt($this->curl, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', - 'Authorization: Bearer ' . $this->sApiKey, + 'Authorization: Bearer ' . $sApiKey, 'Accept: text/event-stream', 'Cache-Control: no-cache', 'Connection: keep-alive', // 保持长连接 @@ -879,6 +885,8 @@ class OpenAi if(empty($sSysMessagePrompt)){ return []; } + // 解析JSON + $sSysMessagePrompt = $this->fixEncoding($sSysMessagePrompt); $aQuestion = empty($aQuestion[$sQuestionLevel]) ? [] : $aQuestion[$sQuestionLevel]; if(empty($aQuestion)){ return []; @@ -892,7 +900,10 @@ class OpenAi } //系统角色 $sSysMessageInfo = empty($aScopeReturn[$key]) ? '' : json_encode($aScopeReturn[$key],JSON_UNESCAPED_UNICODE); + // 解析JSON + $sSysMessageInfo = $this->fixEncoding($sSysMessageInfo); $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $value); + $sUserPrompt = $this->fixEncoding($sUserPrompt); $aMessage[] = [ ['role' => 'system', 'content' => $sSysMessagePrompt.$sSysMessageInfo], ['role' => 'user', 'content' => $sUserPrompt], @@ -1025,6 +1036,8 @@ class OpenAi if(empty($sSysMessagePrompt)){ return []; } + // 解析JSON + $sSysMessagePrompt = $this->fixEncoding($sSysMessagePrompt); $aQuestion = empty($aQuestion[$sQuestionLevel]) ? [] : $aQuestion[$sQuestionLevel]; if(empty($aQuestion)){ return []; @@ -1039,7 +1052,7 @@ class OpenAi if(empty($sSysMessageInfo)){ return []; } - + $sSysMessageInfo = $this->fixEncoding($sSysMessageInfo); //处理数据 $aContent = empty($aSearch['content']) ? '' : $aSearch['content']; if(empty($aContent)){ @@ -1048,6 +1061,7 @@ class OpenAi foreach ($aContent as $key => $value) { $aSearch['content'] = $value; $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $aQuestion[$sQuestionFields]); + $sUserPrompt = $this->fixEncoding($sUserPrompt); $aMessage[] = [ ['role' => 'system', 'content' => $sSysMessagePrompt.$sSysMessageInfo], ['role' => 'user', 'content' => $sUserPrompt], @@ -1055,6 +1069,30 @@ class OpenAi } return $aMessage; } + + private function fixEncoding($content) { + // 1. 检查是否为UTF-8,不是则尝试多种常见编码转换 + if (!mb_check_encoding($content, 'UTF-8')) { + // 优先尝试西方编码(因"10.1016/j"是DOI格式,常见于英文文献) + $encodings = ['Windows-1252', 'ISO-8859-1', 'GBK', 'GB2312']; + foreach ($encodings as $encoding) { + $converted = mb_convert_encoding($content, 'UTF-8', $encoding); + if (mb_check_encoding($converted, 'UTF-8')) { + $content = $converted; + break; + } + } + } + + // 2. 过滤残留的乱码字符(保留字母、数字、空格及DOI相关符号) + // 允许的字符:a-z、A-Z、0-9、空格、.、:、/、-、_、(、)、, + $content = preg_replace('/[^\p{L}\p{N}\s\.\:\/\-\_\(\),]/u', '', $content); + + // 3. 去除首尾多余空格 + $content = trim($content); + + return $content; + } /** * 微信公众号-生成公微内容(CURL) */ From f9ba4c0d6fb0d5814f5aa10b4d374d18e9307ed1 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 27 Nov 2025 09:33:09 +0800 Subject: [PATCH 07/53] =?UTF-8?q?=E6=A0=87=E9=A2=98=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index f1e67aa4..651f5166 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -45,7 +45,7 @@ class Workbench extends Base //标题 $sTitle = empty($aParam['title']) ? '': $aParam['title']; if(!empty($sTitle)){ - $aWhere = ['title' => trim($sTitle)]; + $aWhere = ['title' => ['like','%'.trim($sTitle).'%']]; } //国家 $sCountry = -1; From 27c7f88c0b05203894e6afb51e8da133787559dc Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 27 Nov 2025 10:24:29 +0800 Subject: [PATCH 08/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Preaccept.php | 122 +++++++++++------------ 1 file changed, 56 insertions(+), 66 deletions(-) diff --git a/application/api/controller/Preaccept.php b/application/api/controller/Preaccept.php index e1e2de6b..603a0711 100644 --- a/application/api/controller/Preaccept.php +++ b/application/api/controller/Preaccept.php @@ -37,14 +37,17 @@ class Preaccept extends Base } $dois = $this->production_article_refer_obj->where("p_article_id", $production_info['p_article_id'])->where("refer_doi","<>","")->where("state",0)->group("refer_doi")->having("count(*)>1")->column("refer_doi"); $list = $this->production_article_refer_obj->where("p_article_id", $production_info['p_article_id'])->where('state', 0)->order("index")->select(); + $aRepeat = []; foreach ($list as $k => $v){ if(in_array($v['refer_doi'],$dois)){ $list[$k]['is_repeat'] = 1; + $aRepeat[$v['refer_doi']][] = $v['index']; }else{ $list[$k]['is_repeat'] = 0; } } $re["refers"] = $list; + $re['repeat'] = empty($aRepeat) ? [] : $aRepeat; return jsonSuccess($re); } @@ -752,44 +755,23 @@ class Preaccept extends Base } $article_info = $this->article_obj->where("article_id",$data['article_id'])->find(); $journal_info = $this->journal_obj->where("journal_id",$article_info['journal_id'])->find(); - if(intval($journal_info['fee'])==0||$article_info['ctime']<1735660800){//非收费期刊的文章直接返回 - $re['state'] = 1; - $re['order'] = null; - $re["fee"] = 0; - return jsonSuccess($re); - } $order_info = $this->order_obj->where("article_id",$article_info['article_id'])->find(); - if($order_info==null){//这里有疑问 - $re['state'] = 1; - $re['order'] = null; - $re["fee"] = 0; - return jsonSuccess($re); - } -// if($order_info['pay_type']==2){ - $paystation = $this->paystation_obj->where("ps_id",$order_info['ps_id'])->find(); - $order_info['paystation'] = $paystation; - if($order_info['state']==0){ - $res = object_to_array(json_decode(paystationLookup($paystation['transaction_id']))); - if(isset($res['result']['success'])&&$res['result']['success']){ - $this->article_obj->where("article_id",$order_info['article_id'])->update(['is_buy'=>1]); - $this->order_obj->where("order_id",$order_info['order_id'])->update(['state'=>1]); - $re['state'] = 1; - $re['fee'] = $journal_info['fee']; - $re['order'] = $order_info; - return jsonSuccess($re); - }else{ - $re['state'] = 0; - $re['fee'] = $journal_info['fee']; - $re['order'] = $order_info; - return jsonSuccess($re); - } - }else{ - $re['state'] = 1; - $re['fee'] = $journal_info['fee']; - $re['order'] = $order_info; - return jsonSuccess($re); + $paystation = $this->paystation_obj->where("ps_id",$order_info['ps_id'])->find(); + $order_info['paystation'] = $paystation; + if($order_info['state']==0){ + $res = xml_to_array(paystationLookup($paystation['merchant_session'])); + if(isset($res['PaystationQuickLookup']['LookupResponse']['Authentication']['auth_Status'])&&$res['PaystationQuickLookup']['LookupResponse']['Authentication']['auth_Status']=="Y"){ + $this->article_obj->where("article_id",$order_info['article_id'])->update(['is_buy'=>1]); + $this->order_obj->where("order_id",$order_info['order_id'])->update(['state'=>1]); + $article_info = $this->article_obj->where("article_id",$data['article_id'])->find(); } -// } + } + $re['state'] = $article_info['is_buy']; + $re['order'] = $order_info; + $re["fee"] = $article_info['fee']; + $re['article'] = $article_info; + $re['journal'] = $journal_info; + return jsonSuccess($re); } @@ -1633,9 +1615,9 @@ return null; if(empty($p_article_id)){ return []; } - //查询参考文献 - $aWhere = ['p_article_id' => $p_article_id,'article_id' => $article_id,'state' => 0,'refer_doi' => ['<>','']]; - $aRefer = Db::name('production_article_refer')->where($aWhere)->column('refer_doi'); + // //查询参考文献 + // $aWhere = ['p_article_id' => $p_article_id,'article_id' => $article_id,'state' => 0,'refer_doi' => ['<>','']]; + // $aRefer = Db::name('production_article_refer')->where($aWhere)->column('refer_doi'); $extension = substr($afile, strrpos($afile, '.') + 1); vendor("PHPExcel.PHPExcel"); if ($extension == 'xlsx') { @@ -1660,13 +1642,13 @@ return null; } $aa['refer_doi'] = trim($aa['refer_doi']); $aa['refer_content'] = trim($aa['refer_content']); - //判断是否添加过 - $doiLinkPattern = '/^(https?:\/\/)?([a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+\.[a-zA-Z]{2,}(\/.*)?$/i'; - if(preg_match($doiLinkPattern, $aa['refer_doi'], $matches)){ - if(in_array($aa['refer_doi'], $aRefer)){ - continue; - } - } + // //判断是否添加过 + // $doiLinkPattern = '/^(https?:\/\/)?([a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+\.[a-zA-Z]{2,}(\/.*)?$/i'; + // if(preg_match($doiLinkPattern, $aa['refer_doi'], $matches)){ + // if(in_array($aa['refer_doi'], $aRefer)){ + // continue; + // } + // } $aa['p_article_id'] = $p_article_id; $aa['article_id'] = $article_id; $aa['index'] = $k; @@ -1674,7 +1656,7 @@ return null; $aa['index'] = $k; $aa['refer_frag'] = $aa['refer_content']; $aa['refer_type'] = 'other'; - $aa['is_change'] = 2; + $aa['is_deal'] = 2; $k++; $frag[] = $aa; } @@ -1697,23 +1679,30 @@ return null; } //查询参考文献数据 $aWhere = ['p_article_id' => $iPArticleId,'article_id' => $iArticleId,'state' => 0]; - $iCount = Db::name('production_article_refer')->where($aWhere)->count(); - if(empty($iCount)){ + $aRefer = Db::name('production_article_refer')->where($aWhere)->select(); + if(empty($aRefer)){ return json_encode(array('status' => 3,'msg' => 'Reference is empty','data' => ['total' => 0,'unprocessed_total' => 0,'processed_total' => 0])); } - //获取未处理的数据 - $aWhere['is_deal'] = 2; - $aUnprocessed = Db::name('production_article_refer')->where($aWhere)->select(); - //获取已处理的数据 - $aWhere['is_deal'] = 1; - $aProcessed = Db::name('production_article_refer')->where($aWhere)->select(); - //未处理的数量 - $iUnprocessed = empty($aUnprocessed) ? 0 : count($aUnprocessed); - //已处理的数量 - $iProcessed = empty($aProcessed) ? 0 : count($aProcessed); - - //数据组合 - $aRefer = array_merge($aUnprocessed,$aProcessed); + //获取总数量 + $iCount = empty($aRefer) ? 0 : count($aRefer); + $aWhere["refer_doi"] = ["<>",""]; + $aDoi = Db::name('production_article_refer')->field('count(p_article_id) as num,refer_doi')->where($aWhere)->group('refer_doi')->select(); + $aDoi = empty($aDoi) ? [] : array_column($aDoi, 'num','refer_doi'); + //数据处理 + $iUnprocessed = $iProcessed = 0; + foreach ($aRefer as $key => $value) { + if($value['is_deal'] == 1){ + $iProcessed++; + } + if($value['is_deal'] == 2){ + $iUnprocessed++; + } + $iIsRepat = 0; + if(!empty($value['refer_doi']) && (!empty($aDoi[$value['refer_doi']]) && $aDoi[$value['refer_doi']] > 1)){ + $iIsRepat = 1; + } + $aRefer[$key]['is_repeat'] = $iIsRepat; + } $aRefer = ['total' => $iCount,'unprocessed_total' => $iUnprocessed,'processed_total' => $iProcessed,'refer' => $aRefer]; return json_encode(['status' => 1,'msg' => 'success','data' => $aRefer]); } @@ -1733,10 +1722,10 @@ return null; if(empty($aArticle)){ return jsonError('No article information found'); } - $iIsBuy = empty($aArticle['is_buy']) ? 0 : $aArticle['is_buy']; - if($iIsBuy != 1){ - return jsonError('Article unpaid'); - } + // $iIsBuy = empty($aArticle['is_buy']) ? 0 : $aArticle['is_buy']; + // if($iIsBuy != 1){ + // return jsonError('Article unpaid'); + // } $aWhere = ['article_id' => $iArticleId,'state' => 0]; $aProductionArticle = $this->production_article_obj->field('p_article_id')->where($aWhere)->find(); if(!empty($aProductionArticle)) { @@ -1751,4 +1740,5 @@ return null; } return jsonSuccess([]); } + } From f5c59d222fdc60cd2e4fdd25d8020eea119572f6 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 27 Nov 2025 10:25:33 +0800 Subject: [PATCH 09/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Production.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/api/controller/Production.php b/application/api/controller/Production.php index 769f20f7..38c2a992 100644 --- a/application/api/controller/Production.php +++ b/application/api/controller/Production.php @@ -592,14 +592,17 @@ class Production extends Base $this->refuseReferIndex($data['p_article_id']); $dois = $this->production_article_refer_obj->where("p_article_id", $data['p_article_id'])->where("refer_doi","<>","")->where("state",0)->group("refer_doi")->having("count(*)>1")->column("refer_doi"); $list = $this->production_article_refer_obj->where('p_article_id', $data['p_article_id'])->where('state', 0)->order("index")->select(); + $aRepeat = []; foreach ($list as $k => $v){ if(in_array($v['refer_doi'],$dois)){ $list[$k]['is_repeat'] = 1; + $aRepeat[$v['refer_doi']][] = $v['index']; }else{ $list[$k]['is_repeat'] = 0; } } $re['refers'] = $list; + $re['repeat'] = empty($aRepeat) ? [] : $aRepeat; return jsonSuccess($re); } From 30995b21946ecbf469b4a9a5c681903d5ef86bf1 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 27 Nov 2025 13:38:52 +0800 Subject: [PATCH 10/53] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ArticleParserService.php | 765 ++++++++------------ 1 file changed, 319 insertions(+), 446 deletions(-) diff --git a/application/common/ArticleParserService.php b/application/common/ArticleParserService.php index 9b93a37d..009af51e 100644 --- a/application/common/ArticleParserService.php +++ b/application/common/ArticleParserService.php @@ -225,6 +225,10 @@ class ArticleParserService $aParam['corresponding'] = $oDealFile->getCorrespondingAuthors($aParam); //keywords 和 摘要 $aContent = $oDealFile->extractFromWord(); + if(!mb_check_encoding($sTitle, 'UTF-8')){ + $sTitle = mb_convert_encoding($sTitle, 'UTF-8', 'GBK'); + } + $aParam['title'] = $oDealFile->fullDecode($aParam['title']); $aParam += empty($aContent['data']) ? [] : $aContent['data']; return json_encode(['status' => 1,'msg' => 'success','data' => $aParam]); } @@ -247,183 +251,18 @@ class ArticleParserService } } } - if(!empty($title) && !mb_check_encoding($title, 'UTF-8')){ - $title = mb_convert_encoding($title, 'UTF-8', 'GBK'); - } return $title; } - // 提取作者 - // private function getAuthors($aParam = []) { - // $title = empty($aParam['title']) ? $this->getTitle() : $aParam['title']; - // $sAuthorContent = $this->getNextParagraphAfterText($title); - // if (empty($sAuthorContent)) { - // return ['author' => [], 'report' => []]; - // } - - // //编码修复 - // $possibleEncodings = [ - // 'Windows-1252', 'UTF-8', 'GBK', 'GB2312', - // 'Latin-1', 'ISO-8859-1', 'CP1252' - // ]; - // $encodedContent = @mb_convert_encoding($sAuthorContent, 'UTF-8', implode(',', $possibleEncodings)); - // $sAuthorContent = $encodedContent ?: $sAuthorContent; - - // //清理不可见字符 - // $sAuthorContent = preg_replace('/[\x00-\x1F\x7F\x{200B}-\x{200F}]/u', '', $sAuthorContent); - - // //修复特殊符号乱码 - // $symbolMap = [ - // '†' => '†', 'â ' => '†', 'â' => '†', '?†' => '†', - // ':' => ':', ',' => ',', '—' => '-', - // '啊' => '' // 针对性移除异常字符“啊”(若为固定乱码) - // ]; - // $sAuthorContent = strtr($sAuthorContent, $symbolMap); - - // //格式标准化 - // $sAuthorContent = str_replace([',', ';', ';', '、'], ',', $sAuthorContent); // 统一分隔符 - // $sAuthorContent = preg_replace('/\s+and\s+/i', ', ', $sAuthorContent); // and转逗号 - // $sAuthorContent = preg_replace('/\s+/', ' ', $sAuthorContent); // 合并多余空格 - // $sAuthorContent = trim($sAuthorContent); - - // // 处理作者 - // $content = mb_convert_encoding($sAuthorContent, 'UTF-8', 'auto'); // 确保编码正确 - // $content = str_replace("\xC2\xA0", ' ', $content); // 替换非-breaking空格为普通空格 - // $content = preg_replace('/(\d+)\s*([*#†])/', '$1$2', $content); // 合并"1 *"为"1*"、"1 #"为"1#" - // $content = preg_replace('/,†/', ',†', $content); // 保留"1,†"格式(防止被拆分) - // //标记上标内的逗号+空格(多编号) - // $tempStr = preg_replace('/(\d+)\s*,\s*(\d+)/', '$1$2', $content); - // // 原有步骤2:正则匹配(扩展上标符号支持,保持原有逻辑) - // $pattern = '/ - // ([A-Za-z\s\.\-]+?) # 姓名(支持缩写、空格) - // \s* # 姓名与上标间空格 - // ( # 上标组(扩展符号支持) - // \d+ # 起始数字 - // (?:[†#*,]|\d+)* # 允许:†#*符号、逗号、+数字(兼容1,†、1,*等) - // ) - // \s*,? # 作者间逗号(可选) - // (?=\s|$) # 确保后面是空格或结尾 - // /ux'; - - // preg_match_all($pattern, $tempStr, $matches); - // $authorList = []; - // if(!empty($matches[1])){ - // foreach ($matches[1] as $i => $name) { - // $name = trim($name); - // $superscript = trim($matches[2][$i]); - // $superscript = str_replace('', ',', $superscript); // 恢复多编号逗号 - // $superscript = preg_replace('/,$/', '', $superscript); // 清理末尾逗号 - // // 修复符号与数字间的空格(如原始"1 *"被误处理为"1*"的情况,保持原样) - // $superscript = preg_replace('/(\d)([*#†])/', '$1$2', $superscript); - // if (!empty($name)) { - // $authorList[] = [ - // 'name' => $name, - // 'superscript' => $superscript - // ]; - // } - // } - // }else { - // // 按“两个或多个连续空格”拆分(姓名之间的分隔) - // $authorList = array_filter( - // array_map('trim', - // preg_split('/(,\p{Z}*|\p{Z}{2,})/u', $sAuthorContent) - // ) - // ); - // } - - - // // //处理作者 - // // $authorList = []; - // // // 新正则:匹配“姓名+上标”整体,允许上标含逗号(如1,†) - // // // 逻辑:姓名以字母/中文开头,上标以数字开头、以符号/数字结尾 - // // // if (preg_match_all('/([A-Za-z\x{4e00}-\x{9fa5}][A-Za-z\s·\-\'\x{4e00}-\x{9fa5}]*)\s*([\d,†#*]+)/u', $sAuthorContent, $matches)) { - // // if(preg_match_all('/([A-Za-z\x{4e00}-\x{9fa5}][A-Za-z\s·\-\'\x{4e00}-\x{9fa5}]*)\s*(\d[\d,†#\s*]*)/u', $sAuthorContent, $matches)){ - // // for ($i = 0; $i < count($matches[1]); $i++) { - // // $authorList[] = trim($matches[1][$i] . $matches[2][$i]); - // // } - // // } else { - // // // 按“两个或多个连续空格”拆分(姓名之间的分隔) - // // $authorList = array_filter( - // // array_map('trim', - // // preg_split('/(,\p{Z}*|\p{Z}{2,})/u', $sAuthorContent) - // // ) - // // ); - // // } - // $aAuthorData = []; - // $aReport = []; - // $namePattern = '/ - // (?:[A-Za-z\s·\-\']+| # 英文姓名(支持空格、连字符) - // [\x{4e00}-\x{9fa5}]+| # 中文姓名 - // [\x{1800}-\x{18AF}]+| # 蒙古文姓名 - // [A-Z]\.) # 单字母缩写(如 J.) - // /ux'; - // var_dump($authorList);exit; - // foreach ($authorList as $authorStr) { - // if (empty($authorStr)) continue; - // var_dump($authorList);exit; - // //分离姓名与上标(支持上标含逗号,如1,†) - // $superscript = ''; - // // 新正则:匹配以数字开头、含逗号/符号的完整上标(如1,†、2*#) - // $authorStr = trim(trim($authorStr,','),' '); - // // if (preg_match('/([\d,†#*]+)$/u', $authorStr, $supMatch)) { - // // if(preg_match('/\s*([\d,†#* ]+)$/u', $authorStr, $supMatch)){ - // // if (preg_match('/.*?\s*([\d,†#* ]+)$/u', $authorStr, $supMatch)) { - // // if (preg_match('/.*?\s*([\d,\x{2020}#* ]+?)\s*$/u', $authorStr, $supMatch)) { - // // if (preg_match('/^(.+?)\D*?(\d[\d,#*†,\s]*)$/u', $authorStr, $supMatch)) { - // // $superscript = $supMatch[1]; - // // // 移除上标,保留纯姓名(避免残留符号) - // // $nameStr = trim(preg_replace('/' . preg_quote($superscript, '/') . '$/', '', $authorStr)); - // // } else { - // // $nameStr = $authorStr; - // // } - // $pattern = '/^(.+?)\s*(\d[\d,#*†\s]*?)\s*$/u'; - // if (preg_match($pattern, $authorStr, $supMatch)) { - // $nameStr = empty($supMatch[1]) ? '' : trim($supMatch[1]); // 姓名部分:"Liguo Zhang" - // $superscript = empty($supMatch[2]) ? $nameStr : $nameStr.trim($supMatch[2]); // 上标部分:"1 - // // echo "姓名: $nameStr, 上标: $superscript\n"; - // } else { - // $nameStr = $authorStr; - // } - // //验证姓名合法性(过滤无效内容) - // if (!preg_match($namePattern, $nameStr)) { - // continue; - // } - // //解析上标信息(正确识别1,†中的机构编号和符号) - // $companyId = ''; - // $isSuper = 0; - // $isReport = 0; - // if (!empty($superscript)) { - // // 提取机构编号(忽略上标中的逗号,如1,† → 提取1) - // if (preg_match('/(\d+)/', $superscript, $numMatch)) { - // $companyId = $numMatch[1]; - // } - // // 识别特殊符号(#为超级作者,*†为通讯作者) - // $isSuper = strpos($superscript, '#') !== false ? 1 : 0; - // $isReport = (strpos($superscript, '*') !== false || strpos($superscript, '†') !== false) ? 1 : 0; - // } - // if (preg_match("/^([A-Za-z\s'\.-]+)/u", $nameStr, $match)) { - // $nameStr = trim($match[1]); - // } - // $aAuthorData[] = [ - // 'name' => $nameStr, - // 'company_id' => $companyId, - // 'is_super' => $isSuper, - // 'is_report' => $isReport - // ]; - // if ($isReport) { - // $aReport[] = $nameStr; - // } - // } - // var_dump($aAuthorData);exit; - // return ['author' => $aAuthorData,'report' => array_unique($aReport)]; - // } // 提取作者 private function parseAuthorsWithoutRegex($str = '') { if (empty($str)) { return []; } - // 清理乱码和特殊字符(扩展全角数字处理) - $str = mb_convert_encoding($str, 'UTF-8', 'auto'); + if(!mb_check_encoding($str, 'UTF-8')){ + $str = mb_convert_encoding($str, 'UTF-8', 'GBK'); + } + $str = $this->fullDecode($str); $str = str_replace(["\xC2\xA0", 'ï¼', '�', ',', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], [' ', ' ', ' ', ' ', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], $str); $str = trim(str_replace([' and ', ' AND ', ' And '], ', ', $str)); @@ -584,15 +423,10 @@ class ArticleParserService if (empty($sAuthorContent)) { return ['author' => [], 'report' => []]; } - - //编码修复 - $possibleEncodings = [ - 'Windows-1252', 'UTF-8', 'GBK', 'GB2312', - 'Latin-1', 'ISO-8859-1', 'CP1252' - ]; - $encodedContent = @mb_convert_encoding($sAuthorContent, 'UTF-8', implode(',', $possibleEncodings)); - $sAuthorContent = $encodedContent ?: $sAuthorContent; - + if(!mb_check_encoding($sAuthorContent, 'UTF-8')){ + $sAuthorContent = mb_convert_encoding($sAuthorContent, 'UTF-8', 'GBK'); + } + $sAuthorContent = $this->fullDecode($sAuthorContent); //清理不可见字符 $sAuthorContent = preg_replace('/[\x00-\x1F\x7F\x{200B}-\x{200F}]/u', '', $sAuthorContent); @@ -614,14 +448,10 @@ class ArticleParserService return ['author' => [],'report' => []]; } $aReport = $aAuthorData = []; - foreach ($aAuthor as $key => $value) { if(empty($value['name']) && empty($value['superscript'])){ continue; } - if(!mb_check_encoding($value['name'], 'UTF-8')){ - $value['name'] = mb_convert_encoding($value['name'], 'UTF-8', 'GBK'); - } if(!empty($value['name']) && !empty($value['is_report']) && $value['is_report'] == 1){ $aReport[] = $value['name']; } @@ -629,175 +459,6 @@ class ArticleParserService } return ['author' => $aAuthorData,'report' => array_unique($aReport)]; } -// private function getAuthors($aParam = []) { -// $title = empty($aParam['title']) ? $this->getTitle() : $aParam['title']; -// $sAuthorContent = $this->getNextParagraphAfterText($title); -// if (empty($sAuthorContent)) { -// return ['author' => [], 'report' => []]; -// } - -// //编码修复 -// $possibleEncodings = [ -// 'Windows-1252', 'UTF-8', 'GBK', 'GB2312', -// 'Latin-1', 'ISO-8859-1', 'CP1252' -// ]; -// $encodedContent = @mb_convert_encoding($sAuthorContent, 'UTF-8', implode(',', $possibleEncodings)); -// $sAuthorContent = $encodedContent ?: $sAuthorContent; - -// //清理不可见字符 -// $sAuthorContent = preg_replace('/[\x00-\x1F\x7F\x{200B}-\x{200F}]/u', '', $sAuthorContent); - -// //修复特殊符号乱码 -// $symbolMap = [ -// '†' => '†', 'â ' => '†', 'â' => '†', '?†' => '†', -// ':' => ':', ',' => ',', '—' => '-', -// '啊' => '' // 针对性移除异常字符“啊”(若为固定乱码) -// ]; -// $sAuthorContent = strtr($sAuthorContent, $symbolMap); - -// //格式标准化 -// $sAuthorContent = str_replace([',', ';', ';', '、'], ',', $sAuthorContent); // 统一分隔符 -// $sAuthorContent = preg_replace('/\s+and\s+/i', ', ', $sAuthorContent); // and转逗号 -// $sAuthorContent = preg_replace('/\s+/', ' ', $sAuthorContent); // 合并多余空格 -// $sAuthorContent = trim($sAuthorContent); -// var_dump($this->parseAuthorsWithoutRegex($sAuthorContent));exit; -// // 关键预处理:兼容"and"分隔符、清理乱码、统一空格 -// $content = mb_convert_encoding($sAuthorContent, 'UTF-8', 'auto'); -// $content = str_replace(["\xC2\xA0", 'ï¼', '�', ','], ' ', $content); // 清理乱码和全角符号 -// $content = preg_replace('/\band\b/i', ',', $content); // 将 "and" 转为逗号(统一分隔符) -// $content = preg_replace('/(\d+)\s*([*#†])/', '$1$2', $content); // 合并数字与符号间的空格(如"1 *"→"1*") -// $content = trim(preg_replace('/\s+/', ' ', $content)); // 合并连续空格 - -// // 标记上标内的逗号(多编号处理) -// $tempStr = preg_replace('/(\d+)\s*,\s*(\d+)/', '$1$2', $content); - -// // 核心正则(保持原有结构,扩展符号支持) -// $pattern = '/ -// ([A-Za-z\s\.\-]+?) # 姓名(支持缩写、空格、连字符) -// \s* # 姓名与上标间的空格(允许0或多个) -// ( # 上标组(扩展兼容所有符号) -// \d+ # 起始数字(至少1个数字) -// (?:[†#*,]|\d+)* # 允许:符号(†#*)、逗号、+数字(多编号) -// ) -// \s*,? # 作者间的逗号(可选,允许逗号前有空格) -// (?=\s|$) # 确保后面是空格或字符串结尾(避免跨作者匹配) -// /ux'; - -// preg_match_all($pattern, $tempStr, $matches); - -// // 解析结果并格式化 -// $authorList = []; -// if (!empty($matches[1])) { -// foreach ($matches[1] as $i => $name) { -// $name = trim($name); -// $superscript = trim($matches[2][$i]); -// $superscript = str_replace('', ',', $superscript); // 恢复多编号逗号 -// $superscript = preg_replace('/,$/', '', $superscript); // 清理末尾多余逗号 -// if (!empty($name)) { -// $authorList[] = [ -// 'name' => $name, -// 'superscript' => $superscript -// ]; -// } -// } -// } - -// // 输出结果 -// echo "
";
-// print_r($authorList);
-// echo "
"; -// exit; - -// // 处理作者 -// $content = mb_convert_encoding($sAuthorContent, 'UTF-8', 'auto'); // 确保编码正确 -// $content = str_replace("\xC2\xA0", ' ', $content); // 替换非-breaking空格为普通空格 -// $content = preg_replace('/(\d+)\s*([*#†])/', '$1$2', $content); // 合并"1 *"为"1*"、"1 #"为"1#" -// $content = preg_replace('/,†/', ',†', $content); // 保留"1,†"格式(防止被拆分) - -// //标记上标内的逗号+空格(多编号) -// $tempStr = preg_replace('/(\d+)\s*,\s*(\d+)/', '$1$2', $content); -// // 原有步骤2:正则匹配(扩展上标符号支持,保持原有逻辑) -// $pattern = '/ -// ([A-Za-z\s\.\-]+?) # 姓名(支持缩写、空格) -// \s* # 姓名与上标间空格 -// ( # 上标组(扩展符号支持) -// \d+ # 起始数字 -// (?:[†#*,]|\d+)* # 允许:†#*符号、逗号、+数字(兼容1,†、1,*等) -// ) -// \s*,? # 作者间逗号(可选) -// (?=\s|$) # 确保后面是空格或结尾 -// /ux'; - -// preg_match_all($pattern, $tempStr, $matches); -// var_dump($matches);exit; -// $authorList = []; -// if(!empty($matches[1])){ -// foreach ($matches[1] as $i => $name) { -// $name = trim($name); -// $superscript = trim($matches[2][$i]); -// $superscript = str_replace('', ',', $superscript); // 恢复多编号逗号 -// $superscript = preg_replace('/,$/', '', $superscript); // 清理末尾逗号 -// // 修复符号与数字间的空格(如原始"1 *"被误处理为"1*"的情况,保持原样) -// $superscript = preg_replace('/(\d)([*#†])/', '$1$2', $superscript); -// if (!empty($name)) { -// $authorList[] = [ -// 'name' => $name, -// 'superscript' => $superscript -// ]; -// } -// } -// }else { -// // 按“两个或多个连续空格”拆分(姓名之间的分隔) -// $authorList = array_filter( -// array_map('trim', -// preg_split('/(,\p{Z}*|\p{Z}{2,})/u', $sAuthorContent) -// ) -// ); -// } - - -// // //处理作者 -// $aAuthorData = []; -// $aReport = []; -// $namePattern = '/ -// (?:[A-Za-z\s·\-\']+| # 英文姓名(支持空格、连字符) -// [\x{4e00}-\x{9fa5}]+| # 中文姓名 -// [\x{1800}-\x{18AF}]+| # 蒙古文姓名 -// [A-Z]\.) # 单字母缩写(如 J.) -// /ux'; - -// foreach ($authorList as $authorStr){ -// if (empty($authorStr)) continue; - -// //获取下标 -// $superscript = empty($authorStr['superscript']) ? $authorStr : $authorStr['superscript']; -// $nameStr = empty($authorStr['name']) ? $authorStr : $authorStr['name']; - -// $companyId = []; -// $isSuper = 0; -// $isReport = 0; -// if (!empty($superscript)) { -// // 提取机构编号(忽略上标中的逗号,如1,† → 提取1) -// preg_match_all('/\d+/', $superscript, $numMatch); -// // 识别特殊符号(#为超级作者,*†为通讯作者) -// $isSuper = strpos($superscript, '#') !== false ? 1 : 0; -// $isReport = (strpos($superscript, '*') !== false || strpos($superscript, '†') !== false) ? 1 : 0; -// } -// if (preg_match("/^([A-Za-z\s'\.-]+)/u", $nameStr, $match)) { -// $nameStr = trim($match[1]); -// } -// $aAuthorData[] = [ -// 'name' => $nameStr, -// 'company_id' => empty($numMatch[0]) ? [] : $numMatch[0], -// 'is_super' => $isSuper, -// 'is_report' => $isReport -// ]; -// if ($isReport) { -// $aReport[] = $nameStr; -// } -// } -// return ['author' => $aAuthorData,'report' => array_unique($aReport)]; -// } // 获取机构 private function getCompany($aParam = []){ @@ -807,6 +468,7 @@ class ArticleParserService $sAuthorContent = empty($aParam['authors']) ? $this->getNextParagraphAfterText($title) : $aParam['authors']; //获取作者结构 $allLines = $this->getContentAfterText($sAuthorContent,1); + var_dump($allLines); if(empty($allLines)){ return []; } @@ -815,16 +477,39 @@ class ArticleParserService $currentNumber = null; // 当前序号 foreach ($allLines as $line) { $line = trim($line); - if (empty($line)) continue; - - // 判断是否是新条目的开头:行首为数字(后续可接任意字符或直接接内容) + if (empty($line)) { + continue; + } + if(!mb_check_encoding($line, 'UTF-8')){ + $line = mb_convert_encoding($line, 'UTF-8', 'GBK'); + } + $line = $this->fullDecode($line); $number = ''; $i = 0; $lineLen = strlen($line); // 提取行首的连续数字(作为序号) - while ($i < $lineLen && ctype_digit($line[$i])) { - $number .= $line[$i]; - $i++; + $hasFirstChar = false; + while ($i < $lineLen) { + $currentChar = $line[$i]; + // 首字符处理:允许 26个字母(大小写)或数字 + if (!$hasFirstChar) { + if (ctype_digit($currentChar) || ctype_alpha($currentChar)) { + $number .= $currentChar; + $hasFirstChar = true; + $i++; + } else { + // 首字符不符合(非字母/数字),终止循环 + break; + } + } else { + // 后续字符必须是数字(保持原逻辑) + if (ctype_digit($currentChar)) { + $number .= $currentChar; + $i++; + } else { + break; + } + } } // 若行首有数字,则视为新条目 @@ -840,31 +525,33 @@ class ArticleParserService continue; } - // 非新条目,合并到当前序号的内容中 - if ($currentNumber !== null) { - $grouped[$currentNumber] .= ' ' . $line; - } + // // 非新条目,合并到当前序号的内容中 + // if ($currentNumber !== null) { + // $grouped[$currentNumber] .= ' ' . $line; + // } } - //清理结果 - $possibleEncodings = [ - 'Windows-1252', 'UTF-8', 'GBK', 'GB2312', - 'Latin-1', 'ISO-8859-1', 'CP1252' - ]; $aCompany = []; foreach ($grouped as $number => $institution) { - $encodedContent = @mb_convert_encoding($institution, 'UTF-8', implode(',', $possibleEncodings)); - $sCompany = $encodedContent ?: $sCompany; + $institution = $this->fullDecode($institution); + // 原有基础清理逻辑不变 $institution = preg_replace('/\s+/', ' ', $institution); // 合并多余空格 - $institution = rtrim($institution, '.'); - $institution = preg_replace('/^\d+\s+/', '', $institution); + $institution = rtrim($institution, '.'); // 去除末尾句号 + $institution = preg_replace('/^\d+\s+/', '', $institution); // 去除开头数字 $institution = trim($institution); // 清理首尾空格 - preg_match('/(.*?, [A-Za-z]+ \d+, [A-Za-z]+)/', $institution, $institutionmatches);; - $institution = trim($institutionmatches[1] ?? $institution); - if (preg_match('/^(.*?)(?=\s*\*Email)/', $institution, $matches)) { - $institution = trim($matches[1]); // trim() 去除内容前后多余空格 + + // 增强地址提取:匹配"机构名, 城市 邮编, 国家"格式(兼容更多变体) + // 允许地址中包含多个逗号(如子机构、街道信息),最终以"城市 邮编, 国家"结尾 + // preg_match('/(.*?, [A-Za-z\s]+ \d+, [A-Za-z\s]+)/', $institution, $institutionmatches); + // $institution = trim($institutionmatches[1] ?? $institution); + // 强化冗余信息过滤:去除"*"及之后的内容(包括通讯作者、邮箱等) + // 新增对"#"、"†"等标记的过滤,兼容更多期刊格式 + if (preg_match('/^(.*?)(?=\s*[\*#†]|(?i)\s*Email)/', $institution, $matches)) { + $institution = trim($matches[1]); } - if(!empty($institution) && !mb_check_encoding($institution, 'UTF-8')){ + + // 编码校验不变 + if (!empty($institution) && !mb_check_encoding($institution, 'UTF-8')) { $institution = mb_convert_encoding($institution, 'UTF-8', 'GBK'); } $aCompany[$number] = $institution; @@ -891,13 +578,10 @@ class ArticleParserService // 获取机构后的完整内容 $corrText = $this->getContentAfterText($sCompany); - //编码修复 - $possibleEncodings = [ - 'Windows-1252', 'UTF-8', 'GBK', 'GB2312', - 'Latin-1', 'ISO-8859-1', 'CP1252' - ]; - $encodedContent = @mb_convert_encoding($corrText, 'UTF-8', implode(',', $possibleEncodings)); - $corrText = $encodedContent ?: $corrText; + if(!mb_check_encoding($corrText, 'UTF-8')){ + $corrText = mb_convert_encoding($corrText, 'UTF-8', 'GBK'); + } + $corrText = $this->fullDecode($corrText); // // 调试 // file_put_contents(ROOT_PATH . 'runtime/corr_text_raw.log', $corrText); @@ -927,7 +611,7 @@ class ArticleParserService ]; } if(empty($aCorresponding)){ - $pattern = '/Corresponding Authors: (.*?)(?=$|;)/s'; + $pattern = '/Corresponding Authors|Correspondence to: (.*?)(?=$|;)/s'; preg_match($pattern, $corrText, $match); if (!empty($match[1])) { $corrContent = $match[1]; @@ -1093,13 +777,13 @@ class ArticleParserService } //处理超链接(优先提取链接目标,可能是邮箱) - if ($element instanceof \PhpOffice\PhpWord\Element\Link) { - $target = $element->getTarget(); - if (strpos($target, 'mailto:') === 0) { - $text .= str_replace('mailto:', '', $target) . ' '; // 剥离mailto:前缀 - } - $text .= $element->getText() . ' '; - } + // if ($element instanceof \PhpOffice\PhpWord\Element\Link) { + // $target = $element->getTarget(); + // if (strpos($target, 'mailto:') === 0) { + // $text .= str_replace('mailto:', '', $target) . ' '; // 剥离mailto:前缀 + // } + // $text .= $element->getText() . ' '; + // } //处理字段和注释(可能包含隐藏邮箱) if ($element instanceof \PhpOffice\PhpWord\Element\Field) { @@ -1122,24 +806,88 @@ class ArticleParserService * 从 Word 文档提取摘要和关键词 * @return array 提取结果 */ + function extractContentIntervals($str, $markers = []) { + // 1. 初始化标记(支持自定义,默认值兼容原逻辑) + $defaultMarkers = [ + 'abstract' => 'abstract', + 'keywords' => 'keywords', + 'end_span' => '===========end-span' + ]; + $markers = array_merge($defaultMarkers, $markers); + extract($markers); // 解析为变量 $abstract, $keywords, $end_span + + // 2. 初始化结果(包含元信息) + $result = [ + 'abstract_to_keywords' => '', + 'keywords_to_end' => '', + 'positions' => [ // 标记位置信息(-1 表示未找到) + 'abstract' => -1, + 'keywords' => -1, + 'end_span' => -1 + ], + 'is_valid' => false, // 整体区间是否有效 + 'error' => '' // 错误信息(如标记顺序异常) + ]; + + // 3. 定位 Abstract(不区分大小写) + $absPos = stripos($str, $abstract); + if ($absPos === false) { + $result['error'] = "未找到标记: {$abstract}"; + return $result; + } + $result['positions']['abstract'] = $absPos; + $absEndPos = $absPos + strlen($abstract); + + // 4. 定位 Keywords(需在 Abstract 之后,不区分大小写) + $keyPos = stripos($str, $keywords, $absEndPos); + if ($keyPos === false) { + $result['error'] = "未找到 {$keywords} 或在 {$abstract} 之前"; + return $result; + } + $result['positions']['keywords'] = $keyPos; + $keyEndPos = $keyPos + strlen($keywords); + + // 5. 定位 end-span(需在 Keywords 之后,严格匹配) + $endPos = strpos($str, $end_span, $keyEndPos); + if ($endPos === false) { + $result['error'] = "未找到 {$end_span} 或在 {$keywords} 之前"; + return $result; + } + $result['positions']['end_span'] = $endPos; + + // 6. 截取区间内容(清理标记后的紧邻符号) + // 区间1:Abstract 结束 → Keywords 开始(清理标记后的冒号/空格) + $len1 = $keyPos - $absEndPos; + $part1 = substr($str, $absEndPos, $len1); + $part1 = trim($part1); + // 移除 Abstract 后可能的冒号/短横线(如 "Abstract: ..." → 去掉开头的 ":") + $part1 = ltrim($part1, ': -—'); + $result['abstract_to_keywords'] = trim($part1); + + // 区间2:Keywords 结束 → end-span 开始(同理清理) + $len2 = $endPos - $keyEndPos; + $part2 = substr($str, $keyEndPos, $len2); + $part2 = trim($part2); + $part2 = ltrim($part2, ': -—'); + $result['keywords_to_end'] = trim($part2); + + // 7. 标记为有效 + $result['is_valid'] = true; + return $result; + } public function extractFromWord() { $sContent = ''; //文本处理 $sFundContent = ''; + $aContent = []; foreach ($this->sections as $section) { foreach ($section->getElements() as $element) { $textContent = $this->getTextFromElement($element); if(empty($textContent)){ continue; } - //编码修复 - $possibleEncodings = [ - 'Windows-1252', 'UTF-8', 'GBK', 'GB2312', - 'Latin-1', 'ISO-8859-1', 'CP1252' - ]; - $sContent .= @mb_convert_encoding($textContent, 'UTF-8', implode(',', $possibleEncodings)); - if(stripos($textContent, 'Keywords:') !== false){ - $sContent .= "Keywords-End-Flag"; + if(!empty($textContent) && !mb_check_encoding($textContent, 'UTF-8')){ + $textContent = mb_convert_encoding($textContent, 'UTF-8', 'GBK'); } if(empty($sFundContent)){ $aFund = $this->getMatchedFundPhrases($sContent); @@ -1152,69 +900,194 @@ class ArticleParserService } } } - $sContent .= "\n"; + $sContent .= $textContent."===========end-span"; } } - if(!empty($sContent) && !mb_check_encoding($sContent, 'UTF-8')){ $sContent = mb_convert_encoding($sContent, 'UTF-8', 'GBK'); } - // 2. 基础文本清理(合并多余空格,保留有效换行) - $textContent = preg_replace('/(\S)\s+/', '$1 ', $sContent); - $textContent = trim($textContent); - + $result = $this->extractContentIntervals($sContent); // 3. 提取摘要 - $abstract = ''; - $abstractPattern = '/Abstract\s*([\s\S]*?)(?=Keywords|$)/i'; - if (preg_match($abstractPattern, $textContent, $abstractMatches)) { - $abstract = trim($abstractMatches[1]); - $abstract = preg_replace('/\n+/', ' ', $abstract); + $abstract = empty($result['abstract_to_keywords']) ? '' : $result['abstract_to_keywords']; + if(!empty($abstract) && !mb_check_encoding($abstract, 'UTF-8')){ + $abstract = mb_convert_encoding($abstract, 'UTF-8', 'GBK'); } - // 4. 提取关键词(核心:仅保留两种强制匹配逻辑) - $keywords = []; - // $keywordPattern = '/Keywords:\s*([\s\S]*?)(?=\s*\d+\.|[;,]\s*[\r\n]+\s*[\r\n]+|(?i)\bintroduction|abbreviations\b|$)/i'; - $keywordPattern = '/Keywords\s*(.*?)\s*Keywords-End-Flag/s'; - - if (preg_match($keywordPattern, $textContent, $keywordMatches)) { - $keywordStr = trim($keywordMatches[1]); - - // 清理关键词列表格式(去除换行、末尾多余符号) - $keywordStr = preg_replace('/\n+/', ' ', $keywordStr); - $keywordStr = rtrim($keywordStr, ';,. '); // 去除末尾分号、逗号等 - $keywordStr = trim($keywordStr); - - // 分割并过滤有效关键词 - $keywords = preg_split('/[,;]\s*/', $keywordStr); - $keywords = array_filter(array_map('trim', $keywords), function($item) { - return !empty($item) && !ctype_space($item); - }); + $keywords = empty($result['keywords_to_end']) ? '' : $result['keywords_to_end']; + if(!empty($keywords) && !mb_check_encoding($keywords, 'UTF-8')){ + $keywords = mb_convert_encoding($keywords, 'UTF-8', 'GBK'); } - if(empty($keywords)){ - $keywordPattern = '/Keywords\s*([\s\S]*?)(?=Introduction|$)/i'; - if (preg_match($keywordPattern, $textContent, $keywordMatches)) { - $keywordStr = trim($keywordMatches[1]); - // 清理关键词列表格式(去除换行、末尾多余符号) - $keywordStr = preg_replace('/\n+/', ' ', $keywordStr); - $keywordStr = rtrim($keywordStr, ';,. '); // 去除末尾分号、逗号等 - $keywordStr = trim($keywordStr); - - // 分割并过滤有效关键词 - $keywords = preg_split('/[,;]\s*/', $keywordStr); - $keywords = array_filter(array_map('trim', $keywords), function($item) { - return !empty($item) && !ctype_space($item); - }); - } + if(!empty($sFundContent) && !mb_check_encoding($sFundContent, 'UTF-8')){ + $sFundContent = mb_convert_encoding($sFundContent, 'UTF-8', 'GBK'); } + return [ 'status' => 1, 'msg' => '提取成功', 'data' => [ - 'abstrart' => $abstract, - 'keywords' => $keywords, - 'fund' => $sFundContent + 'abstrart' => empty($abstract) ? '' : $this->fullDecode(str_replace('===========end-span', '',$abstract)), + 'keywords' => empty($keywords) ? '' : $this->fullDecode(str_replace('===========end-span', '',$keywords)), + 'fund' => empty($sFundContent) ? '' : $this->fullDecode(str_replace('===========end-span', '',$sFundContent)) ] ]; } + private function fullDecode($str, $maxDepth = 5) { + // 空值/深度为0,直接返回(提前终止,避免无效操作) + if (empty($str) || $maxDepth <= 0) { + return $str; + } + + // 【性能优化1:预编译所有正则表达式】避免每次循环重新解析正则 + // 预编译:≥专属场景正则 + $regOb0 = '/0B\s*\?0/'; + $regDl18 = '/DL\s*\?.18/'; + // 预编译:≥通用场景正则 + $regQMarkNum = '/\?(\d+)/'; + $regQMarkDotNum = '/\?(\.\d+)/'; + // 预编译:≤、≠空格修复正则 + $regNeNum = '/≠\s*(\d+)/'; + $regLeNum = '/≤\s*(\d+)/'; + // 预编译:混合符号乱码正则(中文顿号/英文逗号) + $regMixCn = '/(\?)\s*、\s*(\?)\s*、\s*(\?)(\d+)/'; + $regMixEn = '/(\?)\s*,\s*(\?)\s*,\s*(\?)(\d+)/'; + // 预编译:≤、≠专属标识正则 + $regLeMark = '/LE\s*\?(\d+)/'; + $regNeMark = '/NE\s*\?(\d+)/'; + // 预编译:Unicode转义正则(提取到外部,避免闭包重复创建) + $regUnicode = '/\\\\u([0-9a-fA-F]{4})/'; + + // 【性能优化2:预定义常量/映射】避免循环内重复创建数组/字符串 + // HTML实体映射(一次性定义,避免循环内重复赋值) + $htmlEntityMap = [ + '≤' => '≤', '≤' => '≤', '≤' => '≤', + '≥' => '≥', '≥' => '≥', '≥' => '≥', + '≠' => '≠', '≠' => '≠', '≠' => '≠', + ]; + // 不间断空格替换数组 + $nbspReplace = [chr(0xC2) . chr(0xA0), chr(0xA0)]; + // Unicode回调函数(预定义,避免循环内重复创建闭包) + $unicodeCallback = function ($m) { + return mb_chr(hexdec($m[1]), 'UTF-8') ?: $m[0]; + }; + + $original = $str; + $depth = 0; + $hasChange = false; // 标记是否有变化,提前终止循环 + + // 循环解码:仅在有变化且未达最大深度时执行 + do { + $depth++; + $hasChange = false; + $prevStr = $str; // 保存当前状态,用于判断变化 + + // 1. 解码Unicode转义(\uXXXX格式) + $str = $this->decodeUnicode($str); + + // 2. 解码HTML实体(先替换专属实体,再执行通用解码) + $str = strtr($str, $htmlEntityMap); // 高性能替换(strtr比str_replace快) + $str = html_entity_decode($str, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // 3. 再次处理遗漏的Unicode转义(使用预编译正则+预定义回调) + $str = preg_replace_callback($regUnicode, $unicodeCallback, $str); + + // 4. 替换不间断空格为普通空格(strtr比str_replace更高效) + $str = str_replace($nbspReplace, ' ', $str); + + // 5. 核心替换逻辑(优化执行顺序,避免覆盖) + // 5.1 原有≥专属场景(保留) + $str = preg_replace($regOb0, '0B≥30', $str, -1, $count1); + $str = preg_replace($regDl18, 'DL≥0.18', $str, -1, $count2); + // 5.2 ≤、≠空格修复(保留) + $str = preg_replace($regNeNum, '≠$1', $str, -1, $count3); + $str = preg_replace($regLeNum, '≤$1', $str, -1, $count4); + // 5.3 原有≥通用场景(保留) + $str = preg_replace($regQMarkNum, '≥$1', $str, -1, $count5); + $str = preg_replace($regQMarkDotNum, '≥0$1', $str, -1, $count6); + // 5.4 混合符号乱码还原(保留) + $str = preg_replace($regMixCn, '≤、≥、≠$4', $str, -1, $count7); + $str = preg_replace($regMixEn, '≤、≥、≠$4', $str, -1, $count8); + // 5.5 ≤、≠专属标识还原(保留) + $str = preg_replace($regLeMark, '≤$1', $str, -1, $count9); + $str = preg_replace($regNeMark, '≠$1', $str, -1, $count10); + + // 5.6 修复前缀"d with "乱码(保留) + $str = str_replace('d with ', 'd with ', $str, $count11); + + // 【性能优化3:统计所有替换次数,判断是否有变化】 + $totalCount = $count1 + $count2 + $count3 + $count4 + $count5 + $count6 + + $count7 + $count8 + $count9 + $count10 + $count11; + if ($totalCount > 0 || $str !== $prevStr) { + $hasChange = true; + $original = $str; + } + + // 【性能优化4:提前终止】单次循环无变化,直接退出 + if (!$hasChange) { + break; + } + + } while ($depth < $maxDepth); // 改用do-while,减少循环判断次数 + + // 最终清理:仅执行一次trim + return trim($str, ':'); + } + // private function fullDecode($str, $maxDepth = 5) { + // if (empty($str) || $maxDepth <= 0) { + // return $str; + // } + + // $original = $str; + // $depth = 0; + + // // 循环解码,直到无变化或达到最大次数 + // while (true) { + // $depth++; + // if ($depth > $maxDepth) { + // break; // 防止过度解码导致死循环 + // } + + // // 1. 解码 Unicode 转义(\uXXXX 格式) + // $str = $this->decodeUnicode($str); + + // // 2. 解码 HTML 实体(&、'、< 等) + // $str = html_entity_decode($str, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // $str = preg_replace_callback('/\\\\u([0-9a-fA-F]{4})/', function ($m) { + // return mb_chr(hexdec($m[1]), 'UTF-8') ?: $m[0]; + // }, $str); + // $str = str_replace([chr(0xC2).chr(0xA0), chr(0xA0)], ' ', $str); + + // // 2. 核心:强制匹配所有可能的乱码格式,还原≥ + // // 匹配:0B?0、0B ?0、0B ?0(空格/制表符)→ 0B≥30 + // $str = preg_replace('/0B\s*\?0/', '0B≥30', $str); + // // 匹配:DL?.18、DL ?.18、DL ?.18 → DL≥0.18 + // $str = preg_replace('/DL\s*\?.18/', 'DL≥0.18', $str); + // // 通用匹配:数字前的?(如?30、?0.18)→ ≥30、≥0.18(防止其他变体) + // $str = preg_replace('/\?(\d+)/', '≥$1', $str); + // $str = preg_replace('/\?(\.\d+)/', '≥0$1', $str); + + // // 3. 修复前缀的"d with "可能的乱码(若有) + // $str = str_replace('d with ', 'd with ', $str); // 若前缀也乱码,可同步替换 + + // // 若解码后无变化,退出循环 + // if ($str === $original) { + // break; + // } + + // $original = $str; + // } + + // return trim($str,':'); + // } + private function decodeUnicode($str) { + return preg_replace_callback( + '/\\\\u([0-9a-fA-F]{4})/', + function ($matches) { + // 将十六进制 Unicode 码转为 UTF-8 字符 + return mb_convert_encoding(pack('H*', $matches[1]), 'UTF-8', 'UCS-2BE'); + }, + $str + ); + } private function getMatchedFundPhrases($content = '') { if (empty($content)) { return []; @@ -1223,7 +1096,7 @@ class ArticleParserService // 基金支持词组列表 $fundPhrases = [ 'Supported by', 'Funded by', 'Sponsored by', 'Supported in part by', - 'Funding was provided by', 'Funded in part by' + 'Funding was provided by', 'Funded in part by','FUNDING:' ]; // 1. 转义词组中的特殊字符,使用 # 作为分隔符 From b904a0d3dfe83fd2a7066207d63afd642cb4c596 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 27 Nov 2025 13:40:46 +0800 Subject: [PATCH 11/53] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ArticleParserService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/application/common/ArticleParserService.php b/application/common/ArticleParserService.php index 009af51e..d03cea0a 100644 --- a/application/common/ArticleParserService.php +++ b/application/common/ArticleParserService.php @@ -468,7 +468,6 @@ class ArticleParserService $sAuthorContent = empty($aParam['authors']) ? $this->getNextParagraphAfterText($title) : $aParam['authors']; //获取作者结构 $allLines = $this->getContentAfterText($sAuthorContent,1); - var_dump($allLines); if(empty($allLines)){ return []; } From 632fede3cba55975dbbe3853db3902421e7627ae Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 27 Nov 2025 13:42:15 +0800 Subject: [PATCH 12/53] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ArticleParserService.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/application/common/ArticleParserService.php b/application/common/ArticleParserService.php index d03cea0a..fd3dfc16 100644 --- a/application/common/ArticleParserService.php +++ b/application/common/ArticleParserService.php @@ -776,13 +776,13 @@ class ArticleParserService } //处理超链接(优先提取链接目标,可能是邮箱) - // if ($element instanceof \PhpOffice\PhpWord\Element\Link) { - // $target = $element->getTarget(); - // if (strpos($target, 'mailto:') === 0) { - // $text .= str_replace('mailto:', '', $target) . ' '; // 剥离mailto:前缀 - // } - // $text .= $element->getText() . ' '; - // } + if ($element instanceof \PhpOffice\PhpWord\Element\Link) { + $target = $element->getTarget(); + if (strpos($target, 'mailto:') === 0) { + $text .= str_replace('mailto:', '', $target) . ' '; // 剥离mailto:前缀 + } + $text .= $element->getText() . ' '; + } //处理字段和注释(可能包含隐藏邮箱) if ($element instanceof \PhpOffice\PhpWord\Element\Field) { From 97e30ab80cedd0f115c951ab9e01e399190e431a Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 28 Nov 2025 09:22:34 +0800 Subject: [PATCH 13/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9C=9F=E5=8D=B7?= =?UTF-8?q?=E5=8F=B7=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common.php | 88 +++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/application/common.php b/application/common.php index a0792cfa..ba27249d 100644 --- a/application/common.php +++ b/application/common.php @@ -850,6 +850,29 @@ function my_doiToFrag2($data) $update['refer_type'] = "journal"; $update['is_ja'] = $joura == trim($bj[0]) ? 0 : 1; $update['dateno'] = str_replace(' ', '', str_replace('-', '–', trim($bj[1]))); + //新增处理 期卷页码 20251127 start + if(!empty($update['dateno'])){ + $sStr = $update['dateno']; + $aStr = explode(':', $sStr); + if(!empty($aStr[1])){ + $parts = explode('–', $aStr[1]); + if(count($parts) == 2){ + $prefix = empty($parts[0]) ? 0 : intval($parts[0]); + $suffix = empty($parts[1]) ? 0 : intval($parts[1]); + if($prefix > $suffix){ + $prefixLen = strlen($prefix); + $suffixLen = strlen($sufix); + $missingLen = $prefixLen - $suffixLen; + if ($missingLen > 0) { + $fillPart = substr($prefix, 0, $missingLen); + $newSuffix = $fillPart . $suffix; + $update['dateno'] = $aStr[0].':'.$prefix.'-'.$newSuffix; + } + } + } + } + } + //新增处理 期卷页码 20251127 end $update['doilink'] = strpos($data['refer_doi'],"http")===false?"https://doi.org/" . $data['refer_doi']:$data['refer_doi']; $update['cs'] = 1; } @@ -998,27 +1021,54 @@ function aliemail($email,$title,$content,$has_hb=1){ return $res; } -function paystationLookup($transactionId){ - $curl = curl_init(); - curl_setopt_array($curl, array( - CURLOPT_URL => 'https://api.paystation.co.nz/v1/transactions/'.$transactionId, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_ENCODING => '', - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 0, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => 'GET', - CURLOPT_HTTPHEADER => array( - 'Content-Type: application/json', - 'Authorization: Bearer '.createPayStationToken() - ) - )); - $response = curl_exec($curl); - curl_close($curl); +//function paystationLookup($transactionId){ +// $curl = curl_init(); +// curl_setopt_array($curl, array( +// CURLOPT_URL => 'https://api.paystation.co.nz/v1/transactions/'.$transactionId, +// CURLOPT_RETURNTRANSFER => true, +// CURLOPT_ENCODING => '', +// CURLOPT_MAXREDIRS => 10, +// CURLOPT_TIMEOUT => 0, +// CURLOPT_FOLLOWLOCATION => true, +// CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, +// CURLOPT_CUSTOMREQUEST => 'GET', +// CURLOPT_HTTPHEADER => array( +// 'Content-Type: application/json', +// 'Authorization: Bearer '.createPayStationToken() +// ) +// )); +// $response = curl_exec($curl); +// curl_close($curl); +// return $response; +//} + + +function paystationLookup($ms){ + $url = "https://payments.paystation.co.nz/lookup/"; + $time = time(); + $params = [ + "pi" => "616562", + "ms" => $ms, + "pstn_HMACTimestamp" => $time + ]; + $secret_key = Env::get("paystation.hmac");// 使用提供的HMAC认证密钥 + $query_string = http_build_query($params); + $hmac_signature = hash_hmac('sha256', $time."paystation".$query_string,$secret_key); + $params["pstn_HMAC"] = $hmac_signature; + $url_with_params = $url . '?' . http_build_query($params); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url_with_params); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $response = curl_exec($ch); + if(curl_errno($ch)) { + echo 'Error:' . curl_error($ch); + } + curl_close($ch); + return $response; } + function createPayStationToken(){ $bodyParams = [ 'client_id' => Env::get("paystation.client_id"), @@ -1122,7 +1172,7 @@ function add_usermsg($userid, $content, $url) return $msg_obj->insert($msgdata); } -function jsonSuccess($data) +function jsonSuccess($data=[]) { return json(['code' => 0, 'msg' => 'success', 'data' => $data]); } From 74f47346d501a86059ce42e40d31cfb1f1d816eb Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 28 Nov 2025 09:25:27 +0800 Subject: [PATCH 14/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9C=9F=E5=8D=B7?= =?UTF-8?q?=E5=8F=B7=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/common.php b/application/common.php index ba27249d..f4fb3041 100644 --- a/application/common.php +++ b/application/common.php @@ -861,7 +861,7 @@ function my_doiToFrag2($data) $suffix = empty($parts[1]) ? 0 : intval($parts[1]); if($prefix > $suffix){ $prefixLen = strlen($prefix); - $suffixLen = strlen($sufix); + $suffixLen = strlen($suffix); $missingLen = $prefixLen - $suffixLen; if ($missingLen > 0) { $fillPart = substr($prefix, 0, $missingLen); From 7cdf825418ad65bdbda104ad2012c16bf72499b9 Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 28 Nov 2025 11:53:12 +0800 Subject: [PATCH 15/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Production.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/application/api/controller/Production.php b/application/api/controller/Production.php index 38c2a992..87e34f47 100644 --- a/application/api/controller/Production.php +++ b/application/api/controller/Production.php @@ -1939,7 +1939,11 @@ class Production extends Base if ($v['refer_doi'] == '') { $this->production_article_refer_obj->where('p_refer_id', $v['p_refer_id'])->update(['refer_frag' => $v['refer_content']]); } else { - Queue::push('app\api\job\ts@fire1', $v, 'ts'); + + //修改队列兼容对接OPENAI接口 chengxiaoling 20251128 start + // Queue::push('app\api\job\ts@fire1', $v, 'ts'); + Queue::push('app\api\job\ArticleReferDetailQueue@fire', $v, 'ArticleReferDetailQueue'); + //修改队列兼容对接OPENAI接口 chengxiaoling 20251128 end } } return jsonSuccess([]); From 55aa94adbe93e1129373460e30bb5b249a3c3963 Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 28 Nov 2025 15:46:00 +0800 Subject: [PATCH 16/53] =?UTF-8?q?=E5=8F=82=E8=80=83=E6=96=87=E7=8C=AE?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/References.php | 541 ++++++++++++++++++ application/api/job/AiCheckRefer.php | 78 +++ application/api/job/AiCheckReferByDoi.php | 85 +++ .../api/job/ArticleReferDetailQueue.php | 92 +++ application/api/job/ArticleReferQueue.php | 85 +++ application/api/job/ProofReadQueue.php | 82 +++ application/common/ProductionArticleRefer.php | 303 ++++++++++ 7 files changed, 1266 insertions(+) create mode 100644 application/api/controller/References.php create mode 100644 application/api/job/AiCheckRefer.php create mode 100644 application/api/job/AiCheckReferByDoi.php create mode 100644 application/api/job/ArticleReferDetailQueue.php create mode 100644 application/api/job/ArticleReferQueue.php create mode 100644 application/api/job/ProofReadQueue.php create mode 100644 application/common/ProductionArticleRefer.php diff --git a/application/api/controller/References.php b/application/api/controller/References.php new file mode 100644 index 00000000..71038535 --- /dev/null +++ b/application/api/controller/References.php @@ -0,0 +1,541 @@ +request->post() : $aParam; + + //必填值验证 + $iPReferId = empty($aParam['p_refer_id']) ? '' : $aParam['p_refer_id']; + if(empty($iPReferId)){ + return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']); + } + $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; + $aRefer = Db::name('production_article_refer')->where($aWhere)->find(); + if(empty($aRefer)){ + return json_encode(['status' => 4,'msg' => 'Reference is empty']); + } + //获取文章信息 + $aParam['p_article_id'] = $aRefer['p_article_id']; + $aArticle = $this->getArticle($aParam); + $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; + if($iStatus != 1){ + return json_encode($aArticle); + } + $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; + if(empty($aArticle)){ + return json_encode(['status' => 3,'msg' => 'The article does not exist']); + } + + //获取参考文献信息作者名.文章题目.期刊名缩写.年卷页.Available at: //https://doi.org/xxxxx + //作者 + $sData = $aRefer['refer_frag']; + if($aRefer['refer_type'] == 'journal'){ + if(!empty($aRefer['doilink'])){ + $sAuthor = empty($aRefer['author']) ? '' : trim(trim($aRefer['author']),'.'); + if(!empty($sAuthor)){ + $aAuthor = explode(',', $sAuthor); + if(count($aAuthor) > 3){ + $sAuthor = implode(',', array_slice($aAuthor, 0,3)); + $sAuthor .= ', et al'; + } + if(count($aAuthor) <= 3 ){ + $sAuthor = implode(',', $aAuthor); + } + } + //文章标题 + $sTitle = empty($aRefer['title']) ? '' : trim(trim($aRefer['title']),'.'); + //期刊名缩写 + $sJoura = empty($aRefer['joura']) ? '' : trim(trim($aRefer['joura']),'.'); + //年卷页 + $sDateno = empty($aRefer['dateno']) ? '' : trim(trim($aRefer['dateno']),'.'); + //DOI + $sDoilink = empty($aRefer['doilink']) ? '' : trim($aRefer['doilink']); + if(!empty($sDoilink)){ + $sDoilink = strpos($sDoilink ,"http")===false ? "https://doi.org/".$sDoilink : $sDoilink; + $sDoilink = str_replace('http://doi.org/', 'https://doi.org/', $sDoilink); + } + $sReferDoi = empty($aRefer['refer_doi']) ? '' : trim($aRefer['refer_doi']); + if(!empty($sReferDoi)){ + $sReferDoi = strpos($sReferDoi ,"http")===false ? "https://doi.org/".$sReferDoi : $sReferDoi; + $sReferDoi = str_replace('http://doi.org/', 'https://doi.org/', $sReferDoi); + } + $sDoilink = empty($sDoilink) ? $sReferDoi : $sDoilink; + + $sData = $sAuthor.'.'.$sTitle.'.'.$sJoura.'.'.$sDateno.".Available at:\n".$sDoilink; + } + } + if($aRefer['refer_type'] == 'book'){ + $sAuthor = empty($aRefer['author']) ? '' : trim(trim($aRefer['author']),'.'); + if(!empty($sAuthor)){ + $aAuthor = explode(',', $sAuthor); + if(count($aAuthor) > 3){ + $sAuthor = implode(',', array_slice($aAuthor, 0,3)); + $sAuthor .= ', et al'; + } + if(count($aAuthor) <= 3 ){ + $sAuthor = implode(',', $aAuthor); + } + } + //文章标题 + $sTitle = empty($aRefer['title']) ? '' : trim(trim($aRefer['title']),'.'); + //期刊名缩写 + $sJoura = empty($aRefer['joura']) ? '' : trim(trim($aRefer['joura']),'.'); + //年卷页 + $sDateno = empty($aRefer['dateno']) ? '' : trim(trim($aRefer['dateno']),'.'); + //DOI + $sDoilink = empty($aRefer['isbn']) ? '' : trim($aRefer['isbn']); + + $sData = $sAuthor.'.'.$sTitle.'.'.$sJoura.'.'.$sDateno.".Available at:\n".$sDoilink; + } + $aRefer['deal_content'] = $sData; + return json_encode(['status' => 1,'msg' => 'success','data' => $aRefer]); + } + + /** + * 修改参考文献的信息 + * @param p_refer_id 主键ID + */ + public function modify($aParam = []){ + + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + + //必填值验证 + $iPReferId = empty($aParam['p_refer_id']) ? '' : $aParam['p_refer_id']; + if(empty($iPReferId)){ + return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']); + } + $sContent = empty($aParam['content']) ? '' : $aParam['content']; + if(empty($sContent)){ + return json_encode(['status' => 2,'msg' => 'Please enter the modification content']); + } + if(!is_string($sContent)){ + return json_encode(['status' => 2,'msg' => 'The content format is incorrect']); + } + + //获取参考文献信息 + $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; + $aRefer = Db::name('production_article_refer')->where($aWhere)->find(); + if(empty($aRefer)){ + return json_encode(['status' => 4,'msg' => 'Reference is empty']); + } + + //获取文章信息 + $aParam['p_article_id'] = $aRefer['p_article_id']; + $aArticle = $this->getArticle($aParam); + $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; + if($iStatus != 1){ + return json_encode($aArticle); + } + $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; + if(empty($aArticle)){ + return json_encode(['status' => 3,'msg' => 'The article does not exist']); + } + + //数据处理 + $aContent = json_decode($this->dealContent(['content' => $sContent]),true); + $aUpdate = empty($aContent['data']) ? [] : $aContent['data']; + if(empty($aUpdate)){ + return json_encode(['status' => 5,'msg' => 'The content format is incorrect']); + } + $aUpdate['refer_content'] = $sContent; + $aUpdate['is_change'] = 1; + $aUpdate['update_time'] = time(); + //更新数据 + $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; + $result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate); + if($result === false){ + return json_encode(['status' => 6,'msg' => 'Update failed']); + } + return json_encode(['status' => 1,'msg' => 'success']); + } + + + /** + * 处理参考文献的信息 + * @param p_refer_id 主键ID + */ + public function dealContent($aParam = []){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + //必填验证 + $sContent = empty($aParam['content']) ? '' : $aParam['content']; + if(empty($sContent)){ + return json_encode(['status' => 2,'msg' => 'Please enter the modification content']); + } + if(!is_string($sContent)){ + return json_encode(['status' => 2,'msg' => 'The content format is incorrect']); + } + $aContent = explode('.', $sContent); + $aUpdate = []; + if(count($aContent) > 1){ + $aField = [0 => 'author',1 => 'title', 2 => 'joura',3 => 'dateno']; + $aStart = array_slice($aContent, 0,4); + foreach ($aStart as $key => $value) { + if(empty($value)){ + continue; + } + $aUpdate[$aField[$key]] = trim(trim($value),'.'); + } + + $sDoi = empty(array_slice($aContent, 4)) ? '' : implode('.', array_slice($aContent, 4)); + // 匹配http/https开头的URL正则 + $urlPattern = '/https?:\/\/[^\s<>"]+|http?:\/\/[^\s<>"]+/i'; + // 执行匹配(preg_match_all返回所有结果) + preg_match_all($urlPattern, $sDoi, $matches); + if(!empty($matches[0])){ + $sDoi = implode(',', array_unique($matches[0])); + } + if(empty($sDoi)){ + return json_encode(['status' => 4,'msg' => 'Reference DOI is empty']); + } + $sDoi = trim(trim($sDoi),':'); + $sDoi = strpos($sDoi ,"http")===false ? "https://doi.org/".$sDoi : $sDoi; + $sDoi = str_replace('http://doi.org/', 'https://doi.org/', $sDoi); + $aUpdate['doilink'] = $sDoi; + $doiPattern = '/10\.\d{4,9}\/[^\s\/?#&=]+/i'; + if (preg_match($doiPattern, $sDoi, $matches)) { + $aUpdate['doi'] = $matches[0]; + }else{ + $aUpdate['doi'] = $sDoi; + } + if(!empty($aUpdate['author'])){ + $aUpdate['author'] = trim(trim($aUpdate['author'])).'.'; + } + + } + return json_encode(['status' => 1,'msg' => 'success','data' => $aUpdate]); + } + + /** + * 获取文章信息 + */ + private function getArticle($aParam = []){ + + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + + //获取生产文章信息 + $iPArticleId = empty($aParam['p_article_id']) ? 0 : $aParam['p_article_id']; + if(empty($iPArticleId)){ + return ['status' => 2,'msg' => 'Please select the article to query']; + } + $aWhere = ['p_article_id' => $iPArticleId,'state' => 0]; + $aProductionArticle = Db::name('production_article')->field('article_id')->where($aWhere)->find(); + $iArticleId = empty($aProductionArticle['article_id']) ? 0 : $aProductionArticle['article_id']; + if(empty($iArticleId)) { + return ['status' => 2,'msg' => 'No articles found']; + } + + //查询条件 + $aWhere = ['article_id' => $iArticleId,'state' => ['in',[5,6]]]; + $aArticle = Db::name('article')->field('article_id')->where($aWhere)->find(); + if(empty($aArticle)){ + return ['status' => 3,'msg' => 'The article does not exist or has not entered the editorial reference status']; + } + $aArticle['p_article_id'] = $iPArticleId; + return ['status' => 1,'msg' => 'success','data' => $aArticle]; + } + /** + * AI检测 + */ + public function checkByAi($aParam = []){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + + //获取文章信息 + $aArticle = $this->getArticle($aParam); + $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; + if($iStatus != 1){ + return json_encode($aArticle); + } + $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; + if(empty($aArticle)){ + return json_encode(['status' => 3,'msg' => 'The article does not exist']); + } + //查询参考文献信息 + $aWhere = ['p_article_id' => $aArticle['p_article_id'],'state' => 0,'doilink' => '']; + $aRefer = Db::name('production_article_refer')->field('p_refer_id,p_article_id,refer_type,refer_content,doilink,refer_doi')->where($aWhere)->select(); + if(empty($aRefer)){ + return json_encode(['status' => 4,'msg' => 'No reference information found']); + } + //数据处理 + foreach ($aRefer as $key => $value) { + if(empty($value['refer_doi'])){ + continue; + } + if($value['refer_doi'] == 'Not Available'){ + continue; + } + if($value['refer_type'] == 'journal' && !empty($value['doilink'])){ + continue; + } + if($value['refer_type'] == 'book' && !empty($value['isbn'])){ + continue; + } + //写入获取参考文献详情队列 + \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$value,'AiCheckReferByDoi'); + } + return json_encode(['status' => 1,'msg' => 'Successfully joined the AI inspection DOI queue']); + } + /** + * 获取结果 + */ + public function getCheckByAiResult($aParam = []){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + + //必填值验证 + $iPReferId = empty($aParam['p_refer_id']) ? '' : $aParam['p_refer_id']; + if(empty($iPReferId)){ + return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']); + } + //获取参考文献信息 + $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; + $aRefer = Db::name('production_article_refer')->field('p_refer_id,p_article_id,refer_type,refer_content,doilink,refer_doi,state,dateno')->where($aWhere)->find(); + if(empty($aRefer)){ + return json_encode(['status' => 4,'msg' => 'Reference is empty'.json_encode($aParam)]); + } + if(empty($aRefer['refer_doi'])){ + return json_encode(['status' => 4,'msg' => 'Reference DOI is empty'.json_encode($aParam)]); + } + if($aRefer['refer_type'] == 'journal' && !empty($aRefer['doilink'])){ + $aDateno = empty($aRefer['dateno']) ? [] : explode(':', $aRefer['dateno']); + if(count($aDateno) > 1){ + return json_encode(['status' => 4,'msg' => 'No need to parse again-journal'.json_encode($aParam)]); + } + } + if($aRefer['refer_type'] == 'book' && !empty($aRefer['isbn'])){ + return json_encode(['status' => 4,'msg' => 'No need to parse again-book'.json_encode($aParam)]); + } + //获取文章信息 + $aParam['p_article_id'] = $aRefer['p_article_id']; + $aArticle = $this->getArticle($aParam); + $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; + if($iStatus != 1){ + return json_encode($aArticle); + } + $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; + if(empty($aArticle)){ + return json_encode(['status' => 3,'msg' => 'The article does not exist']); + } + + //请求AI获取结果 + $aResult = $this->curlOpenAIByDoi(['doi' => $aRefer['refer_doi']]); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + $sMsg = empty($aResult['msg']) ? 'The DOI number AI did not find any relevant information' : $aResult['msg']; + if($iStatus != 1){ + return json_encode(['status' => 4,'msg' => $sMsg]); + } + $aData = empty($aResult['data']) ? [] : $aResult['data']; + if(empty($aData)){ + return json_encode(['status' => 5,'msg' => 'AI obtains empty data']); + } + //写入日志 + $aLog = []; + $aLog['content'] = json_encode($aResult); + $aLog['update_time'] = time(); + $aLog['p_refer_id'] = $iPReferId; + $iLogId = Db::name('production_article_refer_ai')->insertGetId($aLog); + $iIsAiCheck = empty($aData['is_ai_check']) ? 2 : $aData['is_ai_check']; + if($iIsAiCheck != 1){//AI未检测到信息 + return json_encode(['status' => 6,'msg' => 'AI did not find any information'.json_encode($aParam)]); + } + + //数据处理入库 + $aField = ['author','title','joura','dateno','doilink']; + foreach ($aField as $key => $value) { + if(empty($aData[$value])){ + continue; + } + if($value == 'author'){ + $aUpdate['author'] = implode(',', $aData['author']); + // $aUpdate['author'] = str_replace('et al.', '', $aUpdate['author']); + }else{ + $aUpdate[$value] = $aData[$value]; + } + } + if(empty($aUpdate)){ + return json_encode(['status' => 6,'msg' => 'Update data to empty'.json_encode($aData)]); + } + if($aRefer['refer_type'] == 'other'){ + $aUpdate['refer_type'] = 'journal'; + } + if($aRefer['refer_type'] == 'book' && !empty($aUpdate['doilink'])){ + $aUpdate['refer_type'] = $aUpdate['doilink']; + unset($aUpdate['doilink']); + } + $aLog = $aUpdate; + $aUpdate['is_change'] = 1; + $aUpdate['is_ai_check'] = 1; + $aUpdate['update_time'] = time(); + Db::startTrans(); + //更新数据 + $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; + $result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate); + if($result === false){ + return json_encode(['status' => 6,'msg' => 'Update failed']); + } + //更新日志 + if(!empty($iLogId)){ + $aWhere = ['id' => $iLogId]; + if(isset($aLog['refer_type'])){ + unset($aLog['refer_type']); + } + $result = Db::name('production_article_refer_ai')->where($aWhere)->limit(1)->update($aLog); + } + Db::commit(); + return json_encode(['status' => 1,'msg' => 'success']); + } + + /** + * 对接OPENAI + */ + private function curlOpenAIByDoi($aParam = []){ + + //获取DOI + $sDoi = empty($aParam['doi']) ? '' : $aParam['doi']; + if(empty($sDoi)){ + return ['status' => 2,'msg' => 'Reference doi is empty']; + } + //系统角色 + $sSysMessagePrompt = '请完成以下任务: + 1. 根据提供的DOI号,查询该文献的AMA引用格式; + 2. 按照以下规则调整AMA引用格式: + - 第三个作者名字后添加 et al.; + - DOI前加上"Available at: "; + - DOI信息格式调整为"https://doi.org/+真实DOI"(替换真实DOI为文献实际DOI). + 3. 严格按照以下JSON结构返回结果,仅返回JSON数据,不要额外文字,包含字段:doilink(url格式)、title(标题)、author(作者数组)、joura(出版社名称)、dateno(年;卷(期):起始页-终止页),is_ai_check(默认1) + 4. 若未查询到信息,字段is_ai_check为2,相关字段为null。'; + //用户角色 + $sUserPrompt = '我提供的DOI是:'.$sDoi; + $aMessage = [ + ['role' => 'system', 'content' => $sSysMessagePrompt], + ['role' => 'user', 'content' => $sUserPrompt], + ]; + //请求OPENAI接口 + $sModel = empty($aParam['model']) ? 'gpt-4.1' : $aParam['model'];//模型 + $sApiUrl = $this->sApiUrl;//'http://chat.taimed.cn/v1/chat/completions';// + $aParam = ['model' => $sModel,'url' => $sApiUrl,'temperature' => 0,'messages' => $aMessage,'api_key' => $this->sApiKey]; + $oOpenAi = new \app\common\OpenAi; + $aResult = json_decode($oOpenAi->curlOpenAI($aParam),true); + return $aResult; + } + /** + * 作者修改完成发邮件 + */ + public function finishSendEmail(){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + //文章ID + $iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(array('status' => 2,'msg' => 'Please select an article')); + } + //查询条件 + $aWhere = ['article_id' => $iArticleId,'state' => ['in',[5,6]]]; + $aArticle = Db::name('article')->field('article_id,journal_id,accept_sn')->where($aWhere)->find(); + if(empty($aArticle)){ + return json_encode(['status' => 3,'msg' => 'The article does not exist or has not entered the editorial reference status']); + } + $aWhere = ['article_id' => $iArticleId,'state' => 0]; + $aProductionArticle = Db::name('production_article')->field('p_article_id')->where($aWhere)->find(); + if(empty($aProductionArticle)) { + return ['status' => 2,'msg' => 'The article has not entered the production stage']; + } + //查询是否有参考文献 + $aWhere = ['p_article_id' => $aProductionArticle['p_article_id'],'state' => 0]; + $aRefer = Db::name('production_article_refer')->field('article_id')->where($aWhere)->find(); + if(empty($aRefer)) { + return ['status' => 2,'msg' => 'No reference information found, please be patient and wait for the editor to upload']; + } + //查询期刊信息 + if(empty($aArticle['journal_id'])){ + return json_encode(array('status' => 4,'msg' => 'The article is not associated with a journal' )); + } + $aWhere = ['state' => 0,'journal_id' => $aArticle['journal_id']]; + $aJournal = Db::name('journal')->where($aWhere)->find(); + if(empty($aJournal)){ + return json_encode(array('status' => 5,'msg' => 'No journal information found' )); + } + //查询编辑邮箱 + $iUserId = empty($aJournal['editor_id']) ? '' : $aJournal['editor_id']; + if(empty($iUserId)){ + return json_encode(array('status' => 6,'msg' => 'The journal to which the article belongs has not designated a responsible editor' )); + } + $aWhere = ['user_id' => $iUserId,'state' => 0,'email' => ['<>','']]; + $aUser = Db::name('user')->field('user_id,email,realname,account')->where($aWhere)->find(); + if(empty($aUser)){ + return json_encode(['status' => 7,'msg' => "Edit email as empty"]); + } + + //处理发邮件 + //邮件模版 + $aEmailConfig = [ + 'email_subject' => '{journal_title}-{accept_sn}', + 'email_content' => ' + Dear Editor,

+ The authors have revised the formats of all references, please check.
+ Sn:{accept_sn}

+ Sincerely,
Editorial Office
+ Subscribe to this journal
{journal_title}
+ Email: {journal_email}
+ Website: {website}' + ]; + //邮件内容 + $aSearch = [ + '{accept_sn}' => empty($aArticle['accept_sn']) ? '' : $aArticle['accept_sn'],//accept_sn + '{journal_title}' => empty($aJournal['title']) ? '' : $aJournal['title'],//期刊名 + '{journal_issn}' => empty($aJournal['issn']) ? '' : $aJournal['issn'], + '{journal_email}' => empty($aJournal['email']) ? '' : $aJournal['email'], + '{website}' => empty($aJournal['website']) ? '' : $aJournal['website'], + ]; + + //发邮件 + //邮件标题 + $email = $aUser['email']; + $title = str_replace(array_keys($aSearch), array_values($aSearch),$aEmailConfig['email_subject']); + //邮件内容变量替换 + $content = str_replace(array_keys($aSearch), array_values($aSearch), $aEmailConfig['email_content']); + $pre = \think\Env::get('emailtemplete.pre'); + $net = \think\Env::get('emailtemplete.net'); + $net1 = str_replace("{{email}}",trim($email),$net); + $content=$pre.$content.$net1; + //发送邮件 + $memail = empty($aJournal['email']) ? '' : $aJournal['email']; + $mpassword = empty($aJournal['epassword']) ? '' : $aJournal['epassword']; + //期刊标题 + $from_name = empty($aJournal['title']) ? '' : $aJournal['title']; + //邮件队列组装参数 + $aResult = sendEmail($email,$title,$from_name,$content,$memail,$mpassword); + $iStatus = empty($aResult['status']) ? 1 : $aResult['status']; + $iIsSuccess = 2; + $sMsg = empty($aResult['data']) ? '失败' : $aResult['data']; + if($iStatus == 1){ + return json_encode(['status' => 1,'msg' => 'success']); + } + return json_encode(['status' => 8,'msg' => 'fail']); + } +} diff --git a/application/api/job/AiCheckRefer.php b/application/api/job/AiCheckRefer.php new file mode 100644 index 00000000..d63ebb20 --- /dev/null +++ b/application/api/job/AiCheckRefer.php @@ -0,0 +1,78 @@ +oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + } + + public function fire(Job $job, $data) + { + //任务开始判断 + $this->oQueueJob->init($job); + + // 获取 Redis 任务的原始数据 + $rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody(); + $jobData = empty($rawBody) ? [] : json_decode($rawBody, true); + $jobId = empty($jobData['id']) ? 'unknown' : $jobData['id']; + + $this->oQueueJob->log("-----------队列任务开始-----------"); + $this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}"); + + + // 获取生产文章ID + $iPArticleId = empty($data['p_article_id']) ? 0 : $data['p_article_id']; + if (empty($iPArticleId)) { + $this->oQueueJob->log("无效的p_article_id,删除任务"); + $job->delete(); + return; + } + try { + + // 生成Redis键并尝试获取锁 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iPArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + //生成内容 + $oProductionArticleRefer = new \app\api\controller\References; + $response = $oProductionArticleRefer->checkByAi($data); + // 验证API响应 + if (empty($response)) { + throw new \RuntimeException("OpenAI API返回空结果"); + } + // 检查JSON解析错误 + $aResult = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException("解析OpenAI响应失败: " . json_last_error_msg() . " | 原始响应: {$response}"); + } + $sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg']; + //更新完成标识 + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}"); + + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (\Exception $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } finally { + $this->oQueueJob->finnal(); + } + } +} \ No newline at end of file diff --git a/application/api/job/AiCheckReferByDoi.php b/application/api/job/AiCheckReferByDoi.php new file mode 100644 index 00000000..750b6374 --- /dev/null +++ b/application/api/job/AiCheckReferByDoi.php @@ -0,0 +1,85 @@ +oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + } + + public function fire(Job $job, $data) + { + //任务开始判断 + $this->oQueueJob->init($job); + + // 获取 Redis 任务的原始数据 + $rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody(); + $jobData = empty($rawBody) ? [] : json_decode($rawBody, true); + $jobId = empty($jobData['id']) ? 'unknown' : $jobData['id']; + + $this->oQueueJob->log("-----------队列任务开始-----------"); + $this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}"); + + + // 获取生产文章ID + $iPArticleId = empty($data['p_article_id']) ? 0 : $data['p_article_id']; + if (empty($iPArticleId)) { + $this->oQueueJob->log("无效的p_article_id,删除任务"); + $job->delete(); + return; + } + // 获取参考文献ID + $iPReferId = empty($data['p_refer_id']) ? 0 : $data['p_refer_id']; + if (empty($iPArticleId)) { + $this->oQueueJob->log("无效的p_article_id,删除任务"); + $job->delete(); + return; + } + try { + + // 生成Redis键并尝试获取锁 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iPArticleId}:{$iPReferId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + //生成内容 + $oProductionArticleRefer = new \app\api\controller\References; + $response = $oProductionArticleRefer->getCheckByAiResult($data); + // 验证API响应 + if (empty($response)) { + throw new \RuntimeException("OpenAI API返回空结果"); + } + // 检查JSON解析错误 + $aResult = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException("解析OpenAI响应失败: " . json_last_error_msg() . " | 原始响应: {$response}"); + } + $sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg']; + //更新完成标识 + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}"); + + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (\Exception $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } finally { + $this->oQueueJob->finnal(); + } + } +} \ No newline at end of file diff --git a/application/api/job/ArticleReferDetailQueue.php b/application/api/job/ArticleReferDetailQueue.php new file mode 100644 index 00000000..12190846 --- /dev/null +++ b/application/api/job/ArticleReferDetailQueue.php @@ -0,0 +1,92 @@ +oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + } + + public function fire(Job $job, $data) + { + //任务开始判断 + $this->oQueueJob->init($job); + + // 获取 Redis 任务的原始数据 + $rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody(); + $jobData = empty($rawBody) ? [] : json_decode($rawBody, true); + $jobId = empty($jobData['id']) ? 'unknown' : $jobData['id']; + + $this->oQueueJob->log("-----------队列任务开始-----------"); + $this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}"); + + // // 获取文章ID + // $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + // if (empty($iArticleId)) { + // $this->oQueueJob->log("无效的article_id,删除任务"); + // $job->delete(); + // return; + // } + // 获取生产文章ID + $iPArticleId = empty($data['p_article_id']) ? 0 : $data['p_article_id']; + if (empty($iPArticleId)) { + $this->oQueueJob->log("无效的p_article_id,删除任务"); + $job->delete(); + return; + } + // 获取生产文章ID + $iPReferId = empty($data['p_refer_id']) ? 0 : $data['p_refer_id']; + if (empty($iPReferId)) { + $this->oQueueJob->log("无效的p_refer_id,删除任务"); + $job->delete(); + return; + } + try { + + // 生成Redis键并尝试获取锁 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iPArticleId}:{$iPReferId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + //生成内容 + $oProductionArticleRefer = new ProductionArticleRefer; + $response = $oProductionArticleRefer->get($data); + // 验证API响应 + if (empty($response)) { + throw new \RuntimeException("返回空结果"); + } + // 检查JSON解析错误 + $aResult = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException("解析响应失败: " . json_last_error_msg() . " | 原始响应: {$response}"); + } + $sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg']; + //更新完成标识 + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}"); + + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (\Exception $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } finally { + $this->oQueueJob->finnal(); + } + } +} \ No newline at end of file diff --git a/application/api/job/ArticleReferQueue.php b/application/api/job/ArticleReferQueue.php new file mode 100644 index 00000000..e35ecc5a --- /dev/null +++ b/application/api/job/ArticleReferQueue.php @@ -0,0 +1,85 @@ +oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + } + + public function fire(Job $job, $data) + { + //任务开始判断 + $this->oQueueJob->init($job); + + // 获取 Redis 任务的原始数据 + $rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody(); + $jobData = empty($rawBody) ? [] : json_decode($rawBody, true); + $jobId = empty($jobData['id']) ? 'unknown' : $jobData['id']; + + $this->oQueueJob->log("-----------队列任务开始-----------"); + $this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}"); + + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + // 获取生产文章ID + $iPArticleId = empty($data['p_article_id']) ? 0 : $data['p_article_id']; + if (empty($iPArticleId)) { + $this->oQueueJob->log("无效的p_article_id,删除任务"); + $job->delete(); + return; + } + try { + + // 生成Redis键并尝试获取锁 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}:{$iPArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + //生成内容 + $oProductionArticleRefer = new ProductionArticleRefer; + $response = $oProductionArticleRefer->top($data); + // 验证API响应 + if (empty($response)) { + throw new \RuntimeException("OpenAI API返回空结果"); + } + // 检查JSON解析错误 + $aResult = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException("解析OpenAI响应失败: " . json_last_error_msg() . " | 原始响应: {$response}"); + } + $sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg']; + //更新完成标识 + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}"); + + } catch (\RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (\LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (\Exception $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } finally { + $this->oQueueJob->finnal(); + } + } +} \ No newline at end of file diff --git a/application/api/job/ProofReadQueue.php b/application/api/job/ProofReadQueue.php new file mode 100644 index 00000000..393f35ac --- /dev/null +++ b/application/api/job/ProofReadQueue.php @@ -0,0 +1,82 @@ +oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + } + + public function fire(Job $job, $data) + { + //任务开始判断 + $this->oQueueJob->init($job); + + // 获取 Redis 任务的原始数据 + $rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody(); + $jobData = empty($rawBody) ? [] : json_decode($rawBody, true); + $jobId = empty($jobData['id']) ? 'unknown' : $jobData['id']; + + $this->oQueueJob->log("-----------队列任务开始-----------"); + $this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}"); + + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + $sChunkIndex = empty($data['chunkIndex']) ? 0 : $data['chunkIndex']; + $sPrompt = empty($data['prompt']) ? '' : $data['prompt']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + try { + + // 生成Redis键并尝试获取锁 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}:{$sPrompt}_{$sChunkIndex}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + //生成内容 + $oAireview = new ProofRead; + $response = $oAireview->proofReadQueue($data); + + // 验证API响应 + if (empty($response)) { + throw new \RuntimeException("OpenAI API返回空结果"); + } + // 检查JSON解析错误 + $aResult = json_decode($response, true); + echo '
';var_dump($aResult);
+            if (json_last_error() !== JSON_ERROR_NONE) {
+                throw new \RuntimeException("解析OpenAI响应失败: " . json_last_error_msg() . " | 原始响应: {$response}");
+            }
+            $sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg'];
+            //更新完成标识
+            $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue);
+            $job->delete();
+            $this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}");
+
+        } catch (\RuntimeException $e) {
+            $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
+        } catch (\LogicException $e) {
+            $this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job);
+        } catch (\Exception $e) {
+            $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
+        } finally {
+            $this->oQueueJob->finnal();
+        }
+    }
+}
\ No newline at end of file
diff --git a/application/common/ProductionArticleRefer.php b/application/common/ProductionArticleRefer.php
new file mode 100644
index 00000000..397887a0
--- /dev/null
+++ b/application/common/ProductionArticleRefer.php
@@ -0,0 +1,303 @@
+~`|^]+/i';
+
+    // 错误码与错误信息映射(标准化错误处理)
+    private const ERROR_CODES = [
+        'EMPTY_STRING' => 'Input string is empty (preprocessed))',
+        'NO_MATCH' => 'No valid DOI detected',
+        'INVALID_AFTER_CLEAN' => 'No effective DOI after cleaning',
+        'FORCE_EXTRACT_FAILED' => 'Forced extraction still has no valid DOI',
+        'EXTRACTION_EXCEPTION' => 'Exception occurred during DOI extraction process',
+    ];
+
+    /**
+     * 获取未处理的参考文献
+     *
+     * @return void
+     */
+    public function top($aParam = []) {
+
+        //文章ID
+        $iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id'];
+        if(empty($iArticleId)){
+            return json_encode(array('status' => 2,'msg' => 'Please select an article'.json_encode($aParam) ));
+        }
+        // 获取生产文章ID
+        $iPArticleId = empty($aParam['p_article_id']) ? 0 : $aParam['p_article_id'];
+        if(empty($iPArticleId)) {
+            return json_encode(array('status' => 2,'msg' => 'Please select an production article'.json_encode($aParam) ));
+        }
+
+        //查询未处理过的数据
+        $aWhere = ['p_article_id' => $iPArticleId,'article_id' => $iArticleId,'state' => 0,'refer_doi' => ['<>',''],'is_deal' => 2];
+        $aResult = Db::name('production_article_refer')->field('article_id,p_article_id,p_refer_id,refer_doi')->where($aWhere)->select();
+        if(empty($aResult)){
+            return json_encode(array('status' => 2,'msg' => 'The reference data to be processed is empty'.json_encode($aParam)));
+        }
+
+        //数据处理
+        foreach ($aResult as $key => $value) {
+            if(empty($value['refer_doi'])){
+                continue;
+            }
+            //调用获取参考文献详情队列
+            \think\Queue::push('app\api\job\ArticleReferDetailQueue@fire', $value, 'ArticleReferDetailQueue');
+        }
+        return json_encode(['status' => 1,'msg' => 'Add to reference processing queue']);
+    }
+    /**
+     * 处理参考文献
+     *
+     * @return void
+     */
+    public function get($aParam = []) {
+        // 获取生产文章ID
+        $iPReferId = empty($aParam['p_refer_id']) ? 0 : $aParam['p_refer_id'];
+        if(empty($iPReferId)) {
+            return json_encode(array('status' => 2,'msg' => 'Please select a reference'.json_encode($aParam) ));
+        }
+        // 获取生产文章ID
+        $iPArticleId = empty($aParam['p_article_id']) ? 0 : $aParam['p_article_id'];
+        if(empty($iPArticleId)) {
+            return json_encode(array('status' => 2,'msg' => 'Please select an production article'.json_encode($aParam) ));
+        }
+        //查询未处理过的数据
+        $aWhere = ['p_refer_id' => $iPReferId,'p_article_id' => $iPArticleId,'state' => 0];
+        $aRefer = Db::name('production_article_refer')->field('refer_doi,refer_content')->where($aWhere)->find();
+        if(empty($aRefer)){
+            return json_encode(array('status' => 2,'msg' => 'No reference records found'.json_encode($aParam)));
+        }
+        if(empty($aRefer['refer_doi'])){
+            return json_encode(['status' => 4,'msg' => 'Reference DOI is empty'.json_encode($aParam)]);
+        }
+
+        //数据处理
+        $doi = str_replace('/', '%2F', $aRefer['refer_doi']);
+        $url = "https://citation.doi.org/format?doi=$doi&style=cancer-translational-medicine&lang=en-US";
+        $res = myGet($url);
+        $frag = trim(substr($res, strpos($res, '.') + 1));
+        if(empty($frag)){
+            $aUpdate = ['refer_frag' => $aRefer['refer_content'],'refer_type' => 'other','is_deal' => 1,'update_time' => time()];
+            $aWhere = ['p_refer_id' => $iPReferId];
+            $result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate);
+            //写入通过AI获取参考文献详情队列
+            \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi');
+            return json_encode(array('status' => 2,'msg' => 'The data obtained from the interface is empty'.$url));
+        }
+
+        //整理数据入库
+        $update = [];
+        if (mb_substr_count($frag, '.') != 3){
+            $f = $frag . " Available at: " . PHP_EOL . "https://doi.org/" . $aRefer['refer_doi'];
+            $update['refer_type'] = "other";
+            $update['refer_frag'] = $f;
+            $update['cs'] = 1;
+            //写入通过AI获取参考文献详情队列
+            \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi');
+        } 
+        if (mb_substr_count($frag, '.') == 3){
+            $res = explode('.', $frag);
+            $update['author'] = prgeAuthor($res[0]);
+            $update['title'] = trim($res[1]);
+            $bj = bekjournal($res[2]);
+            $joura = formateJournal(trim($bj[0]));
+            $update['joura'] = $joura;
+            $is_js = 0;
+            if ($joura == trim($bj[0])) {
+            }
+            $update['refer_type'] = "journal";
+            $update['is_ja'] = $joura == trim($bj[0]) ? 0 : 1;
+            $update['dateno'] = str_replace(' ', '', str_replace('-', '–', trim($bj[1])));
+            //新增处理 期卷页码 20251127 start
+            if(!empty($update['dateno'])){
+                $sStr = $update['dateno'];
+                $aStr = explode(':', $sStr);
+                if(!empty($aStr[1])){
+                    $parts = explode('–', $aStr[1]);
+                    if(count($parts) == 2){
+                        $prefix = empty($parts[0]) ? 0 : intval($parts[0]);
+                        $suffix = empty($parts[1]) ? 0 : intval($parts[1]);
+                        if($prefix > $suffix){
+                            $prefixLen = strlen($prefix);
+                            $suffixLen = strlen($suffix);
+                            $missingLen = $prefixLen - $suffixLen;
+                            if ($missingLen > 0) {
+                                $fillPart = substr($prefix, 0, $missingLen);
+                                $newSuffix = $fillPart . $suffix;
+                                $update['dateno'] = $aStr[0].':'.$prefix.'-'.$newSuffix;
+                            }
+                        }
+                    }   
+                }
+                if(empty($aStr[1])){
+                    //写入通过AI获取参考文献详情队列
+                    \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi');
+                }
+            }
+            //新增处理 期卷页码 20251127 end
+            $update['doilink'] = strpos($aRefer['refer_doi'],"http")===false?"https://doi.org/" . $aRefer['refer_doi']:$aRefer['refer_doi'];
+            $update['cs'] = 1;
+        }
+        //数据库更新
+        if(empty($update)){
+            return json_encode(array('status' => 3,'msg' => 'Update data to empty'.$url.'====='.$frag));
+        }
+        $aWhere = ['p_refer_id' => $iPReferId];
+        $update += ['is_deal' => 1,'update_time' => time()];
+        $result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($update);
+        if($result === false){
+            return json_encode(array('status' => 3,'msg' => 'Update failed'.json_encode($update)));
+        }
+        return json_encode(['status' => 1,'msg' => 'Update successful']);
+    }
+
+    // /**
+    //  * 实例方法:提取单个DOI(核心逻辑,生产级优化)
+    //  * @param string $str 待检测字符串
+    //  * @param bool $standardize 是否标准化DOI(转小写)
+    //  * @param bool $forceExtract 是否强制提取(忽略微小格式瑕疵)
+    //  * @return array 提取结果(含错误码、错误信息、DOI)
+    //  */
+    // // public function extractDoiFromString(string $str, bool $standardize = true, bool $forceExtract = false): array
+    // // {
+    // //     // 初始化标准化结果
+    // //     $result = [
+    // //         'has_doi'    => false,
+    // //         'doi'        => null,
+    // //         'error_code' => null,
+    // //         'error_msg'  => null,
+    // //     ];
+
+    // //     try {
+    // //         // 严格类型校验(防止非字符串参数传入)
+    // //         if (!is_string($str)) {
+    // //             throw new InvalidArgumentException('输入参数必须为字符串类型', 1001);
+    // //         }
+    // //         // 字符串预处理(生产级:全角转半角、URL解码、HTML标签移除等)
+    // //         $processedStr = $this->preprocessString($str);
+    // //         if (trim($processedStr) === '') {
+    // //             $result['error_code'] = 'EMPTY_STRING';
+    // //             $result['error_msg'] = self::ERROR_CODES['EMPTY_STRING'];
+    // //             return $result;
+    // //         }
+
+    // //         // 性能优化:用preg_match仅匹配首个DOI,替代preg_match_all
+    // //         // 优化后的带前缀版正则
+    // //         $pattern = '/(?:doi[:\s]*|DOI[:\s]*)?\b10\.\d+(?:\.\d+)*\/[a-zA-Z0-9._\-!()%\/:;@$&+=?#[\]<>~`|^'"{},\\\\]+(?![\w?#])/i";
+    // //         if (!preg_match($pattern, $processedStr, $match)) {
+    // //             $result['error_code'] = 'NO_MATCH';
+    // //             $result['error_msg'] = self::ERROR_CODES['NO_MATCH'];
+    // //             return $result;
+    // //         }
+
+    // //         // 清洗并验证首个DOI
+    // //         $cleanDoi = $this->cleanAndValidateDoi($match[0], $standardize, $forceExtract);
+    // //         if ($cleanDoi !== null) {
+    // //             $result['has_doi'] = true;
+    // //             $result['doi'] = $cleanDoi;
+    // //         } else {
+    // //             // 根据是否强制提取设置错误信息
+    // //             $errorKey = $forceExtract ? 'FORCE_EXTRACT_FAILED' : 'INVALID_AFTER_CLEAN';
+    // //             $result['error_code'] = $errorKey;
+    // //             $result['error_msg'] = self::ERROR_CODES[$errorKey];
+    // //         }
+
+    // //     } catch (InvalidArgumentException $e) {
+    // //         // 业务异常:标准化错误码和信息
+    // //         $result['error_code'] = 'INVALID_PARAM';
+    // //         $result['error_msg'] = '参数错误:' . $e->getMessage();
+    // //     } catch (Exception $e) {
+    // //         // 系统异常:隐藏敏感信息,记录通用错误
+    // //         $result['error_code'] = 'EXTRACTION_EXCEPTION';
+    // //         $result['error_msg'] = self::ERROR_CODES['EXTRACTION_EXCEPTION'] . ':' . $e->getMessage();
+    // //     }
+
+    // //     return $result;
+    // // }
+
+    // // /**
+    // //  * 字符串预处理(生产级:覆盖所有编码/格式干扰场景)
+    // //  * @param string $str 原始字符串
+    // //  * @return string 预处理后的纯净字符串
+    // //  */
+    // // private function preprocessString(string $str): string
+    // // {
+    // //     // 1. 全角转半角(解决中文全角字符干扰,如10.1007/s11042-020-10103-4)
+    // //     $str = $this->fullWidthToHalfWidth($str);
+    // //     // 2. 移除所有HTML标签(解决网页文本中DOI被

//等标签包裹的问题) + // // $str = strip_tags($str); + // // // 3. URL解码(处理%2F等URL编码的特殊字符,如10.1007%2Fs11042-020-10103-4) + // // $str = urldecode($str); + // // // 4. 解码HTML实体(处理&、/等HTML实体编码) + // // $str = html_entity_decode($str, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + // // // 5. 移除不可见字符(换行、制表符、零宽空格、控制字符等) + // // $str = preg_replace('/[\x00-\x1F\x7F\x{200B}-\x{200F}]/u', ' ', $str); + // // // 6. 合并多个空格为单个(避免连续空格干扰正则匹配) + // // $str = preg_replace('/\s+/', ' ', $str); + + // // return $str; + // // } + + // // /** + // // * 清洗并验证DOI(生产级:优化正则规则,严格长度校验) + // // * @param string $match 原始正则匹配结果 + // // * @param bool $standardize 是否标准化DOI(转小写) + // // * @param bool $forceExtract 是否强制提取 + // // * @return string|null 有效DOI或null + // // */ + // // private function cleanAndValidateDoi(string $match, bool $standardize, bool $forceExtract): ?string + // // { + // // // 1. 移除DOI前缀(doi:/DOI:)和首尾空白字符 + // // $cleanDoi = preg_replace('/^doi[:\s]?|^DOI[:\s]?/i', '', trim($match)); + // // // 2. 移除尾部常见标点(避免DOI被标点包裹,如10.1007/s11042-020-10103-4.) + // // $cleanDoi = rtrim($cleanDoi, '.,;(){}[]!?"\''); + + // // // 3. 严格的长度校验(DOI官方规范:6-200字符) + // // $doiLength = strlen($cleanDoi); + // // if ($doiLength < 6 || $doiLength > 200) { + // // return null; + // // } + + // // // 4. 验证规则(生产级优化:添加单词边界,避免匹配不完整DOI) + // // // 基础规则:严格遵循官方规范,10.开头+包含/+/后有内容 + // // $basicRule = '/^10\.\d+\/.+$/D'; + // // // 宽松规则:强制提取时使用,添加单词边界,避免匹配被字符包裹的DOI + // // $looseRule = '/\b10\.\d+\/[^\s%]{1,190}\b/'; + + // // $validateRule = $forceExtract ? $looseRule : $basicRule; + // // $isValid = preg_match($validateRule, $cleanDoi) === 1; + + // // // 5. 验证通过则标准化(转小写),否则返回null + // // if ($isValid) { + // // return $standardize ? strtolower($cleanDoi) : $cleanDoi; + // // } + + // // return null; + // // } + + // // /** + // // * 辅助方法:全角转半角 + // // * @param string $str 包含全角字符的字符串 + // // * @return string 半角字符串 + // // */ + // // private function fullWidthToHalfWidth(string $str): string + // // { + // // $fullWidthChars = [ + // // '0' => '0', '1' => '1', '2' => '2', '3' => '3', '4' => '4', + // // '5' => '5', '6' => '6', '7' => '7', '8' => '8', '9' => '9', + // // '.' => '.', '/' => '/', '-' => '-', '%' => '%', '!' => '!', + // // '(' => '(', ')' => ')', ':' => ':', ';' => ';', ',' => ',', + // // '"' => '"', ''' => '\'' + // // ]; + + // // return strtr($str, $fullWidthChars); + // // } +} +?> \ No newline at end of file From c15b784cf8651f1b1a56cc390264e618061af160 Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 28 Nov 2025 16:35:11 +0800 Subject: [PATCH 17/53] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ArticleParserService.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/application/common/ArticleParserService.php b/application/common/ArticleParserService.php index fd3dfc16..526ac52a 100644 --- a/application/common/ArticleParserService.php +++ b/application/common/ArticleParserService.php @@ -244,7 +244,7 @@ class ArticleParserService foreach ($section->getElements() as $element) { $text = $this->getTextFromElement($element); $length = mb_strlen(trim($text)); - if ($length > $maxLength && $length > 10) { // 标题通常较长 + if ($length > $maxLength && $length > 3) { // 标题通常较长 $title = trim($text); $maxLength = $length; break 2; // 取第一个最长段落作为标题 @@ -610,7 +610,8 @@ class ArticleParserService ]; } if(empty($aCorresponding)){ - $pattern = '/Corresponding Authors|Correspondence to: (.*?)(?=$|;)/s'; + $pattern = '/Corresponding Authors|Correspondence to|Correspondence: (.*?)(?=$|;)/s'; + $corrText = trim($corrText,'*'); preg_match($pattern, $corrText, $match); if (!empty($match[1])) { $corrContent = $match[1]; @@ -625,6 +626,16 @@ class ArticleParserService ]; } } + if(empty($authors[1])){ + $authorPattern = '/([A-Za-z0-9\s]+?),\s*([\w@\.\-]+)(?=\.?)/'; + preg_match_all($authorPattern, $corrContent, $authors); + for ($i = 0; $i < count($authors[1]); $i++) { + $aCorresponding[] = [ + 'name' => empty($authors[1][$i]) ? '' : trim($authors[1][$i]), + 'email' => empty($authors[2][$i]) ? '' : trim($authors[2][$i]) + ]; + } + } } } return $aCorresponding; From 93f9e705cb34763e71f2b3e24df54a1f84f6bbe9 Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 1 Dec 2025 11:53:41 +0800 Subject: [PATCH 18/53] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ArticleParserService.php | 363 ++++++++++++++++---- 1 file changed, 295 insertions(+), 68 deletions(-) diff --git a/application/common/ArticleParserService.php b/application/common/ArticleParserService.php index 526ac52a..05881dee 100644 --- a/application/common/ArticleParserService.php +++ b/application/common/ArticleParserService.php @@ -14,7 +14,7 @@ class ArticleParserService { private $phpWord; private $sections; - + private $iNum = 0; public function __construct($filePath = '') { if (!file_exists($filePath)) { @@ -736,6 +736,10 @@ class ArticleParserService // 统一提取元素文本 private function getTextFromElement($element,$lineNumber = 0){ $text = ''; + if ($element instanceof \PhpOffice\PhpWord\Element\ListItemRun) { + $this->iNum++; + $text .= $this->iNum; + } // 处理PreserveText元素 if ($element instanceof \PhpOffice\PhpWord\Element\PreserveText) { // 通过反射获取私有属性 text @@ -940,106 +944,329 @@ class ArticleParserService ] ]; } - private function fullDecode($str, $maxDepth = 5) { + /** + * 核心解码方法(无静态缓存,高性能版) + * @param string $str 待解码字符串 + * @param int $maxDepth 最大解析深度 + * @return string + */ + private function fullDecode($str, $maxDepth = 2) + { // 空值/深度为0,直接返回(提前终止,避免无效操作) if (empty($str) || $maxDepth <= 0) { return $str; } - - // 【性能优化1:预编译所有正则表达式】避免每次循环重新解析正则 - // 预编译:≥专属场景正则 - $regOb0 = '/0B\s*\?0/'; - $regDl18 = '/DL\s*\?.18/'; - // 预编译:≥通用场景正则 - $regQMarkNum = '/\?(\d+)/'; - $regQMarkDotNum = '/\?(\.\d+)/'; - // 预编译:≤、≠空格修复正则 - $regNeNum = '/≠\s*(\d+)/'; - $regLeNum = '/≤\s*(\d+)/'; - // 预编译:混合符号乱码正则(中文顿号/英文逗号) - $regMixCn = '/(\?)\s*、\s*(\?)\s*、\s*(\?)(\d+)/'; - $regMixEn = '/(\?)\s*,\s*(\?)\s*,\s*(\?)(\d+)/'; - // 预编译:≤、≠专属标识正则 - $regLeMark = '/LE\s*\?(\d+)/'; - $regNeMark = '/NE\s*\?(\d+)/'; - // 预编译:Unicode转义正则(提取到外部,避免闭包重复创建) - $regUnicode = '/\\\\u([0-9a-fA-F]{4})/'; - - // 【性能优化2:预定义常量/映射】避免循环内重复创建数组/字符串 - // HTML实体映射(一次性定义,避免循环内重复赋值) - $htmlEntityMap = [ - '≤' => '≤', '≤' => '≤', '≤' => '≤', - '≥' => '≥', '≥' => '≥', '≥' => '≥', - '≠' => '≠', '≠' => '≠', '≠' => '≠', + $str = $this->decodeUnicode($str); + // ========== 预编译所有正则(合并同类型,避免循环内重复解析) ========== + $regexps = [ + // 原有专属场景正则 + 'ob0' => '/0B\s*\?0/', + 'dl18' => '/DL\s*\?.18/', + // 原有通用场景正则 + 'qMarkNum' => '/\?(\d+)/', + 'qMarkDotNum' => '/\?(\.\d+)/', + // ≤、≠空格修复正则 + 'neNum' => '/≠\s*(\d+)/', + 'leNum' => '/≤\s*(\d+)/', + // 混合符号乱码正则(合并中英文顿号/逗号) + 'mixSymbol' => '/(\?)\s*(、|,)\s*(\?)\s*(、|,)\s*(\?)(\d+)/', + // ≤、≠专属标识正则(合并LE/NE) + 'leNeMark' => '/(LE|NE)\s*\?(\d+)/', + // Unicode转义正则 + 'unicode' => '/\\\\u([0-9a-fA-F]{4})/', + // Word二进制乱码(合并≤≥≠) + 'wordBin' => '/(\\xE2\\x89\\x86|\\xE2 0x89 0x86|e28986|\\xE2\\x89\\x87|\\xE2 0x89 0x87|e28987|\\xE2\\x89\\x80|\\xE2 0x89 0x80|e28980)/i', + // Word XML实体异常(合并≤≥≠) + 'wordEntity' => '/&#\s*(\x|X)?\s*(2264|2265|2260)\s*;?/i', + // 不可见控制字符 + 'controlChar' => '/[\x00-\x1F\x7F]/', + // 重复符号去重(合并≤≥≠) + 'repeatSymbol' => '/(≤{2,}|≥{2,}|≠{2,})/', + // GBK编码乱码(合并≤≥≠) + 'gbkSymbol' => '/(\xA1\xF2|\xA1\xF3|\xA1\xF0)/' ]; - // 不间断空格替换数组 - $nbspReplace = [chr(0xC2) . chr(0xA0), chr(0xA0)]; - // Unicode回调函数(预定义,避免循环内重复创建闭包) + + // ========== 预定义所有替换映射(避免循环内重复创建) ========== + $maps = [ + // HTML实体映射(扩展Word实体) + 'htmlEntity' => [ + '≤' => '≤', '≤' => '≤', '≤' => '≤', + '≥' => '≥', '≥' => '≥', '≥' => '≥', + '≠' => '≠', '≠' => '≠', '≠' => '≠', + '&le' => '≤', '&ge' => '≥', '&ne' => '≠', + 'ࣘ' => '≤', 'ࣙ' => '≥', 'ࣔ' => '≠', + '≤' => '≤', '≥' => '≥', '≠' => '≠', + '≤' => '≤', '≥' => '≥', '≠' => '≠', + '<' => '≤', '>' => '≥' + ], + // 空格替换数组(扩展Word中的各种空格) + 'nbsp' => [ + chr(0xC2) . chr(0xA0), // UTF-8不间断空格 + chr(0xA0), // 拉丁1不间断空格 + ' ', // 全角空格 + chr(0x2002), // 方头空格 + chr(0x2003), // 全角空格 + chr(0x2004) // 三分之一全角空格 + ], + // 二进制乱码映射 + 'wordBin' => [ + 'e28986' => '≤', '\\xe2\\x89\\x86' => '≤', '\\xe2 0x89 0x86' => '≤', + 'e28987' => '≥', '\\xe2\\x89\\x87' => '≥', '\\xe2 0x89 0x87' => '≥', + 'e28980' => '≠', '\\xe2\\x89\\x80' => '≠', '\\xe2 0x89 0x80' => '≠' + ], + // XML实体编码映射 + 'wordEntity' => [ + '2264' => '≤', + '2265' => '≥', + '2260' => '≠' + ], + // GBK编码映射 + 'gbkSymbol' => [ + '\xA1\xF2' => '≤', + '\xA1\xF3' => '≥', + '\xA1\xF0' => '≠' + ] + ]; + + // 预定义回调函数(仅创建一次,避免循环内重复实例化) $unicodeCallback = function ($m) { return mb_chr(hexdec($m[1]), 'UTF-8') ?: $m[0]; }; - $original = $str; $depth = 0; - $hasChange = false; // 标记是否有变化,提前终止循环 + $hasChange = false; + $original = $str; // 循环解码:仅在有变化且未达最大深度时执行 do { $depth++; $hasChange = false; - $prevStr = $str; // 保存当前状态,用于判断变化 + $prevStr = $str; - // 1. 解码Unicode转义(\uXXXX格式) - $str = $this->decodeUnicode($str); + // ========== 前置处理(惰性执行,避免无意义操作) ========== + $countCtrl = 0; + // 1. 过滤不可见控制字符(仅当包含时执行) + if (preg_match($regexps['controlChar'], $str)) { + $str = preg_replace($regexps['controlChar'], '', $str, -1, $countCtrl); + } - // 2. 解码HTML实体(先替换专属实体,再执行通用解码) - $str = strtr($str, $htmlEntityMap); // 高性能替换(strtr比str_replace快) - $str = html_entity_decode($str, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + // 2. GBK/GB2312编码转UTF-8(仅当非UTF-8时执行) + if (!mb_check_encoding($str, 'UTF-8')) { + $str = mb_convert_encoding($str, 'UTF-8', 'GBK,GB2312,ISO-8859-1'); + } - // 3. 再次处理遗漏的Unicode转义(使用预编译正则+预定义回调) - $str = preg_replace_callback($regUnicode, $unicodeCallback, $str); + // ========== 核心解码逻辑 ========== + // 1. 解码Unicode转义 + $str = preg_replace_callback($regexps['unicode'], $unicodeCallback, $str); - // 4. 替换不间断空格为普通空格(strtr比str_replace更高效) - $str = str_replace($nbspReplace, ' ', $str); + // 2. 解码HTML实体(高性能strtr替换) + $str = strtr($str, $maps['htmlEntity']); + $str = html_entity_decode($str, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8'); - // 5. 核心替换逻辑(优化执行顺序,避免覆盖) - // 5.1 原有≥专属场景(保留) - $str = preg_replace($regOb0, '0B≥30', $str, -1, $count1); - $str = preg_replace($regDl18, 'DL≥0.18', $str, -1, $count2); - // 5.2 ≤、≠空格修复(保留) - $str = preg_replace($regNeNum, '≠$1', $str, -1, $count3); - $str = preg_replace($regLeNum, '≤$1', $str, -1, $count4); - // 5.3 原有≥通用场景(保留) - $str = preg_replace($regQMarkNum, '≥$1', $str, -1, $count5); - $str = preg_replace($regQMarkDotNum, '≥0$1', $str, -1, $count6); - // 5.4 混合符号乱码还原(保留) - $str = preg_replace($regMixCn, '≤、≥、≠$4', $str, -1, $count7); - $str = preg_replace($regMixEn, '≤、≥、≠$4', $str, -1, $count8); - // 5.5 ≤、≠专属标识还原(保留) - $str = preg_replace($regLeMark, '≤$1', $str, -1, $count9); - $str = preg_replace($regNeMark, '≠$1', $str, -1, $count10); + // 3. 替换各种空格为普通空格 + $str = str_replace($maps['nbsp'], ' ', $str); - // 5.6 修复前缀"d with "乱码(保留) - $str = str_replace('d with ', 'd with ', $str, $count11); + // ========== Word特殊符号乱码修复(合并+惰性) ========== + $countBin = $countEnt = $countGbk = $countRepeat = 0; + + // 1. 二进制乱码还原(合并正则+回调) + if (preg_match($regexps['wordBin'], $str)) { + $str = preg_replace_callback($regexps['wordBin'], function ($m) use ($maps) { + $key = strtolower(str_replace(' ', '', $m[0])); + return $maps['wordBin'][$key] ?? $m[0]; + }, $str, -1, $countBin); + } + + // 2. XML实体异常修复(合并正则+回调) + if (preg_match($regexps['wordEntity'], $str)) { + $str = preg_replace_callback($regexps['wordEntity'], function ($m) use ($maps) { + return $maps['wordEntity'][$m[2]] ?? $m[0]; + }, $str, -1, $countEnt); + } + + // 3. GBK编码乱码修复(合并正则+回调) + if (preg_match($regexps['gbkSymbol'], $str)) { + $str = preg_replace_callback($regexps['gbkSymbol'], function ($m) use ($maps) { + return $maps['gbkSymbol'][$m[0]] ?? $m[0]; + }, $str, -1, $countGbk); + } + + // 4. 重复符号去重(合并正则+极简回调) + if (preg_match($regexps['repeatSymbol'], $str)) { + $str = preg_replace_callback($regexps['repeatSymbol'], function ($m) { + return $m[0][0]; // 取第一个字符实现去重 + }, $str, -1, $countRepeat); + } + + // ========== 原有核心替换逻辑(合并+惰性) ========== + $count1 = $count2 = $count3 = $count4 = $count5 = $count6 = 0; + $count7 = $count8 = $count9 = 0; + + // 1. 专属场景替换(惰性执行) + if (strpos($str, '0B?0') !== false) { + $str = preg_replace($regexps['ob0'], '0B≥30', $str, -1, $count1); + } + if (strpos($str, 'DL?.18') !== false) { + $str = preg_replace($regexps['dl18'], 'DL≥0.18', $str, -1, $count2); + } + + // 2. ≤、≠空格修复(惰性执行) + if (preg_match($regexps['neNum'], $str)) { + $str = preg_replace($regexps['neNum'], '≠$1', $str, -1, $count3); + } + if (preg_match($regexps['leNum'], $str)) { + $str = preg_replace($regexps['leNum'], '≤$1', $str, -1, $count4); + } + + // 3. 通用场景替换(惰性执行) + if (preg_match($regexps['qMarkNum'], $str)) { + $str = preg_replace($regexps['qMarkNum'], '≥$1', $str, -1, $count5); + } + if (preg_match($regexps['qMarkDotNum'], $str)) { + $str = preg_replace($regexps['qMarkDotNum'], '≥0$1', $str, -1, $count6); + } + + // 4. 混合符号乱码还原(合并中英文,惰性执行) + if (preg_match($regexps['mixSymbol'], $str)) { + $str = preg_replace($regexps['mixSymbol'], '≤$2≥$4≠$6', $str, -1, $count7); + } + + // 5. ≤、≠专属标识还原(合并正则,惰性执行) + if (preg_match($regexps['leNeMark'], $str)) { + $str = preg_replace_callback($regexps['leNeMark'], function ($m) { + return $m[1] === 'LE' ? '≤' . $m[2] : '≠' . $m[2]; + }, $str, -1, $count8); + } + + // 6. 修复前缀"d with "乱码(惰性执行) + if (strpos($str, 'd with ') !== false) { + $str = str_replace('d with ', 'd with ', $str, $count9); + } + + // ========== 变化判断(合并计数,减少运算) ========== + $totalCount = $countCtrl + $countBin + $countEnt + $countGbk + $countRepeat + + $count1 + $count2 + $count3 + $count4 + $count5 + $count6 + + $count7 + $count8 + $count9; - // 【性能优化3:统计所有替换次数,判断是否有变化】 - $totalCount = $count1 + $count2 + $count3 + $count4 + $count5 + $count6 + - $count7 + $count8 + $count9 + $count10 + $count11; if ($totalCount > 0 || $str !== $prevStr) { $hasChange = true; $original = $str; } - // 【性能优化4:提前终止】单次循环无变化,直接退出 + // 提前终止:无变化则退出循环 if (!$hasChange) { break; } - } while ($depth < $maxDepth); // 改用do-while,减少循环判断次数 + } while ($depth < $maxDepth); - // 最终清理:仅执行一次trim - return trim($str, ':'); + // 最终清理+兜底替换 + $str = trim($str, ':'); + $str = strtr($str, $maps['htmlEntity']); + + return $str; } + + // private function fullDecode($str, $maxDepth = 5) { + // // 空值/深度为0,直接返回(提前终止,避免无效操作) + // if (empty($str) || $maxDepth <= 0) { + // return $str; + // } + + // // 【性能优化1:预编译所有正则表达式】避免每次循环重新解析正则 + // // 预编译:≥专属场景正则 + // $regOb0 = '/0B\s*\?0/'; + // $regDl18 = '/DL\s*\?.18/'; + // // 预编译:≥通用场景正则 + // $regQMarkNum = '/\?(\d+)/'; + // $regQMarkDotNum = '/\?(\.\d+)/'; + // // 预编译:≤、≠空格修复正则 + // $regNeNum = '/≠\s*(\d+)/'; + // $regLeNum = '/≤\s*(\d+)/'; + // // 预编译:混合符号乱码正则(中文顿号/英文逗号) + // $regMixCn = '/(\?)\s*、\s*(\?)\s*、\s*(\?)(\d+)/'; + // $regMixEn = '/(\?)\s*,\s*(\?)\s*,\s*(\?)(\d+)/'; + // // 预编译:≤、≠专属标识正则 + // $regLeMark = '/LE\s*\?(\d+)/'; + // $regNeMark = '/NE\s*\?(\d+)/'; + // // 预编译:Unicode转义正则(提取到外部,避免闭包重复创建) + // $regUnicode = '/\\\\u([0-9a-fA-F]{4})/'; + + // // 【性能优化2:预定义常量/映射】避免循环内重复创建数组/字符串 + // // HTML实体映射(一次性定义,避免循环内重复赋值) + // $htmlEntityMap = [ + // '≤' => '≤', '≤' => '≤', '≤' => '≤', + // '≥' => '≥', '≥' => '≥', '≥' => '≥', + // '≠' => '≠', '≠' => '≠', '≠' => '≠', + // ]; + // // 不间断空格替换数组 + // $nbspReplace = [chr(0xC2) . chr(0xA0), chr(0xA0)]; + // // Unicode回调函数(预定义,避免循环内重复创建闭包) + // $unicodeCallback = function ($m) { + // return mb_chr(hexdec($m[1]), 'UTF-8') ?: $m[0]; + // }; + + // $original = $str; + // $depth = 0; + // $hasChange = false; // 标记是否有变化,提前终止循环 + + // // 循环解码:仅在有变化且未达最大深度时执行 + // do { + // $depth++; + // $hasChange = false; + // $prevStr = $str; // 保存当前状态,用于判断变化 + + // // 1. 解码Unicode转义(\uXXXX格式) + // $str = $this->decodeUnicode($str); + + // // 2. 解码HTML实体(先替换专属实体,再执行通用解码) + // $str = strtr($str, $htmlEntityMap); // 高性能替换(strtr比str_replace快) + // $str = html_entity_decode($str, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // // 3. 再次处理遗漏的Unicode转义(使用预编译正则+预定义回调) + // $str = preg_replace_callback($regUnicode, $unicodeCallback, $str); + + // // 4. 替换不间断空格为普通空格(strtr比str_replace更高效) + // $str = str_replace($nbspReplace, ' ', $str); + + // // 5. 核心替换逻辑(优化执行顺序,避免覆盖) + // // 5.1 原有≥专属场景(保留) + // $str = preg_replace($regOb0, '0B≥30', $str, -1, $count1); + // $str = preg_replace($regDl18, 'DL≥0.18', $str, -1, $count2); + // // 5.2 ≤、≠空格修复(保留) + // $str = preg_replace($regNeNum, '≠$1', $str, -1, $count3); + // $str = preg_replace($regLeNum, '≤$1', $str, -1, $count4); + // // 5.3 原有≥通用场景(保留) + // $str = preg_replace($regQMarkNum, '≥$1', $str, -1, $count5); + // $str = preg_replace($regQMarkDotNum, '≥0$1', $str, -1, $count6); + // // 5.4 混合符号乱码还原(保留) + // $str = preg_replace($regMixCn, '≤、≥、≠$4', $str, -1, $count7); + // $str = preg_replace($regMixEn, '≤、≥、≠$4', $str, -1, $count8); + // // 5.5 ≤、≠专属标识还原(保留) + // $str = preg_replace($regLeMark, '≤$1', $str, -1, $count9); + // $str = preg_replace($regNeMark, '≠$1', $str, -1, $count10); + + // // 5.6 修复前缀"d with "乱码(保留) + // $str = str_replace('d with ', 'd with ', $str, $count11); + + // // 【性能优化3:统计所有替换次数,判断是否有变化】 + // $totalCount = $count1 + $count2 + $count3 + $count4 + $count5 + $count6 + + // $count7 + $count8 + $count9 + $count10 + $count11; + // if ($totalCount > 0 || $str !== $prevStr) { + // $hasChange = true; + // $original = $str; + // } + + // // 【性能优化4:提前终止】单次循环无变化,直接退出 + // if (!$hasChange) { + // break; + // } + + // } while ($depth < $maxDepth); // 改用do-while,减少循环判断次数 + + // // 最终清理:仅执行一次trim + // return trim($str, ':'); + // } // private function fullDecode($str, $maxDepth = 5) { // if (empty($str) || $maxDepth <= 0) { // return $str; From 705dce5e94b5e049d9aae0df531c3eed7584443b Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 2 Dec 2025 13:17:23 +0800 Subject: [PATCH 19/53] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ArticleParserService.php | 607 +++++++++++++------- 1 file changed, 405 insertions(+), 202 deletions(-) diff --git a/application/common/ArticleParserService.php b/application/common/ArticleParserService.php index 05881dee..ff715a30 100644 --- a/application/common/ArticleParserService.php +++ b/application/common/ArticleParserService.php @@ -553,7 +553,7 @@ class ArticleParserService if (!empty($institution) && !mb_check_encoding($institution, 'UTF-8')) { $institution = mb_convert_encoding($institution, 'UTF-8', 'GBK'); } - $aCompany[$number] = $institution; + $aCompany[$number] = empty($institution) ? '' : trim(trim($institution),'.'); } return $aCompany; } @@ -581,6 +581,7 @@ class ArticleParserService $corrText = mb_convert_encoding($corrText, 'UTF-8', 'GBK'); } $corrText = $this->fullDecode($corrText); + // // 调试 // file_put_contents(ROOT_PATH . 'runtime/corr_text_raw.log', $corrText); @@ -605,24 +606,25 @@ class ArticleParserService $aCorresponding[] = [ 'name' => $sName, 'email' => isset($email[2]) ? trim($email[2]) : '', - 'postal_address' => isset($address[2]) ? trim($address[2]) : '', + 'postal_address' => isset($address[2]) ? trim(trim($address[2]),'.') : '', 'tel' => isset($tel[2]) ? trim($tel[2]) : '' ]; } if(empty($aCorresponding)){ - $pattern = '/Corresponding Authors|Correspondence to|Correspondence: (.*?)(?=$|;)/s'; + // $pattern = '/Corresponding Authors|Correspondence to|Correspondence: (.*?)(?=$|;)/s'; + $pattern = '/(Corresponding Authors|Correspondence to|Correspondence)\s*:\s*([\s\S]+?)(?=\n\s*\n|$|;)/is'; $corrText = trim($corrText,'*'); preg_match($pattern, $corrText, $match); - if (!empty($match[1])) { - $corrContent = $match[1]; + if (!empty($match[2])) { + $corrContent = $match[2]; // 提取每个作者的名称和邮箱(优化正则,支持更多字符) $authorPattern = '/([A-Za-z\s]+?),\s*E-mail:\s*([\w@\.\-]+)/'; preg_match_all($authorPattern, $corrContent, $authors); if(!empty($authors[1])){ for ($i = 0; $i < count($authors[1]); $i++) { $aCorresponding[] = [ - 'name' => empty($authors[1][$i]) ? '' : trim($authors[1][$i]), - 'email' => empty($authors[2][$i]) ? '' : trim($authors[2][$i]) + 'name' => empty($authors[1][$i]) ? '' : trim(trim($authors[1][$i]),'.'), + 'email' => empty($authors[2][$i]) ? '' : trim(trim($authors[2][$i]),'.') ]; } } @@ -631,8 +633,8 @@ class ArticleParserService preg_match_all($authorPattern, $corrContent, $authors); for ($i = 0; $i < count($authors[1]); $i++) { $aCorresponding[] = [ - 'name' => empty($authors[1][$i]) ? '' : trim($authors[1][$i]), - 'email' => empty($authors[2][$i]) ? '' : trim($authors[2][$i]) + 'name' => empty($authors[1][$i]) ? '' : trim(trim($authors[1][$i]),'.'), + 'email' => empty($authors[2][$i]) ? '' : trim(trim($authors[2][$i]),'.') ]; } } @@ -734,88 +736,293 @@ class ArticleParserService } // 统一提取元素文本 - private function getTextFromElement($element,$lineNumber = 0){ + private function getTextFromElement(\PhpOffice\PhpWord\Element\AbstractElement $element, int $lineNumber = 0){ $text = ''; - if ($element instanceof \PhpOffice\PhpWord\Element\ListItemRun) { - $this->iNum++; - $text .= $this->iNum; + + // 1. 常量化特殊引号映射(避免每次调用重建数组,提升循环调用性能) + static $specialQuotesMap = [ + '’' => "'", // 右单引号(U+2019)→ 普通单引号(U+0027) + '‘' => "'", // 左单引号(U+2018)→ 普通单引号(U+0027) + '“' => '"', // 左双引号(U+201C)→ 普通双引号(U+0022) + '”' => '"', // 右双引号(U+201D)→ 普通双引号(U+0022) + '„' => '"', // 下双引号(U+201E)→ 普通双引号(兼容欧洲排版) + '‟' => '"', // 右双引号(U+201F)→ 普通双引号(兼容少见排版) + ]; + + // 支持H1-H9标题格式(优化:移除无用变量 $titleDepth,避免冗余) + if ($element instanceof \PhpOffice\PhpWord\Element\Title) { + $titleContent = $element->getText(); + $titleText = ''; + + if ($titleContent instanceof \PhpOffice\PhpWord\Element\TextRun) { + $titleText = $this->getTextFromElement($titleContent); + } else { + $titleText = strtr((string)$titleContent, $specialQuotesMap); + } + + $text .= $titleText . ' '; + return $this->cleanText($text); } - // 处理PreserveText元素 + + // 项目编号(优化:严格空值判断,避免 0 被 empty 误判) + if ($element instanceof \PhpOffice\PhpWord\Element\ListItemRun) { + $this->iNum = isset($this->iNum) && is_numeric($this->iNum) ? $this->iNum : 0; + $this->iNum++; + $text .= $this->iNum . ' '; + } + + // 处理PreserveText(含HYPERLINK邮箱提取,优化:反射前先判断属性存在) if ($element instanceof \PhpOffice\PhpWord\Element\PreserveText) { - // 通过反射获取私有属性 text - $reflection = new \ReflectionClass($element); - $property = $reflection->getProperty('text'); - $property->setAccessible(true); - $textParts = $property->getValue($element); + try { + $reflection = new \ReflectionClass($element); + // 先判断属性是否存在,避免反射不存在的属性报错(兼容极端版本) + if (!$reflection->hasProperty('text')) { + return $this->cleanText($text); + } + $property = $reflection->getProperty('text'); + $property->setAccessible(true); + $textParts = $property->getValue($element) ?? []; + } catch (\ReflectionException $e) { + return $this->cleanText($text); + } + foreach ($textParts as $part) { + $part = (string)$part; if (strpos($part, 'HYPERLINK') !== false) { - // 解码 HTML 实体(" -> ") - $decoded = html_entity_decode($part); - // 提取 mailto: 后的邮箱 - if (preg_match('/mailto:([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i', $decoded, $match)) { + $decoded = html_entity_decode($part, ENT_QUOTES | ENT_HTML5); + // 邮箱正则不变(已优化,兼容国际域名) + if (preg_match('/mailto:([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,10})/i', $decoded, $match)) { $text .= $match[1] . ' '; } } else { - // 普通文本直接拼接 + $part = strtr($part, $specialQuotesMap); $text .= $part; } } - return $text; + return $this->cleanText($text); } - // 处理表格和单元格(E-mail可能在表格中) + + // 处理表格(优化:避免行尾多余空格,通过 cleanText 自动合并) if ($element instanceof \PhpOffice\PhpWord\Element\Table) { foreach ($element->getRows() as $row) { foreach ($row->getCells() as $cell) { - $text .= $this->getTextFromElement($cell); + $text .= $this->getTextFromElement($cell) . ' '; } + // 移除行尾额外空格(cleanText 会合并连续空格,无需手动添加) } - return $text; + return $this->cleanText($text); } + + // 处理单元格(逻辑不变,保持递归提取) if ($element instanceof \PhpOffice\PhpWord\Element\Cell) { foreach ($element->getElements() as $child) { $text .= $this->getTextFromElement($child); } - return $text; + return $this->cleanText($text); } - //处理嵌套元素(递归提取所有子元素) - if (method_exists($element, 'getElements')) { + // 处理嵌套元素(逻辑不变,增强类型校验可读性) + if (method_exists($element, 'getElements') && is_callable([$element, 'getElements'])) { foreach ($element->getElements() as $child) { - $text .= $this->getTextFromElement($child); + if ($child instanceof \PhpOffice\PhpWord\Element\AbstractElement) { + $text .= $this->getTextFromElement($child); + } } } - //处理文本元素(包括带格式的文本) + // 处理纯文本元素(逻辑不变,保持特殊引号替换) if ($element instanceof \PhpOffice\PhpWord\Element\Text) { - $text .= $element->getText(); + $textPart = (string)$element->getText(); // 显式强制转换,避免类型隐患 + $textPart = strtr($textPart, $specialQuotesMap); + $text .= $textPart; } - //处理超链接(优先提取链接目标,可能是邮箱) + // 处理超链接(逻辑不变,保持邮箱优先提取) if ($element instanceof \PhpOffice\PhpWord\Element\Link) { - $target = $element->getTarget(); + $target = (string)$element->getTarget(); if (strpos($target, 'mailto:') === 0) { - $text .= str_replace('mailto:', '', $target) . ' '; // 剥离mailto:前缀 + $text .= rtrim(str_replace('mailto:', '', $target)) . ' '; } - $text .= $element->getText() . ' '; + $linkText = strtr((string)$element->getText(), $specialQuotesMap); + $text .= $linkText . ' '; } - //处理字段和注释(可能包含隐藏邮箱) + // 处理字段和注释(优化:显式强制转换,避免非字符串拼接) if ($element instanceof \PhpOffice\PhpWord\Element\Field) { - $text .= $element->getContent() . ' '; + $text .= (string)$element->getContent() . ' '; } if ($element instanceof \PhpOffice\PhpWord\Element\Note) { - $text .= $element->getContent() . ' '; + $text .= (string)$element->getContent() . ' '; } - //清理所有不可见字符(关键:移除格式干扰) - $text = preg_replace('/[\x00-\x1F\x7F-\x9F]/', ' ', $text); // 移除控制字符 - $text = str_replace(["\t", "\r", "\n"], ' ', $text); // 统一空白字符 - $text = preg_replace('/\s+/', ' ', $text); // 合并多个空格 - if(!empty($text) && !mb_check_encoding($text, 'UTF-8')){ - $text = mb_convert_encoding($text, 'UTF-8', 'GBK'); - } - return $text; + + return $this->cleanText($text); } + /** + * 统一文本清理方法(稳健、高效、不破坏普通单引号) + * @param string $text 待清理文本 + * @return string 清理后的纯文本 + */ + private function cleanText(string $text){ + + //编码正确 + if (!mb_check_encoding($text, 'UTF-8')) { + $text = mb_convert_encoding( + $text, + 'UTF-8', + 'GBK,GB2312,GB18030,Big5,ISO-8859-1,CP1252,UTF-16,UTF-32' // 补充常见西文编码,兼容更多场景 + ); + } + //移除不可见控制字符 + $text = preg_replace('/[\x00-\x1F\x7F-\x9F]/u', ' ', $text); + + //统一空白字符 + $text = str_replace([ + "\t", "\r", "\n", + chr(0xC2) . chr(0xA0), // 不间断空格( ) + ' ', // 全角空格(U+3000) + chr(0xE2) . chr(0x80) . chr(0xAF), // 窄无中断空格(U+202F) + ], ' ', $text); + + //合并连续空格 + $text = preg_replace('/\s+/u', ' ', $text); + + return $text; + } + // private function getTextFromElement($element, $lineNumber = 0){ + // // 初始化默认空字符串(保持原有逻辑) + // $text = ''; + + // // 1. 常量化特殊引号映射(避免重复创建数组,提升性能) + // static $specialQuotesMap = [ + // '’' => "'", // 右单引号(U+2019)→ 普通单引号(U+0027) + // '‘' => "'", // 左单引号(U+2018)→ 普通单引号(U+0027) + // '“' => '"', // 左双引号(U+201C)→ 普通双引号(U+0022) + // '”' => '"', // 右双引号(U+201D)→ 普通双引号(U+0022) + // '„' => '"', // 下双引号(U+201E)→ 普通双引号(兼容欧洲排版) + // '‟' => '"', // 右双引号(U+201F)→ 普通双引号(兼容少见排版) + // ]; + + // // 2. 提前校验元素合法性(避免后续 instanceof 无效判断,减少报错) + // if (!is_object($element) || !$element instanceof \PhpOffice\PhpWord\Element\AbstractElement) { + // return $text; + // } + + // // 支持H1标题格式(逻辑不变,优化变量命名可读性) + // if ($element instanceof \PhpOffice\PhpWord\Element\Title) { + // $titleContent = $element->getText(); + // $titleText = ''; + + // // 关键修复:判断返回类型,递归提取文本(逻辑不变) + // if ($titleContent instanceof \PhpOffice\PhpWord\Element\TextRun) { + // $titleText = $this->getTextFromElement($titleContent); + // } else { + // $titleText = strtr((string)$titleContent, $specialQuotesMap); + // } + + // $text .= $titleText . ' '; + // return $text; + // } + + // // 项目编号(逻辑不变,优化空值判断为严格判断) + // if ($element instanceof \PhpOffice\PhpWord\Element\ListItemRun) { + // $this->iNum = isset($this->iNum) && is_numeric($this->iNum) ? $this->iNum : 0; + // $this->iNum++; + // $text .= $this->iNum . ' '; + // } + + // // 处理PreserveText元素(核心逻辑不变,增强容错性) + // if ($element instanceof \PhpOffice\PhpWord\Element\PreserveText) { + // try { + // $reflection = new \ReflectionClass($element); + // $property = $reflection->getProperty('text'); + // $property->setAccessible(true); + // // 空值兜底,避免遍历非数组报错 + // $textParts = $property->getValue($element) ?? []; + // } catch (\ReflectionException $e) { + // // 反射失败时返回已拼接文本,不中断流程 + // return $text; + // } + + // foreach ($textParts as $part) { + // $part = (string)$part; // 强制转字符串,避免类型错误 + // if (strpos($part, 'HYPERLINK') !== false) { + // $decoded = html_entity_decode($part, ENT_QUOTES | ENT_HTML5); + // // 邮箱正则不变,保持原有匹配逻辑 + // if (preg_match('/mailto:([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,10})/i', $decoded, $match)) { + // $text .= $match[1] . ' '; + // } + // } else { + // $text .= $part; + // } + // } + // return $text; + // } + + // // 处理表格和单元格(逻辑不变,优化循环变量命名) + // if ($element instanceof \PhpOffice\PhpWord\Element\Table) { + // foreach ($element->getRows() as $row) { + // foreach ($row->getCells() as $cell) { + // $text .= $this->getTextFromElement($cell); + // } + // } + // return $text; + // } + + // if ($element instanceof \PhpOffice\PhpWord\Element\Cell) { + // foreach ($element->getElements() as $child) { + // $text .= $this->getTextFromElement($child); + // } + // return $text; + // } + + // // 处理嵌套元素(逻辑不变,增强方法存在性校验) + // if (method_exists($element, 'getElements') && is_callable([$element, 'getElements'])) { + // foreach ($element->getElements() as $child) { + // // 双重校验,避免非元素对象传入 + // if (is_object($child) && $child instanceof \PhpOffice\PhpWord\Element\AbstractElement) { + // $textPart = $this->getTextFromElement($child); + // $text .= $textPart; + // } + // } + // } + + // // 处理文本元素(逻辑不变,保持特殊引号替换) + // if ($element instanceof \PhpOffice\PhpWord\Element\Text) { + // $textPart = (string)$element->getText(); // 强制转字符串,避免空值 + // $textPart = strtr($textPart, $specialQuotesMap); + // $text .= $textPart; + // } + + // // 处理超链接(逻辑不变,优化变量类型转换) + // if ($element instanceof \PhpOffice\PhpWord\Element\Link) { + // $target = (string)$element->getTarget(); + // if (strpos($target, 'mailto:') === 0) { + // $text .= rtrim(str_replace('mailto:', '', $target)) . ' '; + // } + // $linkText = strtr((string)$element->getText(), $specialQuotesMap); + // $text .= $linkText . ' '; + // } + + // // 处理字段和注释(逻辑不变,增加类型转换,避免非字符串拼接) + // if ($element instanceof \PhpOffice\PhpWord\Element\Field) { + // $text .= (string)$element->getContent() . ' '; + // } + // if ($element instanceof \PhpOffice\PhpWord\Element\Note) { + // $text .= (string)$element->getContent() . ' '; + // } + + // // 清理文本(逻辑不变,优化编码校验顺序,提升性能) + // $text = str_replace(["\t", "\r", "\n"], ' ', $text); + // $text = preg_replace('/\s+/', ' ', $text); + // // 先trim再判断,避免空白字符导致的无效编码转换 + // $textTrimmed = trim($text); + // if (!empty($textTrimmed) && !mb_check_encoding($textTrimmed, 'UTF-8')) { + // $text = mb_convert_encoding($text, 'UTF-8', 'GBK'); + // } + + // return $text; + // } /** * 从 Word 文档提取摘要和关键词 * @return array 提取结果 @@ -950,221 +1157,217 @@ class ArticleParserService * @param int $maxDepth 最大解析深度 * @return string */ - private function fullDecode($str, $maxDepth = 2) - { - // 空值/深度为0,直接返回(提前终止,避免无效操作) - if (empty($str) || $maxDepth <= 0) { - return $str; + private function fullDecode(?string $str, int $maxDepth = 2){ + // 空值/无效深度/纯空格,直接返回(严谨前置判断,避免无效运算) + if ($str === null || trim((string)$str) === '' || $maxDepth <= 0) { + return $str === null ? '' : trim((string)$str); } + + // 确保输入是字符串(兼容非字符串输入场景) + $str = (string)$str; + // 前置Unicode解码(避免转义字符干扰后续匹配) $str = $this->decodeUnicode($str); - // ========== 预编译所有正则(合并同类型,避免循环内重复解析) ========== + + // ========== 预编译正则(优化匹配精度、避免歧义,仅编译一次) ========== $regexps = [ - // 原有专属场景正则 - 'ob0' => '/0B\s*\?0/', - 'dl18' => '/DL\s*\?.18/', - // 原有通用场景正则 - 'qMarkNum' => '/\?(\d+)/', - 'qMarkDotNum' => '/\?(\.\d+)/', - // ≤、≠空格修复正则 - 'neNum' => '/≠\s*(\d+)/', - 'leNum' => '/≤\s*(\d+)/', - // 混合符号乱码正则(合并中英文顿号/逗号) - 'mixSymbol' => '/(\?)\s*(、|,)\s*(\?)\s*(、|,)\s*(\?)(\d+)/', - // ≤、≠专属标识正则(合并LE/NE) - 'leNeMark' => '/(LE|NE)\s*\?(\d+)/', - // Unicode转义正则 - 'unicode' => '/\\\\u([0-9a-fA-F]{4})/', - // Word二进制乱码(合并≤≥≠) - 'wordBin' => '/(\\xE2\\x89\\x86|\\xE2 0x89 0x86|e28986|\\xE2\\x89\\x87|\\xE2 0x89 0x87|e28987|\\xE2\\x89\\x80|\\xE2 0x89 0x80|e28980)/i', - // Word XML实体异常(合并≤≥≠) - 'wordEntity' => '/&#\s*(\x|X)?\s*(2264|2265|2260)\s*;?/i', - // 不可见控制字符 - 'controlChar' => '/[\x00-\x1F\x7F]/', - // 重复符号去重(合并≤≥≠) - 'repeatSymbol' => '/(≤{2,}|≥{2,}|≠{2,})/', - // GBK编码乱码(合并≤≥≠) - 'gbkSymbol' => '/(\xA1\xF2|\xA1\xF3|\xA1\xF0)/' + // 专属场景正则:优化空格匹配(任意空白字符)+ 问号转义(避免正则歧义) + 'ob0' => '/0B\s*\\?0/', // 匹配 0B?0、0B ?0 等场景 + 'dl18' => '/DL\s*\\?\.18/', // 精准匹配 DL?.18(避免误匹配 DL?x.18) + // 通用场景正则:问号转义,确保仅匹配字面问号 + 'qMarkNum' => '/\\?(\d+)/', // 匹配 ?123、?45 等(问号转义) + 'qMarkDotNum' => '/\\?(\.\d+)/', // 匹配 ?.18、?.25 等(问号转义) + // ≤、≠空格修复:支持任意空白字符(含全角空格) + 'neNum' => '/≠\s*(\d+)/u', + 'leNum' => '/≤\s*(\d+)/u', + // 混合符号乱码:用非捕获组减少开销,优化分组逻辑 + 'mixSymbol' => '/\\?\s*(?:、|,)\s*\\?\s*(?:、|,)\s*\\?(\d+)/u', + // ≤、≠专属标识:支持大小写不敏感(覆盖 LE/le/NE/ne) + 'leNeMark' => '/(LE|NE)\s*\\?(\d+)/i', + // Unicode转义:支持 \u/\U 前缀,覆盖更多转义格式 + 'unicode' => '/\\\\[uU]([0-9a-fA-F]{4})/', + // Word二进制乱码:优化正则结构(非捕获组),避免重复分组 + 'wordBin' => '/(?:\\xE2\\x89\\x86|\\xE2\s*0x89\s*0x86|e28986|\\xE2\\x89\\x87|\\xE2\s*0x89\s*0x87|e28987|\\xE2\\x89\\x80|\\xE2\s*0x89\s*0x80|e28980)/i', + // Word XML实体异常:优化匹配(支持无分号、空格间隔) + 'wordEntity' => '/&#\s*(?:x|X)?\s*(2264|2265|2260)\s*;?/i', + // 不可见控制字符:添加UTF-8修饰符,避免匹配多字节字符异常 + 'controlChar' => '/[\x00-\x1F\x7F]/u', + // 重复符号去重:用反向引用优化,匹配更高效(支持≤≥≠) + 'repeatSymbol' => '/(≤|≥|≠)\1+/u', + // GBK编码乱码:优化正则(无冗余分组),确保匹配原生字节 + 'gbkSymbol' => '/\xA1\xF2|\xA1\xF3|\xA1\xF0/' ]; - // ========== 预定义所有替换映射(避免循环内重复创建) ========== + // ========== 预定义替换映射(扩展场景、去冗余、修复转义问题) ========== $maps = [ - // HTML实体映射(扩展Word实体) + // HTML实体映射:补充更多Word常见实体,覆盖不完整实体场景 'htmlEntity' => [ - '≤' => '≤', '≤' => '≤', '≤' => '≤', - '≥' => '≥', '≥' => '≥', '≥' => '≥', - '≠' => '≠', '≠' => '≠', '≠' => '≠', - '&le' => '≤', '&ge' => '≥', '&ne' => '≠', - 'ࣘ' => '≤', 'ࣙ' => '≥', 'ࣔ' => '≠', - '≤' => '≤', '≥' => '≥', '≠' => '≠', - '≤' => '≤', '≥' => '≥', '≠' => '≠', - '<' => '≤', '>' => '≥' + '≤' => '≤', '≤' => '≤', '≤' => '≤', '≤' => '≤', + '≥' => '≥', '≥' => '≥', '≥' => '≥', '≥' => '≥', + '≠' => '≠', '≠' => '≠', '≠' => '≠', '≠' => '≠', + '&le' => '≤', '&ge' => '≥', '&ne' => '≠', // 无分号实体 + 'ࣘ' => '≤', 'ࣙ' => '≥', 'ࣔ' => '≠', // 无分号数字实体 + '≤' => '≤', '≥' => '≥', '≠' => '≠', // 无分号十六进制实体 + '<' => '≤', '>' => '≥', // 业务专属映射(保留) ], - // 空格替换数组(扩展Word中的各种空格) + // 空格替换数组:补充Word中常见的特殊空格,覆盖更多场景 'nbsp' => [ - chr(0xC2) . chr(0xA0), // UTF-8不间断空格 - chr(0xA0), // 拉丁1不间断空格 - ' ', // 全角空格 - chr(0x2002), // 方头空格 - chr(0x2003), // 全角空格 - chr(0x2004) // 三分之一全角空格 + chr(0xC2) . chr(0xA0), // UTF-8不间断空格( ) + chr(0xA0), // 拉丁1不间断空格 + ' ', // 全角空格(U+3000) + chr(0x2002), // 半角空格(U+2002) + chr(0x2003), // 全角空格(U+2003) + chr(0x2004), // 三分之一全角空格(U+2004) + chr(0x2005), // 四分之一全角空格(U+2005) + chr(0x202F), // 窄无中断空格(U+202F,Word常用) ], - // 二进制乱码映射 + // 二进制乱码映射:统一键名格式(去除空格),避免重复匹配 'wordBin' => [ - 'e28986' => '≤', '\\xe2\\x89\\x86' => '≤', '\\xe2 0x89 0x86' => '≤', - 'e28987' => '≥', '\\xe2\\x89\\x87' => '≥', '\\xe2 0x89 0x87' => '≥', - 'e28980' => '≠', '\\xe2\\x89\\x80' => '≠', '\\xe2 0x89 0x80' => '≠' + 'e28986' => '≤', + '\xe2\x89\x86' => '≤', + '\xe20x890x86' => '≤', // 去除空格后的统一键名 + 'e28987' => '≥', + '\xe2\x89\x87' => '≥', + '\xe20x890x87' => '≥', + 'e28980' => '≠', + '\xe2\x89\x80' => '≠', + '\xe20x890x80' => '≠', ], - // XML实体编码映射 + // XML实体编码映射:保持简洁,仅映射核心数字 'wordEntity' => [ '2264' => '≤', '2265' => '≥', - '2260' => '≠' + '2260' => '≠', ], - // GBK编码映射 + // GBK编码映射:修复转义问题(用双引号包裹原生字节,避免匹配失败) 'gbkSymbol' => [ - '\xA1\xF2' => '≤', - '\xA1\xF3' => '≥', - '\xA1\xF0' => '≠' - ] + "\xA1\xF2" => '≤', // 原生GBK字节,无需转义(双引号关键) + "\xA1\xF3" => '≥', + "\xA1\xF0" => '≠', + ], ]; - // 预定义回调函数(仅创建一次,避免循环内重复实例化) + // 预定义回调函数(仅创建一次,提升性能,增加容错) $unicodeCallback = function ($m) { - return mb_chr(hexdec($m[1]), 'UTF-8') ?: $m[0]; + $code = hexdec($m[1]); + // 容错:十六进制转换失败/无效Unicode码点,返回原始值 + return ($code >= 0x20 && $code <= 0x10FFFF) ? mb_chr($code, 'UTF-8') : $m[0]; }; $depth = 0; $hasChange = false; - $original = $str; + $currentStr = $str; - // 循环解码:仅在有变化且未达最大深度时执行 + // 循环解码:仅在有变化且未达最大深度时执行(避免无限循环) do { $depth++; $hasChange = false; - $prevStr = $str; + $prevStr = $currentStr; - // ========== 前置处理(惰性执行,避免无意义操作) ========== - $countCtrl = 0; + // ========== 前置处理(惰性执行,仅在需要时触发) ========== // 1. 过滤不可见控制字符(仅当包含时执行) - if (preg_match($regexps['controlChar'], $str)) { - $str = preg_replace($regexps['controlChar'], '', $str, -1, $countCtrl); + if (preg_match($regexps['controlChar'], $currentStr)) { + $currentStr = preg_replace($regexps['controlChar'], '', $currentStr); } - // 2. GBK/GB2312编码转UTF-8(仅当非UTF-8时执行) - if (!mb_check_encoding($str, 'UTF-8')) { - $str = mb_convert_encoding($str, 'UTF-8', 'GBK,GB2312,ISO-8859-1'); + // 2. 编码校正(非UTF-8时才转换,增加容错机制) + if (!mb_check_encoding($currentStr, 'UTF-8')) { + $converted = mb_convert_encoding( + $currentStr, + 'UTF-8', + 'GBK,GB2312,ISO-8859-1,CP1252' // 补充CP1252(Windows西文编码) + ); + // 容错:转换失败时保留原文本,避免乱码加剧 + $currentStr = mb_check_encoding($converted, 'UTF-8') ? $converted : $currentStr; } - // ========== 核心解码逻辑 ========== - // 1. 解码Unicode转义 - $str = preg_replace_callback($regexps['unicode'], $unicodeCallback, $str); + // ========== 核心解码逻辑(按优先级执行,避免冲突) ========== + // 1. Unicode转义解码(优先处理,避免转义字符干扰后续匹配) + $currentStr = preg_replace_callback($regexps['unicode'], $unicodeCallback, $currentStr); - // 2. 解码HTML实体(高性能strtr替换) - $str = strtr($str, $maps['htmlEntity']); - $str = html_entity_decode($str, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8'); + // 2. HTML实体替换(先精准映射,再解码剩余实体) + $currentStr = strtr($currentStr, $maps['htmlEntity']); + $currentStr = html_entity_decode( + $currentStr, + ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, + 'UTF-8' + ); - // 3. 替换各种空格为普通空格 - $str = str_replace($maps['nbsp'], ' ', $str); + // 3. 统一所有空格为普通空格(避免空格类型导致的匹配失败) + $currentStr = str_replace($maps['nbsp'], ' ', $currentStr); - // ========== Word特殊符号乱码修复(合并+惰性) ========== - $countBin = $countEnt = $countGbk = $countRepeat = 0; - - // 1. 二进制乱码还原(合并正则+回调) - if (preg_match($regexps['wordBin'], $str)) { - $str = preg_replace_callback($regexps['wordBin'], function ($m) use ($maps) { - $key = strtolower(str_replace(' ', '', $m[0])); - return $maps['wordBin'][$key] ?? $m[0]; - }, $str, -1, $countBin); + // ========== Word特殊符号乱码修复(惰性执行,优化效率) ========== + // 1. 二进制乱码还原(先去除空格统一格式,再匹配) + if (preg_match($regexps['wordBin'], $currentStr)) { + $tempStr = str_replace(' ', '', $currentStr); // 去除所有空格,统一键名格式 + $currentStr = str_ireplace(array_keys($maps['wordBin']), $maps['wordBin'], $tempStr); } - // 2. XML实体异常修复(合并正则+回调) - if (preg_match($regexps['wordEntity'], $str)) { - $str = preg_replace_callback($regexps['wordEntity'], function ($m) use ($maps) { - return $maps['wordEntity'][$m[2]] ?? $m[0]; - }, $str, -1, $countEnt); + // 2. XML实体异常修复 + if (preg_match($regexps['wordEntity'], $currentStr)) { + $currentStr = preg_replace_callback($regexps['wordEntity'], function ($m) use ($maps) { + return $maps['wordEntity'][$m[1]] ?? $m[0]; + }, $currentStr); } - // 3. GBK编码乱码修复(合并正则+回调) - if (preg_match($regexps['gbkSymbol'], $str)) { - $str = preg_replace_callback($regexps['gbkSymbol'], function ($m) use ($maps) { - return $maps['gbkSymbol'][$m[0]] ?? $m[0]; - }, $str, -1, $countGbk); + // 3. GBK编码乱码修复(用strtr替代preg_replace_callback,效率更高) + if (preg_match($regexps['gbkSymbol'], $currentStr)) { + $currentStr = strtr($currentStr, $maps['gbkSymbol']); } - // 4. 重复符号去重(合并正则+极简回调) - if (preg_match($regexps['repeatSymbol'], $str)) { - $str = preg_replace_callback($regexps['repeatSymbol'], function ($m) { - return $m[0][0]; // 取第一个字符实现去重 - }, $str, -1, $countRepeat); + // 4. 重复符号去重(用preg_replace简化,无需回调) + if (preg_match($regexps['repeatSymbol'], $currentStr)) { + $currentStr = preg_replace($regexps['repeatSymbol'], '$1', $currentStr); } - // ========== 原有核心替换逻辑(合并+惰性) ========== - $count1 = $count2 = $count3 = $count4 = $count5 = $count6 = 0; - $count7 = $count8 = $count9 = 0; - - // 1. 专属场景替换(惰性执行) - if (strpos($str, '0B?0') !== false) { - $str = preg_replace($regexps['ob0'], '0B≥30', $str, -1, $count1); + // ========== 业务场景专属替换(惰性执行,精准匹配) ========== + // 1. 专属场景替换(0B?0 → 0B≥30,DL?.18 → DL≥0.18) + if (strpos($currentStr, '0B') !== false) { + $currentStr = preg_replace($regexps['ob0'], '0B≥30', $currentStr); } - if (strpos($str, 'DL?.18') !== false) { - $str = preg_replace($regexps['dl18'], 'DL≥0.18', $str, -1, $count2); + if (strpos($currentStr, 'DL') !== false) { + $currentStr = preg_replace($regexps['dl18'], 'DL≥0.18', $currentStr); } - // 2. ≤、≠空格修复(惰性执行) - if (preg_match($regexps['neNum'], $str)) { - $str = preg_replace($regexps['neNum'], '≠$1', $str, -1, $count3); + // 2. ≤、≠空格修复(去除符号与数字间的空格) + if (preg_match($regexps['neNum'], $currentStr)) { + $currentStr = preg_replace($regexps['neNum'], '≠$1', $currentStr); } - if (preg_match($regexps['leNum'], $str)) { - $str = preg_replace($regexps['leNum'], '≤$1', $str, -1, $count4); + if (preg_match($regexps['leNum'], $currentStr)) { + $currentStr = preg_replace($regexps['leNum'], '≤$1', $currentStr); } - // 3. 通用场景替换(惰性执行) - if (preg_match($regexps['qMarkNum'], $str)) { - $str = preg_replace($regexps['qMarkNum'], '≥$1', $str, -1, $count5); + // 3. 通用场景替换(问号 → ≥) + if (preg_match($regexps['qMarkNum'], $currentStr)) { + $currentStr = preg_replace($regexps['qMarkNum'], '≥$1', $currentStr); } - if (preg_match($regexps['qMarkDotNum'], $str)) { - $str = preg_replace($regexps['qMarkDotNum'], '≥0$1', $str, -1, $count6); + if (preg_match($regexps['qMarkDotNum'], $currentStr)) { + $currentStr = preg_replace($regexps['qMarkDotNum'], '≥0$1', $currentStr); } - // 4. 混合符号乱码还原(合并中英文,惰性执行) - if (preg_match($regexps['mixSymbol'], $str)) { - $str = preg_replace($regexps['mixSymbol'], '≤$2≥$4≠$6', $str, -1, $count7); + // 4. 混合符号乱码还原(?、,?、,?123 → ≤≥≠123) + if (preg_match($regexps['mixSymbol'], $currentStr)) { + $currentStr = preg_replace($regexps['mixSymbol'], '≤≥≠$1', $currentStr); } - // 5. ≤、≠专属标识还原(合并正则,惰性执行) - if (preg_match($regexps['leNeMark'], $str)) { - $str = preg_replace_callback($regexps['leNeMark'], function ($m) { - return $m[1] === 'LE' ? '≤' . $m[2] : '≠' . $m[2]; - }, $str, -1, $count8); + // 5. ≤、≠专属标识还原(LE?123 → ≤123,NE?456 → ≠456) + if (preg_match($regexps['leNeMark'], $currentStr)) { + $currentStr = preg_replace_callback($regexps['leNeMark'], function ($m) { + return strtoupper($m[1]) === 'LE' ? '≤' . $m[2] : '≠' . $m[2]; + }, $currentStr); } - // 6. 修复前缀"d with "乱码(惰性执行) - if (strpos($str, 'd with ') !== false) { - $str = str_replace('d with ', 'd with ', $str, $count9); - } + // 6. 移除冗余代码(原代码"d with "替换无意义,直接删除) - // ========== 变化判断(合并计数,减少运算) ========== - $totalCount = $countCtrl + $countBin + $countEnt + $countGbk + $countRepeat + - $count1 + $count2 + $count3 + $count4 + $count5 + $count6 + - $count7 + $count8 + $count9; + // ========== 变化判断(简化逻辑,避免无效计数) ========== + $hasChange = ($currentStr !== $prevStr); - if ($totalCount > 0 || $str !== $prevStr) { - $hasChange = true; - $original = $str; - } + } while ($depth < $maxDepth && $hasChange); - // 提前终止:无变化则退出循环 - if (!$hasChange) { - break; - } + // 最终清理(去除首尾冒号+二次实体替换,确保无遗漏) + $currentStr = trim($currentStr, ':'); + $currentStr = strtr($currentStr, $maps['htmlEntity']); - } while ($depth < $maxDepth); - - // 最终清理+兜底替换 - $str = trim($str, ':'); - $str = strtr($str, $maps['htmlEntity']); - - return $str; + return $currentStr; } // private function fullDecode($str, $maxDepth = 5) { From f15d072b2ec59bcdb6940eff61799c1fb363db55 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 2 Dec 2025 14:26:53 +0800 Subject: [PATCH 20/53] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ArticleParserService.php | 341 ++++++++------------ 1 file changed, 138 insertions(+), 203 deletions(-) diff --git a/application/common/ArticleParserService.php b/application/common/ArticleParserService.php index ff715a30..5b0dd525 100644 --- a/application/common/ArticleParserService.php +++ b/application/common/ArticleParserService.php @@ -1152,222 +1152,157 @@ class ArticleParserService ]; } /** - * 核心解码方法(无静态缓存,高性能版) + * 核心解码方法 * @param string $str 待解码字符串 * @param int $maxDepth 最大解析深度 * @return string */ - private function fullDecode(?string $str, int $maxDepth = 2){ - // 空值/无效深度/纯空格,直接返回(严谨前置判断,避免无效运算) - if ($str === null || trim((string)$str) === '' || $maxDepth <= 0) { - return $str === null ? '' : trim((string)$str); - } - - // 确保输入是字符串(兼容非字符串输入场景) - $str = (string)$str; - // 前置Unicode解码(避免转义字符干扰后续匹配) - $str = $this->decodeUnicode($str); - - // ========== 预编译正则(优化匹配精度、避免歧义,仅编译一次) ========== - $regexps = [ - // 专属场景正则:优化空格匹配(任意空白字符)+ 问号转义(避免正则歧义) - 'ob0' => '/0B\s*\\?0/', // 匹配 0B?0、0B ?0 等场景 - 'dl18' => '/DL\s*\\?\.18/', // 精准匹配 DL?.18(避免误匹配 DL?x.18) - // 通用场景正则:问号转义,确保仅匹配字面问号 - 'qMarkNum' => '/\\?(\d+)/', // 匹配 ?123、?45 等(问号转义) - 'qMarkDotNum' => '/\\?(\.\d+)/', // 匹配 ?.18、?.25 等(问号转义) - // ≤、≠空格修复:支持任意空白字符(含全角空格) - 'neNum' => '/≠\s*(\d+)/u', - 'leNum' => '/≤\s*(\d+)/u', - // 混合符号乱码:用非捕获组减少开销,优化分组逻辑 - 'mixSymbol' => '/\\?\s*(?:、|,)\s*\\?\s*(?:、|,)\s*\\?(\d+)/u', - // ≤、≠专属标识:支持大小写不敏感(覆盖 LE/le/NE/ne) - 'leNeMark' => '/(LE|NE)\s*\\?(\d+)/i', - // Unicode转义:支持 \u/\U 前缀,覆盖更多转义格式 - 'unicode' => '/\\\\[uU]([0-9a-fA-F]{4})/', - // Word二进制乱码:优化正则结构(非捕获组),避免重复分组 - 'wordBin' => '/(?:\\xE2\\x89\\x86|\\xE2\s*0x89\s*0x86|e28986|\\xE2\\x89\\x87|\\xE2\s*0x89\s*0x87|e28987|\\xE2\\x89\\x80|\\xE2\s*0x89\s*0x80|e28980)/i', - // Word XML实体异常:优化匹配(支持无分号、空格间隔) - 'wordEntity' => '/&#\s*(?:x|X)?\s*(2264|2265|2260)\s*;?/i', - // 不可见控制字符:添加UTF-8修饰符,避免匹配多字节字符异常 - 'controlChar' => '/[\x00-\x1F\x7F]/u', - // 重复符号去重:用反向引用优化,匹配更高效(支持≤≥≠) - 'repeatSymbol' => '/(≤|≥|≠)\1+/u', - // GBK编码乱码:优化正则(无冗余分组),确保匹配原生字节 - 'gbkSymbol' => '/\xA1\xF2|\xA1\xF3|\xA1\xF0/' - ]; - - // ========== 预定义替换映射(扩展场景、去冗余、修复转义问题) ========== - $maps = [ - // HTML实体映射:补充更多Word常见实体,覆盖不完整实体场景 - 'htmlEntity' => [ - '≤' => '≤', '≤' => '≤', '≤' => '≤', '≤' => '≤', - '≥' => '≥', '≥' => '≥', '≥' => '≥', '≥' => '≥', - '≠' => '≠', '≠' => '≠', '≠' => '≠', '≠' => '≠', - '&le' => '≤', '&ge' => '≥', '&ne' => '≠', // 无分号实体 - 'ࣘ' => '≤', 'ࣙ' => '≥', 'ࣔ' => '≠', // 无分号数字实体 - '≤' => '≤', '≥' => '≥', '≠' => '≠', // 无分号十六进制实体 - '<' => '≤', '>' => '≥', // 业务专属映射(保留) - ], - // 空格替换数组:补充Word中常见的特殊空格,覆盖更多场景 - 'nbsp' => [ - chr(0xC2) . chr(0xA0), // UTF-8不间断空格( ) - chr(0xA0), // 拉丁1不间断空格 - ' ', // 全角空格(U+3000) - chr(0x2002), // 半角空格(U+2002) - chr(0x2003), // 全角空格(U+2003) - chr(0x2004), // 三分之一全角空格(U+2004) - chr(0x2005), // 四分之一全角空格(U+2005) - chr(0x202F), // 窄无中断空格(U+202F,Word常用) - ], - // 二进制乱码映射:统一键名格式(去除空格),避免重复匹配 - 'wordBin' => [ - 'e28986' => '≤', - '\xe2\x89\x86' => '≤', - '\xe20x890x86' => '≤', // 去除空格后的统一键名 - 'e28987' => '≥', - '\xe2\x89\x87' => '≥', - '\xe20x890x87' => '≥', - 'e28980' => '≠', - '\xe2\x89\x80' => '≠', - '\xe20x890x80' => '≠', - ], - // XML实体编码映射:保持简洁,仅映射核心数字 - 'wordEntity' => [ - '2264' => '≤', - '2265' => '≥', - '2260' => '≠', - ], - // GBK编码映射:修复转义问题(用双引号包裹原生字节,避免匹配失败) - 'gbkSymbol' => [ - "\xA1\xF2" => '≤', // 原生GBK字节,无需转义(双引号关键) - "\xA1\xF3" => '≥', - "\xA1\xF0" => '≠', - ], - ]; - - // 预定义回调函数(仅创建一次,提升性能,增加容错) - $unicodeCallback = function ($m) { - $code = hexdec($m[1]); - // 容错:十六进制转换失败/无效Unicode码点,返回原始值 - return ($code >= 0x20 && $code <= 0x10FFFF) ? mb_chr($code, 'UTF-8') : $m[0]; - }; - - $depth = 0; - $hasChange = false; - $currentStr = $str; - - // 循环解码:仅在有变化且未达最大深度时执行(避免无限循环) - do { - $depth++; - $hasChange = false; - $prevStr = $currentStr; - - // ========== 前置处理(惰性执行,仅在需要时触发) ========== - // 1. 过滤不可见控制字符(仅当包含时执行) - if (preg_match($regexps['controlChar'], $currentStr)) { - $currentStr = preg_replace($regexps['controlChar'], '', $currentStr); + private function fullDecode($str = '', int $maxDepth = 2){ + try { + if ($str === null || trim((string)$str) === '' || $maxDepth <= 0) { + return $str === null ? '' : trim((string)$str); } - // 2. 编码校正(非UTF-8时才转换,增加容错机制) - if (!mb_check_encoding($currentStr, 'UTF-8')) { - $converted = mb_convert_encoding( - $currentStr, - 'UTF-8', - 'GBK,GB2312,ISO-8859-1,CP1252' // 补充CP1252(Windows西文编码) + $str = (string)$str; + + // Unicode解码 + if (method_exists($this, 'decodeUnicode')) { + $str = $this->decodeUnicode($str); + } else { + $str = preg_replace_callback( + '/\\\\[uU]([0-9a-fA-F]{4})/', + function ($m) { + $code = hexdec($m[1]); + return ($code >= 0x20 && $code <= 0x10FFFF) ? mb_chr($code, 'UTF-8') : $m[0]; + }, + $str ); - // 容错:转换失败时保留原文本,避免乱码加剧 - $currentStr = mb_check_encoding($converted, 'UTF-8') ? $converted : $currentStr; } - // ========== 核心解码逻辑(按优先级执行,避免冲突) ========== - // 1. Unicode转义解码(优先处理,避免转义字符干扰后续匹配) - $currentStr = preg_replace_callback($regexps['unicode'], $unicodeCallback, $currentStr); + // 预编译正则 + $regexps = [ + 'ob0' => '/0B\s*\\?0/', + 'dl18' => '/DL\s*\\?\.18/', + 'qMarkNum' => '/\\?(\d+)/', + 'qMarkDotNum' => '/\\?(\.\d+)/', + 'neNum' => '/≠\s*(\d+)/u', + 'leNum' => '/≤\s*(\d+)/u', + 'mixSymbol' => '/\\?\s*(?:、|,)\s*\\?\s*(?:、|,)\s*\\?(\d+)/u', + 'leNeMark' => '/(LE|NE)\s*\\?(\d+)/i', + 'unicode' => '/\\\\[uU]([0-9a-fA-F]{4})/', + 'wordBin' => '/(?:\\xE2\\x89\\x86|\\xE2\s*0x89\s*0x86|e28986|\\xE2\\x89\\x87|\\xE2\s*0x89\s*0x87|e28987|\\xE2\\x89\\x80|\\xE2\s*0x89\s*0x80|e28980)/i', + 'wordEntity' => '/&#\s*(?:x|X)?\s*(2264|2265|2260)\s*;?/i', + 'repeatSymbol' => '/(≤|≥|≠)\1+/u', + 'gbkSymbol' => '/\xA1\xF2|\xA1\xF3|\xA1\xF0/' + ]; - // 2. HTML实体替换(先精准映射,再解码剩余实体) + // 预定义替换映射 + $maps = [ + 'htmlEntity' => [ + '≤' => '≤', '≤' => '≤', '≤' => '≤', '≤' => '≤', + '≥' => '≥', '≥' => '≥', '≥' => '≥', '≥' => '≥', + '≠' => '≠', '≠' => '≠', '≠' => '≠', '≠' => '≠', + '&le' => '≤', '&ge' => '≥', '&ne' => '≠', + 'ࣘ' => '≤', 'ࣙ' => '≥', 'ࣔ' => '≠', + '≤' => '≤', '≥' => '≥', '≠' => '≠', + '<' => '≤', '>' => '≥', + ], + 'wordBin' => [ + "\xE2\x89\x86" => '≤', "\xE2\x89\x87" => '≥', "\xE2\x89\x80" => '≠', + "\xe2\x89\x86" => '≤', "\xe2\x89\x87" => '≥', "\xe2\x89\x80" => '≠', + 'e28986' => '≤', '\xe2\x89\x86' => '≤', '\xe20x890x86' => '≤', + 'e28987' => '≥', '\xe2\x89\x87' => '≥', '\xe20x890x87' => '≥', + 'e28980' => '≠', '\xe2\x89\x80' => '≠', '\xe20x890x80' => '≠', + ], + 'wordEntity' => ['2264' => '≤', '2265' => '≥', '2260' => '≠'], + 'gbkSymbol' => ["\xA1\xF2" => '≤', "\xA1\xF3" => '≥', "\xA1\xF0" => '≠'], + ]; + + $unicodeCallback = function ($m) { + $code = hexdec($m[1]); + return ($code >= 0x20 && $code <= 0x10FFFF) ? mb_chr($code, 'UTF-8') : $m[0]; + }; + + $depth = 0; + $hasChange = false; + $currentStr = $str; + + // 循环解码 + do { + $depth++; + $hasChange = false; + $prevStr = $currentStr; + + // Unicode转义解码 + $currentStr = preg_replace_callback($regexps['unicode'], $unicodeCallback, $currentStr); + + //HTML实体替换 + $currentStr = strtr($currentStr, $maps['htmlEntity']); + $currentStr = html_entity_decode( + $currentStr, + ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, + 'UTF-8' + ); + + // Word特殊符号乱码修复 + if (preg_match($regexps['wordBin'], $currentStr)) { + $tempStr = str_replace(' ', '', $currentStr); + $currentStr = str_ireplace(array_keys($maps['wordBin']), $maps['wordBin'], $tempStr); + } + if (preg_match($regexps['wordEntity'], $currentStr)) { + $currentStr = preg_replace_callback( + $regexps['wordEntity'], + function ($m) use ($maps) { + return $maps['wordEntity'][$m[1]] ?? $m[0]; + }, + $currentStr + ); + } + if (preg_match($regexps['gbkSymbol'], $currentStr)) { + $currentStr = strtr($currentStr, $maps['gbkSymbol']); + } + if (preg_match($regexps['repeatSymbol'], $currentStr)) { + $currentStr = preg_replace($regexps['repeatSymbol'], '$1', $currentStr); + } + + //业务场景专属替换 + if (preg_match($regexps['neNum'], $currentStr)) { + $currentStr = preg_replace($regexps['neNum'], '≠$1', $currentStr); + } + if (preg_match($regexps['leNum'], $currentStr)) { + $currentStr = preg_replace($regexps['leNum'], '≤$1', $currentStr); + } + if (preg_match($regexps['qMarkNum'], $currentStr)) { + $currentStr = preg_replace($regexps['qMarkNum'], '≥$1', $currentStr); + } + if (preg_match($regexps['qMarkDotNum'], $currentStr)) { + $currentStr = preg_replace($regexps['qMarkDotNum'], '≥0$1', $currentStr); + } + if (preg_match($regexps['mixSymbol'], $currentStr)) { + $currentStr = preg_replace($regexps['mixSymbol'], '≤≥≠$1', $currentStr); + } + if (preg_match($regexps['leNeMark'], $currentStr)) { + $currentStr = preg_replace_callback( + $regexps['leNeMark'], + function ($m) { + return strtoupper($m[1]) === 'LE' ? '≤' . $m[2] : '≠' . $m[2]; + }, + $currentStr + ); + } + + $hasChange = ($currentStr !== $prevStr); + } while ($depth < $maxDepth && $hasChange); + + // 最终清理 + $currentStr = trim($currentStr, ':'); $currentStr = strtr($currentStr, $maps['htmlEntity']); - $currentStr = html_entity_decode( - $currentStr, - ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, - 'UTF-8' - ); - // 3. 统一所有空格为普通空格(避免空格类型导致的匹配失败) - $currentStr = str_replace($maps['nbsp'], ' ', $currentStr); + return $currentStr; - // ========== Word特殊符号乱码修复(惰性执行,优化效率) ========== - // 1. 二进制乱码还原(先去除空格统一格式,再匹配) - if (preg_match($regexps['wordBin'], $currentStr)) { - $tempStr = str_replace(' ', '', $currentStr); // 去除所有空格,统一键名格式 - $currentStr = str_ireplace(array_keys($maps['wordBin']), $maps['wordBin'], $tempStr); - } - - // 2. XML实体异常修复 - if (preg_match($regexps['wordEntity'], $currentStr)) { - $currentStr = preg_replace_callback($regexps['wordEntity'], function ($m) use ($maps) { - return $maps['wordEntity'][$m[1]] ?? $m[0]; - }, $currentStr); - } - - // 3. GBK编码乱码修复(用strtr替代preg_replace_callback,效率更高) - if (preg_match($regexps['gbkSymbol'], $currentStr)) { - $currentStr = strtr($currentStr, $maps['gbkSymbol']); - } - - // 4. 重复符号去重(用preg_replace简化,无需回调) - if (preg_match($regexps['repeatSymbol'], $currentStr)) { - $currentStr = preg_replace($regexps['repeatSymbol'], '$1', $currentStr); - } - - // ========== 业务场景专属替换(惰性执行,精准匹配) ========== - // 1. 专属场景替换(0B?0 → 0B≥30,DL?.18 → DL≥0.18) - if (strpos($currentStr, '0B') !== false) { - $currentStr = preg_replace($regexps['ob0'], '0B≥30', $currentStr); - } - if (strpos($currentStr, 'DL') !== false) { - $currentStr = preg_replace($regexps['dl18'], 'DL≥0.18', $currentStr); - } - - // 2. ≤、≠空格修复(去除符号与数字间的空格) - if (preg_match($regexps['neNum'], $currentStr)) { - $currentStr = preg_replace($regexps['neNum'], '≠$1', $currentStr); - } - if (preg_match($regexps['leNum'], $currentStr)) { - $currentStr = preg_replace($regexps['leNum'], '≤$1', $currentStr); - } - - // 3. 通用场景替换(问号 → ≥) - if (preg_match($regexps['qMarkNum'], $currentStr)) { - $currentStr = preg_replace($regexps['qMarkNum'], '≥$1', $currentStr); - } - if (preg_match($regexps['qMarkDotNum'], $currentStr)) { - $currentStr = preg_replace($regexps['qMarkDotNum'], '≥0$1', $currentStr); - } - - // 4. 混合符号乱码还原(?、,?、,?123 → ≤≥≠123) - if (preg_match($regexps['mixSymbol'], $currentStr)) { - $currentStr = preg_replace($regexps['mixSymbol'], '≤≥≠$1', $currentStr); - } - - // 5. ≤、≠专属标识还原(LE?123 → ≤123,NE?456 → ≠456) - if (preg_match($regexps['leNeMark'], $currentStr)) { - $currentStr = preg_replace_callback($regexps['leNeMark'], function ($m) { - return strtoupper($m[1]) === 'LE' ? '≤' . $m[2] : '≠' . $m[2]; - }, $currentStr); - } - - // 6. 移除冗余代码(原代码"d with "替换无意义,直接删除) - - // ========== 变化判断(简化逻辑,避免无效计数) ========== - $hasChange = ($currentStr !== $prevStr); - - } while ($depth < $maxDepth && $hasChange); - - // 最终清理(去除首尾冒号+二次实体替换,确保无遗漏) - $currentStr = trim($currentStr, ':'); - $currentStr = strtr($currentStr, $maps['htmlEntity']); - - return $currentStr; + } catch (\Throwable $e) { + return trim((string)$str); + } } // private function fullDecode($str, $maxDepth = 5) { From 488a312006b03a9926c97424da9bc1320c1dc593 Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 3 Dec 2025 16:59:18 +0800 Subject: [PATCH 21/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 142 +++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index 651f5166..4451d83a 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -463,4 +463,146 @@ class Workbench extends Base } return json_encode(['status' => 1,'msg' => 'success','data' => $aData]); } + /** + * 获取审稿权限 + * @param art_rev_id 审稿记录ID + * @param + */ + public function getReviewerAuth(){ + + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + $aData = ['is_review_auth' => 2];//审稿权限1是2否 + //获取审稿记录ID + $iArtRevId = empty($aParam['art_rev_id']) ? 0 : $aParam['art_rev_id']; + if(empty($iArtRevId)){ + return json_encode(['status' => 2,'msg' => 'Please select a record','data' => $aData]); + } + //获取账号 + $sAccount = empty($aParam['account']) ? '' : $aParam['account']; + if(empty($sAccount)){ + return json_encode(['status' => 2,'msg' => 'Please enter your account','data' => $aData]); + } + //查询用户是否存在 + $aWhere = ['account' => $sAccount,'state' => 0]; + $aUser = Db::name('user')->field('user_id,account,email')->where($aWhere)->find(); + if(empty($aUser)){ + return json_encode(['status' => 3,'msg' => 'Account does not exist','data' => $aData]); + } + $iUserId = $aUser['user_id']; + + //查询审稿记录 + $aWhere = ['art_rev_id' => $iArtRevId]; + $aArticleReviewer = Db::name('article_reviewer')->where($aWhere)->find(); + if(empty($aArticleReviewer)){ + return json_encode(['status' => 4,'msg' => 'Review record does not exist','data' => $aData]); + } + $aData['review'] = $aArticleReviewer; + //获取文章信息 + $aWhere = ['article_id' => $aArticleReviewer['article_id']]; + $aArticle = Db::name('article')->field('article_id,abstrart,title article_title,type,accept_sn,journal_id,state')->where($aWhere)->find(); + if(empty($aArticle)){ + return json_encode(['status' => 5,'msg' => 'The article does not exist','data' => $aData]); + } + $aArticle['type_name'] = translateType($aArticle['type']);//文章类型 + //查询期刊信息 + $aWhere = ['journal_id' => $aArticle['article_id'],'state' => 0]; + $aJournal = Db::name('journal')->field('title as journal_name,website')->find(); + if(!empty($aJournal)){ + $aArticle += $aJournal; + } + //判断是否有权限审稿 + $aData['article'] = $aArticle; + if($aArticleReviewer['reviewer_id'] != $iUserId){ + return json_encode(['status' => 6,'msg' => 'No review permission','data' => $aData]); + } + //判断审稿权限 + if($aArticle['state'] != 2){ + return json_encode(['status' => 7,'msg' => 'The article has not entered the review status','data' => $aData]); + } + //审稿拒绝 + if($aArticleReviewer['state'] == 2){ + //获取审稿答卷 + $aWhere = ['art_rev_id' => $iArtRevId,'state' => 0]; + $aQuestion = Db::name('article_reviewer_question')->field('art_rev_id,recommend')->where($aWhere)->find(); + if(empty($aQuestion)){ + return json_encode(['status' => 8,'msg' => 'You have declined the review','data' => $aData]); + } + } + //审稿已过期 + if($aArticleReviewer['state'] == 4){ + return json_encode(['status' => 9,'msg' => 'The review has expired','data' => $aData]); + } + + //同意/1小改后接收/接收 + //判断是否为邮件 + $iIsCode = 2;//是否邮件进入 + $sAct = empty($aParam['act']) ? '' : $aParam['act']; + $aWhere = ['code' => $sAct,'state' => 0]; + if(!empty($sAct)){ + //查询绑定的用户ID + $aCode = Db::name('login_auto')->field('user_id')->where($aWhere)->find(); + if(empty($aCode)){ + return json_encode(['status' => 10,'msg' => 'Code is illegal','data' => $aData]); + } + if($aCode['user_id'] != $iUserId){ + return json_encode(['status' => 10,'msg' => 'Illegal code operation','data' => $aData]); + } + $iIsCode = 1; + } + //当前时间 + $iNowTime = time(); + // 14天 = 14*24*3600 秒 = 1209600 秒 + $iFourteenDays = 14 * 24 * 3600; + //审稿邀请 + if($aArticleReviewer['state'] == 5){ + if($iIsCode == 1){//邮件进入未同意审稿直接同意 + //添加时间 + $iTime = empty($aArticleReviewer['ctime']) ? 0 : $aArticleReviewer['ctime']; + if (!is_numeric($iTime) || (int)$iTime <= 0) { + return json_encode([ + 'status' => 11, + 'msg' => 'Invalid record time, the review period has expired', + 'data' => $aData + ]); + } + //判断是否超过14天 + $timeDiff = $iTime+$iFourteenDays; + // var_dump(date('Y-m-d H:i:s',$timeDiff),date('Y-m-d H:i:s',$iTime),date('Y-m-d H:i:s',$iNowTime)); + if($timeDiff < $iNowTime){ + return json_encode(['status' => 11,'msg' => 'The number of days for agreeing to review has exceeded 14','data' => $aData]); + } + //执行同意审稿 + $aWhere = ['art_rev_id' => $iArtRevId,'state' => 5]; + $result = Db::name('article_reviewer')->where($aWhere)->limit(1)->update(['state' => 0,'agree_review_time' => time()]); + } + if($iIsCode != 1){ + return json_encode(['status' => 12,'msg' => 'Reviewer did not agree to review','data' => $aData]); + } + } + //同意审稿 + if($aArticleReviewer['state'] == 0){ + //同意审稿的时间 + $iTime = empty($aArticleReviewer['agree_review_time']) ? 0 : $aArticleReviewer['agree_review_time']; + //添加时间 + $iCtime = empty($aArticleReviewer['ctime']) ? 0 : $aArticleReviewer['ctime']; + $iTime = empty($iTime) ? intval($iCtime) : intval($iTime); + if (!is_numeric($iTime) || (int)$iTime <= 0) { + return json_encode([ + 'status' => 11, + 'msg' => 'Invalid record time, the review period has expired', + 'data' => $aData + ]); + } + //判断是否超过14天 + $timeDiff = $iTime+$iFourteenDays; + if($timeDiff < $iNowTime){ + return json_encode(['status' => 11,'msg' => 'The number of days for agreeing to review has exceeded 14','data' => $aData]); + } + $aData['is_review_auth'] = 1; + return json_encode(['status' => 1,'msg' => 'success','data' => $aData]); + } + $aData['is_review_auth'] = 1; + return json_encode(['status' => 1,'msg' => 'success','data' => $aData]); + } } From bf8b4ecf74363921d410f8359fe1c3b277f32129 Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 3 Dec 2025 17:01:54 +0800 Subject: [PATCH 22/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/References.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/api/controller/References.php b/application/api/controller/References.php index 71038535..9124533d 100644 --- a/application/api/controller/References.php +++ b/application/api/controller/References.php @@ -188,6 +188,7 @@ class References extends Base if(!is_string($sContent)){ return json_encode(['status' => 2,'msg' => 'The content format is incorrect']); } + $sContent = str_replace(['?','?'], '.', $sContent); $aContent = explode('.', $sContent); $aUpdate = []; if(count($aContent) > 1){ @@ -218,6 +219,7 @@ class References extends Base $doiPattern = '/10\.\d{4,9}\/[^\s\/?#&=]+/i'; if (preg_match($doiPattern, $sDoi, $matches)) { $aUpdate['doi'] = $matches[0]; + $aUpdate['doilink'] = 'https://doi.org/'.''.$aUpdate['doi']; }else{ $aUpdate['doi'] = $sDoi; } From b1c23c959925dda23bc259d5ec7ac89db949b99b Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 4 Dec 2025 10:48:06 +0800 Subject: [PATCH 23/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index 4451d83a..7b5c7842 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -554,6 +554,8 @@ class Workbench extends Base $iNowTime = time(); // 14天 = 14*24*3600 秒 = 1209600 秒 $iFourteenDays = 14 * 24 * 3600; + //五天 + $iFiveDays = 5 * 24 * 3600; //审稿邀请 if($aArticleReviewer['state'] == 5){ if($iIsCode == 1){//邮件进入未同意审稿直接同意 @@ -566,12 +568,15 @@ class Workbench extends Base 'data' => $aData ]); } - //判断是否超过14天 - $timeDiff = $iTime+$iFourteenDays; - // var_dump(date('Y-m-d H:i:s',$timeDiff),date('Y-m-d H:i:s',$iTime),date('Y-m-d H:i:s',$iNowTime)); + //判断是否超过5天 + $timeDiff = $iTime+$iFiveDays; if($timeDiff < $iNowTime){ - return json_encode(['status' => 11,'msg' => 'The number of days for agreeing to review has exceeded 14','data' => $aData]); + //执行审稿过期 + $aWhere = ['art_rev_id' => $iArtRevId,'state' => 5]; + $result = Db::name('article_reviewer')->where($aWhere)->limit(1)->update(['state' => 4]); + return json_encode(['status' => 11,'msg' => 'The number of days for agreeing to review has exceeded 5','data' => $aData]); } + // var_dump(date('Y-m-d H:i:s',$timeDiff),date('Y-m-d H:i:s',$iTime),date('Y-m-d H:i:s',$iNowTime)); //执行同意审稿 $aWhere = ['art_rev_id' => $iArtRevId,'state' => 5]; $result = Db::name('article_reviewer')->where($aWhere)->limit(1)->update(['state' => 0,'agree_review_time' => time()]); From 402cb828419bb3727634294cf0c77335bef47814 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 4 Dec 2025 14:46:47 +0800 Subject: [PATCH 24/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Crontask.php | 202 +++++++++++++++++------- 1 file changed, 144 insertions(+), 58 deletions(-) diff --git a/application/api/controller/Crontask.php b/application/api/controller/Crontask.php index 4e09a0a9..6a6df239 100644 --- a/application/api/controller/Crontask.php +++ b/application/api/controller/Crontask.php @@ -2,9 +2,11 @@ namespace app\api\controller; use think\Controller; use think\Db; +use app\common\Reviewer; class Crontask extends Controller { + protected $iChunkSize = 500; /** * 批量处理审稿人审稿质量 * @return void @@ -144,7 +146,8 @@ class Crontask extends Controller //获取该文章审核人的信息 $aWhere = [ 'ctime'=>['>',$sDate], - 'state'=>['in',[1,2,3]] + // 'state'=>['in',[1,2,3]] + 'state' => ['BETWEEN',[1,3]] ]; $aReviewer = Db::name('article_reviewer')->field('reviewer_id,count(article_id) as review_num ')->where($aWhere)->order('reviewer_id asc')->group('reviewer_id')->select(); @@ -159,8 +162,10 @@ class Crontask extends Controller exit; } $aUser = empty($aUser) ? [] : array_column($aUser, null,'user_id'); + //分片处理数量 + $iChunkSize = 200; if(!empty($aReviewer)){ - $aChunk = array_chunk($aReviewer, 100); + $aChunk = array_chunk($aReviewer, $iChunkSize); Db::startTrans(); foreach ($aChunk as $key => $item) { //数据分片操作 //需要更新的用户ID @@ -174,8 +179,7 @@ class Crontask extends Controller //拼接更新语句 if(empty($aUser[$iUserId])){ //更新数量 - $aCase['review_num'] .= "WHEN {$iUserId} THEN "; - $aCase['review_num'] .= "'{$value['review_num']}' "; + $aCase['review_num'] .= "WHEN {$iUserId} THEN '{$value['review_num']}' "; $aUpdateId[] = $iUserId; continue; } @@ -185,8 +189,7 @@ class Crontask extends Controller continue; } //审核数量有,变化更新数量 - $aCase['review_num'] .= "WHEN {$iUserId} THEN "; - $aCase['review_num'] .= "'{$value['review_num']}' "; + $aCase['review_num'] .= "WHEN {$iUserId} THEN '{$value['review_num']}' "; $aUpdateId[] = $iUserId; unset($aUser[$iUserId]); } @@ -198,6 +201,7 @@ class Crontask extends Controller } $result = Db::name('user') ->where(['user_id' => ['in',$aUpdateId]]) + ->limit(count($aUpdateId)) ->update([ 'review_num' => Db::raw($aCase['review_num']), ]); @@ -210,14 +214,14 @@ class Crontask extends Controller Db::commit(); } if(!empty($aUser)){ - $aChunk = array_chunk($aUser, 100); + $aChunk = array_chunk($aUser, $iChunkSize); Db::startTrans(); foreach ($aChunk as $key => $item) { //数据分片操作 $aUserId = array_column($item, 'user_id'); if(empty($aUserId)){ continue; } - $result = Db::name('user')->where(['is_reviewer' => 1,'user_id' => ['in',$aUserId]]) + $result = Db::name('user')->where(['user_id' => ['in',$aUserId]]) ->limit(count($aUserId)) ->update([ 'review_num' => 0, @@ -235,11 +239,11 @@ class Crontask extends Controller } /** - * @title 审稿人拒绝审稿[超过七日默认自动拒绝审稿] + * @title 审稿人拒绝审稿[超过五日默认自动拒绝审稿] * */ public function refuseReviewArticle(){ - $sDate = strtotime(date('Y-m-d 00:00:00', strtotime('-7 day'))); + $sDate = strtotime(date('Y-m-d 00:00:00', strtotime('-5 day'))); //获取该文章审核人的信息 $aWhere = [ 'ctime'=>['<',$sDate], @@ -252,11 +256,14 @@ class Crontask extends Controller $this->showMessage('未查询到需要超过七日的稿件',2); exit; } - + //分片处理数量 + $iChunkSize = $this->iChunkSize; + //扣减分值 + $iScore = 3; + //更新审稿人未审稿的数量 + $aChunkReviewerNum = array_chunk($aReviewerNum, $iChunkSize); //更新超过七日未审核的数据 Db::startTrans(); - //更新审稿人未审稿的数量 - $aChunkReviewerNum = array_chunk($aReviewerNum, 500); foreach ($aChunkReviewerNum as $key => $value) { $aId = array_column($value, 'reviewer_id'); if(empty($aId)){ @@ -270,26 +277,38 @@ class Crontask extends Controller }else{ $this->showMessage('更新审稿人审稿状态成功['.$key.']执行SQL条数:'.$iResult."\n",1); } - $aCase = $aUpdateId = []; - $sRdNum = ''; + $aUpdateId = []; + $aCase = ['rd_num' => '','review_score' => '']; foreach ($value as $key => $item) { if($item['reviewer_id'] <=0){ continue; } //审核数量有,变化更新数量 - $sRdNum .= "WHEN {$item['reviewer_id']} THEN "; - $sRdNum .= Db::raw("rd_num + {$item['num']}")." "; + $aCase['rd_num'] .= "WHEN {$item['reviewer_id']} THEN "; + $aCase['rd_num'] .= Db::raw("rd_num + {$item['num']}")." "; + + //扣减分数 + $aCase['review_score'] .= "WHEN {$item['reviewer_id']} THEN "; + $aCase['review_score'] .= Db::raw("review_score - {$iScore}")." "; + $aUpdateId[] = $item['reviewer_id']; } - //SQL拼接最后结尾 - $aCase['rd_num'] ='CASE user_id '.$sRdNum.'END'; + //更新数据库 + foreach ($aCase as $kk => $value) { + if(empty($value)){ + continue; + } + $aUpdateCase[$kk] = Db::raw('CASE user_id '.$value.'END'); + } + if(empty($aUpdateCase) || empty($aId)){ + $this->showMessage('未查询到满足要求的审稿人数据['.$key.']'."\n",2); + continue; + } //执行更新 $result = Db::name('user') ->where(['user_id' => ['in',$aUpdateId]]) ->limit(count($aUpdateId)) - ->update([ - 'rd_num' => Db::raw($aCase['rd_num']), - ]); + ->update($aUpdateCase); if ($result === false) { $this->showMessage('更新用户拒绝审稿数量失败['.$key.']执行SQL:'.Db::getLastSql()."\n",2); }else{ @@ -333,8 +352,7 @@ class Crontask extends Controller continue; } //审核数量有,变化更新数量 - $sRdNum .= "WHEN {$item['reviewer_id']} THEN "; - $sRdNum .= "{$item['num']} "; + $sRdNum .= "WHEN {$item['reviewer_id']} THEN {$item['num']} "; $aUpdateId[] = $item['reviewer_id']; } //SQL拼接最后结尾 @@ -360,7 +378,7 @@ class Crontask extends Controller /** - * 批量处理审稿人活跃度[近两年] + * 批量处理审稿人活跃度/同意审稿数量[近两年] * * @return void */ @@ -368,64 +386,98 @@ class Crontask extends Controller $sDate = strtotime(date('Y-m-d 00:00:00', strtotime('-2 year'))); //获取该文章审核人的信息 $aWhere = [ - 'ctime'=>['>',$sDate], - 'state'=>['in',[1,2,3]] + 'ctime' => ['>',$sDate], + 'state' => ['BETWEEN',[0,5]] ]; - $aReviewer = Db::name('article_reviewer')->field('reviewer_id,count(article_id) as review_num_two_year')->where($aWhere)->order('reviewer_id asc')->group('reviewer_id')->select(); + $aReviewer = Db::name('article_reviewer')->field('reviewer_id,SUM(state IN (0,1,2,3)) AS review_agree_num,SUM(state IN (1,2,3)) AS review_activity_num,SUM(state = 4) AS review_refuse_num,SUM(state IN (0,1,2,3,4,5)) AS review_invite_num')->where($aWhere)->order('reviewer_id asc')->group('reviewer_id')->select(); //查询审稿人数量不为0的审稿信息 - $aUserWhere = [ - // 'is_reviewer' => 1, - 'review_num_two_year' => ['>',0] - ]; - $aUser = Db::name('user')->field('user_id,review_num_two_year')->where($aUserWhere)->select(); + $aUser = Db::name('user')->field('user_id,review_activity_num,review_agree_num,review_refuse_num,review_invite_num')->where('review_invite_num','>',0)->select(); + if(empty($aReviewer) && empty($aUser)){ $this->showMessage('未查询到待处理的审稿人数据【近两年】',2); exit; } + + //处理数量 + $iChunkSize = $this->iChunkSize; $aUser = empty($aUser) ? [] : array_column($aUser, null,'user_id'); + //分片处理数据 if(!empty($aReviewer)){ - $aChunk = array_chunk($aReviewer, 100); + $aChunk = array_chunk($aReviewer, $iChunkSize); Db::startTrans(); foreach ($aChunk as $key => $item) { //数据分片操作 //需要更新的用户ID $aUpdateId = []; //SQL拼接 - $aCase['review_num_two_year'] = 'CASE user_id '; + $aCase = ['review_activity_num' => [], 'review_agree_num' => [],'review_refuse_num' => [],'review_invite_num' => [],'review_agree_rate' => []]; foreach ($item as $key => $value) { //用户ID $iUserId = $value['reviewer_id']; + //活跃度 + $review_activity_num = empty($value['review_activity_num']) ? 0 : $value['review_activity_num']; + //同意审稿数量 + $review_agree_num = empty($value['review_agree_num']) ? 0 : $value['review_agree_num']; + //拒绝审稿数量 + $review_refuse_num = empty($value['review_refuse_num']) ? 0 : $value['review_refuse_num']; + //邀请审稿数量 + $review_invite_num = empty($value['review_invite_num']) ? 0 : $value['review_invite_num']; + //同意审稿占比 + $review_agree_rate = empty($review_invite_num) ? 0 : round($review_agree_num/$review_invite_num,2); + //用户信息 + $aUserInfo = empty($aUser[$iUserId]) ? [] : $aUser[$iUserId]; //拼接更新语句 - if(empty($aUser[$iUserId])){ - //更新数量 - $aCase['review_num_two_year'] .= "WHEN {$iUserId} THEN "; - $aCase['review_num_two_year'] .= "'{$value['review_num_two_year']}' "; + if(empty($aUserInfo)){ + //更新活跃度-审稿数量 + $aCase['review_activity_num'][]= "WHEN {$iUserId} THEN '{$review_activity_num}' "; + //更新同意审稿数量 + $aCase['review_agree_num'][] = "WHEN {$iUserId} THEN '{$review_agree_num}' "; + //更新拒绝审稿数量 + $aCase['review_refuse_num'][] = "WHEN {$iUserId} THEN '{$review_refuse_num}' "; + //更新邀请审稿数量 + $aCase['review_invite_num'][] = "WHEN {$iUserId} THEN '{$review_invite_num}' "; + //同意审稿占比 + $aCase['review_agree_rate'][] = "WHEN {$iUserId} THEN '{$review_agree_rate}' "; + $aUpdateId[] = $iUserId; continue; } - //审核数量无变化,跳过更新 - if($aUser[$iUserId]['review_num_two_year'] == $value['review_num_two_year']){ + //数量无变化,跳过更新 + if($aUserInfo['review_activity_num'] == $value['review_activity_num'] && $aUserInfo['review_agree_num'] == $value['review_agree_num'] && $aUserInfo['review_refuse_num'] == $value['review_refuse_num'] && $aUserInfo['review_invite_num'] == $value['review_invite_num']){ unset($aUser[$iUserId]); continue; } - //审核数量有,变化更新数量 - $aCase['review_num_two_year'] .= "WHEN {$iUserId} THEN "; - $aCase['review_num_two_year'] .= "'{$value['review_num_two_year']}' "; + + //更新活跃度-审稿数量 + $aCase['review_activity_num'][] = "WHEN {$iUserId} THEN '{$review_activity_num}' "; + //更新同意审稿数量 + $aCase['review_agree_num'][] = "WHEN {$iUserId} THEN '{$review_agree_num}' "; + //更新拒绝审稿数量 + $aCase['review_refuse_num'][] = "WHEN {$iUserId} THEN '{$review_refuse_num}' "; + //更新邀请审稿数量 + $aCase['review_invite_num'][] = "WHEN {$iUserId} THEN '{$review_invite_num}' "; + //同意审稿占比 + $aCase['review_agree_rate'][] = "WHEN {$iUserId} THEN '{$review_agree_rate}' "; $aUpdateId[] = $iUserId; unset($aUser[$iUserId]); - } - //SQL拼接最后结尾 - $aCase['review_num_two_year'] .= 'END'; - //执行更新 - if(empty($aUpdateId)){ + } + //更新数据库 + foreach ($aCase as $kk => $value) { + if(empty($value)){ + continue; + } + $sWhere = implode(" ", $value); + $aUpdateCase[$kk] = Db::raw('CASE user_id '.$sWhere.'END'); + } + if(empty($aUpdateCase) || empty($aUpdateId)){ + $this->showMessage('未查询到满足要求的审稿人数据['.$key.']'."\n",2); continue; - } - $result = Db::name('user') - ->where(['user_id' => ['in',$aUpdateId]]) - ->update([ - 'review_num_two_year' => Db::raw($aCase['review_num_two_year']), - ]); + } + //更新数据 + $result = Db::name('user') + ->where(['user_id' => ['in',$aUpdateId]]) + ->update($aUpdateCase); if ($result === false) { $this->showMessage('更新近两年审稿人审核数量失败['.$key.']执行SQL:'.Db::getLastSql()."\n",2); }else{ @@ -435,7 +487,7 @@ class Crontask extends Controller Db::commit(); } if(!empty($aUser)){ - $aChunk = array_chunk($aUser, 100); + $aChunk = array_chunk($aUser, $iChunkSize); Db::startTrans(); foreach ($aChunk as $key => $item) { //数据分片操作 $aUserId = array_column($item, 'user_id'); @@ -445,7 +497,11 @@ class Crontask extends Controller $result = Db::name('user')->where(['user_id' => ['in',$aUserId]]) ->limit(count($aUserId)) ->update([ - 'review_num' => 0, + 'review_activity_num' => 0, + 'review_agree_num' => 0, + 'review_refuse_num' => 0, + 'review_refuse_num' => 0, + 'review_agree_rate' => 0, ]); if ($result === false) { $this->showMessage('清空近两年审稿人审核数量失败['.$key.']执行SQL:'.Db::getLastSql()."\n",2); @@ -458,13 +514,41 @@ class Crontask extends Controller } $this->showMessage('批量更新近两年审稿人审核数量成功'."\n",1); } + + /** + * 批量处理待审核的文章自动推荐审稿人 + * + * @return void + */ + public function recommendedReviewer(){ + + //查询条件 + $aWhere = ['state' => 2,'article_id' => 6311]; + $aArticle = Db::name('article')->field('article_id,accept_sn')->where($aWhere)->limit(1)->select(); + if(empty($aArticle)){ + $this->showMessage('未查询到需要处理的待审核的文章',2); + exit; + } + //数据处理 + foreach ($aArticle as $key => $value) { + $iArticleId = empty($value['article_id']) ? 0 : $value['article_id']; + if(empty($iArticleId)){ + continue; + } + $sQueueId = \think\Queue::push('app\api\job\RecommendReviewer@fire',['article_id' =>$iArticleId], 'RecommendReviewer'); + if($sQueueId === false){ + $this->showMessage('文章入队失败,文章ID:'.$value['article_id'].'['.$value['accept_sn']."]\n",2); + continue; + } + } + $this->showMessage('批量处理待审核的文章自动推荐审稿人成功'."\n",1); + } /** * * 格式化信息输出 * * @access public * @return void - * @author huangpu * @date 2018.09.28 * @param $[message] [<显示信息>] * @param $[status] [<输出信息1成功,2失败>] @@ -477,4 +561,6 @@ class Crontask extends Controller } echo date("Y-m-d H:i:s") . " " . $message . "\n"; } + + } From 5121dc127b1a174bb3998f411797b170213efced Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 4 Dec 2025 15:17:58 +0800 Subject: [PATCH 25/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Crontask.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/application/api/controller/Crontask.php b/application/api/controller/Crontask.php index 6a6df239..dc7ba4cd 100644 --- a/application/api/controller/Crontask.php +++ b/application/api/controller/Crontask.php @@ -239,7 +239,7 @@ class Crontask extends Controller } /** - * @title 审稿人拒绝审稿[超过五日默认自动拒绝审稿] + * @title 审稿人拒绝审稿[超过5日默认自动拒绝审稿] * */ public function refuseReviewArticle(){ @@ -522,8 +522,11 @@ class Crontask extends Controller */ public function recommendedReviewer(){ + $this->showMessage('批量处理待审核的文章自动推荐审稿人成功'."\n",1); + exit; //查询条件 - $aWhere = ['state' => 2,'article_id' => 6311]; + $aWhere = ['state' => 2]; + $aWhere['user_id'] = 54; $aArticle = Db::name('article')->field('article_id,accept_sn')->where($aWhere)->limit(1)->select(); if(empty($aArticle)){ $this->showMessage('未查询到需要处理的待审核的文章',2); From d06175956161958567b17aa751820fc1fc4102aa Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 4 Dec 2025 16:11:28 +0800 Subject: [PATCH 26/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Article.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index 04e55ff4..d0cdd925 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -501,7 +501,7 @@ class Article extends Base $data = $this->request->post(); //查询文章基础数据 $where['t_article.article_id'] = $data['articleId']; - $article_res = $this->article_obj->field('t_article.*,t_journal.title journalname,t_user.account')->join(array(['t_journal', 't_journal.journal_id = t_article.journal_id', 'LEFT'], ['t_user', 't_user.user_id = t_article.user_id', 'LEFT']))->where($where)->find(); + $article_res = $this->article_obj->field('t_article.*,t_journal.title journalname,t_user.account,t_user.email as user_email')->join(array(['t_journal', 't_journal.journal_id = t_article.journal_id', 'LEFT'], ['t_user', 't_user.user_id = t_article.user_id', 'LEFT']))->where($where)->find(); //查询文章状态跟踪信息 $article_msg = $this->article_msg_obj->where(['article_id' => $data['articleId']])->where('state', 0)->select(); //查询审稿人审稿建议 @@ -1585,7 +1585,7 @@ class Article extends Base //为预接收的文章生成production实例 if ($data['state'] == 6) { -// $this->addProductionEx($data['articleId']); + $this->addProductionEx($data['articleId']); $this->addArticleMainEx($data['articleId']); //如果是免费的期刊文章,那么直接变成付款完成 From d60b59dae40f3b23e816cd5c4723e604af3f3a03 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 4 Dec 2025 17:41:21 +0800 Subject: [PATCH 27/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index 7b5c7842..42731651 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -546,7 +546,7 @@ class Workbench extends Base return json_encode(['status' => 10,'msg' => 'Code is illegal','data' => $aData]); } if($aCode['user_id'] != $iUserId){ - return json_encode(['status' => 10,'msg' => 'Illegal code operation','data' => $aData]); + return json_encode(['status' => 11,'msg' => 'Illegal code operation','data' => $aData]); } $iIsCode = 1; } @@ -563,7 +563,7 @@ class Workbench extends Base $iTime = empty($aArticleReviewer['ctime']) ? 0 : $aArticleReviewer['ctime']; if (!is_numeric($iTime) || (int)$iTime <= 0) { return json_encode([ - 'status' => 11, + 'status' => 12, 'msg' => 'Invalid record time, the review period has expired', 'data' => $aData ]); @@ -574,7 +574,7 @@ class Workbench extends Base //执行审稿过期 $aWhere = ['art_rev_id' => $iArtRevId,'state' => 5]; $result = Db::name('article_reviewer')->where($aWhere)->limit(1)->update(['state' => 4]); - return json_encode(['status' => 11,'msg' => 'The number of days for agreeing to review has exceeded 5','data' => $aData]); + return json_encode(['status' => 13,'msg' => 'The number of days for agreeing to review has exceeded 5','data' => $aData]); } // var_dump(date('Y-m-d H:i:s',$timeDiff),date('Y-m-d H:i:s',$iTime),date('Y-m-d H:i:s',$iNowTime)); //执行同意审稿 @@ -582,7 +582,7 @@ class Workbench extends Base $result = Db::name('article_reviewer')->where($aWhere)->limit(1)->update(['state' => 0,'agree_review_time' => time()]); } if($iIsCode != 1){ - return json_encode(['status' => 12,'msg' => 'Reviewer did not agree to review','data' => $aData]); + return json_encode(['status' => 14,'msg' => 'Reviewer did not agree to review','data' => $aData]); } } //同意审稿 @@ -594,7 +594,7 @@ class Workbench extends Base $iTime = empty($iTime) ? intval($iCtime) : intval($iTime); if (!is_numeric($iTime) || (int)$iTime <= 0) { return json_encode([ - 'status' => 11, + 'status' => 15, 'msg' => 'Invalid record time, the review period has expired', 'data' => $aData ]); @@ -602,7 +602,7 @@ class Workbench extends Base //判断是否超过14天 $timeDiff = $iTime+$iFourteenDays; if($timeDiff < $iNowTime){ - return json_encode(['status' => 11,'msg' => 'The number of days for agreeing to review has exceeded 14','data' => $aData]); + return json_encode(['status' => 16,'msg' => 'The number of days for agreeing to review has exceeded 14','data' => $aData]); } $aData['is_review_auth'] = 1; return json_encode(['status' => 1,'msg' => 'success','data' => $aData]); From fac1d1d4d9c60fbdfe329bb418354d969ce8388f Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 5 Dec 2025 11:06:38 +0800 Subject: [PATCH 28/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=96=B0=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 268 +++++++++++++++++++++++ 1 file changed, 268 insertions(+) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index 42731651..08a4ddde 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -610,4 +610,272 @@ class Workbench extends Base $aData['is_review_auth'] = 1; return json_encode(['status' => 1,'msg' => 'success','data' => $aData]); } + /** + * 审稿人邮件链接失效-重新申请邮件 + */ + public function applySendEmail(){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + //获取审稿记录ID + $iArtRevId = empty($aParam['art_rev_id']) ? 0 : $aParam['art_rev_id']; + if(empty($iArtRevId)){ + return json_encode(['status' => 2,'msg' => 'Please select a record']); + } + //获取账号 + $sAccount = empty($aParam['account']) ? '' : $aParam['account']; + if(empty($sAccount)){ + return json_encode(['status' => 2,'msg' => 'Please enter your account']); + } + //查询用户是否存在 + $aWhere = ['account' => $sAccount,'state' => 0]; + $aUser = Db::name('user')->field('user_id')->where($aWhere)->find(); + if(empty($aUser)){ + return json_encode(['status' => 3,'msg' => 'Account does not exist']); + } + $iUserId = $aUser['user_id']; + + //查询审稿记录 + $aWhere = ['art_rev_id' => $iArtRevId]; + $aArticleReviewer = Db::name('article_reviewer')->field('art_rev_id,reviewer_id,article_id,state')->where($aWhere)->find(); + if(empty($aArticleReviewer)){ + return json_encode(['status' => 4,'msg' => 'Review record does not exist']); + } + if($aArticleReviewer['state'] != 4){ + return json_encode(['status' => 5,'msg' => 'The review link has not expired and no application is required']); + } + + //获取文章信息 + $aWhere = ['article_id' => $aArticleReviewer['article_id']]; + $aArticle = Db::name('article')->field('article_id,abstrart,title article_title,type,accept_sn,journal_id,state')->where($aWhere)->find(); + if(empty($aArticle)){ + return json_encode(['status' => 6,'msg' => 'The article does not exist']); + } + if($aArticle['state'] != 2){ + return json_encode(['status' => 7,'msg' => 'The article is not in the review status']); + } + + //查询期刊信息 + $aWhere = ['journal_id' => $aArticle['article_id'],'state' => 0]; + $aJournal = Db::name('journal')->field('title as journal_name,website')->find(); + //查询期刊信息 + if(empty($aArticle['journal_id'])){ + return json_encode(array('status' => 8,'msg' => 'The article is not associated with a journal' )); + } + $aWhere = ['state' => 0,'journal_id' => $aArticle['journal_id']]; + $aJournal = Db::name('journal')->where($aWhere)->find(); + if(empty($aJournal)){ + return json_encode(array('status' => 9,'msg' => 'No journal information found' )); + } + + //查询编辑邮箱 + $iUserId = empty($aJournal['editor_id']) ? 0 : $aJournal['editor_id']; + if(empty($iUserId)){ + return json_encode(array('status' => 10,'msg' => 'The journal to which the article belongs has not designated a responsible editor' )); + } + + //查询审稿人跟编辑的信息 + $aUserId = [$aArticleReviewer['reviewer_id'],$iUserId]; + $aWhere = ['user_id' => ['in',$aUserId],'state' => 0,'email' => ['<>','']]; + $aUser = Db::name('user')->field('user_id,email,realname,account')->where($aWhere)->select(); + if(empty($aUser)){ + return json_encode(['status' => 11,'msg' => "Reviewer and editor information not found"]); + } + $aUser = array_column($aUser, null,'user_id'); + + //处理发邮件 + //邮件模版 + $aEmailConfig = [ + 'email_subject' => 'Request to Reopen Expired Review Link---{accept_sn}', + 'email_content' => ' + Dear Editor,

+ The reviewer would like to reopen the expired review link for the manuscript. Below are the details:
+ Reviewer Information:
+ Real Name:{realname}
+ Email:{email}

+ Sincerely,
Editorial Office
+
Subscribe to this journal
{journal_title}
+ Email: {journal_email}
+ Website: {website}' + ]; + //邮件内容 + $aSearch = [ + '{accept_sn}' => empty($aArticle['accept_sn']) ? '' : $aArticle['accept_sn'],//accept_sn + '{journal_title}' => empty($aJournal['title']) ? '' : $aJournal['title'],//期刊名 + '{journal_issn}' => empty($aJournal['issn']) ? '' : $aJournal['issn'], + '{journal_email}' => empty($aJournal['email']) ? '' : $aJournal['email'], + '{website}' => empty($aJournal['website']) ? '' : $aJournal['website'], + '{realname}' => empty($aUser[$aArticleReviewer['reviewer_id']]['realname']) ? '' : $aUser[$aArticleReviewer['reviewer_id']]['realname'], + '{email}' => empty($aUser[$aArticleReviewer['reviewer_id']]['email']) ? '' : $aUser[$aArticleReviewer['reviewer_id']]['email'], + ]; + + //发邮件 + //邮箱 + $email = empty($aUser[$iUserId]['email']) ? '' : $aUser[$iUserId]['email']; + if(empty($email)){ + return json_encode(['status' => 8,'msg' => 'Edit email as empty']); + } + $title = str_replace(array_keys($aSearch), array_values($aSearch),$aEmailConfig['email_subject']); + //邮件内容变量替换 + $content = str_replace(array_keys($aSearch), array_values($aSearch), $aEmailConfig['email_content']); + $pre = \think\Env::get('emailtemplete.pre'); + $net = \think\Env::get('emailtemplete.net'); + $net1 = str_replace("{{email}}",trim($email),$net); + $content=$pre.$content.$net1; + //发送邮件 + $memail = empty($aJournal['email']) ? '' : $aJournal['email']; + $mpassword = empty($aJournal['epassword']) ? '' : $aJournal['epassword']; + //期刊标题 + $from_name = empty($aJournal['title']) ? '' : $aJournal['title']; + //邮件队列组装参数 + $aResult = sendEmail($email,$title,$from_name,$content,$memail,$mpassword); + $iStatus = empty($aResult['status']) ? 1 : $aResult['status']; + $iIsSuccess = 2; + $sMsg = empty($aResult['data']) ? '失败' : $aResult['data']; + if($iStatus == 1){ + return json_encode(['status' => 1,'msg' => 'success']); + } + return json_encode(['status' => 8,'msg' => 'fail']); + } + /** + * 编辑审稿人邮件链接失效-重开 + */ + public function updateReviewerState(){ + + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + + //获取审稿记录ID + $iArtRevId = empty($aParam['art_rev_id']) ? 0 : $aParam['art_rev_id']; + if(empty($iArtRevId)){ + return json_encode(['status' => 2,'msg' => 'Please select a record']); + } + //获取账号 + $sAccount = empty($aParam['account']) ? '' : $aParam['account']; + if(empty($sAccount)){ + return json_encode(['status' => 2,'msg' => 'Please enter your account']); + } + //查询用户是否存在 + $aWhere = ['account' => $sAccount,'state' => 0]; + $aUser = Db::name('user')->field('user_id,account,email')->where($aWhere)->find(); + if(empty($aUser)){ + return json_encode(['status' => 3,'msg' => 'Account does not exist']); + } + $iUserId = $aUser['user_id']; + + //查询审稿记录 + $aWhere = ['art_rev_id' => $iArtRevId]; + $aArticleReviewer = Db::name('article_reviewer')->field('art_rev_id,reviewer_id,article_id,state')->where($aWhere)->find(); + if(empty($aArticleReviewer)){ + return json_encode(['status' => 4,'msg' => 'Review record does not exist']); + } + if($aArticleReviewer['state'] != 4){ + return json_encode(['status' => 5,'msg' => 'The review link has not expired and no application is required']); + } + + //获取文章信息 + $aWhere = ['article_id' => $aArticleReviewer['article_id']]; + $aArticle = Db::name('article')->field('article_id,abstrart,title,type,accept_sn,journal_id,state')->where($aWhere)->find(); + if(empty($aArticle)){ + return json_encode(['status' => 6,'msg' => 'The article does not exist']); + } + if($aArticle['state'] != 2){ + return json_encode(['status' => 7,'msg' => 'The article is not in the review status']); + } + + //查询期刊信息 + $aWhere = ['journal_id' => $aArticle['article_id'],'state' => 0]; + $aJournal = Db::name('journal')->field('title as journal_name,website')->find(); + //查询期刊信息 + if(empty($aArticle['journal_id'])){ + return json_encode(array('status' => 8,'msg' => 'The article is not associated with a journal' )); + } + $aWhere = ['state' => 0,'journal_id' => $aArticle['journal_id']]; + $aJournal = Db::name('journal')->where($aWhere)->find(); + if(empty($aJournal)){ + return json_encode(array('status' => 9,'msg' => 'No journal information found' )); + } + + //判断编辑的操作权限 + $iEditorId = empty($aJournal['editor_id']) ? 0 : $aJournal['editor_id']; + if($iEditorId != $iUserId){ + return json_encode(array('status' => 10,'msg' => 'This article is not authorized for operation under the journal you are responsible for' )); + } + + //更新文章状态为邀请 + $aWhere = ['art_rev_id' => $iArtRevId,'state' => 4]; + $result = Db::name('article_reviewer')->where($aWhere)->limit(1)->update(['state' => 5,'ctime' => time(),'editor_act' => 1]); + if($result === false){ + return json_encode(array('status' => 11,'msg' => 'Status update failed' )); + } + + //查询审稿人的邮箱 + $aWhere = ['user_id' => $aArticleReviewer['reviewer_id'],'state' => 0,'email' => ['<>','']]; + $aUser = Db::name('user')->field('user_id,email,realname,account')->where($aWhere)->find(); + if(empty($aUser)){ + return json_encode(['status' => 12,'msg' => "Reviewer and editor information not found"]); + } + //处理发邮件 + //邮箱 + $email = empty($aUser['email']) ? '' : $aUser['email']; + if(empty($email)){ + return json_encode(['status' => 13,'msg' => 'Reviewer email as empty']); + } + //邮件模版 + $aEmailConfig = [ + 'email_subject' => 'Invitation to review a manuscript for {journal_title}-[{accept_sn}]', + 'email_content' => ' + Dear Dr. {realname},

+ The manuscript entitled "{article_title}" has been submitted to the journal {journal_title}.The Editor-in-Chief would be most grateful if you could offer an opinion regarding its suitability for publication in the journal {journal_title}.

+ Abstract of the Manuscript:
+ {abstrart}

+ Please let us know if there are any potential conflicts of interest and click the following link to review the manuscript.
+ Click here to accept the invitation to review
+ Your username: {account}
+ Your original password:123456qwe, if you have reset the password, please login with the new one or click the "forgot password".
+ Thank you for your continued support of our journal.

+ Sincerely,
Editorial Office
+ Subscribe to this journal
{journal_title}
+ Email: {journal_email}
+ Website: {website}' + ]; + $aSearch = [ + '{accept_sn}' => empty($aArticle['accept_sn']) ? '' : $aArticle['accept_sn'],//accept_sn + '{article_title}' => empty($aArticle['title']) ? '' : $aArticle['title'],//文章标题 + '{abstrart}' => empty($aArticle['abstrart']) ? '' : $aArticle['abstrart'],//文章摘要 + '{journal_title}' => empty($aJournal['title']) ? '' : $aJournal['title'],//期刊名 + '{journal_issn}' => empty($aJournal['issn']) ? '' : $aJournal['issn'], + '{journal_email}' => empty($aJournal['email']) ? '' : $aJournal['email'], + '{website}' => empty($aJournal['website']) ? '' : $aJournal['website'], + ]; + //用户名 + $realname = empty($aUser['account']) ? '' : $aUser['account']; + $realname = empty($aUser['realname']) ? $realname : $aUser['realname']; + $aSearch['{realname}'] = $realname; + //用户账号 + $aSearch['{account}'] = empty($aUser['account']) ? '' : $aUser['account']; + //审稿链接 + $oArticle = new \app\api\controller\Article; + $aSearch['{creatLoginUrlForreviewer}'] = $oArticle->creatLoginUrlForreviewer(['user_id' => $aArticleReviewer['reviewer_id']],$iArtRevId); + $title = str_replace(array_keys($aSearch), array_values($aSearch),$aEmailConfig['email_subject']); + //邮件内容变量替换 + $content = str_replace(array_keys($aSearch), array_values($aSearch), $aEmailConfig['email_content']); + $pre = \think\Env::get('emailtemplete.pre'); + $net = \think\Env::get('emailtemplete.net'); + $net1 = str_replace("{{email}}",trim($email),$net); + $content=$pre.$content.$net1; + //发送邮件 + $memail = empty($aJournal['email']) ? '' : $aJournal['email']; + $mpassword = empty($aJournal['epassword']) ? '' : $aJournal['epassword']; + //期刊标题 + $from_name = empty($aJournal['title']) ? '' : $aJournal['title']; + //邮件队列组装参数 + $aResult = sendEmail($email,$title,$from_name,$content,$memail,$mpassword); + $iStatus = empty($aResult['status']) ? 1 : $aResult['status']; + $iIsSuccess = 2; + $sMsg = empty($aResult['data']) ? '失败' : $aResult['data']; + if($iStatus == 1){ + return json_encode(['status' => 1,'msg' => 'success']); + } + return json_encode(['status' => 14,'msg' => 'fail']); + } } From 09d61b3785a8cb656fbb4590fba17e7c7dba82bf Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 5 Dec 2025 13:14:01 +0800 Subject: [PATCH 29/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=96=B0=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index 08a4ddde..e544d0b1 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -531,7 +531,7 @@ class Workbench extends Base } //审稿已过期 if($aArticleReviewer['state'] == 4){ - return json_encode(['status' => 9,'msg' => 'The review has expired','data' => $aData]); + return json_encode(['status' => 13,'msg' => 'The review has expired','data' => $aData]); } //同意/1小改后接收/接收 From 2958024f6934789f489645bfe9ef79a2f1b8e9f7 Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 5 Dec 2025 14:41:36 +0800 Subject: [PATCH 30/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=96=B0=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index e544d0b1..7cc2f8ea 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -507,7 +507,7 @@ class Workbench extends Base $aArticle['type_name'] = translateType($aArticle['type']);//文章类型 //查询期刊信息 $aWhere = ['journal_id' => $aArticle['article_id'],'state' => 0]; - $aJournal = Db::name('journal')->field('title as journal_name,website')->find(); + $aJournal = Db::name('journal')->field('title as journal_name,website,email as journal_email')->find(); if(!empty($aJournal)){ $aArticle += $aJournal; } From 78651c21f8f96f6b3b3de0bc97d130e40b1b5c34 Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 5 Dec 2025 16:03:17 +0800 Subject: [PATCH 31/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=96=B0=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index 7cc2f8ea..ec06353a 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -682,6 +682,12 @@ class Workbench extends Base } $aUser = array_column($aUser, null,'user_id'); + //更新审稿人重新申请状态为 + $aWhere = ['art_rev_id' => $iArtRevId,'state' => 4]; + $result = Db::name('article_reviewer')->where($aWhere)->limit(1)->update(['is_reapply' => 1,'reapply_time' => time(),'reviewer_act' => 1]); + if($result === false){ + return json_encode(array('status' => 11,'msg' => 'Application to reopen link failed' )); + } //处理发邮件 //邮件模版 $aEmailConfig = [ @@ -803,7 +809,7 @@ class Workbench extends Base //更新文章状态为邀请 $aWhere = ['art_rev_id' => $iArtRevId,'state' => 4]; - $result = Db::name('article_reviewer')->where($aWhere)->limit(1)->update(['state' => 5,'ctime' => time(),'editor_act' => 1]); + $result = Db::name('article_reviewer')->where($aWhere)->limit(1)->update(['state' => 5,'ctime' => time(),'editor_act' => 1,'is_reapply' => 2,'update_time' => time(),'reviewer_act' => 0]); if($result === false){ return json_encode(array('status' => 11,'msg' => 'Status update failed' )); } From 6d07002cf14efc503174258966cfc62b9f4602ba Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 10 Dec 2025 16:05:19 +0800 Subject: [PATCH 32/53] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Supplementary.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/api/controller/Supplementary.php b/application/api/controller/Supplementary.php index 068fe0e8..c8a0ec99 100644 --- a/application/api/controller/Supplementary.php +++ b/application/api/controller/Supplementary.php @@ -62,6 +62,9 @@ class Supplementary extends Base continue; } $sRealName = empty($aUser[$value['user_id']]) ? '' : $aUser[$value['user_id']]; + if(!empty($sRealName) && strlen($sRealName) >= 3 && substr($sRealName, 0, 3) === "\xEF\xBB\xBF") { + $sRealName = substr($sRealName, 3); + } $aUserData[$sIssn][] = $sRealName; } return json_encode(['status' => 1,'msg' => 'success','data' => $aUserData]); From a614206eaf188434be68f7e0d19cf1f45668ae72 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 16 Dec 2025 09:41:38 +0800 Subject: [PATCH 33/53] =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index ec06353a..94df3f04 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -506,8 +506,8 @@ class Workbench extends Base } $aArticle['type_name'] = translateType($aArticle['type']);//文章类型 //查询期刊信息 - $aWhere = ['journal_id' => $aArticle['article_id'],'state' => 0]; - $aJournal = Db::name('journal')->field('title as journal_name,website,email as journal_email')->find(); + $aWhere = ['journal_id' => $aArticle['journal_id'],'state' => 0]; + $aJournal = Db::name('journal')->field('title as journal_name,website,email as journal_email')->where($aWhere)->find(); if(!empty($aJournal)){ $aArticle += $aJournal; } From f35e6ea0b94631d0317d5aceb329b994fc38ed87 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 23 Dec 2025 14:27:44 +0800 Subject: [PATCH 34/53] =?UTF-8?q?=E5=8F=82=E8=80=83=E6=96=87=E7=8C=AE?= =?UTF-8?q?=E9=A1=B5=E7=A0=81=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ProductionArticleRefer.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/application/common/ProductionArticleRefer.php b/application/common/ProductionArticleRefer.php index 397887a0..0e0bea26 100644 --- a/application/common/ProductionArticleRefer.php +++ b/application/common/ProductionArticleRefer.php @@ -100,6 +100,7 @@ class ProductionArticleRefer $update['refer_frag'] = $f; $update['cs'] = 1; //写入通过AI获取参考文献详情队列 + $aParam['is_split_fail'] = 1; \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi'); } if (mb_substr_count($frag, '.') == 3){ @@ -131,15 +132,15 @@ class ProductionArticleRefer if ($missingLen > 0) { $fillPart = substr($prefix, 0, $missingLen); $newSuffix = $fillPart . $suffix; - $update['dateno'] = $aStr[0].':'.$prefix.'-'.$newSuffix; + $update['dateno'] = $aStr[0].':'.$prefix.'–'.$newSuffix; } } } } - if(empty($aStr[1])){ - //写入通过AI获取参考文献详情队列 - \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi'); - } + // if(empty($aStr[1])){ + // //写入通过AI获取参考文献详情队列 + // \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi'); + // } } //新增处理 期卷页码 20251127 end $update['doilink'] = strpos($aRefer['refer_doi'],"http")===false?"https://doi.org/" . $aRefer['refer_doi']:$aRefer['refer_doi']; From 602842e72ce31a0afb00ca9ccba90a0fbb47610d Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 23 Dec 2025 14:51:30 +0800 Subject: [PATCH 35/53] =?UTF-8?q?=E5=8F=82=E8=80=83=E6=96=87=E7=8C=AE?= =?UTF-8?q?=E9=A1=B5=E7=A0=81=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ProductionArticleRefer.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/application/common/ProductionArticleRefer.php b/application/common/ProductionArticleRefer.php index 0e0bea26..706ec8ee 100644 --- a/application/common/ProductionArticleRefer.php +++ b/application/common/ProductionArticleRefer.php @@ -88,7 +88,7 @@ class ProductionArticleRefer $aWhere = ['p_refer_id' => $iPReferId]; $result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate); //写入通过AI获取参考文献详情队列 - \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi'); + // \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi'); return json_encode(array('status' => 2,'msg' => 'The data obtained from the interface is empty'.$url)); } @@ -100,8 +100,7 @@ class ProductionArticleRefer $update['refer_frag'] = $f; $update['cs'] = 1; //写入通过AI获取参考文献详情队列 - $aParam['is_split_fail'] = 1; - \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi'); + // \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi'); } if (mb_substr_count($frag, '.') == 3){ $res = explode('.', $frag); From 6b1ec4d1d28a923781de47a8a3467752faf7a617 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 25 Dec 2025 15:35:57 +0800 Subject: [PATCH 36/53] =?UTF-8?q?=E5=AE=A1=E7=A8=BF=E4=BA=BA=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E4=BD=9C=E8=80=85=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/Reviewer.php | 91 +++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/application/common/Reviewer.php b/application/common/Reviewer.php index 78247aa5..e33923a0 100644 --- a/application/common/Reviewer.php +++ b/application/common/Reviewer.php @@ -183,7 +183,18 @@ class Reviewer } //查询作者所属机构 $aAuthor = json_decode($this->getAuthor($aArticle),true); - $sCompany = empty($aAuthor['data']) ? '' : $aAuthor['data']; + $aAuthorResult = empty($aAuthor['data']) ? [] : $aAuthor['data']; + $sCompany = empty($aAuthorResult['company']) ? '' : $aAuthorResult['company']; + + //文章作者 + $aAuthorId = empty($aAuthorResult['user_id']) ? [] : $aAuthorResult['user_id']; + + //文章作者机构信息 + $aAuthorCompany = empty($aAuthorResult['author']) ? [] : array_unique(array_column($aAuthorResult['author'], 'company')); + //审稿人机构 + $aReviewerCompany = empty($aAuthorResult['company_list']) ? [] : array_unique(array_values($aAuthorResult['company_list'])); + $aAuthorCompany = array_unique(array_merge($aAuthorCompany,$aReviewerCompany)); + //获取劣迹审稿人 $aBlack = json_decode($this->getBlackReviewer(),true); $aBlack = empty($aBlack['data']) ? [] : $aBlack['data']; @@ -200,6 +211,11 @@ class Reviewer // 查询符合公司条件的审稿人ID,确保去重 $aWhere = ['state' => 0, 'company' => ['<>', $sCompany]]; + if(!empty($aAuthorCompany)){ + array_push($aAuthorCompany, $sCompany); + $aAuthorCompany = array_unique($aAuthorCompany); + $aWhere['company'] = ['not in', $aAuthorCompany]; + } if(!empty($aBlack) && !empty($aParam['not_choose_id'])){ $aBlack = array_unique(array_merge($aBlack, $aParam['not_choose_id'])); $aWhere['reviewer_id'] = ['not in',$aBlack]; @@ -229,6 +245,10 @@ class Reviewer 't_user.state' => 0, 't_user.user_id' => ['<>', $aArticle['user_id']] ]; + if(!empty($aAuthorId)){ + array_push($aAuthorId, $aArticle['user_id']); + $aWhere['t_user.user_id'] = ['not in', $aAuthorId]; + } //根据邮箱检索 if(!empty($aParam['email'])){ $aWhere['t_user.email'] = ['like',"%" . $aParam["email"] . "%"]; @@ -242,7 +262,6 @@ class Reviewer ->join('t_user', 't_user.user_id = t_reviewer_to_journal.reviewer_id') ->join(Db::raw("({$sMajorQuery}) major"),'major.user_id = t_reviewer_to_journal.reviewer_id') ->join(Db::raw("({$sCompanyQuery}) company"),'company.reviewer_id = t_reviewer_to_journal.reviewer_id')->where($aWhere)->group($sGroup)->count(); - if(empty($iCount)){ return json_encode(['status' => 1,'msg' => 'No reviewer data found that meets the criteria','data' => ['total' => 0,'lists' => [],'size' => $iSize]]); } @@ -349,21 +368,25 @@ class Reviewer if(empty($iArticleId)){ return json_encode(['status' => 2,'msg' => 'Please select the article to query']); } - //用户ID - $iUserId = empty($aParam['user_id']) ? 0 : $aParam['user_id']; - if(empty($iUserId)){ - return json_encode(['status' => 2,'msg' => 'User ID cannot be empty']); - } - //查询文章作者详情 作者和审稿人的机构不一致 - $aUserInfo = Db::name('user')->field('email')->where('user_id',$iUserId)->find(); - $sEmail = empty($aUserInfo['email']) ? '' : $aUserInfo['email']; - if(empty($sEmail)){ - return json_encode(['status' => 3,'msg' => 'No user information found']); + //查询文章作者 + $iUserId = empty($aParam['user_id']) ? 0 : $aParam['user_id']; + $aUserId = [$iUserId]; + $aAuthorList = Db::name('article_author')->field('email,company')->where(['article_id'=>$iArticleId,'state' => 0])->select(); + if(!empty($aAuthorList)){ + $aAuthorEmail = array_column($aAuthorList, 'email'); + $aWhere = ['email' => ['in',$aAuthorEmail],'state' => 0]; + $aUserId = Db::name('user')->where($aWhere)->column('user_id'); + if(!empty($aUserId)){ + array_push($aUserId, $iUserId); + } } - //查询和作者同机构的审稿人 - $aAuthor = Db::name('article_author')->field('company')->where(['article_id'=>$iArticleId,'email' => $sEmail,'state' => 0])->find(); - return json_encode(['status' => 1,'msg' => 'success','data' => empty($aAuthor['company']) ? '' : $aAuthor['company']]); + //查询提交文章作者详情 作者和审稿人的机构不一致 + if(!empty($aUserId)){ + //作者同机构的审稿人 + $aAuthorCompany = Db::name('user_reviewer_info')->where(['reviewer_id' => ['in',$aUserId],'state' => 0])->column('reviewer_id,company'); + } + return json_encode(['status' => 1,'msg' => 'success','data' => ['company' => empty($aAuthorCompany[$iUserId]) ? '' : $aAuthorCompany[$iUserId],'user_id' => $aUserId,'author' => $aAuthorList,'company_list' => $aAuthorCompany]]); } /** @@ -384,6 +407,23 @@ class Reviewer return json_encode(['status' => 2,'msg' => 'Reviewers who meet the criteria of the article were not selected']); } + //查询送审中的文章 + $aWhere = ['state' => 2,'article_id' => $iArticleId]; + $aArticle = Db::name('article')->field('article_id,user_id')->where($aWhere)->find(); + if(empty($aArticle)){ + return json_encode(array('status' => 3,'msg' => 'No articles requiring review were found' )); + } + //查询文章作者 + $iArticleUserId = empty($aArticle['user_id']) ? 0 : $aArticle['user_id']; + $aAuthorId = [$iArticleUserId]; + $aAuthorEmail = Db::name('article_author')->where(['article_id'=>$iArticleId,'state' => 0])->column('email'); + if(!empty($aAuthorEmail)){ + $aWhere = ['email' => ['in',$aAuthorEmail],'state' => 0]; + $aAuthorId = Db::name('user')->where($aWhere)->column('user_id'); + if(!empty($aAuthorId)){ + array_push($aAuthorId, $iArticleUserId); + } + } //查询文章审稿人是否存在 $aWhere = ['reviewer_id' => is_array($aReviewerId) ? ['in',$aReviewerId] : $aReviewerId,'article_id' => $iArticleId]; $aReviewer = Db::name('article_reviewer')->where($aWhere)->column('reviewer_id'); @@ -392,6 +432,9 @@ class Reviewer $aInsert = []; $iNowTime = time(); foreach ($aReviewerId as $value) { + if(in_array($value, $aAuthorId)){//排除提交稿件作者跟文章作者 + continue; + } if(in_array($value, $aReviewer)){ continue; } @@ -458,6 +501,17 @@ class Reviewer return json_encode(['status' => 6,'msg' => 'No qualified reviewers were found']); } + //查询文章作者 + $iArticleUserId = empty($aArticle['user_id']) ? 0 : $aArticle['user_id']; + $aAuthorId = [$iArticleUserId]; + $aAuthorEmail = Db::name('article_author')->where(['article_id'=>$iArticleId,'state' => 0])->column('email'); + if(!empty($aAuthorEmail)){ + $aWhere = ['email' => ['in',$aAuthorEmail],'state' => 0]; + $aAuthorId = Db::name('user')->where($aWhere)->column('user_id'); + if(!empty($aAuthorId)){ + array_push($aAuthorId, $iArticleUserId); + } + } //查询用户邮箱 $aUserId = array_keys($aReviewer); $aWhere = ['user_id' => ['in',$aUserId],'state' => 0,'email' => ['<>','']]; @@ -484,7 +538,9 @@ class Reviewer $oArticle = new \app\api\controller\Article; $sMsg = ''; foreach ($aUser as $key => $value) { - + if(in_array($value['user_id'], $aAuthorId)){//排除提交稿件作者跟文章作者 + continue; + } $email = empty($value['email']) ? '' : $value['email']; if(empty($email) || empty($aReviewer[$value['user_id']])){ continue; @@ -557,7 +613,8 @@ class Reviewer //判断文章最新邀请审稿时间判断是否超过七日 $iDay = 7; $sDate = strtotime('-'.$iDay.' day'); - $iMaxTime = DB::name('article_reviewer')->where($aWhere)->max('invited_time'); + $aCountWhere = ['article_id' => $aParam['article_id'],'state' => ['<>',4]]; + $iMaxTime = DB::name('article_reviewer')->where($aCountWhere)->max('invited_time'); $iMaxTime = empty($iMaxTime) ? 0 : $iMaxTime; if($iMaxTime > $sDate){ return json_encode(['status' => 4,'msg' => 'The last invitation for reviewers did not exceed '.$iDay.' days']); From bbe66504e4daeb9d2c9e877452f016acd8458185 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 25 Dec 2025 17:05:40 +0800 Subject: [PATCH 37/53] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Reviewer.php | 31 +++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/application/api/controller/Reviewer.php b/application/api/controller/Reviewer.php index 2977393d..093d05fb 100644 --- a/application/api/controller/Reviewer.php +++ b/application/api/controller/Reviewer.php @@ -2224,12 +2224,35 @@ class Reviewer extends Base } //查询文章期刊ID - $article_info = $this->article_obj->field('journal_id')->where("article_id", $data['article_id'])->find(); + $article_info = $this->article_obj->field('user_id,journal_id')->where("article_id", $data['article_id'])->find(); + //查询文章作者 + $iUserId = empty($article_info['user_id']) ? 0 : $article_info['user_id']; + $aUserId = [$iUserId]; + $article_info = $this->article_obj->field('user_id,journal_id')->where("article_id", $data['article_id'])->find(); + $aAuthorList = Db::name('article_author')->field('email,company')->where(['article_id'=>$data['article_id'],'state' => 0])->select(); + if(!empty($aAuthorList)){ + $aAuthorEmail = array_column($aAuthorList, 'email'); + $aWhere = ['email' => ['in',$aAuthorEmail],'state' => 0]; + $aUserId = Db::name('user')->where($aWhere)->column('user_id'); + if(!empty($aUserId)){ + array_push($aUserId, $iUserId); + } + } + //查询提交文章作者详情 作者和审稿人的机构不一致 + $aReviewerCompany = []; + if(!empty($aUserId)){ + //作者同机构的审稿人 + $aReviewerCompany = Db::name('user_reviewer_info')->where(['reviewer_id' => ['in',$aUserId],'state' => 0])->column('reviewer_id,company'); + } + + //文章作者机构信息 + $aAuthorCompany = empty($aAuthorList) ? [] : array_unique(array_column($aAuthorList, 'company')); + $aAuthorCompany = array_unique(array_merge($aAuthorCompany,$aReviewerCompany)); //期刊ID $iJournalId = empty($article_info['journal_id']) ? 0 : $article_info['journal_id']; //查询文章现有审稿人 $noids = $this->article_reviewer_obj->where('article_id', $data['article_id'])->column('reviewer_id'); - + $noids = empty($aUserId) ? [] : array_unique(array_merge($aUserId,$noids)); //分页配置 $limit_start = ($data['pageIndex'] - 1) * $data['pageSize']; @@ -2251,6 +2274,10 @@ class Reviewer extends Base $where['t_user_reviewer_info.major'] = ['in',$this->majorids($data['major_id'])]; } + //过滤审稿人机构 + if(!empty($aAuthorCompany)){ + $where['t_user_reviewer_info.company'] = ['not in', $aAuthorCompany]; + } // 计算10天之后的时间戳(10天 = 10 * 24 * 60 * 60秒) $iTeenDaysLater = strtotime('-10 days'); From 038d28633b8344860be46d749953beff99255cf7 Mon Sep 17 00:00:00 2001 From: chengxl Date: Thu, 25 Dec 2025 17:19:49 +0800 Subject: [PATCH 38/53] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Reviewer.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/api/controller/Reviewer.php b/application/api/controller/Reviewer.php index 093d05fb..5056475d 100644 --- a/application/api/controller/Reviewer.php +++ b/application/api/controller/Reviewer.php @@ -2252,7 +2252,8 @@ class Reviewer extends Base $iJournalId = empty($article_info['journal_id']) ? 0 : $article_info['journal_id']; //查询文章现有审稿人 $noids = $this->article_reviewer_obj->where('article_id', $data['article_id'])->column('reviewer_id'); - $noids = empty($aUserId) ? [] : array_unique(array_merge($aUserId,$noids)); + $noids = empty($noids) ? [] : $noids; + $noids = empty($aUserId) ? $noids : array_unique(array_merge($aUserId,$noids)); //分页配置 $limit_start = ($data['pageIndex'] - 1) * $data['pageSize']; From 4d46b66d0257441cb02c7fa4350936241547a7f3 Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 29 Dec 2025 13:56:31 +0800 Subject: [PATCH 39/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BB=BB=E5=8A=A1-?= =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/job/ReminderEmailToReviewer.php | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 application/api/job/ReminderEmailToReviewer.php diff --git a/application/api/job/ReminderEmailToReviewer.php b/application/api/job/ReminderEmailToReviewer.php new file mode 100644 index 00000000..312bc8ac --- /dev/null +++ b/application/api/job/ReminderEmailToReviewer.php @@ -0,0 +1,101 @@ +oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + } + + public function fire(Job $job, $data) + { + //任务开始判断 + $this->oQueueJob->init($job); + + // 获取 Redis 任务的原始数据 + $rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody(); + $jobData = empty($rawBody) ? [] : json_decode($rawBody, true); + $jobId = empty($jobData['id']) ? 'unknown' : $jobData['id']; + + $this->oQueueJob->log("-----------队列任务开始-----------"); + $this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}"); + + try { + + // 验证任务数据完整性 + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + //审稿记录表主键ID + $art_rev_id = empty($data['art_rev_id']) ? 0 : $data['art_rev_id']; + //审稿人ID + $reviewer_id = empty($data['reviewer_id']) ? 0 : $data['reviewer_id']; + //邮件类型 + $email_type = empty($data['email_type']) ? 0 : $data['email_type']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + if (empty($art_rev_id)) { + $this->oQueueJob->log("无效的art_rev_id,删除任务"); + $job->delete(); + return; + } + if (empty($reviewer_id)) { + $this->oQueueJob->log("无效的reviewer_id,删除任务"); + $job->delete(); + return; + } + if (empty($email_type)) { + $this->oQueueJob->log("无效的email_type,删除任务"); + $job->delete(); + return; + } + // 生成唯一任务标识 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}:{$reviewer_id}:{$art_rev_id}:{$email_type}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + // 执行核心任务 + //查询是否发送过邮件 + $oCronreview = new Cronreview; + $response = $oCronreview->reminder($data); + // 验证API响应 + if (empty($response)) { + throw new \RuntimeException("OpenAI API返回空结果"); + } + // 检查JSON解析错误 + $aResult = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException("解析OpenAI响应失败: " . json_last_error_msg() . " | 原始响应: {$response}"); + } + $sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg']; + //更新完成标识 + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}"); + + } catch (RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (Exception $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } finally { + $this->oQueueJob->finnal(); + } + } +} \ No newline at end of file From a951558f9bf3370bed571f95a2b8a83716457a08 Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 29 Dec 2025 14:00:44 +0800 Subject: [PATCH 40/53] =?UTF-8?q?=E6=97=B6=E9=97=B4=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index 94df3f04..0d8bf45f 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -551,7 +551,8 @@ class Workbench extends Base $iIsCode = 1; } //当前时间 - $iNowTime = time(); + // $iNowTime = time(); + $iNowTime = strtotime(date('Y-m-d', time())); // 14天 = 14*24*3600 秒 = 1209600 秒 $iFourteenDays = 14 * 24 * 3600; //五天 @@ -569,6 +570,8 @@ class Workbench extends Base ]); } //判断是否超过5天 + $iTime = date('Y-m-d', $iTime); + $iTime = strtotime($iTime);//邀请时间戳 $timeDiff = $iTime+$iFiveDays; if($timeDiff < $iNowTime){ //执行审稿过期 @@ -600,6 +603,8 @@ class Workbench extends Base ]); } //判断是否超过14天 + $iTime = date('Y-m-d', $iTime); + $iTime = strtotime($iTime);//同意审稿时间戳 $timeDiff = $iTime+$iFourteenDays; if($timeDiff < $iNowTime){ return json_encode(['status' => 16,'msg' => 'The number of days for agreeing to review has exceeded 14','data' => $aData]); From 47594a9042fca8046d639a4020c8ea9e71b96f6a Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 29 Dec 2025 14:57:25 +0800 Subject: [PATCH 41/53] =?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/command/agreeReviewReminder.sh | 23 +++++++++++++++++++++ application/command/inviteReviewReminder.sh | 23 +++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100755 application/command/agreeReviewReminder.sh create mode 100755 application/command/inviteReviewReminder.sh diff --git a/application/command/agreeReviewReminder.sh b/application/command/agreeReviewReminder.sh new file mode 100755 index 00000000..346d4f76 --- /dev/null +++ b/application/command/agreeReviewReminder.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# agreeReviewReminder.sh +# 批量处理文章审稿阶段-同意审稿超过十天/十二天发送提醒邮件 +# 调用接口获取需要提醒的审稿人记录 +# 此文件需要在crontab中配置每天【凌晨0:30】运行一次 +# @author chengxiaoling +# @date 2025-12-29 + +# 基础配置 +DOMAIN="http://api.tmrjournals.com/public/index.php/" # 项目域名 +# DOMAIN="http://zmzm.tougao.dev.com" # 项目域名 +ROUTE="/api/Cronreview/agreeReminder" # 控制器路由 +BASE_PATH=$(cd `dirname $0`; pwd) +# 如果日志目录不存在则创建 +logDir=${BASE_PATH}/log/$(date "+%Y")/$(date "+%m") +if [ ! -d $logDir ];then + mkdir -p $logDir +fi + +# 执行请求并记录日志 +curl "${DOMAIN}${ROUTE}" >> ${logDir}/agreeReminder_$(date "+%Y%m%d").log 2>&1 +# 添加时间戳 +echo "[$(date '+%Y-%m-%d %H:%M:%S')] 定时任务已执行" >> ${logDir}/agreeReminder_$(date "+%Y%m%d").log \ No newline at end of file diff --git a/application/command/inviteReviewReminder.sh b/application/command/inviteReviewReminder.sh new file mode 100755 index 00000000..b4ac4715 --- /dev/null +++ b/application/command/inviteReviewReminder.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# inviteReviewReminder.sh +# 批量处理文章审稿阶段-邀请审稿超过三天/五天发送提醒邮件 +# 调用接口获取需要提醒的审稿人记录 +# 此文件需要在crontab中配置每天【凌晨0:30】运行一次 +# @author chengxiaoling +# @date 2025-12-29 + +# 基础配置 +DOMAIN="http://api.tmrjournals.com/public/index.php/" # 项目域名 +# DOMAIN="http://zmzm.tougao.dev.com" # 项目域名 +ROUTE="/api/Cronreview/inviteReminder" # 控制器路由 +BASE_PATH=$(cd `dirname $0`; pwd) +# 如果日志目录不存在则创建 +logDir=${BASE_PATH}/log/$(date "+%Y")/$(date "+%m") +if [ ! -d $logDir ];then + mkdir -p $logDir +fi + +# 执行请求并记录日志 +curl "${DOMAIN}${ROUTE}" >> ${logDir}/inviteReminder_$(date "+%Y%m%d").log 2>&1 +# 添加时间戳 +echo "[$(date '+%Y-%m-%d %H:%M:%S')] 定时任务已执行" >> ${logDir}/inviteReminder_$(date "+%Y%m%d").log \ No newline at end of file From e710ecaf05f88bd19770bfff7db2cd1f6ad1a2ba Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 29 Dec 2025 16:37:36 +0800 Subject: [PATCH 42/53] =?UTF-8?q?=E6=8E=A8=E9=80=81=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Cronreview.php | 407 ++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 application/api/controller/Cronreview.php diff --git a/application/api/controller/Cronreview.php b/application/api/controller/Cronreview.php new file mode 100644 index 00000000..2dd05989 --- /dev/null +++ b/application/api/controller/Cronreview.php @@ -0,0 +1,407 @@ + [ + 'email_subject' => 'Invitation to Review Manuscript for [{accept_sn}]-Reminder', + 'email_content' => ' + Dear Dr. {realname},

+ I hope this email finds you well.

+ On {invite_time}, we sent you the following review request for {journal_title},
+ Manuscript ID:{accept_sn}
+ Title:{article_title}

+ We have not yet received a response from you, and we understand that the original invitation may not have reached you. We would greatly appreciate it if you could kindly inform us whether you are available to undertake this review.

+ For your convenience, please find the relevant links below:
+ Accept the review invitation
+ Reject the review invitation
+ Your username: {account}
+ Your original password:123456qwe, if you have reset the password, please sign in with the new one or click the "forgot password".

+ Thank you once again for considering our invitation. Your input is invaluable to us, and we truly appreciate your time and effort.

+ Please feel free to reply to this email or contact me directly with any questions.

+ Sincerely,
+ Editorial Office
+ Subscribe to this journal
{journal_title}
+ Email: {journal_email}
+ Website: {website}' + ], + 'five' => [ + 'email_subject' => 'Gentle Reminder: Review Invitation for Manuscript {accept_sn}', + 'email_content' => ' + Dear Dr. {realname},

+ This is a brief follow-up regarding our review invitation sent on {invite_time} for the following manuscript submitted to {journal_title}:

+ Manuscript ID:{accept_sn}
+ Title:{article_title}

+ We would appreciate it if you could inform us whether you are able to undertake this review. If you are unavailable or require additional time, please feel free to let us know so that we may make appropriate arrangements.

+ For your convenience, please find the relevant links below:
+ Accept the review invitation
+ Reject the review invitation
+ Your username: {account}
+ Your original password:123456qwe, if you have reset the password, please sign in with the new one or click the "forgot password".

+ Thank you very much for your time and consideration.

+ Sincerely,
+ Editorial Office
+ Subscribe to this journal
{journal_title}
+ Email: {journal_email}
+ Website: {website}' + ], + + 'ten' => [ + 'email_subject' => 'Reminder: Review Report for Manuscript [{accept_sn}]', + 'email_content' => ' + Dear Dr. {realname},

+ I hope this message finds you well.

+ I am writing to kindly follow up regarding the review report for the following manuscript, for which you were so kind to agree to serve as a reviewer for {journal_title}.
+ Manuscript ID:{accept_sn}
+ Title:{article_title}

+ We sincerely appreciate the time and expertise you are dedicating to this review. We fully understand that academic and professional commitments can be demanding, and should you require additional time or experience any difficulty in accessing the manuscript, please do not hesitate to let us know. We would be more than happy to accommodate your schedule or provide any assistance needed.

+ We would greatly appreciate your expert feedback at your earliest convenience, as it will help us proceed smoothly with the editorial process. For your convenience, please find the relevant link below:
+ Click here to submit the review report
+ Your username: {account}
+ Your original password:123456qwe, if you have reset the password, please sign in with the new one or click the "forgot password".

+ Thank you once again for your valued contribution to {journal_title} and for your continued support of our peer-review process.

+ Sincerely,
+ Editorial Office
+ Subscribe to this journal
{journal_title}
+ Email: {journal_email}
+ Website: {website}' + ], + 'twelve' => [ + 'email_subject' => 'Gentle Reminder: Review Report for Manuscript [{accept_sn}]', + 'email_content' => ' + Dear Dr. {realname},

+ This is a gentle reminder regarding the review report for the manuscript listed below, for which you kindly agreed to serve as a reviewer:

+ Manuscript ID:{accept_sn}
+ Title:{article_title}

+ For your convenience, please find the relevant links below:
+ Click here to submit review report
+ Your username: {account}
+ Your original password:123456qwe, if you have reset the password, please login with the new one or click the "forgot password".

+ We would greatly appreciate it if you could submit your review at your convenience before {agree_deadline}. If you require additional time or encounter any difficulties, please feel free to let us know.

+ Thank you very much for your valuable time and contribution to the peer-review process.

+ Sincerely,
+ Editorial Office
+ Subscribe to this journal
{journal_title}
+ Email: {journal_email}
+ Website: {website}' + ], + ]; + private $iDayTime = 86400;//一天秒数 + /** + * 文章审稿阶段-邀请审稿超过三天/五天发送提醒邮件 + * @return void + */ + public function inviteReminder(){ + + //获取当前时间 + $sCurrentDate = date('Y-m-d', time()); + $iTime = $this->iDayTime; + //获取文章信息 + $aResult = $this->getArticle(); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + $sMsg = empty($aResult['msg']) ? 'No data obtained' : $aResult['msg']; + if($iStatus != 1){ + $this->showMessage($sMsg,2); + exit; + } + + //数据处理 + $aArticle = empty($aResult['data']) ? [] : $aResult['data']; + if(empty($aArticle)){ + $this->showMessage('Article or journal information is empty',2); + exit; + } + + //查询文章邀请的审稿人 + $aArticleId = array_column($aArticle, 'article_id'); + $aWhere = ['article_id' => ['in',$aArticleId],'state' => 5]; + $aReviewer = Db::name('article_reviewer')->field('art_rev_id,reviewer_id,article_id,ctime')->where($aWhere)->order('article_id desc')->select(); + if(empty($aReviewer)){ + $this->showMessage('No qualified reviewers were found',2); + exit; + } + + //数据处理 + foreach ($aReviewer as $key => $value) { + if(empty($value['ctime'])){ + continue; + } + //时间处理 + $sTargetDate = date('Y-m-d', $value['ctime']); + //日期转时间戳 + $iTargetTime = strtotime($sTargetDate);//邀请时间戳 + $iCurrentTime = strtotime($sCurrentDate);//当前时间戳 + $iThreeCtime = intval($iTargetTime + (3 * $iTime));//三天 + $iFiveCtime = intval($iTargetTime + (5 * $iTime));//五天 + //对比 + if($iCurrentTime != $iThreeCtime && $iCurrentTime != $iFiveCtime){ + continue; + } + if($iThreeCtime == $iCurrentTime){ //超过三天 + $value['email_type'] = 'three'; + } + if($iFiveCtime == $iCurrentTime){ //超过五天 + $value['email_type'] = 'five'; + } + \think\Queue::push('app\api\job\ReminderEmailToReviewer@fire',$value, 'ReminderEmailToReviewer'); + } + $this->showMessage('邀请审稿超过三天/五天发送提醒邮件处理完成',1); + } + /** + * 文章审稿阶段-同意审稿超过十天/十二天发送提醒邮件 + * @return void + */ + public function agreeReminder(){ + + //获取当前时间 + $sCurrentDate = date('Y-m-d', time()); + $iTime = $this->iDayTime; + //获取文章信息 + $aResult = $this->getArticle(); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + $sMsg = empty($aResult['msg']) ? 'No data obtained' : $aResult['msg']; + if($iStatus != 1){ + $this->showMessage($sMsg,2); + exit; + } + + //数据处理 + $aArticle = empty($aResult['data']) ? [] : $aResult['data']; + if(empty($aArticle)){ + $this->showMessage('Article or journal information is empty',2); + exit; + } + + //查询文章邀请的审稿人 + $aArticleId = array_column($aArticle, 'article_id'); + $aWhere = ['article_id' => ['in',$aArticleId],'state' => 0]; + $aReviewer = Db::name('article_reviewer')->field('art_rev_id,reviewer_id,article_id,agree_review_time')->where($aWhere)->order('article_id desc')->select(); + if(empty($aReviewer)){ + $this->showMessage('No qualified reviewers were found',2); + exit; + } + + //数据处理 + foreach ($aReviewer as $key => $value) { + if(empty($value['agree_review_time'])){ + continue; + } + //时间处理 + $sTargetDate = date('Y-m-d', $value['agree_review_time']); + //日期转时间戳 + $iTargetTime = strtotime($sTargetDate);//邀请时间戳 + $iCurrentTime = strtotime($sCurrentDate);//当前时间戳 + $iTenCtime = intval($iTargetTime + (10 * $iTime));//十天 + $iTwelveCtime = intval($iTargetTime + (12 * $iTime));//十二天 + //对比 + if($iCurrentTime != $iTenCtime && $iCurrentTime != $iTwelveCtime){ + continue; + } + if($iTenCtime == $iCurrentTime){ //超过十天 + $value['email_type'] = 'ten'; + } + if($iTwelveCtime == $iCurrentTime){ //超过十二天 + $value['email_type'] = 'twelve'; + } + \think\Queue::push('app\api\job\ReminderEmailToReviewer@fire',$value, 'ReminderEmailToReviewer'); + } + $this->showMessage('同意审稿超过十天/十二天发送提醒邮件处理完成',1); + } + /** + * 发送邮件提醒 + * @return void + */ + public function reminder($aParam = []){ + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select the article to query']); + } + //审稿人ID + $iReviewerId = empty($aParam['reviewer_id']) ? 0 : $aParam['reviewer_id']; + if(empty($iReviewerId)){ + return json_encode(['status' => 2,'msg' => 'Reviewers who meet the criteria of the article were not selected']); + } + //邮件类型 + $sEmailType = empty($aParam['email_type']) ? '' : $aParam['email_type']; + if(empty($sEmailType)){ + return json_encode(['status' => 2,'msg' => 'Email type cannot be empty']); + } + //判断文章是否送审 + $aWhere = ['state' => 2,'article_id' => $iArticleId]; + $aArticle = Db::name('article')->field('article_id,user_id,journal_id,accept_sn,title,abstrart')->where($aWhere)->find(); + if(empty($aArticle)){ + return json_encode(array('status' => 3,'msg' => 'No articles requiring review were found' )); + } + //查询是否存在审稿记录 + $aState = ['three' => 5,'five' => 5,'ten' => 0,'twelve' => 0]; + $iState = isset($aState[$sEmailType]) ? $aState[$sEmailType] : -1 ; + $aWhere = ['reviewer_id' => $iReviewerId,'article_id' => $iArticleId,'state' => $iState]; + $aReviewer = Db::name('article_reviewer')->field('art_rev_id,reviewer_id,ctime,agree_review_time,state')->where($aWhere)->find(); + if(empty($aReviewer)){ + return json_encode(['status' => 4,'msg' => 'No qualified reviewers were found']); + } + + //查询期刊信息 + if(empty($aArticle['journal_id'])){ + return json_encode(array('status' => 5,'msg' => 'The article is not associated with a journal' )); + } + $aWhere = ['state' => 0,'journal_id' => $aArticle['journal_id']]; + $aJournal = Db::name('journal')->where($aWhere)->find(); + if(empty($aJournal)){ + return json_encode(array('status' => 6,'msg' => 'No journal information found' )); + } + + //查询审稿人邮箱 + $aWhere = ['user_id' => $iReviewerId,'state' => 0,'email' => ['<>','']]; + $aUser = Db::name('user')->field('user_id,email,realname,account')->where($aWhere)->find(); + //收件人 + $sEmail = empty($aUser['email']) ? '' : $aUser['email'];//'1172937051@qq.com';//'publisher@tmrjournals.com';//'1172937051@qq.com';// + if(empty($sEmail)){ + return json_encode(['status' => 7,'msg' => "Reviewer's email information not found"]); + } + + //处理发邮件 + //获取邮件模版 + $aEmailConfig= empty($this->aEmailConfig[$sEmailType]) ? [] : $this->aEmailConfig[$sEmailType]; + if(empty($aEmailConfig)){ + return json_encode(['status' => 8,'msg' => "Email template not obtained"]); + } + //邮件内容 + $aSearch = [ + '{accept_sn}' => empty($aArticle['accept_sn']) ? '' : $aArticle['accept_sn'],//accept_sn + '{article_title}' => empty($aArticle['title']) ? '' : $aArticle['title'],//文章标题 + '{abstrart}' => empty($aArticle['abstrart']) ? '' : $aArticle['abstrart'],//文章摘要 + '{journal_title}' => empty($aJournal['title']) ? '' : $aJournal['title'],//期刊名 + '{journal_issn}' => empty($aJournal['issn']) ? '' : $aJournal['issn'], + '{journal_email}' => empty($aJournal['email']) ? '' : $aJournal['email'], + '{website}' => empty($aJournal['website']) ? '' : $aJournal['website'], + ]; + //用户名 + $realname = empty($aUser['account']) ? '' : $aUser['account']; + $realname = empty($aUser['realname']) ? $realname : $aUser['realname']; + $aSearch['{realname}'] = $realname; + //用户账号 + $aSearch['{account}'] = empty($aUser['account']) ? '' : $aUser['account']; + //审稿链接 + $oArticle = new \app\api\controller\Article; + $aSearch['{creatLoginUrlForreviewer}'] = $oArticle->creatLoginUrlForreviewer(['user_id' => $iReviewerId],$aReviewer['art_rev_id']); + if($aReviewer['state'] == 5){ + $aSearch['{creatRejectUrlForReviewer}'] = $oArticle->creatRejectUrlForReviewer(['user_id' => $iReviewerId],$aReviewer['art_rev_id']); + + } + //邀请时间 + $aSearch['{invite_time}'] = empty($aReviewer['ctime']) ? '' : $this->timestampToEnglishDate($aReviewer['ctime']); + //同意审稿截止时间 + $iAgreeTime = empty($aReviewer['agree_review_time']) ? 0 : $aReviewer['agree_review_time']; + $iAgreeTime = empty($iAgreeTime) ? '' : intval($iAgreeTime + (14 * $this->iDayTime));//十四天 + $aSearch['{agree_deadline}'] = empty($iAgreeTime) ? '' : $this->timestampToEnglishDate($iAgreeTime); + //邮件标题 + $title = str_replace(array_keys($aSearch), array_values($aSearch),$aEmailConfig['email_subject']); + //邮件内容变量替换 + $content = str_replace(array_keys($aSearch), array_values($aSearch), $aEmailConfig['email_content']); + //判断标题和内容是否为空 + if(empty($title) || empty($content)){ + return json_encode(['status' => 9,'msg' => "The email content and title are empty"]); + } + //拼接样式 + $pre = \think\Env::get('emailtemplete.pre'); + $net = \think\Env::get('emailtemplete.net'); + $net1 = str_replace("{{email}}",trim($sEmail),$net); + $content=$pre.$content.$net1; + + //发送邮件 + $memail = empty($aJournal['email']) ? '' : $aJournal['email']; + $mpassword = empty($aJournal['epassword']) ? '' : $aJournal['epassword']; + //期刊标题 + $from_name = empty($aJournal['title']) ? '' : $aJournal['title']; + + //查询是否发送过邮件 + $aStateType = ['three' => 10,'five' => 11,'ten' => 12,'twelve' => 13]; + //邮件日志类型 + $iLogType = empty($aStateType[$sEmailType]) ? 0 : $aStateType[$sEmailType]; + $oReviewer = new \app\common\Reviewer; + $aEmailLog = ['article_id' => $iArticleId,'art_rev_id' => $aReviewer['art_rev_id'],'reviewer_id' => $iReviewerId,'type' => $iLogType,'is_success' => 1]; + $aLog = DB::name('email_reviewer')->field('id')->where($aEmailLog)->find(); + $sMsg = '邮件已发送'; + if(empty($aLog)){ + $aResult = sendEmail($sEmail,$title,$from_name,$content,$memail,$mpassword); + $iStatus = empty($aResult['status']) ? 1 : $aResult['status']; + $iIsSuccess = 2; + $sMsg = empty($aResult['data']) ? '失败' : $aResult['data']; + if($iStatus == 1){ + $iIsSuccess = 1; + $sMsg = '成功'; + } + //记录邮件发送日志 + $aEmailLog['email'] = $sEmail; + $aEmailLog['content'] = $content; + $aEmailLog['create_time'] = time(); + $aEmailLog['is_success'] = $iIsSuccess; + $aEmailLog['msg'] = $sMsg; + //添加邮件发送日志 + $iId = $oReviewer->addLog($aEmailLog); + } + return json_encode(['status' => 1,'msg' => $sMsg]); + } + /** + * 获取审稿状态的文章 + * @return void + */ + private function getArticle(){ + + //查询送审中的文章 + $aWhere = ['state' => 2]; + $aArticle = Db::name('article')->field('article_id')->where($aWhere)->select(); + if(empty($aArticle)){ + return ['status' => 2,'msg' => 'No articles requiring review were found']; + } + + //数据返回 + return ['status' => 1,'msg' => 'Successfully obtained information','data' => $aArticle]; + } + private function timestampToEnglishDate($timestamp) { + //验证时间戳有效性 + if (!is_numeric($timestamp) || $timestamp < 0) { + return ''; + } + + //设置本地化(确保月份为英文全称,兼容Linux/Windows) + $locale = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' + ? 'English_United States.1252' + : 'en_US.UTF-8'; + setlocale(LC_TIME, $locale); + + //格式化:d=日(无前导零)、F=英文月份全称、Y=四位年 strftime的%e在Linux下是无前导零的日,Windows用%#d + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $dateStr = strftime('%#d %B %Y', $timestamp); // Windows + } else { + $dateStr = strftime('%e %B %Y', $timestamp); // Linux/Mac + } + return trim($dateStr); + } + /** + * + * 格式化信息输出 + * + * @access public + * @return void + * @date 2018.09.28 + * @param $[message] [<显示信息>] + * @param $[status] [<输出信息1成功,2失败>] + */ + private function showMessage($message, $status = 1) { + if ($status == 1) { + echo "[SUCCESS]"; + } else { + echo "[ERROR]"; + } + echo date("Y-m-d H:i:s") . " " . $message . "\n"; + } + + +} From f28719058d8e601018b78b79e16937a1657c762e Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 31 Dec 2025 15:07:49 +0800 Subject: [PATCH 43/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Article.php | 98 +++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index d0cdd925..308fc5f7 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -1577,13 +1577,12 @@ class Article extends Base $update_data['rstime'] = time(); } $this->article_obj->where($where_article)->update($update_data); - //拒稿或者录用 - 发送审稿意见 if ($article_info['journal_id'] == 1 && ($data['state'] == 3 || $data['state'] == 5)) { $this->sendEmailToReviewer($data['articleId'], $data['state']); } - //为预接收的文章生成production实例 + //为预接收的文章生成production实例,处理收费 if ($data['state'] == 6) { $this->addProductionEx($data['articleId']); $this->addArticleMainEx($data['articleId']); @@ -1591,6 +1590,9 @@ class Article extends Base //如果是免费的期刊文章,那么直接变成付款完成 if ($journal_info['fee'] == 0 || $article_info['ctime'] < 1735660800) { $this->article_obj->where("article_id", $article_info['article_id'])->update(["is_buy" => 1]); + }else{//如果期刊收费,那么文章费用初始化 + + $this->article_obj->where("article_id", $article_info['article_id'])->update(["fee" => $journal_info['fee']]); } } } @@ -4656,6 +4658,10 @@ class Article extends Base } } + public function phpIn(){ + phpinfo(); + } + public function up_approval_file() { $file = request()->file('articleApproval'); @@ -6234,4 +6240,92 @@ class Article extends Base } return ['status' => 1,'msg' => 'success']; } + + /** + * 添加图片版权声明文件-编辑 + */ + public function addFigureCopyrightForEditor($aParam = []){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + + //获取文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select the article']); + } + //用户ID + $iUserId = empty($aParam['user_id']) ? '' : $aParam['user_id']; + if(empty($iUserId)){ + return json_encode(['status' => 2,'msg' => 'Please select author']); + } + // //是否上传文件 + // $is_figure_copyright = empty($aParam['is_figure_copyright']) ? 3 : $aParam['is_figure_copyright']; + // if($is_figure_copyright == 3){ + // return json_encode(['status' => 3,'msg' => 'Please check whether to upload the figure copyright statement']); + // } + + //获取操作人信息 + $aWhere = ['user_id' => $iUserId,'state' => 0]; + $aUser = Db::name('user')->field('account')->where($aWhere)->find(); + if(empty($aUser)){ + return json_encode(['status' => 5,'msg' => 'No operator information found']); + } + //获取文章信息 + $aWhere = ['article_id' => $iArticleId]; + $aArticle = Db::name('article')->field('article_id,state,editor_id,journal_id')->where($aWhere)->find(); + if(empty($aArticle)){ + return json_encode(['status' => 6,'msg' => 'The article does not exist']); + } + //判断是否有权限操作 + if($iUserId != $aArticle['editor_id']){ + return json_encode(['status' => 7,'msg' => 'Unauthorized operation']); + } + //查询期刊负责编辑 + $aWhere = ['journal_id' => $aArticle['journal_id'],'state' => 0]; + $aJournal = Db::name('journal')->field('editor_id')->where($aWhere)->find(); + if(empty($aJournal)){ + return json_encode(['status' => 7,'msg' => 'No article or journal information found']); + } + if($iUserId != $aJournal['editor_id']){ + return json_encode(['status' => 7,'msg' => 'No operation permission']); + } + //图片路径 + // if($is_figure_copyright == 1){ + //文件地址 + $sUrl = empty($aParam['url']) ? '' : $aParam['url']; + if(empty($sUrl)){ + return json_encode(['status' => 4,'msg' => 'Please choose to upload the file']); + } + // } + + //文件类型 + $sTypeName = 'figurecopyright'; + $sUserAccount = empty($aUser['account']) ? '' : $aUser['account']; + Db::startTrans(); + if(!empty($sUrl)){ + //验证文件是否上传 + $aWhere = ['file_url' => $sUrl,'state' => 0,'article_id' => $iArticleId,'type_name' => $sTypeName]; + $result = Db::name('article_file')->where($aWhere)->find(); + if(empty($result)){ + $aInsert['article_id'] = $iArticleId; + $aInsert['user_id'] = $iUserId; + $aInsert['username'] = $sUserAccount; + $aInsert['file_url'] = $sUrl; + $aInsert['type_name'] = $sTypeName; + $aInsert['ctime'] = time(); + $result = Db::name('article_file')->insertGetId($aInsert); + } + } + // //更新文章内容 + // $aArticleUpdate = ['is_figure_copyright' => $is_figure_copyright]; + // if(!empty($aArticleUpdate)){ + // $aWhere = ['article_id' => $iArticleId]; + // $update_result = Db::name('article')->where($aWhere)->limit(1)->update($aArticleUpdate); + // } + //操作日志 + $aLog = ['article_id' => $iArticleId,'user_id' => $iUserId,'type' => 7,'create_time' => time(),'content' => $sUserAccount . ':Operating the article copyright statement','is_view' => 1]; + Db::name('user_act_log')->insert($aLog); + Db::commit(); + return json_encode(['status' => 1,'msg' => 'success']); + } } From 799da4910b982b6c063c0aad259f675ddca9d7f3 Mon Sep 17 00:00:00 2001 From: chengxl Date: Sun, 4 Jan 2026 14:19:53 +0800 Subject: [PATCH 44/53] =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E6=9C=9F=E5=88=8A=E4=BB=B7=E6=A0=BC=E8=B0=83=E7=94=A8=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E9=87=8C=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Article.php | 27 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index 308fc5f7..140a3683 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -3793,25 +3793,34 @@ class Article extends Base //发送邮件给作者,表示感谢 $tt_t = 'Dear Dr. ' . ($user_res['realname'] == '' ? $user_res['account'] : $user_res['realname']) . ',

'; - if ($article_info['journal_id'] == 1) { + //期刊费用 + $iJournalFee = empty($journal_info['fee']) ? 0 : $journal_info['fee']; + $iNowFee = intval($iJournalFee * 100); + //收费地址 + $aApc = [1 => 'https://www.tmrjournals.com/apc/',10 => 'https://www.tmrjournals.com/mdm/apc/',23 => 'https://www.tmrjournals.com/bmec/apc/']; + $sApc = empty($aApc[$article_info['journal_id']]) ? '' : $aApc[$article_info['journal_id']]; + //邮件主题 + $sSubject = 'Manuscript Submission Confirmation – '.'['.$sArticleSn.'] '.$journal_info['title']; + + if ($iNowFee > 0) { $tt1 = 'Thank you for submitting your manuscript entitled "' . $article_info['title'] . '". Your submission has been assigned the following tracking number: ' . $sArticleSn . '. We will be in touch again as soon as we have reached a decision. You may check on the status of this manuscript in the Submission System. Please quote the tracking number in any communication.

'; - $tt1 .= 'The following information was acknowledged during the submission process: Traditional Medicine Research is an open access journal that charges a publication fee of 600 USD for accepted manuscripts (click here for details).

'; - $tt1 .= 'This e-mail simply acknowledges receipt of your submission. If the editors decide for editorial reasons that the paper is unsuitable for publication in Traditional Medicine Research, you will be informed as soon as possible.

'; - $tt1 .= 'If you encounter any problems, please contact tmr@tmrjournals.com.

Thank you for choosing to submit your manuscript to Traditional Medicine Research.


'; + $tt1 .= 'The following information was acknowledged during the submission process: '.''.$journal_info['title'].''.' is an open access journal that charges a publication fee of '.$iJournalFee.' USD for accepted manuscripts (click here for details).

'; + $tt1 .= 'This e-mail simply acknowledges receipt of your submission. If the editors decide for editorial reasons that the paper is unsuitable for publication in '.''.$journal_info['title'].''.', you will be informed as soon as possible.

'; + $tt1 .= 'If you encounter any problems, please contact '.$journal_info['email'].'.

Thank you for choosing to submit your manuscript to '.''.$journal_info['title'].''.'.


'; $tt1 .= 'Sincerely,
Editorial Office
'; - $tt1 .= $journal_info['title'] . '
'; + $tt1 .= ''.$journal_info['title'].'' . '
'; $tt1 .= 'Email: ' . $journal_info['email'] . '
'; $tt1 .= 'Website: ' . $journal_info['website'] . '
'; $tt1 .= '


If you have any questions, please contact us:
'; $tt1 .= 'Head of publication ethics
Dr. Dan Chen
TMR Publishing Group Limited Company, Auckland, New Zealand
Email: publication.ethics@tmrjournals.com'; } else { - $tt1 = 'Thank you for submitting your manuscript entitled "' . $article_info['title'] . '". Your submission has been assigned the following tracking number:' . $sArticleSn . '. We will be in touch again as soon as we have reached a decision. Please quote the tracking number in any communication. This e-mail simply acknowledges receipt of your submission. If the editors decide for editorial reasons that the paper is unsuitable for publication in ' . $journal_info['title'] . ', you will be informed as soon as possible.


'; + $tt1 = 'Thank you for submitting your manuscript entitled "' . $article_info['title'] . '". Your submission has been assigned the following tracking number:' . $sArticleSn . '. We will be in touch again as soon as we have reached a decision. Please quote the tracking number in any communication. This e-mail simply acknowledges receipt of your submission. If the editors decide for editorial reasons that the paper is unsuitable for publication in ' . ''.$journal_info['title'].'' . ', you will be informed as soon as possible.


'; $tt1 .= 'You may check on the status of this manuscript in the Submission System. If you encounter any problems, please contact ' . $journal_info['email'] . '.

'; - $tt1 .= 'Thank you for choosing to submit your manuscript to ' . $journal_info['title'] . '.


'; + $tt1 .= 'Thank you for choosing to submit your manuscript to ' . ''.$journal_info['title'].'' . '.


'; $tt1 .= 'Sincerely,
Editorial Office
'; $tt1 .= 'Subscribe to this journal
'; - $tt1 .= $journal_info['title'] . '
'; + $tt1 .= ''.$journal_info['title'].'' . '
'; $tt1 .= 'Email: ' . $journal_info['email'] . '
'; $tt1 .= 'Website: ' . $journal_info['website'] . '
'; $tt1 .= '
If you have any questions, please contact us:
'; @@ -3819,7 +3828,7 @@ class Article extends Base } - sendEmail($user_res['email'], $journal_info['title'], $journal_info['title'], $tt_t . $tt1, $journal_info['email'], $journal_info['epassword']); + sendEmail($user_res['email'], $sSubject, $journal_info['title'], $tt_t . $tt1, $journal_info['email'], $journal_info['epassword']); foreach ($author_email as $v) { $cache_str = 'Dear Dr. ' . $v['name'] . ',

'; From e5b72e8a283c26d74b53a507301ab82d736fda06 Mon Sep 17 00:00:00 2001 From: chengxl Date: Sun, 4 Jan 2026 15:03:40 +0800 Subject: [PATCH 45/53] =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E6=9C=9F=E5=88=8A=E4=BB=B7=E6=A0=BC=E8=B0=83=E7=94=A8=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E9=87=8C=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Article.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index 140a3683..95726f2d 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -3812,7 +3812,7 @@ class Article extends Base $tt1 .= 'Email: ' . $journal_info['email'] . '
'; $tt1 .= 'Website: ' . $journal_info['website'] . '
'; $tt1 .= '
If you have any questions, please contact us:
'; - $tt1 .= 'Head of publication ethics
Dr. Dan Chen
TMR Publishing Group Limited Company, Auckland, New Zealand
Email: publication.ethics@tmrjournals.com'; + $tt1 .= 'Head of publication ethics
Dr. Dan Chen
TMR Publishing Group Limited Company, Auckland, New Zealand
Email: publisher@tmrjournals.com'; } else { $tt1 = 'Thank you for submitting your manuscript entitled "' . $article_info['title'] . '". Your submission has been assigned the following tracking number:' . $sArticleSn . '. We will be in touch again as soon as we have reached a decision. Please quote the tracking number in any communication. This e-mail simply acknowledges receipt of your submission. If the editors decide for editorial reasons that the paper is unsuitable for publication in ' . ''.$journal_info['title'].'' . ', you will be informed as soon as possible.


'; @@ -3824,7 +3824,7 @@ class Article extends Base $tt1 .= 'Email: ' . $journal_info['email'] . '
'; $tt1 .= 'Website: ' . $journal_info['website'] . '
'; $tt1 .= '
If you have any questions, please contact us:
'; - $tt1 .= 'Head of publication ethics
Dr. Dan Chen
TMR Publishing Group Limited Company, Auckland, New Zealand
Email: publication.ethics@tmrjournals.com'; + $tt1 .= 'Head of publication ethics
Dr. Dan Chen
TMR Publishing Group Limited Company, Auckland, New Zealand
Email: publisher@tmrjournals.com'; } From e09e407f58d02d4e4d49116861ae0df6df62c74e Mon Sep 17 00:00:00 2001 From: chengxl Date: Sun, 4 Jan 2026 17:17:55 +0800 Subject: [PATCH 46/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E6=94=B6=E8=B4=B9=E9=A1=B5=E9=9D=A2=E5=9C=B0=E5=9D=80=E5=8F=8A?= =?UTF-8?q?=E6=94=B6=E8=B4=B9=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Journal.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/application/api/controller/Journal.php b/application/api/controller/Journal.php index 897a9e26..16948dab 100644 --- a/application/api/controller/Journal.php +++ b/application/api/controller/Journal.php @@ -345,6 +345,16 @@ class Journal extends Base { $update['fee'] = $data['fee']; } + //新增字段 收费说明及链接 20260104 start + //收费链接 + if(isset($data['apc_url'])){ + $update['apc_url'] = trim($data['apc_url']); + } + //收费说明 + if(isset($data['apc_content'])){ + $update['apc_content'] = trim($data['apc_content']); + } + //新增字段 收费说明及链接 20260104 end $update['scope'] = isset($data['scope'])?trim($data['scope']):""; $this->journal_obj->where('journal_id',$data['journal_id'])->update($update); From 537026636d29fc36f1a6cb9525e0c157b351f2bc Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 5 Jan 2026 10:57:13 +0800 Subject: [PATCH 47/53] =?UTF-8?q?=E7=A8=BF=E4=BB=B6=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E7=BB=99=E4=BD=9C=E8=80=85=E5=8F=91=E9=80=81=E9=82=AE=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/job/SendAuthorEmail.php | 89 +++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 application/api/job/SendAuthorEmail.php diff --git a/application/api/job/SendAuthorEmail.php b/application/api/job/SendAuthorEmail.php new file mode 100644 index 00000000..a1d7c0c3 --- /dev/null +++ b/application/api/job/SendAuthorEmail.php @@ -0,0 +1,89 @@ +oQueueJob = new QueueJob; + $this->QueueRedis = QueueRedis::getInstance(); + } + + public function fire(Job $job, $data) + { + //任务开始判断 + $this->oQueueJob->init($job); + + // 获取 Redis 任务的原始数据 + $rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody(); + $jobData = empty($rawBody) ? [] : json_decode($rawBody, true); + $jobId = empty($jobData['id']) ? 'unknown' : $jobData['id']; + + $this->oQueueJob->log("-----------队列任务开始-----------"); + $this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}"); + + try { + + // 验证任务数据完整性 + // 获取文章ID + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + //作者邮箱 + $email = empty($data['email']) ? '' : $data['email']; + //邮件主题 + $subject = empty($data['subject']) ? '' : $data['subject']; + //发送来源 + $title = empty($data['title']) ? '' : $data['title']; + $subject = empty($subject) ? $title : $subject; + //邮件内容 + $content = empty($data['content']) ? '' : $data['content']; + //邮箱 + $temail = empty($data['temail']) ? '' : $data['temail']; + //密码 + $tpassword = empty($data['tpassword']) ? '' : $data['tpassword']; + //邮件类型 + $type = empty($data['type']) ? 1 : $data['type']; + if (empty($iArticleId) || empty($email)) { + $this->oQueueJob->log("无效的article_id/email,删除任务"); + $job->delete(); + return; + } + + // 生成唯一任务标识 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}:{$email}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + // 执行核心任务-发送邮件 + $aResult = sendEmail($email,$subject,$title,$content,$temail,$tpassword); + $iStatus = empty($aResult['status']) ? 1 : $aResult['status']; + $iIsSuccess = 2; + $sMsg = empty($aResult['data']) ? '失败' : $aResult['data']; + if($iStatus == 1){ + $iIsSuccess = 1; + $sMsg = '成功'; + } + // 更新完成标识 + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie, $sRedisValue); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}"); + + } catch (RuntimeException $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (LogicException $e) { + $this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job); + } catch (Exception $e) { + $this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job); + } finally { + $this->oQueueJob->finnal(); + } + } +} \ No newline at end of file From ab69b9d99d42b3b5dc9b15f28b72cf0063c060f1 Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 5 Jan 2026 11:23:06 +0800 Subject: [PATCH 48/53] =?UTF-8?q?=E7=A8=BF=E4=BB=B6=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E7=BB=99=E4=BD=9C=E8=80=85=E9=82=AE=E4=BB=B6=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Article.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index 95726f2d..82575497 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -3797,8 +3797,10 @@ class Article extends Base $iJournalFee = empty($journal_info['fee']) ? 0 : $journal_info['fee']; $iNowFee = intval($iJournalFee * 100); //收费地址 - $aApc = [1 => 'https://www.tmrjournals.com/apc/',10 => 'https://www.tmrjournals.com/mdm/apc/',23 => 'https://www.tmrjournals.com/bmec/apc/']; - $sApc = empty($aApc[$article_info['journal_id']]) ? '' : $aApc[$article_info['journal_id']]; + $sApc = empty($journal_info['apc_url']) ? '' : $journal_info['apc_url']; + // $aApc = [1 => 'https://www.tmrjournals.com/apc/',10 => 'https://www.tmrjournals.com/mdm/apc/',23 => 'https://www.tmrjournals.com/bmec/apc/']; + // $sApcInfo = empty($aApc[$article_info['journal_id']]) ? '' : $aApc[$article_info['journal_id']]; + // $sApc = empty($sApc) ? $sApcInfo : $sApc; //邮件主题 $sSubject = 'Manuscript Submission Confirmation – '.'['.$sArticleSn.'] '.$journal_info['title']; @@ -3837,7 +3839,12 @@ class Article extends Base $maidata['content'] = $cache_str . $tt1; $maidata['tmail'] = $journal_info['email']; $maidata['tpassword'] = $journal_info['epassword']; - Queue::push('app\api\job\mail@fire', $maidata, "tmail"); + //给文章作者发送邮件主题调整 20250105 start + $maidata['subject'] = empty($sSubject) ? '' : $sSubject;//邮件主题 + $maidata['article_id'] = empty($data['article_id']) ? 0 : $data['article_id'];//文章ID + // Queue::push('app\api\job\mail@fire', $maidata, "tmail"); + Queue::push('app\api\job\SendAuthorEmail@fire', $maidata, "SendAuthorEmail"); + //给文章作者发送邮件主题调整 20250105 end } //增加用户操作log From 972331c24e3fdc69b865a37fe8aa0d7c4614a40f Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 6 Jan 2026 10:58:33 +0800 Subject: [PATCH 49/53] =?UTF-8?q?=E7=A8=BF=E4=BB=B6=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E6=98=AF=E5=90=A6=E5=AD=98=E5=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Article.php | 39 +++++++++++++++----------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index 82575497..fb29a794 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -3428,6 +3428,17 @@ class Article extends Base return jsonError("Submission failed"); } $article_id = $data['article_id']; + + //查询标题是否存在 + $oArticle = new \app\common\Article; + $aCheckTitle = $oArticle->checkTitle(['title' => $data['title'],'article_id' => $article_id,'user_id' => $data['user_id']]); + $iStatus = empty($aCheckTitle['status']) ? -1 : $aCheckTitle['status']; + $sMsg = empty($aCheckTitle['msg']) ? '': $aCheckTitle['msg']; + $iDraftId = empty($aCheckTitle['draft_id']) ? 0 : $aCheckTitle['draft_id']; + if($iStatus != 1){ + return json(['code' => $iStatus, 'msg' => $sMsg,'draft_id' => $iDraftId]); + } + //添加文章基础信息 if ($data['article_id'] == 0) { $checkArticle = $this->article_obj->where("title", trim($data['title']))->select(); @@ -4616,20 +4627,10 @@ class Article extends Base $where['editor_id'] = $uidres[0]; } $where['state'] = 0; - $list = $this->journal_obj->where($where)->select(); - - // //获取期刊封面 chengxiaoling 20251027 start - // if(!empty($list)){ - // $aParam = ['issn' => array_column($list, 'issn')]; - // $sUrl = 'http://journalapi.tmrjournals.com/public/index.php/'; - // $sUrl = $sUrl."api/Supplementary/getJournal"; - // $aResult = object_to_array(json_decode(myPost1($sUrl,$aParam),true)); - // $aResult = empty($aResult['data']) ? [] : array_column($aResult['data'], 'icon','issn'); - // foreach ($list as $key => $value) { - // $list[$key]['journal_icon'] = empty($aResult[$value['issn']]) ? '' : $aResult[$value['issn']]; - // } - // } - // //获取期刊封面 chengxiaoling 20251027 end + $list = $this->journal_obj + ->field("journal_id,title,issn,editorinchief,zname,abbr,alias,editor_id,staff_id,level, + jabbr,jour_num,jour_price,email,cycle,scope,fee,kfen,website,journal_icon,apc_url,state") + ->where($where)->select(); return json($list); } @@ -4639,7 +4640,10 @@ class Article extends Base public function getJournalOutLx() { $editorid = $this->request->post('editor_id'); - $list = $this->journal_obj->where('editor_id', $editorid)->where('journal_id', 'not in', '2,3,15')->select(); + $list = $this->journal_obj + ->field("journal_id,title,issn,editorinchief,zname,abbr,alias,editor_id,staff_id,level, + jabbr,jour_num,jour_price,email,cycle,scope,fee,kfen,website,journal_icon,apc_url,state") + ->where('editor_id', $editorid)->where('journal_id', 'not in', '2,3,15')->select(); return json($list); } @@ -4748,7 +4752,10 @@ class Article extends Base if (!$res) { return jsonError('no this user!'); } - $journals = $this->journal_obj->where('editor_id', $res['user_id'])->where('journal_id', 'in', '2,3,15')->select(); + $journals = $this->journal_obj + ->field("journal_id,title,issn,editorinchief,zname,abbr,alias,editor_id,staff_id,level, + jabbr,jour_num,jour_price,email,cycle,scope,fee,kfen,website,journal_icon,apc_url,state") + ->where('editor_id', $res['user_id'])->where('journal_id', 'in', '2,3,15')->select(); $re['journals'] = $journals; return jsonSuccess($re); } From 06e8c3e69b9499387fe5b5cf4245db0508664c32 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 6 Jan 2026 13:12:55 +0800 Subject: [PATCH 50/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E9=87=8D=E5=A4=8D=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/Article.php | 74 +++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/application/common/Article.php b/application/common/Article.php index f731f6e7..005dbcf1 100644 --- a/application/common/Article.php +++ b/application/common/Article.php @@ -84,7 +84,8 @@ class Article } $iAiArticleId = $aAiArticle['ai_article_id']; //必填参数验证 - $aFields = ['article_id','article_type','media_type','journal_id','journal_issn','title_english','covered','title_chinese','digest','research_background','discussion','research_method','conclusion','overview','summary','is_generate']; + // $aFields = ['article_id','article_type','media_type','journal_id','journal_issn','title_english','covered','title_chinese','digest','research_background','discussion','research_method','conclusion','overview','summary','is_generate']; + $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 = []; @@ -393,4 +394,75 @@ class Article return json_encode(['status' => 1,'msg' => 'success']); } + /** + * 验证标题是否重复 + */ + public function checkTitle($aParam = []){ + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + //文章标题 + $sTitle = empty($aParam['title']) ? '' : $aParam['title']; + if(empty($sTitle)){ + return ['status' => 2,'msg' => 'The article title is empty']; + } + //作者ID + $iUserId = empty($aParam['user_id']) ? 0 : $aParam['user_id']; + //查询标题是否存在 + $aWhere = ['title' => trim($sTitle)]; + $aArticle = Db::name('article')->field('article_id,user_id,state,accept_sn')->where($aWhere)->order('article_id desc')->select(); + if(empty($aArticle)){ + return ['status' => 1,'msg' => 'No articles with relevant titles found']; + } + + //处理数据 + $aFlag = []; + $iDraftId = 0; + foreach ($aArticle as $key => $value) { + if(!empty($iArticleId) && $value['article_id'] == $iArticleId){ + continue; + } + //稿件提交者 + $iArticleUserId = empty($value['user_id']) ? 0 : $value['user_id']; + //稿件状态 + $iState = empty($value['state']) ? '' : $value['state']; + //稿号 + $sAcceptSn = empty($value['accept_sn']) ? '' : $value['accept_sn']; + if($iState == 3 && stripos($sAcceptSn, 'Draft') === 0){//自己删除草稿箱稿件 + continue; + } + if($iArticleUserId != $iUserId){//与其他作者的文章标题重名 + $aFlag[] = 2; + break; + }else{ + if($iState == 3){//编辑已拒绝文章 + $aFlag[] = 3; + break; + }elseif($iState == -1){//其他状态 + $aFlag[] = 4; + $iDraftId = $value['article_id']; + }else{ + $aFlag[] = 5; + break; + } + } + } + //返回数据 + $aFlag = empty($aFlag) ? [] : array_unique($aFlag); + $sMsg = ''; + if(empty($aFlag)){ + return ['status' => 1,'msg' => 'Enter the data processing flow']; + } + if(in_array(2, $aFlag) || in_array(5, $aFlag)){ + $sMsg = 'Please note that the manuscript with the same title is already under processing. Do not submit it again.'; + return ['status' => 20,'msg' => $sMsg]; + }elseif(in_array(3, $aFlag)){ + $sMsg = 'Please note that the manuscript with the same title has already been rejected. For more details, please contact the journal.'; + return ['status' => 21,'msg' => $sMsg]; + }elseif(in_array(4, $aFlag)){ + $sMsg = 'Please notice that the manuscript with the same title is already in the draft status. please click here'; + return ['status' => 22,'msg' => $sMsg,'draft_id' => $iDraftId]; + }else{ + return ['status' => 1,'msg' => 'Enter the data processing flow']; + } + } } From 7a629434b841903ece5d90274ce52545b7655ce6 Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 7 Jan 2026 09:25:04 +0800 Subject: [PATCH 51/53] =?UTF-8?q?=E9=AA=8C=E8=AF=81=E6=AD=A5=E9=AA=A4?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=96=B9=E6=B3=95=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Article.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index fb29a794..e0b292e9 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -5445,6 +5445,17 @@ class Article extends Base } } + //查询标题是否存在 + $oArticle = new \app\common\Article; + $aCheckTitle = $oArticle->checkTitle(['title' => $aArticle['title'],'article_id' => $iArticleId,'user_id' => $iUserId]); + $iStatus = empty($aCheckTitle['status']) ? -1 : $aCheckTitle['status']; + $sMsg = empty($aCheckTitle['msg']) ? '': $aCheckTitle['msg']; + $iDraftId = empty($aCheckTitle['draft_id']) ? 0 : $aCheckTitle['draft_id']; + if($iStatus != 1){ + $iFirstStatus = 2; + $sFirstMsg = 'Step 1: '.$sMsg; + } + //判断伦理 if(!empty($aArticle['approval']) && $aArticle['approval'] == 1 && empty($aArticle['approval_file'])){ $iFirstStatus = 2; From 66f914bd35b97b81347647bc4bc084f0e9809cda Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 14 Jan 2026 09:43:56 +0800 Subject: [PATCH 52/53] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index 0d8bf45f..b37b51bd 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -659,9 +659,6 @@ class Workbench extends Base return json_encode(['status' => 7,'msg' => 'The article is not in the review status']); } - //查询期刊信息 - $aWhere = ['journal_id' => $aArticle['article_id'],'state' => 0]; - $aJournal = Db::name('journal')->field('title as journal_name,website')->find(); //查询期刊信息 if(empty($aArticle['journal_id'])){ return json_encode(array('status' => 8,'msg' => 'The article is not associated with a journal' )); @@ -721,7 +718,8 @@ class Workbench extends Base //发邮件 //邮箱 - $email = empty($aUser[$iUserId]['email']) ? '' : $aUser[$iUserId]['email']; + // $email = empty($aUser[$iUserId]['email']) ? '' : $aUser[$iUserId]['email']; + $email = empty($aJournal['email']) ? '' : $aJournal['email']; if(empty($email)){ return json_encode(['status' => 8,'msg' => 'Edit email as empty']); } @@ -793,24 +791,21 @@ class Workbench extends Base return json_encode(['status' => 7,'msg' => 'The article is not in the review status']); } - //查询期刊信息 - $aWhere = ['journal_id' => $aArticle['article_id'],'state' => 0]; - $aJournal = Db::name('journal')->field('title as journal_name,website')->find(); //查询期刊信息 if(empty($aArticle['journal_id'])){ return json_encode(array('status' => 8,'msg' => 'The article is not associated with a journal' )); } - $aWhere = ['state' => 0,'journal_id' => $aArticle['journal_id']]; + $aWhere = ['state' => 0,'journal_id' => 23]; $aJournal = Db::name('journal')->where($aWhere)->find(); if(empty($aJournal)){ return json_encode(array('status' => 9,'msg' => 'No journal information found' )); } //判断编辑的操作权限 - $iEditorId = empty($aJournal['editor_id']) ? 0 : $aJournal['editor_id']; - if($iEditorId != $iUserId){ - return json_encode(array('status' => 10,'msg' => 'This article is not authorized for operation under the journal you are responsible for' )); - } + // $iEditorId = empty($aJournal['editor_id']) ? 0 : $aJournal['editor_id']; + // if($iEditorId != $iUserId){ + // return json_encode(array('status' => 10,'msg' => 'This article is not authorized for operation under the journal you are responsible for' )); + // } //更新文章状态为邀请 $aWhere = ['art_rev_id' => $iArtRevId,'state' => 4]; From f64ca74b66f067bfcc5884fedc7c02cd0ff4fee9 Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 14 Jan 2026 09:46:20 +0800 Subject: [PATCH 53/53] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Workbench.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/api/controller/Workbench.php b/application/api/controller/Workbench.php index b37b51bd..5ccd66c1 100644 --- a/application/api/controller/Workbench.php +++ b/application/api/controller/Workbench.php @@ -795,7 +795,7 @@ class Workbench extends Base if(empty($aArticle['journal_id'])){ return json_encode(array('status' => 8,'msg' => 'The article is not associated with a journal' )); } - $aWhere = ['state' => 0,'journal_id' => 23]; + $aWhere = ['state' => 0,'journal_id' => $aArticle['journal_id']]; $aJournal = Db::name('journal')->where($aWhere)->find(); if(empty($aJournal)){ return json_encode(array('status' => 9,'msg' => 'No journal information found' ));