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,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,
];
}
}