increment('visit_num');
// 用户进房时间刷新
$user->update(['in_time' => now()]);
// 1. 将当前用户加入到 Redis 房间在线列表(包含 VIP 和管理员信息)
$superLevel = (int) Sysparam::getValue('superlevel', '100');
// 获取当前在职职务信息(用于内容显示)
$activePosition = $user->activePosition;
$userData = [
'level' => $user->user_level,
'sex' => $user->sex,
'headface' => $user->headface,
'vip_icon' => $user->vipIcon(),
'vip_name' => $user->vipName(),
'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '',
'is_admin' => $user->user_level >= $superLevel,
'position_icon' => $activePosition?->position?->icon ?? '',
'position_name' => $activePosition?->position?->name ?? '',
];
$this->chatState->userJoin($id, $user->username, $userData);
// 2. 广播 UserJoined 事件,通知房间内的其他人
broadcast(new UserJoined($id, $user->username, $userData))->toOthers();
// 3. 新人首次进入:赠送 6666 金币、播放满场烟花、发送全场欢迎通告
$newbieEffect = null;
if (! $user->has_received_new_gift) {
// 通过统一积分服务发放新人礼包 6666 金币并记录流水
$this->currencyService->change(
$user, 'gold', 6666, CurrencySource::NEWBIE_BONUS, '新人首次入场婿赠的 6666 金币大礼包', $id,
);
$user->update(['has_received_new_gift' => true]);
// 发送新人专属欢迎公告
$newbieMsg = [
'id' => $this->chatState->nextMessageId($id),
'room_id' => $id,
'from_user' => '系统公告',
'to_user' => '大家',
'content' => "🎉 缤纷礼花满天飞,热烈欢迎新朋友 【{$user->username}】 首次驾临本聊天室!系统已自动赠送 6666 金币新人大礼包!",
'is_secret' => false,
'font_color' => '#b91c1c',
'action' => '',
'welcome_user' => $user->username,
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($id, $newbieMsg);
broadcast(new MessageSent($id, $newbieMsg));
// 广播烟花特效给此时已在房间的其他用户
broadcast(new \App\Events\EffectBroadcast($id, 'fireworks', $user->username))->toOthers();
// 传给前端,让新人自己的屏幕上也燃放烟花
$newbieEffect = 'fireworks';
}
// 4. superlevel 管理员进入:触发全房间烟花 + 系统公告,其他人走通用播报
// 每次进入先清理掉历史中旧的欢迎消息,保证同一个人只保留最后一条
$this->chatState->removeOldWelcomeMessages($id, $user->username);
if ($user->user_level >= $superLevel) {
// 管理员专属:全房间烟花
broadcast(new \App\Events\EffectBroadcast($id, 'fireworks', $user->username));
$welcomeMsg = [
'id' => $this->chatState->nextMessageId($id),
'room_id' => $id,
'from_user' => '系统公告',
'to_user' => '大家',
'content' => "🎉 欢迎管理员 【{$user->username}】 驾临本聊天室!请各位文明聊天!",
'is_secret' => false,
'font_color' => '#b91c1c',
'action' => 'admin_welcome',
'welcome_user' => $user->username,
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($id, $welcomeMsg);
broadcast(new MessageSent($id, $welcomeMsg));
} else {
// 5. 非站长:生成通用播报(有职务 > 有VIP > 普通随机词)
[$text, $color] = $this->broadcast->buildEntryBroadcast($user);
$generalWelcomeMsg = [
'id' => $this->chatState->nextMessageId($id),
'room_id' => $id,
'from_user' => '进出播报',
'to_user' => '大家',
'content' => "{$text}",
'is_secret' => false,
'font_color' => $color,
'action' => 'system_welcome',
'welcome_user' => $user->username,
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($id, $generalWelcomeMsg);
broadcast(new MessageSent($id, $generalWelcomeMsg))->toOthers();
}
// 6. 获取历史消息并过滤,只保留和当前用户相关的消息用于初次渲染
// 规则:公众发言(to_user=大家 或空)保留;私聊/系统通知只保留涉及本人的
$allHistory = $this->chatState->getNewMessages($id, 0);
$username = $user->username;
$historyMessages = array_values(array_filter($allHistory, function ($msg) use ($username) {
$toUser = $msg['to_user'] ?? '';
$fromUser = $msg['from_user'] ?? '';
$isSecret = ! empty($msg['is_secret']);
// 公众发言(对大家说):所有人都可以看到
if ($toUser === '大家' || $toUser === '') {
return true;
}
// 私信 / 悄悄话:只显示发给自己或自己发出的
if ($isSecret) {
return $fromUser === $username || $toUser === $username;
}
// 对特定人说话:只显示发给自己或自己发出的(含系统通知)
return $fromUser === $username || $toUser === $username;
}));
// 渲染主聊天框架视图
return view('chat.frame', [
'room' => $room,
'user' => $user,
'weekEffect' => $this->shopService->getActiveWeekEffect($user),
'newbieEffect' => $newbieEffect,
'historyMessages' => $historyMessages,
]);
// 最后:如果用户有在职职务,开始记录这次入场的在职登录
// 此时用户局部变量已初始化,可以安全读取 in_time
$activeUP = $user->activePosition;
if ($activeUP) {
PositionDutyLog::create([
'user_id' => $user->id,
'user_position_id' => $activeUP->id,
'login_at' => now(),
'ip_address' => request()->ip(),
'room_id' => $id,
]);
}
}
/**
* 发送消息 (等同于原版 NEWSAY.ASP)
*
* @param int $id 房间ID
*/
public function send(SendMessageRequest $request, int $id): JsonResponse
{
$data = $request->validated();
$user = Auth::user();
// 0. 检查用户是否被禁言(Redis TTL 自动过期)
$muteKey = "mute:{$id}:{$user->username}";
if (Redis::exists($muteKey)) {
$ttl = Redis::ttl($muteKey);
$minutes = ceil($ttl / 60);
return response()->json([
'status' => 'error',
'message' => "您正在禁言中,还需等待约 {$minutes} 分钟。",
], 403);
}
// 0.5 检查接收方是否在线(防幽灵消息)
$toUser = $data['to_user'] ?? '大家';
if ($toUser !== '大家' && ! in_array($toUser, ['系统公告', '系统传音', '送花播报', '进出播报', '钓鱼播报', '星海小博士', 'AI小班长'])) {
// Redis 保存的在线列表
$isOnline = Redis::hexists("room:{$id}:users", $toUser);
if (! $isOnline) {
// 使用 200 状态码,避免 Nginx 拦截非 2xx 响应后触发重定向导致 405 Method Not Allowed
return response()->json([
'status' => 'error',
'message' => "【{$toUser}】目前已离开聊天室或不在线,消息未发出。",
], 200);
}
}
// 1. 过滤净化消息体
$pureContent = $this->filter->filter($data['content'] ?? '');
if (empty($pureContent)) {
return response()->json(['status' => 'error', 'message' => '消息内容不能为空或不合法。'], 422);
}
// 2. 封装消息对象
$messageData = [
'id' => $this->chatState->nextMessageId($id), // 分布式安全自增序号
'room_id' => $id,
'from_user' => $user->username,
'to_user' => $data['to_user'] ?? '大家',
'content' => $pureContent,
'is_secret' => $data['is_secret'] ?? false,
'font_color' => $data['font_color'] ?? '',
'action' => $data['action'] ?? '',
'sent_at' => now()->toDateTimeString(),
];
// 3. 压入 Redis 缓存列表 (防炸内存,只保留最近 N 条)
$this->chatState->pushMessage($id, $messageData);
// 4. 立刻向 WebSocket 发射广播,前端达到 0 延迟渲染
broadcast(new MessageSent($id, $messageData));
// 5. 丢进异步列队,慢慢持久化到 MySQL,保护数据库连接池
SaveMessageJob::dispatch($messageData);
// 6. 如果用户更换了字体颜色,顺便保存到 s_color 字段,下次进入时恢复
$chosenColor = $data['font_color'] ?? '';
if ($chosenColor && $chosenColor !== ($user->s_color ?? '')) {
$user->s_color = $chosenColor;
$user->save();
}
// 7. 聊天给魅力值(仅对指定用户的非悄悄话公开发言有效)
$toUser = $data['to_user'] ?? '大家';
$isSecret = $data['is_secret'] ?? false;
if ($toUser !== '大家' && ! $isSecret) {
$this->grantChatCharm($user, $toUser);
}
return response()->json(['status' => 'success']);
}
/**
* 自动挂机存点心跳与经验升级 (新增)
* 替代原版定时 iframe 刷新的 save.asp。
*
* @param int $id 房间ID
*/
public function heartbeat(Request $request, int $id): JsonResponse
{
$user = Auth::user();
if (! $user) {
return response()->json(['status' => 'error'], 401);
}
// 1. 心跳奖励:通过 Redis 限制最小间隔(默认30秒),防止频繁点击
$cooldownKey = "heartbeat_exp:{$user->id}";
$canGainReward = ! Redis::exists($cooldownKey);
$actualExpGain = 0;
$actualJjbGain = 0;
if ($canGainReward) {
// 经验奖励(支持固定值 "1" 或范围 "1-10")
$expGain = $this->parseRewardValue(Sysparam::getValue('exp_per_heartbeat', '1'));
$expMultiplier = $this->vipService->getExpMultiplier($user);
$actualExpGain = (int) round($expGain * $expMultiplier);
$user->exp_num += $actualExpGain;
// 金币奖励(支持固定值 "1" 或范围 "1-5")
$jjbGain = $this->parseRewardValue(Sysparam::getValue('jjb_per_heartbeat', '0'));
if ($jjbGain > 0) {
$jjbMultiplier = $this->vipService->getJjbMultiplier($user);
$actualJjbGain = (int) round($jjbGain * $jjbMultiplier);
$user->jjb = ($user->jjb ?? 0) + $actualJjbGain;
}
// 设置冷却(30秒内不再给奖励)
Redis::setex($cooldownKey, 30, 1);
}
// 2. 使用 sysparam 表中可配置的等级-经验阈值计算等级
// 管理员(superlevel 及以上)不参与自动升降级,等级由后台手动设置
$superLevel = (int) Sysparam::getValue('superlevel', '100');
$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(); // 存点入库
// 手动心跳存点:同步更新在职用户的勤务时长
$this->tickDutyLog($user, $id);
// 3. 将新的等级反馈给当前用户的在线名单上
// 确保刚刚升级后别人查看到的也是最准确等级
$activePosition = $user->activePosition;
$this->chatState->userJoin($id, $user->username, [
'level' => $user->user_level,
'sex' => $user->sex,
'headface' => $user->headface,
'vip_icon' => $user->vipIcon(),
'vip_name' => $user->vipName(),
'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '',
'is_admin' => $user->user_level >= $superLevel,
'position_icon' => $activePosition?->position?->icon ?? '',
'position_name' => $activePosition?->position?->name ?? '',
]);
// 4. 如果突破境界,向全房系统喊话广播!
if ($leveledUp) {
// 生成炫酷广播消息发向该频道
$sysMsg = [
'id' => $this->chatState->nextMessageId($id),
'room_id' => $id,
'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($id, $sysMsg);
broadcast(new MessageSent($id, $sysMsg));
// 落库
SaveMessageJob::dispatch($sysMsg);
}
// 5. 随机事件触发(复刻原版 autoact 系统,概率可在后台配置)
$autoEvent = null;
$eventChance = (int) Sysparam::getValue('auto_event_chance', '10');
if ($eventChance > 0 && rand(1, 100) <= $eventChance) {
$autoEvent = Autoact::randomEvent();
if ($autoEvent) {
// 应用经验/金币变化(不低于 0)
if ($autoEvent->exp_change !== 0) {
$user->exp_num = max(0, $user->exp_num + $autoEvent->exp_change);
}
if ($autoEvent->jjb_change !== 0) {
$user->jjb = max(0, ($user->jjb ?? 0) + $autoEvent->jjb_change);
}
$user->save();
// 重新计算等级(经验可能因事件而变化,但管理员不参与自动升降级)
if ($user->user_level < $superLevel) {
$recalcLevel = Sysparam::calculateLevel($user->exp_num);
if ($recalcLevel !== $user->user_level && $recalcLevel < $superLevel) {
$user->user_level = $recalcLevel;
$user->save();
}
}
// 广播随机事件消息到聊天室
$eventMsg = [
'id' => $this->chatState->nextMessageId($id),
'room_id' => $id,
'from_user' => '星海小博士',
'to_user' => '大家',
'content' => $autoEvent->renderText($user->username),
'is_secret' => false,
'font_color' => match ($autoEvent->event_type) {
'good' => '#16a34a', // 绿色(好运)
'bad' => '#dc2626', // 红色(坏运)
default => '#7c3aed', // 紫色(中性)
},
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($id, $eventMsg);
broadcast(new MessageSent($id, $eventMsg));
SaveMessageJob::dispatch($eventMsg);
}
}
// 确定用户称号:管理员 > VIP 名称 > 普通会员
$title = '普通会员';
if ($user->user_level >= $superLevel) {
$title = '管理员';
} elseif ($user->isVip()) {
$title = $user->vipName() ?: '会员';
}
return response()->json([
'status' => 'success',
'data' => [
'exp_num' => $user->exp_num,
'jjb' => $user->jjb ?? 0,
'exp_gain' => $actualExpGain,
'jjb_gain' => $actualJjbGain,
'user_level' => $user->user_level,
'title' => $title,
'leveled_up' => $leveledUp,
'is_max_level' => $user->user_level >= $superLevel,
'auto_event' => $autoEvent ? $autoEvent->renderText($user->username) : null,
],
]);
}
/**
* 离开房间 (等同于原版 LEAVE.ASP)
*
* @param int $id 房间ID
*/
public function leave(Request $request, int $id): JsonResponse
{
$user = Auth::user();
if (! $user) {
return response()->json(['status' => 'error'], 401);
}
// 1. 从 Redis 删除该用户
$this->chatState->userLeave($id, $user->username);
// 记录退出时间和退出信息
$user->update([
'out_time' => now(),
'out_info' => '正常退出了房间',
]);
// 关闭该用户尚未结束的在职登录记录(结算在线时长)
$this->closeDutyLog($user->id);
// 2. 发送离场播报
$superLevel = (int) Sysparam::getValue('superlevel', '100');
if ($user->user_level >= $superLevel) {
// 管理员离场:系统公告
$leaveMsg = [
'id' => $this->chatState->nextMessageId($id),
'room_id' => $id,
'from_user' => '系统公告',
'to_user' => '大家',
'content' => "👋 管理员 【{$user->username}】 已离开聊天室。",
'is_secret' => false,
'font_color' => '#b91c1c',
'action' => 'admin_welcome',
'welcome_user' => $user->username,
'sent_at' => now()->toDateTimeString(),
];
} else {
[$leaveText, $color] = $this->broadcast->buildLeaveBroadcast($user);
$leaveMsg = [
'id' => $this->chatState->nextMessageId($id),
'room_id' => $id,
'from_user' => '进出播报',
'to_user' => '大家',
'content' => "{$leaveText}",
'is_secret' => false,
'font_color' => $color,
'action' => 'system_welcome',
'welcome_user' => $user->username,
'sent_at' => now()->toDateTimeString(),
];
}
$this->chatState->pushMessage($id, $leaveMsg);
// 3. 广播通知他人 (UserLeft 更新用户名单列表,MessageSent 更新消息记录)
broadcast(new UserLeft($id, $user->username))->toOthers();
broadcast(new MessageSent($id, $leaveMsg))->toOthers();
return response()->json(['status' => 'success']);
}
/**
* 获取可用头像列表(返回 JSON)
* 扫描 /public/images/headface/ 目录,返回所有可用头像文件名
*/
public function headfaceList(): JsonResponse
{
$dir = public_path('images/headface');
$files = [];
if (is_dir($dir)) {
$all = scandir($dir);
foreach ($all as $file) {
// 只包含图片文件
if (preg_match('/\.(gif|jpg|jpeg|png|bmp)$/i', $file)) {
$files[] = $file;
}
}
}
// 自然排序(1, 2, 3... 10, 11...)
natsort($files);
return response()->json(['headfaces' => array_values($files)]);
}
/**
* 修改头像(原版 fw.asp 功能)
* 用户选择一个头像文件名,更新到 usersf 字段
*/
public function changeAvatar(Request $request): JsonResponse
{
$user = Auth::user();
$headface = $request->input('headface', '');
if (empty($headface)) {
return response()->json(['status' => 'error', 'message' => '请选择一个头像'], 422);
}
// 验证文件确实存在
if (! file_exists(public_path('images/headface/'.$headface))) {
return response()->json(['status' => 'error', 'message' => '头像文件不存在'], 422);
}
// 更新用户头像
$user->usersf = $headface;
$user->save();
// 将新头像同步到 Redis 在线用户列表中(所有房间)
// 通过更新 Redis 的用户信息,使得其他用户和自己刷新后都能看到新头像
$superLevel = (int) Sysparam::getValue('superlevel', '100');
$rooms = $this->chatState->getUserRooms($user->username);
foreach ($rooms as $roomId) {
$this->chatState->userJoin((int) $roomId, $user->username, [
'level' => $user->user_level,
'sex' => $user->sex,
'headface' => $headface,
'vip_icon' => $user->vipIcon(),
'vip_name' => $user->vipName(),
'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '',
'is_admin' => $user->user_level >= $superLevel,
]);
}
return response()->json([
'status' => 'success',
'message' => '头像修改成功!',
'headface' => $headface,
]);
}
/**
* 设置房间公告/祝福语(滚动显示在聊天室顶部)
* 需要房间主人或等级达到 level_announcement 配置值
*
* @param int $id 房间ID
*/
public function setAnnouncement(Request $request, int $id): JsonResponse
{
$user = Auth::user();
$room = Room::findOrFail($id);
// 权限检查:房间主人 或 等级 >= level_announcement
$requiredLevel = (int) Sysparam::getValue('level_announcement', '10');
if ($user->username !== $room->master && $user->user_level < $requiredLevel) {
return response()->json(['status' => 'error', 'message' => '权限不足,无法修改公告'], 403);
}
$request->validate([
'announcement' => 'required|string|max:500',
]);
$room->announcement = $request->input('announcement');
$room->save();
// 广播公告更新到所有在线用户
$sysMsg = [
'id' => $this->chatState->nextMessageId($id),
'room_id' => $id,
'from_user' => '系统公告',
'to_user' => '大家',
'content' => "📢 {$user->username} 更新了房间公告:{$room->announcement}",
'is_secret' => false,
'font_color' => '#cc0000',
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($id, $sysMsg);
broadcast(new MessageSent($id, $sysMsg));
return response()->json([
'status' => 'success',
'message' => '公告已更新!',
'announcement' => $room->announcement,
]);
}
/**
* 送花/礼物:消耗金币给目标用户增加魅力值
*
* 根据 gift_id 查找 gifts 表中的礼物类型,读取对应的金币消耗和魅力增量。
* 送花成功后在聊天室广播带图片的消息。
*/
public function sendFlower(Request $request): JsonResponse
{
$request->validate([
'to_user' => 'required|string',
'room_id' => 'required|integer',
'gift_id' => 'required|integer',
'count' => 'sometimes|integer|min:1|max:99',
]);
$user = Auth::user();
if (! $user) {
return response()->json(['status' => 'error', 'message' => '请先登录'], 401);
}
$toUsername = $request->input('to_user');
$roomId = $request->input('room_id');
$giftId = $request->integer('gift_id');
$count = $request->integer('count', 1);
// 不能给自己送花
if ($toUsername === $user->username) {
return response()->json(['status' => 'error', 'message' => '不能给自己送花哦~']);
}
// 查找礼物类型
$gift = Gift::where('id', $giftId)->where('is_active', true)->first();
if (! $gift) {
return response()->json(['status' => 'error', 'message' => '礼物不存在或已下架']);
}
// 查找目标用户
$toUser = User::where('username', $toUsername)->first();
if (! $toUser) {
return response()->json(['status' => 'error', 'message' => '用户不存在']);
}
$totalCost = $gift->cost * $count;
$totalCharm = $gift->charm * $count;
// 检查金币余额
if (($user->jjb ?? 0) < $totalCost) {
return response()->json([
'status' => 'error',
'message' => "金币不足!送 {$count} 份【{$gift->name}】需要 {$totalCost} 金币,您当前有 ".($user->jjb ?? 0).' 枚。',
]);
}
// 扣除金币、增加对方魅力
$user->jjb = ($user->jjb ?? 0) - $totalCost;
$user->save();
$toUser->meili = ($toUser->meili ?? 0) + $totalCharm;
$toUser->save();
// 构建礼物图片 URL
$giftImageUrl = $gift->image ? "/images/gifts/{$gift->image}" : '';
// 广播送花消息(含图片标记,前端识别后渲染图片)
$countText = $count > 1 ? " {$count} 份" : '';
$sysMsg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '送花播报',
'to_user' => $toUsername,
'content' => "{$gift->emoji} 【{$user->username}】 向 【{$toUsername}】 送出了{$countText}【{$gift->name}】!魅力 +{$totalCharm}!",
'is_secret' => false,
'font_color' => '#e91e8f',
'action' => '',
'sent_at' => now()->toDateTimeString(),
'gift_image' => $giftImageUrl,
'gift_name' => $gift->name,
];
$this->chatState->pushMessage($roomId, $sysMsg);
broadcast(new MessageSent($roomId, $sysMsg));
SaveMessageJob::dispatch($sysMsg);
return response()->json([
'status' => 'success',
'message' => "送花成功!花费 {$totalCost} 金币,{$toUsername} 魅力 +{$totalCharm}",
'data' => [
'my_jjb' => $user->jjb,
'target_charm' => $toUser->meili,
],
]);
}
/**
* 聊天获取魅力值(方案 B:每条消息触发,Redis 每小时上限控制)
*
* 异性聊天给更多魅力,同性少一些。
* 系统用户(如 AI小班长)不触发魅力奖励。
* 发送者和接收者都会获得对应魅力值。
*
* @param mixed $sender 发送消息的用户模型
* @param string $toUsername 接收消息的用户名
*/
private function grantChatCharm(mixed $sender, string $toUsername): void
{
// 系统用户不参与魅力计算
$systemNames = ['大家', '系统传音', '系统公告', '钓鱼播报', '星海小博士', 'AI小班长', '送花播报'];
if (in_array($toUsername, $systemNames)) {
return;
}
// 查找接收者
$receiver = User::where('username', $toUsername)->first();
if (! $receiver) {
return;
}
// 检查发送者每小时魅力上限(Redis 自动过期)
$capKey = "charm_cap:{$sender->username}:".date('YmdH');
$hourlyLimit = (int) Sysparam::getValue('charm_hourly_limit', '20');
$currentGained = (int) Redis::get($capKey);
if ($currentGained >= $hourlyLimit) {
return; // 已达本小时上限
}
// 根据性别关系计算魅力增量
$senderSex = $sender->sex ?? '';
$receiverSex = $receiver->sex ?? '';
$isCrossSex = ($senderSex !== $receiverSex) && $senderSex !== '' && $receiverSex !== '';
$charmSame = (int) Sysparam::getValue('charm_same_sex', '1');
$charmCross = (int) Sysparam::getValue('charm_cross_sex', '2');
$charmGain = $isCrossSex ? $charmCross : $charmSame;
// 不超过本小时剩余额度
$remaining = $hourlyLimit - $currentGained;
$charmGain = min($charmGain, $remaining);
if ($charmGain <= 0) {
return;
}
// 发送者获得魅力
$sender->meili = ($sender->meili ?? 0) + $charmGain;
$sender->save();
// 更新 Redis 计数器(1 小时过期)
Redis::incrby($capKey, $charmGain);
Redis::expire($capKey, 3600);
}
/**
* 解析奖励数值配置(支持固定值或范围格式)
*
* 支持格式:
* "5" → 固定返回 5
* "1-10" → 随机返回 1~10 之间的整数
* "0" → 返回 0(关闭该奖励)
*
* @param string $value 配置值
* @return int 解析后的奖励数值
*/
private function parseRewardValue(string $value): int
{
$value = trim($value);
// 支持范围格式 "min-max"
if (str_contains($value, '-')) {
$parts = explode('-', $value, 2);
$min = max(0, (int) $parts[0]);
$max = max($min, (int) $parts[1]);
return rand($min, $max);
}
return max(0, (int) $value);
}
/**
* 关闭该用户尚未结束的在职登录记录(结算在线时长)
* 在用户退出房间或心跳超时时调用
*
* @param int $userId 用户 ID
*/
private function closeDutyLog(int $userId): void
{
PositionDutyLog::query()
->where('user_id', $userId)
->whereNull('logout_at')
->update([
'logout_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,
]);
}
}