diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index 27037a8..7b4207a 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -1724,6 +1724,16 @@ class Article extends Base } //文章状态修改为退修状态 给同意审稿但一直未审稿人发送邮件且扣减其分数值 chengxiaoling 20250617 end + //文章状态修改为预接收 对作者提交的稿件内容进行校对 chengxiaoling 20251013 start + if ($data['state'] == 6) { + $iArticleId = empty($article_info['article_id']) ? 0 : $article_info['article_id']; + if (!empty($iArticleId)) { + $iSeconds = 120;//两分钟后执行 + Queue::later($iSeconds,'app\api\job\ArticleProofRead@fire',['article_id' => $iArticleId], 'ArticleProofRead'); + } + } + //文章状态修改为预接收 对作者提交的稿件内容进行校对 chengxiaoling 20251013 end + //重新计算审稿人的审稿质量 chengxiaoling start 0416 $this->reviewQuality($article_info['article_id']); //重新计算审稿人的审稿质量 chengxiaoling end 0416 @@ -4610,7 +4620,7 @@ class Article extends Base */ private function messageTips($article_id, $user_id) { - $article = $this->article_obj->field('t_article.user_id,t_article.editor_id,t_article.accept_sn,t_journal.journal_id,t_journal.title,t_journal.email,t_journal.epassword') + $article = $this->article_obj->field('t_article.user_id,t_article.editor_id,t_article.accept_sn,t_journal.journal_id,t_journal.title,t_journal.email,t_journal.epassword,t_journal.issn as journal_issn,t_journal.website as journal_website') ->join('t_journal', 't_journal.journal_id = t_article.journal_id', 'LEFT') ->where('t_article.article_id', $article_id) ->find(); @@ -4629,12 +4639,64 @@ class Article extends Base if ($res['type'] == 2) { //编辑 - 修改editor_act,并发送给作者发邮件 $this->article_obj->where('article_id', $article_id)->update(['editor_act' => 1]); $author = $this->user_obj->where('user_id', $article['user_id'])->find(); - // 发邮件 - $content = 'Dear ' . $author['realname'] . ',
'; - $content .= 'Thank you for contacting our editor.
ID: ' . $article['accept_sn'] . '.

'; - $content .= ' Your manuscript: ' . $article['accept_sn'] . ' has received a new reply; please login https://submission.tmrjournals.com/login to check.
'; - $content .= 'Sincerely,
' . $article['title']; - sendEmail($author['email'], $article['title'], $article['title'], $content, $article['email'], $article['epassword']); + // // 发邮件 + // $content = 'Dear ' . $author['realname'] . ',
'; + // $content .= 'Thank you for contacting our editor.
ID: ' . $article['accept_sn'] . '.

'; + // $content .= ' Your manuscript: ' . $article['accept_sn'] . ' has received a new reply; please login https://submission.tmrjournals.com/login to check.
'; + // $content .= 'Sincerely,
' . $article['title']; + // sendEmail($author['email'], $article['title'], $article['title'], $content, $article['email'], $article['epassword']); + //邮件内容修改 chengxiaoling 20251015 start + //邮箱 + $email = empty($author['email']) ? '' : $author['email']; + if(!empty($email)){ + //邮件模版 + $aEmailConfig = [ + 'email_subject' => 'Please Check the Editorial Comments in the Submission System', + 'email_content' => 'Dear {author_name},

+ + We would like to inform you that our editors have left a message regarding your submission in the online submission system at https://submission.tmrjournals.com/login.
+ Please log in to your account to view the message and respond at your earliest convenience.

+ + If you have any questions or encounter any issues accessing the submission system, please feel free to contact us.

+ + Sincerely,
+ Editorial Office
+ {journal_title}
+ Subscribe to this journal
+ Email: {journal_email}
+ Website: {website}' + ]; + + //作者姓名 + $realname = empty($author['realname']) ? '' : $author['realname']; + $realname = empty($author['account']) ? $realname : $author['account']; + + //期刊标题 + $from_name = empty($article['title']) ? '' : $article['title']; + //发送邮件 + $memail = empty($article['email']) ? '' : $article['email']; + $mpassword = empty($article['epassword']) ? '' : $article['epassword']; + //处理数据 + $aSearch = [ + '{journal_title}' => $from_name,//期刊名 + '{journal_issn}' => empty($article['journal_issn']) ? '' : $article['journal_issn'], + '{journal_email}' => empty($article['email']) ? '' : $article['email'], + '{website}' => empty($article['journal_website']) ? '' : $article['journal_website'], + '{author_name}' => $realname + ]; + + //邮件标题 + $title = empty($aEmailConfig['email_subject']) ? '' : $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; + //发送邮件 + $result = sendEmail($email,$title,$from_name,$content,$memail,$mpassword); + } + //邮件内容修改 chengxiaoling 20251015 end } } diff --git a/application/api/controller/Board.php b/application/api/controller/Board.php index 8736423..623ea30 100644 --- a/application/api/controller/Board.php +++ b/application/api/controller/Board.php @@ -838,6 +838,127 @@ class Board extends Base { } } + /** + * 新增编辑编委信息-新 + */ + public function addBoardNew(){ + $data = $this->request->post(); + $rule = new Validate([ + "user_id"=>"require", + "journal_id"=>"require", + "type"=>"require", + "board_group_id"=>"require", + "research_areas"=>"require", + "realname"=>"require", + "email"=>"require", + "website"=>"require", + "affiliation"=>"require", + "technical"=>"require", + 'icon' => "require" + ]); + if(!$rule->check($data)){ + return jsonError($rule->getError()); + } + $check = $this->board_to_journal_obj->where('user_id',$data['user_id'])->where('state',0)->find(); + if($check){ + return jsonError("According to TMR Publishing Group Policy, scientists are not allowed to serve on the editorial board of more than one journal at the same time."); + } + //新增必填字段 chengxiaoling 20251020 start + //必填验证 + $sField = ''; + $aFields = ['research_areas','realname','email','website','affiliation','technical','icon']; + foreach ($aFields as $value) { + $sInfo = empty($data[$value]) ? '' : trim($data[$value]); + if(empty($sInfo)){ + $sField = $value; + break; + } + } + if(!empty($sField)){ + return jsonError($sField.' cannot be empty'); + } + //新增必填字段 chengxiaoling 20251020 end + //添加对应关系 + $insert['user_id'] = $data['user_id']; + $insert['journal_id'] = $data['journal_id']; + $insert['type'] = $data['type']; + $insert['board_group_id'] = $data['board_group_id']; + $insert['research_areas'] = trim($data['research_areas']); + //新增必填字段 chengxiaoling 20251020 start + $insert['realname'] = trim($data['realname']); + $insert['email'] = trim($data['email']); + $insert['website'] = trim($data['website']); + $insert['affiliation'] = trim($data['affiliation']); + $insert['technical'] = trim($data['technical']); + $insert['icon'] = trim($data['icon']); + //新增必填字段 chengxiaoling 20251020 end + $this->board_to_journal_obj->insert($insert); + return jsonSuccess([]); + } + /** + * 修改编辑编委信息-新 + */ + public function editBoardNew(){ + $data = $this->request->post(); + $rule = new Validate([ + "btj_id"=>"require", + "type"=>'require', + "board_group_id"=>"require", + "research_areas"=>"require", + "realname"=>"require", + "email"=>"require", + "website"=>"require", + "affiliation"=>"require", + "technical"=>"require", + 'icon' => "require" + ]); + if(!$rule->check($data)){ + return jsonError($rule->getError()); + } + //新增必填字段 chengxiaoling 20251020 start + //必填验证 + $sField = ''; + $aFields = ['research_areas','realname','email','website','affiliation','technical','icon']; + foreach ($aFields as $value) { + $sInfo = empty($data[$value]) ? '' : trim($data[$value]); + if(empty($sInfo)){ + $sField = $value; + break; + } + } + if(!empty($sField)){ + return jsonError($sField.' cannot be empty'); + } + //新增必填字段 chengxiaoling 20251020 end + $update['type'] = $data['type']; + $update['board_group_id'] = $data['type']==2?$data['board_group_id']:0; + $update['research_areas'] = trim($data['research_areas']); + //新增必填字段 chengxiaoling 20251020 start + $update['realname'] = trim($data['realname']); + $update['email'] = trim($data['email']); + $update['website'] = trim($data['website']); + $update['affiliation'] = trim($data['affiliation']); + $update['technical'] = trim($data['technical']); + $update['icon'] = trim($data['icon']); + //新增必填字段 chengxiaoling 20251020 end + $this->board_to_journal_obj->where('btj_id',$data['btj_id'])->update($update); + return jsonSuccess([]); + } + /** + * 编委上传头像 + */ + public function uploadIcon() + { + $file = request()->file('icon'); + if ($file) { + $info = $file->move(ROOT_PATH . 'public' . DS . 'boardusericon'); + if ($info) { + return json(['code' => 0, 'upurl' => str_replace("\\", "/", $info->getSaveName())]); + } else { + return json(['code' => 1, 'msg' => $file->getError()]); + } + } + } } diff --git a/application/api/controller/Chief.php b/application/api/controller/Chief.php index 983ad6c..368a54c 100644 --- a/application/api/controller/Chief.php +++ b/application/api/controller/Chief.php @@ -531,6 +531,14 @@ class Chief extends Controller { public function checkEmailForUser(){ $data = $this->request->post(); $user_info = $this->user_obj->where('account|email',$data['email'])->find(); + + //新增查询数据 chengxiaoling 20251020 start + if(!empty($user_info)){ + $aWhere = ['reviewer_id' => $user_info['user_id']]; + $user_reviewer_info = Db::name('user_reviewer_info')->field('technical,company as affiliation,website,field as research_areas')->where($aWhere)->find(); + $user_info = empty($user_reviewer_info) ? $user_info : array_merge($user_info,$user_reviewer_info); + } + //新增查询数据 chengxiaoling 20251020 end $re['user_info'] = $user_info; return jsonSuccess($re); } diff --git a/application/api/controller/Preaccept.php b/application/api/controller/Preaccept.php index ba66f5b..9f08e11 100644 --- a/application/api/controller/Preaccept.php +++ b/application/api/controller/Preaccept.php @@ -47,33 +47,6 @@ class Preaccept extends Base return jsonSuccess($re); } - - public function spiltContent(){ - $data = $this->request->post(); - $rule = new Validate([ - "content"=>"require" - ]); - if(!$rule->check($data)){ - return jsonError($rule->getError()); - } - $text = $data['content']; - - $text = preg_replace('/(\d+)([a-zA-Z%℃°]+)/', '$1 $2', $text); - - $text = preg_replace('/\s*%\s*/', '%', $text); - - - $pattern_en_dash = '/(\d+)–(\d+)/'; - $text = preg_replace($pattern_en_dash, '$1-$2', $text); - - - $pattern_em_dash = '/(\d+)—(\d+)/'; - $text = preg_replace($pattern_em_dash, '$1-$2', $text); - - - - } - /**清空引用文献 * @return \think\response\Json * @throws \think\Exception @@ -1503,5 +1476,97 @@ return null; } } + public function getArticleMainsNew(){ + $data = $this->request->post(); + $rule = new Validate([ + "article_id"=>"require" + ]); + if(!$rule->check($data)){ + return jsonError($rule->getError()); + } -} \ No newline at end of file + //定义空数组 + $re = ['list' => []]; + //获取数量 + $aWhere = ['article_id' => $data['article_id'],'state' => ['in',[0,2]]]; + $iCount = $this->article_main_obj->where($aWhere)->count(); + if(empty($iCount)){ + $this->addArticleMainEx($data['article_id']); + } + + //获取数据 + $aArticleMain = Db::name("article_main")->where($aWhere)->order("sort asc")->select(); + if(empty($aArticleMain)){ + return null; + } + + //处理数据 + $iSize = 300; + $aChunk = array_chunk($aArticleMain, $iSize); + $mains = []; + foreach ($aChunk as $item) { + $aMId = array_column($item, 'am_id'); + //查询article_main_check + $aWhere = ['am_id' => ['in',$aMId],'state' => 0]; + $aMainCheck = Db::name("article_main_check")->where($aWhere)->select(); + $aMainCheckData = []; + if(!empty($aMainCheck)){ + foreach ($aMainCheck as $value) { + if(empty($value['am_id'])){ + continue; + } + $aMainCheckData[$value['am_id']][] = $value; + } + } + + //获取图片信息 + $aMiId = array_unique(array_column($item, 'ami_id')); + $aWhere = ['ami_id' => ['in',$aMiId],'state' => 0]; + $aArticleMainImage = Db::name("article_main_image")->where($aWhere)->select(); + $aArticleMainImage = empty($aArticleMainImage) ? [] : array_column($aArticleMainImage, null,'ami_id'); + //获取表格信息 + $aMiId = array_unique(array_column($item, 'amt_id')); + $aWhere = ['amt_id' => ['in',$aMiId],'state' => 0]; + $aArticleMainTable = Db::name("article_main_table")->where($aWhere)->select(); + $aArticleMainTable = empty($aArticleMainTable) ? [] : array_column($aArticleMainTable, null,'amt_id'); + + //查询校对数量 t_article_proofread + $aWhere = ['am_id' => ['in',$aMId],'state' => ['between',[1,2]],'is_delete' => 2]; + $aArticleProofread = Db::name("article_proofread")->field('am_id,count(id) as num,state')->where($aWhere)->group('am_id,state')->select(); + $aArticleProofreadData = []; + if(!empty($aArticleProofread)){ + foreach ($aArticleProofread as $key => $value) { + $aArticleProofreadData[$value['am_id']][$value['state']] = $value['num']; + } + } + + foreach ($item as $k => $main) { + if($main['is_h1']==1){ + $main['content'] = "".$main['content'].""; + } + if($main['is_h2']==1||$main['is_h3']==1){ + $main['content'] = "".$main['content'].""; + } + $main['checks'] = empty($aMainCheckData[$main['am_id']]) ? [] : $aMainCheckData[$main['am_id']]; + if($main['type'] == 1){ + $main['image'] = empty($aArticleMainImage[$main['ami_id']]) ? [] : $aArticleMainImage[$main['ami_id']]; + } + if($main['type'] == 2){ + $main['table'] = empty($aArticleMainTable[$main['amt_id']]) ? [] : $aArticleMainTable[$main['amt_id']]; + } + if($main['type'] == 0){ + $aDataInfo = empty($aArticleProofreadData[$main['am_id']]) ? [] : $aArticleProofreadData[$main['am_id']]; + $main['proof_read_num'] = -1; + if(!empty($aDataInfo)){ + $main['proof_read_num'] = empty($aDataInfo[2]) ? 0 : $aDataInfo[2]; + } + } + $mains[] = $main; + } + + } + $re['list'] = $mains; + return jsonSuccess($re); + } + +} diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index 7d55dcd..46e4aa3 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -24,8 +24,6 @@ class Proofread extends Base 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; @@ -34,12 +32,13 @@ class Proofread extends Base if(empty($aArticle)){ return json_encode(array('status' => 3,'msg' => 'No articles requiring review were found' )); } - if($aArticle['state'] != 6){ + + if($aArticle['state'] != 6 && $aArticle['state'] != 5){ return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); } //查询是否进行过校对 - $aProofReadWhere = ['article_id' => $iArticleId,'state' => 2]; + $aProofReadWhere = ['article_id' => $iArticleId,'state' => 2,'is_delete' => 2]; if(!empty($iAmId)){ $aProofReadWhere['am_id'] = $iAmId; } @@ -47,15 +46,11 @@ class Proofread extends Base 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(); + $aArticleMain = Db::table('t_article_main')->field('am_id,content,type,is_h1,is_h2,is_h3,is_proofread')->where($aWhere)->order('sort asc')->select(); if(empty($aArticleMain)){ return json_encode(array('status' => 5,'msg' => 'The content of the article is empty')); } @@ -64,11 +59,14 @@ class Proofread extends Base $oHelperFunction = new \app\common\HelperFunction; $oProofReadService = new \app\common\ProofReadService; //数据处理 - $aH = $aTable = []; + $aId = []; foreach ($aArticleMain as $key => $value) { if(empty($oHelperFunction->filterAllTags($value['content']))){ continue; } + if($value['is_proofread'] == 2){ + $aId[] = $value['am_id']; + } $aResult = $oProofReadService->proofread($value['content']); if(empty($aResult)){ continue; @@ -77,6 +75,15 @@ class Proofread extends Base $aError[] = $aResult; } if(empty($aError)){ + //更新文章内容的校对时间及状态 + $iCount = count($aId); + if($iCount > 0){ + $aUpdateWhere = ['am_id' => ['in',$aId]]; + $update_result = Db::name('article_main')->where($aUpdateWhere)->limit($iCount)->update(['is_proofread' => 1,'proofread_time' => time()]); + if($update_result === false){ + return json_encode(array('status' => 6,'msg' => 'Failed to update paragraph proofreading status-'.$update_result)); + } + } return json_encode(array('status' => 1,'msg' => 'No errors found')); } //数据处理 @@ -89,21 +96,29 @@ class Proofread extends Base $val['article_id'] = $iArticleId; $val['proof_before'] = empty($value['proof_before']) ? '' : $value['proof_before']; $val['proof_after'] = empty($value['proof_after']) ? '' : $value['proof_after']; + $val['create_time'] = time(); $aData[] = $val; } } if(empty($aData)){ return json_encode(array('status' => 1,'msg' => 'Data processing failed')); } + + Db::startTrans(); + //更新文章内容的校对时间及状态 + $iCount = count($aId); + $update_result = true; + if($iCount > 0){ + $aUpdateWhere = ['am_id' => ['in',$aId]]; + $update_result = Db::name('article_main')->where($aUpdateWhere)->limit($iCount)->update(['is_proofread' => 1,'proofread_time' => time()]); + } //插入 $result = Db::name('article_proofread')->insertAll($aData); - if(!$result){ - return json_encode(array('status' => 6,'msg' => 'No errors found')); + if(!$result || $update_result === false){ + return json_encode(array('status' => 6,'msg' => 'Data operation failed:insert-'.$result.';update-'.$update_result)); } - - //查询参考文献 - $aWhere = ['state' => ['in',[0,2]],'article_id' => $iArticleId]; - return json_encode(['status' => 1,'msg' => 'success']); + Db::commit(); + return json_encode(['status' => 1,'msg' => 'Proofreading successful']); } /** * @title 获取每行校对记录 @@ -129,7 +144,7 @@ class Proofread extends Base if(empty($aArticle)){ return json_encode(['status' => 3,'msg' => 'The query article does not exist']); } - if($aArticle['state'] < 5 || $aArticle['state'] == 8){ + if($aArticle['state'] != 6 && $aArticle['state'] != 5){ return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); } @@ -147,8 +162,8 @@ class Proofread extends Base //查询校对内容 $aAmId = array_column($aArticleMain, 'am_id'); - $aWhere = ['am_id' => ['in',$aAmId]]; - $iState = empty($aParam['state']) ? 0 : $aParam['state']; + $aWhere = ['am_id' => ['in',$aAmId],'is_delete' => 2]; + $iState = empty($aParam['state']) ? 0 : explode(',', $aParam['state']); if(!empty($iState)){ $aWhere['state'] = ['in',$iState]; } @@ -260,7 +275,7 @@ class Proofread extends Base } //判断校对记录 - $aWhere = ['am_id' => $iAmId,'article_id' => $iArticleId,'id' => $iId]; + $aWhere = ['am_id' => $iAmId,'article_id' => $iArticleId,'id' => $iId,'is_delete' => 2]; $aProofRead = Db::name('article_proofread')->where($aWhere)->find(); if(empty($aProofRead)){ @@ -316,6 +331,7 @@ class Proofread extends Base } Db::startTrans(); //更新原始内容 + $result_main = true; if(!empty($sUpdateContent)){ $aWhere = ['am_id' => $iAmId,'state' => 0,'type' => 0]; $aUpdate = ['content' => $sUpdateContent]; @@ -326,12 +342,20 @@ class Proofread extends Base //数据库更新 $aWhere = ['id' => $iId]; $result_proofread= Db::name('article_proofread')->where($aWhere)->limit(1)->update($aUpdate); - if(!$result_proofread || !$result_main){ + if($result_proofread === false || $result_main === false){ return json_encode(['status' => 7,'msg' => "Update failed"]); } Db::commit(); + //查询未执行的数量 + $aCount = json_decode($this->getCountByState(['article_id' => $iArticleId,'am_id' => $iAmId]),true); + $aCount = empty($aCount['data']) ? [] : $aCount['data']; + //返回结果 - return json_encode(['status' => 1,'msg' => "Update successful",'data' => $sData]); + $aData = []; + $aData['sum_count'] = empty($aCount['sum_count']) ? 0 : $aCount['sum_count']; + $aData['am_id_count'] = empty($aCount['am_id_count']) ? 0 : $aCount['am_id_count']; + $aData['content'] = $sData; + return json_encode(['status' => 1,'msg' => "Update successful",'data' => $aData]); } /** @@ -469,7 +493,7 @@ class Proofread extends Base $iIsUpdateAll = empty($aParam['is_update_all']) ? 2 : $aParam['is_update_all']; //查询内容是否存在 - $aWhere = ['am_id' => $iAmId,'article_id' => $iArticleId,'id' => $iId]; + $aWhere = ['am_id' => $iAmId,'article_id' => $iArticleId,'id' => $iId,'is_delete' => 2]; $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']); @@ -500,4 +524,244 @@ class Proofread extends Base //返回结果 return json_encode(['status' => 1,'msg' => "Update successful"]); } + + + /** + * @title 根据状态统计数量 + * @param article_id 文章ID + * @param am_id 行号 + * @param state 状态1已执行2未执行3删除 + */ + public function getCountByState($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'] != 6 && $aArticle['state'] != 5){ + return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); + } + + //查询校对内容 + $aState = empty($aParam['state']) ? [1,2] : explode(',', $aParam['state']); + $aWhere['state'] = ['in',$aState]; + $aWhere['is_delete'] = 2; + $aCount = Db::name('article_proofread')->field('am_id,count(id) as num,state')->where($aWhere)->group('am_id,state')->order('am_id')->select(); + $aCountData = []; + $iCount = -1; + if(!empty($aCount)){ + $iCount = 0; + foreach ($aCount as $key => $value) { + $aCountData[$value['am_id']][$value['state']] = $value['num']; + if($value['state'] == 2){ + $iCount += $value['num']; + } + } + } + //总数量 + $am_id_count = -1; + if(!empty($aCountData[$iAmId])){ + $am_id_count = empty($aCountData[$iAmId][2]) ? 0 : $aCountData[$iAmId][2]; + } + $is_proofread = -1; + //查询是否校对 + $aWhere = ['state' => 0,'is_proofread' => 1]; + if(!empty($iAmId)){ + $aWhere['am_id'] = $iAmId; + } + $iProofCount = Db::name('article_main')->where($aWhere)->count(); + $is_proofread = $iProofCount > 0 ? 1 : 2; + + //总数量统计 + if($iCount == -1 && $iProofCount > 0){ + $iCount = 0; + } + if($am_id_count == -1 && $iProofCount > 0){ + $am_id_count = 0; + } + return json_encode(['status' => 1,'msg' => 'success','data' => ['sum_count' => $iCount,'am_id_count' => $am_id_count,'is_proofread' => $is_proofread]]); + } + + /** + * @title AI文章校对-行 + * @param article_id 文章ID + */ + public function proofReadByLine(){ + + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + $iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(array('status' => 2,'msg' => 'Please select an article' )); + } + //行号 + $iAmId = empty($aParam['am_id']) ? 0 : $aParam['am_id']; + if(empty($iAmId)){ + return json_encode(array('status' => 2,'msg' => 'Please select the rows that need to be proofread' )); + } + + //查询文章 + $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 && $aArticle['state'] != 5){ + return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); + } + + //查询文章内容 + $aWhere['type'] = 0; + $aWhere['content'] = ['<>','']; + $aWhere['state'] = 0; + $aWhere['am_id'] = $iAmId; + $aArticleMain = Db::table('t_article_main')->field('am_id,content,type,is_h1,is_h2,is_h3,is_proofread')->where($aWhere)->find(); + if(empty($aArticleMain['content'])){ + return json_encode(array('status' => 5,'msg' => 'The content of the article is empty')); + } + //写入校对行队列 + $sQueue = \think\Queue::push('app\api\job\ArticleProofReadLine@fire',$aParam,'ArticleProofReadLine'); + return json_encode(array('status' => 1,'msg' => 'Proofreading in progress, check the results in one minute')); + } + /** + * @title AI文章校对-行队列 + * @param article_id 文章ID + */ + public function proofReadLineQueue($aParam = []){ + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + $iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(array('status' => 2,'msg' => 'Please select an article' )); + } + //行号 + $iAmId = empty($aParam['am_id']) ? 0 : $aParam['am_id']; + if(empty($iAmId)){ + return json_encode(array('status' => 2,'msg' => 'Please select the rows that need to be proofread' )); + } + + //查询文章 + $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 && $aArticle['state'] != 5){ + return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); + } + + //查询文章内容 + $aWhere['type'] = 0; + $aWhere['content'] = ['<>','']; + $aWhere['state'] = 0; + $aWhere['am_id'] = $iAmId; + $aArticleMain = Db::table('t_article_main')->field('am_id,content,type,is_h1,is_h2,is_h3,is_proofread')->where($aWhere)->find(); + 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; + //数据处理 + + if(empty($oHelperFunction->filterAllTags($aArticleMain['content']))){ + return json_encode(array('status' => 6,'msg' => 'The proofreading content is empty')); + } + $aResult = $oProofReadService->proofread($aArticleMain['content']); + //数据处理 + $aError = empty($aResult['errors']) ? [] : $aResult['errors']; + if(empty($aError)){ + $aUpdateWhere = ['am_id' => $iAmId]; + $update_result = Db::name('article_main')->where($aUpdateWhere)->limit(1)->update(['is_proofread' => 1,'proofread_time' => time()]); + if($update_result === false){ + return json_encode(array('status' => 6,'msg' => 'Failed to update paragraph proofreading status-'.$update_result)); + } + return json_encode(array('status' => 1,'msg' => 'No errors found')); + } + $aData = []; + foreach ($aError as $key => $value) { + $value['am_id'] = $iAmId; + $value['article_id'] = $iArticleId; + $value['proof_before'] = empty($aResult['proof_before']) ? '' : $aResult['proof_before']; + $value['proof_after'] = empty($aResult['proof_after']) ? '' : $aResult['proof_after']; + $value['create_time'] = time(); + $aData[] = $value; + } + if(empty($aData)){ + return json_encode(array('status' => 1,'msg' => 'Data processing failed')); + } + + Db::startTrans(); + $aUpdateWhere = ['am_id' => $iAmId]; + $update_result = Db::name('article_main')->where($aUpdateWhere)->limit(1)->update(['is_proofread' => 1,'proofread_time' => time()]); + //更新之前未执行的数据 + $aWhere = ['am_id' => $iAmId,'article_id' => $iArticleId,'is_delete' => 2,'state' => 2]; + $result_update = Db::name('article_proofread')->where($aWhere)->update(['is_delete' => 1,'update_time' => time()]); + //插入 + $result = Db::name('article_proofread')->insertAll($aData); + if(!$result || $result_update === false || $update_result === false){ + return json_encode(array('status' => 6,'msg' => 'Data operation failed:insert-'.$result.';result_update-'.$result_update.';update_result-'.$update_result)); + } + Db::commit(); + return json_encode(['status' => 1,'msg' => 'Proofreading successful']); + } + /** + * @title AI文章校对-全文章 + * @param article_id 文章ID + */ + public function proofReadByArticle(){ + + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + $iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(array('status' => 2,'msg' => 'Please select an article' )); + } + //查询文章 + $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 && $aArticle['state'] != 5){ + return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); + } + + //查询文章内容 + $aWhere['type'] = 0; + $aWhere['content'] = ['<>','']; + $aWhere['state'] = 0; + $iCount = Db::table('t_article_main')->where($aWhere)->count(); + if(empty($iCount)){ + return json_encode(array('status' => 5,'msg' => 'The content of the article is empty')); + } + //查询是否进行过校对 + $aProofReadWhere = ['article_id' => $iArticleId,'state' => 2,'is_delete' => 2]; + $iCount = Db::name('article_proofread')->where($aProofReadWhere)->count(); + if(!empty($iCount)){ + return json_encode(array('status' => 6,'msg' => 'The article or paragraph has been proofread')); + } + //写入校对行队列 + $sQueue = \think\Queue::push('app\api\job\ArticleProofReadedi@fire',$aParam,'ArticleProofReadedi'); + return json_encode(array('status' => 1,'msg' => 'Proofreading in progress, check the results in one minute')); + } } diff --git a/application/api/job/ArticleProofReadLine.php b/application/api/job/ArticleProofReadLine.php new file mode 100644 index 0000000..c2002a9 --- /dev/null +++ b/application/api/job/ArticleProofReadLine.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 + $iAmId = empty($data['am_id']) ? 0 : $data['am_id']; + if (empty($iAmId)) { + $this->oQueueJob->log("无效的am_id,删除任务"); + $job->delete(); + return; + } + try { + + // 生成Redis键并尝试获取锁 + $sClassName = get_class($this); + $sRedisKey = "queue_job:{$sClassName}:{$iArticleId}_{$iAmId}"; + $sRedisValue = uniqid() . '_' . getmypid(); + if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) { + return; // 未获取到锁,已处理 + } + + //生成内容 + $oProofRead = new Proofread; + $response = $oProofRead->proofReadLineQueue($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/common/JournalArticle.php b/application/common/JournalArticle.php index cd4e75f..febd9e6 100644 --- a/application/common/JournalArticle.php +++ b/application/common/JournalArticle.php @@ -187,14 +187,14 @@ class JournalArticle //发送邮件 $memail = empty($aJournalInfo['email']) ? '' : $aJournalInfo['email']; $mpassword = empty($aJournalInfo['epassword']) ? '' : $aJournalInfo['epassword']; - $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){ - $iIsSuccess = 1; - $sMsg = '成功'; - } + // $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){ + // $iIsSuccess = 1; + // $sMsg = '成功'; + // } $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]; //邮件发送记录 diff --git a/application/common/ProofReadService.php b/application/common/ProofReadService.php index 2a21ac3..2dcecf3 100644 --- a/application/common/ProofReadService.php +++ b/application/common/ProofReadService.php @@ -18,12 +18,12 @@ class ProofReadService $correctedContent = $this->checkTextFormat($correctedContent); //数字格式校对 $correctedContent = $this->checkNumberFormat($correctedContent); + //No. 123456的写法统一 + $correctedContent = $this->checkNoFormatUniformity($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); //检测参考文献是否能打开 @@ -60,41 +60,19 @@ class ProofReadService $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) { // 正则错误处理(不变) @@ -119,34 +97,50 @@ class ProofReadService } } - // 过滤 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'; + // 过滤 URL/DOI + $urlDoiRegex = '~( + https?://[^\s/]{1,100} # 协议(http/https) + 域名(非空白/字符) + (?:/+[A-Za-z0-9\.\-]+(?:-[A-Za-z0-9\.\-]+)*)* # 多级路径(支持.html后接/1/23等格式) + (?:\?[A-Za-z0-9_\-=&%\+\.\~]+)? # 可选查询参数(如?J_num=8&page=1) + (?:\#[A-Za-z0-9_\-]+)? # 可选锚点(如#section) + | + \b[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,} # 无协议域名(如example.com) + (?:/+[A-Za-z0-9\.\-]+(?:-[A-Za-z0-9\.\-]+)*)* # 无协议多级路径 + (?:\?[A-Za-z0-9_\-=&%\+\.\~]+)? # 无协议查询参数 + (?:\#[A-Za-z0-9_\-]+)? # 无协议锚点 + (?=$|[\s\.,;!]) # 结束边界(空白或标点) + | + doi:\s{0,10}\d+\.\d+/[A-Za-z0-9-+×:]+(?:-[A-Za-z0-9-+×:]+)* # DOI格式 + )~iux'; + if (@preg_match($urlDoiRegex, '') === false) { // 正则错误处理(不变) } elseif (preg_match_all($urlDoiRegex, $corrected, $matches, PREG_SET_ORDER)) { + // 按长度降序排序,优先处理长URL(避免短URL被包含时误替换) 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 + // 独立偏移量避免重复匹配,兼容特殊URL格式 $posStart = strpos($originalContent, $original, $searchOffsetForExclude); $posEnd = ($posStart !== false) ? $posStart + strlen($original) : -1; $searchOffsetForExclude = ($posEnd !== -1) ? $posEnd : $searchOffsetForExclude + strlen($original); + // 精准替换当前URL为标记(仅1次,避免全局替换干扰) $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, @@ -158,54 +152,45 @@ class ProofReadService } foreach ($matches as $match) { - $original = $match[0][0]; // 匹配到的原始内容(如 (40 × 33)) + $original = $match[0][0]; // 匹配到的原始内容 $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 中匹配位置前的内容,计算其在原始文本中的长度(排除占位符影响) + $offsetInCorrected = $match[0][1]; // 匹配内容在$corrected中的偏移量 $prefixInCorrected = substr($corrected, 0, $offsetInCorrected); - // 3. 替换占位符为原始内容,得到与 $originalContent 对应的前缀 - $prefixInOriginal = strtr($prefixInCorrected, $excludeMarkers); - // 4. 原始位置 = 前缀长度(确保精准对应) + $prefixInOriginal = strtr($prefixInCorrected, $excludeMarkers); // 还原占位符为原始内容 $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; - } + $contentCheck = substr($originalContent, $posStart, $originalLen); + $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; + } else { + continue; } } - // 生成修正内容 + // 生成修正后的内容 $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; - - // 生成错误信息 + $searchOffsetForCore = $posEnd; // 更新核心规则偏移量,避免重复匹配 $currentCorrected = str_replace($original, $fixed, $corrected); + // 记录错误信息 $errors[] = $this->createError( $original, $fixed, @@ -213,7 +198,8 @@ class ProofReadService $originalContent, $currentCorrected, $posStart, - $posEnd + $posEnd, + $rule['error_type'] ?? '' ); $processedHashes[$hash] = true; $corrected = $currentCorrected; @@ -221,176 +207,206 @@ class ProofReadService } } - // 批量还原 / 和 URL/DOI(不变) + // 批量还原 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); + $original = $excludeMarkers[$marker] ?? '未知内容'; + $restoreErrors[] = "未正常还原的占位符: {$marker}(原始内容: {$original})"; + $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() { + private function getTextCoreRules() + { return [ - // ====================== 1. 括号内数字范围规则(优先级最高,避免与其他减号规则冲突) ====================== + // 1. 最高优先级:特殊格式排除规则(首行专属排除No.编号) [ - '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]' + 'pattern' => '~ + # 【首优先级】No.编号专属排除(如No.: 2023YJZX-LN03/13、NO: KHYJ-2023-05、no. 123-ABC/45) + # 支持变体:No.大小写、冒号可带/不带点、冒号前后空格、编号含-/_/数字/字母 + \b(?:No|NO|no)\.?:?\s* # 前缀:No./NO./no.(冒号可选,点可选,后接任意空格) + [A-Za-z0-9\-\/_]+ # 编号主体:支持字母、数字、-、/、_(覆盖2023YJZX-LN03/13) + (?:-[A-Za-z0-9\-\/_]+)* # 编号后缀:支持多段连接(如2023YJZX-LN03/13-001) + \b # 单词边界:避免编号后接多余字符(如No.: 2023abc) + | + # 括号内分数及百分比组合(如(45/45)、(15.6%, 7/45)) + \(\s*(?:\d+(?:\.\d+)?%?\s*,?\s*)?\d+(?:\.\d+)?\s*/\s*\d+(?:\.\d+)?\s*\) + | + # 独立年份范围(如1849-1850、2023 - 2025) + (?]|from\s+|:\s*|\[[MZACDP]\]\.\s*)\d{4}\s*-\s*\d{4} + (?!\d|[\+\-\*\/=<>]|[,\.:;!]|\+\d+\.) + | + # from+年份范围(如from 1849-1850) + \bfrom\s+\d{4}\s*-\s*\d{4}\b + | + # 带单位的数字范围/倍数(如50-200 nm、10×5 cm) + \b\d+\s*[-×]\s*\d+\s*[a-zA-Z%] + | + # 无No.前缀的项目编号(如2023YJZX-LN03/13、KHYJ-2023-05-01) + [A-Za-z]+-?\d+[-/]\d+[-/]\d* + | + # 参考文献格式(期刊/专著等) + \d{4},\s*\d{1,3}\(\d{1,2}\):\s*\d+-\d+(?:\+\d+)*\.|[^\n]+\[[MZACDP]\]\.\s*[^\n]+,\s*\d{4}:\s*\d+-\d+\. + ~ux', + 'replacement' => '$0', // 完全保留原始格式,不做任何修改 + 'verbatim_texts' => 'No.编号及非运算场景无需处理', + 'explanation' => 'No.系列编号(如No.: 2023YJZX-LN03/13)、括号内分数、年份范围、带单位数字范围、项目编号、参考文献等非运算场景的符号不做处理', + 'error_type' => 'exclude' ], - // ====================== 2. 无括号数字范围规则(次高优先级,避免与减号运算规则冲突) ====================== + // 2. 次高优先级:数字范围规则(避免与-冲突) [ - 'pattern' => '~(\b\d+)\s*—\s*(\d+\b)~u', // 匹配长划线(—) - 'replacement' => '$1-$2', // 替换为短划线(-) - 'verbatim_texts' => '无括号数字范围使用长划线(—)不规范', - 'explanation' => '无括号的数字范围应使用短划线(-),如 5-6' + 'pattern' => '~(\[\s*[-]?\d+\s*)\x{2014}\s*(\d+\s*\])~u', + 'replacement' => '$1-$2', + 'verbatim_texts' => '带括号数字范围长划线不规范', + 'explanation' => '带括号的数字范围应使用短划线[-]', + 'error_type' => 'en-dash' ], [ - '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*[-]?\d+)\s*-\s*(\d+\s*\])~u', + 'replacement' => '$1-$2', + 'verbatim_texts' => '带括号数字范围短划线空格不规范', + 'explanation' => '带括号数字范围的短划线[-]前后不应留空格', + 'error_type' => 'en-dash' ], [ - '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' => '~(\b\d+)\s*—\s*(\d+\b)~u', + 'replacement' => '$1-$2', + 'verbatim_texts' => '无括号数字范围长划线不规范', + 'explanation' => '无括号的数字范围应使用短划线[-]', + 'error_type' => 'bracket_en-dash' ], [ 'pattern' => '~ - (? '$1 - $2', - 'verbatim_texts' => '减法运算符前后空格不规范', - 'explanation' => '减法运算符(-)前后应各留一个空格(非数字范围场景),如 5 - 3' + (? '$1-$2', + 'verbatim_texts' => '无括号数字范围短划线空格不规范', + 'explanation' => '无括号数字范围的短划线[-]前后不应留空格', + 'error_type' => 'bracket_en-dash' ], - // ====================== 4. 特殊符号规则(低优先级,避免干扰核心格式) ====================== + // 3. 核心优先级:运算符规则(精准匹配,排除No.编号干扰) [ - 'pattern' => '~(\d+)\s+%~u', // 数字与百分号 + 'pattern' => '~(\S)\s*([<>!]=|===|!==)\s*(\S)~u', + 'replacement' => '$1 $2 $3', + 'verbatim_texts' => '复合运算符前后空格不规范', + 'explanation' => '复合运算符[>=、<=、==、!=、===、!==]前后应各留一个空格', + 'error_type' => 'composite_operator' + ], + [ + 'pattern' => '~ + (?|\*|\+|-|/) + (\S+?)\s*=\s*(\S+?) + (?!=|<|>|\*|\+|-|/) + ~ux', + 'replacement' => '$1 = $2', + 'verbatim_texts' => '等号前后空格不规范', + 'explanation' => '独立等号[=]前后应各留一个空格', + 'error_type' => 'equal' + ], + // 乘法(排除No.编号中的*) + [ + 'pattern' => '~ + (? '$1 × $3', + 'verbatim_texts' => '乘法运算符格式不规范', + 'explanation' => '乘法运算应使用标准乘号[×],前后各留一个空格', + 'error_type' => 'ride' + ], + // 除法(排除No.编号中的/) + [ + 'pattern' => '~ + (? '$1 $2 $3', + 'verbatim_texts' => '除法运算符前后空格不规范', + 'explanation' => '除法运算符[/]前后应各留一个空格(纯数字运算场景)', + 'error_type' => 'except' + ], + // 加法(排除No.编号中的+) + [ + 'pattern' => '~ + (? '$1 $2 $3', + 'verbatim_texts' => '加法运算符前后空格不规范', + 'explanation' => '加法运算符[+]前后应各留一个空格(纯数字运算场景)', + 'error_type' => 'plus' + ], + // 减法(排除No.编号中的-) + [ + 'pattern' => '~ + (? '$1 $2 $3', + 'verbatim_texts' => '减法运算符前后空格不规范', + 'explanation' => '减法运算符[-]前后应各留一个空格(纯数字运算场景)', + 'error_type' => 'reduce' + ], + + // 4. 低优先级:特殊符号规则 + [ + 'pattern' => '~(\d+)\s+%~u', 'replacement' => '$1%', - 'verbatim_texts' => '数字与百分号之间有多余空格', - 'explanation' => '数字与百分号(%)之间不应留空格,如 50%' + 'verbatim_texts' => '数字与百分号空格不规范', + 'explanation' => '数字与百分号[%]之间不应留空格', + 'error_type' => 'number_percentage' ], [ - 'pattern' => '~(\(\s*\d+)\s+×\s+(\d+\s*\))~u', // 先匹配「(数字 × 数字)」场景(带括号) - 'replacement' => '$1×$2', // 修正为「(数字×数字)」,如 (40×33) - 'verbatim_texts' => '带括号的乘号表示倍数时前后有多余空格', - 'explanation' => '带括号的乘号(×)表示倍数关系时前后不应留空格,如 (3×5)' - ], - [ - 'pattern' => '~(\d+)\s+×\s+(\d+)~u', // 再匹配「数字 × 数字」场景(无括号) + 'pattern' => '~(\d+)\s+×\s+(\d+)~u', 'replacement' => '$1×$2', - 'verbatim_texts' => '乘号表示倍数时前后有多余空格', - 'explanation' => '乘号(×)表示倍数关系时前后不应留空格,如 3×5' + 'verbatim_texts' => '倍数乘号空格不规范', + 'explanation' => '乘号[×]表示倍数时前后不应留空格', + 'error_type' => 'multiple' ], [ - 'pattern' => '~(\d+)\s*\*\s*(\d+)~u', // 星号(*)转乘号(×) - 'replacement' => '$1 × $2', - 'verbatim_texts' => '使用星号(*)作为乘法运算符不规范', - 'explanation' => '乘法运算应使用标准乘号(×)替代星号(*),并前后留空格,如 3 × 5' - ], - [ - 'pattern' => '~(\d+)\s+:\s+(\d+)~u', // 比值符号(:) + 'pattern' => '~(\d+)\s+:\s+(\d+)~u', 'replacement' => '$1:$2', - 'verbatim_texts' => '比值符号前后有多余空格', - 'explanation' => '比值符号(:)前后不应留空格,如 1:2' + 'verbatim_texts' => '比值符号空格不规范', + 'explanation' => '比值符号[:]前后不应留空格', + 'error_type' => 'biliel' ] ]; } /** - * 数字格式校对 + * 数字格式处理 */ private function checkNumberFormat($content) { $errors = []; $defaultReturn = $content; $originalContent = $content; - $searchOffset = 0; // 用于计算位置的偏移量(避免重复定位) + $searchOffset = 0; if (!is_string($content) || trim($content) === '') { $this->handleErrors($errors); @@ -400,78 +416,208 @@ class ProofReadService $correctedContent = $content; $replacements = []; $urlDoiPlaceholders = []; + $prefixFormatPlaceholders = []; + $decimalAlphaPlaceholders = []; + $dateRelatedPlaceholders = []; + $specialDecimalPlaceholders = []; + $softwareVersionPlaceholders = []; + $postalCodePlaceholders = []; // 精准保护邮编 + $bracketedNumPlaceholders = []; // 精准保护括号内数字 - // 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 { + // 保护括号内数字(仅匹配(960-1279)这类格式) + $bracketedNumPattern = '~ + \(\d+[-\d]*\d+\) # 仅匹配带括号的数字/数字范围 + ~ux'; + if (@preg_match($bracketedNumPattern, '') !== false) { $correctedContent = preg_replace_callback( - $urlDoiPattern, - function ($matches) use (&$urlDoiPlaceholders, $originalContent, &$errors, &$searchOffset) { + $bracketedNumPattern, + function ($matches) use (&$bracketedNumPlaceholders, $originalContent, &$searchOffset) { $fullMatch = $matches[0]; - $placeholder = '___URL_DOI_' . time() . '_' . uniqid() . '___'; - $urlDoiPlaceholders[$placeholder] = $fullMatch; - - // 计算URL/DOI在原始文本中的位置 + $placeholder = '___BRACKETED_NUM_' . uniqid() . '___'; + $bracketedNumPlaceholders[$placeholder] = $fullMatch; $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 - // ); + $searchOffset = $posStart !== false ? $posStart + strlen($fullMatch) : $searchOffset; 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); + // 精准保护邮编(仅匹配“地名+空格+4-6位数字” + $postalCodePattern = '~ + \b(?:[A-Za-z]+(?:\s+[A-Za-z]+)*|[\x{4e00}-\x{9fa5}]+)\s+\d{4,6}\b # 强制空格(如Jiangsu 223300、北京 100000) + |\b0\d{2,3}\d{7}\b # 兼容区号+固定电话(02588888888、01012345678) + ~uix'; + if (@preg_match($postalCodePattern, '') !== false) { + $correctedContent = preg_replace_callback( + $postalCodePattern, + function ($matches) use (&$postalCodePlaceholders, $originalContent, &$searchOffset) { + $fullMatch = $matches[0]; + $placeholder = '___POSTAL_CODE_' . uniqid() . '___'; + $postalCodePlaceholders[$placeholder] = $fullMatch; + $posStart = strpos($originalContent, $fullMatch, $searchOffset); + $searchOffset = $posStart !== false ? $posStart + strlen($fullMatch) : $searchOffset; + return $placeholder; + }, + $correctedContent + ); + } - $currentCorrected = strtr($originalContent, $replacements); - $errors[] = $this->createError( - $number, - $corrected, - "删除小数点后无效零(原始:{$number} → 修正:{$corrected})", - $originalContent, - $currentCorrected, - $posStart, - $posEnd - ); - } + //保护软件版本 + $softwareVersionPattern = '~ + \b(?:[A-Za-z]+(?:\s+[A-Za-z]+)*|[\x{4e00}-\x{9fa5}]+(?:\s+[\x{4e00}-\x{9fa5}]+)*)\s+\d+\.\d+(?:\.\d+)*\b + ~uix'; + if (@preg_match($softwareVersionPattern, '') !== false) { + $correctedContent = preg_replace_callback( + $softwareVersionPattern, + function ($matches) use (&$softwareVersionPlaceholders, $originalContent, &$searchOffset) { + $fullMatch = $matches[0]; + $placeholder = '___SOFTWARE_VERSION_' . uniqid() . '___'; + $softwareVersionPlaceholders[$placeholder] = $fullMatch; + $posStart = strpos($originalContent, $fullMatch, $searchOffset); + $searchOffset = $posStart !== false ? $posStart + strlen($fullMatch) : $searchOffset; + return $placeholder; + }, + $correctedContent + ); + } + + //保护特殊小数 + $specialDecimalPattern = '~ + a=\s*[\d+\.\d+[A-Za-z]+\d*\-+]+ + |\b\d+\.\d+[A-Za-z]+\d*\b + |\b\d+\.\d+[-+]\d+\.\d+[A-Za-z]+\d*\b + |\b\d+\.\d+[-+]\d+\.\d+\b + ~ux'; + if (@preg_match($specialDecimalPattern, '') !== false) { + $correctedContent = preg_replace_callback( + $specialDecimalPattern, + function ($matches) use (&$specialDecimalPlaceholders, $originalContent, &$searchOffset) { + $fullMatch = $matches[0]; + $placeholder = '___SPECIAL_DECIMAL_' . uniqid() . '___'; + $specialDecimalPlaceholders[$placeholder] = $fullMatch; + $posStart = strpos($originalContent, $fullMatch, $searchOffset); + $searchOffset = $posStart !== false ? $posStart + strlen($fullMatch) : $searchOffset; + return $placeholder; + }, + $correctedContent + ); + } + + // 保护年份/年月格式(2023、202309、2023-0021等) + $dateRelatedPattern = '~ + \b(?:20\d{2}|20\d{2}(0[1-9]|1[0-2])|20\d{2}-00\d{2})\b(?!\s*[A-Za-z]|\.) + ~ux'; + if (@preg_match($dateRelatedPattern, '') !== false) { + $correctedContent = preg_replace_callback( + $dateRelatedPattern, + function ($matches) use (&$dateRelatedPlaceholders, $originalContent, &$searchOffset) { + $fullMatch = $matches[0]; + $placeholder = '___DATE_PROTECT_' . uniqid() . '___'; + $dateRelatedPlaceholders[$placeholder] = $fullMatch; + $posStart = strpos($originalContent, $fullMatch, $searchOffset); + $searchOffset = $posStart !== false ? $posStart + strlen($fullMatch) : $searchOffset; + return $placeholder; + }, + $correctedContent + ); + } + + //6. 保护0.00Ac类格式(如1.20mL、0.50mg,避免误删末尾零) + $decimalAlphaPattern = '~ + \b(?:\d+\.\d+[A-Za-z]+|\d+\.[A-Za-z]+)\b(?!\s*[0-9.]) + ~ux'; + if (@preg_match($decimalAlphaPattern, '') !== false) { + $correctedContent = preg_replace_callback( + $decimalAlphaPattern, + function ($matches) use (&$decimalAlphaPlaceholders, $originalContent, &$searchOffset) { + $fullMatch = $matches[0]; + $placeholder = '___DECIMAL_ALPHA_' . uniqid() . '___'; + $decimalAlphaPlaceholders[$placeholder] = $fullMatch; + $posStart = strpos($originalContent, $fullMatch, $searchOffset); + $searchOffset = $posStart !== false ? $posStart + strlen($fullMatch) : $searchOffset; + return $placeholder; + }, + $correctedContent + ); + } + + //7. 保护通用前缀格式(如ID 123、REF AB456) + $universalPrefixPattern = '~ + (?:^|\s|\() + (?:(?!No\.|NO\.|PO|SO|SN|BN|REF|ORD|ID|PID)[A-Za-z]{1,3}(?:s?\.?)) + \s* + (?:[A-Za-z]+\d+|\d+[A-Za-z]+|[A-Za-z]+\d+[A-Za-z]+|\d{1,3}(?:,\d{3})*|\d+) + (?:$|\s|\)|\,|\.) + ~ux'; + if (@preg_match($universalPrefixPattern, '') !== false) { + $correctedContent = preg_replace_callback( + $universalPrefixPattern, + function ($matches) use (&$prefixFormatPlaceholders, $originalContent, &$searchOffset) { + $fullMatch = $matches[0]; + $placeholder = '___UNIVERSAL_PREFIX_' . uniqid() . '___'; + $prefixFormatPlaceholders[$placeholder] = $fullMatch; + $posStart = strpos($originalContent, $fullMatch, $searchOffset); + $searchOffset = $posStart !== false ? $posStart + strlen($fullMatch) : $searchOffset; + return $placeholder; + }, + $correctedContent + ); + } + + // 保护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) { + $correctedContent = preg_replace_callback( + $urlDoiPattern, + function ($matches) use (&$urlDoiPlaceholders, $originalContent, &$searchOffset) { + $fullMatch = $matches[0]; + $placeholder = '___URL_DOI_' . uniqid() . '___'; + $urlDoiPlaceholders[$placeholder] = $fullMatch; + $posStart = strpos($originalContent, $fullMatch, $searchOffset); + $searchOffset = $posStart !== false ? $posStart + strlen($fullMatch) : $searchOffset; + return $placeholder; + }, + $correctedContent + ); + } + + // 小数零处理(仅删除普通小数的无效零,跳过特殊格式) + $decimalTrailingZeroPattern = '~(-?\d+\.\d*[1-9])0+(?!\d|e|E|___DATE_PROTECT_|___DECIMAL_ALPHA_|___UNIVERSAL_PREFIX_|No\.|PO|SO|___SPECIAL_DECIMAL_|___SOFTWARE_VERSION_|___POSTAL_CODE_|___BRACKETED_NUM_|\-|\+|[A-Za-z])~ix'; + preg_match_all($decimalTrailingZeroPattern, $correctedContent, $trailingMatches); + foreach (array_unique($trailingMatches[0]) as $number) { + if (strpos($number, '___POSTAL_CODE_') !== false || strpos($number, '___BRACKETED_NUM_') !== false) { + continue; + } + if (preg_match($decimalTrailingZeroPattern, $number, $numMatch)) { + $replacements[$number] = $numMatch[1]; + $posStart = strpos($originalContent, $number, $searchOffset); + $posEnd = $posStart !== false ? $posStart + strlen($number) : -1; + $searchOffset = $posEnd !== -1 ? $posEnd : $searchOffset; + $currentCorrected = strtr($originalContent, $replacements); + $errors[] = $this->createError( + $number, $numMatch[1], "删除普通小数后末尾无效零", + $originalContent, $currentCorrected, $posStart, $posEnd, 'invalid_zero' + ); + } + } + + $decimalAllZeroPattern = '~(-?\d+)\.0+(?!\d|e|E|___DATE_PROTECT_|___DECIMAL_ALPHA_|___UNIVERSAL_PREFIX_|No\.|PO|SO|___SPECIAL_DECIMAL_|___SOFTWARE_VERSION_|___POSTAL_CODE_|___BRACKETED_NUM_|\-|\+|[A-Za-z])~ix'; + preg_match_all($decimalAllZeroPattern, $correctedContent, $allZeroMatches); + foreach (array_unique($allZeroMatches[0]) as $number) { + if (strpos($number, '___POSTAL_CODE_') !== false || strpos($number, '___BRACKETED_NUM_') !== false) { + continue; + } + if (preg_match($decimalAllZeroPattern, $number, $numMatch)) { + $replacements[$number] = $numMatch[1]; + $posStart = strpos($originalContent, $number, $searchOffset); + $posEnd = $posStart !== false ? $posStart + strlen($number) : -1; + $searchOffset = $posEnd !== -1 ? $posEnd : $searchOffset; + $currentCorrected = strtr($originalContent, $replacements); + $errors[] = $this->createError( + $number, $numMatch[1], "删除普通小数后全量无效零", + $originalContent, $currentCorrected, $posStart, $posEnd, 'invalid_zero' + ); } } $correctedContent = strtr($correctedContent, $replacements); @@ -479,51 +625,51 @@ class ProofReadService // 千分位处理 $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}' + '20\d{2}(?:0[1-9]|1[0-2])?(?:0[1-9]|[12]\d|3[01])?|20\d{2}-00\d{2}', + '\d+\.\d+[A-Za-z]+|\d+\.[A-Za-z]+', + '\(\d+[-\d]*\d+\)', + '(?:[A-Za-z]+(?:\s+[A-Za-z]+)*|[\x{4e00}-\x{9fa5}]+)\s+\d{4,6}\b|0\d{2,3}\d{7}\b', + '(?:[A-Za-z]+(?:\s+[A-Za-z]+)*|[\x{4e00}-\x{9fa5}]+(?:\s+[\x{4e00}-\x{9fa5}]+)*)\s+\d+\.\d+(?:\.\d+)*', + '[A-Za-z]{1,3}s?\.?\s*(?:[A-Za-z]+\d+|\d+[A-Za-z]+|\d{1,3}(?:,\d{3})*|\d+)', + 'No\.?\s*\d+|PO\s*\d+|SO\s*\d+|SN\s*\d+', + 'a=\s*[\d+\.\d+[A-Za-z]+\d*\-+]+', + '___DATE_PROTECT_.*?___|___DECIMAL_ALPHA_.*?___|___UNIVERSAL_PREFIX_.*?___|___URL_DOI_.*?___|___SPECIAL_DECIMAL_.*?___|___SOFTWARE_VERSION_.*?___|___POSTAL_CODE_.*?___|___BRACKETED_NUM_.*?___' ]); $thousandPattern = sprintf( - '#\b(?!(?:%s))\d{4,}\b#ixu', + '#(?createError( - '千分位正则错误', - '跳过千分位处理', - "千分位正则错误: {$thousandPattern}", - $originalContent, - $correctedContent, - -1, - -1 - ); - } else { + if (@preg_match($thousandPattern, '') !== false) { $correctedContent = preg_replace_callback( $thousandPattern, - function (array $matches) use (&$replacements, &$errors, $originalContent, &$searchOffset): string { + function ($matches) use (&$replacements, $originalContent, &$searchOffset, &$errors) { $original = $matches[0]; - if (isset($replacements[$original]) || strpos($original, ',') !== false) { + if (preg_match('~20\d{2}(0[1-9]|1[0-2])?(0[1-9]|[12]\d|3[01])?|20\d{2}-00\d{2}|(?:[A-Za-z]+|[\x{4e00}-\x{9fa5}]+)\s*\d{4,6}\b|0\d{2,3}\d{7}\b~u', $original)) { + return $original; + } + $isProtected = strpos($original, '___DATE_PROTECT_') !== false + || strpos($original, '___DECIMAL_ALPHA_') !== false + || strpos($original, '___UNIVERSAL_PREFIX_') !== false + || strpos($original, '___SPECIAL_DECIMAL_') !== false + || strpos($original, '___SOFTWARE_VERSION_') !== false + || strpos($original, '___POSTAL_CODE_') !== false + || strpos($original, '___BRACKETED_NUM_') !== false + || strpos($original, 'No.') !== false + || strpos($original, 'PO') !== false + || strpos($original, 'SO') !== false; + if (isset($replacements[$original]) || strpos($original, ',') !== false || $isProtected) { 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); - + $posEnd = $posStart !== false ? $posStart + strlen($original) : -1; + $searchOffset = $posEnd !== -1 ? $posEnd : $searchOffset; $currentCorrected = strtr($originalContent, $replacements); $errors[] = $this->createError( - $original, - $formatted, - "4位及以上整数添加千分位分隔符", - $originalContent, - $currentCorrected, - $posStart, - $posEnd + $original, $formatted, "四位及以上的数字需要每三位加一个逗号", + $originalContent, $currentCorrected, $posStart, $posEnd, 'thousandth_separator' ); return $formatted; }, @@ -531,143 +677,300 @@ class ProofReadService ); } - // 恢复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; + // 恢复所有保护内容(按优先级反向,避免相互干扰) + $correctedContent = strtr($correctedContent, $bracketedNumPlaceholders); + $correctedContent = strtr($correctedContent, $postalCodePlaceholders); + $correctedContent = strtr($correctedContent, $softwareVersionPlaceholders); + $correctedContent = strtr($correctedContent, $specialDecimalPlaceholders); + $correctedContent = strtr($correctedContent, $dateRelatedPlaceholders); + $correctedContent = strtr($correctedContent, $decimalAlphaPlaceholders); + $correctedContent = strtr($correctedContent, $prefixFormatPlaceholders); + $correctedContent = strtr($correctedContent, $urlDoiPlaceholders); - // 计算残留占位符的位置 - $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 - ); - } - } - } + // 清理残留占位符(防止异常情况下占位符未替换) + $correctedContent = preg_replace('~___(BRACKETED_NUM|POSTAL_CODE|SOFTWARE_VERSION|SPECIAL_DECIMAL|DATE_PROTECT|DECIMAL_ALPHA|UNIVERSAL_PREFIX|URL_DOI)_.*?___~', '', $correctedContent); $this->handleErrors($errors); return is_string($correctedContent) ? $correctedContent : $defaultReturn; } - /** - * 时间单位缩写校对 + * No. 123456格式统一 */ - private function checkTimeUnitAbbreviations($content) { - // 初始化错误数组(统一格式) + private function checkNoFormatUniformity($content) { $errors = []; - // 严格输入验证:空内容/非字符串直接返回 if (!is_string($content) || trim($content) === '') { $this->handleErrors($errors); return $content; } $corrected = $content; - $replaceMap = []; // 存储替换映射 + $replaceMap = []; $originalContent = $corrected; - $searchOffset = 0; // 用于计算错误位置的偏移量(避免重复定位) + $searchOffset = 0; - // 定义时间单位转换规则 + // 关键:精准排除规则 + $postalCodePattern = '~(?:[A-Za-z]+(?:\s+[A-Za-z]+)*|[\x{4e00}-\x{9fa5}]+)\s+\d{4,6}\b~u'; // 邮编 + $areaCodePattern = '~0\d{2,3}\d{7}\b~u'; // 区号 + $urlPattern = '~https?://[^<>\s]+~i'; // URL(如https://test.com/10.1101/2024.11.10) + $doiPattern = '~doi:\s{0,10}\d{1,10}\.\d{1,10}/[A-Za-z0-9\-_]+~i'; // DOI(如doi:11.1/1-1-1-1-9_2) + + $batchNumberRules = [ + [ + 'name' => 'No.前缀批号', + 'pattern' => '~ + \b + (?:[Nn][Oo]\.|[Nn][Oo]|NO\.|NO) + \s* + (\d+[A-Za-z0-9\-_]*) + \b + (?!\s*[年月日]|20\d{2}(?:0[1-9]|1[0-2])?|\.\d+|20\d{2}-00\d{2} + |https?://[^<>\s]+ # 排除URL + |doi:\s{0,10}\d{1,10}\.\d{1,10}/[A-Za-z0-9\-_]+) # 排除DOI + ~ux', + 'standardPrefix' => 'No.', + 'spaceAfterPrefix' => true, + 'description' => '带No.前缀的编号(如No. 123、NO.45-A)' + ], + [ + 'name' => '业务前缀批号', + 'pattern' => '~ + \b + (PO|SO|SN|BN|REF|ORD|ID|PID) + \s* + (\d+[A-Za-z0-9\-_]*) + \b + (?!\s*[年月日]|20\d{2}(?:0[1-9]|1[0-2])?|\.\d+|20\d{2}-00\d{2} + |https?://[^<>\s]+ # 排除URL + |doi:\s{0,10}\d{1,10}\.\d{1,10}/[A-Za-z0-9\-_]+) # 排除DOI + ~iux', + 'standardPrefix' => function($match) { + return strtoupper($match[1]); + }, + 'spaceAfterPrefix' => true, + 'description' => '带业务前缀的编号' + ], + // [ + // 'name' => '多段式批号', + // 'pattern' => '~ + // \b + // (?:\d+[A-Za-z]?[-_/])+ + // \d+[A-Za-z]? + // \b + // (?!\s*[年月日]|20\d{2}(?:0[1-9]|1[0-2])?|20\d{2}-00\d{2} + // |\d+\.\d+[A-Za-z]+ + // |https?://[^<>\s]+ # 排除URL + // |doi:\s{0,10}\d{1,10}\.\d{1,10}/[A-Za-z0-9\-_]+) # 排除DOI + // ~ux', + // 'standardize' => function($original) use ($postalCodePattern, $areaCodePattern, $urlPattern, $doiPattern) { + // // 排除URL、DOI、邮编、区号、日期 + // if (preg_match('~20\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])?|20\d{2}-00\d{2}~', $original) + // || preg_match($postalCodePattern, $original) + // || preg_match($areaCodePattern, $original) + // || preg_match($urlPattern, $original) + // || preg_match($doiPattern, $original)) { + // return $original; + // } + // return preg_replace(['~[-_/]+~', '~\s+~'], ['-', ''], $original); + // }, + // 'description' => '多段式编号(如2023-AB-123、XY_456-78)' + // ], + [ + 'name' => '混合批号', + 'pattern' => '~ + \b + (?: + \d{6,}(?!20\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])?|20\d{2}-00\d{2} + |(?<=[A-Za-z\s])\d{4,6}\b # 排除邮编 + |0\d{2,3}\d{7}\b # 排除区号 + |https?://[^<>\s]+ # 排除URL + |doi:\s{0,10}\d{1,10}\.\d{1,10}/[A-Za-z0-9\-_]+) # 排除DOI + |[A-Za-z]{2,}\d{4,} + |[A-Za-z0-9]{8,}(?!20\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])?|20\d{2}-00\d{2} + |https?://[^<>\s]+ # 排除URL + |doi:\s{0,10}\d{1,10}\.\d{1,10}/[A-Za-z0-9\-_]+) # 排除DOI + ) + \b + (?!\s*[年月日]) + (?!\.\d+) + (?!\d+\.\d+[A-Za-z]+) + (?!(?:^|\s|\()(?:[A-Za-z]{1,3}(?:s?\.?))\s*) + ~ux', + 'standardize' => function($original) use ($postalCodePattern, $areaCodePattern, $urlPattern, $doiPattern) { + // 排除URL、DOI、邮编、区号、日期 + if (preg_match('~20\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])?|20\d{2}-00\d{2}~', $original) + || preg_match($postalCodePattern, $original) + || preg_match($areaCodePattern, $original) + || preg_match($urlPattern, $original) + || preg_match($doiPattern, $original)) { + return $original; + } + return ctype_digit($original) ? $original : $original; + }, + 'description' => '纯数字/字母混合编号' + ] + ]; + + foreach ($batchNumberRules as $rule) { + if (@preg_match($rule['pattern'], '') === false) continue; + if (preg_match_all($rule['pattern'], $corrected, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $originalFull = $match[0]; + $fixedFull = $originalFull; + + // 核心排除逻辑:新增URL和DOI的判断 + if (preg_match($postalCodePattern, $originalFull) + || preg_match($areaCodePattern, $originalFull) + || preg_match('~20\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])?|20\d{2}-00\d{2}~', $originalFull) + || preg_match($urlPattern, $originalFull) // 跳过URL + || preg_match($doiPattern, $originalFull)) { // 跳过DOI + continue; + } + + if (isset($rule['standardPrefix'])) { + preg_match($rule['pattern'], $originalFull, $parts); + $body = $parts[1]; + $standardPrefix = is_callable($rule['standardPrefix']) ? $rule['standardPrefix']($parts) : $rule['standardPrefix']; + $space = $rule['spaceAfterPrefix'] ? ' ' : ''; + $fixedFull = $standardPrefix . $space . $body; + } elseif (isset($rule['standardize']) && is_callable($rule['standardize'])) { + $fixedFull = $rule['standardize']($originalFull); + } + + if ($originalFull !== $fixedFull && !isset($replaceMap[$originalFull])) { + $replaceMap[$originalFull] = $fixedFull; + $posStart = strpos($originalContent, $originalFull, $searchOffset); + $posEnd = $posStart !== false ? $posStart + strlen($originalFull) : -1; + $searchOffset = $posEnd !== -1 ? $posEnd : $searchOffset; + $errorHash = md5($originalFull . $fixedFull); + $errors[$errorHash] = $this->createError( + $originalFull, $fixedFull, + "{$rule['description']}格式不规范,标准格式为「{$fixedFull}」", + $originalContent, strtr($originalContent, $replaceMap), + $posStart, $posEnd, $rule['name'] + ); + } + } + } + } + + $corrected = !empty($replaceMap) ? strtr($corrected, $replaceMap) : $corrected; + $this->handleErrors($errors); + return $corrected; + } + /** + * 时间单位缩写校对 + */ + 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' => '小时' + 'description' => '小时', + 'cn_full' => '小时', // 中文全称 + 'cn_plural' => '小时' // 中文单复数同形 ], [ 'full' => 'minute', 'plural' => 'minutes', 'abbr' => 'min', - 'description' => '分钟' + 'description' => '分钟', + 'cn_full' => '分钟', + 'cn_plural' => '分钟' ], [ 'full' => 'second', 'plural' => 'seconds', 'abbr' => 's', - 'description' => '秒' + 'description' => '秒', + 'cn_full' => '秒', + 'cn_plural' => '秒' ] ]; 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"; + $pattern = "~ + (?createError( - // '时间单位正则错误', - // "跳过{$unit['description']}单位校验", - // "{$unit['description']}单位匹配正则语法错误:{$combinedPattern},已跳过该单位校验", - // $originalContent, - // $corrected, - // -1, - // -1 - // ); + if (@preg_match($pattern, '') === false) { continue; } - // 单次匹配所有相关模式 - if (preg_match_all($combinedPattern, $corrected, $matches, PREG_SET_ORDER)) { + // 仅匹配纯时间场景,排除所有干扰 + if (preg_match_all($pattern, $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") + $original = $match[0]; // 原始内容(如"5 Hour"、"3 分钟"、"2.5 S") + $number = $match[1]; // 数字部分 + $unitPart = $match[2]; // 单位部分 + $fixed = $number . strtolower($unit['abbr']); // 标准格式(5h、3min、2.5s) - // 仅处理需要修正的情况 + // 仅处理非标准格式 if ($original !== $fixed) { - // 确定错误类型(细化错误原因) - if (stripos($unitPart, $unit['full']) !== false) { - $errorReason = "应使用缩写形式'{$unit['abbr']}'"; + // 细化错误原因 + if (stripos($unitPart, $unit['full']) !== false || strpos($unitPart, $unit['cn_full']) !== false) { + $errorReason = "应使用缩写'{$unit['abbr']}'(不使用全称'{$unitPart}')"; } elseif (strpos($original, ' ') !== false) { - $errorReason = "数字与缩写间不应有空格"; + $errorReason = "数字与单位间不应有空格"; } else { - $errorReason = "单位缩写应使用小写'{$unit['abbr']}'"; + $errorReason = "单位缩写应小写'{$unit['abbr']}'(不使用'{$unitPart}')"; } - // 计算错误内容在原始文本中的位置 + // 计算位置(避免重复定位) $posStart = strpos($originalContent, $original, $searchOffset); $posEnd = ($posStart !== false) ? $posStart + strlen($original) : -1; - $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($original); // 更新偏移量 + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($original); - // 错误信息去重(基于原始内容+修正内容哈希) + // 错误去重 $errorHash = md5($original . $fixed); + $errorType = $unit['full']; if (!isset($errors[$errorHash])) { $errors[$errorHash] = $this->createError( $original, $fixed, - "{$unit['description']}单位格式不规范:{$errorReason},正确格式为'数字{$unit['abbr']}'", + "{$unit['description']}格式不规范:{$errorReason},标准格式为[数字{$unit['abbr']}](如3h、2.5min)", $originalContent, strtr($originalContent, $replaceMap + [$original => $fixed]), $posStart, - $posEnd + $posEnd, + $errorType ); } - // 记录替换映射(去重,避免重复替换) + // 记录替换映射 if (!isset($replaceMap[$original])) { $replaceMap[$original] = $fixed; } @@ -676,13 +979,12 @@ class ProofReadService } } - // 批量高效替换 + // 批量替换并处理错误 if (!empty($replaceMap)) { $corrected = strtr($corrected, $replaceMap); } - - // 统一处理错误 $this->handleErrors($errors); + return $corrected; } @@ -702,60 +1004,59 @@ class ProofReadService $originalContent = $corrected; // 保存完整原始内容 $searchOffset = 0; // 用于计算错误位置的偏移量(避免重复定位) - // 优化正则规则(精准匹配毫升单位,支持数字前缀和纯单位场景) - $mlPattern = '/\b(\d+(?:\.\d+)?\s*)?(ml)\b/i'; + // 优化正则规则: + // 1. 排除字母后接ML(如Yeh ML,ML为人名缩写) + // 2. 精准匹配毫升单位(支持数字前缀如“5ml”“3.0 ML”,或纯单位如“ml”“ML”) + $mlPattern = '/ + (?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") + $originalFull = $matchItem[0]; // 原始错误内容(如 "5ml"、" ML"、"2.5 mL") + $prefix = $matchItem[1] ?? ''; // 数字前缀(如 "5"、"3.0 "、"") + $originalUnit = strtolower($matchItem[2]); // 单位部分(统一转小写为"ml") - // 标准毫升单位格式(L大写) + // 标准毫升单位格式(L大写为"mL") $fixedFull = "{$prefix}mL"; + $errorType = 'mL'; - // 仅处理与标准格式不一致的场景 + // 仅处理与标准格式不一致的场景(避免无意义替换) if ($originalFull !== $fixedFull) { - // 计算错误内容在原始文本中的位置 + // 计算错误内容在原始文本中的精准位置(基于偏移量避免重复) $posStart = strpos($originalContent, $originalFull, $searchOffset); $posEnd = ($posStart !== false) ? $posStart + strlen($originalFull) : -1; - $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($originalFull); // 更新偏移量 + $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($originalFull); - // 错误去重(哈希机制) + // 错误去重(通过“原始内容+修正内容”的哈希避免重复记录) $errorHash = md5($originalFull . $fixedFull); if (!isset($errors[$errorHash])) { $errors[$errorHash] = $this->createError( $originalFull, $fixedFull, - '毫升单位格式不规范,标准写法为"mL"', + '毫升单位格式不规范,标准写法为[mL]', $originalContent, strtr($originalContent, $replaceMap + [$originalFull => $fixedFull]), $posStart, - $posEnd + $posEnd, + $errorType ); } - // 记录替换映射(去重) + // 记录替换映射(去重,避免同一内容多次替换) if (!isset($replaceMap[$originalFull])) { $replaceMap[$originalFull] = $fixedFull; } } } - // 批量替换 + // 批量替换所有不规范单位(高效处理,避免循环替换) if (!empty($replaceMap)) { $corrected = strtr($corrected, $replaceMap); } @@ -786,16 +1087,7 @@ class ProofReadService // 正则有效性校验 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") @@ -815,15 +1107,17 @@ class ProofReadService // 错误去重(哈希机制) $errorHash = md5($original . $fixed); + $errorType = 'P'; if (!isset($errors[$errorHash])) { $errors[$errorHash] = $this->createError( $original, $fixed, - '显著性P值格式不规范,P/p应使用斜体', + '显著性P值格式不规范,P/p应使用斜体', $originalContent, strtr($originalContent, $replaceMap + [$original => $fixed]), $posStart, - $posEnd + $posEnd, + $errorType ); } @@ -844,97 +1138,6 @@ class ProofReadService 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. */ @@ -956,15 +1159,7 @@ class ProofReadService // 正则有效性校验 if (@preg_match($titlePattern, '') === false) { - // $errors[] = $this->createError( - // '图表标题正则错误', - // '跳过图表标题格式校验', - // "图表标题匹配正则语法错误:{$titlePattern},已跳过该校验流程", - // $originalContent, - // $corrected, - // -1, - // -1 - // ); + } else { // 全局匹配所有图表标题格式 $matchCount = preg_match_all($titlePattern, $corrected, $allMatches, PREG_SET_ORDER); @@ -981,19 +1176,19 @@ class ProofReadService switch (strtolower($abbrBase)) { case 'fig': $fullName = 'Figure'; - $errorDesc = '图表标题使用缩写"Fig",应改为全称"Figure"'; + $errorDesc = '图表标题使用缩写"Fig",正确:"Figure"'; break; case 'figs': $fullName = 'Figures'; - $errorDesc = '图表标题复数使用缩写"Figs",应改为全称"Figures"'; + $errorDesc = '图表标题复数使用缩写"Figs",正确:"Figures"'; break; case 'tab': $fullName = 'Table'; - $errorDesc = '表格标题使用缩写"Tab",应改为全称"Table"'; + $errorDesc = '表格标题使用缩写"Tab",正确:"Table"'; break; case 'tabs': $fullName = 'Tables'; - $errorDesc = '表格标题复数使用缩写"Tabs",应改为全称"Tables"'; + $errorDesc = '表格标题复数使用缩写"Tabs",正确:"Tables"'; break; default: $fullName = ''; @@ -1026,7 +1221,8 @@ class ProofReadService $originalContent, $currentCorrected, $posStart, - $posEnd + $posEnd, + $fullName ); } @@ -1094,15 +1290,16 @@ class ProofReadService /** * 创建标准化错误信息 */ - private function createError($verbatim, $revised, $explanation,$original,$corrected, $position_start=-1, $position_end=-1) { + private function createError($verbatim='', $revised='', $explanation='',$original='',$corrected='', $position_start=-1, $position_end=-1,$error_type='') { return [ 'verbatim_texts' => $verbatim, 'revised_content' => $revised, 'explanation' => $explanation, 'original' => $original, 'corrected' => $corrected, - 'position_start' => $position_start, // 错误起始位置 - 'position_end' => $position_end // 错误结束位置 + 'position_start' => $position_start, + 'position_end' => $position_end, + 'error_type' => $error_type ]; } /**