From e80d8ea45da0e09dba8cb516ee251521aa666ca6 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Jul 2026 01:00:05 +0800 Subject: [PATCH 1/4] 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. --- assets/admin/src/constants/sections/users.js | 4 +- assets/admin/src/pages/users/UsersBadge.vue | 236 ++++++++++++------ assets/admin/src/pages/users/UsersVerify.vue | 55 ++++ .../admin/src/schema/sections/users/badge.js | 110 ++++++-- .../admin/src/schema/sections/users/verify.js | 89 ++++++- .../schema/class-aksrt-admin-schema-users.php | 68 +++-- .../class-aksrt-settings-user-runtime.php | 164 +++++++++--- 7 files changed, 572 insertions(+), 154 deletions(-) diff --git a/assets/admin/src/constants/sections/users.js b/assets/admin/src/constants/sections/users.js index c657912a..de46f7cc 100644 --- a/assets/admin/src/constants/sections/users.js +++ b/assets/admin/src/constants/sections/users.js @@ -71,7 +71,7 @@ export const usersSection = { component: 'UsersVerify', status: 'available', title: '身份认证', - desc: '身份认证字段与展示配置(审核队列在 用户 → 身份认证)。', + desc: '认证展示、申请门槛与审核策略(审核队列在 用户 → 身份认证)。', canonicalDomain: 'users', section: 'verify', }), @@ -95,7 +95,7 @@ export const usersSection = { component: 'UsersBadge', status: 'available', title: '用户徽章', - desc: '徽章规则、授予条件和前台展示。', + desc: '勋章分类、自动授予条件(get_type)与前台佩戴展示。存储键 user_medal_*。', canonicalDomain: 'users', section: 'badge', }), diff --git a/assets/admin/src/pages/users/UsersBadge.vue b/assets/admin/src/pages/users/UsersBadge.vue index 3dda7781..cc05e113 100644 --- a/assets/admin/src/pages/users/UsersBadge.vue +++ b/assets/admin/src/pages/users/UsersBadge.vue @@ -2,7 +2,7 @@
+ + 转账/提现(balance_transfer、points_transfer、balance_withdraw)默认 exclude all,与 Zibll 一致:未在矩阵中为角色/等级/VIP/认证打开前,前台入口会隐藏或拒绝。 + medal_manually_set 控制谁可授予徽章;管理员 manage_options 始终可通过。 + + +
diff --git a/inc/classes/class-aksrt-user-badges.php b/inc/classes/class-aksrt-user-badges.php index c7e1e59b..f018a9d7 100644 --- a/inc/classes/class-aksrt-user-badges.php +++ b/inc/classes/class-aksrt-user-badges.php @@ -404,4 +404,136 @@ class AKSRT_User_Badges { 'message' => __('已收回徽章', 'aksrt'), ]); } + + + /** + * Auto-grant medals whose get_type metrics meet get_val (skip manually_add). + */ + public static function auto_evaluate(int $user_id): void { + if ($user_id <= 0 || !self::enabled() || !class_exists('AKSRT_Settings')) { + return; + } + $settings = AKSRT_Settings::get_user_identity_settings(); + $medal = is_array($settings['medal'] ?? null) ? $settings['medal'] : []; + $items = is_array($medal['items_by_slug'] ?? null) ? $medal['items_by_slug'] : []; + if (!$items) { + // Fall back to dictionary only — no get_type → nothing to auto-grant. + return; + } + $owned = self::get_owned($user_id); + $owned_map = array_fill_keys($owned, true); + foreach ($items as $slug => $item) { + if (!is_array($item)) { + continue; + } + $slug = sanitize_key((string) $slug); + if ($slug === '' || !empty($owned_map[$slug])) { + continue; + } + $type = sanitize_key((string) ($item['get_type'] ?? 'manually_add')); + if ($type === '' || $type === 'manually_add') { + continue; + } + $need = max(0, (float) ($item['get_val'] ?? 0)); + $have = (float) self::metric_value($user_id, $type); + if ($have >= $need) { + self::grant($user_id, $slug, 0); + $owned_map[$slug] = true; + } + } + } + + /** + * Numeric metric for a medal get_type. + */ + public static function metric_value(int $user_id, string $type): float { + switch ($type) { + case 'registration_time': + $user = get_userdata($user_id); + if (!$user) { + return 0.0; + } + $reg = get_date_from_gmt($user->user_registered, 'Y-m-d H:i:s'); + $now = current_time('Y-m-d H:i:s'); + return max(0.0, (strtotime($now) - strtotime($reg)) / 86400); + + case 'checkin_all_day': + // Prefer dedicated check-in total meta if present. + $v = get_user_meta($user_id, 'aksrt_checkin_all_day', true); + if ($v === '' || $v === false) { + $v = get_user_meta($user_id, 'checkin_all_day', true); + } + return max(0.0, (float) $v); + + case 'new_post': + return (float) count_user_posts($user_id, 'post', true); + + case 'bbs_post': + $types = post_type_exists('forum_topic') ? 'forum_topic' : (post_type_exists('topic') ? 'topic' : ''); + return $types !== '' ? (float) count_user_posts($user_id, $types, true) : 0.0; + + case 'comment': + $counts = get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']); + return (float) $counts; + + case 'followed': + $meta = get_user_meta($user_id, 'followed-user', true); + if (!is_array($meta)) { + $meta = get_user_meta($user_id, 'aksrt_followed_users', true); + } + return is_array($meta) ? (float) count($meta) : 0.0; + + case 'like': + return (float) get_user_meta($user_id, 'aksrt_posts_like_sum', true); + + case 'comment_like': + return (float) get_user_meta($user_id, 'aksrt_comment_like_sum', true); + + case 'bbs_adopt': + return (float) get_user_meta($user_id, 'aksrt_bbs_adopt_count', true); + + case 'bbs_comment_hot': + return (float) get_user_meta($user_id, 'aksrt_bbs_comment_hot_count', true); + + case 'bbs_post_hot': + return (float) get_user_meta($user_id, 'aksrt_bbs_post_hot_count', true); + + case 'bbs_essence': + return (float) get_user_meta($user_id, 'aksrt_bbs_essence_count', true); + + case 'bbs_score': + return (float) get_user_meta($user_id, 'aksrt_bbs_score_sum', true); + + case 'views': + return (float) get_user_meta($user_id, 'aksrt_posts_views_sum', true); + + case 'pay_income': + return (float) get_user_meta($user_id, 'aksrt_pay_income_sum', true); + + case 'pay_rebate': + return (float) get_user_meta($user_id, 'aksrt_pay_rebate_sum', true); + + default: + return 0.0; + } + } + + /** + * @param string $user_login + * @param WP_User $user + */ + public function on_login($user_login, $user): void { + $user_id = $user instanceof WP_User ? (int) $user->ID : 0; + if ($user_id > 0) { + self::auto_evaluate($user_id); + } + } + + public function on_current_user_probe(): void { + $uid = get_current_user_id(); + if ($uid > 0) { + self::auto_evaluate($uid); + } + } + } diff --git a/inc/helpers/aksrt-identity.php b/inc/helpers/aksrt-identity.php index 8ff34f17..1862c1ed 100644 --- a/inc/helpers/aksrt-identity.php +++ b/inc/helpers/aksrt-identity.php @@ -91,6 +91,119 @@ if (!function_exists('aksrt_render_user_identity_chips')) { } } +if (!function_exists('aksrt_get_user_name')) { + /** + * Zibll zib_get_user_name parity: name link + auth + wear medal + level + optional follow. + * + * @param array|int $args User id or args array. + */ + function aksrt_get_user_name($args = []): string { + $defaults = [ + 'id' => 0, + 'class' => 'flex ac flex1', + 'name_class' => '', + 'vip' => false, + 'medal' => true, + 'auth' => true, + 'level' => true, + 'follow' => false, + ]; + if (is_numeric($args)) { + $defaults['id'] = (int) $args; + $args = $defaults; + } else { + $args = wp_parse_args(is_array($args) ? $args : [], $defaults); + } + $user_id = (int) $args['id']; + if ($user_id <= 0) { + $user_id = get_current_user_id(); + } + if ($user_id <= 0) { + return ''; + } + + $user = get_userdata($user_id); + if (!$user) { + return ''; + } + + $home = function_exists('get_author_posts_url') ? get_author_posts_url($user_id) : ''; + $display = $user->display_name; + $vip_html = ''; + if (!empty($args['vip']) && function_exists('aksrt_render_vip_badge')) { + $vip_html = (string) aksrt_render_vip_badge($user_id, 'sm'); + } + $name_class = trim('display-name text-ellipsis ' . (string) $args['name_class']); + $name_link = '' . $vip_html . esc_html($display) . ''; + + $auth_html = ''; + $medal_html = ''; + $level_html = ''; + $signals = aksrt_get_user_identity_signals($user_id); + foreach ($signals as $signal) { + $type = (string) ($signal['type'] ?? ''); + $label = trim((string) ($signal['label'] ?? '')); + if ($label === '') { + continue; + } + $icon_url = esc_url((string) ($signal['icon_url'] ?? '')); + $title = (string) ($signal['title'] ?? $label); + if ($type === 'verify' && !empty($args['auth']) && $auth_html === '') { + $auth_html = ' ' . esc_html($label) . ''; + } elseif ($type === 'level' && !empty($args['level']) && $level_html === '') { + $level_html = ' ' . esc_html($label) . ''; + } elseif ($type === 'badge' && !empty($args['medal']) && $medal_html === '' && !empty($signal['wear'])) { + if ($icon_url !== '') { + $medal_html = '' . esc_attr($label) . ''; + } else { + $medal_html = ' ' . esc_html($label) . ''; + } + } + } + // If medal requested but no wear flag, use first badge with icon. + if (!empty($args['medal']) && $medal_html === '') { + foreach ($signals as $signal) { + if ((string) ($signal['type'] ?? '') !== 'badge') { + continue; + } + $label = trim((string) ($signal['label'] ?? '')); + $icon_url = esc_url((string) ($signal['icon_url'] ?? '')); + $title = (string) ($signal['title'] ?? $label); + if ($label === '') { + continue; + } + if ($icon_url !== '') { + $medal_html = '' . esc_attr($label) . ''; + } else { + $medal_html = ' ' . esc_html($label) . ''; + } + break; + } + } + + $follow_html = ''; + if (!empty($args['follow']) && get_current_user_id() !== $user_id) { + if (is_user_logged_in()) { + $follow_html = ''; + } else { + $login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(); + $follow_html = ''; + } + } + + // Order: name → auth → wear medal → level → follow (Zibll). + $html = '' . $name_link . $auth_html . $medal_html . $level_html . $follow_html . ''; + /** + * Filter rendered user name chrome. + * + * @param string $html + * @param int $user_id + * @param array $args + */ + return (string) apply_filters('aksrt_user_show_name', $html, $user_id, $args); + } +} + if (!function_exists('aksrt_is_blog_comment_hot')) { /** * "神评" for ordinary post comments when likes cross threshold. diff --git a/inc/helpers/aksrt-single-render.php b/inc/helpers/aksrt-single-render.php index c108113d..2f01bdf7 100644 --- a/inc/helpers/aksrt-single-render.php +++ b/inc/helpers/aksrt-single-render.php @@ -331,7 +331,18 @@ if (!function_exists('aksrt_single_render_author_card')) : $author_url = get_author_posts_url($author_id); $author_name = get_the_author(); $bio = trim((string) get_the_author_meta('description')); - $identity_chips = function_exists('aksrt_render_user_identity_chips') + $name_html = function_exists('aksrt_get_user_name') + ? aksrt_get_user_name([ + 'id' => $author_id, + 'class' => 'user-name flex jc ac', + 'vip' => true, + 'medal' => true, + 'auth' => true, + 'level' => true, + 'follow' => false, + ]) + : ''; + $identity_chips = ($name_html === '' && function_exists('aksrt_render_user_identity_chips')) ? aksrt_render_user_identity_chips($author_id, 'card') : ''; $avatar = function_exists('aksrt_get_avatar_html') @@ -362,12 +373,16 @@ if (!function_exists('aksrt_single_render_author_card')) :
-
+