CaiWuXiTong/tests/Feature/Business/JstOrderLabelsPageTest.php
qiuxiang 53cd0c3ce6 聚水潭订单标签明细:新增「货号」列,货号 X00006 的订单判为种菜订单
- 新增 jst_order_labels.item_code 迁移;导入时从商品明细镜像按 o_id 写入货号(被替换/取消的明细不计入)
- 「是否种菜订单」增加规则:货号含 X00006(信誉订单虚拟条码)直接判「是」
- 页面与导出新增货号列;新增 jst-order-labels:sync-item-codes 回填命令
- 测试:新增货号/种菜判定/回填命令用例,并修正导出表头过时断言
2026-10-10 16:06:02 +08:00

325 lines
17 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->bigInteger('o_id')->nullable();
$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, 'o_id' => 101, 'so_id' => '260901-391328633273048', 'shop_name' => '拼多多-优乐猫家具旗舰店', 'order_date' => '2026-09-01 00:00:13', 'labels' => '发货前售后,店铺-拼多多'],
['Id' => 2, 'o_id' => 102, 'so_id' => '3608400015164657', 'shop_name' => '京喜-久伴吾家具京喜自营工厂店', 'order_date' => '2026-09-01 00:00:44', 'labels' => '京东供销订单, 京喜'],
['Id' => 3, 'o_id' => 103, 'so_id' => '110211297173802010', 'shop_name' => '得物-全米', 'order_date' => '2026-08-31 23:59:00', 'labels' => '品牌直发'],
['Id' => 4, 'o_id' => 104, 'so_id' => '260915-000000000000001', 'shop_name' => '得物-全米', 'order_date' => '2026-09-15 10:00:00', 'labels' => ''],
]);
// 商品明细(货号):o_id=101 是换商品后的 X00006 虚拟条码单,102 是普通单
Schema::connection('business_mirror')->create('ops_jst_order_item', function ($table): void {
$table->integer('Id')->primary();
$table->bigInteger('o_id')->nullable();
$table->string('sku_id')->nullable();
$table->string('item_status')->nullable();
});
DB::connection('business_mirror')->table('ops_jst_order_item')->insert([
['Id' => 1, 'o_id' => 101, 'sku_id' => 'OLD-SKU', 'item_status' => 'Replaced'],
['Id' => 2, 'o_id' => 101, 'sku_id' => 'X00006', 'item_status' => null],
['Id' => 3, 'o_id' => 102, 'sku_id' => 'JD-001', 'item_status' => null],
['Id' => 4, 'o_id' => 103, 'sku_id' => 'DW-9', 'item_status' => null],
]);
// 全渠道镜像(天猫等):sync-item-codes 命令的回填来源之一
Schema::connection('business_mirror')->create('ops_jst_qmcust_orders', function ($table): void {
$table->integer('Id')->primary();
$table->bigInteger('o_id')->nullable();
$table->string('so_id')->nullable();
$table->dateTime('order_date')->nullable();
});
Schema::connection('business_mirror')->create('ops_jst_qmcust_order_items', function ($table): void {
$table->integer('Id')->primary();
$table->bigInteger('o_id')->nullable();
$table->string('sku_id')->nullable();
$table->string('item_status')->nullable();
});
DB::connection('business_mirror')->table('ops_jst_qmcust_orders')->insert([
['Id' => 1, 'o_id' => 201, 'so_id' => '3308965970412095568', 'order_date' => '2026-09-02 10:00:00'],
]);
DB::connection('business_mirror')->table('ops_jst_qmcust_order_items')->insert([
['Id' => 1, 'o_id' => 201, 'sku_id' => 'azf', 'item_status' => 'Replaced'],
['Id' => 2, 'o_id' => 201, 'sku_id' => 'X00006', 'item_status' => 'Sent'],
]);
}
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'));
// 货号随导入写入:被替换掉的旧编码不计入
$this->assertSame('X00006', $rows->firstWhere('label', '发货前售后')->item_code);
}
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('货号')
->assertSee('X00006')
->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->assertStringContainsString('JD-001', $csv);
$this->assertStringNotContainsString('260901-391328633273048', $csv);
}
public function test_item_code_marks_planting_orders_and_exports(): void
{
$this->allowPage();
app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true);
$rules = app(\App\Business\JstOrderLabelPlantingRules::class);
// 货号 X00006(信誉订单虚拟条码)直接判为种菜订单,与标签映射无关
$this->assertSame('是', $rules->flag($this->company->id, '发货前售后', 'X00006'));
$this->assertSame('否', $rules->flag($this->company->id, '发货前售后', 'JD-001'));
$this->assertSame('是', $rules->flag($this->company->id, '发货前售后', 'JD-001,X00006'));
// 导出里能看到货号列和「是否种菜订单=是」
$export = $this->loginAs()->get(route('business.planting-data.jst-order-labels.export', []));
$export->assertOk()->assertDownload();
$csv = $export->streamedContent();
$this->assertStringContainsString('是,发货前售后,X00006', $csv);
$this->assertStringContainsString('否,京喜,JD-001', $csv);
}
public function test_sync_item_codes_command_backfills_existing_rows(): void
{
$this->allowPage();
app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true);
// 模拟历史数据:清空货号,并补一条全渠道镜像来源的标签行
JstOrderLabel::query()->where('company_id', $this->company->id)->update(['item_code' => null]);
JstOrderLabel::create([
'company_id' => $this->company->id,
'order_no' => '3308965970412095568',
'shop_name' => '天猫-亿家达旗舰店',
'ordered_at' => '2026-09-02 10:00:00',
'label' => '刷单待处理',
'created_from' => 'jst-order-label-qmcust-import',
]);
$this->artisan('jst-order-labels:sync-item-codes', ['--company' => $this->company->id])
->assertSuccessful();
$rows = JstOrderLabel::query()->where('company_id', $this->company->id)->get();
$this->assertSame('X00006', $rows->firstWhere('label', '发货前售后')->item_code);
$this->assertSame('JD-001', $rows->firstWhere('label', '京喜')->item_code);
$this->assertSame('X00006', $rows->firstWhere('order_no', '3308965970412095568')->item_code);
}
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', '发货后售后'],
// 同一单里只有部分明细行带标签:不能因此多写一行「(无标签)」
['6476899', '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']);
$this->assertSame(0, JstOrderLabel::query()->where('company_id', $this->company->id)
->where('order_no', '3424493003753167')
->where('label', JstOrderLabelImporter::NO_LABEL)->count());
$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;
}
}