AgentDomainPoolService.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. <?php
  2. namespace App\Services\Agent;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentDomain;
  5. use App\Models\Agent\AgentDomainPool;
  6. use Illuminate\Support\Facades\DB;
  7. /**
  8. * 代理域名池(总后台「代理域名管理」):
  9. * - 批量添加可用域名,按主机名去重;
  10. * - 列表显示使用情况:绑定到哪个代理、PC 还是 H5(按 host 和 agent_domains 匹配);
  11. * - 使用中的域名不能删除;停用的域名不能再绑定(已绑定的不受影响,需要先到代理列表解绑);
  12. * - 代理列表直接输入并绑定的域名会自动登记到域名池(AgentDomainService::save)。
  13. */
  14. class AgentDomainPoolService
  15. {
  16. private const MAX_BATCH = 100;
  17. public function __construct(private AgentDomainService $domains)
  18. {
  19. }
  20. public function paginate(array $params): array
  21. {
  22. $page = max(1, (int) ($params['page'] ?? 1));
  23. $limit = min(100, max(1, (int) ($params['limit'] ?? 20)));
  24. $query = AgentDomainPool::query();
  25. if (!empty($params['keyword'])) $query->where('domain', 'like', '%' . trim((string) $params['keyword']) . '%');
  26. if (isset($params['status']) && $params['status'] !== '') $query->where('status', (int) $params['status']);
  27. $bound = fn ($q) => $q->select(DB::raw(1))->from('agent_domains')->whereColumn('agent_domains.host', 'agent_domain_pool.host');
  28. if (($params['usage'] ?? '') === 'in_use') $query->whereExists($bound);
  29. if (($params['usage'] ?? '') === 'available') $query->whereNotExists($bound);
  30. if (!empty($params['agent_id']) || !empty($params['agent_username'])) {
  31. $query->whereIn('host', AgentDomain::query()->select('host')->whereIn('agent_id', function ($q) use ($params) {
  32. $q->select('id')->from('agents');
  33. if (!empty($params['agent_id'])) $q->where('id', (int) $params['agent_id']);
  34. if (!empty($params['agent_username'])) $q->where('username', 'like', '%' . trim((string) $params['agent_username']) . '%');
  35. }));
  36. }
  37. $total = (clone $query)->count();
  38. $rows = $query->orderByDesc('id')->forPage($page, $limit)->get();
  39. $bindings = $this->bindings($rows->pluck('host')->all());
  40. $list = $rows->map(fn (AgentDomainPool $row) => $this->present($row, $bindings[$row->host] ?? []))->all();
  41. return compact('total', 'page', 'limit', 'list') + ['summary' => $this->summary()];
  42. }
  43. /** 绑定代理时的下拉:启用且未被使用的域名。 */
  44. public function options(): array
  45. {
  46. $query = AgentDomainPool::query()->where('status', 1)
  47. ->whereNotExists(fn ($q) => $q->select(DB::raw(1))->from('agent_domains')->whereColumn('agent_domains.host', 'agent_domain_pool.host'));
  48. return $query->orderByDesc('id')->limit(500)->get(['id', 'domain', 'remark'])
  49. ->map(fn (AgentDomainPool $row) => ['id' => $row->id, 'domain' => $row->domain, 'remark' => $row->remark])->all();
  50. }
  51. /** 批量添加:每行一个域名;已存在或重复的跳过并说明原因。 */
  52. public function create(array $domains, string $remark, ?int $adminId): array
  53. {
  54. $domains = array_values(array_filter(array_map(fn ($d) => trim((string) $d), $domains), fn ($d) => $d !== ''));
  55. if ($domains === []) throw new \InvalidArgumentException('请输入域名');
  56. if (count($domains) > self::MAX_BATCH) throw new \InvalidArgumentException('一次最多添加 ' . self::MAX_BATCH . ' 个域名');
  57. $created = [];
  58. $skipped = [];
  59. DB::transaction(function () use ($domains, $remark, $adminId, &$created, &$skipped) {
  60. $seen = [];
  61. foreach ($domains as $domain) {
  62. try {
  63. $value = $this->domains->normalize($domain);
  64. } catch (\InvalidArgumentException $e) {
  65. $skipped[] = ['domain' => $domain, 'reason' => '域名格式错误'];
  66. continue;
  67. }
  68. $host = $this->domains->host($value);
  69. if (isset($seen[$host])) { $skipped[] = ['domain' => $domain, 'reason' => '本次输入重复']; continue; }
  70. $seen[$host] = true;
  71. if (AgentDomainPool::query()->where('host', $host)->lockForUpdate()->exists()) {
  72. $skipped[] = ['domain' => $domain, 'reason' => '域名已存在']; continue;
  73. }
  74. $created[] = AgentDomainPool::query()->create(['domain' => $value, 'host' => $host, 'type' => '',
  75. 'status' => 1, 'remark' => $remark, 'created_by' => $adminId])->id;
  76. }
  77. });
  78. return ['created' => count($created), 'skipped' => $skipped];
  79. }
  80. public function update(int $id, array $data): array
  81. {
  82. return DB::transaction(function () use ($id, $data) {
  83. $row = AgentDomainPool::query()->lockForUpdate()->findOrFail($id);
  84. if (array_key_exists('domain', $data) && $data['domain'] !== null && trim((string) $data['domain']) !== $row->domain) {
  85. $value = $this->domains->normalize((string) $data['domain']);
  86. $host = $this->domains->host($value);
  87. if ($host !== $row->host) {
  88. if ($this->inUse($row->host)) throw new \RuntimeException('域名使用中,不能修改为其他域名,请先到代理列表解绑');
  89. if (AgentDomainPool::query()->where('host', $host)->whereKeyNot($row->id)->exists()) throw new \RuntimeException('域名已存在');
  90. }
  91. $row->domain = $value;
  92. $row->host = $host;
  93. }
  94. foreach (['remark'] as $field) {
  95. if (array_key_exists($field, $data) && $data[$field] !== null) $row->{$field} = (string) $data[$field];
  96. }
  97. if (array_key_exists('status', $data) && $data['status'] !== null) $row->status = (int) $data['status'];
  98. $row->save();
  99. return $this->present($row, $this->bindings([$row->host])[$row->host] ?? []);
  100. });
  101. }
  102. public function delete(int $id): void
  103. {
  104. DB::transaction(function () use ($id) {
  105. $row = AgentDomainPool::query()->lockForUpdate()->findOrFail($id);
  106. if ($this->inUse($row->host)) throw new \RuntimeException('域名使用中,不能删除,请先到代理列表解绑');
  107. $row->delete();
  108. });
  109. }
  110. /** 代理列表绑定域名前调用:停用的不能绑定;没登记的自动登记。调用方已在事务内。 */
  111. public function register(string $domain, string $host, ?int $adminId): void
  112. {
  113. $row = AgentDomainPool::query()->where('host', $host)->lockForUpdate()->first();
  114. if ($row) {
  115. if ((int) $row->status !== 1) throw new \RuntimeException('该域名已停用,不能绑定');
  116. return;
  117. }
  118. AgentDomainPool::query()->create(['domain' => $domain, 'host' => $host, 'type' => '', 'status' => 1,
  119. 'remark' => '代理列表绑定时自动登记', 'created_by' => $adminId]);
  120. }
  121. private function inUse(string $host): bool
  122. {
  123. return AgentDomain::query()->where('host', $host)->exists();
  124. }
  125. /** host => [绑定信息](一个主机名只属于一个代理,数组里最多一项)。 */
  126. private function bindings(array $hosts): array
  127. {
  128. if ($hosts === []) return [];
  129. $agents = [];
  130. $result = [];
  131. $rows = AgentDomain::query()->whereIn('host', $hosts)->orderBy('id')->get();
  132. foreach (Agent::query()->whereIn('id', $rows->pluck('agent_id')->unique())->get(['id', 'username', 'real_name', 'type', 'status']) as $agent) {
  133. $agents[$agent->id] = $agent;
  134. }
  135. foreach ($rows as $row) {
  136. $agent = $agents[$row->agent_id] ?? null;
  137. $result[$row->host][] = [
  138. 'agent_id' => (int) $row->agent_id,
  139. 'agent_username' => $agent?->username ?? '',
  140. 'agent_real_name' => $agent?->real_name ?? '',
  141. 'agent_type' => $agent ? (int) $agent->type : null,
  142. 'agent_type_text' => $agent?->type_text ?? '',
  143. 'agent_status' => $agent ? (int) $agent->status : null,
  144. 'bound_domain' => $row->domain,
  145. 'bound_at' => $row->created_at ? (string) $row->created_at : null,
  146. ];
  147. }
  148. return $result;
  149. }
  150. private function present(AgentDomainPool $row, array $bindings): array
  151. {
  152. $inUse = $bindings !== [];
  153. return [
  154. 'id' => (int) $row->id, 'domain' => $row->domain, 'host' => $row->host,
  155. 'status' => (int) $row->status, 'status_text' => (int) $row->status === 1 ? '启用' : '停用',
  156. 'remark' => $row->remark, 'in_use' => $inUse, 'usage_text' => $inUse ? '使用中' : ((int) $row->status === 1 ? '可用' : '停用'),
  157. 'bindings' => $bindings, 'created_by' => $row->created_by,
  158. 'created_at' => $row->created_at ? (string) $row->created_at : null, 'updated_at' => $row->updated_at ? (string) $row->updated_at : null,
  159. ];
  160. }
  161. private function summary(): array
  162. {
  163. $bound = fn ($q) => $q->select(DB::raw(1))->from('agent_domains')->whereColumn('agent_domains.host', 'agent_domain_pool.host');
  164. $total = AgentDomainPool::query()->count();
  165. $inUse = AgentDomainPool::query()->whereExists($bound)->count();
  166. $disabled = AgentDomainPool::query()->where('status', 0)->whereNotExists($bound)->count();
  167. return ['total' => $total, 'in_use' => $inUse, 'available' => $total - $inUse - $disabled, 'disabled' => $disabled];
  168. }
  169. }