610 lines
32 KiB
PHP
610 lines
32 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;
|
||
|
||
// 用于构建“保留当前筛选上下文”的站内跳转链接:统一抽到 BackUrl,避免 Blade 内闭包口径漂移。
|
||
// 说明:这里不透传 back(避免嵌套/污染)。
|
||
$buildSelfUrl = function (array $overrides = []) use ($currentQuery) {
|
||
return \App\Support\BackUrl::mergeQueryToCurrentPath($currentQuery, $overrides);
|
||
};
|
||
|
||
// back 安全护栏(全页通用):
|
||
// - 仅允许站内相对路径(/ 开头)
|
||
// - 拒绝引号/尖括号(由于本页大量 href 采用 `{!! !!}` 原样输出,必须严控注入风险)
|
||
// - 拒绝 nested back=(避免 URL 膨胀/绕过)
|
||
$incomingBack = (string) request()->query('back', '');
|
||
$safeBackForLinks = \App\Support\BackUrl::sanitizeForLinks($incomingBack);
|
||
|
||
// “从订单详情页来挑订阅”的治理交互:
|
||
// - attach_order_id:表示把选中的订阅绑定回某个订单
|
||
// - attach_back:绑定成功后回跳到哪里(通常是订单详情页)
|
||
$attachOrderId = (int) request()->query('attach_order_id', 0);
|
||
$safeAttachBackForLinks = \App\Support\BackUrl::sanitizeForLinks((string) request()->query('attach_back', ''));
|
||
|
||
// 用于摘要卡等入口:保留当前 query 并覆盖字段,同时安全透传 back。
|
||
$safeFullUrlWithQuery = function (array $overrides = []) use ($safeBackForLinks) {
|
||
return \App\Support\BackUrl::currentPathWithQuery($overrides, $safeBackForLinks);
|
||
};
|
||
|
||
$subscriptionIndexUrl = \App\Support\BackUrl::withBack('/admin/site-subscriptions', $safeBackForLinks);
|
||
@endphp
|
||
<div class="page-header mb-20" data-page="admin.site_subscriptions.index">
|
||
<div class="page-header-main">
|
||
<div>
|
||
<div class="page-header-title">订阅管理</div>
|
||
<div class="page-header-subtitle">这里是总台视角的订阅目录页,承接“套餐 -> 订阅 -> 平台订单”的收费主链中间层。当前阶段先做到:可访问列表、可筛选、统计摘要;后续再接:订阅激活服务 / 续费 / 取消 / 对账。</div>
|
||
</div>
|
||
|
||
<div class="page-header-actions">
|
||
@if($safeBackForLinks !== '')
|
||
<a href="{!! $safeBackForLinks !!}" class="btn btn-secondary btn-sm">返回上一页(保留上下文)</a>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</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>
|
||
<div class="muted mb-10">按到期时间(ends_at)快速定位需要续费/处理的订阅集合(不改变订阅 status 字段)。</div>
|
||
|
||
@if(($filters['expiry'] ?? '') === 'expiring_7d')
|
||
<div class="mt-10">
|
||
<div class="muted"><strong>7天内到期|站点维度提醒清单(Top10)</strong></div>
|
||
<div class="muted muted-xs mt-6">用于运营优先触达:按“到期订阅数”排序,同时展示最早到期时间(min_ends_at)。</div>
|
||
|
||
<table class="mt-10" data-role="expiring-7d-merchant-top10">
|
||
<thead>
|
||
<tr>
|
||
<th>站点</th>
|
||
<th>到期订阅数</th>
|
||
<th>最早到期</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse(($expiryMerchantRows ?? []) as $row)
|
||
@php
|
||
$mid = (int) ($row['merchant_id'] ?? 0);
|
||
$mname = (string) ($row['merchant_name'] ?? '');
|
||
$cnt = (int) ($row['count'] ?? 0);
|
||
$minEndsAt = (string) ($row['min_ends_at'] ?? '');
|
||
$merchantUrl = $buildSelfUrl(['merchant_id' => $mid, 'page' => null]);
|
||
@endphp
|
||
<tr>
|
||
<td>
|
||
<a class="link" href="{!! $merchantUrl !!}">{{ $mname !== '' ? $mname : ('站点#' . $mid) }}</a>
|
||
@php
|
||
$renewalCtaUrlByMerchant = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query([
|
||
'order_type' => 'renewal',
|
||
'require_subscription' => '1',
|
||
'merchant_id' => $mid,
|
||
]), $selfWithoutBack);
|
||
@endphp
|
||
<div class="muted muted-xs mt-6">
|
||
<a class="link" href="{!! $renewalCtaUrlByMerchant !!}">去创建续费订单</a>
|
||
<span class="muted">(建议:进入后按订阅维度续费)</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
{{ $cnt }}
|
||
@php
|
||
$barPct = 0;
|
||
if ((int) ($summaryStats['expiring_7d_subscriptions'] ?? 0) > 0) {
|
||
$barPct = min(100, max(0, round(((int) $cnt / (int) ($summaryStats['expiring_7d_subscriptions'] ?? 1)) * 100, 1)));
|
||
}
|
||
@endphp
|
||
@include('admin.components.mini_bar', [
|
||
'role' => 'expiry-merchant-bar',
|
||
'pct' => $barPct,
|
||
'title' => '占比 ' . $barPct . '%',
|
||
])
|
||
</td>
|
||
<td>{{ $minEndsAt !== '' ? $minEndsAt : '-' }}</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="3" class="muted">暂无数据</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="mt-16">
|
||
<div class="muted"><strong>7天内到期|站点 + 套餐维度提醒清单(Top10)</strong></div>
|
||
<div class="muted muted-xs mt-6">用于更精确续费下单:按到期订阅数排序,展示最早到期时间;并提供 merchant+plan 的续费下单入口。</div>
|
||
|
||
<table class="mt-10" data-role="expiring-7d-merchant-plan-top10">
|
||
<thead>
|
||
<tr>
|
||
<th>站点</th>
|
||
<th>套餐</th>
|
||
<th>到期订阅数</th>
|
||
<th>最早到期</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse(($expiryMerchantPlanRows ?? []) as $row)
|
||
@php
|
||
$mid = (int) ($row['merchant_id'] ?? 0);
|
||
$mname = (string) ($row['merchant_name'] ?? '');
|
||
$pid = (int) ($row['plan_id'] ?? 0);
|
||
$pname = (string) ($row['plan_name'] ?? '');
|
||
$cnt = (int) ($row['count'] ?? 0);
|
||
$minEndsAt = (string) ($row['min_ends_at'] ?? '');
|
||
$mpUrl = $buildSelfUrl(['merchant_id' => $mid, 'plan_id' => $pid, 'page' => null]);
|
||
|
||
$renewalCtaUrlByMerchantPlan = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query([
|
||
'order_type' => 'renewal',
|
||
'require_subscription' => '1',
|
||
'merchant_id' => $mid,
|
||
'plan_id' => $pid,
|
||
]), $selfWithoutBack);
|
||
@endphp
|
||
<tr>
|
||
<td><a class="link" href="{!! $mpUrl !!}">{{ $mname !== '' ? $mname : ('站点#' . $mid) }}</a></td>
|
||
<td>{{ $pname !== '' ? $pname : ('套餐#' . $pid) }}</td>
|
||
<td>
|
||
{{ $cnt }}
|
||
@php
|
||
$barPct = 0;
|
||
if ((int) ($summaryStats['expiring_7d_subscriptions'] ?? 0) > 0) {
|
||
$barPct = min(100, max(0, round(((int) $cnt / (int) ($summaryStats['expiring_7d_subscriptions'] ?? 1)) * 100, 1)));
|
||
}
|
||
@endphp
|
||
@include('admin.components.mini_bar', [
|
||
'role' => 'expiry-merchant-plan-bar',
|
||
'pct' => $barPct,
|
||
'title' => '占比 ' . $barPct . '%',
|
||
])
|
||
</td>
|
||
<td>
|
||
{{ $minEndsAt !== '' ? $minEndsAt : '-' }}
|
||
<div class="muted muted-xs mt-6">
|
||
<a class="link" href="{!! $renewalCtaUrlByMerchantPlan !!}">去创建续费订单</a>
|
||
<span class="muted">(merchant+plan 预填)</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="4" class="muted">暂无数据</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
@php
|
||
$expiredUrl = $buildQuickFilterUrl(['status' => null, 'expiry' => 'expired']);
|
||
$expiring7dUrl = $buildQuickFilterUrl(['status' => null, 'expiry' => 'expiring_7d']);
|
||
@endphp
|
||
|
||
<div class="actions">
|
||
<a class="btn btn-secondary btn-sm" href="{!! $expiredUrl !!}">已过期({{ $summaryStats['expired_subscriptions'] ?? 0 }})</a>
|
||
<a class="btn btn-secondary btn-sm" href="{!! $expiring7dUrl !!}">7天内到期({{ $summaryStats['expiring_7d_subscriptions'] ?? 0 }})</a>
|
||
|
||
@php
|
||
// 当已处于“到期集合”视图时,补一个就近的续费下单入口(带回退到当前列表的 back)。
|
||
$isExpiryView = in_array((string) ($filters['expiry'] ?? ''), ['expired', 'expiring_7d'], true);
|
||
$renewalCtaUrl = '';
|
||
if ($isExpiryView) {
|
||
$q = [
|
||
'order_type' => 'renewal',
|
||
// 续费单必须绑定订阅:集合入口也应尽可能引导到订阅维度续费(批量另行建设)。
|
||
'require_subscription' => '1',
|
||
];
|
||
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'];
|
||
}
|
||
$renewalCtaUrl = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query($q), $selfWithoutBack);
|
||
}
|
||
@endphp
|
||
@if($isExpiryView)
|
||
<a class="btn btn-sm" href="{!! $renewalCtaUrl !!}">创建续费订单(当前集合)</a>
|
||
<span class="muted muted-xs">(建议:进入后按订阅维度续费)</span>
|
||
@endif
|
||
</div>
|
||
|
||
<div class="muted muted-xs mt-6">建议:先处理“7天内到期”续费触达,再处理“已过期”补单或关闭。</div>
|
||
</div>
|
||
|
||
<details class="card mb-20 collapsible filters-card" data-role="collapsible" data-storage-key="admin.site_subscriptions.filters" open>
|
||
<summary class="collapsible-summary filters-summary">
|
||
<div class="flex-between items-center">
|
||
<h3 class="mb-0">筛选条件</h3>
|
||
<span class="muted muted-xs">点击收起/展开</span>
|
||
</div>
|
||
</summary>
|
||
<div class="collapsible-body filters-body">
|
||
<form method="get" action="/admin/site-subscriptions" class="grid-4 filters-grid">
|
||
|
||
@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>
|
||
<div>
|
||
<input type="date" name="ends_from" data-role="sub-ends-from" placeholder="到期时间从" value="{{ $filters['ends_from'] ?? '' }}" class="w-180">
|
||
<div class="muted muted-xs mt-6">
|
||
@php
|
||
// 到期区间快捷入口:日期范围由 Controller 注入,避免 Blade 内多次 now() 调用导致跨天漂移。
|
||
$endsToday = (string) ($endsRangeToday ?? now()->format('Y-m-d'));
|
||
$ends7d = (string) ($endsRange7d ?? now()->addDays(7)->format('Y-m-d'));
|
||
$ends30d = (string) ($endsRange30d ?? now()->addDays(30)->format('Y-m-d'));
|
||
|
||
// 到期区间快捷入口:应保留当前筛选上下文(status/expiry/merchant/plan/keyword/back),仅覆盖 ends_from/ends_to,并清空 page。
|
||
$endsQuickTodayUrl = $safeFullUrlWithQuery(['ends_from' => $endsToday, 'ends_to' => $endsToday, 'page' => null]);
|
||
$endsQuick7dUrl = $safeFullUrlWithQuery(['ends_from' => $endsToday, 'ends_to' => $ends7d, 'page' => null]);
|
||
$endsQuick30dUrl = $safeFullUrlWithQuery(['ends_from' => $endsToday, 'ends_to' => $ends30d, 'page' => null]);
|
||
$endsQuickClearUrl = $safeFullUrlWithQuery(['ends_from' => null, 'ends_to' => null, 'page' => null]);
|
||
@endphp
|
||
<a class="link" href="{!! $endsQuickTodayUrl !!}">今天到期</a>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $endsQuick7dUrl !!}">近7天到期</a>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $endsQuick30dUrl !!}">近30天到期</a>
|
||
<span class="muted">|</span>
|
||
<a class="link" href="{!! $endsQuickClearUrl !!}">清空到期区间</a>
|
||
</div>
|
||
</div>
|
||
<input type="date" name="ends_to" data-role="sub-ends-to" placeholder="到期时间到" value="{{ $filters['ends_to'] ?? '' }}" class="w-180">
|
||
<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>
|
||
</details>
|
||
|
||
<div class="grid-4 mb-20">
|
||
<div class="card">
|
||
<h3>订阅总数</h3>
|
||
<div class="num-md">
|
||
<a class="link" href="{!! $subscriptionIndexUrl !!}">{{ $summaryStats['total_subscriptions'] ?? 0 }}</a>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>已生效</h3>
|
||
<div class="num-md">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['status' => 'activated', 'page' => null]) !!}">{{ $summaryStats['activated_subscriptions'] ?? 0 }}</a>
|
||
</div>
|
||
<div class="muted muted-xs">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['status' => 'pending', 'page' => null]) !!}">查看待生效</a>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>待生效</h3>
|
||
<div class="num-md">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['status' => 'pending', 'page' => null]) !!}">{{ $summaryStats['pending_subscriptions'] ?? 0 }}</a>
|
||
</div>
|
||
<div class="muted muted-xs">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['status' => 'activated', 'page' => null]) !!}">查看已生效</a>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>已取消</h3>
|
||
<div class="num-md">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['status' => 'cancelled', 'page' => null]) !!}">{{ $summaryStats['cancelled_subscriptions'] ?? 0 }}</a>
|
||
</div>
|
||
<div class="muted muted-xs">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['status' => 'activated', 'page' => null]) !!}">查看已生效</a>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>已过期(按到期时间)</h3>
|
||
<div class="num-md">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['expiry' => 'expired', 'page' => null]) !!}">{{ $summaryStats['expired_subscriptions'] ?? 0 }}</a>
|
||
</div>
|
||
<div class="muted muted-xs">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['expiry' => 'expiring_7d', 'page' => null]) !!}">查看7天内到期</a>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>7天内到期</h3>
|
||
<div class="num-md">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['expiry' => 'expiring_7d', 'page' => null]) !!}">{{ $summaryStats['expiring_7d_subscriptions'] ?? 0 }}</a>
|
||
</div>
|
||
<div class="muted muted-xs">
|
||
<a class="link" href="{!! $safeFullUrlWithQuery(['expiry' => 'expired', 'page' => null]) !!}">查看已过期</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-20">
|
||
<h3>工具</h3>
|
||
|
||
@php
|
||
$batchMarkExpiredEnabled = (string) ($filters['expiry'] ?? '') === 'expired';
|
||
$batchMarkExpiredReason = $batchMarkExpiredEnabled ? '' : '请先进入「已过期(expiry=expired)」集合后再执行批量标记。';
|
||
@endphp
|
||
|
||
<div class="grid-2">
|
||
<form method="get" action="/admin/site-subscriptions/export" class="actions gap-10">
|
||
<input type="hidden" name="download" value="1">
|
||
@include('admin.components.filters_hidden_inputs', [
|
||
'filters' => $filters ?? [],
|
||
'keys' => ['status', 'merchant_id', 'plan_id', 'expiry', 'ends_from', 'ends_to', 'keyword'],
|
||
])
|
||
<button class="btn btn-secondary btn-sm" type="submit">导出 CSV</button>
|
||
</form>
|
||
|
||
@php
|
||
$q = [
|
||
'order_type' => 'renewal',
|
||
'require_subscription' => '1',
|
||
];
|
||
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 = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query($q), $selfWithoutBack);
|
||
@endphp
|
||
@if(!($isExpiryView ?? false) && $attachOrderId <= 0)
|
||
<a class="btn btn-sm" href="{!! $createOrderFromSubIndexUrl !!}">续费下单(先选订阅)</a>
|
||
@endif
|
||
|
||
@if($attachOrderId <= 0)
|
||
<form method="post" action="/admin/site-subscriptions/batch-mark-expired" data-action="disable-on-submit" onsubmit="return confirm('确认将当前筛选集合内的订阅批量标记为已过期?该操作会更新 status 字段。');" class="actions gap-10">
|
||
@csrf
|
||
@include('admin.components.filters_hidden_inputs', [
|
||
'filters' => $filters ?? [],
|
||
'keys' => ['status', 'merchant_id', 'plan_id', 'expiry', 'ends_from', 'ends_to', 'keyword'],
|
||
])
|
||
|
||
<label class="muted form-inline-row">
|
||
<span>确认输入</span>
|
||
<input type="text" name="confirm" placeholder="YES" class="w-140" @disabled(! $batchMarkExpiredEnabled)>
|
||
<span>(必须输入 YES 才会执行)</span>
|
||
</label>
|
||
|
||
<div>
|
||
<button class="btn btn-danger btn-sm" type="submit" @disabled(! $batchMarkExpiredEnabled) title="{{ $batchMarkExpiredReason }}">批量标记已过期(当前集合)</button>
|
||
@if(! $batchMarkExpiredEnabled)
|
||
<div class="adm-tool-blocked-hint" data-role="batch-mark-expired-blocked-hint">
|
||
<div>提示:{{ $batchMarkExpiredReason }}</div>
|
||
@php
|
||
// 提效:被阻断时给一键跳转到「已过期(expiry=expired)」集合,避免运营来回找入口。
|
||
// 说明:这里选择保留当前 ends_from/ends_to 区间(若有),避免运营还要重新输入到期范围。
|
||
$goExpiredUrl = $buildSelfUrl(['status' => null, 'expiry' => 'expired', 'page' => null]);
|
||
@endphp
|
||
<div class="mt-6 actions gap-10">
|
||
<a class="btn btn-secondary btn-sm" href="{!! $goExpiredUrl !!}">切到已过期集合</a>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</form>
|
||
@endif
|
||
</div>
|
||
<div class="muted muted-xs mt-6">
|
||
@if($attachOrderId > 0)
|
||
当前为“绑定订阅到订单”模式:请在列表行内直接点击「绑定到订单」。
|
||
@else
|
||
用于运营从订阅目录快速发起续费下单:会把当前 merchant_id/plan_id 作为默认值带到下单页。
|
||
<span class="muted">续费单必须绑定订阅,建议从下方列表行内「续费下单」选择具体订阅。</span>
|
||
@endif
|
||
@if(($isExpiryView ?? false))
|
||
<span class="muted">(已处于到期集合视图:请优先使用上方「创建续费订单(当前集合)」入口)</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card list-card">
|
||
<div class="list-card-header">
|
||
<div>
|
||
<h3 class="list-card-title">订阅列表</h3>
|
||
</div>
|
||
</div>
|
||
<div class="list-card-body">
|
||
<table class="list-card-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 = \App\Support\BackUrl::withBack('/admin/site-subscriptions/' . $subscription->id, $back);
|
||
@endphp
|
||
<a href="{!! $subShowUrl !!}">{{ $subscription->subscription_no }}</a>
|
||
@php
|
||
$remarkPrefix = (string) config('saasshop.platform_orders.renewal_order_remark_prefix', '来自订阅:');
|
||
$q = [
|
||
'order_type' => 'renewal',
|
||
'require_subscription' => '1',
|
||
'site_subscription_id' => $subscription->id,
|
||
'quantity' => 1,
|
||
'remark' => $remarkPrefix . $subscription->subscription_no,
|
||
];
|
||
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 = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query($q), $back);
|
||
@endphp
|
||
<div class="mt-4 actions gap-10">
|
||
<a class="btn btn-secondary btn-sm" href="{!! $renewOrderUrl !!}">续费下单</a>
|
||
|
||
@if($attachOrderId > 0)
|
||
@php
|
||
// 从订单详情进入订阅管理页时:提供“绑定到该订单”的治理按钮
|
||
// 注意:提交后由 attachSubscription 做强校验(续费单 + merchant/plan 一致)
|
||
$attachBack = $safeAttachBackForLinks !== '' ? $safeAttachBackForLinks : $safeBackForLinks;
|
||
if ($attachBack === '') {
|
||
$attachBack = $back;
|
||
}
|
||
@endphp
|
||
<form method="post" action="/admin/platform-orders/{{ $attachOrderId }}/attach-subscription" class="inline-form" data-action="disable-on-submit" onsubmit="return confirm('确认将该订阅绑定到目标订单?');">
|
||
@csrf
|
||
<input type="hidden" name="site_subscription_id" value="{{ $subscription->id }}">
|
||
@if($attachBack !== '')
|
||
<input type="hidden" name="back" value="{!! $attachBack !!}">
|
||
@endif
|
||
<button type="submit" class="btn btn-primary btn-sm">绑定到订单 #{{ $attachOrderId }}</button>
|
||
</form>
|
||
@endif
|
||
|
||
<form method="post" action="/admin/site-subscriptions/{{ $subscription->id }}/set-status">
|
||
@csrf
|
||
<select name="status" onchange="this.form.submit()" class="w-140">
|
||
@foreach(($filterOptions['statuses'] ?? []) as $value => $label)
|
||
<option value="{{ $value }}" @selected($subscription->status === $value)>{{ $label }}</option>
|
||
@endforeach
|
||
</select>
|
||
<noscript><button type="submit" class="btn btn-secondary btn-sm">更新状态</button></noscript>
|
||
</form>
|
||
</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
|
||
// 跳到平台订单页:附带 back 回到“订阅列表页自身(剔除 back)”,并统一走 BackUrl helper。
|
||
$platformOrdersUrl = \App\Support\BackUrl::withBack(
|
||
'/admin/platform-orders?' . \Illuminate\Support\Arr::query([
|
||
'site_subscription_id' => $subscription->id,
|
||
]),
|
||
$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 table-empty">暂无订阅数据,当前阶段先把订阅主表与总台目录立起来,后续再接订阅创建/激活/续费链路。</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pagination-wrap">{{ $subscriptions->links('pagination.admin') }}</div>
|
||
@endsection
|