公共方法
This commit is contained in:
@@ -1,129 +1,340 @@
|
||||
<?php
|
||||
namespace app\common;
|
||||
|
||||
use think\Db;
|
||||
use think\Cache;
|
||||
use app\common\QueueRedis;
|
||||
|
||||
class QueueJob
|
||||
{
|
||||
// 必填参数
|
||||
protected $aField = ['job_id', 'job_class', 'status', 'create_time', 'update_time', 'error', 'params'];
|
||||
private $logPath;
|
||||
private $QueueRedis;
|
||||
private $maxRetries = 2;
|
||||
private $logBuffer = [];
|
||||
private $lastLogTime = 0;
|
||||
private $logMaxSize = 1048576; // 1MB (1*1024*1024)
|
||||
const JSON_OPTIONS = JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR;
|
||||
|
||||
//必填参数
|
||||
protected $aField = ['job_id','job_class','status','create_time','update_time','error','params'];
|
||||
|
||||
//定义redis连接
|
||||
private $redis;
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
$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->QueueRedis = QueueRedis::getInstance();
|
||||
$this->lastLogTime = time();
|
||||
}
|
||||
|
||||
// 记录任务开始
|
||||
public function addLog($aParam = []) {
|
||||
|
||||
//数据处理
|
||||
$aField = $this->aField;
|
||||
/**
|
||||
* 记录任务开始
|
||||
* @param array $aParam
|
||||
* @return int 日志ID,失败返回0
|
||||
*/
|
||||
public function addLog($aParam = [])
|
||||
{
|
||||
// 数据过滤(只保留必填字段)
|
||||
$aInsert = [];
|
||||
foreach ($aField as $key => $value) {
|
||||
if(isset($aParam[$value])){
|
||||
$aInsert[$value] = $aParam[$value];
|
||||
}
|
||||
foreach ($this->aField as $field) {
|
||||
if (isset($aParam[$field])) {
|
||||
$aInsert[$field] = $aParam[$field];
|
||||
}
|
||||
}
|
||||
$result = 0;
|
||||
if(!empty($aInsert)){
|
||||
$result = DB::name('wechat_queue_logs')->insertGetId($aParam);
|
||||
|
||||
// 补充默认值
|
||||
if (!isset($aInsert['create_time'])) {
|
||||
$aInsert['create_time'] = time();
|
||||
}
|
||||
if (!isset($aInsert['update_time'])) {
|
||||
$aInsert['update_time'] = $aInsert['create_time'];
|
||||
}
|
||||
|
||||
try {
|
||||
return Db::name('wechat_queue_logs')->insertGetId($aInsert);
|
||||
} catch (\Exception $e) {
|
||||
$this->log("添加任务日志失败: " . $e->getMessage() . " | 参数: " . json_encode($aInsert, self::JSON_OPTIONS));
|
||||
return 0;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 记录任务成功
|
||||
public function updateLog($aParam = []) {
|
||||
|
||||
/**
|
||||
* 记录任务状态更新
|
||||
* @param array $aParam
|
||||
* @return bool
|
||||
*/
|
||||
public function updateLog($aParam = [])
|
||||
{
|
||||
$iLogId = empty($aParam['log_id']) ? 0 : $aParam['log_id'];
|
||||
if(empty($iLogId)){
|
||||
if (empty($iLogId)) {
|
||||
$this->log("更新日志失败: 缺少log_id");
|
||||
return false;
|
||||
}
|
||||
//数据处理
|
||||
$aField = $this->aField;
|
||||
|
||||
// 数据过滤
|
||||
$aUpdate = [];
|
||||
foreach ($aField as $key => $value) {
|
||||
if(isset($aParam[$value])){
|
||||
$aUpdate[$value] = $aParam[$value];
|
||||
}
|
||||
foreach ($this->aField as $field) {
|
||||
if (isset($aParam[$field])) {
|
||||
$aUpdate[$field] = $aParam[$field];
|
||||
}
|
||||
}
|
||||
|
||||
// 强制更新时间
|
||||
$aUpdate['update_time'] = time();
|
||||
|
||||
try {
|
||||
return Db::name('wechat_queue_logs')
|
||||
->where('log_id', $iLogId)
|
||||
->limit(1)
|
||||
->update($aUpdate) > 0;
|
||||
} catch (\Exception $e) {
|
||||
$this->log("更新任务日志失败 [ID:{$iLogId}]: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
unset($aParam['log_id']);
|
||||
$result = DB::name('wechat_queue_logs')->where('log_id',$iLogId)->limit(1)->update($aUpdate);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入Reids 防止1小时内重复操作
|
||||
* 设置日志路径并确保目录存在
|
||||
* @param string $logPath
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function ensureLogDirExists($logPath = '')
|
||||
{
|
||||
if (empty($logPath)) {
|
||||
$error = "日志路径不能为空";
|
||||
$this->log($error);
|
||||
return $error;
|
||||
}
|
||||
|
||||
public function setRedisLabel($aParam = []){
|
||||
$this->logPath = $logPath;
|
||||
$logDir = dirname($this->logPath);
|
||||
|
||||
//判断数据是否为空
|
||||
if(empty($aParam['redis_key'])){
|
||||
return 3;
|
||||
// 检查并创建目录(处理权限问题)
|
||||
if (!is_dir($logDir)) {
|
||||
$oldUmask = umask(0);
|
||||
$created = mkdir($logDir, 0755, true);
|
||||
umask($oldUmask);
|
||||
|
||||
if (!$created || !is_dir($logDir)) {
|
||||
$error = "无法创建日志目录: {$logDir} (权限不足)";
|
||||
$this->log($error);
|
||||
return $error;
|
||||
}
|
||||
}
|
||||
//获取值
|
||||
$sValue = $this->getRedisLabel($aParam['redis_key']);
|
||||
if($sValue == $aParam['redis_key']){
|
||||
return 4;
|
||||
}
|
||||
$result = Cache::set($aParam['redis_key'], $aParam['redis_key'], 3600);
|
||||
if($result == true){
|
||||
return 1;
|
||||
}
|
||||
//写入
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Reids值
|
||||
* 写入日志到缓冲区
|
||||
* @param string $message
|
||||
*/
|
||||
public function getRedisLabel($sRedisKey = ''){
|
||||
if(empty($sRedisKey)){
|
||||
return '';
|
||||
public function log($message)
|
||||
{
|
||||
// 防止缓冲区溢出
|
||||
if (count($this->logBuffer) >= 1000) {
|
||||
$this->flushLog();
|
||||
}
|
||||
|
||||
$time = date('H:i:s');
|
||||
$this->logBuffer[] = "[$time] $message\n";
|
||||
|
||||
// 缓冲区满或超时则刷新
|
||||
if (count($this->logBuffer) >= 50 || time() - $this->lastLogTime > 10) {
|
||||
$this->flushLog();
|
||||
}
|
||||
return Cache::get($sRedisKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新日志缓冲区到文件
|
||||
*/
|
||||
public function flushLog()
|
||||
{
|
||||
if (empty($this->logBuffer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用SETNX原子操作设置锁
|
||||
public function setRedisLock($key, $value, $expire)
|
||||
{
|
||||
return $this->redis->set($key, $value, ['nx', 'ex' => $expire]);
|
||||
// 检查日志路径是否设置
|
||||
if (empty($this->logPath)) {
|
||||
$this->logBuffer = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小并处理
|
||||
$this->checkAndTruncateLog();
|
||||
|
||||
$fp = fopen($this->logPath, 'a');
|
||||
if ($fp === false) {
|
||||
// 紧急写入失败日志(避免递归)
|
||||
$errorMsg = "[" . date('H:i:s') . "] 错误: 无法打开日志文件 {$this->logPath}\n";
|
||||
error_log($errorMsg); // 写入系统日志
|
||||
$this->logBuffer = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试获取文件锁
|
||||
if (flock($fp, LOCK_EX)) {
|
||||
fwrite($fp, implode('', $this->logBuffer));
|
||||
flock($fp, LOCK_UN);
|
||||
} else {
|
||||
// 无锁情况下尝试写入
|
||||
fwrite($fp, implode('', $this->logBuffer));
|
||||
$this->logBuffer[] = "[" . date('H:i:s') . "] 警告: 日志写入未加锁,可能存在冲突风险\n";
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorMsg = "[" . date('H:i:s') . "] 错误: 写入日志失败: {$e->getMessage()}\n";
|
||||
fwrite($fp, $errorMsg);
|
||||
} finally {
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
$this->logBuffer = [];
|
||||
$this->lastLogTime = time();
|
||||
}
|
||||
|
||||
// 获取Redis值
|
||||
public function getRedisValue($key)
|
||||
|
||||
/**
|
||||
* 检查日志文件大小,超过限制则清空
|
||||
*/
|
||||
public function checkAndTruncateLog()
|
||||
{
|
||||
return $this->redis->get($key);
|
||||
if (empty($this->logPath) || !file_exists($this->logPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除文件状态缓存并获取大小
|
||||
clearstatcache(true, $this->logPath);
|
||||
$fileSize = @filesize($this->logPath);
|
||||
|
||||
if ($fileSize === false) {
|
||||
$this->log("错误: 无法获取日志文件大小 {$this->logPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
if ($fileSize >= $this->logMaxSize) {
|
||||
$fp = fopen($this->logPath, 'w');
|
||||
if ($fp === false) {
|
||||
$this->log("错误: 无法清空日志文件 {$this->logPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (flock($fp, LOCK_EX)) {
|
||||
// 二次检查文件大小(避免竞态条件)
|
||||
clearstatcache(true, $this->logPath);
|
||||
if (filesize($this->logPath) >= $this->logMaxSize) {
|
||||
ftruncate($fp, 0);
|
||||
$this->log("日志文件超过" . $this->formatFileSize($this->logMaxSize) . ",已清空");
|
||||
}
|
||||
flock($fp, LOCK_UN);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->log("错误: 清空日志文件失败: {$e->getMessage()}");
|
||||
} finally {
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 安全释放锁(仅当值匹配时删除)
|
||||
public function releaseRedisLock($key, $value)
|
||||
|
||||
/**
|
||||
* 格式化文件大小(字节转人类可读格式)
|
||||
* @param int $bytes
|
||||
* @return string
|
||||
*/
|
||||
public function formatFileSize($bytes)
|
||||
{
|
||||
|
||||
// 使用Lua脚本确保原子性
|
||||
$script = <<<LUA
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("DEL", KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
LUA;
|
||||
|
||||
return $this->redis->eval($script, [$key, $value], 1);
|
||||
if ($bytes <= 0) {
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$unitIndex = min(floor(log($bytes, 1024)), count($units) - 1);
|
||||
$size = $bytes / pow(1024, $unitIndex);
|
||||
|
||||
return number_format($size, 2) . ' ' . $units[$unitIndex];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
/**
|
||||
* 获取重试延迟时间
|
||||
* @param string $errorMsg
|
||||
* @return int
|
||||
*/
|
||||
public function getRetryDelay($errorMsg)
|
||||
{
|
||||
$delayMap = [
|
||||
'MySQL server has gone away' => 60,
|
||||
'timeout' => 30,
|
||||
'OpenAI' => 45,
|
||||
'network' => 60
|
||||
];
|
||||
|
||||
foreach ($delayMap as $keyword => $delay) {
|
||||
if (stripos($errorMsg, $keyword) !== false) { // 不区分大小写匹配
|
||||
return $delay;
|
||||
}
|
||||
}
|
||||
|
||||
return 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理可重试异常
|
||||
* @param \Exception $e
|
||||
* @param int $iLogId
|
||||
* @param string $sRedisKey
|
||||
* @param \think\queue\Job $job
|
||||
*/
|
||||
public function handleRetryableException($e, $iLogId, $sRedisKey, $job)
|
||||
{
|
||||
$sMsg = empty($e->getMessage()) ? '可重试异常' : $e->getMessage();
|
||||
$sTrace = empty($e->getTraceAsString()) ? '' : $e->getTraceAsString();
|
||||
$this->log("可重试异常: {$sMsg} | 堆栈: {$sTrace}");
|
||||
|
||||
if ($iLogId > 0) {
|
||||
$this->updateLog([
|
||||
'log_id' => $iLogId,
|
||||
'status' => 2,
|
||||
'error' => $sMsg . ':' . $sTrace,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->QueueRedis->finishJob($sRedisKey, 'failed', 3600);
|
||||
|
||||
$attempts = $job->attempts();
|
||||
if ($attempts >= $this->maxRetries) {
|
||||
$this->log("超过最大重试次数({$this->maxRetries}),停止重试");
|
||||
$job->delete();
|
||||
} else {
|
||||
$delay = $this->getRetryDelay($sMsg);
|
||||
$this->log("{$delay}秒后重试({$attempts}/{$this->maxRetries})");
|
||||
$job->release($delay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理不可重试异常
|
||||
* @param \Exception $e
|
||||
* @param int $iLogId
|
||||
* @param string $sRedisKey
|
||||
* @param \think\queue\Job $job
|
||||
*/
|
||||
public function handleNonRetryableException($e, $iLogId, $sRedisKey, $job)
|
||||
{
|
||||
$sMsg = empty($e->getMessage()) ? '不可重试异常' : $e->getMessage();
|
||||
$sTrace = empty($e->getTraceAsString()) ? '' : $e->getTraceAsString();
|
||||
$this->log("不可重试异常: {$sMsg} | 堆栈: {$sTrace}");
|
||||
|
||||
if ($iLogId > 0) {
|
||||
$this->updateLog([
|
||||
'log_id' => $iLogId,
|
||||
'status' => 3, // 3:不可重试失败
|
||||
'error' => $sMsg . ':' . $sTrace,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->QueueRedis->finishJob($sRedisKey, 'failed', 3600);
|
||||
$this->log("不可重试错误,直接删除任务");
|
||||
$job->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 析构函数:确保最后日志被写入
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->flushLog();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user