feat: 商城付费购买流 + 市场契约

- MarketPackage 模型/迁移:市场商品作为可购买实体(payable),复用 Payment 订单
- MarketPurchaseService:购买创建订单、已购判断、免费/付费门槛校验
- payment.paid 监听(核心注册,仅支付插件启用时)标记商品已购
- 市场契约:条目含 name/type/price(分)/download_url;插件市场 Tab 只显示 plugin、主题市场 Tab 只显示 theme;付费商品显示「购买」按钮与「已购买」徽章,未购买无法安装
- 新增主题市场 Tab(原仅插件页有市场)
- 测试:购买/沙箱支付标记已购/重复购买/安装门槛/免费条目/支付插件禁用
This commit is contained in:
ak
2026-08-12 01:34:04 +08:00
parent 4d63c0c683
commit e06e7c4300
11 changed files with 551 additions and 7 deletions
@@ -66,19 +66,32 @@
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem">
@forelse($marketItems as $index => $item)
@php $marketPaid = $item['price'] > 0; @endphp
<x-filament::section :compact="true">
<div class="flex flex-col">
<x-filament::icon icon="heroicon-o-cloud-arrow-down" class="h-8 w-8 text-primary-600" />
<div class="flex items-start justify-between gap-3">
<x-filament::icon icon="heroicon-o-cloud-arrow-down" class="h-8 w-8 text-primary-600" />
@if($marketPaid)
<x-filament::badge color="warning">¥{{ number_format($item['price'] / 100, 2) }}</x-filament::badge>
@else
<x-filament::badge color="success">免费</x-filament::badge>
@endif
</div>
<h3 class="mt-3 text-base font-semibold">{{ $item['title'] ?? $item['name'] }}</h3>
<p class="mt-1 flex-1 text-sm text-gray-500 dark:text-gray-400">{{ $item['description'] ?? '' }}</p>
<p class="mt-2 text-xs text-gray-400">{{ $item['version'] ?? '' }} · {{ $item['author'] ?? '' }}</p>
<div class="mt-4">
<x-filament::button size="sm" color="primary" wire:click="installFromMarket({{ $index }})">安装</x-filament::button>
<div class="mt-4 flex flex-wrap gap-2">
@if($marketPaid && ! $this->isMarketPurchased($index))
<x-filament::button size="sm" color="primary" wire:click="purchaseMarketItem({{ $index }})">购买</x-filament::button>
@elseif($marketPaid)
<x-filament::badge color="success">已购买</x-filament::badge>
@endif
<x-filament::button size="sm" color="gray" wire:click="installFromMarket({{ $index }})">安装</x-filament::button>
</div>
</div>
</x-filament::section>
@empty
<p class="text-sm text-gray-500">点击「刷新市场」拉取可用插件/主题</p>
<p class="text-sm text-gray-500">点击「刷新市场」拉取可用插件</p>
@endforelse
</div>
</x-filament::section>
@@ -64,4 +64,40 @@
<x-filament::button type="submit" color="primary">安装</x-filament::button>
</form>
</x-filament::section>
<x-filament::section heading="主题市场" :description="config('market.url') ?: '未配置远程市场(config/market.php 的 MARKET_URL),仅支持本地 ZIP 安装'">
<div class="mb-4">
<x-filament::button color="gray" wire:click="loadThemeMarket">刷新市场</x-filament::button>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem">
@forelse($marketItems as $index => $item)
@php $marketPaid = $item['price'] > 0; @endphp
<x-filament::section :compact="true">
<div class="flex flex-col">
<div class="flex items-start justify-between gap-3">
<x-filament::icon icon="heroicon-o-swatch" class="h-8 w-8 text-primary-600" />
@if($marketPaid)
<x-filament::badge color="warning">¥{{ number_format($item['price'] / 100, 2) }}</x-filament::badge>
@else
<x-filament::badge color="success">免费</x-filament::badge>
@endif
</div>
<h3 class="mt-3 text-base font-semibold">{{ $item['title'] ?? $item['name'] }}</h3>
<p class="mt-1 flex-1 text-sm text-gray-500 dark:text-gray-400">{{ $item['description'] ?? '' }}</p>
<p class="mt-2 text-xs text-gray-400">{{ $item['version'] ?? '' }} · {{ $item['author'] ?? '' }}</p>
<div class="mt-4 flex flex-wrap gap-2">
@if($marketPaid && ! $this->isThemeMarketPurchased($index))
<x-filament::button size="sm" color="primary" wire:click="purchaseThemeMarketItem({{ $index }})">购买</x-filament::button>
@elseif($marketPaid)
<x-filament::badge color="success">已购买</x-filament::badge>
@endif
<x-filament::button size="sm" color="gray" wire:click="installFromThemeMarket({{ $index }})">安装</x-filament::button>
</div>
</div>
</x-filament::section>
@empty
<p class="text-sm text-gray-500">点击「刷新市场」拉取可用主题</p>
@endforelse
</div>
</x-filament::section>
</x-filament-panels::page>