feat: unify invite flow with referral ref

This commit is contained in:
2026-08-17 13:49:59 +08:00
parent f1d6809109
commit b501cf4611
47 changed files with 1083 additions and 2548 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ import { renderFaIcon, schemaPage, customPage } from './helpers'
/**
* Users SPA keeps config-shaped pages.
* Message / invite / ban / report queues live under users.php native menus.
* Message / referral-code / ban / report queues live under users.php native menus.
*/
export const usersSection = {
key: 'users',
+16 -14
View File
@@ -68,22 +68,25 @@
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>邀请奖励</h3></div>
<n-form :model="cfg.invite_reward" label-placement="left" label-width="200" class="aksrt-form">
<div class="aksrt-section-header"><h3>Ref 推广奖励</h3></div>
<n-form :model="cfg.referral_reward" label-placement="left" label-width="200" class="aksrt-form">
<n-form-item label="启用">
<n-switch v-model:value="cfg.invite_reward.enabled" />
<n-switch v-model:value="cfg.referral_reward.enabled" />
</n-form-item>
<n-form-item label="邀请人奖励">
<n-input-number v-model:value="cfg.invite_reward.inviter_amount" :min="0" :max="10000" />
<n-form-item label="推广人奖励">
<n-input-number v-model:value="cfg.referral_reward.promoter_amount" :min="0" :max="10000" />
</n-form-item>
<n-form-item label="被邀请人奖励">
<n-input-number v-model:value="cfg.invite_reward.invitee_amount" :min="0" :max="10000" />
<n-form-item label="新用户奖励">
<n-input-number v-model:value="cfg.referral_reward.referred_amount" :min="0" :max="10000" />
</n-form-item>
<n-form-item label="推广等级积分">
<n-input-number v-model:value="cfg.referral_reward.level_points" :min="0" :max="10000" />
</n-form-item>
<n-form-item label="需邮箱验证后发放">
<n-switch v-model:value="cfg.invite_reward.require_verify" />
<n-switch v-model:value="cfg.referral_reward.require_verify" />
</n-form-item>
<n-form-item label="同 IP 当日上限">
<n-input-number v-model:value="cfg.invite_reward.ip_daily_cap" :min="0" :max="100" />
<n-input-number v-model:value="cfg.referral_reward.ip_daily_cap" :min="0" :max="100" />
</n-form-item>
</n-form>
</n-card>
@@ -120,14 +123,13 @@ const cfg = reactive({
register_bonus: 10,
daily_signin: { enabled: true, base: 1, streak: { 3: 2, 7: 5, 30: 20 } },
comment_reward: { enabled: true, amount: 1, daily_cap: 5, min_length: 10, on_approved: true },
invite_reward: {
referral_reward: {
enabled: true,
inviter_amount: 5,
invitee_amount: 2,
promoter_amount: 5,
referred_amount: 2,
level_points: 0,
require_verify: true,
ip_daily_cap: 3,
register_field: 'optional',
reject_invalid: true,
},
download_default_cost: 0,
})
@@ -121,7 +121,6 @@ const typeOptions = computed(() => optionsState.types.length
: [
{ label: '全部类型', value: '' },
{ label: '订单返佣', value: 'order' },
{ label: '邀请返佣', value: 'invite' },
])
const columnChoices = [
+1 -1
View File
@@ -92,7 +92,7 @@ const guides = [
{ key: 'cdn', title: '静态资源与 CDN', desc: '核对前端资源地址与缓存策略', url: '#/global/common' },
{ key: 'ip-geo', title: 'IP 归属地', desc: '验证 IP 查询服务与密钥配置', url: '#/extensions/ip' },
{ key: 'payment', title: '支付与会员', desc: '核对网关、会员与充值配置', url: '#/payment/gateway' },
{ key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与邀请奖励', url: '#/users/signin' },
{ key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与推广奖励', url: '#/users/signin' },
{ key: 'badge', title: '等级与徽章', desc: '维护等级、认证与徽章体系', url: '#/users/badge' },
]
@@ -1,512 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="邀请码运营" desc="统一管理用户邀请码与独立邀请码,支持批量生成、批量禁用、备注追踪和筛选导出。" />
<div class="aksrt-invite-ops__stats">
<n-card v-for="card in statCards" :key="card.key" size="small">
<n-statistic :label="card.label" :value="stats[card.key] || 0" />
</n-card>
</div>
<n-grid cols="1 s:1 m:2" :x-gap="16" :y-gap="16" responsive="screen">
<n-grid-item>
<n-card title="批量生成邀请码" size="small">
<n-form :model="generateForm" label-placement="top">
<n-grid cols="2" :x-gap="12">
<n-grid-item>
<n-form-item label="生成数量">
<n-input-number v-model:value="generateForm.count" :min="1" :max="100" style="width: 100%;" />
</n-form-item>
</n-grid-item>
<n-grid-item>
<n-form-item label="随机长度">
<n-input-number v-model:value="generateForm.length" :min="4" :max="12" style="width: 100%;" />
</n-form-item>
</n-grid-item>
</n-grid>
<n-form-item label="前缀">
<n-input v-model:value="generateForm.prefix" placeholder="AK" />
</n-form-item>
<n-form-item label="批次备注">
<n-input
v-model:value="generateForm.note"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
placeholder="例如:618 拉新活动 / 渠道合作批次"
/>
</n-form-item>
<n-space>
<n-button type="primary" :loading="generating" @click="handleGenerate">批量生成</n-button>
<n-button @click="loadData">刷新列表</n-button>
</n-space>
</n-form>
</n-card>
</n-grid-item>
<n-grid-item>
<n-card title="批次输出" size="small">
<n-alert type="info" :show-icon="false">
独立邀请码会接入注册校验链路;导出前建议补齐批次备注,便于渠道回溯。
</n-alert>
<div v-if="generatedCodes.length" class="aksrt-invite-ops__generated">
<n-tag type="success" round :bordered="false">批次 {{ generatedBatchId || '-' }}</n-tag>
<n-input
type="textarea"
:value="generatedCodes.join('\n')"
readonly
:autosize="{ minRows: 7, maxRows: 10 }"
/>
</div>
<n-empty v-else description="生成后的邀请码会显示在这里" />
</n-card>
</n-grid-item>
</n-grid>
<n-card title="邀请码台账" size="small" class="aksrt-invite-ops__table-card">
<n-space vertical :size="12">
<n-grid cols="1 s:2 m:4" :x-gap="12" :y-gap="12" responsive="screen">
<n-grid-item>
<n-input v-model:value="filters.keyword" placeholder="搜索邀请码、批次、用户、备注" clearable />
</n-grid-item>
<n-grid-item>
<n-select v-model:value="filters.status" :options="statusOptions" placeholder="状态" />
</n-grid-item>
<n-grid-item>
<n-select v-model:value="filters.source" :options="sourceOptions" placeholder="来源" />
</n-grid-item>
<n-grid-item>
<n-date-picker
v-model:value="filters.dateRange"
type="daterange"
clearable
style="width: 100%;"
/>
</n-grid-item>
</n-grid>
<n-space justify="space-between" wrap>
<n-space>
<n-button type="primary" @click="handleSearch">筛选</n-button>
<n-button @click="resetFilters">重置</n-button>
<n-button :disabled="!checkedRows.length" @click="handleBulkAction('disable')">批量禁用</n-button>
<n-button :disabled="!checkedRows.length" @click="handleBulkAction('delete')">批量删除</n-button>
</n-space>
<n-button quaternary @click="handleExport">导出当前筛选</n-button>
</n-space>
<n-data-table
:columns="columns"
:data="rows"
:loading="loading"
:bordered="false"
:pagination="pagination"
:row-key="(row) => row.row_key"
:checked-row-keys="checkedRowKeys"
@update:checked-row-keys="handleCheckedRowKeysChange"
/>
</n-space>
</n-card>
</div>
</template>
<script setup>
import { computed, h, onMounted, reactive, ref } from 'vue'
import { NButton, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api'
const message = useMessage()
const dialog = useDialog()
const loading = ref(false)
const generating = ref(false)
const rows = ref([])
const checkedRowKeys = ref([])
const generatedCodes = ref([])
const generatedBatchId = ref('')
const stats = reactive({
total_codes: 0,
active_codes: 0,
used_codes: 0,
disabled_codes: 0,
user_codes: 0,
standalone_codes: 0,
total_invites: 0,
total_rewarded: 0,
})
const generateForm = reactive({
count: 20,
length: 6,
prefix: 'AK',
note: '',
})
const filters = reactive({
keyword: '',
status: '',
source: '',
dateRange: null,
})
const pager = reactive({
page: 1,
pageSize: 20,
itemCount: 0,
})
const optionsState = reactive({
statuses: [],
sources: [],
})
const statCards = [
{ key: 'total_codes', label: '总邀请码' },
{ key: 'active_codes', label: '当前可用' },
{ key: 'used_codes', label: '已使用' },
{ key: 'disabled_codes', label: '已禁用' },
{ key: 'standalone_codes', label: '独立邀请码' },
{ key: 'total_rewarded', label: '已完成兑现' },
]
const statusOptions = computed(() => optionsState.statuses.length
? optionsState.statuses
: [
{ label: '全部状态', value: '' },
{ label: '可用', value: 'active' },
{ label: '已使用', value: 'used' },
{ label: '已禁用', value: 'disabled' },
])
const sourceOptions = computed(() => optionsState.sources.length
? optionsState.sources
: [
{ label: '全部来源', value: '' },
{ label: '用户邀请码', value: 'user' },
{ label: '独立邀请码', value: 'standalone' },
])
const checkedRows = computed(() => rows.value.filter((row) => checkedRowKeys.value.includes(row.row_key)))
const pagination = computed(() => ({
page: pager.page,
pageSize: pager.pageSize,
itemCount: pager.itemCount,
showSizePicker: true,
pageSizes: [10, 20, 50, 100],
onUpdatePage: (page) => {
pager.page = page
loadData()
},
onUpdatePageSize: (pageSize) => {
pager.pageSize = pageSize
pager.page = 1
loadData()
},
}))
const columns = [
{
type: 'selection',
disabled: (row) => !row.can_bulk,
},
{
title: '来源',
key: 'source_type',
width: 120,
render: (row) => h(NTag, { size: 'small', round: true, type: row.source_type === 'standalone' ? 'info' : 'default' }, () => row.source_label),
},
{
title: '邀请码',
key: 'invite_code',
width: 180,
ellipsis: { tooltip: true },
},
{
title: '归属/使用人',
key: 'owner',
width: 180,
render: (row) => row.user?.display_name || row.invitee?.display_name || '-',
},
{
title: '状态',
key: 'status_label',
width: 110,
render: (row) => {
const typeMap = { active: 'success', used: 'warning', disabled: 'error' }
return h(NTag, { size: 'small', round: true, type: typeMap[row.status] || 'default' }, () => row.status_label)
},
},
{
title: '注册状态',
key: 'registration_status',
width: 110,
render: (row) => row.registration_status || '-',
},
{
title: '批次备注',
key: 'note',
minWidth: 180,
ellipsis: { tooltip: true },
render: (row) => row.note || row.batch_id || '-',
},
{
title: '创建时间',
key: 'created_at',
width: 170,
},
{
title: '使用时间',
key: 'used_at',
width: 170,
render: (row) => row.used_at || '-',
},
{
title: '操作',
key: 'actions',
width: 170,
render: (row) => h(NSpace, { size: 'small' }, () => {
const actions = [
h(NButton, {
size: 'tiny',
quaternary: true,
type: 'warning',
onClick: () => handleDisable(row),
}, () => '禁用'),
]
if (row.source_type === 'standalone') {
actions.push(
h(NButton, {
size: 'tiny',
quaternary: true,
type: 'error',
onClick: () => handleDelete(row),
}, () => '删除'),
)
}
return actions
}),
},
]
onMounted(() => {
loadData()
})
function formatDateValue(value) {
const date = new Date(value)
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${year}-${month}-${day}`
}
function buildQuery(page = pager.page, pageSize = pager.pageSize) {
const params = {
page,
per_page: pageSize,
}
if (filters.keyword) params.keyword = filters.keyword
if (filters.status) params.status = filters.status
if (filters.source) params.source = filters.source
if (Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
params.date_from = formatDateValue(filters.dateRange[0])
params.date_to = formatDateValue(filters.dateRange[1])
}
return params
}
async function loadData() {
loading.value = true
try {
const res = await api.request('/invite/codes', { params: buildQuery() })
const data = res?.data || {}
rows.value = data.items || []
pager.itemCount = data.total || 0
Object.assign(stats, data.stats || {})
optionsState.statuses = data.options?.statuses || []
optionsState.sources = data.options?.sources || []
checkedRowKeys.value = []
} catch (error) {
message.error(error?.message || '加载邀请码台账失败')
} finally {
loading.value = false
}
}
function handleSearch() {
pager.page = 1
loadData()
}
function resetFilters() {
filters.keyword = ''
filters.status = ''
filters.source = ''
filters.dateRange = null
pager.page = 1
loadData()
}
function handleCheckedRowKeysChange(keys) {
checkedRowKeys.value = keys
}
async function handleGenerate() {
generating.value = true
try {
const res = await api.request('/invite/codes', {
method: 'POST',
data: {
count: generateForm.count,
length: generateForm.length,
prefix: generateForm.prefix,
note: generateForm.note,
},
})
generatedCodes.value = res?.data?.codes || []
generatedBatchId.value = res?.data?.batch_id || ''
message.success(res?.message || '邀请码已生成')
await loadData()
} catch (error) {
message.error(error?.message || '生成失败')
} finally {
generating.value = false
}
}
function handleDisable(row) {
dialog.warning({
title: '确认禁用',
content: `确定要禁用邀请码 ${row.invite_code} 吗?`,
positiveText: '禁用',
negativeText: '取消',
onPositiveClick: async () => {
try {
if (row.source_type === 'user') {
await api.request(`/invite/codes/${row.user?.id}/disable`, { method: 'POST' })
} else {
await api.request('/invite/codes/bulk', {
method: 'POST',
data: {
action: 'disable',
targets: [{ source_type: 'standalone', code: row.invite_code }],
},
})
}
message.success('邀请码已禁用')
await loadData()
} catch (error) {
message.error(error?.message || '操作失败')
}
},
})
}
function handleDelete(row) {
dialog.warning({
title: '确认删除',
content: `确定要删除独立邀请码 ${row.invite_code} 吗?`,
positiveText: '删除',
negativeText: '取消',
onPositiveClick: async () => {
try {
await api.request('/invite/codes/bulk', {
method: 'POST',
data: {
action: 'delete',
targets: [{ source_type: 'standalone', code: row.invite_code }],
},
})
message.success('邀请码已删除')
await loadData()
} catch (error) {
message.error(error?.message || '删除失败')
}
},
})
}
async function handleBulkAction(action) {
if (!checkedRows.value.length) {
return
}
try {
await api.request('/invite/codes/bulk', {
method: 'POST',
data: {
action,
targets: checkedRows.value.map((row) => ({
source_type: row.source_type,
user_id: row.user?.id || 0,
code: row.invite_code,
})),
},
})
message.success(action === 'delete' ? '批量删除完成' : '批量禁用完成')
await loadData()
} catch (error) {
message.error(error?.message || '批量操作失败')
}
}
async function handleExport() {
try {
const res = await api.request('/invite/codes', { params: buildQuery(1, 200) })
const items = res?.data?.items || []
const header = ['来源', '邀请码', '归属用户', '使用人', '状态', '注册状态', '批次', '备注', '创建时间', '使用时间']
const lines = items.map((row) => [
row.source_label || '',
row.invite_code || '',
row.user?.display_name || '',
row.invitee?.display_name || '',
row.status_label || '',
row.registration_status || '',
row.batch_id || '',
row.note || '',
row.created_at || '',
row.used_at || '',
])
downloadCsv('invite-codes.csv', [header, ...lines])
message.success('导出完成')
} catch (error) {
message.error(error?.message || '导出失败')
}
}
function downloadCsv(filename, rows) {
const csv = `\uFEFF${rows.map((row) => row.map((cell) => `"${String(cell ?? '').replaceAll('"', '""')}"`).join(',')).join('\n')}`
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
</script>
<style scoped>
.aksrt-invite-ops__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.aksrt-invite-ops__generated {
display: grid;
gap: 12px;
margin-top: 16px;
}
.aksrt-invite-ops__table-card {
margin-top: 16px;
}
</style>
+68 -53
View File
@@ -1,7 +1,7 @@
<template>
<UsersSectionPage
title="签到奖励"
desc="直接管理签到与邀请奖励的真实运行配置,保存后会立即影响前台签到、邀请链接和注册页邀请码行为。"
desc="直接管理签到与 Ref 推广奖励,保存后会立即影响前台签到、推广链接和注册页推广码行为。"
:links="[
{ label: '注册登录', path: '/users/auth' },
{ label: '用户积分', path: '/users/credits' },
@@ -20,11 +20,11 @@
<n-tag round :type="form.signin_enabled ? 'success' : 'default'">
签到能力:{{ form.signin_enabled ? '已启用' : '已关闭' }}
</n-tag>
<n-tag round :type="form.invite_enabled ? 'success' : 'default'">
邀请奖励:{{ form.invite_enabled ? '已启用' : '已关闭' }}
<n-tag round :type="form.referral_reward_enabled ? 'success' : 'default'">
推广奖励:{{ form.referral_reward_enabled ? '已启用' : '已关闭' }}
</n-tag>
<n-tag round :type="form.invite_registration_enabled ? 'info' : 'default'">
邀请注册:{{ form.invite_registration_enabled ? '开启' : '关闭' }}
<n-tag round :type="form.referral_registration_enabled ? 'info' : 'default'">
推广码注册:{{ form.referral_registration_enabled ? '开启' : '关闭' }}
</n-tag>
<n-tag round :type="captchaSummary.register ? 'info' : 'default'">
注册验证码:{{ captchaSummary.register ? '启用' : '未启用' }}
@@ -37,7 +37,7 @@
<n-card v-if="!form.credits_enabled" class="aksrt-section-card">
<n-alert type="warning" :show-icon="false">
当前积分系统处于关闭状态。即使保存签到和邀请奖励配置,前台也不会真正发放奖励,直到“用户积分”页重新启用积分系统。
当前积分系统处于关闭状态。即使保存签到和推广奖励配置,前台也不会真正发放奖励,直到“用户积分”页重新启用积分系统。
</n-alert>
</n-card>
</n-spin>
@@ -75,27 +75,34 @@
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>邀请奖励与注册页邀请码</h3>
<h3>Ref 推广奖励与注册</h3>
</div>
<n-form-item label="启用邀请奖励">
<n-switch v-model:value="form.invite_enabled" />
<n-form-item label="启用推广奖励">
<n-switch v-model:value="form.referral_reward_enabled" />
</n-form-item>
<n-form-item label="开放邀请注册">
<n-switch v-model:value="form.invite_registration_enabled" />
<n-form-item label="Ref 码长度">
<n-input-number v-model:value="form.referral_code_length" :min="4" :max="12" style="width: 180px;" />
<template #feedback>
<span class="aksrt-hint-text">关闭后,邀请链接入口、注册页邀请码字段和个人中心邀请模块会一起隐藏。</span>
<span class="aksrt-hint-text">只影响新生成或管理员重新生成的推广码,已有 Ref 不会失效。</span>
</template>
</n-form-item>
<template v-if="form.invite_registration_enabled">
<n-form-item label="邀请码长度">
<n-input-number v-model:value="form.invite_code_length" :min="4" :max="12" style="width: 180px;" />
<n-form-item label="首次触达窗口(天)">
<n-input-number v-model:value="form.referral_window_days" :min="1" :max="365" style="width: 180px;" />
</n-form-item>
<n-form-item label="注册页邀请码字段">
<n-radio-group v-model:value="form.invite_register_field">
<n-form-item label="开放推广码注册">
<n-switch v-model:value="form.referral_registration_enabled" />
<template #feedback>
<span class="aksrt-hint-text">此开关只控制注册页推广码字段;现金返佣和积分奖励仍可独立使用同一 Ref 关系。</span>
</template>
</n-form-item>
<template v-if="form.referral_registration_enabled">
<n-form-item label="注册页推广码字段">
<n-radio-group v-model:value="form.referral_register_field">
<n-space size="large">
<n-radio value="hidden">隐藏</n-radio>
<n-radio value="optional">选填</n-radio>
@@ -104,21 +111,25 @@
</n-radio-group>
</n-form-item>
<n-form-item label="无效邀请码处理" v-if="form.invite_register_field === 'required'">
<n-switch v-model:value="form.invite_reject_invalid" />
<n-form-item label="无效推广码处理" v-if="form.referral_register_field === 'required'">
<n-switch v-model:value="form.referral_reject_invalid" />
<template #feedback>
<span class="aksrt-hint-text">开启后,必填模式下无效邀请码会直接拒绝注册。关闭则忽略邀请码错误并继续注册。</span>
<span class="aksrt-hint-text">开启后,必填模式下无效推广码会直接拒绝注册。首次触达 Ref 始终优先于手填码。</span>
</template>
</n-form-item>
</template>
<template v-if="form.invite_enabled">
<n-form-item label="邀请人奖励">
<n-input-number v-model:value="form.inviter_amount" :min="0" :max="100000" style="width: 180px;" />
<template v-if="form.referral_reward_enabled">
<n-form-item label="推广人奖励">
<n-input-number v-model:value="form.promoter_amount" :min="0" :max="100000" style="width: 180px;" />
</n-form-item>
<n-form-item label="被邀请人奖励">
<n-input-number v-model:value="form.invitee_amount" :min="0" :max="100000" style="width: 180px;" />
<n-form-item label="新用户奖励">
<n-input-number v-model:value="form.referred_amount" :min="0" :max="100000" style="width: 180px;" />
</n-form-item>
<n-form-item label="推广等级积分">
<n-input-number v-model:value="form.level_points" :min="0" :max="100000" style="width: 180px;" />
</n-form-item>
<n-form-item label="验证邮箱后发放">
@@ -162,15 +173,17 @@ const form = reactive({
signin_enabled: true,
signin_base: 1,
signin_streak_text: '3|2\n7|5\n30|20',
invite_enabled: true,
inviter_amount: 5,
invitee_amount: 2,
referral_reward_enabled: true,
promoter_amount: 5,
referred_amount: 2,
level_points: 0,
require_verify: true,
ip_daily_cap: 3,
invite_registration_enabled: true,
invite_code_length: 6,
invite_register_field: 'optional',
invite_reject_invalid: true,
referral_registration_enabled: true,
referral_code_length: 12,
referral_window_days: 30,
referral_register_field: 'optional',
referral_reject_invalid: true,
})
const currencyLabel = computed(() => form.currency_name || '积分')
@@ -226,7 +239,7 @@ async function load() {
const authData = extractData(authRes)
const captchaData = extractData(captchaRes)
const dailySignin = creditData.daily_signin || {}
const inviteReward = creditData.invite_reward || {}
const referralReward = creditData.referral_reward || {}
const enabledScenarios = Array.isArray(captchaData.enabled_scenarios)
? captchaData.enabled_scenarios
: []
@@ -240,15 +253,17 @@ async function load() {
form.signin_enabled = dailySignin.enabled !== false
form.signin_base = Number(dailySignin.base || 0)
form.signin_streak_text = formatStreak(dailySignin.streak || { 3: 2, 7: 5, 30: 20 })
form.invite_enabled = inviteReward.enabled !== false
form.inviter_amount = Number(inviteReward.inviter_amount || 0)
form.invitee_amount = Number(inviteReward.invitee_amount || 0)
form.require_verify = inviteReward.require_verify !== false
form.ip_daily_cap = Number(inviteReward.ip_daily_cap || 0)
form.invite_registration_enabled = authData.invite_registration_enabled !== false
form.invite_code_length = Number(authData.invite_code_length || 6)
form.invite_register_field = authData.invite_register_field || 'optional'
form.invite_reject_invalid = authData.invite_reject_invalid !== false
form.referral_reward_enabled = referralReward.enabled !== false
form.promoter_amount = Number(referralReward.promoter_amount || 0)
form.referred_amount = Number(referralReward.referred_amount || 0)
form.level_points = Number(referralReward.level_points || 0)
form.require_verify = referralReward.require_verify !== false
form.ip_daily_cap = Number(referralReward.ip_daily_cap || 0)
form.referral_registration_enabled = authData.referral_registration_enabled !== false
form.referral_code_length = Number(authData.referral_code_length || 12)
form.referral_window_days = Number(authData.referral_window_days || 30)
form.referral_register_field = authData.referral_register_field || 'optional'
form.referral_reject_invalid = authData.referral_reject_invalid !== false
captchaSummary.register = !!captchaData.enabled && !!captchaData.active_provider && sceneOn('register')
captchaSummary.signin = !!captchaData.enabled && !!captchaData.active_provider && sceneOn('signin')
@@ -270,24 +285,24 @@ async function handleSave() {
base: Number(form.signin_base || 0),
streak: parseStreakText(form.signin_streak_text),
},
invite_reward: {
...(creditSnapshot.value.invite_reward || {}),
enabled: form.invite_enabled,
inviter_amount: Number(form.inviter_amount || 0),
invitee_amount: Number(form.invitee_amount || 0),
referral_reward: {
...(creditSnapshot.value.referral_reward || {}),
enabled: form.referral_reward_enabled,
promoter_amount: Number(form.promoter_amount || 0),
referred_amount: Number(form.referred_amount || 0),
level_points: Number(form.level_points || 0),
require_verify: form.require_verify,
ip_daily_cap: Number(form.ip_daily_cap || 0),
register_field: form.invite_register_field,
reject_invalid: form.invite_reject_invalid,
},
}
const nextAuth = {
...authSnapshot.value,
invite_registration_enabled: form.invite_registration_enabled,
invite_code_length: Number(form.invite_code_length || 6),
invite_register_field: form.invite_register_field,
invite_reject_invalid: form.invite_reject_invalid,
referral_registration_enabled: form.referral_registration_enabled,
referral_code_length: Number(form.referral_code_length || 12),
referral_window_days: Number(form.referral_window_days || 30),
referral_register_field: form.referral_register_field,
referral_reject_invalid: form.referral_reject_invalid,
}
await Promise.all([
+1 -1
View File
@@ -80,7 +80,7 @@ const NATIVE_OPS_REDIRECTS = [
{ path: '/payment/income', title: '创作分成运营', nativeAdminPath: 'admin.php?page=aksrt_income' },
{ path: '/payment/rebate', title: '返佣管理', nativeAdminPath: 'admin.php?page=aksrt_rebate' },
{ path: '/users/messages', title: '消息中心', nativeAdminPath: 'users.php?page=aksrt_messages' },
{ path: '/users/invite-codes', title: '邀请码运营', nativeAdminPath: 'users.php?page=aksrt_invite_codes' },
{ path: '/users/referral-codes', title: '推广码运营', nativeAdminPath: 'users.php?page=aksrt_referral_codes' },
{ path: '/users/ban', title: '禁封管理', nativeAdminPath: 'users.php?page=aksrt_user_ban&tab=restrictions' },
{ path: '/users/report', title: '禁封/举报', nativeAdminPath: 'users.php?page=aksrt_user_ban&tab=reports' },
]
@@ -9,7 +9,6 @@ export const affiliateSchema = {
affiliate_ratio_vip_1: 5,
affiliate_ratio_vip_2: 5,
affiliate_order_types: ['membership', 'content'],
affiliate_touch_window_days: 30,
affiliate_settlement_days: 7,
affiliate_promotion_text: '',
},
@@ -24,7 +23,6 @@ export const affiliateSchema = {
{ key: 'affiliate_ratio_vip_1', label: '一级会员佣金比例(%)', type: 'number', default: 5, min: 0, max: 100, dependsOn: { key: 'affiliate_enabled', value: true } },
{ key: 'affiliate_ratio_vip_2', label: '二级会员佣金比例(%)', type: 'number', default: 5, min: 0, max: 100, dependsOn: { key: 'affiliate_enabled', value: true } },
{ key: 'affiliate_order_types', label: '适用订单类型', type: 'checkbox', default: ['membership', 'content'], sanitize_type: 'array', options: [{ label: '会员订单', value: 'membership' }, { label: '付费内容', value: 'content' }, { label: '购买积分', value: 'credits' }], dependsOn: { key: 'affiliate_enabled', value: true } },
{ key: 'affiliate_touch_window_days', label: '首次触达窗口(天)', type: 'number', default: 30, min: 1, max: 365, dependsOn: { key: 'affiliate_enabled', value: true } },
{ key: 'affiliate_settlement_days', label: '结算观察期(天)', type: 'number', default: 7, min: 0, max: 90, dependsOn: { key: 'affiliate_enabled', value: true } },
],
},
+16 -10
View File
@@ -37,10 +37,13 @@ export const authSchema = {
bind_reminder_expires: 24,
mandatory_bind: 'close',
email_set_captch: true,
invit_code_s: false,
invit_code_mode: 'open',
invit_code_open_desc: '邀请码为选填,没有邀请码也可以注册',
invit_code_must_desc: '请输入邀请码',
referral_registration_enabled: true,
referral_register_field: 'optional',
referral_reject_invalid: true,
referral_code_length: 12,
referral_window_days: 30,
referral_open_desc: '推广码为选填,没有推广码也可以注册',
referral_required_desc: '请输入推广码',
sign_page_background: [],
sign_theme_mode: 'default',
sign_show_header: true,
@@ -115,14 +118,17 @@ export const authSchema = {
],
},
{
key: 'invite_inline',
title: '邀请码注册',
key: 'referral_inline',
title: '推广码注册',
icon: 'fas fa-ticket-alt',
fields: [
{ key: 'invit_code_s', label: '启用邀请码', type: 'switch', default: false, help: '与「邀请码」子页联动,此处开关控制注册表单是否显示邀请码字段' },
{ key: 'invit_code_mode', label: '邀请模式', type: 'select', default: 'open', options: [{ label: '开放注册(邀请码选填)', value: 'open' }, { label: '必须邀请码', value: 'must' }], dependsOn: { key: 'invit_code_s', value: true } },
{ key: 'invit_code_open_desc', label: '开放注册提示', type: 'input', default: '邀请码为选填,没有邀请码也可以注册', dependsOn: { key: 'invit_code_s', value: true } },
{ key: 'invit_code_must_desc', label: '必填提示', type: 'input', default: '请输入邀请码', dependsOn: { key: 'invit_code_s', value: true } },
{ key: 'referral_registration_enabled', label: '开放推广码注册', type: 'switch', default: true },
{ key: 'referral_register_field', label: '注册页推广码字段', type: 'select', default: 'optional', options: [{ label: '隐藏', value: 'hidden' }, { label: '选填', value: 'optional' }, { label: '必填', value: 'required' }], dependsOn: { key: 'referral_registration_enabled', value: true } },
{ key: 'referral_reject_invalid', label: '拒绝无效推广码', type: 'switch', default: true, dependsOn: { key: 'referral_registration_enabled', value: true } },
{ key: 'referral_code_length', label: 'Ref 码长度', type: 'number', default: 12, min: 4, max: 12 },
{ key: 'referral_window_days', label: '首次触达窗口(天)', type: 'number', default: 30, min: 1, max: 365 },
{ key: 'referral_open_desc', label: '选填提示', type: 'input', default: '推广码为选填,没有推广码也可以注册', dependsOn: { key: 'referral_registration_enabled', value: true } },
{ key: 'referral_required_desc', label: '必填提示', type: 'input', default: '请输入推广码', dependsOn: { key: 'referral_registration_enabled', value: true } },
],
},
{
@@ -17,7 +17,6 @@ export const creditsSchema = {
points_free_comment_new: 1,
points_free_comment_like: 1,
points_free_followed: 1,
points_earn_invite: 10,
points_spend_comment: 0,
points_spend_download: 0,
points_spend_view_hidden: 0,
@@ -59,7 +58,6 @@ export const creditsSchema = {
{ key: 'points_free_comment_new', label: '发表评论', type: 'number', default: 1, min: 0, max: 100 },
{ key: 'points_free_comment_like', label: '评论被赞', type: 'number', default: 1, min: 0, max: 100 },
{ key: 'points_free_followed', label: '被关注', type: 'number', default: 1, min: 0, max: 100 },
{ key: 'points_earn_invite', label: '邀请注册奖励', type: 'number', default: 10, min: 0, max: 1000, help: '邀请不计入免费日上限' },
],
},
{
@@ -6,7 +6,6 @@ import { authSchema } from './auth.js'
import { oauthSchema } from './oauth.js'
import { notificationsSchema } from './notifications.js'
import { creditsSchema } from './credits.js'
import { inviteSchema } from './invite.js'
import { featuresSchema } from './features.js'
import { signinSchema } from './signin.js'
import { verifySchema } from './verify.js'
@@ -21,7 +20,6 @@ export const usersSchemas = {
oauth: oauthSchema,
notifications: notificationsSchema,
credits: creditsSchema,
invite: inviteSchema,
features: featuresSchema,
signin: signinSchema,
verify: verifySchema,
@@ -1,68 +0,0 @@
export const inviteSchema = {
key: 'users.invite',
title: '邀请码',
icon: 'fas fa-ticket-alt',
description: '邀请码生成、管理、奖励配置。',
defaults: {
invit_code_s: false,
invit_code_open_desc: '邀请码为选填,没有邀请码也可以注册',
invit_code_must_desc: '请输入邀请码',
invit_code_mode: 'open',
invit_code_length: 8,
invit_code_prefix: '',
invit_code_max_use: 1,
invit_code_expiry: 0,
invit_code_batch: 20,
invit_reward_points: 0,
invit_reward_level: 0,
invit_reward_vip_days: 0,
invit_invitee_reward: false,
invit_code_show_count: true,
invit_code_show_remain: true,
},
groups: [
{
key: 'basic',
title: '邀请设置',
icon: 'fas fa-cog',
fields: [
{ key: 'invit_code_s', label: '启用邀请码', type: 'switch', default: false },
{ key: 'invit_code_open_desc', label: '开放注册提示', type: 'input', default: '邀请码为选填,没有邀请码也可以注册' },
{ key: 'invit_code_must_desc', label: '必填提示', type: 'input', default: '请输入邀请码' },
{ key: 'invit_code_mode', label: '邀请模式', type: 'select', default: 'open', options: [{ label: '开放注册(邀请码选填)', value: 'open' }, { label: '必须邀请码', value: 'must' }] },
],
},
{
key: 'format',
title: '码面设置',
icon: 'fas fa-barcode',
fields: [
{ key: 'invit_code_length', label: '邀请码长度', type: 'number', default: 8, min: 4, max: 32 },
{ key: 'invit_code_prefix', label: '邀请码前缀', type: 'input', default: '' },
{ key: 'invit_code_max_use', label: '单码最大使用次数', type: 'number', default: 1, min: 1, max: 9999 },
{ key: 'invit_code_expiry', label: '有效期(天)', type: 'number', default: 0, min: 0, help: '0为永久有效' },
{ key: 'invit_code_batch', label: '批量生成数量', type: 'number', default: 20, min: 1, max: 500 },
],
},
{
key: 'reward',
title: '邀请奖励',
icon: 'fas fa-gift',
fields: [
{ key: 'invit_reward_points', label: '邀请奖励积分', type: 'number', default: 0, min: 0, max: 1000 },
{ key: 'invit_reward_level', label: '邀请奖励等级', type: 'number', default: 0, min: 0, max: 10 },
{ key: 'invit_reward_vip_days', label: '奖励VIP天数', type: 'number', default: 0, min: 0, max: 3650 },
{ key: 'invit_invitee_reward', label: '被邀请人奖励', type: 'switch', default: false },
],
},
{
key: 'display',
title: '展示设置',
icon: 'fas fa-eye',
fields: [
{ key: 'invit_code_show_count', label: '显示已使用次数', type: 'switch', default: true },
{ key: 'invit_code_show_remain', label: '显示剩余次数', type: 'switch', default: true },
],
},
],
}
+2
View File
@@ -283,6 +283,7 @@
if (result.data.verification_required) {
var verifyMessage = result.data.message || '注册成功,请完成邮箱验证';
if (result.data.notice) verifyMessage += '<br>' + escapeAttribute(result.data.notice);
if (result.data.verify_url) {
verifyMessage += ' <a href="' + escapeAttribute(result.data.verify_url) + '">前往验证</a>';
}
@@ -293,6 +294,7 @@
}
var loginMessage = result.data.message || '注册成功,请登录';
if (result.data.notice) loginMessage += ' ' + result.data.notice;
switchTab('login');
prefillLogin(result.data.user_login || result.data.user_email || '');
showMessage('login', loginMessage, 'success');
+4 -3
View File
@@ -184,7 +184,7 @@
const code = form.querySelector('[name="code"]')?.value?.trim()
const password = form.querySelector('[name="password"]')?.value
const confirm = form.querySelector('[name="password_confirm"]')?.value
const inviteCode = form.querySelector('[name="invite_code"]')?.value?.trim()
const ref = form.querySelector('[name="ref"]')?.value?.trim()
if (!phone || !code || !password) {
showToast('请填写完整信息', 'warning')
@@ -211,14 +211,15 @@
fetch(REST_BASE + '/auth/phone-register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, code, password, invite_code: inviteCode, ...captchaData }),
body: JSON.stringify({ phone, code, password, ref, ...captchaData }),
})
.then(r => r.json().then(data => ({ ok: r.ok, data })))
.then(({ ok, data }) => {
if (ok && data.success) {
showToast(data.message || '注册成功', 'success')
if (data.notice) showToast(data.notice, 'warning')
setTimeout(() => {
window.location.href = data.data?.redirect || '/'
window.location.href = data.redirect || '/'
}, 500)
} else {
showToast(data.message || '注册失败', 'error')
+1 -1
View File
@@ -413,7 +413,7 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayIncome')) {
\AKSRT\Admin\Native\PayIncome::get_instance()->init();
}
// WordPress 原生用户菜单:消息/邀请码/认证/禁封(zibll users.php 子菜单对齐)
// WordPress 原生用户菜单:消息/推广码/认证/禁封(zibll users.php 子菜单对齐)
if (is_admin() && class_exists('\AKSRT\Admin\Native\UsersMenus')) {
\AKSRT\Admin\Native\UsersMenus::get_instance()->init();
}
+2 -2
View File
@@ -42,8 +42,8 @@ if (!function_exists('aksrt_credits_label')) {
'register' => __('注册赠送', 'aksrt'),
'signin' => __('每日签到', 'aksrt'),
'comment' => __('发表评论', 'aksrt'),
'invite' => __('邀请奖励', 'aksrt'),
'invitee' => __('被邀请奖励', 'aksrt'),
'referral' => __('推广注册奖励', 'aksrt'),
'referral_new_user' => __('推广注册新用户奖励', 'aksrt'),
'download' => __('下载消费', 'aksrt'),
'admin' => __('管理员调整', 'aksrt'),
'refund' => __('退款', 'aksrt'),
@@ -78,13 +78,13 @@ $day_max = \AKSRT\User\Level::day_max();
<?php
$methods = \AKSRT\User\Level::integral_defaults();
$methods['checkin'] = 0;
$methods['invitation'] = 0;
$methods['referral'] = 0;
$methods['shop_gift'] = 0;
$invite_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_invite_reward_settings() : [];
$methods['invitation'] = max(0, (int) ($invite_settings['invit_reward_level'] ?? 0));
$referral_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_referral_reward_settings() : [];
$methods['referral'] = max(0, (int) ($referral_settings['level_points'] ?? 0));
foreach ($methods as $key => $default_value) :
if ($key === 'day_max') continue;
$value = in_array($key, ['checkin', 'shop_gift'], true) ? 0 : ($key === 'invitation' ? (int) $default_value : \AKSRT\User\Level::task_points($key));
$value = in_array($key, ['checkin', 'shop_gift'], true) ? 0 : ($key === 'referral' ? (int) $default_value : \AKSRT\User\Level::task_points($key));
if ($value <= 0 && !in_array($key, ['checkin', 'shop_gift'], true)) continue;
?>
<div class="uc-level-methods__item">
@@ -17,10 +17,11 @@ if ($user_id <= 0) {
$affiliate = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_payment_affiliate_settings()
: [];
$affiliate_on = !empty($affiliate['enabled']);
$withdrawal = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_withdrawal_settings()
: [];
$withdraw_on = !empty($affiliate['enabled'])
$withdraw_on = $affiliate_on
&& class_exists('\AKSRT\Credits\Balance')
&& \AKSRT\Credits\Balance::enabled();
if ($withdraw_on && class_exists('\AKSRT\Settings\Capabilities') && !\AKSRT\Settings\Capabilities::user_can('balance_withdraw', $user_id)) {
@@ -46,6 +47,9 @@ $active_reward_tab = sanitize_key((string) ($revenue_request['reward_tab'] ?? ($
if (!in_array($active_reward_tab, ['detail', 'orders', 'users'], true)) {
$active_reward_tab = 'detail';
}
if (!$affiliate_on && $active_reward_tab === 'orders') {
$active_reward_tab = 'detail';
}
$base_url = function_exists('aksrt_get_user_center_tab_url')
? aksrt_get_user_center_tab_url('revenue')
: home_url('/user/revenue/');
@@ -91,6 +95,7 @@ $status_labels = [
];
?>
<div class="aksrt-revenue-page uc-revenue-panel" data-aksrt-revenue-page data-min-amount="<?php echo esc_attr((string) $withdraw_min); ?>">
<?php if ($affiliate_on) : ?>
<section class="uc-commission-summary" aria-label="<?php esc_attr_e('佣金概览', 'aksrt'); ?>">
<article class="uc-commission-card uc-commission-card--ratio">
<div class="uc-commission-card__body">
@@ -107,14 +112,15 @@ $status_labels = [
</div>
</article>
</section>
<?php endif; ?>
<section class="uc-reward-area" aria-labelledby="uc-reward-area-title">
<div class="uc-reward-area__heading">
<h3 id="uc-reward-area-title"><?php esc_html_e('推广收益', 'aksrt'); ?></h3>
<span><?php esc_html_e('推广链接、佣金订单与推广用户', 'aksrt'); ?></span>
<span><?php echo esc_html($affiliate_on ? __('推广链接、佣金订单与推广用户', 'aksrt') : __('推广链接与推广用户', 'aksrt')); ?></span>
</div>
<nav class="uc-reward-tabs" aria-label="<?php esc_attr_e('收益明细', 'aksrt'); ?>">
<a <?php echo $active_reward_tab === 'detail' ? 'aria-current="page"' : ''; ?> class="<?php echo $active_reward_tab === 'detail' ? 'is-active' : ''; ?>" href="<?php echo esc_url($reward_url('detail')); ?>"><?php esc_html_e('佣金详细', 'aksrt'); ?></a>
<a <?php echo $active_reward_tab === 'orders' ? 'aria-current="page"' : ''; ?> class="<?php echo $active_reward_tab === 'orders' ? 'is-active' : ''; ?>" href="<?php echo esc_url($reward_url('orders')); ?>"><?php esc_html_e('佣金明细', 'aksrt'); ?></a>
<a <?php echo $active_reward_tab === 'detail' ? 'aria-current="page"' : ''; ?> class="<?php echo $active_reward_tab === 'detail' ? 'is-active' : ''; ?>" href="<?php echo esc_url($reward_url('detail')); ?>"><?php esc_html_e('推广详情', 'aksrt'); ?></a>
<?php if ($affiliate_on) : ?><a <?php echo $active_reward_tab === 'orders' ? 'aria-current="page"' : ''; ?> class="<?php echo $active_reward_tab === 'orders' ? 'is-active' : ''; ?>" href="<?php echo esc_url($reward_url('orders')); ?>"><?php esc_html_e('佣金明细', 'aksrt'); ?></a><?php endif; ?>
<a <?php echo $active_reward_tab === 'users' ? 'aria-current="page"' : ''; ?> class="<?php echo $active_reward_tab === 'users' ? 'is-active' : ''; ?>" href="<?php echo esc_url($reward_url('users')); ?>"><?php esc_html_e('推广用户', 'aksrt'); ?></a>
</nav>
@@ -127,10 +133,12 @@ $status_labels = [
<button type="button" class="btn-primary btn-sm" data-affiliate-copy><?php esc_html_e('复制', 'aksrt'); ?></button>
<small data-affiliate-feedback role="status" aria-live="polite"></small>
</div>
<div class="uc-revenue-summary-row"><span><?php esc_html_e('佣金比例', 'aksrt'); ?></span><strong data-affiliate-ratio><?php echo esc_html((string) $commission_ratio); ?>%</strong></div>
<?php if ($affiliate_on) : ?><div class="uc-revenue-summary-row"><span><?php esc_html_e('佣金比例', 'aksrt'); ?></span><strong data-affiliate-ratio><?php echo esc_html((string) $commission_ratio); ?>%</strong></div><?php endif; ?>
<div class="uc-revenue-summary-row"><span><?php esc_html_e('推广人数', 'aksrt'); ?></span><div class="uc-revenue-count"><strong data-affiliate-promoters>—</strong><em><?php esc_html_e('人', 'aksrt'); ?></em></div></div>
<?php if ($affiliate_on) : ?>
<div class="uc-revenue-summary-row uc-revenue-summary-row--total"><span><?php esc_html_e('累计佣金', 'aksrt'); ?></span><div><strong>累计<?php echo esc_html($format_amount($commission_stats['total'] ?? 0)); ?>元</strong><strong>待结算<?php echo esc_html($format_amount($commission_stats['pending'] ?? 0)); ?>元</strong></div></div>
<div class="uc-revenue-summary-row uc-revenue-summary-row--explain"><span><?php esc_html_e('返佣详解', 'aksrt'); ?></span><div class="uc-reward-richtext"><?php echo wp_kses_post(wpautop($details_text)); ?></div></div>
<?php endif; ?>
</div>
</div>
<?php elseif ($active_reward_tab === 'orders') : ?>
-1
View File
@@ -352,7 +352,6 @@ class ImportExportController extends ControllerBase {
$expected_tables = [
$wpdb->prefix . 'aksrt_credit_log',
$wpdb->prefix . 'aksrt_signin_log',
$wpdb->prefix . 'aksrt_invite',
$wpdb->prefix . 'aksrt_dl_token',
$wpdb->prefix . 'aksrt_notification',
];
+12 -217
View File
@@ -11,6 +11,7 @@ class UsersMenus {
use UsersMenusVerify;
use UsersMenusVip;
use UsersMenusReferral;
public const CAPABILITY = 'manage_options';
@@ -37,12 +38,21 @@ class UsersMenus {
add_action('admin_post_aksrt_users_add_restriction', [$this, 'handle_add_restriction']);
add_action('admin_post_aksrt_users_remove_restriction', [$this, 'handle_remove_restriction']);
add_action('admin_post_aksrt_users_process_appeal', [$this, 'handle_process_appeal']);
add_action('admin_post_aksrt_users_batch_invite_codes', [$this, 'handle_batch_invite_codes']);
add_action('admin_post_aksrt_users_disable_invite_code', [$this, 'handle_disable_invite_code']);
add_action('admin_post_aksrt_users_mutate_vip', [$this, 'handle_mutate_vip']);
}
public function register_menus(): void {
if (current_user_can('manage_users')) {
add_submenu_page(
'users.php',
__('推广码运营', 'aksrt'),
__('推广码运营', 'aksrt'),
'manage_users',
'aksrt_referral_codes',
[$this, 'render_referral_codes_page']
);
}
if (!current_user_can(self::CAPABILITY)) {
return;
}
@@ -55,14 +65,6 @@ class UsersMenus {
'aksrt_messages',
[$this, 'render_messages_page']
);
add_submenu_page(
'users.php',
__('邀请码管理', 'aksrt'),
__('邀请码管理', 'aksrt'),
self::CAPABILITY,
'aksrt_invite_codes',
[$this, 'render_invite_codes_page']
);
add_submenu_page(
'users.php',
__('用户会员', 'aksrt'),
@@ -164,10 +166,6 @@ class UsersMenus {
'appeal_approved' => __('申诉已通过,限制已解除。', 'aksrt'),
'appeal_rejected' => __('申诉已驳回。', 'aksrt'),
'appeal_failed' => __('申诉处理失败。', 'aksrt'),
'invite_batch_ok' => __('邀请码批量生成完成。', 'aksrt'),
'invite_batch_failed' => __('邀请码批量生成失败。', 'aksrt'),
'invite_disabled' => __('邀请码已禁用。', 'aksrt'),
'invite_failed' => __('邀请码操作失败。', 'aksrt'),
'vip_updated' => __('会员状态已更新。', 'aksrt'),
'vip_partial' => __('部分用户的会员状态已更新。', 'aksrt'),
'vip_failed' => __('会员操作失败。', 'aksrt'),
@@ -281,209 +279,6 @@ class UsersMenus {
$this->redirect('aksrt_messages', $ok ? 'message_sent' : 'message_failed');
}
/* ------------------------------------------------------------------
* Invite codes
* ----------------------------------------------------------------*/
public function render_invite_codes_page(): void {
$this->ensure_cap();
echo '<div class="wrap"><h1>' . esc_html__('邀请码管理', 'aksrt') . '</h1>';
$this->flash();
echo '<h2>' . esc_html__('批量生成独立邀请码', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:12px 0 20px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
echo '<input type="hidden" name="action" value="aksrt_users_batch_invite_codes" />';
wp_nonce_field('aksrt_users_batch_invite_codes');
echo '<p><label>' . esc_html__('数量', 'aksrt') . '<br><input type="number" name="count" min="1" max="100" value="10" /></label></p>';
echo '<p><label>' . esc_html__('长度', 'aksrt') . '<br><input type="number" name="length" min="4" max="12" value="6" /></label></p>';
echo '<p><label>' . esc_html__('前缀', 'aksrt') . '<br><input type="text" name="prefix" value="" /></label></p>';
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" value="" class="regular-text" /></label></p>';
submit_button(__('生成', 'aksrt'), 'primary', '', false);
echo '</form>';
if (!empty($_GET['aksrt_generated'])) {
echo '<div class="notice notice-success"><p>' . esc_html(sprintf(
/* translators: %d count */
__('成功生成 %d 个邀请码。', 'aksrt'),
(int) $_GET['aksrt_generated']
)) . '</p></div>';
}
$standalone = get_option('aksrt_standalone_invite_codes', []);
$standalone = is_array($standalone) ? array_reverse(array_values($standalone)) : [];
echo '<h2>' . esc_html__('独立邀请码', 'aksrt') . '</h2>';
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>' . esc_html__('邀请码', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('批次', 'aksrt') . '</th><th>' . esc_html__('备注', 'aksrt') . '</th><th>' . esc_html__('创建时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$standalone) {
echo '<tr><td colspan="6">' . esc_html__('暂无独立邀请码', 'aksrt') . '</td></tr>';
}
$shown = 0;
foreach ($standalone as $entry) {
if ($shown >= 200) {
break;
}
$code = sanitize_key((string) ($entry['code'] ?? ''));
if ($code === '') {
continue;
}
$disabled = !empty($entry['disabled']);
$used = !empty($entry['used']) || !empty($entry['used_by']);
$status = $disabled ? __('已禁用', 'aksrt') : ($used ? __('已使用', 'aksrt') : __('可用', 'aksrt'));
echo '<tr>';
echo '<td><code>' . esc_html($code) . '</code></td>';
echo '<td>' . esc_html($status) . '</td>';
echo '<td>' . esc_html((string) ($entry['batch_id'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($entry['note'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($entry['created_at'] ?? '')) . '</td>';
echo '<td>';
if (!$disabled && !$used) {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_users_disable_invite_code" />';
echo '<input type="hidden" name="code" value="' . esc_attr($code) . '" />';
wp_nonce_field('aksrt_users_disable_invite_code_' . $code);
submit_button(__('禁用', 'aksrt'), 'small', '', false);
echo '</form>';
} else {
echo '—';
}
echo '</td></tr>';
$shown++;
}
echo '</tbody></table>';
global $wpdb;
$rows = $wpdb->get_results(
"SELECT user_id, meta_value AS invite_code FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' ORDER BY umeta_id DESC LIMIT 200",
ARRAY_A
);
echo '<h2>' . esc_html__('用户邀请码', 'aksrt') . '</h2>';
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('邀请码', 'aksrt') . '</th><th>' . esc_html__('邀请人数', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$rows) {
echo '<tr><td colspan="3">' . esc_html__('暂无用户邀请码', 'aksrt') . '</td></tr>';
}
$invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : '';
foreach ((array) $rows as $row) {
$user_id = (int) ($row['user_id'] ?? 0);
$user = $user_id > 0 ? get_userdata($user_id) : null;
$count = 0;
if ($invite_table) {
$count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$invite_table} WHERE inviter_id = %d",
$user_id
));
}
echo '<tr>';
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
echo '<td><code>' . esc_html((string) ($row['invite_code'] ?? '')) . '</code></td>';
echo '<td>' . esc_html((string) $count) . '</td>';
echo '</tr>';
}
echo '</tbody></table></div>';
}
public function handle_batch_invite_codes(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
check_admin_referer('aksrt_users_batch_invite_codes');
$count = isset($_POST['count']) ? max(1, min(100, (int) $_POST['count'])) : 10;
$length = isset($_POST['length']) ? max(4, min(12, (int) $_POST['length'])) : 6;
$prefix = isset($_POST['prefix']) ? sanitize_key((string) wp_unslash($_POST['prefix'])) : '';
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
$batch = 'INV-' . gmdate('YmdHis') . '-' . wp_generate_password(4, false, false);
$alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
$codes = get_option('aksrt_standalone_invite_codes', []);
$codes = is_array($codes) ? array_values($codes) : [];
$existing = [];
foreach ($codes as $entry) {
$c = sanitize_key((string) ($entry['code'] ?? ''));
if ($c !== '') {
$existing[$c] = true;
}
}
global $wpdb;
$generated = [];
for ($i = 0; $i < $count; $i++) {
$code = '';
for ($attempt = 0; $attempt < 20; $attempt++) {
$candidate = $prefix;
for ($j = 0; $j < $length; $j++) {
$candidate .= $alphabet[random_int(0, strlen($alphabet) - 1)];
}
$candidate = sanitize_key($candidate);
if ($candidate === '' || isset($existing[$candidate])) {
continue;
}
$user_hit = $wpdb->get_var($wpdb->prepare(
"SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' AND meta_value = %s LIMIT 1",
$candidate
));
if ($user_hit) {
continue;
}
$code = $candidate;
break;
}
if ($code === '') {
continue;
}
$existing[$code] = true;
$codes[] = [
'code' => $code,
'created_at' => current_time('mysql'),
'used' => false,
'used_by' => 0,
'used_at' => '',
'disabled' => false,
'note' => $note,
'batch_id' => $batch,
'created_by' => get_current_user_id(),
];
$generated[] = $code;
}
update_option('aksrt_standalone_invite_codes', array_values($codes), false);
$ok = !empty($generated);
$this->redirect('aksrt_invite_codes', $ok ? 'invite_batch_ok' : 'invite_batch_failed', [
'aksrt_generated' => count($generated),
]);
}
public function handle_disable_invite_code(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$code = isset($_POST['code']) ? sanitize_key((string) wp_unslash($_POST['code'])) : '';
check_admin_referer('aksrt_users_disable_invite_code_' . $code);
$ok = false;
if ($code !== '') {
$codes = get_option('aksrt_standalone_invite_codes', []);
$codes = is_array($codes) ? $codes : [];
foreach ($codes as $i => $entry) {
if (sanitize_key((string) ($entry['code'] ?? '')) !== $code) {
continue;
}
$entry['disabled'] = true;
$codes[$i] = $entry;
$ok = true;
break;
}
if ($ok) {
update_option('aksrt_standalone_invite_codes', array_values($codes), false);
}
}
$this->redirect('aksrt_invite_codes', $ok ? 'invite_disabled' : 'invite_failed');
}
/* ------------------------------------------------------------------
* Ban / reports
* ----------------------------------------------------------------*/
+144
View File
@@ -0,0 +1,144 @@
<?php
/**
* Native user referral-code operations.
*
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
trait UsersMenusReferral {
public function render_referral_codes_page(): void {
if (!current_user_can('manage_users')) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
}
$endpoint = rest_url('aksrt/v1/revenue/codes');
$nonce = wp_create_nonce('wp_rest');
?>
<div class="wrap" id="aksrt-referral-ops">
<h1><?php esc_html_e('推广码运营', 'aksrt'); ?></h1>
<p><?php esc_html_e('推广码与用户一一绑定。禁用或重新生成只影响后续访问和注册,不改变已有推广关系。', 'aksrt'); ?></p>
<div class="tablenav top">
<div class="alignleft actions">
<label class="screen-reader-text" for="aksrt-ref-status"><?php esc_html_e('筛选状态', 'aksrt'); ?></label>
<select id="aksrt-ref-status">
<option value="all"><?php esc_html_e('全部状态', 'aksrt'); ?></option>
<option value="active"><?php esc_html_e('已启用', 'aksrt'); ?></option>
<option value="disabled"><?php esc_html_e('已禁用', 'aksrt'); ?></option>
<option value="missing"><?php esc_html_e('未生成', 'aksrt'); ?></option>
</select>
<input type="search" id="aksrt-ref-search" placeholder="<?php esc_attr_e('搜索用户或 Ref 码', 'aksrt'); ?>">
<button type="button" class="button" id="aksrt-ref-filter"><?php esc_html_e('筛选', 'aksrt'); ?></button>
</div>
</div>
<div id="aksrt-ref-notice" role="status" aria-live="polite"></div>
<table class="wp-list-table widefat fixed striped">
<thead><tr>
<th><?php esc_html_e('用户', 'aksrt'); ?></th>
<th><?php esc_html_e('Ref 推广码', 'aksrt'); ?></th>
<th><?php esc_html_e('状态', 'aksrt'); ?></th>
<th><?php esc_html_e('绑定用户', 'aksrt'); ?></th>
<th><?php esc_html_e('有效付费用户', 'aksrt'); ?></th>
<th><?php esc_html_e('操作', 'aksrt'); ?></th>
</tr></thead>
<tbody id="aksrt-ref-rows"><tr><td colspan="6"><?php esc_html_e('正在加载…', 'aksrt'); ?></td></tr></tbody>
</table>
<div class="tablenav bottom"><div class="tablenav-pages" id="aksrt-ref-pages"></div></div>
</div>
<script>
(() => {
const endpoint = <?php echo wp_json_encode($endpoint); ?>;
const nonce = <?php echo wp_json_encode($nonce); ?>;
const rows = document.getElementById('aksrt-ref-rows');
const pages = document.getElementById('aksrt-ref-pages');
const notice = document.getElementById('aksrt-ref-notice');
const search = document.getElementById('aksrt-ref-search');
const status = document.getElementById('aksrt-ref-status');
let currentPage = 1;
const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#039;', '"': '&quot;'
})[char]);
const setNotice = (message, isError = false) => {
notice.className = message ? `notice notice-${isError ? 'error' : 'success'} inline` : '';
notice.innerHTML = message ? `<p>${escapeHtml(message)}</p>` : '';
};
const request = async (url, options = {}) => {
const response = await fetch(url, {
credentials: 'same-origin',
...options,
headers: { 'X-WP-Nonce': nonce, 'Content-Type': 'application/json', ...(options.headers || {}) },
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.message || '<?php echo esc_js(__('请求失败', 'aksrt')); ?>');
return payload;
};
const actionButton = (item) => {
const id = Number(item.user.id);
if (item.status === 'missing') {
return `<button class="button button-primary" data-ref-action="generate" data-user-id="${id}"><?php echo esc_js(__('生成', 'aksrt')); ?></button>`;
}
if (item.status === 'disabled') {
return `<button class="button" data-ref-action="regenerate" data-user-id="${id}"><?php echo esc_js(__('重新生成', 'aksrt')); ?></button>`;
}
return `<button class="button" data-ref-action="disable" data-user-id="${id}"><?php echo esc_js(__('禁用', 'aksrt')); ?></button> `
+ `<button class="button" data-ref-action="regenerate" data-user-id="${id}"><?php echo esc_js(__('重新生成', 'aksrt')); ?></button>`;
};
const load = async (page = 1) => {
currentPage = page;
setNotice('');
rows.innerHTML = '<tr><td colspan="6"><?php echo esc_js(__('正在加载…', 'aksrt')); ?></td></tr>';
const url = new URL(endpoint);
url.searchParams.set('page', String(page));
url.searchParams.set('per_page', '20');
url.searchParams.set('status', status.value);
if (search.value.trim()) url.searchParams.set('search', search.value.trim());
try {
const data = await request(url.toString());
rows.innerHTML = data.items.length ? data.items.map((item) => `
<tr>
<td><strong>${escapeHtml(item.user.name)}</strong><br><small>${escapeHtml(item.user.login)} · #${Number(item.user.id)}</small></td>
<td>${item.ref ? `<code>${escapeHtml(item.ref)}</code>` : '—'}</td>
<td>${escapeHtml({active:'<?php echo esc_js(__('已启用', 'aksrt')); ?>',disabled:'<?php echo esc_js(__('已禁用', 'aksrt')); ?>',missing:'<?php echo esc_js(__('未生成', 'aksrt')); ?>'}[item.status] || item.status)}</td>
<td>${Number(item.bound_count || 0)}</td>
<td>${Number(item.effective_payers || 0)}</td>
<td>${actionButton(item)}</td>
</tr>`).join('') : '<tr><td colspan="6"><?php echo esc_js(__('没有符合条件的用户', 'aksrt')); ?></td></tr>';
pages.innerHTML = Array.from({length: Number(data.max_pages || 1)}, (_, index) => {
const number = index + 1;
return `<button type="button" class="button ${number === Number(data.page) ? 'button-primary' : ''}" data-ref-page="${number}">${number}</button>`;
}).join(' ');
} catch (error) {
rows.innerHTML = '<tr><td colspan="6"><?php echo esc_js(__('推广码数据加载失败', 'aksrt')); ?></td></tr>';
setNotice(error.message, true);
}
};
document.getElementById('aksrt-ref-filter').addEventListener('click', () => load(1));
search.addEventListener('keydown', (event) => { if (event.key === 'Enter') load(1); });
document.getElementById('aksrt-ref-ops').addEventListener('click', async (event) => {
const pageButton = event.target.closest('[data-ref-page]');
if (pageButton) return load(Number(pageButton.dataset.refPage));
const button = event.target.closest('[data-ref-action]');
if (!button) return;
button.disabled = true;
try {
await request(`${endpoint}/${Number(button.dataset.userId)}/${button.dataset.refAction}`, { method: 'POST', body: '{}' });
setNotice('<?php echo esc_js(__('推广码已更新', 'aksrt')); ?>');
await load(currentPage);
} catch (error) {
setNotice(error.message, true);
button.disabled = false;
}
});
load();
})();
</script>
<?php
}
}
-2
View File
@@ -610,7 +610,6 @@ class PaymentController extends ControllerBase {
'types' => [
['label' => __('全部类型', 'aksrt'), 'value' => ''],
['label' => __('订单返佣', 'aksrt'), 'value' => 'order'],
['label' => __('邀请返佣', 'aksrt'), 'value' => 'invite'],
],
],
],
@@ -764,7 +763,6 @@ class PaymentController extends ControllerBase {
];
$typeMap = [
'order' => __('订单返佣', 'aksrt'),
'invite' => __('邀请返佣', 'aksrt'),
];
return [
-1
View File
@@ -147,7 +147,6 @@ class SchemaPayment {
['key' => 'affiliate_ratio_vip_1', 'label' => '一级会员佣金比例(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
['key' => 'affiliate_ratio_vip_2', 'label' => '二级会员佣金比例(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
['key' => 'affiliate_order_types', 'label' => '适用订单类型', 'type' => 'checkbox', 'options' => [['value' => 'membership', 'label' => '会员订单'], ['value' => 'content', 'label' => '付费内容'], ['value' => 'credits', 'label' => '购买积分']], 'default' => ['membership', 'content']],
['key' => 'affiliate_touch_window_days', 'label' => '首次触达窗口(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
['key' => 'affiliate_settlement_days', 'label' => '结算观察期(天)', 'type' => 'number', 'default' => 7, 'min' => 0, 'max' => 90],
]],
['key' => 'text', 'title' => '文案设置', 'fields' => [
+11 -42
View File
@@ -186,14 +186,18 @@ class SchemaUsers {
]],
['key' => 'email_set_captch', 'label' => '绑定邮箱需验证', 'type' => 'switch', 'default' => true, 'desc' => '绑定邮箱时需发送验证码确认'],
]],
['key' => 'invite_inline', 'title' => '邀请码注册', 'fields' => [
['key' => 'invit_code_s', 'label' => '启用邀请码', 'type' => 'switch', 'default' => false, 'desc' => '与「邀请码」子页联动,此处开关控制注册表单是否显示邀请码字段'],
['key' => 'invit_code_mode', 'label' => '邀请模式', 'type' => 'select', 'default' => 'open', 'options' => [
['label' => '开放注册(邀请码选填)', 'value' => 'open'],
['label' => '必须邀请码', 'value' => 'must'],
['key' => 'referral_inline', 'title' => '推广码注册', 'fields' => [
['key' => 'referral_registration_enabled', 'label' => '开放推广码注册', 'type' => 'switch', 'default' => true],
['key' => 'referral_register_field', 'label' => '注册页推广码字段', 'type' => 'select', 'default' => 'optional', 'options' => [
['label' => '隐藏', 'value' => 'hidden'],
['label' => '选填', 'value' => 'optional'],
['label' => '必填', 'value' => 'required'],
]],
['key' => 'invit_code_open_desc', 'label' => '开放注册提示', 'type' => 'input', 'default' => '邀请码为选填,没有邀请码也可以注册'],
['key' => 'invit_code_must_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入邀请码'],
['key' => 'referral_reject_invalid', 'label' => '拒绝无效推广码', 'type' => 'switch', 'default' => true],
['key' => 'referral_code_length', 'label' => 'Ref 码长度', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 12],
['key' => 'referral_window_days', 'label' => '首次触达窗口(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
['key' => 'referral_open_desc', 'label' => '选填提示', 'type' => 'input', 'default' => '推广码为选填,没有推广码也可以注册'],
['key' => 'referral_required_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入推广码'],
]],
['key' => 'sign_page_ui', 'title' => '页面 UI 配置', 'fields' => [
['key' => 'sign_page_background', 'label' => '页面背景图', 'type' => 'gallery', 'default' => [], 'desc' => '登录注册页背景图,多图随机'],
@@ -529,7 +533,6 @@ class SchemaUsers {
['key' => 'points_free_comment_new', 'label' => '发表评论', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
['key' => 'points_free_comment_like', 'label' => '评论被赞', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
['key' => 'points_free_followed', 'label' => '被关注', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
['key' => 'points_earn_invite', 'label' => '邀请注册奖励', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000, 'help' => '邀请奖励不走免费日上限'],
]],
['key' => 'spend', 'title' => '积分消费', 'fields' => [
['key' => 'points_spend_comment', 'label' => '评论消耗', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
@@ -558,40 +561,6 @@ class SchemaUsers {
]],
],
],
'invite' => [
'key' => 'invite',
'title' => '邀请码',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'basic', 'title' => '邀请设置', 'fields' => [
['key' => 'invit_code_s', 'label' => '启用邀请码', 'type' => 'switch', 'default' => false],
['key' => 'invit_code_open_desc', 'label' => '开放注册提示', 'type' => 'input', 'default' => '邀请码为选填,没有邀请码也可以注册'],
['key' => 'invit_code_must_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入邀请码'],
['key' => 'invit_code_mode', 'label' => '邀请模式', 'type' => 'select', 'default' => 'open', 'options' => [
['label' => '开放注册(邀请码选填)', 'value' => 'open'],
['label' => '必须邀请码', 'value' => 'must'],
]],
]],
['key' => 'format', 'title' => '码面设置', 'fields' => [
['key' => 'invit_code_length', 'label' => '邀请码长度', 'type' => 'number', 'default' => 8, 'min' => 4, 'max' => 32],
['key' => 'invit_code_prefix', 'label' => '邀请码前缀', 'type' => 'input', 'default' => ''],
['key' => 'invit_code_max_use', 'label' => '单码最大使用次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 9999],
['key' => 'invit_code_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效'],
['key' => 'invit_code_batch', 'label' => '批量生成数量', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 500],
]],
['key' => 'reward', 'title' => '邀请奖励', 'fields' => [
['key' => 'invit_reward_points', 'label' => '邀请奖励积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 1000],
['key' => 'invit_reward_level', 'label' => '邀请奖励等级', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 10],
['key' => 'invit_reward_vip_days', 'label' => '奖励VIP天数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 3650],
['key' => 'invit_invitee_reward', 'label' => '被邀请人奖励', 'type' => 'switch', 'default' => false],
]],
['key' => 'display', 'title' => '展示设置', 'fields' => [
['key' => 'invit_code_show_count', 'label' => '显示已使用次数', 'type' => 'switch', 'default' => true],
['key' => 'invit_code_show_remain', 'label' => '显示剩余次数', 'type' => 'switch', 'default' => true],
]],
],
],
'features' => [
'key' => 'features',
'title' => '用户功能',
-2
View File
@@ -93,7 +93,6 @@ class ToolsController extends ControllerBase {
$expected_tables = [
$wpdb->prefix . 'aksrt_credit_log',
$wpdb->prefix . 'aksrt_signin_log',
$wpdb->prefix . 'aksrt_invite',
$wpdb->prefix . 'aksrt_dl_token',
$wpdb->prefix . 'aksrt_notification',
];
@@ -142,7 +141,6 @@ class ToolsController extends ControllerBase {
$tables = [
$wpdb->prefix . 'aksrt_credit_log',
$wpdb->prefix . 'aksrt_signin_log',
$wpdb->prefix . 'aksrt_invite',
$wpdb->prefix . 'aksrt_dl_token',
$wpdb->prefix . 'aksrt_notification',
];
+22 -48
View File
@@ -9,7 +9,6 @@ defined('ABSPATH') || exit;
class UserController extends ControllerBase {
use UserControllerReportOperations;
use UserControllerInviteOperations;
use UserControllerVerifyIntegrations;
use UserControllerIdentitySettings;
use UserControllerAuthPhoneOperations;
@@ -208,36 +207,6 @@ class UserController extends ControllerBase {
'permission_callback' => '__return_true',
]);
// Invite codes management (admin)
register_rest_route(self::API_NAMESPACE, '/invite/codes', [
'methods' => 'GET',
'callback' => [$this, 'get_invite_codes'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/invite/codes', [
'methods' => 'POST',
'callback' => [$this, 'generate_invite_codes'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/invite/codes/(?P<id>\d+)/disable', [
'methods' => 'POST',
'callback' => [$this, 'disable_invite_code'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/invite/codes/bulk', [
'methods' => 'POST',
'callback' => [$this, 'bulk_update_invite_codes'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/invite/stats', [
'methods' => 'GET',
'callback' => [$this, 'get_invite_stats'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── Modal Auth (弹窗登录/注册) ──
// Modal login
@@ -432,17 +401,21 @@ class UserController extends ControllerBase {
if ($request->get_method() === 'GET') {
$data = \AKSRT\Settings\Settings::get_all();
$invite = class_exists('\AKSRT\Settings\CreditsRuntime')
? \AKSRT\Settings\CreditsRuntime::get_invite_registration_config()
$referral = class_exists('\AKSRT\Settings\CreditsRuntime')
? \AKSRT\Settings\CreditsRuntime::get_referral_registration_settings()
: [];
$shared = class_exists('\AKSRT\Settings\CreditsRuntime')
? \AKSRT\Settings\CreditsRuntime::get_referral_settings()
: [];
return $this->success(['data' => [
'registration_email_verification_enabled' => \AKSRT\Settings\Settings::registration_requires_email_verification(),
'registration_email_verification_method' => \AKSRT\Settings\Settings::registration_verification_method(),
'invite_registration_enabled' => $invite['enabled'] ?? true,
'invite_code_length' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::invite_code_length() : 6,
'invite_register_field' => $invite['register_field'] ?? 'optional',
'invite_reject_invalid' => $invite['reject_invalid'] ?? false,
'referral_registration_enabled' => $referral['enabled'] ?? true,
'referral_code_length' => $shared['code_length'] ?? 12,
'referral_window_days' => $shared['window_days'] ?? 30,
'referral_register_field' => $referral['register_field'] ?? 'optional',
'referral_reject_invalid' => $referral['reject_invalid'] ?? true,
] + array_intersect_key($data, $allowed)]);
}
@@ -451,20 +424,21 @@ class UserController extends ControllerBase {
$sanitized = $this->sanitize_fields($params, $allowed);
$settings = array_merge($settings, $sanitized);
if (array_key_exists('invite_registration_enabled', $params)) {
$settings['invite_registration_enabled'] = !empty($params['invite_registration_enabled']);
if (array_key_exists('referral_registration_enabled', $params)) {
$settings['referral_registration_enabled'] = !empty($params['referral_registration_enabled']);
}
if (array_key_exists('invite_code_length', $params)) {
$settings['invite_code_length'] = max(4, min(12, (int) $params['invite_code_length']));
if (array_key_exists('referral_code_length', $params)) {
$settings['referral_code_length'] = max(4, min(12, (int) $params['referral_code_length']));
}
if (array_key_exists('invite_register_field', $params)) {
$field = (string) $params['invite_register_field'];
$settings['invit_code_mode'] = $field === 'required' ? 'must' : ($field === 'hidden' ? 'close' : 'open');
if (array_key_exists('referral_window_days', $params)) {
$settings['referral_window_days'] = max(1, min(365, (int) $params['referral_window_days']));
}
if (array_key_exists('invite_reject_invalid', $params)) {
$reward = is_array($settings['invite_reward'] ?? null) ? $settings['invite_reward'] : [];
$reward['reject_invalid'] = !empty($params['invite_reject_invalid']);
$settings['invite_reward'] = $reward;
if (array_key_exists('referral_register_field', $params)) {
$field = sanitize_key((string) $params['referral_register_field']);
$settings['referral_register_field'] = in_array($field, ['hidden', 'optional', 'required'], true) ? $field : 'optional';
}
if (array_key_exists('referral_reject_invalid', $params)) {
$settings['referral_reject_invalid'] = !empty($params['referral_reject_invalid']);
}
if (array_key_exists('registration_email_verification_enabled', $params)) {
$settings['registration_email_verification_enabled'] = !empty($params['registration_email_verification_enabled']);
@@ -67,7 +67,7 @@ trait UserControllerAuthPhoneOperations {
$email = sanitize_email((string) ($payload['user_email'] ?? $payload['email'] ?? ''));
$password = (string) ($payload['user_pass'] ?? $payload['password'] ?? '');
$confirm = (string) ($payload['user_pass_confirm'] ?? $payload['password_confirm'] ?? $payload['confirm_password'] ?? $password);
$invite_code = sanitize_text_field((string) ($payload['invite_code'] ?? ''));
$ref = sanitize_key((string) ($payload['ref'] ?? ''));
$register_nonce = sanitize_text_field((string) ($payload['_register_nonce'] ?? ''));
$agree_terms = !empty($payload['agree_terms']) ? '1' : '';
@@ -91,7 +91,7 @@ trait UserControllerAuthPhoneOperations {
$payload['user_email'] = $email;
$payload['user_pass'] = $password;
$payload['user_pass_confirm'] = $confirm;
$payload['invite_code'] = $invite_code;
$payload['ref'] = $ref;
$payload['_register_nonce'] = $register_nonce;
$payload['agree_terms'] = $agree_terms;
@@ -125,6 +125,7 @@ trait UserControllerAuthPhoneOperations {
'login_url' => $login_url,
'user_login' => $result['user_login'] ?? $username,
'user_email' => $email,
'notice' => (string) ($result['notice'] ?? ''),
'user' => $registered_user ? [
'id' => $registered_user->ID,
'display_name' => $registered_user->display_name,
@@ -232,6 +233,7 @@ trait UserControllerAuthPhoneOperations {
'message' => $result['message'] ?? '注册成功',
'redirect' => $result['redirect'] ?? home_url('/'),
'user_id' => $result['user_id'] ?? 0,
'notice' => (string) ($result['notice'] ?? ''),
]);
}
@@ -1,450 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait UserControllerInviteOperations {
/**
* GET /invite/codes — 邀请码列表(管理后台)
*/
public function get_invite_codes(\WP_REST_Request $request): \WP_REST_Response {
global $wpdb;
$page = max(1, (int) ($request->get_param('page') ?: 1));
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
$status = sanitize_key((string) $request->get_param('status'));
$source = sanitize_key((string) $request->get_param('source'));
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
$usermeta_table = $wpdb->usermeta;
$invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : '';
$invite_rows = [];
if ($invite_table) {
$records = $wpdb->get_results("SELECT * FROM {$invite_table}", ARRAY_A);
foreach ((array) $records as $record) {
$invite_rows[(string) $record['invite_code']] = $record;
}
}
$items = [];
$user_rows = $wpdb->get_results(
"SELECT user_id, meta_value AS invite_code FROM {$usermeta_table} WHERE meta_key = 'aksrt_invite_code' ORDER BY umeta_id DESC",
ARRAY_A
);
foreach ((array) $user_rows as $row) {
$user_id = (int) $row['user_id'];
$user = get_userdata($user_id);
$code = sanitize_key((string) $row['invite_code']);
$record = $invite_rows[$code] ?? null;
$item = [
'row_key' => 'user:' . $user_id,
'source_type' => 'user',
'source_label' => __('用户邀请码', 'aksrt'),
'user' => $this->build_admin_user_summary($user_id, $user),
'invite_code' => $code,
'status' => $record ? 'used' : 'active',
'status_label' => $record ? __('已使用', 'aksrt') : __('可用', 'aksrt'),
'created_at' => $user ? (string) $user->user_registered : '',
'used_at' => $record['created_at'] ?? '',
'note' => '',
'batch_id' => '',
'created_by' => null,
'registration_status' => $record['status'] ?? '',
'invitee' => $this->build_admin_user_summary((int) ($record['invitee_id'] ?? 0)),
'can_bulk' => false,
'actions' => ['disable'],
];
if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) {
$items[] = $item;
}
}
foreach ($this->load_standalone_invite_codes() as $entry) {
$code = sanitize_key((string) ($entry['code'] ?? ''));
$record = $invite_rows[$code] ?? null;
$used_by = (int) ($entry['used_by'] ?? 0);
$disabled = !empty($entry['disabled']);
$used = !empty($entry['used']) || $used_by > 0 || !empty($record['invitee_id']);
$status_key = $disabled ? 'disabled' : ($used ? 'used' : 'active');
$status_map = [
'active' => __('可用', 'aksrt'),
'used' => __('已使用', 'aksrt'),
'disabled' => __('已禁用', 'aksrt'),
];
$invitee_id = $used_by > 0 ? $used_by : (int) ($record['invitee_id'] ?? 0);
$item = [
'row_key' => 'standalone:' . $code,
'source_type' => 'standalone',
'source_label' => __('独立邀请码', 'aksrt'),
'user' => null,
'invite_code' => $code,
'status' => $status_key,
'status_label' => $status_map[$status_key],
'created_at' => (string) ($entry['created_at'] ?? ''),
'used_at' => (string) ($entry['used_at'] ?? ($record['created_at'] ?? '')),
'note' => (string) ($entry['note'] ?? ''),
'batch_id' => (string) ($entry['batch_id'] ?? ''),
'created_by' => $this->build_admin_user_summary((int) ($entry['created_by'] ?? 0)),
'registration_status' => $record['status'] ?? '',
'invitee' => $this->build_admin_user_summary($invitee_id),
'can_bulk' => true,
'actions' => ['disable', 'delete'],
];
if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) {
$items[] = $item;
}
}
usort($items, static function ($left, $right) {
return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? ''));
});
$total = count($items);
$offset = ($page - 1) * $per_page;
$paged = array_slice($items, $offset, $per_page);
$stats = $this->build_invite_operation_stats($items, $invite_rows);
return $this->success([
'data' => [
'items' => array_values($paged),
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
'stats' => $stats,
'options' => [
'statuses' => [
['label' => __('全部状态', 'aksrt'), 'value' => ''],
['label' => __('可用', 'aksrt'), 'value' => 'active'],
['label' => __('已使用', 'aksrt'), 'value' => 'used'],
['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'],
],
'sources' => [
['label' => __('全部来源', 'aksrt'), 'value' => ''],
['label' => __('用户邀请码', 'aksrt'), 'value' => 'user'],
['label' => __('独立邀请码', 'aksrt'), 'value' => 'standalone'],
],
],
],
]);
}
/**
* POST /invite/codes — 批量生成邀请码(管理后台)
* Body: { count: int, length: int, prefix: string }
* 为指定用户或批量创建邀请码。
*/
public function generate_invite_codes(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Credits\Invite')) {
return $this->error(__('邀请系统未启用', 'aksrt'), 500);
}
$params = $this->get_json_params($request);
$count = max(1, min(100, (int) ($params['count'] ?? 1)));
$length = max(4, min(12, (int) ($params['length'] ?? 6)));
$prefix = sanitize_key((string) ($params['prefix'] ?? ''));
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
$batch_id = sanitize_text_field((string) ($params['batch_id'] ?? ''));
if ($batch_id === '') {
$batch_id = 'INV-' . gmdate('YmdHis') . '-' . wp_generate_password(4, false, false);
}
$generated = [];
$codes = $this->load_standalone_invite_codes();
$alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
for ($i = 0; $i < $count; $i++) {
$code = '';
for ($attempt = 0; $attempt < 12; $attempt++) {
$candidate = $prefix;
for ($j = 0; $j < $length; $j++) {
$candidate .= $alphabet[random_int(0, strlen($alphabet) - 1)];
}
$candidate = sanitize_key($candidate);
if (!$this->invite_code_exists_globally($candidate, $codes)) {
$code = $candidate;
break;
}
}
if ($code === '') {
continue;
}
$codes[] = [
'code' => $code,
'created_at' => current_time('mysql'),
'used' => false,
'used_by' => 0,
'used_at' => '',
'disabled' => false,
'note' => $note,
'batch_id' => $batch_id,
'created_by' => get_current_user_id(),
];
$generated[] = $code;
}
$this->save_standalone_invite_codes($codes);
return $this->success([
'message' => sprintf(__('已生成 %d 个邀请码', 'aksrt'), count($generated)),
'data' => [
'codes' => $generated,
'batch_id' => $batch_id,
],
]);
}
/**
* POST /invite/codes/{id}/disable — 禁用邀请码
*/
public function disable_invite_code(\WP_REST_Request $request): \WP_REST_Response {
$user_id = (int) $request->get_param('id');
if ($user_id <= 0) {
return $this->error(__('参数无效', 'aksrt'));
}
$removed = $this->remove_user_invite_code($user_id);
if (!$removed) {
return $this->error(__('邀请码不存在或已禁用', 'aksrt'));
}
return $this->success(['message' => __('邀请码已禁用', 'aksrt')]);
}
public function bulk_update_invite_codes(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$action = sanitize_key((string) ($params['action'] ?? ''));
$targets = isset($params['targets']) && is_array($params['targets']) ? $params['targets'] : [];
if (!in_array($action, ['disable', 'delete'], true) || empty($targets)) {
return $this->error(__('批量操作参数无效', 'aksrt'));
}
$codes = $this->load_standalone_invite_codes();
$processed = 0;
foreach ($targets as $target) {
$source_type = sanitize_key((string) ($target['source_type'] ?? ''));
if ($source_type === 'user') {
$user_id = (int) ($target['user_id'] ?? 0);
if ($action === 'disable' && $this->remove_user_invite_code($user_id)) {
$processed++;
}
continue;
}
if ($source_type !== 'standalone') {
continue;
}
$code = sanitize_key((string) ($target['code'] ?? $target['invite_code'] ?? ''));
foreach ($codes as $index => $entry) {
$entry_code = sanitize_key((string) ($entry['code'] ?? ''));
if ($entry_code !== $code) {
continue;
}
if ($action === 'delete' && empty($entry['used']) && empty($entry['used_by'])) {
unset($codes[$index]);
} else {
$entry['disabled'] = true;
$codes[$index] = $entry;
}
$processed++;
break;
}
}
$this->save_standalone_invite_codes($codes);
return $this->success([
'message' => sprintf(__('已处理 %d 条邀请码记录', 'aksrt'), $processed),
]);
}
/**
* GET /invite/stats — 邀请系统统计
*/
public function get_invite_stats(): \WP_REST_Response {
global $wpdb;
$items = [];
$user_records = $wpdb->get_results(
"SELECT user_id, meta_value AS invite_code FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code'",
ARRAY_A
);
foreach ((array) $user_records as $row) {
$items[] = [
'source_type' => 'user',
'status' => 'active',
];
}
foreach ($this->load_standalone_invite_codes() as $entry) {
$used = !empty($entry['used']) || !empty($entry['used_by']);
$disabled = !empty($entry['disabled']);
$items[] = [
'source_type' => 'standalone',
'status' => $disabled ? 'disabled' : ($used ? 'used' : 'active'),
];
}
$stats = $this->build_invite_operation_stats($items);
return $this->success(['data' => $stats]);
}
private function load_standalone_invite_codes(): array {
$codes = get_option('aksrt_standalone_invite_codes', []);
return is_array($codes) ? array_values($codes) : [];
}
private function save_standalone_invite_codes(array $codes): void {
update_option('aksrt_standalone_invite_codes', array_values($codes), false);
}
private function invite_code_exists_globally(string $candidate, array $standalone_codes = []): bool {
if ($candidate === '') {
return true;
}
global $wpdb;
$existing_user_code = $wpdb->get_var($wpdb->prepare(
"SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' AND meta_value = %s LIMIT 1",
$candidate
));
if ($existing_user_code) {
return true;
}
foreach ($standalone_codes as $entry) {
if (sanitize_key((string) ($entry['code'] ?? '')) === $candidate) {
return true;
}
}
$invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : '';
if ($invite_table) {
$existing_invite = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$invite_table} WHERE invite_code = %s LIMIT 1",
$candidate
));
if ($existing_invite) {
return true;
}
}
return false;
}
private function invite_item_matches_filters(array $item, string $keyword, string $status, string $source, string $date_from, string $date_to): bool {
if ($status !== '' && ($item['status'] ?? '') !== $status) {
return false;
}
if ($source !== '' && ($item['source_type'] ?? '') !== $source) {
return false;
}
$created_at = (string) ($item['created_at'] ?? '');
if ($date_from !== '' && $created_at !== '' && strcmp($created_at, $date_from . ' 00:00:00') < 0) {
return false;
}
if ($date_to !== '' && $created_at !== '' && strcmp($created_at, $date_to . ' 23:59:59') > 0) {
return false;
}
if ($keyword === '') {
return true;
}
$haystack = implode(' ', array_filter([
(string) ($item['invite_code'] ?? ''),
(string) ($item['status_label'] ?? ''),
(string) ($item['note'] ?? ''),
(string) ($item['batch_id'] ?? ''),
(string) ($item['user']['display_name'] ?? ''),
(string) ($item['invitee']['display_name'] ?? ''),
(string) ($item['invitee']['email'] ?? ''),
]));
return mb_stripos($haystack, $keyword) !== false;
}
private function build_invite_operation_stats(array $items, array $invite_rows = []): array {
$stats = [
'total_codes' => count($items),
'active_codes' => 0,
'used_codes' => 0,
'disabled_codes' => 0,
'user_codes' => 0,
'standalone_codes' => 0,
'total_invites' => count($invite_rows),
'total_rewarded' => 0,
];
foreach ($items as $item) {
$status = $item['status'] ?? '';
if ($status === 'active') {
$stats['active_codes']++;
} elseif ($status === 'used') {
$stats['used_codes']++;
} elseif ($status === 'disabled') {
$stats['disabled_codes']++;
}
if (($item['source_type'] ?? '') === 'user') {
$stats['user_codes']++;
} elseif (($item['source_type'] ?? '') === 'standalone') {
$stats['standalone_codes']++;
}
}
foreach ($invite_rows as $row) {
if (($row['status'] ?? '') === 'rewarded') {
$stats['total_rewarded']++;
}
}
return $stats;
}
private function remove_user_invite_code(int $user_id): bool {
$user_id = (int) $user_id;
if ($user_id <= 0) {
return false;
}
$code = get_user_meta($user_id, 'aksrt_invite_code', true);
if (!$code) {
return false;
}
delete_user_meta($user_id, 'aksrt_invite_code');
$map = get_option('aksrt_invite_code_map', []);
if (is_array($map) && isset($map[$code])) {
unset($map[$code]);
update_option('aksrt_invite_code_map', $map, false);
}
return true;
}
}
+2 -2
View File
@@ -30,7 +30,7 @@ class EmailVerification {
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1);
self::clear_email_verification($user_id);
// 仅在状态由"未验证 → 已验证"跳变时触发,避免历史 backfill / 重复调用造成奖励重发。
// 监听方负责自己幂等(aksrt_invite 表的 status 检查已经做到这一点)。
// 监听方负责自己幂等(Ref 关系使用原子奖励状态处理重复事件)。
if (!$was_verified) {
do_action('aksrt_email_verified', $user_id);
}
@@ -447,7 +447,7 @@ class EmailVerification {
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1);
self::clear_email_verification($user_id);
Service::log_event('verify.success', ['uid' => $user_id]);
// 状态跳变时触发 hook,邀请系统等模块据此发奖
// 状态跳变时触发 hook,Ref 推广奖励等模块据此发奖
if (!$was_verified) {
do_action('aksrt_email_verified', $user_id);
}
-29
View File
@@ -41,35 +41,6 @@ final class Service {
private const RESEND_IP_MAX = 6;
private const RESEND_IP_WINDOW = 600;
/**
* 注册流程内的"待消费"邀请码上下文。
*
* 由 register_user() 在 wp_create_user 之前写入,由 \AKSRT\Credits\Invite::on_user_register
* 通过 take_pending_invite_context() 一次性消费。
*
* 设计为请求级静态:避免污染 transient 表,也避免依赖 cookie 在跨标签场景下的不确定性。
*
* @var int
*/
private static $pending_invite_inviter_id = 0;
/** @var string */
private static $pending_invite_code = '';
/**
* 取出并清空当前注册请求的"表单 invite_code 解析结果"。
*
* @return array{inviter_id:int, code:string}
*/
public static function take_pending_invite_context(): array {
$ctx = [
'inviter_id' => (int) self::$pending_invite_inviter_id,
'code' => (string) self::$pending_invite_code,
];
self::$pending_invite_inviter_id = 0;
self::$pending_invite_code = '';
return $ctx;
}
/**
* 启动钩子:注册全局 authenticate 拦截器(阶段 4)
* 调用方:bootstrap.php 在所有类加载完成后调用一次。
+33 -41
View File
@@ -11,10 +11,6 @@ trait ServicePart1a {
public static function register_user(array $post, array $context = []): array {
// 先 reset pending invite context,避免同一进程内残留污染
self::$pending_invite_inviter_id = 0;
self::$pending_invite_code = '';
$user_login = sanitize_user(wp_unslash($post['user_login'] ?? ''), true);
$user_email = sanitize_email(wp_unslash($post['user_email'] ?? ''));
$password = (string) ($post['user_pass'] ?? '');
@@ -60,53 +56,47 @@ trait ServicePart1a {
}
}
// ====== 解析邀请码 ======
// 优先级:表单 invite_code(用户显式填写) > cookie aksrt_invite
$invite_registration_enabled = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::invite_registration_enabled();
$invite_mode = $invite_registration_enabled && class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::invite_register_field_mode()
$ref_service = class_exists('\AKSRT\Payment\ReferralAttribution')
? \AKSRT\Payment\ReferralAttribution::get_instance()
: null;
if ($ref_service) {
$ref_service->clear_registration_context();
}
$registration = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_referral_registration_settings()
: ['enabled' => false, 'register_field' => 'hidden', 'reject_invalid' => true];
$submitted_ref = isset($post['ref']) ? sanitize_key((string) wp_unslash($post['ref'])) : '';
$field_mode = !empty($registration['enabled'])
? (string) ($registration['register_field'] ?? 'hidden')
: 'hidden';
$invite_code = isset($post['invite_code']) ? sanitize_key((string) wp_unslash($post['invite_code'])) : '';
$resolved_inviter_id = 0;
$invite_is_valid = false;
$accept_submitted = !empty($registration['enabled']) && $field_mode !== 'hidden';
$ref_context = $ref_service
? $ref_service->prepare_registration($accept_submitted ? $submitted_ref : '')
: ['ref' => '', 'valid' => false, 'submitted_valid' => false, 'conflict' => false, 'source' => 'none'];
if (!$invite_registration_enabled) {
// 邀请功能关闭,忽略所有邀请码
$invite_code = '';
} elseif ($invite_code !== '') {
if (class_exists('\AKSRT\Credits\Invite')) {
$invite_service = \AKSRT\Credits\Invite::get_instance();
$resolved_inviter_id = $invite_service->resolve_code($invite_code);
$invite_is_valid = $invite_service->validate_code($invite_code);
if ($field_mode === 'required' && empty($ref_context['valid'])) {
if ($submitted_ref === '') {
if ($ref_service) {
$ref_service->clear_registration_context();
}
// 必填模式下,邀请码无效时根据后台开关决定是否拒绝
$reject_invalid = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::invite_reject_invalid();
if ($invite_mode === 'required' && $reject_invalid && !$invite_is_valid) {
return ['success' => false, 'error' => __('邀请码无效,请检查后重试', 'aksrt')];
return ['success' => false, 'error' => __('请填写推广码', 'aksrt')];
}
if (!empty($registration['reject_invalid'])) {
if ($ref_service) {
$ref_service->clear_registration_context();
}
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
}
} elseif ($invite_mode === 'required') {
return ['success' => false, 'error' => __('请填写邀请码', 'aksrt')];
}
// 把解析到的 inviter_id 暂存到静态字段,user_register hook 内部读取(优先于 cookie)
self::$pending_invite_inviter_id = $resolved_inviter_id;
self::$pending_invite_code = $invite_code;
$user_id = wp_create_user($user_login, $password, $user_email);
if (is_wp_error($user_id)) {
self::$pending_invite_inviter_id = 0;
self::$pending_invite_code = '';
if ($ref_service) {
$ref_service->clear_registration_context();
}
return ['success' => false, 'error' => $user_id->get_error_message()];
}
if ($invite_code !== '' && $resolved_inviter_id <= 0 && class_exists('\AKSRT\Credits\Invite')) {
\AKSRT\Credits\Invite::get_instance()->handle_registration_invite((int) $user_id, $invite_code);
}
// wp_create_user 内部已触发 user_register hook,\AKSRT\Credits\Invite::on_user_register 已读取 pending 字段。
// 这里清空,避免污染后续请求。
self::$pending_invite_inviter_id = 0;
self::$pending_invite_code = '';
$ref_notice = $ref_service ? $ref_service->take_registration_notice() : '';
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
@@ -124,6 +114,7 @@ trait ServicePart1a {
'user_id' => (int) $user_id,
'user' => $user,
'user_login' => $user_login,
'notice' => $ref_notice,
];
}
@@ -150,6 +141,7 @@ trait ServicePart1a {
'verification_method' => $method,
'verify_url' => self::get_email_verification_url((int) $user_id, ['mode' => $method]),
'message' => $begin['message'] ?? '',
'notice' => $ref_notice,
];
}
+39 -5
View File
@@ -233,7 +233,7 @@ trait ServicePart1b {
/**
* 手机号注册
*
* @param array $post POST 数据 (phone, code, password, invite_code)
* @param array $post POST 数据 (phone, code, password, ref)
* @return array{success: bool, user_id?: int, error?: string, redirect?: string}
*/
public static function register_by_phone(array $post): array {
@@ -283,9 +283,44 @@ trait ServicePart1b {
$suffix++;
}
$ref_service = class_exists('\AKSRT\Payment\ReferralAttribution')
? \AKSRT\Payment\ReferralAttribution::get_instance()
: null;
if ($ref_service) {
$ref_service->clear_registration_context();
}
$registration = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_referral_registration_settings()
: ['enabled' => false, 'register_field' => 'hidden', 'reject_invalid' => true];
$submitted_ref = sanitize_key((string) ($post['ref'] ?? ''));
$field_mode = !empty($registration['enabled'])
? (string) ($registration['register_field'] ?? 'hidden')
: 'hidden';
$accept_submitted = !empty($registration['enabled']) && $field_mode !== 'hidden';
$ref_context = $ref_service
? $ref_service->prepare_registration($accept_submitted ? $submitted_ref : '')
: ['valid' => false, 'submitted_valid' => false, 'conflict' => false, 'source' => 'none'];
if ($field_mode === 'required' && empty($ref_context['valid'])) {
if ($submitted_ref === '') {
if ($ref_service) {
$ref_service->clear_registration_context();
}
return ['success' => false, 'error' => __('请填写推广码', 'aksrt')];
}
if (!empty($registration['reject_invalid'])) {
if ($ref_service) {
$ref_service->clear_registration_context();
}
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
}
}
// 创建用户
$user_id = wp_create_user($username, $pass, $phone . '@sms.aksrt.local');
if (is_wp_error($user_id)) {
if ($ref_service) {
$ref_service->clear_registration_context();
}
return ['success' => false, 'error' => $user_id->get_error_message()];
}
@@ -296,10 +331,8 @@ trait ServicePart1b {
// 邮箱标记为已验证(手机注册等同于已验证)
update_user_meta($user_id, 'aksrt_email_verified', '1');
// 处理邀请码
$invite_code = sanitize_key((string) ($post['invite_code'] ?? ''));
if ($invite_code !== '' && class_exists('\AKSRT\Credits\Invite')) {
\AKSRT\Credits\Invite::get_instance()->handle_registration_invite($user_id, $invite_code);
if ($ref_service) {
$ref_service->reward_registered_user((int) $user_id);
}
// 自动登录
@@ -315,6 +348,7 @@ trait ServicePart1b {
'user_id' => $user_id,
'redirect' => $redirect,
'message' => __('注册成功', 'aksrt'),
'notice' => $ref_service ? $ref_service->take_registration_notice() : '',
];
}
+1 -1
View File
@@ -14,7 +14,7 @@ class DBMigrator {
use DBMigratorVip;
/** Current schema version; changing it re-runs dbDelta. */
const SCHEMA_VERSION = '1.23.0';
const SCHEMA_VERSION = '1.24.0';
const OPT_KEY = 'aksrt_db_schema_version';
+8 -20
View File
@@ -71,24 +71,7 @@ trait DBMigratorPart2 {
) {$charset_collate};";
dbDelta($sql_signin);
// ---------- 3. 邀请 ----------
$sql_invite = "CREATE TABLE {$prefix}aksrt_invite (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
inviter_id BIGINT UNSIGNED NOT NULL,
invitee_id BIGINT UNSIGNED DEFAULT NULL,
invite_code VARCHAR(32) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL,
rewarded_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_code (invite_code),
KEY idx_inviter (inviter_id),
KEY idx_invitee (invitee_id),
KEY idx_status (status)
) {$charset_collate};";
dbDelta($sql_invite);
// ---------- 4. 下载 token ----------
// ---------- 3. 下载 token ----------
$sql_dl_token = "CREATE TABLE {$prefix}aksrt_dl_token (
token CHAR(48) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
@@ -377,7 +360,7 @@ trait DBMigratorPart2 {
) {$charset_collate};";
dbDelta($sql_rebate_log);
// ── affiliate attribution (new contract; no legacy invite reads) ──
// ── canonical Ref attribution and permanent user binding ──
$sql_affiliate_relation = "CREATE TABLE {$prefix}aksrt_affiliate_relation (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
referral_code VARCHAR(32) NOT NULL,
@@ -386,12 +369,17 @@ trait DBMigratorPart2 {
first_touch_at DATETIME NOT NULL,
bound_at DATETIME DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'captured',
reward_status VARCHAR(20) NOT NULL DEFAULT 'skipped',
rewarded_at DATETIME DEFAULT NULL,
registration_ip VARCHAR(64) DEFAULT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_referral_code (referral_code),
UNIQUE KEY uniq_invitee (invitee_id),
KEY idx_promoter (promoter_id),
KEY idx_status_touch (status, first_touch_at)
KEY idx_status_touch (status, first_touch_at),
KEY idx_reward_status (reward_status),
KEY idx_reward_ip (registration_ip, rewarded_at)
) {$charset_collate};";
dbDelta($sql_affiliate_relation);
-1
View File
@@ -19,7 +19,6 @@ trait DBMigratorPart3 {
'credit_log' => 'aksrt_credit_log',
'balance_log' => 'aksrt_balance_log',
'signin_log' => 'aksrt_signin_log',
'invite' => 'aksrt_invite',
'dl_token' => 'aksrt_dl_token',
'notification' => 'aksrt_notification',
'conversation' => 'aksrt_conversation',
+2 -2
View File
@@ -33,8 +33,8 @@ class Credits {
*
* @param int $user_id
* @param int $delta 必须 > 0
* @param string $reason 例 register/signin/comment/invite/admin
* @param array $ref ['type' => 'post|comment|user|invite', 'id' => int]
* @param string $reason 例 register/signin/comment/referral/admin
* @param array $ref ['type' => 'post|comment|user|referral', 'id' => int]
* @return int|\WP_Error 新余额或错误
*/
public static function add(int $user_id, int $delta, string $reason, array $ref = [], string $remark = '', bool $dispatch_hooks = true) {
-683
View File
@@ -1,683 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Credits;
defined('ABSPATH') || exit;
class Invite {
const COOKIE_NAME = 'aksrt_invite';
const COOKIE_TTL_DAYS = 30;
const META_INVITE_CODE = 'aksrt_invite_code';
const OPT_STANDALONE_CODES = 'aksrt_standalone_invite_codes';
/**
* 反向映射 option:code => user_id
*
* 注意:option 大小受限(默认非 autoload),10 万级用户没问题,
* 百万级仍可走,性能瓶颈在 update_option 写入;此处采用懒写策略:
* 仅在 get_or_create_code 生成新码时同步写入。
*/
const OPT_REVERSE_MAP = 'aksrt_invite_code_map';
private static $instance = null;
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public function init(): void {
// 在请求最早期捕获邀请码
add_action('init', [$this, 'capture_invite_cookie'], 1);
// 注册时记录邀请关系
add_action('user_register', [$this, 'on_user_register'], 20, 1);
// 邮箱验证后兑现
add_action('aksrt_email_verified', [$this, 'on_email_verified'], 10, 1);
// 用户删除时清理邀请码映射
add_action('delete_user', [$this, 'on_delete_user'], 10, 1);
add_action('rest_api_init', [$this, 'register_routes']);
}
public function register_routes(): void {
register_rest_route('aksrt/v1', '/invite/me', [
'methods' => 'GET',
'callback' => [$this, 'rest_me'],
'permission_callback' => function () {
return is_user_logged_in();
},
]);
register_rest_route('aksrt/v1', '/invite/list', [
'methods' => 'GET',
'callback' => [$this, 'rest_list'],
'permission_callback' => function () {
return is_user_logged_in();
},
'args' => [
'page' => [
'type' => 'integer',
'default' => 1,
'minimum' => 1,
],
'per_page' => [
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 50,
],
],
]);
}
/** Capture the unified ?ref=XYZ referral parameter. */
public function capture_invite_cookie(): void {
if (is_admin()) return;
$raw_code = $_GET['ref'] ?? '';
if ($raw_code === '') return;
// 邀请注册功能关闭时不处理
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::invite_registration_enabled()) return;
$code = sanitize_key(wp_unslash((string) $raw_code));
if ($code === '') return;
// 校验邀请码是否对应真实用户
$inviter_id = $this->resolve_code($code);
if ($inviter_id <= 0) return;
// 已登录用户不能用自己的邀请码
if (is_user_logged_in() && $inviter_id === get_current_user_id()) return;
// 不覆盖已存在的(防多次访问被覆盖)
if (!empty($_COOKIE[self::COOKIE_NAME])) return;
$expires = time() + DAY_IN_SECONDS * self::COOKIE_TTL_DAYS;
// 不覆盖 secure 标记的检测
$secure = is_ssl();
// headers_sent 检查
if (!headers_sent()) {
setcookie(self::COOKIE_NAME, $code, [
'expires' => $expires,
'path' => COOKIEPATH ?: '/',
'domain' => COOKIE_DOMAIN,
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
$_COOKIE[self::COOKIE_NAME] = $code;
}
}
/** 注册:写入 aksrt_invite 表 */
public function on_user_register(int $invitee_id): void {
// 邀请注册功能关闭时不记录邀请关系
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::invite_registration_enabled()) return;
// 优先级 1:表单显式填写的邀请码(由 \AKSRT\Auth\Service 注入)
$code = '';
$inviter_id = 0;
if (class_exists('\AKSRT\Auth\Service')) {
$ctx = \AKSRT\Auth\Service::take_pending_invite_context();
if (!empty($ctx['inviter_id']) && $ctx['inviter_id'] !== $invitee_id) {
$inviter_id = (int) $ctx['inviter_id'];
$code = (string) $ctx['code'];
}
}
// 优先级 2:fallback 到 cookie
if ($inviter_id <= 0) {
if (empty($_COOKIE[self::COOKIE_NAME])) return;
$code = sanitize_key((string) $_COOKIE[self::COOKIE_NAME]);
if ($code === '') return;
$inviter_id = $this->get_user_by_code($code);
}
if ($inviter_id <= 0 || $inviter_id === $invitee_id) return;
$this->record_invite_registration($inviter_id, $invitee_id, $code, 'registered');
// 清 cookie,避免再次触发
if (!headers_sent()) {
setcookie(self::COOKIE_NAME, '', time() - 3600, COOKIEPATH ?: '/', COOKIE_DOMAIN);
}
// 若配置不要求邮箱验证就立即兑现
$invite_cfg = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_invite_reward_settings()
: [
'require_verify' => true,
];
if (empty($invite_cfg['require_verify'])) {
$this->reward_pair($inviter_id, $invitee_id);
}
}
/** 邮箱验证后兑现奖励(钩子由 \AKSRT\Auth\Service 触发,可后续接入) */
public function on_email_verified(int $user_id): void {
// 邀请注册功能关闭时不兑现奖励
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::invite_registration_enabled()) return;
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('invite');
if (!$table) return;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$table} WHERE invitee_id = %d AND status IN ('registered', 'verified') LIMIT 1",
$user_id
), ARRAY_A);
if (!$row) return;
$wpdb->update(
$table,
['status' => 'verified'],
['id' => (int) $row['id']],
['%s'],
['%d']
);
if ((int) $row['inviter_id'] <= 0) {
return;
}
$this->reward_pair((int) $row['inviter_id'], $user_id);
}
/** 用户被删除时清理邀请码映射 */
public function on_delete_user(int $user_id): void {
$code = (string) get_user_meta($user_id, self::META_INVITE_CODE, true);
if ($code === '') return;
$map = $this->load_reverse_map();
if (isset($map[$code]) && (int) $map[$code] === $user_id) {
unset($map[$code]);
update_option(self::OPT_REVERSE_MAP, $map, false);
}
}
/** 双向发放奖励 */
private function reward_pair(int $inviter_id, int $invitee_id): void {
if (!class_exists('AKSRT\\Credits\\Credits') || !class_exists('\AKSRT\Settings\Settings')) return;
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('invite');
if (!$table) return;
$claimed = $wpdb->update(
$table,
['status' => 'rewarding'],
['inviter_id' => $inviter_id, 'invitee_id' => $invitee_id, 'status' => 'verified'],
['%s'],
['%d', '%d', '%s']
);
if ($claimed !== 1) return;
$invite_cfg = \AKSRT\Settings\Settings::get_invite_reward_settings();
if (empty($invite_cfg['enabled']) || !$this->ip_daily_check($invite_cfg)) {
$wpdb->update($table, ['status' => 'verified'], ['inviter_id' => $inviter_id, 'invitee_id' => $invitee_id], ['%s'], ['%d', '%d']);
return;
}
$inviter_amount = (int) ($invite_cfg['inviter_amount'] ?? 0);
$invitee_amount = (int) ($invite_cfg['invitee_amount'] ?? 0);
$level_integral = max(0, (int) ($invite_cfg['invit_reward_level'] ?? 0));
if ($inviter_amount > 0) {
Credits::add($inviter_id, $inviter_amount, 'invite', ['type' => 'user', 'id' => $invitee_id]);
}
if ($invitee_amount > 0) {
Credits::add($invitee_id, $invitee_amount, 'invitee', ['type' => 'user', 'id' => $inviter_id]);
}
if ($level_integral > 0 && class_exists('\AKSRT\User\Level')) {
\AKSRT\User\Level::add($inviter_id, 'invitation', $level_integral, true);
}
// 标记 rewarded
$wpdb->update(
$table,
[
'status' => 'rewarded',
'rewarded_at' => current_time('mysql', true),
],
[
'inviter_id' => $inviter_id,
'invitee_id' => $invitee_id,
],
['%s', '%s'],
['%d', '%d']
);
}
/** 同 IP 当日邀请兑现次数限制 */
private function ip_daily_check(array $invite_cfg): bool {
$cap = (int) ($invite_cfg['ip_daily_cap'] ?? 3);
if ($cap <= 0) return true;
$ip = function_exists('aksrt_get_client_ip') ? aksrt_get_client_ip() : '';
if (!$ip) return true;
global $wpdb;
$log_table = \AKSRT\Core\DBMigrator::table('credit_log');
if (!$log_table) return true;
$today_utc = (new \DateTimeImmutable('today', wp_timezone()))->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
$count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$log_table} WHERE reason = 'invite' AND ip = %s AND created_at >= %s",
$ip,
$today_utc
));
return $count < $cap;
}
/** 取或生成用户的邀请码 */
public function get_or_create_code(int $user_id): string {
$code = (string) get_user_meta($user_id, self::META_INVITE_CODE, true);
// 邀请码长度(后台可配置,4-12,默认 6)
$code_length = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::invite_code_length()
: max(4, min(12, (int) aksrt_get_setting('invite_code_length', 6)));
// 兼容配置变更:已有码只要长度合理就复用
if ($code !== '' && strlen($code) <= $code_length + 2) return $code;
// 生成唯一码
$alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
$map = $this->load_reverse_map();
for ($i = 0; $i < 5; $i++) {
$code = '';
for ($j = 0; $j < $code_length; $j++) {
$code .= $alphabet[random_int(0, strlen($alphabet) - 1)];
}
if (!isset($map[$code]) && !$this->code_exists_legacy($code)) break;
}
update_user_meta($user_id, self::META_INVITE_CODE, $code);
// 同步反向映射
$map[$code] = $user_id;
update_option(self::OPT_REVERSE_MAP, $map, false);
return $code;
}
/**
* 公开 API:根据邀请码反查邀请人 user_id(不存在返回 0)。
* 给注册流程在 wp_create_user 之前做"邀请码是否合法"的预校验用。
*/
/**
* Resolve affiliate / invite referrer for a buyer (zibll-style).
*
* Priority:
* 1. Logged-in: inviter_id from aksrt_invite table for this invitee
* 2. Cookie invite code → resolve_code
*
* Respects pay_rebate_self_s when available (via affiliate settings).
*/
public function get_referrer(int $user_id = 0): int {
$uid = $user_id > 0 ? $user_id : get_current_user_id();
$referrer_id = 0;
if ($uid > 0) {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('invite');
if ($table) {
$referrer_id = (int) $wpdb->get_var($wpdb->prepare(
"SELECT inviter_id FROM {$table} WHERE invitee_id = %d AND inviter_id > 0 ORDER BY id DESC LIMIT 1",
$uid
));
}
}
if ($referrer_id <= 0) {
$code = '';
if (!empty($_COOKIE[self::COOKIE_NAME])) {
$code = sanitize_key((string) $_COOKIE[self::COOKIE_NAME]);
}
if ($code !== '') {
$referrer_id = $this->resolve_code($code);
}
}
if ($referrer_id <= 0) {
return 0;
}
// Self-purchase rebate gate (default allow, matching pay_rebate_self_s default true).
$allow_self = true;
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_payment_affiliate_settings')) {
$aff = \AKSRT\Settings\Settings::get_payment_affiliate_settings();
$allow_self = !isset($aff['self_s']) || !empty($aff['self_s']);
}
if ($uid > 0 && $referrer_id === $uid && !$allow_self) {
return 0;
}
return $referrer_id;
}
public function resolve_code(string $code): int {
$code = sanitize_key($code);
if ($code === '') return 0;
return $this->get_user_by_code($code);
}
/**
* 校验邀请码是否合法。
*
* 用户邀请码与后台批量生成的独立邀请码都视为有效。
*/
public function validate_code(string $code): bool {
$code = sanitize_key($code);
if ($code === '') {
return false;
}
if ($this->get_user_by_code($code) > 0) {
return true;
}
return $this->find_standalone_code_index($code) >= 0;
}
/**
* 注册完成后处理邀请码。
*
* - 用户邀请码:沿用 inviter/invitee canonical 关系
* - 独立邀请码:记录使用人并标记为已消耗,不参与奖励
*/
public function handle_registration_invite(int $invitee_id, string $invite_code): void {
$invitee_id = (int) $invitee_id;
$invite_code = sanitize_key($invite_code);
if ($invitee_id <= 0 || $invite_code === '') {
return;
}
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::invite_registration_enabled()) {
return;
}
$inviter_id = $this->get_user_by_code($invite_code);
if ($inviter_id > 0) {
if ($inviter_id === $invitee_id) {
return;
}
$this->record_invite_registration($inviter_id, $invitee_id, $invite_code, 'registered');
return;
}
$index = $this->find_standalone_code_index($invite_code);
if ($index < 0) {
return;
}
$requires_verify = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::registration_requires_email_verification();
$status = $requires_verify ? 'registered' : 'verified';
if ($this->record_invite_registration(0, $invitee_id, $invite_code, $status)) {
$this->mark_standalone_code_used($invite_code, $invitee_id);
}
}
/** 反查邀请码对应的用户(O(1) option 查表,回退 user_meta 扫描) */
private function get_user_by_code(string $code): int {
if ($code === '') return 0;
$map = $this->load_reverse_map();
if (isset($map[$code])) {
$uid = (int) $map[$code];
if ($uid > 0) {
// 校验:确保该用户的 meta 仍然指向该 code(用户可能已被删除或 meta 被清空)
$meta_code = (string) get_user_meta($uid, self::META_INVITE_CODE, true);
if ($meta_code === $code) {
return $uid;
}
// 失配 → 修正映射
unset($map[$code]);
update_option(self::OPT_REVERSE_MAP, $map, false);
}
}
// 回退 + 自愈:扫描 user_meta(应只在迁移期或映射损坏时触发)
$uid = $this->code_exists_legacy($code);
if ($uid > 0) {
$map[$code] = $uid;
update_option(self::OPT_REVERSE_MAP, $map, false);
}
return $uid;
}
private function load_reverse_map(): array {
$map = get_option(self::OPT_REVERSE_MAP, []);
return is_array($map) ? $map : [];
}
/**
* 写入邀请关系记录。
*/
private function record_invite_registration(int $inviter_id, int $invitee_id, string $code, string $status = 'registered'): bool {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('invite');
if (!$table || $invitee_id <= 0 || $code === '') {
return false;
}
$existing_code = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table} WHERE invite_code = %s LIMIT 1",
$code
));
if ($existing_code) {
return false;
}
$existing_invitee = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table} WHERE invitee_id = %d LIMIT 1",
$invitee_id
));
if ($existing_invitee) {
return false;
}
$result = $wpdb->insert(
$table,
[
'inviter_id' => max(0, $inviter_id),
'invitee_id' => $invitee_id,
'invite_code' => $code,
'status' => $status,
'created_at' => current_time('mysql', true),
],
['%d', '%d', '%s', '%s', '%s']
);
return (bool) $result;
}
private function load_standalone_codes(): array {
$codes = get_option(self::OPT_STANDALONE_CODES, []);
return is_array($codes) ? array_values($codes) : [];
}
private function save_standalone_codes(array $codes): void {
update_option(self::OPT_STANDALONE_CODES, array_values($codes), false);
}
private function normalize_standalone_code(array $entry): array {
$code = sanitize_key((string) ($entry['code'] ?? ''));
$used_by = (int) ($entry['used_by'] ?? 0);
$used = !empty($entry['used']) || $used_by > 0;
return [
'code' => $code,
'created_at' => sanitize_text_field((string) ($entry['created_at'] ?? '')),
'used' => $used,
'used_by' => $used_by,
'used_at' => sanitize_text_field((string) ($entry['used_at'] ?? '')),
'disabled' => !empty($entry['disabled']),
'note' => sanitize_textarea_field((string) ($entry['note'] ?? '')),
'batch_id' => sanitize_text_field((string) ($entry['batch_id'] ?? '')),
'created_by' => (int) ($entry['created_by'] ?? 0),
];
}
private function find_standalone_code_index(string $code): int {
$code = sanitize_key($code);
$codes = $this->load_standalone_codes();
foreach ($codes as $index => $entry) {
$normalized = $this->normalize_standalone_code((array) $entry);
if ($normalized['code'] !== $code) {
continue;
}
if ($normalized['disabled'] || $normalized['used']) {
return -1;
}
return $index;
}
return -1;
}
private function mark_standalone_code_used(string $code, int $invitee_id): void {
$code = sanitize_key($code);
$codes = $this->load_standalone_codes();
foreach ($codes as $index => $entry) {
$normalized = $this->normalize_standalone_code((array) $entry);
if ($normalized['code'] !== $code) {
continue;
}
$normalized['used'] = true;
$normalized['used_by'] = $invitee_id;
$normalized['used_at'] = current_time('mysql', true);
$codes[$index] = $normalized;
$this->save_standalone_codes($codes);
return;
}
}
/** 旧路径:通过 user_meta 反查(保留作为自愈兜底) */
private function code_exists_legacy(string $code): int {
$users = get_users([
'meta_key' => self::META_INVITE_CODE,
'meta_value' => $code,
'number' => 1,
'fields' => 'ID',
]);
if (empty($users)) return 0;
return (int) $users[0];
}
private function code_exists(string $code): bool {
return $this->get_user_by_code($code) > 0;
}
// ---------- REST ----------
public function rest_me(\WP_REST_Request $request) {
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::invite_registration_enabled()) {
return new \WP_REST_Response(['code' => 'error', 'message' => '邀请注册功能已关闭'], 403);
}
$user_id = get_current_user_id();
$code = $this->get_or_create_code($user_id);
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('invite');
$stats = [
'registered' => 0,
'verified' => 0,
'rewarded' => 0,
];
if ($table) {
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT status, COUNT(*) AS c FROM {$table} WHERE inviter_id = %d GROUP BY status",
$user_id
), ARRAY_A);
if (is_array($rows)) {
foreach ($rows as $r) {
$status = (string) $r['status'];
$count = (int) $r['c'];
// The stored status is terminal; dashboard stats are milestone totals.
if ($status === 'registered') {
$stats['registered'] += $count;
} elseif ($status === 'verified') {
$stats['registered'] += $count;
$stats['verified'] += $count;
} elseif ($status === 'rewarded') {
$stats['registered'] += $count;
$stats['verified'] += $count;
$stats['rewarded'] += $count;
}
}
}
}
$invite_url = add_query_arg('ref', $code, home_url('/'));
return new \WP_REST_Response([
'code' => $code,
'invite_url' => $invite_url,
'stats' => $stats,
], 200);
}
public function rest_list(\WP_REST_Request $request) {
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::invite_registration_enabled()) {
return new \WP_REST_Response(['code' => 'error', 'message' => '邀请注册功能已关闭'], 403);
}
global $wpdb;
$user_id = get_current_user_id();
$page = max(1, (int) $request->get_param('page'));
$per_page = min(50, max(1, (int) $request->get_param('per_page') ?: 10));
$table = \AKSRT\Core\DBMigrator::table('invite');
if (!$table) {
return new \WP_REST_Response(['items' => [], 'total' => 0], 200);
}
$total = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE inviter_id = %d",
$user_id
));
$total_pages = max(1, (int) ceil($total / $per_page));
$page = min($page, $total_pages);
$offset = ($page - 1) * $per_page;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$table} WHERE inviter_id = %d ORDER BY created_at DESC LIMIT %d OFFSET %d",
$user_id,
$per_page,
$offset
), ARRAY_A);
$items = [];
foreach ((array) $rows as $r) {
$invitee = $r['invitee_id'] ? get_userdata((int) $r['invitee_id']) : null;
$items[] = [
'id' => (int) $r['id'],
'invitee_id' => (int) $r['invitee_id'],
'invitee_name' => $invitee ? $invitee->display_name : '',
'invitee_avatar' => $invitee ? get_avatar_url((int) $invitee->ID, ['size' => 64]) : '',
'status' => (string) $r['status'],
'created_at' => (string) $r['created_at'],
'rewarded_at' => (string) $r['rewarded_at'],
];
}
return new \WP_REST_Response([
'items' => $items,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => $total_pages,
], 200);
}
}
+1 -2
View File
@@ -12,7 +12,6 @@ class Rebate {
private static $instance = null;
const TYPE_ORDER = 'order';
const TYPE_invite = 'invite';
const STATUS_PENDING = 'pending';
const STATUS_SETTLED = 'settled';
@@ -58,7 +57,7 @@ class Rebate {
return 0;
}
$valid_types = [self::TYPE_ORDER, self::TYPE_invite];
$valid_types = [self::TYPE_ORDER];
if (!in_array($type, $valid_types, true)) {
$type = self::TYPE_ORDER;
}
+527 -72
View File
@@ -1,6 +1,6 @@
<?php
/**
* First-touch referral attribution for the affiliate contract.
* Canonical ref-code attribution, registration binding, and referral rewards.
*
* @package AKSRT
*/
@@ -13,8 +13,13 @@ final class ReferralAttribution {
private const COOKIE = 'aksrt_referral';
private const META_CODE = 'aksrt_referral_code';
private const META_STATUS = 'aksrt_referral_code_status';
private const STATUS_ACTIVE = 'active';
private const STATUS_DISABLED = 'disabled';
private static ?self $instance = null;
private static string $pending_registration_ref = '';
private static string $registration_notice = '';
public static function get_instance(): self {
return self::$instance ??= new self();
@@ -25,20 +30,31 @@ final class ReferralAttribution {
public function init(): void {
add_action('init', [$this, 'capture'], 1);
add_action('user_register', [$this, 'bind_registered_user'], 20, 1);
add_action('aksrt_email_verified', [$this, 'reward_registered_user'], 10, 1);
add_action('rest_api_init', [$this, 'register_routes']);
}
/** Capture only the new ref contract; invite/legacy parameters are ignored. */
public function capture(): void {
if (is_admin() || !class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::affiliate_enabled()) {
if (is_admin() || !$this->feature_enabled()) {
return;
}
$raw = isset($_GET['ref']) ? sanitize_key(wp_unslash((string) $_GET['ref'])) : '';
if ($raw === '' || $this->resolve_promoter($raw) <= 0 || !empty($_COOKIE[self::COOKIE])) {
if ($raw === '') {
return;
}
$promoter_id = $this->resolve_promoter($raw);
if ($promoter_id <= 0 || (is_user_logged_in() && $promoter_id === get_current_user_id())) {
return;
}
if ($this->cookie_ref() !== '') {
return;
}
if (!headers_sent()) {
$ttl_days = class_exists('\\AKSRT\\Settings\\Settings') ? (int) (\AKSRT\Settings\Settings::get_affiliate_settings()['window_days'] ?? 30) : 30;
$ttl_days = (int) ($this->shared_settings()['window_days'] ?? 30);
setcookie(self::COOKIE, $raw, [
'expires' => time() + DAY_IN_SECONDS * max(1, min(365, $ttl_days)),
'path' => COOKIEPATH ?: '/',
@@ -51,66 +67,214 @@ final class ReferralAttribution {
}
}
public function create_link(int $user_id, string $url = ''): array {
$code = $this->get_or_create_code($user_id);
$target = $url !== '' ? $url : home_url('/');
return ['code' => $code, 'url' => add_query_arg('ref', $code, $target)];
/** @return array{ref:string,valid:bool,submitted_valid:bool,conflict:bool,source:string} */
public function prepare_registration(string $submitted_ref): array {
self::$registration_notice = '';
$submitted_ref = sanitize_key($submitted_ref);
$cookie_ref = $this->cookie_ref();
$submitted_valid = $submitted_ref !== '' && $this->resolve_promoter($submitted_ref) > 0;
$conflict = $cookie_ref !== '' && $submitted_ref !== '' && $submitted_ref !== $cookie_ref;
if ($conflict) {
self::$registration_notice = __('检测到不同的推广码,已按首次访问的推广关系绑定。', 'aksrt');
}
public function get_or_create_code(int $user_id): string {
if ($user_id <= 0) {
return '';
$ref = $cookie_ref !== '' ? $cookie_ref : ($submitted_valid ? $submitted_ref : '');
self::$pending_registration_ref = $ref;
return [
'ref' => $ref,
'valid' => $ref !== '',
'submitted_valid' => $submitted_valid,
'conflict' => $conflict,
'source' => $cookie_ref !== '' ? 'cookie' : ($submitted_valid ? 'submitted' : 'none'),
];
}
$existing = sanitize_key((string) get_user_meta($user_id, self::META_CODE, true));
if ($existing !== '' && strlen($existing) >= 10) {
return $existing;
public function clear_registration_context(): void {
self::$pending_registration_ref = '';
}
$alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
for ($attempt = 0; $attempt < 10; $attempt++) {
$code = '';
for ($i = 0; $i < 12; $i++) {
$code .= $alphabet[random_int(0, strlen($alphabet) - 1)];
}
if ($this->resolve_promoter($code) <= 0) {
update_user_meta($user_id, self::META_CODE, $code);
return $code;
}
}
return '';
public function take_registration_notice(): string {
$notice = self::$registration_notice;
self::$registration_notice = '';
return $notice;
}
public function bind_registered_user(int $invitee_id): void {
if ($invitee_id <= 0 || !class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::affiliate_enabled()) {
if ($invitee_id <= 0 || !$this->feature_enabled()) {
self::$pending_registration_ref = '';
return;
}
$ref = self::$pending_registration_ref !== ''
? self::$pending_registration_ref
: $this->cookie_ref();
self::$pending_registration_ref = '';
$promoter_id = $this->resolve_promoter($ref);
if ($ref === '' || $promoter_id <= 0 || $promoter_id === $invitee_id) {
return;
}
global $wpdb;
$code = sanitize_key((string) ($_COOKIE[self::COOKIE] ?? ''));
$promoter_id = $this->resolve_promoter($code);
if ($code === '' || $promoter_id <= 0 || $promoter_id === $invitee_id) {
return;
}
$table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
if ($table === '') {
return;
}
$existing = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table} WHERE invitee_id = %d LIMIT 1", $invitee_id));
if ($existing) {
$existing = (int) $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table} WHERE invitee_id = %d LIMIT 1",
$invitee_id
));
if ($existing > 0) {
$this->clear_cookie();
return;
}
$reward = $this->reward_settings();
$now = current_time('mysql', true);
$wpdb->insert($table, [
'referral_code' => $code,
$inserted = $wpdb->insert($table, [
'referral_code' => $ref,
'promoter_id' => $promoter_id,
'invitee_id' => $invitee_id,
'first_touch_at'=> $now,
'first_touch_at' => $now,
'bound_at' => $now,
'status' => 'bound',
'reward_status' => !empty($reward['enabled']) ? 'pending' : 'skipped',
'registration_ip' => $this->client_ip(),
'created_at' => $now,
], ['%s', '%d', '%d', '%s', '%s', '%s', '%s']);
if (!headers_sent()) {
setcookie(self::COOKIE, '', time() - HOUR_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN);
], ['%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']);
if ($inserted) {
$this->clear_cookie();
if (!empty($reward['enabled']) && (empty($reward['require_verify']) || $this->is_verified($invitee_id))) {
$this->reward_registered_user($invitee_id);
}
}
}
public function reward_registered_user(int $invitee_id): void {
$settings = $this->reward_settings();
if ($invitee_id <= 0) {
return;
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
if ($table === '') {
return;
}
if (empty($settings['enabled'])) {
$wpdb->update(
$table,
['reward_status' => 'skipped'],
['invitee_id' => $invitee_id, 'reward_status' => 'pending'],
['%s'],
['%d', '%s']
);
return;
}
if (!class_exists('\AKSRT\Credits\Credits')) {
return;
}
if (!empty($settings['require_verify']) && !$this->is_verified($invitee_id)) {
return;
}
$wpdb->query('START TRANSACTION');
try {
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$table} WHERE invitee_id = %d AND status = 'bound' FOR UPDATE",
$invitee_id
), ARRAY_A);
if (!is_array($row) || (string) ($row['reward_status'] ?? 'pending') !== 'pending') {
$wpdb->query('ROLLBACK');
return;
}
$relation_id = (int) $row['id'];
$promoter_id = (int) $row['promoter_id'];
if ($promoter_id <= 0 || !$this->ip_daily_check($row, $settings)) {
$wpdb->update($table, ['reward_status' => 'skipped'], ['id' => $relation_id], ['%s'], ['%d']);
$wpdb->query('COMMIT');
return;
}
$claimed = $wpdb->update(
$table,
['reward_status' => 'rewarding'],
['id' => $relation_id, 'reward_status' => 'pending'],
['%s'],
['%d', '%s']
);
if ($claimed !== 1) {
$wpdb->query('ROLLBACK');
return;
}
$ref = ['type' => 'referral', 'id' => $relation_id];
$promoter_amount = max(0, (int) ($settings['promoter_amount'] ?? 0));
$referred_amount = max(0, (int) ($settings['referred_amount'] ?? 0));
if ($promoter_amount > 0) {
$result = \AKSRT\Credits\Credits::add($promoter_id, $promoter_amount, 'referral', $ref, __('推广注册奖励', 'aksrt'), false);
if (is_wp_error($result)) {
throw new \RuntimeException($result->get_error_message());
}
}
if ($referred_amount > 0) {
$result = \AKSRT\Credits\Credits::add($invitee_id, $referred_amount, 'referral_new_user', $ref, __('推广注册新用户奖励', 'aksrt'), false);
if (is_wp_error($result)) {
throw new \RuntimeException($result->get_error_message());
}
}
$level_points = max(0, (int) ($settings['level_points'] ?? 0));
if ($level_points > 0 && class_exists('\AKSRT\User\Level')) {
\AKSRT\User\Level::add($promoter_id, 'referral', $level_points, true);
}
$rewarded_at = current_time('mysql', true);
$updated = $wpdb->update(
$table,
['reward_status' => 'rewarded', 'rewarded_at' => $rewarded_at],
['id' => $relation_id, 'reward_status' => 'rewarding'],
['%s', '%s'],
['%d', '%s']
);
if ($updated !== 1) {
throw new \RuntimeException('Unable to finalize referral reward.');
}
$wpdb->query('COMMIT');
do_action('aksrt_referral_rewarded', $relation_id, $promoter_id, $invitee_id);
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK');
}
}
public function create_link(int $user_id, string $url = ''): array {
$record = $this->code_record($user_id);
if ($record['status'] === 'missing' && $this->feature_enabled()) {
$code = $this->generate_code($user_id, false);
$record = ['code' => $code, 'status' => $code !== '' ? self::STATUS_ACTIVE : 'missing'];
}
$target = $url !== '' ? $url : home_url('/');
$active_code = $record['status'] === self::STATUS_ACTIVE ? $record['code'] : '';
return [
'ref' => $active_code,
'url' => $active_code !== '' ? add_query_arg('ref', $active_code, $target) : '',
'status' => $record['status'],
];
}
public function get_or_create_code(int $user_id): string {
return (string) ($this->create_link($user_id)['ref'] ?? '');
}
public function validate_ref(string $ref): bool {
return $this->resolve_promoter($ref) > 0;
}
public function resolve(int $user_id = 0): int {
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
@@ -119,13 +283,11 @@ final class ReferralAttribution {
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
$bound = $table !== '' ? (int) $wpdb->get_var($wpdb->prepare("SELECT promoter_id FROM {$table} WHERE invitee_id = %d AND status = 'bound' LIMIT 1", $user_id)) : 0;
if ($bound > 0) {
return $bound === $user_id ? 0 : $bound;
}
// Orders may only use a permanently bound relation. A browsing cookie
// never becomes an implicit post-login override.
return 0;
$bound = $table !== '' ? (int) $wpdb->get_var($wpdb->prepare(
"SELECT promoter_id FROM {$table} WHERE invitee_id = %d AND status = 'bound' LIMIT 1",
$user_id
)) : 0;
return $bound > 0 && $bound !== $user_id ? $bound : 0;
}
public function stats(int $promoter_id): array {
@@ -134,17 +296,19 @@ final class ReferralAttribution {
if ($promoter_id <= 0 || $table === '') {
return ['promoters' => 0, 'effective_payers' => 0];
}
$promoters = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE promoter_id = %d AND status = 'bound'", $promoter_id));
$promoters = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE promoter_id = %d AND status = 'bound'",
$promoter_id
));
$ledger = \AKSRT\Core\DBMigrator::table('commission_ledger');
$payers = $ledger !== '' ? (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(DISTINCT source_user_id) FROM {$ledger} WHERE promoter_id = %d AND original_amount > 0", $promoter_id)) : 0;
$payers = $ledger !== '' ? (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(DISTINCT source_user_id) FROM {$ledger} WHERE promoter_id = %d AND original_amount > 0",
$promoter_id
)) : 0;
return ['promoters' => $promoters, 'effective_payers' => $payers];
}
/**
* Return users bound through the forward-only ref relation.
*
* @return array{items:array<int,array<string,mixed>>,total:int,page:int,per_page:int,max_pages:int}
*/
/** @return array{items:array<int,array<string,mixed>>,total:int,page:int,per_page:int,max_pages:int} */
public function list_referred_users(int $promoter_id, int $page = 1, int $per_page = 10): array {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
@@ -158,27 +322,33 @@ final class ReferralAttribution {
$promoter_id
));
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT invitee_id, referral_code, bound_at, status FROM {$table} WHERE promoter_id=%d AND status='bound' AND invitee_id > 0 ORDER BY id DESC LIMIT %d OFFSET %d",
"SELECT invitee_id, bound_at, status FROM {$table} WHERE promoter_id=%d AND status='bound' AND invitee_id > 0 ORDER BY id DESC LIMIT %d OFFSET %d",
$promoter_id,
$per_page,
($page - 1) * $per_page
), ARRAY_A);
$items = [];
foreach ((array) $rows as $row) {
$invitee_id = (int) ($row['invitee_id'] ?? 0);
$user = $invitee_id > 0 ? get_userdata($invitee_id) : null;
$user_id = (int) ($row['invitee_id'] ?? 0);
$user = $user_id > 0 ? get_userdata($user_id) : null;
if (!$user instanceof \WP_User) {
continue;
}
$items[] = [
'user_id' => $invitee_id,
'user_id' => $user_id,
'name' => $user->display_name,
'avatar' => get_avatar_url($invitee_id, ['size' => 64]),
'avatar' => get_avatar_url($user_id, ['size' => 64]),
'bound_at' => (string) ($row['bound_at'] ?? ''),
'status' => (string) ($row['status'] ?? 'bound'),
];
}
return ['items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $per_page, 'max_pages' => max(1, (int) ceil($total / $per_page))];
return [
'items' => $items,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'max_pages' => max(1, (int) ceil($total / $per_page)),
];
}
public function register_routes(): void {
@@ -187,30 +357,315 @@ final class ReferralAttribution {
'callback' => [$this, 'rest_me'],
'permission_callback' => static fn() => is_user_logged_in(),
]);
register_rest_route('aksrt/v1', '/revenue/codes', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'rest_codes'],
'permission_callback' => [$this, 'can_manage_codes'],
'args' => [
'page' => ['type' => 'integer', 'default' => 1, 'minimum' => 1],
'per_page' => ['type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 100],
'search' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
'status' => [
'type' => 'string',
'default' => 'all',
'enum' => ['all', self::STATUS_ACTIVE, self::STATUS_DISABLED, 'missing'],
],
],
]);
foreach (['generate', 'disable', 'regenerate'] as $action) {
register_rest_route('aksrt/v1', '/revenue/codes/(?P<user_id>\d+)/' . $action, [
'methods' => \WP_REST_Server::CREATABLE,
'callback' => function (\WP_REST_Request $request) use ($action) {
return $this->rest_mutate_code($request, $action);
},
'permission_callback' => [$this, 'can_manage_codes'],
'args' => [
'user_id' => ['type' => 'integer', 'required' => true, 'minimum' => 1],
],
]);
}
}
public function can_manage_codes(): bool {
return current_user_can('manage_users');
}
public function rest_me(): \WP_REST_Response {
$uid = get_current_user_id();
$link = $this->create_link($uid);
$stats = $this->stats($uid);
$settings = \AKSRT\Settings\Settings::get_affiliate_settings();
$user_id = get_current_user_id();
$link = $this->create_link($user_id);
$stats = $this->stats($user_id);
$affiliate = \AKSRT\Settings\Settings::get_affiliate_settings();
$registration = $this->registration_settings();
$reward = $this->reward_settings();
return new \WP_REST_Response([
'code' => $link['code'],
'ref' => $link['ref'],
'url' => $link['url'],
'current_ratio' => (float) \AKSRT\Settings\Settings::affiliate_ratio_for_user($uid),
'ratios' => (array) ($settings['ratios'] ?? []),
'order_types' => (array) ($settings['order_types'] ?? ['membership', 'content']),
'rules' => ['window_days' => (int) ($settings['window_days'] ?? 30), 'settlement_days' => (int) ($settings['settlement_days'] ?? 7)],
'status' => $link['status'],
'features' => [
'registration' => !empty($registration['enabled']),
'points_reward' => !empty($reward['enabled']),
'affiliate' => !empty($affiliate['enabled']),
],
'current_ratio' => (float) \AKSRT\Settings\Settings::affiliate_ratio_for_user($user_id),
'ratios' => (array) ($affiliate['ratios'] ?? []),
'order_types' => (array) ($affiliate['order_types'] ?? ['membership', 'content']),
'rules' => [
'window_days' => (int) ($this->shared_settings()['window_days'] ?? 30),
'settlement_days' => (int) ($affiliate['settlement_days'] ?? 7),
],
'stats' => $stats,
], 200);
}
public function rest_codes(\WP_REST_Request $request): \WP_REST_Response {
return new \WP_REST_Response($this->list_codes([
'page' => (int) $request['page'],
'per_page' => (int) $request['per_page'],
'search' => (string) $request['search'],
'status' => (string) $request['status'],
]), 200);
}
public function rest_mutate_code(\WP_REST_Request $request, string $action) {
$user_id = (int) $request['user_id'];
if (!get_userdata($user_id)) {
return new \WP_Error('aksrt_ref_user_missing', __('用户不存在', 'aksrt'), ['status' => 404]);
}
$record = $this->code_record($user_id);
if ($action === 'generate') {
if ($record['status'] !== 'missing') {
return new \WP_Error('aksrt_ref_exists', __('该用户已有推广码', 'aksrt'), ['status' => 409]);
}
$code = $this->generate_code($user_id, false);
} elseif ($action === 'disable') {
if ($record['status'] !== self::STATUS_ACTIVE) {
return new \WP_Error('aksrt_ref_not_active', __('推广码当前不可禁用', 'aksrt'), ['status' => 409]);
}
update_user_meta($user_id, self::META_STATUS, self::STATUS_DISABLED);
if ($this->code_record($user_id)['status'] !== self::STATUS_DISABLED) {
return new \WP_Error('aksrt_ref_disable_failed', __('推广码禁用失败', 'aksrt'), ['status' => 500]);
}
$code = $record['code'];
} elseif ($action === 'regenerate') {
if ($record['status'] === 'missing') {
return new \WP_Error('aksrt_ref_missing', __('该用户尚未生成推广码', 'aksrt'), ['status' => 409]);
}
$code = $this->generate_code($user_id, true);
} else {
return new \WP_Error('aksrt_ref_action', __('不支持的推广码操作', 'aksrt'), ['status' => 400]);
}
if ($code === '') {
return new \WP_Error('aksrt_ref_generate_failed', __('推广码生成失败', 'aksrt'), ['status' => 500]);
}
return rest_ensure_response([
'user_id' => $user_id,
'ref' => $code,
'status' => $action === 'disable' ? self::STATUS_DISABLED : self::STATUS_ACTIVE,
]);
}
private function list_codes(array $args): array {
global $wpdb;
$page = max(1, (int) ($args['page'] ?? 1));
$per_page = max(1, min(100, (int) ($args['per_page'] ?? 20)));
$status = (string) ($args['status'] ?? 'all');
$search = trim((string) ($args['search'] ?? ''));
$where = ['1=1'];
$params = [];
if ($search !== '') {
$like = '%' . $wpdb->esc_like($search) . '%';
$where[] = '(u.user_login LIKE %s OR u.display_name LIKE %s OR u.user_email LIKE %s OR code.meta_value LIKE %s)';
array_push($params, $like, $like, $like, $like);
}
if ($status === 'missing') {
$where[] = "(code.meta_value IS NULL OR code.meta_value = '')";
} elseif ($status === self::STATUS_DISABLED) {
$where[] = "code.meta_value <> '' AND state.meta_value = 'disabled'";
} elseif ($status === self::STATUS_ACTIVE) {
$where[] = "code.meta_value <> '' AND (state.meta_value IS NULL OR state.meta_value <> 'disabled')";
}
$where_sql = implode(' AND ', $where);
$joins = $wpdb->prepare(
"LEFT JOIN {$wpdb->usermeta} code ON code.user_id=u.ID AND code.meta_key=%s LEFT JOIN {$wpdb->usermeta} state ON state.user_id=u.ID AND state.meta_key=%s",
self::META_CODE,
self::META_STATUS
);
$count_sql = "SELECT COUNT(DISTINCT u.ID) FROM {$wpdb->users} u {$joins} WHERE {$where_sql}";
$total = (int) ($params
? $wpdb->get_var($wpdb->prepare($count_sql, $params))
: $wpdb->get_var($count_sql));
$relation_table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
$ledger_table = \AKSRT\Core\DBMigrator::table('commission_ledger');
$stats_joins = "LEFT JOIN (SELECT promoter_id, COUNT(*) AS bound_count FROM {$relation_table} WHERE status='bound' GROUP BY promoter_id) relation_stats ON relation_stats.promoter_id=u.ID "
. "LEFT JOIN (SELECT promoter_id, COUNT(DISTINCT source_user_id) AS effective_payers FROM {$ledger_table} WHERE original_amount > 0 GROUP BY promoter_id) payer_stats ON payer_stats.promoter_id=u.ID";
$list_sql = "SELECT DISTINCT u.ID, u.user_login, u.display_name, u.user_email, code.meta_value AS ref, state.meta_value AS ref_status, "
. "COALESCE(relation_stats.bound_count, 0) AS bound_count, COALESCE(payer_stats.effective_payers, 0) AS effective_payers "
. "FROM {$wpdb->users} u {$joins} {$stats_joins} WHERE {$where_sql} ORDER BY u.ID DESC LIMIT %d OFFSET %d";
$list_params = array_merge($params, [$per_page, ($page - 1) * $per_page]);
$rows = $wpdb->get_results($wpdb->prepare($list_sql, $list_params), ARRAY_A);
$items = [];
foreach ((array) $rows as $row) {
$user_id = (int) $row['ID'];
$ref = sanitize_key((string) ($row['ref'] ?? ''));
$items[] = [
'user' => [
'id' => $user_id,
'login' => (string) $row['user_login'],
'name' => (string) $row['display_name'],
'email' => (string) $row['user_email'],
],
'ref' => $ref,
'status' => $ref === '' ? 'missing' : ((string) ($row['ref_status'] ?? '') === self::STATUS_DISABLED ? self::STATUS_DISABLED : self::STATUS_ACTIVE),
'bound_count' => (int) ($row['bound_count'] ?? 0),
'effective_payers' => (int) ($row['effective_payers'] ?? 0),
];
}
return [
'items' => $items,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'max_pages' => max(1, (int) ceil($total / $per_page)),
];
}
private function generate_code(int $user_id, bool $force): string {
if ($user_id <= 0) {
return '';
}
$record = $this->code_record($user_id);
if (!$force && $record['status'] !== 'missing') {
return $record['status'] === self::STATUS_ACTIVE ? $record['code'] : '';
}
$length = (int) ($this->shared_settings()['code_length'] ?? 12);
$alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
for ($attempt = 0; $attempt < 20; $attempt++) {
$code = '';
for ($i = 0; $i < $length; $i++) {
$code .= $alphabet[random_int(0, strlen($alphabet) - 1)];
}
$owners = get_users([
'meta_key' => self::META_CODE,
'meta_value' => $code,
'number' => 1,
'fields' => 'ID',
]);
if (empty($owners)) {
update_user_meta($user_id, self::META_CODE, $code);
update_user_meta($user_id, self::META_STATUS, self::STATUS_ACTIVE);
$updated = $this->code_record($user_id);
if ($updated['code'] === $code && $updated['status'] === self::STATUS_ACTIVE) {
return $code;
}
}
}
return '';
}
/** @return array{code:string,status:string} */
private function code_record(int $user_id): array {
$code = sanitize_key((string) get_user_meta($user_id, self::META_CODE, true));
if ($code === '') {
return ['code' => '', 'status' => 'missing'];
}
$status = (string) get_user_meta($user_id, self::META_STATUS, true);
return [
'code' => $code,
'status' => $status === self::STATUS_DISABLED ? self::STATUS_DISABLED : self::STATUS_ACTIVE,
];
}
private function resolve_promoter(string $code): int {
$code = sanitize_key($code);
if ($code === '') {
return 0;
}
$users = get_users(['meta_key' => self::META_CODE, 'meta_value' => $code, 'number' => 1, 'fields' => 'ID']);
return !empty($users) ? (int) $users[0] : 0;
$users = get_users([
'meta_key' => self::META_CODE,
'meta_value' => $code,
'number' => 1,
'fields' => 'ID',
]);
if (empty($users)) {
return 0;
}
$user_id = (int) $users[0];
return $this->code_record($user_id)['status'] === self::STATUS_ACTIVE ? $user_id : 0;
}
private function cookie_ref(): string {
$code = isset($_COOKIE[self::COOKIE]) ? sanitize_key((string) wp_unslash($_COOKIE[self::COOKIE])) : '';
return $code !== '' && $this->resolve_promoter($code) > 0 ? $code : '';
}
private function clear_cookie(): void {
if (!headers_sent()) {
setcookie(self::COOKIE, '', time() - HOUR_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN);
}
unset($_COOKIE[self::COOKIE]);
}
private function feature_enabled(): bool {
$registration = $this->registration_settings();
$reward = $this->reward_settings();
$affiliate = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::affiliate_enabled();
return !empty($registration['enabled']) || !empty($reward['enabled']) || $affiliate;
}
private function shared_settings(): array {
return class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_referral_settings()
: ['code_length' => 12, 'window_days' => 30];
}
private function registration_settings(): array {
return class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_referral_registration_settings()
: ['enabled' => false, 'register_field' => 'hidden', 'reject_invalid' => true];
}
private function reward_settings(): array {
return class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_referral_reward_settings()
: ['enabled' => false];
}
private function is_verified(int $user_id): bool {
return (int) get_user_meta($user_id, 'aksrt_email_verified', true) === 1;
}
private function ip_daily_check(array $relation, array $settings): bool {
$cap = max(0, (int) ($settings['ip_daily_cap'] ?? 0));
$ip = (string) ($relation['registration_ip'] ?? '');
if ($cap <= 0 || $ip === '') {
return true;
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
$today_utc = (new \DateTimeImmutable('today', wp_timezone()))
->setTimezone(new \DateTimeZone('UTC'))
->format('Y-m-d H:i:s');
$count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE registration_ip = %s AND reward_status = 'rewarded' AND rewarded_at >= %s FOR UPDATE",
$ip,
$today_utc
));
return $count < $cap;
}
private function client_ip(): string {
return function_exists('aksrt_get_client_ip')
? substr((string) aksrt_get_client_ip(), 0, 64)
: '';
}
}
+27 -56
View File
@@ -26,39 +26,19 @@ trait CreditsPaymentGroups {
return CreditsRuntime::get_signin_reward_settings();
}
/**
* Normalized runtime settings for invite rewards and registration gating.
*
* @return array{
* enabled:bool,
* inviter_amount:int,
* invitee_amount:int,
* require_verify:bool,
* ip_daily_cap:int,
* register_field:string,
* reject_invalid:bool,
* registration_enabled:bool
* }
*/
public static function get_invite_reward_settings(): array {
return CreditsRuntime::get_invite_reward_settings();
/** @return array{code_length:int,window_days:int} */
public static function get_referral_settings(): array {
return CreditsRuntime::get_referral_settings();
}
/**
* Canonical invite registration config derived from users.invite fields.
*
* @return array{
* enabled:bool,
* mode:string,
* register_field:string,
* reject_invalid:bool,
* code_length:int,
* open_desc:string,
* must_desc:string
* }
*/
public static function get_invite_registration_config(): array {
return CreditsRuntime::get_invite_registration_config();
/** @return array<string,mixed> */
public static function get_referral_reward_settings(): array {
return CreditsRuntime::get_referral_reward_settings();
}
/** @return array<string,mixed> */
public static function get_referral_registration_settings(): array {
return CreditsRuntime::get_referral_registration_settings();
}
/**
@@ -75,39 +55,30 @@ trait CreditsPaymentGroups {
return CreditsRuntime::get_currency_name();
}
/**
* 邀请系统是否启用(=积分系统启用 && invite_reward.enabled)。
* 用于 UI 入口(个人中心邀请 Tab)的展示门禁。
*/
public static function invite_enabled(): bool {
return CreditsRuntime::invite_enabled();
public static function referral_reward_enabled(): bool {
return CreditsRuntime::referral_reward_enabled();
}
/**
* 邀请注册功能总开关(独立于积分系统)。
* 控制邀请链接、注册页邀请码字段、用户中心邀请卡片的显示。
* 存储在 aksrt_settings(invit_code_s / invite_registration_enabled)。
*/
public static function invite_registration_enabled(): bool {
return CreditsRuntime::invite_registration_enabled();
public static function referral_registration_enabled(): bool {
return CreditsRuntime::referral_registration_enabled();
}
/**
* 注册页邀请码字段模式:'hidden' | 'optional' | 'required'
*/
public static function invite_register_field_mode(): string {
return CreditsRuntime::invite_register_field_mode();
public static function referral_register_field_mode(): string {
return CreditsRuntime::referral_register_field_mode();
}
/**
* 必填模式下邀请码无效是否拒绝注册。
*/
public static function invite_reject_invalid(): bool {
return CreditsRuntime::invite_reject_invalid();
public static function referral_reject_invalid(): bool {
return CreditsRuntime::referral_reject_invalid();
}
public static function invite_code_length(): int {
return CreditsRuntime::invite_code_length();
public static function referral_code_length(): int {
return CreditsRuntime::referral_code_length();
}
public static function referral_enabled(): bool {
return self::referral_registration_enabled()
|| self::referral_reward_enabled()
|| PaymentRuntime::affiliate_enabled();
}
/**
+63 -129
View File
@@ -38,14 +38,13 @@ class CreditsRuntime {
'on_approved' => true,
],
'invite_reward' => [
'referral_reward' => [
'enabled' => true,
'inviter_amount' => 5,
'invitee_amount' => 2,
'promoter_amount' => 5,
'referred_amount' => 2,
'require_verify' => true,
'ip_daily_cap' => 3,
'register_field' => 'optional',
'reject_invalid' => true,
'level_points' => 0,
],
'download_default_cost' => 0,
@@ -54,6 +53,9 @@ class CreditsRuntime {
if (!is_array($saved)) {
$saved = [];
}
// Superseded top-level keys remain physically stored but are not exposed
// to runtime consumers or written back by current admin clients.
$saved = array_intersect_key($saved, $defaults);
return array_replace_recursive($defaults, $saved);
}
@@ -87,119 +89,60 @@ class CreditsRuntime {
}
/**
* Normalized runtime settings for invite rewards and registration gating.
* Shared ref-code settings.
*
* @return array{
* enabled:bool,
* inviter_amount:int,
* invitee_amount:int,
* require_verify:bool,
* ip_daily_cap:int,
* register_field:string,
* reject_invalid:bool,
* registration_enabled:bool
* }
* @return array{code_length:int,window_days:int}
*/
public static function get_invite_reward_settings(): array {
$config = self::get_credit_settings();
$invite = is_array($config['invite_reward'] ?? null) ? $config['invite_reward'] : [];
public static function get_referral_settings(): array {
$all = Settings::get_all();
$register_field = (string) ($invite['register_field'] ?? 'optional');
if (!in_array($register_field, ['hidden', 'optional', 'required'], true)) {
$register_field = 'optional';
}
$inviter_amount = array_key_exists('invit_reward_points', $all)
? max(0, (int) $all['invit_reward_points'])
: max(0, (int) ($invite['inviter_amount'] ?? 0));
$invitee_enabled = array_key_exists('invit_invitee_reward', $all)
? !empty($all['invit_invitee_reward'])
: max(0, (int) ($invite['invitee_amount'] ?? 0)) > 0;
$invitee_amount = $invitee_enabled
? max(0, (int) ($invite['invitee_amount'] ?? $inviter_amount))
: 0;
return [
'enabled' => !empty($config['enabled']) && !empty($invite['enabled']),
'inviter_amount' => $inviter_amount,
'invitee_amount' => $invitee_amount,
'require_verify' => !empty($invite['require_verify']),
'ip_daily_cap' => max(0, (int) ($invite['ip_daily_cap'] ?? 0)),
'register_field' => $register_field,
'reject_invalid' => !empty($invite['reject_invalid']),
'registration_enabled' => self::invite_registration_enabled(),
'invit_reward_points' => $inviter_amount,
'invit_reward_level' => max(0, (int) ($all['invit_reward_level'] ?? 0)),
'invit_reward_vip_days' => max(0, (int) ($all['invit_reward_vip_days'] ?? 0)),
'invit_invitee_reward' => $invitee_enabled,
'code_length' => max(4, min(12, (int) ($all['referral_code_length'] ?? 12))),
'window_days' => max(1, min(365, (int) ($all['referral_window_days'] ?? 30))),
];
}
/**
* Canonical invite registration config derived from users.invite fields.
*
* @return array{
* enabled:bool,
* mode:string,
* register_field:string,
* reject_invalid:bool,
* code_length:int,
* open_desc:string,
* must_desc:string
* }
* @return array{enabled:bool,promoter_amount:int,referred_amount:int,require_verify:bool,ip_daily_cap:int,level_points:int}
*/
public static function get_invite_registration_config(): array {
$all = Settings::get_all();
$credit_config = self::get_credit_settings();
$enabled = array_key_exists('invit_code_s', $all)
? !empty($all['invit_code_s'])
: (!isset($all['invite_registration_enabled']) || !empty($all['invite_registration_enabled']));
$mode = isset($all['invit_code_mode']) ? (string) $all['invit_code_mode'] : '';
if (!in_array($mode, ['open', 'must', 'close'], true)) {
$legacy_mode = (string) ($credit_config['invite_reward']['register_field'] ?? 'optional');
if ($legacy_mode === 'required') {
$mode = 'must';
} elseif ($legacy_mode === 'hidden') {
$mode = 'close';
} else {
$mode = 'open';
}
}
$register_field = 'optional';
if (!$enabled || $mode === 'close') {
$register_field = 'hidden';
} elseif ($mode === 'must') {
$register_field = 'required';
}
$open_desc = trim((string) ($all['invit_code_open_desc'] ?? ''));
if ($open_desc === '') {
$open_desc = __('邀请码为选填,没有邀请码也可以注册', 'aksrt');
}
$must_desc = trim((string) ($all['invit_code_must_desc'] ?? ''));
if ($must_desc === '') {
$must_desc = __('请输入邀请码', 'aksrt');
}
$code_length = isset($all['invit_code_length']) ? (int) $all['invit_code_length'] : 0;
if ($code_length <= 0) {
$code_length = (int) ($all['invite_code_length'] ?? 6);
}
$code_length = max(4, min(12, $code_length));
public static function get_referral_reward_settings(): array {
$config = self::get_credit_settings();
$reward = is_array($config['referral_reward'] ?? null) ? $config['referral_reward'] : [];
return [
'enabled' => $enabled && $mode !== 'close',
'mode' => $mode,
'enabled' => !empty($config['enabled']) && !empty($reward['enabled']),
'promoter_amount' => max(0, (int) ($reward['promoter_amount'] ?? 0)),
'referred_amount' => max(0, (int) ($reward['referred_amount'] ?? 0)),
'require_verify' => !empty($reward['require_verify']),
'ip_daily_cap' => max(0, (int) ($reward['ip_daily_cap'] ?? 0)),
'level_points' => max(0, (int) ($reward['level_points'] ?? 0)),
];
}
/**
* @return array{enabled:bool,register_field:string,reject_invalid:bool,open_desc:string,required_desc:string}
*/
public static function get_referral_registration_settings(): array {
$all = Settings::get_all();
$enabled = !isset($all['referral_registration_enabled']) || !empty($all['referral_registration_enabled']);
$register_field = sanitize_key((string) ($all['referral_register_field'] ?? 'optional'));
if (!in_array($register_field, ['hidden', 'optional', 'required'], true)) {
$register_field = 'optional';
}
$open_desc = trim((string) ($all['referral_open_desc'] ?? ''));
if ($open_desc === '') {
$open_desc = __('推广码为选填,没有推广码也可以注册', 'aksrt');
}
$required_desc = trim((string) ($all['referral_required_desc'] ?? ''));
if ($required_desc === '') {
$required_desc = __('请输入推广码', 'aksrt');
}
return [
'enabled' => $enabled,
'register_field' => $register_field,
'reject_invalid' => !empty($credit_config['invite_reward']['reject_invalid']),
'code_length' => $code_length,
'reject_invalid' => !isset($all['referral_reject_invalid']) || !empty($all['referral_reject_invalid']),
'open_desc' => $open_desc,
'must_desc' => $must_desc,
'required_desc' => $required_desc,
];
}
@@ -222,40 +165,31 @@ class CreditsRuntime {
}
/**
* 邀请系统是否启用(=积分系统启用 && invite_reward.enabled)。
* 用于 UI 入口(个人中心邀请 Tab)的展示门禁。
* Ref 积分奖励是否启用。
*/
public static function invite_enabled(): bool {
$config = self::get_credit_settings();
return !empty($config['enabled']) && !empty($config['invite_reward']['enabled']);
public static function referral_reward_enabled(): bool {
return !empty(self::get_referral_reward_settings()['enabled']);
}
/**
* 邀请注册功能总开关(独立于积分系统)。
* 控制邀请链接、注册页邀请码字段、用户中心邀请卡片的显示。
* 存储在 aksrt_settings(invit_code_s / invite_registration_enabled)。
*/
public static function invite_registration_enabled(): bool {
return !empty(self::get_invite_registration_config()['enabled']);
public static function referral_registration_enabled(): bool {
return !empty(self::get_referral_registration_settings()['enabled']);
}
/**
* 注册页邀请码字段模式:'hidden' | 'optional' | 'required'
*/
public static function invite_register_field_mode(): string {
$mode = (string) (self::get_invite_registration_config()['register_field'] ?? 'hidden');
public static function referral_register_field_mode(): string {
$registration = self::get_referral_registration_settings();
if (empty($registration['enabled'])) {
return 'hidden';
}
$mode = (string) ($registration['register_field'] ?? 'hidden');
return in_array($mode, ['hidden', 'optional', 'required'], true) ? $mode : 'hidden';
}
/**
* 必填模式下邀请码无效是否拒绝注册。
*/
public static function invite_reject_invalid(): bool {
return !empty(self::get_invite_registration_config()['reject_invalid']);
public static function referral_reject_invalid(): bool {
return !empty(self::get_referral_registration_settings()['reject_invalid']);
}
public static function invite_code_length(): int {
return max(4, min(12, (int) (self::get_invite_registration_config()['code_length'] ?? 6)));
public static function referral_code_length(): int {
return (int) self::get_referral_settings()['code_length'];
}
/**
+1 -1
View File
@@ -294,7 +294,7 @@ class PaymentRuntime {
'vip_2' => $clamp_ratio($settings['affiliate_ratio_vip_2'] ?? 5),
],
'order_types' => $types,
'window_days' => max(1, min(365, (int) ($settings['affiliate_touch_window_days'] ?? 30))),
'window_days' => (int) CreditsRuntime::get_referral_settings()['window_days'],
'settlement_days' => max(0, min(90, (int) ($settings['affiliate_settlement_days'] ?? 7))),
'promotion_text' => sanitize_textarea_field((string) ($settings['affiliate_promotion_text'] ?? '')),
];
+1 -1
View File
@@ -219,7 +219,7 @@ class Center {
$credits_on = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled();
$balance_on = class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled();
$revenue_tab_on = class_exists('\AKSRT\Settings\Settings')
&& !empty(\AKSRT\Settings\Settings::get_affiliate_settings()['enabled']);
&& \AKSRT\Settings\Settings::referral_enabled();
$creator_tab_on = class_exists('\AKSRT\Settings\Settings')
&& !empty(\AKSRT\Settings\Settings::get_payment_revenue_settings()['enabled']);
$vip_on = class_exists('\AKSRT\Settings\Settings')
+2 -2
View File
@@ -226,8 +226,8 @@ class CreditsAdmin {
'register' => __('注册赠送', 'aksrt'),
'signin' => __('每日签到', 'aksrt'),
'comment' => __('评论奖励', 'aksrt'),
'invite' => __('邀请奖励', 'aksrt'),
'invitee' => __('被邀请奖励', 'aksrt'),
'referral' => __('推广注册奖励', 'aksrt'),
'referral_new_user' => __('推广注册新用户奖励', 'aksrt'),
'download' => __('下载消费', 'aksrt'),
'admin' => __('管理员调整', 'aksrt'),
];
+1 -1
View File
@@ -88,7 +88,7 @@ class Level {
'sign_up' => __('首次注册', 'aksrt'),
'sign_in' => __('每日登录', 'aksrt'),
'checkin' => __('每日签到', 'aksrt'),
'invitation' => __('邀请奖励', 'aksrt'),
'referral' => __('推广奖励', 'aksrt'),
'shop_gift' => __('购物赠送', 'aksrt'),
'followed' => __('被关注', 'aksrt'),
'post_new' => __('发布文章', 'aksrt'),
+19 -17
View File
@@ -102,6 +102,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['user_login'], $_POST[
if (class_exists('\AKSRT\Auth\Service')) {
$result = \AKSRT\Auth\Service::register_user($_POST, ['oauth_bind_data' => null, 'obk' => '']);
if (!empty($result['success'])) {
$ref_notice = trim((string) ($result['notice'] ?? ''));
if (!empty($result['verification_required'])) {
$register_success = sprintf(
'%s <a href="%s">%s</a>',
@@ -115,19 +116,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['user_login'], $_POST[
esc_url(aksrt_get_login_url())
);
}
if ($ref_notice !== '') {
$register_success .= ' ' . esc_html($ref_notice);
}
} else {
$register_error = $result['error'] ?? __('注册失败', 'aksrt');
}
}
}
$invite_prefill = '';
if (!empty($_GET['ref'])) {
$invite_prefill = sanitize_key(wp_unslash($_GET['ref']));
} elseif (!empty($_GET['invite'])) {
$invite_prefill = sanitize_key(wp_unslash($_GET['invite']));
} elseif (!empty($_COOKIE['aksrt_invite'])) {
$invite_prefill = sanitize_key(wp_unslash($_COOKIE['aksrt_invite']));
$ref_prefill = '';
if (!empty($_COOKIE['aksrt_referral'])) {
$ref_prefill = sanitize_key(wp_unslash($_COOKIE['aksrt_referral']));
} elseif (!empty($_GET['ref'])) {
$ref_prefill = sanitize_key(wp_unslash($_GET['ref']));
}
$agreement = function_exists('aksrt_get_auth_agreement_view')
@@ -246,18 +248,18 @@ $sign_css_ver = file_exists(get_template_directory() . '/assets/css/routes/sign.
</div>
</div>
<?php
$invite_cfg = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_invite_registration_config() : null;
if ($invite_cfg && ($invite_cfg['register_field'] ?? 'hidden') !== 'hidden') {
$mode = (string) $invite_cfg['register_field'];
$invit_hint = $mode === 'required'
? (string) ($invite_cfg['must_desc'] ?? '')
: (string) ($invite_cfg['open_desc'] ?? '');
$ref_cfg = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_referral_registration_settings() : null;
if ($ref_cfg && !empty($ref_cfg['enabled']) && ($ref_cfg['register_field'] ?? 'hidden') !== 'hidden') {
$mode = (string) $ref_cfg['register_field'];
$ref_hint = $mode === 'required'
? (string) ($ref_cfg['required_desc'] ?? '')
: (string) ($ref_cfg['open_desc'] ?? '');
?>
<div class="form-field">
<label for="sign-invite"><?php esc_html_e('邀请码', 'aksrt'); ?><?php echo $mode === 'optional' ? ' <small style="color:var(--muted-color);">(' . esc_html__('选填', 'aksrt') . ')</small>' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></label>
<div class="input-wrap"><input type="text" name="invite_code" id="sign-invite" maxlength="32" autocomplete="off" value="<?php echo esc_attr($invite_prefill); ?>" <?php echo $mode === 'required' ? 'required' : ''; ?>></div>
<?php if ($invit_hint !== '') : ?>
<p class="sign-hint"><?php echo wp_kses_post($invit_hint); ?></p>
<label for="sign-ref"><?php esc_html_e('推广码', 'aksrt'); ?><?php echo $mode === 'optional' ? ' <small style="color:var(--muted-color);">(' . esc_html__('选填', 'aksrt') . ')</small>' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></label>
<div class="input-wrap"><input type="text" name="ref" id="sign-ref" maxlength="12" autocomplete="off" value="<?php echo esc_attr($ref_prefill); ?>" <?php echo $mode === 'required' ? 'required' : ''; ?>></div>
<?php if ($ref_hint !== '') : ?>
<p class="sign-hint"><?php echo wp_kses_post($ref_hint); ?></p>
<?php endif; ?>
</div>
<?php } ?>
+19 -21
View File
@@ -19,9 +19,9 @@ if (class_exists('\AKSRT\Auth\OAuth')) {
$registration_open = true;
$email_verification = false;
$close_signin = false;
$invite_field_mode = 'hidden';
$invite_field_hint = '';
$invite_prefill = '';
$ref_field_mode = 'hidden';
$ref_field_hint = '';
$ref_prefill = '';
$show_qrcode_tab = false;
if (class_exists('\AKSRT\Settings\Settings')) {
@@ -32,19 +32,17 @@ if (class_exists('\AKSRT\Settings\Settings')) {
$email_verification = \AKSRT\Settings\Settings::registration_requires_email_verification();
$close_signin = \AKSRT\Settings\Settings::close_signin();
$invite_cfg = \AKSRT\Settings\Settings::get_invite_registration_config();
$invite_field_mode = (string) ($invite_cfg['register_field'] ?? 'hidden');
$invite_field_hint = $invite_field_mode === 'required'
? (string) ($invite_cfg['must_desc'] ?? '')
: (string) ($invite_cfg['open_desc'] ?? '');
$ref_cfg = \AKSRT\Settings\Settings::get_referral_registration_settings();
$ref_field_mode = !empty($ref_cfg['enabled']) ? (string) ($ref_cfg['register_field'] ?? 'hidden') : 'hidden';
$ref_field_hint = $ref_field_mode === 'required'
? (string) ($ref_cfg['required_desc'] ?? '')
: (string) ($ref_cfg['open_desc'] ?? '');
}
if (!empty($_GET['ref'])) {
$invite_prefill = sanitize_key(wp_unslash($_GET['ref']));
} elseif (!empty($_GET['invite'])) {
$invite_prefill = sanitize_key(wp_unslash($_GET['invite']));
} elseif (!empty($_COOKIE['aksrt_invite'])) {
$invite_prefill = sanitize_key(wp_unslash($_COOKIE['aksrt_invite']));
if (!empty($_COOKIE['aksrt_referral'])) {
$ref_prefill = sanitize_key(wp_unslash($_COOKIE['aksrt_referral']));
} elseif (!empty($_GET['ref'])) {
$ref_prefill = sanitize_key(wp_unslash($_GET['ref']));
}
foreach ($oauth_providers as $provider) {
@@ -171,17 +169,17 @@ $logo_url = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings:
</div>
</div>
<?php if ($invite_field_mode !== 'hidden') : ?>
<?php if ($ref_field_mode !== 'hidden') : ?>
<div class="aksrt-modal-auth__field">
<label for="modal-reg-invite">
<?php esc_html_e('邀请码', 'aksrt'); ?>
<?php if ($invite_field_mode === 'optional') : ?>
<label for="modal-reg-ref">
<?php esc_html_e('推广码', 'aksrt'); ?>
<?php if ($ref_field_mode === 'optional') : ?>
<small><?php esc_html_e('(选填)', 'aksrt'); ?></small>
<?php endif; ?>
</label>
<input type="text" id="modal-reg-invite" name="invite_code" value="<?php echo esc_attr($invite_prefill); ?>" <?php echo $invite_field_mode === 'required' ? 'required' : ''; ?> placeholder="<?php esc_attr_e('请输入邀请码', 'aksrt'); ?>">
<?php if ($invite_field_hint !== '') : ?>
<p class="aksrt-modal-auth__hint"><?php echo wp_kses_post($invite_field_hint); ?></p>
<input type="text" id="modal-reg-ref" name="ref" maxlength="12" value="<?php echo esc_attr($ref_prefill); ?>" <?php echo $ref_field_mode === 'required' ? 'required' : ''; ?> placeholder="<?php esc_attr_e('请输入推广码', 'aksrt'); ?>">
<?php if ($ref_field_hint !== '') : ?>
<p class="aksrt-modal-auth__hint"><?php echo wp_kses_post($ref_field_hint); ?></p>
<?php endif; ?>
</div>
<?php endif; ?>