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