315 lines
11 KiB
PHP
315 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Business\KukaPostingPerformance;
|
|
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Throwable;
|
|
|
|
class KukaPostingPerformancePageData
|
|
{
|
|
private const EXCLUDED_PLATFORMS = ['京东网银钱包'];
|
|
|
|
private const INCOME_SQL = 'COALESCE(income_amount, 0)'
|
|
. ' + COALESCE(discount_amount, 0)'
|
|
. ' + COALESCE(offset_amount, 0)'
|
|
. ' + COALESCE(refund_amount, 0)'
|
|
. ' + COALESCE(default_compensation_amount, 0)';
|
|
|
|
/**
|
|
* 业绩明细的 12 家店铺月度收入汇总。
|
|
*
|
|
* 收入口径:账单收入 + 优惠金额 + 冲减金额 + 退款金额 + 违约赔付(冲减收入)。
|
|
*
|
|
* @return array<string,mixed>
|
|
*/
|
|
public function build(Request $request): array
|
|
{
|
|
$stores = array_values(array_unique(array_filter(array_map(
|
|
static fn (mixed $store): string => trim((string) $store),
|
|
(array) config('kuka_posting.stores', []),
|
|
))));
|
|
|
|
if ($stores === []) {
|
|
return $this->emptyResult($stores, '尚未配置灵珊店铺业绩的店铺范围。');
|
|
}
|
|
|
|
try {
|
|
$months = $this->availableMonths();
|
|
$month = $this->selectedMonth((string) $request->query('month', ''), $months);
|
|
if ($month === null) {
|
|
return $this->emptyResult($stores, '业绩明细暂时没有可汇总的月份。');
|
|
}
|
|
|
|
$storeRows = cache()->remember(
|
|
$this->cacheKey('stores', $month, $stores),
|
|
now()->addMinutes(10),
|
|
fn (): array => $this->storeRows($month, $stores),
|
|
);
|
|
|
|
$monthlyTotals = cache()->remember(
|
|
$this->cacheKey('months', implode('|', $months), $stores),
|
|
now()->addMinutes(10),
|
|
fn (): array => $this->monthlyTotals($months, $stores),
|
|
);
|
|
|
|
$summary = $this->summary($storeRows);
|
|
|
|
return [
|
|
'stores' => $storeRows,
|
|
'months' => $months,
|
|
'month' => $month,
|
|
'month_label' => $this->monthLabel($month),
|
|
'summary' => $summary,
|
|
'monthly_totals' => $monthlyTotals,
|
|
'error' => null,
|
|
];
|
|
} catch (Throwable $exception) {
|
|
report($exception);
|
|
|
|
return $this->emptyResult(
|
|
$stores,
|
|
'灵珊店铺业绩暂时无法读取,请稍后重试;这不代表该月收入为 0。',
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function availableMonths(): array
|
|
{
|
|
return cache()->remember(
|
|
$this->cacheKey('available-months'),
|
|
now()->addMinutes(10),
|
|
function (): array {
|
|
$range = $this->connection()
|
|
->table('store_receipt_order_match_rows')
|
|
->whereNotIn('platform', self::EXCLUDED_PLATFORMS)
|
|
->whereNotNull('report_month')
|
|
->selectRaw('MIN(report_month) as min_month, MAX(report_month) as max_month')
|
|
->first();
|
|
|
|
if (($range->min_month ?? null) === null || ($range->max_month ?? null) === null) {
|
|
return [];
|
|
}
|
|
|
|
$end = CarbonImmutable::parse((string) $range->max_month)->startOfMonth();
|
|
$start = CarbonImmutable::parse((string) $range->min_month)->startOfMonth();
|
|
$earliest = now()->startOfMonth()->subMonthsNoOverflow(11);
|
|
if ($start->lt($earliest)) {
|
|
$start = $earliest;
|
|
}
|
|
|
|
$months = [];
|
|
for ($cursor = $end; $cursor->gte($start); $cursor = $cursor->subMonthNoOverflow()) {
|
|
$months[] = $cursor->format('Y-m');
|
|
}
|
|
|
|
return $months;
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $months
|
|
*/
|
|
private function selectedMonth(string $requested, array $months): ?string
|
|
{
|
|
if (preg_match('/^\d{4}-\d{2}$/', $requested) === 1 && in_array($requested, $months, true)) {
|
|
return $requested;
|
|
}
|
|
|
|
$previousMonth = now()->subMonthNoOverflow()->format('Y-m');
|
|
|
|
return in_array($previousMonth, $months, true) ? $previousMonth : ($months[0] ?? null);
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $stores
|
|
* @return list<array<string,mixed>>
|
|
*/
|
|
private function storeRows(string $month, array $stores): array
|
|
{
|
|
$aggregates = $this->connection()
|
|
->table('store_receipt_order_match_rows')
|
|
->where('report_month', $month . '-01')
|
|
->whereIn('store_name', $stores)
|
|
->whereNotIn('platform', self::EXCLUDED_PLATFORMS)
|
|
->selectRaw(
|
|
'store_name,
|
|
COUNT(*) as row_count,
|
|
SUM(COALESCE(income_amount, 0)) as income_amount,
|
|
SUM(COALESCE(discount_amount, 0)) as discount_amount,
|
|
SUM(COALESCE(offset_amount, 0)) as offset_amount,
|
|
SUM(COALESCE(refund_amount, 0)) as refund_amount,
|
|
SUM(COALESCE(default_compensation_amount, 0)) as default_compensation_amount,
|
|
SUM(' . self::INCOME_SQL . ') as total_income,
|
|
MAX(imported_at) as imported_at'
|
|
)
|
|
->groupBy('store_name')
|
|
->get()
|
|
->keyBy('store_name');
|
|
|
|
return array_map(static function (string $store) use ($aggregates): array {
|
|
$row = $aggregates->get($store);
|
|
|
|
return [
|
|
'store_name' => $store,
|
|
'row_count' => (int) ($row->row_count ?? 0),
|
|
'income_amount' => round((float) ($row->income_amount ?? 0), 2),
|
|
'discount_amount' => round((float) ($row->discount_amount ?? 0), 2),
|
|
'offset_amount' => round((float) ($row->offset_amount ?? 0), 2),
|
|
'refund_amount' => round((float) ($row->refund_amount ?? 0), 2),
|
|
'default_compensation_amount' => round((float) ($row->default_compensation_amount ?? 0), 2),
|
|
'total_income' => round((float) ($row->total_income ?? 0), 2),
|
|
'imported_at' => $row->imported_at ?? null,
|
|
'has_data' => $row !== null,
|
|
];
|
|
}, $stores);
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $months
|
|
* @param list<string> $stores
|
|
* @return list<array{month:string,label:string,total_income:float,row_count:int}>
|
|
*/
|
|
private function monthlyTotals(array $months, array $stores): array
|
|
{
|
|
$totals = [];
|
|
foreach ($months as $month) {
|
|
$row = $this->connection()
|
|
->table('store_receipt_order_match_rows')
|
|
->where('report_month', $month . '-01')
|
|
->whereIn('store_name', $stores)
|
|
->whereNotIn('platform', self::EXCLUDED_PLATFORMS)
|
|
->selectRaw(
|
|
'COUNT(*) as row_count,
|
|
SUM(' . self::INCOME_SQL . ') as total_income'
|
|
)
|
|
->first();
|
|
|
|
$totals[] = [
|
|
'month' => $month,
|
|
'label' => $this->monthLabel($month),
|
|
'total_income' => round((float) ($row->total_income ?? 0), 2),
|
|
'row_count' => (int) ($row->row_count ?? 0),
|
|
];
|
|
}
|
|
|
|
return $totals;
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string,mixed>> $rows
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function summary(array $rows): array
|
|
{
|
|
$summary = [
|
|
'row_count' => 0,
|
|
'stores_with_data' => 0,
|
|
'income_amount' => 0.0,
|
|
'discount_amount' => 0.0,
|
|
'offset_amount' => 0.0,
|
|
'refund_amount' => 0.0,
|
|
'default_compensation_amount' => 0.0,
|
|
'total_income' => 0.0,
|
|
'imported_at' => null,
|
|
];
|
|
|
|
foreach ($rows as $row) {
|
|
$summary['row_count'] += (int) $row['row_count'];
|
|
$summary['stores_with_data'] += $row['has_data'] ? 1 : 0;
|
|
foreach ([
|
|
'income_amount',
|
|
'discount_amount',
|
|
'offset_amount',
|
|
'refund_amount',
|
|
'default_compensation_amount',
|
|
'total_income',
|
|
] as $field) {
|
|
$summary[$field] += (float) $row[$field];
|
|
}
|
|
|
|
if ($row['imported_at'] !== null
|
|
&& ($summary['imported_at'] === null || $row['imported_at'] > $summary['imported_at'])) {
|
|
$summary['imported_at'] = $row['imported_at'];
|
|
}
|
|
}
|
|
|
|
foreach ([
|
|
'income_amount',
|
|
'discount_amount',
|
|
'offset_amount',
|
|
'refund_amount',
|
|
'default_compensation_amount',
|
|
'total_income',
|
|
] as $field) {
|
|
$summary[$field] = round((float) $summary[$field], 2);
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $stores
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function emptyResult(array $stores, string $error): array
|
|
{
|
|
return [
|
|
'stores' => array_map(static fn (string $store): array => [
|
|
'store_name' => $store,
|
|
'row_count' => 0,
|
|
'income_amount' => 0.0,
|
|
'discount_amount' => 0.0,
|
|
'offset_amount' => 0.0,
|
|
'refund_amount' => 0.0,
|
|
'default_compensation_amount' => 0.0,
|
|
'total_income' => 0.0,
|
|
'imported_at' => null,
|
|
'has_data' => false,
|
|
], $stores),
|
|
'months' => [],
|
|
'month' => null,
|
|
'month_label' => '—',
|
|
'summary' => [
|
|
'row_count' => 0,
|
|
'stores_with_data' => 0,
|
|
'income_amount' => 0.0,
|
|
'discount_amount' => 0.0,
|
|
'offset_amount' => 0.0,
|
|
'refund_amount' => 0.0,
|
|
'default_compensation_amount' => 0.0,
|
|
'total_income' => 0.0,
|
|
'imported_at' => null,
|
|
],
|
|
'monthly_totals' => [],
|
|
'error' => $error,
|
|
];
|
|
}
|
|
|
|
private function connection()
|
|
{
|
|
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
}
|
|
|
|
private function monthLabel(string $month): string
|
|
{
|
|
return substr($month, 0, 4) . '年' . (int) substr($month, 5, 2) . '月';
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $stores
|
|
*/
|
|
private function cacheKey(string $scope, string $period = '', array $stores = []): string
|
|
{
|
|
return 'kuka-posting-performance-v1:' . $scope . ':' . md5(json_encode([
|
|
$period,
|
|
$stores,
|
|
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
|
|
}
|
|
}
|