功能:用户列表增加在线状态列,支持点击排序
- UserManagerController 注入 ChatStateService,从 Redis 聚合 所有活跃房间在线用户名(跨房间去重) - 排序白名单加入 'online',在线排序用 orderByRaw CASE WHEN 虚拟列 desc = 在线用户优先显示,asc = 离线用户优先 - 视图表头加「在线 ↕」可排序列(绿色高亮箭头) - 每行显示绿色实心点+「在线」/灰点+「离线」小徽章 - my-duty-logs 分页已有 paginate(30)+withQueryString+links(),无需改动
This commit is contained in:
@@ -14,6 +14,7 @@ namespace App\Http\Controllers\Admin;
|
|||||||
use App\Enums\CurrencySource;
|
use App\Enums\CurrencySource;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\ChatStateService;
|
||||||
use App\Services\UserCurrencyService;
|
use App\Services\UserCurrencyService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
@@ -25,14 +26,15 @@ use Illuminate\View\View;
|
|||||||
class UserManagerController extends Controller
|
class UserManagerController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 注入统一积分服务(用于管理员调整经验/金币/魅力时记录流水)
|
* 注入统一积分服务和聊天室状态服务
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly UserCurrencyService $currencyService,
|
private readonly UserCurrencyService $currencyService,
|
||||||
|
private readonly ChatStateService $chatState,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示用户列表及搜索(支持按等级/经验/金币/魅力排序)
|
* 显示用户列表及搜索(支持按等级/经验/金币/魅力/在线状态排序)
|
||||||
*/
|
*/
|
||||||
public function index(Request $request): View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
@@ -42,21 +44,41 @@ class UserManagerController extends Controller
|
|||||||
$query->where('username', 'like', '%'.$request->input('username').'%');
|
$query->where('username', 'like', '%'.$request->input('username').'%');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 Redis 获取所有在线用户名(跨所有房间去重)
|
||||||
|
$onlineUsernames = collect();
|
||||||
|
foreach ($this->chatState->getAllActiveRoomIds() as $roomId) {
|
||||||
|
$onlineUsernames = $onlineUsernames->merge(array_keys($this->chatState->getRoomUsers($roomId)));
|
||||||
|
}
|
||||||
|
$onlineUsernames = $onlineUsernames->unique()->values();
|
||||||
|
|
||||||
// 排序:允许的字段白名单,防止 SQL 注入
|
// 排序:允许的字段白名单,防止 SQL 注入
|
||||||
$sortable = ['user_level', 'exp_num', 'jjb', 'meili', 'id'];
|
$sortable = ['user_level', 'exp_num', 'jjb', 'meili', 'id', 'online'];
|
||||||
$sortBy = in_array($request->input('sort_by'), $sortable) ? $request->input('sort_by') : 'id';
|
$sortBy = in_array($request->input('sort_by'), $sortable) ? $request->input('sort_by') : 'id';
|
||||||
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
|
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
|
||||||
|
|
||||||
|
if ($sortBy === 'online') {
|
||||||
|
// 用虚拟列排序:在线用户标记为 1,离线为 0;desc = 在线优先
|
||||||
|
if ($onlineUsernames->isNotEmpty()) {
|
||||||
|
$placeholders = implode(',', array_fill(0, $onlineUsernames->count(), '?'));
|
||||||
|
$query->orderByRaw(
|
||||||
|
"CASE WHEN username IN ({$placeholders}) THEN 1 ELSE 0 END {$sortDir}",
|
||||||
|
$onlineUsernames->toArray(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$query->orderBy('id', 'desc'); // 二级排序
|
||||||
|
} else {
|
||||||
|
$query->orderBy($sortBy, $sortDir);
|
||||||
|
}
|
||||||
|
|
||||||
$users = $query
|
$users = $query
|
||||||
->with(['activePosition.position.department', 'vipLevel'])
|
->with(['activePosition.position.department', 'vipLevel'])
|
||||||
->orderBy($sortBy, $sortDir)
|
|
||||||
->paginate(20)
|
->paginate(20)
|
||||||
->withQueryString();
|
->withQueryString();
|
||||||
|
|
||||||
// VIP 等级选项列表(供编辑弹窗使用)
|
// VIP 等级选项列表(供编辑弹窗使用)
|
||||||
$vipLevels = \App\Models\VipLevel::orderBy('sort_order')->get();
|
$vipLevels = \App\Models\VipLevel::orderBy('sort_order')->get();
|
||||||
|
|
||||||
return view('admin.users.index', compact('users', 'vipLevels', 'sortBy', 'sortDir'));
|
return view('admin.users.index', compact('users', 'vipLevels', 'sortBy', 'sortDir', 'onlineUsernames'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th class="p-4">注册时间</th>
|
<th class="p-4">注册时间</th>
|
||||||
|
<th class="p-4">
|
||||||
|
<a href="{{ $sortLink('online') }}" class="hover:text-green-600 flex items-center gap-1">
|
||||||
|
在线<span class="text-green-500">{{ $arrow('online') }}</span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th class="p-4 text-right">管理操作</th>
|
<th class="p-4 text-right">管理操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -114,6 +119,16 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="p-4 text-sm font-mono text-gray-500">{{ $user->created_at->format('Y/m/d H:i') }}
|
<td class="p-4 text-sm font-mono text-gray-500">{{ $user->created_at->format('Y/m/d H:i') }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="p-4">
|
||||||
|
@php $isOnline = $onlineUsernames->contains($user->username); @endphp
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1.5 text-xs font-bold px-2 py-0.5 rounded-full
|
||||||
|
{{ $isOnline ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-400' }}">
|
||||||
|
<span
|
||||||
|
class="w-1.5 h-1.5 rounded-full {{ $isOnline ? 'bg-green-500' : 'bg-gray-300' }}"></span>
|
||||||
|
{{ $isOnline ? '在线' : '离线' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td class="p-4 text-right space-x-2 relative">
|
<td class="p-4 text-right space-x-2 relative">
|
||||||
<button
|
<button
|
||||||
@click="editingUser = {
|
@click="editingUser = {
|
||||||
@@ -182,7 +197,8 @@
|
|||||||
{{-- 经验 --}}
|
{{-- 经验 --}}
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-gray-600 mb-1">经验值</label>
|
<label class="block text-xs font-bold text-gray-600 mb-1">经验值</label>
|
||||||
<input type="number" name="exp_num" x-model="editingUser.exp_num" required min="0"
|
<input type="number" name="exp_num" x-model="editingUser.exp_num" required
|
||||||
|
min="0"
|
||||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||||
</div>
|
</div>
|
||||||
{{-- 金币 --}}
|
{{-- 金币 --}}
|
||||||
|
|||||||
Reference in New Issue
Block a user