feat: 增强玩家 API,新增 locale 和时间字段,更新钱包 API 以支持可用余额计算,添加错误码与多语言支持
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Player;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Player;
|
||||
use App\Models\PlayerWallet;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 后台:按玩家查询钱包余额(`player_wallets` 全币种)。
|
||||
*
|
||||
* 路由:`GET /api/v1/admin/players/{player}/wallets`
|
||||
*/
|
||||
final class PlayerWalletShowController extends Controller
|
||||
{
|
||||
public function __invoke(Player $player): JsonResponse
|
||||
{
|
||||
$wallets = PlayerWallet::query()
|
||||
->where('player_id', $player->id)
|
||||
->orderBy('wallet_type')
|
||||
->orderBy('currency_code')
|
||||
->get();
|
||||
|
||||
$rows = $wallets->map(static function (PlayerWallet $w): array {
|
||||
$bal = (int) $w->balance;
|
||||
$frozen = (int) $w->frozen_balance;
|
||||
|
||||
return [
|
||||
'id' => $w->id,
|
||||
'wallet_type' => $w->wallet_type,
|
||||
'currency_code' => $w->currency_code,
|
||||
'balance' => $bal,
|
||||
'frozen_balance' => $frozen,
|
||||
'available_balance' => max(0, $bal - $frozen),
|
||||
'status' => (int) $w->status,
|
||||
'version' => (int) $w->version,
|
||||
];
|
||||
})->values()->all();
|
||||
|
||||
return ApiResponse::success([
|
||||
'player' => [
|
||||
'id' => $player->id,
|
||||
'site_code' => $player->site_code,
|
||||
'site_player_id' => $player->site_player_id,
|
||||
'username' => $player->username,
|
||||
'nickname' => $player->nickname,
|
||||
'default_currency' => $player->default_currency,
|
||||
'status' => (int) $player->status,
|
||||
],
|
||||
'wallets' => $rows,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Wallet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\TransferOrder;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 后台:转账单列表(主站 ↔ 彩票 {@see transfer_orders})。
|
||||
* 检索口径对齐《01-界面文档》§5.11「钱包流水与对账」转账侧扩展。
|
||||
*
|
||||
* Query:
|
||||
* - `page`(默认 1)
|
||||
* - `per_page` 或 `size`(每页条数,默认 20,最大 100)
|
||||
* - `player_id`(可选,按玩家主键)
|
||||
* - `player_account`(可选,模糊匹配 `players.site_player_id` / `username`;与 `player_id` 同时传时以 `player_id` 为准)
|
||||
* - `transfer_no`(可选,模糊匹配本地单号)
|
||||
* - `external_ref_no`(可选,模糊匹配主站流水号)
|
||||
* - `created_from` / `created_to`(可选,`Y-m-d`,筛选创建时间)
|
||||
* - `status`(可选,逗号分隔:`processing`,`success`,`failed`,`pending_reconcile`)
|
||||
* - `abnormal=1`(仅看异常:processing / failed / pending_reconcile,与 `status` 同时出现时优先)
|
||||
*/
|
||||
final class TransferOrderListController extends Controller
|
||||
{
|
||||
private const ALLOWED_STATUS = ['processing', 'success', 'failed', 'pending_reconcile'];
|
||||
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$validated = validator($request->query(), [
|
||||
'page' => ['sometimes', 'integer', 'min:1'],
|
||||
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
||||
'size' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
||||
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||
'player_account' => ['sometimes', 'nullable', 'string', 'max:128'],
|
||||
'transfer_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
||||
'external_ref_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
||||
'created_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
||||
'created_to' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
||||
'status' => ['sometimes', 'nullable', 'string', 'max:256'],
|
||||
])->validate();
|
||||
|
||||
$perPage = (int) ($validated['per_page'] ?? $validated['size'] ?? 20);
|
||||
$perPage = min(100, max(1, $perPage));
|
||||
$page = max(1, (int) ($validated['page'] ?? 1));
|
||||
|
||||
$query = TransferOrder::query()
|
||||
->with(['player:id,site_code,site_player_id,username,nickname'])
|
||||
->orderByDesc('id');
|
||||
|
||||
if (! empty($validated['player_id'])) {
|
||||
$query->where('player_id', (int) $validated['player_id']);
|
||||
} elseif (! empty($validated['player_account'])) {
|
||||
$term = '%'.addcslashes(trim($validated['player_account']), '%_\\').'%';
|
||||
$query->whereHas('player', function ($q) use ($term): void {
|
||||
$q->where('site_player_id', 'like', $term)
|
||||
->orWhere('username', 'like', $term);
|
||||
});
|
||||
}
|
||||
|
||||
if (! empty($validated['transfer_no'])) {
|
||||
$q = '%'.addcslashes(trim($validated['transfer_no']), '%_\\').'%';
|
||||
$query->where('transfer_no', 'like', $q);
|
||||
}
|
||||
|
||||
if (! empty($validated['external_ref_no'])) {
|
||||
$q = '%'.addcslashes(trim($validated['external_ref_no']), '%_\\').'%';
|
||||
$query->where('external_ref_no', 'like', $q);
|
||||
}
|
||||
|
||||
if (! empty($validated['created_from'])) {
|
||||
$query->where('created_at', '>=', $validated['created_from'].' 00:00:00');
|
||||
}
|
||||
|
||||
if (! empty($validated['created_to'])) {
|
||||
$query->where('created_at', '<=', $validated['created_to'].' 23:59:59');
|
||||
}
|
||||
|
||||
if ($request->boolean('abnormal')) {
|
||||
$query->whereIn('status', ['processing', 'failed', 'pending_reconcile']);
|
||||
} elseif (! empty($validated['status'])) {
|
||||
$parts = array_filter(array_map('trim', explode(',', (string) $validated['status'])));
|
||||
$statuses = array_values(array_intersect($parts, self::ALLOWED_STATUS));
|
||||
if ($statuses !== []) {
|
||||
$query->whereIn('status', $statuses);
|
||||
}
|
||||
}
|
||||
|
||||
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
$items = $paginator->getCollection()->map(fn (TransferOrder $o) => $this->formatRow($o));
|
||||
|
||||
return ApiResponse::success([
|
||||
'items' => $items,
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatRow(TransferOrder $o): array
|
||||
{
|
||||
$p = $o->player;
|
||||
|
||||
return [
|
||||
'id' => $o->id,
|
||||
'transfer_no' => $o->transfer_no,
|
||||
'player_id' => $o->player_id,
|
||||
'site_code' => $p?->site_code,
|
||||
'site_player_id' => $p?->site_player_id,
|
||||
'username' => $p?->username,
|
||||
'nickname' => $p?->nickname,
|
||||
'direction' => $o->direction,
|
||||
'currency_code' => $o->currency_code,
|
||||
'amount' => (int) $o->amount,
|
||||
'idempotent_key' => $o->idempotent_key,
|
||||
'status' => $o->status,
|
||||
'external_ref_no' => $o->external_ref_no,
|
||||
'external_request_payload' => $o->external_request_payload,
|
||||
'external_response_payload' => $o->external_response_payload,
|
||||
'fail_reason' => $o->fail_reason,
|
||||
'created_at' => $o->created_at?->toIso8601String(),
|
||||
'finished_at' => $o->finished_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Wallet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\WalletTxn;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 后台:彩票钱包流水列表 {@see wallet_txns}。
|
||||
* 列表字段对齐《01-界面文档》§5.11;`updated_at` 作「完成时间」展示(`created_at` 为请求时间)。
|
||||
*
|
||||
* Query:
|
||||
* - `page`、`per_page` / `size`
|
||||
* - `player_id`(可选)
|
||||
* - `player_account`(可选,模糊匹配 `site_player_id` / `username`;与 `player_id` 同时传时以 `player_id` 为准)
|
||||
* - `txn_no`(可选,模糊匹配流水号)
|
||||
* - `external_ref_no`(可选,模糊匹配主站流水号)
|
||||
* - `created_from` / `created_to`(可选,`Y-m-d`,按 `created_at`)
|
||||
* - `biz_type`(可选)
|
||||
* - `status`(可选,逗号:`posted`,`pending_reconcile`)
|
||||
* - `abnormal=1`(仅 `pending_reconcile`,优先于 `status`)
|
||||
*/
|
||||
final class WalletTransactionListController extends Controller
|
||||
{
|
||||
private const ALLOWED_STATUS = ['posted', 'pending_reconcile'];
|
||||
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$validated = validator($request->query(), [
|
||||
'page' => ['sometimes', 'integer', 'min:1'],
|
||||
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
||||
'size' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
||||
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||
'player_account' => ['sometimes', 'nullable', 'string', 'max:128'],
|
||||
'txn_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
||||
'external_ref_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
||||
'created_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
||||
'created_to' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
||||
'biz_type' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||
'status' => ['sometimes', 'nullable', 'string', 'max:128'],
|
||||
])->validate();
|
||||
|
||||
$perPage = (int) ($validated['per_page'] ?? $validated['size'] ?? 20);
|
||||
$perPage = min(100, max(1, $perPage));
|
||||
$page = max(1, (int) ($validated['page'] ?? 1));
|
||||
|
||||
$query = WalletTxn::query()
|
||||
->with(['player:id,site_code,site_player_id,username,nickname'])
|
||||
->orderByDesc('id');
|
||||
|
||||
if (! empty($validated['player_id'])) {
|
||||
$query->where('player_id', (int) $validated['player_id']);
|
||||
} elseif (! empty($validated['player_account'])) {
|
||||
$term = '%'.addcslashes(trim($validated['player_account']), '%_\\').'%';
|
||||
$query->whereHas('player', function ($q) use ($term): void {
|
||||
$q->where('site_player_id', 'like', $term)
|
||||
->orWhere('username', 'like', $term);
|
||||
});
|
||||
}
|
||||
|
||||
if (! empty($validated['txn_no'])) {
|
||||
$q = '%'.addcslashes(trim($validated['txn_no']), '%_\\').'%';
|
||||
$query->where('txn_no', 'like', $q);
|
||||
}
|
||||
|
||||
if (! empty($validated['external_ref_no'])) {
|
||||
$q = '%'.addcslashes(trim($validated['external_ref_no']), '%_\\').'%';
|
||||
$query->where('external_ref_no', 'like', $q);
|
||||
}
|
||||
|
||||
if (! empty($validated['created_from'])) {
|
||||
$query->where('created_at', '>=', $validated['created_from'].' 00:00:00');
|
||||
}
|
||||
|
||||
if (! empty($validated['created_to'])) {
|
||||
$query->where('created_at', '<=', $validated['created_to'].' 23:59:59');
|
||||
}
|
||||
|
||||
if (! empty($validated['biz_type'])) {
|
||||
$query->where('biz_type', trim((string) $validated['biz_type']));
|
||||
}
|
||||
|
||||
if ($request->boolean('abnormal')) {
|
||||
$query->where('status', 'pending_reconcile');
|
||||
} elseif (! empty($validated['status'])) {
|
||||
$parts = array_filter(array_map('trim', explode(',', (string) $validated['status'])));
|
||||
$statuses = array_values(array_intersect($parts, self::ALLOWED_STATUS));
|
||||
if ($statuses !== []) {
|
||||
$query->whereIn('status', $statuses);
|
||||
}
|
||||
}
|
||||
|
||||
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
$items = $paginator->getCollection()->map(fn (WalletTxn $t) => $this->formatRow($t));
|
||||
|
||||
return ApiResponse::success([
|
||||
'items' => $items,
|
||||
'total' => $paginator->total(),
|
||||
'page' => $paginator->currentPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatRow(WalletTxn $t): array
|
||||
{
|
||||
$p = $t->player;
|
||||
|
||||
return [
|
||||
'id' => $t->id,
|
||||
'txn_no' => $t->txn_no,
|
||||
'player_id' => $t->player_id,
|
||||
'site_code' => $p?->site_code,
|
||||
'site_player_id' => $p?->site_player_id,
|
||||
'username' => $p?->username,
|
||||
'wallet_id' => $t->wallet_id,
|
||||
'biz_type' => $t->biz_type,
|
||||
'biz_no' => $t->biz_no,
|
||||
'direction' => (int) $t->direction,
|
||||
'amount' => (int) $t->amount,
|
||||
'balance_before' => (int) $t->balance_before,
|
||||
'balance_after' => (int) $t->balance_after,
|
||||
'status' => $t->status,
|
||||
'external_ref_no' => $t->external_ref_no,
|
||||
'idempotent_key' => $t->idempotent_key,
|
||||
'remark' => $t->remark,
|
||||
'created_at' => $t->created_at?->toIso8601String(),
|
||||
/** 展示用「完成时间」;无业务终态时可与 created_at 相同 */
|
||||
'updated_at' => $t->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ use Illuminate\Http\Request;
|
||||
* 鉴权自检:返回当前 Token 对应的玩家公开字段(不含密码)。
|
||||
*
|
||||
* 路由:GET /api/v1/player/me ,需 middleware lottery.player。
|
||||
*
|
||||
* 补充字段与 PRD「识别玩家」及前端引导一致:`locale` 为当次 API 实际使用的语言(与 `NegotiateLotteryLocale` 一致);
|
||||
* 时间类为 ISO 8601 字符串,便于 H5 展示与排错。
|
||||
*/
|
||||
class MeController extends Controller
|
||||
{
|
||||
@@ -28,6 +31,9 @@ class MeController extends Controller
|
||||
'nickname' => $player->nickname,
|
||||
'default_currency' => $player->default_currency,
|
||||
'status' => $player->status,
|
||||
'locale' => $request->lotteryLocale(),
|
||||
'last_login_at' => $player->last_login_at?->toIso8601String(),
|
||||
'created_at' => $player->created_at?->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use Illuminate\Http\Request;
|
||||
* 【行为要点】
|
||||
* - 币种:优先 Query `currency`,否则使用玩家 `default_currency`,再回退 `config('lottery.default_currency')`
|
||||
* - 若尚无 `player_wallets` 记录:按 `wallet_type=lottery` + 币种 **首次开立**一行(余额 0),便于新玩家直接进入查询
|
||||
* - `available_balance`:`balance - frozen_balance`,表示当前可用于下注的整数最小货币单位(不为负)
|
||||
* - `main_balance`:主站钱包余额占位,接入主站 API 后再返回实数;当前固定 `null`
|
||||
*/
|
||||
class WalletBalanceController extends Controller
|
||||
@@ -49,12 +50,16 @@ class WalletBalanceController extends Controller
|
||||
],
|
||||
);
|
||||
|
||||
$balance = (int) $wallet->balance;
|
||||
$frozen = (int) $wallet->frozen_balance;
|
||||
|
||||
return ApiResponse::success([
|
||||
'balance' => $wallet->balance,
|
||||
'balance' => $balance,
|
||||
'available_balance' => max(0, $balance - $frozen),
|
||||
'main_balance' => null,
|
||||
'currency_code' => $wallet->currency_code,
|
||||
'wallet_type' => $wallet->wallet_type,
|
||||
'frozen_balance' => $wallet->frozen_balance,
|
||||
'frozen_balance' => $frozen,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Wallet;
|
||||
|
||||
use App\Exceptions\WalletOperationException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Wallet\WalletTransferRequest;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Models\Player;
|
||||
use App\Services\Wallet\LotteryTransferService;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Support\LotteryMessage;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 转入:主站扣款 → 彩票钱包加款。
|
||||
*
|
||||
* `POST /api/v1/wallet/transfer-in` ,body JSON:`amount`(最小货币单位整数), `idempotent_key`, `currency`(可选)
|
||||
*/
|
||||
class WalletTransferInController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LotteryTransferService $transferService,
|
||||
) {}
|
||||
|
||||
public function __invoke(WalletTransferRequest $request): JsonResponse
|
||||
{
|
||||
$player = $request->lotteryPlayer();
|
||||
abort_if($player === null, 500, 'lottery_player missing');
|
||||
|
||||
$currency = $this->resolveCurrencyCode($request, $player);
|
||||
if ($currency instanceof JsonResponse) {
|
||||
return $currency;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->transferService->transferIn(
|
||||
$player,
|
||||
$currency,
|
||||
(int) $request->validated('amount'),
|
||||
(string) $request->validated('idempotent_key'),
|
||||
);
|
||||
} catch (WalletOperationException $e) {
|
||||
return ApiResponse::error(
|
||||
LotteryMessage::wallet($request, $e->lotteryCode),
|
||||
$e->lotteryCode,
|
||||
null,
|
||||
$e->httpStatus,
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse::success($data);
|
||||
}
|
||||
|
||||
private function resolveCurrencyCode(Request $request, Player $player): string|JsonResponse
|
||||
{
|
||||
$raw = $request->input('currency');
|
||||
if (is_string($raw) && $raw !== '') {
|
||||
$code = strtoupper(substr(trim($raw), 0, 16));
|
||||
} else {
|
||||
$fallback = $player->default_currency ?? config('lottery.default_currency', 'NPR');
|
||||
$code = strtoupper(substr(trim((string) $fallback), 0, 16));
|
||||
}
|
||||
|
||||
if (! preg_match('/^[A-Z0-9]{1,16}$/', $code)) {
|
||||
return ApiResponse::error(
|
||||
__('wallet.invalid_currency'),
|
||||
ErrorCode::WalletInvalidCurrency->value,
|
||||
null,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Wallet;
|
||||
|
||||
use App\Exceptions\WalletOperationException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Wallet\WalletTransferRequest;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Models\Player;
|
||||
use App\Services\Wallet\LotteryTransferService;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Support\LotteryMessage;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 转出:彩票钱包扣款 → 主站加款;主站失败则冲正彩票余额。
|
||||
*
|
||||
* `POST /api/v1/wallet/transfer-out`
|
||||
*/
|
||||
class WalletTransferOutController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LotteryTransferService $transferService,
|
||||
) {}
|
||||
|
||||
public function __invoke(WalletTransferRequest $request): JsonResponse
|
||||
{
|
||||
$player = $request->lotteryPlayer();
|
||||
abort_if($player === null, 500, 'lottery_player missing');
|
||||
|
||||
$currency = $this->resolveCurrencyCode($request, $player);
|
||||
if ($currency instanceof JsonResponse) {
|
||||
return $currency;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $this->transferService->transferOut(
|
||||
$player,
|
||||
$currency,
|
||||
(int) $request->validated('amount'),
|
||||
(string) $request->validated('idempotent_key'),
|
||||
);
|
||||
} catch (WalletOperationException $e) {
|
||||
return ApiResponse::error(
|
||||
LotteryMessage::wallet($request, $e->lotteryCode),
|
||||
$e->lotteryCode,
|
||||
null,
|
||||
$e->httpStatus,
|
||||
);
|
||||
}
|
||||
|
||||
return ApiResponse::success($data);
|
||||
}
|
||||
|
||||
private function resolveCurrencyCode(Request $request, Player $player): string|JsonResponse
|
||||
{
|
||||
$raw = $request->input('currency');
|
||||
if (is_string($raw) && $raw !== '') {
|
||||
$code = strtoupper(substr(trim($raw), 0, 16));
|
||||
} else {
|
||||
$fallback = $player->default_currency ?? config('lottery.default_currency', 'NPR');
|
||||
$code = strtoupper(substr(trim((string) $fallback), 0, 16));
|
||||
}
|
||||
|
||||
if (! preg_match('/^[A-Z0-9]{1,16}$/', $code)) {
|
||||
return ApiResponse::error(
|
||||
__('wallet.invalid_currency'),
|
||||
ErrorCode::WalletInvalidCurrency->value,
|
||||
null,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user