>|null */ private ?array $rules = null; /** @var array|null */ private ?array $ownerMap = null; /** @var array */ private array $ownerFallback = []; public function __construct( private readonly StoreViolationRuleRepository $ruleRepository, private readonly StoreAccountMirrorRepository $storeAccounts, ) { } public function flush(): void { $this->rules = null; $this->ownerMap = null; $this->ownerFallback = []; } /** @return array 预警相关字段(用于同步/导入写入) */ public function calculate( string $platform, ?string $shopName, ?string $violationType, ?string $violationDate, ?string $appealDeadline, ): array { $rule = self::matchRule($this->rules(), $platform, $violationType); $needsAlert = $rule !== null && (bool) ($rule['needs_alert'] ?? true); $level = null; if ($needsAlert) { $level = trim((string) ($rule['alert_level'] ?? '')); if ($level === '' || ! array_key_exists($level, (array) config('store_violations.alert_levels', []))) { $level = 'P2'; } } $hours = isset($rule['handling_hours']) && $rule['handling_hours'] !== null ? (int) $rule['handling_hours'] : null; return [ 'needs_alert' => $needsAlert, 'alert_level' => $level, 'alert_rule_id' => $needsAlert ? ($rule['id'] ?? null) : null, 'alert_action' => $needsAlert ? self::nullableText($rule['suggested_action'] ?? null, 500) : null, 'alert_channel' => $needsAlert ? self::nullableText($rule['remind_channel'] ?? null, 64) : null, 'handling_hours' => $needsAlert ? $hours : null, 'owner_name' => $this->ownerFor($shopName), 'handling_deadline' => self::deadlineFor($violationDate, $appealDeadline, $hours, $level), ]; } /** @return list> */ private function rules(): array { if ($this->rules === null) { try { $this->rules = $this->ruleRepository->activeRules(); } catch (Throwable) { $this->rules = []; } } return $this->rules; } /** * 清理违规类型原文:反复去掉成对括号(含处置说明)并去除空白。 */ public static function cleanViolationType(?string $text): string { $text = trim((string) $text); if ($text === '') { return ''; } $previous = null; while ($previous !== $text) { $previous = $text; $text = preg_replace('/[((【\[][^((【\[))】\]]*[))】\]]/u', '', $text) ?? $text; } return preg_replace('/[\s\x{3000}]+/u', '', $text) ?? $text; } private static function loose(string $text): string { return preg_replace('/[\/、,,。.;;::·\-—_]+/u', '', $text) ?? $text; } /** * 按顺序匹配字典:相等 → 前缀 → 分词包含 → 宽松包含 → 关键词包含。 * * @param list> $rules 已按优先级排序 * @return array|null */ public static function matchRule(array $rules, string $platform, ?string $violationType): ?array { $record = self::cleanViolationType($violationType); if ($record === '') { return null; } $looseRecord = self::loose($record); foreach ($rules as $rule) { if (trim((string) ($rule['platform'] ?? '')) !== $platform) { continue; } $ruleType = self::cleanViolationType($rule['violation_type'] ?? null); if ($ruleType !== '') { if ($record === $ruleType || str_starts_with($record, $ruleType)) { return $rule; } foreach (preg_split('/[\/、]/u', $ruleType) ?: [] as $part) { $part = trim((string) $part); if ($part !== '' && mb_strlen($part) >= 2 && mb_stripos($record, $part) !== false) { return $rule; } } $looseRule = self::loose($ruleType); if ($looseRule !== '' && str_contains($looseRecord, $looseRule)) { return $rule; } } foreach (preg_split('/[|、,\/]/u', (string) ($rule['match_keywords'] ?? '')) ?: [] as $keyword) { $keyword = self::loose(self::cleanViolationType($keyword)); if ($keyword !== '' && str_contains($looseRecord, $keyword)) { return $rule; } } } return null; } /** * 处理截止:申诉截止优先;否则字典时效(小时)或级别默认天数(当天 23:59:59)。 */ public static function deadlineFor( ?string $violationDate, ?string $appealDeadline, ?int $handlingHours, ?string $alertLevel, ): ?string { $appeal = trim((string) $appealDeadline); if ($appeal !== '') { try { return Carbon::parse($appeal)->endOfDay()->toDateTimeString(); } catch (Throwable) { // 解析失败时退回按级别计算 } } $date = trim((string) $violationDate); if ($date === '') { return null; } try { $base = Carbon::parse($date)->startOfDay(); } catch (Throwable) { return null; } if ($handlingHours !== null && $handlingHours > 0) { return $base->addHours($handlingHours)->toDateTimeString(); } $level = trim((string) $alertLevel); if ($level === '') { return null; } $days = (int) (config('store_violations.alert_default_days.' . $level) ?? 0); return $days > 0 ? $base->addDays($days)->endOfDay()->toDateTimeString() : null; } /** 是否超时:处理状态不在终态且当前时间已过处理截止。 */ public static function isOverdue(mixed $handlingStatus, mixed $handlingDeadline, ?Carbon $now = null): bool { $status = trim((string) $handlingStatus); if (in_array($status, ['处理完成', '已忽略', '已处理', '无需处理'], true)) { return false; } $deadline = trim((string) $handlingDeadline); if ($deadline === '') { return false; } try { return ($now ?? Carbon::now())->greaterThan(Carbon::parse($deadline)); } catch (Throwable) { return false; } } private function ownerFor(?string $shopName): ?string { $name = trim((string) $shopName); if ($name === '') { return null; } try { if ($this->ownerMap === null) { $this->ownerMap = $this->storeAccounts->ownershipByStoreName(); } $key = StoreAccountMirrorRepository::storeNameKey($name); $owner = trim((string) ($this->ownerMap[$key]['operation_owner'] ?? '')); if ($owner !== '') { return mb_substr($owner, 0, 64); } } catch (Throwable) { $this->ownerMap ??= []; } // 兜底:店铺档案(经营分析副本库)的店铺运营负责人。 if (array_key_exists($name, $this->ownerFallback)) { return $this->ownerFallback[$name]; } $head = null; try { $row = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')) ->table('ops_fys_shop_info') ->where('shopName', $name) ->orderByDesc('id') ->first(['shopHead']); $head = self::nullableText($row->shopHead ?? null, 64); } catch (Throwable) { $head = null; } return $this->ownerFallback[$name] = $head; } private static function nullableText(mixed $value, int $limit): ?string { $text = trim((string) $value); return $text === '' ? null : mb_substr($text, 0, $limit); } }