diff --git a/config/upstream_mirrors.php b/config/upstream_mirrors.php index 96360423..8a3ec9be 100644 --- a/config/upstream_mirrors.php +++ b/config/upstream_mirrors.php @@ -168,6 +168,25 @@ return [ ], ], 'operations_replica.rpt.jst_shopInfo' => ['source_key' => 'operations_replica', 'schema' => 'rpt', 'object' => 'jst_shopInfo', 'target' => 'ops_jst_shop_info', 'keys' => ['Id'], 'watermark' => 'updateTime'], + 'operations_replica.rpt.jst_originalBillInfo' => [ + 'source_key' => 'operations_replica', + 'schema' => 'rpt', + 'object' => 'jst_originalBillInfo', + 'target' => 'ops_jst_original_bill_info', + 'keys' => ['Id'], + 'watermark' => 'updateTime', + 'columns' => [ + 'Id', 'shop_id', 'channel', 'fee_time', 'del_flag', 'primary_key', + 'createTime', 'updateTime', 'auto_id', 'fee_name', 'inout_type', + 'oc_amount', 'p_auto_id', 'shop_i_id', 'shop_sku_id', 'sku_id', + 'so_id', 'combine_type', 'amount', + ], + 'indexes' => [ + ['Id'], + ['shop_id', 'fee_time', 'Id'], + ['fee_time', 'Id'], + ], + ], // 聚水潭聚工单:协同工单列表。只复制工单查询需要的非敏感字段, // 正式 API 从共享镜像关联“仓内未发”类型,不在请求中直连上游。 diff --git a/database/migrations/2026_09_07_000000_create_original_bill_info_mirror_table.php b/database/migrations/2026_09_07_000000_create_original_bill_info_mirror_table.php new file mode 100644 index 00000000..9dfde35b --- /dev/null +++ b/database/migrations/2026_09_07_000000_create_original_bill_info_mirror_table.php @@ -0,0 +1,57 @@ +connection()); + + if ($schema->hasTable('ops_jst_original_bill_info')) { + return; + } + + $schema->create('ops_jst_original_bill_info', function (Blueprint $table): void { + $table->char('_mirror_key', 64)->primary(); + $table->integer('Id'); + $table->integer('shop_id')->nullable(); + $table->string('channel', 100)->nullable(); + $table->dateTime('fee_time')->nullable(); + $table->integer('del_flag')->nullable(); + $table->string('primary_key', 100)->nullable(); + $table->dateTime('createTime')->nullable(); + $table->dateTime('updateTime')->nullable(); + $table->bigInteger('auto_id')->nullable(); + $table->string('fee_name', 100)->nullable(); + $table->string('inout_type', 100)->nullable(); + $table->string('oc_amount', 100)->nullable(); + $table->string('p_auto_id', 100)->nullable(); + $table->string('shop_i_id', 100)->nullable(); + $table->string('shop_sku_id', 100)->nullable(); + $table->string('sku_id', 100)->nullable(); + $table->string('so_id', 100)->nullable(); + $table->string('combine_type', 100)->nullable(); + $table->decimal('amount', 18, 2)->nullable(); + $table->uuid('_mirror_generation')->nullable()->index(); + $table->dateTime('_mirror_synced_at', 6)->index(); + + $table->unique('Id', 'ops_original_bill_source_id_unique'); + $table->index('updateTime', 'ops_original_bill_update_time_index'); + $table->index(['shop_id', 'fee_time', 'Id'], 'ops_original_bill_shop_fee_index'); + $table->index(['fee_time', 'Id'], 'ops_original_bill_fee_time_index'); + }); + } + + public function down(): void + { + Schema::connection($this->connection())->dropIfExists('ops_jst_original_bill_info'); + } +}; diff --git a/docs/上游业务镜像清单.md b/docs/上游业务镜像清单.md index 8d01eb8a..cbbda492 100644 --- a/docs/上游业务镜像清单.md +++ b/docs/上游业务镜像清单.md @@ -20,6 +20,7 @@ | 本次建立 | `config/upstream_mirrors.php` 中登记全部当前代码引用的原表;通用表级同步框架负责建表、稳定游标、断点续传、增量重叠和全量校准。 | | 已开始逐域切换 | 公司、店铺、商标页面在对应镜像完成首次全量后自动改读完整镜像表;首次全量前保留旧快照作为兼容回滚。其他业务域仍需逐项切换,不得宣称全项目已经完成。 | | 中央同步接入 | 店铺维度报表从 `jiansun.dbo.jst_originalBillInfo` 和 `dbo.fys_shopInfo_flow` 自动、可断点同步;不再依赖开发者电脑、SQLite 或人工 XLSX。 | +| 已登记,待首次同步 | `operations_replica/rpt.jst_originalBillInfo` 登记为通用原始账单镜像 `ops_jst_original_bill_info`;保留当前 19 个业务字段,按 `Id` 断点全量、按 `updateTime` 重叠增量。部署迁移只创建空表,不自动启动约 1400 万行的首次同步。 | ## 经营分析副本库(operations_replica) @@ -40,6 +41,7 @@ | 推广费用 | `rpt.ads_itemid_sales_cost_ratio_v2_di` | `ops_item_sales_cost_ratio` | | 退款退货 | `rpt.jst_returnAndRefundInfo`、`rpt.jst_returnAndRefundItem` | `ops_jst_return_refund_*` | | 聚水潭店铺 | `rpt.jst_shopInfo` | `ops_jst_shop_info` | +| 聚水潭原始账单 | `rpt.jst_originalBillInfo` | `ops_jst_original_bill_info` | | 店铺维度报表 | `jiansun/dbo.jst_originalBillInfo + dbo.jst_shopInfo + dbo.fys_shopInfo_flow` | `store_report_bill_rows`、`store_report_lifecycle_rows`、`store_report_daily_rows` | | 聚水潭聚工单(仓内未发) | `rpt.work_order_main`、`rpt.work_order_sub`、`rpt.work_order_form_config`、`rpt.work_order_form_value` | `ops_work_order_main`、`ops_work_order_sub`、`ops_work_order_form_config`、`ops_work_order_form_value` | @@ -72,6 +74,7 @@ 7. 普通商品的采购单价同步以采购主表 `updateTime` 加明细 `Id` 作为稳定游标;页面按商品编码选择最近完成的 1688 一键代发价格。同步保存全部状态,使完成单后续取消时也能撤回旧价格;首次全量前只显示普通商品自身采购价。 8. 聚水潭官方接口尚未接入时,普通商品历史残留排除由负责人提供的最新完整 XLSX 导出刷新。只排除“导出生成前已创建、但导出中缺失”的镜像行,导出生成后的新增商品保持可见。导入使用新同步代次,只有完整解析成功才原子替换排除表;中断或错误保留上一版。后续接入聚水潭官方“普通商品资料查询”及删除/恢复记录后,由官方接口取代人工导出校准。 9. 店铺维度报表是已批准的中央专用数据集:生产节点按 `Id` 断点推进并回看最近 50000 行;原始账单同步后增量维护按店铺/日期汇总的 `store_report_daily_rows`,页面不得现场扫描千万级原始账单。开发机不运行刷新 worker。 +10. `operations_replica.rpt.jst_originalBillInfo` 是供多个业务功能复用的原始账单镜像。首次同步必须单独授权并在低峰期分批续传;部署、页面请求和普通开发机均不得自动启动。同步完成后,开发者通过固定 Repository 查询 `ops_jst_original_bill_info`,不得各自创建整表快照。 ## 后续切换顺序 diff --git a/tests/Unit/EcommerceFinance/GenericUpstreamPipelineTest.php b/tests/Unit/EcommerceFinance/GenericUpstreamPipelineTest.php index 01cbfde2..9f722682 100644 --- a/tests/Unit/EcommerceFinance/GenericUpstreamPipelineTest.php +++ b/tests/Unit/EcommerceFinance/GenericUpstreamPipelineTest.php @@ -112,6 +112,48 @@ class GenericUpstreamPipelineTest extends TestCase $this->assertFalse($registry->scheduled($key)); } + public function test_original_bill_dataset_uses_stable_cursor_and_explicit_shared_schema(): void + { + $configuration = require base_path('config/upstream_mirrors.php'); + config(['upstream_mirrors.datasets' => $configuration['datasets']]); + + $key = 'operations_replica.rpt.jst_originalBillInfo'; + $registry = app(BusinessMirrorDatasetRegistry::class); + $dataset = $registry->get($key); + + $this->assertSame('operations_replica', $dataset->sourceKey()); + $this->assertSame('rpt', $dataset->sourceSchema()); + $this->assertSame('jst_originalBillInfo', $dataset->sourceObject()); + $this->assertSame('ops_jst_original_bill_info', $dataset->targetTable()); + $this->assertSame(['Id'], $dataset->keyColumns()); + $this->assertSame('updateTime', $dataset->watermarkColumn()); + $this->assertSame([ + ['Id'], + ['shop_id', 'fee_time', 'Id'], + ['fee_time', 'Id'], + ], $dataset->indexDefinitions()); + $this->assertSame([ + 'Id', 'shop_id', 'channel', 'fee_time', 'del_flag', 'primary_key', + 'createTime', 'updateTime', 'auto_id', 'fee_name', 'inout_type', + 'oc_amount', 'p_auto_id', 'shop_i_id', 'shop_sku_id', 'sku_id', + 'so_id', 'combine_type', 'amount', + ], $registry->columnSelection($key)); + } + + public function test_original_bill_migration_creates_compact_shared_table(): void + { + $migration = require base_path('database/migrations/2026_09_07_000000_create_original_bill_info_mirror_table.php'); + $migration->up(); + + $schema = Schema::connection('business_mirror'); + $this->assertTrue($schema->hasTable('ops_jst_original_bill_info')); + $this->assertTrue($schema->hasColumn('ops_jst_original_bill_info', 'Id')); + $this->assertTrue($schema->hasColumn('ops_jst_original_bill_info', 'updateTime')); + $this->assertTrue($schema->hasColumn('ops_jst_original_bill_info', '_mirror_synced_at')); + $this->assertSame('string', $schema->getColumnType('ops_jst_original_bill_info', 'channel')); + $this->assertSame('string', $schema->getColumnType('ops_jst_original_bill_info', 'so_id')); + } + /** @dataProvider nativeDrivers */ public function test_native_tls_catalog_first_sync_pagination_and_incremental(string $driver, bool $timezoneAware = false, ?string $fixtureHost = null): void {