feat(users): medal auto-grant + aksrt_get_user_name chrome

Add AKSRT_User_Badges::auto_evaluate on login for get_type metrics,
introduce aksrt_get_user_name (auth→wear medal→level→follow order),
wire single author bar/card, and document transfer/withdraw default-closed
semantics on capabilities page. audit errors=0.
This commit is contained in:
2026-07-17 01:12:09 +08:00
parent 1b0eed6ef7
commit b4d04ae346
5 changed files with 299 additions and 16 deletions
+132
View File
@@ -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);
}
}
}
+113
View File
@@ -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 = '<a class="' . esc_attr($name_class) . '" href="' . esc_url($home) . '">' . $vip_html . esc_html($display) . '</a>';
$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 = '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--success" title="' . esc_attr($title) . '"><i class="fa fa-check-circle" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
} elseif ($type === 'level' && !empty($args['level']) && $level_html === '') {
$level_html = '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--info" title="' . esc_attr($title) . '"><i class="fa fa-layer-group" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
} elseif ($type === 'badge' && !empty($args['medal']) && $medal_html === '' && !empty($signal['wear'])) {
if ($icon_url !== '') {
$medal_html = '<img class="medal-icon img-icon ml3 aksrt-identity-medal is-wear" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
} else {
$medal_html = '<span class="badg badg-sm ml3 aksrt-identity-chip is-wear" title="' . esc_attr($title) . '"><i class="fa fa-award" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
}
}
}
// 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 = '<img class="medal-icon img-icon ml3 aksrt-identity-medal" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
} else {
$medal_html = '<span class="badg badg-sm ml3 aksrt-identity-chip" title="' . esc_attr($title) . '"><i class="fa fa-award" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
}
break;
}
}
$follow_html = '';
if (!empty($args['follow']) && get_current_user_id() !== $user_id) {
if (is_user_logged_in()) {
$follow_html = '<button type="button" class="focus-color ml10 follow flex0 but" data-aksrt-follow="' . (int) $user_id . '"><i class="fa fa-heart em09" aria-hidden="true"></i> ' . esc_html__('关注', 'aksrt') . '</button>';
} else {
$login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url();
$follow_html = '<a class="focus-color ml10 follow flex0 but" href="' . esc_url($login) . '"><i class="fa fa-heart em09" aria-hidden="true"></i> ' . esc_html__('关注', 'aksrt') . '</a>';
}
}
// Order: name → auth → wear medal → level → follow (Zibll).
$html = '<name class="' . esc_attr((string) $args['class']) . '">' . $name_link . $auth_html . $medal_html . $level_html . $follow_html . '</name>';
/**
* 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.
+21 -6
View File
@@ -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')) :
<div class="user-content">
<div class="user-avatar"><?php echo $avatar; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="user-info mt20 mb10">
<div class="user-name flex jc ac">
<a class="display-name" href="<?php echo esc_url($author_url); ?>"><?php echo esc_html($author_name); ?></a>
<?php if ($identity_chips !== '') : ?>
<?php echo $identity_chips; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php if ($name_html !== '') : ?>
<?php echo $name_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php else : ?>
<div class="user-name flex jc ac">
<a class="display-name" href="<?php echo esc_url($author_url); ?>"><?php echo esc_html($author_name); ?></a>
<?php if ($identity_chips !== '') : ?>
<?php echo $identity_chips; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php if ($bio !== '') : ?>
<div class="user-desc mt10 muted-2-color em09"><?php echo esc_html($bio); ?></div>
<?php endif; ?>