Files
ITStudioMainSite/plugins/itstudio-recruitment/includes/join-system.php
T

2709 lines
116 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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_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') : '';
?>
<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_date">第一次面试开始时间</label></th>
<td>
<input type="datetime-local" id="itstudio_join_first_interview_date" name="itstudio_join_settings[first_interview_date]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['first_interview_date'], false)); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row"><label for="itstudio_join_first_interview_end">第一次面试结束时间</label></th>
<td>
<input type="datetime-local" id="itstudio_join_first_interview_end" name="itstudio_join_settings[first_interview_end]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['first_interview_end'], true)); ?>" class="regular-text">
</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_date">第二次面试开始时间</label></th>
<td>
<input type="datetime-local" id="itstudio_join_second_interview_date" name="itstudio_join_settings[second_interview_date]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['second_interview_date'], false)); ?>" class="regular-text">
</td>
</tr>
<tr>
<th scope="row"><label for="itstudio_join_second_interview_end">第二次面试结束时间</label></th>
<td>
<input type="datetime-local" id="itstudio_join_second_interview_end" name="itstudio_join_settings[second_interview_end]" value="<?php echo esc_attr(itstudio_join_to_datetime_local_input_value((string) $settings['second_interview_end'], true)); ?>" class="regular-text">
</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
}