优化定时任务调度耗时
This commit is contained in:
@@ -16,9 +16,11 @@ namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Events\MessageSent;
|
||||
use App\Jobs\AiFishingJob;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\Autoact;
|
||||
use App\Models\DailySignIn;
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Services\AiFinanceService;
|
||||
@@ -61,11 +63,15 @@ class AiHeartbeatCommand extends Command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$startedAt = microtime(true);
|
||||
|
||||
// 1. 检查总开关
|
||||
if (Sysparam::getValue('chatbot_enabled', '0') !== '1') {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$config = $this->heartbeatConfig();
|
||||
|
||||
// 2. 获取 AI 实体
|
||||
$user = User::where('username', 'AI小班长')->first();
|
||||
if (! $user) {
|
||||
@@ -73,21 +79,26 @@ class AiHeartbeatCommand extends Command
|
||||
}
|
||||
|
||||
// 心跳开始前,若手上金币已高于 100 万,则先把超出的部分转入银行。
|
||||
$this->aiFinance->bankExcessGold($user);
|
||||
if ((int) ($user->jjb ?? 0) > AiFinanceService::AVAILABLE_GOLD_RESERVE) {
|
||||
$this->aiFinance->bankExcessGold($user);
|
||||
}
|
||||
|
||||
// 2.5 自动每日签到(今日已签时 claim() 幂等返回,不重复发奖)
|
||||
$this->performDailySignIn($user);
|
||||
if ($this->performDailySignIn($user)) {
|
||||
// 签到可能发放经验、金币或魅力,后续心跳计算必须基于最新余额。
|
||||
$user->refresh();
|
||||
}
|
||||
|
||||
// 3. 常规心跳经验与金币发放
|
||||
// (模拟前端每30-60秒发一次心跳的过程,此处每分钟跑一次,发放单人心跳奖励)
|
||||
$expGain = $this->parseRewardValue(Sysparam::getValue('exp_per_heartbeat', '1'));
|
||||
$expGain = $this->parseRewardValue($config['exp_per_heartbeat']);
|
||||
if ($expGain > 0) {
|
||||
$expMultiplier = $this->vipService->getExpMultiplier($user);
|
||||
$actualExpGain = (int) round($expGain * $expMultiplier);
|
||||
$user->exp_num += $actualExpGain;
|
||||
}
|
||||
|
||||
$jjbGain = $this->parseRewardValue(Sysparam::getValue('jjb_per_heartbeat', '0'));
|
||||
$jjbGain = $this->parseRewardValue($config['jjb_per_heartbeat']);
|
||||
if ($jjbGain > 0) {
|
||||
$jjbMultiplier = $this->vipService->getJjbMultiplier($user);
|
||||
$actualJjbGain = (int) round($jjbGain * $jjbMultiplier);
|
||||
@@ -95,30 +106,35 @@ class AiHeartbeatCommand extends Command
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$user->refresh();
|
||||
|
||||
// 4. 重算等级(基础心跳升级)
|
||||
$superLevel = (int) Sysparam::getValue('superlevel', '100');
|
||||
$superLevel = (int) $config['superlevel'];
|
||||
$leveledUp = $this->calculateNewLevel($user, $superLevel);
|
||||
|
||||
// 5. 随机事件触发
|
||||
$eventChance = (int) Sysparam::getValue('auto_event_chance', '10');
|
||||
$eventChance = (int) $config['auto_event_chance'];
|
||||
if ($eventChance > 0 && rand(1, 100) <= $eventChance) {
|
||||
$autoEvent = Autoact::randomEvent();
|
||||
if ($autoEvent) {
|
||||
$hasCurrencyChange = false;
|
||||
|
||||
// 执行随机事件的金钱经验惩奖
|
||||
if ($autoEvent->exp_change !== 0) {
|
||||
$this->currencyService->change(
|
||||
$user, 'exp', $autoEvent->exp_change, CurrencySource::AUTO_EVENT, "随机事件:{$autoEvent->text_body}", 1
|
||||
);
|
||||
$hasCurrencyChange = true;
|
||||
}
|
||||
if ($autoEvent->jjb_change !== 0) {
|
||||
$this->currencyService->change(
|
||||
$user, 'gold', $autoEvent->jjb_change, CurrencySource::AUTO_EVENT, "随机事件:{$autoEvent->text_body}", 1
|
||||
);
|
||||
$hasCurrencyChange = true;
|
||||
}
|
||||
|
||||
$user->refresh();
|
||||
if ($hasCurrencyChange) {
|
||||
$user->refresh();
|
||||
}
|
||||
|
||||
// 重新计算等级
|
||||
if ($this->calculateNewLevel($user, $superLevel)) {
|
||||
@@ -149,39 +165,68 @@ class AiHeartbeatCommand extends Command
|
||||
}
|
||||
|
||||
// 7. 钓鱼小游戏随机参与逻辑
|
||||
$fishingEnabled = Sysparam::getValue('chatbot_fishing_enabled', '0') === '1';
|
||||
$fishingChance = (int) Sysparam::getValue('chatbot_fishing_chance', '100'); // 默认 5% 概率
|
||||
if ($fishingEnabled && $fishingChance > 0 && rand(1, 100) <= $fishingChance && \App\Models\GameConfig::isEnabled('fishing')) {
|
||||
$cost = (int) (\App\Models\GameConfig::param('fishing', 'fishing_cost') ?? Sysparam::getValue('fishing_cost', '5'));
|
||||
// 常规小游戏只使用当前手上金币,不再自动从银行补到 100 万。
|
||||
if ($this->aiFinance->prepareSpend($user, $cost)) {
|
||||
// 先扣除费用
|
||||
$this->currencyService->change(
|
||||
$user, 'gold', -$cost,
|
||||
CurrencySource::FISHING_COST,
|
||||
"AI小班长钓鱼抛竿消耗 {$cost} 金币",
|
||||
1,
|
||||
);
|
||||
$fishingEnabled = $config['chatbot_fishing_enabled'] === '1';
|
||||
$fishingChance = (int) $config['chatbot_fishing_chance']; // 默认 100% 概率,保持原有配置默认值。
|
||||
if ($fishingEnabled && $fishingChance > 0 && rand(1, 100) <= $fishingChance) {
|
||||
$fishingConfig = GameConfig::forGame('fishing');
|
||||
|
||||
// 模拟玩家等待时间
|
||||
$waitMin = (int) (\App\Models\GameConfig::param('fishing', 'fishing_wait_min') ?? Sysparam::getValue('fishing_wait_min', '8'));
|
||||
$waitMax = (int) (\App\Models\GameConfig::param('fishing', 'fishing_wait_max') ?? Sysparam::getValue('fishing_wait_max', '15'));
|
||||
$waitTime = rand($waitMin, $waitMax);
|
||||
if ($fishingConfig?->enabled) {
|
||||
$cost = (int) ($fishingConfig->params['fishing_cost'] ?? $config['fishing_cost']);
|
||||
// 常规小游戏只使用当前手上金币,不再自动从银行补到 100 万。
|
||||
if ($this->aiFinance->prepareSpend($user, $cost)) {
|
||||
// 先扣除抛竿费用,再派发延迟收竿任务,避免当前心跳等待钓鱼结果。
|
||||
$this->currencyService->change(
|
||||
$user, 'gold', -$cost,
|
||||
CurrencySource::FISHING_COST,
|
||||
"AI小班长钓鱼抛竿消耗 {$cost} 金币",
|
||||
1,
|
||||
);
|
||||
|
||||
// 延迟派发收竿事件(AI目前统一将事件播报到房间 1,或者拿 active room ids)
|
||||
$activeRoomIds = $this->chatState->getAllActiveRoomIds();
|
||||
$roomId = ! empty($activeRoomIds) ? $activeRoomIds[0] : 1;
|
||||
// 模拟玩家等待时间
|
||||
$waitMin = (int) ($fishingConfig->params['fishing_wait_min'] ?? $config['fishing_wait_min']);
|
||||
$waitMax = (int) ($fishingConfig->params['fishing_wait_max'] ?? $config['fishing_wait_max']);
|
||||
$waitTime = rand($waitMin, $waitMax);
|
||||
|
||||
\App\Jobs\AiFishingJob::dispatch($user, $roomId)->delay(now()->addSeconds($waitTime));
|
||||
// 延迟派发收竿事件(AI目前统一将事件播报到房间 1,或者拿 active room ids)
|
||||
$activeRoomIds = $this->chatState->getAllActiveRoomIds();
|
||||
$roomId = ! empty($activeRoomIds) ? $activeRoomIds[0] : 1;
|
||||
|
||||
AiFishingJob::dispatch($user, $roomId)->delay(now()->addSeconds($waitTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 心跳结束后,若新增金币让手上金额超过 100 万,则把超出的部分重新转回银行。
|
||||
$this->aiFinance->bankExcessGold($user);
|
||||
if ((int) ($user->jjb ?? 0) > AiFinanceService::AVAILABLE_GOLD_RESERVE) {
|
||||
$this->aiFinance->bankExcessGold($user);
|
||||
}
|
||||
|
||||
$elapsedMs = (int) round((microtime(true) - $startedAt) * 1000);
|
||||
$this->info("AI心跳完成,耗时 {$elapsedMs}ms。");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取本轮心跳需要的系统配置,避免命令流程中重复触发配置读取。
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function heartbeatConfig(): array
|
||||
{
|
||||
return [
|
||||
'exp_per_heartbeat' => Sysparam::getValue('exp_per_heartbeat', '1'),
|
||||
'jjb_per_heartbeat' => Sysparam::getValue('jjb_per_heartbeat', '0'),
|
||||
'superlevel' => Sysparam::getValue('superlevel', '100'),
|
||||
'auto_event_chance' => Sysparam::getValue('auto_event_chance', '10'),
|
||||
'chatbot_fishing_enabled' => Sysparam::getValue('chatbot_fishing_enabled', '0'),
|
||||
'chatbot_fishing_chance' => Sysparam::getValue('chatbot_fishing_chance', '100'),
|
||||
'fishing_cost' => Sysparam::getValue('fishing_cost', '5'),
|
||||
'fishing_wait_min' => Sysparam::getValue('fishing_wait_min', '8'),
|
||||
'fishing_wait_max' => Sysparam::getValue('fishing_wait_max', '15'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算并更新用户等级
|
||||
*/
|
||||
@@ -222,7 +267,7 @@ class AiHeartbeatCommand extends Command
|
||||
/**
|
||||
* 尝试为 AI小班长 执行今日签到,成功时广播签到通知。
|
||||
*/
|
||||
private function performDailySignIn(User $user): void
|
||||
private function performDailySignIn(User $user): bool
|
||||
{
|
||||
// 先检查今日是否已签,避免每分钟都调用事务
|
||||
$alreadySigned = DailySignIn::query()
|
||||
@@ -231,7 +276,7 @@ class AiHeartbeatCommand extends Command
|
||||
->exists();
|
||||
|
||||
if ($alreadySigned) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取活跃房间作为签到归属(默认房间 1)
|
||||
@@ -242,7 +287,7 @@ class AiHeartbeatCommand extends Command
|
||||
|
||||
// 仅当本次心跳实际完成签到时才广播(幂等保护)
|
||||
if (! $dailySignIn->wasRecentlyCreated) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$rewardParts = [];
|
||||
@@ -265,6 +310,8 @@ class AiHeartbeatCommand extends Command
|
||||
.$dailySignIn->streak_days.' 天,获得 '.$rewardText.$identityText.'。';
|
||||
|
||||
$this->broadcastSystemMessage('系统传音', $content, '#0f766e');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ use App\Enums\CurrencySource;
|
||||
use App\Events\MessageSent;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\PositionDutyLog;
|
||||
use App\Models\Room;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Services\ChatStateService;
|
||||
@@ -27,6 +28,7 @@ use App\Services\ChatUserPresenceService;
|
||||
use App\Services\UserCurrencyService;
|
||||
use App\Services\VipService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
@@ -65,6 +67,8 @@ class AutoSaveExp extends Command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$startedAt = microtime(true);
|
||||
|
||||
// 读取奖励配置
|
||||
$expGainRaw = Sysparam::getValue('exp_per_heartbeat', '1');
|
||||
$jjbGainRaw = Sysparam::getValue('jjb_per_heartbeat', '0');
|
||||
@@ -81,15 +85,22 @@ class AutoSaveExp extends Command
|
||||
|
||||
// 统计本次处理总人次(一个用户在多个房间会被计算多次)
|
||||
$totalProcessed = 0;
|
||||
$usersByUsername = $this->preloadOnlineUsers($roomMap);
|
||||
|
||||
foreach ($roomMap as $roomId => $usernames) {
|
||||
foreach ($usernames as $username) {
|
||||
$this->processUser($username, $roomId, $expGainRaw, $jjbGainRaw, $superLevel);
|
||||
$user = $usersByUsername->get($username);
|
||||
if (! $user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->processUser($user, $roomId, $expGainRaw, $jjbGainRaw, $superLevel);
|
||||
$totalProcessed++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("自动存点完成,共处理 {$totalProcessed} 个在线用户。");
|
||||
$elapsedMs = (int) round((microtime(true) - $startedAt) * 1000);
|
||||
$this->info('自动存点完成,共扫描 '.count($roomMap)." 个在线房间,处理 {$totalProcessed} 个在线用户,耗时 {$elapsedMs}ms。");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
@@ -108,7 +119,7 @@ class AutoSaveExp extends Command
|
||||
$roomMap = [];
|
||||
|
||||
// 从数据库取出所有房间 ID
|
||||
$roomIds = \App\Models\Room::pluck('id');
|
||||
$roomIds = Room::pluck('id');
|
||||
|
||||
foreach ($roomIds as $roomId) {
|
||||
// Laravel 的 Redis facade 会自动加配置的前缀,与 ChatStateService 存入时完全一致
|
||||
@@ -121,27 +132,46 @@ class AutoSaveExp extends Command
|
||||
return $roomMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载所有在线用户名对应的用户资料与身份关系,避免循环内逐个查询用户和身份信息。
|
||||
*
|
||||
* @param array<int, array<string>> $roomMap 在线房间与用户名映射
|
||||
* @return Collection<string, User> 以用户名为键的用户集合
|
||||
*/
|
||||
private function preloadOnlineUsers(array $roomMap): Collection
|
||||
{
|
||||
$usernames = collect($roomMap)
|
||||
->flatten()
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($usernames->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return User::query()
|
||||
->with(['activePosition.position.department', 'vipLevel'])
|
||||
->whereIn('username', $usernames)
|
||||
->get()
|
||||
->keyBy('username');
|
||||
}
|
||||
|
||||
/**
|
||||
* 为单个在线用户执行存点逻辑,并在其所在房间推送存点通知。
|
||||
*
|
||||
* @param string $username 用户名
|
||||
* @param User $user 已预加载身份关系的在线用户
|
||||
* @param int $roomId 所在房间ID
|
||||
* @param string $expGainRaw 经验奖励原始配置(支持 "1" 或 "1-10" 范围)
|
||||
* @param string $jjbGainRaw 金币奖励原始配置
|
||||
* @param int $superLevel 管理员等级阈值
|
||||
*/
|
||||
private function processUser(
|
||||
string $username,
|
||||
User $user,
|
||||
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);
|
||||
@@ -165,8 +195,11 @@ class AutoSaveExp extends Command
|
||||
$user, 'gold', $actualJjbGain, CurrencySource::AUTO_SAVE, '自动存点', $roomId,
|
||||
);
|
||||
}
|
||||
$user->refresh(); // 刷新获取最新属性(service 已原子更新)
|
||||
$user->load(['activePosition.position.department', 'vipLevel']); // 存点通知需要展示部门、职务与会员身份。
|
||||
if ($actualExpGain > 0 || $actualJjbGain > 0) {
|
||||
// 刷新获取最新属性(service 已原子更新),同时保留后续通知需要展示的身份关系。
|
||||
$user->refresh();
|
||||
$user->load(['activePosition.position.department', 'vipLevel']);
|
||||
}
|
||||
|
||||
// 3. 自动升降级逻辑
|
||||
// - 有在职职务的用户:等级固定为职务对应等级,不随经验变化
|
||||
@@ -241,7 +274,7 @@ class AutoSaveExp extends Command
|
||||
'id' => $this->chatState->nextMessageId($roomId),
|
||||
'room_id' => $roomId,
|
||||
'from_user' => '系统',
|
||||
'to_user' => $username, // 定向推送给本人
|
||||
'to_user' => $user->username, // 定向推送给本人
|
||||
'content' => $content,
|
||||
'is_secret' => true, // 私信模式:前端过滤,只有收件人才能看到
|
||||
'font_color' => '#16a34a', // 草绿色
|
||||
|
||||
Reference in New Issue
Block a user