From 58c4fc27aaf01163e3300508b4ec1a64186abe43 Mon Sep 17 00:00:00 2001 From: chengxl Date: Tue, 22 Jul 2025 11:19:38 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=9F=E5=88=97=E6=8F=90=E5=8F=96=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/common/Aireview.php | 97 +++++++++ application/common/Article.php | 324 ++++++++++++++++++++++++++++++ application/common/QueueRedis.php | 212 +++++++++++++++++++ 3 files changed, 633 insertions(+) create mode 100644 application/common/Aireview.php create mode 100644 application/common/Article.php create mode 100644 application/common/QueueRedis.php diff --git a/application/common/Aireview.php b/application/common/Aireview.php new file mode 100644 index 0000000..b4dc0c2 --- /dev/null +++ b/application/common/Aireview.php @@ -0,0 +1,97 @@ + 2,'msg' => 'Please select the article to be reviewed']; + } + $iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id']; + if(empty($iJournalId)){ + return ['status' => 2,'msg' => 'The journal to which the article belongs cannot be empty']; + } + //返回数组 + $aResult = ['status' => 1,'msg' => 'AI review successful']; + //数据库参数 + $aFields = ['journal_scope','attribute','contradiction','unreasonable','ethics','academic','conclusion','fund_number','hotspot','submit_direction','references_past_three','references_past_five','references_ratio_JCR1','references_ratio_JCR2','registration_assessment','cite_rate','references_num','article_field']; + foreach ($aParam as $key => $value) { + if(empty($value)){ + continue; + } + if(is_array($value)){ + if(!empty($value['assessment'])){ + $sField = $key.'_'.'assessment'; + $sAssessment = empty($value['assessment']) ? '' : $value['assessment']; + if(!empty($sAssessment)){ + $sAssessment = is_array($sAssessment) ? json_encode($sAssessment) : htmlspecialchars($value['assessment']); + } + $aInsert[$sField] = $sAssessment; + } + if(!empty($value['explanation'])){ + $sField = $key.'_'.'explanation'; + $aInsert[$sField] = empty($value['explanation']) ? '' : htmlspecialchars($value['explanation']); + } + }else{ + $aInsert[$key] = empty($value) ? '' : htmlspecialchars($value); + } + } + if(empty($aInsert)){ + return ['status' => 3,'msg' => 'Data is empty']; + } + + //查询文章审核内容-判断新增或修改 + $aWhere = ['article_id' => $iArticleId,'journal_id' => $iJournalId]; + $aAiReview = Db::table('t_article_ai_review')->field('id')->where($aWhere)->find(); + $iLogId = empty($aAiReview['id']) ? 0 : $aAiReview['id']; + //新增 + if(empty($iLogId)){ + $aInsert['create_time'] = date('Y-m-d H:i:s'); + $aInsert['content'] = $iArticleId; + $iLogId = Db::name('article_ai_review')->insertGetId($aInsert); + if(empty($iLogId)){ + $aResult = ['status' => 4,'msg' => 'Failed to add AI audit content']; + } + $aResult['data'] = ['id' => $iLogId]; + return $aResult; + } + if(!empty($iLogId)){ + $aWhere = ['id' => $iLogId]; + $aInsert['update_time'] = date('Y-m-d H:i:s'); + if(!Db::name('article_ai_review')->where($aWhere)->limit(1)->update($aInsert)){ + $aResult = ['status' => 5,'msg' => 'Failed to add AI audit content']; + } + $aAiReview = Db::table('t_article_ai_review')->where($aWhere)->find(); + $aResult['data'] = $aAiReview; + return $aResult; + } + return ['status' => 6,'msg' => 'illegal request']; + } + + /** + * @title 文章AI审核内容查询 + * @param article_id 文章ID + */ + public function get($aParam = []){ + + //获取参数 + $aParam = empty($aParam) ? $this->request->post() : $aParam; + if(empty($aParam['article_id'])){ + return ['status' => 2,'msg' => 'Please select an article']; + } + //查询文章审核内容 + $aWhere = ['article_id' => $aParam['article_id']]; + $aAiReview = Db::table('t_article_ai_review')->where($aWhere)->find(); + return ['status' => 1,'msg' => 'Successfully obtained article review content','data' => $aAiReview]; + } +} diff --git a/application/common/Article.php b/application/common/Article.php new file mode 100644 index 0000000..9067ce0 --- /dev/null +++ b/application/common/Article.php @@ -0,0 +1,324 @@ + 2,'msg' => 'Please select an article']); + } + + //获取文件内容 + $aWhere = ['article_id' => $iArticleId,'type_name' => 'manuscirpt']; + $aFile = Db::name('article_file')->field('file_url')->where($aWhere)->order('ctime desc')->limit(1)->find(); + if(empty($aFile['file_url'])){ + return json_encode(['status' => 2,'msg' => 'No Manuscript']); + } + + //接口获取上传文件 + $sUrl = $this->sJavaUrl."api/typeset/readDocx"; + $aParam['fileRoute'] = $this->sFileUrl.$aFile['file_url']; + $aResult = object_to_array(json_decode(myPost($sUrl,$aParam))); + return json_encode($aResult); + } + /** + * 获取期刊内容 + */ + public function getJournalPaperArt($aParam = []){ + + //判断文章ID + $sIssn = empty($aParam['issn']) ? [] : $aParam['issn']; + if(empty($sIssn)){ + return json_encode(['status' => 2,'msg' => 'Please select an article']); + } + //接口获取期刊内容 + $sUrl = $this->sTmrUrl."/api/Supplementary/getJournalPaperArt"; + $aParam = ['issn' => $sIssn]; + $aResult = object_to_array(json_decode(myPost($sUrl,$aParam),true)); + return json_encode($aResult); + } + /** + * 更新AI生成内容入库 + * @param $messages 内容 + * @param $model 模型类型 + */ + public function updateAiArticle($aParam = []){ + //文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + //查询内容是否存在 + $aWhere = ['is_delete' => 2]; + if(empty($iArticleId)){ + return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']); + } + + //查询文章是否生成AI内容 + $aWhere= ['is_delete' => 2,'article_id' => $iArticleId]; + $aAiArticle = Db::name('ai_article')->field('ai_article_id')->where($aWhere)->find(); + if(empty($aAiArticle)){ + return json_encode(['status' => 3,'msg' => 'The article content of WeChat official account has not been generated']); + } + $iAiArticleId = $aAiArticle['ai_article_id']; + //必填参数验证 + $aFields = ['article_id','title_english','title_chinese','journal_issn','covered','digest','research_result','content','highlights','discussion','prospect','research_background','discussion_results','research_method','overview','summary','is_generate']; + $sFiled = ''; + $aUpdateParam = []; + foreach($aFields as $val){ + if(!isset($aParam[$val])){ + continue; + } + if(is_array($aParam[$val])){ + $aParam[$val] = implode(";",$aParam[$val]); + } + $aUpdateParam[$val] = empty($aParam[$val]) ? '' : addslashes($aParam[$val]); + } + if(empty($aUpdateParam)){ + return json_encode(['status' => 1,'msg' => 'No data currently being processed']); + } + //执行入库 + $aUpdateParam['update_time'] = time(); + $result = Db::name('ai_article')->where('ai_article_id',$iAiArticleId)->limit(1)->update($aUpdateParam); + if($result === false){ + return json_encode(['status' => 4,'msg' => 'UPDATEING AI article failed']); + } + return json_encode(['status' => 1,'msg' => 'No data currently being processed']); + } + + + /** + * 构建文章领域-处理提示词 + */ + private function buildFieldPrompt($aSearch = []){ + + //必填验证 + if(empty($aSearch)){ + return []; + } + + $sSysMessagePrompt = ' + 你是一位资深的医学期刊学术评审专家,负责严谨、客观地评估学术文章。返回格式必须严格遵循以下JSON结构!请根据文章的标题和摘要从目标期刊下所有领域中筛选出符合文章的领域'; + $sSysMessagePrompt .= json_encode([ + "article_field" => [ + "assessment" => [ + "major_id" => "领域ID多个,分隔", + "major_name" => "领域名称多个,分隔" + ], + "explanation" =>"请详细解释说明.请返回中文解释!" + ] + ],JSON_UNESCAPED_UNICODE); + //组装问题 + $sUserPrompt = '根据文章的标题:{title};摘要:{abstrart}从目标期刊:【{journal_name}】包含的领域【json结构】{journal_major}中筛选出最符合文章领域【小于等于3个】'; + $sUserPrompt = str_replace(array_keys($aSearch), array_values($aSearch), $sUserPrompt); + return [ + ['role' => 'system', 'content' => $sSysMessagePrompt], ['role' => 'user', 'content' => $sUserPrompt] + ]; + } + + /** + * 获取文章领域【AI】 + * @param bool $assoc 是否返回关联数组(默认true) + */ + public function getAiField($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]; + $aArticle = json_decode($this->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' )); + } + + //获取文章领域 + $aArticleField = $this->getArticleField($aWhere); + if(!empty($aArticleField['data'])){ + return json_encode(array('status' => 4,'msg' =>'The article has been added to the field' )); + } + //文章标题 + $title = empty($aArticle['title']) ? '' : $aArticle['title']; + if(empty($title)){ + return json_encode(array('status' => 5,'msg' => 'The title cannot be empty')); + } + //摘要 + $abstrart = empty($aArticle['abstrart']) ? '' : $aArticle['abstrart']; + if(empty($abstrart)){ + return json_encode(array('status' => 5,'msg' => 'The abstract cannot be empty' )); + } + //查询该期刊下的所有领域 + $aWhere = ['journal_id' => $aArticle['journal_id']]; + $aResult = $this->getJouarnalMajor($aWhere); + //期刊领域 + $aMaJor = empty($aResult['data']['major']) ? [] : $aResult['data']['major']; + //期刊信息 + $aJournal = empty($aResult['data']['journal']) ? [] : $aResult['data']['journal']; + if(empty($aJournal) || empty($aMaJor)){ + return json_encode($aResult); + } + + //变量替换 + $aSearch = []; + $title = empty($aArticle['title']) ? '' : '文章标题:'.$aArticle['title']; + $abstrart = empty($aArticle['abstrart']) ? '' : '文章摘要:'.$aArticle['abstrart']; + $aSearch['{title}'] = 'title:'.$title; + $aSearch['{abstrart}'] = 'abstract:'.$abstrart; + $aSearch['{journal_major}'] = empty($aMaJor) ? '' : json_encode($aMaJor);//期刊领域 + $aSearch['{journal_name}'] = empty($aJournal['title']) ? '' : $aJournal['title']; + $aMessage = $this->buildFieldPrompt($aSearch); + //请求OPENAI接口-非重要维度一次请求获取答案 + if(empty($aMessage)){ + return json_encode(['status' => 6,'msg' => 'AI Q&A content not obtained']); + } + + //请求OPENAI + $oOpenAi = new OpenAi; + $aParam = ['messages' => $aMessage,'model' => empty($aParam['api_model']) ? 'gpt-4.1' : $aParam['api_model']]; + $aResult = json_decode($oOpenAi->curlOpenAIStream($aParam),true); + //处理返回信息 + $aData = empty($aResult['data']) ? [] : $aResult['data']; + if(empty($aData)){ + return json_encode(['status' => 7,'msg' => empty($aResult['msg']) ? 'OPENAI returns empty content' : $aResult['msg']]); + } + + //数据处理 + $aData = $oOpenAi->extractAndParse($aData); + if(empty($aData['data'])){ + return json_encode($aData); + } + + //关联文章领域 + $aData = $aData['data']; + $aMaJorId = empty($aData['article_field']['assessment']) ? [] : $aData['article_field']['assessment']; + $sMaJorId = empty($aMaJorId['major_id']) ? '' : $aMaJorId['major_id']; + $aAddResult = $this->addArticleField(['article_field' => $sMaJorId,'article_id' => $iArticleId]); + + //插入文章审核记录表 + $oAireview = new Aireview; + $aData['article_id'] = $iArticleId; + $aData['journal_id'] = $aArticle['journal_id']; + $aResult = $oAireview->addAiReview($aData); + return json_encode($aAddResult); + + } + + /** + * @title 文章内容 + * @param article_id 文章ID + */ + public function get($aParam = []){ + + //获取参数 + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return json_encode(array('status' => 2,'msg' => 'Please select an article' )); + } + //查询文章 + $aWhere = ['article_id' => $iArticleId]; + $aArticle = Db::name('article')->where($aWhere)->find(); + return json_encode(['status' => 1,'msg' => 'success','data' => $aArticle]); + } + + /** + * @title 获取文章期刊领域 + * @param article_id 文章ID + */ + public function getJouarnalMajor($aParam = []){ + + //期刊ID + $iJournalId = empty($aParam['journal_id']) ? 0 : $aParam['journal_id']; + if(empty($iJournalId)){ + return ['status' => 2,'msg' => 'Please select an journal']; + } + //根据期刊ID查询期刊信息 + $aWhere = ['journal_id' => $iJournalId,'state' => 2]; + $aJournal = Db::name('journal')->field('title,issn,journal_id')->where('journal_id', $iJournalId)->find(); + if(empty($aJournal)){ + return ['status' => 3,'msg' => 'This article is not associated with a journal']; + } + $sIssn = empty($aJournal['issn']) ? '' : $aJournal['issn']; + if(empty($sIssn)){ + return ['status' => 4,'msg' => 'The issn of the journal is empty']; + } + //查询期刊领域ID + $aWhere = ['journal_issn' => $sIssn,'mtj_state' => 0]; + $aMaJorId = Db::name('major_to_journal')->where($aWhere)->column('major_id'); + if(empty($aMaJorId)){ + return ['status' => 5,'msg' => 'The field of the journal is empty']; + } + //查询领域名称 + $aWhere = ['major_state' => 0,'major_id' => ['in',$aMaJorId],'pid' => ['>',0]]; + $aMaJor = Db::name('major')->where($aWhere)->column('major_id,major_title'); + return ['status' => 5,'msg' => 'success','data' => ['major' => $aMaJor,'journal' => $aJournal]]; + } + + /** + * 获取文章所属领域 + * @param article_id 文章ID + */ + public function getArticleField($aParam = []){ + + // 文章ID + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return ['status' => 2,'msg' => 'Please select a Article']; + } + //查询审稿人领域信息 + $aParam['state'] = 0; + $aWhere = ['article_id' => $iArticleId,'state' => 0]; + $aArticleField = Db::name('major_to_article')->field('major_id')->where($aWhere)->select(); + return ['status' => 1,'msg' => "success",'data' => $aArticleField]; + } + /** + * 添加文章所属领域 + * @param reviewer_id 审核人ID + * @param + */ + public function addArticleField($aParam = []){ + + $iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id']; + if(empty($iArticleId)){ + return ['status' => 2,'msg' => 'Please select a article']; + } + $sField = empty($aParam['article_field']) ? '' : $aParam['article_field']; + if(empty($sField)){ + return ['status' => 2,'msg' => "Please select the field to which the article belongs"]; + } + $aField = is_array($sField) ? $sField : explode(',', trim($sField,',')); + if(empty($aField)){ + return ['status' => 3,'msg' => "Please select the user's field of expertise"]; + } + + //领域入库 + $aInsertParam = []; + foreach ($aField as $key => $value) { + if(empty($value)){ + continue; + } + $aInsertParam[] = ['major_id' => $value,'article_id' => $iArticleId,'ctime' => time(),'state' => 0]; + } + if(empty($aInsertParam)){ + return ['status' => 4,'msg' => "Please select the user's field of expertise"]; + } + $result = Db::name('major_to_article')->insertAll($aInsertParam); + if($result === false){ + return ['status' => 5,'msg' => "Failed to add article field"]; + } + return ['status' => 1,'msg' => "Successfully added the article field"]; + } + +} diff --git a/application/common/QueueRedis.php b/application/common/QueueRedis.php new file mode 100644 index 0000000..052265b --- /dev/null +++ b/application/common/QueueRedis.php @@ -0,0 +1,212 @@ +config = \think\Config::get('queue'); + $this->connect(); + } + + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new self(); + } + return self::$instance; + } + + private function connect() + { + // 只在首次调用或连接断开时创建连接 + if (!$this->redis) { + $this->redis = new \Redis(); + + // 使用长连接(pconnect)避免频繁创建连接 + $connectMethod = $this->config['persistent'] ? 'pconnect' : 'connect'; + $this->redis->$connectMethod( + $this->config['host'] ?? '127.0.0.1', + $this->config['port'] ?? 6379 + ); + + // 始终执行认证(空密码会被 Redis 忽略) + $this->redis->auth($this->config['password'] ?? ''); + + $this->redis->select($this->config['select'] ?? 0); + } + return $this->redis; + } + + // 使用 SET 命令原子操作设置锁 + public function setRedisLock($key, $value, $expire) + { + try { + return $this->connect()->set($key, $value, ['nx', 'ex' => $expire]); + } catch (\Exception $e) { + return false; + } + } + + // 设置Redis值 + public function setRedisValue($key, $value, $expire = null) + { + try { + $redis = $this->connect(); + if ($expire) { + return $redis->setex($key, $expire, $value); + } else { + return $redis->set($key, $value); + } + } catch (\Exception $e) { + return false; + } + } + + // 获取Redis值 + public function getRedisValue($key) + { + try { + return $this->connect()->get($key); + } catch (\Exception $e) { + return null; + } + } + + // 安全释放锁(仅当值匹配时删除) + public function releaseRedisLock($key, $value) + { + + // 使用Lua脚本确保原子性 + $script = <<connect(); + $result = $redis->eval($script, [$key, $value], 1); + return $result; + } + + // 获取锁剩余时间 + public function getLockTtl($key) + { + try { + return $this->connect()->ttl($key); + } catch (\Exception $e) { + return -1; + } + } + + // 任务开始时的批量操作 + public function startJob($sRedisKey, $sRedisValue, $expire) + { + try { + $redis = $this->connect(); + // 先尝试设置锁,成功后再设置状态 + if ($redis->set($sRedisKey, $sRedisValue, ['nx', 'ex' => $expire])) { + $redis->set($sRedisKey . ':status', 'processing', $expire); + return true; + } + return false; + } catch (\Exception $e) { + Log::error("Redis批量操作失败: {$e->getMessage()}"); + return false; + } + } + + // 任务结束时的批量操作 +public function finishJob($sRedisKey, $status, $expire) +{ + try { + $redis = $this->connect(); + // 使用Lua脚本确保原子性 + $script = <<eval($script, [$sRedisKey, $status, $expire], 1) === 1; + } catch (\Exception $e) { + Log::error("Redis完成任务失败: {$e->getMessage()}"); + return false; + } +} + + // 记录处理进度 + public function recordProcessingStart($key, $totalQuestions) + { + try { + $redis = $this->connect(); + $redis->hMSet($key, [ + 'status' => 'processing', + 'total' => $totalQuestions, + 'completed' => 0, + 'start_time' => time() + ]); + $redis->expire($key, 21600); // 6小时过期 + return true; + } catch (\Exception $e) { + return false; + } + } + + // 更新处理进度 + public function updateProcessingProgress($key, $completed) + { + $redis = $this->connect(); + // 获取总数 + $total = $redis->hGet($key, 'total'); + if (!$total) { + return false; + } + + // 计算进度 + $iProgress = round(($completed / $total) * 100, 2); + // 事务更新多个字段 + $redis->hSet($key, 'completed', $completed); + $redis->hSet($key, 'progress', $iProgress); + if ($iProgress >= 100) { + $redis->hSet($key, 'status', 'completed'); + $redis->hSet($key, 'end_time', time()); + } + return $iProgress; + + } + + // 保存分块进度 + public function saveChunkProgress($key, $chunkIndex, $content) + { + $redis = $this->connect(); + $redis->hSet($key, "chunk_{$chunkIndex}", $content); + // 确保设置过期时间(如果已设置则忽略) + $redis->expire($key, 86400); + return true; + + } + + public function getJobStatus($jobId) + { + try { + return $this->getRedisValue($jobId . ':status'); + } catch (\Exception $e) { + return null; + } + } + + public function getConnectionStatus() + { + try { + return $this->connect()->ping() === '+PONG'; + } catch (\Exception $e) { + return false; + } + } + +} +?> \ No newline at end of file