From 0ed02e95f7bcf9e7ec0fa0335ab5e12f9a2455ce Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Wed, 4 Nov 2026 10:08:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BE=9B=E5=BA=94=E5=95=86=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=88=86=E9=A1=B5=E5=B9=B6=E5=A2=9E=E5=8A=A0=E6=95=B4?= =?UTF-8?q?=E6=9C=88=E9=80=80=E8=B4=A7=E6=8E=A8=E9=80=81=20(#847)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/SupplierDingTalkNotifier.php | 26 +++++- .../View/SupplierReconciliationPageData.php | 82 +++++++++++++++--- .../finance-data.blade.php | 86 +++++++++++++++++-- .../SupplierReconciliationDingTalkTest.php | 86 +++++++++++++++++++ 4 files changed, 262 insertions(+), 18 deletions(-) diff --git a/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php b/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php index 899280bf..401026ed 100644 --- a/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php +++ b/modules/EcommerceFinance/Services/SupplierDingTalkNotifier.php @@ -25,7 +25,31 @@ class SupplierDingTalkNotifier /** @return array{configured:bool,webhook_hint:string,at_all:bool,last_pushed_at:string,last_push_count:int} */ public function configuration(string $supplier): array { - $config = $this->configs()[$supplier] ?? null; + return $this->configurationFrom($this->configs()[$supplier] ?? null); + } + + /** + * 一次读取配置并返回多个厂家的推送状态,避免供应商列表逐行查询。 + * + * @param list $suppliers + * @return array + */ + public function configurationMap(array $suppliers): array + { + $configs = $this->configs(); + $result = []; + foreach (array_unique(array_filter(array_map('trim', $suppliers))) as $supplier) { + $result[$supplier] = $this->configurationFrom($configs[$supplier] ?? null); + } + + return $result; + } + + /** + * @return array{configured:bool,webhook_hint:string,at_all:bool,last_pushed_at:string,last_push_count:int} + */ + private function configurationFrom(mixed $config): array + { $webhook = is_array($config) ? trim((string) ($config['webhook'] ?? '')) : ''; return [ diff --git a/modules/EcommerceFinance/View/SupplierReconciliationPageData.php b/modules/EcommerceFinance/View/SupplierReconciliationPageData.php index f14be245..b2b224a9 100644 --- a/modules/EcommerceFinance/View/SupplierReconciliationPageData.php +++ b/modules/EcommerceFinance/View/SupplierReconciliationPageData.php @@ -27,6 +27,9 @@ class SupplierReconciliationPageData /** 财务端比对明细:每页 15 条。 */ private const COMPARISON_PER_PAGE = 15; + /** 财务端供应商列表:每页 15 家。 */ + private const SUPPLIER_PER_PAGE = 15; + public function __construct( private readonly SupplierPayableOverrideStore $store, private readonly SupplierStatementStore $statements, @@ -46,8 +49,24 @@ class SupplierReconciliationPageData PayablesFinalActions::DATASET_PREFIX . 'supplier:payment-reconciliation', $companyId, ); + $returnRows = $this->store->listRows( + PayablesFinalActions::DATASET_PREFIX . 'supplier:return-reconciliation', + $companyId, + ); $months = []; + $returnSupplierMonths = []; + foreach ($returnRows as $row) { + $returnMonth = $this->returnRowMonth($row); + $supplier = trim((string) ($row['supplier'] ?? '')); + if ($returnMonth !== '') { + $months[$returnMonth] = true; + } + if ($returnMonth !== '' && $supplier !== '') { + $returnSupplierMonths[$supplier][$returnMonth] = + (int) ($returnSupplierMonths[$supplier][$returnMonth] ?? 0) + 1; + } + } foreach ($mine as $row) { $month = substr(trim((string) ($row['summary_month'] ?? '')), 0, 7); if ($month !== '') { @@ -95,10 +114,7 @@ class SupplierReconciliationPageData foreach ($this->statements->uploadedSuppliers($companyId) as $name) { $suppliers[$name] = true; } - foreach ($this->store->listRows( - PayablesFinalActions::DATASET_PREFIX . 'supplier:return-reconciliation', - $companyId, - ) as $row) { + foreach ($returnRows as $row) { $name = trim((string) ($row['supplier'] ?? '')); if ($name !== '') { $suppliers[$name] = true; @@ -107,16 +123,27 @@ class SupplierReconciliationPageData $supplierNames = array_keys($suppliers); sort($supplierNames); - $uploads = []; - foreach ($supplierNames as $name) { - $latest = $this->statements->uploads($name, $companyId, 1); - $uploads[$name] = $latest[0] ?? null; - } + $supplierTotal = count($supplierNames); + $supplierPages = max((int) ceil($supplierTotal / self::SUPPLIER_PER_PAGE), 1); + $supplierPage = min(max((int) request()->query('supplier_page', 1), 1), $supplierPages); + $supplierListRows = array_slice( + $supplierNames, + ($supplierPage - 1) * self::SUPPLIER_PER_PAGE, + self::SUPPLIER_PER_PAGE, + ); // 选中厂家时页面切换成「比对明细」视图(列表不再渲染,避免 100+ 家时往下翻)。 $supplierQuery = trim((string) request()->query('supplier', '')); $selected = in_array($supplierQuery, $supplierNames, true) ? $supplierQuery : ''; $supplierNotFound = $supplierQuery !== '' && $selected === ''; + + $uploads = []; + $uploadSuppliers = $selected !== '' ? [$selected] : $supplierListRows; + foreach ($uploadSuppliers as $name) { + $latest = $this->statements->uploads($name, $companyId, 1); + $uploads[$name] = $latest[0] ?? null; + } + $detail = null; if ($selected !== '') { $detail = $this->detail($mine, $selected, $companyId); @@ -133,11 +160,39 @@ class SupplierReconciliationPageData self::COMPARISON_PER_PAGE, ); } - $returnPushMonth = $month !== '' ? $month : now()->format('Y-m'); + + $returnLatestMonths = []; + $returnDisplayMonths = []; + $returnCounts = []; + foreach ($supplierListRows as $supplier) { + $supplierMonths = array_keys((array) ($returnSupplierMonths[$supplier] ?? [])); + rsort($supplierMonths); + $latestMonth = (string) ($supplierMonths[0] ?? ''); + $displayMonth = $month !== '' ? $month : $latestMonth; + + $returnLatestMonths[$supplier] = $latestMonth; + $returnDisplayMonths[$supplier] = $displayMonth; + $returnCounts[$supplier] = (int) ($returnSupplierMonths[$supplier][$displayMonth] ?? 0); + } + + $selectedMonths = array_keys((array) ($returnSupplierMonths[$selected] ?? [])); + rsort($selectedMonths); + $selectedLatestReturnMonth = (string) ($selectedMonths[0] ?? ''); + $returnPushMonth = $month !== '' + ? $month + : ($selectedLatestReturnMonth !== '' ? $selectedLatestReturnMonth : ($months[0] ?? now()->format('Y-m'))); + + $robotSuppliers = $selected !== '' ? [$selected] : $supplierListRows; + $robotConfigurations = $this->dingTalkNotifier->configurationMap($robotSuppliers); return [ 'view' => 'business.pages.supplier-reconciliation.finance-data', 'supplierNames' => $supplierNames, + 'supplierListRows' => $supplierListRows, + 'supplierTotal' => $supplierTotal, + 'supplierPages' => $supplierPages, + 'supplierPage' => $supplierPage, + 'supplierPerPage' => self::SUPPLIER_PER_PAGE, 'supplierQuery' => $supplierQuery, 'supplierNotFound' => $supplierNotFound, 'uploads' => $uploads, @@ -151,12 +206,17 @@ class SupplierReconciliationPageData ? $this->financeReturnPanel($selected, $companyId, $month, $reconciler) : null, 'returnPushMonth' => $returnPushMonth, - 'robot' => $selected !== '' ? $this->dingTalkNotifier->configuration($selected) : null, + 'returnLatestMonths' => $returnLatestMonths, + 'returnDisplayMonths' => $returnDisplayMonths, + 'returnCounts' => $returnCounts, + 'robot' => $selected !== '' ? ($robotConfigurations[$selected] ?? null) : null, + 'robotConfigurations' => $robotConfigurations, 'listUrl' => route('business.pages.show', array_filter([ 'section' => 'supplier-reconciliation', 'page' => 'finance-data', 'month' => $month, 'reconciler' => $reconciler, + 'supplier_page' => $supplierPage > 1 ? $supplierPage : null, ], static fn (mixed $value): bool => $value !== '' && $value !== null)), 'statementTemplateUrl' => route('business.supplier-reconciliation.template'), 'uploadUrl' => route('business.supplier-reconciliation.statement.upload'), diff --git a/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php b/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php index 6f23a06a..dab6f6e6 100644 --- a/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php +++ b/resources/views/business/pages/supplier-reconciliation/finance-data.blade.php @@ -15,6 +15,8 @@ .supplier-reconciliation-page .sr-button.is-primary { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-dark, #856632); color: #fff; } .supplier-reconciliation-page .sr-button.is-disabled { opacity: .55; cursor: not-allowed; } .supplier-reconciliation-page .sr-button.is-small { height: 26px; padding: 0 9px; font-size: .75rem; } + .supplier-reconciliation-page .sr-list-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } + .supplier-reconciliation-page .sr-list-actions form { margin: 0; } .supplier-reconciliation-page .sr-hint { margin: 0; color: var(--vip-content-muted, #595959); font-size: .75rem; line-height: 1.7; } .supplier-reconciliation-page .sr-scroll { min-width: 0; max-width: 100%; overflow-x: auto; scrollbar-width: thin; } .supplier-reconciliation-page table { width: 100%; min-width: 720px; border-collapse: collapse; font-size: .8125rem; } @@ -234,33 +236,105 @@ @else

各厂家上传情况(实时)

+

+ 每页显示 15 家。选择具体月份后,退货推送按该月份统计;全部月份下优先显示各厂家最新有数据的退货月份。 + 整月退货会包含待确认、已确认和异议中记录,并自动按钉钉消息容量分批发送。 +

- +
- + + + - @forelse ($supplierNames as $supplier) - @php $upload = $uploads[$supplier] ?? null; @endphp + @forelse ($supplierListRows as $supplier) + @php + $upload = $uploads[$supplier] ?? null; + $returnMonth = (string) ($returnDisplayMonths[$supplier] ?? ''); + $returnCount = (int) ($returnCounts[$supplier] ?? 0); + $robotConfigured = (bool) ($robotConfigurations[$supplier]['configured'] ?? false); + $detailUrl = route('business.pages.show', array_filter([ + 'section' => $section['slug'], + 'page' => $page['slug'], + 'month' => $month, + 'reconciler' => $reconciler, + 'supplier' => $supplier, + 'supplier_page' => $supplierPage > 1 ? $supplierPage : null, + ], static fn (mixed $value): bool => $value !== '' && $value !== null)); + @endphp + + @empty - + @endforelse
供应商 最近上传时间 上传行数比对退货月份退货行数操作
{{ $supplier }} {{ $upload['uploaded_at'] ?? '—' }} {{ $upload ? number_format((int) ($upload['rows'] ?? 0)) : '—' }}{{ $returnMonth !== '' ? $returnMonth : '—' }}{{ $returnCount > 0 ? number_format($returnCount) : '—' }} - 查看比对 +
+ 查看比对 + @if ($returnCount > 0) + @if ($robotConfigured) +
+ @csrf + + + +
+ @else + 配置机器人 + @endif + @endif +
还没有可对账的供应商:请先在「供应商应付(终版)→ 货款对账」导入我方结算数据。
还没有可对账的供应商:请先在「供应商应付(终版)→ 货款对账」导入我方结算数据。
+
+ + 共 {{ number_format($supplierTotal) }} 家 + · 每页 {{ number_format($supplierPerPage) }} 家 + · 第 {{ $supplierPage }} / {{ $supplierPages }} 页 + + @php + $supplierPageStart = max(1, min($supplierPage - 4, $supplierPages - 8)); + $supplierPageEnd = min($supplierPages, $supplierPageStart + 8); + $supplierPageUrl = fn (int $number): string => route('business.pages.show', array_merge(request()->query(), [ + 'section' => $section['slug'], + 'page' => $page['slug'], + 'supplier_page' => $number, + ])); + @endphp + @if ($supplierPage > 1) + 上一页 + @endif + @if ($supplierPageStart > 1) + 1 + @if ($supplierPageStart > 2) … @endif + @endif + @for ($supplierPageNumber = $supplierPageStart; $supplierPageNumber <= $supplierPageEnd; $supplierPageNumber++) + {{ $supplierPageNumber }} + @endfor + @if ($supplierPageEnd < $supplierPages) + @if ($supplierPageEnd < $supplierPages - 1) … @endif + {{ $supplierPages }} + @endif + @if ($supplierPage < $supplierPages) + 下一页 + @endif +
@endif diff --git a/tests/Feature/Business/SupplierReconciliationDingTalkTest.php b/tests/Feature/Business/SupplierReconciliationDingTalkTest.php index 908fba76..6e7ee943 100644 --- a/tests/Feature/Business/SupplierReconciliationDingTalkTest.php +++ b/tests/Feature/Business/SupplierReconciliationDingTalkTest.php @@ -241,6 +241,92 @@ class SupplierReconciliationDingTalkTest extends FeatureTestCase ->assertSee('name="month"', false); } + public function test_finance_supplier_list_is_paginated_fifteen_per_page_with_return_push_action(): void + { + $companyId = (int) $this->company->id; + $store = app(SupplierPayableOverrideStore::class); + for ($index = 1; $index <= 16; $index++) { + $supplier = sprintf('分页厂家%02d', $index); + $store->save('final:supplier:return-reconciliation', 'return:' . $index, [ + 'summary_month' => '2026-10', + 'send_time' => '2026-10-' . str_pad((string) $index, 2, '0', STR_PAD_LEFT) . ' 10:00:00', + 'supplier' => $supplier, + 'barcode' => 'RETURN-PAGE-' . $index, + 'sku_name' => '分页退货', + 'qty' => '1', + 'unit_price' => '10', + 'amount' => '10', + ], $companyId, null); + } + app(SupplierDingTalkNotifier::class)->save('分页厂家01', self::WEBHOOK, 'SEC-test-123', false); + + $pageUrl = route('business.pages.show', [ + 'company_id' => $companyId, + 'section' => 'supplier-reconciliation', + 'page' => 'finance-data', + ]); + + $firstPage = $this->loginAs()->get($pageUrl)->assertOk(); + $firstPage->assertSeeText('共 16 家') + ->assertSeeText('每页 15 家') + ->assertSeeText('第 1 / 2 页') + ->assertSeeText('一键推送退货') + ->assertSee('name="month" value="2026-10"', false); + $firstHtml = (string) $firstPage->getContent(); + $firstTableStart = strpos($firstHtml, 'data-finance-supplier-list'); + $firstTableEnd = strpos($firstHtml, '', $firstTableStart); + $this->assertNotFalse($firstTableStart); + $this->assertNotFalse($firstTableEnd); + $firstTable = substr($firstHtml, $firstTableStart, $firstTableEnd - $firstTableStart); + $this->assertStringContainsString('分页厂家01', $firstTable); + $this->assertStringContainsString('分页厂家15', $firstTable); + $this->assertStringNotContainsString('分页厂家16', $firstTable); + + $secondPage = $this->loginAs()->get($pageUrl . '?supplier_page=2')->assertOk(); + $secondPage->assertSeeText('第 2 / 2 页'); + $secondHtml = (string) $secondPage->getContent(); + $secondTableStart = strpos($secondHtml, 'data-finance-supplier-list'); + $secondTableEnd = strpos($secondHtml, '', $secondTableStart); + $this->assertNotFalse($secondTableStart); + $this->assertNotFalse($secondTableEnd); + $secondTable = substr($secondHtml, $secondTableStart, $secondTableEnd - $secondTableStart); + $this->assertStringContainsString('分页厂家16', $secondTable); + $this->assertStringNotContainsString('分页厂家01', $secondTable); + } + + public function test_return_months_are_available_and_default_to_the_latest_imported_month(): void + { + $this->seedReconciliation(); + $this->saveRobot(); + + app(SupplierPayableOverrideStore::class)->save( + 'final:supplier:return-reconciliation', + 'return:october-entry', + [ + 'summary_month' => '2026-10', + 'send_time' => '2026-10-08 10:00:00', + 'supplier' => '晨丰', + 'barcode' => 'RETURN-OCTOBER-ENTRY', + 'sku_name' => '十月退货', + 'qty' => '1', + 'unit_price' => '10', + 'amount' => '10', + ], + (int) $this->company->id, + null, + ); + + $this->loginAs()->get(route('business.pages.show', [ + 'section' => 'supplier-reconciliation', + 'page' => 'finance-data', + 'supplier' => '晨丰', + ])) + ->assertOk() + ->assertSee('