diff --git a/app/Console/Commands/AutoSaveExp.php b/app/Console/Commands/AutoSaveExp.php
index 6ca0e2d..0f1b8ba 100644
--- a/app/Console/Commands/AutoSaveExp.php
+++ b/app/Console/Commands/AutoSaveExp.php
@@ -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),
diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php
index b83c52c..ebc560d 100644
--- a/app/Http/Controllers/ChatController.php
+++ b/app/Http/Controllers/ChatController.php
@@ -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,
diff --git a/app/Services/ChatUserPresenceService.php b/app/Services/ChatUserPresenceService.php
index 7ba8869..fc1115a 100644
--- a/app/Services/ChatUserPresenceService.php
+++ b/app/Services/ChatUserPresenceService.php
@@ -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}",
+ ];
+ }
+
/**
* 读取用户当前仍然有效的当日状态。
*
diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php
index 5a8372e..afaac9f 100644
--- a/resources/views/chat/partials/scripts.blade.php
+++ b/resources/views/chat/partials/scripts.blade.php
@@ -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 =
- `【${levelTitle}存点】您的最新情况:${levelInfo} ${gainInfo}(${timeStr})`;
+ `【${escapeHtml(levelTitle)}存点】${escapeHtml(levelInfo + gainInfo)}(${timeStr})`;
container2.appendChild(detailDiv);
if (autoScroll) container2.scrollTop = container2.scrollHeight;
} else {
diff --git a/tests/Feature/AutoSaveExpCommandTest.php b/tests/Feature/AutoSaveExpCommandTest.php
new file mode 100644
index 0000000..5936044
--- /dev/null
+++ b/tests/Feature/AutoSaveExpCommandTest.php
@@ -0,0 +1,101 @@
+ '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']);
+ }
+}
diff --git a/tests/Feature/ChatControllerTest.php b/tests/Feature/ChatControllerTest.php
index 39d9117..4d8e9ba 100644
--- a/tests/Feature/ChatControllerTest.php
+++ b/tests/Feature/ChatControllerTest.php
@@ -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 频道会返回仍在有效期内的当日状态载荷。
*/