$platform) { if ($normalized === $slug) { return $slug; } foreach ($platform['source_values'] ?? [] as $sourceValue) { if ($normalized === mb_strtolower(trim((string) $sourceValue))) { return $slug; } } } return null; } public function categoryForContent(?string $content): string { $content = trim((string) $content); if ($content === '') { return (string) config('store_violations.default_category', '其他'); } foreach ((array) config('store_violations.category_rules', []) as $category => $keywords) { foreach ((array) $keywords as $keyword) { if ($keyword !== '' && mb_stripos($content, (string) $keyword) !== false) { return (string) $category; } } } return (string) config('store_violations.default_category', '其他'); } /** * 上游抓取行(rpt.crawl_shop_violation)→ 统一字段。 * * @param array $row * @return array|null 平台无法识别时返回 null */ public function mapCrawlRow(array $row): ?array { $platform = $this->platformForSourceValue($row['platform'] ?? null); if ($platform === null) { return null; } $content = trim((string) ($row['violation_content'] ?? '')); return [ 'platform' => $platform, 'shop_name' => mb_substr(trim((string) ($row['shop_name'] ?? '')), 0, 191), 'violation_type' => $content === '' ? null : mb_substr($content, 0, 500), 'violation_category' => $this->categoryForContent($content), // 上游视图补齐这两列后由同步命令自动带出(见 SyncStoreViolations 的列探测)。 'violation_reason' => $this->normalizeLabel($row['violation_reason'] ?? null, 500), 'disposition' => $this->normalizeLabel($row['disposition'] ?? null, 500), 'violation_no' => mb_substr(trim((string) ($row['violation_no'] ?? '')), 0, 100), 'violation_date' => $this->normalizeDate($row['violation_date'] ?? null), 'status' => $this->normalizeLabel($row['status'] ?? null, 64), 'appeal_deadline' => $this->normalizeDate($row['deadline'] ?? null), 'window_name' => $this->normalizeLabel($row['window_name'] ?? null, 191), 'crawl_date' => $this->normalizeDate($row['crawl_date'] ?? null), 'source' => 'crawl', ]; } public function normalizeDate(mixed $value): ?string { if ($value === null || $value === '') { return null; } if ($value instanceof DateTimeInterface) { return $value->format('Y-m-d'); } if (is_numeric($value)) { $number = (float) $value; if ($number >= 20000 && $number <= 80000) { return ExcelDate::excelToDateTimeObject($number)->format('Y-m-d'); } } $text = trim((string) $value); if ($text === '' || in_array($text, ['-', '—', '无'], true)) { return null; } $text = str_replace(['年', '月'], '-', str_replace('日', '', $text)); $text = str_replace(['/', '.'], '-', $text); $text = trim(preg_replace('/\s+/', ' ', $text) ?? $text); try { return (new DateTimeImmutable($text))->format('Y-m-d'); } catch (\Throwable) { throw new InvalidArgumentException('无法识别的日期:' . $value); } } public function normalizePoints(mixed $value): ?float { if ($value === null || $value === '') { return null; } if (is_numeric($value)) { return (float) $value; } $text = trim((string) $value); if ($text === '' || in_array($text, ['-', '—', '无', '不扣分'], true)) { return null; } $text = preg_replace('/(分|点|个?扣|扣除)/u', '', $text) ?? $text; $text = trim($text); if ($text === '' || ! is_numeric($text)) { throw new InvalidArgumentException('无法识别的扣分:' . $value); } return (float) $text; } private function normalizeLabel(mixed $value, int $limit): ?string { $text = trim((string) $value); return $text === '' ? null : mb_substr($text, 0, $limit); } }