用户功能设置收口与废弃后台链路清理
This commit is contained in:
@@ -17,8 +17,6 @@ class SchemaTools {
|
||||
'import-export' => SchemaToolsImportExport::build(),
|
||||
'health' => SchemaToolsHealth::build(),
|
||||
'backup' => SchemaToolsBackup::build(),
|
||||
'email-rollback' => SchemaToolsEmailRollback::build(),
|
||||
'frontend-modal-rollout' => SchemaToolsFrontendModalRollout::build(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaToolsEmailRollback {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'email-rollback',
|
||||
'title' => '邮件回滚',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'rollback', 'title' => '邮件回滚', 'fields' => [
|
||||
['key' => 'rollback_type', 'label' => '回滚类型', 'type' => 'select', 'options' => [
|
||||
['label' => '按时间范围', 'value' => 'date_range'],
|
||||
['label' => '按邮件类型', 'value' => 'email_type'],
|
||||
['label' => '全部回滚', 'value' => 'all'],
|
||||
]],
|
||||
['key' => 'rollback_date_from', 'label' => '开始日期', 'type' => 'date'],
|
||||
['key' => 'rollback_date_to', 'label' => '结束日期', 'type' => 'date'],
|
||||
['key' => 'rollback_execute', 'label' => '执行回滚', 'type' => 'button', 'danger' => true, 'help' => '此操作不可逆'],
|
||||
]],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Schema;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaToolsFrontendModalRollout {
|
||||
|
||||
public static function build(): array {
|
||||
return [
|
||||
'key' => 'frontend-modal-rollout',
|
||||
'title' => '前台弹窗灰度',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
[
|
||||
'key' => 'rollout',
|
||||
'title' => '弹窗新流程开关',
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'frontend_modal_rollout_identity',
|
||||
'label' => '官方认证新流程',
|
||||
'type' => 'switch',
|
||||
'default' => true,
|
||||
'help' => '启用统一弹窗运行时与认证快照流程。',
|
||||
],
|
||||
[
|
||||
'key' => 'frontend_modal_rollout_order',
|
||||
'label' => '订单新流程',
|
||||
'type' => 'switch',
|
||||
'default' => true,
|
||||
'help' => '启用订单详情快速开壳与状态同步流程。',
|
||||
],
|
||||
[
|
||||
'key' => 'frontend_modal_rollout_vip',
|
||||
'label' => '会员支付新流程',
|
||||
'type' => 'switch',
|
||||
'default' => true,
|
||||
'help' => '启用统一会员购买、支付和结果同步流程。',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -475,26 +475,6 @@ class SchemaUsers {
|
||||
]],
|
||||
],
|
||||
],
|
||||
'signin' => [
|
||||
'key' => 'signin',
|
||||
'title' => '签到奖励',
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'rules', 'title' => '签到规则', 'fields' => [
|
||||
['key' => 'signin_enabled', 'label' => '启用签到', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'signin_reward', 'label' => '每日签到奖励', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
|
||||
['key' => 'signin_consecutive_bonus', 'label' => '连续签到加成', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'signin_consecutive_days', 'label' => '加成周期', 'type' => 'number', 'default' => 7, 'min' => 2, 'max' => 30],
|
||||
['key' => 'signin_bonus_reward', 'label' => '周期奖励', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 100],
|
||||
]],
|
||||
['key' => 'display', 'title' => '签到展示', 'fields' => [
|
||||
['key' => 'signin_header_user_show', 'label' => '导航栏显示签到', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'signin_modal_auto_s', 'label' => '自动弹出签到', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'signin_modal_auto_expires', 'label' => '弹出间隔(天)', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 30],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'credits' => [
|
||||
'key' => 'credits',
|
||||
'title' => '积分设置',
|
||||
@@ -551,72 +531,23 @@ class SchemaUsers {
|
||||
'version' => '1.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'center', 'title' => '用户中心', 'fields' => [
|
||||
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,level,comments,favorites,vip,credits,revenue,auth', 'help' => '逗号分隔。level=我的等级;creator=创作中心;revenue=推广收益中心;auth=官方认证;等级、VIP、订单、创作中心、收益中心和认证会按功能开关自动注入'],
|
||||
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
|
||||
['key' => 'user_city_s', 'label' => '显示城市(IP 归属地)', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_city_type', 'label' => '位置精度', 'type' => 'radio', 'default' => 'province', 'options' => [
|
||||
['label' => '省份', 'value' => 'province'],
|
||||
['label' => '省份+城市', 'value' => 'province_city'],
|
||||
['label' => '国家+省份', 'value' => 'country_province'],
|
||||
]],
|
||||
['key' => 'user_join_day_my_name', 'label' => '站名', 'type' => 'input', 'default' => '本站', 'desc' => '用于「已加入 X 天」显示中的站点名称'],
|
||||
['key' => 'user_cover_img', 'label' => '默认封面', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'user_rewrite_slug', 'label' => '用户页slug', 'type' => 'input', 'default' => 'user'],
|
||||
['key' => 'user_follow_s', 'label' => '启用关注功能', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_online_s', 'label' => '显示在线状态', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_last_active_s', 'label' => '显示最后活跃', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'profile-media', 'title' => '用户资料', 'fields' => [
|
||||
['key' => 'avatar_upload_s', 'label' => '允许用户上传头像', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'avatar_external', 'label' => '外部头像源', 'type' => 'radio', 'default' => '', 'options' => [
|
||||
['label' => '关闭', 'value' => ''],
|
||||
['label' => 'Gravatar', 'value' => 'gravatar'],
|
||||
['label' => 'WeAvatar', 'value' => 'weavatar'],
|
||||
['label' => 'Cravatar', 'value' => 'cravatar'],
|
||||
['label' => '极客族 Gravatar 服务', 'value' => 'geekzu'],
|
||||
['label' => 'loli Gravatar 服务', 'value' => 'loli'],
|
||||
['label' => 'sep.cc Gravatar 服务', 'value' => 'sep'],
|
||||
['label' => 'v2ex Gravatar 服务', 'value' => 'v2ex'],
|
||||
], 'help' => '启用后,用户没有自定义头像时按邮箱获取外部头像。'],
|
||||
['key' => 'avatar_default_img', 'label' => '用户默认头像', 'type' => 'media', 'default' => get_theme_file_uri('assets/img/avatar-default.png'), 'help' => '建议尺寸 100×100。'],
|
||||
['key' => 'user_cover_upload_s', 'label' => '允许用户上传封面', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_cover_img', 'label' => '用户默认封面', 'type' => 'media', 'default' => get_theme_file_uri('assets/img/user_t.jpg'), 'help' => '建议尺寸 1000×400;用户未设置封面时显示此图。'],
|
||||
['key' => 'user_desc_std', 'label' => '用户默认签名', 'type' => 'input', 'default' => '这家伙很懒,什么都没有写...', 'help' => '用户未设置个人简介时显示的默认文案。'],
|
||||
]],
|
||||
['key' => 'profile', 'title' => '资料页字段', 'fields' => [
|
||||
['key' => 'user_profile_nickname', 'label' => '昵称', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_email', 'label' => '邮箱', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_url', 'label' => '个人主页', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_desc', 'label' => '个人简介', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_avatar', 'label' => '头像', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_cover', 'label' => '封面图', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_profile_qq', 'label' => 'QQ', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_wechat', 'label' => '微信', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_weibo', 'label' => '微博', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_github', 'label' => 'GitHub', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_bilibili', 'label' => 'B站', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_twitter', 'label' => 'Twitter', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_linkedin', 'label' => 'LinkedIn', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_facebook', 'label' => 'Facebook', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_telegram', 'label' => 'Telegram', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_douyin', 'label' => '抖音', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_profile_zhihu', 'label' => '知乎', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'card', 'title' => '用户卡片', 'fields' => [
|
||||
['key' => 'user_card_avatar', 'label' => '显示头像', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_name', 'label' => '显示用户名', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_desc', 'label' => '显示简介', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_count', 'label' => '显示统计', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_follow', 'label' => '显示关注按钮', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_card_position', 'label' => '卡片位置', 'type' => 'radio', 'default' => 'right', 'options' => [
|
||||
['label' => '右侧', 'value' => 'right'],
|
||||
['label' => '左侧', 'value' => 'left'],
|
||||
['label' => '顶部', 'value' => 'top'],
|
||||
]],
|
||||
['key' => 'user_card_style', 'label' => '卡片样式', 'type' => 'radio', 'default' => 'default', 'options' => [
|
||||
['label' => '默认', 'value' => 'default'],
|
||||
['label' => '简洁', 'value' => 'simple'],
|
||||
['label' => '详细', 'value' => 'detail'],
|
||||
]],
|
||||
['key' => 'user_card_bg', 'label' => '卡片背景', 'type' => 'media', 'default' => ''],
|
||||
['key' => 'user_card_avatar_size', 'label' => '头像大小', 'type' => 'number', 'default' => 80, 'min' => 40, 'max' => 200],
|
||||
]],
|
||||
['key' => 'privacy', 'title' => '隐私设置', 'fields' => [
|
||||
['key' => 'user_privacy_posts', 'label' => '隐藏文章', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_comments', 'label' => '隐藏评论', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_favorites', 'label' => '隐藏收藏', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_credits', 'label' => '隐藏积分', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_badge', 'label' => '隐藏徽章', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_online', 'label' => '隐藏在线状态', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_last_active', 'label' => '隐藏最后活跃', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_ip', 'label' => '隐藏IP归属地', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_location', 'label' => '隐藏位置', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
|
||||
],
|
||||
],
|
||||
'verify' => [
|
||||
|
||||
@@ -17,34 +17,6 @@ class ToolsController extends ControllerBase {
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// Email rollback: list users with recent email changes
|
||||
register_rest_route(self::API_NAMESPACE, '/email-rollback/users', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_email_rollback_list'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Email rollback: execute rollback
|
||||
register_rest_route(self::API_NAMESPACE, '/email-rollback/users/(?P<id>\d+)/rollback', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_email_rollback_execute'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Tools: system health (compact format for ExtensionTools.vue)
|
||||
register_rest_route(self::API_NAMESPACE, '/tools/health', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_tools_health'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Tools: maintenance actions
|
||||
register_rest_route(self::API_NAMESPACE, '/tools/maintenance', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'run_maintenance'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Tools: update center
|
||||
register_rest_route(self::API_NAMESPACE, '/tools/update-info', [
|
||||
'methods' => 'GET',
|
||||
@@ -65,85 +37,6 @@ class ToolsController extends ControllerBase {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /tools/health — compact health data for the tools page.
|
||||
*/
|
||||
public function get_tools_health(): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$upload_dir = wp_upload_dir();
|
||||
$upload_ok = !empty($upload_dir['error']) === false && is_dir($upload_dir['basedir']);
|
||||
|
||||
$db_ok = true;
|
||||
$db_status = '正常';
|
||||
$expected_tables = [
|
||||
$wpdb->prefix . 'aksrt_credit_log',
|
||||
$wpdb->prefix . 'aksrt_signin_log',
|
||||
$wpdb->prefix . 'aksrt_dl_token',
|
||||
$wpdb->prefix . 'aksrt_notification',
|
||||
];
|
||||
$missing = [];
|
||||
foreach ($expected_tables as $table) {
|
||||
if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) !== $table) {
|
||||
$missing[] = str_replace($wpdb->prefix, '', $table);
|
||||
}
|
||||
}
|
||||
if (!empty($missing)) {
|
||||
$db_ok = false;
|
||||
$db_status = '缺少: ' . implode(', ', $missing);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'php_version' => PHP_VERSION,
|
||||
'wp_version' => get_bloginfo('version'),
|
||||
'db_status' => $db_status,
|
||||
'db_ok' => $db_ok,
|
||||
'upload_dir' => $upload_dir['basedir'],
|
||||
'upload_ok' => $upload_ok,
|
||||
'theme_version' => wp_get_theme()->get('Version') ?? 'unknown',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /tools/maintenance — run a maintenance action.
|
||||
*/
|
||||
public function run_maintenance(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$action = sanitize_key((string) ($params['action'] ?? ''));
|
||||
|
||||
switch ($action) {
|
||||
case 'flush_cache':
|
||||
// Flush WordPress object cache
|
||||
wp_cache_flush();
|
||||
// Clear transients
|
||||
global $wpdb;
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_aksrt_%' OR option_name LIKE '_transient_timeout_aksrt_%'");
|
||||
return $this->success(['message' => __('缓存已清除', 'aksrt')]);
|
||||
|
||||
case 'optimize_db':
|
||||
global $wpdb;
|
||||
$tables = [
|
||||
$wpdb->prefix . 'aksrt_credit_log',
|
||||
$wpdb->prefix . 'aksrt_signin_log',
|
||||
$wpdb->prefix . 'aksrt_dl_token',
|
||||
$wpdb->prefix . 'aksrt_notification',
|
||||
];
|
||||
$optimized = 0;
|
||||
foreach ($tables as $table) {
|
||||
if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table) {
|
||||
$wpdb->query("OPTIMIZE TABLE {$table}");
|
||||
$optimized++;
|
||||
}
|
||||
}
|
||||
return $this->success(['message' => sprintf(__('已优化 %d 个数据表', 'aksrt'), $optimized)]);
|
||||
|
||||
default:
|
||||
return $this->error('未知的维护操作: ' . esc_html($action));
|
||||
}
|
||||
}
|
||||
|
||||
public function get_update_info(): \WP_REST_Response {
|
||||
$theme = wp_get_theme();
|
||||
$info = [
|
||||
@@ -199,32 +92,4 @@ class ToolsController extends ControllerBase {
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_email_rollback_list(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\EmailRollback')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20]]);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->get_param('page'));
|
||||
$per_page = min(100, max(1, (int) $request->get_param('per_page')));
|
||||
$data = \AKSRT\Auth\EmailRollback::list_pending($page, $per_page);
|
||||
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
|
||||
public function rest_email_rollback_execute(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\EmailRollback')) {
|
||||
return $this->error(__('邮箱回滚功能不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$user_id = (int) $request->get_param('id');
|
||||
$result = \AKSRT\Auth\EmailRollback::rollback($user_id, get_current_user_id());
|
||||
if (empty($result['success'])) {
|
||||
return $this->error((string) ($result['error'] ?? __('邮箱回滚失败', 'aksrt')), 422);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => (string) ($result['message'] ?? __('邮箱已回滚', 'aksrt')),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,10 @@ trait UserControllerProfileMediaOperations {
|
||||
return $this->error(__('请先登录', 'aksrt'), 401);
|
||||
}
|
||||
|
||||
$capability = $is_avatar ? 'upload_avatar' : 'upload_profile_background';
|
||||
if (!function_exists('aksrt_user_can') || !aksrt_user_can($capability, $user_id)) {
|
||||
$upload_enabled = $is_avatar
|
||||
? function_exists('aksrt_is_avatar_upload_enabled') && aksrt_is_avatar_upload_enabled()
|
||||
: function_exists('aksrt_is_user_cover_upload_enabled') && aksrt_is_user_cover_upload_enabled();
|
||||
if (!$upload_enabled) {
|
||||
return $this->error($is_avatar
|
||||
? __('当前账号没有上传头像的权限', 'aksrt')
|
||||
: __('当前账号没有上传用户资料卡背景图的权限', 'aksrt'), 403);
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Auth;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class EmailRollback {
|
||||
|
||||
const META_PREVIOUS = 'aksrt_email_previous';
|
||||
const META_CHANGED_AT = 'aksrt_email_changed_at';
|
||||
const WINDOW_SECONDS = 1209600; // 14 days
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_filter('user_row_actions', [$this, 'add_row_action'], 20, 2);
|
||||
add_action('admin_post_aksrt_email_rollback', [$this, 'handle_admin_rollback']);
|
||||
// cron 清理过期 meta
|
||||
add_action('aksrt_email_rollback_cleanup', [self::class, 'cron_cleanup']);
|
||||
add_action('wp', [self::class, 'maybe_schedule_cleanup']);
|
||||
}
|
||||
|
||||
public static function maybe_schedule_cleanup(): void {
|
||||
if (!wp_next_scheduled('aksrt_email_rollback_cleanup')) {
|
||||
wp_schedule_event(time() + DAY_IN_SECONDS, 'daily', 'aksrt_email_rollback_cleanup');
|
||||
}
|
||||
}
|
||||
|
||||
public static function cron_cleanup(): void {
|
||||
global $wpdb;
|
||||
$cutoff = time() - self::WINDOW_SECONDS;
|
||||
$rows = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND CAST(meta_value AS UNSIGNED) < %d",
|
||||
self::META_CHANGED_AT,
|
||||
$cutoff
|
||||
));
|
||||
if (empty($rows)) return;
|
||||
foreach ($rows as $uid) {
|
||||
delete_user_meta((int) $uid, self::META_PREVIOUS);
|
||||
delete_user_meta((int) $uid, self::META_CHANGED_AT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 14 天窗口内?
|
||||
*/
|
||||
public static function is_within_window(int $user_id): bool {
|
||||
$changed_at = (int) get_user_meta($user_id, self::META_CHANGED_AT, true);
|
||||
if ($changed_at <= 0) return false;
|
||||
return (time() - $changed_at) <= self::WINDOW_SECONDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出窗口内可回滚的用户。
|
||||
*/
|
||||
public static function list_pending(int $page = 1, int $per_page = 20): array {
|
||||
global $wpdb;
|
||||
$page = max(1, $page);
|
||||
$per_page = min(100, max(1, $per_page));
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$cutoff = time() - self::WINDOW_SECONDS;
|
||||
|
||||
$sql_count = $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT um1.user_id)
|
||||
FROM {$wpdb->usermeta} um1
|
||||
INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s
|
||||
WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d",
|
||||
self::META_PREVIOUS,
|
||||
self::META_CHANGED_AT,
|
||||
$cutoff
|
||||
);
|
||||
$total = (int) $wpdb->get_var($sql_count);
|
||||
|
||||
$sql = $wpdb->prepare(
|
||||
"SELECT um1.user_id, um1.meta_value AS changed_at, um2.meta_value AS previous_email
|
||||
FROM {$wpdb->usermeta} um1
|
||||
INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s
|
||||
WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d
|
||||
ORDER BY CAST(um1.meta_value AS UNSIGNED) DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
self::META_PREVIOUS,
|
||||
self::META_CHANGED_AT,
|
||||
$cutoff,
|
||||
$per_page,
|
||||
$offset
|
||||
);
|
||||
$rows = $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $r) {
|
||||
$uid = (int) $r['user_id'];
|
||||
$u = get_userdata($uid);
|
||||
if (!$u) continue;
|
||||
$items[] = [
|
||||
'user_id' => $uid,
|
||||
'login' => $u->user_login,
|
||||
'display_name' => $u->display_name,
|
||||
'current_email' => $u->user_email,
|
||||
'previous_email' => (string) $r['previous_email'],
|
||||
'changed_at' => (int) $r['changed_at'],
|
||||
'expires_in' => max(0, (int) $r['changed_at'] + self::WINDOW_SECONDS - time()),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行回滚。
|
||||
*
|
||||
* @return array{success:bool, message?:string, error?:string}
|
||||
*/
|
||||
public static function rollback(int $user_id, int $by_admin_id = 0): array {
|
||||
if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
|
||||
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
|
||||
}
|
||||
if (!self::is_within_window($user_id)) {
|
||||
return ['success' => false, 'error' => __('回滚窗口已过期或当前账号未发起过邮箱变更', 'aksrt')];
|
||||
}
|
||||
|
||||
$previous = (string) get_user_meta($user_id, self::META_PREVIOUS, true);
|
||||
if ($previous === '' || !is_email($previous)) {
|
||||
return ['success' => false, 'error' => __('旧邮箱无效', 'aksrt')];
|
||||
}
|
||||
|
||||
// 检查旧邮箱是否被其他用户占用(理论上极少见)
|
||||
$occupier = email_exists($previous);
|
||||
if ($occupier && (int) $occupier !== $user_id) {
|
||||
return ['success' => false, 'error' => __('旧邮箱已被其他账号占用,无法回滚', 'aksrt')];
|
||||
}
|
||||
|
||||
$update = wp_update_user([
|
||||
'ID' => $user_id,
|
||||
'user_email' => $previous,
|
||||
]);
|
||||
if (is_wp_error($update)) {
|
||||
return ['success' => false, 'error' => $update->get_error_message()];
|
||||
}
|
||||
|
||||
// 清空回滚 meta(一次性)
|
||||
delete_user_meta($user_id, self::META_PREVIOUS);
|
||||
delete_user_meta($user_id, self::META_CHANGED_AT);
|
||||
|
||||
// 记录管理员操作日志
|
||||
$log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true);
|
||||
if (!is_array($log)) $log = [];
|
||||
$log[] = [
|
||||
't' => time(),
|
||||
'action' => 'email_rollback',
|
||||
'by' => $by_admin_id ?: get_current_user_id(),
|
||||
];
|
||||
if (count($log) > 10) $log = array_slice($log, -10);
|
||||
update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => sprintf(__('已将用户邮箱回滚为 %s', 'aksrt'), $previous),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在用户列表行操作中加入"回滚邮箱"。
|
||||
*/
|
||||
public function add_row_action(array $actions, \WP_User $user): array {
|
||||
if (!current_user_can('edit_user', $user->ID)) return $actions;
|
||||
if (!self::is_within_window((int) $user->ID)) return $actions;
|
||||
|
||||
$url = wp_nonce_url(
|
||||
admin_url('admin-post.php?action=aksrt_email_rollback&user_id=' . (int) $user->ID),
|
||||
'aksrt_email_rollback_' . (int) $user->ID
|
||||
);
|
||||
$actions['aksrt_email_rollback'] = sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url($url),
|
||||
esc_html__('回滚邮箱', 'aksrt')
|
||||
);
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function handle_admin_rollback(): void {
|
||||
$user_id = isset($_GET['user_id']) ? absint($_GET['user_id']) : 0;
|
||||
if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
check_admin_referer('aksrt_email_rollback_' . $user_id);
|
||||
|
||||
$r = self::rollback($user_id, get_current_user_id());
|
||||
$type = !empty($r['success']) ? 'success' : 'error';
|
||||
$msg = $r['message'] ?? $r['error'] ?? '';
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [['type' => $type, 'msg' => $msg]], 30);
|
||||
|
||||
wp_safe_redirect(wp_get_referer() ?: admin_url('users.php'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -543,11 +543,6 @@ trait ScriptsPart1c {
|
||||
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
||||
// Alias used by modal-auth / follow / report (same value as restUrl).
|
||||
'apiUrl' => esc_url_raw(rest_url('aksrt/v1')),
|
||||
'frontendModalRollout' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_frontend_modal_rollout() : [
|
||||
'vip' => true,
|
||||
'order' => true,
|
||||
'identity' => true,
|
||||
],
|
||||
'stickyEnabled' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s']) : true,
|
||||
'stickyPages' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s'] : ['home', 'cat'],
|
||||
'badgeShow' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['list_badge_show']) : true,
|
||||
|
||||
@@ -49,8 +49,6 @@ class Capabilities {
|
||||
'comment_delete_my_post' => ['label' => __('删除自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
|
||||
'comment_delete_other' => ['label' => __('删除其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
|
||||
|
||||
'upload_avatar' => ['label' => __('上传头像', 'aksrt'), 'default' => ['logged' => true]],
|
||||
'upload_profile_background' => ['label' => __('上传用户资料卡背景图', 'aksrt'), 'default' => ['logged' => true]],
|
||||
'upload_submit_image' => ['label' => __('投稿图片上传', 'aksrt'), 'exclude_roles' => ['all']],
|
||||
'upload_comment_image' => ['label' => __('评论图片上传', 'aksrt'), 'exclude_roles' => ['all']],
|
||||
'upload_resource_attachment'=> ['label' => __('资源附件上传', 'aksrt'), 'exclude_roles' => ['all']],
|
||||
@@ -153,36 +151,4 @@ class Capabilities {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取"系统工具"设置
|
||||
*/
|
||||
public static function get_system_tools_settings(): array {
|
||||
$s = Settings::get_all();
|
||||
$time_format = sanitize_text_field((string) ($s['time_format'] ?? 'n月j日 H:i'));
|
||||
|
||||
return [
|
||||
'remove_emoji' => !empty($s['remove_emoji'] ?? true),
|
||||
'remove_open_sans' => !empty($s['remove_open_sans'] ?? true),
|
||||
'remove_more_wp_head' => !empty($s['remove_more_wp_head'] ?? true),
|
||||
'hide_admin_bar' => !empty($s['hide_admin_bar'] ?? true),
|
||||
'remove_dashboard_widgets' => !empty($s['remove_dashboard_widgets'] ?? true),
|
||||
'admin_frontend_set' => !empty($s['admin_frontend_set'] ?? true),
|
||||
'display_wp_update' => !empty($s['display_wp_update'] ?? true),
|
||||
'close_gutenberg' => !empty($s['close_gutenberg']),
|
||||
'admin_user_del_fields' => !empty($s['admin_user_del_fields'] ?? true),
|
||||
'split_upload_s' => !empty($s['split_upload_s'] ?? true),
|
||||
'split_minimum_size' => max(4, min(100, (int) ($s['split_minimum_size'] ?? 20))),
|
||||
'newfilename' => !empty($s['newfilename']),
|
||||
'newfilename_type' => in_array($s['newfilename_type'] ?? 'time', ['random', 'time'], true)
|
||||
? $s['newfilename_type'] : 'time',
|
||||
'disabled_large_thumbnail' => !empty($s['disabled_large_thumbnail'] ?? true),
|
||||
'disabled_autoembed' => !empty($s['disabled_autoembed'] ?? true),
|
||||
'mce_float_toolbar' => [
|
||||
'pc_s' => !empty($s['mce_float_toolbar_pc_s'] ?? true),
|
||||
'm_s' => !empty($s['mce_float_toolbar_m_s'] ?? true),
|
||||
],
|
||||
'time_ago_s' => !empty($s['time_ago_s'] ?? true),
|
||||
'time_format' => $time_format,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,11 +146,4 @@ trait DomainGroups {
|
||||
return Capabilities::user_can($cap, $user_id, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取"系统工具"设置
|
||||
*/
|
||||
public static function get_system_tools_settings(): array {
|
||||
return Capabilities::get_system_tools_settings();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Frontend modal runtime settings.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Settings;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class FrontendModalRuntime {
|
||||
|
||||
/**
|
||||
* Read the per-domain frontend modal rollout switches.
|
||||
*
|
||||
* @return array{vip:bool,order:bool,identity:bool}
|
||||
*/
|
||||
public static function get_frontend_modal_rollout(): array {
|
||||
$settings = Settings::get_all();
|
||||
|
||||
return [
|
||||
// Canonical-only flows must be live when the option has never been configured.
|
||||
// An explicit false remains an intentional per-domain kill switch.
|
||||
'vip' => !array_key_exists('frontend_modal_rollout_vip', $settings) || !empty($settings['frontend_modal_rollout_vip']),
|
||||
'order' => !array_key_exists('frontend_modal_rollout_order', $settings) || !empty($settings['frontend_modal_rollout_order']),
|
||||
'identity' => !array_key_exists('frontend_modal_rollout_identity', $settings) || !empty($settings['frontend_modal_rollout_identity']),
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
-11
@@ -513,17 +513,6 @@ class Settings {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取前台弹窗按领域灰度开关。
|
||||
*
|
||||
* @return array{vip:bool,order:bool,identity:bool}
|
||||
*/
|
||||
public static function get_frontend_modal_rollout(): array {
|
||||
return FrontendModalRuntime::get_frontend_modal_rollout();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取侧边栏配置
|
||||
*
|
||||
@@ -557,6 +546,17 @@ class Settings {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Canonical avatar, cover, and signature settings.
|
||||
*
|
||||
* @return array{avatar_upload_s:bool,avatar_external:string,avatar_default_img:string,user_cover_upload_s:bool,user_cover_img:string,user_desc_std:string}
|
||||
*/
|
||||
public static function get_user_feature_settings(): array {
|
||||
return UserRuntime::get_user_feature_settings();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Private user-center display settings.
|
||||
*
|
||||
|
||||
@@ -9,6 +9,31 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class UserRuntime {
|
||||
|
||||
/**
|
||||
* Canonical avatar, cover, and signature settings.
|
||||
*
|
||||
* @return array{avatar_upload_s:bool,avatar_external:string,avatar_default_img:string,user_cover_upload_s:bool,user_cover_img:string,user_desc_std:string}
|
||||
*/
|
||||
public static function get_user_feature_settings(): array {
|
||||
$settings = Settings::read_domain('users', 'features');
|
||||
$source = sanitize_key((string) ($settings['avatar_external'] ?? ''));
|
||||
if (!in_array($source, ['', 'gravatar', 'weavatar', 'cravatar', 'geekzu', 'loli', 'sep', 'v2ex'], true)) {
|
||||
$source = '';
|
||||
}
|
||||
|
||||
$avatar = esc_url_raw((string) ($settings['avatar_default_img'] ?? ''));
|
||||
$cover = esc_url_raw((string) ($settings['user_cover_img'] ?? ''));
|
||||
|
||||
return [
|
||||
'avatar_upload_s' => !array_key_exists('avatar_upload_s', $settings) || !empty($settings['avatar_upload_s']),
|
||||
'avatar_external' => $source,
|
||||
'avatar_default_img' => $avatar !== '' ? $avatar : get_theme_file_uri('assets/img/avatar-default.png'),
|
||||
'user_cover_upload_s' => !array_key_exists('user_cover_upload_s', $settings) || !empty($settings['user_cover_upload_s']),
|
||||
'user_cover_img' => $cover !== '' ? $cover : get_theme_file_uri('assets/img/user_t.jpg'),
|
||||
'user_desc_std' => sanitize_text_field((string) ($settings['user_desc_std'] ?? '这家伙很懒,什么都没有写...')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Public user-home display settings.
|
||||
*
|
||||
@@ -70,59 +95,11 @@ class UserRuntime {
|
||||
*
|
||||
* @return array{tabs:string[]}
|
||||
*/
|
||||
public static function get_user_center_settings(): array {
|
||||
$settings = Settings::get_all();
|
||||
$raw_tabs = $settings['user_center_tabs'] ?? 'profile,level,comments,credits,revenue,favorites,auth';
|
||||
$tabs = is_array($raw_tabs)
|
||||
? $raw_tabs
|
||||
: preg_split('/[\s,]+/', (string) $raw_tabs, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
// Built-in switchable panels. VIP is always auto-injected when enabled
|
||||
// by AKSRT\User\Center — keep it allowed for explicit config too.
|
||||
$allowed_tabs = ['profile', 'level', 'comments', 'favorites', 'credits', 'revenue', 'vip', 'orders', 'auth', 'settings'];
|
||||
$normalized_tabs = ['profile'];
|
||||
|
||||
foreach ((array) $tabs as $tab) {
|
||||
$tab = sanitize_key((string) $tab);
|
||||
if ($tab === 'profile') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($tab, $allowed_tabs, true) && !in_array($tab, $normalized_tabs, true)) {
|
||||
$normalized_tabs[] = $tab;
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array('comments', $normalized_tabs, true)) {
|
||||
$normalized_tabs[] = 'comments';
|
||||
}
|
||||
|
||||
// Ensure VIP appears in configured tabs when membership is on, even if
|
||||
// the stored option string is older and omits it.
|
||||
$vip_on = !empty(Settings::get_payment_vip_settings()['enabled']);
|
||||
if ($vip_on && !in_array('vip', $normalized_tabs, true)) {
|
||||
// Prefer after credits, else after favorites, else append before settings.
|
||||
$insert_at = count($normalized_tabs);
|
||||
foreach (['credits', 'favorites', 'posts'] as $anchor) {
|
||||
$idx = array_search($anchor, $normalized_tabs, true);
|
||||
if ($idx !== false) {
|
||||
$insert_at = (int) $idx + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
array_splice($normalized_tabs, $insert_at, 0, ['vip']);
|
||||
}
|
||||
if (!$vip_on) {
|
||||
$normalized_tabs = array_values(array_filter(
|
||||
$normalized_tabs,
|
||||
static fn(string $t): bool => $t !== 'vip'
|
||||
));
|
||||
}
|
||||
|
||||
return [
|
||||
'tabs' => $normalized_tabs,
|
||||
];
|
||||
}
|
||||
public static function get_user_center_settings(): array {
|
||||
return [
|
||||
'tabs' => ['profile', 'level', 'comments', 'credits', 'revenue', 'favorites', 'auth'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* User identity, level, medal, and report governance settings.
|
||||
|
||||
@@ -42,7 +42,8 @@ final class UserRenderer {
|
||||
$card_classes = 'aksrt-user-card is-guest';
|
||||
$cover = '';
|
||||
if (!empty($i['show_cover'])) {
|
||||
$cover = '<div class="aksrt-user-card__cover"><img' . aksrt_get_lazy_attr('lazy_cover', get_theme_file_uri('assets/img/person_background.png'), 'lazyload fit-cover', aksrt_get_lazy_thumb('lg')) . ' alt=""></div>';
|
||||
$cover_url = function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url(0) : get_theme_file_uri('assets/img/user_t.jpg');
|
||||
$cover = '<div class="aksrt-user-card__cover"><img' . aksrt_get_lazy_attr('lazy_cover', $cover_url, 'lazyload fit-cover', aksrt_get_lazy_thumb('lg')) . ' alt=""></div>';
|
||||
} else {
|
||||
$card_classes .= ' no-cover';
|
||||
}
|
||||
@@ -83,7 +84,8 @@ final class UserRenderer {
|
||||
private static function profile_card(\WP_User $user, array $i, bool $self_card): string {
|
||||
$user_id = (int) $user->ID;
|
||||
$home = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : get_author_posts_url($user_id);
|
||||
$cover = (string) get_user_meta($user_id, 'aksrt_background_image', true);
|
||||
$cover = function_exists('aksrt_get_user_cover_url') ? aksrt_get_user_cover_url($user_id) : '';
|
||||
$description = function_exists('aksrt_get_user_description') ? aksrt_get_user_description($user_id) : (string) $user->description;
|
||||
$followers = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id) : 0;
|
||||
$following = class_exists('AKSRT\\User\\Follow') ? \AKSRT\User\Follow::get_instance()->get_following_count($user_id) : 0;
|
||||
$html = '<article class="aksrt-user-card' . (!$self_card ? ' is-author' : '') . (empty($i['show_cover']) ? ' no-cover' : '') . '">';
|
||||
@@ -94,7 +96,7 @@ final class UserRenderer {
|
||||
if (function_exists('aksrt_render_user_identity_chips')) {
|
||||
$html .= '<div class="aksrt-user-card__identity">' . aksrt_render_user_identity_chips($user_id, 'widget') . '</div>';
|
||||
}
|
||||
$html .= '<p>' . esc_html(wp_trim_words((string) $user->description, 24)) . '</p><div class="aksrt-user-card__stats"><span><b>' . number_format_i18n(count_user_posts($user_id, 'post', true)) . '</b>' . esc_html__('文章', 'aksrt') . '</span><span><b>' . number_format_i18n($followers) . '</b>' . esc_html__('粉丝', 'aksrt') . '</span><span><b>' . number_format_i18n($following) . '</b>' . esc_html__('关注', 'aksrt') . '</span></div>';
|
||||
$html .= '<p>' . esc_html(wp_trim_words($description, 24)) . '</p><div class="aksrt-user-card__stats"><span><b>' . number_format_i18n(count_user_posts($user_id, 'post', true)) . '</b>' . esc_html__('文章', 'aksrt') . '</span><span><b>' . number_format_i18n($followers) . '</b>' . esc_html__('粉丝', 'aksrt') . '</span><span><b>' . number_format_i18n($following) . '</b>' . esc_html__('关注', 'aksrt') . '</span></div>';
|
||||
if (!empty($i['show_buttons'])) {
|
||||
$html .= '<div class="aksrt-user-card__actions">' . self::action((string) $i['primary_action'], $user, true) . self::action((string) $i['secondary_action'], $user, false) . '</div>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user