0, 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); } function db(): PDO { static $pdo; if ($pdo instanceof PDO) { return $pdo; } global $config; $db = $config['db']; $dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=%s', $db['host'], $db['port'], $db['database'], $db['charset']); $pdo = new PDO($dsn, $db['username'], $db['password'], [ PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); return $pdo; } function now(): string { return date('Y-m-d H:i:s'); } function e(mixed $value): string { return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function csrf_token(): string { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(24)); } return $_SESSION['csrf']; } function verify_csrf(): void { $token = (string) ($_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''); if (!hash_equals((string) ($_SESSION['csrf'] ?? ''), $token)) { throw new RuntimeException('页面已过期,请刷新后重试。'); } } function setting(string $key, string $default = ''): string { $stmt = db()->prepare('SELECT `value` FROM settings WHERE `key` = ?'); $stmt->execute([$key]); $value = $stmt->fetchColumn(); return $value === false ? $default : (string) $value; } function current_user(bool $fresh = false): ?array { static $cached = false; if (!$fresh && is_array($cached)) { return $cached; } $id = (int) ($_SESSION['user_id'] ?? 0); if ($id < 1) { return null; } $stmt = db()->prepare('SELECT * FROM users WHERE id = ?'); $stmt->execute([$id]); $user = $stmt->fetch(); if (!$user || (int) $user['status'] !== 1) { unset($_SESSION['user_id']); return null; } $cached = $user; return $user; } function current_admin(): ?array { $id = (int) ($_SESSION['admin_id'] ?? 0); if ($id < 1) { return null; } $stmt = db()->prepare('SELECT id, username FROM admins WHERE id = ?'); $stmt->execute([$id]); return $stmt->fetch() ?: null; } function require_user(): array { $user = current_user(); if (!$user) { header('Location: /index.php?view=login'); exit; } return $user; } function require_admin(): array { $admin = current_admin(); if (!$admin) { header('Location: /admin/login.php'); exit; } return $admin; } function flash(string $type, string $message): void { $_SESSION['flash'] = ['type' => $type, 'message' => $message]; } function take_flash(): ?array { $flash = $_SESSION['flash'] ?? null; unset($_SESSION['flash']); return is_array($flash) ? $flash : null; } function redirect(string $url): never { header('Location: ' . $url); exit; } function client_ip(): string { return substr((string) ($_SERVER['REMOTE_ADDR'] ?? ''), 0, 45); } function auth_limit_key(string $identifier): string { return hash('sha256', mb_strtolower(trim($identifier))); } function assert_auth_not_limited(string $scope, string $identifier, int $maximum = 5, int $windowSeconds = 900): void { $cutoff = date('Y-m-d H:i:s', time() - $windowSeconds); $stmt = db()->prepare('SELECT COUNT(*) FROM auth_attempts WHERE scope=? AND succeeded=0 AND attempted_at>=? AND (identifier_hash=? OR ip_address=?)'); $stmt->execute([$scope, $cutoff, auth_limit_key($identifier), client_ip()]); if ((int) $stmt->fetchColumn() >= $maximum) { throw new RuntimeException('尝试次数过多,请稍后再试。'); } } function record_auth_attempt(string $scope, string $identifier, bool $succeeded): void { $pdo = db(); $key = auth_limit_key($identifier); $ip = client_ip(); if ($succeeded) { $stmt = $pdo->prepare('DELETE FROM auth_attempts WHERE scope=? AND (identifier_hash=? OR ip_address=?)'); $stmt->execute([$scope, $key, $ip]); return; } $stmt = $pdo->prepare('INSERT INTO auth_attempts(scope,identifier_hash,ip_address,succeeded,attempted_at) VALUES(?,?,?,0,?)'); $stmt->execute([$scope, $key, $ip, now()]); if (random_int(1, 100) === 1) { $pdo->prepare('DELETE FROM auth_attempts WHERE attempted_atexecute([date('Y-m-d H:i:s', time() - 604800)]); } } function admin_audit(PDO $pdo, string $action, string $targetType = '', string|int $targetId = '', mixed $before = null, mixed $after = null): void { $adminId = (int) ($_SESSION['admin_id'] ?? 0); if ($adminId < 1) return; $encode = static fn(mixed $value): ?string => $value === null ? null : json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR); $stmt = $pdo->prepare('INSERT INTO admin_audit_logs(admin_id,action,target_type,target_id,before_json,after_json,ip_address,created_at) VALUES(?,?,?,?,?,?,?,?)'); $stmt->execute([$adminId, mb_substr($action, 0, 50), mb_substr($targetType, 0, 40), mb_substr((string) $targetId, 0, 64), $encode($before), $encode($after), client_ip(), now()]); } function create_ledger(PDO $pdo, int $userId, string $type, float $amount, float $before, float $after, string $note, ?string $reference = null): void { $stmt = $pdo->prepare('INSERT INTO balance_logs (user_id, type, amount, balance_before, balance_after, note, reference_no, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'); $stmt->execute([$userId, $type, $amount, $before, $after, $note, $reference, now()]); } function order_no(string $prefix = 'RC'): string { return $prefix . date('YmdHis') . strtoupper(bin2hex(random_bytes(3))); } function generate_invite_code(PDO $pdo): string { $stmt = $pdo->prepare('SELECT 1 FROM users WHERE invite_code=?'); do { $code = strtoupper(bin2hex(random_bytes(5))); $stmt->execute([$code]); } while ($stmt->fetchColumn()); return $code; } function redeem_recharge_card(int $userId, string $code, string $note = '充值卡兑换'): array { $code = strtoupper(trim($code)); if (!preg_match('/^[A-Z0-9]{4,32}$/', $code)) throw new RuntimeException('充值卡不存在或不可用。'); $pdo = db(); $pdo->beginTransaction(); try { $lock = ' FOR UPDATE'; $stmt = $pdo->prepare('SELECT * FROM recharge_cards WHERE code=?'.$lock); $stmt->execute([$code]); $card = $stmt->fetch(); if (!$card) throw new RuntimeException('充值卡不存在或不可用。'); $isShared = ($card['card_type'] ?? 'single') === 'shared'; if ($isShared) { if ($card['status'] !== 'active') throw new RuntimeException('该公共充值卡已停用。'); $stmt = $pdo->prepare('SELECT 1 FROM recharge_card_redemptions WHERE card_id=? AND user_id=?'); $stmt->execute([$card['id'], $userId]); if ($stmt->fetchColumn()) throw new RuntimeException('您已经使用过该公共充值卡,每个账号仅可兑换一次。'); } elseif ($card['status'] !== 'unused') { throw new RuntimeException('充值卡不存在或已使用。'); } $stmt = $pdo->prepare('SELECT balance FROM users WHERE id=?'.$lock); $stmt->execute([$userId]); $balance = $stmt->fetchColumn(); if ($balance === false) throw new RuntimeException('用户不存在。'); $amount = round((float)$card['amount'], 2); if ($amount <= 0) throw new RuntimeException('充值卡金额无效。'); $before = (float)$balance; $after = round($before + $amount, 2); if ($isShared) { try { $pdo->prepare('INSERT INTO recharge_card_redemptions(card_id,user_id,amount,created_at) VALUES(?,?,?,?)')->execute([$card['id'], $userId, $amount, now()]); } catch (PDOException $e) { if ((string)$e->getCode() === '23000') throw new RuntimeException('您已经使用过该公共充值卡,每个账号仅可兑换一次。'); throw $e; } } else { $pdo->prepare("UPDATE recharge_cards SET status='used',used_by=?,used_at=? WHERE id=? AND status='unused'")->execute([$userId, now(), $card['id']]); } $pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after, now(), $userId]); $reference = $isShared ? 'SC'.$card['id'].'U'.$userId : $code; create_ledger($pdo, $userId, $isShared ? 'shared_card_recharge' : 'card_recharge', $amount, $before, $after, $note, $reference); credit_recharge_commissions($pdo, $userId, $amount, $isShared ? 'shared_card' : 'card', $reference); $pdo->commit(); return ['amount'=>$amount, 'card_type'=>$isShared ? 'shared' : 'single']; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $e; } } function create_user_account(string $username, string $password, string $phone = '', string $inviteCode = ''): array { $pdo = db(); $ip = client_ip(); $inviteCode = strtoupper(trim($inviteCode)); $pdo->beginTransaction(); try { $lock = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? ' FOR UPDATE' : ''; $inviter = null; if ($inviteCode !== '') { $stmt = $pdo->prepare('SELECT * FROM users WHERE invite_code=? AND status=1'.$lock); $stmt->execute([$inviteCode]); $inviter = $stmt->fetch(); if (!$inviter) throw new RuntimeException('邀请码无效,请核对后重试。'); } $bonus = max(0, (float)setting('register_bonus', '0')); $code = generate_invite_code($pdo); try { $stmt = $pdo->prepare('INSERT INTO users(username,password_hash,phone,invite_code,inviter_id,register_ip,balance,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,1,?,?)'); $stmt->execute([$username,password_hash($password,PASSWORD_DEFAULT),$phone,$code,$inviter['id']??null,$ip,$bonus,now(),now()]); } catch (PDOException $e) { if ((string)$e->getCode() === '23000') throw new RuntimeException('该账号已被注册。'); throw $e; } $userId = (int)$pdo->lastInsertId(); if ($bonus > 0) create_ledger($pdo,$userId,'register_bonus',$bonus,0,$bonus,'新用户注册奖励'); $referralStatus = null; if ($inviter) { $reward = round(max(0,(float)setting('invite_reward','0')),2); $inviterIp = (string)($inviter['register_ip'] ?: $inviter['last_login_ip']); $status = $reward > 0 ? 'rewarded' : 'qualified'; $reason = ''; if ($ip === '') { $status = 'rejected'; $reason = '无法识别注册 IP'; } elseif ($inviterIp !== '' && hash_equals($inviterIp,$ip)) { $status = 'rejected'; $reason = '邀请人与被邀请人 IP 相同'; } elseif ($reward <= 0) { $reason = '注册奖励未启用'; } $qualified = false; if (in_array($status,['rewarded','qualified'],true)) { try { $pdo->prepare('INSERT INTO referral_rewards(inviter_id,invitee_id,invitee_ip,reward_ip,reward_amount,status,reason,created_at) VALUES(?,?,?,?,?,?,?,?)') ->execute([$inviter['id'],$userId,$ip,$ip,$status === 'rewarded' ? $reward : 0,$status,$reason,now()]); $qualified = true; } catch (PDOException $e) { if ((string)$e->getCode() !== '23000') throw $e; $status = 'rejected'; $reason = '该 IP 已建立过有效邀请关系'; } } if (!$qualified) { $pdo->prepare('INSERT INTO referral_rewards(inviter_id,invitee_id,invitee_ip,reward_ip,reward_amount,status,reason,created_at) VALUES(?,?,?,NULL,0,?,?,?)') ->execute([$inviter['id'],$userId,$ip,$status,$reason,now()]); } elseif ($status === 'rewarded') { $before = (float)$inviter['balance']; $after = $before + $reward; $pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after,now(),$inviter['id']]); create_ledger($pdo,(int)$inviter['id'],'invite_reward',$reward,$before,$after,'成功邀请用户 '.$username,'INV'.$userId); } $referralStatus = $status; } $pdo->commit(); return ['user_id'=>$userId,'invite_code'=>$code,'referral_status'=>$referralStatus]; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $e; } } function referral_link_qualified(PDO $pdo, int $inviteeId, int $inviterId): bool { $stmt = $pdo->prepare("SELECT status FROM referral_rewards WHERE invitee_id=? AND inviter_id=? AND status IN ('rewarded','qualified')"); $stmt->execute([$inviteeId,$inviterId]); return (bool)$stmt->fetchColumn(); } function credit_recharge_commissions(PDO $pdo, int $userId, float $amount, string $sourceType, string $sourceRef): void { if ($amount <= 0) return; $lock = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? ' FOR UPDATE' : ''; $stmt = $pdo->prepare('SELECT id,inviter_id FROM users WHERE id=?'.$lock); $stmt->execute([$userId]); $rechargingUser = $stmt->fetch(); if (!$rechargingUser || !(int)$rechargingUser['inviter_id']) return; $directId = (int)$rechargingUser['inviter_id']; if (!referral_link_qualified($pdo,$userId,$directId)) return; credit_recharge_commission($pdo,$userId,$directId,1,$amount,(float)setting('direct_commission_rate','10'),$sourceType,$sourceRef); $stmt = $pdo->prepare('SELECT inviter_id FROM users WHERE id=?'.$lock); $stmt->execute([$directId]); $secondId = (int)$stmt->fetchColumn(); if ($secondId > 0 && referral_link_qualified($pdo,$directId,$secondId)) { credit_recharge_commission($pdo,$userId,$secondId,2,$amount,(float)setting('second_commission_rate','1'),$sourceType,$sourceRef); } } function credit_recharge_commission(PDO $pdo, int $rechargingUserId, int $beneficiaryId, int $level, float $rechargeAmount, float $rate, string $sourceType, string $sourceRef): void { $rate = min(100.0,max(0.0,$rate)); $commission = round($rechargeAmount * $rate / 100,2); if ($commission <= 0) return; try { $stmt = $pdo->prepare('INSERT INTO recharge_commissions(source_type,source_ref,recharging_user_id,beneficiary_user_id,level,rate,recharge_amount,commission_amount,created_at) VALUES(?,?,?,?,?,?,?,?,?)'); $stmt->execute([$sourceType,mb_substr($sourceRef,0,40),$rechargingUserId,$beneficiaryId,$level,$rate,$rechargeAmount,$commission,now()]); } catch (PDOException $e) { if ((string)$e->getCode() === '23000') return; throw $e; } $commissionId = (int)$pdo->lastInsertId(); $lock = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? ' FOR UPDATE' : ''; $stmt = $pdo->prepare('SELECT balance FROM users WHERE id=?'.$lock); $stmt->execute([$beneficiaryId]); $before = (float)$stmt->fetchColumn(); $after = $before + $commission; $pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after,now(),$beneficiaryId]); create_ledger($pdo,$beneficiaryId,'recharge_commission_l'.$level,$commission,$before,$after,$level===1?'一级下线充值佣金':'二级下线充值佣金','COM'.$commissionId); } function reverse_recharge_order(PDO $pdo, int $orderId, string $note): void { $note = mb_substr(trim($note), 0, 200); if ($note === '') throw new RuntimeException('请填写冲正原因。'); $pdo->beginTransaction(); try { $lock = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql' ? ' FOR UPDATE' : ''; $stmt = $pdo->prepare('SELECT * FROM recharge_orders WHERE id=?'.$lock); $stmt->execute([$orderId]); $order = $stmt->fetch(); if (!$order || $order['status'] !== 'approved') throw new RuntimeException('仅可冲正已到账且未冲正的订单。'); $stmt = $pdo->prepare("SELECT * FROM recharge_commissions WHERE source_type='order' AND source_ref=? AND reversed_at IS NULL ORDER BY id".$lock); $stmt->execute([$order['order_no']]); $commissions = $stmt->fetchAll(); $deductions = [(int) $order['user_id'] => (float) $order['amount']]; foreach ($commissions as $commission) { $beneficiaryId = (int) $commission['beneficiary_user_id']; $deductions[$beneficiaryId] = ($deductions[$beneficiaryId] ?? 0) + (float) $commission['commission_amount']; } ksort($deductions); $ids = array_keys($deductions); $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare('SELECT id,balance FROM users WHERE id IN ('.$placeholders.') ORDER BY id'.$lock); $stmt->execute($ids); $balances = []; foreach ($stmt->fetchAll() as $row) $balances[(int) $row['id']] = (float) $row['balance']; foreach ($deductions as $userId => $amount) { if (!array_key_exists($userId, $balances) || $balances[$userId] + 0.00001 < $amount) { throw new RuntimeException('相关用户余额不足,无法自动冲正,请先核对资金。'); } } foreach ($commissions as $commission) { $userId = (int) $commission['beneficiary_user_id']; $amount = (float) $commission['commission_amount']; $before = $balances[$userId]; $after = round($before - $amount, 2); $pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after, now(), $userId]); $pdo->prepare('UPDATE recharge_commissions SET reversed_by=?,reversed_at=? WHERE id=? AND reversed_at IS NULL')->execute([(int) $_SESSION['admin_id'], now(), $commission['id']]); create_ledger($pdo, $userId, 'commission_reversal', -$amount, $before, $after, '充值佣金冲正', 'RVC'.$commission['id']); $balances[$userId] = $after; } $userId = (int) $order['user_id']; $amount = (float) $order['amount']; $before = $balances[$userId]; $after = round($before - $amount, 2); $pdo->prepare('UPDATE users SET balance=?,updated_at=? WHERE id=?')->execute([$after, now(), $userId]); create_ledger($pdo, $userId, 'recharge_reversal', -$amount, $before, $after, '充值订单冲正:'.$note, 'RV'.$order['order_no']); $pdo->prepare("UPDATE recharge_orders SET status='reversed',reversed_by=?,reversed_at=?,reversal_note=? WHERE id=? AND status='approved'")->execute([(int) $_SESSION['admin_id'], now(), $note, $orderId]); admin_audit($pdo, 'recharge_reversal', 'recharge_order', $orderId, ['status' => 'approved', 'amount' => $amount], ['status' => 'reversed', 'reason' => $note]); $pdo->commit(); } catch (Throwable $exception) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $exception; } } function settings_map(): array { $rows = db()->query('SELECT `key`, `value` FROM settings')->fetchAll(); $settings = []; foreach ($rows as $row) { $settings[(string) $row['key']] = (string) $row['value']; } return $settings; }