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_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(); $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' => itstudio_join_sanitize_datetime_local_value($input['first_interview_date'] ?? ''), 'first_interview_end' => itstudio_join_sanitize_datetime_local_value($input['first_interview_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' => itstudio_join_sanitize_datetime_local_value($input['second_interview_date'] ?? ''), 'second_interview_end' => itstudio_join_sanitize_datetime_local_value($input['second_interview_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_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', ); } $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_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'])); $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'], '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') : ''; ?>
每行一条记录,格式:姓名,QQ,邮箱,学号。可用逗号、中文逗号、竖线或 Tab 分隔。
支持 CSV / XLSX。列结构:姓名,QQ,邮箱,学号,手机,是否通过(1=通过,其他=未通过)。
用于配置「加入我们」页面的时间节点、阶段结果和表单。前台始终展示当前生效学期。
已保存。
已新增学期。
已设为当前生效学期。
已删除学期。
提示:未检测到 Formidable Forms 插件。报名表单将无法在前台渲染。
提示:未检测到 WP Mail SMTP 插件。建议启用后再开放报名邮件通知。
姓名,QQ,邮箱,学号,手机,是否通过。1 表示通过;其他任意值(含 0、空)都按未通过处理。国庆能力摸底默认固定为每年 10 月 1 日至 10 月 7 日;如填写上方“摸底开始/结束日期(调试)”则优先使用调试时间,留空则恢复默认固定窗口。
| 阶段 | 时间 | 状态 |
|---|---|---|