|
|
<?php
|
|
|
|
|
|
namespace App\Support;
|
|
|
|
|
|
/**
|
|
|
* 课程报名表单「动态字段」答案的存储约定(course_signups.form_answers)。
|
|
|
*
|
|
|
* 设计目标:选项的展示文案 / value 可在字典或业务表中变更,历史报名仍通过稳定 key 解析。
|
|
|
*
|
|
|
* form_answers 结构(字段 key => 答案对象):
|
|
|
* - 文本/日期等:{ "t": "text"|"date"|"datetime"|"file", "v": "..." }
|
|
|
* - 单选/下拉:{ "t": "option", "k": "<stable_key>" }
|
|
|
* - 多选: { "t": "options", "ks": ["<key1>", "<key2>"] }
|
|
|
*
|
|
|
* signup_form_schema 中选项字段需声明 options_source:
|
|
|
* - static: options[] 每项 { key, label, value? }
|
|
|
* - dict: { type:"dict", dict_code:"signup_gender" },运行时 key = (string) dict_items.id
|
|
|
* - table: { type:"table", table:"signup_channels", key_field:"code", ... }(预留)
|
|
|
*/
|
|
|
class SignupFormAnswers
|
|
|
{
|
|
|
private const PLAIN_TYPES = ['text', 'textarea', 'date', 'datetime', 'file'];
|
|
|
|
|
|
private const OPTION_TYPES = ['radio', 'select'];
|
|
|
|
|
|
private const MULTI_OPTION_TYPES = ['checkbox'];
|
|
|
|
|
|
/**
|
|
|
* 规范化并校验 form_answers(创建报名时调用)。
|
|
|
*
|
|
|
* @param array<string, mixed> $answers
|
|
|
* @param array<int, array<string, mixed>>|null $schema
|
|
|
* @return array<string, mixed>
|
|
|
*/
|
|
|
public static function normalize(array $answers, ?array $schema): array
|
|
|
{
|
|
|
if ($schema === null || $schema === []) {
|
|
|
return $answers;
|
|
|
}
|
|
|
|
|
|
$fieldMap = [];
|
|
|
foreach ($schema as $field) {
|
|
|
if (! is_array($field)) {
|
|
|
continue;
|
|
|
}
|
|
|
$key = $field['key'] ?? null;
|
|
|
if (is_string($key) && $key !== '') {
|
|
|
$fieldMap[$key] = $field;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$out = [];
|
|
|
foreach ($answers as $fieldKey => $raw) {
|
|
|
if (! is_string($fieldKey) || $fieldKey === '') {
|
|
|
continue;
|
|
|
}
|
|
|
if (! isset($fieldMap[$fieldKey])) {
|
|
|
continue;
|
|
|
}
|
|
|
$normalized = self::normalizeOne($fieldMap[$fieldKey], $raw);
|
|
|
if ($normalized !== null) {
|
|
|
$out[$fieldKey] = $normalized;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return $out;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<string, mixed> $fieldDef
|
|
|
* @param mixed $raw
|
|
|
* @return array<string, mixed>|null
|
|
|
*/
|
|
|
protected static function normalizeOne(array $fieldDef, mixed $raw): ?array
|
|
|
{
|
|
|
if (! is_array($raw)) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
$type = (string) ($fieldDef['type'] ?? 'text');
|
|
|
|
|
|
if (in_array($type, self::OPTION_TYPES, true)) {
|
|
|
$k = $raw['k'] ?? $raw['key'] ?? null;
|
|
|
if ($k === null || $k === '') {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
return ['t' => 'option', 'k' => (string) $k];
|
|
|
}
|
|
|
|
|
|
if (in_array($type, self::MULTI_OPTION_TYPES, true)) {
|
|
|
$ks = $raw['ks'] ?? $raw['keys'] ?? null;
|
|
|
if (! is_array($ks)) {
|
|
|
return null;
|
|
|
}
|
|
|
$keys = array_values(array_filter(array_map('strval', $ks), fn ($x) => $x !== ''));
|
|
|
|
|
|
return ['t' => 'options', 'ks' => $keys];
|
|
|
}
|
|
|
|
|
|
if (in_array($type, self::PLAIN_TYPES, true)) {
|
|
|
$t = $type === 'textarea' ? 'text' : $type;
|
|
|
if (! array_key_exists('v', $raw) && ! array_key_exists('value', $raw)) {
|
|
|
return null;
|
|
|
}
|
|
|
$v = $raw['v'] ?? $raw['value'];
|
|
|
|
|
|
return ['t' => $t, 'v' => is_scalar($v) ? (string) $v : json_encode($v)];
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
}
|