546 lines
25 KiB
PHP
546 lines
25 KiB
PHP
<?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")
|
||
// 口径对齐平台订单页:可同步 = 未同步且非失败
|
||
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') 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;
|
||
|
||
// 订阅维度:对账不一致订单数(与平台订单列表 reconcile_mismatch 口径保持一致)
|
||
$reconcileMismatchOrders = 0;
|
||
|
||
foreach ($metaOrders as $o) {
|
||
$meta = $o->meta ?? [];
|
||
|
||
$receiptTotal = (float) $o->receiptTotal();
|
||
|
||
if ($receiptTotal > 0) {
|
||
$receiptOrders++;
|
||
$totalReceiptAmount += $receiptTotal;
|
||
} else {
|
||
$noReceiptOrders++;
|
||
}
|
||
|
||
if ($o->isReconcileMismatch()) {
|
||
$reconcileMismatchOrders++;
|
||
}
|
||
|
||
$refundTotal = (float) $o->refundTotal();
|
||
|
||
if ($refundTotal > 0) {
|
||
$refundOrders++;
|
||
$totalRefundedAmount += $refundTotal;
|
||
} else {
|
||
$noRefundOrders++;
|
||
}
|
||
|
||
if ($o->isRefundInconsistent()) {
|
||
$refundInconsistentOrders++;
|
||
}
|
||
}
|
||
|
||
// 订阅维度:BMPA(批量标记支付并生效)失败订单数(与平台订单列表 bmpa_failed_only 口径一致)
|
||
$bmpaFailedOrders = (clone $baseOrdersQuery)
|
||
->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_paid_and_activate_error.message') IS NOT NULL")
|
||
->count();
|
||
|
||
$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,
|
||
// 对账不一致订单(订阅维度)
|
||
'reconcile_mismatch_orders' => (int) $reconcileMismatchOrders,
|
||
// BMPA 失败订单(订阅维度)
|
||
'bmpa_failed_orders' => (int) $bmpaFailedOrders,
|
||
// 对账差额:回执总额 - 已付总额(订阅维度)
|
||
'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();
|
||
|
||
// BMPA 失败原因聚合(Top3):订阅维度快速判断“常见批量标记支付失败原因”
|
||
$bmpaFailedReasonRows = (clone $baseOrdersQuery)
|
||
->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_paid_and_activate_error.message') IS NOT NULL")
|
||
->selectRaw("JSON_EXTRACT(meta, '$.batch_mark_paid_and_activate_error.message') as reason, count(*) as cnt")
|
||
->groupBy('reason')
|
||
->orderByDesc('cnt')
|
||
->limit(3)
|
||
->get();
|
||
|
||
$bmpaFailedReasonStats = $bmpaFailedReasonRows->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")
|
||
->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') 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,
|
||
'bmpaFailedReasonStats' => $bmpaFailedReasonStats,
|
||
'statusLabels' => $this->statusLabels(),
|
||
'expiryLabel' => $expiryLabel,
|
||
]);
|
||
}
|
||
|
||
public function export(Request $request): StreamedResponse
|
||
{
|
||
$this->ensurePlatformAdmin($request);
|
||
|
||
// 安全阀:必须显式声明 download=1,避免浏览器预取/误触发导致频繁导出
|
||
if ((string) $request->query('download', '') !== '1') {
|
||
abort(400, 'download=1 required');
|
||
}
|
||
|
||
$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', '')),
|
||
'ends_from' => trim((string) $request->query('ends_from', '')),
|
||
'ends_to' => trim((string) $request->query('ends_to', '')),
|
||
];
|
||
|
||
$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_7d:7 天内到期(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);
|
||
|
||
$expiryMerchantRows = [];
|
||
$expiryMerchantPlanRows = [];
|
||
if ((string) ($filters['expiry'] ?? '') === 'expiring_7d') {
|
||
// 到期提醒清单:站点维度 Top10(用于运营快速触达/续费跟进)
|
||
$expiryMerchantRows = $this->applyFilters(SiteSubscription::query(), $filters)
|
||
->leftJoin('merchants', 'site_subscriptions.merchant_id', '=', 'merchants.id')
|
||
->whereNotNull('site_subscriptions.ends_at')
|
||
->selectRaw('site_subscriptions.merchant_id as merchant_id, merchants.name as merchant_name, count(*) as cnt, min(site_subscriptions.ends_at) as min_ends_at')
|
||
->groupBy('site_subscriptions.merchant_id', 'merchants.name')
|
||
->orderByDesc('cnt')
|
||
->orderBy('min_ends_at')
|
||
->limit(10)
|
||
->get()
|
||
->map(function ($row) {
|
||
return [
|
||
'merchant_id' => (int) ($row->merchant_id ?? 0),
|
||
'merchant_name' => (string) ($row->merchant_name ?? ''),
|
||
'count' => (int) ($row->cnt ?? 0),
|
||
'min_ends_at' => (string) ($row->min_ends_at ?? ''),
|
||
];
|
||
})
|
||
->values()
|
||
->all();
|
||
|
||
// 到期提醒清单:站点+套餐维度 Top10(更精确的续费下单入口)
|
||
$expiryMerchantPlanRows = $this->applyFilters(SiteSubscription::query(), $filters)
|
||
->leftJoin('merchants', 'site_subscriptions.merchant_id', '=', 'merchants.id')
|
||
->leftJoin('plans', 'site_subscriptions.plan_id', '=', 'plans.id')
|
||
->whereNotNull('site_subscriptions.ends_at')
|
||
->selectRaw('site_subscriptions.merchant_id as merchant_id, merchants.name as merchant_name, site_subscriptions.plan_id as plan_id, plans.name as plan_name, count(*) as cnt, min(site_subscriptions.ends_at) as min_ends_at')
|
||
->groupBy('site_subscriptions.merchant_id', 'merchants.name', 'site_subscriptions.plan_id', 'plans.name')
|
||
->orderByDesc('cnt')
|
||
->orderBy('min_ends_at')
|
||
->limit(10)
|
||
->get()
|
||
->map(function ($row) {
|
||
return [
|
||
'merchant_id' => (int) ($row->merchant_id ?? 0),
|
||
'merchant_name' => (string) ($row->merchant_name ?? ''),
|
||
'plan_id' => (int) ($row->plan_id ?? 0),
|
||
'plan_name' => (string) ($row->plan_name ?? ''),
|
||
'count' => (int) ($row->cnt ?? 0),
|
||
'min_ends_at' => (string) ($row->min_ends_at ?? ''),
|
||
];
|
||
})
|
||
->values()
|
||
->all();
|
||
}
|
||
|
||
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(),
|
||
],
|
||
'expiryMerchantRows' => $expiryMerchantRows,
|
||
'expiryMerchantPlanRows' => $expiryMerchantPlanRows,
|
||
]);
|
||
}
|
||
|
||
protected function statusLabels(): array
|
||
{
|
||
return [
|
||
'pending' => '待生效',
|
||
'activated' => '已生效',
|
||
'cancelled' => '已取消',
|
||
'expired' => '已过期',
|
||
];
|
||
}
|
||
|
||
public function setStatus(Request $request, SiteSubscription $subscription): \Illuminate\Http\RedirectResponse
|
||
{
|
||
$this->ensurePlatformAdmin($request);
|
||
|
||
$data = $request->validate([
|
||
'status' => ['required', \Illuminate\Validation\Rule::in(array_keys($this->statusLabels()))],
|
||
]);
|
||
|
||
$subscription->status = (string) $data['status'];
|
||
$subscription->save();
|
||
|
||
return redirect()->back()->with('success', '订阅状态已更新:' . ($this->statusLabels()[$subscription->status] ?? $subscription->status));
|
||
}
|
||
|
||
public function batchMarkExpired(Request $request): \Illuminate\Http\RedirectResponse
|
||
{
|
||
$this->ensurePlatformAdmin($request);
|
||
|
||
// 仅支持在“已过期(expiry=expired)集合”上执行,避免误把正常订阅批量标记为已过期。
|
||
$filters = [
|
||
'status' => trim((string) $request->input('status', '')),
|
||
'keyword' => trim((string) $request->input('keyword', '')),
|
||
'merchant_id' => trim((string) $request->input('merchant_id', '')),
|
||
'plan_id' => trim((string) $request->input('plan_id', '')),
|
||
'expiry' => trim((string) $request->input('expiry', '')),
|
||
];
|
||
|
||
if ((string) ($filters['expiry'] ?? '') !== 'expired') {
|
||
return redirect()->back()->with('warning', '为避免误操作:批量标记已过期仅允许在「已过期(expiry=expired)」集合视图下执行。');
|
||
}
|
||
|
||
// 防误操作:需要二次确认
|
||
if ((string) $request->input('confirm', '') !== 'YES') {
|
||
return redirect()->back()->with('warning', '为避免误操作,请在确认框输入 YES 后再批量标记已过期。');
|
||
}
|
||
|
||
$query = $this->applyFilters(SiteSubscription::query(), $filters);
|
||
|
||
// 再加一道硬条件:ends_at 必须 < now(与 expiry=expired 一致)
|
||
$query->whereNotNull('ends_at')->where('ends_at', '<', now());
|
||
|
||
// 仅把“非已过期”的订阅更新为 expired
|
||
$affected = (clone $query)
|
||
->where('status', '!=', 'expired')
|
||
->update([
|
||
'status' => 'expired',
|
||
]);
|
||
|
||
return redirect()->back()->with('success', '已批量标记已过期:' . (int) $affected . ' 条。');
|
||
}
|
||
|
||
protected function applyFilters(Builder $query, array $filters): Builder
|
||
{
|
||
// 说明:该方法既会用于普通列表查询(无 join),也会用于「到期提醒 Top10」统计(有 join merchants/plans)。
|
||
// 为避免在 SQLite 下出现 "ambiguous column name",这里统一对 site_subscriptions 的字段加上表前缀。
|
||
$t = 'site_subscriptions';
|
||
|
||
return $query
|
||
->when($filters['status'] !== '', fn (Builder $builder) => $builder->where($t . '.status', $filters['status']))
|
||
->when(($filters['merchant_id'] ?? '') !== '', fn (Builder $builder) => $builder->where($t . '.merchant_id', (int) $filters['merchant_id']))
|
||
->when(($filters['plan_id'] ?? '') !== '', fn (Builder $builder) => $builder->where($t . '.plan_id', (int) $filters['plan_id']))
|
||
->when(($filters['expiry'] ?? '') !== '', function (Builder $builder) use ($filters, $t) {
|
||
$expiry = (string) ($filters['expiry'] ?? '');
|
||
if ($expiry === 'expired') {
|
||
$builder->whereNotNull($t . '.ends_at')->where($t . '.ends_at', '<', now());
|
||
} elseif ($expiry === 'expiring_7d') {
|
||
$builder->whereNotNull($t . '.ends_at')
|
||
->where($t . '.ends_at', '>=', now())
|
||
->where($t . '.ends_at', '<', now()->addDays(7));
|
||
}
|
||
})
|
||
->when(($filters['ends_from'] ?? '') !== '' || ($filters['ends_to'] ?? '') !== '', function (Builder $builder) use ($filters, $t) {
|
||
// 到期时间范围筛选:用于运营按 ends_at 精确定位
|
||
// 容错:仅接受 YYYY-MM-DD
|
||
$from = trim((string) ($filters['ends_from'] ?? ''));
|
||
$to = trim((string) ($filters['ends_to'] ?? ''));
|
||
|
||
if ($from !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) {
|
||
$from = '';
|
||
}
|
||
if ($to !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
|
||
$to = '';
|
||
}
|
||
|
||
if ($from !== '' && $to !== '') {
|
||
$builder->whereBetween($t . '.ends_at', [$from . ' 00:00:00', $to . ' 23:59:59']);
|
||
} elseif ($from !== '') {
|
||
$builder->where($t . '.ends_at', '>=', $from . ' 00:00:00');
|
||
} elseif ($to !== '') {
|
||
$builder->where($t . '.ends_at', '<=', $to . ' 23:59:59');
|
||
}
|
||
})
|
||
->when($filters['keyword'] !== '', function (Builder $builder) use ($filters, $t) {
|
||
// 关键词搜索:订阅号 / 站点 / 套餐 / 计费周期
|
||
$keyword = trim((string) ($filters['keyword'] ?? ''));
|
||
if ($keyword === '') {
|
||
return;
|
||
}
|
||
|
||
$builder->where(function (Builder $q) use ($keyword, $t) {
|
||
$q->where($t . '.subscription_no', 'like', '%' . $keyword . '%')
|
||
->orWhere($t . '.plan_name', 'like', '%' . $keyword . '%')
|
||
->orWhere($t . '.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($t . '.id', (int) $keyword);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|