152 lines
5.0 KiB
PHP
152 lines
5.0 KiB
PHP
<?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);
|
||
}
|
||
}
|