参考文献相关上传

This commit is contained in:
chengxl
2025-11-28 15:46:00 +08:00
parent 7cdf825418
commit 55aa94adbe
7 changed files with 1266 additions and 0 deletions

View File

@@ -0,0 +1,541 @@
<?php
namespace app\api\controller;
use app\api\controller\Base;
use think\Db;
/**
* @title 参考文献
* @description 相关方法汇总
*/
class References extends Base
{
public function __construct(\think\Request $request = null) {
parent::__construct($request);
}
//OPENAI token
private $sApiKey = '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';
//OPENAI URL
private $sApiUrl = 'https://api.openai.com/v1/chat/completions';
/**
* 获取参考文献的信息
* @param p_refer_id 主键ID
*/
public function get($aParam = []){
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
//必填值验证
$iPReferId = empty($aParam['p_refer_id']) ? '' : $aParam['p_refer_id'];
if(empty($iPReferId)){
return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']);
}
$aWhere = ['p_refer_id' => $iPReferId,'state' => 0];
$aRefer = Db::name('production_article_refer')->where($aWhere)->find();
if(empty($aRefer)){
return json_encode(['status' => 4,'msg' => 'Reference is empty']);
}
//获取文章信息
$aParam['p_article_id'] = $aRefer['p_article_id'];
$aArticle = $this->getArticle($aParam);
$iStatus = empty($aArticle['status']) ? 0 : $aArticle['status'];
if($iStatus != 1){
return json_encode($aArticle);
}
$aArticle = empty($aArticle['data']) ? [] : $aArticle['data'];
if(empty($aArticle)){
return json_encode(['status' => 3,'msg' => 'The article does not exist']);
}
//获取参考文献信息作者名.文章题目.期刊名缩写.年卷页.Available at: //https://doi.org/xxxxx
//作者
$sData = $aRefer['refer_frag'];
if($aRefer['refer_type'] == 'journal'){
if(!empty($aRefer['doilink'])){
$sAuthor = empty($aRefer['author']) ? '' : trim(trim($aRefer['author']),'.');
if(!empty($sAuthor)){
$aAuthor = explode(',', $sAuthor);
if(count($aAuthor) > 3){
$sAuthor = implode(',', array_slice($aAuthor, 0,3));
$sAuthor .= ', et al';
}
if(count($aAuthor) <= 3 ){
$sAuthor = implode(',', $aAuthor);
}
}
//文章标题
$sTitle = empty($aRefer['title']) ? '' : trim(trim($aRefer['title']),'.');
//期刊名缩写
$sJoura = empty($aRefer['joura']) ? '' : trim(trim($aRefer['joura']),'.');
//年卷页
$sDateno = empty($aRefer['dateno']) ? '' : trim(trim($aRefer['dateno']),'.');
//DOI
$sDoilink = empty($aRefer['doilink']) ? '' : trim($aRefer['doilink']);
if(!empty($sDoilink)){
$sDoilink = strpos($sDoilink ,"http")===false ? "https://doi.org/".$sDoilink : $sDoilink;
$sDoilink = str_replace('http://doi.org/', 'https://doi.org/', $sDoilink);
}
$sReferDoi = empty($aRefer['refer_doi']) ? '' : trim($aRefer['refer_doi']);
if(!empty($sReferDoi)){
$sReferDoi = strpos($sReferDoi ,"http")===false ? "https://doi.org/".$sReferDoi : $sReferDoi;
$sReferDoi = str_replace('http://doi.org/', 'https://doi.org/', $sReferDoi);
}
$sDoilink = empty($sDoilink) ? $sReferDoi : $sDoilink;
$sData = $sAuthor.'.'.$sTitle.'.'.$sJoura.'.'.$sDateno.".Available at:\n".$sDoilink;
}
}
if($aRefer['refer_type'] == 'book'){
$sAuthor = empty($aRefer['author']) ? '' : trim(trim($aRefer['author']),'.');
if(!empty($sAuthor)){
$aAuthor = explode(',', $sAuthor);
if(count($aAuthor) > 3){
$sAuthor = implode(',', array_slice($aAuthor, 0,3));
$sAuthor .= ', et al';
}
if(count($aAuthor) <= 3 ){
$sAuthor = implode(',', $aAuthor);
}
}
//文章标题
$sTitle = empty($aRefer['title']) ? '' : trim(trim($aRefer['title']),'.');
//期刊名缩写
$sJoura = empty($aRefer['joura']) ? '' : trim(trim($aRefer['joura']),'.');
//年卷页
$sDateno = empty($aRefer['dateno']) ? '' : trim(trim($aRefer['dateno']),'.');
//DOI
$sDoilink = empty($aRefer['isbn']) ? '' : trim($aRefer['isbn']);
$sData = $sAuthor.'.'.$sTitle.'.'.$sJoura.'.'.$sDateno.".Available at:\n".$sDoilink;
}
$aRefer['deal_content'] = $sData;
return json_encode(['status' => 1,'msg' => 'success','data' => $aRefer]);
}
/**
* 修改参考文献的信息
* @param p_refer_id 主键ID
*/
public function modify($aParam = []){
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
//必填值验证
$iPReferId = empty($aParam['p_refer_id']) ? '' : $aParam['p_refer_id'];
if(empty($iPReferId)){
return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']);
}
$sContent = empty($aParam['content']) ? '' : $aParam['content'];
if(empty($sContent)){
return json_encode(['status' => 2,'msg' => 'Please enter the modification content']);
}
if(!is_string($sContent)){
return json_encode(['status' => 2,'msg' => 'The content format is incorrect']);
}
//获取参考文献信息
$aWhere = ['p_refer_id' => $iPReferId,'state' => 0];
$aRefer = Db::name('production_article_refer')->where($aWhere)->find();
if(empty($aRefer)){
return json_encode(['status' => 4,'msg' => 'Reference is empty']);
}
//获取文章信息
$aParam['p_article_id'] = $aRefer['p_article_id'];
$aArticle = $this->getArticle($aParam);
$iStatus = empty($aArticle['status']) ? 0 : $aArticle['status'];
if($iStatus != 1){
return json_encode($aArticle);
}
$aArticle = empty($aArticle['data']) ? [] : $aArticle['data'];
if(empty($aArticle)){
return json_encode(['status' => 3,'msg' => 'The article does not exist']);
}
//数据处理
$aContent = json_decode($this->dealContent(['content' => $sContent]),true);
$aUpdate = empty($aContent['data']) ? [] : $aContent['data'];
if(empty($aUpdate)){
return json_encode(['status' => 5,'msg' => 'The content format is incorrect']);
}
$aUpdate['refer_content'] = $sContent;
$aUpdate['is_change'] = 1;
$aUpdate['update_time'] = time();
//更新数据
$aWhere = ['p_refer_id' => $iPReferId,'state' => 0];
$result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate);
if($result === false){
return json_encode(['status' => 6,'msg' => 'Update failed']);
}
return json_encode(['status' => 1,'msg' => 'success']);
}
/**
* 处理参考文献的信息
* @param p_refer_id 主键ID
*/
public function dealContent($aParam = []){
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
//必填验证
$sContent = empty($aParam['content']) ? '' : $aParam['content'];
if(empty($sContent)){
return json_encode(['status' => 2,'msg' => 'Please enter the modification content']);
}
if(!is_string($sContent)){
return json_encode(['status' => 2,'msg' => 'The content format is incorrect']);
}
$aContent = explode('.', $sContent);
$aUpdate = [];
if(count($aContent) > 1){
$aField = [0 => 'author',1 => 'title', 2 => 'joura',3 => 'dateno'];
$aStart = array_slice($aContent, 0,4);
foreach ($aStart as $key => $value) {
if(empty($value)){
continue;
}
$aUpdate[$aField[$key]] = trim(trim($value),'.');
}
$sDoi = empty(array_slice($aContent, 4)) ? '' : implode('.', array_slice($aContent, 4));
// 匹配http/https开头的URL正则
$urlPattern = '/https?:\/\/[^\s<>"]+|http?:\/\/[^\s<>"]+/i';
// 执行匹配preg_match_all返回所有结果
preg_match_all($urlPattern, $sDoi, $matches);
if(!empty($matches[0])){
$sDoi = implode(',', array_unique($matches[0]));
}
if(empty($sDoi)){
return json_encode(['status' => 4,'msg' => 'Reference DOI is empty']);
}
$sDoi = trim(trim($sDoi),':');
$sDoi = strpos($sDoi ,"http")===false ? "https://doi.org/".$sDoi : $sDoi;
$sDoi = str_replace('http://doi.org/', 'https://doi.org/', $sDoi);
$aUpdate['doilink'] = $sDoi;
$doiPattern = '/10\.\d{4,9}\/[^\s\/?#&=]+/i';
if (preg_match($doiPattern, $sDoi, $matches)) {
$aUpdate['doi'] = $matches[0];
}else{
$aUpdate['doi'] = $sDoi;
}
if(!empty($aUpdate['author'])){
$aUpdate['author'] = trim(trim($aUpdate['author'])).'.';
}
}
return json_encode(['status' => 1,'msg' => 'success','data' => $aUpdate]);
}
/**
* 获取文章信息
*/
private function getArticle($aParam = []){
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
//获取生产文章信息
$iPArticleId = empty($aParam['p_article_id']) ? 0 : $aParam['p_article_id'];
if(empty($iPArticleId)){
return ['status' => 2,'msg' => 'Please select the article to query'];
}
$aWhere = ['p_article_id' => $iPArticleId,'state' => 0];
$aProductionArticle = Db::name('production_article')->field('article_id')->where($aWhere)->find();
$iArticleId = empty($aProductionArticle['article_id']) ? 0 : $aProductionArticle['article_id'];
if(empty($iArticleId)) {
return ['status' => 2,'msg' => 'No articles found'];
}
//查询条件
$aWhere = ['article_id' => $iArticleId,'state' => ['in',[5,6]]];
$aArticle = Db::name('article')->field('article_id')->where($aWhere)->find();
if(empty($aArticle)){
return ['status' => 3,'msg' => 'The article does not exist or has not entered the editorial reference status'];
}
$aArticle['p_article_id'] = $iPArticleId;
return ['status' => 1,'msg' => 'success','data' => $aArticle];
}
/**
* AI检测
*/
public function checkByAi($aParam = []){
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
//获取文章信息
$aArticle = $this->getArticle($aParam);
$iStatus = empty($aArticle['status']) ? 0 : $aArticle['status'];
if($iStatus != 1){
return json_encode($aArticle);
}
$aArticle = empty($aArticle['data']) ? [] : $aArticle['data'];
if(empty($aArticle)){
return json_encode(['status' => 3,'msg' => 'The article does not exist']);
}
//查询参考文献信息
$aWhere = ['p_article_id' => $aArticle['p_article_id'],'state' => 0,'doilink' => ''];
$aRefer = Db::name('production_article_refer')->field('p_refer_id,p_article_id,refer_type,refer_content,doilink,refer_doi')->where($aWhere)->select();
if(empty($aRefer)){
return json_encode(['status' => 4,'msg' => 'No reference information found']);
}
//数据处理
foreach ($aRefer as $key => $value) {
if(empty($value['refer_doi'])){
continue;
}
if($value['refer_doi'] == 'Not Available'){
continue;
}
if($value['refer_type'] == 'journal' && !empty($value['doilink'])){
continue;
}
if($value['refer_type'] == 'book' && !empty($value['isbn'])){
continue;
}
//写入获取参考文献详情队列
\think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$value,'AiCheckReferByDoi');
}
return json_encode(['status' => 1,'msg' => 'Successfully joined the AI inspection DOI queue']);
}
/**
* 获取结果
*/
public function getCheckByAiResult($aParam = []){
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
//必填值验证
$iPReferId = empty($aParam['p_refer_id']) ? '' : $aParam['p_refer_id'];
if(empty($iPReferId)){
return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']);
}
//获取参考文献信息
$aWhere = ['p_refer_id' => $iPReferId,'state' => 0];
$aRefer = Db::name('production_article_refer')->field('p_refer_id,p_article_id,refer_type,refer_content,doilink,refer_doi,state,dateno')->where($aWhere)->find();
if(empty($aRefer)){
return json_encode(['status' => 4,'msg' => 'Reference is empty'.json_encode($aParam)]);
}
if(empty($aRefer['refer_doi'])){
return json_encode(['status' => 4,'msg' => 'Reference DOI is empty'.json_encode($aParam)]);
}
if($aRefer['refer_type'] == 'journal' && !empty($aRefer['doilink'])){
$aDateno = empty($aRefer['dateno']) ? [] : explode(':', $aRefer['dateno']);
if(count($aDateno) > 1){
return json_encode(['status' => 4,'msg' => 'No need to parse again-journal'.json_encode($aParam)]);
}
}
if($aRefer['refer_type'] == 'book' && !empty($aRefer['isbn'])){
return json_encode(['status' => 4,'msg' => 'No need to parse again-book'.json_encode($aParam)]);
}
//获取文章信息
$aParam['p_article_id'] = $aRefer['p_article_id'];
$aArticle = $this->getArticle($aParam);
$iStatus = empty($aArticle['status']) ? 0 : $aArticle['status'];
if($iStatus != 1){
return json_encode($aArticle);
}
$aArticle = empty($aArticle['data']) ? [] : $aArticle['data'];
if(empty($aArticle)){
return json_encode(['status' => 3,'msg' => 'The article does not exist']);
}
//请求AI获取结果
$aResult = $this->curlOpenAIByDoi(['doi' => $aRefer['refer_doi']]);
$iStatus = empty($aResult['status']) ? 0 : $aResult['status'];
$sMsg = empty($aResult['msg']) ? 'The DOI number AI did not find any relevant information' : $aResult['msg'];
if($iStatus != 1){
return json_encode(['status' => 4,'msg' => $sMsg]);
}
$aData = empty($aResult['data']) ? [] : $aResult['data'];
if(empty($aData)){
return json_encode(['status' => 5,'msg' => 'AI obtains empty data']);
}
//写入日志
$aLog = [];
$aLog['content'] = json_encode($aResult);
$aLog['update_time'] = time();
$aLog['p_refer_id'] = $iPReferId;
$iLogId = Db::name('production_article_refer_ai')->insertGetId($aLog);
$iIsAiCheck = empty($aData['is_ai_check']) ? 2 : $aData['is_ai_check'];
if($iIsAiCheck != 1){//AI未检测到信息
return json_encode(['status' => 6,'msg' => 'AI did not find any information'.json_encode($aParam)]);
}
//数据处理入库
$aField = ['author','title','joura','dateno','doilink'];
foreach ($aField as $key => $value) {
if(empty($aData[$value])){
continue;
}
if($value == 'author'){
$aUpdate['author'] = implode(',', $aData['author']);
// $aUpdate['author'] = str_replace('et al.', '', $aUpdate['author']);
}else{
$aUpdate[$value] = $aData[$value];
}
}
if(empty($aUpdate)){
return json_encode(['status' => 6,'msg' => 'Update data to empty'.json_encode($aData)]);
}
if($aRefer['refer_type'] == 'other'){
$aUpdate['refer_type'] = 'journal';
}
if($aRefer['refer_type'] == 'book' && !empty($aUpdate['doilink'])){
$aUpdate['refer_type'] = $aUpdate['doilink'];
unset($aUpdate['doilink']);
}
$aLog = $aUpdate;
$aUpdate['is_change'] = 1;
$aUpdate['is_ai_check'] = 1;
$aUpdate['update_time'] = time();
Db::startTrans();
//更新数据
$aWhere = ['p_refer_id' => $iPReferId,'state' => 0];
$result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate);
if($result === false){
return json_encode(['status' => 6,'msg' => 'Update failed']);
}
//更新日志
if(!empty($iLogId)){
$aWhere = ['id' => $iLogId];
if(isset($aLog['refer_type'])){
unset($aLog['refer_type']);
}
$result = Db::name('production_article_refer_ai')->where($aWhere)->limit(1)->update($aLog);
}
Db::commit();
return json_encode(['status' => 1,'msg' => 'success']);
}
/**
* 对接OPENAI
*/
private function curlOpenAIByDoi($aParam = []){
//获取DOI
$sDoi = empty($aParam['doi']) ? '' : $aParam['doi'];
if(empty($sDoi)){
return ['status' => 2,'msg' => 'Reference doi is empty'];
}
//系统角色
$sSysMessagePrompt = '请完成以下任务:
1. 根据提供的DOI号查询该文献的AMA引用格式
2. 按照以下规则调整AMA引用格式
- 第三个作者名字后添加 et al.
- DOI前加上"Available at: "
- DOI信息格式调整为"https://doi.org/+真实DOI"替换真实DOI为文献实际DOI.
3. 严格按照以下JSON结构返回结果仅返回JSON数据不要额外文字,包含字段doilinkurl格式、title标题、author作者数组、joura出版社名称、dateno年;卷(期):起始页-终止页),is_ai_check(默认1)
4. 若未查询到信息字段is_ai_check为2,相关字段为null。';
//用户角色
$sUserPrompt = '我提供的DOI是:'.$sDoi;
$aMessage = [
['role' => 'system', 'content' => $sSysMessagePrompt],
['role' => 'user', 'content' => $sUserPrompt],
];
//请求OPENAI接口
$sModel = empty($aParam['model']) ? 'gpt-4.1' : $aParam['model'];//模型
$sApiUrl = $this->sApiUrl;//'http://chat.taimed.cn/v1/chat/completions';//
$aParam = ['model' => $sModel,'url' => $sApiUrl,'temperature' => 0,'messages' => $aMessage,'api_key' => $this->sApiKey];
$oOpenAi = new \app\common\OpenAi;
$aResult = json_decode($oOpenAi->curlOpenAI($aParam),true);
return $aResult;
}
/**
* 作者修改完成发邮件
*/
public function finishSendEmail(){
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
//文章ID
$iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id'];
if(empty($iArticleId)){
return json_encode(array('status' => 2,'msg' => 'Please select an article'));
}
//查询条件
$aWhere = ['article_id' => $iArticleId,'state' => ['in',[5,6]]];
$aArticle = Db::name('article')->field('article_id,journal_id,accept_sn')->where($aWhere)->find();
if(empty($aArticle)){
return json_encode(['status' => 3,'msg' => 'The article does not exist or has not entered the editorial reference status']);
}
$aWhere = ['article_id' => $iArticleId,'state' => 0];
$aProductionArticle = Db::name('production_article')->field('p_article_id')->where($aWhere)->find();
if(empty($aProductionArticle)) {
return ['status' => 2,'msg' => 'The article has not entered the production stage'];
}
//查询是否有参考文献
$aWhere = ['p_article_id' => $aProductionArticle['p_article_id'],'state' => 0];
$aRefer = Db::name('production_article_refer')->field('article_id')->where($aWhere)->find();
if(empty($aRefer)) {
return ['status' => 2,'msg' => 'No reference information found, please be patient and wait for the editor to upload'];
}
//查询期刊信息
if(empty($aArticle['journal_id'])){
return json_encode(array('status' => 4,'msg' => 'The article is not associated with a journal' ));
}
$aWhere = ['state' => 0,'journal_id' => $aArticle['journal_id']];
$aJournal = Db::name('journal')->where($aWhere)->find();
if(empty($aJournal)){
return json_encode(array('status' => 5,'msg' => 'No journal information found' ));
}
//查询编辑邮箱
$iUserId = empty($aJournal['editor_id']) ? '' : $aJournal['editor_id'];
if(empty($iUserId)){
return json_encode(array('status' => 6,'msg' => 'The journal to which the article belongs has not designated a responsible editor' ));
}
$aWhere = ['user_id' => $iUserId,'state' => 0,'email' => ['<>','']];
$aUser = Db::name('user')->field('user_id,email,realname,account')->where($aWhere)->find();
if(empty($aUser)){
return json_encode(['status' => 7,'msg' => "Edit email as empty"]);
}
//处理发邮件
//邮件模版
$aEmailConfig = [
'email_subject' => '{journal_title}-{accept_sn}',
'email_content' => '
Dear Editor,<br><br>
The authors have revised the formats of all references, please check.<br>
Sn:{accept_sn}<br><br>
Sincerely,<br>Editorial Office<br>
<a href="https://www.tmrjournals.com/draw_up.html?issn={journal_issn}">Subscribe to this journal</a><br>{journal_title}<br>
Email: {journal_email}<br>
Website: {website}'
];
//邮件内容
$aSearch = [
'{accept_sn}' => empty($aArticle['accept_sn']) ? '' : $aArticle['accept_sn'],//accept_sn
'{journal_title}' => empty($aJournal['title']) ? '' : $aJournal['title'],//期刊名
'{journal_issn}' => empty($aJournal['issn']) ? '' : $aJournal['issn'],
'{journal_email}' => empty($aJournal['email']) ? '' : $aJournal['email'],
'{website}' => empty($aJournal['website']) ? '' : $aJournal['website'],
];
//发邮件
//邮件标题
$email = $aUser['email'];
$title = str_replace(array_keys($aSearch), array_values($aSearch),$aEmailConfig['email_subject']);
//邮件内容变量替换
$content = str_replace(array_keys($aSearch), array_values($aSearch), $aEmailConfig['email_content']);
$pre = \think\Env::get('emailtemplete.pre');
$net = \think\Env::get('emailtemplete.net');
$net1 = str_replace("{{email}}",trim($email),$net);
$content=$pre.$content.$net1;
//发送邮件
$memail = empty($aJournal['email']) ? '' : $aJournal['email'];
$mpassword = empty($aJournal['epassword']) ? '' : $aJournal['epassword'];
//期刊标题
$from_name = empty($aJournal['title']) ? '' : $aJournal['title'];
//邮件队列组装参数
$aResult = sendEmail($email,$title,$from_name,$content,$memail,$mpassword);
$iStatus = empty($aResult['status']) ? 1 : $aResult['status'];
$iIsSuccess = 2;
$sMsg = empty($aResult['data']) ? '失败' : $aResult['data'];
if($iStatus == 1){
return json_encode(['status' => 1,'msg' => 'success']);
}
return json_encode(['status' => 8,'msg' => 'fail']);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace app\api\job;
use think\queue\Job;
use app\common\QueueJob;
use app\common\QueueRedis;
use think\Db;
class AiCheckRefer
{
private $oQueueJob;
private $QueueRedis;
private $completedExprie = 3600;
public function __construct()
{
$this->oQueueJob = new QueueJob;
$this->QueueRedis = QueueRedis::getInstance();
}
public function fire(Job $job, $data)
{
//任务开始判断
$this->oQueueJob->init($job);
// 获取 Redis 任务的原始数据
$rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody();
$jobData = empty($rawBody) ? [] : json_decode($rawBody, true);
$jobId = empty($jobData['id']) ? 'unknown' : $jobData['id'];
$this->oQueueJob->log("-----------队列任务开始-----------");
$this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}");
// 获取生产文章ID
$iPArticleId = empty($data['p_article_id']) ? 0 : $data['p_article_id'];
if (empty($iPArticleId)) {
$this->oQueueJob->log("无效的p_article_id删除任务");
$job->delete();
return;
}
try {
// 生成Redis键并尝试获取锁
$sClassName = get_class($this);
$sRedisKey = "queue_job:{$sClassName}:{$iPArticleId}";
$sRedisValue = uniqid() . '_' . getmypid();
if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) {
return; // 未获取到锁,已处理
}
//生成内容
$oProductionArticleRefer = new \app\api\controller\References;
$response = $oProductionArticleRefer->checkByAi($data);
// 验证API响应
if (empty($response)) {
throw new \RuntimeException("OpenAI API返回空结果");
}
// 检查JSON解析错误
$aResult = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException("解析OpenAI响应失败: " . json_last_error_msg() . " | 原始响应: {$response}");
}
$sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg'];
//更新完成标识
$this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue);
$job->delete();
$this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}");
} catch (\RuntimeException $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\LogicException $e) {
$this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\Exception $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} finally {
$this->oQueueJob->finnal();
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace app\api\job;
use think\queue\Job;
use app\common\QueueJob;
use app\common\QueueRedis;
use think\Db;
class AiCheckReferByDoi
{
private $oQueueJob;
private $QueueRedis;
private $completedExprie = 3600;
public function __construct()
{
$this->oQueueJob = new QueueJob;
$this->QueueRedis = QueueRedis::getInstance();
}
public function fire(Job $job, $data)
{
//任务开始判断
$this->oQueueJob->init($job);
// 获取 Redis 任务的原始数据
$rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody();
$jobData = empty($rawBody) ? [] : json_decode($rawBody, true);
$jobId = empty($jobData['id']) ? 'unknown' : $jobData['id'];
$this->oQueueJob->log("-----------队列任务开始-----------");
$this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}");
// 获取生产文章ID
$iPArticleId = empty($data['p_article_id']) ? 0 : $data['p_article_id'];
if (empty($iPArticleId)) {
$this->oQueueJob->log("无效的p_article_id删除任务");
$job->delete();
return;
}
// 获取参考文献ID
$iPReferId = empty($data['p_refer_id']) ? 0 : $data['p_refer_id'];
if (empty($iPArticleId)) {
$this->oQueueJob->log("无效的p_article_id删除任务");
$job->delete();
return;
}
try {
// 生成Redis键并尝试获取锁
$sClassName = get_class($this);
$sRedisKey = "queue_job:{$sClassName}:{$iPArticleId}:{$iPReferId}";
$sRedisValue = uniqid() . '_' . getmypid();
if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) {
return; // 未获取到锁,已处理
}
//生成内容
$oProductionArticleRefer = new \app\api\controller\References;
$response = $oProductionArticleRefer->getCheckByAiResult($data);
// 验证API响应
if (empty($response)) {
throw new \RuntimeException("OpenAI API返回空结果");
}
// 检查JSON解析错误
$aResult = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException("解析OpenAI响应失败: " . json_last_error_msg() . " | 原始响应: {$response}");
}
$sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg'];
//更新完成标识
$this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue);
$job->delete();
$this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}");
} catch (\RuntimeException $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\LogicException $e) {
$this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\Exception $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} finally {
$this->oQueueJob->finnal();
}
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace app\api\job;
use think\queue\Job;
use app\common\QueueJob;
use app\common\QueueRedis;
use app\common\ProductionArticleRefer;
use think\Db;
class ArticleReferDetailQueue
{
private $oQueueJob;
private $QueueRedis;
private $completedExprie = 3600;
public function __construct()
{
$this->oQueueJob = new QueueJob;
$this->QueueRedis = QueueRedis::getInstance();
}
public function fire(Job $job, $data)
{
//任务开始判断
$this->oQueueJob->init($job);
// 获取 Redis 任务的原始数据
$rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody();
$jobData = empty($rawBody) ? [] : json_decode($rawBody, true);
$jobId = empty($jobData['id']) ? 'unknown' : $jobData['id'];
$this->oQueueJob->log("-----------队列任务开始-----------");
$this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}");
// // 获取文章ID
// $iArticleId = empty($data['article_id']) ? 0 : $data['article_id'];
// if (empty($iArticleId)) {
// $this->oQueueJob->log("无效的article_id删除任务");
// $job->delete();
// return;
// }
// 获取生产文章ID
$iPArticleId = empty($data['p_article_id']) ? 0 : $data['p_article_id'];
if (empty($iPArticleId)) {
$this->oQueueJob->log("无效的p_article_id删除任务");
$job->delete();
return;
}
// 获取生产文章ID
$iPReferId = empty($data['p_refer_id']) ? 0 : $data['p_refer_id'];
if (empty($iPReferId)) {
$this->oQueueJob->log("无效的p_refer_id删除任务");
$job->delete();
return;
}
try {
// 生成Redis键并尝试获取锁
$sClassName = get_class($this);
$sRedisKey = "queue_job:{$sClassName}:{$iPArticleId}:{$iPReferId}";
$sRedisValue = uniqid() . '_' . getmypid();
if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) {
return; // 未获取到锁,已处理
}
//生成内容
$oProductionArticleRefer = new ProductionArticleRefer;
$response = $oProductionArticleRefer->get($data);
// 验证API响应
if (empty($response)) {
throw new \RuntimeException("返回空结果");
}
// 检查JSON解析错误
$aResult = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException("解析响应失败: " . json_last_error_msg() . " | 原始响应: {$response}");
}
$sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg'];
//更新完成标识
$this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue);
$job->delete();
$this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}");
} catch (\RuntimeException $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\LogicException $e) {
$this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\Exception $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} finally {
$this->oQueueJob->finnal();
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace app\api\job;
use think\queue\Job;
use app\common\QueueJob;
use app\common\QueueRedis;
use app\common\ProductionArticleRefer;
use think\Db;
class ArticleReferQueue
{
private $oQueueJob;
private $QueueRedis;
private $completedExprie = 180;
public function __construct()
{
$this->oQueueJob = new QueueJob;
$this->QueueRedis = QueueRedis::getInstance();
}
public function fire(Job $job, $data)
{
//任务开始判断
$this->oQueueJob->init($job);
// 获取 Redis 任务的原始数据
$rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody();
$jobData = empty($rawBody) ? [] : json_decode($rawBody, true);
$jobId = empty($jobData['id']) ? 'unknown' : $jobData['id'];
$this->oQueueJob->log("-----------队列任务开始-----------");
$this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}");
// 获取文章ID
$iArticleId = empty($data['article_id']) ? 0 : $data['article_id'];
if (empty($iArticleId)) {
$this->oQueueJob->log("无效的article_id删除任务");
$job->delete();
return;
}
// 获取生产文章ID
$iPArticleId = empty($data['p_article_id']) ? 0 : $data['p_article_id'];
if (empty($iPArticleId)) {
$this->oQueueJob->log("无效的p_article_id删除任务");
$job->delete();
return;
}
try {
// 生成Redis键并尝试获取锁
$sClassName = get_class($this);
$sRedisKey = "queue_job:{$sClassName}:{$iArticleId}:{$iPArticleId}";
$sRedisValue = uniqid() . '_' . getmypid();
if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) {
return; // 未获取到锁,已处理
}
//生成内容
$oProductionArticleRefer = new ProductionArticleRefer;
$response = $oProductionArticleRefer->top($data);
// 验证API响应
if (empty($response)) {
throw new \RuntimeException("OpenAI API返回空结果");
}
// 检查JSON解析错误
$aResult = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException("解析OpenAI响应失败: " . json_last_error_msg() . " | 原始响应: {$response}");
}
$sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg'];
//更新完成标识
$this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue);
$job->delete();
$this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}");
} catch (\RuntimeException $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\LogicException $e) {
$this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\Exception $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} finally {
$this->oQueueJob->finnal();
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace app\api\job;
use think\queue\Job;
use app\common\QueueJob;
use app\common\QueueRedis;
use app\common\ProofRead;
class ProofReadQueue
{
private $oQueueJob;
private $QueueRedis;
private $completedExprie = 3600;
public function __construct()
{
$this->oQueueJob = new QueueJob;
$this->QueueRedis = QueueRedis::getInstance();
}
public function fire(Job $job, $data)
{
//任务开始判断
$this->oQueueJob->init($job);
// 获取 Redis 任务的原始数据
$rawBody = empty($job->getRawBody()) ? '' : $job->getRawBody();
$jobData = empty($rawBody) ? [] : json_decode($rawBody, true);
$jobId = empty($jobData['id']) ? 'unknown' : $jobData['id'];
$this->oQueueJob->log("-----------队列任务开始-----------");
$this->oQueueJob->log("当前任务ID: {$jobId}, 尝试次数: {$job->attempts()}");
// 获取文章ID
$iArticleId = empty($data['article_id']) ? 0 : $data['article_id'];
$sChunkIndex = empty($data['chunkIndex']) ? 0 : $data['chunkIndex'];
$sPrompt = empty($data['prompt']) ? '' : $data['prompt'];
if (empty($iArticleId)) {
$this->oQueueJob->log("无效的article_id删除任务");
$job->delete();
return;
}
try {
// 生成Redis键并尝试获取锁
$sClassName = get_class($this);
$sRedisKey = "queue_job:{$sClassName}:{$iArticleId}:{$sPrompt}_{$sChunkIndex}";
$sRedisValue = uniqid() . '_' . getmypid();
if (!$this->oQueueJob->acquireLock($sRedisKey, $sRedisValue, $job)) {
return; // 未获取到锁,已处理
}
//生成内容
$oAireview = new ProofRead;
$response = $oAireview->proofReadQueue($data);
// 验证API响应
if (empty($response)) {
throw new \RuntimeException("OpenAI API返回空结果");
}
// 检查JSON解析错误
$aResult = json_decode($response, true);
echo '<pre>';var_dump($aResult);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException("解析OpenAI响应失败: " . json_last_error_msg() . " | 原始响应: {$response}");
}
$sMsg = empty($aResult['msg']) ? 'success' : $aResult['msg'];
//更新完成标识
$this->QueueRedis->finishJob($sRedisKey, 'completed', $this->completedExprie,$sRedisValue);
$job->delete();
$this->oQueueJob->log("任务执行成功 | 日志ID: {$sRedisKey} | 执行日志:{$sMsg}");
} catch (\RuntimeException $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\LogicException $e) {
$this->oQueueJob->handleNonRetryableException($e,$sRedisKey,$sRedisValue, $job);
} catch (\Exception $e) {
$this->oQueueJob->handleRetryableException($e,$sRedisKey,$sRedisValue, $job);
} finally {
$this->oQueueJob->finnal();
}
}
}

View File

@@ -0,0 +1,303 @@
<?php
namespace app\common;
use think\Db;
use think\Env;
class ProductionArticleRefer
{
// DOI匹配核心正则生产级优化支持%字符、限制长度、单词边界断言)
// private const DOI_PATTERN = '/(?:doi[:\s]?|DOI[:\s]?)?\b10\.\d+(?:\.\d+)*\/[a-zA-Z0-9._\-!()%\/:;@$&+=?#[\]<>~`|^]+/i';
// 错误码与错误信息映射(标准化错误处理)
private const ERROR_CODES = [
'EMPTY_STRING' => 'Input string is empty (preprocessed)',
'NO_MATCH' => 'No valid DOI detected',
'INVALID_AFTER_CLEAN' => 'No effective DOI after cleaning',
'FORCE_EXTRACT_FAILED' => 'Forced extraction still has no valid DOI',
'EXTRACTION_EXCEPTION' => 'Exception occurred during DOI extraction process',
];
/**
* 获取未处理的参考文献
*
* @return void
*/
public function top($aParam = []) {
//文章ID
$iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id'];
if(empty($iArticleId)){
return json_encode(array('status' => 2,'msg' => 'Please select an article'.json_encode($aParam) ));
}
// 获取生产文章ID
$iPArticleId = empty($aParam['p_article_id']) ? 0 : $aParam['p_article_id'];
if(empty($iPArticleId)) {
return json_encode(array('status' => 2,'msg' => 'Please select an production article'.json_encode($aParam) ));
}
//查询未处理过的数据
$aWhere = ['p_article_id' => $iPArticleId,'article_id' => $iArticleId,'state' => 0,'refer_doi' => ['<>',''],'is_deal' => 2];
$aResult = Db::name('production_article_refer')->field('article_id,p_article_id,p_refer_id,refer_doi')->where($aWhere)->select();
if(empty($aResult)){
return json_encode(array('status' => 2,'msg' => 'The reference data to be processed is empty'.json_encode($aParam)));
}
//数据处理
foreach ($aResult as $key => $value) {
if(empty($value['refer_doi'])){
continue;
}
//调用获取参考文献详情队列
\think\Queue::push('app\api\job\ArticleReferDetailQueue@fire', $value, 'ArticleReferDetailQueue');
}
return json_encode(['status' => 1,'msg' => 'Add to reference processing queue']);
}
/**
* 处理参考文献
*
* @return void
*/
public function get($aParam = []) {
// 获取生产文章ID
$iPReferId = empty($aParam['p_refer_id']) ? 0 : $aParam['p_refer_id'];
if(empty($iPReferId)) {
return json_encode(array('status' => 2,'msg' => 'Please select a reference'.json_encode($aParam) ));
}
// 获取生产文章ID
$iPArticleId = empty($aParam['p_article_id']) ? 0 : $aParam['p_article_id'];
if(empty($iPArticleId)) {
return json_encode(array('status' => 2,'msg' => 'Please select an production article'.json_encode($aParam) ));
}
//查询未处理过的数据
$aWhere = ['p_refer_id' => $iPReferId,'p_article_id' => $iPArticleId,'state' => 0];
$aRefer = Db::name('production_article_refer')->field('refer_doi,refer_content')->where($aWhere)->find();
if(empty($aRefer)){
return json_encode(array('status' => 2,'msg' => 'No reference records found'.json_encode($aParam)));
}
if(empty($aRefer['refer_doi'])){
return json_encode(['status' => 4,'msg' => 'Reference DOI is empty'.json_encode($aParam)]);
}
//数据处理
$doi = str_replace('/', '%2F', $aRefer['refer_doi']);
$url = "https://citation.doi.org/format?doi=$doi&style=cancer-translational-medicine&lang=en-US";
$res = myGet($url);
$frag = trim(substr($res, strpos($res, '.') + 1));
if(empty($frag)){
$aUpdate = ['refer_frag' => $aRefer['refer_content'],'refer_type' => 'other','is_deal' => 1,'update_time' => time()];
$aWhere = ['p_refer_id' => $iPReferId];
$result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate);
//写入通过AI获取参考文献详情队列
\think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi');
return json_encode(array('status' => 2,'msg' => 'The data obtained from the interface is empty'.$url));
}
//整理数据入库
$update = [];
if (mb_substr_count($frag, '.') != 3){
$f = $frag . " Available at: " . PHP_EOL . "https://doi.org/" . $aRefer['refer_doi'];
$update['refer_type'] = "other";
$update['refer_frag'] = $f;
$update['cs'] = 1;
//写入通过AI获取参考文献详情队列
\think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi');
}
if (mb_substr_count($frag, '.') == 3){
$res = explode('.', $frag);
$update['author'] = prgeAuthor($res[0]);
$update['title'] = trim($res[1]);
$bj = bekjournal($res[2]);
$joura = formateJournal(trim($bj[0]));
$update['joura'] = $joura;
$is_js = 0;
if ($joura == trim($bj[0])) {
}
$update['refer_type'] = "journal";
$update['is_ja'] = $joura == trim($bj[0]) ? 0 : 1;
$update['dateno'] = str_replace(' ', '', str_replace('-', '', trim($bj[1])));
//新增处理 期卷页码 20251127 start
if(!empty($update['dateno'])){
$sStr = $update['dateno'];
$aStr = explode(':', $sStr);
if(!empty($aStr[1])){
$parts = explode('', $aStr[1]);
if(count($parts) == 2){
$prefix = empty($parts[0]) ? 0 : intval($parts[0]);
$suffix = empty($parts[1]) ? 0 : intval($parts[1]);
if($prefix > $suffix){
$prefixLen = strlen($prefix);
$suffixLen = strlen($suffix);
$missingLen = $prefixLen - $suffixLen;
if ($missingLen > 0) {
$fillPart = substr($prefix, 0, $missingLen);
$newSuffix = $fillPart . $suffix;
$update['dateno'] = $aStr[0].':'.$prefix.'-'.$newSuffix;
}
}
}
}
if(empty($aStr[1])){
//写入通过AI获取参考文献详情队列
\think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$aParam,'AiCheckReferByDoi');
}
}
//新增处理 期卷页码 20251127 end
$update['doilink'] = strpos($aRefer['refer_doi'],"http")===false?"https://doi.org/" . $aRefer['refer_doi']:$aRefer['refer_doi'];
$update['cs'] = 1;
}
//数据库更新
if(empty($update)){
return json_encode(array('status' => 3,'msg' => 'Update data to empty'.$url.'====='.$frag));
}
$aWhere = ['p_refer_id' => $iPReferId];
$update += ['is_deal' => 1,'update_time' => time()];
$result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($update);
if($result === false){
return json_encode(array('status' => 3,'msg' => 'Update failed'.json_encode($update)));
}
return json_encode(['status' => 1,'msg' => 'Update successful']);
}
// /**
// * 实例方法提取单个DOI核心逻辑生产级优化
// * @param string $str 待检测字符串
// * @param bool $standardize 是否标准化DOI转小写
// * @param bool $forceExtract 是否强制提取(忽略微小格式瑕疵)
// * @return array 提取结果含错误码、错误信息、DOI
// */
// // public function extractDoiFromString(string $str, bool $standardize = true, bool $forceExtract = false): array
// // {
// // // 初始化标准化结果
// // $result = [
// // 'has_doi' => false,
// // 'doi' => null,
// // 'error_code' => null,
// // 'error_msg' => null,
// // ];
// // try {
// // // 严格类型校验(防止非字符串参数传入)
// // if (!is_string($str)) {
// // throw new InvalidArgumentException('输入参数必须为字符串类型', 1001);
// // }
// // // 字符串预处理生产级全角转半角、URL解码、HTML标签移除等
// // $processedStr = $this->preprocessString($str);
// // if (trim($processedStr) === '') {
// // $result['error_code'] = 'EMPTY_STRING';
// // $result['error_msg'] = self::ERROR_CODES['EMPTY_STRING'];
// // return $result;
// // }
// // // 性能优化用preg_match仅匹配首个DOI替代preg_match_all
// // // 优化后的带前缀版正则
// // $pattern = '/(?:doi[:\s]*|DOI[:\s]*)?\b10\.\d+(?:\.\d+)*\/[a-zA-Z0-9._\-!()%\/:;@$&+=?#[\]<>~`|^'"{},\\\\]+(?![\w?#])/i";
// // if (!preg_match($pattern, $processedStr, $match)) {
// // $result['error_code'] = 'NO_MATCH';
// // $result['error_msg'] = self::ERROR_CODES['NO_MATCH'];
// // return $result;
// // }
// // // 清洗并验证首个DOI
// // $cleanDoi = $this->cleanAndValidateDoi($match[0], $standardize, $forceExtract);
// // if ($cleanDoi !== null) {
// // $result['has_doi'] = true;
// // $result['doi'] = $cleanDoi;
// // } else {
// // // 根据是否强制提取设置错误信息
// // $errorKey = $forceExtract ? 'FORCE_EXTRACT_FAILED' : 'INVALID_AFTER_CLEAN';
// // $result['error_code'] = $errorKey;
// // $result['error_msg'] = self::ERROR_CODES[$errorKey];
// // }
// // } catch (InvalidArgumentException $e) {
// // // 业务异常:标准化错误码和信息
// // $result['error_code'] = 'INVALID_PARAM';
// // $result['error_msg'] = '参数错误:' . $e->getMessage();
// // } catch (Exception $e) {
// // // 系统异常:隐藏敏感信息,记录通用错误
// // $result['error_code'] = 'EXTRACTION_EXCEPTION';
// // $result['error_msg'] = self::ERROR_CODES['EXTRACTION_EXCEPTION'] . '' . $e->getMessage();
// // }
// // return $result;
// // }
// // /**
// // * 字符串预处理(生产级:覆盖所有编码/格式干扰场景)
// // * @param string $str 原始字符串
// // * @return string 预处理后的纯净字符串
// // */
// // private function preprocessString(string $str): string
// // {
// // // 1. 全角转半角(解决中文全角字符干扰,如10.1007/s11042-020-10103-4)
// // $str = $this->fullWidthToHalfWidth($str);
// // // 2. 移除所有HTML标签解决网页文本中DOI被<p>/<a>/<b>等标签包裹的问题)
// // $str = strip_tags($str);
// // // 3. URL解码处理%2F等URL编码的特殊字符如10.1007%2Fs11042-020-10103-4
// // $str = urldecode($str);
// // // 4. 解码HTML实体处理&amp;、&#47;等HTML实体编码
// // $str = html_entity_decode($str, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// // // 5. 移除不可见字符(换行、制表符、零宽空格、控制字符等)
// // $str = preg_replace('/[\x00-\x1F\x7F\x{200B}-\x{200F}]/u', ' ', $str);
// // // 6. 合并多个空格为单个(避免连续空格干扰正则匹配)
// // $str = preg_replace('/\s+/', ' ', $str);
// // return $str;
// // }
// // /**
// // * 清洗并验证DOI生产级优化正则规则严格长度校验
// // * @param string $match 原始正则匹配结果
// // * @param bool $standardize 是否标准化DOI转小写
// // * @param bool $forceExtract 是否强制提取
// // * @return string|null 有效DOI或null
// // */
// // private function cleanAndValidateDoi(string $match, bool $standardize, bool $forceExtract): ?string
// // {
// // // 1. 移除DOI前缀doi:/DOI:)和首尾空白字符
// // $cleanDoi = preg_replace('/^doi[:\s]?|^DOI[:\s]?/i', '', trim($match));
// // // 2. 移除尾部常见标点避免DOI被标点包裹如10.1007/s11042-020-10103-4.
// // $cleanDoi = rtrim($cleanDoi, '.,;(){}[]!?"\'');
// // // 3. 严格的长度校验DOI官方规范6-200字符
// // $doiLength = strlen($cleanDoi);
// // if ($doiLength < 6 || $doiLength > 200) {
// // return null;
// // }
// // // 4. 验证规则生产级优化添加单词边界避免匹配不完整DOI
// // // 基础规则严格遵循官方规范10.开头+包含/+/后有内容
// // $basicRule = '/^10\.\d+\/.+$/D';
// // // 宽松规则强制提取时使用添加单词边界避免匹配被字符包裹的DOI
// // $looseRule = '/\b10\.\d+\/[^\s%]{1,190}\b/';
// // $validateRule = $forceExtract ? $looseRule : $basicRule;
// // $isValid = preg_match($validateRule, $cleanDoi) === 1;
// // // 5. 验证通过则标准化转小写否则返回null
// // if ($isValid) {
// // return $standardize ? strtolower($cleanDoi) : $cleanDoi;
// // }
// // return null;
// // }
// // /**
// // * 辅助方法:全角转半角
// // * @param string $str 包含全角字符的字符串
// // * @return string 半角字符串
// // */
// // private function fullWidthToHalfWidth(string $str): string
// // {
// // $fullWidthChars = [
// // '' => '0', '' => '1', '' => '2', '' => '3', '' => '4',
// // '' => '5', '' => '6', '' => '7', '' => '8', '' => '9',
// // '' => '.', '' => '/', '' => '-', '' => '%', '' => '!',
// // '' => '(', '' => ')', '' => ':', '' => ';', '' => ',',
// // '' => '"', '' => '\''
// // ];
// // return strtr($str, $fullWidthChars);
// // }
}
?>