2788 lines
119 KiB
PHP
2788 lines
119 KiB
PHP
<?php
|
||
/**
|
||
* 招新“加入我们”系统主体:设置读写与清洗、时间解析、结果字段/文件映射、
|
||
* CSV/XLSX 解析、Formidable 对接、身份识别与查询匹配、阶段时间状态机、
|
||
* 录取进度查询、前端 payload、AJAX 接口、资源入队、后台“招新设置”页面。
|
||
* 抽离自主题 functions.php(lift-and-shift,逻辑未改)。
|
||
*/
|
||
|
||
if (!defined('ABSPATH')) {
|
||
exit;
|
||
}
|
||
|
||
function itstudio_is_join_page_context() {
|
||
$is_join = is_page('join') || is_page_template('page-join.php');
|
||
if (!$is_join && is_404()) {
|
||
global $wp;
|
||
$request = isset($wp->request) ? trim((string) $wp->request, '/') : '';
|
||
$is_join = ($request === 'join');
|
||
}
|
||
|
||
return $is_join;
|
||
}
|
||
|
||
function itstudio_join_get_default_settings() {
|
||
return array(
|
||
'registration_start' => '',
|
||
'registration_end' => '',
|
||
'first_interview_date' => '',
|
||
'first_interview_end' => '',
|
||
'first_interview_location_cn' => '',
|
||
'first_interview_location_en' => '',
|
||
'second_interview_date' => '',
|
||
'second_interview_end' => '',
|
||
'second_interview_location_cn' => '',
|
||
'second_interview_location_en' => '',
|
||
'assessment_start_date' => '',
|
||
'assessment_end_date' => '',
|
||
'notice_start_date' => '',
|
||
'result_data_source' => 'file',
|
||
'result_formidable_form_id' => '',
|
||
'result_formidable_name_field' => '',
|
||
'result_formidable_qq_field' => '',
|
||
'result_formidable_email_field' => '',
|
||
'result_formidable_student_id_field' => '',
|
||
'result_formidable_registration_field' => '',
|
||
'result_formidable_first_interview_field' => '',
|
||
'result_formidable_assessment_field' => '',
|
||
'result_formidable_second_interview_field' => '',
|
||
'result_formidable_admission_field' => '',
|
||
'result_first_interview_file' => 0,
|
||
'result_assessment_file' => 0,
|
||
'result_second_interview_file' => 0,
|
||
'result_admission_file' => 0,
|
||
'result_registration_records' => '',
|
||
'result_first_interview_records' => '',
|
||
'result_assessment_records' => '',
|
||
'result_second_interview_records' => '',
|
||
'result_admission_records' => '',
|
||
'photo_registration' => 0,
|
||
'photo_first_interview' => 0,
|
||
'photo_assessment' => 0,
|
||
'photo_second_interview' => 0,
|
||
'photo_public_notice' => 0,
|
||
'photo_inactive' => 0,
|
||
'signup_form_shortcode' => '',
|
||
'otp_enabled' => 0,
|
||
'otp_form_id' => '',
|
||
'otp_email_field_id' => '',
|
||
'otp_code_field_id' => '',
|
||
);
|
||
}
|
||
|
||
function itstudio_join_get_photo_field_map() {
|
||
return array(
|
||
'registration' => 'photo_registration',
|
||
'first_interview' => 'photo_first_interview',
|
||
'assessment' => 'photo_assessment',
|
||
'second_interview' => 'photo_second_interview',
|
||
'public_notice' => 'photo_public_notice',
|
||
'inactive' => 'photo_inactive',
|
||
);
|
||
}
|
||
|
||
function itstudio_join_sanitize_datetime_local_value($value) {
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
|
||
$value = preg_replace('/\s+/', 'T', $value);
|
||
|
||
// 兼容旧数据:仅日期
|
||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||
return $value;
|
||
}
|
||
|
||
// 兼容输入:YYYY-MM-DDTHH:MM 或 YYYY-MM-DDTHH:MM:SS
|
||
if (preg_match('/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(:\d{2})?$/', $value, $matches)) {
|
||
return $matches[1] . 'T' . $matches[2];
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
function itstudio_join_sanitize_date_value($value) {
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
|
||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||
return '';
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
function itstudio_join_sanitize_time_value($value) {
|
||
$value = trim((string) $value);
|
||
if (!preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/', $value)) {
|
||
return '';
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
function itstudio_join_sanitize_interview_schedule($input, $prefix) {
|
||
$input = is_array($input) ? $input : array();
|
||
$prefix = trim((string) $prefix);
|
||
$day_key = $prefix . '_day';
|
||
$start_time_key = $prefix . '_start_time';
|
||
$end_time_key = $prefix . '_end_time';
|
||
|
||
if (!array_key_exists($day_key, $input)
|
||
&& !array_key_exists($start_time_key, $input)
|
||
&& !array_key_exists($end_time_key, $input)) {
|
||
return array(
|
||
'start' => itstudio_join_sanitize_datetime_local_value($input[$prefix . '_date'] ?? ''),
|
||
'end' => itstudio_join_sanitize_datetime_local_value($input[$prefix . '_end'] ?? ''),
|
||
);
|
||
}
|
||
|
||
$day = itstudio_join_sanitize_date_value($input[$day_key] ?? '');
|
||
if ($day === '') {
|
||
return array('start' => '', 'end' => '');
|
||
}
|
||
|
||
$start_time = itstudio_join_sanitize_time_value($input[$start_time_key] ?? '');
|
||
$end_time = itstudio_join_sanitize_time_value($input[$end_time_key] ?? '');
|
||
|
||
return array(
|
||
'start' => $start_time !== '' ? $day . 'T' . $start_time : $day,
|
||
'end' => $end_time !== '' ? $day . 'T' . $end_time : '',
|
||
);
|
||
}
|
||
|
||
function itstudio_join_sanitize_shortcode_value($value) {
|
||
if (!is_string($value)) {
|
||
return '';
|
||
}
|
||
|
||
return trim(sanitize_text_field(wp_unslash($value)));
|
||
}
|
||
|
||
function itstudio_join_sanitize_records_value($value) {
|
||
if (!is_string($value)) {
|
||
return '';
|
||
}
|
||
|
||
$value = sanitize_textarea_field(wp_unslash($value));
|
||
$value = str_replace(array("\r\n", "\r"), "\n", $value);
|
||
$lines = explode("\n", $value);
|
||
$clean_lines = array();
|
||
foreach ($lines as $line) {
|
||
$line = trim((string) $line);
|
||
if ($line === '') {
|
||
continue;
|
||
}
|
||
$clean_lines[] = $line;
|
||
}
|
||
|
||
return implode("\n", $clean_lines);
|
||
}
|
||
|
||
function itstudio_join_sanitize_settings($input) {
|
||
$defaults = itstudio_join_get_default_settings();
|
||
$input = is_array($input) ? $input : array();
|
||
$first_interview_schedule = itstudio_join_sanitize_interview_schedule($input, 'first_interview');
|
||
$second_interview_schedule = itstudio_join_sanitize_interview_schedule($input, 'second_interview');
|
||
|
||
$sanitized = array(
|
||
'registration_start' => itstudio_join_sanitize_datetime_local_value($input['registration_start'] ?? ''),
|
||
'registration_end' => itstudio_join_sanitize_datetime_local_value($input['registration_end'] ?? ''),
|
||
'first_interview_date' => $first_interview_schedule['start'],
|
||
'first_interview_end' => $first_interview_schedule['end'],
|
||
'first_interview_location_cn' => itstudio_join_sanitize_shortcode_value($input['first_interview_location_cn'] ?? ''),
|
||
'first_interview_location_en' => itstudio_join_sanitize_shortcode_value($input['first_interview_location_en'] ?? ''),
|
||
'second_interview_date' => $second_interview_schedule['start'],
|
||
'second_interview_end' => $second_interview_schedule['end'],
|
||
'second_interview_location_cn' => itstudio_join_sanitize_shortcode_value($input['second_interview_location_cn'] ?? ''),
|
||
'second_interview_location_en' => itstudio_join_sanitize_shortcode_value($input['second_interview_location_en'] ?? ''),
|
||
'assessment_start_date' => itstudio_join_sanitize_date_value($input['assessment_start_date'] ?? ''),
|
||
'assessment_end_date' => itstudio_join_sanitize_date_value($input['assessment_end_date'] ?? ''),
|
||
'notice_start_date' => itstudio_join_sanitize_date_value($input['notice_start_date'] ?? ''),
|
||
// 固定使用文件结果模式,避免后台再配置字段映射。
|
||
'result_data_source' => 'file',
|
||
'result_formidable_form_id' => itstudio_join_sanitize_shortcode_value($input['result_formidable_form_id'] ?? ''),
|
||
'result_formidable_name_field' => itstudio_join_sanitize_shortcode_value($input['result_formidable_name_field'] ?? ''),
|
||
'result_formidable_qq_field' => itstudio_join_sanitize_shortcode_value($input['result_formidable_qq_field'] ?? ''),
|
||
'result_formidable_email_field' => itstudio_join_sanitize_shortcode_value($input['result_formidable_email_field'] ?? ''),
|
||
'result_formidable_student_id_field' => itstudio_join_sanitize_shortcode_value($input['result_formidable_student_id_field'] ?? ''),
|
||
'result_formidable_registration_field' => itstudio_join_sanitize_shortcode_value($input['result_formidable_registration_field'] ?? ''),
|
||
'result_formidable_first_interview_field' => itstudio_join_sanitize_shortcode_value($input['result_formidable_first_interview_field'] ?? ''),
|
||
'result_formidable_assessment_field' => itstudio_join_sanitize_shortcode_value($input['result_formidable_assessment_field'] ?? ''),
|
||
'result_formidable_second_interview_field' => itstudio_join_sanitize_shortcode_value($input['result_formidable_second_interview_field'] ?? ''),
|
||
'result_formidable_admission_field' => itstudio_join_sanitize_shortcode_value($input['result_formidable_admission_field'] ?? ''),
|
||
'result_first_interview_file' => isset($input['result_first_interview_file']) ? absint($input['result_first_interview_file']) : 0,
|
||
'result_assessment_file' => isset($input['result_assessment_file']) ? absint($input['result_assessment_file']) : 0,
|
||
'result_second_interview_file' => isset($input['result_second_interview_file']) ? absint($input['result_second_interview_file']) : 0,
|
||
'result_admission_file' => isset($input['result_admission_file']) ? absint($input['result_admission_file']) : 0,
|
||
'result_registration_records' => itstudio_join_sanitize_records_value($input['result_registration_records'] ?? ''),
|
||
'result_first_interview_records' => itstudio_join_sanitize_records_value($input['result_first_interview_records'] ?? ''),
|
||
'result_assessment_records' => itstudio_join_sanitize_records_value($input['result_assessment_records'] ?? ''),
|
||
'result_second_interview_records' => itstudio_join_sanitize_records_value($input['result_second_interview_records'] ?? ''),
|
||
'result_admission_records' => itstudio_join_sanitize_records_value($input['result_admission_records'] ?? ''),
|
||
'signup_form_shortcode' => itstudio_join_sanitize_shortcode_value($input['signup_form_shortcode'] ?? ''),
|
||
'otp_enabled' => !empty($input['otp_enabled']) ? 1 : 0,
|
||
'otp_form_id' => absint($input['otp_form_id'] ?? 0),
|
||
'otp_email_field_id' => absint($input['otp_email_field_id'] ?? 0),
|
||
'otp_code_field_id' => absint($input['otp_code_field_id'] ?? 0),
|
||
);
|
||
|
||
foreach (itstudio_join_get_photo_field_map() as $field_key) {
|
||
$sanitized[$field_key] = isset($input[$field_key]) ? absint($input[$field_key]) : 0;
|
||
}
|
||
|
||
return array_merge($defaults, $sanitized);
|
||
}
|
||
|
||
function itstudio_join_get_settings($term_id = null) {
|
||
$defaults = itstudio_join_get_default_settings();
|
||
|
||
// 学期感知:无参时取当前生效学期;有参时取指定学期。
|
||
if (function_exists('itstudio_join_get_terms')) {
|
||
$target_id = $term_id !== null ? (string) $term_id : itstudio_join_get_active_term_id();
|
||
$terms = itstudio_join_get_terms();
|
||
if ($target_id !== '' && isset($terms[$target_id]) && is_array($terms[$target_id]['settings'] ?? null)) {
|
||
return array_merge($defaults, itstudio_join_sanitize_settings($terms[$target_id]['settings']));
|
||
}
|
||
}
|
||
|
||
// 兜底:迁移尚未发生时读旧的单一选项。
|
||
$stored = get_option('itstudio_join_settings', array());
|
||
if (!is_array($stored)) {
|
||
return $defaults;
|
||
}
|
||
|
||
return array_merge($defaults, itstudio_join_sanitize_settings($stored));
|
||
}
|
||
|
||
function itstudio_join_parse_datetime_local($value, $date_only_as_end_of_day = false) {
|
||
$value = itstudio_join_sanitize_datetime_local_value($value);
|
||
if ($value === '') {
|
||
return null;
|
||
}
|
||
|
||
$timezone = wp_timezone();
|
||
$format = 'Y-m-d\TH:i';
|
||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||
$format = 'Y-m-d';
|
||
}
|
||
|
||
$parsed = DateTimeImmutable::createFromFormat('!' . $format, $value, $timezone);
|
||
$errors = DateTimeImmutable::getLastErrors();
|
||
if (!($parsed instanceof DateTimeImmutable)) {
|
||
return null;
|
||
}
|
||
|
||
if (is_array($errors) && (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0)) {
|
||
return null;
|
||
}
|
||
|
||
if ($format === 'Y-m-d') {
|
||
return $date_only_as_end_of_day
|
||
? $parsed->setTime(23, 59, 59)
|
||
: $parsed->setTime(0, 0, 0);
|
||
}
|
||
|
||
return $parsed;
|
||
}
|
||
|
||
function itstudio_join_parse_date($value) {
|
||
$value = itstudio_join_sanitize_date_value($value);
|
||
if ($value === '') {
|
||
return null;
|
||
}
|
||
|
||
$timezone = wp_timezone();
|
||
$parsed = DateTimeImmutable::createFromFormat('!Y-m-d', $value, $timezone);
|
||
$errors = DateTimeImmutable::getLastErrors();
|
||
if (!($parsed instanceof DateTimeImmutable)) {
|
||
return null;
|
||
}
|
||
|
||
if (is_array($errors) && (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0)) {
|
||
return null;
|
||
}
|
||
|
||
return $parsed;
|
||
}
|
||
|
||
function itstudio_join_to_datetime_local_input_value($value, $date_only_as_end_of_day = false) {
|
||
$parsed = itstudio_join_parse_datetime_local($value, $date_only_as_end_of_day);
|
||
return $parsed instanceof DateTimeImmutable ? $parsed->format('Y-m-d\TH:i') : '';
|
||
}
|
||
|
||
function itstudio_join_to_date_input_value($value) {
|
||
$parsed = itstudio_join_parse_date($value);
|
||
if (!($parsed instanceof DateTimeImmutable)) {
|
||
$parsed = itstudio_join_parse_datetime_local($value, false);
|
||
}
|
||
return $parsed instanceof DateTimeImmutable ? $parsed->format('Y-m-d') : '';
|
||
}
|
||
|
||
function itstudio_join_to_time_input_value($value, $date_only_as_end_of_day = false) {
|
||
$parsed = itstudio_join_parse_datetime_local($value, $date_only_as_end_of_day);
|
||
return $parsed instanceof DateTimeImmutable ? $parsed->format('H:i') : '';
|
||
}
|
||
|
||
function itstudio_join_get_result_field_map() {
|
||
return array(
|
||
'registration' => 'result_registration_records',
|
||
'first_interview' => 'result_first_interview_records',
|
||
'assessment' => 'result_assessment_records',
|
||
'second_interview' => 'result_second_interview_records',
|
||
'public_notice' => 'result_admission_records',
|
||
);
|
||
}
|
||
|
||
function itstudio_join_get_result_file_field_map() {
|
||
return array(
|
||
'first_interview' => 'result_first_interview_file',
|
||
'assessment' => 'result_assessment_file',
|
||
'second_interview' => 'result_second_interview_file',
|
||
'public_notice' => 'result_admission_file',
|
||
);
|
||
}
|
||
|
||
function itstudio_join_is_file_result_mode($settings) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
return (($settings['result_data_source'] ?? 'manual') === 'file');
|
||
}
|
||
|
||
function itstudio_join_get_result_file_attachment_id($settings, $stage_key) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
$map = itstudio_join_get_result_file_field_map();
|
||
$setting_key = isset($map[$stage_key]) ? $map[$stage_key] : '';
|
||
if ($setting_key === '') {
|
||
return 0;
|
||
}
|
||
return absint((string) ($settings[$setting_key] ?? ''));
|
||
}
|
||
|
||
function itstudio_join_csv_delimiter_for_line($line) {
|
||
$line = (string) $line;
|
||
$candidates = array(',', ';', "\t", '|');
|
||
$best = ',';
|
||
$best_count = -1;
|
||
foreach ($candidates as $delimiter) {
|
||
$count = substr_count($line, $delimiter);
|
||
if ($count > $best_count) {
|
||
$best_count = $count;
|
||
$best = $delimiter;
|
||
}
|
||
}
|
||
return $best;
|
||
}
|
||
|
||
function itstudio_join_read_csv_rows($path) {
|
||
if (!is_string($path) || $path === '' || !is_readable($path)) {
|
||
return array();
|
||
}
|
||
|
||
$rows = array();
|
||
$handle = fopen($path, 'rb');
|
||
if ($handle === false) {
|
||
return array();
|
||
}
|
||
|
||
$delimiter = ',';
|
||
$first_line = fgets($handle);
|
||
if ($first_line !== false) {
|
||
$first_line = preg_replace('/^\xEF\xBB\xBF/', '', (string) $first_line);
|
||
$delimiter = itstudio_join_csv_delimiter_for_line($first_line);
|
||
rewind($handle);
|
||
}
|
||
|
||
while (($data = fgetcsv($handle, 0, $delimiter)) !== false) {
|
||
if (!is_array($data)) {
|
||
continue;
|
||
}
|
||
$row = array();
|
||
foreach ($data as $cell) {
|
||
$row[] = trim((string) $cell);
|
||
}
|
||
if (!empty(array_filter($row, static function ($value) {
|
||
return $value !== '';
|
||
}))) {
|
||
$rows[] = $row;
|
||
}
|
||
}
|
||
|
||
fclose($handle);
|
||
return $rows;
|
||
}
|
||
|
||
function itstudio_join_xlsx_column_to_index($column_ref) {
|
||
$column_ref = strtoupper((string) $column_ref);
|
||
$length = strlen($column_ref);
|
||
$index = 0;
|
||
for ($i = 0; $i < $length; $i++) {
|
||
$ch = ord($column_ref[$i]);
|
||
if ($ch < 65 || $ch > 90) {
|
||
continue;
|
||
}
|
||
$index = ($index * 26) + ($ch - 64);
|
||
}
|
||
return max(0, $index - 1);
|
||
}
|
||
|
||
function itstudio_join_xlsx_shared_strings($zip) {
|
||
$shared = array();
|
||
$xml = $zip->getFromName('xl/sharedStrings.xml');
|
||
if (!is_string($xml) || trim($xml) === '') {
|
||
return $shared;
|
||
}
|
||
|
||
$sx = simplexml_load_string($xml);
|
||
if (!($sx instanceof SimpleXMLElement)) {
|
||
return $shared;
|
||
}
|
||
|
||
foreach ($sx->si as $si) {
|
||
if (isset($si->t)) {
|
||
$shared[] = (string) $si->t;
|
||
continue;
|
||
}
|
||
$chunks = array();
|
||
if (isset($si->r)) {
|
||
foreach ($si->r as $run) {
|
||
$chunks[] = (string) $run->t;
|
||
}
|
||
}
|
||
$shared[] = implode('', $chunks);
|
||
}
|
||
|
||
return $shared;
|
||
}
|
||
|
||
function itstudio_join_read_xlsx_rows($path) {
|
||
if (!class_exists('ZipArchive') || !class_exists('SimpleXMLElement')) {
|
||
return array();
|
||
}
|
||
if (!is_string($path) || $path === '' || !is_readable($path)) {
|
||
return array();
|
||
}
|
||
|
||
$zip = new ZipArchive();
|
||
if ($zip->open($path) !== true) {
|
||
return array();
|
||
}
|
||
|
||
$sheetXml = $zip->getFromName('xl/worksheets/sheet1.xml');
|
||
if (!is_string($sheetXml) || trim($sheetXml) === '') {
|
||
// fallback: first worksheet found
|
||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||
$name = $zip->getNameIndex($i);
|
||
if (is_string($name) && preg_match('#^xl/worksheets/sheet\d+\.xml$#', $name)) {
|
||
$sheetXml = $zip->getFromName($name);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!is_string($sheetXml) || trim($sheetXml) === '') {
|
||
$zip->close();
|
||
return array();
|
||
}
|
||
|
||
$sharedStrings = itstudio_join_xlsx_shared_strings($zip);
|
||
$zip->close();
|
||
|
||
$sheet = simplexml_load_string($sheetXml);
|
||
if (!($sheet instanceof SimpleXMLElement) || !isset($sheet->sheetData)) {
|
||
return array();
|
||
}
|
||
|
||
$rows = array();
|
||
foreach ($sheet->sheetData->row as $rowNode) {
|
||
$row = array();
|
||
foreach ($rowNode->c as $cell) {
|
||
$ref = (string) ($cell['r'] ?? '');
|
||
$type = (string) ($cell['t'] ?? '');
|
||
preg_match('/^[A-Z]+/i', $ref, $matches);
|
||
$colRef = isset($matches[0]) ? $matches[0] : '';
|
||
$colIndex = itstudio_join_xlsx_column_to_index($colRef);
|
||
|
||
$value = '';
|
||
if ($type === 's') {
|
||
$sharedIndex = isset($cell->v) ? (int) $cell->v : -1;
|
||
$value = ($sharedIndex >= 0 && isset($sharedStrings[$sharedIndex])) ? (string) $sharedStrings[$sharedIndex] : '';
|
||
} elseif ($type === 'inlineStr' && isset($cell->is->t)) {
|
||
$value = (string) $cell->is->t;
|
||
} else {
|
||
$value = isset($cell->v) ? (string) $cell->v : '';
|
||
}
|
||
|
||
$row[$colIndex] = trim($value);
|
||
}
|
||
|
||
if (!empty($row)) {
|
||
ksort($row);
|
||
$normalized = array_values($row);
|
||
if (!empty(array_filter($normalized, static function ($value) {
|
||
return $value !== '';
|
||
}))) {
|
||
$rows[] = $normalized;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $rows;
|
||
}
|
||
|
||
function itstudio_join_read_result_rows_from_attachment($attachment_id) {
|
||
static $cache = array();
|
||
$attachment_id = absint($attachment_id);
|
||
if ($attachment_id <= 0) {
|
||
return array();
|
||
}
|
||
if (isset($cache[$attachment_id])) {
|
||
return $cache[$attachment_id];
|
||
}
|
||
|
||
$path = get_attached_file($attachment_id);
|
||
if (!is_string($path) || $path === '' || !is_readable($path)) {
|
||
$cache[$attachment_id] = array();
|
||
return array();
|
||
}
|
||
|
||
$ext = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
|
||
$rows = array();
|
||
if ($ext === 'csv') {
|
||
$rows = itstudio_join_read_csv_rows($path);
|
||
} elseif ($ext === 'xlsx') {
|
||
$rows = itstudio_join_read_xlsx_rows($path);
|
||
} else {
|
||
// try csv as fallback
|
||
$rows = itstudio_join_read_csv_rows($path);
|
||
}
|
||
|
||
$cache[$attachment_id] = is_array($rows) ? $rows : array();
|
||
return $cache[$attachment_id];
|
||
}
|
||
|
||
function itstudio_join_parse_result_rows_to_records($rows) {
|
||
$rows = is_array($rows) ? $rows : array();
|
||
$records = array();
|
||
|
||
$is_header_row = static function ($name, $qq, $email, $student_id, $phone, $passed_raw) {
|
||
$joined = implode(',', array($name, $qq, $email, $student_id, $phone, $passed_raw));
|
||
$joined = preg_replace('/^\xEF\xBB\xBF/', '', (string) $joined);
|
||
$joined = preg_replace('/\s+/u', '', (string) $joined);
|
||
$joined_lower = function_exists('mb_strtolower') ? mb_strtolower($joined, 'UTF-8') : strtolower($joined);
|
||
|
||
$is_cn_header = (
|
||
strpos($joined_lower, '姓名') !== false
|
||
|| strpos($joined_lower, 'qq') !== false
|
||
|| strpos($joined_lower, '邮箱') !== false
|
||
|| strpos($joined_lower, '学号') !== false
|
||
|| strpos($joined_lower, '手机') !== false
|
||
|| strpos($joined_lower, '是否通过') !== false
|
||
);
|
||
|
||
$is_en_header = (
|
||
strpos($joined_lower, 'name') !== false
|
||
|| strpos($joined_lower, 'qq') !== false
|
||
|| strpos($joined_lower, 'email') !== false
|
||
|| strpos($joined_lower, 'student') !== false
|
||
|| strpos($joined_lower, 'phone') !== false
|
||
|| strpos($joined_lower, 'pass') !== false
|
||
);
|
||
|
||
return $is_cn_header || $is_en_header;
|
||
};
|
||
|
||
foreach ($rows as $idx => $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
|
||
$name = trim((string) ($row[0] ?? ''));
|
||
$qq = trim((string) ($row[1] ?? ''));
|
||
$email = trim((string) ($row[2] ?? ''));
|
||
$student_id = trim((string) ($row[3] ?? ''));
|
||
$phone = trim((string) ($row[4] ?? ''));
|
||
$passed_raw = trim((string) ($row[5] ?? ''));
|
||
|
||
// 过滤标题行(支持中英文标题)
|
||
if ($idx === 0 && $is_header_row($name, $qq, $email, $student_id, $phone, $passed_raw)) {
|
||
continue;
|
||
}
|
||
// 容错:有些表格会在中间重复表头
|
||
if ($is_header_row($name, $qq, $email, $student_id, $phone, $passed_raw)) {
|
||
continue;
|
||
}
|
||
|
||
$record = array(
|
||
'name' => itstudio_join_normalize_lookup_value('name', $name),
|
||
'qq' => itstudio_join_normalize_lookup_value('qq', $qq),
|
||
'email' => itstudio_join_normalize_lookup_value('email', $email),
|
||
'student_id' => itstudio_join_normalize_lookup_value('student_id', $student_id),
|
||
'phone' => itstudio_join_normalize_lookup_value('phone', $phone),
|
||
'passed' => ($passed_raw === '1'),
|
||
);
|
||
|
||
if ($record['name'] === '' && $record['qq'] === '' && $record['email'] === '' && $record['student_id'] === '' && $record['phone'] === '') {
|
||
continue;
|
||
}
|
||
|
||
$records[] = $record;
|
||
}
|
||
|
||
return $records;
|
||
}
|
||
|
||
function itstudio_join_is_formidable_result_mode($settings) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
return (($settings['result_data_source'] ?? 'manual') === 'formidable');
|
||
}
|
||
|
||
function itstudio_join_get_formidable_form_id($settings) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
return absint((string) ($settings['result_formidable_form_id'] ?? ''));
|
||
}
|
||
|
||
function itstudio_join_get_formidable_identity_field_refs($settings) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
return array(
|
||
'name' => trim((string) ($settings['result_formidable_name_field'] ?? '')),
|
||
'qq' => trim((string) ($settings['result_formidable_qq_field'] ?? '')),
|
||
'email' => trim((string) ($settings['result_formidable_email_field'] ?? '')),
|
||
'student_id' => trim((string) ($settings['result_formidable_student_id_field'] ?? '')),
|
||
);
|
||
}
|
||
|
||
function itstudio_join_get_formidable_stage_field_ref($settings, $stage_key) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
$map = array(
|
||
'registration' => 'result_formidable_registration_field',
|
||
'first_interview' => 'result_formidable_first_interview_field',
|
||
'assessment' => 'result_formidable_assessment_field',
|
||
'second_interview' => 'result_formidable_second_interview_field',
|
||
'public_notice' => 'result_formidable_admission_field',
|
||
);
|
||
$setting_key = isset($map[$stage_key]) ? $map[$stage_key] : '';
|
||
if ($setting_key === '') {
|
||
return '';
|
||
}
|
||
return trim((string) ($settings[$setting_key] ?? ''));
|
||
}
|
||
|
||
function itstudio_join_resolve_formidable_field_id($form_id, $field_ref) {
|
||
static $cache = array();
|
||
$form_id = absint($form_id);
|
||
$field_ref = trim((string) $field_ref);
|
||
if ($field_ref === '') {
|
||
return 0;
|
||
}
|
||
|
||
$cache_key = $form_id . '|' . $field_ref;
|
||
if (isset($cache[$cache_key])) {
|
||
return (int) $cache[$cache_key];
|
||
}
|
||
|
||
if (ctype_digit($field_ref)) {
|
||
$cache[$cache_key] = (int) $field_ref;
|
||
return (int) $cache[$cache_key];
|
||
}
|
||
|
||
$field_id = 0;
|
||
if (class_exists('FrmField') && method_exists('FrmField', 'get_id_by_key')) {
|
||
$field_id = (int) FrmField::get_id_by_key($field_ref);
|
||
}
|
||
|
||
if ($field_id <= 0) {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'frm_fields';
|
||
if ($form_id > 0) {
|
||
$field_id = (int) $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table} WHERE field_key = %s AND form_id = %d LIMIT 1", $field_ref, $form_id));
|
||
} else {
|
||
$field_id = (int) $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table} WHERE field_key = %s LIMIT 1", $field_ref));
|
||
}
|
||
}
|
||
|
||
$cache[$cache_key] = $field_id > 0 ? $field_id : 0;
|
||
return (int) $cache[$cache_key];
|
||
}
|
||
|
||
function itstudio_join_get_formidable_entry_meta_value($entry_id, $field_id) {
|
||
static $cache = array();
|
||
$entry_id = absint($entry_id);
|
||
$field_id = absint($field_id);
|
||
if ($entry_id <= 0 || $field_id <= 0) {
|
||
return '';
|
||
}
|
||
|
||
$cache_key = $entry_id . '|' . $field_id;
|
||
if (array_key_exists($cache_key, $cache)) {
|
||
return $cache[$cache_key];
|
||
}
|
||
|
||
$value = '';
|
||
if (class_exists('FrmEntryMeta') && method_exists('FrmEntryMeta', 'get_entry_meta_by_field')) {
|
||
$value = FrmEntryMeta::get_entry_meta_by_field($entry_id, $field_id, true);
|
||
} else {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'frm_item_metas';
|
||
$value = $wpdb->get_var($wpdb->prepare("SELECT meta_value FROM {$table} WHERE item_id = %d AND field_id = %d LIMIT 1", $entry_id, $field_id));
|
||
}
|
||
|
||
$cache[$cache_key] = $value;
|
||
return $value;
|
||
}
|
||
|
||
function itstudio_join_is_truthy_result_value($value) {
|
||
if (is_array($value)) {
|
||
foreach ($value as $item) {
|
||
if (itstudio_join_is_truthy_result_value($item)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (is_bool($value)) {
|
||
return $value;
|
||
}
|
||
|
||
if (is_numeric($value)) {
|
||
return ((float) $value) > 0;
|
||
}
|
||
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return false;
|
||
}
|
||
|
||
if (is_serialized($value)) {
|
||
$decoded = maybe_unserialize($value);
|
||
if ($decoded !== $value) {
|
||
return itstudio_join_is_truthy_result_value($decoded);
|
||
}
|
||
}
|
||
|
||
$normalized = function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
|
||
return in_array($normalized, array(
|
||
'1', 'true', 'yes', 'on', 'y',
|
||
'pass', 'passed', 'admit', 'admitted',
|
||
'是', '通过', '已通过', '录取', '已录取', '完成', '成功',
|
||
), true);
|
||
}
|
||
|
||
function itstudio_join_find_formidable_entry_id_by_query($settings, $query) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
$query = is_array($query) ? $query : array();
|
||
if (!class_exists('FrmEntry') || !class_exists('FrmEntryMeta')) {
|
||
return 0;
|
||
}
|
||
|
||
$form_id = itstudio_join_get_formidable_form_id($settings);
|
||
if ($form_id <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
$identity_refs = itstudio_join_get_formidable_identity_field_refs($settings);
|
||
$active_filters = array();
|
||
foreach (array('name', 'qq', 'email', 'student_id') as $field) {
|
||
$query_value = trim((string) ($query[$field] ?? ''));
|
||
if ($query_value === '') {
|
||
continue;
|
||
}
|
||
$field_ref = trim((string) ($identity_refs[$field] ?? ''));
|
||
if ($field_ref === '') {
|
||
continue;
|
||
}
|
||
$field_id = itstudio_join_resolve_formidable_field_id($form_id, $field_ref);
|
||
if ($field_id <= 0) {
|
||
continue;
|
||
}
|
||
$active_filters[$field] = $field_id;
|
||
}
|
||
|
||
if (empty($active_filters)) {
|
||
return 0;
|
||
}
|
||
|
||
$entries = FrmEntry::getAll(
|
||
array(
|
||
'it.form_id' => $form_id,
|
||
'is_draft' => 0,
|
||
),
|
||
' ORDER BY it.id DESC'
|
||
);
|
||
|
||
if (empty($entries)) {
|
||
return 0;
|
||
}
|
||
|
||
foreach ($entries as $entry) {
|
||
$entry_id = absint(is_object($entry) ? ($entry->id ?? 0) : (is_array($entry) ? ($entry['id'] ?? 0) : 0));
|
||
if ($entry_id <= 0) {
|
||
continue;
|
||
}
|
||
|
||
$matched = true;
|
||
foreach ($active_filters as $field => $field_id) {
|
||
$raw_value = itstudio_join_get_formidable_entry_meta_value($entry_id, $field_id);
|
||
$normalized_entry_value = itstudio_join_normalize_lookup_value($field, (string) $raw_value);
|
||
$normalized_query_value = itstudio_join_normalize_lookup_value($field, (string) ($query[$field] ?? ''));
|
||
if ($normalized_query_value === '' || $normalized_entry_value === '' || $normalized_entry_value !== $normalized_query_value) {
|
||
$matched = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($matched) {
|
||
return $entry_id;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
function itstudio_join_formidable_entry_matches_stage($settings, $entry_id, $stage_key) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
$entry_id = absint($entry_id);
|
||
$stage_key = (string) $stage_key;
|
||
if ($entry_id <= 0) {
|
||
return false;
|
||
}
|
||
|
||
$form_id = itstudio_join_get_formidable_form_id($settings);
|
||
if ($form_id <= 0) {
|
||
return false;
|
||
}
|
||
|
||
$field_ref = itstudio_join_get_formidable_stage_field_ref($settings, $stage_key);
|
||
if ($field_ref === '') {
|
||
return $stage_key === 'registration';
|
||
}
|
||
|
||
$field_id = itstudio_join_resolve_formidable_field_id($form_id, $field_ref);
|
||
if ($field_id <= 0) {
|
||
return false;
|
||
}
|
||
|
||
$raw_value = itstudio_join_get_formidable_entry_meta_value($entry_id, $field_id);
|
||
return itstudio_join_is_truthy_result_value($raw_value);
|
||
}
|
||
|
||
function itstudio_join_formidable_stage_has_data($settings, $stage_key) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
if (!class_exists('FrmEntry') || !class_exists('FrmEntryMeta')) {
|
||
return false;
|
||
}
|
||
|
||
$form_id = itstudio_join_get_formidable_form_id($settings);
|
||
if ($form_id <= 0) {
|
||
return false;
|
||
}
|
||
|
||
global $wpdb;
|
||
$items_table = $wpdb->prefix . 'frm_items';
|
||
$metas_table = $wpdb->prefix . 'frm_item_metas';
|
||
|
||
$entry_count = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(1) FROM {$items_table} WHERE form_id = %d AND is_draft = 0", $form_id));
|
||
if ($entry_count <= 0) {
|
||
return false;
|
||
}
|
||
|
||
if ($stage_key === 'registration') {
|
||
return true;
|
||
}
|
||
|
||
$field_ref = itstudio_join_get_formidable_stage_field_ref($settings, $stage_key);
|
||
$field_id = itstudio_join_resolve_formidable_field_id($form_id, $field_ref);
|
||
if ($field_id <= 0) {
|
||
return false;
|
||
}
|
||
|
||
$hit = (int) $wpdb->get_var($wpdb->prepare(
|
||
"SELECT COUNT(1)
|
||
FROM {$metas_table} AS m
|
||
INNER JOIN {$items_table} AS i ON i.id = m.item_id
|
||
WHERE i.form_id = %d
|
||
AND i.is_draft = 0
|
||
AND m.field_id = %d
|
||
AND COALESCE(m.meta_value, '') <> ''",
|
||
$form_id,
|
||
$field_id
|
||
));
|
||
|
||
return $hit > 0 || $entry_count > 0;
|
||
}
|
||
|
||
function itstudio_join_formidable_has_queryable_identity_field($settings, $query) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
$query = is_array($query) ? $query : array();
|
||
$form_id = itstudio_join_get_formidable_form_id($settings);
|
||
if ($form_id <= 0) {
|
||
return false;
|
||
}
|
||
|
||
$identity_refs = itstudio_join_get_formidable_identity_field_refs($settings);
|
||
foreach (array('name', 'qq', 'email', 'student_id') as $field) {
|
||
$query_value = trim((string) ($query[$field] ?? ''));
|
||
if ($query_value === '') {
|
||
continue;
|
||
}
|
||
$field_ref = trim((string) ($identity_refs[$field] ?? ''));
|
||
if ($field_ref === '') {
|
||
continue;
|
||
}
|
||
$field_id = itstudio_join_resolve_formidable_field_id($form_id, $field_ref);
|
||
if ($field_id > 0) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function itstudio_join_has_uploaded_result_for_stage($settings, $stage_key) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
if (itstudio_join_is_file_result_mode($settings)) {
|
||
$attachment_id = itstudio_join_get_result_file_attachment_id($settings, $stage_key);
|
||
if ($attachment_id <= 0) {
|
||
return false;
|
||
}
|
||
$rows = itstudio_join_read_result_rows_from_attachment($attachment_id);
|
||
$records = itstudio_join_parse_result_rows_to_records($rows);
|
||
return !empty($records);
|
||
}
|
||
|
||
if (itstudio_join_is_formidable_result_mode($settings)) {
|
||
return itstudio_join_formidable_stage_has_data($settings, $stage_key);
|
||
}
|
||
|
||
$field_map = itstudio_join_get_result_field_map();
|
||
$field_key = isset($field_map[$stage_key]) ? $field_map[$stage_key] : '';
|
||
if ($field_key === '') {
|
||
return false;
|
||
}
|
||
|
||
return trim((string) ($settings[$field_key] ?? '')) !== '';
|
||
}
|
||
|
||
function itstudio_join_normalize_lookup_value($field, $value) {
|
||
$field = trim((string) $field);
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
|
||
if ($field === 'qq' || $field === 'phone') {
|
||
return preg_replace('/\D+/', '', $value);
|
||
}
|
||
|
||
if ($field === 'email') {
|
||
return strtolower($value);
|
||
}
|
||
|
||
if ($field === 'student_id') {
|
||
return strtoupper(preg_replace('/\s+/u', '', $value));
|
||
}
|
||
|
||
$value = preg_replace('/\s+/u', '', $value);
|
||
if (function_exists('mb_strtolower')) {
|
||
return mb_strtolower($value, 'UTF-8');
|
||
}
|
||
|
||
return strtolower($value);
|
||
}
|
||
|
||
function itstudio_join_detect_progress_query_identity($raw_identity) {
|
||
$raw_identity = trim((string) $raw_identity);
|
||
$compacted = preg_replace('/\s+/u', '', $raw_identity);
|
||
|
||
$detected = array(
|
||
'raw' => $raw_identity,
|
||
'field' => '',
|
||
'value' => '',
|
||
);
|
||
|
||
if ($compacted === '') {
|
||
return $detected;
|
||
}
|
||
|
||
$email = sanitize_email($compacted);
|
||
if ($email !== '' && is_email($email)) {
|
||
$detected['field'] = 'email';
|
||
$detected['value'] = itstudio_join_normalize_lookup_value('email', $email);
|
||
return $detected;
|
||
}
|
||
|
||
if (preg_match('/^\d+$/', $compacted)) {
|
||
$digits = $compacted;
|
||
$len = strlen($digits);
|
||
|
||
if (preg_match('/^1[3-9]\d{9}$/', $digits)) {
|
||
$detected['field'] = 'phone';
|
||
$detected['value'] = itstudio_join_normalize_lookup_value('phone', $digits);
|
||
return $detected;
|
||
}
|
||
|
||
if ($len >= 10 && $len <= 12) {
|
||
$detected['field'] = 'student_id';
|
||
$detected['value'] = itstudio_join_normalize_lookup_value('student_id', $digits);
|
||
return $detected;
|
||
}
|
||
|
||
if ($len >= 5 && $len <= 12) {
|
||
$detected['field'] = 'qq';
|
||
$detected['value'] = itstudio_join_normalize_lookup_value('qq', $digits);
|
||
return $detected;
|
||
}
|
||
|
||
return $detected;
|
||
}
|
||
|
||
if (preg_match('/^[\x{4e00}-\x{9fff}·]{2,20}$/u', $compacted)) {
|
||
$detected['field'] = 'name';
|
||
$detected['value'] = itstudio_join_normalize_lookup_value('name', $compacted);
|
||
return $detected;
|
||
}
|
||
|
||
return $detected;
|
||
}
|
||
|
||
function itstudio_join_parse_result_records($raw) {
|
||
$raw = str_replace(array("\r\n", "\r"), "\n", (string) $raw);
|
||
if ($raw === '') {
|
||
return array();
|
||
}
|
||
|
||
$records = array();
|
||
$lines = explode("\n", $raw);
|
||
foreach ($lines as $line) {
|
||
$line = trim((string) $line);
|
||
if ($line === '' || strpos($line, '#') === 0) {
|
||
continue;
|
||
}
|
||
|
||
$header_probe = preg_replace('/\s+/u', '', $line);
|
||
$header_probe = function_exists('mb_strtolower') ? mb_strtolower($header_probe, 'UTF-8') : strtolower($header_probe);
|
||
$is_cn_header = (strpos($header_probe, '姓名') !== false) && (strpos($header_probe, 'qq') !== false || strpos($header_probe, '邮箱') !== false || strpos($header_probe, '学号') !== false);
|
||
$is_en_header = (strpos($header_probe, 'name') !== false) && (strpos($header_probe, 'qq') !== false || strpos($header_probe, 'email') !== false || strpos($header_probe, 'student') !== false);
|
||
if ($is_cn_header || $is_en_header) {
|
||
continue;
|
||
}
|
||
|
||
$parts = preg_split('/[,\|,\t]+/u', $line);
|
||
$parts = is_array($parts) ? array_values(array_filter(array_map('trim', $parts), static function ($item) {
|
||
return $item !== '';
|
||
})) : array();
|
||
if (empty($parts)) {
|
||
continue;
|
||
}
|
||
|
||
$name = '';
|
||
$qq = '';
|
||
$email = '';
|
||
$student_id = '';
|
||
|
||
if (count($parts) === 1) {
|
||
$single = (string) $parts[0];
|
||
if (strpos($single, '@') !== false) {
|
||
$email = $single;
|
||
} elseif (preg_match('/^\d{5,}$/', $single)) {
|
||
$qq = $single;
|
||
} else {
|
||
$name = $single;
|
||
}
|
||
} else {
|
||
$name = (string) ($parts[0] ?? '');
|
||
$qq = (string) ($parts[1] ?? '');
|
||
$email = (string) ($parts[2] ?? '');
|
||
$student_id = (string) ($parts[3] ?? '');
|
||
}
|
||
|
||
$record = array(
|
||
'name' => itstudio_join_normalize_lookup_value('name', $name),
|
||
'qq' => itstudio_join_normalize_lookup_value('qq', $qq),
|
||
'email' => itstudio_join_normalize_lookup_value('email', $email),
|
||
'student_id' => itstudio_join_normalize_lookup_value('student_id', $student_id),
|
||
);
|
||
|
||
if ($record['name'] === '' && $record['qq'] === '' && $record['email'] === '' && $record['student_id'] === '') {
|
||
continue;
|
||
}
|
||
|
||
$records[] = $record;
|
||
}
|
||
|
||
return $records;
|
||
}
|
||
|
||
function itstudio_join_record_matches_query($record, $query) {
|
||
$record = is_array($record) ? $record : array();
|
||
$query = is_array($query) ? $query : array();
|
||
$has_any_query = false;
|
||
|
||
foreach (array('name', 'qq', 'email', 'student_id', 'phone') as $field) {
|
||
$q = trim((string) ($query[$field] ?? ''));
|
||
if ($q === '') {
|
||
continue;
|
||
}
|
||
$has_any_query = true;
|
||
$r = trim((string) ($record[$field] ?? ''));
|
||
if ($r === '' || $r !== $q) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return $has_any_query;
|
||
}
|
||
|
||
function itstudio_join_find_record_in_stage_results($settings, $stage_key, $query) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
if (itstudio_join_is_file_result_mode($settings)) {
|
||
$attachment_id = itstudio_join_get_result_file_attachment_id($settings, $stage_key);
|
||
if ($attachment_id <= 0) {
|
||
return null;
|
||
}
|
||
$rows = itstudio_join_read_result_rows_from_attachment($attachment_id);
|
||
$records = itstudio_join_parse_result_rows_to_records($rows);
|
||
if (empty($records)) {
|
||
return null;
|
||
}
|
||
foreach ($records as $record) {
|
||
if (itstudio_join_record_matches_query($record, $query)) {
|
||
return !empty($record['passed']);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
if (itstudio_join_is_formidable_result_mode($settings)) {
|
||
static $entry_cache = array();
|
||
$cache_key = md5(wp_json_encode(array(
|
||
'form_id' => itstudio_join_get_formidable_form_id($settings),
|
||
'query' => $query,
|
||
)));
|
||
if (!isset($entry_cache[$cache_key])) {
|
||
$entry_cache[$cache_key] = itstudio_join_find_formidable_entry_id_by_query($settings, $query);
|
||
}
|
||
$entry_id = (int) $entry_cache[$cache_key];
|
||
if ($entry_id <= 0) {
|
||
return null;
|
||
}
|
||
return itstudio_join_formidable_entry_matches_stage($settings, $entry_id, $stage_key);
|
||
}
|
||
|
||
$field_map = itstudio_join_get_result_field_map();
|
||
$field_key = isset($field_map[$stage_key]) ? $field_map[$stage_key] : '';
|
||
if ($field_key === '') {
|
||
return null;
|
||
}
|
||
|
||
$raw = (string) ($settings[$field_key] ?? '');
|
||
if (trim($raw) === '') {
|
||
return null;
|
||
}
|
||
|
||
$records = itstudio_join_parse_result_records($raw);
|
||
if (empty($records)) {
|
||
return null;
|
||
}
|
||
|
||
foreach ($records as $record) {
|
||
if (itstudio_join_record_matches_query($record, $query)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function itstudio_join_progress_record_not_found_response($response) {
|
||
$response = is_array($response) ? $response : array();
|
||
$response['message_cn'] = '未查询到该报名者,请检查输入信息是否正确。';
|
||
$response['message_en'] = 'No matching applicant was found. Please check the information and try again.';
|
||
$response['tone'] = 'warning';
|
||
return $response;
|
||
}
|
||
|
||
function itstudio_join_get_stage_status_by_key($runtime, $stage_key) {
|
||
$runtime = is_array($runtime) ? $runtime : array();
|
||
$stages = isset($runtime['stages']) && is_array($runtime['stages']) ? $runtime['stages'] : array();
|
||
foreach ($stages as $stage) {
|
||
if (!is_array($stage)) {
|
||
continue;
|
||
}
|
||
if ((string) ($stage['key'] ?? '') === (string) $stage_key) {
|
||
return (string) ($stage['status'] ?? 'pending');
|
||
}
|
||
}
|
||
|
||
return 'pending';
|
||
}
|
||
|
||
function itstudio_join_resolve_progress_lookup($runtime = array(), $request_source = null) {
|
||
$runtime = is_array($runtime) ? $runtime : array();
|
||
$settings = isset($runtime['settings']) && is_array($runtime['settings']) ? $runtime['settings'] : itstudio_join_get_settings();
|
||
$request_source = is_array($request_source) ? $request_source : $_GET;
|
||
|
||
$raw_identity = isset($request_source['join_query_identity']) ? sanitize_text_field(wp_unslash((string) $request_source['join_query_identity'])) : '';
|
||
$raw_name = isset($request_source['join_query_name']) ? sanitize_text_field(wp_unslash((string) $request_source['join_query_name'])) : '';
|
||
$raw_qq = isset($request_source['join_query_qq']) ? sanitize_text_field(wp_unslash((string) $request_source['join_query_qq'])) : '';
|
||
$raw_email = isset($request_source['join_query_email']) ? sanitize_text_field(wp_unslash((string) $request_source['join_query_email'])) : '';
|
||
$raw_student_id = isset($request_source['join_query_student_id']) ? sanitize_text_field(wp_unslash((string) $request_source['join_query_student_id'])) : '';
|
||
|
||
if (trim($raw_identity) === '') {
|
||
foreach (array($raw_name, $raw_qq, $raw_email, $raw_student_id) as $legacy_value) {
|
||
$legacy_value = trim((string) $legacy_value);
|
||
if ($legacy_value !== '') {
|
||
$raw_identity = $legacy_value;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
$detected_identity = itstudio_join_detect_progress_query_identity($raw_identity);
|
||
|
||
$query = array(
|
||
'name' => '',
|
||
'qq' => '',
|
||
'email' => '',
|
||
'student_id' => '',
|
||
'phone' => '',
|
||
);
|
||
if ($detected_identity['field'] !== '') {
|
||
$query[$detected_identity['field']] = (string) $detected_identity['value'];
|
||
}
|
||
|
||
$has_input_value = trim((string) $raw_identity) !== '';
|
||
$has_query_value = $detected_identity['field'] !== '';
|
||
|
||
$submitted = isset($request_source['join_progress_lookup']) || $has_input_value;
|
||
$response = array(
|
||
'submitted' => $submitted,
|
||
'has_query' => $has_query_value,
|
||
'identity' => $raw_identity,
|
||
'identity_field' => (string) ($detected_identity['field'] ?? ''),
|
||
'name' => $raw_name,
|
||
'qq' => $raw_qq,
|
||
'email' => $raw_email,
|
||
'student_id' => $raw_student_id,
|
||
'message_cn' => '',
|
||
'message_en' => '',
|
||
'tone' => 'info',
|
||
);
|
||
|
||
if (!$submitted) {
|
||
return $response;
|
||
}
|
||
|
||
if (!$has_input_value) {
|
||
$response['message_cn'] = '请输入姓名(中文)/ QQ / 邮箱 / 学号 / 手机号。';
|
||
$response['message_en'] = 'Please enter Name (Chinese) / QQ / Email / Student ID / Phone.';
|
||
$response['tone'] = 'warning';
|
||
return $response;
|
||
}
|
||
|
||
if (!$has_query_value) {
|
||
$response['message_cn'] = '无法识别输入内容,请输入姓名(中文)/ QQ / 邮箱 / 学号 / 手机号。';
|
||
$response['message_en'] = 'Input cannot be recognized. Please enter Name (Chinese) / QQ / Email / Student ID / Phone.';
|
||
$response['tone'] = 'warning';
|
||
return $response;
|
||
}
|
||
|
||
$status_registration = itstudio_join_get_stage_status_by_key($runtime, 'registration');
|
||
$status_first = itstudio_join_get_stage_status_by_key($runtime, 'first_interview');
|
||
$status_assessment = itstudio_join_get_stage_status_by_key($runtime, 'assessment');
|
||
$status_second = itstudio_join_get_stage_status_by_key($runtime, 'second_interview');
|
||
$status_notice = itstudio_join_get_stage_status_by_key($runtime, 'public_notice');
|
||
|
||
$uploaded_first = itstudio_join_has_uploaded_result_for_stage($settings, 'first_interview');
|
||
$uploaded_assessment = itstudio_join_has_uploaded_result_for_stage($settings, 'assessment');
|
||
$uploaded_second = itstudio_join_has_uploaded_result_for_stage($settings, 'second_interview');
|
||
$uploaded_notice = itstudio_join_has_uploaded_result_for_stage($settings, 'public_notice');
|
||
|
||
if ($status_registration === 'active' || $status_registration === 'upcoming') {
|
||
$response['message_cn'] = '报名阶段暂不开放进度查询。';
|
||
$response['message_en'] = 'Progress lookup is not available during registration stage.';
|
||
$response['tone'] = 'success';
|
||
return $response;
|
||
}
|
||
if ($status_first === 'upcoming' || $status_first === 'pending' || $status_first === 'active') {
|
||
$response['message_cn'] = '当前暂不可查询,请等待第一次面试结束后查看结果。';
|
||
$response['message_en'] = 'Lookup is not available yet. Please wait until Interview I results are published.';
|
||
$response['tone'] = 'warning';
|
||
return $response;
|
||
}
|
||
|
||
if ($status_first === 'completed') {
|
||
if (!$uploaded_first) {
|
||
$response['message_cn'] = '第一次面试已结束,结果尚未上传。';
|
||
$response['message_en'] = 'Interview I has ended, but results are not uploaded yet.';
|
||
$response['tone'] = 'warning';
|
||
return $response;
|
||
}
|
||
|
||
$passed_first = itstudio_join_find_record_in_stage_results($settings, 'first_interview', $query);
|
||
if ($passed_first === null) {
|
||
return itstudio_join_progress_record_not_found_response($response);
|
||
}
|
||
if (!$passed_first) {
|
||
$response['message_cn'] = '未通过第一次面试,期待来年再次报名。';
|
||
$response['message_en'] = 'You did not pass Interview I. Welcome to apply again next year.';
|
||
$response['tone'] = 'error';
|
||
return $response;
|
||
}
|
||
|
||
if ($status_assessment === 'upcoming' || $status_assessment === 'pending' || $status_assessment === 'active') {
|
||
$response['message_cn'] = '恭喜,您已通过第一次面试。';
|
||
$response['message_en'] = 'Congratulations! You have passed Interview I.';
|
||
$response['tone'] = 'success';
|
||
return $response;
|
||
}
|
||
}
|
||
|
||
if ($status_assessment === 'completed') {
|
||
if (!$uploaded_assessment) {
|
||
$response['message_cn'] = '国庆能力摸底已结束,结果尚未上传。';
|
||
$response['message_en'] = 'Assessment stage has ended, but results are not uploaded yet.';
|
||
$response['tone'] = 'warning';
|
||
return $response;
|
||
}
|
||
|
||
$passed_assessment = itstudio_join_find_record_in_stage_results($settings, 'assessment', $query);
|
||
if ($passed_assessment === null) {
|
||
return itstudio_join_progress_record_not_found_response($response);
|
||
}
|
||
if (!$passed_assessment) {
|
||
$response['message_cn'] = '未通过国庆能力摸底,期待来年再次报名。';
|
||
$response['message_en'] = 'You did not pass the assessment stage. Welcome to apply again next year.';
|
||
$response['tone'] = 'error';
|
||
return $response;
|
||
}
|
||
|
||
if ($status_second === 'upcoming' || $status_second === 'pending' || $status_second === 'active') {
|
||
$response['message_cn'] = '恭喜,您已通过国庆能力摸底。';
|
||
$response['message_en'] = 'Congratulations! You have passed the assessment stage.';
|
||
$response['tone'] = 'success';
|
||
return $response;
|
||
}
|
||
}
|
||
|
||
if ($status_second === 'completed') {
|
||
if (!$uploaded_second) {
|
||
$response['message_cn'] = '第二次面试已结束,结果尚未上传。';
|
||
$response['message_en'] = 'Interview II has ended, but results are not uploaded yet.';
|
||
$response['tone'] = 'warning';
|
||
return $response;
|
||
}
|
||
|
||
$passed_second = itstudio_join_find_record_in_stage_results($settings, 'second_interview', $query);
|
||
if ($passed_second === null) {
|
||
return itstudio_join_progress_record_not_found_response($response);
|
||
}
|
||
if (!$passed_second) {
|
||
$response['message_cn'] = '未通过第二次面试,期待来年再次报名。';
|
||
$response['message_en'] = 'You did not pass Interview II. Welcome to apply again next year.';
|
||
$response['tone'] = 'error';
|
||
return $response;
|
||
}
|
||
|
||
if ($status_notice === 'pending' || $status_notice === 'upcoming' || $status_notice === 'active') {
|
||
if ($status_notice === 'active' && $uploaded_notice) {
|
||
$admitted = itstudio_join_find_record_in_stage_results($settings, 'public_notice', $query);
|
||
if ($admitted === null) {
|
||
return itstudio_join_progress_record_not_found_response($response);
|
||
}
|
||
if ($admitted) {
|
||
$response['message_cn'] = '恭喜,您已被录取。';
|
||
$response['message_en'] = 'Congratulations! You have been admitted.';
|
||
$response['tone'] = 'success';
|
||
return $response;
|
||
}
|
||
|
||
$response['message_cn'] = '很遗憾,您未被录取,期待来年再次报名。';
|
||
$response['message_en'] = 'Sorry, you were not admitted. Welcome to apply again next year.';
|
||
$response['tone'] = 'error';
|
||
return $response;
|
||
}
|
||
|
||
$response['message_cn'] = '恭喜,您已通过第二次面试,请等待录取结果公布。';
|
||
$response['message_en'] = 'Congratulations! You have passed Interview II. Please wait for the final result.';
|
||
$response['tone'] = 'success';
|
||
return $response;
|
||
}
|
||
}
|
||
|
||
if (($status_notice === 'active' || $status_notice === 'completed') && $uploaded_notice) {
|
||
$admitted = itstudio_join_find_record_in_stage_results($settings, 'public_notice', $query);
|
||
if ($admitted === null) {
|
||
return itstudio_join_progress_record_not_found_response($response);
|
||
}
|
||
if ($admitted) {
|
||
$response['message_cn'] = '恭喜,您已被录取。';
|
||
$response['message_en'] = 'Congratulations! You have been admitted.';
|
||
$response['tone'] = 'success';
|
||
} else {
|
||
$response['message_cn'] = '很遗憾,您未被录取,期待来年再次报名。';
|
||
$response['message_en'] = 'Sorry, you were not admitted. Welcome to apply again next year.';
|
||
$response['tone'] = 'error';
|
||
}
|
||
return $response;
|
||
}
|
||
|
||
$response['message_cn'] = '已报名。';
|
||
$response['message_en'] = 'Registered.';
|
||
$response['tone'] = 'success';
|
||
return $response;
|
||
}
|
||
|
||
function itstudio_join_datetime_to_ms($date) {
|
||
if (!($date instanceof DateTimeImmutable)) {
|
||
return null;
|
||
}
|
||
|
||
return (int) $date->format('U') * 1000;
|
||
}
|
||
|
||
function itstudio_join_resolve_stage_status($now, $start, $end) {
|
||
if (!($now instanceof DateTimeImmutable)) {
|
||
return 'pending';
|
||
}
|
||
|
||
if (!($start instanceof DateTimeImmutable) && !($end instanceof DateTimeImmutable)) {
|
||
return 'pending';
|
||
}
|
||
|
||
if ($start instanceof DateTimeImmutable && $now < $start) {
|
||
return 'upcoming';
|
||
}
|
||
|
||
if ($start instanceof DateTimeImmutable && $end instanceof DateTimeImmutable) {
|
||
if ($now >= $start && $now <= $end) {
|
||
return 'active';
|
||
}
|
||
if ($now > $end) {
|
||
return 'completed';
|
||
}
|
||
}
|
||
|
||
if ($start instanceof DateTimeImmutable && !($end instanceof DateTimeImmutable)) {
|
||
if ($now >= $start) {
|
||
return 'active';
|
||
}
|
||
}
|
||
|
||
if (!($start instanceof DateTimeImmutable) && $end instanceof DateTimeImmutable) {
|
||
return $now <= $end ? 'active' : 'completed';
|
||
}
|
||
|
||
return 'upcoming';
|
||
}
|
||
|
||
function itstudio_join_is_in_window($now, $start, $end) {
|
||
if (!($now instanceof DateTimeImmutable) || !($start instanceof DateTimeImmutable)) {
|
||
return false;
|
||
}
|
||
|
||
if ($now < $start) {
|
||
return false;
|
||
}
|
||
|
||
if ($end instanceof DateTimeImmutable && $now > $end) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
function itstudio_join_format_stage_range($start, $end, $all_day = false) {
|
||
if (!($start instanceof DateTimeImmutable) && !($end instanceof DateTimeImmutable)) {
|
||
return array(
|
||
'cn' => '时间待定',
|
||
'en' => 'Schedule TBA',
|
||
);
|
||
}
|
||
|
||
$format_cn_day = 'Y.m.d';
|
||
$format_cn_full = 'Y.m.d H:i';
|
||
$format_en_day = 'M j, Y';
|
||
$format_en_full = 'M j, Y H:i';
|
||
|
||
if ($start instanceof DateTimeImmutable && !($end instanceof DateTimeImmutable)) {
|
||
return array(
|
||
'cn' => $all_day ? $start->format($format_cn_day) : $start->format($format_cn_full),
|
||
'en' => $all_day ? $start->format($format_en_day) : $start->format($format_en_full),
|
||
);
|
||
}
|
||
|
||
if (!($start instanceof DateTimeImmutable) && $end instanceof DateTimeImmutable) {
|
||
return array(
|
||
'cn' => $all_day ? $end->format($format_cn_day) : $end->format($format_cn_full),
|
||
'en' => $all_day ? $end->format($format_en_day) : $end->format($format_en_full),
|
||
);
|
||
}
|
||
|
||
if (!($start instanceof DateTimeImmutable) || !($end instanceof DateTimeImmutable)) {
|
||
return array(
|
||
'cn' => '时间待定',
|
||
'en' => 'Schedule TBA',
|
||
);
|
||
}
|
||
|
||
if (!$all_day && $start->format('Y-m-d') === $end->format('Y-m-d')) {
|
||
$date = $start->format($format_cn_day);
|
||
$start_time = $start->format('H:i');
|
||
$end_time = $end->format('H:i');
|
||
$range = $date . ' ' . $start_time;
|
||
if ($start_time !== $end_time) {
|
||
$range .= '-' . $end_time;
|
||
}
|
||
return array(
|
||
'cn' => $range,
|
||
'en' => $range,
|
||
);
|
||
}
|
||
|
||
$start_cn = $all_day ? $start->format($format_cn_day) : $start->format($format_cn_full);
|
||
$end_cn = $all_day ? $end->format($format_cn_day) : $end->format($format_cn_full);
|
||
$start_en = $all_day ? $start->format($format_en_day) : $start->format($format_en_full);
|
||
$end_en = $all_day ? $end->format($format_en_day) : $end->format($format_en_full);
|
||
|
||
if ($start_cn === $end_cn) {
|
||
return array(
|
||
'cn' => $start_cn,
|
||
'en' => $start_en,
|
||
);
|
||
}
|
||
|
||
return array(
|
||
'cn' => $start_cn . ' - ' . $end_cn,
|
||
'en' => $start_en . ' - ' . $end_en,
|
||
);
|
||
}
|
||
|
||
function itstudio_join_format_stage_overlay_range($start, $end, $all_day = false) {
|
||
if (!$all_day) {
|
||
$date = $start instanceof DateTimeImmutable ? $start : $end;
|
||
$is_same_day = !($start instanceof DateTimeImmutable)
|
||
|| !($end instanceof DateTimeImmutable)
|
||
|| $start->format('Y-m-d') === $end->format('Y-m-d');
|
||
if ($date instanceof DateTimeImmutable && $is_same_day) {
|
||
$formatted = $date->format('Y.m.d');
|
||
return array(
|
||
'cn' => $formatted,
|
||
'en' => $formatted,
|
||
);
|
||
}
|
||
}
|
||
|
||
return itstudio_join_format_stage_range($start, $end, $all_day);
|
||
}
|
||
|
||
function itstudio_join_get_stage_photo_url($settings, $stage_key) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
$field_map = itstudio_join_get_photo_field_map();
|
||
$field_key = isset($field_map[$stage_key]) ? $field_map[$stage_key] : '';
|
||
|
||
$attachment_id = 0;
|
||
if ($field_key !== '' && isset($settings[$field_key])) {
|
||
$attachment_id = absint($settings[$field_key]);
|
||
}
|
||
|
||
if ($attachment_id > 0) {
|
||
$photo_url = wp_get_attachment_image_url($attachment_id, 'full');
|
||
if (is_string($photo_url) && $photo_url !== '') {
|
||
return $photo_url;
|
||
}
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
function itstudio_join_get_runtime_data() {
|
||
static $cached = null;
|
||
if (is_array($cached)) {
|
||
return $cached;
|
||
}
|
||
|
||
$settings = itstudio_join_get_settings();
|
||
$timezone = wp_timezone();
|
||
$now = new DateTimeImmutable('now', $timezone);
|
||
|
||
$registration_start = itstudio_join_parse_datetime_local($settings['registration_start'], false);
|
||
$registration_end = itstudio_join_parse_datetime_local($settings['registration_end'], true);
|
||
if ($registration_start instanceof DateTimeImmutable) {
|
||
$registration_start = $registration_start->setTime(0, 0, 0);
|
||
}
|
||
if ($registration_end instanceof DateTimeImmutable) {
|
||
$registration_end = $registration_end->setTime(23, 59, 59);
|
||
}
|
||
if ($registration_start instanceof DateTimeImmutable && $registration_end instanceof DateTimeImmutable && $registration_end < $registration_start) {
|
||
$registration_end = $registration_start;
|
||
}
|
||
|
||
$first_interview_start_raw = isset($settings['first_interview_date']) ? (string) $settings['first_interview_date'] : '';
|
||
$first_interview_end_raw = isset($settings['first_interview_end']) ? (string) $settings['first_interview_end'] : '';
|
||
$first_interview_start = itstudio_join_parse_datetime_local($first_interview_start_raw, false);
|
||
$first_interview_end = itstudio_join_parse_datetime_local($first_interview_end_raw, true);
|
||
if ($first_interview_start instanceof DateTimeImmutable && !($first_interview_end instanceof DateTimeImmutable)) {
|
||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', trim($first_interview_start_raw))) {
|
||
$first_interview_end = $first_interview_start->setTime(23, 59, 59);
|
||
} else {
|
||
$first_interview_end = $first_interview_start;
|
||
}
|
||
} elseif (!($first_interview_start instanceof DateTimeImmutable) && $first_interview_end instanceof DateTimeImmutable) {
|
||
$first_interview_start = $first_interview_end;
|
||
} elseif ($first_interview_start instanceof DateTimeImmutable && $first_interview_end instanceof DateTimeImmutable && $first_interview_end < $first_interview_start) {
|
||
$first_interview_end = $first_interview_start;
|
||
}
|
||
|
||
$second_interview_start_raw = isset($settings['second_interview_date']) ? (string) $settings['second_interview_date'] : '';
|
||
$second_interview_end_raw = isset($settings['second_interview_end']) ? (string) $settings['second_interview_end'] : '';
|
||
$second_interview_start = itstudio_join_parse_datetime_local($second_interview_start_raw, false);
|
||
$second_interview_end = itstudio_join_parse_datetime_local($second_interview_end_raw, true);
|
||
if ($second_interview_start instanceof DateTimeImmutable && !($second_interview_end instanceof DateTimeImmutable)) {
|
||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', trim($second_interview_start_raw))) {
|
||
$second_interview_end = $second_interview_start->setTime(23, 59, 59);
|
||
} else {
|
||
$second_interview_end = $second_interview_start;
|
||
}
|
||
} elseif (!($second_interview_start instanceof DateTimeImmutable) && $second_interview_end instanceof DateTimeImmutable) {
|
||
$second_interview_start = $second_interview_end;
|
||
} elseif ($second_interview_start instanceof DateTimeImmutable && $second_interview_end instanceof DateTimeImmutable && $second_interview_end < $second_interview_start) {
|
||
$second_interview_end = $second_interview_start;
|
||
}
|
||
|
||
$first_interview_location_cn = trim((string) ($settings['first_interview_location_cn'] ?? ''));
|
||
$first_interview_location_en = trim((string) ($settings['first_interview_location_en'] ?? ''));
|
||
if ($first_interview_location_en === '') {
|
||
$first_interview_location_en = $first_interview_location_cn;
|
||
}
|
||
|
||
$second_interview_location_cn = trim((string) ($settings['second_interview_location_cn'] ?? ''));
|
||
$second_interview_location_en = trim((string) ($settings['second_interview_location_en'] ?? ''));
|
||
if ($second_interview_location_en === '') {
|
||
$second_interview_location_en = $second_interview_location_cn;
|
||
}
|
||
|
||
$notice_start_day = itstudio_join_parse_date($settings['notice_start_date']);
|
||
$notice_start = $notice_start_day instanceof DateTimeImmutable ? $notice_start_day->setTime(0, 0, 0) : null;
|
||
$notice_end = $notice_start instanceof DateTimeImmutable ? $notice_start->modify('+6 days')->setTime(23, 59, 59) : null;
|
||
|
||
$recruitment_year = null;
|
||
if ($registration_start instanceof DateTimeImmutable) {
|
||
$recruitment_year = (int) $registration_start->format('Y');
|
||
} elseif ($notice_start instanceof DateTimeImmutable) {
|
||
$recruitment_year = (int) $notice_start->format('Y');
|
||
} else {
|
||
$recruitment_year = (int) $now->format('Y');
|
||
}
|
||
|
||
$default_assessment_start = DateTimeImmutable::createFromFormat('!Y-m-d', sprintf('%04d-10-01', $recruitment_year), $timezone);
|
||
$default_assessment_end_base = DateTimeImmutable::createFromFormat('!Y-m-d', sprintf('%04d-10-07', $recruitment_year), $timezone);
|
||
$default_assessment_end = $default_assessment_end_base instanceof DateTimeImmutable ? $default_assessment_end_base->setTime(23, 59, 59) : null;
|
||
|
||
$assessment_start_day = itstudio_join_parse_date($settings['assessment_start_date']);
|
||
$assessment_end_day = itstudio_join_parse_date($settings['assessment_end_date']);
|
||
$assessment_start = $assessment_start_day instanceof DateTimeImmutable
|
||
? $assessment_start_day->setTime(0, 0, 0)
|
||
: $default_assessment_start;
|
||
$assessment_end = $assessment_end_day instanceof DateTimeImmutable
|
||
? $assessment_end_day->setTime(23, 59, 59)
|
||
: $default_assessment_end;
|
||
|
||
if ($assessment_start_day instanceof DateTimeImmutable && !($assessment_end_day instanceof DateTimeImmutable)) {
|
||
$assessment_end = $assessment_start->setTime(23, 59, 59);
|
||
} elseif (!($assessment_start_day instanceof DateTimeImmutable) && $assessment_end_day instanceof DateTimeImmutable) {
|
||
$assessment_start = $assessment_end_day->setTime(0, 0, 0);
|
||
}
|
||
if ($assessment_start instanceof DateTimeImmutable && $assessment_end instanceof DateTimeImmutable && $assessment_end < $assessment_start) {
|
||
$assessment_end = $assessment_start->setTime(23, 59, 59);
|
||
}
|
||
|
||
$stage_seed = array(
|
||
array(
|
||
'key' => 'registration',
|
||
'label_cn' => '报名阶段',
|
||
'label_en' => 'Registration',
|
||
'short_cn' => '报名',
|
||
'short_en' => 'Reg',
|
||
'start' => $registration_start,
|
||
'end' => $registration_end,
|
||
'all_day' => true,
|
||
'location_cn' => '',
|
||
'location_en' => '',
|
||
'result_uploaded' => false,
|
||
),
|
||
array(
|
||
'key' => 'first_interview',
|
||
'label_cn' => '第一次面试',
|
||
'label_en' => 'Interview I',
|
||
'short_cn' => '一面',
|
||
'short_en' => 'I-1',
|
||
'start' => $first_interview_start,
|
||
'end' => $first_interview_end,
|
||
'all_day' => false,
|
||
'location_cn' => $first_interview_location_cn,
|
||
'location_en' => $first_interview_location_en,
|
||
'result_uploaded' => itstudio_join_has_uploaded_result_for_stage($settings, 'first_interview'),
|
||
),
|
||
array(
|
||
'key' => 'assessment',
|
||
'label_cn' => '国庆能力摸底',
|
||
'label_en' => 'Assessment',
|
||
'short_cn' => '摸底',
|
||
'short_en' => 'Assess',
|
||
'start' => $assessment_start,
|
||
'end' => $assessment_end,
|
||
'all_day' => true,
|
||
'location_cn' => '',
|
||
'location_en' => '',
|
||
'result_uploaded' => itstudio_join_has_uploaded_result_for_stage($settings, 'assessment'),
|
||
),
|
||
array(
|
||
'key' => 'second_interview',
|
||
'label_cn' => '第二次面试',
|
||
'label_en' => 'Interview II',
|
||
'short_cn' => '二面',
|
||
'short_en' => 'I-2',
|
||
'start' => $second_interview_start,
|
||
'end' => $second_interview_end,
|
||
'all_day' => false,
|
||
'location_cn' => $second_interview_location_cn,
|
||
'location_en' => $second_interview_location_en,
|
||
'result_uploaded' => itstudio_join_has_uploaded_result_for_stage($settings, 'second_interview'),
|
||
),
|
||
array(
|
||
'key' => 'public_notice',
|
||
'label_cn' => '录取结果公布',
|
||
'label_en' => 'Public Notice',
|
||
'short_cn' => '公布',
|
||
'short_en' => 'Notice',
|
||
'start' => $notice_start,
|
||
'end' => $notice_end,
|
||
'all_day' => true,
|
||
'location_cn' => '',
|
||
'location_en' => '',
|
||
'result_uploaded' => itstudio_join_has_uploaded_result_for_stage($settings, 'public_notice'),
|
||
),
|
||
);
|
||
|
||
$stages = array();
|
||
foreach ($stage_seed as $stage) {
|
||
$range = itstudio_join_format_stage_range($stage['start'], $stage['end'], !empty($stage['all_day']));
|
||
$overlay_range = itstudio_join_format_stage_overlay_range($stage['start'], $stage['end'], !empty($stage['all_day']));
|
||
$status = itstudio_join_resolve_stage_status($now, $stage['start'], $stage['end']);
|
||
$stages[] = array(
|
||
'key' => $stage['key'],
|
||
'label_cn' => $stage['label_cn'],
|
||
'label_en' => $stage['label_en'],
|
||
'short_cn' => $stage['short_cn'],
|
||
'short_en' => $stage['short_en'],
|
||
'status' => $status,
|
||
'range_cn' => $range['cn'],
|
||
'range_en' => $range['en'],
|
||
'overlay_range_cn' => $overlay_range['cn'],
|
||
'overlay_range_en' => $overlay_range['en'],
|
||
'location_cn' => isset($stage['location_cn']) ? (string) $stage['location_cn'] : '',
|
||
'location_en' => isset($stage['location_en']) ? (string) $stage['location_en'] : '',
|
||
'result_uploaded' => !empty($stage['result_uploaded']),
|
||
'start_ts' => itstudio_join_datetime_to_ms($stage['start']),
|
||
'end_ts' => itstudio_join_datetime_to_ms($stage['end']),
|
||
);
|
||
}
|
||
|
||
$current_stage_index = -1;
|
||
foreach ($stages as $index => $stage) {
|
||
if ($stage['status'] === 'active') {
|
||
$current_stage_index = (int) $index;
|
||
break;
|
||
}
|
||
}
|
||
|
||
$next_stage_index = -1;
|
||
if ($current_stage_index < 0) {
|
||
foreach ($stages as $index => $stage) {
|
||
if (($stage['status'] ?? '') === 'upcoming') {
|
||
$next_stage_index = (int) $index;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
$current_stage_mode = 'inactive';
|
||
$display_stage_index = -1;
|
||
if ($current_stage_index >= 0 && isset($stages[$current_stage_index])) {
|
||
$display_stage_index = $current_stage_index;
|
||
$current_stage_mode = 'active';
|
||
} elseif ($next_stage_index >= 0 && isset($stages[$next_stage_index])) {
|
||
$display_stage_index = $next_stage_index;
|
||
$current_stage_mode = 'next';
|
||
}
|
||
|
||
$current_stage = ($display_stage_index >= 0 && isset($stages[$display_stage_index]))
|
||
? $stages[$display_stage_index]
|
||
: array(
|
||
'key' => 'inactive',
|
||
'label_cn' => '当前未在招新时段',
|
||
'label_en' => 'Recruitment is currently closed',
|
||
'short_cn' => '',
|
||
'short_en' => '',
|
||
'status' => 'inactive',
|
||
'range_cn' => '请关注后续通知',
|
||
'range_en' => 'Please check later updates',
|
||
'location_cn' => '',
|
||
'location_en' => '',
|
||
'result_uploaded' => false,
|
||
'start_ts' => null,
|
||
'end_ts' => null,
|
||
);
|
||
|
||
$is_registration_open = itstudio_join_is_in_window($now, $registration_start, $registration_end);
|
||
|
||
$is_query_open = false;
|
||
$query_start = null;
|
||
if ($registration_end instanceof DateTimeImmutable) {
|
||
$query_start = $registration_end->modify('+1 second');
|
||
} elseif ($first_interview_start instanceof DateTimeImmutable) {
|
||
$query_start = $first_interview_start;
|
||
}
|
||
if ($query_start instanceof DateTimeImmutable) {
|
||
if ($notice_end instanceof DateTimeImmutable) {
|
||
$is_query_open = itstudio_join_is_in_window($now, $query_start, $notice_end);
|
||
} else {
|
||
$is_query_open = ($now >= $query_start);
|
||
}
|
||
}
|
||
|
||
$is_notice_open = itstudio_join_is_in_window($now, $notice_start, $notice_end);
|
||
$is_notice_finished = ($notice_end instanceof DateTimeImmutable) && ($now > $notice_end);
|
||
$current_stage_photo_url = itstudio_join_get_stage_photo_url($settings, (string) ($current_stage['key'] ?? ''));
|
||
if ($current_stage_photo_url === '') {
|
||
$current_stage_photo_url = get_template_directory_uri() . '/resources/it_logo_2024.svg';
|
||
}
|
||
|
||
$season_start = null;
|
||
$season_end = null;
|
||
foreach ($stage_seed as $stage_window) {
|
||
$stage_start = $stage_window['start'] instanceof DateTimeImmutable ? $stage_window['start'] : null;
|
||
$stage_end = $stage_window['end'] instanceof DateTimeImmutable ? $stage_window['end'] : null;
|
||
if (!($stage_start instanceof DateTimeImmutable) && !($stage_end instanceof DateTimeImmutable)) {
|
||
continue;
|
||
}
|
||
|
||
$window_start = $stage_start instanceof DateTimeImmutable ? $stage_start : $stage_end;
|
||
$window_end = $stage_end instanceof DateTimeImmutable ? $stage_end : $stage_start;
|
||
if (!($window_start instanceof DateTimeImmutable) || !($window_end instanceof DateTimeImmutable)) {
|
||
continue;
|
||
}
|
||
|
||
if (!($season_start instanceof DateTimeImmutable) || $window_start < $season_start) {
|
||
$season_start = $window_start;
|
||
}
|
||
if (!($season_end instanceof DateTimeImmutable) || $window_end > $season_end) {
|
||
$season_end = $window_end;
|
||
}
|
||
}
|
||
|
||
$show_progress_visual = false;
|
||
if ($season_start instanceof DateTimeImmutable && $season_end instanceof DateTimeImmutable) {
|
||
$show_progress_visual = ($now >= $season_start && $now <= $season_end);
|
||
} elseif ($current_stage_index >= 0) {
|
||
$show_progress_visual = true;
|
||
}
|
||
|
||
$cached = array(
|
||
'settings' => $settings,
|
||
'timezone' => $timezone->getName(),
|
||
'recruitment_year' => $recruitment_year,
|
||
'now_ts' => (int) $now->format('U') * 1000,
|
||
'stages' => $stages,
|
||
'current_stage_index' => $current_stage_index,
|
||
'current_stage_mode' => $current_stage_mode,
|
||
'current_stage' => $current_stage,
|
||
'is_registration_open' => $is_registration_open,
|
||
'is_query_open' => $is_query_open,
|
||
'is_notice_open' => $is_notice_open,
|
||
'is_notice_finished' => $is_notice_finished,
|
||
'show_progress_visual' => $show_progress_visual,
|
||
'current_stage_photo_url' => $current_stage_photo_url,
|
||
'query_deadline_cn' => $notice_end instanceof DateTimeImmutable ? $notice_end->format('Y-m-d H:i') : '',
|
||
'query_deadline_en' => $notice_end instanceof DateTimeImmutable ? $notice_end->format('M j, Y H:i') : '',
|
||
);
|
||
|
||
return $cached;
|
||
}
|
||
|
||
function itstudio_join_get_frontend_payload() {
|
||
$runtime = itstudio_join_get_runtime_data();
|
||
return array(
|
||
'nowTs' => (int) ($runtime['now_ts'] ?? 0),
|
||
'currentStageIndex' => (int) ($runtime['current_stage_index'] ?? -1),
|
||
'stages' => array_values(array_map(static function ($stage) {
|
||
return array(
|
||
'key' => (string) ($stage['key'] ?? ''),
|
||
'labelCn' => (string) ($stage['label_cn'] ?? ''),
|
||
'labelEn' => (string) ($stage['label_en'] ?? ''),
|
||
'shortCn' => (string) ($stage['short_cn'] ?? ''),
|
||
'shortEn' => (string) ($stage['short_en'] ?? ''),
|
||
'status' => (string) ($stage['status'] ?? 'pending'),
|
||
'rangeCn' => (string) ($stage['range_cn'] ?? ''),
|
||
'rangeEn' => (string) ($stage['range_en'] ?? ''),
|
||
'locationCn' => (string) ($stage['location_cn'] ?? ''),
|
||
'locationEn' => (string) ($stage['location_en'] ?? ''),
|
||
'resultUploaded' => !empty($stage['result_uploaded']),
|
||
'startTs' => isset($stage['start_ts']) ? $stage['start_ts'] : null,
|
||
'endTs' => isset($stage['end_ts']) ? $stage['end_ts'] : null,
|
||
);
|
||
}, (array) ($runtime['stages'] ?? array()))),
|
||
'progressLookup' => array(
|
||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||
'action' => 'itstudio_join_progress_lookup',
|
||
'nonce' => wp_create_nonce('itstudio_join_progress_lookup'),
|
||
),
|
||
);
|
||
}
|
||
|
||
function itstudio_join_ajax_progress_lookup() {
|
||
if (!check_ajax_referer('itstudio_join_progress_lookup', 'nonce', false)) {
|
||
wp_send_json_error(
|
||
array(
|
||
'tone' => 'error',
|
||
'message_cn' => '请求已过期,请刷新页面后重试。',
|
||
'message_en' => 'Request expired. Please refresh the page and try again.',
|
||
),
|
||
403
|
||
);
|
||
}
|
||
|
||
$runtime = itstudio_join_get_runtime_data();
|
||
$request_source = $_POST;
|
||
if (!isset($request_source['join_progress_lookup'])) {
|
||
$request_source['join_progress_lookup'] = '1';
|
||
}
|
||
|
||
$lookup = itstudio_join_resolve_progress_lookup($runtime, $request_source);
|
||
$tone = trim((string) ($lookup['tone'] ?? 'info'));
|
||
if (!in_array($tone, array('success', 'warning', 'error', 'info'), true)) {
|
||
$tone = 'info';
|
||
}
|
||
|
||
wp_send_json_success(
|
||
array(
|
||
'submitted' => !empty($lookup['submitted']),
|
||
'has_query' => !empty($lookup['has_query']),
|
||
'identity' => (string) ($lookup['identity'] ?? ''),
|
||
'identity_field' => (string) ($lookup['identity_field'] ?? ''),
|
||
'tone' => $tone,
|
||
'message_cn' => (string) ($lookup['message_cn'] ?? ''),
|
||
'message_en' => (string) ($lookup['message_en'] ?? ''),
|
||
)
|
||
);
|
||
}
|
||
add_action('wp_ajax_itstudio_join_progress_lookup', 'itstudio_join_ajax_progress_lookup');
|
||
add_action('wp_ajax_nopriv_itstudio_join_progress_lookup', 'itstudio_join_ajax_progress_lookup');
|
||
|
||
function itstudio_join_force_formidable_ajax_submit($form) {
|
||
if (!itstudio_is_join_page_context() || !is_object($form)) {
|
||
return $form;
|
||
}
|
||
|
||
if (!isset($form->options) || !is_array($form->options)) {
|
||
$form->options = array();
|
||
}
|
||
|
||
$form->options['ajax_submit'] = 1;
|
||
|
||
return $form;
|
||
}
|
||
add_filter('frm_pre_display_form', 'itstudio_join_force_formidable_ajax_submit', 20);
|
||
|
||
function itstudio_join_enqueue_assets() {
|
||
if (!itstudio_is_join_page_context()) {
|
||
return;
|
||
}
|
||
|
||
wp_enqueue_style(
|
||
'itstudio-join-page',
|
||
get_template_directory_uri() . '/assets/css/join-page.css',
|
||
array('itstudio-content'),
|
||
'1.1.1'
|
||
);
|
||
|
||
wp_enqueue_script(
|
||
'itstudio-animejs',
|
||
get_template_directory_uri() . '/assets/js/vendor/anime.min.js',
|
||
array(),
|
||
'3.2.2',
|
||
true
|
||
);
|
||
|
||
wp_enqueue_script(
|
||
'itstudio-join-canvas',
|
||
get_template_directory_uri() . '/assets/js/join-canvas.js',
|
||
array('itstudio-animejs'),
|
||
'1.1.1',
|
||
true
|
||
);
|
||
|
||
wp_localize_script('itstudio-join-canvas', 'itstudioJoinData', itstudio_join_get_frontend_payload());
|
||
}
|
||
add_action('wp_enqueue_scripts', 'itstudio_join_enqueue_assets', 30);
|
||
|
||
function itstudio_join_register_settings() {
|
||
register_setting(
|
||
'itstudio_join_settings_group',
|
||
'itstudio_join_settings',
|
||
array(
|
||
'type' => 'array',
|
||
'sanitize_callback' => 'itstudio_join_sanitize_settings',
|
||
'default' => itstudio_join_get_default_settings(),
|
||
)
|
||
);
|
||
}
|
||
add_action('admin_init', 'itstudio_join_register_settings');
|
||
|
||
function itstudio_join_allow_result_file_mimes($mimes) {
|
||
$mimes = is_array($mimes) ? $mimes : array();
|
||
// 允许在媒体库上传招新结果文件。
|
||
$mimes['csv'] = 'text/csv';
|
||
$mimes['xlsx'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||
return $mimes;
|
||
}
|
||
add_filter('upload_mimes', 'itstudio_join_allow_result_file_mimes');
|
||
|
||
function itstudio_join_register_settings_page() {
|
||
add_options_page(
|
||
'招新设置',
|
||
'招新设置',
|
||
'manage_options',
|
||
'itstudio-join-settings',
|
||
'itstudio_join_render_settings_page'
|
||
);
|
||
}
|
||
add_action('admin_menu', 'itstudio_join_register_settings_page');
|
||
|
||
function itstudio_join_enqueue_settings_page_media($hook_suffix) {
|
||
if ($hook_suffix !== 'settings_page_itstudio-join-settings') {
|
||
return;
|
||
}
|
||
|
||
wp_enqueue_media();
|
||
}
|
||
add_action('admin_enqueue_scripts', 'itstudio_join_enqueue_settings_page_media');
|
||
|
||
/**
|
||
* 统一跳回招新设置页并带上提示参数。
|
||
*/
|
||
function itstudio_join_settings_redirect($args) {
|
||
$args = array_merge(array('page' => 'itstudio-join-settings'), $args);
|
||
wp_safe_redirect(add_query_arg($args, admin_url('options-general.php')));
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* 错误码转中文提示(供跳转后展示)。
|
||
*/
|
||
function itstudio_join_settings_error_message($code) {
|
||
$map = array(
|
||
'delete_active' => '无法删除“当前生效”学期,请先把其它学期设为生效。',
|
||
'delete_last' => '至少要保留一个学期。',
|
||
'delete_failed' => '删除失败,学期不存在。',
|
||
'save_failed' => '保存失败,学期不存在。',
|
||
'result_upload_failed' => '结果文件上传失败,请确认文件为 CSV 或 XLSX 后重试。',
|
||
'need_year' => '请填写学年或自定义名称。',
|
||
);
|
||
return isset($map[$code]) ? $map[$code] : '操作失败。';
|
||
}
|
||
|
||
function itstudio_join_handle_result_file_uploads($settings) {
|
||
$settings = is_array($settings) ? $settings : array();
|
||
|
||
foreach (itstudio_join_get_result_file_field_map() as $field_key) {
|
||
$upload_key = 'itstudio_join_upload_' . $field_key;
|
||
if (!isset($_FILES[$upload_key]) || !is_array($_FILES[$upload_key])) {
|
||
continue;
|
||
}
|
||
|
||
$upload_error = isset($_FILES[$upload_key]['error']) ? (int) $_FILES[$upload_key]['error'] : UPLOAD_ERR_NO_FILE;
|
||
if ($upload_error === UPLOAD_ERR_NO_FILE) {
|
||
continue;
|
||
}
|
||
if ($upload_error !== UPLOAD_ERR_OK) {
|
||
return new WP_Error('result_upload_failed');
|
||
}
|
||
|
||
$filename = isset($_FILES[$upload_key]['name']) ? sanitize_file_name(wp_unslash($_FILES[$upload_key]['name'])) : '';
|
||
$extension = strtolower((string) pathinfo($filename, PATHINFO_EXTENSION));
|
||
if (!in_array($extension, array('csv', 'xlsx'), true)) {
|
||
return new WP_Error('result_upload_failed');
|
||
}
|
||
|
||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||
|
||
$attachment_id = media_handle_upload($upload_key, 0);
|
||
if (is_wp_error($attachment_id)) {
|
||
return new WP_Error('result_upload_failed', $attachment_id->get_error_message());
|
||
}
|
||
|
||
$settings[$field_key] = absint($attachment_id);
|
||
}
|
||
|
||
return $settings;
|
||
}
|
||
|
||
/**
|
||
* 保存某学期的配置(含学期元信息)。
|
||
*/
|
||
function itstudio_join_handle_save_term_settings() {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('权限不足');
|
||
}
|
||
check_admin_referer('itstudio_join_save_term_settings');
|
||
|
||
$term_id = isset($_POST['itstudio_join_term_id']) ? sanitize_text_field(wp_unslash($_POST['itstudio_join_term_id'])) : '';
|
||
$raw = (isset($_POST['itstudio_join_settings']) && is_array($_POST['itstudio_join_settings'])) ? wp_unslash($_POST['itstudio_join_settings']) : array();
|
||
$meta = (isset($_POST['itstudio_join_term_meta']) && is_array($_POST['itstudio_join_term_meta'])) ? wp_unslash($_POST['itstudio_join_term_meta']) : array();
|
||
|
||
$raw = itstudio_join_handle_result_file_uploads($raw);
|
||
if (is_wp_error($raw)) {
|
||
itstudio_join_settings_redirect(array('term' => $term_id, 'error' => $raw->get_error_code()));
|
||
}
|
||
|
||
if (!empty($meta)) {
|
||
itstudio_join_update_term_meta(
|
||
$term_id,
|
||
isset($meta['academic_year']) ? $meta['academic_year'] : '',
|
||
isset($meta['season']) ? $meta['season'] : 'fall',
|
||
isset($meta['label']) ? $meta['label'] : ''
|
||
);
|
||
}
|
||
|
||
if (!itstudio_join_save_term_settings($term_id, $raw)) {
|
||
itstudio_join_settings_redirect(array('term' => $term_id, 'error' => 'save_failed'));
|
||
}
|
||
|
||
itstudio_join_settings_redirect(array('term' => $term_id, 'updated' => '1'));
|
||
}
|
||
add_action('admin_post_itstudio_join_save_term_settings', 'itstudio_join_handle_save_term_settings');
|
||
|
||
/**
|
||
* 新增学期。
|
||
*/
|
||
function itstudio_join_handle_create_term() {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('权限不足');
|
||
}
|
||
check_admin_referer('itstudio_join_create_term');
|
||
|
||
$year = isset($_POST['academic_year']) ? sanitize_text_field(wp_unslash($_POST['academic_year'])) : '';
|
||
$season = isset($_POST['season']) ? sanitize_text_field(wp_unslash($_POST['season'])) : 'fall';
|
||
$label = isset($_POST['label']) ? sanitize_text_field(wp_unslash($_POST['label'])) : '';
|
||
|
||
if (trim($year) === '' && trim($label) === '') {
|
||
itstudio_join_settings_redirect(array('error' => 'need_year'));
|
||
}
|
||
|
||
$id = itstudio_join_create_term($year, $season, $label, null);
|
||
itstudio_join_settings_redirect(array('term' => $id, 'created' => '1'));
|
||
}
|
||
add_action('admin_post_itstudio_join_create_term', 'itstudio_join_handle_create_term');
|
||
|
||
/**
|
||
* 把某学期设为当前生效。
|
||
*/
|
||
function itstudio_join_handle_activate_term() {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('权限不足');
|
||
}
|
||
check_admin_referer('itstudio_join_activate_term');
|
||
|
||
$term_id = isset($_POST['term_id']) ? sanitize_text_field(wp_unslash($_POST['term_id'])) : '';
|
||
itstudio_join_set_active_term($term_id);
|
||
itstudio_join_settings_redirect(array('term' => $term_id, 'activated' => '1'));
|
||
}
|
||
add_action('admin_post_itstudio_join_activate_term', 'itstudio_join_handle_activate_term');
|
||
|
||
/**
|
||
* 删除某学期(不能删当前生效或最后一个)。
|
||
*/
|
||
function itstudio_join_handle_delete_term() {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_die('权限不足');
|
||
}
|
||
check_admin_referer('itstudio_join_delete_term');
|
||
|
||
$term_id = isset($_POST['term_id']) ? sanitize_text_field(wp_unslash($_POST['term_id'])) : '';
|
||
$terms = itstudio_join_get_terms();
|
||
if (count($terms) <= 1) {
|
||
itstudio_join_settings_redirect(array('term' => $term_id, 'error' => 'delete_last'));
|
||
}
|
||
if (itstudio_join_get_active_term_id() === $term_id) {
|
||
itstudio_join_settings_redirect(array('term' => $term_id, 'error' => 'delete_active'));
|
||
}
|
||
if (!itstudio_join_delete_term($term_id)) {
|
||
itstudio_join_settings_redirect(array('term' => $term_id, 'error' => 'delete_failed'));
|
||
}
|
||
itstudio_join_settings_redirect(array('deleted' => '1'));
|
||
}
|
||
add_action('admin_post_itstudio_join_delete_term', 'itstudio_join_handle_delete_term');
|
||
|
||
function itstudio_join_render_photo_field_row($field_key, $label, $settings) {
|
||
$attachment_id = isset($settings[$field_key]) ? absint($settings[$field_key]) : 0;
|
||
$preview_url = $attachment_id > 0 ? wp_get_attachment_image_url($attachment_id, 'medium_large') : '';
|
||
?>
|
||
<tr>
|
||
<th scope="row"><label><?php echo esc_html($label); ?></label></th>
|
||
<td>
|
||
<input type="hidden" class="itstudio-join-photo-id" name="itstudio_join_settings[<?php echo esc_attr($field_key); ?>]" value="<?php echo esc_attr($attachment_id); ?>">
|
||
<div class="itstudio-join-photo-preview-wrap" style="margin-bottom:10px;">
|
||
<img class="itstudio-join-photo-preview" src="<?php echo esc_url($preview_url); ?>" alt="" style="max-width:300px;height:auto;border:1px solid #dcdcde;border-radius:8px;display:<?php echo $preview_url !== '' ? 'block' : 'none'; ?>;">
|
||
</div>
|
||
<button type="button" class="button itstudio-join-photo-upload"><?php esc_html_e('上传 / 选择图片', 'itstudio'); ?></button>
|
||
<button type="button" class="button-link-delete itstudio-join-photo-clear" style="margin-left:8px;<?php echo $preview_url !== '' ? '' : 'display:none;'; ?>"><?php esc_html_e('移除', 'itstudio'); ?></button>
|
||
</td>
|
||
</tr>
|
||
<?php
|
||
}
|
||
|
||
function itstudio_join_render_result_records_row($field_key, $label, $settings, $description = '') {
|
||
$value = isset($settings[$field_key]) ? (string) $settings[$field_key] : '';
|
||
?>
|
||
<tr>
|
||
<th scope="row"><label for="<?php echo esc_attr('itstudio_join_' . $field_key); ?>"><?php echo esc_html($label); ?></label></th>
|
||
<td>
|
||
<textarea
|
||
id="<?php echo esc_attr('itstudio_join_' . $field_key); ?>"
|
||
name="itstudio_join_settings[<?php echo esc_attr($field_key); ?>]"
|
||
rows="6"
|
||
class="large-text code"
|
||
placeholder="姓名,QQ,邮箱,学号"
|
||
><?php echo esc_textarea($value); ?></textarea>
|
||
<?php if ($description !== '') : ?>
|
||
<p class="description"><?php echo esc_html($description); ?></p>
|
||
<?php else : ?>
|
||
<p class="description">每行一条记录,格式:姓名,QQ,邮箱,学号。可用逗号、中文逗号、竖线或 Tab 分隔。</p>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php
|
||
}
|
||
|
||
function itstudio_join_render_text_setting_row($field_key, $label, $settings, $description = '', $placeholder = '') {
|
||
$value = isset($settings[$field_key]) ? (string) $settings[$field_key] : '';
|
||
?>
|
||
<tr>
|
||
<th scope="row"><label for="<?php echo esc_attr('itstudio_join_' . $field_key); ?>"><?php echo esc_html($label); ?></label></th>
|
||
<td>
|
||
<input
|
||
type="text"
|
||
id="<?php echo esc_attr('itstudio_join_' . $field_key); ?>"
|
||
name="itstudio_join_settings[<?php echo esc_attr($field_key); ?>]"
|
||
value="<?php echo esc_attr($value); ?>"
|
||
class="regular-text code"
|
||
<?php if ($placeholder !== '') : ?>placeholder="<?php echo esc_attr($placeholder); ?>"<?php endif; ?>
|
||
>
|
||
<?php if ($description !== '') : ?>
|
||
<p class="description"><?php echo esc_html($description); ?></p>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php
|
||
}
|
||
|
||
function itstudio_join_render_result_file_row($field_key, $label, $settings, $description = '') {
|
||
$attachment_id = isset($settings[$field_key]) ? absint($settings[$field_key]) : 0;
|
||
$file_url = $attachment_id > 0 ? wp_get_attachment_url($attachment_id) : '';
|
||
$filename = $attachment_id > 0 ? basename((string) get_attached_file($attachment_id)) : '';
|
||
static $result_files = null;
|
||
if ($result_files === null) {
|
||
$result_files = get_posts(array(
|
||
'post_type' => 'attachment',
|
||
'post_status' => 'inherit',
|
||
'post_mime_type' => array(
|
||
'text/csv',
|
||
'application/csv',
|
||
'application/vnd.ms-excel',
|
||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
),
|
||
'posts_per_page' => 100,
|
||
'orderby' => 'date',
|
||
'order' => 'DESC',
|
||
));
|
||
}
|
||
if ($attachment_id > 0 && !in_array($attachment_id, wp_list_pluck($result_files, 'ID'), true)) {
|
||
$current_file = get_post($attachment_id);
|
||
if ($current_file instanceof WP_Post && $current_file->post_type === 'attachment') {
|
||
$result_files[] = $current_file;
|
||
}
|
||
}
|
||
?>
|
||
<tr>
|
||
<th scope="row"><label><?php echo esc_html($label); ?></label></th>
|
||
<td>
|
||
<div class="itstudio-join-result-file-preview" style="margin-bottom:10px;<?php echo $file_url !== '' ? '' : 'display:none;'; ?>">
|
||
<a class="itstudio-join-result-file-link" href="<?php echo esc_url($file_url); ?>" target="_blank" rel="noopener"><?php echo esc_html($filename !== '' ? $filename : $file_url); ?></a>
|
||
</div>
|
||
<select class="itstudio-join-result-file-id" name="itstudio_join_settings[<?php echo esc_attr($field_key); ?>]">
|
||
<option value="0">不选择已有文件</option>
|
||
<?php foreach ($result_files as $result_file) : ?>
|
||
<?php
|
||
$result_file_id = absint($result_file->ID);
|
||
$result_file_url = wp_get_attachment_url($result_file_id);
|
||
$result_file_name = basename((string) get_attached_file($result_file_id));
|
||
?>
|
||
<option
|
||
value="<?php echo esc_attr($result_file_id); ?>"
|
||
data-url="<?php echo esc_url($result_file_url); ?>"
|
||
data-filename="<?php echo esc_attr($result_file_name); ?>"
|
||
<?php selected($attachment_id, $result_file_id); ?>
|
||
><?php echo esc_html($result_file_name !== '' ? $result_file_name : $result_file->post_title); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<input
|
||
type="file"
|
||
class="itstudio-join-result-file-upload"
|
||
name="<?php echo esc_attr('itstudio_join_upload_' . $field_key); ?>"
|
||
accept=".csv,.xlsx,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||
style="display:block;margin-top:8px;"
|
||
>
|
||
<button type="button" class="button-link-delete itstudio-join-result-file-clear" style="margin-left:8px;<?php echo $file_url !== '' ? '' : 'display:none;'; ?>"><?php esc_html_e('移除', 'itstudio'); ?></button>
|
||
<?php if ($description !== '') : ?>
|
||
<p class="description"><?php echo esc_html($description); ?></p>
|
||
<?php else : ?>
|
||
<p class="description">支持 CSV / XLSX。列结构:姓名,QQ,邮箱,学号,手机,是否通过(1=通过,其他=未通过)。</p>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php
|
||
}
|
||
|
||
function itstudio_join_render_settings_page() {
|
||
if (!current_user_can('manage_options')) {
|
||
return;
|
||
}
|
||
|
||
$terms = itstudio_join_get_terms();
|
||
$active_id = itstudio_join_get_active_term_id();
|
||
$editing_term_id = isset($_GET['term']) ? sanitize_text_field(wp_unslash($_GET['term'])) : '';
|
||
if ($editing_term_id === '' || !isset($terms[$editing_term_id])) {
|
||
$editing_term_id = $active_id;
|
||
}
|
||
$editing_term = isset($terms[$editing_term_id]) ? $terms[$editing_term_id] : null;
|
||
$is_editing_active = ($editing_term_id === $active_id);
|
||
$seasons = itstudio_join_get_seasons();
|
||
|
||
$settings = itstudio_join_get_settings($editing_term_id);
|
||
$runtime = itstudio_join_get_runtime_data();
|
||
$formidable_active = shortcode_exists('formidable') || class_exists('FrmFormsController');
|
||
$smtp_active = class_exists('\WPMailSMTP\WP') || defined('WPMS_PLUGIN_VER');
|
||
$admin_post_url = admin_url('admin-post.php');
|
||
?>
|
||
<div class="wrap">
|
||
<h1>爱特工作室招新设置</h1>
|
||
<p>用于配置「加入我们」页面的时间节点、阶段结果和表单。前台始终展示<strong>当前生效学期</strong>。</p>
|
||
|
||
<?php if (isset($_GET['updated'])) : ?>
|
||
<div class="notice notice-success is-dismissible"><p>已保存。</p></div>
|
||
<?php endif; ?>
|
||
<?php if (isset($_GET['created'])) : ?>
|
||
<div class="notice notice-success is-dismissible"><p>已新增学期。</p></div>
|
||
<?php endif; ?>
|
||
<?php if (isset($_GET['activated'])) : ?>
|
||
<div class="notice notice-success is-dismissible"><p>已设为当前生效学期。</p></div>
|
||
<?php endif; ?>
|
||
<?php if (isset($_GET['deleted'])) : ?>
|
||
<div class="notice notice-success is-dismissible"><p>已删除学期。</p></div>
|
||
<?php endif; ?>
|
||
<?php if (isset($_GET['error'])) : ?>
|
||
<div class="notice notice-error is-dismissible"><p><?php echo esc_html(itstudio_join_settings_error_message(sanitize_text_field(wp_unslash($_GET['error'])))); ?></p></div>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!$formidable_active) : ?>
|
||
<div class="notice notice-warning inline">
|
||
<p><strong>提示:</strong>未检测到 Formidable Forms 插件。报名表单将无法在前台渲染。</p>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!$smtp_active) : ?>
|
||
<div class="notice notice-warning inline">
|
||
<p><strong>提示:</strong>未检测到 WP Mail SMTP 插件。建议启用后再开放报名邮件通知。</p>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div style="background:#fff;border:1px solid #c3c4c7;border-radius:6px;padding:12px 16px;margin:16px 0;">
|
||
<h2 style="margin-top:0;">招新学期</h2>
|
||
<form method="get" action="<?php echo esc_url(admin_url('options-general.php')); ?>" style="display:inline-block;margin-right:16px;">
|
||
<input type="hidden" name="page" value="itstudio-join-settings">
|
||
<label for="itstudio_join_term_switch">正在编辑:</label>
|
||
<select name="term" id="itstudio_join_term_switch" onchange="this.form.submit()">
|
||
<?php foreach ($terms as $tid => $t) : ?>
|
||
<option value="<?php echo esc_attr($tid); ?>" <?php selected($tid, $editing_term_id); ?>>
|
||
<?php echo esc_html(itstudio_join_term_label($t)); ?><?php echo $tid === $active_id ? '(当前生效)' : ''; ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<noscript><button type="submit" class="button">切换</button></noscript>
|
||
</form>
|
||
|
||
<?php if (!$is_editing_active) : ?>
|
||
<form method="post" action="<?php echo esc_url($admin_post_url); ?>" style="display:inline-block;margin-right:8px;">
|
||
<input type="hidden" name="action" value="itstudio_join_activate_term">
|
||
<input type="hidden" name="term_id" value="<?php echo esc_attr($editing_term_id); ?>">
|
||
<?php wp_nonce_field('itstudio_join_activate_term'); ?>
|
||
<button type="submit" class="button button-primary">将此学期设为当前生效</button>
|
||
</form>
|
||
<?php if (count($terms) > 1) : ?>
|
||
<form method="post" action="<?php echo esc_url($admin_post_url); ?>" style="display:inline-block;" onsubmit="return confirm('确定删除该学期?此操作不可恢复。');">
|
||
<input type="hidden" name="action" value="itstudio_join_delete_term">
|
||
<input type="hidden" name="term_id" value="<?php echo esc_attr($editing_term_id); ?>">
|
||
<?php wp_nonce_field('itstudio_join_delete_term'); ?>
|
||
<button type="submit" class="button-link-delete">删除此学期</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php else : ?>
|
||
<span style="color:#008a20;font-weight:600;">✔ 这是当前生效学期(前台展示的就是它)</span>
|
||
<?php endif; ?>
|
||
|
||
<hr style="margin:12px 0;">
|
||
<form method="post" action="<?php echo esc_url($admin_post_url); ?>" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
||
<input type="hidden" name="action" value="itstudio_join_create_term">
|
||
<?php wp_nonce_field('itstudio_join_create_term'); ?>
|
||
<label>新增学期 · 学年<br><input type="text" name="academic_year" placeholder="2026-2027" style="width:140px;"></label>
|
||
<label>季节<br>
|
||
<select name="season">
|
||
<?php foreach ($seasons as $sk => $sv) : ?>
|
||
<option value="<?php echo esc_attr($sk); ?>"><?php echo esc_html($sv); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</label>
|
||
<label>自定义名称(可选)<br><input type="text" name="label" placeholder="如:秋季补招" style="width:160px;"></label>
|
||
<button type="submit" class="button">新增学期</button>
|
||
</form>
|
||
</div>
|
||
|
||
<form method="post" action="<?php echo esc_url($admin_post_url); ?>" enctype="multipart/form-data">
|
||
<input type="hidden" name="action" value="itstudio_join_save_term_settings">
|
||
<input type="hidden" name="itstudio_join_term_id" value="<?php echo esc_attr($editing_term_id); ?>">
|
||
<?php wp_nonce_field('itstudio_join_save_term_settings'); ?>
|
||
<h2 style="margin-top:20px;">学期信息(正在编辑:<?php echo esc_html($editing_term ? itstudio_join_term_label($editing_term) : ''); ?>)</h2>
|
||
<table class="form-table" role="presentation">
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_meta_year">学年</label></th>
|
||
<td><input type="text" id="itstudio_join_meta_year" name="itstudio_join_term_meta[academic_year]" value="<?php echo esc_attr((string) ($editing_term['academic_year'] ?? '')); ?>" placeholder="2025-2026" style="width:160px;"></td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_meta_season">季节</label></th>
|
||
<td>
|
||
<select id="itstudio_join_meta_season" name="itstudio_join_term_meta[season]">
|
||
<?php foreach ($seasons as $sk => $sv) : ?>
|
||
<option value="<?php echo esc_attr($sk); ?>" <?php selected($sk, (string) ($editing_term['season'] ?? 'fall')); ?>><?php echo esc_html($sv); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_meta_label">自定义名称</label></th>
|
||
<td><input type="text" id="itstudio_join_meta_label" name="itstudio_join_term_meta[label]" value="<?php echo esc_attr((string) ($editing_term['label'] ?? '')); ?>" placeholder="留空则用「学年 + 季节招新」" class="regular-text"></td>
|
||
</tr>
|
||
</table>
|
||
|
||
<h2>时间与结果配置</h2>
|
||
<table class="form-table" role="presentation">
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_registration_start">报名开始时间</label></th>
|
||
<td>
|
||
<input type="datetime-local" id="itstudio_join_registration_start" name="itstudio_join_settings[registration_start]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['registration_start'], false)); ?>" class="regular-text">
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_registration_end">报名结束时间</label></th>
|
||
<td>
|
||
<input type="datetime-local" id="itstudio_join_registration_end" name="itstudio_join_settings[registration_end]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['registration_end'], true)); ?>" class="regular-text">
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_first_interview_day">第一次面试日期与时间</label></th>
|
||
<td>
|
||
<fieldset style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;">
|
||
<label>日期<br><input type="date" id="itstudio_join_first_interview_day" name="itstudio_join_settings[first_interview_day]" value="<?php echo esc_attr(itstudio_join_to_date_input_value((string) $settings['first_interview_date'])); ?>"></label>
|
||
<label>开始时间<br><input type="time" name="itstudio_join_settings[first_interview_start_time]" value="<?php echo esc_attr(itstudio_join_to_time_input_value((string) $settings['first_interview_date'], false)); ?>"></label>
|
||
<label>结束时间<br><input type="time" name="itstudio_join_settings[first_interview_end_time]" value="<?php echo esc_attr(itstudio_join_to_time_input_value((string) $settings['first_interview_end'], true)); ?>"></label>
|
||
</fieldset>
|
||
<p class="description">面试按同一天计算,请依次设置日期、开始时间和结束时间。</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_first_interview_location_cn">第一次面试地点(中文)</label></th>
|
||
<td>
|
||
<input type="text" id="itstudio_join_first_interview_location_cn" name="itstudio_join_settings[first_interview_location_cn]" value="<?php echo esc_attr((string) ($settings['first_interview_location_cn'] ?? '')); ?>" class="regular-text">
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_first_interview_location_en">第一次面试地点(英文)</label></th>
|
||
<td>
|
||
<input type="text" id="itstudio_join_first_interview_location_en" name="itstudio_join_settings[first_interview_location_en]" value="<?php echo esc_attr((string) ($settings['first_interview_location_en'] ?? '')); ?>" class="regular-text">
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_second_interview_day">第二次面试日期与时间</label></th>
|
||
<td>
|
||
<fieldset style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;">
|
||
<label>日期<br><input type="date" id="itstudio_join_second_interview_day" name="itstudio_join_settings[second_interview_day]" value="<?php echo esc_attr(itstudio_join_to_date_input_value((string) $settings['second_interview_date'])); ?>"></label>
|
||
<label>开始时间<br><input type="time" name="itstudio_join_settings[second_interview_start_time]" value="<?php echo esc_attr(itstudio_join_to_time_input_value((string) $settings['second_interview_date'], false)); ?>"></label>
|
||
<label>结束时间<br><input type="time" name="itstudio_join_settings[second_interview_end_time]" value="<?php echo esc_attr(itstudio_join_to_time_input_value((string) $settings['second_interview_end'], true)); ?>"></label>
|
||
</fieldset>
|
||
<p class="description">面试按同一天计算,请依次设置日期、开始时间和结束时间。</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_second_interview_location_cn">第二次面试地点(中文)</label></th>
|
||
<td>
|
||
<input type="text" id="itstudio_join_second_interview_location_cn" name="itstudio_join_settings[second_interview_location_cn]" value="<?php echo esc_attr((string) ($settings['second_interview_location_cn'] ?? '')); ?>" class="regular-text">
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_second_interview_location_en">第二次面试地点(英文)</label></th>
|
||
<td>
|
||
<input type="text" id="itstudio_join_second_interview_location_en" name="itstudio_join_settings[second_interview_location_en]" value="<?php echo esc_attr((string) ($settings['second_interview_location_en'] ?? '')); ?>" class="regular-text">
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_assessment_start_date">国庆能力摸底开始日期(调试)</label></th>
|
||
<td>
|
||
<input type="date" id="itstudio_join_assessment_start_date" name="itstudio_join_settings[assessment_start_date]" value="<?php echo esc_attr(itstudio_join_to_date_input_value((string) ($settings['assessment_start_date'] ?? ''))); ?>" class="regular-text">
|
||
<p class="description">留空时默认使用每年 10 月 1 日。</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_assessment_end_date">国庆能力摸底结束日期(调试)</label></th>
|
||
<td>
|
||
<input type="date" id="itstudio_join_assessment_end_date" name="itstudio_join_settings[assessment_end_date]" value="<?php echo esc_attr(itstudio_join_to_date_input_value((string) ($settings['assessment_end_date'] ?? ''))); ?>" class="regular-text">
|
||
<p class="description">留空时默认使用每年 10 月 7 日。</p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_notice_start_date">录取结果公布开始日期</label></th>
|
||
<td>
|
||
<input type="date" id="itstudio_join_notice_start_date" name="itstudio_join_settings[notice_start_date]" value="<?php echo esc_attr(itstudio_join_to_date_input_value((string) $settings['notice_start_date'])); ?>" class="regular-text">
|
||
<p class="description">公布会自动持续 7 天(开始日 + 后续 6 天)。</p>
|
||
</td>
|
||
</tr>
|
||
<?php itstudio_join_render_photo_field_row('photo_registration', '报名阶段图片', $settings); ?>
|
||
<?php itstudio_join_render_photo_field_row('photo_first_interview', '第一次面试图片', $settings); ?>
|
||
<?php itstudio_join_render_photo_field_row('photo_assessment', '国庆能力摸底图片', $settings); ?>
|
||
<?php itstudio_join_render_photo_field_row('photo_second_interview', '第二次面试图片', $settings); ?>
|
||
<?php itstudio_join_render_photo_field_row('photo_public_notice', '录取结果公布阶段图片', $settings); ?>
|
||
<?php itstudio_join_render_photo_field_row('photo_inactive', '非招新时段图片', $settings); ?>
|
||
<tr>
|
||
<th scope="row"><label>结果数据来源</label></th>
|
||
<td>
|
||
<input type="hidden" name="itstudio_join_settings[result_data_source]" value="file">
|
||
<strong>CSV / XLSX 文件上传</strong>
|
||
<p class="description">上传第一次面试、国庆能力摸底、第二次面试、录取结果四个阶段文件,系统按“姓名,QQ,邮箱,学号,手机,是否通过”进行查询。最后一列为 1 视为通过,否则未通过。</p>
|
||
</td>
|
||
</tr>
|
||
<?php itstudio_join_render_result_file_row('result_first_interview_file', '第一次面试结果文件', $settings); ?>
|
||
<?php itstudio_join_render_result_file_row('result_assessment_file', '国庆能力摸底结果文件', $settings); ?>
|
||
<?php itstudio_join_render_result_file_row('result_second_interview_file', '第二次面试结果文件', $settings); ?>
|
||
<?php itstudio_join_render_result_file_row('result_admission_file', '录取结果文件', $settings); ?>
|
||
<tr>
|
||
<th scope="row"><label for="itstudio_join_signup_shortcode">报名表单 Shortcode</label></th>
|
||
<td>
|
||
<input type="text" id="itstudio_join_signup_shortcode" name="itstudio_join_settings[signup_form_shortcode]" value="<?php echo esc_attr((string) $settings['signup_form_shortcode']); ?>" class="regular-text code">
|
||
<p class="description">示例:<code>[formidable id="12"]</code></p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th scope="row">邮箱 OTP 验证</th>
|
||
<td>
|
||
<label for="itstudio_join_otp_enabled">
|
||
<input type="checkbox" id="itstudio_join_otp_enabled" name="itstudio_join_settings[otp_enabled]" value="1" <?php checked(!empty($settings['otp_enabled'])); ?>>
|
||
在报名表单提交前启用邮箱验证码
|
||
</label>
|
||
<p class="description">启用后,前台会在指定验证码字段旁自动显示“发送验证码”按钮,不需要手动添加 HTML 按钮。</p>
|
||
<p style="margin:10px 0 4px;"><label for="itstudio_join_otp_form_id">Formidable 表单 ID</label></p>
|
||
<input type="number" min="1" step="1" id="itstudio_join_otp_form_id" name="itstudio_join_settings[otp_form_id]" value="<?php echo esc_attr((string) ($settings['otp_form_id'] ?? '')); ?>" class="small-text">
|
||
<p style="margin:10px 0 4px;"><label for="itstudio_join_otp_email_field_id">邮箱字段 ID</label></p>
|
||
<input type="number" min="1" step="1" id="itstudio_join_otp_email_field_id" name="itstudio_join_settings[otp_email_field_id]" value="<?php echo esc_attr((string) ($settings['otp_email_field_id'] ?? '')); ?>" class="small-text">
|
||
<p style="margin:10px 0 4px;"><label for="itstudio_join_otp_code_field_id">验证码字段 ID</label></p>
|
||
<input type="number" min="1" step="1" id="itstudio_join_otp_code_field_id" name="itstudio_join_settings[otp_code_field_id]" value="<?php echo esc_attr((string) ($settings['otp_code_field_id'] ?? '')); ?>" class="small-text">
|
||
<p class="description">验证码字段建议使用 Short Text,设置为仅允许数字、最大长度 6。邮箱字段必须是 Email 类型。验证码有效期 10 分钟,同一邮箱 60 秒内不能重复发送,最多尝试 5 次。</p>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<?php submit_button('保存设置'); ?>
|
||
</form>
|
||
|
||
<div class="notice notice-info" style="padding:12px 14px; margin: 16px 0;">
|
||
<h2 style="margin:0 0 8px;">阶段结果文件使用说明</h2>
|
||
<ol style="margin:0 0 0 18px;">
|
||
<li>上传四个阶段结果文件:第一次面试、国庆能力摸底、第二次面试、录取结果(报名阶段无需结果文件)。</li>
|
||
<li>文件列顺序固定为:<code>姓名,QQ,邮箱,学号,手机,是否通过</code>。</li>
|
||
<li><strong>是否通过</strong> 字段:<code>1</code> 表示通过;其他任意值(含 <code>0</code>、空)都按未通过处理。</li>
|
||
<li>前台用户可使用姓名、QQ、邮箱、学号进行查询,系统会按当前阶段自动返回对应进度。</li>
|
||
</ol>
|
||
</div>
|
||
<hr>
|
||
<h2>阶段预览</h2>
|
||
<p>国庆能力摸底默认固定为每年 10 月 1 日至 10 月 7 日;如填写上方“摸底开始/结束日期(调试)”则优先使用调试时间,留空则恢复默认固定窗口。</p>
|
||
<table class="widefat striped">
|
||
<thead>
|
||
<tr>
|
||
<th>阶段</th>
|
||
<th>时间</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ((array) ($runtime['stages'] ?? array()) as $stage) : ?>
|
||
<?php
|
||
$status = (string) ($stage['status'] ?? 'pending');
|
||
$status_label = '待设置';
|
||
if ($status === 'completed') {
|
||
$status_label = '已完成';
|
||
} elseif ($status === 'active') {
|
||
$status_label = '进行中';
|
||
} elseif ($status === 'upcoming') {
|
||
$status_label = '未开始';
|
||
}
|
||
?>
|
||
<tr>
|
||
<td><?php echo esc_html((string) ($stage['label_cn'] ?? '')); ?></td>
|
||
<td><?php echo esc_html((string) ($stage['range_cn'] ?? '')); ?></td>
|
||
<td><?php echo esc_html($status_label); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<script>
|
||
(function () {
|
||
const uploadButtons = document.querySelectorAll('.itstudio-join-photo-upload');
|
||
if (!uploadButtons.length) {
|
||
return;
|
||
}
|
||
|
||
uploadButtons.forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
if (!window.wp || !wp.media) {
|
||
return;
|
||
}
|
||
|
||
const row = button.closest('td');
|
||
if (!row) {
|
||
return;
|
||
}
|
||
|
||
const input = row.querySelector('.itstudio-join-photo-id');
|
||
const preview = row.querySelector('.itstudio-join-photo-preview');
|
||
const clearBtn = row.querySelector('.itstudio-join-photo-clear');
|
||
if (!input || !preview) {
|
||
return;
|
||
}
|
||
|
||
const frame = wp.media({
|
||
title: '选择阶段图片',
|
||
button: { text: '使用此图片' },
|
||
multiple: false,
|
||
library: { type: 'image' },
|
||
});
|
||
|
||
frame.on('select', () => {
|
||
const selection = frame.state().get('selection').first();
|
||
if (!selection) {
|
||
return;
|
||
}
|
||
const data = selection.toJSON();
|
||
const imageUrl = (data.sizes && data.sizes.medium_large && data.sizes.medium_large.url)
|
||
? data.sizes.medium_large.url
|
||
: data.url;
|
||
input.value = data.id || '';
|
||
preview.src = imageUrl || '';
|
||
preview.style.display = imageUrl ? 'block' : 'none';
|
||
if (clearBtn) {
|
||
clearBtn.style.display = imageUrl ? 'inline' : 'none';
|
||
}
|
||
});
|
||
|
||
frame.open();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.itstudio-join-photo-clear').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
const row = button.closest('td');
|
||
if (!row) {
|
||
return;
|
||
}
|
||
const input = row.querySelector('.itstudio-join-photo-id');
|
||
const preview = row.querySelector('.itstudio-join-photo-preview');
|
||
if (input) {
|
||
input.value = '';
|
||
}
|
||
if (preview) {
|
||
preview.src = '';
|
||
preview.style.display = 'none';
|
||
}
|
||
button.style.display = 'none';
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.itstudio-join-result-file-id').forEach((select) => {
|
||
select.addEventListener('change', () => {
|
||
const row = select.closest('td');
|
||
const option = select.options[select.selectedIndex];
|
||
const preview = row ? row.querySelector('.itstudio-join-result-file-preview') : null;
|
||
const link = row ? row.querySelector('.itstudio-join-result-file-link') : null;
|
||
const clearBtn = row ? row.querySelector('.itstudio-join-result-file-clear') : null;
|
||
const upload = row ? row.querySelector('.itstudio-join-result-file-upload') : null;
|
||
const fileUrl = option ? option.dataset.url || '' : '';
|
||
const filename = option ? option.dataset.filename || '' : '';
|
||
if (upload) {
|
||
upload.value = '';
|
||
}
|
||
if (preview && link) {
|
||
link.href = fileUrl || '#';
|
||
link.textContent = filename || fileUrl;
|
||
preview.style.display = fileUrl ? 'block' : 'none';
|
||
}
|
||
if (clearBtn) {
|
||
clearBtn.style.display = fileUrl ? 'inline' : 'none';
|
||
}
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.itstudio-join-result-file-clear').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
const row = button.closest('td');
|
||
if (!row) {
|
||
return;
|
||
}
|
||
const input = row.querySelector('.itstudio-join-result-file-id');
|
||
const upload = row.querySelector('.itstudio-join-result-file-upload');
|
||
const preview = row.querySelector('.itstudio-join-result-file-preview');
|
||
const link = row.querySelector('.itstudio-join-result-file-link');
|
||
if (input) {
|
||
input.value = '0';
|
||
}
|
||
if (upload) {
|
||
upload.value = '';
|
||
}
|
||
if (link) {
|
||
link.href = '#';
|
||
link.textContent = '';
|
||
}
|
||
if (preview) {
|
||
preview.style.display = 'none';
|
||
}
|
||
button.style.display = 'none';
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
</div>
|
||
<?php
|
||
}
|