104 lines
5.4 KiB
PHP
104 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Repositories;
|
|
|
|
use Illuminate\Database\DatabaseManager;
|
|
use Illuminate\Database\Query\Builder;
|
|
use RuntimeException;
|
|
|
|
class StoreReceivingJdWalletBillRepository
|
|
{
|
|
public function __construct(private readonly DatabaseManager $databases) {}
|
|
|
|
public function paginate(array $filters, string $sort, string $direction, int $page, int $perPage): array
|
|
{
|
|
$periods = $this->periods();
|
|
$query = $this->query($filters, $periods);
|
|
$summary = (clone $query)->selectRaw('COUNT(*) AS count, COALESCE(SUM(income), 0) AS income, COALESCE(SUM(expense), 0) AS expense')->first();
|
|
$total = (int) $summary->count;
|
|
$page = max(1, $page);
|
|
$perPage = min(100, max(1, $perPage));
|
|
// Skip deep pages using narrow indexed keys, then load JSON only for the visible rows.
|
|
$keys = $this->ordered($query, $sort, $direction)->forPage($page, $perPage)->get(['generation', 'record_code']);
|
|
$rows = [];
|
|
foreach ($keys->groupBy('generation') as $generation => $group) {
|
|
$records = $this->db()->table('jd_wallet_bill_rows')->where('generation', $generation)
|
|
->whereIn('record_code', $group->pluck('record_code')->all())->get(['record_code', 'row_data']);
|
|
foreach ($records as $record) $rows[$generation . ':' . $record->record_code] = json_decode($record->row_data, true, 512, JSON_THROW_ON_ERROR);
|
|
}
|
|
$balance = $this->latestBalanceTotal($filters, $periods);
|
|
return [
|
|
'data' => $keys->map(fn ($key) => $rows[$key->generation . ':' . $key->record_code])->all(),
|
|
'total' => $total, 'current_page' => $page, 'last_page' => max(1, (int) ceil($total / $perPage)), 'per_page' => $perPage,
|
|
'summary' => ['count' => $total, 'income' => round((float) $summary->income, 2), 'expense' => round((float) $summary->expense, 2),
|
|
'balance' => $balance],
|
|
'sync' => ['status' => 'imported', 'source' => '胜算原始导出',
|
|
'last_succeeded_at' => $periods->max('imported_at'),
|
|
'periods' => $periods->map(fn ($p) => substr($p->period_start, 0, 7))->values()->all()],
|
|
];
|
|
}
|
|
|
|
public function exportRows(array $filters, string $sort, string $direction): iterable
|
|
{
|
|
$query = $this->ordered($this->query($filters, $this->periods()), $sort, $direction);
|
|
return $this->stream($query);
|
|
}
|
|
|
|
private function stream(Builder $query): iterable
|
|
{
|
|
foreach ($query->select('row_data')->cursor() as $row) yield json_decode($row->row_data, true, 512, JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
public function filterOptions(): array
|
|
{
|
|
$q = $this->query([], $this->periods());
|
|
return [
|
|
'shops' => (clone $q)->distinct()->orderBy('store_name')->pluck('store_name')->all(),
|
|
'bill_items' => (clone $q)->distinct()->orderBy('bill_item')->pluck('bill_item')->all(),
|
|
];
|
|
}
|
|
|
|
private function periods(): \Illuminate\Support\Collection
|
|
{
|
|
if (! $this->db()->getSchemaBuilder()->hasTable('jd_wallet_bill_periods')) throw new RuntimeException('jd_wallet_bill_mirror_not_ready');
|
|
$periods = $this->db()->table('jd_wallet_bill_periods')->orderBy('period_start')->get();
|
|
if ($periods->isEmpty()) throw new RuntimeException('jd_wallet_bill_mirror_not_ready');
|
|
return $periods;
|
|
}
|
|
|
|
private function query(array $filters, \Illuminate\Support\Collection $periods): Builder
|
|
{
|
|
$q = $this->db()->table('jd_wallet_bill_rows')->whereIn('generation', $periods->pluck('active_generation')->all());
|
|
foreach (['store_name', 'bill_item'] as $field) {
|
|
if (($filters[$field] ?? '') !== '') $q->where($field, $filters[$field]);
|
|
}
|
|
if (($filters['start'] ?? '') !== '') $q->where('occurred_at', '>=', $filters['start'] . ' 00:00:00');
|
|
if (($filters['end'] ?? '') !== '') $q->where('occurred_at', '<', date('Y-m-d', strtotime($filters['end'] . ' +1 day')) . ' 00:00:00');
|
|
if (($filters['keyword'] ?? '') !== '') {
|
|
$like = '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $filters['keyword']) . '%';
|
|
$q->where(fn (Builder $sub) => $sub->where('record_code', 'like', $like)->orWhere('order_no', 'like', $like)->orWhere('row_data', 'like', $like));
|
|
}
|
|
return $q;
|
|
}
|
|
|
|
private function ordered(Builder $q, string $sort, string $direction): Builder
|
|
{
|
|
$sort = in_array($sort, ['occurred_at', 'record_code', 'income', 'expense', 'balance'], true) ? $sort : 'occurred_at';
|
|
$direction = $direction === 'asc' ? 'asc' : 'desc';
|
|
return $q->orderBy($sort, $direction)->orderBy('record_code', $direction);
|
|
}
|
|
|
|
private function latestBalanceTotal(array $filters, \Illuminate\Support\Collection $periods): float
|
|
{
|
|
// Balances are snapshots: sum only the latest row per shop/account within the selected results.
|
|
$ranked = $this->query($filters, $periods)->select('balance')
|
|
->selectRaw('ROW_NUMBER() OVER (PARTITION BY store_name, account_name ORDER BY occurred_at DESC, LENGTH(record_code) DESC, record_code DESC) AS balance_rank');
|
|
return round((float) $this->db()->query()->fromSub($ranked, 'latest')->where('balance_rank', 1)->sum('balance'), 2);
|
|
}
|
|
|
|
private function db(): \Illuminate\Database\Connection
|
|
{
|
|
return $this->databases->connection(config('upstream_mirrors.connection', 'business_mirror'));
|
|
}
|
|
}
|