2026-03-01 21:00:21 +08:00
|
|
|
|
<?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; // 净损失 = 本金+惩罚
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ⑤ 写游戏日志
|
2026-03-01 21:10:41 +08:00
|
|
|
|
$resultLabel = SlotMachineLog::resultLabel($resultType);
|
2026-03-01 21:00:21 +08:00
|
|
|
|
SlotMachineLog::create([
|
|
|
|
|
|
'user_id' => $user->id,
|
|
|
|
|
|
'reel1' => $r1,
|
|
|
|
|
|
'reel2' => $r2,
|
|
|
|
|
|
'reel3' => $r3,
|
|
|
|
|
|
'result_type' => $resultType,
|
|
|
|
|
|
'cost' => $cost,
|
|
|
|
|
|
'payout' => $payout,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-03-01 21:10:41 +08:00
|
|
|
|
// ⑥ 广播通知
|
|
|
|
|
|
$e1 = $symbols[$r1]['emoji'];
|
|
|
|
|
|
$e2 = $symbols[$r2]['emoji'];
|
|
|
|
|
|
$e3 = $symbols[$r3]['emoji'];
|
|
|
|
|
|
|
2026-03-01 21:00:21 +08:00
|
|
|
|
if ($resultType === 'jackpot') {
|
2026-03-01 21:10:41 +08:00
|
|
|
|
// 三个7:全服公屏广播
|
2026-03-01 21:00:21 +08:00
|
|
|
|
$this->broadcastJackpot($user->username, $payout, $cost);
|
2026-03-01 21:10:41 +08:00
|
|
|
|
} elseif (in_array($resultType, ['triple_gem', 'triple', 'pair'], true)) {
|
|
|
|
|
|
// 普通中奖:仅向本人发送聊天室系统通知
|
|
|
|
|
|
$net = $payout - $cost;
|
2026-03-04 15:00:02 +08:00
|
|
|
|
$content = "🎰 {$resultLabel}!{$e1}{$e2}{$e3} 赢得 +💰".number_format($net).' 金币';
|
2026-03-01 21:10:41 +08:00
|
|
|
|
$this->broadcastPersonal($user->username, $content);
|
|
|
|
|
|
} elseif ($resultType === 'curse') {
|
|
|
|
|
|
// 诅咒:通知本人
|
2026-03-04 15:00:02 +08:00
|
|
|
|
$content = "☠️ 三骷髅诅咒!{$e1}{$e2}{$e3} 额外扣除 💰".number_format($cost).' 金币!';
|
2026-03-01 21:10:41 +08:00
|
|
|
|
$this->broadcastPersonal($user->username, $content);
|
2026-03-01 21:00:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$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️⃣!"
|
2026-03-04 15:00:02 +08:00
|
|
|
|
.'狂揽 💰'.number_format($net).' 金币!全服见证奇迹!';
|
2026-03-01 21:00:21 +08:00
|
|
|
|
|
|
|
|
|
|
$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);
|
|
|
|
|
|
}
|
2026-03-01 21:10:41 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 向特定用户发送聊天室私人系统通知(仅该用户可见)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @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);
|
|
|
|
|
|
}
|
2026-03-01 21:00:21 +08:00
|
|
|
|
}
|