CaiWuXiTong/modules/EcommerceFinance/Repositories/JdDailyBillRepository.php
qiuxiang 7175d3f73e 店铺收款账户账单:所有页签新增「总账备注」筛选(页面与导出同口径)
- AccountLedgerRemarkMap:新增 页签→平台→账单项目 的翻译(remarksForTab / billItemGroupsForTab / billItemsForTab);淘系支付宝、淘系微信同页签混有多个平台,按店铺名前缀再分平台
- LedgerRemarkFilter(新):把「总账备注」翻译成 SQL 条件,单平台页签 whereIn(账单项目),多平台页签 (店铺前缀 AND 账单项目 IN ...) OR ...
- 控制器/仓库:AccountBills、AlipayBills、Pinduoduo/Dewu/TaobaoWechat 账户账单 + 对应仓库(含抖音权威、京东日账单、京东钱包、共享原始账单镜像)全部接入 ledger_remark 筛选;filter-options 统一返回 ledger_remarks 供下拉
- 前端:AccountBills.vue / TaobaoAlipayBills.vue 在「店铺」右侧新增「总账备注」下拉,参与查询、重置与导出
- 测试:新增映射翻译、SQL 条件、唯品会JX 页面+导出筛选用例;同步 filter-options 断言
2026-10-06 18:46:16 +08:00

136 lines
6.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Repositories;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Query\Builder;
use Modules\EcommerceFinance\JdBillPlatform;
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
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]);
}
LedgerRemarkFilter::itemsWhereIn($q, (array) ($filters['bill_items'] ?? []), 'bill_item');
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'));
}
}