feat: 增强代理和玩家管理功能

- 在多个控制器中更新权限检查逻辑,确保管理员能够更灵活地管理代理和玩家。
- 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。
- 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。
- 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。
- 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义,提升代码复用性。
This commit is contained in:
2026-06-04 18:00:50 +08:00
parent 96545f87f6
commit a44679665d
183 changed files with 10054 additions and 857 deletions

View File

@@ -2,18 +2,17 @@
namespace App\Http\Controllers\Api\V1\Wallet;
use App\Models\WalletTxn;
use Illuminate\Support\Str;
use App\Models\TransferOrder;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use App\Models\TransferOrder;
use App\Support\PaginationTrait;
use Illuminate\Http\JsonResponse;
use App\Support\CurrencyFormatter;
use App\Http\Controllers\Controller;
use App\Services\Wallet\PlayerLedgerLogsService;
/**
* PRD §10.1.1`GET /api/v1/wallet/logs` 钱包流水
* PRD §10.1.1`GET /api/v1/wallet/logs` 钱包/信用流水(按玩家资金模式分表)
*
* Query`page``size`(每页条数,默认 20)、`type`逗号分隔transfer_in,transfer_out,bet,prize,refund,reversal
*/
@@ -21,15 +20,9 @@ final class WalletLogsController extends Controller
{
use PaginationTrait;
/** PRD 对外类型 → 本地 biz_type */
private const TYPE_TO_BIZ = [
'transfer_in' => ['transfer_in'],
'transfer_out' => ['transfer_out'],
'refund' => ['transfer_out_refund'],
'reversal' => ['reversal', 'bet_reverse'],
'bet' => ['bet_deduct', 'bet'],
'prize' => ['settle_payout', 'prize', 'jackpot_manual_payout'],
];
public function __construct(
private readonly PlayerLedgerLogsService $ledgerLogs,
) {}
public function __invoke(Request $request): JsonResponse
{
@@ -38,45 +31,21 @@ final class WalletLogsController extends Controller
$perPage = $this->perPage($request, 'size', 20, 100);
$page = $this->page($request);
$currencyCode = strtoupper(trim((string) $request->query('currency', '')));
$typeFilter = (string) $request->query('type', '');
$pendingPayload = $this->pendingReconcilePayload((int) $player->id, $currencyCode);
$bizFilter = $this->resolveBizTypeFilter((string) $request->query('type', ''));
if (is_array($bizFilter) && $bizFilter === []) {
return ApiResponse::success([
'items' => [],
'total' => 0,
'page' => $page,
'per_page' => $perPage,
'pending_reconcile' => $pendingPayload,
]);
}
$query = WalletTxn::query()
->where('player_id', $player->id)
->with('wallet')
->orderByDesc('id');
if ($currencyCode !== '') {
$query->whereHas('wallet', fn ($q) => $q->where('currency_code', $currencyCode));
}
if ($bizFilter !== null) {
$query->whereIn('biz_type', $bizFilter);
}
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
$items = $paginator->getCollection()->map(fn (WalletTxn $txn) => $this->formatTxn($txn));
$result = $this->ledgerLogs->listForPlayerApi($player, $page, $perPage, $currencyCode, $typeFilter);
return ApiResponse::success([
'items' => $items,
'total' => $paginator->total(),
'page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'items' => $result['items'],
'total' => $result['total'],
'page' => $result['page'],
'per_page' => $result['per_page'],
'ledger_source' => $result['ledger_source'],
'funding_mode' => $result['funding_mode'],
'auth_source' => $result['auth_source'],
'pending_reconcile' => $pendingPayload,
]);
}
@@ -97,84 +66,6 @@ final class WalletLogsController extends Controller
->all();
}
/**
* @return list<string>|null null 表示不过滤;空列表表示过滤后无合法 type结果应为空
*/
private function resolveBizTypeFilter(string $raw): ?array
{
$raw = trim($raw);
if ($raw === '') {
return null;
}
$parts = array_filter(array_map('trim', explode(',', $raw)));
if ($parts === []) {
return null;
}
$biz = [];
foreach ($parts as $p) {
$key = Str::lower($p);
if (! isset(self::TYPE_TO_BIZ[$key])) {
continue;
}
foreach (self::TYPE_TO_BIZ[$key] as $b) {
$biz[] = $b;
}
}
return array_values(array_unique($biz));
}
/**
* @return array<string, mixed>
*/
private function formatTxn(WalletTxn $txn): array
{
$currency = $txn->wallet?->currency_code ?? '';
$amount = (int) $txn->amount;
$balanceAfter = (int) $txn->balance_after;
return [
'log_id' => $txn->txn_no,
'type' => $this->bizToPublicType((string) $txn->biz_type),
'biz_type' => $txn->biz_type,
'amount' => $this->signedAmount($txn),
'amount_formatted' => CurrencyFormatter::fromMinor($amount),
'amount_abs' => $amount,
'amount_abs_formatted' => CurrencyFormatter::fromMinor($amount),
'direction' => (int) $txn->direction === 1 ? 'in' : 'out',
'currency_code' => $currency,
'balance_after' => $balanceAfter,
'balance_after_formatted' => CurrencyFormatter::fromMinor($balanceAfter),
'ref_id' => $txn->biz_no,
'idempotent_key' => $txn->idempotent_key,
'external_ref_no' => $txn->external_ref_no,
'status' => $txn->status,
'remark' => $txn->remark,
'created_at' => $txn->created_at?->toIso8601String(),
];
}
private function bizToPublicType(string $biz): string
{
return match ($biz) {
'transfer_out_refund' => 'refund',
'bet_deduct', 'bet' => 'bet',
'bet_reverse' => 'reversal',
'settle_payout', 'prize', 'jackpot_manual_payout' => 'prize',
'reversal' => 'reversal',
default => $biz,
};
}
private function signedAmount(WalletTxn $txn): int
{
$a = (int) $txn->amount;
return (int) $txn->direction === 1 ? $a : -$a;
}
/**
* @return array<string, mixed>
*/