CaiWuXiTong/modules/EcommerceFinance/Services/StoreReportInitialPeriods.php
WuYongJie 78857a51d9 feat: 店铺维度报表按主体期间拆分并支持期初数据维护
- 改为按期初数据+系统主体+爬虫新主体生成主体期间,一行一期间,修复流程历史重复行
- 变更日当天归新主体、老主体失效显示变更日前一天;关店和其他非正常状态两列显示 -
- 期初数据首次打开固化为系统库设置,可在「规则信息 → 店铺维度报表逻辑配置」在线增删改
- 爬虫店铺主体按税号归一,首次抓到的新主体写入期间变更表并自动生成新期间
- 新增期间变更表迁移与 crawl_shop_company 镜像数据集
2026-10-06 18:42:12 +08:00

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();
}
}