更新 AdminJackpotPoolUpdateController 校验规则,禁止传入 current_amount。 优化 AdminRiskPoolManualStatusController:更新奖池状态后同步 Redis 状态。 在 TransferOrderReconcileController 中新增 completeCredit 方法,用于处理卡住的转账订单对账。 调整 TransferOrderListController:优化转账订单处理条件。 在 TicketItemsIndexController 中实现支持时区的日期筛选,提升日期处理准确性。 扩展 JackpotPool 模型,新增 adjustments 关联关系。 改进票据与钱包相关服务中的错误处理和事务管理。
192 lines
6.1 KiB
PHP
192 lines
6.1 KiB
PHP
<?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(),
|
||
];
|
||
}
|
||
}
|