files as $file) { if (is_file($file)) { unlink($file); } } parent::tearDown(); } public function test_import_then_page_filter_and_export(): void { $this->allowPage(); $result = app(PlantingDetailImporter::class)->import($this->csvFixture(), $this->company->id, true, null, 'test-import'); $this->assertSame(2, $result['inserted']); $this->assertSame('2665.3600', $result['selling_price']); $this->assertSame('2665.8600', $result['payment_amount']); $this->assertSame('2026-06-01', $result['date_min']); $this->assertSame('2026-07-15', $result['date_max']); $page = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'planting-data', 'page' => 'planting-details', ])); $page->assertOk() ->assertSee('种菜明细表') ->assertSee('3306215028042824171') ->assertSee('260529-659019537933390') ->assertSee('2,665.35') ->assertSee('阿里巴巴-常州乐畅家居有限公司'); // 时间筛选是可用的日期区间控件:点整块开日历、单个清除、快捷区间按钮 $page->assertSee('id="planting-date-from"', false) ->assertSee('id="planting-date-to"', false) ->assertSee('data-planting-clear="planting-date-from"', false) ->assertSee('data-planting-range="this-month"', false) ->assertSee('showPicker', false) ->assertSee('addEventListener(\'change\'', false); // 按店铺 + 日期筛选 $filtered = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'planting-data', 'page' => 'planting-details', 'store' => '拼多多-亿家达实木家具官方旗舰店', 'date_from' => '2026-07-01', ])); $filtered->assertOk() ->assertSee('260529-659019537933390') ->assertDontSee('3306215028042824171'); // 导出:表头 + 行数与页面筛选一致 $export = $this->loginAs()->get(route('business.planting-data.planting-details.export', [ 'store' => '拼多多-亿家达实木家具官方旗舰店', ])); $export->assertOk()->assertDownload(); $csv = $export->streamedContent(); $this->assertStringContainsString('日期,订单编号,财务售价', explode("\n", $csv)[0]); $this->assertStringContainsString('260529-659019537933390', $csv); $this->assertStringNotContainsString('3306215028042824171', $csv); $this->assertStringContainsString('2026-07-15', $csv); } public function test_importer_handles_excel_serial_dates_and_grouped_amounts(): void { $path = $this->writeFile('planting.xlsx'); $book = new Spreadsheet(); $sheet = $book->getActiveSheet(); $sheet->fromArray([ ['日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款', '财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户', '类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID'], [46174, 'ORDER-1', '1,234.56', 0, '0.5', 0, 0, 0, '1,235.06', 2, '', '天猫-亿家达旗舰店', '仝孟欣', '', 'CCDD', '齐静春', 'CCDD', '运营中心A', '', 'ID-1'], // 无效日期(空)+ 无效订单号 → 跳过 ['', '', 10, 0, 0, 0, 0, 0, 10, 1, '', '天猫-亿家达旗舰店', '', '', '', '', '', '', '', ''], ]); (new Xlsx($book))->save($path); $book->disconnectWorksheets(); $result = app(PlantingDetailImporter::class)->import($path, $this->company->id, true, null, 'test-xlsx'); $this->assertSame(1, $result['inserted']); $this->assertSame(1, $result['skipped']); $this->assertSame('2026-06-01', $result['date_min']); $record = PlantingDetail::query()->where('company_id', $this->company->id)->firstOrFail(); $this->assertSame('2026-06-01', $record->bill_date->toDateString()); $this->assertSame('1234.5600', $record->finance_selling_price); $this->assertSame('1235.0600', $record->finance_payment_amount); $this->assertSame(2, $record->pieces); $this->assertSame('CCDD', $record->type); $this->assertSame('运营中心A', $record->profit_center); } /** 源表 ID 列存在「一个格子多个 ID、换行分隔」的行,导入要保留而不是报错截断。 */ public function test_importer_keeps_multi_line_external_ids(): void { $longId = implode("\n", ['10205993844217', '10205981846604', '10205993844217', '10210832117878']); $path = $this->writeFile('planting-multi-id.csv'); $handle = fopen($path, 'wb'); fputcsv($handle, ['日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款', '财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户', '类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID']); fputcsv($handle, ['2026-03-05', 'ORDER-9', '1356', '0', '0', '0', '0', '0', '1356', '1', '', '京喜-招财邸喜店工厂店', '李金林', '6228760405003956861', '', '骆冰', '', '运营中心A', '', $longId]); fclose($handle); $result = app(PlantingDetailImporter::class)->import($path, $this->company->id, true, null, 'test-multi-id'); $this->assertSame(1, $result['inserted']); $record = PlantingDetail::query()->where('company_id', $this->company->id)->firstOrFail(); $this->assertSame($longId, $record->external_id); } /** * 种菜报销工单同步:字段映射、日期=完成日-1、利润中心回填、重复执行不翻倍。 */ public function test_sync_from_work_orders_maps_fields_and_dedupes(): void { $this->allowPage(); $this->fakeWorkOrderMirror(); // 历史行提供 店铺 → 利润中心 映射 PlantingDetail::query()->create([ 'company_id' => $this->company->id, 'bill_date' => '2026-06-01', 'order_no' => 'HISTORY-1', 'store' => '天猫-亿家达旗舰店', 'profit_center' => '运营中心A', 'finance_payment_amount' => '1.0000', ]); // 本月里没有对应工单的人工行:默认不删 $manual = PlantingDetail::query()->create([ 'company_id' => $this->company->id, 'bill_date' => '2026-09-10', 'order_no' => 'MANUAL-ONLY', 'store' => '天猫-亿家达旗舰店', 'finance_payment_amount' => '9.0000', ]); $result = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id); // 只认「种菜报销 + 已完成 + 完成时间在 9/2~10/1」的两张工单 $this->assertSame(2, $result['orders']); $this->assertSame(2, $result['inserted']); $this->assertSame(0, $result['deleted']); $first = PlantingDetail::query()->where('company_id', $this->company->id) ->where('order_no', '3316392626048007769')->firstOrFail(); $this->assertSame('2026-09-01', $first->bill_date->toDateString()); // 完成日 9-02 → 日期 9-01 $this->assertSame('37.7900', $first->finance_selling_price); $this->assertSame('2.0000', $first->finance_commission); $this->assertSame('37.7900', $first->finance_payment_amount); $this->assertSame('天猫-亿家达旗舰店', $first->store); $this->assertSame('黄文梳', $first->owner); $this->assertSame('6214830551373771', $first->account); $this->assertSame('CCDD', $first->type); $this->assertSame('CCDD', $first->tag); $this->assertSame('黄文梳', $first->creator); $this->assertSame('1074872715022', $first->external_id); $this->assertSame('运营中心A', $first->profit_center); // 从历史行回填 $this->assertSame(1, $first->pieces); $this->assertSame(PlantingWorkOrderSynchronizer::SOURCE, $first->created_from); $second = PlantingDetail::query()->where('company_id', $this->company->id) ->where('order_no', '260901-000000000000001')->firstOrFail(); $this->assertSame('2026-09-29', $second->bill_date->toDateString()); // 完成日 9-30 // 店铺在历史里没有利润中心,用店铺维度报表的 ERP 店铺资料补齐 $this->assertSame('运营中心G', $second->profit_center); $this->assertSame(1, $result['profit_center_from_erp']); // 再跑一遍:同月同订单号先删后写,不会翻倍 $again = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id); $this->assertSame(2, $again['deleted']); $this->assertSame(2, $again['inserted']); $this->assertSame(1, PlantingDetail::query()->where('company_id', $this->company->id) ->where('order_no', '3316392626048007769')->count()); $this->assertSame(1, PlantingDetail::query()->where('company_id', $this->company->id) ->whereKey($manual->id)->count()); // dry-run 不写库 $dry = app(PlantingWorkOrderSynchronizer::class)->sync('2026-09-01', '2026-09-30', $this->company->id, true); $this->assertTrue($dry['dry_run']); $this->assertSame(2, $dry['orders']); $this->assertSame(0, $dry['inserted']); } public function test_sync_force_clear_replaces_whole_month(): void { $this->allowPage(); $this->fakeWorkOrderMirror(); PlantingDetail::query()->create([ 'company_id' => $this->company->id, 'bill_date' => '2026-09-10', 'order_no' => 'MANUAL-ONLY', 'finance_payment_amount' => '9.0000', ]); $result = app(PlantingWorkOrderSynchronizer::class) ->sync('2026-09-01', '2026-09-30', $this->company->id, false, true); $this->assertSame(1, $result['deleted']); $this->assertSame(2, $result['inserted']); $this->assertSame(0, PlantingDetail::query()->where('company_id', $this->company->id) ->where('order_no', 'MANUAL-ONLY')->count()); $this->assertSame(2, PlantingDetail::query()->where('company_id', $this->company->id)->count()); } /** 造一个只有聚水潭协同工单镜像的 sqlite 业务镜像连接。 */ private function fakeWorkOrderMirror(): void { config(['upstream_mirrors.connection' => 'business_mirror']); config(['database.connections.business_mirror' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]]); DB::purge('business_mirror'); Schema::connection('business_mirror')->create('js_jst_work_order_info', function ($table): void { $table->increments('Id'); $table->string('woNo')->nullable(); $table->string('woTypeId')->nullable(); $table->string('status')->nullable(); $table->string('createby')->nullable(); $table->string('completeTime')->nullable(); $table->text('businessFieldList')->nullable(); }); // 店铺维度报表用的 ERP 店铺资料:只给第二张工单的店铺,用来验证「历史没有就用 ERP 补」 Schema::connection('business_mirror')->create('ops_fys_shop_info', function ($table): void { $table->increments('Id'); $table->string('erpShopName')->nullable(); $table->string('responsibleDepartment')->nullable(); }); DB::connection('business_mirror')->table('ops_fys_shop_info')->insert([ ['erpShopName' => '拼多多-归来舍下住宅家具旗舰店', 'responsibleDepartment' => '运营中心G'], ]); foreach ($this->workOrderFixtures() as $row) { DB::connection('business_mirror')->table('js_jst_work_order_info')->insert($row); } } /** @return list> */ private function workOrderFixtures(): array { $planting = PlantingWorkOrderSynchronizer::WORK_TYPE_ID; $fields = [ ['fieldId' => 'orderField', 'type' => 'TEXT', 'value' => '3316392626048007769'], ['fieldId' => 'shopField', 'type' => 'SELECT', 'value' => '18501511', 'valueLabel' => '天猫-亿家达旗舰店'], ['fieldId' => 'NUMBERField1413856620250924131615', 'type' => 'NUMBER', 'value' => '37.79'], ['fieldId' => 'NUMBERField1413856620251230154457', 'type' => 'NUMBER', 'value' => '2'], ['fieldId' => 'NUMBERField1413856620251117101250', 'type' => 'NUMBER', 'value' => '37.79'], ['fieldId' => 'TEXTField1413856620251117180931', 'type' => 'TEXT', 'value' => '黄文梳'], ['fieldId' => 'TEXTField1413856620251117113451', 'type' => 'TEXT', 'value' => '6214830551373771'], ['fieldId' => 'TEXTField1413856620251113150546', 'type' => 'TEXT', 'value' => 'CCDD'], ['fieldId' => 'businessRemarkField', 'type' => 'TEXTAREA', 'value' => 'CCDD'], ['fieldId' => 'itemInfo', 'type' => 'FORM', 'childFieldList' => [[ ['fieldId' => 'erpItemId', 'type' => 'TEXT', 'value' => '11961631'], ['fieldId' => 'platformItemId', 'type' => 'TEXT', 'value' => '1074872715022'], ]]], ]; $plain = [ ['fieldId' => 'orderField', 'type' => 'TEXT', 'value' => '260901-000000000000001'], ['fieldId' => 'shopField', 'type' => 'SELECT', 'value' => '21355501', 'valueLabel' => '拼多多-归来舍下住宅家具旗舰店'], ['fieldId' => 'NUMBERField1413856620250924131615', 'type' => 'NUMBER', 'value' => '0.06'], ['fieldId' => 'NUMBERField1413856620251230154457', 'type' => 'NUMBER', 'value' => '0.5'], ['fieldId' => 'NUMBERField1413856620251117101250', 'type' => 'NUMBER', 'value' => '0.56'], ]; return [ [ 'woNo' => 'GD565967381', 'woTypeId' => $planting, 'status' => 'CONFIRMED', 'createby' => '黄文梳', 'completeTime' => '2026-09-02 11:47:53', 'businessFieldList' => json_encode($fields, JSON_UNESCAPED_UNICODE), ], [ 'woNo' => 'GD999999999', 'woTypeId' => $planting, 'status' => 'CONFIRMED', 'createby' => '陆小凤', 'completeTime' => '2026-09-30 09:27:29', 'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE), ], // 未完成 → 忽略 [ 'woNo' => 'GD888888888', 'woTypeId' => $planting, 'status' => 'WAIT_CONFIRM', 'createby' => '陆小凤', 'completeTime' => '2026-09-15 09:27:29', 'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE), ], // 别的工单类型 → 忽略 [ 'woNo' => 'GD777777777', 'woTypeId' => 'ORDER_AFSALES', 'status' => 'CONFIRMED', 'createby' => '关雎', 'completeTime' => '2026-09-15 09:27:29', 'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE), ], // 完成时间 10/2 → 日期 10/1,不在 9 月窗口 → 忽略 [ 'woNo' => 'GD666666666', 'woTypeId' => $planting, 'status' => 'CONFIRMED', 'createby' => '陆小凤', 'completeTime' => '2026-10-02 09:27:29', 'businessFieldList' => json_encode($plain, JSON_UNESCAPED_UNICODE), ], ]; } private function csvFixture(): string { $path = $this->writeFile('planting.csv'); $rows = [ ['日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款', '财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户', '类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID'], ['2026-06-01', '3306215028042824171', '2665.35', '0', '0', '0', '0', '0', '2665.35', '1', '', '阿里巴巴-常州乐畅家居有限公司', '叶颖龙', '6222031202029761854', '', '勇武', 'AABB', '运营中心B', '', '1056008452997'], ['2026-07-15', '260529-659019537933390', '0.01', '0', '0.5', '0', '0', '0', '0.51', '1', '', '拼多多-亿家达实木家具官方旗舰店', '屠锦超', '6222031211003375839', 'SSDD', '玄鸟', 'SSDD', '运营中心G', '', '959767499187'], ]; $handle = fopen($path, 'wb'); foreach ($rows as $row) { fputcsv($handle, $row); } fclose($handle); return $path; } private function writeFile(string $name): string { $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('planting-', true) . '-' . $name; $this->files[] = $path; return $path; } private function allowPage(): void { $this->user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } }