文章目录
-
- 在文创电商领域,商品往往具有限量生产、预售周期长、库存灵活调整等特点。传统的WordPress电商插件难以满足这些特殊需求。本教程将指导您开发一个专门针对文创商品的柔性预售与库存联动插件。 核心需求分析: 支持商品预售状态设置 实现库存与预售订单的智能联动 提供预售进度可视化 自动库存预警功能 与WooCommerce无缝集成
- 首先,我们需要创建插件的基本文件结构: <?php /** * Plugin Name: 文创商品柔性预售与库存联动 * Plugin URI: https://yourwebsite.com/ * Description: 专为文创商品设计的预售与库存管理插件 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('FLEX_PRESALE_VERSION', '1.0.0'); define('FLEX_PRESALE_PATH', plugin_dir_path(__FILE__)); define('FLEX_PRESALE_URL', plugin_dir_url(__FILE__)); // 初始化插件 class FlexPresalePlugin { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 激活/停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化 add_action('plugins_loaded', array($this, 'init')); } public function activate() { // 创建必要的数据库表 $this->create_tables(); // 设置默认选项 update_option('flex_presale_version', FLEX_PRESALE_VERSION); } public function deactivate() { // 清理临时数据 // 注意:这里不删除数据,以便用户重新激活时保留设置 } public function init() { // 检查WooCommerce是否激活 if (!class_exists('WooCommerce')) { add_action('admin_notices', array($this, 'woocommerce_missing_notice')); return; } // 加载核心功能 $this->load_core(); } private function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'flex_presale_logs'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, product_id bigint(20) NOT NULL, order_id bigint(20) DEFAULT NULL, action_type varchar(50) NOT NULL, old_value varchar(255) DEFAULT NULL, new_value varchar(255) DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY product_id (product_id), KEY order_id (order_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } public function woocommerce_missing_notice() { ?> <div class="notice notice-error"> <p><?php _e('文创商品柔性预售插件需要WooCommerce才能正常工作!', 'flex-presale'); ?></p> </div> <?php } private function load_core() { // 加载核心类文件 require_once FLEX_PRESALE_PATH . 'includes/class-product-manager.php'; require_once FLEX_PRESALE_PATH . 'includes/class-inventory-sync.php'; require_once FLEX_PRESALE_PATH . 'includes/class-admin-interface.php'; // 初始化核心类 new FlexPresale_Product_Manager(); new FlexPresale_Inventory_Sync(); new FlexPresale_Admin_Interface(); } } // 启动插件 FlexPresalePlugin::get_instance(); ?>
- 接下来,我们实现商品预售的核心功能: <?php // 文件路径: includes/class-product-manager.php class FlexPresale_Product_Manager { public function __construct() { // 添加商品预售字段 add_action('woocommerce_product_options_inventory_product_data', array($this, 'add_presale_fields')); add_action('woocommerce_process_product_meta', array($this, 'save_presale_fields')); // 修改前端显示 add_filter('woocommerce_get_availability', array($this, 'modify_availability_text'), 10, 2); // 添加预售进度条短代码 add_shortcode('presale_progress', array($this, 'presale_progress_shortcode')); } /** * 在商品编辑页面添加预售字段 */ public function add_presale_fields() { global $post; echo '<div class="options_group">'; // 预售开关 woocommerce_wp_checkbox(array( 'id' => '_is_presale', 'label' => __('启用预售', 'flex-presale'), 'description' => __('启用此商品的预售功能', 'flex-presale'), 'desc_tip' => true, )); // 预售开始时间 woocommerce_wp_text_input(array( 'id' => '_presale_start_date', 'label' => __('预售开始时间', 'flex-presale'), 'type' => 'datetime-local', 'desc_tip' => true, 'description' => __('设置预售开始的具体时间', 'flex-presale'), )); // 预售结束时间 woocommerce_wp_text_input(array( 'id' => '_presale_end_date', 'label' => __('预售结束时间', 'flex-presale'), 'type' => 'datetime-local', 'desc_tip' => true, 'description' => __('设置预售结束的具体时间', 'flex-presale'), )); // 预售目标数量 woocommerce_wp_text_input(array( 'id' => '_presale_target_qty', 'label' => __('预售目标数量', 'flex-presale'), 'type' => 'number', 'custom_attributes' => array( 'min' => '1', 'step' => '1' ), 'desc_tip' => true, 'description' => __('达到此数量后可以考虑提前生产', 'flex-presale'), )); // 柔性库存比例 woocommerce_wp_text_input(array( 'id' => '_flex_inventory_ratio', 'label' => __('柔性库存比例 (%)', 'flex-presale'), 'type' => 'number', 'custom_attributes' => array( 'min' => '0', 'max' => '100', 'step' => '5' ), 'desc_tip' => true, 'description' => __('基于预售订单自动增加的库存比例', 'flex-presale'), )); echo '</div>'; } /** * 保存预售字段数据 */ public function save_presale_fields($post_id) { $is_presale = isset($_POST['_is_presale']) ? 'yes' : 'no'; update_post_meta($post_id, '_is_presale', $is_presale); if (isset($_POST['_presale_start_date'])) { update_post_meta($post_id, '_presale_start_date', sanitize_text_field($_POST['_presale_start_date'])); } if (isset($_POST['_presale_end_date'])) { update_post_meta($post_id, '_presale_end_date', sanitize_text_field($_POST['_presale_end_date'])); } if (isset($_POST['_presale_target_qty'])) { update_post_meta($post_id, '_presale_target_qty', absint($_POST['_presale_target_qty'])); } if (isset($_POST['_flex_inventory_ratio'])) { $ratio = floatval($_POST['_flex_inventory_ratio']); $ratio = max(0, min(100, $ratio)); // 限制在0-100之间 update_post_meta($post_id, '_flex_inventory_ratio', $ratio); } } /** * 修改商品库存状态显示 */ public function modify_availability_text($availability, $product) { $is_presale = get_post_meta($product->get_id(), '_is_presale', true); if ($is_presale === 'yes') { $presale_end_date = get_post_meta($product->get_id(), '_presale_end_date', true); if ($presale_end_date) { $end_date = date_i18n(get_option('date_format'), strtotime($presale_end_date)); $availability['availability'] = sprintf( __('预售中,截止日期:%s', 'flex-presale'), $end_date ); $availability['class'] = 'available-on-backorder'; } else { $availability['availability'] = __('预售中', 'flex-presale'); $availability['class'] = 'available-on-backorder'; } } return $availability; } /** * 预售进度条短代码 */ public function presale_progress_shortcode($atts) { $atts = shortcode_atts(array( 'product_id' => get_the_ID(), ), $atts, 'presale_progress'); $product_id = intval($atts['product_id']); $product = wc_get_product($product_id); if (!$product) { return ''; } $is_presale = get_post_meta($product_id, '_is_presale', true); if ($is_presale !== 'yes') { return ''; } $presale_target = get_post_meta($product_id, '_presale_target_qty', true); $current_orders = $this->get_presale_order_count($product_id); if (!$presale_target || $presale_target <= 0) { return ''; } $percentage = min(100, ($current_orders / $presale_target) * 100); ob_start(); ?> <div class="flex-presale-progress"> <h4><?php _e('预售进度', 'flex-presale'); ?></h4> <div class="progress-bar"> <div class="progress-fill" style="width: <?php echo esc_attr($percentage); ?>%;"></div> </div> <div class="progress-stats"> <span class="current"><?php echo sprintf(__('已预售:%d', 'flex-presale'), $current_orders); ?></span> <span class="target"><?php echo sprintf(__('目标:%d', 'flex-presale'), $presale_target); ?></span> <span class="percentage"><?php echo round($percentage, 1); ?>%</span> </div> <?php if ($percentage >= 100): ?> <div class="presale-target-reached"> <?php _e('🎉 预售目标已达成!', 'flex-presale'); ?> </div> <?php endif; ?> </div> <style> .flex-presale-progress { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; } .progress-bar { height: 20px; background: #f0f0f0; border-radius: 10px; overflow: hidden; margin: 10px 0; } .progress-fill { height: 100%; background: linear-gradient(90deg, #4CAF50, #8BC34A); transition: width 0.5s ease; } .progress-stats { display: flex; justify-content: space-between; font-size: 14px; color: #666; } .presale-target-reached { margin-top: 10px; padding: 10px; background: #e8f5e9; border-radius: 5px; text-align: center; font-weight: bold; color: #2e7d32; } </style> <?php return ob_get_clean(); } /** * 获取预售订单数量 */ private function get_presale_order_count($product_id) { global $wpdb; $query = $wpdb->prepare(" SELECT SUM(oi.meta_value) as total_qty FROM {$wpdb->prefix}woocommerce_order_itemmeta oim JOIN {$wpdb->prefix}woocommerce_order_items oi ON oim.order_item_id = oi.order_item_id JOIN {$wpdb->posts} p ON oi.order_id = p.ID WHERE oim.meta_key = '_product_id' AND oim.meta_value = %d AND p.post_status IN ('wc-processing', 'wc-completed') AND p.post_date >= ( SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_presale_start_date' LIMIT 1 ) ", $product_id, $product_id); $result = $wpdb->get_var($query); return $result ? intval($result) : 0; } } ?>
- 库存联动是插件的核心功能,确保预售订单与实际库存的智能同步: <?php // 文件路径: includes/class-inventory-sync.php class FlexPresale_Inventory_Sync { public function __construct() { // 订单状态变化时更新库存 add_action('woocommerce_order_status_changed', array($this, 'sync_inventory_on_order_change'), 10, 3); // 定期检查库存预警 add_action('flex_presale_daily_inventory_check', array($this, 'daily_inventory_check')); // 安排定时任务 $this->schedule_cron_jobs(); } /** * 订单状态变化时同步库存 */ public function sync_inventory_on_order_change($order_id, $old_status, $new_status) { $order = wc_get_order($order_id); if (!$order) { return; } foreach ($order->get_items() as $item) { $product_id = $item->get_product_id(); $is_presale = get_post_meta($product_id, '_is_presale', true); if ($is_presale !== 'yes') { continue; } $this->adjust_flexible_inventory($product_id, $item->get_quantity(), $new_status); $this->log_inventory_change($product_id, $order_id, $old_status, $new_status, $item->get_quantity()); } } /** * 调整柔性库存 */ private function adjust_flexible_inventory($product_id, $quantity, $order_status) { $product = wc_get_product($product_id); if (!$product) { return; } $flex_ratio = get_post_meta($product_id, '_flex_inventory_ratio', true); $flex_ratio = $flex_ratio ? floatval($flex_ratio) / 100 : 0.1; // 默认10% // 计算需要增加的库存 $inventory_increase = ceil($quantity * $flex_ratio); // 获取当前库存 $current_stock = $product->get_stock_quantity(); if ($order_status === 'processing' || $order_status === 'completed') { // 订单处理中或完成时,增加柔性库存 $new_stock = $current_stock + $inventory_increase; $product->set_stock_quantity($new_stock); $product->save(); // 发送库存更新通知 $this->send_inventory_notification($product_id, $inventory_increase, $new_stock); } } /** * 记录库存变化日志 */ private function log_inventory_change($product_id, $order_id, $old_status, $new_status, $quantity) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'flex_presale_logs', array( 'product_id' => $product_id, 'order_id' => $order_id, 'action_type' => 'inventory_adjustment', 'old_value' => $old_status, 'new_value' => $new_status . '|qty:' . $quantity, 'created_at' => current_time('mysql') ), array('%d', '%d', '%s', '%s', '%s', '%s') ); } /** * 安排定时任务 */ private function schedule_cron_jobs() { if (!wp_next_scheduled('flex_presale_daily_inventory_check')) { wp_schedule_event(time(), 'daily', 'flex_presale_daily_inventory_check'); } } /** * 每日库存检查 */ public function daily_inventory_check() { $args = array( 'post_type' => 'product', 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => '_is_presale', 'value' => 'yes', 'compare' => '=' ) ) ); $products = get_posts($args); foreach ($products as $product_post) { $product = wc_get_product($product_post->ID); $current_stock = $product->get_stock_quantity(); $presale_target = get_post_meta($product_post->ID, '_presale_target_qty', true); $current_orders = $this->get_presale_order_count($product_post->ID); // 检查是否需要预警 $this->check_inventory_alert($product_post->ID, $current_stock, $current_orders, $presale_target); } } /** * 检查库存预警 */ private function check_inventory_alert($product_id, $current_stock, $current_orders, $presale_target) { $alert_threshold = apply_filters('flex_presale_alert_threshold', 0.2); // 默认20%阈值 if ($presale_target > 0) { $fulfillment_ratio = $current_orders / $presale_target; // 如果预售达成率超过阈值且库存不足 if ($fulfillment_ratio >= $alert_threshold && $current_stock < $current_orders * 1.1) { $this->send_alert_notification($product_id, $current_stock, $current_orders, $fulfillment_ratio); } } } /** * 发送预警通知 */ private function send_alert_notification($product_id, $current_stock, $current_orders, $fulfillment_ratio) { $product = wc_get_product($product_id); $admin_email = get_option('admin_email'); $subject = sprintf(__('【库存预警】商品 "%s" 需要补货', 'flex-presale'), $product->get_name()); $message = sprintf(__( "商品名称:%sn" . "商品ID:%dn" . "当前库存:%dn" . "预售订单量:%dn" . "预售达成率:%.1f%%n" . "建议立即安排生产补货。nn" . "查看详情:%s", 'flex-presale' ), $product->get_name(), $product_id, $current_stock, $current_orders, $fulfillment_ratio * 100, admin_url('post.php?post=' . $product_id . '&action=edit') ); wp_mail($admin_email, $subject, $message); // 同时记录到系统日志 $this->log_alert($product_id, $current_stock, $current_orders, $fulfillment_ratio); } /** * 发送库存更新通知 */ private function send_inventory_notification($product_id, $increase_amount, $new_stock) { // 这里可以扩展为发送给生产部门或供应商 error_log(sprintf( '商品ID %d 库存已增加 %d,新库存为 %d', $product_id, $increase_amount, $new_stock )); } /** * 记录预警日志 */ private function log_alert($product_id, $current_stock, $current_orders, $fulfillment_ratio) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'flex_presale_logs', array( 'product_id' => $product_id, 'action_type' => 'inventory_alert', 'old_value' => 'stock:' . $current_stock, 'new_value' => 'orders:' . $current_orders . '|ratio:' . $fulfillment_ratio, 'created_at' => current_time('mysql') ), array('%d', '%s', '%s', '%s', '%s') ); } /** * 获取预售订单数量(复用Product Manager的方法) */ private function get_presale_order_count($product_id) { global $wpdb; $query = $wpdb->prepare(" SELECT SUM(oi.meta_value) as total_qty FROM {$wpdb->prefix}woocommerce_order_itemmeta oim JOIN {$wpdb->prefix}woocommerce_order_items oi ON oim.order_item_id = oi.order_item_id JOIN {$wpdb->posts} p ON oi.order_id = p.ID WHERE oim.meta_key = '_product_id' AND oim.meta_value = %d AND p.post_status IN ('wc-processing', 'wc-completed') ", $product_id); $result = $wpdb->get_var($query); return $result ? intval($result) : 0; } }?> ## 五、管理界面开发 为方便管理,我们需要创建一个直观的后台管理界面: <?php// 文件路径: includes/class-admin-interface.php class FlexPresale_Admin_Interface { public function __construct() { // 添加管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 添加商品列表列 add_filter('manage_product_posts_columns', array($this, 'add_product_columns')); add_action('manage_product_posts_custom_column', array($this, 'render_product_columns'), 10, 2); // 添加快速编辑字段 add_action('quick_edit_custom_box', array($this, 'add_quick_edit_fields'), 10, 2); add_action('save_post', array($this, 'save_quick_edit_fields')); // 加载脚本和样式 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } /** * 添加管理菜单 */ public function add_admin_menu() { add_submenu_page( 'woocommerce', __('文创预售管理', 'flex-presale'), __('文创预售', 'flex-presale'), 'manage_woocommerce', 'flex-presale-dashboard', array($this, 'render_dashboard') ); add_submenu_page( 'woocommerce', __('预售报表', 'flex-presale'), __('预售报表', 'flex-presale'), 'manage_woocommerce', 'flex-presale-reports', array($this, 'render_reports_page') ); } /** * 渲染仪表板 */ public function render_dashboard() { ?> <div class="wrap flex-presale-dashboard"> <h1><?php _e('文创商品预售管理仪表板', 'flex-presale'); ?></h1> <div class="dashboard-widgets"> <div class="widget"> <h3><?php _e('预售概况', 'flex-presale'); ?></h3> <?php $this->render_presale_overview(); ?> </div> <div class="widget"> <h3><?php _e('库存预警', 'flex-presale'); ?></h3> <?php $this->render_inventory_alerts(); ?> </div> <div class="widget full-width"> <h3><?php _e('近期预售活动', 'flex-presale'); ?></h3> <?php $this->render_recent_presales(); ?> </div> </div> </div> <style> .flex-presale-dashboard { padding: 20px; } .dashboard-widgets { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 20px; } .dashboard-widgets .widget { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .dashboard-widgets .widget.full-width { grid-column: 1 / -1; } .presale-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-top: 15px; } .stat-item { text-align: center; padding: 15px; background: #f8f9fa; border-radius: 6px; } .stat-value { font-size: 24px; font-weight: bold; color: #0073aa; display: block; } .stat-label { font-size: 12px; color: #666; text-transform: uppercase; } .alert-list { margin-top: 15px; } .alert-item { padding: 10px; border-left: 4px solid #ff9800; background: #fff8e1; margin-bottom: 10px; border-radius: 4px; } .alert-item.critical { border-left-color: #f44336; background: #ffebee; } .presale-table { width: 100%; border-collapse: collapse; margin-top: 15px; } .presale-table th, .presale-table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; } .presale-table th { background: #f8f9fa; font-weight: 600; } .progress-cell { min-width: 150px; } .progress-bar-small { height: 8px; background: #f0f0f0; border-radius: 4px; overflow: hidden; margin-top: 5px; } .progress-fill-small { height: 100%; background: linear-gradient(90deg, #4CAF50, #8BC34A); } </style> <?php } /** * 渲染预售概况 */ private function render_presale_overview() { global $wpdb; // 获取统计数据 $total_presale_products = $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_is_presale' AND meta_value = 'yes' "); $active_presales = $wpdb->get_var($wpdb->prepare(" SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm1 ON p.ID = pm1.post_id JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id WHERE p.post_type = 'product' AND p.post_status = 'publish' AND pm1.meta_key = '_is_presale' AND pm1.meta_value = 'yes' AND pm2.meta_key = '_presale_end_date' AND pm2.meta_value > %s ", current_time('mysql'))); $total_presale_orders = $wpdb->get_var(" SELECT SUM(oi.meta_value) FROM {$wpdb->prefix}woocommerce_order_itemmeta oim JOIN {$wpdb->prefix}woocommerce_order_items oi ON oim.order_item_id = oi.order_item_id JOIN {$wpdb->posts} p ON oi.order_id = p.ID JOIN {$wpdb->postmeta} pm ON oim.meta_value = pm.post_id WHERE oim.meta_key = '_product_id' AND p.post_status IN ('wc-processing', 'wc-completed') AND pm.meta_key = '_is_presale' AND pm.meta_value = 'yes' "); ?> <div class="presale-stats"> <div class="stat-item"> <span class="stat-value"><?php echo esc_html($total_presale_products); ?></span> <span class="stat-label"><?php _e('预售商品总数', 'flex-presale'); ?></span> </div> <div class="stat-item"> <span class="stat-value"><?php echo esc_html($active_presales); ?></span> <span class="stat-label"><?php _e('进行中预售', 'flex-presale'); ?></span> </div> <div class="stat-item"> <span class="stat-value"><?php echo esc_html($total_presale_orders ?: 0); ?></span> <span class="stat-label"><?php _e('总预售数量', 'flex-presale'); ?></span> </div> </div> <?php } /** * 渲染库存预警 */ private function render_inventory_alerts() { $args = array( 'post_type' => 'product', 'posts_per_page' => 5, 'meta_query' => array( 'relation' => 'AND', array( 'key' => '_is_presale', 'value' => 'yes', 'compare' => '=' ), array( 'key' => '_stock', 'value' => 10, 'compare' => '<=', 'type' => 'NUMERIC' ) ) ); $products = get_posts($args); if (empty($products)) { echo '<p>' . __('暂无库存预警', 'flex-presale') . '</p>'; return; } echo '<div class="alert-list">'; foreach ($products as $product_post) { $product = wc_get_product($product_post->ID); $stock = $product->get_stock_quantity(); $alert_class = $stock <= 5 ? 'critical' : ''; echo sprintf( '<div class="alert-item %s">%s - %s: %d</div>', $alert_class, esc_html($product->get_sku()), esc_html($product->get_name()), $stock ); } echo '</div>'; } /** * 渲染近期预售活动 */ private function render_recent_presales() { $args = array( 'post_type' => 'product', 'posts_per_page' => 10, 'meta_key' => '_presale_end_date', 'orderby' => 'meta_value', 'order' => 'ASC', 'meta_query' => array( array( 'key' => '_is_presale', 'value' => 'yes', 'compare' => '=' ), array( 'key' => '_presale_end_date', 'value' => current_time('mysql'), 'compare' => '>', 'type' => 'DATETIME' ) ) ); $products = get_posts($args); if (empty($products)) { echo '<p>' . __('暂无进行中的预售活动', 'flex-presale') . '</p>'; return; } echo '<table class="presale-table">'; echo '<thead> <tr> <th>' . __('商品', 'flex-presale') . '</th> <th>' . __('预售结束时间', 'flex-presale') . '</th> <th>' . __('目标数量', 'flex-presale') . '</th> <th>' . __('当前进度', 'flex-presale') . '</th> <th>' . __('库存', 'flex-presale') . '</th> </tr> </thead>'; echo '<tbody>'; foreach ($products as $product_post) { $product = wc_get_product($product_post->ID); $end_date = get_post_meta($product_post->ID, '_presale_end_date', true); $target = get_post_meta($product_post->ID, '_presale_target_qty', true); $current_orders = $this->get_presale_order_count($product_post->ID); $progress = $target > 0 ? min(100, ($current_orders / $target) * 100) : 0; echo '<tr>'; echo '<td><strong>' . esc_html($product->get_name()) . '</strong><br><small>' . esc_html($product->get_sku()) . '</small></td>'; echo '<td>' . ($end_date ? date_i18n(get_option('date_format'), strtotime($end_date)) : '-') . '</td>'; echo '<td>' . esc_html($target ?: '-') . '</td>'; echo '<td class="progress-cell"> ' . esc_html($current_orders) . ' <div class="progress-bar-small"> <div class="progress-fill-small" style="width: ' . esc_attr($progress) . '%;"></div> </div> </td>'; echo '<td>' . esc_html($product->get_stock_quantity()) . '</td>'; echo '</tr>'; } echo '</tbody></table>'; } /** * 添加商品列表列 */ public function add_product_columns($columns) { $new_columns = array(); foreach ($columns as $key => $value) { $new_columns[$key] = $value; if ($key === 'name') { $new_columns['presale_status'] = __('预售状态', 'flex-presale'); $new_columns['presale_progress'] = __('预售进度', 'flex-presale'); } } return $new_columns; } /** * 渲染商品列表列 */ public function render_product_columns($column, $product_id) { if ($column === 'presale_status') { $is_presale = get_post_meta($product_id, '_is_presale', true); if ($is_presale === 'yes') { $end_date = get_post_meta($product_id, '_pres
在文创电商领域,商品往往具有限量生产、预售周期长、库存灵活调整等特点。传统的WordPress电商插件难以满足这些特殊需求。本教程将指导您开发一个专门针对文创商品的柔性预售与库存联动插件。
核心需求分析:
- 支持商品预售状态设置
- 实现库存与预售订单的智能联动
- 提供预售进度可视化
- 自动库存预警功能
- 与WooCommerce无缝集成
首先,我们需要创建插件的基本文件结构:
<?php
/**
* Plugin Name: 文创商品柔性预售与库存联动
* Plugin URI: https://yourwebsite.com/
* Description: 专为文创商品设计的预售与库存管理插件
* Version: 1.0.0
* Author: 您的名称
* License: GPL v2 or later
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('FLEX_PRESALE_VERSION', '1.0.0');
define('FLEX_PRESALE_PATH', plugin_dir_path(__FILE__));
define('FLEX_PRESALE_URL', plugin_dir_url(__FILE__));
// 初始化插件
class FlexPresalePlugin {
private static $instance = null;
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
}
private function init_hooks() {
// 激活/停用钩子
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
// 初始化
add_action('plugins_loaded', array($this, 'init'));
}
public function activate() {
// 创建必要的数据库表
$this->create_tables();
// 设置默认选项
update_option('flex_presale_version', FLEX_PRESALE_VERSION);
}
public function deactivate() {
// 清理临时数据
// 注意:这里不删除数据,以便用户重新激活时保留设置
}
public function init() {
// 检查WooCommerce是否激活
if (!class_exists('WooCommerce')) {
add_action('admin_notices', array($this, 'woocommerce_missing_notice'));
return;
}
// 加载核心功能
$this->load_core();
}
private function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'flex_presale_logs';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
product_id bigint(20) NOT NULL,
order_id bigint(20) DEFAULT NULL,
action_type varchar(50) NOT NULL,
old_value varchar(255) DEFAULT NULL,
new_value varchar(255) DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY product_id (product_id),
KEY order_id (order_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
public function woocommerce_missing_notice() {
?>
<div class="notice notice-error">
<p><?php _e('文创商品柔性预售插件需要WooCommerce才能正常工作!', 'flex-presale'); ?></p>
</div>
<?php
}
private function load_core() {
// 加载核心类文件
require_once FLEX_PRESALE_PATH . 'includes/class-product-manager.php';
require_once FLEX_PRESALE_PATH . 'includes/class-inventory-sync.php';
require_once FLEX_PRESALE_PATH . 'includes/class-admin-interface.php';
// 初始化核心类
new FlexPresale_Product_Manager();
new FlexPresale_Inventory_Sync();
new FlexPresale_Admin_Interface();
}
}
// 启动插件
FlexPresalePlugin::get_instance();
?>
接下来,我们实现商品预售的核心功能:
<?php
// 文件路径: includes/class-product-manager.php
class FlexPresale_Product_Manager {
public function __construct() {
// 添加商品预售字段
add_action('woocommerce_product_options_inventory_product_data', array($this, 'add_presale_fields'));
add_action('woocommerce_process_product_meta', array($this, 'save_presale_fields'));
// 修改前端显示
add_filter('woocommerce_get_availability', array($this, 'modify_availability_text'), 10, 2);
// 添加预售进度条短代码
add_shortcode('presale_progress', array($this, 'presale_progress_shortcode'));
}
/**
* 在商品编辑页面添加预售字段
*/
public function add_presale_fields() {
global $post;
echo '<div class="options_group">';
// 预售开关
woocommerce_wp_checkbox(array(
'id' => '_is_presale',
'label' => __('启用预售', 'flex-presale'),
'description' => __('启用此商品的预售功能', 'flex-presale'),
'desc_tip' => true,
));
// 预售开始时间
woocommerce_wp_text_input(array(
'id' => '_presale_start_date',
'label' => __('预售开始时间', 'flex-presale'),
'type' => 'datetime-local',
'desc_tip' => true,
'description' => __('设置预售开始的具体时间', 'flex-presale'),
));
// 预售结束时间
woocommerce_wp_text_input(array(
'id' => '_presale_end_date',
'label' => __('预售结束时间', 'flex-presale'),
'type' => 'datetime-local',
'desc_tip' => true,
'description' => __('设置预售结束的具体时间', 'flex-presale'),
));
// 预售目标数量
woocommerce_wp_text_input(array(
'id' => '_presale_target_qty',
'label' => __('预售目标数量', 'flex-presale'),
'type' => 'number',
'custom_attributes' => array(
'min' => '1',
'step' => '1'
),
'desc_tip' => true,
'description' => __('达到此数量后可以考虑提前生产', 'flex-presale'),
));
// 柔性库存比例
woocommerce_wp_text_input(array(
'id' => '_flex_inventory_ratio',
'label' => __('柔性库存比例 (%)', 'flex-presale'),
'type' => 'number',
'custom_attributes' => array(
'min' => '0',
'max' => '100',
'step' => '5'
),
'desc_tip' => true,
'description' => __('基于预售订单自动增加的库存比例', 'flex-presale'),
));
echo '</div>';
}
/**
* 保存预售字段数据
*/
public function save_presale_fields($post_id) {
$is_presale = isset($_POST['_is_presale']) ? 'yes' : 'no';
update_post_meta($post_id, '_is_presale', $is_presale);
if (isset($_POST['_presale_start_date'])) {
update_post_meta($post_id, '_presale_start_date', sanitize_text_field($_POST['_presale_start_date']));
}
if (isset($_POST['_presale_end_date'])) {
update_post_meta($post_id, '_presale_end_date', sanitize_text_field($_POST['_presale_end_date']));
}
if (isset($_POST['_presale_target_qty'])) {
update_post_meta($post_id, '_presale_target_qty', absint($_POST['_presale_target_qty']));
}
if (isset($_POST['_flex_inventory_ratio'])) {
$ratio = floatval($_POST['_flex_inventory_ratio']);
$ratio = max(0, min(100, $ratio)); // 限制在0-100之间
update_post_meta($post_id, '_flex_inventory_ratio', $ratio);
}
}
/**
* 修改商品库存状态显示
*/
public function modify_availability_text($availability, $product) {
$is_presale = get_post_meta($product->get_id(), '_is_presale', true);
if ($is_presale === 'yes') {
$presale_end_date = get_post_meta($product->get_id(), '_presale_end_date', true);
if ($presale_end_date) {
$end_date = date_i18n(get_option('date_format'), strtotime($presale_end_date));
$availability['availability'] = sprintf(
__('预售中,截止日期:%s', 'flex-presale'),
$end_date
);
$availability['class'] = 'available-on-backorder';
} else {
$availability['availability'] = __('预售中', 'flex-presale');
$availability['class'] = 'available-on-backorder';
}
}
return $availability;
}
/**
* 预售进度条短代码
*/
public function presale_progress_shortcode($atts) {
$atts = shortcode_atts(array(
'product_id' => get_the_ID(),
), $atts, 'presale_progress');
$product_id = intval($atts['product_id']);
$product = wc_get_product($product_id);
if (!$product) {
return '';
}
$is_presale = get_post_meta($product_id, '_is_presale', true);
if ($is_presale !== 'yes') {
return '';
}
$presale_target = get_post_meta($product_id, '_presale_target_qty', true);
$current_orders = $this->get_presale_order_count($product_id);
if (!$presale_target || $presale_target <= 0) {
return '';
}
$percentage = min(100, ($current_orders / $presale_target) * 100);
ob_start();
?>
<div class="flex-presale-progress">
<h4><?php _e('预售进度', 'flex-presale'); ?></h4>
<div class="progress-bar">
<div class="progress-fill" style="width: <?php echo esc_attr($percentage); ?>%;"></div>
</div>
<div class="progress-stats">
<span class="current"><?php echo sprintf(__('已预售:%d', 'flex-presale'), $current_orders); ?></span>
<span class="target"><?php echo sprintf(__('目标:%d', 'flex-presale'), $presale_target); ?></span>
<span class="percentage"><?php echo round($percentage, 1); ?>%</span>
</div>
<?php if ($percentage >= 100): ?>
<div class="presale-target-reached">
<?php _e('🎉 预售目标已达成!', 'flex-presale'); ?>
</div>
<?php endif; ?>
</div>
<style>
.flex-presale-progress {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.progress-bar {
height: 20px;
background: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
transition: width 0.5s ease;
}
.progress-stats {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #666;
}
.presale-target-reached {
margin-top: 10px;
padding: 10px;
background: #e8f5e9;
border-radius: 5px;
text-align: center;
font-weight: bold;
color: #2e7d32;
}
</style>
<?php
return ob_get_clean();
}
/**
* 获取预售订单数量
*/
private function get_presale_order_count($product_id) {
global $wpdb;
$query = $wpdb->prepare("
SELECT SUM(oi.meta_value) as total_qty
FROM {$wpdb->prefix}woocommerce_order_itemmeta oim
JOIN {$wpdb->prefix}woocommerce_order_items oi ON oim.order_item_id = oi.order_item_id
JOIN {$wpdb->posts} p ON oi.order_id = p.ID
WHERE oim.meta_key = '_product_id'
AND oim.meta_value = %d
AND p.post_status IN ('wc-processing', 'wc-completed')
AND p.post_date >= (
SELECT meta_value
FROM {$wpdb->postmeta}
WHERE post_id = %d
AND meta_key = '_presale_start_date'
LIMIT 1
)
", $product_id, $product_id);
$result = $wpdb->get_var($query);
return $result ? intval($result) : 0;
}
}
?>
库存联动是插件的核心功能,确保预售订单与实际库存的智能同步:
<?php
// 文件路径: includes/class-inventory-sync.php
class FlexPresale_Inventory_Sync {
public function __construct() {
// 订单状态变化时更新库存
add_action('woocommerce_order_status_changed', array($this, 'sync_inventory_on_order_change'), 10, 3);
// 定期检查库存预警
add_action('flex_presale_daily_inventory_check', array($this, 'daily_inventory_check'));
// 安排定时任务
$this->schedule_cron_jobs();
}
/**
* 订单状态变化时同步库存
*/
public function sync_inventory_on_order_change($order_id, $old_status, $new_status) {
$order = wc_get_order($order_id);
if (!$order) {
return;
}
foreach ($order->get_items() as $item) {
$product_id = $item->get_product_id();
$is_presale = get_post_meta($product_id, '_is_presale', true);
if ($is_presale !== 'yes') {
continue;
}
$this->adjust_flexible_inventory($product_id, $item->get_quantity(), $new_status);
$this->log_inventory_change($product_id, $order_id, $old_status, $new_status, $item->get_quantity());
}
}
/**
* 调整柔性库存
*/
private function adjust_flexible_inventory($product_id, $quantity, $order_status) {
$product = wc_get_product($product_id);
if (!$product) {
return;
}
$flex_ratio = get_post_meta($product_id, '_flex_inventory_ratio', true);
$flex_ratio = $flex_ratio ? floatval($flex_ratio) / 100 : 0.1; // 默认10%
// 计算需要增加的库存
$inventory_increase = ceil($quantity * $flex_ratio);
// 获取当前库存
$current_stock = $product->get_stock_quantity();
if ($order_status === 'processing' || $order_status === 'completed') {
// 订单处理中或完成时,增加柔性库存
$new_stock = $current_stock + $inventory_increase;
$product->set_stock_quantity($new_stock);
$product->save();
// 发送库存更新通知
$this->send_inventory_notification($product_id, $inventory_increase, $new_stock);
}
}
/**
* 记录库存变化日志
*/
private function log_inventory_change($product_id, $order_id, $old_status, $new_status, $quantity) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'flex_presale_logs',
array(
'product_id' => $product_id,
'order_id' => $order_id,
'action_type' => 'inventory_adjustment',
'old_value' => $old_status,
'new_value' => $new_status . '|qty:' . $quantity,
'created_at' => current_time('mysql')
),
array('%d', '%d', '%s', '%s', '%s', '%s')
);
}
/**
* 安排定时任务
*/
private function schedule_cron_jobs() {
if (!wp_next_scheduled('flex_presale_daily_inventory_check')) {
wp_schedule_event(time(), 'daily', 'flex_presale_daily_inventory_check');
}
}
/**
* 每日库存检查
*/
public function daily_inventory_check() {
$args = array(
'post_type' => 'product',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_is_presale',
'value' => 'yes',
'compare' => '='
)
)
);
$products = get_posts($args);
foreach ($products as $product_post) {
$product = wc_get_product($product_post->ID);
$current_stock = $product->get_stock_quantity();
$presale_target = get_post_meta($product_post->ID, '_presale_target_qty', true);
$current_orders = $this->get_presale_order_count($product_post->ID);
// 检查是否需要预警
$this->check_inventory_alert($product_post->ID, $current_stock, $current_orders, $presale_target);
}
}
/**
* 检查库存预警
*/
private function check_inventory_alert($product_id, $current_stock, $current_orders, $presale_target) {
$alert_threshold = apply_filters('flex_presale_alert_threshold', 0.2); // 默认20%阈值
if ($presale_target > 0) {
$fulfillment_ratio = $current_orders / $presale_target;
// 如果预售达成率超过阈值且库存不足
if ($fulfillment_ratio >= $alert_threshold && $current_stock < $current_orders * 1.1) {
$this->send_alert_notification($product_id, $current_stock, $current_orders, $fulfillment_ratio);
}
}
}
/**
* 发送预警通知
*/
private function send_alert_notification($product_id, $current_stock, $current_orders, $fulfillment_ratio) {
$product = wc_get_product($product_id);
$admin_email = get_option('admin_email');
$subject = sprintf(__('【库存预警】商品 "%s" 需要补货', 'flex-presale'), $product->get_name());
$message = sprintf(__(
"商品名称:%sn" .
"商品ID:%dn" .
"当前库存:%dn" .
"预售订单量:%dn" .
"预售达成率:%.1f%%n" .
"建议立即安排生产补货。nn" .
"查看详情:%s",
'flex-presale'
),
$product->get_name(),
$product_id,
$current_stock,
$current_orders,
$fulfillment_ratio * 100,
admin_url('post.php?post=' . $product_id . '&action=edit')
);
wp_mail($admin_email, $subject, $message);
// 同时记录到系统日志
$this->log_alert($product_id, $current_stock, $current_orders, $fulfillment_ratio);
}
/**
* 发送库存更新通知
*/
private function send_inventory_notification($product_id, $increase_amount, $new_stock) {
// 这里可以扩展为发送给生产部门或供应商
error_log(sprintf(
'商品ID %d 库存已增加 %d,新库存为 %d',
$product_id,
$increase_amount,
$new_stock
));
}
/**
* 记录预警日志
*/
private function log_alert($product_id, $current_stock, $current_orders, $fulfillment_ratio) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'flex_presale_logs',
array(
'product_id' => $product_id,
'action_type' => 'inventory_alert',
'old_value' => 'stock:' . $current_stock,
'new_value' => 'orders:' . $current_orders . '|ratio:' . $fulfillment_ratio,
'created_at' => current_time('mysql')
),
array('%d', '%s', '%s', '%s', '%s')
);
}
/**
* 获取预售订单数量(复用Product Manager的方法)
*/
private function get_presale_order_count($product_id) {
global $wpdb;
$query = $wpdb->prepare("
SELECT SUM(oi.meta_value) as total_qty
FROM {$wpdb->prefix}woocommerce_order_itemmeta oim
JOIN {$wpdb->prefix}woocommerce_order_items oi ON oim.order_item_id = oi.order_item_id
JOIN {$wpdb->posts} p ON oi.order_id = p.ID
WHERE oim.meta_key = '_product_id'
AND oim.meta_value = %d
AND p.post_status IN ('wc-processing', 'wc-completed')
", $product_id);
$result = $wpdb->get_var($query);
return $result ? intval($result) : 0;
}
}
?>
## 五、管理界面开发
为方便管理,我们需要创建一个直观的后台管理界面:
<?php
// 文件路径: includes/class-admin-interface.php
class FlexPresale_Admin_Interface {
public function __construct() {
// 添加管理菜单
add_action('admin_menu', array($this, 'add_admin_menu'));
// 添加商品列表列
add_filter('manage_product_posts_columns', array($this, 'add_product_columns'));
add_action('manage_product_posts_custom_column', array($this, 'render_product_columns'), 10, 2);
// 添加快速编辑字段
add_action('quick_edit_custom_box', array($this, 'add_quick_edit_fields'), 10, 2);
add_action('save_post', array($this, 'save_quick_edit_fields'));
// 加载脚本和样式
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
}
/**
* 添加管理菜单
*/
public function add_admin_menu() {
add_submenu_page(
'woocommerce',
__('文创预售管理', 'flex-presale'),
__('文创预售', 'flex-presale'),
'manage_woocommerce',
'flex-presale-dashboard',
array($this, 'render_dashboard')
);
add_submenu_page(
'woocommerce',
__('预售报表', 'flex-presale'),
__('预售报表', 'flex-presale'),
'manage_woocommerce',
'flex-presale-reports',
array($this, 'render_reports_page')
);
}
/**
* 渲染仪表板
*/
public function render_dashboard() {
?>
<div class="wrap flex-presale-dashboard">
<h1><?php _e('文创商品预售管理仪表板', 'flex-presale'); ?></h1>
<div class="dashboard-widgets">
<div class="widget">
<h3><?php _e('预售概况', 'flex-presale'); ?></h3>
<?php $this->render_presale_overview(); ?>
</div>
<div class="widget">
<h3><?php _e('库存预警', 'flex-presale'); ?></h3>
<?php $this->render_inventory_alerts(); ?>
</div>
<div class="widget full-width">
<h3><?php _e('近期预售活动', 'flex-presale'); ?></h3>
<?php $this->render_recent_presales(); ?>
</div>
</div>
</div>
<style>
.flex-presale-dashboard {
padding: 20px;
}
.dashboard-widgets {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.dashboard-widgets .widget {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dashboard-widgets .widget.full-width {
grid-column: 1 / -1;
}
.presale-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-top: 15px;
}
.stat-item {
text-align: center;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #0073aa;
display: block;
}
.stat-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
}
.alert-list {
margin-top: 15px;
}
.alert-item {
padding: 10px;
border-left: 4px solid #ff9800;
background: #fff8e1;
margin-bottom: 10px;
border-radius: 4px;
}
.alert-item.critical {
border-left-color: #f44336;
background: #ffebee;
}
.presale-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.presale-table th,
.presale-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.presale-table th {
background: #f8f9fa;
font-weight: 600;
}
.progress-cell {
min-width: 150px;
}
.progress-bar-small {
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
margin-top: 5px;
}
.progress-fill-small {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
}
</style>
<?php
}
/**
* 渲染预售概况
*/
private function render_presale_overview() {
global $wpdb;
// 获取统计数据
$total_presale_products = $wpdb->get_var("
SELECT COUNT(*)
FROM {$wpdb->postmeta}
WHERE meta_key = '_is_presale'
AND meta_value = 'yes'
");
$active_presales = $wpdb->get_var($wpdb->prepare("
SELECT COUNT(DISTINCT p.ID)
FROM {$wpdb->posts} p
JOIN {$wpdb->postmeta} pm1 ON p.ID = pm1.post_id
JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id
WHERE p.post_type = 'product'
AND p.post_status = 'publish'
AND pm1.meta_key = '_is_presale'
AND pm1.meta_value = 'yes'
AND pm2.meta_key = '_presale_end_date'
AND pm2.meta_value > %s
", current_time('mysql')));
$total_presale_orders = $wpdb->get_var("
SELECT SUM(oi.meta_value)
FROM {$wpdb->prefix}woocommerce_order_itemmeta oim
JOIN {$wpdb->prefix}woocommerce_order_items oi ON oim.order_item_id = oi.order_item_id
JOIN {$wpdb->posts} p ON oi.order_id = p.ID
JOIN {$wpdb->postmeta} pm ON oim.meta_value = pm.post_id
WHERE oim.meta_key = '_product_id'
AND p.post_status IN ('wc-processing', 'wc-completed')
AND pm.meta_key = '_is_presale'
AND pm.meta_value = 'yes'
");
?>
<div class="presale-stats">
<div class="stat-item">
<span class="stat-value"><?php echo esc_html($total_presale_products); ?></span>
<span class="stat-label"><?php _e('预售商品总数', 'flex-presale'); ?></span>
</div>
<div class="stat-item">
<span class="stat-value"><?php echo esc_html($active_presales); ?></span>
<span class="stat-label"><?php _e('进行中预售', 'flex-presale'); ?></span>
</div>
<div class="stat-item">
<span class="stat-value"><?php echo esc_html($total_presale_orders ?: 0); ?></span>
<span class="stat-label"><?php _e('总预售数量', 'flex-presale'); ?></span>
</div>
</div>
<?php
}
/**
* 渲染库存预警
*/
private function render_inventory_alerts() {
$args = array(
'post_type' => 'product',
'posts_per_page' => 5,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_is_presale',
'value' => 'yes',
'compare' => '='
),
array(
'key' => '_stock',
'value' => 10,
'compare' => '<=',
'type' => 'NUMERIC'
)
)
);
$products = get_posts($args);
if (empty($products)) {
echo '<p>' . __('暂无库存预警', 'flex-presale') . '</p>';
return;
}
echo '<div class="alert-list">';
foreach ($products as $product_post) {
$product = wc_get_product($product_post->ID);
$stock = $product->get_stock_quantity();
$alert_class = $stock <= 5 ? 'critical' : '';
echo sprintf(
'<div class="alert-item %s">%s - %s: %d</div>',
$alert_class,
esc_html($product->get_sku()),
esc_html($product->get_name()),
$stock
);
}
echo '</div>';
}
/**
* 渲染近期预售活动
*/
private function render_recent_presales() {
$args = array(
'post_type' => 'product',
'posts_per_page' => 10,
'meta_key' => '_presale_end_date',
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_query' => array(
array(
'key' => '_is_presale',
'value' => 'yes',
'compare' => '='
),
array(
'key' => '_presale_end_date',
'value' => current_time('mysql'),
'compare' => '>',
'type' => 'DATETIME'
)
)
);
$products = get_posts($args);
if (empty($products)) {
echo '<p>' . __('暂无进行中的预售活动', 'flex-presale') . '</p>';
return;
}
echo '<table class="presale-table">';
echo '<thead>
<tr>
<th>' . __('商品', 'flex-presale') . '</th>
<th>' . __('预售结束时间', 'flex-presale') . '</th>
<th>' . __('目标数量', 'flex-presale') . '</th>
<th>' . __('当前进度', 'flex-presale') . '</th>
<th>' . __('库存', 'flex-presale') . '</th>
</tr>
</thead>';
echo '<tbody>';
foreach ($products as $product_post) {
$product = wc_get_product($product_post->ID);
$end_date = get_post_meta($product_post->ID, '_presale_end_date', true);
$target = get_post_meta($product_post->ID, '_presale_target_qty', true);
$current_orders = $this->get_presale_order_count($product_post->ID);
$progress = $target > 0 ? min(100, ($current_orders / $target) * 100) : 0;
echo '<tr>';
echo '<td><strong>' . esc_html($product->get_name()) . '</strong><br><small>' . esc_html($product->get_sku()) . '</small></td>';
echo '<td>' . ($end_date ? date_i18n(get_option('date_format'), strtotime($end_date)) : '-') . '</td>';
echo '<td>' . esc_html($target ?: '-') . '</td>';
echo '<td class="progress-cell">
' . esc_html($current_orders) . '
<div class="progress-bar-small">
<div class="progress-fill-small" style="width: ' . esc_attr($progress) . '%;"></div>
</div>
</td>';
echo '<td>' . esc_html($product->get_stock_quantity()) . '</td>';
echo '</tr>';
}
echo '</tbody></table>';
}
/**
* 添加商品列表列
*/
public function add_product_columns($columns) {
$new_columns = array();
foreach ($columns as $key => $value) {
$new_columns[$key] = $value;
if ($key === 'name') {
$new_columns['presale_status'] = __('预售状态', 'flex-presale');
$new_columns['presale_progress'] = __('预售进度', 'flex-presale');
}
}
return $new_columns;
}
/**
* 渲染商品列表列
*/
public function render_product_columns($column, $product_id) {
if ($column === 'presale_status') {
$is_presale = get_post_meta($product_id, '_is_presale', true);
if ($is_presale === 'yes') {
$end_date = get_post_meta($product_id, '_pres


