\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, ]; } }