Rewrite /users/level to Zibll-like user_level_s/max/opt and user_integral_opt. Runtime grants XP with day_max, resolves level, hooks login/register/post/comment/like/favorite/follow. Profile signals read integral-backed levels. audit errors=0.
380 lines
11 KiB
PHP
380 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* User level + integral XP — Zibll user_level / user_integral parity.
|
|
*
|
|
* Meta:
|
|
* aksrt_level_integral int total XP
|
|
* aksrt_user_level int resolved level
|
|
* aksrt_level_integral_detail array reason => [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<string,mixed>,integral_opt:array<string,mixed>,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<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,
|
|
'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');
|
|
}
|
|
}
|