卡尼奶资源商店

插件购买校验接口接入说明

这个页面用于展示资源商店的插件购买校验教程,帮助其它卡尼奶插件通过商品密钥、域名和主题授权码判断当前站点是否有权使用对应插件。

接口说明

这个接口用于其它卡尼奶插件校验当前插件能不能使用。接口会先通过 product_key 商品密钥找到资源商店商品:如果商品是免费资源,直接返回 allowed: true;如果商品是付费资源,再用 domain + license_key 找到当前站点授权码对应的卡尼奶账号,并查询这个账号是否购买过该密钥对应商品。

只有接口返回 allowed: true 时,插件才允许继续使用;否则插件应停止核心功能并提示用户购买。

后台配置

在资源商店后台添加或编辑资源商品时,填写“商品密钥”:

  • 可以手动输入,例如 knnrs_merchant_settlement_2026。
  • 也可以点击“生成密钥”自动填充。
  • 每个付费插件应该使用一个唯一商品密钥。
  • 插件源码里携带这个商品密钥,接口会用它找到对应资源商品。为了降低源码里直接搜索密钥的风险,可以像下面示例一样把密钥 base64 保存,运行时再还原成 product_key 传给接口。
  • “最新版本号”用于插件端判断是否有新版本,例如 1.0.13。
  • “更新包地址”用于 WordPress 自动更新下载 zip 安装包。这个地址只会在购买校验通过、确实有新版本、且后台已填写更新包地址时返回给插件端。

接口地址

POST https://www.kaninai.cn/wp-json/knn-store/v1/license/check-purchase

正式插件建议使用 POST,避免授权码出现在浏览器地址或日志里。

请求参数

参数 必填 说明
product_key 是 资源商品后台填写的“商品密钥”。
domain 付费必填 当前网站域名,例如 www.example.com。接口会自动兼容带不带 www。免费资源可不传。
license_key 付费必填 用户在当前网站填写的卡尼奶主题授权码。也兼容参数名 auth_key。免费资源可不传。
version 否 当前插件版本号。

返回示例

已购买:

{
  "success": true,
  "allowed": true,
  "purchased": true,
  "code": "purchased",
  "message": "账号已购买该插件,可以使用",
  "product": { "id": 123, "title": "某某插件", "status": 1, "is_free": false },
  "account": { "id": 1, "display_name": "卡尼奶" },
  "order": { "order_no": "RS202609101234560001", "paid_at": "2026-09-10 12:34:56", "amount": "99.00" }
}

未购买:

{
  "success": true,
  "allowed": false,
  "purchased": false,
  "code": "not_purchased",
  "message": "该授权码对应账号未购买该插件,无法使用"
}

免费资源:

{
  "success": true,
  "allowed": true,
  "purchased": true,
  "code": "free_product",
  "message": "该插件为免费资源,无需购买即可使用"
}

curl 调用示例

curl -X POST "https://www.kaninai.cn/wp-json/knn-store/v1/license/check-purchase" \
  -H "Content-Type: application/json" \
  -d '{"domain":"www.example.com","license_key":"你的主题授权码","product_key":"knnrs_cUucTKpuaB3S7ymQN2HDVn2MhShRQxfGt6FM8LHucmXqJ5k8","version":"1.0.0"}'

其它插件 PHP 接入示例

建议在插件启用时校验一次。校验通过后保存本地授权状态;校验失败就立即停用当前插件并提示用户。插件正常运行时只读取本地授权状态,不要每次页面加载都远程请求。

下面示例演示的是付费插件接入方式。KNN_DEMO_RESOURCE_TOKEN 里保存的是商品密钥的 base64 结果,运行时通过 base64_decode() 还原后作为 product_key 传给接口。这样做只是降低源码里直接搜索密钥的风险,不是强加密;真正判断能不能用,仍然以后端接口根据“域名 + 主题授权码 + 商品密钥 + 购买订单”为准。

当前示例密钥:knnrs_cUucTKpuaB3S7ymQN2HDVn2MhShRQxfGt6FM8LHucmXqJ5k8

当前示例密钥 base64:a25ucnNfY1V1Y1RLcHVhQjNTN3ltUU4ySERWbjJNaFNoUlF4Zkd0NkZNOExIdWNtWHFKNWs4

生成 base64 可以用下面命令,把里面的商品密钥换成当前插件自己的密钥:

php -r "echo base64_encode('knnrs_cUucTKpuaB3S7ymQN2HDVn2MhShRQxfGt61266126515');"
<?php
/**
 * Plugin Name: 示例付费插件
 */

if (!defined('ABSPATH')) {
    exit;
}

define('KNN_DEMO_RESOURCE_SOURCE', 'https://www.kaninai.cn/wp-json/knn-store/v1/license/check-purchase');
define('KNN_DEMO_RESOURCE_TOKEN', 'a25ucnNfY1V1Y1RLcHVhQjNTN3ltUU4ySERWbjJNaFNoUlF4Zkd0NkZNOExIdWNtWHFKNWs4');
define('KNN_DEMO_RESOURCE_PLUGIN_VERSION', '1.0.0');
define('KNN_DEMO_RESOURCE_AUTH_OPTION', 'knn_demo_purchase_auth_status');

function knn_demo_get_product_key() {
    return (string) base64_decode(KNN_DEMO_RESOURCE_TOKEN);
}

function knn_demo_get_theme_license() {
    if (function_exists('knn_theme_get_license_data')) {
        $data = knn_theme_get_license_data();
        if (is_array($data)) {
            return $data;
        }
    }

    $data = get_option('knn_theme_license', array());
    return is_array($data) ? $data : array();
}

function knn_demo_get_current_domain($license = array()) {
    $domain = '';
    if (is_array($license) && !empty($license['license_domain'])) {
        $domain = (string) $license['license_domain'];
    }
    if ($domain === '') {
        $domain = (string) wp_parse_url(home_url('/'), PHP_URL_HOST);
    }
    if ($domain === '' && !empty($_SERVER['HTTP_HOST'])) {
        $domain = (string) $_SERVER['HTTP_HOST'];
    }
    return strtolower(trim(preg_replace('/:\d+$/', '', $domain)));
}

function knn_demo_remote_check_purchase() {
    $license = knn_demo_get_theme_license();
    $license_key = isset($license['license_key']) ? trim((string) $license['license_key']) : '';
    if ($license_key === '') {
        return new WP_Error('missing_license', '请先填写卡尼奶主题授权码');
    }

    $product_key = knn_demo_get_product_key();
    if ($product_key === '') {
        return new WP_Error('missing_product_key', '插件商品密钥为空,无法校验购买状态');
    }

    $response = wp_remote_post(KNN_DEMO_RESOURCE_SOURCE, array(
        'timeout' => 12,
        'headers' => array(
            'Content-Type' => 'application/json',
        ),
        'body' => wp_json_encode(array(
            'domain' => knn_demo_get_current_domain($license),
            'license_key' => $license_key,
            'product_key' => $product_key,
            'version' => KNN_DEMO_RESOURCE_PLUGIN_VERSION,
        )),
    ));

    if (is_wp_error($response)) {
        return $response;
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    if (empty($body['success']) || empty($body['allowed'])) {
        $message = !empty($body['message']) ? $body['message'] : '该账号未购买此插件,无法启用';
        return new WP_Error('not_purchased', $message);
    }

    return $body;
}

function knn_demo_activate_check_purchase() {
    $result = knn_demo_remote_check_purchase();
    if (is_wp_error($result)) {
        deactivate_plugins(plugin_basename(__FILE__));
        wp_die(
            esc_html($result->get_error_message()),
            '插件购买校验失败',
            array('back_link' => true)
        );
    }

    update_option(KNN_DEMO_RESOURCE_AUTH_OPTION, array(
        'allowed' => true,
        'checked_at' => time(),
        'domain' => knn_demo_get_current_domain(knn_demo_get_theme_license()),
        'product_key_hash' => md5(knn_demo_get_product_key()),
    ), false);
}
register_activation_hook(__FILE__, 'knn_demo_activate_check_purchase');

function knn_demo_is_authorized() {
    $status = get_option(KNN_DEMO_RESOURCE_AUTH_OPTION, array());
    return is_array($status)
        && !empty($status['allowed'])
        && !empty($status['product_key_hash'])
        && hash_equals((string) $status['product_key_hash'], md5(knn_demo_get_product_key()));
}

add_action('admin_notices', function () {
    if (!current_user_can('manage_options') || knn_demo_is_authorized()) {
        return;
    }

    echo '<div class="notice notice-error"><p>该插件未完成购买校验,请停用后重新启用插件进行校验。</p></div>';
});

add_action('plugins_loaded', function () {
    if (!knn_demo_is_authorized()) {
        return;
    }

    // 已购买并通过启用校验后,才加载插件核心功能。
    // require_once __DIR__ . '/includes/core.php';
});
如果你希望后台有“重新校验授权”按钮,可以在按钮回调里再次调用 knn_demo_remote_check_purchase(),通过后更新 KNN_DEMO_RESOURCE_AUTH_OPTION。

插件更新接口

这个接口用于 WordPress 插件列表里的原生更新检查。插件端把当前版本号传给资源商店,资源商店用后台商品的“最新版本号”判断是否需要更新;如果当前授权允许使用、后台填写了“更新包地址”、并且最新版本号大于当前插件版本,就返回可安装的 zip 地址。

接口地址:

POST https://www.kaninai.cn/wp-json/knn-store/v1/license/update-info

请求参数和购买校验接口一致:

参数 必填 说明
product_key 是 资源商品后台填写的“商品密钥”。
domain 付费必填 当前网站域名。免费资源可不传。
license_key 付费必填 用户在当前网站填写的卡尼奶主题授权码。也兼容参数名 auth_key。免费资源可不传。
version 是 当前插件版本号,用来和后台“最新版本号”比较。

有更新返回示例:

{
  "success": true,
  "allowed": true,
  "code": "purchased",
  "message": "已获取插件更新信息",
  "version": "1.0.12",
  "latest_version": "1.0.13",
  "update_available": true,
  "package": "https://www.example.com/plugin.zip",
  "product": { "id": 123, "title": "某某插件", "status": 1, "is_free": false },
  "checked_at": "2026-09-17 10:00:00"
}

没有更新或没有更新包地址时,update_available 为 false,package 为空:

{
  "success": true,
  "allowed": true,
  "code": "purchased",
  "version": "1.0.13",
  "latest_version": "1.0.13",
  "update_available": false,
  "package": ""
}

curl 调用示例:

curl -X POST "https://www.kaninai.cn/wp-json/knn-store/v1/license/update-info" \
  -H "Content-Type: application/json" \
  -d '{"domain":"www.example.com","license_key":"你的主题授权码","product_key":"knnrs_cUucTKpuaB3S7ymQN2HDVn2MhShRQxfGt6FM8LHucmXqJ5k8","version":"1.0.12"}'

WordPress 插件端接入更新

第一步:声明更新源。主插件文件必须填写版本号和 Update URI。地址主机名 www.kaninai.cn 对应动态过滤器 update_plugins_www.kaninai.cn。

/**
 * Plugin Name: 示例插件
 * Version: 1.0.0
 * Update URI: https://www.kaninai.cn/wp-json/knn-store/v1/license/update-info
 */

define('KNN_DEMO_PLUGIN_VERSION', '1.0.0');
define('KNN_DEMO_PLUGIN_FILE', __FILE__);
define('KNN_DEMO_UPDATE_SOURCE', 'https://www.kaninai.cn/wp-json/knn-store/v1/license/update-info');

第二步:请求接口并转换为 WordPress 更新对象。下面代码复用前文的商品密钥、主题授权和域名读取函数。

function knn_demo_update_cache_key() {
    return 'knn_demo_update_info_' . md5(KNN_DEMO_PLUGIN_VERSION . '|' . home_url('/'));
}

function knn_demo_remote_update_info($force = false) {
    $cache_key = knn_demo_update_cache_key();
    if (!$force) {
        $cached = get_site_transient($cache_key);
        if (is_array($cached)) return $cached;
    }

    $license = knn_demo_get_theme_license();
    $response = wp_remote_post(KNN_DEMO_UPDATE_SOURCE, array(
        'timeout' => 10,
        'headers' => array('Content-Type' => 'application/json'),
        'body' => wp_json_encode(array(
            'domain' => knn_demo_get_current_domain($license),
            'license_key' => (string) ($license['license_key'] ?? ''),
            'product_key' => knn_demo_get_product_key(),
            'version' => KNN_DEMO_PLUGIN_VERSION,
        )),
    ));
    if (is_wp_error($response)) return $response;

    $body = json_decode(wp_remote_retrieve_body($response), true);
    if (!is_array($body) || empty($body['success'])) {
        return new WP_Error('update_check_failed', '插件更新检查失败');
    }

    set_site_transient($cache_key, $body, 30 * MINUTE_IN_SECONDS);
    return $body;
}

function knn_demo_update_response($info) {
    $plugin_file = plugin_basename(KNN_DEMO_PLUGIN_FILE);
    $latest = (string) ($info['latest_version'] ?? '');
    $package = esc_url_raw((string) ($info['package'] ?? ''));
    if (empty($info['allowed']) || empty($info['update_available']) || $latest === '' || $package === '') return null;

    return (object) array(
        'id' => KNN_DEMO_UPDATE_SOURCE,
        'slug' => dirname($plugin_file),
        'plugin' => $plugin_file,
        'version' => $latest,
        'new_version' => $latest,
        'url' => 'https://www.kaninai.cn/',
        'package' => $package,
        'requires' => '5.8',
        'requires_php' => '7.4',
        'tested' => get_bloginfo('version'),
    );
}

第三步:注册原生更新源。返回更新对象后,WordPress 会自动生成插件列表更新提示、“现在更新”链接和官方 Ajax,不需要自己输出更新按钮或跳转页面。

add_filter('update_plugins_www.kaninai.cn', function ($update, $plugin_data, $plugin_file, $locales) {
    if ($plugin_file !== plugin_basename(KNN_DEMO_PLUGIN_FILE)) return $update;

    $info = knn_demo_remote_update_info();
    if (is_wp_error($info)) return false;

    return knn_demo_update_response($info) ?: false;
}, 10, 4);

第四步:在官方 Ajax 更新前刷新数据。官方“现在更新”按钮请求 admin-ajax.php?action=update-plugin,以下钩子在核心处理之前更新缓存,避免缓存过期后被判断为最新版。

add_action('wp_ajax_update-plugin', function () {
    $plugin_file = isset($_POST['plugin']) ? plugin_basename(sanitize_text_field(wp_unslash($_POST['plugin']))) : '';
    if ($plugin_file !== plugin_basename(KNN_DEMO_PLUGIN_FILE) || !current_user_can('update_plugins')) return;
    if (!check_ajax_referer('updates', false, false)) return;

    $info = knn_demo_remote_update_info(true);
    if (!is_wp_error($info) && is_array($info)) {
        $update = knn_demo_update_response($info);
        if (!$update) return;

        $transient = get_site_transient('update_plugins');
        if (!is_object($transient)) $transient = new stdClass();

        $plugin_file = plugin_basename(KNN_DEMO_PLUGIN_FILE);
        if (empty($transient->checked) || !is_array($transient->checked)) $transient->checked = array();
        if (empty($transient->response) || !is_array($transient->response)) $transient->response = array();
        $transient->checked[$plugin_file] = KNN_DEMO_PLUGIN_VERSION;
        $transient->response[$plugin_file] = $update;
        $transient->last_checked = time();
        set_site_transient('update_plugins', $transient);
    }
}, 0);
卡尼奶主题关闭更新功能后,WordPress 不再读取插件更新缓存,插件列表和自动更新都不会工作。需要接收插件更新时,请先在主题设置中开启更新功能。plugins_api 只用于补充插件详情弹窗,可按需接入。未购买、接口失败、授权错误或 package 为空时,不要返回更新包。

接入规则

  1. 每个插件在资源商店后台都要有一个对应资源商品,并填写唯一“商品密钥”。
  2. 免费资源只需要传 product_key 就会返回可用。
  3. 付费资源必须传 domain、license_key、product_key。
  4. 服务端校验顺序是:先用域名和授权码找到当前账号,再用当前账号和商品密钥查询是否存在已支付订单。
  5. 返回 allowed: false、请求失败、授权错误、接口不可达,都按不可使用处理。
  6. 建议在插件启用时校验一次即可,校验通过后保存本地授权状态;不要每个页面都远程请求。需要重新校验时,在插件后台提供“重新校验授权”按钮。
  7. 自动更新只依赖后台“最新版本号”和“更新包地址”:最新版本号大于插件当前版本且更新包地址不为空时,插件列表才会提示更新。
  8. 卡尼奶主题关闭更新功能后,插件更新提示、官方 Ajax 和自动更新都会停用;需要更新插件时先开启主题更新功能。
  9. “更新包地址”必须能直接下载 zip 文件,不能返回登录页、网盘预览页或 HTML 页面。
  10. zip 解压后的顶层目录应与插件目录一致,主插件文件的 Version 必须改成发布的新版本,否则安装后仍会继续提示旧版本。
社交账号登录
{{ unread>99?'99+':unread }}
{{ room ? room.name : '卡尼奶聊天室' }}
{{ wsOk?'WebSocket 已连接':(wsStatus==='connecting'?'连接中':'轮询模式') }} · {{ online }} 人在线
全部群聊
{{ r.name }}
{{ r.online }} 在线
{{ r.unread }}
{{ room.announcement }}
{{ booting ? '聊天室加载中…' : '聊天消息加载中…' }}
{{ e }}
内容审核已开启 · 请勿发布违规内容 发送
在线成员 · {{ members.length }}
{{ u.name }}管理
{{ rpDetail.sender.name }} 的红包
{{ rpDetail.rp.message }}
{{ rpDetail.rp.luck?'拼手气红包':'普通红包' }} · 已领 {{ rpDetail.rp.taken }}/{{ rpDetail.rp.num }} 个
{{ l.user.name }}¥ {{ Number(l.amount).toFixed(2) }}
发红包
普通红包 拼手气
{{ rpForm.luck ? '总金额' : '红包金额' }}
¥
红包个数
{{ rpForm.num }} 个
祝福语({{ rpForm.message.length }}/20)
余额支付 ¥ {{ Number(me.balance||0).toFixed(2) }}
开通会员
在手机上浏览此页面