公共方法

This commit is contained in:
chengxl
2025-07-22 16:40:27 +08:00
parent 41063dfd06
commit fb7db7aaa1
5 changed files with 373 additions and 476 deletions

View File

@@ -3,6 +3,8 @@ namespace app\common;
use think\Cache;
use think\Db;
use think\Queue;
use app\common\Article;
use app\common\QueueRedis;
class OpenAi
{
protected $sApiKey = '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';
@@ -65,20 +67,21 @@ class OpenAi
];
//定义redis连接
private $redis;
private $oQueueRedis;
public function __construct()
{
// 初始化 Redis 连接
$config = \think\Config::get('queue');
$this->redis = new \Redis();
$this->redis->connect($config['host'], $config['port']);
if (!empty($config['password'])) {
$this->redis->auth($config['password']);
}
$this->redis->select($config['select']);
// 初始化 Redis 连接
// $this->redis = Cache::store('redis')->handler();
$this->redis->select($config['select']);
$this->oQueueRedis = QueueRedis::getInstance();
}
/**
@@ -341,205 +344,6 @@ class OpenAi
curl_close($this->curl);
return json_encode(['status' => 1,'msg' => 'success','data' => $aContent]);
}
/**
* 对接OPENAI接口-并行CURL请求【重要维度单独询问】
*/
public function curlMultiOpenAIImportant($aSearch = [],$timeout = 120, $iChunkSize = 2) {
// 入参校验
if (empty($aSearch)) {
return json_encode(['status' => 2, 'msg' => 'Parameter is empty']);
}
//提问问题类型
$sKey = empty($aSearch['question']) ? '' : $aSearch['question'];
if (empty($sKey)) {
return json_encode(['status' => 2, 'msg' => 'Please select the type of question']);
}
//获取问题
$aQuestion = $this->$sKey;
if (empty($aQuestion)) {
return json_encode(['status' => 2, 'msg' => 'question is empty']);
}
//分批处理(核心优化:控制并发量)
$aChunk = array_chunk($aQuestion, $iChunkSize); // 按批次拆分每批最多5个请求
//定义空数组用于接收数据
$aEmptyData = $aLog = $aReturnData = [];
//分批次处理开始
foreach ($aChunk as $iChunkKey => $item) {
// 初始化多curl句柄
$oCurlMulti = curl_multi_init();
$aCurl = [];
// 批量初始化请求
foreach ($item as $key => $value) {
// 跳过无效参数
if (empty($value)) {
$aLog[] = [
'content' => $iChunkKey.'-'.$key.':Invalid parameter'
];
continue;
}
//问题处理-变量替换
$aQuestionInfo = $this->buildReviewPromptImportant($aSearch,$value);
if(empty($aQuestionInfo)){
$aLog[] = [
'content' => $iChunkKey.'-'.$key.':The problem is empty:'.json_encode($value)
];
continue;
}
// 核心配置优化
$oCurl = curl_init();
curl_setopt_array($oCurl, [
CURLOPT_URL => $this->sUrl,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->sApiKey,
'Expect:',
],
CURLOPT_PROXY => $this->proxy,
// SSL验证优化若代理证书不可信临时关闭生产环境需配置信任证书
CURLOPT_SSL_VERIFYPEER => true, // 调试时设为false生产环境设为true
CURLOPT_SSL_VERIFYHOST => 2, // 调试时设为0生产环境设为2
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($aQuestionInfo),
CURLOPT_RETURNTRANSFER => true,
// 超时优化:延长响应超时,新增连接超时
CURLOPT_TIMEOUT => $timeout, // 总超时建议60-120
CURLOPT_CONNECTTIMEOUT => 20, // 连接超时(秒),避免无限等待
CURLOPT_LOW_SPEED_LIMIT => 1024, // 最低速度(字节/秒),低于此值触发超时
CURLOPT_LOW_SPEED_TIME => 30, // 持续低速时间(秒),超过则终止
]);
curl_multi_add_handle($oCurlMulti, $oCurl);
$aCurl[$key] = $oCurl;
}
// 空请求处理
if (empty($aCurl)) {
curl_multi_close($oCurlMulti);
continue;
}
// 核心优化修复curl_multi循环逻辑确保所有请求完成
$active = null;
$mrc = CURLM_OK;
// 第一阶段:处理瞬时可完成的请求
do {
$mrc = curl_multi_exec($oCurlMulti, $active);
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
// 第二阶段:等待所有活跃请求完成(关键优化)
while ($active > 0 && $mrc == CURLM_OK) {
// 等待事件超时1秒避免CPU空转
if (curl_multi_select($oCurlMulti, 1.0) != -1) {
// 处理就绪的请求
do {
$mrc = curl_multi_exec($oCurlMulti, $active);
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
} else {
// 无事件时,检查是否超时(防止无限阻塞)
$timedOut = false;
foreach ($aCurl as $oCurl) {
$startTime = curl_getinfo($oCurl, CURLINFO_STARTTRANSFER_TIME);
if ($startTime > 0 && (microtime(true) - $startTime) > $timeout) {
$timedOut = true;
break;
}
}
if ($timedOut) break; // 超时则强制退出
}
}
// 处理当前批次结果
foreach ($aCurl as $key => $oCurl) {
// 1. 捕获curl错误连接失败、超时等
$sError = curl_error($oCurl);
if (!empty($sError)) {
$aLog[] = [
'content' => "Curl error: {$sError}"
];
$aEmptyData[] = $key;
curl_multi_remove_handle($oCurlMulti, $oCurl);
curl_close($oCurl);
continue;
}
// 2. 获取HTTP状态码关键优化处理OpenAI的API错误
$httpCode = curl_getinfo($oCurl, CURLINFO_HTTP_CODE);
$sContent = curl_multi_getcontent($oCurl);
// 3. 处理非200状态码如限流、服务不可用
if ($httpCode != 200) {
$errorMsg = "HTTP {$httpCode}: " . (empty($sContent) ? 'No response' : $sContent);
// 记录关键错误日志(便于调试)
$aLog[] = [
'http_code' => $httpCode,
'content' => $errorMsg,
];
$aEmptyData[] = $key;
curl_multi_remove_handle($oCurlMulti, $oCurl);
curl_close($oCurl);
continue;
}
// 4. 解析响应内容(原逻辑优化)
$aResult = json_decode($sContent, true);
if (json_last_error() != JSON_ERROR_NONE) {
$aLog[] = [
'content' => "Invalid JSON: {$sContent}",
];
$aEmptyData[] = $key;
curl_multi_remove_handle($oCurlMulti, $oCurl);
curl_close($oCurl);
continue;
}
// 5. 提取OpenAI的content简化判断逻辑
$aOpenAiContent = empty($aResult['choices'][0]['message']['content']) ? '' : $aResult['choices'][0]['message']['content'];
if (empty($aOpenAiContent)) {
$aLog[] = [
'content' => "OPENAI returns empty content",
];
$aEmptyData[] = $key;
curl_multi_remove_handle($oCurlMulti, $oCurl);
curl_close($oCurl);
continue;
}
// 6. 处理业务解析原extractAndParse逻辑
$aData = $this->extractAndParse($aOpenAiContent);
$aContent = empty($aData['data']) ? [] : $aData['data'];
$sMsg = empty($aData['msg']) ? 'Success' : $aData['msg'];
if (empty($aContent)) {
$aEmptyData[] = $key;
}
$aLog[] = [
'content' => $sMsg,
];
$aReturnData += $aContent;
// 释放资源
curl_multi_remove_handle($oCurlMulti, $oCurl);
curl_close($oCurl);
}
// 关闭当前批次的multi句柄
curl_multi_close($oCurlMulti);
// 批次间隔核心优化避免触发OpenAI限流
if ($iChunkKey < count($aChunk) - 1) {
usleep(1000000); // 批次间间隔1秒根据OpenAI配额调整
}
}
$aParam = [
'status' => 1,
'msg' => 'success',
'data' => empty($aReturnData) ? [] : $aReturnData,
'empty_data' => empty($aEmptyData) ? [] : $aEmptyData,
'log_data' => empty($aLog) ? [] : $aLog,
'open_ai_id' => empty($aSearch['open_ai_id']) ? 0 : $aSearch['open_ai_id']
];
//日志记录
$this->addLog($aParam);
return json_encode($aParam);
}
/**
* CURL 发送请求到 OpenAI【流式】
* @param $messages 内容
@@ -557,7 +361,7 @@ class OpenAi
//超时设置
$timeout = empty($aParam['timeout']) ? 300 : $aParam['timeout'];
//接口地址
$sUrl = $this->sUrl;
$sUrl = empty($aParam['url']) ? $this->sUrl : $aParam['url'];
//组装数据
$data = [
@@ -624,8 +428,7 @@ class OpenAi
/**
* 解析流式响应
*/
private function parseMedicalStreamResponse($streamContent)
{
private function parseMedicalStreamResponse($streamContent){
$fullContent = '';
$lines = explode("\n", $streamContent);
foreach ($lines as $line) {
@@ -639,57 +442,10 @@ class OpenAi
return $fullContent;
}
/**
* 记录处理进度【Redis】
*/
private function recordProcessingStart($key,$totalQuestions)
{
$this->redis->hMSet($key, [
'status' => 'processing',
'total' => $totalQuestions,
'completed' => 0,
'start_time' => time()
]);
$this->redis->expire($key, 86400); // 24小时过期
}
/**
* 更新处理进度【Redis】
*/
private function updateProcessingProgress($key,$iId,$completed)
{
$this->redis->hSet($key, 'completed', $completed);
//完成进度
$iProgress = round(($completed / $this->redis->hGet($key, 'total')) * 100, 2);
if($iProgress == 100){
$this->recordProcessingComplete($key,$iId);
}
$this->redis->hSet($key, 'progress', $iProgress);
}
/**
* 记录处理完成【Redis】
*/
private function recordProcessingComplete($key,$iId)
{
$this->redis->hSet($key, 'status', 'completed');
$this->redis->hSet($key, 'end_time', time());
$this->wechatGegnerate(['article_id' => $iId]);
}
/**
* 保存分块进度【Redis】
*/
private function saveChunkProgress($key, $chunkIndex, $content)
{
$this->redis->hset($key, "chunk_{$chunkIndex}", $content);
$this->redis->expire($key, 86400); // 进度保存24小时
}
/**
* 微信公众号-生成公微内容(CURL)
*/
public function createWechatContent($aParam = [])
{
public function createWechatContent($aParam = []){
//主键ID
$iId = empty($aParam['redis_id']) ? 0 : $aParam['redis_id'];
if(empty($iId)){
@@ -703,7 +459,7 @@ class OpenAi
//记录处理开始
$iNum = count($aMessage);
$sRedisKey = 'ai_create_article_'.$iId;
$this->recordProcessingStart($sRedisKey,$iNum);
$this->oQueueRedis->recordProcessingStart($sRedisKey,$iNum);
//定义空数组
$aChunkResult = $aFail = [];
foreach ($aMessage as $key => $value) {
@@ -733,72 +489,64 @@ class OpenAi
$iMaxNum = empty($aParam['count_num']) ? 0 : $aParam['count_num'];
//请求OPENAI
$aResult = $this->curlOpenAIStream($aParam);
//更新处理进度
$iIndex = empty($aParam['chunkIndex']) ? 0 : $aParam['chunkIndex'];
$sRedisKey = 'ai_create_article_'.$iId;
$this->updateProcessingProgress($sRedisKey,$iId,$iIndex + 1);
$iProgress = $this->oQueueRedis->updateProcessingProgress($sRedisKey,$iIndex + 1);
//保存内容
$sRedisKey = 'ai_create_article_progress_'.$iId;
$this->saveChunkProgress($sRedisKey, $iIndex,$aResult);
$this->oQueueRedis->saveChunkProgress($sRedisKey, $iIndex,$aResult);
//更新入库
$aReturnData = json_decode($aResult,true);
$aDataInfo =empty($aReturnData['data']) ? [] : $aReturnData['data'];
$aData = empty($aDataInfo) ? [] : $this->extractAndParse($aDataInfo);
$aData = empty($aData['data']) ? [] : $aData['data'];
if(!empty($aData)){
if(!empty($aData)){//更新AI审稿记录表
if($iProgress >= 100){
$aData['is_generate'] = 1;
}
$aData['article_id'] = $iId;
$this->updateAiArticle($aData);
$this->updateAiContent($aData);
}
return $aResult;
}
/**
* 获取期刊内容
* 微信公众号-更新AI生成内容
*/
public function getJournalPaperArt($aParam = []){
private function updateAiContent($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);
}
/**
* 获取文章文件内容
*/
public function getFileContent($aParam = []){
//判断文章ID
$iArticleId = empty($aParam['article_id']) ? [] : $aParam['article_id'];
//文章ID
$iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id'];
if(empty($iArticleId)){
return json_encode(['status' => 2,'msg' => 'Please select an article']);
return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']);
}
//更新生成状态
$oArticle = new Article;
$aResult = json_decode($oArticle->updateAiArticle($aParam),true);
$iStatus = empty($aResult['status']) ? 0 : $aResult['status'];
$sMsg = empty($aResult['msg']) ? '更新状态失败' : $aResult['msg'];
//是否生成
$is_generate = empty($aParam['is_generate']) ? 2 : $aParam['is_generate'];
//获取文件内容
$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']);
//内容生成完成推送上传素材队列
if($is_generate == 1){
if($iStatus == 1){
//四小时后推送上传素材并推送草稿箱
$iDelaySeconds = 4 * 3600; // 4小时的秒数
Queue::later($iDelaySeconds,'app\api\job\WechatMaterial@fire', ['article_id' => $iArticleId], 'WechatMaterial');
$sMsg = '文章AI内容生成成功';
}else{
$iStatus = 2;
}
//插入日志记录
$oMaterial = new Material;
$aLogInfo = ['article_id' => $iArticleId,'type' => 5,'msg' =>$sMsg,'status' => $iStatus,'create_time' => time()];
$result = $oMaterial->addWechatLog($aLogInfo);
}
//接口获取上传文件
$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);
}
/**
* 添加接口访问日志
*/
@@ -819,87 +567,13 @@ class OpenAi
return DB::name('openapi_log')->insertGetId($aInsert);
}
/**
* 更新AI生成内容入库
* @param $messages 内容
* @param $model 模型类型
*/
private 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']);
}
$aWhere['article_id'] = $iArticleId;
$aAiArticle = Db::name('ai_article')->field('ai_article_id')->where($aWhere)->find();
if(empty($aAiArticle)){
return json_encode(['status' => 3,'msg' => 'he 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 wechatGegnerate($aParam = []){
//文章ID
$iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id'];
if(empty($iArticleId)){
return json_encode(['status' => 2,'msg' => 'Please select the article to be modified']);
}
//更新生成状态
$aParam['is_generate'] = 1;
$aResult = json_decode($this->updateAiArticle($aParam),true);
$iStatus = empty($aResult['status']) ? 0 : $aResult['status'];
$sMsg = empty($aResult['msg']) ? '更新状态失败' : $aResult['msg'];
if($iStatus == 1){
//四小时后推送上传素材并推送草稿箱
$iDelaySeconds = 4 * 3600; // 4小时的秒数
Queue::later($iDelaySeconds,'app\api\job\WechatMaterial@fire', ['article_id' => $iArticleId], 'WechatMaterial');
$sMsg = '文章AI内容生成成功';
}else{
$iStatus = 2;
}
//插入日志记录
$oMaterial = new Material;
$aLogInfo = ['article_id' => $iArticleId,'type' => 5,'msg' =>$sMsg,'status' => $iStatus,'create_time' => time()];
$result = json_decode($oMaterial->addWechatLog($aLogInfo),true);
return json_encode($aResult);
}
/**
* 从文本中提取被```json```和```包裹的JSON内容并解析
* @param string $text 包含JSON代码块的文本
* @param bool $assoc 是否返回关联数组默认true
* @return array|object 解析后的JSON数据失败时返回null
*/
private function extractAndParse($text, $assoc = true){
public function extractAndParse($text, $assoc = true){
// 使用正则表达式提取JSON代码块
preg_match('/```json\s*(\{.*?\})\s*```/s', $text, $matches);