diff --git a/app/Console/Commands/SyncStoreReportPaidSales.php b/app/Console/Commands/SyncStoreReportPaidSales.php index 7dd68029..19c3cad4 100644 --- a/app/Console/Commands/SyncStoreReportPaidSales.php +++ b/app/Console/Commands/SyncStoreReportPaidSales.php @@ -7,7 +7,8 @@ use Modules\EcommerceFinance\Services\StoreReportMirrorSynchronizer; use Throwable; /** - * 店铺维度报表的「销量」汇总:业绩明细「订单实付/实收金额」按「店铺名称 + 发生日期」预汇总。 + * 店铺维度报表的「销量」汇总:业绩明细「收入金额 + 优惠金额 + 冲减金额 + 退款金额」 + * 按「店铺名称 + 发生日期」预汇总。 * * 业绩明细同步(store-receipt:order-match:sync)结束后会自动刷新一次; * 这个命令用于单独重跑(例如补数据或按最新代码强制重建)。 @@ -17,7 +18,7 @@ class SyncStoreReportPaidSales extends Command protected $signature = 'business-mirror:sync-store-report-paid-sales {--force : 忽略来源指纹,强制重建销量汇总}'; - protected $description = '把业绩明细的订单实付/实收金额按店铺和发生日期汇总,供店铺维度报表销量列读取'; + protected $description = '把业绩明细的收入/优惠/冲减/退款金额按店铺和发生日期汇总,供店铺维度报表销量列读取'; public function handle(StoreReportMirrorSynchronizer $synchronizer): int { diff --git a/config/business_data.php b/config/business_data.php index f0145a54..348db6c9 100644 --- a/config/business_data.php +++ b/config/business_data.php @@ -180,7 +180,7 @@ return [ 'store_report_paid_sales_rows' => [ 'role' => 'aggregate', 'target_store' => 'mariadb', - 'consumer' => '店铺维度报表销量(业绩明细订单实付/实收金额)', + 'consumer' => '店铺维度报表销量(业绩明细收入+优惠+冲减+退款金额合计)', ], ], ]; diff --git a/config/business_navigation.php b/config/business_navigation.php index bc674359..87ac40fa 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -252,7 +252,7 @@ $navigation = [ 'directory' => [ 'icon' => 'query_stats', 'status' => '共享镜像', - 'description' => '店铺资料、主体生效区间统一读取团队共享业务镜像;销量取业绩明细的「订单实付/实收金额」,按店铺和发生时间汇总后落到主体期间。', + 'description' => '店铺资料、主体生效区间统一读取团队共享业务镜像;销量取业绩明细的「收入金额 + 优惠金额 + 冲减金额 + 退款金额」合计,按店铺和发生日期逐日汇总后落到主体期间。', 'empty' => '店铺维度报表尚未完成共享数据导入。', 'fields' => [ ['name' => 'erp_store_name', 'label' => 'ERP系统店铺名称'], diff --git a/modules/EcommerceFinance/Repositories/StoreReportRepository.php b/modules/EcommerceFinance/Repositories/StoreReportRepository.php index 38c361f5..e2e4ecdd 100644 --- a/modules/EcommerceFinance/Repositories/StoreReportRepository.php +++ b/modules/EcommerceFinance/Repositories/StoreReportRepository.php @@ -16,8 +16,9 @@ use RuntimeException; * 失效时间为变更日前一天、新主体从变更日当天生效,收入按实际日期归集,不重复统计。 * * 两列金额口径不同,页面分别展示: - * 销量(period_sales / sales_NN):业绩明细(店铺收款匹配订单明细)的「订单实付/实收金额」, - * 按「店铺名称 + 发生时间」预汇总到 store_report_paid_sales_rows,再按发生日期落到主体期间; + * 销量(period_sales / sales_NN):业绩明细(店铺收款匹配订单明细)的 + * 「收入金额 + 优惠金额 + 冲减金额 + 退款金额」合计,按「店铺名称 + 发生日期」 + * 预汇总到 store_report_paid_sales_rows,再按发生日期落到主体期间; * 收入(rolling_12m_income / income_NN):平台账单日汇总 store_report_daily_rows 的收入, * 供主体维度经营报表按主体汇总使用。 */ @@ -28,7 +29,7 @@ class StoreReportRepository public const LIFECYCLES_DATASET = 'store-report.store-lifecycles'; /** 平台账单日汇总:主体维度经营报表的「收入」列仍按这里取数。 */ public const DAILY_DATASET = 'store-report.daily-sales'; - /** 销量数据集:业绩明细「订单实付/实收金额」按「店铺名称 + 发生日期」预汇总。 */ + /** 销量数据集:业绩明细四项金额合计按「店铺名称 + 发生日期」预汇总。 */ public const PAID_SALES_DATASET = 'store-report.paid-sales'; /** 销量汇总表;由业绩明细表汇总而来,页面按店铺名与发生日期关联主体期间。 */ public const PAID_SALES_TABLE = 'store_report_paid_sales_rows'; @@ -36,7 +37,8 @@ class StoreReportRepository public const PAID_SALES_SOURCE_TABLE = 'store_receipt_order_match_rows'; /** - * 销量来源:业绩明细的「订单实付/实收金额」(按店铺 + 发生日期预汇总)。 + * 销量来源:收入+优惠+冲减+退款四项合计(按店铺 + 发生日期预汇总)。 + * 列名沿用历史命名 paid_amount,避免共享镜像表结构变更影响线上旧版本。 * * @var array{table:string,amount:string} */ diff --git a/modules/EcommerceFinance/Services/StoreReportLogic.php b/modules/EcommerceFinance/Services/StoreReportLogic.php index 8643a35e..4b82ca8f 100644 --- a/modules/EcommerceFinance/Services/StoreReportLogic.php +++ b/modules/EcommerceFinance/Services/StoreReportLogic.php @@ -80,7 +80,7 @@ class StoreReportLogic [ 'code' => 'R9', 'object' => '销量口径', - 'logic' => '销量=业绩明细(店铺收款匹配订单明细)的「订单实付/实收金额」,按「店铺名称 + 发生时间」汇总;订单实付为空的未匹配行不计入。收入列仍按平台账单日汇总,两个口径分开统计、不互相覆盖。', + 'logic' => '销量=业绩明细(店铺收款匹配订单明细)的「收入金额 + 优惠金额 + 冲减金额 + 退款金额」合计,按「店铺名称 + 发生日期」逐日汇总;退款、冲减按账单净额(负数)计入,不再使用订单实付/实收金额。收入列仍按平台账单日汇总,两个口径分开统计、不互相覆盖。', 'source' => 'store_receipt_order_match_rows → store_report_paid_sales_rows', 'priority' => '与业绩明细同口径', ], diff --git a/modules/EcommerceFinance/Services/StoreReportMirrorSynchronizer.php b/modules/EcommerceFinance/Services/StoreReportMirrorSynchronizer.php index 92143eff..6f4af0ba 100644 --- a/modules/EcommerceFinance/Services/StoreReportMirrorSynchronizer.php +++ b/modules/EcommerceFinance/Services/StoreReportMirrorSynchronizer.php @@ -447,7 +447,8 @@ class StoreReportMirrorSynchronizer } /** - * 店铺维度报表的「销量」:把业绩明细的「订单实付/实收金额」按「店铺名称 + 发生日期」预汇总。 + * 店铺维度报表的「销量」:把业绩明细的 + * 「收入金额 + 优惠金额 + 冲减金额 + 退款金额」按「店铺名称 + 发生日期」预汇总。 * * 业绩明细由 store-receipt:order-match:sync 按「账期 + 平台」整体重写,这里用 * 「行数 + 最近写入时间 + 金额合计」的指纹判断是否变化:没变化就复用当前版本, @@ -481,7 +482,9 @@ class StoreReportMirrorSynchronizer $this->connection->statement( 'INSERT INTO ' . StoreReportRepository::PAID_SALES_TABLE . ' (generation, store_name, occurred_on, paid_amount, order_count, updated_at)' - . ' SELECT ?, store_name, occurred_date, SUM(COALESCE(paid_amount, 0)), COUNT(paid_amount), ?' + . ' SELECT ?, store_name, occurred_date,' + . ' SUM(COALESCE(income_amount, 0) + COALESCE(discount_amount, 0)' + . ' + COALESCE(offset_amount, 0) + COALESCE(refund_amount, 0)), COUNT(*), ?' . ' FROM ' . StoreReportRepository::PAID_SALES_SOURCE_TABLE . " WHERE store_name IS NOT NULL AND store_name <> '' AND occurred_date IS NOT NULL" . ' GROUP BY store_name, occurred_date', @@ -529,18 +532,20 @@ class StoreReportMirrorSynchronizer } } - /** 业绩明细是否变化的指纹:行数 + 最近写入时间 + 订单实付合计。 */ + /** 业绩明细是否变化的指纹:行数 + 最近写入时间 + 销量口径四项金额合计。 */ private function paidSalesFingerprint(): string { $row = $this->connection->selectOne( 'SELECT COUNT(*) AS rows_total, MAX(imported_at) AS last_imported_at,' - . ' COALESCE(SUM(paid_amount), 0) AS paid_total FROM ' . StoreReportRepository::PAID_SALES_SOURCE_TABLE, + . ' COALESCE(SUM(COALESCE(income_amount, 0) + COALESCE(discount_amount, 0)' + . ' + COALESCE(offset_amount, 0) + COALESCE(refund_amount, 0)), 0) AS sales_total' + . ' FROM ' . StoreReportRepository::PAID_SALES_SOURCE_TABLE, ); return hash('sha256', implode('|', [ (string) ($row->rows_total ?? 0), (string) ($row->last_imported_at ?? ''), - (string) ($row->paid_total ?? '0'), + (string) ($row->sales_total ?? '0'), ])); } diff --git a/tests/Feature/Console/StoreReportMirrorSyncTest.php b/tests/Feature/Console/StoreReportMirrorSyncTest.php index 31ee3a46..5a0cb6d7 100644 --- a/tests/Feature/Console/StoreReportMirrorSyncTest.php +++ b/tests/Feature/Console/StoreReportMirrorSyncTest.php @@ -228,9 +228,11 @@ class StoreReportMirrorSyncTest extends TestCase $mirror->table('store_receipt_order_match_rows')->insert([ $this->perfRow('k1', '2026-08-02', '店铺A', 100.5), $this->perfRow('k2', '2026-08-02', '店铺A', 9.5), - // 未匹配行没有订单实付,落到 0,不计入销量金额。 + // 未匹配行没有订单实付、也没有收入,四项金额合计为 0。 $this->perfRow('k3', '2026-08-03', '店铺A', null), $this->perfRow('k4', '2026-08-03', '店铺B', 20), + // 销量=收入 + 优惠 + 冲减 + 退款:100 + 5 + (−3) + (−10) = 92。 + $this->perfRow('k6', '2026-08-04', '店铺C', 100, discount: 5, offset: -3, refund: -10), ]); $upstream = $this->createMock(StoreReportUpstreamRepository::class); $upstream->method('lifecycleBatch')->willReturn([]); @@ -250,9 +252,13 @@ class StoreReportMirrorSyncTest extends TestCase 'generation' => $generation, 'store_name' => '店铺B', 'occurred_on' => '2026-08-03', 'paid_amount' => 20, 'order_count' => 1, ], 'business_mirror'); - $this->assertSame(3, $mirror->table('store_report_paid_sales_rows')->count()); + $this->assertDatabaseHas('store_report_paid_sales_rows', [ + 'generation' => $generation, 'store_name' => '店铺C', 'occurred_on' => '2026-08-04', + 'paid_amount' => 92, 'order_count' => 1, + ], 'business_mirror'); + $this->assertSame(4, $mirror->table('store_report_paid_sales_rows')->count()); $this->assertDatabaseHas('store_report_import_states', [ - 'dataset_key' => StoreReportRepository::PAID_SALES_DATASET, 'status' => 'succeeded', 'row_count' => 3, + 'dataset_key' => StoreReportRepository::PAID_SALES_DATASET, 'status' => 'succeeded', 'row_count' => 4, ], 'business_mirror'); // 业绩明细没有变化:沿用同一版本,不重复全量重建。 @@ -262,22 +268,36 @@ class StoreReportMirrorSyncTest extends TestCase $this->assertSame($generation, $mirror->table('store_report_paid_sales_rows')->value('generation')); // 账期重写后指纹变化:重建并只保留新版本,页面不会读到两个版本。 - $mirror->table('store_receipt_order_match_rows')->insert($this->perfRow('k5', '2026-08-04', '店铺B', 5, '2026-10-16 12:00:00')); + $mirror->table('store_receipt_order_match_rows')->insert($this->perfRow('k5', '2026-08-05', '店铺B', 5, '2026-10-16 12:00:00')); $third = $synchronizer->sync(100, 2); - $newGeneration = $mirror->table('store_report_paid_sales_rows')->where('occurred_on', '2026-08-04')->value('generation'); + $newGeneration = $mirror->table('store_report_paid_sales_rows')->where('occurred_on', '2026-08-05')->value('generation'); $this->assertTrue($third['paid_sales']['changed']); $this->assertNotSame($generation, $newGeneration); - $this->assertSame(4, $mirror->table('store_report_paid_sales_rows')->count()); + $this->assertSame(5, $mirror->table('store_report_paid_sales_rows')->count()); $this->assertSame(0, $mirror->table('store_report_paid_sales_rows')->where('generation', $generation)->count()); } /** @return array */ - private function perfRow(string $key, string $occurredOn, string $store, ?float $paid, string $importedAt = '2026-10-16 10:00:00'): array + private function perfRow( + string $key, + string $occurredOn, + string $store, + ?float $income, + string $importedAt = '2026-10-16 10:00:00', + float $discount = 0, + float $offset = 0, + float $refund = 0, + ): array { return [ 'report_month' => '2026-08-01', 'platform' => '淘系支付宝', 'row_key' => $key, 'occurred_date' => $occurredOn, 'store_name' => $store, - 'paid_amount' => $paid, 'imported_at' => $importedAt, + 'income_amount' => $income, + 'discount_amount' => $discount, + 'offset_amount' => $offset, + 'refund_amount' => $refund, + 'paid_amount' => $income, + 'imported_at' => $importedAt, ]; } @@ -371,6 +391,10 @@ class StoreReportMirrorSyncTest extends TestCase $table->string('row_key'); $table->date('occurred_date')->nullable(); $table->string('store_name')->nullable(); + $table->decimal('income_amount', 20, 4)->nullable(); + $table->decimal('discount_amount', 20, 4)->nullable(); + $table->decimal('offset_amount', 20, 4)->nullable(); + $table->decimal('refund_amount', 20, 4)->nullable(); $table->decimal('paid_amount', 20, 4)->nullable(); $table->dateTime('imported_at'); $table->primary(['report_month', 'row_key']); diff --git a/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php b/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php index 42749bf2..0927404d 100644 --- a/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php @@ -57,7 +57,7 @@ class StoreReportRepositoryTest extends TestCase $table->decimal('income', 20, 4)->default(0); $table->decimal('expense', 20, 4)->default(0); }); - // 销量来源:业绩明细「订单实付/实收金额」按店铺 + 发生日期的预汇总。 + // 销量来源:业绩明细「收入+优惠+冲减+退款金额」按店铺 + 发生日期的预汇总。 $schema->create('store_report_paid_sales_rows', function (Blueprint $table): void { $table->increments('id'); $table->string('generation'); @@ -224,13 +224,13 @@ class StoreReportRepositoryTest extends TestCase $this->repository->rows([['erp_store_name' => '店铺A']], 2026); } - public function test_sales_use_performance_details_paid_amount_and_income_keeps_bill_summary(): void + public function test_sales_use_performance_details_amount_components_and_income_keeps_bill_summary(): void { Carbon::setTestNow('2026-09-15 10:00:00'); - // 同一店铺同一天:账单收入 100,业绩明细订单实付 88 —— 销量取实付、收入取账单。 + // 同一店铺同一天:账单收入 100,业绩明细四项金额合计 88 —— 销量取合计、收入取账单。 $this->insertIncome('店铺A', '2026-07-05', 100); - $this->insertPaidSales('店铺A', '2026-07-05', 88); - $this->insertPaidSales('店铺A', '2026-08-03', 5); + $this->insertSales('店铺A', '2026-07-05', 88); + $this->insertSales('店铺A', '2026-08-03', 5); $rows = $this->repository->rows([ ['erp_store_name' => '店铺A', 'company_name' => '主体甲', 'store_status' => '0'], @@ -351,8 +351,8 @@ class StoreReportRepositoryTest extends TestCase private function insertBill(string $store, string $occurredOn, float $income): void { $this->insertIncome($store, $occurredOn, $income); - // 销量与收入同源不同表:默认按同额写入业绩明细的订单实付,需要区分口径的用例单独调用。 - $this->insertPaidSales($store, $occurredOn, $income); + // 销量与收入同源不同表:默认按同额写入业绩明细的四项金额合计,需要区分口径的用例单独调用。 + $this->insertSales($store, $occurredOn, $income); } /** 平台账单日汇总的收入行(主体维度经营报表的「收入」列)。 */ @@ -367,14 +367,14 @@ class StoreReportRepositoryTest extends TestCase ]); } - /** 业绩明细「订单实付/实收金额」按店铺 + 发生日期的预汇总行。 */ - private function insertPaidSales(string $store, string $occurredOn, float $paid): void + /** 业绩明细「收入+优惠+冲减+退款金额」按店铺 + 发生日期的预汇总行。 */ + private function insertSales(string $store, string $occurredOn, float $amount): void { DB::connection('business_mirror')->table('store_report_paid_sales_rows')->insert([ 'generation' => $this->paidGeneration, 'store_name' => $store, 'occurred_on' => $occurredOn, - 'paid_amount' => $paid, + 'paid_amount' => $amount, 'order_count' => 1, ]); }