chore: init saasshop repo + sql migrations runner + gitee go

This commit is contained in:
萝卜
2026-03-10 11:31:02 +00:00
commit 50f15cdea8
210 changed files with 29534 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>总台管理登录 - SaaSShop</title>
<link rel="stylesheet" href="/css/admin-base.css">
<link rel="stylesheet" href="/css/admin.css">
</head>
<body class="login-page">
<div class="card-login">
<h1>总台管理登录</h1>
<p class="muted">当前入口面向平台运营方。演示账号platform.admin@demo.local / Platform@123456</p>
@if ($errors->any())
<div class="error">{{ $errors->first() }}</div>
@endif
<form method="post" action="/admin/login">
@csrf
<input type="email" name="email" placeholder="邮箱" value="{{ old('email', 'platform.admin@demo.local') }}">
<input type="password" name="password" placeholder="密码" value="Platform@123456">
<button type="submit">登录总台管理</button>
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,32 @@
@extends('admin.layouts.app')
@section('title', '总台管理仪表盘')
@section('page_title', '总台管理仪表盘')
@section('content')
<div class="two-col mb-20">
<div class="card">
<p>欢迎回来,{{ $adminName }}。当前入口已明确为 <strong>总台管理</strong>,用于平台运营方统一查看站点、渠道、全局配置和平台级业务数据。</p>
<p class="muted">当前平台上下文已通过封装统一解析,不再依赖控制器里零散读取 session。</p>
<p class="muted mb-0">仪表盘统计已接入缓存:{{ $cacheMeta['store'] }} / TTL {{ $cacheMeta['ttl'] }}</p>
</div>
<div class="card">
<h3 class="mt-0">平台定位</h3>
<table>
<tr><th>后台角色</th><td>{{ $platformOverview['system_role'] }}</td></tr>
<tr><th>当前视角</th><td>{{ $platformOverview['current_scope'] }}</td></tr>
<tr><th>商家模式</th><td>{{ $platformOverview['merchant_mode'] }}</td></tr>
<tr><th>渠道数</th><td>{{ $platformOverview['channel_count'] }}</td></tr>
<tr><th>活跃商家</th><td>{{ $platformOverview['active_merchants'] }}</td></tr>
<tr><th>待处理订单</th><td>{{ $platformOverview['pending_orders'] }}</td></tr>
</table>
</div>
</div>
<div class="grid">
<div class="card"><div class="muted">站点</div><div class="num">{{ $stats['merchants'] }}</div></div>
<div class="card"><div class="muted">管理员</div><div class="num">{{ $stats['admins'] }}</div></div>
<div class="card"><div class="muted">用户</div><div class="num">{{ $stats['users'] }}</div></div>
<div class="card"><div class="muted">商品</div><div class="num">{{ $stats['products'] }}</div></div>
<div class="card"><div class="muted">订单</div><div class="num">{{ $stats['orders'] }}</div></div>
</div>
@endsection

View File

@@ -0,0 +1,68 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', 'SaaSShop 总台管理')</title>
<link rel="stylesheet" href="/css/admin-base.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="stylesheet" href="/css/admin-components.css">
</head>
<body>
<div class="layout">
<aside class="sidebar">
<h2 class="sidebar-title">SaaSShop</h2>
<div class="muted">总台管理 / Platform Ops</div>
<div class="group-title">总览</div>
<a href="/admin">总台仪表盘</a>
<div class="group-title">平台运营</div>
<a href="/admin/merchants">站点管理</a>
<a href="/admin/plans">套餐管理</a>
<a href="/admin/orders">订单监控</a>
<a href="/admin/platform-orders">平台订单</a>
<a href="/admin/site-subscriptions">订阅管理</a>
<a href="/admin/products">商品巡检</a>
<a href="/admin/product-categories">商品分类</a>
<div class="group-title">平台配置</div>
<a href="/admin/settings/system">系统配置</a>
<a href="/admin/settings/channels">渠道配置</a>
<div class="group-title">外部入口</div>
<a href="/">返回前台</a>
<form method="post" action="/admin/logout" class="logout-form">@csrf <button type="submit">退出登录</button></form>
</aside>
<main class="content">
<div class="top">
<div>
<h1 class="page-title">@yield('page_title', '总台管理')</h1>
<div class="muted">当前登录:{{ session('admin_name') }}{{ session('admin_email') }} / 角色:{{ session('admin_role', 'unknown') }}</div>
<div class="badge">登录范围:{{ session('admin_scope', 'platform') === 'platform' ? '总台管理员' : '商家管理员' }}</div>
</div>
</div>
@if(session('success'))
<div class="flash">{{ session('success') }}</div>
@endif
@if(session('warning'))
<div class="warning">{{ session('warning') }}</div>
@endif
@if(session('error'))
<div class="error-box">{{ session('error') }}</div>
@endif
@if($errors->any())
<div class="error-box">
<strong>提交失败:</strong>
<ul class="list-indent">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@yield('content')
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,50 @@
@extends('admin.layouts.app')
@section('title', '站点管理')
@section('page_title', '站点管理')
@section('content')
<div class="card mb-20">
<p class="muted muted-tight">这里是总台视角的站点管理入口,用于开通、查看和维护 SaaS 站点主体。</p>
<p class="muted">当前站点列表已接入缓存:{{ $cacheMeta['store'] }} / TTL {{ $cacheMeta['ttl'] }}</p>
<h3>新增站点</h3>
<form method="post" action="/admin/merchants">
@csrf
<div class="grid-3">
<input name="name" placeholder="站点名称" value="{{ old('name') }}">
<input name="slug" placeholder="站点标识 slug" value="{{ old('slug') }}">
<input name="plan" placeholder="套餐,如 pro/basic" value="{{ old('plan', 'basic') }}">
<input name="status" placeholder="状态" value="{{ old('status', 'active') }}">
<input name="contact_name" placeholder="联系人" value="{{ old('contact_name') }}">
<input name="contact_phone" placeholder="联系电话" value="{{ old('contact_phone') }}">
</div>
<div class="mt-12"><input name="contact_email" placeholder="联系邮箱" class="w-full" value="{{ old('contact_email') }}"></div>
<div class="mt-12"><button type="submit">创建站点</button></div>
</form>
</div>
<div class="card">
<h3>站点列表</h3>
<table>
<thead><tr><th>ID</th><th>站点名称</th><th>Slug</th><th>套餐</th><th>状态</th><th>联系人</th><th>操作</th></tr></thead>
<tbody>
@foreach($merchants as $merchant)
<tr>
<td>{{ $merchant->id }}</td>
<td>{{ $merchant->name }}</td>
<td>{{ $merchant->slug }}</td>
<td>{{ $merchant->plan }}</td>
<td>{{ $merchant->status }}</td>
<td>{{ $merchant->contact_name }} / {{ $merchant->contact_phone }}</td>
<td>
<a href="/site-admin/login" target="_blank" rel="noopener">进入站点后台</a>
<div class="muted muted-xs">当前阶段请使用该站点管理员账号登录</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="pagination-wrap">{{ $merchants->links() }}</div>
@endsection

View File

@@ -0,0 +1,171 @@
@extends('admin.layouts.app')
@section('title', '订单监控')
@section('page_title', '订单监控')
@section('content')
<div class="card mb-20">
<p class="muted muted-tight">当前页面用于总台视角查看全局订单状态,后续商家后台将承接商家侧订单处理动作。</p>
<p class="muted">当前订单列表已接入缓存:{{ $cacheMeta['store'] }} / TTL {{ $cacheMeta['ttl'] }}</p>
@php
$exportQuery = http_build_query(array_filter($filters, fn ($value) => $value !== null && $value !== ''));
@endphp
<div class="mb-12">
<a href="{{ '/admin/orders/export' . ($exportQuery ? ('?' . $exportQuery) : '') }}">导出当前筛选结果 CSV</a>
</div>
<h3>筛选条件</h3>
@if(!empty($filters['validation_errors']))
<div class="filter-error">
<strong>筛选条件有误:</strong>
<ul class="list-indent">
@foreach($filters['validation_errors'] as $validationError)
<li>{{ $validationError }}</li>
@endforeach
</ul>
</div>
@endif
<form method="get" action="/admin/orders">
<div class="grid-5">
<select name="status"><option value="">全部状态</option>@foreach($filterOptions['statuses'] as $status)<option value="{{ $statusLabels[$status] ?? $status }}" @selected($filters['status'] === $status)>{{ $statusLabels[$status] ?? $status }}</option>@endforeach</select>
<select name="payment_status"><option value="">全部支付状态</option>@foreach($filterOptions['paymentStatuses'] as $paymentStatus)<option value="{{ $paymentStatusLabels[$paymentStatus] ?? $paymentStatus }}" @selected($filters['payment_status'] === $paymentStatus)>{{ $paymentStatusLabels[$paymentStatus] ?? $paymentStatus }}</option>@endforeach</select>
<select name="platform"><option value="">全部平台</option>@foreach($filterOptions['platforms'] as $platform)<option value="{{ $platformLabels[$platform] ?? $platform }}" @selected($filters['platform'] === $platform)>{{ $platformLabels[$platform] ?? $platform }}</option>@endforeach</select>
<select name="device_type"><option value="">全部设备类型</option>@foreach($filterOptions['deviceTypes'] as $deviceType)<option value="{{ $deviceType }}" @selected($filters['device_type'] === $deviceType)>{{ $deviceTypeLabels[$deviceType] ?? $deviceType }}</option>@endforeach</select>
<select name="payment_channel"><option value="">全部支付渠道</option>@foreach($filterOptions['paymentChannels'] as $paymentChannel)<option value="{{ $paymentChannel }}" @selected($filters['payment_channel'] === $paymentChannel)>{{ $paymentChannelLabels[$paymentChannel] ?? $paymentChannel }}</option>@endforeach</select>
<input name="keyword" placeholder="订单号 / 买家 / 手机 / 邮箱" value="{{ $filters['keyword'] }}">
<select name="time_range">@foreach($filterOptions['timeRanges'] as $timeRangeValue => $timeRangeLabel)<option value="{{ $timeRangeValue }}" @selected(($filters['time_range'] ?? 'all') === $timeRangeValue)>{{ $timeRangeLabel }}</option>@endforeach</select>
<input type="date" name="start_date" value="{{ $filters['start_date'] }}">
<input type="date" name="end_date" value="{{ $filters['end_date'] }}">
<input type="number" step="0.01" min="0" name="min_pay_amount" placeholder="最低实付金额" value="{{ $filters['min_pay_amount'] }}">
<input type="number" step="0.01" min="0" name="max_pay_amount" placeholder="最高实付金额" value="{{ $filters['max_pay_amount'] }}">
<select name="sort">@foreach($filterOptions['sortOptions'] as $sortValue => $sortLabel)<option value="{{ $sortValue }}" @selected(($filters['sort'] ?? 'latest') === $sortValue)>{{ $sortLabel }}</option>@endforeach</select>
<div class="actions"><button type="submit">筛选</button><a href="/admin/orders">重置</a></div>
</div>
</form>
</div>
<div class="card section-dark mb-20">
<h3 class="mt-0">当前筛选摘要</h3>
<div class="grid-3">
@foreach($activeFilterSummary as $summaryLabel => $summaryValue)
<div class="summary-box"><div class="muted">{{ $summaryLabel }}</div><strong class="num-md">{{ $summaryValue }}</strong></div>
@endforeach
</div>
</div>
<div class="card mb-20">
<h3 class="mt-0">订单汇总</h3>
<div class="grid-5">
<div class="stat-box-light"><div class="muted">订单总数</div><strong class="num-md">{{ $summaryStats['total_orders'] ?? 0 }}</strong></div>
<div class="stat-box-light"><div class="muted">实付总额</div><strong class="num-md">¥{{ number_format($summaryStats['total_pay_amount'] ?? 0, 2) }}</strong></div>
<div class="stat-box-light"><div class="muted">平均客单价</div><strong class="num-md">¥{{ number_format($summaryStats['average_order_amount'] ?? 0, 2) }}</strong></div>
<div class="stat-box-light"><div class="muted">待支付金额</div><strong class="num-md">¥{{ number_format($summaryStats['unpaid_pay_amount'] ?? 0, 2) }}</strong></div>
<div class="stat-box-light"><div class="muted">已支付金额</div><strong class="num-md">¥{{ number_format($summaryStats['paid_pay_amount'] ?? 0, 2) }}</strong></div>
<div class="stat-box-light"><div class="muted">已支付订单数</div><strong class="num-md">{{ $summaryStats['paid_orders'] ?? 0 }}</strong></div>
<div class="stat-box-light"><div class="muted">支付率</div><strong class="num-md">{{ number_format($summaryStats['payment_rate'] ?? 0, 2) }}%</strong></div>
<div class="stat-box-light"><div class="muted">退款订单数</div><strong class="num-md">{{ $summaryStats['refunded_orders'] ?? 0 }}</strong></div>
<div class="stat-box-light"><div class="muted">退款率</div><strong class="num-md">{{ number_format($summaryStats['refund_rate'] ?? 0, 2) }}%</strong></div>
<div class="stat-box-light"><div class="muted">待发货订单数</div><strong class="num-md">{{ $summaryStats['pending_shipment_orders'] ?? 0 }}</strong></div>
<div class="stat-box-light"><div class="muted">完成率</div><strong class="num-md">{{ number_format($summaryStats['completion_rate'] ?? 0, 2) }}%</strong></div>
<div class="stat-box-light"><div class="muted">支付失败订单</div><strong class="num-md">{{ $summaryStats['failed_payment_orders'] ?? 0 }}</strong></div>
<div class="stat-box-light"><div class="muted">已取消订单</div><strong class="num-md">{{ $summaryStats['cancelled_orders'] ?? 0 }}</strong></div>
<div class="stat-box-light"><div class="muted">取消率</div><strong class="num-md">{{ number_format($summaryStats['cancellation_rate'] ?? 0, 2) }}%</strong></div>
</div>
</div>
<div class="card mb-20">
<h3 class="mt-0">时间趋势指标</h3>
<div class="grid-5">
<div class="stat-box-light"><div class="muted">今日订单数</div><strong class="num-md">{{ $trendStats['today_orders'] ?? 0 }}</strong></div>
<div class="stat-box-light"><div class="muted">今日实付金额</div><strong class="num-md">¥{{ number_format($trendStats['today_pay_amount'] ?? 0, 2) }}</strong></div>
<div class="stat-box-light"><div class="muted">近7天订单数</div><strong class="num-md">{{ $trendStats['last_7_days_orders'] ?? 0 }}</strong></div>
<div class="stat-box-light"><div class="muted">近7天实付金额</div><strong class="num-md">¥{{ number_format($trendStats['last_7_days_pay_amount'] ?? 0, 2) }}</strong></div>
</div>
</div>
<div class="card section-dark card-spaced">
<h3 class="mt-0">运营关注项</h3>
<div class="grid-3">
<div class="focus-box">
<div class="muted">订单盘面</div>
<strong class="text-md">{{ $operationsFocus['headline'] ?? '当前总台订单运营信息已就绪。' }}</strong>
<div class="muted mt-8">建议动作</div>
<div class="inline-links mt-6">
@foreach(($operationsFocus['actions'] ?? []) as $action)
<a href="{{ $action['url'] }}">{{ $action['label'] }}</a>
@endforeach
</div>
</div>
<div class="focus-box">
<div class="muted">当前信号</div>
<div class="grid-3 mt-8">
@foreach(($operationsFocus['signals'] ?? []) as $label => $value)
<div class="summary-box"><div class="muted">{{ $label }}</div><strong class="num-sm">{{ $value }}</strong></div>
@endforeach
</div>
</div>
<div class="focus-box">
<div class="muted">工作台导航</div>
<div class="inline-links mt-8">
@foreach(($operationsFocus['workbench'] ?? []) as $label => $url)
<a href="{{ $url }}">{{ $label }}</a>
@endforeach
</div>
</div>
</div>
</div>
<div class="card mb-20">
<h3 class="mt-0">状态统计</h3>
@php
$baseQuery = [
'payment_status' => $filters['payment_status'] ?: null,
'platform' => $filters['platform'] ?: null,
'device_type' => $filters['device_type'] ?: null,
'payment_channel' => $filters['payment_channel'] ?: null,
'keyword' => $filters['keyword'] ?: null,
'start_date' => $filters['start_date'] ?: null,
'end_date' => $filters['end_date'] ?: null,
'min_pay_amount' => $filters['min_pay_amount'] ?: null,
'max_pay_amount' => $filters['max_pay_amount'] ?: null,
'time_range' => ($filters['time_range'] ?? 'all') !== 'all' ? $filters['time_range'] : null,
'sort' => ($filters['sort'] ?? 'latest') !== 'latest' ? $filters['sort'] : null,
];
@endphp
<div class="grid-6">
<a href="{{ '/admin/orders?' . http_build_query(array_filter($baseQuery, fn ($value) => $value !== null && $value !== '')) }}" class="status-link-light {{ $filters['status'] === '' ? 'is-active-light' : '' }}"><div class="muted">全部</div><strong class="num-md">{{ $statusStats['all'] ?? 0 }}</strong></a>
@foreach($filterOptions['statuses'] as $status)
<a href="{{ '/admin/orders?' . http_build_query(array_filter(array_merge($baseQuery, ['status' => $status]), fn ($value) => $value !== null && $value !== '')) }}" class="status-link-light {{ $filters['status'] === $status ? 'is-active-light' : '' }}"><div class="muted">{{ $statusLabels[$status] ?? $status }}</div><strong class="num-md">{{ $statusStats[$status] ?? 0 }}</strong></a>
@endforeach
</div>
</div>
<div class="card">
<h3>订单列表</h3>
<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($orders as $order)
<tr>
<td>{{ $order->id }}</td>
<td>{{ $order->merchant?->name ?? ('商家#'.$order->merchant_id) }}</td>
<td><a href="/admin/orders/{{ $order->id }}">{{ $order->order_no }}</a></td>
<td>{{ $platformLabels[$order->platform] ?? $order->platform }}</td>
<td>{{ $order->buyer_name }}</td>
<td><div>{{ $paymentChannelLabels[$order->payment_channel] ?? $order->payment_channel }}</div><div class="muted">{{ $paymentStatusLabels[$order->payment_status] ?? $order->payment_status }}</div></td>
<td>¥{{ number_format($order->pay_amount, 2) }}</td>
<td>{{ $order->created_at?->format('Y-m-d H:i') }}</td>
<td>{{ $order->paid_at?->format('Y-m-d H:i') ?? '-' }}</td>
<td>{{ $order->shipped_at?->format('Y-m-d H:i') ?? '-' }}</td>
<td>{{ $order->completed_at?->format('Y-m-d H:i') ?? '-' }}</td>
<td>{{ $statusLabels[$order->status] ?? $order->status }}</td>
<td><form class="inline" method="post" action="/admin/orders/{{ $order->id }}/status">@csrf <select name="status"><option value="pending" @selected($order->status==='pending')>待处理</option><option value="paid" @selected($order->status==='paid')>已支付</option><option value="shipped" @selected($order->status==='shipped')>已发货</option><option value="completed" @selected($order->status==='completed')>已完成</option><option value="cancelled" @selected($order->status==='cancelled')>已取消</option></select><button type="submit">更新</button></form></td>
</tr>
@empty
<tr><td colspan="13" class="muted text-center">暂无订单</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="pagination-wrap">{{ $orders->links() }}</div>
@endsection

View File

@@ -0,0 +1,50 @@
@extends('admin.layouts.app')
@section('title', '平台订单详情')
@section('page_title', '平台订单详情')
@section('content')
<div class="card mb-20">
<h3>订单 {{ $order->order_no }}</h3>
<table>
<tr><th>ID</th><td>{{ $order->id }}</td></tr>
<tr><th>商家</th><td>{{ $order->merchant?->name ?? ('商家#'.$order->merchant_id) }}</td></tr>
<tr><th>平台</th><td>{{ $order->platform }}</td></tr>
<tr><th>订单状态</th><td>{{ $order->status }}</td></tr>
<tr><th>支付渠道</th><td>{{ $order->payment_channel }}</td></tr>
<tr><th>支付状态</th><td>{{ $order->payment_status }}</td></tr>
<tr><th>买家</th><td>{{ $order->buyer_name }}</td></tr>
<tr><th>手机</th><td>{{ $order->buyer_phone }}</td></tr>
<tr><th>邮箱</th><td>{{ $order->buyer_email }}</td></tr>
<tr><th>商品金额</th><td>¥{{ number_format($order->product_amount, 2) }}</td></tr>
<tr><th>优惠金额</th><td>¥{{ number_format($order->discount_amount, 2) }}</td></tr>
<tr><th>运费</th><td>¥{{ number_format($order->shipping_amount, 2) }}</td></tr>
<tr><th>应付金额</th><td>¥{{ number_format($order->pay_amount, 2) }}</td></tr>
<tr><th>备注</th><td>{{ $order->remark }}</td></tr>
<tr><th>创建时间</th><td>{{ $order->created_at }}</td></tr>
</table>
</div>
<div class="card">
<h3>订单明细</h3>
<table>
<thead><tr><th>ID</th><th>商品</th><th>SKU</th><th>单价</th><th>数量</th><th>小计</th><th>快照</th></tr></thead>
<tbody>
@forelse($order->items as $item)
<tr>
<td>{{ $item->id }}</td>
<td>{{ $item->product_title }}</td>
<td>{{ $item->product_sku }}</td>
<td>¥{{ number_format($item->product_price, 2) }}</td>
<td>{{ $item->quantity }}</td>
<td>¥{{ number_format($item->line_total_amount, 2) }}</td>
<td>{{ $item->snapshot['category'] ?? '-' }}</td>
</tr>
@empty
<tr><td colspan="7" class="muted">暂无订单明细</td></tr>
@endforelse
</tbody>
</table>
<p class="mt-16"><a href="/admin/orders">返回订单列表</a></p>
</div>
@endsection

View File

@@ -0,0 +1,74 @@
@extends('admin.layouts.app')
@section('title', $plan->exists ? '编辑套餐' : '新建套餐')
@section('page_title', $plan->exists ? '编辑套餐' : '新建套餐')
@section('content')
<div class="card mb-20">
<p class="muted muted-tight">套餐是平台授权与收费的基础单位,这里维护套餐的基本信息与售卖口径。</p>
<p class="muted">当前阶段先提交套餐主数据,后续再补授权项、配额与订阅联动。</p>
</div>
<form method="post" action="{{ $formAction }}" class="card form-grid">
@csrf
<label>
<span>套餐名称</span>
<input name="name" value="{{ old('name', $plan->name) }}" required>
</label>
<label>
<span>套餐编码</span>
<input name="code" value="{{ old('code', $plan->code) }}" required>
<small class="muted">仅限字母、数字、短横线和下划线,用于接口/内部引用。</small>
</label>
<label>
<span>计费周期</span>
<select name="billing_cycle" required>
@foreach($billingCycleLabels as $value => $label)
<option value="{{ $value }}" @selected(old('billing_cycle', $plan->billing_cycle) === $value)>{{ $label }}</option>
@endforeach
</select>
</label>
<label>
<span>售价</span>
<input type="number" step="0.01" min="0" name="price" value="{{ old('price', $plan->price) }}" required>
</label>
<label>
<span>划线价</span>
<input type="number" step="0.01" min="0" name="list_price" value="{{ old('list_price', $plan->list_price) }}">
</label>
<label>
<span>状态</span>
<select name="status" required>
@foreach($statusLabels as $value => $label)
<option value="{{ $value }}" @selected(old('status', $plan->status) === $value)>{{ $label }}</option>
@endforeach
</select>
</label>
<label>
<span>排序</span>
<input type="number" min="0" name="sort" value="{{ old('sort', $plan->sort ?? 0) }}">
</label>
<label class="full">
<span>发布时间</span>
<input type="datetime-local" name="published_at" value="{{ old('published_at', optional($plan->published_at)->format('Y-m-d\TH:i')) }}">
</label>
<label class="full">
<span>套餐说明</span>
<textarea name="description" rows="4" placeholder="可描述套餐包含的能力与适用场景">{{ old('description', $plan->description) }}</textarea>
</label>
<div class="form-actions">
<a href="/admin/plans" class="btn-secondary">返回</a>
<button type="submit">保存套餐</button>
</div>
</form>
@endsection

View File

@@ -0,0 +1,139 @@
@extends('admin.layouts.app')
@section('title', '套餐管理')
@section('page_title', '套餐管理')
@section('content')
<div class="card mb-20">
<p class="muted muted-tight">这里是总台视角的套餐目录页,用于沉淀平台可售卖的标准能力包。</p>
<p class="muted">当前阶段先完成套餐主数据可见、可筛与口径收拢,后续再接授权项、售价规则与上下架动作。</p>
</div>
<div class="card mb-20">
<h3>工具</h3>
<form method="get" action="/admin/plans/export">
<input type="hidden" name="status" value="{{ $filters['status'] ?? '' }}">
<input type="hidden" name="published" value="{{ $filters['published'] ?? '' }}">
<input type="hidden" name="billing_cycle" value="{{ $filters['billing_cycle'] ?? '' }}">
<input type="hidden" name="keyword" value="{{ $filters['keyword'] ?? '' }}">
<button type="submit">导出当前筛选结果CSV</button>
</form>
</div>
<div class="card mb-20">
<h3>筛选条件</h3>
<form method="get" action="/admin/plans" class="grid-3">
<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="published">
<option value="">全部发布状态</option>
<option value="published" @selected(($filters['published'] ?? '') === 'published')>已发布</option>
<option value="unpublished" @selected(($filters['published'] ?? '') === 'unpublished')>未发布</option>
</select>
<select name="billing_cycle">
<option value="">全部计费周期</option>
@foreach(($filterOptions['billingCycles'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected(($filters['billing_cycle'] ?? '') === $value)>{{ $label }}</option>
@endforeach
</select>
<input name="keyword" placeholder="搜索套餐名称 / 编码 / 描述" value="{{ $filters['keyword'] ?? '' }}">
<div>
<button type="submit">应用筛选</button>
</div>
</form>
</div>
<div class="grid-4 mb-20">
<div class="card">
<h3>套餐总数</h3>
<div class="num-md">{{ $summaryStats['total_plans'] ?? 0 }}</div>
</div>
<div class="card">
<h3>启用中套餐</h3>
<div class="num-md">{{ $summaryStats['active_plans'] ?? 0 }}</div>
</div>
<div class="card">
<h3>月付套餐</h3>
<div class="num-md">{{ $summaryStats['monthly_plans'] ?? 0 }}</div>
</div>
<div class="card">
<h3>年付套餐</h3>
<div class="num-md">{{ $summaryStats['yearly_plans'] ?? 0 }}</div>
</div>
<div class="card">
<h3>已发布</h3>
<div class="num-md">{{ $summaryStats['published_plans'] ?? 0 }}</div>
</div>
<div class="card">
<h3>未发布</h3>
<div class="num-md">{{ $summaryStats['unpublished_plans'] ?? 0 }}</div>
</div>
</div>
<div class="card">
<div class="flex-between">
<div>
<h3>套餐列表</h3>
<p class="muted muted-xs">后续将从这里进入套餐详情、授权项与订阅联动。</p>
</div>
<a href="/admin/plans/create" class="btn">新建套餐</a>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>套餐名称</th>
<th>编码</th>
<th>计费周期</th>
<th>售价</th>
<th>划线价</th>
<th>状态</th>
<th>排序</th>
<th>发布时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@forelse($plans as $plan)
<tr>
<td>{{ $plan->id }}</td>
<td>
<strong>{{ $plan->name }}</strong>
<div class="muted muted-xs">{{ $plan->description ?: '暂无说明' }}</div>
</td>
<td>{{ $plan->code }}</td>
<td>{{ $billingCycleLabels[$plan->billing_cycle] ?? $plan->billing_cycle }}</td>
<td>¥{{ number_format((float) $plan->price, 2) }}</td>
<td>¥{{ number_format((float) $plan->list_price, 2) }}</td>
<td>{{ $statusLabels[$plan->status] ?? $plan->status }}</td>
<td>{{ $plan->sort }}</td>
<td>{{ optional($plan->published_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
<td>
<a href="/admin/plans/{{ $plan->id }}/edit" class="link">编辑</a>
<form method="post" action="/admin/plans/{{ $plan->id }}/set-status" style="margin-top:6px;">
@csrf
<select name="status" onchange="this.form.submit()" style="width:140px;">
@foreach(($filterOptions['statuses'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected($plan->status === $value)>{{ $label }}</option>
@endforeach
</select>
<noscript><button type="submit">更新状态</button></noscript>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="10" class="muted">暂无套餐数据,当前阶段先把套餐主表与总台目录立起来,后续可继续接套餐创建、授权项与订阅关联。</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="pagination-wrap">{{ $plans->links() }}</div>
@endsection

View File

@@ -0,0 +1,298 @@
@extends('admin.layouts.app')
@section('title', '平台订单')
@section('page_title', '平台订单')
@section('content')
<div class="card mb-20">
<p class="muted muted-tight">这里是总台视角的平台收费主链骨架页,当前阶段先承接套餐订购 / 续费 / 生效跟踪。</p>
<p class="muted">本页先提供可访问列表、基础筛选与摘要卡,后续再补详情、导出、支付记录与退款轨迹。</p>
</div>
<div class="card mb-20">
<h3>筛选条件</h3>
<form method="get" action="/admin/platform-orders" class="grid-3">
<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="payment_status">
<option value="">全部支付状态</option>
@foreach(($filterOptions['paymentStatuses'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected(($filters['payment_status'] ?? '') === $value)>{{ $label }}</option>
@endforeach
</select>
<select name="sync_status">
<option value="">全部同步状态</option>
<option value="unsynced" @selected(($filters['sync_status'] ?? '') === 'unsynced')>未同步</option>
<option value="synced" @selected(($filters['sync_status'] ?? '') === 'synced')>已同步</option>
<option value="failed" @selected(($filters['sync_status'] ?? '') === 'failed')>同步失败</option>
</select>
<label class="form-inline-row">
<input type="checkbox" name="fail_only" value="1" @checked(($filters['fail_only'] ?? '') === '1')>
<span>只看同步失败</span>
</label>
<label class="form-inline-row">
<input type="checkbox" name="synced_only" value="1" @checked(($filters['synced_only'] ?? '') === '1')>
<span>只看已同步</span>
</label>
<label class="form-inline-row">
<input type="checkbox" name="syncable_only" value="1" @checked(($filters['syncable_only'] ?? '') === '1')>
<span>只看可同步</span>
</label>
<label class="form-inline-row">
<input type="checkbox" name="batch_synced_24h" value="1" @checked(($filters['batch_synced_24h'] ?? '') === '1')>
<span>最近24小时批量同步过</span>
</label>
<div>
<button type="submit">应用筛选</button>
</div>
</form>
</div>
<div class="grid-3 mb-20">
<div class="card">
<h3>平台订单总数</h3>
<div class="metric-number">{{ $summaryStats['total_orders'] ?? 0 }}</div>
</div>
<div class="card">
<h3>已支付 / 已生效</h3>
<div class="metric-number">{{ $summaryStats['paid_orders'] ?? 0 }} / {{ $summaryStats['activated_orders'] ?? 0 }}</div>
</div>
<div class="card">
<h3>已同步 / 未同步</h3>
<div class="metric-number">{{ $summaryStats['synced_orders'] ?? 0 }} / {{ $summaryStats['unsynced_orders'] ?? 0 }}</div>
</div>
<div class="card">
<h3>同步失败数</h3>
<div class="metric-number">{{ $summaryStats['failed_sync_orders'] ?? 0 }}</div>
</div>
<div class="card">
<h3>同步失败原因 TOP5</h3>
@php $failedReasonStats = $failedReasonStats ?? []; @endphp
@if(count($failedReasonStats) > 0)
<div class="muted mt-6">
@foreach($failedReasonStats as $item)
<div>{{ $item['reason'] }} <span class="muted">{{ $item['count'] }}</span></div>
@endforeach
</div>
@else
<div class="muted">暂无失败原因聚合数据</div>
@endif
</div>
</div>
<div class="card mb-20">
<h3>工具</h3>
<div class="muted mb-10">清除仅影响订单 meta 中的失败标记,不改变订单/订阅状态。</div>
<form method="get" action="/admin/platform-orders/export" class="mb-10">
<input type="hidden" name="status" value="{{ $filters['status'] ?? '' }}">
<input type="hidden" name="payment_status" value="{{ $filters['payment_status'] ?? '' }}">
<input type="hidden" name="fail_only" value="{{ $filters['fail_only'] ?? '' }}">
<input type="hidden" name="synced_only" value="{{ $filters['synced_only'] ?? '' }}">
<input type="hidden" name="sync_status" value="{{ $filters['sync_status'] ?? '' }}">
<input type="hidden" name="syncable_only" value="{{ $filters['syncable_only'] ?? '' }}">
<input type="hidden" name="batch_synced_24h" value="{{ $filters['batch_synced_24h'] ?? '' }}">
<label class="form-inline-row mb-8">
<input type="checkbox" name="include_meta" value="1">
<span>包含原始 meta(JSON)(用于排障)</span>
</label>
<button type="submit">导出当前筛选结果CSV</button>
</form>
<form method="post" action="/admin/platform-orders/batch-activate-subscriptions" onsubmit="return confirm('确认批量同步当前筛选范围内“可同步”的订单?(仅处理:已支付+已生效+未同步)');" class="mb-10">
@csrf
<input type="hidden" name="scope" value="filtered">
<input type="hidden" name="status" value="{{ $filters['status'] ?? '' }}">
<input type="hidden" name="payment_status" value="{{ $filters['payment_status'] ?? '' }}">
<input type="hidden" name="fail_only" value="{{ $filters['fail_only'] ?? '' }}">
<input type="hidden" name="synced_only" value="{{ $filters['synced_only'] ?? '' }}">
<input type="hidden" name="sync_status" value="{{ $filters['sync_status'] ?? '' }}">
<input type="hidden" name="syncable_only" value="{{ $filters['syncable_only'] ?? '' }}">
<input type="hidden" name="batch_synced_24h" value="{{ $filters['batch_synced_24h'] ?? '' }}">
<label class="muted form-inline-row mb-8">
<span>本次最多处理</span>
<input type="number" name="limit" value="50" min="1" max="500" class="w-90">
<span>条(安全阀)</span>
</label>
<div class="muted mb-8">提示:建议先勾选筛选条件「只看可同步」,再执行批量同步。</div>
<button type="submit">批量同步订阅(当前筛选范围)</button>
</form>
<form method="post" action="/admin/platform-orders/batch-activate-subscriptions" onsubmit="return confirm('确认对全部订单执行批量同步?该操作仍只处理“已支付+已生效+未同步”的订单,但范围可能很大。');" class="mb-10">
@csrf
<input type="hidden" name="scope" value="all">
<label class="muted form-inline-row mb-8">
<span>确认输入</span>
<input type="text" name="confirm" placeholder="YES" class="w-140">
<span>(必须输入 YES 才会执行)</span>
</label>
<label class="muted form-inline-row mb-8">
<span>本次最多处理</span>
<input type="number" name="limit" value="50" min="1" max="500" class="w-90">
<span>条(安全阀)</span>
</label>
<button type="submit">批量同步订阅(全部订单)</button>
</form>
<form method="post" action="/admin/platform-orders/clear-sync-errors" onsubmit="return confirm('确认清除当前筛选范围内命中的订单的“同步失败”标记?');" class="mb-10">
@csrf
<input type="hidden" name="scope" value="filtered">
<input type="hidden" name="status" value="{{ $filters['status'] ?? '' }}">
<input type="hidden" name="payment_status" value="{{ $filters['payment_status'] ?? '' }}">
<input type="hidden" name="fail_only" value="{{ $filters['fail_only'] ?? '' }}">
<input type="hidden" name="synced_only" value="{{ $filters['synced_only'] ?? '' }}">
<input type="hidden" name="sync_status" value="{{ $filters['sync_status'] ?? '' }}">
<input type="hidden" name="syncable_only" value="{{ $filters['syncable_only'] ?? '' }}">
<input type="hidden" name="batch_synced_24h" value="{{ $filters['batch_synced_24h'] ?? '' }}">
<button type="submit">清除当前筛选范围的失败标记</button>
</form>
<form method="post" action="/admin/platform-orders/clear-sync-errors" onsubmit="return confirm('确认清除全部订单的“同步失败”标记?');">
@csrf
<input type="hidden" name="scope" value="all">
<button type="submit">清除全部失败标记</button>
</form>
</div>
<div class="card">
<h3>平台订单列表</h3>
<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>
<th>订阅到期</th>
<th>同步时间</th>
<th>最近批量同步</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@forelse($orders as $order)
<tr>
<td>{{ $order->id }}</td>
<td><a href="/admin/platform-orders/{{ $order->id }}">{{ $order->order_no }}</a></td>
<td>{{ $order->merchant?->name ?? '未关联站点' }}</td>
<td>{{ $order->plan_name ?: ($order->plan?->name ?? '未设置') }}</td>
<td>{{ $order->order_type }}</td>
<td>
{{ $statusLabels[$order->status] ?? $order->status }}
<div class="muted">{{ $order->status }}</div>
</td>
<td>
{{ $paymentStatusLabels[$order->payment_status] ?? $order->payment_status }}
<div class="muted">{{ $order->payment_status }}</div>
</td>
<td>¥{{ number_format((float) $order->payable_amount, 2) }}</td>
<td>¥{{ number_format((float) $order->paid_amount, 2) }}</td>
<td>{{ optional($order->placed_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
<td>{{ optional($order->paid_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
<td>
@php
$syncedId = (int) data_get($order->meta, 'subscription_activation.subscription_id', 0);
$syncErr = (string) (data_get($order->meta, 'subscription_activation_error.message') ?? '');
@endphp
@if($syncedId > 0)
<span>已同步</span>
@elseif($syncErr !== '')
<span class="text-danger">同步失败</span>
@else
<span class="muted">未同步</span>
@endif
</td>
<td>{{ $order->siteSubscription?->subscription_no ?: '-' }}</td>
<td>{{ optional($order->siteSubscription?->ends_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
<td>{{ data_get($order->meta, 'subscription_activation.synced_at') ?: '-' }}</td>
<td>
@php
// 优先使用扁平字段 meta.batch_activation便于筛选/统计,也避免遍历 audit
$batchActivation = (array) (data_get($order->meta, 'batch_activation', []) ?? []);
$batchAt = (string) (data_get($batchActivation, 'at') ?? '');
$batchAdminId = data_get($batchActivation, 'admin_id');
// 兼容旧数据:若尚未写入 batch_activation则回退从 meta.audit[] 中取最近一次批量同步记录
$audit = (array) (data_get($order->meta, 'audit', []) ?? []);
$lastBatch = null;
if ($batchAt === '' && count($audit) > 0) {
foreach (array_reverse($audit) as $item) {
if ((string) data_get($item, 'action') === 'batch_activate_subscription') {
$lastBatch = $item;
break;
}
}
}
if ($batchAt === '' && $lastBatch) {
$batchAt = (string) (data_get($lastBatch, 'at') ?? '');
$batchAdminId = data_get($lastBatch, 'admin_id');
}
@endphp
@if($batchAt !== '')
<div>{{ $batchAt }}</div>
<div class="muted">管理员:{{ $batchAdminId ?: '-' }}</div>
@else
<span class="muted">-</span>
@endif
</td>
<td>
@php
$canActivate = ($order->payment_status === 'paid') && ($order->status === 'activated');
$alreadySynced = (int) data_get($order->meta, 'subscription_activation.subscription_id', 0) > 0;
$syncError = data_get($order->meta, 'subscription_activation_error.message');
$syncErrorAt = data_get($order->meta, 'subscription_activation_error.at');
@endphp
@php
$canMarkPaid = ($order->payment_status !== 'paid') || ($order->status !== 'activated');
@endphp
<form method="post" action="/admin/platform-orders/{{ $order->id }}/mark-paid-and-activate" onsubmit="return confirm('确认将该订单标记为已支付并生效?此操作会推进状态并尝试同步订阅');" class="mb-6">
@csrf
<button type="submit" @disabled(! $canMarkPaid)>标记支付并生效</button>
</form>
<form method="post" action="/admin/platform-orders/{{ $order->id }}/activate-subscription" onsubmit="return confirm('确认同步订阅?将根据该订单创建/续期订阅');">
@csrf
<button type="submit" @disabled(! $canActivate || $alreadySynced)>{{ $alreadySynced ? '已同步' : '同步订阅' }}</button>
</form>
@if(! $canActivate)
<div class="muted mt-6">需已支付且订单状态为已生效</div>
@elseif($alreadySynced)
<div class="muted mt-6">该订单已完成同步(幂等保护)</div>
@endif
@if($syncError)
<div class="muted text-danger mt-6">同步失败:{{ $syncError }}{{ $syncErrorAt ? '' . $syncErrorAt . '' : '' }}</div>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="17" class="muted">暂无平台订单,当前阶段骨架已就位,可继续补套餐下单、支付回执与订阅生效链路。</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="pagination-wrap">{{ $orders->links() }}</div>
@endsection

View File

@@ -0,0 +1,158 @@
@extends('admin.layouts.app')
@section('title', '平台订单详情')
@section('page_title', '平台订单详情')
@section('content')
<div class="card mb-20">
<p class="muted muted-tight">这里用于运营排查订单核心字段、关联订阅、以及订阅同步元数据meta</p>
</div>
<div class="card mb-20">
<h3>订单信息</h3>
<table>
<tbody>
<tr><th style="width:160px;">ID</th><td>{{ $order->id }}</td></tr>
<tr><th>订单号</th><td>{{ $order->order_no }}</td></tr>
<tr><th>站点</th><td>{{ $order->merchant?->name ?? '未关联站点' }}</td></tr>
<tr><th>套餐</th><td>{{ $order->plan_name ?: ($order->plan?->name ?? '-') }}</td></tr>
<tr><th>订单类型</th><td>{{ $order->order_type }}</td></tr>
<tr><th>订单状态</th><td>{{ $statusLabels[$order->status] ?? $order->status }} <span class="muted">({{ $order->status }})</span></td></tr>
<tr><th>支付状态</th><td>{{ $paymentStatusLabels[$order->payment_status] ?? $order->payment_status }} <span class="muted">({{ $order->payment_status }})</span></td></tr>
<tr><th>应付/已付</th><td>¥{{ number_format((float) $order->payable_amount, 2) }} / ¥{{ number_format((float) $order->paid_amount, 2) }}</td></tr>
<tr><th>下单时间</th><td>{{ optional($order->placed_at)->format('Y-m-d H:i:s') ?: '-' }}</td></tr>
<tr><th>支付时间</th><td>{{ optional($order->paid_at)->format('Y-m-d H:i:s') ?: '-' }}</td></tr>
<tr><th>生效时间</th><td>{{ optional($order->activated_at)->format('Y-m-d H:i:s') ?: '-' }}</td></tr>
</tbody>
</table>
@php
$canActivate = ($order->payment_status === 'paid') && ($order->status === 'activated');
$alreadySynced = (int) data_get($order->meta, 'subscription_activation.subscription_id', 0) > 0;
$canMarkPaid = ($order->payment_status !== 'paid') || ($order->status !== 'activated');
$syncError = data_get($order->meta, 'subscription_activation_error.message');
$syncErrorAt = data_get($order->meta, 'subscription_activation_error.at');
@endphp
<div style="margin-top:16px; display:flex; gap:12px; flex-wrap:wrap;">
<form method="post" action="/admin/platform-orders/{{ $order->id }}/mark-paid-and-activate" onsubmit="return confirm('确认将该订单标记为已支付并生效?此操作会推进状态并尝试同步订阅');">
@csrf
<button type="submit" @disabled(! $canMarkPaid)>标记支付并生效</button>
</form>
<form method="post" action="/admin/platform-orders/{{ $order->id }}/activate-subscription" onsubmit="return confirm('确认同步订阅?将根据该订单创建/续期订阅');">
@csrf
<button type="submit" @disabled(! $canActivate || $alreadySynced)>{{ $alreadySynced ? '已同步' : '同步订阅' }}</button>
</form>
</div>
@if(! $canActivate)
<div class="muted" style="margin-top:10px;">同步订阅需满足:已支付 + 订单状态已生效</div>
@elseif($alreadySynced)
<div class="muted" style="margin-top:10px;">该订单已完成同步(幂等保护)</div>
@endif
@if($syncError)
<div class="muted" style="margin-top:10px; color:#b42318;">最近同步失败:{{ $syncError }}{{ $syncErrorAt ? '' . $syncErrorAt . '' : '' }}</div>
@endif
</div>
<div class="card mb-20">
<h3>关联订阅</h3>
@if($order->siteSubscription)
<table>
<tbody>
<tr><th style="width:160px;">订阅ID</th><td>{{ $order->siteSubscription->id }}</td></tr>
<tr><th>订阅号</th><td>{{ $order->siteSubscription->subscription_no }}</td></tr>
<tr><th>订阅管理</th><td><a href="/admin/site-subscriptions?keyword={{ urlencode($order->siteSubscription->subscription_no) }}">打开订阅管理(按订阅号筛选)</a></td></tr>
<tr><th>状态</th><td>{{ $order->siteSubscription->status }}</td></tr>
<tr><th>开始/到期</th><td>{{ optional($order->siteSubscription->starts_at)->format('Y-m-d H:i:s') ?: '-' }} / {{ optional($order->siteSubscription->ends_at)->format('Y-m-d H:i:s') ?: '-' }}</td></tr>
</tbody>
</table>
@else
<p class="muted">该订单尚未关联订阅site_subscription_id 为空)。</p>
@endif
</div>
@php
$activation = data_get($order->meta, 'subscription_activation');
$activationError = data_get($order->meta, 'subscription_activation_error');
$audit = (array) (data_get($order->meta, 'audit', []) ?? []);
@endphp
<div class="card mb-20">
<h3>订阅同步记录</h3>
@if($activation)
<table>
<tbody>
<tr><th style="width:160px;">订阅ID</th><td>{{ data_get($activation, 'subscription_id') }}</td></tr>
<tr><th>同步时间</th><td>{{ data_get($activation, 'synced_at') ?: '-' }}</td></tr>
<tr><th>操作管理员</th><td>{{ data_get($activation, 'admin_id') ?: '-' }}</td></tr>
</tbody>
</table>
@else
<p class="muted">暂无同步记录。</p>
@endif
</div>
<div class="card mb-20">
<h3>最近一次同步失败</h3>
@if($activationError)
<table>
<tbody>
<tr><th style="width:160px;">失败原因</th><td>{{ data_get($activationError, 'message') }}</td></tr>
<tr><th>失败时间</th><td>{{ data_get($activationError, 'at') ?: '-' }}</td></tr>
<tr><th>操作管理员</th><td>{{ data_get($activationError, 'admin_id') ?: '-' }}</td></tr>
</tbody>
</table>
@else
<p class="muted">暂无失败记录。</p>
@endif
</div>
<div class="card mb-20">
<h3>审计记录(最近 20 条)</h3>
@if(count($audit) > 0)
@php
$auditItems = array_slice(array_reverse($audit), 0, 20);
$auditActionLabels = [
'clear_sync_error' => '清除同步失败标记',
'batch_activate_subscription' => '批量同步订阅',
];
@endphp
<table>
<thead>
<tr>
<th style="width:180px;">动作</th>
<th style="width:120px;">范围</th>
<th style="width:200px;">时间</th>
<th style="width:100px;">管理员</th>
<th>备注</th>
</tr>
</thead>
<tbody>
@foreach($auditItems as $item)
<tr>
<td>{{ $auditActionLabels[data_get($item, 'action')] ?? (data_get($item, 'action') ?: '-') }}</td>
<td>{{ data_get($item, 'scope') ?: '-' }}</td>
<td>{{ data_get($item, 'at') ?: '-' }}</td>
<td>{{ data_get($item, 'admin_id') ?: '-' }}</td>
<td class="muted">{{ data_get($item, 'note') ?: '' }}</td>
</tr>
@endforeach
</tbody>
</table>
@else
<p class="muted">暂无审计记录。</p>
@endif
</div>
<div class="card">
<h3>原始 metaJSON</h3>
<pre style="white-space: pre-wrap;">{{ json_encode($order->meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
</div>
<div class="mb-20" style="margin-top:16px;">
<a href="/admin/platform-orders"> 返回平台订单列表</a>
</div>
@endsection

View File

@@ -0,0 +1,70 @@
@extends('admin.layouts.app')
@section('title', '商品分类')
@section('page_title', '商品分类')
@section('content')
<div class="card mb-20">
<p class="muted muted-tight">这里是总台视角的商品分类巡检与维护入口,可跨站点查看分类结构。</p>
<p class="muted">当前分类列表已接入缓存:{{ $cacheMeta['store'] }} / TTL {{ $cacheMeta['ttl'] }}</p>
<h3>新增分类</h3>
<form method="post" action="/admin/product-categories">
@csrf
<div class="grid-3">
<select name="merchant_id">
@foreach($merchants as $merchant)
<option value="{{ $merchant->id }}" @selected((string) old('merchant_id', $merchants->first()?->id) === (string) $merchant->id)>{{ $merchant->id }} / {{ $merchant->name }}</option>
@endforeach
</select>
<input name="name" placeholder="分类名称" value="{{ old('name') }}">
<input name="slug" placeholder="slug" value="{{ old('slug') }}">
<select name="status">
<option value="active" @selected(old('status', 'active') === 'active')>active</option>
<option value="inactive" @selected(old('status') === 'inactive')>inactive</option>
</select>
<input name="sort" placeholder="排序" value="{{ old('sort', 0) }}">
<input name="description" placeholder="分类说明" class="span-2" value="{{ old('description') }}">
</div>
<div class="mt-12"><button type="submit">创建分类</button></div>
</form>
</div>
<div class="card">
<h3>分类列表</h3>
<table>
<thead><tr><th>ID</th><th>商家</th><th>名称</th><th>Slug</th><th>状态</th><th>排序</th><th>说明</th><th>操作</th></tr></thead>
<tbody>
@foreach($categories as $category)
<tr>
<td>{{ $category->id }}</td>
<td>{{ $category->merchant?->name ?? ('商家#'.$category->merchant_id) }}</td>
<td>
<form method="post" action="/admin/product-categories/{{ $category->id }}" class="actions">
@csrf
<input name="name" value="{{ $category->name }}">
</td>
<td><input name="slug" value="{{ $category->slug }}"></td>
<td>
<select name="status">
<option value="active" @selected($category->status==='active')>active</option>
<option value="inactive" @selected($category->status==='inactive')>inactive</option>
</select>
</td>
<td><input name="sort" value="{{ $category->sort }}" class="w-90"></td>
<td><input name="description" value="{{ $category->description }}"></td>
<td>
<button type="submit">更新</button>
</form>
<form method="post" action="/admin/product-categories/{{ $category->id }}/delete" class="inline" onsubmit="return confirm('确定删除这个分类?');">
@csrf
<button type="submit" class="button-danger">删除</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="pagination-wrap">{{ $categories->links() }}</div>
@endsection

View File

@@ -0,0 +1,109 @@
@extends('admin.layouts.app')
@section('title', '平台商品导入历史')
@section('page_title', '平台商品导入历史')
@section('content')
<div class="card">
<p class="muted muted-tight">这里集中查看平台视角的商品导入历史,适合运营侧复盘批量导入结果、失败明细与清理后文件状态。</p>
@php
$exportHistoryQuery = http_build_query(array_filter([
'merchant_id' => $importHistoryFilters['merchant_id'] ?? '',
'import_result_status' => $importHistoryFilters['result_status'] ?? 'all',
'import_time_range' => $importHistoryFilters['time_range'] ?? 'all',
'start_date' => $importHistoryFilters['start_date'] ?? '',
'end_date' => $importHistoryFilters['end_date'] ?? '',
'import_sort' => $importHistoryFilters['sort'] ?? 'latest',
], fn ($value, $key) => match ($key) {
'start_date', 'end_date' => $value !== null && $value !== '',
default => $value !== null && $value !== '' && $value !== 'all' && $value !== 'latest',
}, ARRAY_FILTER_USE_BOTH));
@endphp
<form method="get" action="/admin/products/import-histories" class="mb-12">
<div class="actions">
<select name="merchant_id">
<option value="">全部商家</option>
@foreach(($historyMerchants ?? collect()) as $merchant)
<option value="{{ $merchant->id }}" @selected(($importHistoryFilters['merchant_id'] ?? '') === (string) $merchant->id)>{{ $merchant->id }} / {{ $merchant->name }}</option>
@endforeach
</select>
<select name="import_result_status">
<option value="all" @selected(($importHistoryFilters['result_status'] ?? 'all') === 'all')>全部导入结果</option>
<option value="success_only" @selected(($importHistoryFilters['result_status'] ?? 'all') === 'success_only')>仅看成功导入</option>
<option value="has_failures" @selected(($importHistoryFilters['result_status'] ?? 'all') === 'has_failures')>仅看含失败导入</option>
</select>
<select name="import_time_range">
<option value="all" @selected(($importHistoryFilters['time_range'] ?? 'all') === 'all')>全部时间</option>
<option value="today" @selected(($importHistoryFilters['time_range'] ?? 'all') === 'today')>今天</option>
<option value="last_7_days" @selected(($importHistoryFilters['time_range'] ?? 'all') === 'last_7_days')>近7天</option>
<option value="custom" @selected(($importHistoryFilters['time_range'] ?? 'all') === 'custom')>自定义区间</option>
</select>
<input type="date" name="start_date" value="{{ $importHistoryFilters['start_date'] ?? '' }}">
<span class="muted"></span>
<input type="date" name="end_date" value="{{ $importHistoryFilters['end_date'] ?? '' }}">
<select name="import_sort">
@foreach(($importHistoryFilterOptions['sorts'] ?? []) as $sortValue => $sortLabel)
<option value="{{ $sortValue }}" @selected(($importHistoryFilters['sort'] ?? 'latest') === $sortValue)>{{ $sortLabel }}</option>
@endforeach
</select>
<button type="submit">筛选导入历史</button>
<a href="{{ '/admin/products/import-histories/export' . ($exportHistoryQuery ? ('?' . $exportHistoryQuery) : '') }}">导出当前筛选 CSV</a>
<a href="/admin/products/import-histories">清空筛选</a>
<a href="/admin/products">返回商品页</a>
</div>
</form>
@if(!empty($importHistoryFilters['date_errors']))
<div class="info-warning">
@foreach($importHistoryFilters['date_errors'] as $dateError)
<div>{{ $dateError }}</div>
@endforeach
</div>
@endif
<div class="grid-4 mb-12">
<div class="stat-box"><div class="muted">累计导入批次</div><strong class="num-sm">{{ $importHistoryStats['total_imports'] ?? 0 }}</strong></div>
<div class="stat-box"><div class="muted">累计成功商品</div><strong class="num-sm">{{ $importHistoryStats['total_success'] ?? 0 }}</strong></div>
<div class="stat-box"><div class="muted">累计失败商品</div><strong class="num-sm">{{ $importHistoryStats['total_failed'] ?? 0 }}</strong></div>
<div class="stat-box"><div class="muted">含失败批次</div><strong class="num-sm">{{ $importHistoryStats['warning_imports'] ?? 0 }}</strong></div>
</div>
<table>
<thead><tr><th>ID</th><th>导入时间</th><th>商家</th><th>上传文件</th><th>结果</th><th>成功</th><th>失败</th><th>操作者</th><th>失败明细</th></tr></thead>
<tbody>
@forelse($importHistories as $history)
<tr>
<td>{{ $history->id }}</td>
<td>{{ $history->imported_at?->format('Y-m-d H:i:s') }}</td>
<td>{{ $history->merchant?->name ?? '平台批量导入' }}</td>
<td>{{ $history->file_name }}</td>
<td>
@if(($history->failed_count ?? 0) > 0)
<span class="result-warning">部分失败</span>
@else
<span class="result-success">成功</span>
@endif
</td>
<td>{{ $history->success_count }}</td>
<td>{{ $history->failed_count }}</td>
<td>{{ $history->admin?->name ?? '-' }}</td>
<td>
@if($history->failure_file && ($history->failure_file_available ?? false))
<a href="/admin/products/import-failures/{{ $history->failure_file }}">下载 failure CSV</a>
@elseif($history->failure_file)
<span class="muted">文件已过保留期</span>
@else
<span class="muted"></span>
@endif
</td>
</tr>
@empty
<tr><td colspan="9" class="muted">暂无导入记录</td></tr>
@endforelse
</tbody>
</table>
<div class="pagination-wrap">{{ $importHistories->links() }}</div>
</div>
@endsection

View File

@@ -0,0 +1,225 @@
@extends('admin.layouts.app')
@section('title', '商品巡检')
@section('page_title', '商品巡检')
@section('content')
<div class="card mb-20">
<p class="muted muted-tight">当前页面作为总台视角的商品巡检与演示管理入口。后续商家商品运营将拆分到独立商家后台。</p>
<p class="muted">当前商品列表已接入缓存:{{ $cacheMeta['store'] }} / TTL {{ $cacheMeta['ttl'] }}</p>
@php
$exportQuery = http_build_query(array_filter($filters, fn ($value) => $value !== null && $value !== ''));
@endphp
<div class="card my-16">
<h3 class="mt-0">批量导入商品</h3>
<p class="muted">先下载模板,按模板填写后上传 CSV。平台模板首列需要填写 `merchant_id`,分类通过 `category_slug` 匹配。</p>
<div class="card-link-list mb-12">
<a href="/admin/products/import-template">下载导入模板</a>
<a href="{{ '/admin/products/export' . ($exportQuery ? ('?' . $exportQuery) : '') }}">导出当前筛选结果 CSV</a>
<a href="/admin/products/import-histories">查看独立导入历史页</a>
</div>
@if(session('import_result'))
@php $importResult = session('import_result'); @endphp
<div class="card section-dark mb-12">
<div><strong>导入结果:</strong>成功 {{ $importResult['success'] ?? 0 }} 条,失败 {{ $importResult['failed'] ?? 0 }} 条。</div>
@if(!empty($importResult['messages']))
<div class="muted mt-8">失败原因预览:</div>
<ul class="list-indent">
@foreach($importResult['messages'] as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
@endif
@if(!empty($importResult['failure_file']))
<div class="mt-10"><a href="/admin/products/import-failures/{{ $importResult['failure_file'] }}">下载失败明细 CSV</a></div>
@endif
</div>
@endif
<form method="post" action="/admin/products/import" enctype="multipart/form-data">
@csrf
<div class="actions"><input type="file" name="import_file" accept=".csv,text/csv"><button type="submit">开始导入</button></div>
</form>
<div class="mt-16">
<div class="actions-spread mb-10">
<div>
<h4 class="mb-6">导入历史摘要</h4>
<div class="muted">商品页仅保留摘要与最近 5 条记录;完整筛选、分页与复盘请前往独立导入历史页。</div>
</div>
<a href="/admin/products/import-histories">进入完整导入历史页</a>
</div>
<div class="grid-4 mb-12">
<div class="stat-box"><div class="muted">累计导入批次</div><strong class="num-sm">{{ $importHistoryStats['total_imports'] ?? 0 }}</strong></div>
<div class="stat-box"><div class="muted">累计成功商品</div><strong class="num-sm">{{ $importHistoryStats['total_success'] ?? 0 }}</strong></div>
<div class="stat-box"><div class="muted">累计失败商品</div><strong class="num-sm">{{ $importHistoryStats['total_failed'] ?? 0 }}</strong></div>
<div class="stat-box"><div class="muted">含失败批次</div><strong class="num-sm">{{ $importHistoryStats['warning_imports'] ?? 0 }}</strong></div>
</div>
<table>
<thead><tr><th>ID</th><th>导入时间</th><th>商家</th><th>上传文件</th><th>结果</th><th>失败明细</th></tr></thead>
<tbody>
@forelse($importHistories->take(5) as $history)
<tr>
<td>{{ $history->id }}</td>
<td>{{ $history->imported_at?->format('Y-m-d H:i:s') }}</td>
<td>{{ $history->merchant?->name ?? '平台批量导入' }}</td>
<td>{{ $history->file_name }}</td>
<td>@if(($history->failed_count ?? 0) > 0)<span class="result-warning">部分失败({{ $history->failed_count }}</span>@else<span class="result-success">成功</span>@endif</td>
<td>
@if($history->failure_file && ($history->failure_file_available ?? false))
<a href="/admin/products/import-failures/{{ $history->failure_file }}">下载</a>
@elseif($history->failure_file)
<span class="muted">已过保留期</span>
@else
<span class="muted"></span>
@endif
</td>
</tr>
@empty
<tr><td colspan="6" class="muted">暂无导入记录</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
<h3>筛选条件</h3>
<form method="get" action="/admin/products" class="mb-20">
<div class="grid-4">
<select name="merchant_id"><option value="">全部商家</option>@foreach($merchants as $merchant)<option value="{{ $merchant->id }}" @selected($filters['merchant_id'] === (string) $merchant->id)>{{ $merchant->id }} / {{ $merchant->name }}</option>@endforeach</select>
<select name="category_id"><option value="">全部分类</option>@foreach($categories as $category)<option value="{{ $category->id }}" @selected($filters['category_id'] === (string) $category->id)>商家{{ $category->merchant_id }} / {{ $category->name }}</option>@endforeach</select>
<select name="status"><option value="">全部状态</option>@foreach($filterOptions['statuses'] as $status)<option value="{{ $statusLabels[$status] ?? $status }}" @selected($filters['status'] === $status)>{{ $statusLabels[$status] ?? $status }}</option>@endforeach</select>
<input name="keyword" placeholder="标题 / slug / SKU" value="{{ $filters['keyword'] }}">
<input type="number" step="0.01" min="0" name="min_price" placeholder="最低价格" value="{{ $filters['min_price'] }}">
<input type="number" step="0.01" min="0" name="max_price" placeholder="最高价格" value="{{ $filters['max_price'] }}">
<input type="number" min="0" name="min_stock" placeholder="最低库存" value="{{ $filters['min_stock'] }}">
<input type="number" min="0" name="max_stock" placeholder="最高库存" value="{{ $filters['max_stock'] }}">
<select name="sort">@foreach($filterOptions['sortOptions'] as $sortValue => $sortLabel)<option value="{{ $sortValue }}" @selected(($filters['sort'] ?? 'latest') === $sortValue)>{{ $sortLabel }}</option>@endforeach</select>
</div>
<div class="mt-12 actions"><button type="submit">筛选</button><a href="/admin/products">重置</a></div>
</form>
<div class="card section-dark my-20"><h3 class="mt-0">当前筛选摘要</h3><div class="grid-4">@foreach($activeFilterSummary as $summaryLabel => $summaryValue)<div class="summary-box"><div class="muted">{{ $summaryLabel }}</div><strong class="num-md">{{ $summaryValue }}</strong></div>@endforeach</div></div>
<div class="card my-20"><h3 class="mt-0">商品运营汇总</h3><div class="grid-4"><div class="stat-box"><div class="muted">商品总数</div><strong class="num-md">{{ $summaryStats['total_products'] ?? 0 }}</strong></div><div class="stat-box"><div class="muted">总库存</div><strong class="num-md">{{ $summaryStats['total_stock'] ?? 0 }}</strong></div><div class="stat-box"><div class="muted">总货值</div><strong class="num-md">¥{{ number_format($summaryStats['total_stock_value'] ?? 0, 2) }}</strong></div><div class="stat-box"><div class="muted">平均售价</div><strong class="num-md">¥{{ number_format($summaryStats['average_price'] ?? 0, 2) }}</strong></div></div></div>
<div class="card section-dark card-spaced">
<h3 class="mt-0">运营关注项</h3>
<div class="grid-3">
<div class="focus-box">
<div class="muted">商品盘面</div>
<strong class="text-md">{{ $operationsFocus['headline'] ?? '当前总台商品运营信息已就绪。' }}</strong>
<div class="muted mt-8">建议动作</div>
<div class="inline-links mt-6">
@foreach(($operationsFocus['actions'] ?? []) as $action)
<a href="{{ $action['url'] }}">{{ $action['label'] }}</a>
@endforeach
</div>
</div>
<div class="focus-box">
<div class="muted">当前信号</div>
<div class="grid-3 mt-8">
@foreach(($operationsFocus['signals'] ?? []) as $label => $value)
<div class="summary-box"><div class="muted">{{ $label }}</div><strong class="num-sm">{{ $value }}</strong></div>
@endforeach
</div>
</div>
<div class="focus-box">
<div class="muted">工作台导航</div>
<div class="inline-links mt-8">
@foreach(($operationsFocus['workbench'] ?? []) as $label => $url)
<a href="{{ $url }}">{{ $label }}</a>
@endforeach
</div>
</div>
</div>
</div>
<div class="card section-dark my-20">
<h3 class="mt-0">商品状态统计</h3>
@php
$productBaseQuery = [
'merchant_id' => $filters['merchant_id'] ?: null,
'category_id' => $filters['category_id'] ?: null,
'keyword' => $filters['keyword'] ?: null,
'min_price' => $filters['min_price'] ?: null,
'max_price' => $filters['max_price'] ?: null,
'min_stock' => $filters['min_stock'] ?: null,
'max_stock' => $filters['max_stock'] ?: null,
'sort' => $filters['sort'] ?: 'latest',
];
@endphp
<div class="grid-4">
<a href="{{ '/admin/products?' . http_build_query(array_filter($productBaseQuery, fn ($value) => $value !== null && $value !== '')) }}" class="status-link {{ $filters['status'] === '' ? 'is-active-dark' : '' }}"><div class="muted">全部</div><strong class="num-md">{{ $statusStats['all'] ?? 0 }}</strong></a>
@foreach($filterOptions['statuses'] as $status)
<a href="{{ '/admin/products?' . http_build_query(array_filter(array_merge($productBaseQuery, ['status' => $status]), fn ($value) => $value !== null && $value !== '')) }}" class="status-link {{ $filters['status'] === $status ? 'is-active-dark' : '' }}"><div class="muted">{{ $statusLabels[$status] ?? $status }}</div><strong class="num-md">{{ $statusStats[$status] ?? 0 }}</strong></a>
@endforeach
</div>
</div>
<h3>新增商品</h3>
<form method="post" action="/admin/products">
@csrf
<div class="grid-3">
<select name="merchant_id">@foreach($merchants as $merchant)<option value="{{ $merchant->id }}" @selected((string) old('merchant_id', $merchants->first()?->id) === (string) $merchant->id)>{{ $merchant->id }} / {{ $merchant->name }}</option>@endforeach</select>
<select name="category_id"><option value="">选择分类(可留空)</option>@foreach($categories as $category)<option value="{{ $category->id }}" @selected((string) old('category_id') === (string) $category->id)>商家{{ $category->merchant_id }} / {{ $category->name }}</option>@endforeach</select>
<input name="title" placeholder="商品标题" value="{{ old('title') }}">
<input name="slug" placeholder="slug" value="{{ old('slug') }}">
<input name="sku" placeholder="SKU" value="{{ old('sku') }}">
<input name="price" placeholder="价格" value="{{ old('price') }}">
<input name="stock" placeholder="库存" value="{{ old('stock') }}">
</div>
<div class="mt-12"><input name="summary" placeholder="简介" class="w-full" value="{{ old('summary') }}"></div>
<div class="mt-12"><button type="submit">创建商品</button></div>
</form>
</div>
<div class="card">
<h3>商品列表</h3>
<form id="platform-batch-form" method="post" action="/admin/products/batch" onsubmit="return confirm('确认执行本次批量操作?');">
@csrf
<input type="hidden" name="return_url" value="{{ request()->fullUrl() }}">
<div class="actions mb-12">
<select name="action"><option value="change_status">批量改状态</option><option value="change_category">批量改分类</option></select>
<select name="status"><option value="">选择状态(用于批量改状态)</option>@foreach($filterOptions['statuses'] as $status)<option value="{{ $statusLabels[$status] ?? $status }}">{{ $statusLabels[$status] ?? $status }}</option>@endforeach</select>
<select name="category_id"><option value="">清空分类 / 不指定(用于批量改分类)</option>@foreach($categories as $category)<option value="{{ $category->id }}">商家{{ $category->merchant_id }} / {{ $category->name }}</option>@endforeach</select>
<button type="submit">执行批量操作</button>
</div>
</form>
<p class="muted muted-tight">平台侧批量改分类会校验所选分类是否属于被勾选商品对应商家;若混选了不同商家的商品,请选择各自可用的分类或先分批处理。</p>
<table>
<thead><tr><th><input type="checkbox" data-check-all="platform-products"></th><th>ID</th><th>商家</th><th>标题</th><th>分类</th><th>SKU</th><th>售价/原价</th><th>库存</th><th>创建时间</th><th>更新时间</th><th>状态</th><th>操作</th></tr></thead>
<tbody>
@foreach($products as $product)
<tr>
<td><input type="checkbox" name="product_ids[]" value="{{ $product->id }}" data-check-item="platform-products" form="platform-batch-form"></td>
<td>{{ $product->id }}</td>
<td>{{ $product->merchant?->name ?? ('商家#'.$product->merchant_id) }}</td>
<td><form method="post" action="/admin/products/{{ $product->id }}" class="actions">@csrf <input name="title" value="{{ $product->title }}"></td>
<td><select name="category_id"><option value="">未分类</option>@foreach($categories->where('merchant_id', $product->merchant_id) as $category)<option value="{{ $category->id }}" @selected($product->category_id === $category->id)>{{ $category->name }}</option>@endforeach</select></td>
<td><input name="slug" value="{{ $product->slug }}" placeholder="slug" class="w-140 block mb-6"><input name="sku" value="{{ $product->sku }}" placeholder="SKU" class="w-140"></td>
<td><input name="price" value="{{ $product->price }}" class="w-90 block mb-6"><div class="muted">原价:¥{{ number_format((float) $product->original_price, 2) }}</div></td>
<td><input name="stock" value="{{ $product->stock }}" class="w-90"></td>
<td>{{ $product->created_at?->format('Y-m-d H:i') }}</td>
<td>{{ $product->updated_at?->format('Y-m-d H:i') }}</td>
<td><select name="status"><option value="draft" @selected($product->status==='draft')>草稿</option><option value="published" @selected($product->status==='published')>已上架</option><option value="offline" @selected($product->status==='offline')>已下架</option></select></td>
<td><button type="submit">更新</button></form><form method="post" action="/admin/products/{{ $product->id }}/delete" class="inline" onsubmit="return confirm('确定删除这个商品?');">@csrf <button type="submit" class="button-danger">删除</button></form></td>
</tr>
@endforeach
</tbody>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const master = document.querySelector('[data-check-all="platform-products"]');
const items = document.querySelectorAll('[data-check-item="platform-products"]');
if (!master || !items.length) {
return;
}
master.addEventListener('change', function () {
items.forEach(function (item) {
item.checked = master.checked;
});
});
});
</script>
<div class="pagination-wrap">{{ $products->links() }}</div>
@endsection

View File

@@ -0,0 +1,98 @@
@extends('admin.layouts.app')
@section('title', '渠道配置')
@section('page_title', '渠道配置')
@section('content')
<div class="card mb-20">
<p class="mt-0">渠道配置已经切到数据库读取,当前用于承接多端渠道和平台支付能力的基线定义,并已支持基础编辑。</p>
<div class="muted">当前渠道与支付配置概览已接入缓存读取。</div>
<div class="muted">当前平台商家数:{{ $merchantCount }}</div>
</div>
<div class="card mb-20">
<h3 class="mt-0">渠道基线</h3>
<table>
<thead>
<tr>
<th>渠道编码</th>
<th>渠道名称</th>
<th>类型</th>
<th>状态</th>
<th>入口</th>
<th>登录</th>
<th>支付</th>
<th>分享</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach($channels as $channel)
<tr>
<form method="post" action="/admin/settings/channels/{{ $channel->id }}">
@csrf
<td>{{ $channel->channel_code }}</td>
<td><input name="channel_name" value="{{ old('channel_name', $channel->channel_name) }}"></td>
<td><input name="channel_type" value="{{ old('channel_type', $channel->channel_type) }}"></td>
<td>
<select name="status">
@foreach(['enabled', 'disabled', 'reserved'] as $status)
<option value="{{ $status }}" @selected(old('status', $channel->status) === $status)>{{ $status }}</option>
@endforeach
</select>
</td>
<td><input name="entry_path" value="{{ old('entry_path', $channel->entry_path) }}"></td>
<td><label><input type="checkbox" name="supports_login" value="1" @checked((bool) old('supports_login', $channel->supports_login))> yes</label></td>
<td><label><input type="checkbox" name="supports_payment" value="1" @checked((bool) old('supports_payment', $channel->supports_payment))> yes</label></td>
<td><label><input type="checkbox" name="supports_share" value="1" @checked((bool) old('supports_share', $channel->supports_share))> yes</label></td>
<td><textarea name="remark" rows="2" class="w-full">{{ old('remark', $channel->remark) }}</textarea></td>
<td><button type="submit">保存</button></td>
</form>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="card">
<h3 class="mt-0">支付配置基线</h3>
<table>
<thead>
<tr>
<th>支付编码</th>
<th>支付名称</th>
<th>提供方</th>
<th>状态</th>
<th>沙箱</th>
<th>退款</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach($paymentConfigs as $payment)
<tr>
<form method="post" action="/admin/settings/payments/{{ $payment->id }}">
@csrf
<td>{{ $payment->payment_code }}</td>
<td><input name="payment_name" value="{{ old('payment_name', $payment->payment_name) }}"></td>
<td><input name="provider" value="{{ old('provider', $payment->provider) }}"></td>
<td>
<select name="status">
@foreach(['enabled', 'disabled', 'reserved'] as $status)
<option value="{{ $status }}" @selected(old('status', $payment->status) === $status)>{{ $status }}</option>
@endforeach
</select>
</td>
<td><label><input type="checkbox" name="is_sandbox" value="1" @checked((bool) old('is_sandbox', $payment->is_sandbox))> yes</label></td>
<td><label><input type="checkbox" name="supports_refund" value="1" @checked((bool) old('supports_refund', $payment->supports_refund))> yes</label></td>
<td><textarea name="remark" rows="2" class="w-full">{{ old('remark', $payment->remark) }}</textarea></td>
<td><button type="submit">保存</button></td>
</form>
</tr>
@endforeach
</tbody>
</table>
</div>
@endsection

View File

@@ -0,0 +1,79 @@
@extends('admin.layouts.app')
@section('title', '系统配置')
@section('page_title', '系统配置')
@section('content')
<div class="card mb-20">
<p class="mt-0">系统配置已经从静态骨架切到数据库读取,当前已支持基础编辑、保存和缓存刷新。</p>
<div class="muted">当前系统配置列表已接入缓存读取。</div>
<div class="muted">当前配置分组数:{{ $groupedCount }}</div>
</div>
@foreach($systemSettings->groupBy('group') as $group => $items)
<div class="card mb-20">
<h3 class="mt-0">配置分组:{{ $group }}</h3>
<table>
<thead>
<tr>
<th>配置键</th>
<th>名称</th>
<th>配置值</th>
<th>类型</th>
<th>自动加载</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach($items as $item)
@php
$isEditing = $editingConfigId === $item->id;
$configName = $isEditing ? old('config_name', $item->config_name) : $item->config_name;
$configValue = $isEditing ? old('config_value', $item->config_value) : $item->config_value;
$valueType = $isEditing ? old('value_type', $item->value_type) : $item->value_type;
$autoload = $isEditing ? old('autoload', $item->autoload) : $item->autoload;
$remark = $isEditing ? old('remark', $item->remark) : $item->remark;
@endphp
<tr>
<form method="post" action="/admin/settings/system/{{ $item->id }}">
@csrf
<td>{{ $item->config_key }}<input type="hidden" name="group" value="{{ $item->group }}"></td>
<td><input name="config_name" value="{{ $configName }}"></td>
<td>
@if($valueType === 'json')
<textarea name="config_value" rows="4" class="w-full">{{ $configValue }}</textarea>
@if($isEditing && $errors->has('config_value'))
<div class="mt-6 muted" role="alert">{{ $errors->first('config_value') }}</div>
@endif
@elseif($valueType === 'boolean')
<select name="config_value">
<option value="1" @selected($configValue == '1')>true</option>
<option value="0" @selected($configValue == '0')>false</option>
</select>
@elseif($valueType === 'number')
<input type="number" step="any" name="config_value" value="{{ $configValue }}">
@else
<input name="config_value" value="{{ $configValue }}">
@endif
</td>
<td>
<select name="value_type">
@foreach($valueTypeOptions as $type)
<option value="{{ $type }}" @selected($valueType === $type)>{{ $type }}</option>
@endforeach
</select>
</td>
<td>
<label><input type="checkbox" name="autoload" value="1" @checked((bool) $autoload)> yes</label>
</td>
<td><input name="remark" value="{{ $remark }}"></td>
<td><button type="submit">保存</button></td>
</form>
</tr>
@endforeach
</tbody>
</table>
</div>
@endforeach
@endsection

View File

@@ -0,0 +1,144 @@
@extends('admin.layouts.app')
@section('title', '订阅管理')
@section('page_title', '订阅管理')
@section('content')
<div class="card mb-20">
<p class="muted muted-tight">这里是总台视角的订阅目录页,承接“套餐 -> 订阅 -> 平台订单”的收费主链中间层。</p>
<p class="muted">当前阶段先做到:可访问列表、可筛选、统计摘要;后续再接:订阅激活服务 / 续费 / 取消 / 对账。</p>
</div>
<div class="card mb-20">
<h3>筛选条件</h3>
<form method="get" action="/admin/site-subscriptions" class="grid-4">
<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>
<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 type="submit">应用筛选</button>
</div>
</form>
</div>
<div class="grid-4 mb-20">
<div class="card">
<h3>订阅总数</h3>
<div class="num-md">{{ $summaryStats['total_subscriptions'] ?? 0 }}</div>
</div>
<div class="card">
<h3>已生效</h3>
<div class="num-md">{{ $summaryStats['activated_subscriptions'] ?? 0 }}</div>
</div>
<div class="card">
<h3>待生效</h3>
<div class="num-md">{{ $summaryStats['pending_subscriptions'] ?? 0 }}</div>
</div>
<div class="card">
<h3>已取消</h3>
<div class="num-md">{{ $summaryStats['cancelled_subscriptions'] ?? 0 }}</div>
</div>
<div class="card">
<h3>已过期(按到期时间)</h3>
<div class="num-md">{{ $summaryStats['expired_subscriptions'] ?? 0 }}</div>
</div>
<div class="card">
<h3>7天内到期</h3>
<div class="num-md">{{ $summaryStats['expiring_7d_subscriptions'] ?? 0 }}</div>
</div>
</div>
<div class="card mb-20">
<h3>工具</h3>
<form method="get" action="/admin/site-subscriptions/export">
<input type="hidden" name="status" value="{{ $filters['status'] ?? '' }}">
<input type="hidden" name="merchant_id" value="{{ $filters['merchant_id'] ?? '' }}">
<input type="hidden" name="plan_id" value="{{ $filters['plan_id'] ?? '' }}">
<input type="hidden" name="expiry" value="{{ $filters['expiry'] ?? '' }}">
<input type="hidden" name="keyword" value="{{ $filters['keyword'] ?? '' }}">
<button type="submit">导出当前筛选结果CSV</button>
</form>
</div>
<div class="card">
<h3>订阅列表</h3>
<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>
</tr>
</thead>
<tbody>
@forelse($subscriptions as $subscription)
<tr>
<td>{{ $subscription->id }}</td>
<td>{{ $subscription->subscription_no }}</td>
<td>{{ $subscription->merchant?->name ?? '未关联站点' }}</td>
<td>{{ $subscription->plan_name ?: ($subscription->plan?->name ?? '未设置') }}</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>{{ optional($subscription->activated_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
</tr>
@empty
<tr>
<td colspan="12" class="muted">暂无订阅数据,当前阶段先把订阅主表与总台目录立起来,后续再接订阅创建/激活/续费链路。</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="pagination-wrap">{{ $subscriptions->links() }}</div>
@endsection

View File

@@ -0,0 +1 @@
@extends('admin.merchants.index')