216 lines
6.5 KiB
PHP
216 lines
6.5 KiB
PHP
<?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);
|
||
}
|
||
}
|
||
}
|