feat(users): rewrite medal catalog + verify apply SPA contract
Replace user_badge_dictionary textarea with structured user_medal_* catalog (categories/items/get_type). Expand verify SPA/PHP to Zibll apply fields. Reader derives badge.dictionary from medal catalog for existing AKSRT_User_Badges wear/grant runtime. No dual-read of old pipe dictionary. audit-admin-contracts: 0 errors.
This commit is contained in:
@@ -580,7 +580,7 @@ class AKSRT_Admin_Schema_Users {
|
||||
'verify' => [
|
||||
'key' => 'verify',
|
||||
'title' => '身份认证',
|
||||
'version' => '1.0.0',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '公开展示', 'fields' => [
|
||||
@@ -591,12 +591,13 @@ class AKSRT_Admin_Schema_Users {
|
||||
['key' => 'workflow', 'title' => '审核流程', 'fields' => [
|
||||
['key' => 'user_verify_accept_email_verified', 'label' => '邮箱验证通过视为已认证', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_require_manual_review', 'label' => '保留人工审核位', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_verify_apply_option', 'label' => '申请选项', 'type' => 'textarea', 'default' => "real_name|实名认证\norganization|企业认证"],
|
||||
['key' => 'user_verify_apply_option', 'label' => '申请类型选项', 'type' => 'textarea', 'default' => "real_name|实名认证\norganization|企业认证", 'desc' => '每行:slug|显示名称'],
|
||||
]],
|
||||
['key' => 'restrict', 'title' => '限制设置', 'fields' => [
|
||||
['key' => 'user_verify_disable_desc', 'label' => '未认证限制说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'user_verify_limit_level', 'label' => '限制等级', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'user_verify_desc', 'label' => '认证说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'apply', 'title' => '申请设置', 'fields' => [
|
||||
['key' => 'user_verify_allow_apply', 'label' => '允许用户前台申请', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_disable_desc', 'label' => '停用申请说明', 'type' => 'textarea', 'default' => "由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员"],
|
||||
['key' => 'user_verify_limit_level', 'label' => '申请要求等级', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 99, 'desc' => '0 表示不限制;依赖用户等级'],
|
||||
['key' => 'user_verify_desc', 'label' => '申请介绍', 'type' => 'textarea', 'default' => '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
@@ -639,21 +640,56 @@ class AKSRT_Admin_Schema_Users {
|
||||
'badge' => [
|
||||
'key' => 'badge',
|
||||
'title' => '用户徽章',
|
||||
'version' => '1.0.0',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '徽章展示', 'fields' => [
|
||||
['key' => 'user_badge_enabled', 'label' => '启用徽章系统', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_badge_public_display', 'label' => '公开页显示徽章', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_badge_limit', 'label' => '最多显示数量', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 8],
|
||||
['key' => 'user_medal_s', 'label' => '启用徽章/勋章系统', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_medal_public_display', 'label' => '公开页显示徽章', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_medal_limit', 'label' => '最多显示数量', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 8],
|
||||
]],
|
||||
['key' => 'dictionary', 'title' => '徽章字典', 'fields' => [
|
||||
['key' => 'catalog', 'title' => '徽章明细', 'fields' => [
|
||||
[
|
||||
'key' => 'user_badge_dictionary',
|
||||
'label' => '徽章列表',
|
||||
'type' => 'textarea',
|
||||
'default' => "founder|创始成员|warning\nstaff|站务团队|info",
|
||||
'help' => '每行:slug|显示文案|色调(neutral/info/success/warning/brand)|图标URL(可选)。配置 icon_url 后前台显示勋章图;用户 meta aksrt_wear_badge 指定当前佩戴。',
|
||||
'key' => 'user_medal_args',
|
||||
'label' => '分类与条目',
|
||||
'type' => 'json',
|
||||
'default' => [
|
||||
[
|
||||
'cat_name' => '成长',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'rookie',
|
||||
'name' => '初出茅庐',
|
||||
'desc' => '注册时间超过30天',
|
||||
'icon' => '',
|
||||
'get_type' => 'registration_time',
|
||||
'get_val' => 30,
|
||||
],
|
||||
[
|
||||
'slug' => 'checkin-30',
|
||||
'name' => '签到达人',
|
||||
'desc' => '累计签到30天',
|
||||
'icon' => '',
|
||||
'get_type' => 'checkin_all_day',
|
||||
'get_val' => 30,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'cat_name' => '限定',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'annual-star',
|
||||
'name' => '年度人气达人',
|
||||
'desc' => '活动限定徽章',
|
||||
'icon' => '',
|
||||
'get_type' => 'manually_add',
|
||||
'get_val' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'help' => '结构化目录:分类 → 条目(slug/name/desc/icon/get_type/get_val)。由 SPA UsersBadge 编辑。',
|
||||
],
|
||||
]],
|
||||
],
|
||||
|
||||
@@ -115,11 +115,12 @@ class AKSRT_Settings_User_Runtime {
|
||||
}
|
||||
|
||||
/**
|
||||
* User identity, level, badge, and report governance settings.
|
||||
* User identity, level, medal, and report governance settings.
|
||||
*
|
||||
* @return array{
|
||||
* verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,require_manual_review:bool},
|
||||
* verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,require_manual_review:bool,allow_apply:bool,disable_desc:string,limit_level:int,desc:string,apply_option:string},
|
||||
* 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,icon_url?:string}>},
|
||||
* report:array{enabled:bool,public_entry:bool,public_notice:bool,ban_notice_text:string,categories:array<int,array{slug:string,label:string}>}
|
||||
* }
|
||||
@@ -129,6 +130,7 @@ class AKSRT_Settings_User_Runtime {
|
||||
$verify_badge_text = trim((string) ($settings['user_verify_badge_text'] ?? ''));
|
||||
$level_prefix = trim((string) ($settings['user_level_prefix'] ?? ''));
|
||||
$ban_notice_text = trim((string) ($settings['user_report_ban_notice_text'] ?? ''));
|
||||
$medal = self::parse_medal_catalog($settings);
|
||||
|
||||
return [
|
||||
'verify' => [
|
||||
@@ -137,22 +139,36 @@ class AKSRT_Settings_User_Runtime {
|
||||
'badge_text' => $verify_badge_text !== '' ? $verify_badge_text : __('已认证', 'aksrt'),
|
||||
'accept_email_verified' => !isset($settings['user_verify_accept_email_verified']) || !empty($settings['user_verify_accept_email_verified']),
|
||||
'require_manual_review' => !empty($settings['user_verify_require_manual_review']),
|
||||
'allow_apply' => !isset($settings['user_verify_allow_apply']) || !empty($settings['user_verify_allow_apply']),
|
||||
'disable_desc' => (string) ($settings['user_verify_disable_desc'] ?? ''),
|
||||
'limit_level' => max(0, (int) ($settings['user_verify_limit_level'] ?? 0)),
|
||||
'desc' => (string) ($settings['user_verify_desc'] ?? ''),
|
||||
'apply_option' => (string) ($settings['user_verify_apply_option'] ?? ''),
|
||||
],
|
||||
'level' => [
|
||||
'enabled' => !empty($settings['user_level_enabled']),
|
||||
'enabled' => !empty($settings['user_level_enabled']) || !empty($settings['user_level_s']),
|
||||
'public_display' => !isset($settings['user_level_public_display']) || !empty($settings['user_level_public_display']),
|
||||
'source' => (function() use ($settings) {
|
||||
'source' => (static function () use ($settings) {
|
||||
$v = $settings['user_level_source'] ?? 'credits_balance';
|
||||
return in_array($v, ['credits_balance', 'manual_meta'], true) ? $v : 'credits_balance';
|
||||
return in_array($v, ['credits_balance', 'manual_meta', 'integral'], true) ? $v : 'credits_balance';
|
||||
})(),
|
||||
'label_prefix' => $level_prefix !== '' ? $level_prefix : 'Lv.',
|
||||
'thresholds' => self::parse_level_thresholds((string) ($settings['user_level_thresholds'] ?? "1|0\n2|30\n3|100\n4|300")),
|
||||
],
|
||||
'medal' => $medal,
|
||||
// Alias for existing call sites that still read badge.*; dictionary is derived from medal catalog.
|
||||
'badge' => [
|
||||
'enabled' => !empty($settings['user_badge_enabled']),
|
||||
'public_display' => !isset($settings['user_badge_public_display']) || !empty($settings['user_badge_public_display']),
|
||||
'limit' => max(1, min(8, (int) ($settings['user_badge_limit'] ?? 3))),
|
||||
'dictionary' => self::parse_badge_dictionary((string) ($settings['user_badge_dictionary'] ?? "founder|创始成员|warning\nstaff|站务团队|info")),
|
||||
'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',
|
||||
'icon_url' => $item['icon'],
|
||||
];
|
||||
}, $medal['items_by_slug'])),
|
||||
],
|
||||
'report' => [
|
||||
'enabled' => !empty($settings['user_report_enabled']),
|
||||
@@ -207,6 +223,100 @@ class AKSRT_Settings_User_Runtime {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
'bbs_post',
|
||||
'comment',
|
||||
'followed',
|
||||
'like',
|
||||
'comment_like',
|
||||
'bbs_adopt',
|
||||
'bbs_comment_hot',
|
||||
'bbs_post_hot',
|
||||
'bbs_essence',
|
||||
'bbs_score',
|
||||
'views',
|
||||
'pay_income',
|
||||
'pay_rebate',
|
||||
'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>
|
||||
*/
|
||||
@@ -238,41 +348,13 @@ class AKSRT_Settings_User_Runtime {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
$lines = preg_split('/[\r\n]+/', $raw, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$allowed_tones = ['neutral', 'info', 'success', 'warning', 'brand'];
|
||||
$items = [];
|
||||
|
||||
foreach ((array) $lines as $line) {
|
||||
$parts = array_map('trim', explode('|', (string) $line));
|
||||
if (count($parts) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$slug = sanitize_key((string) $parts[0]);
|
||||
$label = sanitize_text_field((string) $parts[1]);
|
||||
$tone = isset($parts[2]) ? sanitize_key((string) $parts[2]) : 'neutral';
|
||||
$icon_url = isset($parts[3]) ? esc_url_raw((string) $parts[3]) : '';
|
||||
|
||||
if ($slug === '' || $label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_array($tone, $allowed_tones, true)) {
|
||||
$tone = 'neutral';
|
||||
}
|
||||
|
||||
$items[] = [
|
||||
'slug' => $slug,
|
||||
'label' => $label,
|
||||
'tone' => $tone,
|
||||
'icon_url' => $icon_url,
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
// Rewrite policy: no dual-read of pipe dictionary. Callers must use parse_medal_catalog / get_user_identity_settings()['medal'].
|
||||
unset($raw);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user