feat: 更新开发环境配置,支持在 local 和 testing 环境下的玩家身份绕过,新增钱包余额 API 路由
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Wallet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Player;
|
||||
use App\Models\PlayerWallet;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 【玩家】查询彩票侧钱包余额。
|
||||
*
|
||||
* 路由:`GET /api/v1/wallet/balance`,需 middleware `lottery.player`。
|
||||
* PRD:`01-产品文档.md` §10.1.1;金额单位为最小货币 bigint(参见 `docs/04` §8)。
|
||||
*
|
||||
* 【行为要点】
|
||||
* - 币种:优先 Query `currency`,否则使用玩家 `default_currency`,再回退 `config('lottery.default_currency')`
|
||||
* - 若尚无 `player_wallets` 记录:按 `wallet_type=lottery` + 币种 **首次开立**一行(余额 0),便于新玩家直接进入查询
|
||||
* - `main_balance`:主站钱包余额占位,接入主站 API 后再返回实数;当前固定 `null`
|
||||
*/
|
||||
class WalletBalanceController extends Controller
|
||||
{
|
||||
private const WALLET_TYPE_LOTTERY = 'lottery';
|
||||
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$player = $request->lotteryPlayer();
|
||||
abort_if($player === null, 500, 'lottery_player missing');
|
||||
|
||||
$currencyCode = $this->resolveCurrencyCode($request, $player);
|
||||
if ($currencyCode instanceof JsonResponse) {
|
||||
return $currencyCode;
|
||||
}
|
||||
|
||||
$wallet = PlayerWallet::query()->firstOrCreate(
|
||||
[
|
||||
'player_id' => $player->id,
|
||||
'wallet_type' => self::WALLET_TYPE_LOTTERY,
|
||||
'currency_code' => $currencyCode,
|
||||
],
|
||||
[
|
||||
'balance' => 0,
|
||||
'frozen_balance' => 0,
|
||||
'status' => 0,
|
||||
'version' => 0,
|
||||
],
|
||||
);
|
||||
|
||||
return ApiResponse::success([
|
||||
'balance' => $wallet->balance,
|
||||
'main_balance' => null,
|
||||
'currency_code' => $wallet->currency_code,
|
||||
'wallet_type' => $wallet->wallet_type,
|
||||
'frozen_balance' => $wallet->frozen_balance,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|JsonResponse 合法币种码或错误响应(code 1003:参数非法)
|
||||
*/
|
||||
private function resolveCurrencyCode(Request $request, Player $player): string|JsonResponse
|
||||
{
|
||||
$raw = $request->query('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));
|
||||
}
|
||||
|
||||
// 币种码:字母数字,长度 1–16,与 migrations 字段一致
|
||||
if (! preg_match('/^[A-Z0-9]{1,16}$/', $code)) {
|
||||
// 业务码占用 1000–1999 钱包段;1003 已在 PRD 保留为「金额超出限制」,币种非法单用 1005
|
||||
return ApiResponse::error(
|
||||
__('wallet.invalid_currency'),
|
||||
1005,
|
||||
null,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ use Illuminate\Http\Request;
|
||||
* 从请求头解析玩家身份,返回已落库的 {@see Player}。
|
||||
*
|
||||
* 两种模式(互斥优先级:先判断 dev,再走 JWT):
|
||||
* 1) 开发绕过:仅当 `APP_ENV=local` 且 `LOTTERY_PLAYER_AUTH_DEV_BYPASS=true`,
|
||||
* 接受 `Authorization: Bearer dev:{players.id}`,直连主键查库(勿上生产)。
|
||||
* 1) 开发绕过:当 `LOTTERY_PLAYER_AUTH_DEV_BYPASS=true` 且运行环境为 `local` 或 **`testing`**(PHPUnit)时,
|
||||
* 接受 `Authorization: Bearer dev:{players.id}`,直连主键查库(**禁止在生产开启**)。
|
||||
* 2) 生产:使用 `MAIN_SITE_SSO_JWT_SECRET` 验签 JWT(默认 HS256),
|
||||
* 从 payload 读取 `site_code`、`site_player_id`(字段名可 env 覆盖)再查 players 表。
|
||||
*
|
||||
@@ -55,7 +55,7 @@ final class PlayerTokenResolver
|
||||
private function devBypassAllowed(): bool
|
||||
{
|
||||
return (bool) config('lottery.player_auth.dev_bypass')
|
||||
&& app()->environment('local');
|
||||
&& app()->environment(['local', 'testing']);
|
||||
}
|
||||
|
||||
private function resolveDevToken(string $token): Player
|
||||
|
||||
6
lang/en/wallet.php
Normal file
6
lang/en/wallet.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
/** PRD 钱包段多语言;NegotiateLotteryLocale 后由 __() 选用 */
|
||||
return [
|
||||
'invalid_currency' => 'Invalid currency code',
|
||||
];
|
||||
5
lang/ne/wallet.php
Normal file
5
lang/ne/wallet.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'invalid_currency' => 'मुद्रा कोड अमान्य',
|
||||
];
|
||||
5
lang/zh/wallet.php
Normal file
5
lang/zh/wallet.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'invalid_currency' => '币种参数不合法',
|
||||
];
|
||||
@@ -32,5 +32,6 @@
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
<env name="LOTTERY_PLAYER_AUTH_DEV_BYPASS" value="true"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
@@ -4,12 +4,14 @@ use App\Http\Controllers\Api\V1\Admin\PingController as AdminPingController;
|
||||
use App\Http\Controllers\Api\V1\HealthController;
|
||||
use App\Http\Controllers\Api\V1\Player\MeController;
|
||||
use App\Http\Controllers\Api\V1\Player\PingController as PlayerPingController;
|
||||
use App\Http\Controllers\Api\V1\Wallet\WalletBalanceController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
| Laravel 默认在 bootstrap 中为 api 文件加前缀 `api`,故实际 URL:
|
||||
| Laravel 在 bootstrap 中为本文件自动加前缀 `api` + 中间件组 `api`,故实际 URL:
|
||||
| /api/v1/health
|
||||
| /api/v1/player/...
|
||||
| /api/v1/wallet/...
|
||||
| /api/v1/admin/...
|
||||
*/
|
||||
|
||||
@@ -17,19 +19,32 @@ Route::prefix('v1')->group(function (): void {
|
||||
// 探活:无鉴权
|
||||
Route::get('health', HealthController::class)->name('api.v1.health');
|
||||
|
||||
// 玩家前缀下:仅 ping 公开;me 与 wallet/* 共用 lottery.player(见下方大组)
|
||||
Route::prefix('player')
|
||||
->name('api.v1.player.')
|
||||
->group(function (): void {
|
||||
// 不需 Bearer
|
||||
Route::get('ping', PlayerPingController::class)->name('ping');
|
||||
|
||||
// 需 Bearer:PlayerTokenResolver + EnsurePlayerApi
|
||||
Route::middleware('lottery.player')->group(function (): void {
|
||||
Route::get('me', MeController::class)->name('me');
|
||||
});
|
||||
});
|
||||
|
||||
// 后台 API 前缀;中间件 lottery.admin 内预留 Sanctum / RBAC
|
||||
/*
|
||||
| 已登录玩家:PRD 把路径拆成 `/v1/player/*` 与 `/v1/wallet/*`,这里用同一中间件块避免重复写。
|
||||
| 勿把 wallet 挂到 player 前缀下,否则会变成 /api/v1/player/wallet/...,与 PRD §10.1.1 不一致。
|
||||
*/
|
||||
Route::middleware('lottery.player')->group(function (): void {
|
||||
Route::prefix('player')
|
||||
->name('api.v1.player.')
|
||||
->group(function (): void {
|
||||
Route::get('me', MeController::class)->name('me');
|
||||
});
|
||||
|
||||
Route::prefix('wallet')
|
||||
->name('api.v1.wallet.')
|
||||
->group(function (): void {
|
||||
Route::get('balance', WalletBalanceController::class)->name('balance');
|
||||
});
|
||||
});
|
||||
|
||||
// 后台 API;lottery.admin 内预留 Sanctum / RBAC
|
||||
Route::middleware('lottery.admin')
|
||||
->prefix('admin')
|
||||
->name('api.v1.admin.')
|
||||
|
||||
49
tests/Feature/WalletBalanceTest.php
Normal file
49
tests/Feature/WalletBalanceTest.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\PlayerWallet;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('wallet balance creates lottery wallet row and returns zeros', function () {
|
||||
$player = Player::query()->create([
|
||||
'site_code' => 'test',
|
||||
'site_player_id' => 'p1',
|
||||
'username' => null,
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer dev:'.$player->id,
|
||||
'X-Locale' => 'zh',
|
||||
])->getJson('/api/v1/wallet/balance');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.balance', 0)
|
||||
->assertJsonPath('data.frozen_balance', 0)
|
||||
->assertJsonPath('data.currency_code', 'NPR')
|
||||
->assertJsonPath('data.wallet_type', 'lottery')
|
||||
->assertJsonPath('data.main_balance', null);
|
||||
|
||||
expect(PlayerWallet::query()->where('player_id', $player->id)->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('wallet balance rejects illegal currency query', function () {
|
||||
$player = Player::query()->create([
|
||||
'site_code' => 'test',
|
||||
'site_player_id' => 'p2',
|
||||
'username' => null,
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->getJson('/api/v1/wallet/balance?currency=!!')
|
||||
->assertStatus(400)
|
||||
->assertJsonPath('code', 1005);
|
||||
});
|
||||
Reference in New Issue
Block a user