文件
2026-08-16 17:03:10 +08:00

601 行
20 KiB
PHP

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
<?php
if (!defined('IN_APP')) {
http_response_code(403);
exit('Forbidden');
}
function h($str)
{
return htmlspecialchars((string) $str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function money($n)
{
return '¥' . number_format((float) $n, 2);
}
function redirect($url)
{
if (is_string($url)
&& (strpos($url, '
|| preg_match('
$url = 'index.php';
}
header('Location: ' . $url);
exit;
}
function getClientIp()
{
$candidates = [];
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$candidates = array_merge($candidates, explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
}
foreach (['HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $k) {
if (!empty($_SERVER[$k])) {
$candidates[] = $_SERVER[$k];
}
}
foreach ($candidates as $ip) {
$ip = trim($ip);
if ($ip !== '' && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
return trim((string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'));
}
function fnwHttpPost($url, $body, $timeout = 10)
{
$url = (string) $url;
if (!preg_match('
return null;
}
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
CURLOPT_POSTFIELDS => $body,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$r = curl_exec($ch);
curl_close($ch);
return $r === false ? null : (string) $r;
}
if (function_exists('stream_context_create')) {
$ctx = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\nAccept: application/json\r\n",
'content' => $body,
'timeout' => $timeout,
]]);
$r = @file_get_contents($url, false, $ctx);
return $r === false ? null : (string) $r;
}
return null;
}
function regCountToday($ip)
{
try {
$stmt = db()->prepare('SELECT COUNT(*) FROM ' . tn('users') . ' WHERE reg_ip = ? AND DATE(created_at) = CURDATE()');
$stmt->execute([$ip]);
return (int) $stmt->fetchColumn();
} catch (Throwable $e) {
return 0;
}
}
function siteBaseUrl()
{
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? '') == '443' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$dir = dirname($_SERVER['SCRIPT_NAME'] ?? '/index.php');
$dir = ($dir === '/' || $dir === '\\') ? '' : rtrim($dir, '/');
return $scheme . ':
}
function avatarText($name)
{
$name = trim((string) $name);
if ($name === '') {
return '?';
}
return mb_strtoupper(mb_substr($name, 0, 1, 'UTF-8'), 'UTF-8');
}
function sendVerifyMail($userId)
{
$stmt = db()->prepare('SELECT id, username, nickname, email FROM ' . tn('users') . ' WHERE id = ?');
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user || $user['email'] === '') {
return ['ok' => false, 'error' => '该账号没有可用邮箱', 'log' => []];
}
$token = bin2hex(random_bytes(24));
db()->prepare('UPDATE ' . tn('users') . ' SET email_token = ? WHERE id = ?')
->execute([$token, $user['id']]);
$url = siteBaseUrl() . '/verify.php?token=' . $token;
$name = $user['nickname'] ?: $user['username'];
$subject = '[' . SITE_NAME . '] 请验证你的邮箱';
$body = '<div style="font-family:Segoe UI,Helvetica,Arial,sans-serif;max-width:480px;margin:0 auto;padding:24px;'
. 'border:1px solid
. '<h2 style="color:
. '<p style="color:
. '<p style="color:
. '<p style="margin:18px 0;"><a href="' . h($url) . '" style="background:
. 'border-radius:999px;text-decoration:none;display:inline-block;font-weight:600;">验证邮箱</a></p>'
. '<p style="color:
. '<hr style="border:none;border-top:1px solid
. '<p style="color:
. '</div>';
return fnwSendMail($user['email'], $subject, $body);
}
function currentUser($forceRefresh = false)
{
if (empty($_SESSION['user_id'])) {
return null;
}
static $cache = null;
if ($cache !== null && !$forceRefresh) {
return $cache;
}
$cache = null;
$pdo = db();
$stmt = $pdo->prepare('SELECT id, username, email, nickname, verified, is_admin, status, points, invite_code, invited_by, avatar FROM ' . tn('users') . ' WHERE id = ?');
$stmt->execute([$_SESSION['user_id']]);
$row = $stmt->fetch();
if (!$row || (int) $row['status'] !== 1) {
unset($_SESSION['user_id']);
$cache = null;
return null;
}
$cache = $row;
return $cache;
}
function refreshCurrentUser()
{
currentUser(true);
}
function isLoggedIn()
{
return currentUser() !== null;
}
function currentAdmin()
{
if (empty($_SESSION['admin_id'])) {
return null;
}
static $cache = null;
if ($cache !== null) {
return $cache;
}
$pdo = db();
$stmt = $pdo->prepare('SELECT id, username, email, status FROM ' . tn('users') . ' WHERE id = ? AND is_admin = 1');
$stmt->execute([$_SESSION['admin_id']]);
$row = $stmt->fetch();
if (!$row || (int) $row['status'] !== 1) {
unset($_SESSION['admin_id']);
$cache = null;
return null;
}
$cache = $row;
return $cache;
}
function requireLogin($next = '')
{
if (!isLoggedIn()) {
$q = $next ? '?next=' . urlencode($next) : '';
redirect('login.php' . $q);
}
}
function requireAdmin()
{
if (empty($_SESSION['admin_id']) || !currentAdmin()) {
redirect('login.php');
}
}
function currentStaff()
{
if (empty($_SESSION['admin_id'])) {
return null;
}
static $cache = null;
if ($cache !== null) {
return $cache;
}
$pdo = db();
$stmt = $pdo->prepare(
'SELECT id, username, email, status, role, is_admin FROM ' . tn('users') . ' WHERE id = ? AND status = 1 AND role IN (\'admin\', \'cs\')'
);
$stmt->execute([$_SESSION['admin_id']]);
$row = $stmt->fetch();
if (!$row) {
unset($_SESSION['admin_id']);
$cache = null;
return null;
}
$cache = $row;
return $cache;
}
function requireStaff()
{
if (empty($_SESSION['admin_id']) || !currentStaff()) {
redirect('login.php');
}
}
function isAdminStaff()
{
$s = currentStaff();
return $s !== null && (int) $s['is_admin'] === 1;
}
function csrfToken()
{
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf'];
}
function csrfField()
{
return '<input type="hidden" name="csrf" value="' . csrfToken() . '">';
}
function verifyCsrf()
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf'] ?? '';
$sess = $_SESSION['csrf'] ?? '';
if ($token === '' || $sess === '' || !hash_equals($sess, $token)) {
http_response_code(403);
exit('CSRF 校验失败');
}
}
}
function verifyCsrfToken($token)
{
return !empty($token) && !empty($_SESSION['csrf']) && hash_equals((string)$_SESSION['csrf'], (string)$token);
}
function currentTheme()
{
return (!empty($_COOKIE['fnw_theme']) && $_COOKIE['fnw_theme'] === 'dark') ? 'dark' : 'light';
}
function orderStatusLabel($status)
{
$map = [
'pending' => '待处理',
'paid' => '已受理',
'shipped' => '已发货',
'completed' => '已完成',
'cancelled' => '已取消',
];
return $map[$status] ?? $status;
}
function getSetting($k, $default = '')
{
try {
$stmt = db()->prepare('SELECT v FROM ' . tn('settings') . ' WHERE k = ?');
$stmt->execute([$k]);
$v = $stmt->fetchColumn();
return $v === false ? $default : $v;
} catch (Throwable $e) {
return $default;
}
}
function saveSetting($k, $v)
{
db()->prepare(
'INSERT INTO ' . tn('settings') . ' (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = ?'
)->execute([$k, $v, $v]);
}
function smtpConfig()
{
static $c = null;
if ($c !== null) {
return $c;
}
$keys = ['smtp_host', 'smtp_port', 'smtp_enc', 'smtp_user', 'smtp_pass', 'smtp_from', 'smtp_fromname', 'notify_emails'];
$map = [];
try {
$pdo = db();
$ph = implode(',', array_fill(0, count($keys), '?'));
$stmt = $pdo->prepare('SELECT k, v FROM ' . tn('settings') . ' WHERE k IN (' . $ph . ')');
$stmt->execute($keys);
foreach ($stmt->fetchAll() as $row) {
$map[$row['k']] = $row['v'];
}
} catch (Throwable $e) {
}
$c = [];
foreach ($keys as $k) {
$c[$k] = $map[$k] ?? '';
}
return $c;
}
function fnwSendMail($to, $subject, $body)
{
$cfg = smtpConfig();
if (empty($cfg['smtp_host'])) {
return ['ok' => false, 'error' => 'SMTP 未配置'];
}
$rl = mailRateLimit($to);
if (!$rl['allowed']) {
return ['ok' => false, 'error' => '发送频率过高,请 ' . ceil($rl['wait'] / 60) . ' 分钟后再试'];
}
require_once __DIR__ . '/mailer.php';
$mailer = new SmtpMailer($cfg['smtp_host'], $cfg['smtp_port'], $cfg['smtp_enc'], $cfg['smtp_user'], $cfg['smtp_pass']);
$from = $cfg['smtp_from'] ?: $cfg['smtp_user'];
$ok = $mailer->send($to, $subject, $body, $from, $cfg['smtp_fromname'] ?: SITE_NAME);
if ($ok) {
return ['ok' => true, 'log' => $mailer->log];
}
return ['ok' => false, 'error' => $mailer->lastError, 'log' => $mailer->log];
}
function ticketStatusLabel($status)
{
$map = [
'open' => '待处理',
'pending' => '处理中',
'resolved' => '已解决',
'closed' => '已关闭',
];
return $map[$status] ?? $status;
}
function ticketPriorityLabel($p)
{
$map = [1 => '低', 2 => '普通', 3 => '高'];
return $map[(int) $p] ?? '普通';
}
function periodLabel($days)
{
$days = (int) $days;
if ($days <= 0) {
return '一次性';
}
if ($days % 365 === 0) {
return ($days / 365) . ' 年';
}
if ($days % 30 === 0) {
return ($days / 30) . ' 个月';
}
return $days . ' 天';
}
function ticketTypeLabel($t)
{
$map = [
'general' => '普通',
'renewal' => '续期申请',
];
return $map[$t] ?? '普通';
}
function expiryText($dt)
{
if ($dt === null || $dt === '') {
return '—';
}
return date('Y-m-d H:i', strtotime($dt));
}
function isExpired($dt)
{
if ($dt === null || $dt === '') {
return false;
}
return strtotime($dt) < time();
}
function expiryState($dt)
{
if ($dt === null || $dt === '' || $dt === '0000-00-00 00:00:00') {
return ['has' => false, 'text' => '—', 'days' => null, 'level' => 'none'];
}
$ts = strtotime($dt);
if ($ts === false) {
return ['has' => false, 'text' => '—', 'days' => null, 'level' => 'none'];
}
$now = time();
$days = ($ts - $now) / 86400;
$daysLeft = (int) ceil($days);
if ($ts < $now) {
return ['has' => true, 'text' => '已到期(' . date('Y-m-d H:i', $ts) . '', 'days' => $daysLeft, 'level' => 'expired'];
}
$level = $daysLeft <= 3 ? 'danger' : ($daysLeft <= 7 ? 'warn' : 'ok');
return ['has' => true, 'text' => date('Y-m-d H:i', $ts) . '(剩 ' . $daysLeft . ' 天)', 'days' => $daysLeft, 'level' => $level];
}
function expiryBadge($dt)
{
$s = expiryState($dt);
if (!$s['has']) {
return '<span class="exp-badge exp-none">无到期时间</span>';
}
$icon = [
'ok' => 'fa-calendar-check',
'warn' => 'fa-clock',
'danger' => 'fa-triangle-exclamation',
'expired' => 'fa-circle-xmark',
][$s['level']] ?? 'fa-calendar';
return '<span class="exp-badge exp-' . $s['level'] . '"><i class="fas ' . $icon . '"></i> ' . h($s['text']) . '</span>';
}
function payTypeLabel($t)
{
$map = [
'cash' => '现金(历史)',
'points' => '积分支付',
'direct' => '直接下单',
];
return $map[$t] ?? '直接下单';
}
function getUserPoints($uid)
{
try {
$stmt = db()->prepare('SELECT points FROM ' . tn('users') . ' WHERE id = ?');
$stmt->execute([(int) $uid]);
$v = $stmt->fetchColumn();
return $v === false ? 0 : (int) $v;
} catch (Throwable $e) {
return 0;
}
}
function addPoints($uid, $type, $amount, $remark = '')
{
$uid = (int) $uid;
$amount = (int) $amount;
try {
$pdo = db();
$pdo->beginTransaction();
$stmt = $pdo->prepare('SELECT points FROM ' . tn('users') . ' WHERE id = ? FOR UPDATE');
$stmt->execute([$uid]);
$cur = (int) $stmt->fetchColumn();
$balance = max(0, $cur + $amount);
$pdo->prepare('UPDATE ' . tn('users') . ' SET points = ? WHERE id = ?')
->execute([$balance, $uid]);
$pdo->prepare(
'INSERT INTO ' . tn('points_log') . ' (user_id, type, amount, balance, remark, created_at) VALUES (?,?,?,?,?,NOW())'
)->execute([$uid, $type, $amount, $balance, $remark]);
$pdo->commit();
return ['ok' => true, 'balance' => $balance, 'error' => ''];
} catch (Throwable $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
return ['ok' => false, 'balance' => 0, 'error' => $e->getMessage()];
}
}
function genInviteCode()
{
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
for ($i = 0; $i < 20; $i++) {
$code = '';
for ($j = 0; $j < 8; $j++) {
$code .= $chars[random_int(0, strlen($chars) - 1)];
}
$stmt = db()->prepare('SELECT 1 FROM ' . tn('users') . ' WHERE invite_code = ?');
$stmt->execute([$code]);
if (!$stmt->fetch()) {
return $code;
}
}
return strtoupper(substr(md5(uniqid('', true)), 0, 8));
}
function getInviterIdByCode($code)
{
$code = trim((string) $code);
if ($code === '') {
return 0;
}
$stmt = db()->prepare('SELECT id FROM ' . tn('users') . ' WHERE invite_code = ?');
$stmt->execute([$code]);
$id = $stmt->fetchColumn();
return $id === false ? 0 : (int) $id;
}
function hasSignedToday($uid)
{
try {
$stmt = db()->prepare('SELECT 1 FROM ' . tn('sign_logs') . ' WHERE user_id = ? AND sign_date = CURDATE()');
$stmt->execute([(int) $uid]);
return (bool) $stmt->fetchColumn();
} catch (Throwable $e) {
return false;
}
}
function getSiteName() {
global $_site_name;
if ($_site_name !== null) {
return $_site_name;
}
try {
$db = db();
$stmt = $db->query("SELECT v FROM " . tn('settings') . " WHERE k = 'site_name'");
$val = $stmt->fetchColumn();
$_site_name = ($val !== false && $val !== null && $val !== '') ? $val : '自由云商城';
} catch (Exception $e) {
$_site_name = '自由云商城';
}
return $_site_name;
}
function defaultPurchaseAgreement()
{
return "1. 您须遵守《中华人民共和国网络安全法》及一切现行法律法规,不得利用所购资源从事任何违法违规活动。\n"
. "2. 严禁利用本商品提供的服务器、主机、网络等资源搭建违法违规网站或平台。\n"
. "3. 严禁利用本商品提供的资源搭建 VPN、代理、翻墙等未经许可的跨境网络访问服务。\n"
. "4. 严禁用于诈骗、赌博、侵权、传播不良信息及其他侵害他人权益的行为。\n"
. "5. 您须对使用行为承担全部法律责任;一经发现违规,平台有权立即关停相关服务并保留上报权利。";
}
function getProductAgreement($product)
{
$custom = $product['agreement'] ?? '';
$custom = is_string($custom) ? trim($custom) : '';
return $custom !== '' ? $custom : defaultPurchaseAgreement();
}
function recordVisit()
{
try {
$ip = getClientIp();
$uri = isset($_SERVER['REQUEST_URI']) ? substr($_SERVER['REQUEST_URI'], 0, 255) : '';
$method = isset($_SERVER['REQUEST_METHOD']) ? substr($_SERVER['REQUEST_METHOD'], 0, 8) : 'GET';
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 255) : '';
$ref = isset($_SERVER['HTTP_REFERER']) ? substr($_SERVER['HTTP_REFERER'], 0, 255) : '';
$uid = (int) ($_SESSION['user_id'] ?? 0);
db()->prepare(
'INSERT INTO ' . tn('visit_logs') . ' (ip, method, uri, user_id, ua, referer, created_at) VALUES (?,?,?,?,?,?,NOW())'
)->execute([$ip, $method, $uri, $uid, $ua, $ref]);
} catch (Throwable $e) {
}
}
function sendResetMail($userId)
{
$stmt = db()->prepare('SELECT id, username, nickname, email FROM ' . tn('users') . ' WHERE id = ?');
$stmt->execute([(int) $userId]);
$user = $stmt->fetch();
if (!$user || $user['email'] === '') {
return ['ok' => false, 'error' => '该账号没有可用邮箱'];
}
$token = bin2hex(random_bytes(24));
$expire = date('Y-m-d H:i:s', strtotime('+30 minutes'));
db()->prepare('UPDATE ' . tn('users') . ' SET reset_token = ?, reset_expire = ? WHERE id = ?')
->execute([$token, $expire, $user['id']]);
$url = siteBaseUrl() . '/reset_password.php?token=' . $token;
$name = $user['nickname'] ?: $user['username'];
$subject = '[' . SITE_NAME . '] 重置你的密码';
$body = '<div style="font-family:Segoe UI,Helvetica,Arial,sans-serif;max-width:480px;margin:0 auto;padding:24px;'
. 'border:1px solid
. '<h2 style="color:
. '<p style="color:
. '<p style="color:
. '<p style="margin:18px 0;"><a href="' . h($url) . '" style="background:
. 'border-radius:999px;text-decoration:none;display:inline-block;font-weight:600;">重置密码</a></p>'
. '<p style="color:
. '<hr style="border:none;border-top:1px solid
. '<p style="color:
. '</div>';
return fnwSendMail($user['email'], $subject, $body);
}
function getNotifyPrefs($uid)
{
$uid = (int) $uid;
try {
$stmt = db()->prepare('SELECT * FROM ' . tn('user_notify_prefs') . ' WHERE user_id = ?');
$stmt->execute([$uid]);
$row = $stmt->fetch();
if ($row) return $row;
} catch (Throwable $e) {
}
return [
'user_id' => $uid,
'email_enabled' => 1,
'order_shipped' => 1,
'expiry_reminder' => 1,
'promotions' => 0,
];
}
function saveNotifyPrefs($uid, array $data)
{
$uid = (int) $uid;
$bool = function ($v) { return !empty($v) ? 1 : 0; };
$email_enabled = $bool($data['email_enabled'] ?? 0);
$order_shipped = $bool($data['order_shipped'] ?? 0);
$expiry_reminder = $bool($data['expiry_reminder'] ?? 0);
$promotions = $bool($data['promotions'] ?? 0);
try {
db()->prepare(
'INSERT INTO ' . tn('user_notify_prefs') . ' (user_id, email_enabled, order_shipped, expiry_reminder, promotions, updated_at) '
. 'VALUES (?,?,?,?,?,NOW()) ON DUPLICATE KEY UPDATE email_enabled=?, order_shipped=?, expiry_reminder=?, promotions=?, updated_at=NOW()'
)->execute([$uid, $email_enabled, $order_shipped, $expiry_reminder, $promotions,
$email_enabled, $order_shipped, $expiry_reminder, $promotions]);
} catch (Throwable $e) {
}
}