188 lines
7.6 KiB
PHP
188 lines
7.6 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Modules\EcommerceFinance\Repositories\StoreViolationRuleRepository;
|
||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||
use Throwable;
|
||
|
||
/**
|
||
* 店铺违规信息预警:违规字典维护(Excel 导入 / 更新,按 平台+违规类型 幂等)。
|
||
*
|
||
* 表头(顺序不限):平台、违规分类、违规类型、匹配关键词、预警级别、是否需要提醒、
|
||
* 建议处理动作、处理时效、提醒方式、排序、启用、备注。
|
||
* 导入后按需执行 store-violations:recalc-alerts 让存量记录套用新字典。
|
||
*/
|
||
class ImportStoreViolationRules extends Command
|
||
{
|
||
protected $signature = 'store-violations:import-rules
|
||
{file : xlsx / xls / csv 文件路径}
|
||
{--dry-run : 只解析并统计,不写库}';
|
||
|
||
protected $description = '导入违规字典(预警级别、提醒规则等)';
|
||
|
||
private const HEADERS = [
|
||
'platform' => ['平台', 'platform'],
|
||
'violation_category' => ['违规分类', '分类', 'violation_category'],
|
||
'violation_type' => ['违规类型', '类型', 'violation_type'],
|
||
'match_keywords' => ['匹配关键词', '关键词', 'match_keywords'],
|
||
'alert_level' => ['预警级别', '级别', '预警等级', 'alert_level'],
|
||
'needs_alert' => ['是否需要提醒', '需提醒', '需要提醒', 'needs_alert'],
|
||
'suggested_action' => ['建议处理动作', '处理建议', '建议动作', 'suggested_action'],
|
||
'handling_hours' => ['处理时效', '时效', 'handling_hours'],
|
||
'remind_channel' => ['提醒方式', 'remind_channel'],
|
||
'sort' => ['排序', 'sort'],
|
||
'enabled' => ['启用', 'enabled'],
|
||
'remark' => ['备注', 'remark'],
|
||
];
|
||
|
||
private const PLATFORM_MAP = [
|
||
'淘系' => 'taobao', '淘宝' => 'taobao', '天猫' => 'taobao',
|
||
'京东' => 'jd', '京喜' => 'jingxi', '抖音' => 'douyin',
|
||
'拼多多' => 'pinduoduo', '阿里巴巴' => 'alibaba',
|
||
];
|
||
|
||
public function handle(): int
|
||
{
|
||
$file = (string) $this->argument('file');
|
||
if (! is_file($file)) {
|
||
$this->error('文件不存在:' . $file);
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
try {
|
||
$reader = IOFactory::createReaderForFile($file);
|
||
if (method_exists($reader, 'setReadDataOnly')) {
|
||
$reader->setReadDataOnly(true);
|
||
}
|
||
if (method_exists($reader, 'setReadFilter')) {
|
||
// 字典文件通常只有几十行;部分导出文件带巨大空白维度,限制读取范围。
|
||
$reader->setReadFilter(new class implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter {
|
||
public function readCell($columnAddress, $row, $worksheetName = ''): bool
|
||
{
|
||
return $row <= 5000;
|
||
}
|
||
});
|
||
}
|
||
$matrix = $reader->load($file)->getActiveSheet()->toArray(null, true, true, false);
|
||
} catch (Throwable $exception) {
|
||
$this->error('无法读取文件:' . $exception->getMessage());
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
if ($matrix === []) {
|
||
$this->error('文件为空。');
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$map = [];
|
||
foreach ($matrix[0] as $index => $cell) {
|
||
$label = mb_strtolower(trim((string) $cell));
|
||
foreach (self::HEADERS as $key => $aliases) {
|
||
foreach ($aliases as $alias) {
|
||
if ($label === mb_strtolower($alias)) {
|
||
$map[$key] = $index;
|
||
break 2;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (! isset($map['platform'], $map['violation_type'])) {
|
||
$this->error('缺少必需表头:平台、违规类型。');
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
||
if (! app(StoreViolationRuleRepository::class)->available()) {
|
||
$this->error('违规字典表尚未建立,请先执行数据库迁移。');
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$created = 0;
|
||
$updated = 0;
|
||
$skipped = 0;
|
||
$sort = 0;
|
||
|
||
foreach (array_slice($matrix, 1) as $row) {
|
||
$value = static fn (string $key): string => isset($map[$key]) ? trim((string) ($row[$map[$key]] ?? '')) : '';
|
||
|
||
$platform = $value('platform');
|
||
$platform = self::PLATFORM_MAP[$platform] ?? mb_strtolower($platform);
|
||
$type = $value('violation_type');
|
||
if ($platform === '' || $type === '') {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
$sort += 10;
|
||
|
||
$levelRaw = strtoupper($value('alert_level'));
|
||
$level = in_array($levelRaw, ['P0', 'P1', 'P2'], true) ? $levelRaw : null;
|
||
|
||
$needsAlertRaw = mb_strtolower($value('needs_alert'));
|
||
$needsAlert = ! in_array($needsAlertRaw, ['否', 'no', '0', 'false'], true);
|
||
|
||
$hoursRaw = $value('handling_hours');
|
||
$hours = is_numeric($hoursRaw) ? max((int) $hoursRaw, 0) : null;
|
||
|
||
$payload = [
|
||
'platform' => $platform,
|
||
'violation_category' => $value('violation_category') !== '' ? mb_substr($value('violation_category'), 0, 64) : null,
|
||
'violation_type' => mb_substr($type, 0, 500),
|
||
'match_keywords' => $value('match_keywords') !== '' ? mb_substr($value('match_keywords'), 0, 500) : null,
|
||
'alert_level' => $needsAlert ? $level : null,
|
||
'needs_alert' => $needsAlert ? 1 : 0,
|
||
'suggested_action' => $value('suggested_action') !== '' ? mb_substr($value('suggested_action'), 0, 500) : null,
|
||
'handling_hours' => $hours,
|
||
'remind_channel' => $value('remind_channel') !== '' ? mb_substr($value('remind_channel'), 0, 191) : null,
|
||
'sort' => is_numeric($value('sort')) ? (int) $value('sort') : $sort,
|
||
'enabled' => ! in_array(mb_strtolower($value('enabled')), ['否', 'no', '0', 'false'], true) ? 1 : 0,
|
||
'remark' => $value('remark') !== '' ? mb_substr($value('remark'), 0, 191) : null,
|
||
'updated_at' => now(),
|
||
];
|
||
|
||
$exists = $connection->table(StoreViolationRuleRepository::TABLE)
|
||
->where('platform', $platform)
|
||
->where('violation_type', $payload['violation_type'])
|
||
->exists();
|
||
|
||
if ($this->option('dry-run')) {
|
||
$exists ? $updated++ : $created++;
|
||
continue;
|
||
}
|
||
|
||
if ($exists) {
|
||
$connection->table(StoreViolationRuleRepository::TABLE)
|
||
->where('platform', $platform)
|
||
->where('violation_type', $payload['violation_type'])
|
||
->update($payload);
|
||
$updated++;
|
||
} else {
|
||
$connection->table(StoreViolationRuleRepository::TABLE)->insert($payload + [
|
||
'created_at' => now(),
|
||
]);
|
||
$created++;
|
||
}
|
||
}
|
||
|
||
$this->info(sprintf(
|
||
'字典导入%s:新增 %d 条,更新 %d 条,跳过 %d 条。',
|
||
$this->option('dry-run') ? '(试运行,未写库)' : '完成',
|
||
$created,
|
||
$updated,
|
||
$skipped,
|
||
));
|
||
$this->line('提示:需要让存量记录套用新字典时,执行 store-violations:recalc-alerts。');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
}
|