diff --git a/app/api/controller/v1/Platform.php b/app/api/controller/v1/Platform.php new file mode 100644 index 0000000..ae7fb37 --- /dev/null +++ b/app/api/controller/v1/Platform.php @@ -0,0 +1,66 @@ +platformService = new PlatformService(); + } + + /** + * 平台账号列表。 + * + * GET /api/v1/platform/accounts + * + * 请求参数: + * - `platform`:平台编号,可选;为空时返回全部平台 + * + * 返回结构: + * - `filters`:当前平台筛选项 + * - `summary`:当前筛选结果统计 + * - `list`:账号列表,含账号授权、数据授权、异常状态 + */ + public function accounts() + { + try { + $payload = $this->request->payload ?? null; + if (!$payload || empty($payload['userid'])) { + return Response::error('未登录', 401); + } + + $platformInput = $this->request->get('platform'); + $platform = null; + + if ($platformInput !== null && $platformInput !== '' && $platformInput !== 'all') { + if (!is_numeric((string) $platformInput)) { + return Response::error('平台参数格式错误', 400); + } + + $platform = (int) $platformInput; + } + + $result = $this->platformService->getAccountList((int) $payload['userid'], $platform); + + return Response::success($result); + } catch (\Exception $exception) { + return Response::error($exception->getMessage(), $exception->getCode() ?: 500); + } + } +} diff --git a/app/api/model/DyVideoUser.php b/app/api/model/DyVideoUser.php index 1f22edb..c55bc5f 100644 --- a/app/api/model/DyVideoUser.php +++ b/app/api/model/DyVideoUser.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace app\api\model; use think\Model; +use think\Collection; /** * 授权账号模型(对应 dy_video_user 表) @@ -32,4 +33,49 @@ class DyVideoUser extends Model ->where('disabled', 0) ->count(); } + + /** + * 获取当前用户的平台账号列表。 + * + * 这里按 acgpmw `platform::index()` 和 `platform::get_zhanghu_list()` 的查询条件对齐: + * - 仅返回当前用户数据 + * - 仅返回 `disabled=0` 的有效账号 + * - 排序保持 `is_endauth desc, id desc` + * + * @param int $userid 用户ID + * @param int|null $platform 指定平台;为空时返回全部平台 + * @return Collection + */ + public static function getPlatformAccountsByUserId(int $userid, ?int $platform = null): Collection + { + $query = self::where('userid', $userid) + ->where('disabled', 0) + ->order(['is_endauth' => 'desc', 'id' => 'desc']) + ->field([ + 'id', + 'userid', + 'platform', + 'is_lanv', + 'is_endauth', + 'aa_endauth', + 'browser', + 'is_qyh', + 'proviceid', + 'info_shouji', + 'dy_avatar', + 'dy_nickname', + 'dy_intro', + 'dy_account', + 'dy_openid', + 'dy_unique_id', + 'addtime', + 'updatetime', + ]); + + if ($platform !== null) { + $query->where('platform', $platform); + } + + return $query->select(); + } } diff --git a/app/api/model/DyVideoUserTitk.php b/app/api/model/DyVideoUserTitk.php new file mode 100644 index 0000000..0be1518 --- /dev/null +++ b/app/api/model/DyVideoUserTitk.php @@ -0,0 +1,33 @@ +find(); + } +} diff --git a/app/api/route/app.php b/app/api/route/app.php index 81d28ad..dccb7d7 100644 --- a/app/api/route/app.php +++ b/app/api/route/app.php @@ -3,6 +3,7 @@ declare(strict_types=1); use think\facade\Route; use app\api\controller\v1\Auth; +use app\api\controller\v1\Platform; /** * API 应用路由 @@ -19,3 +20,8 @@ Route::group('v1/auth', function () { Route::post('logout', [Auth::class, 'logout']); Route::post('password', [Auth::class, 'password']); })->middleware(\app\api\middleware\Auth::class); + +// v1 平台账号管理接口(需登录) +Route::group('v1/platform', function () { + Route::get('accounts', [Platform::class, 'accounts']); +})->middleware(\app\api\middleware\Auth::class); diff --git a/app/api/service/PlatformService.php b/app/api/service/PlatformService.php new file mode 100644 index 0000000..1433708 --- /dev/null +++ b/app/api/service/PlatformService.php @@ -0,0 +1,325 @@ + 'douyin', + 1 => 'kuaishou', + 2 => 'baijiahao', + 3 => 'xiaohongshu', + 4 => 'shipinhao', + 5 => 'bilibili', + 6 => 'gongzhonghao', + 10 => 'tiktok', + ]; + + /** + * 页面展示名称。 + * + * acgpmw 模板内部会通过 `$this->platform_info[$platform]` 展示中文平台名, + * 这里在 API 中直接返回前端所需文案,避免小程序自行猜测。 + */ + private const PLATFORM_NAME_MAP = [ + 0 => '抖音', + 1 => '快手', + 2 => '百家号', + 3 => '小红书', + 4 => '视频号', + 5 => 'B站', + 6 => '公众号', + 10 => 'TikTok', + ]; + + /** + * 获取平台账号管理页列表数据。 + * + * @param int $userid 当前登录用户ID + * @param int|null $platform 当前筛选平台;为空时返回全部可见平台 + * @return array + * @throws \Exception + */ + public function getAccountList(int $userid, ?int $platform = null): array + { + $member = Member::findByUserid($userid); + if (!$member) { + throw new \Exception('用户不存在', 4006); + } + + $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); + } + + $targetPlatform = $platform; + $accounts = DyVideoUser::getPlatformAccountsByUserId($userid, $targetPlatform); + + $accountItems = []; + foreach ($accounts as $account) { + $accountPlatform = (int) $account->platform; + + if (!in_array($accountPlatform, $availablePlatforms, true)) { + continue; + } + + $accountItems[] = $this->buildAccountItem($account->toArray()); + } + + $platformTabs = $this->buildPlatformTabs($availablePlatforms, $userid); + $summary = $this->buildSummary($accountItems); + + return [ + 'filters' => [ + 'current_platform' => $targetPlatform, + 'platforms' => $platformTabs, + ], + 'summary' => $summary, + 'list' => $accountItems, + ]; + } + + /** + * 计算当前用户可见平台。 + * + * 该逻辑直接对齐 acgpmw `platform::index()`: + * 1. 从套餐 `product_list.platforms` 读取平台权限 + * 2. 若为 146/147 且不在特殊账号名单中,仅允许抖音平台 + * + * @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; + } + + /** + * 构建前端平台筛选项。 + * + * @param array $platforms 当前用户可见平台 + * @param int $userid 当前用户ID + * @return array> + */ + private function buildPlatformTabs(array $platforms, int $userid): array + { + $tabs = [ + [ + 'id' => null, + 'name' => '全部平台', + 'key' => 'all', + 'count' => 0, + ], + ]; + + $allCount = 0; + foreach ($platforms as $platform) { + $count = DyVideoUser::where('userid', $userid) + ->where('disabled', 0) + ->where('platform', $platform) + ->count(); + + $allCount += (int) $count; + $tabs[] = [ + 'id' => $platform, + 'name' => self::PLATFORM_NAME_MAP[$platform] ?? ('平台' . $platform), + 'key' => self::PLATFORM_KEY_MAP[$platform] ?? ('platform_' . $platform), + 'count' => (int) $count, + ]; + } + + $tabs[0]['count'] = $allCount; + + return $tabs; + } + + /** + * 把数据库记录转换为前端可直接渲染的账号卡片。 + * + * 状态字段解释按 acgpmw `get_zhanghu_list()` 三列展示对齐: + * - `is_endauth` => 账号授权状态 + * - `aa_endauth` => 数据授权状态 + * - `browser` => 异常状态 + * + * @param array $account dy_video_user 单条记录 + * @return array + */ + private function buildAccountItem(array $account): array + { + $platform = (int) ($account['platform'] ?? 0); + $countryName = ''; + + if ($platform === 10) { + $titkInfo = DyVideoUserTitk::findByVuid((int) $account['id']); + $countryName = (string) ($titkInfo->country_name ?? ''); + } + + $accountAuth = (int) ($account['is_endauth'] ?? 0) === 1 + ? $this->buildStatusBlock('授权到期', 'danger', '请重新完成平台授权') + : $this->buildStatusBlock('授权正常', 'success', '当前账号授权可用'); + + $dataAuth = (int) ($account['aa_endauth'] ?? 0) === 1 + ? $this->buildStatusBlock('授权到期', 'danger', '请重新完成数据授权') + : $this->buildStatusBlock('授权正常', 'success', '当前数据授权可用'); + + $exceptionStatus = (int) ($account['browser'] ?? 0) === 1 + ? $this->buildStatusBlock('发布异常', 'warning', '发布链路存在异常,需要人工处理') + : $this->buildStatusBlock('使用正常', 'success', '当前账号发布链路正常'); + + return [ + 'id' => (int) $account['id'], + 'platform' => $platform, + 'platform_name' => self::PLATFORM_NAME_MAP[$platform] ?? ('平台' . $platform), + 'platform_key' => self::PLATFORM_KEY_MAP[$platform] ?? ('platform_' . $platform), + 'nickname' => (string) ($account['dy_nickname'] ?? ''), + 'avatar' => (string) ($account['dy_avatar'] ?? ''), + 'intro' => (string) ($account['dy_intro'] ?? ''), + 'phone' => (string) ($account['info_shouji'] ?? ''), + 'country_name' => $countryName, + 'proviceid' => (int) ($account['proviceid'] ?? 0), + 'is_lanv' => (int) ($account['is_lanv'] ?? 0) === 1, + 'is_qyh' => (int) ($account['is_qyh'] ?? 0) === 1, + 'account_status' => $accountAuth, + 'data_status' => $dataAuth, + 'exception_status' => $exceptionStatus, + 'status_overview' => $this->buildOverviewStatus($accountAuth, $dataAuth, $exceptionStatus), + 'display_code' => '#P' . (int) $account['id'], + ]; + } + + /** + * 构建单个状态块。 + * + * @param string $text 显示文案 + * @param string $tone 视觉色调:success / warning / danger + * @param string $hint 补充说明 + * @return array + */ + private function buildStatusBlock(string $text, string $tone, string $hint): array + { + return [ + 'text' => $text, + 'tone' => $tone, + 'hint' => $hint, + ]; + } + + /** + * 汇总账号卡片主状态,便于列表页用一个醒目标记表示当前账号健康度。 + * + * @param array $accountAuth 账号授权状态 + * @param array $dataAuth 数据授权状态 + * @param array $exceptionStatus 异常状态 + * @return array + */ + private function buildOverviewStatus(array $accountAuth, array $dataAuth, array $exceptionStatus): array + { + if ($accountAuth['tone'] === 'danger' || $dataAuth['tone'] === 'danger') { + return $this->buildStatusBlock('需重新授权', 'danger', '账号授权或数据授权已到期'); + } + + if ($exceptionStatus['tone'] === 'warning') { + return $this->buildStatusBlock('存在异常', 'warning', '当前账号发布链路异常'); + } + + return $this->buildStatusBlock('状态正常', 'success', '账号与发布状态均正常'); + } + + /** + * 统计当前筛选结果。 + * + * @param array> $items 列表项 + * @return array + */ + private function buildSummary(array $items): array + { + $summary = [ + 'total' => count($items), + 'need_reauth_count' => 0, + 'browser_exception_count' => 0, + 'normal_count' => 0, + ]; + + foreach ($items as $item) { + $needReauth = $item['account_status']['tone'] === 'danger' || $item['data_status']['tone'] === 'danger'; + $hasBrowserException = $item['exception_status']['tone'] === 'warning'; + + if ($needReauth) { + $summary['need_reauth_count']++; + } + + if ($hasBrowserException) { + $summary['browser_exception_count']++; + } + + if (!$needReauth && !$hasBrowserException) { + $summary['normal_count']++; + } + } + + return $summary; + } + + /** + * 加载 acgpmw 的特殊账号名单。 + * + * 这里优先读取基线项目 `/root/work/acgpmw/data/other/tw_special_account.php`, + * 保证 146/147 套餐用户的平台权限限制与原系统保持一致; + * 若文件不存在,则退回空数组,接口仍可工作。 + * + * @return array + */ + private function loadSpecialAccounts(): array + { + $specialAccountFile = '/root/work/acgpmw/data/other/tw_special_account.php'; + + if (!is_file($specialAccountFile)) { + return []; + } + + $accounts = require $specialAccountFile; + + return is_array($accounts) + ? array_values(array_map('intval', $accounts)) + : []; + } +}