From 3be1639fb28d4124e0f445a629cdac9fcb41f78d Mon Sep 17 00:00:00 2001 From: qiuxiang <394405259@qq.com> Date: Tue, 27 Oct 2026 19:27:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A8=E5=B9=BF=E8=B4=B9=E8=90=A5?= =?UTF-8?q?=E9=94=80=E8=B4=A6=E6=88=B7=E6=98=8E=E7=BB=86=E6=B1=87=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增推广费(营销账户)页签、平台与原始店铺汇总、备注筛选和明细弹窗。 --- config/business_navigation.php | 1 + .../Http/Controllers/AccountBills.php | 140 +++++ .../PromotionMarketingExpenseRepository.php | 491 ++++++++++++++++++ .../js/components/business/AccountBills.vue | 271 +++++++++- resources/assets/js/views/business/pages.js | 6 +- routes/admin.php | 2 + .../PromotionMarketingAccountBillsTest.php | 125 +++++ ...romotionMarketingExpenseRepositoryTest.php | 207 ++++++++ tests/Unit/View/AccountBillsTableTest.php | 33 +- 9 files changed, 1251 insertions(+), 25 deletions(-) create mode 100644 modules/EcommerceFinance/Repositories/PromotionMarketingExpenseRepository.php create mode 100644 tests/Feature/Business/PromotionMarketingAccountBillsTest.php create mode 100644 tests/Unit/EcommerceFinance/PromotionMarketingExpenseRepositoryTest.php diff --git a/config/business_navigation.php b/config/business_navigation.php index 88ae629f..3e517fb9 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -554,6 +554,7 @@ $navigation = [ ['slug' => 'dewu', 'title' => '得物账户'], ['slug' => 'vipshop', 'title' => '唯品会MP账户'], ['slug' => 'vipshop-jx', 'title' => '唯品会JX账户'], + ['slug' => 'promotion-marketing', 'title' => '推广费(营销账户)'], ], ], [ diff --git a/modules/EcommerceFinance/Http/Controllers/AccountBills.php b/modules/EcommerceFinance/Http/Controllers/AccountBills.php index 1386aa87..966457b2 100644 --- a/modules/EcommerceFinance/Http/Controllers/AccountBills.php +++ b/modules/EcommerceFinance/Http/Controllers/AccountBills.php @@ -14,7 +14,9 @@ use Modules\EcommerceFinance\Repositories\StoreReceivingJdWalletBillRepository; use Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository; use Modules\EcommerceFinance\Repositories\StoreReceivingDewuBillMirrorRepository; use Modules\EcommerceFinance\Repositories\StoreReceivingPinduoduoBillMirrorRepository; +use Modules\EcommerceFinance\Repositories\PromotionMarketingExpenseRepository; use RuntimeException; +use Throwable; class AccountBills extends Controller { @@ -40,6 +42,10 @@ class AccountBills extends Controller $billType = $bill_type; + if ($billType === 'promotion-marketing') { + return $this->promotionMarketingIndex($request); + } + if (! AccountBillColumns::has($billType)) { abort(404, '未知的账户类型。'); } @@ -109,6 +115,136 @@ class AccountBills extends Controller return response()->json($this->withLedgerRemarks($data, $billType)); } + /** + * 推广费(营销账户)页签的固定响应结构。 + * + * 各平台取数口径尚未配置,先返回明确的空结果与店铺明细列定义;后续接入时 + * 只替换这里的数据来源,不改页签、卡片和弹窗契约。 + */ + private function promotionMarketingIndex(Request $request) + { + $page = max(1, (int) $request->get('page', 1)); + $limit = min(max((int) $request->get('limit', 10), 1), 100); + + try { + $result = app(PromotionMarketingExpenseRepository::class)->paginate( + $this->promotionMarketingFilters($request), + $page, + $limit, + ); + + return response()->json([ + 'data' => $result['rows'], + 'total' => $result['total'], + 'current_page' => $result['current_page'], + 'last_page' => $result['last_page'], + 'per_page' => $result['per_page'], + 'summary' => [ + 'income' => 0, + 'expense' => $result['summary']['expense'], + 'balance' => round(-$result['summary']['expense'], 2), + 'count' => $result['summary']['count'], + ], + 'columns' => [ + 'title' => '推广费(营销账户)', + 'groups' => [], + 'columns' => self::promotionExpenseDetailColumns(), + ], + ]); + } catch (Throwable $exception) { + report($exception); + + return response()->json([ + 'message' => '推广费(营销账户)数据暂时无法读取,请稍后重试。', + ], 503); + } + } + + /** + * 推广费(营销账户)的店铺明细。当前先提供统一空结果,待逐平台取数口径 + * 确定后由专用 Repository 填充;筛选参数与主账单页保持一致。 + */ + public function promotionExpenseDetails(Request $request, string $bill_type) + { + abort_unless($bill_type === 'promotion-marketing', 404); + $this->authorizeStoreReceivingBills(); + + try { + $result = app(PromotionMarketingExpenseRepository::class)->storeSummary( + $this->promotionMarketingFilters($request) + ); + $storeCount = count($result['store_summary']); + + return response()->json([ + 'store_summary' => $result['store_summary'], + 'total' => $storeCount, + 'summary' => [ + 'expense' => $result['total'], + 'count' => $storeCount, + ], + 'platform_summary' => $result['platform_summary'], + 'message' => $storeCount === 0 ? '当前筛选范围内暂无推广费支出明细。' : '', + ]); + } catch (Throwable $exception) { + report($exception); + + return response()->json([ + 'message' => '店铺明细暂时无法读取,请稍后重试。', + ], 503); + } + } + + /** @return array{start:string,end:string,store_name:string,note:string} */ + private function promotionMarketingFilters(Request $request): array + { + $filters = $request->validate([ + 'store_name' => ['nullable', 'string', 'max:255'], + 'start' => ['nullable', 'date_format:Y-m-d'], + 'end' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start'], + 'note' => ['nullable', 'string', 'max:255'], + ]); + + return [ + 'start' => trim((string) ($filters['start'] ?? '')), + 'end' => trim((string) ($filters['end'] ?? '')), + 'store_name' => trim((string) ($filters['store_name'] ?? '')), + 'note' => trim((string) ($filters['note'] ?? '')), + ]; + } + + private function promotionMarketingFilterOptions(Request $request) + { + try { + return response()->json([ + 'shops' => app(PromotionMarketingExpenseRepository::class)->storeOptions( + $this->promotionMarketingFilters($request), + ), + 'bill_items' => [], + 'ledger_remarks' => [], + ]); + } catch (Throwable $exception) { + report($exception); + + return response()->json([ + 'message' => '推广费店铺选项暂时无法读取。', + 'shops' => [], + 'bill_items' => [], + 'ledger_remarks' => [], + ], 503); + } + } + + /** @return array */ + private static function promotionExpenseDetailColumns(): array + { + return [ + ['label' => '店铺名称', 'field' => 'store_name', 'kind' => 'text'], + ['label' => '交易时间', 'field' => 'occurred_at', 'kind' => 'date'], + ['label' => '备注', 'field' => 'notes', 'kind' => 'text'], + ['label' => '支出金额', 'field' => 'expense', 'kind' => 'money'], + ]; + } + public function export(Request $request, string $bill_type) { $this->authorizeStoreReceivingBills(); @@ -145,6 +281,10 @@ class AccountBills extends Controller { $this->authorizeStoreReceivingBills(); + if ($bill_type === 'promotion-marketing') { + return $this->promotionMarketingFilterOptions(request()); + } + $ledgerRemarks = $this->ledgerRemarkMap()->remarksForTab($bill_type); if (in_array($bill_type, ['vipshop', 'vipshop-jx'], true)) { diff --git a/modules/EcommerceFinance/Repositories/PromotionMarketingExpenseRepository.php b/modules/EcommerceFinance/Repositories/PromotionMarketingExpenseRepository.php new file mode 100644 index 00000000..fd3123b2 --- /dev/null +++ b/modules/EcommerceFinance/Repositories/PromotionMarketingExpenseRepository.php @@ -0,0 +1,491 @@ + */ + private array $queries = []; + + public function __construct(private readonly UpstreamReadOnlyQueryFactory $factory) + { + } + + /** + * @return array{ + * rows:array>, + * total:int, + * current_page:int, + * last_page:int, + * per_page:int, + * summary:array{expense: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; + $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']; + $total += $values['count']; + $expenseTotal += $values['expense']; + } + } + + 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' => [ + 'expense' => round($expenseTotal, 2), + 'count' => $total, + ], + 'platform_summary' => $platformSummary, + ]; + }); + } + + /** + * 平台 + 原始店铺的推广费汇总,供弹窗“平台汇总 / 店铺明细”使用。 + * + * 弹窗内的平台和店铺筛选在已汇总的小结果集上执行,不再重复查询上游。 + * + * @return array{ + * store_summary:array, + * platform_summary:array, + * 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 */ + 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['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)), + 'expense' => round((float) ($row['expense'] ?? 0), 2), + ]; + } + + return $result; + } + + /** + * @param array> $histograms + * @param array $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['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 $countByDate + * @return array{0:array,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 + { + $expense = round((float) ($row['expense'] ?? 0), 2); + if ($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, + 'expense' => $expense, + ]; + } + + /** + * @return array{0:string,1:array} + */ + private function where( + array $source, + string $start, + string $end, + string $store, + bool $endExclusive = false, + ?string $note = null, + ): array { + $conditions = [$source['expense_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> */ + private function sources(): array + { + return [ + 'taobao' => [ + 'order' => 1, + 'connection' => 'jiansun', + 'table' => '[dbo].[tmall_spread_bills]', + 'date' => 'c.[transTime]', + 'store' => 'c.[shopName]', + 'expense' => '(CAST(c.[amount] AS decimal(18,2)) / 100.0)', + 'expense_condition' => "c.[finType] = '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]', + 'expense' => 'CAST(c.[tradeAMTOut] AS decimal(18,2))', + 'expense_condition' => '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]', + 'expense' => 'CAST(c.[amount] AS decimal(18,2))', + 'expense_condition' => 'c.[flowType] = 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]', + 'expense' => 'CAST(c.[amount] AS decimal(18,2))', + 'expense_condition' => "c.[in_out_type] = '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]', + 'expense' => 'CAST(c.[cost] AS decimal(18,2))', + 'expense_condition' => "c.[rowType] = N'daily' AND c.[status] = N'success' AND 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]', + 'expense' => 'CAST(c.[trade_amount_yuan] AS decimal(18,2))', + 'expense_condition' => "c.[flow_type] = 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_亿家达', + 'expense' => 'CAST(c.[costYuan] AS decimal(18,2))', + 'expense_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> */ + protected function select(string $connection, string $sql, array $bindings): array + { + $query = $this->queries[$connection] ??= $this->factory->forSourceWithTimeout($connection, 45); + + return $query->select($sql, $bindings); + } +} diff --git a/resources/assets/js/components/business/AccountBills.vue b/resources/assets/js/components/business/AccountBills.vue index 7307cba6..9b2b9ef9 100644 --- a/resources/assets/js/components/business/AccountBills.vue +++ b/resources/assets/js/components/business/AccountBills.vue @@ -1,7 +1,137 @@