fix(user): close level parity gaps
This commit is contained in:
@@ -71,6 +71,15 @@
|
||||
.uc-level-road__item strong { font-size: 12px; }
|
||||
.uc-level-road__item small { color: var(--muted-2-color, #8b9198); font-size: 10px; }
|
||||
|
||||
.uc-level-methods { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 10px; }
|
||||
.uc-level-methods__item { display: grid; grid-template-columns: 34px 1fr auto; align-items: center; gap: 10px; padding: 11px; border: 1px solid var(--color-line-soft, rgba(0,0,0,.06)); border-radius: 9px; }
|
||||
.uc-level-methods__item > span { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; background: rgba(245,158,11,.12); color: #d97706; }
|
||||
.uc-level-methods__item strong, .uc-level-methods__item small { display: block; }
|
||||
.uc-level-methods__item strong { font-size: 12px; }
|
||||
.uc-level-methods__item small { margin-top: 2px; color: var(--muted-2-color, #8b9198); font-size: 10px; }
|
||||
.uc-level-methods__item b { color: #7c3aed; }
|
||||
.uc-level-ledger__row time + small { display: block; margin-top: 2px; color: var(--muted-2-color, #8b9198); font-size: 10px; }
|
||||
|
||||
.uc-level-history-grid { display: grid; grid-template-columns: minmax(0,1.35fr) minmax(240px,.65fr); gap: 16px; }
|
||||
.uc-level-ledger, .uc-level-dates { display: grid; }
|
||||
.uc-level-ledger__row { display: grid; grid-template-columns: 36px 1fr auto; align-items: center; gap: 10px; padding: 11px 0; border-bottom: 1px solid var(--color-line-soft, rgba(0,0,0,.05)); }
|
||||
@@ -87,5 +96,6 @@
|
||||
@media (max-width: 767px) {
|
||||
.uc-level-hero__stats { grid-template-columns: 1fr; }
|
||||
.uc-level-history-grid { grid-template-columns: 1fr; }
|
||||
.uc-level-methods { grid-template-columns: 1fr; }
|
||||
.uc-level-road { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
}
|
||||
|
||||
@@ -72,6 +72,30 @@ $day_max = \AKSRT\User\Level::day_max();
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="uc-level-section zib-widget">
|
||||
<div class="uc-level-section__head"><h3><?php esc_html_e('获取经验值', 'aksrt'); ?></h3><span><?php esc_html_e('完成互动即可成长', 'aksrt'); ?></span></div>
|
||||
<div class="uc-level-methods">
|
||||
<?php
|
||||
$methods = \AKSRT\User\Level::integral_defaults();
|
||||
$methods['checkin'] = 0;
|
||||
$methods['invitation'] = 0;
|
||||
$methods['shop_gift'] = 0;
|
||||
$invite_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_invite_reward_settings() : [];
|
||||
$methods['invitation'] = max(0, (int) ($invite_settings['invit_reward_level'] ?? 0));
|
||||
foreach ($methods as $key => $default_value) :
|
||||
if ($key === 'day_max') continue;
|
||||
$value = in_array($key, ['checkin', 'shop_gift'], true) ? 0 : ($key === 'invitation' ? (int) $default_value : \AKSRT\User\Level::task_points($key));
|
||||
if ($value <= 0 && !in_array($key, ['checkin', 'shop_gift'], true)) continue;
|
||||
?>
|
||||
<div class="uc-level-methods__item">
|
||||
<span><i class="fas fa-bolt" aria-hidden="true"></i></span>
|
||||
<div><strong><?php echo esc_html($labels[$key] ?? $key); ?></strong><small><?php echo in_array($key, ['checkin', 'shop_gift'], true) ? esc_html__('按活动或商品配置发放', 'aksrt') : esc_html__('完成后自动计入经验', 'aksrt'); ?></small></div>
|
||||
<b><?php echo $value > 0 ? '+' . esc_html((string) $value) : esc_html__('动态', 'aksrt'); ?></b>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="uc-level-history-grid">
|
||||
<section class="uc-level-section zib-widget">
|
||||
<div class="uc-level-section__head"><h3><?php esc_html_e('经验明细', 'aksrt'); ?></h3><span><?php esc_html_e('最近 50 条', 'aksrt'); ?></span></div>
|
||||
@@ -83,7 +107,7 @@ $day_max = \AKSRT\User\Level::day_max();
|
||||
?>
|
||||
<div class="uc-level-ledger__row">
|
||||
<span class="uc-level-ledger__icon"><i class="fas fa-chart-line" aria-hidden="true"></i></span>
|
||||
<div><strong><?php echo esc_html($labels[$key] ?? $key); ?></strong><time><?php echo esc_html((string) ($row['time'] ?? '')); ?></time></div>
|
||||
<div><strong><?php echo esc_html($labels[$key] ?? $key); ?></strong><time><?php echo esc_html((string) ($row['time'] ?? '')); ?></time><small><?php printf(esc_html__('累计:%d', 'aksrt'), max(0, (int) ($row['integral'] ?? 0))); ?></small></div>
|
||||
<b>+<?php echo esc_html((string) $value); ?></b>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -9,6 +9,27 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class SchemaUsers {
|
||||
|
||||
private static function default_level_options(): array {
|
||||
$options = [];
|
||||
for ($level = 1; $level <= 10; $level++) {
|
||||
$options['name_' . $level] = 'LV' . $level;
|
||||
$options['icon_img_' . $level] = '';
|
||||
$options['upgrade_integral_' . $level] = $level === 1 ? 0 : ($level - 1) * 500 * $level;
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private static function default_integral_options(): 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 build(): array {
|
||||
return [
|
||||
'domain' => 'users',
|
||||
@@ -576,7 +597,6 @@ class SchemaUsers {
|
||||
['key' => 'user_privacy_comments', 'label' => '隐藏评论', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_favorites', 'label' => '隐藏收藏', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_credits', 'label' => '隐藏积分', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_level', 'label' => '隐藏等级', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_badge', 'label' => '隐藏徽章', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_online', 'label' => '隐藏在线状态', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_last_active', 'label' => '隐藏最后活跃', 'type' => 'switch', 'default' => false],
|
||||
@@ -621,10 +641,10 @@ class SchemaUsers {
|
||||
['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true],
|
||||
]],
|
||||
['key' => 'level_opt', 'title' => '升级阈值', 'fields' => [
|
||||
['key' => 'user_level_opt', 'label' => '等级参数', 'type' => 'json', 'sanitize_type' => 'array', 'default' => [], 'help' => 'name_N / icon_img_N / upgrade_integral_N'],
|
||||
['key' => 'user_level_opt', 'label' => '等级参数', 'type' => 'json', 'sanitize_type' => 'array', 'default' => self::default_level_options(), 'help' => 'name_N / icon_img_N / upgrade_integral_N'],
|
||||
]],
|
||||
['key' => 'integral', 'title' => '经验值得分', 'fields' => [
|
||||
['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'sanitize_type' => 'array', 'default' => [], 'help' => 'day_max + 行为得分键'],
|
||||
['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'sanitize_type' => 'array', 'default' => self::default_integral_options(), 'help' => 'day_max + 行为得分键'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -190,14 +190,27 @@ class Invite {
|
||||
private function reward_pair(int $inviter_id, int $invitee_id): void {
|
||||
if (!class_exists('AKSRT\\Credits\\Credits') || !class_exists('\AKSRT\Settings\Settings')) return;
|
||||
|
||||
$invite_cfg = \AKSRT\Settings\Settings::get_invite_reward_settings();
|
||||
if (empty($invite_cfg['enabled'])) return;
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('invite');
|
||||
if (!$table) return;
|
||||
$claimed = $wpdb->update(
|
||||
$table,
|
||||
['status' => 'rewarding'],
|
||||
['inviter_id' => $inviter_id, 'invitee_id' => $invitee_id, 'status' => 'verified'],
|
||||
['%s'],
|
||||
['%d', '%d', '%s']
|
||||
);
|
||||
if ($claimed !== 1) return;
|
||||
|
||||
// 反作弊:同 IP 当日上限
|
||||
if (!$this->ip_daily_check($invite_cfg)) return;
|
||||
$invite_cfg = \AKSRT\Settings\Settings::get_invite_reward_settings();
|
||||
if (empty($invite_cfg['enabled']) || !$this->ip_daily_check($invite_cfg)) {
|
||||
$wpdb->update($table, ['status' => 'verified'], ['inviter_id' => $inviter_id, 'invitee_id' => $invitee_id], ['%s'], ['%d', '%d']);
|
||||
return;
|
||||
}
|
||||
|
||||
$inviter_amount = (int) ($invite_cfg['inviter_amount'] ?? 0);
|
||||
$invitee_amount = (int) ($invite_cfg['invitee_amount'] ?? 0);
|
||||
$level_integral = max(0, (int) ($invite_cfg['invit_reward_level'] ?? 0));
|
||||
|
||||
if ($inviter_amount > 0) {
|
||||
Credits::add($inviter_id, $inviter_amount, 'invite', ['type' => 'user', 'id' => $invitee_id]);
|
||||
@@ -205,12 +218,12 @@ class Invite {
|
||||
if ($invitee_amount > 0) {
|
||||
Credits::add($invitee_id, $invitee_amount, 'invitee', ['type' => 'user', 'id' => $inviter_id]);
|
||||
}
|
||||
if ($level_integral > 0 && class_exists('\AKSRT\User\Level')) {
|
||||
\AKSRT\User\Level::add($inviter_id, 'invitation', $level_integral, true);
|
||||
}
|
||||
|
||||
// 标记 rewarded
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('invite');
|
||||
if ($table) {
|
||||
$wpdb->update(
|
||||
$wpdb->update(
|
||||
$table,
|
||||
[
|
||||
'status' => 'rewarded',
|
||||
@@ -220,10 +233,9 @@ class Invite {
|
||||
'inviter_id' => $inviter_id,
|
||||
'invitee_id' => $invitee_id,
|
||||
],
|
||||
['%s', '%s'],
|
||||
['%d', '%d']
|
||||
);
|
||||
}
|
||||
['%s', '%s'],
|
||||
['%d', '%d']
|
||||
);
|
||||
}
|
||||
|
||||
/** 同 IP 当日邀请兑现次数限制 */
|
||||
|
||||
@@ -139,7 +139,7 @@ class Signin {
|
||||
return new \WP_Error('aksrt_signin_dup', __('签到失败', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
|
||||
do_action('aksrt_user_signed_in', $user_id);
|
||||
do_action('aksrt_user_signed_in', $user_id, $reward);
|
||||
|
||||
if ($reward > 0) {
|
||||
if (class_exists('AKSRT\\Credits\\PointsFree')) {
|
||||
|
||||
@@ -367,6 +367,8 @@ trait Part3 {
|
||||
private function process_gifts(string $order_number, array $order, array $items): void {
|
||||
$user_id = (int) ($order['user_id'] ?? 0);
|
||||
if ($user_id <= 0) return;
|
||||
$claim_key = '_aksrt_shop_gifts_' . sanitize_key($order_number);
|
||||
if (!add_user_meta($user_id, $claim_key, 'processing', true)) return;
|
||||
|
||||
foreach ($items as $item) {
|
||||
$gifts = $item['gift_data'] ?? [];
|
||||
@@ -378,6 +380,8 @@ trait Part3 {
|
||||
|
||||
if ($gift_type === 'credits' && class_exists('\AKSRT\Credits\Credits') && (int) $gift_value > 0) {
|
||||
\AKSRT\Credits\Credits::add((int) $gift_value, $user_id, 'shop_gift', sprintf(__('订单 %s 赠品', 'aksrt'), $order_number));
|
||||
} elseif (in_array($gift_type, ['level_integral', 'experience'], true) && class_exists('\AKSRT\User\Level') && (int) $gift_value > 0) {
|
||||
\AKSRT\User\Level::add($user_id, 'shop_gift', (int) $gift_value, true);
|
||||
} elseif ($gift_type === 'vip_days' && (int) $gift_value > 0) {
|
||||
$current = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
|
||||
$base = max($current, time());
|
||||
@@ -390,5 +394,6 @@ trait Part3 {
|
||||
}
|
||||
}
|
||||
}
|
||||
update_user_meta($user_id, $claim_key, '1');
|
||||
}
|
||||
}
|
||||
|
||||
+38
-18
@@ -31,7 +31,7 @@ class Level {
|
||||
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, 1);
|
||||
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);
|
||||
@@ -101,6 +101,9 @@ class Level {
|
||||
return [
|
||||
'sign_up' => __('首次注册', 'aksrt'),
|
||||
'sign_in' => __('每日登录', 'aksrt'),
|
||||
'checkin' => __('每日签到', 'aksrt'),
|
||||
'invitation' => __('邀请奖励', 'aksrt'),
|
||||
'shop_gift' => __('购物赠送', 'aksrt'),
|
||||
'followed' => __('被关注', 'aksrt'),
|
||||
'post_new' => __('发布文章', 'aksrt'),
|
||||
'post_like' => __('文章获赞', 'aksrt'),
|
||||
@@ -134,10 +137,16 @@ class Level {
|
||||
}
|
||||
|
||||
public static function get_level(int $user_id): int {
|
||||
if ($user_id <= 0 || !self::settings()['enabled']) {
|
||||
$cfg = self::settings();
|
||||
if ($user_id <= 0 || !$cfg['enabled']) {
|
||||
return 0;
|
||||
}
|
||||
return self::resolve_level(self::get_integral($user_id));
|
||||
$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 {
|
||||
@@ -194,7 +203,7 @@ class Level {
|
||||
public static function get_progress(int $user_id): array {
|
||||
$cfg = self::settings();
|
||||
$integral = self::get_integral($user_id);
|
||||
$level = self::resolve_level($integral, $cfg);
|
||||
$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))));
|
||||
@@ -252,6 +261,9 @@ class Level {
|
||||
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;
|
||||
@@ -261,11 +273,8 @@ class Level {
|
||||
$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) {
|
||||
if ($today_total >= $day_max) {
|
||||
return false;
|
||||
}
|
||||
$points = min($points, $day_max - $today_total);
|
||||
if (!$bypass_day_max && $day_max > 0 && $today_total >= $day_max) {
|
||||
return false;
|
||||
}
|
||||
if ($points <= 0) {
|
||||
return false;
|
||||
@@ -289,7 +298,11 @@ class Level {
|
||||
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 = self::resolve_level($after, $cfg);
|
||||
$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);
|
||||
@@ -368,8 +381,10 @@ class Level {
|
||||
}
|
||||
}
|
||||
|
||||
public function on_checkin(int $user_id): void {
|
||||
$this->on_login('', get_userdata($user_id));
|
||||
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 {
|
||||
@@ -407,11 +422,7 @@ class Level {
|
||||
if (!$comment instanceof \WP_Comment || (int) $comment->user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
$post = get_post((int) $comment->comment_post_ID);
|
||||
$key = $post instanceof \WP_Post && $post->post_type === 'aksrt_forum_topic' ? 'bbs_posts_new' : 'comment_new';
|
||||
if ($key === 'comment_new') {
|
||||
self::award_once((int) $comment->user_id, $key, 'comment', (int) $comment->comment_ID, '_aksrt_level_comment_new');
|
||||
}
|
||||
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 {
|
||||
@@ -471,7 +482,16 @@ class Level {
|
||||
|
||||
public function on_forum_scored($topic_id, $author_id, $amount): void {
|
||||
if ((float) $amount > 0) {
|
||||
self::award_once((int) $author_id, 'bbs_score_extra', 'post', (int) $topic_id, '_aksrt_level_bbs_score_extra', 5);
|
||||
$score_detail = get_post_meta((int) $topic_id, '_aksrt_score_detail', true);
|
||||
$score_detail = is_array($score_detail) ? $score_detail : [];
|
||||
$actor_id = 0;
|
||||
foreach (array_reverse($score_detail, true) as $candidate => $entry) {
|
||||
if ((float) ($entry['amount'] ?? 0) === (float) $amount) {
|
||||
$actor_id = (int) $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
self::award_once((int) $author_id, 'bbs_score_extra', 'post', (int) $topic_id, '_aksrt_level_bbs_score_extra', 5, $actor_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,11 +109,17 @@ class LevelAdmin {
|
||||
</tr>
|
||||
<?php if ($can_manage) : ?>
|
||||
<tr>
|
||||
<th><label for="aksrt_level_integral"><?php esc_html_e('累计经验值', 'aksrt'); ?></label></th>
|
||||
<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']); ?>">
|
||||
<p class="description"><?php esc_html_e('保存后按当前等级阈值重新计算等级。直接替换当前经验,不追加经验流水。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
@@ -130,7 +136,8 @@ class LevelAdmin {
|
||||
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::resolve_level($integral));
|
||||
update_user_meta($user_id, Level::META_LEVEL, $level);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user