Files
chatroom/app/Services/VipPaymentService.php

210 lines
7.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能VIP 支付业务服务
* 负责本地 VIP 订单创建、同步远端支付单、处理回调并在支付成功后开通会员
*/
namespace App\Services;
use App\Models\User;
use App\Models\VipLevel;
use App\Models\VipPaymentOrder;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use RuntimeException;
class VipPaymentService
{
/**
* 构造函数
*
* @param VipPaymentCenterClient $paymentCenterClient 支付中心客户端
* @param VipService $vipService VIP 开通服务
*/
public function __construct(
private readonly VipPaymentCenterClient $paymentCenterClient,
private readonly VipService $vipService,
) {}
/**
* 创建本地 VIP 支付订单
*
* @param User $user 购买用户
* @param VipLevel $vipLevel 目标 VIP 等级
*/
public function createLocalOrder(User $user, VipLevel $vipLevel): VipPaymentOrder
{
if ((float) $vipLevel->price <= 0) {
throw new RuntimeException('当前 VIP 等级未设置在线支付价格,暂不支持直接购买。');
}
return VipPaymentOrder::create([
'order_no' => $this->generateOrderNo(),
'merchant_order_no' => $this->generateOrderNo(),
'user_id' => $user->id,
'vip_level_id' => $vipLevel->id,
'status' => 'created',
'amount' => $vipLevel->price,
'subject' => '购买 VIP 会员 - '.$vipLevel->name,
'provider' => 'alipay',
'vip_name' => $vipLevel->name,
'vip_duration_days' => (int) $vipLevel->duration_days,
'meta' => [
'username' => $user->username,
],
]);
}
/**
* 调用支付中心创建远端支付单
*
* @param VipPaymentOrder $vipPaymentOrder 本地 VIP 支付订单
* @return array<string, mixed>
*/
public function createRemoteOrder(VipPaymentOrder $vipPaymentOrder): array
{
$remoteOrder = $this->paymentCenterClient->createOrder(
$vipPaymentOrder,
route('vip.payment.return'),
route('vip.payment.notify')
);
// 将远端平台支付单号回填到本地订单,后续回调和补单都依赖它。
$vipPaymentOrder->update([
'payment_order_no' => (string) ($remoteOrder['payment_order_no'] ?? ''),
'status' => 'pending',
]);
return $remoteOrder;
}
/**
* 根据远端支付单号查找本地订单
*
* @param string|null $paymentOrderNo 远端平台支付单号
*/
public function findByPaymentOrderNo(?string $paymentOrderNo): ?VipPaymentOrder
{
if (! $paymentOrderNo) {
return null;
}
return VipPaymentOrder::query()->where('payment_order_no', $paymentOrderNo)->first();
}
/**
* 根据本地业务订单号查找本地订单
*
* @param string|null $merchantOrderNo 商户业务订单号
*/
public function findByMerchantOrderNo(?string $merchantOrderNo): ?VipPaymentOrder
{
if (! $merchantOrderNo) {
return null;
}
return VipPaymentOrder::query()->where('merchant_order_no', $merchantOrderNo)->first();
}
/**
* 在同步回调时主动补查远端状态
*
* @param VipPaymentOrder $vipPaymentOrder 本地 VIP 支付订单
*/
public function syncRemoteStatus(VipPaymentOrder $vipPaymentOrder): VipPaymentOrder
{
if (! $vipPaymentOrder->payment_order_no || $vipPaymentOrder->isVipOpened()) {
return $vipPaymentOrder;
}
$remoteOrder = $this->paymentCenterClient->queryOrder($vipPaymentOrder->payment_order_no);
if (($remoteOrder['status'] ?? null) === 'paid') {
return $this->markOrderAsPaid($vipPaymentOrder, $remoteOrder, 'sync');
}
if (($remoteOrder['status'] ?? null) === 'closed') {
$vipPaymentOrder->update(['status' => 'closed']);
}
return $vipPaymentOrder->fresh();
}
/**
* 记录同步回调原始参数
*
* @param VipPaymentOrder $vipPaymentOrder 本地 VIP 支付订单
* @param array<string, mixed> $payload 同步回调参数
*/
public function recordSyncReturn(VipPaymentOrder $vipPaymentOrder, array $payload): void
{
$vipPaymentOrder->update([
'sync_return_payload' => $payload,
]);
}
/**
* 根据异步通知将订单标记为已支付,并完成会员开通
*
* @param VipPaymentOrder $vipPaymentOrder 本地 VIP 支付订单
* @param array<string, mixed> $payload 支付中心回调数据
* @param string $source 触发来源sync|async
*/
public function markOrderAsPaid(VipPaymentOrder $vipPaymentOrder, array $payload, string $source = 'async'): VipPaymentOrder
{
return DB::transaction(function () use ($vipPaymentOrder, $payload, $source) {
$lockedOrder = VipPaymentOrder::query()
->with(['user', 'vipLevel'])
->lockForUpdate()
->findOrFail($vipPaymentOrder->id);
$amount = number_format((float) $lockedOrder->amount, 2, '.', '');
$callbackAmount = number_format((float) ($payload['amount'] ?? 0), 2, '.', '');
if ($amount !== $callbackAmount) {
throw new RuntimeException('支付金额校验失败,已拒绝开通会员。');
}
// 无论是同步还是异步,都保留原始回调数据,方便后台排查问题。
$lockedOrder->fill([
'payment_order_no' => (string) ($payload['payment_order_no'] ?? $lockedOrder->payment_order_no),
'provider' => (string) ($payload['provider'] ?? $lockedOrder->provider ?? 'alipay'),
'provider_trade_no' => (string) ($payload['provider_trade_no'] ?? $lockedOrder->provider_trade_no),
'status' => 'paid',
'paid_at' => isset($payload['paid_at']) ? Carbon::parse((string) $payload['paid_at']) : ($lockedOrder->paid_at ?? now()),
]);
if ($source === 'async') {
$lockedOrder->async_notify_payload = $payload;
}
if ($source === 'sync') {
$lockedOrder->sync_return_payload = array_merge($lockedOrder->sync_return_payload ?? [], $payload);
}
$lockedOrder->save();
if (! $lockedOrder->isVipOpened()) {
// 只在首次成功支付时开通会员,防止重复回调导致会员时长重复叠加。
$user = User::query()->lockForUpdate()->findOrFail($lockedOrder->user_id);
$this->vipService->grantVip($user, $lockedOrder->vip_level_id, (int) $lockedOrder->vip_duration_days);
$lockedOrder->update([
'opened_vip_at' => now(),
]);
}
return $lockedOrder->fresh(['user', 'vipLevel']);
});
}
/**
* 生成本地 VIP 订单号
*/
private function generateOrderNo(): string
{
return 'VPO'.date('YmdHis').random_int(1000, 9999);
}
}