- 手动模式读取 `aksrt_user_level` user meta;自动模式则根据当前积分余额和上方阈值表实时计算等级,不需要单独写库。
+
+
+
+ Lv.1 通常为 0。数值为升到该级所需的累计经验。修改最高等级后会自动补齐/截断行。
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ group }}
+
+
+
+
+
+
@@ -98,38 +82,78 @@
+
+
diff --git a/assets/admin/src/schema/sections/users/level.js b/assets/admin/src/schema/sections/users/level.js
index 42f96ce9..f87c8543 100644
--- a/assets/admin/src/schema/sections/users/level.js
+++ b/assets/admin/src/schema/sections/users/level.js
@@ -1,33 +1,82 @@
+/**
+ * User level + integral XP (Zibll user_level_s / user_level_opt / user_integral_opt).
+ */
+
+export const INTEGRAL_CATALOG = [
+ { key: 'sign_up', label: '首次注册', default: 20, group: '用户' },
+ { key: 'sign_in', label: '每日登录', default: 5, group: '用户' },
+ { key: 'followed', label: '被关注', default: 5, group: '用户' },
+ { key: 'post_new', label: '发布文章', default: 5, group: '文章' },
+ { key: 'post_like', label: '文章获赞', default: 1, group: '文章' },
+ { key: 'post_favorite', label: '文章被收藏', default: 2, group: '文章' },
+ { key: 'comment_new', label: '发表评论', default: 2, group: '文章' },
+ { key: 'comment_like', label: '评论获赞', default: 1, group: '文章' },
+ { key: 'bbs_posts_new', label: '发布帖子', default: 3, group: '论坛' },
+ { key: 'bbs_score_extra', label: '帖子被加分', default: 1, group: '论坛' },
+ { key: 'bbs_essence', label: '帖子评为精华', default: 2, group: '论坛' },
+ { key: 'bbs_posts_hot', label: '帖子成为热门', default: 2, group: '论坛' },
+ { key: 'bbs_plate_new', label: '创建版块', default: 2, group: '论坛' },
+ { key: 'bbs_plate_hot', label: '版块成为热门', default: 2, group: '论坛' },
+ { key: 'bbs_adopt', label: '回答被采纳', default: 2, group: '论坛' },
+ { key: 'bbs_comment_hot', label: '评论成为神评', default: 2, group: '论坛' },
+]
+
+export function defaultLevelOpt(max = 10) {
+ const opt = {}
+ for (let i = 1; i <= max; i++) {
+ // Rough Zibll-like curve: LV1=0, then grows
+ opt['upgrade_integral_' + i] = i === 1 ? 0 : Math.round(20 * Math.pow(i - 1, 1.6))
+ opt['name_' + i] = 'Lv.' + i
+ }
+ return opt
+}
+
+export function defaultIntegralOpt() {
+ const opt = { day_max: 100 }
+ for (const row of INTEGRAL_CATALOG) {
+ opt[row.key] = row.default
+ }
+ return opt
+}
+
export const levelSchema = {
key: 'users.level',
title: '用户等级',
icon: 'fas fa-level-up-alt',
- description: '配置等级展示前缀和等级阈值,当前可基于积分余额自动计算。',
+ description: '等级与经验值:最高等级、升级阈值、经验获取矩阵(对齐 Zibll user_level / user_integral)。',
defaults: {
- user_level_enabled: false,
+ user_level_s: true,
user_level_public_display: true,
- user_level_source: 'credits_balance',
user_level_prefix: 'Lv.',
- user_level_thresholds: '1|0\n2|30\n3|100\n4|300',
+ user_level_max: 10,
+ user_level_opt: defaultLevelOpt(10),
+ user_integral_opt: defaultIntegralOpt(),
+ user_level_notify: true,
},
groups: [
{
key: 'display',
title: '等级显示',
- icon: 'fas fa-signal',
fields: [
- { key: 'user_level_enabled', label: '启用等级系统', type: 'switch', default: false },
- { key: 'user_level_public_display', label: '公开页显示等级', type: 'switch', default: true, dependsOn: { key: 'user_level_enabled', value: true } },
- { key: 'user_level_prefix', label: '等级前缀', type: 'input', default: 'Lv.', dependsOn: { key: 'user_level_enabled', value: true } },
+ { key: 'user_level_s', label: '启用用户等级', type: 'switch', default: true },
+ { key: 'user_level_public_display', label: '公开页显示等级', type: 'switch', default: true, dependsOn: { key: 'user_level_s', value: true } },
+ { key: 'user_level_prefix', label: '等级前缀', type: 'input', default: 'Lv.', dependsOn: { key: 'user_level_s', value: true } },
+ { key: 'user_level_max', label: '最高等级', type: 'number', default: 10, min: 3, max: 30, dependsOn: { key: 'user_level_s', value: true } },
+ { key: 'user_level_notify', label: '升级通知', type: 'switch', default: true, dependsOn: { key: 'user_level_s', value: true } },
],
},
{
- key: 'rules',
- title: '等级规则',
- icon: 'fas fa-list-ol',
+ key: 'level_opt',
+ title: '等级升级阈值',
fields: [
- { key: 'user_level_source', label: '等级来源', type: 'radio', default: 'credits_balance', options: [{ label: '积分余额', value: 'credits_balance' }, { label: '手动 Meta', value: 'manual_meta' }], dependsOn: { key: 'user_level_enabled', value: true } },
- { key: 'user_level_thresholds', label: '等级阈值', type: 'textarea', default: '1|0\n2|30\n3|100\n4|300', help: '每行一个等级,格式:等级|所需积分,例如 3|100', dependsOn: { key: 'user_level_enabled', value: true } },
+ { key: 'user_level_opt', label: '升级经验配置', type: 'json', default: defaultLevelOpt(10), help: 'upgrade_integral_N 为升到 N 级所需累计经验' },
+ ],
+ },
+ {
+ key: 'integral',
+ title: '经验值得分',
+ fields: [
+ { key: 'user_integral_opt', label: '经验值获取', type: 'json', default: defaultIntegralOpt(), help: 'day_max + 各行为得分;对应功能未开则将分值设 0' },
],
},
],
diff --git a/functions.php b/functions.php
index 93fb92ff..ec0f108a 100644
--- a/functions.php
+++ b/functions.php
@@ -370,6 +370,9 @@ if (class_exists('AKSRT_User_Badges')) {
if (class_exists('AKSRT_User_Badges_Admin')) {
AKSRT_User_Badges_Admin::get_instance()->init();
}
+if (class_exists('AKSRT_User_Level')) {
+ AKSRT_User_Level::get_instance()->init();
+}
// WordPress 原生侧栏:支付/订单运营中心(zibll zibpay 对齐)
if (is_admin() && class_exists('AKSRT_Admin_Pay_Menus')) {
diff --git a/inc/bootstrap.php b/inc/bootstrap.php
index 2d195051..7e9bc3b7 100644
--- a/inc/bootstrap.php
+++ b/inc/bootstrap.php
@@ -221,6 +221,7 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-credits-admin.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-balance-admin.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges-admin.php';
+require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-level.php';
// 加载数据表迁移器(CMS / 积分 / 下载 token)
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php';
diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-users.php b/inc/classes/admin/schema/class-aksrt-admin-schema-users.php
index 16e91272..9eab100b 100644
--- a/inc/classes/admin/schema/class-aksrt-admin-schema-users.php
+++ b/inc/classes/admin/schema/class-aksrt-admin-schema-users.php
@@ -604,37 +604,27 @@ class AKSRT_Admin_Schema_Users {
'level' => [
'key' => 'level',
'title' => '用户等级',
- 'version' => '1.0.0',
+ 'version' => '2.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'display', 'title' => '等级显示', 'fields' => [
- ['key' => 'user_level_enabled', 'label' => '启用等级系统', 'type' => 'switch', 'default' => false],
+ ['key' => 'user_level_s', 'label' => '启用用户等级', 'type' => 'switch', 'default' => true],
+ ['key' => 'user_level_enabled', 'label' => '启用等级系统(别名)', 'type' => 'switch', 'default' => true, 'help' => '与 user_level_s 同步写入'],
['key' => 'user_level_public_display', 'label' => '公开页显示等级', 'type' => 'switch', 'default' => true],
['key' => 'user_level_prefix', 'label' => '等级前缀', 'type' => 'input', 'default' => 'Lv.'],
- ['key' => 'user_level_color', 'label' => '等级颜色', 'type' => 'color', 'default' => '#f04494'],
- ['key' => 'user_level_icon', 'label' => '等级图标', 'type' => 'icon', 'default' => 'fas fa-star'],
- ]],
- ['key' => 'rules', 'title' => '等级规则', 'fields' => [
- ['key' => 'user_level_source', 'label' => '等级来源', 'type' => 'radio', 'default' => 'credits_balance'],
- ['key' => 'user_level_thresholds', 'label' => '等级阈值', 'type' => 'textarea', 'default' => "1|0\n2|30\n3|100\n4|300"],
- ['key' => 'user_level_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 99],
- ['key' => 'user_level_decay', 'label' => '等级衰减', 'type' => 'switch', 'default' => false, 'help' => '长期不活跃自动降级'],
- ['key' => 'user_level_decay_days', 'label' => '衰减周期(天)', 'type' => 'number', 'default' => 90, 'min' => 7, 'max' => 365],
- ]],
- ['key' => 'privilege', 'title' => '等级特权', 'fields' => [
- ['key' => 'user_level_download_limit', 'label' => '下载限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户每日下载次数增加'],
- ['key' => 'user_level_upload_limit', 'label' => '上传限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户可上传更大文件'],
- ['key' => 'user_level_comment_cooldown', 'label' => '评论冷却缩短', 'type' => 'switch', 'default' => false, 'help' => '高等级用户评论间隔更短'],
- ['key' => 'user_level_custom_title', 'label' => '自定义头衔', 'type' => 'switch', 'default' => false, 'help' => '允许高等级用户自定义头衔'],
- ]],
- ['key' => 'notify', 'title' => '升级通知', 'fields' => [
+ ['key' => 'user_level_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 3, 'max' => 30],
['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true],
- ['key' => 'user_level_notify_style', 'label' => '通知方式', 'type' => 'select', 'default' => 'toast', 'options' => [
- ['label' => '弹窗提示', 'value' => 'toast'],
- ['label' => '站内信', 'value' => 'message'],
- ['label' => '两者都有', 'value' => 'both'],
+ ['key' => 'user_level_source', 'label' => '等级来源', 'type' => 'radio', 'default' => 'integral', 'options' => [
+ ['label' => '经验值', 'value' => 'integral'],
+ ['label' => '手动 Meta', 'value' => 'manual_meta'],
]],
]],
+ ['key' => 'level_opt', 'title' => '升级阈值', 'fields' => [
+ ['key' => 'user_level_opt', 'label' => '等级参数', 'type' => 'json', 'default' => [], 'help' => 'upgrade_integral_N / name_N'],
+ ]],
+ ['key' => 'integral', 'title' => '经验值得分', 'fields' => [
+ ['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'default' => [], 'help' => 'day_max + 行为得分键'],
+ ]],
],
],
'badge' => [
diff --git a/inc/classes/class-aksrt-user-home.php b/inc/classes/class-aksrt-user-home.php
index 373d4cb1..6be42551 100644
--- a/inc/classes/class-aksrt-user-home.php
+++ b/inc/classes/class-aksrt-user-home.php
@@ -279,22 +279,24 @@ class AKSRT_User_Home {
}
/**
- * @param array{source:string,thresholds:array} $level_settings
+ * @param array{source?:string,thresholds?:array} $level_settings
*/
private function get_user_level(int $user_id, array $level_settings): int {
- if (($level_settings['source'] ?? 'credits_balance') === 'manual_meta') {
+ $source = (string) ($level_settings['source'] ?? 'integral');
+ if ($source === 'manual_meta') {
return max(0, (int) get_user_meta($user_id, 'aksrt_user_level', true));
}
-
+ if (class_exists('AKSRT_User_Level')) {
+ return AKSRT_User_Level::get_level($user_id);
+ }
+ // Fallback: credits_balance thresholds (legacy) if integral class missing.
$score = function_exists('aksrt_user_credits') ? (int) aksrt_user_credits($user_id) : 0;
$level = 0;
-
foreach ((array) ($level_settings['thresholds'] ?? []) as $candidate => $threshold) {
if ($score >= (int) $threshold) {
$level = (int) $candidate;
}
}
-
return $level;
}
diff --git a/inc/classes/class-aksrt-user-level.php b/inc/classes/class-aksrt-user-level.php
new file mode 100644
index 00000000..6ba275f0
--- /dev/null
+++ b/inc/classes/class-aksrt-user-level.php
@@ -0,0 +1,379 @@
+ [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');
+ }
+}
diff --git a/inc/classes/settings/class-aksrt-settings-user-runtime.php b/inc/classes/settings/class-aksrt-settings-user-runtime.php
index 6e49c5dd..03d5e424 100644
--- a/inc/classes/settings/class-aksrt-settings-user-runtime.php
+++ b/inc/classes/settings/class-aksrt-settings-user-runtime.php
@@ -149,11 +149,12 @@ class AKSRT_Settings_User_Runtime {
'enabled' => !empty($settings['user_level_enabled']) || !empty($settings['user_level_s']),
'public_display' => !isset($settings['user_level_public_display']) || !empty($settings['user_level_public_display']),
'source' => (static function () use ($settings) {
- $v = $settings['user_level_source'] ?? 'credits_balance';
- return in_array($v, ['credits_balance', 'manual_meta', 'integral'], true) ? $v : 'credits_balance';
+ $v = $settings['user_level_source'] ?? 'integral';
+ return in_array($v, ['credits_balance', 'manual_meta', 'integral'], true) ? $v : 'integral';
})(),
'label_prefix' => $level_prefix !== '' ? $level_prefix : 'Lv.',
'thresholds' => self::parse_level_thresholds((string) ($settings['user_level_thresholds'] ?? "1|0\n2|30\n3|100\n4|300")),
+ 'max' => max(3, min(30, (int) ($settings['user_level_max'] ?? 10))),
],
'medal' => $medal,
// Alias for existing call sites that still read badge.*; dictionary is derived from medal catalog.