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被