Files
saasshop/app/Http/Controllers/Admin/SiteSubscriptionController.php
2026-03-10 12:29:38 +00:00

271 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Concerns\ResolvesPlatformAdminContext;
use App\Http\Controllers\Controller;
use App\Models\SiteSubscription;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Models\PlatformOrder;
class SiteSubscriptionController extends Controller
{
use ResolvesPlatformAdminContext;
public function show(Request $request, SiteSubscription $subscription): View
{
$this->ensurePlatformAdmin($request);
$subscription->loadMissing(['merchant', 'plan']);
$platformOrdersQuery = PlatformOrder::query()
->where('site_subscription_id', $subscription->id);
$platformOrders = (clone $platformOrdersQuery)
->latest('id')
->paginate(10)
->withQueryString();
// 可治理摘要:订阅下的订单同步情况
$summaryStats = [
'total_orders' => (clone $platformOrdersQuery)->count(),
'synced_orders' => (clone $platformOrdersQuery)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NOT NULL")
->count(),
'failed_orders' => (clone $platformOrdersQuery)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL")
->count(),
'unsynced_orders' => (clone $platformOrdersQuery)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL")
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL")
->count(),
'syncable_orders' => (clone $platformOrdersQuery)
->where('payment_status', 'paid')
->where('status', 'activated')
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL")
->count(),
];
$endsAt = $subscription->ends_at;
$expiryLabel = '无到期';
if ($endsAt) {
if ($endsAt->lt(now())) {
$expiryLabel = '已过期';
} elseif ($endsAt->lt(now()->addDays(7))) {
$expiryLabel = '7天内到期';
} else {
$expiryLabel = '未到期';
}
}
return view('admin.site_subscriptions.show', [
'subscription' => $subscription,
'platformOrders' => $platformOrders,
'summaryStats' => $summaryStats,
'statusLabels' => $this->statusLabels(),
'expiryLabel' => $expiryLabel,
]);
}
public function export(Request $request): StreamedResponse
{
$this->ensurePlatformAdmin($request);
$filters = [
'status' => trim((string) $request->query('status', '')),
'keyword' => trim((string) $request->query('keyword', '')),
'merchant_id' => trim((string) $request->query('merchant_id', '')),
'plan_id' => trim((string) $request->query('plan_id', '')),
'expiry' => trim((string) $request->query('expiry', '')),
];
$query = $this->applyFilters(
SiteSubscription::query()->with(['merchant', 'plan'])->withCount('platformOrders'),
$filters
)->orderBy('id');
$filename = 'site_subscriptions_' . now()->format('Ymd_His') . '.csv';
return response()->streamDownload(function () use ($query) {
$out = fopen('php://output', 'w');
// UTF-8 BOM避免 Excel 打开中文乱码
fwrite($out, "\xEF\xBB\xBF");
fputcsv($out, [
'ID',
'订阅号',
'站点',
'套餐',
'关联订单数',
'状态',
'计费周期',
'周期(月)',
'金额',
'开始时间',
'到期时间',
'到期状态',
'试用到期',
'生效时间',
'取消时间',
]);
$statusLabels = $this->statusLabels();
$query->chunkById(500, function ($subs) use ($out, $statusLabels) {
foreach ($subs as $sub) {
$endsAt = $sub->ends_at;
$expiryLabel = '无到期';
if ($endsAt) {
if ($endsAt->lt(now())) {
$expiryLabel = '已过期';
} elseif ($endsAt->lt(now()->addDays(7))) {
$expiryLabel = '7天内到期';
} else {
$expiryLabel = '未到期';
}
}
$status = (string) ($sub->status ?? '');
$statusText = ($statusLabels[$status] ?? $status);
$statusText = $statusText . ' (' . $status . ')';
fputcsv($out, [
$sub->id,
$sub->subscription_no,
$sub->merchant?->name ?? '',
$sub->plan_name ?: ($sub->plan?->name ?? ''),
(int) ($sub->platform_orders_count ?? 0),
$statusText,
$sub->billing_cycle ?: '',
(int) $sub->period_months,
(float) $sub->amount,
optional($sub->starts_at)->format('Y-m-d H:i:s') ?: '',
optional($sub->ends_at)->format('Y-m-d H:i:s') ?: '',
$expiryLabel,
optional($sub->trial_ends_at)->format('Y-m-d H:i:s') ?: '',
optional($sub->activated_at)->format('Y-m-d H:i:s') ?: '',
optional($sub->cancelled_at)->format('Y-m-d H:i:s') ?: '',
]);
}
});
fclose($out);
}, $filename, [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
public function index(Request $request): View
{
$this->ensurePlatformAdmin($request);
$filters = [
'status' => trim((string) $request->query('status', '')),
'keyword' => trim((string) $request->query('keyword', '')),
'merchant_id' => trim((string) $request->query('merchant_id', '')),
'plan_id' => trim((string) $request->query('plan_id', '')),
// 到期辅助筛选(不改变 status 字段,仅按 ends_at 计算)
// - expired已过期ends_at < now
// - expiring_7d7 天内到期now <= ends_at < now+7d
'expiry' => trim((string) $request->query('expiry', '')),
];
$query = $this->applyFilters(
SiteSubscription::query()->with(['merchant', 'plan'])->withCount('platformOrders'),
$filters
);
$subscriptions = (clone $query)
->latest('id')
->paginate(10)
->withQueryString();
$baseQuery = $this->applyFilters(SiteSubscription::query(), $filters);
return view('admin.site_subscriptions.index', [
'subscriptions' => $subscriptions,
'filters' => $filters,
'statusLabels' => $this->statusLabels(),
'filterOptions' => [
'statuses' => $this->statusLabels(),
],
'merchants' => SiteSubscription::query()->with('merchant')->select('merchant_id')->distinct()->get()->pluck('merchant')->filter()->unique('id')->values(),
'plans' => SiteSubscription::query()->with('plan')->select('plan_id')->whereNotNull('plan_id')->distinct()->get()->pluck('plan')->filter()->unique('id')->values(),
'summaryStats' => [
'total_subscriptions' => (clone $baseQuery)->count(),
'activated_subscriptions' => (clone $baseQuery)->where('status', 'activated')->count(),
'pending_subscriptions' => (clone $baseQuery)->where('status', 'pending')->count(),
'cancelled_subscriptions' => (clone $baseQuery)->where('status', 'cancelled')->count(),
// 可治理辅助指标:按 ends_at 计算
'expired_subscriptions' => (clone $baseQuery)
->whereNotNull('ends_at')
->where('ends_at', '<', now())
->count(),
'expiring_7d_subscriptions' => (clone $baseQuery)
->whereNotNull('ends_at')
->where('ends_at', '>=', now())
->where('ends_at', '<', now()->addDays(7))
->count(),
],
]);
}
protected function statusLabels(): array
{
return [
'pending' => '待生效',
'activated' => '已生效',
'cancelled' => '已取消',
'expired' => '已过期',
];
}
protected function applyFilters(Builder $query, array $filters): Builder
{
return $query
->when($filters['status'] !== '', fn (Builder $builder) => $builder->where('status', $filters['status']))
->when(($filters['merchant_id'] ?? '') !== '', fn (Builder $builder) => $builder->where('merchant_id', (int) $filters['merchant_id']))
->when(($filters['plan_id'] ?? '') !== '', fn (Builder $builder) => $builder->where('plan_id', (int) $filters['plan_id']))
->when(($filters['expiry'] ?? '') !== '', function (Builder $builder) use ($filters) {
$expiry = (string) ($filters['expiry'] ?? '');
if ($expiry === 'expired') {
$builder->whereNotNull('ends_at')->where('ends_at', '<', now());
} elseif ($expiry === 'expiring_7d') {
$builder->whereNotNull('ends_at')
->where('ends_at', '>=', now())
->where('ends_at', '<', now()->addDays(7));
}
})
->when($filters['keyword'] !== '', function (Builder $builder) use ($filters) {
// 关键词搜索:订阅号 / 站点 / 套餐 / 计费周期
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword === '') {
return;
}
$builder->where(function (Builder $q) use ($keyword) {
$q->where('subscription_no', 'like', '%' . $keyword . '%')
->orWhere('plan_name', 'like', '%' . $keyword . '%')
->orWhere('billing_cycle', 'like', '%' . $keyword . '%')
->orWhereHas('merchant', function (Builder $mq) use ($keyword) {
$mq->where('name', 'like', '%' . $keyword . '%')
->orWhere('slug', 'like', '%' . $keyword . '%');
})
->orWhereHas('plan', function (Builder $pq) use ($keyword) {
$pq->where('name', 'like', '%' . $keyword . '%')
->orWhere('code', 'like', '%' . $keyword . '%');
});
if (ctype_digit($keyword)) {
$q->orWhere('id', (int) $keyword);
}
});
});
}
}