feat: 初始化MNBT虚拟主机销售WordPress插件
20 个文件变更
+2743
-0
13172193298@163.com
| •wp_seller_plugin/includes/admin/class-admin.php | +369 -0 |
| •wp_seller_plugin/includes/admin/views/hosts.php | +163 -0 |
| •wp_seller_plugin/includes/admin/views/logs.php | +54 -0 |
| •wp_seller_plugin/includes/admin/views/settings.php | +207 -0 |
| •wp_seller_plugin/includes/class-activator.php | +144 -0 |
| •wp_seller_plugin/includes/class-crypto.php | +73 -0 |
| •wp_seller_plugin/includes/class-helpers.php | +133 -0 |
| •wp_seller_plugin/includes/class-logger.php | +62 -0 |
| •wp_seller_plugin/includes/class-plugin.php | +109 -0 |
| •wp_seller_plugin/includes/front/class-front.php | +155 -0 |
| •wp_seller_plugin/includes/front/views/my-hosts.php | +144 -0 |
| •wp_seller_plugin/includes/mnbt/class-client.php | +302 -0 |
| •wp_seller_plugin/includes/mnbt/class-exception.php | +45 -0 |
| •wp_seller_plugin/includes/mnbt/interface-provider.php | +111 -0 |
| •wp_seller_plugin/includes/services/class-billing.php | +176 -0 |
| •wp_seller_plugin/includes/services/class-cron.php | +90 -0 |
| •wp_seller_plugin/includes/services/class-provision.php | +160 -0 |
| •wp_seller_plugin/includes/woocommerce/class-woo.php | +183 -0 |
| •wp_seller_plugin/uninstall.php | +22 -0 |
| •wp_seller_plugin/wp-seller-plugin.php | +41 -0 |
变更内容
diff --git a/wp_seller_plugin/includes/admin/class-admin.php b/wp_seller_plugin/includes/admin/class-admin.php
new file mode 100644
index 0000000..95b29cf
--- /dev/null
+++ b/wp_seller_plugin/includes/admin/class-admin.php
@@ -0,0 +1,369 @@
+<?php
+/**
+ * 后台管理:菜单、连接配置页、API 日志页与 AJAX 处理。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp;
+
+use MnbtWp\Mnbt\Client;
+use MnbtWp\Mnbt\Exception as MnbtException;
+use MnbtWp\Services\Provision;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 后台管理类。
+ */
+class Admin {
+
+ /**
+ * 构造:挂载钩子。
+ */
+ public function __construct() {
+ add_action( 'admin_menu', array( $this, 'menu' ) );
+ add_action( 'wp_ajax_mnbtwp_save_provider', array( $this, 'ajax_save_provider' ) );
+ add_action( 'wp_ajax_mnbtwp_get_provider', array( $this, 'ajax_get_provider' ) );
+ add_action( 'wp_ajax_mnbtwp_delete_provider', array( $this, 'ajax_delete_provider' ) );
+ add_action( 'wp_ajax_mnbtwp_test_provider', array( $this, 'ajax_test_provider' ) );
+ add_action( 'wp_ajax_mnbtwp_admin_host_action', array( $this, 'ajax_admin_host_action' ) );
+ add_action( 'wp_ajax_mnbtwp_admin_retry_provision', array( $this, 'ajax_admin_retry_provision' ) );
+ }
+
+ /**
+ * 注册菜单。
+ */
+ public function menu() {
+ add_menu_page(
+ __( 'MNBT 主机销售', 'wp-seller-plugin' ),
+ __( 'MNBT 主机销售', 'wp-seller-plugin' ),
+ 'manage_options',
+ 'mnbtwp',
+ array( $this, 'page_settings' ),
+ 'dashicons-cloud',
+ 56
+ );
+ add_submenu_page(
+ 'mnbtwp',
+ __( '主机管理', 'wp-seller-plugin' ),
+ __( '主机管理', 'wp-seller-plugin' ),
+ 'manage_options',
+ 'mnbtwp-hosts',
+ array( $this, 'page_hosts' )
+ );
+ add_submenu_page(
+ 'mnbtwp',
+ __( '连接配置', 'wp-seller-plugin' ),
+ __( '连接配置', 'wp-seller-plugin' ),
+ 'manage_options',
+ 'mnbtwp',
+ array( $this, 'page_settings' )
+ );
+ add_submenu_page(
+ 'mnbtwp',
+ __( 'API 日志', 'wp-seller-plugin' ),
+ __( 'API 日志', 'wp-seller-plugin' ),
+ 'manage_options',
+ 'mnbtwp-logs',
+ array( $this, 'page_logs' )
+ );
+ }
+
+ /**
+ * 主机总览页。
+ */
+ public function page_hosts() {
+ global $wpdb;
+ $hosts = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}mnbt_hosts ORDER BY id DESC LIMIT 500", ARRAY_A );
+ $provider_map = array();
+ foreach ( $hosts as $h ) {
+ if ( ! isset( $provider_map[ $h['provider_id'] ] ) ) {
+ $provider_map[ $h['provider_id'] ] = Helpers::get_provider( $h['provider_id'] );
+ }
+ }
+ $failed_orders = array();
+ if ( class_exists( 'WooCommerce' ) ) {
+ $failed_orders = wc_get_orders(
+ array(
+ 'limit' => 50,
+ 'status' => array( 'pending', 'processing', 'on-hold', 'completed' ),
+ 'meta_key' => '_mnbtwp_provision_error',
+ 'meta_compare' => 'EXISTS',
+ )
+ );
+ }
+ include MNBTWP_DIR . 'includes/admin/views/hosts.php';
+ }
+
+ /**
+ * 连接配置页。
+ */
+ public function page_settings() {
+ global $wpdb;
+ $providers = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}mnbt_providers ORDER BY id ASC", ARRAY_A );
+ include MNBTWP_DIR . 'includes/admin/views/settings.php';
+ }
+
+ /**
+ * API 日志页。
+ */
+ public function page_logs() {
+ global $wpdb;
+ $logs = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}mnbt_api_log ORDER BY id DESC LIMIT 200", ARRAY_A );
+ include MNBTWP_DIR . 'includes/admin/views/logs.php';
+ }
+
+ /**
+ * 保存/更新节点配置。
+ */
+ public function ajax_save_provider() {
+ check_ajax_referer( 'mnbtwp_admin', 'nonce' );
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( __( '权限不足', 'wp-seller-plugin' ) );
+ }
+
+ global $wpdb;
+ $id = (int) ( $_POST['id'] ?? 0 );
+ $name = sanitize_text_field( $_POST['name'] ?? '' );
+ $api_url = esc_url_raw( trim( (string) ( $_POST['api_url'] ?? '' ) ) );
+ $btdh = sanitize_text_field( $_POST['btdh'] ?? '' );
+ $mn_key = sanitize_text_field( $_POST['mn_key'] ?? '' );
+ $mn_keye = sanitize_text_field( $_POST['mn_keye'] ?? '' );
+ $mn_vs = max( 15, (int) ( $_POST['mn_vs'] ?? 15 ) );
+ $qk = ( ! empty( $_POST['qk'] ) && '1' === (string) $_POST['qk'] ) ? 'true' : 'false';
+
+ if ( '' === $name || '' === $api_url || '' === $btdh || '' === $mn_key || '' === $mn_keye ) {
+ wp_send_json_error( __( '参数不能为空', 'wp-seller-plugin' ) );
+ }
+ if ( ! preg_match( '#^https?://#i', $api_url ) ) {
+ wp_send_json_error( __( 'API 地址需以 http(s):// 开头', 'wp-seller-plugin' ) );
+ }
+
+ $table = $wpdb->prefix . 'mnbt_providers';
+
+ if ( $id > 0 ) {
+ $current = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id=%d", $id ), ARRAY_A );
+ if ( ! $current ) {
+ wp_send_json_error( __( '节点不存在', 'wp-seller-plugin' ) );
+ }
+ // 密钥未修改则保留原密文,避免每次保存变更密文。
+ $mn_key_enc = ( Crypto::decrypt( $current['mn_key_enc'] ) === $mn_key ) ? $current['mn_key_enc'] : Crypto::encrypt( $mn_key );
+ $mn_keye_enc = ( Crypto::decrypt( $current['mn_keye_enc'] ) === $mn_keye ) ? $current['mn_keye_enc'] : Crypto::encrypt( $mn_keye );
+
+ $wpdb->update(
+ $table,
+ array(
+ 'name' => $name,
+ 'api_url' => $api_url,
+ 'btdh' => $btdh,
+ 'mn_key_enc' => $mn_key_enc,
+ 'mn_keye_enc' => $mn_keye_enc,
+ 'mn_vs' => $mn_vs,
+ 'qk' => $qk,
+ 'updated_at' => current_time( 'mysql' ),
+ ),
+ array( 'id' => $id )
+ );
+ } else {
+ $wpdb->insert(
+ $table,
+ array(
+ 'name' => $name,
+ 'api_url' => $api_url,
+ 'btdh' => $btdh,
+ 'mn_key_enc' => Crypto::encrypt( $mn_key ),
+ 'mn_keye_enc' => Crypto::encrypt( $mn_keye ),
+ 'mn_vs' => $mn_vs,
+ 'qk' => $qk,
+ 'created_at' => current_time( 'mysql' ),
+ 'updated_at' => current_time( 'mysql' ),
+ )
+ );
+ }
+
+ if ( $wpdb->last_error ) {
+ wp_send_json_error( __( '保存失败', 'wp-seller-plugin' ) . ':' . $wpdb->last_error );
+ }
+ wp_send_json_success( __( '保存成功', 'wp-seller-plugin' ) );
+ }
+
+ /**
+ * 获取节点配置(编辑回显,解密密钥)。
+ */
+ public function ajax_get_provider() {
+ check_ajax_referer( 'mnbtwp_admin', 'nonce' );
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( __( '权限不足', 'wp-seller-plugin' ) );
+ }
+ global $wpdb;
+ $id = (int) ( $_POST['id'] ?? 0 );
+ $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mnbt_providers WHERE id=%d", $id ), ARRAY_A );
+ if ( ! $row ) {
+ wp_send_json_error( __( '节点不存在', 'wp-seller-plugin' ) );
+ }
+ wp_send_json_success(
+ array(
+ 'id' => (int) $row['id'],
+ 'name' => $row['name'],
+ 'api_url' => $row['api_url'],
+ 'btdh' => $row['btdh'],
+ 'mn_key' => Crypto::decrypt( $row['mn_key_enc'] ),
+ 'mn_keye' => Crypto::decrypt( $row['mn_keye_enc'] ),
+ 'mn_vs' => (int) $row['mn_vs'],
+ 'qk' => $row['qk'],
+ )
+ );
+ }
+
+ /**
+ * 删除节点配置。
+ */
+ public function ajax_delete_provider() {
+ check_ajax_referer( 'mnbtwp_admin', 'nonce' );
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( __( '权限不足', 'wp-seller-plugin' ) );
+ }
+ global $wpdb;
+ $id = (int) ( $_POST['id'] ?? 0 );
+ $wpdb->delete( $wpdb->prefix . 'mnbt_providers', array( 'id' => $id ), array( '%d' ) );
+ if ( $wpdb->last_error ) {
+ wp_send_json_error( __( '删除失败', 'wp-seller-plugin' ) . ':' . $wpdb->last_error );
+ }
+ wp_send_json_success( __( '删除成功', 'wp-seller-plugin' ) );
+ }
+
+ /**
+ * 连接测试(cfif)。
+ */
+ public function ajax_test_provider() {
+ check_ajax_referer( 'mnbtwp_admin', 'nonce' );
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( __( '权限不足', 'wp-seller-plugin' ) );
+ }
+ global $wpdb;
+ $id = (int) ( $_POST['id'] ?? 0 );
+ $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mnbt_providers WHERE id=%d", $id ), ARRAY_A );
+ if ( ! $row ) {
+ wp_send_json_error( __( '节点不存在', 'wp-seller-plugin' ) );
+ }
+ try {
+ $client = new Client(
+ array(
+ 'api_url' => $row['api_url'],
+ 'btdh' => $row['btdh'],
+ 'mn_key' => Crypto::decrypt( $row['mn_key_enc'] ),
+ 'mn_keye' => Crypto::decrypt( $row['mn_keye_enc'] ),
+ 'mn_vs' => (int) $row['mn_vs'],
+ 'provider_id' => (int) $row['id'],
+ )
+ );
+ $result = $client->testConnection();
+ /* translators: %s: MNBT 返回消息 */
+ wp_send_json_success( sprintf( __( '连接成功:%s', 'wp-seller-plugin' ), $result['msg'] ) );
+ } catch ( MnbtException $e ) {
+ wp_send_json_error( __( '连接失败', 'wp-seller-plugin' ) . ':' . $e->getMessage() );
+ }
+ }
+
+ /**
+ * 管理端主机操作:start/stop/suspend/resume/delete/change_pass/renew/change_package。
+ */
+ public function ajax_admin_host_action() {
+ check_ajax_referer( 'mnbtwp_admin', 'nonce' );
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( __( '权限不足', 'wp-seller-plugin' ) );
+ }
+ $action = sanitize_key( $_POST['host_action'] ?? '' );
+ $host_id = (int) ( $_POST['host_id'] ?? 0 );
+ $host = Helpers::get_host( $host_id );
+ if ( ! $host ) {
+ wp_send_json_error( __( '主机不存在', 'wp-seller-plugin' ) );
+ }
+ $provider = Helpers::get_provider( $host['provider_id'] );
+ if ( ! $provider ) {
+ wp_send_json_error( __( '节点不可用', 'wp-seller-plugin' ) );
+ }
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'mnbt_hosts';
+
+ try {
+ $client = Helpers::client_for_provider( $provider );
+ switch ( $action ) {
+ case 'start':
+ $client->startSite( $host['username'] );
+ break;
+ case 'stop':
+ $client->stopSite( $host['username'] );
+ break;
+ case 'suspend':
+ $client->suspendHost( $host['username'] );
+ $wpdb->update( $table, array( 'status' => 'suspended', 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $host_id ) );
+ break;
+ case 'resume':
+ $client->resumeHost( $host['username'] );
+ $wpdb->update( $table, array( 'status' => 'active', 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $host_id ) );
+ break;
+ case 'delete':
+ $client->deleteHost( $host['username'] );
+ $wpdb->delete( $table, array( 'id' => $host_id ), array( '%d' ) );
+ break;
+ case 'change_pass':
+ $password = sanitize_text_field( $_POST['password'] ?? '' );
+ if ( mb_strlen( $password ) < 6 ) {
+ wp_send_json_error( __( '密码不能少于 6 位', 'wp-seller-plugin' ) );
+ }
+ $client->changePassword( $host['username'], $password );
+ $wpdb->update( $table, array( 'password_enc' => Crypto::encrypt( $password ), 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $host_id ) );
+ break;
+ case 'renew':
+ $setdate = sanitize_text_field( $_POST['setdate'] ?? '' );
+ if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $setdate ) ) {
+ wp_send_json_error( __( '到期日期格式错误', 'wp-seller-plugin' ) );
+ }
+ $client->renewHost( $host['username'], $setdate );
+ $wpdb->update( $table, array( 'expire_date' => $setdate, 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $host_id ) );
+ break;
+ case 'change_package':
+ $web = max( 0, (int) ( $_POST['websize'] ?? 0 ) );
+ $sql = max( 0, (int) ( $_POST['sqlsize'] ?? 0 ) );
+ $flow = max( 0, (int) ( $_POST['ll'] ?? 0 ) );
+ $client->changePackage( $host['username'], array( 'websize' => $web, 'sqlsize' => $sql, 'll' => $flow ) );
+ $quota = Helpers::parse_quota( $host['quota_json'] );
+ $quota['web']['max'] = $web;
+ $quota['sql']['max'] = $sql;
+ $quota['flow']['max'] = $flow;
+ $wpdb->update( $table, array( 'quota_json' => wp_json_encode( $quota ), 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $host_id ) );
+ break;
+ default:
+ wp_send_json_error( __( '未知操作', 'wp-seller-plugin' ) );
+ }
+ wp_send_json_success( __( '操作成功', 'wp-seller-plugin' ) );
+ } catch ( \Exception $e ) {
+ wp_send_json_error( $e->getMessage() );
+ }
+ }
+
+ /**
+ * 重试开通失败的订单。
+ */
+ public function ajax_admin_retry_provision() {
+ check_ajax_referer( 'mnbtwp_admin', 'nonce' );
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( __( '权限不足', 'wp-seller-plugin' ) );
+ }
+ $order_id = (int) ( $_POST['order_id'] ?? 0 );
+ if ( ! $order_id ) {
+ wp_send_json_error( __( '参数错误', 'wp-seller-plugin' ) );
+ }
+ try {
+ $result = Provision::provision_order( $order_id );
+ wp_send_json_success( $result['msg'] );
+ } catch ( \Exception $e ) {
+ wp_send_json_error( $e->getMessage() );
+ }
+ }
+}
diff --git a/wp_seller_plugin/includes/admin/views/hosts.php b/wp_seller_plugin/includes/admin/views/hosts.php
new file mode 100644
index 0000000..9aa4aaf
--- /dev/null
+++ b/wp_seller_plugin/includes/admin/views/hosts.php
@@ -0,0 +1,163 @@
+<?php
+/**
+ * 主机总览页视图。
+ *
+ * @package MnbtWp
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+$mnbtwp_nonce = wp_create_nonce( 'mnbtwp_admin' );
+?>
+<div class="wrap">
+ <h1><?php esc_html_e( '主机管理', 'wp-seller-plugin' ); ?></h1>
+ <p class="description"><?php esc_html_e( '所有已开通主机(含购买自动开通与手动开通)。操作会直接调用 MNBT 生效。', 'wp-seller-plugin' ); ?></p>
+
+ <?php if ( ! empty( $failed_orders ) ) : ?>
+ <h2 style="margin-top:20px;"><?php esc_html_e( '开通失败订单', 'wp-seller-plugin' ); ?></h2>
+ <p class="description"><?php esc_html_e( '以下订单支付成功但自动开通失败,可重试开通。', 'wp-seller-plugin' ); ?></p>
+ <table class="widefat striped">
+ <thead>
+ <tr>
+ <th><?php esc_html_e( '订单', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '客户', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '金额', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '错误信息', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '操作', 'wp-seller-plugin' ); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ( $failed_orders as $mnbtwp_fo ) : ?>
+ <?php $mnbtwp_err = get_post_meta( $mnbtwp_fo->get_id(), '_mnbtwp_provision_error', true ); ?>
+ <tr>
+ <td><a href="<?php echo esc_url( $mnbtwp_fo->get_edit_order_url() ); ?>">#<?php echo (int) $mnbtwp_fo->get_id(); ?></a></td>
+ <td><?php echo esc_html( $mnbtwp_fo->get_billing_email() ? $mnbtwp_fo->get_billing_email() : '#' . (int) $mnbtwp_fo->get_user_id() ); ?></td>
+ <td><?php echo esc_html( $mnbtwp_fo->get_total() . ' ' . $mnbtwp_fo->get_currency() ); ?></td>
+ <td style="color:#c62828;"><?php echo esc_html( $mnbtwp_err ); ?></td>
+ <td>
+ <button type="button" class="button button-small button-primary" onclick="mnbtwpRetry(<?php echo (int) $mnbtwp_fo->get_id(); ?>)"><?php esc_html_e( '重试开通', 'wp-seller-plugin' ); ?></button>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ <?php endif; ?>
+
+ <h2 style="margin-top:20px;"><?php esc_html_e( '主机列表', 'wp-seller-plugin' ); ?></h2>
+ <?php if ( empty( $hosts ) ) : ?>
+ <p><?php esc_html_e( '暂无主机记录。', 'wp-seller-plugin' ); ?></p>
+ <?php else : ?>
+ <table class="widefat striped">
+ <thead>
+ <tr>
+ <th><?php esc_html_e( 'ID', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '节点', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '用户名', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '站点', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '状态', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '到期', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '订单', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '操作', 'wp-seller-plugin' ); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ( $hosts as $mnbtwp_h ) : ?>
+ <?php
+ $mnbtwp_p = isset( $provider_map[ $mnbtwp_h['provider_id'] ] ) ? $provider_map[ $mnbtwp_h['provider_id'] ] : null;
+ $mnbtwp_label = Helpers::host_status_label( $mnbtwp_h['status'] );
+ ?>
+ <tr>
+ <td><?php echo (int) $mnbtwp_h['id']; ?></td>
+ <td><?php echo $mnbtwp_p ? esc_html( $mnbtwp_p['name'] ) : esc_html( '#' . (int) $mnbtwp_h['provider_id'] ); ?></td>
+ <td><code><?php echo esc_html( $mnbtwp_h['username'] ); ?></code></td>
+ <td><?php echo esc_html( $mnbtwp_h['site_domain'] ? $mnbtwp_h['site_domain'] : '-' ); ?></td>
+ <td>
+ <?php if ( in_array( $mnbtwp_h['status'], array( 'active', 'provisioning' ), true ) ) : ?>
+ <span style="color:#1a7f37;"><?php echo esc_html( $mnbtwp_label ); ?></span>
+ <?php else : ?>
+ <span style="color:#c62828;"><?php echo esc_html( $mnbtwp_label ); ?></span>
+ <?php endif; ?>
+ </td>
+ <td><?php echo esc_html( $mnbtwp_h['expire_date'] && '0000-00-00' !== $mnbtwp_h['expire_date'] ? $mnbtwp_h['expire_date'] : __( '长期', 'wp-seller-plugin' ) ); ?></td>
+ <td><?php echo $mnbtwp_h['order_id'] ? '<a href="' . esc_url( get_edit_post_link( (int) $mnbtwp_h['order_id'] ) ) . '">#' . (int) $mnbtwp_h['order_id'] . '</a>' : '-'; ?></td>
+ <td>
+ <button type="button" class="button button-small" onclick="mnbtwpHostAction(<?php echo (int) $mnbtwp_h['id']; ?>,'start')"><?php esc_html_e( '启动', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button button-small" onclick="mnbtwpHostAction(<?php echo (int) $mnbtwp_h['id']; ?>,'stop')"><?php esc_html_e( '停止', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button button-small" onclick="mnbtwpHostAction(<?php echo (int) $mnbtwp_h['id']; ?>,'suspend')"><?php esc_html_e( '暂停', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button button-small" onclick="mnbtwpHostAction(<?php echo (int) $mnbtwp_h['id']; ?>,'resume')"><?php esc_html_e( '恢复', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button button-small" onclick="mnbtwpPass(<?php echo (int) $mnbtwp_h['id']; ?>)"><?php esc_html_e( '改密', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button button-small" onclick="mnbtwpRenew(<?php echo (int) $mnbtwp_h['id']; ?>)"><?php esc_html_e( '续费', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button button-small" onclick="mnbtwpPackage(<?php echo (int) $mnbtwp_h['id']; ?>)"><?php esc_html_e( '升降级', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button button-small button-link-delete" onclick="mnbtwpDelete(<?php echo (int) $mnbtwp_h['id']; ?>)"><?php esc_html_e( '删除', 'wp-seller-plugin' ); ?></button>
+ <?php if ( 'failed' === $mnbtwp_h['status'] && $mnbtwp_h['order_id'] ) : ?>
+ <button type="button" class="button button-small button-primary" onclick="mnbtwpRetry(<?php echo (int) $mnbtwp_h['order_id']; ?>)"><?php esc_html_e( '重试开通', 'wp-seller-plugin' ); ?></button>
+ <?php endif; ?>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ <?php endif; ?>
+</div>
+
+<script type="text/javascript">
+ var mnbtwpAdminNonce = '<?php echo esc_js( $mnbtwp_nonce ); ?>';
+
+ function mnbtwpAdminAjax(data, done) {
+ data.nonce = mnbtwpAdminNonce;
+ jQuery.post(ajaxurl, data, function (r) {
+ if (r && r.success) {
+ if (done) done(r); else location.reload();
+ } else {
+ alert((r && r.data) ? r.data : '<?php echo esc_js( __( '操作失败', 'wp-seller-plugin' ) ); ?>');
+ }
+ }).fail(function () {
+ alert('<?php echo esc_js( __( '网络错误', 'wp-seller-plugin' ) ); ?>');
+ });
+ }
+
+ function mnbtwpHostAction(id, action) {
+ mnbtwpAdminAjax({ action: 'mnbtwp_admin_host_action', host_action: action, host_id: id });
+ }
+
+ function mnbtwpPass(id) {
+ var pw = prompt('<?php echo esc_js( __( '请输入新密码(至少 6 位)', 'wp-seller-plugin' ) ); ?>');
+ if (!pw || pw.length < 6) return;
+ mnbtwpAdminAjax({ action: 'mnbtwp_admin_host_action', host_action: 'change_pass', host_id: id, password: pw });
+ }
+
+ function mnbtwpRenew(id) {
+ var d = prompt('<?php echo esc_js( __( '请输入新到期日期(YYYY-MM-DD)', 'wp-seller-plugin' ) ); ?>');
+ if (!d || !/^\d{4}-\d{2}-\d{2}$/.test(d)) return;
+ mnbtwpAdminAjax({ action: 'mnbtwp_admin_host_action', host_action: 'renew', host_id: id, setdate: d });
+ }
+
+ function mnbtwpPackage(id) {
+ var web = prompt('<?php echo esc_js( __( '网站空间 (MB)', 'wp-seller-plugin' ) ); ?>', '0');
+ if (web === null) return;
+ var sql = prompt('<?php echo esc_js( __( '数据库空间 (MB)', 'wp-seller-plugin' ) ); ?>', '0');
+ if (sql === null) return;
+ var ll = prompt('<?php echo esc_js( __( '流量 (MB,0=不限)', 'wp-seller-plugin' ) ); ?>', '0');
+ if (ll === null) return;
+ mnbtwpAdminAjax({
+ action: 'mnbtwp_admin_host_action',
+ host_action: 'change_package',
+ host_id: id,
+ websize: parseInt(web, 10) || 0,
+ sqlsize: parseInt(sql, 10) || 0,
+ ll: parseInt(ll, 10) || 0
+ });
+ }
+
+ function mnbtwpDelete(id) {
+ if (!confirm('<?php echo esc_js( __( '删除后站点、数据库、FTP 将被清除且不可恢复,确定删除?', 'wp-seller-plugin' ) ); ?>')) return;
+ mnbtwpAdminAjax({ action: 'mnbtwp_admin_host_action', host_action: 'delete', host_id: id });
+ }
+
+ function mnbtwpRetry(orderId) {
+ if (!confirm('<?php echo esc_js( __( '确定重试开通该订单?', 'wp-seller-plugin' ) ); ?>')) return;
+ mnbtwpAdminAjax({ action: 'mnbtwp_admin_retry_provision', order_id: orderId });
+ }
+</script>
diff --git a/wp_seller_plugin/includes/admin/views/logs.php b/wp_seller_plugin/includes/admin/views/logs.php
new file mode 100644
index 0000000..a7f0875
--- /dev/null
+++ b/wp_seller_plugin/includes/admin/views/logs.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * API 日志页视图。
+ *
+ * @package MnbtWp
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+?>
+<div class="wrap">
+ <h1><?php esc_html_e( 'MNBT API 日志', 'wp-seller-plugin' ); ?></h1>
+ <?php if ( empty( $logs ) ) : ?>
+ <p class="description"><?php esc_html_e( '暂无日志。配置节点并测试连接后,这里会记录每次 API 调用(密钥已脱敏)。', 'wp-seller-plugin' ); ?></p>
+ <?php else : ?>
+ <table class="widefat striped">
+ <thead>
+ <tr>
+ <th><?php esc_html_e( 'ID', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '节点', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '动作', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '参数', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( 'Code', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '消息', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '耗时(s)', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '时间', 'wp-seller-plugin' ); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ( $logs as $mnbtwp_log ) : ?>
+ <tr>
+ <td><?php echo (int) $mnbtwp_log['id']; ?></td>
+ <td><?php echo (int) $mnbtwp_log['provider_id']; ?></td>
+ <td><code><?php echo esc_html( $mnbtwp_log['action'] ); ?></code></td>
+ <td style="max-width:300px;word-break:break-all;">
+ <code style="font-size:11px;"><?php echo esc_html( $mnbtwp_log['params_json'] ); ?></code>
+ </td>
+ <td>
+ <?php if ( 200 === (int) $mnbtwp_log['code'] ) : ?>
+ <span style="color:#00a32a;"><?php echo (int) $mnbtwp_log['code']; ?></span>
+ <?php else : ?>
+ <span style="color:#d63638;"><?php echo (int) $mnbtwp_log['code']; ?></span>
+ <?php endif; ?>
+ </td>
+ <td style="max-width:240px;"><?php echo esc_html( $mnbtwp_log['msg'] ); ?></td>
+ <td><?php echo esc_html( $mnbtwp_log['duration'] ); ?></td>
+ <td><?php echo esc_html( $mnbtwp_log['created_at'] ); ?></td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ <?php endif; ?>
+</div>
diff --git a/wp_seller_plugin/includes/admin/views/settings.php b/wp_seller_plugin/includes/admin/views/settings.php
new file mode 100644
index 0000000..4a76b95
--- /dev/null
+++ b/wp_seller_plugin/includes/admin/views/settings.php
@@ -0,0 +1,207 @@
+<?php
+/**
+ * 连接配置页视图。
+ *
+ * @package MnbtWp
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+$mnbtwp_nonce = wp_create_nonce( 'mnbtwp_admin' );
+?>
+<div class="wrap">
+ <h1><?php esc_html_e( 'MNBT 连接配置', 'wp-seller-plugin' ); ?></h1>
+ <p class="description">
+ <?php esc_html_e( '配置 MNBT(梦奈宝塔)节点连接信息。密钥在「系统设置 → API 密钥」与「宝塔列表 → ktmy」处获取。', 'wp-seller-plugin' ); ?>
+ </p>
+
+ <div style="display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap;">
+ <!-- 左侧:表单 -->
+ <div style="min-width:380px;max-width:520px;flex:1;">
+ <form id="mnbtwp-provider-form" autocomplete="off">
+ <input type="hidden" id="mnbtwp-pid" value="0">
+ <table class="form-table" role="presentation">
+ <tr>
+ <th scope="row"><label for="mnbtwp-name"><?php esc_html_e( '名称', 'wp-seller-plugin' ); ?></label></th>
+ <td>
+ <input type="text" id="mnbtwp-name" class="regular-text" placeholder="<?php esc_attr_e( '如:主节点', 'wp-seller-plugin' ); ?>">
+ </td>
+ </tr>
+ <tr>
+ <th scope="row"><label for="mnbtwp-api-url"><?php esc_html_e( 'MNBT 站点地址', 'wp-seller-plugin' ); ?></label></th>
+ <td>
+ <input type="text" id="mnbtwp-api-url" class="regular-text" placeholder="https://mnbt.example.com">
+ <p class="description"><?php esc_html_e( 'MNBT 站点根地址,接口为 地址/api/api.php', 'wp-seller-plugin' ); ?></p>
+ </td>
+ </tr>
+ <tr>
+ <th scope="row"><label for="mnbtwp-btdh"><?php esc_html_e( '宝塔节点代号 (btdh)', 'wp-seller-plugin' ); ?></label></th>
+ <td>
+ <input type="text" id="mnbtwp-btdh" class="regular-text" placeholder="如 mn123456f">
+ <p class="description"><?php esc_html_e( 'MNBT 后台「宝塔列表」中的宝塔编号', 'wp-seller-plugin' ); ?></p>
+ </td>
+ </tr>
+ <tr>
+ <th scope="row"><label for="mnbtwp-mn-key"><?php esc_html_e( '系统 API 密钥 (mn_key)', 'wp-seller-plugin' ); ?></label></th>
+ <td>
+ <input type="password" id="mnbtwp-mn-key" class="regular-text" autocomplete="new-password">
+ <p class="description"><?php esc_html_e( 'MNBT 后台「系统设置 → API 密钥」', 'wp-seller-plugin' ); ?></p>
+ </td>
+ </tr>
+ <tr>
+ <th scope="row"><label for="mnbtwp-mn-keye"><?php esc_html_e( '节点调用密钥 (mn_keye)', 'wp-seller-plugin' ); ?></label></th>
+ <td>
+ <input type="password" id="mnbtwp-mn-keye" class="regular-text" autocomplete="new-password">
+ <p class="description"><?php esc_html_e( 'MNBT 后台「宝塔列表」ktmy 列 👁 显示的值(md5(ktmy.qmk))', 'wp-seller-plugin' ); ?></p>
+ </td>
+ </tr>
+ <tr>
+ <th scope="row"><label for="mnbtwp-mn-vs"><?php esc_html_e( 'API 版本号 (mn_vs)', 'wp-seller-plugin' ); ?></label></th>
+ <td>
+ <input type="number" id="mnbtwp-mn-vs" class="small-text" value="15" min="15">
+ <p class="description"><?php esc_html_e( '默认 15,MNBT 要求 ≥ 15', 'wp-seller-plugin' ); ?></p>
+ </td>
+ </tr>
+ <tr>
+ <th scope="row"><?php esc_html_e( '启用', 'wp-seller-plugin' ); ?></th>
+ <td>
+ <label>
+ <input type="checkbox" id="mnbtwp-qk" checked>
+ <?php esc_html_e( '启用该节点', 'wp-seller-plugin' ); ?>
+ </label>
+ </td>
+ </tr>
+ </table>
+ <p class="submit">
+ <button type="button" class="button button-primary" onclick="mnbtwpSaveProvider()"><?php esc_html_e( '保存', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button" onclick="mnbtwpResetForm()"><?php esc_html_e( '重置', 'wp-seller-plugin' ); ?></button>
+ </p>
+ </form>
+ </div>
+
+ <!-- 右侧:节点列表 -->
+ <div style="min-width:520px;flex:2;">
+ <h2><?php esc_html_e( '已配置节点', 'wp-seller-plugin' ); ?></h2>
+ <?php if ( empty( $providers ) ) : ?>
+ <p class="description"><?php esc_html_e( '暂无配置,请在左侧添加。', 'wp-seller-plugin' ); ?></p>
+ <?php else : ?>
+ <table class="widefat striped">
+ <thead>
+ <tr>
+ <th><?php esc_html_e( 'ID', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '名称', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( 'MNBT 地址', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '节点代号', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '状态', 'wp-seller-plugin' ); ?></th>
+ <th><?php esc_html_e( '操作', 'wp-seller-plugin' ); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ( $providers as $mnbtwp_p ) : ?>
+ <tr>
+ <td><?php echo (int) $mnbtwp_p['id']; ?></td>
+ <td><?php echo esc_html( $mnbtwp_p['name'] ); ?></td>
+ <td><?php echo esc_html( $mnbtwp_p['api_url'] ); ?></td>
+ <td><code><?php echo esc_html( $mnbtwp_p['btdh'] ); ?></code></td>
+ <td>
+ <?php if ( 'true' === $mnbtwp_p['qk'] ) : ?>
+ <span class="dashicons dashicons-yes" style="color:#00a32a;" title="<?php esc_attr_e( '启用', 'wp-seller-plugin' ); ?>"></span>
+ <?php else : ?>
+ <span class="dashicons dashicons-no" style="color:#d63638;" title="<?php esc_attr_e( '禁用', 'wp-seller-plugin' ); ?>"></span>
+ <?php endif; ?>
+ </td>
+ <td>
+ <button type="button" class="button button-small" onclick="mnbtwpEditProvider(<?php echo (int) $mnbtwp_p['id']; ?>)"><?php esc_html_e( '编辑', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button button-small" onclick="mnbtwpTestProvider(<?php echo (int) $mnbtwp_p['id']; ?>)"><?php esc_html_e( '测试连接', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button button-small button-link-delete" onclick="mnbtwpDeleteProvider(<?php echo (int) $mnbtwp_p['id']; ?>)"><?php esc_html_e( '删除', 'wp-seller-plugin' ); ?></button>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ <?php endif; ?>
+ </div>
+ </div>
+</div>
+
+<script type="text/javascript">
+ var mnbtwpNonce = '<?php echo esc_js( $mnbtwp_nonce ); ?>';
+
+ function mnbtwpResetForm() {
+ document.getElementById('mnbtwp-pid').value = '0';
+ document.getElementById('mnbtwp-name').value = '';
+ document.getElementById('mnbtwp-api-url').value = '';
+ document.getElementById('mnbtwp-btdh').value = '';
+ document.getElementById('mnbtwp-mn-key').value = '';
+ document.getElementById('mnbtwp-mn-keye').value = '';
+ document.getElementById('mnbtwp-mn-vs').value = '15';
+ document.getElementById('mnbtwp-qk').checked = true;
+ }
+
+ function mnbtwpSaveProvider() {
+ var data = {
+ action: 'mnbtwp_save_provider',
+ nonce: mnbtwpNonce,
+ id: document.getElementById('mnbtwp-pid').value,
+ name: document.getElementById('mnbtwp-name').value,
+ api_url: document.getElementById('mnbtwp-api-url').value,
+ btdh: document.getElementById('mnbtwp-btdh').value,
+ mn_key: document.getElementById('mnbtwp-mn-key').value,
+ mn_keye: document.getElementById('mnbtwp-mn-keye').value,
+ mn_vs: document.getElementById('mnbtwp-mn-vs').value,
+ qk: document.getElementById('mnbtwp-qk').checked ? '1' : '0'
+ };
+ jQuery.post(ajaxurl, data, function (r) {
+ if (r && r.success) {
+ alert(r.data);
+ location.reload();
+ } else {
+ alert((r && r.data) ? r.data : '<?php echo esc_js( __( '保存失败', 'wp-seller-plugin' ) ); ?>');
+ }
+ }).fail(function () {
+ alert('<?php echo esc_js( __( '网络错误', 'wp-seller-plugin' ) ); ?>');
+ });
+ }
+
+ function mnbtwpEditProvider(id) {
+ jQuery.post(ajaxurl, { action: 'mnbtwp_get_provider', nonce: mnbtwpNonce, id: id }, function (r) {
+ if (!r || !r.success) {
+ alert((r && r.data) ? r.data : '<?php echo esc_js( __( '获取失败', 'wp-seller-plugin' ) ); ?>');
+ return;
+ }
+ var d = r.data;
+ document.getElementById('mnbtwp-pid').value = d.id;
+ document.getElementById('mnbtwp-name').value = d.name;
+ document.getElementById('mnbtwp-api-url').value = d.api_url;
+ document.getElementById('mnbtwp-btdh').value = d.btdh;
+ document.getElementById('mnbtwp-mn-key').value = d.mn_key;
+ document.getElementById('mnbtwp-mn-keye').value = d.mn_keye;
+ document.getElementById('mnbtwp-mn-vs').value = d.mn_vs;
+ document.getElementById('mnbtwp-qk').checked = (d.qk === 'true');
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ });
+ }
+
+ function mnbtwpTestProvider(id) {
+ jQuery.post(ajaxurl, { action: 'mnbtwp_test_provider', nonce: mnbtwpNonce, id: id }, function (r) {
+ if (r && r.success) {
+ alert(r.data);
+ } else {
+ alert((r && r.data) ? r.data : '<?php echo esc_js( __( '连接失败', 'wp-seller-plugin' ) ); ?>');
+ }
+ });
+ }
+
+ function mnbtwpDeleteProvider(id) {
+ if (!confirm('<?php echo esc_js( __( '确定删除该节点配置?不影响 MNBT 中已有主机。', 'wp-seller-plugin' ) ); ?>')) return;
+ jQuery.post(ajaxurl, { action: 'mnbtwp_delete_provider', nonce: mnbtwpNonce, id: id }, function (r) {
+ if (r && r.success) {
+ location.reload();
+ } else {
+ alert((r && r.data) ? r.data : '<?php echo esc_js( __( '删除失败', 'wp-seller-plugin' ) ); ?>');
+ }
+ });
+ }
+</script>
diff --git a/wp_seller_plugin/includes/class-activator.php b/wp_seller_plugin/includes/class-activator.php
new file mode 100644
index 0000000..85a86ba
--- /dev/null
+++ b/wp_seller_plugin/includes/class-activator.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * 激活/停用逻辑:建表、默认配置、定时任务注册。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 激活器。
+ */
+class Activator {
+
+ /**
+ * 激活:建表 + 默认设置 + 注册定时任务。
+ */
+ public static function activate() {
+ self::create_tables();
+ self::defaults();
+ self::schedule_cron();
+ }
+
+ /**
+ * 停用:清理定时任务(数据保留)。
+ */
+ public static function deactivate() {
+ self::clear_cron();
+ }
+
+ /**
+ * 创建插件数据表。
+ */
+ private static function create_tables() {
+ global $wpdb;
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+
+ $charset_collate = $wpdb->get_charset_collate();
+ $prefix = $wpdb->prefix;
+
+ $tables = array();
+
+ // MNBT 节点连接配置(可多节点)。
+ $tables[] = "CREATE TABLE {$prefix}mnbt_providers (
+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ name varchar(100) NOT NULL DEFAULT '',
+ api_url varchar(255) NOT NULL DEFAULT '',
+ btdh varchar(50) NOT NULL DEFAULT '',
+ mn_key_enc varchar(255) NOT NULL DEFAULT '',
+ mn_keye_enc varchar(255) NOT NULL DEFAULT '',
+ mn_vs int(11) NOT NULL DEFAULT 15,
+ qk varchar(20) NOT NULL DEFAULT 'true',
+ created_at datetime DEFAULT NULL,
+ updated_at datetime DEFAULT NULL,
+ PRIMARY KEY (id),
+ KEY qk (qk)
+ ) $charset_collate;";
+
+ // 主机记录(本地状态缓存,购买开通后写入)。
+ $tables[] = "CREATE TABLE {$prefix}mnbt_hosts (
+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ provider_id bigint(20) unsigned NOT NULL DEFAULT 0,
+ order_id bigint(20) unsigned NOT NULL DEFAULT 0,
+ user_id bigint(20) unsigned NOT NULL DEFAULT 0,
+ username varchar(64) NOT NULL DEFAULT '',
+ password_enc varchar(255) NOT NULL DEFAULT '',
+ site_domain varchar(128) NOT NULL DEFAULT '',
+ status varchar(20) NOT NULL DEFAULT 'pending',
+ expire_date varchar(20) NOT NULL DEFAULT '',
+ quota_json text NOT NULL,
+ last_remind datetime DEFAULT NULL,
+ created_at datetime DEFAULT NULL,
+ updated_at datetime DEFAULT NULL,
+ PRIMARY KEY (id),
+ KEY user_id (user_id),
+ KEY username (username),
+ KEY status (status)
+ ) $charset_collate;";
+
+ // API 调用日志(脱敏)。
+ $tables[] = "CREATE TABLE {$prefix}mnbt_api_log (
+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ provider_id bigint(20) unsigned NOT NULL DEFAULT 0,
+ action varchar(50) NOT NULL DEFAULT '',
+ params_json text NOT NULL,
+ code int(11) NOT NULL DEFAULT 0,
+ msg text NOT NULL,
+ duration float NOT NULL DEFAULT 0,
+ created_at datetime DEFAULT NULL,
+ PRIMARY KEY (id),
+ KEY action (action),
+ KEY created_at (created_at)
+ ) $charset_collate;";
+
+ foreach ( $tables as $sql ) {
+ dbDelta( $sql );
+ }
+ }
+
+ /**
+ * 默认设置。
+ */
+ private static function defaults() {
+ if ( false === get_option( 'mnbtwp_settings' ) ) {
+ add_option(
+ 'mnbtwp_settings',
+ array(
+ 'expire_days' => 3, // 到期前提醒天数。
+ 'grace_days' => 0, // 宽限期(天)。
+ 'keep_data_on_del' => 1, // 卸载时保留数据。
+ )
+ );
+ }
+ }
+
+ /**
+ * 注册定时任务:每日到期维护 + 每 10 分钟状态同步。
+ */
+ public static function schedule_cron() {
+ if ( ! wp_next_scheduled( 'mnbtwp_daily_maintenance' ) ) {
+ wp_schedule_event( time() + 60, 'daily', 'mnbtwp_daily_maintenance' );
+ }
+ if ( ! wp_next_scheduled( 'mnbtwp_sync_hosts' ) ) {
+ wp_schedule_event( time() + 120, 'twicedaily', 'mnbtwp_sync_hosts' );
+ }
+ }
+
+ /**
+ * 清理定时任务。
+ */
+ private static function clear_cron() {
+ foreach ( array( 'mnbtwp_daily_maintenance', 'mnbtwp_sync_hosts' ) as $hook ) {
+ $timestamp = wp_next_scheduled( $hook );
+ if ( $timestamp ) {
+ wp_unschedule_event( $timestamp, $hook );
+ }
+ }
+ }
+}
diff --git a/wp_seller_plugin/includes/class-crypto.php b/wp_seller_plugin/includes/class-crypto.php
new file mode 100644
index 0000000..3c61ba4
--- /dev/null
+++ b/wp_seller_plugin/includes/class-crypto.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * 密钥加解密工具:基于 WordPress 盐派生密钥,AES-256-CBC 加密。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 密钥加解密。
+ */
+class Crypto {
+
+ /**
+ * 派生 32 字节 AES 密钥(基于 wp_salt('auth'),站点级唯一)。
+ *
+ * @return string
+ */
+ private static function key() {
+ return substr( hash( 'sha256', wp_salt( 'auth' ) ), 0, 32 );
+ }
+
+ /**
+ * 加密明文。
+ *
+ * @param string $plain 明文。
+ * @return string 密文(base64),空串原样返回。
+ */
+ public static function encrypt( $plain ) {
+ $plain = (string) $plain;
+ if ( '' === $plain ) {
+ return '';
+ }
+ if ( ! function_exists( 'openssl_encrypt' ) ) {
+ return base64_encode( $plain );
+ }
+ $iv = openssl_random_pseudo_bytes( 16 );
+ $cipher = openssl_encrypt( $plain, 'AES-256-CBC', self::key(), 0, $iv );
+ if ( false === $cipher ) {
+ return '';
+ }
+ return base64_encode( $iv . $cipher );
+ }
+
+ /**
+ * 解密密文。
+ *
+ * @param string $enc 密文(base64)。
+ * @return string 明文,失败返回空串。
+ */
+ public static function decrypt( $enc ) {
+ $enc = (string) $enc;
+ if ( '' === $enc ) {
+ return '';
+ }
+ $raw = base64_decode( $enc, true );
+ if ( false === $raw || strlen( $raw ) < 16 ) {
+ return '';
+ }
+ $iv = substr( $raw, 0, 16 );
+ $cipher = substr( $raw, 16 );
+ if ( ! function_exists( 'openssl_decrypt' ) ) {
+ return '';
+ }
+ $plain = openssl_decrypt( $cipher, 'AES-256-CBC', self::key(), 0, $iv );
+ return ( false === $plain ) ? '' : $plain;
+ }
+}
diff --git a/wp_seller_plugin/includes/class-helpers.php b/wp_seller_plugin/includes/class-helpers.php
new file mode 100644
index 0000000..f89aa31
--- /dev/null
+++ b/wp_seller_plugin/includes/class-helpers.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ * 通用工具:节点/主机查询、Client 构造、用户名生成、状态文案。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp;
+
+use MnbtWp\Mnbt\Client;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 工具类。
+ */
+class Helpers {
+
+ /**
+ * 按 ID 取节点配置行。
+ *
+ * @param int $id 节点 ID。
+ * @return array|null
+ */
+ public static function get_provider( $id ) {
+ global $wpdb;
+ $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mnbt_providers WHERE id=%d", (int) $id ), ARRAY_A );
+ return $row ? $row : null;
+ }
+
+ /**
+ * 根据节点行构造 Client(解密密钥)。
+ *
+ * @param array $provider 节点行。
+ * @return Client
+ */
+ public static function client_for_provider( array $provider ) {
+ return new Client(
+ array(
+ 'api_url' => $provider['api_url'],
+ 'btdh' => $provider['btdh'],
+ 'mn_key' => Crypto::decrypt( $provider['mn_key_enc'] ),
+ 'mn_keye' => Crypto::decrypt( $provider['mn_keye_enc'] ),
+ 'mn_vs' => (int) $provider['mn_vs'],
+ 'provider_id' => (int) $provider['id'],
+ )
+ );
+ }
+
+ /**
+ * 按 ID 取主机行。
+ *
+ * @param int $id 主机 ID。
+ * @return array|null
+ */
+ public static function get_host( $id ) {
+ global $wpdb;
+ $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mnbt_hosts WHERE id=%d", (int) $id ), ARRAY_A );
+ return $row ? $row : null;
+ }
+
+ /**
+ * 主机状态文案。
+ *
+ * @param string $status 状态码。
+ * @return string
+ */
+ public static function host_status_label( $status ) {
+ $labels = array(
+ 'pending' => __( '待开通', 'wp-seller-plugin' ),
+ 'provisioning' => __( '开通中', 'wp-seller-plugin' ),
+ 'active' => __( '运行中', 'wp-seller-plugin' ),
+ 'suspended' => __( '已暂停', 'wp-seller-plugin' ),
+ 'expired' => __( '已到期', 'wp-seller-plugin' ),
+ 'failed' => __( '开通失败', 'wp-seller-plugin' ),
+ );
+ return isset( $labels[ $status ] ) ? $labels[ $status ] : $status;
+ }
+
+ /**
+ * 生成主机用户名(≥6 位,避免与 MNBT 现有用户冲突)。
+ *
+ * @param int $user_id WP 用户 ID。
+ * @return string
+ */
+ public static function generate_username( $user_id ) {
+ $suffix = substr( str_replace( array( '0', '.' ), '', uniqid( '', true ) ), 0, 4 );
+ return 'wp' . (int) $user_id . $suffix;
+ }
+
+ /**
+ * 主机控制台地址(MNBT 用户登录页)。
+ *
+ * @param array $provider 节点行。
+ * @return string
+ */
+ public static function control_url( array $provider ) {
+ return untrailingslashit( $provider['api_url'] ) . '/user/login.php';
+ }
+
+ /**
+ * 从 quota_json 读取配额(含用量)。
+ *
+ * @param string $quota_json 主机 quota_json。
+ * @return array ['web'=>['max','used'],'sql'=>...,'flow'=>...]
+ */
+ public static function parse_quota( $quota_json ) {
+ $data = json_decode( (string) $quota_json, true );
+ if ( ! is_array( $data ) ) {
+ $data = array();
+ }
+ return array(
+ 'web' => isset( $data['web'] ) && is_array( $data['web'] ) ? $data['web'] : array( 'max' => 0, 'used' => 0 ),
+ 'sql' => isset( $data['sql'] ) && is_array( $data['sql'] ) ? $data['sql'] : array( 'max' => 0, 'used' => 0 ),
+ 'flow' => isset( $data['flow'] ) && is_array( $data['flow'] ) ? $data['flow'] : array( 'max' => 0, 'used' => 0 ),
+ );
+ }
+
+ /**
+ * 发送邮件(包装 wp_mail,统一发件人)。
+ *
+ * @param string $to 收件人。
+ * @param string $subject 主题。
+ * @param string $body HTML 正文。
+ * @return bool
+ */
+ public static function send_mail( $to, $subject, $body ) {
+ $headers = array( 'Content-Type: text/html; charset=UTF-8' );
+ return wp_mail( $to, $subject, $body, $headers );
+ }
+}
diff --git a/wp_seller_plugin/includes/class-logger.php b/wp_seller_plugin/includes/class-logger.php
new file mode 100644
index 0000000..1110a73
--- /dev/null
+++ b/wp_seller_plugin/includes/class-logger.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * 日志:API 调用日志(脱敏写入 mnbt_api_log 表)。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 日志器。
+ */
+class Logger {
+
+ /**
+ * 记录一次 API 调用。
+ *
+ * @param int $provider_id 节点配置 ID。
+ * @param string $action MNBT gn(cfif/kt/...)。
+ * @param array $params 请求参数(写入前脱敏)。
+ * @param int $code 返回 code(200 成功 / 100 业务失败 / 0 网络错误)。
+ * @param string $msg 返回消息或错误信息。
+ * @param float $duration 耗时(秒)。
+ */
+ public static function api( $provider_id, $action, array $params, $code, $msg, $duration ) {
+ global $wpdb;
+
+ $wpdb->insert(
+ $wpdb->prefix . 'mnbt_api_log',
+ array(
+ 'provider_id' => (int) $provider_id,
+ 'action' => sanitize_key( (string) $action ),
+ 'params_json' => wp_json_encode( self::mask( $params ) ),
+ 'code' => (int) $code,
+ 'msg' => mb_substr( (string) $msg, 0, 500 ),
+ 'duration' => round( (float) $duration, 3 ),
+ 'created_at' => current_time( 'mysql' ),
+ ),
+ array( '%d', '%s', '%s', '%d', '%s', '%f', '%s' )
+ );
+ }
+
+ /**
+ * 对敏感参数脱敏。
+ *
+ * @param array $params 原始参数。
+ * @return array
+ */
+ private static function mask( array $params ) {
+ $masked = $params;
+ foreach ( array( 'mn_key', 'mn_keye', 'password' ) as $k ) {
+ if ( array_key_exists( $k, $masked ) && '' !== $masked[ $k ] ) {
+ $masked[ $k ] = '***';
+ }
+ }
+ return $masked;
+ }
+}
diff --git a/wp_seller_plugin/includes/class-plugin.php b/wp_seller_plugin/includes/class-plugin.php
new file mode 100644
index 0000000..2081166
--- /dev/null
+++ b/wp_seller_plugin/includes/class-plugin.php
@@ -0,0 +1,109 @@
+<?php
+/**
+ * 插件生命周期:加载、文本域、激活/停用入口。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 插件主类(单例)。
+ */
+final class Plugin {
+
+ /**
+ * 单例实例。
+ *
+ * @var Plugin|null
+ */
+ private static $instance = null;
+
+ /**
+ * 获取单例。
+ *
+ * @return Plugin
+ */
+ public static function instance() {
+ if ( null === self::$instance ) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * 构造:加载依赖并挂载钩子。
+ */
+ private function __construct() {
+ $this->includes();
+ $this->hooks();
+ }
+
+ /**
+ * 加载插件类文件(按依赖顺序)。
+ */
+ private function includes() {
+ require_once MNBTWP_DIR . 'includes/class-activator.php';
+ require_once MNBTWP_DIR . 'includes/class-crypto.php';
+ require_once MNBTWP_DIR . 'includes/class-logger.php';
+ require_once MNBTWP_DIR . 'includes/class-helpers.php';
+ require_once MNBTWP_DIR . 'includes/mnbt/class-exception.php';
+ require_once MNBTWP_DIR . 'includes/mnbt/interface-provider.php';
+ require_once MNBTWP_DIR . 'includes/mnbt/class-client.php';
+ require_once MNBTWP_DIR . 'includes/services/class-provision.php';
+ require_once MNBTWP_DIR . 'includes/services/class-billing.php';
+ require_once MNBTWP_DIR . 'includes/services/class-cron.php';
+ require_once MNBTWP_DIR . 'includes/woocommerce/class-woo.php';
+ require_once MNBTWP_DIR . 'includes/front/class-front.php';
+
+ if ( is_admin() ) {
+ require_once MNBTWP_DIR . 'includes/admin/class-admin.php';
+ }
+ }
+
+ /**
+ * 挂载全局钩子。
+ */
+ private function hooks() {
+ add_action( 'init', array( $this, 'load_textdomain' ) );
+
+ \MnbtWp\Services\Cron::init();
+
+ if ( class_exists( 'WooCommerce' ) ) {
+ new \MnbtWp\Woo\Woo();
+ }
+
+ if ( ! is_admin() ) {
+ new Front();
+ } else {
+ new Admin();
+ }
+ }
+
+ /**
+ * 加载语言包。
+ */
+ public function load_textdomain() {
+ load_plugin_textdomain( 'wp-seller-plugin', false, dirname( plugin_basename( MNBTWP_FILE ) ) . '/languages' );
+ }
+
+ /**
+ * 激活钩子。
+ */
+ public static function activate() {
+ require_once MNBTWP_DIR . 'includes/class-activator.php';
+ Activator::activate();
+ }
+
+ /**
+ * 停用钩子。
+ */
+ public static function deactivate() {
+ require_once MNBTWP_DIR . 'includes/class-activator.php';
+ Activator::deactivate();
+ }
+}
diff --git a/wp_seller_plugin/includes/front/class-front.php b/wp_seller_plugin/includes/front/class-front.php
new file mode 100644
index 0000000..409e89a
--- /dev/null
+++ b/wp_seller_plugin/includes/front/class-front.php
@@ -0,0 +1,155 @@
+<?php
+/**
+ * 前台:我的主机 Shortcode + 前端 AJAX。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp;
+
+use MnbtWp\Services\Provision;
+use MnbtWp\Woo\Woo;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 前台控制器。
+ */
+class Front {
+
+ /**
+ * 构造:注册 Shortcode 与 AJAX。
+ */
+ public function __construct() {
+ add_shortcode( 'mnbt_my_hosts', array( $this, 'shortcode_my_hosts' ) );
+ add_action( 'wp_ajax_mnbtwp_host_action', array( $this, 'ajax_host_action' ) );
+ add_action( 'wp_ajax_nopriv_mnbtwp_host_action', array( $this, 'ajax_denied' ) );
+ add_action( 'wp_ajax_mnbtwp_create_renew', array( $this, 'ajax_create_renew' ) );
+ add_action( 'wp_ajax_nopriv_mnbtwp_create_renew', array( $this, 'ajax_denied' ) );
+ }
+
+ /**
+ * 未登录 AJAX 拒绝。
+ */
+ public function ajax_denied() {
+ wp_send_json_error( __( '请先登录', 'wp-seller-plugin' ) );
+ }
+
+ /**
+ * [mnbt_my_hosts] Shortcode:列出当前用户主机。
+ *
+ * @param array $atts Shortcode 属性。
+ * @return string
+ */
+ public function shortcode_my_hosts( $atts ) {
+ if ( ! is_user_logged_in() ) {
+ return '<p>' . esc_html__( '请先登录后查看您的主机。', 'wp-seller-plugin' ) . '</p>';
+ }
+
+ global $wpdb;
+ $user_id = get_current_user_id();
+ $hosts = $wpdb->get_results(
+ $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mnbt_hosts WHERE user_id=%d ORDER BY id DESC", $user_id ),
+ ARRAY_A
+ );
+
+ $provider_map = array();
+ foreach ( $hosts as $h ) {
+ if ( ! isset( $provider_map[ $h['provider_id'] ] ) ) {
+ $provider_map[ $h['provider_id'] ] = Helpers::get_provider( $h['provider_id'] );
+ }
+ }
+
+ ob_start();
+ include MNBTWP_DIR . 'includes/front/views/my-hosts.php';
+ return ob_get_clean();
+ }
+
+ /**
+ * 主机操作:start / stop / change_pass / delete。
+ */
+ public function ajax_host_action() {
+ check_ajax_referer( 'mnbtwp_front', 'nonce' );
+ if ( ! is_user_logged_in() ) {
+ wp_send_json_error( __( '请先登录', 'wp-seller-plugin' ) );
+ }
+
+ $action = sanitize_key( $_POST['host_action'] ?? '' );
+ $host_id = (int) ( $_POST['host_id'] ?? 0 );
+ $host = Helpers::get_host( $host_id );
+
+ if ( ! $host || (int) $host['user_id'] !== get_current_user_id() ) {
+ wp_send_json_error( __( '无权操作', 'wp-seller-plugin' ) );
+ }
+ $provider = Helpers::get_provider( $host['provider_id'] );
+ if ( ! $provider ) {
+ wp_send_json_error( __( '节点不可用', 'wp-seller-plugin' ) );
+ }
+
+ try {
+ $client = Helpers::client_for_provider( $provider );
+ switch ( $action ) {
+ case 'start':
+ $client->startSite( $host['username'] );
+ break;
+ case 'stop':
+ $client->stopSite( $host['username'] );
+ break;
+ case 'change_pass':
+ $password = sanitize_text_field( $_POST['password'] ?? '' );
+ if ( mb_strlen( $password ) < 6 ) {
+ wp_send_json_error( __( '密码不能少于 6 位', 'wp-seller-plugin' ) );
+ }
+ $client->changePassword( $host['username'], $password );
+ global $wpdb;
+ $wpdb->update(
+ $wpdb->prefix . 'mnbt_hosts',
+ array( 'password_enc' => Crypto::encrypt( $password ), 'updated_at' => current_time( 'mysql' ) ),
+ array( 'id' => (int) $host['id'] )
+ );
+ break;
+ case 'delete':
+ $client->deleteHost( $host['username'] );
+ global $wpdb;
+ $wpdb->delete( $wpdb->prefix . 'mnbt_hosts', array( 'id' => (int) $host['id'] ), array( '%d' ) );
+ break;
+ default:
+ wp_send_json_error( __( '未知操作', 'wp-seller-plugin' ) );
+ }
+ wp_send_json_success( __( '操作成功', 'wp-seller-plugin' ) );
+ } catch ( \Exception $e ) {
+ wp_send_json_error( $e->getMessage() );
+ }
+ }
+
+ /**
+ * 创建续费订单并返回结账支付地址。
+ */
+ public function ajax_create_renew() {
+ check_ajax_referer( 'mnbtwp_front', 'nonce' );
+ if ( ! is_user_logged_in() ) {
+ wp_send_json_error( __( '请先登录', 'wp-seller-plugin' ) );
+ }
+ $host_id = (int) ( $_POST['host_id'] ?? 0 );
+ $months = max( 1, (int) ( $_POST['months'] ?? 1 ) );
+ $host = Helpers::get_host( $host_id );
+ if ( ! $host || (int) $host['user_id'] !== get_current_user_id() ) {
+ wp_send_json_error( __( '无权操作', 'wp-seller-plugin' ) );
+ }
+ if ( ! class_exists( 'WooCommerce' ) ) {
+ wp_send_json_error( __( '未启用 WooCommerce', 'wp-seller-plugin' ) );
+ }
+ try {
+ $order = Woo::create_renew_order( $host_id, $months );
+ wp_send_json_success(
+ array(
+ 'checkout_url' => $order->get_checkout_payment_url(),
+ )
+ );
+ } catch ( \Exception $e ) {
+ wp_send_json_error( $e->getMessage() );
+ }
+ }
+}
diff --git a/wp_seller_plugin/includes/front/views/my-hosts.php b/wp_seller_plugin/includes/front/views/my-hosts.php
new file mode 100644
index 0000000..3cfe9b1
--- /dev/null
+++ b/wp_seller_plugin/includes/front/views/my-hosts.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * 我的主机视图([mnbt_my_hosts])。
+ *
+ * @package MnbtWp
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+$mnbtwp_nonce = wp_create_nonce( 'mnbtwp_front' );
+?>
+<div class="mnbtwp-hosts">
+ <?php if ( empty( $hosts ) ) : ?>
+ <p><?php esc_html_e( '您还没有购买任何主机。', 'wp-seller-plugin' ); ?></p>
+ <?php else : ?>
+ <?php foreach ( $hosts as $mnbtwp_host ) : ?>
+ <?php
+ $mnbtwp_provider = isset( $provider_map[ $mnbtwp_host['provider_id'] ] ) ? $provider_map[ $mnbtwp_host['provider_id'] ] : null;
+ $mnbtwp_quota = Helpers::parse_quota( $mnbtwp_host['quota_json'] );
+ $mnbtwp_status = $mnbtwp_host['status'];
+ $mnbtwp_label = Helpers::host_status_label( $mnbtwp_status );
+ $mnbtwp_console = $mnbtwp_provider ? Helpers::control_url( $mnbtwp_provider ) : '';
+ ?>
+ <div class="mnbtwp-host-card" style="border:1px solid #ddd;border-radius:8px;padding:16px;margin-bottom:16px;background:#fff;">
+ <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
+ <div>
+ <strong style="font-size:16px;"><?php echo esc_html( $mnbtwp_host['site_domain'] ? $mnbtwp_host['site_domain'] : $mnbtwp_host['username'] ); ?></strong>
+ <span style="margin-left:8px;padding:2px 8px;border-radius:10px;font-size:12px;
+ <?php echo in_array( $mnbtwp_status, array( 'active', 'provisioning' ), true ) ? 'background:#e6f7e6;color:#1a7f37;' : 'background:#fdeaea;color:#c62828;'; ?>">
+ <?php echo esc_html( $mnbtwp_label ); ?>
+ </span>
+ </div>
+ <div>
+ <?php if ( $mnbtwp_console ) : ?>
+ <a class="button" href="<?php echo esc_url( $mnbtwp_console ); ?>" target="_blank" rel="noopener">
+ <?php esc_html_e( '打开控制台', 'wp-seller-plugin' ); ?>
+ </a>
+ <?php endif; ?>
+ </div>
+ </div>
+
+ <table style="width:100%;margin-top:12px;border-collapse:collapse;font-size:13px;">
+ <tr>
+ <td style="padding:4px 0;color:#666;width:90px;"><?php esc_html_e( '主机账号', 'wp-seller-plugin' ); ?></td>
+ <td><code><?php echo esc_html( $mnbtwp_host['username'] ); ?></code></td>
+ <td style="padding:4px 0;color:#666;width:90px;"><?php esc_html_e( '到期时间', 'wp-seller-plugin' ); ?></td>
+ <td>
+ <?php
+ echo esc_html(
+ $mnbtwp_host['expire_date'] && '0000-00-00' !== $mnbtwp_host['expire_date']
+ ? $mnbtwp_host['expire_date']
+ : __( '长期', 'wp-seller-plugin' )
+ );
+ ?>
+ </td>
+ </tr>
+ </table>
+
+ <?php foreach ( array( 'web' => __( '网站空间', 'wp-seller-plugin' ), 'sql' => __( '数据库', 'wp-seller-plugin' ), 'flow' => __( '流量', 'wp-seller-plugin' ) ) as $mnbtwp_key => $mnbtwp_name ) : ?>
+ <?php
+ $mnbtwp_max = (int) ( $mnbtwp_quota[ $mnbtwp_key ]['max'] ?? 0 );
+ $mnbtwp_used = (int) ( $mnbtwp_quota[ $mnbtwp_key ]['used'] ?? 0 );
+ if ( $mnbtwp_max <= 0 ) {
+ continue;
+ }
+ $mnbtwp_pct = min( 100, (int) round( $mnbtwp_used / $mnbtwp_max * 100 ) );
+ ?>
+ <div style="margin-top:8px;">
+ <div style="display:flex;justify-content:space-between;font-size:12px;color:#666;">
+ <span><?php echo esc_html( $mnbtwp_name ); ?></span>
+ <span><?php echo (int) $mnbtwp_used; ?> / <?php echo (int) $mnbtwp_max; ?> MB</span>
+ </div>
+ <div style="background:#eee;border-radius:4px;height:8px;margin-top:4px;">
+ <div style="width:<?php echo (int) $mnbtwp_pct; ?>%;height:8px;border-radius:4px;background:<?php echo $mnbtwp_pct >= 90 ? '#d63638' : '#2271b1'; ?>;"></div>
+ </div>
+ </div>
+ <?php endforeach; ?>
+
+ <div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap;">
+ <button type="button" class="button" onclick="mnbtwpAction(<?php echo (int) $mnbtwp_host['id']; ?>,'start')"><?php esc_html_e( '启动', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button" onclick="mnbtwpAction(<?php echo (int) $mnbtwp_host['id']; ?>,'stop')"><?php esc_html_e( '停止', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button" onclick="mnbtwpChangePass(<?php echo (int) $mnbtwp_host['id']; ?>)"><?php esc_html_e( '重置密码', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button" onclick="mnbtwpRenew(<?php echo (int) $mnbtwp_host['id']; ?>)"><?php esc_html_e( '续费', 'wp-seller-plugin' ); ?></button>
+ <button type="button" class="button" style="color:#c62828;" onclick="mnbtwpDelete(<?php echo (int) $mnbtwp_host['id']; ?>)"><?php esc_html_e( '删除', 'wp-seller-plugin' ); ?></button>
+ </div>
+ </div>
+ <?php endforeach; ?>
+ <?php endif; ?>
+</div>
+
+<style>
+.mnbtwp-host-card .button { margin: 0; }
+</style>
+
+<script type="text/javascript">
+ var mnbtwpFrontNonce = '<?php echo esc_js( $mnbtwp_nonce ); ?>';
+
+ function mnbtwpAjax(data, done) {
+ data.nonce = mnbtwpFrontNonce;
+ jQuery.post('<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>', data, function (r) {
+ if (r && r.success) {
+ done(r);
+ } else {
+ alert((r && r.data) ? r.data : '<?php echo esc_js( __( '操作失败', 'wp-seller-plugin' ) ); ?>');
+ }
+ }).fail(function () {
+ alert('<?php echo esc_js( __( '网络错误', 'wp-seller-plugin' ) ); ?>');
+ });
+ }
+
+ function mnbtwpAction(hostId, action) {
+ if (action === 'delete') return;
+ mnbtwpAjax({ action: 'mnbtwp_host_action', host_action: action, host_id: hostId }, function () {
+ location.reload();
+ });
+ }
+
+ function mnbtwpChangePass(hostId) {
+ var pw = prompt('<?php echo esc_js( __( '请输入新密码(至少 6 位)', 'wp-seller-plugin' ) ); ?>');
+ if (!pw || pw.length < 6) return;
+ mnbtwpAjax({ action: 'mnbtwp_host_action', host_action: 'change_pass', host_id: hostId, password: pw }, function () {
+ alert('<?php echo esc_js( __( '密码已重置', 'wp-seller-plugin' ) ); ?>');
+ });
+ }
+
+ function mnbtwpDelete(hostId) {
+ if (!confirm('<?php echo esc_js( __( '删除后站点、数据库、FTP 将被清除且不可恢复,确定删除?', 'wp-seller-plugin' ) ); ?>')) return;
+ mnbtwpAjax({ action: 'mnbtwp_host_action', host_action: 'delete', host_id: hostId }, function () {
+ location.reload();
+ });
+ }
+
+ function mnbtwpRenew(hostId) {
+ var months = prompt('<?php echo esc_js( __( '请输入续费月数(如 1 / 3 / 6 / 12)', 'wp-seller-plugin' ) ); ?>', '1');
+ if (!months || parseInt(months, 10) < 1) return;
+ mnbtwpAjax({ action: 'mnbtwp_create_renew', host_id: hostId, months: parseInt(months, 10) }, function (r) {
+ if (r.data && r.data.checkout_url) {
+ window.location.href = r.data.checkout_url;
+ }
+ });
+ }
+</script>
diff --git a/wp_seller_plugin/includes/mnbt/class-client.php b/wp_seller_plugin/includes/mnbt/class-client.php
new file mode 100644
index 0000000..67a6644
--- /dev/null
+++ b/wp_seller_plugin/includes/mnbt/class-client.php
@@ -0,0 +1,302 @@
+<?php
+/**
+ * MNBT API 客户端(适配器实现)。
+ *
+ * 封装 MNBT `api/api.php` 的鉴权、请求、重试与错误归一化。
+ * 鉴权参数:mn_bh(节点代号)/ mn_key(系统密钥)/ mn_keye(调用密钥 md5(ktmy.qmk))/ mn_vs(版本号)。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp\Mnbt;
+
+use MnbtWp\Logger;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * MNBT API 客户端。
+ */
+class Client implements HostProviderInterface {
+
+ /**
+ * 节点配置。
+ *
+ * @var array
+ */
+ private $config = array();
+
+ /**
+ * 构造。
+ *
+ * @param array $config 节点配置(api_url/btdh/mn_key/mn_keye/mn_vs/provider_id)。
+ */
+ public function __construct( array $config ) {
+ $this->config = wp_parse_args(
+ $config,
+ array(
+ 'api_url' => '',
+ 'btdh' => '',
+ 'mn_key' => '',
+ 'mn_keye' => '',
+ 'mn_vs' => 15,
+ 'provider_id' => 0,
+ )
+ );
+ }
+
+ /**
+ * 拼接 API 地址。
+ *
+ * @return string
+ */
+ private function url() {
+ return untrailingslashit( $this->config['api_url'] ) . '/api/api.php';
+ }
+
+ /**
+ * 连接测试。
+ *
+ * @return array
+ */
+ public function testConnection() {
+ return $this->request( 'cfif', array( 'username' => 'mnbtwp_test' ) );
+ }
+
+ /**
+ * 开通主机。
+ *
+ * @param array $params 开通参数。
+ * @return array
+ */
+ public function createHost( array $params ) {
+ $params = wp_parse_args(
+ $params,
+ array(
+ 'password' => '',
+ 'sizemax' => 0,
+ 'dqtime' => 0,
+ 'webdx' => 0,
+ 'sqldx' => 0,
+ 'ymbds' => 0,
+ )
+ );
+ return $this->request( 'kt', $params );
+ }
+
+ /**
+ * 暂停主机。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function suspendHost( $username ) {
+ return $this->request( 'zt', array( 'username' => $username ) );
+ }
+
+ /**
+ * 恢复主机。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function resumeHost( $username ) {
+ return $this->request( 'jc', array( 'username' => $username ) );
+ }
+
+ /**
+ * 删除主机。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function deleteHost( $username ) {
+ return $this->request( 'tz', array( 'username' => $username ) );
+ }
+
+ /**
+ * 续费。
+ *
+ * @param string $username 主机用户名。
+ * @param string $expire_date 到期日期 Y-m-d。
+ * @return array
+ */
+ public function renewHost( $username, $expire_date ) {
+ return $this->request(
+ 'xf',
+ array(
+ 'username' => $username,
+ 'setdate' => $expire_date,
+ )
+ );
+ }
+
+ /**
+ * 重置密码。
+ *
+ * @param string $username 主机用户名。
+ * @param string $password 新密码。
+ * @return array
+ */
+ public function changePassword( $username, $password ) {
+ return $this->request(
+ 'czmm',
+ array(
+ 'username' => $username,
+ 'password' => $password,
+ )
+ );
+ }
+
+ /**
+ * 升降级。
+ *
+ * @param string $username 主机用户名。
+ * @param array $quota 配额(websize/sqlsize/ll,MB)。
+ * @return array
+ */
+ public function changePackage( $username, array $quota ) {
+ $quota = wp_parse_args(
+ $quota,
+ array(
+ 'websize' => 0,
+ 'sqlsize' => 0,
+ 'll' => 0,
+ )
+ );
+ return $this->request(
+ 'zjmode',
+ array(
+ 'username' => $username,
+ 'websize' => $quota['websize'],
+ 'sqlsize' => $quota['sqlsize'],
+ 'll' => $quota['ll'],
+ )
+ );
+ }
+
+ /**
+ * 启动站点。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function startSite( $username ) {
+ return $this->request( 'start', array( 'username' => $username ) );
+ }
+
+ /**
+ * 停止站点。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function stopSite( $username ) {
+ return $this->request( 'stop', array( 'username' => $username ) );
+ }
+
+ /**
+ * 状态与配额查询。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function getHostStatus( $username ) {
+ return $this->request( 'ztcx', array( 'username' => $username ) );
+ }
+
+ /**
+ * 发起 MNBT API 请求。
+ *
+ * 网络层失败(超时/连接失败)自动重试 2 次(1s/2s 退避);
+ * 业务失败(code!=200)不重试,抛出异常。
+ *
+ * @param string $gn API 动作。
+ * @param array $params 业务参数。
+ * @return array ['ok'=>true, 'msg'=>string, 'data'=>array]
+ *
+ * @throws Exception 请求失败时抛出。
+ */
+ private function request( $gn, array $params ) {
+ $body = array_merge(
+ array(
+ 'mn_bh' => $this->config['btdh'],
+ 'mn_key' => $this->config['mn_key'],
+ 'mn_keye' => $this->config['mn_keye'],
+ 'mn_vs' => (int) $this->config['mn_vs'],
+ ),
+ $params
+ );
+ if ( empty( $body['username'] ) ) {
+ $body['username'] = 'mnbtwp_test';
+ }
+
+ $start = microtime( true );
+ $response = null;
+ $last_error = '';
+
+ // 网络层失败重试(最多 3 次尝试)。
+ for ( $attempt = 0; $attempt < 3; $attempt++ ) {
+ if ( $attempt > 0 ) {
+ usleep( $attempt * 1000000 ); // 1s / 2s 退避。
+ }
+ $response = wp_remote_post(
+ $this->url(),
+ array(
+ 'timeout' => 15,
+ 'body' => $body,
+ 'sslverify' => true,
+ )
+ );
+ if ( ! is_wp_error( $response ) ) {
+ break;
+ }
+ $last_error = $response->get_error_message();
+ }
+
+ $duration = microtime( true ) - $start;
+
+ if ( is_wp_error( $response ) ) {
+ Logger::api( (int) $this->config['provider_id'], $gn, $body, 0, '网络错误:' . $last_error, $duration );
+ throw ( new Exception( '网络错误:' . $last_error ) )->set_context(
+ array( 'gn' => $gn, 'provider_id' => $this->config['provider_id'] )
+ );
+ }
+
+ $code = wp_remote_retrieve_response_code( $response );
+ if ( 200 !== $code ) {
+ Logger::api( (int) $this->config['provider_id'], $gn, $body, 0, 'HTTP ' . $code, $duration );
+ throw ( new Exception( 'MNBT 接口返回 HTTP ' . $code ) )->set_context(
+ array( 'gn' => $gn, 'provider_id' => $this->config['provider_id'], 'http_code' => $code )
+ );
+ }
+
+ $raw = wp_remote_retrieve_body( $response );
+ $data = json_decode( $raw, true );
+ if ( ! is_array( $data ) ) {
+ Logger::api( (int) $this->config['provider_id'], $gn, $body, 0, '响应解析失败', $duration );
+ throw ( new Exception( 'MNBT 响应解析失败' ) )->set_context(
+ array( 'gn' => $gn, 'provider_id' => $this->config['provider_id'] )
+ );
+ }
+
+ $ok = isset( $data['code'] ) && 200 === (int) $data['code'];
+ $msg = isset( $data['msg'] ) ? (string) $data['msg'] : 'ok';
+
+ Logger::api( (int) $this->config['provider_id'], $gn, $body, $ok ? 200 : 100, $msg, $duration );
+
+ if ( ! $ok ) {
+ throw ( new Exception( $msg ) )->set_context(
+ array( 'gn' => $gn, 'provider_id' => $this->config['provider_id'], 'code' => isset( $data['code'] ) ? $data['code'] : null )
+ );
+ }
+
+ return array(
+ 'ok' => true,
+ 'msg' => $msg,
+ 'data' => isset( $data['data'] ) && is_array( $data['data'] ) ? $data['data'] : array(),
+ );
+ }
+}
diff --git a/wp_seller_plugin/includes/mnbt/class-exception.php b/wp_seller_plugin/includes/mnbt/class-exception.php
new file mode 100644
index 0000000..ace7890
--- /dev/null
+++ b/wp_seller_plugin/includes/mnbt/class-exception.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * MNBT 对接异常:错误归一化后统一抛出。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp\Mnbt;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * MNBT API 异常。
+ */
+class Exception extends \Exception {
+
+ /**
+ * 原始响应数据(脱敏后),便于上层记录上下文。
+ *
+ * @var array
+ */
+ protected $context = array();
+
+ /**
+ * 设置上下文。
+ *
+ * @param array $context 上下文。
+ * @return $this
+ */
+ public function set_context( array $context ) {
+ $this->context = $context;
+ return $this;
+ }
+
+ /**
+ * 获取上下文。
+ *
+ * @return array
+ */
+ public function get_context() {
+ return $this->context;
+ }
+}
diff --git a/wp_seller_plugin/includes/mnbt/interface-provider.php b/wp_seller_plugin/includes/mnbt/interface-provider.php
new file mode 100644
index 0000000..8e15c30
--- /dev/null
+++ b/wp_seller_plugin/includes/mnbt/interface-provider.php
@@ -0,0 +1,111 @@
+<?php
+/**
+ * 主机服务商适配器接口。
+ *
+ * 业务层只依赖本接口,便于未来扩展其他面板/系统
+ * (如直接对接宝塔、其他财务系统),不修改业务代码。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp\Mnbt;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 主机服务商接口。
+ */
+interface HostProviderInterface {
+
+ /**
+ * 连接测试。
+ *
+ * @return array ['ok'=>bool, 'msg'=>string, 'data'=>array]
+ */
+ public function testConnection();
+
+ /**
+ * 开通主机(建站 + FTP + 数据库)。
+ *
+ * @param array $params 开通参数(password/sizemax/dqtime/webdx/sqldx/ymbds 等)。
+ * @return array
+ */
+ public function createHost( array $params );
+
+ /**
+ * 暂停主机。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function suspendHost( $username );
+
+ /**
+ * 恢复主机。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function resumeHost( $username );
+
+ /**
+ * 删除主机。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function deleteHost( $username );
+
+ /**
+ * 续费(更新到期时间)。
+ *
+ * @param string $username 主机用户名。
+ * @param string $expire_date 到期日期 Y-m-d。
+ * @return array
+ */
+ public function renewHost( $username, $expire_date );
+
+ /**
+ * 重置密码(FTP + 控制面板)。
+ *
+ * @param string $username 主机用户名。
+ * @param string $password 新密码。
+ * @return array
+ */
+ public function changePassword( $username, $password );
+
+ /**
+ * 升降级(更新配额)。
+ *
+ * @param string $username 主机用户名。
+ * @param array $quota 配额 ['websize'=>, 'sqlsize'=>, 'll'=>](MB)。
+ * @return array
+ */
+ public function changePackage( $username, array $quota );
+
+ /**
+ * 启动站点。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function startSite( $username );
+
+ /**
+ * 停止站点。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function stopSite( $username );
+
+ /**
+ * 查询主机状态与配额用量。
+ *
+ * @param string $username 主机用户名。
+ * @return array
+ */
+ public function getHostStatus( $username );
+}
diff --git a/wp_seller_plugin/includes/services/class-billing.php b/wp_seller_plugin/includes/services/class-billing.php
new file mode 100644
index 0000000..9de4e54
--- /dev/null
+++ b/wp_seller_plugin/includes/services/class-billing.php
@@ -0,0 +1,176 @@
+<?php
+/**
+ * 计费:续费处理与到期维护(暂停/提醒)。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp\Services;
+
+use MnbtWp\Helpers;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 计费服务。
+ */
+class Billing {
+
+ /**
+ * 续费订单支付完成后处理:xf 更新到期时间,必要时恢复主机。
+ *
+ * @param int $order_id 订单 ID(含 _mnbtwp_renew_host_id / _mnbtwp_renew_months meta)。
+ * @return array
+ *
+ * @throws \Exception 续费失败。
+ */
+ public static function renew_order( $order_id ) {
+ $host_id = (int) get_post_meta( $order_id, '_mnbtwp_renew_host_id', true );
+ if ( ! $host_id ) {
+ throw new \Exception( __( '续费订单缺少主机信息', 'wp-seller-plugin' ) );
+ }
+ if ( get_post_meta( $order_id, '_mnbtwp_renewed', true ) ) {
+ return array( 'ok' => true, 'msg' => __( '该订单已续费', 'wp-seller-plugin' ) );
+ }
+
+ $host = Helpers::get_host( $host_id );
+ if ( ! $host ) {
+ throw new \Exception( __( '主机不存在', 'wp-seller-plugin' ) );
+ }
+
+ $months = max( 1, (int) get_post_meta( $order_id, '_mnbtwp_renew_months', true ) ?: 1 );
+ $base = ( $host['expire_date'] && '0000-00-00' !== $host['expire_date'] && strtotime( $host['expire_date'] ) > time() )
+ ? $host['expire_date']
+ : date( 'Y-m-d' );
+ $new_date = date( 'Y-m-d', strtotime( "+{$months} months", strtotime( $base ) ) );
+
+ $provider = Helpers::get_provider( $host['provider_id'] );
+ if ( ! $provider ) {
+ throw new \Exception( __( '节点配置不存在', 'wp-seller-plugin' ) );
+ }
+
+ $client = Helpers::client_for_provider( $provider );
+ $client->renewHost( $host['username'], $new_date );
+
+ global $wpdb;
+ $wpdb->update(
+ $wpdb->prefix . 'mnbt_hosts',
+ array(
+ 'expire_date' => $new_date,
+ 'updated_at' => current_time( 'mysql' ),
+ ),
+ array( 'id' => (int) $host['id'] )
+ );
+
+ // 已到期/暂停的主机续费后恢复。
+ if ( in_array( $host['status'], array( 'expired', 'suspended' ), true ) ) {
+ try {
+ $client->resumeHost( $host['username'] );
+ $wpdb->update(
+ $wpdb->prefix . 'mnbt_hosts',
+ array( 'status' => 'active', 'updated_at' => current_time( 'mysql' ) ),
+ array( 'id' => (int) $host['id'] )
+ );
+ } catch ( \Exception $e ) { // 恢复失败不影响续费结果。
+ }
+ }
+
+ update_post_meta( $order_id, '_mnbtwp_renewed', '1' );
+ self::notify_renew( $host, $new_date );
+
+ return array( 'ok' => true, 'msg' => __( '续费成功', 'wp-seller-plugin' ), 'expire_date' => $new_date );
+ }
+
+ /**
+ * 每日到期维护:过期自动暂停 + 到期提醒。
+ */
+ public static function expire_maintenance() {
+ global $wpdb;
+
+ $settings = get_option( 'mnbtwp_settings', array() );
+ $grace_days = max( 0, (int) ( $settings['grace_days'] ?? 0 ) );
+ $remind_days = max( 0, (int) ( $settings['expire_days'] ?? 3 ) );
+
+ $hosts = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}mnbt_hosts WHERE status='active'", ARRAY_A );
+ $now = time();
+
+ foreach ( $hosts as $host ) {
+ if ( empty( $host['expire_date'] ) || '0000-00-00' === $host['expire_date'] ) {
+ continue;
+ }
+ $expire_ts = strtotime( $host['expire_date'] . ' 23:59:59' );
+
+ // 已过期且超宽限期 → 暂停。
+ if ( $expire_ts + $grace_days * DAY_IN_SECONDS < $now ) {
+ $provider = Helpers::get_provider( $host['provider_id'] );
+ if ( $provider ) {
+ try {
+ Helpers::client_for_provider( $provider )->suspendHost( $host['username'] );
+ $wpdb->update(
+ $wpdb->prefix . 'mnbt_hosts',
+ array( 'status' => 'expired', 'updated_at' => current_time( 'mysql' ) ),
+ array( 'id' => (int) $host['id'] )
+ );
+ } catch ( \Exception $e ) {
+ // 暂停失败留待下次维护重试。
+ }
+ }
+ continue;
+ }
+
+ // 到期前提醒(仅一次/每周期一次)。
+ if ( $remind_days > 0 ) {
+ $remind_ts = $expire_ts - $remind_days * DAY_IN_SECONDS;
+ if ( $now >= $remind_ts && $now < $expire_ts ) {
+ $last = $host['last_remind'] ? strtotime( $host['last_remind'] ) : 0;
+ if ( $last < $remind_ts ) {
+ self::send_expire_remind( $host );
+ $wpdb->update(
+ $wpdb->prefix . 'mnbt_hosts',
+ array( 'last_remind' => current_time( 'mysql' ) ),
+ array( 'id' => (int) $host['id'] )
+ );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 发送到期提醒邮件。
+ *
+ * @param array $host 主机行。
+ */
+ private static function send_expire_remind( array $host ) {
+ $user = get_userdata( (int) $host['user_id'] );
+ if ( ! $user || empty( $user->user_email ) ) {
+ return;
+ }
+ $subject = sprintf( __( '【%s】主机即将到期', 'wp-seller-plugin' ), get_bloginfo( 'name' ) );
+ $body = '<p>' . esc_html__( '您的主机即将到期,请及时续费以免暂停服务:', 'wp-seller-plugin' ) . '</p>'
+ . '<p>主机账号:<code>' . esc_html( $host['username'] ) . '</code></p>'
+ . '<p>' . esc_html__( '到期时间:', 'wp-seller-plugin' ) . esc_html( $host['expire_date'] ) . '</p>'
+ . '<p>' . esc_html__( '请在「我的主机」中点击续费完成支付。', 'wp-seller-plugin' ) . '</p>';
+ Helpers::send_mail( $user->user_email, $subject, $body );
+ }
+
+ /**
+ * 发送续费成功邮件。
+ *
+ * @param array $host 主机行。
+ * @param string $new_date 新到期日期。
+ */
+ private static function notify_renew( array $host, $new_date ) {
+ $user = get_userdata( (int) $host['user_id'] );
+ if ( ! $user || empty( $user->user_email ) ) {
+ return;
+ }
+ $subject = sprintf( __( '【%s】主机续费成功', 'wp-seller-plugin' ), get_bloginfo( 'name' ) );
+ $body = '<p>' . esc_html__( '您的主机已成功续费:', 'wp-seller-plugin' ) . '</p>'
+ . '<p>主机账号:<code>' . esc_html( $host['username'] ) . '</code></p>'
+ . '<p>' . esc_html__( '新到期时间:', 'wp-seller-plugin' ) . esc_html( $new_date ) . '</p>';
+ Helpers::send_mail( $user->user_email, $subject, $body );
+ }
+}
diff --git a/wp_seller_plugin/includes/services/class-cron.php b/wp_seller_plugin/includes/services/class-cron.php
new file mode 100644
index 0000000..2773a37
--- /dev/null
+++ b/wp_seller_plugin/includes/services/class-cron.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * 定时任务:每日到期维护 + 状态/用量同步。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp\Services;
+
+use MnbtWp\Helpers;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 定时任务调度。
+ */
+class Cron {
+
+ /**
+ * 注册定时任务回调。
+ */
+ public static function init() {
+ add_action( 'mnbtwp_daily_maintenance', array( __CLASS__, 'daily' ) );
+ add_action( 'mnbtwp_sync_hosts', array( __CLASS__, 'sync' ) );
+ }
+
+ /**
+ * 每日到期维护。
+ */
+ public static function daily() {
+ Billing::expire_maintenance();
+ }
+
+ /**
+ * 同步主机状态与用量(ztcx),带锁防并发。
+ */
+ public static function sync() {
+ if ( get_transient( 'mnbtwp_sync_lock' ) ) {
+ return;
+ }
+ set_transient( 'mnbtwp_sync_lock', 1, 10 * MINUTE_IN_SECONDS );
+
+ global $wpdb;
+ $hosts = $wpdb->get_results(
+ "SELECT * FROM {$wpdb->prefix}mnbt_hosts WHERE status IN ('active','suspended','expired')",
+ ARRAY_A
+ );
+
+ foreach ( $hosts as $host ) {
+ $provider = Helpers::get_provider( $host['provider_id'] );
+ if ( ! $provider ) {
+ continue;
+ }
+ try {
+ $result = Helpers::client_for_provider( $provider )->getHostStatus( $host['username'] );
+ $data = isset( $result['data'] ) && is_array( $result['data'] ) ? $result['data'] : array();
+
+ $quota = Helpers::parse_quota( $host['quota_json'] );
+ if ( isset( $data['quota'] ) && is_array( $data['quota'] ) ) {
+ $q = $data['quota'];
+ $quota['web']['max'] = (int) ( $q['web_size_max'] ?? $quota['web']['max'] );
+ $quota['web']['used'] = (int) ( $q['web_size_used'] ?? $quota['web']['used'] );
+ $quota['sql']['max'] = (int) ( $q['sql_size_max'] ?? $quota['sql']['max'] );
+ $quota['sql']['used'] = (int) ( $q['sql_size_used'] ?? $quota['sql']['used'] );
+ $quota['flow']['max'] = (int) ( $q['flow_max'] ?? $quota['flow']['max'] );
+ $quota['flow']['used'] = (int) ( $q['flow_used'] ?? $quota['flow']['used'] );
+ }
+
+ $site_domain = isset( $data['user']['domain'] ) ? (string) $data['user']['domain'] : '';
+
+ $wpdb->update(
+ $wpdb->prefix . 'mnbt_hosts',
+ array(
+ 'site_domain' => $site_domain ? $site_domain : $host['site_domain'],
+ 'quota_json' => wp_json_encode( $quota ),
+ 'updated_at' => current_time( 'mysql' ),
+ ),
+ array( 'id' => (int) $host['id'] )
+ );
+ } catch ( \Exception $e ) {
+ // 单台同步失败不影响其他主机。
+ continue;
+ }
+ }
+
+ delete_transient( 'mnbtwp_sync_lock' );
+ }
+}
diff --git a/wp_seller_plugin/includes/services/class-provision.php b/wp_seller_plugin/includes/services/class-provision.php
new file mode 100644
index 0000000..3f30c64
--- /dev/null
+++ b/wp_seller_plugin/includes/services/class-provision.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ * 开通编排:订单支付成功后创建主机(幂等 + 失败记录可重试)。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp\Services;
+
+use MnbtWp\Crypto;
+use MnbtWp\Helpers;
+use MnbtWp\Mnbt\Exception as MnbtException;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * 开通服务。
+ */
+class Provision {
+
+ /**
+ * 根据订单开通主机。
+ *
+ * 幂等:订单已开通(_mnbtwp_provisioned)则直接返回。
+ * 失败:抛出异常并记录 _mnbtwp_provision_error,可后台重试。
+ *
+ * @param int $order_id 订单 ID。
+ * @return array ['ok'=>bool, 'msg'=>string, 'host_id'=>int]
+ *
+ * @throws \Exception 开通失败。
+ */
+ public static function provision_order( $order_id ) {
+ $order = wc_get_order( $order_id );
+ if ( ! $order ) {
+ throw new \Exception( __( '订单不存在', 'wp-seller-plugin' ) );
+ }
+
+ if ( get_post_meta( $order_id, '_mnbtwp_provisioned', true ) ) {
+ return array(
+ 'ok' => true,
+ 'msg' => __( '该订单已开通', 'wp-seller-plugin' ),
+ 'host_id' => (int) get_post_meta( $order_id, '_mnbtwp_host_id', true ),
+ );
+ }
+
+ // 定位主机商品。
+ $product_id = 0;
+ foreach ( $order->get_items() as $item ) {
+ $pid = (int) $item->get_product_id();
+ if ( 'yes' === get_post_meta( $pid, '_mnbtwp_enabled', true ) ) {
+ $product_id = $pid;
+ break;
+ }
+ }
+ if ( ! $product_id ) {
+ throw new \Exception( __( '订单中无主机商品', 'wp-seller-plugin' ) );
+ }
+
+ $provider_id = (int) get_post_meta( $product_id, '_mnbtwp_provider_id', true );
+ $provider = Helpers::get_provider( $provider_id );
+ if ( ! $provider || 'true' !== $provider['qk'] ) {
+ throw new \Exception( __( '节点配置不可用', 'wp-seller-plugin' ) );
+ }
+
+ $user_id = (int) $order->get_user_id();
+ $username = Helpers::generate_username( $user_id );
+ $password = wp_generate_password( 12, true );
+
+ $months = max( 1, (int) get_post_meta( $product_id, '_mnbtwp_period_months', true ) ?: 12 );
+ $expire = date( 'Y-m-d', strtotime( "+{$months} months" ) );
+ $webdx = (int) get_post_meta( $product_id, '_mnbtwp_webdx', true );
+ $sqldx = (int) get_post_meta( $product_id, '_mnbtwp_sqldx', true );
+ $flow = (int) get_post_meta( $product_id, '_mnbtwp_ll', true );
+ $ymbds = (int) get_post_meta( $product_id, '_mnbtwp_ymbds', true );
+
+ $client = Helpers::client_for_provider( $provider );
+ try {
+ $client->createHost(
+ array(
+ 'username' => $username,
+ 'password' => $password,
+ 'webdx' => $webdx,
+ 'sqldx' => $sqldx,
+ 'll' => $flow,
+ 'ymbds' => $ymbds,
+ 'dqtime' => $expire,
+ )
+ );
+ } catch ( MnbtException $e ) {
+ update_post_meta( $order_id, '_mnbtwp_provision_error', $e->getMessage() );
+ throw $e;
+ }
+
+ global $wpdb;
+ $quota = wp_json_encode(
+ array(
+ 'web' => array( 'max' => $webdx, 'used' => 0 ),
+ 'sql' => array( 'max' => $sqldx, 'used' => 0 ),
+ 'flow' => array( 'max' => $flow, 'used' => 0 ),
+ )
+ );
+
+ $wpdb->insert(
+ $wpdb->prefix . 'mnbt_hosts',
+ array(
+ 'provider_id' => $provider_id,
+ 'order_id' => (int) $order_id,
+ 'user_id' => $user_id,
+ 'username' => $username,
+ 'password_enc' => Crypto::encrypt( $password ),
+ 'site_domain' => '',
+ 'status' => 'active',
+ 'expire_date' => $expire,
+ 'quota_json' => $quota,
+ 'created_at' => current_time( 'mysql' ),
+ 'updated_at' => current_time( 'mysql' ),
+ ),
+ array( '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
+ );
+ $host_id = (int) $wpdb->insert_id;
+
+ update_post_meta( $order_id, '_mnbtwp_provisioned', '1' );
+ update_post_meta( $order_id, '_mnbtwp_host_id', $host_id );
+ delete_post_meta( $order_id, '_mnbtwp_provision_error' );
+
+ self::notify( $user_id, $username, $password, $provider, $expire );
+
+ return array(
+ 'ok' => true,
+ 'msg' => __( '主机开通成功', 'wp-seller-plugin' ),
+ 'host_id' => $host_id,
+ );
+ }
+
+ /**
+ * 发送开通通知邮件(含账号/密码/控制台地址)。
+ *
+ * @param int $user_id WP 用户 ID。
+ * @param string $username 主机用户名。
+ * @param string $password 明文密码。
+ * @param array $provider 节点行。
+ * @param string $expire 到期日期。
+ */
+ private static function notify( $user_id, $username, $password, array $provider, $expire ) {
+ $user = get_userdata( $user_id );
+ if ( ! $user || empty( $user->user_email ) ) {
+ return;
+ }
+ $subject = sprintf( __( '【%s】主机开通成功', 'wp-seller-plugin' ), get_bloginfo( 'name' ) );
+ $body = '<p>' . esc_html__( '您购买的虚拟主机已开通:', 'wp-seller-plugin' ) . '</p>'
+ . '<p>主机账号:<code>' . esc_html( $username ) . '</code></p>'
+ . '<p>主机密码:<code>' . esc_html( $password ) . '</code></p>'
+ . '<p>' . esc_html__( '到期时间:', 'wp-seller-plugin' ) . esc_html( $expire ) . '</p>'
+ . '<p>' . esc_html__( '控制台:', 'wp-seller-plugin' ) . '<a href="' . esc_url( Helpers::control_url( $provider ) ) . '">' . esc_html( Helpers::control_url( $provider ) ) . '</a></p>'
+ . '<p style="color:#888;">' . esc_html__( '请妥善保管账号密码,可在「我的主机」中随时重置密码。', 'wp-seller-plugin' ) . '</p>';
+ Helpers::send_mail( $user->user_email, $subject, $body );
+ }
+}
diff --git a/wp_seller_plugin/includes/woocommerce/class-woo.php b/wp_seller_plugin/includes/woocommerce/class-woo.php
new file mode 100644
index 0000000..5e8c01c
--- /dev/null
+++ b/wp_seller_plugin/includes/woocommerce/class-woo.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * WooCommerce 集成:商品扩展、支付回调开通/续费、续费下单。
+ *
+ * @package MnbtWp
+ */
+
+namespace MnbtWp\Woo;
+
+use MnbtWp\Helpers;
+use MnbtWp\Services\Billing;
+use MnbtWp\Services\Provision;
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * WooCommerce 集成。
+ */
+class Woo {
+
+ /**
+ * 构造:挂载钩子。
+ */
+ public function __construct() {
+ add_action( 'woocommerce_product_options_general_product_data', array( $this, 'product_meta_fields' ) );
+ add_action( 'woocommerce_process_product_meta', array( $this, 'save_product_meta' ) );
+ add_action( 'woocommerce_payment_complete', array( $this, 'on_order_paid' ), 20, 1 );
+ add_action( 'woocommerce_order_status_completed', array( $this, 'on_order_paid' ), 20, 1 );
+ }
+
+ /**
+ * 商品常规设置面板输出 MNBT 主机套餐字段。
+ */
+ public function product_meta_fields() {
+ global $post;
+ $pid = (int) $post->ID;
+
+ $enabled = 'yes' === get_post_meta( $pid, '_mnbtwp_enabled', true );
+ $provider = (int) get_post_meta( $pid, '_mnbtwp_provider_id', true );
+ $webdx = (int) get_post_meta( $pid, '_mnbtwp_webdx', true );
+ $sqldx = (int) get_post_meta( $pid, '_mnbtwp_sqldx', true );
+ $flow = (int) get_post_meta( $pid, '_mnbtwp_ll', true );
+ $ymbds = (int) get_post_meta( $pid, '_mnbtwp_ymbds', true );
+ $months = (int) get_post_meta( $pid, '_mnbtwp_period_months', true ) ?: 12;
+
+ global $wpdb;
+ $providers = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}mnbt_providers WHERE qk='true' ORDER BY id ASC", ARRAY_A );
+
+ echo '<div class="options_group show_if_simple">';
+ echo '<p class="form-field"><label style="font-weight:600;">' . esc_html__( 'MNBT 主机套餐', 'wp-seller-plugin' ) . '</label></p>';
+
+ woocommerce_wp_checkbox(
+ array(
+ 'id' => '_mnbtwp_enabled',
+ 'label' => __( '作为 MNBT 主机套餐', 'wp-seller-plugin' ),
+ 'cbvalue' => 'yes',
+ )
+ );
+
+ echo '<p class="form-field _mnbtwp_fields">';
+ echo '<label for="_mnbtwp_provider_id">' . esc_html__( '节点', 'wp-seller-plugin' ) . '</label>';
+ echo '<select id="_mnbtwp_provider_id" name="_mnbtwp_provider_id" class="select short">';
+ echo '<option value="0">' . esc_html__( '选择节点', 'wp-seller-plugin' ) . '</option>';
+ foreach ( $providers as $p ) {
+ $sel = ( $provider === (int) $p['id'] ) ? ' selected' : '';
+ echo '<option value="' . (int) $p['id'] . '"' . $sel . '>' . esc_html( $p['name'] ) . '</option>';
+ }
+ echo '</select></p>';
+
+ woocommerce_wp_text_input( array( 'id' => '_mnbtwp_webdx', 'label' => __( '网站空间 (MB)', 'wp-seller-plugin' ), 'type' => 'number', 'value' => $webdx ) );
+ woocommerce_wp_text_input( array( 'id' => '_mnbtwp_sqldx', 'label' => __( '数据库空间 (MB)', 'wp-seller-plugin' ), 'type' => 'number', 'value' => $sqldx ) );
+ woocommerce_wp_text_input( array( 'id' => '_mnbtwp_ll', 'label' => __( '流量 (MB,0=不限)', 'wp-seller-plugin' ), 'type' => 'number', 'value' => $flow ) );
+ woocommerce_wp_text_input( array( 'id' => '_mnbtwp_ymbds', 'label' => __( '域名绑定数', 'wp-seller-plugin' ), 'type' => 'number', 'value' => $ymbds ) );
+ woocommerce_wp_text_input( array( 'id' => '_mnbtwp_period_months', 'label' => __( '周期 (月)', 'wp-seller-plugin' ), 'type' => 'number', 'value' => $months ) );
+ echo '</div>';
+
+ // 当启用开关变化时显隐字段。
+ ?>
+ <script type="text/javascript">
+ jQuery(function ($) {
+ function mnbtwpToggle() {
+ if ($('#_mnbtwp_enabled').prop('checked')) {
+ $('._mnbtwp_fields').show();
+ } else {
+ $('._mnbtwp_fields').hide();
+ }
+ }
+ $('#_mnbtwp_enabled').on('change', mnbtwpToggle);
+ mnbtwpToggle();
+ });
+ </script>
+ <?php
+ }
+
+ /**
+ * 保存商品 meta。
+ *
+ * @param int $post_id 商品 ID。
+ */
+ public function save_product_meta( $post_id ) {
+ if ( ! empty( $_POST['_mnbtwp_enabled'] ) && 'yes' === $_POST['_mnbtwp_enabled'] ) {
+ update_post_meta( $post_id, '_mnbtwp_enabled', 'yes' );
+ } else {
+ delete_post_meta( $post_id, '_mnbtwp_enabled' );
+ }
+ update_post_meta( $post_id, '_mnbtwp_provider_id', max( 0, (int) ( $_POST['_mnbtwp_provider_id'] ?? 0 ) ) );
+ update_post_meta( $post_id, '_mnbtwp_webdx', max( 0, (int) ( $_POST['_mnbtwp_webdx'] ?? 0 ) ) );
+ update_post_meta( $post_id, '_mnbtwp_sqldx', max( 0, (int) ( $_POST['_mnbtwp_sqldx'] ?? 0 ) ) );
+ update_post_meta( $post_id, '_mnbtwp_ll', max( 0, (int) ( $_POST['_mnbtwp_ll'] ?? 0 ) ) );
+ update_post_meta( $post_id, '_mnbtwp_ymbds', max( 0, (int) ( $_POST['_mnbtwp_ymbds'] ?? 0 ) ) );
+ update_post_meta( $post_id, '_mnbtwp_period_months', max( 1, (int) ( $_POST['_mnbtwp_period_months'] ?? 12 ) ) );
+ }
+
+ /**
+ * 订单支付完成:续费订单走续费,否则走开通。
+ *
+ * @param int $order_id 订单 ID。
+ */
+ public function on_order_paid( $order_id ) {
+ $order = wc_get_order( $order_id );
+ if ( ! $order ) {
+ return;
+ }
+ try {
+ if ( get_post_meta( $order_id, '_mnbtwp_renew_host_id', true ) ) {
+ Billing::renew_order( $order_id );
+ } else {
+ Provision::provision_order( $order_id );
+ }
+ } catch ( \Exception $e ) {
+ // 记录失败(开通失败可在后台重试;续费失败待支付单手动处理)。
+ $order->add_order_note( 'MNBT 处理失败:' . $e->getMessage() );
+ }
+ }
+
+ /**
+ * 创建续费订单(含原商品 + 续费 meta),返回订单对象。
+ *
+ * @param int $host_id 主机 ID。
+ * @param int $months 续费月数。
+ * @return \WC_Order
+ *
+ * @throws \Exception 原商品/订单缺失时抛出。
+ */
+ public static function create_renew_order( $host_id, $months ) {
+ $host = Helpers::get_host( $host_id );
+ if ( ! $host ) {
+ throw new \Exception( __( '主机不存在', 'wp-seller-plugin' ) );
+ }
+ $months = max( 1, (int) $months );
+
+ $order = $host['order_id'] ? wc_get_order( (int) $host['order_id'] ) : null;
+ $product_id = 0;
+ if ( $order ) {
+ foreach ( $order->get_items() as $item ) {
+ $pid = (int) $item->get_product_id();
+ if ( 'yes' === get_post_meta( $pid, '_mnbtwp_enabled', true ) ) {
+ $product_id = $pid;
+ break;
+ }
+ }
+ }
+ if ( ! $product_id ) {
+ throw new \Exception( __( '找不到原主机商品', 'wp-seller-plugin' ) );
+ }
+ $product = wc_get_product( $product_id );
+ if ( ! $product ) {
+ throw new \Exception( __( '原商品不存在', 'wp-seller-plugin' ) );
+ }
+
+ $order = wc_create_order( array( 'customer_id' => (int) $host['user_id'] ) );
+ $order->add_product( $product, $months );
+ $order->add_meta_data( '_mnbtwp_renew_host_id', (int) $host_id );
+ $order->add_meta_data( '_mnbtwp_renew_months', $months );
+ $order->set_status( 'pending' );
+ $order->calculate_totals();
+ $order->save();
+
+ return $order;
+ }
+}
diff --git a/wp_seller_plugin/uninstall.php b/wp_seller_plugin/uninstall.php
new file mode 100644
index 0000000..702555b
--- /dev/null
+++ b/wp_seller_plugin/uninstall.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * 卸载脚本:默认保留数据(避免误删客户主机记录)。
+ * 如需彻底删除数据,取消下方注释。
+ *
+ * @package MnbtWp
+ */
+
+if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
+ exit;
+}
+
+$mnbtwp_keep = get_option( 'mnbtwp_settings', array() );
+$mnbtwp_keep = is_array( $mnbtwp_keep ) ? $mnbtwp_keep : array();
+
+if ( empty( $mnbtwp_keep['keep_data_on_del'] ) ) {
+ global $wpdb;
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}mnbt_providers" );
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}mnbt_hosts" );
+ $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}mnbt_api_log" );
+ delete_option( 'mnbtwp_settings' );
+}
diff --git a/wp_seller_plugin/wp-seller-plugin.php b/wp_seller_plugin/wp-seller-plugin.php
new file mode 100644
index 0000000..c9161b5
--- /dev/null
+++ b/wp_seller_plugin/wp-seller-plugin.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Plugin Name: MNBT 虚拟主机销售
+ * Plugin URI: https://github.com/MNBT/wp-seller-plugin
+ * Description: 在 WordPress 上售卖 MNBT(梦奈宝塔)虚拟主机:商品下单、自动开通、我的主机与到期管理,向 cPanel 式体验靠拢。
+ * Version: 0.1.0
+ * Requires at least: 6.0
+ * Requires PHP: 7.4
+ * Author: MNBT
+ * License: GPL-2.0-or-later
+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
+ * Text Domain: wp-seller-plugin
+ * Domain Path: /languages
+ *
+ * @package MnbtWp
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+define( 'MNBTWP_VERSION', '0.1.0' );
+define( 'MNBTWP_FILE', __FILE__ );
+define( 'MNBTWP_DIR', plugin_dir_path( __FILE__ ) );
+define( 'MNBTWP_URL', plugin_dir_url( __FILE__ ) );
+
+require_once MNBTWP_DIR . 'includes/class-plugin.php';
+
+register_activation_hook( __FILE__, array( '\MnbtWp\Plugin', 'activate' ) );
+register_deactivation_hook( __FILE__, array( '\MnbtWp\Plugin', 'deactivate' ) );
+
+/**
+ * 插件入口单例。
+ *
+ * @return \MnbtWp\Plugin
+ */
+function mnbtwp() {
+ return \MnbtWp\Plugin::instance();
+}
+
+mnbtwp();