功能:钓鱼游戏后台管理系统
一、钓鱼全局开关
- 钓鱼纳入 GameConfig(game_key=fishing),游戏管理页可一键开关
- cast() 接口加开关校验,关闭时返回 403 友好提示
- GameConfigSeeder 新增 fishing 配置(含4个参数)
二、钓鱼事件数据库化
- 新建 fishing_events 表(emoji/name/message/exp/jjb/weight/is_active/sort)
- FishingEvent 模型含 rollOne() 加权随机方法
- FishingEventSeeder 填充7条初始事件(经验降低、金币提升)
- FishingController::randomFishResult() 改为读数据库事件
三、钓鱼参数迁移至 GameConfig
- fishing_cost/wait_min/wait_max/cooldown 改为 GameConfig::param() 读取
- 保留 Sysparam fallback 兼容旧数据
四、后台管理页面
- 新建 FishingEventController(CRUD + AJAX toggle)
- 新建 admin/fishing/index.blade.php(事件列表+概率显示+编辑弹窗)
- 侧边栏「游戏管理」下方新增「🎣 钓鱼事件」入口
- 游戏管理视图 gameParamLabels 新增钓鱼参数标签
This commit is contained in:
108
app/Http/Controllers/Admin/FishingEventController.php
Normal file
108
app/Http/Controllers/Admin/FishingEventController.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:钓鱼事件后台管理控制器
|
||||
* 提供钓鱼事件的列表展示、创建、编辑、删除、启用/禁用功能
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\FishingEvent;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class FishingEventController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示所有钓鱼事件列表
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$events = FishingEvent::orderBy('sort')->orderBy('id')->get();
|
||||
$totalWeight = $events->where('is_active', true)->sum('weight');
|
||||
|
||||
return view('admin.fishing.index', compact('events', 'totalWeight'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新钓鱼事件
|
||||
*/
|
||||
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',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$data['is_active'] = $request->boolean('is_active', true);
|
||||
FishingEvent::create($data);
|
||||
|
||||
return redirect()->route('admin.fishing.index')->with('success', '钓鱼事件已添加!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新钓鱼事件
|
||||
*
|
||||
* @param FishingEvent $fishing 路由模型绑定
|
||||
*/
|
||||
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',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$data['is_active'] = $request->boolean('is_active');
|
||||
$fishing->update($data);
|
||||
|
||||
return redirect()->route('admin.fishing.index')->with('success', "事件「{$fishing->name}」已更新!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换事件启用/禁用状态(AJAX)
|
||||
*
|
||||
* @param FishingEvent $fishing 路由模型绑定
|
||||
*/
|
||||
public function toggle(FishingEvent $fishing): JsonResponse
|
||||
{
|
||||
$fishing->update(['is_active' => ! $fishing->is_active]);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'is_active' => $fishing->is_active,
|
||||
'message' => $fishing->is_active ? "「{$fishing->name}」已启用" : "「{$fishing->name}」已禁用",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除钓鱼事件
|
||||
*
|
||||
* @param FishingEvent $fishing 路由模型绑定
|
||||
*/
|
||||
public function destroy(FishingEvent $fishing): RedirectResponse
|
||||
{
|
||||
$name = $fishing->name;
|
||||
$fishing->delete();
|
||||
|
||||
return redirect()->route('admin.fishing.index')->with('success', "事件「{$name}」已删除!");
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\FishingEvent;
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\Sysparam;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\ShopService;
|
||||
@@ -56,6 +58,11 @@ class FishingController extends Controller
|
||||
return response()->json(['status' => 'error', 'message' => '请先登录'], 401);
|
||||
}
|
||||
|
||||
// 检查钓鱼全局开关
|
||||
if (! GameConfig::isEnabled('fishing')) {
|
||||
return response()->json(['status' => 'error', 'message' => '钓鱼功能暂未开放。'], 403);
|
||||
}
|
||||
|
||||
// 1. 检查冷却时间(Redis TTL)
|
||||
$cooldownKey = "fishing:cd:{$user->id}";
|
||||
if (Redis::exists($cooldownKey)) {
|
||||
@@ -69,7 +76,7 @@ class FishingController extends Controller
|
||||
}
|
||||
|
||||
// 2. 检查金币是否足够
|
||||
$cost = (int) Sysparam::getValue('fishing_cost', '5');
|
||||
$cost = (int) (GameConfig::param('fishing', 'fishing_cost') ?? Sysparam::getValue('fishing_cost', '5'));
|
||||
if (($user->jjb ?? 0) < $cost) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
@@ -87,8 +94,8 @@ class FishingController extends Controller
|
||||
$user->refresh();
|
||||
|
||||
// 4. 生成一次性 token,存入 Redis(TTL = 等待时间 + 收竿窗口 + 缓冲)
|
||||
$waitMin = (int) Sysparam::getValue('fishing_wait_min', '8');
|
||||
$waitMax = (int) 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}";
|
||||
@@ -169,7 +176,7 @@ class FishingController extends Controller
|
||||
Redis::del($tokenKey);
|
||||
|
||||
// 2. 设置冷却时间
|
||||
$cooldown = (int) Sysparam::getValue('fishing_cooldown', '300');
|
||||
$cooldown = (int) (GameConfig::param('fishing', 'fishing_cooldown') ?? Sysparam::getValue('fishing_cooldown', '300'));
|
||||
Redis::setex("fishing:cd:{$user->id}", $cooldown, time());
|
||||
|
||||
// 3. 随机决定钓鱼结果
|
||||
@@ -229,57 +236,31 @@ class FishingController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机钓鱼结果(复刻原版概率分布)
|
||||
* 随机钓鱼结果(从数据库 fishing_events 加权随机抽取)
|
||||
*
|
||||
* 若数据库中无激活事件,回退到兜底结果。
|
||||
*
|
||||
* @return array{emoji: string, message: string, exp: int, jjb: int}
|
||||
*/
|
||||
private function randomFishResult(): array
|
||||
{
|
||||
$roll = rand(1, 100);
|
||||
$event = FishingEvent::rollOne();
|
||||
|
||||
return match (true) {
|
||||
$roll <= 15 => [
|
||||
'emoji' => '🦈',
|
||||
'message' => '钓到一条大鲨鱼!增加经验100、金币20',
|
||||
'exp' => 100,
|
||||
'jjb' => 20,
|
||||
],
|
||||
$roll <= 30 => [
|
||||
'emoji' => '🐟',
|
||||
'message' => '钓到一条娃娃鱼,到集市卖得30个金币',
|
||||
'exp' => 0,
|
||||
'jjb' => 30,
|
||||
],
|
||||
$roll <= 50 => [
|
||||
'emoji' => '🐠',
|
||||
'message' => '钓到一只大草鱼,吃下增加经验50',
|
||||
'exp' => 50,
|
||||
'jjb' => 0,
|
||||
],
|
||||
$roll <= 70 => [
|
||||
'emoji' => '🐡',
|
||||
'message' => '钓到一条小鲤鱼,增加经验50、金币10',
|
||||
'exp' => 50,
|
||||
'jjb' => 10,
|
||||
],
|
||||
$roll <= 85 => [
|
||||
'emoji' => '💧',
|
||||
'message' => '鱼没钓到,摔到河里经验减少50',
|
||||
'exp' => -50,
|
||||
'jjb' => 0,
|
||||
],
|
||||
$roll <= 95 => [
|
||||
'emoji' => '👊',
|
||||
'message' => '偷钓鱼塘被主人发现,一阵殴打!经验减少20、金币减少3',
|
||||
'exp' => -20,
|
||||
'jjb' => -3,
|
||||
],
|
||||
default => [
|
||||
'emoji' => '🎉',
|
||||
'message' => '运气爆棚!钓到大鲨鱼、大草鱼、小鲤鱼各一条!经验+150,金币+50!',
|
||||
'exp' => 150,
|
||||
'jjb' => 50,
|
||||
],
|
||||
};
|
||||
// 数据库无事件时的兜底
|
||||
if (! $event) {
|
||||
return [
|
||||
'emoji' => '🐟',
|
||||
'message' => '钓到一条小鱼,获得金币10',
|
||||
'exp' => 0,
|
||||
'jjb' => 10,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'emoji' => $event->emoji,
|
||||
'message' => $event->message,
|
||||
'exp' => $event->exp,
|
||||
'jjb' => $event->jjb,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user