diff --git a/app/Console/Commands/AutoSaveExp.php b/app/Console/Commands/AutoSaveExp.php index a89d003..983bd4f 100644 --- a/app/Console/Commands/AutoSaveExp.php +++ b/app/Console/Commands/AutoSaveExp.php @@ -9,22 +9,24 @@ * 3. 在聊天室内推送"系统为你自动存点"提示 * 4. 若用户等级提升,向全频道广播恭喜消息 * - * @package App\Console\Commands * @author ChatRoom Laravel + * * @version 1.0.0 */ namespace App\Console\Commands; -use App\Events\MessageSent; use App\Enums\CurrencySource; +use App\Events\MessageSent; use App\Jobs\SaveMessageJob; +use App\Models\PositionDutyLog; 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\DB; use Illuminate\Support\Facades\Redis; class AutoSaveExp extends Command @@ -43,8 +45,8 @@ 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(); @@ -64,13 +66,14 @@ class AutoSaveExp extends Command // 读取奖励配置 $expGainRaw = Sysparam::getValue('exp_per_heartbeat', '1'); $jjbGainRaw = Sysparam::getValue('jjb_per_heartbeat', '0'); - $superLevel = (int) Sysparam::getValue('superlevel', '100'); + $superLevel = (int) Sysparam::getValue('superlevel', '100'); // 从 Redis 扫描所有在线房间 $roomMap = $this->scanOnlineRooms(); if (empty($roomMap)) { $this->info('当前没有在线用户,跳过存点。'); + return Command::SUCCESS; } @@ -119,11 +122,11 @@ class AutoSaveExp extends Command /** * 为单个在线用户执行存点逻辑,并在其所在房间推送存点通知。 * - * @param string $username 用户名 - * @param int $roomId 所在房间ID + * @param string $username 用户名 + * @param int $roomId 所在房间ID * @param string $expGainRaw 经验奖励原始配置(支持 "1" 或 "1-10" 范围) * @param string $jjbGainRaw 金币奖励原始配置 - * @param int $superLevel 管理员等级阈值 + * @param int $superLevel 管理员等级阈值 */ private function processUser( string $username, @@ -138,11 +141,11 @@ class AutoSaveExp extends Command } // 1. 计算奖励量(经验/金币 均支持 VIP 倍率) - $expGain = $this->parseRewardValue($expGainRaw); + $expGain = $this->parseRewardValue($expGainRaw); $expMultiplier = $this->vipService->getExpMultiplier($user); $actualExpGain = (int) round($expGain * $expMultiplier); - $jjbGain = $this->parseRewardValue($jjbGainRaw); + $jjbGain = $this->parseRewardValue($jjbGainRaw); $actualJjbGain = 0; if ($jjbGain > 0) { $jjbMultiplier = $this->vipService->getJjbMultiplier($user); @@ -163,14 +166,14 @@ class AutoSaveExp extends Command $user->refresh(); // 刷新获取最新属性(service 已原子更新) // 3. 自动升降级(管理员不参与) - $oldLevel = $user->user_level; + $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); + $leveledUp = ($newLevel > $oldLevel); } } @@ -179,15 +182,15 @@ class AutoSaveExp extends Command // 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, + '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(), + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $sysMsg); broadcast(new MessageSent($roomId, $sysMsg)); @@ -203,26 +206,29 @@ class AutoSaveExp extends Command $gainParts[] = "金币+{$actualJjbGain}"; } $jjbDisplay = $user->jjb ?? 0; - $gainStr = ! empty($gainParts) ? ' 本次获得:' . implode(',', $gainParts) : ''; + $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}"; + $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, // 私信模式:前端过滤,只有收件人才能看到 + '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(), + 'action' => '', + 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $noticeMsg); broadcast(new MessageSent($roomId, $noticeMsg)); + + // 6. 同步更新在职用户的勤务时长 + $this->tickDutyLog($user, $roomId); } /** @@ -233,16 +239,63 @@ class AutoSaveExp extends Command * - 随机范围:如 "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; } + + /** + * 自动存点时同步更新或创建在职用户的勤务日志。 + * + * 逻辑同 ChatController::tickDutyLog: + * 1. 无在职职务 → 跳过 + * 2. 今日已有开放日志 → 刷新 duration_seconds + * 3. 今日无日志 → 新建,login_at 取 user->in_time(进房时间) + * + * @param \App\Models\User $user 已 refresh 的用户实例 + * @param int $roomId 所在房间 ID + */ + private function tickDutyLog(User $user, int $roomId): void + { + $activeUP = $user->activePosition; + if (! $activeUP) { + return; + } + + $openLog = PositionDutyLog::query() + ->where('user_id', $user->id) + ->whereNull('logout_at') + ->whereDate('login_at', today()) + ->first(); + + if ($openLog) { + // 绕过模型 cast(integer),使用 DB::table 直接执行 SQL 表达式 + DB::table('position_duty_logs') + ->where('id', $openLog->id) + ->update(['duration_seconds' => DB::raw('GREATEST(0, TIMESTAMPDIFF(SECOND, login_at, NOW()))')]); + + return; + } + + // 今日无日志,新建:取进房时间为 login_at(防止时长丢失) + $loginAt = $user->in_time && $user->in_time->isToday() + ? $user->in_time + : now(); + + PositionDutyLog::create([ + 'user_id' => $user->id, + 'user_position_id' => $activeUP->id, + 'login_at' => $loginAt, + 'ip_address' => '0.0.0.0', // 定时任务无 HTTP 请求,占位符 + 'room_id' => $roomId, + ]); + } } diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index 50e9f8a..9f38125 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -340,6 +340,9 @@ class ChatController extends Controller $user->save(); // 存点入库 + // 手动心跳存点:同步更新在职用户的勤务时长 + $this->tickDutyLog($user, $id); + // 3. 将新的等级反馈给当前用户的在线名单上 // 确保刚刚升级后别人查看到的也是最准确等级 $activePosition = $user->activePosition; @@ -820,7 +823,57 @@ class ChatController extends Controller ->whereNull('logout_at') ->update([ 'logout_at' => now(), - 'duration_seconds' => DB::raw('TIMESTAMPDIFF(SECOND, login_at, NOW())'), + 'duration_seconds' => DB::raw('GREATEST(0, TIMESTAMPDIFF(SECOND, login_at, NOW()))'), ]); } + + /** + * 存点时同步更新或创建在职用户的勤务日志。 + * + * 逻辑: + * 1. 用户无在职职务 → 跳过 + * 2. 今日已有开放日志(无 logout_at)→ 刷新 duration_seconds(实时时长) + * 3. 今日无任何日志 → 新建,login_at 取 user->in_time(进房时间),保证时长不丢失 + * + * @param \App\Models\User $user 当前用户(必须已 fresh/refresh) + * @param int $roomId 所在房间 ID + */ + private function tickDutyLog(User $user, int $roomId): void + { + // 无在职职务,无需记录 + $activeUP = $user->activePosition; + if (! $activeUP) { + return; + } + + // 今日是否已有开放日志 + $openLog = PositionDutyLog::query() + ->where('user_id', $user->id) + ->whereNull('logout_at') + ->whereDate('login_at', today()) + ->first(); + + if ($openLog) { + // 刷新实时在线时长 + // 绕过模型 cast(integer),使用 DB::table 直接执行 SQL 表达式 + DB::table('position_duty_logs') + ->where('id', $openLog->id) + ->update(['duration_seconds' => DB::raw('GREATEST(0, TIMESTAMPDIFF(SECOND, login_at, NOW()))')]); + + return; + } + + // 今日无日志 → 新建,login_at 优先用进房时间 + $loginAt = $user->in_time && $user->in_time->isToday() + ? $user->in_time + : now(); + + PositionDutyLog::create([ + 'user_id' => $user->id, + 'user_position_id' => $activeUP->id, + 'login_at' => $loginAt, + 'ip_address' => request()->ip(), + 'room_id' => $roomId, + ]); + } } diff --git a/config/database.php b/config/database.php index df933e7..0bfb8da 100644 --- a/config/database.php +++ b/config/database.php @@ -58,6 +58,7 @@ return [ 'prefix_indexes' => true, 'strict' => true, 'engine' => null, + 'timezone' => '+08:00', // 与 PHP Asia/Shanghai 时区对齐,NOW() 返回北京时间 'options' => extension_loaded('pdo_mysql') ? array_filter([ (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [],