功能:新增用户积分流水系统
- 新建 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:
@@ -17,10 +17,12 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Events\MessageSent;
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\UserCurrencyService;
|
||||
use App\Services\VipService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
@@ -41,8 +43,9 @@ class AutoSaveExp extends Command
|
||||
* 注入依赖服务
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState,
|
||||
private readonly VipService $vipService,
|
||||
private readonly ChatStateService $chatState,
|
||||
private readonly VipService $vipService,
|
||||
private readonly UserCurrencyService $currencyService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -134,21 +137,31 @@ class AutoSaveExp extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 发放经验奖励(支持 VIP 倍率)
|
||||
// 1. 计算奖励量(经验/金币 均支持 VIP 倍率)
|
||||
$expGain = $this->parseRewardValue($expGainRaw);
|
||||
$expMultiplier = $this->vipService->getExpMultiplier($user);
|
||||
$actualExpGain = (int) round($expGain * $expMultiplier);
|
||||
$user->exp_num += $actualExpGain;
|
||||
|
||||
// 2. 发放金币奖励(支持 VIP 倍率)
|
||||
$jjbGain = $this->parseRewardValue($jjbGainRaw);
|
||||
$actualJjbGain = 0;
|
||||
if ($jjbGain > 0) {
|
||||
$jjbMultiplier = $this->vipService->getJjbMultiplier($user);
|
||||
$actualJjbGain = (int) round($jjbGain * $jjbMultiplier);
|
||||
$user->jjb = ($user->jjb ?? 0) + $actualJjbGain;
|
||||
}
|
||||
|
||||
// 2. 通过统一积分服务发放奖励(原子写入 + 流水记录)
|
||||
if ($actualExpGain > 0) {
|
||||
$this->currencyService->change(
|
||||
$user, 'exp', $actualExpGain, CurrencySource::AUTO_SAVE, '自动存点', $roomId,
|
||||
);
|
||||
}
|
||||
if ($actualJjbGain > 0) {
|
||||
$this->currencyService->change(
|
||||
$user, 'gold', $actualJjbGain, CurrencySource::AUTO_SAVE, '自动存点', $roomId,
|
||||
);
|
||||
}
|
||||
$user->refresh(); // 刷新获取最新属性(service 已原子更新)
|
||||
|
||||
// 3. 自动升降级(管理员不参与)
|
||||
$oldLevel = $user->user_level;
|
||||
$leveledUp = false;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:积分来源活动枚举
|
||||
* 集中管理所有合法的 source 标识值,新增活动只需在此加一行常量,数据库字段无需任何变更。
|
||||
* 对应数据表:user_currency_logs.source(varchar 字段,非 ENUM,可自由扩展)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum CurrencySource: string
|
||||
{
|
||||
/** 自动存点(Horizon 定时任务,每5分钟给在线用户加经验/金币) */
|
||||
case AUTO_SAVE = 'auto_save';
|
||||
|
||||
/** 钓鱼收竿奖励(获得经验或金币) */
|
||||
case FISHING_GAIN = 'fishing_gain';
|
||||
|
||||
/** 钓鱼抛竿消耗(扣除金币) */
|
||||
case FISHING_COST = 'fishing_cost';
|
||||
|
||||
/** 送出礼物(送方扣金币) */
|
||||
case SEND_GIFT = 'send_gift';
|
||||
|
||||
/** 收到礼物(收方魅力增加) */
|
||||
case RECV_GIFT = 'recv_gift';
|
||||
|
||||
/** 新人礼包(首次登录赠送金币) */
|
||||
case NEWBIE_BONUS = 'newbie_bonus';
|
||||
|
||||
/** 商城购买消耗(扣除金币) */
|
||||
case SHOP_BUY = 'shop_buy';
|
||||
|
||||
/** 管理员手动调整(后台直接修改经验/金币/魅力) */
|
||||
case ADMIN_ADJUST = 'admin_adjust';
|
||||
|
||||
// ─── 以后新增活动,在这里加一行即可,数据库无需变更 ───────────
|
||||
// case SIGN_IN = 'sign_in'; // 每日签到
|
||||
// case TASK_REWARD = 'task_reward'; // 任务奖励
|
||||
// case PVP_WIN = 'pvp_win'; // PVP 胜利奖励
|
||||
|
||||
/**
|
||||
* 返回该来源的中文名称,用于后台统计展示。
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::AUTO_SAVE => '自动存点',
|
||||
self::FISHING_GAIN => '钓鱼奖励',
|
||||
self::FISHING_COST => '钓鱼消耗',
|
||||
self::SEND_GIFT => '送出礼物',
|
||||
self::RECV_GIFT => '收到礼物',
|
||||
self::NEWBIE_BONUS => '新人礼包',
|
||||
self::SHOP_BUY => '商城购买',
|
||||
self::ADMIN_ADJUST => '管理员调整',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户积分流水 Eloquent 模型
|
||||
* 对应表:user_currency_logs
|
||||
* 只读写,不允许 update(流水记录不可更改)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserCurrencyLog extends Model
|
||||
{
|
||||
/**
|
||||
* 只有 created_at,没有 updated_at(流水记录只写不改)
|
||||
*/
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
/**
|
||||
* 允许批量赋值的字段
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'username',
|
||||
'currency',
|
||||
'amount',
|
||||
'balance_after',
|
||||
'source',
|
||||
'remark',
|
||||
'room_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* 字段类型转换
|
||||
*/
|
||||
protected $casts = [
|
||||
'amount' => 'integer',
|
||||
'balance_after'=> 'integer',
|
||||
'room_id' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
// ─── 关联 ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 关联用户(流水属于哪个用户)
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
// ─── 作用域(Scope)快捷查询 ──────────────────────────────
|
||||
|
||||
/**
|
||||
* 按货币类型过滤
|
||||
*/
|
||||
public function scopeCurrency(Builder $query, string $currency): Builder
|
||||
{
|
||||
return $query->where('currency', $currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* 今日数据
|
||||
*/
|
||||
public function scopeToday(Builder $query): Builder
|
||||
{
|
||||
return $query->whereDate('created_at', today());
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定日期数据
|
||||
*/
|
||||
public function scopeOnDate(Builder $query, string $date): Builder
|
||||
{
|
||||
return $query->whereDate('created_at', $date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅正向增加(用于排行榜,不算消耗)
|
||||
*/
|
||||
public function scopeGain(Builder $query): Builder
|
||||
{
|
||||
return $query->where('amount', '>', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按来源过滤
|
||||
*/
|
||||
public function scopeSource(Builder $query, string $source): Builder
|
||||
{
|
||||
return $query->where('source', $source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户积分统一变更服务
|
||||
* 所有修改 exp_num(经验)、jjb(金币)、meili(魅力) 的操作必须经由此服务,
|
||||
* 禁止在 Controller 中直接操作 User 属性并 save()。
|
||||
* 本服务负责:原子性更新用户属性、写入流水记录、提供统计与排行数据。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCurrencyLog;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UserCurrencyService
|
||||
{
|
||||
/**
|
||||
* currency 标识与 users 表字段名的映射关系。
|
||||
* 以后新增货币类型,在此加一行即可。
|
||||
*/
|
||||
private const FIELD_MAP = [
|
||||
'exp' => 'exp_num',
|
||||
'gold' => 'jjb',
|
||||
'charm' => 'meili',
|
||||
];
|
||||
|
||||
/**
|
||||
* 统一变更用户货币属性并写入流水记录。
|
||||
* 使用数据库事务保证原子性:用户属性更新 + 流水写入同时成功或同时回滚。
|
||||
*
|
||||
* @param User $user 目标用户
|
||||
* @param string $currency 货币类型('exp' / 'gold' / 'charm')
|
||||
* @param int $amount 变更量,正数增加,负数扣除
|
||||
* @param CurrencySource $source 来源活动枚举
|
||||
* @param string $remark 备注说明
|
||||
* @param int|null $roomId 所在房间 ID(可选)
|
||||
*/
|
||||
public function change(
|
||||
User $user,
|
||||
string $currency,
|
||||
int $amount,
|
||||
CurrencySource $source,
|
||||
string $remark = '',
|
||||
?int $roomId = null,
|
||||
): void {
|
||||
if ($amount === 0) {
|
||||
return; // 变更量为 0 不写记录
|
||||
}
|
||||
|
||||
$field = self::FIELD_MAP[$currency] ?? null;
|
||||
if (! $field) {
|
||||
return; // 未知货币类型,静默忽略(不抛异常,避免影响主流程)
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($user, $currency, $amount, $source, $remark, $roomId, $field) {
|
||||
// 原子性更新用户属性(用 increment/decrement 防并发竞态)
|
||||
if ($amount > 0) {
|
||||
$user->increment($field, $amount);
|
||||
} else {
|
||||
// 扣除时不让余额低于 0
|
||||
$user->decrement($field, min(abs($amount), $user->$field ?? 0));
|
||||
}
|
||||
|
||||
// 重新读取最新余额(避免缓存脏数据)
|
||||
$balanceAfter = (int) $user->fresh()->$field;
|
||||
|
||||
// 写入流水记录(快照当前用户名,排行 JOIN 时再取最新名)
|
||||
UserCurrencyLog::create([
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'currency' => $currency,
|
||||
'amount' => $amount,
|
||||
'balance_after'=> $balanceAfter,
|
||||
'source' => $source->value,
|
||||
'remark' => $remark,
|
||||
'room_id' => $roomId,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量变更多个用户的货币属性(适用于自动存点:一次操作多人)。
|
||||
* 每位用户仍独立走事务,单人失败不影响其他人。
|
||||
*
|
||||
* @param array $items [['user' => User, 'changes' => ['exp'=>1,'gold'=>2]], ...]
|
||||
* @param CurrencySource $source
|
||||
* @param int|null $roomId
|
||||
*/
|
||||
public function batchChange(array $items, CurrencySource $source, ?int $roomId = null): void
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
$user = $item['user'];
|
||||
$changes = $item['changes'] ?? [];
|
||||
foreach ($changes as $currency => $amount) {
|
||||
$this->change($user, $currency, (int) $amount, $source, '', $roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询某日各来源活动的产出统计(后台统计页面使用)。
|
||||
* 返回格式:Collection of stdClass { source, currency, total_amount, participant_count }
|
||||
*
|
||||
* @param string|null $date 日期字符串如 '2026-02-28',默认今日
|
||||
*/
|
||||
public function activityStats(?string $date = null): Collection
|
||||
{
|
||||
$date = $date ?? today()->toDateString();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 今日排行榜(按 user_id 聚合,展示最新用户名)。
|
||||
* 只统计正向变更(amount > 0),不因消耗而扣分。
|
||||
*
|
||||
* @param string $currency 'exp' | 'gold' | 'charm'
|
||||
* @param int $limit 返回条数
|
||||
* @param string|null $date 日期,默认今日
|
||||
*/
|
||||
public function todayLeaderboard(string $currency, int $limit = 20, ?string $date = null): Collection
|
||||
{
|
||||
$date = $date ?? today()->toDateString();
|
||||
|
||||
return UserCurrencyLog::query()
|
||||
->whereDate('created_at', $date)
|
||||
->where('currency', $currency)
|
||||
->where('amount', '>', 0) // 只统计正向
|
||||
->selectRaw('user_id, SUM(amount) as total')
|
||||
->groupBy('user_id')
|
||||
->orderByRaw('SUM(amount) DESC')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function ($row) {
|
||||
// JOIN 取最新用户名(避免改名后显示旧名)
|
||||
$user = User::select('id', 'username', 'user_level', 'sex', 'headface')
|
||||
->find($row->user_id);
|
||||
|
||||
return (object) [
|
||||
'user_id' => $row->user_id,
|
||||
'username' => $user?->username ?? '未知用户',
|
||||
'level' => $user?->user_level ?? 0,
|
||||
'sex' => $user?->sex ?? 1,
|
||||
'headface' => $user?->headface ?? '1.gif',
|
||||
'total' => $row->total,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户个人流水明细(用户查询自己的日志)。
|
||||
*
|
||||
* @param int $userId 用户 ID
|
||||
* @param string|null $currency 为 null 时返回所有货币类型
|
||||
* @param int $days 查询最近多少天
|
||||
*/
|
||||
public function userLogs(int $userId, ?string $currency = null, int $days = 7): Collection
|
||||
{
|
||||
return UserCurrencyLog::query()
|
||||
->where('user_id', $userId)
|
||||
->when($currency, fn ($q) => $q->where('currency', $currency))
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->orderByDesc('created_at')
|
||||
->limit(200)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 货币类型中文名映射(用于视图展示)。
|
||||
*/
|
||||
public static function currencyLabel(string $currency): string
|
||||
{
|
||||
return match ($currency) {
|
||||
'exp' => '经验',
|
||||
'gold' => '金币',
|
||||
'charm' => '魅力',
|
||||
default => $currency,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user