diff --git a/application/api/controller/Aiarticle.php b/application/api/controller/Aiarticle.php index eecd549..b4520a8 100644 --- a/application/api/controller/Aiarticle.php +++ b/application/api/controller/Aiarticle.php @@ -264,7 +264,7 @@ class Aiarticle extends Base //是否查询作者 1是2否 $iSelectAuthor = empty($aParam['is_select_author']) ? 2 : $aParam['is_select_author']; //查询AI生成的文章内容 - $aWhere = ['is_delete' => 2,'is_generate' => 1]; + $aWhere = ['is_delete' => 2];//,'is_generate' => 1 if(!empty($iArticleId)){ $aWhere['article_id'] = $iArticleId; } @@ -292,7 +292,10 @@ class Aiarticle extends Base } } - return json_encode(['status' => 1,'msg' => 'success','data' => ['ai_article' => $aAiArticle,'ai_article_author' => $aAiAuthor]]); + //查询结论 + $aWhere = ['article_id' => $iArticleId,'is_delete' => 2]; + $aArticleResult = DB::name('ai_article_results')->field('id,title,content')->where($aWhere)->select(); + return json_encode(['status' => 1,'msg' => 'success','data' => ['ai_article' => $aAiArticle,'ai_article_author' => $aAiAuthor,'ai_article_results' => $aArticleResult]]); } /** * @title 处理作者数据 @@ -376,7 +379,14 @@ class Aiarticle extends Base //获取参数 $aParam = empty($aParam) ? $this->request->post() : $aParam; + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + //查询内容是否存在 + $aWhere = ['is_delete' => 2]; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']); + } //更新AI文章内容 $oArticle = new \app\common\Article; $aResult = json_decode($oArticle->updateAiArticle($aParam),true); @@ -385,66 +395,101 @@ class Aiarticle extends Base return json_encode($aResult); } + //结论信息 + $aResultInfo = empty($aParam['results']) ? [] : $aParam['results']; + $aResultInfo = is_array( $aResultInfo) ? $aResultInfo : json_decode($aResultInfo,true); //更新作者信息 $aAuthorList = empty($aParam['author_list']) ? []: $aParam['author_list']; $aAuthorList = is_array( $aAuthorList) ? $aAuthorList: json_decode($aAuthorList,true); - if(empty($aAuthorList)){ + if(empty($aAuthorList) && empty($aResultInfo)){ return json_encode($aResult); } //根据邮箱查询作者信息 - $aAuthorList = array_column($aAuthorList, null,'email'); - $aEmail = array_keys($aAuthorList); - if(empty($aEmail)){ - return json_encode($aResult); - } - $aUserParam = ['email' => ['in',$aEmail]]; - $aUserInfo = Db::name('user')->field('email,user_id')->where($aUserParam)->select(); - if(empty($aUserInfo)){ - return json_encode($aResult); - } + if(!empty($aAuthorList)){ + $aAuthorList = array_column($aAuthorList, null,'email'); + $aEmail = array_keys($aAuthorList); + if(empty($aEmail)){ + return json_encode($aResult); + } + $aUserParam = ['email' => ['in',$aEmail]]; + $aUserInfo = Db::name('user')->field('email,user_id')->where($aUserParam)->select(); + if(empty($aUserInfo)){ + return json_encode($aResult); + } - //查询用户附表 - $aUserId = array_column($aUserInfo, 'user_id'); - $aWhere = ['reviewer_id' => ['in',$aUserId]]; - $aUserReviewer = Db::name('user_reviewer_info')->where($aWhere)->column('reviewer_id'); - //更新用户信息 - Db::startTrans(); - foreach ($aUserInfo as $key => $value) { + //查询用户附表 + $aUserId = array_column($aUserInfo, 'user_id'); + $aWhere = ['reviewer_id' => ['in',$aUserId]]; + $aUserReviewer = Db::name('user_reviewer_info')->where($aWhere)->column('reviewer_id'); + //更新用户信息 + Db::startTrans(); + foreach ($aUserInfo as $key => $value) { - $aUser = empty($aAuthorList[$value['email']]) ? [] : $aAuthorList[$value['email']]; - if(empty($aUser)){ - continue; - } - //更新作者名字 - if(isset($aUser['author_name'])){ - $aUpdate = ['localname' => $aUser['author_name']]; - Db::name('user')->where('user_id',$value['user_id'])->update($aUpdate); - } - //更新作者简介和单位 - $aUpdateReviewer = []; - if(isset($aUser['technical'])){ - $aUpdateReviewer['technical'] = $aUser['technical']; - } - if(isset($aUser['introduction'])){ - $aUpdateReviewer['introduction'] = $aUser['introduction']; - } - if(isset($aUser['company'])){ - $aUpdateReviewer['company'] = $aUser['company']; - } - if(isset($aUpdateReviewer)){ - if(in_array($value['user_id'], $aUserReviewer)){//更新 - Db::name('user_reviewer_info')->where('reviewer_id',$value['user_id'])->limit(1)->update($aUpdateReviewer); + $aUser = empty($aAuthorList[$value['email']]) ? [] : $aAuthorList[$value['email']]; + if(empty($aUser)){ + continue; } - if(!in_array($value['user_id'], $aUserReviewer)){//插入 - $aUpdateReviewer['reviewer_id'] = $value['user_id']; - Db::name('user_reviewer_info')->insert($aUpdateReviewer); + //更新作者名字 + if(isset($aUser['author_name'])){ + $aUpdate = ['localname' => $aUser['author_name']]; + Db::name('user')->where('user_id',$value['user_id'])->update($aUpdate); + } + //更新作者简介和单位 + $aUpdateReviewer = []; + if(isset($aUser['technical'])){ + $aUpdateReviewer['technical'] = $aUser['technical']; + } + if(isset($aUser['introduction'])){ + $aUpdateReviewer['introduction'] = $aUser['introduction']; + } + if(isset($aUser['company'])){ + $aUpdateReviewer['company'] = $aUser['company']; + } + if(isset($aUpdateReviewer)){ + if(in_array($value['user_id'], $aUserReviewer)){//更新 + Db::name('user_reviewer_info')->where('reviewer_id',$value['user_id'])->limit(1)->update($aUpdateReviewer); + } + if(!in_array($value['user_id'], $aUserReviewer)){//插入 + $aUpdateReviewer['reviewer_id'] = $value['user_id']; + Db::name('user_reviewer_info')->insert($aUpdateReviewer); + } } } + Db::commit(); } - Db::commit(); - $aResult['data'] = $aParam; + //更新结论 + if(!empty($aResultInfo)){ + Db::startTrans(); + foreach ($aResultInfo as $key => $value) { + //记录ID + $iRecordId = empty($value['record_id']) ? 0 : $value['record_id']; + if(empty($iRecordId)){ + continue; + } + //标题 + $sTitle = empty($value['title']) ? '' : $value['title']; + //内容 + $sContent = empty($value['content']) ? '' : $value['content']; + if(empty($sTitle) && empty($sContent)){ + continue; + } + if(!empty($sTitle)){ + $aUpdate['title'] = $sTitle; + } + if(!empty($sContent)){ + $aUpdate['content'] = $sContent; + } + if(!empty($aUpdate)){ + $aUpdate['update_time'] = time(); + $aWhere = ['article_id' => $iArticleId,'id' => $iRecordId]; + $result = DB::name('ai_article_results')->where($aWhere)->limit(1)->update($aUpdate); + } + } + Db::commit(); + } + // $aResult['data'] = $aParam; return json_encode($aResult); } diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index 2a09f19..ad47d6b 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -536,6 +536,22 @@ class Article extends Base $article_res['majors'] = $majors; //查询文章作者信息 $author_res = $this->article_author_obj->where('article_id', $data['articleId'])->where('state', 0)->select(); + //查询作者scopus chengxiaoling 20260924 start + if(!empty($author_res)){ + $aEmail = array_unique(array_column($author_res, 'email')); + $aWhere = ['email' => ['in',$aEmail]]; + $aUserData = Db::name('user')->field('user_id,email,scopus_index,scopus_website,google_index,wos_index')->where($aWhere)->select(); + $aUserData = empty($aUserData) ? [] : array_column($aUserData, null,'email'); + foreach ($author_res as $key => $value) { + $aUserInfo = empty($aUserData[$value['email']]) ? [] : $aUserData[$value['email']]; + $author_res[$key]['scopus_index'] = isset($aUserInfo['scopus_index']) ? $aUserInfo['scopus_index'] : ''; + $author_res[$key]['scopus_website'] = empty($aUserInfo['scopus_website']) ? '' : $aUserInfo['scopus_website']; + $author_res[$key]['google_index'] = isset($aUserInfo['google_index']) ? $aUserInfo['google_index'] : ''; + $author_res[$key]['wos_index'] = isset($aUserInfo['wos_index']) ? $aUserInfo['wos_index'] : ''; + $author_res[$key]['user_id'] = empty($aUserInfo['user_id']) ? 0 : $aUserInfo['user_id']; + } + } + //查询作者scopus chengxiaoling 20260924 end //查询转投信息 $transfer_res = $this->article_transfer_obj->where('article_id', $data['articleId'])->select(); //查询建议转投详情 diff --git a/application/api/controller/Cronmonitor.php b/application/api/controller/Cronmonitor.php index 2ed6ece..80cba2b 100644 --- a/application/api/controller/Cronmonitor.php +++ b/application/api/controller/Cronmonitor.php @@ -76,6 +76,7 @@ class Cronmonitor extends Controller $aParam['date'] = $sDate; $aResult = object_to_array(json_decode(myPost($sUrl, $aParam))); $aContent = empty($aResult['data']) ? [] : $aResult['data']; + if(empty($aContent)){ $this->showMessage($aResult['msg'] ?? '接口异常',2); exit; @@ -91,23 +92,20 @@ class Cronmonitor extends Controller foreach ($aArticleCite as $key => $value) { $aCite[$value['article_id']][] = $value; } + //获取数据-文章通讯作者数据 $aArticleAuthor = empty($aContent['article_author']) ? [] : $aContent['article_author']; if(empty($aArticleAuthor)){ $this->showMessage('未查询到引用文章通讯作者数据:'.$sDate,2); exit; } + $aArticleId = array_unique(array_column($aArticleAuthor, 'article_id')); //获取数据-文章数据 $aArticle = empty($aContent['article']) ? [] : $aContent['article']; //期刊数据 $aJournal = empty($aContent['journal']) ? [] : $aContent['journal']; - //查询发送人信息 - $aEmail = array_column($aArticleAuthor, 'email'); - $aWhere = ['email' => ['in',$aEmail]]; - $aUser = Db::name('user')->where($aWhere)->column('email,realname,localname'); - //查询邮件发送日志 $aArticleCiteId = array_column($aArticleCite, 'article_cite_id'); $aWhere = ['article_cite_id' => ['in',$aArticleCiteId],'is_success' => 1]; @@ -123,28 +121,32 @@ class Cronmonitor extends Controller $aEmailCite= $this->aEmailConfig['cite']; //邮件发送日志记录数组 $aEmailLog = []; + $aSendEmail = [];//['2101' => ['mohammadalipour_z@yahoo.com']]; foreach ($aArticleAuthor as $key => $value) { - if(empty($value['email'])){//邮箱为空 + + //作者邮箱-发送信息 + $email = empty($value['email']) ? '' : $value['email']; + if(empty($email)){ continue; } + + //获取引用文章信息 $aArticleCite = empty($aCite[$value['article_id']]) ? [] : $aCite[$value['article_id']]; if(empty($aArticleCite)){ $this->showMessage('未查询到文章引用信息为空,文章ID:'.$value['article_id']."\n\n",2); continue; } - //数据组装-接收邮箱 - // $email = '1172937051@qq.com';//$value['email']; - $email = $value['email']; + + //判断同一个邮箱是否重复发送 + if(!empty($aSendEmail[$value['article_id']]) && in_array($email, $aSendEmail[$value['article_id']])){ + continue; + } //用户信息 - $aUserInfo = empty($aUser[$value['email']]) ? [] : $aUser[$value['email']]; - $realname = empty($aUserInfo['realname']) ? '' : $aUserInfo['realname']; - $localname = empty($aUserInfo['localname']) ? '' : $aUserInfo['localname']; - $realname = empty($realname) ? $localname : $realname; - $realname = empty($value['author_name']) ? $realname : $value['author_name']; - + $realname = empty($value['author_name']) ? $email : $value['author_name']; //邮件日志 $aEmailLogInfo = empty($aLog[$email]) ? [] : $aLog[$email]; + foreach ($aArticleCite as $val) { if(in_array($val['article_cite_id'], $aEmailLogInfo)){ $this->showMessage('文章标题为:'.$val['article_name'].'邮箱账号为:'.$email."已发送过邮件\n\n",2); @@ -194,13 +196,14 @@ class Cronmonitor extends Controller $aResult = sendEmail($email,$title,$from_name,$content,$memail,$mpassword); $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; $iIsSuccess = 2; - $sMsg = empty($aResult['data']) ? '失败' : $aResult['data']; + $sMsg = empty($aResult['msg']) ? '未知' : $aResult['msg']; if($iStatus == 1){ $iIsSuccess = 1; $sMsg = '成功'; } $this->showMessage('给邮箱:'.$email.'发送邮件-'.$sMsg."\n\n",2); $aEmailLog[] = ['article_id' => $val['article_id'],'article_cite_id' => $val['article_cite_id'],'email' => $email,'content' => $content,'create_tiem' => time(),'is_success' => $iIsSuccess,'journal_id' => $val['journal_id'],'journal_issn' => $aJournalInfo['issn'],'msg' => $sMsg]; + $aSendEmail[$value['article_id']][] = $email; } } diff --git a/application/api/controller/Finalreview.php b/application/api/controller/Finalreview.php index 3f100d6..ff7c965 100644 --- a/application/api/controller/Finalreview.php +++ b/application/api/controller/Finalreview.php @@ -1040,7 +1040,7 @@ class Finalreview extends Base } /** - * @title 获取文章的终审意见 + * @title 获取文章的审稿意见 * @param article_id */ public function getArticleFinalReview($aParam = []){ @@ -1052,18 +1052,81 @@ class Finalreview extends Base if(empty($iArticleId)){ return json_encode(['status' => 2,'msg' => 'Please select a article']); } - //查询审稿记录 + //查询文章审稿记录 + $aWhere = ['article_id' => $iArticleId,'state' => ['between',[1,3]]]; + $aArticleReviewer = Db::name('article_reviewer')->field('art_rev_id,state,ctime')->where($aWhere)->select(); + if(!empty($aArticleReviewer)){ + $aArtRevId = array_column($aArticleReviewer, 'art_rev_id'); + $aWhere = ['art_rev_id' => ['in',$aArtRevId],'state' => 0]; + //查询初审问卷 + $aQuestion = Db::name('article_reviewer_question')->field('art_rev_id,ctime,score,rated')->where($aWhere)->order('ctime asc')->select(); + $aQuestion = empty($aQuestion) ? [] : array_column($aQuestion, null,'art_rev_id'); + + //查询复审 + $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(); + if(!empty($aReviewerRepeat)){ + foreach ($aReviewerRepeat as $key => $value) { + $aReviewerRepeatLists[$value['art_rev_id']][] = $value; + } + } + foreach ($aArticleReviewer as $key => $value) { + $aQuestionData = empty($aQuestion[$value['art_rev_id']]) ? [] : $aQuestion[$value['art_rev_id']]; + $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['rated'] = empty($aQuestionData['rated']) ? 0 : $aQuestionData['rated']; + $aArticleReviewer[$key] = $value; + } + } + //查询终审-审稿记录 $aWhere = ['article_id' => $iArticleId,'state' => ['in',[1,2,3]]]; $aReviewerFinal = Db::name('article_reviewer_final')->field('id,state,suggest_for_editor,suggest_for_author,update_time,reviewer_id,is_anonymous')->where($aWhere)->select(); if(!empty($aReviewerFinal)){ //查询作者信息 - $aUserId = array_unique(array_column($aReviewerFinal, 'user_id')); + $aUserId = array_unique(array_column($aReviewerFinal, 'reviewer_id')); $aWhere = ['user_id' => ['in',$aUserId],'state' => 0]; $aUser = Db::name('user')->where($aWhere)->column('user_id,realname'); foreach ($aReviewerFinal as $key => $value) { $aReviewerFinal[$key]['realname'] = empty($aUser[$value['reviewer_id']]) ? '' : $aUser[$value['reviewer_id']]; } } + $aData = ['review' => $aArticleReviewer,'final_review' => $aReviewerFinal]; + return json_encode(['status' => 1,'msg' => 'success','data' => $aData]); + } + + /** + * @title 查询终审状态 + * @param record_id 记录ID + * @param state 状态 + */ + public function getById(){ + //获取参数 + $aParam = $this->request->post(); + //主键ID + $iId = empty($aParam['record_id']) ? 0 : $aParam['record_id']; + if(empty($iId)){ + return json_encode(['status' => 2,'msg' => 'Please select the review record']); + } + //参数验证-审稿人ID + $iReviewerId = empty($aParam['reviewer_id']) ? 0 : $aParam['reviewer_id']; + if(empty($iReviewerId)){ + return json_encode(['status' => 2,'msg' => 'Please select a reviewer']); + } + //稿件状态 + //判断审稿人是否是编委/主编/副主编 + $aWhere = ['user_id' => $iReviewerId,'state' => 0]; + $aBoard = Db::name('board_to_journal')->where($aWhere)->column('journal_id'); + if(empty($aBoard)){ + return json_encode(['status' => 2,'msg' => 'The reviewer role does not meet the review requirements']); + } + //查询审稿记录 + $aWhere = ['reviewer_id' => $iReviewerId,'id' => $iId]; + $aReviewerFinal = Db::name('article_reviewer_final')->field('id,article_id,state,reviewer_id')->where($aWhere)->find(); + if(empty($aReviewerFinal)){ + return json_encode(['status' => 3,'msg' => 'Review record does not exist or review has been completed']); + } return json_encode(['status' => 1,'msg' => 'success','data' => $aReviewerFinal]); } } diff --git a/application/api/controller/Production.php b/application/api/controller/Production.php index d3a6490..4974567 100644 --- a/application/api/controller/Production.php +++ b/application/api/controller/Production.php @@ -1606,6 +1606,23 @@ class Production extends Base return jsonError($rule->getError()); } $old_article_author_info = $this->production_article_author_obj->where('p_article_author_id', $data['p_article_author_id'])->find(); + + //判断邮箱是否重复 chengxiaoling 20250926 start + if(empty($old_article_author_info)){ + return jsonError('Author information does not exist'); + } + $iPArticleId = empty($old_article_author_info['p_article_id']) ? 0 : $old_article_author_info['p_article_id']; + $sEmail = empty($data['email']) ? '' : $data['email']; + $iAuthorId = empty($old_article_author_info['p_article_author_id']) ? 0 : $old_article_author_info['p_article_author_id']; + if(!empty($sEmail) && !empty($iPArticleId)){ + $aWhere = ['p_article_id' => $iPArticleId,'email' => $sEmail,'state' => 0,'p_article_author_id' => ['<>',$iAuthorId]]; + $aAuthor = $this->production_article_author_obj->field('p_article_author_id')->where($aWhere)->find(); + if(!empty($aAuthor)){ + return jsonError("Email has been bound by another author"); + } + } + //判断邮箱是否重复 chengxiaoling 20250926 end + $article_info = $this->article_obj->where('article_id', $old_article_author_info['article_id'])->find(); $updata['author_name'] = $article_info['journal_id'] == 21 ? trim($data['last_name']) . trim($data['first_name']) : trim($data['first_name']) . ' ' . trim($data['last_name']); $updata['first_name'] = trim($data['first_name']); @@ -1695,6 +1712,19 @@ class Production extends Base $p_info = $this->production_article_obj->where('p_article_id', $data['p_article_id'])->find(); $article_info = $this->article_obj->where('article_id', $p_info['article_id'])->find(); + + //判断邮箱是否重复 chengxiaoling 20250926 start + $iPArticleId = empty($data['p_article_id']) ? 0 : $data['p_article_id']; + $sEmail = empty($data['email']) ? '' : $data['email']; + if(!empty($sEmail) && !empty($iPArticleId)){ + $aWhere = ['p_article_id' => $iPArticleId,'email' => $sEmail,'state' => 0]; + $aAuthor = $this->production_article_author_obj->field('p_article_author_id')->where($aWhere)->find(); + if(!empty($aAuthor)){ + return jsonError("Email has been bound by another author"); + } + } + //判断邮箱是否重复 chengxiaoling 20250926 end + $insert['p_article_id'] = $data['p_article_id']; $insert['article_id'] = $p_info['article_id']; $insert['author_name'] = $article_info['journal_id'] == 21 ? trim($data['last_name']) . trim($data['first_name']) : trim($data['first_name']) . ' ' . trim($data['last_name']); diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php new file mode 100644 index 0000000..7d55dcd --- /dev/null +++ b/application/api/controller/Proofread.php @@ -0,0 +1,503 @@ +request->post() : $aParam; + $iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(array('status' => 2,'msg' => 'Please select an article' )); + } + //行号 + $iAmId = empty($aParam['am_id']) ? 0 : $aParam['am_id']; + //查询文章 + $aWhere = ['article_id' => $iArticleId]; + $oArticle = new Article; + $aArticle = json_decode($oArticle->get($aWhere),true); + $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; + if(empty($aArticle)){ + return json_encode(array('status' => 3,'msg' => 'No articles requiring review were found' )); + } + if($aArticle['state'] != 6){ + return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); + } + + //查询是否进行过校对 + $aProofReadWhere = ['article_id' => $iArticleId,'state' => 2]; + if(!empty($iAmId)){ + $aProofReadWhere['am_id'] = $iAmId; + } + $iCount = Db::name('article_proofread')->where($aProofReadWhere)->count(); + if(!empty($iCount)){ + return json_encode(array('status' => 5,'msg' => 'The article or paragraph has been proofread')); + } + + //查询文章内容 + $aWhere['type'] = 0; + $aWhere['content'] = ['<>','']; + $aWhere['state'] = 0; + if(!empty($iAmId)){ + $aWhere['am_id'] = $iAmId; + } + $aArticleMain = Db::table('t_article_main')->field('am_id,content,type,is_h1,is_h2,is_h3,ami_id,amt_id')->where($aWhere)->select(); + if(empty($aArticleMain)){ + return json_encode(array('status' => 5,'msg' => 'The content of the article is empty')); + } + + //实例化公共方法 + $oHelperFunction = new \app\common\HelperFunction; + $oProofReadService = new \app\common\ProofReadService; + //数据处理 + $aH = $aTable = []; + foreach ($aArticleMain as $key => $value) { + if(empty($oHelperFunction->filterAllTags($value['content']))){ + continue; + } + $aResult = $oProofReadService->proofread($value['content']); + if(empty($aResult)){ + continue; + } + $aResult['am_id'] = $value['am_id']; + $aError[] = $aResult; + } + if(empty($aError)){ + return json_encode(array('status' => 1,'msg' => 'No errors found')); + } + //数据处理 + foreach ($aError as $key => $value) { + if(empty($value['errors'])){ + continue; + } + foreach ($value['errors'] as $k => $val) { + $val['am_id'] = $value['am_id']; + $val['article_id'] = $iArticleId; + $val['proof_before'] = empty($value['proof_before']) ? '' : $value['proof_before']; + $val['proof_after'] = empty($value['proof_after']) ? '' : $value['proof_after']; + $aData[] = $val; + } + } + if(empty($aData)){ + return json_encode(array('status' => 1,'msg' => 'Data processing failed')); + } + //插入 + $result = Db::name('article_proofread')->insertAll($aData); + if(!$result){ + return json_encode(array('status' => 6,'msg' => 'No errors found')); + } + + //查询参考文献 + $aWhere = ['state' => ['in',[0,2]],'article_id' => $iArticleId]; + return json_encode(['status' => 1,'msg' => 'success']); + } + /** + * @title 获取每行校对记录 + * @param article_id 文章ID + * @param am_id 行号 + * @param state 状态1已执行2未执行3删除 + */ + public function get($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 a article']); + } + //行号 + $iAmId = empty($aParam['am_id']) ? 0 : $aParam['am_id']; + + //查询文章 + $aWhere = ['article_id' => $iArticleId]; + $aArticle = Db::name('article')->field('journal_id,state')->where($aWhere)->find(); + if(empty($aArticle)){ + return json_encode(['status' => 3,'msg' => 'The query article does not exist']); + } + if($aArticle['state'] < 5 || $aArticle['state'] == 8){ + return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); + } + + //查询文章内容 + $aWhere['type'] = 0; + $aWhere['content'] = ['<>','']; + $aWhere['state'] = 0; + if(!empty($iAmId)){ + $aWhere['am_id'] = $iAmId; + } + $aArticleMain = Db::name('article_main')->field('am_id,content')->where($aWhere)->select(); + if(empty($aArticleMain)){ + return json_encode(array('status' => 5,'msg' => 'The content of the article is empty')); + } + + //查询校对内容 + $aAmId = array_column($aArticleMain, 'am_id'); + $aWhere = ['am_id' => ['in',$aAmId]]; + $iState = empty($aParam['state']) ? 0 : $aParam['state']; + if(!empty($iState)){ + $aWhere['state'] = ['in',$iState]; + } + $aProofRead = Db::name('article_proofread')->field('id,am_id,verbatim_texts,revised_content,explanation,state')->where($aWhere)->select(); + if(empty($aProofRead)){ + return json_encode(array('status' => 1,'msg' => 'Proofreading record is empty')); + } + //数据处理 + $aData = []; + $aArticleMain = array_column($aArticleMain, 'content','am_id'); + foreach ($aProofRead as $key => $value) { + $aData[$value['am_id']][] = $value; + } + + // 存储每个文章的最新处理内容 + // 样式定义:标红+下划线 + // 定义标红+下划线样式 + // $style = 'color: red; text-decoration: underline;'; + + // // 存储最终处理后的文章内容 + // $aFinalContent = []; + + // // 外层循环:遍历每个文章的错误分组($key为am_id) + // foreach ($aData as $amId => $errors) { + // // 获取原始文章内容(若不存在则跳过) + // $sContent = empty($aArticleMain[$amId]) ? '' : $aArticleMain[$amId]; + // if (empty($sContent)) { + // continue; + // } + + // // 初始化当前内容为原始内容,后续替换将基于此更新 + // $currentContent = $sContent; + + // // 内层循环:处理当前文章的每条错误记录 + // foreach ($errors as $val) { + // // 只处理状态为2的错误 + // if ($val['state'] != 2) { + // continue; + // } + + // // 获取错误内容(待标红的文本) + // $verbatim_texts = trim($val['verbatim_texts'] ?? ''); + // if (empty($verbatim_texts)) { + // continue; // 空错误内容跳过 + // } + + // // 构建带样式的替换内容(用span标签包裹,添加CSS样式) + // $revised_content = '' . htmlspecialchars($verbatim_texts, ENT_QUOTES, 'UTF-8') . ''; + + // // 定位错误内容在当前内容中的位置(支持多字节字符,如中文) + // $startPos = mb_strpos($currentContent, $verbatim_texts, 0, 'UTF-8'); + // if ($startPos === false) { + // continue; // 未找到错误内容,跳过 + // } + + // // 计算错误内容的字节长度(用于替换) + // $byteLength = strlen($verbatim_texts); + // // 转换字符起始位置为字节位置(适配substr_replace的字节处理) + // $byteStart = 0; + // for ($i = 0; $i < $startPos; $i++) { + // $byteStart += strlen(mb_substr($currentContent, $i, 1, 'UTF-8')); + // } + + // // 执行替换:用带样式的内容替换原始错误内容 + // $currentContent = substr_replace($currentContent, $revised_content, $byteStart, $byteLength); + // } + + // // 保存当前文章处理后的最终内容 + // $aFinalContent[$amId] = $currentContent; + // } + return json_encode(['status' => 1,'msg' => 'success','data' => ['record' => $aData]]);//,'record_style' => $aFinalContent + } + + /** + * @title 更新状态 + * @param article_id 文章ID + * @param am_id 行号 + * @param record_id 记录ID + * @param state 1已执行2未执行3撤销 + */ + public function change(){ + //获取参数 + $aParam = $this->request->post(); + //参数验证-文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select a article']); + } + //参数验证-行号ID + $iAmId = empty($aParam['am_id']) ? 0 : $aParam['am_id']; + if(empty($iAmId)){ + return json_encode(['status' => 2,'msg' => 'Please select the proofreading content']); + } + //主键ID + $iId = empty($aParam['record_id']) ? 0 : $aParam['record_id']; + if(empty($iId)){ + return json_encode(['status' => 2,'msg' => 'Please select the review record']); + } + //状态 + $iState = empty($aParam['state']) ? 0 : intval($aParam['state']); + if(!in_array($iState, [1,2,3])){ + return json_encode(['status' => 2,'msg' => 'Illegal review status']); + } + + //修改内容 + $sContent = empty($aParam['content']) ? '' : $aParam['content']; + if(in_array($iState, [1,2]) && empty($sContent)){ + return json_encode(['status' => 2,'msg' => 'The operation content cannot be empty']); + } + + //判断校对记录 + $aWhere = ['am_id' => $iAmId,'article_id' => $iArticleId,'id' => $iId]; + $aProofRead = Db::name('article_proofread')->where($aWhere)->find(); + + if(empty($aProofRead)){ + return json_encode(['status' => 3,'msg' => 'Proofreading record is empty']); + } + if($aProofRead['state'] == 3){ + return json_encode(['status' => 4,'msg' => 'Record deleted']); + } + + //状态一致 + if($iState == $aProofRead['state']){ + return json_encode(['status' => 5,'msg' => 'Consistent status without modification']); + } + + //判断记录是否执行 + if($iState == 3 && $aProofRead['state'] == 1){ + return json_encode(['status' => 6,'msg' => 'This record has been executed and cannot be deleted']); + } + $sData = $sUpdateContent = ''; + if($iState == 1){ //执行替换操作 + $aProofRead['content'] = $sContent; + $aResult = $this->replaceError($aProofRead); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + if($iStatus != 1){ + return json_encode($aResult); + } + //获取内容 + $sUpdateContent = empty($aResult['data']) ? '' : $aResult['data']; + if(empty($sUpdateContent)){ + return json_encode(['status' => 5,'msg' => 'Content processing failed']); + } + // $aDealData = json_decode($this->get(['am_id' => $iAmId,'article_id' => $iArticleId]),true); + // $aDealData = empty($aDealData['data']) ? [] : $aDealData['data']; + // $sData = empty($aDealData['record_style']) ? '' : $aDealData['record_style']; + $sData = $sUpdateContent; + } + if($iState == 2){ //执行替换操作 + $aProofRead['content'] = $sContent; + $aResult = $this->removeReplaceError($aProofRead); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + if($iStatus != 1){ + return json_encode($aResult); + } + //获取内容 + $sUpdateContent = empty($aResult['data']) ? '' : $aResult['data']; + if(empty($sUpdateContent)){ + return json_encode(['status' => 5,'msg' => 'Content processing failed']); + } + // $aDealData = json_decode($this->get(['am_id' => $iAmId,'article_id' => $iArticleId]),true); + // $aDealData = empty($aDealData['data']) ? [] : $aDealData['data']; + // $sData = empty($aDealData['record_style']) ? '' : $aDealData['record_style']; + $sData = $sUpdateContent; + } + Db::startTrans(); + //更新原始内容 + if(!empty($sUpdateContent)){ + $aWhere = ['am_id' => $iAmId,'state' => 0,'type' => 0]; + $aUpdate = ['content' => $sUpdateContent]; + $result_main = Db::name('article_main')->where($aWhere)->limit(1)->update($aUpdate); + } + //判断更新参数 + $aUpdate = ['state' => $iState,'update_time' => time()]; + //数据库更新 + $aWhere = ['id' => $iId]; + $result_proofread= Db::name('article_proofread')->where($aWhere)->limit(1)->update($aUpdate); + if(!$result_proofread || !$result_main){ + return json_encode(['status' => 7,'msg' => "Update failed"]); + } + Db::commit(); + //返回结果 + return json_encode(['status' => 1,'msg' => "Update successful",'data' => $sData]); + } + + /** + * 替换错误 + * @param string $errorId 错误ID + * @return bool 替换是否成功 + */ + private function replaceError($aParam = []) + { + if(empty($aParam)){ + return ['status' => 2,'msg' => 'The content is empty']; + } + //原始内容 + $sContent = empty($aParam['content']) ? '' : $aParam['content']; + //错误内容 + $verbatim_texts = empty($aParam['verbatim_texts']) ? '' : $aParam['verbatim_texts']; + //正确内容 + $revised_content = empty($aParam['revised_content']) ? 0 : $aParam['revised_content']; + + $iLength = strlen($verbatim_texts); + //内容替换 + $sContent = $this->replaceByLengthAndKeyword($sContent,$iLength,$verbatim_texts,$revised_content); + + return ['status' => 1,'msg' => 'success','data' => $sContent]; + } + + /** + * 撤回替换错误 + * @param string $errorId 错误ID + * @return bool 替换是否成功 + */ + private function removeReplaceError($aParam = []) + { + if(empty($aParam)){ + return ['status' => 2,'msg' => 'The content is empty']; + } + //原始内容 + $sContent = empty($aParam['content']) ? '' : $aParam['content']; + //错误内容 + $verbatim_texts = empty($aParam['verbatim_texts']) ? '' : $aParam['verbatim_texts']; + //正确内容 + $revised_content = empty($aParam['revised_content']) ? '' : $aParam['revised_content']; + + $iLength = strlen($revised_content); + //内容替换 + $sContent = $this->replaceByLengthAndKeyword($sContent,$iLength,$revised_content,$verbatim_texts); + + return ['status' => 1,'msg' => 'success','data' => $sContent]; + } + /** + * 按长度和内容特征快速定位并替换字符串 + * @param string $str 原始字符串 + * @param int $targetLength 目标子串长度(字节数,多字节字符需注意) + * @param string $keyword 筛选关键词(目标子串需包含此关键词) + * @param string $replace 替换内容 + * @param string $encoding 字符编码(默认UTF-8,处理多字节字符) + * @return string 替换后的字符串 + */ + private function replaceByLengthAndKeyword($str, $targetLength, $keyword, $replace, $encoding = 'UTF-8') { + // 边界校验:避免无效参数导致错误 + if (empty($str) || $targetLength <= 0 || empty($keyword)) { + return ''; + } + $strLength = strlen($str); + if ($targetLength > $strLength) { + return ''; // 目标长度超过原始字符串,无需处理 + } + + $result = $str; + $targets = []; // 存储目标子串的起始位置(字节索引) + + // 遍历字符串,提取符合长度且包含关键词的子串位置(一次遍历完成筛选,减少内存占用) + for ($i = 0; $i <= $strLength - $targetLength; $i++) { + // 截取目标长度的子串 + $substr = substr($result, $i, $targetLength); + // 检查子串是否包含关键词(多字节安全) + if (mb_strpos($substr, $keyword, 0, $encoding) !== false) { + $targets[] = $i; // 只存起始位置,减少内存占用 + } + } + + // 若没有匹配目标,直接返回原始字符串 + if (empty($targets)) { + return ''; + } + + // 从后往前替换(避免前面替换导致后面位置偏移) + // 倒序遍历数组,无需额外排序,效率更高 + for ($k = count($targets) - 1; $k >= 0; $k--) { + $start = $targets[$k]; + // 二次校验:替换前确认子串仍符合条件(防止重复替换或中间修改导致的偏差) + $currentSubstr = substr($result, $start, $targetLength); + if (mb_strpos($currentSubstr, $keyword, 0, $encoding) !== false) { + $result = substr_replace($result, $replace, $start, $targetLength); + } + } + + return $result; + } + + /** + * @title 更新内容 + * @param article_id 文章ID + * @param am_id 行号 + * @param record_id 记录ID + * @param revised_content 修改内容 + * @param is_update_all 是否更新所有1是2否 + */ + public function modify(){ + //获取参数 + $aParam = $this->request->post(); + //参数验证-文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select a article']); + } + //参数验证-行号ID + $iAmId = empty($aParam['am_id']) ? 0 : $aParam['am_id']; + if(empty($iAmId)){ + return json_encode(['status' => 2,'msg' => 'Please select the proofreading content']); + } + //主键ID + $iId = empty($aParam['record_id']) ? 0 : $aParam['record_id']; + if(empty($iId)){ + return json_encode(['status' => 2,'msg' => 'Please select the review record']); + } + //修改后的内容 + $sRevisedContent = empty($aParam['revised_content']) ? '' : $aParam['revised_content']; + if(empty($sRevisedContent)){ + return json_encode(['status' => 2,'msg' => 'Please enter the modification content']); + } + //解释说明 + $sExplanation = empty($aParam['explanation']) ? '' : $aParam['explanation']; + //是否更新所有1是2否 + $iIsUpdateAll = empty($aParam['is_update_all']) ? 2 : $aParam['is_update_all']; + + //查询内容是否存在 + $aWhere = ['am_id' => $iAmId,'article_id' => $iArticleId,'id' => $iId]; + $aProofRead = Db::name('article_proofread')->field('verbatim_texts,revised_content,explanation,state')->where($aWhere)->find(); + if(empty($aProofRead)){ + return json_encode(['status' => 3,'msg' => 'Proofreading record is empty']); + } + if($aProofRead['state'] == 3){ + return json_encode(['status' => 4,'msg' => 'Record deleted']); + } + if($aProofRead['state'] == 1){ + return json_encode(['status' => 5,'msg' => 'Record executed']); + } + //判断更新参数 + $aUpdate = ['revised_content' => $sRevisedContent,'update_time' => time()]; + if(!empty($sExplanation)){ + $aUpdate['explanation'] = $sExplanation; + } + //数据库更新 + $aWhere = ['id' => $iId]; + if($iIsUpdateAll == 1){ + if(empty($aProofRead['verbatim_texts']) || empty($aProofRead['revised_content'])){ + return json_encode(['status' => 6,'msg' => 'AI proofreading content is empty']); + } + $aWhere = ['verbatim_texts' => $aProofRead['verbatim_texts'],'revised_content' => $aProofRead['revised_content'],'article_id' => $iArticleId,'state' => 2]; + } + $result = Db::name('article_proofread')->where($aWhere)->update($aUpdate); + if(!$result){ + return json_encode(['status' => 7,'msg' => "Update failed"]); + } + //返回结果 + return json_encode(['status' => 1,'msg' => "Update successful"]); + } +} diff --git a/application/api/controller/Publish.php b/application/api/controller/Publish.php index f27bd03..002085a 100644 --- a/application/api/controller/Publish.php +++ b/application/api/controller/Publish.php @@ -421,6 +421,13 @@ class Publish extends Base $pra['journal_stage_id'] = $data['journal_stage_id']; $res = object_to_array(json_decode(myPost($url, $pra))); $res1 = object_to_array(json_decode(myPost($url1, $pra))); + + //同步分期下的文章PDF文件到ftp.portico.org chengxiaoling 20250925 start + if(!empty($data['journal_stage_id'])){ + $sQueueId = \think\Queue::push('app\api\job\SyncArticleData@fire', ['journal_stage_id' => $data['journal_stage_id']], 'SyncArticleData'); + } + //同步分期下的文章PDF文件到ftp.portico.org chengxiaoling 20250925 end + return jsonSuccess([]); } diff --git a/application/api/controller/Supplementary.php b/application/api/controller/Supplementary.php index 0b55701..068fe0e 100644 --- a/application/api/controller/Supplementary.php +++ b/application/api/controller/Supplementary.php @@ -31,28 +31,40 @@ class Supplementary extends Base if(empty($sIssn)){ return json_encode(['status' => 2,'msg' => 'Please select an journal']); } - + if(is_string($sIssn)){ + $sIssn = explode(',', $sIssn); + } //根据期刊issn查询期刊ID - $aWhere = ['state' => 0,'issn' => $sIssn]; - $aJournal = Db::name('journal')->field('journal_id')->where($aWhere)->find(); + $aWhere = ['state' => 0,'issn' => ['in',$sIssn]]; + $aJournal = Db::name('journal')->where($aWhere)->column('journal_id,issn'); if(empty($aJournal)){ return json_encode(['status' => 3,'msg' => 'No journal information found']); } //查询期刊编辑信息 - $aWhere = ['state' => 0,'journal_id' => $aJournal['journal_id']]; + $aWhere = ['state' => 0,'journal_id' => ['in',array_keys($aJournal)]]; if(isset($aParam['type'])){//编辑类型 $aWhere['type'] = $aParam['type']; } - $aJournalBoard = Db::name('board_to_journal')->where($aWhere)->column('user_id'); + $aJournalBoard = Db::name('board_to_journal')->field('journal_id,user_id')->where($aWhere)->select(); if(empty($aJournalBoard)){ return json_encode(['status' => 4,'msg' => 'No editorial information was found for the journal']); } //查询编辑详情 - $aWhere = ['state' => 0,'user_id' => ['in',$aJournalBoard]]; - $aUser = Db::name('user')->field('user_id,realname')->where($aWhere)->select(); - return json_encode(['status' => 1,'msg' => 'success','data' => $aUser]); + $aUserId = array_column($aJournalBoard, 'user_id'); + $aWhere = ['state' => 0,'user_id' => ['in',$aUserId]]; + $aUser = Db::name('user')->where($aWhere)->column('user_id,realname'); + $aUserData = []; + foreach ($aJournalBoard as $key => $value) { + $sIssn = empty($aJournal[$value['journal_id']]) ? '' : $aJournal[$value['journal_id']]; + if(empty($sIssn)){ + continue; + } + $sRealName = empty($aUser[$value['user_id']]) ? '' : $aUser[$value['user_id']]; + $aUserData[$sIssn][] = $sRealName; + } + return json_encode(['status' => 1,'msg' => 'success','data' => $aUserData]); } } diff --git a/application/api/controller/Syncdata.php b/application/api/controller/Syncdata.php new file mode 100644 index 0000000..eeec63a --- /dev/null +++ b/application/api/controller/Syncdata.php @@ -0,0 +1,128 @@ +request->post(); + + //期刊ID + $iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id']; + if(empty($iJournalId)){ + return json_encode(['code' => 2, 'msg' => 'Please select a journal']); + } + //获取期刊下的子刊 + $sUrl = $this->sJournalUrl."api/Syncdata/getJournalStage"; + $aResult = object_to_array(json_decode(myPost1($sUrl,$aParam),true)); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + + $sMsg = empty($aResult['msg']) ? 'Illegal operation-getJournalStage' : $aResult['msg']; + if($iStatus != 1){ + return json_encode(['code' => 3, 'msg' => $sMsg]); + } + //获取期刊下的子刊 + $aJournalStage = empty($aResult['data']) ? [] : $aResult['data']; + if(empty($aJournalStage)){ + return json_encode(['code' => 4, 'msg' => 'Journal data is empty']); + } + + //数据处理 + foreach ($aJournalStage as $key => $value) { + // $aArticle = json_decode($this->getJournalStageArticle(['journal_stage_id' => $value['journal_stage_id']]),true); + // var_dump($aArticle);exit; + //写入查询期刊文章队列 + $sQueue = Queue::push('app\api\job\SyncArticleData@fire', ['journal_stage_id' => $value['journal_stage_id']], 'SyncArticleData'); + } + return json_encode(['code' => 1, 'msg' => 'Synchronization queue addition completed','data' => $sQueue]); + } + /** + * @title 获取期刊下的文章 + * @param journal_id 期刊ID + * @param journal_stage_id 子刊ID + */ + public function getJournalStageArticle($aParam = []){ + + //获取数据 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + + //期刊ID + $iJournalStageId = empty($aParam['journal_stage_id']) ? 0 : $aParam['journal_stage_id']; + if(empty($iJournalStageId)){ + return json_encode(['code' => 2, 'msg' => 'Please select a sub issue under the journal']); + } + //获取期刊下的子刊 + $sUrl = $this->sJournalUrl."api/Syncdata/getJournalStageArticle"; + $aResult = object_to_array(json_decode(myPost1($sUrl,$aParam),true)); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + $sMsg = empty($aResult['msg']) ? 'Illegal operation-getJournalStageArticle' : $aResult['msg']; + if($iStatus != 1){ + return json_encode(['code' => 3, 'msg' => $sMsg]); + } + //获取文章 + $aArticle = empty($aResult['data']) ? [] : $aResult['data']; + if(empty($aArticle)){ + return json_encode(['code' => 4, 'msg' => 'Article data is empty']); + } + + //写入上传文章队列 + $sQueue = Queue::push('app\api\job\SyncArticleUpload@fire', $aArticle, 'SyncArticleUpload'); + // $aResult = json_decode($this->uploadArticle($aArticle),true); + return json_encode(['code' => 1, 'msg' => 'Joined the upload file queue','data' => $sQueue]); + } + /** + * @title 同步文章数据到服务器 + * @param file 文章数据 + * @param journal_stage_id 子刊ID + */ + public function uploadArticle($aParam = []){ + + //获取数据 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + + //期刊ID + $iJournalStageId = empty($aParam['journal_stage_id']) ? 0 : $aParam['journal_stage_id']; + if(empty($iJournalStageId)){ + return json_encode(['code' => 2, 'msg' => 'Please select a sub issue under the journal']); + } + //文章数据 + $aArticle = empty($aParam['article']) ? [] : $aParam['article']; + if(empty($aArticle)){ + return json_encode(['code' => 2, 'msg' => 'Article data is empty']); + } + //获取期刊下的子刊 + $sUrl = $this->sJournalUrl."api/Syncdata/uploadArticle"; + $aResult = object_to_array(json_decode(myPost1($sUrl,$aParam),true)); + $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; + $sMsg = empty($aResult['msg']) ? 'Illegal operation-uploadArticle' : $aResult['msg']; + if($iStatus != 1){ + return json_encode(['code' => 3, 'msg' => $sMsg]); + } + $aResult = empty($aResult['data']) ? [] : $aResult['data']; + return json_encode(['code' => 1, 'msg' => $sMsg,'data' => $aResult]); + } +} diff --git a/application/api/job/ArticleProofRead.php b/application/api/job/ArticleProofRead.php new file mode 100644 index 0000000..0781508 --- /dev/null +++ b/application/api/job/ArticleProofRead.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 + $iArticleId = empty($data['article_id']) ? 0 : $data['article_id']; + if (empty($iArticleId)) { + $this->oQueueJob->log("无效的article_id,删除任务"); + $job->delete(); + return; + } + try { + + // 生成Redis键并尝试获取锁 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + //生成内容 + $oProofRead = new Proofread; + $response = $oProofRead->proofRead($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/SendRelatedArticleEmail.php b/application/api/job/SendRelatedArticleEmail.php index 975688a..1e029c1 100644 --- a/application/api/job/SendRelatedArticleEmail.php +++ b/application/api/job/SendRelatedArticleEmail.php @@ -70,7 +70,7 @@ class SendRelatedArticleEmail // 执行核心任务 //查询是否发送过邮件 $oJournalArticle = new JournalArticle; - $aLog = json_decode($oJournalArticle::getLog(['article_id' => $iArticleId,'article_author_id' => $article_author_id,'related_article_id' => $related_article_id,'is_success' => 1]),true); + $aLog = json_decode($oJournalArticle::getLog(['article_id' => $iArticleId,'email' => $email,'related_article_id' => $related_article_id,'is_success' => 1]),true); $sMsg = '邮件已发送'; if(empty($aLog['data'])){ $aResult = sendEmail($email,$title,$from_name,$content,$memail,$mpassword); diff --git a/application/api/job/SyncArticleData.php b/application/api/job/SyncArticleData.php new file mode 100644 index 0000000..41cf313 --- /dev/null +++ b/application/api/job/SyncArticleData.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 + $iJournalStageId = empty($data['journal_stage_id']) ? 0 : $data['journal_stage_id']; + if (empty($iJournalStageId)) { + $this->oQueueJob->log("无效的journal_stage_id,删除任务"); + $job->delete(); + return; + } + try { + + // 生成Redis键并尝试获取锁 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iJournalStageId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + //生成内容 + $oAireview = new Syncdata; + $response = $oAireview->getJournalStageArticle($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/SyncArticleUpload.php b/application/api/job/SyncArticleUpload.php new file mode 100644 index 0000000..d6c4b8a --- /dev/null +++ b/application/api/job/SyncArticleUpload.php @@ -0,0 +1,79 @@ +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 + $iJournalStageId = empty($data['journal_stage_id']) ? 0 : $data['journal_stage_id']; + if (empty($iJournalStageId)) { + $this->oQueueJob->log("无效的journal_stage_id,删除任务"); + $job->delete(); + return; + } + try { + + // 生成Redis键并尝试获取锁 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iJournalStageId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + //生成内容 + $oAireview = new Syncdata; + $response = $oAireview->uploadArticle($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']; + $sData = empty($aResult['data']) ? '' : json_encode($aResult['data']); + //更新完成标识 + $this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue); + $job->delete(); + $this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg} | 数据:{$sData}"); + + } 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/Article.php b/application/common/Article.php index bf7c741..f731f6e 100644 --- a/application/common/Article.php +++ b/application/common/Article.php @@ -84,7 +84,7 @@ class Article } $iAiArticleId = $aAiArticle['ai_article_id']; //必填参数验证 - $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']; + $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']; $sFiled = ''; $aUpdateParam = []; diff --git a/application/common/JournalArticle.php b/application/common/JournalArticle.php index 469dc35..cd4e75f 100644 --- a/application/common/JournalArticle.php +++ b/application/common/JournalArticle.php @@ -7,7 +7,7 @@ class JournalArticle { //官网接口地址 - protected static $sApiUrl = 'http://journalapi.tmrjournals.com/public/index.php/';//'http://zmzm.journal.dev.com/'; + protected static $sApiUrl = 'http://journalapi.tmrjournals.com/public/index.php/';//'http://zmzm.journal.dev.com/';// //期刊官网 protected static $sJournalUsx = 'https://www.tmrjournals.com/'; @@ -85,19 +85,20 @@ class JournalArticle if(empty($aJournal)){ return json_encode(['status' => 5,'msg' => 'The journal to which the article belongs does not exist']); } - //文章作者 + + //获取数据-文章通讯作者数据 $aAuthor = empty($aData['author']) ? [] : $aData['author']; if(empty($aAuthor)){ - return json_encode(['status' => 6,'msg' => 'No author information found for the article']); + return json_encode(['status' => 5,'msg' => '未查询到引用文章作者数据']); } //查询邮件发送日志 $aWhere = ['article_id' => $iArticleId,'is_success' => 1]; - $aEmailLog = Db::name('email_related_article')->field('related_article_id,article_author_id')->where($aWhere)->select(); + $aEmailLog = Db::name('email_related_article')->field('related_article_id,email')->where($aWhere)->select(); $aLog = []; if(!empty($aEmailLog)){ foreach ($aEmailLog as $key => $value) { - $aLog[$value['related_article_id']][] = $value['article_author_id']; + $aLog[$value['related_article_id']][] = $value['email']; } } @@ -106,18 +107,26 @@ class JournalArticle //数据处理 $aEmailLog = []; $sErrorMsg = ''; - foreach ($aAuthor as $key => $value) { + $aSendEmail = [];//['2101' => ['mohammadalipour_z@yahoo.com']]; + foreach ($aAuthor as $key => $value) { + if(empty($value['is_report']) || (!empty($value['is_report']) && $value['is_report'] != 1)){ + continue; + } //作者邮箱-发送信息 - $email = empty($value['email']) ? '' : $value['email']; + $email = empty($value['email']) ? '' : $value['email'];//'tmr@tmrjournals.com';//'1172937051@qq.com';// if(empty($email)){ continue; } - $email = $value['email'];//'tmr@tmrjournals.com';//'1172937051@qq.com';// + + //判断同一个邮箱是否重复发送 + if(!empty($aSendEmail[$value['article_id']]) && in_array($email, $aSendEmail[$value['article_id']])){ + continue; + } + //判断是否发送过邮件 - //邮件日志 - $aEmailLogInfo = empty($aLog[$value['article_id']]) ? [] : $aLog[$value['article_id']]; - if(in_array($value['article_author_id'], $aEmailLogInfo)){ + $aEmailLogInfo = empty($aLog[$value['article_id']]) ? [] : $aLog[$value['article_id']];//邮件日志 + if(in_array($value['email'], $aEmailLogInfo)){ continue; } @@ -186,9 +195,12 @@ class JournalArticle $iIsSuccess = 1; $sMsg = '成功'; } - $aEmaiParam = ['article_id' => $iArticleId,'article_author_id' =>$value['article_author_id'],'related_article_id' => $value['article_id'],'email' => $email,'content' => $content,'create_time' => time(),'journal_id' => $aJournalInfo['journal_id'],'journal_issn' => $aJournalInfo['issn'],'email' => $email,'title' => $title,'from_name' => $from_name,'content' => $content,'memail' => $memail,'mpassword' => $mpassword]; + $aEmailParam = ['article_id' => $iArticleId,'article_author_id' =>$value['article_author_id'],'related_article_id' => $value['article_id'],'email' => $email,'content' => $content,'create_time' => time(),'journal_id' => $aJournalInfo['journal_id'],'journal_issn' => $aJournalInfo['issn'],'title' => $title,'from_name' => $from_name,'memail' => $memail,'mpassword' => $mpassword]; + + //邮件发送记录 + $aSendEmail[$value['article_id']][] = $email; //调用邮件发送队列 - Queue::push('app\api\job\SendRelatedArticleEmail@fire', $aEmaiParam, 'SendRelatedArticleEmail'); + Queue::push('app\api\job\SendRelatedArticleEmail@fire', $aEmailParam, 'SendRelatedArticleEmail'); } return json_encode(['status' => 1,'msg' => 'Added to email sending queue']); } @@ -221,9 +233,8 @@ class JournalArticle * @return void */ static function getLog($aParam = []) { - //查询条件判断 - if(empty($aParam['article_id']) || empty($aParam['article_author_id']) || empty($aParam['related_article_id'])){ + if(empty($aParam['article_id']) || empty($aParam['email']) || empty($aParam['related_article_id'])){ return json_encode(['status' => 2,'msg' => 'Missing parameter']); } diff --git a/application/common/OpenAi.php b/application/common/OpenAi.php index d2e3f07..258ac87 100644 --- a/application/common/OpenAi.php +++ b/application/common/OpenAi.php @@ -103,7 +103,7 @@ class OpenAi 'contradiction_explanation' => "解释说明", ], 'fund_number' => [ - 'fund_number' => "1.[内容是否有基金号]2.[解释说明][返回格式字符串]" + 'fund_number' => "1.[内容是否有基金号]2.[列出基金号]3.[解释说明][返回格式字符串]" ], 'attribute' => [ 'attribute_assessment' => "内容是否有科学性和创新性[包括但不限于科学性(结论是否科学、参考文献是否新颖等);创新性(结论与当前研究水平相比是否有明显突破、参考文献的时间)][返回是/否]", diff --git a/application/common/ProofReadService.php b/application/common/ProofReadService.php new file mode 100644 index 0000000..2a21ac3 --- /dev/null +++ b/application/common/ProofReadService.php @@ -0,0 +1,1236 @@ +errors = []; + $this->excludedFormats = []; + $correctedContent = $content; + + //时间单位缩写校对 + $correctedContent = $this->checkTimeUnitAbbreviations($correctedContent); + //横线/运算符校对 + $correctedContent = $this->checkTextFormat($correctedContent); + //数字格式校对 + $correctedContent = $this->checkNumberFormat($correctedContent); + //毫升单位校对 + $correctedContent = $this->checkMlUnit($correctedContent); + //显著性P斜体校对 + $correctedContent = $this->checkPSignificance($correctedContent); + //No. 123456的写法统一 + $correctedContent = $this->checkNoFormatUniformity($correctedContent); + //图表标题一律使用全称Figure 1, Table 1.不能写成Fig. 1, Tab 1. + $correctedContent = $this->checkFigureTableTitle($correctedContent); + //检测参考文献是否能打开 + // $correctedContent = $this->checkDoi($correctedContent); + //判断是否为空错误信息 + if(empty($this->errors)){ + return []; + } + return [ + 'proof_before' => $content, + 'proof_after' => $correctedContent, + 'errors' => $this->errors + ]; + } + + /** + * 横线/运算符校对/数字和单位(高可用版) + */ + private function checkTextFormat($content) { + // 初始化错误数组 + $errors = []; + $defaultReturn = $content; + $originalContent = $content; // 保存完整原始内容 + $searchOffsetForExclude = 0; // 【新增】仅用于「特殊内容过滤」的偏移量 + $searchOffsetForCore = 0; // 【新增】仅用于「核心规则处理」的偏移量 + + // 验证数据 + if (!is_string($content) || trim($content) === '') { + $this->handleErrors($errors); + return $defaultReturn; + } + + $corrected = $content; + $excludeMarkers = []; // 存储 URL/DOI + / 的占位符映射 + $processedHashes = []; + + // 编码处理(不变) + $originalEncoding = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'ISO-8859-1'], true); + if ($originalEncoding === false) { + // $converted = @mb_convert_encoding($content, 'UTF-8', 'auto'); + // $corrected = $converted !== false ? $converted : $content; + // $posStart = 0; + // $posEnd = min(20, strlen($originalContent)); + // $errors[] = $this->createError( + // '内容编码检测失败', + // '已尝试强制UTF-8编码', + // '输入内容编码无法识别,已尝试自动转换为UTF-8', + // $originalContent, + // $corrected, + // $posStart, + // $posEnd + // ); + } else { + $converted = @mb_convert_encoding($content, 'UTF-8', $originalEncoding); + $corrected = $converted !== false ? $converted : $content; + if ($converted === false) { + $posStart = 0; + $posEnd = min(20, strlen($originalContent)); + $errors[] = $this->createError( + '编码转换失败', + '保留原始编码内容', + "从[{$originalEncoding}]转换为UTF-8失败,保留原始内容", + $originalContent, + $corrected, + $posStart, + $posEnd + ); + } + } + + // 过滤 /(复杂标签优先) + $mathTagRegex = '~<(wmath|math)[^>]*?>.*?~is'; + if (@preg_match($mathTagRegex, '') === false) { + // 正则错误处理(不变) + } elseif (preg_match_all($mathTagRegex, $corrected, $matches, PREG_SET_ORDER)) { + usort($matches, function($a, $b) { + return strlen($b[0]) - strlen($a[0]); + }); + + foreach ($matches as $index => $match) { + $fullTag = $match[0]; + $tagType = $match[1]; + $marker = "___EXCLUDE_{$tagType}_" . time() . "_{$index}___"; + $excludeMarkers[$marker] = $fullTag; + + // 【修改】使用独立偏移量 $searchOffsetForExclude + $posStart = strpos($originalContent, $fullTag, $searchOffsetForExclude); + $posEnd = ($posStart !== false) ? $posStart + strlen($fullTag) : -1; + $searchOffsetForExclude = ($posEnd !== -1) ? $posEnd : $searchOffsetForExclude + strlen($fullTag); + + $safeFullTag = preg_quote($fullTag, '~'); + $corrected = preg_replace("~{$safeFullTag}~u", $marker, $corrected, 1); + } + } + + // 过滤 URL/DOI(放在数学标签后) + $urlDoiRegex = '~(https?://[^\s/]{1,100}(?:/+[A-Za-z0-9\.\-]+(?:-[A-Za-z0-9\.\-]+)*)*|\b[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,}(?:/+[A-Za-z0-9\.\-]+(?:-[A-Za-z0-9\.\-]+)*)*(?=$|[\s\.,;!])|doi:\s{0,10}\d+\.\d+/[A-Za-z0-9-+×:]+(?:-[A-Za-z0-9-+×:]+)*)~iu'; + if (@preg_match($urlDoiRegex, '') === false) { + // 正则错误处理(不变) + } elseif (preg_match_all($urlDoiRegex, $corrected, $matches, PREG_SET_ORDER)) { + usort($matches, function($a, $b) { return strlen($b[1]) - strlen($a[1]); }); + foreach ($matches as $index => $match) { + $original = $match[1]; + $marker = "___EXCLUDE_URL_" . time() . "_{$index}___"; + $excludeMarkers[$marker] = $original; + + // 【修改】使用独立偏移量 $searchOffsetForExclude + $posStart = strpos($originalContent, $original, $searchOffsetForExclude); + $posEnd = ($posStart !== false) ? $posStart + strlen($original) : -1; + $searchOffsetForExclude = ($posEnd !== -1) ? $posEnd : $searchOffsetForExclude + strlen($original); + + $corrected = preg_replace("~" . preg_quote($original, '~') . "~u", $marker, $corrected, 1); + } + } + + // 核心格式规则处理(关键修改) + $coreRules = $this->getTextCoreRules(); + foreach ($coreRules as $rule) { + if (@preg_match($rule['pattern'], '') === false) { + continue; + } + + // 【修改】确保匹配时使用原始 $corrected(未被占位符干扰),并保留偏移量 + $matchCount = preg_match_all( + $rule['pattern'], + $corrected, + $matches, + PREG_SET_ORDER | PREG_OFFSET_CAPTURE + ); + if ($matchCount === 0) { + continue; + } + + foreach ($matches as $match) { + $original = $match[0][0]; // 匹配到的原始内容(如 (40 × 33)) + $originalLen = strlen($original); + $hash = md5($original); + + if (isset($processedHashes[$hash])) { + continue; + } + + // 【关键修复1:精准定位,不受其他偏移量干扰】 + $posStart = -1; + $posEnd = -1; + // 1. 先尝试用 PREG_OFFSET_CAPTURE 得到的偏移量反推原始位置 + $offsetInCorrected = $match[0][1]; // 匹配内容在 $corrected 中的偏移量 + // 2. 提取 $corrected 中匹配位置前的内容,计算其在原始文本中的长度(排除占位符影响) + $prefixInCorrected = substr($corrected, 0, $offsetInCorrected); + // 3. 替换占位符为原始内容,得到与 $originalContent 对应的前缀 + $prefixInOriginal = strtr($prefixInCorrected, $excludeMarkers); + // 4. 原始位置 = 前缀长度(确保精准对应) + $posStart = strlen($prefixInOriginal); + $posEnd = $posStart + $originalLen; + + // 【关键修复2:二次验证,确保位置正确】 + if ($posStart !== -1) { + $contentCheck = substr($originalContent, $posStart, $originalLen); + // 转换为 UTF-8 编码后用 strcmp 比较(依赖 iconv 扩展) + $contentCheckConv = iconv('UTF-8', 'UTF-8//IGNORE', $contentCheck); + $originalConv = iconv('UTF-8', 'UTF-8//IGNORE', $original); + if (strcmp($contentCheckConv, $originalConv) !== 0) { + // 二次验证失败时,用局部正则重新定位 + $localPattern = '~' . preg_quote($original, '~') . '~u'; + if (preg_match($localPattern, $originalContent, $localMatch, PREG_OFFSET_CAPTURE, $searchOffsetForCore)) { + $posStart = $localMatch[0][1]; + $posEnd = $posStart + $originalLen; + } + } + } + + // 生成修正内容 + $fixed = is_callable($rule['replacement']) + ? call_user_func($rule['replacement'], $match) + : preg_replace($rule['pattern'], $rule['replacement'], $original); + + if ($original !== $fixed && $fixed !== null) { + // 【修改】更新核心规则专用偏移量 + $searchOffsetForCore = ($posEnd !== -1) ? $posEnd : $searchOffsetForCore + $originalLen; + + // 生成错误信息 + $currentCorrected = str_replace($original, $fixed, $corrected); + $errors[] = $this->createError( + $original, + $fixed, + $rule['explanation'], + $originalContent, + $currentCorrected, + $posStart, + $posEnd + ); + $processedHashes[$hash] = true; + $corrected = $currentCorrected; + } + } + } + + // 批量还原 / 和 URL/DOI(不变) + $restoreErrors = []; + if (!empty($excludeMarkers)) { + $corrected = strtr($corrected, $excludeMarkers); + if (preg_match_all('~___EXCLUDE_(wmath|math|URL)_\d+_\d+___~', $corrected, $remaining)) { + foreach ($remaining[0] as $marker) { + $original = $excludeMarkers[$marker] ?? '未知数学公式/链接'; + $restoreErrors[] = $original; + $posStart = strpos($corrected, $marker, $searchOffsetForExclude); + $posEnd = ($posStart !== false) ? $posStart + strlen($marker) : -1; + $searchOffsetForExclude = ($posEnd !== -1) ? $posEnd : $searchOffsetForExclude + strlen($marker); + $corrected = str_replace($marker, $original, $corrected); + } + } + } + + // if (!empty($restoreErrors)) { + // $posStart = 0; + // $posEnd = min(50, strlen($originalContent)); + // $errors[] = $this->createError( + // '特殊内容恢复不完全', + // '已强制恢复原始内容', + // "恢复失败的内容: " . implode('; ', $restoreErrors), + // $originalContent, + // $corrected, + // $posStart, + // $posEnd + // ); + // } + + $this->handleErrors($errors); + return is_string($corrected) ? $corrected : $defaultReturn; + } + /** + * 获取文本格式核心规则 + */ + private function getTextCoreRules() { + return [ + // ====================== 1. 括号内数字范围规则(优先级最高,避免与其他减号规则冲突) ====================== + [ + 'pattern' => '~(\[\s*[-]?\d+\s*)\x{2014}\s*(\d+\s*\])~u', // 匹配长划线(—) + 'replacement' => '$1-$2', // 替换为短划线(-) + 'verbatim_texts' => '带括号数字范围使用长划线(—)不规范', + 'explanation' => '带括号的数字范围应使用短划线(-),如 [1-5]' + ], + [ + 'pattern' => '~(\[\s*[-]?\d+\s*)-\s*(\d+\s*\])~u', // 匹配连接符(-)及可能的空格 + 'replacement' => '$1-$2', // 统一为无空格短划线(-) + 'verbatim_texts' => '带括号数字范围使用连接符(-)格式不规范', + 'explanation' => '带括号的数字范围应使用短划线(-)且前后无空格,如 [2-5]' + ], + [ + 'pattern' => '~(\[\s*[-]?\d+)\s+-\s*(\d+\s*\])~u', // 短划线前多余空格 + 'replacement' => '$1-$2', // 移除前导空格 + 'verbatim_texts' => '数字范围短划线前有多余空格', + 'explanation' => '带括号数字范围的短划线(-)前不应留空格,如 [3-5]' + ], + [ + 'pattern' => '~(\[\s*[-]?\d+)\s*-\s+(\d+\s*\])~u', // 短划线后多余空格 + 'replacement' => '$1-$2', // 移除后导空格 + 'verbatim_texts' => '数字范围短划线后有多余空格', + 'explanation' => '带括号数字范围的短划线(-)后不应留空格,如 [4-5]' + ], + + // ====================== 2. 无括号数字范围规则(次高优先级,避免与减号运算规则冲突) ====================== + [ + 'pattern' => '~(\b\d+)\s*—\s*(\d+\b)~u', // 匹配长划线(—) + 'replacement' => '$1-$2', // 替换为短划线(-) + 'verbatim_texts' => '无括号数字范围使用长划线(—)不规范', + 'explanation' => '无括号的数字范围应使用短划线(-),如 5-6' + ], + [ + 'pattern' => '~(\b\d+)\s*-\s*(\d+\b)~u', // 匹配连接符(-)及可能的空格 + 'replacement' => '$1-$2', // 统一为无空格短划线(-) + 'verbatim_texts' => '无括号数字范围使用连接符(-)格式不规范', + 'explanation' => '无括号的数字范围应使用短划线(-)且前后无空格,如 5-7' + ], + + // ====================== 3. 运算符空格规则(按「复合→独立」顺序,避免冲突) ====================== + [ + 'pattern' => '~(\S)\s*([<>!]=|===|!==)\s*(\S)~u', // 复合运算符(>=、<=、==、!=、===、!==) + 'replacement' => '$1 $2 $3', + 'verbatim_texts' => '复合运算符前后空格不规范', + 'explanation' => '复合运算符(>=、<=、==、!=、===、!==)前后应各留一个空格,如 x >= 5' + ], + [ + 'pattern' => '~(?|\*|\+|-|/)(\S+?)\s*=\s*(\S+?)(?!=|<|>|\*|\+|-|/)~u', + // 捕获组说明: + // $1:等号前内容(非空字符,避免匹配空格) + // $2:等号后内容(非空字符,避免匹配空格) + // 前后否定断言:排除与其他运算符(如+=、*=)的冲突 + 'replacement' => '$1 = $2', // 正确拼接“前内容 + 规范等号 + 后内容” + 'verbatim_texts' => '等号前后空格不规范', + 'explanation' => '独立等号(=)前后应各留一个空格,如 a = 3' // 移除无效的$1/$2 + ], + [ + 'pattern' => '~(\d+)\s*\+\s*(\d+)~u', // 加法运算符(+) + 'replacement' => '$1 + $2', + 'verbatim_texts' => '加法运算符前后空格不规范', + 'explanation' => '加法运算符(+)前后应各留一个空格,如 2 + 3' + ], + [ + 'pattern' => '~(\d+)\s*\*\s*(\d+)~u', // 乘法运算符(*) + 'replacement' => '$1 * $2', + 'verbatim_texts' => '乘法运算符前后空格不规范', + 'explanation' => '乘法运算符(*)前后应各留一个空格,如 3 * 4' + ], + [ + 'pattern' => '~(\d+)\s*/\s*(\d+)~u', // 除法运算符(/) + 'replacement' => '$1 / $2', + 'verbatim_texts' => '除法运算符前后空格不规范', + 'explanation' => '除法运算符(/)前后应各留一个空格,如 8 / 2' + ], + [ + 'pattern' => '~ + (? '$1 - $2', + 'verbatim_texts' => '减法运算符前后空格不规范', + 'explanation' => '减法运算符(-)前后应各留一个空格(非数字范围场景),如 5 - 3' + ], + + // ====================== 4. 特殊符号规则(低优先级,避免干扰核心格式) ====================== + [ + 'pattern' => '~(\d+)\s+%~u', // 数字与百分号 + 'replacement' => '$1%', + 'verbatim_texts' => '数字与百分号之间有多余空格', + 'explanation' => '数字与百分号(%)之间不应留空格,如 50%' + ], + [ + 'pattern' => '~(\(\s*\d+)\s+×\s+(\d+\s*\))~u', // 先匹配「(数字 × 数字)」场景(带括号) + 'replacement' => '$1×$2', // 修正为「(数字×数字)」,如 (40×33) + 'verbatim_texts' => '带括号的乘号表示倍数时前后有多余空格', + 'explanation' => '带括号的乘号(×)表示倍数关系时前后不应留空格,如 (3×5)' + ], + [ + 'pattern' => '~(\d+)\s+×\s+(\d+)~u', // 再匹配「数字 × 数字」场景(无括号) + 'replacement' => '$1×$2', + 'verbatim_texts' => '乘号表示倍数时前后有多余空格', + 'explanation' => '乘号(×)表示倍数关系时前后不应留空格,如 3×5' + ], + [ + 'pattern' => '~(\d+)\s*\*\s*(\d+)~u', // 星号(*)转乘号(×) + 'replacement' => '$1 × $2', + 'verbatim_texts' => '使用星号(*)作为乘法运算符不规范', + 'explanation' => '乘法运算应使用标准乘号(×)替代星号(*),并前后留空格,如 3 × 5' + ], + [ + 'pattern' => '~(\d+)\s+:\s+(\d+)~u', // 比值符号(:) + 'replacement' => '$1:$2', + 'verbatim_texts' => '比值符号前后有多余空格', + 'explanation' => '比值符号(:)前后不应留空格,如 1:2' + ] + ]; + } + /** + * 数字格式校对 + */ + private function checkNumberFormat($content) { + $errors = []; + $defaultReturn = $content; + $originalContent = $content; + $searchOffset = 0; // 用于计算位置的偏移量(避免重复定位) + + if (!is_string($content) || trim($content) === '') { + $this->handleErrors($errors); + return $defaultReturn; + } + + $correctedContent = $content; + $replacements = []; + $urlDoiPlaceholders = []; + + // URL/DOI保护(保持不变,新增位置记录) + $urlDoiPattern = '#([^\w]|^)(https?://[^<>\s]+|doi:\s{0,10}\d{1,10}\.\d{1,10}/[A-Za-z0-9]{1,30})([^\w]|$)#i'; + if (@preg_match($urlDoiPattern, '') === false) { + // 正则错误:位置默认-1 + // $errors[] = $this->createError( + // 'URL/DOI正则错误', + // '跳过URL/DOI保护', + // "URL/DOI正则语法错误: {$urlDoiPattern}", + // $originalContent, + // $correctedContent, + // -1, + // -1 + // ); + } else { + $correctedContent = preg_replace_callback( + $urlDoiPattern, + function ($matches) use (&$urlDoiPlaceholders, $originalContent, &$errors, &$searchOffset) { + $fullMatch = $matches[0]; + $placeholder = '___URL_DOI_' . time() . '_' . uniqid() . '___'; + $urlDoiPlaceholders[$placeholder] = $fullMatch; + + // 计算URL/DOI在原始文本中的位置 + $posStart = strpos($originalContent, $fullMatch, $searchOffset); + $posEnd = ($posStart !== false) ? $posStart + strlen($fullMatch) : -1; + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($fullMatch); + + // $errors[] = $this->createError( + // "URL/DOI保护: {$fullMatch}", + // "已替换为占位符", + // "保护URL/DOI内容,避免数字格式规则误处理", + // $originalContent, + // str_replace($fullMatch, $placeholder, $originalContent), + // $posStart, + // $posEnd + // ); + return $placeholder; + }, + $correctedContent + ); + } + + // 核心修复:处理纯小数零(如-5.0 → -5) + $decimalZeroPattern = '~(-?\d+)\.0+(?!\d|e|E)~ix'; + preg_match_all($decimalZeroPattern, $correctedContent, $matches); + + $uniqueNumbers = array_unique($matches[0]); + + foreach ($uniqueNumbers as $number) { + if (preg_match($decimalZeroPattern, $number, $numMatch)) { + $integerPart = $numMatch[1]; + $corrected = $integerPart; + + if (!isset($replacements[$number])) { + $replacements[$number] = $corrected; + + // 计算小数零在原始文本中的位置 + $posStart = strpos($originalContent, $number, $searchOffset); + $posEnd = ($posStart !== false) ? $posStart + strlen($number) : -1; + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($number); + + $currentCorrected = strtr($originalContent, $replacements); + $errors[] = $this->createError( + $number, + $corrected, + "删除小数点后无效零(原始:{$number} → 修正:{$corrected})", + $originalContent, + $currentCorrected, + $posStart, + $posEnd + ); + } + } + } + $correctedContent = strtr($correctedContent, $replacements); + + // 千分位处理 + $excludePatterns = implode('|', [ + 'https?://[^<>\s]+|doi:\s{0,10}\d{1,10}\.\d{1,10}/[A-Za-z0-9]{1,30}', + '\d{1,3}(,\d{3})+', '[A-Za-z]+\d+|\d+[A-Za-z]+', + '1\d{3}|2\d{3}', '\d{6}', '1[3-9]\d{9}', + '\d{3}[-\s]?\d{3}[-\s]?\d{4}', '\d{1,3}' + ]); + $thousandPattern = sprintf( + '#\b(?!(?:%s))\d{4,}\b#ixu', + str_replace('#', '\#', $excludePatterns) + ); + + if (@preg_match($thousandPattern, '') === false) { + $errors[] = $this->createError( + '千分位正则错误', + '跳过千分位处理', + "千分位正则错误: {$thousandPattern}", + $originalContent, + $correctedContent, + -1, + -1 + ); + } else { + $correctedContent = preg_replace_callback( + $thousandPattern, + function (array $matches) use (&$replacements, &$errors, $originalContent, &$searchOffset): string { + $original = $matches[0]; + if (isset($replacements[$original]) || strpos($original, ',') !== false) { + return $original; + } + + $formatted = number_format($original); + $replacements[$original] = $formatted; + + // 计算千分位数字在原始文本中的位置 + $posStart = strpos($originalContent, $original, $searchOffset); + $posEnd = ($posStart !== false) ? $posStart + strlen($original) : -1; + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($original); + + $currentCorrected = strtr($originalContent, $replacements); + $errors[] = $this->createError( + $original, + $formatted, + "4位及以上整数添加千分位分隔符", + $originalContent, + $currentCorrected, + $posStart, + $posEnd + ); + return $formatted; + }, + $correctedContent + ); + } + + // 恢复URL/DOI(新增位置记录) + $restoreFailed = []; + if (!empty($urlDoiPlaceholders)) { + $correctedContent = strtr($correctedContent, $urlDoiPlaceholders); + + if (preg_match_all('#___URL_DOI_.*?___#', $correctedContent, $remaining)) { + foreach ($remaining[0] as $marker) { + $original = $urlDoiPlaceholders[$marker] ?? '未知链接'; + $restoreFailed[] = $original; + + // 计算残留占位符的位置 + $posStart = strpos($correctedContent, $marker, $searchOffset); + $posEnd = ($posStart !== false) ? $posStart + strlen($marker) : -1; + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($marker); + + $correctedContent = str_replace($marker, $original, $correctedContent); + $errors[] = $this->createError( + "残留URL/DOI占位符: {$marker}", + "已恢复为原始内容", + "URL/DOI恢复不完全,已强制恢复", + $originalContent, + $correctedContent, + $posStart, + $posEnd + ); + } + } + } + + $this->handleErrors($errors); + return is_string($correctedContent) ? $correctedContent : $defaultReturn; + } + + /** + * 时间单位缩写校对 + */ + private function checkTimeUnitAbbreviations($content) { + // 初始化错误数组(统一格式) + $errors = []; + // 严格输入验证:空内容/非字符串直接返回 + if (!is_string($content) || trim($content) === '') { + $this->handleErrors($errors); + return $content; + } + + $corrected = $content; + $replaceMap = []; // 存储替换映射 + $originalContent = $corrected; + $searchOffset = 0; // 用于计算错误位置的偏移量(避免重复定位) + + // 定义时间单位转换规则 + $timeUnits = [ + [ + 'full' => 'hour', + 'plural' => 'hours', + 'abbr' => 'h', + 'description' => '小时' + ], + [ + 'full' => 'minute', + 'plural' => 'minutes', + 'abbr' => 'min', + 'description' => '分钟' + ], + [ + 'full' => 'second', + 'plural' => 'seconds', + 'abbr' => 's', + 'description' => '秒' + ] + ]; + + foreach ($timeUnits as $unit) { + // 合并所有匹配模式为单一正则 + $fullPattern = $unit['full'] . 's?'; + $capitalizedPattern = ucfirst($unit['full']) . 's?'; + $abbrPattern = $unit['abbr'] . '|' . strtoupper($unit['abbr']); + + $combinedPattern = "~(\d+(?:\.\d+)?)(?:\s+|)(" . + "{$fullPattern}|{$capitalizedPattern}|{$abbrPattern}" . + ")\b~i"; + + // 正则有效性校验 + if (@preg_match($combinedPattern, '') === false) { + // 正则错误:位置默认-1 + $errorHash = md5('time_unit_regex_error_' . $unit['description']); + // $errors[$errorHash] = $this->createError( + // '时间单位正则错误', + // "跳过{$unit['description']}单位校验", + // "{$unit['description']}单位匹配正则语法错误:{$combinedPattern},已跳过该单位校验", + // $originalContent, + // $corrected, + // -1, + // -1 + // ); + continue; + } + + // 单次匹配所有相关模式 + if (preg_match_all($combinedPattern, $corrected, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $original = $match[0]; // 原始错误内容(如 "5 Hour"、"3 MIN") + $number = $match[1]; // 数字部分(如 "5"、"3") + $unitPart = $match[2]; // 单位部分(如 "Hour"、"MIN") + $fixed = $number . strtolower($unit['abbr']); // 修正后内容(如 "5h"、"3min") + + // 仅处理需要修正的情况 + if ($original !== $fixed) { + // 确定错误类型(细化错误原因) + if (stripos($unitPart, $unit['full']) !== false) { + $errorReason = "应使用缩写形式'{$unit['abbr']}'"; + } elseif (strpos($original, ' ') !== false) { + $errorReason = "数字与缩写间不应有空格"; + } else { + $errorReason = "单位缩写应使用小写'{$unit['abbr']}'"; + } + + // 计算错误内容在原始文本中的位置 + $posStart = strpos($originalContent, $original, $searchOffset); + $posEnd = ($posStart !== false) ? $posStart + strlen($original) : -1; + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($original); // 更新偏移量 + + // 错误信息去重(基于原始内容+修正内容哈希) + $errorHash = md5($original . $fixed); + if (!isset($errors[$errorHash])) { + $errors[$errorHash] = $this->createError( + $original, + $fixed, + "{$unit['description']}单位格式不规范:{$errorReason},正确格式为'数字{$unit['abbr']}'", + $originalContent, + strtr($originalContent, $replaceMap + [$original => $fixed]), + $posStart, + $posEnd + ); + } + + // 记录替换映射(去重,避免重复替换) + if (!isset($replaceMap[$original])) { + $replaceMap[$original] = $fixed; + } + } + } + } + } + + // 批量高效替换 + if (!empty($replaceMap)) { + $corrected = strtr($corrected, $replaceMap); + } + + // 统一处理错误 + $this->handleErrors($errors); + return $corrected; + } + + + /** + * 毫升单位校对 + */ + private function checkMlUnit($content) { + $errors = []; + if (!is_string($content) || trim($content) === '') { + $this->handleErrors($errors); + return $content; + } + + $corrected = $content; + $replaceMap = []; + $originalContent = $corrected; // 保存完整原始内容 + $searchOffset = 0; // 用于计算错误位置的偏移量(避免重复定位) + + // 优化正则规则(精准匹配毫升单位,支持数字前缀和纯单位场景) + $mlPattern = '/\b(\d+(?:\.\d+)?\s*)?(ml)\b/i'; + + // 正则有效性校验 + if (@preg_match($mlPattern, '') === false) { + // // 正则错误:位置默认-1 + // $errorHash = md5('ml_unit_regex_error'); + // $errors[$errorHash] = $this->createError( + // '毫升单位正则错误', + // '跳过毫升单位校验', + // "毫升单位匹配正则语法错误:{$mlPattern},已跳过该校验流程", + // $originalContent, + // $corrected, + // -1, + // -1 + // ); + } elseif (preg_match_all($mlPattern, $corrected, $allMatches, PREG_SET_ORDER)) { + foreach ($allMatches as $matchItem) { + $originalFull = $matchItem[0]; // 原始错误内容(如 "5ml"、" ML") + $prefix = $matchItem[1] ?? ''; // 数字前缀(如 "5"、"3.0 ") + $originalUnit = strtolower($matchItem[2]); // 单位部分("ml") + + // 标准毫升单位格式(L大写) + $fixedFull = "{$prefix}mL"; + + // 仅处理与标准格式不一致的场景 + if ($originalFull !== $fixedFull) { + // 计算错误内容在原始文本中的位置 + $posStart = strpos($originalContent, $originalFull, $searchOffset); + $posEnd = ($posStart !== false) ? $posStart + strlen($originalFull) : -1; + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($originalFull); // 更新偏移量 + + // 错误去重(哈希机制) + $errorHash = md5($originalFull . $fixedFull); + if (!isset($errors[$errorHash])) { + $errors[$errorHash] = $this->createError( + $originalFull, + $fixedFull, + '毫升单位格式不规范,标准写法为"mL"', + $originalContent, + strtr($originalContent, $replaceMap + [$originalFull => $fixedFull]), + $posStart, + $posEnd + ); + } + + // 记录替换映射(去重) + if (!isset($replaceMap[$originalFull])) { + $replaceMap[$originalFull] = $fixedFull; + } + } + } + + // 批量替换 + if (!empty($replaceMap)) { + $corrected = strtr($corrected, $replaceMap); + } + } + + $this->handleErrors($errors); + return $corrected; + } + + + /** + * 显著性P斜体校对 + */ + private function checkPSignificance($content) { + $errors = []; + if (!is_string($content) || trim($content) === '') { + $this->handleErrors($errors); + return $content; + } + + $corrected = $content; + $replaceMap = []; + $originalContent = $corrected; // 保存完整原始内容 + $searchOffset = 0; // 用于计算错误位置的偏移量(避免重复定位) + + // 优化正则规则(覆盖P/p全场景,支持科学计数法) + $pValuePattern = '/\b([Pp])(\s*=?\s*)(\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/'; + + // 正则有效性校验 + if (@preg_match($pValuePattern, '') === false) { + // // 正则错误:位置默认-1 + // $errors[] = $this->createError( + // 'P值正则错误', + // '跳过P值斜体校验', + // "P值匹配正则语法错误:{$pValuePattern},已跳过该校验流程", + // $originalContent, + // $corrected, + // -1, + // -1 + // ); + } elseif (preg_match_all($pValuePattern, $corrected, $allMatches, PREG_SET_ORDER)) { + foreach ($allMatches as $matchItem) { + $original = $matchItem[0]; // 原始P值内容(如 "P=0.05"、"p < 0.01") + $pChar = $matchItem[1]; // P/p字符(如 "P"、"p") + $separator = $matchItem[2];// 分隔符(如 "="、" < ") + $number = $matchItem[3]; // 数值部分(如 "0.05"、"1.2e-3") + + // 生成修正内容(仅P/p加斜体) + $fixed = "{$pChar}{$separator}{$number}"; + + // 仅处理有变化的场景 + if ($original !== $fixed) { + // 计算原始P值内容在完整原始文本中的位置 + $posStart = strpos($originalContent, $original, $searchOffset); + $posEnd = ($posStart !== false) ? $posStart + strlen($original) : -1; + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($original); // 更新偏移量 + + // 错误去重(哈希机制) + $errorHash = md5($original . $fixed); + if (!isset($errors[$errorHash])) { + $errors[$errorHash] = $this->createError( + $original, + $fixed, + '显著性P值格式不规范,P/p应使用斜体', + $originalContent, + strtr($originalContent, $replaceMap + [$original => $fixed]), + $posStart, + $posEnd + ); + } + + // 记录替换映射(去重) + if (!isset($replaceMap[$original])) { + $replaceMap[$original] = $fixed; + } + } + } + + // 批量替换 + if (!empty($replaceMap)) { + $corrected = strtr($corrected, $replaceMap); + } + } + + $this->handleErrors($errors); + return $corrected; + } + + /** + * No. 123456格式统一 + */ + private function checkNoFormatUniformity($content) { + $errors = []; + // 严格输入验证:空内容/非字符串直接返回(保持与checkTextFormat一致) + if (!is_string($content) || trim($content) === '') { + $this->handleErrors($errors); + return $content; + } + + $corrected = $content; + $replaceMap = []; + $originalContent = $corrected; // 备份完整原始内容,用于错误信息的"original"字段 + $searchOffset = 0; // 用于计算错误位置的偏移量(避免重复定位同一错误) + + // 正则规则(精准匹配No.格式,包含大小写、空格、数字场景,如 NO.123、no. 456 等) + $combinedPattern = '/\b([Nn][Oo]\.)(\s*)(\d+)\b/'; + // 正则有效性校验(避免无效正则导致崩溃,与checkTextFormat逻辑一致) + if (@preg_match($combinedPattern, '') === false) { + // // 正则错误:位置默认-1 + // $errorHash = md5('no_format_regex_error'); + // $errors[$errorHash] = $this->createError( + // 'No.格式正则错误', // verbatim_texts:具体错误标识 + // '跳过No.格式校验', // revised_content:处理结果 + // "No.格式匹配正则语法错误:{$combinedPattern},已跳过该校验流程", // explanation:错误说明 + // $originalContent, // original:完整原始内容 + // $corrected, // corrected:当前完整修正内容(未处理,故与原始一致) + // -1, // position_start:默认-1(定位失败) + // -1 // position_end:默认-1(定位失败) + // ); + } elseif (preg_match_all($combinedPattern, $corrected, $matches, PREG_SET_ORDER)) { + foreach ($matches as $item) { + $originalFull = $item[0]; // 匹配到的单个错误片段(如 "NO.123"、"no. 456") + $originalPrefix = $item[1]; // 前缀部分(如 "NO."、"no.") + $spaces = $item[2]; // 空格部分(如空、单个空格、多个空格) + $number = $item[3]; // 数字部分(如 "123"、"456") + + // 标准化格式:No.(首字母大写+o小写+点) + 1个空格 + 数字 + $fixedPrefix = 'No.'; + $fixedSpaced = ' '; + $fixedFull = "{$fixedPrefix}{$fixedSpaced}{$number}"; // 单个错误片段的修正结果 + + // 仅处理与标准格式不一致的场景(避免无意义的替换和错误记录) + if ($originalFull !== $fixedFull) { + // 计算错误片段在完整原始文本中的位置 + $posStart = strpos($originalContent, $originalFull, $searchOffset); + $posEnd = ($posStart !== false) ? $posStart + strlen($originalFull) : -1; + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($originalFull); // 更新偏移量,避免重复定位 + + // 细化错误原因(分场景说明,提升可读性) + $errorReasons = []; + if ($originalPrefix !== $fixedPrefix) { + $errorReasons[] = "前缀格式不规范(应使用 \"No.\",当前为 \"{$originalPrefix}\")"; + } + if (trim($spaces) !== $fixedSpaced || strlen($spaces) !== 1) { + $errorReasons[] = empty($spaces) + ? '缺少空格(.后需加1个空格)' + : "空格数量不规范(当前为 " . strlen($spaces) . " 个,应保留1个空格)"; + } + + // 记录替换映射(去重,避免重复处理相同错误片段) + if (!isset($replaceMap[$originalFull])) { + $replaceMap[$originalFull] = $fixedFull; + } + + // 错误信息去重(基于单个错误片段的原始值+修正值哈希,避免重复记录) + $errorHash = md5($originalFull . $fixedFull); + if (!isset($errors[$errorHash])) { + $errors[$errorHash] = $this->createError( + $originalFull, // verbatim_texts:具体错误片段 + $fixedFull, // revised_content:错误片段的修正结果 + 'No. 格式不规范:' . implode(',', $errorReasons) . ',正确格式为「No. 数字」', // explanation:错误说明 + $originalContent, // original:完整原始内容(整个输入文本) + strtr($originalContent, $replaceMap), // corrected:完整修正内容(基于当前替换映射生成) + $posStart, // position_start:错误起始位置 + $posEnd // position_end:错误结束位置 + ); + } + } + } + // 批量替换所有错误片段(高效处理,避免循环内重复替换) + if (!empty($replaceMap)) { + $corrected = strtr($corrected, $replaceMap); + } + } + + $this->handleErrors($errors); + return $corrected; + } + + /** + * 图表标题一律使用全称Figure 1, Table 1.不能写成Fig. 1, Tab 1. + */ + private function checkFigureTableTitle($content) { + $errors = []; + // 严格输入验证:空内容/非字符串直接返回 + if (!is_string($content) || trim($content) === '') { + $this->handleErrors($errors); + return $content; + } + + $corrected = $content; + $replaceMap = []; + $originalContent = $corrected; // 备份原始内容,用于错误信息 + $searchOffset = 0; // 错误位置计算偏移量,避免重复定位 + + // 图表标题匹配正则(支持 Fig/Figs/Tab/Tabs、特殊空格、数字范围) + $titlePattern = '/(?createError( + // '图表标题正则错误', + // '跳过图表标题格式校验', + // "图表标题匹配正则语法错误:{$titlePattern},已跳过该校验流程", + // $originalContent, + // $corrected, + // -1, + // -1 + // ); + } else { + // 全局匹配所有图表标题格式 + $matchCount = preg_match_all($titlePattern, $corrected, $allMatches, PREG_SET_ORDER); + + if ($matchCount > 0) { + foreach ($allMatches as $matchItem) { + $originalFull = $matchItem[0]; // 完整错误片段(如 "Fig 1"、"Tabs-2") + $abbrBase = $matchItem[1]; // 缩写主体(Fig/Figs/Tab/Tabs) + $dot = $matchItem[2]; // 可能的点(.) + $space = $matchItem[3]; // 可能的空格(含特殊空格) + $number = $matchItem[4]; // 数字部分(支持范围如 "2-3") + + // 确定全称及错误描述 + switch (strtolower($abbrBase)) { + case 'fig': + $fullName = 'Figure'; + $errorDesc = '图表标题使用缩写"Fig",应改为全称"Figure"'; + break; + case 'figs': + $fullName = 'Figures'; + $errorDesc = '图表标题复数使用缩写"Figs",应改为全称"Figures"'; + break; + case 'tab': + $fullName = 'Table'; + $errorDesc = '表格标题使用缩写"Tab",应改为全称"Table"'; + break; + case 'tabs': + $fullName = 'Tables'; + $errorDesc = '表格标题复数使用缩写"Tabs",应改为全称"Tables"'; + break; + default: + $fullName = ''; + $errorDesc = ''; + continue 2; // 修复警告:跳出 switch + 跳过当前 foreach 迭代 + } + + // 生成标准格式(全称 + 单个空格 + 数字) + $fixed = "{$fullName} {$number}"; + + // 仅处理需要修正的场景(避免无意义操作) + if ($originalFull !== $fixed) { + // 计算错误片段在原始文本中的位置 + $posStart = strpos($originalContent, $originalFull, $searchOffset); + $posEnd = ($posStart !== false) ? $posStart + strlen($originalFull) : -1; + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($originalFull); + + // 错误信息去重(基于原始+修正内容哈希) + $errorHash = md5($originalFull . $fixed); + if (!isset($errors[$errorHash])) { + // 生成临时修正内容,用于错误信息预览 + $tempReplace = $replaceMap; + $tempReplace[$originalFull] = $fixed; + $currentCorrected = strtr($originalContent, $tempReplace); + + $errors[$errorHash] = $this->createError( + $originalFull, + $fixed, + $errorDesc, + $originalContent, + $currentCorrected, + $posStart, + $posEnd + ); + } + + // 记录替换规则(去重,避免重复替换) + if (!isset($replaceMap[$originalFull])) { + $replaceMap[$originalFull] = $fixed; + } + } + } + + // 批量执行所有替换(高效处理) + if (!empty($replaceMap)) { + $corrected = strtr($corrected, $replaceMap); + } + } + } + + // 处理错误信息(需确保 handleErrors 方法已实现) + $this->handleErrors($errors); + return $corrected; + } + + + + /** + * 添加错误信息 + */ + private function addError($error = []) { + if (!empty($error) && is_array($error)) { + // 确保错误信息结构完整 + $safeError = array_merge([ + 'verbatim_texts' => '', + 'revised_content' => '', + 'explanation' => '', + 'original' => '', + 'corrected' => '', + 'position_start' => '', + 'position_end' => '', + ], $error); + $this->errors[] = $safeError; + } + } + + /** + * 处理错误信息(去重和存储) + */ + private function handleErrors($errors) { + if (empty($errors)) return; + + // 错误去重 + $uniqueErrors = []; + foreach ($errors as $error) { + $errorHash = md5($error['verbatim_texts'] . $error['revised_content']. $error['position_start']. $error['position_end']); + if (!isset($uniqueErrors[$errorHash])) { + $uniqueErrors[$errorHash] = $error; + } + } + + // 批量添加错误 + foreach (array_values($uniqueErrors) as $error) { + $this->addError($error); + } + } + + /** + * 创建标准化错误信息 + */ + private function createError($verbatim, $revised, $explanation,$original,$corrected, $position_start=-1, $position_end=-1) { + return [ + 'verbatim_texts' => $verbatim, + 'revised_content' => $revised, + 'explanation' => $explanation, + 'original' => $original, + 'corrected' => $corrected, + 'position_start' => $position_start, // 错误起始位置 + 'position_end' => $position_end // 错误结束位置 + ]; + } + /** + * 检查doi链接是否都能打开 + */ + private function checkDoi($content) { + $errors = []; + if (!is_string($content) || trim($content) === '') { + $this->handleErrors($errors); // 注意:原代码笔误“handleErrorsErrors”已修正 + return $content; + } + + $corrected = $content; + $originalContent = $corrected; + $checkedDois = []; // 用于去重,避免同一DOI重复校验 + + try { + // 优化正则:匹配标准DOI格式(覆盖所有常见场景) + // 匹配规则说明: + // 1. (?createError( + 'DOI正则错误', + '跳过DOI校验', + "DOI匹配正则语法错误:{$doiPattern},已跳过该校验流程", + $originalContent, + $corrected + ); + } else { + // 匹配所有符合标准的DOI(PREG_SET_ORDER按匹配项分组) + $matchCount = preg_match_all($doiPattern, $corrected, $allMatches, PREG_SET_ORDER); + + if ($matchCount > 0) { + foreach ($allMatches as $matchItem) { + $fullDoi = strtolower($matchItem[1]) . ':' . $matchItem[2]; // 完整DOI(统一转为小写,如“doi:10.1017/abc”) + $doiCore = $matchItem[2]; // DOI核心部分(如“10.1017/abc”,用于拼接访问链接) + + // 去重:同一DOI仅校验一次 + if (isset($checkedDois[$fullDoi])) { + continue; + } + $checkedDois[$fullDoi] = true; + + // 测试DOI链接是否可访问 + $isAccessible = $this->testDoiAccessibility($doiCore); + // 生成错误/状态信息 + if ($isAccessible) { + $errorDesc = "DOI「{$fullDoi}」格式规范,且链接可正常访问"; + } else { + $errorDesc = "DOI「{$fullDoi}」格式规范,但链接无法访问(可能无效或网络问题)"; + } + + // 记录校验结果(DOI无需修正,仅记录状态) + $errors[] = $this->createError( + $fullDoi, + $fullDoi, // 修正后内容与原始一致(DOI格式无需修改) + $errorDesc, + $originalContent, + $corrected + ); + } + } else { + // 无匹配时记录提示(可选,根据业务需求决定是否保留) + $errors[] = $this->createError( + '未匹配到DOI', + '无修正', + '文本中未发现符合标准格式的DOI(如doi:10.1017/abc、DOI: 10.1038/nature12345)', + $originalContent, + $corrected + ); + } + } + + } catch (Exception $e) { + $errors[] = $this->createError( + 'DOI校验全局异常', + '已回滚原始内容', + "DOI校验出错:{$e->getMessage()}(行号:{$e->getLine()}),已恢复原始输入", + $originalContent, + $originalContent + ); + $corrected = $originalContent; + } + + $this->handleErrors($errors); + return $corrected; + } + + /** + * 测试DOI链接是否可访问(基于DOI官方解析地址) + * @param string $doiCore DOI核心部分(如“10.1017/abc”,不含“doi:”前缀) + * @return bool 可访问返回true,否则返回false + */ + private function testDoiAccessibility($doiCore) { + // 处理DOI核心部分的空格(若存在) + $doiCore = trim($doiCore); + // DOI官方解析地址:https://doi.org/ + 编码后的DOI核心部分 + $doiUrl = 'https://doi.org/' . $doiCore; + var_dump($doiUrl,$doiCore);exit; + + // 初始化cURL(支持HTTPS,忽略证书问题避免环境限制) + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $doiUrl, + CURLOPT_RETURNTRANSFER => true, // 不直接输出响应 + CURLOPT_HEADER => true, // 获取响应头(用于判断状态码) + CURLOPT_TIMEOUT => 15, // 超时时间(避免长时间阻塞) + CURLOPT_FOLLOWLOCATION => true, // 跟随301/302重定向(DOI常跳转到期刊页面) + CURLOPT_SSL_VERIFYPEER => false, // 忽略SSL证书校验(适合测试环境) + CURLOPT_SSL_VERIFYHOST => false + ]); + + curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // 获取HTTP状态码 + curl_close($ch); + + // 状态码200-399表示可访问(200成功,3xx重定向均视为有效) + return $httpCode >= 200 && $httpCode < 400; + } +} +?>