265 lines
9.4 KiB
PHP
265 lines
9.4 KiB
PHP
<?php
|
||
|
||
namespace app\common\library\live\message;
|
||
|
||
use app\common\model\config\System;
|
||
|
||
/**
|
||
* 直播消息服务商管理器
|
||
* 服务商统一由通用设置 live_basic.live_message_type 决定,切换后对所有直播间(含历史直播间)立即生效
|
||
*/
|
||
class Manager
|
||
{
|
||
/**
|
||
* 服务商名称与实现类的映射
|
||
* 新增服务商时在此注册即可
|
||
* @var array
|
||
*/
|
||
protected static $providers = [
|
||
'aodianyun' => \app\common\library\live\message\provider\Aodianyun::class,
|
||
'aliyun' => \app\common\library\live\message\provider\Aliyun::class,
|
||
];
|
||
|
||
/**
|
||
* 兼容历史中文值(前端 radio 曾误保存 label)到标准 provider key 的映射
|
||
* @var array
|
||
*/
|
||
protected static $aliasMap = [
|
||
'奥点云消息' => 'aodianyun',
|
||
'奥点云' => 'aodianyun',
|
||
'阿里云消息' => 'aliyun',
|
||
'阿里云' => 'aliyun',
|
||
];
|
||
|
||
/**
|
||
* 已实例化的服务商(按 provider 名缓存)
|
||
* @var array
|
||
*/
|
||
protected static $instances = [];
|
||
|
||
/**
|
||
* 归一化服务商名称
|
||
* 兼容历史中文值;无效值降级为奥点云
|
||
* @param string|null $providerName
|
||
* @return string
|
||
*/
|
||
public static function normalizeProvider($providerName)
|
||
{
|
||
if (!is_string($providerName) || $providerName === '') {
|
||
return 'aodianyun';
|
||
}
|
||
|
||
if (isset(self::$aliasMap[$providerName])) {
|
||
$providerName = self::$aliasMap[$providerName];
|
||
}
|
||
|
||
return isset(self::$providers[$providerName]) ? $providerName : 'aodianyun';
|
||
}
|
||
|
||
/**
|
||
* 判断消息服务是否已开启
|
||
* 读取 live_basic.live_message_status 配置,"1" 为开启,"0" 为关闭
|
||
* @return bool
|
||
*/
|
||
public static function isMessageEnabled()
|
||
{
|
||
try {
|
||
$config = System::getConfig('live_basic');
|
||
} catch (\Exception $e) {
|
||
return true;
|
||
}
|
||
|
||
if (!is_array($config) || !isset($config['live_message_status'])) {
|
||
return true;
|
||
}
|
||
|
||
return $config['live_message_status'] !== '0' && $config['live_message_status'] !== 0;
|
||
}
|
||
|
||
/**
|
||
* 获取全局配置的服务商名称
|
||
* 统一从 System::getConfig('live_basic')['live_message_type'] 读取
|
||
* @return string
|
||
*/
|
||
public static function getGlobalProviderName()
|
||
{
|
||
try {
|
||
$config = System::getConfig('live_basic');
|
||
} catch (\Exception $e) {
|
||
return 'aodianyun';
|
||
}
|
||
|
||
$providerName = (is_array($config) && isset($config['live_message_type'])) ? $config['live_message_type'] : '';
|
||
return self::normalizeProvider($providerName);
|
||
}
|
||
|
||
/**
|
||
* 获取服务商实例
|
||
* @param string|null $providerName 服务商名称(aodianyun/aliyun),为空则读取全局配置;非空也会归一化,避免中文值导致错误降级
|
||
* @return ProviderInterface
|
||
*/
|
||
public static function provider($providerName = null)
|
||
{
|
||
if ($providerName === null || $providerName === '') {
|
||
$providerName = self::getGlobalProviderName();
|
||
} else {
|
||
$providerName = self::normalizeProvider($providerName);
|
||
}
|
||
|
||
if (!isset(self::$instances[$providerName])) {
|
||
$class = self::$providers[$providerName];
|
||
self::$instances[$providerName] = new $class();
|
||
}
|
||
|
||
return self::$instances[$providerName];
|
||
}
|
||
|
||
/**
|
||
* 根据课程ID获取服务商实例
|
||
* 服务商由全局配置统一决定,参数保留仅为兼容旧调用签名
|
||
* @param int $courseId 课程ID(保留参数以兼容既有调用)
|
||
* @return ProviderInterface
|
||
*/
|
||
public static function providerByCourseId($courseId)
|
||
{
|
||
return self::provider(self::getGlobalProviderName());
|
||
}
|
||
|
||
/**
|
||
* 根据课程ID获取服务商名称
|
||
* 服务商由全局配置统一决定,参数保留仅为兼容旧调用签名
|
||
* @param int $courseId 课程ID(保留参数以兼容既有调用)
|
||
* @return string
|
||
*/
|
||
public static function providerNameByCourseId($courseId)
|
||
{
|
||
return self::getGlobalProviderName();
|
||
}
|
||
|
||
/**
|
||
* 注册自定义服务商(便于扩展)
|
||
* @param string $name 服务商名称
|
||
* @param string $class 实现类(必须实现 ProviderInterface)
|
||
* @return void
|
||
*/
|
||
public static function register($name, $class)
|
||
{
|
||
self::$providers[$name] = $class;
|
||
// 清除已缓存实例以便下次使用新注册类
|
||
unset(self::$instances[$name]);
|
||
}
|
||
|
||
/**
|
||
* 确保阿里云互动消息群组已创建
|
||
* 用于兼容历史直播间:直播间创建时若不是阿里云模式则未建群,切换到阿里云后需要幂等地补建
|
||
* 已存在的群会被阿里云识别为重复请求(GroupExist / 类似错误码),此处按成功处理
|
||
* @param string $groupId 群组ID(对应 tuzhi_live_room.message_topic)
|
||
* @param string $groupName 群组名称
|
||
* @return bool 是否可用(true=群存在或补建成功;false=补建失败)
|
||
*/
|
||
public static function ensureAliyunGroup($groupId, $groupName = '')
|
||
{
|
||
if (!$groupId) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
$result = self::provider('aliyun')->createGroup($groupId, $groupName, '');
|
||
if ($result->isSuccess()) {
|
||
return true;
|
||
}
|
||
|
||
// 阿里云群组已存在类错误按成功处理,保证幂等
|
||
$error = is_string($result->error) ? $result->error : '';
|
||
if ($error !== '' && (
|
||
stripos($error, 'exist') !== false ||
|
||
stripos($error, 'duplicate') !== false ||
|
||
stripos($error, 'already') !== false
|
||
)) {
|
||
return true;
|
||
}
|
||
|
||
\think\Log::error('[live Manager ensureAliyunGroup] createGroup failed: ' . $error . ' group_id=' . $groupId);
|
||
return false;
|
||
} catch (\Exception $e) {
|
||
\think\Log::error('[live Manager ensureAliyunGroup] exception: ' . $e->getMessage() . ' group_id=' . $groupId);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理已删除课程对应的阿里云互动消息群组
|
||
*
|
||
* 逻辑:
|
||
* 1. 查找 status=-1(已删除)且课程类型为 live 的课程
|
||
* 2. 关联 tuzhi_live_room 获取 message_topic
|
||
* 3. 调用 deleteGroup 删除阿里云端的群组
|
||
* 4. 清空 live_room.message_topic 标记已清理
|
||
*
|
||
* @param int $batchSize 单次最多处理数量,防止超时
|
||
* @param bool $dryRun 仅预览,不实际删除
|
||
* @return array ['deleted' => int, 'failed' => int, 'skipped' => int, 'items' => array]
|
||
*/
|
||
public static function cleanupOrphanGroups($batchSize = 50, $dryRun = false)
|
||
{
|
||
$provider = self::getGlobalProviderName();
|
||
if ($provider !== 'aliyun') {
|
||
return ['deleted' => 0, 'failed' => 0, 'skipped' => 0, 'items' => [], 'msg' => '当前消息服务非阿里云,无需清理'];
|
||
}
|
||
|
||
// 查找已删除课程中仍保留 message_topic 的直播间
|
||
$rows = \think\Db::table('tuzhi_live_room r')
|
||
->join('tuzhi_course c', 'c.id = r.course_id', 'LEFT')
|
||
->where('r.message_topic', '<>', '')
|
||
->where('r.message_topic', 'IS NOT NULL')
|
||
->where(function ($q) {
|
||
// 课程已删除 或 课程不存在(孤儿记录)
|
||
$q->where('c.status', -1)->whereOr('c.id', 'IS NULL');
|
||
})
|
||
->limit($batchSize)
|
||
->select(['r.id', 'r.course_id', 'r.message_topic']);
|
||
|
||
$deleted = 0;
|
||
$failed = 0;
|
||
$items = [];
|
||
|
||
$aliyunProvider = self::provider('aliyun');
|
||
|
||
foreach ($rows as $row) {
|
||
$groupId = $row['message_topic'];
|
||
$roomId = $row['id'];
|
||
$courseId = $row['course_id'];
|
||
|
||
if ($dryRun) {
|
||
$items[] = ['room_id' => $roomId, 'course_id' => $courseId, 'group_id' => $groupId, 'action' => 'preview'];
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
$result = $aliyunProvider->deleteGroup($groupId, 'system-cleanup');
|
||
if ($result->isSuccess()) {
|
||
// 清空 message_topic 标记已清理
|
||
\think\Db::table('tuzhi_live_room')->where('id', $roomId)->update(['message_topic' => '']);
|
||
$deleted++;
|
||
$items[] = ['room_id' => $roomId, 'course_id' => $courseId, 'group_id' => $groupId, 'action' => 'deleted'];
|
||
} else {
|
||
$failed++;
|
||
$items[] = ['room_id' => $roomId, 'course_id' => $courseId, 'group_id' => $groupId, 'action' => 'failed', 'error' => $result->error];
|
||
\think\Log::error('[live Manager cleanupOrphanGroups] deleteGroup failed: ' . $result->error . ' group_id=' . $groupId);
|
||
}
|
||
} catch (\Exception $e) {
|
||
$failed++;
|
||
$items[] = ['room_id' => $roomId, 'course_id' => $courseId, 'group_id' => $groupId, 'action' => 'error', 'error' => $e->getMessage()];
|
||
\think\Log::error('[live Manager cleanupOrphanGroups] exception: ' . $e->getMessage() . ' group_id=' . $groupId);
|
||
}
|
||
}
|
||
|
||
return [
|
||
'deleted' => $deleted,
|
||
'failed' => $failed,
|
||
'skipped' => count($rows) - $deleted - $failed,
|
||
'items' => $items,
|
||
];
|
||
}
|
||
}
|