Files
saasshop/app/Http/Controllers/Admin/SiteSubscriptionController.php

372 lines
16 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']);
$baseOrdersQuery = PlatformOrder::query()
->where('site_subscription_id', $subscription->id);
// 可治理摘要:订阅下的订单同步情况(基于全量关联订单,不受页面筛选影响)
$summaryStats = [
'total_orders' => (clone $baseOrdersQuery)->count(),
'synced_orders' => (clone $baseOrdersQuery)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NOT NULL")
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL")
->count(),
'failed_orders' => (clone $baseOrdersQuery)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL")
->count(),
'unsynced_orders' => (clone $baseOrdersQuery)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL")
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL")
->count(),
'syncable_orders' => (clone $baseOrdersQuery)
->where('payment_status', 'paid')
->where('status', 'activated')
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL")
->count(),
];
// 可治理摘要:订阅维度的回执/退款汇总(口径与平台订单列表一致:优先 summary缺省回退 receipts
$metaOrders = (clone $baseOrdersQuery)->get(['id', 'paid_amount', 'payment_status', 'meta']);
$totalReceiptAmount = 0.0;
$receiptOrders = 0;
$noReceiptOrders = 0;
$totalRefundedAmount = 0.0;
$refundOrders = 0;
$noRefundOrders = 0;
// 订阅维度:退款不一致订单数(与平台订单列表 refund_inconsistent 口径保持一致)
$refundInconsistentOrders = 0;
foreach ($metaOrders as $o) {
$meta = $o->meta ?? [];
$receiptTotal = (float) $o->receiptTotal();
if ($receiptTotal > 0) {
$receiptOrders++;
$totalReceiptAmount += $receiptTotal;
} else {
$noReceiptOrders++;
}
$refundTotal = (float) $o->refundTotal();
if ($refundTotal > 0) {
$refundOrders++;
$totalRefundedAmount += $refundTotal;
} else {
$noRefundOrders++;
}
if ($o->isRefundInconsistent()) {
$refundInconsistentOrders++;
}
}
$summaryStats = $summaryStats + [
'receipt_orders' => $receiptOrders,
'no_receipt_orders' => $noReceiptOrders,
'total_receipt_amount' => (float) $totalReceiptAmount,
'refund_orders' => $refundOrders,
'no_refund_orders' => $noRefundOrders,
'total_refunded_amount' => (float) $totalRefundedAmount,
// 退款不一致订单(订阅维度)
'refund_inconsistent_orders' => (int) $refundInconsistentOrders,
// 对账差额:回执总额 - 已付总额(订阅维度)
'reconciliation_delta' => (float) ($totalReceiptAmount - (float) $metaOrders->sum('paid_amount')),
];
// 同步失败原因聚合Top3订阅维度快速判断“常见失败原因”
$failedReasonRows = (clone $baseOrdersQuery)
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL")
->selectRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') as reason, count(*) as cnt")
->groupBy('reason')
->orderByDesc('cnt')
->limit(3)
->get();
$failedReasonStats = $failedReasonRows->map(function ($row) {
$reason = (string) ($row->reason ?? '');
$reason = trim($reason, "\" ");
return [
'reason' => $reason !== '' ? $reason : '(空)',
'count' => (int) ($row->cnt ?? 0),
];
})->values()->all();
// 页面列表筛选:仅影响“关联平台订单”列表展示,不影响摘要统计
$orderSyncStatus = trim((string) $request->query('order_sync_status', ''));
$displayOrdersQuery = (clone $baseOrdersQuery);
if ($orderSyncStatus === 'synced') {
$displayOrdersQuery
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NOT NULL")
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL");
} elseif ($orderSyncStatus === 'failed') {
$displayOrdersQuery
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL");
} elseif ($orderSyncStatus === 'unsynced') {
$displayOrdersQuery
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL")
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL");
} elseif ($orderSyncStatus === 'syncable') {
$displayOrdersQuery
->where('payment_status', 'paid')
->where('status', 'activated')
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL");
}
$platformOrders = $displayOrdersQuery
->latest('id')
->paginate(10)
->withQueryString();
// 可治理摘要:订阅下的订单同步情况
$summaryStats = $summaryStats + [
'current_order_sync_status' => $orderSyncStatus,
];
$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,
'failedReasonStats' => $failedReasonStats,
'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);
}
});
});
}
}