feat: 任命/撤销通知系统 + 用户名片UI优化
- 任命/撤销事件增加 type 字段区分类型 - 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息 - 撤销:灰色弹窗 + 灰色系统消息,无礼花 - 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏 - 系统消息加随机鼓励语(各5条轮换) - ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds) - 用户名片折叠优化:管理员视野、职务履历均可折叠 - 管理操作 + 职务操作合并为「🔧 管理操作」折叠区 - 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:职务任命服务
|
||||
* 处理职务系统的核心业务逻辑:任命、撤销、权限校验
|
||||
* 所有权限操作均写入 position_authority_logs 留存审计
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Position;
|
||||
use App\Models\PositionAuthorityLog;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPosition;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AppointmentService
|
||||
{
|
||||
/**
|
||||
* 获取用户当前在职记录(无则返回 null)
|
||||
*/
|
||||
public function getActivePosition(User $user): ?UserPosition
|
||||
{
|
||||
return UserPosition::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('is_active', true)
|
||||
->with(['position.department', 'position.appointablePositions'])
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验操作人是否有权将目标用户任命到指定职务
|
||||
*
|
||||
* id=1 超级管理员绕过所有要目校验,可直接任命任意职务。
|
||||
*
|
||||
* @return array{ok: bool, message: string}
|
||||
*/
|
||||
public function validateAppoint(User $operator, User $target, Position $targetPosition): array
|
||||
{
|
||||
// 超级管理员(id=1)特权:跳过职务和白名单校验,只检查被任命人是否已有职务
|
||||
if ($operator->id === 1) {
|
||||
$existingPosition = $this->getActivePosition($target);
|
||||
if ($existingPosition) {
|
||||
$currentName = $existingPosition->position->name;
|
||||
|
||||
return ['ok' => false, 'message' => "【{$target->username}】当前已担任【{$currentName}】,请先撤销其职务再重新任命。"];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => '超级管理员直接授权'];
|
||||
}
|
||||
|
||||
// 操作人必须有在职职务
|
||||
$operatorPosition = $this->getActivePosition($operator);
|
||||
if (! $operatorPosition) {
|
||||
return ['ok' => false, 'message' => '您当前无在职职务,无法进行任命操作。'];
|
||||
}
|
||||
|
||||
// 校验任命白名单:目标职务是否在操作人职务的可任命列表内
|
||||
$isAllowed = $operatorPosition->position
|
||||
->appointablePositions()
|
||||
->where('positions.id', $targetPosition->id)
|
||||
->exists();
|
||||
|
||||
if (! $isAllowed) {
|
||||
return ['ok' => false, 'message' => "您的职务无权任命【{$targetPosition->name}】职位。"];
|
||||
}
|
||||
|
||||
// 检查目标职务是否已满员
|
||||
if ($targetPosition->isFull()) {
|
||||
return ['ok' => false, 'message' => "【{$targetPosition->name}】职位人数已满,无法继续任命。"];
|
||||
}
|
||||
|
||||
// 检查被任命人是否已有在职职务
|
||||
$existingPosition = $this->getActivePosition($target);
|
||||
if ($existingPosition) {
|
||||
$currentName = $existingPosition->position->name;
|
||||
|
||||
return ['ok' => false, 'message' => "【{$target->username}】当前已担任【{$currentName}】,请先撤销其职务再重新任命。"];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => '校验通过'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任命操作
|
||||
* 任命成功后自动同步 user_level 并写入权限日志
|
||||
*
|
||||
* @return array{ok: bool, message: string, userPosition?: UserPosition}
|
||||
*/
|
||||
public function appoint(User $operator, User $target, Position $targetPosition, ?string $remark = null): array
|
||||
{
|
||||
// 权限校验
|
||||
$validation = $this->validateAppoint($operator, $target, $targetPosition);
|
||||
if (! $validation['ok']) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// id=1 超级管理员无需在职职务,直接任命
|
||||
$operatorPosition = $operator->id === 1 ? null : $this->getActivePosition($operator);
|
||||
|
||||
DB::transaction(function () use ($operator, $target, $targetPosition, $remark, $operatorPosition, &$userPosition) {
|
||||
// 创建任职记录
|
||||
$userPosition = UserPosition::create([
|
||||
'user_id' => $target->id,
|
||||
'position_id' => $targetPosition->id,
|
||||
'appointed_by_user_id' => $operator->id,
|
||||
'appointed_at' => now(),
|
||||
'remark' => $remark,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// 同步 user_level
|
||||
$target->update(['user_level' => $targetPosition->level]);
|
||||
|
||||
// 写入权限操作日志
|
||||
$this->logAuthority(
|
||||
operator: $operator,
|
||||
operatorPosition: $operatorPosition,
|
||||
actionType: 'appoint',
|
||||
target: $target,
|
||||
targetPosition: $targetPosition,
|
||||
remark: $remark
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => "已成功将【{$target->username}】任命为【{$targetPosition->name}】。",
|
||||
'userPosition' => $userPosition,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验操作人是否有权撤销目标用户的职务
|
||||
*
|
||||
* id=1 超级管理员可直接撤销任意职务。
|
||||
*
|
||||
* @return array{ok: bool, message: string}
|
||||
*/
|
||||
public function validateRevoke(User $operator, User $target): array
|
||||
{
|
||||
// 超级管理员(id=1)特权:跳过白名单校验,直接撤销任意职务
|
||||
if ($operator->id === 1) {
|
||||
$targetPosition = $this->getActivePosition($target);
|
||||
if (! $targetPosition) {
|
||||
return ['ok' => false, 'message' => "【{$target->username}】当前没有在职职务。"];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => '超级管理员直接授权'];
|
||||
}
|
||||
|
||||
// 操作人必须有在职职务
|
||||
$operatorPosition = $this->getActivePosition($operator);
|
||||
if (! $operatorPosition) {
|
||||
return ['ok' => false, 'message' => '您当前无在职职务,无法进行撤职操作。'];
|
||||
}
|
||||
|
||||
// 被撤销人必须有在职职务
|
||||
$targetPosition = $this->getActivePosition($target);
|
||||
if (! $targetPosition) {
|
||||
return ['ok' => false, 'message' => "【{$target->username}】当前没有在职职务。"];
|
||||
}
|
||||
|
||||
// 操作人不能撤销自己
|
||||
if ($operator->id === $target->id) {
|
||||
return ['ok' => false, 'message' => '不能撤销自己的职务。'];
|
||||
}
|
||||
|
||||
// 操作人的任命白名单中必须包含目标职务(即有权任命该职务,也就有权撤销)
|
||||
$isAllowed = $operatorPosition->position
|
||||
->appointablePositions()
|
||||
->where('positions.id', $targetPosition->position_id)
|
||||
->exists();
|
||||
|
||||
if (! $isAllowed) {
|
||||
return ['ok' => false, 'message' => "您的职务无权撤销【{$targetPosition->position->name}】职位的人员。"];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => '校验通过'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行撤销职务操作
|
||||
* 撤销后 user_level 归 1,并写入权限日志
|
||||
*
|
||||
* @return array{ok: bool, message: string}
|
||||
*/
|
||||
public function revoke(User $operator, User $target, ?string $remark = null): array
|
||||
{
|
||||
// 权限校验
|
||||
$validation = $this->validateRevoke($operator, $target);
|
||||
if (! $validation['ok']) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
$operatorPosition = $this->getActivePosition($operator);
|
||||
$targetUP = $this->getActivePosition($target);
|
||||
|
||||
DB::transaction(function () use ($operator, $target, $remark, $operatorPosition, $targetUP) {
|
||||
// 撤销在职记录
|
||||
$targetUP->update([
|
||||
'is_active' => false,
|
||||
'revoked_at' => now(),
|
||||
'revoked_by_user_id' => $operator->id,
|
||||
]);
|
||||
|
||||
// 关闭尚未结束的 duty_log
|
||||
$target->activePosition?->dutyLogs()
|
||||
->whereNull('logout_at')
|
||||
->update([
|
||||
'logout_at' => now(),
|
||||
'duration_seconds' => DB::raw('TIMESTAMPDIFF(SECOND, login_at, NOW())'),
|
||||
]);
|
||||
|
||||
// user_level 归 1(由系统经验值自然升级机制重新成长)
|
||||
$target->update(['user_level' => 1]);
|
||||
|
||||
// 写入权限操作日志
|
||||
$this->logAuthority(
|
||||
operator: $operator,
|
||||
operatorPosition: $operatorPosition,
|
||||
actionType: 'revoke',
|
||||
target: $target,
|
||||
targetPosition: $targetUP->position,
|
||||
remark: $remark
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => "已成功撤销【{$target->username}】的【{$targetUP->position->name}】职务,其等级已归 1。",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录权限操作日志(各类管理操作公共调用)
|
||||
*
|
||||
* @param string $actionType 操作类型(appoint/revoke/reward/warn/kick/mute/banip/other)
|
||||
*/
|
||||
public function logAuthority(
|
||||
User $operator,
|
||||
?UserPosition $operatorPosition,
|
||||
string $actionType,
|
||||
User $target,
|
||||
?Position $targetPosition = null,
|
||||
?int $amount = null,
|
||||
?string $remark = null,
|
||||
): void {
|
||||
// 无在职职务的操作不记录(普通管理员通过 user_level 操作不进此表)
|
||||
if (! $operatorPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
PositionAuthorityLog::create([
|
||||
'user_id' => $operator->id,
|
||||
'user_position_id' => $operatorPosition->id,
|
||||
'action_type' => $actionType,
|
||||
'target_user_id' => $target->id,
|
||||
'target_position_id' => $targetPosition?->id,
|
||||
'amount' => $amount,
|
||||
'remark' => $remark,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视图用:操作人有权任命的职务列表(用于后台/弹窗任命下拉选择)
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, Position>
|
||||
*/
|
||||
public function getAppointablePositions(User $operator)
|
||||
{
|
||||
$operatorPosition = $this->getActivePosition($operator);
|
||||
if (! $operatorPosition) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $operatorPosition->position
|
||||
->appointablePositions()
|
||||
->with('department')
|
||||
->orderByDesc('rank')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -53,15 +53,18 @@ class ChatStateService
|
||||
public function getUserRooms(string $username): array
|
||||
{
|
||||
$rooms = [];
|
||||
$prefix = config('database.redis.options.prefix', '');
|
||||
$cursor = '0';
|
||||
do {
|
||||
[$cursor, $keys] = Redis::scan($cursor, ['match' => 'room:*:users', 'count' => 100]);
|
||||
foreach ($keys ?? [] as $key) {
|
||||
if (Redis::hexists($key, $username)) {
|
||||
// 从 key "room:123:users" 中提取房间 ID
|
||||
preg_match('/room:(\d+):users/', $key, $matches);
|
||||
if (isset($matches[1])) {
|
||||
$rooms[] = (int) $matches[1];
|
||||
// scan 带前缀通配,返回的 key 也带前缀
|
||||
[$cursor, $keys] = Redis::scan($cursor, ['match' => $prefix.'room:*:users', 'count' => 100]);
|
||||
foreach ($keys ?? [] as $fullKey) {
|
||||
// 去掉前缀得到 Laravel Redis Facade 认识的短少 key
|
||||
$shortKey = $prefix ? ltrim(substr($fullKey, strlen($prefix)), '') : $fullKey;
|
||||
if (Redis::hexists($shortKey, $username)) {
|
||||
preg_match('/room:(\d+):users/', $shortKey, $m);
|
||||
if (isset($m[1])) {
|
||||
$rooms[] = (int) $m[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,6 +91,34 @@ class ChatStateService
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描 Redis,返回当前所有有在线用户的房间 ID 数组(用于全局广播)。
|
||||
*
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getAllActiveRoomIds(): array
|
||||
{
|
||||
$roomIds = [];
|
||||
$prefix = config('database.redis.options.prefix', '');
|
||||
$cursor = '0';
|
||||
do {
|
||||
// scan 带前缀通配,返回的 key 也带前缀
|
||||
[$cursor, $keys] = Redis::scan($cursor, ['match' => $prefix.'room:*:users', 'count' => 100]);
|
||||
foreach ($keys ?? [] as $fullKey) {
|
||||
$shortKey = $prefix ? substr($fullKey, strlen($prefix)) : $fullKey;
|
||||
// 只有 hash 非空(有在线用户)才算活跃房间
|
||||
if (Redis::hlen($shortKey) > 0) {
|
||||
preg_match('/room:(\d+):users/', $shortKey, $m);
|
||||
if (isset($m[1])) {
|
||||
$roomIds[] = (int) $m[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
} while ($cursor !== '0');
|
||||
|
||||
return array_unique($roomIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一条新发言推入 Redis 列表,并限制最大保留数量,防止内存泄漏。
|
||||
*
|
||||
@@ -115,6 +146,42 @@ class ChatStateService
|
||||
Redis::del($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定房间内,关于某个用户的旧欢迎消息(支持普通人、管理员、新人)。
|
||||
* 保证聊天记录里只保留最新的一条,解决频繁进出造成的刷屏问题。
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $username 用户名
|
||||
*/
|
||||
public function removeOldWelcomeMessages(int $roomId, string $username): void
|
||||
{
|
||||
$key = "room:{$roomId}:messages";
|
||||
$messages = Redis::lrange($key, 0, -1);
|
||||
|
||||
if (empty($messages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
|
||||
foreach ($messages as $msgJson) {
|
||||
$msg = json_decode($msgJson, true);
|
||||
// 只要消息里带了 welcome_user 且等于当前用户,就抛弃这条旧的
|
||||
if ($msg && isset($msg['welcome_user']) && $msg['welcome_user'] === $username) {
|
||||
continue;
|
||||
}
|
||||
$filtered[] = $msgJson;
|
||||
}
|
||||
|
||||
// 重新写回 Redis(如果发生了过滤)
|
||||
if (count($filtered) !== count($messages)) {
|
||||
Redis::del($key);
|
||||
if (! empty($filtered)) {
|
||||
Redis::rpush($key, ...$filtered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定房间的新发言记录。
|
||||
* 在高频长轮询或前端断线重连拉取时使用。
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天室入场/离场播报服务
|
||||
* 负责构建进出播报文本与颜色,按优先级(职务 > VIP > 普通随机词)选择合适的播报样式。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class RoomBroadcastService
|
||||
{
|
||||
/**
|
||||
* 构造函数注入 VIP 服务(用于获取 VIP 专属入场/离场模板)
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly VipService $vipService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 构建入场播报,返回 [文本, 颜色]
|
||||
* 优先级:有职务 > 有 VIP(专属模板优先)> 普通随机词
|
||||
*
|
||||
* @return array{string, string}
|
||||
*/
|
||||
public function buildEntryBroadcast(User $user): array
|
||||
{
|
||||
$position = $user->activePosition?->position;
|
||||
|
||||
// 有职务:显示职务图标 + 随机入场词
|
||||
if ($position) {
|
||||
$icon = $position->icon ?? '🎖️';
|
||||
$name = $position->name;
|
||||
$text = '【'.$icon.' '.$name.'】'.$this->randomWelcomeMsg($user);
|
||||
|
||||
return [$text, '#7c3aed']; // 紫色
|
||||
}
|
||||
|
||||
// 有 VIP:优先用专属进入模板,无模板则随机词加前缀
|
||||
if ($user->isVip() && $user->vipLevel) {
|
||||
$color = $user->vipLevel->color ?: '#f59e0b';
|
||||
$template = $this->vipService->getJoinMessage($user);
|
||||
|
||||
if ($template) {
|
||||
return [$template, $color];
|
||||
}
|
||||
|
||||
$text = '【'.$user->vipIcon().' '.$user->vipName().'】'.$this->randomWelcomeMsg($user);
|
||||
|
||||
return [$text, $color];
|
||||
}
|
||||
|
||||
// 普通用户:绿色随机词
|
||||
return [$this->randomWelcomeMsg($user), '#16a34a'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建离场播报,返回 [文本, 颜色]
|
||||
* 优先级:有职务 > 有 VIP(专属模板优先)> 普通随机词
|
||||
*
|
||||
* @return array{string, string}
|
||||
*/
|
||||
public function buildLeaveBroadcast(User $user): array
|
||||
{
|
||||
$position = $user->activePosition?->position;
|
||||
|
||||
// 有职务:显示职务图标 + 随机离场词
|
||||
if ($position) {
|
||||
$icon = $position->icon ?? '🎖️';
|
||||
$name = $position->name;
|
||||
$text = '【'.$icon.' '.$name.'】'.$this->randomLeaveMsg($user);
|
||||
|
||||
return [$text, '#7c3aed']; // 紫色
|
||||
}
|
||||
|
||||
// 有 VIP:优先用专属离场模板,无模板则随机词加前缀
|
||||
if ($user->isVip() && $user->vipLevel) {
|
||||
$color = $user->vipLevel->color ?: '#f59e0b';
|
||||
$template = $this->vipService->getLeaveMessage($user);
|
||||
|
||||
if ($template) {
|
||||
return [$template, $color];
|
||||
}
|
||||
|
||||
$text = '【'.$user->vipIcon().' '.$user->vipName().'】'.$this->randomLeaveMsg($user);
|
||||
|
||||
return [$text, $color];
|
||||
}
|
||||
|
||||
// 普通用户:橙色随机词
|
||||
return [$this->randomLeaveMsg($user), '#cc6600'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机趣味入场词
|
||||
*/
|
||||
public function randomWelcomeMsg(User $user): string
|
||||
{
|
||||
$gender = $user->sex == 2 ? '美女' : '帅哥';
|
||||
$uname = $user->username;
|
||||
|
||||
$templates = [
|
||||
$gender.'【'.$uname.'】开着刚买不久的车来到了,见到各位大虾,拱手曰:"众位大虾,小生有礼了"',
|
||||
$gender.'【'.$uname.'】骑着小毛驴哼着小调,悠闲地走了进来,对大家嘿嘿一笑',
|
||||
$gender.'【'.$uname.'】坐着豪华轿车缓缓驶入,推门而出,拍了拍身上的灰,霸气说道:"我来也!"',
|
||||
$gender.'【'.$uname.'】踩着七彩祥云从天而降,众人皆惊,抱拳道:"各位久等了!"',
|
||||
$gender.'【'.$uname.'】划着小船飘然而至,微微一笑,翩然上岸',
|
||||
$gender.'【'.$uname.'】骑着自行车铃铛叮当响,远远就喊:"我来啦!想我没?"',
|
||||
$gender.'【'.$uname.'】开着拖拉机突突突地开了进来,下车后拍了拍手说:"交通不便,来迟了!"',
|
||||
$gender.'【'.$uname.'】坐着火箭嗖的一声到了,吓了大家一跳,嘿嘿笑道:"别怕别怕,是我啊"',
|
||||
$gender.'【'.$uname.'】骑着白马翩翩而来,英姿飒爽,拱手道:"江湖路远,各位有礼了"',
|
||||
$gender.'【'.$uname.'】开着宝马一路飞驰到此,推开车门走了下来,向大家挥了挥手',
|
||||
$gender.'【'.$uname.'】踩着风火轮呼啸而至,在人群中潇洒亮相',
|
||||
$gender.'【'.$uname.'】乘坐滑翔伞从天空缓缓降落,对大家喊道:"hello,我从天上来!"',
|
||||
$gender.'【'.$uname.'】从地下钻了出来,拍了拍土,说:"哎呀,走错路了,不过总算到了"',
|
||||
$gender.'【'.$uname.'】蹦蹦跳跳地跑了进来,嘻嘻哈哈地跟大家打招呼',
|
||||
$gender.'【'.$uname.'】悄悄地溜了进来,生怕被人发现,东张西望了一番',
|
||||
$gender.'【'.$uname.'】迈着六亲不认的步伐走进来,气场两米八',
|
||||
];
|
||||
|
||||
return $templates[array_rand($templates)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机趣味离场词
|
||||
*/
|
||||
public function randomLeaveMsg(User $user): string
|
||||
{
|
||||
$gender = $user->sex == 2 ? '美女' : '帅哥';
|
||||
$uname = $user->username;
|
||||
|
||||
$templates = [
|
||||
$gender.'【'.$uname.'】潇洒地挥了挥手,骑着小毛驴哼着小调离去了',
|
||||
$gender.'【'.$uname.'】开着跑车扬长而去,留下一路烟尘',
|
||||
$gender.'【'.$uname.'】踩着七彩祥云飘然远去,消失在天际',
|
||||
$gender.'【'.$uname.'】悄无声息地溜走了,连个招呼都不打',
|
||||
$gender.'【'.$uname.'】跳上直升机螺旋桨呼呼作响,朝大家喊道:"我先走啦!"',
|
||||
$gender.'【'.$uname.'】拱手告别:"各位大虾,后会有期!"随后翩然离去',
|
||||
$gender.'【'.$uname.'】骑着自行车铃铛叮当响,远远就喊:"下次再聊!拜拜!"',
|
||||
$gender.'【'.$uname.'】坐着热气球缓缓升空,朝大家挥手告别',
|
||||
$gender.'【'.$uname.'】迈着六亲不认的步伐离开了,留下一众人目瞪口呆',
|
||||
$gender.'【'.$uname.'】化作一缕青烟消散在空气中……',
|
||||
];
|
||||
|
||||
return $templates[array_rand($templates)];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user