钉钉工作台里点进来的这个入口,现在只显示这一张页面:不再渲染全局侧边栏, 内容区占满宽度,其余页面和网页端登录完全不受影响。 判断依据:当前页面是 financial-risk-control/store-violation-alert,且请求来自 钉钉容器(UA 含 DingTalk)或显式带 ?focus=1;路由参数取不到时按路径段兜底。
333 lines
14 KiB
PHP
333 lines
14 KiB
PHP
<?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_dingtalk_entry_shows_only_this_page_without_the_global_menu(): void
|
||
{
|
||
$this->allowPage();
|
||
|
||
// 钉钉工作台入口:隐藏全局菜单,只留这一页。
|
||
$this->loginAs()
|
||
->withHeaders(['User-Agent' => 'Mozilla/5.0 (Macintosh) AppleWebKit/605.1.15 DingTalk(9.0.1-macOS-arm64) webDt/PC'])
|
||
->get(route('business.pages.show', [
|
||
'section' => 'financial-risk-control',
|
||
'page' => 'store-violation-alert',
|
||
]))
|
||
->assertOk()
|
||
->assertDontSee('id="sidenav-main"', false)
|
||
->assertSeeText('店铺违规信息预警');
|
||
|
||
// 网页端(非钉钉)照旧显示完整菜单。
|
||
$this->withHeaders(['User-Agent' => 'Mozilla/5.0 (Macintosh) Safari/605.1.15'])
|
||
->get(route('business.pages.show', [
|
||
'section' => 'financial-risk-control',
|
||
'page' => 'store-violation-alert',
|
||
]))
|
||
->assertOk()
|
||
->assertSee('id="sidenav-main"', 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()
|
||
);
|
||
}
|
||
}
|