refactor: flatten src/AKSRT → src (drop theme-identifier dir)
Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
/**
|
||||
* Identity verification facade for existing presentation call sites.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
use AKSRT\Identity\ApplicationRepository;
|
||||
use AKSRT\Identity\EvidenceService;
|
||||
use AKSRT\Identity\EventRepository;
|
||||
use AKSRT\Identity\TransitionService;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AuthApply {
|
||||
|
||||
private static ?self $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
return self::$instance ??= new self();
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (is_admin()) {
|
||||
add_filter('manage_users_columns', [$this, 'add_admin_user_column']);
|
||||
add_filter('manage_users_custom_column', [$this, 'render_admin_user_column'], 10, 3);
|
||||
add_action('show_user_profile', [$this, 'render_admin_profile_status']);
|
||||
add_action('edit_user_profile', [$this, 'render_admin_profile_status']);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function settings(): array {
|
||||
$identity = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_user_identity_settings() : [];
|
||||
return is_array($identity['verify'] ?? null) ? $identity['verify'] : [];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function type_settings(string $type): array {
|
||||
$types = self::settings()['types'] ?? [];
|
||||
$type = sanitize_key($type);
|
||||
return is_array($types[$type] ?? null) ? $types[$type] : [];
|
||||
}
|
||||
|
||||
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']) : '';
|
||||
}
|
||||
|
||||
public static function is_verified(int $user_id): bool {
|
||||
return (new ApplicationRepository())->verified_for_user($user_id) !== null;
|
||||
}
|
||||
|
||||
/** @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']),
|
||||
] : [];
|
||||
}
|
||||
|
||||
/** @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,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int,array{id:int,url:string,thumbnail_url:string}> */
|
||||
public static function get_evidence_items(array $application, int $owner_id = 0, bool $admin_context = false): array {
|
||||
$record = is_array($application['_application'] ?? null) ? $application['_application'] : null;
|
||||
if (!$record && !empty($application['application_uuid'])) {
|
||||
$record = (new ApplicationRepository())->find_by_uuid((string) $application['application_uuid']);
|
||||
}
|
||||
return $record ? (new EvidenceService())->items($record, $admin_context) : [];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function application_view(array $application, int $owner_id = 0, bool $admin_context = false): array {
|
||||
return [
|
||||
'name' => (string) ($application['name'] ?? ''),
|
||||
'real_name' => (string) ($application['real_name'] ?? ''),
|
||||
'desc' => (string) ($application['desc'] ?? ''),
|
||||
'msg' => (string) ($application['msg'] ?? ''),
|
||||
'type' => sanitize_key((string) ($application['type'] ?? '')),
|
||||
'time' => (string) ($application['time'] ?? ''),
|
||||
'evidence' => self::get_evidence_items($application, $owner_id, $admin_context),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int,array<string,mixed>> */
|
||||
public static function get_review_history(int $user_id): array {
|
||||
$application = (new ApplicationRepository())->latest_for_user($user_id);
|
||||
if (!$application) {
|
||||
return [];
|
||||
}
|
||||
$history = [];
|
||||
foreach ((new EventRepository())->for_application((int) $application['id']) as $event) {
|
||||
$history[] = [
|
||||
'status' => TransitionService::public_status((string) ($event['to_status'] ?? '')),
|
||||
'reason' => (string) ($event['result_code'] ?? ''),
|
||||
'reviewer' => (int) ($event['actor_user_id'] ?? 0),
|
||||
'time' => (string) ($event['created_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $history;
|
||||
}
|
||||
|
||||
public static function review(int $user_id, string $status, string $note, int $reviewer_id) {
|
||||
$repo = new ApplicationRepository();
|
||||
$application = $repo->latest_for_user($user_id);
|
||||
if (!$application) {
|
||||
return new \WP_Error('aksrt_verify_missing', __('认证申请不存在', 'aksrt'));
|
||||
}
|
||||
$status = sanitize_key($status);
|
||||
$note = sanitize_textarea_field($note);
|
||||
if (!in_array($status, ['verified', 'rejected', 'cancelled'], true)) {
|
||||
return new \WP_Error('aksrt_verify_status', __('审核状态无效', 'aksrt'));
|
||||
}
|
||||
if (in_array($status, ['rejected', 'cancelled'], true) && $note === '') {
|
||||
return new \WP_Error('aksrt_verify_note', __('请填写处理原因', 'aksrt'));
|
||||
}
|
||||
$result = (new TransitionService($repo))->transition($application, $status, [
|
||||
'review_note' => $note,
|
||||
'reviewer_id' => $reviewer_id,
|
||||
], ['source' => 'admin', 'event_type' => 'manual_review', 'actor_user_id' => $reviewer_id]);
|
||||
return is_wp_error($result) ? $result : ['status' => $status, 'reviewed_at' => (string) $result['completed_at']];
|
||||
}
|
||||
|
||||
public function add_admin_user_column(array $columns): array {
|
||||
$columns['aksrt_identity_status'] = __('身份认证', 'aksrt');
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_admin_user_column(string $output, string $column_name, int $user_id): string {
|
||||
if ($column_name !== 'aksrt_identity_status') {
|
||||
return $output;
|
||||
}
|
||||
$status = self::get_status($user_id);
|
||||
$labels = ['pending' => __('进行中', 'aksrt'), 'verified' => __('已认证', 'aksrt'), 'rejected' => __('未通过', 'aksrt'), 'cancelled' => __('已取消', 'aksrt')];
|
||||
return $status === '' ? esc_html__('未认证', 'aksrt') : esc_html($labels[$status] ?? $status);
|
||||
}
|
||||
|
||||
public function render_admin_profile_status(\WP_User $user): void {
|
||||
$status = self::get_status((int) $user->ID);
|
||||
$info = self::get_auth_info((int) $user->ID);
|
||||
?>
|
||||
<h2><?php esc_html_e('身份认证(AKSRT)', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr><th><?php esc_html_e('认证状态', 'aksrt'); ?></th><td><?php echo esc_html($status ?: __('未认证', 'aksrt')); ?></td></tr>
|
||||
<?php if (!empty($info['name'])) : ?><tr><th><?php esc_html_e('认证身份', 'aksrt'); ?></th><td><?php echo esc_html((string) $info['name']); ?></td></tr><?php endif; ?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* Auto-grant medals from catalog thresholds (zibll zib_auto_add_user_medal lite).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class BadgeAutoAward {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
// Run after login / current user bootstrap.
|
||||
add_action('wp_login', [$this, 'on_login'], 20, 2);
|
||||
add_action('wp', [$this, 'maybe_for_current_user'], 30);
|
||||
// Cheap hooks for common counters.
|
||||
add_action('publish_post', [$this, 'on_content_change'], 20, 1);
|
||||
add_action('comment_post', [$this, 'on_content_change'], 20, 1);
|
||||
add_action('aksrt_user_followed', [$this, 'on_follow'], 20, 2);
|
||||
add_action('aksrt_checkin_done', [$this, 'on_checkin'], 20, 1);
|
||||
}
|
||||
|
||||
public function on_login($user_login, $user): void {
|
||||
if ($user instanceof \WP_User) {
|
||||
$this->evaluate((int) $user->ID);
|
||||
}
|
||||
}
|
||||
|
||||
public function maybe_for_current_user(): void {
|
||||
if (!is_user_logged_in() || is_admin()) {
|
||||
return;
|
||||
}
|
||||
// Throttle: once per 10 minutes per user.
|
||||
$uid = get_current_user_id();
|
||||
$key = 'aksrt_badge_auto_' . $uid;
|
||||
if (get_transient($key)) {
|
||||
return;
|
||||
}
|
||||
set_transient($key, 1, 10 * MINUTE_IN_SECONDS);
|
||||
$this->evaluate($uid);
|
||||
}
|
||||
|
||||
public function on_content_change($arg = null): void {
|
||||
$uid = get_current_user_id();
|
||||
if ($uid > 0) {
|
||||
$this->evaluate($uid);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_follow(int $follower_id, int $following_id): void {
|
||||
if ($following_id > 0) {
|
||||
$this->evaluate($following_id);
|
||||
}
|
||||
if ($follower_id > 0) {
|
||||
$this->evaluate($follower_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_checkin(int $user_id): void {
|
||||
if ($user_id > 0) {
|
||||
$this->evaluate($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function evaluate(int $user_id): void {
|
||||
if ($user_id <= 0 || !class_exists('\AKSRT\User\Badges') || !Badges::enabled()) {
|
||||
return;
|
||||
}
|
||||
Badges::auto_evaluate($user_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,740 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Badges {
|
||||
|
||||
public const META_OWNED = 'aksrt_user_badges';
|
||||
public const META_WEAR = 'aksrt_wear_badge';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
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('rest_api_init', [$this, 'register_routes']);
|
||||
add_action('aksrt_badge_granted', [$this, 'on_badge_granted_notify'], 10, 3);
|
||||
// HTML modal for remote-modal (zibll user_medal_info_modal).
|
||||
add_action('wp_ajax_aksrt_user_badge_info_modal', [$this, 'ajax_info_modal']);
|
||||
add_action('wp_ajax_nopriv_aksrt_user_badge_info_modal', [$this, 'ajax_info_modal']);
|
||||
add_action('wp_ajax_aksrt_single_badge_info_modal', [$this, 'ajax_single_info_modal']);
|
||||
add_action('wp_ajax_nopriv_aksrt_single_badge_info_modal', [$this, 'ajax_single_info_modal']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: full badge list modal HTML.
|
||||
*/
|
||||
public function ajax_info_modal(): void {
|
||||
$user_id = max(0, (int) ($_REQUEST['id'] ?? $_REQUEST['user_id'] ?? 0)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ($user_id <= 0) {
|
||||
$user_id = get_current_user_id();
|
||||
}
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
wp_send_json_error(['message' => __('用户不存在', 'aksrt')], 404);
|
||||
}
|
||||
$html = self::render_info_modal_html($user_id);
|
||||
wp_send_json_success(['html' => $html]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: single badge detail modal.
|
||||
*/
|
||||
public function ajax_single_info_modal(): void {
|
||||
$slug = sanitize_key((string) ($_REQUEST['name'] ?? $_REQUEST['slug'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$user_id = max(0, (int) ($_REQUEST['user_id'] ?? $_REQUEST['id'] ?? 0)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ($slug === '') {
|
||||
wp_send_json_error(['message' => __('徽章不存在', 'aksrt')], 404);
|
||||
}
|
||||
$html = self::render_single_modal_html($slug, $user_id);
|
||||
wp_send_json_success(['html' => $html]);
|
||||
}
|
||||
|
||||
/**
|
||||
* zibll zib_get_user_medal_info_modal lite.
|
||||
*/
|
||||
public static function render_info_modal_html(int $user_id): string {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return '<div class="aksrt-badge-modal"><p>' . 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' => 64])
|
||||
: get_avatar_url($user_id, ['size' => 64]);
|
||||
$data = self::list_for_user($user_id, $is_self || self::can_manually_set($current));
|
||||
$items = is_array($data['items'] ?? null) ? $data['items'] : [];
|
||||
$wear = (string) ($data['wear'] ?? '');
|
||||
|
||||
$html = '<div class="aksrt-badge-modal">';
|
||||
$html .= '<div class="aksrt-badge-modal__head">';
|
||||
$html .= '<img class="aksrt-badge-modal__avatar" src="' . esc_url($avatar) . '" alt="" width="40" height="40" />';
|
||||
$html .= '<div class="aksrt-badge-modal__title">' . esc_html(sprintf(__('%s的徽章', 'aksrt'), $display));
|
||||
if ($wear !== '') {
|
||||
$map = self::get_dictionary_map();
|
||||
if (isset($map[$wear])) {
|
||||
$html .= ' <span class="aksrt-badge-modal__wear-tag">' . esc_html(sprintf(__('佩戴:%s', 'aksrt'), (string) $map[$wear]['label'])) . '</span>';
|
||||
}
|
||||
} else {
|
||||
$html .= ' <span class="badg badg-sm c-red">' . esc_html__('暂无徽章', 'aksrt') . '</span>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="aksrt-badge-modal__list mini-scrollbar">';
|
||||
if (!$items) {
|
||||
$html .= '<div class="aksrt-badge-modal__empty">' . esc_html__('暂无徽章', 'aksrt') . '</div>';
|
||||
} else {
|
||||
foreach ($items as $item) {
|
||||
$slug = (string) ($item['slug'] ?? '');
|
||||
$label = (string) ($item['label'] ?? $slug);
|
||||
$icon = (string) ($item['icon_url'] ?? '');
|
||||
$is_wear = !empty($item['wear']);
|
||||
$html .= '<div class="aksrt-badge-modal__item' . ($is_wear ? ' is-wear' : '') . '" data-slug="' . esc_attr($slug) . '">';
|
||||
if ($icon !== '') {
|
||||
$html .= '<img class="medal-icon img-icon" src="' . esc_url($icon) . '" alt="" width="40" height="40" loading="lazy" />';
|
||||
} else {
|
||||
$html .= '<span class="aksrt-badge-modal__icon-fa"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
}
|
||||
$html .= '<div class="aksrt-badge-modal__meta"><div class="aksrt-badge-modal__name">' . esc_html($label) . '</div>';
|
||||
if ($is_wear) {
|
||||
$html .= '<div class="em09 c-blue">' . esc_html__('当前佩戴', 'aksrt') . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
if ($is_self && !$is_wear) {
|
||||
$html .= '<button type="button" class="but jb-blue but-sm" data-aksrt-badge-wear="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</button>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
}
|
||||
}
|
||||
$html .= '</div></div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single badge card 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="aksrt-badge-modal"><p>' . esc_html__('徽章不存在', 'aksrt') . '</p></div>';
|
||||
}
|
||||
$item = $map[$slug];
|
||||
$current = get_current_user_id();
|
||||
$is_self = $user_id > 0 && $current === $user_id;
|
||||
$owned = $user_id > 0 ? self::get_owned($user_id) : [];
|
||||
$has = in_array($slug, $owned, true);
|
||||
$wear = $user_id > 0 ? self::get_wear($user_id) : '';
|
||||
$is_wear = $has && $wear === $slug;
|
||||
|
||||
$html = '<div class="aksrt-badge-modal aksrt-badge-modal--single">';
|
||||
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '<div class="aksrt-badge-modal__card">';
|
||||
if (!empty($item['icon_url'])) {
|
||||
$html .= '<img class="medal-icon img-icon" src="' . esc_url((string) $item['icon_url']) . '" alt="" width="72" height="72" />';
|
||||
} else {
|
||||
$html .= '<span class="aksrt-badge-modal__icon-fa lg"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
}
|
||||
$html .= '<div class="aksrt-badge-modal__name">' . esc_html((string) $item['label']) . '</div>';
|
||||
if ($is_self) {
|
||||
$html .= $has
|
||||
? '<div class="c-blue em09 mt6">' . esc_html__('已获得', 'aksrt') . '</div>'
|
||||
: '<div class="c-yellow em09 mt6">' . esc_html__('暂未获得', 'aksrt') . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
if ($is_self && $has && !$is_wear) {
|
||||
$html .= '<div class="aksrt-badge-modal__actions"><button type="button" class="but jb-blue btn-block" data-aksrt-badge-wear="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</button></div>';
|
||||
} elseif ($is_self && $is_wear) {
|
||||
$html .= '<div class="aksrt-badge-modal__actions"><span class="badg btn-block c-blue">' . esc_html__('当前佩戴此徽章', 'aksrt') . '</span></div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote-modal trigger for a user's badge list (zibll zib_get_user_medal_info_link).
|
||||
*/
|
||||
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_remote_modal_link')) {
|
||||
return '';
|
||||
}
|
||||
$url = add_query_arg(
|
||||
[
|
||||
'action' => 'aksrt_user_badge_info_modal',
|
||||
'id' => $user_id,
|
||||
],
|
||||
admin_url('admin-ajax.php')
|
||||
);
|
||||
return aksrt_get_remote_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',
|
||||
'callback' => [$this, 'rest_list'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
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' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges/revoke', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_revoke'],
|
||||
'permission_callback' => [$this, 'rest_can_manually_set'],
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_can_manually_set(): bool {
|
||||
return self::can_manually_set(get_current_user_id());
|
||||
}
|
||||
|
||||
public static function enabled(): bool {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return false;
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
|
||||
return !empty($settings['badge']['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['badge']['enabled']) && !empty($settings['badge']['public_display']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary(): array {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return [];
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
|
||||
$dict = is_array($settings['badge']['dictionary'] ?? null) ? $settings['badge']['dictionary'] : [];
|
||||
$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'),
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary_map(): array {
|
||||
$map = [];
|
||||
foreach (self::get_dictionary() as $item) {
|
||||
$map[$item['slug']] = $item;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function get_owned(int $user_id): array {
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
$raw = get_user_meta($user_id, self::META_OWNED, true);
|
||||
if (!is_array($raw)) {
|
||||
$raw = preg_split('/[\s,]+/', (string) $raw, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$out = [];
|
||||
foreach ((array) $raw as $slug) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$out[] = $slug;
|
||||
}
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
$clean = [];
|
||||
foreach ($slugs as $slug) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug !== '' && isset($map[$slug])) {
|
||||
$clean[] = $slug;
|
||||
}
|
||||
}
|
||||
$clean = array_values(array_unique($clean));
|
||||
update_user_meta($user_id, self::META_OWNED, $clean);
|
||||
|
||||
$wear = self::get_wear($user_id);
|
||||
if ($wear !== '' && !in_array($wear, $clean, true)) {
|
||||
delete_user_meta($user_id, self::META_WEAR);
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
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;
|
||||
}
|
||||
$owned[] = $slug;
|
||||
self::set_owned($user_id, $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;
|
||||
}
|
||||
if (class_exists('\AKSRT\Settings\Capabilities')) {
|
||||
return \AKSRT\Settings\Capabilities::user_can('medal_manually_set', $actor);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public DTO for REST / modals.
|
||||
*
|
||||
* @return array{enabled:bool,public_display:bool,wear:string,items:list<array>}
|
||||
*/
|
||||
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();
|
||||
$can_see = $include_private
|
||||
|| $public
|
||||
|| ($viewer > 0 && $viewer === $user_id)
|
||||
|| self::can_manually_set($viewer);
|
||||
|
||||
if (!$enabled || !$can_see || $user_id <= 0) {
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'public_display' => $public,
|
||||
'wear' => '',
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$map = self::get_dictionary_map();
|
||||
$owned = self::get_owned($user_id);
|
||||
$wear = self::get_wear($user_id);
|
||||
// Prefer worn first (Zibll).
|
||||
if ($wear !== '') {
|
||||
$owned = array_values(array_unique(array_merge([$wear], $owned)));
|
||||
}
|
||||
$items = [];
|
||||
foreach ($owned as $slug) {
|
||||
if (!isset($map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'slug' => $slug,
|
||||
'label' => $map[$slug]['label'],
|
||||
'tone' => $map[$slug]['tone'],
|
||||
'icon_url' => $map[$slug]['icon_url'],
|
||||
'wear' => ($slug === $wear),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'public_display' => $public,
|
||||
'wear' => $wear,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
public function rest_list(\WP_REST_Request $request) {
|
||||
$user_id = (int) $request['id'];
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
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]);
|
||||
}
|
||||
|
||||
public function rest_wear(\WP_REST_Request $request) {
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$result = self::wear(get_current_user_id(), $slug);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return 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) {
|
||||
$user_id = (int) $request['id'];
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new \WP_Error('aksrt_badge_user', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$result = self::grant($user_id, $slug, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user($user_id, true),
|
||||
'message' => __('已授予徽章', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_revoke(\WP_REST_Request $request) {
|
||||
$user_id = (int) $request['id'];
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new \WP_Error('aksrt_badge_user', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$result = self::revoke($user_id, $slug, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user($user_id, true),
|
||||
'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\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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class BadgesAdmin {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('show_user_profile', [$this, 'render_section']);
|
||||
add_action('edit_user_profile', [$this, 'render_section']);
|
||||
add_action('personal_options_update', [$this, 'handle_update']);
|
||||
add_action('edit_user_profile_update', [$this, 'handle_update']);
|
||||
}
|
||||
|
||||
private function can_manage(): bool {
|
||||
return class_exists('AKSRT\\User\\Badges') && Badges::can_manually_set(get_current_user_id());
|
||||
}
|
||||
|
||||
public function render_section(\WP_User $user): void {
|
||||
if (!class_exists('AKSRT\\User\\Badges') || !Badges::enabled()) {
|
||||
return;
|
||||
}
|
||||
$dict = Badges::get_dictionary();
|
||||
if (!$dict) {
|
||||
return;
|
||||
}
|
||||
$owned = Badges::get_owned((int) $user->ID);
|
||||
$wear = Badges::get_wear((int) $user->ID);
|
||||
$can = $this->can_manage();
|
||||
?>
|
||||
<a id="aksrt-user-badges"></a>
|
||||
<h2><?php esc_html_e('AKSRT 徽章', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th><label><?php esc_html_e('已授予徽章', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<?php if (!$can) : ?>
|
||||
<p class="description"><?php esc_html_e('当前账号无权授予/收回徽章。', 'aksrt'); ?></p>
|
||||
<?php endif; ?>
|
||||
<fieldset <?php disabled(!$can); ?>>
|
||||
<?php foreach ($dict as $item) : ?>
|
||||
<?php
|
||||
$slug = $item['slug'];
|
||||
$checked = in_array($slug, $owned, true);
|
||||
?>
|
||||
<label style="display:inline-flex;align-items:center;gap:6px;margin:0 16px 10px 0;">
|
||||
<input type="checkbox" name="aksrt_user_badges[]" value="<?php echo esc_attr($slug); ?>" <?php checked($checked); ?> <?php disabled(!$can); ?>>
|
||||
<?php if (!empty($item['icon_url'])) : ?>
|
||||
<img src="<?php echo esc_url($item['icon_url']); ?>" alt="" width="18" height="18" style="object-fit:contain;vertical-align:middle;">
|
||||
<?php endif; ?>
|
||||
<span><?php echo esc_html($item['label']); ?> <code><?php echo esc_html($slug); ?></code></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<p class="description"><?php esc_html_e('勾选即授予;取消勾选并保存即收回。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="aksrt_wear_badge"><?php esc_html_e('当前佩戴', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<select name="aksrt_wear_badge" id="aksrt_wear_badge" <?php disabled(!$can); ?>>
|
||||
<option value=""><?php esc_html_e('(未佩戴)', 'aksrt'); ?></option>
|
||||
<?php foreach ($dict as $item) : ?>
|
||||
<?php if (!in_array($item['slug'], $owned, true) && $item['slug'] !== $wear) : ?>
|
||||
<?php continue; ?>
|
||||
<?php endif; ?>
|
||||
<option value="<?php echo esc_attr($item['slug']); ?>" <?php selected($wear, $item['slug']); ?>>
|
||||
<?php echo esc_html($item['label']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e('仅可佩戴已授予徽章;用户也可在前台自行佩戴。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
wp_nonce_field('aksrt_user_badges_admin', 'aksrt_user_badges_nonce');
|
||||
}
|
||||
|
||||
public function handle_update(int $user_id): void {
|
||||
if (!$this->can_manage()) {
|
||||
return;
|
||||
}
|
||||
if (!isset($_POST['aksrt_user_badges_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_user_badges_nonce'])), 'aksrt_user_badges_admin')) {
|
||||
return;
|
||||
}
|
||||
if (!class_exists('AKSRT\\User\\Badges') || !Badges::enabled()) {
|
||||
return;
|
||||
}
|
||||
$raw = isset($_POST['aksrt_user_badges']) ? (array) wp_unslash($_POST['aksrt_user_badges']) : [];
|
||||
$slugs = array_map('sanitize_key', $raw);
|
||||
Badges::set_owned($user_id, $slugs);
|
||||
|
||||
$wear = isset($_POST['aksrt_wear_badge']) ? sanitize_key((string) wp_unslash($_POST['aksrt_wear_badge'])) : '';
|
||||
if ($wear === '') {
|
||||
delete_user_meta($user_id, Badges::META_WEAR);
|
||||
} else {
|
||||
$res = Badges::wear($user_id, $wear);
|
||||
if (is_wp_error($res)) {
|
||||
// leave owned as-is; wear ignored if invalid
|
||||
delete_user_meta($user_id, Badges::META_WEAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class BalanceAdmin {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_filter('manage_users_columns', [$this, 'add_column']);
|
||||
add_filter('manage_users_custom_column', [$this, 'render_column'], 10, 3);
|
||||
add_filter('manage_users_sortable_columns', [$this, 'sortable']);
|
||||
add_action('pre_get_users', [$this, 'sort_users']);
|
||||
add_action('show_user_profile', [$this, 'render_section']);
|
||||
add_action('edit_user_profile', [$this, 'render_section']);
|
||||
add_action('personal_options_update', [$this, 'handle_update']);
|
||||
add_action('edit_user_profile_update', [$this, 'handle_update']);
|
||||
}
|
||||
|
||||
private function can_manage(): bool {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
|
||||
public function add_column(array $columns): array {
|
||||
if (!class_exists('\AKSRT\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) {
|
||||
return $columns;
|
||||
}
|
||||
$columns['aksrt_wallet_balance'] = __('现金余额', 'aksrt');
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_column(string $output, string $column_name, int $user_id): string {
|
||||
if ($column_name !== 'aksrt_wallet_balance') {
|
||||
return $output;
|
||||
}
|
||||
if (!class_exists('\AKSRT\Credits\Balance')) {
|
||||
return '—';
|
||||
}
|
||||
return '<strong>' . esc_html(\AKSRT\Credits\Balance::format(\AKSRT\Credits\Balance::balance($user_id))) . '</strong>';
|
||||
}
|
||||
|
||||
public function sortable(array $columns): array {
|
||||
$columns['aksrt_wallet_balance'] = 'aksrt_wallet_balance';
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function sort_users($query): void {
|
||||
if (!is_admin() || $query->get('orderby') !== 'aksrt_wallet_balance') {
|
||||
return;
|
||||
}
|
||||
$query->set('meta_key', 'aksrt_balance');
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
}
|
||||
|
||||
public function render_section(\WP_User $user): void {
|
||||
if (!class_exists('\AKSRT\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) {
|
||||
return;
|
||||
}
|
||||
$balance = \AKSRT\Credits\Balance::balance((int) $user->ID);
|
||||
$can = $this->can_manage();
|
||||
$log = \AKSRT\Credits\Balance::get_log(['user_id' => (int) $user->ID, 'page' => 1, 'per_page' => 8]);
|
||||
?>
|
||||
<a id="aksrt-user-wallet"></a>
|
||||
<h2><?php esc_html_e('现金钱包(AKSRT)', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th><?php esc_html_e('当前余额', 'aksrt'); ?></th>
|
||||
<td><strong style="font-size:18px;"><?php echo esc_html(\AKSRT\Credits\Balance::format($balance)); ?></strong> <?php esc_html_e('元', 'aksrt'); ?></td>
|
||||
</tr>
|
||||
<?php if ($can) : ?>
|
||||
<tr>
|
||||
<th><?php esc_html_e('调整余额', 'aksrt'); ?></th>
|
||||
<td>
|
||||
<?php wp_nonce_field('aksrt_user_balance_adjust_' . (int) $user->ID, '_aksrt_balance_nonce'); ?>
|
||||
<label for="aksrt_balance_delta"><?php esc_html_e('变动金额(正数=增加,负数=扣减)', 'aksrt'); ?></label><br>
|
||||
<input type="number" step="0.01" name="aksrt_balance_delta" id="aksrt_balance_delta" value="" class="regular-text" />
|
||||
<p>
|
||||
<label for="aksrt_balance_reason"><?php esc_html_e('备注', 'aksrt'); ?></label><br>
|
||||
<input type="text" name="aksrt_balance_reason" id="aksrt_balance_reason" class="regular-text" />
|
||||
</p>
|
||||
<p class="description"><?php esc_html_e('保存用户资料时生效,并写入余额流水。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<th><?php esc_html_e('近期流水', 'aksrt'); ?></th>
|
||||
<td>
|
||||
<?php if (!empty($log['items'])) : ?>
|
||||
<table class="widefat striped" style="max-width:640px;">
|
||||
<thead><tr><th><?php esc_html_e('时间', 'aksrt'); ?></th><th><?php esc_html_e('变动', 'aksrt'); ?></th><th><?php esc_html_e('余额', 'aksrt'); ?></th><th><?php esc_html_e('原因', 'aksrt'); ?></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($log['items'] as $row) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html(get_date_from_gmt((string) ($row['created_at'] ?? ''), 'Y-m-d H:i')); ?></td>
|
||||
<td><?php echo esc_html(($row['delta'] > 0 ? '+' : '') . \AKSRT\Credits\Balance::format((float) $row['delta'])); ?></td>
|
||||
<td><?php echo esc_html(\AKSRT\Credits\Balance::format((float) ($row['balance_after'] ?? 0))); ?></td>
|
||||
<td><?php echo esc_html(\AKSRT\Credits\Balance::reason_label((string) ($row['reason'] ?? ''))); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else : ?>
|
||||
<p><?php esc_html_e('暂无流水', 'aksrt'); ?></p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function handle_update(int $user_id): void {
|
||||
if (!$this->can_manage() || !class_exists('\AKSRT\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) {
|
||||
return;
|
||||
}
|
||||
if (empty($_POST['_aksrt_balance_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['_aksrt_balance_nonce'])), 'aksrt_user_balance_adjust_' . $user_id)) {
|
||||
return;
|
||||
}
|
||||
if (!isset($_POST['aksrt_balance_delta']) || $_POST['aksrt_balance_delta'] === '') {
|
||||
return;
|
||||
}
|
||||
$delta = round((float) wp_unslash($_POST['aksrt_balance_delta']), 2);
|
||||
if (abs($delta) < 0.0001) {
|
||||
return;
|
||||
}
|
||||
$reason = isset($_POST['aksrt_balance_reason'])
|
||||
? sanitize_text_field(wp_unslash((string) $_POST['aksrt_balance_reason']))
|
||||
: __('管理员调整', 'aksrt');
|
||||
if ($delta > 0) {
|
||||
$result = \AKSRT\Credits\Balance::add($user_id, $delta, 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
|
||||
} else {
|
||||
$result = \AKSRT\Credits\Balance::deduct($user_id, abs($delta), 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
|
||||
}
|
||||
if (is_wp_error($result)) {
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'error',
|
||||
'msg' => $result->get_error_message(),
|
||||
]], 30);
|
||||
return;
|
||||
}
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'success',
|
||||
'msg' => __('现金余额已更新', 'aksrt'),
|
||||
]], 30);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/**
|
||||
* Frontend ban appeal (zibll user_ban_appeal parity) on Restriction meta.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class BanAppeal {
|
||||
|
||||
public const META_STATUS = 'aksrt_ban_appeal_status'; // pending|approved|rejected|''
|
||||
public const META_PAYLOAD = 'aksrt_ban_appeal_payload';
|
||||
public const META_HISTORY = 'aksrt_ban_appeal_history';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
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_ban_appeal_submit', [$this, 'ajax_submit']);
|
||||
add_action('wp_ajax_aksrt_ban_appeal_status', [$this, 'ajax_status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled:bool,desc:string,keys:string[]}
|
||||
*/
|
||||
public static function settings(): array {
|
||||
$all = class_exists('\AKSRT\Settings\Settings') ? (array) \AKSRT\Settings\Settings::get_all() : [];
|
||||
$keys_raw = (string) ($all['ban_appeal_keys'] ?? '');
|
||||
$keys = array_values(array_filter(array_map('trim', preg_split('/[,,\n]/', $keys_raw) ?: [])));
|
||||
return [
|
||||
'enabled' => !isset($all['user_report_enabled']) || !empty($all['user_report_enabled']),
|
||||
'desc' => (string) ($all['ban_appeal_desc'] ?? ''),
|
||||
'keys' => $keys,
|
||||
];
|
||||
}
|
||||
|
||||
public static function get_status(int $user_id): string {
|
||||
return sanitize_key((string) get_user_meta($user_id, self::META_STATUS, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function get_payload(int $user_id): array {
|
||||
$p = get_user_meta($user_id, self::META_PAYLOAD, true);
|
||||
return is_array($p) ? $p : [];
|
||||
}
|
||||
|
||||
public function ajax_status(): void {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json(['error' => 1, 'msg' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
$ban = class_exists('\AKSRT\User\Restriction')
|
||||
? \AKSRT\User\Restriction::get_instance()->get_active_ban($user_id)
|
||||
: null;
|
||||
wp_send_json([
|
||||
'error' => 0,
|
||||
'banned' => (bool) $ban,
|
||||
'ban' => $ban,
|
||||
'status' => self::get_status($user_id),
|
||||
'payload'=> self::get_payload($user_id),
|
||||
]);
|
||||
}
|
||||
|
||||
public function ajax_submit(): void {
|
||||
if (!is_user_logged_in()) {
|
||||
wp_send_json(['error' => 1, 'msg' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
if (!check_ajax_referer('aksrt_ban_appeal', '_wpnonce', false)) {
|
||||
wp_send_json(['error' => 1, 'msg' => __('安全校验失败', 'aksrt')]);
|
||||
}
|
||||
$user_id = get_current_user_id();
|
||||
$s = self::settings();
|
||||
if (!$s['enabled']) {
|
||||
wp_send_json(['error' => 1, 'msg' => __('申诉功能未开启', 'aksrt')]);
|
||||
}
|
||||
|
||||
$banned = class_exists('\AKSRT\User\Restriction')
|
||||
&& \AKSRT\User\Restriction::get_instance()->is_banned($user_id);
|
||||
if (!$banned) {
|
||||
wp_send_json(['error' => 1, 'msg' => __('当前账号未处于禁封状态', 'aksrt')]);
|
||||
}
|
||||
if (self::get_status($user_id) === 'pending') {
|
||||
wp_send_json(['error' => 1, 'msg' => __('申诉正在处理中,请勿重复提交', 'aksrt')]);
|
||||
}
|
||||
|
||||
$reason_key = isset($_POST['reason_key']) ? sanitize_text_field(wp_unslash((string) $_POST['reason_key'])) : '';
|
||||
$reason = isset($_POST['reason']) ? sanitize_textarea_field(wp_unslash((string) $_POST['reason'])) : '';
|
||||
if ($reason === '') {
|
||||
wp_send_json(['error' => 1, 'msg' => __('请填写申诉说明', 'aksrt')]);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'reason_key' => $reason_key,
|
||||
'reason' => $reason,
|
||||
'time' => current_time('Y-m-d H:i'),
|
||||
];
|
||||
update_user_meta($user_id, self::META_PAYLOAD, $payload);
|
||||
update_user_meta($user_id, self::META_STATUS, 'pending');
|
||||
|
||||
$history = get_user_meta($user_id, self::META_HISTORY, true);
|
||||
$history = is_array($history) ? $history : [];
|
||||
$history[] = $payload;
|
||||
update_user_meta($user_id, self::META_HISTORY, array_slice($history, -20));
|
||||
|
||||
// Notify admins via site message table if available.
|
||||
if (class_exists('\AKSRT\Messages\Message')) {
|
||||
$admins = get_users(['role' => 'administrator', 'fields' => 'ID', 'number' => 20]);
|
||||
$summary = sprintf(
|
||||
/* translators: 1: user login 2: reason */
|
||||
__('用户 %1$s 提交禁封申诉:%2$s', 'aksrt'),
|
||||
wp_get_current_user()->user_login,
|
||||
mb_substr($reason, 0, 80)
|
||||
);
|
||||
foreach ($admins as $admin_id) {
|
||||
\AKSRT\Messages\Message::get_instance()->send(
|
||||
(int) $admin_id,
|
||||
'ban_appeal',
|
||||
$summary,
|
||||
[
|
||||
'title' => __('禁封申诉', 'aksrt'),
|
||||
'target_url' => admin_url('users.php?page=aksrt_user_ban&tab=appeals'),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param array $payload
|
||||
*/
|
||||
do_action('aksrt_ban_appeal_submitted', $user_id, $payload);
|
||||
|
||||
wp_send_json([
|
||||
'error' => 0,
|
||||
'msg' => __('申诉已提交,请等待管理员处理', 'aksrt'),
|
||||
'reload' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Center {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** @var array 面板注册表 */
|
||||
private $panels = [];
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
$this->register_default_panels();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册默认面板
|
||||
*/
|
||||
private function register_default_panels(): void {
|
||||
$settings = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_user_center_settings()
|
||||
: [
|
||||
'tabs' => ['profile', 'posts', 'comments', 'favorites'],
|
||||
'avatar_upload' => true,
|
||||
'cover_upload' => true,
|
||||
'social_links' => true,
|
||||
];
|
||||
|
||||
$definitions = [
|
||||
'profile' => [
|
||||
'label' => __('个人信息', 'aksrt'),
|
||||
'icon' => 'user',
|
||||
'priority' => 10,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'level' => [
|
||||
'label' => __('我的等级', 'aksrt'),
|
||||
'icon' => 'chart-line',
|
||||
'priority' => 15,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'posts' => [
|
||||
'label' => __('我的文章', 'aksrt'),
|
||||
'icon' => 'file-alt',
|
||||
'priority' => 20,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'comments' => [
|
||||
'label' => __('我的评论', 'aksrt'),
|
||||
'icon' => 'comment',
|
||||
'priority' => 30,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'favorites' => [
|
||||
'label' => __('我的收藏', 'aksrt'),
|
||||
'icon' => 'star',
|
||||
'priority' => 40,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'vip' => [
|
||||
'label' => __('我的会员', 'aksrt'),
|
||||
'icon' => 'crown',
|
||||
'priority' => 45,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'orders' => [
|
||||
'label' => __('我的订单', 'aksrt'),
|
||||
'icon' => 'receipt',
|
||||
'priority' => 48,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'credits' => [
|
||||
'label' => class_exists('\AKSRT\Settings\Settings')
|
||||
? sprintf(__('我的%s', 'aksrt'), \AKSRT\Settings\Settings::get_currency_name())
|
||||
: __('我的积分', 'aksrt'),
|
||||
'icon' => 'wallet',
|
||||
'priority' => 50,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'notif' => [
|
||||
'label' => __('消息通知', 'aksrt'),
|
||||
'icon' => 'bell',
|
||||
'priority' => 60,
|
||||
'type' => 'link',
|
||||
],
|
||||
'auth' => [
|
||||
'label' => __('官方认证', 'aksrt'),
|
||||
'icon' => 'id-badge',
|
||||
'priority' => 65,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'settings' => [
|
||||
'label' => __('账号设置', 'aksrt'),
|
||||
'icon' => 'cog',
|
||||
'priority' => 70,
|
||||
'type' => 'panel',
|
||||
],
|
||||
];
|
||||
|
||||
$this->panels = [];
|
||||
$configured = array_values(array_filter(array_map('sanitize_key', (array) ($settings['tabs'] ?? []))));
|
||||
if ($configured === []) {
|
||||
$configured = ['profile', 'posts', 'comments', 'favorites'];
|
||||
}
|
||||
|
||||
$level_on = class_exists('\AKSRT\User\Level')
|
||||
&& !empty(\AKSRT\User\Level::settings()['enabled']);
|
||||
$credits_on = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled();
|
||||
$balance_on = class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled();
|
||||
$vip_on = class_exists('\AKSRT\Settings\Settings')
|
||||
&& !empty(\AKSRT\Settings\Settings::get_payment_vip_settings()['enabled']);
|
||||
|
||||
// Follow configured order; always auto-include enabled surfaces.
|
||||
$order = $configured;
|
||||
|
||||
// Growth level is a dedicated user-center surface, separate from spendable points.
|
||||
if ($level_on && !in_array('level', $order, true)) {
|
||||
$insert_at = array_search('profile', $order, true);
|
||||
array_splice($order, $insert_at === false ? 0 : (int) $insert_at + 1, 0, ['level']);
|
||||
}
|
||||
if (!$level_on) {
|
||||
$order = array_values(array_filter($order, static fn(string $key): bool => $key !== 'level'));
|
||||
}
|
||||
|
||||
if ($vip_on && !in_array('vip', $order, true)) {
|
||||
$insert_at = count($order);
|
||||
foreach (['credits', 'favorites', 'posts'] as $anchor) {
|
||||
$idx = array_search($anchor, $order, true);
|
||||
if ($idx !== false) {
|
||||
$insert_at = (int) $idx + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
array_splice($order, $insert_at, 0, ['vip']);
|
||||
}
|
||||
// Orders panel: always available when payment stack exists (UserOrders or Order).
|
||||
$orders_on = class_exists('\AKSRT\Payment\UserOrders')
|
||||
|| class_exists('\AKSRT\Payment\Order');
|
||||
if ($orders_on && !in_array('orders', $order, true)) {
|
||||
$insert_at = count($order);
|
||||
foreach (['vip', 'credits', 'favorites'] as $anchor) {
|
||||
$idx = array_search($anchor, $order, true);
|
||||
if ($idx !== false) {
|
||||
$insert_at = (int) $idx + ($anchor === 'vip' ? 1 : 0);
|
||||
if ($anchor !== 'vip') {
|
||||
$insert_at = (int) $idx + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
array_splice($order, $insert_at, 0, ['orders']);
|
||||
}
|
||||
if (($credits_on || $balance_on) && !in_array('credits', $order, true)) {
|
||||
$order[] = 'credits';
|
||||
}
|
||||
$auth_on = class_exists('\AKSRT\User\AuthApply')
|
||||
&& !empty(\AKSRT\User\AuthApply::settings()['enabled']);
|
||||
if ($auth_on && !in_array('auth', $order, true)) {
|
||||
$order[] = 'auth';
|
||||
}
|
||||
if (!in_array('settings', $order, true)) {
|
||||
$order[] = 'settings';
|
||||
}
|
||||
|
||||
foreach ($order as $key) {
|
||||
if (!isset($definitions[$key])) {
|
||||
continue;
|
||||
}
|
||||
if ($key === 'level' && !$level_on) {
|
||||
continue;
|
||||
}
|
||||
if ($key === 'credits' && !$credits_on && !$balance_on) {
|
||||
continue;
|
||||
}
|
||||
if ($key === 'vip' && !$vip_on) {
|
||||
continue;
|
||||
}
|
||||
if ($key === 'orders' && !$orders_on) {
|
||||
continue;
|
||||
}
|
||||
if ($key === 'auth' && !$auth_on) {
|
||||
continue;
|
||||
}
|
||||
$panel = $definitions[$key];
|
||||
if ($key === 'credits' && $balance_on) {
|
||||
$panel['label'] = __('我的资产', 'aksrt');
|
||||
$panel['icon'] = 'wallet';
|
||||
}
|
||||
$this->panels[$key] = $panel;
|
||||
}
|
||||
|
||||
// Notif is a link entry (messages page), not a dashboard panel body.
|
||||
$this->panels['notif'] = $definitions['notif'];
|
||||
|
||||
// Keep stable order by priority.
|
||||
uasort($this->panels, static function (array $a, array $b): int {
|
||||
return ((int) ($a['priority'] ?? 100)) <=> ((int) ($b['priority'] ?? 100));
|
||||
});
|
||||
|
||||
/**
|
||||
* 允许插件/子主题注册额外面板
|
||||
*
|
||||
* @param array $panels 面板注册表
|
||||
*/
|
||||
$this->panels = apply_filters('aksrt_user_center_panels', $this->panels);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取面板注册表
|
||||
*/
|
||||
public function get_panels(): array {
|
||||
return $this->panels;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可切换的本地面板(不含外链入口)。
|
||||
*/
|
||||
public function get_dashboard_tabs(): array {
|
||||
return array_filter($this->panels, static function (array $panel): bool {
|
||||
return ($panel['type'] ?? 'panel') === 'panel';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前页面是否为本人中心
|
||||
*/
|
||||
public static function is_page(): bool {
|
||||
return function_exists('aksrt_is_user_center_page') && aksrt_is_user_center_page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class CreditsAdmin {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
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_filter('manage_users_columns', [$this, 'add_balance_column']);
|
||||
add_filter('manage_users_custom_column', [$this, 'render_balance_column'], 10, 3);
|
||||
add_filter('manage_users_sortable_columns', [$this, 'make_balance_sortable']);
|
||||
add_action('pre_get_users', [$this, 'sort_users_by_credits']);
|
||||
add_filter('user_row_actions', [$this, 'add_row_actions'], 30, 2);
|
||||
|
||||
add_action('show_user_profile', [$this, 'render_user_credits_section']);
|
||||
add_action('edit_user_profile', [$this, 'render_user_credits_section']);
|
||||
add_action('personal_options_update', [$this, 'handle_user_credits_update']);
|
||||
add_action('edit_user_profile_update', [$this, 'handle_user_credits_update']);
|
||||
}
|
||||
|
||||
private function can_manage_credits(): bool {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
|
||||
// ========== 用户列表:积分余额列 ==========
|
||||
|
||||
public function add_balance_column(array $columns): array {
|
||||
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
$columns['aksrt_credits_balance'] = sprintf(__('%s余额', 'aksrt'), $currency_name);
|
||||
$columns['aksrt_user_commerce'] = __('资产/分成', 'aksrt');
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_balance_column(string $output, string $column_name, int $user_id): string {
|
||||
if ($column_name === 'aksrt_user_commerce') {
|
||||
return $this->render_commerce_column($user_id);
|
||||
}
|
||||
if ($column_name !== 'aksrt_credits_balance') {
|
||||
return $output;
|
||||
}
|
||||
if (!class_exists('\AKSRT\Credits\Credits')) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
$balance = \AKSRT\Credits\Credits::balance($user_id);
|
||||
return '<strong>' . esc_html((string) $balance) . '</strong>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact commerce snapshot: VIP, revenue, rebate, download logs link.
|
||||
*/
|
||||
private function render_commerce_column(int $user_id): string {
|
||||
$bits = [];
|
||||
if (function_exists('aksrt_get_user_vip_level')) {
|
||||
$lv = (int) aksrt_get_user_vip_level($user_id);
|
||||
if ($lv > 0) {
|
||||
$bits[] = '<span class="aksrt-vip-badge">VIP' . esc_html((string) $lv) . '</span>';
|
||||
}
|
||||
}
|
||||
if (class_exists('\AKSRT\Payment\Revenue')) {
|
||||
$stats = \AKSRT\Payment\Revenue::get_instance()->get_stats($user_id);
|
||||
$pending = (float) ($stats['pending'] ?? 0);
|
||||
$total = (float) ($stats['total'] ?? 0);
|
||||
if ($total > 0 || $pending > 0) {
|
||||
$bits[] = sprintf(
|
||||
/* translators: 1 total 2 pending */
|
||||
esc_html__('分成 ¥%1$s(待结 ¥%2$s)', 'aksrt'),
|
||||
number_format($total, 2),
|
||||
number_format($pending, 2)
|
||||
);
|
||||
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_income&user_id=' . $user_id)) . '">' . esc_html__('分成明细', 'aksrt') . '</a>';
|
||||
}
|
||||
}
|
||||
if (class_exists('\AKSRT\Payment\Rebate')) {
|
||||
$list = \AKSRT\Payment\Rebate::get_instance()->get_list(['user_id' => $user_id, 'limit' => 1]);
|
||||
$total_r = (int) ($list['total'] ?? 0);
|
||||
if ($total_r > 0) {
|
||||
$bits[] = sprintf(esc_html__('返佣 %d 笔', 'aksrt'), $total_r);
|
||||
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_rebate&user_id=' . $user_id)) . '">' . esc_html__('返佣', 'aksrt') . '</a>';
|
||||
}
|
||||
}
|
||||
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_download_logs&user_id=' . $user_id)) . '">' . esc_html__('下载记录', 'aksrt') . '</a>';
|
||||
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_orders&user_id=' . $user_id)) . '">' . esc_html__('订单', 'aksrt') . '</a>';
|
||||
|
||||
if (!$bits) {
|
||||
return '—';
|
||||
}
|
||||
return '<div style="font-size:12px;line-height:1.5;">' . implode('<br>', $bits) . '</div>';
|
||||
}
|
||||
|
||||
public function make_balance_sortable(array $columns): array {
|
||||
$columns['aksrt_credits_balance'] = 'aksrt_credits_balance';
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function sort_users_by_credits($query): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
if ($query->get('orderby') !== 'aksrt_credits_balance') {
|
||||
return;
|
||||
}
|
||||
$query->set('meta_key', 'aksrt_credits');
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
}
|
||||
|
||||
// ========== 用户列表:行操作 ==========
|
||||
|
||||
public function add_row_actions(array $actions, \WP_User $user): array {
|
||||
if (!$this->can_manage_credits()) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$edit_url = get_edit_user_link($user->ID);
|
||||
if ($edit_url) {
|
||||
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
$actions['aksrt_edit_credits'] = sprintf(
|
||||
'<a href="%s#aksrt-user-credits">%s</a>',
|
||||
esc_url($edit_url),
|
||||
esc_html(sprintf(__('调整%s', 'aksrt'), $currency_name))
|
||||
);
|
||||
}
|
||||
$actions['aksrt_user_dl_logs'] = sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url(admin_url('admin.php?page=aksrt_download_logs&user_id=' . (int) $user->ID)),
|
||||
esc_html__('下载记录', 'aksrt')
|
||||
);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
// ========== 用户编辑页:积分管理区块 ==========
|
||||
|
||||
public function render_user_credits_section(\WP_User $user): void {
|
||||
if (!class_exists('\AKSRT\Credits\Credits')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$balance = \AKSRT\Credits\Credits::balance((int) $user->ID);
|
||||
$can_manage = $this->can_manage_credits();
|
||||
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
?>
|
||||
<a id="aksrt-user-credits"></a>
|
||||
<h2><?php echo esc_html(sprintf(__('%s管理(AKSRT)', 'aksrt'), $currency_name)); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th><?php esc_html_e('当前余额', 'aksrt'); ?></th>
|
||||
<td>
|
||||
<strong style="font-size:18px;"><?php echo esc_html((string) $balance); ?></strong>
|
||||
<span style="color:#666;margin-left:6px;"><?php echo esc_html($currency_name); ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php if ($can_manage) : ?>
|
||||
<tr>
|
||||
<th><?php echo esc_html(sprintf(__('调整%s', 'aksrt'), $currency_name)); ?></th>
|
||||
<td>
|
||||
<?php wp_nonce_field('aksrt_user_credits_adjust_' . (int) $user->ID, '_aksrt_credits_nonce'); ?>
|
||||
<fieldset>
|
||||
<label for="aksrt_credits_delta" style="display:block;margin-bottom:6px;">
|
||||
<?php esc_html_e('变动数量(正数=增加,负数=扣减)', 'aksrt'); ?>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="aksrt_credits_delta"
|
||||
name="aksrt_credits_delta"
|
||||
class="regular-text"
|
||||
min="-100000"
|
||||
max="100000"
|
||||
step="1"
|
||||
value="0"
|
||||
>
|
||||
<p class="description" style="margin-bottom:12px;">
|
||||
<?php esc_html_e('填写 0 不会执行任何变更。', 'aksrt'); ?>
|
||||
</p>
|
||||
|
||||
<label for="aksrt_credits_reason" style="display:block;margin-bottom:6px;">
|
||||
<?php esc_html_e('调整原因', 'aksrt'); ?>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="aksrt_credits_reason"
|
||||
name="aksrt_credits_reason"
|
||||
class="regular-text"
|
||||
maxlength="100"
|
||||
placeholder="<?php echo esc_attr__('例如:手工补偿 / 运营赠送 / 退款修正', 'aksrt'); ?>"
|
||||
>
|
||||
<p class="description">
|
||||
<?php esc_html_e('勾选后点击页面底部"更新用户"保存即可生效。', 'aksrt'); ?>
|
||||
</p>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
|
||||
</table>
|
||||
|
||||
<?php
|
||||
// ========== 近期积分流水 ==========
|
||||
$log = \AKSRT\Credits\Credits::get_log(['user_id' => (int) $user->ID, 'page' => 1, 'per_page' => 20]);
|
||||
$items = $log['items'] ?? [];
|
||||
$reason_labels = [
|
||||
'register' => __('注册赠送', 'aksrt'),
|
||||
'signin' => __('每日签到', 'aksrt'),
|
||||
'comment' => __('评论奖励', 'aksrt'),
|
||||
'invite' => __('邀请奖励', 'aksrt'),
|
||||
'invitee' => __('被邀请奖励', 'aksrt'),
|
||||
'download' => __('下载消费', 'aksrt'),
|
||||
'admin' => __('管理员调整', 'aksrt'),
|
||||
];
|
||||
?>
|
||||
<h3 style="margin-top:24px;"><?php echo esc_html(sprintf(__('近期%s流水', 'aksrt'), $currency_name)); ?></h3>
|
||||
<?php if (!empty($items)) : ?>
|
||||
<table class="widefat striped" style="max-width:800px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e('时间', 'aksrt'); ?></th>
|
||||
<th><?php esc_html_e('类型', 'aksrt'); ?></th>
|
||||
<th><?php esc_html_e('备注', 'aksrt'); ?></th>
|
||||
<th><?php esc_html_e('变动', 'aksrt'); ?></th>
|
||||
<th><?php esc_html_e('余额', 'aksrt'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($items as $row) :
|
||||
$delta = (int) $row['delta'];
|
||||
$reason_key = (string) ($row['reason'] ?? '');
|
||||
$label = $reason_labels[$reason_key] ?? $reason_key;
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html(get_date_from_gmt((string) $row['created_at'], 'Y-m-d H:i')); ?></td>
|
||||
<td><?php echo esc_html($label); ?></td>
|
||||
<td><?php echo esc_html((string) ($row['remark'] ?? '')); ?></td>
|
||||
<td style="color:<?php echo $delta >= 0 ? '#16a34a' : '#ef4444'; ?>; font-weight:600;">
|
||||
<?php echo esc_html(($delta > 0 ? '+' : '') . $delta); ?>
|
||||
</td>
|
||||
<td><?php echo esc_html((string) (int) $row['balance_after']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else : ?>
|
||||
<p style="color:#888;"><?php echo esc_html(sprintf(__('暂无%s流水记录。', 'aksrt'), $currency_name)); ?></p>
|
||||
<?php endif;
|
||||
}
|
||||
|
||||
// ========== 处理积分调整 ==========
|
||||
|
||||
public function handle_user_credits_update(int $user_id): void {
|
||||
if (!$this->can_manage_credits() || !class_exists('\AKSRT\Credits\Credits')) {
|
||||
return;
|
||||
}
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return;
|
||||
}
|
||||
if (!isset($_POST['_aksrt_credits_nonce'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nonce = sanitize_text_field((string) $_POST['_aksrt_credits_nonce']);
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_user_credits_adjust_' . $user_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$delta = isset($_POST['aksrt_credits_delta']) ? (int) $_POST['aksrt_credits_delta'] : 0;
|
||||
$reason = isset($_POST['aksrt_credits_reason']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_credits_reason'])) : '';
|
||||
|
||||
if ($delta === 0) {
|
||||
return;
|
||||
}
|
||||
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
if ($reason === '') {
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'error',
|
||||
'msg' => sprintf(__('调整%s时请填写原因。', 'aksrt'), $currency_name),
|
||||
]], 30);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($delta > 0) {
|
||||
$result = \AKSRT\Credits\Credits::add($user_id, $delta, 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
|
||||
} else {
|
||||
$result = \AKSRT\Credits\Credits::deduct($user_id, abs($delta), 'admin', ['type' => 'user', 'id' => get_current_user_id()], $reason);
|
||||
}
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'error',
|
||||
'msg' => $result->get_error_message(),
|
||||
]], 30);
|
||||
return;
|
||||
}
|
||||
|
||||
set_transient('aksrt_admin_notice_' . get_current_user_id(), [[
|
||||
'type' => 'success',
|
||||
'msg' => sprintf(__('%s调整成功,当前余额:%d', 'aksrt'), $currency_name, (int) $result),
|
||||
]], 30);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Follow {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function table(): string {
|
||||
return \AKSRT\Core\DBMigrator::table('follow');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关注用户
|
||||
*
|
||||
* @param int $follower_id 关注者 ID
|
||||
* @param int $following_id 被关注者 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function follow(int $follower_id, int $following_id): bool {
|
||||
if ($follower_id <= 0 || $following_id <= 0 || $follower_id === $following_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已关注则跳过
|
||||
if ($this->is_following($follower_id, $following_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$result = $wpdb->insert($this->table(), [
|
||||
'follower_id' => $follower_id,
|
||||
'following_id' => $following_id,
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%d', '%s']);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
do_action('aksrt_user_followed', $follower_id, $following_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消关注
|
||||
*/
|
||||
public function unfollow(int $follower_id, int $following_id): bool {
|
||||
if ($follower_id <= 0 || $following_id <= 0) return false;
|
||||
|
||||
global $wpdb;
|
||||
$result = $wpdb->delete($this->table(), [
|
||||
'follower_id' => $follower_id,
|
||||
'following_id' => $following_id,
|
||||
], ['%d', '%d']);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换关注状态
|
||||
*
|
||||
* @return bool true=已关注, false=已取消
|
||||
*/
|
||||
public function toggle(int $follower_id, int $following_id): bool {
|
||||
if ($this->is_following($follower_id, $following_id)) {
|
||||
$this->unfollow($follower_id, $following_id);
|
||||
return false;
|
||||
}
|
||||
$this->follow($follower_id, $following_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已关注
|
||||
*/
|
||||
public function is_following(int $follower_id, int $following_id): bool {
|
||||
if ($follower_id <= 0 || $following_id <= 0) return false;
|
||||
|
||||
global $wpdb;
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE follower_id = %d AND following_id = %d LIMIT 1",
|
||||
$follower_id, $following_id
|
||||
));
|
||||
|
||||
return (int) $exists > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取粉丝数(谁关注了 target_id)
|
||||
*/
|
||||
public function get_follower_count(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} WHERE following_id = %d",
|
||||
$user_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关注数(target_id 关注了谁)
|
||||
*/
|
||||
public function get_following_count(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} WHERE follower_id = %d",
|
||||
$user_id
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取粉丝列表
|
||||
*/
|
||||
public function get_followers(int $user_id, int $page = 1, int $per_page = 20): array {
|
||||
return $this->get_user_list($user_id, 'following_id', $page, $per_page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关注列表
|
||||
*/
|
||||
public function get_following(int $user_id, int $page = 1, int $per_page = 20): array {
|
||||
return $this->get_user_list($user_id, 'follower_id', $page, $per_page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用用户列表查询
|
||||
*/
|
||||
private function get_user_list(int $user_id, string $where_field, int $page, int $per_page): array {
|
||||
global $wpdb;
|
||||
|
||||
$other_field = $where_field === 'follower_id' ? 'following_id' : 'follower_id';
|
||||
$offset = max(0, ($page - 1) * $per_page);
|
||||
|
||||
$total = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table()} WHERE {$where_field} = %d",
|
||||
$user_id
|
||||
));
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT {$other_field} as user_id, created_at
|
||||
FROM {$this->table()}
|
||||
WHERE {$where_field} = %d
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %d OFFSET %d",
|
||||
$user_id, $per_page, $offset
|
||||
), ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$uid = (int) $row['user_id'];
|
||||
$user = get_userdata($uid);
|
||||
$items[] = [
|
||||
'user_id' => $uid,
|
||||
'display_name' => $user ? $user->display_name : '',
|
||||
'avatar_url' => $user ? get_avatar_url($uid, ['size' => 48]) : '',
|
||||
'created_at' => $row['created_at'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'total_pages' => max(1, (int) ceil($total / $per_page)),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Home {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** @var array Tab 注册表 */
|
||||
private $tabs = [];
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
$this->register_default_tabs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册默认公开 Tab
|
||||
*/
|
||||
private function register_default_tabs(): void {
|
||||
$this->tabs = [
|
||||
'posts' => [
|
||||
'label' => __('文章', 'aksrt'),
|
||||
'icon' => 'file-alt',
|
||||
'priority' => 10,
|
||||
],
|
||||
'comments' => [
|
||||
'label' => __('评论', 'aksrt'),
|
||||
'icon' => 'comment',
|
||||
'priority' => 20,
|
||||
],
|
||||
'favorites' => [
|
||||
'label' => __('收藏', 'aksrt'),
|
||||
'icon' => 'star',
|
||||
'priority' => 30,
|
||||
],
|
||||
'credits' => [
|
||||
'label' => class_exists('\AKSRT\Settings\Settings')
|
||||
? sprintf(__('我的%s', 'aksrt'), \AKSRT\Settings\Settings::get_currency_name())
|
||||
: __('我的积分', 'aksrt'),
|
||||
'icon' => 'wallet',
|
||||
'priority' => 40,
|
||||
],
|
||||
'follow' => [
|
||||
'label' => __('粉丝', 'aksrt'),
|
||||
'icon' => 'user-plus',
|
||||
'priority' => 80,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 允许插件/子主题注册额外公开 Tab
|
||||
*
|
||||
* @param array $tabs Tab 注册表
|
||||
*/
|
||||
$this->tabs = apply_filters('aksrt_user_home_tabs', $this->tabs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Tab 注册表
|
||||
*/
|
||||
public function get_tabs(): array {
|
||||
return $this->tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户主页实际可见的公开 Tab。
|
||||
*/
|
||||
public function get_visible_tabs(bool $is_own_profile = false): array {
|
||||
$settings = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_user_home_settings()
|
||||
: [
|
||||
'tabs' => ['posts', 'comments', 'favorites'],
|
||||
];
|
||||
|
||||
$visible = [];
|
||||
foreach ((array) ($settings['tabs'] ?? []) as $key) {
|
||||
if (!isset($this->tabs[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($key === 'credits' && (!$is_own_profile || !class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::credits_enabled())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$visible[$key] = $this->tabs[$key];
|
||||
}
|
||||
|
||||
if (empty($visible)) {
|
||||
foreach (['posts', 'comments', 'favorites'] as $fallback) {
|
||||
if (isset($this->tabs[$fallback])) {
|
||||
$visible[$fallback] = $this->tabs[$fallback];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公开展示的身份信号(认证 / 等级 / 徽章)。
|
||||
*
|
||||
* @return array<int,array{label:string,icon:string,tone:string,icon_url?:string,type?:string,slug?:string,wear?:bool}>
|
||||
*/
|
||||
public function get_profile_signals(int $user_id): array {
|
||||
if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
|
||||
$signals = [];
|
||||
|
||||
if (!empty($settings['verify']['enabled']) && !empty($settings['verify']['public_display']) && $this->is_user_verified($user_id, $settings['verify'])) {
|
||||
$info = class_exists('AKSRT\\User\\AuthApply') ? AuthApply::get_auth_info($user_id) : [];
|
||||
$application = class_exists('AKSRT\\User\\AuthApply') ? AuthApply::get_apply_payload($user_id) : [];
|
||||
$type = sanitize_key((string) ($info['type'] ?? $application['type'] ?? ''));
|
||||
$type_settings = is_array($settings['verify']['types'][$type] ?? null) ? $settings['verify']['types'][$type] : [];
|
||||
$label = trim((string) ($type_settings['badge_text'] ?? $type_settings['label'] ?? ''));
|
||||
if ($label === '') {
|
||||
foreach ((array) ($settings['verify']['types'] ?? []) as $configured_type) {
|
||||
if (is_array($configured_type) && !empty($configured_type['badge_text'])) {
|
||||
$label = trim((string) $configured_type['badge_text']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$signals[] = [
|
||||
'type' => 'verify',
|
||||
'label' => $label,
|
||||
'title' => $label,
|
||||
'icon' => 'check-circle',
|
||||
'icon_url' => get_theme_file_uri('/assets/img/user/user-certification.svg'),
|
||||
'tone' => 'success',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($settings['level']['enabled']) && !empty($settings['level']['public_display'])) {
|
||||
$level = $this->get_user_level($user_id, $settings['level']);
|
||||
if ($level > 0) {
|
||||
$signals[] = [
|
||||
'type' => 'level',
|
||||
'label' => Level::get_level_name($level),
|
||||
'title' => Level::get_level_name($level),
|
||||
'icon' => 'layer-group',
|
||||
'icon_url' => Level::get_level_icon_url($level),
|
||||
'tone' => 'info',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
$badge_count = 0;
|
||||
foreach ((array) ($list['items'] ?? []) as $item) {
|
||||
if ($badge_count >= $limit) {
|
||||
break;
|
||||
}
|
||||
$signals[] = [
|
||||
'type' => 'badge',
|
||||
'slug' => (string) ($item['slug'] ?? ''),
|
||||
'label' => (string) ($item['label'] ?? ''),
|
||||
'icon' => 'award',
|
||||
'tone' => (string) ($item['tone'] ?? 'neutral'),
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
'wear' => !empty($item['wear']),
|
||||
];
|
||||
$badge_count++;
|
||||
}
|
||||
} 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);
|
||||
if (!is_array($assigned)) {
|
||||
$assigned = preg_split('/[\s,]+/', (string) $assigned, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $signals;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公开治理提示。
|
||||
*
|
||||
* @return array{state:string,text:string}|array{}
|
||||
*/
|
||||
public function get_profile_notice(int $user_id): array {
|
||||
if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
|
||||
if (empty($settings['report']['enabled']) || empty($settings['report']['public_notice'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$state = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_ban_state', true));
|
||||
if (!in_array($state, ['muted', 'restricted', 'banned'], true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => $state,
|
||||
'text' => (string) $settings['report']['ban_notice_text'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开主页是否显示举报入口。
|
||||
*/
|
||||
public function show_report_entry(): bool {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
|
||||
return !empty($settings['report']['enabled']) && !empty($settings['report']['public_entry']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{accept_email_verified:bool} $verify_settings
|
||||
*/
|
||||
private function is_user_verified(int $user_id, array $verify_settings): bool {
|
||||
if (class_exists('\\AKSRT\\User\\AuthApply') && AuthApply::is_verified($user_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($verify_settings['accept_email_verified']) && class_exists('\AKSRT\Auth\Service')) {
|
||||
return \AKSRT\Auth\Service::user_is_email_verified($user_id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{source?:string,thresholds?:array<int,int>} $level_settings
|
||||
*/
|
||||
private function get_user_level(int $user_id, array $level_settings): int {
|
||||
unset($level_settings);
|
||||
return class_exists('AKSRT\\User\\Level') ? Level::get_level($user_id) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前页面是否为公开用户主页
|
||||
*/
|
||||
public static function is_page(): bool {
|
||||
return function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
/**
|
||||
* User growth levels and experience awards.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Level {
|
||||
|
||||
public const META_INTEGRAL = 'aksrt_level_integral';
|
||||
public const META_LEVEL = 'aksrt_user_level';
|
||||
public const META_DETAIL = 'aksrt_level_integral_detail';
|
||||
public const META_DATE_DETAIL = 'aksrt_level_integral_date_detail';
|
||||
public const META_FOLLOWED = 'aksrt_level_integral_followed';
|
||||
public const META_LOGIN_DAY = 'aksrt_level_integral_login_day';
|
||||
|
||||
/** @var self|null */
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('user_register', [$this, 'on_register'], 20, 1);
|
||||
add_action('wp_login', [$this, 'on_login'], 20, 2);
|
||||
add_action('aksrt_user_signed_in', [$this, 'on_checkin'], 20, 2);
|
||||
add_action('transition_post_status', [$this, 'on_post_status'], 20, 3);
|
||||
add_action('comment_post', [$this, 'on_comment_post'], 20, 3);
|
||||
add_action('transition_comment_status', [$this, 'on_comment_status'], 20, 3);
|
||||
add_action('aksrt_user_followed', [$this, 'on_followed'], 20, 2);
|
||||
add_action('aksrt_post_liked', [$this, 'on_post_like'], 20, 3);
|
||||
add_action('aksrt_comment_liked', [$this, 'on_comment_like'], 20, 3);
|
||||
add_action('aksrt_post_favorited', [$this, 'on_post_favorite'], 20, 3);
|
||||
add_action('aksrt_user_level_changed', [$this, 'on_level_changed'], 20, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled:bool,public_display:bool,max:int,level_opt:array<string,mixed>,integral_opt:array<string,mixed>}
|
||||
*/
|
||||
public static function settings(): array {
|
||||
$all = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
$max = max(3, min(30, (int) ($all['user_level_max'] ?? 10)));
|
||||
|
||||
return [
|
||||
'enabled' => !isset($all['user_level_s']) || !empty($all['user_level_s']),
|
||||
'public_display' => !isset($all['user_level_public_display']) || !empty($all['user_level_public_display']),
|
||||
'max' => $max,
|
||||
'level_opt' => self::normalize_array_option($all['user_level_opt'] ?? []),
|
||||
'integral_opt' => self::normalize_array_option($all['user_integral_opt'] ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function normalize_array_option($value): array {
|
||||
if (is_string($value) && $value !== '') {
|
||||
$decoded = json_decode($value, true);
|
||||
$value = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
return is_array($value) ? $value : [];
|
||||
}
|
||||
|
||||
/** @return array<string,int> */
|
||||
public static function integral_defaults(): array {
|
||||
return [
|
||||
'day_max' => 100,
|
||||
'sign_up' => 20,
|
||||
'sign_in' => 5,
|
||||
'followed' => 5,
|
||||
'post_new' => 5,
|
||||
'post_like' => 1,
|
||||
'post_favorite' => 2,
|
||||
'comment_new' => 2,
|
||||
'comment_like' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
public static function integral_labels(): array {
|
||||
return [
|
||||
'sign_up' => __('首次注册', 'aksrt'),
|
||||
'sign_in' => __('每日登录', 'aksrt'),
|
||||
'checkin' => __('每日签到', 'aksrt'),
|
||||
'invitation' => __('邀请奖励', 'aksrt'),
|
||||
'shop_gift' => __('购物赠送', 'aksrt'),
|
||||
'followed' => __('被关注', 'aksrt'),
|
||||
'post_new' => __('发布文章', 'aksrt'),
|
||||
'post_like' => __('文章获赞', 'aksrt'),
|
||||
'post_favorite' => __('文章被收藏', 'aksrt'),
|
||||
'comment_new' => __('发表评论', 'aksrt'),
|
||||
'comment_like' => __('评论获赞', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function task_points(string $key): int {
|
||||
$opt = self::settings()['integral_opt'];
|
||||
$defaults = self::integral_defaults();
|
||||
return max(0, (int) ($opt[$key] ?? $defaults[$key] ?? 0));
|
||||
}
|
||||
|
||||
public static function day_max(): int {
|
||||
$opt = self::settings()['integral_opt'];
|
||||
return max(0, (int) ($opt['day_max'] ?? self::integral_defaults()['day_max']));
|
||||
}
|
||||
|
||||
public static function get_integral(int $user_id): int {
|
||||
return $user_id > 0 ? max(0, (int) get_user_meta($user_id, self::META_INTEGRAL, true)) : 0;
|
||||
}
|
||||
|
||||
public static function get_level(int $user_id): int {
|
||||
$cfg = self::settings();
|
||||
if ($user_id <= 0 || !$cfg['enabled']) {
|
||||
return 0;
|
||||
}
|
||||
$level = (int) get_user_meta($user_id, self::META_LEVEL, true);
|
||||
if ($level <= 0) {
|
||||
$level = self::resolve_level(self::get_integral($user_id), $cfg);
|
||||
update_user_meta($user_id, self::META_LEVEL, $level);
|
||||
}
|
||||
return max(1, min($cfg['max'], $level));
|
||||
}
|
||||
|
||||
public static function get_level_name(int $level): string {
|
||||
$cfg = self::settings();
|
||||
$level = max(1, min($cfg['max'], $level));
|
||||
return sanitize_text_field((string) ($cfg['level_opt']['name_' . $level] ?? ('LV' . $level)));
|
||||
}
|
||||
|
||||
public static function get_level_icon_url(int $level): string {
|
||||
$cfg = self::settings();
|
||||
$level = max(1, min($cfg['max'], $level));
|
||||
$custom = esc_url_raw((string) ($cfg['level_opt']['icon_img_' . $level] ?? ''));
|
||||
if ($custom !== '') {
|
||||
return $custom;
|
||||
}
|
||||
$path = AKSRT_DIR . '/assets/img/user-level-' . $level . '.png';
|
||||
return is_file($path) ? AKSRT_URI . '/assets/img/user-level-' . $level . '.png' : '';
|
||||
}
|
||||
|
||||
public static function get_badge_html(int $user_id, string $class = ''): string {
|
||||
$cfg = self::settings();
|
||||
if (!$cfg['enabled'] || !$cfg['public_display']) {
|
||||
return '';
|
||||
}
|
||||
$level = self::get_level($user_id);
|
||||
if ($level <= 0) {
|
||||
return '';
|
||||
}
|
||||
$name = self::get_level_name($level);
|
||||
$icon = self::get_level_icon_url($level);
|
||||
if ($icon !== '') {
|
||||
return '<img class="aksrt-user-level-icon img-icon ' . esc_attr($class) . '" src="' . esc_url($icon) . '" alt="' . esc_attr($name) . '" title="' . esc_attr($name) . '" loading="lazy" decoding="async">';
|
||||
}
|
||||
return '<span class="badg badg-sm aksrt-user-level-badge ' . esc_attr($class) . '" title="' . esc_attr($name) . '">LV' . esc_html((string) $level) . '</span>';
|
||||
}
|
||||
|
||||
/** @param array{max?:int,level_opt?:array<string,mixed>}|null $cfg */
|
||||
public static function resolve_level(int $integral, ?array $cfg = null): int {
|
||||
$cfg = $cfg ?? self::settings();
|
||||
$max = max(1, (int) ($cfg['max'] ?? 10));
|
||||
$opt = is_array($cfg['level_opt'] ?? null) ? $cfg['level_opt'] : [];
|
||||
$level = 1;
|
||||
for ($i = 2; $i <= $max; $i++) {
|
||||
$need = max(0, (int) ($opt['upgrade_integral_' . $i] ?? (($i - 1) * 500 * $i)));
|
||||
if ($integral < $need) {
|
||||
break;
|
||||
}
|
||||
$level = $i;
|
||||
}
|
||||
return $level;
|
||||
}
|
||||
|
||||
/** @return array{level:int,name:string,icon:string,integral:int,current_need:int,next_need:int,remaining:int,progress:int,is_max:bool} */
|
||||
public static function get_progress(int $user_id): array {
|
||||
$cfg = self::settings();
|
||||
$integral = self::get_integral($user_id);
|
||||
$level = self::get_level($user_id);
|
||||
$current_need = $level <= 1 ? 0 : max(0, (int) ($cfg['level_opt']['upgrade_integral_' . $level] ?? (($level - 1) * 500 * $level)));
|
||||
$is_max = $level >= $cfg['max'];
|
||||
$next_need = $is_max ? $integral : max($current_need + 1, (int) ($cfg['level_opt']['upgrade_integral_' . ($level + 1)] ?? ($level * 500 * ($level + 1))));
|
||||
$span = max(1, $next_need - $current_need);
|
||||
$progress = $is_max ? 100 : (int) min(100, max(0, (($integral - $current_need) / $span) * 100));
|
||||
|
||||
return [
|
||||
'level' => $level,
|
||||
'name' => self::get_level_name($level),
|
||||
'icon' => self::get_level_icon_url($level),
|
||||
'integral' => $integral,
|
||||
'current_need' => $current_need,
|
||||
'next_need' => $next_need,
|
||||
'remaining' => $is_max ? 0 : max(0, $next_need - $integral),
|
||||
'progress' => $progress,
|
||||
'is_max' => $is_max,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int,array{integral:int,key:string,value:int,time:string}> */
|
||||
public static function get_details(int $user_id): array {
|
||||
$details = get_user_meta($user_id, self::META_DETAIL, true);
|
||||
return is_array($details) ? array_values($details) : [];
|
||||
}
|
||||
|
||||
/** @return array<string,int> */
|
||||
public static function get_date_details(int $user_id): array {
|
||||
$details = get_user_meta($user_id, self::META_DATE_DETAIL, true);
|
||||
return is_array($details) ? $details : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Award experience and append its immutable display ledger.
|
||||
*
|
||||
* @return int|false Granted amount or false.
|
||||
*/
|
||||
public static function add(int $user_id, string $key, int $points = 0, bool $bypass_day_max = false) {
|
||||
if ($user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
$lock = self::acquire_lock('user:' . $user_id);
|
||||
if ($lock === '') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return self::add_locked($user_id, $key, $points, $bypass_day_max);
|
||||
} finally {
|
||||
self::release_lock($lock);
|
||||
}
|
||||
}
|
||||
|
||||
private static function add_locked(int $user_id, string $key, int $points, bool $bypass_day_max) {
|
||||
$cfg = self::settings();
|
||||
$key = sanitize_key($key);
|
||||
if ($user_id <= 0 || !$cfg['enabled'] || $key === '' || $key === 'day_max') {
|
||||
return false;
|
||||
}
|
||||
if (class_exists('\AKSRT\User\Restriction') && \AKSRT\User\Restriction::get_instance()->is_banned($user_id)) {
|
||||
return false;
|
||||
}
|
||||
$points = $points > 0 ? $points : self::task_points($key);
|
||||
if ($points <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$today = current_time('Y-m-d');
|
||||
$date_details = self::get_date_details($user_id);
|
||||
$today_total = max(0, (int) ($date_details[$today] ?? 0));
|
||||
$day_max = self::day_max();
|
||||
if (!$bypass_day_max && $day_max > 0 && $today_total >= $day_max) {
|
||||
return false;
|
||||
}
|
||||
if ($points <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$before = self::get_integral($user_id);
|
||||
$after = $before + $points;
|
||||
update_user_meta($user_id, self::META_INTEGRAL, $after);
|
||||
|
||||
$date_details[$today] = $today_total + $points;
|
||||
uksort($date_details, static fn(string $a, string $b): int => strcmp($b, $a));
|
||||
update_user_meta($user_id, self::META_DATE_DETAIL, array_slice($date_details, 0, 30, true));
|
||||
|
||||
$details = self::get_details($user_id);
|
||||
array_unshift($details, [
|
||||
'integral' => $after,
|
||||
'key' => $key,
|
||||
'value' => $points,
|
||||
'time' => current_time('mysql'),
|
||||
]);
|
||||
update_user_meta($user_id, self::META_DETAIL, array_slice($details, 0, 50));
|
||||
|
||||
$old_level = max(1, (int) get_user_meta($user_id, self::META_LEVEL, true));
|
||||
$new_level = $old_level;
|
||||
$next_need = max(0, (int) ($cfg['level_opt']['upgrade_integral_' . ($old_level + 1)] ?? PHP_INT_MAX));
|
||||
if ($old_level < $cfg['max'] && $after >= $next_need) {
|
||||
$new_level = self::resolve_level($after, $cfg);
|
||||
}
|
||||
update_user_meta($user_id, self::META_LEVEL, $new_level);
|
||||
if ($new_level !== $old_level) {
|
||||
do_action('aksrt_user_level_changed', $user_id, $new_level, $old_level);
|
||||
}
|
||||
return $points;
|
||||
}
|
||||
|
||||
private static function acquire_lock(string $scope): string {
|
||||
global $wpdb;
|
||||
$name = substr($wpdb->prefix . 'aksrt_level_' . md5($scope), 0, 64);
|
||||
$acquired = (int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, %d)', $name, 5));
|
||||
return $acquired === 1 ? $name : '';
|
||||
}
|
||||
|
||||
private static function release_lock(string $name): void {
|
||||
global $wpdb;
|
||||
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $name));
|
||||
}
|
||||
|
||||
private static function award_once(int $user_id, string $key, string $meta_type, int $entity_id, string $marker, int $limit = 1, int $actor_id = 0): void {
|
||||
if ($user_id <= 0 || $entity_id <= 0) {
|
||||
return;
|
||||
}
|
||||
$lock = self::acquire_lock($meta_type . ':' . $entity_id . ':' . $marker);
|
||||
if ($lock === '') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
self::award_once_locked($user_id, $key, $meta_type, $entity_id, $marker, $limit, $actor_id);
|
||||
} finally {
|
||||
self::release_lock($lock);
|
||||
}
|
||||
}
|
||||
|
||||
private static function award_once_locked(int $user_id, string $key, string $meta_type, int $entity_id, string $marker, int $limit, int $actor_id): void {
|
||||
$get = $meta_type === 'comment' ? 'get_comment_meta' : 'get_post_meta';
|
||||
$update = $meta_type === 'comment' ? 'update_comment_meta' : 'update_post_meta';
|
||||
$awarded = $get($entity_id, $marker, true);
|
||||
$awarded = is_array($awarded) ? array_values(array_unique(array_map('intval', $awarded))) : [];
|
||||
if ($actor_id > 0) {
|
||||
if (in_array($actor_id, $awarded, true) || count($awarded) >= $limit) {
|
||||
return;
|
||||
}
|
||||
} elseif ($awarded !== []) {
|
||||
return;
|
||||
}
|
||||
if (self::add($user_id, $key)) {
|
||||
$awarded[] = $actor_id > 0 ? $actor_id : 0;
|
||||
$update($entity_id, $marker, $awarded);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_register(int $user_id): void {
|
||||
self::add($user_id, 'sign_up', 0, true);
|
||||
}
|
||||
|
||||
public function on_login($user_login, $user): void {
|
||||
$user_id = $user instanceof \WP_User ? (int) $user->ID : 0;
|
||||
$today = current_time('Ymd');
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
$lock = self::acquire_lock('login:' . $user_id . ':' . $today);
|
||||
if ($lock === '') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (get_user_meta($user_id, self::META_LOGIN_DAY, true) === $today) {
|
||||
return;
|
||||
}
|
||||
if (self::add($user_id, 'sign_in')) {
|
||||
update_user_meta($user_id, self::META_LOGIN_DAY, $today);
|
||||
}
|
||||
} finally {
|
||||
self::release_lock($lock);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_checkin(int $user_id, int $reward = 0): void {
|
||||
if ($reward > 0) {
|
||||
self::add($user_id, 'checkin', $reward);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_post_status(string $new_status, string $old_status, $post): void {
|
||||
if (!$post instanceof \WP_Post || $new_status !== 'publish' || $old_status === 'publish') {
|
||||
return;
|
||||
}
|
||||
$key = '';
|
||||
if ($post->post_type === 'post') {
|
||||
$key = 'post_new';
|
||||
}
|
||||
if ($key !== '') {
|
||||
self::award_once((int) $post->post_author, $key, 'post', (int) $post->ID, '_aksrt_level_' . $key);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_comment_post($comment_id, $approved, $commentdata): void {
|
||||
if ((string) $approved !== '1') {
|
||||
return;
|
||||
}
|
||||
$comment = get_comment((int) $comment_id);
|
||||
$this->award_comment_new($comment);
|
||||
}
|
||||
|
||||
public function on_comment_status(string $new_status, string $old_status, $comment): void {
|
||||
if ($new_status === 'approved' && $old_status !== 'approved') {
|
||||
$this->award_comment_new($comment);
|
||||
}
|
||||
}
|
||||
|
||||
private function award_comment_new($comment): void {
|
||||
if (!$comment instanceof \WP_Comment || (int) $comment->user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::award_once((int) $comment->user_id, 'comment_new', 'comment', (int) $comment->comment_ID, '_aksrt_level_comment_new');
|
||||
}
|
||||
|
||||
public function on_followed($follower_id, $followed_id): void {
|
||||
$follower_id = (int) $follower_id;
|
||||
$followed_id = (int) $followed_id;
|
||||
if ($follower_id <= 0 || $followed_id <= 0) {
|
||||
return;
|
||||
}
|
||||
$lock = self::acquire_lock('follow:' . $followed_id);
|
||||
if ($lock === '') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$awarded = get_user_meta($followed_id, self::META_FOLLOWED, true);
|
||||
$awarded = is_array($awarded) ? array_map('intval', $awarded) : [];
|
||||
if (in_array($follower_id, $awarded, true)) {
|
||||
return;
|
||||
}
|
||||
if (self::add($followed_id, 'followed')) {
|
||||
$awarded[] = $follower_id;
|
||||
update_user_meta($followed_id, self::META_FOLLOWED, array_values(array_unique($awarded)));
|
||||
}
|
||||
} finally {
|
||||
self::release_lock($lock);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_post_like($post_id, $user_id, $active = true): void {
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
$post = get_post((int) $post_id);
|
||||
if ($post instanceof \WP_Post && (int) $post->post_author !== (int) $user_id) {
|
||||
self::award_once((int) $post->post_author, 'post_like', 'post', (int) $post_id, '_aksrt_level_post_like', 5, (int) $user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_post_favorite($post_id, $user_id, $active = true): void {
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
$post = get_post((int) $post_id);
|
||||
if ($post instanceof \WP_Post && (int) $post->post_author !== (int) $user_id) {
|
||||
self::award_once((int) $post->post_author, 'post_favorite', 'post', (int) $post_id, '_aksrt_level_post_favorite', 5, (int) $user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_comment_like($comment_id, $user_id, $active = true): void {
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
$comment = get_comment((int) $comment_id);
|
||||
if ($comment instanceof \WP_Comment && (int) $comment->user_id !== (int) $user_id) {
|
||||
self::award_once((int) $comment->user_id, 'comment_like', 'comment', (int) $comment_id, '_aksrt_level_comment_like', 2, (int) $user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_level_changed(int $user_id, int $new_level, int $old_level): void {
|
||||
$all = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||
if ($new_level <= $old_level || isset($all['user_level_notify']) && empty($all['user_level_notify'])) {
|
||||
return;
|
||||
}
|
||||
if (class_exists('\AKSRT\Messages\Message')) {
|
||||
$name = self::get_level_name($new_level);
|
||||
\AKSRT\Messages\Message::get_instance()->send(
|
||||
$user_id,
|
||||
'level_up',
|
||||
sprintf(__('恭喜你升级至「%s」', 'aksrt'), $name),
|
||||
[
|
||||
'title' => __('等级提升', 'aksrt'),
|
||||
'target_url' => function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('level') : home_url('/'),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
/**
|
||||
* Native WordPress user-level operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class LevelAdmin {
|
||||
|
||||
/** @var self|null */
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
|
||||
add_filter('manage_users_columns', [$this, 'add_column']);
|
||||
add_filter('manage_users_custom_column', [$this, 'render_column'], 10, 3);
|
||||
add_filter('manage_users_sortable_columns', [$this, 'sortable_columns']);
|
||||
add_action('pre_get_users', [$this, 'sort_users']);
|
||||
add_filter('user_row_actions', [$this, 'add_row_action'], 25, 2);
|
||||
add_action('show_user_profile', [$this, 'render_section']);
|
||||
add_action('edit_user_profile', [$this, 'render_section']);
|
||||
add_action('personal_options_update', [$this, 'handle_update']);
|
||||
add_action('edit_user_profile_update', [$this, 'handle_update']);
|
||||
}
|
||||
|
||||
private function enabled(): bool {
|
||||
return class_exists(Level::class) && !empty(Level::settings()['enabled']);
|
||||
}
|
||||
|
||||
public function enqueue_assets(string $hook): void {
|
||||
if (!in_array($hook, ['users.php', 'user-edit.php', 'profile.php'], true)) {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_style(
|
||||
'aksrt-user-level-admin',
|
||||
get_theme_file_uri('/assets/css/admin/user-level.css'),
|
||||
[],
|
||||
defined('AKSRT_VERSION') ? AKSRT_VERSION : null
|
||||
);
|
||||
}
|
||||
|
||||
private function can_manage(int $user_id): bool {
|
||||
return current_user_can('manage_options') && current_user_can('edit_user', $user_id);
|
||||
}
|
||||
|
||||
public function add_column(array $columns): array {
|
||||
if ($this->enabled()) {
|
||||
$columns['aksrt_user_level'] = __('用户等级', 'aksrt');
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function render_column(string $output, string $column_name, int $user_id): string {
|
||||
if ($column_name !== 'aksrt_user_level' || !$this->enabled()) {
|
||||
return $output;
|
||||
}
|
||||
$level = Level::get_level($user_id);
|
||||
$icon = Level::get_badge_html($user_id, 'aksrt-user-level-icon--admin');
|
||||
return '<span style="display:inline-flex;align-items:center;gap:6px;">' . $icon
|
||||
. '<strong>' . esc_html(Level::get_level_name($level)) . '</strong>'
|
||||
. '<small>(' . esc_html((string) Level::get_integral($user_id)) . ' EXP)</small></span>';
|
||||
}
|
||||
|
||||
public function sortable_columns(array $columns): array {
|
||||
if ($this->enabled()) {
|
||||
$columns['aksrt_user_level'] = 'aksrt_user_level';
|
||||
}
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function sort_users($query): void {
|
||||
if (!is_admin() || $query->get('orderby') !== 'aksrt_user_level') {
|
||||
return;
|
||||
}
|
||||
$query->set('meta_key', Level::META_INTEGRAL);
|
||||
$query->set('orderby', 'meta_value_num');
|
||||
}
|
||||
|
||||
public function add_row_action(array $actions, \WP_User $user): array {
|
||||
if ($this->enabled() && $this->can_manage((int) $user->ID)) {
|
||||
$url = get_edit_user_link((int) $user->ID);
|
||||
if ($url) {
|
||||
$actions['aksrt_edit_level'] = '<a href="' . esc_url($url . '#aksrt-user-level') . '">' . esc_html__('调整等级经验', 'aksrt') . '</a>';
|
||||
}
|
||||
}
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function render_section(\WP_User $user): void {
|
||||
if (!$this->enabled()) {
|
||||
return;
|
||||
}
|
||||
$progress = Level::get_progress((int) $user->ID);
|
||||
$can_manage = $this->can_manage((int) $user->ID);
|
||||
?>
|
||||
<a id="aksrt-user-level"></a>
|
||||
<h2><?php esc_html_e('用户等级(AKSRT)', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th><?php esc_html_e('当前等级', 'aksrt'); ?></th>
|
||||
<td>
|
||||
<span style="display:inline-flex;align-items:center;gap:8px;font-size:16px;">
|
||||
<?php echo Level::get_badge_html((int) $user->ID, 'aksrt-user-level-icon--admin'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<strong><?php echo esc_html((string) $progress['name']); ?></strong>
|
||||
</span>
|
||||
<p class="description"><?php printf(esc_html__('累计经验:%1$d;距离下一等级:%2$d', 'aksrt'), (int) $progress['integral'], (int) $progress['remaining']); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if ($can_manage) : ?>
|
||||
<tr>
|
||||
<th><label for="aksrt_user_level"><?php esc_html_e('用户等级', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<?php wp_nonce_field('aksrt_user_level_admin_' . (int) $user->ID, 'aksrt_user_level_nonce'); ?>
|
||||
<input type="number" class="small-text" id="aksrt_user_level" name="aksrt_user_level" min="1" max="<?php echo esc_attr((string) Level::settings()['max']); ?>" step="1" value="<?php echo esc_attr((string) $progress['level']); ?>">
|
||||
<p class="description"><?php esc_html_e('手动设置等级时请同步调整累计经验,使两项符合等级阈值。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="aksrt_level_integral"><?php esc_html_e('累计经验值', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<input type="number" class="regular-text" id="aksrt_level_integral" name="aksrt_level_integral" min="0" max="9999999999999999" step="1" value="<?php echo esc_attr((string) $progress['integral']); ?>">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function handle_update(int $user_id): void {
|
||||
if (!$this->enabled() || !$this->can_manage($user_id) || !isset($_POST['aksrt_user_level_nonce'])) {
|
||||
return;
|
||||
}
|
||||
$nonce = sanitize_text_field(wp_unslash((string) $_POST['aksrt_user_level_nonce']));
|
||||
if (!wp_verify_nonce($nonce, 'aksrt_user_level_admin_' . $user_id)) {
|
||||
return;
|
||||
}
|
||||
$integral = max(0, (int) ($_POST['aksrt_level_integral'] ?? 0));
|
||||
$level = max(1, min(Level::settings()['max'], (int) ($_POST['aksrt_user_level'] ?? 1)));
|
||||
update_user_meta($user_id, Level::META_INTEGRAL, $integral);
|
||||
update_user_meta($user_id, Level::META_LEVEL, $level);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Metrics {
|
||||
|
||||
public const META_POST_LIKE_SUM = 'aksrt_posts_like_sum';
|
||||
public const META_COMMENT_LIKE_SUM = 'aksrt_comment_like_sum';
|
||||
public const META_POSTS_VIEWS_SUM = 'aksrt_posts_views_sum';
|
||||
public const META_PAY_INCOME = 'aksrt_pay_income_sum';
|
||||
public const META_PAY_REBATE = 'aksrt_pay_rebate_sum';
|
||||
public const META_CHECKIN_ALL_DAY = 'aksrt_checkin_all_day';
|
||||
|
||||
/** @var self|null */
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('aksrt_post_liked', [$this, 'on_post_liked'], 20, 3);
|
||||
add_action('aksrt_comment_liked', [$this, 'on_comment_liked'], 20, 3);
|
||||
add_action('aksrt_post_favorited', [$this, 'on_post_favorited'], 20, 3);
|
||||
add_action('aksrt_user_followed', [$this, 'on_user_followed'], 20, 2);
|
||||
add_action('aksrt_post_viewed', [$this, 'on_post_viewed'], 20, 2);
|
||||
|
||||
add_action('aksrt_user_signed_in', [$this, 'on_user_signed_in'], 20, 1);
|
||||
add_action('aksrt_creator_income_added', [$this, 'on_creator_income_added'], 20, 3);
|
||||
add_action('aksrt_pay_rebate_added', [$this, 'on_pay_rebate_added'], 20, 3);
|
||||
}
|
||||
|
||||
public static function bump_user_meta(int $user_id, string $meta_key, float $delta = 1.0): float {
|
||||
if ($user_id <= 0 || $meta_key === '' || $delta == 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
$current = (float) get_user_meta($user_id, $meta_key, true);
|
||||
$next = max(0.0, $current + $delta);
|
||||
update_user_meta($user_id, $meta_key, $next);
|
||||
return $next;
|
||||
}
|
||||
|
||||
public static function reevaluate_medals(int $user_id): void {
|
||||
if ($user_id > 0 && class_exists('AKSRT\\User\\Badges') && method_exists('AKSRT\\User\\Badges', 'auto_evaluate')) {
|
||||
Badges::auto_evaluate($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_post_liked(int $post_id, int $actor_id, bool $active): void {
|
||||
$post = get_post($post_id);
|
||||
if (!$post instanceof \WP_Post || $post->post_type !== 'post') {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0 || $author === $actor_id) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author, self::META_POST_LIKE_SUM, $active ? 1.0 : -1.0);
|
||||
if ($active) {
|
||||
self::reevaluate_medals($author);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_comment_liked(int $comment_id, int $actor_id, bool $active): void {
|
||||
$comment = get_comment($comment_id);
|
||||
if (!$comment) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $comment->user_id;
|
||||
if ($author <= 0 || $author === $actor_id) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author, self::META_COMMENT_LIKE_SUM, $active ? 1.0 : -1.0);
|
||||
if ($active) {
|
||||
self::reevaluate_medals($author);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_post_favorited(int $post_id, int $actor_id, bool $active): void {
|
||||
unset($post_id, $actor_id, $active);
|
||||
// Favorite is free-points / level only; no dedicated medal get_type.
|
||||
}
|
||||
|
||||
public function on_user_followed(int $follower_id, int $following_id): void {
|
||||
unset($follower_id);
|
||||
if ($following_id > 0) {
|
||||
self::reevaluate_medals($following_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_post_viewed(int $post_id, int $viewer_id = 0): void {
|
||||
unset($viewer_id);
|
||||
$post = get_post($post_id);
|
||||
if (!$post instanceof \WP_Post || $post->post_type !== 'post') {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($author, self::META_POSTS_VIEWS_SUM, 1.0);
|
||||
// Cheap throttle: reevaluate every 50 views on author sum.
|
||||
$sum = (float) get_user_meta($author, self::META_POSTS_VIEWS_SUM, true);
|
||||
if (((int) $sum % 50) === 0) {
|
||||
self::reevaluate_medals($author);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $topic_id
|
||||
* @param int $author_id
|
||||
*/
|
||||
public function on_user_signed_in(int $user_id): void {
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($user_id, self::META_CHECKIN_ALL_DAY, 1.0);
|
||||
self::reevaluate_medals($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param float $amount
|
||||
* @param array $context
|
||||
*/
|
||||
public function on_creator_income_added($user_id, $amount, $context = []): void {
|
||||
unset($context);
|
||||
$user_id = (int) $user_id;
|
||||
$amount = (float) $amount;
|
||||
if ($user_id <= 0 || $amount <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($user_id, self::META_PAY_INCOME, $amount);
|
||||
self::reevaluate_medals($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param float $amount
|
||||
* @param array $context
|
||||
*/
|
||||
public function on_pay_rebate_added($user_id, $amount, $context = []): void {
|
||||
unset($context);
|
||||
$user_id = (int) $user_id;
|
||||
$amount = (float) $amount;
|
||||
if ($user_id <= 0 || $amount <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($user_id, self::META_PAY_REBATE, $amount);
|
||||
self::reevaluate_medals($user_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\User;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Restriction {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const TYPE_WARN = 'warn';
|
||||
const TYPE_MUTE = 'mute';
|
||||
const TYPE_SUSPEND = 'suspend';
|
||||
const TYPE_BAN = 'ban';
|
||||
|
||||
const SCOPE_GLOBAL = 'global';
|
||||
const SCOPE_COMMENT = 'comment';
|
||||
|
||||
const TYPES = [self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_SUSPEND, self::TYPE_BAN];
|
||||
const SCOPES = [self::SCOPE_GLOBAL, self::SCOPE_COMMENT];
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
// Enforce bans on login
|
||||
add_filter('authenticate', [$this, 'enforce_login_ban'], 999, 3);
|
||||
// Enforce comment mute
|
||||
add_filter('pre_comment_on_post', [$this, 'enforce_comment_mute']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the restriction table name.
|
||||
*/
|
||||
private function table(): string {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . 'aksrt_user_restriction';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a restriction to a user.
|
||||
*/
|
||||
public function restrict(int $user_id, string $type, string $reason, string $scope = self::SCOPE_GLOBAL, string $expires_at = '', int $created_by = 0): array {
|
||||
global $wpdb;
|
||||
|
||||
if (!in_array($type, self::TYPES, true)) {
|
||||
return ['success' => false, 'message' => 'Invalid restriction type.'];
|
||||
}
|
||||
if (!in_array($scope, self::SCOPES, true)) {
|
||||
return ['success' => false, 'message' => 'Invalid scope.'];
|
||||
}
|
||||
if ($created_by <= 0) {
|
||||
$created_by = get_current_user_id();
|
||||
}
|
||||
|
||||
$data = [
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'reason' => sanitize_textarea_field($reason),
|
||||
'scope' => $scope,
|
||||
'expires_at' => $expires_at ? gmdate('Y-m-d H:i:s', strtotime($expires_at)) : null,
|
||||
'created_by' => $created_by,
|
||||
'created_at' => current_time('mysql', true),
|
||||
];
|
||||
|
||||
$result = $wpdb->insert($this->table(), $data);
|
||||
if ($result === false) {
|
||||
return ['success' => false, 'message' => 'Database error.'];
|
||||
}
|
||||
|
||||
// Update user meta for quick lookup
|
||||
$this->sync_user_restriction_meta($user_id);
|
||||
|
||||
return ['success' => true, 'id' => $wpdb->insert_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a restriction by ID.
|
||||
*/
|
||||
public function remove_restriction(int $restriction_id): bool {
|
||||
global $wpdb;
|
||||
$restriction = $this->get_restriction($restriction_id);
|
||||
if (!$restriction) return false;
|
||||
|
||||
$result = $wpdb->delete($this->table(), ['id' => $restriction_id]);
|
||||
if ($result) {
|
||||
$this->sync_user_restriction_meta((int) $restriction['user_id']);
|
||||
}
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single restriction by ID.
|
||||
*/
|
||||
public function get_restriction(int $restriction_id): ?array {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE id = %d",
|
||||
$restriction_id
|
||||
), ARRAY_A);
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active restrictions for a user.
|
||||
*/
|
||||
public function get_user_restrictions(int $user_id, bool $active_only = true): array {
|
||||
global $wpdb;
|
||||
$now = current_time('mysql', true);
|
||||
|
||||
if ($active_only) {
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE user_id = %d AND (expires_at IS NULL OR expires_at > %s) ORDER BY created_at DESC",
|
||||
$user_id, $now
|
||||
), ARRAY_A);
|
||||
} else {
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE user_id = %d ORDER BY created_at DESC",
|
||||
$user_id
|
||||
), ARRAY_A);
|
||||
}
|
||||
|
||||
return $rows ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has an active restriction of a given type and scope.
|
||||
*/
|
||||
public function is_restricted(int $user_id, string $type = '', string $scope = ''): bool {
|
||||
$restrictions = $this->get_user_restrictions($user_id, true);
|
||||
foreach ($restrictions as $r) {
|
||||
if ($type && $r['type'] !== $type) continue;
|
||||
if ($scope && $r['scope'] !== $scope && $r['scope'] !== self::SCOPE_GLOBAL) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is banned (global).
|
||||
*/
|
||||
public function is_banned(int $user_id): bool {
|
||||
return $this->is_restricted($user_id, self::TYPE_BAN, self::SCOPE_GLOBAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active ban restriction for a user (if any).
|
||||
*/
|
||||
public function get_active_ban(int $user_id): ?array {
|
||||
$restrictions = $this->get_user_restrictions($user_id, true);
|
||||
foreach ($restrictions as $r) {
|
||||
if ($r['type'] === self::TYPE_BAN && $r['scope'] === self::SCOPE_GLOBAL) {
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce ban on login.
|
||||
*/
|
||||
public function enforce_login_ban($user, string $username, string $password) {
|
||||
if (is_wp_error($user) || !$user instanceof \WP_User) {
|
||||
return $user;
|
||||
}
|
||||
if ($this->is_banned($user->ID)) {
|
||||
$ban = $this->get_active_ban($user->ID);
|
||||
$message = '您的账号已被封禁。';
|
||||
if (!empty($ban['reason'])) {
|
||||
$message .= '原因:' . $ban['reason'];
|
||||
}
|
||||
return new \WP_Error('user_banned', $message);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce comment mute.
|
||||
*/
|
||||
public function enforce_comment_mute($post_id) {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id > 0 && $this->is_restricted($user_id, self::TYPE_MUTE, self::SCOPE_COMMENT)) {
|
||||
wp_die('您已被限制评论。', '评论限制', ['response' => 403]);
|
||||
}
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync restriction meta for quick lookup.
|
||||
*/
|
||||
private function sync_user_restriction_meta(int $user_id): void {
|
||||
$active = $this->get_user_restrictions($user_id, true);
|
||||
$types = array_unique(array_column($active, 'type'));
|
||||
update_user_meta($user_id, '_aksrt_active_restrictions', $types);
|
||||
update_user_meta($user_id, '_aksrt_is_banned', in_array(self::TYPE_BAN, $types, true) ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restriction stats for admin overview.
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
global $wpdb;
|
||||
$now = current_time('mysql', true);
|
||||
$table = $this->table();
|
||||
|
||||
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
|
||||
$active = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE expires_at IS NULL OR expires_at > %s", $now
|
||||
));
|
||||
$banned = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE type = %s AND (expires_at IS NULL OR expires_at > %s)",
|
||||
self::TYPE_BAN, $now
|
||||
));
|
||||
|
||||
return ['total' => $total, 'active' => $active, 'banned' => $banned];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user