Files
chatroom/app/Http/Controllers/SlotMachineController.php
lkddi 040dbdef3c 优化:全站金币图标由 🪙(银灰色)统一替换为 💰(金黄色)
🪙 在多数平台/字体上渲染为银灰色,与「金币」语义不符;
💰 各平台均渲染为金黄色,更直观传达金币概念。

涉及文件(43处):
- app/Jobs:百家乐、赛马结算广播
- app/Http/Controllers:管理员命令、红包、老虎机、神秘箱子
- app/Listeners
- resources/views:聊天室各游戏面板、商店、toolbar、后台页面等
2026-03-04 15:00:02 +08:00

289 lines
9.9 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能:老虎机游戏前台控制器
*
* 提供老虎机转动 API
* - 检查游戏开关、每日限制
* - 扣除金币、生成三列图案
* - 判断结果、赔付金币、写流水
* - 三个7时全服公屏广播
* - 返回结果供前端播放动画
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers;
use App\Enums\CurrencySource;
use App\Events\MessageSent;
use App\Jobs\SaveMessageJob;
use App\Models\GameConfig;
use App\Models\SlotMachineLog;
use App\Services\ChatStateService;
use App\Services\UserCurrencyService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SlotMachineController extends Controller
{
public function __construct(
private readonly UserCurrencyService $currency,
private readonly ChatStateService $chatState,
) {}
/**
* 获取老虎机配置信息(图案表、赔率、今日剩余次数)。
*/
public function info(Request $request): JsonResponse
{
if (! GameConfig::isEnabled('slot_machine')) {
return response()->json(['enabled' => false]);
}
$config = GameConfig::forGame('slot_machine')?->params ?? [];
$user = $request->user();
$dailyLimit = (int) ($config['daily_limit'] ?? 0);
$usedToday = 0;
if ($dailyLimit > 0) {
$usedToday = SlotMachineLog::query()
->where('user_id', $user->id)
->whereDate('created_at', today())
->count();
}
return response()->json([
'enabled' => true,
'cost_per_spin' => (int) ($config['cost_per_spin'] ?? 100),
'daily_limit' => $dailyLimit,
'used_today' => $usedToday,
'remaining' => $dailyLimit > 0 ? max(0, $dailyLimit - $usedToday) : null,
'symbols' => collect(SlotMachineLog::symbols())->map(fn ($s) => $s['emoji']),
]);
}
/**
* 执行一次转动。
*
* 流程:检查可玩 → 扣费 → 摇号 → 赔付 → 写日志 → 全服广播三7→ 返回结果
*/
public function spin(Request $request): JsonResponse
{
if (! GameConfig::isEnabled('slot_machine')) {
return response()->json(['ok' => false, 'message' => '老虎机未开放。']);
}
$config = GameConfig::forGame('slot_machine')?->params ?? [];
$cost = (int) ($config['cost_per_spin'] ?? 100);
$dailyLimit = (int) ($config['daily_limit'] ?? 0);
$user = $request->user();
// 金币余额检查
if (($user->jjb ?? 0) < $cost) {
return response()->json(['ok' => false, 'message' => "金币不足,每次转动需 {$cost} 金币。"]);
}
// 每日次数限制检查
if ($dailyLimit > 0) {
$usedToday = SlotMachineLog::query()
->where('user_id', $user->id)
->whereDate('created_at', today())
->count();
if ($usedToday >= $dailyLimit) {
return response()->json(['ok' => false, 'message' => "今日已转动 {$dailyLimit} 次,明日再来!"]);
}
}
return DB::transaction(function () use ($user, $cost, $config): JsonResponse {
// ① 扣费
$this->currency->change(
$user,
'gold',
-$cost,
CurrencySource::SLOT_SPIN,
'老虎机转动消耗',
);
// ② 摇号
$r1 = SlotMachineLog::randomSymbol();
$r2 = SlotMachineLog::randomSymbol();
$r3 = SlotMachineLog::randomSymbol();
$resultType = SlotMachineLog::judgeResult($r1, $r2, $r3);
$symbols = SlotMachineLog::symbols();
// ③ 计算赔付金额
$payout = $this->calcPayout($resultType, $cost, $config);
// ④ 赔付金币
if ($payout > 0) {
$this->currency->change(
$user,
'gold',
$payout,
CurrencySource::SLOT_WIN,
"老虎机 {$resultType} 中奖",
);
} elseif ($resultType === 'curse' && ($config['curse_enabled'] ?? true)) {
// 诅咒:再扣一倍本金
$cursePenalty = $cost;
if (($user->jjb ?? 0) >= $cursePenalty) {
$this->currency->change(
$user,
'gold',
-$cursePenalty,
CurrencySource::SLOT_CURSE,
'老虎机三骷髅诅咒额外扣除',
);
$payout = -$cursePenalty; // 净损失 = 本金+惩罚
}
}
// ⑤ 写游戏日志
$resultLabel = SlotMachineLog::resultLabel($resultType);
SlotMachineLog::create([
'user_id' => $user->id,
'reel1' => $r1,
'reel2' => $r2,
'reel3' => $r3,
'result_type' => $resultType,
'cost' => $cost,
'payout' => $payout,
]);
// ⑥ 广播通知
$e1 = $symbols[$r1]['emoji'];
$e2 = $symbols[$r2]['emoji'];
$e3 = $symbols[$r3]['emoji'];
if ($resultType === 'jackpot') {
// 三个7全服公屏广播
$this->broadcastJackpot($user->username, $payout, $cost);
} elseif (in_array($resultType, ['triple_gem', 'triple', 'pair'], true)) {
// 普通中奖:仅向本人发送聊天室系统通知
$net = $payout - $cost;
$content = "🎰 {$resultLabel}{$e1}{$e2}{$e3} 赢得 +💰".number_format($net).' 金币';
$this->broadcastPersonal($user->username, $content);
} elseif ($resultType === 'curse') {
// 诅咒:通知本人
$content = "☠️ 三骷髅诅咒!{$e1}{$e2}{$e3} 额外扣除 💰".number_format($cost).' 金币!';
$this->broadcastPersonal($user->username, $content);
}
$user->refresh();
return response()->json([
'ok' => true,
'reels' => [$r1, $r2, $r3],
'emojis' => [
$symbols[$r1]['emoji'],
$symbols[$r2]['emoji'],
$symbols[$r3]['emoji'],
],
'result_type' => $resultType,
'result_label' => SlotMachineLog::resultLabel($resultType),
'payout' => $payout, // 净变化(正=赢,负=额外亏)
'net_change' => $payout - $cost, // 相对本金的盈亏debug 用)
'balance' => $user->jjb ?? 0,
]);
});
}
/**
* 查询最近10条个人记录。
*/
public function history(Request $request): JsonResponse
{
$logs = SlotMachineLog::query()
->where('user_id', $request->user()->id)
->orderByDesc('id')
->limit(10)
->get(['id', 'reel1', 'reel2', 'reel3', 'result_type', 'cost', 'payout', 'created_at']);
$symbols = SlotMachineLog::symbols();
return response()->json([
'history' => $logs->map(fn ($l) => [
'emojis' => [$symbols[$l->reel1]['emoji'], $symbols[$l->reel2]['emoji'], $symbols[$l->reel3]['emoji']],
'result_label' => SlotMachineLog::resultLabel($l->result_type),
'payout' => $l->payout,
'created_at' => $l->created_at->format('H:i'),
]),
]);
}
/**
* 计算赔付金额(赢时返还 = 本金 × 赔率)。
*
* @return int 正数=赢得金额含本金返还0=不赔付
*/
private function calcPayout(string $resultType, int $cost, array $config): int
{
$multiplier = match ($resultType) {
'jackpot' => (int) ($config['jackpot_payout'] ?? 100),
'triple_gem' => (int) ($config['triple_payout'] ?? 50),
'triple' => (int) ($config['same_payout'] ?? 10),
'pair' => (int) ($config['pair_payout'] ?? 2),
default => 0,
};
return $multiplier > 0 ? $cost * $multiplier : 0;
}
/**
* 三个7全服公屏广播。
*/
private function broadcastJackpot(string $username, int $payout, int $cost): void
{
$net = $payout - $cost;
$content = "🎰🎉【老虎机大奖】恭喜 【{$username}】 转出三个7"
.'狂揽 💰'.number_format($net).' 金币!全服见证奇迹!';
$msg = [
'id' => $this->chatState->nextMessageId(1),
'room_id' => 1,
'from_user' => '系统传音',
'to_user' => '大家',
'content' => $content,
'is_secret' => false,
'font_color' => '#f59e0b',
'action' => '大声宣告',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage(1, $msg);
broadcast(new MessageSent(1, $msg));
SaveMessageJob::dispatch($msg);
}
/**
* 向特定用户发送聊天室私人系统通知(仅该用户可见)。
*
* @param string $toUsername 接收用户名
* @param string $content 消息内容
*/
private function broadcastPersonal(string $toUsername, string $content): void
{
$msg = [
'id' => $this->chatState->nextMessageId(1),
'room_id' => 1,
'from_user' => '系统传音',
'to_user' => $toUsername,
'content' => $content,
'is_secret' => true,
'font_color' => '#f59e0b',
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
broadcast(new MessageSent(1, $msg));
SaveMessageJob::dispatch($msg);
}
}