功能:新增用户积分流水系统

- 新建 user_currency_logs 流水表 (Migration)
- App\Enums\CurrencySource 来源枚举(可扩展)
- App\Models\UserCurrencyLog 流水模型
- App\Services\UserCurrencyService 统一积分变更服务
- FishingController:抛竿/收竿接入流水记录
- AutoSaveExp:自动存点接入流水记录
- Admin/UserManagerController:管理员调整接入流水记录
- LeaderboardController:新增今日三榜(经验/金币/魅力)+ 个人流水日志页
- Admin/CurrencyStatsController:后台活动统计页
- views:新增个人日志页、后台统计页;排行榜新增今日榜数据传递
- routes:新增个人日志路由 /my/currency-logs、后台路由 /admin/currency-stats
This commit is contained in:
2026-02-28 12:49:26 +08:00
parent 3f5d0e9539
commit 0c5e218aa8
14 changed files with 1045 additions and 223 deletions
@@ -0,0 +1,69 @@
<?php
/**
* 文件功能:后台积分活动统计控制器
* 展示今日(或指定日期)各来源活动产生的经验/金币/魅力统计,以及今日净流通量。
* 仅限 superlevel 以上管理员访问。
*
* @author ChatRoom Laravel
* @version 1.0.0
*/
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
{
/**
* 注入积分统计服务
*/
public function __construct(
private readonly UserCurrencyService $currencyService,
) {}
/**
* 显示指定日期的积分活动统计(默认今日)。
*/
public function index(Request $request): View
{
// 日期选择(默认今日)
$date = $request->input('date', today()->toDateString());
// 各来源活动产出统计(按 source + currency 分组汇总)
$stats = $this->currencyService->activityStats($date);
// 按货币类型分组,方便视图展示
$statsByType = $stats->groupBy('currency')->map(
fn ($rows) => $rows->keyBy('source')
);
// 今日净流通量(正向增加 - 负向消耗),可判断通货膨胀
$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, // 净增量
];
}
// 所有已知来源(供视图展示缺失来源的空行)
$allSources = CurrencySource::cases();
return view('admin.currency-stats.index', compact(
'date', 'stats', 'statsByType', 'netFlow', 'allSources',
));
}
}
@@ -12,7 +12,9 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Enums\CurrencySource;
use App\Models\User;
use App\Services\UserCurrencyService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -22,6 +24,12 @@ use Illuminate\View\View;
class UserManagerController extends Controller
{
/**
* 注入统一积分服务(用于管理员调整经验/金币/魅力时记录流水)
*/
public function __construct(
private readonly UserCurrencyService $currencyService,
) {}
/**
* 显示用户列表及搜索(支持按等级/经验/金币/魅力排序)
*/
@@ -90,13 +98,35 @@ class UserManagerController extends Controller
$targetUser->sex = $validated['sex'];
}
if (isset($validated['exp_num'])) {
$targetUser->exp_num = $validated['exp_num'];
// 计算差值并通过统一服务记录流水(管理员手动调整)
$expDiff = $validated['exp_num'] - ($targetUser->exp_num ?? 0);
if ($expDiff !== 0) {
$this->currencyService->change(
$targetUser, 'exp', $expDiff, CurrencySource::ADMIN_ADJUST,
"管理员 {$currentUser->username} 手动调整经验",
);
$targetUser->refresh();
}
}
if (isset($validated['jjb'])) {
$targetUser->jjb = $validated['jjb'];
$jjbDiff = $validated['jjb'] - ($targetUser->jjb ?? 0);
if ($jjbDiff !== 0) {
$this->currencyService->change(
$targetUser, 'gold', $jjbDiff, CurrencySource::ADMIN_ADJUST,
"管理员 {$currentUser->username} 手动调整金币",
);
$targetUser->refresh();
}
}
if (isset($validated['meili'])) {
$targetUser->meili = $validated['meili'];
$meiliDiff = $validated['meili'] - ($targetUser->meili ?? 0);
if ($meiliDiff !== 0) {
$this->currencyService->change(
$targetUser, 'charm', $meiliDiff, CurrencySource::ADMIN_ADJUST,
"管理员 {$currentUser->username} 手动调整魅力",
);
$targetUser->refresh();
}
}
if (array_key_exists('qianming', $validated)) {
$targetUser->qianming = $validated['qianming'];
+25 -9
View File
@@ -13,8 +13,10 @@
namespace App\Http\Controllers;
use App\Events\MessageSent;
use App\Enums\CurrencySource;
use App\Models\Sysparam;
use App\Services\ChatStateService;
use App\Services\UserCurrencyService;
use App\Services\VipService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -24,8 +26,9 @@ use Illuminate\Support\Facades\Redis;
class FishingController extends Controller
{
public function __construct(
private readonly ChatStateService $chatState,
private readonly VipService $vipService,
private readonly ChatStateService $chatState,
private readonly VipService $vipService,
private readonly UserCurrencyService $currencyService,
) {}
/**
@@ -61,9 +64,16 @@ class FishingController extends Controller
], 422);
}
// 3. 扣除金币
$user->jjb = max(0, ($user->jjb ?? 0) - $cost);
$user->save();
// 3. 扣除金币(通过统一积分服务记录流水)
$this->currencyService->change(
$user,
'gold',
-$cost,
CurrencySource::FISHING_COST,
"钓鱼抛竿消耗 {$cost} 金币",
$id,
);
$user->refresh(); // 刷新本地模型(service 已原子更新)
// 4. 设置"正在钓鱼"标记(防止重复抛竿,30秒后自动过期)
Redis::setex("fishing:active:{$user->id}", 30, time());
@@ -113,18 +123,24 @@ class FishingController extends Controller
// 3. 随机决定钓鱼结果
$result = $this->randomFishResult();
// 4. 更新用户经验和金币(正向奖励按 VIP 倍率加成,负面惩罚不变)
// 4. 通过统一积分服务更新经验和金币,写入流水
$expMul = $this->vipService->getExpMultiplier($user);
$jjbMul = $this->vipService->getJjbMultiplier($user);
if ($result['exp'] !== 0) {
$finalExp = $result['exp'] > 0 ? (int) round($result['exp'] * $expMul) : $result['exp'];
$user->exp_num = max(0, ($user->exp_num ?? 0) + $finalExp);
$this->currencyService->change(
$user, 'exp', $finalExp, CurrencySource::FISHING_GAIN,
"钓鱼收竿:{$result['message']}", $id,
);
}
if ($result['jjb'] !== 0) {
$finalJjb = $result['jjb'] > 0 ? (int) round($result['jjb'] * $jjbMul) : $result['jjb'];
$user->jjb = max(0, ($user->jjb ?? 0) + $finalJjb);
$this->currencyService->change(
$user, 'gold', $finalJjb, CurrencySource::FISHING_GAIN,
"钓鱼收竿:{$result['message']}", $id,
);
}
$user->save();
$user->refresh(); // 刷新获取最新余额
// 5. 广播钓鱼结果到聊天室
$sysMsg = [
+44 -6
View File
@@ -3,6 +3,7 @@
/**
* 文件功能:全局风云排行榜控制器
* 各种维度(等级、经验、交友币、魅力)的前20名抓取与缓存展示。
* 新增今日榜:显示今天经验成长、今日金币获得、今日魅力增长最多的用户。
*
* @author ChatRoom Laravel
*
@@ -12,26 +13,33 @@
namespace App\Http\Controllers;
use App\Models\User;
use App\Services\UserCurrencyService;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class LeaderboardController extends Controller
{
/**
* 渲染排行榜主视角
* 注入积分统计服务(用于今日榜单数据查询)
*/
public function __construct(
private readonly UserCurrencyService $currencyService,
) {}
/**
* 渲染排行榜主视角(包含累计榜 + 今日榜)
*/
public function index(): View
{
// 缓存 15 分钟,防止每秒几百个人看排行榜把数据库扫死
// 选用 remember 则在过期时自动执行闭包查询并重置缓存
$ttl = 60 * 15;
// 管理员等级阈值,排行榜中隐藏管理员
$superLevel = (int) \App\Models\Sysparam::getValue('superlevel', '100');
// 排行榜显示人数(后台可配置)
$topN = (int) \App\Models\Sysparam::getValue('leaderboard_limit', '20');
// ── 累计榜(15分钟缓存)──────────────────────────────
$ttl = 60 * 15;
// 1. 境界榜 (以 user_level 为尊)
$topLevels = Cache::remember('leaderboard:top_levels', $ttl, function () use ($superLevel, $topN) {
return User::select('id', 'username', 'usersf', 'user_level', 'sex')
@@ -76,6 +84,36 @@ class LeaderboardController extends Controller
->get();
});
return view('leaderboard.index', compact('topLevels', 'topExp', 'topWealth', 'topCharm'));
// ── 今日榜(5分钟缓存,数据来自 user_currency_logs 流水表)──
$todayTtl = 60 * 5;
$today = today()->toDateString();
$todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl,
fn () => $this->currencyService->todayLeaderboard('exp', $topN, $today)
);
$todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl,
fn () => $this->currencyService->todayLeaderboard('gold', $topN, $today)
);
$todayCharm = Cache::remember("leaderboard:today_charm:{$today}", $todayTtl,
fn () => $this->currencyService->todayLeaderboard('charm', $topN, $today)
);
return view('leaderboard.index', compact(
'topLevels', 'topExp', 'topWealth', 'topCharm',
'todayExp', 'todayGold', 'todayCharm',
));
}
/**
* 用户个人流水日志页(查询自己的经验/金币/魅力操作历史)
*/
public function myLogs(): View
{
$user = auth()->user();
$currency = request('currency');
$days = (int) request('days', 7);
$logs = $this->currencyService->userLogs($user->id, $currency ?: null, $days);
return view('leaderboard.my-logs', compact('logs', 'user', 'currency', 'days'));
}
}