ensurePlatformAdmin($request); $merchants = Merchant::query()->orderBy('id')->get(['id', 'name']); $plans = Plan::query()->orderBy('sort')->orderByDesc('id')->get(); // 支持从其它页面(例如订阅详情)带默认值跳转过来,提高运营效率 $defaults = [ 'merchant_id' => (int) $request->query('merchant_id', 0), 'plan_id' => (int) $request->query('plan_id', 0), 'site_subscription_id' => (int) $request->query('site_subscription_id', 0), 'order_type' => (string) $request->query('order_type', 'new_purchase'), 'quantity' => (int) $request->query('quantity', 1), 'discount_amount' => (float) $request->query('discount_amount', 0), 'payment_channel' => (string) $request->query('payment_channel', ''), 'remark' => (string) $request->query('remark', ''), ]; $siteSubscription = null; $siteSubscriptionId = (int) ($defaults['site_subscription_id'] ?? 0); if ($siteSubscriptionId > 0) { $siteSubscription = SiteSubscription::query()->with(['merchant', 'plan'])->find($siteSubscriptionId); } return view('admin.platform_orders.form', [ 'merchants' => $merchants, 'plans' => $plans, 'siteSubscription' => $siteSubscription, 'billingCycleLabels' => $this->billingCycleLabels(), 'orderTypeLabels' => $this->orderTypeLabels(), 'defaults' => $defaults, ]); } public function store(Request $request): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); $data = $request->validate([ 'merchant_id' => ['required', 'integer', 'exists:merchants,id'], 'plan_id' => ['required', 'integer', 'exists:plans,id'], 'site_subscription_id' => ['nullable', 'integer', 'exists:site_subscriptions,id'], 'order_type' => ['required', Rule::in(array_keys($this->orderTypeLabels()))], 'quantity' => ['required', 'integer', 'min:1', 'max:120'], 'discount_amount' => ['nullable', 'numeric', 'min:0'], 'payment_channel' => ['nullable', 'string', 'max:30'], 'remark' => ['nullable', 'string', 'max:2000'], ]); $plan = Plan::query()->findOrFail((int) $data['plan_id']); $periodMonths = $this->periodMonthsFromBillingCycle((string) $plan->billing_cycle); $quantity = (int) $data['quantity']; $listAmount = (float) $plan->price * $quantity; $discount = (float) ($data['discount_amount'] ?? 0); $discount = max(0, min($listAmount, $discount)); $payable = max(0, $listAmount - $discount); $now = now(); // 订单号:PO + 时间 + 4位随机数(足够用于当前阶段演示与手工补单) $orderNo = 'PO' . $now->format('YmdHis') . str_pad((string) random_int(1, 9999), 4, '0', STR_PAD_LEFT); $order = PlatformOrder::query()->create([ 'merchant_id' => (int) $data['merchant_id'], 'plan_id' => $plan->id, 'site_subscription_id' => (int) ($data['site_subscription_id'] ?? 0) ?: null, 'created_by_admin_id' => $admin->id, 'order_no' => $orderNo, 'order_type' => (string) $data['order_type'], 'status' => 'pending', 'payment_status' => 'unpaid', 'payment_channel' => $data['payment_channel'] ?? null, 'plan_name' => $plan->name, 'billing_cycle' => $plan->billing_cycle, 'period_months' => $periodMonths, 'quantity' => $quantity, 'list_amount' => $listAmount, 'discount_amount' => $discount, 'payable_amount' => $payable, 'paid_amount' => 0, 'placed_at' => $now, 'plan_snapshot' => [ 'plan_id' => $plan->id, 'code' => $plan->code, 'name' => $plan->name, 'billing_cycle' => $plan->billing_cycle, 'price' => (float) $plan->price, 'list_price' => (float) $plan->list_price, 'status' => $plan->status, 'published_at' => optional($plan->published_at)->toDateTimeString(), ], 'meta' => [ 'created_from' => 'manual_form', ], 'remark' => $data['remark'] ?? null, ]); return redirect('/admin/platform-orders/' . $order->id) ->with('success', '平台订单已创建:' . $order->order_no . '(待支付/待生效)'); } public function index(Request $request): View { $this->ensurePlatformAdmin($request); $filters = [ 'status' => trim((string) $request->query('status', '')), 'payment_status' => trim((string) $request->query('payment_status', '')), 'merchant_id' => trim((string) $request->query('merchant_id', '')), 'plan_id' => trim((string) $request->query('plan_id', '')), // 精确过滤:订阅ID(用于从订阅详情页跳转到平台订单列表时锁定范围) 'site_subscription_id' => trim((string) $request->query('site_subscription_id', '')), 'fail_only' => (string) $request->query('fail_only', ''), 'synced_only' => (string) $request->query('synced_only', ''), 'sync_status' => trim((string) $request->query('sync_status', '')), 'keyword' => trim((string) $request->query('keyword', '')), // 同步失败原因关键词:用于快速定位同原因失败订单(可治理) 'sync_error_keyword' => trim((string) $request->query('sync_error_keyword', '')), // 只看“可同步订阅”的订单:已支付 + 已生效 + 未同步(用于运营快速处理) 'syncable_only' => (string) $request->query('syncable_only', ''), // 只看最近 24 小时批量同步过的订单(可治理追踪) 'batch_synced_24h' => (string) $request->query('batch_synced_24h', ''), // 只看最近 24 小时批量“仅标记为已生效”过的订单(可治理追踪) 'batch_mark_activated_24h' => (string) $request->query('batch_mark_activated_24h', ''), // 只看“对账不一致”的订单(粗版):meta.payment_summary.total_amount 与 paid_amount 不一致 'reconcile_mismatch' => (string) $request->query('reconcile_mismatch', ''), // 支付回执筛选:has(有回执)/none(无回执) 'receipt_status' => trim((string) $request->query('receipt_status', '')), // 退款轨迹筛选:has(有退款)/none(无退款) 'refund_status' => trim((string) $request->query('refund_status', '')), // 退款数据不一致(可治理):基于 refund_summary.total_amount 与 paid_amount 对比 'refund_inconsistent' => (string) $request->query('refund_inconsistent', ''), ]; $orders = $this->applyFilters(PlatformOrder::query()->with(['merchant', 'plan', 'siteSubscription']), $filters) ->latest('id') ->paginate(10) ->withQueryString(); // 列表行级对账视图:回执总额 / 差额(便于运营快速定位问题订单) $orders->getCollection()->transform(function (PlatformOrder $o) { $receiptTotal = (float) $this->receiptTotalForOrder($o); $o->setAttribute('receipt_total', $receiptTotal); $o->setAttribute('reconciliation_delta_row', $receiptTotal - (float) $o->paid_amount); return $o; }); $baseQuery = $this->applyFilters(PlatformOrder::query(), $filters); // 同步失败原因聚合(Top 5):用于运营快速判断“常见失败原因” // 注意:这里用 JSON_EXTRACT 做 group by,MySQL 会返回带引号的 JSON 字符串,展示时做一次 trim 处理。 $failedReasonRows = (clone $baseQuery) ->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(5) ->get(); $failedReasonStats = $failedReasonRows->map(function ($row) { $reason = (string) ($row->reason ?? ''); $reason = trim($reason, "\" "); return [ 'reason' => $reason !== '' ? $reason : '(空)', 'count' => (int) ($row->cnt ?? 0), ]; })->values()->all(); // 运营摘要中的 meta 汇总(refund/payment)需要遍历订单 meta。 // 为避免重复 get(),在当前筛选范围内一次性拉取所需字段。 $metaOrders = (clone $baseQuery)->get(['id', 'paid_amount', 'meta']); $totalRefundedAmount = (float) $metaOrders->sum(function ($o) { // 优先读 meta.refund_summary.total_amount,回退汇总 meta.refund_receipts[].amount $total = (float) (data_get($o->meta, 'refund_summary.total_amount') ?? 0); if ($total > 0) { return $total; } $refunds = (array) (data_get($o->meta, 'refund_receipts', []) ?? []); $sum = 0.0; foreach ($refunds as $r) { $sum += (float) (data_get($r, 'amount') ?? 0); } return $sum; }); $totalReceiptAmount = (float) $this->sumReceiptAmount($metaOrders); $totalPayableAmount = (float) ((clone $baseQuery)->sum('payable_amount') ?: 0); $totalPaidAmount = (float) ((clone $baseQuery)->sum('paid_amount') ?: 0); return view('admin.platform_orders.index', [ 'orders' => $orders, 'filters' => $filters, 'filterOptions' => [ 'statuses' => $this->statusLabels(), 'paymentStatuses' => $this->paymentStatusLabels(), ], 'merchants' => PlatformOrder::query()->with('merchant') ->select('merchant_id') ->whereNotNull('merchant_id') ->distinct() ->get() ->pluck('merchant') ->filter() ->unique('id') ->values(), 'plans' => PlatformOrder::query()->with('plan') ->select('plan_id') ->whereNotNull('plan_id') ->distinct() ->get() ->pluck('plan') ->filter() ->unique('id') ->values(), 'statusLabels' => $this->statusLabels(), 'paymentStatusLabels' => $this->paymentStatusLabels(), 'summaryStats' => [ 'total_orders' => (clone $baseQuery)->count(), 'paid_orders' => (clone $baseQuery)->where('payment_status', 'paid')->count(), 'activated_orders' => (clone $baseQuery)->where('status', 'activated')->count(), 'synced_orders' => (clone $baseQuery) ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NOT NULL") ->count(), 'failed_sync_orders' => (clone $baseQuery) ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL") ->count(), 'unsynced_orders' => (clone $baseQuery) ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL") ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL") ->count(), 'total_payable_amount' => $totalPayableAmount, 'total_paid_amount' => $totalPaidAmount, 'syncable_orders' => (clone $baseQuery) ->where('payment_status', 'paid') ->where('status', 'activated') ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL") ->count(), 'batch_synced_24h_orders' => (function () use ($baseQuery) { $since = now()->subHours(24)->format('Y-m-d H:i:s'); $q = (clone $baseQuery) ->whereRaw("JSON_EXTRACT(meta, '$.batch_activation.at') IS NOT NULL"); $driver = $q->getQuery()->getConnection()->getDriverName(); if ($driver === 'sqlite') { $q->whereRaw("JSON_EXTRACT(meta, '$.batch_activation.at') >= ?", [$since]); } else { $q->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.batch_activation.at')) >= ?", [$since]); } return $q->count(); })(), 'batch_mark_activated_24h_orders' => (function () use ($baseQuery) { $since = now()->subHours(24)->format('Y-m-d H:i:s'); $q = (clone $baseQuery) ->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_activated.at') IS NOT NULL"); $driver = $q->getQuery()->getConnection()->getDriverName(); if ($driver === 'sqlite') { $q->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_activated.at') >= ?", [$since]); } else { $q->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.batch_mark_activated.at')) >= ?", [$since]); } return $q->count(); })(), 'partially_refunded_orders' => (clone $baseQuery)->where('payment_status', 'partially_refunded')->count(), 'refunded_orders' => (clone $baseQuery)->where('payment_status', 'refunded')->count(), 'total_refunded_amount' => $totalRefundedAmount, 'receipt_orders' => (clone $baseQuery) ->where(function (Builder $q) { $q->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL") ->orWhereRaw("JSON_EXTRACT(meta, '$.payment_receipts[0].amount') IS NOT NULL"); }) ->count(), 'no_receipt_orders' => (clone $baseQuery) ->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NULL") ->whereRaw("JSON_EXTRACT(meta, '$.payment_receipts[0].amount') IS NULL") ->count(), 'refund_orders' => (clone $baseQuery) ->where(function (Builder $q) { $q->whereRaw("JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL") ->orWhereRaw("JSON_EXTRACT(meta, '$.refund_receipts[0].amount') IS NOT NULL"); }) ->count(), 'no_refund_orders' => (clone $baseQuery) ->whereRaw("JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NULL") ->whereRaw("JSON_EXTRACT(meta, '$.refund_receipts[0].amount') IS NULL") ->count(), 'total_receipt_amount' => $totalReceiptAmount, // 对账差额:回执总额 - 订单已付总额(当前筛选范围) 'reconciliation_delta' => (float) ($totalReceiptAmount - $totalPaidAmount), 'reconciliation_delta_note' => '回执总额 - 订单已付总额', // 对账不一致订单数(在当前筛选范围基础上叠加 mismatch 口径) 'reconcile_mismatch_orders' => (function () use ($filters) { $mismatchFilters = $filters; $mismatchFilters['reconcile_mismatch'] = '1'; return $this->applyFilters(PlatformOrder::query(), $mismatchFilters)->count(); })(), // 退款数据不一致订单数(在当前筛选范围基础上叠加 inconsistent 口径) 'refund_inconsistent_orders' => (function () use ($filters) { $f = $filters; $f['refund_inconsistent'] = '1'; return $this->applyFilters(PlatformOrder::query(), $f)->count(); })(), ], 'failedReasonStats' => $failedReasonStats, ]); } public function show(Request $request, PlatformOrder $order): View { $this->ensurePlatformAdmin($request); $order->loadMissing(['merchant', 'plan', 'siteSubscription']); return view('admin.platform_orders.show', [ 'order' => $order, 'statusLabels' => $this->statusLabels(), 'paymentStatusLabels' => $this->paymentStatusLabels(), ]); } public function activateSubscription(Request $request, PlatformOrder $order, SubscriptionActivationService $service): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); // 治理优先:当订单命中金额/状态不一致时,不建议直接同步订阅(避免把“带病订单”同步到订阅) if ($order->isReconcileMismatch() || $order->isRefundInconsistent()) { return redirect()->back()->with('warning', '当前订单命中「对账不一致/退款不一致」,为避免带病同步,请先完成金额/状态治理(补回执/核对退款/修正状态)后再同步订阅。'); } try { $subscription = $service->activateOrder($order->id, $admin->id); // 同步成功:清理失败记录(若存在)+ 写入审计记录 $meta = (array) ($order->meta ?? []); data_forget($meta, 'subscription_activation_error'); $audit = (array) (data_get($meta, 'audit', []) ?? []); $audit[] = [ 'action' => 'activate_subscription', 'scope' => 'single', 'at' => now()->toDateTimeString(), 'admin_id' => $admin->id, 'subscription_id' => $subscription->id, 'note' => '手动点击订单详情【同步订阅】', ]; data_set($meta, 'audit', $audit); $order->meta = $meta; $order->save(); } catch (\Throwable $e) { // 同步失败:写入错误信息,便于运营排查(可治理) $meta = (array) ($order->meta ?? []); data_set($meta, 'subscription_activation_error', [ 'message' => $e->getMessage(), 'at' => now()->toDateTimeString(), 'admin_id' => $admin->id, ]); $order->meta = $meta; $order->save(); return redirect()->back()->with('error', '订阅同步失败:' . $e->getMessage()); } return redirect()->back()->with('success', '订阅已同步:' . $subscription->subscription_no); } public function markPaidAndActivate(Request $request, PlatformOrder $order, SubscriptionActivationService $service): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); // 治理优先:若该订单已有退款轨迹(refund_summary/refund_receipts),不允许直接“标记支付并生效”,避免出现带退款的订单被强行推进并同步订阅 if ((float) $order->refundTotal() > 0) { return redirect()->back()->with('warning', '当前订单已存在退款记录/退款汇总,请先核对退款轨迹与订单状态后再处理(不建议直接标记支付并生效)。'); } // 最小状态推进:将订单标记为已支付 + 已生效,并补齐时间与金额字段 $now = now(); $order->payment_status = 'paid'; $order->status = 'activated'; $order->paid_at = $order->paid_at ?: $now; $order->activated_at = $order->activated_at ?: $now; $order->paid_amount = $order->paid_amount > 0 ? $order->paid_amount : $order->payable_amount; // 兼容:若尚未写入“支付回执”,则自动补一条(可治理) $meta = (array) ($order->meta ?? []); $receipts = (array) (data_get($meta, 'payment_receipts', []) ?? []); if (count($receipts) === 0) { $receipts[] = [ 'type' => 'manual_mark_paid', 'channel' => (string) ($order->payment_channel ?? ''), 'amount' => (float) $order->paid_amount, 'paid_at' => $order->paid_at ? $order->paid_at->format('Y-m-d H:i:s') : $now->toDateTimeString(), 'note' => '由【标记支付并生效】自动补记(可治理)', 'created_at' => $now->toDateTimeString(), 'admin_id' => $admin->id, ]; data_set($meta, 'payment_receipts', $receipts); // 扁平统计:避免在列表/汇总处频繁遍历支付回执数组(可治理) $totalPaid = 0.0; foreach ($receipts as $r) { $totalPaid += (float) (data_get($r, 'amount') ?? 0); } $latest = count($receipts) > 0 ? end($receipts) : null; data_set($meta, 'payment_summary', [ 'count' => count($receipts), 'total_amount' => $totalPaid, 'last_at' => (string) (data_get($latest, 'paid_at') ?? ''), 'last_amount' => (float) (data_get($latest, 'amount') ?? 0), 'last_channel' => (string) (data_get($latest, 'channel') ?? ''), ]); $order->meta = $meta; } $order->save(); // 立刻同步订阅 try { $subscription = $service->activateOrder($order->id, $admin->id); $meta = (array) ($order->meta ?? []); data_forget($meta, 'subscription_activation_error'); $order->meta = $meta; $order->save(); } catch (\Throwable $e) { $meta = (array) ($order->meta ?? []); data_set($meta, 'subscription_activation_error', [ 'message' => $e->getMessage(), 'at' => now()->toDateTimeString(), 'admin_id' => $admin->id, ]); $order->meta = $meta; $order->save(); return redirect()->back()->with('error', '订单已标记为已支付/已生效,但订阅同步失败:' . $e->getMessage()); } return redirect()->back()->with('success', '订单已标记支付并生效,订阅已同步:' . $subscription->subscription_no); } public function addPaymentReceipt(Request $request, PlatformOrder $order): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); $data = $request->validate([ 'type' => ['required', 'string', 'max:30'], 'channel' => ['nullable', 'string', 'max:30'], 'amount' => ['required', 'numeric', 'min:0'], 'paid_at' => ['nullable', 'date'], 'note' => ['nullable', 'string', 'max:2000'], ]); $now = now(); $meta = (array) ($order->meta ?? []); $receipts = (array) (data_get($meta, 'payment_receipts', []) ?? []); $receipts[] = [ 'type' => (string) $data['type'], 'channel' => (string) ($data['channel'] ?? ''), 'amount' => (float) $data['amount'], 'paid_at' => $data['paid_at'] ? (string) $data['paid_at'] : null, 'note' => (string) ($data['note'] ?? ''), 'created_at' => $now->toDateTimeString(), 'admin_id' => $admin->id, ]; data_set($meta, 'payment_receipts', $receipts); // 扁平统计:避免在列表/汇总处频繁遍历支付回执数组(可治理) $totalPaid = 0.0; foreach ($receipts as $r) { $totalPaid += (float) (data_get($r, 'amount') ?? 0); } $latest = count($receipts) > 0 ? end($receipts) : null; data_set($meta, 'payment_summary', [ 'count' => count($receipts), 'total_amount' => $totalPaid, 'last_at' => (string) (data_get($latest, 'paid_at') ?? ''), 'last_amount' => (float) (data_get($latest, 'amount') ?? 0), 'last_channel' => (string) (data_get($latest, 'channel') ?? ''), ]); $order->meta = $meta; $order->save(); return redirect()->back()->with('success', '已追加支付回执记录(仅用于对账留痕,不自动改状态)。'); } public function addRefundReceipt(Request $request, PlatformOrder $order): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); $data = $request->validate([ 'type' => ['required', 'string', 'max:30'], 'channel' => ['nullable', 'string', 'max:30'], 'amount' => ['required', 'numeric', 'min:0'], 'refunded_at' => ['nullable', 'date'], 'note' => ['nullable', 'string', 'max:2000'], ]); $now = now(); $meta = (array) ($order->meta ?? []); $refunds = (array) (data_get($meta, 'refund_receipts', []) ?? []); $refunds[] = [ 'type' => (string) $data['type'], 'channel' => (string) ($data['channel'] ?? ''), 'amount' => (float) $data['amount'], 'refunded_at' => $data['refunded_at'] ? (string) $data['refunded_at'] : null, 'note' => (string) ($data['note'] ?? ''), 'created_at' => $now->toDateTimeString(), 'admin_id' => $admin->id, ]; data_set($meta, 'refund_receipts', $refunds); // 扁平统计:避免在列表/汇总处频繁遍历退款数组(可治理) $totalRefunded = 0.0; foreach ($refunds as $r) { $totalRefunded += (float) (data_get($r, 'amount') ?? 0); } $latestRefund = count($refunds) > 0 ? end($refunds) : null; data_set($meta, 'refund_summary', [ 'count' => count($refunds), 'total_amount' => $totalRefunded, 'last_at' => (string) (data_get($latestRefund, 'refunded_at') ?? ''), 'last_amount' => (float) (data_get($latestRefund, 'amount') ?? 0), 'last_channel' => (string) (data_get($latestRefund, 'channel') ?? ''), ]); // 可治理辅助:自动推进退款标记(仅当退款金额>0 时) // 注意:允许从 paid / partially_refunded 推进到 partially_refunded / refunded // 且不会把已 refunded 的订单降级。 if ((float) $data['amount'] > 0 && in_array($order->payment_status, ['paid', 'partially_refunded'], true)) { $paidAmount = (float) ($order->paid_amount ?? 0); // 退款总额 >= 已付金额 => 视为已退款;否则视为部分退款 if ($paidAmount > 0 && $totalRefunded >= $paidAmount) { $order->payment_status = 'refunded'; $order->refunded_at = $order->refunded_at ?: now(); } else { $order->payment_status = 'partially_refunded'; $order->refunded_at = $order->refunded_at ?: now(); } } $order->meta = $meta; $order->save(); return redirect()->back()->with('success', '已追加退款记录(用于退款轨迹留痕)。'); } public function markRefunded(Request $request, PlatformOrder $order): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); $paidAmount = (float) ($order->paid_amount ?? 0); if ($paidAmount <= 0) { return redirect()->back()->with('warning', '当前订单已付金额为 0,无法标记为已退款。'); } if ((string) $order->payment_status === 'refunded') { return redirect()->back()->with('warning', '当前订单已是已退款状态,无需重复操作。'); } // 安全阀:仅允许在“退款总额已达到/超过已付金额”时标记为已退款 $refundTotal = (float) $this->refundTotalForOrder($order); if (round($refundTotal * 100) + 1 < round($paidAmount * 100)) { return redirect()->back()->with('warning', '退款总额尚未达到已付金额,无法标记为已退款。请先核对/补齐退款记录。'); } $now = now(); $order->payment_status = 'refunded'; $order->refunded_at = $order->refunded_at ?: $now; $meta = (array) ($order->meta ?? []); $audit = (array) (data_get($meta, 'audit', []) ?? []); $audit[] = [ 'action' => 'mark_refunded', 'scope' => 'single', 'at' => $now->toDateTimeString(), 'admin_id' => $admin->id, 'note' => '手动标记为已退款(仅修正支付状态,不自动写退款回执)', 'snapshot' => [ 'paid_amount' => $paidAmount, 'refund_total' => $refundTotal, ], ]; data_set($meta, 'audit', $audit); $order->meta = $meta; $order->save(); return redirect()->back()->with('success', '已将订单支付状态标记为已退款(未自动写入退款回执)。'); } public function markPartiallyRefunded(Request $request, PlatformOrder $order): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); $paidAmount = (float) ($order->paid_amount ?? 0); if ($paidAmount <= 0) { return redirect()->back()->with('warning', '当前订单已付金额为 0,无法标记为部分退款。'); } if ((string) $order->payment_status === 'partially_refunded') { return redirect()->back()->with('warning', '当前订单已是部分退款状态,无需重复操作。'); } // 安全阀:部分退款需要“退款总额>0 且未达到已付金额” $refundTotal = (float) $this->refundTotalForOrder($order); if (round($refundTotal * 100) <= 0) { return redirect()->back()->with('warning', '退款总额为 0,无法标记为部分退款。'); } if (round($refundTotal * 100) + 1 >= round($paidAmount * 100)) { return redirect()->back()->with('warning', '退款总额已达到/超过已付金额,建议标记为已退款。'); } $now = now(); $order->payment_status = 'partially_refunded'; $order->refunded_at = $order->refunded_at ?: $now; $meta = (array) ($order->meta ?? []); $audit = (array) (data_get($meta, 'audit', []) ?? []); $audit[] = [ 'action' => 'mark_partially_refunded', 'scope' => 'single', 'at' => $now->toDateTimeString(), 'admin_id' => $admin->id, 'note' => '手动标记为部分退款(仅修正支付状态,不自动写退款回执)', 'snapshot' => [ 'paid_amount' => $paidAmount, 'refund_total' => $refundTotal, ], ]; data_set($meta, 'audit', $audit); $order->meta = $meta; $order->save(); return redirect()->back()->with('success', '已将订单支付状态标记为部分退款(未自动写入退款回执)。'); } public function markPaidStatus(Request $request, PlatformOrder $order): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); $paidAmount = (float) ($order->paid_amount ?? 0); if ($paidAmount <= 0) { return redirect()->back()->with('warning', '当前订单已付金额为 0,无法标记为已支付。'); } if ((string) $order->payment_status === 'unpaid') { return redirect()->back()->with('warning', '当前订单为未支付状态,不允许直接标记为已支付,请使用「标记支付并生效」或补回执/金额后再处理。'); } if ((string) $order->payment_status === 'paid') { return redirect()->back()->with('warning', '当前订单已是已支付状态,无需重复操作。'); } $now = now(); $order->payment_status = 'paid'; // paid 状态不强依赖 refunded_at,这里不做清空,避免丢历史痕迹 $meta = (array) ($order->meta ?? []); $audit = (array) (data_get($meta, 'audit', []) ?? []); $audit[] = [ 'action' => 'mark_paid_status', 'scope' => 'single', 'at' => $now->toDateTimeString(), 'admin_id' => $admin->id, 'note' => '手动标记为已支付(仅修正支付状态,不自动写回执/退款回执)', 'snapshot' => [ 'paid_amount' => $paidAmount, 'refund_total' => (float) $this->refundTotalForOrder($order), ], ]; data_set($meta, 'audit', $audit); $order->meta = $meta; $order->save(); return redirect()->back()->with('success', '已将订单支付状态标记为已支付(未自动写入回执/退款回执)。'); } public function markActivated(Request $request, PlatformOrder $order): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); // 仅标记“已生效”:用于处理已支付但未生效的订单(不改 payment_status) if ($order->payment_status !== 'paid') { return redirect()->back()->with('warning', '当前订单尚未支付,无法仅标记为已生效。'); } if ($order->status === 'activated') { return redirect()->back()->with('warning', '当前订单已是已生效状态,无需重复操作。'); } $now = now(); $order->status = 'activated'; $order->activated_at = $order->activated_at ?: $now; $order->save(); // 轻量审计:记录这次“仅标记生效”的动作,便于追溯 $meta = (array) ($order->meta ?? []); $audit = (array) (data_get($meta, 'audit', []) ?? []); $audit[] = [ 'action' => 'mark_activated', 'scope' => 'single', 'at' => $now->toDateTimeString(), 'admin_id' => $admin->id, ]; data_set($meta, 'audit', $audit); $order->meta = $meta; $order->save(); return redirect()->back()->with('success', '订单已标记为已生效(未修改支付状态)。'); } public function export(Request $request): StreamedResponse { $this->ensurePlatformAdmin($request); $filters = [ 'status' => trim((string) $request->query('status', '')), 'payment_status' => trim((string) $request->query('payment_status', '')), 'merchant_id' => trim((string) $request->query('merchant_id', '')), 'plan_id' => trim((string) $request->query('plan_id', '')), // 精确过滤:订阅ID(用于从订阅详情页跳转到平台订单列表时锁定范围) 'site_subscription_id' => trim((string) $request->query('site_subscription_id', '')), 'fail_only' => (string) $request->query('fail_only', ''), 'synced_only' => (string) $request->query('synced_only', ''), 'sync_status' => trim((string) $request->query('sync_status', '')), 'keyword' => trim((string) $request->query('keyword', '')), // 同步失败原因关键词:用于快速定位同原因失败订单(可治理) 'sync_error_keyword' => trim((string) $request->query('sync_error_keyword', '')), // 只看“可同步订阅”的订单:已支付 + 已生效 + 未同步(用于运营快速处理) 'syncable_only' => (string) $request->query('syncable_only', ''), // 只看最近 24 小时批量同步过的订单(可治理追踪) 'batch_synced_24h' => (string) $request->query('batch_synced_24h', ''), // 只看最近 24 小时批量“仅标记为已生效”过的订单(可治理追踪) 'batch_mark_activated_24h' => (string) $request->query('batch_mark_activated_24h', ''), // 只看“对账不一致”的订单(粗版):meta.payment_summary.total_amount 与 paid_amount 不一致 'reconcile_mismatch' => (string) $request->query('reconcile_mismatch', ''), // 支付回执筛选:has(有回执)/none(无回执) 'receipt_status' => trim((string) $request->query('receipt_status', '')), // 退款轨迹筛选:has(有退款)/none(无退款) 'refund_status' => trim((string) $request->query('refund_status', '')), // 退款数据不一致(可治理):基于 refund_summary.total_amount 与 paid_amount 对比 'refund_inconsistent' => (string) $request->query('refund_inconsistent', ''), ]; $includeMeta = (string) $request->query('include_meta', '') === '1'; $query = $this->applyFilters( PlatformOrder::query()->with(['merchant', 'plan', 'siteSubscription']), $filters )->orderBy('id'); $filename = 'platform_orders_' . now()->format('Ymd_His') . '.csv'; return response()->streamDownload(function () use ($query, $includeMeta) { $out = fopen('php://output', 'w'); // UTF-8 BOM,避免 Excel 打开中文乱码 fwrite($out, "\xEF\xBB\xBF"); $headers = [ 'ID', '订单号', '站点', '套餐', '订单类型', '订单状态', '支付状态', '应付金额', '已付金额', '下单时间', '支付时间', '生效时间', '同步状态', '订阅ID', '订阅号', '订阅到期', '同步时间', '同步失败原因', '同步失败时间', '最近批量生效时间', '最近批量生效管理员', '支付回执数', '最近回执时间', '最近回执金额', '最近回执渠道', '退款记录数', '最近退款时间', '最近退款金额', '最近退款渠道', '退款总额', '回执总额', '对账差额', ]; if ($includeMeta) { $headers[] = '原始meta(JSON)'; } fputcsv($out, $headers); $query->chunkById(500, function ($orders) use ($out, $includeMeta) { foreach ($orders as $order) { $syncedId = (int) data_get($order->meta, 'subscription_activation.subscription_id', 0); $syncErr = (string) (data_get($order->meta, 'subscription_activation_error.message') ?? ''); if ($syncedId > 0) { $syncStatus = '已同步'; } elseif ($syncErr !== '') { $syncStatus = '同步失败'; } else { $syncStatus = '未同步'; } $receipts = (array) (data_get($order->meta, 'payment_receipts', []) ?? []); $receiptSummaryCount = data_get($order->meta, 'payment_summary.count'); $receiptCount = $receiptSummaryCount !== null ? (int) $receiptSummaryCount : count($receipts); $latestReceipt = count($receipts) > 0 ? end($receipts) : null; $refunds = (array) (data_get($order->meta, 'refund_receipts', []) ?? []); $refundSummaryCount = data_get($order->meta, 'refund_summary.count'); $refundCount = $refundSummaryCount !== null ? (int) $refundSummaryCount : count($refunds); $latestRefund = count($refunds) > 0 ? end($refunds) : null; $row = [ $order->id, $order->order_no, $order->merchant?->name ?? '', $order->plan_name ?: ($order->plan?->name ?? ''), $order->order_type, $order->status, $order->payment_status, (float) $order->payable_amount, (float) $order->paid_amount, optional($order->placed_at)->format('Y-m-d H:i:s') ?: '', optional($order->paid_at)->format('Y-m-d H:i:s') ?: '', optional($order->activated_at)->format('Y-m-d H:i:s') ?: '', $syncStatus, (int) ($order->site_subscription_id ?? 0), $order->siteSubscription?->subscription_no ?: '', optional($order->siteSubscription?->ends_at)->format('Y-m-d H:i:s') ?: '', (string) (data_get($order->meta, 'subscription_activation.synced_at') ?? ''), $syncErr, (string) (data_get($order->meta, 'subscription_activation_error.at') ?? ''), (string) (data_get($order->meta, 'batch_mark_activated.at') ?? ''), (string) (data_get($order->meta, 'batch_mark_activated.admin_id') ?? ''), $receiptCount, (string) (data_get($latestReceipt, 'paid_at') ?? ''), (float) (data_get($latestReceipt, 'amount') ?? 0), (string) (data_get($latestReceipt, 'channel') ?? ''), $refundCount, (string) (data_get($latestRefund, 'refunded_at') ?? ''), (float) (data_get($latestRefund, 'amount') ?? 0), (string) (data_get($latestRefund, 'channel') ?? ''), (float) $order->refundTotal(), (float) $order->receiptTotal(), (float) $order->receiptTotal() - (float) ($order->paid_amount ?? 0), ]; if ($includeMeta) { $row[] = json_encode($order->meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } fputcsv($out, $row); } }); fclose($out); }, $filename, [ 'Content-Type' => 'text/csv; charset=UTF-8', ]); } public function batchActivateSubscriptions(Request $request, SubscriptionActivationService $service): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); // 支持两种 scope: // - scope=filtered:只处理当前筛选范围内的订单(更安全,默认) // - scope=all:处理全部订单(谨慎) $scope = (string) $request->input('scope', 'filtered'); $filters = [ 'status' => trim((string) $request->input('status', '')), 'payment_status' => trim((string) $request->input('payment_status', '')), 'merchant_id' => trim((string) $request->input('merchant_id', '')), 'plan_id' => trim((string) $request->input('plan_id', '')), 'site_subscription_id' => trim((string) $request->input('site_subscription_id', '')), 'fail_only' => (string) $request->input('fail_only', ''), 'synced_only' => (string) $request->input('synced_only', ''), 'sync_status' => trim((string) $request->input('sync_status', '')), 'keyword' => trim((string) $request->input('keyword', '')), 'sync_error_keyword' => trim((string) $request->input('sync_error_keyword', '')), 'syncable_only' => (string) $request->input('syncable_only', ''), 'batch_synced_24h' => (string) $request->input('batch_synced_24h', ''), // 与列表页筛选保持一致(可治理):用于在批量操作后仍能回到同一口径 'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''), 'reconcile_mismatch' => (string) $request->input('reconcile_mismatch', ''), 'receipt_status' => trim((string) $request->input('receipt_status', '')), 'refund_status' => trim((string) $request->input('refund_status', '')), 'refund_inconsistent' => (string) $request->input('refund_inconsistent', ''), ]; // 防误操作:批量同步默认要求先勾选“只看可同步”,避免无意识扩大处理范围 if ($scope === 'filtered' && ($filters['syncable_only'] ?? '') !== '1') { return redirect()->back()->with('warning', '为避免误操作,请先在筛选条件中勾选「只看可同步」,再执行批量同步订阅。'); } // 防误操作(治理优先):当筛选集合同时命中“对账不一致/退款不一致”时,不允许直接批量同步,避免把“带病订单”同步到订阅 if ($scope === 'filtered' && ($filters['syncable_only'] ?? '') === '1' && ((string) ($filters['reconcile_mismatch'] ?? '') === '1' || (string) ($filters['refund_inconsistent'] ?? '') === '1')) { return redirect()->back()->with('warning', '当前筛选集合包含「对账不一致/退款不一致」订单,为避免带病同步,请先完成金额/状态治理(补回执/核对退款)后再批量同步订阅。'); } // 防误操作:scope=all 需要二次确认 if ($scope === 'all' && (string) $request->input('confirm', '') !== 'YES') { return redirect()->back()->with('warning', '为避免误操作,执行全量批量同步前请在确认框输入 YES。'); } $query = PlatformOrder::query(); if ($scope === 'filtered') { $query = $this->applyFilters($query, $filters); } // 只处理“可同步”的订单(双保险,避免误操作) $query = $query ->where('payment_status', 'paid') ->where('status', 'activated') ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL"); $limit = (int) $request->input('limit', 50); $limit = max(1, min(500, $limit)); $matchedTotal = (clone $query)->count(); // 默认按最新订单优先处理:避免 seed/demo 数据干扰测试,同时也更符合“先处理新问题”的运营直觉 $orders = $query->orderByDesc('id')->limit($limit)->get(['id']); $processed = $orders->count(); $success = 0; $failed = 0; $failedReasonCounts = []; // 筛选摘要:用于审计记录(避免每条订单都手写拼接,且便于追溯本次批量处理口径) $filterSummaryParts = []; foreach ($filters as $k => $v) { if ((string) $v !== '') { $filterSummaryParts[] = $k . '=' . (string) $v; } } $filterSummary = implode('&', $filterSummaryParts); foreach ($orders as $orderRow) { try { $subscription = $service->activateOrder($orderRow->id, $admin->id); // 轻量审计:记录批量同步动作(方便追溯) $order = PlatformOrder::query()->find($orderRow->id); if ($order) { $meta = (array) ($order->meta ?? []); $audit = (array) (data_get($meta, 'audit', []) ?? []); $nowStr = now()->toDateTimeString(); $audit[] = [ 'action' => 'batch_activate_subscription', 'scope' => $scope, 'at' => $nowStr, 'admin_id' => $admin->id, 'subscription_id' => $subscription->id, 'filters' => $filterSummary, 'note' => '批量同步订阅(limit=' . $limit . ', matched=' . $matchedTotal . ', processed=' . $processed . ')', ]; data_set($meta, 'audit', $audit); // 便于筛选/统计:记录最近一次批量同步信息(扁平字段) data_set($meta, 'batch_activation', [ 'at' => $nowStr, 'admin_id' => $admin->id, 'scope' => $scope, ]); $order->meta = $meta; $order->save(); } $success++; } catch (\Throwable $e) { $failed++; $reason = trim((string) $e->getMessage()); $reason = $reason !== '' ? $reason : '未知错误'; $failedReasonCounts[$reason] = ($failedReasonCounts[$reason] ?? 0) + 1; // 批量同步失败也需要可治理:写入失败原因到订单 meta,便于后续筛选/导出/清理 $order = PlatformOrder::query()->find($orderRow->id); if ($order) { $meta = (array) ($order->meta ?? []); data_set($meta, 'subscription_activation_error', [ 'message' => $reason, 'at' => now()->toDateTimeString(), 'admin_id' => $admin->id, ]); $order->meta = $meta; $order->save(); } } } $msg = '批量同步订阅完成:成功 ' . $success . ' 条,失败 ' . $failed . ' 条(命中 ' . $matchedTotal . ' 条,本次处理 ' . $processed . ' 条,limit=' . $limit . ')'; if ($failed > 0 && count($failedReasonCounts) > 0) { arsort($failedReasonCounts); $top = array_slice($failedReasonCounts, 0, 3, true); $topText = collect($top)->map(function ($cnt, $reason) { $reason = mb_substr((string) $reason, 0, 60); return $reason . '(' . $cnt . ')'; })->implode(';'); $msg .= ';失败原因Top:' . $topText; } return redirect()->back()->with('success', $msg); } public function batchMarkActivated(Request $request): RedirectResponse { $admin = $this->ensurePlatformAdmin($request); // 支持两种 scope: // - scope=filtered:只处理当前筛选范围内的订单(更安全,默认) // - scope=all:处理全部订单(谨慎) $scope = (string) $request->input('scope', 'filtered'); $filters = [ 'status' => trim((string) $request->input('status', '')), 'payment_status' => trim((string) $request->input('payment_status', '')), 'merchant_id' => trim((string) $request->input('merchant_id', '')), 'plan_id' => trim((string) $request->input('plan_id', '')), 'site_subscription_id' => trim((string) $request->input('site_subscription_id', '')), 'fail_only' => (string) $request->input('fail_only', ''), 'synced_only' => (string) $request->input('synced_only', ''), 'sync_status' => trim((string) $request->input('sync_status', '')), 'keyword' => trim((string) $request->input('keyword', '')), 'sync_error_keyword' => trim((string) $request->input('sync_error_keyword', '')), 'syncable_only' => (string) $request->input('syncable_only', ''), 'batch_synced_24h' => (string) $request->input('batch_synced_24h', ''), // 与列表页筛选保持一致(可治理):用于在批量操作后仍能回到同一口径 'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''), 'reconcile_mismatch' => (string) $request->input('reconcile_mismatch', ''), 'receipt_status' => trim((string) $request->input('receipt_status', '')), 'refund_status' => trim((string) $request->input('refund_status', '')), 'refund_inconsistent' => (string) $request->input('refund_inconsistent', ''), ]; // 防误操作:批量“仅标记为已生效”默认要求当前筛选口径为「已支付 + 待处理(pending)」 if ($scope === 'filtered') { if (($filters['payment_status'] ?? '') !== 'paid' || ($filters['status'] ?? '') !== 'pending') { return redirect()->back()->with('warning', '为避免误操作,请先筛选「支付状态=已支付」且「订单状态=待处理」,再执行批量仅标记为已生效。'); } } // 防误操作:scope=all 需要二次确认 if ($scope === 'all' && (string) $request->input('confirm', '') !== 'YES') { return redirect()->back()->with('warning', '为避免误操作,执行全量批量生效前请在确认框输入 YES。'); } $query = PlatformOrder::query(); if ($scope === 'filtered') { $query = $this->applyFilters($query, $filters); } // 只处理“已支付 + 待处理”的订单(双保险) $query = $query ->where('payment_status', 'paid') ->where('status', 'pending'); $limit = (int) $request->input('limit', 50); $limit = max(1, min(500, $limit)); $matchedTotal = (clone $query)->count(); $orders = $query->orderByDesc('id')->limit($limit)->get(['id']); $processed = $orders->count(); $success = 0; $nowStr = now()->toDateTimeString(); // 筛选摘要:用于审计记录(便于追溯本次批量处理口径) $filterSummaryParts = []; foreach ($filters as $k => $v) { if ((string) $v !== '') { $filterSummaryParts[] = $k . '=' . (string) $v; } } $filterSummary = implode('&', $filterSummaryParts); foreach ($orders as $row) { $order = PlatformOrder::query()->find($row->id); if (! $order) { continue; } // 再次防御:仅推进 pending if ($order->payment_status !== 'paid' || $order->status !== 'pending') { continue; } $order->status = 'activated'; $order->activated_at = $order->activated_at ?: now(); $meta = (array) ($order->meta ?? []); $audit = (array) (data_get($meta, 'audit', []) ?? []); $audit[] = [ 'action' => 'batch_mark_activated', 'scope' => $scope, 'at' => $nowStr, 'admin_id' => $admin->id, 'filters' => $filterSummary, 'note' => '批量仅标记为已生效(limit=' . $limit . ', matched=' . $matchedTotal . ', processed=' . $processed . ')', ]; data_set($meta, 'audit', $audit); // 便于筛选/统计:记录最近一次批量生效信息(扁平字段) data_set($meta, 'batch_mark_activated', [ 'at' => $nowStr, 'admin_id' => $admin->id, 'scope' => $scope, ]); $order->meta = $meta; $order->save(); $success++; } $msg = '批量仅标记为已生效完成:成功 ' . $success . ' 条(命中 ' . $matchedTotal . ' 条,本次处理 ' . $processed . ' 条,limit=' . $limit . ')'; return redirect()->back()->with('success', $msg); } public function clearSyncErrors(Request $request): RedirectResponse { $this->ensurePlatformAdmin($request); // 支持两种模式: // - scope=all(默认):清理所有订单的失败标记(需要 confirm=YES) // - scope=filtered:仅清理当前筛选结果命中的订单(更安全) $scope = (string) $request->input('scope', 'all'); // 防误操作:scope=all 需要二次确认 if ($scope === 'all' && (string) $request->input('confirm', '') !== 'YES') { return redirect()->back()->with('warning', '为避免误操作,清除全部失败标记前请在确认框输入 YES。'); } $filters = [ 'status' => trim((string) $request->input('status', '')), 'payment_status' => trim((string) $request->input('payment_status', '')), 'merchant_id' => trim((string) $request->input('merchant_id', '')), 'plan_id' => trim((string) $request->input('plan_id', '')), 'site_subscription_id' => trim((string) $request->input('site_subscription_id', '')), 'fail_only' => (string) $request->input('fail_only', ''), 'synced_only' => (string) $request->input('synced_only', ''), 'sync_status' => trim((string) $request->input('sync_status', '')), 'keyword' => trim((string) $request->input('keyword', '')), 'sync_error_keyword' => trim((string) $request->input('sync_error_keyword', '')), 'syncable_only' => (string) $request->input('syncable_only', ''), 'batch_synced_24h' => (string) $request->input('batch_synced_24h', ''), // 与列表页筛选保持一致(可治理):用于在批量操作后仍能回到同一口径 'batch_mark_activated_24h' => (string) $request->input('batch_mark_activated_24h', ''), 'reconcile_mismatch' => (string) $request->input('reconcile_mismatch', ''), 'receipt_status' => trim((string) $request->input('receipt_status', '')), 'refund_status' => trim((string) $request->input('refund_status', '')), 'refund_inconsistent' => (string) $request->input('refund_inconsistent', ''), ]; $query = PlatformOrder::query() ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL"); if ($scope === 'filtered') { $query = $this->applyFilters($query, $filters); } $orders = $query->get(['id', 'meta']); $matched = $orders->count(); $cleared = 0; foreach ($orders as $order) { $meta = (array) ($order->meta ?? []); if (! data_get($meta, 'subscription_activation_error')) { continue; } data_forget($meta, 'subscription_activation_error'); // 轻量审计:记录清理动作(不做独立表,先落 meta,便于排查) $audit = (array) (data_get($meta, 'audit', []) ?? []); $audit[] = [ 'action' => 'clear_sync_error', 'scope' => $scope, 'at' => now()->toDateTimeString(), 'admin_id' => $this->platformAdminId($request), ]; data_set($meta, 'audit', $audit); $order->meta = $meta; $order->save(); $cleared++; } $msg = $scope === 'filtered' ? '已清除当前筛选范围内的同步失败标记:' : '已清除全部订单的同步失败标记:'; return redirect()->back()->with('success', $msg . $cleared . ' 条(命中 ' . $matched . ' 条)'); } protected function applyFilters(Builder $query, array $filters): Builder { return $query ->when($filters['status'] !== '', fn (Builder $builder) => $builder->where('status', $filters['status'])) ->when($filters['payment_status'] !== '', fn (Builder $builder) => $builder->where('payment_status', $filters['payment_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['site_subscription_id'] ?? '') !== '', fn (Builder $builder) => $builder->where('site_subscription_id', (int) $filters['site_subscription_id'])) ->when(($filters['fail_only'] ?? '') !== '', function (Builder $builder) { // 只看同步失败:meta.subscription_activation_error.message 存在即视为失败 $builder->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL"); }) ->when(($filters['synced_only'] ?? '') !== '', function (Builder $builder) { // 只看已同步:meta.subscription_activation.subscription_id 存在即视为已同步 $builder->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NOT NULL"); }) ->when(($filters['sync_status'] ?? '') !== '', function (Builder $builder) use ($filters) { // 同步状态筛选:unsynced / synced / failed if (($filters['sync_status'] ?? '') === 'synced') { $builder->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NOT NULL") ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL"); } elseif (($filters['sync_status'] ?? '') === 'failed') { $builder->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NOT NULL"); } elseif (($filters['sync_status'] ?? '') === 'unsynced') { $builder->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL") ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') IS NULL"); } }) ->when(($filters['keyword'] ?? '') !== '', function (Builder $builder) use ($filters) { // 关键词搜索:订单号 / 站点名称 / 订阅号 $kw = trim((string) ($filters['keyword'] ?? '')); if ($kw === '') { return; } $builder->where(function (Builder $q) use ($kw) { $q->where('order_no', 'like', '%' . $kw . '%') ->orWhere('order_type', 'like', '%' . $kw . '%') ->orWhere('plan_name', 'like', '%' . $kw . '%') ->orWhereHas('merchant', function (Builder $mq) use ($kw) { $mq->where('name', 'like', '%' . $kw . '%') ->orWhere('slug', 'like', '%' . $kw . '%'); }) ->orWhereHas('siteSubscription', function (Builder $sq) use ($kw) { $sq->where('subscription_no', 'like', '%' . $kw . '%'); }); if (ctype_digit($kw)) { $q->orWhere('id', (int) $kw); } }); }) ->when(($filters['sync_error_keyword'] ?? '') !== '', function (Builder $builder) use ($filters) { // 同步失败原因关键词:subscription_activation_error.message like $kw = trim((string) ($filters['sync_error_keyword'] ?? '')); if ($kw === '') { return; } $driver = $builder->getQuery()->getConnection()->getDriverName(); if ($driver === 'sqlite') { $builder->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation_error.message') LIKE ?", ['%' . $kw . '%']); } else { $builder->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.subscription_activation_error.message')) LIKE ?", ['%' . $kw . '%']); } }) ->when(($filters['syncable_only'] ?? '') !== '', function (Builder $builder) { // 只看可同步:已支付 + 已生效 + 尚未写入 subscription_activation.subscription_id $builder->where('payment_status', 'paid') ->where('status', 'activated') ->whereRaw("JSON_EXTRACT(meta, '$.subscription_activation.subscription_id') IS NULL"); }) ->when(($filters['batch_synced_24h'] ?? '') !== '', function (Builder $builder) { // 只看最近 24 小时批量同步过的订单(基于 meta.batch_activation.at) $since = now()->subHours(24)->format('Y-m-d H:i:s'); $builder->whereRaw("JSON_EXTRACT(meta, '$.batch_activation.at') IS NOT NULL"); // sqlite 测试库没有 JSON_UNQUOTE(),需要做兼容 $driver = $builder->getQuery()->getConnection()->getDriverName(); if ($driver === 'sqlite') { $builder->whereRaw("JSON_EXTRACT(meta, '$.batch_activation.at') >= ?", [$since]); } else { $builder->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.batch_activation.at')) >= ?", [$since]); } }) ->when(($filters['batch_mark_activated_24h'] ?? '') !== '', function (Builder $builder) { // 只看最近 24 小时批量“仅标记为已生效”过的订单(基于 meta.batch_mark_activated.at) $since = now()->subHours(24)->format('Y-m-d H:i:s'); $builder->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_activated.at') IS NOT NULL"); $driver = $builder->getQuery()->getConnection()->getDriverName(); if ($driver === 'sqlite') { $builder->whereRaw("JSON_EXTRACT(meta, '$.batch_mark_activated.at') >= ?", [$since]); } else { $builder->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.batch_mark_activated.at')) >= ?", [$since]); } }) ->when(($filters['reconcile_mismatch'] ?? '') !== '', function (Builder $builder) { // 只看“对账不一致”的订单:支付回执总额 与订单 paid_amount 不一致 // 口径:优先使用 meta.payment_summary.total_amount;若为空,则回退汇总 meta.payment_receipts[].amount // 注意:该筛选需要读取 JSON 字段,MySQL/SQLite 写法略有差异。 $driver = $builder->getQuery()->getConnection()->getDriverName(); if ($driver === 'sqlite') { // sqlite 下 JSON_EXTRACT 直接返回标量(数值或字符串),这里用“按分”取整避免浮点误差导致 0.01 边界不稳定 // total_cents = (payment_summary.total_amount 存在 ? summary*100 : sum(payment_receipts[].amount)*100) $builder->whereRaw("ABS(ROUND((CASE WHEN JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL THEN CAST(JSON_EXTRACT(meta, '$.payment_summary.total_amount') AS REAL) ELSE (SELECT IFNULL(SUM(CAST(JSON_EXTRACT(value, '$.amount') AS REAL)), 0) FROM json_each(COALESCE(JSON_EXTRACT(meta, '$.payment_receipts'), '[]'))) END) * 100) - ROUND(paid_amount * 100)) >= 1"); } else { // MySQL 下 JSON_EXTRACT 返回 JSON,需要 JSON_UNQUOTE 再 cast;同样按分取整避免浮点误差 // total_cents = (payment_summary.total_amount 存在 ? summary*100 : SUM(payment_receipts[].amount)*100) $builder->whereRaw("ABS(ROUND((CASE WHEN JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(meta, '$.payment_summary.total_amount')) AS DECIMAL(12,2)) ELSE (SELECT IFNULL(SUM(j.amount), 0) FROM JSON_TABLE(meta, '$.payment_receipts[*]' COLUMNS(amount DECIMAL(12,2) PATH '$.amount')) j) END) * 100) - ROUND(paid_amount * 100)) >= 1"); } }) ->when(($filters['receipt_status'] ?? '') !== '', function (Builder $builder) use ($filters) { // 支付回执筛选: // - has:有回执(payment_summary.total_amount 存在 或 payment_receipts[0].amount 存在) // - none:无回执(两者都不存在) $status = (string) ($filters['receipt_status'] ?? ''); if ($status === 'has') { $builder->where(function (Builder $q) { $q->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NOT NULL") ->orWhereRaw("JSON_EXTRACT(meta, '$.payment_receipts[0].amount') IS NOT NULL"); }); } elseif ($status === 'none') { $builder->whereRaw("JSON_EXTRACT(meta, '$.payment_summary.total_amount') IS NULL") ->whereRaw("JSON_EXTRACT(meta, '$.payment_receipts[0].amount') IS NULL"); } }) ->when(($filters['refund_status'] ?? '') !== '', function (Builder $builder) use ($filters) { // 退款轨迹筛选: // - has:有退款(refund_summary.total_amount 存在 或 refund_receipts[0].amount 存在) // - none:无退款(两者都不存在) $status = (string) ($filters['refund_status'] ?? ''); if ($status === 'has') { $builder->where(function (Builder $q) { $q->whereRaw("JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL") ->orWhereRaw("JSON_EXTRACT(meta, '$.refund_receipts[0].amount') IS NOT NULL"); }); } elseif ($status === 'none') { $builder->whereRaw("JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NULL") ->whereRaw("JSON_EXTRACT(meta, '$.refund_receipts[0].amount') IS NULL"); } }) ->when(($filters['refund_inconsistent'] ?? '') !== '', function (Builder $builder) { // 退款数据不一致(可治理): // - 状态=refunded 但 退款总额 < 已付金额(允许 0.01 容差) // - 状态!=refunded 且 已付金额>0 且 退款总额 >= 已付金额 // 退款总额口径:优先 refund_summary.total_amount;缺省回退汇总 refund_receipts[].amount $driver = $builder->getQuery()->getConnection()->getDriverName(); if ($driver === 'sqlite') { $refundTotalExpr = "(CASE WHEN JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL THEN CAST(JSON_EXTRACT(meta, '$.refund_summary.total_amount') AS REAL) ELSE (SELECT IFNULL(SUM(CAST(JSON_EXTRACT(value, '$.amount') AS REAL)), 0) FROM json_each(COALESCE(JSON_EXTRACT(meta, '$.refund_receipts'), '[]'))) END)"; $builder->where(function (Builder $q) use ($refundTotalExpr) { // refunded 但退款不够 $q->where(function (Builder $q2) use ($refundTotalExpr) { $q2->where('payment_status', 'refunded') ->whereRaw("paid_amount > 0") ->whereRaw("(ROUND($refundTotalExpr * 100) + 1) < ROUND(paid_amount * 100)"); }) // 非 refunded 但退款已达到/超过已付 ->orWhere(function (Builder $q2) use ($refundTotalExpr) { $q2->where('payment_status', '!=', 'refunded') ->whereRaw("paid_amount > 0") ->whereRaw("ROUND($refundTotalExpr * 100) >= ROUND(paid_amount * 100)"); }); }); } else { $refundTotalExpr = "(CASE WHEN JSON_EXTRACT(meta, '$.refund_summary.total_amount') IS NOT NULL THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(meta, '$.refund_summary.total_amount')) AS DECIMAL(12,2)) ELSE (SELECT IFNULL(SUM(j.amount), 0) FROM JSON_TABLE(meta, '$.refund_receipts[*]' COLUMNS(amount DECIMAL(12,2) PATH '$.amount')) j) END)"; $builder->where(function (Builder $q) use ($refundTotalExpr) { $q->where(function (Builder $q2) use ($refundTotalExpr) { $q2->where('payment_status', 'refunded') ->whereRaw("paid_amount > 0") ->whereRaw("(ROUND($refundTotalExpr * 100) + 1) < ROUND(paid_amount * 100)"); })->orWhere(function (Builder $q2) use ($refundTotalExpr) { $q2->where('payment_status', '!=', 'refunded') ->whereRaw("paid_amount > 0") ->whereRaw("ROUND($refundTotalExpr * 100) >= ROUND(paid_amount * 100)"); }); }); } }); } private function receiptTotalForOrder(PlatformOrder $order): float { // 口径统一:集中到模型方法,避免多处复制导致漂移 return (float) $order->receiptTotal(); } private function refundTotalForOrder(PlatformOrder $order): float { // 口径统一:集中到模型方法,避免多处复制导致漂移 return (float) $order->refundTotal(); } protected function sumReceiptAmount($orders): float { $total = 0.0; foreach ($orders as $o) { $t = (float) (data_get($o->meta, 'payment_summary.total_amount') ?? 0); if ($t > 0) { $total += $t; continue; } $receipts = (array) (data_get($o->meta, 'payment_receipts', []) ?? []); foreach ($receipts as $r) { $total += (float) (data_get($r, 'amount') ?? 0); } } return $total; } protected function statusLabels(): array { return [ 'pending' => '待处理', 'paid' => '已支付', 'activated' => '已生效', 'cancelled' => '已取消', 'refunded' => '已退款', ]; } protected function paymentStatusLabels(): array { return [ 'unpaid' => '未支付', 'paid' => '已支付', 'partially_refunded' => '部分退款', 'refunded' => '已退款', 'failed' => '支付失败', ]; } protected function orderTypeLabels(): array { return [ 'new_purchase' => '新购', 'renewal' => '续费', 'upgrade' => '升级', 'downgrade' => '降级', ]; } protected function billingCycleLabels(): array { return [ 'monthly' => '月付', 'quarterly' => '季付', 'yearly' => '年付', 'one_time' => '一次性', ]; } protected function periodMonthsFromBillingCycle(string $billingCycle): int { return match ($billingCycle) { 'monthly' => 1, 'quarterly' => 3, 'yearly' => 12, 'one_time' => 1, default => 1, }; } }