form_snapshot : null; if (is_array($snapshot) && array_key_exists('fields', $snapshot) && is_array($snapshot['fields'])) return $snapshot['fields']; $current = CourseForm::where('course_id', $courseId)->orderBy('sort')->orderBy('id')->get()->toArray(); if (!$sign) return $current; if (!$sign->data) return []; // A legacy registration already stores its labels/options. Do not retroactively // apply newly introduced conditions or limits when that registration is edited. $legacy = []; foreach ($sign->data as $old) { if (!is_array($old) || !isset($old['field'])) continue; $matched = null; foreach ($current as $field) if ($field['field'] === $old['field']) { $matched = $field; break; } $definition = $matched ?: ['belong_user' => false, 'belong_user_table' => null]; foreach (['id', 'name', 'field', 'edit_input', 'rule', 'sort', 'help', 'select_item'] as $key) if (array_key_exists($key, $old)) $definition[$key] = $old[$key]; $definition['config'] = []; $definition['legacy'] = true; if (!$matched) { $definition['readonly'] = true; $definition['rule'] = ''; } $legacy[] = $definition; } return $legacy ?: $current; } public static function field(array $field): array { $field['config'] = Rules::arrayValue($field['config'] ?? []); $field['select_item'] = Rules::arrayValue($field['select_item'] ?? []); $field['need_fill'] = in_array('required', explode(',', $field['rule'] ?? ''), true); return $field; } public static function validateDefinition(array $field): array { $originalNeedFill = $field['need_fill'] ?? 0; $field = self::field($field); if (!is_string($field['field'] ?? null) || trim($field['field']) === '') throw new InvalidArgumentException('请填写字段标识'); $linked = $field['belong_user_table'] ?? ''; if ($linked && !in_array($linked, self::USER_FIELDS, true)) throw new InvalidArgumentException('请选择有效的学员字段'); $candidate = $field; $candidate['field'] = 'legacy_field'; // Old Chinese/custom identifiers remain valid and unchanged. $candidate['edit_input'] = in_array($field['edit_input'], ['radio', 'checkbox', 'text', 'textarea', 'number', 'date', 'datetime'], true) ? $field['edit_input'] : 'text'; $candidate['config']['user_field'] = ''; $validated = Rules::question($candidate); unset($validated['config']['user_field']); $field['config'] = $validated['config']; $field['select_item'] = $validated['select_item']; $field['need_fill'] = $originalNeedFill; return $field; } public static function selected(array $field, array $answer): array { if (isset($answer['selected_keys']) && (!is_array($answer['selected_keys']) || count(array_filter($answer['selected_keys'], fn ($key) => !is_scalar($key))))) throw new InvalidArgumentException('选项标识格式不正确'); if (isset($answer['selected_keys']) && is_array($answer['selected_keys'])) return array_values(array_unique(array_map('strval', $answer['selected_keys']))); $value = $answer['value'] ?? ''; if (Rules::blank($value)) return []; $values = is_array($value) ? $value : (($field['edit_input'] ?? '') === 'checkbox' ? explode(',', (string) $value) : [$value]); $keys = []; foreach ($values as $value) { $found = false; foreach ($field['select_item'] ?? [] as $option) { if (is_scalar($value) && (string) $option['value'] === (string) $value) { $keys[] = (string) $option['key']; $found = true; break; } } if (!$found) throw new InvalidArgumentException(($field['name'] ?? '').':选项已变更,请重新加载报名表单'); } return array_values(array_unique($keys)); } public static function normalize(array $fields, $data, array $previous = []): array { if (!is_array($data)) throw new InvalidArgumentException('报名信息格式不正确'); $raw = []; foreach ($data as $answer) { if (!is_array($answer) || !isset($answer['field'])) throw new InvalidArgumentException('报名字段格式不正确'); if (isset($raw[$answer['field']])) throw new InvalidArgumentException('报名字段重复'); $raw[$answer['field']] = $answer; } $definitions = $input = $invalid = []; $previousByField = []; foreach ($previous as $answer) if (isset($answer['field'])) $previousByField[$answer['field']] = $answer; foreach ($fields as $index => $field) { $field = self::field($field); $field['id'] = $field['id'] ?? ($index + 1); $answer = $raw[$field['field']] ?? []; foreach (['option_texts', 'other_text', 'fileList', 'fileListArr'] as $extra) if (!array_key_exists($extra, $answer) && isset($previousByField[$field['field']][$extra])) $answer[$extra] = $previousByField[$field['field']][$extra]; if (!empty($field['readonly'])) { foreach ($previous as $old) if (($old['field'] ?? null) === $field['field']) $answer = $old; $raw[$field['field']] = $answer; } $adapter = $field; if (!empty($field['readonly'])) { $adapter['edit_input'] = 'text'; $adapter['rule'] = ''; $adapter['need_fill'] = false; $adapter['config'] = []; } if (!in_array($adapter['edit_input'], ['text', 'textarea', 'radio', 'checkbox', 'number', 'date', 'datetime'], true)) $adapter['edit_input'] = 'text'; $definitions[] = $adapter; $value = $answer['value'] ?? null; if (!empty($field['legacy']) && in_array($adapter['edit_input'], ['date', 'datetime'], true) && isset($previousByField[$field['field']]) && ($previousByField[$field['field']]['value'] ?? null) === $value) $adapter['edit_input'] = 'text'; $definitions[count($definitions) - 1] = $adapter; if (!empty($field['readonly'])) $value = null; elseif (in_array($field['edit_input'], ['radio', 'checkbox'], true)) { try { $keys = self::selected($field, $answer); } catch (InvalidArgumentException $error) { $invalid[$index] = $error; $keys = []; } $value = $field['edit_input'] === 'checkbox' ? $keys : ($keys[0] ?? null); } $input[] = ['id' => $field['id'], 'value' => $value, 'option_texts' => $answer['option_texts'] ?? [], 'other_text' => $answer['other_text'] ?? '']; } // Hidden fields must not fail validation because of stale/unrecognised options. $map = Rules::answerMap($input); foreach ($definitions as $index => $field) { if (!Rules::visible($field, $definitions, $map)) $input[$index]['value'] = null; elseif (isset($invalid[$index])) throw $invalid[$index]; } $answers = Rules::normalize($definitions, $input, true); foreach ($answers as $index => &$answer) { $field = self::field($fields[$index]); if (!empty($field['readonly']) && isset($previousByField[$field['field']])) { $answer = $previousByField[$field['field']]; $answer['readonly'] = true; $answer['belong_user'] = false; continue; } $answer['edit_input'] = $field['edit_input']; $answer['answer_version'] = 2; if (in_array($field['edit_input'], ['radio', 'checkbox'], true)) { $answer['selected_keys'] = $answer['hidden'] ? [] : array_map('strval', (array) $answer['value']); $labels = []; foreach ($field['select_item'] as $option) if (in_array((string) $option['key'], $answer['selected_keys'], true)) $labels[] = $option['value']; $answer['value'] = $answer['hidden'] ? null : implode(',', $labels); // Original consumers still receive readable scalar values. } if ($field['edit_input'] === 'files') { $source = $raw[$field['field']] ?? []; $answer['fileList'] = $answer['hidden'] ? [] : Rules::arrayValue($source['fileList'] ?? $source['fileListArr'] ?? []); $answer['fileListArr'] = $answer['fileList']; } } unset($answer); // Retired legacy fields remain in the historical record, never silently discarded. $known = array_column($fields, 'field'); foreach ($previous as $old) if (is_array($old) && isset($old['field']) && !in_array($old['field'], $known, true)) $answers[] = $old; return $answers; } public static function changes(array $data, $user): array { $changes = []; foreach ($data as $answer) { if (!empty($answer['hidden']) || empty($answer['belong_user'])) continue; $field = $answer['belong_user_table'] ?? $answer['field']; if (!in_array($field, self::USER_FIELDS, true) || Rules::blank($answer['value'] ?? null)) continue; $current = $user->$field ?? null; if (is_array($current)) $current = implode(',', $current); if ((string) $current !== (string) $answer['value']) $changes[] = [$field => $answer['value']]; } return $changes; } public static function display(array $data): array { return array_map(function ($field) { $field = self::field($field); if (!empty($field['hidden'])) { $field['display_value'] = ''; return $field; } $value = $field['value'] ?? ''; if (isset($field['selected_keys']) && in_array($field['edit_input'] ?? '', ['radio', 'checkbox'], true)) { $value = Rules::display($field, ($field['edit_input'] === 'radio' ? ($field['selected_keys'][0] ?? null) : $field['selected_keys']), true); if ($value === '未填写') $value = ''; } elseif (is_array($value)) $value = implode('、', array_filter($value, 'is_scalar')); if (($field['edit_input'] ?? '') === 'files') { $files = $field['fileList'] ?? []; $value = $files ? implode('、', array_map(function ($file) { $file = $file['response'] ?? $file; return $file['original_name'] ?? $file['name'] ?? basename($file['url'] ?? ''); }, $files)) : implode('、', array_map('basename', array_filter(explode(',', (string) $value)))); } $field['display_value'] = (string) $value; $notes = []; foreach ($field['select_item'] as $option) { $key = (string) $option['key']; if (isset($field['selected_keys']) && !in_array($key, array_map('strval', $field['selected_keys']), true)) continue; $text = $field['option_texts'][$key] ?? ''; if (is_scalar($text) && trim((string) $text) !== '') $notes[] = $option['value'].':'.$text; } if (!empty($field['other_text']) && is_scalar($field['other_text'])) $notes[] = '补充:'.$field['other_text']; $field['supplement_text'] = implode("\n", $notes); return $field; }, $data); } }