From a8c01a2534c946ebae2121062dbfd11afec5bf44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E6=9C=A8?= <594326199@qq.com> Date: Tue, 13 Oct 2026 20:03:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=97=E9=93=BA=E8=BF=9D=E8=A7=84=E9=A2=84?= =?UTF-8?q?=E8=AD=A6=EF=BC=9A=E4=BF=AE=E5=A4=8D=E9=87=8D=E7=AE=97=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E4=B8=A5=E6=A0=BC=E6=A8=A1=E5=BC=8F=E6=8A=A5=E9=94=99?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E8=BF=9D=E8=A7=84=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 行 --- .../Commands/ImportStoreViolationRules.php | 177 ++++++++++++++++++ .../Commands/RecalcStoreViolationAlerts.php | 10 +- 2 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/ImportStoreViolationRules.php diff --git a/app/Console/Commands/ImportStoreViolationRules.php b/app/Console/Commands/ImportStoreViolationRules.php new file mode 100644 index 00000000..18fb67d2 --- /dev/null +++ b/app/Console/Commands/ImportStoreViolationRules.php @@ -0,0 +1,177 @@ + ['平台', '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; + } +} diff --git a/app/Console/Commands/RecalcStoreViolationAlerts.php b/app/Console/Commands/RecalcStoreViolationAlerts.php index 1698581e..2e381924 100644 --- a/app/Console/Commands/RecalcStoreViolationAlerts.php +++ b/app/Console/Commands/RecalcStoreViolationAlerts.php @@ -34,7 +34,7 @@ class RecalcStoreViolationAlerts extends Command $total = 0; $query = $connection->table(StoreViolationRecordRepository::TABLE) - ->select(['id', 'platform', 'shop_name', 'violation_type', 'violation_reason', 'violation_date', 'appeal_deadline']) + ->select(['id', 'platform', 'violation_no', 'shop_name', 'violation_type', 'violation_reason', 'violation_date', 'appeal_deadline']) ->orderBy('id'); if ($platform !== '') { $query->where('platform', $platform); @@ -47,7 +47,13 @@ class RecalcStoreViolationAlerts extends Command ? (string) $row->violation_type : ($row->violation_reason ?? null); - $updates[] = ['id' => $row->id] + $calculator->calculate( + // upsert 的插入侧会校验非空列,带上 platform / violation_no 以通过严格模式; + // 命中已有记录时只会更新派生字段(update 列表)。 + $updates[] = [ + 'id' => $row->id, + 'platform' => (string) $row->platform, + 'violation_no' => (string) ($row->violation_no ?? ''), + ] + $calculator->calculate( (string) $row->platform, $row->shop_name, $matchType, -- 2.47.2