You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Services\Course;
use App\Models\CourseForm;
use App\Models\CourseSign;
use App\Services\Survey\SurveySchema as Rules;
use InvalidArgumentException;
/** Adapts legacy label/comma answers to shared rules without rewriting stored data. */
class CourseFormSchema
{
public const USER_FIELDS = ['username', 'name', 'sex', 'birthday', 'mobile', 'idcard', 'from', 'talent_tags', 'education', 'introduce', 'company_name', 'company_introduce', 'company_date', 'company_position', 'company_has_share', 'type', 'company_type', 'company_need_fund', 'company_fund', 'company_area', 'company_industry', 'company_address', 'company_product', 'school', 'speciality', 'overseas_experience', 'sign_from', 'email', 'sales_volume', 'valuation', 'market_value', 'is_yuanhe'];
public static function definitions($courseId, ?CourseSign $sign = null): array
{
$snapshot = $sign ? $sign->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);
}
}