|
|
@@ -0,0 +1,189 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Services\Agent;
|
|
|
+
|
|
|
+use App\Models\Agent\Agent;
|
|
|
+use App\Models\Agent\AgentDomain;
|
|
|
+use App\Models\Agent\AgentDomainPool;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 代理域名池(总后台「代理域名管理」):
|
|
|
+ * - 批量添加可用域名,按主机名去重;
|
|
|
+ * - 列表显示使用情况:绑定到哪个代理、PC 还是 H5(按 host 和 agent_domains 匹配);
|
|
|
+ * - 使用中的域名不能删除;停用的域名不能再绑定(已绑定的不受影响,需要先到代理列表解绑);
|
|
|
+ * - 代理列表直接输入并绑定的域名会自动登记到域名池(AgentDomainService::save)。
|
|
|
+ */
|
|
|
+class AgentDomainPoolService
|
|
|
+{
|
|
|
+ private const MAX_BATCH = 100;
|
|
|
+
|
|
|
+ public function __construct(private AgentDomainService $domains)
|
|
|
+ {
|
|
|
+ }
|
|
|
+
|
|
|
+ public function paginate(array $params): array
|
|
|
+ {
|
|
|
+ $page = max(1, (int) ($params['page'] ?? 1));
|
|
|
+ $limit = min(100, max(1, (int) ($params['limit'] ?? 20)));
|
|
|
+ $query = AgentDomainPool::query();
|
|
|
+ if (!empty($params['keyword'])) $query->where('domain', 'like', '%' . trim((string) $params['keyword']) . '%');
|
|
|
+ if (isset($params['type']) && $params['type'] !== null) $query->where('type', (string) $params['type']);
|
|
|
+ if (isset($params['status']) && $params['status'] !== '') $query->where('status', (int) $params['status']);
|
|
|
+ $bound = fn ($q) => $q->select(DB::raw(1))->from('agent_domains')->whereColumn('agent_domains.host', 'agent_domain_pool.host');
|
|
|
+ if (($params['usage'] ?? '') === 'in_use') $query->whereExists($bound);
|
|
|
+ if (($params['usage'] ?? '') === 'available') $query->whereNotExists($bound);
|
|
|
+ if (!empty($params['agent_id']) || !empty($params['agent_username'])) {
|
|
|
+ $query->whereIn('host', AgentDomain::query()->select('host')->whereIn('agent_id', function ($q) use ($params) {
|
|
|
+ $q->select('id')->from('agents');
|
|
|
+ if (!empty($params['agent_id'])) $q->where('id', (int) $params['agent_id']);
|
|
|
+ if (!empty($params['agent_username'])) $q->where('username', 'like', '%' . trim((string) $params['agent_username']) . '%');
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ $total = (clone $query)->count();
|
|
|
+ $rows = $query->orderByDesc('id')->forPage($page, $limit)->get();
|
|
|
+ $bindings = $this->bindings($rows->pluck('host')->all());
|
|
|
+ $list = $rows->map(fn (AgentDomainPool $row) => $this->present($row, $bindings[$row->host] ?? []))->all();
|
|
|
+ return compact('total', 'page', 'limit', 'list') + ['summary' => $this->summary()];
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 绑定代理时的下拉:启用、未被使用、类型匹配(通用的两种都可用)。 */
|
|
|
+ public function options(?string $type): array
|
|
|
+ {
|
|
|
+ $query = AgentDomainPool::query()->where('status', 1)
|
|
|
+ ->whereNotExists(fn ($q) => $q->select(DB::raw(1))->from('agent_domains')->whereColumn('agent_domains.host', 'agent_domain_pool.host'));
|
|
|
+ if ($type) $query->whereIn('type', ['', $type]);
|
|
|
+ return $query->orderByDesc('id')->limit(500)->get(['id', 'domain', 'type', 'remark'])
|
|
|
+ ->map(fn (AgentDomainPool $row) => ['id' => $row->id, 'domain' => $row->domain, 'type' => $row->type,
|
|
|
+ 'type_text' => AgentDomainPool::TYPE_TEXT[$row->type] ?? '', 'remark' => $row->remark])->all();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 批量添加:每行一个域名;已存在或重复的跳过并说明原因。 */
|
|
|
+ public function create(array $domains, string $type, string $remark, ?int $adminId): array
|
|
|
+ {
|
|
|
+ $domains = array_values(array_filter(array_map(fn ($d) => trim((string) $d), $domains), fn ($d) => $d !== ''));
|
|
|
+ if ($domains === []) throw new \InvalidArgumentException('请输入域名');
|
|
|
+ if (count($domains) > self::MAX_BATCH) throw new \InvalidArgumentException('一次最多添加 ' . self::MAX_BATCH . ' 个域名');
|
|
|
+ $created = [];
|
|
|
+ $skipped = [];
|
|
|
+ DB::transaction(function () use ($domains, $type, $remark, $adminId, &$created, &$skipped) {
|
|
|
+ $seen = [];
|
|
|
+ foreach ($domains as $domain) {
|
|
|
+ try {
|
|
|
+ $value = $this->domains->normalize($domain);
|
|
|
+ } catch (\InvalidArgumentException $e) {
|
|
|
+ $skipped[] = ['domain' => $domain, 'reason' => '域名格式错误'];
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $host = $this->domains->host($value);
|
|
|
+ if (isset($seen[$host])) { $skipped[] = ['domain' => $domain, 'reason' => '本次输入重复']; continue; }
|
|
|
+ $seen[$host] = true;
|
|
|
+ if (AgentDomainPool::query()->where('host', $host)->lockForUpdate()->exists()) {
|
|
|
+ $skipped[] = ['domain' => $domain, 'reason' => '域名已存在']; continue;
|
|
|
+ }
|
|
|
+ $created[] = AgentDomainPool::query()->create(['domain' => $value, 'host' => $host, 'type' => $type,
|
|
|
+ 'status' => 1, 'remark' => $remark, 'created_by' => $adminId])->id;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return ['created' => count($created), 'skipped' => $skipped];
|
|
|
+ }
|
|
|
+
|
|
|
+ public function update(int $id, array $data): array
|
|
|
+ {
|
|
|
+ return DB::transaction(function () use ($id, $data) {
|
|
|
+ $row = AgentDomainPool::query()->lockForUpdate()->findOrFail($id);
|
|
|
+ if (array_key_exists('domain', $data) && $data['domain'] !== null && trim((string) $data['domain']) !== $row->domain) {
|
|
|
+ $value = $this->domains->normalize((string) $data['domain']);
|
|
|
+ $host = $this->domains->host($value);
|
|
|
+ if ($host !== $row->host) {
|
|
|
+ if ($this->inUse($row->host)) throw new \RuntimeException('域名使用中,不能修改为其他域名,请先到代理列表解绑');
|
|
|
+ if (AgentDomainPool::query()->where('host', $host)->whereKeyNot($row->id)->exists()) throw new \RuntimeException('域名已存在');
|
|
|
+ }
|
|
|
+ $row->domain = $value;
|
|
|
+ $row->host = $host;
|
|
|
+ }
|
|
|
+ foreach (['type', 'remark'] as $field) {
|
|
|
+ if (array_key_exists($field, $data) && $data[$field] !== null) $row->{$field} = (string) $data[$field];
|
|
|
+ }
|
|
|
+ if (array_key_exists('status', $data) && $data['status'] !== null) $row->status = (int) $data['status'];
|
|
|
+ $row->save();
|
|
|
+ return $this->present($row, $this->bindings([$row->host])[$row->host] ?? []);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ public function delete(int $id): void
|
|
|
+ {
|
|
|
+ DB::transaction(function () use ($id) {
|
|
|
+ $row = AgentDomainPool::query()->lockForUpdate()->findOrFail($id);
|
|
|
+ if ($this->inUse($row->host)) throw new \RuntimeException('域名使用中,不能删除,请先到代理列表解绑');
|
|
|
+ $row->delete();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 代理列表绑定域名前调用:停用的不能绑定;没登记的自动登记。调用方已在事务内。 */
|
|
|
+ public function register(string $domain, string $host, string $slotType, ?int $adminId): void
|
|
|
+ {
|
|
|
+ $row = AgentDomainPool::query()->where('host', $host)->lockForUpdate()->first();
|
|
|
+ if ($row) {
|
|
|
+ if ((int) $row->status !== 1) throw new \RuntimeException('该域名已停用,不能绑定');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ AgentDomainPool::query()->create(['domain' => $domain, 'host' => $host, 'type' => $slotType, 'status' => 1,
|
|
|
+ 'remark' => '代理列表绑定时自动登记', 'created_by' => $adminId]);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function inUse(string $host): bool
|
|
|
+ {
|
|
|
+ return AgentDomain::query()->where('host', $host)->exists();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** host => [绑定信息…](同一代理的 PC 和 H5 可能绑定同一主机名)。 */
|
|
|
+ private function bindings(array $hosts): array
|
|
|
+ {
|
|
|
+ if ($hosts === []) return [];
|
|
|
+ $agents = [];
|
|
|
+ $result = [];
|
|
|
+ $rows = AgentDomain::query()->whereIn('host', $hosts)->orderBy('id')->get();
|
|
|
+ foreach (Agent::query()->whereIn('id', $rows->pluck('agent_id')->unique())->get(['id', 'username', 'real_name', 'type', 'status']) as $agent) {
|
|
|
+ $agents[$agent->id] = $agent;
|
|
|
+ }
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ $agent = $agents[$row->agent_id] ?? null;
|
|
|
+ $result[$row->host][] = [
|
|
|
+ 'agent_id' => (int) $row->agent_id,
|
|
|
+ 'agent_username' => $agent?->username ?? '',
|
|
|
+ 'agent_real_name' => $agent?->real_name ?? '',
|
|
|
+ 'agent_type' => $agent ? (int) $agent->type : null,
|
|
|
+ 'agent_type_text' => $agent?->type_text ?? '',
|
|
|
+ 'agent_status' => $agent ? (int) $agent->status : null,
|
|
|
+ 'slot' => $row->type,
|
|
|
+ 'slot_text' => AgentDomainPool::TYPE_TEXT[$row->type] ?? $row->type,
|
|
|
+ 'bound_domain' => $row->domain,
|
|
|
+ 'bound_at' => $row->created_at ? (string) $row->created_at : null,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function present(AgentDomainPool $row, array $bindings): array
|
|
|
+ {
|
|
|
+ $inUse = $bindings !== [];
|
|
|
+ return [
|
|
|
+ 'id' => (int) $row->id, 'domain' => $row->domain, 'host' => $row->host,
|
|
|
+ 'type' => $row->type, 'type_text' => AgentDomainPool::TYPE_TEXT[$row->type] ?? '',
|
|
|
+ 'status' => (int) $row->status, 'status_text' => (int) $row->status === 1 ? '启用' : '停用',
|
|
|
+ 'remark' => $row->remark, 'in_use' => $inUse, 'usage_text' => $inUse ? '使用中' : ((int) $row->status === 1 ? '可用' : '停用'),
|
|
|
+ 'bindings' => $bindings, 'created_by' => $row->created_by,
|
|
|
+ 'created_at' => $row->created_at ? (string) $row->created_at : null, 'updated_at' => $row->updated_at ? (string) $row->updated_at : null,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ private function summary(): array
|
|
|
+ {
|
|
|
+ $bound = fn ($q) => $q->select(DB::raw(1))->from('agent_domains')->whereColumn('agent_domains.host', 'agent_domain_pool.host');
|
|
|
+ $total = AgentDomainPool::query()->count();
|
|
|
+ $inUse = AgentDomainPool::query()->whereExists($bound)->count();
|
|
|
+ $disabled = AgentDomainPool::query()->where('status', 0)->whereNotExists($bound)->count();
|
|
|
+ return ['total' => $total, 'in_use' => $inUse, 'available' => $total - $inUse - $disabled, 'disabled' => $disabled];
|
|
|
+ }
|
|
|
+}
|