功能更新与UI优化:游戏图标移除、用户名片修复、婚礼红包界面重设计

- 移除聊天室右下角浮动游戏图标(占卜、百家乐、赛马、老虎机)
- 用户名片按钮区:修复已婚/已好友时按钮换行问题,统一单行显示
- 婚礼红包弹窗:重设计为喜庆鲜红背景,领取按钮改为圆形米黄样式
- 新增婚礼红包恢复接口(/wedding/pending-envelopes),刷新后自动恢复领取按钮
- 修复 Alpine :style 字符串覆盖静态 style 导致圆形按钮失效的问题
- 撤职后用户等级改为根据经验值重新计算,不再无条件重置为1
- 管理员修改用户经验值后自动重算等级,有职务用户等级锁定
- 娱乐大厅钓鱼游戏按钮直接调用 startFishing() 简化操作流程
- 新增赛马、占卜、百家乐游戏及相关后端逻辑
This commit is contained in:
2026-03-03 23:19:59 +08:00
parent 602dcd7cf1
commit f45483bcba
32 changed files with 3746 additions and 370 deletions
@@ -88,7 +88,7 @@ class UserManagerController extends Controller
*/
public function update(Request $request, User $user): JsonResponse|RedirectResponse
{
$targetUser = $user;
$targetUser = $user;
$currentUser = Auth::user();
// 超级管理员专属:仅 id=1 的账号可编辑用户信息
@@ -129,7 +129,24 @@ class UserManagerController extends Controller
);
$targetUser->refresh();
}
// 调整经验后重新计算等级(有职务用户锁定职务等级,无职务用户按经验重算)
$targetUser->load('activePosition.position');
$superLevel = (int) \App\Models\Sysparam::getValue('superlevel', '100');
if ($targetUser->activePosition?->position) {
// 有在职职务:等级锁定为职务级,不受经验影响
$lockedLevel = (int) $targetUser->activePosition->position->level;
if ($lockedLevel > 0 && $targetUser->user_level !== $lockedLevel) {
$targetUser->user_level = $lockedLevel;
}
} elseif ($targetUser->user_level < $superLevel) {
// 无职务普通用户:按经验重算等级(不超过满级阈值)
$newLevel = \App\Models\Sysparam::calculateLevel($targetUser->exp_num ?? 0);
$safeLevel = max(1, min($newLevel, $superLevel - 1));
$targetUser->user_level = $safeLevel;
}
}
if (isset($validated['jjb'])) {
$jjbDiff = $validated['jjb'] - ($targetUser->jjb ?? 0);
if ($jjbDiff !== 0) {
@@ -185,7 +202,7 @@ class UserManagerController extends Controller
*/
public function destroy(Request $request, User $user): RedirectResponse
{
$targetUser = $user;
$targetUser = $user;
$currentUser = Auth::user();
// 超级管理员专属:仅 id=1 的账号可删除用户
@@ -0,0 +1,166 @@
<?php
/**
* 文件功能:神秘占卜前台控制器
*
* 提供用户每日占卜功能:
* - 查询今日占卜状态(已占卜/未占卜/剩余次数)
* - 执行占卜(免费或付费)
* - 查询占卜历史记录
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers;
use App\Enums\CurrencySource;
use App\Models\FortuneLog;
use App\Models\GameConfig;
use App\Services\UserCurrencyService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class FortuneTellingController extends Controller
{
public function __construct(
private readonly UserCurrencyService $currency,
) {}
/**
* 查询今日占卜状态(用于面板初始化和刷新)。
*/
public function todayStatus(Request $request): JsonResponse
{
if (! GameConfig::isEnabled('fortune_telling')) {
return response()->json(['enabled' => false]);
}
$user = $request->user();
$config = GameConfig::forGame('fortune_telling')?->params ?? [];
$freeCount = (int) ($config['free_count_per_day'] ?? 1);
$extraCost = (int) ($config['extra_cost'] ?? 500);
$todayCount = FortuneLog::todayCount($user->id);
$todayLatest = FortuneLog::todayLatest($user->id);
$freeUsed = FortuneLog::query()
->where('user_id', $user->id)
->where('fortune_date', today())
->where('is_free', true)
->count();
$hasFreeLeft = $freeUsed < $freeCount;
return response()->json([
'enabled' => true,
'today_count' => $todayCount,
'free_count' => $freeCount,
'free_used' => $freeUsed,
'has_free_left' => $hasFreeLeft,
'extra_cost' => $extraCost,
'latest' => $todayLatest ? [
'grade' => $todayLatest->grade,
'grade_label' => $todayLatest->gradeLabel(),
'grade_color' => $todayLatest->gradeColor(),
'text' => $todayLatest->text,
'buff_desc' => $todayLatest->buff_desc,
'created_at' => $todayLatest->created_at->format('H:i'),
] : null,
]);
}
/**
* 执行一次占卜。
*
* 免费次数用完后每次消耗 extra_cost 金币。
*/
public function tell(Request $request): JsonResponse
{
if (! GameConfig::isEnabled('fortune_telling')) {
return response()->json(['ok' => false, 'message' => '神秘占卜当前未开启。']);
}
$user = $request->user();
$config = GameConfig::forGame('fortune_telling')?->params ?? [];
$freeCount = (int) ($config['free_count_per_day'] ?? 1);
$extraCost = (int) ($config['extra_cost'] ?? 500);
// 判断今日免费次数是否已用完
$freeUsed = FortuneLog::query()
->where('user_id', $user->id)
->where('fortune_date', today())
->where('is_free', true)
->count();
$isFree = $freeUsed < $freeCount;
$cost = $isFree ? 0 : $extraCost;
// 检查余额
if (! $isFree && ($user->jjb ?? 0) < $cost) {
return response()->json(['ok' => false, 'message' => "金币不足,额外占卜需要 {$cost} 金币。"]);
}
// 扣费
if (! $isFree && $cost > 0) {
$this->currency->change(
$user,
'gold',
-$cost,
CurrencySource::FORTUNE_COST,
'神秘占卜额外次数消耗',
);
}
// 抽签
$grade = FortuneLog::rollGrade($config);
$fortune = FortuneLog::rollFortune($grade);
// 记录
$log = FortuneLog::create([
'user_id' => $user->id,
'grade' => $grade,
'text' => $fortune['text'],
'buff_desc' => $fortune['buff_desc'] ?? null,
'is_free' => $isFree,
'cost' => $cost,
'fortune_date' => today(),
]);
return response()->json([
'ok' => true,
'grade' => $log->grade,
'grade_label' => $log->gradeLabel(),
'grade_color' => $log->gradeColor(),
'text' => $log->text,
'buff_desc' => $log->buff_desc,
'is_free' => $isFree,
'cost' => $cost,
]);
}
/**
* 查询近20条个人占卜历史记录。
*/
public function history(Request $request): JsonResponse
{
$logs = FortuneLog::query()
->where('user_id', $request->user()->id)
->orderByDesc('id')
->limit(20)
->get(['grade', 'text', 'buff_desc', 'is_free', 'cost', 'fortune_date', 'created_at'])
->map(fn ($log) => [
'grade' => $log->grade,
'grade_label' => $log->gradeLabel(),
'grade_color' => $log->gradeColor(),
'text' => $log->text,
'buff_desc' => $log->buff_desc,
'cost' => $log->cost,
'date' => $log->fortune_date->format('m-d'),
'time' => $log->created_at->format('H:i'),
]);
return response()->json(['history' => $logs]);
}
}
@@ -0,0 +1,224 @@
<?php
/**
* 文件功能:赛马竞猜前台控制器
*
* 提供用户在聊天室内参与赛马的 API 接口:
* - 查询当前场次信息(马匹、注池、赔率)
* - 提交下注(扣除金币 + 写入下注记录)
* - 查询本人下注状态
* - 查询最近历史记录
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers;
use App\Enums\CurrencySource;
use App\Models\GameConfig;
use App\Models\HorseBet;
use App\Models\HorseRace;
use App\Services\UserCurrencyService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class HorseRaceController extends Controller
{
public function __construct(
private readonly UserCurrencyService $currency,
) {}
/**
* 获取当前进行中的场次信息(前端轮询或事件触发后调用)。
*/
public function currentRace(Request $request): JsonResponse
{
$race = HorseRace::currentRace();
if (! $race) {
return response()->json(['race' => null]);
}
$user = $request->user();
$myBet = HorseBet::query()
->where('race_id', $race->id)
->where('user_id', $user->id)
->first();
// 计算各马匹当前注额
$config = GameConfig::forGame('horse_racing')?->params ?? [];
$houseTake = (int) ($config['house_take_percent'] ?? 5);
$horsePools = HorseBet::query()
->where('race_id', $race->id)
->groupBy('horse_id')
->selectRaw('horse_id, SUM(amount) as pool')
->pluck('pool', 'horse_id')
->toArray();
// 计算实时赔率
$horses = $race->horses ?? [];
$horsesWithBets = array_map(function ($horse) use ($horsePools, $houseTake) {
$horsePool = (int) ($horsePools[$horse['id']] ?? 0);
$totalPool = array_sum(array_values($horsePools));
$netPool = $totalPool * (1 - $houseTake / 100);
$odds = $horsePool > 0 ? round($netPool / $horsePool, 2) : null;
return [
'id' => $horse['id'],
'name' => $horse['name'],
'emoji' => $horse['emoji'],
'pool' => $horsePool,
'odds' => $odds,
];
}, $horses);
return response()->json([
'race' => [
'id' => $race->id,
'status' => $race->status,
'bet_closes_at' => $race->bet_closes_at?->toIso8601String(),
'seconds_left' => $race->status === 'betting'
? max(0, (int) now()->diffInSeconds($race->bet_closes_at, false))
: 0,
'horses' => $horsesWithBets,
'total_pool' => $race->total_pool + array_sum(array_values($horsePools)),
'my_bet' => $myBet ? [
'horse_id' => $myBet->horse_id,
'amount' => $myBet->amount,
] : null,
],
]);
}
/**
* 用户提交下注。
*
* 同一场每人限下一注,下注成功后立即扣除金币。
*/
public function bet(Request $request): JsonResponse
{
if (! GameConfig::isEnabled('horse_racing')) {
return response()->json(['ok' => false, 'message' => '赛马竞猜当前未开启。']);
}
$data = $request->validate([
'race_id' => 'required|integer|exists:horse_races,id',
'horse_id' => 'required|integer|min:1',
'amount' => 'required|integer|min:1',
]);
$config = GameConfig::forGame('horse_racing')?->params ?? [];
$minBet = (int) ($config['min_bet'] ?? 100);
$maxBet = (int) ($config['max_bet'] ?? 100000);
if ($data['amount'] < $minBet || $data['amount'] > $maxBet) {
return response()->json(['ok' => false, 'message' => "押注金额须在 {$minBet}~{$maxBet} 金币之间。"]);
}
$race = HorseRace::find($data['race_id']);
if (! $race || ! $race->isBettingOpen()) {
return response()->json(['ok' => false, 'message' => '当前不在下注时间内。']);
}
// 验证马匹 ID 是否有效
$horses = $race->horses ?? [];
$validIds = array_column($horses, 'id');
if (! in_array($data['horse_id'], $validIds, true)) {
return response()->json(['ok' => false, 'message' => '无效的马匹编号。']);
}
$user = $request->user();
$currency = $this->currency;
// 校验余额
if (($user->jjb ?? 0) < $data['amount']) {
return response()->json(['ok' => false, 'message' => '金币不足,无法下注。']);
}
return DB::transaction(function () use ($user, $race, $data, $currency, $horses): JsonResponse {
// 幂等:同一场只能下一注
$existing = HorseBet::query()
->where('race_id', $race->id)
->where('user_id', $user->id)
->lockForUpdate()
->exists();
if ($existing) {
return response()->json(['ok' => false, 'message' => '本场您已下注,请等待开奖。']);
}
// 找出马匹名称
$horseName = '';
foreach ($horses as $horse) {
if ((int) $horse['id'] === (int) $data['horse_id']) {
$horseName = ($horse['emoji'] ?? '').($horse['name'] ?? '');
break;
}
}
// 扣除金币
$currency->change(
$user,
'gold',
-$data['amount'],
CurrencySource::HORSE_BET,
"赛马 #{$race->id} 押注 {$horseName}",
);
// 写入下注记录
HorseBet::create([
'race_id' => $race->id,
'user_id' => $user->id,
'horse_id' => $data['horse_id'],
'amount' => $data['amount'],
'status' => 'pending',
]);
return response()->json([
'ok' => true,
'message' => "✅ 已押注「{$horseName}{$data['amount']} 金币,等待开跑!",
'amount' => $data['amount'],
'horse_id' => $data['horse_id'],
]);
});
}
/**
* 查询最近10场历史记录(前端展示胜负趋势)。
*/
public function history(): JsonResponse
{
$races = HorseRace::query()
->where('status', 'settled')
->orderByDesc('id')
->limit(10)
->get(['id', 'horses', 'winner_horse_id', 'total_pool', 'total_bets', 'settled_at']);
// 转换获胜马匹名称
$history = $races->map(function ($race) {
$winnerName = '未知';
foreach (($race->horses ?? []) as $horse) {
if (($horse['id'] ?? 0) === (int) $race->winner_horse_id) {
$winnerName = ($horse['emoji'] ?? '').($horse['name'] ?? '');
break;
}
}
return [
'id' => $race->id,
'winner_id' => $race->winner_horse_id,
'winner_name' => $winnerName,
'total_pool' => $race->total_pool,
'total_bets' => $race->total_bets,
'settled_at' => $race->settled_at?->toDateTimeString(),
];
});
return response()->json(['history' => $history]);
}
}
@@ -107,4 +107,36 @@ class WeddingController extends Controller
'expires_at' => $ceremony->expires_at,
]);
}
/**
* 查询当前用户所有未领取且未过期的婚礼红包(页面刷新后恢复领取按钮用)。
*/
public function pendingEnvelopes(Request $request): JsonResponse
{
$userId = $request->user()->id;
// 查询有未领取 claim 的 active 婚礼(未过期)
$claims = \App\Models\WeddingEnvelopeClaim::query()
->where('user_id', $userId)
->where('claimed', false)
->with(['ceremony.marriage.user', 'ceremony.marriage.partner', 'ceremony.tier'])
->get()
->filter(fn ($c) => $c->ceremony
&& in_array($c->ceremony->status, ['active'])
&& (! $c->ceremony->expires_at || $c->ceremony->expires_at->isFuture()))
->values();
return response()->json([
'envelopes' => $claims->map(fn ($c) => [
'ceremony_id' => $c->ceremony_id,
'amount' => $c->amount,
'total_amount' => $c->ceremony->total_amount,
'groom' => $c->ceremony->marriage->user->username ?? '—',
'bride' => $c->ceremony->marriage->partner->username ?? '—',
'tier_name' => $c->ceremony->tier?->name ?? '婚礼',
'tier_icon' => $c->ceremony->tier?->icon ?? '🎊',
'expires_at' => $c->ceremony->expires_at?->toDateTimeString(),
]),
]);
}
}