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