connection = $databases->connection( (string) config('upstream_mirrors.connection', 'business_mirror'), ); } public static function initialEffectiveAt(): string { return StoreReportInitialPeriods::effectiveAt(); } /** * @param array> $stores 报表店铺行;只需要 erp_store_name,company_name 作为系统主体缺失时的回退值。 * @return array> */ public function build(array $stores): array { $names = []; $fallbackCompanies = []; $knownEntities = []; foreach ($stores as $store) { $name = trim((string) ($store['erp_store_name'] ?? '')); if ($name === '') { continue; } $names[$name] = $name; $company = trim((string) ($store['company_name'] ?? '')); if ($company !== '') { $fallbackCompanies[$name] = $company; } // 店铺行已带系统主体时不再重复查询镜像;null 表示系统确实没有登记主体。 if (array_key_exists('system_company_name', $store)) { $knownEntities[$name] = $this->cleanCompany($store['system_company_name']); } } return $this->assemble(array_values($names), $fallbackCompanies, $knownEntities); } /** * 店铺维度报表当前仍存续的主体:失效时间为空,且生效时间最新。 * * @param array> $stores * @return array */ public function currentCompanies(array $stores): array { $result = []; foreach ($this->build($stores) as $storeName => $periods) { $active = null; foreach ($periods as $period) { if (($period['expires_at'] ?? null) !== null) { continue; } $company = $this->cleanCompany($period['company_name'] ?? null); if ($company === null) { continue; } $effectiveAt = $this->date($period['effective_at'] ?? null) ?? ''; if ($active === null || strcmp($effectiveAt, $active['effective_at']) > 0) { $active = [ 'company_name' => $company, 'effective_at' => $effectiveAt, ]; } } if ($active !== null) { $result[$storeName] = $active['company_name']; } } return $result; } /** * @param list $names * @param array $fallbackCompanies * @param array $knownEntities * @return array> */ private function assemble(array $names, array $fallbackCompanies, array $knownEntities = []): array { if ($names === []) { return []; } $initial = StoreReportInitialPeriods::shops(); $initialDate = self::initialEffectiveAt(); $missing = array_values(array_diff($names, array_keys($knownEntities))); $entities = $knownEntities + $this->systemEntities($missing); $changes = $this->crawledChanges($names); $periods = []; foreach ($names as $name) { $rule = $initial[$name] ?? null; $changedAt = is_array($rule) ? $this->date($rule['changed_at'] ?? null) : null; // 变更日必须晚于期初日,否则老主体期间为空,直接按新主体从期初日起算。 if (is_array($rule) && $changedAt !== null && $changedAt > $initialDate) { $list = [ [ 'company_name' => $this->cleanCompany($rule['previous_company'] ?? null), 'effective_at' => $initialDate, 'expires_at' => $this->dayBefore($changedAt), 'source' => 'initial', ], [ 'company_name' => $this->cleanCompany($rule['company'] ?? null), 'effective_at' => $changedAt, 'expires_at' => null, 'source' => 'initial', ], ]; } else { $list = [[ 'company_name' => (is_array($rule) ? $this->cleanCompany($rule['company'] ?? null) : null) ?? ($entities[$name] ?? ($fallbackCompanies[$name] ?? null)), 'effective_at' => $initialDate, 'expires_at' => null, 'source' => 'baseline', ]]; } foreach ($changes[$name] ?? [] as $change) { $company = $this->cleanCompany($change['company_name'] ?? null); $observed = $this->date($change['observed_on'] ?? null); if ($company === null || $observed === null || $observed < $initialDate) { continue; } $last = &$list[count($list) - 1]; if ($this->sameCompany($last['company_name'], $company)) { continue; } // 变更必须晚于当前期间生效日,避免爬虫记录早于期初数据时产生重叠。 if (strcmp($observed, (string) $last['effective_at']) <= 0) { continue; } $last['expires_at'] = $this->dayBefore($observed); $list[] = [ 'company_name' => $company, 'effective_at' => $observed, 'expires_at' => null, 'source' => 'crawl', ]; unset($last); } unset($last); $periods[$name] = $list; } return $periods; } /** * 财务系统登记的店铺主体(与 rpt.fys_shopInfo + fys_companyInfo 一致)。 * * @param list $names * @return array */ private function systemEntities(array $names): array { $entities = []; foreach (array_chunk($names, 500) as $chunk) { $placeholders = implode(',', array_fill(0, count($chunk), '?')); try { $rows = $this->connection->select( "SELECT s.erpShopName AS store_name, c.businessLicenseName AS company_name FROM ops_fys_shop_info AS s LEFT JOIN ( SELECT unifiedSocialCreditCode, MAX(Id) AS latest_id FROM ops_fys_company_info WHERE unifiedSocialCreditCode IS NOT NULL GROUP BY unifiedSocialCreditCode ) AS latest ON latest.unifiedSocialCreditCode = s.unifiedSocialCreditCode LEFT JOIN ops_fys_company_info AS c ON c.Id = latest.latest_id WHERE s.erpShopName IN ({$placeholders})", $chunk, ); } catch (Throwable) { return []; } foreach ($rows as $row) { $name = trim((string) ($row->store_name ?? '')); $company = $this->cleanCompany($row->company_name ?? null); if ($name !== '' && $company !== null) { $entities[$name] = $company; } } } return $entities; } /** * 爬虫首次抓到的新主体记录,按抓到日期升序构成后续期间。 * * @param list $names * @return array> */ private function crawledChanges(array $names): array { $changes = []; foreach (array_chunk($names, 500) as $chunk) { $placeholders = implode(',', array_fill(0, count($chunk), '?')); try { $rows = $this->connection->select( "SELECT erp_store_name, company_name, observed_on FROM " . self::CHANGES_TABLE . " WHERE erp_store_name IN ({$placeholders}) ORDER BY observed_on ASC, id ASC", $chunk, ); } catch (Throwable) { return []; } foreach ($rows as $row) { $name = trim((string) ($row->erp_store_name ?? '')); if ($name === '') { continue; } $changes[$name][] = [ 'company_name' => $row->company_name ?? null, 'observed_on' => $row->observed_on ?? null, ]; } } return $changes; } private function cleanCompany(mixed $value): ?string { $company = trim((string) ($value ?? '')); return $company === '' ? null : $company; } private function sameCompany(?string $left, ?string $right): bool { return mb_strtolower(trim((string) $left)) === mb_strtolower(trim((string) $right)); } private function date(mixed $value): ?string { $value = trim((string) ($value ?? '')); if ($value === '') { return null; } try { return Carbon::parse($value)->format('Y-m-d'); } catch (Throwable) { return null; } } private function dayBefore(string $date): string { return Carbon::parse($date)->subDay()->format('Y-m-d'); } }