From ad618ded59965398425df9282a7bcb039d0f91c4 Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Thu, 6 Aug 2026 16:20:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=8E=A5=E6=89=8B?= =?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E8=B5=94=E5=81=BF=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SupplierPayableRepository.php | 155 +++++++++--------- .../compensation-details.blade.php | 47 ++++-- .../Business/SupplierPayablePagesTest.php | 19 +++ .../SupplierPayableRepositoryTest.php | 110 +++++++++++++ 4 files changed, 242 insertions(+), 89 deletions(-) create mode 100644 tests/Unit/EcommerceFinance/SupplierPayableRepositoryTest.php diff --git a/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php b/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php index 8de2a03c..96dfc4f1 100644 --- a/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php +++ b/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php @@ -3,6 +3,8 @@ namespace Modules\EcommerceFinance\Repositories; use Modules\EcommerceFinance\Services\JianSunConnection; +use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery; +use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory; use PDO; /** @@ -11,7 +13,7 @@ use PDO; * 封装 5 个子页面的数据访问逻辑: * - returnReconciliations() 退货对账 * - invoiceDeductions() 发票扣款 - * - compensationDetails() 赔偿明细(UNION 纸板 + 压板) + * - compensationDetails() 赔偿明细 * - advanceDetails() 借支明细 * - sampleDeductions() 样品扣款 * @@ -21,6 +23,13 @@ class SupplierPayableRepository { private ?PDO $db = null; + private readonly UpstreamReadOnlyQuery $query; + + public function __construct(UpstreamReadOnlyQueryFactory $queries) + { + $this->query = $queries->forSource('jiansun'); + } + private function db(): PDO { return $this->db ??= JianSunConnection::get(); @@ -257,90 +266,85 @@ class SupplierPayableRepository } // ============================================================ - // 3. 赔偿明细 — fys_paper_factory + fys_pressing_factory UNION + // 3. 赔偿明细 — fys_supplierPaymentSummary // ============================================================ public function compensationDetails(array $filters, int $page, int $pageSize): array { - $where = []; - $params = []; + [$whereSql, $bindings] = $this->compensationWhere($filters); + $page = max($page, 1); + $pageSize = min(max($pageSize, 1), 100); + $offset = ($page - 1) * $pageSize; + $total = (int) ($this->query->scalar( + "SELECT COUNT(*) FROM [dbo].[fys_supplierPaymentSummary] WHERE $whereSql", + $bindings, + ) ?? 0); - if (!empty($filters['supplier'])) { - $where[] = 'supplier LIKE ?'; - $params[] = '%' . $filters['supplier'] . '%'; - } - if (!empty($filters['month_from'])) { - $where[] = 'month >= ?'; - $params[] = $filters['month_from']; - } - if (!empty($filters['month_to'])) { - $where[] = 'month <= ?'; - $params[] = $filters['month_to']; + $rows = $this->query->select( + "SELECT + [Month] AS month, + [supplier] AS supplier, + CAST([Totalindemnity] AS decimal(18,2)) AS amount, + [reason] AS reason, + [remarks] AS remark + FROM [dbo].[fys_supplierPaymentSummary] + WHERE $whereSql + ORDER BY [Month] DESC, [id] DESC + OFFSET $offset ROWS FETCH NEXT $pageSize ROWS ONLY", + $bindings, + ); + + foreach ($rows as &$row) { + $row['amount'] = (float) ($row['amount'] ?? 0); } + unset($row); - $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; - - $sql = "SELECT - month, - supplier, - CAST(compensation_amount AS decimal(18,2)) AS amount, - reason, - '' AS remark, - creator, - CONVERT(varchar(19), create_time, 120) AS create_time, - '纸板厂' AS factory_type - FROM ( - SELECT - month, - factory_subject_name AS supplier, - compensation_amount, - reason, - creator, - create_time - FROM fys_paper_factory_compensation_detail - UNION ALL - SELECT - month, - factory_subject_name AS supplier, - compensation_amount, - reason, - creator, - create_time - FROM fys_pressing_factory_compensation_detail - ) AS t - $whereSql"; - - return $this->runQuery($sql, $params, $page, $pageSize, 'month DESC, create_time DESC'); + return ['data' => $rows, 'total' => $total]; } public function compensationDetailSummary(array $filters): array { - $where = []; - $params = []; - if (!empty($filters['supplier'])) { - $where[] = 'supplier LIKE ?'; - $params[] = '%' . $filters['supplier'] . '%'; - } - $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; - - $sql = "SELECT - ISNULL(SUM(compensation_amount), 0) AS total_amount, - COUNT(*) AS total_count - FROM ( - SELECT compensation_amount, factory_subject_name AS supplier FROM fys_paper_factory_compensation_detail - UNION ALL - SELECT compensation_amount, factory_subject_name AS supplier FROM fys_pressing_factory_compensation_detail - ) AS t - $whereSql"; - $stmt = $this->db()->prepare($sql); - $stmt->execute($params); - $row = $stmt->fetch(); + [$whereSql, $bindings] = $this->compensationWhere($filters); + $row = $this->query->first( + "SELECT + ISNULL(SUM([Totalindemnity]), 0) AS total_amount, + COUNT(*) AS total_count + FROM [dbo].[fys_supplierPaymentSummary] + WHERE $whereSql", + $bindings, + ) ?? []; return [ - 'totalAmount' => (float) $row['total_amount'], - 'totalCount' => (int) $row['total_count'], + 'totalAmount' => (float) ($row['total_amount'] ?? 0), + 'totalCount' => (int) ($row['total_count'] ?? 0), ]; } + /** @return array{0:string,1:array} */ + private function compensationWhere(array $filters): array + { + $where = ['[Totalindemnity] IS NOT NULL', '[Totalindemnity] <> 0']; + $bindings = []; + + if (! empty($filters['supplier'])) { + $where[] = "[supplier] LIKE ? ESCAPE '\\'"; + $bindings[] = '%' . str_replace( + ['\\', '%', '_'], + ['\\\\', '\\%', '\\_'], + trim((string) $filters['supplier']), + ) . '%'; + } + if (! empty($filters['month_from'])) { + $where[] = '[Month] >= ?'; + $bindings[] = (string) $filters['month_from']; + } + if (! empty($filters['month_to'])) { + $where[] = '[Month] <= ?'; + $bindings[] = (string) $filters['month_to']; + } + + return [implode(' AND ', $where), $bindings]; + } + // ============================================================ // 4. 借支明细 — finance_advanceLoans (4行, 字段不足,部分列显示-) // ============================================================ @@ -682,11 +686,12 @@ class SupplierPayableRepository public function filterOptionsForCompensation(): array { - $suppliers = $this->fetchDistinct("SELECT DISTINCT supplier FROM ( - SELECT factory_subject_name AS supplier FROM fys_paper_factory_compensation_detail WHERE factory_subject_name IS NOT NULL - UNION - SELECT factory_subject_name AS supplier FROM fys_pressing_factory_compensation_detail WHERE factory_subject_name IS NOT NULL - ) AS t ORDER BY supplier"); + $suppliers = array_column($this->query->select( + 'SELECT DISTINCT TOP 200 [supplier] AS value ' + . 'FROM [dbo].[fys_supplierPaymentSummary] ' + . 'WHERE [Totalindemnity] IS NOT NULL AND [Totalindemnity] <> 0 AND [supplier] IS NOT NULL ' + . 'ORDER BY [supplier]', + ), 'value'); return ['suppliers' => $suppliers]; } diff --git a/resources/views/business/pages/supplier-payables/compensation-details.blade.php b/resources/views/business/pages/supplier-payables/compensation-details.blade.php index f5b21ec2..ddfce9dd 100644 --- a/resources/views/business/pages/supplier-payables/compensation-details.blade.php +++ b/resources/views/business/pages/supplier-payables/compensation-details.blade.php @@ -1,14 +1,40 @@ + +
{{-- 统计卡片 --}} -
-
-
赔付总金额
-
@{{ formatMoney(summary.totalAmount) }}
+
+
+ 赔付总金额 + @{{ formatMoney(summary.totalAmount) }}
-
-
总记录数
-
@{{ (summary.totalCount || 0).toLocaleString() }}
+
+ 总记录数 + @{{ (summary.totalCount || 0).toLocaleString() }}
@@ -31,7 +57,6 @@ 加载中...
- @@ -43,12 +68,6 @@ - - - -
diff --git a/tests/Feature/Business/SupplierPayablePagesTest.php b/tests/Feature/Business/SupplierPayablePagesTest.php index 25c08a9a..57ba2558 100644 --- a/tests/Feature/Business/SupplierPayablePagesTest.php +++ b/tests/Feature/Business/SupplierPayablePagesTest.php @@ -105,6 +105,25 @@ class SupplierPayablePagesTest extends FeatureTestCase $this->assertCount(34, $columns[0]); } + public function testCompensationPageUsesTheFiveColumnsFromTheReferenceSheet(): void + { + $this->grantAdminPermission(); + + $response = $this->loginAs()->get(route('business.pages.show', [ + 'section' => 'supplier-payables', + 'page' => 'compensation-details', + ])); + + $response + ->assertOk() + ->assertSee('id="compensation-detail-app"', false) + ->assertSee('赔付原因') + ->assertSee('备注') + ->assertSee('public/js/ecommerce-finance/compensation-details.min.js', false); + + $this->assertSame(5, substr_count($response->getContent(), 'grantAdminPermission(); diff --git a/tests/Unit/EcommerceFinance/SupplierPayableRepositoryTest.php b/tests/Unit/EcommerceFinance/SupplierPayableRepositoryTest.php new file mode 100644 index 00000000..dd0a2ee7 --- /dev/null +++ b/tests/Unit/EcommerceFinance/SupplierPayableRepositoryTest.php @@ -0,0 +1,110 @@ +createMock(UpstreamReadOnlyQuery::class); + $bindings = ['%Factory\\%\\_%', '2026-07', '2026-08']; + + $query->expects($this->once()) + ->method('scalar') + ->with( + $this->callback(function (string $sql): bool { + UpstreamReadOnlyQuery::assertReadOnlySql($sql); + $this->assertStringContainsString('[dbo].[fys_supplierPaymentSummary]', $sql); + $this->assertStringContainsString('[Totalindemnity] <> 0', $sql); + + return true; + }), + $bindings, + ) + ->willReturn(1); + $query->expects($this->once()) + ->method('select') + ->with( + $this->callback(function (string $sql): bool { + UpstreamReadOnlyQuery::assertReadOnlySql($sql); + $this->assertStringContainsString('[Totalindemnity] AS decimal(18,2)', $sql); + $this->assertStringContainsString('[remarks] AS remark', $sql); + $this->assertStringContainsString('OFFSET 25 ROWS FETCH NEXT 25 ROWS ONLY', $sql); + $this->assertStringNotContainsString('fys_paper_factory_compensation_detail', $sql); + + return true; + }), + $bindings, + ) + ->willReturn([[ + 'month' => '2026-07', + 'supplier' => 'Factory%_', + 'amount' => '12.50', + 'reason' => 'Reason', + 'remark' => 'Remark', + ]]); + + $result = $this->repository($query)->compensationDetails([ + 'supplier' => 'Factory%_', + 'month_from' => '2026-07', + 'month_to' => '2026-08', + ], 2, 25); + + $this->assertSame(1, $result['total']); + $this->assertSame(12.5, $result['data'][0]['amount']); + $this->assertSame('Remark', $result['data'][0]['remark']); + } + + public function test_compensation_summary_and_options_are_bounded_read_only_queries(): void + { + $query = $this->createMock(UpstreamReadOnlyQuery::class); + $query->expects($this->once()) + ->method('first') + ->with( + $this->callback(function (string $sql): bool { + UpstreamReadOnlyQuery::assertReadOnlySql($sql); + $this->assertStringContainsString('SUM([Totalindemnity])', $sql); + + return true; + }), + [], + ) + ->willReturn(['total_amount' => '20.25', 'total_count' => '2']); + $query->expects($this->once()) + ->method('select') + ->with($this->callback(function (string $sql): bool { + UpstreamReadOnlyQuery::assertReadOnlySql($sql); + $this->assertStringContainsString('DISTINCT TOP 200', $sql); + + return true; + })) + ->willReturn([['value' => 'Supplier A']]); + + $repository = $this->repository($query); + + $this->assertSame( + ['totalAmount' => 20.25, 'totalCount' => 2], + $repository->compensationDetailSummary([]), + ); + $this->assertSame( + ['suppliers' => ['Supplier A']], + $repository->filterOptionsForCompensation(), + ); + } + + private function repository(UpstreamReadOnlyQuery $query): SupplierPayableRepository + { + $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); + $factory->expects($this->once()) + ->method('forSource') + ->with('jiansun') + ->willReturn($query); + + return new SupplierPayableRepository($factory); + } +} -- 2.47.2