feat: unify invite flow with referral ref #3
@@ -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',
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
<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([
|
||||
|
||||
@@ -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 } },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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') : ?>
|
||||
|
||||
@@ -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
@@ -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
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
@@ -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) => ({
|
||||
'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'
|
||||
})[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
|
||||
}
|
||||
}
|
||||
@@ -610,7 +610,6 @@ class PaymentController extends ControllerBase {
|
||||
'types' => [
|
||||
['label' => __('全部类型', 'aksrt'), 'value' => ''],
|
||||
['label' => __('订单返佣', 'aksrt'), 'value' => 'order'],
|
||||
['label' => __('邀请返佣', 'aksrt'), 'value' => 'invite'],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -763,8 +762,7 @@ class PaymentController extends ControllerBase {
|
||||
'cancelled' => __('已取消', 'aksrt'),
|
||||
];
|
||||
$typeMap = [
|
||||
'order' => __('订单返佣', 'aksrt'),
|
||||
'invite' => __('邀请返佣', 'aksrt'),
|
||||
'order' => __('订单返佣', 'aksrt'),
|
||||
];
|
||||
|
||||
return [
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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' => '用户功能',
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 在所有类加载完成后调用一次。
|
||||
|
||||
+36
-44
@@ -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()
|
||||
: 'hidden';
|
||||
$invite_code = isset($post['invite_code']) ? sanitize_key((string) wp_unslash($post['invite_code'])) : '';
|
||||
$resolved_inviter_id = 0;
|
||||
$invite_is_valid = false;
|
||||
|
||||
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);
|
||||
}
|
||||
// 必填模式下,邀请码无效时根据后台开关决定是否拒绝
|
||||
$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')];
|
||||
}
|
||||
} elseif ($invite_mode === 'required') {
|
||||
return ['success' => false, 'error' => __('请填写邀请码', 'aksrt')];
|
||||
$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';
|
||||
$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'];
|
||||
|
||||
// 把解析到的 inviter_id 暂存到静态字段,user_register hook 内部读取(优先于 cookie)
|
||||
self::$pending_invite_inviter_id = $resolved_inviter_id;
|
||||
self::$pending_invite_code = $invite_code;
|
||||
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($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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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() : '',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,67 +67,215 @@ 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');
|
||||
}
|
||||
|
||||
$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'),
|
||||
];
|
||||
}
|
||||
|
||||
public function get_or_create_code(int $user_id): string {
|
||||
if ($user_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
$existing = sanitize_key((string) get_user_meta($user_id, self::META_CODE, true));
|
||||
if ($existing !== '' && strlen($existing) >= 10) {
|
||||
return $existing;
|
||||
}
|
||||
$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 clear_registration_context(): void {
|
||||
self::$pending_registration_ref = '';
|
||||
}
|
||||
|
||||
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,
|
||||
'promoter_id' => $promoter_id,
|
||||
'invitee_id' => $invitee_id,
|
||||
'first_touch_at'=> $now,
|
||||
'bound_at' => $now,
|
||||
'status' => 'bound',
|
||||
'created_at' => $now,
|
||||
], ['%s', '%d', '%d', '%s', '%s', '%s', '%s']);
|
||||
if (!headers_sent()) {
|
||||
setcookie(self::COOKIE, '', time() - HOUR_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN);
|
||||
$inserted = $wpdb->insert($table, [
|
||||
'referral_code' => $ref,
|
||||
'promoter_id' => $promoter_id,
|
||||
'invitee_id' => $invitee_id,
|
||||
'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', '%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();
|
||||
if ($user_id <= 0) {
|
||||
@@ -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)
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+66
-132
@@ -38,14 +38,13 @@ class CreditsRuntime {
|
||||
'on_approved' => true,
|
||||
],
|
||||
|
||||
'invite_reward' => [
|
||||
'enabled' => true,
|
||||
'inviter_amount' => 5,
|
||||
'invitee_amount' => 2,
|
||||
'require_verify' => true,
|
||||
'ip_daily_cap' => 3,
|
||||
'register_field' => 'optional',
|
||||
'reject_invalid' => true,
|
||||
'referral_reward' => [
|
||||
'enabled' => true,
|
||||
'promoter_amount' => 5,
|
||||
'referred_amount' => 2,
|
||||
'require_verify' => true,
|
||||
'ip_daily_cap' => 3,
|
||||
'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'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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
@@ -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'),
|
||||
|
||||
@@ -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,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; ?>
|
||||
|
||||
Reference in New Issue
Block a user