|
|
<?php
|
|
|
|
|
|
namespace App\Support;
|
|
|
|
|
|
use App\Models\Application;
|
|
|
use App\Models\Competition;
|
|
|
|
|
|
/**
|
|
|
* 报名表 schema 字段与 applications 表列对齐。
|
|
|
* 标量列名单从 Application::$fillable 自动派生;新增数据库列后只需加入 fillable 即可全链路生效。
|
|
|
*/
|
|
|
final class SignupApplicationFieldRegistry
|
|
|
{
|
|
|
public const COMMITMENT_KEY = 'commitment_accepted';
|
|
|
|
|
|
public const PROMISE_SIGNATURE_KEY = 'promise_signature';
|
|
|
|
|
|
/** @var list<string> */
|
|
|
public const FILE_FIELD_KEYS = ['plan', 'supporting'];
|
|
|
|
|
|
/** 无专属类型约束时,标量字段允许的 schema type */
|
|
|
private const GENERIC_SCALAR_TYPES = [
|
|
|
'text',
|
|
|
'textarea',
|
|
|
'email',
|
|
|
'tel',
|
|
|
'select',
|
|
|
'number',
|
|
|
'date',
|
|
|
];
|
|
|
|
|
|
/**
|
|
|
* 可选:为常用字段收窄允许的 schema type;未列出的 fillable 列使用 GENERIC_SCALAR_TYPES。
|
|
|
*
|
|
|
* @var array<string, list<string>>
|
|
|
*/
|
|
|
private const SCALAR_TYPE_HINTS = [
|
|
|
'player_name' => ['text'],
|
|
|
'school' => ['text'],
|
|
|
'degree' => ['select'],
|
|
|
'contact_email' => ['email', 'text'],
|
|
|
'contact_mobile' => ['tel', 'text'],
|
|
|
'company_name' => ['text'],
|
|
|
'project_name' => ['text'],
|
|
|
'track' => ['select'],
|
|
|
'location_country' => ['select'],
|
|
|
'location_province' => ['select', 'text'],
|
|
|
'location_city' => ['select', 'text'],
|
|
|
'oversea_country' => ['text'],
|
|
|
'event_location' => ['select', 'text'],
|
|
|
'team_members' => ['text', 'textarea'],
|
|
|
'intro' => ['textarea', 'text'],
|
|
|
'recommend' => ['text', 'textarea'],
|
|
|
];
|
|
|
|
|
|
/**
|
|
|
* @return list<string>
|
|
|
*/
|
|
|
public static function scalarColumnKeys(): array
|
|
|
{
|
|
|
return Application::signupScalarFillableKeys();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @return list<string>
|
|
|
*/
|
|
|
public static function allSupportedFormKeys(): array
|
|
|
{
|
|
|
return array_values(array_unique([
|
|
|
...self::scalarColumnKeys(),
|
|
|
...self::FILE_FIELD_KEYS,
|
|
|
self::COMMITMENT_KEY,
|
|
|
]));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 供管理端报名表编辑器拉取:与后端 fillable 实时同步。
|
|
|
*
|
|
|
* @return array{
|
|
|
* scalar_field_keys: list<string>,
|
|
|
* file_field_keys: list<string>,
|
|
|
* commitment_field_key: string,
|
|
|
* scalar_type_hints: array<string, list<string>>,
|
|
|
* generic_scalar_types: list<string>,
|
|
|
* all_supported_form_keys: list<string>
|
|
|
* }
|
|
|
*/
|
|
|
public static function manifest(): array
|
|
|
{
|
|
|
$hints = [];
|
|
|
foreach (self::scalarColumnKeys() as $key) {
|
|
|
if (isset(self::SCALAR_TYPE_HINTS[$key])) {
|
|
|
$hints[$key] = self::SCALAR_TYPE_HINTS[$key];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
'scalar_field_keys' => self::scalarColumnKeys(),
|
|
|
'file_field_keys' => self::FILE_FIELD_KEYS,
|
|
|
'commitment_field_key' => self::COMMITMENT_KEY,
|
|
|
'scalar_type_hints' => $hints,
|
|
|
'generic_scalar_types' => self::GENERIC_SCALAR_TYPES,
|
|
|
'all_supported_form_keys' => self::allSupportedFormKeys(),
|
|
|
];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<int, mixed>|null $schemaJson
|
|
|
* @return list<string>
|
|
|
*/
|
|
|
public static function keysFromSchemaJson(?array $schemaJson): array
|
|
|
{
|
|
|
if (! is_array($schemaJson)) {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
$keys = [];
|
|
|
foreach ($schemaJson as $row) {
|
|
|
if (! is_array($row)) {
|
|
|
continue;
|
|
|
}
|
|
|
$key = trim((string) ($row['key'] ?? ''));
|
|
|
if ($key !== '') {
|
|
|
$keys[] = $key;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return array_values(array_unique($keys));
|
|
|
}
|
|
|
|
|
|
public static function schemaHasKey(Competition $competition, string $key): bool
|
|
|
{
|
|
|
$competition->loadMissing('formSchema');
|
|
|
$rows = $competition->formSchema?->schema_json;
|
|
|
|
|
|
return in_array($key, self::keysFromSchemaJson(is_array($rows) ? $rows : null), true);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @return list<string>
|
|
|
*/
|
|
|
public static function allowedTypesForScalarKey(string $key): array
|
|
|
{
|
|
|
return self::SCALAR_TYPE_HINTS[$key] ?? self::GENERIC_SCALAR_TYPES;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<int, mixed> $schemaJson
|
|
|
*/
|
|
|
public static function validateSignupSchemaJson(array $schemaJson): ?string
|
|
|
{
|
|
|
$seen = [];
|
|
|
$scalarKeys = array_flip(self::scalarColumnKeys());
|
|
|
|
|
|
foreach ($schemaJson as $index => $row) {
|
|
|
if (! is_array($row)) {
|
|
|
return '报名表字段定义第 '.($index + 1).' 项格式无效';
|
|
|
}
|
|
|
|
|
|
$key = trim((string) ($row['key'] ?? ''));
|
|
|
$type = trim((string) ($row['type'] ?? ''));
|
|
|
$label = trim((string) ($row['label'] ?? ''));
|
|
|
|
|
|
if ($key === '') {
|
|
|
return '报名表存在未填写字段 key 的项(须与 applications 表列名一致)';
|
|
|
}
|
|
|
if ($label === '') {
|
|
|
return '字段「'.$key.'」缺少显示标签';
|
|
|
}
|
|
|
if (isset($seen[$key])) {
|
|
|
return '字段 key「'.$key.'」重复';
|
|
|
}
|
|
|
$seen[$key] = true;
|
|
|
|
|
|
if ($key === self::COMMITMENT_KEY) {
|
|
|
if ($type !== 'checkbox') {
|
|
|
return '参赛承诺书字段须为 checkbox 类型(key='.self::COMMITMENT_KEY.')';
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
if (in_array($key, self::FILE_FIELD_KEYS, true)) {
|
|
|
if ($type !== 'file') {
|
|
|
return '附件字段「'.$key.'」须为 file 类型';
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
if (! isset($scalarKeys[$key])) {
|
|
|
if (! in_array($type, self::GENERIC_SCALAR_TYPES, true)) {
|
|
|
return '自定义字段「'.$key.'」类型须为:'.implode(' / ', self::GENERIC_SCALAR_TYPES);
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
$allowedTypes = self::allowedTypesForScalarKey($key);
|
|
|
if ($type === '' || ! in_array($type, $allowedTypes, true)) {
|
|
|
return '字段「'.$key.'」类型须为:'.implode(' / ', $allowedTypes);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @return array<string, mixed>
|
|
|
*/
|
|
|
public static function scalarAttributesFromApplication(Application $app): array
|
|
|
{
|
|
|
$out = [];
|
|
|
foreach (self::scalarColumnKeys() as $key) {
|
|
|
$out[$key] = $app->{$key};
|
|
|
}
|
|
|
|
|
|
return $out;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<string, mixed> $data
|
|
|
* @return array<string, mixed>
|
|
|
*/
|
|
|
public static function filterFillableScalarData(array $data): array
|
|
|
{
|
|
|
$allowed = array_flip(self::scalarColumnKeys());
|
|
|
$out = [];
|
|
|
foreach ($data as $key => $value) {
|
|
|
if (isset($allowed[$key])) {
|
|
|
$out[$key] = $value;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return $out;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<string, mixed> $data
|
|
|
* @return array<string, mixed>
|
|
|
*/
|
|
|
public static function filterAnswersJsonData(array $data): array
|
|
|
{
|
|
|
$columnKeys = array_flip(self::scalarColumnKeys());
|
|
|
$out = [];
|
|
|
foreach ($data as $key => $value) {
|
|
|
if (! is_string($key) || $key === '' || isset($columnKeys[$key])) {
|
|
|
continue;
|
|
|
}
|
|
|
if (in_array($key, self::FILE_FIELD_KEYS, true) || $key === self::COMMITMENT_KEY || $key === self::PROMISE_SIGNATURE_KEY) {
|
|
|
continue;
|
|
|
}
|
|
|
$out[$key] = $value;
|
|
|
}
|
|
|
|
|
|
return $out;
|
|
|
}
|
|
|
}
|