feat(users): level integral XP schema + AKSRT_User_Level runtime
Rewrite /users/level to Zibll-like user_level_s/max/opt and user_integral_opt. Runtime grants XP with day_max, resolves level, hooks login/register/post/comment/like/favorite/follow. Profile signals read integral-backed levels. audit errors=0.
This commit is contained in:
1 parent
e80d8ea45d
commit
1b0eed6ef7
8 files changed
+658
-141
No files matched your search
@@ -2,7 +2,7 @@
|
|||||||
<div class="aksrt-page-container">
|
<div class="aksrt-page-container">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="用户等级"
|
title="用户等级"
|
||||||
desc="配置等级来源、展示前缀和阈值规则。前台公开主页会按照这里的规则输出等级身份芯片。"
|
desc="配置等级开关、最高等级、升级经验阈值与经验获取矩阵(对齐 Zibll 用户等级 / 经验值)。"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<n-space>
|
<n-space>
|
||||||
@@ -14,80 +14,64 @@
|
|||||||
|
|
||||||
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="aksrt-section-card">
|
||||||
<div class="aksrt-section-header">
|
<div class="aksrt-section-header"><h3>当前摘要</h3></div>
|
||||||
<h3>当前联动摘要</h3>
|
|
||||||
</div>
|
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-tag round :type="form.user_level_enabled ? 'success' : 'default'">
|
<n-tag round :type="form.user_level_s ? 'success' : 'default'">
|
||||||
等级系统:{{ form.user_level_enabled ? '已启用' : '已关闭' }}
|
等级系统:{{ form.user_level_s ? '已启用' : '已关闭' }}
|
||||||
</n-tag>
|
|
||||||
<n-tag round :type="creditsSummary.enabled ? 'info' : 'warning'">
|
|
||||||
积分系统:{{ creditsSummary.enabled ? '已启用' : '未启用' }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag round type="info">
|
|
||||||
当前货币:{{ creditsSummary.currencyName }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag round :type="form.user_level_source === 'credits_balance' ? 'success' : 'warning'">
|
|
||||||
来源:{{ form.user_level_source === 'credits_balance' ? '积分余额' : '手动 Meta' }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag round type="info">
|
|
||||||
阈值条目:{{ thresholdCount }}
|
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
<n-tag round type="info">最高等级:{{ form.user_level_max }}</n-tag>
|
||||||
|
<n-tag round type="info">日经验上限:{{ form.user_integral_opt.day_max }}</n-tag>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="aksrt-section-card">
|
||||||
<div class="aksrt-section-header">
|
<div class="aksrt-section-header"><h3>等级显示</h3></div>
|
||||||
<h3>等级显示</h3>
|
|
||||||
</div>
|
|
||||||
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
||||||
<n-form-item label="启用等级系统">
|
<n-form-item label="启用用户等级">
|
||||||
<n-switch v-model:value="form.user_level_enabled" />
|
<n-switch v-model:value="form.user_level_s" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item v-if="form.user_level_s" label="公开页显示等级">
|
||||||
<n-form-item label="公开页显示等级" v-if="form.user_level_enabled">
|
|
||||||
<n-switch v-model:value="form.user_level_public_display" />
|
<n-switch v-model:value="form.user_level_public_display" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item v-if="form.user_level_s" label="等级前缀">
|
||||||
<n-form-item label="等级前缀" v-if="form.user_level_enabled">
|
<n-input v-model:value="form.user_level_prefix" maxlength="12" style="width: 180px;" />
|
||||||
<n-input v-model:value="form.user_level_prefix" maxlength="12" placeholder="Lv." />
|
</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>
|
||||||
|
<n-form-item v-if="form.user_level_s" label="升级通知">
|
||||||
|
<n-switch v-model:value="form.user_level_notify" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
<n-card v-if="form.user_level_s" class="aksrt-section-card">
|
||||||
<div class="aksrt-section-header">
|
<div class="aksrt-section-header"><h3>升级所需经验(累计)</h3></div>
|
||||||
<h3>等级规则</h3>
|
<n-alert type="info" :show-icon="false" style="margin-bottom: 12px;">
|
||||||
</div>
|
Lv.1 通常为 0。数值为升到该级所需的累计经验。修改最高等级后会自动补齐/截断行。
|
||||||
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
|
||||||
<n-form-item label="等级来源">
|
|
||||||
<n-radio-group v-model:value="form.user_level_source" :disabled="!form.user_level_enabled">
|
|
||||||
<n-space size="large">
|
|
||||||
<n-radio value="credits_balance">积分余额</n-radio>
|
|
||||||
<n-radio value="manual_meta">手动 Meta</n-radio>
|
|
||||||
</n-space>
|
|
||||||
</n-radio-group>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="等级阈值">
|
|
||||||
<n-input
|
|
||||||
v-model:value="form.user_level_thresholds"
|
|
||||||
type="textarea"
|
|
||||||
:autosize="{ minRows: 5, maxRows: 10 }"
|
|
||||||
placeholder="1|0 2|30 3|100 4|300"
|
|
||||||
:disabled="!form.user_level_enabled"
|
|
||||||
/>
|
|
||||||
<template #feedback>
|
|
||||||
<span class="aksrt-hint-text">每行一个等级,格式 `等级|所需积分`。手动 Meta 模式下仍建议保留完整字典,方便后续切换回自动计算。</span>
|
|
||||||
</template>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<n-alert type="info" :show-icon="false">
|
|
||||||
手动模式读取 `aksrt_user_level` user meta;自动模式则根据当前积分余额和上方阈值表实时计算等级,不需要单独写库。
|
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
<n-data-table :columns="levelColumns" :data="levelRows" :bordered="false" size="small" />
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-card v-if="form.user_level_s" class="aksrt-section-card">
|
||||||
|
<div class="aksrt-section-header"><h3>经验值得分</h3></div>
|
||||||
|
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
||||||
|
<n-form-item label="每日上限">
|
||||||
|
<n-input-number v-model:value="form.user_integral_opt.day_max" :min="0" :max="10000" style="width: 180px;" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<n-divider />
|
||||||
|
<div v-for="group in integralGroups" :key="group" style="margin-bottom: 16px;">
|
||||||
|
<h4 style="margin: 0 0 8px;">{{ group }}</h4>
|
||||||
|
<n-grid cols="1 s:2 m:3" :x-gap="12" :y-gap="8" responsive="screen">
|
||||||
|
<n-grid-item v-for="row in INTEGRAL_CATALOG.filter((r) => r.group === group)" :key="row.key">
|
||||||
|
<div class="integral-field">
|
||||||
|
<div class="integral-field__label">{{ row.label }}</div>
|
||||||
|
<n-input-number v-model:value="form.user_integral_opt[row.key]" :min="0" :max="1000" style="width: 100%;" />
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<div class="aksrt-form-actions">
|
<div class="aksrt-form-actions">
|
||||||
@@ -98,38 +82,78 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useMessage } from 'naive-ui'
|
import { NInput, NInputNumber, useMessage } from 'naive-ui'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import PageHeader from '@/components/PageHeader.vue'
|
import PageHeader from '@/components/PageHeader.vue'
|
||||||
|
import {
|
||||||
|
INTEGRAL_CATALOG,
|
||||||
|
defaultIntegralOpt,
|
||||||
|
defaultLevelOpt,
|
||||||
|
} from '@/schema/sections/users/level.js'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
user_level_enabled: false,
|
user_level_s: true,
|
||||||
user_level_public_display: true,
|
user_level_public_display: true,
|
||||||
user_level_source: 'credits_balance',
|
|
||||||
user_level_prefix: 'Lv.',
|
user_level_prefix: 'Lv.',
|
||||||
user_level_thresholds: '1|0\n2|30\n3|100\n4|300',
|
user_level_max: 10,
|
||||||
|
user_level_opt: defaultLevelOpt(10),
|
||||||
|
user_integral_opt: defaultIntegralOpt(),
|
||||||
|
user_level_notify: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const creditsSummary = reactive({
|
const integralGroups = computed(() => [...new Set(INTEGRAL_CATALOG.map((r) => r.group))])
|
||||||
enabled: true,
|
|
||||||
currencyName: '积分',
|
const levelRows = computed(() => {
|
||||||
|
const max = Math.max(3, Math.min(30, Number(form.user_level_max) || 10))
|
||||||
|
const rows = []
|
||||||
|
for (let i = 1; i <= max; i++) {
|
||||||
|
rows.push({
|
||||||
|
level: i,
|
||||||
|
name: form.user_level_opt['name_' + i] ?? form.user_level_prefix + i,
|
||||||
|
upgrade: Number(form.user_level_opt['upgrade_integral_' + i] ?? 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rows
|
||||||
})
|
})
|
||||||
|
|
||||||
const thresholdCount = computed(() => {
|
const levelColumns = [
|
||||||
return String(form.user_level_thresholds || '')
|
{ title: '等级', key: 'level', width: 80 },
|
||||||
.split(/\r?\n/)
|
{
|
||||||
.map((item) => item.trim())
|
title: '名称',
|
||||||
.filter(Boolean)
|
key: 'name',
|
||||||
.length
|
render: (row) =>
|
||||||
})
|
h(NInput, {
|
||||||
|
value: form.user_level_opt['name_' + row.level] ?? '',
|
||||||
|
size: 'small',
|
||||||
|
onUpdateValue: (v) => {
|
||||||
|
form.user_level_opt['name_' + row.level] = v
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '升级所需累计经验',
|
||||||
|
key: 'upgrade',
|
||||||
|
width: 200,
|
||||||
|
render: (row) =>
|
||||||
|
h(NInputNumber, {
|
||||||
|
value: Number(form.user_level_opt['upgrade_integral_' + row.level] ?? 0),
|
||||||
|
min: 0,
|
||||||
|
max: 9999999,
|
||||||
|
size: 'small',
|
||||||
|
style: 'width: 160px;',
|
||||||
|
onUpdateValue: (v) => {
|
||||||
|
form.user_level_opt['upgrade_integral_' + row.level] = Number(v || 0)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
function go(path) {
|
function go(path) {
|
||||||
router.push(path)
|
router.push(path)
|
||||||
@@ -139,25 +163,74 @@ function extractData(response) {
|
|||||||
return response?.data ?? response ?? {}
|
return response?.data ?? response ?? {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMaxChange(val) {
|
||||||
|
const max = Math.max(3, Math.min(30, Number(val) || 10))
|
||||||
|
form.user_level_max = max
|
||||||
|
const next = { ...form.user_level_opt }
|
||||||
|
const filled = defaultLevelOpt(max)
|
||||||
|
for (let i = 1; i <= max; i++) {
|
||||||
|
if (next['upgrade_integral_' + i] === undefined) {
|
||||||
|
next['upgrade_integral_' + i] = filled['upgrade_integral_' + i]
|
||||||
|
}
|
||||||
|
if (next['name_' + i] === undefined) {
|
||||||
|
next['name_' + i] = form.user_level_prefix + i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// drop extras
|
||||||
|
for (const k of Object.keys(next)) {
|
||||||
|
const m = k.match(/_(\d+)$/)
|
||||||
|
if (m && Number(m[1]) > max) {
|
||||||
|
delete next[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form.user_level_opt = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeIntegral(raw) {
|
||||||
|
const base = defaultIntegralOpt()
|
||||||
|
if (!raw || typeof raw !== 'object') {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
for (const row of INTEGRAL_CATALOG) {
|
||||||
|
if (raw[row.key] !== undefined) {
|
||||||
|
base[row.key] = Math.max(0, Number(raw[row.key]) || 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (raw.day_max !== undefined) {
|
||||||
|
base.day_max = Math.max(0, Number(raw.day_max) || 0)
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLevelOpt(raw, max) {
|
||||||
|
const base = defaultLevelOpt(max)
|
||||||
|
if (!raw || typeof raw !== 'object') {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
for (let i = 1; i <= max; i++) {
|
||||||
|
if (raw['upgrade_integral_' + i] !== undefined) {
|
||||||
|
base['upgrade_integral_' + i] = Math.max(0, Number(raw['upgrade_integral_' + i]) || 0)
|
||||||
|
}
|
||||||
|
if (raw['name_' + i] !== undefined) {
|
||||||
|
base['name_' + i] = String(raw['name_' + i] || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [schemaRes, creditRes] = await Promise.all([
|
const schemaRes = await api.getSchemaPage('users', 'level')
|
||||||
api.getSchemaPage('users', 'level'),
|
const d = extractData(schemaRes)
|
||||||
api.getCreditSettings(),
|
const enabled = d.user_level_s !== undefined ? !!d.user_level_s : !!d.user_level_enabled
|
||||||
])
|
form.user_level_s = enabled
|
||||||
|
form.user_level_public_display = d.user_level_public_display !== false
|
||||||
const schemaData = extractData(schemaRes)
|
form.user_level_prefix = d.user_level_prefix || 'Lv.'
|
||||||
const creditData = extractData(creditRes)
|
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_enabled = !!schemaData.user_level_enabled
|
form.user_level_opt = normalizeLevelOpt(d.user_level_opt, form.user_level_max)
|
||||||
form.user_level_public_display = schemaData.user_level_public_display !== false
|
form.user_integral_opt = normalizeIntegral(d.user_integral_opt)
|
||||||
form.user_level_source = schemaData.user_level_source || 'credits_balance'
|
|
||||||
form.user_level_prefix = schemaData.user_level_prefix || 'Lv.'
|
|
||||||
form.user_level_thresholds = schemaData.user_level_thresholds || '1|0\n2|30\n3|100\n4|300'
|
|
||||||
|
|
||||||
creditsSummary.enabled = creditData.enabled !== false
|
|
||||||
creditsSummary.currencyName = creditData.currency_name || '积分'
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || '加载用户等级配置失败')
|
message.error(error.message || '加载用户等级配置失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -168,12 +241,23 @@ async function load() {
|
|||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
await api.saveSchemaPage('users', 'level', {
|
const max = Math.max(3, Math.min(30, Number(form.user_level_max) || 10))
|
||||||
user_level_enabled: form.user_level_enabled,
|
const payload = {
|
||||||
user_level_public_display: form.user_level_public_display,
|
user_level_s: !!form.user_level_s,
|
||||||
user_level_source: form.user_level_source,
|
user_level_enabled: !!form.user_level_s,
|
||||||
user_level_prefix: form.user_level_prefix,
|
user_level_public_display: !!form.user_level_public_display,
|
||||||
user_level_thresholds: form.user_level_thresholds,
|
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),
|
||||||
|
}
|
||||||
|
await api.saveSchemaPage('users', 'level', payload)
|
||||||
|
Object.assign(form, {
|
||||||
|
user_level_max: max,
|
||||||
|
user_level_opt: payload.user_level_opt,
|
||||||
|
user_integral_opt: payload.user_integral_opt,
|
||||||
})
|
})
|
||||||
message.success('用户等级配置已保存')
|
message.success('用户等级配置已保存')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -185,3 +269,11 @@ async function handleSave() {
|
|||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.integral-field__label {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,33 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* User level + integral XP (Zibll user_level_s / user_level_opt / user_integral_opt).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const INTEGRAL_CATALOG = [
|
||||||
|
{ key: 'sign_up', label: '首次注册', default: 20, group: '用户' },
|
||||||
|
{ key: 'sign_in', label: '每日登录', default: 5, group: '用户' },
|
||||||
|
{ key: 'followed', label: '被关注', default: 5, group: '用户' },
|
||||||
|
{ key: 'post_new', label: '发布文章', default: 5, group: '文章' },
|
||||||
|
{ key: 'post_like', label: '文章获赞', default: 1, group: '文章' },
|
||||||
|
{ key: 'post_favorite', label: '文章被收藏', default: 2, group: '文章' },
|
||||||
|
{ key: 'comment_new', label: '发表评论', default: 2, group: '文章' },
|
||||||
|
{ key: 'comment_like', label: '评论获赞', default: 1, group: '文章' },
|
||||||
|
{ key: 'bbs_posts_new', label: '发布帖子', default: 3, group: '论坛' },
|
||||||
|
{ key: 'bbs_score_extra', label: '帖子被加分', default: 1, group: '论坛' },
|
||||||
|
{ key: 'bbs_essence', label: '帖子评为精华', default: 2, group: '论坛' },
|
||||||
|
{ key: 'bbs_posts_hot', label: '帖子成为热门', default: 2, group: '论坛' },
|
||||||
|
{ key: 'bbs_plate_new', label: '创建版块', default: 2, group: '论坛' },
|
||||||
|
{ key: 'bbs_plate_hot', label: '版块成为热门', default: 2, group: '论坛' },
|
||||||
|
{ key: 'bbs_adopt', label: '回答被采纳', default: 2, group: '论坛' },
|
||||||
|
{ key: 'bbs_comment_hot', label: '评论成为神评', default: 2, group: '论坛' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function defaultLevelOpt(max = 10) {
|
||||||
|
const opt = {}
|
||||||
|
for (let i = 1; i <= max; i++) {
|
||||||
|
// Rough Zibll-like curve: LV1=0, then grows
|
||||||
|
opt['upgrade_integral_' + i] = i === 1 ? 0 : Math.round(20 * Math.pow(i - 1, 1.6))
|
||||||
|
opt['name_' + i] = 'Lv.' + i
|
||||||
|
}
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultIntegralOpt() {
|
||||||
|
const opt = { day_max: 100 }
|
||||||
|
for (const row of INTEGRAL_CATALOG) {
|
||||||
|
opt[row.key] = row.default
|
||||||
|
}
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
export const levelSchema = {
|
export const levelSchema = {
|
||||||
key: 'users.level',
|
key: 'users.level',
|
||||||
title: '用户等级',
|
title: '用户等级',
|
||||||
icon: 'fas fa-level-up-alt',
|
icon: 'fas fa-level-up-alt',
|
||||||
description: '配置等级展示前缀和等级阈值,当前可基于积分余额自动计算。',
|
description: '等级与经验值:最高等级、升级阈值、经验获取矩阵(对齐 Zibll user_level / user_integral)。',
|
||||||
defaults: {
|
defaults: {
|
||||||
user_level_enabled: false,
|
user_level_s: true,
|
||||||
user_level_public_display: true,
|
user_level_public_display: true,
|
||||||
user_level_source: 'credits_balance',
|
|
||||||
user_level_prefix: 'Lv.',
|
user_level_prefix: 'Lv.',
|
||||||
user_level_thresholds: '1|0\n2|30\n3|100\n4|300',
|
user_level_max: 10,
|
||||||
|
user_level_opt: defaultLevelOpt(10),
|
||||||
|
user_integral_opt: defaultIntegralOpt(),
|
||||||
|
user_level_notify: true,
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
key: 'display',
|
key: 'display',
|
||||||
title: '等级显示',
|
title: '等级显示',
|
||||||
icon: 'fas fa-signal',
|
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'user_level_enabled', label: '启用等级系统', type: 'switch', default: false },
|
{ key: 'user_level_s', label: '启用用户等级', type: 'switch', default: true },
|
||||||
{ key: 'user_level_public_display', label: '公开页显示等级', type: 'switch', default: true, dependsOn: { key: 'user_level_enabled', value: 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_enabled', value: true } },
|
{ key: 'user_level_prefix', label: '等级前缀', type: 'input', default: 'Lv.', dependsOn: { key: 'user_level_s', value: true } },
|
||||||
|
{ key: 'user_level_max', label: '最高等级', type: 'number', default: 10, min: 3, max: 30, dependsOn: { key: 'user_level_s', value: true } },
|
||||||
|
{ key: 'user_level_notify', label: '升级通知', type: 'switch', default: true, dependsOn: { key: 'user_level_s', value: true } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rules',
|
key: 'level_opt',
|
||||||
title: '等级规则',
|
title: '等级升级阈值',
|
||||||
icon: 'fas fa-list-ol',
|
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'user_level_source', label: '等级来源', type: 'radio', default: 'credits_balance', options: [{ label: '积分余额', value: 'credits_balance' }, { label: '手动 Meta', value: 'manual_meta' }], dependsOn: { key: 'user_level_enabled', value: true } },
|
{ key: 'user_level_opt', label: '升级经验配置', type: 'json', default: defaultLevelOpt(10), help: 'upgrade_integral_N 为升到 N 级所需累计经验' },
|
||||||
{ key: 'user_level_thresholds', label: '等级阈值', type: 'textarea', default: '1|0\n2|30\n3|100\n4|300', help: '每行一个等级,格式:等级|所需积分,例如 3|100', dependsOn: { key: 'user_level_enabled', value: true } },
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'integral',
|
||||||
|
title: '经验值得分',
|
||||||
|
fields: [
|
||||||
|
{ key: 'user_integral_opt', label: '经验值获取', type: 'json', default: defaultIntegralOpt(), help: 'day_max + 各行为得分;对应功能未开则将分值设 0' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -370,6 +370,9 @@ if (class_exists('AKSRT_User_Badges')) {
|
|||||||
if (class_exists('AKSRT_User_Badges_Admin')) {
|
if (class_exists('AKSRT_User_Badges_Admin')) {
|
||||||
AKSRT_User_Badges_Admin::get_instance()->init();
|
AKSRT_User_Badges_Admin::get_instance()->init();
|
||||||
}
|
}
|
||||||
|
if (class_exists('AKSRT_User_Level')) {
|
||||||
|
AKSRT_User_Level::get_instance()->init();
|
||||||
|
}
|
||||||
|
|
||||||
// WordPress 原生侧栏:支付/订单运营中心(zibll zibpay 对齐)
|
// WordPress 原生侧栏:支付/订单运营中心(zibll zibpay 对齐)
|
||||||
if (is_admin() && class_exists('AKSRT_Admin_Pay_Menus')) {
|
if (is_admin() && class_exists('AKSRT_Admin_Pay_Menus')) {
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-credits-admin.php';
|
|||||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-balance-admin.php';
|
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-balance-admin.php';
|
||||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges.php';
|
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges.php';
|
||||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges-admin.php';
|
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges-admin.php';
|
||||||
|
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-level.php';
|
||||||
|
|
||||||
// 加载数据表迁移器(CMS / 积分 / 下载 token)
|
// 加载数据表迁移器(CMS / 积分 / 下载 token)
|
||||||
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php';
|
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php';
|
||||||
|
|||||||
@@ -604,37 +604,27 @@ class AKSRT_Admin_Schema_Users {
|
|||||||
'level' => [
|
'level' => [
|
||||||
'key' => 'level',
|
'key' => 'level',
|
||||||
'title' => '用户等级',
|
'title' => '用户等级',
|
||||||
'version' => '1.0.0',
|
'version' => '2.0.0',
|
||||||
'option_key' => 'aksrt_settings',
|
'option_key' => 'aksrt_settings',
|
||||||
'groups' => [
|
'groups' => [
|
||||||
['key' => 'display', 'title' => '等级显示', 'fields' => [
|
['key' => 'display', 'title' => '等级显示', 'fields' => [
|
||||||
['key' => 'user_level_enabled', 'label' => '启用等级系统', 'type' => 'switch', 'default' => false],
|
['key' => 'user_level_s', 'label' => '启用用户等级', 'type' => 'switch', 'default' => true],
|
||||||
|
['key' => 'user_level_enabled', 'label' => '启用等级系统(别名)', 'type' => 'switch', 'default' => true, 'help' => '与 user_level_s 同步写入'],
|
||||||
['key' => 'user_level_public_display', 'label' => '公开页显示等级', 'type' => 'switch', 'default' => true],
|
['key' => 'user_level_public_display', 'label' => '公开页显示等级', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'user_level_prefix', 'label' => '等级前缀', 'type' => 'input', 'default' => 'Lv.'],
|
['key' => 'user_level_prefix', 'label' => '等级前缀', 'type' => 'input', 'default' => 'Lv.'],
|
||||||
['key' => 'user_level_color', 'label' => '等级颜色', 'type' => 'color', 'default' => '#f04494'],
|
['key' => 'user_level_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 3, 'max' => 30],
|
||||||
['key' => 'user_level_icon', 'label' => '等级图标', 'type' => 'icon', 'default' => 'fas fa-star'],
|
|
||||||
]],
|
|
||||||
['key' => 'rules', 'title' => '等级规则', 'fields' => [
|
|
||||||
['key' => 'user_level_source', 'label' => '等级来源', 'type' => 'radio', 'default' => 'credits_balance'],
|
|
||||||
['key' => 'user_level_thresholds', 'label' => '等级阈值', 'type' => 'textarea', 'default' => "1|0\n2|30\n3|100\n4|300"],
|
|
||||||
['key' => 'user_level_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 99],
|
|
||||||
['key' => 'user_level_decay', 'label' => '等级衰减', 'type' => 'switch', 'default' => false, 'help' => '长期不活跃自动降级'],
|
|
||||||
['key' => 'user_level_decay_days', 'label' => '衰减周期(天)', 'type' => 'number', 'default' => 90, 'min' => 7, 'max' => 365],
|
|
||||||
]],
|
|
||||||
['key' => 'privilege', 'title' => '等级特权', 'fields' => [
|
|
||||||
['key' => 'user_level_download_limit', 'label' => '下载限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户每日下载次数增加'],
|
|
||||||
['key' => 'user_level_upload_limit', 'label' => '上传限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户可上传更大文件'],
|
|
||||||
['key' => 'user_level_comment_cooldown', 'label' => '评论冷却缩短', 'type' => 'switch', 'default' => false, 'help' => '高等级用户评论间隔更短'],
|
|
||||||
['key' => 'user_level_custom_title', 'label' => '自定义头衔', 'type' => 'switch', 'default' => false, 'help' => '允许高等级用户自定义头衔'],
|
|
||||||
]],
|
|
||||||
['key' => 'notify', 'title' => '升级通知', 'fields' => [
|
|
||||||
['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true],
|
['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true],
|
||||||
['key' => 'user_level_notify_style', 'label' => '通知方式', 'type' => 'select', 'default' => 'toast', 'options' => [
|
['key' => 'user_level_source', 'label' => '等级来源', 'type' => 'radio', 'default' => 'integral', 'options' => [
|
||||||
['label' => '弹窗提示', 'value' => 'toast'],
|
['label' => '经验值', 'value' => 'integral'],
|
||||||
['label' => '站内信', 'value' => 'message'],
|
['label' => '手动 Meta', 'value' => 'manual_meta'],
|
||||||
['label' => '两者都有', 'value' => 'both'],
|
|
||||||
]],
|
]],
|
||||||
]],
|
]],
|
||||||
|
['key' => 'level_opt', 'title' => '升级阈值', 'fields' => [
|
||||||
|
['key' => 'user_level_opt', 'label' => '等级参数', 'type' => 'json', 'default' => [], 'help' => 'upgrade_integral_N / name_N'],
|
||||||
|
]],
|
||||||
|
['key' => 'integral', 'title' => '经验值得分', 'fields' => [
|
||||||
|
['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'default' => [], 'help' => 'day_max + 行为得分键'],
|
||||||
|
]],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'badge' => [
|
'badge' => [
|
||||||
|
|||||||
@@ -279,22 +279,24 @@ class AKSRT_User_Home {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{source:string,thresholds:array<int,int>} $level_settings
|
* @param array{source?:string,thresholds?:array<int,int>} $level_settings
|
||||||
*/
|
*/
|
||||||
private function get_user_level(int $user_id, array $level_settings): int {
|
private function get_user_level(int $user_id, array $level_settings): int {
|
||||||
if (($level_settings['source'] ?? 'credits_balance') === 'manual_meta') {
|
$source = (string) ($level_settings['source'] ?? 'integral');
|
||||||
|
if ($source === 'manual_meta') {
|
||||||
return max(0, (int) get_user_meta($user_id, 'aksrt_user_level', true));
|
return max(0, (int) get_user_meta($user_id, 'aksrt_user_level', true));
|
||||||
}
|
}
|
||||||
|
if (class_exists('AKSRT_User_Level')) {
|
||||||
|
return AKSRT_User_Level::get_level($user_id);
|
||||||
|
}
|
||||||
|
// Fallback: credits_balance thresholds (legacy) if integral class missing.
|
||||||
$score = function_exists('aksrt_user_credits') ? (int) aksrt_user_credits($user_id) : 0;
|
$score = function_exists('aksrt_user_credits') ? (int) aksrt_user_credits($user_id) : 0;
|
||||||
$level = 0;
|
$level = 0;
|
||||||
|
|
||||||
foreach ((array) ($level_settings['thresholds'] ?? []) as $candidate => $threshold) {
|
foreach ((array) ($level_settings['thresholds'] ?? []) as $candidate => $threshold) {
|
||||||
if ($score >= (int) $threshold) {
|
if ($score >= (int) $threshold) {
|
||||||
$level = (int) $candidate;
|
$level = (int) $candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $level;
|
return $level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,379 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* User level + integral XP — Zibll user_level / user_integral parity.
|
||||||
|
*
|
||||||
|
* Meta:
|
||||||
|
* aksrt_level_integral int total XP
|
||||||
|
* aksrt_user_level int resolved level
|
||||||
|
* aksrt_level_integral_detail array reason => [value, integral]
|
||||||
|
* aksrt_level_integral_date_detail array Y-m-d => int
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class AKSRT_User_Level {
|
||||||
|
|
||||||
|
public const META_INTEGRAL = 'aksrt_level_integral';
|
||||||
|
public const META_LEVEL = 'aksrt_user_level';
|
||||||
|
public const META_DETAIL = 'aksrt_level_integral_detail';
|
||||||
|
public const META_DATE_DETAIL = 'aksrt_level_integral_date_detail';
|
||||||
|
public const META_TODAY_FOLLOW = 'aksrt_level_integral_followed';
|
||||||
|
|
||||||
|
/** @var self|null */
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
public static function get_instance(): self {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init(): void {
|
||||||
|
add_action('user_register', [$this, 'on_register'], 20, 1);
|
||||||
|
add_action('wp_login', [$this, 'on_login'], 20, 2);
|
||||||
|
add_action('aksrt_user_signed_in', [$this, 'on_sign_in'], 20, 1);
|
||||||
|
add_action('publish_post', [$this, 'on_publish_post'], 20, 2);
|
||||||
|
add_action('comment_post', [$this, 'on_comment_post'], 20, 3);
|
||||||
|
add_action('aksrt_user_followed', [$this, 'on_followed'], 20, 2);
|
||||||
|
add_action('aksrt_post_liked', [$this, 'on_post_like'], 20, 2);
|
||||||
|
add_action('aksrt_comment_liked', [$this, 'on_comment_like'], 20, 2);
|
||||||
|
add_action('aksrt_post_favorited', [$this, 'on_post_favorite'], 20, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{enabled:bool,public_display:bool,prefix:string,max:int,source:string,level_opt:array<string,mixed>,integral_opt:array<string,mixed>,notify:bool}
|
||||||
|
*/
|
||||||
|
public static function settings(): array {
|
||||||
|
$all = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_all() : [];
|
||||||
|
$enabled = !empty($all['user_level_s']) || !empty($all['user_level_enabled']);
|
||||||
|
$max = max(3, min(30, (int) ($all['user_level_max'] ?? 10)));
|
||||||
|
$source = (string) ($all['user_level_source'] ?? 'integral');
|
||||||
|
if (!in_array($source, ['integral', 'manual_meta'], true)) {
|
||||||
|
$source = 'integral';
|
||||||
|
}
|
||||||
|
$level_opt = $all['user_level_opt'] ?? [];
|
||||||
|
if (is_string($level_opt)) {
|
||||||
|
$decoded = json_decode($level_opt, true);
|
||||||
|
$level_opt = is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
if (!is_array($level_opt)) {
|
||||||
|
$level_opt = [];
|
||||||
|
}
|
||||||
|
$integral_opt = $all['user_integral_opt'] ?? [];
|
||||||
|
if (is_string($integral_opt)) {
|
||||||
|
$decoded = json_decode($integral_opt, true);
|
||||||
|
$integral_opt = is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
if (!is_array($integral_opt)) {
|
||||||
|
$integral_opt = [];
|
||||||
|
}
|
||||||
|
$prefix = trim((string) ($all['user_level_prefix'] ?? 'Lv.'));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => $enabled,
|
||||||
|
'public_display' => !isset($all['user_level_public_display']) || !empty($all['user_level_public_display']),
|
||||||
|
'prefix' => $prefix !== '' ? $prefix : 'Lv.',
|
||||||
|
'max' => $max,
|
||||||
|
'source' => $source,
|
||||||
|
'level_opt' => $level_opt,
|
||||||
|
'integral_opt' => $integral_opt,
|
||||||
|
'notify' => !isset($all['user_level_notify']) || !empty($all['user_level_notify']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalog of integral earn keys (defaults).
|
||||||
|
*
|
||||||
|
* @return array<string,int>
|
||||||
|
*/
|
||||||
|
public static function integral_defaults(): array {
|
||||||
|
return [
|
||||||
|
'day_max' => 100,
|
||||||
|
'sign_up' => 20,
|
||||||
|
'sign_in' => 5,
|
||||||
|
'followed' => 5,
|
||||||
|
'post_new' => 5,
|
||||||
|
'post_like' => 1,
|
||||||
|
'post_favorite' => 2,
|
||||||
|
'comment_new' => 2,
|
||||||
|
'comment_like' => 1,
|
||||||
|
'bbs_posts_new' => 3,
|
||||||
|
'bbs_score_extra' => 1,
|
||||||
|
'bbs_essence' => 2,
|
||||||
|
'bbs_posts_hot' => 2,
|
||||||
|
'bbs_plate_new' => 2,
|
||||||
|
'bbs_plate_hot' => 2,
|
||||||
|
'bbs_adopt' => 2,
|
||||||
|
'bbs_comment_hot' => 2,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function task_points(string $key): int {
|
||||||
|
$cfg = self::settings();
|
||||||
|
$opt = $cfg['integral_opt'];
|
||||||
|
$def = self::integral_defaults();
|
||||||
|
if (array_key_exists($key, $opt)) {
|
||||||
|
return max(0, (int) $opt[$key]);
|
||||||
|
}
|
||||||
|
return max(0, (int) ($def[$key] ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function day_max(): int {
|
||||||
|
$cfg = self::settings();
|
||||||
|
$opt = $cfg['integral_opt'];
|
||||||
|
if (array_key_exists('day_max', $opt)) {
|
||||||
|
return max(0, (int) $opt['day_max']);
|
||||||
|
}
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_integral(int $user_id): int {
|
||||||
|
if ($user_id <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return max(0, (int) get_user_meta($user_id, self::META_INTEGRAL, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_level(int $user_id): int {
|
||||||
|
if ($user_id <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$cfg = self::settings();
|
||||||
|
if (!$cfg['enabled']) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ($cfg['source'] === 'manual_meta') {
|
||||||
|
return max(0, (int) get_user_meta($user_id, self::META_LEVEL, true));
|
||||||
|
}
|
||||||
|
return self::resolve_level(self::get_integral($user_id), $cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{max:int,level_opt:array} $cfg
|
||||||
|
*/
|
||||||
|
public static function resolve_level(int $integral, ?array $cfg = null): int {
|
||||||
|
$cfg = $cfg ?? self::settings();
|
||||||
|
$max = (int) $cfg['max'];
|
||||||
|
$opt = $cfg['level_opt'];
|
||||||
|
$level = 1;
|
||||||
|
for ($i = 1; $i <= $max; $i++) {
|
||||||
|
$need = (int) ($opt['upgrade_integral_' . $i] ?? ($i === 1 ? 0 : PHP_INT_MAX));
|
||||||
|
if ($integral >= $need) {
|
||||||
|
$level = $i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max(0, min($max, $level));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grant integral for a task key. Respects day_max.
|
||||||
|
*
|
||||||
|
* @return int|false points granted or false
|
||||||
|
*/
|
||||||
|
public static function add(int $user_id, string $key, int $points = 0) {
|
||||||
|
if ($user_id <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$cfg = self::settings();
|
||||||
|
if (!$cfg['enabled'] || $cfg['source'] !== 'integral') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$key = sanitize_key($key);
|
||||||
|
if ($key === '' || $key === 'day_max') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($points <= 0) {
|
||||||
|
$points = self::task_points($key);
|
||||||
|
}
|
||||||
|
if ($points <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$day_max = self::day_max();
|
||||||
|
$today = gmdate('Y-m-d', current_time('timestamp'));
|
||||||
|
$date_detail = get_user_meta($user_id, self::META_DATE_DETAIL, true);
|
||||||
|
$date_detail = is_array($date_detail) ? $date_detail : [];
|
||||||
|
$today_sum = max(0, (int) ($date_detail[$today] ?? 0));
|
||||||
|
if ($day_max > 0 && $today_sum >= $day_max) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($day_max > 0) {
|
||||||
|
$points = min($points, $day_max - $today_sum);
|
||||||
|
}
|
||||||
|
if ($points <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = self::get_integral($user_id);
|
||||||
|
$after = $before + $points;
|
||||||
|
update_user_meta($user_id, self::META_INTEGRAL, $after);
|
||||||
|
|
||||||
|
$date_detail[$today] = $today_sum + $points;
|
||||||
|
// Keep last 90 days.
|
||||||
|
if (count($date_detail) > 90) {
|
||||||
|
krsort($date_detail);
|
||||||
|
$date_detail = array_slice($date_detail, 0, 90, true);
|
||||||
|
}
|
||||||
|
update_user_meta($user_id, self::META_DATE_DETAIL, $date_detail);
|
||||||
|
|
||||||
|
$detail = get_user_meta($user_id, self::META_DETAIL, true);
|
||||||
|
$detail = is_array($detail) ? $detail : [];
|
||||||
|
$prev = is_array($detail[$key] ?? null) ? $detail[$key] : ['value' => 0, 'integral' => 0];
|
||||||
|
$detail[$key] = [
|
||||||
|
'value' => (int) ($prev['value'] ?? 0) + $points,
|
||||||
|
'integral' => (int) ($prev['integral'] ?? 0) + $points,
|
||||||
|
];
|
||||||
|
update_user_meta($user_id, self::META_DETAIL, $detail);
|
||||||
|
|
||||||
|
$old_level = (int) get_user_meta($user_id, self::META_LEVEL, true);
|
||||||
|
$new_level = self::resolve_level($after, $cfg);
|
||||||
|
if ($new_level !== $old_level) {
|
||||||
|
update_user_meta($user_id, self::META_LEVEL, $new_level);
|
||||||
|
/**
|
||||||
|
* @param int $user_id
|
||||||
|
* @param int $new_level
|
||||||
|
* @param int $old_level
|
||||||
|
*/
|
||||||
|
do_action('aksrt_user_level_changed', $user_id, $new_level, $old_level);
|
||||||
|
} else {
|
||||||
|
update_user_meta($user_id, self::META_LEVEL, $new_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $points;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function on_register(int $user_id): void {
|
||||||
|
self::add($user_id, 'sign_up');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $user_login
|
||||||
|
* @param WP_User $user
|
||||||
|
*/
|
||||||
|
public function on_login($user_login, $user): void {
|
||||||
|
$user_id = $user instanceof WP_User ? (int) $user->ID : 0;
|
||||||
|
if ($user_id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Once per calendar day for login XP.
|
||||||
|
$today = gmdate('Y-m-d', current_time('timestamp'));
|
||||||
|
$flag = (string) get_user_meta($user_id, '_aksrt_integral_sign_in_day', true);
|
||||||
|
if ($flag === $today) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$granted = self::add($user_id, 'sign_in');
|
||||||
|
if ($granted) {
|
||||||
|
update_user_meta($user_id, '_aksrt_integral_sign_in_day', $today);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function on_sign_in(int $user_id): void {
|
||||||
|
// Dedicated check-in hook can also grant if separate from login; reuse sign_in key once/day.
|
||||||
|
$this->on_login('', get_userdata($user_id) ?: new WP_User($user_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $post_id
|
||||||
|
* @param WP_Post $post
|
||||||
|
*/
|
||||||
|
public function on_publish_post($post_id, $post): void {
|
||||||
|
if (!$post instanceof WP_Post || $post->post_type !== 'post' || $post->post_status !== 'publish') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only when transitioning to publish first time.
|
||||||
|
if ((string) get_post_meta($post_id, '_aksrt_integral_post_new', true) === '1') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$author = (int) $post->post_author;
|
||||||
|
if ($author <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self::add($author, 'post_new')) {
|
||||||
|
update_post_meta($post_id, '_aksrt_integral_post_new', '1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $comment_id
|
||||||
|
* @param int|string $approved
|
||||||
|
* @param array $commentdata
|
||||||
|
*/
|
||||||
|
public function on_comment_post($comment_id, $approved, $commentdata): void {
|
||||||
|
if ((string) $approved !== '1' && $approved !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$user_id = (int) ($commentdata['user_id'] ?? 0);
|
||||||
|
if ($user_id <= 0) {
|
||||||
|
$user_id = (int) get_comment($comment_id)->user_id;
|
||||||
|
}
|
||||||
|
if ($user_id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::add($user_id, 'comment_new');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $follower_id
|
||||||
|
* @param int $followed_id
|
||||||
|
*/
|
||||||
|
public function on_followed($follower_id, $followed_id): void {
|
||||||
|
$followed_id = (int) $followed_id;
|
||||||
|
if ($followed_id <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::add($followed_id, 'followed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $post_id
|
||||||
|
* @param int $user_id liker
|
||||||
|
*/
|
||||||
|
public function on_post_like($post_id, $user_id): void {
|
||||||
|
$post = get_post((int) $post_id);
|
||||||
|
if (!$post instanceof WP_Post) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$author = (int) $post->post_author;
|
||||||
|
if ($author <= 0 || $author === (int) $user_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::add($author, 'post_like');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $comment_id
|
||||||
|
* @param int $user_id
|
||||||
|
*/
|
||||||
|
public function on_comment_like($comment_id, $user_id): void {
|
||||||
|
$c = get_comment((int) $comment_id);
|
||||||
|
if (!$c) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$author = (int) $c->user_id;
|
||||||
|
if ($author <= 0 || $author === (int) $user_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::add($author, 'comment_like');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $post_id
|
||||||
|
* @param int $user_id
|
||||||
|
*/
|
||||||
|
public function on_post_favorite($post_id, $user_id): void {
|
||||||
|
$post = get_post((int) $post_id);
|
||||||
|
if (!$post instanceof WP_Post) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$author = (int) $post->post_author;
|
||||||
|
if ($author <= 0 || $author === (int) $user_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::add($author, 'post_favorite');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,11 +149,12 @@ class AKSRT_Settings_User_Runtime {
|
|||||||
'enabled' => !empty($settings['user_level_enabled']) || !empty($settings['user_level_s']),
|
'enabled' => !empty($settings['user_level_enabled']) || !empty($settings['user_level_s']),
|
||||||
'public_display' => !isset($settings['user_level_public_display']) || !empty($settings['user_level_public_display']),
|
'public_display' => !isset($settings['user_level_public_display']) || !empty($settings['user_level_public_display']),
|
||||||
'source' => (static function () use ($settings) {
|
'source' => (static function () use ($settings) {
|
||||||
$v = $settings['user_level_source'] ?? 'credits_balance';
|
$v = $settings['user_level_source'] ?? 'integral';
|
||||||
return in_array($v, ['credits_balance', 'manual_meta', 'integral'], true) ? $v : 'credits_balance';
|
return in_array($v, ['credits_balance', 'manual_meta', 'integral'], true) ? $v : 'integral';
|
||||||
})(),
|
})(),
|
||||||
'label_prefix' => $level_prefix !== '' ? $level_prefix : 'Lv.',
|
'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")),
|
'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,
|
'medal' => $medal,
|
||||||
// Alias for existing call sites that still read badge.*; dictionary is derived from medal catalog.
|
// Alias for existing call sites that still read badge.*; dictionary is derived from medal catalog.
|
||||||
|
|||||||
Reference in new issue
Block a user