From 0aa07f8c7b61bbe71acbe92c0b52f1409c89bf40 Mon Sep 17 00:00:00 2001 From: yueyang <2495544247@qq.com> Date: Tue, 15 Sep 2026 10:59:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=E5=BE=97=E7=89=A9?= =?UTF-8?q?=E6=94=B6=E6=AC=BE=E8=B4=A6=E5=8D=95=E9=95=9C=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/business_data.php | 3 + config/upstream_mirrors.php | 25 +++ docs/上游业务镜像清单.md | 1 + .../Http/Controllers/AccountBills.php | 21 +- ...oreReceivingAlipayBillMirrorRepository.php | 60 +++++- ...StoreReceivingDewuBillMirrorRepository.php | 49 +++++ .../js/components/business/AccountBills.vue | 2 +- .../Business/DewuReceivingBillsTest.php | 124 ++++++++++++ .../BusinessMirrorSynchronizerTest.php | 6 + ...eReceivingDewuBillMirrorRepositoryTest.php | 189 ++++++++++++++++++ tests/Unit/View/AccountBillsTableTest.php | 2 +- 11 files changed, 467 insertions(+), 15 deletions(-) create mode 100644 modules/EcommerceFinance/Repositories/StoreReceivingDewuBillMirrorRepository.php create mode 100644 tests/Feature/Business/DewuReceivingBillsTest.php create mode 100644 tests/Unit/EcommerceFinance/StoreReceivingDewuBillMirrorRepositoryTest.php diff --git a/config/business_data.php b/config/business_data.php index 7a1c31aa..782d5f90 100644 --- a/config/business_data.php +++ b/config/business_data.php @@ -97,6 +97,9 @@ return [ 'jiansun.dbo.jst_originalBillInfo.pinduoduo_receiving' => [ 'role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '店铺收款账单/拼多多账户', ], + 'jiansun.dbo.jst_originalBillInfo.dewu_receiving' => [ + 'role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '店铺收款账单/得物账户', + ], 'jiansun.dbo.alipay_Bill_Detail' => ['role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '店铺收款账单/淘系支付宝账户'], 'jiansun.dbo.tmall_spread_bills' => ['role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '淘宝推广账单'], 'jiansun.dbo.pdd_spread_bills' => ['role' => 'serving_fact', 'target_store' => 'mariadb', 'consumer' => '拼多多推广账单'], diff --git a/config/upstream_mirrors.php b/config/upstream_mirrors.php index 585a67d5..1325eb65 100644 --- a/config/upstream_mirrors.php +++ b/config/upstream_mirrors.php @@ -436,6 +436,31 @@ return [ ['fee_name'], ], ], + // 聚水潭得物货款账户:只复制主账单渠道,保证金、跨境和推广流水 + // 由其他账户域处理,不混入店铺收款账户页面。 + 'jiansun.dbo.jst_originalBillInfo.dewu_receiving' => [ + 'source_key' => 'jiansun', + 'schema' => 'dbo', + 'object' => 'jst_originalBillInfo', + 'target' => 'js_jst_dewu_receiving_bill_rows', + 'keys' => ['Id'], + 'watermark' => 'updateTime', + 'fixed_filters' => [ + 'channel' => 'Dewu', + ], + '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'], + ['fee_time', 'Id', 'inout_type', 'amount'], + ['shop_id', 'fee_time', 'Id', 'inout_type', 'amount', 'fee_name'], + ['fee_name'], + ], + ], 'jiansun.dbo.alipay_Bill_Detail' => [ 'source_key' => 'jiansun', 'schema' => 'dbo', diff --git a/docs/上游业务镜像清单.md b/docs/上游业务镜像清单.md index 8d624f40..ed4ccf1b 100644 --- a/docs/上游业务镜像清单.md +++ b/docs/上游业务镜像清单.md @@ -25,6 +25,7 @@ | 页面已切换 | 店铺收款账单的淘系支付宝账户读取 `operations_replica.rpt.jst_originalBillInfo`,并关联 `rpt.jst_shopInfo` 展示店铺;分页、筛选、汇总和导出统一读取共享业务镜像。 | | 抖音按月校准 | 抖音账户对已核对的完整月份读取 `douyin_authoritative_bills` 中胜算原始明细,`douyin_authoritative_periods` 原子选择有效版本;该月份不再叠加聚水潭拆分行。未校准月份继续读取原始账单镜像。导入命令为 `business-mirror:import-douyin-authoritative --count=<核对笔数> --income=<收入> --expense=<支出>`,只接受完整月份和精确核对值,保留旧版本、文件摘要及原镜像供回滚。生产执行前备份状态与受影响月份镜像,并使用 `/run/caiwu-upstream-sync.lock`。实际财务数据只放私有目录,不进入 Git。 | | 已接入待首次同步 | 店铺收款账单的拼多多账户读取 `jiansun.dbo.jst_originalBillInfo` 中固定 `channel = PinduoduoImport` 的独立切片镜像 `js_jst_pdd_receiving_bill_rows`;不使用慢一天的 `operations_replica` 副本或营销账户 `pdd_spread_bills`。 | +| 已接入待首次同步 | 店铺收款账单的得物账户读取 `jiansun.dbo.jst_originalBillInfo` 中固定 `channel = Dewu` 的独立切片镜像 `js_jst_dewu_receiving_bill_rows`;`Dewubzj`、`DewuKJ`、`DewuTK` 保留给保证金、跨境或推广等其他账户域,不混入得物收款账户。 | | 已完成首次导入 | 提现管理的每日店铺余额读取聚水潭“平台原始账单[新] → 店铺余额表”接口 JSON/XLSX 及多平台“平台资金账户余额”导出,进入 `jushuitan_shop_balance_snapshots`;已导入 2026-09-11 的淘系、拼多多和抖音共 164 条余额。页面按店铺名称精确匹配并强制使用平台消歧,未匹配和多匹配不会显示为零。 | | 已接入待首次同步 | 店铺营销账户余额的京东账户读取 `js_data_middle_platform.dbo.jd_jzt_finance_records` 镜像 `jsm_jd_jzt_finance_records`;按 `id` 稳定断点、`updated_at` 重叠增量,页面不直连上游。 | | 本次接入 | 普通商品资料的工作平台采购价格接入 `jiansun.dbo.pm_priceAdjustment`(镜像 `js_pm_price_adjustment`):按 `skuCode` 关联 `jst_sku.sku_id`,取 `effectTime` 不晚于当前时间的最新 `purchasePrice`,并在页面和导出中标记“工作平台价格”;源表没有可靠更新时间,按 `Id` 稳定键追加,首次全量完成前只显示普通商品自身采购价。 | diff --git a/modules/EcommerceFinance/Http/Controllers/AccountBills.php b/modules/EcommerceFinance/Http/Controllers/AccountBills.php index d7067d5f..aa9df058 100644 --- a/modules/EcommerceFinance/Http/Controllers/AccountBills.php +++ b/modules/EcommerceFinance/Http/Controllers/AccountBills.php @@ -8,6 +8,7 @@ use Illuminate\Routing\Controller; use Modules\EcommerceFinance\AccountBillColumns; use Modules\EcommerceFinance\Models\AccountBill; use Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository; +use Modules\EcommerceFinance\Repositories\StoreReceivingDewuBillMirrorRepository; use Modules\EcommerceFinance\Repositories\StoreReceivingPinduoduoBillMirrorRepository; use RuntimeException; @@ -28,7 +29,7 @@ class AccountBills extends Controller abort(404, '未知的账户类型。'); } - if (in_array($billType, ['douyin', 'pinduoduo'], true)) { + if (in_array($billType, ['douyin', 'pinduoduo', 'dewu'], true)) { return $request->boolean('export') ? $this->export($request, $billType) : $this->mirrorIndex($request, $billType); @@ -117,7 +118,7 @@ class AccountBills extends Controller abort_unless(AccountBillColumns::has($bill_type), 404, '未知的账户类型。'); - if (in_array($bill_type, ['douyin', 'pinduoduo'], true)) { + if (in_array($bill_type, ['douyin', 'pinduoduo', 'dewu'], true)) { return $this->exportMirror($request, $bill_type); } @@ -141,7 +142,7 @@ class AccountBills extends Controller { $this->authorizeStoreReceivingBills(); - abort_unless(in_array($bill_type, ['douyin', 'pinduoduo'], true), 404, '该账户类型没有可用的筛选选项。'); + abort_unless(in_array($bill_type, ['douyin', 'pinduoduo', 'dewu'], true), 404, '该账户类型没有可用的筛选选项。'); $label = $this->mirrorLabel($bill_type); @@ -362,14 +363,20 @@ class AccountBills extends Controller private function mirrorRepository(string $billType): object { - return $billType === 'pinduoduo' - ? app(StoreReceivingPinduoduoBillMirrorRepository::class) - : app(StoreReceivingDouyinBillMirrorRepository::class); + return match ($billType) { + 'pinduoduo' => app(StoreReceivingPinduoduoBillMirrorRepository::class), + 'dewu' => app(StoreReceivingDewuBillMirrorRepository::class), + default => app(StoreReceivingDouyinBillMirrorRepository::class), + }; } private function mirrorLabel(string $billType): string { - return $billType === 'pinduoduo' ? '拼多多' : '抖音'; + return match ($billType) { + 'pinduoduo' => '拼多多', + 'dewu' => '得物', + default => '抖音', + }; } private function sort(Request $request): string diff --git a/modules/EcommerceFinance/Repositories/StoreReceivingAlipayBillMirrorRepository.php b/modules/EcommerceFinance/Repositories/StoreReceivingAlipayBillMirrorRepository.php index 1080b831..de4dcffc 100644 --- a/modules/EcommerceFinance/Repositories/StoreReceivingAlipayBillMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/StoreReceivingAlipayBillMirrorRepository.php @@ -58,7 +58,7 @@ class StoreReceivingAlipayBillMirrorRepository $perPage = min(max($perPage, 1), 100); $direction = $direction === 'asc' ? 'asc' : 'desc'; $sortColumn = [ - 'record_code' => $mapping === 'pinduoduo' ? 'b.auto_id' : 'b.Id', + 'record_code' => in_array($mapping, ['pinduoduo', 'dewu'], true) ? 'b.auto_id' : 'b.Id', 'occurred_at' => 'b.fee_time', 'income' => 'b.amount', 'expense' => 'b.amount', @@ -103,7 +103,7 @@ class StoreReceivingAlipayBillMirrorRepository 'summary' => [ 'income' => $income, 'expense' => $expense, - 'balance' => $income - $expense, + 'balance' => round($income - $expense, 2), 'count' => $total, ], 'sync' => [ @@ -146,7 +146,7 @@ class StoreReceivingAlipayBillMirrorRepository $shopNames = $this->allShopNames(); $direction = $direction === 'desc' ? 'desc' : 'asc'; $sortColumn = [ - 'record_code' => $mapping === 'pinduoduo' ? 'b.auto_id' : 'b.Id', + 'record_code' => in_array($mapping, ['pinduoduo', 'dewu'], true) ? 'b.auto_id' : 'b.Id', 'occurred_at' => 'b.fee_time', 'income' => 'b.amount', 'expense' => 'b.amount', @@ -491,7 +491,7 @@ class StoreReceivingAlipayBillMirrorRepository 'occurred_at' => $this->dateTime($row->fee_time), 'income' => $income, 'expense' => $expense, - 'balance' => $income - $expense, + 'balance' => round($income - $expense, 2), 'business_type' => $this->directionName($row->inout_type), 'remark' => null, 'online_order_no' => $row->so_id, @@ -512,6 +512,10 @@ class StoreReceivingAlipayBillMirrorRepository return $this->mapPinduoduoRow($row, $shopName); } + if ($mapping === 'dewu') { + return $this->mapDewuRow($row, $shopName); + } + if ($mapping !== 'douyin') { return $this->mapRow($row, $shopName); } @@ -533,7 +537,7 @@ class StoreReceivingAlipayBillMirrorRepository 'occurred_at' => $this->dateTime($row->fee_time), 'income' => $income, 'expense' => $expense, - 'balance' => $income - $expense, + 'balance' => round($income - $expense, 2), 'remark' => null, 'modifier' => '聚水潭', 'created_at' => $this->dateTime($row->createTime), @@ -576,7 +580,7 @@ class StoreReceivingAlipayBillMirrorRepository 'occurred_at' => $this->dateTime($row->fee_time), 'income' => $income, 'expense' => $expense, - 'balance' => $income - $expense, + 'balance' => round($income - $expense, 2), 'remark' => null, 'updated_at' => $this->dateTime($row->updateTime), 'modifier' => 'RPA', @@ -589,6 +593,50 @@ class StoreReceivingAlipayBillMirrorRepository ]; } + /** @return array */ + private function mapDewuRow(object $row, ?string $shopName): array + { + $amount = abs((float) ($row->amount ?? 0)); + $income = (string) ($row->inout_type ?? '') === '1' ? $amount : 0.0; + $expense = (string) ($row->inout_type ?? '') === '-1' ? $amount : 0.0; + $orderNo = $row->so_id ?: $row->p_auto_id; + $goodsNo = $row->sku_id ?: $row->shop_sku_id ?: $row->shop_i_id; + + return [ + 'id' => (string) $row->Id, + 'record_code' => (string) ($row->auto_id ?: $row->Id), + 'store_name' => $shopName, + 'finance_remark' => null, + 'order_no' => $orderNo, + 'amount' => $amount, + 'custom_remark' => null, + 'bill_item' => $row->fee_name, + 'occurred_at' => $this->dateTime($row->fee_time), + 'income' => $income, + 'expense' => $expense, + 'balance' => round($income - $expense, 2), + 'remark' => null, + 'updated_at' => $this->dateTime($row->updateTime), + 'modifier' => '聚水潭', + 'payload' => [ + 'online_order_no' => $orderNo, + 'store_2' => $shopName, + 'order_type' => null, + 'goods_name' => null, + 'goods_no' => $goodsNo, + 'quantity' => null, + 'channel' => $row->channel, + 'business_type' => $this->directionName($row->inout_type), + 'online_real_store' => $shopName, + 'order_created_at' => null, + 'ship_return_at' => null, + 'settle_status' => null, + 'pay_period' => null, + 'pulled_at' => $this->dateTime($row->createTime), + ], + ]; + } + private function directionCode(string $value): string { return match ($value) { diff --git a/modules/EcommerceFinance/Repositories/StoreReceivingDewuBillMirrorRepository.php b/modules/EcommerceFinance/Repositories/StoreReceivingDewuBillMirrorRepository.php new file mode 100644 index 00000000..1af74db8 --- /dev/null +++ b/modules/EcommerceFinance/Repositories/StoreReceivingDewuBillMirrorRepository.php @@ -0,0 +1,49 @@ + */ + public function paginate(array $filters, string $sort, string $direction, int $page, int $perPage): array + { + return $this->source->paginateChannel( + $filters, + $sort, + $direction, + $page, + $perPage, + self::SOURCE_CHANNEL, + 'dewu', + self::DATASET, + ); + } + + /** @return iterable> */ + public function exportRows(array $filters, string $sort = 'occurred_at', string $direction = 'asc'): iterable + { + return $this->source->exportChannelRows( + $filters, + $sort, + $direction, + self::SOURCE_CHANNEL, + 'dewu', + self::DATASET, + ); + } + + /** @return array{shops:array,bill_items:array} */ + public function filterOptions(): array + { + return $this->source->filterOptionsForChannel(self::SOURCE_CHANNEL, self::DATASET); + } +} diff --git a/resources/assets/js/components/business/AccountBills.vue b/resources/assets/js/components/business/AccountBills.vue index b94daeac..11f32e02 100644 --- a/resources/assets/js/components/business/AccountBills.vue +++ b/resources/assets/js/components/business/AccountBills.vue @@ -59,7 +59,7 @@ export default { data(){return{loading:false,loadError:false,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:20,filters:{store_name:'',bill_item:'',start:'',end:'',keyword:''},filterOptions:{shops:[],bill_items:[]},filterOptionsLoading:false,filterOptionsError:false,requestSerial:0,sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible'};}, computed:{ isDouyin(){return this.billType==='douyin';}, - usesMirrorOptions(){return ['douyin','pinduoduo'].indexOf(this.billType)!==-1;}, + usesMirrorOptions(){return ['douyin','pinduoduo','dewu'].indexOf(this.billType)!==-1;}, invalidDateRange(){return Boolean(this.filters.start&&this.filters.end&&this.filters.start>this.filters.end);}, visibleColumns(){return this.configuredColumns.filter(column=>column.visible);}, visibleGroups(){return this.visibleColumns.reduce((groups,column)=>{if(!column.group)return groups;const last=groups[groups.length-1];if(last&&last.label===column.group)last.span+=1;else groups.push({label:column.group,span:1});return groups;},[]);}, diff --git a/tests/Feature/Business/DewuReceivingBillsTest.php b/tests/Feature/Business/DewuReceivingBillsTest.php new file mode 100644 index 00000000..4f7de9fc --- /dev/null +++ b/tests/Feature/Business/DewuReceivingBillsTest.php @@ -0,0 +1,124 @@ +allowPage(); + $filters = [ + 'store_name' => '得物-测试店', + 'bill_item' => '交易收入', + 'start' => '2026-08-01', + 'end' => '2026-08-31', + 'keyword' => '110211', + ]; + $repository = Mockery::mock(StoreReceivingDewuBillMirrorRepository::class); + $repository->shouldReceive('paginate')->once()->with( + $filters, 'occurred_at', 'desc', 2, 20, + )->andReturn([ + 'current_page' => 2, + 'last_page' => 3, + 'per_page' => 20, + 'total' => 41, + 'data' => [[ + 'id' => '14654480', + 'record_code' => '2000000001248775179', + 'store_name' => '得物-测试店', + 'income' => 21.0, + 'expense' => 0.0, + 'payload' => [ + 'online_order_no' => '110211311661075313', + 'channel' => 'Dewu', + ], + ]], + 'summary' => ['income' => 21.0, 'expense' => 0.0, 'balance' => 21.0, 'count' => 41], + ]); + $this->app->instance(StoreReceivingDewuBillMirrorRepository::class, $repository); + + $this->withExceptionHandling() + ->loginAs() + ->getJson(route('ecommerce-finance.account-bills.index', [ + 'bill_type' => 'dewu', + ...$filters, + 'page' => 2, + 'limit' => 20, + ])) + ->assertOk() + ->assertJsonPath('data.0.payload.channel', 'Dewu') + ->assertJsonPath('columns.title', '得物账户') + ->assertJsonPath('summary.count', 41); + } + + public function test_filter_options_and_selected_column_export_use_the_same_mirror(): void + { + $this->allowPage(); + $repository = Mockery::mock(StoreReceivingDewuBillMirrorRepository::class); + $repository->shouldReceive('filterOptions')->once()->andReturn([ + 'shops' => ['得物-A店', '得物-B店'], + 'bill_items' => ['交易收入', '平台服务费'], + ]); + $repository->shouldReceive('exportRows')->once()->with( + ['store_name' => '得物-A店'], 'record_code', 'asc', + )->andReturn([[ + 'record_code' => '2000000001248775179', + 'store_name' => '得物-A店', + 'payload' => ['channel' => 'Dewu'], + ]]); + $this->app->instance(StoreReceivingDewuBillMirrorRepository::class, $repository); + + $this->loginAs() + ->withHeader('X-Requested-With', 'XMLHttpRequest') + ->get(route('ecommerce-finance.account-bills.filter-options', ['bill_type' => 'dewu'])) + ->assertOk() + ->assertExactJson([ + 'shops' => ['得物-A店', '得物-B店'], + 'bill_items' => ['交易收入', '平台服务费'], + ]); + + $response = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [ + 'bill_type' => 'dewu', + 'store_name' => '得物-A店', + 'sort' => 'record_code', + 'dir' => 'asc', + 'columns' => 'payload.channel,record_code,store_name', + ])); + + $response->assertOk() + ->assertHeader('content-type', 'text/csv; charset=UTF-8') + ->assertDownload('dewu_account_bills.csv'); + $csv = $response->streamedContent(); + $this->assertStringContainsString('渠道,记录编码,店铺', $csv); + $this->assertStringContainsString('Dewu,"=""2000000001248775179""",得物-A店', $csv); + } + + public function test_unready_mirror_returns_a_clear_retryable_response(): void + { + $this->allowPage(); + $repository = Mockery::mock(StoreReceivingDewuBillMirrorRepository::class); + $repository->shouldReceive('paginate')->once()->andThrow( + new RuntimeException('store_receiving_alipay_bill_mirror_not_ready') + ); + $this->app->instance(StoreReceivingDewuBillMirrorRepository::class, $repository); + + $this->loginAs() + ->withHeader('X-Requested-With', 'XMLHttpRequest') + ->get(route('ecommerce-finance.account-bills.index', ['bill_type' => 'dewu'])) + ->assertStatus(503) + ->assertJsonPath('message', '得物账户账单共享业务镜像正在首次同步,完成后将自动显示。'); + } + + private function allowPage(): void + { + $this->user->attachPermission( + Permission::where('name', 'read-admin-panel')->firstOrFail() + ); + } +} diff --git a/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php b/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php index 7861c781..a92cb530 100644 --- a/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php +++ b/tests/Unit/EcommerceFinance/BusinessMirrorSynchronizerTest.php @@ -483,6 +483,12 @@ class BusinessMirrorSynchronizerTest extends TestCase $this->assertSame('jst_originalBillInfo', $pinduoduo->sourceObject()); $this->assertSame('js_jst_pdd_receiving_bill_rows', $pinduoduo->targetTable()); $this->assertSame(['channel' => 'PinduoduoImport'], $pinduoduo->fixedFilters()); + + $dewu = $datasets['jiansun.dbo.jst_originalBillInfo.dewu_receiving']; + $this->assertSame('jiansun', $dewu->sourceKey()); + $this->assertSame('jst_originalBillInfo', $dewu->sourceObject()); + $this->assertSame('js_jst_dewu_receiving_bill_rows', $dewu->targetTable()); + $this->assertSame(['channel' => 'Dewu'], $dewu->fixedFilters()); } /** @return array */ diff --git a/tests/Unit/EcommerceFinance/StoreReceivingDewuBillMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/StoreReceivingDewuBillMirrorRepositoryTest.php new file mode 100644 index 00000000..e97f98c8 --- /dev/null +++ b/tests/Unit/EcommerceFinance/StoreReceivingDewuBillMirrorRepositoryTest.php @@ -0,0 +1,189 @@ + [ + 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', + ], + 'upstream_mirrors.connection' => 'business_mirror', + 'upstream_mirrors.datasets' => [ + StoreReceivingDewuBillMirrorRepository::DATASET => [ + 'source_key' => 'jiansun', 'schema' => 'dbo', + 'object' => 'jst_originalBillInfo', 'target' => 'js_jst_dewu_receiving_bill_rows', + 'keys' => ['Id'], + 'fixed_filters' => ['channel' => StoreReceivingDewuBillMirrorRepository::SOURCE_CHANNEL], + ], + StoreReceivingAlipayBillMirrorRepository::SHOP_DATASET => [ + 'source_key' => 'operations_replica', 'schema' => 'rpt', + 'object' => 'jst_shopInfo', 'target' => 'ops_jst_shop_info', 'keys' => ['Id'], + ], + ], + ]); + DB::purge('business_mirror'); + $schema = Schema::connection('business_mirror'); + $schema->create('business_mirror_table_states', function (Blueprint $table): void { + $table->string('dataset_key')->primary(); + $table->dateTime('initial_completed_at')->nullable(); + $table->string('status')->nullable(); + $table->dateTime('last_succeeded_at')->nullable(); + $table->unsignedBigInteger('mirror_row_count')->nullable(); + $table->boolean('mirror_row_count_is_exact')->default(false); + }); + $schema->create('js_jst_dewu_receiving_bill_rows', function (Blueprint $table): void { + $table->integer('Id'); + $table->integer('shop_id')->nullable(); + foreach (['channel', 'primary_key', 'fee_name', 'inout_type', 'p_auto_id', 'shop_i_id', 'shop_sku_id', 'sku_id', 'so_id', 'combine_type'] as $column) { + $table->string($column)->nullable(); + } + $table->dateTime('fee_time')->nullable(); + $table->dateTime('createTime')->nullable(); + $table->dateTime('updateTime')->nullable(); + $table->bigInteger('auto_id')->nullable(); + $table->decimal('amount', 20, 6)->nullable(); + }); + $schema->create('ops_jst_shop_info', function (Blueprint $table): void { + $table->integer('Id'); + $table->integer('shop_id')->nullable(); + $table->string('shop_name')->nullable(); + }); + + $source = new StoreReceivingAlipayBillMirrorRepository( + new BusinessMirrorDatasetRegistry(), new GenericBusinessMirrorRepository(app('db')), + ); + $this->repository = new StoreReceivingDewuBillMirrorRepository($source); + $this->markReady(); + } + + public function test_it_reads_only_the_dewu_dataset_and_maps_available_source_fields(): void + { + DB::connection('business_mirror')->table('ops_jst_shop_info')->insert([ + ['Id' => 10, 'shop_id' => 7, 'shop_name' => '得物-测试店'], + ['Id' => 11, 'shop_id' => 8, 'shop_name' => '其他店'], + ]); + DB::connection('business_mirror')->table('js_jst_dewu_receiving_bill_rows')->insert([ + $this->row(1, 7, '2026-09-13 18:48:12', '1', 21.00, '交易收入'), + $this->row(2, 7, '2026-09-13 19:00:00', '-1', 18.07, '平台服务费'), + ]); + + $result = $this->repository->paginate([], 'occurred_at', 'desc', 1, 10); + + $this->assertSame(2, $result['total']); + $this->assertSame('200000000000000002', $result['data'][0]['record_code']); + $this->assertSame('得物-测试店', $result['data'][0]['store_name']); + $this->assertSame('110211-ORDER-2', $result['data'][0]['order_no']); + $this->assertSame('Dewu', $result['data'][0]['payload']['channel']); + $this->assertSame('支出', $result['data'][0]['payload']['business_type']); + $this->assertSame('SKU-2', $result['data'][0]['payload']['goods_no']); + $this->assertSame('聚水潭', $result['data'][0]['modifier']); + $this->assertSame(21.0, $result['summary']['income']); + $this->assertSame(18.07, $result['summary']['expense']); + $this->assertSame(2.93, $result['summary']['balance']); + } + + public function test_filters_options_export_and_exact_total_use_the_fixed_channel_slice(): void + { + DB::connection('business_mirror')->table('ops_jst_shop_info')->insert([ + ['Id' => 10, 'shop_id' => 7, 'shop_name' => '得物-A店'], + ['Id' => 11, 'shop_id' => 8, 'shop_name' => '得物-B店'], + ]); + DB::connection('business_mirror')->table('js_jst_dewu_receiving_bill_rows')->insert([ + $this->row(1, 7, '2026-08-12 09:00:00', '1', 22.80, '交易收入'), + $this->row(2, 8, '2026-08-13 09:00:00', '-1', 0.14, '平台服务费'), + ]); + $connection = DB::connection('business_mirror'); + $connection->table('business_mirror_table_states') + ->where('dataset_key', StoreReceivingDewuBillMirrorRepository::DATASET) + ->update(['mirror_row_count' => 2, 'mirror_row_count_is_exact' => true]); + $connection->enableQueryLog(); + $connection->flushQueryLog(); + + $unfiltered = $this->repository->paginate([], 'record_code', 'asc', 1, 10); + $filtered = $this->repository->paginate([ + 'store_name' => 'A店', + 'bill_item' => '交易', + 'start' => '2026-08-12', + 'end' => '2026-08-12', + 'keyword' => '110211-ORDER-1', + ], 'record_code', 'asc', 1, 10); + $options = $this->repository->filterOptions(); + $export = iterator_to_array($this->repository->exportRows(['bill_item' => '平台服务费'])); + + $this->assertSame(2, $unfiltered['total']); + $this->assertSame(1, $filtered['total']); + $this->assertSame('200000000000000001', $filtered['data'][0]['record_code']); + $this->assertSame(['得物-A店', '得物-B店'], $options['shops']); + $this->assertSame(['交易收入', '平台服务费'], $options['bill_items']); + $this->assertCount(1, $export); + $this->assertSame('200000000000000002', $export[0]['record_code']); + + $targetQueries = array_filter( + $connection->getQueryLog(), + static fn (array $query): bool => str_contains($query['query'], 'js_jst_dewu_receiving_bill_rows'), + ); + $this->assertNotEmpty($targetQueries); + $this->assertCount(0, array_filter( + $targetQueries, + static fn (array $query): bool => str_contains($query['query'], '"b"."channel" = ?'), + )); + $connection->disableQueryLog(); + } + + private function markReady(): void + { + foreach ([StoreReceivingDewuBillMirrorRepository::DATASET, StoreReceivingAlipayBillMirrorRepository::SHOP_DATASET] as $dataset) { + DB::connection('business_mirror')->table('business_mirror_table_states')->insert([ + 'dataset_key' => $dataset, + 'initial_completed_at' => '2026-09-14 09:00:00', + 'status' => 'succeeded', + 'last_succeeded_at' => '2026-09-14 09:00:00', + ]); + } + } + + private function row( + int $id, + int $shopId, + string $time, + string $direction, + float $amount, + string $feeName, + ): array { + return [ + 'Id' => $id, + 'shop_id' => $shopId, + 'channel' => 'Dewu', + 'primary_key' => 'KEY-' . $id, + 'fee_name' => $feeName, + 'inout_type' => $direction, + 'p_auto_id' => 'P-' . $id, + 'shop_i_id' => 'ITEM-' . $id, + 'shop_sku_id' => 'SHOP-SKU-' . $id, + 'sku_id' => 'SKU-' . $id, + 'so_id' => '110211-ORDER-' . $id, + 'combine_type' => 'normal', + 'fee_time' => $time, + 'createTime' => '2026-09-14 13:00:48', + 'updateTime' => '2026-09-14 13:02:32', + 'auto_id' => 200000000000000000 + $id, + 'amount' => $amount, + ]; + } +} diff --git a/tests/Unit/View/AccountBillsTableTest.php b/tests/Unit/View/AccountBillsTableTest.php index ff92c907..119f2d83 100644 --- a/tests/Unit/View/AccountBillsTableTest.php +++ b/tests/Unit/View/AccountBillsTableTest.php @@ -11,7 +11,7 @@ class AccountBillsTableTest extends TestCase $source = file_get_contents(__DIR__ . '/../../../resources/assets/js/components/business/AccountBills.vue'); $this->assertStringContainsString("isDouyin(){return this.billType==='douyin';}", $source); - $this->assertStringContainsString("['douyin','pinduoduo'].indexOf(this.billType)!==-1", $source); + $this->assertStringContainsString("['douyin','pinduoduo','dewu'].indexOf(this.billType)!==-1", $source); $this->assertStringContainsString('v-if="usesMirrorOptions && !filterOptionsError"', $source); $this->assertStringContainsString('page-key="store-receiving-bills"', $source); $this->assertStringContainsString(':tab-key="billType"', $source); -- 2.47.2