优化存点
This commit is contained in:
@@ -23,6 +23,7 @@ use App\Models\PositionDutyLog;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\ChatUserPresenceService;
|
||||
use App\Services\UserCurrencyService;
|
||||
use App\Services\VipService;
|
||||
use Illuminate\Console\Command;
|
||||
@@ -46,6 +47,7 @@ class AutoSaveExp extends Command
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState,
|
||||
private readonly ChatUserPresenceService $chatUserPresenceService,
|
||||
private readonly VipService $vipService,
|
||||
private readonly UserCurrencyService $currencyService,
|
||||
) {
|
||||
@@ -164,7 +166,7 @@ class AutoSaveExp extends Command
|
||||
);
|
||||
}
|
||||
$user->refresh(); // 刷新获取最新属性(service 已原子更新)
|
||||
$user->load('activePosition.position'); // 确保职务及职位关联已加载
|
||||
$user->load(['activePosition.position.department', 'vipLevel']); // 存点通知需要展示部门、职务与会员身份。
|
||||
|
||||
// 3. 自动升降级逻辑
|
||||
// - 有在职职务的用户:等级固定为职务对应等级,不随经验变化
|
||||
@@ -229,9 +231,11 @@ class AutoSaveExp extends Command
|
||||
$jjbDisplay = $user->jjb ?? 0;
|
||||
$gainStr = ! empty($gainParts) ? ' 本次获得:'.implode(',', $gainParts) : '';
|
||||
|
||||
// 格式:⏰ 自动存点 · LV.100 · 经验 10,468 · 金币 8,345 · 已满级 · 本次获得:经验+1,金币+3
|
||||
$identitySummary = $this->chatUserPresenceService->buildIdentitySummary($user);
|
||||
|
||||
// 格式:⏰ 自动存点 · 部门 X · 职务 Y · 会员 Z · LV.100 · 经验 10,468 · 金币 8,345 · 已满级 · 本次获得:经验+1,金币+3
|
||||
$statusTag = $user->user_level >= $superLevel ? ' · 已满级 ✓' : '';
|
||||
$content = "⏰ 自动存点 · LV.{$user->user_level} · 经验 {$user->exp_num} · 金币 {$jjbDisplay}{$statusTag}{$gainStr}";
|
||||
$content = "⏰ 自动存点 · {$identitySummary['inline']} · LV.{$user->user_level} · 经验 {$user->exp_num} · 金币 {$jjbDisplay}{$statusTag}{$gainStr}";
|
||||
|
||||
$noticeMsg = [
|
||||
'id' => $this->chatState->nextMessageId($roomId),
|
||||
|
||||
@@ -634,6 +634,7 @@ class ChatController extends Controller
|
||||
} elseif ($user->user_level >= $superLevel) {
|
||||
$title = '管理员';
|
||||
}
|
||||
$identitySummary = $this->chatUserPresenceService->buildIdentitySummary($user);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
@@ -644,6 +645,11 @@ class ChatController extends Controller
|
||||
'jjb_gain' => $actualJjbGain,
|
||||
'user_level' => $user->user_level,
|
||||
'title' => $title,
|
||||
'identity_summary' => $identitySummary['inline'],
|
||||
'department_name' => $identitySummary['department_name'],
|
||||
'position_name' => $identitySummary['position_name'],
|
||||
'vip_name' => $identitySummary['vip_name'],
|
||||
'vip_icon' => $identitySummary['vip_icon'],
|
||||
'leveled_up' => $leveledUp,
|
||||
'is_max_level' => $user->user_level >= $superLevel,
|
||||
'auto_event' => $autoEvent ? $autoEvent->renderText($user->username) : null,
|
||||
|
||||
@@ -11,6 +11,9 @@ use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Support\ChatDailyStatusCatalog;
|
||||
|
||||
/**
|
||||
* 类功能:统一生成聊天室在线用户与身份展示相关载荷。
|
||||
*/
|
||||
class ChatUserPresenceService
|
||||
{
|
||||
/**
|
||||
@@ -21,8 +24,8 @@ class ChatUserPresenceService
|
||||
public function build(User $user): array
|
||||
{
|
||||
$superLevel = (int) Sysparam::getValue('superlevel', '100');
|
||||
$activePosition = $user->activePosition;
|
||||
$position = $activePosition?->position;
|
||||
$user->loadMissing(['activePosition.position.department', 'vipLevel']);
|
||||
$position = $user->activePosition?->position;
|
||||
$payload = [
|
||||
'id' => $user->id,
|
||||
'user_id' => $user->id,
|
||||
@@ -64,6 +67,43 @@ class ChatUserPresenceService
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户部门、职务与会员展示信息。
|
||||
*
|
||||
* @return array{
|
||||
* department_name: string,
|
||||
* position_icon: string,
|
||||
* position_name: string,
|
||||
* vip_icon: string,
|
||||
* vip_name: string,
|
||||
* vip_label: string,
|
||||
* inline: string
|
||||
* }
|
||||
*/
|
||||
public function buildIdentitySummary(User $user): array
|
||||
{
|
||||
$user->loadMissing(['activePosition.position.department', 'vipLevel']);
|
||||
|
||||
$position = $user->activePosition?->position;
|
||||
$departmentName = $position?->department?->name ?? '无部门';
|
||||
$positionIcon = $position?->icon ?? '';
|
||||
$positionName = $position?->name ?? '无职务';
|
||||
$vipIcon = $user->vipIcon();
|
||||
$vipName = $user->vipName() ?: '普通会员';
|
||||
$vipLabel = trim($vipIcon.' '.$vipName);
|
||||
$positionLabel = trim($positionIcon.' '.$positionName);
|
||||
|
||||
return [
|
||||
'department_name' => $departmentName,
|
||||
'position_icon' => $positionIcon,
|
||||
'position_name' => $positionName,
|
||||
'vip_icon' => $vipIcon,
|
||||
'vip_name' => $vipName,
|
||||
'vip_label' => $vipLabel,
|
||||
'inline' => "部门 {$departmentName} · 职务 {$positionLabel} · 会员 {$vipLabel}",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取用户当前仍然有效的当日状态。
|
||||
*
|
||||
|
||||
@@ -3716,12 +3716,13 @@
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
const d = data.data;
|
||||
const levelTitle = d.title || '普通会员';
|
||||
const identitySummary = d.identity_summary ? `${d.identity_summary} · ` : '';
|
||||
|
||||
let levelInfo = '';
|
||||
if (d.is_max_level) {
|
||||
levelInfo = `级别(${d.user_level});经验(${d.exp_num});金币(${d.jjb}枚);已满级。`;
|
||||
levelInfo = `⏰ 自动存点 · ${identitySummary}LV.${d.user_level} · 经验 ${d.exp_num} · 金币 ${d.jjb} · 已满级 ✓`;
|
||||
} else {
|
||||
levelInfo = `级别(${d.user_level});经验(${d.exp_num});金币(${d.jjb}枚)。`;
|
||||
levelInfo = `⏰ 自动存点 · ${identitySummary}LV.${d.user_level} · 经验 ${d.exp_num} · 金币 ${d.jjb}`;
|
||||
}
|
||||
|
||||
// 本次获得的奖励提示
|
||||
@@ -3730,7 +3731,7 @@
|
||||
const parts = [];
|
||||
if (d.exp_gain > 0) parts.push(`经验+${d.exp_gain}`);
|
||||
if (d.jjb_gain > 0) parts.push(`金币+${d.jjb_gain}`);
|
||||
gainInfo = `(本次: ${parts.join(', ')})`;
|
||||
gainInfo = ` 本次获得:${parts.join(',')}`;
|
||||
}
|
||||
|
||||
if (data.data.leveled_up) {
|
||||
@@ -3746,7 +3747,7 @@
|
||||
const detailDiv = document.createElement('div');
|
||||
detailDiv.className = 'msg-line';
|
||||
detailDiv.innerHTML =
|
||||
`<span style="color: green;">【${levelTitle}存点】您的最新情况:${levelInfo} ${gainInfo}</span><span class="msg-time">(${timeStr})</span>`;
|
||||
`<span style="color: green;">【${escapeHtml(levelTitle)}存点】${escapeHtml(levelInfo + gainInfo)}</span><span class="msg-time">(${timeStr})</span>`;
|
||||
container2.appendChild(detailDiv);
|
||||
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:自动存点命令功能测试
|
||||
*
|
||||
* 覆盖定时自动存点私信内容,确保用户身份信息展示完整。
|
||||
*/
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\Department;
|
||||
use App\Models\Position;
|
||||
use App\Models\Room;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPosition;
|
||||
use App\Models\VipLevel;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 类功能:验证自动存点命令推送给用户的状态通知内容。
|
||||
*/
|
||||
class AutoSaveExpCommandTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 每个测试前清空 Redis 与配置缓存,避免在线状态和系统参数串扰。
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Redis::flushall();
|
||||
Cache::flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试自动存点私信会展示部门、职务与会员信息。
|
||||
*/
|
||||
public function test_auto_save_notice_includes_department_position_and_vip_identity(): void
|
||||
{
|
||||
Event::fake([MessageSent::class]);
|
||||
|
||||
Sysparam::updateOrCreate(['alias' => 'exp_per_heartbeat'], ['body' => '0']);
|
||||
Sysparam::updateOrCreate(['alias' => 'jjb_per_heartbeat'], ['body' => '0']);
|
||||
Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']);
|
||||
Cache::flush();
|
||||
|
||||
$room = Room::create(['room_name' => 'asave']);
|
||||
$vipLevel = VipLevel::factory()->create([
|
||||
'name' => '至尊会员',
|
||||
'icon' => '👑',
|
||||
]);
|
||||
$user = User::factory()->create([
|
||||
'username' => 'autosave-user',
|
||||
'user_level' => 100,
|
||||
'exp_num' => 168762,
|
||||
'jjb' => 1100017,
|
||||
'vip_level_id' => $vipLevel->id,
|
||||
'hy_time' => now()->addDay(),
|
||||
]);
|
||||
$department = Department::create([
|
||||
'name' => '办公厅',
|
||||
'rank' => 100,
|
||||
'color' => '#1d4ed8',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
$position = Position::create([
|
||||
'department_id' => $department->id,
|
||||
'name' => '厅长',
|
||||
'icon' => '🏛️',
|
||||
'rank' => 100,
|
||||
'level' => 100,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
UserPosition::create([
|
||||
'user_id' => $user->id,
|
||||
'position_id' => $position->id,
|
||||
'appointed_at' => now(),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Redis::hset("room:{$room->id}:users", $user->username, json_encode(['username' => $user->username], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$this->artisan('chatroom:auto-save-exp')->assertSuccessful();
|
||||
|
||||
$messages = collect(Redis::lrange("room:{$room->id}:messages", 0, -1))
|
||||
->map(fn (string $message): array => json_decode($message, true));
|
||||
$notice = $messages->first(fn (array $message): bool => ($message['to_user'] ?? '') === $user->username);
|
||||
|
||||
$this->assertIsArray($notice);
|
||||
$this->assertStringContainsString('部门 办公厅 · 职务 🏛️ 厅长 · 会员 👑 至尊会员', $notice['content']);
|
||||
$this->assertStringContainsString('LV.100 · 经验 168762 · 金币 1100017 · 已满级 ✓', $notice['content']);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,10 @@ use App\Events\MessageSent;
|
||||
use App\Models\Department;
|
||||
use App\Models\Position;
|
||||
use App\Models\Room;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPosition;
|
||||
use App\Models\VipLevel;
|
||||
use App\Support\PositionPermissionRegistry;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
@@ -21,6 +23,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
@@ -149,6 +152,63 @@ class ChatControllerTest extends TestCase
|
||||
$this->assertSame($position?->department?->name, $authorizedPayload['department_name'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试手动存点接口会返回部门、职务与会员展示字段,供前端复用自动存点格式。
|
||||
*/
|
||||
public function test_heartbeat_returns_identity_summary_for_save_notice(): void
|
||||
{
|
||||
Sysparam::updateOrCreate(['alias' => 'exp_per_heartbeat'], ['body' => '0']);
|
||||
Sysparam::updateOrCreate(['alias' => 'jjb_per_heartbeat'], ['body' => '0']);
|
||||
Sysparam::updateOrCreate(['alias' => 'auto_event_chance'], ['body' => '0']);
|
||||
Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']);
|
||||
Cache::flush();
|
||||
|
||||
$room = Room::create([
|
||||
'room_name' => 'hbident',
|
||||
'door_open' => true,
|
||||
]);
|
||||
$vipLevel = VipLevel::factory()->create([
|
||||
'name' => '至尊会员',
|
||||
'icon' => '👑',
|
||||
]);
|
||||
$user = User::factory()->create([
|
||||
'user_level' => 100,
|
||||
'exp_num' => 168762,
|
||||
'jjb' => 1100017,
|
||||
'vip_level_id' => $vipLevel->id,
|
||||
'hy_time' => now()->addDay(),
|
||||
]);
|
||||
$department = Department::create([
|
||||
'name' => '办公厅',
|
||||
'rank' => 100,
|
||||
'color' => '#1d4ed8',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
$position = Position::create([
|
||||
'department_id' => $department->id,
|
||||
'name' => '厅长',
|
||||
'icon' => '🏛️',
|
||||
'rank' => 100,
|
||||
'level' => 100,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
UserPosition::create([
|
||||
'user_id' => $user->id,
|
||||
'position_id' => $position->id,
|
||||
'appointed_at' => now(),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->postJson(route('chat.heartbeat', $room->id));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.identity_summary', '部门 办公厅 · 职务 🏛️ 厅长 · 会员 👑 至尊会员');
|
||||
$response->assertJsonPath('data.department_name', '办公厅');
|
||||
$response->assertJsonPath('data.position_name', '厅长');
|
||||
$response->assertJsonPath('data.vip_name', '至尊会员');
|
||||
$response->assertJsonPath('data.vip_icon', '👑');
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试聊天室 Presence 频道会返回仍在有效期内的当日状态载荷。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user