优化ai小班长
This commit is contained in:
@@ -20,11 +20,15 @@ use App\Jobs\SaveMessageJob;
|
||||
use App\Models\Autoact;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Services\AiFinanceService;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\UserCurrencyService;
|
||||
use App\Services\VipService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* 定时模拟 AI小班长心跳,并同步维护其常规存款理财行为。
|
||||
*/
|
||||
class AiHeartbeatCommand extends Command
|
||||
{
|
||||
/**
|
||||
@@ -37,14 +41,21 @@ class AiHeartbeatCommand extends Command
|
||||
*/
|
||||
protected $description = '模拟 AI 小班长客户端心跳,触发经验金币与随机事件';
|
||||
|
||||
/**
|
||||
* 注入聊天室状态、VIP、积分与 AI 资金调度服务。
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState,
|
||||
private readonly VipService $vipService,
|
||||
private readonly UserCurrencyService $currencyService,
|
||||
private readonly AiFinanceService $aiFinance,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 AI小班长单次心跳,并处理奖励、随机事件与资金调度。
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
// 1. 检查总开关
|
||||
@@ -58,6 +69,9 @@ class AiHeartbeatCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// 心跳开始前,若手上金币已高于 100 万,则先把超出的部分转入银行。
|
||||
$this->aiFinance->bankExcessGold($user);
|
||||
|
||||
// 3. 常规心跳经验与金币发放
|
||||
// (模拟前端每30-60秒发一次心跳的过程,此处每分钟跑一次,发放单人心跳奖励)
|
||||
$expGain = $this->parseRewardValue(Sysparam::getValue('exp_per_heartbeat', '1'));
|
||||
@@ -133,7 +147,8 @@ class AiHeartbeatCommand extends Command
|
||||
$fishingChance = (int) Sysparam::getValue('chatbot_fishing_chance', '100'); // 默认 5% 概率
|
||||
if ($fishingEnabled && $fishingChance > 0 && rand(1, 100) <= $fishingChance && \App\Models\GameConfig::isEnabled('fishing')) {
|
||||
$cost = (int) (\App\Models\GameConfig::param('fishing', 'fishing_cost') ?? Sysparam::getValue('fishing_cost', '5'));
|
||||
if ($user->jjb >= $cost) {
|
||||
// 常规小游戏只使用当前手上金币,不再自动从银行补到 100 万。
|
||||
if ($this->aiFinance->prepareSpend($user, $cost)) {
|
||||
// 先扣除费用
|
||||
$this->currencyService->change(
|
||||
$user, 'gold', -$cost,
|
||||
@@ -155,6 +170,9 @@ class AiHeartbeatCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
// 心跳结束后,若新增金币让手上金额超过 100 万,则把超出的部分重新转回银行。
|
||||
$this->aiFinance->bankExcessGold($user);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Events\MessageSent;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\Sysparam;
|
||||
use App\Services\AiChatService;
|
||||
use App\Services\AiFinanceService;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -25,6 +26,9 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
/**
|
||||
* 处理用户与 AI小班长的对话、金币福利与上下文清理。
|
||||
*/
|
||||
class ChatBotController extends Controller
|
||||
{
|
||||
/**
|
||||
@@ -34,6 +38,7 @@ class ChatBotController extends Controller
|
||||
private readonly AiChatService $aiChat,
|
||||
private readonly ChatStateService $chatState,
|
||||
private readonly UserCurrencyService $currencyService,
|
||||
private readonly AiFinanceService $aiFinance,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -90,7 +95,8 @@ class ChatBotController extends Controller
|
||||
if ($dailyCount < $maxDailyRewards) {
|
||||
$goldAmount = rand(100, $maxGold);
|
||||
|
||||
if ($aiUser && $aiUser->jjb >= $goldAmount) {
|
||||
// 常规发福利只检查 AI 当前手上金币,不再为了维持 100 万而自动从银行提钱。
|
||||
if ($aiUser && $this->aiFinance->prepareSpend($aiUser, $goldAmount)) {
|
||||
Redis::incr($redisKey);
|
||||
Redis::expire($redisKey, 86400); // 缓存 24 小时
|
||||
|
||||
@@ -129,6 +135,9 @@ class ChatBotController extends Controller
|
||||
$this->chatState->pushMessage($roomId, $sysMsg);
|
||||
broadcast(new MessageSent($roomId, $sysMsg));
|
||||
SaveMessageJob::dispatch($sysMsg);
|
||||
|
||||
// 福利发放完成后,若手上金币仍高于 100 万,则把超出的部分回存银行。
|
||||
$this->aiFinance->bankExcessGold($aiUser);
|
||||
} else {
|
||||
// 如果余额不足
|
||||
$reply .= "\n\n(哎呀,我这个月的工资花光啦,没钱发金币了,大家多赏点吧~)";
|
||||
|
||||
@@ -23,6 +23,8 @@ use App\Models\BaccaratRound;
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Services\AiFinanceService;
|
||||
use App\Services\BaccaratLossCoverService;
|
||||
use App\Services\BaccaratPredictionService;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\UserCurrencyService;
|
||||
@@ -35,15 +37,24 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
/**
|
||||
* 控制 AI小班长在百家乐中的下注、观望与资金调度行为。
|
||||
*/
|
||||
class AiBaccaratBetJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* 注入当前需要处理的百家乐局次。
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly BaccaratRound $round,
|
||||
) {}
|
||||
|
||||
public function handle(UserCurrencyService $currency): void
|
||||
/**
|
||||
* 执行 AI小班长本局百家乐的完整决策流程。
|
||||
*/
|
||||
public function handle(UserCurrencyService $currency, AiFinanceService $aiFinance): void
|
||||
{
|
||||
// 1. 检查总开关与游戏开关
|
||||
if (Sysparam::getValue('chatbot_enabled', '0') !== '1' || ! GameConfig::isEnabled('baccarat')) {
|
||||
@@ -55,10 +66,8 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
$chatState = app(ChatStateService::class);
|
||||
|
||||
// 2. 资金管理:自动存款与领取补偿
|
||||
$this->manageFinances($user, $chatState);
|
||||
$this->manageFinances($user, $aiFinance);
|
||||
|
||||
$round = $this->round->fresh();
|
||||
if (! $round || ! $round->isBettingOpen()) {
|
||||
@@ -75,14 +84,20 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
$minBet = (int) ($config['min_bet'] ?? 100);
|
||||
$maxBet = (int) ($config['max_bet'] ?? 50000);
|
||||
|
||||
// 至少保留 100万 金币底仓(确保有充足资金参与多轮下注)
|
||||
$reserve = 1000000;
|
||||
$availableGold = ($user->jjb ?? 0) - $reserve;
|
||||
if ($availableGold < $minBet) {
|
||||
// 5. 查询当前是否命中“你玩游戏我买单”活动窗口。
|
||||
$lossCoverService = app(BaccaratLossCoverService::class);
|
||||
$lossCoverEvent = $lossCoverService->findEventForBetTime(now());
|
||||
$hasActiveLossCover = $lossCoverEvent?->status === 'active';
|
||||
|
||||
// 买单活动进行中时允许 AI 全仓下注;平时只动用当前手上的 jjb,不再强制保留 100 万。
|
||||
$bettableGold = $hasActiveLossCover
|
||||
? $aiFinance->getTotalGoldAssets($user)
|
||||
: $aiFinance->getSpendableGold($user);
|
||||
if ($bettableGold < $minBet) {
|
||||
return; // 资金不足以支撑最小下注
|
||||
}
|
||||
|
||||
// 5. 获取近期路单和 AI 历史下注
|
||||
// 6. 获取近期路单和 AI 历史下注
|
||||
$recentResults = BaccaratRound::query()
|
||||
->where('status', 'settled')
|
||||
->orderByDesc('id')
|
||||
@@ -108,12 +123,13 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
})
|
||||
->toArray();
|
||||
|
||||
// 5. 调用 AI 接口出具统筹策略
|
||||
// 7. 调用 AI 接口出具统筹策略
|
||||
$predictionService = app(BaccaratPredictionService::class);
|
||||
$context = [
|
||||
'recent_results' => $recentResults,
|
||||
'available_gold' => $availableGold,
|
||||
'available_gold' => $bettableGold,
|
||||
'historical_bets' => $historicalBets,
|
||||
'loss_cover_active' => $hasActiveLossCover,
|
||||
];
|
||||
|
||||
$aiPrediction = $predictionService->predict($context);
|
||||
@@ -128,14 +144,19 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
$percent = $aiPrediction['percentage'];
|
||||
$reason = $aiPrediction['reason'];
|
||||
|
||||
// 限定单局最高下注不超过可用金额的 5% 以防止 AI "乱梭哈" 破产
|
||||
$percent = min(5, max(0, $percent));
|
||||
|
||||
if ($betType !== 'pass') {
|
||||
$amount = (int) round($availableGold * ($percent / 100.0));
|
||||
$amount = max($minBet, min($amount, $maxBet));
|
||||
if ($amount > $user->jjb) {
|
||||
$amount = $user->jjb;
|
||||
if ($hasActiveLossCover) {
|
||||
// 买单活动进行中时,AI 可放心动用全部总资产,因为本局若输可返还。
|
||||
$amount = $bettableGold;
|
||||
$reason = trim($reason.' 买单活动进行中,本局采用总资产全额下注策略。');
|
||||
} else {
|
||||
// 非买单活动期间,限定单局最高下注不超过手头金币的 5% 以防止 AI 破产。
|
||||
$percent = min(5, max(0, $percent));
|
||||
$amount = (int) round($bettableGold * ($percent / 100.0));
|
||||
$amount = max($minBet, min($amount, $maxBet));
|
||||
if ($amount > $bettableGold) {
|
||||
$amount = $bettableGold;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -156,9 +177,15 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
}
|
||||
|
||||
if ($betType !== 'pass') {
|
||||
$percent = rand(2, 5) / 100.0;
|
||||
$amount = (int) round($availableGold * $percent);
|
||||
$amount = max($minBet, min($amount, $maxBet));
|
||||
if ($hasActiveLossCover) {
|
||||
// 本地兜底策略命中买单活动时,同样执行总资产全额下注。
|
||||
$amount = $bettableGold;
|
||||
$reason = '买单活动进行中,采用本地总资产全额下注兜底策略。';
|
||||
} else {
|
||||
$percent = rand(2, 5) / 100.0;
|
||||
$amount = (int) round($bettableGold * $percent);
|
||||
$amount = max($minBet, min($amount, $maxBet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +207,14 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
// 买单活动期间允许全仓下注;非活动期间只检查当前手上金币是否够本次下注。
|
||||
$isReadyToSpend = $hasActiveLossCover
|
||||
? $aiFinance->prepareAllInSpend($user, $amount)
|
||||
: $aiFinance->prepareSpend($user, $amount);
|
||||
if (! $isReadyToSpend) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 二次校验,防止大模型接口调用太慢导致下注时该局已关闭
|
||||
$round->refresh();
|
||||
if (! $round->isBettingOpen()) {
|
||||
@@ -188,12 +223,17 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. 执行下注 (同 BaccaratController::bet 逻辑)
|
||||
DB::transaction(function () use ($user, $round, $betType, $amount, $currency) {
|
||||
// 8. 执行下注 (同 BaccaratController::bet 逻辑)
|
||||
DB::transaction(function () use ($user, $round, $betType, $amount, $currency, $lossCoverEvent, $lossCoverService) {
|
||||
$lockedUser = User::query()->lockForUpdate()->find($user->id);
|
||||
if (! $lockedUser || (int) ($lockedUser->jjb ?? 0) < $amount) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 幂等:同一局只能下一注
|
||||
$existing = BaccaratBet::query()
|
||||
->where('round_id', $round->id)
|
||||
->where('user_id', $user->id)
|
||||
->where('user_id', $lockedUser->id)
|
||||
->lockForUpdate()
|
||||
->exists();
|
||||
|
||||
@@ -203,7 +243,7 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
|
||||
// 扣除金币
|
||||
$currency->change(
|
||||
$user,
|
||||
$lockedUser,
|
||||
'gold',
|
||||
-$amount,
|
||||
CurrencySource::BACCARAT_BET,
|
||||
@@ -213,14 +253,20 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
);
|
||||
|
||||
// 写入下注记录
|
||||
BaccaratBet::create([
|
||||
$bet = BaccaratBet::create([
|
||||
'round_id' => $round->id,
|
||||
'user_id' => $user->id,
|
||||
'user_id' => $lockedUser->id,
|
||||
'loss_cover_event_id' => $lossCoverEvent?->id,
|
||||
'bet_type' => $betType,
|
||||
'amount' => $amount,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
// 命中买单活动的下注需要登记到活动聚合记录里,确保后续能正确补偿返还。
|
||||
if ($lossCoverEvent) {
|
||||
$lossCoverService->registerBet($bet);
|
||||
}
|
||||
|
||||
// 更新局次汇总统计
|
||||
$field = 'total_bet_'.$betType;
|
||||
$countField = 'bet_count_'.$betType;
|
||||
@@ -232,6 +278,9 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
event(new \App\Events\BaccaratPoolUpdated($round));
|
||||
});
|
||||
|
||||
// 下注完成后,若手上金币仍高于 100 万,则把超出的部分继续回存银行。
|
||||
$aiFinance->bankExcessGold($user);
|
||||
|
||||
// 下注成功后,在聊天室发送一条普通聊天消息告知大家
|
||||
$this->broadcastBetMessage($user, $round->room_id ?? 1, $betType, $amount, $decisionSource);
|
||||
}
|
||||
@@ -311,9 +360,9 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 资金管理逻辑:自动存款、领取买单补偿
|
||||
* AI 资金管理逻辑:优先领取补偿,再按“超过 100 万才存款”的规则整理持仓。
|
||||
*/
|
||||
private function manageFinances(User $user, ChatStateService $chatState): void
|
||||
private function manageFinances(User $user, AiFinanceService $aiFinance): void
|
||||
{
|
||||
// 1. 检查是否有“买单”活动补偿可领取 (jjb 较低时优先领取)
|
||||
$lossCoverService = app(\App\Services\BaccaratLossCoverService::class);
|
||||
@@ -330,60 +379,6 @@ class AiBaccaratBetJob implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 自动存款逻辑:长期目标 1000万 / 3000万
|
||||
$bankTarget1 = 10000000;
|
||||
$bankTarget2 = 30000000;
|
||||
$currentBank = (int) $user->bank_jjb;
|
||||
|
||||
// 如果手上金币超过 10万,且存款未达 3000万,则进行存款
|
||||
if ($user->jjb > 1000000 && $currentBank < $bankTarget2) {
|
||||
$depositAmount = $user->jjb - 500000; // 留 5万在手上作为本金
|
||||
|
||||
// 如果存款快到目标了,按需存入
|
||||
if ($currentBank < $bankTarget1 && ($currentBank + $depositAmount) > $bankTarget1) {
|
||||
$depositAmount = $bankTarget1 - $currentBank;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($user, $depositAmount, $currentBank, $bankTarget1, $bankTarget2, $chatState) {
|
||||
$user->decrement('jjb', $depositAmount);
|
||||
$user->increment('bank_jjb', $depositAmount);
|
||||
$newBank = $user->bank_jjb;
|
||||
|
||||
\App\Models\BankLog::create([
|
||||
'user_id' => $user->id,
|
||||
'type' => 'deposit',
|
||||
'amount' => $depositAmount,
|
||||
'balance_after' => $newBank,
|
||||
]);
|
||||
|
||||
// 达成阶段性目标时大声宣布
|
||||
$milestone = 0;
|
||||
if ($currentBank < $bankTarget1 && $newBank >= $bankTarget1) {
|
||||
$milestone = 1000; // 1000万
|
||||
} elseif ($currentBank < $bankTarget2 && $newBank >= $bankTarget2) {
|
||||
$milestone = 3000; // 3000万
|
||||
}
|
||||
|
||||
if ($milestone > 0) {
|
||||
$content = "🏆 🎉 <b>【全站公告】</b> 恭喜 <b>AI小班长</b> 达成理财新高度!<br/>他在银行的存款已成功突破 <span style='color:#e11d48;font-weight:bold;'>{$milestone}万</span> 金币!💰✨";
|
||||
$msg = [
|
||||
'id' => $chatState->nextMessageId(1),
|
||||
'room_id' => 1,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '大家',
|
||||
'content' => $content,
|
||||
'is_secret' => false,
|
||||
'font_color' => '#f59e0b',
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
$chatState->pushMessage(1, $msg);
|
||||
broadcast(new MessageSent(1, $msg));
|
||||
SaveMessageJob::dispatch($msg);
|
||||
}
|
||||
});
|
||||
|
||||
Log::info("AI小班长自动存款: Amount: {$depositAmount}, New Bank Balance: {$user->bank_jjb}");
|
||||
}
|
||||
$aiFinance->rebalanceHoldings($user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:AI小班长资金调度服务
|
||||
*
|
||||
* 统一维护 AI小班长的银行存取与阶段性理财里程碑公告,
|
||||
* 常规场景下仅在手上金币超过 100 万时自动存银行,
|
||||
* 特殊场景(如买单活动全仓下注)才会动用银行资产。
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Events\MessageSent;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\BankLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 负责 AI小班长的常规存款、全仓取款与里程碑播报。
|
||||
*/
|
||||
class AiFinanceService
|
||||
{
|
||||
/**
|
||||
* AI小班长手上需要保留的最低可用金币。
|
||||
*/
|
||||
public const AVAILABLE_GOLD_RESERVE = 1000000;
|
||||
|
||||
/**
|
||||
* 银行存款的阶段性目标。
|
||||
*
|
||||
* @var list<int>
|
||||
*/
|
||||
private const BANK_MILESTONES = [
|
||||
10000000,
|
||||
30000000,
|
||||
];
|
||||
|
||||
/**
|
||||
* 注入聊天室状态服务,用于里程碑公告广播。
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 计算 AI 当前手上可直接使用的金币。
|
||||
*/
|
||||
public function getSpendableGold(User $user): int
|
||||
{
|
||||
return (int) ($user->jjb ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 AI 当前持有的金币总资产(手上 + 银行)。
|
||||
*/
|
||||
public function getTotalGoldAssets(User $user): int
|
||||
{
|
||||
return (int) ($user->jjb ?? 0) + (int) ($user->bank_jjb ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 AI 当前手上金币是否足够支付本次支出。
|
||||
*/
|
||||
public function prepareSpend(User $user, int $spendAmount): bool
|
||||
{
|
||||
if ($spendAmount <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$user->refresh();
|
||||
|
||||
return (int) ($user->jjb ?? 0) >= $spendAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为全仓支出准备手头金币。
|
||||
*
|
||||
* 该模式不会保留手上余额阈值,适用于买单活动等可接受全额下注的场景。
|
||||
*/
|
||||
public function prepareAllInSpend(User $user, int $spendAmount): bool
|
||||
{
|
||||
if ($spendAmount <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->raiseWalletTo($user, $spendAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 100 万以上的富余金币自动转存到银行。
|
||||
*/
|
||||
public function bankExcessGold(User $user): void
|
||||
{
|
||||
$milestones = DB::transaction(function () use ($user): array {
|
||||
$lockedUser = User::query()->lockForUpdate()->find($user->id);
|
||||
if (! $lockedUser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$walletGold = (int) ($lockedUser->jjb ?? 0);
|
||||
if ($walletGold <= self::AVAILABLE_GOLD_RESERVE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$bankBefore = (int) ($lockedUser->bank_jjb ?? 0);
|
||||
$depositAmount = $walletGold - self::AVAILABLE_GOLD_RESERVE;
|
||||
|
||||
// 只把超过 100 万的部分转入银行,手上保留不高于 100 万的常规活动资金。
|
||||
$lockedUser->jjb = self::AVAILABLE_GOLD_RESERVE;
|
||||
$lockedUser->bank_jjb = $bankBefore + $depositAmount;
|
||||
$lockedUser->save();
|
||||
|
||||
BankLog::create([
|
||||
'user_id' => $lockedUser->id,
|
||||
'type' => 'deposit',
|
||||
'amount' => $depositAmount,
|
||||
'balance_after' => (int) $lockedUser->bank_jjb,
|
||||
]);
|
||||
|
||||
return array_values(array_filter(
|
||||
self::BANK_MILESTONES,
|
||||
fn (int $milestone): bool => $bankBefore < $milestone && (int) $lockedUser->bank_jjb >= $milestone,
|
||||
));
|
||||
});
|
||||
|
||||
$user->refresh();
|
||||
|
||||
foreach ($milestones as $milestone) {
|
||||
$this->broadcastMilestoneAnnouncement($milestone);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性完成 AI 常规理财:仅把超过 100 万的部分转入银行。
|
||||
*/
|
||||
public function rebalanceHoldings(User $user): void
|
||||
{
|
||||
$this->bankExcessGold($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将手上金币抬升到目标值,必要时从银行自动取款。
|
||||
*/
|
||||
private function raiseWalletTo(User $user, int $targetWalletGold): bool
|
||||
{
|
||||
$reachedTarget = DB::transaction(function () use ($user, $targetWalletGold): bool {
|
||||
$lockedUser = User::query()->lockForUpdate()->find($user->id);
|
||||
if (! $lockedUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$walletGold = (int) ($lockedUser->jjb ?? 0);
|
||||
if ($walletGold >= $targetWalletGold) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$bankGold = (int) ($lockedUser->bank_jjb ?? 0);
|
||||
$withdrawAmount = min($targetWalletGold - $walletGold, $bankGold);
|
||||
if ($withdrawAmount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 优先把银行金币提回手上,保证 AI 的即时可用余额尽量回到目标线。
|
||||
$lockedUser->jjb = $walletGold + $withdrawAmount;
|
||||
$lockedUser->bank_jjb = $bankGold - $withdrawAmount;
|
||||
$lockedUser->save();
|
||||
|
||||
BankLog::create([
|
||||
'user_id' => $lockedUser->id,
|
||||
'type' => 'withdraw',
|
||||
'amount' => $withdrawAmount,
|
||||
'balance_after' => (int) $lockedUser->bank_jjb,
|
||||
]);
|
||||
|
||||
return (int) $lockedUser->jjb >= $targetWalletGold;
|
||||
});
|
||||
|
||||
$user->refresh();
|
||||
|
||||
return $reachedTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播 AI小班长达成银行存款目标的全站公告。
|
||||
*/
|
||||
private function broadcastMilestoneAnnouncement(int $milestone): void
|
||||
{
|
||||
$roomIds = $this->chatState->getAllActiveRoomIds();
|
||||
if (empty($roomIds)) {
|
||||
$roomIds = [1];
|
||||
}
|
||||
|
||||
$milestoneInWan = (int) ($milestone / 10000);
|
||||
$content = "🏆 🎉 <b>【全站公告】</b> 恭喜 <b>AI小班长</b> 达成理财新高度!<br/>他在银行的存款已成功突破 <span style='color:#e11d48;font-weight:bold;'>{$milestoneInWan}万</span> 金币!💰✨";
|
||||
|
||||
foreach ($roomIds as $roomId) {
|
||||
$message = [
|
||||
'id' => $this->chatState->nextMessageId($roomId),
|
||||
'room_id' => $roomId,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '大家',
|
||||
'content' => $content,
|
||||
'is_secret' => false,
|
||||
'font_color' => '#f59e0b',
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
$this->chatState->pushMessage($roomId, $message);
|
||||
broadcast(new MessageSent($roomId, $message));
|
||||
SaveMessageJob::dispatch($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:AI小班长百家乐下注任务测试
|
||||
*
|
||||
* 覆盖 AI 在“你玩游戏我买单”活动进行中时的全额下注策略,
|
||||
* 并验证下注会正确挂到买单活动记录上。
|
||||
*/
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\AiBaccaratBetJob;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\BaccaratLossCoverEvent;
|
||||
use App\Models\BaccaratRound;
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Services\BaccaratPredictionService;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 验证 AI小班长在百家乐中的特殊下注策略。
|
||||
*/
|
||||
class AiBaccaratBetJobTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 初始化百家乐开关与下注限额配置。
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
GameConfig::updateOrCreate(
|
||||
['game_key' => 'baccarat'],
|
||||
[
|
||||
'name' => 'Baccarat',
|
||||
'icon' => 'baccarat',
|
||||
'description' => 'Baccarat Game',
|
||||
'enabled' => true,
|
||||
'params' => [
|
||||
'min_bet' => 100,
|
||||
'max_bet' => 50000,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
Sysparam::updateOrCreate(['alias' => 'chatbot_enabled'], ['body' => '1']);
|
||||
Sysparam::clearCache('chatbot_enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* 买单活动进行中时,AI 会动用全部总资产下注,并把下注挂到活动名下。
|
||||
*/
|
||||
public function test_ai_bets_all_spendable_gold_when_loss_cover_event_is_active(): void
|
||||
{
|
||||
Event::fake();
|
||||
Queue::fake([SaveMessageJob::class]);
|
||||
|
||||
$this->mock(ChatStateService::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('nextMessageId')->andReturn(1);
|
||||
$mock->shouldReceive('pushMessage')->zeroOrMoreTimes();
|
||||
$mock->shouldReceive('getAllActiveRoomIds')->andReturn([1]);
|
||||
});
|
||||
|
||||
$this->mock(BaccaratPredictionService::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('predict')
|
||||
->once()
|
||||
->andReturn([
|
||||
'action' => 'big',
|
||||
'percentage' => 1,
|
||||
'reason' => '测试用低仓位建议',
|
||||
]);
|
||||
});
|
||||
|
||||
$aiUser = User::factory()->create([
|
||||
'username' => 'AI小班长',
|
||||
'jjb' => 1000000,
|
||||
'bank_jjb' => 200000,
|
||||
]);
|
||||
|
||||
$event = BaccaratLossCoverEvent::factory()->create([
|
||||
'status' => 'active',
|
||||
'starts_at' => now()->subMinutes(2),
|
||||
'ends_at' => now()->addMinutes(10),
|
||||
]);
|
||||
|
||||
$round = BaccaratRound::forceCreate([
|
||||
'status' => 'betting',
|
||||
'bet_opens_at' => now()->subSeconds(10),
|
||||
'bet_closes_at' => now()->addMinute(),
|
||||
'total_bet_big' => 0,
|
||||
'total_bet_small' => 0,
|
||||
'total_bet_triple' => 0,
|
||||
'bet_count' => 0,
|
||||
'bet_count_big' => 0,
|
||||
'bet_count_small' => 0,
|
||||
'bet_count_triple' => 0,
|
||||
'total_payout' => 0,
|
||||
]);
|
||||
|
||||
$job = new AiBaccaratBetJob($round);
|
||||
$job->handle(app(\App\Services\UserCurrencyService::class), app(\App\Services\AiFinanceService::class));
|
||||
|
||||
$this->assertDatabaseHas('baccarat_bets', [
|
||||
'round_id' => $round->id,
|
||||
'user_id' => $aiUser->id,
|
||||
'loss_cover_event_id' => $event->id,
|
||||
'bet_type' => 'big',
|
||||
'amount' => 1200000,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('baccarat_loss_cover_records', [
|
||||
'event_id' => $event->id,
|
||||
'user_id' => $aiUser->id,
|
||||
'total_bet_amount' => 1200000,
|
||||
'claim_status' => 'not_eligible',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('baccarat_loss_cover_events', [
|
||||
'id' => $event->id,
|
||||
'participant_count' => 1,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $aiUser->id,
|
||||
'jjb' => 0,
|
||||
'bank_jjb' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:AI小班长资金调度服务测试
|
||||
*
|
||||
* 覆盖 AI小班长的常规存款、全仓取款与阶段性目标公告逻辑,
|
||||
* 防止后续调整后再次出现“超过 100 万未存款”或全仓提取异常。
|
||||
*/
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Events\MessageSent;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\User;
|
||||
use App\Services\AiFinanceService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 验证 AI小班长的常规存款与全仓调仓规则。
|
||||
*/
|
||||
class AiFinanceServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 常规支出只检查手上金币,不会为了凑够金额自动从银行取款。
|
||||
*/
|
||||
public function test_prepare_spend_only_checks_wallet_gold_without_withdrawing_from_bank(): void
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'jjb' => 700000,
|
||||
'bank_jjb' => 600000,
|
||||
]);
|
||||
|
||||
$result = app(AiFinanceService::class)->prepareSpend($user, 800000);
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'jjb' => 700000,
|
||||
'bank_jjb' => 600000,
|
||||
]);
|
||||
$this->assertDatabaseMissing('bank_logs', [
|
||||
'user_id' => $user->id,
|
||||
'type' => 'withdraw',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全仓支出场景允许从银行提取差额,把总资产集中到手上。
|
||||
*/
|
||||
public function test_prepare_all_in_spend_withdraws_needed_gold_from_bank(): void
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'jjb' => 1000000,
|
||||
'bank_jjb' => 200000,
|
||||
]);
|
||||
|
||||
$result = app(AiFinanceService::class)->prepareAllInSpend($user, 1150000);
|
||||
|
||||
$this->assertTrue($result);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'jjb' => 1150000,
|
||||
'bank_jjb' => 50000,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('bank_logs', [
|
||||
'user_id' => $user->id,
|
||||
'type' => 'withdraw',
|
||||
'amount' => 150000,
|
||||
'balance_after' => 50000,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 超过 100 万的金币会自动存入银行,并在跨过阶段目标时发送全站公告。
|
||||
*/
|
||||
public function test_bank_excess_gold_deposits_surplus_and_broadcasts_when_crossing_milestone(): void
|
||||
{
|
||||
Event::fake([MessageSent::class]);
|
||||
Queue::fake([SaveMessageJob::class]);
|
||||
|
||||
$user = User::factory()->create([
|
||||
'username' => 'AI小班长',
|
||||
'jjb' => 1250000,
|
||||
'bank_jjb' => 9900000,
|
||||
]);
|
||||
|
||||
app(AiFinanceService::class)->bankExcessGold($user);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'jjb' => 1000000,
|
||||
'bank_jjb' => 10150000,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('bank_logs', [
|
||||
'user_id' => $user->id,
|
||||
'type' => 'deposit',
|
||||
'amount' => 250000,
|
||||
'balance_after' => 10150000,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(MessageSent::class);
|
||||
Queue::assertPushed(SaveMessageJob::class);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:AI小班长聊天控制器测试
|
||||
*
|
||||
* 覆盖 AI 开关、普通回复、金币福利以及上下文清理逻辑,
|
||||
* 同时验证金币福利发放时不会自动动用 AI 的银行存款。
|
||||
*/
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Sysparam;
|
||||
@@ -10,17 +17,26 @@ use Illuminate\Support\Facades\Redis;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 验证 AI小班长对话与金币福利接口行为。
|
||||
*/
|
||||
class ChatBotControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 每个测试结束后清空 Redis,避免每日奖励计数彼此污染。
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Redis::flushall();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_chatbot_disabled_by_default()
|
||||
/**
|
||||
* 默认情况下,AI 功能关闭时应直接拒绝聊天请求。
|
||||
*/
|
||||
public function test_chatbot_disabled_by_default(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
@@ -33,7 +49,10 @@ class ChatBotControllerTest extends TestCase
|
||||
$response->assertJson(['status' => 'error']);
|
||||
}
|
||||
|
||||
public function test_chatbot_can_reply()
|
||||
/**
|
||||
* AI 功能开启后,控制器会返回模型生成的正常回复。
|
||||
*/
|
||||
public function test_chatbot_can_reply(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
@@ -69,7 +88,10 @@ class ChatBotControllerTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_chatbot_can_give_gold()
|
||||
/**
|
||||
* 发放金币福利时,只使用 AI 当前手上金币,不会为了凑金额自动动用银行存款。
|
||||
*/
|
||||
public function test_chatbot_can_give_gold(): void
|
||||
{
|
||||
$user = User::factory()->create(['jjb' => 0]);
|
||||
|
||||
@@ -80,7 +102,8 @@ class ChatBotControllerTest extends TestCase
|
||||
User::factory()->create([
|
||||
'username' => 'AI小班长',
|
||||
'exp_num' => 0,
|
||||
'jjb' => 1000, // Ensure AI bot has enough gold
|
||||
'jjb' => 1000000,
|
||||
'bank_jjb' => 1000,
|
||||
]);
|
||||
|
||||
// Mock the AiChatService
|
||||
@@ -105,9 +128,16 @@ class ChatBotControllerTest extends TestCase
|
||||
$user->refresh();
|
||||
$this->assertGreaterThan(0, $user->jjb);
|
||||
$this->assertLessThanOrEqual(500, $user->jjb);
|
||||
|
||||
$aiUser = User::query()->where('username', 'AI小班长')->firstOrFail();
|
||||
$this->assertLessThan(1000000, (int) $aiUser->jjb);
|
||||
$this->assertSame(1000, (int) $aiUser->bank_jjb);
|
||||
}
|
||||
|
||||
public function test_clear_context()
|
||||
/**
|
||||
* 用户主动清空上下文时,应调用 AI 服务清理其上下文缓存。
|
||||
*/
|
||||
public function test_clear_context(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user