feat: 增加自定义头像上传、自动压缩与自动清理功能,统一全站头像路径读取逻辑
This commit is contained in:
@@ -304,11 +304,11 @@ class AutoSaveExp extends Command
|
||||
: now();
|
||||
|
||||
PositionDutyLog::create([
|
||||
'user_id' => $user->id,
|
||||
'user_id' => $user->id,
|
||||
'user_position_id' => $activeUP->id,
|
||||
'login_at' => $loginAt,
|
||||
'ip_address' => '0.0.0.0',
|
||||
'room_id' => $roomId,
|
||||
'login_at' => $loginAt,
|
||||
'ip_address' => '0.0.0.0',
|
||||
'room_id' => $roomId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ class ClearRoomOnlineCache extends Command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$prefix = config('database.redis.options.prefix', '');
|
||||
$cursor = '0';
|
||||
$prefix = config('database.redis.options.prefix', '');
|
||||
$cursor = '0';
|
||||
$cleaned = 0;
|
||||
|
||||
$this->info("Redis 前缀:\"{$prefix}\"");
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 前端收到后弹出 Toast 通知展示到账金额。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -24,9 +25,9 @@ class RedPacketClaimed implements ShouldBroadcastNow
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* @param User $claimer 领取用户
|
||||
* @param int $amount 领取金额
|
||||
* @param int $envelopeId 红包 ID
|
||||
* @param User $claimer 领取用户
|
||||
* @param int $amount 领取金额
|
||||
* @param int $envelopeId 红包 ID
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly User $claimer,
|
||||
@@ -51,8 +52,8 @@ class RedPacketClaimed implements ShouldBroadcastNow
|
||||
{
|
||||
return [
|
||||
'envelope_id' => $this->envelopeId,
|
||||
'amount' => $this->amount,
|
||||
'message' => "🧧 成功抢到 {$this->amount} 金币礼包!",
|
||||
'amount' => $this->amount,
|
||||
'message' => "🧧 成功抢到 {$this->amount} 金币礼包!",
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 前端接收后显示红包卡片弹窗,并在聊天窗口追加系统公告。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -23,20 +24,20 @@ class RedPacketSent implements ShouldBroadcastNow
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* @param int $roomId 房间 ID
|
||||
* @param int $envelopeId 红包 ID
|
||||
* @param string $senderUsername 发包人用户名
|
||||
* @param int $totalAmount 总金额(金币)
|
||||
* @param int $totalCount 总份数
|
||||
* @param int $expireSeconds 过期秒数(用于前端倒计时)
|
||||
* @param int $roomId 房间 ID
|
||||
* @param int $envelopeId 红包 ID
|
||||
* @param string $senderUsername 发包人用户名
|
||||
* @param int $totalAmount 总金额(金币)
|
||||
* @param int $totalCount 总份数
|
||||
* @param int $expireSeconds 过期秒数(用于前端倒计时)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $roomId,
|
||||
public readonly int $envelopeId,
|
||||
public readonly int $roomId,
|
||||
public readonly int $envelopeId,
|
||||
public readonly string $senderUsername,
|
||||
public readonly int $totalAmount,
|
||||
public readonly int $totalCount,
|
||||
public readonly int $expireSeconds,
|
||||
public readonly int $totalAmount,
|
||||
public readonly int $totalCount,
|
||||
public readonly int $expireSeconds,
|
||||
public readonly string $type = 'gold',
|
||||
) {}
|
||||
|
||||
@@ -58,12 +59,12 @@ class RedPacketSent implements ShouldBroadcastNow
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'envelope_id' => $this->envelopeId,
|
||||
'envelope_id' => $this->envelopeId,
|
||||
'sender_username' => $this->senderUsername,
|
||||
'total_amount' => $this->totalAmount,
|
||||
'total_count' => $this->totalCount,
|
||||
'expire_seconds' => $this->expireSeconds,
|
||||
'type' => $this->type,
|
||||
'total_amount' => $this->totalAmount,
|
||||
'total_count' => $this->totalCount,
|
||||
'expire_seconds' => $this->expireSeconds,
|
||||
'type' => $this->type,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 仅限 superlevel 以上管理员访问。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -46,14 +47,14 @@ class CurrencyStatsController extends Controller
|
||||
// 今日净流通量(正向增加 - 负向消耗),可判断通货膨胀
|
||||
$netFlow = [];
|
||||
foreach (['exp', 'gold', 'charm'] as $currency) {
|
||||
$totalIn = UserCurrencyLog::whereDate('created_at', $date)
|
||||
$totalIn = UserCurrencyLog::whereDate('created_at', $date)
|
||||
->where('currency', $currency)->where('amount', '>', 0)
|
||||
->sum('amount');
|
||||
$totalOut = UserCurrencyLog::whereDate('created_at', $date)
|
||||
->where('currency', $currency)->where('amount', '<', 0)
|
||||
->sum('amount');
|
||||
$netFlow[$currency] = [
|
||||
'in' => $totalIn,
|
||||
'in' => $totalIn,
|
||||
'out' => abs($totalOut),
|
||||
'net' => $totalIn + $totalOut, // 净增量
|
||||
];
|
||||
|
||||
@@ -25,7 +25,7 @@ class FishingEventController extends Controller
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$events = FishingEvent::orderBy('sort')->orderBy('id')->get();
|
||||
$events = FishingEvent::orderBy('sort')->orderBy('id')->get();
|
||||
$totalWeight = $events->where('is_active', true)->sum('weight');
|
||||
|
||||
return view('admin.fishing.index', compact('events', 'totalWeight'));
|
||||
@@ -37,13 +37,13 @@ class FishingEventController extends Controller
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'emoji' => 'required|string|max:10',
|
||||
'name' => 'required|string|max:100',
|
||||
'message' => 'required|string|max:255',
|
||||
'exp' => 'required|integer',
|
||||
'jjb' => 'required|integer',
|
||||
'weight' => 'required|integer|min:1|max:9999',
|
||||
'sort' => 'required|integer|min:0',
|
||||
'emoji' => 'required|string|max:10',
|
||||
'name' => 'required|string|max:100',
|
||||
'message' => 'required|string|max:255',
|
||||
'exp' => 'required|integer',
|
||||
'jjb' => 'required|integer',
|
||||
'weight' => 'required|integer|min:1|max:9999',
|
||||
'sort' => 'required|integer|min:0',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
@@ -61,13 +61,13 @@ class FishingEventController extends Controller
|
||||
public function update(Request $request, FishingEvent $fishing): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'emoji' => 'required|string|max:10',
|
||||
'name' => 'required|string|max:100',
|
||||
'message' => 'required|string|max:255',
|
||||
'exp' => 'required|integer',
|
||||
'jjb' => 'required|integer',
|
||||
'weight' => 'required|integer|min:1|max:9999',
|
||||
'sort' => 'required|integer|min:0',
|
||||
'emoji' => 'required|string|max:10',
|
||||
'name' => 'required|string|max:100',
|
||||
'message' => 'required|string|max:255',
|
||||
'exp' => 'required|integer',
|
||||
'jjb' => 'required|integer',
|
||||
'weight' => 'required|integer|min:1|max:9999',
|
||||
'sort' => 'required|integer|min:0',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
@@ -87,9 +87,9 @@ class FishingEventController extends Controller
|
||||
$fishing->update(['is_active' => ! $fishing->is_active]);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'ok' => true,
|
||||
'is_active' => $fishing->is_active,
|
||||
'message' => $fishing->is_active ? "「{$fishing->name}」已启用" : "「{$fishing->name}」已禁用",
|
||||
'message' => $fishing->is_active ? "「{$fishing->name}」已启用" : "「{$fishing->name}」已禁用",
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,8 +88,8 @@ class OpsController extends Controller
|
||||
abort(403, '无权限操作');
|
||||
}
|
||||
|
||||
$prefix = config('database.redis.options.prefix', '');
|
||||
$cursor = '0';
|
||||
$prefix = config('database.redis.options.prefix', '');
|
||||
$cursor = '0';
|
||||
$cleaned = 0;
|
||||
|
||||
do {
|
||||
|
||||
@@ -36,17 +36,17 @@ class RoomManagerController extends Controller
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'room_name' => 'required|string|max:100|unique:rooms,room_name',
|
||||
'room_des' => 'nullable|string|max:500',
|
||||
'room_owner' => 'nullable|string|max:50',
|
||||
'room_name' => 'required|string|max:100|unique:rooms,room_name',
|
||||
'room_des' => 'nullable|string|max:500',
|
||||
'room_owner' => 'nullable|string|max:50',
|
||||
'permit_level' => 'required|integer|min:0|max:15',
|
||||
'door_open' => 'required|boolean',
|
||||
'door_open' => 'required|boolean',
|
||||
], [
|
||||
'room_name.unique' => '房间名称已存在,请换一个名称。',
|
||||
]);
|
||||
|
||||
// 设置新建房间的默认值
|
||||
$data['room_keep'] = false; // 新建房间均为非系统房间,可删除
|
||||
$data['room_keep'] = false; // 新建房间均为非系统房间,可删除
|
||||
$data['build_time'] = now();
|
||||
|
||||
$room = Room::create($data);
|
||||
@@ -62,12 +62,12 @@ class RoomManagerController extends Controller
|
||||
public function update(Request $request, Room $room): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'room_name' => 'required|string|max:100',
|
||||
'room_des' => 'nullable|string|max:500',
|
||||
'room_name' => 'required|string|max:100',
|
||||
'room_des' => 'nullable|string|max:500',
|
||||
'announcement' => 'nullable|string|max:500',
|
||||
'room_owner' => 'nullable|string|max:50',
|
||||
'room_owner' => 'nullable|string|max:50',
|
||||
'permit_level' => 'required|integer|min:0|max:15',
|
||||
'door_open' => 'required|boolean',
|
||||
'door_open' => 'required|boolean',
|
||||
]);
|
||||
|
||||
$room->update($data);
|
||||
|
||||
@@ -65,7 +65,7 @@ class SmtpController extends Controller
|
||||
public function test(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'test_email' => 'required|email'
|
||||
'test_email' => 'required|email',
|
||||
]);
|
||||
|
||||
$testEmail = $request->input('test_email');
|
||||
@@ -78,7 +78,7 @@ class SmtpController extends Controller
|
||||
|
||||
return redirect()->route('admin.smtp.edit')->with('success', "测试邮件已成功发送至 {$testEmail},请注意查收。");
|
||||
} catch (\Throwable $e) {
|
||||
return redirect()->route('admin.smtp.edit')->with('error', "测试发出失败,原因:" . $e->getMessage());
|
||||
return redirect()->route('admin.smtp.edit')->with('error', '测试发出失败,原因:'.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class VerificationController extends Controller
|
||||
public function sendEmailCode(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email'
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
$email = $request->input('email');
|
||||
@@ -27,23 +27,24 @@ class VerificationController extends Controller
|
||||
if (SysParam::where('alias', 'smtp_enabled')->value('body') !== '1') {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => '抱歉,当前系统未开启外部邮件发信服务,请联系管理员。'
|
||||
'message' => '抱歉,当前系统未开启外部邮件发信服务,请联系管理员。',
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 2. 检查是否有频率限制(同一用户或同一邮箱,60秒只允许发1次)
|
||||
$throttleKey = 'email_throttle_' . $user->id;
|
||||
$throttleKey = 'email_throttle_'.$user->id;
|
||||
if (Cache::has($throttleKey)) {
|
||||
$ttl = Cache::ttl($throttleKey);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => "发送过于频繁,请等待 {$ttl} 秒后再试。"
|
||||
'message' => "发送过于频繁,请等待 {$ttl} 秒后再试。",
|
||||
], 429);
|
||||
}
|
||||
|
||||
// 3. 生成 6 位随机验证码并缓存,有效期 5 分钟
|
||||
$code = mt_rand(100000, 999999);
|
||||
$codeKey = 'email_verify_code_' . $user->id . '_' . $email;
|
||||
$codeKey = 'email_verify_code_'.$user->id.'_'.$email;
|
||||
Cache::put($codeKey, $code, now()->addMinutes(5));
|
||||
|
||||
// 设置频率锁,过期时间 60 秒
|
||||
@@ -57,14 +58,15 @@ class VerificationController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => '验证码已发送,请注意查收邮件。'
|
||||
'message' => '验证码已发送,请注意查收邮件。',
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 如果发信失败,主动接触频率限制锁方便用户下一次立重试
|
||||
Cache::forget($throttleKey);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => '邮件系统发送异常,请稍后再试: ' . $e->getMessage()
|
||||
'message' => '邮件系统发送异常,请稍后再试: '.$e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class ChatController extends Controller
|
||||
{
|
||||
@@ -683,9 +686,12 @@ class ChatController extends Controller
|
||||
return response()->json(['status' => 'error', 'message' => '头像文件不存在'], 422);
|
||||
}
|
||||
|
||||
// 更新用户头像
|
||||
$user->usersf = $headface;
|
||||
$user->save();
|
||||
// 更新前如为自定义头像,将其从磁盘删除,节约空间
|
||||
if ($user->usersf !== $headface) {
|
||||
$user->deleteCustomAvatar();
|
||||
$user->usersf = $headface;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
// 将新头像同步到 Redis 在线用户列表中(所有房间)
|
||||
// 通过更新 Redis 的用户信息,使得其他用户和自己刷新后都能看到新头像
|
||||
@@ -710,6 +716,71 @@ class ChatController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传自定义头像
|
||||
*/
|
||||
public function uploadAvatar(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|image|mimes:jpeg,png,jpg,gif,webp|max:2048',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
if (! $user) {
|
||||
return response()->json(['status' => 'error', 'message' => '未登录'], 401);
|
||||
}
|
||||
|
||||
$file = $request->file('file');
|
||||
|
||||
try {
|
||||
$manager = new ImageManager(new Driver);
|
||||
$image = $manager->read($file);
|
||||
|
||||
// 裁剪正方形并压缩为 112x112
|
||||
$image->cover(112, 112);
|
||||
|
||||
// 生成相对路径
|
||||
$filename = 'custom_'.$user->id.'_'.time().'.'.$file->extension();
|
||||
$path = 'avatars/'.$filename;
|
||||
|
||||
// 保存以高质量 JPG 或原格式
|
||||
Storage::disk('public')->put($path, (string) $image->encode());
|
||||
|
||||
$dbValue = 'storage/'.$path;
|
||||
|
||||
// 更新前如为自定义头像,将其从磁盘删除,节约空间
|
||||
if ($user->usersf !== $dbValue) {
|
||||
$user->deleteCustomAvatar();
|
||||
$user->usersf = $dbValue;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
// 同步 Redis 状态
|
||||
$superLevel = (int) Sysparam::getValue('superlevel', '100');
|
||||
$rooms = $this->chatState->getUserRooms($user->username);
|
||||
foreach ($rooms as $roomId) {
|
||||
$this->chatState->userJoin((int) $roomId, $user->username, [
|
||||
'level' => $user->user_level,
|
||||
'sex' => $user->sex,
|
||||
'headface' => $user->headface, // Use accessor
|
||||
'vip_icon' => $user->vipIcon(),
|
||||
'vip_name' => $user->vipName(),
|
||||
'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '',
|
||||
'is_admin' => $user->user_level >= $superLevel,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => '头像上传成功!',
|
||||
'headface' => $user->headface,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['status' => 'error', 'message' => '上传失败: '.$e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置房间公告/祝福语(滚动显示在聊天室顶部)
|
||||
* 需要房间主人或等级达到 level_announcement 配置值
|
||||
|
||||
@@ -94,8 +94,8 @@ class FishingController extends Controller
|
||||
$user->refresh();
|
||||
|
||||
// 4. 生成一次性 token,存入 Redis(TTL = 等待时间 + 收竿窗口 + 缓冲)
|
||||
$waitMin = (int) (GameConfig::param('fishing', 'fishing_wait_min') ?? Sysparam::getValue('fishing_wait_min', '8'));
|
||||
$waitMax = (int) (GameConfig::param('fishing', 'fishing_wait_max') ?? Sysparam::getValue('fishing_wait_max', '15'));
|
||||
$waitMin = (int) (GameConfig::param('fishing', 'fishing_wait_min') ?? Sysparam::getValue('fishing_wait_min', '8'));
|
||||
$waitMax = (int) (GameConfig::param('fishing', 'fishing_wait_max') ?? Sysparam::getValue('fishing_wait_max', '15'));
|
||||
$waitTime = rand($waitMin, $waitMax);
|
||||
$token = Str::random(32);
|
||||
$tokenKey = "fishing:token:{$user->id}";
|
||||
@@ -249,18 +249,18 @@ class FishingController extends Controller
|
||||
// 数据库无事件时的兜底
|
||||
if (! $event) {
|
||||
return [
|
||||
'emoji' => '🐟',
|
||||
'emoji' => '🐟',
|
||||
'message' => '钓到一条小鱼,获得金币10',
|
||||
'exp' => 0,
|
||||
'jjb' => 10,
|
||||
'exp' => 0,
|
||||
'jjb' => 10,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'emoji' => $event->emoji,
|
||||
'emoji' => $event->emoji,
|
||||
'message' => $event->message,
|
||||
'exp' => $event->exp,
|
||||
'jjb' => $event->jjb,
|
||||
'exp' => $event->exp,
|
||||
'jjb' => $event->jjb,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,13 +201,13 @@ class FriendController extends Controller
|
||||
$row = $myRows->get($u->username);
|
||||
|
||||
return [
|
||||
'username' => $u->username,
|
||||
'headface' => $u->headface,
|
||||
'username' => $u->username,
|
||||
'headface' => $u->headface,
|
||||
'user_level' => $u->user_level,
|
||||
'sex' => $u->sex,
|
||||
'mutual' => $addedMeNames->contains($u->username), // 是否互相添加
|
||||
'sub_time' => $row?->sub_time?->format('Y-m-d H:i') ?? '',
|
||||
'is_online' => $onlineUsernames->contains($u->username),
|
||||
'sex' => $u->sex,
|
||||
'mutual' => $addedMeNames->contains($u->username), // 是否互相添加
|
||||
'sub_time' => $row?->sub_time?->format('Y-m-d H:i') ?? '',
|
||||
'is_online' => $onlineUsernames->contains($u->username),
|
||||
];
|
||||
})
|
||||
->sortByDesc('is_online') // 在线好友排在前面
|
||||
@@ -221,19 +221,19 @@ class FriendController extends Controller
|
||||
$row = $addedMeRows->get($u->username);
|
||||
|
||||
return [
|
||||
'username' => $u->username,
|
||||
'headface' => $u->headface,
|
||||
'username' => $u->username,
|
||||
'headface' => $u->headface,
|
||||
'user_level' => $u->user_level,
|
||||
'sex' => $u->sex,
|
||||
'added_at' => $row?->sub_time?->format('Y-m-d H:i') ?? '',
|
||||
'is_online' => $onlineUsernames->contains($u->username),
|
||||
'sex' => $u->sex,
|
||||
'added_at' => $row?->sub_time?->format('Y-m-d H:i') ?? '',
|
||||
'is_online' => $onlineUsernames->contains($u->username),
|
||||
];
|
||||
})
|
||||
->sortByDesc('is_online')
|
||||
->values();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'status' => 'success',
|
||||
'friends' => $friends,
|
||||
'pending' => $pending,
|
||||
]);
|
||||
|
||||
@@ -86,12 +86,12 @@ class LeaderboardController extends Controller
|
||||
|
||||
// ── 今日榜(5分钟缓存,数据来自 user_currency_logs 流水表)──
|
||||
$todayTtl = 60 * 5;
|
||||
$today = today()->toDateString();
|
||||
$today = today()->toDateString();
|
||||
|
||||
$todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl,
|
||||
$todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl,
|
||||
fn () => $this->currencyService->todayLeaderboard('exp', $topN, $today)
|
||||
);
|
||||
$todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl,
|
||||
$todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl,
|
||||
fn () => $this->currencyService->todayLeaderboard('gold', $topN, $today)
|
||||
);
|
||||
$todayCharm = Cache::remember("leaderboard:today_charm:{$today}", $todayTtl,
|
||||
@@ -109,13 +109,13 @@ class LeaderboardController extends Controller
|
||||
public function todayIndex(): View
|
||||
{
|
||||
$todayTtl = 60 * 5;
|
||||
$today = today()->toDateString();
|
||||
$topN = (int) \App\Models\Sysparam::getValue('leaderboard_limit', '20');
|
||||
$today = today()->toDateString();
|
||||
$topN = (int) \App\Models\Sysparam::getValue('leaderboard_limit', '20');
|
||||
|
||||
$todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl,
|
||||
$todayExp = Cache::remember("leaderboard:today_exp:{$today}", $todayTtl,
|
||||
fn () => $this->currencyService->todayLeaderboard('exp', $topN, $today)
|
||||
);
|
||||
$todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl,
|
||||
$todayGold = Cache::remember("leaderboard:today_gold:{$today}", $todayTtl,
|
||||
fn () => $this->currencyService->todayLeaderboard('gold', $topN, $today)
|
||||
);
|
||||
$todayCharm = Cache::remember("leaderboard:today_charm:{$today}", $todayTtl,
|
||||
@@ -130,10 +130,10 @@ class LeaderboardController extends Controller
|
||||
*/
|
||||
public function myLogs(): View
|
||||
{
|
||||
$user = auth()->user();
|
||||
$user = auth()->user();
|
||||
$currency = request('currency');
|
||||
$days = (int) request('days', 7);
|
||||
$logs = $this->currencyService->userLogs($user->id, $currency ?: null, $days);
|
||||
$days = (int) request('days', 7);
|
||||
$logs = $this->currencyService->userLogs($user->id, $currency ?: null, $days);
|
||||
|
||||
return view('leaderboard.my-logs', compact('logs', 'user', 'currency', 'days'));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* 5. 公屏广播结果(中奖/踩雷)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -58,12 +59,12 @@ class MysteryBoxController extends Controller
|
||||
$secondsLeft = $box->expires_at ? max(0, now()->diffInSeconds($box->expires_at, false)) : null;
|
||||
|
||||
return response()->json([
|
||||
'active' => true,
|
||||
'box_id' => $box->id,
|
||||
'box_type' => $box->box_type,
|
||||
'type_name' => $box->typeName(),
|
||||
'type_emoji' => $box->typeEmoji(),
|
||||
'passcode' => $box->passcode,
|
||||
'active' => true,
|
||||
'box_id' => $box->id,
|
||||
'box_type' => $box->box_type,
|
||||
'type_name' => $box->typeName(),
|
||||
'type_emoji' => $box->typeEmoji(),
|
||||
'passcode' => $box->passcode,
|
||||
'seconds_left' => $secondsLeft,
|
||||
]);
|
||||
}
|
||||
@@ -105,15 +106,15 @@ class MysteryBoxController extends Controller
|
||||
$source = $reward >= 0 ? CurrencySource::MYSTERY_BOX : CurrencySource::MYSTERY_BOX_TRAP;
|
||||
$remark = $reward >= 0
|
||||
? "神秘箱子【{$box->typeName()}】奖励"
|
||||
: "神秘箱子【黑化箱】陷阱扣除";
|
||||
: '神秘箱子【黑化箱】陷阱扣除';
|
||||
|
||||
$this->currency->change($user, 'gold', $reward, $source, $remark, $box->room_id);
|
||||
|
||||
// ③ 写领取记录 + 更新箱子状态
|
||||
MysteryBoxClaim::create([
|
||||
'mystery_box_id' => $box->id,
|
||||
'user_id' => $user->id,
|
||||
'reward_amount' => $reward,
|
||||
'user_id' => $user->id,
|
||||
'reward_amount' => $reward,
|
||||
]);
|
||||
|
||||
$box->update(['status' => 'claimed']);
|
||||
@@ -123,12 +124,12 @@ class MysteryBoxController extends Controller
|
||||
$this->broadcastResult($box, $user->username, $reward);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'reward' => $reward,
|
||||
'ok' => true,
|
||||
'reward' => $reward,
|
||||
'balance' => $user->jjb ?? 0,
|
||||
'message' => $reward >= 0
|
||||
? "🎉 恭喜!开箱获得 +{$reward} 金币!"
|
||||
: "☠️ 中了黑化陷阱!扣除 " . abs($reward) . ' 金币!',
|
||||
: '☠️ 中了黑化陷阱!扣除 '.abs($reward).' 金币!',
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -136,9 +137,9 @@ class MysteryBoxController extends Controller
|
||||
/**
|
||||
* 公屏广播开箱结果。
|
||||
*
|
||||
* @param MysteryBox $box 箱子实例
|
||||
* @param string $username 领取者用户名
|
||||
* @param int $reward 奖励金额(正/负)
|
||||
* @param MysteryBox $box 箱子实例
|
||||
* @param string $username 领取者用户名
|
||||
* @param int $reward 奖励金额(正/负)
|
||||
*/
|
||||
private function broadcastResult(MysteryBox $box, string $username, int $reward): void
|
||||
{
|
||||
@@ -147,24 +148,24 @@ class MysteryBoxController extends Controller
|
||||
|
||||
if ($reward >= 0) {
|
||||
$content = "{$emoji}【开箱播报】恭喜 【{$username}】 抢到了神秘{$typeName}!"
|
||||
. "获得 💰" . number_format($reward) . " 金币!";
|
||||
.'获得 💰'.number_format($reward).' 金币!';
|
||||
$color = $box->box_type === 'rare' ? '#c4b5fd' : '#34d399';
|
||||
} else {
|
||||
$content = "☠️【黑化陷阱】haha!【{$username}】 中了神秘黑化箱的陷阱!"
|
||||
. "被扣除 💰" . number_format(abs($reward)) . " 金币!点背~";
|
||||
.'被扣除 💰'.number_format(abs($reward)).' 金币!点背~';
|
||||
$color = '#f87171';
|
||||
}
|
||||
|
||||
$msg = [
|
||||
'id' => $this->chatState->nextMessageId(1),
|
||||
'room_id' => 1,
|
||||
'id' => $this->chatState->nextMessageId(1),
|
||||
'room_id' => 1,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '大家',
|
||||
'content' => $content,
|
||||
'to_user' => '大家',
|
||||
'content' => $content,
|
||||
'is_secret' => false,
|
||||
'font_color' => $color,
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
$this->chatState->pushMessage(1, $msg);
|
||||
|
||||
@@ -170,6 +170,10 @@ class UserController extends Controller
|
||||
\Illuminate\Support\Facades\Cache::forget($codeKey);
|
||||
}
|
||||
|
||||
if (isset($data['headface']) && $data['headface'] !== $user->headface) {
|
||||
$user->deleteCustomAvatar();
|
||||
}
|
||||
|
||||
$user->update($data);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => '资料更新成功。']);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* 3. 设定定时关闭任务(windows_seconds 秒后过期)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -32,8 +33,8 @@ class DropMysteryBoxJob implements ShouldQueue
|
||||
public int $tries = 1;
|
||||
|
||||
/**
|
||||
* @param string $boxType 箱子类型:normal | rare | trap
|
||||
* @param int|null $roomId 投放目标房间,null 时默认 1
|
||||
* @param string $boxType 箱子类型:normal | rare | trap
|
||||
* @param int|null $roomId 投放目标房间,null 时默认 1
|
||||
* @param string|null $passcode 手动指定暗号(null=自动生成)
|
||||
* @param int|null $droppedBy 投放者用户ID(null=系统自动)
|
||||
*/
|
||||
@@ -79,11 +80,11 @@ class DropMysteryBoxJob implements ShouldQueue
|
||||
|
||||
// 创建箱子记录
|
||||
$box = MysteryBox::create([
|
||||
'box_type' => $this->boxType,
|
||||
'passcode' => $passcode,
|
||||
'box_type' => $this->boxType,
|
||||
'passcode' => $passcode,
|
||||
'reward_min' => $rewardMin,
|
||||
'reward_max' => $rewardMax,
|
||||
'status' => 'open',
|
||||
'status' => 'open',
|
||||
'expires_at' => now()->addSeconds($claimWindow),
|
||||
'dropped_by' => $this->droppedBy,
|
||||
]);
|
||||
@@ -94,22 +95,22 @@ class DropMysteryBoxJob implements ShouldQueue
|
||||
$source = $this->droppedBy ? '管理员' : '系统';
|
||||
|
||||
$content = "{$emoji}【{$typeName}】{$source}投放了一个神秘箱子!"
|
||||
. "发送暗号「{$passcode}」即可开箱!限时 {$claimWindow} 秒,先到先得!";
|
||||
."发送暗号「{$passcode}」即可开箱!限时 {$claimWindow} 秒,先到先得!";
|
||||
|
||||
$msg = [
|
||||
'id' => $chatState->nextMessageId($targetRoom),
|
||||
'room_id' => $targetRoom,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '大家',
|
||||
'content' => $content,
|
||||
'is_secret' => false,
|
||||
'id' => $chatState->nextMessageId($targetRoom),
|
||||
'room_id' => $targetRoom,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '大家',
|
||||
'content' => $content,
|
||||
'is_secret' => false,
|
||||
'font_color' => match ($this->boxType) {
|
||||
'rare' => '#c4b5fd',
|
||||
'trap' => '#f87171',
|
||||
'rare' => '#c4b5fd',
|
||||
'trap' => '#f87171',
|
||||
default => '#34d399',
|
||||
},
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
$chatState->pushMessage($targetRoom, $msg);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 在箱子到期后将其状态更新为 expired(若尚未被领取),并向公屏广播过期通知。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -48,15 +49,15 @@ class ExpireMysteryBoxJob implements ShouldQueue
|
||||
|
||||
// 公屏广播过期通知
|
||||
$msg = [
|
||||
'id' => $chatState->nextMessageId(1),
|
||||
'room_id' => 1,
|
||||
'id' => $chatState->nextMessageId(1),
|
||||
'room_id' => 1,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '大家',
|
||||
'content' => "⏰ 神秘箱子(暗号:{$box->passcode})已超时,箱子消失了!下次要快哦~",
|
||||
'to_user' => '大家',
|
||||
'content' => "⏰ 神秘箱子(暗号:{$box->passcode})已超时,箱子消失了!下次要快哦~",
|
||||
'is_secret' => false,
|
||||
'font_color' => '#9ca3af',
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
$chatState->pushMessage(1, $msg);
|
||||
|
||||
@@ -42,11 +42,11 @@ class FishingEvent extends Model
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'exp' => 'integer',
|
||||
'jjb' => 'integer',
|
||||
'weight' => 'integer',
|
||||
'exp' => 'integer',
|
||||
'jjb' => 'integer',
|
||||
'weight' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'sort' => 'integer',
|
||||
'sort' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ class FishingEvent extends Model
|
||||
|
||||
// 计算总权重后加权随机
|
||||
$totalWeight = $events->sum('weight');
|
||||
$roll = random_int(1, max($totalWeight, 1));
|
||||
$cumulative = 0;
|
||||
$roll = random_int(1, max($totalWeight, 1));
|
||||
$cumulative = 0;
|
||||
|
||||
foreach ($events as $event) {
|
||||
$cumulative += $event->weight;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 对应表:mystery_boxes
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -81,9 +82,9 @@ class MysteryBox extends Model
|
||||
{
|
||||
return match ($this->box_type) {
|
||||
'normal' => '📦',
|
||||
'rare' => '💎',
|
||||
'trap' => '☠️',
|
||||
default => '📦',
|
||||
'rare' => '💎',
|
||||
'trap' => '☠️',
|
||||
default => '📦',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,9 +95,9 @@ class MysteryBox extends Model
|
||||
{
|
||||
return match ($this->box_type) {
|
||||
'normal' => '普通箱',
|
||||
'rare' => '稀有箱',
|
||||
'trap' => '黑化箱',
|
||||
default => '神秘箱',
|
||||
'rare' => '稀有箱',
|
||||
'trap' => '黑化箱',
|
||||
default => '神秘箱',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 对应表:mystery_box_claims
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* envelope_id + user_id 联合唯一约束保证幂等性。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 先到先得,领完或超时后自动关闭。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* 对应原版 ASP 聊天室的 sysparam 配置表
|
||||
* 管理员可在后台修改等级经验阈值等系统参数
|
||||
*
|
||||
* @package App\Models
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -27,9 +27,8 @@ class Sysparam extends Model
|
||||
* 获取指定参数的值
|
||||
* 带缓存,避免频繁查库
|
||||
*
|
||||
* @param string $alias 参数别名
|
||||
* @param string $default 默认值
|
||||
* @return string
|
||||
* @param string $alias 参数别名
|
||||
* @param string $default 默认值
|
||||
*/
|
||||
public static function getValue(string $alias, string $default = ''): string
|
||||
{
|
||||
@@ -56,7 +55,7 @@ class Sysparam extends Model
|
||||
/**
|
||||
* 根据经验值计算应该达到的等级
|
||||
*
|
||||
* @param int $expNum 当前经验值
|
||||
* @param int $expNum 当前经验值
|
||||
* @return int 对应的等级
|
||||
*/
|
||||
public static function calculateLevel(int $expNum): int
|
||||
@@ -81,7 +80,7 @@ class Sysparam extends Model
|
||||
/**
|
||||
* 清除指定参数的缓存
|
||||
*
|
||||
* @param string $alias 参数别名
|
||||
* @param string $alias 参数别名
|
||||
*/
|
||||
public static function clearCache(string $alias): void
|
||||
{
|
||||
|
||||
+48
-5
@@ -90,18 +90,61 @@ class User extends Authenticatable
|
||||
* 头像文件名访问器
|
||||
*
|
||||
* 原 ASP 系统的头像文件名存储在 usersf 字段中(如 "75.gif"),
|
||||
* 但项目中各处通过 $user->headface 来引用头像。
|
||||
* 此 accessor 将 headface 属性映射到 usersf 字段,保持代码一致性。
|
||||
* 同时也支持用户自定义上传的头像,保存在 Laravel Storage 的 public 磁盘下。
|
||||
* 此 accessor 将 headface 属性映射到 usersf 字段,如果包含 storage/ 则当作独立路径,自动转换旧版后缀小写。
|
||||
*/
|
||||
protected function headface(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
// 自动将后缀转小写,兼容数据库中的 .GIF 大写存量
|
||||
get: fn () => strtolower($this->usersf ?: '1.gif'),
|
||||
set: fn (string $value) => ['usersf' => strtolower($value)],
|
||||
get: function () {
|
||||
$val = $this->usersf ?: '1.gif';
|
||||
if (str_starts_with($val, 'storage/')) {
|
||||
return $val;
|
||||
}
|
||||
|
||||
// 仅对非 storage 下的旧头像做小写处理,兼容旧库数据
|
||||
return strtolower($val);
|
||||
},
|
||||
set: function ($value) {
|
||||
if (str_starts_with($value, 'storage/')) {
|
||||
return tap($value, fn () => $this->attributes['usersf'] = $value);
|
||||
}
|
||||
|
||||
return tap(strtolower($value), fn () => $this->attributes['usersf'] = strtolower($value));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带前缀的完整头像 URL
|
||||
* 避免前端多处硬编码 '/images/headface/'
|
||||
*/
|
||||
protected function headfaceUrl(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$hf = $this->headface;
|
||||
if (str_starts_with((string) $hf, 'storage/')) {
|
||||
return '/'.$hf;
|
||||
}
|
||||
|
||||
return '/images/headface/'.$hf;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果当前头像是自定义上传的图片,则从本地存储中删除此文件
|
||||
*/
|
||||
public function deleteCustomAvatar(): void
|
||||
{
|
||||
$hf = (string) $this->usersf;
|
||||
if (str_starts_with($hf, 'storage/')) {
|
||||
$path = substr($hf, 8); // 去除 'storage/' 前缀
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联:用户所属的 VIP 会员等级
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 只读写,不允许 update(流水记录不可更改)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -40,10 +41,10 @@ class UserCurrencyLog extends Model
|
||||
* 字段类型转换
|
||||
*/
|
||||
protected $casts = [
|
||||
'amount' => 'integer',
|
||||
'balance_after'=> 'integer',
|
||||
'room_id' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'amount' => 'integer',
|
||||
'balance_after' => 'integer',
|
||||
'room_id' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
// ─── 关联 ─────────────────────────────────────────────────
|
||||
|
||||
@@ -131,7 +131,7 @@ class ChatStateService
|
||||
{
|
||||
$usernames = [];
|
||||
foreach ($this->getAllActiveRoomIds() as $roomId) {
|
||||
$key = "room:{$roomId}:users";
|
||||
$key = "room:{$roomId}:users";
|
||||
$users = Redis::hkeys($key); // 只取 key(用户名),不取 value
|
||||
foreach ($users as $username) {
|
||||
$usernames[] = $username;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 本服务负责:原子性更新用户属性、写入流水记录、提供统计与排行数据。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -25,8 +26,8 @@ class UserCurrencyService
|
||||
* 以后新增货币类型,在此加一行即可。
|
||||
*/
|
||||
private const FIELD_MAP = [
|
||||
'exp' => 'exp_num',
|
||||
'gold' => 'jjb',
|
||||
'exp' => 'exp_num',
|
||||
'gold' => 'jjb',
|
||||
'charm' => 'meili',
|
||||
];
|
||||
|
||||
@@ -34,20 +35,20 @@ class UserCurrencyService
|
||||
* 统一变更用户货币属性并写入流水记录。
|
||||
* 使用数据库事务保证原子性:用户属性更新 + 流水写入同时成功或同时回滚。
|
||||
*
|
||||
* @param User $user 目标用户
|
||||
* @param string $currency 货币类型('exp' / 'gold' / 'charm')
|
||||
* @param int $amount 变更量,正数增加,负数扣除
|
||||
* @param CurrencySource $source 来源活动枚举
|
||||
* @param string $remark 备注说明
|
||||
* @param int|null $roomId 所在房间 ID(可选)
|
||||
* @param User $user 目标用户
|
||||
* @param string $currency 货币类型('exp' / 'gold' / 'charm')
|
||||
* @param int $amount 变更量,正数增加,负数扣除
|
||||
* @param CurrencySource $source 来源活动枚举
|
||||
* @param string $remark 备注说明
|
||||
* @param int|null $roomId 所在房间 ID(可选)
|
||||
*/
|
||||
public function change(
|
||||
User $user,
|
||||
string $currency,
|
||||
int $amount,
|
||||
User $user,
|
||||
string $currency,
|
||||
int $amount,
|
||||
CurrencySource $source,
|
||||
string $remark = '',
|
||||
?int $roomId = null,
|
||||
string $remark = '',
|
||||
?int $roomId = null,
|
||||
): void {
|
||||
if ($amount === 0) {
|
||||
return; // 变更量为 0 不写记录
|
||||
@@ -72,14 +73,14 @@ class UserCurrencyService
|
||||
|
||||
// 写入流水记录(快照当前用户名,排行 JOIN 时再取最新名)
|
||||
UserCurrencyLog::create([
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'currency' => $currency,
|
||||
'amount' => $amount,
|
||||
'balance_after'=> $balanceAfter,
|
||||
'source' => $source->value,
|
||||
'remark' => $remark,
|
||||
'room_id' => $roomId,
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'currency' => $currency,
|
||||
'amount' => $amount,
|
||||
'balance_after' => $balanceAfter,
|
||||
'source' => $source->value,
|
||||
'remark' => $remark,
|
||||
'room_id' => $roomId,
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -88,14 +89,12 @@ class UserCurrencyService
|
||||
* 批量变更多个用户的货币属性(适用于自动存点:一次操作多人)。
|
||||
* 每位用户仍独立走事务,单人失败不影响其他人。
|
||||
*
|
||||
* @param array $items [['user' => User, 'changes' => ['exp'=>1,'gold'=>2]], ...]
|
||||
* @param CurrencySource $source
|
||||
* @param int|null $roomId
|
||||
* @param array $items [['user' => User, 'changes' => ['exp'=>1,'gold'=>2]], ...]
|
||||
*/
|
||||
public function batchChange(array $items, CurrencySource $source, ?int $roomId = null): void
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
$user = $item['user'];
|
||||
$user = $item['user'];
|
||||
$changes = $item['changes'] ?? [];
|
||||
foreach ($changes as $currency => $amount) {
|
||||
$this->change($user, $currency, (int) $amount, $source, '', $roomId);
|
||||
@@ -127,8 +126,8 @@ class UserCurrencyService
|
||||
* 只统计正向变更(amount > 0),不因消耗而扣分。
|
||||
*
|
||||
* @param string $currency 'exp' | 'gold' | 'charm'
|
||||
* @param int $limit 返回条数
|
||||
* @param string|null $date 日期,默认今日
|
||||
* @param int $limit 返回条数
|
||||
* @param string|null $date 日期,默认今日
|
||||
*/
|
||||
public function todayLeaderboard(string $currency, int $limit = 20, ?string $date = null): Collection
|
||||
{
|
||||
@@ -149,12 +148,12 @@ class UserCurrencyService
|
||||
->find($row->user_id);
|
||||
|
||||
return (object) [
|
||||
'user_id' => $row->user_id,
|
||||
'user_id' => $row->user_id,
|
||||
'username' => $user?->username ?? '未知用户',
|
||||
'level' => $user?->user_level ?? 0,
|
||||
'sex' => $user?->sex ?? 1,
|
||||
'level' => $user?->user_level ?? 0,
|
||||
'sex' => $user?->sex ?? 1,
|
||||
'headface' => $user?->headface ?? '1.gif',
|
||||
'total' => $row->total,
|
||||
'total' => $row->total,
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -162,9 +161,9 @@ class UserCurrencyService
|
||||
/**
|
||||
* 用户个人流水明细(用户查询自己的日志)。
|
||||
*
|
||||
* @param int $userId 用户 ID
|
||||
* @param int $userId 用户 ID
|
||||
* @param string|null $currency 为 null 时返回所有货币类型
|
||||
* @param int $days 查询最近多少天
|
||||
* @param int $days 查询最近多少天
|
||||
*/
|
||||
public function userLogs(int $userId, ?string $currency = null, int $days = 7): Collection
|
||||
{
|
||||
@@ -183,8 +182,8 @@ class UserCurrencyService
|
||||
public static function currencyLabel(string $currency): string
|
||||
{
|
||||
return match ($currency) {
|
||||
'exp' => '经验',
|
||||
'gold' => '金币',
|
||||
'exp' => '经验',
|
||||
'gold' => '金币',
|
||||
'charm' => '魅力',
|
||||
default => $currency,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user