init
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user