feat: add vip payment and member center
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台 VIP 支付配置控制器
|
||||
* 用于管理聊天室对接 NovaLink 支付中心所需的开关、地址、App Key 与 App Secret
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\UpdateVipPaymentConfigRequest;
|
||||
use App\Models\SysParam;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class VipPaymentConfigController extends Controller
|
||||
{
|
||||
/**
|
||||
* 构造函数注入聊天室状态服务
|
||||
*
|
||||
* @param ChatStateService $chatState 系统参数缓存同步服务
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 显示 VIP 支付配置页
|
||||
*/
|
||||
public function edit(): View
|
||||
{
|
||||
$aliases = array_keys($this->fieldDescriptions());
|
||||
|
||||
// 仅读取 VIP 支付专属配置,避免与系统参数页重复展示。
|
||||
$params = SysParam::query()
|
||||
->whereIn('alias', $aliases)
|
||||
->pluck('body', 'alias')
|
||||
->toArray();
|
||||
|
||||
return view('admin.vip-payment.config', [
|
||||
'params' => $params,
|
||||
'descriptions' => $this->fieldDescriptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 VIP 支付配置并刷新缓存
|
||||
*
|
||||
* @param UpdateVipPaymentConfigRequest $request 已校验的后台配置请求
|
||||
*/
|
||||
public function update(UpdateVipPaymentConfigRequest $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$descriptions = $this->fieldDescriptions();
|
||||
|
||||
foreach ($descriptions as $alias => $guidetxt) {
|
||||
$body = (string) ($data[$alias] ?? '');
|
||||
|
||||
// 写入数据库并同步描述文案,确保后续后台与缓存读取一致。
|
||||
SysParam::updateOrCreate(
|
||||
['alias' => $alias],
|
||||
['body' => $body, 'guidetxt' => $guidetxt]
|
||||
);
|
||||
|
||||
$this->chatState->setSysParam($alias, $body);
|
||||
SysParam::clearCache($alias);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.vip-payment.edit')->with('success', 'VIP 支付配置已成功保存。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 VIP 支付字段说明文案
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function fieldDescriptions(): array
|
||||
{
|
||||
return [
|
||||
'vip_payment_enabled' => 'VIP 在线支付开关(1=开启,0=关闭)',
|
||||
'vip_payment_base_url' => 'NovaLink 支付中心地址(例如 https://novalink.test)',
|
||||
'vip_payment_app_key' => 'NovaLink 支付中心 App Key',
|
||||
'vip_payment_app_secret' => 'NovaLink 支付中心 App Secret',
|
||||
'vip_payment_timeout' => '调用支付中心超时时间(秒)',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台会员购买日志控制器
|
||||
* 负责展示聊天室 VIP 在线支付订单列表,并支持按用户、状态、订单号和日期筛选
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\VipPaymentOrder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class VipPaymentLogController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示会员购买日志列表
|
||||
*
|
||||
* @param Request $request 当前查询请求
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = VipPaymentOrder::query()->with(['user:id,username', 'vipLevel:id,name,color,icon']);
|
||||
|
||||
if ($request->filled('username')) {
|
||||
$username = (string) $request->input('username');
|
||||
|
||||
// 通过用户关联模糊匹配用户名,便于后台快速定位某个会员订单。
|
||||
$query->whereHas('user', function ($builder) use ($username): void {
|
||||
$builder->where('username', 'like', '%'.$username.'%');
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', (string) $request->input('status'));
|
||||
}
|
||||
|
||||
if ($request->filled('order_no')) {
|
||||
$keyword = (string) $request->input('order_no');
|
||||
$query->where(function ($builder) use ($keyword): void {
|
||||
$builder->where('order_no', 'like', '%'.$keyword.'%')
|
||||
->orWhere('merchant_order_no', 'like', '%'.$keyword.'%')
|
||||
->orWhere('payment_order_no', 'like', '%'.$keyword.'%')
|
||||
->orWhere('provider_trade_no', 'like', '%'.$keyword.'%');
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('date_start')) {
|
||||
$query->whereDate('created_at', '>=', (string) $request->input('date_start'));
|
||||
}
|
||||
|
||||
if ($request->filled('date_end')) {
|
||||
$query->whereDate('created_at', '<=', (string) $request->input('date_end'));
|
||||
}
|
||||
|
||||
$logs = $query->latest('id')->paginate(30)->withQueryString();
|
||||
|
||||
return view('admin.vip-payment-logs.index', [
|
||||
'logs' => $logs,
|
||||
'statusOptions' => [
|
||||
'created' => '待创建',
|
||||
'pending' => '待支付',
|
||||
'paid' => '已支付',
|
||||
'closed' => '已关闭',
|
||||
'failed' => '失败',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:前台会员中心控制器
|
||||
* 负责展示会员等级、权益说明、当前会员状态以及用户自己的购买记录
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\VipLevel;
|
||||
use App\Models\VipPaymentOrder;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class VipCenterController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示会员中心页面
|
||||
*
|
||||
* @param Request $request 当前请求对象
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// 会员等级按后台排序字段展示,方便用户对比不同档位权益。
|
||||
$vipLevels = VipLevel::query()
|
||||
->withCount('users')
|
||||
->orderByDesc('sort_order')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
// 仅展示当前用户自己的购买记录,避免泄露其他会员订单信息。
|
||||
$paymentLogs = $this->buildPaymentLogs($user->id);
|
||||
|
||||
$paidOrders = VipPaymentOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('status', 'paid')
|
||||
->count();
|
||||
|
||||
$totalAmount = (float) VipPaymentOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('status', 'paid')
|
||||
->sum('amount');
|
||||
|
||||
return view('vip.center', [
|
||||
'user' => $user,
|
||||
'vipLevels' => $vipLevels,
|
||||
'paymentLogs' => $paymentLogs,
|
||||
'vipPaymentEnabled' => Sysparam::getValue('vip_payment_enabled', '0') === '1',
|
||||
'paidOrders' => $paidOrders,
|
||||
'totalAmount' => $totalAmount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建当前用户的购买记录分页数据
|
||||
*
|
||||
* @param int $userId 当前登录用户 ID
|
||||
*/
|
||||
private function buildPaymentLogs(int $userId): LengthAwarePaginator
|
||||
{
|
||||
return VipPaymentOrder::query()
|
||||
->with('vipLevel:id,name,color,icon')
|
||||
->where('user_id', $userId)
|
||||
->latest('id')
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
}
|
||||
}
|
||||
@@ -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