CaiWuXiTong/tests/Feature/Business/StoreViolationAlertPageTest.php
苏木 8e93e71024 店铺违规预警:新增违规原因与处置方式两列(页面/统一表/导入导出)
- 页面在「违规分类」后新增「违规原因」「处置方式」,统一表头、导出、模板、导入同步支持
- 新迁移:统一表 store_violation_records 增加 violation_reason、disposition 两列
- 同步命令:上游视图补齐 violation_reason/disposition(或 reason/handling_method)后自动参与同步;未提供时不触碰这两列,导入补充的数据不会被每日同步清空
- 导入:违规原因/处置方式独立映射;仅提供违规原因时,违规分类按原因关键词自动归类
- 测试:StoreViolation*、NavigationTest 共 23 项通过(含新导入用例与列探测用例)
2026-10-13 11:31:52 +08:00

308 lines
13 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\Models\Auth\Permission;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\StoreViolationRecordRepository;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Tests\Feature\FeatureTestCase;
/**
* 财务经营风险管控 › 店铺违规信息预警(统一表头 + 查询 + 导入导出)。
*/
class StoreViolationAlertPageTest extends FeatureTestCase
{
protected function setUp(): void
{
parent::setUp();
// 导入接口带 throttle:import 中间件,测试内多次上传需要放宽限制。
config(['app.throttles.import' => 1000]);
config(['database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]]);
DB::purge('business_mirror');
Schema::connection('business_mirror')->create('store_violation_records', function ($table): void {
$table->bigIncrements('id');
$table->string('platform', 32);
$table->string('shop_name', 191)->default('');
$table->string('violation_type', 500)->nullable();
$table->string('violation_category', 64)->nullable();
$table->string('violation_reason', 500)->nullable();
$table->string('disposition', 500)->nullable();
$table->string('violation_no', 100);
$table->date('violation_date')->nullable();
$table->decimal('points_deducted', 10, 2)->nullable();
$table->string('status', 64)->nullable();
$table->date('penalty_date')->nullable();
$table->date('appeal_deadline')->nullable();
$table->string('handling_status', 32)->default('未处理');
$table->string('source', 16)->default('crawl');
$table->string('window_name', 191)->nullable();
$table->date('crawl_date')->nullable();
$table->dateTime('synced_at')->nullable();
$table->timestamps();
$table->unique(['platform', 'violation_no']);
});
Schema::connection('business_mirror')->create('business_mirror_table_states', function ($table): void {
$table->string('dataset_key', 191)->primary();
$table->string('source_key', 64)->default('');
$table->string('source_object', 191)->default('');
$table->string('target_table', 64)->default('');
$table->string('status', 24)->default('pending');
$table->dateTime('last_attempted_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
$table->unsignedBigInteger('mirror_row_count')->default(0);
$table->string('last_error_code', 120)->nullable();
$table->dateTime('initial_completed_at')->nullable();
$table->timestamps();
});
DB::connection('business_mirror')->table('store_violation_records')->insert([
[
'platform' => 'jd', 'shop_name' => '京东-亿家达全屋旗舰店', 'violation_type' => '延迟发货/缺货无货',
'violation_category' => '发货履约', 'violation_reason' => '订单存在延迟发货、缺货无货', 'disposition' => '警告',
'violation_no' => 'JD202610040001', 'violation_date' => '2026-10-04',
'points_deducted' => 2, 'status' => '待处理', 'penalty_date' => null, 'appeal_deadline' => '2026-10-11',
'handling_status' => '未处理', 'source' => 'crawl', 'crawl_date' => '2026-10-04',
'created_at' => '2026-10-05 08:00:00', 'updated_at' => '2026-10-05 08:00:00',
],
[
'platform' => 'jd', 'shop_name' => '京东-卓禾卧室家具旗舰店', 'violation_type' => '订单存在揽收超时行为',
'violation_category' => '发货履约', 'violation_reason' => '订单存在揽收超时行为', 'disposition' => null,
'violation_no' => 'JD202610040002', 'violation_date' => '2026-10-03',
'points_deducted' => null, 'status' => '超期未申诉', 'penalty_date' => '2026-10-05', 'appeal_deadline' => '2026-10-08',
'handling_status' => '处理中', 'source' => 'crawl', 'crawl_date' => '2026-10-04',
'created_at' => '2026-10-05 08:00:00', 'updated_at' => '2026-10-05 08:00:00',
],
[
'platform' => 'jingxi', 'shop_name' => '京喜-久伴吾家具京喜自营工厂店', 'violation_type' => '发票违规',
'violation_category' => '发票与考核', 'violation_reason' => null, 'disposition' => '维持原判',
'violation_no' => 'JX202610040001', 'violation_date' => '2026-10-04',
'points_deducted' => null, 'status' => '已处置', 'penalty_date' => null, 'appeal_deadline' => null,
'handling_status' => '已处理', 'source' => 'crawl', 'crawl_date' => '2026-10-04',
'created_at' => '2026-10-05 08:00:00', 'updated_at' => '2026-10-05 08:00:00',
],
]);
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
'dataset_key' => StoreViolationRecordRepository::DATASET_KEY,
'source_key' => 'operations_replica',
'source_object' => 'rpt.crawl_shop_violation',
'target_table' => StoreViolationRecordRepository::TABLE,
'status' => 'succeeded',
'last_succeeded_at' => '2026-10-05 05:30:00',
'mirror_row_count' => 3,
'created_at' => '2026-10-05 05:30:00',
'updated_at' => '2026-10-05 05:30:00',
]);
}
public function test_page_renders_unified_headers_and_platform_rows(): void
{
$this->allowPage();
$page = $this->loginAs()->get(route('business.pages.show', [
'section' => 'financial-risk-control',
'page' => 'store-violation-alert',
'platform' => 'jd',
]));
$page->assertOk()
->assertSee('店铺名')
->assertSee('违规类型')
->assertSee('违规分类')
->assertSee('违规原因')
->assertSee('处置方式')
->assertSee('违规单号')
->assertSee('违规日期')
->assertSee('扣分')
->assertSee('申诉截止日期')
->assertSee('处理状态')
->assertSee('订单存在延迟发货、缺货无货')
->assertSee('JD202610040001')
->assertSee('京东-亿家达全屋旗舰店')
->assertSee('data-platform-panel="jingxi"', false);
}
public function test_page_filters_by_keyword(): void
{
$this->allowPage();
$page = $this->loginAs()->get(route('business.pages.show', [
'section' => 'financial-risk-control',
'page' => 'store-violation-alert',
'platform' => 'jd',
'keyword' => '卓禾',
]));
$page->assertOk()
->assertSee('JD202610040002')
->assertDontSee('JD202610040001')
->assertDontSee('JX202610040001');
}
public function test_export_streams_unified_headers(): void
{
$this->allowPage();
$response = $this->loginAs()->get(route('business.store-violation-alert.export', [
'platform' => 'jd',
'keyword' => '亿家达',
]));
$response->assertOk();
$this->assertStringContainsString(
'spreadsheetml',
(string) $response->headers->get('content-type'),
);
$binary = $response->streamedContent();
$this->assertNotSame('', $binary);
$temp = tempnam(sys_get_temp_dir(), 'store-violation-export');
file_put_contents($temp, $binary);
$sheet = IOFactory::load($temp)->getActiveSheet();
$headers = $sheet->rangeToArray('A1:L1')[0];
$this->assertSame(
['店铺名', '违规类型', '违规分类', '违规原因', '处置方式', '违规单号', '违规日期', '扣分', '状态', '处罚日期', '申诉截止日期', '处理状态'],
$headers,
);
$this->assertSame('订单存在延迟发货、缺货无货', $sheet->getCell('D2')->getValue());
$this->assertSame('警告', $sheet->getCell('E2')->getValue());
$this->assertSame('JD202610040001', $sheet->getCell('F2')->getValue());
unlink($temp);
}
public function test_import_preview_maps_headers_and_commit_upserts(): void
{
$this->allowPage();
$file = $this->makeWorkbook([
['店铺', '违规编号', '违规内容', '违规日期', '扣分'],
['京东-纪辰家具旗舰店', 'JD202610040099', '订单延迟发货', '2026-10-02', 1],
['京东-亿家达全屋旗舰店', 'JD202610040001', '延迟发货/缺货无货', '2026-10-04', 2],
['缺少单号的店铺', '', '无效行', '2026-10-02', ''],
]);
$preview = $this->loginAs()->post(route('business.store-violation-alert.import.preview'), [
'platform' => 'jd',
'import' => $file,
]);
$preview->assertRedirect();
$payload = $preview->getSession()->get('store_violation_preview');
$this->assertIsArray($payload);
$this->assertSame(1, $payload['summary']['new']);
$this->assertSame(1, $payload['summary']['same']);
$this->assertSame(1, $payload['summary']['invalid']);
$this->assertNotEmpty($payload['token']);
$commit = $this->loginAs()->post(route('business.store-violation-alert.import.commit'), [
'preview_token' => $payload['token'],
]);
$commit->assertRedirect();
$record = DB::connection('business_mirror')
->table('store_violation_records')
->where('platform', 'jd')
->where('violation_no', 'JD202610040099')
->first();
$this->assertNotNull($record);
$this->assertSame('发货履约', $record->violation_category);
$this->assertSame('京东-纪辰家具旗舰店', $record->shop_name);
$this->assertSame('import', $record->source);
}
public function test_import_preview_requires_unified_required_columns(): void
{
$this->allowPage();
$file = $this->makeWorkbook([
['店铺名', '违规类型'],
['京东-纪辰家具旗舰店', '订单延迟发货'],
]);
$response = $this->loginAs()->post(route('business.store-violation-alert.import.preview'), [
'platform' => 'jd',
'import' => $file,
]);
$response->assertRedirect();
$response->assertSessionHasErrors('import');
}
public function test_import_maps_reason_and_disposition_columns(): void
{
$this->allowPage();
$file = $this->makeWorkbook([
['店铺名', '违规单号', '违规原因', '处置方式'],
['京东-纪辰家具旗舰店', 'JD202610040088', '订单存在揽收超时行为', '警告;整改'],
]);
$preview = $this->loginAs()->post(route('business.store-violation-alert.import.preview'), [
'platform' => 'jd',
'import' => $file,
]);
$preview->assertRedirect();
$payload = $preview->getSession()->get('store_violation_preview');
$this->assertIsArray($payload);
$this->assertSame(1, $payload['summary']['new']);
$commit = $this->loginAs()->post(route('business.store-violation-alert.import.commit'), [
'preview_token' => $payload['token'],
]);
$commit->assertRedirect();
$record = DB::connection('business_mirror')
->table('store_violation_records')
->where('platform', 'jd')
->where('violation_no', 'JD202610040088')
->first();
$this->assertNotNull($record);
$this->assertSame('订单存在揽收超时行为', $record->violation_reason);
$this->assertSame('警告;整改', $record->disposition);
// 未提供违规类型时,违规分类按违规原因关键词自动归类。
$this->assertSame('发货履约', $record->violation_category);
}
/** @param list<list<mixed>> $rows */
private function makeWorkbook(array $rows): UploadedFile
{
$spreadsheet = new Spreadsheet();
$spreadsheet->getActiveSheet()->fromArray($rows, null, 'A1');
$path = tempnam(sys_get_temp_dir(), 'store-violation-import') . '.xlsx';
(new Xlsx($spreadsheet))->save($path);
$binary = (string) file_get_contents($path);
unlink($path);
return UploadedFile::fake()->createWithContent('店铺违规信息.xlsx', $binary);
}
private function allowPage(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
}