From d060e1b797f1aa825727d08abb70e65167b45f4e Mon Sep 17 00:00:00 2001 From: lkddi Date: Mon, 13 Apr 2026 17:25:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BE=AE=E4=BF=A1=E6=94=AF?= =?UTF-8?q?=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/VipPaymentController.php | 7 +- .../Requests/CreateVipPaymentOrderRequest.php | 13 ++-- app/Services/VipPaymentCenterClient.php | 7 +- app/Services/VipPaymentService.php | 23 +++++- .../chat/partials/layout/toolbar.blade.php | 34 +++++++-- resources/views/rooms/guide.blade.php | 12 +++- resources/views/vip/center.blade.php | 27 +++++-- .../Feature/VipPaymentIntegrationTest.php | 71 +++++++++++++++++-- 8 files changed, 165 insertions(+), 29 deletions(-) diff --git a/app/Http/Controllers/VipPaymentController.php b/app/Http/Controllers/VipPaymentController.php index 989b6b6..91d0abb 100644 --- a/app/Http/Controllers/VipPaymentController.php +++ b/app/Http/Controllers/VipPaymentController.php @@ -16,6 +16,10 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use RuntimeException; +/** + * 前台 VIP 支付控制器 + * 负责接收用户选择的支付渠道,下发支付中心订单并处理回调结果。 + */ class VipPaymentController extends Controller { /** @@ -41,10 +45,11 @@ class VipPaymentController extends Controller } $vipLevel = VipLevel::query()->findOrFail((int) $request->validated('vip_level_id')); + $provider = (string) $request->validated('provider'); try { // 先创建本地订单,再向支付中心发起下单,确保回调时有本地单据可追踪。 - $vipPaymentOrder = $this->vipPaymentService->createLocalOrder($request->user(), $vipLevel); + $vipPaymentOrder = $this->vipPaymentService->createLocalOrder($request->user(), $vipLevel, $provider); $remoteOrder = $this->vipPaymentService->createRemoteOrder($vipPaymentOrder); $payUrl = (string) ($remoteOrder['pay_url'] ?? ''); diff --git a/app/Http/Requests/CreateVipPaymentOrderRequest.php b/app/Http/Requests/CreateVipPaymentOrderRequest.php index a57bf20..f7affaf 100644 --- a/app/Http/Requests/CreateVipPaymentOrderRequest.php +++ b/app/Http/Requests/CreateVipPaymentOrderRequest.php @@ -8,7 +8,12 @@ namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Validator; +/** + * 创建 VIP 支付订单请求 + * 负责校验会员购买等级与支付渠道,确保下单时明确指定支付方式。 + */ class CreateVipPaymentOrderRequest extends FormRequest { /** @@ -28,16 +33,14 @@ class CreateVipPaymentOrderRequest extends FormRequest { return [ 'vip_level_id' => ['required', 'integer', 'exists:vip_levels,id'], + 'provider' => ['required', 'string', 'in:alipay,wechat'], ]; } /** * 配置验证器实例。 - * - * @param \Illuminate\Validation\Validator $validator - * @return void */ - public function withValidator($validator): void + public function withValidator(Validator $validator): void { $validator->after(function ($validator) { $user = $this->user(); @@ -71,6 +74,8 @@ class CreateVipPaymentOrderRequest extends FormRequest return [ 'vip_level_id.required' => '请选择要购买的 VIP 等级', 'vip_level_id.exists' => '所选 VIP 等级不存在或已被删除', + 'provider.required' => '请选择支付方式', + 'provider.in' => '当前支付方式不受支持,请重新选择', ]; } } diff --git a/app/Services/VipPaymentCenterClient.php b/app/Services/VipPaymentCenterClient.php index 2b811d8..a2bf244 100644 --- a/app/Services/VipPaymentCenterClient.php +++ b/app/Services/VipPaymentCenterClient.php @@ -13,6 +13,10 @@ use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; use RuntimeException; +/** + * NovaLink 支付中心客户端 + * 负责构建开放接口签名请求,并与远端支付中心完成下单和查单通信。 + */ class VipPaymentCenterClient { /** @@ -65,7 +69,8 @@ class VipPaymentCenterClient 'subject' => $vipPaymentOrder->subject, 'body' => '聊天室 VIP 会员购买:'.$vipPaymentOrder->vip_name, 'amount' => number_format((float) $vipPaymentOrder->amount, 2, '.', ''), - 'channel' => 'web', + // 支付中心要求商户显式指定 provider,不能依赖平台自动推断。 + 'provider' => $vipPaymentOrder->provider, 'business_type' => 'chatroom_vip', 'sync_return_url' => $syncReturnUrl, 'async_notify_url' => $asyncNotifyUrl, diff --git a/app/Services/VipPaymentService.php b/app/Services/VipPaymentService.php index 6138505..0b79d38 100644 --- a/app/Services/VipPaymentService.php +++ b/app/Services/VipPaymentService.php @@ -40,11 +40,13 @@ class VipPaymentService * * @param User $user 购买用户 * @param VipLevel $vipLevel 目标 VIP 等级 + * @param string $provider 用户选择的支付渠道 */ - public function createLocalOrder(User $user, VipLevel $vipLevel): VipPaymentOrder + public function createLocalOrder(User $user, VipLevel $vipLevel, string $provider): VipPaymentOrder { $currentVip = $user->isVip() ? $user->vipLevel : null; $isUpgrade = $currentVip && $vipLevel->isHigherThan($currentVip); + $provider = $this->normalizeProvider($provider); // 如果已经是该等级或更高级别,且不是永久会员续费(逻辑上续费应该用原价,但此处 user 需求是升级补差价) // 这里我们主要处理补差价升级逻辑。 @@ -73,7 +75,8 @@ class VipPaymentService 'status' => 'created', 'amount' => $price, 'subject' => ($isUpgrade ? '【升级】' : '购买').' VIP 会员 - '.$vipLevel->name, - 'provider' => 'alipay', + // 下单时必须固化用户选择的支付渠道,避免支付中心拒绝未指定 provider 的请求。 + 'provider' => $provider, 'vip_name' => $vipLevel->name, 'vip_duration_days' => (int) $vipLevel->duration_days, 'meta' => [ @@ -250,6 +253,22 @@ class VipPaymentService return 'VPO'.date('YmdHis').random_int(1000, 9999); } + /** + * 规范化并校验支付渠道 + * + * @param string $provider 前端提交的支付渠道 + */ + private function normalizeProvider(string $provider): string + { + $provider = trim(strtolower($provider)); + + if (! in_array($provider, ['alipay', 'wechat'], true)) { + throw new RuntimeException('当前支付方式不受支持。'); + } + + return $provider; + } + /** * 向用户当前在线的聊天室广播 VIP 购买成功喜报与烟花特效。 * diff --git a/resources/views/chat/partials/layout/toolbar.blade.php b/resources/views/chat/partials/layout/toolbar.blade.php index 1e0d381..b31c249 100644 --- a/resources/views/chat/partials/layout/toolbar.blade.php +++ b/resources/views/chat/partials/layout/toolbar.blade.php @@ -1695,11 +1695,24 @@ async function generateWechatBindCode() { / ${v.duration_days}天 ${showUpgradeInfo ? `
已省 ¥${(v.price - v.upgrade_price).toFixed(2)}
` : ''} - + ${!d.vipPaymentEnabled || isDisabled + ? `` + : `
+ + +
+
${btnText}后将跳转到对应支付页面
` + } `; @@ -1798,7 +1811,7 @@ async function generateWechatBindCode() { } } - window.buyVip = function(levelId) { + window.buyVip = function(levelId, provider = 'alipay') { // 这里我们模拟提交表单,因为支付逻辑通常需要页面跳转 // 修改为在新窗口打开支付,避免聊天室页面丢失 const form = document.createElement('form'); @@ -1818,6 +1831,12 @@ async function generateWechatBindCode() { idInput.value = levelId; form.appendChild(idInput); + const providerInput = document.createElement('input'); + providerInput.type = 'hidden'; + providerInput.name = 'provider'; + providerInput.value = provider; + form.appendChild(providerInput); + document.body.appendChild(form); form.submit(); document.body.removeChild(form); @@ -1825,7 +1844,8 @@ async function generateWechatBindCode() { // 提交后关闭弹窗并提示用户 closeVipModal(); if (window.chatDialog) { - window.chatDialog.alert('正在为您前往支付中心,请在新页面完成支付。', '支付提示', '#3b82f6'); + const providerText = provider === 'wechat' ? '微信支付二维码页' : '支付宝支付页'; + window.chatDialog.alert(`正在为您打开${providerText},请在新页面完成支付。`, '支付提示', '#3b82f6'); } }; diff --git a/resources/views/rooms/guide.blade.php b/resources/views/rooms/guide.blade.php index d365a4b..cb5aba9 100644 --- a/resources/views/rooms/guide.blade.php +++ b/resources/views/rooms/guide.blade.php @@ -406,12 +406,18 @@ @if ($vip->price > 0 && $vipPaymentEnabled)
+ class="inline-flex items-center gap-2"> @csrf - +
@elseif ($vip->price > 0) diff --git a/resources/views/vip/center.blade.php b/resources/views/vip/center.blade.php index 441eb30..d35c73f 100644 --- a/resources/views/vip/center.blade.php +++ b/resources/views/vip/center.blade.php @@ -165,14 +165,29 @@ @if ($vipPaymentEnabled) -
+ @csrf - + @if ($isDisabled) + + @else +
+ + +
+

+ {{ $btnText }}后,支付宝会打开支付页,微信会跳转到平台二维码页。 +

+ @endif
@endif diff --git a/tests/Feature/Feature/VipPaymentIntegrationTest.php b/tests/Feature/Feature/VipPaymentIntegrationTest.php index a5d2b0d..eaccd4c 100644 --- a/tests/Feature/Feature/VipPaymentIntegrationTest.php +++ b/tests/Feature/Feature/VipPaymentIntegrationTest.php @@ -60,14 +60,68 @@ class VipPaymentIntegrationTest extends TestCase $response = $this->actingAs($user)->post(route('vip.payment.store'), [ 'vip_level_id' => $vipLevel->id, + 'provider' => 'alipay', ]); $response->assertRedirect('https://novalink.test/payment/checkout/alipay/PO202604111200001234'); + Http::assertSent(function ($request) { + $data = $request->data(); + + return $request->url() === 'https://novalink.test/api/open/v1/pay/orders' + && ($data['provider'] ?? null) === 'alipay'; + }); $this->assertDatabaseHas('vip_payment_orders', [ 'user_id' => $user->id, 'vip_level_id' => $vipLevel->id, 'payment_order_no' => 'PO202604111200001234', 'status' => 'pending', + 'provider' => 'alipay', + ]); + } + + /** + * 测试用户可以选择微信支付并跳转到平台二维码页面 + */ + public function test_user_can_create_wechat_vip_payment_order_and_redirect_to_wechat_checkout_page(): void + { + $this->seedVipPaymentConfig(); + + $user = User::factory()->create(); + $vipLevel = VipLevel::factory()->create([ + 'name' => '黄金会员', + 'price' => 68, + 'duration_days' => 30, + ]); + + Http::fake([ + 'https://novalink.test/api/open/v1/pay/orders' => Http::response([ + 'success' => true, + 'message' => '支付单创建成功', + 'data' => [ + 'payment_order_no' => 'PO202604131530001234', + 'pay_url' => 'https://novalink.test/payment/checkout/wechat/PO202604131530001234', + ], + ], 201), + ]); + + $response = $this->actingAs($user)->post(route('vip.payment.store'), [ + 'vip_level_id' => $vipLevel->id, + 'provider' => 'wechat', + ]); + + $response->assertRedirect('https://novalink.test/payment/checkout/wechat/PO202604131530001234'); + Http::assertSent(function ($request) { + $data = $request->data(); + + return $request->url() === 'https://novalink.test/api/open/v1/pay/orders' + && ($data['provider'] ?? null) === 'wechat'; + }); + $this->assertDatabaseHas('vip_payment_orders', [ + 'user_id' => $user->id, + 'vip_level_id' => $vipLevel->id, + 'payment_order_no' => 'PO202604131530001234', + 'status' => 'pending', + 'provider' => 'wechat', ]); } @@ -89,6 +143,7 @@ class VipPaymentIntegrationTest extends TestCase 'user_id' => $user->id, 'vip_level_id' => $vipLevel->id, 'amount' => 88.00, + 'provider' => 'wechat', 'vip_name' => $vipLevel->name, 'vip_duration_days' => $vipLevel->duration_days, 'payment_order_no' => 'PO202604111530001234', @@ -100,8 +155,8 @@ class VipPaymentIntegrationTest extends TestCase 'merchant_order_no' => $order->merchant_order_no, 'status' => 'paid', 'amount' => '88.00', - 'provider' => 'alipay', - 'provider_trade_no' => '2026041122001499999999999999', + 'provider' => 'wechat', + 'provider_trade_no' => '4200002512202604139876543210', 'paid_at' => '2026-04-11 15:35:12', ]; @@ -146,6 +201,7 @@ class VipPaymentIntegrationTest extends TestCase $this->assertDatabaseHas('vip_payment_orders', [ 'id' => $order->id, 'status' => 'paid', + 'provider' => 'wechat', ]); } @@ -239,9 +295,14 @@ class VipPaymentIntegrationTest extends TestCase { Sysparam::updateOrCreate(['alias' => 'vip_payment_enabled'], ['body' => '1']); Sysparam::clearCache('vip_payment_enabled'); + Sysparam::updateOrCreate(['alias' => 'vip_payment_base_url'], ['body' => 'https://novalink.test']); + Sysparam::updateOrCreate(['alias' => 'vip_payment_app_key'], ['body' => 'chatroom-app']); + Sysparam::updateOrCreate(['alias' => 'vip_payment_app_secret'], ['body' => 'chatroom-secret']); + Sysparam::updateOrCreate(['alias' => 'vip_payment_timeout'], ['body' => '10']); - config(['services.novalink_payment_center.client_id' => 'chatroom-app']); - config(['services.novalink_payment_center.client_secret' => 'chatroom-secret']); - config(['services.novalink_payment_center.base_url' => 'https://novalink.test/api/open/v1']); + Sysparam::clearCache('vip_payment_base_url'); + Sysparam::clearCache('vip_payment_app_key'); + Sysparam::clearCache('vip_payment_app_secret'); + Sysparam::clearCache('vip_payment_timeout'); } }