291 lines
13 KiB
PHP
291 lines
13 KiB
PHP
@extends('admin.layouts.app')
|
||
|
||
@section('title', '订阅管理')
|
||
@section('page_title', '订阅管理')
|
||
|
||
@section('content')
|
||
@php
|
||
// back 参数用于“返回上一页(保留上下文)”,但 back 本身不应再包含 back(避免无限嵌套导致 URL 膨胀)
|
||
// 注意:使用相对路径而非绝对 URL,避免不同 APP_URL 环境影响,以及 show 页 back 安全校验(要求以 / 开头)
|
||
$selfWithoutBack = \App\Support\BackUrl::selfWithoutBack();
|
||
|
||
$currentQuery = request()->query();
|
||
unset($currentQuery['back']);
|
||
$back = $selfWithoutBack;
|
||
|
||
// 用于构建“保留当前筛选上下文”的站内跳转链接(且不透传 back,避免嵌套/污染)
|
||
$buildSelfUrl = function (array $overrides = []) use ($currentQuery) {
|
||
$q = $currentQuery;
|
||
foreach ($overrides as $k => $v) {
|
||
if ($v === null) {
|
||
unset($q[$k]);
|
||
} else {
|
||
$q[$k] = $v;
|
||
}
|
||
}
|
||
|
||
$url = '/' . ltrim(request()->path(), '/');
|
||
if (count($q) > 0) {
|
||
$url .= '?' . \Illuminate\Support\Arr::query($q);
|
||
}
|
||
|
||
return $url;
|
||
};
|
||
|
||
// back 安全护栏(全页通用):
|
||
// - 仅允许站内相对路径(/ 开头)
|
||
// - 拒绝引号/尖括号(由于本页大量 href 采用 `{!! !!}` 原样输出,必须严控注入风险)
|
||
// - 拒绝 nested back=(避免 URL 膨胀/绕过)
|
||
$incomingBack = (string) request()->query('back', '');
|
||
$safeBackForLinks = \App\Support\BackUrl::sanitizeForLinks($incomingBack);
|
||
@endphp
|
||
<div class="card mb-20">
|
||
<p class="muted muted-tight">这里是总台视角的订阅目录页,承接“套餐 -> 订阅 -> 平台订单”的收费主链中间层。</p>
|
||
<p class="muted">当前阶段先做到:可访问列表、可筛选、统计摘要;后续再接:订阅激活服务 / 续费 / 取消 / 对账。</p>
|
||
|
||
@if($safeBackForLinks !== '')
|
||
<div class="mt-10">
|
||
<a href="{!! $safeBackForLinks !!}" class="muted">← 返回上一页(保留上下文)</a>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div class="card mb-20">
|
||
<h3>快捷筛选</h3>
|
||
<div class="muted mb-10">用于运营快速定位需要处理的订阅集合(口径基于筛选条件组合)。</div>
|
||
|
||
@php
|
||
// 快捷筛选:仅保留“上下文”字段(站点/套餐/keyword/安全 back),避免把其它筛选条件叠加导致空结果
|
||
$buildQuickFilterUrl = function (array $overrides) use ($safeBackForLinks) {
|
||
return \App\Support\BackUrl::currentPathQuickFilter(['merchant_id', 'plan_id', 'keyword'], $overrides, $safeBackForLinks);
|
||
};
|
||
|
||
// “全部”:清空筛选,但保留安全 back(用于返回来源页)
|
||
$allUrl = \App\Support\BackUrl::withBack('/admin/site-subscriptions', $safeBackForLinks);
|
||
@endphp
|
||
|
||
<div>
|
||
<a href="{!! $allUrl !!}" class="muted">全部</a>
|
||
<span class="muted">|</span>
|
||
<a href="{!! $buildQuickFilterUrl(['status' => 'activated', 'expiry' => null]) !!}" class="muted">已生效</a>
|
||
<span class="muted">|</span>
|
||
<a href="{!! $buildQuickFilterUrl(['status' => 'pending', 'expiry' => null]) !!}" class="muted">待生效</a>
|
||
<span class="muted">|</span>
|
||
<a href="{!! $buildQuickFilterUrl(['status' => 'cancelled', 'expiry' => null]) !!}" class="muted">已取消</a>
|
||
<span class="muted">|</span>
|
||
<a href="{!! $buildQuickFilterUrl(['status' => null, 'expiry' => 'expired']) !!}" class="muted">已过期</a>
|
||
<span class="muted">|</span>
|
||
<a href="{!! $buildQuickFilterUrl(['status' => null, 'expiry' => 'expiring_7d']) !!}" class="muted">7天内到期</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-20">
|
||
<h3>筛选条件</h3>
|
||
<form method="get" action="/admin/site-subscriptions" class="grid-4">
|
||
@if($safeBackForLinks !== '')
|
||
<input type="hidden" name="back" value="{!! $safeBackForLinks !!}">
|
||
@endif
|
||
<select name="status">
|
||
<option value="">全部状态</option>
|
||
@foreach(($filterOptions['statuses'] ?? []) as $value => $label)
|
||
<option value="{{ $value }}" @selected(($filters['status'] ?? '') === $value)>{{ $label }}</option>
|
||
@endforeach
|
||
</select>
|
||
<select name="expiry">
|
||
<option value="">全部到期状态</option>
|
||
<option value="expiring_7d" @selected(($filters['expiry'] ?? '') === 'expiring_7d')>7天内到期</option>
|
||
<option value="expired" @selected(($filters['expiry'] ?? '') === 'expired')>已过期</option>
|
||
</select>
|
||
<select name="merchant_id">
|
||
<option value="">全部站点</option>
|
||
@foreach(($merchants ?? []) as $merchant)
|
||
<option value="{{ $merchant->id }}" @selected(($filters['merchant_id'] ?? '') == $merchant->id)>{{ $merchant->name }}</option>
|
||
@endforeach
|
||
</select>
|
||
<select name="plan_id">
|
||
<option value="">全部套餐</option>
|
||
@foreach(($plans ?? []) as $plan)
|
||
<option value="{{ $plan->id }}" @selected(($filters['plan_id'] ?? '') == $plan->id)>{{ $plan->name }}</option>
|
||
@endforeach
|
||
</select>
|
||
<input name="keyword" placeholder="搜索订阅号 / 站点 / 套餐 / 计费周期" value="{{ $filters['keyword'] ?? '' }}">
|
||
<div>
|
||
<button class="btn btn-sm" type="submit">应用筛选</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="grid-4 mb-20">
|
||
<div class="card">
|
||
<h3>订阅总数</h3>
|
||
<div class="num-md">{{ $summaryStats['total_subscriptions'] ?? 0 }}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>已生效</h3>
|
||
<div class="num-md">{{ $summaryStats['activated_subscriptions'] ?? 0 }}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>待生效</h3>
|
||
<div class="num-md">{{ $summaryStats['pending_subscriptions'] ?? 0 }}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>已取消</h3>
|
||
<div class="num-md">{{ $summaryStats['cancelled_subscriptions'] ?? 0 }}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>已过期(按到期时间)</h3>
|
||
<div class="num-md">{{ $summaryStats['expired_subscriptions'] ?? 0 }}</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>7天内到期</h3>
|
||
<div class="num-md">{{ $summaryStats['expiring_7d_subscriptions'] ?? 0 }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-20">
|
||
<h3>工具</h3>
|
||
<div class="grid-2">
|
||
<form method="get" action="/admin/site-subscriptions/export" class="actions gap-10">
|
||
<input type="hidden" name="download" value="1">
|
||
<input type="hidden" name="status" value="{{ $filters['status'] ?? '' }}">
|
||
<input type="hidden" name="merchant_id" value="{{ $filters['merchant_id'] ?? '' }}">
|
||
<input type="hidden" name="plan_id" value="{{ $filters['plan_id'] ?? '' }}">
|
||
<input type="hidden" name="expiry" value="{{ $filters['expiry'] ?? '' }}">
|
||
<input type="hidden" name="keyword" value="{{ $filters['keyword'] ?? '' }}">
|
||
<button class="btn btn-secondary btn-sm" type="submit">导出当前筛选结果(CSV)</button>
|
||
</form>
|
||
|
||
@php
|
||
$q = [
|
||
'order_type' => 'renewal',
|
||
'back' => $selfWithoutBack,
|
||
];
|
||
if ((int) ($filters['merchant_id'] ?? 0) > 0) {
|
||
$q['merchant_id'] = (int) $filters['merchant_id'];
|
||
}
|
||
if ((int) ($filters['plan_id'] ?? 0) > 0) {
|
||
$q['plan_id'] = (int) $filters['plan_id'];
|
||
}
|
||
$createOrderFromSubIndexUrl = '/admin/platform-orders/create?' . \Illuminate\Support\Arr::query($q);
|
||
@endphp
|
||
<a class="btn btn-sm" href="{!! $createOrderFromSubIndexUrl !!}">创建续费订单(带当前筛选)</a>
|
||
</div>
|
||
<div class="muted muted-xs mt-6">用于运营从订阅目录快速补单/续费:会把当前 merchant_id/plan_id/site_subscription_id 作为默认值带到下单页。</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>订阅列表</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>订阅号</th>
|
||
<th>站点</th>
|
||
<th>套餐</th>
|
||
<th>状态</th>
|
||
<th>计费周期</th>
|
||
<th>周期(月)</th>
|
||
<th>金额</th>
|
||
<th>开始时间</th>
|
||
<th>到期时间</th>
|
||
<th>到期状态</th>
|
||
<th>关联订单数</th>
|
||
<th>生效时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse($subscriptions as $subscription)
|
||
<tr>
|
||
<td>{{ $subscription->id }}</td>
|
||
<td>
|
||
@php
|
||
$subShowUrl = '/admin/site-subscriptions/' . $subscription->id . '?' . \Illuminate\Support\Arr::query([
|
||
'back' => $back,
|
||
]);
|
||
@endphp
|
||
<a href="{!! $subShowUrl !!}">{{ $subscription->subscription_no }}</a>
|
||
@php
|
||
$remarkPrefix = (string) config('saasshop.platform_orders.renewal_order_remark_prefix', '来自订阅:');
|
||
$q = [
|
||
'order_type' => 'renewal',
|
||
'site_subscription_id' => $subscription->id,
|
||
'quantity' => 1,
|
||
'remark' => $remarkPrefix . $subscription->subscription_no,
|
||
'back' => $back,
|
||
];
|
||
if ((int) ($subscription->merchant_id ?? 0) > 0) {
|
||
$q['merchant_id'] = (int) $subscription->merchant_id;
|
||
}
|
||
if ((int) ($subscription->plan_id ?? 0) > 0) {
|
||
$q['plan_id'] = (int) $subscription->plan_id;
|
||
}
|
||
$renewOrderUrl = '/admin/platform-orders/create?' . \Illuminate\Support\Arr::query($q);
|
||
@endphp
|
||
<div class="mt-4">
|
||
<a class="btn btn-secondary btn-sm" href="{!! $renewOrderUrl !!}">续费下单</a>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
@if($subscription->merchant)
|
||
<a class="link" href="{!! $buildSelfUrl(['merchant_id' => $subscription->merchant->id, 'page' => null]) !!}">{{ $subscription->merchant->name }}</a>
|
||
@else
|
||
未关联站点
|
||
@endif
|
||
</td>
|
||
<td>
|
||
@if($subscription->plan)
|
||
<a class="link" href="{!! $buildSelfUrl(['plan_id' => $subscription->plan->id, 'page' => null]) !!}">{{ $subscription->plan_name ?: $subscription->plan->name }}</a>
|
||
@else
|
||
{{ $subscription->plan_name ?: '未设置' }}
|
||
@endif
|
||
</td>
|
||
<td>{{ ($statusLabels[$subscription->status] ?? $subscription->status) }} <span class="muted">({{ $subscription->status }})</span></td>
|
||
<td>{{ $subscription->billing_cycle ?: '-' }}</td>
|
||
<td>{{ $subscription->period_months }}</td>
|
||
<td>¥{{ number_format((float) $subscription->amount, 2) }}</td>
|
||
<td>{{ optional($subscription->starts_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
|
||
<td>{{ optional($subscription->ends_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
|
||
<td>
|
||
@php
|
||
$endsAt = $subscription->ends_at;
|
||
$expiryLabel = '无到期';
|
||
if ($endsAt) {
|
||
if ($endsAt->lt(now())) {
|
||
$expiryLabel = '已过期';
|
||
} elseif ($endsAt->lt(now()->addDays(7))) {
|
||
$expiryLabel = '7天内到期';
|
||
} else {
|
||
$expiryLabel = '未到期';
|
||
}
|
||
}
|
||
@endphp
|
||
{{ $expiryLabel }}
|
||
</td>
|
||
<td>
|
||
@php $cnt = (int) ($subscription->platform_orders_count ?? 0); @endphp
|
||
@if($cnt > 0)
|
||
@php
|
||
$platformOrdersUrl = '/admin/platform-orders?' . \Illuminate\Support\Arr::query([
|
||
'site_subscription_id' => $subscription->id,
|
||
'back' => $back,
|
||
]);
|
||
@endphp
|
||
<a href="{!! $platformOrdersUrl !!}">{{ $cnt }}</a>
|
||
@else
|
||
<span class="muted">0</span>
|
||
@endif
|
||
</td>
|
||
<td>{{ optional($subscription->activated_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="13" class="muted">暂无订阅数据,当前阶段先把订阅主表与总台目录立起来,后续再接订阅创建/激活/续费链路。</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="pagination-wrap">{{ $subscriptions->links() }}</div>
|
||
@endsection
|