- 改为按期初数据+系统主体+爬虫新主体生成主体期间,一行一期间,修复流程历史重复行 - 变更日当天归新主体、老主体失效显示变更日前一天;关店和其他非正常状态两列显示 - - 期初数据首次打开固化为系统库设置,可在「规则信息 → 店铺维度报表逻辑配置」在线增删改 - 爬虫店铺主体按税号归一,首次抓到的新主体写入期间变更表并自动生成新期间 - 新增期间变更表迁移与 crawl_shop_company 镜像数据集
235 lines
7.6 KiB
PHP
235 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Services;
|
|
|
|
/**
|
|
* 「店铺维度报表逻辑配置」里可维护的期初主体数据。
|
|
*
|
|
* 首次读取时把 config/store_report_initial_periods.php 的内容复制成设置值,
|
|
* 之后页面的新增、修改、删除都写设置,不再依赖代码文件。
|
|
*/
|
|
class StoreReportInitialPeriods
|
|
{
|
|
private const PREFIX = 'store_report.initial_periods';
|
|
|
|
private const EFFECTIVE_AT_KEY = self::PREFIX . '.effective_at';
|
|
|
|
public const DEFAULT_EFFECTIVE_AT = '2026-07-01';
|
|
|
|
/** @return array{initial_effective_at:string,shops:array<string,array{previous_company:string,company:string,changed_at:string}>} */
|
|
public static function current(): array
|
|
{
|
|
if (! self::seeded()) {
|
|
return self::fromConfig();
|
|
}
|
|
|
|
$shops = [];
|
|
foreach (self::storedShopNames() as $name) {
|
|
$rule = self::storedRule($name);
|
|
if ($rule !== null) {
|
|
$shops[$name] = $rule;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'initial_effective_at' => self::cleanDate(setting(self::EFFECTIVE_AT_KEY, null)) ?? self::DEFAULT_EFFECTIVE_AT,
|
|
'shops' => self::normalizeShops($shops),
|
|
];
|
|
}
|
|
|
|
public static function effectiveAt(): string
|
|
{
|
|
return self::current()['initial_effective_at'];
|
|
}
|
|
|
|
/** @return array<string,array{previous_company:string,company:string,changed_at:string}> */
|
|
public static function shops(): array
|
|
{
|
|
return self::current()['shops'];
|
|
}
|
|
|
|
public static function seeded(): bool
|
|
{
|
|
return setting(self::signatureKey(), null) !== null;
|
|
}
|
|
|
|
/** 把代码文件里的期初数据固化成可编辑的设置值。 */
|
|
public static function seedFromConfig(): void
|
|
{
|
|
if (self::seeded()) {
|
|
return;
|
|
}
|
|
|
|
$config = self::fromConfig();
|
|
$payload = [self::EFFECTIVE_AT_KEY => $config['initial_effective_at']];
|
|
foreach ($config['shops'] as $name => $rule) {
|
|
$payload += self::rulePayload($name, $rule);
|
|
}
|
|
$payload[self::shopListKey()] = implode("\n", array_keys($config['shops']));
|
|
$payload[self::signatureKey()] = (string) count($config['shops']);
|
|
|
|
setting($payload)->save();
|
|
}
|
|
|
|
/** @param array<string,mixed> $shops */
|
|
public static function saveShops(array $shops, ?string $effectiveAt = null): void
|
|
{
|
|
$current = self::current();
|
|
$normalized = self::normalizeShops($shops);
|
|
$payload = [
|
|
self::EFFECTIVE_AT_KEY => self::cleanDate($effectiveAt) ?? $current['initial_effective_at'],
|
|
self::shopListKey() => implode("\n", array_keys($normalized)),
|
|
self::signatureKey() => (string) count($normalized),
|
|
];
|
|
foreach ($normalized as $name => $rule) {
|
|
$payload += self::rulePayload($name, $rule);
|
|
}
|
|
|
|
setting($payload)->save();
|
|
|
|
// 删除本次保存后不再存在的店铺键。
|
|
foreach ($current['shops'] as $name => $rule) {
|
|
if (isset($normalized[$name])) {
|
|
continue;
|
|
}
|
|
self::forgetKeys($name);
|
|
}
|
|
}
|
|
|
|
/** @return array{previous_company:string,company:string,changed_at:string}|null */
|
|
public static function shop(string $storeName): ?array
|
|
{
|
|
return self::shops()[trim($storeName)] ?? null;
|
|
}
|
|
|
|
/** @param array{previous_company:string,company:string,changed_at:string} $rule */
|
|
public static function putShop(string $storeName, array $rule): void
|
|
{
|
|
$shops = self::shops();
|
|
$shops[trim($storeName)] = $rule;
|
|
self::saveShops($shops);
|
|
}
|
|
|
|
public static function forgetShop(string $storeName): void
|
|
{
|
|
$shops = self::shops();
|
|
$name = trim($storeName);
|
|
unset($shops[$name]);
|
|
self::saveShops($shops);
|
|
self::forgetKeys($name);
|
|
}
|
|
|
|
/**
|
|
* @return array{initial_effective_at:string,shops:array<string,array{previous_company:string,company:string,changed_at:string}>}
|
|
*/
|
|
private static function fromConfig(): array
|
|
{
|
|
return [
|
|
'initial_effective_at' => self::cleanDate(config('store_report_initial_periods.initial_effective_at'))
|
|
?? self::DEFAULT_EFFECTIVE_AT,
|
|
'shops' => self::normalizeShops((array) config('store_report_initial_periods.shops', [])),
|
|
];
|
|
}
|
|
|
|
/** @param array<string,mixed> $shops */
|
|
private static function normalizeShops(array $shops): array
|
|
{
|
|
$clean = [];
|
|
foreach ($shops as $storeName => $rule) {
|
|
$name = trim((string) $storeName);
|
|
if ($name === '' || ! is_array($rule)) {
|
|
continue;
|
|
}
|
|
$changedAt = self::cleanDate($rule['changed_at'] ?? null);
|
|
if ($changedAt === null) {
|
|
continue;
|
|
}
|
|
$clean[$name] = [
|
|
'previous_company' => trim((string) ($rule['previous_company'] ?? '')),
|
|
'company' => trim((string) ($rule['company'] ?? '')),
|
|
'changed_at' => $changedAt,
|
|
];
|
|
}
|
|
|
|
uasort($clean, static fn (array $left, array $right): int => strcmp($left['changed_at'], $right['changed_at']));
|
|
|
|
return $clean;
|
|
}
|
|
|
|
private static function cleanDate(mixed $value): ?string
|
|
{
|
|
$value = trim((string) ($value ?? ''));
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/D', $value) !== 1) {
|
|
return null;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
private static function shopKey(string $storeName): string
|
|
{
|
|
return sha1(mb_strtolower(trim($storeName)));
|
|
}
|
|
|
|
private static function shopListKey(): string
|
|
{
|
|
return self::PREFIX . '.shop_list';
|
|
}
|
|
|
|
private static function signatureKey(): string
|
|
{
|
|
return self::PREFIX . '.seeded';
|
|
}
|
|
|
|
private static function ruleKey(string $storeName, string $field): string
|
|
{
|
|
return self::PREFIX . '.shop.' . self::shopKey($storeName) . '.' . $field;
|
|
}
|
|
|
|
/** @return list<string> */
|
|
private static function storedShopNames(): array
|
|
{
|
|
$list = setting(self::shopListKey(), '');
|
|
$list = is_array($list) ? implode("\n", $list) : (string) $list;
|
|
|
|
return array_values(array_filter(array_map('trim', explode("\n", $list))));
|
|
}
|
|
|
|
/** @return array{previous_company:string,company:string,changed_at:string}|null */
|
|
private static function storedRule(string $storeName): ?array
|
|
{
|
|
$changedAt = self::cleanDate(setting(self::ruleKey($storeName, 'changed_at'), null));
|
|
if ($changedAt === null) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'previous_company' => trim((string) setting(self::ruleKey($storeName, 'previous_company'), '')),
|
|
'company' => trim((string) setting(self::ruleKey($storeName, 'company'), '')),
|
|
'changed_at' => $changedAt,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array{previous_company:string,company:string,changed_at:string} $rule
|
|
* @return array<string,string>
|
|
*/
|
|
private static function rulePayload(string $storeName, array $rule): array
|
|
{
|
|
return [
|
|
self::ruleKey($storeName, 'previous_company') => $rule['previous_company'],
|
|
self::ruleKey($storeName, 'company') => $rule['company'],
|
|
self::ruleKey($storeName, 'changed_at') => $rule['changed_at'],
|
|
self::ruleKey($storeName, 'name') => trim($storeName),
|
|
];
|
|
}
|
|
|
|
private static function forgetKeys(string $storeName): void
|
|
{
|
|
foreach (['previous_company', 'company', 'changed_at', 'name'] as $field) {
|
|
setting()->forget(self::ruleKey($storeName, $field));
|
|
}
|
|
setting()->save();
|
|
}
|
|
}
|