Files
chatroom/app/Console/Commands/AutoSaveExp.php

236 lines
8.2 KiB
PHP
Raw Normal View History

<?php
/**
* 文件功能:定时自动存点指令
*
* 每5分钟由 Laravel Scheduler 调用,为所有在线用户:
* 1. 发放经验和金币奖励
* 2. 自动计算并更新等级
* 3. 在聊天室内推送"系统为你自动存点"提示
* 4. 若用户等级提升,向全频道广播恭喜消息
*
* @package App\Console\Commands
* @author ChatRoom Laravel
* @version 1.0.0
*/
namespace App\Console\Commands;
use App\Events\MessageSent;
use App\Jobs\SaveMessageJob;
use App\Models\Sysparam;
use App\Models\User;
use App\Services\ChatStateService;
use App\Services\VipService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class AutoSaveExp extends Command
{
/**
* Artisan 指令名称
*/
protected $signature = 'chatroom:auto-save-exp';
/**
* 指令描述(在 artisan list 中显示)
*/
protected $description = '为所有聊天室在线用户执行自动存点(经验/金币/等级每5分钟运行一次';
/**
* 注入依赖服务
*/
public function __construct(
private readonly ChatStateService $chatState,
private readonly VipService $vipService,
) {
parent::__construct();
}
/**
* 指令入口:扫描所有房间,为在线用户发放经验和金币奖励。
*
* 处理流程:
* 1. Redis 扫描所有 room:*:users 的哈希表
* 2. 对每个在线用户应用经验/金币奖励
* 3. 重新计算等级,若升级则推送全频道广播
* 4. 向用户所在房间推送"系统为你自动存点"私信
*/
public function handle(): int
{
// 读取奖励配置
$expGainRaw = Sysparam::getValue('exp_per_heartbeat', '1');
$jjbGainRaw = Sysparam::getValue('jjb_per_heartbeat', '0');
$superLevel = (int) Sysparam::getValue('superlevel', '100');
// 从 Redis 扫描所有在线房间
$roomMap = $this->scanOnlineRooms();
if (empty($roomMap)) {
$this->info('当前没有在线用户,跳过存点。');
return Command::SUCCESS;
}
// 统计本次处理总人次(一个用户在多个房间会被计算多次)
$totalProcessed = 0;
foreach ($roomMap as $roomId => $usernames) {
foreach ($usernames as $username) {
$this->processUser($username, $roomId, $expGainRaw, $jjbGainRaw, $superLevel);
$totalProcessed++;
}
}
$this->info("自动存点完成,共处理 {$totalProcessed} 个在线用户。");
return Command::SUCCESS;
}
/**
* 查询所有活跃房间及其在线用户列表。
*
* 改为从数据库获取所有房间,再用 Redis::hkeys() 查询在线用户。
* 这样可以避免 Redis SCAN + 前缀匹配不一致的问题,
* Redis::hkeys() 会自动正确地加上前缀,与 ChatStateService::userJoin() 一致。
*
* @return array<int, array<string>> 格式:[房间ID => [用户名, ...]]
*/
private function scanOnlineRooms(): array
{
$roomMap = [];
// 从数据库取出所有房间 ID
$roomIds = \App\Models\Room::pluck('id');
foreach ($roomIds as $roomId) {
// Laravel 的 Redis facade 会自动加配置的前缀,与 ChatStateService 存入时完全一致
$usernames = Redis::hkeys("room:{$roomId}:users");
if (! empty($usernames)) {
$roomMap[(int) $roomId] = $usernames;
}
}
return $roomMap;
}
/**
* 为单个在线用户执行存点逻辑,并在其所在房间推送存点通知。
*
* @param string $username 用户名
* @param int $roomId 所在房间ID
* @param string $expGainRaw 经验奖励原始配置(支持 "1" "1-10" 范围)
* @param string $jjbGainRaw 金币奖励原始配置
* @param int $superLevel 管理员等级阈值
*/
private function processUser(
string $username,
int $roomId,
string $expGainRaw,
string $jjbGainRaw,
int $superLevel
): void {
$user = User::where('username', $username)->first();
if (! $user) {
return;
}
// 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;
}
// 3. 自动升降级(管理员不参与)
$oldLevel = $user->user_level;
$leveledUp = false;
if ($oldLevel < $superLevel) {
$newLevel = Sysparam::calculateLevel($user->exp_num);
if ($newLevel !== $oldLevel && $newLevel < $superLevel) {
$user->user_level = $newLevel;
$leveledUp = ($newLevel > $oldLevel);
}
}
$user->save();
// 4. 若升级,向全频道广播升级消息
if ($leveledUp) {
$sysMsg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统传音',
'to_user' => '大家',
'content' => "🌟 天道酬勤!恭喜侠客【{$user->username}】挂机苦修,境界突破至 LV.{$user->user_level}",
'is_secret' => false,
'font_color' => '#d97706',
'action' => '大声宣告',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($roomId, $sysMsg);
broadcast(new MessageSent($roomId, $sysMsg));
SaveMessageJob::dispatch($sysMsg);
}
// 5. 向用户私人推送"系统为你自动存点"信息,在其聊天框显示
$gainParts = [];
if ($actualExpGain > 0) {
$gainParts[] = "经验+{$actualExpGain}";
}
if ($actualJjbGain > 0) {
$gainParts[] = "金币+{$actualJjbGain}";
}
$gainStr = ! empty($gainParts) ? '(' . implode('', $gainParts) . ')' : '';
$jjbDisplay = $user->jjb ?? 0;
$levelInfo = $user->user_level >= $superLevel
? "级别({$user->user_level});经验({$user->exp_num});金币({$jjbDisplay}枚);已满级。"
: "级别({$user->user_level});经验({$user->exp_num});金币({$jjbDisplay}枚)。";
$noticeMsg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统',
'to_user' => $username, // 定向推送给本人
'content' => "【系统为你自动存点】{$levelInfo} {$gainStr}",
'is_secret' => true, // 私信模式,只有本人能看到
'font_color' => '#16a34a', // 草绿色
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($roomId, $noticeMsg);
broadcast(new MessageSent($roomId, $noticeMsg));
}
/**
* 解析奖励值配置字符串,支持固定值和随机范围两种格式。
*
* 格式说明:
* - 固定值:直接写数字,如 "5" 返回 5
* - 随机范围:如 "3-10" 返回 [3, 10] 之间的随机整数
*
* @param string $raw 原始配置字符串
* @return int
*/
private function parseRewardValue(string $raw): int
{
$raw = trim($raw);
if (str_contains($raw, '-')) {
[$min, $max] = explode('-', $raw, 2);
return rand((int) $min, (int) $max);
}
return (int) $raw;
}
}