Files
lotteryLaravel/app/Http/Controllers/Api/V1/Wallet/WalletLogsController.php
kang c8c90e3e94 feat: 增强奖池与钱包管理功能
更新 AdminJackpotPoolUpdateController 校验规则,禁止传入 current_amount。
优化 AdminRiskPoolManualStatusController:更新奖池状态后同步 Redis 状态。
在 TransferOrderReconcileController 中新增 completeCredit 方法,用于处理卡住的转账订单对账。
调整 TransferOrderListController:优化转账订单处理条件。
在 TicketItemsIndexController 中实现支持时区的日期筛选,提升日期处理准确性。
扩展 JackpotPool 模型,新增 adjustments 关联关系。
改进票据与钱包相关服务中的错误处理和事务管理。
2026-05-26 14:58:41 +08:00

192 lines
6.1 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_deduct', 'bet', 'bet_reverse'],
'prize' => ['settle_payout', 'prize', 'jackpot_manual_payout'],
];
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',
'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>
*/
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(),
];
}
}