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"],
]);
?>
= nl2br(h((string) ($flash['message'] ?? ''))) ?>