feat: unify invite flow with referral ref

This commit is contained in:
2026-08-17 13:49:59 +08:00
parent f1d6809109
commit b501cf4611
47 changed files with 1083 additions and 2548 deletions
-1
View File
@@ -352,7 +352,6 @@ class ImportExportController extends ControllerBase {
$expected_tables = [
$wpdb->prefix . 'aksrt_credit_log',
$wpdb->prefix . 'aksrt_signin_log',
$wpdb->prefix . 'aksrt_invite',
$wpdb->prefix . 'aksrt_dl_token',
$wpdb->prefix . 'aksrt_notification',
];
+12 -217
View File
@@ -11,6 +11,7 @@ class UsersMenus {
use UsersMenusVerify;
use UsersMenusVip;
use UsersMenusReferral;
public const CAPABILITY = 'manage_options';
@@ -37,12 +38,21 @@ class UsersMenus {
add_action('admin_post_aksrt_users_add_restriction', [$this, 'handle_add_restriction']);
add_action('admin_post_aksrt_users_remove_restriction', [$this, 'handle_remove_restriction']);
add_action('admin_post_aksrt_users_process_appeal', [$this, 'handle_process_appeal']);
add_action('admin_post_aksrt_users_batch_invite_codes', [$this, 'handle_batch_invite_codes']);
add_action('admin_post_aksrt_users_disable_invite_code', [$this, 'handle_disable_invite_code']);
add_action('admin_post_aksrt_users_mutate_vip', [$this, 'handle_mutate_vip']);
}
public function register_menus(): void {
if (current_user_can('manage_users')) {
add_submenu_page(
'users.php',
__('推广码运营', 'aksrt'),
__('推广码运营', 'aksrt'),
'manage_users',
'aksrt_referral_codes',
[$this, 'render_referral_codes_page']
);
}
if (!current_user_can(self::CAPABILITY)) {
return;
}
@@ -55,14 +65,6 @@ class UsersMenus {
'aksrt_messages',
[$this, 'render_messages_page']
);
add_submenu_page(
'users.php',
__('邀请码管理', 'aksrt'),
__('邀请码管理', 'aksrt'),
self::CAPABILITY,
'aksrt_invite_codes',
[$this, 'render_invite_codes_page']
);
add_submenu_page(
'users.php',
__('用户会员', 'aksrt'),
@@ -164,10 +166,6 @@ class UsersMenus {
'appeal_approved' => __('申诉已通过,限制已解除。', 'aksrt'),
'appeal_rejected' => __('申诉已驳回。', 'aksrt'),
'appeal_failed' => __('申诉处理失败。', 'aksrt'),
'invite_batch_ok' => __('邀请码批量生成完成。', 'aksrt'),
'invite_batch_failed' => __('邀请码批量生成失败。', 'aksrt'),
'invite_disabled' => __('邀请码已禁用。', 'aksrt'),
'invite_failed' => __('邀请码操作失败。', 'aksrt'),
'vip_updated' => __('会员状态已更新。', 'aksrt'),
'vip_partial' => __('部分用户的会员状态已更新。', 'aksrt'),
'vip_failed' => __('会员操作失败。', 'aksrt'),
@@ -281,209 +279,6 @@ class UsersMenus {
$this->redirect('aksrt_messages', $ok ? 'message_sent' : 'message_failed');
}
/* ------------------------------------------------------------------
* Invite codes
* ----------------------------------------------------------------*/
public function render_invite_codes_page(): void {
$this->ensure_cap();
echo '<div class="wrap"><h1>' . esc_html__('邀请码管理', 'aksrt') . '</h1>';
$this->flash();
echo '<h2>' . esc_html__('批量生成独立邀请码', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:12px 0 20px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
echo '<input type="hidden" name="action" value="aksrt_users_batch_invite_codes" />';
wp_nonce_field('aksrt_users_batch_invite_codes');
echo '<p><label>' . esc_html__('数量', 'aksrt') . '<br><input type="number" name="count" min="1" max="100" value="10" /></label></p>';
echo '<p><label>' . esc_html__('长度', 'aksrt') . '<br><input type="number" name="length" min="4" max="12" value="6" /></label></p>';
echo '<p><label>' . esc_html__('前缀', 'aksrt') . '<br><input type="text" name="prefix" value="" /></label></p>';
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" value="" class="regular-text" /></label></p>';
submit_button(__('生成', 'aksrt'), 'primary', '', false);
echo '</form>';
if (!empty($_GET['aksrt_generated'])) {
echo '<div class="notice notice-success"><p>' . esc_html(sprintf(
/* translators: %d count */
__('成功生成 %d 个邀请码。', 'aksrt'),
(int) $_GET['aksrt_generated']
)) . '</p></div>';
}
$standalone = get_option('aksrt_standalone_invite_codes', []);
$standalone = is_array($standalone) ? array_reverse(array_values($standalone)) : [];
echo '<h2>' . esc_html__('独立邀请码', 'aksrt') . '</h2>';
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>' . esc_html__('邀请码', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('批次', 'aksrt') . '</th><th>' . esc_html__('备注', 'aksrt') . '</th><th>' . esc_html__('创建时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$standalone) {
echo '<tr><td colspan="6">' . esc_html__('暂无独立邀请码', 'aksrt') . '</td></tr>';
}
$shown = 0;
foreach ($standalone as $entry) {
if ($shown >= 200) {
break;
}
$code = sanitize_key((string) ($entry['code'] ?? ''));
if ($code === '') {
continue;
}
$disabled = !empty($entry['disabled']);
$used = !empty($entry['used']) || !empty($entry['used_by']);
$status = $disabled ? __('已禁用', 'aksrt') : ($used ? __('已使用', 'aksrt') : __('可用', 'aksrt'));
echo '<tr>';
echo '<td><code>' . esc_html($code) . '</code></td>';
echo '<td>' . esc_html($status) . '</td>';
echo '<td>' . esc_html((string) ($entry['batch_id'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($entry['note'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($entry['created_at'] ?? '')) . '</td>';
echo '<td>';
if (!$disabled && !$used) {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_users_disable_invite_code" />';
echo '<input type="hidden" name="code" value="' . esc_attr($code) . '" />';
wp_nonce_field('aksrt_users_disable_invite_code_' . $code);
submit_button(__('禁用', 'aksrt'), 'small', '', false);
echo '</form>';
} else {
echo '—';
}
echo '</td></tr>';
$shown++;
}
echo '</tbody></table>';
global $wpdb;
$rows = $wpdb->get_results(
"SELECT user_id, meta_value AS invite_code FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' ORDER BY umeta_id DESC LIMIT 200",
ARRAY_A
);
echo '<h2>' . esc_html__('用户邀请码', 'aksrt') . '</h2>';
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('邀请码', 'aksrt') . '</th><th>' . esc_html__('邀请人数', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$rows) {
echo '<tr><td colspan="3">' . esc_html__('暂无用户邀请码', 'aksrt') . '</td></tr>';
}
$invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : '';
foreach ((array) $rows as $row) {
$user_id = (int) ($row['user_id'] ?? 0);
$user = $user_id > 0 ? get_userdata($user_id) : null;
$count = 0;
if ($invite_table) {
$count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$invite_table} WHERE inviter_id = %d",
$user_id
));
}
echo '<tr>';
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
echo '<td><code>' . esc_html((string) ($row['invite_code'] ?? '')) . '</code></td>';
echo '<td>' . esc_html((string) $count) . '</td>';
echo '</tr>';
}
echo '</tbody></table></div>';
}
public function handle_batch_invite_codes(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
check_admin_referer('aksrt_users_batch_invite_codes');
$count = isset($_POST['count']) ? max(1, min(100, (int) $_POST['count'])) : 10;
$length = isset($_POST['length']) ? max(4, min(12, (int) $_POST['length'])) : 6;
$prefix = isset($_POST['prefix']) ? sanitize_key((string) wp_unslash($_POST['prefix'])) : '';
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
$batch = 'INV-' . gmdate('YmdHis') . '-' . wp_generate_password(4, false, false);
$alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
$codes = get_option('aksrt_standalone_invite_codes', []);
$codes = is_array($codes) ? array_values($codes) : [];
$existing = [];
foreach ($codes as $entry) {
$c = sanitize_key((string) ($entry['code'] ?? ''));
if ($c !== '') {
$existing[$c] = true;
}
}
global $wpdb;
$generated = [];
for ($i = 0; $i < $count; $i++) {
$code = '';
for ($attempt = 0; $attempt < 20; $attempt++) {
$candidate = $prefix;
for ($j = 0; $j < $length; $j++) {
$candidate .= $alphabet[random_int(0, strlen($alphabet) - 1)];
}
$candidate = sanitize_key($candidate);
if ($candidate === '' || isset($existing[$candidate])) {
continue;
}
$user_hit = $wpdb->get_var($wpdb->prepare(
"SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' AND meta_value = %s LIMIT 1",
$candidate
));
if ($user_hit) {
continue;
}
$code = $candidate;
break;
}
if ($code === '') {
continue;
}
$existing[$code] = true;
$codes[] = [
'code' => $code,
'created_at' => current_time('mysql'),
'used' => false,
'used_by' => 0,
'used_at' => '',
'disabled' => false,
'note' => $note,
'batch_id' => $batch,
'created_by' => get_current_user_id(),
];
$generated[] = $code;
}
update_option('aksrt_standalone_invite_codes', array_values($codes), false);
$ok = !empty($generated);
$this->redirect('aksrt_invite_codes', $ok ? 'invite_batch_ok' : 'invite_batch_failed', [
'aksrt_generated' => count($generated),
]);
}
public function handle_disable_invite_code(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$code = isset($_POST['code']) ? sanitize_key((string) wp_unslash($_POST['code'])) : '';
check_admin_referer('aksrt_users_disable_invite_code_' . $code);
$ok = false;
if ($code !== '') {
$codes = get_option('aksrt_standalone_invite_codes', []);
$codes = is_array($codes) ? $codes : [];
foreach ($codes as $i => $entry) {
if (sanitize_key((string) ($entry['code'] ?? '')) !== $code) {
continue;
}
$entry['disabled'] = true;
$codes[$i] = $entry;
$ok = true;
break;
}
if ($ok) {
update_option('aksrt_standalone_invite_codes', array_values($codes), false);
}
}
$this->redirect('aksrt_invite_codes', $ok ? 'invite_disabled' : 'invite_failed');
}
/* ------------------------------------------------------------------
* Ban / reports
* ----------------------------------------------------------------*/
+144
View File
@@ -0,0 +1,144 @@
<?php
/**
* Native user referral-code operations.
*
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
trait UsersMenusReferral {
public function render_referral_codes_page(): void {
if (!current_user_can('manage_users')) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
}
$endpoint = rest_url('aksrt/v1/revenue/codes');
$nonce = wp_create_nonce('wp_rest');
?>
<div class="wrap" id="aksrt-referral-ops">
<h1><?php esc_html_e('推广码运营', 'aksrt'); ?></h1>
<p><?php esc_html_e('推广码与用户一一绑定。禁用或重新生成只影响后续访问和注册,不改变已有推广关系。', 'aksrt'); ?></p>
<div class="tablenav top">
<div class="alignleft actions">
<label class="screen-reader-text" for="aksrt-ref-status"><?php esc_html_e('筛选状态', 'aksrt'); ?></label>
<select id="aksrt-ref-status">
<option value="all"><?php esc_html_e('全部状态', 'aksrt'); ?></option>
<option value="active"><?php esc_html_e('已启用', 'aksrt'); ?></option>
<option value="disabled"><?php esc_html_e('已禁用', 'aksrt'); ?></option>
<option value="missing"><?php esc_html_e('未生成', 'aksrt'); ?></option>
</select>
<input type="search" id="aksrt-ref-search" placeholder="<?php esc_attr_e('搜索用户或 Ref 码', 'aksrt'); ?>">
<button type="button" class="button" id="aksrt-ref-filter"><?php esc_html_e('筛选', 'aksrt'); ?></button>
</div>
</div>
<div id="aksrt-ref-notice" role="status" aria-live="polite"></div>
<table class="wp-list-table widefat fixed striped">
<thead><tr>
<th><?php esc_html_e('用户', 'aksrt'); ?></th>
<th><?php esc_html_e('Ref 推广码', 'aksrt'); ?></th>
<th><?php esc_html_e('状态', 'aksrt'); ?></th>
<th><?php esc_html_e('绑定用户', 'aksrt'); ?></th>
<th><?php esc_html_e('有效付费用户', 'aksrt'); ?></th>
<th><?php esc_html_e('操作', 'aksrt'); ?></th>
</tr></thead>
<tbody id="aksrt-ref-rows"><tr><td colspan="6"><?php esc_html_e('正在加载…', 'aksrt'); ?></td></tr></tbody>
</table>
<div class="tablenav bottom"><div class="tablenav-pages" id="aksrt-ref-pages"></div></div>
</div>
<script>
(() => {
const endpoint = <?php echo wp_json_encode($endpoint); ?>;
const nonce = <?php echo wp_json_encode($nonce); ?>;
const rows = document.getElementById('aksrt-ref-rows');
const pages = document.getElementById('aksrt-ref-pages');
const notice = document.getElementById('aksrt-ref-notice');
const search = document.getElementById('aksrt-ref-search');
const status = document.getElementById('aksrt-ref-status');
let currentPage = 1;
const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#039;', '"': '&quot;'
})[char]);
const setNotice = (message, isError = false) => {
notice.className = message ? `notice notice-${isError ? 'error' : 'success'} inline` : '';
notice.innerHTML = message ? `<p>${escapeHtml(message)}</p>` : '';
};
const request = async (url, options = {}) => {
const response = await fetch(url, {
credentials: 'same-origin',
...options,
headers: { 'X-WP-Nonce': nonce, 'Content-Type': 'application/json', ...(options.headers || {}) },
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.message || '<?php echo esc_js(__('请求失败', 'aksrt')); ?>');
return payload;
};
const actionButton = (item) => {
const id = Number(item.user.id);
if (item.status === 'missing') {
return `<button class="button button-primary" data-ref-action="generate" data-user-id="${id}"><?php echo esc_js(__('生成', 'aksrt')); ?></button>`;
}
if (item.status === 'disabled') {
return `<button class="button" data-ref-action="regenerate" data-user-id="${id}"><?php echo esc_js(__('重新生成', 'aksrt')); ?></button>`;
}
return `<button class="button" data-ref-action="disable" data-user-id="${id}"><?php echo esc_js(__('禁用', 'aksrt')); ?></button> `
+ `<button class="button" data-ref-action="regenerate" data-user-id="${id}"><?php echo esc_js(__('重新生成', 'aksrt')); ?></button>`;
};
const load = async (page = 1) => {
currentPage = page;
setNotice('');
rows.innerHTML = '<tr><td colspan="6"><?php echo esc_js(__('正在加载…', 'aksrt')); ?></td></tr>';
const url = new URL(endpoint);
url.searchParams.set('page', String(page));
url.searchParams.set('per_page', '20');
url.searchParams.set('status', status.value);
if (search.value.trim()) url.searchParams.set('search', search.value.trim());
try {
const data = await request(url.toString());
rows.innerHTML = data.items.length ? data.items.map((item) => `
<tr>
<td><strong>${escapeHtml(item.user.name)}</strong><br><small>${escapeHtml(item.user.login)} · #${Number(item.user.id)}</small></td>
<td>${item.ref ? `<code>${escapeHtml(item.ref)}</code>` : '—'}</td>
<td>${escapeHtml({active:'<?php echo esc_js(__('已启用', 'aksrt')); ?>',disabled:'<?php echo esc_js(__('已禁用', 'aksrt')); ?>',missing:'<?php echo esc_js(__('未生成', 'aksrt')); ?>'}[item.status] || item.status)}</td>
<td>${Number(item.bound_count || 0)}</td>
<td>${Number(item.effective_payers || 0)}</td>
<td>${actionButton(item)}</td>
</tr>`).join('') : '<tr><td colspan="6"><?php echo esc_js(__('没有符合条件的用户', 'aksrt')); ?></td></tr>';
pages.innerHTML = Array.from({length: Number(data.max_pages || 1)}, (_, index) => {
const number = index + 1;
return `<button type="button" class="button ${number === Number(data.page) ? 'button-primary' : ''}" data-ref-page="${number}">${number}</button>`;
}).join(' ');
} catch (error) {
rows.innerHTML = '<tr><td colspan="6"><?php echo esc_js(__('推广码数据加载失败', 'aksrt')); ?></td></tr>';
setNotice(error.message, true);
}
};
document.getElementById('aksrt-ref-filter').addEventListener('click', () => load(1));
search.addEventListener('keydown', (event) => { if (event.key === 'Enter') load(1); });
document.getElementById('aksrt-ref-ops').addEventListener('click', async (event) => {
const pageButton = event.target.closest('[data-ref-page]');
if (pageButton) return load(Number(pageButton.dataset.refPage));
const button = event.target.closest('[data-ref-action]');
if (!button) return;
button.disabled = true;
try {
await request(`${endpoint}/${Number(button.dataset.userId)}/${button.dataset.refAction}`, { method: 'POST', body: '{}' });
setNotice('<?php echo esc_js(__('推广码已更新', 'aksrt')); ?>');
await load(currentPage);
} catch (error) {
setNotice(error.message, true);
button.disabled = false;
}
});
load();
})();
</script>
<?php
}
}
+1 -3
View File
@@ -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 [
-1
View File
@@ -147,7 +147,6 @@ class SchemaPayment {
['key' => 'affiliate_ratio_vip_1', 'label' => '一级会员佣金比例(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
['key' => 'affiliate_ratio_vip_2', 'label' => '二级会员佣金比例(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
['key' => 'affiliate_order_types', 'label' => '适用订单类型', 'type' => 'checkbox', 'options' => [['value' => 'membership', 'label' => '会员订单'], ['value' => 'content', 'label' => '付费内容'], ['value' => 'credits', 'label' => '购买积分']], 'default' => ['membership', 'content']],
['key' => 'affiliate_touch_window_days', 'label' => '首次触达窗口(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
['key' => 'affiliate_settlement_days', 'label' => '结算观察期(天)', 'type' => 'number', 'default' => 7, 'min' => 0, 'max' => 90],
]],
['key' => 'text', 'title' => '文案设置', 'fields' => [
+11 -42
View File
@@ -186,14 +186,18 @@ class SchemaUsers {
]],
['key' => 'email_set_captch', 'label' => '绑定邮箱需验证', 'type' => 'switch', 'default' => true, 'desc' => '绑定邮箱时需发送验证码确认'],
]],
['key' => 'invite_inline', 'title' => '邀请码注册', 'fields' => [
['key' => 'invit_code_s', 'label' => '启用邀请码', 'type' => 'switch', 'default' => false, 'desc' => '与「邀请码」子页联动,此处开关控制注册表单是否显示邀请码字段'],
['key' => 'invit_code_mode', 'label' => '邀请模式', 'type' => 'select', 'default' => 'open', 'options' => [
['label' => '开放注册(邀请码选填)', 'value' => 'open'],
['label' => '必须邀请码', 'value' => 'must'],
['key' => 'referral_inline', 'title' => '推广码注册', 'fields' => [
['key' => 'referral_registration_enabled', 'label' => '开放推广码注册', 'type' => 'switch', 'default' => true],
['key' => 'referral_register_field', 'label' => '注册页推广码字段', 'type' => 'select', 'default' => 'optional', 'options' => [
['label' => '隐藏', 'value' => 'hidden'],
['label' => '选填', 'value' => 'optional'],
['label' => '必填', 'value' => 'required'],
]],
['key' => 'invit_code_open_desc', 'label' => '开放注册提示', 'type' => 'input', 'default' => '邀请码为选填,没有邀请码也可以注册'],
['key' => 'invit_code_must_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入邀请码'],
['key' => 'referral_reject_invalid', 'label' => '拒绝无效推广码', 'type' => 'switch', 'default' => true],
['key' => 'referral_code_length', 'label' => 'Ref 码长度', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 12],
['key' => 'referral_window_days', 'label' => '首次触达窗口(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
['key' => 'referral_open_desc', 'label' => '选填提示', 'type' => 'input', 'default' => '推广码为选填,没有推广码也可以注册'],
['key' => 'referral_required_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入推广码'],
]],
['key' => 'sign_page_ui', 'title' => '页面 UI 配置', 'fields' => [
['key' => 'sign_page_background', 'label' => '页面背景图', 'type' => 'gallery', 'default' => [], 'desc' => '登录注册页背景图,多图随机'],
@@ -529,7 +533,6 @@ class SchemaUsers {
['key' => 'points_free_comment_new', 'label' => '发表评论', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
['key' => 'points_free_comment_like', 'label' => '评论被赞', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
['key' => 'points_free_followed', 'label' => '被关注', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
['key' => 'points_earn_invite', 'label' => '邀请注册奖励', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000, 'help' => '邀请奖励不走免费日上限'],
]],
['key' => 'spend', 'title' => '积分消费', 'fields' => [
['key' => 'points_spend_comment', 'label' => '评论消耗', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
@@ -558,40 +561,6 @@ class SchemaUsers {
]],
],
],
'invite' => [
'key' => 'invite',
'title' => '邀请码',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'basic', 'title' => '邀请设置', 'fields' => [
['key' => 'invit_code_s', 'label' => '启用邀请码', 'type' => 'switch', 'default' => false],
['key' => 'invit_code_open_desc', 'label' => '开放注册提示', 'type' => 'input', 'default' => '邀请码为选填,没有邀请码也可以注册'],
['key' => 'invit_code_must_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入邀请码'],
['key' => 'invit_code_mode', 'label' => '邀请模式', 'type' => 'select', 'default' => 'open', 'options' => [
['label' => '开放注册(邀请码选填)', 'value' => 'open'],
['label' => '必须邀请码', 'value' => 'must'],
]],
]],
['key' => 'format', 'title' => '码面设置', 'fields' => [
['key' => 'invit_code_length', 'label' => '邀请码长度', 'type' => 'number', 'default' => 8, 'min' => 4, 'max' => 32],
['key' => 'invit_code_prefix', 'label' => '邀请码前缀', 'type' => 'input', 'default' => ''],
['key' => 'invit_code_max_use', 'label' => '单码最大使用次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 9999],
['key' => 'invit_code_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效'],
['key' => 'invit_code_batch', 'label' => '批量生成数量', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 500],
]],
['key' => 'reward', 'title' => '邀请奖励', 'fields' => [
['key' => 'invit_reward_points', 'label' => '邀请奖励积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 1000],
['key' => 'invit_reward_level', 'label' => '邀请奖励等级', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 10],
['key' => 'invit_reward_vip_days', 'label' => '奖励VIP天数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 3650],
['key' => 'invit_invitee_reward', 'label' => '被邀请人奖励', 'type' => 'switch', 'default' => false],
]],
['key' => 'display', 'title' => '展示设置', 'fields' => [
['key' => 'invit_code_show_count', 'label' => '显示已使用次数', 'type' => 'switch', 'default' => true],
['key' => 'invit_code_show_remain', 'label' => '显示剩余次数', 'type' => 'switch', 'default' => true],
]],
],
],
'features' => [
'key' => 'features',
'title' => '用户功能',
-2
View File
@@ -93,7 +93,6 @@ class ToolsController extends ControllerBase {
$expected_tables = [
$wpdb->prefix . 'aksrt_credit_log',
$wpdb->prefix . 'aksrt_signin_log',
$wpdb->prefix . 'aksrt_invite',
$wpdb->prefix . 'aksrt_dl_token',
$wpdb->prefix . 'aksrt_notification',
];
@@ -142,7 +141,6 @@ class ToolsController extends ControllerBase {
$tables = [
$wpdb->prefix . 'aksrt_credit_log',
$wpdb->prefix . 'aksrt_signin_log',
$wpdb->prefix . 'aksrt_invite',
$wpdb->prefix . 'aksrt_dl_token',
$wpdb->prefix . 'aksrt_notification',
];
+22 -48
View File
@@ -9,7 +9,6 @@ defined('ABSPATH') || exit;
class UserController extends ControllerBase {
use UserControllerReportOperations;
use UserControllerInviteOperations;
use UserControllerVerifyIntegrations;
use UserControllerIdentitySettings;
use UserControllerAuthPhoneOperations;
@@ -208,36 +207,6 @@ class UserController extends ControllerBase {
'permission_callback' => '__return_true',
]);
// Invite codes management (admin)
register_rest_route(self::API_NAMESPACE, '/invite/codes', [
'methods' => 'GET',
'callback' => [$this, 'get_invite_codes'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/invite/codes', [
'methods' => 'POST',
'callback' => [$this, 'generate_invite_codes'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/invite/codes/(?P<id>\d+)/disable', [
'methods' => 'POST',
'callback' => [$this, 'disable_invite_code'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/invite/codes/bulk', [
'methods' => 'POST',
'callback' => [$this, 'bulk_update_invite_codes'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/invite/stats', [
'methods' => 'GET',
'callback' => [$this, 'get_invite_stats'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── Modal Auth (弹窗登录/注册) ──
// Modal login
@@ -432,17 +401,21 @@ class UserController extends ControllerBase {
if ($request->get_method() === 'GET') {
$data = \AKSRT\Settings\Settings::get_all();
$invite = class_exists('\AKSRT\Settings\CreditsRuntime')
? \AKSRT\Settings\CreditsRuntime::get_invite_registration_config()
$referral = class_exists('\AKSRT\Settings\CreditsRuntime')
? \AKSRT\Settings\CreditsRuntime::get_referral_registration_settings()
: [];
$shared = class_exists('\AKSRT\Settings\CreditsRuntime')
? \AKSRT\Settings\CreditsRuntime::get_referral_settings()
: [];
return $this->success(['data' => [
'registration_email_verification_enabled' => \AKSRT\Settings\Settings::registration_requires_email_verification(),
'registration_email_verification_method' => \AKSRT\Settings\Settings::registration_verification_method(),
'invite_registration_enabled' => $invite['enabled'] ?? true,
'invite_code_length' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::invite_code_length() : 6,
'invite_register_field' => $invite['register_field'] ?? 'optional',
'invite_reject_invalid' => $invite['reject_invalid'] ?? false,
'referral_registration_enabled' => $referral['enabled'] ?? true,
'referral_code_length' => $shared['code_length'] ?? 12,
'referral_window_days' => $shared['window_days'] ?? 30,
'referral_register_field' => $referral['register_field'] ?? 'optional',
'referral_reject_invalid' => $referral['reject_invalid'] ?? true,
] + array_intersect_key($data, $allowed)]);
}
@@ -451,20 +424,21 @@ class UserController extends ControllerBase {
$sanitized = $this->sanitize_fields($params, $allowed);
$settings = array_merge($settings, $sanitized);
if (array_key_exists('invite_registration_enabled', $params)) {
$settings['invite_registration_enabled'] = !empty($params['invite_registration_enabled']);
if (array_key_exists('referral_registration_enabled', $params)) {
$settings['referral_registration_enabled'] = !empty($params['referral_registration_enabled']);
}
if (array_key_exists('invite_code_length', $params)) {
$settings['invite_code_length'] = max(4, min(12, (int) $params['invite_code_length']));
if (array_key_exists('referral_code_length', $params)) {
$settings['referral_code_length'] = max(4, min(12, (int) $params['referral_code_length']));
}
if (array_key_exists('invite_register_field', $params)) {
$field = (string) $params['invite_register_field'];
$settings['invit_code_mode'] = $field === 'required' ? 'must' : ($field === 'hidden' ? 'close' : 'open');
if (array_key_exists('referral_window_days', $params)) {
$settings['referral_window_days'] = max(1, min(365, (int) $params['referral_window_days']));
}
if (array_key_exists('invite_reject_invalid', $params)) {
$reward = is_array($settings['invite_reward'] ?? null) ? $settings['invite_reward'] : [];
$reward['reject_invalid'] = !empty($params['invite_reject_invalid']);
$settings['invite_reward'] = $reward;
if (array_key_exists('referral_register_field', $params)) {
$field = sanitize_key((string) $params['referral_register_field']);
$settings['referral_register_field'] = in_array($field, ['hidden', 'optional', 'required'], true) ? $field : 'optional';
}
if (array_key_exists('referral_reject_invalid', $params)) {
$settings['referral_reject_invalid'] = !empty($params['referral_reject_invalid']);
}
if (array_key_exists('registration_email_verification_enabled', $params)) {
$settings['registration_email_verification_enabled'] = !empty($params['registration_email_verification_enabled']);
@@ -67,7 +67,7 @@ trait UserControllerAuthPhoneOperations {
$email = sanitize_email((string) ($payload['user_email'] ?? $payload['email'] ?? ''));
$password = (string) ($payload['user_pass'] ?? $payload['password'] ?? '');
$confirm = (string) ($payload['user_pass_confirm'] ?? $payload['password_confirm'] ?? $payload['confirm_password'] ?? $password);
$invite_code = sanitize_text_field((string) ($payload['invite_code'] ?? ''));
$ref = sanitize_key((string) ($payload['ref'] ?? ''));
$register_nonce = sanitize_text_field((string) ($payload['_register_nonce'] ?? ''));
$agree_terms = !empty($payload['agree_terms']) ? '1' : '';
@@ -91,7 +91,7 @@ trait UserControllerAuthPhoneOperations {
$payload['user_email'] = $email;
$payload['user_pass'] = $password;
$payload['user_pass_confirm'] = $confirm;
$payload['invite_code'] = $invite_code;
$payload['ref'] = $ref;
$payload['_register_nonce'] = $register_nonce;
$payload['agree_terms'] = $agree_terms;
@@ -125,6 +125,7 @@ trait UserControllerAuthPhoneOperations {
'login_url' => $login_url,
'user_login' => $result['user_login'] ?? $username,
'user_email' => $email,
'notice' => (string) ($result['notice'] ?? ''),
'user' => $registered_user ? [
'id' => $registered_user->ID,
'display_name' => $registered_user->display_name,
@@ -232,6 +233,7 @@ trait UserControllerAuthPhoneOperations {
'message' => $result['message'] ?? '注册成功',
'redirect' => $result['redirect'] ?? home_url('/'),
'user_id' => $result['user_id'] ?? 0,
'notice' => (string) ($result['notice'] ?? ''),
]);
}
@@ -1,450 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait UserControllerInviteOperations {
/**
* GET /invite/codes — 邀请码列表(管理后台)
*/
public function get_invite_codes(\WP_REST_Request $request): \WP_REST_Response {
global $wpdb;
$page = max(1, (int) ($request->get_param('page') ?: 1));
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
$status = sanitize_key((string) $request->get_param('status'));
$source = sanitize_key((string) $request->get_param('source'));
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
$usermeta_table = $wpdb->usermeta;
$invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : '';
$invite_rows = [];
if ($invite_table) {
$records = $wpdb->get_results("SELECT * FROM {$invite_table}", ARRAY_A);
foreach ((array) $records as $record) {
$invite_rows[(string) $record['invite_code']] = $record;
}
}
$items = [];
$user_rows = $wpdb->get_results(
"SELECT user_id, meta_value AS invite_code FROM {$usermeta_table} WHERE meta_key = 'aksrt_invite_code' ORDER BY umeta_id DESC",
ARRAY_A
);
foreach ((array) $user_rows as $row) {
$user_id = (int) $row['user_id'];
$user = get_userdata($user_id);
$code = sanitize_key((string) $row['invite_code']);
$record = $invite_rows[$code] ?? null;
$item = [
'row_key' => 'user:' . $user_id,
'source_type' => 'user',
'source_label' => __('用户邀请码', 'aksrt'),
'user' => $this->build_admin_user_summary($user_id, $user),
'invite_code' => $code,
'status' => $record ? 'used' : 'active',
'status_label' => $record ? __('已使用', 'aksrt') : __('可用', 'aksrt'),
'created_at' => $user ? (string) $user->user_registered : '',
'used_at' => $record['created_at'] ?? '',
'note' => '',
'batch_id' => '',
'created_by' => null,
'registration_status' => $record['status'] ?? '',
'invitee' => $this->build_admin_user_summary((int) ($record['invitee_id'] ?? 0)),
'can_bulk' => false,
'actions' => ['disable'],
];
if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) {
$items[] = $item;
}
}
foreach ($this->load_standalone_invite_codes() as $entry) {
$code = sanitize_key((string) ($entry['code'] ?? ''));
$record = $invite_rows[$code] ?? null;
$used_by = (int) ($entry['used_by'] ?? 0);
$disabled = !empty($entry['disabled']);
$used = !empty($entry['used']) || $used_by > 0 || !empty($record['invitee_id']);
$status_key = $disabled ? 'disabled' : ($used ? 'used' : 'active');
$status_map = [
'active' => __('可用', 'aksrt'),
'used' => __('已使用', 'aksrt'),
'disabled' => __('已禁用', 'aksrt'),
];
$invitee_id = $used_by > 0 ? $used_by : (int) ($record['invitee_id'] ?? 0);
$item = [
'row_key' => 'standalone:' . $code,
'source_type' => 'standalone',
'source_label' => __('独立邀请码', 'aksrt'),
'user' => null,
'invite_code' => $code,
'status' => $status_key,
'status_label' => $status_map[$status_key],
'created_at' => (string) ($entry['created_at'] ?? ''),
'used_at' => (string) ($entry['used_at'] ?? ($record['created_at'] ?? '')),
'note' => (string) ($entry['note'] ?? ''),
'batch_id' => (string) ($entry['batch_id'] ?? ''),
'created_by' => $this->build_admin_user_summary((int) ($entry['created_by'] ?? 0)),
'registration_status' => $record['status'] ?? '',
'invitee' => $this->build_admin_user_summary($invitee_id),
'can_bulk' => true,
'actions' => ['disable', 'delete'],
];
if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) {
$items[] = $item;
}
}
usort($items, static function ($left, $right) {
return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? ''));
});
$total = count($items);
$offset = ($page - 1) * $per_page;
$paged = array_slice($items, $offset, $per_page);
$stats = $this->build_invite_operation_stats($items, $invite_rows);
return $this->success([
'data' => [
'items' => array_values($paged),
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
'stats' => $stats,
'options' => [
'statuses' => [
['label' => __('全部状态', 'aksrt'), 'value' => ''],
['label' => __('可用', 'aksrt'), 'value' => 'active'],
['label' => __('已使用', 'aksrt'), 'value' => 'used'],
['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'],
],
'sources' => [
['label' => __('全部来源', 'aksrt'), 'value' => ''],
['label' => __('用户邀请码', 'aksrt'), 'value' => 'user'],
['label' => __('独立邀请码', 'aksrt'), 'value' => 'standalone'],
],
],
],
]);
}
/**
* POST /invite/codes — 批量生成邀请码(管理后台)
* Body: { count: int, length: int, prefix: string }
* 为指定用户或批量创建邀请码。
*/
public function generate_invite_codes(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Credits\Invite')) {
return $this->error(__('邀请系统未启用', 'aksrt'), 500);
}
$params = $this->get_json_params($request);
$count = max(1, min(100, (int) ($params['count'] ?? 1)));
$length = max(4, min(12, (int) ($params['length'] ?? 6)));
$prefix = sanitize_key((string) ($params['prefix'] ?? ''));
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
$batch_id = sanitize_text_field((string) ($params['batch_id'] ?? ''));
if ($batch_id === '') {
$batch_id = 'INV-' . gmdate('YmdHis') . '-' . wp_generate_password(4, false, false);
}
$generated = [];
$codes = $this->load_standalone_invite_codes();
$alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
for ($i = 0; $i < $count; $i++) {
$code = '';
for ($attempt = 0; $attempt < 12; $attempt++) {
$candidate = $prefix;
for ($j = 0; $j < $length; $j++) {
$candidate .= $alphabet[random_int(0, strlen($alphabet) - 1)];
}
$candidate = sanitize_key($candidate);
if (!$this->invite_code_exists_globally($candidate, $codes)) {
$code = $candidate;
break;
}
}
if ($code === '') {
continue;
}
$codes[] = [
'code' => $code,
'created_at' => current_time('mysql'),
'used' => false,
'used_by' => 0,
'used_at' => '',
'disabled' => false,
'note' => $note,
'batch_id' => $batch_id,
'created_by' => get_current_user_id(),
];
$generated[] = $code;
}
$this->save_standalone_invite_codes($codes);
return $this->success([
'message' => sprintf(__('已生成 %d 个邀请码', 'aksrt'), count($generated)),
'data' => [
'codes' => $generated,
'batch_id' => $batch_id,
],
]);
}
/**
* POST /invite/codes/{id}/disable — 禁用邀请码
*/
public function disable_invite_code(\WP_REST_Request $request): \WP_REST_Response {
$user_id = (int) $request->get_param('id');
if ($user_id <= 0) {
return $this->error(__('参数无效', 'aksrt'));
}
$removed = $this->remove_user_invite_code($user_id);
if (!$removed) {
return $this->error(__('邀请码不存在或已禁用', 'aksrt'));
}
return $this->success(['message' => __('邀请码已禁用', 'aksrt')]);
}
public function bulk_update_invite_codes(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$action = sanitize_key((string) ($params['action'] ?? ''));
$targets = isset($params['targets']) && is_array($params['targets']) ? $params['targets'] : [];
if (!in_array($action, ['disable', 'delete'], true) || empty($targets)) {
return $this->error(__('批量操作参数无效', 'aksrt'));
}
$codes = $this->load_standalone_invite_codes();
$processed = 0;
foreach ($targets as $target) {
$source_type = sanitize_key((string) ($target['source_type'] ?? ''));
if ($source_type === 'user') {
$user_id = (int) ($target['user_id'] ?? 0);
if ($action === 'disable' && $this->remove_user_invite_code($user_id)) {
$processed++;
}
continue;
}
if ($source_type !== 'standalone') {
continue;
}
$code = sanitize_key((string) ($target['code'] ?? $target['invite_code'] ?? ''));
foreach ($codes as $index => $entry) {
$entry_code = sanitize_key((string) ($entry['code'] ?? ''));
if ($entry_code !== $code) {
continue;
}
if ($action === 'delete' && empty($entry['used']) && empty($entry['used_by'])) {
unset($codes[$index]);
} else {
$entry['disabled'] = true;
$codes[$index] = $entry;
}
$processed++;
break;
}
}
$this->save_standalone_invite_codes($codes);
return $this->success([
'message' => sprintf(__('已处理 %d 条邀请码记录', 'aksrt'), $processed),
]);
}
/**
* GET /invite/stats — 邀请系统统计
*/
public function get_invite_stats(): \WP_REST_Response {
global $wpdb;
$items = [];
$user_records = $wpdb->get_results(
"SELECT user_id, meta_value AS invite_code FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code'",
ARRAY_A
);
foreach ((array) $user_records as $row) {
$items[] = [
'source_type' => 'user',
'status' => 'active',
];
}
foreach ($this->load_standalone_invite_codes() as $entry) {
$used = !empty($entry['used']) || !empty($entry['used_by']);
$disabled = !empty($entry['disabled']);
$items[] = [
'source_type' => 'standalone',
'status' => $disabled ? 'disabled' : ($used ? 'used' : 'active'),
];
}
$stats = $this->build_invite_operation_stats($items);
return $this->success(['data' => $stats]);
}
private function load_standalone_invite_codes(): array {
$codes = get_option('aksrt_standalone_invite_codes', []);
return is_array($codes) ? array_values($codes) : [];
}
private function save_standalone_invite_codes(array $codes): void {
update_option('aksrt_standalone_invite_codes', array_values($codes), false);
}
private function invite_code_exists_globally(string $candidate, array $standalone_codes = []): bool {
if ($candidate === '') {
return true;
}
global $wpdb;
$existing_user_code = $wpdb->get_var($wpdb->prepare(
"SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' AND meta_value = %s LIMIT 1",
$candidate
));
if ($existing_user_code) {
return true;
}
foreach ($standalone_codes as $entry) {
if (sanitize_key((string) ($entry['code'] ?? '')) === $candidate) {
return true;
}
}
$invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : '';
if ($invite_table) {
$existing_invite = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$invite_table} WHERE invite_code = %s LIMIT 1",
$candidate
));
if ($existing_invite) {
return true;
}
}
return false;
}
private function invite_item_matches_filters(array $item, string $keyword, string $status, string $source, string $date_from, string $date_to): bool {
if ($status !== '' && ($item['status'] ?? '') !== $status) {
return false;
}
if ($source !== '' && ($item['source_type'] ?? '') !== $source) {
return false;
}
$created_at = (string) ($item['created_at'] ?? '');
if ($date_from !== '' && $created_at !== '' && strcmp($created_at, $date_from . ' 00:00:00') < 0) {
return false;
}
if ($date_to !== '' && $created_at !== '' && strcmp($created_at, $date_to . ' 23:59:59') > 0) {
return false;
}
if ($keyword === '') {
return true;
}
$haystack = implode(' ', array_filter([
(string) ($item['invite_code'] ?? ''),
(string) ($item['status_label'] ?? ''),
(string) ($item['note'] ?? ''),
(string) ($item['batch_id'] ?? ''),
(string) ($item['user']['display_name'] ?? ''),
(string) ($item['invitee']['display_name'] ?? ''),
(string) ($item['invitee']['email'] ?? ''),
]));
return mb_stripos($haystack, $keyword) !== false;
}
private function build_invite_operation_stats(array $items, array $invite_rows = []): array {
$stats = [
'total_codes' => count($items),
'active_codes' => 0,
'used_codes' => 0,
'disabled_codes' => 0,
'user_codes' => 0,
'standalone_codes' => 0,
'total_invites' => count($invite_rows),
'total_rewarded' => 0,
];
foreach ($items as $item) {
$status = $item['status'] ?? '';
if ($status === 'active') {
$stats['active_codes']++;
} elseif ($status === 'used') {
$stats['used_codes']++;
} elseif ($status === 'disabled') {
$stats['disabled_codes']++;
}
if (($item['source_type'] ?? '') === 'user') {
$stats['user_codes']++;
} elseif (($item['source_type'] ?? '') === 'standalone') {
$stats['standalone_codes']++;
}
}
foreach ($invite_rows as $row) {
if (($row['status'] ?? '') === 'rewarded') {
$stats['total_rewarded']++;
}
}
return $stats;
}
private function remove_user_invite_code(int $user_id): bool {
$user_id = (int) $user_id;
if ($user_id <= 0) {
return false;
}
$code = get_user_meta($user_id, 'aksrt_invite_code', true);
if (!$code) {
return false;
}
delete_user_meta($user_id, 'aksrt_invite_code');
$map = get_option('aksrt_invite_code_map', []);
if (is_array($map) && isset($map[$code])) {
unset($map[$code]);
update_option('aksrt_invite_code_map', $map, false);
}
return true;
}
}