对照参考主题(Zibll)修正四项偏差,申诉链路从页面整体迁移至弹窗:
- feat(ban): 新增三个 1:1 复刻弹窗(REST /user-ban/{info,appeal,log}-modal):
查看禁封状态(多彩头 c-gray + 信息行 + 申诉按钮)、禁封申诉(edit-header
gavel/c-blue + 信息框 + 资料项 data[key] 输入 + 审核中态 + 禁止申诉态)、
禁封记录(多彩头 c-yellow + 计数 + 逐条卡片);user-ban.js 增加
data-aksrt-ban-info/appeal/log 委托与 admin-ajax 申诉提交
- feat(ban): 禁封/解封弹窗的「当前用户已有N次禁封记录」静态文案改为可点击
黄链(N>1),打开禁封记录弹窗;解封视图信息行改复用统一数据源
- refactor(ban): 治理展示态收敛至 Restriction::state_badges()(封号=c-red
违规已封号 / 小黑屋=c-yellow 小黑屋禁封中 / 禁言=c-gray 禁言中,颜色与
字段统一管理)+ get_ban_info()(状态/起止时间/原因/说明/申诉权限,
三弹窗与禁封页共用)
- feat(ban): 登录行为对齐参考主题——移除登录期拦截(enforce_login_ban)
与 complete_login 禁封闸门,封号用户可正常登录;新增 template_redirect
前台禁封页(1:1 zib_user_ban_restrict_login/zib_die_page:红盒提示 +
信息行 + 申诉权限 + 退出登录/申诉按钮,模板 templates/parts/user/ban-block.php)
- feat(profile): 公开主页禁封状态从资料卡下方提示框改为占据身份行元信息位
的 badg 徽章(参考主题 author_header_identity 语义);用户中心元信息改为
but 按钮(徽章+箭头)打开查看禁封状态弹窗;评论拒绝态链接同源
- refactor: 移除申诉页面模板(page-ban-appeal.php)及其资产/注册/入队、
get_profile_notice/profile_notice 链、state_labels/state_notices/
login_ban_message、UserRuntime 的 public_notice/ban_notice_text 死配置、
report_settings 死键、无消费方的 aksrt_ban_appeal_status 端点
- feat(ban): BanAppeal::ajax_submit 兼容弹窗 data[key] 表单结构
(逐项清洗入库,申诉说明取 data['申诉说明'],旧平铺字段保持兼容)
- chore: 重新构建 public.css/public.min.css(三个新弹窗几何与头部样式)
- test: 契约测试重写(徽章元信息位、三弹窗与记录链接、禁封页、允许登录 +
前台拦截、死代码移除断言),全量 362 项通过
410 lines
17 KiB
PHP
410 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Settings;
|
|
|
|
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.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_user_home_settings(): array {
|
|
$settings = Settings::read_domain('page_display', 'user-home');
|
|
$tab_options = is_array($settings['author_main_tab_opt'] ?? null)
|
|
? $settings['author_main_tab_opt']
|
|
: [
|
|
'post' => ['pc_s' => true, 'm_s' => true],
|
|
'favorite' => ['pc_s' => true, 'm_s' => true],
|
|
'comment' => ['pc_s' => true, 'm_s' => true],
|
|
'follow' => ['pc_s' => true, 'm_s' => true],
|
|
];
|
|
$map = ['post' => 'posts', 'favorite' => 'favorites', 'comment' => 'comments', 'follow' => 'follow'];
|
|
$ordered = [];
|
|
$pc_hide = [];
|
|
$mobile_hide = [];
|
|
foreach ($tab_options as $key => $option) {
|
|
$key = sanitize_key((string) $key);
|
|
if (!isset($map[$key]) || !is_array($option)) {
|
|
continue;
|
|
}
|
|
$tab = $map[$key];
|
|
$ordered[] = $tab;
|
|
if (empty($option['pc_s'])) {
|
|
$pc_hide[] = $tab;
|
|
}
|
|
if (empty($option['m_s'])) {
|
|
$mobile_hide[] = $tab;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'route_identifier' => 'nicename',
|
|
'tabs' => $ordered,
|
|
'tabs_order' => $ordered,
|
|
'tabs_pc_hide' => $pc_hide,
|
|
'tabs_mobile_hide' => $mobile_hide,
|
|
'sidebar' => false,
|
|
'cover' => true,
|
|
'follow' => true,
|
|
'posts_per_page' => max(1, (int) get_option('posts_per_page', 10)),
|
|
'avatar' => true,
|
|
'name' => true,
|
|
'desc' => true,
|
|
'stats' => false,
|
|
'social' => true,
|
|
'tab_swiper' => !empty($settings['author_main_tab_swiper']),
|
|
'message' => true,
|
|
'private' => true,
|
|
'paging_ajax' => true,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Private user-center display settings.
|
|
*
|
|
* @return array{tabs:string[]}
|
|
*/
|
|
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.
|
|
*
|
|
* @return array{
|
|
* verify:array{enabled:bool,public_display:bool,allow_apply:bool,disable_desc:string,desc:string,types:array<string,array{slug:string,label:string,badge_text:string,fee:string,free_applications:int,require_real_name:bool}>},
|
|
* level:array{enabled:bool,public_display:bool,source:string,label_prefix:string,thresholds:array<int,int>},
|
|
* medal:array{enabled:bool,public_display:bool,limit:int,categories:array<int,array{cat_name:string,items:array<int,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int}>}>,items_by_slug:array<string,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int,cat_name:string}>},
|
|
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,desc:string,icon_url?:string}>},
|
|
* report:array{enabled:bool,public_entry:bool,categories:array<int,array{slug:string,label:string}>}
|
|
* }
|
|
*/
|
|
public static function get_user_identity_settings(): array {
|
|
$settings = Settings::get_all();
|
|
$verify_types = [
|
|
'personal' => [
|
|
'slug' => 'personal',
|
|
'label' => __('个人认证', 'aksrt'),
|
|
'badge_text' => trim((string) ($settings['user_verify_personal_badge_text'] ?? '')) ?: __('个人认证', 'aksrt'),
|
|
'fee' => self::identity_fee_setting($settings, 'user_verify_personal_fee'),
|
|
'free_applications' => max(0, (int) ($settings['user_verify_personal_free_applications'] ?? 0)),
|
|
'require_real_name' => true,
|
|
],
|
|
'enterprise' => [
|
|
'slug' => 'enterprise',
|
|
'label' => __('企业认证', 'aksrt'),
|
|
'badge_text' => trim((string) ($settings['user_verify_enterprise_badge_text'] ?? '')) ?: __('企业认证', 'aksrt'),
|
|
'fee' => self::identity_fee_setting($settings, 'user_verify_enterprise_fee'),
|
|
'free_applications' => max(0, (int) ($settings['user_verify_enterprise_free_applications'] ?? 0)),
|
|
'require_real_name' => true,
|
|
],
|
|
];
|
|
$medal = self::parse_medal_catalog($settings);
|
|
$provider = sanitize_key((string) ($settings['user_verify_provider'] ?? 'aliyun'));
|
|
if (!in_array($provider, ['aliyun', 'tencent'], true)) {
|
|
$provider = 'aliyun';
|
|
}
|
|
|
|
return [
|
|
'verify' => [
|
|
'enabled' => !isset($settings['user_verify_enabled']) || !empty($settings['user_verify_enabled']),
|
|
'public_display' => !isset($settings['user_verify_public_display']) || !empty($settings['user_verify_public_display']),
|
|
'allow_apply' => !isset($settings['user_verify_allow_apply']) || !empty($settings['user_verify_allow_apply']),
|
|
'disable_desc' => (string) ($settings['user_verify_disable_desc'] ?? ''),
|
|
'desc' => (string) ($settings['user_verify_desc'] ?? ''),
|
|
'provider' => $provider,
|
|
'allow_manual_fallback' => !isset($settings['user_verify_allow_manual_fallback']) || !empty($settings['user_verify_allow_manual_fallback']),
|
|
'poll_interval_seconds' => max(3, min(60, (int) ($settings['user_verify_poll_interval_seconds'] ?? 5))),
|
|
'types' => $verify_types,
|
|
],
|
|
'level' => [
|
|
'enabled' => !isset($settings['user_level_s']) || !empty($settings['user_level_s']),
|
|
'public_display' => !isset($settings['user_level_public_display']) || !empty($settings['user_level_public_display']),
|
|
'max' => max(3, min(30, (int) ($settings['user_level_max'] ?? 10))),
|
|
],
|
|
'medal' => $medal,
|
|
// Alias for existing call sites that still read badge.*; dictionary is derived from medal catalog.
|
|
'badge' => [
|
|
'enabled' => $medal['enabled'],
|
|
'public_display' => $medal['public_display'],
|
|
'limit' => $medal['limit'],
|
|
'dictionary' => array_values(array_map(static function (array $item): array {
|
|
return [
|
|
'slug' => $item['slug'],
|
|
'label' => $item['name'],
|
|
'tone' => 'info',
|
|
'desc' => (string) ($item['desc'] ?? ''),
|
|
'icon_url' => $item['icon'],
|
|
];
|
|
}, $medal['items_by_slug'])),
|
|
],
|
|
'report' => [
|
|
// 默认开启:对齐后台 schema default true 与参考主题 _pz('user_report_s', true)。
|
|
'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? true),
|
|
'public_entry' => !isset($settings['user_report_public_entry']) || !empty($settings['user_report_public_entry']),
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @param array<string,mixed> $settings */
|
|
private static function identity_fee_setting(array $settings, string $key): string {
|
|
$value = array_key_exists($key, $settings) ? $settings[$key] : '0.00';
|
|
if (!is_string($value)) {
|
|
return '';
|
|
}
|
|
try {
|
|
return \AKSRT\Ledger\AssetAmount::balance(trim($value))->decimal();
|
|
} catch (\InvalidArgumentException $e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 禁封/举报分区的运行时读取。
|
|
*
|
|
* get_all() 是裸 option:用户从未保存过“禁封/举报”分区时,
|
|
* ban_preset_reason / user_report_img_s / user_report_desc 等键不存在,
|
|
* schema 默认值只活在后台 UI 里。参考主题 _pz($key, $default) 的语义是
|
|
* 缺键即用默认值,因此这里改走 SchemaProvider 的带默认读取。
|
|
*/
|
|
private static function read_report_section(): array {
|
|
$settings = class_exists('\AKSRT\Admin\SchemaProvider')
|
|
? Settings::read_domain('users', 'report')
|
|
: [];
|
|
return is_array($settings) && $settings ? $settings : Settings::get_all();
|
|
}
|
|
|
|
/**
|
|
* 禁封设置
|
|
*/
|
|
public static function ban_settings(): array {
|
|
$settings = self::read_report_section();
|
|
|
|
// 预置原因:group 数组 [{t:...}](对齐参考主题)或旧版每行一条文本。
|
|
// slug 不能用 sanitize_key(标签) 派生:中文标签会被清洗成空串,
|
|
// 导致前端按 slug 非空过滤后整列表被丢弃。改用标签内容的稳定短哈希
|
|
// (跨请求一致、可通过服务端 sanitize_key、ASCII 标签同样稳定)。
|
|
$reasons_raw = $settings['ban_preset_reason'] ?? null;
|
|
$reasons = [];
|
|
$reason_slug = static function (string $text): string {
|
|
$key = sanitize_key($text);
|
|
return $key !== '' ? $key : substr(md5($text), 0, 12);
|
|
};
|
|
if (is_array($reasons_raw)) {
|
|
foreach ($reasons_raw as $row) {
|
|
$text = trim((string) ($row['t'] ?? ''));
|
|
if ($text !== '') {
|
|
$reasons[] = ['slug' => $reason_slug($text), 'name' => sanitize_text_field($text)];
|
|
}
|
|
}
|
|
} else {
|
|
foreach (preg_split('/\r\n|\r|\n/', (string) $reasons_raw) as $line) {
|
|
$line = trim((string) $line);
|
|
if ($line === '') {
|
|
continue;
|
|
}
|
|
$parts = explode('|', $line, 2);
|
|
$reasons[] = [
|
|
'slug' => $reason_slug((string) ($parts[1] ?? $parts[0])),
|
|
'name' => sanitize_text_field($parts[1] ?? $parts[0]),
|
|
];
|
|
}
|
|
}
|
|
|
|
// 申诉资料:group 数组 [{t:...}] 或旧版逗号分隔文本
|
|
$keys_raw = $settings['ban_appeal_keys'] ?? null;
|
|
$appeal_keys = [];
|
|
if (is_array($keys_raw)) {
|
|
foreach ($keys_raw as $row) {
|
|
$text = trim((string) ($row['t'] ?? ''));
|
|
if ($text !== '') {
|
|
$appeal_keys[] = sanitize_text_field($text);
|
|
}
|
|
}
|
|
} else {
|
|
$appeal_keys = array_filter(array_map('trim', explode(',', (string) $keys_raw)));
|
|
}
|
|
|
|
return [
|
|
'reasons' => $reasons,
|
|
'appeal_desc' => wp_kses_post((string) ($settings['ban_appeal_desc'] ?? '')),
|
|
'appeal_keys' => array_values($appeal_keys),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 举报设置
|
|
*/
|
|
public static function report_settings(): array {
|
|
$settings = self::read_report_section();
|
|
return [
|
|
// 默认开启:对齐后台 schema default true 与参考主题 _pz('user_report_s', true)。
|
|
'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? true),
|
|
'public_entry' => !isset($settings['user_report_public_entry']) || !empty($settings['user_report_public_entry']),
|
|
'img_enabled' => !empty($settings['user_report_img_s']),
|
|
'desc' => wp_kses_post((string) ($settings['user_report_desc'] ?? '')),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Parse structured user_medal_args catalog (rewrite; no badge dictionary dual-read).
|
|
*
|
|
* @param array<string,mixed> $settings
|
|
* @return array{enabled:bool,public_display:bool,limit:int,categories:array,items_by_slug:array}
|
|
*/
|
|
public static function parse_medal_catalog(array $settings): array {
|
|
$enabled = !empty($settings['user_medal_s']);
|
|
$public = !isset($settings['user_medal_public_display']) || !empty($settings['user_medal_public_display']);
|
|
$limit = max(1, min(8, (int) ($settings['user_medal_limit'] ?? 3)));
|
|
|
|
$allowed_types = [
|
|
'registration_time',
|
|
'checkin_all_day',
|
|
'new_post',
|
|
'comment',
|
|
'followed',
|
|
'like',
|
|
'comment_like',
|
|
'views',
|
|
'creator_income',
|
|
'affiliate_commission',
|
|
'manually_add',
|
|
];
|
|
|
|
$raw = $settings['user_medal_args'] ?? null;
|
|
if (is_string($raw) && $raw !== '') {
|
|
$decoded = json_decode($raw, true);
|
|
$raw = is_array($decoded) ? $decoded : [];
|
|
}
|
|
if (!is_array($raw)) {
|
|
$raw = [];
|
|
}
|
|
|
|
$categories = [];
|
|
$items_by_slug = [];
|
|
|
|
foreach ($raw as $cat) {
|
|
if (!is_array($cat)) {
|
|
continue;
|
|
}
|
|
$cat_name = sanitize_text_field((string) ($cat['cat_name'] ?? $cat['name'] ?? ''));
|
|
if ($cat_name === '') {
|
|
$cat_name = __('未命名', 'aksrt');
|
|
}
|
|
$items_out = [];
|
|
$items = is_array($cat['items'] ?? null) ? $cat['items'] : [];
|
|
foreach ($items as $item) {
|
|
if (!is_array($item)) {
|
|
continue;
|
|
}
|
|
$slug = sanitize_key((string) ($item['slug'] ?? ''));
|
|
$name = sanitize_text_field((string) ($item['name'] ?? ''));
|
|
if ($slug === '' || $name === '') {
|
|
continue;
|
|
}
|
|
$get_type = sanitize_key((string) ($item['get_type'] ?? 'manually_add'));
|
|
if (!in_array($get_type, $allowed_types, true)) {
|
|
$get_type = 'manually_add';
|
|
}
|
|
$icon = esc_url_raw((string) ($item['icon'] ?? $item['icon_url'] ?? ''));
|
|
$row = [
|
|
'slug' => $slug,
|
|
'name' => $name,
|
|
'desc' => sanitize_text_field((string) ($item['desc'] ?? '')),
|
|
'icon' => $icon,
|
|
'get_type' => $get_type,
|
|
'get_val' => max(0, (int) ($item['get_val'] ?? 0)),
|
|
'cat_name' => $cat_name,
|
|
];
|
|
$items_out[] = $row;
|
|
$items_by_slug[$slug] = $row;
|
|
}
|
|
$categories[] = [
|
|
'cat_name' => $cat_name,
|
|
'items' => $items_out,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'enabled' => $enabled,
|
|
'public_display' => $public,
|
|
'limit' => $limit,
|
|
'categories' => $categories,
|
|
'items_by_slug' => $items_by_slug,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<int,int>
|
|
*/
|
|
public static function parse_level_thresholds(string $raw): array {
|
|
$lines = preg_split('/[\r\n]+/', $raw, -1, PREG_SPLIT_NO_EMPTY);
|
|
$thresholds = [];
|
|
|
|
foreach ((array) $lines as $line) {
|
|
$parts = preg_split('/[|:,]/', trim($line), 2);
|
|
if (!is_array($parts) || count($parts) < 2) {
|
|
continue;
|
|
}
|
|
|
|
$level = (int) trim((string) $parts[0]);
|
|
$value = (int) trim((string) $parts[1]);
|
|
if ($level <= 0 || $value < 0) {
|
|
continue;
|
|
}
|
|
|
|
$thresholds[$level] = $value;
|
|
}
|
|
|
|
if (empty($thresholds)) {
|
|
$thresholds = [1 => 0, 2 => 30, 3 => 100, 4 => 300];
|
|
}
|
|
|
|
ksort($thresholds, SORT_NUMERIC);
|
|
return $thresholds;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Removed — medal catalog is structured user_medal_args. Method kept empty stub intentionally deleted.
|
|
* @return array<int,array{slug:string,label:string,tone:string,icon_url:string}>
|
|
*/
|
|
public static function parse_badge_dictionary(string $raw): array {
|
|
// Rewrite policy: no dual-read of pipe dictionary. Callers must use parse_medal_catalog / get_user_identity_settings()['medal'].
|
|
unset($raw);
|
|
return [];
|
|
}
|
|
}
|