用户功能设置收口与废弃后台链路清理

This commit is contained in:
2026-08-23 22:09:12 +08:00
parent 081eb0870d
commit 86692ff3f9
59 changed files with 506 additions and 1767 deletions
-2
View File
@@ -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' => '启用统一会员购买、支付和结果同步流程。',
],
],
],
],
];
}
}
+16 -85
View File
@@ -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' => [
-135
View File
@@ -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);
-210
View File
@@ -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;
}
}
-5
View File
@@ -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,
-34
View File
@@ -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,
];
}
}
-7
View File
@@ -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();
}
}
-30
View File
@@ -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
View File
@@ -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.
*
+30 -53
View File
@@ -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.
+5 -3
View File
@@ -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>';
}