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

236 lines
8.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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}";
}
$jjbDisplay = $user->jjb ?? 0;
$gainStr = ! empty($gainParts) ? ' 本次获得:' . implode('', $gainParts) : '';
// 格式:⏰ 自动存点 · LV.100 · 经验 10,468 · 金币 8,345 · 已满级 · 本次获得:经验+1金币+3
$statusTag = $user->user_level >= $superLevel ? ' · 已满级 ✓' : '';
$content = "⏰ 自动存点 · LV.{$user->user_level} · 经验 {$user->exp_num} · 金币 {$jjbDisplay}{$statusTag}{$gainStr}";
$noticeMsg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统',
'to_user' => $username, // 定向推送给本人
'content' => $content,
'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;
}
}