Files
coruna-lab/app/Http/Controllers/Admin/ChannelController.php
T
hashbro 74a67c3900 init
2026-08-08 05:09:01 +08:00

297 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Concerns\PortalAware;
use App\Http\Controllers\Controller;
use App\Models\Channel;
use App\Models\User;
use App\Services\ChannelProjectService;
use App\Services\SettingsService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class ChannelController extends Controller
{
use PortalAware;
public function index()
{
$agents = $this->isAgentPortal()
? collect()
: User::query()->orderBy('username')->get(['id', 'username']);
return view('admin.channels.index', [
'portal' => $this->portal(),
'agents' => $agents,
'maxPerAgent' => Channel::maxPerAgent(),
]);
}
public function data(Request $request)
{
$q = Channel::query()->with('user:id,username');
if ($agent = $this->agent()) {
$q->where('user_id', $agent->id);
}
$channelId = trim((string) $request->query('channel_id', ''));
$agentUserId = $request->query('agent_user_id');
$status = $request->query('status');
if ($channelId !== '') {
$q->where('channel_id', 'like', '%'.$channelId.'%');
}
if (! $this->isAgentPortal() && $agentUserId !== null && $agentUserId !== '') {
$q->where('user_id', (int) $agentUserId);
}
if ($status !== null && $status !== '') {
$q->where('status', (int) $status);
}
$sortable = ['id', 'channel_id', 'status', 'user_id', 'created_at', 'updated_at'];
$field = (string) $request->query('field', 'id');
$order = strtolower((string) $request->query('order', 'desc')) === 'asc' ? 'asc' : 'desc';
if (! in_array($field, $sortable, true)) {
$field = 'id';
}
$q->orderBy($field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$page = max(1, (int) $request->query('page', 1));
$paginator = $q->paginate($limit, ['*'], 'page', $page);
$data = collect($paginator->items())->map(function (Channel $c) {
$domains = Channel::normalizeDomainList($c->domains);
return [
'id' => $c->id,
'channel_id' => $c->channel_id,
'user_id' => (int) $c->user_id,
'agent_username' => $c->agentLabel(),
'domains' => $domains,
'domains_text' => implode("\n", $domains),
'remark' => $c->remark ?: '',
'status' => (int) $c->status,
'links' => $c->supportLinks(),
'created_at' => optional($c->created_at)->format('Y-m-d H:i:s'),
'updated_at' => optional($c->updated_at)->format('Y-m-d H:i:s'),
];
})->values();
return response()->json([
'code' => 0,
'msg' => '',
'count' => $paginator->total(),
'data' => $data,
]);
}
public function randomId()
{
return response()->json([
'code' => 0,
'msg' => '',
'data' => ['channel_id' => Channel::randomChannelId()],
]);
}
public function store(Request $request, ChannelProjectService $projects)
{
abort_if($this->isAgentPortal(), 403);
$data = $request->validate([
'channel_id' => ['required', 'string', 'size:32', 'regex:/^[a-z0-9]+$/', Rule::unique('channels', 'channel_id')],
'user_id' => ['nullable', 'integer', 'min:0'],
'domains' => ['nullable', 'string', 'max:4000'],
'remark' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'integer', Rule::in([0, 1])],
]);
$userId = (int) ($data['user_id'] ?? Channel::OFFICIAL_USER_ID);
if ($userId > 0 && ! User::query()->whereKey($userId)->exists()) {
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
}
$domains = SettingsService::parseDomains($data['domains'] ?? null);
$this->assertAgentChannelQuota($userId);
try {
$projects->generate($data['channel_id'], $domains);
} catch (\Throwable $e) {
return response()->json([
'code' => 1,
'msg' => $e->getMessage() ?: '渠道资源生成失败',
], 422);
}
try {
$channel = DB::transaction(function () use ($data, $userId, $domains) {
if ($userId > 0) {
$userExists = User::query()->lockForUpdate()->whereKey($userId)->exists();
if (! $userExists) {
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
}
// The user row lock serializes quota checks for this agent.
$this->assertAgentChannelQuota($userId);
}
return Channel::query()->create([
'channel_id' => $data['channel_id'],
'user_id' => $userId,
'domains' => $domains,
'remark' => $data['remark'] ?? null,
'status' => (int) ($data['status'] ?? 1),
]);
});
} catch (\Throwable $e) {
$this->compensateBuildUnlessChannelExists($projects, $data['channel_id']);
return response()->json([
'code' => 1,
'msg' => $e->getMessage() ?: '创建失败',
], 422);
}
return response()->json([
'code' => 0,
'msg' => 'ok',
'data' => [
'id' => $channel->id,
'links' => $channel->supportLinks(),
],
]);
}
public function update(Request $request, Channel $channel)
{
$this->authorizeChannel($channel);
if ($this->isAgentPortal()) {
$data = $request->validate([
'remark' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'integer', Rule::in([0, 1])],
]);
if (array_key_exists('remark', $data)) {
$channel->remark = $data['remark'];
}
if (array_key_exists('status', $data) && $data['status'] !== null) {
$channel->status = (int) $data['status'];
}
} else {
$data = $request->validate([
'user_id' => ['nullable', 'integer', 'min:0'],
'domains' => ['nullable', 'string', 'max:4000'],
'remark' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'integer', Rule::in([0, 1])],
]);
if (array_key_exists('user_id', $data)) {
$userId = (int) ($data['user_id'] ?? Channel::OFFICIAL_USER_ID);
if ($userId > 0 && ! User::query()->whereKey($userId)->exists()) {
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
}
if ($userId !== (int) $channel->user_id && $userId > 0) {
$this->assertAgentChannelQuota($userId, $channel->id);
}
$channel->user_id = $userId;
}
if (array_key_exists('domains', $data)) {
$channel->domains = SettingsService::parseDomains($data['domains']);
}
if (array_key_exists('remark', $data)) {
$channel->remark = $data['remark'];
}
if (array_key_exists('status', $data) && $data['status'] !== null) {
$channel->status = (int) $data['status'];
}
}
$channel->save();
return response()->json([
'code' => 0,
'msg' => 'ok',
'data' => ['links' => $channel->supportLinks()],
]);
}
public function destroy(Channel $channel, ChannelProjectService $projects)
{
abort_if($this->isAgentPortal(), 403);
$this->authorizeChannel($channel);
$channelId = $channel->channel_id;
try {
// Delete remotely first: a failed remote delete leaves the DB row available
// for a safe retry instead of orphaning an unreachable static project.
$projects->deleteWebTree($channelId);
DB::transaction(static fn () => $channel->delete());
} catch (\Throwable $e) {
return response()->json([
'code' => 1,
'msg' => $e->getMessage() ?: '删除失败',
], 422);
}
return response()->json(['code' => 0, 'msg' => 'ok']);
}
private function assertAgentChannelQuota(int $userId, ?int $exceptChannelId = null): void
{
if ($userId <= 0) {
return;
}
$q = Channel::query()->where('user_id', $userId);
if ($exceptChannelId) {
$q->where('id', '!=', $exceptChannelId);
}
$count = $q->count();
$max = Channel::maxPerAgent();
if ($count >= $max) {
throw ValidationException::withMessages([
'user_id' => "该代理最多只能拥有 {$max} 条渠道链接",
]);
}
}
private function authorizeChannel(Channel $channel): void
{
if ($agent = $this->agent()) {
abort_unless((int) $channel->user_id === (int) $agent->id, 403);
}
}
private function compensateBuildUnlessChannelExists(
ChannelProjectService $projects,
string $channelId
): void {
try {
// A concurrent request may have won the unique channel_id insert. Its
// project must not be removed by this request's compensation.
if (Channel::query()->where('channel_id', $channelId)->exists()) {
return;
}
} catch (\Throwable $e) {
Log::warning('Skipped channel build compensation because DB state is unknown', [
'channel_id' => $channelId,
'error' => $e->getMessage(),
]);
return;
}
try {
$projects->deleteWebTree($channelId);
} catch (\Throwable $e) {
Log::error('Failed to compensate channel build', [
'channel_id' => $channelId,
'error' => $e->getMessage(),
]);
}
}
}