[value, integral] * aksrt_level_integral_date_detail array Y-m-d => int * * @package AKSRT */ defined('ABSPATH') || exit; class AKSRT_User_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_TODAY_FOLLOW = 'aksrt_level_integral_followed'; /** @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_sign_in'], 20, 1); add_action('publish_post', [$this, 'on_publish_post'], 20, 2); add_action('comment_post', [$this, 'on_comment_post'], 20, 3); add_action('aksrt_user_followed', [$this, 'on_followed'], 20, 2); add_action('aksrt_post_liked', [$this, 'on_post_like'], 20, 2); add_action('aksrt_comment_liked', [$this, 'on_comment_like'], 20, 2); add_action('aksrt_post_favorited', [$this, 'on_post_favorite'], 20, 2); } /** * @return array{enabled:bool,public_display:bool,prefix:string,max:int,source:string,level_opt:array,integral_opt:array,notify:bool} */ public static function settings(): array { $all = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_all() : []; $enabled = !empty($all['user_level_s']) || !empty($all['user_level_enabled']); $max = max(3, min(30, (int) ($all['user_level_max'] ?? 10))); $source = (string) ($all['user_level_source'] ?? 'integral'); if (!in_array($source, ['integral', 'manual_meta'], true)) { $source = 'integral'; } $level_opt = $all['user_level_opt'] ?? []; if (is_string($level_opt)) { $decoded = json_decode($level_opt, true); $level_opt = is_array($decoded) ? $decoded : []; } if (!is_array($level_opt)) { $level_opt = []; } $integral_opt = $all['user_integral_opt'] ?? []; if (is_string($integral_opt)) { $decoded = json_decode($integral_opt, true); $integral_opt = is_array($decoded) ? $decoded : []; } if (!is_array($integral_opt)) { $integral_opt = []; } $prefix = trim((string) ($all['user_level_prefix'] ?? 'Lv.')); return [ 'enabled' => $enabled, 'public_display' => !isset($all['user_level_public_display']) || !empty($all['user_level_public_display']), 'prefix' => $prefix !== '' ? $prefix : 'Lv.', 'max' => $max, 'source' => $source, 'level_opt' => $level_opt, 'integral_opt' => $integral_opt, 'notify' => !isset($all['user_level_notify']) || !empty($all['user_level_notify']), ]; } /** * Catalog of integral earn keys (defaults). * * @return array */ 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, 'bbs_posts_new' => 3, 'bbs_score_extra' => 1, 'bbs_essence' => 2, 'bbs_posts_hot' => 2, 'bbs_plate_new' => 2, 'bbs_plate_hot' => 2, 'bbs_adopt' => 2, 'bbs_comment_hot' => 2, ]; } public static function task_points(string $key): int { $cfg = self::settings(); $opt = $cfg['integral_opt']; $def = self::integral_defaults(); if (array_key_exists($key, $opt)) { return max(0, (int) $opt[$key]); } return max(0, (int) ($def[$key] ?? 0)); } public static function day_max(): int { $cfg = self::settings(); $opt = $cfg['integral_opt']; if (array_key_exists('day_max', $opt)) { return max(0, (int) $opt['day_max']); } return 100; } public static function get_integral(int $user_id): int { if ($user_id <= 0) { return 0; } return max(0, (int) get_user_meta($user_id, self::META_INTEGRAL, true)); } public static function get_level(int $user_id): int { if ($user_id <= 0) { return 0; } $cfg = self::settings(); if (!$cfg['enabled']) { return 0; } if ($cfg['source'] === 'manual_meta') { return max(0, (int) get_user_meta($user_id, self::META_LEVEL, true)); } return self::resolve_level(self::get_integral($user_id), $cfg); } /** * @param array{max:int,level_opt:array} $cfg */ public static function resolve_level(int $integral, ?array $cfg = null): int { $cfg = $cfg ?? self::settings(); $max = (int) $cfg['max']; $opt = $cfg['level_opt']; $level = 1; for ($i = 1; $i <= $max; $i++) { $need = (int) ($opt['upgrade_integral_' . $i] ?? ($i === 1 ? 0 : PHP_INT_MAX)); if ($integral >= $need) { $level = $i; } else { break; } } return max(0, min($max, $level)); } /** * Grant integral for a task key. Respects day_max. * * @return int|false points granted or false */ public static function add(int $user_id, string $key, int $points = 0) { if ($user_id <= 0) { return false; } $cfg = self::settings(); if (!$cfg['enabled'] || $cfg['source'] !== 'integral') { return false; } $key = sanitize_key($key); if ($key === '' || $key === 'day_max') { return false; } if ($points <= 0) { $points = self::task_points($key); } if ($points <= 0) { return false; } $day_max = self::day_max(); $today = gmdate('Y-m-d', current_time('timestamp')); $date_detail = get_user_meta($user_id, self::META_DATE_DETAIL, true); $date_detail = is_array($date_detail) ? $date_detail : []; $today_sum = max(0, (int) ($date_detail[$today] ?? 0)); if ($day_max > 0 && $today_sum >= $day_max) { return false; } if ($day_max > 0) { $points = min($points, $day_max - $today_sum); } if ($points <= 0) { return false; } $before = self::get_integral($user_id); $after = $before + $points; update_user_meta($user_id, self::META_INTEGRAL, $after); $date_detail[$today] = $today_sum + $points; // Keep last 90 days. if (count($date_detail) > 90) { krsort($date_detail); $date_detail = array_slice($date_detail, 0, 90, true); } update_user_meta($user_id, self::META_DATE_DETAIL, $date_detail); $detail = get_user_meta($user_id, self::META_DETAIL, true); $detail = is_array($detail) ? $detail : []; $prev = is_array($detail[$key] ?? null) ? $detail[$key] : ['value' => 0, 'integral' => 0]; $detail[$key] = [ 'value' => (int) ($prev['value'] ?? 0) + $points, 'integral' => (int) ($prev['integral'] ?? 0) + $points, ]; update_user_meta($user_id, self::META_DETAIL, $detail); $old_level = (int) get_user_meta($user_id, self::META_LEVEL, true); $new_level = self::resolve_level($after, $cfg); if ($new_level !== $old_level) { update_user_meta($user_id, self::META_LEVEL, $new_level); /** * @param int $user_id * @param int $new_level * @param int $old_level */ do_action('aksrt_user_level_changed', $user_id, $new_level, $old_level); } else { update_user_meta($user_id, self::META_LEVEL, $new_level); } return $points; } public function on_register(int $user_id): void { self::add($user_id, 'sign_up'); } /** * @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) { return; } // Once per calendar day for login XP. $today = gmdate('Y-m-d', current_time('timestamp')); $flag = (string) get_user_meta($user_id, '_aksrt_integral_sign_in_day', true); if ($flag === $today) { return; } $granted = self::add($user_id, 'sign_in'); if ($granted) { update_user_meta($user_id, '_aksrt_integral_sign_in_day', $today); } } public function on_sign_in(int $user_id): void { // Dedicated check-in hook can also grant if separate from login; reuse sign_in key once/day. $this->on_login('', get_userdata($user_id) ?: new WP_User($user_id)); } /** * @param int $post_id * @param WP_Post $post */ public function on_publish_post($post_id, $post): void { if (!$post instanceof WP_Post || $post->post_type !== 'post' || $post->post_status !== 'publish') { return; } // Only when transitioning to publish first time. if ((string) get_post_meta($post_id, '_aksrt_integral_post_new', true) === '1') { return; } $author = (int) $post->post_author; if ($author <= 0) { return; } if (self::add($author, 'post_new')) { update_post_meta($post_id, '_aksrt_integral_post_new', '1'); } } /** * @param int $comment_id * @param int|string $approved * @param array $commentdata */ public function on_comment_post($comment_id, $approved, $commentdata): void { if ((string) $approved !== '1' && $approved !== 1) { return; } $user_id = (int) ($commentdata['user_id'] ?? 0); if ($user_id <= 0) { $user_id = (int) get_comment($comment_id)->user_id; } if ($user_id <= 0) { return; } self::add($user_id, 'comment_new'); } /** * @param int $follower_id * @param int $followed_id */ public function on_followed($follower_id, $followed_id): void { $followed_id = (int) $followed_id; if ($followed_id <= 0) { return; } self::add($followed_id, 'followed'); } /** * @param int $post_id * @param int $user_id liker */ public function on_post_like($post_id, $user_id): void { $post = get_post((int) $post_id); if (!$post instanceof WP_Post) { return; } $author = (int) $post->post_author; if ($author <= 0 || $author === (int) $user_id) { return; } self::add($author, 'post_like'); } /** * @param int $comment_id * @param int $user_id */ public function on_comment_like($comment_id, $user_id): void { $c = get_comment((int) $comment_id); if (!$c) { return; } $author = (int) $c->user_id; if ($author <= 0 || $author === (int) $user_id) { return; } self::add($author, 'comment_like'); } /** * @param int $post_id * @param int $user_id */ public function on_post_favorite($post_id, $user_id): void { $post = get_post((int) $post_id); if (!$post instanceof WP_Post) { return; } $author = (int) $post->post_author; if ($author <= 0 || $author === (int) $user_id) { return; } self::add($author, 'post_favorite'); } }