query = $factory->forSource(self::SOURCE); } public function usesBusinessMirror(string $ledger): bool { $dataset = $ledger === 'consumption' ? MarketingAccountBillMirrorRepository::JD_CONSUMPTION : MarketingAccountBillMirrorRepository::JD_RECHARGE; return $this->mirror?->ready($dataset) ?? false; } public function usesBusinessMirrorForOptions(): bool { return $this->usesBusinessMirror('recharge') && $this->usesBusinessMirror('consumption'); } /** @return string[] */ public function shopOptions(): array { if ($this->usesBusinessMirrorForOptions()) { return $this->mirror->jdShopOptions(); } $rows = $this->query->select( "SELECT TOP (500) shopName FROM (SELECT shopName FROM " . self::RECHARGE_TABLE . " UNION SELECT shopName FROM " . self::CONSUMPTION_TABLE . ") shops WHERE shopName IS NOT NULL AND LTRIM(RTRIM(shopName)) <> '' ORDER BY shopName" ); return array_values(array_map( static fn (array $row): string => (string) ($row['shopName'] ?? ''), $rows, )); } /** @return array{rows: array>, total: int} */ public function paginateRecharge( ?string $shopName = null, ?string $fromDate = null, ?string $toDate = null, int $page = 1, int $pageSize = 25, ): array { if ($this->usesBusinessMirror('recharge')) { $result = $this->mirror->paginateJdRecharge( $shopName, $fromDate, $toDate, $page, $pageSize, ); return [ 'rows' => array_map(fn (array $row): array => $this->normalizeRecharge($row), $result['rows']), 'total' => $result['total'], ]; } return $this->paginate(self::RECHARGE_TABLE, $this->rechargeWhere($shopName, $fromDate, $toDate), $page, $pageSize, [ 'id', 'shopName', 'amount', 'balanceAmount', 'orderTypeName', 'pk', 'createDate', 'updateTime', 'statusName', 'remark', ], 'createDate', 'id', fn (array $row): array => $this->normalizeRecharge($row)); } /** @return array{rows: array>, total: int} */ public function paginateConsumption( ?string $shopName = null, ?string $fromDate = null, ?string $toDate = null, int $page = 1, int $pageSize = 25, ): array { if ($this->usesBusinessMirror('consumption')) { $result = $this->mirror->paginateJdConsumption( $shopName, $fromDate, $toDate, $page, $pageSize, ); return [ 'rows' => array_map(fn (array $row): array => $this->normalizeConsumption($row), $result['rows']), 'total' => $result['total'], ]; } return $this->paginate(self::CONSUMPTION_TABLE, $this->consumptionWhere($shopName, $fromDate, $toDate), $page, $pageSize, [ 'id', 'shopName', 'amount', 'balanceAmount', 'orderTypeName', 'creatTime', 'settleTime', 'pk', 'invoiceApplyTypeName', ], 'creatTime', 'id', fn (array $row): array => $this->normalizeConsumption($row)); } /** @return \Generator> */ public function exportRows( string $ledger, ?string $shopName = null, ?string $fromDate = null, ?string $toDate = null, int $batchSize = 100, ): \Generator { $ledger = $ledger === 'consumption' ? 'consumption' : 'recharge'; if (! $this->usesBusinessMirror($ledger)) { throw new \RuntimeException('京东账单共享业务镜像尚未完成同步。'); } $batchSize = max(1, min(100, $batchSize)); $page = 1; $exported = 0; do { $result = $ledger === 'consumption' ? $this->mirror->paginateJdConsumption($shopName, $fromDate, $toDate, $page, $batchSize) : $this->mirror->paginateJdRecharge($shopName, $fromDate, $toDate, $page, $batchSize); $rows = is_array($result['rows'] ?? null) ? $result['rows'] : []; $total = max(0, (int) ($result['total'] ?? 0)); foreach ($rows as $row) { yield $ledger === 'consumption' ? $this->normalizeConsumption($row) : $this->normalizeRecharge($row); } $count = count($rows); $exported += $count; $page++; } while ($count > 0 && $exported < $total); } /** @return array{amount_total: float, balance_total: float, count: int} */ public function summaryRecharge(?string $shopName = null, ?string $fromDate = null, ?string $toDate = null): array { if ($this->usesBusinessMirror('recharge')) { return $this->normalizeSummary($this->mirror->summarizeJd( MarketingAccountBillMirrorRepository::JD_RECHARGE, 'createDate', $shopName, $fromDate, $toDate, )); } return $this->summary(self::RECHARGE_TABLE, $this->rechargeWhere($shopName, $fromDate, $toDate)); } /** @return array{amount_total: float, balance_total: float, count: int} */ public function summaryConsumption(?string $shopName = null, ?string $fromDate = null, ?string $toDate = null): array { if ($this->usesBusinessMirror('consumption')) { return $this->normalizeSummary($this->mirror->summarizeJd( MarketingAccountBillMirrorRepository::JD_CONSUMPTION, 'creatTime', $shopName, $fromDate, $toDate, )); } return $this->summary(self::CONSUMPTION_TABLE, $this->consumptionWhere($shopName, $fromDate, $toDate)); } /** @param array{sql:string,bindings:array} $where */ private function paginate(string $table, array $where, int $page, int $pageSize, array $fields, string $dateField, string $idField, callable $normalize): array { $page = max(1, $page); $pageSize = min(max(1, $pageSize), 100); $total = (int) $this->query->scalar('SELECT COUNT(*) FROM ' . $table . ' src' . $where['sql'], $where['bindings']); if ($total === 0) { return ['rows' => [], 'total' => 0]; } $start = (($page - 1) * $pageSize) + 1; $end = $page * $pageSize; $select = implode(', ', array_map(static fn (string $field): string => 'src.' . $field, $fields)); $sql = 'SELECT * FROM (' . 'SELECT ' . $select . ', ROW_NUMBER() OVER (ORDER BY src.' . $dateField . ' DESC, src.' . $idField . ' DESC) AS rn ' . 'FROM ' . $table . ' src' . $where['sql'] . ') AS t WHERE t.rn BETWEEN ? AND ? ORDER BY t.rn'; $rows = $this->query->select($sql, array_merge($where['bindings'], [$start, $end])); return ['rows' => array_map($normalize, $rows), 'total' => $total]; } /** @param array{sql:string,bindings:array} $where */ private function summary(string $table, array $where): array { $row = $this->query->first( 'SELECT COALESCE(SUM(ABS(amount)), 0) AS amount_total, COALESCE(SUM(balanceAmount), 0) AS balance_total, COUNT(*) AS cnt FROM ' . $table . ' src' . $where['sql'], $where['bindings'], ) ?? []; return $this->normalizeSummary($row); } /** @return array{amount_total: float, balance_total: float, count: int} */ private function normalizeSummary(array $row): array { return [ 'amount_total' => round((float) ($row['amount_total'] ?? 0), 2), 'balance_total' => round((float) ($row['balance_total'] ?? 0), 2), 'count' => (int) ($row['cnt'] ?? 0), ]; } /** @return array{sql:string,bindings:array} */ private function rechargeWhere(?string $shopName, ?string $fromDate, ?string $toDate): array { return $this->dateWhere('src.createDate', $shopName, $fromDate, $toDate); } /** @return array{sql:string,bindings:array} */ private function consumptionWhere(?string $shopName, ?string $fromDate, ?string $toDate): array { return $this->dateWhere('src.creatTime', $shopName, $fromDate, $toDate); } /** @return array{sql:string,bindings:array} */ private function dateWhere(string $dateField, ?string $shopName, ?string $fromDate, ?string $toDate): array { $conditions = []; $bindings = []; if ($shopName !== null && $shopName !== '') { $conditions[] = 'src.shopName = ?'; $bindings[] = $shopName; } if ($fromDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate) === 1) { $conditions[] = $dateField . ' >= ?'; $bindings[] = $fromDate . ' 00:00:00'; } if ($toDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate) === 1) { $conditions[] = $dateField . ' < DATEADD(day, 1, CAST(? AS date))'; $bindings[] = $toDate; } return ['sql' => $conditions === [] ? '' : ' WHERE ' . implode(' AND ', $conditions), 'bindings' => $bindings]; } private function normalizeRecharge(array $row): array { return [ 'id' => (string) ($row['id'] ?? $row['pk'] ?? ''), 'store_name' => (string) ($row['shopName'] ?? ''), 'amount' => round((float) ($row['amount'] ?? 0), 2), 'balance' => round((float) ($row['balanceAmount'] ?? 0), 2), 'transaction_type' => (string) ($row['orderTypeName'] ?? ''), 'order_number' => (string) ($row['pk'] ?? ''), 'recharge_time' => (string) ($row['createDate'] ?? ''), 'arrival_time' => (string) ($row['updateTime'] ?? ''), 'serial_number' => (string) ($row['id'] ?? $row['pk'] ?? ''), 'status' => (string) ($row['statusName'] ?? ''), 'notes' => (string) ($row['remark'] ?? ''), ]; } private function normalizeConsumption(array $row): array { $amount = abs((float) ($row['amount'] ?? 0)); return [ 'id' => (string) ($row['id'] ?? $row['pk'] ?? ''), 'store_name' => (string) ($row['shopName'] ?? ''), 'amount' => round($amount, 2), 'balance' => round((float) ($row['balanceAmount'] ?? 0), 2), 'transaction_type' => (string) ($row['orderTypeName'] ?? ''), 'investment_date' => (string) ($row['creatTime'] ?? ''), 'settlement_date' => (string) ($row['settleTime'] ?? ''), 'serial_number' => (string) ($row['pk'] ?? $row['id'] ?? ''), 'notes' => (string) ($row['invoiceApplyTypeName'] ?? ''), ]; } }