CaiWuXiTong/app/Business/StoreViolationMapper.php
苏木 8e93e71024 店铺违规预警:新增违规原因与处置方式两列(页面/统一表/导入导出)
- 页面在「违规分类」后新增「违规原因」「处置方式」,统一表头、导出、模板、导入同步支持
- 新迁移:统一表 store_violation_records 增加 violation_reason、disposition 两列
- 同步命令:上游视图补齐 violation_reason/disposition(或 reason/handling_method)后自动参与同步;未提供时不触碰这两列,导入补充的数据不会被每日同步清空
- 导入:违规原因/处置方式独立映射;仅提供违规原因时,违规分类按原因关键词自动归类
- 测试:StoreViolation*、NavigationTest 共 23 项通过(含新导入用例与列探测用例)
2026-10-13 11:31:52 +08:00

152 lines
5.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\Business;
use DateTimeImmutable;
use DateTimeInterface;
use InvalidArgumentException;
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
/**
* 店铺违规信息预警:上游抓取数据与导入文件到统一表头的映射。
*/
class StoreViolationMapper
{
public function platformForSourceValue(mixed $value): ?string
{
$normalized = mb_strtolower(trim((string) $value));
if ($normalized === '') {
return null;
}
foreach ((array) config('store_violations.platforms', []) as $slug => $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<string,mixed> $row
* @return array<string,mixed>|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);
}
}