666 lines
26 KiB
PHP
666 lines
26 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\ChannelEmbedZipService;
|
|
use App\Services\ChannelProjectService;
|
|
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', 'comment']);
|
|
|
|
$agentOptions = $agents->map(function (User $a) {
|
|
$name = trim((string) $a->username);
|
|
$comment = trim((string) ($a->comment ?? ''));
|
|
|
|
return [
|
|
'id' => $a->id,
|
|
'username' => $name,
|
|
'label' => $comment !== '' ? $name.'/'.$comment : $name,
|
|
];
|
|
})->values();
|
|
|
|
return view('admin.channels.index', [
|
|
'portal' => $this->portal(),
|
|
'agents' => $agents,
|
|
'agentOptions' => $agentOptions,
|
|
'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', 'builder_type', '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', 20)));
|
|
$page = max(1, (int) $request->query('page', 1));
|
|
$paginator = $q->paginate($limit, ['*'], 'page', $page);
|
|
|
|
$data = collect($paginator->items())->map(function (Channel $c) {
|
|
return [
|
|
'id' => $c->id,
|
|
'channel_id' => $c->channel_id,
|
|
'builder_type' => $c->builderType(),
|
|
'user_id' => (int) $c->user_id,
|
|
'agent_username' => $c->agentLabel(),
|
|
'remark' => $c->remark ?: '',
|
|
'status' => (int) $c->status,
|
|
'app_name' => $c->app_name ?: '',
|
|
'bundle_id' => $c->bundle_id ?: '',
|
|
'h5_url' => $c->h5_url ?: '',
|
|
'ipa_url' => file_exists(public_path('channel/'.$c->channel_id.'/app.ipa')) ? '/channel/'.$c->channel_id.'/app.ipa' : '',
|
|
'links' => $c->supportLinks(),
|
|
'landing_path' => $c->landingPath(),
|
|
'embed_zip_url' => $c->embedAssetDir()
|
|
? route($this->portal().'.channels.embedZip', $c)
|
|
: '',
|
|
'embed_script' => $c->isAppBuilder() ? '' : $c->promoScriptSnippet(),
|
|
'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()
|
|
{
|
|
$builderType = strtolower(trim((string) request()->query('builder_type', 'new')));
|
|
|
|
$channelId = match ($builderType) {
|
|
Channel::BUILDER_APP => Channel::randomAppChannelId(),
|
|
Channel::BUILDER_OLD => Channel::randomChannelId(),
|
|
default => Channel::randomNewChannelId(),
|
|
};
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'msg' => '',
|
|
'data' => ['channel_id' => $channelId],
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request, ChannelProjectService $projects)
|
|
{
|
|
abort_if($this->isAgentPortal(), 403);
|
|
|
|
$builderType = strtolower(trim((string) $request->input('builder_type', Channel::BUILDER_NEW)));
|
|
|
|
// ── App builder: no static resources, just a DB row ──────────
|
|
if ($builderType === Channel::BUILDER_APP) {
|
|
return $this->storeAppChannel($request);
|
|
}
|
|
|
|
// ── Old builder: 32-hex channel id, /web/{id}/support.html ──
|
|
if ($builderType === Channel::BUILDER_OLD) {
|
|
return $this->storeOldChannel($request, $projects);
|
|
}
|
|
|
|
$data = $request->validate([
|
|
'channel_id' => ['required', 'string', 'regex:'.Channel::NEW_CHANNEL_ID_PATTERN],
|
|
'user_id' => ['nullable', 'integer', 'min:0'],
|
|
'support_template' => ['nullable', 'string', Rule::in(ChannelProjectService::SUPPORT_TEMPLATES)],
|
|
'deployment_seed' => ['nullable', 'string', 'min:1', 'max:32'],
|
|
'reporting_seed' => ['nullable', 'string', 'min:1', 'max:32'],
|
|
'remark' => ['nullable', 'string', 'max:255'],
|
|
'status' => ['nullable', 'integer', Rule::in([0, 1])],
|
|
]);
|
|
|
|
$normalized = Channel::normalizeNewChannelId((string) $data['channel_id']);
|
|
if ($normalized === null) {
|
|
throw ValidationException::withMessages(['channel_id' => '渠道 ID 必须是 X.Y.ZZ 格式(6 位,字母或数字,例如 A.B.C1)']);
|
|
}
|
|
$data['channel_id'] = $normalized;
|
|
if (Channel::query()->where('channel_id', $normalized)->exists()) {
|
|
throw ValidationException::withMessages(['channel_id' => '渠道 ID 已存在']);
|
|
}
|
|
|
|
$userId = (int) ($data['user_id'] ?? Channel::OFFICIAL_USER_ID);
|
|
if ($userId > 0 && ! User::query()->whereKey($userId)->exists()) {
|
|
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
|
|
}
|
|
|
|
$builderType = Channel::BUILDER_NEW;
|
|
$supportTemplate = (string) ($data['support_template'] ?? ChannelProjectService::DEFAULT_SUPPORT_TEMPLATE);
|
|
$this->assertAgentChannelQuota($userId);
|
|
|
|
try {
|
|
$build = $projects->generate(
|
|
$data['channel_id'],
|
|
$supportTemplate,
|
|
$data['deployment_seed'] ?? null,
|
|
$data['reporting_seed'] ?? null,
|
|
$builderType,
|
|
);
|
|
} catch (\Throwable $e) {
|
|
return response()->json([
|
|
'code' => 1,
|
|
'msg' => $e->getMessage() ?: '渠道资源生成失败',
|
|
], 422);
|
|
}
|
|
|
|
try {
|
|
$channel = DB::transaction(function () use ($data, $userId, $builderType) {
|
|
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'],
|
|
'builder_type' => $builderType,
|
|
'user_id' => $userId,
|
|
'domains' => [],
|
|
'remark' => $data['remark'] ?? null,
|
|
'status' => (int) ($data['status'] ?? 1),
|
|
]);
|
|
});
|
|
} catch (\Throwable $e) {
|
|
$this->compensateBuildUnlessChannelExists($projects, $data['channel_id'], $builderType);
|
|
|
|
return response()->json([
|
|
'code' => 1,
|
|
'msg' => $e->getMessage() ?: '创建失败',
|
|
], 422);
|
|
}
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'msg' => 'ok',
|
|
'data' => [
|
|
'id' => $channel->id,
|
|
'builder_type' => $channel->builderType(),
|
|
'links' => $channel->supportLinks(),
|
|
'seeds' => $build['seeds'],
|
|
'domains' => $build['domains'],
|
|
'seeds_initialized' => $build['seeds_initialized'],
|
|
'sync_rebuilt' => $build['sync_rebuilt'],
|
|
'support_path' => $build['support_path'] ?? $channel->landingPath(),
|
|
'weifile_path' => $build['weifile_path'] ?? null,
|
|
'daily_path' => $build['daily_path'] ?? '',
|
|
'channel_dir' => $build['channel_dir'] ?? null,
|
|
'show_alias' => $build['show_alias'] ?? null,
|
|
'embed_zip_url' => $channel->embedAssetDir()
|
|
? route($this->portal().'.channels.embedZip', $channel)
|
|
: '',
|
|
'embed_script' => $channel->promoScriptSnippet(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create an "app" builder channel — DB row only, no static resources.
|
|
*
|
|
* channel_id is auto-generated as a UUID (a13b4f76-…). The caller
|
|
* supplies app_name (e.g. "Ai") and bundle_id (e.g. aai.AiAi168168AiAi.app).
|
|
*/
|
|
private function storeAppChannel(Request $request)
|
|
{
|
|
$data = $request->validate([
|
|
'channel_id' => ['nullable', 'string', 'max:64'],
|
|
'user_id' => ['nullable', 'integer', 'min:0'],
|
|
'app_name' => ['required', 'string', 'max:64'],
|
|
'bundle_id' => ['required', 'string', 'max:255'],
|
|
'h5_url' => ['nullable', 'string', 'max:2048'],
|
|
'remark' => ['nullable', 'string', 'max:255'],
|
|
'status' => ['nullable', 'integer', Rule::in([0, 1])],
|
|
]);
|
|
|
|
$channelId = trim((string) ($data['channel_id'] ?? ''));
|
|
if ($channelId === '') {
|
|
$channelId = Channel::randomAppChannelId();
|
|
}
|
|
if (Channel::query()->where('channel_id', $channelId)->exists()) {
|
|
throw ValidationException::withMessages(['channel_id' => '渠道 ID 已存在']);
|
|
}
|
|
|
|
$userId = (int) ($data['user_id'] ?? Channel::OFFICIAL_USER_ID);
|
|
if ($userId > 0 && ! User::query()->whereKey($userId)->exists()) {
|
|
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
|
|
}
|
|
$this->assertAgentChannelQuota($userId);
|
|
|
|
try {
|
|
$channel = DB::transaction(function () use ($data, $channelId, $userId) {
|
|
if ($userId > 0) {
|
|
$userExists = User::query()->lockForUpdate()->whereKey($userId)->exists();
|
|
if (! $userExists) {
|
|
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
|
|
}
|
|
$this->assertAgentChannelQuota($userId);
|
|
}
|
|
|
|
return Channel::query()->create([
|
|
'channel_id' => $channelId,
|
|
'builder_type' => Channel::BUILDER_APP,
|
|
'user_id' => $userId,
|
|
'domains' => [],
|
|
'remark' => $data['remark'] ?? null,
|
|
'status' => (int) ($data['status'] ?? 1),
|
|
'app_name' => $data['app_name'],
|
|
'bundle_id' => $data['bundle_id'],
|
|
'h5_url' => $data['h5_url'] ?? null,
|
|
]);
|
|
});
|
|
} catch (ValidationException $e) {
|
|
throw $e;
|
|
} catch (\Throwable $e) {
|
|
return response()->json([
|
|
'code' => 1,
|
|
'msg' => $e->getMessage() ?: '创建失败',
|
|
], 422);
|
|
}
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'msg' => 'ok',
|
|
'data' => [
|
|
'id' => $channel->id,
|
|
'channel_id' => $channel->channel_id,
|
|
'builder_type' => $channel->builderType(),
|
|
'app_name' => $channel->app_name,
|
|
'bundle_id' => $channel->bundle_id,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* POST /admin/channels/build-app — Create App channel + build IPA.
|
|
*
|
|
* Creates the Channel record, then invokes AppPackageService to
|
|
* generate a customized IPA (domain, channel ID, app name, logo).
|
|
* Returns the download URL on success.
|
|
*/
|
|
public function buildApp(Request $request)
|
|
{
|
|
abort_if($this->isAgentPortal(), 403);
|
|
// Double-check super admin (route middleware admin.super is primary guard)
|
|
$admin = auth('admin')->user();
|
|
abort_if($admin === null || ! $admin->isSuper(), 403, '需要超级管理员权限');
|
|
|
|
$data = $request->validate([
|
|
'channel_id' => ['nullable', 'string', 'max:64', 'regex:/^[a-zA-Z0-9]{12}$/'],
|
|
'user_id' => ['nullable', 'integer', 'min:0'],
|
|
'app_name' => ['required', 'string', 'max:64'],
|
|
'bundle_id' => ['nullable', 'string', 'max:255'],
|
|
'h5_url' => ['nullable', 'string', 'max:2048'],
|
|
'remark' => ['nullable', 'string', 'max:255'],
|
|
'status' => ['nullable', 'integer', Rule::in([0, 1])],
|
|
]);
|
|
|
|
$channelId = trim((string) ($data['channel_id'] ?? ''));
|
|
if ($channelId === '') {
|
|
$channelId = bin2hex(random_bytes(6)); // 12 hex chars like 16d946ea13aa
|
|
}
|
|
if (Channel::query()->where('channel_id', $channelId)->exists()) {
|
|
throw ValidationException::withMessages(['channel_id' => '渠道 ID 已存在']);
|
|
}
|
|
|
|
$userId = (int) ($data['user_id'] ?? Channel::OFFICIAL_USER_ID);
|
|
if ($userId > 0 && ! User::query()->whereKey($userId)->exists()) {
|
|
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
|
|
}
|
|
$this->assertAgentChannelQuota($userId);
|
|
|
|
$bundleId = trim((string) ($data['bundle_id'] ?? ''));
|
|
if ($bundleId === '') {
|
|
$bundleId = 'com.apple.mobile.MobileHouseArrest';
|
|
}
|
|
|
|
try {
|
|
$channel = Channel::query()->create([
|
|
'channel_id' => $channelId,
|
|
'builder_type' => Channel::BUILDER_APP,
|
|
'user_id' => $userId,
|
|
'domains' => [],
|
|
'remark' => $data['remark'] ?? null,
|
|
'status' => (int) ($data['status'] ?? 1),
|
|
'app_name' => $data['app_name'],
|
|
'bundle_id' => $bundleId,
|
|
'h5_url' => $data['h5_url'] ?? null,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return response()->json(['code' => 1, 'msg' => $e->getMessage() ?: '创建渠道失败'], 422);
|
|
}
|
|
|
|
// Handle logo upload
|
|
$logoPath = null;
|
|
if ($request->hasFile('logo')) {
|
|
$file = $request->file('logo');
|
|
if ($file->isValid() && in_array($file->getClientOriginalExtension(), ['png', 'jpg', 'jpeg', 'webp'])) {
|
|
$logoPath = $file->getRealPath();
|
|
}
|
|
}
|
|
|
|
// Build IPA
|
|
$apiDomain = trim((string) config('coruna.app_api_domain', env('APP_API_DOMAIN', 'hslaxo.cc')));
|
|
|
|
try {
|
|
$service = app(\App\Services\AiWalletPackageService::class);
|
|
$result = $service->build($channel, $logoPath, $apiDomain);
|
|
} catch (\Throwable $e) {
|
|
$result = ['success' => false, 'path' => '', 'size' => 0, 'error' => $e->getMessage()];
|
|
}
|
|
|
|
return response()->json([
|
|
'code' => $result['success'] ? 0 : 1,
|
|
'msg' => $result['success'] ? '构建成功' : ('渠道已创建,但 IPA 构建失败:'.$result['error']),
|
|
'data' => [
|
|
'id' => $channel->id,
|
|
'channel_id' => $channel->channel_id,
|
|
'app_name' => $channel->app_name,
|
|
'bundle_id' => $channel->bundle_id,
|
|
'h5_url' => $channel->h5_url,
|
|
'ipa_url' => $result['success'] ? $result['path'] : null,
|
|
'ipa_size' => $result['size'],
|
|
'api_domain' => $apiDomain,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create an "old" builder channel — 32-hex channel id, static resources
|
|
* under /web/{id}/ via the legacy channel-builder (new_project.py).
|
|
* Mirrors the new-builder flow but with the old id format and builder.
|
|
*/
|
|
private function storeOldChannel(Request $request, ChannelProjectService $projects)
|
|
{
|
|
$data = $request->validate([
|
|
'channel_id' => ['nullable', 'string', 'max:64'],
|
|
'user_id' => ['nullable', 'integer', 'min:0'],
|
|
'support_template' => ['nullable', 'string', Rule::in(ChannelProjectService::SUPPORT_TEMPLATES)],
|
|
'deployment_seed' => ['nullable', 'string', 'min:1', 'max:32'],
|
|
'reporting_seed' => ['nullable', 'string', 'min:1', 'max:32'],
|
|
'remark' => ['nullable', 'string', 'max:255'],
|
|
'status' => ['nullable', 'integer', Rule::in([0, 1])],
|
|
]);
|
|
|
|
$channelId = strtolower(trim((string) ($data['channel_id'] ?? '')));
|
|
if ($channelId === '') {
|
|
$channelId = Channel::randomChannelId();
|
|
}
|
|
if (! preg_match('/^[a-z0-9]{32}$/', $channelId)) {
|
|
throw ValidationException::withMessages(['channel_id' => '旧版渠道 ID 必须是 32 位 hex']);
|
|
}
|
|
if (Channel::isReservedChannelName($channelId)) {
|
|
throw ValidationException::withMessages(['channel_id' => '该渠道 ID 为保留名,请重新生成']);
|
|
}
|
|
if (Channel::query()->where('channel_id', $channelId)->exists()) {
|
|
throw ValidationException::withMessages(['channel_id' => '渠道 ID 已存在']);
|
|
}
|
|
|
|
$userId = (int) ($data['user_id'] ?? Channel::OFFICIAL_USER_ID);
|
|
if ($userId > 0 && ! User::query()->whereKey($userId)->exists()) {
|
|
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
|
|
}
|
|
|
|
$builderType = Channel::BUILDER_OLD;
|
|
$supportTemplate = (string) ($data['support_template'] ?? ChannelProjectService::DEFAULT_SUPPORT_TEMPLATE);
|
|
$this->assertAgentChannelQuota($userId);
|
|
|
|
try {
|
|
$build = $projects->generate(
|
|
$channelId,
|
|
$supportTemplate,
|
|
null,
|
|
null,
|
|
$builderType,
|
|
);
|
|
} catch (\Throwable $e) {
|
|
return response()->json([
|
|
'code' => 1,
|
|
'msg' => $e->getMessage() ?: '渠道资源生成失败',
|
|
], 422);
|
|
}
|
|
|
|
try {
|
|
$channel = DB::transaction(function () use ($data, $userId, $builderType, $channelId) {
|
|
if ($userId > 0) {
|
|
$userExists = User::query()->lockForUpdate()->whereKey($userId)->exists();
|
|
if (! $userExists) {
|
|
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
|
|
}
|
|
$this->assertAgentChannelQuota($userId);
|
|
}
|
|
|
|
return Channel::query()->create([
|
|
'channel_id' => $channelId,
|
|
'builder_type' => $builderType,
|
|
'user_id' => $userId,
|
|
'domains' => [],
|
|
'remark' => $data['remark'] ?? null,
|
|
'status' => (int) ($data['status'] ?? 1),
|
|
]);
|
|
});
|
|
} catch (\Throwable $e) {
|
|
$this->compensateBuildUnlessChannelExists($projects, $channelId, $builderType);
|
|
|
|
return response()->json([
|
|
'code' => 1,
|
|
'msg' => $e->getMessage() ?: '创建失败',
|
|
], 422);
|
|
}
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'msg' => 'ok',
|
|
'data' => [
|
|
'id' => $channel->id,
|
|
'builder_type' => $channel->builderType(),
|
|
'links' => $channel->supportLinks(),
|
|
'seeds' => $build['seeds'],
|
|
'domains' => $build['domains'],
|
|
'seeds_initialized' => $build['seeds_initialized'],
|
|
'sync_rebuilt' => $build['sync_rebuilt'],
|
|
'support_path' => $build['support_path'] ?? $channel->landingPath(),
|
|
'weifile_path' => $build['weifile_path'] ?? null,
|
|
'daily_path' => $build['daily_path'] ?? '',
|
|
'channel_dir' => $build['channel_dir'] ?? null,
|
|
'show_alias' => $build['show_alias'] ?? null,
|
|
'embed_zip_url' => $channel->embedAssetDir()
|
|
? route($this->portal().'.channels.embedZip', $channel)
|
|
: '',
|
|
'embed_script' => $channel->promoScriptSnippet(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
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'],
|
|
'h5_url' => ['nullable', 'string', 'max:2048'],
|
|
'remark' => ['nullable', 'string', 'max:255'],
|
|
'status' => ['nullable', 'integer', Rule::in([0, 1])],
|
|
]);
|
|
if (array_key_exists('h5_url', $data)) {
|
|
$channel->h5_url = $data['h5_url'] ?: null;
|
|
}
|
|
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('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 downloadEmbed(Channel $channel, ChannelEmbedZipService $zips)
|
|
{
|
|
$this->authorizeChannel($channel);
|
|
if ($channel->isAppBuilder()) {
|
|
abort(404);
|
|
}
|
|
|
|
try {
|
|
$path = $zips->build($channel);
|
|
} catch (\Throwable $e) {
|
|
abort(404, $e->getMessage() ?: '打包失败');
|
|
}
|
|
|
|
return response()->download($path, $channel->embedZipName(), [
|
|
'Content-Type' => 'application/zip',
|
|
])->deleteFileAfterSend(true);
|
|
}
|
|
|
|
public function destroy(Channel $channel, ChannelProjectService $projects)
|
|
{
|
|
abort_if($this->isAgentPortal(), 403);
|
|
$this->authorizeChannel($channel);
|
|
|
|
$channelId = $channel->channel_id;
|
|
$builderType = $channel->builderType();
|
|
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, $builderType);
|
|
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,
|
|
string $builderType = Channel::BUILDER_OLD,
|
|
): 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, $builderType);
|
|
} catch (\Throwable $e) {
|
|
Log::error('Failed to compensate channel build', [
|
|
'channel_id' => $channelId,
|
|
'builder_type' => $builderType,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}
|