WP 插件开发实战:自定义数据表 + CRUD + 后台列表页

WP 插件开发实战:自定义数据表 + CRUD + 后台列表页(含完整示例代码)

很多 WordPress 插件写到后面都会遇到同一个现实:options 只适合存少量配置,不适合存“记录型数据”(例如:线索、日志、任务队列、表单提交、同步结果)。一旦你开始把大量记录塞进 postmetaoptions,性能和维护都会越来越痛苦。

这篇文章我写一个“最小可用”的记录型插件:创建自定义表 wp_mwt_leads,提供后台菜单页,支持新增、删除、分页列表。重点还是:结构、规范、安全和可维护性。


1)插件目标与结构

目标:

  • 插件激活时创建数据表
  • 后台菜单“Leads”,展示列表(分页)
  • 可新增记录(表单 + nonce)
  • 可删除记录(GET 操作 + nonce)

目录结构建议:

mwt-leads/
  mwt-leads.php

(先单文件实现,后续再拆类更清晰)


2)完整示例代码(可直接运行)

新建:wp-content/plugins/mwt-leads/mwt-leads.php

```php prefix . 'mwt_leads'; $charset_collate = $wpdb->get_charset_collate();

// 用 dbDelta 创建/升级表结构
require_once ABSPATH . 'wp-admin/includes/upgrade.php';

$sql = "CREATE TABLE $table ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(120) NOT NULL, email VARCHAR(190) NOT NULL, note TEXT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY email (email) ) $charset_collate;";

dbDelta($sql);

add_option('mwt_leads_db_version', $mwt_leads_db_version);

}

add_action('admin_menu', function() { add_menu_page( 'MWT Leads', 'MWT Leads', 'manage_options', 'mwt-leads', 'mwt_leads_render_page', 'dashicons-id', 56 ); });

function mwt_leads_render_page() { if (!current_user_can('manage_options')) return;

global $wpdb;
$table = $wpdb->prefix . 'mwt_leads';

// 1) 处理新增
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mwt_action']) && $_POST['mwt_action'] === 'add') {
    check_admin_referer('mwt_leads_add');

    $name  = sanitize_text_field($_POST['name'] ?? '');
    $email = sanitize_email($_POST['email'] ?? '');
    $note  = sanitize_textarea_field($_POST['note'] ?? '');

    if ($name && $email) {
        $wpdb->insert(
            $table,
            [
                'name' => $name,
                'email' => $email,
                'note' => $note,
                'created_at' => current_time('mysql')
            ],
            ['%s','%s','%s','%s']
        );
        echo '<div class="notice notice-success"><p>Lead 已添加。</p></div>';
    } else {
        echo '<div class="notice notice-error"><p>name 和 email 不能为空。</p></div>';
    }
}

// 2) 处理删除
if (isset($_GET['action'], $_GET['lead_id']) && $_GET['action'] === 'delete') {
    $lead_id = absint($_GET['lead_id']);
    check_admin_referer('mwt_leads_delete_' . $lead_id);

    if ($lead_id > 0) {
        $wpdb->delete($table, ['id' => $lead_id], ['%d']);
        echo '<div class="notice notice-success"><p>Lead 已删除。</p></div>';
    }
}

// 3) 分页查询
$per_page = 10;
$paged = max(1, absint($_GET['paged'] ?? 1));
$offset = ($paged - 1) * $per_page;

$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table");
$rows  = $wpdb->get_results($wpdb->prepare(
    "SELECT * FROM $table ORDER BY id DESC LIMIT %d OFFSET %d",
    $per_page,
    $offset
));

$total_pages = (int) ceil($total / $per_page);

?>
<div class="wrap">
    <h1>MWT Leads</h1>

    <h2>新增 Lead</h2>
    <form method="post">

        <input type="hidden" name="mwt_action" value="add" />

        <table class="form-table" role="presentation">
            <tr>
                <th scope="row"><label for="name">Name</label></th>
                <td><input class="regular-text" id="name" name="name" type="text" required></td>
            </tr>
            <tr>
                <th scope="row"><label for="email">Email</label></th>
                <td><input class="regular-text" id="email" name="email" type="email" required></td>
            </tr>
            <tr>
                <th scope="row"><label for="note">Note</label></th>
                <td><textarea class="large-text" id="note" name="note" rows="3"></textarea></td>
            </tr>
        </table>


    </form>

    <hr />

    <h2>列表()</h2>
    <table class="widefat fixed striped">
        <thead>
            <tr>
                <th width="60">ID</th>
                <th>Name</th>
                <th>Email</th>
                <th>Note</th>
                <th width="160">Created</th>
                <th width="90">Action</th>
            </tr>
        </thead>
        <tbody>

                <tr><td colspan="6">暂无数据</td></tr>


                    <tr>
                        <td>id; ?></td>
                        <td>name); ?></td>
                        <td>email); ?></td>
                        <td>note, 12, '...')); ?></td>
                        <td>created_at); ?></td>
                        <td>
                            id),
                                'mwt_leads_delete_' . (int)$r->id
                            );
                            ?>
                            <a href="&lt;?php echo esc_url($del_url); ?>">Delete</a>
                        &lt;/td&gt;
                    &lt;/tr&gt;


        &lt;/tbody&gt;
    &lt;/table&gt;

     1): ?&gt;
        &lt;div class="tablenav"&gt;
            &lt;div class="tablenav-pages"&gt;
                 $base,
                    'format'    =&gt; '&amp;paged=%#%',
                    'current'   =&gt; $paged,
                    'total'     =&gt; $total_pages,
                    'prev_text' =&gt; '«',
                    'next_text' =&gt; '»',
                ]);
                ?&gt;
            &lt;/div&gt;
        &lt;/div&gt;


    &lt;p style="color:#666;margin-top:12px;"&gt;
        说明:这是最小可用示例,真实项目建议用 WP_List_Table 封装列表与批量操作。
    &lt;/p&gt;
&lt;/div&gt;

评论 0