Files
webman-buildadmin/app/common/library/finance/WithdrawFlow.php
2026-04-20 10:31:14 +08:00

165 lines
5.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\common\library\finance;
use app\common\service\GameHotDataRedis;
/**
* 提现打码量(流水)门槛工具库
*
* 业务口径(打码量即提现配额模型):
* - 每笔提现消耗等额打码配额:折算 = withdraw_coin × ratio
* - lifetime_withdrawable_from_flow = bet_flow_coin / ratio
* - max_withdraw_by_flow = max(0, lifetime_withdrawable_from_flow - total_withdraw_coin)
* - 单笔上限max_withdrawable = min(coin_balance, max_withdraw_by_flow)
* - ratio 来自 game_config.withdraw_bet_flow_ratioratio = 0 代表不限制打码量,此时
* max_withdraw_by_flow 视为"无限大"(由 UNLIMITED_FLOW 哨兵值表示API 层兜底用余额)
*
* 向后兼容:原门槛 bet_flow_coin >= (total_deposit - total_withdraw) × ratio 已被
* "单笔上限 ≤ max_withdraw_by_flow" 取代且语义等价更细腻:任何通过新校验的请求必然
* 也满足旧门槛口径。字段 required_bet_flow / remaining_bet_flow / eligible 保留仅作展示。
*/
final class WithdrawFlow
{
public const CONFIG_KEY = 'withdraw_bet_flow_ratio';
public const DEFAULT_RATIO = '1.0000';
/** 当 ratio = 0不限打码max_withdraw_by_flow 用此哨兵表示"无限"。14 位整数位足够覆盖任何业务金额。 */
public const UNLIMITED_FLOW = '99999999999999.9999';
/** 单用户最多允许同时存在的「待审核」(withdraw_order.status=0) 提现订单数。 */
public const MAX_PENDING_WITHDRAW = 3;
/**
* 读取当前打码倍数(字符串 4 位小数,至少 0
*/
public static function ratio(): string
{
$row = GameHotDataRedis::gameConfigRow(self::CONFIG_KEY);
if (!$row) {
return self::DEFAULT_RATIO;
}
$val = $row['config_value'] ?? '';
if (!is_string($val) || trim($val) === '' || !is_numeric(trim($val))) {
return self::DEFAULT_RATIO;
}
$normalized = bcadd(trim($val), '0', 4);
if (bccomp($normalized, '0', 4) < 0) {
return '0.0000';
}
return $normalized;
}
/**
* 归一化金额字段到 4 位小数字符串,非法输入返回 '0.0000'
*/
public static function amountString($raw): string
{
if ($raw === null || $raw === '') {
return '0.0000';
}
if (is_string($raw)) {
$s = trim($raw);
} elseif (is_int($raw) || is_float($raw)) {
$s = strval($raw);
} else {
return '0.0000';
}
if (!is_numeric($s)) {
return '0.0000';
}
return bcadd($s, '0', 4);
}
/**
* 核算玩家当前打码量状态
*
* @param array{
* total_deposit_coin?: mixed,
* total_withdraw_coin?: mixed,
* bet_flow_coin?: mixed,
* }|null $userSnapshot 允许外部传入字典(节省一次查询);为 null 时按 $userId 从库取
*
* @return array{
* ratio: string,
* net_deposit: string,
* required_bet_flow: string,
* bet_flow_coin: string,
* remaining_bet_flow: string,
* eligible: bool,
* max_withdraw_by_flow: string,
* flow_unlimited: bool,
* }
*/
public static function status(?int $userId, ?array $userSnapshot = null): array
{
if ($userSnapshot === null && $userId !== null) {
$userSnapshot = Db::name('user')
->field(['total_deposit_coin', 'total_withdraw_coin', 'bet_flow_coin'])
->where('id', $userId)
->find();
}
$userSnapshot = is_array($userSnapshot) ? $userSnapshot : [];
$deposit = self::amountString($userSnapshot['total_deposit_coin'] ?? '0');
$withdraw = self::amountString($userSnapshot['total_withdraw_coin'] ?? '0');
$flow = self::amountString($userSnapshot['bet_flow_coin'] ?? '0');
$net = bcsub($deposit, $withdraw, 4);
if (bccomp($net, '0', 4) < 0) {
$net = '0.0000';
}
$ratio = self::ratio();
$required = bcmul($net, $ratio, 4);
$remaining = bcsub($required, $flow, 4);
if (bccomp($remaining, '0', 4) < 0) {
$remaining = '0.0000';
}
$eligible = bccomp($flow, $required, 4) >= 0;
// max_withdraw_by_flow = max(0, bet_flow_coin / ratio - total_withdraw_coin)
$unlimited = bccomp($ratio, '0', 4) === 0;
if ($unlimited) {
$maxByFlow = self::UNLIMITED_FLOW;
} else {
$lifetime = bcdiv($flow, $ratio, 4);
$maxByFlow = bcsub($lifetime, $withdraw, 4);
if (bccomp($maxByFlow, '0', 4) < 0) {
$maxByFlow = '0.0000';
}
}
return [
'ratio' => $ratio,
'net_deposit' => $net,
'required_bet_flow' => $required,
'bet_flow_coin' => $flow,
'remaining_bet_flow' => $remaining,
'eligible' => $eligible,
'max_withdraw_by_flow' => $maxByFlow,
'flow_unlimited' => $unlimited,
];
}
/**
* 取单笔最大可提现额 = min(coin_balance, max_withdraw_by_flow)。
* 返回值为 4 位小数字符串,已与 ratio=0不限逻辑兼容。
*/
public static function maxWithdrawable(string $coinBalance, array $flowStatus): string
{
$coin = self::amountString($coinBalance);
if (bccomp($coin, '0', 4) < 0) {
$coin = '0.0000';
}
if (!empty($flowStatus['flow_unlimited'])) {
return $coin;
}
$byFlow = self::amountString($flowStatus['max_withdraw_by_flow'] ?? '0');
return bccomp($coin, $byFlow, 4) <= 0 ? $coin : $byFlow;
}
}