星期一 , 14 4 月 2025

通过csv上传产品分类并解析上下层级

<?php
/**
* Plugin Name: Product Category Importer
* Description: Import product categories from CSV into WordPress custom taxonomy with parent-child relationships
* Version: 1.0.2
* Author: Your Name
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
class Product_Category_Importer {
private $taxonomy = 'product_category';
private $upload_dir = '';
private $notices = array();
private $id_term_map = array(); // Map original IDs to WordPress term IDs
public function __construct() {
// Set upload directory
$upload_dir = wp_upload_dir();
$this->upload_dir = $upload_dir['basedir'] . '/product-category-imports/';
// Create directory if it doesn't exist
if (!file_exists($this->upload_dir)) {
wp_mkdir_p($this->upload_dir);
// Create an index.php file to prevent directory listing
file_put_contents($this->upload_dir . 'index.php', '<?php // Silence is golden.');
}
// Register hooks
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_init', array($this, 'handle_form_submission'));
add_action('admin_notices', array($this, 'display_admin_notices'));
add_action('init', array($this, 'register_term_meta'));
}
/**
* Register custom term meta field
*/
public function register_term_meta() {
register_meta('term', 'pc_content', array(
'type' => 'string',
'description' => 'Product category detailed content',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'wp_kses_post',
));
register_meta('term', 'pc_level', array(
'type' => 'number',
'description' => 'Product category term level',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'wp_kses_post',
));
register_meta('term', 'pc_meta_title', array(
'type' => 'string',
'description' => 'Product category meta title',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'wp_kses_post',
));
register_meta('term', 'pc_meta_description', array(
'type' => 'string',
'description' => 'Product category meta description',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'wp_kses_post',
));
register_meta('term', 'pc_thumbs', array(
'type' => 'string',
'description' => 'Product category thumbnails',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'wp_kses_post',
));
register_meta('term', 'pc_fields', array(
'type' => 'string',
'description' => 'Product category meta fields',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'wp_kses_post',
));
register_meta('term', 'pc_original_id', array(
'type' => 'string',
'description' => 'Original category ID from import',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
));
}
/**
* Add the admin menu item
*/
public function add_admin_menu() {
add_submenu_page(
'tools.php',
'Import Product Categories',
'Import Product Categories',
'manage_options',
'product-category-importer',
array($this, 'render_admin_page')
);
}
/**
* Display admin notices
*/
public function display_admin_notices() {
foreach ($this->notices as $notice) {
echo '<div class="notice notice-' . esc_attr($notice['type']) . ' is-dismissible"><p>' . wp_kses_post($notice['message']) . '</p></div>';
}
}
/**
* Add a notice message
*/
private function add_notice($message, $type = 'success') {
$this->notices[] = array(
'message' => $message,
'type' => $type
);
}
/**
* Handle form submissions
*/
public function handle_form_submission() {
// Only run on our admin page
if (!isset($_GET['page']) || $_GET['page'] !== 'product-category-importer') {
return;
}
// File upload handling
if (isset($_POST['upload_csv']) && check_admin_referer('upload_csv_nonce', 'upload_csv_nonce')) {
if (!empty($_FILES['csv_file']['name'])) {
// Validate file type
$file_info = wp_check_filetype(basename($_FILES['csv_file']['name']), array('csv' => 'text/csv'));
if (empty($file_info['ext'])) {
$this->add_notice('Error: Please upload a valid CSV file.', 'error');
return;
}
// Handle the upload
$upload_result = $this->handle_file_upload();
if (is_wp_error($upload_result)) {
$this->add_notice('Error: ' . $upload_result->get_error_message(), 'error');
} else {
$this->add_notice('CSV file uploaded successfully. You can now import the categories.', 'success');
}
} else {
$this->add_notice('Error: Please select a CSV file to upload.', 'error');
}
}
// Import processing
if (isset($_POST['process_import']) && check_admin_referer('process_import_nonce', 'process_import_nonce')) {
if (isset($_POST['csv_file_path']) && !empty($_POST['csv_file_path'])) {
$file_path = sanitize_text_field($_POST['csv_file_path']);
// Verify file exists and is in our upload directory
if (!file_exists($file_path)) {
$this->add_notice('Error: File does not exist.', 'error');
return;
}
// Additional security check - ensure the file is in our directory
$real_upload_dir = realpath($this->upload_dir);
$real_file_path = realpath($file_path);
if (!$real_file_path || strpos($real_file_path, $real_upload_dir) !== 0) {
$this->add_notice('Error: Invalid file location.', 'error');
return;
}
$import_result = $this->import_categories($file_path);
if (is_wp_error($import_result)) {
$this->add_notice('Import failed: ' . $import_result->get_error_message(), 'error');
} else {
$this->add_notice(
sprintf(
'Import completed: %d terms added, %d terms updated, %d parent relationships set, %d errors.',
$import_result['imported'],
$import_result['updated'],
$import_result['parent_relationships'],
$import_result['errors']
),
'success'
);
}
} else {
$this->add_notice('Error: No CSV file selected for import.', 'error');
}
}
}
/**
* Handle file upload
*/
private function handle_file_upload() {
// Security checks
if (!function_exists('wp_handle_upload')) {
require_once(ABSPATH . 'wp-admin/includes/file.php');
}
$upload_overrides = array(
'test_form' => false,
'test_type' => true,
'mimes' => array('csv' => 'text/csv')
);
// Process the upload
$uploaded_file = wp_handle_upload($_FILES['csv_file'], $upload_overrides);
if (isset($uploaded_file['error'])) {
return new WP_Error('upload_error', $uploaded_file['error']);
}
if (!isset($uploaded_file['file']) || !file_exists($uploaded_file['file'])) {
return new WP_Error('upload_error', 'Upload failed. File not found.');
}
// Move to our plugin's directory for better management
$file_name = basename($uploaded_file['file']);
$new_file_path = $this->upload_dir . $file_name;
// Ensure directory exists before copying
if (!is_dir($this->upload_dir)) {
if (!wp_mkdir_p($this->upload_dir)) {
return new WP_Error('directory_error', 'Failed to create upload directory.');
}
}
// Copy instead of move to avoid permission issues
if (@copy($uploaded_file['file'], $new_file_path)) {
@unlink($uploaded_file['file']); // Try to remove the original file
return $new_file_path;
} else {
return new WP_Error('move_error', 'Failed to copy uploaded file to plugin directory.');
}
}
/**
* List uploaded CSV files
*/
private function get_uploaded_files() {
$files = array();
if (is_dir($this->upload_dir)) {
$dir_files = scandir($this->upload_dir);
if ($dir_files) {
foreach ($dir_files as $file) {
$file_path = $this->upload_dir . $file;
if ($file !== '.' && $file !== '..' && pathinfo($file, PATHINFO_EXTENSION) === 'csv' && file_exists($file_path)) {
$files[] = array(
'name' => $file,
'path' => $file_path,
'date' => date('Y-m-d H:i:s', filemtime($file_path))
);
}
}
}
}
// Sort by newest first
usort($files, function($a, $b) {
return strtotime($b['date']) - strtotime($a['date']);
});
return $files;
}
/**
* Import categories from CSV
*/
private function import_categories($file_path) {
// Check if file exists
if (!file_exists($file_path)) {
return new WP_Error('file_not_found', 'CSV file not found.');
}
// Check if taxonomy exists
if (!taxonomy_exists($this->taxonomy)) {
return new WP_Error('taxonomy_not_exists', "The taxonomy '{$this->taxonomy}' does not exist.");
}
// Open the file
$handle = fopen($file_path, 'r');
if (!$handle) {
return new WP_Error('file_open_failed', 'Could not open the CSV file.');
}
// Get headers
$headers = fgetcsv($handle, 0, ',');
if (!$headers) {
fclose($handle);
return new WP_Error('invalid_csv', 'Could not parse CSV headers.');
}
// Find the indices of the columns we need
$id_index = array_search('id', $headers);
$pid_index = array_search('pid', $headers);
$title_index = array_search('title', $headers);
$name_index = array_search('name', $headers);
$content_index = array_search('content', $headers);
$level_index = array_search('level', $headers);
$pagetitle_index = array_search('pagetitle', $headers);
$description_index = array_search('description', $headers);
$thumbs_index = array_search('thumbs', $headers);
$field_index = array_search('field', $headers);
if ($title_index === false || $name_index === false || $id_index === false) {
fclose($handle);
return new WP_Error('missing_columns', 'Required columns (id, title, name) not found in CSV. Found: ' . implode(', ', $headers));
}
$stats = array(
'imported' => 0,
'updated' => 0,
'parent_relationships' => 0,
'errors' => 0
);
// Reset the ID to term mapping
$this->id_term_map = array();
// First pass: create or update all terms
$categories_to_process = array();
// Rewind the file to just after the headers
rewind($handle);
fgetcsv($handle); // Skip headers
// First pass: Create all terms without setting parent relationships
while (($data = fgetcsv($handle, 0, ',')) !== false) {
// Skip if required data is missing or row doesn't have enough columns
if (!isset($data[$id_index]) || !isset($data[$title_index]) || !isset($data[$name_index]) || 
empty($data[$id_index]) || empty($data[$title_index]) || empty($data[$name_index])) {
$stats['errors']++;
continue;
}
$original_id = sanitize_text_field($data[$id_index]);
$parent_id = ($pid_index !== false && isset($data[$pid_index])) ? sanitize_text_field($data[$pid_index]) : '';
$term_name = sanitize_text_field($data[$title_index]);
$term_slug = sanitize_title($data[$name_index]);
$term_content = isset($data[$content_index]) ? $data[$content_index] : '';
$term_level = isset($data[$level_index]) ? $data[$level_index] : '';
$term_pagetitle = isset($data[$pagetitle_index]) ? $data[$pagetitle_index] : '';
$term_description = isset($data[$description_index]) ? $data[$description_index] : ''; 
$term_thumbs = isset($data[$thumbs_index]) ? $data[$thumbs_index] : '';  
$term_field = isset($data[$field_index]) ? $data[$field_index] : ''; 
// Check if the term already exists
$existing_term = term_exists($term_slug, $this->taxonomy);
if ($existing_term) {
// Update existing term
$term_id = $existing_term['term_id'];
$update_result = wp_update_term($term_id, $this->taxonomy, array(
'name' => $term_name,
'slug' => $term_slug,
'description' => $term_description
));
if (!is_wp_error($update_result)) {
// Update custom fields
update_term_meta($term_id, 'pc_content', $term_content);
update_term_meta($term_id, 'pc_level', $term_level);
update_term_meta($term_id, 'pc_meta_title', $term_pagetitle);
update_term_meta($term_id, 'pc_meta_description', $term_description);
update_term_meta($term_id, 'pc_thumbs', $term_thumbs);
update_term_meta($term_id, 'pc_fields', $term_field);
update_term_meta($term_id, 'pc_original_id', $original_id);
// Map the original ID to the WordPress term ID
$this->id_term_map[$original_id] = $term_id;
// Store for second pass to set parent
$categories_to_process[] = array(
'term_id' => $term_id,
'parent_id' => $parent_id
);
$stats['updated']++;
} else {
$stats['errors']++;
}
} else {
// Create new term (initially without parent)
$term = wp_insert_term($term_name, $this->taxonomy, array(
'slug' => $term_slug,
'description' => $term_description
));
if (!is_wp_error($term)) {
$term_id = $term['term_id'];
// Add custom fields
add_term_meta($term_id, 'pc_content', $term_content, true);
add_term_meta($term_id, 'pc_level', $term_level, true);
add_term_meta($term_id, 'pc_meta_title', $term_pagetitle, true);
add_term_meta($term_id, 'pc_meta_description', $term_description, true);
add_term_meta($term_id, 'pc_thumbs', $term_thumbs, true);
add_term_meta($term_id, 'pc_fields', $term_field, true);
add_term_meta($term_id, 'pc_original_id', $original_id, true);
// Map the original ID to the WordPress term ID
$this->id_term_map[$original_id] = $term_id;
// Store for second pass to set parent
$categories_to_process[] = array(
'term_id' => $term_id,
'parent_id' => $parent_id
);
$stats['imported']++;
} else {
$stats['errors']++;
}
}
}
fclose($handle);
// Second pass: Set up parent-child relationships
foreach ($categories_to_process as $category) {
$term_id = $category['term_id'];
$parent_id = $category['parent_id'];
// Skip if no parent ID or if it's zero/empty (root level)
if (empty($parent_id) || $parent_id === '0') {
continue;
}
// If the parent ID exists in our mapping
if (isset($this->id_term_map[$parent_id])) {
$wp_parent_id = $this->id_term_map[$parent_id];
// Update the term to set the parent
$update_result = wp_update_term($term_id, $this->taxonomy, array(
'parent' => $wp_parent_id
));
if (!is_wp_error($update_result)) {
$stats['parent_relationships']++;
} else {
$stats['errors']++;
}
}
}
return $stats;
}
/**
* Render the admin page
*/
public function render_admin_page() {
// Get uploaded files
$uploaded_files = $this->get_uploaded_files();
?>
<div class="wrap">
<h1>Import Product Categories</h1>
<?php
// Check if the custom taxonomy exists
if (!taxonomy_exists($this->taxonomy)) {
echo '<div class="notice notice-error"><p>The product_category taxonomy does not exist. Please create it first.</p></div>';
return;
}
?>
<div class="card" style="padding: 15px; margin-top: 15px;">
<h2>Step 1: Upload CSV File</h2>
<p>Upload a CSV file containing your product categories. The file must include 'id', 'title', and 'name' columns. Include a 'pid' column for parent-child relationships.</p>
<form method="post" enctype="multipart/form-data">
<?php wp_nonce_field('upload_csv_nonce', 'upload_csv_nonce'); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="csv_file">Select CSV File</label></th>
<td>
<input type="file" name="csv_file" id="csv_file" accept=".csv" required>
<p class="description">File must be in CSV format</p>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="upload_csv" class="button button-primary" value="Upload CSV">
</p>
</form>
</div>
<?php if (!empty($uploaded_files)): ?>
<div class="card" style="padding: 15px; margin-top: 20px;">
<h2>Step 2: Process Import</h2>
<p>Select a CSV file to import from the list below.</p>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>File Name</th>
<th>Upload Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($uploaded_files as $file): ?>
<tr>
<td><?php echo esc_html($file['name']); ?></td>
<td><?php echo esc_html($file['date']); ?></td>
<td>
<form method="post">
<?php wp_nonce_field('process_import_nonce', 'process_import_nonce'); ?>
<input type="hidden" name="csv_file_path" value="<?php echo esc_attr($file['path']); ?>">
<button type="submit" name="process_import" class="button button-secondary">Import</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<div class="card" style="padding: 15px; margin-top: 20px;">
<h2>CSV Format Requirements</h2>
<p>Your CSV file should include the following columns:</p>
<ul style="list-style-type: disc; margin-left: 20px;">
<li><strong>id</strong> - Unique identifier for the category</li>
<li><strong>pid</strong> - Parent category ID (leave empty or 0 for top-level categories)</li>
<li><strong>title</strong> - Will be used as the term name</li>
<li><strong>name</strong> - Will be used as the term slug</li>
<li><strong>content</strong> - Will be added as custom field 'pc_content'</li>
</ul>
<p>Example:</p>
<pre style="background: #f5f5f5; padding: 10px; overflow: auto;">id,pid,title,name,content
1,0,"Electronics","electronics","&lt;h5&gt;Electronics category&lt;/h5&gt;"
2,1,"Mobile Phones","mobile-phones","&lt;p&gt;Mobile phones subcategory&lt;/p&gt;"
3,1,"Laptops","laptops","&lt;p&gt;Laptops subcategory&lt;/p&gt;"
4,2,"Android Phones","android-phones","&lt;p&gt;Android phones subcategory&lt;/p&gt;"</pre>
</div>
</div>
<?php
}
}
// Initialize the plugin
new Product_Category_Importer();