feat: 增强玩家 API,新增 locale 和时间字段,更新钱包 API 以支持可用余额计算,添加错误码与多语言支持
This commit is contained in:
174
app/Http/Controllers/Api/V1/Wallet/WalletLogsController.php
Normal file
174
app/Http/Controllers/Api/V1/Wallet/WalletLogsController.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Wallet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\TransferOrder;
|
||||
use App\Models\WalletTxn;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* PRD §10.1.1:`GET /api/v1/wallet/logs` — 钱包流水。
|
||||
*
|
||||
* Query:`page`、`size`(每页条数,默认 20)、`type`(逗号分隔:transfer_in,transfer_out,bet,prize,refund)
|
||||
*/
|
||||
class WalletLogsController extends Controller
|
||||
{
|
||||
/** PRD 对外类型 → 本地 biz_type */
|
||||
private const TYPE_TO_BIZ = [
|
||||
'transfer_in' => ['transfer_in'],
|
||||
'transfer_out' => ['transfer_out'],
|
||||
'refund' => ['transfer_out_refund'],
|
||||
'bet' => ['bet'],
|
||||
'prize' => ['prize'],
|
||||
];
|
||||
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$player = $request->lotteryPlayer();
|
||||
abort_if($player === null, 500, 'lottery_player missing');
|
||||
|
||||
$perPage = min(100, max(1, (int) $request->query('size', $request->query('per_page', 20))));
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
|
||||
$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 ?? '';
|
||||
|
||||
return [
|
||||
'log_id' => $txn->txn_no,
|
||||
'type' => $this->bizToPublicType((string) $txn->biz_type),
|
||||
'biz_type' => $txn->biz_type,
|
||||
'amount' => $this->signedAmount($txn),
|
||||
'amount_abs' => (int) $txn->amount,
|
||||
'direction' => (int) $txn->direction === 1 ? 'in' : 'out',
|
||||
'currency_code' => $currency,
|
||||
'balance_after' => (int) $txn->balance_after,
|
||||
'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',
|
||||
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
|
||||
{
|
||||
return [
|
||||
'transfer_no' => $order->transfer_no,
|
||||
'direction' => $order->direction,
|
||||
'type' => $order->direction === 'in' ? 'transfer_in' : 'transfer_out',
|
||||
'currency_code' => $order->currency_code,
|
||||
'amount' => (int) $order->amount,
|
||||
'status' => $order->status,
|
||||
'fail_reason' => $order->fail_reason,
|
||||
'idempotent_key' => $order->idempotent_key,
|
||||
'created_at' => $order->created_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user