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

1 week ago
<?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);
}
}