AI相关调整
This commit is contained in:
@@ -21,30 +21,22 @@ class Aireview
|
||||
if(empty($iJournalId)){
|
||||
return ['status' => 2,'msg' => 'The journal to which the article belongs cannot be empty'];
|
||||
}
|
||||
|
||||
$oArticle = new \app\common\Article;
|
||||
//返回数组
|
||||
$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'];
|
||||
$aFields = ['journal_scope_assessment','journal_scope_explanation','other_journal_assessment','other_journal_issn','other_journal_explanation','attribute_assessment','attribute_explanation','contradiction_assessment','contradiction_explanation','unreasonable_assessment','unreasonable_explanation','ethics_assessment','ethics_explanation','academic_assessment','academic_explanation','conclusion_assessment','conclusion_explanation','fund_number','hotspot','submit_direction','references_past_three','references_num','references_past_five','references_ratio_JCR1','references_ratio_JCR2','registration_assessment','registration_explanation','cite_rate','article_field_assessment','article_field_explanation','is_finish','article_id','journal_id'];
|
||||
$oHelperFunction = new \app\common\HelperFunction;
|
||||
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(!in_array($key, $aFields)){
|
||||
continue;
|
||||
}
|
||||
$value = is_array($value) ? json_encode($value) : $value;
|
||||
$aInsert[$key] = $oHelperFunction->func_safe($value);
|
||||
}
|
||||
if(empty($aInsert)){
|
||||
return ['status' => 3,'msg' => 'Data is empty'];
|
||||
@@ -71,8 +63,7 @@ class Aireview
|
||||
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;
|
||||
$aResult['data'] = $aInsert;
|
||||
return $aResult;
|
||||
}
|
||||
return ['status' => 6,'msg' => 'illegal request'];
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace app\common;
|
||||
use think\Db;
|
||||
use app\common\OpenAi;
|
||||
use app\common\Aireview;
|
||||
use app\common\HelperFunction;
|
||||
class Article
|
||||
{
|
||||
//JAVA接口
|
||||
@@ -10,7 +11,9 @@ class Article
|
||||
//官网文件地址
|
||||
protected $sFileUrl = "https://submission.tmrjournals.com/public/";
|
||||
//Ai地址
|
||||
protected $sAiUrl = "http://125.39.141.154:10002";
|
||||
protected $sAiUrl = "http://chat.taimed.cn/v1/chat/completions";
|
||||
//tmr
|
||||
protected $sTmrUrl = "http://journalapi.tmrjournals.com/public/index.php";//"http://zmzm.journal.dev.com/"; //
|
||||
/**
|
||||
* 获取文章文件内容
|
||||
*/
|
||||
@@ -48,7 +51,8 @@ class Article
|
||||
//接口获取期刊内容
|
||||
$sUrl = $this->sTmrUrl."/api/Supplementary/getJournalPaperArt";
|
||||
$aParam = ['issn' => $sIssn];
|
||||
$aResult = object_to_array(json_decode(myPost($sUrl,$aParam),true));
|
||||
|
||||
$aResult = object_to_array(json_decode(myPost1($sUrl,$aParam),true));
|
||||
return json_encode($aResult);
|
||||
}
|
||||
/**
|
||||
@@ -83,6 +87,8 @@ class Article
|
||||
$aFields = ['article_id','article_type','media_type','journal_id','journal_issn','title_english','title_chinese','covered','research_method','digest','research_background','overview','summary','conclusion','is_generate'];
|
||||
$sFiled = '';
|
||||
$aUpdateParam = [];
|
||||
|
||||
$oHelperFunction = new HelperFunction;
|
||||
foreach($aFields as $val){
|
||||
if(!isset($aParam[$val])){
|
||||
continue;
|
||||
@@ -90,7 +96,7 @@ class Article
|
||||
if(is_array($aParam[$val])){
|
||||
$aParam[$val] = implode(";",$aParam[$val]);
|
||||
}
|
||||
$aUpdateParam[$val] = empty($aParam[$val]) ? '' : $this->func_safe($aParam[$val]);
|
||||
$aUpdateParam[$val] = empty($aParam[$val]) ? '' : $oHelperFunction->func_safe($aParam[$val]);
|
||||
}
|
||||
if(empty($aUpdateParam)){
|
||||
return json_encode(['status' => 1,'msg' => 'No data currently being processed']);
|
||||
@@ -119,11 +125,11 @@ class Article
|
||||
你是一位资深的医学期刊学术评审专家,负责严谨、客观地评估学术文章。返回格式必须严格遵循以下JSON结构!请根据文章的标题和摘要从目标期刊下所有领域中筛选出符合文章的领域';
|
||||
$sSysMessagePrompt .= json_encode([
|
||||
"article_field" => [
|
||||
"assessment" => [
|
||||
"article_field_assessment" => [
|
||||
"major_id" => "领域ID多个,分隔",
|
||||
"major_name" => "领域名称多个,分隔"
|
||||
],
|
||||
"explanation" =>"请详细解释说明.请返回中文解释!"
|
||||
"article_field_explanation" =>"请详细解释说明.请返回中文解释!"
|
||||
]
|
||||
],JSON_UNESCAPED_UNICODE);
|
||||
//组装问题
|
||||
@@ -155,9 +161,9 @@ class Article
|
||||
|
||||
//获取文章领域
|
||||
$aArticleField = $this->getArticleField($aWhere);
|
||||
if(!empty($aArticleField['data'])){
|
||||
return json_encode(array('status' => 4,'msg' =>'The article has been added to the field' ));
|
||||
}
|
||||
// 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)){
|
||||
@@ -204,14 +210,15 @@ class Article
|
||||
}
|
||||
|
||||
//数据处理
|
||||
$aData = $oOpenAi->extractAndParse($aData);
|
||||
$oHelperFunction = new HelperFunction;
|
||||
$aData = $oHelperFunction->extractAndParse($aData);
|
||||
if(empty($aData['data'])){
|
||||
return json_encode($aData);
|
||||
}
|
||||
|
||||
//关联文章领域
|
||||
$aData = $aData['data'];
|
||||
$aMaJorId = empty($aData['article_field']['assessment']) ? [] : $aData['article_field']['assessment'];
|
||||
$aData = empty($aData['data']['article_field']) ? [] : $aData['data']['article_field'];
|
||||
$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]);
|
||||
|
||||
@@ -360,6 +367,7 @@ class Article
|
||||
$aFields = ['article_id','title','content','current_am_id','next_am_id','ami_id'];
|
||||
$sFiled = '';
|
||||
$aUpdateParam = [];
|
||||
$oHelperFunction = new HelperFunction;
|
||||
foreach($aFields as $val){
|
||||
if(!isset($aParam[$val])){
|
||||
continue;
|
||||
@@ -367,7 +375,7 @@ class Article
|
||||
if(is_array($aParam[$val])){
|
||||
$aParam[$val] = implode(";",$aParam[$val]);
|
||||
}
|
||||
$aUpdateParam[$val] = empty($aParam[$val]) ? '' : $this->func_safe($aParam[$val]);
|
||||
$aUpdateParam[$val] = empty($aParam[$val]) ? '' : $oHelperFunction->func_safe($aParam[$val]);
|
||||
}
|
||||
if(empty($aUpdateParam)){
|
||||
return json_encode(['status' => 1,'msg' => 'No data currently being processed']);
|
||||
@@ -384,26 +392,5 @@ class Article
|
||||
}
|
||||
return json_encode(['status' => 1,'msg' => 'success']);
|
||||
}
|
||||
/**
|
||||
* 字符串过滤
|
||||
* @param $messages 内容
|
||||
* @param $model 模型类型
|
||||
*/
|
||||
private function func_safe($data,$ignore_magic_quotes=false){
|
||||
if(is_string($data)){
|
||||
$data=trim(htmlspecialchars($data));//防止被挂马,跨站攻击
|
||||
if(($ignore_magic_quotes==true)||(!get_magic_quotes_gpc())){
|
||||
$data = addslashes($data);//防止sql注入
|
||||
}
|
||||
return $data;
|
||||
}else if(is_array($data)){//如果是数组采用递归过滤
|
||||
foreach($data as $key=>$value){
|
||||
$data[$key]=func_safe($value);
|
||||
}
|
||||
return $data;
|
||||
}else{
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,22 +4,73 @@ namespace app\common;
|
||||
use think\Db;
|
||||
use think\Cache;
|
||||
use app\common\QueueRedis;
|
||||
use app\common\traits\QueueDbHATrait;
|
||||
class QueueJob
|
||||
{
|
||||
// 必填参数
|
||||
protected $aField = ['job_id', 'job_class', 'status', 'create_time', 'update_time', 'error', 'params'];
|
||||
private $logPath;
|
||||
private $QueueRedis;
|
||||
private $maxRetries = 2;
|
||||
private $maxRetries = 3;//最大重试次数
|
||||
const JSON_OPTIONS = JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR;
|
||||
// 引入高可用数据库管理 trait
|
||||
use QueueDbHATrait;
|
||||
private $warningThreshold = 120; // 进程超时预警阈值(秒)
|
||||
// 进程最大运行时间(秒)
|
||||
protected $maxRunTime = 21600; // 6个小时,短于数据库wait_timeout
|
||||
private $lockExpire = 180; // 锁过期时间(3分钟,根据任务实际耗时调整)
|
||||
private $maxDelay = 300; // 最大重试延迟(5分钟)
|
||||
// 静态变量:记录进程启动时间(跨任务共享,每个进程仅初始化一次)
|
||||
protected static $processStartTime = null;
|
||||
private $startTime;
|
||||
public function __construct()
|
||||
{
|
||||
$this->QueueRedis = QueueRedis::getInstance();
|
||||
// 初始化进程启动时间(仅在进程首次启动时执行)
|
||||
if (is_null(self::$processStartTime)) {
|
||||
self::$processStartTime = time();
|
||||
// 增加进程ID标识
|
||||
$pid = getmypid();
|
||||
$this->log("队列进程启动 [PID:{$pid}],启动时间:" . date('Y-m-d H:i:s', self::$processStartTime));
|
||||
}
|
||||
//任务开始时间
|
||||
$this->startTime = microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务初始化验证
|
||||
* @return bool
|
||||
*/
|
||||
public function init($job){
|
||||
// 检查进程是否已超时,提前退出
|
||||
if ($this->isProcessTimeout(self::$processStartTime)) {
|
||||
$this->log("进程已超时,放弃处理任务");
|
||||
$job->release(15); // 短延迟后重新入队
|
||||
return;
|
||||
}
|
||||
|
||||
// 进程超时预警
|
||||
$this->checkProcessTimeoutWarning(self::$processStartTime);
|
||||
|
||||
// 检查Redis连接状态
|
||||
if (!$this->QueueRedis->getConnectionStatus()) {
|
||||
$this->log("Redis连接失败,10秒后重试");
|
||||
$job->release(15);
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 任务结束
|
||||
* @return bool
|
||||
*/
|
||||
public function finnal(){
|
||||
$executionTime = microtime(true) - $this->startTime;
|
||||
$this->log("任务执行完成,耗时: " . number_format($executionTime, 4) . "秒");
|
||||
gc_collect_cycles();
|
||||
|
||||
// 任务完成后,检查进程是否超时
|
||||
if ($this->isProcessTimeout(self::$processStartTime)) {
|
||||
$this->log("进程已运行超过{$this->maxRunTime}秒,任务完成后自动退出以刷新连接");
|
||||
exit(1); // 退出进程,触发supervisor重启
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 写入日志到缓冲区
|
||||
* @param string $message
|
||||
@@ -43,13 +94,11 @@ class QueueJob
|
||||
'OpenAI' => 45,
|
||||
'network' => 60
|
||||
];
|
||||
|
||||
foreach ($delayMap as $keyword => $delay) {
|
||||
if (stripos($errorMsg, $keyword) !== false) { // 不区分大小写匹配
|
||||
return $delay;
|
||||
}
|
||||
}
|
||||
|
||||
return 10;
|
||||
}
|
||||
|
||||
@@ -60,25 +109,32 @@ class QueueJob
|
||||
* @param string $sRedisValue
|
||||
* @param \think\queue\Job $job
|
||||
*/
|
||||
public function handleRetryableException($e,$sRedisKey,$sRedisValue,$job)
|
||||
public function handleRetryableException($e, $sRedisKey, $sRedisValue, $job)
|
||||
{
|
||||
$sMsg = empty($e->getMessage()) ? '可重试异常' : $e->getMessage();
|
||||
$sTrace = empty($e->getTraceAsString()) ? '' : $e->getTraceAsString();
|
||||
$this->log("可重试异常: {$sMsg} | 堆栈: {$sTrace}");
|
||||
|
||||
$this->QueueRedis->finishJob($sRedisKey, 'failed', 3600,$sRedisValue);
|
||||
if ($this->isFatalDatabaseError($e)) {
|
||||
$this->handleDatabaseErrorAndRestartWithRetry($e, $sRedisKey, $sRedisValue, $job);
|
||||
return;
|
||||
}
|
||||
|
||||
// 原子化更新任务状态
|
||||
$this->QueueRedis->atomicJobUpdate($sRedisKey, 'failed', 3600, $sRedisValue);
|
||||
|
||||
$attempts = $job->attempts();
|
||||
|
||||
// 双重限制:次数
|
||||
if ($attempts >= $this->maxRetries) {
|
||||
$this->log("超过最大重试次数({$this->maxRetries}),停止重试 | 执行日志:{$sMsg}");
|
||||
$job->delete();
|
||||
} else {
|
||||
$delay = $this->getRetryDelay($sMsg);
|
||||
$this->log("{$delay}秒后重试({$attempts}/{$this->maxRetries}) | 执行日志:{$sMsg}");
|
||||
$delay = $this->getRetryDelay($sMsg, $attempts); // 动态延迟
|
||||
$this->log("{$delay}秒后重试({$attempts}/{$this->maxRetries})");
|
||||
$job->release($delay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理不可重试异常
|
||||
* @param \Exception $e
|
||||
@@ -96,15 +152,118 @@ class QueueJob
|
||||
$job->delete();
|
||||
}
|
||||
|
||||
// 判断是否为需要重启的致命数据库错误
|
||||
private function isFatalDatabaseError(\Exception $e)
|
||||
{
|
||||
// 1. 检查是否为PDO异常
|
||||
if ($e instanceof \PDOException) {
|
||||
$fatalCodes = [2006, 2013, 2002]; // 2006=连接断开,2013=连接丢失,2002=无法连接
|
||||
$errorCode = (int)$e->getCode();
|
||||
if (in_array($errorCode, $fatalCodes)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 2. 检查错误消息关键词(覆盖非PDOException的数据库错误)
|
||||
$errorMsg = strtolower($e->getMessage());
|
||||
$fatalKeywords = [
|
||||
'mysql server has gone away',
|
||||
'lost connection to mysql server',
|
||||
'error while sending stmt_prepare packet',
|
||||
'database connection failed',
|
||||
'sqlstate[hy000]' // 通用数据库错误前缀
|
||||
];
|
||||
foreach ($fatalKeywords as $keyword) {
|
||||
if (strpos($errorMsg, $keyword) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//处理数据库错误,释放任务后重启(保留任务)
|
||||
private function handleDatabaseErrorAndRestartWithRetry($e, $sRedisKey, $sRedisValue, $job)
|
||||
{
|
||||
$this->log("检测到致命数据库错误,释放任务后重启队列 | 错误: {$e->getMessage()}");
|
||||
$attempts = $job->attempts();
|
||||
if ($attempts >= $this->maxRetries) {
|
||||
$this->log("数据库错误重试达上限,标记任务失败");
|
||||
$this->QueueRedis->finishJob($sRedisKey, 'failed', 3600, $sRedisValue);
|
||||
$job->delete();
|
||||
exit(1);
|
||||
}
|
||||
// 1. 释放Redis锁(必须先释放,否则新进程无法获取锁)
|
||||
if (!empty($sRedisKey) && !empty($sRedisValue)) {
|
||||
$this->QueueRedis->forceReleaseLock($sRedisKey, $sRedisValue);
|
||||
}
|
||||
// 2. 释放任务回队列(设置短延迟,避免重启前被其他进程处理)
|
||||
$job->release(60); // 60秒后重新入队,给进程重启留时间
|
||||
$this->log("任务已释放回队列,等待新进程处理");
|
||||
// 3. 强制退出进程,触发Supervisor重启
|
||||
$this->log("数据库错误,重启进程以刷新连接,新进程将处理释放的任务");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库连接检查与重建(高可用版)
|
||||
* 解决 MySQL server has gone away 等连接超时问题
|
||||
* @param bool $force 是否强制检查(忽略缓存时间)
|
||||
* @return bool 连接是否有效
|
||||
* 获取分布式锁
|
||||
* @param string $sRedisKey
|
||||
* @param string $sRedisValue
|
||||
* @param Job $job
|
||||
* @return bool
|
||||
*/
|
||||
public function checkDbConnection($force = false)
|
||||
public function acquireLock($sRedisKey, $sRedisValue, $job)
|
||||
{
|
||||
return $this->checkDbConnectionTrait();
|
||||
$isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $this->lockExpire);
|
||||
|
||||
if (!$isLocked) {
|
||||
$currentLockValue = $this->QueueRedis->getRedisValue($sRedisKey); // 获取当前锁值
|
||||
$jobStatus = $this->QueueRedis->getJobStatus($sRedisKey);
|
||||
|
||||
// 若锁值为空或过期,强制抢占锁
|
||||
if (empty($currentLockValue) || $jobStatus === false) {
|
||||
$this->log("数据为空 | 状态: {$currentLockValue} | 键: {$jobStatus}");
|
||||
$isLocked = $this->QueueRedis->startJob($sRedisKey, $sRedisValue, $this->lockExpire);
|
||||
if ($isLocked) return true;
|
||||
}
|
||||
if (in_array($jobStatus, ['completed', 'failed'])) {
|
||||
$this->log("任务已完成或失败,删除任务 | 状态: {$jobStatus} | 键: {$sRedisKey}");
|
||||
$job->delete();
|
||||
} else {
|
||||
$attempts = $job->attempts();
|
||||
if ($attempts >= $this->maxRetries) {
|
||||
$this->log("超过最大重试次数({$this->maxRetries}),停止重试 | 键: {$sRedisKey}");
|
||||
$job->delete();
|
||||
} else {
|
||||
$lockTtl = $this->QueueRedis->getLockTtl($sRedisKey);
|
||||
$delay = $lockTtl > 0 ? $lockTtl + 5 : 30;
|
||||
// 限制最大延迟时间
|
||||
$delay = min($delay, $this->maxDelay);
|
||||
$this->log("锁竞争,{$delay}秒后重试({$attempts}/{$this->maxRetries}) | 键: {$sRedisKey}");
|
||||
$job->release($delay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
$this->log("写入成功 | 状态: {$sRedisKey} | 键: {$sRedisValue}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查进程是否超时
|
||||
* @return bool
|
||||
*/
|
||||
public function isProcessTimeout($processStartTime)
|
||||
{
|
||||
return time() - $processStartTime > $this->maxRunTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查进程超时预警
|
||||
*/
|
||||
public function checkProcessTimeoutWarning($processStartTime)
|
||||
{
|
||||
$remainingTime = $this->maxRunTime - (time() - $processStartTime);
|
||||
if ($remainingTime > 0 && $remainingTime < $this->warningThreshold) {
|
||||
$this->log("进程即将超时,剩余时间:{$remainingTime}秒");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user