重构:提取 calculateNewLevel() 私有方法,增加在职职务等级保护逻辑

This commit is contained in:
2026-03-12 06:52:40 +08:00
parent 529a59551c
commit 174ee8241d
3 changed files with 164 additions and 98 deletions
+72 -34
View File
@@ -80,14 +80,14 @@ class ChatController extends Controller
// 获取当前在职职务信息(用于内容显示)
$activePosition = $user->activePosition;
$userData = [
'user_id' => $user->id,
'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,
'user_id' => $user->id,
'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 ?? '',
];
@@ -419,16 +419,7 @@ class ChatController extends Controller
// 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);
}
}
$leveledUp = $this->calculateNewLevel($user, $superLevel);
$user->save(); // 存点入库
@@ -506,12 +497,9 @@ class ChatController extends Controller
$user->refresh();
// 重新计算等级(经验可能因事件而变化,但管理员不参与自动升降级)
if ($user->user_level < $superLevel) {
$recalcLevel = Sysparam::calculateLevel($user->exp_num);
if ($recalcLevel !== $user->user_level && $recalcLevel < $superLevel) {
$user->user_level = $recalcLevel;
$user->save();
}
if ($this->calculateNewLevel($user, $superLevel)) {
$leveledUp = true; // 随机事件触发了升级,补充标记以便广播
$user->save();
}
// 广播随机事件消息到聊天室
@@ -576,11 +564,11 @@ class ChatController extends Controller
$onlineCount = count($this->chatState->getRoomUsers($room->id));
return [
'id' => $room->id,
'name' => $room->room_name,
'online' => $onlineCount,
'id' => $room->id,
'name' => $room->room_name,
'online' => $onlineCount,
'permit_level' => $room->permit_level ?? 0,
'door_open' => (bool) $room->door_open,
'door_open' => (bool) $room->door_open,
];
});
@@ -960,7 +948,7 @@ class ChatController extends Controller
->whereNull('logout_at')
->whereDate('login_at', today())
->update([
'logout_at' => now(),
'logout_at' => now(),
'duration_seconds' => DB::raw('GREATEST(0, TIMESTAMPDIFF(SECOND, login_at, NOW()))'),
]);
@@ -970,7 +958,7 @@ class ChatController extends Controller
->whereNull('logout_at')
->whereDate('login_at', '<', today())
->update([
'logout_at' => DB::raw('login_at'), // 时长 = 0
'logout_at' => DB::raw('login_at'), // 时长 = 0
'duration_seconds' => 0,
]);
}
@@ -1017,11 +1005,61 @@ class ChatController extends Controller
: now();
PositionDutyLog::create([
'user_id' => $user->id,
'user_id' => $user->id,
'user_position_id' => $activeUP->id,
'login_at' => $loginAt,
'ip_address' => request()->ip(),
'room_id' => $roomId,
'login_at' => $loginAt,
'ip_address' => request()->ip(),
'room_id' => $roomId,
]);
}
/**
* 根据经验值重新计算用户等级,申升减级均会直接修改 $user->user_level。
*
* PHP 对象引用传递,方法内对 $user 的修改会直接反映到调用方。
* 本方法不负责 save(),由调用方决定何时落库。
*
* @param \App\Models\User $user 当前用户模型
* @param int $superLevel 管理员等级阈值(达到后不参与自动升降级)
* @return bool 是否发生了升级(true = 等级提升)
*/
private function calculateNewLevel(\App\Models\User $user, int $superLevel): bool
{
// 管理员等级由后台手动维护,不参与自动升降级
if ($user->user_level >= $superLevel) {
return false;
}
$newLevel = Sysparam::calculateLevel($user->exp_num);
// 等级无变化,或计算结果达到管理员阈值(异常情况),均跳过
if ($newLevel === $user->user_level || $newLevel >= $superLevel) {
return false;
}
$isLeveledUp = $newLevel > $user->user_level;
// 在职职务成员:等级保护逻辑
$activeUP = $user->activePosition;
if ($activeUP) {
$positionLevel = $activeUP->position->level ?? 0;
// 职务要求高于当前等级 → 强制补级到职务最低要求
if ($positionLevel > $user->user_level) {
$user->user_level = $positionLevel;
return true; // 等级提升,调用方需保存并广播
}
// 降级 且 降后等级低于职务要求 → 阻止
if (! $isLeveledUp && $newLevel < $positionLevel) {
return false;
}
}
// PHP 对象引用传递,这里对 $user->user_level 的修改将直接反映到调用方
$user->user_level = $newLevel;
return $isLeveledUp;
}
}
+4 -1
View File
@@ -126,7 +126,10 @@ class RedPacketController extends Controller
});
// 广播系统公告,含可点击「立即抢包」按钮
$btnHtml = '<button onclick="showRedPacketModal('
// 注意这里不能死命传 self::EXPIRE_SECONDS,因为这句话会被存入数据库的历史记录。我们需要在取出来的时候能根据发包时间动态变化!
// 啊等等!由于这条消息是直接静态写入 `chat_messages` 内容里的,这就意味着如果在这里计算,存进去的还是 300。
// 所以我们还是传 `self::EXPIRE_SECONDS` 作为总寿命,在前端逻辑里利用 `Date.now()` 和消息的 `sent_at` 来算出真实剩余倒计时更为严谨!
$btnHtml = '<button data-sent-at="'.time().'" onclick="showRedPacketModal('
.$envelope->id
.',\''.$user->username.'\','
.self::TOTAL_AMOUNT.','
@@ -384,9 +384,19 @@
* @param {number} expireSeconds 有效秒数
* @param {'gold'|'exp'} type 货币类型
*/
window.showRedPacketModal = function(envelopeId, senderUsername, totalAmount, totalCount, expireSeconds,
window.showRedPacketModal = async function(envelopeId, senderUsername, totalAmount, totalCount,
expireSeconds,
type) {
try {
// 尝试获取点击按钮附带的发包真实时间戳(兼容历史数据)
let sentAtUnix = null;
if (window.event && window.event.currentTarget) {
const btn = window.event.currentTarget;
if (btn.dataset && btn.dataset.sentAt) {
sentAtUnix = parseInt(btn.dataset.sentAt, 10);
}
}
console.log('showRedPacketModal 触发,当前状态:', {
envelopeId,
senderUsername,
@@ -394,13 +404,87 @@
totalCount,
expireSeconds,
type,
sentAtUnix,
oldId: _rpEnvelopeId
});
// 计算真实过期时间点
let calculatedExpireAt = Date.now() + expireSeconds * 1000;
if (sentAtUnix && !isNaN(sentAtUnix) && sentAtUnix > 0) {
calculatedExpireAt = (sentAtUnix + expireSeconds) * 1000;
}
// 【前置拦截1】如果有时间戳并算出已过期,直接杀死不弹窗
if (sentAtUnix && Date.now() >= calculatedExpireAt) {
if (typeof window.chatToast === 'function') {
window.chatToast('该红包已过期。', 'info');
} else {
alert('该红包已过期。');
}
console.log('红包已准确断定过期,拦截弹窗显示:', envelopeId);
return;
}
// 【统一前置拦截】无论新老红包、有无时间戳,为彻底杜绝闪现,强制上云查册生死再放行!
let currentRemaining = totalCount;
try {
const res = await fetch(`/red-packet/${envelopeId}/status`, {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')
.content,
},
});
const initialStatusData = await res.json();
if (initialStatusData.status === 'success') {
if (initialStatusData.is_expired || initialStatusData.envelope_status ===
'expired') {
window.chatToast?.show({
title: '⏰ 礼包已过期',
message: '该红包已过期,无法领取。',
icon: '⏰',
color: '#9ca3af',
duration: 4000,
});
return; // 判定死亡,直接退出,永不渲染弹窗!
}
if (initialStatusData.remaining_count <= 0 || initialStatusData
.envelope_status === 'completed') {
window.chatToast?.show({
title: '😅 手慢了!',
message: '红包已被抢完,下次要快一点哦!',
icon: '🧧',
color: '#f59e0b',
duration: 4000,
});
return;
}
if (initialStatusData.has_claimed) {
window.chatToast?.show({
title: '✅ 已领取',
message: '您已成功领取过本次礼包!',
icon: '🧧',
color: '#10b981',
duration: 4000,
});
return; // 判定已领取,直接退出
}
// 记录真实的剩余倒计时以备展示
currentRemaining = initialStatusData.remaining_count;
totalCount = initialStatusData.total_count || totalCount;
}
} catch (e) {
console.error('红包状态前置预查失败:', e);
}
// --------- 到此,证实它不仅没死甚至还很活泼、也没领取过。开始安心布置并渲染弹窗 ---------
_rpEnvelopeId = envelopeId;
_rpClaimed = false;
_rpType = type || 'gold';
_rpExpireAt = calculatedExpireAt;
_rpTotalSeconds = expireSeconds;
_rpExpireAt = Date.now() + expireSeconds * 1000;
_rpType = type || 'gold'; // 保存类型供 claimRedPacket 使用
// 根据类型调整配色和标签
const isExp = (type === 'exp');
@@ -436,7 +520,7 @@
document.getElementById('rp-sender-name').textContent = senderUsername + ' 的礼包';
document.getElementById('rp-total-amount').textContent = totalAmount;
document.getElementById('rp-total-count').textContent = totalCount;
document.getElementById('rp-remaining').textContent = totalCount;
document.getElementById('rp-remaining').textContent = currentRemaining;
document.getElementById('rp-countdown').textContent = expireSeconds;
document.getElementById('rp-timer-bar').style.width = '100%';
document.getElementById('rp-status-msg').textContent = '';
@@ -478,65 +562,6 @@
document.getElementById('rp-status-msg').textContent = '红包已过期。';
}
}, 1000);
// 异步拉取服务端最新状态(实时刷新剩余份数)
fetch(`/red-packet/${envelopeId}/status`, {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
})
.then(r => r.json())
.then(data => {
if (data.status !== 'success') {
return;
}
// 更新剩余份数显示
document.getElementById('rp-remaining').textContent = data.remaining_count;
// 若已过期 → 关闭弹窗 + Toast 提示
if (data.is_expired || data.envelope_status === 'expired') {
clearInterval(_rpTimer);
closeRedPacketModal();
window.chatToast?.show({
title: '⏰ 礼包已过期',
message: '该礼包已超过有效期,无法领取。',
icon: '⏰',
color: '#9ca3af',
duration: 4000,
});
return;
}
// 若已抢完 → 关闭弹窗 + Toast 提示
if (data.remaining_count <= 0 || data.envelope_status === 'completed') {
clearInterval(_rpTimer);
closeRedPacketModal();
window.chatToast?.show({
title: '😅 手慢了!',
message: '礼包已被抢完,下次要快一点哦!',
icon: '🧧',
color: '#f59e0b',
duration: 4000,
});
return;
}
// 若本人已领取 → 关闭弹窗 + Toast 提示
if (data.has_claimed) {
clearInterval(_rpTimer);
closeRedPacketModal();
window.chatToast?.show({
title: '✅ 已领取',
message: '您已成功领取过本次礼包!',
icon: '🧧',
color: '#10b981',
duration: 4000,
});
}
})
.catch(() => {}); // 静默忽略网络错误,不影响弹窗展示
};
// ── 抢包/关闭逻辑 ─────────────────────────────────────