perf(modal): unify cancellable frontend modal flows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+141
-27
@@ -11,6 +11,7 @@ use AKSRT\Identity\ApplicationRepository;
|
||||
use AKSRT\Identity\EvidenceService;
|
||||
use AKSRT\Identity\EventRepository;
|
||||
use AKSRT\Identity\TransitionService;
|
||||
use AKSRT\Identity\SettingsRuntime;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
@@ -44,49 +45,162 @@ class AuthApply {
|
||||
return is_array($types[$type] ?? null) ? $types[$type] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current user's identity state from one repository context.
|
||||
*
|
||||
* The repository memoizes the latest/active/verified records for the
|
||||
* request, while this typed facade keeps all presentation callers on the
|
||||
* same application selection and next-action contract.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function snapshot(int $user_id): array {
|
||||
$user_id = absint($user_id);
|
||||
$settings = self::settings();
|
||||
$records = (new ApplicationRepository())->snapshot_for_user($user_id);
|
||||
$verified = is_array($records['verified'] ?? null) ? $records['verified'] : null;
|
||||
$latest = is_array($records['latest'] ?? null) ? $records['latest'] : null;
|
||||
$active = is_array($records['active'] ?? null) ? $records['active'] : null;
|
||||
$status_record = $verified ?: $latest;
|
||||
$status = $status_record
|
||||
? TransitionService::public_status((string) ($status_record['status'] ?? ''))
|
||||
: '';
|
||||
$current = $active ?: $latest;
|
||||
$stage = $active
|
||||
? (string) ($active['status'] ?? '')
|
||||
: (string) ($latest['status'] ?? '');
|
||||
$next_action = self::next_action($stage);
|
||||
$runtime_settings = class_exists('\\AKSRT\\Settings\\Settings') ? SettingsRuntime::get() : [];
|
||||
$poll_interval = max(5, (int) ($runtime_settings['poll_interval_seconds'] ?? 5));
|
||||
$can_apply = !empty($settings['enabled']) && !empty($settings['allow_apply']) && $status !== 'verified';
|
||||
|
||||
return [
|
||||
'user_id' => $user_id,
|
||||
'verified' => $verified !== null,
|
||||
'status' => $status,
|
||||
'stage' => $stage,
|
||||
'next_action' => $next_action,
|
||||
'poll_interval' => $poll_interval,
|
||||
'can_apply' => $can_apply,
|
||||
'latest' => $latest,
|
||||
'active' => $active,
|
||||
'current' => $current,
|
||||
'info' => $verified ? self::info_from_application($verified) : [],
|
||||
'apply' => self::payload_from_application($latest),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe browser-facing snapshot; raw application rows never cross this
|
||||
* boundary.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function snapshot_view(int $user_id): array {
|
||||
$snapshot = self::snapshot($user_id);
|
||||
$latest = is_array($snapshot['latest'] ?? null) ? $snapshot['latest'] : null;
|
||||
$active = is_array($snapshot['active'] ?? null) ? $snapshot['active'] : null;
|
||||
$current = is_array($snapshot['current'] ?? null) ? $snapshot['current'] : null;
|
||||
|
||||
return [
|
||||
'user_id' => (int) ($snapshot['user_id'] ?? 0),
|
||||
'verified' => !empty($snapshot['verified']),
|
||||
'status' => (string) ($snapshot['status'] ?? ''),
|
||||
'stage' => (string) ($snapshot['stage'] ?? ''),
|
||||
'next_action' => (string) ($snapshot['next_action'] ?? 'wait'),
|
||||
'poll_interval' => (int) ($snapshot['poll_interval'] ?? 5),
|
||||
'can_apply' => !empty($snapshot['can_apply']),
|
||||
'info' => is_array($snapshot['info'] ?? null) ? $snapshot['info'] : [],
|
||||
'current' => $current ? self::application_view_model($current, (int) ($snapshot['poll_interval'] ?? 5)) : null,
|
||||
'active' => $active ? self::application_view_model($active, (int) ($snapshot['poll_interval'] ?? 5)) : null,
|
||||
'latest' => $latest ? self::application_view_model($latest, (int) ($snapshot['poll_interval'] ?? 5)) : null,
|
||||
];
|
||||
}
|
||||
|
||||
private static function next_action(string $stage): string {
|
||||
if (in_array($stage, ['provider_pending', 'provider_processing'], true)) {
|
||||
return 'poll';
|
||||
}
|
||||
if (in_array($stage, ['provider_failed', 'provider_unsupported', 'provider_expired'], true)) {
|
||||
return 'manual_or_retry';
|
||||
}
|
||||
if ($stage === 'verified') {
|
||||
return 'complete';
|
||||
}
|
||||
return 'wait';
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function info_from_application(array $application): array {
|
||||
return [
|
||||
'name' => (string) ($application['public_name'] ?? ''),
|
||||
'desc' => (string) ($application['public_desc'] ?? ''),
|
||||
'type' => (string) ($application['type'] ?? ''),
|
||||
'time' => (string) (($application['completed_at'] ?? '') ?: ($application['updated_at'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function payload_from_application(?array $application): array {
|
||||
if (!$application) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'application_uuid' => (string) ($application['application_uuid'] ?? ''),
|
||||
'user_id' => (int) ($application['user_id'] ?? 0),
|
||||
'name' => (string) ($application['public_name'] ?? ''),
|
||||
'real_name' => (string) ($application['person_name_masked'] ?? ''),
|
||||
'desc' => (string) ($application['public_desc'] ?? ''),
|
||||
'msg' => (string) ($application['manual_reason'] ?? ''),
|
||||
'type' => (string) ($application['type'] ?? ''),
|
||||
'time' => (string) ($application['created_at'] ?? ''),
|
||||
'fee' => (float) ($application['fee_amount'] ?? 0),
|
||||
'_application' => $application,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function application_view_model(array $application, int $poll_interval): array {
|
||||
$stage = (string) ($application['status'] ?? '');
|
||||
return [
|
||||
'uuid' => (string) ($application['application_uuid'] ?? ''),
|
||||
'type' => (string) ($application['type'] ?? ''),
|
||||
'status' => TransitionService::public_status($stage),
|
||||
'stage' => $stage,
|
||||
'next_action' => self::next_action($stage),
|
||||
'public_name' => (string) ($application['public_name'] ?? ''),
|
||||
'public_desc' => (string) ($application['public_desc'] ?? ''),
|
||||
'result_code' => (string) ($application['provider_result_code'] ?? ''),
|
||||
'message' => (string) ($application['provider_result_summary'] ?? ''),
|
||||
'redirect_url' => esc_url_raw((string) ($application['redirect_url'] ?? '')),
|
||||
'expires_at' => (string) ($application['expires_at'] ?? ''),
|
||||
'created_at' => (string) ($application['created_at'] ?? ''),
|
||||
'poll_interval' => $poll_interval,
|
||||
];
|
||||
}
|
||||
|
||||
public static function application_count(int $user_id, string $type): int {
|
||||
return (new ApplicationRepository())->count_user_type($user_id, sanitize_key($type));
|
||||
}
|
||||
|
||||
public static function get_status(int $user_id): string {
|
||||
$repo = new ApplicationRepository();
|
||||
$application = $repo->verified_for_user($user_id) ?: $repo->latest_for_user($user_id);
|
||||
return $application ? TransitionService::public_status((string) $application['status']) : '';
|
||||
return (string) (self::snapshot($user_id)['status'] ?? '');
|
||||
}
|
||||
|
||||
public static function is_verified(int $user_id): bool {
|
||||
return (new ApplicationRepository())->verified_for_user($user_id) !== null;
|
||||
return !empty(self::snapshot($user_id)['verified']);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function get_auth_info(int $user_id): array {
|
||||
$application = (new ApplicationRepository())->verified_for_user($user_id);
|
||||
return $application ? [
|
||||
'name' => (string) $application['public_name'],
|
||||
'desc' => (string) $application['public_desc'],
|
||||
'type' => (string) $application['type'],
|
||||
'time' => (string) ($application['completed_at'] ?: $application['updated_at']),
|
||||
] : [];
|
||||
$info = self::snapshot($user_id)['info'] ?? [];
|
||||
return is_array($info) ? $info : [];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function get_apply_payload(int $user_id): array {
|
||||
$application = (new ApplicationRepository())->latest_for_user($user_id);
|
||||
if (!$application) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'application_uuid' => (string) $application['application_uuid'],
|
||||
'user_id' => (int) $application['user_id'],
|
||||
'name' => (string) $application['public_name'],
|
||||
'real_name' => (string) $application['person_name_masked'],
|
||||
'desc' => (string) $application['public_desc'],
|
||||
'msg' => (string) ($application['manual_reason'] ?? ''),
|
||||
'type' => (string) $application['type'],
|
||||
'time' => (string) $application['created_at'],
|
||||
'fee' => (float) $application['fee_amount'],
|
||||
'_application' => $application,
|
||||
];
|
||||
$payload = self::snapshot($user_id)['apply'] ?? [];
|
||||
return is_array($payload) ? $payload : [];
|
||||
}
|
||||
|
||||
/** @return array<int,array{id:int,url:string,thumbnail_url:string}> */
|
||||
|
||||
+121
-296
@@ -17,6 +17,11 @@ class 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 = [];
|
||||
/** @var array<string,array<string,mixed>> */
|
||||
private static array $list_cache = [];
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
@@ -32,6 +37,25 @@ class Badges {
|
||||
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]);
|
||||
foreach (array_keys(self::$list_cache) as $key) {
|
||||
if (strpos($key, $user_id . ':') === 0) {
|
||||
unset(self::$list_cache[$key]);
|
||||
}
|
||||
}
|
||||
do_action('aksrt_badges_changed', $user_id);
|
||||
return;
|
||||
}
|
||||
self::$user_context_cache = [];
|
||||
self::$list_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).
|
||||
*/
|
||||
@@ -66,220 +90,8 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* REST response for the badge list modal.
|
||||
* Register the canonical JSON badge routes.
|
||||
*/
|
||||
public function rest_modal_list(\WP_REST_Request $request) {
|
||||
$user_id = absint($request->get_param('id'));
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new \WP_Error('aksrt_badge_user_missing', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
return new \WP_REST_Response(['html' => self::render_info_modal_html($user_id)], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* REST response for a single badge modal.
|
||||
*/
|
||||
public function rest_modal_single(\WP_REST_Request $request) {
|
||||
$slug = sanitize_key((string) $request->get_param('slug'));
|
||||
$user_id = absint($request->get_param('id'));
|
||||
if ($slug === '') {
|
||||
return new \WP_Error('aksrt_badge_missing', __('徽章不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
return new \WP_REST_Response(['html' => self::render_single_modal_html($slug, $user_id)], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge list modal HTML.
|
||||
*
|
||||
* Head (avatar + owner + worn medal) and category-grouped medal card grid.
|
||||
*/
|
||||
public static function render_info_modal_html(int $user_id): string {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return '<div class="medal-modal"><p class="medal-modal__empty">' . esc_html__('用户不存在', 'aksrt') . '</p></div>';
|
||||
}
|
||||
$current = get_current_user_id();
|
||||
$is_self = $current > 0 && $current === $user_id;
|
||||
$display = $is_self ? __('我', 'aksrt') : $user->display_name;
|
||||
$avatar = function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user_id, ['size' => 80])
|
||||
: get_avatar_url($user_id, ['size' => 80]);
|
||||
$data = self::list_for_user($user_id, $is_self || self::can_manually_set($current));
|
||||
$categories = is_array($data['categories'] ?? null) ? $data['categories'] : [];
|
||||
$wear = (string) ($data['wear'] ?? '');
|
||||
$map = self::get_dictionary_map();
|
||||
|
||||
// Head slot: worn medal icon with tooltip, or an empty-state tag (zibll).
|
||||
$wear_slot = '';
|
||||
if ($wear !== '' && isset($map[$wear])) {
|
||||
$wear_label = (string) ($map[$wear]['label'] ?? $wear);
|
||||
$wear_icon = (string) ($map[$wear]['icon_url'] ?? '');
|
||||
if ($wear_icon !== '') {
|
||||
$wear_slot = '<img class="medal-icon img-icon ml3" src="' . esc_url($wear_icon) . '" title="' . esc_attr(sprintf(__('当前佩戴:%s', 'aksrt'), $wear_label)) . '" alt="' . esc_attr($wear_label) . '" />';
|
||||
}
|
||||
} elseif ((int) ($data['count'] ?? 0) === 0) {
|
||||
$wear_slot = '<span class="badg badg-sm c-red ml6">' . esc_html__('暂无徽章', 'aksrt') . '</span>';
|
||||
}
|
||||
|
||||
$html = '<div class="medal-modal">';
|
||||
$html .= '<button type="button" class="aksrt-modal-runtime__close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '<div class="medal-modal__head">';
|
||||
$html .= '<img class="medal-modal__avatar" src="' . esc_url((string) $avatar) . '" alt="" width="42" height="42" />';
|
||||
$html .= '<div class="medal-modal__title"><span class="em09">' . esc_html(sprintf(__('%s的徽章', 'aksrt'), $display)) . '</span>' . $wear_slot . '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="medal-modal__scroll mini-scrollbar">';
|
||||
if (!$categories) {
|
||||
$html .= '<div class="empty-state empty-state--sm">'
|
||||
. '<div class="empty-state__icon"><i class="fas fa-award" aria-hidden="true"></i></div>'
|
||||
. '<div class="empty-state__title">' . esc_html__('暂无徽章', 'aksrt') . '</div>'
|
||||
. '<div class="empty-state__desc">' . esc_html__('积极参与社区互动即可获得徽章', 'aksrt') . '</div>'
|
||||
. '</div>';
|
||||
} else {
|
||||
foreach ($categories as $cat) {
|
||||
$html .= '<div class="medal-cat-box">';
|
||||
$html .= '<div class="medal-cat-head"><b class="medal-cat-name">' . esc_html((string) ($cat['cat_name'] ?? '')) . '</b></div>';
|
||||
$html .= '<div class="medal-list-box">';
|
||||
foreach ((array) ($cat['items'] ?? []) as $item) {
|
||||
$html .= self::render_card_link($user_id, is_array($item) ? $item : []);
|
||||
}
|
||||
$html .= '</div></div>';
|
||||
}
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
// Footer: quick jump to my own badges while browsing someone else (zibll).
|
||||
if (!$is_self && $current > 0) {
|
||||
$my_url = rest_url('aksrt/v1/users/' . $current . '/badges/modal');
|
||||
$html .= '<div class="medal-modal__buts">';
|
||||
$html .= '<a class="but c-blue" href="' . esc_url($my_url) . '" data-aksrt-modal="remote" data-aksrt-modal-url="' . esc_url($my_url) . '" data-aksrt-modal-class="full-sm" data-aksrt-modal-mobile-bottom="true"><i class="fas fa-medal" aria-hidden="true"></i> ' . esc_html__('查看我的徽章', 'aksrt') . '</a>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single medal card inside the list grid; deep-links to the detail modal.
|
||||
*
|
||||
* @param array{slug?:string,label?:string,icon_url?:string,owned?:bool,wear?:bool} $item
|
||||
*/
|
||||
protected static function render_card_link(int $user_id, array $item): string {
|
||||
$slug = (string) ($item['slug'] ?? '');
|
||||
$label = (string) ($item['label'] ?? $slug);
|
||||
$icon = (string) ($item['icon_url'] ?? '');
|
||||
$owned = !empty($item['owned']);
|
||||
$wear = !empty($item['wear']);
|
||||
if ($slug === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$icon_html = $icon !== ''
|
||||
? '<img class="medal-icon img-icon" src="' . esc_url($icon) . '" alt="' . esc_attr($label) . '" loading="lazy" decoding="async" />'
|
||||
: '<span class="medal-icon medal-icon--fa"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
|
||||
$card = '<div class="medal-card' . ($owned ? ' is-has' : ' no-has') . ($wear ? ' is-wear' : '') . '">';
|
||||
$card .= $icon_html;
|
||||
$card .= '<div class="medal-card__name">' . esc_html($label) . '</div>';
|
||||
if ($wear) {
|
||||
$card .= '<div class="medal-card__wear">' . esc_html__('佩戴中', 'aksrt') . '</div>';
|
||||
}
|
||||
$card .= '</div>';
|
||||
|
||||
$url = rest_url('aksrt/v1/users/' . $user_id . '/badges/' . rawurlencode($slug) . '/modal');
|
||||
|
||||
return '<a class="medal-list" href="' . esc_url($url) . '" data-aksrt-modal="remote" data-aksrt-modal-url="' . esc_url($url) . '" data-aksrt-modal-class="modal-mini" data-aksrt-modal-mobile-bottom="true">' . $card . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Single badge detail modal (zibll zib_get_single_medal_info_modal parity).
|
||||
*
|
||||
* medal-background.svg backdrop + large icon + name + desc + obtain time,
|
||||
* with wear action for the owner and a back link into the list modal.
|
||||
*/
|
||||
public static function render_single_modal_html(string $slug, int $user_id = 0): string {
|
||||
$map = self::get_dictionary_map();
|
||||
$slug = sanitize_key($slug);
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
return '<div class="medal-modal medal-modal--single"><p class="medal-modal__empty">' . esc_html__('徽章不存在', 'aksrt') . '</p></div>';
|
||||
}
|
||||
$item = $map[$slug];
|
||||
$current = get_current_user_id();
|
||||
$is_self = $user_id > 0 && $current === $user_id;
|
||||
$owned_map = $user_id > 0 ? self::get_owned_map($user_id) : [];
|
||||
$has = isset($owned_map[$slug]);
|
||||
$is_wear = $has && self::get_wear($user_id) === $slug;
|
||||
|
||||
$label = (string) ($item['label'] ?? $slug);
|
||||
$desc = (string) ($item['desc'] ?? '');
|
||||
$icon = (string) ($item['icon_url'] ?? '');
|
||||
$bg = get_template_directory_uri() . '/assets/img/medal/medal-background.svg';
|
||||
|
||||
$icon_html = $icon !== ''
|
||||
? '<img class="medal-icon img-icon" src="' . esc_url($icon) . '" alt="' . esc_attr($label) . '" loading="lazy" decoding="async" />'
|
||||
: '<span class="medal-icon medal-icon--fa lg"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
|
||||
// Obtain state line (self only, zibll).
|
||||
$state = '';
|
||||
if ($is_self) {
|
||||
$state = $has
|
||||
? '<div class="medal-single-state c-blue">' . esc_html(sprintf(__('%s 获得', 'aksrt'), (string) $owned_map[$slug])) . '</div>'
|
||||
: '<div class="medal-single-state c-yellow">' . esc_html__('暂未获得', 'aksrt') . '</div>';
|
||||
}
|
||||
|
||||
$html = '<div class="medal-modal medal-modal--single">';
|
||||
$html .= '<button type="button" class="aksrt-modal-runtime__close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '<div class="medal-single-card">';
|
||||
$html .= '<div class="medal-single-bg"><img class="fit-cover" src="' . esc_url($bg) . '" alt="" /></div>';
|
||||
$html .= '<div class="medal-single-main">' . $icon_html;
|
||||
$html .= '<div class="medal-single-name">' . esc_html($label) . '</div>';
|
||||
if ($desc !== '') {
|
||||
$html .= '<div class="medal-single-desc muted-2-color">' . esc_html($desc) . '</div>';
|
||||
}
|
||||
$html .= $state . '</div></div>';
|
||||
|
||||
if ($is_self && $has && !$is_wear) {
|
||||
$html .= '<div class="medal-modal__actions"><button type="button" class="but jb-blue btn-block radius" data-aksrt-badge-wear="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</button></div>';
|
||||
} elseif ($is_self && $is_wear) {
|
||||
$html .= '<div class="medal-modal__actions"><span class="badg btn-block c-blue">' . esc_html__('当前佩戴此徽章', 'aksrt') . '</span></div>';
|
||||
}
|
||||
|
||||
// Back to the full badge list modal.
|
||||
if ($user_id > 0) {
|
||||
$back_url = rest_url('aksrt/v1/users/' . $user_id . '/badges/modal');
|
||||
$html .= '<a class="medal-modal__back muted-2-color em09" href="' . esc_url($back_url) . '" data-aksrt-modal="remote" data-aksrt-modal-url="' . esc_url($back_url) . '" data-aksrt-modal-class="full-sm" data-aksrt-modal-mobile-bottom="true"><i class="fas fa-angle-left" aria-hidden="true"></i> ' . esc_html__('返回徽章列表', 'aksrt') . '</a>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declarative modal runtime trigger for a user's badge list.
|
||||
*/
|
||||
public static function get_info_link(int $user_id, string $class = '', ?string $text = null): string {
|
||||
if ($user_id <= 0 || !self::enabled()) {
|
||||
return '';
|
||||
}
|
||||
if ($text === null) {
|
||||
$text = __('徽章明细', 'aksrt');
|
||||
}
|
||||
if (!function_exists('aksrt_get_modal_link')) {
|
||||
return '';
|
||||
}
|
||||
$url = rest_url('aksrt/v1/users/' . $user_id . '/badges/modal');
|
||||
return aksrt_get_modal_link([
|
||||
'tag' => 'a',
|
||||
'class' => trim('user-medal-info ' . $class),
|
||||
'text' => $text,
|
||||
'url' => $url,
|
||||
'data_class' => 'full-sm',
|
||||
'height' => 240,
|
||||
'mobile_bottom' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges', [
|
||||
'methods' => 'GET',
|
||||
@@ -294,29 +106,14 @@ class Badges {
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges/modal', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_modal_list'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges/(?P<slug>[a-z0-9_-]+)/modal', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_modal_single'],
|
||||
'permission_callback' => '__return_true',
|
||||
register_rest_route('aksrt/v1', '/users/me/badges/wear', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_wear'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
'slug' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
@@ -325,19 +122,16 @@ class Badges {
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/me/badges/wear', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_wear'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges/grant', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_grant'],
|
||||
'permission_callback' => [$this, 'rest_can_manually_set'],
|
||||
'args' => [
|
||||
'slug' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_key',
|
||||
],
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
@@ -351,6 +145,11 @@ class Badges {
|
||||
'callback' => [$this, 'rest_revoke'],
|
||||
'permission_callback' => [$this, 'rest_can_manually_set'],
|
||||
'args' => [
|
||||
'slug' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_key',
|
||||
],
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
@@ -384,8 +183,11 @@ class Badges {
|
||||
* @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 [];
|
||||
return self::$dictionary_cache = [];
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
|
||||
$dict = is_array($settings['badge']['dictionary'] ?? null) ? $settings['badge']['dictionary'] : [];
|
||||
@@ -403,13 +205,17 @@ class Badges {
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
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;
|
||||
@@ -417,29 +223,48 @@ class Badges {
|
||||
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 {
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
$raw = get_user_meta($user_id, self::META_OWNED, true);
|
||||
if (!is_array($raw)) {
|
||||
return [];
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$out = [];
|
||||
foreach ($raw as $slug => $time) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$out[$slug] = is_string($time) ? $time : '';
|
||||
}
|
||||
return $out;
|
||||
return self::user_context($user_id)['owned_map'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -448,7 +273,7 @@ class Badges {
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function get_owned(int $user_id): array {
|
||||
return array_keys(self::get_owned_map($user_id));
|
||||
return self::user_context($user_id)['owned'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,23 +303,11 @@ class Badges {
|
||||
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 {
|
||||
if ($user_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
$wear = sanitize_key((string) get_user_meta($user_id, self::META_WEAR, true));
|
||||
if ($wear === '') {
|
||||
// zibll alias.
|
||||
$wear = sanitize_key((string) get_user_meta($user_id, 'wear_medal', true));
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
if ($wear === '') {
|
||||
// Default to newest/first owned badge when unset (zibll behavior).
|
||||
return $owned[0] ?? '';
|
||||
}
|
||||
return in_array($wear, $owned, true) ? $wear : '';
|
||||
return self::user_context($user_id)['wear'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -517,6 +330,7 @@ class Badges {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -600,13 +414,15 @@ class Badges {
|
||||
$public = self::public_display();
|
||||
$viewer = get_current_user_id();
|
||||
$is_self = $viewer > 0 && $viewer === $user_id;
|
||||
$can_see = $include_private
|
||||
|| $public
|
||||
|| $is_self
|
||||
|| self::can_manually_set($viewer);
|
||||
$is_manager = self::can_manually_set($viewer);
|
||||
$cache_key = $user_id . ':' . ($include_private ? 'private' : 'public') . ':' . $viewer . ':' . (int) $enabled . ':' . (int) $public;
|
||||
if (array_key_exists($cache_key, self::$list_cache)) {
|
||||
return self::$list_cache[$cache_key];
|
||||
}
|
||||
$can_see = $include_private || $public || $is_self || $is_manager;
|
||||
|
||||
if (!$enabled || !$can_see || $user_id <= 0) {
|
||||
return [
|
||||
return self::$list_cache[$cache_key] = [
|
||||
'enabled' => $enabled,
|
||||
'public_display' => $public,
|
||||
'is_self' => $is_self,
|
||||
@@ -623,9 +439,11 @@ class Badges {
|
||||
$medal = is_array($settings['medal'] ?? null) ? $settings['medal'] : [];
|
||||
$catalog = is_array($medal['categories'] ?? null) ? $medal['categories'] : [];
|
||||
|
||||
$owned_map = self::get_owned_map($user_id);
|
||||
$wear = self::get_wear($user_id);
|
||||
$show_unearned = $include_private || $is_self || self::can_manually_set($viewer);
|
||||
$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) {
|
||||
@@ -652,7 +470,7 @@ class Badges {
|
||||
'icon_url' => (string) ($row['icon'] ?? ''),
|
||||
'owned' => $has,
|
||||
'wear' => $has && $slug === $wear,
|
||||
'time' => $has ? (string) $owned_map[$slug] : '',
|
||||
'time' => $has && $show_private ? (string) $owned_map[$slug] : '',
|
||||
];
|
||||
// Owned first within each category (zibll is-has / no-has order).
|
||||
if ($has) {
|
||||
@@ -671,7 +489,7 @@ class Badges {
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
return self::$list_cache[$cache_key] = [
|
||||
'enabled' => true,
|
||||
'public_display' => $public,
|
||||
'is_self' => $is_self,
|
||||
@@ -690,7 +508,8 @@ class Badges {
|
||||
return new \WP_Error('aksrt_badge_user', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$data = self::list_for_user($user_id);
|
||||
return rest_ensure_response(['success' => true, 'data' => $data]);
|
||||
$response = rest_ensure_response(['success' => true, 'data' => $data]);
|
||||
return self::rest_no_store($response);
|
||||
}
|
||||
|
||||
public function rest_wear(\WP_REST_Request $request) {
|
||||
@@ -698,16 +517,16 @@ class Badges {
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? ''));
|
||||
$result = self::wear(get_current_user_id(), $slug);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
return self::rest_no_store(rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user(get_current_user_id(), true),
|
||||
'message' => __('已佩戴徽章', 'aksrt'),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
public function rest_grant(\WP_REST_Request $request) {
|
||||
@@ -719,16 +538,16 @@ class Badges {
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? ''));
|
||||
$result = self::grant($user_id, $slug, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
return self::rest_no_store(rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user($user_id, true),
|
||||
'message' => __('已授予徽章', 'aksrt'),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
public function rest_revoke(\WP_REST_Request $request) {
|
||||
@@ -740,16 +559,22 @@ class Badges {
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? ''));
|
||||
$result = self::revoke($user_id, $slug, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
return self::rest_no_store(rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user($user_id, true),
|
||||
'message' => __('已收回徽章', 'aksrt'),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
private static function rest_no_store(\WP_REST_Response $response): \WP_REST_Response {
|
||||
$response->header('Cache-Control', 'no-store, private');
|
||||
$response->header('Pragma', 'no-cache');
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -119,5 +119,6 @@ class BadgesAdmin {
|
||||
delete_user_meta($user_id, Badges::META_WEAR);
|
||||
}
|
||||
}
|
||||
Badges::invalidate_cache($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-37
@@ -164,7 +164,7 @@ class Home {
|
||||
if (!empty($settings['badge']['enabled']) && !empty($settings['badge']['public_display'])) {
|
||||
$limit = max(1, (int) ($settings['badge']['limit'] ?? 3));
|
||||
if (class_exists('AKSRT\\User\\Badges')) {
|
||||
$list = Badges::list_for_user($user_id, true);
|
||||
$list = Badges::list_for_user($user_id);
|
||||
$badge_count = 0;
|
||||
$done = false;
|
||||
foreach ((array) ($list['categories'] ?? []) as $cat) {
|
||||
@@ -191,42 +191,6 @@ class Home {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$badge_map = [];
|
||||
foreach ((array) ($settings['badge']['dictionary'] ?? []) as $item) {
|
||||
if (!empty($item['slug'])) {
|
||||
$badge_map[(string) $item['slug']] = $item;
|
||||
}
|
||||
}
|
||||
$assigned = get_user_meta($user_id, 'aksrt_user_badges', true);
|
||||
// Storage shape: associative map slug => grant time.
|
||||
$assigned = is_array($assigned) ? array_keys($assigned) : [];
|
||||
$wear = sanitize_key((string) get_user_meta($user_id, 'aksrt_wear_badge', true));
|
||||
$assigned = array_values(array_filter(array_map(static function ($slug) {
|
||||
return sanitize_key((string) $slug);
|
||||
}, (array) $assigned)));
|
||||
if ($wear !== '' && in_array($wear, $assigned, true) && isset($badge_map[$wear])) {
|
||||
$assigned = array_values(array_unique(array_merge([$wear], $assigned)));
|
||||
}
|
||||
$badge_count = 0;
|
||||
foreach ($assigned as $slug) {
|
||||
if ($slug === '' || !isset($badge_map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$signals[] = [
|
||||
'type' => 'badge',
|
||||
'slug' => $slug,
|
||||
'label' => (string) $badge_map[$slug]['label'],
|
||||
'icon' => 'award',
|
||||
'tone' => (string) ($badge_map[$slug]['tone'] ?? 'neutral'),
|
||||
'icon_url' => (string) ($badge_map[$slug]['icon_url'] ?? ''),
|
||||
'wear' => ($wear !== '' && $slug === $wear),
|
||||
];
|
||||
$badge_count++;
|
||||
if ($badge_count >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user