feat: Payment 订单商品化——多态 payable 关联购买实体

- payments 表新增 payable_type/payable_id(nullableMorphs),订单可关联文章/套餐等可购买实体
- createOrder 接收 $payable 模型,取代 description 字符串解析
- membership 支付回调按 payable instanceof 分发(激活订阅 / 解锁单篇)
- Post/MembershipPlan 提供 payableLabel/payableUrl 约定方法;Payment 用约定方法生成展示标签与后台链接(皮肤等新实体可直接接入)
- 订单表格新增「类型」徽章(文章/会员套餐),商品列链接到对应后台编辑页;无 payable 的旧订单显示 —
This commit is contained in:
ak
2026-08-12 00:22:50 +08:00
parent 4f59afb7ce
commit ec087fe2a8
9 changed files with 178 additions and 36 deletions
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->nullableMorphs('payable');
});
}
public function down(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->dropMorphs('payable');
});
}
};
@@ -42,7 +42,22 @@ class PaymentResource extends Resource
->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('subject')->label('商品')
->limit(30)
->url(fn (Payment $record) => $record->payable_url),
\Filament\Tables\Columns\TextColumn::make('payable_type')->label('类型')
->badge()
->placeholder('—')
->formatStateUsing(fn ($state) => match (class_basename((string) $state)) {
'Post' => '文章',
'MembershipPlan' => '会员套餐',
default => $state ? class_basename((string) $state) : '—',
})
->color(fn ($state) => match (class_basename((string) $state)) {
'Post' => 'info',
'MembershipPlan' => 'success',
default => 'gray',
}),
\Filament\Tables\Columns\TextColumn::make('amount')->label('金额')
->formatStateUsing(fn ($state) => '¥'.number_format((float) $state / 100, 2)),
\Filament\Tables\Columns\TextColumn::make('channel')->label('渠道')->badge(),
@@ -61,7 +76,6 @@ class PaymentResource extends Resource
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')
@@ -8,11 +8,13 @@ namespace Plugins\Neatstudio\Payment\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Payment extends Model
{
protected $fillable = [
'order_no', 'user_id', 'subject', 'description', 'amount', 'channel', 'status', 'gateway_trade_no', 'payload', 'paid_at',
'payable_type', 'payable_id',
];
protected $casts = [
@@ -25,6 +27,40 @@ class Payment extends Model
return $this->belongsTo(User::class);
}
/**
* 可购买实体:文章(单篇解锁)、会员套餐、皮肤等。
*/
public function payable(): MorphTo
{
return $this->morphTo();
}
public function getPayableLabelAttribute(): string
{
$payable = $this->payable;
if (! $payable) {
return '—';
}
if (method_exists($payable, 'payableLabel')) {
return $payable->payableLabel();
}
return $payable->title ?? $payable->name ?? ('#'.$payable->getKey());
}
public function getPayableUrlAttribute(): ?string
{
$payable = $this->payable;
if (! $payable || ! method_exists($payable, 'payableUrl')) {
return null;
}
return $payable->payableUrl();
}
public function markPaid(string $gatewayTradeNo = null): void
{
$this->update([
@@ -7,6 +7,7 @@ namespace Plugins\Neatstudio\Payment\Services;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Plugins\Neatstudio\Payment\Models\Payment;
use Yansongda\Pay\Pay;
@@ -15,7 +16,10 @@ use Yansongda\Pay\Provider\Wechat;
class PaymentManager
{
public function createOrder(User $user, string $subject, int $amountCents, string $channel = 'alipay', string $description = null): Payment
/**
* 创建订单。$payable 为可购买实体(文章/套餐/皮肤),支付成功后经 payment.paid 钩子分发。
*/
public function createOrder(User $user, string $subject, int $amountCents, ?Model $payable = null, string $channel = 'alipay', string $description = null): Payment
{
$orderNo = date('YmdHis').Str::random(8);
@@ -27,6 +31,8 @@ class PaymentManager
'amount' => $amountCents,
'channel' => $channel,
'status' => 'pending',
'payable_type' => $payable?->getMorphClass(),
'payable_id' => $payable?->getKey(),
]);
}