Files
wordpress-theme-aksrt/src/User/Badges.php
T

711 lines
28 KiB
PHP

<?php
/**
* @package AKSRT
*/
namespace AKSRT\User;
defined('ABSPATH') || exit;
class Badges {
/**
* Owned badges storage (3.4.0-dev rewrite): user meta holds an
* associative map `slug => grant time (Y-m-d H:i:s)`, newest first.
*/
public const META_OWNED = 'aksrt_user_badges';
public const META_WEAR = 'aksrt_wear_badge';
private static $instance = null;
private static ?array $dictionary_cache = null;
/** @var array<int,array{owned_map:array<string,string>,owned:list<string>,wear:string}> */
private static array $user_context_cache = [];
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_action('wp_ajax_aksrt_user_medal_info_modal', [$this, 'ajax_info_modal']);
add_action('wp_ajax_nopriv_aksrt_user_medal_info_modal', [$this, 'ajax_info_modal']);
add_action('wp_ajax_aksrt_single_medal_info_modal', [$this, 'ajax_single_modal']);
add_action('wp_ajax_nopriv_aksrt_single_medal_info_modal', [$this, 'ajax_single_modal']);
add_action('wp_ajax_aksrt_user_medal_manually_add_modal', [$this, 'ajax_grant_modal']);
add_action('wp_ajax_aksrt_user_medal_wear', [$this, 'ajax_wear']);
add_action('wp_ajax_nopriv_aksrt_user_medal_wear', [$this, 'ajax_wear']);
add_action('wp_ajax_aksrt_user_medal_manually_add', [$this, 'ajax_grant']);
add_action('wp_ajax_nopriv_aksrt_user_medal_manually_add', [$this, 'ajax_grant']);
add_action('wp_ajax_aksrt_user_medal_manually_remove', [$this, 'ajax_revoke']);
add_action('wp_ajax_nopriv_aksrt_user_medal_manually_remove', [$this, 'ajax_revoke']);
add_action('aksrt_badge_granted', [$this, 'on_badge_granted_notify'], 10, 3);
}
private static function clear_context(int $user_id = 0): void {
if ($user_id > 0) {
unset(self::$user_context_cache[$user_id]);
do_action('aksrt_badges_changed', $user_id);
return;
}
self::$user_context_cache = [];
}
public static function invalidate_cache(int $user_id = 0): void {
self::clear_context($user_id);
}
/**
* Notify user when a badge is granted (zibll user_add_medal message).
*/
public function on_badge_granted_notify(int $user_id, string $slug, int $actor = 0): void {
if ($user_id <= 0 || $slug === '') {
return;
}
$map = self::get_dictionary_map();
if (!isset($map[$slug])) {
return;
}
$label = (string) ($map[$slug]['label'] ?? $slug);
$title = sprintf(
/* translators: %s badge name */
__('恭喜您!获得新的徽章[%s]', 'aksrt'),
$label
);
$content = sprintf(
/* translators: %s badge name */
__('恭喜您获得徽章「%s」', 'aksrt'),
$label
);
if (class_exists('\AKSRT\Messages\Message')) {
\AKSRT\Messages\Message::get_instance()->send($user_id, 'medal', $content, [
'title' => $title,
'target_url' => function_exists('aksrt_get_user_center_url')
? aksrt_get_user_center_url()
: home_url('/'),
'slug' => $slug,
]);
}
}
public static function enabled(): bool {
if (!class_exists('\AKSRT\Settings\Settings')) {
return false;
}
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
return !empty($settings['medal']['enabled']);
}
public static function public_display(): bool {
if (!class_exists('\AKSRT\Settings\Settings')) {
return false;
}
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
return !empty($settings['medal']['enabled']) && !empty($settings['medal']['public_display']);
}
/**
* @return list<array{slug:string,label:string,tone:string,desc:string,icon_url:string}>
*/
public static function get_dictionary(): array {
if (self::$dictionary_cache !== null) {
return self::$dictionary_cache;
}
if (!class_exists('\AKSRT\Settings\Settings')) {
return self::$dictionary_cache = [];
}
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
$medal = is_array($settings['medal'] ?? null) ? $settings['medal'] : [];
$dict = [];
foreach ((array) ($medal['items_by_slug'] ?? []) as $item) {
if (!is_array($item)) {
continue;
}
$dict[] = [
'slug' => sanitize_key((string) ($item['slug'] ?? '')),
'label' => (string) ($item['name'] ?? $item['slug'] ?? ''),
'tone' => 'neutral',
'desc' => (string) ($item['desc'] ?? ''),
'icon_url' => (string) ($item['icon'] ?? ''),
];
}
$out = [];
foreach ($dict as $item) {
$slug = sanitize_key((string) ($item['slug'] ?? ''));
if ($slug === '') {
continue;
}
$out[] = [
'slug' => $slug,
'label' => (string) ($item['label'] ?? $slug),
'tone' => (string) ($item['tone'] ?? 'neutral'),
'desc' => (string) ($item['desc'] ?? ''),
'icon_url' => (string) ($item['icon_url'] ?? ''),
];
}
return self::$dictionary_cache = $out;
}
/**
* @return array<string,array{slug:string,label:string,tone:string,desc:string,icon_url:string}>
*/
public static function get_dictionary_map(): array {
static $map = null;
if ($map !== null) {
return $map;
}
$map = [];
foreach (self::get_dictionary() as $item) {
$map[$item['slug']] = $item;
}
return $map;
}
/** @return array{owned_map:array<string,string>,owned:list<string>,wear:string} */
private static function user_context(int $user_id): array {
if ($user_id <= 0) {
return ['owned_map' => [], 'owned' => [], 'wear' => ''];
}
if (isset(self::$user_context_cache[$user_id])) {
return self::$user_context_cache[$user_id];
}
$raw = get_user_meta($user_id, self::META_OWNED, true);
$raw = is_array($raw) ? $raw : [];
$map = self::get_dictionary_map();
$owned_map = [];
foreach ($raw as $slug => $time) {
$slug = sanitize_key((string) $slug);
if ($slug === '' || !isset($map[$slug])) {
continue;
}
$owned_map[$slug] = is_string($time) ? $time : '';
}
$owned = array_keys($owned_map);
$wear = sanitize_key((string) get_user_meta($user_id, self::META_WEAR, true));
if ($wear === '') {
$wear = $owned[0] ?? '';
} elseif (!in_array($wear, $owned, true)) {
$wear = '';
}
return self::$user_context_cache[$user_id] = [
'owned_map' => $owned_map,
'owned' => $owned,
'wear' => $wear,
];
}
/**
* Owned badge map: slug => grant time (`Y-m-d H:i:s`), newest first.
*
* @return array<string,string>
*/
public static function get_owned_map(int $user_id): array {
return self::user_context($user_id)['owned_map'];
}
/**
* Owned badge slugs, newest first.
*
* @return list<string>
*/
public static function get_owned(int $user_id): array {
return self::user_context($user_id)['owned'];
}
/**
* Replace the owned set; existing slugs keep their grant time, new slugs
* are stamped with the current time. Order follows the input list.
*
* @param list<string> $slugs
*/
public static function set_owned(int $user_id, array $slugs): void {
if ($user_id <= 0) {
return;
}
$map = self::get_dictionary_map();
$existing = self::get_owned_map($user_id);
$now = current_time('Y-m-d H:i:s');
$clean = [];
foreach ($slugs as $slug) {
$slug = sanitize_key((string) $slug);
if ($slug === '' || !isset($map[$slug]) || isset($clean[$slug])) {
continue;
}
$clean[$slug] = $existing[$slug] ?? $now;
}
update_user_meta($user_id, self::META_OWNED, $clean);
$wear = self::get_wear($user_id);
if ($wear !== '' && !isset($clean[$wear])) {
delete_user_meta($user_id, self::META_WEAR);
}
self::clear_context($user_id);
}
public static function get_wear(int $user_id): string {
return self::user_context($user_id)['wear'];
}
/**
* @return true|\WP_Error
*/
public static function wear(int $user_id, string $slug) {
if ($user_id <= 0) {
return new \WP_Error('aksrt_badge_user', __('无效用户', 'aksrt'), ['status' => 400]);
}
if (!self::enabled()) {
return new \WP_Error('aksrt_badge_disabled', __('徽章系统未启用', 'aksrt'), ['status' => 403]);
}
$slug = sanitize_key($slug);
$map = self::get_dictionary_map();
if ($slug === '' || !isset($map[$slug])) {
return new \WP_Error('aksrt_badge_unknown', __('徽章不存在', 'aksrt'), ['status' => 404]);
}
$owned = self::get_owned($user_id);
if (!in_array($slug, $owned, true)) {
return new \WP_Error('aksrt_badge_not_owned', __('尚未获得该徽章', 'aksrt'), ['status' => 403]);
}
update_user_meta($user_id, self::META_WEAR, $slug);
self::clear_context($user_id);
return true;
}
/**
* @return true|\WP_Error
*/
public static function grant(int $user_id, string $slug, int $actor = 0) {
if ($user_id <= 0) {
return new \WP_Error('aksrt_badge_user', __('无效用户', 'aksrt'), ['status' => 400]);
}
if (!self::enabled()) {
return new \WP_Error('aksrt_badge_disabled', __('徽章系统未启用', 'aksrt'), ['status' => 403]);
}
$slug = sanitize_key($slug);
$map = self::get_dictionary_map();
if ($slug === '' || !isset($map[$slug])) {
return new \WP_Error('aksrt_badge_unknown', __('徽章不存在', 'aksrt'), ['status' => 404]);
}
$owned = self::get_owned($user_id);
if (in_array($slug, $owned, true)) {
return true;
}
// Newest first (zibll medal ordering); grant time stamped by set_owned.
self::set_owned($user_id, array_merge([$slug], $owned));
/**
* Fires after a badge is granted.
*
* @param int $user_id
* @param string $slug
* @param int $actor
*/
do_action('aksrt_badge_granted', $user_id, $slug, $actor);
return true;
}
/**
* @return true|\WP_Error
*/
public static function revoke(int $user_id, string $slug, int $actor = 0) {
if ($user_id <= 0) {
return new \WP_Error('aksrt_badge_user', __('无效用户', 'aksrt'), ['status' => 400]);
}
$slug = sanitize_key($slug);
$owned = self::get_owned($user_id);
$owned = array_values(array_filter($owned, static function ($s) use ($slug) {
return $s !== $slug;
}));
self::set_owned($user_id, $owned);
/**
* @param int $user_id
* @param string $slug
* @param int $actor
*/
do_action('aksrt_badge_revoked', $user_id, $slug, $actor);
return true;
}
public static function can_manually_set(int $actor = 0): bool {
$actor = $actor > 0 ? $actor : get_current_user_id();
if ($actor <= 0) {
return false;
}
if (user_can($actor, 'manage_options')) {
return true;
}
return false;
}
/** Build the privacy-aware catalog used by the server-rendered modal. */
public static function list_for_user(int $user_id, bool $include_private = false): array {
$enabled = self::enabled();
$public = self::public_display();
$viewer = get_current_user_id();
$is_self = $viewer > 0 && $viewer === $user_id;
$is_manager = self::can_manually_set($viewer);
$can_see = $include_private || $public || $is_self || $is_manager;
if (!$enabled || !$can_see || $user_id <= 0) {
return [
'enabled' => $enabled,
'public_display' => $public,
'is_self' => $is_self,
'can_wear' => $is_self,
'user_id' => $user_id,
'wear' => '',
'count' => 0,
'show_unearned' => false,
'categories' => [],
];
}
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
$medal = is_array($settings['medal'] ?? null) ? $settings['medal'] : [];
$catalog = is_array($medal['categories'] ?? null) ? $medal['categories'] : [];
$context = self::user_context($user_id);
$owned_map = $context['owned_map'];
$wear = $context['wear'];
$show_private = $include_private || $is_self || $is_manager;
$show_unearned = $show_private;
$categories = [];
foreach ($catalog as $cat) {
$cat_name = (string) ($cat['cat_name'] ?? '');
$rows = is_array($cat['items'] ?? null) ? $cat['items'] : [];
$has_rows = [];
$no_rows = [];
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
$slug = sanitize_key((string) ($row['slug'] ?? ''));
if ($slug === '') {
continue;
}
$has = isset($owned_map[$slug]);
if (!$has && !$show_unearned) {
continue;
}
$item = [
'slug' => $slug,
'label' => (string) ($row['name'] ?? $slug),
'desc' => (string) ($row['desc'] ?? ''),
'icon_url' => (string) ($row['icon'] ?? ''),
'owned' => $has,
'wear' => $has && $slug === $wear,
'time' => $has && $show_private ? (string) $owned_map[$slug] : '',
'get_type' => sanitize_key((string) ($row['get_type'] ?? 'manually_add')),
];
// Owned first within each category (zibll is-has / no-has order).
if ($has) {
$has_rows[] = $item;
} else {
$no_rows[] = $item;
}
}
$items = array_merge($has_rows, $no_rows);
if (!$items) {
continue;
}
$categories[] = [
'cat_name' => $cat_name,
'items' => $items,
];
}
return [
'enabled' => true,
'public_display' => $public,
'is_self' => $is_self,
'can_wear' => $is_self,
'user_id' => $user_id,
'wear' => $wear,
'count' => count($owned_map),
'show_unearned' => $show_unearned,
'categories' => $categories,
];
}
private static function ajax_nonce(): string { return 'aksrt_public_nonce'; }
private static function ajax_error(string $message): void {
wp_send_json_error(['message' => $message]);
}
private static function ajax_require_nonce(): void {
$nonce = isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash((string) $_REQUEST['nonce'])) : '';
if (!$nonce || !wp_verify_nonce($nonce, self::ajax_nonce())) {
self::ajax_error(__('请求已过期,请刷新页面后重试', 'aksrt'));
}
}
private static function ajax_user_id(): int {
return isset($_REQUEST['id']) ? absint($_REQUEST['id']) : 0;
}
private static function ajax_slug(): string {
return isset($_REQUEST['slug']) ? sanitize_key(wp_unslash((string) $_REQUEST['slug'])) : '';
}
private static function icon_html(array $item, string $class = 'img-icon medal-icon'): string {
$icon = esc_url((string) ($item['icon_url'] ?? ''));
$label = esc_attr((string) ($item['label'] ?? __('徽章', 'aksrt')));
if ($icon !== '') {
return '<img src="' . $icon . '" class="' . esc_attr($class) . '" alt="' . $label . '" decoding="async">';
}
return '<span class="medal-icon medal-icon--fa"><i class="fa fas fa-trophy" aria-hidden="true"></i></span>';
}
private static function item_by_slug(array $data, string $slug): array {
foreach ((array) ($data['categories'] ?? []) as $category) {
foreach ((array) ($category['items'] ?? []) as $item) {
if ((string) ($item['slug'] ?? '') === $slug) return $item;
}
}
return [];
}
private static function modal_header(int $user_id, string $text): string {
$avatar = function_exists('aksrt_get_avatar_html') ? aksrt_get_avatar_html($user_id, 42, ['class' => 'medal-modal__avatar']) : get_avatar($user_id, 42);
return '<div class="medal-modal__head">' . $avatar . '<div class="medal-modal__title">' . $text . '</div></div>';
}
private static function modal_info_html(int $user_id): string {
$user = get_userdata($user_id);
if (!$user) return '<div class="medal-modal__empty">' . esc_html__('用户不存在', 'aksrt') . '</div>';
if (!self::enabled()) return '<div class="medal-modal__empty">' . esc_html__('徽章系统未启用', 'aksrt') . '</div>';
$data = self::list_for_user($user_id);
$name = $user_id === get_current_user_id() ? __('我的徽章', 'aksrt') : sprintf(__('%s的徽章', 'aksrt'), $user->display_name);
$wear = self::item_by_slug($data, (string) ($data['wear'] ?? ''));
if ($wear) $name .= self::icon_html($wear, 'img-icon medal-icon ml3');
elseif (!(int) ($data['count'] ?? 0)) $name .= '<span class="badg-sm badg c-red ml6">' . esc_html__('暂无徽章', 'aksrt') . '</span>';
$html = '<div class="medal-modal">' . self::modal_header($user_id, '<span class="em09">' . wp_kses_post($name) . '</span>') . '<div class="mini-scrollbar scroll-y max-vh5 medal-modal__scroll">';
$has = false;
foreach ((array) ($data['categories'] ?? []) as $category) {
$cards = '';
foreach ((array) ($category['items'] ?? []) as $item) {
if (empty($item['owned']) && empty($data['show_unearned'])) continue;
$has = true;
$card = '<div class="medal-card' . (!empty($item['owned']) ? ' is-has' : ' no-has') . '">' . self::icon_html($item) . '<div class="muted-color px12 mt6">' . esc_html((string) $item['label']) . '</div></div>';
$url = add_query_arg(['action' => 'aksrt_single_medal_info_modal', 'name' => $item['slug'], 'user_id' => $user_id], admin_url('admin-ajax.php'));
$cards .= '<div class="medal-list"><a class="single-medal-info aksrt-modal-link" data-aksrt-modal="remote" data-aksrt-modal-url="' . esc_url($url) . '" data-aksrt-modal-class="aksrt-badge-modal aksrt-badge-modal--single" data-aksrt-modal-mobile-bottom="true" data-aksrt-modal-cache="false" href="' . esc_url($url) . '">' . $card . '</a></div>';
}
if ($cards) $html .= '<div class="medal-cat-box muted-box mb20"><div class="mb20"><b class="medal-cat-name">' . esc_html((string) $category['cat_name']) . '</b></div><div class="medal-list-box">' . $cards . '</div></div>';
}
if (!$has) $html .= '<div class="medal-modal__empty">' . esc_html__('暂无徽章', 'aksrt') . '</div>';
$html .= '</div>';
if ($user_id !== get_current_user_id()) {
$mine = add_query_arg(['action' => 'aksrt_user_medal_info_modal', 'id' => get_current_user_id()], admin_url('admin-ajax.php'));
$grant = add_query_arg(['action' => 'aksrt_user_medal_manually_add_modal', 'id' => $user_id], admin_url('admin-ajax.php'));
$buttons = '';
if (get_current_user_id()) $buttons .= '<a class="but aksrt-modal-link" data-aksrt-modal="remote" data-aksrt-modal-url="' . esc_url($mine) . '" data-aksrt-modal-class="aksrt-badge-modal aksrt-badge-modal--overview" data-aksrt-modal-cache="false" href="' . esc_url($mine) . '">' . esc_html__('查看我的徽章', 'aksrt') . '</a>';
if (self::can_manually_set()) $buttons .= '<a class="but c-blue aksrt-modal-link" data-aksrt-modal="remote" data-aksrt-modal-url="' . esc_url($grant) . '" data-aksrt-modal-class="aksrt-badge-modal aksrt-badge-modal--grant" data-aksrt-modal-cache="false" href="' . esc_url($grant) . '">' . esc_html__('授予徽章', 'aksrt') . '</a>';
if ($buttons !== '') $html .= '<div class="medal-modal__buts">' . $buttons . '</div>';
}
$html .= '</div>';
return $html;
}
private static function modal_single_html(int $user_id, string $slug): string {
if (!$user_id || !get_userdata($user_id)) return '<div class="medal-modal__empty">' . esc_html__('用户不存在', 'aksrt') . '</div>';
if (!self::enabled()) return '<div class="medal-modal__empty">' . esc_html__('徽章系统未启用', 'aksrt') . '</div>';
$data = self::list_for_user($user_id);
$item = self::item_by_slug($data, $slug);
if (!$item) return '<div class="medal-modal__empty">' . esc_html__('参数传入错误或没有此徽章', 'aksrt') . '</div>';
$state = '';
if (!empty($item['owned']) && !empty($item['time'])) $state = '<div class="c-blue px12 mt10">' . esc_html(sprintf(__('%s 获得', 'aksrt'), $item['time'])) . '</div>';
elseif (empty($item['owned'])) $state = '<div class="c-yellow px12 mt20">' . esc_html__('暂未获得', 'aksrt') . '</div>';
$html = '<div class="medal-modal medal-modal--single"><div class="medal-single-card"><div class="medal-single-bg"><img src="' . esc_url(get_theme_file_uri('/assets/img/medal/medal-background.svg')) . '" alt=""></div><div class="relative">' . self::icon_html($item) . '<div class="mt20">' . esc_html($item['label']) . '</div><div class="muted-2-color px12 mt10">' . esc_html($item['desc']) . '</div>' . $state . '</div></div>';
if ($user_id === get_current_user_id() && !empty($item['owned'])) {
if (!empty($item['wear'])) $html .= '<div class="box-body"><span class="badg btn-block c-blue">' . esc_html__('当前佩戴此徽章', 'aksrt') . '</span></div>';
else $html .= '<div class="box-body"><a href="javascript:;" class="but btn-block radius jb-blue aksrt-badge-action" data-aksrt-badge-action="wear" data-slug="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</a></div>';
} elseif ($user_id !== get_current_user_id() && self::can_manually_set() && !empty($item['owned'])) {
$html .= '<div class="box-body"><a href="javascript:;" class="but btn-block radius c-red aksrt-badge-action" data-aksrt-badge-action="revoke" data-user-id="' . (int) $user_id . '" data-slug="' . esc_attr($slug) . '">' . esc_html__('收回此徽章', 'aksrt') . '</a></div>';
} elseif ($user_id !== get_current_user_id() && self::can_manually_set() && empty($item['owned']) && (($item['get_type'] ?? '') === 'manually_add')) {
$html .= '<div class="box-body"><a href="javascript:;" class="but btn-block radius c-blue aksrt-badge-action" data-aksrt-badge-action="grant" data-user-id="' . (int) $user_id . '" data-slug="' . esc_attr($slug) . '">' . esc_html__('授予此徽章', 'aksrt') . '</a></div>';
}
return $html . '</div>';
}
public function ajax_info_modal(): void { echo self::modal_info_html(self::ajax_user_id()); exit; }
public function ajax_single_modal(): void { echo self::modal_single_html(absint($_REQUEST['user_id'] ?? 0), sanitize_key(wp_unslash((string) ($_REQUEST['name'] ?? '')))); exit; }
public function ajax_grant_modal(): void {
$user_id = self::ajax_user_id();
if (!$user_id || !get_userdata($user_id)) { echo '<div class="medal-modal__empty">' . esc_html__('用户不存在', 'aksrt') . '</div>'; exit; }
if (!self::enabled()) { echo '<div class="medal-modal__empty">' . esc_html__('徽章系统未启用', 'aksrt') . '</div>'; exit; }
if (!self::can_manually_set()) { echo '<div class="medal-modal__empty">' . esc_html__('您没有此权限', 'aksrt') . '</div>'; exit; }
$data = self::list_for_user($user_id, true);
$html = '<div class="medal-modal">' . self::modal_header($user_id, '<span class="em09">' . sprintf(__('为%s授予新的徽章', 'aksrt'), esc_html(get_the_author_meta('display_name', $user_id))) . '</span>') . '<div class="mini-scrollbar scroll-y max-vh5 medal-modal__scroll">';
$has = false;
foreach ((array) ($data['categories'] ?? []) as $category) {
$cards = '';
foreach ((array) ($category['items'] ?? []) as $item) {
if (($item['get_type'] ?? '') !== 'manually_add') continue;
$has = true;
$action = !empty($item['owned']) ? 'revoke' : 'grant';
$label = $action === 'revoke' ? __('收回', 'aksrt') : __('授予', 'aksrt');
$cards .= '<div class="medal-grant-item"><div class="medal-card">' . self::icon_html($item) . '<div class="mt6 em09">' . esc_html($item['label']) . '</div><div class="muted-2-color px12 mt6">' . esc_html($item['desc']) . '</div></div><a href="javascript:;" class="but btn-block radius px12 c-' . ($action === 'revoke' ? 'red' : 'blue') . ' mt6 aksrt-badge-action" data-aksrt-badge-action="' . esc_attr($action) . '" data-user-id="' . (int) $user_id . '" data-slug="' . esc_attr($item['slug']) . '">' . esc_html($label) . '</a></div>';
}
if ($cards) $html .= '<div class="medal-cat-box muted-box mb20"><div class="mb20"><b class="medal-cat-name">' . esc_html($category['cat_name']) . '</b></div><div class="medal-list-box medal-list-box--grant">' . $cards . '</div></div>';
}
if (!$has) $html .= '<div class="medal-modal__empty">' . esc_html__('暂无可授予的徽章', 'aksrt') . '</div>';
echo $html . '</div></div>';
exit;
}
private function ajax_mutation(string $operation): void {
self::ajax_require_nonce();
if (!is_user_logged_in()) self::ajax_error(__('请先登录', 'aksrt'));
if (!self::enabled()) self::ajax_error(__('徽章系统未启用', 'aksrt'));
$user_id = $operation === 'wear' ? get_current_user_id() : self::ajax_user_id();
$slug = self::ajax_slug();
if (!$user_id || !get_userdata($user_id) || !$slug) self::ajax_error(__('参数传入错误', 'aksrt'));
if ($operation !== 'wear' && !self::can_manually_set()) self::ajax_error(__('暂无此权限', 'aksrt'));
$item = self::item_by_slug(self::list_for_user($user_id, true), $slug);
if (!$item) self::ajax_error(__('徽章不存在', 'aksrt'));
if ($operation === 'grant' && ($item['get_type'] ?? '') !== 'manually_add') self::ajax_error(__('该徽章不可手动授予', 'aksrt'));
if ($operation === 'revoke' && empty($item['owned'])) self::ajax_error(__('用户未获得该徽章', 'aksrt'));
$result = $operation === 'wear' ? self::wear($user_id, $slug) : ($operation === 'grant' ? self::grant($user_id, $slug, get_current_user_id()) : self::revoke($user_id, $slug, get_current_user_id()));
if (is_wp_error($result)) self::ajax_error($result->get_error_message());
$message = ['wear' => __('已佩戴徽章', 'aksrt'), 'grant' => __('已授予徽章', 'aksrt'), 'revoke' => __('已收回徽章', 'aksrt')][$operation];
wp_send_json_success(['message' => $message, 'reload' => true]);
}
public function ajax_wear(): void { $this->ajax_mutation('wear'); }
public function ajax_grant(): void { $this->ajax_mutation('grant'); }
public function ajax_revoke(): void { $this->ajax_mutation('revoke'); }
/**
* 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\Settings')) {
return;
}
$settings = \AKSRT\Settings\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 'comment':
$counts = get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']);
return (float) $counts;
case 'followed':
if (class_exists('AKSRT\\User\\Follow')) {
return (float) Follow::get_instance()->get_follower_count($user_id);
}
return 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 'views':
$sum = (float) get_user_meta($user_id, 'aksrt_posts_views_sum', true);
if ($sum > 0) {
return $sum;
}
// Lazy backfill once from postmeta.
global $wpdb;
$sum = (float) $wpdb->get_var($wpdb->prepare(
"SELECT COALESCE(SUM(CAST(pm.meta_value AS UNSIGNED)),0)
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = 'aksrt_post_views'
WHERE p.post_author = %d AND p.post_type = 'post' AND p.post_status = 'publish'",
$user_id
));
if ($sum > 0) {
update_user_meta($user_id, 'aksrt_posts_views_sum', $sum);
}
return $sum;
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);
}
}
}