$businessRemarks 账单上的业务备注(二次备注/账务备注/二级备注等) * @return array{rule:array,matched:bool,reason:string}|null */ public function match(string $platform, string $item, array $businessRemarks = [], string $goodsName = ''): ?array { $candidates = []; foreach ($this->store->all() as $rule) { if ($this->sameText((string) $rule['platform'], $platform) && $this->sameText((string) $rule['item'], $item)) { $candidates[] = $rule; } } if ($candidates === []) { return null; } $hits = []; $fallback = null; foreach ($candidates as $rule) { $memo = trim((string) ($rule['memo'] ?? '')); $goods = trim((string) ($rule['goods'] ?? '')); if ($memo === '' && $goods === '') { $fallback ??= $rule; continue; } if ($memo !== '' && ! $this->rematchMemo($memo, $businessRemarks)) { continue; } if ($goods !== '' && ! $this->containsKeyword($goodsName, $goods)) { continue; } $hits[] = ['rule' => $rule, 'score' => ($goods !== '' ? 2 : 0) + ($memo !== '' ? 1 : 0)]; } if ($hits !== []) { usort($hits, static fn (array $a, array $b): int => $b['score'] <=> $a['score'] ?: ((int) $a['rule']['id'] <=> (int) $b['rule']['id'])); return [ 'rule' => $hits[0]['rule'], 'matched' => true, 'reason' => $this->hitsReason($hits[0]['rule'], $businessRemarks, $goodsName), ]; } $rule = $fallback ?? $candidates[0]; return [ 'rule' => $rule, 'matched' => false, 'reason' => $fallback !== null ? '没有命中具体条件,用同平台同项目的兜底规则' : '没有命中任何条件,也没有兜底规则,暂按第一条处理(需要在映射表里补条件)', ]; } /** @param array $businessRemarks */ private function rematchMemo(string $keyword, array $businessRemarks): bool { foreach ($businessRemarks as $value) { $value = trim((string) $value); if ($value === '' || $this->isPeriodLabel($value)) { continue; } if ($this->containsKeyword($value, $keyword)) { return true; } } return false; } /** 形如 202607订单 / 2311退款 的时间标签,不参与业务备注匹配 */ public function isPeriodLabel(string $value): bool { return preg_match(self::PERIOD_PATTERN, trim($value)) === 1; } private function containsKeyword(string $haystack, string $keyword): bool { $needles = preg_split('/[\s,,、|]+/u', $keyword) ?: []; $haystack = $this->squash($haystack); foreach ($needles as $needle) { $needle = $this->squash($needle); if ($needle !== '' && ! str_contains($haystack, $needle)) { return false; } } return $haystack !== ''; } /** 去空格、去大小写差异,便于「推广费-百亿补贴」这类带前后缀的备注命中 */ private function squash(string $value): string { return mb_strtolower(preg_replace('/\s+/u', '', $value) ?? $value); } private function sameText(string $a, string $b): bool { return $this->squash($a) === $this->squash($b); } /** @param array $rule @param array $businessRemarks */ private function hitsReason(array $rule, array $businessRemarks, string $goodsName): string { $parts = []; if (trim((string) ($rule['memo'] ?? '')) !== '') { $parts[] = '业务备注命中「' . trim((string) $rule['memo']) . '」'; } if (trim((string) ($rule['goods'] ?? '')) !== '') { $parts[] = '商品名称命中「' . trim((string) $rule['goods']) . '」'; } return $parts === [] ? '兜底规则' : implode(',', $parts); } }