M5: Workerman 常驻服务 + AI 审核/润色插件 + 支付插件(支付宝/微信/沙箱)+ 会员插件(套餐/订阅/付费文章)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Neatstudio\Payment\Filament\Pages;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class PaymentSettings extends Page
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static \UnitEnum|string|null $navigationGroup = '管理';
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-credit-card';
|
||||
|
||||
protected static ?string $navigationLabel = '支付设置';
|
||||
|
||||
protected string $view = 'filament.pages.plugin-settings';
|
||||
|
||||
public array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill([
|
||||
'pay_sandbox' => (int) Setting::get('pay_sandbox', 1) === 1,
|
||||
'pay_alipay_app_id' => Setting::get('pay_alipay_app_id', ''),
|
||||
'pay_alipay_app_secret' => Setting::get('pay_alipay_app_secret', ''),
|
||||
'pay_wechat_app_id' => Setting::get('pay_wechat_app_id', ''),
|
||||
'pay_wechat_mch_id' => Setting::get('pay_wechat_mch_id', ''),
|
||||
'pay_wechat_mch_secret' => Setting::get('pay_wechat_mch_secret', ''),
|
||||
]);
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make('通用')
|
||||
->schema([
|
||||
Toggle::make('pay_sandbox')->label('沙箱模式(无需真实密钥即可测试支付流程)')->default(true),
|
||||
]),
|
||||
Section::make('支付宝')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('pay_alipay_app_id')->label('App ID')->columnSpanFull(),
|
||||
TextInput::make('pay_alipay_app_secret')->label('应用私钥(app_secret_cert)')->password()->columnSpanFull(),
|
||||
]),
|
||||
Section::make('微信支付')
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('pay_wechat_app_id')->label('App ID'),
|
||||
TextInput::make('pay_wechat_mch_id')->label('商户号 MCH ID'),
|
||||
TextInput::make('pay_wechat_mch_secret')->label('API 密钥')->password()->columnSpanFull(),
|
||||
]),
|
||||
])
|
||||
->statePath('data');
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
Setting::set($key, is_bool($value) ? (string) (int) $value : (string) $value);
|
||||
}
|
||||
|
||||
Notification::make()->title('支付设置已保存')->success()->send();
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('save')->label('保存')->submit('save'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Neatstudio\Payment\Filament\Resources\Pages;
|
||||
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Plugins\Neatstudio\Payment\Filament\Resources\PaymentResource;
|
||||
|
||||
class ListPayments extends ListRecords
|
||||
{
|
||||
protected static string $resource = PaymentResource::class;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Neatstudio\Payment\Filament\Resources;
|
||||
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Plugins\Neatstudio\Payment\Filament\Resources\Pages\ListPayments;
|
||||
use Plugins\Neatstudio\Payment\Models\Payment;
|
||||
|
||||
class PaymentResource extends Resource
|
||||
{
|
||||
protected static \UnitEnum|string|null $navigationGroup = '管理';
|
||||
|
||||
protected static ?string $model = Payment::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedBanknotes;
|
||||
|
||||
protected static ?string $navigationLabel = '订单';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
\Filament\Forms\Components\TextInput::make('order_no')->label('订单号')->disabled(),
|
||||
\Filament\Forms\Components\TextInput::make('subject')->label('商品')->disabled(),
|
||||
\Filament\Forms\Components\TextInput::make('amount')->label('金额(分)')->numeric()->disabled(),
|
||||
\Filament\Forms\Components\TextInput::make('channel')->label('渠道')->disabled(),
|
||||
\Filament\Forms\Components\TextInput::make('status')->label('状态')->disabled(),
|
||||
\Filament\Forms\Components\DateTimePicker::make('paid_at')->label('支付时间')->disabled(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
\Filament\Tables\Columns\TextColumn::make('order_no')->label('订单号')->searchable(),
|
||||
\Filament\Tables\Columns\TextColumn::make('user.name')->label('用户'),
|
||||
\Filament\Tables\Columns\TextColumn::make('subject')->label('商品')->limit(30),
|
||||
\Filament\Tables\Columns\TextColumn::make('amount')->label('金额')
|
||||
->formatStateUsing(fn ($state) => '¥'.number_format((float) $state / 100, 2)),
|
||||
\Filament\Tables\Columns\TextColumn::make('channel')->label('渠道')->badge(),
|
||||
\Filament\Tables\Columns\TextColumn::make('status')->label('状态')
|
||||
->badge()
|
||||
->formatStateUsing(fn ($state) => match ($state) {
|
||||
'paid' => '已支付',
|
||||
'pending' => '待支付',
|
||||
'failed' => '失败',
|
||||
'closed' => '已关闭',
|
||||
default => $state,
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
'paid' => 'success',
|
||||
'pending' => 'warning',
|
||||
default => 'gray',
|
||||
}),
|
||||
\Filament\Tables\Columns\TextColumn::make('paid_at')->label('支付时间')->dateTime('Y-m-d H:i'),
|
||||
\Filament\Tables\Columns\TextColumn::make('created_at')->label('创建时间')->dateTime('Y-m-d H:i'),
|
||||
])
|
||||
->filters([
|
||||
\Filament\Tables\Filters\SelectFilter::make('status')
|
||||
->options(['paid' => '已支付', 'pending' => '待支付', 'failed' => '失败', 'closed' => '已关闭']),
|
||||
])
|
||||
->recordActions([])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListPayments::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Neatstudio\Payment\Http;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
use Plugins\Neatstudio\Payment\Models\Payment;
|
||||
use Plugins\Neatstudio\Payment\Services\PaymentManager;
|
||||
|
||||
class PaymentController
|
||||
{
|
||||
public function __construct(private PaymentManager $manager)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起支付:创建订单并跳转。
|
||||
*/
|
||||
public function checkout(Request $request, Payment $payment)
|
||||
{
|
||||
if ($payment->user_id !== auth()->id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$url = $this->manager->pay($payment);
|
||||
|
||||
return redirect()->away($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 沙箱模拟支付页。
|
||||
*/
|
||||
public function sandbox(string $orderNo)
|
||||
{
|
||||
$payment = Payment::where('order_no', $orderNo)->firstOrFail();
|
||||
|
||||
return theme_view('payments.sandbox', compact('payment'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 沙箱确认支付。
|
||||
*/
|
||||
public function sandboxConfirm(Request $request, string $orderNo)
|
||||
{
|
||||
$payment = Payment::where('order_no', $orderNo)->firstOrFail();
|
||||
|
||||
$this->manager->simulatePay($payment);
|
||||
|
||||
return redirect()->route('pay.result', $payment->order_no);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付结果页(沙箱 / 回跳共用)。
|
||||
*/
|
||||
public function result(string $orderNo)
|
||||
{
|
||||
$payment = Payment::where('order_no', $orderNo)->firstOrFail();
|
||||
|
||||
return theme_view('payments.result', compact('payment'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步回调(支付宝/微信 notify)。
|
||||
*/
|
||||
public function notify(Request $request, string $channel)
|
||||
{
|
||||
$result = $this->manager->handleNotify($channel, $request->all());
|
||||
|
||||
return response($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步回跳。
|
||||
*/
|
||||
public function return(Request $request, string $channel)
|
||||
{
|
||||
$orderNo = $request->input('out_trade_no');
|
||||
|
||||
if (! $orderNo) {
|
||||
return redirect()->route('home');
|
||||
}
|
||||
|
||||
return redirect()->route('pay.result', $orderNo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Neatstudio\Payment\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Payment extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'order_no', 'user_id', 'subject', 'description', 'amount', 'channel', 'status', 'gateway_trade_no', 'payload', 'paid_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'payload' => 'array',
|
||||
'paid_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function markPaid(string $gatewayTradeNo = null): void
|
||||
{
|
||||
$this->update([
|
||||
'status' => 'paid',
|
||||
'gateway_trade_no' => $gatewayTradeNo,
|
||||
'paid_at' => now(),
|
||||
]);
|
||||
|
||||
app(\App\Blog\Support\PluginManager::class)->doAction('payment.paid', $this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Neatstudio\Payment;
|
||||
|
||||
use App\Blog\Support\PluginManager;
|
||||
use App\Blog\Support\PluginServiceProvider;
|
||||
|
||||
class ServiceProvider extends PluginServiceProvider
|
||||
{
|
||||
protected function boot(PluginManager $manager): void
|
||||
{
|
||||
$this->loadRoutes(__DIR__.'/../routes/web.php');
|
||||
|
||||
$manager->addFilter('payment.gateway', function (array $gateways) {
|
||||
$gateways['alipay'] = '支付宝';
|
||||
$gateways['wechat'] = '微信支付';
|
||||
$gateways['sandbox'] = '沙箱模拟';
|
||||
|
||||
return $gateways;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Neatstudio\Payment\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
use Plugins\Neatstudio\Payment\Models\Payment;
|
||||
use Yansongda\Pay\Pay;
|
||||
use Yansongda\Pay\Provider\Alipay;
|
||||
use Yansongda\Pay\Provider\Wechat;
|
||||
|
||||
class PaymentManager
|
||||
{
|
||||
public function createOrder(User $user, string $subject, int $amountCents, string $channel = 'alipay', string $description = null): Payment
|
||||
{
|
||||
$orderNo = date('YmdHis').Str::random(8);
|
||||
|
||||
return Payment::create([
|
||||
'order_no' => $orderNo,
|
||||
'user_id' => $user->id,
|
||||
'subject' => $subject,
|
||||
'description' => $description,
|
||||
'amount' => $amountCents,
|
||||
'channel' => $channel,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起支付,返回跳转地址(沙箱模式返回模拟支付页)。
|
||||
*/
|
||||
public function pay(Payment $payment): string
|
||||
{
|
||||
$this->assertPending($payment);
|
||||
|
||||
if ($this->sandboxEnabled()) {
|
||||
return route('pay.sandbox', $payment->order_no);
|
||||
}
|
||||
|
||||
try {
|
||||
return match ($payment->channel) {
|
||||
'alipay' => $this->alipay()->wap([
|
||||
'out_trade_no' => $payment->order_no,
|
||||
'subject' => $payment->subject,
|
||||
'total_amount' => number_format($payment->amount / 100, 2),
|
||||
])->toArray()['h5_url'] ?? throw new \RuntimeException('支付宝未返回支付链接'),
|
||||
'wechat' => $this->wechat()->wap([
|
||||
'out_trade_no' => $payment->order_no,
|
||||
'description' => $payment->subject,
|
||||
'amount' => ['total' => $payment->amount],
|
||||
])->toArray()['h5_url'] ?? throw new \RuntimeException('微信未返回支付链接'),
|
||||
default => throw new \InvalidArgumentException('未知支付渠道'),
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
if (str_contains($e->getMessage(), '未返回支付链接')) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
throw new \RuntimeException('支付网关调用失败,请检查渠道配置或开启沙箱模式:'.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 沙箱模拟支付:直接标记订单已支付。
|
||||
*/
|
||||
public function simulatePay(Payment $payment): void
|
||||
{
|
||||
$this->assertPending($payment);
|
||||
$payment->markPaid('sandbox_'.Str::random(12));
|
||||
}
|
||||
|
||||
public function handleNotify(string $channel, array $params): string
|
||||
{
|
||||
if ($this->sandboxEnabled()) {
|
||||
return 'fail';
|
||||
}
|
||||
|
||||
try {
|
||||
$result = match ($channel) {
|
||||
'alipay' => $this->alipay()->callback($params),
|
||||
'wechat' => $this->wechat()->callback($params),
|
||||
default => throw new \InvalidArgumentException('未知回调渠道'),
|
||||
};
|
||||
|
||||
$orderNo = $result['out_trade_no'] ?? null;
|
||||
$tradeNo = $result['trade_no'] ?? null;
|
||||
|
||||
if ($orderNo) {
|
||||
$payment = Payment::where('order_no', $orderNo)->first();
|
||||
$payment?->markPaid($tradeNo);
|
||||
}
|
||||
|
||||
return 'success';
|
||||
} catch (\Throwable) {
|
||||
return 'fail';
|
||||
}
|
||||
}
|
||||
|
||||
public function sandboxEnabled(): bool
|
||||
{
|
||||
return (int) Setting::get('pay_sandbox', 1) === 1;
|
||||
}
|
||||
|
||||
public function alipay(): Alipay
|
||||
{
|
||||
return Pay::alipay([
|
||||
'default' => [
|
||||
'notify_url' => route('pay.notify', ['channel' => 'alipay']),
|
||||
'return_url' => route('pay.return', ['channel' => 'alipay']),
|
||||
],
|
||||
'app_id' => Setting::get('pay_alipay_app_id', ''),
|
||||
'app_secret_cert' => Setting::get('pay_alipay_app_secret', ''),
|
||||
'app_public_cert_path' => null,
|
||||
'alipay_public_cert_path' => null,
|
||||
'alipay_root_cert_path' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function wechat(): Wechat
|
||||
{
|
||||
return Pay::wechat([
|
||||
'default' => [
|
||||
'notify_url' => route('pay.notify', ['channel' => 'wechat']),
|
||||
'return_url' => route('pay.return', ['channel' => 'wechat']),
|
||||
],
|
||||
'mch_id' => Setting::get('pay_wechat_mch_id', ''),
|
||||
'mch_secret_key' => Setting::get('pay_wechat_mch_secret', ''),
|
||||
'mch_secret_cert' => null,
|
||||
'mch_public_cert_path' => null,
|
||||
'mp_app_id' => Setting::get('pay_wechat_app_id', ''),
|
||||
]);
|
||||
}
|
||||
|
||||
private function assertPending(Payment $payment): void
|
||||
{
|
||||
if ($payment->status !== 'pending') {
|
||||
throw new \RuntimeException('订单状态不是待支付');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user