From aa1299c01813421ae052c5651513a04d6c1a3947 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Thu, 23 Apr 2026 10:15:01 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=96=B0=E5=A2=9E=E8=8E=B7=E5=8F=96=E5=85=85?= =?UTF-8?q?=E5=80=BC/=E6=8F=90=E7=8E=B0=E9=85=8D=E7=BD=AE=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3/api/finance/depositWithdrawConfig=202.=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=85=85=E5=80=BC=E5=92=8C=E6=8F=90=E7=8E=B0=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/Channel.php | 88 ++--- app/admin/controller/Dashboard.php | 14 +- .../controller/config/DepositChannel.php | 52 +-- app/admin/controller/config/DepositTier.php | 40 ++ .../config/FinanceCashierConfig.php | 28 -- .../controller/order/DepositChannelOrder.php | 29 -- app/admin/controller/order/WithdrawOrder.php | 42 +-- app/admin/controller/user/User.php | 12 +- app/api/controller/Account.php | 24 +- app/api/controller/Auth.php | 2 +- app/api/controller/Finance.php | 345 ++++++++++++++---- app/api/controller/Game.php | 16 +- app/api/controller/Wallet.php | 24 +- app/api/lang/en.php | 1 + app/api/lang/zh-cn.php | 1 + .../library/finance/DepositMockGateway.php | 71 ++++ .../library/finance/DepositSettlement.php | 32 +- app/common/library/finance/WithdrawFlow.php | 57 +-- app/common/library/game/DepositChannel.php | 34 +- app/common/library/game/DepositTier.php | 24 +- .../library/game/FinanceCashierConfig.php | 6 +- app/common/library/game/StreakWinReward.php | 4 +- .../service/DepositOrderExpireService.php | 81 ++++ app/common/service/GameBetSettleService.php | 30 +- app/common/service/GameLiveService.php | 20 +- app/common/service/GameRecordStatService.php | 20 +- app/process/DepositOrderExpireTicker.php | 22 ++ config/app.php | 7 + config/process.php | 6 + config/route.php | 6 + web/src/lang/backend/en/channel.ts | 2 +- .../backend/en/config/financeCashierConfig.ts | 6 +- .../backend/en/order/depositChannelOrder.ts | 3 - .../lang/backend/en/order/withdrawOrder.ts | 3 + web/src/lang/backend/zh-cn/channel.ts | 2 +- .../zh-cn/config/financeCashierConfig.ts | 6 +- .../zh-cn/order/depositChannelOrder.ts | 3 - .../lang/backend/zh-cn/order/withdrawOrder.ts | 3 + .../backend/agent/commissionRecord/index.vue | 40 +- .../agent/commissionRecord/popupForm.vue | 6 +- .../backend/agent/settlementPeriod/index.vue | 22 +- .../agent/settlementPeriod/popupForm.vue | 6 +- web/src/views/backend/channel/popupForm.vue | 8 +- .../backend/config/depositChannel/index.vue | 25 -- .../config/depositChannel/popupForm.vue | 21 -- .../backend/config/depositTier/index.vue | 6 +- .../backend/config/depositTier/popupForm.vue | 29 +- .../config/financeCashierConfig/index.vue | 33 +- web/src/views/backend/game/live/index.vue | 4 +- web/src/views/backend/game/record/index.vue | 2 +- .../views/backend/order/betOrder/index.vue | 2 +- .../order/depositChannelOrder/index.vue | 214 ----------- .../backend/order/depositOrder/index.vue | 2 +- .../backend/order/withdrawOrder/index.vue | 46 ++- .../backend/order/withdrawOrder/popupForm.vue | 19 +- 55 files changed, 901 insertions(+), 750 deletions(-) delete mode 100644 app/admin/controller/order/DepositChannelOrder.php create mode 100644 app/common/library/finance/DepositMockGateway.php create mode 100644 app/common/service/DepositOrderExpireService.php create mode 100644 app/process/DepositOrderExpireTicker.php delete mode 100644 web/src/lang/backend/en/order/depositChannelOrder.ts delete mode 100644 web/src/lang/backend/zh-cn/order/depositChannelOrder.ts delete mode 100644 web/src/views/backend/order/depositChannelOrder/index.vue diff --git a/app/admin/controller/Channel.php b/app/admin/controller/Channel.php index c64d73c..496b9ff 100644 --- a/app/admin/controller/Channel.php +++ b/app/admin/controller/Channel.php @@ -541,7 +541,7 @@ class Channel extends Backend return $this->error('该渠道下暂无管理员,无法配置分配比例'); } - $enabledSum = '0.0000'; + $enabledSum = '0.00'; $insertRows = []; foreach ($rowsRaw as $line) { if (!is_array($line)) { @@ -553,12 +553,12 @@ class Channel extends Backend } $status = ((int) ($line['status'] ?? 1)) === 1 ? 1 : 0; $shareRaw = $line['share_rate'] ?? null; - $shareRate = self::normalizeAmountScale($shareRaw === null ? '0' : (string) $shareRaw, 4); - if (bccomp($shareRate, '0', 4) < 0 || bccomp($shareRate, '100', 4) > 0) { + $shareRate = self::normalizeAmountScale($shareRaw === null ? '0' : (string) $shareRaw, 2); + if (bccomp($shareRate, '0', 2) < 0 || bccomp($shareRate, '100', 2) > 0) { return $this->error('分配比例必须在0到100之间'); } if ($status === 1) { - $enabledSum = bcadd($enabledSum, $shareRate, 4); + $enabledSum = bcadd($enabledSum, $shareRate, 2); } $insertRows[] = [ 'channel_id' => (int) $row['id'], @@ -572,7 +572,7 @@ class Channel extends Backend if ($insertRows === []) { return $this->error('请至少配置一条有效分配记录'); } - if (bccomp($enabledSum, '100.0000', 4) !== 0) { + if (bccomp($enabledSum, '100.00', 2) !== 0) { return $this->error('启用的分配比例总和必须等于100'); } @@ -696,7 +696,7 @@ class Channel extends Backend $stats = $this->aggregateBetOrderForChannel($channelId, $periodStartTs, $lastEnd !== null, $endTs); $totalBet = $stats['total_bet']; $totalPayout = $stats['total_payout']; - $profit = bcsub($totalBet, $totalPayout, 4); + $profit = bcsub($totalBet, $totalPayout, 2); $mode = (string) ($row['agent_mode'] ?? 'turnover'); $commission = $this->computeCommissionAmounts($row, $totalBet, $profit, $mode); @@ -740,7 +740,7 @@ class Channel extends Backend $base = $flag . $channelPart . $timePart; for ($i = 0; $i < 8; $i++) { - $randPart = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8)); + $randPart = strtoupper(substr(bin2hex(random_bytes(4)), 0, 2)); $no = $base . $randPart; if (!Db::name('agent_settlement_period')->where('settlement_no', $no)->value('id')) { return $no; @@ -784,10 +784,10 @@ class Channel extends Backend } $row = $query->field('SUM(total_amount) AS tb, SUM(win_amount) AS tw, SUM(jackpot_extra_amount) AS tj')->find(); - $tb = $row && $row['tb'] !== null && $row['tb'] !== '' ? (string) $row['tb'] : '0.0000'; - $tw = $row && $row['tw'] !== null && $row['tw'] !== '' ? (string) $row['tw'] : '0.0000'; - $tj = $row && $row['tj'] !== null && $row['tj'] !== '' ? (string) $row['tj'] : '0.0000'; - $totalPayout = bcadd($tw, $tj, 4); + $tb = $row && $row['tb'] !== null && $row['tb'] !== '' ? (string) $row['tb'] : '0.00'; + $tw = $row && $row['tw'] !== null && $row['tw'] !== '' ? (string) $row['tw'] : '0.00'; + $tj = $row && $row['tj'] !== null && $row['tj'] !== '' ? (string) $row['tj'] : '0.00'; + $totalPayout = bcadd($tw, $tj, 2); return [ 'total_bet' => number_format((float) $tb, 4, '.', ''), @@ -805,8 +805,8 @@ class Channel extends Backend if ($ratePercent === null || $ratePercent === '') { return '普通返水代理未配置返水分红比例'; } - $rateDec = bcdiv((string) $ratePercent, '100', 6); - $amount = bcmul($totalBet, $rateDec, 4); + $rateDec = bcdiv((string) $ratePercent, '100', 2); + $amount = bcmul($totalBet, $rateDec, 2); return [ 'commission_rate' => $rateDec, 'calc_base_amount' => $totalBet, @@ -825,27 +825,27 @@ class Channel extends Backend return '联营阶梯规则无效或为空'; } - if (bccomp($platformProfit, '0', 4) <= 0) { + if (bccomp($platformProfit, '0', 2) <= 0) { return [ - 'commission_rate' => '0.000000', - 'calc_base_amount' => '0.0000', - 'commission_amount' => '0.0000', + 'commission_rate' => '0.00', + 'calc_base_amount' => '0.00', + 'commission_amount' => '0.00', ]; } - $afterFee = bcmul($platformProfit, bcsub('1', (string) $fee, 8), 4); - if (bccomp($afterFee, '0', 4) <= 0) { + $afterFee = bcmul($platformProfit, bcsub('1', (string) $fee, 2), 2); + if (bccomp($afterFee, '0', 2) <= 0) { return [ - 'commission_rate' => '0.000000', - 'calc_base_amount' => '0.0000', - 'commission_amount' => '0.0000', + 'commission_rate' => '0.00', + 'calc_base_amount' => '0.00', + 'commission_amount' => '0.00', ]; } $playerLoss = $platformProfit; $share = $this->pickAffiliateShareRateFromLadder($rules, $playerLoss); $rateDec = number_format($share, 6, '.', ''); - $amount = bcmul($afterFee, $rateDec, 4); + $amount = bcmul($afterFee, $rateDec, 2); return [ 'commission_rate' => $rateDec, @@ -888,7 +888,7 @@ class Channel extends Backend ]; } usort($out, function ($a, $b) { - return bccomp($a['minLoss'], $b['minLoss'], 4); + return bccomp($a['minLoss'], $b['minLoss'], 2); }); return $out; } @@ -900,7 +900,7 @@ class Channel extends Backend { $chosen = (float) $rules[0]['shareRate']; foreach ($rules as $rule) { - if (bccomp($playerLoss, $rule['minLoss'], 4) >= 0) { + if (bccomp($playerLoss, $rule['minLoss'], 2) >= 0) { $chosen = (float) $rule['shareRate']; } } @@ -959,24 +959,24 @@ class Channel extends Backend ->select() ->toArray(); if ($rows !== []) { - $sum = '0.0000'; + $sum = '0.00'; $out = []; foreach ($rows as $row) { $adminId = (int) ($row['admin_id'] ?? 0); if ($adminId <= 0) { continue; } - $shareRate = self::normalizeAmountScale((string) ($row['share_rate'] ?? '0'), 4); - if (bccomp($shareRate, '0', 4) <= 0) { + $shareRate = self::normalizeAmountScale((string) ($row['share_rate'] ?? '0'), 2); + if (bccomp($shareRate, '0', 2) <= 0) { continue; } - $sum = bcadd($sum, $shareRate, 4); + $sum = bcadd($sum, $shareRate, 2); $out[] = [ 'admin_id' => $adminId, 'share_rate' => $shareRate, ]; } - if ($out !== [] && bccomp($sum, '100.0000', 4) === 0) { + if ($out !== [] && bccomp($sum, '100.00', 2) === 0) { return $out; } } @@ -987,7 +987,7 @@ class Channel extends Backend } return [[ 'admin_id' => (int) $fallbackAdminId, - 'share_rate' => '100.0000', + 'share_rate' => '100.00', ]]; } @@ -1007,19 +1007,19 @@ class Channel extends Backend if ($shareRows === []) { return []; } - $sum = '0.0000'; + $sum = '0.00'; $rows = []; $lastIndex = count($shareRows) - 1; foreach ($shareRows as $index => $shareRow) { - $shareRate = self::normalizeAmountScale((string) ($shareRow['share_rate'] ?? '0'), 4); - $shareDec = bcdiv($shareRate, '100', 8); + $shareRate = self::normalizeAmountScale((string) ($shareRow['share_rate'] ?? '0'), 2); + $shareDec = bcdiv($shareRate, '100', 2); $amount = $index === $lastIndex - ? bcsub($commissionTotal, $sum, 4) - : bcmul($commissionTotal, $shareDec, 4); + ? bcsub($commissionTotal, $sum, 2) + : bcmul($commissionTotal, $shareDec, 2); if ($index !== $lastIndex) { - $sum = bcadd($sum, $amount, 4); + $sum = bcadd($sum, $amount, 2); } - $effectiveRate = bccomp($calcBaseAmount, '0', 4) <= 0 ? '0.000000' : bcdiv($amount, $calcBaseAmount, 6); + $effectiveRate = bccomp($calcBaseAmount, '0', 2) <= 0 ? '0.00' : bcdiv($amount, $calcBaseAmount, 2); $rows[] = [ 'settlement_period_id' => $periodId, 'channel_id' => $channelId, @@ -1052,17 +1052,17 @@ class Channel extends Backend } $adminNames = Db::name('admin')->where('id', 'in', $adminIds)->column('username', 'id'); - $sum = '0.0000'; + $sum = '0.00'; $out = []; $lastIndex = count($shareRows) - 1; foreach ($shareRows as $index => $shareRow) { - $shareRate = self::normalizeAmountScale((string) ($shareRow['share_rate'] ?? '0'), 4); - $shareDec = bcdiv($shareRate, '100', 8); + $shareRate = self::normalizeAmountScale((string) ($shareRow['share_rate'] ?? '0'), 2); + $shareDec = bcdiv($shareRate, '100', 2); $amount = $index === $lastIndex - ? bcsub($commissionTotal, $sum, 4) - : bcmul($commissionTotal, $shareDec, 4); + ? bcsub($commissionTotal, $sum, 2) + : bcmul($commissionTotal, $shareDec, 2); if ($index !== $lastIndex) { - $sum = bcadd($sum, $amount, 4); + $sum = bcadd($sum, $amount, 2); } $adminId = (int) ($shareRow['admin_id'] ?? 0); $out[] = [ diff --git a/app/admin/controller/Dashboard.php b/app/admin/controller/Dashboard.php index db362e9..927704f 100644 --- a/app/admin/controller/Dashboard.php +++ b/app/admin/controller/Dashboard.php @@ -108,7 +108,7 @@ class Dashboard extends Backend if ($scope !== null) { $q->whereIn('channel_id', $scope); } - $rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(amount AS DECIMAL(18,4))),0) AS s')->find(); + $rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(amount AS DECIMAL(18,2))),0) AS s')->find(); if (!is_array($rows)) { $rows = []; } @@ -146,7 +146,7 @@ class Dashboard extends Backend if ($scope !== null) { $q->whereIn('channel_id', $scope); } - $rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(total_amount AS DECIMAL(18,4))),0) AS s')->find(); + $rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(total_amount AS DECIMAL(18,2))),0) AS s')->find(); if (!is_array($rows)) { $rows = []; } @@ -181,7 +181,7 @@ class Dashboard extends Backend if ($scope !== null) { $dq->whereIn('channel_id', $scope); } - $drow = $dq->fieldRaw('COALESCE(SUM(CAST(amount AS DECIMAL(18,4))),0) AS s')->find(); + $drow = $dq->fieldRaw('COALESCE(SUM(CAST(amount AS DECIMAL(18,2))),0) AS s')->find(); $dsum = is_array($drow) && isset($drow['s']) ? strval($drow['s']) : '0'; $depositAmounts[] = $this->formatMoney2($dsum); @@ -192,7 +192,7 @@ class Dashboard extends Backend if ($scope !== null) { $bq->whereIn('channel_id', $scope); } - $brow = $bq->fieldRaw('COALESCE(SUM(CAST(total_amount AS DECIMAL(18,4))),0) AS s')->find(); + $brow = $bq->fieldRaw('COALESCE(SUM(CAST(total_amount AS DECIMAL(18,2))),0) AS s')->find(); $bsum = is_array($brow) && isset($brow['s']) ? strval($brow['s']) : '0'; $betAmounts[] = $this->formatMoney2($bsum); } @@ -264,7 +264,7 @@ class Dashboard extends Backend { $q = Db::name('deposit_order') ->where('status', 1) - ->fieldRaw('channel_id, COALESCE(SUM(CAST(amount AS DECIMAL(18,4))),0) AS s') + ->fieldRaw('channel_id, COALESCE(SUM(CAST(amount AS DECIMAL(18,2))),0) AS s') ->group('channel_id'); if ($scope !== null) { $q->whereIn('channel_id', $scope); @@ -308,7 +308,7 @@ class Dashboard extends Backend $rest = array_slice($list, 8); $other = '0'; foreach ($rest as $item) { - $other = bcadd($other, $item['value'], 4); + $other = bcadd($other, $item['value'], 2); } $otherFormatted = $this->formatMoney2($other); if (bccomp($otherFormatted, '0', 2) > 0) { @@ -366,7 +366,7 @@ class Dashboard extends Backend if (!is_numeric($amount)) { return '0.00'; } - $normalized = bcadd($amount, '0', 4); + $normalized = bcadd($amount, '0', 2); return bcadd($normalized, '0', 2); } diff --git a/app/admin/controller/config/DepositChannel.php b/app/admin/controller/config/DepositChannel.php index d1edf51..badd05f 100644 --- a/app/admin/controller/config/DepositChannel.php +++ b/app/admin/controller/config/DepositChannel.php @@ -4,7 +4,6 @@ namespace app\admin\controller\config; use app\common\controller\Backend; use app\common\library\game\DepositChannel as DepositChannelLib; -use app\common\library\game\DepositTier as DepositTierLib; use app\common\library\game\FinanceCashierConfig as FinanceCashierConfigLib; use app\common\service\GameHotDataCoordinator; use app\common\service\GameHotDataLock; @@ -56,7 +55,7 @@ class DepositChannel extends Backend } /** - * 列表(baTable:list / total / remark)+ registry + tier_options(弹窗用) + * 列表(baTable:list / total / remark)+ registry(弹窗展示名) */ public function index(WebmanRequest $request): Response { @@ -71,7 +70,6 @@ class DepositChannel extends Backend return $this->error(__('Parameter error')); } - $tierOptions = $this->buildTierOptions(); $registryOut = $this->buildRegistryOut(); $parsed = DepositChannelLib::parseStoredOverridesFromDb(); @@ -136,7 +134,6 @@ class DepositChannel extends Backend 'total' => $total, 'remark' => '', 'registry' => $registryOut, - 'tier_options' => $tierOptions, 'items' => $pageRows, ]); } @@ -308,32 +305,6 @@ class DepositChannel extends Backend } } - /** - * @return list - */ - private function buildTierOptions(): array - { - $tierRow = Db::name('game_config')->where('config_key', DepositTierLib::CONFIG_KEY)->find(); - $allTiers = DepositTierLib::parseFromConfigValue($tierRow['config_value'] ?? null); - $tierOptions = []; - foreach ($allTiers as $t) { - if (!is_array($t)) { - continue; - } - $tid = isset($t['id']) && is_string($t['id']) ? $t['id'] : ''; - if ($tid === '') { - continue; - } - $title = isset($t['title']) && is_string($t['title']) ? trim($t['title']) : ''; - $tierOptions[] = [ - 'id' => $tid, - 'label' => $title !== '' ? $title . ' (' . $tid . ')' : $tid, - ]; - } - - return $tierOptions; - } - /** * @return array */ @@ -376,12 +347,6 @@ class DepositChannel extends Backend } else { $form['status'] = $current['status'] ?? 0; } - if (array_key_exists('tier_ids', $payload) && is_array($payload['tier_ids'])) { - $form['tier_ids'] = $payload['tier_ids']; - } else { - $form['tier_ids'] = isset($current['tier_ids']) && is_array($current['tier_ids']) ? $current['tier_ids'] : []; - } - return $this->normalizeChannelFormRow($form, $code); } @@ -398,24 +363,11 @@ class DepositChannel extends Backend if ($st === true || $st === 1 || $st === '1') { $status = 1; } - $tierIds = []; - if (isset($payload['tier_ids']) && is_array($payload['tier_ids'])) { - foreach ($payload['tier_ids'] as $tid) { - if (is_string($tid)) { - $t = trim($tid); - if ($t !== '' && preg_match('/^[a-zA-Z0-9_\-]{1,32}$/', $t)) { - $tierIds[] = $t; - } - } - } - $tierIds = array_values(array_unique($tierIds)); - } - return [ 'code' => $code, 'sort' => $sort, 'status' => $status, - 'tier_ids' => $tierIds, + 'tier_ids' => [], ]; } } diff --git a/app/admin/controller/config/DepositTier.php b/app/admin/controller/config/DepositTier.php index 18e4976..364442e 100644 --- a/app/admin/controller/config/DepositTier.php +++ b/app/admin/controller/config/DepositTier.php @@ -3,6 +3,7 @@ namespace app\admin\controller\config; use app\common\controller\Backend; +use app\common\library\game\FinanceCashierConfig as FinanceCashierConfigLib; use app\common\library\game\DepositTier as DepositTierLib; use app\common\service\GameHotDataCoordinator; use app\common\service\GameHotDataLock; @@ -21,6 +22,45 @@ class DepositTier extends Backend protected array $noNeedPermission = ['index', 'save']; + /** + * 读取支付货币下拉(来源:game_config.finance_cashier.currencies)。 + * 用于充值档位表单的“支付货币”选项,避免前端硬编码。 + */ + public function currencyOptions(WebmanRequest $request): Response + { + $response = $this->initializeBackend($request); + if ($response !== null) { + return $response; + } + if (!$this->hasNodePermission($request, 'index')) { + return $this->error(__('You have no permission'), [], 401); + } + if ($request->method() !== 'GET') { + return $this->error(__('Parameter error')); + } + $row = Db::name('game_config')->where('config_key', FinanceCashierConfigLib::CONFIG_KEY)->find(); + $cfg = FinanceCashierConfigLib::parseFromConfigValue($row['config_value'] ?? null); + $currencies = []; + if (isset($cfg['currencies']) && is_array($cfg['currencies'])) { + foreach ($cfg['currencies'] as $item) { + if (!is_array($item)) { + continue; + } + $code = isset($item['code']) && is_string($item['code']) ? strtoupper(trim($item['code'])) : ''; + if ($code === '') { + continue; + } + $currencies[] = $code; + } + } + $currencies = array_values(array_unique($currencies)); + if ($currencies === []) { + $currencies = ['MYR', 'CNY', 'USD', 'USDT', 'VND', 'THB', 'SGD', 'IDR']; + } + + return $this->success('', ['list' => $currencies]); + } + private function hasNodePermission(WebmanRequest $request, string $action): bool { if (!$this->auth) { diff --git a/app/admin/controller/config/FinanceCashierConfig.php b/app/admin/controller/config/FinanceCashierConfig.php index 53f2455..c1ad7a6 100644 --- a/app/admin/controller/config/FinanceCashierConfig.php +++ b/app/admin/controller/config/FinanceCashierConfig.php @@ -6,7 +6,6 @@ namespace app\admin\controller\config; use app\common\controller\Backend; use app\common\library\game\DepositChannel as DepositChannelLib; -use app\common\library\game\DepositTier as DepositTierLib; use app\common\library\game\FinanceCashierConfig as FinanceCashierConfigLib; use app\common\service\GameHotDataCoordinator; use app\common\service\GameHotDataLock; @@ -91,7 +90,6 @@ class FinanceCashierConfig extends Backend return $this->success('', [ 'form' => $form, 'registry' => $this->buildRegistryOut(), - 'tier_options' => $this->buildTierOptions(), ]); } @@ -176,32 +174,6 @@ class FinanceCashierConfig extends Backend } } - /** - * @return list - */ - private function buildTierOptions(): array - { - $tierRow = Db::name('game_config')->where('config_key', DepositTierLib::CONFIG_KEY)->find(); - $allTiers = DepositTierLib::parseFromConfigValue(is_array($tierRow) ? ($tierRow['config_value'] ?? null) : null); - $tierOptions = []; - foreach ($allTiers as $t) { - if (!is_array($t)) { - continue; - } - $tid = isset($t['id']) && is_string($t['id']) ? $t['id'] : ''; - if ($tid === '') { - continue; - } - $title = isset($t['title']) && is_string($t['title']) ? trim($t['title']) : ''; - $tierOptions[] = [ - 'id' => $tid, - 'label' => $title !== '' ? $title . ' (' . $tid . ')' : $tid, - ]; - } - - return $tierOptions; - } - /** * @return array */ diff --git a/app/admin/controller/order/DepositChannelOrder.php b/app/admin/controller/order/DepositChannelOrder.php deleted file mode 100644 index 05608e2..0000000 --- a/app/admin/controller/order/DepositChannelOrder.php +++ /dev/null @@ -1,29 +0,0 @@ -> $where - */ - protected function appendDepositOrderIndexWhere(array &$where, string $mainShort): void - { - if ($mainShort === '') { - return; - } - $effective = DepositChannel::effectiveRowsFromDb(); - $codes = DepositChannel::enabledPayChannelCodes($effective); - if ($codes === []) { - $where[] = [$mainShort . '.pay_channel', '=', '__no_pay_channel__']; - - return; - } - $where[] = [$mainShort . '.pay_channel', 'in', $codes]; - } -} diff --git a/app/admin/controller/order/WithdrawOrder.php b/app/admin/controller/order/WithdrawOrder.php index a5ff8e9..eac55fb 100644 --- a/app/admin/controller/order/WithdrawOrder.php +++ b/app/admin/controller/order/WithdrawOrder.php @@ -24,7 +24,7 @@ class WithdrawOrder extends Backend protected bool $modelSceneValidate = true; - protected string|array $quickSearchField = ['id', 'order_no', 'remark']; + protected string|array $quickSearchField = ['id', 'order_no', 'idempotency_key', 'receive_type', 'receive_account', 'remark']; protected string|array $defaultSortField = ['id' => 'desc']; @@ -119,16 +119,16 @@ class WithdrawOrder extends Backend $newAmount = $this->decimalParam($request->post('amount'), '0'); $newFee = $this->decimalParam($request->post('fee'), '0'); - if (bccomp($newAmount, '0', 4) <= 0) { + if (bccomp($newAmount, '0', 2) <= 0) { return $this->error('申请金额必须大于 0'); } - if (bccomp($newFee, '0', 4) < 0) { + if (bccomp($newFee, '0', 2) < 0) { return $this->error('手续费不能为负'); } - if (bccomp($newFee, $newAmount, 4) > 0) { + if (bccomp($newFee, $newAmount, 2) > 0) { return $this->error('手续费不能大于申请金额'); } - $newActual = bcsub($newAmount, $newFee, 4); + $newActual = bcsub($newAmount, $newFee, 2); $remarkRaw = $request->post('remark'); $remark = is_string($remarkRaw) ? trim($remarkRaw) : ''; @@ -149,8 +149,8 @@ class WithdrawOrder extends Backend if ($userId <= 0) { return $this->error('订单缺少用户信息'); } - $oldAmount = bcadd(strval($order['amount'] ?? '0'), '0', 4); - $diff = bcsub($newAmount, $oldAmount, 4); + $oldAmount = bcadd(strval($order['amount'] ?? '0'), '0', 2); + $diff = bcsub($newAmount, $oldAmount, 2); $now = time(); $adminId = $this->intParam($this->auth->id ?? 0); @@ -168,7 +168,7 @@ class WithdrawOrder extends Backend Db::startTrans(); try { // 金额调整差额处理 - $cmp = bccomp($diff, '0', 4); + $cmp = bccomp($diff, '0', 2); if ($cmp > 0) { // 新金额更大:再冻结用户 diff $userRow = Db::name('user')->where('id', $userId)->find(); @@ -176,12 +176,12 @@ class WithdrawOrder extends Backend Db::rollback(); return $this->error('关联用户不存在'); } - $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 4); - if (bccomp($beforeCoin, $diff, 4) < 0) { + $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2); + if (bccomp($beforeCoin, $diff, 2) < 0) { Db::rollback(); return $this->error('用户余额不足以补扣调整差额'); } - $afterCoin = bcsub($beforeCoin, $diff, 4); + $afterCoin = bcsub($beforeCoin, $diff, 2); Db::name('user')->where('id', $userId)->update([ 'coin' => $afterCoin, 'total_withdraw_coin' => Db::raw('total_withdraw_coin + ' . $diff), @@ -205,14 +205,14 @@ class WithdrawOrder extends Backend ]); } elseif ($cmp < 0) { // 新金额更小:退回差额 - $abs = bcsub('0', $diff, 4); + $abs = bcsub('0', $diff, 2); $userRow = Db::name('user')->where('id', $userId)->find(); if (!$userRow) { Db::rollback(); return $this->error('关联用户不存在'); } - $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 4); - $afterCoin = bcadd($beforeCoin, $abs, 4); + $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2); + $afterCoin = bcadd($beforeCoin, $abs, 2); Db::name('user')->where('id', $userId)->update([ 'coin' => $afterCoin, 'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $abs), @@ -301,7 +301,7 @@ class WithdrawOrder extends Backend if ($userId <= 0) { return $this->error('订单缺少用户信息'); } - $amount = bcadd(strval($order['amount'] ?? '0'), '0', 4); + $amount = bcadd(strval($order['amount'] ?? '0'), '0', 2); $channelIdRaw = $order['channel_id'] ?? null; $channelId = ($channelIdRaw === null || $channelIdRaw === '') ? null @@ -318,8 +318,8 @@ class WithdrawOrder extends Backend Db::rollback(); return $this->error('关联用户不存在'); } - $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 4); - $afterCoin = bcadd($beforeCoin, $amount, 4); + $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2); + $afterCoin = bcadd($beforeCoin, $amount, 2); Db::name('user')->where('id', $userId)->update([ 'coin' => $afterCoin, 'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $amount), @@ -413,9 +413,9 @@ class WithdrawOrder extends Backend private function decimalParam($raw, string $default): string { if ($raw === null || $raw === '' || !is_numeric(strval($raw))) { - return bcadd($default, '0', 4); + return bcadd($default, '0', 2); } - return bcadd(strval($raw), '0', 4); + return bcadd(strval($raw), '0', 2); } private function adminDisplayName(): string @@ -432,14 +432,14 @@ class WithdrawOrder extends Backend } /** - * 把 4 位小数金额压缩成最多 2 位小数用于展示(不影响落库精度) + * 把 2 位小数金额压缩成最多 2 位小数用于展示(不影响落库精度) */ private function shortAmount(string $amount): string { if (!is_numeric($amount)) { return $amount; } - $normalized = bcadd($amount, '0', 4); + $normalized = bcadd($amount, '0', 2); $negative = false; if (str_starts_with($normalized, '-')) { $negative = true; diff --git a/app/admin/controller/user/User.php b/app/admin/controller/user/User.php index feaf940..f4d5ab3 100644 --- a/app/admin/controller/user/User.php +++ b/app/admin/controller/user/User.php @@ -232,7 +232,7 @@ class User extends Backend if ($amountText === '' || !is_numeric($amountText)) { return $this->error('金额格式不正确'); } - if (bccomp($amountText, '0', 4) <= 0) { + if (bccomp($amountText, '0', 2) <= 0) { return $this->error('金额必须大于0'); } @@ -267,16 +267,16 @@ class User extends Backend } $before = strval($user['coin'] ?? '0'); - $delta = self::normalizeAmountScale($amountText, 4); + $delta = self::normalizeAmountScale($amountText, 2); if ($op === 'credit') { - $after = bcadd($before, $delta, 4); + $after = bcadd($before, $delta, 2); $bizType = 'admin_credit'; $direction = 1; } else { - if (bccomp($before, $delta, 4) < 0) { + if (bccomp($before, $delta, 2) < 0) { return $this->error('余额不足,扣点失败'); } - $after = bcsub($before, $delta, 4); + $after = bcsub($before, $delta, 2); $bizType = 'admin_deduct'; $direction = 2; } @@ -365,7 +365,7 @@ class User extends Backend private static function formatAmountForDisplay(string $amount): string { - $normalized = self::normalizeAmountScale($amount, 4); + $normalized = self::normalizeAmountScale($amount, 2); $negative = false; if (str_starts_with($normalized, '-')) { $negative = true; diff --git a/app/api/controller/Account.php b/app/api/controller/Account.php index 20a7711..ace499d 100644 --- a/app/api/controller/Account.php +++ b/app/api/controller/Account.php @@ -77,20 +77,20 @@ class Account extends Frontend 'create_time' => $user->create_time ?? 0, // 资金字段(4 位小数字符串,与 /api/wallet/balanceSummary 对齐) - 'coin' => $coinBalance, - 'coin_balance' => $coinBalance, - 'frozen_balance' => '0.0000', - 'total_deposit_coin' => WithdrawFlow::amountString($user->total_deposit_coin ?? '0'), - 'total_withdraw_coin' => WithdrawFlow::amountString($user->total_withdraw_coin ?? '0'), - 'bet_flow_coin' => $flow['bet_flow_coin'], - 'max_withdrawable' => $maxWithdrawable, + 'coin' => floatval($coinBalance), + 'coin_balance' => floatval($coinBalance), + 'frozen_balance' => 0.00, + 'total_deposit_coin' => floatval(WithdrawFlow::amountString($user->total_deposit_coin ?? '0')), + 'total_withdraw_coin' => floatval(WithdrawFlow::amountString($user->total_withdraw_coin ?? '0')), + 'bet_flow_coin' => floatval($flow['bet_flow_coin']), + 'max_withdrawable' => floatval($maxWithdrawable), 'withdraw_flow' => [ - 'ratio' => $flow['ratio'], - 'net_deposit' => $flow['net_deposit'], - 'required_bet_flow' => $flow['required_bet_flow'], - 'remaining_bet_flow' => $flow['remaining_bet_flow'], + 'ratio' => floatval($flow['ratio']), + 'net_deposit' => floatval($flow['net_deposit']), + 'required_bet_flow' => floatval($flow['required_bet_flow']), + 'remaining_bet_flow' => floatval($flow['remaining_bet_flow']), 'eligible' => $flow['eligible'], - 'max_withdraw_by_flow' => $flow['flow_unlimited'] ? null : $flow['max_withdraw_by_flow'], + 'max_withdraw_by_flow' => $flow['flow_unlimited'] ? null : floatval($flow['max_withdraw_by_flow']), 'flow_unlimited' => $flow['flow_unlimited'], 'pending_withdraw' => [ 'count' => $pendingWithdrawCount, diff --git a/app/api/controller/Auth.php b/app/api/controller/Auth.php index 84eaf05..e8aa85f 100644 --- a/app/api/controller/Auth.php +++ b/app/api/controller/Auth.php @@ -140,7 +140,7 @@ class Auth extends MobileBase 'user' => [ 'username' => $userInfo['username'] ?? '', 'uuid' => $userInfo['uuid'] ?? '', - 'coin' => $userInfo['coin'] ?? '0.0000', + 'coin' => $userInfo['coin'] ?? '0.00', 'channel_id' => $userInfo['channel_id'] ?? null, 'risk_flags' => $userInfo['risk_flags'] ?? 0, ], diff --git a/app/api/controller/Finance.php b/app/api/controller/Finance.php index a673be2..c800e32 100644 --- a/app/api/controller/Finance.php +++ b/app/api/controller/Finance.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace app\api\controller; +use app\common\library\finance\DepositMockGateway; use app\common\library\finance\DepositSettlement; use app\common\library\finance\WithdrawFlow; use app\common\library\game\DepositChannel as DepositChannelLib; @@ -12,13 +13,26 @@ use app\common\library\game\FinanceCashierConfig as FinanceCashierConfigLib; use app\common\model\DepositOrder; use app\common\model\GameConfig; use app\common\model\WithdrawOrder; +use app\common\service\DepositOrderExpireService; +use app\common\service\UserPushService; use support\Response; use support\think\Db; use Throwable; use Webman\Http\Request; +use function response; class Finance extends MobileBase { + /** + * 模拟第三方收银台页与支付回调,无需 user-token,仅 HMAC 防篡改。 + */ + protected array $noNeedLogin = ['depositMockPayPage', 'depositMockNotify']; + + /** + * 允许浏览器直接打开 pay_url 而不带 auth-token。 + */ + protected array $noNeedAuthToken = ['depositMockPayPage', 'depositMockNotify']; + /** * 充值档位列表(仅启用档位,按 sort 升序) */ @@ -36,7 +50,7 @@ class Finance extends MobileBase foreach ($tiers as $tier) { $amount = $this->amountString($tier['amount'] ?? '0'); $bonus = $this->amountString($tier['bonus_amount'] ?? '0'); - $total = bcadd($amount, $bonus, 4); + $total = bcadd($amount, $bonus, 2); $payAmount = $this->amountString($tier['pay_amount'] ?? '0'); $currency = isset($tier['currency']) && is_string($tier['currency']) ? strtoupper(trim($tier['currency'])) : 'CNY'; if ($currency === '') { @@ -49,10 +63,10 @@ class Finance extends MobileBase 'tier_key' => $tierId, 'title' => $localized['title'], 'currency' => $currency, - 'pay_amount' => $payAmount, - 'amount' => $amount, - 'bonus_amount' => $bonus, - 'total_amount' => $total, + 'pay_amount' => $this->amountNumber($payAmount), + 'amount' => $this->amountNumber($amount), + 'bonus_amount' => $this->amountNumber($bonus), + 'total_amount' => $this->amountNumber($total), 'desc' => $localized['desc'], 'channels' => DepositChannelLib::channelsForTier($tierId, $effectiveChannels, $lang), ]; @@ -77,15 +91,18 @@ class Finance extends MobileBase /** * 创建充值订单 * - * 当前为 mock 支付网关,点击即成功:服务端直接在同一请求内完成订单入账。 - * 未来接入真实第三方支付时,仅需把 "立即结算" 替换为 "返回 pay_url 进入网关", - * 并把入账动作放到网关回调里完成(回调中调用 DepositSettlement::settle)。 + * 当前为 mock 支付网关:本接口仅创建待支付订单并返回 pay_url。 + * 未来接入真实第三方支付时,仅需替换 pay_url 生成与回调验签,入账仍在回调中调用 DepositSettlement::settle。 * * 请求:application/json 或 x-www-form-urlencoded * - tier_id / tier_key: 必填,档位唯一标识(与 depositTierList 中 id、tier_key 一致) * - channel_code: 必填,支付渠道代码(与 depositTierList 各档位 channels[].code 一致) * - idempotency_key: 必填,客户端幂等键,短时间内重复提交只生成一次订单 * + * 流程:仅创建 `status=0` 的待支付订单,返回 `pay_url`(含签名的模拟「第三方收银台」页);玩家打开后点确认, + * 由服务端 `depositMockNotify` 模拟网关联调完成入账。未来接入真实三方时,将「打开 pay_url + 等回调」替换为 + * 真网关,入账仍走 `DepositSettlement::settle`。 + * * 响应(统一结构,未来接入第三方也保持此形状): * - order_no / amount / pay_channel / paid / pay_url / status / create_time / pay_time */ @@ -119,20 +136,33 @@ class Finance extends MobileBase return $this->mobileError(2004, 'Pay channel not available'); } - // 幂等命中:直接返回已有订单 + $user = $this->auth->getUser(); + $userId = intval(strval($user->id)); + // 先做超时清理,再做幂等命中与“最多三笔待支付”限制 + DepositOrderExpireService::expirePendingOrders($userId, null); + + // 幂等命中:直接返回已有订单(允许客户端重试拿回同一 pay_url) try { $existing = DepositOrder::where('idempotency_key', $idempotencyKey)->find(); if ($existing) { if (intval($existing->user_id) !== intval($this->auth->id)) { return $this->mobileError(1002, 'Idempotency key conflict'); } - return $this->mobileSuccess($this->buildDepositResponse($existing)); + return $this->mobileSuccess($this->buildDepositResponse($existing, $this->publicOriginFromRequest($request))); } } catch (Throwable $e) { // 忽略幂等查询失败,继续创建 } - $user = $this->auth->getUser(); + $pendingCount = DepositOrderExpireService::pendingCountByUserId($userId); + if ($pendingCount >= DepositOrderExpireService::MAX_PENDING_DEPOSIT) { + return $this->mobileError(2005, 'Too many pending deposit orders', [ + 'max_pending' => DepositOrderExpireService::MAX_PENDING_DEPOSIT, + 'pending_count' => $pendingCount, + 'expire_seconds' => DepositOrderExpireService::EXPIRE_SECONDS, + ]); + } + $orderNo = 'DP' . date('YmdHis') . substr(str_replace('.', '', uniqid('', true)), -6); $curSnap = isset($tier['currency']) && is_string($tier['currency']) ? strtoupper(trim($tier['currency'])) : 'CNY'; if ($curSnap === '') { @@ -157,7 +187,6 @@ class Finance extends MobileBase $channelId = intval(strval($user->channel_id)); } - $orderId = 0; try { $order = DepositOrder::create([ 'order_no' => $orderNo, @@ -169,70 +198,207 @@ class Finance extends MobileBase 'status' => 0, 'pay_channel' => $channelCode, 'deposit_tier_id' => $tier['id'], - 'proof_image' => '', 'pay_account_snapshot' => json_encode($tierSnapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'remark' => '', 'create_time' => $now, 'update_time' => $now, ]); - $orderId = intval($order->id); } catch (Throwable $e) { $msg = $e->getMessage(); if (stripos($msg, 'Duplicate') !== false && stripos($msg, 'uk_deposit_order_idem') !== false) { $existing = DepositOrder::where('idempotency_key', $idempotencyKey)->find(); if ($existing) { - return $this->mobileSuccess($this->buildDepositResponse($existing)); + return $this->mobileSuccess($this->buildDepositResponse($existing, $this->publicOriginFromRequest($request))); } } return $this->mobileError(2000, $msg); } - // Mock 网关:立即结算,入账到钱包 - try { - DepositSettlement::settle( - $orderId, - DepositSettlement::SOURCE_MOCK_GATEWAY, - 'mock gateway auto settled', - null, - 'channel_code=' . $channelCode - ); - } catch (Throwable $e) { - return $this->mobileError(2000, $e->getMessage()); - } - - $settled = DepositOrder::where('id', $orderId)->find(); - if (!$settled) { - return $this->mobileError(2000, 'Order not found after settle'); - } - return $this->mobileSuccess($this->buildDepositResponse($settled)); + // 仅落待支付单;真实入账在模拟网关联调 depositMockNotify 中完成 + return $this->mobileSuccess($this->buildDepositResponse($order, $this->publicOriginFromRequest($request))); } /** * 将订单模型转换为统一的创建/详情响应数据 + * + * @param string|null $publicOrigin 如 https://api.xxx.com,待支付时用于拼完整 pay_url;为 null 时仅返回以 / 开头的 path+query */ - private function buildDepositResponse($order): array + private function buildDepositResponse($order, ?string $publicOrigin = null): array { $status = $this->mapDepositStatus($order->status); $paid = $status === 'paid'; $amount = $this->amountString($order->amount); $bonus = $this->amountString($order->bonus_amount); - $total = bcadd($amount, $bonus, 4); + $total = bcadd($amount, $bonus, 2); + $on = is_string($order->order_no) ? $order->order_no : strval($order->order_no); + $payUrl = ''; + if ($this->intValue($order->status) === 0 && $on !== '') { + $payUrl = DepositMockGateway::payPageUrl($on, $publicOrigin); + } return [ - 'order_no' => is_string($order->order_no) ? $order->order_no : strval($order->order_no), - 'amount' => $amount, - 'bonus_amount' => $bonus, - 'total_amount' => $total, + 'order_no' => $on, + 'amount' => $this->amountNumber($amount), + 'bonus_amount' => $this->amountNumber($bonus), + 'total_amount' => $this->amountNumber($total), 'status' => $status, 'paid' => $paid, 'pay_channel' => is_string($order->pay_channel) ? $order->pay_channel : strval($order->pay_channel), - 'pay_url' => '', + 'pay_url' => $payUrl, 'create_time' => is_numeric(strval($order->create_time)) ? intval(strval($order->create_time)) : 0, 'pay_time' => is_numeric(strval($order->pay_time)) ? intval(strval($order->pay_time)) : 0, ]; } /** - * 将任意金额输入归一化为 4 位小数字符串(不做类型强制转换) + * 根据请求拼出公网 origin,用于给客户端直接可用的完整 pay_url。 + */ + private function publicOriginFromRequest(Request $request): string + { + $proto = strtolower((string) $request->header('x-forwarded-proto', '')); + $https = $proto === 'https' || strtolower((string) $request->header('x-forwarded-ssl', '')) === 'on'; + $scheme = $https ? 'https' : 'http'; + $host = trim((string) $request->header('host', '')); + if ($host === '') { + $host = trim((string) ($request->header('x-forwarded-host', ''))); + } + if ($host === '') { + $host = '127.0.0.1:8787'; + } + + return $scheme . '://' . $host; + } + + /** + * 模拟第三方支付收银台(HTML)。玩家浏览器打开,点击按钮即向 depositMockNotify 发起回调。 + */ + public function depositMockPayPage(Request $request): Response + { + $response = $this->initializeMobile($request); + if ($response !== null) { + return $response; + } + $orderNo = $this->stringParam($request->input('order_no')); + $sign = $this->stringParam($request->input('sign')); + if ($orderNo === '' || $sign === '' || !DepositMockGateway::verifyOrderNo($orderNo, $sign)) { + return response('Invalid or expired payment link', 403, [ + 'Content-Type' => 'text/plain; charset=utf-8', + ]); + } + $order = DepositOrder::where('order_no', $orderNo)->find(); + if (!$order) { + return response('Order not found', 404, [ + 'Content-Type' => 'text/plain; charset=utf-8', + ]); + } + DepositOrderExpireService::expirePendingOrders(null, $orderNo); + $order = DepositOrder::where('order_no', $orderNo)->find(); + if (!$order) { + return response('Order not found', 404, [ + 'Content-Type' => 'text/plain; charset=utf-8', + ]); + } + if ($this->intValue($order->status) !== 0) { + $st = $this->mapDepositStatus($order->status); + $msg = 'Order status: ' . $st; + if ($st === 'paid') { + $msg = 'This order is already paid. You can return to the app.'; + } + $msgEsc = htmlspecialchars($msg, ENT_QUOTES, 'UTF-8'); + + return response('充值

' . $msgEsc . '

', 200, [ + 'Content-Type' => 'text/html; charset=utf-8', + ]); + } + $amount = $this->amountString($order->amount); + $bonus = $this->amountString($order->bonus_amount); + $noEsc = htmlspecialchars($orderNo, ENT_QUOTES, 'UTF-8'); + $signEsc = htmlspecialchars($sign, ENT_QUOTES, 'UTF-8'); + $payChannel = is_string($order->pay_channel) ? htmlspecialchars($order->pay_channel, ENT_QUOTES, 'UTF-8') : ''; + $html = '模拟支付'; + $html .= '

模拟第三方收银台

'; + $html .= '

订单号:' . $noEsc . '

'; + $html .= '

支付渠道:' . $payChannel . '

'; + $html .= '

金额(法币/标价):' . htmlspecialchars($amount, ENT_QUOTES, 'UTF-8') . ' + 赠送 ' . htmlspecialchars($bonus, ENT_QUOTES, 'UTF-8') . '(币)

'; + $html .= '

点击下方按钮即视为第三方支付成功,服务端会回调并到账。

'; + $html .= '
'; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= '
'; + + return response($html, 200, [ + 'Content-Type' => 'text/html; charset=utf-8', + ]); + } + + /** + * 模拟第三方异步通知:验签后调用 DepositSettlement::settle 入账,并推送 wallet.changed。 + */ + public function depositMockNotify(Request $request): Response + { + $response = $this->initializeMobile($request); + if ($response !== null) { + return $response; + } + $orderNo = $this->stringParam($request->input('order_no')); + $sign = $this->stringParam($request->input('sign')); + if ($orderNo === '' || $sign === '') { + return $this->mobileError(1001, 'Missing parameters'); + } + if (!DepositMockGateway::verifyOrderNo($orderNo, $sign)) { + return $this->mobileError(1003, 'Invalid parameter value'); + } + $order = DepositOrder::where('order_no', $orderNo)->find(); + if (!$order) { + return $this->mobileError(2003, 'Order does not exist'); + } + DepositOrderExpireService::expirePendingOrders(null, $orderNo); + $order = DepositOrder::where('order_no', $orderNo)->find(); + if (!$order) { + return $this->mobileError(2003, 'Order does not exist'); + } + if ($this->intValue($order->status) !== 0) { + return $this->mobileSuccess($this->buildDepositResponse($order, null)); + } + $orderId = intval(strval($order->id)); + if ($orderId <= 0) { + return $this->mobileError(2000, 'Order id invalid'); + } + $pc = is_string($order->pay_channel) ? $order->pay_channel : strval($order->pay_channel); + try { + $result = DepositSettlement::settle( + $orderId, + DepositSettlement::SOURCE_THIRD_PARTY, + 'mock third party notify', + null, + 'channel_code=' . $pc + ); + $uid = intval(strval($order->user_id)); + if ($uid > 0) { + $coinAfter = is_string($result['balance_after'] ?? null) ? $result['balance_after'] : strval($result['balance_after'] ?? '0'); + $credit = is_string($result['credit'] ?? null) ? $result['credit'] : strval($result['credit'] ?? '0'); + UserPushService::publish($uid, UserPushService::EVT_WALLET_CHANGED, [ + 'reason' => 'deposit', + 'ref_type' => 'deposit_order', + 'ref_id' => (string) $orderId, + 'order_no' => $orderNo, + 'delta' => $credit, + 'balance_after' => $coinAfter, + ]); + } + } catch (Throwable $e) { + return $this->mobileError(2000, $e->getMessage()); + } + $fresh = DepositOrder::where('order_no', $orderNo)->find(); + if (!$fresh) { + return $this->mobileError(2000, 'Order not found after settle'); + } + + return $this->mobileSuccess($this->buildDepositResponse($fresh, null)); + } + + /** + * 将任意金额输入归一化为 2 位小数字符串(不做类型强制转换) */ private function amountString($raw): string { @@ -241,12 +407,17 @@ class Finance extends MobileBase } elseif (is_int($raw) || is_float($raw)) { $s = strval($raw); } else { - return '0.0000'; + return '0.00'; } if ($s === '' || !is_numeric($s)) { - return '0.0000'; + return '0.00'; } - return bcadd($s, '0', 4); + return bcadd($s, '0', 2); + } + + private function amountNumber($raw): float + { + return floatval($this->amountString($raw)); } /** @@ -262,11 +433,12 @@ class Finance extends MobileBase if ($orderNo === '') { return $this->mobileError(1001, 'Missing parameters'); } + DepositOrderExpireService::expirePendingOrders(null, $orderNo); $order = DepositOrder::where('order_no', $orderNo)->where('user_id', $this->auth->id)->find(); if (!$order) { return $this->mobileError(2003, 'Order does not exist'); } - return $this->mobileSuccess($this->buildDepositResponse($order)); + return $this->mobileSuccess($this->buildDepositResponse($order, $this->publicOriginFromRequest($request))); } /** @@ -279,6 +451,7 @@ class Finance extends MobileBase if ($response !== null) { return $response; } + DepositOrderExpireService::expirePendingOrders(intval(strval($this->auth->id)), null); $page = $this->intValue($request->input('page', 1)); if ($page <= 0) { $page = 1; @@ -295,8 +468,8 @@ class Finance extends MobileBase foreach ($paginate->items() as $row) { $list[] = [ 'order_no' => $row->order_no, - 'amount' => $this->amountString($row->amount ?? '0'), - 'bonus_amount' => $this->amountString($row->bonus_amount ?? '0'), + 'amount' => $this->amountNumber($row->amount ?? '0'), + 'bonus_amount' => $this->amountNumber($row->bonus_amount ?? '0'), 'status' => $this->mapDepositStatus($row->status ?? null), ]; } @@ -324,14 +497,34 @@ class Finance extends MobileBase if ($withdrawCoin === '' || $receiveAccount === '' || $receiveType === '' || $idempotencyKey === '') { return $this->mobileError(1001, 'Missing parameters'); } - if (!is_numeric($withdrawCoin) || bccomp($withdrawCoin, '0', 4) <= 0) { + if (mb_strlen($idempotencyKey) > 64) { + return $this->mobileError(1002, 'Idempotency key is too long'); + } + if (!is_numeric($withdrawCoin) || bccomp($withdrawCoin, '0', 2) <= 0) { return $this->mobileError(1001, 'Invalid withdraw amount'); } - $withdrawCoin = bcadd($withdrawCoin, '0', 4); + $withdrawCoin = bcadd($withdrawCoin, '0', 2); $user = $this->auth->getUser(); $userId = intval(strval($user->id)); + // 幂等:相同 idempotency_key 重试直接返回已创建订单 + $idemOrder = Db::name('withdraw_order')->where('idempotency_key', $idempotencyKey)->find(); + if ($idemOrder) { + $idemUserId = is_numeric(strval($idemOrder['user_id'] ?? null)) ? intval(strval($idemOrder['user_id'])) : 0; + if ($idemUserId !== $userId) { + return $this->mobileError(1002, 'Idempotency key conflict'); + } + $idemStatus = $this->intValue($idemOrder['status'] ?? 0); + return $this->mobileSuccess([ + 'order_no' => is_string($idemOrder['order_no'] ?? null) ? $idemOrder['order_no'] : strval($idemOrder['order_no'] ?? ''), + 'status' => $this->mapWithdrawStatus($idemStatus), + 'fee_coin' => $this->amountNumber($idemOrder['fee'] ?? '0'), + 'actual_arrival_coin' => $this->amountNumber($idemOrder['actual_amount'] ?? '0'), + 'risk_review_required' => $idemStatus === 0, + ]); + } + // 待审核订单数限制:同一用户最多 MAX_PENDING_WITHDRAW 笔 status=0(待审核) $pendingCount = Db::name('withdraw_order') ->where('user_id', $userId) @@ -344,8 +537,8 @@ class Finance extends MobileBase ]); } - $balanceBefore = bcadd(strval($user->coin ?? '0'), '0', 4); - if (bccomp($balanceBefore, $withdrawCoin, 4) < 0) { + $balanceBefore = bcadd(strval($user->coin ?? '0'), '0', 2); + if (bccomp($balanceBefore, $withdrawCoin, 2) < 0) { return $this->mobileError(2001, 'Insufficient balance'); } @@ -359,14 +552,14 @@ class Finance extends MobileBase 'bet_flow_coin' => $user->bet_flow_coin ?? '0', ]); $maxWithdrawable = WithdrawFlow::maxWithdrawable($balanceBefore, $flowStatus); - if (bccomp($withdrawCoin, $maxWithdrawable, 4) > 0) { + if (bccomp($withdrawCoin, $maxWithdrawable, 2) > 0) { return $this->mobileError(2002, 'Withdraw exceeds available bet flow', [ - 'max_withdrawable' => $maxWithdrawable, - 'coin_balance' => $balanceBefore, - 'bet_flow_coin' => $flowStatus['bet_flow_coin'], - 'total_withdraw_coin' => WithdrawFlow::amountString($user->total_withdraw_coin ?? '0'), - 'ratio' => $flowStatus['ratio'], - 'max_withdraw_by_flow' => $flowStatus['flow_unlimited'] ? null : $flowStatus['max_withdraw_by_flow'], + 'max_withdrawable' => $this->amountNumber($maxWithdrawable), + 'coin_balance' => $this->amountNumber($balanceBefore), + 'bet_flow_coin' => $this->amountNumber($flowStatus['bet_flow_coin']), + 'total_withdraw_coin' => $this->amountNumber(WithdrawFlow::amountString($user->total_withdraw_coin ?? '0')), + 'ratio' => floatval($flowStatus['ratio']), + 'max_withdraw_by_flow' => $flowStatus['flow_unlimited'] ? null : $this->amountNumber($flowStatus['max_withdraw_by_flow']), ]); } @@ -376,9 +569,9 @@ class Finance extends MobileBase : null; $orderNo = 'WD' . date('YmdHis') . substr(str_replace('.', '', uniqid('', true)), -6); - $feeCoin = bcmul($withdrawCoin, '0.005', 4); - $actualArrivalCoin = bcsub($withdrawCoin, $feeCoin, 4); - $balanceAfter = bcsub($balanceBefore, $withdrawCoin, 4); + $feeCoin = bcmul($withdrawCoin, '0.005', 2); + $actualArrivalCoin = bcsub($withdrawCoin, $feeCoin, 2); + $balanceAfter = bcsub($balanceBefore, $withdrawCoin, 2); $now = time(); Db::startTrans(); @@ -399,11 +592,14 @@ class Finance extends MobileBase $orderId = Db::name('withdraw_order')->insertGetId([ 'order_no' => $orderNo, + 'idempotency_key' => $idempotencyKey, 'user_id' => $userId, 'channel_id' => $channelId, 'amount' => $withdrawCoin, 'fee' => $feeCoin, 'actual_amount' => $actualArrivalCoin, + 'receive_type' => $receiveType, + 'receive_account' => $receiveAccount, 'status' => 0, 'review_admin_id' => null, 'review_time' => null, @@ -436,8 +632,8 @@ class Finance extends MobileBase return $this->mobileSuccess([ 'order_no' => $orderNo, 'status' => 'pending_review', - 'fee_coin' => $feeCoin, - 'actual_arrival_coin' => $actualArrivalCoin, + 'fee_coin' => $this->amountNumber($feeCoin), + 'actual_arrival_coin' => $this->amountNumber($actualArrivalCoin), 'risk_review_required' => true, ]); } @@ -465,9 +661,11 @@ class Finance extends MobileBase return $this->mobileSuccess([ 'order_no' => $order->order_no, 'status' => $this->mapWithdrawStatus($statusCode), - 'withdraw_coin' => $order->amount, - 'fee_coin' => $order->fee, - 'actual_arrival_coin' => $order->actual_amount, + 'withdraw_coin' => $this->amountNumber($order->amount ?? '0'), + 'fee_coin' => $this->amountNumber($order->fee ?? '0'), + 'actual_arrival_coin' => $this->amountNumber($order->actual_amount ?? '0'), + 'receive_type' => is_string($order->receive_type ?? null) ? $order->receive_type : strval($order->receive_type ?? ''), + 'receive_account' => is_string($order->receive_account ?? null) ? $order->receive_account : strval($order->receive_account ?? ''), 'reject_reason' => $statusCode === 2 && $remark !== '' ? $remark : null, 'create_time' => $order->create_time, 'review_time' => $order->review_time, @@ -500,7 +698,7 @@ class Finance extends MobileBase foreach ($paginate->items() as $row) { $list[] = [ 'order_no' => $row->order_no, - 'amount' => $this->amountString($row->amount ?? '0'), + 'amount' => $this->amountNumber($row->amount ?? '0'), 'status' => $this->mapWithdrawStatus($row->status ?? null), ]; } @@ -518,6 +716,19 @@ class Finance extends MobileBase * 收银台配置:货币列表(含充值/提现汇率)、支付渠道(pay_channels)、提现银行与文案(供充值/提现页展示) */ public function cashierConfig(Request $request): Response + { + return $this->buildDepositWithdrawConfig($request); + } + + /** + * 充值/提现配置(推荐新接口,兼容 cashierConfig 相同返回结构) + */ + public function depositWithdrawConfig(Request $request): Response + { + return $this->buildDepositWithdrawConfig($request); + } + + private function buildDepositWithdrawConfig(Request $request): Response { $response = $this->initializeMobile($request); if ($response !== null) { diff --git a/app/api/controller/Game.php b/app/api/controller/Game.php index c7b9e48..fa6f9fc 100644 --- a/app/api/controller/Game.php +++ b/app/api/controller/Game.php @@ -59,8 +59,8 @@ class Game extends MobileBase ], 'bet_config' => [ 'pick_max_number_count' => $this->getPickMaxNumberCount(), - 'chips' => ['1.0000', '5.0000', '10.0000', '25.0000', '50.0000', '100.0000'], - 'single_number_max_bet' => $this->getConfigValue('single_number_max_bet', '500.0000'), + 'chips' => ['1.00', '5.00', '10.00', '25.00', '50.00', '100.00'], + 'single_number_max_bet' => $this->getConfigValue('single_number_max_bet', '500.00'), ], 'dictionary' => $items, 'user_snapshot' => [ @@ -157,10 +157,10 @@ class Game extends MobileBase if ($periodNo === '' || $betAmount === '' || $idempotencyKey === '') { return $this->mobileError(1001, 'Missing parameters'); } - if (!is_numeric($betAmount) || bccomp($betAmount, '0', 4) <= 0) { + if (!is_numeric($betAmount) || bccomp($betAmount, '0', 2) <= 0) { return $this->mobileError(1003, 'Invalid parameter value'); } - $totalAmount = bcadd($betAmount, '0', 4); + $totalAmount = bcadd($betAmount, '0', 2); $numbers = $this->parseBetNumbersFromRequest($numbersRaw); if ($numbers === []) { @@ -189,7 +189,7 @@ class Game extends MobileBase } $user = $this->auth->getUser(); - if (bccomp((string) $user->coin, $totalAmount, 4) < 0) { + if (bccomp((string) $user->coin, $totalAmount, 2) < 0) { return $this->mobileError(2001, 'Insufficient balance'); } @@ -209,7 +209,7 @@ class Game extends MobileBase return $this->mobileError(5000, 'System is busy, please try again later'); } $before = (string) ($coinRow['coin'] ?? '0'); - if (bccomp($before, $totalAmount, 4) < 0) { + if (bccomp($before, $totalAmount, 2) < 0) { return $this->mobileError(2001, 'Insufficient balance'); } @@ -225,7 +225,7 @@ class Game extends MobileBase } $now = time(); - $after = bcsub($before, $totalAmount, 4); + $after = bcsub($before, $totalAmount, 2); $orderNo = 'BO' . date('YmdHis') . substr(str_replace('.', '', uniqid('', true)), -6); $streakAtBet = (int) ($coinRow['current_streak'] ?? 0); @@ -286,7 +286,7 @@ class Game extends MobileBase 'order_no' => $orderNo, 'period_no' => $period->period_no, 'status' => 'accepted', - 'locked_balance' => '0.0000', + 'locked_balance' => '0.00', 'balance_after' => $after, 'current_streak' => $streakAtBet, ]); diff --git a/app/api/controller/Wallet.php b/app/api/controller/Wallet.php index a99fd74..1db1513 100644 --- a/app/api/controller/Wallet.php +++ b/app/api/controller/Wallet.php @@ -26,20 +26,20 @@ class Wallet extends MobileBase ]); $maxWithdrawable = WithdrawFlow::maxWithdrawable($coinBalance, $flow); return $this->mobileSuccess([ - 'coin_balance' => $coinBalance, - 'frozen_balance' => '0.0000', - 'withdrawable_balance' => $coinBalance, - 'max_withdrawable' => $maxWithdrawable, - 'total_deposit_coin' => WithdrawFlow::amountString($user->total_deposit_coin ?? '0'), - 'total_withdraw_coin' => WithdrawFlow::amountString($user->total_withdraw_coin ?? '0'), - 'bet_flow_coin' => $flow['bet_flow_coin'], + 'coin_balance' => floatval($coinBalance), + 'frozen_balance' => 0.00, + 'withdrawable_balance' => floatval($coinBalance), + 'max_withdrawable' => floatval($maxWithdrawable), + 'total_deposit_coin' => floatval(WithdrawFlow::amountString($user->total_deposit_coin ?? '0')), + 'total_withdraw_coin' => floatval(WithdrawFlow::amountString($user->total_withdraw_coin ?? '0')), + 'bet_flow_coin' => floatval($flow['bet_flow_coin']), 'withdraw_flow' => [ - 'ratio' => $flow['ratio'], - 'net_deposit' => $flow['net_deposit'], - 'required_bet_flow' => $flow['required_bet_flow'], - 'remaining_bet_flow' => $flow['remaining_bet_flow'], + 'ratio' => floatval($flow['ratio']), + 'net_deposit' => floatval($flow['net_deposit']), + 'required_bet_flow' => floatval($flow['required_bet_flow']), + 'remaining_bet_flow' => floatval($flow['remaining_bet_flow']), 'eligible' => $flow['eligible'], - 'max_withdraw_by_flow' => $flow['flow_unlimited'] ? null : $flow['max_withdraw_by_flow'], + 'max_withdraw_by_flow' => $flow['flow_unlimited'] ? null : floatval($flow['max_withdraw_by_flow']), 'flow_unlimited' => $flow['flow_unlimited'], ], ]); diff --git a/app/api/lang/en.php b/app/api/lang/en.php index 807bc33..ebc828c 100644 --- a/app/api/lang/en.php +++ b/app/api/lang/en.php @@ -45,6 +45,7 @@ return [ 'Order not found after settle' => 'Order not found after settlement', 'Invalid withdraw amount' => 'Invalid withdraw amount', 'Withdraw exceeds available bet flow' => 'The withdraw amount exceeds the available bet-flow quota', + 'Too many pending deposit orders' => 'You already have multiple pending deposit orders, please complete payment first or wait for timeout', 'Too many pending withdraw orders' => 'You already have withdraw orders under review, please wait for them to be processed', // Member center account 'Data updated successfully~' => 'Data updated successfully~', diff --git a/app/api/lang/zh-cn.php b/app/api/lang/zh-cn.php index 4775563..551ede0 100644 --- a/app/api/lang/zh-cn.php +++ b/app/api/lang/zh-cn.php @@ -77,6 +77,7 @@ return [ 'Order not found after settle' => '充值成功后未找到订单', 'Invalid withdraw amount' => '提现金额不合法', 'Withdraw exceeds available bet flow' => '提现金额超出可提现额度', + 'Too many pending deposit orders' => '存在多笔待支付充值订单,请先完成支付或等待超时', 'Too many pending withdraw orders' => '用户当前存在多笔提现订单,请等待审核', // 会员中心 account 'Data updated successfully~' => '资料更新成功~', diff --git a/app/common/library/finance/DepositMockGateway.php b/app/common/library/finance/DepositMockGateway.php new file mode 100644 index 0000000..53ac3da --- /dev/null +++ b/app/common/library/finance/DepositMockGateway.php @@ -0,0 +1,71 @@ + $orderNo, + 'sign' => $sign, + ]); + $path = '/api/finance/depositMockPayPage?' . $q; + if ($publicOrigin === null) { + return $path; + } + $base = rtrim($publicOrigin, '/'); + + return $base . $path; + } +} diff --git a/app/common/library/finance/DepositSettlement.php b/app/common/library/finance/DepositSettlement.php index e0ee5f7..99f0d20 100644 --- a/app/common/library/finance/DepositSettlement.php +++ b/app/common/library/finance/DepositSettlement.php @@ -75,7 +75,7 @@ final class DepositSettlement // 如果已结算,直接返回已有结果(幂等) if ($status === 1) { $userId = is_numeric($order['user_id'] ?? null) ? intval($order['user_id']) : 0; - $coinAfter = '0.0000'; + $coinAfter = '0.00'; if ($userId > 0) { $coin = Db::name('user')->where('id', $userId)->value('coin'); $coinAfter = is_string($coin) ? $coin : strval($coin); @@ -87,7 +87,7 @@ final class DepositSettlement 'order_no' => $orderNo, 'amount' => $amt, 'bonus_amount' => $bns, - 'credit' => bcadd($amt, $bns, 4), + 'credit' => bcadd($amt, $bns, 2), 'balance_before' => $coinAfter, 'balance_after' => $coinAfter, 'pay_time' => is_numeric($order['pay_time'] ?? null) ? intval($order['pay_time']) : 0, @@ -100,14 +100,14 @@ final class DepositSettlement } $amount = self::amountString($order['amount'] ?? '0'); - if (bccomp($amount, '0', 4) <= 0) { + if (bccomp($amount, '0', 2) <= 0) { throw new RuntimeException('订单金额异常'); } $bonus = self::amountString($order['bonus_amount'] ?? '0'); - if (bccomp($bonus, '0', 4) < 0) { - $bonus = '0.0000'; + if (bccomp($bonus, '0', 2) < 0) { + $bonus = '0.00'; } - $credit = bcadd($amount, $bonus, 4); + $credit = bcadd($amount, $bonus, 2); $userId = is_numeric($order['user_id'] ?? null) ? intval($order['user_id']) : 0; if ($userId <= 0) { @@ -121,7 +121,7 @@ final class DepositSettlement $channelId = is_numeric($order['channel_id'] ?? null) ? intval($order['channel_id']) : null; $balanceBefore = self::amountString($user['coin'] ?? '0'); - $balanceAfter = bcadd($balanceBefore, $credit, 4); + $balanceAfter = bcadd($balanceBefore, $credit, 2); $now = time(); $baseRemark = is_string($order['remark'] ?? null) ? $order['remark'] : ''; @@ -142,12 +142,10 @@ final class DepositSettlement ->where('id', $orderId) ->where('status', 0) ->update([ - 'status' => 1, - 'pay_time' => $now, - 'review_admin_id' => $operatorAdminId, - 'review_time' => $operatorAdminId !== null ? $now : null, - 'remark' => $finalRemark, - 'update_time' => $now, + 'status' => 1, + 'pay_time' => $now, + 'remark' => $finalRemark, + 'update_time' => $now, ]); if ($affected <= 0) { throw new RuntimeException('订单状态已变更,请刷新后重试'); @@ -199,7 +197,7 @@ final class DepositSettlement } /** - * 将任意数值输入格式化为 4 位小数字符串(不做强制类型转换) + * 将任意数值输入格式化为 2 位小数字符串(不做强制类型转换) */ private static function amountString($raw): string { @@ -208,11 +206,11 @@ final class DepositSettlement } elseif (is_int($raw) || is_float($raw)) { $s = strval($raw); } else { - return '0.0000'; + return '0.00'; } if (!is_numeric($s)) { - return '0.0000'; + return '0.00'; } - return bcadd($s, '0', 4); + return bcadd($s, '0', 2); } } diff --git a/app/common/library/finance/WithdrawFlow.php b/app/common/library/finance/WithdrawFlow.php index 469bbdb..5c100dd 100644 --- a/app/common/library/finance/WithdrawFlow.php +++ b/app/common/library/finance/WithdrawFlow.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace app\common\library\finance; use app\common\service\GameHotDataRedis; +use support\think\Db; /** * 提现打码量(流水)门槛工具库 @@ -25,16 +26,16 @@ final class WithdrawFlow { public const CONFIG_KEY = 'withdraw_bet_flow_ratio'; - public const DEFAULT_RATIO = '1.0000'; + public const DEFAULT_RATIO = '1.00'; /** 当 ratio = 0(不限打码)时,max_withdraw_by_flow 用此哨兵表示"无限"。14 位整数位足够覆盖任何业务金额。 */ - public const UNLIMITED_FLOW = '99999999999999.9999'; + public const UNLIMITED_FLOW = '99999999999999.99'; /** 单用户最多允许同时存在的「待审核」(withdraw_order.status=0) 提现订单数。 */ public const MAX_PENDING_WITHDRAW = 3; /** - * 读取当前打码倍数(字符串 4 位小数,至少 0) + * 读取当前打码倍数(字符串 2 位小数,至少 0) */ public static function ratio(): string { @@ -46,32 +47,32 @@ final class WithdrawFlow 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'; + $normalized = bcadd(trim($val), '0', 2); + if (bccomp($normalized, '0', 2) < 0) { + return '0.00'; } return $normalized; } /** - * 归一化金额字段到 4 位小数字符串,非法输入返回 '0.0000' + * 归一化金额字段到 2 位小数字符串,非法输入返回 '0.00' */ public static function amountString($raw): string { if ($raw === null || $raw === '') { - return '0.0000'; + return '0.00'; } if (is_string($raw)) { $s = trim($raw); } elseif (is_int($raw) || is_float($raw)) { $s = strval($raw); } else { - return '0.0000'; + return '0.00'; } if (!is_numeric($s)) { - return '0.0000'; + return '0.00'; } - return bcadd($s, '0', 4); + return bcadd($s, '0', 2); } /** @@ -108,28 +109,28 @@ final class WithdrawFlow $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'; + $net = bcsub($deposit, $withdraw, 2); + if (bccomp($net, '0', 2) < 0) { + $net = '0.00'; } $ratio = self::ratio(); - $required = bcmul($net, $ratio, 4); - $remaining = bcsub($required, $flow, 4); - if (bccomp($remaining, '0', 4) < 0) { - $remaining = '0.0000'; + $required = bcmul($net, $ratio, 2); + $remaining = bcsub($required, $flow, 2); + if (bccomp($remaining, '0', 2) < 0) { + $remaining = '0.00'; } - $eligible = bccomp($flow, $required, 4) >= 0; + $eligible = bccomp($flow, $required, 2) >= 0; // max_withdraw_by_flow = max(0, bet_flow_coin / ratio - total_withdraw_coin) - $unlimited = bccomp($ratio, '0', 4) === 0; + $unlimited = bccomp($ratio, '0', 2) === 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'; + $lifetime = bcdiv($flow, $ratio, 2); + $maxByFlow = bcsub($lifetime, $withdraw, 2); + if (bccomp($maxByFlow, '0', 2) < 0) { + $maxByFlow = '0.00'; } } @@ -147,18 +148,18 @@ final class WithdrawFlow /** * 取单笔最大可提现额 = min(coin_balance, max_withdraw_by_flow)。 - * 返回值为 4 位小数字符串,已与 ratio=0(不限)逻辑兼容。 + * 返回值为 2 位小数字符串,已与 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 (bccomp($coin, '0', 2) < 0) { + $coin = '0.00'; } if (!empty($flowStatus['flow_unlimited'])) { return $coin; } $byFlow = self::amountString($flowStatus['max_withdraw_by_flow'] ?? '0'); - return bccomp($coin, $byFlow, 4) <= 0 ? $coin : $byFlow; + return bccomp($coin, $byFlow, 2) <= 0 ? $coin : $byFlow; } } diff --git a/app/common/library/game/DepositChannel.php b/app/common/library/game/DepositChannel.php index e24bc63..cd0c39e 100644 --- a/app/common/library/game/DepositChannel.php +++ b/app/common/library/game/DepositChannel.php @@ -10,9 +10,9 @@ use support\think\Db; /** * 充值支付渠道:优先读取 game_config.finance_cashier.channels;无此键时回退 game_config.deposit_channel(迁移期镜像) * - * 每项:code(须在代码/环境注册表内)、sort、status(0/1)、tier_ids(空=全部启用档位) + * 每项:code(须在代码/环境注册表内)、sort、status(0/1)。 * - * 渠道展示名以代码注册表为准;运营只配置开关、排序、可用档位。 + * 渠道展示名以代码注册表为准;运营只配置开关与排序,默认兼容全部充值档位。 */ final class DepositChannel { @@ -150,23 +150,11 @@ final class DepositChannel $sort = isset($row['sort']) && is_numeric($row['sort']) ? intval($row['sort']) : 0; $status = isset($row['status']) && is_numeric($row['status']) ? intval($row['status']) : 1; $status = $status === 1 ? 1 : 0; - $tierIds = []; - if (isset($row['tier_ids']) && is_array($row['tier_ids'])) { - foreach ($row['tier_ids'] as $tid) { - if (is_string($tid)) { - $t = trim($tid); - if ($t !== '' && preg_match('/^[a-zA-Z0-9_\-]{1,32}$/', $t)) { - $tierIds[] = $t; - } - } - } - $tierIds = array_values(array_unique($tierIds)); - } $out[] = [ 'code' => $code, 'sort' => $sort, 'status' => $status, - 'tier_ids' => $tierIds, + 'tier_ids' => [], ]; } @@ -219,17 +207,8 @@ final class DepositChannel */ public static function isTierAllowed(array $overrideRow, string $tierId): bool { - $ids = $overrideRow['tier_ids'] ?? []; - if (!is_array($ids) || $ids === []) { - return true; - } - foreach ($ids as $id) { - if (is_string($id) && $id === $tierId) { - return true; - } - } - - return false; + // 渠道不再配置档位白名单:默认兼容全部充值档位。 + return true; } /** @@ -268,9 +247,6 @@ final class DepositChannel if (!isset($registry[$code])) { continue; } - if (!self::isTierAllowed($row, $tierId)) { - continue; - } $meta = $registry[$code]; $name = self::pickLangName($meta, $lang); $sortRaw = $row['sort'] ?? 0; diff --git a/app/common/library/game/DepositTier.php b/app/common/library/game/DepositTier.php index dd29899..4adc881 100644 --- a/app/common/library/game/DepositTier.php +++ b/app/common/library/game/DepositTier.php @@ -14,9 +14,9 @@ use InvalidArgumentException; * - title : string,档位中文名称(必填,前端中文环境展示) * - title_en : string,档位英文名称(可选,前端英文环境展示;为空时回退到 title) * - currency : string,支付货币代码(3~8 位大写字母,如 MYR、CNY) - * - pay_amount : string,玩家支付的法币/支付货币额度(4 位小数) - * - amount : string,到账基础平台币(4 位小数) - * - bonus_amount : string,赠送平台币(4 位小数,可为 0) + * - pay_amount : string,玩家支付的法币/支付货币额度(2 位小数) + * - amount : string,到账基础平台币(2 位小数) + * - bonus_amount : string,赠送平台币(2 位小数,可为 0) * - desc : string,档位中文描述(可空,<=255) * - desc_en : string,档位英文描述(可空,<=255,为空时回退到 desc) * - sort : int,排序权重(小值在前) @@ -95,7 +95,7 @@ final class DepositTier $payAmount = self::normalizeAmount($row['pay_amount'] ?? ''); $amount = self::normalizeAmount($row['amount'] ?? ''); $bonus = self::normalizeAmount($row['bonus_amount'] ?? '0'); - if (bccomp($payAmount, '0', 4) <= 0 && bccomp($amount, '0', 4) > 0) { + if (bccomp($payAmount, '0', 2) <= 0 && bccomp($amount, '0', 2) > 0) { // 历史数据仅有 amount(平台币)时,用占位同步 pay_amount,运营应在后台改为真实支付额度 $payAmount = $amount; } @@ -187,17 +187,17 @@ final class DepositTier } $payAmount = self::normalizeAmount($row['pay_amount'] ?? ''); - if (bccomp($payAmount, '0', 4) <= 0) { + if (bccomp($payAmount, '0', 2) <= 0) { throw new InvalidArgumentException('第 ' . $no . ' 行支付货币额度必须大于 0'); } $amount = self::normalizeAmount($row['amount'] ?? ''); - if (bccomp($amount, '0', 4) <= 0) { + if (bccomp($amount, '0', 2) <= 0) { throw new InvalidArgumentException('第 ' . $no . ' 行基础平台币到账必须大于 0'); } $bonus = self::normalizeAmount($row['bonus_amount'] ?? '0'); - if (bccomp($bonus, '0', 4) < 0) { + if (bccomp($bonus, '0', 2) < 0) { throw new InvalidArgumentException('第 ' . $no . ' 行赠送金额不能为负数'); } @@ -333,25 +333,25 @@ final class DepositTier } /** - * 将金额归一化为 4 位小数字符串;非法输入返回 '0.0000' + * 将金额归一化为 2 位小数字符串;非法输入返回 '0.00' */ public static function normalizeAmount($raw): string { if ($raw === null || $raw === '') { - return '0.0000'; + return '0.00'; } if (is_string($raw)) { $s = trim($raw); } elseif (is_int($raw) || is_float($raw)) { $s = strval($raw); } else { - return '0.0000'; + return '0.00'; } $s = str_replace(',', '.', $s); if (!is_numeric($s)) { - return '0.0000'; + return '0.00'; } - return bcadd($s, '0', 4); + return bcadd($s, '0', 2); } /** diff --git a/app/common/library/game/FinanceCashierConfig.php b/app/common/library/game/FinanceCashierConfig.php index df0e2cc..a57c1ca 100644 --- a/app/common/library/game/FinanceCashierConfig.php +++ b/app/common/library/game/FinanceCashierConfig.php @@ -353,10 +353,10 @@ final class FinanceCashierConfig $seenCodes[$code] = true; $dep = $row['deposit_coins_per_fiat'] ?? ''; $wdr = $row['withdraw_coins_per_fiat'] ?? ''; - if (!is_string($dep) || $dep === '' || !is_numeric($dep) || bccomp($dep, '0', 8) <= 0) { + if (!is_string($dep) || $dep === '' || !is_numeric($dep) || bccomp($dep, '0', 2) <= 0) { throw new InvalidArgumentException('第 ' . ($idx + 1) . ' 行:充值汇率须为大于 0 的数字'); } - if (!is_string($wdr) || $wdr === '' || !is_numeric($wdr) || bccomp($wdr, '0', 8) <= 0) { + if (!is_string($wdr) || $wdr === '' || !is_numeric($wdr) || bccomp($wdr, '0', 2) <= 0) { throw new InvalidArgumentException('第 ' . ($idx + 1) . ' 行:提现汇率须为大于 0 的数字'); } } @@ -379,7 +379,7 @@ final class FinanceCashierConfig if (isset($p['withdraw_limits']) && is_array($p['withdraw_limits'])) { foreach (['min_ewallet', 'min_bank'] as $k) { $v = $p['withdraw_limits'][$k] ?? '0'; - if (!is_string($v) || !is_numeric($v) || bccomp($v, '0', 4) < 0) { + if (!is_string($v) || !is_numeric($v) || bccomp($v, '0', 2) < 0) { throw new InvalidArgumentException('提现最低限额须为不小于 0 的数字'); } } diff --git a/app/common/library/game/StreakWinReward.php b/app/common/library/game/StreakWinReward.php index 5cc4813..5e5091c 100644 --- a/app/common/library/game/StreakWinReward.php +++ b/app/common/library/game/StreakWinReward.php @@ -147,7 +147,7 @@ final class StreakWinReward } /** - * 返回该注单适用的「赔率乘数」字符串(= 配置档位的 odds_factor),供 bcmul(total_amount, ..., 4)。 + * 返回该注单适用的「赔率乘数」字符串(= 配置档位的 odds_factor),供 bcmul(total_amount, ..., 2)。 */ public static function totalOddsMultiplierForStreakAtBet(int $streakAtBet): string { @@ -156,7 +156,7 @@ final class StreakWinReward $factor = 1; } - return bcadd((string) $factor, '0', 4); + return bcadd((string) $factor, '0', 2); } /** diff --git a/app/common/service/DepositOrderExpireService.php b/app/common/service/DepositOrderExpireService.php new file mode 100644 index 0000000..5c61396 --- /dev/null +++ b/app/common/service/DepositOrderExpireService.php @@ -0,0 +1,81 @@ +where('status', 0) + ->where('create_time', '<=', $expireBefore); + if ($userId !== null && $userId > 0) { + $query->where('user_id', $userId); + } + if ($orderNo !== null && $orderNo !== '') { + $query->where('order_no', $orderNo); + } + $rows = $query->field(['id', 'remark'])->select()->toArray(); + if ($rows === []) { + return 0; + } + $now = time(); + $affectedCount = 0; + foreach ($rows as $row) { + $id = isset($row['id']) && is_numeric($row['id']) ? intval($row['id']) : 0; + if ($id <= 0) { + continue; + } + $oldRemark = isset($row['remark']) && is_string($row['remark']) ? trim($row['remark']) : ''; + $reason = '[timeout] unpaid over ' . self::EXPIRE_SECONDS . 's'; + $remark = $oldRemark === '' ? $reason : mb_substr($oldRemark . ' | ' . $reason, 0, 255); + $affected = Db::name('deposit_order') + ->where('id', $id) + ->where('status', 0) + ->update([ + 'status' => 2, + 'remark' => $remark, + 'update_time' => $now, + ]); + if (is_numeric($affected) && intval($affected) > 0) { + $affectedCount++; + } + } + + return $affectedCount; + } + + public static function pendingCountByUserId(int $userId): int + { + if ($userId <= 0) { + return 0; + } + + return Db::name('deposit_order') + ->where('user_id', $userId) + ->where('status', 0) + ->count(); + } +} + diff --git a/app/common/service/GameBetSettleService.php b/app/common/service/GameBetSettleService.php index a61e734..b32485c 100644 --- a/app/common/service/GameBetSettleService.php +++ b/app/common/service/GameBetSettleService.php @@ -59,7 +59,7 @@ final class GameBetSettleService } $win = self::computeWinAmount($bet, $resultNumber); - $jackpot = '0.0000'; + $jackpot = '0.00'; $affected = Db::name('bet_order') ->where('id', $betId) @@ -78,10 +78,10 @@ final class GameBetSettleService self::creditUserBetFlow($bet, $now); if ($userId > 0) { - if (bccomp($win, '0', 4) > 0) { + if (bccomp($win, '0', 2) > 0) { $userOutcome[$userId]['had_win'] = true; } - if (bccomp($win, '0', 4) > 0 && StreakWinReward::isJackpotForStreakAtBet((int) ($bet['streak_at_bet'] ?? 0))) { + if (bccomp($win, '0', 2) > 0 && StreakWinReward::isJackpotForStreakAtBet((int) ($bet['streak_at_bet'] ?? 0))) { $jackpotNotify[$userId] = true; } } @@ -91,7 +91,7 @@ final class GameBetSettleService } $balanceAfter = (string) (Db::name('user')->where('id', $userId)->value('coin') ?? '0'); - if (bccomp($win, '0', 4) > 0) { + if (bccomp($win, '0', 2) > 0) { $paid = self::creditUserPayout($bet, $betId, $win, $now); if ($paid !== null) { $balanceAfter = $paid; @@ -102,17 +102,17 @@ final class GameBetSettleService if (!isset($aggregateByUser[$userId])) { $aggregateByUser[$userId] = [ 'period_no' => $periodNo, - 'total_win' => '0.0000', + 'total_win' => '0.00', 'balance_after' => $balanceAfter, 'orders' => [], ]; } - $aggregateByUser[$userId]['total_win'] = bcadd($aggregateByUser[$userId]['total_win'], $win, 4); + $aggregateByUser[$userId]['total_win'] = bcadd($aggregateByUser[$userId]['total_win'], $win, 2); $aggregateByUser[$userId]['balance_after'] = $balanceAfter; $aggregateByUser[$userId]['orders'][] = [ 'order_no' => (string) $betId, 'win_amount' => $win, - 'hit' => bccomp($win, '0', 4) > 0, + 'hit' => bccomp($win, '0', 2) > 0, ]; } @@ -150,7 +150,7 @@ final class GameBetSettleService 'balance_after' => $agg['balance_after'], ]); - if (bccomp($agg['total_win'], '0', 4) > 0) { + if (bccomp($agg['total_win'], '0', 2) > 0) { UserPushService::publish((int) $userId, UserPushService::EVT_WALLET_CHANGED, [ 'reason' => 'payout', 'ref_type' => 'game_period', @@ -167,7 +167,7 @@ final class GameBetSettleService continue; } $agg = $aggregateByUser[$uid]; - if (bccomp($agg['total_win'], '0', 4) <= 0) { + if (bccomp($agg['total_win'], '0', 2) <= 0) { continue; } $jackpotHits[] = [ @@ -187,7 +187,7 @@ final class GameBetSettleService public static function settlePendingForEndedRecords(): int { $rows = Db::name('game_record') - ->where('status', 4) + ->where('status', 2) ->whereNotNull('result_number') ->field(['id', 'result_number']) ->order('id', 'asc') @@ -237,13 +237,13 @@ final class GameBetSettleService $pickNumbers = []; } if (!in_array($resultNumber, array_map('intval', $pickNumbers), true)) { - return '0.0000'; + return '0.00'; } $total = (string) ($bet['total_amount'] ?? '0'); $streak = (int) ($bet['streak_at_bet'] ?? 0); $odds = StreakWinReward::totalOddsMultiplierForStreakAtBet($streak); - return bcmul($total, $odds, 4); + return bcmul($total, $odds, 2); } /** @@ -263,8 +263,8 @@ final class GameBetSettleService if ($total === '' || !is_numeric($total)) { return; } - $flow = bcadd($total, '0', 4); - if (bccomp($flow, '0', 4) <= 0) { + $flow = bcadd($total, '0', 2); + if (bccomp($flow, '0', 2) <= 0) { return; } Db::name('user') @@ -299,7 +299,7 @@ final class GameBetSettleService } $before = (string) ($user['coin'] ?? '0'); - $after = bcadd($before, $winAmount, 4); + $after = bcadd($before, $winAmount, 2); Db::name('user_wallet_record')->insert([ 'user_id' => $userId, diff --git a/app/common/service/GameLiveService.php b/app/common/service/GameLiveService.php index 9bf1ea2..cbe43df 100644 --- a/app/common/service/GameLiveService.php +++ b/app/common/service/GameLiveService.php @@ -207,12 +207,12 @@ final class GameLiveService for ($n = 1; $n <= self::DRAW_NUMBER_MAX; $n++) { $loss = self::estimateLossForNumber($bets, $n); $candidates[] = ['number' => $n, 'estimated_loss' => $loss]; - if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) { + if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 2) < 0) { $bestLoss = $loss; $bestNumbers = [$n]; continue; } - if (bccomp((string) $loss, (string) $bestLoss, 4) === 0) { + if (bccomp((string) $loss, (string) $bestLoss, 2) === 0) { $bestNumbers[] = $n; } } @@ -225,7 +225,7 @@ final class GameLiveService } $finalNumber = $manualNumber ?? $bestNumber; - $finalLoss = '0.0000'; + $finalLoss = '0.00'; if ($finalNumber !== null) { $finalLoss = self::estimateLossForNumber($bets, $finalNumber); } @@ -558,7 +558,7 @@ final class GameLiveService if ($betId <= 0) { continue; } - if ($userId <= 0 || bccomp($total, '0', 4) <= 0) { + if ($userId <= 0 || bccomp($total, '0', 2) <= 0) { Db::name('bet_order')->where('id', $betId)->where('status', 1)->update([ 'status' => 3, 'update_time' => $now, @@ -566,7 +566,7 @@ final class GameLiveService continue; } $before = (string) (Db::name('user')->where('id', $userId)->value('coin') ?? '0'); - $after = bcadd($before, $total, 4); + $after = bcadd($before, $total, 2); $u = Db::name('user')->where('id', $userId)->where('coin', $before)->update([ 'coin' => $after, 'update_time' => $now, @@ -836,12 +836,12 @@ final class GameLiveService $bestNumbers = []; for ($n = 1; $n <= self::DRAW_NUMBER_MAX; $n++) { $loss = self::estimateLossForNumber($bets, $n); - if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) { + if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 2) < 0) { $bestLoss = $loss; $bestNumbers = [$n]; continue; } - if (bccomp((string) $loss, (string) $bestLoss, 4) === 0) { + if (bccomp((string) $loss, (string) $bestLoss, 2) === 0) { $bestNumbers[] = $n; } } @@ -878,7 +878,7 @@ final class GameLiveService private static function estimateLossForNumber(array $bets, int $number): string { - $payout = '0.0000'; + $payout = '0.00'; foreach ($bets as $bet) { $pickNumbers = $bet['pick_numbers']; if (is_string($pickNumbers)) { @@ -894,8 +894,8 @@ final class GameLiveService $total = (string) ($bet['total_amount'] ?? '0'); $streak = (int) ($bet['streak_at_bet'] ?? 0); $odds = StreakWinReward::totalOddsMultiplierForStreakAtBet($streak); - $orderPayout = bcmul($total, $odds, 4); - $payout = bcadd($payout, $orderPayout, 4); + $orderPayout = bcmul($total, $odds, 2); + $payout = bcadd($payout, $orderPayout, 2); } return $payout; } diff --git a/app/common/service/GameRecordStatService.php b/app/common/service/GameRecordStatService.php index 1f6b2c9..7af9715 100644 --- a/app/common/service/GameRecordStatService.php +++ b/app/common/service/GameRecordStatService.php @@ -29,7 +29,7 @@ final class GameRecordStatService if ($status !== 4) { Db::name('game_record')->where('id', $recordId)->update([ - 'platform_profit_amount' => '0.0000', + 'platform_profit_amount' => '0.00', 'winner_user_count' => 0, 'update_time' => $now, ]); @@ -45,8 +45,8 @@ final class GameRecordStatService $resultNum = (int) $resultRaw; $bets = Db::name('bet_order')->where('period_id', $recordId)->select()->toArray(); - $totalBet = '0.0000'; - $totalPayout = '0.0000'; + $totalBet = '0.00'; + $totalPayout = '0.00'; $winnerUserIds = []; foreach ($bets as $bet) { @@ -55,16 +55,16 @@ final class GameRecordStatService continue; } $tb = (string) ($bet['total_amount'] ?? '0'); - $totalBet = bcadd($totalBet, $tb, 4); + $totalBet = bcadd($totalBet, $tb, 2); if ($st === 2) { - $payout = bcadd((string) ($bet['win_amount'] ?? '0'), (string) ($bet['jackpot_extra_amount'] ?? '0'), 4); + $payout = bcadd((string) ($bet['win_amount'] ?? '0'), (string) ($bet['jackpot_extra_amount'] ?? '0'), 2); } else { $payout = self::estimatePayoutForBet($bet, $resultNum); } - $totalPayout = bcadd($totalPayout, $payout, 4); - if (bccomp($payout, '0', 4) > 0) { + $totalPayout = bcadd($totalPayout, $payout, 2); + if (bccomp($payout, '0', 2) > 0) { $uid = (int) ($bet['user_id'] ?? 0); if ($uid > 0) { $winnerUserIds[$uid] = true; @@ -72,7 +72,7 @@ final class GameRecordStatService } } - $profit = bcsub($totalBet, $totalPayout, 4); + $profit = bcsub($totalBet, $totalPayout, 2); Db::name('game_record')->where('id', $recordId)->update([ 'platform_profit_amount' => $profit, @@ -96,12 +96,12 @@ final class GameRecordStatService $pickNumbers = []; } if (!in_array($resultNumber, array_map('intval', $pickNumbers), true)) { - return '0.0000'; + return '0.00'; } $total = (string) ($bet['total_amount'] ?? '0'); $streak = (int) ($bet['streak_at_bet'] ?? 0); $odds = StreakWinReward::totalOddsMultiplierForStreakAtBet($streak); - return bcmul($total, $odds, 4); + return bcmul($total, $odds, 2); } } diff --git a/app/process/DepositOrderExpireTicker.php b/app/process/DepositOrderExpireTicker.php new file mode 100644 index 0000000..672a230 --- /dev/null +++ b/app/process/DepositOrderExpireTicker.php @@ -0,0 +1,22 @@ + is_string(getenv('DEPOSIT_MOCK_HMAC_KEY')) && trim((string) getenv('DEPOSIT_MOCK_HMAC_KEY')) !== '' + ? trim((string) getenv('DEPOSIT_MOCK_HMAC_KEY')) + : '', 'debug' => true, 'error_reporting' => E_ALL, 'default_timezone' => 'Asia/Shanghai', diff --git a/config/process.php b/config/process.php index ac045c3..f587f2f 100644 --- a/config/process.php +++ b/config/process.php @@ -52,6 +52,12 @@ return [ 'count' => 1, 'reloadable' => false, ], + // 充值订单:超时未支付自动失败(每 10 秒扫描一次) + 'depositOrderExpireTicker' => [ + 'handler' => app\process\DepositOrderExpireTicker::class, + 'count' => 1, + 'reloadable' => false, + ], // File update detection and automatic reload 'monitor' => [ diff --git a/config/route.php b/config/route.php index 2f6b276..42055c6 100644 --- a/config/route.php +++ b/config/route.php @@ -137,9 +137,12 @@ Route::add(['GET', 'POST'], '/api/wallet/recordList', [\app\api\controller\Walle Route::add(['GET', 'POST'], '/api/finance/depositTierList', [\app\api\controller\Finance::class, 'depositTierList']); Route::post('/api/finance/depositCreate', [\app\api\controller\Finance::class, 'depositCreate']); +Route::get('/api/finance/depositMockPayPage', [\app\api\controller\Finance::class, 'depositMockPayPage']); +Route::post('/api/finance/depositMockNotify', [\app\api\controller\Finance::class, 'depositMockNotify']); Route::add(['GET', 'POST'], '/api/finance/depositDetail', [\app\api\controller\Finance::class, 'depositDetail']); Route::add(['GET', 'POST'], '/api/finance/depositList', [\app\api\controller\Finance::class, 'depositList']); Route::add(['GET', 'POST'], '/api/finance/cashierConfig', [\app\api\controller\Finance::class, 'cashierConfig']); +Route::add(['GET', 'POST'], '/api/finance/depositWithdrawConfig', [\app\api\controller\Finance::class, 'depositWithdrawConfig']); Route::post('/api/finance/withdrawCreate', [\app\api\controller\Finance::class, 'withdrawCreate']); Route::add(['GET', 'POST'], '/api/finance/withdrawDetail', [\app\api\controller\Finance::class, 'withdrawDetail']); Route::add(['GET', 'POST'], '/api/finance/withdrawList', [\app\api\controller\Finance::class, 'withdrawList']); @@ -262,6 +265,9 @@ Route::get('/admin/security/dataRecycleLog/index', [\app\admin\controller\securi Route::post('/admin/security/dataRecycleLog/restore', [\app\admin\controller\security\DataRecycleLog::class, 'restore']); Route::get('/admin/security/dataRecycleLog/info', [\app\admin\controller\security\DataRecycleLog::class, 'info']); +// admin/config/depositTier +Route::get('/admin/config/depositTier/currencyOptions', [\app\admin\controller\config\DepositTier::class, 'currencyOptions']); + // ==================== CRUD 生成的根级控制器(/admin/item/index 或 /admin/Item/index,无子目录、无点号) ==================== // 显式路由在上,此处作为兜底;与 /admin/module.controller/action 互补 Route::add( diff --git a/web/src/lang/backend/en/channel.ts b/web/src/lang/backend/en/channel.ts index 51b2994..6047937 100644 --- a/web/src/lang/backend/en/channel.ts +++ b/web/src/lang/backend/en/channel.ts @@ -36,7 +36,7 @@ export default { affiliate_effective_start_at: 'affiliate_effective_start_at', affiliate_effective_end_at: 'affiliate_effective_end_at', affiliate_ladder_rules: 'affiliate_ladder_rules', - affiliate_ladder_rules_placeholder: 'Input JSON, e.g. [{\"minLoss\":\"0.0000\",\"shareRate\":\"0.200000\"}]', + affiliate_ladder_rules_placeholder: 'Input JSON, e.g. [{\"minLoss\":\"0.00\",\"shareRate\":\"0.200000\"}]', ladder_min_loss: 'min loss', ladder_share_rate: 'share rate', ladder_rule_required: 'At least one ladder rule is required', diff --git a/web/src/lang/backend/en/config/financeCashierConfig.ts b/web/src/lang/backend/en/config/financeCashierConfig.ts index 3d4f5f4..b5a7e25 100644 --- a/web/src/lang/backend/en/config/financeCashierConfig.ts +++ b/web/src/lang/backend/en/config/financeCashierConfig.ts @@ -1,5 +1,5 @@ export default { - desc: 'Mobile pay & receipt settings: platform coin labels, currencies and rates, deposit pay channels (on/off, sort, tier scope), withdraw banks, limits, copy, and required withdraw fields. Deposit tiers are configured separately.', + desc: 'Mobile pay & receipt settings: platform coin labels, currencies and rates, deposit pay channels (on/off and sort; all tiers are supported automatically), withdraw banks, limits, copy, and required withdraw fields.', btn_save: 'Save', btn_add_row: 'Add row', sec_platform: 'Platform coin labels', @@ -7,7 +7,7 @@ export default { platform_label_en: 'Label (English)', sec_currencies: 'Currencies (deposit/withdraw selectors)', sec_deposit_channels: 'Deposit pay channels', - deposit_channels_hint: 'Display names come from the registry; here you only set enabled state, sort order, and applicable deposit tiers. Leave tiers empty to allow all tiers.', + deposit_channels_hint: 'Display names come from the registry; here you only set enabled state and sort order. All enabled channels automatically support all deposit tiers.', currency_rates_hint: 'Deposit rate: platform coins credited per 1 fiat paid. Withdraw rate: platform coins needed per 1 fiat redeemed (e.g. 100 ⇒ 100 coins = 1 fiat unit).', err_dup_code: 'Duplicate currency codes are not allowed.', sec_banks: 'Withdraw bank codes', @@ -45,6 +45,4 @@ export default { ch_display_name: 'Display name', ch_sort: 'Sort', ch_status: 'Enabled', - ch_tier_ids: 'Allowed deposit tiers', - ch_tier_ids_ph: 'Empty = all tiers', } diff --git a/web/src/lang/backend/en/order/depositChannelOrder.ts b/web/src/lang/backend/en/order/depositChannelOrder.ts deleted file mode 100644 index fb628bf..0000000 --- a/web/src/lang/backend/en/order/depositChannelOrder.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default { - 'quick Search Fields': 'order no / pay channel / tier id / idempotency key', -} diff --git a/web/src/lang/backend/en/order/withdrawOrder.ts b/web/src/lang/backend/en/order/withdrawOrder.ts index ab4c056..b6d030a 100644 --- a/web/src/lang/backend/en/order/withdrawOrder.ts +++ b/web/src/lang/backend/en/order/withdrawOrder.ts @@ -7,6 +7,9 @@ export default { amount: 'Apply amount', fee: 'Fee', actual_amount: 'Actual amount', + receive_type: 'Receive type', + receive_account: 'Receive account', + idempotency_key: 'Idempotency key', status: 'Status', 'status 0': 'Pending review', 'status 1': 'Approved', diff --git a/web/src/lang/backend/zh-cn/channel.ts b/web/src/lang/backend/zh-cn/channel.ts index 0e91ef5..4863948 100644 --- a/web/src/lang/backend/zh-cn/channel.ts +++ b/web/src/lang/backend/zh-cn/channel.ts @@ -36,7 +36,7 @@ export default { affiliate_effective_start_at: '联营生效开始', affiliate_effective_end_at: '联营生效结束', affiliate_ladder_rules: '联营阶梯规则', - affiliate_ladder_rules_placeholder: '请输入 JSON,例如 [{\"minLoss\":\"0.0000\",\"shareRate\":\"0.200000\"}]', + affiliate_ladder_rules_placeholder: '请输入 JSON,例如 [{\"minLoss\":\"0.00\",\"shareRate\":\"0.200000\"}]', ladder_min_loss: '起始客损', ladder_share_rate: '占成比例', ladder_rule_required: '联营阶梯规则至少需要一条', diff --git a/web/src/lang/backend/zh-cn/config/financeCashierConfig.ts b/web/src/lang/backend/zh-cn/config/financeCashierConfig.ts index e90c5c4..473995c 100644 --- a/web/src/lang/backend/zh-cn/config/financeCashierConfig.ts +++ b/web/src/lang/backend/zh-cn/config/financeCashierConfig.ts @@ -1,5 +1,5 @@ export default { - desc: '配置移动端支付与收款展示:平台币名称、货币与汇率、充值支付渠道(开关/排序/适用档位)、提现银行、最低限额、文案与提现表单字段。充值档位在「充值档位」中维护。', + desc: '配置移动端支付与收款展示:平台币名称、货币与汇率、充值支付渠道(开关/排序,自动兼容全部档位)、提现银行、最低限额、文案与提现表单字段。', btn_save: '保存', btn_add_row: '新增一行', sec_platform: '平台币展示名', @@ -7,7 +7,7 @@ export default { platform_label_en: '名称(英文)', sec_currencies: '货币列表(充值/提现货币下拉)', sec_deposit_channels: '充值支付渠道', - deposit_channels_hint: '展示名由环境注册表决定,此处仅维护启用状态、排序与适用充值档位;不选档位表示全部档位可用。', + deposit_channels_hint: '展示名由环境注册表决定,此处仅维护启用状态与排序;所有启用渠道自动兼容全部充值档位。', currency_rates_hint: '充值汇率:每支付 1 单位该货币到账的平台币;提现汇率:每兑换 1 单位该货币所需平台币(例 100 表示 100 平台币 = 1 单位)。', err_dup_code: '货币代码不能重复,请检查后再保存。', sec_banks: '提现支持银行代码', @@ -46,6 +46,4 @@ export default { ch_display_name: '展示名称', ch_sort: '排序', ch_status: '启用', - ch_tier_ids: '适用充值档位', - ch_tier_ids_ph: '不选表示全部档位', } diff --git a/web/src/lang/backend/zh-cn/order/depositChannelOrder.ts b/web/src/lang/backend/zh-cn/order/depositChannelOrder.ts deleted file mode 100644 index 63176f1..0000000 --- a/web/src/lang/backend/zh-cn/order/depositChannelOrder.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default { - 'quick Search Fields': '订单号/支付通道/档位ID/幂等键', -} diff --git a/web/src/lang/backend/zh-cn/order/withdrawOrder.ts b/web/src/lang/backend/zh-cn/order/withdrawOrder.ts index 87c2939..3bd2f15 100644 --- a/web/src/lang/backend/zh-cn/order/withdrawOrder.ts +++ b/web/src/lang/backend/zh-cn/order/withdrawOrder.ts @@ -7,6 +7,9 @@ export default { amount: '申请金额', fee: '手续费', actual_amount: '实际到账', + receive_type: '收款类型', + receive_account: '收款账号', + idempotency_key: '幂等键', status: '状态', 'status 0': '待审核', 'status 1': '已通过', diff --git a/web/src/views/backend/agent/commissionRecord/index.vue b/web/src/views/backend/agent/commissionRecord/index.vue index 828de7f..1f6fb01 100644 --- a/web/src/views/backend/agent/commissionRecord/index.vue +++ b/web/src/views/backend/agent/commissionRecord/index.vue @@ -30,6 +30,17 @@ const { t } = useI18n() const tableRef = useTemplateRef('tableRef') const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']) +function formatAmount2(_row: anyObj, _column: any, cellValue: unknown) { + if (cellValue === null || cellValue === undefined || cellValue === '') { + return '-' + } + const n = Number(cellValue) + if (!Number.isFinite(n)) { + return String(cellValue) + } + return n.toFixed(2) +} + const baTable = new baTableClass( new baTableApi('/admin/agent.CommissionRecord/'), { @@ -74,9 +85,30 @@ const baTable = new baTableClass( operatorPlaceholder: t('Fuzzy query'), render: 'tags', }, - { label: t('agent.commissionRecord.commission_rate'), prop: 'commission_rate', align: 'center', minWidth: 110, operator: 'RANGE' }, - { label: t('agent.commissionRecord.calc_base_amount'), prop: 'calc_base_amount', align: 'center', minWidth: 120, operator: 'RANGE' }, - { label: t('agent.commissionRecord.commission_amount'), prop: 'commission_amount', align: 'center', minWidth: 120, operator: 'RANGE' }, + { + label: t('agent.commissionRecord.commission_rate'), + prop: 'commission_rate', + align: 'center', + minWidth: 110, + operator: 'RANGE', + formatter: formatAmount2, + }, + { + label: t('agent.commissionRecord.calc_base_amount'), + prop: 'calc_base_amount', + align: 'center', + minWidth: 120, + operator: 'RANGE', + formatter: formatAmount2, + }, + { + label: t('agent.commissionRecord.commission_amount'), + prop: 'commission_amount', + align: 'center', + minWidth: 120, + operator: 'RANGE', + formatter: formatAmount2, + }, { label: t('agent.commissionRecord.status'), prop: 'status', @@ -138,7 +170,7 @@ const baTable = new baTableClass( ], }, { - defaultItems: { status: 0, commission_rate: '0.0000' }, + defaultItems: { status: 0, commission_rate: '0.00' }, } ) diff --git a/web/src/views/backend/agent/commissionRecord/popupForm.vue b/web/src/views/backend/agent/commissionRecord/popupForm.vue index 9b03456..fb7123d 100644 --- a/web/src/views/backend/agent/commissionRecord/popupForm.vue +++ b/web/src/views/backend/agent/commissionRecord/popupForm.vue @@ -45,9 +45,9 @@ placeholder: t('Click select'), }" /> - - - + + + diff --git a/web/src/views/backend/agent/settlementPeriod/index.vue b/web/src/views/backend/agent/settlementPeriod/index.vue index 8bb5fc3..2eba740 100644 --- a/web/src/views/backend/agent/settlementPeriod/index.vue +++ b/web/src/views/backend/agent/settlementPeriod/index.vue @@ -30,6 +30,17 @@ const { t } = useI18n() const tableRef = useTemplateRef('tableRef') const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']) +function formatAmount2(_row: anyObj, _column: any, cellValue: unknown) { + if (cellValue === null || cellValue === undefined || cellValue === '') { + return '-' + } + const n = Number(cellValue) + if (!Number.isFinite(n)) { + return String(cellValue) + } + return n.toFixed(2) +} + const baTable = new baTableClass( new baTableApi('/admin/agent.SettlementPeriod/'), { @@ -67,13 +78,21 @@ const baTable = new baTableClass( sortable: 'custom', timeFormat: 'yyyy-mm-dd hh:MM:ss', }, - { label: t('agent.settlementPeriod.total_bet_amount'), prop: 'total_bet_amount', align: 'center', operator: 'RANGE', minWidth: 120 }, + { + label: t('agent.settlementPeriod.total_bet_amount'), + prop: 'total_bet_amount', + align: 'center', + operator: 'RANGE', + minWidth: 120, + formatter: formatAmount2, + }, { label: t('agent.settlementPeriod.total_payout_amount'), prop: 'total_payout_amount', align: 'center', operator: 'RANGE', minWidth: 120, + formatter: formatAmount2, }, { label: t('agent.settlementPeriod.platform_profit_amount'), @@ -81,6 +100,7 @@ const baTable = new baTableClass( align: 'center', operator: 'RANGE', minWidth: 120, + formatter: formatAmount2, }, { label: t('agent.settlementPeriod.status'), diff --git a/web/src/views/backend/agent/settlementPeriod/popupForm.vue b/web/src/views/backend/agent/settlementPeriod/popupForm.vue index d14cf4a..02dd3fe 100644 --- a/web/src/views/backend/agent/settlementPeriod/popupForm.vue +++ b/web/src/views/backend/agent/settlementPeriod/popupForm.vue @@ -9,9 +9,9 @@ - - - + + + diff --git a/web/src/views/backend/channel/popupForm.vue b/web/src/views/backend/channel/popupForm.vue index 520a43b..21d1271 100644 --- a/web/src/views/backend/channel/popupForm.vue +++ b/web/src/views/backend/channel/popupForm.vue @@ -91,7 +91,7 @@ type="number" v-model="baTable.form.items!.affiliate_share_rate" prop="affiliate_share_rate" - :input-attr="{ step: 0.000001, precision: 6, min: 0, max: 1 }" + :input-attr="{ step: 0.0001, precision: 2, min: 0, max: 1 }" :placeholder="`${t('Please input field', { field: t('channel.affiliate_share_rate') })} (例如 0.240000)`" /> @@ -140,7 +140,7 @@
- + {{ t('Delete') }}
{{ t('Add') }} @@ -480,7 +480,7 @@ baTable.before.onSubmit = ({ items }) => { } } items.affiliate_ladder_rules = sorted.map((r) => ({ - minLoss: Number(r.minLoss).toFixed(4), + minLoss: Number(r.minLoss).toFixed(2), shareRate: Number(r.shareRate).toFixed(6), })) } else { diff --git a/web/src/views/backend/config/depositChannel/index.vue b/web/src/views/backend/config/depositChannel/index.vue index c9fdfe3..dea4597 100644 --- a/web/src/views/backend/config/depositChannel/index.vue +++ b/web/src/views/backend/config/depositChannel/index.vue @@ -44,7 +44,6 @@ const baTable = new baTableClass( defaultOrder: { prop: 'sort', order: 'asc' }, extend: { registry: {} as Record, - tier_options: [] as { id: string; label: string }[], }, column: [ { type: 'selection', align: 'center', operator: false }, @@ -83,25 +82,6 @@ const baTable = new baTableClass( operatorPlaceholder: t('Fuzzy query'), showOverflowTooltip: true, }, - { - label: t('config.depositChannel.tier_ids'), - prop: 'tier_ids', - align: 'center', - minWidth: 240, - operator: false, - render: 'tags', - formatter: (row: anyObj) => { - const ids = row.tier_ids - if (!Array.isArray(ids) || ids.length === 0) { - return [t('config.depositChannel.tier_all')] - } - const opts = (baTable.table.extend?.tier_options ?? []) as { id: string; label: string }[] - return ids.map((id: string) => { - const o = opts.find((x) => x.id === id) - return o ? o.label : id - }) - }, - }, { label: t('Operate'), align: 'center', @@ -119,7 +99,6 @@ const baTable = new baTableClass( code: '', sort: 10, status: 1, - tier_ids: [] as string[], }, }, {}, @@ -128,15 +107,11 @@ const baTable = new baTableClass( const d = res.data as | { registry?: Record - tier_options?: { id: string; label: string }[] } | undefined if (d?.registry) { baTable.table.extend.registry = d.registry } - if (d?.tier_options) { - baTable.table.extend.tier_options = d.tier_options - } }, } ) diff --git a/web/src/views/backend/config/depositChannel/popupForm.vue b/web/src/views/backend/config/depositChannel/popupForm.vue index ca983e5..dc88b39 100644 --- a/web/src/views/backend/config/depositChannel/popupForm.vue +++ b/web/src/views/backend/config/depositChannel/popupForm.vue @@ -43,20 +43,6 @@
- - - - - @@ -77,18 +63,11 @@ import { computed, inject, useTemplateRef } from 'vue' import { useI18n } from 'vue-i18n' import { useConfig } from '/@/stores/config' -type TierOpt = { id: string; label: string } - const config = useConfig() const formRef = useTemplateRef('formRef') const baTable = inject('baTable') as baTable const { t } = useI18n() -const tierOptions = computed(() => { - const raw = baTable.table.extend?.tier_options - return Array.isArray(raw) ? (raw as TierOpt[]) : [] -}) - const registryDisplayName = computed(() => { const code = String(baTable.form.items?.code ?? '') const reg = baTable.table.extend?.registry as Record | undefined diff --git a/web/src/views/backend/config/depositTier/index.vue b/web/src/views/backend/config/depositTier/index.vue index 6fe5467..40070a8 100644 --- a/web/src/views/backend/config/depositTier/index.vue +++ b/web/src/views/backend/config/depositTier/index.vue @@ -42,7 +42,7 @@ function formatAmount4(_row: anyObj, _column: any, cellValue: unknown) { if (!Number.isFinite(n)) { return String(cellValue) } - return n.toFixed(4) + return n.toFixed(2) } function formatPayCell(row: anyObj, _column: any, cellValue: unknown) { @@ -59,7 +59,7 @@ function formatTotalPlatform(row: anyObj) { const b = parseFloat(String(row.bonus_amount ?? '0').replace(',', '.')) const base = Number.isFinite(a) ? a : 0 const bonus = Number.isFinite(b) ? b : 0 - return (base + bonus).toFixed(4) + return (base + bonus).toFixed(2) } const baTable = new baTableClass( @@ -68,7 +68,7 @@ const baTable = new baTableClass( pk: 'id', filter: { page: 1, - limit: 20, + limit: 100, }, defaultOrder: { prop: 'sort', order: 'asc' }, column: [ diff --git a/web/src/views/backend/config/depositTier/popupForm.vue b/web/src/views/backend/config/depositTier/popupForm.vue index 0bd918b..4d9bc69 100644 --- a/web/src/views/backend/config/depositTier/popupForm.vue +++ b/web/src/views/backend/config/depositTier/popupForm.vue @@ -87,16 +87,37 @@ diff --git a/web/src/views/backend/order/depositOrder/index.vue b/web/src/views/backend/order/depositOrder/index.vue index 0a89acc..31921f4 100644 --- a/web/src/views/backend/order/depositOrder/index.vue +++ b/web/src/views/backend/order/depositOrder/index.vue @@ -195,7 +195,7 @@ const baTable = new baTableClass( ], }, { - defaultItems: { status: 0, amount: '0.0000', bonus_amount: '0.0000' }, + defaultItems: { status: 0, amount: '0.00', bonus_amount: '0.00' }, } ) diff --git a/web/src/views/backend/order/withdrawOrder/index.vue b/web/src/views/backend/order/withdrawOrder/index.vue index 483064c..94512e9 100644 --- a/web/src/views/backend/order/withdrawOrder/index.vue +++ b/web/src/views/backend/order/withdrawOrder/index.vue @@ -31,6 +31,17 @@ const { t } = useI18n() const tableRef = useTemplateRef('tableRef') const optButtons: OptButton[] = defaultOptButtons(['edit']) +function formatAmount(_row: anyObj, _column: any, cellValue: unknown) { + if (cellValue === null || cellValue === undefined || cellValue === '') { + return '-' + } + const n = Number(cellValue) + if (!Number.isFinite(n)) { + return String(cellValue) + } + return n.toFixed(2) +} + const baTable = new baTableClass( new baTableApi('/admin/order.WithdrawOrder/'), { @@ -65,9 +76,36 @@ const baTable = new baTableClass( operatorPlaceholder: t('Fuzzy query'), render: 'tags', }, - { label: t('order.withdrawOrder.amount'), prop: 'amount', align: 'center', minWidth: 110, operator: 'RANGE' }, - { label: t('order.withdrawOrder.fee'), prop: 'fee', align: 'center', minWidth: 110, operator: 'RANGE' }, - { label: t('order.withdrawOrder.actual_amount'), prop: 'actual_amount', align: 'center', minWidth: 110, operator: 'RANGE' }, + { label: t('order.withdrawOrder.amount'), prop: 'amount', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount }, + { label: t('order.withdrawOrder.fee'), prop: 'fee', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount }, + { label: t('order.withdrawOrder.actual_amount'), prop: 'actual_amount', align: 'center', minWidth: 110, operator: 'RANGE', formatter: formatAmount }, + { + label: t('order.withdrawOrder.receive_type'), + prop: 'receive_type', + align: 'center', + minWidth: 120, + operator: 'LIKE', + operatorPlaceholder: t('Fuzzy query'), + }, + { + label: t('order.withdrawOrder.receive_account'), + prop: 'receive_account', + align: 'center', + minWidth: 180, + operator: 'LIKE', + operatorPlaceholder: t('Fuzzy query'), + showOverflowTooltip: true, + }, + { + label: t('order.withdrawOrder.idempotency_key'), + prop: 'idempotency_key', + align: 'center', + minWidth: 170, + operator: 'LIKE', + operatorPlaceholder: t('Fuzzy query'), + showOverflowTooltip: true, + show: false, + }, { label: t('order.withdrawOrder.status'), prop: 'status', @@ -144,7 +182,7 @@ const baTable = new baTableClass( ], }, { - defaultItems: { status: 0, amount: '0.0000', fee: '0.0000', actual_amount: '0.0000' }, + defaultItems: { status: 0, amount: '0.00', fee: '0.00', actual_amount: '0.00' }, } ) diff --git a/web/src/views/backend/order/withdrawOrder/popupForm.vue b/web/src/views/backend/order/withdrawOrder/popupForm.vue index 149a8c5..a23abbd 100644 --- a/web/src/views/backend/order/withdrawOrder/popupForm.vue +++ b/web/src/views/backend/order/withdrawOrder/popupForm.vue @@ -39,6 +39,15 @@ + + + + + + + + + { form.fee = parseNumber(row['fee']) form.status = Number(row['status'] ?? 0) form.remark = String(row['remark'] ?? '') + form.idempotency_key = String(row['idempotency_key'] ?? '') + form.receive_type = String(row['receive_type'] ?? '') + form.receive_account = String(row['receive_account'] ?? '') form.create_time_text = formatTime(row['create_time']) form.review_time_text = formatTime(row['review_time']) form.user_text = resolveRelationText(row, 'user', row['user_id']) @@ -316,8 +331,8 @@ const submitApprove = async () => { method: 'POST', data: { id: form.id, - amount: form.amount.toFixed(4), - fee: form.fee.toFixed(4), + amount: form.amount.toFixed(2), + fee: form.fee.toFixed(2), }, }, { showSuccessMessage: true }