From b60f3615c1761a5ab2cd7e9456014868917f3a13 Mon Sep 17 00:00:00 2001 From: pllx Date: Thu, 30 Apr 2026 10:02:59 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E5=BA=A7=E9=A9=BE=E8=B4=AD?= =?UTF-8?q?=E4=B9=B0=E9=87=91=E5=B8=81=E6=B5=81=E6=B0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Enums/CurrencySource.php | 4 +++ app/Http/Controllers/RideController.php | 2 +- app/Services/RideService.php | 33 +++++++++++++++++++++---- tests/Feature/RideControllerTest.php | 14 +++++++++++ tests/TestCase.php | 15 ++++++++--- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/app/Enums/CurrencySource.php b/app/Enums/CurrencySource.php index 68c9062..06c36e6 100644 --- a/app/Enums/CurrencySource.php +++ b/app/Enums/CurrencySource.php @@ -35,6 +35,9 @@ enum CurrencySource: string /** 商城购买消耗(扣除金币) */ case SHOP_BUY = 'shop_buy'; + /** 购买聊天室座驾消耗(扣除金币) */ + case RIDE_BUY = 'ride_buy'; + /** 管理员手动调整(后台直接修改经验/金币/魅力) */ case ADMIN_ADJUST = 'admin_adjust'; @@ -174,6 +177,7 @@ enum CurrencySource: string self::RECV_GIFT => '收到礼物', self::NEWBIE_BONUS => '新人礼包', self::SHOP_BUY => '商城购买', + self::RIDE_BUY => '座驾购买', self::ADMIN_ADJUST => '管理员调整', self::POSITION_REWARD => '职务奖励', self::SIGN_IN => '每日签到', diff --git a/app/Http/Controllers/RideController.php b/app/Http/Controllers/RideController.php index 340f50c..529629a 100644 --- a/app/Http/Controllers/RideController.php +++ b/app/Http/Controllers/RideController.php @@ -61,7 +61,7 @@ class RideController extends Controller } $item = Ride::query()->findOrFail((int) $request->integer('item_id')); - $result = $this->rideService->buy($user, $item); + $result = $this->rideService->buy($user, $item, $roomId); if (! $result['ok']) { return response()->json(['status' => 'error', 'message' => $result['message']], 400); diff --git a/app/Services/RideService.php b/app/Services/RideService.php index 16773db..f4b9d2a 100644 --- a/app/Services/RideService.php +++ b/app/Services/RideService.php @@ -8,6 +8,7 @@ namespace App\Services; +use App\Enums\CurrencySource; use App\Models\Ride; use App\Models\User; use App\Models\UserRidePurchase; @@ -22,6 +23,13 @@ use Illuminate\Support\Facades\DB; */ class RideService { + /** + * 构造座驾服务依赖。 + */ + public function __construct( + private readonly UserCurrencyService $currencyService, + ) {} + /** * 获取全部上架座驾商品。 * @@ -116,7 +124,7 @@ class RideService * * @return array{ok:bool, message:string, current_ride?:array} */ - public function buy(User $user, Ride $item): array + public function buy(User $user, Ride $item, ?int $roomId = null): array { if (! $item->is_active) { return ['ok' => false, 'message' => '该座驾暂未上架。']; @@ -131,7 +139,7 @@ class RideService return ['ok' => false, 'message' => "金币不足,购买【{$item->name}】需要 {$item->price} 金币,当前仅有 {$user->jjb} 金币。"]; } - DB::transaction(function () use ($user, $item, $days): void { + $purchased = DB::transaction(function () use ($user, $item, $days, $roomId): bool { $now = Carbon::now(); // 先清理已过期的 active 座驾,避免旧状态影响替换判断。 @@ -149,8 +157,17 @@ class RideService ->orderByDesc('expires_at') ->first(); - // 座驾购买必须先扣金币,后续续期或替换都在同一个事务内完成。 - $user->decrement('jjb', $item->price); + $balanceAfter = $this->currencyService->deductGoldIfEnough( + $user, + (int) $item->price, + CurrencySource::RIDE_BUY, + "购买聊天室座驾:{$item->name}", + $roomId, + ); + + if ($balanceAfter === null) { + return false; + } if ($activeRide && (int) $activeRide->ride_id === (int) $item->id) { $baseTime = $activeRide->expires_at && $activeRide->expires_at->greaterThan($now) @@ -167,7 +184,7 @@ class RideService 'expires_at' => $baseTime->copy()->addDays($days), ]); - return; + return true; } if ($activeRide) { @@ -182,8 +199,14 @@ class RideService 'price_paid' => $item->price, 'expires_at' => $now->copy()->addDays($days), ]); + + return true; }); + if (! $purchased) { + return ['ok' => false, 'message' => "金币不足,购买【{$item->name}】需要 {$item->price} 金币,当前仅有 {$user->fresh()->jjb} 金币。"]; + } + return [ 'ok' => true, 'message' => "购买成功!{$item->icon} {$item->name} 已激活({$days}天有效)。", diff --git a/tests/Feature/RideControllerTest.php b/tests/Feature/RideControllerTest.php index ea4525a..675c822 100644 --- a/tests/Feature/RideControllerTest.php +++ b/tests/Feature/RideControllerTest.php @@ -8,6 +8,7 @@ namespace Tests\Feature; +use App\Enums\CurrencySource; use App\Models\Ride; use App\Models\Room; use App\Models\User; @@ -80,6 +81,10 @@ class RideControllerTest extends TestCase 'user_id' => $user->id, 'ride_id' => $ride->id, ]); + $this->assertDatabaseMissing('user_currency_logs', [ + 'user_id' => $user->id, + 'source' => CurrencySource::RIDE_BUY->value, + ]); } /** @@ -108,6 +113,15 @@ class RideControllerTest extends TestCase 'status' => 'active', 'price_paid' => 18888, ]); + $this->assertDatabaseHas('user_currency_logs', [ + 'user_id' => $user->id, + 'currency' => 'gold', + 'amount' => -18888, + 'balance_after' => 11112, + 'source' => CurrencySource::RIDE_BUY->value, + 'remark' => '购买聊天室座驾:测试座驾', + 'room_id' => $room->id, + ]); } /** diff --git a/tests/TestCase.php b/tests/TestCase.php index 271fdc0..55483cc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,10 +16,17 @@ abstract class TestCase extends BaseTestCase */ protected function flushChatRoomRedisState(): void { - $keys = Redis::keys('room:*'); + $prefix = config('database.redis.options.prefix', ''); + $cursor = '0'; - if ($keys !== []) { - Redis::del(...$keys); - } + do { + [$cursor, $keys] = Redis::scan($cursor, ['match' => $prefix.'room:*', 'count' => 200]); + + foreach ($keys ?? [] as $fullKey) { + // Laravel Redis Facade 写入时会自动追加前缀,删除时要还原成业务短 key。 + $shortKey = $prefix ? substr((string) $fullKey, strlen($prefix)) : (string) $fullKey; + Redis::del($shortKey); + } + } while ($cursor !== '0'); } }