chore: init saasshop repo + sql migrations runner + gitee go
This commit is contained in:
57
app/Http/Controllers/SiteAdmin/AuthController.php
Normal file
57
app/Http/Controllers/SiteAdmin/AuthController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\SiteAdmin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Admin;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function showLogin(): View
|
||||
{
|
||||
return view('site_admin.auth.login');
|
||||
}
|
||||
|
||||
public function login(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$admin = Admin::query()->with('merchant')->where('email', $data['email'])->first();
|
||||
if (! $admin || ! Hash::check($data['password'], $admin->password)) {
|
||||
return back()->withErrors(['email' => '账号或密码错误'])->withInput();
|
||||
}
|
||||
|
||||
if (! $admin->isMerchantAdmin()) {
|
||||
return back()->withErrors(['email' => '当前账号不是站点管理员,不能登录站点后台'])->withInput();
|
||||
}
|
||||
|
||||
$siteId = $admin->merchantId();
|
||||
|
||||
$request->session()->put('admin_id', $admin->id);
|
||||
$request->session()->put('admin_name', $admin->name);
|
||||
$request->session()->put('admin_email', $admin->email);
|
||||
$request->session()->put('admin_role', $admin->role);
|
||||
$request->session()->put('admin_merchant_id', $siteId);
|
||||
$request->session()->put('admin_site_id', $siteId);
|
||||
$request->session()->put('admin_scope', 'site');
|
||||
$request->session()->put('site_name', $admin->merchant?->name);
|
||||
|
||||
$admin->forceFill(['last_login_at' => now()])->save();
|
||||
|
||||
return redirect('/site-admin');
|
||||
}
|
||||
|
||||
public function logout(Request $request): RedirectResponse
|
||||
{
|
||||
$request->session()->forget(['admin_id', 'admin_name', 'admin_email', 'admin_role', 'admin_merchant_id', 'admin_site_id', 'admin_scope', 'site_name']);
|
||||
|
||||
return redirect('/site-admin/login');
|
||||
}
|
||||
}
|
||||
46
app/Http/Controllers/SiteAdmin/DashboardController.php
Normal file
46
app/Http/Controllers/SiteAdmin/DashboardController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\SiteAdmin;
|
||||
|
||||
use App\Http\Controllers\Concerns\ResolvesSiteContext;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Admin;
|
||||
use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use App\Support\CacheKeys;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
use ResolvesSiteContext;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$siteId = $this->siteId($request);
|
||||
$site = $this->site($request);
|
||||
|
||||
$stats = Cache::remember(
|
||||
CacheKeys::merchantDashboardStats($siteId),
|
||||
now()->addMinutes(10),
|
||||
fn () => [
|
||||
'admins' => Admin::query()->where('merchant_id', $siteId)->count(),
|
||||
'users' => User::query()->forMerchant($siteId)->count(),
|
||||
'products' => Product::query()->forMerchant($siteId)->count(),
|
||||
'orders' => Order::query()->forMerchant($siteId)->count(),
|
||||
'pending_orders' => Order::query()->forMerchant($siteId)->where('status', 'pending')->count(),
|
||||
]
|
||||
);
|
||||
|
||||
return view('site_admin.dashboard', [
|
||||
'site' => $site,
|
||||
'stats' => $stats,
|
||||
'cacheMeta' => [
|
||||
'store' => config('cache.default'),
|
||||
'ttl' => '10m',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
228
app/Http/Controllers/SiteAdmin/MerchantController.php
Normal file
228
app/Http/Controllers/SiteAdmin/MerchantController.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\SiteAdmin;
|
||||
|
||||
use App\Http\Controllers\Concerns\ResolvesSiteContext;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Merchant;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class MerchantController extends Controller
|
||||
{
|
||||
use ResolvesSiteContext;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$site = $this->site($request);
|
||||
$filters = $this->filters($request);
|
||||
|
||||
$query = $this->applySorting(
|
||||
$this->applyFilters(
|
||||
Merchant::query()
|
||||
->withCount(['admins', 'users', 'products', 'orders', 'categories'])
|
||||
->whereKey($site->id),
|
||||
$filters
|
||||
),
|
||||
$filters
|
||||
);
|
||||
|
||||
$merchants = $query->get();
|
||||
$summaryStats = $this->buildSummaryStats($site);
|
||||
|
||||
return view('site_admin.merchants.index', [
|
||||
'site' => $site,
|
||||
'merchants' => $merchants,
|
||||
'filters' => $filters,
|
||||
'activeFilterSummary' => $this->buildActiveFilterSummary($filters),
|
||||
'summaryStats' => $summaryStats,
|
||||
'statusLabels' => $this->statusLabels(),
|
||||
'planLabels' => $this->planLabels(),
|
||||
'filterOptions' => [
|
||||
'statuses' => array_keys($this->statusLabels()),
|
||||
'plans' => array_keys($this->planLabels()),
|
||||
'sortOptions' => [
|
||||
'latest' => '最近激活优先',
|
||||
'name_asc' => '名称 A-Z',
|
||||
'name_desc' => '名称 Z-A',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request): StreamedResponse
|
||||
{
|
||||
$site = $this->site($request);
|
||||
$filters = $this->filters($request);
|
||||
|
||||
$merchants = $this->applySorting(
|
||||
$this->applyFilters(
|
||||
Merchant::query()
|
||||
->withCount(['admins', 'users', 'products', 'orders', 'categories'])
|
||||
->whereKey($site->id),
|
||||
$filters
|
||||
),
|
||||
$filters
|
||||
)->get();
|
||||
|
||||
$summaryStats = $this->buildSummaryStats($site);
|
||||
$fileName = 'site_' . $site->id . '_merchants_' . now()->format('Ymd_His') . '.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($site, $filters, $merchants, $summaryStats) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
|
||||
fputcsv($handle, ['导出信息', '站点商家导出']);
|
||||
fputcsv($handle, ['站点ID', $site->id]);
|
||||
fputcsv($handle, ['站点名称', $site->name]);
|
||||
fputcsv($handle, ['关键词', ($filters['keyword'] ?? '') !== '' ? $filters['keyword'] : '全部']);
|
||||
fputcsv($handle, ['状态', $this->statusLabel($filters['status'] ?? '')]);
|
||||
fputcsv($handle, ['套餐', $this->planLabel($filters['plan'] ?? '')]);
|
||||
fputcsv($handle, ['排序', $this->sortLabel($filters['sort'] ?? 'latest')]);
|
||||
fputcsv($handle, ['承接站点数', $summaryStats['site_count'] ?? 0]);
|
||||
fputcsv($handle, ['启用中站点', $summaryStats['active_site_count'] ?? 0]);
|
||||
fputcsv($handle, ['站点管理员数', $summaryStats['admin_count'] ?? 0]);
|
||||
fputcsv($handle, ['站点用户数', $summaryStats['user_count'] ?? 0]);
|
||||
fputcsv($handle, ['站点商品数', $summaryStats['product_count'] ?? 0]);
|
||||
fputcsv($handle, ['站点订单数', $summaryStats['order_count'] ?? 0]);
|
||||
fputcsv($handle, ['商品分类数', $summaryStats['category_count'] ?? 0]);
|
||||
fputcsv($handle, []);
|
||||
|
||||
fputcsv($handle, ['当前站点资料', '']);
|
||||
fputcsv($handle, ['站点标识', $site->slug]);
|
||||
fputcsv($handle, ['当前状态', $this->statusLabel((string) $site->status)]);
|
||||
fputcsv($handle, ['当前套餐', $this->planLabel((string) $site->plan)]);
|
||||
fputcsv($handle, ['联系人', $site->contact_name ?: '未设置']);
|
||||
fputcsv($handle, ['联系电话', $site->contact_phone ?: '未设置']);
|
||||
fputcsv($handle, ['联系邮箱', $site->contact_email ?: '未设置']);
|
||||
fputcsv($handle, ['激活时间', $site->activated_at?->format('Y-m-d H:i:s') ?? '未激活']);
|
||||
fputcsv($handle, []);
|
||||
|
||||
fputcsv($handle, ['ID', '名称', 'Slug', '状态', '套餐', '联系人', '联系电话', '联系邮箱', '管理员数', '用户数', '商品数', '订单数', '商品分类数', '激活时间']);
|
||||
|
||||
foreach ($merchants as $merchant) {
|
||||
fputcsv($handle, [
|
||||
$merchant->id,
|
||||
$merchant->name,
|
||||
$merchant->slug,
|
||||
$this->statusLabel((string) $merchant->status),
|
||||
$this->planLabel((string) $merchant->plan),
|
||||
$merchant->contact_name ?: '未设置',
|
||||
$merchant->contact_phone ?: '未设置',
|
||||
$merchant->contact_email ?: '未设置',
|
||||
$merchant->admins_count ?? 0,
|
||||
$merchant->users_count ?? 0,
|
||||
$merchant->products_count ?? 0,
|
||||
$merchant->orders_count ?? 0,
|
||||
$merchant->categories_count ?? 0,
|
||||
$merchant->activated_at?->format('Y-m-d H:i:s') ?? '未激活',
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}, $fileName, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function filters(Request $request): array
|
||||
{
|
||||
return [
|
||||
'keyword' => trim((string) $request->string('keyword')),
|
||||
'status' => trim((string) $request->string('status')),
|
||||
'plan' => trim((string) $request->string('plan')),
|
||||
'sort' => trim((string) $request->string('sort', 'latest')),
|
||||
];
|
||||
}
|
||||
|
||||
protected function applyFilters(Builder $query, array $filters): Builder
|
||||
{
|
||||
return $query
|
||||
->when(($filters['status'] ?? '') !== '', fn ($builder) => $builder->where('status', $filters['status']))
|
||||
->when(($filters['plan'] ?? '') !== '', fn ($builder) => $builder->where('plan', $filters['plan']))
|
||||
->when(($filters['keyword'] ?? '') !== '', function ($builder) use ($filters) {
|
||||
$keyword = $filters['keyword'];
|
||||
|
||||
$builder->where(function ($subQuery) use ($keyword) {
|
||||
$subQuery->where('name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('slug', 'like', '%' . $keyword . '%')
|
||||
->orWhere('contact_name', 'like', '%' . $keyword . '%')
|
||||
->orWhere('contact_phone', 'like', '%' . $keyword . '%')
|
||||
->orWhere('contact_email', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected function applySorting(Builder $query, array $filters): Builder
|
||||
{
|
||||
return match ($filters['sort'] ?? 'latest') {
|
||||
'name_asc' => $query->orderBy('name')->orderBy('id'),
|
||||
'name_desc' => $query->orderByDesc('name')->orderByDesc('id'),
|
||||
default => $query->orderByDesc('activated_at')->orderByDesc('id'),
|
||||
};
|
||||
}
|
||||
|
||||
protected function buildSummaryStats(Merchant $site): array
|
||||
{
|
||||
$site->loadCount(['admins', 'users', 'products', 'orders', 'categories']);
|
||||
|
||||
return [
|
||||
'site_count' => 1,
|
||||
'active_site_count' => $site->status === 'active' ? 1 : 0,
|
||||
'admin_count' => (int) ($site->admins_count ?? 0),
|
||||
'user_count' => (int) ($site->users_count ?? 0),
|
||||
'product_count' => (int) ($site->products_count ?? 0),
|
||||
'order_count' => (int) ($site->orders_count ?? 0),
|
||||
'category_count' => (int) ($site->categories_count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildActiveFilterSummary(array $filters): array
|
||||
{
|
||||
return [
|
||||
'关键词' => ($filters['keyword'] ?? '') !== '' ? $filters['keyword'] : '全部',
|
||||
'状态' => $this->statusLabel($filters['status'] ?? ''),
|
||||
'套餐' => $this->planLabel($filters['plan'] ?? ''),
|
||||
'排序' => $this->sortLabel($filters['sort'] ?? 'latest'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function statusLabels(): array
|
||||
{
|
||||
return [
|
||||
'active' => '启用中',
|
||||
'inactive' => '未启用',
|
||||
'suspended' => '已停用',
|
||||
];
|
||||
}
|
||||
|
||||
protected function statusLabel(string $status): string
|
||||
{
|
||||
return $this->statusLabels()[$status] ?? '全部';
|
||||
}
|
||||
|
||||
protected function planLabels(): array
|
||||
{
|
||||
return [
|
||||
'basic' => '基础版',
|
||||
'pro' => '专业版',
|
||||
'enterprise' => '企业版',
|
||||
];
|
||||
}
|
||||
|
||||
protected function planLabel(string $plan): string
|
||||
{
|
||||
return $this->planLabels()[$plan] ?? (($plan === '') ? '全部' : $plan);
|
||||
}
|
||||
|
||||
protected function sortLabel(string $sort): string
|
||||
{
|
||||
return match ($sort) {
|
||||
'name_asc' => '名称 A-Z',
|
||||
'name_desc' => '名称 Z-A',
|
||||
default => '最近激活优先',
|
||||
};
|
||||
}
|
||||
}
|
||||
643
app/Http/Controllers/SiteAdmin/OrderController.php
Normal file
643
app/Http/Controllers/SiteAdmin/OrderController.php
Normal file
@@ -0,0 +1,643 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\SiteAdmin;
|
||||
|
||||
use App\Http\Controllers\Concerns\ResolvesSiteContext;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
use ResolvesSiteContext;
|
||||
|
||||
protected array $statuses = ['pending', 'paid', 'shipped', 'completed', 'cancelled'];
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$siteId = $this->siteId($request);
|
||||
$site = $this->site($request);
|
||||
$filters = $this->filters($request);
|
||||
$statusStatsFilters = $filters;
|
||||
$statusStatsFilters['status'] = '';
|
||||
|
||||
if ($filters['has_validation_error'] ?? false) {
|
||||
return view('site_admin.orders.index', [
|
||||
'site' => $site,
|
||||
'orders' => Order::query()->whereRaw('1 = 0')->paginate(10)->withQueryString(),
|
||||
'summaryStats' => $this->emptySummaryStats(),
|
||||
'statusStats' => $this->emptyStatusStats(),
|
||||
'activeFilterSummary' => $this->buildActiveFilterSummary($filters),
|
||||
'operationsFocus' => $this->buildOperationsFocus($siteId, $this->emptySummaryStats(), $filters),
|
||||
'workbenchLinks' => $this->workbenchLinks(),
|
||||
'filters' => $filters,
|
||||
'statusLabels' => $this->statusLabels(),
|
||||
'paymentStatusLabels' => $this->paymentStatusLabels(),
|
||||
'platformLabels' => $this->platformLabels(),
|
||||
'deviceTypeLabels' => $this->deviceTypeLabels(),
|
||||
'paymentChannelLabels' => $this->paymentChannelLabels(),
|
||||
'filterOptions' => [
|
||||
'statuses' => $this->statuses,
|
||||
'paymentStatuses' => ['unpaid', 'paid', 'refunded', 'failed'],
|
||||
'platforms' => ['pc', 'h5', 'wechat_mp', 'wechat_mini', 'app'],
|
||||
'paymentChannels' => ['wechat_pay', 'alipay'],
|
||||
'sortOptions' => [
|
||||
'latest' => '创建时间倒序',
|
||||
'oldest' => '创建时间正序',
|
||||
'pay_amount_desc' => '实付金额从高到低',
|
||||
'pay_amount_asc' => '实付金额从低到高',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$summaryStats = $this->buildSummaryStats(
|
||||
$this->applyFilters(Order::query()->forMerchant($siteId), $statusStatsFilters)
|
||||
);
|
||||
|
||||
return view('site_admin.orders.index', [
|
||||
'site' => $site,
|
||||
'orders' => $this->applySorting(
|
||||
$this->applyFilters(Order::query()->forMerchant($siteId), $filters),
|
||||
$filters
|
||||
)->paginate(10)->withQueryString(),
|
||||
'summaryStats' => $summaryStats,
|
||||
'statusStats' => $this->buildStatusStats(
|
||||
$this->applyFilters(Order::query()->forMerchant($siteId), $statusStatsFilters)
|
||||
),
|
||||
'activeFilterSummary' => $this->buildActiveFilterSummary($filters),
|
||||
'operationsFocus' => $this->buildOperationsFocus($siteId, $summaryStats, $filters),
|
||||
'workbenchLinks' => $this->workbenchLinks(),
|
||||
'filters' => $filters,
|
||||
'statusLabels' => $this->statusLabels(),
|
||||
'paymentStatusLabels' => $this->paymentStatusLabels(),
|
||||
'platformLabels' => $this->platformLabels(),
|
||||
'deviceTypeLabels' => $this->deviceTypeLabels(),
|
||||
'paymentChannelLabels' => $this->paymentChannelLabels(),
|
||||
'filterOptions' => [
|
||||
'statuses' => $this->statuses,
|
||||
'paymentStatuses' => ['unpaid', 'paid', 'refunded', 'failed'],
|
||||
'platforms' => ['pc', 'h5', 'wechat_mp', 'wechat_mini', 'app'],
|
||||
'paymentChannels' => ['wechat_pay', 'alipay'],
|
||||
'sortOptions' => [
|
||||
'latest' => '创建时间倒序',
|
||||
'oldest' => '创建时间正序',
|
||||
'pay_amount_desc' => '实付金额从高到低',
|
||||
'pay_amount_asc' => '实付金额从低到高',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request): StreamedResponse|RedirectResponse
|
||||
{
|
||||
$siteId = $this->siteId($request);
|
||||
$filters = $this->filters($request);
|
||||
|
||||
if ($filters['has_validation_error'] ?? false) {
|
||||
return redirect('/site-admin/orders?' . http_build_query($this->exportableFilters($filters)))
|
||||
->withErrors($filters['validation_errors'] ?? ['订单筛选条件不合法,请先修正后再导出。']);
|
||||
}
|
||||
|
||||
$fileName = 'site_' . $siteId . '_orders_' . now()->format('Ymd_His') . '.csv';
|
||||
$exportSummary = $this->buildSummaryStats(
|
||||
$this->applyFilters(Order::query()->forMerchant($siteId), $filters)
|
||||
);
|
||||
|
||||
return response()->streamDownload(function () use ($siteId, $filters, $exportSummary) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
|
||||
fputcsv($handle, ['导出信息', '站点订单导出']);
|
||||
fputcsv($handle, ['站点ID', $siteId]);
|
||||
fputcsv($handle, ['关键词', ($filters['keyword'] ?? '') !== '' ? $filters['keyword'] : '全部']);
|
||||
fputcsv($handle, ['订单状态', $this->statusLabel($filters['status'] ?? '')]);
|
||||
fputcsv($handle, ['支付状态', $this->paymentStatusLabel($filters['payment_status'] ?? '')]);
|
||||
fputcsv($handle, ['平台', $this->platformLabel($filters['platform'] ?? '')]);
|
||||
fputcsv($handle, ['设备类型', $this->displayFilterValue((string) ($filters['device_type'] ?? ''), $this->deviceTypeLabels())]);
|
||||
fputcsv($handle, ['支付渠道', $this->displayFilterValue((string) ($filters['payment_channel'] ?? ''), $this->paymentChannelLabels())]);
|
||||
fputcsv($handle, ['最低实付金额', $this->displayMoneyValue($filters['min_pay_amount'] ?? '')]);
|
||||
fputcsv($handle, ['最高实付金额', $this->displayMoneyValue($filters['max_pay_amount'] ?? '')]);
|
||||
fputcsv($handle, ['排序', $this->sortLabel($filters['sort'] ?? 'latest')]);
|
||||
fputcsv($handle, ['导出订单数', $exportSummary['total_orders'] ?? 0]);
|
||||
fputcsv($handle, ['导出实付总额', number_format((float) ($exportSummary['total_pay_amount'] ?? 0), 2, '.', '')]);
|
||||
fputcsv($handle, ['导出平均客单价', number_format((float) ($exportSummary['average_order_amount'] ?? 0), 2, '.', '')]);
|
||||
fputcsv($handle, ['导出已支付订单数', $exportSummary['paid_orders'] ?? 0]);
|
||||
fputcsv($handle, ['导出支付失败订单', $exportSummary['failed_payment_orders'] ?? 0]);
|
||||
fputcsv($handle, []);
|
||||
|
||||
fputcsv($handle, [
|
||||
'ID',
|
||||
'订单号',
|
||||
'订单状态',
|
||||
'支付状态',
|
||||
'平台',
|
||||
'设备类型',
|
||||
'支付渠道',
|
||||
'买家姓名',
|
||||
'买家手机',
|
||||
'买家邮箱',
|
||||
'商品金额',
|
||||
'优惠金额',
|
||||
'运费',
|
||||
'实付金额',
|
||||
'创建时间',
|
||||
'支付时间',
|
||||
'完成时间',
|
||||
'备注',
|
||||
]);
|
||||
|
||||
foreach ($this->applySorting($this->applyFilters(Order::query()->forMerchant($siteId), $filters), $filters)->cursor() as $order) {
|
||||
fputcsv($handle, [
|
||||
$order->id,
|
||||
$order->order_no,
|
||||
$this->statusLabel((string) $order->status),
|
||||
$this->paymentStatusLabel((string) $order->payment_status),
|
||||
$this->platformLabel((string) $order->platform),
|
||||
$this->deviceTypeLabel((string) $order->device_type),
|
||||
$this->paymentChannelLabel((string) $order->payment_channel),
|
||||
$order->buyer_name,
|
||||
$order->buyer_phone,
|
||||
$order->buyer_email,
|
||||
number_format((float) $order->product_amount, 2, '.', ''),
|
||||
number_format((float) $order->discount_amount, 2, '.', ''),
|
||||
number_format((float) $order->shipping_amount, 2, '.', ''),
|
||||
number_format((float) $order->pay_amount, 2, '.', ''),
|
||||
optional($order->created_at)?->format('Y-m-d H:i:s'),
|
||||
optional($order->paid_at)?->format('Y-m-d H:i:s'),
|
||||
optional($order->completed_at)?->format('Y-m-d H:i:s'),
|
||||
$order->remark,
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}, $fileName, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function filters(Request $request): array
|
||||
{
|
||||
$minPayAmount = trim((string) $request->string('min_pay_amount'));
|
||||
$maxPayAmount = trim((string) $request->string('max_pay_amount'));
|
||||
$validationErrors = [];
|
||||
|
||||
if ($minPayAmount !== '' && ! is_numeric($minPayAmount)) {
|
||||
$validationErrors[] = '最低实付金额必须为数字。';
|
||||
}
|
||||
|
||||
if ($maxPayAmount !== '' && ! is_numeric($maxPayAmount)) {
|
||||
$validationErrors[] = '最高实付金额必须为数字。';
|
||||
}
|
||||
|
||||
if ($minPayAmount !== '' && $maxPayAmount !== '' && is_numeric($minPayAmount) && is_numeric($maxPayAmount) && (float) $minPayAmount > (float) $maxPayAmount) {
|
||||
$validationErrors[] = '最低实付金额不能大于最高实付金额。';
|
||||
}
|
||||
|
||||
return [
|
||||
'keyword' => trim((string) $request->string('keyword')),
|
||||
'status' => trim((string) $request->string('status')),
|
||||
'payment_status' => trim((string) $request->string('payment_status')),
|
||||
'platform' => trim((string) $request->string('platform')),
|
||||
'device_type' => trim((string) $request->string('device_type')),
|
||||
'payment_channel' => trim((string) $request->string('payment_channel')),
|
||||
'min_pay_amount' => $minPayAmount,
|
||||
'max_pay_amount' => $maxPayAmount,
|
||||
'sort' => trim((string) $request->string('sort', 'latest')),
|
||||
'validation_errors' => $validationErrors,
|
||||
'has_validation_error' => ! empty($validationErrors),
|
||||
];
|
||||
}
|
||||
|
||||
protected function applyFilters(Builder $query, array $filters): Builder
|
||||
{
|
||||
return $query
|
||||
->when(($filters['status'] ?? '') !== '', fn ($builder) => $builder->where('status', $filters['status']))
|
||||
->when(($filters['payment_status'] ?? '') !== '', fn ($builder) => $builder->where('payment_status', $filters['payment_status']))
|
||||
->when(($filters['platform'] ?? '') !== '', fn ($builder) => $builder->where('platform', $filters['platform']))
|
||||
->when(($filters['device_type'] ?? '') !== '', fn ($builder) => $builder->where('device_type', $filters['device_type']))
|
||||
->when(($filters['payment_channel'] ?? '') !== '', fn ($builder) => $builder->where('payment_channel', $filters['payment_channel']))
|
||||
->when(($filters['keyword'] ?? '') !== '', fn ($builder) => $builder->where(function ($subQuery) use ($filters) {
|
||||
$subQuery->where('order_no', 'like', '%' . $filters['keyword'] . '%')
|
||||
->orWhere('buyer_name', 'like', '%' . $filters['keyword'] . '%')
|
||||
->orWhere('buyer_phone', 'like', '%' . $filters['keyword'] . '%')
|
||||
->orWhere('buyer_email', 'like', '%' . $filters['keyword'] . '%');
|
||||
}))
|
||||
->when(($filters['min_pay_amount'] ?? '') !== '' && is_numeric($filters['min_pay_amount']), fn ($builder) => $builder->where('pay_amount', '>=', $filters['min_pay_amount']))
|
||||
->when(($filters['max_pay_amount'] ?? '') !== '' && is_numeric($filters['max_pay_amount']), fn ($builder) => $builder->where('pay_amount', '<=', $filters['max_pay_amount']));
|
||||
}
|
||||
|
||||
protected function applySorting(Builder $query, array $filters): Builder
|
||||
{
|
||||
return match ($filters['sort'] ?? 'latest') {
|
||||
'oldest' => $query->orderBy('created_at')->orderBy('id'),
|
||||
'pay_amount_desc' => $query->orderByDesc('pay_amount')->orderByDesc('id'),
|
||||
'pay_amount_asc' => $query->orderBy('pay_amount')->orderByDesc('id'),
|
||||
default => $query->latest(),
|
||||
};
|
||||
}
|
||||
|
||||
protected function buildSummaryStats(Builder $query): array
|
||||
{
|
||||
$summary = (clone $query)
|
||||
->selectRaw('COUNT(*) as total_orders')
|
||||
->selectRaw('COALESCE(SUM(pay_amount), 0) as total_pay_amount')
|
||||
->selectRaw('COALESCE(AVG(pay_amount), 0) as average_order_amount')
|
||||
->selectRaw("SUM(CASE WHEN payment_status = 'paid' THEN 1 ELSE 0 END) as paid_orders")
|
||||
->selectRaw("SUM(CASE WHEN payment_status = 'failed' THEN 1 ELSE 0 END) as failed_payment_orders")
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total_orders' => (int) ($summary->total_orders ?? 0),
|
||||
'total_pay_amount' => (float) ($summary->total_pay_amount ?? 0),
|
||||
'average_order_amount' => (float) ($summary->average_order_amount ?? 0),
|
||||
'paid_orders' => (int) ($summary->paid_orders ?? 0),
|
||||
'failed_payment_orders' => (int) ($summary->failed_payment_orders ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildStatusStats(Builder $query): array
|
||||
{
|
||||
$counts = (clone $query)
|
||||
->selectRaw('status, COUNT(*) as aggregate')
|
||||
->groupBy('status')
|
||||
->pluck('aggregate', 'status');
|
||||
|
||||
$stats = ['all' => (int) $counts->sum()];
|
||||
|
||||
foreach ($this->statuses as $status) {
|
||||
$stats[$status] = (int) ($counts[$status] ?? 0);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
protected function emptySummaryStats(): array
|
||||
{
|
||||
return [
|
||||
'total_orders' => 0,
|
||||
'total_pay_amount' => 0,
|
||||
'average_order_amount' => 0,
|
||||
'paid_orders' => 0,
|
||||
'failed_payment_orders' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
protected function emptyStatusStats(): array
|
||||
{
|
||||
$stats = ['all' => 0];
|
||||
|
||||
foreach ($this->statuses as $status) {
|
||||
$stats[$status] = 0;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
protected function exportableFilters(array $filters): array
|
||||
{
|
||||
return [
|
||||
'keyword' => $filters['keyword'] ?? '',
|
||||
'status' => $filters['status'] ?? '',
|
||||
'payment_status' => $filters['payment_status'] ?? '',
|
||||
'platform' => $filters['platform'] ?? '',
|
||||
'device_type' => $filters['device_type'] ?? '',
|
||||
'payment_channel' => $filters['payment_channel'] ?? '',
|
||||
'min_pay_amount' => $filters['min_pay_amount'] ?? '',
|
||||
'max_pay_amount' => $filters['max_pay_amount'] ?? '',
|
||||
'sort' => $filters['sort'] ?? 'latest',
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildActiveFilterSummary(array $filters): array
|
||||
{
|
||||
return [
|
||||
'关键词' => ($filters['keyword'] ?? '') !== '' ? $filters['keyword'] : '全部',
|
||||
'订单状态' => $this->statusLabel($filters['status'] ?? ''),
|
||||
'支付状态' => $this->paymentStatusLabel($filters['payment_status'] ?? ''),
|
||||
'平台' => $this->platformLabel($filters['platform'] ?? ''),
|
||||
'设备类型' => $this->displayFilterValue((string) ($filters['device_type'] ?? ''), $this->deviceTypeLabels()),
|
||||
'支付渠道' => $this->displayFilterValue((string) ($filters['payment_channel'] ?? ''), $this->paymentChannelLabels()),
|
||||
'实付金额区间' => $this->formatMoneyRange($filters['min_pay_amount'] ?? '', $filters['max_pay_amount'] ?? ''),
|
||||
'排序' => $this->sortLabel($filters['sort'] ?? 'latest'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function statusLabels(): array
|
||||
{
|
||||
return [
|
||||
'pending' => '待处理',
|
||||
'paid' => '已支付',
|
||||
'shipped' => '已发货',
|
||||
'completed' => '已完成',
|
||||
'cancelled' => '已取消',
|
||||
];
|
||||
}
|
||||
|
||||
protected function statusLabel(string $status): string
|
||||
{
|
||||
return $this->statusLabels()[$status] ?? '全部';
|
||||
}
|
||||
|
||||
protected function paymentStatusLabels(): array
|
||||
{
|
||||
return [
|
||||
'unpaid' => '未支付',
|
||||
'paid' => '已支付',
|
||||
'refunded' => '已退款',
|
||||
'failed' => '支付失败',
|
||||
];
|
||||
}
|
||||
|
||||
protected function paymentStatusLabel(string $status): string
|
||||
{
|
||||
return $this->paymentStatusLabels()[$status] ?? '全部';
|
||||
}
|
||||
|
||||
protected function platformLabels(): array
|
||||
{
|
||||
return [
|
||||
'pc' => 'PC 端',
|
||||
'h5' => 'H5',
|
||||
'wechat_mp' => '微信公众号',
|
||||
'wechat_mini' => '微信小程序',
|
||||
'app' => 'APP 接口预留',
|
||||
];
|
||||
}
|
||||
|
||||
protected function platformLabel(string $platform): string
|
||||
{
|
||||
return $this->platformLabels()[$platform] ?? '全部';
|
||||
}
|
||||
|
||||
protected function deviceTypeLabels(): array
|
||||
{
|
||||
return [
|
||||
'desktop' => '桌面浏览器',
|
||||
'mobile' => '移动浏览器',
|
||||
'mini-program' => '小程序环境',
|
||||
'mobile-webview' => '微信内网页',
|
||||
'app-api' => 'APP 接口',
|
||||
];
|
||||
}
|
||||
|
||||
protected function deviceTypeLabel(string $deviceType): string
|
||||
{
|
||||
return $this->deviceTypeLabels()[$deviceType] ?? ($deviceType === '' ? '未设置' : $deviceType);
|
||||
}
|
||||
|
||||
protected function paymentChannelLabels(): array
|
||||
{
|
||||
return [
|
||||
'wechat_pay' => '微信支付',
|
||||
'alipay' => '支付宝',
|
||||
];
|
||||
}
|
||||
|
||||
protected function paymentChannelLabel(string $paymentChannel): string
|
||||
{
|
||||
return $this->paymentChannelLabels()[$paymentChannel] ?? ($paymentChannel === '' ? '未设置' : $paymentChannel);
|
||||
}
|
||||
|
||||
protected function formatMoneyRange(string $min, string $max): string
|
||||
{
|
||||
if ($min === '' && $max === '') {
|
||||
return '全部';
|
||||
}
|
||||
|
||||
$minLabel = $min !== '' && is_numeric($min)
|
||||
? ('¥' . number_format((float) $min, 2, '.', ''))
|
||||
: '不限';
|
||||
$maxLabel = $max !== '' && is_numeric($max)
|
||||
? ('¥' . number_format((float) $max, 2, '.', ''))
|
||||
: '不限';
|
||||
|
||||
return $minLabel . ' ~ ' . $maxLabel;
|
||||
}
|
||||
|
||||
protected function sortLabel(string $sort): string
|
||||
{
|
||||
return match ($sort) {
|
||||
'oldest' => '创建时间正序',
|
||||
'pay_amount_desc' => '实付金额从高到低',
|
||||
'pay_amount_asc' => '实付金额从低到高',
|
||||
default => '创建时间倒序',
|
||||
};
|
||||
}
|
||||
|
||||
protected function displayFilterValue(string $value, array $options): string
|
||||
{
|
||||
if ($value === '') {
|
||||
return '全部';
|
||||
}
|
||||
|
||||
return (string) ($options[$value] ?? $value);
|
||||
}
|
||||
|
||||
protected function displayMoneyValue(string $value): string
|
||||
{
|
||||
if ($value === '') {
|
||||
return '全部';
|
||||
}
|
||||
|
||||
return is_numeric($value) ? ('¥' . number_format((float) $value, 2, '.', '')) : $value;
|
||||
}
|
||||
|
||||
protected function workbenchLinks(): array
|
||||
{
|
||||
return [
|
||||
'paid_high_amount' => '/site-admin/orders?sort=pay_amount_desc&payment_status=paid',
|
||||
'pending_latest' => '/site-admin/orders?sort=latest&payment_status=unpaid',
|
||||
'failed_latest' => '/site-admin/orders?sort=latest&payment_status=failed',
|
||||
'completed_latest' => '/site-admin/orders?sort=latest&status=completed',
|
||||
'current' => '/site-admin/orders',
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildOperationsFocus(int $siteId, array $summaryStats, array $filters): array
|
||||
{
|
||||
$pendingCount = (int) Order::query()->forMerchant($siteId)->where('payment_status', 'unpaid')->count();
|
||||
$failedCount = (int) Order::query()->forMerchant($siteId)->where('payment_status', 'failed')->count();
|
||||
$completedCount = (int) Order::query()->forMerchant($siteId)->where('status', 'completed')->count();
|
||||
$links = $this->workbenchLinks();
|
||||
$currentQuery = http_build_query(array_filter($this->exportableFilters($filters), fn ($value) => $value !== null && $value !== ''));
|
||||
$currentUrl = $links['current'] . ($currentQuery !== '' ? ('?' . $currentQuery) : '');
|
||||
$workbench = [
|
||||
'高金额已支付' => $links['paid_high_amount'],
|
||||
'待支付跟进' => $links['pending_latest'],
|
||||
'支付失败排查' => $links['failed_latest'],
|
||||
'最近完成订单' => $links['completed_latest'],
|
||||
'返回当前筛选视图' => $currentUrl,
|
||||
];
|
||||
$signals = [
|
||||
'待支付订单' => $pendingCount,
|
||||
'支付失败订单' => $failedCount,
|
||||
'已完成订单' => $completedCount,
|
||||
];
|
||||
|
||||
if (($filters['platform'] ?? '') === 'wechat_mini') {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦微信小程序订单,建议优先关注下单到支付转化是否顺畅,并同步排查小程序端支付回流体验。',
|
||||
'actions' => [
|
||||
['label' => '继续查看微信小程序订单', 'url' => $currentUrl],
|
||||
['label' => '去看待支付订单', 'url' => $links['pending_latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($filters['payment_channel'] ?? '') === 'wechat_pay') {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦微信支付订单,建议优先核对支付成功率、回调稳定性与失败重试转化。',
|
||||
'actions' => [
|
||||
['label' => '继续查看微信支付订单', 'url' => $currentUrl],
|
||||
['label' => '去看支付失败订单', 'url' => $links['failed_latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($filters['device_type'] ?? '') === 'mini-program') {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦小程序环境订单,建议优先核对授权链路、支付唤起表现与下单回流是否顺畅。',
|
||||
'actions' => [
|
||||
['label' => '继续查看小程序环境订单', 'url' => $currentUrl],
|
||||
['label' => '去看微信支付订单', 'url' => $links['failed_latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($filters['device_type'] ?? '') === 'mobile-webview') {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦微信内网页订单,建议优先关注授权静默登录、页面跳转稳定性与支付拉起后的回流体验。',
|
||||
'actions' => [
|
||||
['label' => '继续查看微信内网页订单', 'url' => $currentUrl],
|
||||
['label' => '去看待支付订单', 'url' => $links['pending_latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($filters['device_type'] ?? '') === 'mobile') {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦移动浏览器订单,建议优先关注 H5 下单链路、页面加载稳定性与支付转化流失点。',
|
||||
'actions' => [
|
||||
['label' => '继续查看移动浏览器订单', 'url' => $currentUrl],
|
||||
['label' => '去看待支付订单', 'url' => $links['pending_latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($filters['device_type'] ?? '') === 'desktop') {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦桌面浏览器订单,建议优先关注 PC 端下单流程、页面首屏稳定性与高客单转化表现。',
|
||||
'actions' => [
|
||||
['label' => '继续查看桌面浏览器订单', 'url' => $currentUrl],
|
||||
['label' => '去看高金额已支付订单', 'url' => $links['paid_high_amount']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($filters['payment_status'] ?? '') === 'failed') {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦支付失败订单,建议优先排查支付渠道、回调结果与用户重试情况。',
|
||||
'actions' => [
|
||||
['label' => '继续查看支付失败订单', 'url' => $currentUrl],
|
||||
['label' => '去看高金额已支付订单', 'url' => $links['paid_high_amount']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($filters['payment_status'] ?? '') === 'unpaid') {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦待支付订单,建议优先跟进下单未支付用户,并观察支付转化时效。',
|
||||
'actions' => [
|
||||
['label' => '继续查看待支付订单', 'url' => $currentUrl],
|
||||
['label' => '去看高金额已支付订单', 'url' => $links['paid_high_amount']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($filters['payment_status'] ?? '') === 'paid') {
|
||||
return [
|
||||
'headline' => '当前正在查看已支付订单,建议优先关注高金额订单履约进度与异常售后风险。',
|
||||
'actions' => [
|
||||
['label' => '继续查看已支付订单', 'url' => $currentUrl],
|
||||
['label' => '去看最近完成订单', 'url' => $links['completed_latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($filters['status'] ?? '') === 'completed') {
|
||||
return [
|
||||
'headline' => '当前正在查看已完成订单,建议复盘高客单成交与复购来源,沉淀更稳定的转化路径。',
|
||||
'actions' => [
|
||||
['label' => '继续查看已完成订单', 'url' => $currentUrl],
|
||||
['label' => '去看高金额已支付订单', 'url' => $links['paid_high_amount']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($summaryStats['total_orders'] ?? 0) <= 0) {
|
||||
return [
|
||||
'headline' => '当前站点暂无订单,建议先确认交易链路、支付链路与回写链路是否都已打通。',
|
||||
'actions' => [
|
||||
['label' => '先看订单整体情况', 'url' => $links['paid_high_amount']],
|
||||
['label' => '去看待支付订单', 'url' => $links['pending_latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($summaryStats['total_orders'] ?? 0) < 5) {
|
||||
return [
|
||||
'headline' => '当前站点已有少量订单沉淀,建议优先关注待支付订单,并同步查看已支付订单质量。',
|
||||
'actions' => [
|
||||
['label' => '去看待支付订单', 'url' => $links['pending_latest']],
|
||||
['label' => '去看已支付订单', 'url' => $links['paid_high_amount']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'headline' => $failedCount > 0
|
||||
? '当前站点订单已形成基础规模,建议优先关注待支付、支付失败与高金额已支付订单。'
|
||||
: '当前站点订单已形成基础规模,建议优先关注待支付与高金额已支付订单,保持交易闭环稳定。',
|
||||
'actions' => $failedCount > 0
|
||||
? [
|
||||
['label' => '去看待支付订单', 'url' => $links['pending_latest']],
|
||||
['label' => '去看支付失败订单', 'url' => $links['failed_latest']],
|
||||
]
|
||||
: [
|
||||
['label' => '去看待支付订单', 'url' => $links['pending_latest']],
|
||||
['label' => '去看高金额已支付订单', 'url' => $links['paid_high_amount']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
}
|
||||
699
app/Http/Controllers/SiteAdmin/ProductController.php
Normal file
699
app/Http/Controllers/SiteAdmin/ProductController.php
Normal file
@@ -0,0 +1,699 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\SiteAdmin;
|
||||
|
||||
use App\Http\Controllers\Concerns\ResolvesSiteContext;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductCategory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
use ResolvesSiteContext;
|
||||
|
||||
protected array $statusOptions = ['draft', 'published', 'offline'];
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$siteId = $this->siteId($request);
|
||||
$site = $this->site($request);
|
||||
$filters = $this->filters($request);
|
||||
$statusStatsFilters = $filters;
|
||||
$statusStatsFilters['status'] = '';
|
||||
|
||||
if ($filters['has_validation_error'] ?? false) {
|
||||
return view('site_admin.products.index', [
|
||||
'site' => $site,
|
||||
'products' => Product::query()->whereRaw('1 = 0')->paginate(10)->withQueryString(),
|
||||
'summaryStats' => $this->emptySummaryStats(),
|
||||
'statusStats' => $this->emptyStatusStats(),
|
||||
'activeFilterSummary' => $this->buildActiveFilterSummary($filters),
|
||||
'operationsFocus' => $this->buildOperationsFocus($siteId, $this->emptySummaryStats(), $filters),
|
||||
'workbenchLinks' => $this->workbenchLinks(),
|
||||
'filters' => $filters,
|
||||
'statusLabels' => $this->statusLabels(),
|
||||
'filterOptions' => [
|
||||
'statuses' => $this->statusOptions,
|
||||
'sortOptions' => [
|
||||
'latest' => '最新创建',
|
||||
'price_asc' => '价格从低到高',
|
||||
'price_desc' => '价格从高到低',
|
||||
'stock_asc' => '库存从低到高',
|
||||
'stock_desc' => '库存从高到低',
|
||||
],
|
||||
],
|
||||
'categories' => ProductCategory::query()->forMerchant($siteId)->orderBy('sort')->orderBy('id')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
$summaryStats = $this->buildSummaryStats(
|
||||
$this->applyFilters(Product::query()->forMerchant($siteId), $statusStatsFilters)
|
||||
);
|
||||
|
||||
return view('site_admin.products.index', [
|
||||
'site' => $site,
|
||||
'products' => $this->applySorting(
|
||||
$this->applyFilters(Product::query()->with('category')->forMerchant($siteId), $filters),
|
||||
$filters
|
||||
)->paginate(10)->withQueryString(),
|
||||
'summaryStats' => $summaryStats,
|
||||
'statusStats' => $this->buildStatusStats(
|
||||
$this->applyFilters(Product::query()->forMerchant($siteId), $statusStatsFilters)
|
||||
),
|
||||
'activeFilterSummary' => $this->buildActiveFilterSummary($filters),
|
||||
'operationsFocus' => $this->buildOperationsFocus($siteId, $summaryStats, $filters),
|
||||
'workbenchLinks' => $this->workbenchLinks(),
|
||||
'filters' => $filters,
|
||||
'statusLabels' => $this->statusLabels(),
|
||||
'filterOptions' => [
|
||||
'statuses' => $this->statusOptions,
|
||||
'sortOptions' => [
|
||||
'latest' => '最新创建',
|
||||
'price_asc' => '价格从低到高',
|
||||
'price_desc' => '价格从高到低',
|
||||
'stock_asc' => '库存从低到高',
|
||||
'stock_desc' => '库存从高到低',
|
||||
],
|
||||
],
|
||||
'categories' => ProductCategory::query()->forMerchant($siteId)->orderBy('sort')->orderBy('id')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request): StreamedResponse|RedirectResponse
|
||||
{
|
||||
$siteId = $this->siteId($request);
|
||||
$filters = $this->filters($request);
|
||||
|
||||
if ($filters['has_validation_error'] ?? false) {
|
||||
return redirect('/site-admin/products?' . http_build_query($this->exportableFilters($filters)))
|
||||
->withErrors($filters['validation_errors'] ?? ['商品筛选条件不合法,请先修正后再导出。']);
|
||||
}
|
||||
|
||||
$fileName = 'site_' . $siteId . '_products_' . now()->format('Ymd_His') . '.csv';
|
||||
|
||||
$exportSummary = $this->buildSummaryStats(
|
||||
$this->applyFilters(Product::query()->forMerchant($siteId), $filters)
|
||||
);
|
||||
|
||||
return response()->streamDownload(function () use ($siteId, $filters, $exportSummary) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
|
||||
fputcsv($handle, ['导出信息', '站点商品导出']);
|
||||
fputcsv($handle, ['站点ID', $siteId]);
|
||||
fputcsv($handle, ['关键词', $filters['keyword'] !== '' ? $filters['keyword'] : '全部']);
|
||||
fputcsv($handle, ['状态', $this->statusLabel($filters['status'] ?? '')]);
|
||||
fputcsv($handle, ['分类', $this->categoryLabel($filters['category_id'] ?? '')]);
|
||||
fputcsv($handle, ['最低价格', $filters['min_price'] !== '' && is_numeric($filters['min_price']) ? ('¥' . number_format((float) $filters['min_price'], 2, '.', '')) : '全部']);
|
||||
fputcsv($handle, ['最高价格', $filters['max_price'] !== '' && is_numeric($filters['max_price']) ? ('¥' . number_format((float) $filters['max_price'], 2, '.', '')) : '全部']);
|
||||
fputcsv($handle, ['最低库存', $filters['min_stock'] !== '' ? ($filters['min_stock'] . ' 件') : '全部']);
|
||||
fputcsv($handle, ['最高库存', $filters['max_stock'] !== '' ? ($filters['max_stock'] . ' 件') : '全部']);
|
||||
fputcsv($handle, ['排序', $this->sortLabel($filters['sort'] ?? 'latest')]);
|
||||
fputcsv($handle, ['导出商品数', $exportSummary['total_products'] ?? 0]);
|
||||
fputcsv($handle, ['导出总库存', $exportSummary['total_stock'] ?? 0]);
|
||||
fputcsv($handle, ['导出总货值', number_format((float) ($exportSummary['total_stock_value'] ?? 0), 2, '.', '')]);
|
||||
fputcsv($handle, ['导出平均售价', number_format((float) ($exportSummary['average_price'] ?? 0), 2, '.', '')]);
|
||||
fputcsv($handle, []);
|
||||
|
||||
fputcsv($handle, [
|
||||
'ID',
|
||||
'分类ID',
|
||||
'分类名称',
|
||||
'商品标题',
|
||||
'商品Slug',
|
||||
'SKU',
|
||||
'售价',
|
||||
'划线价',
|
||||
'库存',
|
||||
'状态',
|
||||
'商品简介',
|
||||
'创建时间',
|
||||
'更新时间',
|
||||
]);
|
||||
|
||||
foreach ($this->applySorting($this->applyFilters(Product::query()->with('category')->forMerchant($siteId), $filters), $filters)->cursor() as $product) {
|
||||
fputcsv($handle, [
|
||||
$product->id,
|
||||
$product->category_id,
|
||||
$product->category?->name ?? '',
|
||||
$product->title,
|
||||
$product->slug,
|
||||
$product->sku,
|
||||
number_format((float) $product->price, 2, '.', ''),
|
||||
number_format((float) $product->original_price, 2, '.', ''),
|
||||
$product->stock,
|
||||
$this->statusLabel($product->status),
|
||||
$product->summary,
|
||||
optional($product->created_at)?->format('Y-m-d H:i:s'),
|
||||
optional($product->updated_at)?->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}, $fileName, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function filters(Request $request): array
|
||||
{
|
||||
$minPrice = trim((string) $request->string('min_price'));
|
||||
$maxPrice = trim((string) $request->string('max_price'));
|
||||
$minStock = trim((string) $request->string('min_stock'));
|
||||
$maxStock = trim((string) $request->string('max_stock'));
|
||||
$validationErrors = [];
|
||||
|
||||
if ($minPrice !== '' && ! is_numeric($minPrice)) {
|
||||
$validationErrors[] = '最低价格必须为数字。';
|
||||
}
|
||||
|
||||
if ($maxPrice !== '' && ! is_numeric($maxPrice)) {
|
||||
$validationErrors[] = '最高价格必须为数字。';
|
||||
}
|
||||
|
||||
if ($minPrice !== '' && $maxPrice !== '' && is_numeric($minPrice) && is_numeric($maxPrice) && (float) $minPrice > (float) $maxPrice) {
|
||||
$validationErrors[] = '最低价格不能大于最高价格。';
|
||||
}
|
||||
|
||||
if ($minStock !== '' && filter_var($minStock, FILTER_VALIDATE_INT) === false) {
|
||||
$validationErrors[] = '最低库存必须为整数。';
|
||||
}
|
||||
|
||||
if ($maxStock !== '' && filter_var($maxStock, FILTER_VALIDATE_INT) === false) {
|
||||
$validationErrors[] = '最高库存必须为整数。';
|
||||
}
|
||||
|
||||
if ($minStock !== '' && $maxStock !== '' && filter_var($minStock, FILTER_VALIDATE_INT) !== false && filter_var($maxStock, FILTER_VALIDATE_INT) !== false && (int) $minStock > (int) $maxStock) {
|
||||
$validationErrors[] = '最低库存不能大于最高库存。';
|
||||
}
|
||||
|
||||
return [
|
||||
'keyword' => trim((string) $request->string('keyword')),
|
||||
'status' => trim((string) $request->string('status')),
|
||||
'category_id' => trim((string) $request->string('category_id')),
|
||||
'min_price' => $minPrice,
|
||||
'max_price' => $maxPrice,
|
||||
'min_stock' => $minStock,
|
||||
'max_stock' => $maxStock,
|
||||
'sort' => trim((string) $request->string('sort', 'latest')),
|
||||
'validation_errors' => $validationErrors,
|
||||
'has_validation_error' => ! empty($validationErrors),
|
||||
];
|
||||
}
|
||||
|
||||
protected function applyFilters(Builder $query, array $filters): Builder
|
||||
{
|
||||
return $query
|
||||
->when(($filters['keyword'] ?? '') !== '', fn ($builder) => $builder->where(function ($subQuery) use ($filters) {
|
||||
$subQuery->where('title', 'like', '%' . $filters['keyword'] . '%')
|
||||
->orWhere('sku', 'like', '%' . $filters['keyword'] . '%')
|
||||
->orWhere('slug', 'like', '%' . $filters['keyword'] . '%');
|
||||
}))
|
||||
->when(($filters['status'] ?? '') !== '', fn ($builder) => $builder->where('status', $filters['status']))
|
||||
->when(($filters['category_id'] ?? '') !== '', fn ($builder) => $builder->where('category_id', $filters['category_id']))
|
||||
->when(($filters['min_price'] ?? '') !== '' && is_numeric($filters['min_price']), fn ($builder) => $builder->where('price', '>=', $filters['min_price']))
|
||||
->when(($filters['max_price'] ?? '') !== '' && is_numeric($filters['max_price']), fn ($builder) => $builder->where('price', '<=', $filters['max_price']))
|
||||
->when(($filters['min_stock'] ?? '') !== '' && filter_var($filters['min_stock'], FILTER_VALIDATE_INT) !== false, fn ($builder) => $builder->where('stock', '>=', (int) $filters['min_stock']))
|
||||
->when(($filters['max_stock'] ?? '') !== '' && filter_var($filters['max_stock'], FILTER_VALIDATE_INT) !== false, fn ($builder) => $builder->where('stock', '<=', (int) $filters['max_stock']));
|
||||
}
|
||||
|
||||
protected function applySorting(Builder $query, array $filters): Builder
|
||||
{
|
||||
return match ($filters['sort'] ?? 'latest') {
|
||||
'price_asc' => $query->orderBy('price')->orderByDesc('id'),
|
||||
'price_desc' => $query->orderByDesc('price')->orderByDesc('id'),
|
||||
'stock_asc' => $query->orderBy('stock')->orderByDesc('id'),
|
||||
'stock_desc' => $query->orderByDesc('stock')->orderByDesc('id'),
|
||||
default => $query->latest(),
|
||||
};
|
||||
}
|
||||
|
||||
protected function buildSummaryStats(Builder $query): array
|
||||
{
|
||||
$summary = (clone $query)
|
||||
->selectRaw('COUNT(*) as total_products')
|
||||
->selectRaw('COALESCE(SUM(stock), 0) as total_stock')
|
||||
->selectRaw('COALESCE(SUM(price * stock), 0) as total_stock_value')
|
||||
->selectRaw('COALESCE(AVG(price), 0) as average_price')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total_products' => (int) ($summary->total_products ?? 0),
|
||||
'total_stock' => (int) ($summary->total_stock ?? 0),
|
||||
'total_stock_value' => (float) ($summary->total_stock_value ?? 0),
|
||||
'average_price' => (float) ($summary->average_price ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildStatusStats(Builder $query): array
|
||||
{
|
||||
$counts = (clone $query)
|
||||
->selectRaw('status, COUNT(*) as aggregate')
|
||||
->groupBy('status')
|
||||
->pluck('aggregate', 'status');
|
||||
|
||||
$stats = ['all' => (int) $counts->sum()];
|
||||
|
||||
foreach ($this->statusOptions as $status) {
|
||||
$stats[$status] = (int) ($counts[$status] ?? 0);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
protected function emptySummaryStats(): array
|
||||
{
|
||||
return [
|
||||
'total_products' => 0,
|
||||
'total_stock' => 0,
|
||||
'total_stock_value' => 0,
|
||||
'average_price' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
protected function emptyStatusStats(): array
|
||||
{
|
||||
$stats = ['all' => 0];
|
||||
|
||||
foreach ($this->statusOptions as $status) {
|
||||
$stats[$status] = 0;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
protected function exportableFilters(array $filters): array
|
||||
{
|
||||
return [
|
||||
'keyword' => $filters['keyword'] ?? '',
|
||||
'status' => $filters['status'] ?? '',
|
||||
'category_id' => $filters['category_id'] ?? '',
|
||||
'min_price' => $filters['min_price'] ?? '',
|
||||
'max_price' => $filters['max_price'] ?? '',
|
||||
'min_stock' => $filters['min_stock'] ?? '',
|
||||
'max_stock' => $filters['max_stock'] ?? '',
|
||||
'sort' => $filters['sort'] ?? 'latest',
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildActiveFilterSummary(array $filters): array
|
||||
{
|
||||
return [
|
||||
'关键词' => $this->displayTextValue($filters['keyword'] ?? '', '全部'),
|
||||
'状态' => $this->statusLabel($filters['status'] ?? ''),
|
||||
'分类' => $this->categoryLabel($filters['category_id'] ?? ''),
|
||||
'价格区间' => $this->formatMoneyRange($filters['min_price'] ?? '', $filters['max_price'] ?? ''),
|
||||
'库存区间' => $this->formatStockRange($filters['min_stock'] ?? '', $filters['max_stock'] ?? ''),
|
||||
'排序' => $this->sortLabel($filters['sort'] ?? 'latest'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function statusLabels(): array
|
||||
{
|
||||
return [
|
||||
'draft' => '草稿',
|
||||
'published' => '已上架',
|
||||
'offline' => '已下架',
|
||||
];
|
||||
}
|
||||
|
||||
protected function statusLabel(string $status): string
|
||||
{
|
||||
return $this->statusLabels()[$status] ?? '全部';
|
||||
}
|
||||
|
||||
protected function categoryLabel(string $categoryId): string
|
||||
{
|
||||
if ($categoryId === '') {
|
||||
return '全部';
|
||||
}
|
||||
|
||||
$category = ProductCategory::query()->find($categoryId);
|
||||
|
||||
return $category?->name ?? ('分类 #' . $categoryId);
|
||||
}
|
||||
|
||||
protected function formatMoneyRange(string $min, string $max): string
|
||||
{
|
||||
if ($min === '' && $max === '') {
|
||||
return '全部';
|
||||
}
|
||||
|
||||
$minLabel = $min !== '' && is_numeric($min)
|
||||
? ('¥' . number_format((float) $min, 2, '.', ''))
|
||||
: '不限';
|
||||
$maxLabel = $max !== '' && is_numeric($max)
|
||||
? ('¥' . number_format((float) $max, 2, '.', ''))
|
||||
: '不限';
|
||||
|
||||
return $minLabel . ' ~ ' . $maxLabel;
|
||||
}
|
||||
|
||||
protected function formatStockRange(string $min, string $max): string
|
||||
{
|
||||
if ($min === '' && $max === '') {
|
||||
return '全部';
|
||||
}
|
||||
|
||||
$minLabel = $min !== '' ? $min : '不限';
|
||||
$maxLabel = $max !== '' ? $max : '不限';
|
||||
|
||||
return $minLabel . ' ~ ' . $maxLabel . ' 件';
|
||||
}
|
||||
|
||||
protected function sortLabel(string $sort): string
|
||||
{
|
||||
return match ($sort) {
|
||||
'price_asc' => '价格从低到高',
|
||||
'price_desc' => '价格从高到低',
|
||||
'stock_asc' => '库存从低到高',
|
||||
'stock_desc' => '库存从高到低',
|
||||
default => '最新创建',
|
||||
};
|
||||
}
|
||||
|
||||
protected function displayTextValue(string $value, string $default = '未设置'): string
|
||||
{
|
||||
return $value === '' ? $default : $value;
|
||||
}
|
||||
|
||||
protected function displayMoneyValue(string $value): string
|
||||
{
|
||||
if ($value === '') {
|
||||
return '全部';
|
||||
}
|
||||
|
||||
return is_numeric($value) ? ('¥' . number_format((float) $value, 2, '.', '')) : $value;
|
||||
}
|
||||
|
||||
protected function displayStockValue(string $value): string
|
||||
{
|
||||
return $value === '' ? '全部' : ($value . ' 件');
|
||||
}
|
||||
|
||||
protected function workbenchLinks(): array
|
||||
{
|
||||
return [
|
||||
'published_stock_desc' => '/site-admin/products?sort=stock_desc&status=published',
|
||||
'published_stock_asc' => '/site-admin/products?sort=stock_asc&status=published',
|
||||
'latest' => '/site-admin/products?sort=latest',
|
||||
'draft' => '/site-admin/products?status=draft&sort=latest',
|
||||
'current' => '/site-admin/products',
|
||||
];
|
||||
}
|
||||
|
||||
protected function hasCategoryFilter(array $filters): bool
|
||||
{
|
||||
return ($filters['category_id'] ?? '') !== '';
|
||||
}
|
||||
|
||||
protected function hasKeywordFilter(array $filters): bool
|
||||
{
|
||||
return ($filters['keyword'] ?? '') !== '';
|
||||
}
|
||||
|
||||
protected function hasPriceRangeFilter(array $filters): bool
|
||||
{
|
||||
return (($filters['min_price'] ?? '') !== '') || (($filters['max_price'] ?? '') !== '');
|
||||
}
|
||||
|
||||
protected function hasPublishedStockFocus(array $filters): bool
|
||||
{
|
||||
return ($filters['status'] ?? '') === 'published'
|
||||
&& ((($filters['max_stock'] ?? '') !== '')
|
||||
|| ((($filters['min_stock'] ?? '') !== '')
|
||||
&& is_numeric($filters['min_stock'])
|
||||
&& (int) $filters['min_stock'] <= 20));
|
||||
}
|
||||
|
||||
protected function buildOperationsFocusResponse(string $headline, array $actions, array $workbench, array $signals): array
|
||||
{
|
||||
return [
|
||||
'headline' => $headline,
|
||||
'actions' => $actions,
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildOperationsFocus(int $siteId, array $summaryStats, array $filters): array
|
||||
{
|
||||
$publishedCount = (int) Product::query()->forMerchant($siteId)->where('status', 'published')->count();
|
||||
$lowStockCount = (int) Product::query()->forMerchant($siteId)->where('status', 'published')->where('stock', '<=', 20)->count();
|
||||
$categoryCount = (int) ProductCategory::query()->forMerchant($siteId)->count();
|
||||
$links = $this->workbenchLinks();
|
||||
$currentQuery = http_build_query(array_filter($this->exportableFilters($filters), fn ($value) => $value !== null && $value !== ''));
|
||||
$currentUrl = $links['current'] . ($currentQuery !== '' ? ('?' . $currentQuery) : '');
|
||||
$workbench = [
|
||||
'高库存已上架' => $links['published_stock_desc'],
|
||||
'低库存补货' => $links['published_stock_asc'],
|
||||
'最近新增' => $links['latest'],
|
||||
'草稿待整理' => $links['draft'],
|
||||
'返回当前筛选视图' => $currentUrl,
|
||||
];
|
||||
$signals = [
|
||||
'已上架商品' => $publishedCount,
|
||||
'低库存商品' => $lowStockCount,
|
||||
'分类覆盖数' => $categoryCount,
|
||||
];
|
||||
|
||||
$isPublished = ($filters['status'] ?? '') === 'published';
|
||||
$hasCategoryFilter = $this->hasCategoryFilter($filters);
|
||||
$hasKeywordFilter = $this->hasKeywordFilter($filters);
|
||||
$hasPriceRangeFilter = $this->hasPriceRangeFilter($filters);
|
||||
$categoryLabel = $hasCategoryFilter ? $this->categoryLabel((string) ($filters['category_id'] ?? '')) : '';
|
||||
$keyword = (string) ($filters['keyword'] ?? '');
|
||||
$priceRange = $hasPriceRangeFilter ? $this->formatMoneyRange((string) ($filters['min_price'] ?? ''), (string) ($filters['max_price'] ?? '')) : '';
|
||||
$categoryUrl = $links['current'] . '?category_id=' . ($filters['category_id'] ?? '');
|
||||
$categoryKeywordUrl = $categoryUrl . '&keyword=' . urlencode($keyword);
|
||||
$publishedCategoryUrl = $links['current'] . '?status=published&category_id=' . ($filters['category_id'] ?? '');
|
||||
$publishedKeywordUrl = $links['current'] . '?status=published&keyword=' . urlencode($keyword);
|
||||
$publishedCategoryKeywordUrl = $publishedCategoryUrl . '&keyword=' . urlencode($keyword);
|
||||
|
||||
if (($filters['status'] ?? '') === 'draft') {
|
||||
return $this->buildOperationsFocusResponse(
|
||||
'当前正在查看草稿商品,建议优先补齐标题、分类、价格与库存后再安排上架。',
|
||||
[
|
||||
['label' => '继续查看当前草稿', 'url' => $currentUrl],
|
||||
['label' => '去看已上架商品', 'url' => $links['published_stock_desc']],
|
||||
],
|
||||
$workbench,
|
||||
$signals,
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->hasPublishedStockFocus($filters)) {
|
||||
return $this->buildOperationsFocusResponse(
|
||||
'当前筛选已聚焦已上架库存视角,建议优先确认低库存补货节奏,并同步观察高库存结构是否健康。',
|
||||
[
|
||||
['label' => '继续查看当前库存视角', 'url' => $currentUrl],
|
||||
['label' => '去看高库存商品', 'url' => $links['published_stock_desc']],
|
||||
],
|
||||
$workbench,
|
||||
$signals,
|
||||
);
|
||||
}
|
||||
|
||||
if ($isPublished && $hasCategoryFilter && $hasKeywordFilter && $hasPriceRangeFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦已上架的“' . $categoryLabel . '”分类下关键词“' . $keyword . '”命中的价格带 ' . $priceRange . ' 商品,建议优先核对在售商品命名、分类承接、价格梯度与搜索结果是否一致,并同步观察库存结构是否健康。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前已上架分类关键词价格带商品', 'url' => $currentUrl],
|
||||
['label' => '去看当前已上架分类关键词商品', 'url' => $publishedCategoryKeywordUrl],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($isPublished && $hasCategoryFilter && $hasKeywordFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦已上架的“' . $categoryLabel . '”分类下关键词“' . $keyword . '”命中的商品,建议优先核对在售商品命名、分类承接与搜索结果是否一致,并同步观察价格带与库存结构是否健康。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前已上架分类关键词商品', 'url' => $currentUrl],
|
||||
['label' => '去看当前已上架分类商品', 'url' => $publishedCategoryUrl],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($isPublished && $hasCategoryFilter && $hasPriceRangeFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦已上架的“' . $categoryLabel . '”分类下价格带 ' . $priceRange . ' 商品,建议优先核对该分类在售商品的价格结构、库存分布与承接效率是否协调。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前已上架分类价格带商品', 'url' => $currentUrl],
|
||||
['label' => '去看当前已上架分类商品', 'url' => $publishedCategoryUrl],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($isPublished && $hasKeywordFilter && $hasPriceRangeFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦已上架商品中关键词“' . $keyword . '”命中的价格带 ' . $priceRange . ' 结果,建议优先核对在售商品命名、卖点表达与价格梯度是否一致,并同步观察库存结构与搜索承接是否健康。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前已上架关键词价格带商品', 'url' => $currentUrl],
|
||||
['label' => '去看当前已上架关键词商品', 'url' => $publishedKeywordUrl],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($isPublished && $hasKeywordFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦已上架商品中关键词“' . $keyword . '”命中的结果,建议优先核对在售商品命名、卖点表达与搜索承接是否一致,并同步观察价格带与库存结构是否健康。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前已上架关键词商品', 'url' => $currentUrl],
|
||||
['label' => '去看全部已上架商品', 'url' => $links['published_stock_desc']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasCategoryFilter && $hasKeywordFilter && $hasPriceRangeFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦“' . $categoryLabel . '”分类下关键词“' . $keyword . '”命中的价格带 ' . $priceRange . ' 商品,建议优先核对分类承接、命名卖点与价格梯度是否一致,并同步观察库存结构与搜索承接是否健康。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前分类关键词价格带商品', 'url' => $currentUrl],
|
||||
['label' => '去看当前分类关键词商品', 'url' => $categoryKeywordUrl],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasCategoryFilter && $hasKeywordFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦“' . $categoryLabel . '”分类下关键词“' . $keyword . '”命中的商品,建议优先核对分类承接、命名卖点与搜索结果是否一致,并同步观察相关商品的价格带与库存结构。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前分类关键词商品', 'url' => $currentUrl],
|
||||
['label' => '去看当前分类商品', 'url' => $categoryUrl],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasCategoryFilter && $hasPriceRangeFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦“' . $categoryLabel . '”分类下价格带 ' . $priceRange . ' 商品,建议优先核对该分类价格结构是否连贯,并同步观察库存分布与承接效率是否健康。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前分类价格带商品', 'url' => $currentUrl],
|
||||
['label' => '去看当前分类商品', 'url' => $categoryUrl],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($isPublished && $hasCategoryFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦已上架的“' . $categoryLabel . '”分类商品,建议优先核对该分类在售商品的价格带、库存结构与承接质量是否均衡。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前已上架分类商品', 'url' => $currentUrl],
|
||||
['label' => '去看当前分类商品', 'url' => $categoryUrl],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasCategoryFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦“' . $categoryLabel . '”分类商品,建议优先核对分类承接是否准确,并同步观察价格带与库存结构是否均衡。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前分类商品', 'url' => $currentUrl],
|
||||
['label' => '去看低库存商品', 'url' => $links['published_stock_asc']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasKeywordFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦关键词“' . $keyword . '”命中的商品,建议优先核对命名、卖点与搜索承接是否一致,并同步观察相关商品的价格带与库存结构。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前关键词商品', 'url' => $currentUrl],
|
||||
['label' => '去看最近新增商品', 'url' => $links['latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasPriceRangeFilter) {
|
||||
return [
|
||||
'headline' => '当前筛选已聚焦价格带 ' . $priceRange . ' 商品,建议优先核对定价梯度是否连贯,并同步观察库存结构与转化表现是否匹配。',
|
||||
'actions' => [
|
||||
['label' => '继续查看当前价格带商品', 'url' => $currentUrl],
|
||||
['label' => '去看最近新增商品', 'url' => $links['latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if ($isPublished) {
|
||||
return [
|
||||
'headline' => '当前正在查看已上架商品,建议优先关注库存结构、价格带与分类覆盖是否均衡。',
|
||||
'actions' => [
|
||||
['label' => '去看低库存商品', 'url' => $links['published_stock_asc']],
|
||||
['label' => '继续查看已上架商品', 'url' => $currentUrl],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($summaryStats['total_products'] ?? 0) <= 0) {
|
||||
return [
|
||||
'headline' => '当前站点暂无商品,建议先补齐基础商品数据,再开始做上架与库存运营。',
|
||||
'actions' => [
|
||||
['label' => '先看商品空白情况', 'url' => $links['latest']],
|
||||
['label' => '查看草稿商品', 'url' => $links['draft']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
if (($summaryStats['total_products'] ?? 0) < 3) {
|
||||
return [
|
||||
'headline' => '当前站点商品仍较少,建议优先查看最近新增与已上架商品,确认基础信息是否完整。',
|
||||
'actions' => [
|
||||
['label' => '去看最近新增商品', 'url' => $links['latest']],
|
||||
['label' => '去看已上架商品', 'url' => $links['published_stock_desc']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'headline' => $lowStockCount > 0
|
||||
? '当前站点商品已形成基础规模,建议优先巡检低库存商品,并同步关注高库存结构是否均衡。'
|
||||
: '当前站点商品已形成基础规模,建议优先关注高库存结构与最近新增商品质量。',
|
||||
'actions' => $lowStockCount > 0
|
||||
? [
|
||||
['label' => '去看低库存商品', 'url' => $links['published_stock_asc']],
|
||||
['label' => '去看高库存商品', 'url' => $links['published_stock_desc']],
|
||||
]
|
||||
: [
|
||||
['label' => '去看高库存商品', 'url' => $links['published_stock_desc']],
|
||||
['label' => '去看最近新增商品', 'url' => $links['latest']],
|
||||
],
|
||||
'workbench' => $workbench,
|
||||
'signals' => $signals,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user