feat: new chain
This commit is contained in:
@@ -46,6 +46,9 @@ class AgentUserController extends Controller
|
||||
'username' => $u->username,
|
||||
'status' => (int) $u->status,
|
||||
'comment' => $u->comment ?: '',
|
||||
'chat_id' => $u->chat_id ?: '',
|
||||
'bot_token' => $u->bot_token ?: '',
|
||||
'telegram_ready' => $u->hasTelegramBot(),
|
||||
'channels_count' => (int) $u->channels_count,
|
||||
'created_at' => optional($u->created_at)->format('Y-m-d H:i:s'),
|
||||
'updated_at' => optional($u->updated_at)->format('Y-m-d H:i:s'),
|
||||
@@ -67,6 +70,8 @@ class AgentUserController extends Controller
|
||||
'password' => ['required', 'string', 'min:6', 'max:128'],
|
||||
'comment' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['nullable', 'integer', Rule::in([0, 1])],
|
||||
'chat_id' => ['nullable', 'string', 'max:64'],
|
||||
'bot_token' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$user = User::query()->create([
|
||||
@@ -74,6 +79,8 @@ class AgentUserController extends Controller
|
||||
'password' => $data['password'],
|
||||
'comment' => $data['comment'] ?? null,
|
||||
'status' => (int) ($data['status'] ?? 1),
|
||||
'chat_id' => $this->nullableTrim($data['chat_id'] ?? null),
|
||||
'bot_token' => $this->nullableTrim($data['bot_token'] ?? null),
|
||||
]);
|
||||
|
||||
return response()->json(['code' => 0, 'msg' => 'ok', 'data' => ['id' => $user->id]]);
|
||||
@@ -85,6 +92,8 @@ class AgentUserController extends Controller
|
||||
'password' => ['nullable', 'string', 'min:6', 'max:128'],
|
||||
'comment' => ['nullable', 'string', 'max:255'],
|
||||
'status' => ['nullable', 'integer', Rule::in([0, 1])],
|
||||
'chat_id' => ['nullable', 'string', 'max:64'],
|
||||
'bot_token' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
if (array_key_exists('comment', $data)) {
|
||||
@@ -93,6 +102,15 @@ class AgentUserController extends Controller
|
||||
if (array_key_exists('status', $data) && $data['status'] !== null) {
|
||||
$agent->status = (int) $data['status'];
|
||||
}
|
||||
if (array_key_exists('chat_id', $data)) {
|
||||
$agent->chat_id = $this->nullableTrim($data['chat_id']);
|
||||
}
|
||||
if (array_key_exists('bot_token', $data)) {
|
||||
$token = $this->nullableTrim($data['bot_token']);
|
||||
// Empty string in edit form means "leave unchanged" only when field omitted;
|
||||
// explicit empty clears. UI sends current value when unchanged.
|
||||
$agent->bot_token = $token;
|
||||
}
|
||||
if (! empty($data['password'])) {
|
||||
$agent->password = $data['password'];
|
||||
}
|
||||
@@ -118,4 +136,14 @@ class AgentUserController extends Controller
|
||||
|
||||
return response()->json(['code' => 0, 'msg' => '', 'data' => $rows]);
|
||||
}
|
||||
|
||||
private function nullableTrim(mixed $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
$value = trim((string) $value);
|
||||
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,18 @@ namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Concerns\PortalAware;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Device;
|
||||
use App\Models\PageVisit;
|
||||
use App\Models\User;
|
||||
use App\Support\AgentScope;
|
||||
use Carbon\Carbon;
|
||||
use App\Services\DashboardStatsService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
use PortalAware;
|
||||
|
||||
public function __construct(
|
||||
private readonly DashboardStatsService $stats,
|
||||
) {}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$agents = $this->isAgentPortal()
|
||||
@@ -29,103 +30,19 @@ class DashboardController extends Controller
|
||||
|
||||
public function data(Request $request)
|
||||
{
|
||||
$range = (string) $request->query('range', '30d');
|
||||
[$from, $to] = $this->rangeBounds($range);
|
||||
|
||||
$channelId = trim((string) $request->query('channel_id', ''));
|
||||
$agentUserId = $this->isAgentPortal()
|
||||
? (int) ($this->agent()?->id ?? 0)
|
||||
: (int) $request->query('agent_user_id', 0);
|
||||
|
||||
$base = Device::query();
|
||||
AgentScope::applyDeviceChannelScope($base, $this->agent());
|
||||
if (! $this->isAgentPortal() && $agentUserId > 0) {
|
||||
AgentScope::applyAgentUserFilter($base, $agentUserId);
|
||||
}
|
||||
if ($channelId !== '') {
|
||||
$base->where('channel_id', 'like', '%'.$channelId.'%');
|
||||
}
|
||||
|
||||
$total = (clone $base)->count();
|
||||
$newCount = (clone $base)->whereBetween('created_at', [$from, $to])->count();
|
||||
$activeCount = (clone $base)->whereBetween('updated_at', [$from, $to])->count();
|
||||
|
||||
$visits = PageVisit::query();
|
||||
AgentScope::applyChannelIdScope($visits, $this->agent());
|
||||
if (! $this->isAgentPortal() && $agentUserId > 0) {
|
||||
AgentScope::applyChannelIdAgentUserFilter($visits, $agentUserId);
|
||||
}
|
||||
if ($channelId !== '') {
|
||||
$visits->where('channel_id', 'like', '%'.$channelId.'%');
|
||||
}
|
||||
$visits->whereBetween('created_at', [$from, $to]);
|
||||
|
||||
$pv = (clone $visits)->count();
|
||||
$uv = (int) (clone $visits)->selectRaw('COUNT(DISTINCT client_uid) as aggregate')->value('aggregate');
|
||||
|
||||
$byOs = (clone $visits)
|
||||
->select('os')
|
||||
->selectRaw('COUNT(*) as pv')
|
||||
->selectRaw('COUNT(DISTINCT client_uid) as uv')
|
||||
->groupBy('os')
|
||||
->orderByDesc('pv')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(static fn ($r) => [
|
||||
'label' => ((string) ($r->os ?? '')) !== '' ? (string) $r->os : 'Unknown',
|
||||
'pv' => (int) $r->pv,
|
||||
'uv' => (int) $r->uv,
|
||||
])
|
||||
->values();
|
||||
|
||||
$byBrowser = (clone $visits)
|
||||
->select('browser')
|
||||
->selectRaw('COUNT(*) as pv')
|
||||
->selectRaw('COUNT(DISTINCT client_uid) as uv')
|
||||
->groupBy('browser')
|
||||
->orderByDesc('pv')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(static fn ($r) => [
|
||||
'label' => ((string) ($r->browser ?? '')) !== '' ? (string) $r->browser : 'Unknown',
|
||||
'pv' => (int) $r->pv,
|
||||
'uv' => (int) $r->uv,
|
||||
])
|
||||
->values();
|
||||
$data = $this->stats->collect([
|
||||
'range' => (string) $request->query('range', '30d'),
|
||||
'channel_id' => trim((string) $request->query('channel_id', '')),
|
||||
'agent_user_id' => $this->isAgentPortal()
|
||||
? 0
|
||||
: (int) $request->query('agent_user_id', 0),
|
||||
'agent' => $this->agent(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'msg' => '',
|
||||
'data' => [
|
||||
'total' => $total,
|
||||
'new_count' => $newCount,
|
||||
'active_count' => $activeCount,
|
||||
'pv' => $pv,
|
||||
'uv' => $uv,
|
||||
'by_os' => $byOs,
|
||||
'by_browser' => $byBrowser,
|
||||
'range' => $range,
|
||||
'from' => $from->toDateTimeString(),
|
||||
'to' => $to->toDateTimeString(),
|
||||
],
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Carbon, 1: Carbon}
|
||||
*/
|
||||
private function rangeBounds(string $range): array
|
||||
{
|
||||
$now = Carbon::now();
|
||||
|
||||
return match ($range) {
|
||||
'today' => [$now->copy()->startOfDay(), $now->copy()->endOfDay()],
|
||||
'yesterday' => [
|
||||
$now->copy()->subDay()->startOfDay(),
|
||||
$now->copy()->subDay()->endOfDay(),
|
||||
],
|
||||
'7d' => [$now->copy()->subDays(6)->startOfDay(), $now->copy()->endOfDay()],
|
||||
default => [$now->copy()->subDays(29)->startOfDay(), $now->copy()->endOfDay()],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class DeviceController extends Controller
|
||||
'device_model' => $d->device_model ?: '',
|
||||
'ios_version' => $d->ios_version ?: '',
|
||||
'ip' => $d->ip ?: '',
|
||||
'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),
|
||||
@@ -143,6 +144,69 @@ class DeviceController extends Controller
|
||||
->header('Content-Type', $mime);
|
||||
}
|
||||
|
||||
public function update(Request $request, Device $device)
|
||||
{
|
||||
$this->authorizeDevice($device);
|
||||
|
||||
$data = $request->validate([
|
||||
'album_storage' => ['required', 'integer', 'in:0,1'],
|
||||
]);
|
||||
|
||||
$device->album_storage = (int) $data['album_storage'] === 1;
|
||||
$device->save();
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'msg' => 'ok',
|
||||
'data' => [
|
||||
'id' => $device->id,
|
||||
'album_storage' => $device->albumStorageEnabled() ? 1 : 0,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function clearPhotos(Device $device)
|
||||
{
|
||||
$this->authorizeDevice($device);
|
||||
|
||||
$photos = $device->photos()->get(['id', 'path']);
|
||||
$deletedFiles = 0;
|
||||
foreach ($photos as $photo) {
|
||||
$path = trim((string) ($photo->path ?? ''));
|
||||
if ($path === '') {
|
||||
continue;
|
||||
}
|
||||
if (Storage::disk('local')->exists($path)) {
|
||||
Storage::disk('local')->delete($path);
|
||||
$deletedFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
$deletedRows = $device->photos()->delete();
|
||||
|
||||
$dir = 'c2/photos/'.$device->device_id;
|
||||
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 authorizeDevice(Device $device): void
|
||||
{
|
||||
if ($agent = $this->agent()) {
|
||||
|
||||
Reference in New Issue
Block a user