重构:提取 calculateNewLevel() 私有方法,增加在职职务等级保护逻辑
This commit is contained in:
@@ -80,14 +80,14 @@ class ChatController extends Controller
|
|||||||
// 获取当前在职职务信息(用于内容显示)
|
// 获取当前在职职务信息(用于内容显示)
|
||||||
$activePosition = $user->activePosition;
|
$activePosition = $user->activePosition;
|
||||||
$userData = [
|
$userData = [
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'level' => $user->user_level,
|
'level' => $user->user_level,
|
||||||
'sex' => $user->sex,
|
'sex' => $user->sex,
|
||||||
'headface' => $user->headface,
|
'headface' => $user->headface,
|
||||||
'vip_icon' => $user->vipIcon(),
|
'vip_icon' => $user->vipIcon(),
|
||||||
'vip_name' => $user->vipName(),
|
'vip_name' => $user->vipName(),
|
||||||
'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '',
|
'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '',
|
||||||
'is_admin' => $user->user_level >= $superLevel,
|
'is_admin' => $user->user_level >= $superLevel,
|
||||||
'position_icon' => $activePosition?->position?->icon ?? '',
|
'position_icon' => $activePosition?->position?->icon ?? '',
|
||||||
'position_name' => $activePosition?->position?->name ?? '',
|
'position_name' => $activePosition?->position?->name ?? '',
|
||||||
];
|
];
|
||||||
@@ -419,16 +419,7 @@ class ChatController extends Controller
|
|||||||
// 2. 使用 sysparam 表中可配置的等级-经验阈值计算等级
|
// 2. 使用 sysparam 表中可配置的等级-经验阈值计算等级
|
||||||
// 管理员(superlevel 及以上)不参与自动升降级,等级由后台手动设置
|
// 管理员(superlevel 及以上)不参与自动升降级,等级由后台手动设置
|
||||||
$superLevel = (int) Sysparam::getValue('superlevel', '100');
|
$superLevel = (int) Sysparam::getValue('superlevel', '100');
|
||||||
$oldLevel = $user->user_level;
|
$leveledUp = $this->calculateNewLevel($user, $superLevel);
|
||||||
$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(); // 存点入库
|
$user->save(); // 存点入库
|
||||||
|
|
||||||
@@ -506,12 +497,9 @@ class ChatController extends Controller
|
|||||||
$user->refresh();
|
$user->refresh();
|
||||||
|
|
||||||
// 重新计算等级(经验可能因事件而变化,但管理员不参与自动升降级)
|
// 重新计算等级(经验可能因事件而变化,但管理员不参与自动升降级)
|
||||||
if ($user->user_level < $superLevel) {
|
if ($this->calculateNewLevel($user, $superLevel)) {
|
||||||
$recalcLevel = Sysparam::calculateLevel($user->exp_num);
|
$leveledUp = true; // 随机事件触发了升级,补充标记以便广播
|
||||||
if ($recalcLevel !== $user->user_level && $recalcLevel < $superLevel) {
|
$user->save();
|
||||||
$user->user_level = $recalcLevel;
|
|
||||||
$user->save();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 广播随机事件消息到聊天室
|
// 广播随机事件消息到聊天室
|
||||||
@@ -576,11 +564,11 @@ class ChatController extends Controller
|
|||||||
$onlineCount = count($this->chatState->getRoomUsers($room->id));
|
$onlineCount = count($this->chatState->getRoomUsers($room->id));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $room->id,
|
'id' => $room->id,
|
||||||
'name' => $room->room_name,
|
'name' => $room->room_name,
|
||||||
'online' => $onlineCount,
|
'online' => $onlineCount,
|
||||||
'permit_level' => $room->permit_level ?? 0,
|
'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')
|
->whereNull('logout_at')
|
||||||
->whereDate('login_at', today())
|
->whereDate('login_at', today())
|
||||||
->update([
|
->update([
|
||||||
'logout_at' => now(),
|
'logout_at' => now(),
|
||||||
'duration_seconds' => DB::raw('GREATEST(0, TIMESTAMPDIFF(SECOND, login_at, NOW()))'),
|
'duration_seconds' => DB::raw('GREATEST(0, TIMESTAMPDIFF(SECOND, login_at, NOW()))'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -970,7 +958,7 @@ class ChatController extends Controller
|
|||||||
->whereNull('logout_at')
|
->whereNull('logout_at')
|
||||||
->whereDate('login_at', '<', today())
|
->whereDate('login_at', '<', today())
|
||||||
->update([
|
->update([
|
||||||
'logout_at' => DB::raw('login_at'), // 时长 = 0
|
'logout_at' => DB::raw('login_at'), // 时长 = 0
|
||||||
'duration_seconds' => 0,
|
'duration_seconds' => 0,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1017,11 +1005,61 @@ class ChatController extends Controller
|
|||||||
: now();
|
: now();
|
||||||
|
|
||||||
PositionDutyLog::create([
|
PositionDutyLog::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'user_position_id' => $activeUP->id,
|
'user_position_id' => $activeUP->id,
|
||||||
'login_at' => $loginAt,
|
'login_at' => $loginAt,
|
||||||
'ip_address' => request()->ip(),
|
'ip_address' => request()->ip(),
|
||||||
'room_id' => $roomId,
|
'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
|
.$envelope->id
|
||||||
.',\''.$user->username.'\','
|
.',\''.$user->username.'\','
|
||||||
.self::TOTAL_AMOUNT.','
|
.self::TOTAL_AMOUNT.','
|
||||||
|
|||||||
@@ -384,9 +384,19 @@
|
|||||||
* @param {number} expireSeconds 有效秒数
|
* @param {number} expireSeconds 有效秒数
|
||||||
* @param {'gold'|'exp'} type 货币类型
|
* @param {'gold'|'exp'} type 货币类型
|
||||||
*/
|
*/
|
||||||
window.showRedPacketModal = function(envelopeId, senderUsername, totalAmount, totalCount, expireSeconds,
|
window.showRedPacketModal = async function(envelopeId, senderUsername, totalAmount, totalCount,
|
||||||
|
expireSeconds,
|
||||||
type) {
|
type) {
|
||||||
try {
|
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 触发,当前状态:', {
|
console.log('showRedPacketModal 触发,当前状态:', {
|
||||||
envelopeId,
|
envelopeId,
|
||||||
senderUsername,
|
senderUsername,
|
||||||
@@ -394,13 +404,87 @@
|
|||||||
totalCount,
|
totalCount,
|
||||||
expireSeconds,
|
expireSeconds,
|
||||||
type,
|
type,
|
||||||
|
sentAtUnix,
|
||||||
oldId: _rpEnvelopeId
|
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;
|
_rpEnvelopeId = envelopeId;
|
||||||
_rpClaimed = false;
|
_rpClaimed = false;
|
||||||
|
_rpType = type || 'gold';
|
||||||
|
_rpExpireAt = calculatedExpireAt;
|
||||||
_rpTotalSeconds = expireSeconds;
|
_rpTotalSeconds = expireSeconds;
|
||||||
_rpExpireAt = Date.now() + expireSeconds * 1000;
|
|
||||||
_rpType = type || 'gold'; // 保存类型供 claimRedPacket 使用
|
|
||||||
|
|
||||||
// 根据类型调整配色和标签
|
// 根据类型调整配色和标签
|
||||||
const isExp = (type === 'exp');
|
const isExp = (type === 'exp');
|
||||||
@@ -436,7 +520,7 @@
|
|||||||
document.getElementById('rp-sender-name').textContent = senderUsername + ' 的礼包';
|
document.getElementById('rp-sender-name').textContent = senderUsername + ' 的礼包';
|
||||||
document.getElementById('rp-total-amount').textContent = totalAmount;
|
document.getElementById('rp-total-amount').textContent = totalAmount;
|
||||||
document.getElementById('rp-total-count').textContent = totalCount;
|
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-countdown').textContent = expireSeconds;
|
||||||
document.getElementById('rp-timer-bar').style.width = '100%';
|
document.getElementById('rp-timer-bar').style.width = '100%';
|
||||||
document.getElementById('rp-status-msg').textContent = '';
|
document.getElementById('rp-status-msg').textContent = '';
|
||||||
@@ -478,65 +562,6 @@
|
|||||||
document.getElementById('rp-status-msg').textContent = '红包已过期。';
|
document.getElementById('rp-status-msg').textContent = '红包已过期。';
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 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