feat: add vip payment and member center
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:前台 VIP 支付控制器
|
||||
* 负责用户发起 VIP 支付、接收同步回调、接收异步通知并驱动本地会员开通
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\CreateVipPaymentOrderRequest;
|
||||
use App\Models\VipLevel;
|
||||
use App\Services\VipPaymentCenterClient;
|
||||
use App\Services\VipPaymentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use RuntimeException;
|
||||
|
||||
class VipPaymentController extends Controller
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param VipPaymentService $vipPaymentService 本地 VIP 支付服务
|
||||
* @param VipPaymentCenterClient $paymentCenterClient 远端支付中心客户端
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly VipPaymentService $vipPaymentService,
|
||||
private readonly VipPaymentCenterClient $paymentCenterClient,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建 VIP 支付订单并跳转到 NovaLink 支付页
|
||||
*
|
||||
* @param CreateVipPaymentOrderRequest $request 已校验的购买请求
|
||||
*/
|
||||
public function store(CreateVipPaymentOrderRequest $request): RedirectResponse
|
||||
{
|
||||
if (! $this->paymentCenterClient->isEnabled() || ! $this->paymentCenterClient->hasValidConfig()) {
|
||||
return redirect()->route('guide')->with('error', 'VIP 支付暂未开放,请联系管理员完成后台配置。');
|
||||
}
|
||||
|
||||
$vipLevel = VipLevel::query()->findOrFail((int) $request->validated('vip_level_id'));
|
||||
|
||||
try {
|
||||
// 先创建本地订单,再向支付中心发起下单,确保回调时有本地单据可追踪。
|
||||
$vipPaymentOrder = $this->vipPaymentService->createLocalOrder($request->user(), $vipLevel);
|
||||
$remoteOrder = $this->vipPaymentService->createRemoteOrder($vipPaymentOrder);
|
||||
$payUrl = (string) ($remoteOrder['pay_url'] ?? '');
|
||||
|
||||
if ($payUrl === '') {
|
||||
throw new RuntimeException('支付中心未返回可用的支付地址。');
|
||||
}
|
||||
|
||||
return redirect()->away($payUrl);
|
||||
} catch (\Throwable $exception) {
|
||||
return redirect()->route('guide')->with('error', '创建 VIP 支付订单失败:'.$exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付完成后的同步回调
|
||||
*
|
||||
* @param Request $request 支付中心跳转返回参数
|
||||
*/
|
||||
public function handleReturn(Request $request): RedirectResponse
|
||||
{
|
||||
$payload = $request->all();
|
||||
$vipPaymentOrder = $this->vipPaymentService->findByPaymentOrderNo($request->string('payment_order_no')->toString())
|
||||
?? $this->vipPaymentService->findByMerchantOrderNo($request->string('merchant_order_no')->toString());
|
||||
|
||||
if (! $vipPaymentOrder) {
|
||||
return redirect()->route('guide')->with('error', '未找到对应的 VIP 支付订单,请稍后在后台核对。');
|
||||
}
|
||||
|
||||
$this->vipPaymentService->recordSyncReturn($vipPaymentOrder, $payload);
|
||||
|
||||
try {
|
||||
// 同步回调只做页面回跳,但这里补查一次可让用户尽快看到最终结果。
|
||||
$vipPaymentOrder = $this->vipPaymentService->syncRemoteStatus($vipPaymentOrder);
|
||||
} catch (\Throwable $exception) {
|
||||
return redirect()->route('guide')->with('error', '支付结果正在确认中,请稍后刷新查看。');
|
||||
}
|
||||
|
||||
if ($vipPaymentOrder->isVipOpened()) {
|
||||
return redirect()->route('guide')->with('success', 'VIP 支付成功,会员已开通。');
|
||||
}
|
||||
|
||||
return redirect()->route('guide')->with('success', '支付页面已返回,系统正在确认支付结果,请稍后刷新查看。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收 NovaLink 支付中心的异步通知
|
||||
*
|
||||
* @param Request $request 支付中心回调请求
|
||||
*/
|
||||
public function notify(Request $request): Response
|
||||
{
|
||||
$rawBody = $request->getContent();
|
||||
$signature = (string) $request->header('X-Payment-Signature', '');
|
||||
|
||||
if (! $this->paymentCenterClient->isValidWebhookSignature($signature, $rawBody)) {
|
||||
return response('invalid signature', 401);
|
||||
}
|
||||
|
||||
$payload = $request->json()->all();
|
||||
$vipPaymentOrder = $this->vipPaymentService->findByPaymentOrderNo($payload['payment_order_no'] ?? null)
|
||||
?? $this->vipPaymentService->findByMerchantOrderNo($payload['merchant_order_no'] ?? null);
|
||||
|
||||
if (! $vipPaymentOrder) {
|
||||
return response('order not found', 404);
|
||||
}
|
||||
|
||||
if (($payload['status'] ?? '') !== 'paid') {
|
||||
return response('ignored', 200);
|
||||
}
|
||||
|
||||
try {
|
||||
// 异步回调才是最终支付成功依据,这里完成幂等开通 VIP 的核心逻辑。
|
||||
$this->vipPaymentService->markOrderAsPaid($vipPaymentOrder, $payload, 'async');
|
||||
|
||||
return response('success', 200);
|
||||
} catch (\Throwable $exception) {
|
||||
return response('error: '.$exception->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user