670 lines
31 KiB
PHP
670 lines
31 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\Http;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Illuminate\Support\Facades\URL;
|
||
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('owner_name', 64)->nullable();
|
||
$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->dateTime('handling_deadline')->nullable();
|
||
$table->string('handling_status', 32)->default('未处理');
|
||
$table->string('ignore_reason', 191)->nullable();
|
||
$table->string('source', 16)->default('crawl');
|
||
$table->string('window_name', 191)->nullable();
|
||
$table->date('crawl_date')->nullable();
|
||
$table->dateTime('synced_at')->nullable();
|
||
$table->boolean('needs_alert')->default(false);
|
||
$table->string('alert_level', 8)->nullable();
|
||
$table->unsignedBigInteger('alert_rule_id')->nullable();
|
||
$table->string('alert_action', 500)->nullable();
|
||
$table->string('alert_channel', 64)->nullable();
|
||
$table->unsignedInteger('handling_hours')->nullable();
|
||
$table->string('push_status', 16)->default('未推送');
|
||
$table->dateTime('last_pushed_at')->nullable();
|
||
$table->timestamps();
|
||
$table->unique(['platform', 'violation_no']);
|
||
});
|
||
|
||
Schema::connection('business_mirror')->create('store_violation_rules', function ($table): void {
|
||
$table->bigIncrements('id');
|
||
$table->string('platform', 32);
|
||
$table->string('violation_category', 64)->nullable();
|
||
$table->string('violation_type', 500)->nullable();
|
||
$table->string('match_keywords', 500)->nullable();
|
||
$table->string('alert_level', 8)->nullable();
|
||
$table->boolean('needs_alert')->default(true);
|
||
$table->string('suggested_action', 500)->nullable();
|
||
$table->unsignedInteger('handling_hours')->nullable();
|
||
$table->string('remind_channel', 191)->nullable();
|
||
$table->unsignedInteger('sort')->default(0);
|
||
$table->boolean('enabled')->default(true);
|
||
$table->string('remark', 191)->nullable();
|
||
$table->timestamps();
|
||
});
|
||
|
||
Schema::connection('business_mirror')->create('ops_fys_shop_info', function ($table): void {
|
||
$table->bigIncrements('id');
|
||
$table->string('shopName', 191)->nullable();
|
||
$table->string('shopHead', 64)->nullable();
|
||
});
|
||
|
||
Schema::connection('business_mirror')->create('store_violation_push_logs', function ($table): void {
|
||
$table->bigIncrements('id');
|
||
$table->string('push_no', 32)->unique();
|
||
$table->dateTime('pushed_at')->nullable();
|
||
$table->string('channel', 32)->default('dingtalk_robot');
|
||
$table->string('message_type', 16)->default('instant');
|
||
$table->string('platform', 32)->nullable();
|
||
$table->string('violation_no', 100)->nullable();
|
||
$table->string('message_digest', 500)->nullable();
|
||
$table->string('mentioned', 191)->nullable();
|
||
$table->string('result', 16)->default('success');
|
||
$table->string('error', 500)->nullable();
|
||
$table->timestamps();
|
||
});
|
||
|
||
config(['store_violations.push' => [
|
||
'enabled' => true,
|
||
'channel' => 'dingtalk_robot',
|
||
'webhook' => 'https://oapi.dingtalk.com/robot/send?access_token=test-token',
|
||
'secret' => '',
|
||
'window_start' => '09:00',
|
||
'window_end' => '21:00',
|
||
'instant_levels' => ['P0', 'P1'],
|
||
'digest_time' => '10:00',
|
||
'at_all' => false,
|
||
]]);
|
||
|
||
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_rules')->insert([
|
||
['id' => 1, 'platform' => 'jd', 'violation_category' => '发货履约', 'violation_type' => '缺货/无货',
|
||
'match_keywords' => null, 'alert_level' => 'P1', 'needs_alert' => true, 'suggested_action' => '联系客户补发',
|
||
'handling_hours' => null, 'remind_channel' => null, 'sort' => 0, 'enabled' => true, 'remark' => null,
|
||
'created_at' => '2026-10-13 09:00:00', 'updated_at' => '2026-10-13 09:00:00'],
|
||
['id' => 2, 'platform' => 'jd', 'violation_category' => '发货履约', 'violation_type' => '订单延迟发货',
|
||
'match_keywords' => null, 'alert_level' => 'P1', 'needs_alert' => true, 'suggested_action' => '致电客户协商',
|
||
'handling_hours' => 24, 'remind_channel' => null, 'sort' => 10, 'enabled' => true, 'remark' => null,
|
||
'created_at' => '2026-10-13 09:00:00', 'updated_at' => '2026-10-13 09:00:00'],
|
||
['id' => 3, 'platform' => 'jingxi', 'violation_category' => '发票与考核', 'violation_type' => '发票违规',
|
||
'match_keywords' => null, 'alert_level' => 'P0', 'needs_alert' => true, 'suggested_action' => '立即补开发票',
|
||
'handling_hours' => null, 'remind_channel' => null, 'sort' => 0, 'enabled' => true, 'remark' => null,
|
||
'created_at' => '2026-10-13 09:00:00', 'updated_at' => '2026-10-13 09:00:00'],
|
||
['id' => 4, 'platform' => 'jd', 'violation_category' => '发货履约', 'violation_type' => '订单存在揽收超时行为',
|
||
'match_keywords' => null, 'alert_level' => null, 'needs_alert' => true, 'suggested_action' => null,
|
||
'handling_hours' => null, 'remind_channel' => null, 'sort' => 20, 'enabled' => true, 'remark' => null,
|
||
'created_at' => '2026-10-13 09:00:00', 'updated_at' => '2026-10-13 09:00:00'],
|
||
]);
|
||
|
||
DB::connection('business_mirror')->table('ops_fys_shop_info')->insert([
|
||
['shopName' => '京东-亿家达全屋旗舰店', 'shopHead' => '张三'],
|
||
['shopName' => '京东-卓禾卧室家具旗舰店', 'shopHead' => '李四'],
|
||
['shopName' => '京东-纪辰家具旗舰店', 'shopHead' => '王五'],
|
||
['shopName' => '京喜-久伴吾家具京喜自营工厂店', 'shopHead' => '幽竹'],
|
||
]);
|
||
|
||
DB::connection('business_mirror')->table('store_violation_records')->insert([
|
||
[
|
||
'platform' => 'jd', 'shop_name' => '京东-亿家达全屋旗舰店', 'owner_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_deadline' => '2026-10-12 23:59:59', 'handling_status' => '未处理', 'ignore_reason' => null,
|
||
'source' => 'crawl', 'window_name' => null, 'crawl_date' => '2026-10-04', 'synced_at' => null,
|
||
'needs_alert' => true, 'alert_level' => 'P1', 'alert_rule_id' => 1, 'alert_action' => '联系客户补发',
|
||
'alert_channel' => null, 'handling_hours' => null, 'push_status' => '未推送', 'last_pushed_at' => null,
|
||
'created_at' => '2026-10-05 08:00:00', 'updated_at' => '2026-10-05 08:00:00',
|
||
],
|
||
[
|
||
'platform' => 'jd', 'shop_name' => '京东-卓禾卧室家具旗舰店', 'owner_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_deadline' => '2026-10-14 23:59:59', 'handling_status' => '处理中', 'ignore_reason' => null,
|
||
'source' => 'crawl', 'window_name' => null, 'crawl_date' => '2026-10-04', 'synced_at' => null,
|
||
'needs_alert' => true, 'alert_level' => 'P2', 'alert_rule_id' => 4, 'alert_action' => null,
|
||
'alert_channel' => null, 'handling_hours' => null, 'push_status' => '已推送', 'last_pushed_at' => '2026-10-13 09:10:00',
|
||
'created_at' => '2026-10-05 08:00:00', 'updated_at' => '2026-10-05 08:00:00',
|
||
],
|
||
[
|
||
'platform' => 'jd', 'shop_name' => '京东-无提醒示例店', 'owner_name' => null,
|
||
'violation_type' => '普通通知事项', 'violation_category' => '其他',
|
||
'violation_reason' => null, 'disposition' => null,
|
||
'violation_no' => 'JD202610040003', 'violation_date' => '2026-10-05',
|
||
'points_deducted' => null, 'status' => '待处理', 'penalty_date' => null, 'appeal_deadline' => null,
|
||
'handling_deadline' => null, 'handling_status' => '未处理', 'ignore_reason' => null,
|
||
'source' => 'crawl', 'window_name' => null, 'crawl_date' => '2026-10-05', 'synced_at' => null,
|
||
'needs_alert' => false, 'alert_level' => null, 'alert_rule_id' => null, 'alert_action' => null,
|
||
'alert_channel' => null, 'handling_hours' => null, 'push_status' => '未推送', 'last_pushed_at' => null,
|
||
'created_at' => '2026-10-05 08:00:00', 'updated_at' => '2026-10-05 08:00:00',
|
||
],
|
||
[
|
||
'platform' => 'jingxi', 'shop_name' => '京喜-久伴吾家具京喜自营工厂店', 'owner_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_deadline' => '2026-10-13 23:59:59', 'handling_status' => '处理完成', 'ignore_reason' => null,
|
||
'source' => 'crawl', 'window_name' => null, 'crawl_date' => '2026-10-04', 'synced_at' => null,
|
||
'needs_alert' => true, 'alert_level' => 'P0', 'alert_rule_id' => 3, 'alert_action' => '立即补开发票',
|
||
'alert_channel' => null, 'handling_hours' => null, 'push_status' => '已推送', 'last_pushed_at' => '2026-10-13 09:10:00',
|
||
'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('处理截止')
|
||
->assertSee('是否超时')
|
||
->assertSee('推送状态')
|
||
->assertSee('处理状态')
|
||
->assertSee('操作')
|
||
->assertSee('P1-较重')
|
||
->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)
|
||
->assertDontSee('data-open-tabs', false)
|
||
->assertSeeText('店铺违规信息预警');
|
||
|
||
// 同目录下的另一个页面(流程统计)在钉钉里同样是单页模式。
|
||
$this->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' => 'process-statistics',
|
||
]))
|
||
->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' => '卓禾',
|
||
'view' => 'all',
|
||
]));
|
||
|
||
$page->assertOk()
|
||
->assertSee('JD202610040002')
|
||
->assertDontSee('JD202610040001')
|
||
->assertDontSee('JX202610040001');
|
||
}
|
||
|
||
public function test_default_view_only_shows_alert_and_pending_records(): void
|
||
{
|
||
$this->allowPage();
|
||
|
||
$default = $this->loginAs()->get(route('business.pages.show', [
|
||
'section' => 'financial-risk-control',
|
||
'page' => 'store-violation-alert',
|
||
'platform' => 'jd',
|
||
]));
|
||
$default->assertOk()
|
||
->assertSee('JD202610040001')
|
||
->assertDontSee('JD202610040002')
|
||
->assertDontSee('JD202610040003');
|
||
|
||
$all = $this->loginAs()->get(route('business.pages.show', [
|
||
'section' => 'financial-risk-control',
|
||
'page' => 'store-violation-alert',
|
||
'platform' => 'jd',
|
||
'view' => 'all',
|
||
]));
|
||
$all->assertOk()
|
||
->assertSee('JD202610040001')
|
||
->assertSee('JD202610040002')
|
||
->assertSee('JD202610040003')
|
||
->assertSee('不提醒');
|
||
}
|
||
|
||
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:R1')[0];
|
||
|
||
$this->assertSame(
|
||
[
|
||
'店铺名', '预警级别', '违规类型', '违规分类', '违规原因', '处置方式', '违规单号', '违规日期', '扣分',
|
||
'责任人', '状态', '处罚日期', '申诉截止日期', '处理截止', '是否超时', '推送状态', '处理状态', '忽略原因',
|
||
],
|
||
$headers,
|
||
);
|
||
$this->assertSame('P1-较重', $sheet->getCell('B2')->getValue());
|
||
$this->assertSame('订单存在延迟发货、缺货无货', $sheet->getCell('E2')->getValue());
|
||
$this->assertSame('警告', $sheet->getCell('F2')->getValue());
|
||
$this->assertSame('JD202610040001', $sheet->getCell('G2')->getValue());
|
||
$this->assertSame('张三', $sheet->getCell('J2')->getValue());
|
||
$this->assertSame('已超时', $sheet->getCell('O2')->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);
|
||
// 预警派生字段按字典自动计算(无级别配置的规则默认 P2)。
|
||
$this->assertTrue((bool) $record->needs_alert);
|
||
$this->assertSame('P2', $record->alert_level);
|
||
}
|
||
|
||
public function test_import_commit_calculates_alert_fields_from_dictionary(): void
|
||
{
|
||
$this->allowPage();
|
||
|
||
$file = $this->makeWorkbook([
|
||
['店铺名', '违规单号', '违规类型', '违规日期'],
|
||
['京东-纪辰家具旗舰店', 'JD202610040099', '订单延迟发货', '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->loginAs()->post(route('business.store-violation-alert.import.commit'), [
|
||
'preview_token' => $payload['token'],
|
||
])->assertRedirect();
|
||
|
||
$record = DB::connection('business_mirror')
|
||
->table('store_violation_records')
|
||
->where('platform', 'jd')
|
||
->where('violation_no', 'JD202610040099')
|
||
->first();
|
||
|
||
$this->assertNotNull($record);
|
||
$this->assertTrue((bool) $record->needs_alert);
|
||
$this->assertSame('P1', $record->alert_level);
|
||
$this->assertSame('王五', $record->owner_name);
|
||
// 字典时效 24 小时:违规日期 2026-10-02 起算。
|
||
$this->assertSame('2026-10-03 00:00:00', (string) $record->handling_deadline);
|
||
}
|
||
|
||
public function test_update_handling_status_requires_reason_when_ignored(): void
|
||
{
|
||
$this->allowPage();
|
||
$this->withExceptionHandling();
|
||
|
||
$record = DB::connection('business_mirror')->table('store_violation_records')
|
||
->where('violation_no', 'JD202610040001')->first();
|
||
|
||
// 已忽略缺原因 → 报错且状态不变
|
||
$this->loginAs()->post(route('business.store-violation-alert.update'), [
|
||
'id' => $record->id,
|
||
'handling_status' => '已忽略',
|
||
])->assertSessionHasErrors('ignore_reason');
|
||
$this->assertSame('未处理', DB::connection('business_mirror')->table('store_violation_records')->where('id', $record->id)->value('handling_status'));
|
||
|
||
// 认领 → 处理中
|
||
$this->loginAs()->post(route('business.store-violation-alert.update'), [
|
||
'id' => $record->id,
|
||
'handling_status' => '处理中',
|
||
])->assertRedirect();
|
||
$this->assertSame('处理中', DB::connection('business_mirror')->table('store_violation_records')->where('id', $record->id)->value('handling_status'));
|
||
|
||
// 忽略(带原因)
|
||
$this->loginAs()->post(route('business.store-violation-alert.update'), [
|
||
'id' => $record->id,
|
||
'handling_status' => '已忽略',
|
||
'ignore_reason' => '误报,无需处理',
|
||
])->assertRedirect();
|
||
$updated = DB::connection('business_mirror')->table('store_violation_records')->where('id', $record->id)->first();
|
||
$this->assertSame('已忽略', $updated->handling_status);
|
||
$this->assertSame('误报,无需处理', $updated->ignore_reason);
|
||
}
|
||
|
||
public function test_claim_route_marks_record_processing_via_signed_link(): void
|
||
{
|
||
$this->allowPage();
|
||
$this->withExceptionHandling();
|
||
$this->loginAs();
|
||
|
||
$record = DB::connection('business_mirror')->table('store_violation_records')
|
||
->where('violation_no', 'JD202610040001')->first();
|
||
|
||
$url = URL::signedRoute('business.store-violation-alert.claim', [
|
||
'record' => $record->id,
|
||
'company_id' => $this->company->id,
|
||
]);
|
||
$this->get($url)->assertRedirect();
|
||
$this->assertSame('处理中', DB::connection('business_mirror')->table('store_violation_records')->where('id', $record->id)->value('handling_status'));
|
||
|
||
// 幂等:再次访问不报错。
|
||
$this->get($url)->assertRedirect();
|
||
|
||
// 无签名 → 403。
|
||
$this->get(route('business.store-violation-alert.claim', [
|
||
'record' => $record->id,
|
||
'company_id' => $this->company->id,
|
||
]))
|
||
->assertStatus(403);
|
||
}
|
||
|
||
public function test_preview_statistics_page_renders_kpi_cards_and_pending_rows(): void
|
||
{
|
||
$this->allowPage();
|
||
|
||
$response = $this->loginAs()->get(route('business.pages.show', [
|
||
'section' => 'financial-risk-control',
|
||
'page' => 'preview-statistics',
|
||
'platform' => 'jd',
|
||
]));
|
||
|
||
$response->assertOk()
|
||
->assertSeeText('预览统计')
|
||
->assertSeeText('今日新增')
|
||
->assertSeeText('待处理总数')
|
||
->assertSeeText('P0 未处理')
|
||
->assertSeeText('已超时')
|
||
->assertSeeText('待处理 1 条')
|
||
->assertSee('JD202610040001')
|
||
->assertDontSee('JD202610040002')
|
||
->assertDontSee('JD202610040003')
|
||
->assertSee('data-risk-preview-tabs', false);
|
||
}
|
||
|
||
public function test_push_instant_sends_p0_p1_and_marks_pushed(): void
|
||
{
|
||
$this->allowPage();
|
||
Http::fake(['*' => Http::response(['errcode' => 0, 'errmsg' => 'ok'])]);
|
||
|
||
$this->artisan('store-violations:push-instant')->assertExitCode(0);
|
||
|
||
$record = DB::connection('business_mirror')->table('store_violation_records')
|
||
->where('violation_no', 'JD202610040001')->first();
|
||
$this->assertSame('已推送', $record->push_status);
|
||
$this->assertNotNull($record->last_pushed_at);
|
||
|
||
$log = DB::connection('business_mirror')->table('store_violation_push_logs')
|
||
->where('message_type', 'instant')->first();
|
||
$this->assertNotNull($log);
|
||
$this->assertSame('JD202610040001', $log->violation_no);
|
||
$this->assertSame('success', $log->result);
|
||
$this->assertSame('张三', $log->mentioned);
|
||
|
||
Http::assertSent(function ($request): bool {
|
||
$text = (string) ($request['markdown']['text'] ?? '');
|
||
|
||
return str_contains($request->url(), 'oapi.dingtalk.com/robot/send')
|
||
&& str_contains($text, 'P1-较重')
|
||
&& str_contains($text, '去认领');
|
||
});
|
||
}
|
||
|
||
public function test_push_digest_sends_daily_summary(): void
|
||
{
|
||
$this->allowPage();
|
||
Http::fake(['*' => Http::response(['errcode' => 0, 'errmsg' => 'ok'])]);
|
||
|
||
$this->artisan('store-violations:push-digest')->assertExitCode(0);
|
||
|
||
$log = DB::connection('business_mirror')->table('store_violation_push_logs')
|
||
->where('message_type', 'daily')->first();
|
||
$this->assertNotNull($log);
|
||
$this->assertSame('success', $log->result);
|
||
|
||
Http::assertSent(function ($request): bool {
|
||
$text = (string) ($request['markdown']['text'] ?? '');
|
||
|
||
return str_contains($text, '每日汇总') && str_contains($text, '已超时未认领');
|
||
});
|
||
}
|
||
|
||
/** @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()
|
||
);
|
||
}
|
||
}
|