优化存点

This commit is contained in:
2026-04-25 02:24:24 +08:00
parent 5bfcd75442
commit 8bd1dae9e1
6 changed files with 221 additions and 9 deletions
+7 -3
View File
@@ -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),
+6
View File
@@ -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,
+42 -2
View File
@@ -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 {
+101
View File
@@ -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']);
}
}
+60
View File
@@ -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 频道会返回仍在有效期内的当日状态载荷。
*/