From 0b2ece953021acdbd82e95d2e41c991fb5783e82 Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 13 Oct 2025 17:58:36 +0800 Subject: [PATCH 01/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 58 ++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index 7d55dcd..2dc93d0 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -129,7 +129,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){ return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); } @@ -326,12 +326,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,'state' => 2,'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]); } /** @@ -500,4 +508,48 @@ 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){ + return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); + } + + //查询校对内容 + if(!empty($iAmId)){ + $aWhere['am_id'] = $iAmId; + } + $iState = empty($aParam['state']) ? 2 : explode(',', $aParam['state']); + if(!empty($iState)){ + $aWhere['state'] = ['in',$iState]; + } + $aCount = Db::name('article_proofread')->field('am_id,count(id) as num')->where($aWhere)->group('am_id')->select(); + $aCount = empty($aCount) ? [] : array_column($aCount, 'num','am_id'); + //总数量 + $iCount = empty($aCount) ? 0 : array_sum(array_values($aCount)); + $am_id_count = empty($aCount[$iAmId]) ? 0 : $aCount[$iAmId]; + return json_encode(['status' => 1,'msg' => 'success','data' => ['sum_count' => $iCount,'am_id_count' => $am_id_count]]); + } } From 4f178b79bc6c3ce44b351e5c01a649d6e0f6ddef Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 14 Oct 2025 09:14:46 +0800 Subject: [PATCH 02/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index 2dc93d0..d287db8 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -538,9 +538,6 @@ class Proofread extends Base } //查询校对内容 - if(!empty($iAmId)){ - $aWhere['am_id'] = $iAmId; - } $iState = empty($aParam['state']) ? 2 : explode(',', $aParam['state']); if(!empty($iState)){ $aWhere['state'] = ['in',$iState]; From db3e162bf2f97b04d50fdeb6285bf9318fe6d5ae Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 14 Oct 2025 09:20:26 +0800 Subject: [PATCH 03/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 1 + 1 file changed, 1 insertion(+) diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index d287db8..541aedc 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -316,6 +316,7 @@ class Proofread extends Base } Db::startTrans(); //更新原始内容 + $result_main = true; if(!empty($sUpdateContent)){ $aWhere = ['am_id' => $iAmId,'state' => 0,'type' => 0]; $aUpdate = ['content' => $sUpdateContent]; From c9316b3d051a35ac80304a6858ac0d5dd18ab1c2 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 14 Oct 2025 10:49:06 +0800 Subject: [PATCH 04/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 148 +++++++++++++++++-- application/api/job/ArticleProofReadLine.php | 85 +++++++++++ 2 files changed, 218 insertions(+), 15 deletions(-) create mode 100644 application/api/job/ArticleProofReadLine.php diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index 541aedc..c6430b1 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; @@ -39,7 +37,7 @@ class Proofread extends Base } //查询是否进行过校对 - $aProofReadWhere = ['article_id' => $iArticleId,'state' => 2]; + $aProofReadWhere = ['article_id' => $iArticleId,'state' => 2,'is_delete' => 2]; if(!empty($iAmId)){ $aProofReadWhere['am_id'] = $iAmId; } @@ -47,14 +45,10 @@ 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(); if(empty($aArticleMain)){ return json_encode(array('status' => 5,'msg' => 'The content of the article is empty')); @@ -89,6 +83,7 @@ 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; } } @@ -100,10 +95,7 @@ class Proofread extends Base 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']); + return json_encode(['status' => 1,'msg' => 'Proofreading successful']); } /** * @title 获取每行校对记录 @@ -147,8 +139,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 +252,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)){ @@ -478,7 +470,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']); @@ -543,6 +535,7 @@ class Proofread extends Base if(!empty($iState)){ $aWhere['state'] = ['in',$iState]; } + $aWhere['is_delete'] = 2; $aCount = Db::name('article_proofread')->field('am_id,count(id) as num')->where($aWhere)->group('am_id')->select(); $aCount = empty($aCount) ? [] : array_column($aCount, 'num','am_id'); //总数量 @@ -550,4 +543,129 @@ class Proofread extends Base $am_id_count = empty($aCount[$iAmId]) ? 0 : $aCount[$iAmId]; return json_encode(['status' => 1,'msg' => 'success','data' => ['sum_count' => $iCount,'am_id_count' => $am_id_count]]); } + + /** + * @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){ + 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,ami_id,amt_id')->where($aWhere)->find(); + if(empty($aArticleMain)){ + 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){ + 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,ami_id,amt_id')->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']); + if(empty($aResult)){ + return json_encode(array('status' => 7,'msg' => 'Not returned to school regarding the content')); + } + //数据处理 + $aError = empty($aResult['errors']) ? [] : $aResult['errors']; + if(empty($aError)){ + 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(); + //更新之前未执行的数据 + $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){ + return json_encode(array('status' => 6,'msg' => 'No errors found')); + } + Db::commit(); + return json_encode(['status' => 1,'msg' => 'Proofreading successful']); + } } diff --git a/application/api/job/ArticleProofReadLine.php b/application/api/job/ArticleProofReadLine.php new file mode 100644 index 0000000..5ddb216 --- /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 From 24ca5df1e3e57dde99f5225e64145327eb32bae2 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 14 Oct 2025 13:35:42 +0800 Subject: [PATCH 05/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index c6430b1..54a18a5 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -636,7 +636,7 @@ class Proofread extends Base } $aResult = $oProofReadService->proofread($aArticleMain['content']); if(empty($aResult)){ - return json_encode(array('status' => 7,'msg' => 'Not returned to school regarding the content')); + return json_encode(array('status' => 7,'msg' => 'Failed to obtain proofreading content')); } //数据处理 $aError = empty($aResult['errors']) ? [] : $aResult['errors']; From b3ee7de7ba7668b6bd3be7724123e984f293daa5 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 14 Oct 2025 14:55:44 +0800 Subject: [PATCH 06/19] =?UTF-8?q?=E9=98=9F=E5=88=97=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ProofReadService.php | 167 ++++++++++++++---------- 1 file changed, 101 insertions(+), 66 deletions(-) diff --git a/application/common/ProofReadService.php b/application/common/ProofReadService.php index 2a21ac3..66171ef 100644 --- a/application/common/ProofReadService.php +++ b/application/common/ProofReadService.php @@ -82,15 +82,15 @@ class ProofReadService if ($converted === false) { $posStart = 0; $posEnd = min(20, strlen($originalContent)); - $errors[] = $this->createError( - '编码转换失败', - '保留原始编码内容', - "从[{$originalEncoding}]转换为UTF-8失败,保留原始内容", - $originalContent, - $corrected, - $posStart, - $posEnd - ); + // $errors[] = $this->createError( + // '编码转换失败', + // '保留原始编码内容', + // "从[{$originalEncoding}]转换为UTF-8失败,保留原始内容", + // $originalContent, + // $corrected, + // $posStart, + // $posEnd + // ); } } @@ -213,7 +213,8 @@ class ProofReadService $originalContent, $currentCorrected, $posStart, - $posEnd + $posEnd, + empty($rule['error_type']) ? '' : $rule['error_type'] ); $processedHashes[$hash] = true; $corrected = $currentCorrected; @@ -264,25 +265,30 @@ class ProofReadService 'pattern' => '~(\[\s*[-]?\d+\s*)\x{2014}\s*(\d+\s*\])~u', // 匹配长划线(—) 'replacement' => '$1-$2', // 替换为短划线(-) 'verbatim_texts' => '带括号数字范围使用长划线(—)不规范', - 'explanation' => '带括号的数字范围应使用短划线(-),如 [1-5]' + 'explanation' => '带括号的数字范围应使用短划线[-]', + 'error_type' => 'en-dash' ], [ 'pattern' => '~(\[\s*[-]?\d+\s*)-\s*(\d+\s*\])~u', // 匹配连接符(-)及可能的空格 'replacement' => '$1-$2', // 统一为无空格短划线(-) 'verbatim_texts' => '带括号数字范围使用连接符(-)格式不规范', - 'explanation' => '带括号的数字范围应使用短划线(-)且前后无空格,如 [2-5]' + 'explanation' => '带括号的数字范围应使用短划线[-]且前后无空格', + 'error_type' => 'en-dash' + ], [ 'pattern' => '~(\[\s*[-]?\d+)\s+-\s*(\d+\s*\])~u', // 短划线前多余空格 'replacement' => '$1-$2', // 移除前导空格 'verbatim_texts' => '数字范围短划线前有多余空格', - 'explanation' => '带括号数字范围的短划线(-)前不应留空格,如 [3-5]' + 'explanation' => '带括号数字范围的短划线[-]前不应留空格', + 'error_type' => 'en-dash' ], [ 'pattern' => '~(\[\s*[-]?\d+)\s*-\s+(\d+\s*\])~u', // 短划线后多余空格 'replacement' => '$1-$2', // 移除后导空格 'verbatim_texts' => '数字范围短划线后有多余空格', - 'explanation' => '带括号数字范围的短划线(-)后不应留空格,如 [4-5]' + 'explanation' => '带括号数字范围的短划线[-]后不应留空格', + 'error_type' => 'en-dash' ], // ====================== 2. 无括号数字范围规则(次高优先级,避免与减号运算规则冲突) ====================== @@ -290,13 +296,15 @@ class ProofReadService 'pattern' => '~(\b\d+)\s*—\s*(\d+\b)~u', // 匹配长划线(—) 'replacement' => '$1-$2', // 替换为短划线(-) 'verbatim_texts' => '无括号数字范围使用长划线(—)不规范', - 'explanation' => '无括号的数字范围应使用短划线(-),如 5-6' + 'explanation' => '无括号的数字范围应使用短划线[-]', + 'error_type' => 'bracket_en-dash' ], [ 'pattern' => '~(\b\d+)\s*-\s*(\d+\b)~u', // 匹配连接符(-)及可能的空格 'replacement' => '$1-$2', // 统一为无空格短划线(-) 'verbatim_texts' => '无括号数字范围使用连接符(-)格式不规范', - 'explanation' => '无括号的数字范围应使用短划线(-)且前后无空格,如 5-7' + 'explanation' => '无括号的数字范围应使用短划线[-]且前后无空格', + 'error_type' => 'bracket_en-dash' ], // ====================== 3. 运算符空格规则(按「复合→独立」顺序,避免冲突) ====================== @@ -304,7 +312,8 @@ class ProofReadService 'pattern' => '~(\S)\s*([<>!]=|===|!==)\s*(\S)~u', // 复合运算符(>=、<=、==、!=、===、!==) 'replacement' => '$1 $2 $3', 'verbatim_texts' => '复合运算符前后空格不规范', - 'explanation' => '复合运算符(>=、<=、==、!=、===、!==)前后应各留一个空格,如 x >= 5' + 'explanation' => '复合运算符[>=、<=、==、!=、===、!==]前后应各留一个空格', + 'error_type' => 'composite_operator' ], [ 'pattern' => '~(?|\*|\+|-|/)(\S+?)\s*=\s*(\S+?)(?!=|<|>|\*|\+|-|/)~u', @@ -314,25 +323,29 @@ class ProofReadService // 前后否定断言:排除与其他运算符(如+=、*=)的冲突 'replacement' => '$1 = $2', // 正确拼接“前内容 + 规范等号 + 后内容” 'verbatim_texts' => '等号前后空格不规范', - 'explanation' => '独立等号(=)前后应各留一个空格,如 a = 3' // 移除无效的$1/$2 + 'explanation' => '独立等号[=]前后应各留一个空格', + 'error_type' => 'equal' ], [ 'pattern' => '~(\d+)\s*\+\s*(\d+)~u', // 加法运算符(+) 'replacement' => '$1 + $2', 'verbatim_texts' => '加法运算符前后空格不规范', - 'explanation' => '加法运算符(+)前后应各留一个空格,如 2 + 3' + 'explanation' => '加法运算符[+]前后应各留一个空格', + 'error_type' => 'plus' ], [ 'pattern' => '~(\d+)\s*\*\s*(\d+)~u', // 乘法运算符(*) 'replacement' => '$1 * $2', 'verbatim_texts' => '乘法运算符前后空格不规范', - 'explanation' => '乘法运算符(*)前后应各留一个空格,如 3 * 4' + 'explanation' => '乘法运算符[*]前后应各留一个空格', + 'error_type' => 'ride' ], [ 'pattern' => '~(\d+)\s*/\s*(\d+)~u', // 除法运算符(/) 'replacement' => '$1 / $2', 'verbatim_texts' => '除法运算符前后空格不规范', - 'explanation' => '除法运算符(/)前后应各留一个空格,如 8 / 2' + 'explanation' => '除法运算符[/]前后应各留一个空格', + 'error_type' => 'except' ], [ 'pattern' => '~ @@ -347,7 +360,8 @@ class ProofReadService ~ux', // 减法运算符(-,仅处理纯数字减法,排除文献引用等场景) 'replacement' => '$1 - $2', 'verbatim_texts' => '减法运算符前后空格不规范', - 'explanation' => '减法运算符(-)前后应各留一个空格(非数字范围场景),如 5 - 3' + 'explanation' => '减法运算符[-]前后应各留一个空格(非数字范围场景)', + 'error_type' => 'reduce' ], // ====================== 4. 特殊符号规则(低优先级,避免干扰核心格式) ====================== @@ -355,31 +369,38 @@ class ProofReadService 'pattern' => '~(\d+)\s+%~u', // 数字与百分号 'replacement' => '$1%', 'verbatim_texts' => '数字与百分号之间有多余空格', - 'explanation' => '数字与百分号(%)之间不应留空格,如 50%' + 'explanation' => '数字与百分号[%]之间有多余空格', + 'error_type' => 'number_percentage' ], [ 'pattern' => '~(\(\s*\d+)\s+×\s+(\d+\s*\))~u', // 先匹配「(数字 × 数字)」场景(带括号) 'replacement' => '$1×$2', // 修正为「(数字×数字)」,如 (40×33) 'verbatim_texts' => '带括号的乘号表示倍数时前后有多余空格', - 'explanation' => '带括号的乘号(×)表示倍数关系时前后不应留空格,如 (3×5)' + 'explanation' => '带括号的乘号[×]表示倍数关系时前后有多余空格', + 'error_type' => 'multiple' + ], [ 'pattern' => '~(\d+)\s+×\s+(\d+)~u', // 再匹配「数字 × 数字」场景(无括号) 'replacement' => '$1×$2', 'verbatim_texts' => '乘号表示倍数时前后有多余空格', - 'explanation' => '乘号(×)表示倍数关系时前后不应留空格,如 3×5' + 'explanation' => '乘号[×]表示倍数关系时前后不应留空格', + 'error_type' => 'multiple' ], [ 'pattern' => '~(\d+)\s*\*\s*(\d+)~u', // 星号(*)转乘号(×) 'replacement' => '$1 × $2', 'verbatim_texts' => '使用星号(*)作为乘法运算符不规范', - 'explanation' => '乘法运算应使用标准乘号(×)替代星号(*),并前后留空格,如 3 × 5' + // 'explanation' => '乘法运算应使用标准乘号(×)替代星号(*),并前后留空格,如 3 × 5' + 'explanation' => '乘法运算应使用标准乘号[×]替代星号[*]', + 'error_type' => 'ride' ], [ 'pattern' => '~(\d+)\s+:\s+(\d+)~u', // 比值符号(:) 'replacement' => '$1:$2', 'verbatim_texts' => '比值符号前后有多余空格', - 'explanation' => '比值符号(:)前后不应留空格,如 1:2' + 'explanation' => '比值符号[:]前后有多余空格', + 'error_type' => 'biliel' ] ]; } @@ -452,7 +473,7 @@ class ProofReadService if (preg_match($decimalZeroPattern, $number, $numMatch)) { $integerPart = $numMatch[1]; $corrected = $integerPart; - + $errorType = 'invalid_zero'; if (!isset($replacements[$number])) { $replacements[$number] = $corrected; @@ -465,11 +486,12 @@ class ProofReadService $errors[] = $this->createError( $number, $corrected, - "删除小数点后无效零(原始:{$number} → 修正:{$corrected})", + "删除小数点后无效零", $originalContent, $currentCorrected, $posStart, - $posEnd + $posEnd, + $errorType ); } } @@ -489,15 +511,15 @@ class ProofReadService ); if (@preg_match($thousandPattern, '') === false) { - $errors[] = $this->createError( - '千分位正则错误', - '跳过千分位处理', - "千分位正则错误: {$thousandPattern}", - $originalContent, - $correctedContent, - -1, - -1 - ); + // $errors[] = $this->createError( + // '千分位正则错误', + // '跳过千分位处理', + // "千分位正则错误: {$thousandPattern}", + // $originalContent, + // $correctedContent, + // -1, + // -1 + // ); } else { $correctedContent = preg_replace_callback( $thousandPattern, @@ -516,6 +538,7 @@ class ProofReadService $searchOffset = ($posEnd !== -1) ? $posEnd : $searchOffset + strlen($original); $currentCorrected = strtr($originalContent, $replacements); + $errorType = 'thousandth_separator'; $errors[] = $this->createError( $original, $formatted, @@ -523,7 +546,9 @@ class ProofReadService $originalContent, $currentCorrected, $posStart, - $posEnd + $posEnd, + $errorType + ); return $formatted; }, @@ -547,15 +572,15 @@ class ProofReadService $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 - ); + // $errors[] = $this->createError( + // "残留URL/DOI占位符: {$marker}", + // "已恢复为原始内容", + // "URL/DOI恢复不完全,已强制恢复", + // $originalContent, + // $correctedContent, + // $posStart, + // $posEnd + // ); } } } @@ -655,15 +680,17 @@ class ProofReadService // 错误信息去重(基于原始内容+修正内容哈希) $errorHash = md5($original . $fixed); + $errorType = empty( $unit['full']) ? '' : $unit['full']; if (!isset($errors[$errorHash])) { $errors[$errorHash] = $this->createError( $original, $fixed, - "{$unit['description']}单位格式不规范:{$errorReason},正确格式为'数字{$unit['abbr']}'", + "{$unit['description']}单位格式不规范:{$errorReason},正确格式为[数字{$unit['abbr']}]", $originalContent, strtr($originalContent, $replaceMap + [$original => $fixed]), $posStart, - $posEnd + $posEnd, + $errorType ); } @@ -726,7 +753,7 @@ class ProofReadService // 标准毫升单位格式(L大写) $fixedFull = "{$prefix}mL"; - + $errorType = 'mL'; // 仅处理与标准格式不一致的场景 if ($originalFull !== $fixedFull) { // 计算错误内容在原始文本中的位置 @@ -740,11 +767,12 @@ class ProofReadService $errors[$errorHash] = $this->createError( $originalFull, $fixedFull, - '毫升单位格式不规范,标准写法为"mL"', + '毫升单位格式不规范,标准写法为[mL]', $originalContent, strtr($originalContent, $replaceMap + [$originalFull => $fixedFull]), $posStart, - $posEnd + $posEnd, + $errorType ); } @@ -815,15 +843,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 ); } @@ -912,15 +942,18 @@ class ProofReadService // 错误信息去重(基于单个错误片段的原始值+修正值哈希,避免重复记录) $errorHash = md5($originalFull . $fixedFull); + $errorType = 'No.'; if (!isset($errors[$errorHash])) { $errors[$errorHash] = $this->createError( $originalFull, // verbatim_texts:具体错误片段 $fixedFull, // revised_content:错误片段的修正结果 - 'No. 格式不规范:' . implode(',', $errorReasons) . ',正确格式为「No. 数字」', // explanation:错误说明 + 'No. 格式不规范,正确格式为「No. 数字」', // explanation:错误说明 + // 'No. 格式不规范:' . implode(',', $errorReasons) . ',正确格式为「No. 数字」', // explanation:错误说明 $originalContent, // original:完整原始内容(整个输入文本) strtr($originalContent, $replaceMap), // corrected:完整修正内容(基于当前替换映射生成) $posStart, // position_start:错误起始位置 - $posEnd // position_end:错误结束位置 + $posEnd, // position_end:错误结束位置 + $errorType //错误类型 ); } } @@ -981,19 +1014,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 +1059,8 @@ class ProofReadService $originalContent, $currentCorrected, $posStart, - $posEnd + $posEnd, + $fullName ); } @@ -1094,15 +1128,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 ]; } /** From 1e8949007f2cb3ac6b1efe274f683ca8cbea6ed4 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 14 Oct 2025 15:56:14 +0800 Subject: [PATCH 07/19] =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=B8=BA=E9=A2=84?= =?UTF-8?q?=E6=8E=A5=E6=94=B6=E8=BF=9B=E8=A1=8C=E6=96=87=E7=AB=A0=E6=A0=A1?= =?UTF-8?q?=E5=AF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Article.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index ad47d6b..50eadc1 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -1722,6 +1722,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 From 6286bb0bdb7626d7a2bbe7d49a1354349df91844 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 14 Oct 2025 16:20:39 +0800 Subject: [PATCH 08/19] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index 54a18a5..75803d9 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -580,7 +580,7 @@ class Proofread extends Base $aWhere['state'] = 0; $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)->find(); - if(empty($aArticleMain)){ + if(empty($aArticleMain['content'])){ return json_encode(array('status' => 5,'msg' => 'The content of the article is empty')); } //写入校对行队列 @@ -635,9 +635,6 @@ class Proofread extends Base return json_encode(array('status' => 6,'msg' => 'The proofreading content is empty')); } $aResult = $oProofReadService->proofread($aArticleMain['content']); - if(empty($aResult)){ - return json_encode(array('status' => 7,'msg' => 'Failed to obtain proofreading content')); - } //数据处理 $aError = empty($aResult['errors']) ? [] : $aResult['errors']; if(empty($aError)){ From b19e9956c2fcaaee6b96d976332b8be4fada7626 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 14 Oct 2025 17:11:25 +0800 Subject: [PATCH 09/19] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Preaccept.php | 121 +++++++++++++++++------ 1 file changed, 93 insertions(+), 28 deletions(-) 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); + } + +} From 0d676ccd433960113e89dc3db551f08cb5682d1e Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 14 Oct 2025 18:13:12 +0800 Subject: [PATCH 10/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 27 +++++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index 75803d9..b419599 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -502,6 +502,7 @@ class Proofread extends Base return json_encode(['status' => 1,'msg' => "Update successful"]); } + /** * @title 根据状态统计数量 * @param article_id 文章ID @@ -531,16 +532,26 @@ class Proofread extends Base } //查询校对内容 - $iState = empty($aParam['state']) ? 2 : explode(',', $aParam['state']); - if(!empty($iState)){ - $aWhere['state'] = ['in',$iState]; - } + $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')->where($aWhere)->group('am_id')->select(); - $aCount = empty($aCount) ? [] : array_column($aCount, 'num','am_id'); + $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']; + } + } + } //总数量 - $iCount = empty($aCount) ? 0 : array_sum(array_values($aCount)); - $am_id_count = empty($aCount[$iAmId]) ? 0 : $aCount[$iAmId]; + $am_id_count = -1; + if(!empty($aCountData[$iAmId])){ + $am_id_count = empty($aCountData[$iAmId][2]) ? 0 : $aCountData[$iAmId][2]; + } return json_encode(['status' => 1,'msg' => 'success','data' => ['sum_count' => $iCount,'am_id_count' => $am_id_count]]); } From dc8660d4e01a7c426cfe8d0a35afd0e5731fa0ef Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 15 Oct 2025 09:21:04 +0800 Subject: [PATCH 11/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index b419599..04e1453 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -324,7 +324,7 @@ class Proofread extends Base } Db::commit(); //查询未执行的数量 - $aCount = json_decode($this->getCountByState(['article_id' => $iArticleId,'state' => 2,'am_id' => $iAmId]),true); + $aCount = json_decode($this->getCountByState(['article_id' => $iArticleId,'am_id' => $iAmId]),true); $aCount = empty($aCount['data']) ? [] : $aCount['data']; //返回结果 @@ -502,7 +502,7 @@ class Proofread extends Base return json_encode(['status' => 1,'msg' => "Update successful"]); } - + /** * @title 根据状态统计数量 * @param article_id 文章ID From 24c58614c89cbe6230ab28da653227d309e972a5 Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 15 Oct 2025 14:01:24 +0800 Subject: [PATCH 12/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 53 ++++++++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index 04e1453..deedae5 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -49,7 +49,7 @@ class Proofread extends Base $aWhere['type'] = 0; $aWhere['content'] = ['<>','']; $aWhere['state'] = 0; - $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')); } @@ -58,11 +58,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; @@ -71,6 +74,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')); } //数据处理 @@ -90,11 +102,21 @@ class Proofread extends Base 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)); } + Db::commit(); return json_encode(['status' => 1,'msg' => 'Proofreading successful']); } /** @@ -552,7 +574,13 @@ class Proofread extends Base if(!empty($aCountData[$iAmId])){ $am_id_count = empty($aCountData[$iAmId][2]) ? 0 : $aCountData[$iAmId][2]; } - return json_encode(['status' => 1,'msg' => 'success','data' => ['sum_count' => $iCount,'am_id_count' => $am_id_count]]); + $is_proofread = -1; + if(!empty($iAmId)){ + $aWhere = ['state' => 0,'am_id' => $iAmId]; + $aArticleMain = Db::name('article_main')->field('is_proofread')->where($aWhere)->find(); + $is_proofread = empty($aArticleMain['is_proofread']) ? $is_proofread : $aArticleMain['is_proofread']; + } + return json_encode(['status' => 1,'msg' => 'success','data' => ['sum_count' => $iCount,'am_id_count' => $am_id_count,'is_proofread' => $is_proofread]]); } /** @@ -590,7 +618,7 @@ class Proofread extends Base $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,ami_id,amt_id')->where($aWhere)->find(); + $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')); } @@ -632,7 +660,7 @@ class Proofread extends Base $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,ami_id,amt_id')->where($aWhere)->find(); + $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')); } @@ -649,6 +677,11 @@ class Proofread extends Base //数据处理 $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 = []; @@ -665,13 +698,15 @@ class Proofread extends Base } 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){ - return json_encode(array('status' => 6,'msg' => 'No errors found')); + 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']); From aa0b5a4f575f1a1b0e44a7dca71f26455e0a37e9 Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 15 Oct 2025 14:16:46 +0800 Subject: [PATCH 13/19] =?UTF-8?q?=E6=97=B6=E9=97=B4=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/job/ArticleProofReadLine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/api/job/ArticleProofReadLine.php b/application/api/job/ArticleProofReadLine.php index 5ddb216..c2002a9 100644 --- a/application/api/job/ArticleProofReadLine.php +++ b/application/api/job/ArticleProofReadLine.php @@ -9,7 +9,7 @@ class ArticleProofReadLine { private $oQueueJob; private $QueueRedis; - private $completedExprie = 600; + private $completedExprie = 180; public function __construct() { From 2b91d8da34e3af30243caa003f1748d26665a40a Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 15 Oct 2025 15:01:27 +0800 Subject: [PATCH 14/19] =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/JournalArticle.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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]; //邮件发送记录 From ac7d1678db5940ab07daf8d4b1dbaaa50cfe6104 Mon Sep 17 00:00:00 2001 From: chengxl Date: Wed, 15 Oct 2025 16:13:58 +0800 Subject: [PATCH 15/19] =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Article.php | 66 +++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index 50eadc1..27339f4 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -4618,7 +4618,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(); @@ -4637,12 +4637,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 } } From 4f517e6e569037d3f11c93e4f94dbbd143399934 Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 17 Oct 2025 13:32:41 +0800 Subject: [PATCH 16/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Proofread.php | 69 +++++++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/application/api/controller/Proofread.php b/application/api/controller/Proofread.php index deedae5..46e4aa3 100644 --- a/application/api/controller/Proofread.php +++ b/application/api/controller/Proofread.php @@ -32,7 +32,8 @@ 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')); } @@ -143,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'] != 6){ + if($aArticle['state'] != 6 && $aArticle['state'] != 5){ return json_encode(array('status' => 4,'msg' => 'The article has not entered the proofreading stage')); } @@ -549,7 +550,7 @@ class Proofread extends Base if(empty($aArticle)){ return json_encode(['status' => 3,'msg' => 'The query article does not exist']); } - 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')); } @@ -575,10 +576,20 @@ class Proofread extends Base $am_id_count = empty($aCountData[$iAmId][2]) ? 0 : $aCountData[$iAmId][2]; } $is_proofread = -1; + //查询是否校对 + $aWhere = ['state' => 0,'is_proofread' => 1]; if(!empty($iAmId)){ - $aWhere = ['state' => 0,'am_id' => $iAmId]; - $aArticleMain = Db::name('article_main')->field('is_proofread')->where($aWhere)->find(); - $is_proofread = empty($aArticleMain['is_proofread']) ? $is_proofread : $aArticleMain['is_proofread']; + $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]]); } @@ -609,7 +620,7 @@ 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')); } @@ -651,7 +662,7 @@ 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')); } @@ -711,4 +722,46 @@ class Proofread extends Base 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')); + } } From b8307f0ff8b221e307814b59cc690eaf32dac547 Mon Sep 17 00:00:00 2001 From: chengxl Date: Fri, 17 Oct 2025 17:44:01 +0800 Subject: [PATCH 17/19] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/ProofReadService.php | 1148 +++++++++++++---------- 1 file changed, 655 insertions(+), 493 deletions(-) diff --git a/application/common/ProofReadService.php b/application/common/ProofReadService.php index 66171ef..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, @@ -214,7 +199,7 @@ class ProofReadService $currentCorrected, $posStart, $posEnd, - empty($rule['error_type']) ? '' : $rule['error_type'] + $rule['error_type'] ?? '' ); $processedHashes[$hash] = true; $corrected = $currentCorrected; @@ -222,196 +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' => '带括号数字范围使用长划线(—)不规范', + '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. 次高优先级:数字范围规则(避免与-冲突) + [ + 'pattern' => '~(\[\s*[-]?\d+\s*)\x{2014}\s*(\d+\s*\])~u', + 'replacement' => '$1-$2', + 'verbatim_texts' => '带括号数字范围长划线不规范', 'explanation' => '带括号的数字范围应使用短划线[-]', 'error_type' => 'en-dash' ], [ - 'pattern' => '~(\[\s*[-]?\d+\s*)-\s*(\d+\s*\])~u', // 匹配连接符(-)及可能的空格 - 'replacement' => '$1-$2', // 统一为无空格短划线(-) - 'verbatim_texts' => '带括号数字范围使用连接符(-)格式不规范', - 'explanation' => '带括号的数字范围应使用短划线[-]且前后无空格', - 'error_type' => 'en-dash' - - ], - [ - 'pattern' => '~(\[\s*[-]?\d+)\s+-\s*(\d+\s*\])~u', // 短划线前多余空格 - 'replacement' => '$1-$2', // 移除前导空格 - 'verbatim_texts' => '数字范围短划线前有多余空格', - 'explanation' => '带括号数字范围的短划线[-]前不应留空格', + 'pattern' => '~(\[\s*[-]?\d+)\s*-\s*(\d+\s*\])~u', + 'replacement' => '$1-$2', + 'verbatim_texts' => '带括号数字范围短划线空格不规范', + 'explanation' => '带括号数字范围的短划线[-]前后不应留空格', 'error_type' => 'en-dash' ], [ - 'pattern' => '~(\[\s*[-]?\d+)\s*-\s+(\d+\s*\])~u', // 短划线后多余空格 - 'replacement' => '$1-$2', // 移除后导空格 - 'verbatim_texts' => '数字范围短划线后有多余空格', - 'explanation' => '带括号数字范围的短划线[-]后不应留空格', - 'error_type' => 'en-dash' - ], - - // ====================== 2. 无括号数字范围规则(次高优先级,避免与减号运算规则冲突) ====================== - [ - 'pattern' => '~(\b\d+)\s*—\s*(\d+\b)~u', // 匹配长划线(—) - 'replacement' => '$1-$2', // 替换为短划线(-) - 'verbatim_texts' => '无括号数字范围使用长划线(—)不规范', + 'pattern' => '~(\b\d+)\s*—\s*(\d+\b)~u', + 'replacement' => '$1-$2', + 'verbatim_texts' => '无括号数字范围长划线不规范', 'explanation' => '无括号的数字范围应使用短划线[-]', 'error_type' => 'bracket_en-dash' ], [ - 'pattern' => '~(\b\d+)\s*-\s*(\d+\b)~u', // 匹配连接符(-)及可能的空格 - 'replacement' => '$1-$2', // 统一为无空格短划线(-) - 'verbatim_texts' => '无括号数字范围使用连接符(-)格式不规范', - 'explanation' => '无括号的数字范围应使用短划线[-]且前后无空格', + 'pattern' => '~ + (? '$1-$2', + 'verbatim_texts' => '无括号数字范围短划线空格不规范', + 'explanation' => '无括号数字范围的短划线[-]前后不应留空格', 'error_type' => 'bracket_en-dash' ], - // ====================== 3. 运算符空格规则(按「复合→独立」顺序,避免冲突) ====================== + // 3. 核心优先级:运算符规则(精准匹配,排除No.编号干扰) [ - 'pattern' => '~(\S)\s*([<>!]=|===|!==)\s*(\S)~u', // 复合运算符(>=、<=、==、!=、===、!==) + 'pattern' => '~(\S)\s*([<>!]=|===|!==)\s*(\S)~u', 'replacement' => '$1 $2 $3', 'verbatim_texts' => '复合运算符前后空格不规范', 'explanation' => '复合运算符[>=、<=、==、!=、===、!==]前后应各留一个空格', 'error_type' => 'composite_operator' ], [ - 'pattern' => '~(?|\*|\+|-|/)(\S+?)\s*=\s*(\S+?)(?!=|<|>|\*|\+|-|/)~u', - // 捕获组说明: - // $1:等号前内容(非空字符,避免匹配空格) - // $2:等号后内容(非空字符,避免匹配空格) - // 前后否定断言:排除与其他运算符(如+=、*=)的冲突 - 'replacement' => '$1 = $2', // 正确拼接“前内容 + 规范等号 + 后内容” + 'pattern' => '~ + (?|\*|\+|-|/) + (\S+?)\s*=\s*(\S+?) + (?!=|<|>|\*|\+|-|/) + ~ux', + 'replacement' => '$1 = $2', 'verbatim_texts' => '等号前后空格不规范', - 'explanation' => '独立等号[=]前后应各留一个空格', + 'explanation' => '独立等号[=]前后应各留一个空格', 'error_type' => 'equal' ], - [ - 'pattern' => '~(\d+)\s*\+\s*(\d+)~u', // 加法运算符(+) - 'replacement' => '$1 + $2', - 'verbatim_texts' => '加法运算符前后空格不规范', - 'explanation' => '加法运算符[+]前后应各留一个空格', - 'error_type' => 'plus' - ], - [ - 'pattern' => '~(\d+)\s*\*\s*(\d+)~u', // 乘法运算符(*) - 'replacement' => '$1 * $2', - 'verbatim_texts' => '乘法运算符前后空格不规范', - 'explanation' => '乘法运算符[*]前后应各留一个空格', - 'error_type' => 'ride' - ], - [ - 'pattern' => '~(\d+)\s*/\s*(\d+)~u', // 除法运算符(/) - 'replacement' => '$1 / $2', - 'verbatim_texts' => '除法运算符前后空格不规范', - 'explanation' => '除法运算符[/]前后应各留一个空格', - 'error_type' => 'except' - ], + // 乘法(排除No.编号中的*) [ 'pattern' => '~ - (? '$1 - $2', + (? '$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' => '减法运算符[-]前后应各留一个空格(非数字范围场景)', + 'explanation' => '减法运算符[-]前后应各留一个空格(纯数字运算场景)', 'error_type' => 'reduce' ], - // ====================== 4. 特殊符号规则(低优先级,避免干扰核心格式) ====================== + // 4. 低优先级:特殊符号规则 [ - 'pattern' => '~(\d+)\s+%~u', // 数字与百分号 + 'pattern' => '~(\d+)\s+%~u', 'replacement' => '$1%', - 'verbatim_texts' => '数字与百分号之间有多余空格', - 'explanation' => '数字与百分号[%]之间有多余空格', + 'verbatim_texts' => '数字与百分号空格不规范', + 'explanation' => '数字与百分号[%]之间不应留空格', 'error_type' => 'number_percentage' ], [ - 'pattern' => '~(\(\s*\d+)\s+×\s+(\d+\s*\))~u', // 先匹配「(数字 × 数字)」场景(带括号) - 'replacement' => '$1×$2', // 修正为「(数字×数字)」,如 (40×33) - 'verbatim_texts' => '带括号的乘号表示倍数时前后有多余空格', - 'explanation' => '带括号的乘号[×]表示倍数关系时前后有多余空格', - 'error_type' => 'multiple' - - ], - [ - 'pattern' => '~(\d+)\s+×\s+(\d+)~u', // 再匹配「数字 × 数字」场景(无括号) + 'pattern' => '~(\d+)\s+×\s+(\d+)~u', 'replacement' => '$1×$2', - 'verbatim_texts' => '乘号表示倍数时前后有多余空格', - 'explanation' => '乘号[×]表示倍数关系时前后不应留空格', + 'verbatim_texts' => '倍数乘号空格不规范', + 'explanation' => '乘号[×]表示倍数时前后不应留空格', 'error_type' => 'multiple' ], [ - 'pattern' => '~(\d+)\s*\*\s*(\d+)~u', // 星号(*)转乘号(×) - 'replacement' => '$1 × $2', - 'verbatim_texts' => '使用星号(*)作为乘法运算符不规范', - // 'explanation' => '乘法运算应使用标准乘号(×)替代星号(*),并前后留空格,如 3 × 5' - 'explanation' => '乘法运算应使用标准乘号[×]替代星号[*]', - 'error_type' => 'ride' - ], - [ - 'pattern' => '~(\d+)\s+:\s+(\d+)~u', // 比值符号(:) + 'pattern' => '~(\d+)\s+:\s+(\d+)~u', 'replacement' => '$1:$2', - 'verbatim_texts' => '比值符号前后有多余空格', - 'explanation' => '比值符号[:]前后有多余空格', + '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); @@ -421,79 +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; - $errorType = 'invalid_zero'; - 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, - "删除小数点后无效零", - $originalContent, - $currentCorrected, - $posStart, - $posEnd, - $errorType - ); - } + //保护软件版本 + $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); @@ -501,54 +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); - $errorType = 'thousandth_separator'; $errors[] = $this->createError( - $original, - $formatted, - "4位及以上整数添加千分位分隔符", - $originalContent, - $currentCorrected, - $posStart, - $posEnd, - $errorType - + $original, $formatted, "四位及以上的数字需要每三位加一个逗号", + $originalContent, $currentCorrected, $posStart, $posEnd, 'thousandth_separator' ); return $formatted; }, @@ -556,136 +677,291 @@ 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 = empty( $unit['full']) ? '' : $unit['full']; + $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, @@ -694,7 +970,7 @@ class ProofReadService ); } - // 记录替换映射(去重,避免重复替换) + // 记录替换映射 if (!isset($replaceMap[$original])) { $replaceMap[$original] = $fixed; } @@ -703,13 +979,12 @@ class ProofReadService } } - // 批量高效替换 + // 批量替换并处理错误 if (!empty($replaceMap)) { $corrected = strtr($corrected, $replaceMap); } - - // 统一处理错误 $this->handleErrors($errors); + return $corrected; } @@ -729,39 +1004,37 @@ 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( @@ -776,14 +1049,14 @@ class ProofReadService ); } - // 记录替换映射(去重) + // 记录替换映射(去重,避免同一内容多次替换) if (!isset($replaceMap[$originalFull])) { $replaceMap[$originalFull] = $fixedFull; } } } - // 批量替换 + // 批量替换所有不规范单位(高效处理,避免循环替换) if (!empty($replaceMap)) { $corrected = strtr($corrected, $replaceMap); } @@ -814,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") @@ -874,100 +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); - $errorType = 'No.'; - if (!isset($errors[$errorHash])) { - $errors[$errorHash] = $this->createError( - $originalFull, // verbatim_texts:具体错误片段 - $fixedFull, // revised_content:错误片段的修正结果 - 'No. 格式不规范,正确格式为「No. 数字」', // explanation:错误说明 - // 'No. 格式不规范:' . implode(',', $errorReasons) . ',正确格式为「No. 数字」', // explanation:错误说明 - $originalContent, // original:完整原始内容(整个输入文本) - strtr($originalContent, $replaceMap), // corrected:完整修正内容(基于当前替换映射生成) - $posStart, // position_start:错误起始位置 - $posEnd, // position_end:错误结束位置 - $errorType //错误类型 - ); - } - } - } - // 批量替换所有错误片段(高效处理,避免循环内重复替换) - if (!empty($replaceMap)) { - $corrected = strtr($corrected, $replaceMap); - } - } - - $this->handleErrors($errors); - return $corrected; - } - /** * 图表标题一律使用全称Figure 1, Table 1.不能写成Fig. 1, Tab 1. */ @@ -989,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); From 40957f1402ee63d1f38d700dccc6c2ef61ea4c01 Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 20 Oct 2025 16:55:08 +0800 Subject: [PATCH 18/19] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Chief.php | 8 ++++++++ 1 file changed, 8 insertions(+) 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); } From 017a4b19df85d2387ef9f6005d2ec3614116ccdb Mon Sep 17 00:00:00 2001 From: chengxl Date: Mon, 20 Oct 2025 16:58:08 +0800 Subject: [PATCH 19/19] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Board.php | 121 +++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) 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()]); + } + } + } }