优化积分流水筛选与用户管理样式
This commit is contained in:
@@ -17,6 +17,9 @@ use App\Models\UserCurrencyLog;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 类功能:提供后台全局金币/积分流水查询与多条件筛选。
|
||||
*/
|
||||
class CurrencyLogController extends Controller
|
||||
{
|
||||
/**
|
||||
@@ -26,6 +29,12 @@ class CurrencyLogController extends Controller
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = UserCurrencyLog::query()->with('user');
|
||||
$allSources = CurrencySource::cases();
|
||||
$allowedSources = collect($allSources)->map(fn (CurrencySource $source) => $source->value)->all();
|
||||
$selectedSources = collect($request->array('sources'))
|
||||
->filter(fn (string $source) => in_array($source, $allowedSources, true))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
// 查询条件过滤
|
||||
if ($request->filled('username')) {
|
||||
@@ -36,8 +45,8 @@ class CurrencyLogController extends Controller
|
||||
$query->where('currency', $request->input('currency'));
|
||||
}
|
||||
|
||||
if ($request->filled('source')) {
|
||||
$query->where('source', $request->input('source'));
|
||||
if ($selectedSources !== []) {
|
||||
$query->whereIn('source', $selectedSources);
|
||||
}
|
||||
|
||||
if ($request->filled('remark')) {
|
||||
@@ -63,8 +72,6 @@ class CurrencyLogController extends Controller
|
||||
// 默认按时间倒序
|
||||
$logs = $query->latest('id')->paginate(50)->withQueryString();
|
||||
|
||||
$allSources = CurrencySource::cases();
|
||||
|
||||
return view('admin.currency-logs.index', compact('logs', 'allSources'));
|
||||
return view('admin.currency-logs.index', compact('logs', 'allSources', 'selectedSources'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\UserCurrencyLog;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 类功能:展示后台积分流水统计与指定日期净流通数据。
|
||||
*/
|
||||
class CurrencyStatsController extends Controller
|
||||
{
|
||||
/**
|
||||
@@ -45,20 +47,7 @@ class CurrencyStatsController extends Controller
|
||||
);
|
||||
|
||||
// 今日净流通量(正向增加 - 负向消耗),可判断通货膨胀
|
||||
$netFlow = [];
|
||||
foreach (['exp', 'gold', 'charm'] as $currency) {
|
||||
$totalIn = UserCurrencyLog::whereDate('created_at', $date)
|
||||
->where('currency', $currency)->where('amount', '>', 0)
|
||||
->sum('amount');
|
||||
$totalOut = UserCurrencyLog::whereDate('created_at', $date)
|
||||
->where('currency', $currency)->where('amount', '<', 0)
|
||||
->sum('amount');
|
||||
$netFlow[$currency] = [
|
||||
'in' => $totalIn,
|
||||
'out' => abs($totalOut),
|
||||
'net' => $totalIn + $totalOut, // 净增量
|
||||
];
|
||||
}
|
||||
$netFlow = $this->currencyService->netFlowStats($date);
|
||||
|
||||
// 所有已知来源(供视图展示缺失来源的空行)
|
||||
$allSources = CurrencySource::cases();
|
||||
|
||||
@@ -12,11 +12,15 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Models\User;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 类功能:展示全站排行榜、今日排行榜与用户个人积分流水记录。
|
||||
*/
|
||||
class LeaderboardController extends Controller
|
||||
{
|
||||
/**
|
||||
@@ -133,8 +137,15 @@ class LeaderboardController extends Controller
|
||||
$user = auth()->user();
|
||||
$currency = request('currency');
|
||||
$days = (int) request('days', 7);
|
||||
$logs = $this->currencyService->userLogs($user->id, $currency ?: null, $days);
|
||||
$direction = in_array(request('direction'), ['income', 'expense'], true) ? request('direction') : null;
|
||||
$sourceOptions = CurrencySource::cases();
|
||||
$allowedSources = collect($sourceOptions)->map(fn (CurrencySource $source) => $source->value)->all();
|
||||
$selectedSources = collect(request()->array('sources'))
|
||||
->filter(fn (string $source) => in_array($source, $allowedSources, true))
|
||||
->values()
|
||||
->all();
|
||||
$logs = $this->currencyService->userLogs($user->id, $currency ?: null, $days, $direction, $selectedSources);
|
||||
|
||||
return view('leaderboard.my-logs', compact('logs', 'user', 'currency', 'days'));
|
||||
return view('leaderboard.my-logs', compact('logs', 'user', 'currency', 'days', 'direction', 'sourceOptions', 'selectedSources'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace App\Services;
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCurrencyLog;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
@@ -165,15 +167,75 @@ class UserCurrencyService
|
||||
*/
|
||||
public function activityStats(?string $date = null): Collection
|
||||
{
|
||||
$date = $date ?? today()->toDateString();
|
||||
[$date, $rangeStart, $rangeEnd] = $this->statsDateBounds($date);
|
||||
|
||||
return UserCurrencyLog::query()
|
||||
->whereDate('created_at', $date)
|
||||
->selectRaw('source, currency, SUM(amount) as total_amount, COUNT(DISTINCT user_id) as participant_count')
|
||||
->groupBy('source', 'currency')
|
||||
->orderBy('currency')
|
||||
->orderByRaw('ABS(SUM(amount)) DESC')
|
||||
->get();
|
||||
return Cache::remember("currency_stats:activity:{$date}", 300, function () use ($rangeStart, $rangeEnd) {
|
||||
return UserCurrencyLog::query()
|
||||
->where('created_at', '>=', $rangeStart)
|
||||
->where('created_at', '<', $rangeEnd)
|
||||
->selectRaw('source, currency, SUM(amount) as total_amount, COUNT(DISTINCT user_id) as participant_count')
|
||||
->groupBy('source', 'currency')
|
||||
->orderBy('currency')
|
||||
->orderByRaw('ABS(SUM(amount)) DESC')
|
||||
->get();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某日三种货币的净流通量(流入、流出、净增)。
|
||||
*
|
||||
* @param string|null $date 日期字符串如 '2026-02-28',默认今日
|
||||
* @return array<string, array{in:int, out:int, net:int}>
|
||||
*/
|
||||
public function netFlowStats(?string $date = null): array
|
||||
{
|
||||
[$date, $rangeStart, $rangeEnd] = $this->statsDateBounds($date);
|
||||
|
||||
$rows = Cache::remember("currency_stats:net_flow:{$date}", 300, function () use ($rangeStart, $rangeEnd) {
|
||||
return UserCurrencyLog::query()
|
||||
->where('created_at', '>=', $rangeStart)
|
||||
->where('created_at', '<', $rangeEnd)
|
||||
->selectRaw('
|
||||
currency,
|
||||
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) as total_in,
|
||||
ABS(SUM(CASE WHEN amount < 0 THEN amount ELSE 0 END)) as total_out,
|
||||
SUM(amount) as net_total
|
||||
')
|
||||
->groupBy('currency')
|
||||
->get()
|
||||
->keyBy('currency');
|
||||
});
|
||||
|
||||
$netFlow = [];
|
||||
|
||||
foreach (['exp', 'gold', 'charm'] as $currency) {
|
||||
$row = $rows->get($currency);
|
||||
|
||||
$netFlow[$currency] = [
|
||||
'in' => (int) ($row->total_in ?? 0),
|
||||
'out' => (int) ($row->total_out ?? 0),
|
||||
'net' => (int) ($row->net_total ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
return $netFlow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析统计查询的日期边界,统一复用缓存 key 与时间范围。
|
||||
*
|
||||
* @param string|null $date 日期字符串如 '2026-02-28'
|
||||
* @return array{0:string, 1:CarbonImmutable, 2:CarbonImmutable}
|
||||
*/
|
||||
private function statsDateBounds(?string $date = null): array
|
||||
{
|
||||
$statsDate = CarbonImmutable::parse($date ?? today()->toDateString())->startOfDay();
|
||||
|
||||
return [
|
||||
$statsDate->toDateString(),
|
||||
$statsDate,
|
||||
$statsDate->addDay(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,12 +282,17 @@ class UserCurrencyService
|
||||
* @param int $userId 用户 ID
|
||||
* @param string|null $currency 为 null 时返回所有货币类型
|
||||
* @param int $days 查询最近多少天
|
||||
* @param string|null $direction income=收入 / expense=支出 / null=全部
|
||||
* @param array<int, string> $sources 来源 source 值列表,为空时不过滤
|
||||
*/
|
||||
public function userLogs(int $userId, ?string $currency = null, int $days = 7): Collection
|
||||
public function userLogs(int $userId, ?string $currency = null, int $days = 7, ?string $direction = null, array $sources = []): Collection
|
||||
{
|
||||
return UserCurrencyLog::query()
|
||||
->where('user_id', $userId)
|
||||
->when($currency, fn ($q) => $q->where('currency', $currency))
|
||||
->when($direction === 'income', fn ($q) => $q->where('amount', '>', 0))
|
||||
->when($direction === 'expense', fn ($q) => $q->where('amount', '<', 0))
|
||||
->when($sources !== [], fn ($q) => $q->whereIn('source', $sources))
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->orderByDesc('created_at')
|
||||
->limit(200)
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
@section('title', '金币/积分流水查询')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$selectedSourceCount = count($selectedSources ?? []);
|
||||
@endphp
|
||||
|
||||
<!-- 筛选面板 -->
|
||||
<div class="bg-white p-5 rounded-xl border border-gray-100 shadow-sm mb-6">
|
||||
<form action="{{ route('admin.currency-logs.index') }}" method="GET" class="flex flex-wrap items-end gap-4">
|
||||
@@ -39,17 +43,37 @@
|
||||
</div>
|
||||
|
||||
<!-- 来源途径 -->
|
||||
<div class="flex-1 min-w-[140px]">
|
||||
<label for="source" class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">来源途径</label>
|
||||
<select name="source" id="source"
|
||||
class="w-full border-gray-300 rounded-lg shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm px-3 py-2">
|
||||
<option value="">全部来源</option>
|
||||
@foreach ($allSources as $src)
|
||||
<option value="{{ $src->value }}" {{ request('source') == $src->value ? 'selected' : '' }}>
|
||||
{{ $src->label() }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="flex-1 min-w-[180px]">
|
||||
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">来源途径</label>
|
||||
<details class="relative" data-admin-source-filter>
|
||||
<summary
|
||||
class="flex w-full cursor-pointer list-none items-center justify-between rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 shadow-sm transition hover:border-indigo-400">
|
||||
<span class="truncate">{{ $selectedSourceCount > 0 ? '已选 '.$selectedSourceCount.' 项' : '全部来源' }}</span>
|
||||
<svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</summary>
|
||||
|
||||
<div class="absolute left-0 z-20 mt-2 flex w-full min-w-[220px] max-w-sm flex-col rounded-lg border border-gray-200 bg-white shadow-lg">
|
||||
<div class="grid max-h-64 gap-1 overflow-y-auto p-3">
|
||||
@foreach ($allSources as $src)
|
||||
<label class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-gray-700 hover:bg-gray-50">
|
||||
<input type="checkbox" name="sources[]" value="{{ $src->value }}"
|
||||
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
@checked(in_array($src->value, $selectedSources ?? [], true))>
|
||||
<span class="truncate" title="{{ $src->label() }}">{{ $src->label() }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="sticky bottom-0 flex items-center justify-between border-t border-gray-100 bg-white px-3 py-3">
|
||||
<a href="{{ route('admin.currency-logs.index', request()->except('sources')) }}"
|
||||
class="text-xs text-gray-400 hover:text-gray-600">清空来源</a>
|
||||
<span class="text-xs text-gray-400">勾选后点查询生效</span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- 备注关键词 -->
|
||||
@@ -176,4 +200,26 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sourceFilter = document.querySelector('[data-admin-source-filter]');
|
||||
|
||||
if (!sourceFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
if (!sourceFilter.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceFilter.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sourceFilter.removeAttribute('open');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@@ -3,16 +3,31 @@
|
||||
@section('title', '用户检索与管理')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$filterLabelClass = 'block mb-1 text-xs font-semibold uppercase tracking-wider text-gray-500';
|
||||
$filterInputClass = 'px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm';
|
||||
$tableHeadClass = 'p-4 text-xs font-semibold uppercase tracking-wider text-gray-500';
|
||||
$primaryTextClass = 'text-sm font-semibold text-gray-800';
|
||||
$secondaryTextClass = 'text-xs text-gray-400';
|
||||
$numericTextClass = 'text-sm font-mono';
|
||||
$statusBadgeClass = 'inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-semibold border';
|
||||
$actionButtonClass = 'rounded-lg px-3 py-1.5 text-xs font-semibold transition cursor-pointer';
|
||||
$modalLabelClass = 'mb-1 block text-xs font-semibold uppercase tracking-wider text-gray-500';
|
||||
$modalInputClass = 'w-full rounded-md border border-gray-300 p-2 text-sm text-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500';
|
||||
@endphp
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6" x-data="userEditor()">
|
||||
<div class="p-6 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
|
||||
<form action="{{ route('admin.users.index') }}" method="GET" class="flex gap-2">
|
||||
<input type="text" name="username" value="{{ request('username') }}" placeholder="搜索用户名..."
|
||||
class="px-3 py-1.5 border border-gray-300 rounded shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
<div class="p-6 border-b border-gray-100 bg-gray-50">
|
||||
<form action="{{ route('admin.users.index') }}" method="GET" class="flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<label class="{{ $filterLabelClass }}">用户名</label>
|
||||
<input type="text" name="username" value="{{ request('username') }}" placeholder="搜索用户名..."
|
||||
class="w-64 {{ $filterInputClass }}">
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white px-4 py-1.5 rounded hover:bg-indigo-700 font-bold shadow-sm transition">搜索</button>
|
||||
class="bg-indigo-600 text-white px-4 py-2 rounded-lg font-semibold shadow-sm transition hover:bg-indigo-700">搜索</button>
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
class="px-4 py-1.5 bg-white border border-gray-300 rounded text-gray-700 hover:bg-gray-50">重置</a>
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg text-sm font-semibold text-gray-700 hover:bg-gray-50 transition">重置</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -36,59 +51,58 @@
|
||||
@endphp
|
||||
<table class="w-full text-left border-collapse whitespace-nowrap">
|
||||
<thead>
|
||||
<tr
|
||||
class="bg-gray-50 border-b border-gray-100 text-xs text-gray-500 uppercase font-bold tracking-wider">
|
||||
<th class="p-4">
|
||||
<a href="{{ $sortLink('id') }}" class="hover:text-indigo-600 flex items-center gap-1">
|
||||
<tr class="bg-gray-50 border-b border-gray-100">
|
||||
<th class="{{ $tableHeadClass }}">
|
||||
<a href="{{ $sortLink('id') }}" class="flex items-center gap-1 hover:text-indigo-600">
|
||||
ID<span class="text-indigo-500">{{ $arrow('id') }}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th class="p-4">注册名</th>
|
||||
<th class="p-4">性别</th>
|
||||
<th class="p-4">
|
||||
<a href="{{ $sortLink('user_level') }}" class="hover:text-indigo-600 flex items-center gap-1">
|
||||
<th class="{{ $tableHeadClass }}">注册名</th>
|
||||
<th class="{{ $tableHeadClass }}">性别</th>
|
||||
<th class="{{ $tableHeadClass }}">
|
||||
<a href="{{ $sortLink('user_level') }}" class="flex items-center gap-1 hover:text-indigo-600">
|
||||
等级<span class="text-indigo-500">{{ $arrow('user_level') }}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th class="p-4">职务</th>
|
||||
<th class="p-4">
|
||||
<a href="{{ $sortLink('exp_num') }}" class="hover:text-indigo-600 flex items-center gap-1">
|
||||
<th class="{{ $tableHeadClass }}">职务</th>
|
||||
<th class="{{ $tableHeadClass }}">
|
||||
<a href="{{ $sortLink('exp_num') }}" class="flex items-center gap-1 hover:text-indigo-600">
|
||||
经验<span class="text-indigo-500">{{ $arrow('exp_num') }}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th class="p-4">
|
||||
<a href="{{ $sortLink('jjb') }}" class="hover:text-yellow-600 flex items-center gap-1">
|
||||
<th class="{{ $tableHeadClass }}">
|
||||
<a href="{{ $sortLink('jjb') }}" class="flex items-center gap-1 hover:text-yellow-600">
|
||||
金币<span class="text-yellow-500">{{ $arrow('jjb') }}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th class="p-4">
|
||||
<a href="{{ $sortLink('meili') }}" class="hover:text-pink-600 flex items-center gap-1">
|
||||
<th class="{{ $tableHeadClass }}">
|
||||
<a href="{{ $sortLink('meili') }}" class="flex items-center gap-1 hover:text-pink-600">
|
||||
魅力<span class="text-pink-500">{{ $arrow('meili') }}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th class="p-4">注册时间</th>
|
||||
<th class="p-4">
|
||||
<a href="{{ $sortLink('wxid') }}" class="hover:text-green-600 flex items-center gap-1">
|
||||
<th class="{{ $tableHeadClass }}">注册时间</th>
|
||||
<th class="{{ $tableHeadClass }}">
|
||||
<a href="{{ $sortLink('wxid') }}" class="flex items-center gap-1 hover:text-green-600">
|
||||
微信绑定<span class="text-green-500">{{ $arrow('wxid') }}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th class="p-4">
|
||||
<a href="{{ $sortLink('online') }}" class="hover:text-green-600 flex items-center gap-1">
|
||||
<th class="{{ $tableHeadClass }}">
|
||||
<a href="{{ $sortLink('online') }}" class="flex items-center gap-1 hover:text-green-600">
|
||||
在线<span class="text-green-500">{{ $arrow('online') }}</span>
|
||||
</a>
|
||||
</th>
|
||||
<th class="p-4 text-right">管理操作</th>
|
||||
<th class="{{ $tableHeadClass }} text-right">管理操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@foreach ($users as $user)
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="p-4 font-mono text-xs text-gray-500">{{ $user->id }}</td>
|
||||
<td class="p-4 text-xs font-mono text-gray-500">{{ $user->id }}</td>
|
||||
<td class="p-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="{{ $user->headface_url ?? '/images/headface/1.gif' }}"
|
||||
class="w-8 h-8 rounded border object-cover">
|
||||
<span class="font-bold text-gray-800">{{ $user->username }}</span>
|
||||
<span class="{{ $primaryTextClass }}">{{ $user->username }}</span>
|
||||
@if ($user->isVip())
|
||||
<span title="{{ $user->vipName() }}"
|
||||
style="color: {{ $user->vipLevel?->color ?? '#f59e0b' }}">{{ $user->vipIcon() }}</span>
|
||||
@@ -105,32 +119,32 @@
|
||||
<td class="p-4">
|
||||
@if ($user->activePosition)
|
||||
@php $pos = $user->activePosition->position; @endphp
|
||||
<div class="text-xs text-gray-400">{{ $pos->department->name }}</div>
|
||||
<div class="font-bold text-sm" style="color: {{ $pos->department->color }}">
|
||||
<div class="{{ $secondaryTextClass }}">{{ $pos->department->name }}</div>
|
||||
<div class="font-semibold text-sm" style="color: {{ $pos->department->color }}">
|
||||
{{ $pos->icon }} {{ $pos->name }}
|
||||
</div>
|
||||
@else
|
||||
<span class="text-gray-300 text-xs">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="p-4 text-sm font-mono text-gray-600">
|
||||
<td class="p-4 {{ $numericTextClass }} text-gray-600">
|
||||
{{ number_format($user->exp_num ?? 0) }}
|
||||
</td>
|
||||
<td class="p-4 text-sm font-mono text-yellow-700">
|
||||
<td class="p-4 {{ $numericTextClass }} text-yellow-700">
|
||||
{{ number_format($user->jjb ?? 0) }}
|
||||
</td>
|
||||
<td class="p-4 text-sm font-mono text-pink-600">
|
||||
<td class="p-4 {{ $numericTextClass }} text-pink-600">
|
||||
{{ number_format($user->meili ?? 0) }}
|
||||
</td>
|
||||
<td class="p-4 text-sm font-mono text-gray-500">{{ $user->created_at->format('y-m-d') }}
|
||||
<td class="p-4 {{ $numericTextClass }} text-gray-500">{{ $user->created_at->format('y-m-d') }}
|
||||
</td>
|
||||
<td class="p-4">
|
||||
@if(!empty($user->wxid))
|
||||
<span class="inline-flex items-center gap-1.5 text-xs font-bold px-2 py-0.5 rounded-full bg-green-50 text-green-700 border border-green-200">
|
||||
<span class="{{ $statusBadgeClass }} bg-green-50 text-green-700 border-green-200">
|
||||
已绑定
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center gap-1.5 text-xs font-bold px-2 py-0.5 rounded-full bg-gray-50 text-gray-400 border border-gray-200">
|
||||
<span class="{{ $statusBadgeClass }} bg-gray-50 text-gray-400 border-gray-200">
|
||||
未绑定
|
||||
</span>
|
||||
@endif
|
||||
@@ -138,8 +152,8 @@
|
||||
<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' }}">
|
||||
class="{{ $statusBadgeClass }}
|
||||
{{ $isOnline ? 'bg-green-100 text-green-700 border-green-200' : 'bg-gray-100 text-gray-400 border-gray-200' }}">
|
||||
<span
|
||||
class="w-1.5 h-1.5 rounded-full {{ $isOnline ? 'bg-green-500' : 'bg-gray-300' }}"></span>
|
||||
{{ $isOnline ? '在线' : '离线' }}
|
||||
@@ -187,7 +201,7 @@
|
||||
|
||||
<!-- 分页链接 -->
|
||||
@if ($users->hasPages())
|
||||
<div class="p-4 border-t border-gray-100">
|
||||
<div class="p-4 border-t border-gray-100 text-sm text-gray-600">
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -199,7 +213,7 @@
|
||||
class="bg-white rounded-xl shadow-2xl w-full max-w-lg transform transition-all" x-transition>
|
||||
<div
|
||||
class="bg-indigo-900 border-b border-indigo-800 px-6 py-4 flex justify-between items-center rounded-t-xl text-white">
|
||||
<h3 class="font-bold text-lg">编辑用户:<span x-text="editingUser.username" class="text-indigo-300"></span>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wider">编辑用户:<span x-text="editingUser.username" class="text-indigo-300"></span>
|
||||
</h3>
|
||||
<button @click="showEditModal = false" class="text-gray-400 hover:text-white">×</button>
|
||||
</div>
|
||||
@@ -209,7 +223,7 @@
|
||||
<div x-show="editToast" x-transition style="display:none;"
|
||||
:class="editToastOk ? 'bg-green-50 border-green-400 text-green-800' :
|
||||
'bg-red-50 border-red-400 text-red-800'"
|
||||
class="mb-4 px-4 py-2 border-l-4 rounded text-sm font-bold" x-text="editToastMsg">
|
||||
class="mb-4 rounded border-l-4 px-4 py-2 text-sm font-semibold" x-text="editToastMsg">
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitEditUser($el)" method="POST">
|
||||
@@ -218,28 +232,28 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{-- 经验 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">经验值</label>
|
||||
<label class="{{ $modalLabelClass }}">经验值</label>
|
||||
<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="{{ $modalInputClass }}">
|
||||
</div>
|
||||
{{-- 金币 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">金币 (jjb)</label>
|
||||
<label class="{{ $modalLabelClass }}">金币 (jjb)</label>
|
||||
<input type="number" name="jjb" x-model="editingUser.jjb" 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="{{ $modalInputClass }}">
|
||||
</div>
|
||||
{{-- 魅力 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">魅力值</label>
|
||||
<label class="{{ $modalLabelClass }}">魅力值</label>
|
||||
<input type="number" name="meili" x-model="editingUser.meili" 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="{{ $modalInputClass }}">
|
||||
</div>
|
||||
{{-- 性别 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">性别</label>
|
||||
<label class="{{ $modalLabelClass }}">性别</label>
|
||||
<select name="sex" x-model="editingUser.sex"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
class="{{ $modalInputClass }}">
|
||||
<option value="1">男</option>
|
||||
<option value="2">女</option>
|
||||
<option value="0">保密</option>
|
||||
@@ -247,7 +261,7 @@
|
||||
</div>
|
||||
{{-- 访问次数 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">访问次数</label>
|
||||
<label class="{{ $modalLabelClass }}">访问次数</label>
|
||||
<input type="text" disabled :value="editingUser.visit_num"
|
||||
class="w-full bg-gray-100 border-gray-200 rounded-md p-2 border text-sm text-gray-500">
|
||||
</div>
|
||||
@@ -255,17 +269,17 @@
|
||||
|
||||
{{-- 签名 --}}
|
||||
<div class="mt-4">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">个性签名</label>
|
||||
<label class="{{ $modalLabelClass }}">个性签名</label>
|
||||
<input type="text" name="qianming" x-model="editingUser.qianming" maxlength="255"
|
||||
placeholder="暂无签名"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
class="{{ $modalInputClass }}">
|
||||
</div>
|
||||
|
||||
{{-- 任命职务 --}}
|
||||
<div class="mt-4">
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">职务</label>
|
||||
<label class="{{ $modalLabelClass }}">职务</label>
|
||||
<select name="position_id" x-model="editingUser.position_id"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
class="{{ $modalInputClass }}">
|
||||
<option value="">无职务</option>
|
||||
@foreach ($departments as $department)
|
||||
<optgroup label="{{ $department->name }}">
|
||||
@@ -280,9 +294,9 @@
|
||||
{{-- VIP 会员设置 --}}
|
||||
<div class="mt-4 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">VIP 会员等级</label>
|
||||
<label class="{{ $modalLabelClass }}">VIP 会员等级</label>
|
||||
<select name="vip_level_id" x-model="editingUser.vip_level_id"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
class="{{ $modalInputClass }}">
|
||||
<option value="">无(普通用户)</option>
|
||||
@foreach ($vipLevels as $vl)
|
||||
<option value="{{ $vl->id }}">{{ $vl->icon }}
|
||||
@@ -292,18 +306,18 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-600 mb-1">会员到期时间
|
||||
<span class="font-normal text-gray-400">(留空=永久)</span></label>
|
||||
<label class="{{ $modalLabelClass }}">会员到期时间
|
||||
<span class="normal-case font-normal tracking-normal text-gray-400">(留空=永久)</span></label>
|
||||
<input type="date" name="hy_time" x-model="editingUser.hy_time"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
|
||||
class="{{ $modalInputClass }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 密码 --}}
|
||||
<div class="mt-4">
|
||||
<label
|
||||
class="block text-xs font-bold pl-2 text-red-600 border-l-4 border-red-500 bg-red-50 p-2 mb-1">强制重置密码
|
||||
<span class="font-normal text-gray-500">(留空不修改)</span></label>
|
||||
class="mb-1 block border-l-4 border-red-500 bg-red-50 p-2 pl-2 text-xs font-semibold uppercase tracking-wider text-red-600">强制重置密码
|
||||
<span class="normal-case font-normal tracking-normal text-gray-500">(留空不修改)</span></label>
|
||||
<input type="text" name="password" placeholder="输入新密码"
|
||||
class="w-full border-red-300 rounded-md shadow-sm focus:border-red-500 focus:ring-red-500 p-2 border text-sm placeholder-red-300">
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{--
|
||||
文件功能:用户个人积分流水日志页面
|
||||
用户可筛选查看自己的经验、金币、魅力变动历史,按日期倒序排列
|
||||
用户可筛选查看自己的经验、金币、魅力收入和支出变动历史,按日期倒序排列
|
||||
|
||||
@extends layouts.app
|
||||
--}}
|
||||
@@ -13,11 +13,25 @@
|
||||
@section('content')
|
||||
<main class="p-4 sm:p-6 lg:p-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
@php
|
||||
$selectedSourceCount = count($selectedSources ?? []);
|
||||
@endphp
|
||||
|
||||
{{-- 筛选栏 --}}
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-6 flex flex-wrap gap-3 items-center">
|
||||
<span class="text-gray-600 text-sm font-medium">筛选:</span>
|
||||
|
||||
{{-- 收支方向 --}}
|
||||
<div class="flex gap-2">
|
||||
@foreach(['' => '全部', 'income' => '收入', 'expense' => '支出'] as $val => $label)
|
||||
<a href="{{ route('currency.my-logs', array_merge(request()->query(), ['direction' => $val, 'days' => $days])) }}"
|
||||
class="px-3 py-1 rounded-full text-xs font-medium transition
|
||||
{{ ($direction ?? '') === $val ? 'bg-green-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-green-100' }}">
|
||||
{{ $label }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- 货币类型 --}}
|
||||
<div class="flex gap-2">
|
||||
@foreach(['' => '全部', 'exp' => '⚡ 经验', 'gold' => '💰 金币', 'charm' => '🌸 魅力'] as $val => $label)
|
||||
@@ -29,6 +43,41 @@
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- 来源筛选 --}}
|
||||
<form method="GET" action="{{ route('currency.my-logs') }}" class="relative">
|
||||
<input type="hidden" name="direction" value="{{ $direction ?? '' }}">
|
||||
<input type="hidden" name="currency" value="{{ $currency ?? '' }}">
|
||||
<input type="hidden" name="days" value="{{ $days }}">
|
||||
|
||||
<details class="relative">
|
||||
<summary class="list-none cursor-pointer select-none px-3 py-1 rounded-full text-xs font-medium transition bg-gray-100 text-gray-600 hover:bg-indigo-100">
|
||||
来源:{{ $selectedSourceCount > 0 ? '已选 '.$selectedSourceCount.' 项' : '全部' }} ▾
|
||||
</summary>
|
||||
|
||||
<div class="absolute left-0 z-20 mt-2 flex w-72 max-h-80 flex-col rounded-lg border border-gray-200 bg-white shadow-lg">
|
||||
<div class="grid max-h-60 gap-1 overflow-y-auto p-3">
|
||||
@foreach($sourceOptions as $sourceOption)
|
||||
<label class="flex items-center gap-2 rounded-md px-2 py-1.5 text-xs text-gray-600 hover:bg-gray-50">
|
||||
<input type="checkbox" name="sources[]" value="{{ $sourceOption->value }}"
|
||||
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
@checked(in_array($sourceOption->value, $selectedSources ?? [], true))>
|
||||
<span class="truncate" title="{{ $sourceOption->label() }}">{{ $sourceOption->label() }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="sticky bottom-0 flex items-center justify-between border-t border-gray-100 bg-white px-3 py-3">
|
||||
<a href="{{ route('currency.my-logs', request()->except('sources')) }}"
|
||||
class="text-xs text-gray-400 hover:text-gray-600">清空来源</a>
|
||||
<button type="submit"
|
||||
class="rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-indigo-700">
|
||||
应用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</form>
|
||||
|
||||
{{-- 日期范围 --}}
|
||||
<div class="flex gap-2 ml-auto">
|
||||
@foreach([7 => '7 天', 14 => '14 天', 30 => '30 天'] as $d => $label)
|
||||
@@ -87,7 +136,8 @@
|
||||
<td class="px-4 py-3 text-right text-gray-500">
|
||||
{{ number_format($log->balance_after) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-400 text-xs max-w-[160px] truncate">
|
||||
<td class="px-4 py-3 text-gray-400 text-xs max-w-[160px] truncate"
|
||||
title="{{ $log->remark ?: '—' }}">
|
||||
{{ $log->remark ?: '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台金币/积分流水查询测试
|
||||
*
|
||||
* 覆盖后台流水页对来源多选筛选的查询行为。
|
||||
*/
|
||||
|
||||
namespace Tests\Feature\Feature;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCurrencyLog;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 类功能:验证后台金币/积分流水查询页筛选行为。
|
||||
*/
|
||||
class AdminCurrencyLogControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 验证后台流水页支持按多个来源同时筛选。
|
||||
*/
|
||||
public function test_admin_can_filter_currency_logs_by_multiple_sources(): void
|
||||
{
|
||||
Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']);
|
||||
$admin = User::factory()->create(['user_level' => 100]);
|
||||
|
||||
foreach ([CurrencySource::AUTO_SAVE, CurrencySource::SIGN_IN, CurrencySource::ADMIN_ADJUST] as $index => $source) {
|
||||
UserCurrencyLog::query()->create([
|
||||
'user_id' => $admin->id,
|
||||
'username' => $admin->username,
|
||||
'currency' => 'gold',
|
||||
'amount' => 10 + $index,
|
||||
'balance_after' => 100 + $index,
|
||||
'source' => $source->value,
|
||||
'remark' => $source->label(),
|
||||
'created_at' => now()->subMinutes($index),
|
||||
]);
|
||||
}
|
||||
|
||||
$response = $this->actingAs($admin)->get(route('admin.currency-logs.index', [
|
||||
'sources' => [
|
||||
CurrencySource::AUTO_SAVE->value,
|
||||
CurrencySource::SIGN_IN->value,
|
||||
],
|
||||
]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('admin.currency-logs.index');
|
||||
$response->assertViewHas('selectedSources', [
|
||||
CurrencySource::AUTO_SAVE->value,
|
||||
CurrencySource::SIGN_IN->value,
|
||||
]);
|
||||
$this->assertSame(
|
||||
[CurrencySource::AUTO_SAVE->value, CurrencySource::SIGN_IN->value],
|
||||
$response->viewData('logs')->getCollection()->pluck('source')->sort()->values()->all()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台积分流水统计页面测试
|
||||
*
|
||||
* 覆盖后台统计页的来源聚合与净流通量口径。
|
||||
*/
|
||||
|
||||
namespace Tests\Feature\Feature;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCurrencyLog;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 类功能:验证后台积分流水统计页能正确汇总指定日期数据。
|
||||
*/
|
||||
class AdminCurrencyStatsControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 验证后台积分流水统计页会返回指定日期的来源统计与净流通数据。
|
||||
*/
|
||||
public function test_admin_can_view_currency_stats_summary_for_selected_date(): void
|
||||
{
|
||||
Cache::flush();
|
||||
Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']);
|
||||
|
||||
$admin = User::factory()->create(['user_level' => 100]);
|
||||
$date = '2026-04-26';
|
||||
|
||||
UserCurrencyLog::query()->create([
|
||||
'user_id' => $admin->id,
|
||||
'username' => $admin->username,
|
||||
'currency' => 'gold',
|
||||
'amount' => 120,
|
||||
'balance_after' => 120,
|
||||
'source' => CurrencySource::SIGN_IN->value,
|
||||
'remark' => '签到奖励',
|
||||
'created_at' => "{$date} 10:00:00",
|
||||
]);
|
||||
|
||||
UserCurrencyLog::query()->create([
|
||||
'user_id' => $admin->id,
|
||||
'username' => $admin->username,
|
||||
'currency' => 'gold',
|
||||
'amount' => -20,
|
||||
'balance_after' => 100,
|
||||
'source' => CurrencySource::FISHING_COST->value,
|
||||
'remark' => '钓鱼消耗',
|
||||
'created_at' => "{$date} 11:00:00",
|
||||
]);
|
||||
|
||||
UserCurrencyLog::query()->create([
|
||||
'user_id' => $admin->id,
|
||||
'username' => $admin->username,
|
||||
'currency' => 'exp',
|
||||
'amount' => 80,
|
||||
'balance_after' => 80,
|
||||
'source' => CurrencySource::AUTO_SAVE->value,
|
||||
'remark' => '自动存点',
|
||||
'created_at' => "{$date} 12:00:00",
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->get(route('admin.currency-stats.index', ['date' => $date]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('admin.currency-stats.index');
|
||||
$response->assertViewHas('date', $date);
|
||||
$response->assertViewHas('netFlow', [
|
||||
'exp' => ['in' => 80, 'out' => 0, 'net' => 80],
|
||||
'gold' => ['in' => 120, 'out' => 20, 'net' => 100],
|
||||
'charm' => ['in' => 0, 'out' => 0, 'net' => 0],
|
||||
]);
|
||||
|
||||
$statsByType = $response->viewData('statsByType');
|
||||
|
||||
$this->assertSame(120, (int) $statsByType['gold'][CurrencySource::SIGN_IN->value]->total_amount);
|
||||
$this->assertSame(80, (int) $statsByType['exp'][CurrencySource::AUTO_SAVE->value]->total_amount);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,24 @@ class AdminUserManagerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 方法功能:验证后台用户管理页可以正常打开并展示核心文案。
|
||||
*/
|
||||
public function test_site_owner_can_view_user_manager_page(): void
|
||||
{
|
||||
$siteOwner = $this->createSiteOwner();
|
||||
User::factory()->create([
|
||||
'username' => 'viewer-target',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($siteOwner)->get(route('admin.users.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('用户检索与管理');
|
||||
$response->assertSee('注册名');
|
||||
$response->assertSee('管理操作');
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法功能:验证站长可以在用户编辑页直接任命职务。
|
||||
*/
|
||||
|
||||
@@ -2,16 +2,24 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCurrencyLog;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 功能说明:验证排行榜与个人积分流水页面的访问和筛选行为。
|
||||
*/
|
||||
class LeaderboardControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_can_view_leaderboard_index()
|
||||
/**
|
||||
* 验证用户可以访问排行榜首页并看到过滤后的榜单数据。
|
||||
*/
|
||||
public function test_can_view_leaderboard_index(): void
|
||||
{
|
||||
$user = User::factory()->create(['exp_num' => 10, 'jjb' => 100, 'meili' => 5]);
|
||||
|
||||
@@ -51,7 +59,10 @@ class LeaderboardControllerTest extends TestCase
|
||||
$this->assertEquals(50, $topCharm->first()->meili);
|
||||
}
|
||||
|
||||
public function test_can_view_today_leaderboard()
|
||||
/**
|
||||
* 验证用户可以访问今日排行榜页面。
|
||||
*/
|
||||
public function test_can_view_today_leaderboard(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
@@ -61,7 +72,10 @@ class LeaderboardControllerTest extends TestCase
|
||||
$response->assertViewIs('leaderboard.today');
|
||||
}
|
||||
|
||||
public function test_can_view_my_currency_logs()
|
||||
/**
|
||||
* 验证用户可以访问自己的积分流水页面。
|
||||
*/
|
||||
public function test_can_view_my_currency_logs(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
@@ -70,4 +84,84 @@ class LeaderboardControllerTest extends TestCase
|
||||
$response->assertStatus(200);
|
||||
$response->assertViewIs('leaderboard.my-logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证个人积分流水可以按收入和支出 tab 分开筛选。
|
||||
*/
|
||||
public function test_can_filter_my_currency_logs_by_direction(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
UserCurrencyLog::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'currency' => 'gold',
|
||||
'amount' => 100,
|
||||
'balance_after' => 100,
|
||||
'source' => 'admin_adjust',
|
||||
'remark' => '测试收入',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
UserCurrencyLog::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'currency' => 'gold',
|
||||
'amount' => -30,
|
||||
'balance_after' => 70,
|
||||
'source' => 'admin_adjust',
|
||||
'remark' => '测试支出',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$incomeResponse = $this->actingAs($user)->get(route('currency.my-logs', ['direction' => 'income']));
|
||||
|
||||
$incomeResponse->assertStatus(200);
|
||||
$this->assertSame([100], $incomeResponse->viewData('logs')->pluck('amount')->all());
|
||||
$this->assertSame('income', $incomeResponse->viewData('direction'));
|
||||
|
||||
$expenseResponse = $this->actingAs($user)->get(route('currency.my-logs', ['direction' => 'expense']));
|
||||
|
||||
$expenseResponse->assertStatus(200);
|
||||
$this->assertSame([-30], $expenseResponse->viewData('logs')->pluck('amount')->all());
|
||||
$this->assertSame('expense', $expenseResponse->viewData('direction'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证个人积分流水可以按多个来源叠加筛选。
|
||||
*/
|
||||
public function test_can_filter_my_currency_logs_by_multiple_sources(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
foreach ([CurrencySource::AUTO_SAVE, CurrencySource::SIGN_IN, CurrencySource::ADMIN_ADJUST] as $index => $source) {
|
||||
UserCurrencyLog::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'currency' => 'gold',
|
||||
'amount' => 10 + $index,
|
||||
'balance_after' => 100 + $index,
|
||||
'source' => $source->value,
|
||||
'remark' => $source->label(),
|
||||
'created_at' => now()->subMinutes($index),
|
||||
]);
|
||||
}
|
||||
|
||||
$response = $this->actingAs($user)->get(route('currency.my-logs', [
|
||||
'sources' => [
|
||||
CurrencySource::AUTO_SAVE->value,
|
||||
CurrencySource::SIGN_IN->value,
|
||||
],
|
||||
]));
|
||||
|
||||
$response->assertStatus(200);
|
||||
$this->assertSame(
|
||||
[CurrencySource::AUTO_SAVE->value, CurrencySource::SIGN_IN->value],
|
||||
$response->viewData('logs')->pluck('source')->sort()->values()->all()
|
||||
);
|
||||
$this->assertSame(
|
||||
[CurrencySource::AUTO_SAVE->value, CurrencySource::SIGN_IN->value],
|
||||
$response->viewData('selectedSources')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user