0.使用模拟数据进行充值和提现

1.优化提现接口/api/finance/withdrawCreate
2.优化充值接口/api/finance/depositCreate
This commit is contained in:
2026-05-20 15:57:19 +08:00
parent b9e4d806f7
commit 1b8d947f97
25 changed files with 2022 additions and 179 deletions

View File

@@ -48,6 +48,30 @@ final class DDPayGateway
return $scheme . '://' . $host;
}
/**
* 是否已配置 DDPay 入金所需项(未配置时不应调用三方接口)
*/
public static function isConfigured(): bool
{
$envMap = [
'ddpay_client_id' => 'DDPAY_CLIENT_ID',
'ddpay_identifier' => 'DDPAY_IDENTIFIER',
'ddpay_api_secret' => 'DDPAY_API_SECRET',
'ddpay_deposit_init_url' => 'DDPAY_DEPOSIT_INIT_URL',
];
foreach ($envMap as $cfgKey => $envKey) {
$v = getenv($envKey);
if (!is_string($v) || trim($v) === '') {
$cfg = config('app.' . $cfgKey, '');
if (!is_string($cfg) || trim($cfg) === '') {
return false;
}
}
}
return true;
}
/**
* @return array<string, mixed>
*/

View File

@@ -96,7 +96,7 @@ final class DepositSettlement
];
}
if ($status !== 0) {
if ($status !== 0 && $status !== 3) {
throw new RuntimeException('Order status does not allow settlement');
}
@@ -139,9 +139,10 @@ final class DepositSettlement
Db::startTrans();
try {
$statusBefore = $status === 3 ? 3 : 0;
$affected = Db::name('deposit_order')
->where('id', $orderId)
->where('status', 0)
->where('status', $statusBefore)
->update([
'status' => 1,
'pay_time' => $now,

View File

@@ -0,0 +1,363 @@
<?php
declare(strict_types=1);
namespace app\common\library\finance;
use app\common\service\DepositOrderExpireService;
/**
* 模拟支付(无真实商户网关):用于开发/联调充值与提现审核。
*/
final class MockPay
{
public const CHANNEL_CODE = 'mock';
/** 待审核(用户已在模拟页确认支付,等待后台审核) */
public const DEPOSIT_STATUS_PENDING_REVIEW = 3;
public static function isEnabled(): bool
{
$raw = getenv('FINANCE_MOCK_PAY_ENABLED');
if (is_string($raw) && trim($raw) !== '') {
$norm = strtolower(trim($raw));
if (in_array($norm, ['0', 'false', 'no', 'off'], true)) {
return false;
}
if (in_array($norm, ['1', 'true', 'yes', 'on'], true)) {
return true;
}
}
$cfg = config('app.finance_mock_pay_enabled', null);
if ($cfg === true) {
return true;
}
if ($cfg === false) {
return false;
}
$debugRaw = getenv('APP_DEBUG');
if (is_string($debugRaw) && trim($debugRaw) !== '') {
return in_array(strtolower(trim($debugRaw)), ['1', 'true', 'yes', 'on'], true);
}
return false;
}
/**
* 提现审核后是否走模拟出金(不调用 DDPay
* - pay_channel=mock始终模拟
* - FINANCE_MOCK_PAY_ENABLED 开启ddpay/空 一律模拟(审核通过即成功);
* - 未开启 mock 且未配置 DDPayddpay/空 也模拟,避免误调网关。
*/
public static function shouldSimulateWithdrawPayout(string $payChannel): bool
{
$ch = strtolower(trim($payChannel));
if ($ch === self::CHANNEL_CODE) {
return true;
}
if ($ch !== '' && $ch !== 'ddpay') {
return false;
}
if (self::isEnabled()) {
return true;
}
return !DDPayGateway::isConfigured();
}
/**
* 计算链接过期时间与签名(防猜单号)
*
* @return array{expire_at: int, sign: string}
*/
public static function buildDepositLinkAuth(string $orderNo, int $createTime): array
{
$expireAt = $createTime + DepositOrderExpireService::pendingExpireSeconds();
return [
'expire_at' => $expireAt,
'sign' => self::signDepositLink($orderNo, $expireAt),
];
}
public static function signDepositLink(string $orderNo, int $expireAt): string
{
$params = [
'expire_at' => strval($expireAt),
'order_no' => $orderNo,
'secret' => self::linkSecret(),
];
ksort($params);
$pairs = [];
foreach ($params as $key => $value) {
$pairs[] = $key . '=' . $value;
}
return strtoupper(md5(implode('&', $pairs)));
}
public static function verifyDepositLink(string $orderNo, int $expireAt, string $sign): bool
{
$signNorm = strtoupper(trim($sign));
if ($signNorm === '' || $orderNo === '' || $expireAt <= 0) {
return false;
}
return hash_equals(self::signDepositLink($orderNo, $expireAt), $signNorm);
}
/**
* 前端静态收银台 URL优先于服务端内联页
*
* @param string $amountDisplay 2 位小数字符串,供页面展示
* @param string $bonusDisplay 2 位小数字符串
*/
public static function depositPageUrl(
string $orderNo,
string $publicOrigin,
int $expireAt,
string $sign,
string $amountDisplay = '',
string $bonusDisplay = ''
): string {
$htmlBase = self::resolveHtmlBase($publicOrigin);
$apiBase = rtrim($publicOrigin, '/');
$query = [
'order_no' => $orderNo,
'expire_at' => strval($expireAt),
'sign' => $sign,
'api_base' => $apiBase,
];
if ($amountDisplay !== '') {
$query['amount'] = $amountDisplay;
}
if ($bonusDisplay !== '') {
$query['bonus'] = $bonusDisplay;
}
return $htmlBase . '/mock-deposit.html?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
}
/**
* 模拟页内确认支付接口(无需 auth-token须携带 sign + expire_at
*/
public static function depositConfirmUrl(string $orderNo, string $publicOrigin, int $expireAt, string $sign): string
{
$base = rtrim($publicOrigin, '/');
$query = http_build_query([
'order_no' => $orderNo,
'expire_at' => strval($expireAt),
'sign' => $sign,
], '', '&', PHP_QUERY_RFC3986);
return $base . '/api/finance/mockDepositConfirm?' . $query;
}
/**
* 解析前端静态页根地址MOCK_DEPOSIT_HTML_BASE > DDPAY_PUBLIC_BASE_URL > API 公网根
*/
public static function resolveHtmlBase(string $publicOrigin): string
{
$raw = getenv('MOCK_DEPOSIT_HTML_BASE');
if (is_string($raw) && trim($raw) !== '') {
return rtrim(trim($raw), '/');
}
$ddpayPublic = getenv('DDPAY_PUBLIC_BASE_URL');
if (is_string($ddpayPublic) && trim($ddpayPublic) !== '') {
return rtrim(trim($ddpayPublic), '/');
}
$cfg = config('app.ddpay_public_base_url', '');
if (is_string($cfg) && trim($cfg) !== '') {
return rtrim(trim($cfg), '/');
}
return rtrim($publicOrigin, '/');
}
private static function linkSecret(): string
{
$raw = getenv('FINANCE_MOCK_PAY_LINK_SECRET');
if (is_string($raw) && trim($raw) !== '') {
return trim($raw);
}
$auth = getenv('AUTH_TOKEN_SECRET');
if (is_string($auth) && trim($auth) !== '') {
return trim($auth);
}
return 'mock-deposit-link-dev-secret';
}
}

View File

@@ -4,13 +4,14 @@ declare(strict_types=1);
namespace app\common\library\game;
use app\common\library\finance\MockPay;
use InvalidArgumentException;
use support\think\Db;
/**
* 充值支付渠道:优先读取 game_config.finance_cashier.channels无此键时回退 game_config.deposit_channel迁移期镜像
*
* 每项code须在代码/环境注册表内、sort、status(0/1)。**代码注册表当前仅内置 `ddpay`**DDPay 网关)。
* 每项code须在代码/环境注册表内、sort、status(0/1)。内置 `ddpay`DDPay)、`mock`(模拟支付,见 FINANCE_MOCK_PAY_ENABLED)。
*
* 渠道展示名以代码注册表为准;运营只配置开关、排序与支持币种,默认兼容全部充值档位。
*/
@@ -23,9 +24,9 @@ final class DepositChannel
*/
public static function codeRegistry(): array
{
// 仅保留 DDPay充值/回调只走网关文档约定,不再提供模拟或其它渠道码
$base = [
'ddpay' => ['name' => 'DDPay', 'name_en' => 'DDPay', 'sort' => 10],
'mock' => ['name' => '模拟支付', 'name_en' => 'Mock Pay', 'sort' => 5],
];
$extra = self::registryFromEnv();
foreach ($extra as $code => $meta) {
@@ -287,6 +288,9 @@ final class DepositChannel
if (!isset($registry[$code])) {
continue;
}
if ($code === MockPay::CHANNEL_CODE && !MockPay::isEnabled()) {
continue;
}
if ($fiatCurrencyCode !== '' && !self::isCurrencyAllowedForRow($row, $fiatCurrencyCode)) {
continue;
}
@@ -409,7 +413,12 @@ final class DepositChannel
*/
public static function withdrawPayoutChannelCodes(): array
{
return ['ddpay'];
$codes = ['ddpay'];
if (MockPay::isEnabled()) {
$codes[] = MockPay::CHANNEL_CODE;
}
return $codes;
}
/**

View File

@@ -27,4 +27,9 @@ class DepositOrder extends Model
{
return $this->belongsTo(Channel::class, 'channel_id', 'id');
}
public function reviewAdmin(): \think\model\relation\BelongsTo
{
return $this->belongsTo(\app\admin\model\Admin::class, 'review_admin_id', 'id');
}
}

View File

@@ -1,81 +1,127 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use support\think\Db;
/**
* 充值待支付订单超时处理:
* - 同用户最多允许 3 笔待支付订单
* - 待支付订单创建后 60 秒未支付,自动标记为失败并写失败原因
*/
final class DepositOrderExpireService
{
public const MAX_PENDING_DEPOSIT = 3;
public const EXPIRE_SECONDS = 60;
/**
* 超时失效处理。
*
* @param int|null $userId 仅处理某用户null 表示不过滤用户
* @param string|null $orderNo 仅处理某订单null 表示不过滤订单
*
* @return int 本次转失败的订单数
*/
public static function expirePendingOrders(?int $userId = null, ?string $orderNo = null): int
{
$expireBefore = time() - self::EXPIRE_SECONDS;
$query = Db::name('deposit_order')
->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();
}
}
<?php
declare(strict_types=1);
namespace app\common\service;
use support\think\Db;
/**
* 充值待支付订单超时处理:
* - 同用户最多允许 3 笔待支付订单
* - 待支付订单创建后超过有效期未支付,自动标记为失败
* - 有效秒数由 .env DEPOSIT_PENDING_EXPIRE_SECONDS 配置(默认 60全渠道统一
*/
final class DepositOrderExpireService
{
public const MAX_PENDING_DEPOSIT = 3;
/**
* 待支付充值单有效秒数(与 config app.deposit_pending_expire_seconds / .env 一致)
*/
public static function pendingExpireSeconds(): int
{
$cfg = config('app.deposit_pending_expire_seconds', 60);
if (is_numeric($cfg)) {
$v = intval($cfg);
if ($v > 0) {
return $v;
}
}
return 60;
}
/**
* @param array<string, mixed> $order
*/
public static function expireSecondsForOrder(array $order): int
{
return self::pendingExpireSeconds();
}
/**
* @param int|null $userId 仅处理某用户null 表示不过滤用户
* @param string|null $orderNo 仅处理某订单null 表示不过滤订单
*
* @return int 本次转失败的订单数
*/
public static function expirePendingOrders(?int $userId = null, ?string $orderNo = null): int
{
$query = Db::name('deposit_order')->where('status', 0);
if ($userId !== null && $userId > 0) {
$query->where('user_id', $userId);
}
if ($orderNo !== null && $orderNo !== '') {
$query->where('order_no', $orderNo);
}
$rows = $query->field(['id', 'remark', 'pay_channel', 'create_time'])->select()->toArray();
if ($rows === []) {
return 0;
}
$now = time();
$expireSec = self::pendingExpireSeconds();
$affectedCount = 0;