feat(user): complete growth level chain
@@ -33,9 +33,6 @@
|
||||
<n-form-item v-if="form.user_level_s" label="公开页显示等级">
|
||||
<n-switch v-model:value="form.user_level_public_display" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="form.user_level_s" label="等级前缀">
|
||||
<n-input v-model:value="form.user_level_prefix" maxlength="12" style="width: 180px;" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="form.user_level_s" label="最高等级">
|
||||
<n-input-number v-model:value="form.user_level_max" :min="3" :max="30" style="width: 180px;" @update:value="onMaxChange" />
|
||||
</n-form-item>
|
||||
@@ -101,7 +98,6 @@ const saving = ref(false)
|
||||
const form = reactive({
|
||||
user_level_s: true,
|
||||
user_level_public_display: true,
|
||||
user_level_prefix: 'Lv.',
|
||||
user_level_max: 10,
|
||||
user_level_opt: defaultLevelOpt(10),
|
||||
user_integral_opt: defaultIntegralOpt(),
|
||||
@@ -116,7 +112,7 @@ const levelRows = computed(() => {
|
||||
for (let i = 1; i <= max; i++) {
|
||||
rows.push({
|
||||
level: i,
|
||||
name: form.user_level_opt['name_' + i] ?? form.user_level_prefix + i,
|
||||
name: form.user_level_opt['name_' + i] ?? 'LV' + i,
|
||||
upgrade: Number(form.user_level_opt['upgrade_integral_' + i] ?? 0),
|
||||
})
|
||||
}
|
||||
@@ -137,6 +133,19 @@ const levelColumns = [
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '等级图标 URL',
|
||||
key: 'icon',
|
||||
render: (row) =>
|
||||
h(NInput, {
|
||||
value: form.user_level_opt['icon_img_' + row.level] ?? '',
|
||||
size: 'small',
|
||||
placeholder: row.level <= 10 ? '留空使用主题内置图标' : '请输入图片 URL',
|
||||
onUpdateValue: (v) => {
|
||||
form.user_level_opt['icon_img_' + row.level] = v
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '升级所需累计经验',
|
||||
key: 'upgrade',
|
||||
@@ -173,7 +182,10 @@ function onMaxChange(val) {
|
||||
next['upgrade_integral_' + i] = filled['upgrade_integral_' + i]
|
||||
}
|
||||
if (next['name_' + i] === undefined) {
|
||||
next['name_' + i] = form.user_level_prefix + i
|
||||
next['name_' + i] = 'LV' + i
|
||||
}
|
||||
if (next['icon_img_' + i] === undefined) {
|
||||
next['icon_img_' + i] = ''
|
||||
}
|
||||
}
|
||||
// drop extras
|
||||
@@ -214,6 +226,9 @@ function normalizeLevelOpt(raw, max) {
|
||||
if (raw['name_' + i] !== undefined) {
|
||||
base['name_' + i] = String(raw['name_' + i] || '')
|
||||
}
|
||||
if (raw['icon_img_' + i] !== undefined) {
|
||||
base['icon_img_' + i] = String(raw['icon_img_' + i] || '')
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
@@ -223,10 +238,9 @@ async function load() {
|
||||
try {
|
||||
const schemaRes = await api.getSchemaPage('users', 'level')
|
||||
const d = extractData(schemaRes)
|
||||
const enabled = d.user_level_s !== undefined ? !!d.user_level_s : !!d.user_level_enabled
|
||||
const enabled = d.user_level_s !== false
|
||||
form.user_level_s = enabled
|
||||
form.user_level_public_display = d.user_level_public_display !== false
|
||||
form.user_level_prefix = d.user_level_prefix || 'Lv.'
|
||||
form.user_level_max = Math.max(3, Math.min(30, Number(d.user_level_max || 10)))
|
||||
form.user_level_notify = d.user_level_notify !== false
|
||||
form.user_level_opt = normalizeLevelOpt(d.user_level_opt, form.user_level_max)
|
||||
@@ -244,12 +258,9 @@ async function handleSave() {
|
||||
const max = Math.max(3, Math.min(30, Number(form.user_level_max) || 10))
|
||||
const payload = {
|
||||
user_level_s: !!form.user_level_s,
|
||||
user_level_enabled: !!form.user_level_s,
|
||||
user_level_public_display: !!form.user_level_public_display,
|
||||
user_level_prefix: form.user_level_prefix || 'Lv.',
|
||||
user_level_max: max,
|
||||
user_level_notify: !!form.user_level_notify,
|
||||
user_level_source: 'integral',
|
||||
user_level_opt: normalizeLevelOpt(form.user_level_opt, max),
|
||||
user_integral_opt: normalizeIntegral(form.user_integral_opt),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export const featuresSchema = {
|
||||
icon: 'fas fa-user-cog',
|
||||
description: '配置用户中心和个人主页的功能模块。',
|
||||
defaults: {
|
||||
user_center_tabs: 'profile,posts,comments,favorites,vip,credits,auth',
|
||||
user_center_tabs: 'profile,level,posts,comments,favorites,vip,credits,auth',
|
||||
user_cover_upload: true,
|
||||
user_social_links: true,
|
||||
user_desc_std: '这家伙很懒…',
|
||||
@@ -24,7 +24,7 @@ export const featuresSchema = {
|
||||
title: '用户中心',
|
||||
icon: 'fas fa-user-shield',
|
||||
fields: [
|
||||
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,posts,comments,favorites,vip,credits,auth', help: '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入' },
|
||||
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,level,posts,comments,favorites,vip,credits,auth', help: '逗号分隔。level=我的等级;auth=官方认证;等级、VIP、订单和认证会按功能开关自动注入' },
|
||||
{ key: 'user_cover_upload', label: '封面上传', type: 'switch', default: true },
|
||||
{ key: 'user_social_links', label: '社交链接', type: 'switch', default: true },
|
||||
{ key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' },
|
||||
|
||||
@@ -24,9 +24,9 @@ export const INTEGRAL_CATALOG = [
|
||||
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
|
||||
opt['upgrade_integral_' + i] = i === 1 ? 0 : (i - 1) * 500 * i
|
||||
opt['name_' + i] = 'LV' + i
|
||||
opt['icon_img_' + i] = ''
|
||||
}
|
||||
return opt
|
||||
}
|
||||
@@ -47,7 +47,6 @@ export const levelSchema = {
|
||||
defaults: {
|
||||
user_level_s: true,
|
||||
user_level_public_display: true,
|
||||
user_level_prefix: 'Lv.',
|
||||
user_level_max: 10,
|
||||
user_level_opt: defaultLevelOpt(10),
|
||||
user_integral_opt: defaultIntegralOpt(),
|
||||
@@ -60,7 +59,6 @@ export const levelSchema = {
|
||||
fields: [
|
||||
{ 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 } },
|
||||
],
|
||||
@@ -69,14 +67,14 @@ export const levelSchema = {
|
||||
key: 'level_opt',
|
||||
title: '等级升级阈值',
|
||||
fields: [
|
||||
{ key: 'user_level_opt', label: '升级经验配置', type: 'json', default: defaultLevelOpt(10), help: 'upgrade_integral_N 为升到 N 级所需累计经验' },
|
||||
{ key: 'user_level_opt', label: '升级经验配置', type: 'json', sanitizeType: 'array', 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' },
|
||||
{ key: 'user_integral_opt', label: '经验值获取', type: 'json', sanitizeType: 'array', default: defaultIntegralOpt(), help: 'day_max + 各行为得分;对应功能未开则将分值设 0' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -535,6 +535,14 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-user-level-icon {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
height: 1.15em;
|
||||
max-width: 6em;
|
||||
object-fit: contain;
|
||||
vertical-align: -.18em;
|
||||
}
|
||||
|
||||
/* 用户中心 layout 间距;双栏 / sticky 见 user-center/shell.css(body.aksrt-user-center)。 */
|
||||
.aksrt-profile__layout {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import url("user-center/shell.css");
|
||||
@import url("user-center/credits.css");
|
||||
@import url("user-center/level.css");
|
||||
@import url("user-center/settings.css");
|
||||
@import url("user-center/responsive.css");
|
||||
|
||||
@@ -1031,38 +1031,6 @@ body.uc-modal-open {
|
||||
.uc-badges-empty { padding: 16px 0; text-align: center; }
|
||||
|
||||
|
||||
/* Level / integral card */
|
||||
.uc-level-card {
|
||||
margin: 16px 0;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(240,68,148,.12), rgba(99,102,241,.10));
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.uc-level-card__badge { margin-top: 12px; }
|
||||
.uc-level-card .progress,
|
||||
.integral-progress.progress {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(127,127,127,.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
.uc-level-card .progress-bar {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #f04494, #6366f1);
|
||||
}
|
||||
.uc-level-growth__row {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr auto;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid rgba(127,127,127,.12);
|
||||
font-size: 12px;
|
||||
}
|
||||
.uc-level-growth__row.is-done { opacity: .7; }
|
||||
.uc-level-growth__row.is-next { font-weight: 700; color: var(--focus-color, #f04494); }
|
||||
.uc-points-tasks__row.is-done { opacity: .72; }
|
||||
.uc-points-tasks__progress-bar {
|
||||
height: 6px;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/* User level — dedicated center panel */
|
||||
.aksrt-user-level-icon {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
height: 1.35em;
|
||||
max-width: 6em;
|
||||
object-fit: contain;
|
||||
vertical-align: -.2em;
|
||||
}
|
||||
.aksrt-user-level-icon--admin { height: 22px; }
|
||||
|
||||
.uc-level-page { display: grid; gap: 16px; }
|
||||
.uc-level-page .zib-widget {
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--color-line-soft, rgba(0,0,0,.06));
|
||||
border-radius: var(--main-radius, 10px);
|
||||
background: var(--main-bg-color, var(--surface-1, #fff));
|
||||
box-shadow: 0 0 10px var(--main-shadow, rgba(116,116,116,.08));
|
||||
}
|
||||
.uc-level-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, rgba(124,58,237,.12), rgba(236,72,153,.10)) !important;
|
||||
}
|
||||
.uc-level-hero::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
right: -80px;
|
||||
top: -100px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,.2);
|
||||
pointer-events: none;
|
||||
}
|
||||
.uc-level-hero__identity { display: flex; align-items: center; gap: 14px; position: relative; z-index: 1; }
|
||||
.uc-level-hero__identity p { margin: 0 0 3px; color: var(--muted-2-color, #8b9198); font-size: 12px; }
|
||||
.uc-level-hero__identity h2 { margin: 0; font-size: 22px; }
|
||||
.uc-level-hero__icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,.62);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,.55);
|
||||
}
|
||||
.uc-level-hero__icon .aksrt-user-level-icon { max-width: 48px; height: 42px; }
|
||||
.uc-level-hero__stats { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 10px; margin: 20px 0 16px; position: relative; z-index: 1; }
|
||||
.uc-level-hero__stats > div { padding: 12px; border-radius: 10px; background: rgba(255,255,255,.46); }
|
||||
.uc-level-hero__stats strong, .uc-level-hero__stats span { display: block; }
|
||||
.uc-level-hero__stats strong { font-size: 18px; }
|
||||
.uc-level-hero__stats span { margin-top: 3px; font-size: 11px; color: var(--muted-2-color, #747b83); }
|
||||
.uc-level-hero__progress { position: relative; z-index: 1; }
|
||||
.uc-level-hero__progress-meta { display: flex; justify-content: space-between; margin-bottom: 7px; font-size: 12px; font-weight: 700; }
|
||||
.uc-level-progress { height: 9px; overflow: hidden; border-radius: 999px; background: rgba(127,127,127,.18); }
|
||||
.uc-level-progress > span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #7c3aed, #ec4899); }
|
||||
|
||||
.uc-level-section__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||
.uc-level-section__head h3 { margin: 0; font-size: 16px; }
|
||||
.uc-level-section__head span { color: var(--muted-2-color, #8b9198); font-size: 11px; }
|
||||
.uc-level-road { display: grid; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); gap: 10px; }
|
||||
.uc-level-road__item { position: relative; display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 14px 8px; border: 1px solid var(--color-line-soft, rgba(0,0,0,.06)); border-radius: 10px; opacity: .58; text-align: center; }
|
||||
.uc-level-road__item.is-done { opacity: .78; }
|
||||
.uc-level-road__item.is-current { opacity: 1; border-color: #8b5cf6; box-shadow: 0 0 0 2px rgba(139,92,246,.12); }
|
||||
.uc-level-road__item.is-current::after { content: '当前'; position: absolute; top: 6px; right: 6px; padding: 1px 5px; border-radius: 999px; background: #8b5cf6; color: #fff; font-size: 9px; }
|
||||
.uc-level-road__icon { display: grid; place-items: center; height: 40px; }
|
||||
.uc-level-road__icon img { max-width: 52px; max-height: 38px; object-fit: contain; }
|
||||
.uc-level-road__icon span { font-weight: 800; color: #7c3aed; }
|
||||
.uc-level-road__item strong { font-size: 12px; }
|
||||
.uc-level-road__item small { 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)); }
|
||||
.uc-level-ledger__row:last-child, .uc-level-dates > div:last-child { border-bottom: 0; }
|
||||
.uc-level-ledger__icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 9px; background: rgba(124,58,237,.10); color: #7c3aed; }
|
||||
.uc-level-ledger__row strong, .uc-level-ledger__row time { display: block; }
|
||||
.uc-level-ledger__row strong { font-size: 13px; }
|
||||
.uc-level-ledger__row time { margin-top: 2px; color: var(--muted-2-color, #8b9198); font-size: 10px; }
|
||||
.uc-level-ledger__row b { color: #16a34a; font-size: 13px; }
|
||||
.uc-level-dates > div { display: flex; justify-content: space-between; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--color-line-soft, rgba(0,0,0,.05)); font-size: 12px; }
|
||||
.uc-level-dates span { color: #16a34a; font-weight: 700; }
|
||||
.uc-level-empty { margin: 0; padding: 24px 0; color: var(--muted-2-color, #8b9198); text-align: center; }
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.uc-level-hero__stats { grid-template-columns: 1fr; }
|
||||
.uc-level-history-grid { grid-template-columns: 1fr; }
|
||||
.uc-level-road { grid-template-columns: repeat(2, minmax(0,1fr)); }
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -444,6 +444,9 @@ if (class_exists('\AKSRT\User\BadgesAdmin')) {
|
||||
if (class_exists('\AKSRT\User\Level')) {
|
||||
\AKSRT\User\Level::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\User\LevelAdmin')) {
|
||||
\AKSRT\User\LevelAdmin::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\User\Metrics')) {
|
||||
\AKSRT\User\Metrics::get_instance()->init();
|
||||
}
|
||||
|
||||
@@ -159,7 +159,11 @@ if (!function_exists('aksrt_get_user_name')) {
|
||||
if ($type === 'verify' && !empty($args['auth']) && $auth_html === '') {
|
||||
$auth_html = '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--success" title="' . esc_attr($title) . '"><i class="fa fa-check-circle" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
|
||||
} elseif ($type === 'level' && !empty($args['level']) && $level_html === '') {
|
||||
$level_html = '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--info" title="' . esc_attr($title) . '"><i class="fa fa-layer-group" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
|
||||
if ($icon_url !== '') {
|
||||
$level_html = '<img class="aksrt-user-level-icon img-icon ml3" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
|
||||
} else {
|
||||
$level_html = '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--info" title="' . esc_attr($title) . '"><i class="fa fa-layer-group" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
|
||||
}
|
||||
} elseif ($type === 'badge' && !empty($args['medal']) && $medal_html === '' && !empty($signal['wear'])) {
|
||||
if ($icon_url !== '') {
|
||||
$medal_html = '<img class="medal-icon img-icon ml3 aksrt-identity-medal is-wear" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
|
||||
|
||||
@@ -409,7 +409,7 @@ if (!function_exists('aksrt_resolve_allow_view_rule')) :
|
||||
$ok = true;
|
||||
}
|
||||
if (!$ok && $need_level > 0) {
|
||||
$user_level = (int) get_user_meta($user_id, 'aksrt_user_level', true);
|
||||
$user_level = class_exists('\AKSRT\User\Level') ? \AKSRT\User\Level::get_level($user_id) : 0;
|
||||
if ($user_level >= $need_level) {
|
||||
$ok = true;
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ if (!function_exists('aksrt_capture_user_center_view')) {
|
||||
function aksrt_capture_user_center_view(string $view, array $context): string {
|
||||
// Built-in panels map to renderer files; "profile" content lives in profile-form.php.
|
||||
$map = [
|
||||
'level' => 'level',
|
||||
'profile' => 'form',
|
||||
'posts' => 'posts',
|
||||
'comments' => 'comments',
|
||||
@@ -173,7 +174,7 @@ if (!function_exists('aksrt_capture_user_center_panel')) {
|
||||
* Capture a built-in or registered user-center panel body.
|
||||
*/
|
||||
function aksrt_capture_user_center_panel(string $tab_key, array $tab, array $context): string {
|
||||
$built_in_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'settings'];
|
||||
$built_in_tabs = ['profile', 'level', 'posts', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'settings'];
|
||||
if (in_array($tab_key, $built_in_tabs, true)) {
|
||||
return aksrt_capture_user_center_view($tab_key, $context);
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* User center: my level + integral growth (Zibll level card parity).
|
||||
*
|
||||
* Expects $user_id from profile-credits context when required.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!class_exists('\AKSRT\User\Level')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cfg = \AKSRT\User\Level::settings();
|
||||
if (empty($cfg['enabled'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = (int) ($user_id ?? get_current_user_id());
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$level = \AKSRT\User\Level::get_level($user_id);
|
||||
$integral = \AKSRT\User\Level::get_integral($user_id);
|
||||
$max = max(1, (int) ($cfg['max'] ?? 10));
|
||||
$prefix = (string) ($cfg['prefix'] ?? 'Lv.');
|
||||
$opt = is_array($cfg['level_opt'] ?? null) ? $cfg['level_opt'] : [];
|
||||
$name = (string) ($opt['name_' . $level] ?? ($prefix . $level));
|
||||
|
||||
$next_need = 0;
|
||||
$is_max = $level >= $max;
|
||||
if (!$is_max) {
|
||||
$next_need = (int) ($opt['upgrade_integral_' . ($level + 1)] ?? 0);
|
||||
if ($next_need <= $integral) {
|
||||
// Resolve again in case meta lag.
|
||||
$level = \AKSRT\User\Level::resolve_level($integral, $cfg);
|
||||
$is_max = $level >= $max;
|
||||
$next_need = $is_max ? $integral : (int) ($opt['upgrade_integral_' . ($level + 1)] ?? $integral);
|
||||
$name = (string) ($opt['name_' . $level] ?? ($prefix . $level));
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_max) {
|
||||
$proportion = 100;
|
||||
$need_html = esc_html__('已为最高等级', 'aksrt');
|
||||
$frac_html = esc_html((string) $integral) . '/' . esc_html((string) $integral);
|
||||
} else {
|
||||
$den = max(1, $next_need);
|
||||
$proportion = (int) min(100, max(0, ($integral / $den) * 100));
|
||||
$need_left = max(0, $next_need - $integral);
|
||||
$need_html = sprintf(
|
||||
/* translators: %s: remaining XP */
|
||||
esc_html__('升级还需%s经验', 'aksrt'),
|
||||
'<span class="em14 font-bold ml3 mr3">' . esc_html((string) $need_left) . '</span>'
|
||||
);
|
||||
$frac_html = esc_html((string) $integral) . '/' . esc_html((string) $next_need);
|
||||
}
|
||||
|
||||
$avatar = function_exists('aksrt_get_avatar_html')
|
||||
? aksrt_get_avatar_html($user_id, 42, ['class' => 'avatar avatar-mini'])
|
||||
: get_avatar($user_id, 42);
|
||||
|
||||
// Growth rows (next few levels).
|
||||
$growth = '';
|
||||
$from = max(1, $level);
|
||||
$to = min($max, $level + 5);
|
||||
for ($i = $from; $i <= $to; $i++) {
|
||||
$need_i = (int) ($opt['upgrade_integral_' . $i] ?? 0);
|
||||
$nm = (string) ($opt['name_' . $i] ?? ($prefix . $i));
|
||||
$state = $integral >= $need_i ? 'is-done' : ($i === $level + 1 ? 'is-next' : '');
|
||||
$growth .= '<div class="uc-level-growth__row ' . esc_attr($state) . '">';
|
||||
$growth .= '<span class="uc-level-growth__lv">' . esc_html($prefix . $i) . '</span>';
|
||||
$growth .= '<span class="uc-level-growth__name">' . esc_html($nm) . '</span>';
|
||||
$growth .= '<span class="uc-level-growth__need">' . esc_html((string) $need_i) . '</span>';
|
||||
$growth .= '</div>';
|
||||
}
|
||||
?>
|
||||
<div class="uc-level-card zib-widget">
|
||||
<div class="uc-level-card__head flex ac">
|
||||
<div class="avatar-mini mr10"><?php echo $avatar; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
||||
<div class="font-bold"><?php esc_html_e('我的等级', 'aksrt'); ?></div>
|
||||
</div>
|
||||
<div class="uc-level-card__body">
|
||||
<div class="uc-level-card__badge flex ac em14">
|
||||
<span class="badg badg-sm mr10 aksrt-identity-chip aksrt-identity-chip--info"><?php echo esc_html($prefix . $level); ?></span>
|
||||
<span class="font-bold"><?php echo esc_html($name); ?></span>
|
||||
</div>
|
||||
<div class="uc-level-card__progress mt10">
|
||||
<div class="flex jsb mb6 ab">
|
||||
<div class="em12 font-bold"><?php echo $frac_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
||||
<div class="opacity8 em09"><?php echo $need_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
||||
</div>
|
||||
<div class="integral-progress progress">
|
||||
<div class="progress-bar" style="width: <?php echo (int) $proportion; ?>%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($growth !== '') : ?>
|
||||
<div class="uc-level-growth mt12">
|
||||
<div class="uc-level-growth__title muted-2-color em09 mb6"><?php esc_html_e('成长路线', 'aksrt'); ?></div>
|
||||
<?php echo $growth; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -54,7 +54,7 @@ $can_apply = !empty($settings['enabled']) && !empty($settings['allow_apply']) &&
|
||||
<?php if (empty($settings['allow_apply'])) : ?>
|
||||
<div class="uc-auth-card__notice"><?php echo wp_kses_post(nl2br(esc_html((string) ($settings['disable_desc'] ?? __('暂时不能申请身份认证', 'aksrt'))))); ?></div>
|
||||
<?php elseif ($blocked_level) : ?>
|
||||
<div class="uc-auth-card__notice"><?php printf(esc_html__('达到 LV.%d 后即可申请', 'aksrt'), $limit); ?></div>
|
||||
<div class="uc-auth-card__notice"><?php printf(esc_html__('达到 %s 后即可申请', 'aksrt'), esc_html(class_exists('\AKSRT\User\Level') ? \AKSRT\User\Level::get_level_name($limit) : ('LV' . $limit))); ?></div>
|
||||
<?php elseif (in_array($status, ['rejected', 'cancelled'], true) && $reason !== '') : ?>
|
||||
<div class="uc-auth-card__notice">
|
||||
<strong><?php echo $status === 'cancelled' ? esc_html__('认证已取消', 'aksrt') : esc_html__('上次申请未通过', 'aksrt'); ?></strong><br>
|
||||
|
||||
@@ -52,9 +52,6 @@ if (!empty($show_vip)) {
|
||||
if ($show_credits) {
|
||||
require $dir . '/credits-signin.php';
|
||||
}
|
||||
if (class_exists('\AKSRT\User\Level')) {
|
||||
require $dir . '/credits-level.php';
|
||||
}
|
||||
|
||||
// Tabbed ledgers (Zibll record tabs).
|
||||
if ($show_wallet || $show_credits) :
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* User center: growth level, route and experience history.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$context = is_array($args['context'] ?? null) ? $args['context'] : [];
|
||||
$user_id = (int) ($context['user_id'] ?? get_current_user_id());
|
||||
if ($user_id <= 0 || !class_exists('\AKSRT\User\Level')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cfg = \AKSRT\User\Level::settings();
|
||||
if (empty($cfg['enabled'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$progress = \AKSRT\User\Level::get_progress($user_id);
|
||||
$details = \AKSRT\User\Level::get_details($user_id);
|
||||
$date_details = \AKSRT\User\Level::get_date_details($user_id);
|
||||
$labels = \AKSRT\User\Level::integral_labels();
|
||||
$opt = is_array($cfg['level_opt'] ?? null) ? $cfg['level_opt'] : [];
|
||||
$today = current_time('Y-m-d');
|
||||
$today_integral = max(0, (int) ($date_details[$today] ?? 0));
|
||||
$day_max = \AKSRT\User\Level::day_max();
|
||||
?>
|
||||
<div class="uc-level-page">
|
||||
<section class="uc-level-hero zib-widget">
|
||||
<div class="uc-level-hero__identity">
|
||||
<div class="uc-level-hero__icon">
|
||||
<?php echo \AKSRT\User\Level::get_badge_html($user_id); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</div>
|
||||
<div>
|
||||
<p><?php esc_html_e('我的等级', 'aksrt'); ?></p>
|
||||
<h2><?php echo esc_html((string) $progress['name']); ?></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uc-level-hero__stats">
|
||||
<div><strong><?php echo esc_html((string) $progress['integral']); ?></strong><span><?php esc_html_e('累计经验', 'aksrt'); ?></span></div>
|
||||
<div><strong><?php echo esc_html((string) $today_integral); ?><?php echo $day_max > 0 ? '/' . esc_html((string) $day_max) : ''; ?></strong><span><?php esc_html_e('今日经验', 'aksrt'); ?></span></div>
|
||||
<div><strong><?php echo esc_html((string) $progress['remaining']); ?></strong><span><?php echo !empty($progress['is_max']) ? esc_html__('已达最高等级', 'aksrt') : esc_html__('升级还需', 'aksrt'); ?></span></div>
|
||||
</div>
|
||||
<div class="uc-level-hero__progress">
|
||||
<div class="uc-level-hero__progress-meta">
|
||||
<span><?php echo esc_html((string) $progress['integral']); ?> EXP</span>
|
||||
<span><?php echo !empty($progress['is_max']) ? esc_html__('最高等级', 'aksrt') : esc_html((string) $progress['next_need']) . ' EXP'; ?></span>
|
||||
</div>
|
||||
<div class="uc-level-progress"><span style="width:<?php echo esc_attr((string) $progress['progress']); ?>%"></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="uc-level-section zib-widget">
|
||||
<div class="uc-level-section__head"><h3><?php esc_html_e('成长路线', 'aksrt'); ?></h3><span><?php printf(esc_html__('最高 %d 级', 'aksrt'), (int) $cfg['max']); ?></span></div>
|
||||
<div class="uc-level-road">
|
||||
<?php for ($level = 1; $level <= (int) $cfg['max']; $level++) :
|
||||
$need = $level === 1 ? 0 : max(0, (int) ($opt['upgrade_integral_' . $level] ?? (($level - 1) * 500 * $level)));
|
||||
$name = \AKSRT\User\Level::get_level_name($level);
|
||||
$state = $level < (int) $progress['level'] ? ' is-done' : ($level === (int) $progress['level'] ? ' is-current' : '');
|
||||
?>
|
||||
<div class="uc-level-road__item<?php echo esc_attr($state); ?>">
|
||||
<div class="uc-level-road__icon">
|
||||
<?php $icon = \AKSRT\User\Level::get_level_icon_url($level); ?>
|
||||
<?php if ($icon !== '') : ?><img src="<?php echo esc_url($icon); ?>" alt="" loading="lazy"><?php else : ?><span>LV<?php echo esc_html((string) $level); ?></span><?php endif; ?>
|
||||
</div>
|
||||
<strong><?php echo esc_html($name); ?></strong>
|
||||
<small><?php echo esc_html((string) $need); ?> EXP</small>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</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>
|
||||
<?php if ($details) : ?>
|
||||
<div class="uc-level-ledger">
|
||||
<?php foreach ($details as $row) :
|
||||
$key = sanitize_key((string) ($row['key'] ?? ''));
|
||||
$value = max(0, (int) ($row['value'] ?? 0));
|
||||
?>
|
||||
<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>
|
||||
<b>+<?php echo esc_html((string) $value); ?></b>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<p class="uc-level-empty"><?php esc_html_e('还没有经验获取记录。', 'aksrt'); ?></p>
|
||||
<?php endif; ?>
|
||||
</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('最近 30 天', 'aksrt'); ?></span></div>
|
||||
<?php if ($date_details) : ?>
|
||||
<div class="uc-level-dates">
|
||||
<?php foreach ($date_details as $date => $value) : ?>
|
||||
<div><time><?php echo esc_html((string) $date); ?></time><span>+<?php echo esc_html((string) max(0, (int) $value)); ?> EXP</span></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<p class="uc-level-empty"><?php esc_html_e('还没有每日经验记录。', 'aksrt'); ?></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -8,8 +8,8 @@
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$cart_url = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : '';
|
||||
$level_url = isset($dashboard_tabs['credits'])
|
||||
? add_query_arg('asset_tab', 'points', aksrt_get_user_center_tab_url('credits'))
|
||||
$level_url = isset($dashboard_tabs['level'])
|
||||
? aksrt_get_user_center_tab_url('level')
|
||||
: '';
|
||||
$auth_url = isset($dashboard_tabs['auth']) ? aksrt_get_user_center_tab_url('auth') : '';
|
||||
|
||||
|
||||
@@ -515,7 +515,7 @@ class SchemaUsers {
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'center', 'title' => '用户中心', 'fields' => [
|
||||
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,posts,comments,favorites,vip,credits,auth', 'help' => '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入'],
|
||||
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,level,posts,comments,favorites,vip,credits,auth', 'help' => '逗号分隔。level=我的等级;auth=官方认证;等级、VIP、订单和认证会按功能开关自动注入'],
|
||||
['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
|
||||
@@ -616,21 +616,15 @@ class SchemaUsers {
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '等级显示', 'fields' => [
|
||||
['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_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 3, 'max' => 30],
|
||||
['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true],
|
||||
['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' => 'user_level_opt', 'label' => '等级参数', 'type' => 'json', 'sanitize_type' => 'array', 'default' => [], 'help' => 'name_N / icon_img_N / upgrade_integral_N'],
|
||||
]],
|
||||
['key' => 'integral', 'title' => '经验值得分', 'fields' => [
|
||||
['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'default' => [], 'help' => 'day_max + 行为得分键'],
|
||||
['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'sanitize_type' => 'array', 'default' => [], 'help' => 'day_max + 行为得分键'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -442,6 +442,7 @@ class SchemaProvider {
|
||||
'radio' => 'string',
|
||||
'checkbox' => 'array',
|
||||
'repeater' => 'array',
|
||||
'json' => 'array',
|
||||
// 自定义面板(验证码服务商等)按对象存储
|
||||
'custom' => 'array',
|
||||
'color' => 'string',
|
||||
|
||||
@@ -745,7 +745,14 @@ trait BoardServicePart2 {
|
||||
$threshold = (int) ($config['other']['hot_plate_threshold'] ?? 100);
|
||||
$posts_count = (int) get_post_meta($plate_id, '_aksrt_posts_count', true);
|
||||
$follow_count = (int) get_post_meta($plate_id, '_aksrt_follow_count', true);
|
||||
return ($posts_count + $follow_count * 2) >= $threshold;
|
||||
$is_hot = ($posts_count + $follow_count * 2) >= $threshold;
|
||||
if ($is_hot && get_post_meta($plate_id, '_aksrt_level_hot_awarded', true) !== '1') {
|
||||
$plate = get_post($plate_id);
|
||||
$author_id = $plate instanceof \WP_Post ? (int) $plate->post_author : 0;
|
||||
update_post_meta($plate_id, '_aksrt_level_hot_awarded', '1');
|
||||
do_action('aksrt_forum_plate_hot', $plate_id, $author_id);
|
||||
}
|
||||
return $is_hot;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -132,7 +132,7 @@ class Capabilities {
|
||||
|
||||
$level_threshold = (int) ($row['level'] ?? -1);
|
||||
if ($level_threshold >= 0) {
|
||||
$user_level = (int) get_user_meta($user_id, 'aksrt_user_level', true);
|
||||
$user_level = class_exists('\AKSRT\User\Level') ? \AKSRT\User\Level::get_level($user_id) : 0;
|
||||
if ($user_level >= $level_threshold) {
|
||||
return true;
|
||||
}
|
||||
@@ -140,7 +140,7 @@ class Capabilities {
|
||||
|
||||
$vip_threshold = (int) ($row['vip'] ?? -1);
|
||||
if ($vip_threshold >= 0) {
|
||||
$user_vip = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
|
||||
$user_vip = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0;
|
||||
if ($user_vip >= $vip_threshold) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -83,14 +83,14 @@ class UserRuntime {
|
||||
*/
|
||||
public static function get_user_center_settings(): array {
|
||||
$settings = Settings::get_all();
|
||||
$raw_tabs = $settings['user_center_tabs'] ?? 'profile,posts,comments,credits,favorites,auth';
|
||||
$raw_tabs = $settings['user_center_tabs'] ?? 'profile,level,posts,comments,credits,favorites,auth';
|
||||
$tabs = is_array($raw_tabs)
|
||||
? $raw_tabs
|
||||
: preg_split('/[\s,]+/', (string) $raw_tabs, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
// Built-in switchable panels. VIP is always auto-injected when enabled
|
||||
// by AKSRT\User\Center — keep it allowed for explicit config too.
|
||||
$allowed_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'settings'];
|
||||
$allowed_tabs = ['profile', 'level', 'posts', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'settings'];
|
||||
$normalized_tabs = ['profile'];
|
||||
|
||||
foreach ((array) $tabs as $tab) {
|
||||
@@ -152,7 +152,6 @@ class UserRuntime {
|
||||
public static function get_user_identity_settings(): array {
|
||||
$settings = Settings::get_all();
|
||||
$verify_badge_text = trim((string) ($settings['user_verify_badge_text'] ?? ''));
|
||||
$level_prefix = trim((string) ($settings['user_level_prefix'] ?? ''));
|
||||
$ban_notice_text = trim((string) ($settings['user_report_ban_notice_text'] ?? ''));
|
||||
$medal = self::parse_medal_catalog($settings);
|
||||
|
||||
@@ -169,14 +168,8 @@ class UserRuntime {
|
||||
'apply_option' => (string) ($settings['user_verify_apply_option'] ?? ''),
|
||||
],
|
||||
'level' => [
|
||||
'enabled' => !empty($settings['user_level_enabled']) || !empty($settings['user_level_s']),
|
||||
'enabled' => !isset($settings['user_level_s']) || !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'] ?? '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,
|
||||
|
||||
@@ -77,92 +77,6 @@ class BadgeAutoAward {
|
||||
if ($user_id <= 0 || !class_exists('\AKSRT\User\Badges') || !Badges::enabled()) {
|
||||
return;
|
||||
}
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return;
|
||||
}
|
||||
$identity = \AKSRT\Settings\Settings::get_user_identity_settings();
|
||||
$medal = is_array($identity['medal'] ?? null) ? $identity['medal'] : [];
|
||||
$items = is_array($medal['items_by_slug'] ?? null) ? $medal['items_by_slug'] : [];
|
||||
if (!$items) {
|
||||
// Fall back to badge dictionary without thresholds.
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($items as $slug => $item) {
|
||||
$get_type = sanitize_key((string) ($item['get_type'] ?? 'manually_add'));
|
||||
if ($get_type === 'manually_add' || $get_type === '') {
|
||||
continue;
|
||||
}
|
||||
$need = max(0, (int) ($item['get_val'] ?? 0));
|
||||
if ($need <= 0) {
|
||||
continue;
|
||||
}
|
||||
$val = $this->metric($user_id, $get_type);
|
||||
if ($val >= $need) {
|
||||
Badges::grant($user_id, (string) $slug, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function metric(int $user_id, string $type): int {
|
||||
switch ($type) {
|
||||
case 'registration_time':
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return 0;
|
||||
}
|
||||
$reg = strtotime((string) $user->user_registered);
|
||||
if ($reg <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return max(0, (int) floor((time() - $reg) / DAY_IN_SECONDS));
|
||||
|
||||
case 'checkin_all_day':
|
||||
$v = (int) get_user_meta($user_id, 'aksrt_checkin_all_day', true);
|
||||
if ($v <= 0) {
|
||||
$v = (int) get_user_meta($user_id, 'checkin_all_day', true);
|
||||
}
|
||||
return $v;
|
||||
|
||||
case 'new_post':
|
||||
return (int) count_user_posts($user_id, 'post', true);
|
||||
|
||||
case 'bbs_post':
|
||||
return post_type_exists('aksrt_forum_topic')
|
||||
? (int) count_user_posts($user_id, 'aksrt_forum_topic', true)
|
||||
: 0;
|
||||
|
||||
case 'comment':
|
||||
return (int) get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']);
|
||||
|
||||
case 'followed':
|
||||
if (class_exists('\AKSRT\User\Follow')) {
|
||||
return (int) Follow::get_instance()->get_follower_count($user_id);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case 'like':
|
||||
return (int) get_user_meta($user_id, 'aksrt_like_count', true)
|
||||
?: (int) get_user_meta($user_id, 'like-posts', true);
|
||||
|
||||
case 'views':
|
||||
return (int) get_user_meta($user_id, 'aksrt_user_views', true);
|
||||
|
||||
case 'pay_income':
|
||||
return (int) get_user_meta($user_id, 'aksrt_pay_income', true);
|
||||
|
||||
case 'pay_rebate':
|
||||
return (int) get_user_meta($user_id, 'aksrt_pay_rebate', true);
|
||||
|
||||
default:
|
||||
/**
|
||||
* Allow extensions to provide metric values.
|
||||
*
|
||||
* @param int $value
|
||||
* @param int $user_id
|
||||
* @param string $type
|
||||
*/
|
||||
return (int) apply_filters('aksrt_badge_metric_value', 0, $user_id, $type);
|
||||
}
|
||||
Badges::auto_evaluate($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +419,6 @@ class Badges {
|
||||
return new \WP_Error('aksrt_badge_not_owned', __('尚未获得该徽章', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
update_user_meta($user_id, self::META_WEAR, $slug);
|
||||
update_user_meta($user_id, 'wear_medal', $slug); // zibll alias
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -439,10 +438,11 @@ class Badges {
|
||||
return new \WP_Error('aksrt_badge_unknown', __('徽章不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
if (!in_array($slug, $owned, true)) {
|
||||
$owned[] = $slug;
|
||||
self::set_owned($user_id, $owned);
|
||||
if (in_array($slug, $owned, true)) {
|
||||
return true;
|
||||
}
|
||||
$owned[] = $slug;
|
||||
self::set_owned($user_id, $owned);
|
||||
/**
|
||||
* Fires after a badge is granted.
|
||||
*
|
||||
|
||||
@@ -47,6 +47,12 @@ class Center {
|
||||
'priority' => 10,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'level' => [
|
||||
'label' => __('我的等级', 'aksrt'),
|
||||
'icon' => 'chart-line',
|
||||
'priority' => 15,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'posts' => [
|
||||
'label' => __('我的文章', 'aksrt'),
|
||||
'icon' => 'file-alt',
|
||||
@@ -111,13 +117,25 @@ class Center {
|
||||
$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 VIP/orders/credits when feature on.
|
||||
// Follow configured order; always auto-include enabled product 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) {
|
||||
@@ -163,6 +181,9 @@ class Center {
|
||||
if (!isset($definitions[$key])) {
|
||||
continue;
|
||||
}
|
||||
if ($key === 'level' && !$level_on) {
|
||||
continue;
|
||||
}
|
||||
if ($key === 'credits' && !$credits_on && !$balance_on) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -151,10 +151,12 @@ class Home {
|
||||
$level = $this->get_user_level($user_id, $settings['level']);
|
||||
if ($level > 0) {
|
||||
$signals[] = [
|
||||
'type' => 'level',
|
||||
'label' => (string) $settings['level']['label_prefix'] . $level,
|
||||
'icon' => 'layer-group',
|
||||
'tone' => 'info',
|
||||
'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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -280,22 +282,8 @@ class Home {
|
||||
* @param array{source?:string,thresholds?:array<int,int>} $level_settings
|
||||
*/
|
||||
private function get_user_level(int $user_id, array $level_settings): int {
|
||||
$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 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;
|
||||
unset($level_settings);
|
||||
return class_exists('AKSRT\\User\\Level') ? Level::get_level($user_id) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* User growth levels and experience awards.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
@@ -9,11 +11,12 @@ 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_TODAY_FOLLOW = 'aksrt_level_integral_followed';
|
||||
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;
|
||||
@@ -28,61 +31,49 @@ 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_sign_in'], 20, 1);
|
||||
add_action('publish_post', [$this, 'on_publish_post'], 20, 2);
|
||||
add_action('aksrt_user_signed_in', [$this, 'on_checkin'], 20, 1);
|
||||
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, 2);
|
||||
add_action('aksrt_comment_liked', [$this, 'on_comment_like'], 20, 2);
|
||||
add_action('aksrt_post_favorited', [$this, 'on_post_favorite'], 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_forum_topic_scored', [$this, 'on_forum_scored'], 20, 3);
|
||||
add_action('aksrt_forum_topic_featured', [$this, 'on_forum_featured'], 20, 3);
|
||||
add_action('aksrt_forum_topic_hot', [$this, 'on_forum_hot'], 20, 2);
|
||||
add_action('aksrt_forum_plate_hot', [$this, 'on_forum_plate_hot'], 20, 2);
|
||||
add_action('aksrt_forum_reply_adopted', [$this, 'on_forum_adopted'], 20, 3);
|
||||
add_action('aksrt_forum_comment_hot', [$this, 'on_forum_comment_hot'], 20, 2);
|
||||
add_action('aksrt_user_level_changed', [$this, 'on_level_changed'], 20, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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}
|
||||
* @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() : [];
|
||||
$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.'));
|
||||
$max = max(3, min(30, (int) ($all['user_level_max'] ?? 10)));
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'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']),
|
||||
'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']),
|
||||
'level_opt' => self::normalize_array_option($all['user_level_opt'] ?? []),
|
||||
'integral_opt' => self::normalize_array_option($all['user_integral_opt'] ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog of integral earn keys (defaults).
|
||||
*
|
||||
* @return array<string,int>
|
||||
*/
|
||||
/** @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,
|
||||
@@ -105,269 +96,360 @@ class Level {
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
public static function integral_labels(): array {
|
||||
return [
|
||||
'sign_up' => __('首次注册', 'aksrt'),
|
||||
'sign_in' => __('每日登录', 'aksrt'),
|
||||
'followed' => __('被关注', 'aksrt'),
|
||||
'post_new' => __('发布文章', 'aksrt'),
|
||||
'post_like' => __('文章获赞', 'aksrt'),
|
||||
'post_favorite' => __('文章被收藏', 'aksrt'),
|
||||
'comment_new' => __('发表评论', 'aksrt'),
|
||||
'comment_like' => __('评论获赞', 'aksrt'),
|
||||
'bbs_posts_new' => __('发布帖子', 'aksrt'),
|
||||
'bbs_score_extra' => __('帖子被加分', 'aksrt'),
|
||||
'bbs_essence' => __('帖子评为精华', 'aksrt'),
|
||||
'bbs_posts_hot' => __('帖子成为热门', 'aksrt'),
|
||||
'bbs_plate_new' => __('创建版块', 'aksrt'),
|
||||
'bbs_plate_hot' => __('版块成为热门', 'aksrt'),
|
||||
'bbs_adopt' => __('回答被采纳', 'aksrt'),
|
||||
'bbs_comment_hot' => __('评论成为神评', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
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));
|
||||
$opt = self::settings()['integral_opt'];
|
||||
$defaults = self::integral_defaults();
|
||||
return max(0, (int) ($opt[$key] ?? $defaults[$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;
|
||||
$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 {
|
||||
if ($user_id <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return max(0, (int) get_user_meta($user_id, self::META_INTEGRAL, true));
|
||||
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 {
|
||||
if ($user_id <= 0) {
|
||||
if ($user_id <= 0 || !self::settings()['enabled']) {
|
||||
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);
|
||||
return self::resolve_level(self::get_integral($user_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{max:int,level_opt:array} $cfg
|
||||
*/
|
||||
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 = (int) $cfg['max'];
|
||||
$opt = $cfg['level_opt'];
|
||||
$max = max(1, (int) ($cfg['max'] ?? 10));
|
||||
$opt = is_array($cfg['level_opt'] ?? null) ? $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 {
|
||||
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 max(0, min($max, $level));
|
||||
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::resolve_level($integral, $cfg);
|
||||
$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 : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant integral for a task key. Respects day_max.
|
||||
* Award experience and append its immutable display ledger.
|
||||
*
|
||||
* @return int|false points granted or false
|
||||
* @return int|false Granted amount or false.
|
||||
*/
|
||||
public static function add(int $user_id, string $key, int $points = 0) {
|
||||
if ($user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
public static function add(int $user_id, string $key, int $points = 0, bool $bypass_day_max = false) {
|
||||
$cfg = self::settings();
|
||||
if (!$cfg['enabled'] || $cfg['source'] !== 'integral') {
|
||||
return false;
|
||||
}
|
||||
$key = sanitize_key($key);
|
||||
if ($key === '' || $key === 'day_max') {
|
||||
if ($user_id <= 0 || !$cfg['enabled'] || $key === '' || $key === 'day_max') {
|
||||
return false;
|
||||
}
|
||||
if ($points <= 0) {
|
||||
$points = self::task_points($key);
|
||||
}
|
||||
$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();
|
||||
$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 (!$bypass_day_max && $day_max > 0) {
|
||||
if ($today_total >= $day_max) {
|
||||
return false;
|
||||
}
|
||||
$points = min($points, $day_max - $today_total);
|
||||
}
|
||||
if ($points <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$before = self::get_integral($user_id);
|
||||
$after = $before + $points;
|
||||
$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);
|
||||
$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));
|
||||
|
||||
$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);
|
||||
$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 = (int) get_user_meta($user_id, self::META_LEVEL, true);
|
||||
$old_level = max(1, (int) get_user_meta($user_id, self::META_LEVEL, true));
|
||||
$new_level = self::resolve_level($after, $cfg);
|
||||
update_user_meta($user_id, self::META_LEVEL, $new_level);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
$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');
|
||||
self::add($user_id, 'sign_up', 0, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
$today = current_time('Ymd');
|
||||
if ($user_id <= 0 || get_user_meta($user_id, self::META_LOGIN_DAY, true) === $today) {
|
||||
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);
|
||||
if (self::add($user_id, 'sign_in')) {
|
||||
update_user_meta($user_id, self::META_LOGIN_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));
|
||||
public function on_checkin(int $user_id): void {
|
||||
$this->on_login('', get_userdata($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') {
|
||||
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;
|
||||
}
|
||||
// Only when transitioning to publish first time.
|
||||
if ((string) get_post_meta($post_id, '_aksrt_integral_post_new', true) === '1') {
|
||||
return;
|
||||
$key = '';
|
||||
if ($post->post_type === 'post') {
|
||||
$key = 'post_new';
|
||||
} elseif ($post->post_type === 'aksrt_forum_topic') {
|
||||
$key = 'bbs_posts_new';
|
||||
} elseif ($post->post_type === 'aksrt_forum_plate') {
|
||||
$key = 'bbs_plate_new';
|
||||
}
|
||||
$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');
|
||||
if ($key !== '') {
|
||||
self::award_once((int) $post->post_author, $key, 'post', (int) $post->ID, '_aksrt_level_' . $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
if ((string) $approved !== '1') {
|
||||
return;
|
||||
}
|
||||
$user_id = (int) ($commentdata['user_id'] ?? 0);
|
||||
if ($user_id <= 0) {
|
||||
$user_id = (int) get_comment($comment_id)->user_id;
|
||||
$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);
|
||||
}
|
||||
if ($user_id <= 0) {
|
||||
}
|
||||
|
||||
private function award_comment_new($comment): void {
|
||||
if (!$comment instanceof \WP_Comment || (int) $comment->user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::add($user_id, 'comment_new');
|
||||
$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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $follower_id
|
||||
* @param int $followed_id
|
||||
*/
|
||||
public function on_followed($follower_id, $followed_id): void {
|
||||
$follower_id = (int) $follower_id;
|
||||
$followed_id = (int) $followed_id;
|
||||
if ($followed_id <= 0) {
|
||||
$awarded = get_user_meta($followed_id, self::META_FOLLOWED, true);
|
||||
$awarded = is_array($awarded) ? array_map('intval', $awarded) : [];
|
||||
if ($follower_id <= 0 || $followed_id <= 0 || in_array($follower_id, $awarded, true)) {
|
||||
return;
|
||||
}
|
||||
self::add($followed_id, 'followed');
|
||||
if (self::add($followed_id, 'followed')) {
|
||||
$awarded[] = $follower_id;
|
||||
update_user_meta($followed_id, self::META_FOLLOWED, array_values(array_unique($awarded)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $post_id
|
||||
* @param int $user_id liker
|
||||
*/
|
||||
public function on_post_like($post_id, $user_id): void {
|
||||
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) {
|
||||
return;
|
||||
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);
|
||||
}
|
||||
$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) {
|
||||
public function on_post_favorite($post_id, $user_id, $active = true): void {
|
||||
if (!$active) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0 || $author === (int) $user_id) {
|
||||
$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_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);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_forum_featured($topic_id, $author_id): void {
|
||||
self::award_once((int) $author_id, 'bbs_essence', 'post', (int) $topic_id, '_aksrt_level_bbs_essence');
|
||||
}
|
||||
|
||||
public function on_forum_hot($topic_id, $author_id): void {
|
||||
self::award_once((int) $author_id, 'bbs_posts_hot', 'post', (int) $topic_id, '_aksrt_level_bbs_posts_hot');
|
||||
}
|
||||
|
||||
public function on_forum_plate_hot($plate_id, $author_id): void {
|
||||
self::award_once((int) $author_id, 'bbs_plate_hot', 'post', (int) $plate_id, '_aksrt_level_bbs_plate_hot');
|
||||
}
|
||||
|
||||
public function on_forum_adopted($topic_id, $comment_id, $author_id): void {
|
||||
self::award_once((int) $author_id, 'bbs_adopt', 'comment', (int) $comment_id, '_aksrt_level_bbs_adopt');
|
||||
}
|
||||
|
||||
public function on_forum_comment_hot($comment_id, $author_id): void {
|
||||
self::award_once((int) $author_id, 'bbs_comment_hot', 'comment', (int) $comment_id, '_aksrt_level_bbs_comment_hot');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
self::add($author, 'post_favorite');
|
||||
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,136 @@
|
||||
<?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_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']);
|
||||
}
|
||||
|
||||
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_level_integral"><?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="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; ?>
|
||||
</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));
|
||||
update_user_meta($user_id, Level::META_INTEGRAL, $integral);
|
||||
update_user_meta($user_id, Level::META_LEVEL, Level::resolve_level($integral));
|
||||
}
|
||||
}
|
||||
@@ -47,19 +47,6 @@ class Metrics {
|
||||
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);
|
||||
|
||||
// Free-points / social hooks also feed level XP.
|
||||
add_action('aksrt_post_liked', [$this, 'level_on_post_liked'], 25, 3);
|
||||
add_action('aksrt_comment_liked', [$this, 'level_on_comment_liked'], 25, 3);
|
||||
add_action('aksrt_post_favorited', [$this, 'level_on_post_favorited'], 25, 3);
|
||||
add_action('aksrt_user_followed', [$this, 'level_on_user_followed'], 25, 2);
|
||||
add_action('aksrt_forum_topic_featured', [$this, 'level_on_forum_topic_featured'], 25, 2);
|
||||
add_action('aksrt_forum_reply_adopted', [$this, 'level_on_forum_reply_adopted'], 25, 3);
|
||||
add_action('aksrt_forum_comment_hot', [$this, 'level_on_forum_comment_hot'], 25, 2);
|
||||
add_action('aksrt_forum_topic_hot', [$this, 'level_on_forum_topic_hot'], 25, 2);
|
||||
add_action('aksrt_forum_topic_scored', [$this, 'level_on_forum_topic_scored'], 25, 3);
|
||||
add_action('aksrt_forum_topic_created', [$this, 'level_on_forum_topic_created'], 25, 2);
|
||||
add_action('aksrt_user_signed_in', [$this, 'level_on_user_signed_in'], 25, 1);
|
||||
}
|
||||
|
||||
public static function bump_user_meta(int $user_id, string $meta_key, float $delta = 1.0): float {
|
||||
@@ -259,100 +246,4 @@ class Metrics {
|
||||
self::bump_user_meta($user_id, self::META_PAY_REBATE, $amount);
|
||||
self::reevaluate_medals($user_id);
|
||||
}
|
||||
|
||||
/* ---------- level XP bridges ---------- */
|
||||
|
||||
private static function level_add(int $user_id, string $key): void {
|
||||
if ($user_id > 0 && class_exists('AKSRT\\User\\Level')) {
|
||||
Level::add($user_id, $key);
|
||||
}
|
||||
}
|
||||
|
||||
public function level_on_post_liked(int $post_id, int $actor_id, bool $active): void {
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
$post = get_post($post_id);
|
||||
if (!$post instanceof \WP_Post) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author > 0 && $author !== $actor_id) {
|
||||
self::level_add($author, 'post_like');
|
||||
}
|
||||
}
|
||||
|
||||
public function level_on_comment_liked(int $comment_id, int $actor_id, bool $active): void {
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
$c = get_comment($comment_id);
|
||||
if (!$c) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $c->user_id;
|
||||
if ($author > 0 && $author !== $actor_id) {
|
||||
self::level_add($author, 'comment_like');
|
||||
}
|
||||
}
|
||||
|
||||
public function level_on_post_favorited(int $post_id, int $actor_id, bool $active): void {
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
$post = get_post($post_id);
|
||||
if (!$post instanceof \WP_Post) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author > 0 && $author !== $actor_id) {
|
||||
self::level_add($author, 'post_favorite');
|
||||
}
|
||||
}
|
||||
|
||||
public function level_on_user_followed(int $follower_id, int $following_id): void {
|
||||
unset($follower_id);
|
||||
self::level_add((int) $following_id, 'followed');
|
||||
}
|
||||
|
||||
public function level_on_forum_topic_featured($topic_id, $author_id = 0): void {
|
||||
unset($topic_id);
|
||||
self::level_add((int) $author_id, 'bbs_essence');
|
||||
}
|
||||
|
||||
public function level_on_forum_reply_adopted($topic_id, $comment_id, $adoptee_id): void {
|
||||
unset($topic_id, $comment_id);
|
||||
self::level_add((int) $adoptee_id, 'bbs_adopt');
|
||||
}
|
||||
|
||||
public function level_on_forum_comment_hot($comment_id, $author_id = 0): void {
|
||||
unset($comment_id);
|
||||
self::level_add((int) $author_id, 'bbs_comment_hot');
|
||||
}
|
||||
|
||||
public function level_on_forum_topic_hot($topic_id, $author_id = 0): void {
|
||||
unset($topic_id);
|
||||
self::level_add((int) $author_id, 'bbs_posts_hot');
|
||||
}
|
||||
|
||||
public function level_on_forum_topic_scored($topic_id, $author_id, $delta = 0): void {
|
||||
unset($topic_id);
|
||||
if ((float) $delta > 0) {
|
||||
self::level_add((int) $author_id, 'bbs_score_extra');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param int $topic_id
|
||||
*/
|
||||
public function level_on_forum_topic_created($user_id, $topic_id = 0): void {
|
||||
unset($topic_id);
|
||||
self::level_add((int) $user_id, 'bbs_posts_new');
|
||||
}
|
||||
|
||||
public function level_on_user_signed_in(int $user_id): void {
|
||||
// Sign-in XP: use sign_in key once/day via existing login guard path as well.
|
||||
self::level_add($user_id, 'sign_in');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,13 +123,8 @@ $pm_url = ($pm_enabled && $messages_url !== '')
|
||||
<?php if ($city_badge !== '') : ?>
|
||||
<?php echo $city_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($signals)) : ?>
|
||||
<?php foreach ($signals as $signal) : ?>
|
||||
<span class="aksrt-identity-chip aksrt-identity-chip--<?php echo esc_attr($signal['tone']); ?>">
|
||||
<i class="fas fa-<?php echo esc_attr($signal['icon']); ?>" aria-hidden="true"></i>
|
||||
<span><?php echo esc_html($signal['label']); ?></span>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
<?php if (function_exists('aksrt_render_user_identity_chips')) : ?>
|
||||
<?php echo aksrt_render_user_identity_chips($user_id, 'author', ['include_vip' => false]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||