feat: unify invite flow with referral ref
This commit is contained in:
+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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user