
本文介绍如何在管理员手动保存或发布 woocommerce 商品时,精准触发自定义逻辑(如创建衍生商品),避免递归调用,并提供稳定、可复用的钩子方案与完整示例代码。
本文介绍如何在管理员手动保存或发布 woocommerce 商品时,精准触发自定义逻辑(如创建衍生商品),避免递归调用,并提供稳定、可复用的钩子方案与完整示例代码。
在 WooCommerce 开发中,若需在管理员手动创建并发布产品后自动执行操作(例如生成配套订阅商品、同步库存、写入日志或调用外部 API),关键在于选择语义明确、时机可靠且天然规避递归风险的钩子。transition_post_status 虽然通用,但存在两大缺陷:一是它会在后台批量操作、程序化更新、甚至 REST API 修改状态时被触发;二是新创建的商品初始状态可能为 auto-draft → publish,而 old_status 并非总是空字符串或可预测值,导致条件判断失效;更严重的是,你代码中新建商品后调用 $product->save() 会再次触发 woocommerce_process_product_meta,若未加防护极易引发无限递归。
✅ 推荐使用:woocommerce_process_product_meta
该钩子仅在 WooCommerce 商品编辑页点击「更新」或「发布」按钮时由后台表单提交触发,天然限定于人工操作场景,且不响应 wp_insert_post() 等程序化创建行为,从根本上避免了递归问题。自 WooCommerce 3.0 起稳定支持,经实测兼容 WC 6.2+ 版本。
以下是一个生产就绪的示例,用于在每次手动发布普通商品时,自动创建一个价格为 ¥1、订阅周期为 12 个月的配套订阅商品:
/**
* 在手动保存/发布商品后,创建关联的订阅商品
* 此钩子仅响应 admin 页面的表单提交,安全无递归
*/
function create_companion_subscription_on_publish($post_id, $post) {
// 防御:跳过自动保存、AJAX 请求及非主循环调用
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (!isset($_POST['post_ID']) || intval($_POST['post_ID']) !== $post_id) return;
if (!current_user_can('edit_post', $post_id)) return;
// 确保是新建并首次发布(非更新已有已发布商品)
$post_status = get_post_status($post_id);
if ($post_status !== 'publish' || get_post_field('post_status', $post_id) !== 'publish') {
return; // 非发布状态或非最终发布态,跳过
}
// 创建订阅商品实例(需确保 WC_Subscription 插件已激活)
$subscription_data = array(
'post_title' => 'Subscription for ' . esc_html($post->post_title),
'post_name' => sanitize_title('subscription-for-' . $post->post_title),
'post_status' => 'publish',
'post_type' => 'product',
'post_author' => $post->post_author,
);
$sub_product_id = wp_insert_post($subscription_data);
if (!$sub_product_id || is_wp_error($sub_product_id)) {
error_log('Failed to create subscription product for post ID: ' . $post_id);
return;
}
// 初始化并配置订阅商品(以 WC_Product_Subscription 为例)
$subscription = new WC_Product_Subscription($sub_product_id);
$subscription->set_name('Monthly Plan for ' . $post->post_title);
$subscription->set_status('publish');
$subscription->set_price(1.00);
$subscription->set_regular_price(1.00);
$subscription->set_subscription_length(12);
$subscription->set_subscription_period('month');
$subscription->set_catalog_visibility('visible');
$subscription->save();
// 可选:建立父子关系(如需后续查询)
update_post_meta($sub_product_id, '_parent_product_id', $post_id);
update_post_meta($post_id, '_has_companion_subscription', $sub_product_id);
// 记录日志(建议使用 WP 日志系统,而非 file_put_contents)
error_log(sprintf('[WooCommerce Hook] Created subscription %d for product %d', $sub_product_id, $post_id));
}
add_action('woocommerce_process_product_meta', 'create_companion_subscription_on_publish', 10, 2);? 重要注意事项:
- ✅ 依赖检查:若使用 WC_Product_Subscription,请确保 WooCommerce Subscriptions 插件已启用,否则实例化会失败。可添加 class_exists('WC_Product_Subscription') 判断。
- ✅ 权限与安全性:示例中已加入 current_user_can() 和 DOING_AUTOSAVE 校验,防止越权与重复执行。
- ⚠️ 避免硬编码路径日志:file_put_contents('./log_*.log') 易因路径权限或 Web 服务器配置失败;推荐统一使用 error_log() 或 WordPress 的 wc_get_logger()。
- ? 递归防护:本钩子本身不因 wp_insert_post() 触发,但若你在其他地方监听 save_post_product,务必添加 remove_action('save_post_product', ...) 临时移除再恢复,或使用 doing_action('woocommerce_process_product_meta') 判断上下文。
- ? 调试建议:启用 WP_DEBUG_LOG,并在函数开头添加 error_log("Hook fired for post {$post_id}"); 快速验证是否命中。
通过此方案,你不仅能精准响应人工发布行为,还能构建健壮、可维护的 WooCommerce 扩展逻辑。

















