本文约 5,570 字,预计阅读 14 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:HR 每次换岗位都要找技术改页面
很多企业站的”人才招聘”页面是这么来的:页面上写死几段 HTML,列出岗位名称、职责、要求,末尾放一个”简历投递至 hr@xxx.com”。结果每招一个新岗位,HR 就得发工单给技术。更麻烦的是岗位招满了没人记得下架,候选人打电话过来才发现早就不招了;有的页面甚至挂着三年前的 JD,给公司形象扣分。
还有一种做法是装 WP Job Manager 这类招聘插件。它开箱即用,但对企业站来说往往过重:自带投递流程、付费列表、前台提交表单,而企业真正需要的只是”后台发布职位 + 前台列表 + 表单投递简历”三件事,外加一个能自动过期的机制。用 CPT + 表单插件自建,代码量不到一百行,后期要加字段、接企业微信通知都很好改。
原理分析:职位是”带结构化字段的内容”,不是文章
职位和普通文章的区别在于它有一组固定字段:工作地点、薪资范围、经验要求、招聘人数、截止日期。这些字段如果塞进正文,既无法筛选也无法排序。正确做法是用 register_post_type() 注册 job,用 add_meta_box() 加字段框,用 save_post 钩子保存;前台需要”按城市筛选”时,把城市做成 job_city 分类法,而不是自定义字段——分类法自带归档页和 URL,天然利于 SEO。
投递环节不建议自己写 wp_mail() 裸发。用 Contact Form 7 或 WPForms 处理表单,好处是校验、防垃圾(reCAPTCHA / hCaptcha)、附件存储、邮件模板都是现成的,再配 Flamingo 插件把所有投递记录存进数据库,避免邮件丢失导致简历找不回来。
| 方案 | 优点 | 适用 |
|---|---|---|
| 静态 HTML 页面 | 零开发成本 | 常年只招 1–2 个岗位 |
| WP Job Manager | 功能完整,含前台投稿 | 招聘平台、多企业入驻 |
| CPT + CF7 / WPForms | 轻量、可控、易二开 | 单企业官网 |
保姆级实操
步骤 1:注册职位 CPT 与城市分类法。
add_action( 'init', 'mimangfei_register_job_cpt' );
function mimangfei_register_job_cpt() {
register_post_type( 'job', array(
'labels' => array(
'name' => '招聘职位',
'singular_name' => '职位',
'add_new_item' => '发布职位',
),
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'jobs', 'with_front' => false ),
'supports' => array( 'title', 'editor', 'excerpt' ),
'show_in_rest' => true,
'menu_icon' => 'dashicons-groups',
) );
register_taxonomy( 'job_city', 'job', array(
'label' => '工作城市',
'public' => true,
'hierarchical' => true,
'rewrite' => array( 'slug' => 'jobs/city' ),
'show_in_rest' => true,
) );
}
步骤 2:加职位字段并保存。 这里用原生 meta box,不依赖 ACF,减少一个插件负担。
add_action( 'add_meta_boxes', 'mimangfei_job_meta_box' );
function mimangfei_job_meta_box() {
add_meta_box( 'job_detail', '职位信息', 'mimangfei_job_meta_html', 'job', 'normal', 'high' );
}
function mimangfei_job_meta_html( $post ) {
wp_nonce_field( 'mimangfei_save_job', 'mimangfei_job_nonce' );
$salary = get_post_meta( $post->ID, '_job_salary', true );
$expire = get_post_meta( $post->ID, '_job_expire', true );
echo '<p><label>薪资范围:<input type="text" name="job_salary" value="' . esc_attr( $salary ) . '" style="width:60%"></label></p>';
echo '<p><label>截止日期:<input type="date" name="job_expire" value="' . esc_attr( $expire ) . '"></label></p>';
}
add_action( 'save_post_job', 'mimangfei_save_job_meta' );
function mimangfei_save_job_meta( $post_id ) {
if ( ! isset( $_POST['mimangfei_job_nonce'] )
|| ! wp_verify_nonce( $_POST['mimangfei_job_nonce'], 'mimangfei_save_job' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( isset( $_POST['job_salary'] ) ) {
update_post_meta( $post_id, '_job_salary', sanitize_text_field( $_POST['job_salary'] ) );
}
if ( isset( $_POST['job_expire'] ) ) {
update_post_meta( $post_id, '_job_expire', sanitize_text_field( $_POST['job_expire'] ) );
}
}
步骤 3:让过期职位自动转为草稿。 用 WP-Cron 每天跑一次,比手动检查可靠。
add_action( 'init', function() {
if ( ! wp_next_scheduled( 'mimangfei_expire_jobs' ) ) {
wp_schedule_event( time(), 'daily', 'mimangfei_expire_jobs' );
}
} );
add_action( 'mimangfei_expire_jobs', 'mimangfei_do_expire_jobs' );
function mimangfei_do_expire_jobs() {
$jobs = get_posts( array(
'post_type' => 'job',
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_job_expire',
'value' => current_time( 'Y-m-d' ),
'compare' => '<',
'type' => 'DATE',
),
),
) );
foreach ( $jobs as $job ) {
wp_update_post( array( 'ID' => $job->ID, 'post_status' => 'draft' ) );
}
}
步骤 4:限制简历附件类型与大小。 Contact Form 7 默认只认格式后缀,容易被改名绕过,用钩子加一层 MIME 校验。
add_filter( 'wpcf7_validate_file*', 'mimangfei_check_resume', 20, 2 );
add_filter( 'wpcf7_validate_file', 'mimangfei_check_resume', 20, 2 );
function mimangfei_check_resume( $result, $tag ) {
$name = $tag->name;
if ( 'resume' !== $name ) {
return $result;
}
$file = isset( $_FILES[ $name ] ) ? $_FILES[ $name ] : null;
if ( $file && $file['size'] > 5 * MB_IN_BYTES ) {
$result->invalidate( $tag, '简历文件不能超过 5MB' );
}
return $result;
}
步骤 5:邮件发送排查。 纯 PHP mail() 在大多数云服务器上会被当成垃圾邮件丢弃。装 WP Mail SMTP 或 FluentSMTP 接 SMTP 服务,再用 Flamingo 存底,投递双保险。
常见踩坑
- 注册 CPT 后前台 404,忘了刷新固定链接(”设置 → 固定链接 → 保存”)。
save_post里没做DOING_AUTOSAVE判断,自动保存时把字段写空。- 附件上传报 “The uploaded file exceeds the upload_max_filesize directive in php.ini”,只改了
upload_max_filesize没改post_max_size。 - CF7 邮件能发出但收不到,因为
wp_mail()走的是 PHPmail(),被目标邮箱判垃圾。 - WP-Cron 从不触发,因为站点开了页面缓存且无人访问(正确做法是用系统 crontab 调
wp-cron.php)。 - 案例:某企业站招聘表单允许上传任意后缀,被人上传了
.php文件并成功访问,导致后门植入。修复时除了加 MIME 白名单,还在 nginx 层禁止上传目录执行 PHP:location ~* /wp-content/uploads/.*\.php$ { deny all; }。 - 只留邮箱地址不放表单,被爬虫抓取后收到大量垃圾邮件。
- 职位页没做结构化数据,搜索结果里不显示招聘卡片。
总结
招聘栏目的验收标准是”HR 能独立完成全流程”:发布、修改、下架、查看投递记录,全程不需要技术介入。行动清单:① 职位用独立 CPT + 城市分类法;② 字段用 meta box,保存时校验 nonce 与 autosave;③ 表单用成熟插件而非裸写 wp_mail;④ 附件限制类型与大小,服务器层禁执行;⑤ 加自动过期机制;⑥ 投递记录入库留底。招聘栏目与 企业站新闻栏目 SEO、企业站上线前检查清单 以及 WordPress 表单插件对比 属于同一套内容体系,建议统一规划。
