Files
lotteryLaravel/app/Http/Controllers/Api/V1/Wallet/WalletLogsController.php
kang d877b5e37a feat: 新增玩家管理与转账对账相关功能
1.  新增完整的后台玩家管理CRUD接口,包括列表、创建、详情、更新、删除
2.  新增转账订单冲正和人工处理功能,支持待对账订单状态变更
3.  扩展钱包流水和转账订单的状态支持,新增reversed、manually_processed等状态
4.  新增玩家API数据统一输出类,标准化玩家信息返回格式
2026-05-14 10:43:33 +08:00

189 lines
5.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Api\V1\Wallet;
use App\Models\WalletTxn;
use Illuminate\Support\Str;
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;
/**
* PRD §10.1.1`GET /api/v1/wallet/logs` — 钱包流水。
*
* Query`page`、`size`(每页条数,默认 20、`type`逗号分隔transfer_in,transfer_out,bet,prize,refund,reversal
*/
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' => ['bet'],
'prize' => ['prize'],
];
public function __invoke(Request $request): JsonResponse
{
$player = $request->lotteryPlayer();
abort_if($player === null, 500, 'lottery_player missing');
$perPage = $this->perPage($request, 'size', 20, 100);
$page = $this->page($request);
$pendingPayload = $this->pendingReconcilePayload((int) $player->id);
$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 ($bizFilter !== null) {
$query->whereIn('biz_type', $bizFilter);
}
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
$items = $paginator->getCollection()->map(fn (WalletTxn $txn) => $this->formatTxn($txn));
return ApiResponse::success([
'items' => $items,
'total' => $paginator->total(),
'page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'pending_reconcile' => $pendingPayload,
]);
}
/**
* @return list<array<string, mixed>>
*/
private function pendingReconcilePayload(int $playerId): array
{
return TransferOrder::query()
->where('player_id', $playerId)
->where('status', 'pending_reconcile')
->orderByDesc('id')
->limit(50)
->get()
->map(fn (TransferOrder $o) => $this->formatPendingOrder($o))
->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',
'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>
*/
private function formatPendingOrder(TransferOrder $order): array
{
$amount = (int) $order->amount;
return [
'transfer_no' => $order->transfer_no,
'direction' => $order->direction,
'type' => $order->direction === 'in' ? 'transfer_in' : 'transfer_out',
'currency_code' => $order->currency_code,
'amount' => $amount,
'amount_formatted' => CurrencyFormatter::fromMinor($amount),
'status' => $order->status,
'fail_reason' => $order->fail_reason,
'idempotent_key' => $order->idempotent_key,
'created_at' => $order->created_at?->toIso8601String(),
];
}
}