alias('a') ->leftJoin('admin u', 'u.id = a.owner_admin_id') ->leftJoin('dept d', 'd.id = a.dept_id') ->whereNull('a.delete_time'); $accounts = $accountsQuery ->field('a.id,a.corp_id,a.corp_name,a.agent_id,a.auth_status,a.owner_admin_id,a.dept_id,a.authorized_at,a.last_refresh_at,a.create_time,u.name as owner_name,d.name as dept_name') ->order('a.auth_status', 'desc') ->order('a.id', 'desc') ->select()->toArray(); foreach ($accounts as &$account) { $account['corp_id_masked'] = self::mask((string) ($account['corp_id'] ?? '')); unset($account['corp_id']); } unset($account); $poolsQuery = Db::name('qywx_promotion_pool')->alias('p') ->leftJoin('admin u', 'u.id = p.owner_admin_id') ->leftJoin('dept d', 'd.id = p.dept_id') ->whereNull('p.delete_time'); self::applyOwnerScope($poolsQuery, 'p', $visibleIds); $pools = $poolsQuery ->field('p.id,p.name,p.public_key,p.status,p.fallback_url,p.click_count,p.owner_admin_id,p.dept_id,p.create_time,p.update_time,u.name as owner_name,d.name as dept_name') ->order('p.id', 'desc') ->select()->toArray(); $poolIds = array_values(array_filter(array_map('intval', array_column($pools, 'id')))); $links = []; if ($poolIds !== []) { $links = Db::name('qywx_promotion_link')->alias('l') ->leftJoin('qywx_promotion_account a', 'a.id = l.account_id AND a.delete_time IS NULL') ->whereNull('l.delete_time') ->whereIn('l.pool_id', $poolIds) ->field('l.id,l.pool_id,l.account_id,l.name,l.group_name,l.wecom_url,l.weight,l.status,l.daily_limit,l.today_count,l.today_date,l.active_start,l.active_end,l.click_count,l.last_click_time,l.remark,l.create_time,l.update_time,a.corp_name,a.auth_status') ->order('l.status', 'desc') ->order('l.weight', 'desc') ->order('l.id', 'desc') ->select()->toArray(); } $domain = rtrim($domain, '/'); foreach ($pools as &$pool) { $key = (string) $pool['public_key']; $scriptUrl = $domain . '/api/qywx-promotion/js/' . $key; $goUrl = $domain . '/api/qywx-promotion/go/' . $key; $pool['script_url'] = $scriptUrl; $pool['go_url'] = $goUrl; $pool['install_code'] = ''; $pool['trigger_code'] = '添加企业微信'; } unset($pool); $today = date('Y-m-d'); $todayClicks = 0; $onlineLinks = 0; foreach ($links as $link) { if ((int) ($link['status'] ?? 0) === 1) { $onlineLinks++; } if ((string) ($link['today_date'] ?? '') === $today) { $todayClicks += (int) ($link['today_count'] ?? 0); } } $config = QywxPromotionOpenWorkService::configurationStatus(); $config['provider_callback_url'] = $domain . '/api/qywx-promotion/provider/callback'; $config['auth_callback_url'] = QywxPromotionOpenWorkService::configuredRedirectUri( $domain . '/api/qywx-promotion/auth/callback' ); return [ 'meta' => [ 'scope_label' => DataScopeService::scopeLabel(DataScopeService::getEffectiveScope($adminInfo)), 'can_authorize' => self::canAuthorize($adminId, $adminInfo), 'generated_at' => date('Y-m-d H:i:s'), ], 'config' => $config, 'summary' => [ 'authorized_accounts' => count(array_filter($accounts, static fn (array $row): bool => (int) ($row['auth_status'] ?? 0) === 1)), 'pool_count' => count($pools), 'online_links' => $onlineLinks, 'today_clicks' => $todayClicks, ], 'accounts' => $accounts, 'pools' => $pools, 'links' => $links, 'allowed_link_hosts' => array_values((array) config('qywx_promotion.allowed_link_hosts', [])), ]; } public static function authorizationUrl(int $adminId, array $adminInfo, string $domain): array { if (!self::canAuthorize($adminId, $adminInfo)) { throw new RuntimeException('只有系统管理员可以发起企业微信应用授权'); } $redirectUri = rtrim($domain, '/') . '/api/qywx-promotion/auth/callback'; return ['url' => QywxPromotionOpenWorkService::authorizationUrl($adminId, $redirectUri)]; } public static function verifyAccount(int $id, int $adminId, array $adminInfo): array { if (!self::canAuthorize($adminId, $adminInfo)) { throw new RuntimeException('只有系统管理员可以验证企业微信授权凭证'); } self::assertAuthorizedAccount($id, false); return QywxPromotionOpenWorkService::verifyAccount($id); } public static function savePool(array $params, int $adminId, array $adminInfo): array { $id = max(0, (int) ($params['id'] ?? 0)); $name = trim((string) ($params['name'] ?? '')); if ($name === '' || mb_strlen($name) > 60) { throw new RuntimeException('请输入 1-60 个字符的分流方案名称'); } $fallback = trim((string) ($params['fallback_url'] ?? '')); if (!QywxPromotionOpenWorkService::isAllowedPromotionUrl($fallback, true)) { throw new RuntimeException('兜底链接必须是已允许的 HTTPS 企业微信链接'); } $now = time(); $data = [ 'name' => $name, 'status' => (int) ($params['status'] ?? 1) === 1 ? 1 : 0, 'fallback_url' => $fallback, 'update_time' => $now, ]; if ($id > 0) { self::assertScopedRow('qywx_promotion_pool', $id, $adminId, $adminInfo); Db::name('qywx_promotion_pool')->where('id', $id)->update($data); } else { $data += [ 'public_key' => bin2hex(random_bytes(16)), 'owner_admin_id' => $adminId, 'dept_id' => self::primaryDeptId($adminId), 'click_count' => 0, 'create_time' => $now, ]; $id = (int) Db::name('qywx_promotion_pool')->insertGetId($data); } return ['id' => $id]; } public static function deletePool(int $id, int $adminId, array $adminInfo): void { self::assertScopedRow('qywx_promotion_pool', $id, $adminId, $adminInfo); $now = time(); Db::transaction(function () use ($id, $now): void { Db::name('qywx_promotion_pool')->where('id', $id)->update(['delete_time' => $now, 'update_time' => $now]); Db::name('qywx_promotion_link')->where('pool_id', $id)->whereNull('delete_time')->update(['delete_time' => $now, 'update_time' => $now]); }); } public static function saveLink(array $params, int $adminId, array $adminInfo): array { $id = max(0, (int) ($params['id'] ?? 0)); $poolId = max(0, (int) ($params['pool_id'] ?? 0)); $pool = self::assertScopedRow('qywx_promotion_pool', $poolId, $adminId, $adminInfo); $name = trim((string) ($params['name'] ?? '')); if ($name === '' || mb_strlen($name) > 80) { throw new RuntimeException('请输入 1-80 个字符的推广链接名称'); } $url = trim((string) ($params['wecom_url'] ?? '')); if (!QywxPromotionOpenWorkService::isAllowedPromotionUrl($url)) { throw new RuntimeException('推广链接必须是已允许的 HTTPS 企业微信链接'); } $accountId = max(0, (int) ($params['account_id'] ?? 0)); if ($accountId > 0) { self::assertAuthorizedAccount($accountId, true); } $startAt = self::parseTime($params['active_start'] ?? null); $endAt = self::parseTime($params['active_end'] ?? null); if ($startAt > 0 && $endAt > 0 && $startAt >= $endAt) { throw new RuntimeException('生效结束时间必须晚于开始时间'); } $now = time(); $data = [ 'pool_id' => $poolId, 'account_id' => $accountId, 'name' => $name, 'group_name' => mb_substr(trim((string) ($params['group_name'] ?? '默认分组')), 0, 60), 'wecom_url' => $url, 'weight' => min(100, max(1, (int) ($params['weight'] ?? 1))), 'status' => (int) ($params['status'] ?? 1) === 1 ? 1 : 0, 'daily_limit' => min(1000000, max(0, (int) ($params['daily_limit'] ?? 0))), 'active_start' => $startAt, 'active_end' => $endAt, 'remark' => mb_substr(trim((string) ($params['remark'] ?? '')), 0, 255), 'update_time' => $now, ]; if ($id > 0) { self::assertScopedRow('qywx_promotion_link', $id, $adminId, $adminInfo); Db::name('qywx_promotion_link')->where('id', $id)->update($data); } else { $data += [ 'owner_admin_id' => (int) ($pool['owner_admin_id'] ?? 0) ?: $adminId, 'dept_id' => (int) ($pool['dept_id'] ?? 0) ?: self::primaryDeptId($adminId), 'click_count' => 0, 'today_count' => 0, 'today_date' => null, 'last_click_time' => 0, 'create_time' => $now, ]; $id = (int) Db::name('qywx_promotion_link')->insertGetId($data); } return ['id' => $id]; } public static function toggleLink(int $id, int $status, int $adminId, array $adminInfo): void { self::assertScopedRow('qywx_promotion_link', $id, $adminId, $adminInfo); Db::name('qywx_promotion_link')->where('id', $id)->update([ 'status' => $status === 1 ? 1 : 0, 'update_time' => time(), ]); } public static function deleteLink(int $id, int $adminId, array $adminInfo): void { self::assertScopedRow('qywx_promotion_link', $id, $adminId, $adminInfo); Db::name('qywx_promotion_link')->where('id', $id)->update([ 'delete_time' => time(), 'update_time' => time(), ]); } private static function assertScopedRow(string $table, int $id, int $adminId, array $adminInfo): array { if ($id <= 0) { throw new RuntimeException('数据不存在'); } $query = Db::name($table)->where('id', $id)->whereNull('delete_time'); $visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo); if ($visibleIds !== null) { if ($visibleIds === []) { throw new RuntimeException('无权访问该数据'); } $query->whereIn('owner_admin_id', $visibleIds); } $row = $query->find(); if (!$row) { throw new RuntimeException('数据不存在或超出当前权限范围'); } return $row; } private static function assertAuthorizedAccount(int $id, bool $requireActive): array { if ($id <= 0) { throw new RuntimeException('授权企业不存在'); } $query = Db::name('qywx_promotion_account')->where('id', $id)->whereNull('delete_time'); if ($requireActive) { $query->where('auth_status', 1); } $row = $query->find(); if (!$row) { throw new RuntimeException($requireActive ? '授权企业无效或已取消授权' : '授权企业不存在'); } return $row; } private static function applyOwnerScope($query, string $alias, ?array $visibleIds): void { if ($visibleIds === null) { return; } if ($visibleIds === []) { $query->whereRaw('1 = 0'); return; } $query->whereIn($alias . '.owner_admin_id', $visibleIds); } private static function canAuthorize(int $adminId, array $adminInfo): bool { if ((int) ($adminInfo['root'] ?? 0) === 1) { return true; } return Db::name('admin_role')->alias('ar') ->join('system_role r', 'r.id = ar.role_id AND r.delete_time IS NULL') ->where('ar.admin_id', $adminId) ->where('r.name', '管理员') ->count() > 0; } private static function primaryDeptId(int $adminId): int { return (int) (Db::name('admin_dept')->where('admin_id', $adminId)->order('dept_id', 'asc')->value('dept_id') ?? 0); } private static function parseTime(mixed $value): int { if ($value === null || $value === '') { return 0; } if (is_numeric($value)) { return max(0, (int) $value); } $time = strtotime((string) $value); return $time === false ? 0 : $time; } private static function mask(string $value): string { $length = strlen($value); if ($length <= 8) { return $value === '' ? '' : str_repeat('*', $length); } return substr($value, 0, 4) . str_repeat('*', max(4, $length - 8)) . substr($value, -4); } }