在京东账单右侧新增京喜账单页签,复用京东日账单的取数、列定义、筛选、排序、分页与导出,只增加平台维度过滤:京东账单排除京喜店铺,京喜账单只取京喜店铺。平台口径集中在 config/jd_bill_platform.php 与 JdBillPlatform(含口径切换起始日期参数,默认全量历史),仓库通过 forPlatform() 收窄,两个口径互补、总额守恒,底层镜像数据不修改不删除。真实镜像实测:京喜 488,464 笔 + 京东 210,983 笔 = 拆分前 699,447 笔,收入/支出/差额逐项误差为 0。
134 lines
6.3 KiB
PHP
134 lines
6.3 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Illuminate\Database\DatabaseManager;
|
||
use Illuminate\Database\Query\Builder;
|
||
use Modules\EcommerceFinance\JdBillPlatform;
|
||
use RuntimeException;
|
||
|
||
class JdDailyBillRepository
|
||
{
|
||
/** 平台口径(JdBillPlatform::platformForBillType);null = 不做平台拆分,保持原行为。 */
|
||
private ?string $platform = null;
|
||
|
||
public function __construct(private readonly DatabaseManager $databases) {}
|
||
|
||
/** 京东账单/京喜账单共用本仓库,用平台口径区分数据范围。 */
|
||
public function forPlatform(?string $platform): static
|
||
{
|
||
$clone = clone $this;
|
||
$clone->platform = $platform;
|
||
|
||
return $clone;
|
||
}
|
||
|
||
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_daily_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);
|
||
}
|
||
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' => round((float) $summary->income - (float) $summary->expense, 2)],
|
||
'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_daily_bill_periods')) throw new RuntimeException('jd_daily_bill_mirror_not_ready');
|
||
$periods = $this->db()->table('jd_daily_bill_periods')->orderBy('period_start')->get();
|
||
if ($periods->isEmpty()) throw new RuntimeException('jd_daily_bill_mirror_not_ready');
|
||
return $periods;
|
||
}
|
||
|
||
private function query(array $filters, \Illuminate\Support\Collection $periods): Builder
|
||
{
|
||
$q = $this->db()->table('jd_daily_bill_rows')->whereIn('generation', $periods->pluck('active_generation')->all());
|
||
$this->applyPlatformScope($q);
|
||
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 applyPlatformScope(Builder $query): void
|
||
{
|
||
$scope = JdBillPlatform::storeNameScope($this->platform);
|
||
if ($scope === null) {
|
||
return;
|
||
}
|
||
|
||
$like = '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $scope['keyword']) . '%';
|
||
$start = $scope['start_date'];
|
||
|
||
if ($scope['mode'] === 'only') {
|
||
$query->where('store_name', 'like', $like);
|
||
if ($start !== null) $query->where('occurred_at', '>=', $start . ' 00:00:00');
|
||
|
||
return;
|
||
}
|
||
|
||
// 起始日期之前取到的京喜行仍留在京东账单,保证两边相加等于拆分前。
|
||
$query->where(function (Builder $sub) use ($like, $start): void {
|
||
$sub->where('store_name', 'not like', $like);
|
||
if ($start !== null) $sub->orWhere('occurred_at', '<', $start . ' 00:00:00');
|
||
});
|
||
}
|
||
|
||
private function ordered(Builder $q, string $sort, string $direction): Builder
|
||
{
|
||
$sort = in_array($sort, ['occurred_at', 'record_code', 'income', 'expense'], true) ? $sort : 'occurred_at';
|
||
$direction = $direction === 'asc' ? 'asc' : 'desc';
|
||
return $q->orderBy($sort, $direction)->orderBy('record_code', $direction);
|
||
}
|
||
|
||
private function db(): \Illuminate\Database\Connection
|
||
{
|
||
return $this->databases->connection(config('upstream_mirrors.connection', 'business_mirror'));
|
||
}
|
||
}
|