prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?" ); $statement->execute([$table, $column]); return (int) $statement->fetchColumn() > 0; } function ensure_schema(): void { static $ensured = false; if ($ensured) { return; } $pdo = db(); $pdo->exec( "CREATE TABLE IF NOT EXISTS users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, email VARCHAR(190) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, user_code CHAR(6) NOT NULL UNIQUE, vip_level TINYINT UNSIGNED NOT NULL DEFAULT 0, email_verified_at DATETIME NULL, last_login_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); $pdo->exec( "CREATE TABLE IF NOT EXISTS wallets ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL UNIQUE, available_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00, frozen_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_wallet_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); $pdo->exec( "CREATE TABLE IF NOT EXISTS task_orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, task_slug VARCHAR(64) NOT NULL, task_title VARCHAR(120) NOT NULL, task_type VARCHAR(80) NOT NULL, platform_name VARCHAR(80) NOT NULL, reward_usdt DECIMAL(12,2) NOT NULL DEFAULT 0.00, vip_required TINYINT UNSIGNED NOT NULL DEFAULT 0, countdown_seconds INT UNSIGNED NOT NULL DEFAULT 60, status VARCHAR(32) NOT NULL DEFAULT 'claimed', proof_note TEXT NULL, claimed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, submitted_at DATETIME NULL, reviewed_at DATETIME NULL, review_note VARCHAR(255) NULL, CONSTRAINT fk_task_order_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, INDEX idx_task_orders_user_status (user_id, status), INDEX idx_task_orders_status_review (status, reviewed_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); $pdo->exec( "CREATE TABLE IF NOT EXISTS wallet_logs ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, entry_type VARCHAR(32) NOT NULL, amount DECIMAL(12,2) NOT NULL DEFAULT 0.00, frozen_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00, available_after DECIMAL(12,2) NOT NULL DEFAULT 0.00, frozen_after DECIMAL(12,2) NOT NULL DEFAULT 0.00, note VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_wallet_log_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, INDEX idx_wallet_logs_user_created (user_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); $pdo->exec( "CREATE TABLE IF NOT EXISTS vip_orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, from_level TINYINT UNSIGNED NOT NULL DEFAULT 0, to_level TINYINT UNSIGNED NOT NULL, price_usdt DECIMAL(12,2) NOT NULL DEFAULT 0.00, status VARCHAR(32) NOT NULL DEFAULT 'completed', available_after DECIMAL(12,2) NOT NULL DEFAULT 0.00, note VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, completed_at DATETIME NULL, CONSTRAINT fk_vip_order_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, INDEX idx_vip_orders_user_created (user_id, created_at), INDEX idx_vip_orders_status_created (status, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); $pdo->exec( "CREATE TABLE IF NOT EXISTS email_verification_codes ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, email VARCHAR(190) NOT NULL, purpose VARCHAR(32) NOT NULL DEFAULT 'signup', password_hash VARCHAR(255) NOT NULL, code_hash VARCHAR(255) NOT NULL, attempt_count TINYINT UNSIGNED NOT NULL DEFAULT 0, max_attempts TINYINT UNSIGNED NOT NULL DEFAULT 5, expires_at DATETIME NOT NULL, verified_at DATETIME NULL, consumed_at DATETIME NULL, ip_address VARCHAR(64) NULL, meta_note VARCHAR(255) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_email_codes_email_purpose (email, purpose, created_at), INDEX idx_email_codes_status (purpose, consumed_at, expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); if (!schema_has_column($pdo, 'users', 'email_verified_at')) { $pdo->exec("ALTER TABLE users ADD COLUMN email_verified_at DATETIME NULL AFTER vip_level"); } if (!schema_has_column($pdo, 'users', 'last_login_at')) { $pdo->exec("ALTER TABLE users ADD COLUMN last_login_at DATETIME NULL AFTER email_verified_at"); } $ensured = true; } ensure_schema(); function h($value): string { return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8"); } function project_name(): string { $name = trim((string) ($_SERVER["PROJECT_NAME"] ?? "")); return $name !== "" ? $name : "Atlas Rebate"; } function project_description(): string { $description = trim((string) ($_SERVER["PROJECT_DESCRIPTION"] ?? "")); return $description !== "" ? $description : "Mobile-first rebate operations slice with email onboarding, task execution, manual review, and wallet ledger."; } function asset_version(string $relativePath): string { $absolutePath = __DIR__ . "/" . ltrim($relativePath, "/"); return file_exists($absolutePath) ? (string) filemtime($absolutePath) : (string) time(); } function app_url(string $path, array $query = []): string { return $query ? $path . "?" . http_build_query($query) : $path; } function redirect(string $url): void { header("Location: " . $url); exit; } function format_usdt($amount): string { return number_format((float) $amount, 2) . " USDT"; } function format_delta($amount): string { $amount = (float) $amount; $prefix = $amount > 0 ? "+" : ""; return $prefix . number_format($amount, 2); } function format_datetime(?string $value): string { if (!$value) { return "—"; } $timestamp = strtotime($value); if ($timestamp === false) { return h($value); } return date("Y-m-d H:i", $timestamp); } function client_ip(): string { return (string) ($_SERVER["REMOTE_ADDR"] ?? "0.0.0.0"); } function text_length(string $value): int { return function_exists("mb_strlen") ? mb_strlen($value) : strlen($value); } function csrf_token(): string { if (empty($_SESSION["csrf_token"])) { $_SESSION["csrf_token"] = bin2hex(random_bytes(16)); } return (string) $_SESSION["csrf_token"]; } function verify_csrf_or_fail(): void { $token = (string) ($_POST["csrf_token"] ?? ""); $sessionToken = (string) ($_SESSION["csrf_token"] ?? ""); if ($token === "" || $sessionToken === "" || !hash_equals($sessionToken, $token)) { throw new RuntimeException("请求已失效,请刷新页面后重试。"); } } function flash(string $tone, string $message): void { $_SESSION["flash_messages"][] = [ "tone" => $tone, "message" => $message, ]; } function pull_flashes(): array { $messages = $_SESSION["flash_messages"] ?? []; unset($_SESSION["flash_messages"]); return is_array($messages) ? $messages : []; } function vip_catalog(): array { return [ 0 => [ "level" => 0, "name" => "VIP0", "price" => 0, "reward" => 24, "daily_tasks" => 2, "benefit" => "新手演示任务、自动钱包、资金流水可视化", ], 1 => [ "level" => 1, "name" => "VIP1", "price" => 1000, "reward" => 200, "daily_tasks" => 2, "benefit" => "1000 USDT 开通,单任务佣金 200 USDT", ], 2 => [ "level" => 2, "name" => "VIP2", "price" => 3000, "reward" => 600, "daily_tasks" => 2, "benefit" => "3000 USDT 开通,单任务佣金 600 USDT", ], 3 => [ "level" => 3, "name" => "VIP3", "price" => 5000, "reward" => 1000, "daily_tasks" => 2, "benefit" => "5000 USDT 开通,单任务佣金 1000 USDT", ], 4 => [ "level" => 4, "name" => "VIP4", "price" => 10000, "reward" => 2000, "daily_tasks" => 2, "benefit" => "10000 USDT 开通,单任务佣金 2000 USDT", ], ]; } function vip_info(int $vipLevel): array { $catalog = vip_catalog(); return $catalog[$vipLevel] ?? $catalog[0]; } function task_catalog(): array { return [ "watch-video" => [ "slug" => "watch-video", "title" => "观看 YouTube 视频", "platform" => "YouTube", "type" => "视频", "reward" => 18, "vip_required" => 0, "countdown" => 60, "summary" => "打开视频并保持活跃,倒计时结束后提交完成备注。", "steps" => [ "点击开始任务后进入倒计时。", "保持页面停留并完成观看动作。", "倒计时结束后提交一句完成备注。", ], ], "social-like" => [ "slug" => "social-like", "title" => "点赞 TikTok 视频", "platform" => "TikTok", "type" => "点赞", "reward" => 24, "vip_required" => 0, "countdown" => 60, "summary" => "适合新用户演示完整闭环:领取、计时、提交、审核入账。", "steps" => [ "领取任务并进入倒计时。", "模拟完成点赞或互动行为。", "提交完成说明,等待后台审核。", ], ], "follow-brand" => [ "slug" => "follow-brand", "title" => "关注 Instagram 博主", "platform" => "Instagram", "type" => "社媒", "reward" => 200, "vip_required" => 1, "countdown" => 90, "summary" => "VIP1 开始解锁,高佣金任务与更强审核优先级。", "steps" => [ "确认当前账号已开通对应 VIP。", "完成关注动作并保持页面停留。", "提交账户备注供后台核对。", ], ], "comment-flow" => [ "slug" => "comment-flow", "title" => "发布社媒评论", "platform" => "Community Feed", "type" => "社媒", "reward" => 600, "vip_required" => 2, "countdown" => 110, "summary" => "VIP2 解锁的中高客单价任务,用于验证冻结佣金逻辑。", "steps" => [ "领取后完成评论发布。", "保留评论内容关键词。", "在系统里提交评论摘要。", ], ], "site-browse" => [ "slug" => "site-browse", "title" => "浏览网站 60 秒", "platform" => "Website", "type" => "网站", "reward" => 1000, "vip_required" => 3, "countdown" => 140, "summary" => "VIP3 任务,适合做高佣金站点浏览与停留验证。", "steps" => [ "进入指定站点浏览多个页面。", "保持页面活跃直到倒计时结束。", "填写浏览到的产品或栏目名称。", ], ], "app-download" => [ "slug" => "app-download", "title" => "下载应用体验", "platform" => "App Center", "type" => "应用", "reward" => 2000, "vip_required" => 4, "countdown" => 180, "summary" => "VIP4 顶级任务,高收益、高风控、人工复核。", "steps" => [ "下载并打开指定应用。", "完成首次启动与页面停留。", "提交设备备注或体验摘要。", ], ], ]; } function task_by_slug(string $slug): ?array { $catalog = task_catalog(); return $catalog[$slug] ?? null; } function generate_unique_user_code(): string { $pdo = db(); $statement = $pdo->prepare("SELECT id FROM users WHERE user_code = ? LIMIT 1"); for ($i = 0; $i < 50; $i++) { $candidate = str_pad((string) random_int(0, 999999), 6, "0", STR_PAD_LEFT); $statement->execute([$candidate]); if (!$statement->fetchColumn()) { return $candidate; } } throw new RuntimeException("未能生成唯一 6 位数字 ID,请重试。"); } function fetch_user(int $userId): ?array { $statement = db()->prepare( "SELECT u.*, w.available_balance, w.frozen_balance, w.updated_at AS wallet_updated_at FROM users u INNER JOIN wallets w ON w.user_id = u.id WHERE u.id = ? LIMIT 1" ); $statement->execute([$userId]); $user = $statement->fetch(); return $user ?: null; } function current_user(): ?array { static $resolved = false; static $user = null; if ($resolved) { return $user; } $resolved = true; if (empty($_SESSION["user_id"])) { return null; } $user = fetch_user((int) $_SESSION["user_id"]); if (!$user) { unset($_SESSION["user_id"]); } return $user; } function safe_redirect_target(?string $target): string { $target = trim((string) $target); if ($target === '') { return 'index.php'; } $target = str_replace([" ", " "], '', $target); if (preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*:/', $target) || str_starts_with($target, '//')) { return 'index.php'; } if (str_contains($target, '..')) { return 'index.php'; } if (str_starts_with($target, '/')) { $target = ltrim($target, '/'); } return $target !== '' ? $target : 'index.php'; } function current_request_target(): string { return safe_redirect_target((string) ($_SERVER['REQUEST_URI'] ?? 'index.php')); } function auth_page_url(string $page, ?string $redirectTarget = null, array $query = []): string { if ($redirectTarget !== null && trim($redirectTarget) !== '') { $query['redirect'] = safe_redirect_target($redirectTarget); } return app_url($page, $query); } function start_page_url(?string $redirectTarget = null, array $query = []): string { return auth_page_url('start.php', $redirectTarget, $query); } function register_page_url(?string $redirectTarget = null, array $query = []): string { return auth_page_url('register.php', $redirectTarget, $query); } function login_page_url(?string $redirectTarget = null, array $query = []): string { return auth_page_url('login.php', $redirectTarget, $query); } function verify_page_url(string $email, ?string $redirectTarget = null, array $query = []): string { $query['email'] = normalize_email($email); return auth_page_url('verify.php', $redirectTarget, $query); } function require_user(): array { $user = current_user(); if (!$user) { flash('warning', '请先使用邮箱登录后再继续。'); redirect(login_page_url(current_request_target())); } return $user; } function set_logged_in_user(int $userId): void { $_SESSION['user_id'] = $userId; } function logout_current_user(): void { unset($_SESSION['user_id']); } function normalize_email(string $email): string { return strtolower(trim($email)); } function validate_email_or_fail(string $email): string { $email = normalize_email($email); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new RuntimeException('请输入有效邮箱地址。'); } return $email; } function ensure_password_strength(string $password): void { if (text_length($password) < 6) { throw new RuntimeException('密码至少需要 6 位字符。'); } } function verification_code_ttl_minutes(): int { return 10; } function generate_email_code(): string { return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); } function mask_email(string $email): string { $email = normalize_email($email); if (!str_contains($email, '@')) { return $email; } [$local, $domain] = explode('@', $email, 2); $visible = substr($local, 0, min(2, strlen($local))); $masked = $visible . str_repeat('*', max(2, strlen($local) - strlen($visible))); return $masked . '@' . $domain; } function send_signup_verification_email(string $email, string $code): array { require_once __DIR__ . '/mail/MailService.php'; $siteName = h(project_name()); $safeCode = h($code); $ttlMinutes = verification_code_ttl_minutes(); $subject = project_name() . ' 邮箱验证码'; $html = '
' . '

' . $siteName . '

' . '

你的邮箱验证码

' . '

请在 ' . $ttlMinutes . ' 分钟内输入下面这组 6 位数字:

' . '
' . $safeCode . '
' . '

如果这不是你本人操作,请直接忽略这封邮件。

' . '
'; $text = project_name() . ' 邮箱验证码:' . $code . '。请在 ' . $ttlMinutes . ' 分钟内完成验证。若非本人操作请忽略。'; return MailService::sendMail($email, $subject, $html, $text); } function mail_delivery_error_message(array $mailResult): string { $detail = strtolower((string) ($mailResult['error'] ?? '')); if ($detail === '') { return '验证码发送失败,请稍后重试。'; } if (str_contains($detail, 'recipient')) { return '验证码发送失败:当前收件地址无效,请重新输入邮箱。'; } return '验证码发送失败:当前 SMTP 未配置或连接失败,请先在平台环境变量里配置 MAIL_/SMTP_ 参数。'; } function create_user_account_record(PDO $pdo, string $email, string $passwordHash, ?string $emailVerifiedAt = null): int { $email = validate_email_or_fail($email); $check = $pdo->prepare('SELECT id FROM users WHERE email = ? LIMIT 1'); $check->execute([$email]); if ($check->fetchColumn()) { throw new RuntimeException('该邮箱已注册,请直接登录。'); } $userCode = generate_unique_user_code(); $insertUser = $pdo->prepare( 'INSERT INTO users (email, password_hash, user_code, vip_level, email_verified_at, last_login_at) VALUES (?, ?, ?, 0, ?, NULL)' ); $insertUser->execute([$email, $passwordHash, $userCode, $emailVerifiedAt]); $userId = (int) $pdo->lastInsertId(); $insertWallet = $pdo->prepare( 'INSERT INTO wallets (user_id, available_balance, frozen_balance) VALUES (?, 0.00, 0.00)' ); $insertWallet->execute([$userId]); insert_wallet_log( $pdo, $userId, 'wallet_opened', 0, 0, '邮箱注册成功,系统已自动创建钱包与 VIP0。', 0, 0 ); return $userId; } function register_user(string $email, string $password): array { $email = validate_email_or_fail($email); ensure_password_strength($password); $pdo = db(); $pdo->beginTransaction(); try { $userId = create_user_account_record( $pdo, $email, password_hash($password, PASSWORD_DEFAULT), date('Y-m-d H:i:s') ); $pdo->commit(); set_logged_in_user($userId); return fetch_user($userId) ?? []; } catch (Throwable $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $exception; } } function latest_signup_verification(string $email, bool $pendingOnly = false): ?array { $email = validate_email_or_fail($email); $sql = 'SELECT * FROM email_verification_codes WHERE email = ? AND purpose = ?'; if ($pendingOnly) { $sql .= ' AND consumed_at IS NULL'; } $sql .= ' ORDER BY id DESC LIMIT 1'; $statement = db()->prepare($sql); $statement->execute([$email, 'signup']); $record = $statement->fetch(); return $record ?: null; } function issue_signup_verification_from_hash(string $email, string $passwordHash): array { $email = validate_email_or_fail($email); $code = generate_email_code(); $expiresAt = date('Y-m-d H:i:s', time() + (verification_code_ttl_minutes() * 60)); $pdo = db(); $pdo->beginTransaction(); try { $resetPending = $pdo->prepare( "UPDATE email_verification_codes SET consumed_at = NOW(), meta_note = 'superseded by new signup code' WHERE email = ? AND purpose = 'signup' AND consumed_at IS NULL AND verified_at IS NULL" ); $resetPending->execute([$email]); $insert = $pdo->prepare( 'INSERT INTO email_verification_codes ( email, purpose, password_hash, code_hash, expires_at, ip_address, meta_note ) VALUES (?, ?, ?, ?, ?, ?, ?)' ); $insert->execute([ $email, 'signup', $passwordHash, password_hash($code, PASSWORD_DEFAULT), $expiresAt, client_ip(), 'signup verification pending', ]); $verificationId = (int) $pdo->lastInsertId(); $pdo->commit(); } catch (Throwable $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $exception; } $mailResult = send_signup_verification_email($email, $code); if (empty($mailResult['success'])) { $cleanup = db()->prepare( "UPDATE email_verification_codes SET consumed_at = NOW(), meta_note = 'mail delivery failed' WHERE id = ? AND consumed_at IS NULL" ); $cleanup->execute([$verificationId]); error_log('Signup verification mail failed for ' . $email . ': ' . (string) ($mailResult['error'] ?? 'unknown')); throw new RuntimeException(mail_delivery_error_message($mailResult)); } $_SESSION['last_signup_email'] = $email; return [ 'id' => $verificationId, 'email' => $email, 'expires_at' => $expiresAt, ]; } function begin_signup_verification(string $email, string $password): array { $email = validate_email_or_fail($email); ensure_password_strength($password); $check = db()->prepare('SELECT id FROM users WHERE email = ? LIMIT 1'); $check->execute([$email]); if ($check->fetchColumn()) { throw new RuntimeException('该邮箱已注册,请直接登录。'); } return issue_signup_verification_from_hash($email, password_hash($password, PASSWORD_DEFAULT)); } function resend_signup_verification(string $email): array { $email = validate_email_or_fail($email); $check = db()->prepare('SELECT id FROM users WHERE email = ? LIMIT 1'); $check->execute([$email]); if ($check->fetchColumn()) { throw new RuntimeException('该邮箱已注册,请直接登录。'); } $latest = latest_signup_verification($email, false); if (!$latest || empty($latest['password_hash'])) { throw new RuntimeException('未找到待验证的注册记录,请先返回注册页填写邮箱和密码。'); } return issue_signup_verification_from_hash($email, (string) $latest['password_hash']); } function complete_signup_verification(string $email, string $code): array { $email = validate_email_or_fail($email); $code = trim($code); if (!preg_match('/^\d{6}$/', $code)) { throw new RuntimeException('请输入 6 位数字验证码。'); } $pdo = db(); $pdo->beginTransaction(); try { $statement = $pdo->prepare( "SELECT * FROM email_verification_codes WHERE email = ? AND purpose = 'signup' AND consumed_at IS NULL ORDER BY id DESC LIMIT 1 FOR UPDATE" ); $statement->execute([$email]); $record = $statement->fetch(); if (!$record) { throw new RuntimeException('验证码记录不存在,请返回注册页重新发送。'); } if ((string) ($record['verified_at'] ?? '') !== '') { throw new RuntimeException('该验证码已经使用过,请直接登录。'); } if (strtotime((string) $record['expires_at']) < time()) { $expired = $pdo->prepare( "UPDATE email_verification_codes SET consumed_at = NOW(), meta_note = 'code expired' WHERE id = ?" ); $expired->execute([(int) $record['id']]); $pdo->commit(); throw new RuntimeException('验证码已过期,请重新发送。'); } $maxAttempts = (int) ($record['max_attempts'] ?? 5); $attemptCount = (int) ($record['attempt_count'] ?? 0); if ($attemptCount >= $maxAttempts) { $locked = $pdo->prepare( "UPDATE email_verification_codes SET consumed_at = NOW(), meta_note = 'max attempts reached' WHERE id = ? AND consumed_at IS NULL" ); $locked->execute([(int) $record['id']]); $pdo->commit(); throw new RuntimeException('验证码错误次数过多,请重新发送验证码。'); } if (!password_verify($code, (string) $record['code_hash'])) { $attemptCount++; if ($attemptCount >= $maxAttempts) { $failed = $pdo->prepare( "UPDATE email_verification_codes SET attempt_count = ?, consumed_at = NOW(), meta_note = 'max attempts reached' WHERE id = ?" ); $failed->execute([$attemptCount, (int) $record['id']]); $pdo->commit(); throw new RuntimeException('验证码错误次数过多,请重新发送验证码。'); } $failed = $pdo->prepare( "UPDATE email_verification_codes SET attempt_count = ?, meta_note = 'verification code mismatch' WHERE id = ?" ); $failed->execute([$attemptCount, (int) $record['id']]); $pdo->commit(); throw new RuntimeException('验证码错误,请重新输入。'); } $userCheck = $pdo->prepare('SELECT id FROM users WHERE email = ? LIMIT 1'); $userCheck->execute([$email]); if ($userCheck->fetchColumn()) { $duplicate = $pdo->prepare( "UPDATE email_verification_codes SET consumed_at = NOW(), meta_note = 'email already registered' WHERE id = ?" ); $duplicate->execute([(int) $record['id']]); $pdo->commit(); throw new RuntimeException('该邮箱已注册,请直接登录。'); } $userId = create_user_account_record( $pdo, $email, (string) $record['password_hash'], date('Y-m-d H:i:s') ); $verified = $pdo->prepare( "UPDATE email_verification_codes SET verified_at = NOW(), consumed_at = NOW(), meta_note = 'signup completed' WHERE id = ?" ); $verified->execute([(int) $record['id']]); $pdo->commit(); unset($_SESSION['last_signup_email']); return fetch_user($userId) ?? []; } catch (Throwable $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $exception; } } function authenticate_user(string $email, string $password): array { $email = validate_email_or_fail($email); $statement = db()->prepare('SELECT * FROM users WHERE email = ? LIMIT 1'); $statement->execute([$email]); $record = $statement->fetch(); if (!$record || !password_verify($password, (string) $record['password_hash'])) { throw new RuntimeException('邮箱或密码错误,请重试。'); } $update = db()->prepare( 'UPDATE users SET last_login_at = NOW(), email_verified_at = COALESCE(email_verified_at, NOW()) WHERE id = ?' ); $update->execute([(int) $record['id']]); set_logged_in_user((int) $record['id']); return fetch_user((int) $record['id']) ?? []; } function wallet_snapshot(int $userId): array { $statement = db()->prepare( "SELECT available_balance, frozen_balance FROM wallets WHERE user_id = ? LIMIT 1" ); $statement->execute([$userId]); $wallet = $statement->fetch(); return $wallet ?: ["available_balance" => 0, "frozen_balance" => 0]; } function insert_wallet_log( PDO $pdo, int $userId, string $entryType, float $amount, float $frozenAmount, string $note, float $availableAfter, float $frozenAfter ): void { $statement = $pdo->prepare( "INSERT INTO wallet_logs ( user_id, entry_type, amount, frozen_amount, available_after, frozen_after, note ) VALUES (?, ?, ?, ?, ?, ?, ?)" ); $statement->execute([ $userId, $entryType, number_format($amount, 2, ".", ""), number_format($frozenAmount, 2, ".", ""), number_format($availableAfter, 2, ".", ""), number_format($frozenAfter, 2, ".", ""), $note, ]); } function get_dashboard_stats(int $userId): array { $wallet = wallet_snapshot($userId); $pdo = db(); $todayStatement = $pdo->prepare( "SELECT COALESCE(SUM(amount), 0) AS today_earnings FROM wallet_logs WHERE user_id = ? AND entry_type = 'task_credit' AND DATE(created_at) = CURDATE()" ); $todayStatement->execute([$userId]); $todayEarnings = (float) ($todayStatement->fetchColumn() ?: 0); $monthStatement = $pdo->prepare( "SELECT COALESCE(SUM(amount), 0) AS month_earnings FROM wallet_logs WHERE user_id = ? AND entry_type = 'task_credit' AND DATE_FORMAT(created_at, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')" ); $monthStatement->execute([$userId]); $monthEarnings = (float) ($monthStatement->fetchColumn() ?: 0); $todayTaskStatement = $pdo->prepare( "SELECT COUNT(*) FROM task_orders WHERE user_id = ? AND DATE(claimed_at) = CURDATE()" ); $todayTaskStatement->execute([$userId]); $todayTasks = (int) ($todayTaskStatement->fetchColumn() ?: 0); $activeStatement = $pdo->prepare( "SELECT COUNT(*) FROM task_orders WHERE user_id = ? AND status IN ('claimed', 'pending_review')" ); $activeStatement->execute([$userId]); $activeOrders = (int) ($activeStatement->fetchColumn() ?: 0); $completedStatement = $pdo->prepare( "SELECT COUNT(*) FROM task_orders WHERE user_id = ? AND status = 'approved'" ); $completedStatement->execute([$userId]); $completedOrders = (int) ($completedStatement->fetchColumn() ?: 0); $totalBalance = (float) $wallet["available_balance"] + (float) $wallet["frozen_balance"]; return [ "available_balance" => (float) $wallet["available_balance"], "frozen_balance" => (float) $wallet["frozen_balance"], "total_balance" => $totalBalance, "today_earnings" => $todayEarnings, "month_earnings" => $monthEarnings, "today_tasks" => $todayTasks, "active_orders" => $activeOrders, "completed_orders" => $completedOrders, ]; } function get_user_orders(int $userId, int $limit = 12): array { $statement = db()->prepare( "SELECT * FROM task_orders WHERE user_id = ? ORDER BY id DESC LIMIT ?" ); $statement->bindValue(1, $userId, PDO::PARAM_INT); $statement->bindValue(2, $limit, PDO::PARAM_INT); $statement->execute(); return $statement->fetchAll() ?: []; } function get_user_orders_for_task(int $userId, string $taskSlug, int $limit = 6): array { $statement = db()->prepare( "SELECT * FROM task_orders WHERE user_id = ? AND task_slug = ? ORDER BY id DESC LIMIT ?" ); $statement->bindValue(1, $userId, PDO::PARAM_INT); $statement->bindValue(2, $taskSlug, PDO::PARAM_STR); $statement->bindValue(3, $limit, PDO::PARAM_INT); $statement->execute(); return $statement->fetchAll() ?: []; } function get_wallet_logs(int $userId, int $limit = 20): array { $statement = db()->prepare( "SELECT * FROM wallet_logs WHERE user_id = ? ORDER BY id DESC LIMIT ?" ); $statement->bindValue(1, $userId, PDO::PARAM_INT); $statement->bindValue(2, $limit, PDO::PARAM_INT); $statement->execute(); return $statement->fetchAll() ?: []; } function get_user_vip_orders(int $userId, int $limit = 12): array { $statement = db()->prepare( "SELECT * FROM vip_orders WHERE user_id = ? ORDER BY COALESCE(completed_at, created_at) DESC, id DESC LIMIT ?" ); $statement->bindValue(1, $userId, PDO::PARAM_INT); $statement->bindValue(2, $limit, PDO::PARAM_INT); $statement->execute(); return $statement->fetchAll() ?: []; } function get_vip_order_by_id(int $vipOrderId, ?int $userId = null): ?array { $sql = "SELECT * FROM vip_orders WHERE id = ?"; $params = [$vipOrderId]; if ($userId !== null) { $sql .= " AND user_id = ?"; $params[] = $userId; } $sql .= " LIMIT 1"; $statement = db()->prepare($sql); $statement->execute($params); $order = $statement->fetch(); return $order ?: null; } function purchase_vip_upgrade(int $userId, int $targetLevel): array { $catalog = vip_catalog(); if ($targetLevel <= 0 || !isset($catalog[$targetLevel])) { throw new RuntimeException("请选择有效的 VIP 等级。"); } $pdo = db(); $pdo->beginTransaction(); try { $userStatement = $pdo->prepare( "SELECT * FROM users WHERE id = ? LIMIT 1 FOR UPDATE" ); $userStatement->execute([$userId]); $user = $userStatement->fetch(); if (!$user) { throw new RuntimeException("用户不存在,请重新登录后再试。"); } $currentLevel = (int) $user["vip_level"]; if ($targetLevel <= $currentLevel) { throw new RuntimeException("当前账号已开通 " . vip_info($currentLevel)["name"] . ",请选择更高等级。"); } $walletStatement = $pdo->prepare( "SELECT available_balance, frozen_balance FROM wallets WHERE user_id = ? LIMIT 1 FOR UPDATE" ); $walletStatement->execute([$userId]); $wallet = $walletStatement->fetch(); if (!$wallet) { throw new RuntimeException("钱包不存在,请重新登录后再试。"); } $targetVip = $catalog[$targetLevel]; $price = (float) $targetVip["price"]; $availableBefore = (float) $wallet["available_balance"]; $frozenAfter = (float) $wallet["frozen_balance"]; if ($availableBefore < $price) { $gap = $price - $availableBefore; throw new RuntimeException("当前可用余额不足,还差 " . number_format($gap, 2) . " USDT,请先充值后再开通。"); } $availableAfter = $availableBefore - $price; $note = vip_info($currentLevel)["name"] . " 升级到 " . $targetVip["name"] . ",系统已从可用余额扣款。"; $updateUser = $pdo->prepare("UPDATE users SET vip_level = ? WHERE id = ?"); $updateUser->execute([$targetLevel, $userId]); $updateWallet = $pdo->prepare( "UPDATE wallets SET available_balance = ?, updated_at = NOW() WHERE user_id = ?" ); $updateWallet->execute([ number_format($availableAfter, 2, ".", ""), $userId, ]); $insertOrder = $pdo->prepare( "INSERT INTO vip_orders ( user_id, from_level, to_level, price_usdt, status, available_after, note, completed_at ) VALUES (?, ?, ?, ?, 'completed', ?, ?, NOW())" ); $insertOrder->execute([ $userId, $currentLevel, $targetLevel, number_format($price, 2, ".", ""), number_format($availableAfter, 2, ".", ""), $note, ]); $vipOrderId = (int) $pdo->lastInsertId(); insert_wallet_log( $pdo, $userId, "vip_upgrade", -$price, 0, $note, $availableAfter, $frozenAfter ); $pdo->commit(); return get_vip_order_by_id($vipOrderId, $userId) ?? [ "id" => $vipOrderId, "from_level" => $currentLevel, "to_level" => $targetLevel, "price_usdt" => $price, "available_after" => $availableAfter, "status" => "completed", "note" => $note, ]; } catch (Throwable $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $exception; } } function active_order_for_task(int $userId, string $taskSlug): ?array { $statement = db()->prepare( "SELECT * FROM task_orders WHERE user_id = ? AND task_slug = ? AND status IN ('claimed', 'pending_review') ORDER BY id DESC LIMIT 1" ); $statement->execute([$userId, $taskSlug]); $order = $statement->fetch(); return $order ?: null; } function get_order_by_id(int $orderId, ?int $userId = null): ?array { $sql = "SELECT o.*, u.email, u.user_code, u.vip_level FROM task_orders o INNER JOIN users u ON u.id = o.user_id WHERE o.id = ?"; $params = [$orderId]; if ($userId !== null) { $sql .= " AND o.user_id = ?"; $params[] = $userId; } $sql .= " LIMIT 1"; $statement = db()->prepare($sql); $statement->execute($params); $order = $statement->fetch(); return $order ?: null; } function can_access_task(array $user, array $task): bool { return (int) $user["vip_level"] >= (int) $task["vip_required"]; } function create_task_order(int $userId, array $task): array { $user = fetch_user($userId); if (!$user) { throw new RuntimeException("用户不存在,请重新登录。"); } if (!can_access_task($user, $task)) { throw new RuntimeException("当前 VIP 等级不足,暂不能领取该任务。"); } if (active_order_for_task($userId, (string) $task["slug"])) { throw new RuntimeException("你已经有一个进行中的同类任务,请先完成当前任务。"); } $statement = db()->prepare( "INSERT INTO task_orders ( user_id, task_slug, task_title, task_type, platform_name, reward_usdt, vip_required, countdown_seconds, status ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'claimed')" ); $statement->execute([ $userId, $task["slug"], $task["title"], $task["type"], $task["platform"], number_format((float) $task["reward"], 2, ".", ""), (int) $task["vip_required"], (int) $task["countdown"], ]); $orderId = (int) db()->lastInsertId(); return get_order_by_id($orderId, $userId) ?? []; } function is_submit_unlocked(array $order): bool { $claimedTimestamp = strtotime((string) $order["claimed_at"]); if ($claimedTimestamp === false) { return false; } return time() >= $claimedTimestamp + (int) $order["countdown_seconds"]; } function submit_task_order(int $userId, int $orderId, string $proofNote): array { $proofNote = trim($proofNote); if (text_length($proofNote) < 6) { throw new RuntimeException("请填写至少 6 个字符的完成备注,方便后台审核。"); } $pdo = db(); $pdo->beginTransaction(); try { $statement = $pdo->prepare( "SELECT * FROM task_orders WHERE id = ? AND user_id = ? LIMIT 1 FOR UPDATE" ); $statement->execute([$orderId, $userId]); $order = $statement->fetch(); if (!$order) { throw new RuntimeException("任务订单不存在。"); } if ((string) $order["status"] !== "claimed") { throw new RuntimeException("当前任务状态不可提交。"); } if (!is_submit_unlocked($order)) { throw new RuntimeException("倒计时尚未结束,请等待后再提交。"); } $walletStatement = $pdo->prepare( "SELECT available_balance, frozen_balance FROM wallets WHERE user_id = ? LIMIT 1 FOR UPDATE" ); $walletStatement->execute([$userId]); $wallet = $walletStatement->fetch(); if (!$wallet) { throw new RuntimeException("钱包不存在,请重新登录。"); } $availableAfter = (float) $wallet["available_balance"]; $frozenAfter = (float) $wallet["frozen_balance"] + (float) $order["reward_usdt"]; $updateOrder = $pdo->prepare( "UPDATE task_orders SET status = 'pending_review', proof_note = ?, submitted_at = NOW() WHERE id = ?" ); $updateOrder->execute([$proofNote, $orderId]); $updateWallet = $pdo->prepare( "UPDATE wallets SET frozen_balance = ?, updated_at = NOW() WHERE user_id = ?" ); $updateWallet->execute([number_format($frozenAfter, 2, ".", ""), $userId]); insert_wallet_log( $pdo, $userId, "task_freeze", 0, (float) $order["reward_usdt"], "任务已提交审核,佣金进入冻结余额。", $availableAfter, $frozenAfter ); $pdo->commit(); return get_order_by_id($orderId, $userId) ?? []; } catch (Throwable $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $exception; } } function review_task_order(int $orderId, string $decision, string $note = ""): array { $decision = $decision === "approve" ? "approved" : "rejected"; $note = trim($note); $pdo = db(); $pdo->beginTransaction(); try { $orderStatement = $pdo->prepare( "SELECT * FROM task_orders WHERE id = ? LIMIT 1 FOR UPDATE" ); $orderStatement->execute([$orderId]); $order = $orderStatement->fetch(); if (!$order) { throw new RuntimeException("待审核订单不存在。"); } if ((string) $order["status"] !== "pending_review") { throw new RuntimeException("该订单已处理,无需重复审核。"); } $walletStatement = $pdo->prepare( "SELECT available_balance, frozen_balance FROM wallets WHERE user_id = ? LIMIT 1 FOR UPDATE" ); $walletStatement->execute([(int) $order["user_id"]]); $wallet = $walletStatement->fetch(); if (!$wallet) { throw new RuntimeException("钱包记录不存在。"); } $reward = (float) $order["reward_usdt"]; $availableAfter = (float) $wallet["available_balance"]; $frozenAfter = max(0, (float) $wallet["frozen_balance"] - $reward); $entryType = "task_reject"; $logAmount = 0.0; $logFrozen = -$reward; $defaultNote = "任务审核未通过,冻结佣金已释放。"; if ($decision === "approved") { $availableAfter += $reward; $entryType = "task_credit"; $logAmount = $reward; $defaultNote = "任务审核通过,佣金已转入可提现余额。"; } $updateOrder = $pdo->prepare( "UPDATE task_orders SET status = ?, reviewed_at = NOW(), review_note = ? WHERE id = ?" ); $updateOrder->execute([$decision, $note !== "" ? $note : $defaultNote, $orderId]); $updateWallet = $pdo->prepare( "UPDATE wallets SET available_balance = ?, frozen_balance = ?, updated_at = NOW() WHERE user_id = ?" ); $updateWallet->execute([ number_format($availableAfter, 2, ".", ""), number_format($frozenAfter, 2, ".", ""), (int) $order["user_id"], ]); insert_wallet_log( $pdo, (int) $order["user_id"], $entryType, $logAmount, $logFrozen, $note !== "" ? $note : $defaultNote, $availableAfter, $frozenAfter ); $pdo->commit(); return get_order_by_id($orderId, null) ?? []; } catch (Throwable $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $exception; } } function get_operator_orders(string $status, int $limit = 20): array { $statement = db()->prepare( "SELECT o.*, u.email, u.user_code, u.vip_level FROM task_orders o INNER JOIN users u ON u.id = o.user_id WHERE o.status = ? ORDER BY COALESCE(o.submitted_at, o.claimed_at) DESC LIMIT ?" ); $statement->bindValue(1, $status, PDO::PARAM_STR); $statement->bindValue(2, $limit, PDO::PARAM_INT); $statement->execute(); return $statement->fetchAll() ?: []; } function get_recent_reviewed_orders(int $limit = 12): array { $statement = db()->prepare( "SELECT o.*, u.email, u.user_code, u.vip_level FROM task_orders o INNER JOIN users u ON u.id = o.user_id WHERE o.status IN ('approved', 'rejected') ORDER BY o.reviewed_at DESC, o.id DESC LIMIT ?" ); $statement->bindValue(1, $limit, PDO::PARAM_INT); $statement->execute(); return $statement->fetchAll() ?: []; } function get_operator_stats(): array { $pdo = db(); $todayUsers = (int) ($pdo->query("SELECT COUNT(*) FROM users WHERE DATE(created_at) = CURDATE()")->fetchColumn() ?: 0); $pending = (int) ($pdo->query("SELECT COUNT(*) FROM task_orders WHERE status = 'pending_review'")->fetchColumn() ?: 0); $completed = (int) ($pdo->query("SELECT COUNT(*) FROM task_orders WHERE status = 'approved'")->fetchColumn() ?: 0); $credited = (float) ($pdo->query("SELECT COALESCE(SUM(amount), 0) FROM wallet_logs WHERE entry_type = 'task_credit'")->fetchColumn() ?: 0); return [ "today_users" => $todayUsers, "pending_reviews" => $pending, "approved_orders" => $completed, "credited_amount" => $credited, ]; } function status_label(string $status): string { return [ "claimed" => "进行中", "pending_review" => "待审核", "approved" => "已入账", "rejected" => "已驳回", ][$status] ?? $status; } function status_tone(string $status): string { return [ "claimed" => "muted", "pending_review" => "warning", "approved" => "success", "rejected" => "danger", ][$status] ?? "muted"; } function wallet_entry_label(string $entryType): string { return [ "wallet_opened" => "钱包创建", "task_freeze" => "冻结佣金", "task_credit" => "佣金到账", "task_reject" => "审核释放", "vip_upgrade" => "VIP 开通", ][$entryType] ?? $entryType; } function render_status_badge(string $status): string { return '' . h(status_label($status)) . ''; } function front_nav_items(): array { return [ ["key" => "home", "label" => "首页", "href" => "index.php", "icon" => "home"], ["key" => "tasks", "label" => "任务", "href" => "task.php", "icon" => "tasks"], ["key" => "vip", "label" => "VIP", "href" => "vip.php", "icon" => "vip"], ["key" => "wallet", "label" => "钱包", "href" => "wallet.php", "icon" => "wallet"], ["key" => "profile", "label" => "我的", "href" => "profile.php", "icon" => "profile"], ]; } function is_front_app_nav(string $active): bool { foreach (front_nav_items() as $item) { if ($item["key"] === $active) { return true; } } return false; } function app_user_name(?array $user): string { if (!$user) { return 'User'; } $email = (string) ($user['email'] ?? ''); $local = strstr($email, '@', true); if ($local !== false && $local !== '') { return $local; } return 'ID ' . (string) ($user['user_code'] ?? '000000'); } function app_user_initial(?array $user): string { $name = app_user_name($user); $first = strtoupper(substr($name, 0, 1)); return $first !== '' ? $first : 'U'; } function task_category_options(): array { return [ 'all' => '全部', 'video' => '视频', 'like' => '点赞', 'social' => '社媒', 'web' => '网站', 'app' => '应用', ]; } function task_visual_meta(array $task): array { $map = [ 'watch-video' => [ 'brand' => 'YouTube', 'icon' => 'play', 'accent' => 'accent-red', 'category' => 'video', 'tag' => '视频', 'hint' => '观看视频', ], 'social-like' => [ 'brand' => 'TikTok', 'icon' => 'heart', 'accent' => 'accent-pink', 'category' => 'like', 'tag' => '点赞', 'hint' => '点赞互动', ], 'follow-brand' => [ 'brand' => 'Instagram', 'icon' => 'spark', 'accent' => 'accent-violet', 'category' => 'social', 'tag' => '社媒', 'hint' => '关注账号', ], 'comment-flow' => [ 'brand' => 'Community', 'icon' => 'chat', 'accent' => 'accent-cyan', 'category' => 'social', 'tag' => '社媒', 'hint' => '评论互动', ], 'site-browse' => [ 'brand' => 'Website', 'icon' => 'globe', 'accent' => 'accent-blue', 'category' => 'web', 'tag' => '网站', 'hint' => '网站浏览', ], 'app-download' => [ 'brand' => 'App Center', 'icon' => 'download', 'accent' => 'accent-orange', 'category' => 'app', 'tag' => '应用', 'hint' => '下载体验', ], ]; $slug = (string) ($task['slug'] ?? ''); $fallback = [ 'brand' => (string) ($task['platform'] ?? 'Task'), 'icon' => 'spark', 'accent' => 'accent-violet', 'category' => 'all', 'tag' => (string) ($task['type'] ?? '任务'), 'hint' => (string) ($task['summary'] ?? '任务返佣'), ]; return $map[$slug] ?? $fallback; } function app_icon_svg(string $icon): string { $icons = [ 'home' => '', 'tasks' => '', 'vip' => '', 'wallet' => '', 'profile' => '', 'play' => '', 'deposit' => '', 'withdraw' => '', 'swap' => '', 'history' => '', 'heart' => '', 'spark' => '', 'chat' => '', 'globe' => '', 'download' => '', 'check' => '', 'lock' => '', 'support' => '', 'chevron-right' => '', ]; $paths = $icons[$icon] ?? $icons['spark']; return ''; } function render_head(string $pageTitle, string $pageDescription = ""): void { $siteName = project_name(); $description = trim($pageDescription) !== "" ? $pageDescription : project_description(); $fullTitle = trim($pageTitle) !== "" ? $pageTitle . " · " . $siteName : $siteName; $projectDescription = $_SERVER["PROJECT_DESCRIPTION"] ?? ""; $projectImageUrl = $_SERVER["PROJECT_IMAGE_URL"] ?? ""; ?> <?= h($fullTitle) ?> " rel="stylesheet" /> "ops", "label" => "运营台", "href" => "operations.php"], ["key" => "health", "label" => "Healthz", "href" => "healthz.php"], ]); ?>