181 lines
9.3 KiB
PHP
181 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Repositories;
|
|
|
|
use Illuminate\Database\DatabaseManager;
|
|
use Illuminate\Database\Query\Builder;
|
|
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
|
use RuntimeException;
|
|
|
|
class DouyinAuthoritativeBillRepository
|
|
{
|
|
public function __construct(
|
|
private readonly DatabaseManager $databases,
|
|
private readonly GenericBusinessMirrorRepository $mirror,
|
|
private readonly BusinessMirrorDatasetRegistry $datasets,
|
|
private readonly StoreReceivingAlipayBillMirrorRepository $source,
|
|
) {}
|
|
|
|
public function available(): bool
|
|
{
|
|
return $this->db()->getSchemaBuilder()->hasTable('douyin_authoritative_periods')
|
|
&& $this->db()->table('douyin_authoritative_periods')->exists();
|
|
}
|
|
|
|
public function paginate(array $filters, string $sort, string $direction, int $page, int $perPage): array
|
|
{
|
|
$query = $this->query($filters);
|
|
$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));
|
|
$rows = $this->ordered($query, $sort, $direction)->forPage($page, $perPage)->get()->all();
|
|
$income = round((float) $summary->income, 2);
|
|
$expense = round((float) $summary->expense, 2);
|
|
$state = $this->mirror->state($this->datasets->get(StoreReceivingAlipayBillMirrorRepository::DATASET)) ?? [];
|
|
return [
|
|
'data' => $this->mapRows($rows), 'total' => $total, 'current_page' => $page,
|
|
'last_page' => max(1, (int) ceil($total / $perPage)), 'per_page' => $perPage,
|
|
'summary' => ['count' => $total, 'income' => $income, 'expense' => $expense, 'balance' => round($income - $expense, 2)],
|
|
'sync' => ['status' => $state['status'] ?? 'unknown', 'last_succeeded_at' => $state['last_succeeded_at'] ?? null],
|
|
];
|
|
}
|
|
|
|
public function exportRows(array $filters, string $sort, string $direction): iterable
|
|
{
|
|
$query = $this->ordered($this->query($filters), $sort, $direction);
|
|
// Freeze the selected generations in query() so activation cannot mix versions during export.
|
|
return $this->batches($query);
|
|
}
|
|
|
|
private function batches(Builder $query): iterable
|
|
{
|
|
$batch = [];
|
|
foreach ($query->cursor() as $row) {
|
|
$batch[] = $row;
|
|
if (count($batch) === 500) {
|
|
yield from $this->mapRows($batch);
|
|
$batch = [];
|
|
}
|
|
}
|
|
if ($batch !== []) yield from $this->mapRows($batch);
|
|
}
|
|
|
|
public function filterOptions(): array
|
|
{
|
|
$query = $this->query([], true);
|
|
return [
|
|
'shops' => (clone $query)->whereNotNull('store_name')->where('store_name', '<>', '')->distinct()->orderBy('store_name')->pluck('store_name')->all(),
|
|
'bill_items' => (clone $query)->whereNotNull('bill_item')->where('bill_item', '<>', '')->distinct()->orderBy('bill_item')->pluck('bill_item')->all(),
|
|
];
|
|
}
|
|
|
|
private function query(array $filters, bool $withOptions = false): Builder
|
|
{
|
|
$dataset = $this->datasets->get(StoreReceivingAlipayBillMirrorRepository::DATASET);
|
|
if (! $this->mirror->ready($dataset) || ! $this->mirror->ready($this->datasets->get(StoreReceivingAlipayBillMirrorRepository::SHOP_DATASET))) {
|
|
throw new RuntimeException('store_receiving_alipay_bill_mirror_not_ready');
|
|
}
|
|
$db = $this->db();
|
|
$periods = $db->table('douyin_authoritative_periods')->get();
|
|
$legacy = $db->table($dataset->targetTable() . ' as b')
|
|
->where('b.channel', StoreReceivingDouyinBillMirrorRepository::SOURCE_CHANNEL);
|
|
if ($withOptions) {
|
|
$shops = $db->table('ops_jst_shop_info')->select('shop_id')->selectRaw('MAX(shop_name) AS shop_name')->groupBy('shop_id');
|
|
$legacy->leftJoinSub($shops, 's', fn ($join) => $join->on('b.shop_id', '=', 's.shop_id'));
|
|
}
|
|
foreach ($periods as $period) {
|
|
$legacy->where(fn (Builder $q) => $q->where('b.fee_time', '<', $period->period_start)
|
|
->orWhere('b.fee_time', '>=', $period->period_end)->orWhereNull('b.fee_time'));
|
|
}
|
|
$legacy->selectRaw("CAST(b.Id AS CHAR) AS record_code, b.fee_time AS occurred_at,
|
|
CASE WHEN b.inout_type = '1' THEN ABS(COALESCE(b.amount, 0)) ELSE 0 END AS income,
|
|
CASE WHEN b.inout_type = '-1' THEN ABS(COALESCE(b.amount, 0)) ELSE 0 END AS expense,
|
|
CASE WHEN b.inout_type = '1' THEN ABS(COALESCE(b.amount, 0)) WHEN b.inout_type = '-1' THEN -ABS(COALESCE(b.amount, 0)) ELSE 0 END AS balance,
|
|
b.Id AS source_id, NULL AS generation");
|
|
$anchor = $db->table('douyin_authoritative_bills')
|
|
->whereIn('generation', $periods->pluck('active_generation')->all())
|
|
->select(['record_code', 'occurred_at', 'income', 'expense', 'balance'])
|
|
->selectRaw('NULL AS source_id, generation');
|
|
if ($withOptions) {
|
|
$legacy->addSelect(['s.shop_name as store_name', 'b.fee_name as bill_item']);
|
|
$anchor->addSelect(['store_name', 'bill_item']);
|
|
}
|
|
// Apply selective filters before UNION ALL to avoid materializing the full account for a monthly view.
|
|
$this->filters($legacy, $filters, true);
|
|
$this->filters($anchor, $filters, false);
|
|
return $db->query()->fromSub($legacy->unionAll($anchor), 'bills');
|
|
}
|
|
|
|
private function filters(Builder $query, array $filters, bool $legacy): void
|
|
{
|
|
$date = $legacy ? 'b.fee_time' : 'occurred_at';
|
|
$item = $legacy ? 'b.fee_name' : 'bill_item';
|
|
if (($filters['store_name'] ?? '') !== '') {
|
|
$value = '%' . $this->escape($filters['store_name']) . '%';
|
|
if ($legacy) {
|
|
$ids = $this->db()->table('ops_jst_shop_info')->where('shop_name', 'like', $value)->pluck('shop_id')->all();
|
|
$query->whereIn('b.shop_id', $ids);
|
|
} else {
|
|
$query->where('store_name', 'like', $value);
|
|
}
|
|
}
|
|
if (($filters['bill_item'] ?? '') !== '') $query->where($item, 'like', '%' . $this->escape($filters['bill_item']) . '%');
|
|
if (($filters['start'] ?? '') !== '') $query->where($date, '>=', $filters['start'] . ' 00:00:00');
|
|
if (($filters['end'] ?? '') !== '') $query->where($date, '<', date('Y-m-d', strtotime($filters['end'] . ' +1 day')) . ' 00:00:00');
|
|
if (($filters['keyword'] ?? '') !== '') {
|
|
if (($filters['keyword_match'] ?? '') === 'exact-record') {
|
|
$keyword = (string) $filters['keyword'];
|
|
if ($legacy && (! preg_match('/^(0|[1-9]\d{0,9})$/', $keyword) || (float) $keyword > 2147483647)) {
|
|
$query->whereRaw('1 = 0');
|
|
} else {
|
|
$query->where($legacy ? 'b.Id' : 'record_code', $keyword);
|
|
}
|
|
return;
|
|
}
|
|
$value = '%' . $this->escape($filters['keyword']) . '%';
|
|
$columns = $legacy ? ['b.Id', 'b.auto_id', 'b.fee_name', 'b.primary_key', 'b.p_auto_id', 'b.so_id', 'b.sku_id', 'b.shop_sku_id', 'b.shop_i_id', 'b.channel']
|
|
: ['record_code', 'order_no', 'bill_item', 'row_data'];
|
|
$query->where(function (Builder $q) use ($value, $columns): void {
|
|
foreach ($columns as $column) $q->orWhere($column, 'like', $value);
|
|
});
|
|
}
|
|
}
|
|
|
|
private function ordered(Builder $query, string $sort, string $direction): Builder
|
|
{
|
|
$sort = in_array($sort, ['record_code', 'occurred_at', 'income', 'expense', 'balance'], true) ? $sort : 'occurred_at';
|
|
$direction = $direction === 'asc' ? 'asc' : 'desc';
|
|
return $query->orderBy($sort, $direction)->orderBy('record_code', $direction);
|
|
}
|
|
|
|
private function mapRows(array $rows): array
|
|
{
|
|
$ids = array_values(array_filter(array_map(fn ($r) => $r->source_id, $rows), fn ($id) => $id !== null));
|
|
$legacy = [];
|
|
if ($ids !== []) {
|
|
$raw = $this->db()->table($this->datasets->get(StoreReceivingAlipayBillMirrorRepository::DATASET)->targetTable())->whereIn('Id', $ids)->get()->all();
|
|
foreach ($this->source->mapDouyinRows($raw) as $row) $legacy[$row['id']] = $row;
|
|
}
|
|
$authoritative = [];
|
|
foreach (collect($rows)->filter(fn ($row) => $row->source_id === null)->groupBy('generation') as $generation => $group) {
|
|
$data = $this->db()->table('douyin_authoritative_bills')->where('generation', $generation)
|
|
->whereIn('record_code', $group->pluck('record_code')->all())->get(['record_code', 'row_data']);
|
|
foreach ($data as $row) $authoritative[$generation . ':' . $row->record_code] = json_decode($row->row_data, true, 512, JSON_THROW_ON_ERROR);
|
|
}
|
|
return array_map(fn ($row) => $row->source_id === null
|
|
? $authoritative[$row->generation . ':' . $row->record_code]
|
|
: $legacy[(string) $row->source_id], $rows);
|
|
}
|
|
|
|
private function db(): \Illuminate\Database\Connection
|
|
{
|
|
return $this->databases->connection(config('upstream_mirrors.connection', 'business_mirror'));
|
|
}
|
|
|
|
private function escape(string $value): string
|
|
{
|
|
return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
|
|
}
|
|
}
|