feat(admin): ручные варианты UTM и независимый редактор config (#30)

This commit is contained in:
Panfilov Konstantin committed 2026-10-26 05:42:03 +03:00
1 parent 47405b27d8
commit 941a4f01aa
28 files changed
+108 -174

No files matched your search

-14
View File
@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace PrelandAdmin;
final class Catalog
{
public static function read(Store $store,\stdClass $config): ?array {
$catalog=$store->read('catalog.json',null);if(!is_array($catalog)||!is_string($catalog['generated_at']??null))return null;
$at=strtotime($catalog['generated_at']);if($at===false||$at<time()-180||$at>time()+30)return null;
$host=static fn($value)=>strtolower(rtrim((string)parse_url(str_contains((string)$value,'://')?(string)$value:'https://'.$value,PHP_URL_HOST),'.'));
if(!$host($config->host??'')||$host($catalog['host']??'')!==$host($config->host??''))return null;
foreach(['degr','tp','cons'] as $group){if(!is_array($catalog['groups'][$group]??null))return null;foreach($catalog['groups'][$group] as $entry)if(!is_array($entry)||!is_int($entry['number']??null)||$entry['number']<1||$entry['number']>9||!is_string($entry['name']??null))return null;}
return $catalog;
}
}
+2 -9
View File
@@ -54,7 +54,7 @@ final class Config
}
$change['path']=$path;return $change;
}
public static function validate(\stdClass $config, callable $setExists,?array $catalog=null): array {
public static function validate(\stdClass $config): array {
self::assertWritable($config);
PercentWeights::validate($config);
if (isset($config->host)) {
@@ -74,7 +74,7 @@ final class Config
$number=static function($value) use($maps,$canonical): string {
if (!is_int($value) && !is_string($value)) throw new Problem('Вариант UTM должен быть номером.');
$value=(string)$value;
if (isset($maps[$canonical])) { $digit=array_search($value,$maps[$canonical],true); if ($digit!==false) return (string)$digit; if (!isset($maps[$canonical][$value])) throw new Problem("Недопустимый вариант {$canonical}."); }
if (isset($maps[$canonical])) { $digit=array_search($value,$maps[$canonical],true); if ($digit!==false) return (string)$digit; }
if (!preg_match('/^[1-9]$/D',$value)) throw new Problem('Вариант UTM должен быть 1–9.');
return $value;
};
@@ -83,15 +83,8 @@ final class Config
$digit=$number($variant);
if (isset($seen[$digit])) throw new Problem('Конфликт semantic и numeric вариантов UTM.'); $seen[$digit]=true;
if ((!is_int($weight) && !is_float($weight)) || !is_finite((float)$weight) || $weight<0) throw new Problem('Вес должен быть конечным неотрицательным числом.');
if ($canonical==='indpic' && !$setExists((int)$digit)) throw new Problem("Набор {$digit} отсутствует или не прошёл проверку.");
if($catalog!==null&&in_array($canonical,['degr','tp','cons'],true)&&!in_array((int)$digit,array_column($catalog['groups'][$canonical]??[],'number'),true))throw new Problem("Группа {$canonical} {$digit} отсутствует в доступном каталоге основного сайта.");
}
if (in_array($canonical,['indpic','degr','tp'],true)&&!isset($seen[$default])) throw new Problem("Добавьте вариант {$default} в таблицу весов {$key} (допустим вес 0).");
if ($canonical==='indpic' && !$setExists((int)$default)) throw new Problem("Набор default {$default} отсутствует или не прошёл проверку.");
if (in_array($canonical,['degr','tp','cons'],true)) {
if($catalog===null)$warnings[]="{$canonical}: существование группы на основном сайте не проверено.";
elseif(!in_array((int)$default,array_column($catalog['groups'][$canonical]??[],'number'),true))throw new Problem("Группа default {$canonical} {$default} отсутствует в доступном каталоге основного сайта.");
}
}
return $warnings;
}
+2 -2
View File
@@ -93,8 +93,8 @@ final class Copy
$changes[]=['path'=>['utm',$canonical,'default'],'value'=>$default];
$changes[]=['path'=>['utm',$canonical,'weights'],'value'=>$weights];
}
// Наличие физических наборов и каталог проверяются отдельно на каждом получателе.
Config::validate($projection,static fn(int $number): bool=>true);
// Редактор проверяет формат конфига, а не реализацию вариантов на сайтах.
Config::validate($projection);
return $changes;
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ final class Jobs
if(isset($p['changes']))foreach($p['changes'] as &$change){if(isset($change['value_json']))$change['value']=json_decode($change['value_json'],false,64,JSON_THROW_ON_ERROR);}unset($change);
if($type==='copy')Copy::assertAliases($current['config']);
$next=$type==='restore'?Config::decode($this->store->backupContent($target['id'],$p['id'])):Config::patch($current['config'],$p['changes']??[]);
$checked=[];$warnings=Config::validate($next,function($number)use($target,&$checked){if(array_key_exists($number,$checked))return $checked[$number];try{(new Sets($this->store))->check($target,$number);return $checked[$number]=true;}catch(\Throwable){return $checked[$number]=false;}},Catalog::read($this->store,$next));
$warnings=Config::validate($next);
$content=Config::encode($next);$job['results'][$index]['desired_hash']=hash('sha256',$content);$save();
$written=$transport->writeConfig($expected,$content);$out=['message'=>'Настройки сохранены и проверены.','version'=>$written['version'],'warnings'=>$warnings];
}else{
+1 -3
View File
@@ -7,7 +7,6 @@ namespace PrelandAdmin;
final class LinkBuilder
{
private const KEYS = ['indpic', 'theme', 'degr', 'reg', 'banch', 'tp', 'cons', 'focus'];
private const ENUMS = ['theme', 'reg', 'banch', 'focus'];
public function __construct(private readonly Store $store) {}
@@ -72,8 +71,7 @@ final class LinkBuilder
$selection = [];
foreach (self::KEYS as $key) {
$value = $input[$key] ?? null;
if (!is_string($value) || ($value !== 'rnd' && !preg_match('/^[1-9]$/D', $value))
|| (in_array($key, self::ENUMS, true) && !in_array($value, ['1', '2', 'rnd'], true))) {
if (!is_string($value) || ($value !== 'rnd' && !preg_match('/^[1-9]$/D', $value))) {
throw new Problem("Недопустимое значение параметра {$key} конструктора ссылки.");
}
$selection[$key] = $value;