'D音', 1 => 'K手', 2 => 'B家号', 3 => '小红薯', 4 => '视P号', 5 => 'B哩哔哩', 6 => '公Z号', 10 => 'TiTk', ]; /** * 平台英文键按 acgpmw `data/other/platforms_py.php` 对齐。 */ private const PLATFORM_KEY_MAP = [ 0 => 'douyin', 1 => 'kuaishou', 2 => 'baijiahao', 3 => 'xiaohongshu', 4 => 'shipinhao', 5 => 'bilibili', 6 => 'gongzhonghao', 10 => 'tiktok', ]; /** * 获取视频作品列表。 * * @param int $userid 当前登录用户ID * @param array $params 前端查询参数 * @return array * @throws \Exception */ public function getVideoList(int $userid, array $params = []): array { $member = Member::findByUserid($userid); if (!$member) { throw new \Exception('用户不存在', 4004); } $platform = $this->normalizeOptionalInt($params['platform'] ?? 'all', '平台参数格式错误'); $vuid = $this->normalizeOptionalInt($params['vuid'] ?? 'all', '账号参数格式错误'); $page = max(1, (int) ($params['page'] ?? 1)); $pageSize = min(30, max(1, (int) ($params['page_size'] ?? 12))); $productInfo = $member->getProductInfo(); $availablePlatforms = $this->resolveAvailablePlatforms($member, $productInfo); if (empty($availablePlatforms)) { throw new \Exception('暂无视频作品权限', 4004); } if ($platform !== null && !in_array($platform, $availablePlatforms, true)) { throw new \Exception('当前套餐暂无该平台权限', 4004); } $accountOptions = $this->loadAccountOptions($userid, $availablePlatforms); if ($vuid !== null && !array_key_exists($vuid, $accountOptions['map'])) { throw new \Exception('当前账号不存在或已解绑', 4004); } $baseQuery = $this->buildFilteredQuery($userid, [ 'platform' => $platform, 'vuid' => $vuid, ]); $total = (int) (clone $baseQuery)->count(); $records = (clone $baseQuery) ->field([ 'id', 'vuid', 'cron_id', 'aweme_id', 'aweme_nid', 'dy_item_id', 'dy_title', 'kwords', 'dy_video_status', 'dy_cover', 'dy_create_time', 'dy_share_url', 'dy_comment_count', 'dy_digg_count', 'dy_play_count', 'addtime', 'fabu_type', 'disabled', 'status', 'fberrinfo', 'platform', 'tplid', ]) ->order('id', 'desc') ->page($page, $pageSize) ->select() ->toArray(); $items = []; foreach ($records as $record) { $items[] = $this->buildVideoItem($record, $accountOptions['map']); } return [ 'filters' => [ 'current_platform' => $platform, 'platforms' => $this->buildPlatformFilters($userid, $availablePlatforms, $vuid, $platform), 'current_vuid' => $vuid, 'accounts' => $accountOptions['items'], ], 'summary' => $this->buildSummary(clone $baseQuery), 'pagination' => [ 'page' => $page, 'page_size' => $pageSize, 'total' => $total, 'has_more' => $page * $pageSize < $total, ], 'list' => $items, ]; } /** * 加载账号筛选项。 * * @param int $userid 用户ID * @param array $availablePlatforms 可见平台 * @return array{items: array>, map: array>} */ private function loadAccountOptions(int $userid, array $availablePlatforms): array { $accounts = DyVideoUser::getVideoFilterAccountsByUserId($userid)->toArray(); $items = [ [ 'id' => null, 'name' => '全部账号', ], ]; $accountMap = []; foreach ($accounts as $account) { $platform = (int) ($account['platform'] ?? 0); if (!in_array($platform, $availablePlatforms, true)) { continue; } $accountId = (int) ($account['id'] ?? 0); $accountMap[$accountId] = $account; $items[] = [ 'id' => $accountId, 'name' => sprintf( '%s(%s)#P%d', self::PLATFORM_NAME_MAP[$platform] ?? ('平台' . $platform), (string) ($account['dy_nickname'] ?? '未命名账号'), $accountId ), ]; } return [ 'items' => $items, 'map' => $accountMap, ]; } /** * 构建筛选后的作品查询。 * * @param int $userid 用户ID * @param array $filters 筛选条件 * @return Query */ private function buildFilteredQuery(int $userid, array $filters): Query { $query = DysVideoLog::buildUserQuery($userid); if (($filters['vuid'] ?? null) !== null) { $query->where('vuid', (int) $filters['vuid']); } if (($filters['platform'] ?? null) !== null) { $query->where('platform', (int) $filters['platform']); } return $query; } /** * 构建平台筛选项。 * * 这里按当前账号筛选条件统计数量,使前端切平台时与当前账号范围一致。 * * @param int $userid 用户ID * @param array $availablePlatforms 可见平台 * @param int|null $vuid 当前账号筛选 * @param int|null $currentPlatform 当前平台筛选 * @return array> */ private function buildPlatformFilters(int $userid, array $availablePlatforms, ?int $vuid, ?int $currentPlatform): array { $tabs = [ [ 'id' => null, 'name' => '全部平台', 'key' => 'all', 'count' => 0, ], ]; $allCount = 0; foreach ($availablePlatforms as $platform) { $query = $this->buildFilteredQuery($userid, [ 'platform' => $platform, 'vuid' => $vuid, ]); $count = (int) $query->count(); $allCount += $count; $tabs[] = [ 'id' => $platform, 'name' => self::PLATFORM_NAME_MAP[$platform] ?? ('平台' . $platform), 'key' => self::PLATFORM_KEY_MAP[$platform] ?? ('platform_' . $platform), 'count' => $count, 'active' => $currentPlatform === $platform, ]; } $tabs[0]['count'] = $allCount; $tabs[0]['active'] = $currentPlatform === null; return $tabs; } /** * 构建顶部统计卡片。 * * @param Query $query 已按当前筛选条件收窄的查询对象 * @return array */ private function buildSummary(Query $query): array { return [ 'total' => (int) (clone $query)->count(), 'published_count' => (int) (clone $query)->whereRaw('(dy_video_status in (1,5) or aweme_id <> "" or dy_item_id <> "" or aweme_nid <> "")')->count(), 'reviewing_count' => (int) (clone $query)->where('dy_video_status', 4)->count(), 'failed_count' => (int) (clone $query)->whereIn('status', [2, 3])->count(), ]; } /** * 构建前端作品卡片数据。 * * @param array $record 原始作品记录 * @param array> $accountMap 账号信息映射 * @return array */ private function buildVideoItem(array $record, array $accountMap): array { $platform = (int) ($record['platform'] ?? 0); $accountId = (int) ($record['vuid'] ?? 0); $account = $accountMap[$accountId] ?? []; $title = $this->buildVideoTitle($record); $previewUrl = $this->buildPreviewUrl($record); $statusInfo = $this->buildStatusInfo($record); $publishTimeText = $this->buildPublishTimeText($record); return [ 'id' => (int) ($record['id'] ?? 0), 'title' => $title, 'cover' => (string) ($record['dy_cover'] ?? ''), 'platform' => $platform, 'platform_name' => self::PLATFORM_NAME_MAP[$platform] ?? ('平台' . $platform), 'platform_key' => self::PLATFORM_KEY_MAP[$platform] ?? ('platform_' . $platform), 'account_id' => $accountId, 'account_name' => (string) ($account['dy_nickname'] ?? '未命名账号'), 'account_avatar' => (string) ($account['dy_avatar'] ?? ''), 'cron_id' => (int) ($record['cron_id'] ?? 0), 'tplid' => (int) ($record['tplid'] ?? 0), 'play_count' => (int) ($record['dy_play_count'] ?? 0), 'digg_count' => (int) ($record['dy_digg_count'] ?? 0), 'comment_count' => (int) ($record['dy_comment_count'] ?? 0), 'publish_time_text' => $publishTimeText, 'status' => $statusInfo, 'preview_url' => $previewUrl, 'has_preview' => $previewUrl !== '', 'is_article' => (int) ($record['fabu_type'] ?? 0) === 101, 'display_code' => '#V' . (int) ($record['id'] ?? 0), 'task_code' => '#C' . (int) ($record['cron_id'] ?? 0), 'material_code' => (int) ($record['tplid'] ?? 0) > 0 ? '#M' . (int) $record['tplid'] : '', ]; } /** * 组装作品标题。 * * 对齐 acgpmw:优先 `dy_title`,否则使用 `kwords` 拼接。 * * @param array $record 原始作品记录 * @return string */ private function buildVideoTitle(array $record): string { $title = trim((string) ($record['dy_title'] ?? '')); if ($title !== '') { return $title; } $keywords = trim((string) ($record['kwords'] ?? ''), ','); if ($keywords === '') { return '暂无作品信息'; } return '#' . str_replace(',', ' #', $keywords); } /** * 构建作品状态块。 * * 对齐 acgpmw `get_videos()` 的状态展示优先级: * 1. 定时中 * 2. 发布失败 * 3. 已发布 / 未公开 / 审核中 / 好友可见 / 私密视频 * 4. 待处理 * * @param array $record 原始作品记录 * @return array */ private function buildStatusInfo(array $record): array { $platform = (int) ($record['platform'] ?? 0); $dyCreateTime = (int) ($record['dy_create_time'] ?? 0); $status = (int) ($record['status'] ?? 0); $dyVideoStatus = (int) ($record['dy_video_status'] ?? 0); $isPublished = !empty($record['aweme_id']) || !empty($record['dy_item_id']) || !empty($record['aweme_nid']); if (in_array($platform, [0, 4], true) && $dyCreateTime > time()) { return [ 'text' => '定时中', 'tone' => 'warning', 'hint' => '作品已进入定时发布队列', ]; } if (in_array($status, [2, 3], true)) { $errorText = $this->translatePublishError((string) ($record['fberrinfo'] ?? ''), $platform); return [ 'text' => '发布失败', 'tone' => 'danger', 'hint' => $errorText !== '' ? $errorText : '作品发布失败,请回到原系统查看详情', ]; } if (in_array($dyVideoStatus, [1, 5], true) || $isPublished) { return [ 'text' => '已发布', 'tone' => 'success', 'hint' => '作品已成功发布到对应平台', ]; } if ($dyVideoStatus === 2) { return [ 'text' => '未公开', 'tone' => 'danger', 'hint' => '作品当前为未公开状态', ]; } if ($dyVideoStatus === 4) { return [ 'text' => '审核中', 'tone' => 'primary', 'hint' => '作品已提交平台审核,请稍后查看', ]; } if ($dyVideoStatus === 6) { return [ 'text' => '好友可见', 'tone' => 'info', 'hint' => '作品仅好友可见', ]; } if ($dyVideoStatus === 7) { return [ 'text' => '私密视频', 'tone' => 'warning', 'hint' => '作品当前为私密视频', ]; } return [ 'text' => '待处理', 'tone' => 'slate', 'hint' => '作品正在处理或等待平台返回结果', ]; } /** * 构建发布时间文案。 * * @param array $record 原始作品记录 * @return string */ private function buildPublishTimeText(array $record): string { $platform = (int) ($record['platform'] ?? 0); $dyCreateTime = (int) ($record['dy_create_time'] ?? 0); $addtime = (int) ($record['addtime'] ?? 0); $publishTime = $addtime; if ($dyCreateTime > 0) { $publishTime = $dyCreateTime; } if (in_array($platform, [0, 4], true) && $dyCreateTime > time()) { return '将于 ' . $this->formatDateTime($dyCreateTime) . ' 发布'; } return $this->formatDateTime($publishTime); } /** * 生成作品预览链接。 * * 对齐 acgpmw `get_videos()` 中各平台的链接处理规则。 * * @param array $record 原始作品记录 * @return string */ private function buildPreviewUrl(array $record): string { $platform = (int) ($record['platform'] ?? 0); $status = (int) ($record['status'] ?? 0); $shareUrl = (string) ($record['dy_share_url'] ?? ''); $awemeId = (string) ($record['aweme_id'] ?? ''); $dyItemId = (string) ($record['dy_item_id'] ?? ''); $dyCreateTime = (int) ($record['dy_create_time'] ?? 0); if ($platform === 0 && $shareUrl !== '') { $shareUrl = 'https://www.douyin.com/video/' . $awemeId; if (strpos((string) ($record['dy_share_url'] ?? ''), 'https://video-cn.snssdk.com/') !== false) { $shareUrl = (string) ($record['dy_share_url'] ?? ''); } if ($dyCreateTime < time() && $awemeId !== '') { $shareUrl = 'https://www.douyin.com/video/' . $awemeId; } } if ($platform === 5 && $status === 1 && $dyItemId !== '') { $shareUrl = 'https://www.bilibili.com/video/' . $dyItemId; } if ($platform === 2 && $status === 1 && $dyItemId !== '') { $shareUrl = 'https://mbd.baidu.com/newspage/data/videolanding?nid=sv_' . $dyItemId; } if ($platform === 4 && strpos($shareUrl, 'https://weixin.qq.com/sph/') === false) { $shareUrl = ''; } return $shareUrl; } /** * 解析发布异常原因。 * * 这里直接沿用 acgpmw `video_info::getycinfo()` 的关键信息匹配规则。 * * @param string $errorInfo 原始错误信息 * @param int $platform 平台编号 * @return string */ private function translatePublishError(string $errorInfo, int $platform): string { if ($errorInfo === '') { return ''; } if ($platform === 0) { $map = [ '"error_code":10008' => '授权过期', '用户未登录' => '授权过期', '上传视频按钮超时' => '视频上传超时', '寻找描述输入框超时' => '素材或内容处理超时', '"error_code":2114033,' => '包含不合法文字', '"status_code":-2158,' => '发布频繁或异常', '"error_code":2100005,' => '参数不合法', '"error_code":2190003,' => '授权异常', '"error_code":2114005,' => '视频投稿功能已封禁[#G]', '"error_code":2100004,' => '系统繁忙异常', '"status_code":-20' => '视频投稿功能已封禁[#M]', '"error_code":2190019,' => '系统发布失败或异常', '"status_msg":"sms"' => '发布短信验证', '健康分不足投稿功能受限' => '健康分不足投稿功能受限', ]; return $this->matchErrorText($errorInfo, $map); } if ($platform === 1) { return $this->matchErrorText($errorInfo, [ 'ACCESS_DENIED' => '授权过期', 'user_request_limit' => '发布限制或异常', 'video_not_uploaded' => '视频上传异常', ]); } if ($platform === 2) { return $this->matchErrorText($errorInfo, [ '"errno":60001009' => '账号内容质量不足,今日还可发布0篇内容', '"errno":60000005' => '账号状态异常', '"errno":105011' => '百家号开放平台业务调整,已关闭您的接口使用权限', ]); } if ($platform === 3) { return $this->matchErrorText($errorInfo, [ '当日发布数已达到上限' => '当日发布数已达到上限', '系统正在升级中' => '系统平台升级维护中', '发布笔记需要绑定手机' => '发布笔记需要绑定手机号', '笔记发布失败' => '作品笔记发布失败', '用户被封号' => '用户被封号', ]); } if ($platform === 4) { return $this->matchErrorText($errorInfo, [ '"errcode":-11217' => '暂未绑定手机号', '"errcode":-11216' => '完成实名后才可以发表', 'cookie失效' => '授权过期', '"errCode":300334' => '授权过期(授权完有其他扫码操作)', ]); } return ''; } /** * 按关键字匹配错误提示。 * * @param string $errorInfo 原始错误信息 * @param array $map 关键字到提示文案映射 * @return string */ private function matchErrorText(string $errorInfo, array $map): string { foreach ($map as $needle => $message) { if (strpos($errorInfo, $needle) !== false) { return $message; } } return ''; } /** * 计算当前用户可见平台。 * * 对齐 acgpmw: * 1. 先使用已按 `MemberController` 二次加工后的套餐平台 * 2. 146/147 且不在特殊名单中,只允许 D音 * * @param Member $member 当前登录用户 * @param array|null $productInfo 套餐信息 * @return array */ private function resolveAvailablePlatforms(Member $member, ?array $productInfo): array { $platformText = trim((string) ($productInfo['platforms'] ?? ''), ','); if ($platformText === '') { return []; } $platforms = array_values(array_filter(array_map('intval', explode(',', $platformText)), static function ($item) { return array_key_exists($item, self::PLATFORM_NAME_MAP); })); $specialAccounts = $this->loadSpecialAccounts(); $isRestrictedVType = in_array((int) $member->v_type, [146, 147], true); $isSpecialAccount = in_array((int) $member->userid, $specialAccounts, true); if ($isRestrictedVType && !$isSpecialAccount) { return [0]; } return $platforms; } /** * 加载云图文特殊账号名单。 * * @return array */ private function loadSpecialAccounts(): array { $file = root_path() . '../acgpmw/data/other/tw_special_account.php'; if (!is_file($file)) { return []; } $accounts = require $file; return is_array($accounts) ? array_values(array_map('intval', $accounts)) : []; } /** * 规范化可选整数参数。 * * @param mixed $value 原始输入 * @param string $errorMessage 错误提示 * @return int|null * @throws \Exception */ private function normalizeOptionalInt($value, string $errorMessage): ?int { if ($value === null || $value === '' || $value === 'all') { return null; } if (!is_numeric((string) $value)) { throw new \Exception($errorMessage, 400); } return (int) $value; } /** * 统一格式化时间。 * * @param int $timestamp 时间戳 * @return string */ private function formatDateTime(int $timestamp): string { if ($timestamp <= 0) { return '--'; } return date('Y-m-d H:i', $timestamp); } }