重构:提取 calculateNewLevel() 私有方法,增加在职职务等级保护逻辑
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() => {}); // 静默忽略网络错误,不影响弹窗展示
|
||||
};
|
||||
|
||||
// ── 抢包/关闭逻辑 ─────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user