510 lines
22 KiB
PHP
510 lines
22 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Repositories;
|
|
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
|
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* 七个营销账户平台的统一推广费收支明细。
|
|
*
|
|
* 所有平台先按交易日期做 COUNT/SUM 直方图确定全局分页窗口,再只读取当前页
|
|
* 涉及的日期。这样不需要把单月十几万条拼多多流水一次性拉进网页进程。
|
|
* 收入与支出分别读取各平台同源的入账/出账字段,余额统一按收入减支出计算。
|
|
*/
|
|
class PromotionMarketingExpenseRepository
|
|
{
|
|
private const CACHE_SECONDS = 120;
|
|
|
|
/** @var array<string,UpstreamReadOnlyQuery> */
|
|
private array $queries = [];
|
|
|
|
public function __construct(private readonly UpstreamReadOnlyQueryFactory $factory)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* rows:array<int,array<string,mixed>>,
|
|
* total:int,
|
|
* current_page:int,
|
|
* last_page:int,
|
|
* per_page:int,
|
|
* summary:array{income:float,expense:float,balance:float,count:int}
|
|
* }
|
|
*/
|
|
public function paginate(array $filters, int $page = 1, int $limit = 25): array
|
|
{
|
|
[$start, $end] = $this->dateRange($filters);
|
|
$store = trim((string) ($filters['store_name'] ?? ''));
|
|
$page = max(1, $page);
|
|
$limit = min(max($limit, 1), 100);
|
|
$cacheKey = 'promotion-marketing-expense:page:' . hash('sha256', json_encode([
|
|
$start, $end, $store, $page, $limit,
|
|
], JSON_UNESCAPED_UNICODE));
|
|
|
|
return Cache::remember($cacheKey, self::CACHE_SECONDS, function () use ($start, $end, $store, $page, $limit): array {
|
|
$histograms = [];
|
|
$countByDate = [];
|
|
$expenseByPlatform = [];
|
|
$total = 0;
|
|
$incomeTotal = 0.0;
|
|
$expenseTotal = 0.0;
|
|
|
|
foreach ($this->sources() as $key => $source) {
|
|
$histograms[$key] = $this->histogram($source, $start, $end, $store);
|
|
foreach ($histograms[$key] as $date => $values) {
|
|
$countByDate[$date] = ($countByDate[$date] ?? 0) + $values['count'];
|
|
$expenseByPlatform[$source['platform_category']] =
|
|
($expenseByPlatform[$source['platform_category']] ?? 0.0) + ($values['expense'] ?? 0.0);
|
|
$total += $values['count'];
|
|
$incomeTotal += $values['income'] ?? 0.0;
|
|
$expenseTotal += $values['expense'] ?? 0.0;
|
|
}
|
|
}
|
|
|
|
krsort($countByDate, SORT_STRING);
|
|
[$dates, $skip] = $this->pageWindow($countByDate, ($page - 1) * $limit, $limit);
|
|
$rows = $dates === [] ? [] : $this->rowsForDates($histograms, $dates, $start, $end, $store);
|
|
usort($rows, static function (array $left, array $right): int {
|
|
foreach ([
|
|
strcmp($right['report_date'], $left['report_date']),
|
|
$left['source_order'] <=> $right['source_order'],
|
|
strcmp($left['store_name'], $right['store_name']),
|
|
strcmp($left['notes'], $right['notes']),
|
|
$right['expense'] <=> $left['expense'],
|
|
] as $comparison) {
|
|
if ($comparison !== 0) {
|
|
return $comparison;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
|
|
$rows = array_slice($rows, $skip, $limit);
|
|
foreach ($rows as $index => &$row) {
|
|
unset($row['report_date'], $row['source'], $row['source_order']);
|
|
$row['id'] = hash('sha256', $index . '|' . json_encode($row, JSON_UNESCAPED_UNICODE));
|
|
}
|
|
unset($row);
|
|
|
|
$platformSummary = [];
|
|
foreach ($expenseByPlatform as $platform => $amount) {
|
|
$platformSummary[] = ['platform' => $platform, 'amount' => round($amount, 2)];
|
|
}
|
|
usort($platformSummary, static fn (array $left, array $right): int =>
|
|
[$right['amount'], $left['platform']] <=> [$left['amount'], $right['platform']]);
|
|
|
|
return [
|
|
'rows' => array_values($rows),
|
|
'total' => $total,
|
|
'current_page' => $page,
|
|
'last_page' => max(1, (int) ceil($total / $limit)),
|
|
'per_page' => $limit,
|
|
'summary' => [
|
|
'income' => round($incomeTotal, 2),
|
|
'expense' => round($expenseTotal, 2),
|
|
'balance' => round($incomeTotal - $expenseTotal, 2),
|
|
'count' => $total,
|
|
],
|
|
'platform_summary' => $platformSummary,
|
|
];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 平台 + 原始店铺的推广费汇总,供弹窗“平台汇总 / 店铺明细”使用。
|
|
*
|
|
* 弹窗内的平台和店铺筛选在已汇总的小结果集上执行,不再重复查询上游。
|
|
*
|
|
* @return array{
|
|
* store_summary:array<int,array{platform:string,store_name:string,amount:float}>,
|
|
* platform_summary:array<int,array{platform:string,amount:float}>,
|
|
* total:float
|
|
* }
|
|
*/
|
|
public function storeSummary(array $filters): array
|
|
{
|
|
[$start, $end] = $this->dateRange($filters);
|
|
$note = trim((string) ($filters['note'] ?? ''));
|
|
$cacheKey = 'promotion-marketing-expense:stores-summary:'
|
|
. hash('sha256', $start . '|' . $end . '|' . $note);
|
|
|
|
return Cache::remember($cacheKey, self::CACHE_SECONDS, function () use ($start, $end, $note): array {
|
|
$merged = [];
|
|
foreach ($this->sources() as $source) {
|
|
[$where, $bindings] = $this->where($source, $start, $end, '', false, $note);
|
|
$sql = 'SELECT ' . $source['store'] . ' AS store_name,'
|
|
. ' COALESCE(SUM(' . $source['expense'] . '), 0) AS amount'
|
|
. ' FROM ' . $source['table'] . ' c ' . $where
|
|
. ' AND ' . $source['store'] . " IS NOT NULL AND LTRIM(RTRIM(" . $source['store'] . ")) <> N''"
|
|
. ' GROUP BY ' . $source['store']
|
|
. ' HAVING ABS(SUM(' . $source['expense'] . ')) > 0.004';
|
|
foreach ($this->select($source['connection'], $sql, $bindings) as $row) {
|
|
$store = $source['store_label'] ?? trim((string) ($row['store_name'] ?? ''));
|
|
if ($store === '') {
|
|
continue;
|
|
}
|
|
$key = $source['platform_category'] . '|' . $store;
|
|
$merged[$key] = ($merged[$key] ?? 0.0) + (float) ($row['amount'] ?? 0);
|
|
}
|
|
}
|
|
|
|
$stores = [];
|
|
$platforms = [];
|
|
$total = 0.0;
|
|
foreach ($merged as $key => $amount) {
|
|
[$platform, $store] = explode('|', $key, 2);
|
|
$amount = round($amount, 2);
|
|
$stores[] = ['platform' => $platform, 'store_name' => $store, 'amount' => $amount];
|
|
$platforms[$platform] = ($platforms[$platform] ?? 0.0) + $amount;
|
|
$total += $amount;
|
|
}
|
|
usort($stores, static fn (array $left, array $right): int =>
|
|
[$right['amount'], $left['platform'], $left['store_name']]
|
|
<=> [$left['amount'], $right['platform'], $right['store_name']]);
|
|
|
|
$platformSummary = [];
|
|
foreach ($platforms as $platform => $amount) {
|
|
$platformSummary[] = ['platform' => $platform, 'amount' => round($amount, 2)];
|
|
}
|
|
usort($platformSummary, static fn (array $left, array $right): int =>
|
|
[$right['amount'], $left['platform']] <=> [$left['amount'], $right['platform']]);
|
|
|
|
return [
|
|
'store_summary' => $stores,
|
|
'platform_summary' => $platformSummary,
|
|
'total' => round($total, 2),
|
|
];
|
|
});
|
|
}
|
|
|
|
/** @return array<int,string> */
|
|
public function storeOptions(array $filters = []): array
|
|
{
|
|
[$start, $end] = $this->dateRange($filters);
|
|
$cacheKey = 'promotion-marketing-expense:stores:' . hash('sha256', $start . '|' . $end);
|
|
|
|
return Cache::remember($cacheKey, 3600, function () use ($start, $end): array {
|
|
$stores = [];
|
|
foreach ($this->sources() as $source) {
|
|
[$where, $bindings] = $this->where($source, $start, $end, '');
|
|
$sql = 'SELECT DISTINCT TOP (500) ' . $source['store'] . ' AS store_name'
|
|
. ' FROM ' . $source['table'] . ' c ' . $where
|
|
. ' AND ' . $source['store'] . " IS NOT NULL AND LTRIM(RTRIM(" . $source['store'] . ")) <> N''"
|
|
. ' ORDER BY store_name';
|
|
foreach ($this->select($source['connection'], $sql, $bindings) as $row) {
|
|
$name = $source['store_label'] ?? trim((string) ($row['store_name'] ?? ''));
|
|
if ($name !== '') {
|
|
$stores[$name] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$names = array_keys($stores);
|
|
sort($names, SORT_NATURAL);
|
|
|
|
return array_slice($names, 0, 500);
|
|
});
|
|
}
|
|
|
|
private function histogram(array $source, string $start, string $end, string $store): array
|
|
{
|
|
[$where, $bindings] = $this->where($source, $start, $end, $store);
|
|
$date = 'CONVERT(varchar(10), ' . $source['date'] . ', 23)';
|
|
$sql = 'SELECT ' . $date . ' AS report_date, COUNT(*) AS row_count,'
|
|
. ' COALESCE(SUM(' . $source['income'] . '), 0) AS income,'
|
|
. ' COALESCE(SUM(' . $source['expense'] . '), 0) AS expense'
|
|
. ' FROM ' . $source['table'] . ' c ' . $where
|
|
. ' GROUP BY ' . $date;
|
|
|
|
$result = [];
|
|
foreach ($this->select($source['connection'], $sql, $bindings) as $row) {
|
|
$dateValue = substr(trim((string) ($row['report_date'] ?? '')), 0, 10);
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateValue) !== 1) {
|
|
continue;
|
|
}
|
|
$result[$dateValue] = [
|
|
'count' => max(0, (int) ($row['row_count'] ?? 0)),
|
|
'income' => round((float) ($row['income'] ?? 0), 2),
|
|
'expense' => round((float) ($row['expense'] ?? 0), 2),
|
|
];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param array<string,array<string,array{count:int,income:float,expense:float}>> $histograms
|
|
* @param array<int,string> $dates
|
|
*/
|
|
private function rowsForDates(array $histograms, array $dates, string $start, string $end, string $store): array
|
|
{
|
|
$first = min($dates);
|
|
$last = max($dates);
|
|
$lastExclusive = CarbonImmutable::parse($last, 'Asia/Shanghai')->addDay()->toDateString();
|
|
$rows = [];
|
|
|
|
foreach ($this->sources() as $key => $source) {
|
|
if (! isset($histograms[$key]) || $histograms[$key] === []) {
|
|
continue;
|
|
}
|
|
[$where, $bindings] = $this->where($source, $first, $lastExclusive, $store, true);
|
|
$sql = 'SELECT ' . $source['row'] . ', '
|
|
. 'CONVERT(varchar(10), ' . $source['date'] . ', 23) AS report_date, '
|
|
. $source['store'] . ' AS store_name, '
|
|
. $source['income'] . ' AS income, '
|
|
. $source['expense'] . ' AS expense'
|
|
. ' FROM ' . $source['table'] . ' c ' . $where;
|
|
foreach ($this->select($source['connection'], $sql, $bindings) as $row) {
|
|
$reportDate = substr(trim((string) ($row['report_date'] ?? '')), 0, 10);
|
|
if (! isset($histograms[$key][$reportDate])) {
|
|
continue;
|
|
}
|
|
$normalized = $this->normalize($key, $source, $row, $reportDate);
|
|
if ($normalized !== null) {
|
|
$rows[] = $normalized;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @param array<string,int> $countByDate
|
|
* @return array{0:array<int,string>,1:int}
|
|
*/
|
|
private function pageWindow(array $countByDate, int $offset, int $limit): array
|
|
{
|
|
$dates = [];
|
|
$skip = 0;
|
|
$cumulative = 0;
|
|
$target = $offset + $limit;
|
|
|
|
foreach ($countByDate as $date => $count) {
|
|
$next = $cumulative + $count;
|
|
if ($next <= $offset) {
|
|
$cumulative = $next;
|
|
continue;
|
|
}
|
|
if ($dates === []) {
|
|
$skip = $offset - $cumulative;
|
|
}
|
|
$dates[] = $date;
|
|
$cumulative = $next;
|
|
if ($cumulative >= $target) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [$dates, max(0, $skip)];
|
|
}
|
|
|
|
private function normalize(string $key, array $source, array $row, string $reportDate): ?array
|
|
{
|
|
$income = max(0.0, round((float) ($row['income'] ?? 0), 2));
|
|
$expense = max(0.0, round((float) ($row['expense'] ?? 0), 2));
|
|
if ($income <= 0 && $expense <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$primary = trim((string) ($row['note_primary'] ?? ''));
|
|
$secondary = trim((string) ($row['note_secondary'] ?? ''));
|
|
if ($key === 'pinduoduo') {
|
|
$secondary = ['1' => '现金', '3' => '红包/券'][$secondary] ?? '';
|
|
}
|
|
$notes = implode(' · ', array_values(array_filter(
|
|
[$primary, $secondary],
|
|
static fn (string $value): bool => $value !== '',
|
|
)));
|
|
|
|
return [
|
|
'source' => $key,
|
|
'source_order' => $source['order'],
|
|
'report_date' => $reportDate,
|
|
'store_name' => $source['store_label'] ?? trim((string) ($row['store_name'] ?? '')),
|
|
'occurred_at' => trim((string) ($row['occurred_at'] ?? '')),
|
|
'notes' => $notes,
|
|
'income' => $income,
|
|
'expense' => $expense,
|
|
'balance' => round($income - $expense, 2),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{0:string,1:array<int,mixed>}
|
|
*/
|
|
private function where(
|
|
array $source,
|
|
string $start,
|
|
string $end,
|
|
string $store,
|
|
bool $endExclusive = false,
|
|
?string $note = null,
|
|
): array {
|
|
$conditions = [$source['movement_condition']];
|
|
$bindings = [$start];
|
|
$conditions[] = $source['date'] . ' >= CAST(? AS date)';
|
|
|
|
if ($endExclusive) {
|
|
$conditions[] = $source['date'] . ' < CAST(? AS date)';
|
|
$bindings[] = $end;
|
|
} else {
|
|
$conditions[] = $source['date'] . ' < DATEADD(day, 1, CAST(? AS date))';
|
|
$bindings[] = $end;
|
|
}
|
|
|
|
if ($store !== '') {
|
|
if (isset($source['store_label'])) {
|
|
if ($store !== $source['store_label']) {
|
|
$conditions[] = '1 = 0';
|
|
}
|
|
} else {
|
|
$conditions[] = $source['store'] . ' = ?';
|
|
$bindings[] = $store;
|
|
}
|
|
}
|
|
if ($note !== null && $note !== '') {
|
|
$conditions[] = '(' . $source['note_filter'] . ") LIKE ? ESCAPE '!'";
|
|
$bindings[] = $this->contains($note);
|
|
}
|
|
|
|
return [' WHERE ' . implode(' AND ', $conditions), $bindings];
|
|
}
|
|
|
|
private function contains(string $value): string
|
|
{
|
|
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], $value) . '%';
|
|
}
|
|
|
|
/** @return array{0:string,1:string} */
|
|
private function dateRange(array $filters): array
|
|
{
|
|
$start = trim((string) ($filters['start'] ?? ''));
|
|
$end = trim((string) ($filters['end'] ?? ''));
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $start) !== 1) {
|
|
$start = now('Asia/Shanghai')->startOfMonth()->toDateString();
|
|
}
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $end) !== 1) {
|
|
$end = now('Asia/Shanghai')->endOfMonth()->toDateString();
|
|
}
|
|
if ($start > $end) {
|
|
throw new RuntimeException('推广费查询的结束日期不能早于开始日期。');
|
|
}
|
|
|
|
return [$start, $end];
|
|
}
|
|
|
|
/** @return array<string,array<string,mixed>> */
|
|
private function sources(): array
|
|
{
|
|
return [
|
|
'taobao' => [
|
|
'order' => 1,
|
|
'connection' => 'jiansun',
|
|
'table' => '[dbo].[tmall_spread_bills]',
|
|
'date' => 'c.[transTime]',
|
|
'store' => 'c.[shopName]',
|
|
'income' => "CASE WHEN c.[finType] = 'DEBIT' THEN CAST(c.[amount] AS decimal(18,2)) / 100.0 ELSE 0 END",
|
|
'expense' => "CASE WHEN c.[finType] = 'CREDIT' THEN CAST(c.[amount] AS decimal(18,2)) / 100.0 ELSE 0 END",
|
|
'movement_condition' => "c.[finType] IN ('DEBIT', 'CREDIT') AND ISNULL(c.[amount], 0) <> 0",
|
|
'note_filter' => 'c.[comment]',
|
|
'platform_category' => '淘系',
|
|
'row' => "CONVERT(varchar(10), c.[transTime], 23) AS occurred_at, c.[comment] AS note_primary, NULL AS note_secondary",
|
|
],
|
|
'alibaba' => [
|
|
'order' => 2,
|
|
'connection' => 'jiansun',
|
|
'table' => '[dbo].[alibaba_bill]',
|
|
'date' => 'CAST(c.[tradeDay] AS date)',
|
|
'store' => 'c.[shopName]',
|
|
'income' => 'CAST(ISNULL(c.[tradeAMTIn], 0) AS decimal(18,2))',
|
|
'expense' => 'CAST(ISNULL(c.[tradeAMTOut], 0) AS decimal(18,2))',
|
|
'movement_condition' => '(ISNULL(c.[tradeAMTIn], 0) <> 0 OR ISNULL(c.[tradeAMTOut], 0) <> 0)',
|
|
'note_filter' => 'c.[tradeTypeName]',
|
|
'platform_category' => '淘系',
|
|
'row' => "CONVERT(varchar(10), CAST(c.[tradeDay] AS date), 23) AS occurred_at, c.[tradeTypeName] AS note_primary, NULL AS note_secondary",
|
|
],
|
|
'pinduoduo' => [
|
|
'order' => 3,
|
|
'connection' => 'jiansun',
|
|
'table' => '[dbo].[pdd_spread_bills]',
|
|
'date' => 'c.[createTime]',
|
|
'store' => 'c.[shopName]',
|
|
'income' => 'CASE WHEN c.[flowType] = 1 THEN CAST(c.[amount] AS decimal(18,2)) ELSE 0 END',
|
|
'expense' => 'CASE WHEN c.[flowType] = 2 THEN CAST(c.[amount] AS decimal(18,2)) ELSE 0 END',
|
|
'movement_condition' => 'c.[flowType] IN (1, 2) AND ISNULL(c.[amount], 0) <> 0',
|
|
'note_filter' => "CONCAT(ISNULL(c.[summary], N''), N' ', CASE c.[toAdvertiserFundType] WHEN 1 THEN N'现金' WHEN 3 THEN N'红包/券' ELSE N'' END)",
|
|
'platform_category' => '拼多多',
|
|
'row' => "CONVERT(varchar(19), c.[createTime], 120) AS occurred_at, c.[summary] AS note_primary, CONVERT(varchar(10), c.[toAdvertiserFundType]) AS note_secondary",
|
|
],
|
|
'jd' => [
|
|
'order' => 4,
|
|
'connection' => 'js_data_middle_platform',
|
|
'table' => '[JSDataMiddlePlatform].[dbo].[jd_jzt_finance_records]',
|
|
'date' => 'CAST(c.[show_date] AS date)',
|
|
'store' => 'c.[shop_name]',
|
|
'income' => "CASE WHEN c.[in_out_type] = 'IN' THEN CAST(c.[amount] AS decimal(18,2)) ELSE 0 END",
|
|
'expense' => "CASE WHEN c.[in_out_type] = 'OUT' THEN CAST(c.[amount] AS decimal(18,2)) ELSE 0 END",
|
|
'movement_condition' => "c.[in_out_type] IN ('IN', 'OUT') AND ISNULL(c.[amount], 0) <> 0",
|
|
'note_filter' => "CONCAT(ISNULL(c.[remark], N''), N' ', ISNULL(c.[trade_type_name], N''))",
|
|
'platform_category' => '京东',
|
|
'row' => "CONVERT(varchar(10), CAST(c.[show_date] AS date), 23) AS occurred_at, c.[remark] AS note_primary, c.[trade_type_name] AS note_secondary",
|
|
],
|
|
'douyin' => [
|
|
'order' => 5,
|
|
'connection' => 'js_data_middle_platform',
|
|
'table' => '[JSDataMiddlePlatform].[dbo].[dy_qianchuan_finance_transactions]',
|
|
'date' => 'c.[statDate]',
|
|
'store' => 'c.[shopName]',
|
|
'income' => 'CAST(ISNULL(c.[income], 0) AS decimal(18,2))',
|
|
'expense' => 'CAST(ISNULL(c.[cost], 0) AS decimal(18,2))',
|
|
'movement_condition' => "c.[rowType] = N'daily' AND c.[status] = N'success' AND (ISNULL(c.[income], 0) <> 0 OR ISNULL(c.[cost], 0) <> 0)",
|
|
'note_filter' => 'c.[remark]',
|
|
'platform_category' => '抖音',
|
|
'row' => "CONVERT(varchar(10), c.[statDate], 23) AS occurred_at, c.[remark] AS note_primary, NULL AS note_secondary",
|
|
],
|
|
'dewu' => [
|
|
'order' => 6,
|
|
'connection' => 'js_data_middle_platform',
|
|
'table' => '[JSDataMiddlePlatform].[dbo].[dw_ad_finance_records]',
|
|
'date' => 'c.[trade_date]',
|
|
'store' => 'c.[shop_name]',
|
|
'income' => "CASE WHEN c.[flow_type] = N'收入' THEN CAST(c.[trade_amount_yuan] AS decimal(18,2)) ELSE 0 END",
|
|
'expense' => "CASE WHEN c.[flow_type] = N'支出' THEN CAST(c.[trade_amount_yuan] AS decimal(18,2)) ELSE 0 END",
|
|
'movement_condition' => "c.[flow_type] IN (N'收入', N'支出') AND ISNULL(c.[trade_amount_yuan], 0) <> 0",
|
|
'note_filter' => "CONCAT(ISNULL(c.[remark], N''), N' ', ISNULL(c.[amount_type], N''))",
|
|
'platform_category' => '得物',
|
|
'row' => "CONVERT(varchar(10), c.[trade_date], 23) AS occurred_at, c.[remark] AS note_primary, c.[amount_type] AS note_secondary",
|
|
],
|
|
'vip' => [
|
|
'order' => 7,
|
|
'connection' => 'js_data_middle_platform',
|
|
'table' => '[JSDataMiddlePlatform].[dbo].[vip_marketingDailyReport]',
|
|
'date' => 'c.[reportDate]',
|
|
'store' => 'c.[accountName]',
|
|
'store_label' => '唯品会JITX_亿家达',
|
|
'income' => 'CAST(0 AS decimal(18,2))',
|
|
'expense' => 'CAST(c.[costYuan] AS decimal(18,2))',
|
|
'movement_condition' => 'ISNULL(c.[costYuan], 0) <> 0',
|
|
'note_filter' => "N'推广费'",
|
|
'platform_category' => '唯品会',
|
|
'row' => "CONVERT(varchar(10), c.[reportDate], 23) AS occurred_at, N'推广费' AS note_primary, NULL AS note_secondary",
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @return array<int,array<string,mixed>> */
|
|
protected function select(string $connection, string $sql, array $bindings): array
|
|
{
|
|
$query = $this->queries[$connection] ??= $this->factory->forSourceWithTimeout($connection, 45);
|
|
|
|
return $query->select($sql, $bindings);
|
|
}
|
|
}
|