CaiWuXiTong/tests/Feature/Business/JstOrderLabelsPageTest.php
qiuxiang daa4c939e7 聚水潭订单标签明细:支持按聚水潭导出的订单 Excel 导入,并放开筛选下拉上限
镜像 ops_jst_orders 只覆盖 2026-04-01 之后,缺的历史区间只能靠财务在聚水潭「导出订单」导文件补。所以:- 新增 JstOrderExportReader:ZipArchive+expat 流式解析聚水潭导出 xlsx。这类文件字符串走 inlineStr、dimension 只写 A1、单文件上万行 ×135 列(解压 78MB),常规读法会读成空表或吃满内存;实测 12286 行 3.5 秒、峰值内存 10MB。
- jst-order-labels:import 新增 --file=:按「线上订单号+标签」去重后拆行写入,可配合 --from/--to 过滤窗口;带 --replace 时先成功读到数据行再清区间,避免文件有问题却先删了线上数据。
- 筛选下拉上限 500 -> 5000:导入 4~7 月后店铺已达 604 个,原来下拉里会缺 104 个店铺;提交筛选值的截断上限同步放开。
- 补两个测试:导出文件去重拆行、窗口过滤+replace。
2026-10-07 16:53:55 +08:00

233 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Business;
use App\Business\JstOrderLabelImporter;
use App\Models\Auth\Permission;
use App\Models\Business\JstOrderLabel;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use Tests\Feature\FeatureTestCase;
/**
* 账单流水数据 › 聚水潭订单标签明细(按标签拆行导入 + 页面 + 筛选 + 导出)。
*/
class JstOrderLabelsPageTest extends FeatureTestCase
{
protected function setUp(): void
{
parent::setUp();
config(['database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]]);
DB::purge('business_mirror');
Schema::connection('business_mirror')->create('ops_jst_orders', function ($table): void {
$table->integer('Id')->primary();
$table->string('so_id')->nullable();
$table->string('shop_name')->nullable();
$table->dateTime('order_date')->nullable();
$table->text('labels')->nullable();
});
DB::connection('business_mirror')->table('ops_jst_orders')->insert([
['Id' => 1, 'so_id' => '260901-391328633273048', 'shop_name' => '拼多多-优乐猫家具旗舰店', 'order_date' => '2026-09-01 00:00:13', 'labels' => '发货前售后,店铺-拼多多'],
['Id' => 2, 'so_id' => '3608400015164657', 'shop_name' => '京喜-久伴吾家具京喜自营工厂店', 'order_date' => '2026-09-01 00:00:44', 'labels' => '京东供销订单, 京喜'],
['Id' => 3, 'so_id' => '110211297173802010', 'shop_name' => '得物-全米', 'order_date' => '2026-08-31 23:59:00', 'labels' => '品牌直发'],
['Id' => 4, 'so_id' => '260915-000000000000001', 'shop_name' => '得物-全米', 'order_date' => '2026-09-15 10:00:00', 'labels' => ''],
]);
}
public function test_import_splits_labels_without_deduping_orders(): void
{
$this->allowPage();
$result = app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true);
$this->assertSame(3, $result['orders']);
$this->assertSame(5, $result['rows']);
$this->assertSame(1, $result['no_label_orders']);
$rows = JstOrderLabel::query()->where('company_id', $this->company->id)->orderBy('id')->get();
$this->assertSame(['发货前售后', '店铺-拼多多', '京东供销订单', '京喜', '(无标签)'], $rows->pluck('label')->all());
// 同一订单拆成多行,订单不去重
$this->assertSame(2, $rows->where('order_no', '260901-391328633273048')->count());
$this->assertSame('2026-09-15 10:00:00', $rows->last()->ordered_at->format('Y-m-d H:i:s'));
}
public function test_import_window_excludes_out_of_range_orders(): void
{
$this->allowPage();
// 8-31 的订单不在 9 月窗口内
$result = app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true);
$orders = JstOrderLabel::query()->where('company_id', $this->company->id)->pluck('order_no')->unique()->all();
$this->assertNotContains('110211297173802010', $orders);
$this->assertSame(3, $result['orders']);
}
public function test_page_filters_and_export(): void
{
$this->allowPage();
app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true);
$page = $this->loginAs()->get(route('business.pages.show', [
'section' => 'planting-data',
'page' => 'jst-order-labels',
]));
$page->assertOk()
->assertSee('聚水潭订单标签明细')
->assertSee('260901-391328633273048')
->assertSee('店铺-拼多多')
->assertSee('(无标签)')
->assertSee('id="jst-label-date-from"', false)
->assertSee('data-jst-labels-clear="jst-label-date-to"', false);
$filtered = $this->loginAs()->get(route('business.pages.show', [
'section' => 'planting-data',
'page' => 'jst-order-labels',
'label' => '京喜',
]));
$filtered->assertOk()
->assertSee('3608400015164657')
->assertDontSee('260901-391328633273048');
$export = $this->loginAs()->get(route('business.planting-data.jst-order-labels.export', [
'label' => '京喜',
]));
$export->assertOk()->assertDownload();
$csv = $export->streamedContent();
$this->assertStringContainsString('线上订单号,店铺名称,下单时间,标签', explode("\n", $csv)[0]);
$this->assertStringContainsString('3608400015164657', $csv);
$this->assertStringNotContainsString('260901-391328633273048', $csv);
}
private function allowPage(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
public function test_import_from_erp_export_file_dedupes_lines_and_labels(): void
{
$this->allowPage();
$path = $this->makeExportFile([
['内部订单号', '线上订单号', '店铺名称', '下单时间', '标签'],
['6476896', '3424493003753167', '京东-柯诺苏家具旗舰店', '2026-03-01 01:30:52', '发货后售后,店铺-京东,晚发赔'],
['6476897', '3424493003753167', '京东-柯诺苏家具旗舰店', '2026-03-01 01:30:52', '发货后售后,店铺-京东,晚发赔'],
['6476898', '3424493003753167', '京东-柯诺苏家具旗舰店', '2026-03-01 01:30:52', '发货后售后'],
['6477350', '110197275064171099', '得物-米客米家家居旗舰店', '2026-03-01 08:05:36', ''],
['9535498', '3290154852795260451', '淘宝-亿家达家居鞋柜电脑桌', '2026-03-11 19:01:30', '历史订单'],
]);
try {
$result = app(JstOrderLabelImporter::class)->importFromFile($path, null, null, $this->company->id, false);
} finally {
@unlink($path);
}
// 同一订单的多条明细行只算一单,重复标签只写一行
$this->assertSame(3, $result['orders']);
$this->assertSame(5, $result['rows']);
$this->assertSame(1, $result['no_label_orders']);
$labels = JstOrderLabel::query()->where('company_id', $this->company->id)
->where('order_no', '3424493003753167')->pluck('label')->sort()->values()->all();
$this->assertSame(['发货后售后', '店铺-京东', '晚发赔'], $labels);
$noLabel = JstOrderLabel::query()->where('company_id', $this->company->id)
->where('order_no', '110197275064171099')->firstOrFail();
$this->assertSame(JstOrderLabelImporter::NO_LABEL, $noLabel->label);
$this->assertSame('得物-米客米家家居旗舰店', $noLabel->shop_name);
}
public function test_import_from_file_respects_window_and_replace(): void
{
$this->allowPage();
app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true);
$path = $this->makeExportFile([
['线上订单号', '店铺名称', '下单时间', '标签'],
['A-1', '店铺甲', '2026-03-05 10:00:00', '刷单待处理'],
['A-2', '店铺甲', '2026-03-06 10:00:00', '刷单待处理'],
]);
try {
$result = app(JstOrderLabelImporter::class)->importFromFile(
$path, '2026-03-06', '2026-03-31', $this->company->id, true, 'test'
);
} finally {
@unlink($path);
}
// 窗口外的 3-05 被跳过;9 月的老数据在窗口外因此不受 --replace 影响
$this->assertSame(1, $result['orders']);
$this->assertSame(1, $result['skipped']);
$this->assertSame(['A-2'], JstOrderLabel::query()->where('company_id', $this->company->id)
->where('order_no', 'like', 'A-%')->pluck('order_no')->all());
// 原有 9 月 5 行 + 新增 A-2 一行
$this->assertSame(6, JstOrderLabel::query()->where('company_id', $this->company->id)->count());
}
/**
* 按聚水潭「导出订单」的真实形态造一个最小 xlsx:
* 字符串走 inlineStr、dimension 只写 A1(尺寸不可信)、一单多行。
*
* @param list<list<string>> $rows
*/
private function makeExportFile(array $rows): string
{
$path = tempnam(sys_get_temp_dir(), 'jst-export-') . '.xlsx';
$sheet = '<?xml version="1.0" encoding="UTF-8"?>'
. '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
. '<dimension ref="A1"/><sheetData>';
foreach ($rows as $rowIndex => $values) {
$sheet .= '<row r="' . ($rowIndex + 1) . '">';
foreach ($values as $columnIndex => $value) {
$reference = Coordinate::stringFromColumnIndex($columnIndex + 1) . ($rowIndex + 1);
$sheet .= '<c r="' . $reference . '" t="inlineStr"><is><t>'
. htmlspecialchars((string) $value, ENT_XML1) . '</t></is></c>';
}
$sheet .= '</row>';
}
$sheet .= '</sheetData></worksheet>';
$zip = new \ZipArchive();
$this->assertTrue($zip->open($path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true);
$zip->addFromString('[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8"?>'
. '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
. '<Default Extension="xml" ContentType="application/xml"/>'
. '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
. '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
. '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
. '</Types>');
$zip->addFromString('_rels/.rels', '<?xml version="1.0" encoding="UTF-8"?>'
. '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
. '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
. '</Relationships>');
$zip->addFromString('xl/workbook.xml', '<?xml version="1.0" encoding="UTF-8"?>'
. '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
. 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
. '<sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets></workbook>');
$zip->addFromString('xl/_rels/workbook.xml.rels', '<?xml version="1.0" encoding="UTF-8"?>'
. '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
. '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'
. '</Relationships>');
$zip->addFromString('xl/sharedStrings.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>'
. '<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="0" uniqueCount="0"></sst>');
$zip->addFromString('xl/worksheets/sheet1.xml', "\xEF\xBB\xBF" . $sheet);
$zip->close();
return $path;
}
}