CaiWuXiTong/app/Console/Commands/ImportStoreViolationRules.php
苏木 a8c01a2534 店铺违规预警:修复重算命令严格模式报错,新增违规字典导入命令
- RecalcStoreViolationAlerts:upsert 插入侧补齐 platform / violation_no,避免 MySQL 严格模式报错(Field platform doesn't have a default value)
- 新增 store-violations:import-rules:Excel 维护违规字典(平台/分类/类型/关键词/级别/需提醒/建议动作/时效/提醒方式/排序/启用/备注),按 平台+违规类型 幂等更新,支持 --dry-run
- 生产已用等价脚本完成全量重算:11,528 行,需提醒 7,040 行(61%),责任人覆盖 11,517 行
2026-10-13 20:03:26 +08:00

178 lines
7.0 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 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 {
$sheet = IOFactory::load($file)->getActiveSheet();
$matrix = $sheet->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(),
];
if ($this->option('dry-run')) {
$skipped += 0;
$updated += 0;
$created += 0;
continue;
}
$exists = $connection->table(StoreViolationRuleRepository::TABLE)
->where('platform', $platform)
->where('violation_type', $payload['violation_type'])
->exists();
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;
}
}